Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f149ac4fa | |||
| c0dbffaa82 | |||
| 707f03a3eb | |||
| 8372462041 | |||
| 3fbbe2ccde | |||
| e7880fcde0 | |||
| ee5e76e4b1 | |||
| 1ae8d6bc46 | |||
| de7dd09f60 | |||
| 1649785826 | |||
| 91fc41cb15 | |||
| 9c80ad832b | |||
| 86b7162d99 | |||
| b8f8acd33d | |||
| d8905d23a4 | |||
| 5d2d124e5a | |||
| 4fda203def | |||
| 905bfc7a38 | |||
| 7c6973f0e6 | |||
| 969e39c541 | |||
| b256939cc4 | |||
| c46137644b | |||
| 1bd7ef56c4 | |||
| c258b99cec | |||
| d3abb32cb4 | |||
| 223ede9ff9 | |||
| 9d31a4eba1 | |||
| ea0e40c269 | |||
| bc24ea45a4 | |||
| 86d0c9b397 | |||
| c275ccf0da | |||
| 447a48f306 | |||
| 70b5cbc8db | |||
| 3459de0375 | |||
| c6817479bd | |||
| f02aae21f6 | |||
| 87a62a1791 | |||
| a0d1643021 | |||
| 856a946fa7 | |||
| 9d97164c20 | |||
| 6d526e7043 | |||
| 946072fc5a | |||
| 6b8dfdf02d | |||
| ba85fdf470 | |||
| 295f8601e3 | |||
| a38ccd8624 | |||
| b2dcec2f8a | |||
| e8355029d9 | |||
| 2b374b98d2 | |||
| d14d76a0c8 | |||
| d0f1c7065c | |||
| 3edb66d76d | |||
| bba8cdace1 | |||
| 22f82a67ee | |||
| b32c60ca73 | |||
| c93620cba5 | |||
| dd05ed4d4c | |||
| 21636defda | |||
| bb022646e3 | |||
| 37ad2c7ceb | |||
| b992631e00 | |||
| 35a5cc6717 | |||
| 310b56fe5d | |||
| 148234382e | |||
| c1a97b77b0 | |||
| 557ccb4854 | |||
| 83e7e8aab9 | |||
| 7bf490c59d | |||
| 9670babdbb | |||
| 42dfc2d5cf | |||
| 1527f0e275 | |||
| 66bd957ee0 | |||
| 72e3c5baa2 | |||
| 9318346ab3 | |||
| 1d6aceab0a | |||
| c1b5a9d270 | |||
| f0b2a12724 | |||
| c45677ed48 |
@@ -4,7 +4,7 @@ runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Setup PNPM
|
||||
uses: pnpm/action-setup@v2
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8
|
||||
- name: Setup NodeJS
|
||||
|
||||
@@ -9,3 +9,7 @@ updates:
|
||||
directory: '/' # Location of package manifests
|
||||
schedule:
|
||||
interval: 'monthly'
|
||||
- package-ecosystem: 'github-actions'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: 'monthly'
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
|
||||
@@ -5,13 +5,14 @@ on:
|
||||
jobs:
|
||||
validate:
|
||||
uses: './.github/workflows/validate.yml'
|
||||
secrets: inherit
|
||||
|
||||
publish:
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- validate
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: pnpm/action-setup@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8
|
||||
- uses: actions/setup-node@v4
|
||||
|
||||
@@ -10,25 +10,25 @@ jobs:
|
||||
formatting:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup
|
||||
- run: pnpm format:check
|
||||
lint:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup
|
||||
- run: pnpm lint
|
||||
type-check:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup
|
||||
- run: pnpm compile
|
||||
validate-demo:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup
|
||||
- run: pnpm tsc --noEmit
|
||||
working-directory: demo
|
||||
@@ -41,17 +41,17 @@ jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup
|
||||
- name: pnpm test:coverage
|
||||
run: pnpm test:coverage --reporter=default --reporter=hanging-process
|
||||
- uses: codecov/codecov-action@v3
|
||||
- uses: codecov/codecov-action@v4
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
windows-tests:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup
|
||||
- run: pnpm test run
|
||||
template:
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
- vanilla
|
||||
- vue
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup
|
||||
- run: pnpm pack
|
||||
- run: npm i
|
||||
|
||||
+334
-6
@@ -1,5 +1,312 @@
|
||||
# Changelog
|
||||
|
||||
## v0.17.2
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.1...v0.17.2)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Don't use sub-dependency binaries directly ([#482](https://github.com/wxt-dev/wxt/pull/482))
|
||||
|
||||
## v0.17.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.0...v0.17.1)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Content scripts not loading in dev mode ([3fbbe2c](https://github.com/wxt-dev/wxt/commit/3fbbe2c))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Lots of small typo fixes ([#480](https://github.com/wxt-dev/wxt/pull/480))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Leo Shklovskii ([@leos](https://github.com/leos))
|
||||
|
||||
## v0.17.0
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.11...v0.17.0)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- **storage:** ⚠️ Improved support for default values on storage items ([#477](https://github.com/wxt-dev/wxt/pull/477))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- **storage:** ⚠️ Only run migrations when the extension is updated ([#478](https://github.com/wxt-dev/wxt/pull/478))
|
||||
- Improve dev mode for content scripts registered at runtime ([#474](https://github.com/wxt-dev/wxt/pull/474))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- **storage:** Update docs ([91fc41c](https://github.com/wxt-dev/wxt/commit/91fc41c))
|
||||
|
||||
#### ⚠️ Breaking Changes
|
||||
|
||||
`v0.17.0` introduces several breaking changes to `wxt/storage`.
|
||||
|
||||
First, if you were using `defineItem` with versioning and no default value, you will need to add `defaultValue: null` to the options and update the first type parameter:
|
||||
|
||||
```ts
|
||||
// < 0.17
|
||||
const item = storage.defineItem<number>("local:count", {
|
||||
version: ...,
|
||||
migrations: ...,
|
||||
})
|
||||
|
||||
// >= 0.17
|
||||
const item = storage.defineItem<number | null>("local:count", {
|
||||
defaultValue: null,
|
||||
version: ...,
|
||||
migrations: ...,
|
||||
})
|
||||
```
|
||||
|
||||
The `defaultValue` property is now required if passing in the second options argument.
|
||||
|
||||
If you exclude the second options argument, it will default to being nullable, as before.
|
||||
|
||||
```ts
|
||||
const item: WxtStorageItem<number | null> = storage.defineItem<number>("local:count");
|
||||
const value: number | null = await item.getValue();
|
||||
```
|
||||
|
||||
> If you don't use typescript, there aren't any breaking changes, this is just a type change.
|
||||
|
||||
For storage items that are not nullable, the `watch` callback types has improved and will use the default value instead of `null` when the value is missing:
|
||||
|
||||
```ts
|
||||
// >=0.17
|
||||
const item = storage.defineItem<number>("local:count", { defaultValue: 0 });
|
||||
item.watch((newValue: number | null, oldValue: number | null) => {
|
||||
// ...
|
||||
});
|
||||
|
||||
// >=0.17
|
||||
const item = storage.defineItem<number>("local:count", { defaultValue: 0 });
|
||||
item.watch((newValue: number, oldValue: number) => {
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
You can also access the default value directly off the item:
|
||||
|
||||
```ts
|
||||
console.log(item.defaultValue); // 0
|
||||
```
|
||||
|
||||
The second breaking change is that migrations for versioned items only run when the extension is updated. Before, they were ran whenever the storage item was created, in any entrypoint (background, popup, content script, etc). Now, in v0.17, storage items will only run migrations when the `browser.runtime.onInstalled` event is fired with `reason = "update"` in the background. See the updated docs to make sure they run correctly: https://wxt.dev/guide/storage.html#running-migrations. TLDR: you need to import all storage items into the background entrypoint for the `onInstalled` hook to fire properly and thus run the migrations.
|
||||
|
||||
To keep the old behavior, call the new `migrate` function to run migrations as soon as an item is defined:
|
||||
|
||||
```ts
|
||||
const item = storage.defineItem(...);
|
||||
item.migrate();
|
||||
```
|
||||
|
||||
## v0.16.11
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.10...v0.16.11)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Output main JS file for HTML entrypoints to chunks directory ([#473](https://github.com/wxt-dev/wxt/pull/473))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **e2e:** Remove log ([4fda203](https://github.com/wxt-dev/wxt/commit/4fda203))
|
||||
|
||||
### 🤖 CI
|
||||
|
||||
- Fix codecov warning in release workflow ([7c6973f](https://github.com/wxt-dev/wxt/commit/7c6973f))
|
||||
- Upgrade `pnpm/action-setup` to v3 ([905bfc7](https://github.com/wxt-dev/wxt/commit/905bfc7))
|
||||
|
||||
## v0.16.10
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.9...v0.16.10)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Customize when content scripts are registered, in the manifest or at runtime ([#471](https://github.com/wxt-dev/wxt/pull/471))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Don't assume react when importing JSX entrypoints during build ([#470](https://github.com/wxt-dev/wxt/pull/470))
|
||||
- Respect `configFile` option ([#472](https://github.com/wxt-dev/wxt/pull/472))
|
||||
|
||||
## v0.16.9
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.8...v0.16.9)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Support setting side panel options in HTML file ([#468](https://github.com/wxt-dev/wxt/pull/468))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Fix order of ShadowRootUI hooks calling ([#459](https://github.com/wxt-dev/wxt/pull/459))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Add wrapper div to react's `createShadowRootUi` example ([bc24ea4](https://github.com/wxt-dev/wxt/commit/bc24ea4))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Simplify entrypoint types ([#464](https://github.com/wxt-dev/wxt/pull/464))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Okou ([@ookkoouu](https://github.com/ookkoouu))
|
||||
|
||||
## v0.16.8
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.7...v0.16.8)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Watch files outside project root during development ([#454](https://github.com/wxt-dev/wxt/pull/454))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Add loading and error states for "Who's using WXT" section ([447a48f](https://github.com/wxt-dev/wxt/commit/447a48f))
|
||||
|
||||
## v0.16.7
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.6...v0.16.7)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Generate ESLint globals file for auto-imports ([#450](https://github.com/wxt-dev/wxt/pull/450))
|
||||
|
||||
### 🔥 Performance
|
||||
|
||||
- Upgrade Vite to 5.1 ([#452](https://github.com/wxt-dev/wxt/pull/452))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Add section about dev mode differences ([a0d1643](https://github.com/wxt-dev/wxt/commit/a0d1643))
|
||||
- Remove anchor from content script ui examples ([87a62a1](https://github.com/wxt-dev/wxt/commit/87a62a1))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **e2e:** Use `wxt prepare` instead of `wxt build` when possible to speed up E2E tests ([#451](https://github.com/wxt-dev/wxt/pull/451))
|
||||
|
||||
## v0.16.6
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.5...v0.16.6)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add option to customize the analysis artifacts output ([#431](https://github.com/wxt-dev/wxt/pull/431))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Use `insertBefore` on mounting content script UI ([ba85fdf](https://github.com/wxt-dev/wxt/commit/ba85fdf))
|
||||
|
||||
### 💅 Refactors
|
||||
|
||||
- Use `Element.prepend` on mounting UI ([295f860](https://github.com/wxt-dev/wxt/commit/295f860))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Fix `createShadowRootUi` unmount calls ([946072f](https://github.com/wxt-dev/wxt/commit/946072f))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Enable skipped test since it works now ([6b8dfdf](https://github.com/wxt-dev/wxt/commit/6b8dfdf))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Lionelhorn ([@Lionelhorn](https://github.com/Lionelhorn))
|
||||
- Okou ([@ookkoouu](https://github.com/ookkoouu))
|
||||
|
||||
## v0.16.5
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.4...v0.16.5)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Support node 20 when running `wxt submit` ([e835502](https://github.com/wxt-dev/wxt/commit/e835502))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Remove "coming soon" from automated publishing feature ([2b374b9](https://github.com/wxt-dev/wxt/commit/2b374b9))
|
||||
|
||||
## v0.16.4
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.3...v0.16.4)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Automatically convert MV3 `web_accessible_resources` to MV2 ([#423](https://github.com/wxt-dev/wxt/pull/423))
|
||||
- Add option to customize the analysis output filename ([#426](https://github.com/wxt-dev/wxt/pull/426))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Don't use immer for `transformManifest` ([#424](https://github.com/wxt-dev/wxt/pull/424))
|
||||
- Exclude analysis files from the build summary ([#425](https://github.com/wxt-dev/wxt/pull/425))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Fix fake path in test data generator ([d0f1c70](https://github.com/wxt-dev/wxt/commit/d0f1c70))
|
||||
|
||||
## v0.16.3
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.2...v0.16.3)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Hooks ([#419](https://github.com/wxt-dev/wxt/pull/419))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- **init:** Use `ungh` to prevent rate limits when loading templates ([37ad2c7](https://github.com/wxt-dev/wxt/commit/37ad2c7))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Fix typo of intuitive ([#415](https://github.com/wxt-dev/wxt/pull/415))
|
||||
- Fix typo of opinionated ([#416](https://github.com/wxt-dev/wxt/pull/416))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Add dependabot for github actions ([#404](https://github.com/wxt-dev/wxt/pull/404))
|
||||
- **deps-dev:** Bump happy-dom from 12.10.3 to 13.3.8 ([#411](https://github.com/wxt-dev/wxt/pull/411))
|
||||
- **deps-dev:** Bump typescript from 5.3.2 to 5.3.3 ([#409](https://github.com/wxt-dev/wxt/pull/409))
|
||||
- Register global `wxt` instance ([#418](https://github.com/wxt-dev/wxt/pull/418))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Chen Hua ([@hcljsq](https://github.com/hcljsq))
|
||||
- Florian Metz ([@Timeraa](http://github.com/Timeraa))
|
||||
|
||||
## v0.16.2
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.1...v0.16.2)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Don't crash background service worker when using `import.meta.url` ([#402](https://github.com/wxt-dev/wxt/pull/402))
|
||||
|
||||
## v0.16.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.0...v0.16.1)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Don't require config to run `wxt submit init` ([9318346](https://github.com/wxt-dev/wxt/commit/9318346))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Add premid extension to homepage ([#399](https://github.com/wxt-dev/wxt/pull/399))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **templates:** Upgrade to wxt `^0.16.0` ([f0b2a12](https://github.com/wxt-dev/wxt/commit/f0b2a12))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Florian Metz ([@Timeraa](http://github.com/Timeraa))
|
||||
|
||||
## v0.16.0
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.15.4...v0.16.0)
|
||||
@@ -20,7 +327,28 @@
|
||||
|
||||
#### ⚠️ Breaking Changes
|
||||
|
||||
- ⚠️ ESM background support ([#398](https://github.com/wxt-dev/wxt/pull/398))
|
||||
In [#398](https://github.com/wxt-dev/wxt/pull/398), HTML pages' JS entrypoints in the output directory have been moved. Unless you're doing some kind of post-build work referencing files, you don't have to make any changes.
|
||||
|
||||
- Before:
|
||||
```
|
||||
.output/
|
||||
<target>/
|
||||
chunks/
|
||||
some-shared-chunk-<hash>.js
|
||||
popup-<hash>.js
|
||||
popup.html
|
||||
```
|
||||
- After:
|
||||
```
|
||||
.output/
|
||||
<target>/
|
||||
chunks/
|
||||
some-shared-chunk-<hash>.js
|
||||
popup.html
|
||||
popup.js
|
||||
```
|
||||
|
||||
This effects all HTML files, not just the Popup. The hash has been removed, and it's been moved to the root of the build target folder, not inside the `chunks/` directory. Moving files like this has not historically increased review times or triggered in-depth reviews when submitting updates to the stores.
|
||||
|
||||
## v0.15.4
|
||||
|
||||
@@ -610,7 +938,7 @@ Renamed undocumented constants for detecting the build config at runtime in [#38
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Remove whitespace from genearted `.wxt` files ([#211](https://github.com/wxt-dev/wxt/pull/211))
|
||||
- Remove whitespace from generated `.wxt` files ([#211](https://github.com/wxt-dev/wxt/pull/211))
|
||||
- Upgrade templates to `wxt@^0.9.0` ([#214](https://github.com/wxt-dev/wxt/pull/214))
|
||||
- Update Vite dependency range to `^4.0.0 || ^5.0.0-0` ([f1e8084](https://github.com/wxt-dev/wxt/commit/f1e8084be89e512dde441b9197a99183c497f67d))
|
||||
|
||||
@@ -1118,8 +1446,8 @@ Renamed undocumented constants for detecting the build config at runtime in [#38
|
||||
- **deps-dev:** Bump vitest from 0.34.1 to 0.34.3 ([#99](https://github.com/wxt-dev/wxt/pull/99))
|
||||
- Increase E2E test timeout because GitHub Actions Window runner is slow ([2a0842b](https://github.com/wxt-dev/wxt/commit/2a0842b))
|
||||
- **deps-dev:** Bump vitepress from 1.0.0-rc.4 to 1.0.0-rc.10 ([#96](https://github.com/wxt-dev/wxt/pull/96))
|
||||
- Fix test watcher restarting indefinetly ([2c7922c](https://github.com/wxt-dev/wxt/commit/2c7922c))
|
||||
- Remove explict icon config from templates ([93bfee0](https://github.com/wxt-dev/wxt/commit/93bfee0))
|
||||
- Fix test watcher restarting indefinitely ([2c7922c](https://github.com/wxt-dev/wxt/commit/2c7922c))
|
||||
- Remove explicit icon config from templates ([93bfee0](https://github.com/wxt-dev/wxt/commit/93bfee0))
|
||||
- Use import aliases in Vue template ([#104](https://github.com/wxt-dev/wxt/pull/104))
|
||||
|
||||
#### ⚠️ Breaking Changes
|
||||
@@ -1179,7 +1507,7 @@ Renamed undocumented constants for detecting the build config at runtime in [#38
|
||||
- Branding and logo ([#60](https://github.com/wxt-dev/wxt/pull/60))
|
||||
- Simplify binary setup ([#62](https://github.com/wxt-dev/wxt/pull/62))
|
||||
- Add Solid template ([#63](https://github.com/wxt-dev/wxt/pull/63))
|
||||
- Increate E2E test timeout to fix flakey test ([dfe424f](https://github.com/wxt-dev/wxt/commit/dfe424f))
|
||||
- Increase E2E test timeout to fix flakey test ([dfe424f](https://github.com/wxt-dev/wxt/commit/dfe424f))
|
||||
|
||||
### 🤖 CI
|
||||
|
||||
@@ -1455,7 +1783,7 @@ Initial release of WXT. Full support for production builds and initial toolkit f
|
||||
- Export and bootstrap the `/client` package ([5b07c95](https://github.com/wxt-dev/wxt/commit/5b07c95))
|
||||
- Resolve entrypoints based on filesystem ([a63f061](https://github.com/wxt-dev/wxt/commit/a63f061))
|
||||
- Separate output directories for each browser/manifest version ([f09ffbb](https://github.com/wxt-dev/wxt/commit/f09ffbb))
|
||||
- Build entrypoints and output `manfiest.json` ([1e7c738](https://github.com/wxt-dev/wxt/commit/1e7c738))
|
||||
- Build entrypoints and output `manifest.json` ([1e7c738](https://github.com/wxt-dev/wxt/commit/1e7c738))
|
||||
- Automatically add CSS files to content scripts ([047ce04](https://github.com/wxt-dev/wxt/commit/047ce04))
|
||||
- Download and bundle remote URL imports ([523c7df](https://github.com/wxt-dev/wxt/commit/523c7df))
|
||||
- Generate type declarations and config for project types and auto-imports ([21debad](https://github.com/wxt-dev/wxt/commit/21debad))
|
||||
|
||||
+1
-1
@@ -108,7 +108,7 @@ Then run `npm i` again.
|
||||
To add a template, copy the vanilla template and give it a new name.
|
||||
|
||||
```sh
|
||||
cp -r templates/vailla templates/<new-template-name>
|
||||
cp -r templates/vanilla templates/<new-template-name>
|
||||
```
|
||||
|
||||
That's it. Once your template is merged, it will be available inside `wxt init` immediately. You don't need to release a new version of WXT to release a new template.
|
||||
|
||||
@@ -55,14 +55,11 @@ Or see the [installation guide](https://wxt.dev/guide/installation.html) to get
|
||||
- 📂 File based entrypoints
|
||||
- 🚔 TypeScript
|
||||
- 🦾 Auto-imports
|
||||
- ⬇️ Download and bundle remote URL imports
|
||||
- 🤖 Automated publishing
|
||||
- 🎨 Frontend framework agnostic: works with Vue, React, Svelte, etc
|
||||
- 🖍️ Quickly bootstrap a new project
|
||||
- 📏 Bundle analysis
|
||||
|
||||
### Coming Soon
|
||||
|
||||
- 🤖 Automated publishing
|
||||
- ⬇️ Download and bundle remote URL imports
|
||||
|
||||
## Contributors
|
||||
|
||||
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* A alias around `publish-extension` that is always installed on the path without having to install
|
||||
* `publish-browser-extension` as a direct dependency (like for PNPM, which doesn't link
|
||||
* sub-dependency binaries to "node_modules/.bin")
|
||||
*/
|
||||
require('publish-browser-extension/cli');
|
||||
+1
-1
@@ -19,7 +19,7 @@
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"vitest": "^1.1.0"
|
||||
"vitest": "^1.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.34",
|
||||
|
||||
@@ -7,6 +7,7 @@ export default defineBackground({
|
||||
console.log(browser.runtime.id);
|
||||
logId();
|
||||
console.log({
|
||||
url: import.meta.url,
|
||||
browser: import.meta.env.BROWSER,
|
||||
chrome: import.meta.env.CHROME,
|
||||
firefox: import.meta.env.FIREFOX,
|
||||
|
||||
@@ -17,13 +17,15 @@ const chromeExtensionIds = [
|
||||
'okifoaikfmpfcamplcfjkpdnhfodpkil', // Doozy: Ai Made Easy
|
||||
'lknmjhcajhfbbglglccadlfdjbaiifig', // tl;dv - Record, Transcribe & ChatGPT for Google Meet
|
||||
'youtube中文配音/oglffgiaiekgeicdgkdlnlkhliajdlja', // Youtube中文配音
|
||||
'agjnjboanicjcpenljmaaigopkgdnihi', // PreMiD
|
||||
];
|
||||
|
||||
const { data } = useListExtensionDetails(chromeExtensionIds);
|
||||
const { data, err, isLoading } = useListExtensionDetails(chromeExtensionIds);
|
||||
const sortedExtensions = computed(() => {
|
||||
if (!data.value?.length) return [];
|
||||
|
||||
return [...data.value]
|
||||
.filter((item) => item != null)
|
||||
.map((item) => ({
|
||||
...item,
|
||||
// Sort based on the user count weighted by the rating
|
||||
@@ -48,7 +50,16 @@ function getStoreUrl(extension: ChromeExtension) {
|
||||
Battle tested and ready for production. Explore chrome extensions made
|
||||
with WXT.
|
||||
</p>
|
||||
<ul>
|
||||
<p v-if="isLoading" style="text-align: center; opacity: 50%">
|
||||
Loading...
|
||||
</p>
|
||||
<p
|
||||
v-else-if="err || sortedExtensions.length === 0"
|
||||
style="text-align: center; opacity: 50%"
|
||||
>
|
||||
Failed to load extension details.
|
||||
</p>
|
||||
<ul v-else>
|
||||
<li
|
||||
v-for="extension of sortedExtensions"
|
||||
:key="extension.id"
|
||||
|
||||
@@ -26,6 +26,7 @@ const query = `query ${operationName}($ids:[String!]!) {
|
||||
export default function (ids: string[]) {
|
||||
const data = ref<ChromeExtension[]>();
|
||||
const err = ref<unknown>();
|
||||
const isLoading = ref(true);
|
||||
|
||||
fetch('https://queue.wxt.dev/api', {
|
||||
method: 'POST',
|
||||
@@ -36,6 +37,7 @@ export default function (ids: string[]) {
|
||||
}),
|
||||
})
|
||||
.then(async (res) => {
|
||||
isLoading.value = false;
|
||||
const {
|
||||
data: { chromeExtensions },
|
||||
} = await res.json();
|
||||
@@ -43,6 +45,7 @@ export default function (ids: string[]) {
|
||||
err.value = undefined;
|
||||
})
|
||||
.catch((error) => {
|
||||
isLoading.value = false;
|
||||
console.error(error);
|
||||
data.value = undefined;
|
||||
err.value = error;
|
||||
@@ -51,5 +54,6 @@ export default function (ids: string[]) {
|
||||
return {
|
||||
data,
|
||||
err,
|
||||
isLoading,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ For MV2, the background is added as a script to the background page. For MV3, th
|
||||
## Definition
|
||||
|
||||
:::warning
|
||||
The main function of the background **_CANNOT BE ASYNC_**. Event listeners must be added syncronously on background startup. If your main function returns a promise, WXT will log an error.
|
||||
The main function of the background **_CANNOT BE ASYNC_**. Event listeners must be added synchronously on background startup. If your main function returns a promise, WXT will log an error.
|
||||
:::
|
||||
|
||||
```ts
|
||||
|
||||
@@ -37,6 +37,9 @@ export default defineContentScript({
|
||||
// Configure how CSS is injected onto the page
|
||||
cssInjectionMode: undefined | "manifest" | "manual" | "ui",
|
||||
|
||||
// Configure how/when content script will be registered
|
||||
registration: undefined | "manifest" | "runtime",
|
||||
|
||||
main(ctx) {
|
||||
// Executed when content script is loaded
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
WXT can build CSS entrypoints individually. CSS entrypoints are always unlisted.
|
||||
|
||||
See [Content Script CSS](/entrypoints/content-scripts#css) documentation for the recomended approach to include CSS with a content script.
|
||||
See [Content Script CSS](/entrypoints/content-scripts#css) documentation for the recommended approach to include CSS with a content script.
|
||||
|
||||
:::info
|
||||
If the recommended approach doesn't work for your use case, you can use any of the filename patterns below to build the styles separate from the JS and use the [`transformManifest` hook](/api/wxt/interfaces/InlineConfig#transformmanifest) to manually add your CSS file to the manifest.
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/reference/sidePanel/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/user_interface/Sidebars)
|
||||
|
||||
In Chrome, side panels use the "side_panel" API, while Firefox uses the "sidebar_action" API.
|
||||
|
||||
:::warning
|
||||
Chrome added support for sidepanels in Manifest V3, they are not available in Manfiest V2.
|
||||
Chrome added support for sidepanels in Manifest V3, they are not available in Manifest V2.
|
||||
:::
|
||||
|
||||
## Filenames
|
||||
@@ -25,7 +27,17 @@ Chrome added support for sidepanels in Manifest V3, they are not available in Ma
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Title</title>
|
||||
<title>Default Side Panel Title</title>
|
||||
<meta
|
||||
name="manifest.default_icon"
|
||||
content="{
|
||||
16: '/icon-16.png',
|
||||
24: '/icon-24.png',
|
||||
...
|
||||
}"
|
||||
/>
|
||||
<meta name="manifest.open_at_install" content="true|false" />
|
||||
<meta name="manifest.browser_style" content="true|false" />
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
|
||||
@@ -65,7 +65,7 @@ import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
imports: {
|
||||
// Add auto-imports for vue fuctions like createApp, ref, computed, watch, toRaw, etc...
|
||||
// Add auto-imports for vue functions like createApp, ref, computed, watch, toRaw, etc...
|
||||
presets: ['vue'],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -40,7 +40,7 @@ WXT's main goal is improving the development experience (DX) of creating web ext
|
||||
|
||||
Opening a browser automatically makes it super easy to start and stop development without having to manually load the extension in your browser.
|
||||
|
||||
Reloading each part of the extension individually improves your iteration speed while developing UIs. This is because reloading the entire extension on every change will close the popup and any tabs open to an extension page, like options. If you save a file associated with a UI and a content script while working on the UI, it will randomly close because it needed to reload the extension when the content script changed. This interupts your development flow and is really annoying.
|
||||
Reloading each part of the extension individually improves your iteration speed while developing UIs. This is because reloading the entire extension on every change will close the popup and any tabs open to an extension page, like options. If you save a file associated with a UI and a content script while working on the UI, it will randomly close because it needed to reload the extension when the content script changed. This interrupts your development flow and is really annoying.
|
||||
|
||||
WXT solves this problem by reloading HTML pages and content scripts individually (when possible) to keep your UIs open while you develop them. This is a MV3 feature, so if you're developing a MV2 extension, you'll get the same dev experience as Plasmo.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Configuration
|
||||
|
||||
WXT's behavior can be configured via the `wxt.config.ts` file. In this file, you can add Vite plugins, change the directory strucutre of your project, and set fields on your `manifest.json`.
|
||||
WXT's behavior can be configured via the `wxt.config.ts` file. In this file, you can add Vite plugins, change the directory structure of your project, and set fields on your `manifest.json`.
|
||||
|
||||
## Config File
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ export default defineContentScript({
|
||||
main(ctx) {
|
||||
const ui = createIntegratedUi(ctx, {
|
||||
position: 'inline',
|
||||
anchor: '#anchor',
|
||||
onMount: (container) => {
|
||||
// Append children to the container
|
||||
const app = document.createElement('p');
|
||||
@@ -52,7 +51,6 @@ export default defineContentScript({
|
||||
main(ctx) {
|
||||
const ui = createIntegratedUi(ctx, {
|
||||
position: 'inline',
|
||||
anchor: '#anchor',
|
||||
onMount: (container) => {
|
||||
// Create the app and mount it to the UI container
|
||||
const app = createApp(App);
|
||||
@@ -82,7 +80,6 @@ export default defineContentScript({
|
||||
main(ctx) {
|
||||
const ui = createIntegratedUi(ctx, {
|
||||
position: 'inline',
|
||||
anchor: '#anchor',
|
||||
onMount: (container) => {
|
||||
// Create a root on the UI container and render a component
|
||||
const root = ReactDOM.createRoot(container);
|
||||
@@ -111,7 +108,6 @@ export default defineContentScript({
|
||||
main(ctx) {
|
||||
const ui = createIntegratedUi(ctx, {
|
||||
position: 'inline',
|
||||
anchor: '#anchor',
|
||||
onMount: (container) => {
|
||||
// Create the Svelte app inside the UI container
|
||||
const app = new App({
|
||||
@@ -141,7 +137,6 @@ export default defineContentScript({
|
||||
main(ctx) {
|
||||
const ui = createIntegratedUi(ctx, {
|
||||
position: 'inline',
|
||||
anchor: '#anchor',
|
||||
onMount: (container) => {
|
||||
// Render your app to the UI container
|
||||
const unmount = render(() => <div>...</div>, container);
|
||||
@@ -162,7 +157,7 @@ export default defineContentScript({
|
||||
|
||||
See the [API Reference](/api/wxt/client/functions/createIntegratedUi) for the complete list of options.
|
||||
|
||||
You can control how CSS is injected for an integrated content script UI with the [`cssInjectionMode`](/api/wxt/interfaces/ContentScriptBaseDefinition#cssinjectionmode) property. Usually, you'll want to leave it as `"manifest"`, the default, so the UI inherits its style from the website's CSS.
|
||||
You can control how CSS is injected for an integrated content script UI with the [`cssInjectionMode`](/api/wxt/interfaces/BaseContentScriptEntrypointOptions#cssinjectionmode) property. Usually, you'll want to leave it as `"manifest"`, the default, so the UI inherits its style from the website's CSS.
|
||||
|
||||
## Shadow Root
|
||||
|
||||
@@ -173,7 +168,7 @@ WXT's [`createShadowRootUi`](/api/wxt/client/functions/createShadowRootUi) abstr
|
||||
To use `createShadowRootUi`, follow these steps:
|
||||
|
||||
1. Import your CSS file at the top of your content script
|
||||
2. Set [`cssInjectionMode: "ui"`](/api/wxt/interfaces/ContentScriptBaseDefinition#cssinjectionmode) inside `defineContentScript`
|
||||
2. Set [`cssInjectionMode: "ui"`](/api/wxt/interfaces/BaseContentScriptEntrypointOptions#cssinjectionmode) inside `defineContentScript`
|
||||
3. Define your UI with `createShadowRootUi()`
|
||||
4. Mount the UI so it is visible to users
|
||||
|
||||
@@ -192,7 +187,6 @@ export default defineContentScript({
|
||||
// 3. Define your UI
|
||||
const ui = await createShadowRootUi(ctx, {
|
||||
name: 'example-ui',
|
||||
anchor: '#anchor',
|
||||
position: 'inline',
|
||||
onMount(container) {
|
||||
// Define how your UI will be mounted inside the container
|
||||
@@ -223,7 +217,6 @@ export default defineContentScript({
|
||||
// 3. Define your UI
|
||||
const ui = await createShadowRootUi(ctx, {
|
||||
name: 'example-ui',
|
||||
anchor: '#anchor',
|
||||
position: 'inline',
|
||||
onMount: (container) => {
|
||||
// Define how your UI will be mounted inside the container
|
||||
@@ -233,7 +226,7 @@ export default defineContentScript({
|
||||
},
|
||||
onRemove: (app) => {
|
||||
// Unmount the app when the UI is removed
|
||||
app.unmount();
|
||||
app?.unmount();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -259,16 +252,19 @@ export default defineContentScript({
|
||||
const ui = await createShadowRootUi(ctx, {
|
||||
name: 'example-ui',
|
||||
position: 'inline',
|
||||
anchor: '#anchor',
|
||||
onMount: (container) => {
|
||||
// Container is a body, and React warns when creating a root on the body, so create a wrapper div
|
||||
const app = document.createElement('div');
|
||||
container.append(app);
|
||||
|
||||
// Create a root on the UI container and render a component
|
||||
const root = ReactDOM.createRoot(container);
|
||||
const root = ReactDOM.createRoot(app);
|
||||
root.render(<App />);
|
||||
return root;
|
||||
},
|
||||
onRemove: (root) => {
|
||||
// Unmount the root when the UI is removed
|
||||
root.unmount();
|
||||
root?.unmount();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -293,7 +289,6 @@ export default defineContentScript({
|
||||
const ui = await createShadowRootUi(ctx, {
|
||||
name: 'example-ui',
|
||||
position: 'inline',
|
||||
anchor: '#anchor',
|
||||
onMount: (container) => {
|
||||
// Create the Svelte app inside the UI container
|
||||
const app = new App({
|
||||
@@ -303,7 +298,7 @@ export default defineContentScript({
|
||||
},
|
||||
onRemove: (app) => {
|
||||
// Destroy the app when the UI is removed
|
||||
app.$destroy();
|
||||
app?.$destroy();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -328,14 +323,13 @@ export default defineContentScript({
|
||||
const ui = await createShadowRootUi(ctx, {
|
||||
name: 'example-ui',
|
||||
position: 'inline',
|
||||
anchor: '#anchor',
|
||||
onMount: (container) => {
|
||||
// Render your app to the UI container
|
||||
const unmount = render(() => <div>...</div>, container);
|
||||
},
|
||||
onRemove: (unmount) => {
|
||||
// Unmount the app when the UI is removed
|
||||
unmount();
|
||||
unmount?.();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -374,7 +368,7 @@ WXT provides a helper function, [`createIframeUi`](/api/wxt/client/functions/cre
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
1. Add the page to the manifest's `web_accessible_resouces`
|
||||
1. Add the page to the manifest's `web_accessible_resources`
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
@@ -398,7 +392,6 @@ WXT provides a helper function, [`createIframeUi`](/api/wxt/client/functions/cre
|
||||
// Define the UI
|
||||
const ui = await createIframeUi(ctx, {
|
||||
page: '/example-iframe.html',
|
||||
anchor: '#anchor',
|
||||
position: 'inline',
|
||||
onMount: (wrapper, iframe) => {
|
||||
// Add styles to the iframe like width
|
||||
|
||||
@@ -13,6 +13,24 @@ WXT's main goal is providing the best DX it possibly can. When running your exte
|
||||
| `wxt.config.ts` | | | | 🟡 See [#10](https://github.com/wxt-dev/wxt/issues/10) |
|
||||
| `web-ext.config.ts` | | | | 🟡 See [#10](https://github.com/wxt-dev/wxt/issues/10) |
|
||||
|
||||
## Dev Mode vs Production Builds
|
||||
|
||||
There are some notable differences between the development and production versions of an extension. During development:
|
||||
|
||||
1. **Content scripts are not listed in the `manifest.json`** when targeting MV3. Instead, the [`scripting`](https://developer.chrome.com/docs/extensions/reference/api/scripting) permission is used to register content scripts at runtime so they can be reloaded individually.
|
||||
|
||||
To get the list of content scripts during development, run the following in the background's console:
|
||||
|
||||
```ts
|
||||
await chrome.scripting.getRegisteredContentScripts();
|
||||
```
|
||||
|
||||
2. **The CSP is modified to allow loading scripts from the dev server**. Make sure you're using Chrome v110 or above for HMR to work.
|
||||
|
||||
3. If you don't include a background script/service worker, one will be created to perform various tasks in dev mode, mostly related to reloading different parts of the extension on change.
|
||||
|
||||
For production builds, none of the above modifications will be applied, and you're extension/manifest will only include what you have defined.
|
||||
|
||||
## Configure Browser Startup
|
||||
|
||||
WXT uses [`web-ext` by Mozilla](https://github.com/mozilla/web-ext) to automatically open a browser with the extension installed. You can configure the runner's behavior via the [`runner`](/api/wxt/interfaces/InlineConfig#runner) option, or in a separate gitignored file, `web-ext.config.ts`.
|
||||
@@ -41,7 +59,7 @@ export default defineRunnerConfig({
|
||||
|
||||
### Browser Binaries
|
||||
|
||||
`web-ext`'s browser discovery is very limitted. By default, it only guesses at where Chrome and Firefox are installed. If you've customized your install locations, you may need to tell `web-ext` where the binaries/executables are located using the [`binaries` option](/api/wxt/interfaces/ExtensionRunnerConfig#binaries). For other Chromium based browsers, like Edge or Opera, you'll need to explicitly list them in the `binaries` option as well, otherwise they will open in Chrome by default.
|
||||
`web-ext`'s browser discovery is very limited. By default, it only guesses at where Chrome and Firefox are installed. If you've customized your install locations, you may need to tell `web-ext` where the binaries/executables are located using the [`binaries` option](/api/wxt/interfaces/ExtensionRunnerConfig#binaries). For other Chromium based browsers, like Edge or Opera, you'll need to explicitly list them in the `binaries` option as well, otherwise they will open in Chrome by default.
|
||||
|
||||
```ts
|
||||
// ~/web-ext.config.ts
|
||||
@@ -83,6 +101,6 @@ Normally, to manually reload an extension, you have to visit `chrome://extension
|
||||
|
||||
When running `wxt` command to start the dev server, WXT adds a keyboard shortcut, `ctrl+E` for Windows/Linux and `cmd+E` for Mac, that reloads the extension when pressed, without visiting `chrome://extensions`.
|
||||
|
||||
:::note
|
||||
:::info
|
||||
This shortcut is only available during development, and is not be added to your extension when running `wxt build` or `wxt-zip`.
|
||||
:::
|
||||
|
||||
@@ -58,7 +58,7 @@ See [`/entrypoints` folder](/entrypoints/background) documentation for a full li
|
||||
|
||||
## Entrypoint Options
|
||||
|
||||
Some entrypoints, like content scripts, actions, or the background, can recieve additional options.
|
||||
Some entrypoints, like content scripts, actions, or the background, can receive additional options.
|
||||
|
||||
In HTML files, options are listed as `meta` tags:
|
||||
|
||||
@@ -118,7 +118,7 @@ This throws an error because WXT needs to import each entrypoint during the buil
|
||||
|
||||
:::details Why?
|
||||
|
||||
When importing your entrypoint to get its definition, the file is imported in a **_node environement_**, and doesn't have access to the `window`, `chrome`, or `browser` globals a web extension ususally has access to. If WXT doesn't remove all the imports from the file, the imported modules could try and access one of these variables, throwing an error.
|
||||
When importing your entrypoint to get its definition, the file is imported in a **_node environment_**, and doesn't have access to the `window`, `chrome`, or `browser` globals a web extension usually has access to. If WXT doesn't remove all the imports from the file, the imported modules could try and access one of these variables, throwing an error.
|
||||
|
||||
:::
|
||||
|
||||
@@ -126,7 +126,7 @@ When importing your entrypoint to get its definition, the file is imported in a
|
||||
See [`wxt-dev/wxt#336`](https://github.com/wxt-dev/wxt/issues/336) to track the status of this bug.
|
||||
:::
|
||||
|
||||
Usually, this error occurs when you try to extract options into a shared file or try to run code outside the `main` function. To fix the example from above, use litteral values when defining an entrypoint instead of importing them:
|
||||
Usually, this error occurs when you try to extract options into a shared file or try to run code outside the `main` function. To fix the example from above, use literal values when defining an entrypoint instead of importing them:
|
||||
|
||||
```ts
|
||||
import { GOOGLE_MATCHES } from '~/utils/match-patterns'; // [!code --]
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ export default defineBackground({
|
||||
```
|
||||
|
||||
:::warning
|
||||
Only MV3 support ESM background scripts/service workers. When targetting MV2, the `type` option is ignored and the background is always bundled into a single file as IIFE.
|
||||
Only MV3 support ESM background scripts/service workers. When targeting MV2, the `type` option is ignored and the background is always bundled into a single file as IIFE.
|
||||
:::
|
||||
|
||||
## Content Scripts
|
||||
|
||||
@@ -133,7 +133,7 @@ The dev command will build the extension for development, open the browser, and
|
||||
When running the dev command, WXT will make several changes to your `manifest.json` to improve your development experience:
|
||||
|
||||
- If missing, add a background script/service worker to enable fast reloads
|
||||
- Add serveral `permissions` and `host_permissions` to enable HMR and fast reloads
|
||||
- Add several `permissions` and `host_permissions` to enable HMR and fast reloads
|
||||
- Modify the CSP to allow connections with the dev server
|
||||
- Remove `content_scripts` and register them at runtime so they can be easily reloaded when you save a file
|
||||
|
||||
@@ -147,4 +147,4 @@ You're ready to build your web extension!
|
||||
- Learn how to [add entrypoints](./entrypoints) like the popup, options page, or content scripts
|
||||
- Configure your entrypoints to [use ESM](./esm) at runtime
|
||||
- [Configure WXT](./configuration) by creating a `wxt.config.ts` file
|
||||
- Checkout [example projects](https://github.com/wxt-dev/wxt-examples) to see how to perfom common tasks with WXT
|
||||
- Checkout [example projects](https://github.com/wxt-dev/wxt-examples) to see how to perform common tasks with WXT
|
||||
|
||||
@@ -7,7 +7,7 @@ head:
|
||||
|
||||
# Introduction
|
||||
|
||||
WXT is a free and open source framework for building web extensions in an conventional, intuative, and safe way **_for all browsers_**.
|
||||
WXT is a free and open source framework for building web extensions in an conventional, intuitive, and safe way **_for all browsers_**.
|
||||
|
||||
WXT is based of [Nuxt](https://nuxt.com), and aims to provide the same great DX with TypeScript, auto-imports, and an opinionated project structure.
|
||||
|
||||
@@ -15,12 +15,12 @@ WXT is based of [Nuxt](https://nuxt.com), and aims to provide the same great DX
|
||||
|
||||
## Conventions
|
||||
|
||||
WXT is an optionated framework. This helps keep projects consistent and easy to pick up.
|
||||
WXT is an opinionated framework. This helps keep projects consistent and easy to pick up.
|
||||
|
||||
- **Generated manifest**: Based on your project's file structure
|
||||
- **Entrypoint configuration**: Configure entrypoints from the same file they're declare in
|
||||
- **Type-safety is a priority**: Out-of-the-box TypeScript support with improved browser API typing
|
||||
- **Simple output file structure**: Ouptut file paths minimize the path at runtime
|
||||
- **Simple output file structure**: Output file paths minimize the path at runtime
|
||||
|
||||
## Development
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ Every project is different, so there's no one-solution-fits-all to migrating you
|
||||
|
||||
## Popular Tools/Frameworks
|
||||
|
||||
Here's specific steps for other popuplar frameworks/build tools.
|
||||
Here's specific steps for other popular frameworks/build tools.
|
||||
|
||||
### `vite-plugin-web-extension`
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Multiple Browsers
|
||||
|
||||
You can build an extension for any combination of browser and manifest verison. Different browsers and manifest versions support different APIs and entrypoints, so be sure to check that your extension functions as expected for each target.
|
||||
You can build an extension for any combination of browser and manifest version. Different browsers and manifest versions support different APIs and entrypoints, so be sure to check that your extension functions as expected for each target.
|
||||
|
||||
Separate build targets are written to their own output directories:
|
||||
|
||||
@@ -45,7 +45,7 @@ wxt --mv2
|
||||
wxt build --mv2
|
||||
```
|
||||
|
||||
When the `-b --browser` flag is not passed, it defaults to `chrome`. So here, we're targetting MV2 for Chrome.
|
||||
When the `-b --browser` flag is not passed, it defaults to `chrome`. So here, we're targeting MV2 for Chrome.
|
||||
|
||||
## Customizing Entrypoints
|
||||
|
||||
@@ -116,7 +116,7 @@ Only `defineBackground` and `defineContentScript` support per-browser options ri
|
||||
To determine the browser or manifest version at runtime, you can use any of the below variables:
|
||||
|
||||
- `import.meta.env.BROWSER`: A string, the target browser, usually equal to the `--browser` flag
|
||||
- `import.meta.env.MANIFEST_VERSION`: A number, either `2` or `3`, depending on the manifest version targetted
|
||||
- `import.meta.env.MANIFEST_VERSION`: A number, either `2` or `3`, depending on the manifest version targeted
|
||||
- `import.meta.env.CHROME`: A boolean equivalent to `import.meta.env.BROWSER === "chrome"`
|
||||
- `import.meta.env.FIREFOX`: A boolean equivalent to `import.meta.env.BROWSER === "firefox"`
|
||||
- `import.meta.env.EDGE`: A boolean equivalent to `import.meta.env.BROWSER === "edge"`
|
||||
|
||||
@@ -64,7 +64,7 @@ See the [Firefox Addon Store](#firefox-addon-store) section for more details abo
|
||||
|
||||
## GitHub Action
|
||||
|
||||
Here's an example of a GitHub Action to automate submiting new versions of your extension for review. Ensure that you've added all required secrets used in the workflow to the repo's settings.
|
||||
Here's an example of a GitHub Action to automate submitting new versions of your extension for review. Ensure that you've added all required secrets used in the workflow to the repo's settings.
|
||||
|
||||
```yml
|
||||
# TODO
|
||||
|
||||
+37
-2
@@ -1,3 +1,7 @@
|
||||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Storage API
|
||||
|
||||
WXT provides a simplified API to replace the `browser.storage.*` APIs. Use the `storage` auto-import from `wxt/storage` or import it manually to get started:
|
||||
@@ -35,6 +39,8 @@ await storage.watch<number>(
|
||||
await storage.getMeta<{ v: number }>('local:installDate');
|
||||
```
|
||||
|
||||
For a full list of methods available, see the [API reference](/api/wxt/storage/interfaces/WxtStorage).
|
||||
|
||||
## Watchers
|
||||
|
||||
To listen for storage changes, use the `storage.watch` function. It lets you setup a listener for a single key:
|
||||
@@ -58,7 +64,7 @@ unwatch();
|
||||
|
||||
`wxt/storage` also supports setting metadata for keys, stored at `key + "$"`. Metadata is a collection of properties associated with a key. It might be a version number, last modified date, etc.
|
||||
|
||||
[Other than versioning](#versioning-and-migrations), you are responsible for managing a field's metadata:
|
||||
[Other than versioning](#versioning), you are responsible for managing a field's metadata:
|
||||
|
||||
```ts
|
||||
await Promise.all([
|
||||
@@ -116,7 +122,9 @@ const unwatch = showChangelogOnUpdate.watch(() => {
|
||||
});
|
||||
```
|
||||
|
||||
### Versioning and Migrations
|
||||
For a full list of properties and methods available, see the [API reference](/api/wxt/storage/interfaces/WxtStorageItem).
|
||||
|
||||
### Versioning
|
||||
|
||||
You can add versioning to storage items if you expect them to grow or change over time. When defining the first version of an item, start with version 1.
|
||||
|
||||
@@ -252,3 +260,30 @@ export const ignoredWebsites = storage.defineItem<IgnoredWebsiteV2[]>( // [!code
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### Running Migrations
|
||||
|
||||
To run migrations, you have two options:
|
||||
|
||||
1. Import all versioned storage items into your background script and they will run automatically whenever your extension updates
|
||||
2. Manually call `item.migrate()`
|
||||
|
||||
The first approach is recommended. To make importing all your storage items easy, you can define all of them in a single file, `utils/storage.ts`, and import that file into your background entrypoint:
|
||||
|
||||
```ts
|
||||
// utils/storage.ts
|
||||
export countStorage = storage.defineItem(...);
|
||||
export themeStorage = storage.defineItem(...);
|
||||
export someOtherStorage = storage.defineItem(...);
|
||||
```
|
||||
|
||||
```ts
|
||||
// entrypoints/background.ts
|
||||
import '@/utils/storage'; // This import runs migrations on updates
|
||||
|
||||
export default defineBackground({
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
> When you call `storage.defineItem`, a `browser.runtime.onInstalled` listener is added. `onInstalled` listeners are only triggered in the background, which is why you must import them into the background.
|
||||
|
||||
+10
-10
@@ -6,7 +6,7 @@ title: Next-gen Web Extension Framework
|
||||
hero:
|
||||
name: WXT
|
||||
text: Next-gen Web Extension Framework
|
||||
tagline: An open source tool that makes Chrome Extension devlopment faster than ever before.
|
||||
tagline: An open source tool that makes Chrome Extension development faster than ever before.
|
||||
image:
|
||||
src: /hero-logo.svg
|
||||
alt: WXT
|
||||
@@ -47,11 +47,9 @@ features:
|
||||
details: Nuxt-like auto-imports to speed up development.
|
||||
link: /guide/auto-imports
|
||||
linkText: Read docs
|
||||
- icon: ⬇️
|
||||
title: Bundle Remote Code
|
||||
details: Downloads and bundles remote code imported from URLs.
|
||||
link: /guide/remote-code
|
||||
linkText: Read docs
|
||||
- icon: 🤖
|
||||
title: Automated Publishing
|
||||
details: Automatically zip, upload, submit, and publish extensions.
|
||||
- icon: 🎨
|
||||
title: Frontend Framework Agnostic
|
||||
details: Works with any front-end framework with a Vite plugin.
|
||||
@@ -64,10 +62,12 @@ features:
|
||||
linkText: See templates
|
||||
- icon: 📏
|
||||
title: Bundle Analysis
|
||||
details: Tools for analyizing the final extension bundle and minimizing your extension's size.
|
||||
- icon: 🤖
|
||||
title: Automated Publishing
|
||||
details: 'Coming soon. Automatically zip, upload, and release extensions.'
|
||||
details: Tools for analyzing the final extension bundle and minimizing your extension's size.
|
||||
- icon: ⬇️
|
||||
title: Bundle Remote Code
|
||||
details: Downloads and bundles remote code imported from URLs.
|
||||
link: /guide/remote-code
|
||||
linkText: Read docs
|
||||
---
|
||||
|
||||
<section class="vp-doc">
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { TestProject } from '../utils';
|
||||
import { resetBundleIncrement } from '~/core/builders/vite/plugins';
|
||||
|
||||
describe('Analysis', () => {
|
||||
beforeEach(() => {
|
||||
resetBundleIncrement();
|
||||
});
|
||||
|
||||
it('should output a stats.html with no part files by default', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html');
|
||||
project.addFile('entrypoints/options.html');
|
||||
project.addFile(
|
||||
'entrypoints/background.ts',
|
||||
'export default defineBackground(() => {});',
|
||||
);
|
||||
|
||||
await project.build({
|
||||
analysis: {
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(await project.fileExists('stats.html')).toBe(true);
|
||||
expect(await project.fileExists('.output/chrome-mv3/stats-0.json')).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should save part files when requested', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html');
|
||||
project.addFile('entrypoints/options.html');
|
||||
project.addFile(
|
||||
'entrypoints/background.ts',
|
||||
'export default defineBackground(() => {});',
|
||||
);
|
||||
|
||||
await project.build({
|
||||
analysis: {
|
||||
enabled: true,
|
||||
keepArtifacts: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(await project.fileExists('stats.html')).toBe(true);
|
||||
expect(await project.fileExists('stats-0.json')).toBe(true);
|
||||
expect(await project.fileExists('stats-1.json')).toBe(true);
|
||||
});
|
||||
|
||||
it('should support customizing the stats output directory', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html');
|
||||
project.addFile('entrypoints/options.html');
|
||||
project.addFile(
|
||||
'entrypoints/background.ts',
|
||||
'export default defineBackground(() => {});',
|
||||
);
|
||||
|
||||
await project.build({
|
||||
analysis: {
|
||||
enabled: true,
|
||||
outputFile: 'stats/bundle.html',
|
||||
},
|
||||
});
|
||||
|
||||
expect(await project.fileExists('stats/bundle.html')).toBe(true);
|
||||
});
|
||||
|
||||
it('should place artifacts next to the custom output file', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html');
|
||||
project.addFile('entrypoints/options.html');
|
||||
project.addFile(
|
||||
'entrypoints/background.ts',
|
||||
'export default defineBackground(() => {});',
|
||||
);
|
||||
|
||||
await project.build({
|
||||
analysis: {
|
||||
enabled: true,
|
||||
outputFile: 'stats/bundle.html',
|
||||
keepArtifacts: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(await project.fileExists('stats/bundle.html')).toBe(true);
|
||||
expect(await project.fileExists('stats/bundle-0.json')).toBe(true);
|
||||
expect(await project.fileExists('stats/bundle-1.json')).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,7 @@ describe('Auto Imports', () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html', `<html></html>`);
|
||||
|
||||
await project.build();
|
||||
await project.prepare();
|
||||
|
||||
expect(await project.serializeFile('.wxt/types/imports.d.ts'))
|
||||
.toMatchInlineSnapshot(`
|
||||
@@ -38,7 +38,7 @@ describe('Auto Imports', () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html', `<html></html>`);
|
||||
|
||||
await project.build();
|
||||
await project.prepare();
|
||||
|
||||
expect(await project.serializeFile('.wxt/wxt.d.ts'))
|
||||
.toMatchInlineSnapshot(`
|
||||
@@ -63,7 +63,7 @@ describe('Auto Imports', () => {
|
||||
});
|
||||
project.addFile('entrypoints/popup.html', `<html></html>`);
|
||||
|
||||
await project.build();
|
||||
await project.prepare();
|
||||
|
||||
expect(await project.fileExists('.wxt/types/imports.d.ts')).toBe(false);
|
||||
});
|
||||
@@ -75,7 +75,7 @@ describe('Auto Imports', () => {
|
||||
});
|
||||
project.addFile('entrypoints/popup.html', `<html></html>`);
|
||||
|
||||
await project.build();
|
||||
await project.prepare();
|
||||
|
||||
expect(
|
||||
await project.serializeFile('.wxt/wxt.d.ts'),
|
||||
@@ -93,4 +93,82 @@ describe('Auto Imports', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('eslintrc', () => {
|
||||
it('should output the globals list for ESLint to consume', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html', `<html></html>`);
|
||||
|
||||
await project.prepare({
|
||||
imports: {
|
||||
eslintrc: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(await project.serializeFile('.wxt/eslintrc-auto-import.json'))
|
||||
.toMatchInlineSnapshot(`
|
||||
".wxt/eslintrc-auto-import.json
|
||||
----------------------------------------
|
||||
{
|
||||
"globals": {
|
||||
"ContentScriptContext": true,
|
||||
"InvalidMatchPattern": true,
|
||||
"MatchPattern": true,
|
||||
"browser": true,
|
||||
"createIframeUi": true,
|
||||
"createIntegratedUi": true,
|
||||
"createShadowRootUi": true,
|
||||
"defineBackground": true,
|
||||
"defineConfig": true,
|
||||
"defineContentScript": true,
|
||||
"defineUnlistedScript": true,
|
||||
"fakeBrowser": true,
|
||||
"storage": true
|
||||
}
|
||||
}
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should allow customizing the output', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html', `<html></html>`);
|
||||
|
||||
await project.prepare({
|
||||
imports: {
|
||||
eslintrc: {
|
||||
enabled: true,
|
||||
filePath: project.resolvePath('example.json'),
|
||||
globalsPropValue: 'readonly',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(await project.serializeFile('example.json'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"example.json
|
||||
----------------------------------------
|
||||
{
|
||||
"globals": {
|
||||
"ContentScriptContext": "readonly",
|
||||
"InvalidMatchPattern": "readonly",
|
||||
"MatchPattern": "readonly",
|
||||
"browser": "readonly",
|
||||
"createIframeUi": "readonly",
|
||||
"createIntegratedUi": "readonly",
|
||||
"createShadowRootUi": "readonly",
|
||||
"defineBackground": "readonly",
|
||||
"defineConfig": "readonly",
|
||||
"defineContentScript": "readonly",
|
||||
"defineUnlistedScript": "readonly",
|
||||
"fakeBrowser": "readonly",
|
||||
"storage": "readonly"
|
||||
}
|
||||
}
|
||||
"
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { TestProject } from '../utils';
|
||||
import { WxtHooks } from '~/types';
|
||||
|
||||
const hooks: WxtHooks = {
|
||||
ready: vi.fn(),
|
||||
'build:before': vi.fn(),
|
||||
'build:done': vi.fn(),
|
||||
'build:manifestGenerated': vi.fn(),
|
||||
'entrypoints:resolved': vi.fn(),
|
||||
'entrypoints:grouped': vi.fn(),
|
||||
};
|
||||
|
||||
function expectHooksToBeCalled(called: Record<keyof WxtHooks, boolean>) {
|
||||
Object.keys(hooks).forEach((key) => {
|
||||
const hookName = key as keyof WxtHooks;
|
||||
const times = called[hookName] ? 1 : 0;
|
||||
expect(
|
||||
hooks[hookName],
|
||||
`Expected "${hookName}" to be called ${times} time(s)`,
|
||||
).toBeCalledTimes(called[hookName] ? 1 : 0);
|
||||
});
|
||||
}
|
||||
|
||||
describe('Hooks', () => {
|
||||
beforeEach(() => {
|
||||
Object.values(hooks).forEach((fn) => fn.mockReset());
|
||||
});
|
||||
|
||||
it('prepare should call hooks', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html', '<html></html>');
|
||||
|
||||
await project.prepare({ hooks });
|
||||
|
||||
expectHooksToBeCalled({
|
||||
ready: true,
|
||||
'build:before': false,
|
||||
'build:done': false,
|
||||
'build:manifestGenerated': false,
|
||||
'entrypoints:grouped': false,
|
||||
'entrypoints:resolved': true,
|
||||
});
|
||||
});
|
||||
|
||||
it('build should call hooks', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html', '<html></html>');
|
||||
|
||||
await project.build({ hooks });
|
||||
|
||||
expectHooksToBeCalled({
|
||||
ready: true,
|
||||
'build:before': true,
|
||||
'build:done': true,
|
||||
'build:manifestGenerated': true,
|
||||
'entrypoints:grouped': true,
|
||||
'entrypoints:resolved': true,
|
||||
});
|
||||
});
|
||||
|
||||
it('zip should call hooks', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html', '<html></html>');
|
||||
|
||||
await project.zip({ hooks });
|
||||
|
||||
expectHooksToBeCalled({
|
||||
ready: true,
|
||||
'build:before': true,
|
||||
'build:done': true,
|
||||
'build:manifestGenerated': true,
|
||||
'entrypoints:grouped': true,
|
||||
'entrypoints:resolved': true,
|
||||
});
|
||||
});
|
||||
|
||||
it('server.start should call hooks', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html', '<html></html>');
|
||||
|
||||
const server = await project.startServer({
|
||||
hooks,
|
||||
runner: {
|
||||
disabled: true,
|
||||
},
|
||||
});
|
||||
await server.stop();
|
||||
|
||||
expectHooksToBeCalled({
|
||||
ready: true,
|
||||
'build:before': true,
|
||||
'build:done': true,
|
||||
'build:manifestGenerated': true,
|
||||
'entrypoints:grouped': true,
|
||||
'entrypoints:resolved': true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -270,43 +270,43 @@ describe('Output Directory Structure', () => {
|
||||
|
||||
expect(await project.serializeFile('.output/chrome-mv3/background.js'))
|
||||
.toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/background.js
|
||||
----------------------------------------
|
||||
import { l as logHello } from "./chunks/log-bezs0tt4.js";
|
||||
function defineBackground(arg) {
|
||||
if (typeof arg === "function")
|
||||
return { main: arg };
|
||||
return arg;
|
||||
}
|
||||
const definition = defineBackground({
|
||||
type: "module",
|
||||
main() {
|
||||
logHello("background");
|
||||
".output/chrome-mv3/background.js
|
||||
----------------------------------------
|
||||
import { l as logHello } from "./chunks/log-bezs0tt4.js";
|
||||
function defineBackground(arg) {
|
||||
if (typeof arg === "function")
|
||||
return { main: arg };
|
||||
return arg;
|
||||
}
|
||||
});
|
||||
chrome;
|
||||
function print(method, ...args) {
|
||||
return;
|
||||
}
|
||||
var logger = {
|
||||
debug: (...args) => print(console.debug, ...args),
|
||||
log: (...args) => print(console.log, ...args),
|
||||
warn: (...args) => print(console.warn, ...args),
|
||||
error: (...args) => print(console.error, ...args)
|
||||
};
|
||||
try {
|
||||
const res = definition.main();
|
||||
if (res instanceof Promise) {
|
||||
console.warn(
|
||||
"The background's main() function return a promise, but it must be synchonous"
|
||||
);
|
||||
const definition = defineBackground({
|
||||
type: "module",
|
||||
main() {
|
||||
logHello("background");
|
||||
}
|
||||
});
|
||||
chrome;
|
||||
function print(method, ...args) {
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("The background crashed on startup!");
|
||||
throw err;
|
||||
}
|
||||
"
|
||||
`);
|
||||
var logger = {
|
||||
debug: (...args) => print(console.debug, ...args),
|
||||
log: (...args) => print(console.log, ...args),
|
||||
warn: (...args) => print(console.warn, ...args),
|
||||
error: (...args) => print(console.error, ...args)
|
||||
};
|
||||
try {
|
||||
const res = definition.main();
|
||||
if (res instanceof Promise) {
|
||||
console.warn(
|
||||
"The background's main() function return a promise, but it must be synchronous"
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("The background crashed on startup!");
|
||||
throw err;
|
||||
}
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should generate IIFE background script when type=undefined', async () => {
|
||||
@@ -381,7 +381,7 @@ describe('Output Directory Structure', () => {
|
||||
const res = definition.main();
|
||||
if (res instanceof Promise) {
|
||||
console.warn(
|
||||
"The background's main() function return a promise, but it must be synchonous"
|
||||
"The background's main() function return a promise, but it must be synchronous"
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('TypeScript Project', () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/unlisted.html');
|
||||
|
||||
await project.build();
|
||||
await project.prepare();
|
||||
|
||||
const output = await project.serializeFile('.wxt/types/globals.d.ts');
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
@@ -38,7 +38,7 @@ describe('TypeScript Project', () => {
|
||||
project.addFile('entrypoints/options.html');
|
||||
project.addFile('entrypoints/sandbox.html');
|
||||
|
||||
await project.build();
|
||||
await project.prepare();
|
||||
|
||||
const output = await project.serializeFile('.wxt/types/paths.d.ts');
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
@@ -103,7 +103,7 @@ describe('TypeScript Project', () => {
|
||||
},
|
||||
});
|
||||
|
||||
await project.build();
|
||||
await project.prepare();
|
||||
|
||||
const output = await project.serializeFile('.wxt/types/i18n.d.ts');
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
@@ -225,7 +225,7 @@ describe('TypeScript Project', () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/unlisted.html');
|
||||
|
||||
await project.build();
|
||||
await project.prepare();
|
||||
|
||||
const output = await project.serializeFile('.wxt/wxt.d.ts');
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
@@ -245,7 +245,7 @@ describe('TypeScript Project', () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/unlisted.html');
|
||||
|
||||
await project.build();
|
||||
await project.prepare();
|
||||
|
||||
const output = await project.serializeFile('.wxt/tsconfig.json');
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
@@ -289,7 +289,7 @@ describe('TypeScript Project', () => {
|
||||
srcDir: 'src',
|
||||
});
|
||||
|
||||
await project.build();
|
||||
await project.prepare();
|
||||
|
||||
const output = await project.serializeFile('.wxt/tsconfig.json');
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
@@ -337,7 +337,7 @@ describe('TypeScript Project', () => {
|
||||
},
|
||||
});
|
||||
|
||||
await project.build();
|
||||
await project.prepare();
|
||||
|
||||
const output = await project.serializeFile('.wxt/tsconfig.json');
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
|
||||
@@ -107,4 +107,29 @@ describe('User Config', () => {
|
||||
});
|
||||
expect(withoutPolyfill).not.toBe(withPolyfill);
|
||||
});
|
||||
|
||||
it('should respect changing config files', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'src/entrypoints/background.ts',
|
||||
`export default defineBackground(
|
||||
() => console.log('Hello background'),
|
||||
);`,
|
||||
);
|
||||
project.addFile(
|
||||
'test.config.ts',
|
||||
`import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
outDir: ".custom-output",
|
||||
srcDir: "src",
|
||||
});`,
|
||||
);
|
||||
|
||||
await project.build({ configFile: 'test.config.ts' });
|
||||
|
||||
expect(
|
||||
await project.fileExists('.custom-output/chrome-mv3/background.js'),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
+44
-12
@@ -2,7 +2,14 @@ import { dirname, join, relative, resolve } from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import glob from 'fast-glob';
|
||||
import { execaCommand } from 'execa';
|
||||
import { InlineConfig, UserConfig, build } from '../src';
|
||||
import {
|
||||
InlineConfig,
|
||||
UserConfig,
|
||||
build,
|
||||
createServer,
|
||||
prepare,
|
||||
zip,
|
||||
} from '../src';
|
||||
import { normalizePath } from '../src/core/utils/paths';
|
||||
import merge from 'lodash.merge';
|
||||
|
||||
@@ -63,15 +70,41 @@ export class TestProject {
|
||||
if (filename === 'wxt.config.ts') this.config = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the files to the test directory install dependencies, and build the project.
|
||||
*/
|
||||
async prepare(config: InlineConfig = {}) {
|
||||
await this.writeProjectToDisk();
|
||||
await prepare({ ...config, root: this.root });
|
||||
}
|
||||
|
||||
async build(config: InlineConfig = {}) {
|
||||
await this.writeProjectToDisk();
|
||||
await build({ ...config, root: this.root });
|
||||
}
|
||||
|
||||
async zip(config: InlineConfig = {}) {
|
||||
await this.writeProjectToDisk();
|
||||
await zip({ ...config, root: this.root });
|
||||
}
|
||||
|
||||
async startServer(config: InlineConfig = {}) {
|
||||
await this.writeProjectToDisk();
|
||||
const server = await createServer({ ...config, root: this.root });
|
||||
await server.start();
|
||||
return server;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call `path.resolve` relative to the project's root directory.
|
||||
*/
|
||||
resolvePath(...path: string[]): string {
|
||||
return resolve(this.root, ...path);
|
||||
}
|
||||
|
||||
private async writeProjectToDisk() {
|
||||
if (this.config == null) this.setConfigFileConfig();
|
||||
|
||||
for (const file of this.files) {
|
||||
const [name, content] = file;
|
||||
const filePath = resolve(this.root, name);
|
||||
const filePath = this.resolvePath(name);
|
||||
const fileDir = dirname(filePath);
|
||||
await fs.ensureDir(fileDir);
|
||||
await fs.writeFile(filePath, content ?? '', 'utf-8');
|
||||
@@ -80,7 +113,6 @@ export class TestProject {
|
||||
await execaCommand('pnpm --ignore-workspace i --ignore-scripts', {
|
||||
cwd: this.root,
|
||||
});
|
||||
await build({ ...config, root: this.root });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,13 +147,13 @@ export class TestProject {
|
||||
ignoreContentsOfFilenames?: string[],
|
||||
): Promise<string> {
|
||||
const outputFiles = await glob('**/*', {
|
||||
cwd: resolve(this.root, dir),
|
||||
cwd: this.resolvePath(dir),
|
||||
ignore: ['**/node_modules', '**/.output'],
|
||||
});
|
||||
outputFiles.sort();
|
||||
const fileContents = [];
|
||||
for (const file of outputFiles) {
|
||||
const path = resolve(this.root, dir, file);
|
||||
const path = this.resolvePath(dir, file);
|
||||
const isContentIgnored = !!ignoreContentsOfFilenames?.find(
|
||||
(ignoredFile) => normalizePath(path).endsWith(ignoredFile),
|
||||
);
|
||||
@@ -131,12 +163,12 @@ export class TestProject {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path An abosolute path to a file or a path relative to the root.
|
||||
* @param path An absolute path to a file or a path relative to the root.
|
||||
* @param ignoreContents An optional boolean that, when true, causes this function to not print
|
||||
* the file contents.
|
||||
*/
|
||||
async serializeFile(path: string, ignoreContents?: boolean): Promise<string> {
|
||||
const absolutePath = resolve(this.root, path);
|
||||
const absolutePath = this.resolvePath(path);
|
||||
return [
|
||||
normalizePath(relative(this.root, absolutePath)),
|
||||
ignoreContents ? '<contents-ignored>' : await fs.readFile(absolutePath),
|
||||
@@ -144,12 +176,12 @@ export class TestProject {
|
||||
}
|
||||
|
||||
fileExists(path: string): Promise<boolean> {
|
||||
return fs.exists(resolve(this.root, path));
|
||||
return fs.exists(this.resolvePath(path));
|
||||
}
|
||||
|
||||
async getOutputManifest(
|
||||
path: string = '.output/chrome-mv3/manifest.json',
|
||||
): Promise<any> {
|
||||
return await fs.readJson(resolve(this.root, path));
|
||||
return await fs.readJson(this.resolvePath(path));
|
||||
}
|
||||
}
|
||||
|
||||
+11
-9
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wxt",
|
||||
"type": "module",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.2",
|
||||
"description": "Next gen framework for developing web extensions",
|
||||
"engines": {
|
||||
"node": ">=18",
|
||||
@@ -30,7 +30,10 @@
|
||||
"bin",
|
||||
"dist"
|
||||
],
|
||||
"bin": "./bin/wxt.mjs",
|
||||
"bin": {
|
||||
"wxt": "./bin/wxt.mjs",
|
||||
"wxt-publish-extension": "./bin/wxt-publish-extension.cjs"
|
||||
},
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -103,6 +106,7 @@
|
||||
"sync-releases": "pnpx changelogen@latest gh release"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aklinker1/rollup-plugin-visualizer": "5.12.0",
|
||||
"@types/webextension-polyfill": "^0.10.5",
|
||||
"@webext-core/fake-browser": "^1.3.1",
|
||||
"@webext-core/isolated-element": "^1.1.1",
|
||||
@@ -120,22 +124,20 @@
|
||||
"fs-extra": "^11.1.1",
|
||||
"get-port": "^7.0.0",
|
||||
"giget": "^1.1.3",
|
||||
"immer": "^10.0.3",
|
||||
"hookable": "^5.5.3",
|
||||
"is-wsl": "^3.0.0",
|
||||
"jiti": "^1.21.0",
|
||||
"json5": "^2.2.3",
|
||||
"linkedom": "^0.16.1",
|
||||
"manage-path": "^2.0.0",
|
||||
"minimatch": "^9.0.3",
|
||||
"natural-compare": "^1.4.0",
|
||||
"normalize-path": "^3.0.0",
|
||||
"ora": "^7.0.1",
|
||||
"picocolors": "^1.0.0",
|
||||
"prompts": "^2.4.2",
|
||||
"publish-browser-extension": "^2.1.0",
|
||||
"rollup-plugin-visualizer": "^5.9.2",
|
||||
"publish-browser-extension": "^2.1.3",
|
||||
"unimport": "^3.4.0",
|
||||
"vite": "^5.0.12",
|
||||
"vite": "^5.1.3",
|
||||
"web-ext-run": "^0.2.0",
|
||||
"webextension-polyfill": "^0.10.0",
|
||||
"zip-dir": "^2.0.0"
|
||||
@@ -150,7 +152,7 @@
|
||||
"@types/prompts": "^2.4.9",
|
||||
"@vitest/coverage-v8": "^1.0.1",
|
||||
"execa": "^8.0.1",
|
||||
"happy-dom": "^12.10.3",
|
||||
"happy-dom": "^13.3.8",
|
||||
"lint-staged": "^15.2.0",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"npm-run-all": "^4.1.5",
|
||||
@@ -165,7 +167,7 @@
|
||||
"typedoc-vitepress-theme": "1.0.0-next.3",
|
||||
"typescript": "^5.3.2",
|
||||
"vitepress": "1.0.0-rc.34",
|
||||
"vitest": "^1.1.0",
|
||||
"vitest": "^1.2.2",
|
||||
"vitest-mock-extended": "^1.3.1",
|
||||
"vue": "^3.3.10"
|
||||
},
|
||||
|
||||
Generated
+138
-151
@@ -8,9 +8,12 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@aklinker1/rollup-plugin-visualizer':
|
||||
specifier: 5.12.0
|
||||
version: 5.12.0
|
||||
'@types/webextension-polyfill':
|
||||
specifier: ^0.10.5
|
||||
version: 0.10.5
|
||||
version: 0.10.7
|
||||
'@webext-core/fake-browser':
|
||||
specifier: ^1.3.1
|
||||
version: 1.3.1
|
||||
@@ -37,7 +40,7 @@ importers:
|
||||
version: 3.2.3
|
||||
defu:
|
||||
specifier: ^6.1.3
|
||||
version: 6.1.3
|
||||
version: 6.1.4
|
||||
dequal:
|
||||
specifier: ^2.0.3
|
||||
version: 2.0.3
|
||||
@@ -59,9 +62,9 @@ importers:
|
||||
giget:
|
||||
specifier: ^1.1.3
|
||||
version: 1.1.3
|
||||
immer:
|
||||
specifier: ^10.0.3
|
||||
version: 10.0.3
|
||||
hookable:
|
||||
specifier: ^5.5.3
|
||||
version: 5.5.3
|
||||
is-wsl:
|
||||
specifier: ^3.0.0
|
||||
version: 3.1.0
|
||||
@@ -74,9 +77,6 @@ importers:
|
||||
linkedom:
|
||||
specifier: ^0.16.1
|
||||
version: 0.16.1
|
||||
manage-path:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
minimatch:
|
||||
specifier: ^9.0.3
|
||||
version: 9.0.3
|
||||
@@ -96,17 +96,14 @@ importers:
|
||||
specifier: ^2.4.2
|
||||
version: 2.4.2
|
||||
publish-browser-extension:
|
||||
specifier: ^2.1.0
|
||||
version: 2.1.0
|
||||
rollup-plugin-visualizer:
|
||||
specifier: ^5.9.2
|
||||
version: 5.9.3
|
||||
specifier: ^2.1.3
|
||||
version: 2.1.3
|
||||
unimport:
|
||||
specifier: ^3.4.0
|
||||
version: 3.4.0
|
||||
vite:
|
||||
specifier: ^5.0.12
|
||||
version: 5.0.12(@types/node@20.10.3)(sass@1.69.5)
|
||||
specifier: ^5.1.3
|
||||
version: 5.1.3(@types/node@20.10.3)(sass@1.69.5)
|
||||
web-ext-run:
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0
|
||||
@@ -140,13 +137,13 @@ importers:
|
||||
version: 2.4.9
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^1.0.1
|
||||
version: 1.2.2(vitest@1.1.0)
|
||||
version: 1.2.2(vitest@1.2.2)
|
||||
execa:
|
||||
specifier: ^8.0.1
|
||||
version: 8.0.1
|
||||
happy-dom:
|
||||
specifier: ^12.10.3
|
||||
version: 12.10.3
|
||||
specifier: ^13.3.8
|
||||
version: 13.3.8
|
||||
lint-staged:
|
||||
specifier: ^15.2.0
|
||||
version: 15.2.1
|
||||
@@ -170,13 +167,13 @@ importers:
|
||||
version: 2.9.0
|
||||
tsup:
|
||||
specifier: ^8.0.1
|
||||
version: 8.0.1(typescript@5.3.2)
|
||||
version: 8.0.1(typescript@5.3.3)
|
||||
tsx:
|
||||
specifier: ^4.6.2
|
||||
version: 4.7.0
|
||||
typedoc:
|
||||
specifier: ^0.25.4
|
||||
version: 0.25.4(typescript@5.3.2)
|
||||
version: 0.25.4(typescript@5.3.3)
|
||||
typedoc-plugin-markdown:
|
||||
specifier: 4.0.0-next.23
|
||||
version: 4.0.0-next.23(typedoc@0.25.4)
|
||||
@@ -185,19 +182,19 @@ importers:
|
||||
version: 1.0.0-next.3(typedoc-plugin-markdown@4.0.0-next.23)
|
||||
typescript:
|
||||
specifier: ^5.3.2
|
||||
version: 5.3.2
|
||||
version: 5.3.3
|
||||
vitepress:
|
||||
specifier: 1.0.0-rc.34
|
||||
version: 1.0.0-rc.34(@types/node@20.10.3)(typescript@5.3.2)
|
||||
version: 1.0.0-rc.34(@types/node@20.10.3)(typescript@5.3.3)
|
||||
vitest:
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0(@types/node@20.10.3)(happy-dom@12.10.3)(sass@1.69.5)
|
||||
specifier: ^1.2.2
|
||||
version: 1.2.2(@types/node@20.10.3)(happy-dom@13.3.8)(sass@1.69.5)
|
||||
vitest-mock-extended:
|
||||
specifier: ^1.3.1
|
||||
version: 1.3.1(typescript@5.3.2)(vitest@1.1.0)
|
||||
version: 1.3.1(typescript@5.3.3)(vitest@1.2.2)
|
||||
vue:
|
||||
specifier: ^3.3.10
|
||||
version: 3.3.10(typescript@5.3.2)
|
||||
version: 3.3.10(typescript@5.3.3)
|
||||
|
||||
demo:
|
||||
dependencies:
|
||||
@@ -208,8 +205,8 @@ importers:
|
||||
specifier: ^18.2.0
|
||||
version: 18.2.0(react@18.2.0)
|
||||
vitest:
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0(@types/node@20.10.3)(happy-dom@12.10.3)(sass@1.69.5)
|
||||
specifier: ^1.2.2
|
||||
version: 1.2.2(@types/node@20.10.3)(happy-dom@13.3.8)(sass@1.69.5)
|
||||
devDependencies:
|
||||
'@types/react':
|
||||
specifier: ^18.2.34
|
||||
@@ -226,6 +223,22 @@ importers:
|
||||
|
||||
packages:
|
||||
|
||||
/@aklinker1/rollup-plugin-visualizer@5.12.0:
|
||||
resolution: {integrity: sha512-X24LvEGw6UFmy0lpGJDmXsMyBD58XmX1bbwsaMLhNoM+UMQfQ3b2RtC+nz4b/NoRK5r6QJSKJHBNVeUdwqybaQ==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
rollup: 2.x || 3.x || 4.x
|
||||
peerDependenciesMeta:
|
||||
rollup:
|
||||
optional: true
|
||||
dependencies:
|
||||
open: 8.4.2
|
||||
picomatch: 2.3.1
|
||||
source-map: 0.7.4
|
||||
yargs: 17.7.2
|
||||
dev: false
|
||||
|
||||
/@algolia/autocomplete-core@1.9.3(algoliasearch@4.20.0):
|
||||
resolution: {integrity: sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw==}
|
||||
dependencies:
|
||||
@@ -1058,7 +1071,6 @@ packages:
|
||||
|
||||
/@types/estree@1.0.4:
|
||||
resolution: {integrity: sha512-2JwWnHK9H+wUZNorf2Zr6ves96WHoWDJIftkcxPKsS7Djta6Zu519LarhRNljPXkpsZR2ZMwNCPeW7omW07BJw==}
|
||||
dev: false
|
||||
|
||||
/@types/fs-extra@11.0.4:
|
||||
resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==}
|
||||
@@ -1156,8 +1168,8 @@ packages:
|
||||
resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
|
||||
dev: true
|
||||
|
||||
/@types/webextension-polyfill@0.10.5:
|
||||
resolution: {integrity: sha512-LlmbFLUB7+BDrb9nMuM0wlqtx9LZbBV2x3W98o02cD7Y8i10+sBenTlhG56vr47dzC7WIVXbURii+5jMJsyjLw==}
|
||||
/@types/webextension-polyfill@0.10.7:
|
||||
resolution: {integrity: sha512-10ql7A0qzBmFB+F+qAke/nP1PIonS0TXZAOMVOxEUsm+lGSW6uwVcISFNa0I4Oyj0884TZVWGGMIWeXOVSNFHw==}
|
||||
dev: false
|
||||
|
||||
/@types/yauzl@2.10.3:
|
||||
@@ -1168,18 +1180,18 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@vitejs/plugin-vue@5.0.2(vite@5.0.12)(vue@3.4.3):
|
||||
/@vitejs/plugin-vue@5.0.2(vite@5.1.3)(vue@3.4.3):
|
||||
resolution: {integrity: sha512-kEjJHrLb5ePBvjD0SPZwJlw1QTRcjjCA9sB5VyfonoXVBxTS7TMnqL6EkLt1Eu61RDeiuZ/WN9Hf6PxXhPI2uA==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
peerDependencies:
|
||||
vite: ^5.0.0
|
||||
vue: ^3.2.25
|
||||
dependencies:
|
||||
vite: 5.0.12(@types/node@20.10.3)(sass@1.69.5)
|
||||
vue: 3.4.3(typescript@5.3.2)
|
||||
vite: 5.1.3(@types/node@20.10.3)(sass@1.69.5)
|
||||
vue: 3.4.3(typescript@5.3.3)
|
||||
dev: true
|
||||
|
||||
/@vitest/coverage-v8@1.2.2(vitest@1.1.0):
|
||||
/@vitest/coverage-v8@1.2.2(vitest@1.2.2):
|
||||
resolution: {integrity: sha512-IHyKnDz18SFclIEEAHb9Y4Uxx0sPKC2VO1kdDCs1BF6Ip4S8rQprs971zIsooLUn7Afs71GRxWMWpkCGZpRMhw==}
|
||||
peerDependencies:
|
||||
vitest: ^1.0.0
|
||||
@@ -1197,41 +1209,42 @@ packages:
|
||||
std-env: 3.6.0
|
||||
test-exclude: 6.0.0
|
||||
v8-to-istanbul: 9.2.0
|
||||
vitest: 1.1.0(@types/node@20.10.3)(happy-dom@12.10.3)(sass@1.69.5)
|
||||
vitest: 1.2.2(@types/node@20.10.3)(happy-dom@13.3.8)(sass@1.69.5)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@vitest/expect@1.1.0:
|
||||
resolution: {integrity: sha512-9IE2WWkcJo2BR9eqtY5MIo3TPmS50Pnwpm66A6neb2hvk/QSLfPXBz2qdiwUOQkwyFuuXEUj5380CbwfzW4+/w==}
|
||||
/@vitest/expect@1.2.2:
|
||||
resolution: {integrity: sha512-3jpcdPAD7LwHUUiT2pZTj2U82I2Tcgg2oVPvKxhn6mDI2On6tfvPQTjAI4628GUGDZrCm4Zna9iQHm5cEexOAg==}
|
||||
dependencies:
|
||||
'@vitest/spy': 1.1.0
|
||||
'@vitest/utils': 1.1.0
|
||||
'@vitest/spy': 1.2.2
|
||||
'@vitest/utils': 1.2.2
|
||||
chai: 4.3.10
|
||||
|
||||
/@vitest/runner@1.1.0:
|
||||
resolution: {integrity: sha512-zdNLJ00pm5z/uhbWF6aeIJCGMSyTyWImy3Fcp9piRGvueERFlQFbUwCpzVce79OLm2UHk9iwaMSOaU9jVHgNVw==}
|
||||
/@vitest/runner@1.2.2:
|
||||
resolution: {integrity: sha512-JctG7QZ4LSDXr5CsUweFgcpEvrcxOV1Gft7uHrvkQ+fsAVylmWQvnaAr/HDp3LAH1fztGMQZugIheTWjaGzYIg==}
|
||||
dependencies:
|
||||
'@vitest/utils': 1.1.0
|
||||
'@vitest/utils': 1.2.2
|
||||
p-limit: 5.0.0
|
||||
pathe: 1.1.1
|
||||
|
||||
/@vitest/snapshot@1.1.0:
|
||||
resolution: {integrity: sha512-5O/wyZg09V5qmNmAlUgCBqflvn2ylgsWJRRuPrnHEfDNT6tQpQ8O1isNGgo+VxofISHqz961SG3iVvt3SPK/QQ==}
|
||||
/@vitest/snapshot@1.2.2:
|
||||
resolution: {integrity: sha512-SmGY4saEw1+bwE1th6S/cZmPxz/Q4JWsl7LvbQIky2tKE35US4gd0Mjzqfr84/4OD0tikGWaWdMja/nWL5NIPA==}
|
||||
dependencies:
|
||||
magic-string: 0.30.5
|
||||
pathe: 1.1.1
|
||||
pretty-format: 29.7.0
|
||||
|
||||
/@vitest/spy@1.1.0:
|
||||
resolution: {integrity: sha512-sNOVSU/GE+7+P76qYo+VXdXhXffzWZcYIPQfmkiRxaNCSPiLANvQx5Mx6ZURJ/ndtEkUJEpvKLXqAYTKEY+lTg==}
|
||||
/@vitest/spy@1.2.2:
|
||||
resolution: {integrity: sha512-k9Gcahssw8d7X3pSLq3e3XEu/0L78mUkCjivUqCQeXJm9clfXR/Td8+AP+VC1O6fKPIDLcHDTAmBOINVuv6+7g==}
|
||||
dependencies:
|
||||
tinyspy: 2.2.0
|
||||
|
||||
/@vitest/utils@1.1.0:
|
||||
resolution: {integrity: sha512-z+s510fKmYz4Y41XhNs3vcuFTFhcij2YF7F8VQfMEYAAUfqQh0Zfg7+w9xdgFGhPf3tX3TicAe+8BDITk6ampQ==}
|
||||
/@vitest/utils@1.2.2:
|
||||
resolution: {integrity: sha512-WKITBHLsBHlpjnDQahr+XK6RE7MiAsgrIkr0pGhQ9ygoxBfUeG0lUG5iLlzqjmKSlBv3+j5EGsriBzh+C3Tq9g==}
|
||||
dependencies:
|
||||
diff-sequences: 29.6.3
|
||||
estree-walker: 3.0.3
|
||||
loupe: 2.3.7
|
||||
pretty-format: 29.7.0
|
||||
|
||||
@@ -1293,7 +1306,7 @@ packages:
|
||||
'@vue/shared': 3.4.3
|
||||
estree-walker: 2.0.2
|
||||
magic-string: 0.30.5
|
||||
postcss: 8.4.32
|
||||
postcss: 8.4.35
|
||||
source-map-js: 1.0.2
|
||||
dev: true
|
||||
|
||||
@@ -1374,7 +1387,7 @@ packages:
|
||||
dependencies:
|
||||
'@vue/compiler-ssr': 3.3.10
|
||||
'@vue/shared': 3.3.10
|
||||
vue: 3.3.10(typescript@5.3.2)
|
||||
vue: 3.3.10(typescript@5.3.3)
|
||||
dev: true
|
||||
|
||||
/@vue/server-renderer@3.4.3(vue@3.4.3):
|
||||
@@ -1384,7 +1397,7 @@ packages:
|
||||
dependencies:
|
||||
'@vue/compiler-ssr': 3.4.3
|
||||
'@vue/shared': 3.4.3
|
||||
vue: 3.4.3(typescript@5.3.2)
|
||||
vue: 3.4.3(typescript@5.3.3)
|
||||
dev: true
|
||||
|
||||
/@vue/shared@3.3.10:
|
||||
@@ -1484,8 +1497,8 @@ packages:
|
||||
resolution: {integrity: sha512-NY39ACqCxdKBmHgw361M9pfJma8e4AZo20w9AY+5ZjIj1W2dvXC8J31G5fjfOGbulW9w4WKpT8fPooi0mLkn9A==}
|
||||
dev: false
|
||||
|
||||
/acorn-walk@8.3.0:
|
||||
resolution: {integrity: sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==}
|
||||
/acorn-walk@8.3.2:
|
||||
resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
/acorn@8.11.2:
|
||||
@@ -1744,7 +1757,7 @@ packages:
|
||||
resolution: {integrity: sha512-BWZRJgDEveT8uI+cliCwvYSSSSvb4xKoiiu5S0jaDbKBopQLQF7E+bq9xKk1pTcG+mUa3yXuFO7bD9d8Lr9Xxg==}
|
||||
dependencies:
|
||||
chokidar: 3.5.3
|
||||
defu: 6.1.3
|
||||
defu: 6.1.4
|
||||
dotenv: 16.3.1
|
||||
giget: 1.1.3
|
||||
jiti: 1.21.0
|
||||
@@ -2053,9 +2066,6 @@ packages:
|
||||
engines: {node: '>= 6'}
|
||||
dev: false
|
||||
|
||||
/css.escape@1.5.1:
|
||||
resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
|
||||
|
||||
/cssom@0.5.0:
|
||||
resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==}
|
||||
dev: false
|
||||
@@ -2159,8 +2169,8 @@ packages:
|
||||
object-keys: 1.1.1
|
||||
dev: true
|
||||
|
||||
/defu@6.1.3:
|
||||
resolution: {integrity: sha512-Vy2wmG3NTkmHNg/kzpuvHhkqeIx3ODWqasgCRbKtbXEN0G+HpEEv9BtJLp7ZG1CZloFaC41Ah3ZFbq7aqCqMeQ==}
|
||||
/defu@6.1.4:
|
||||
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
|
||||
dev: false
|
||||
|
||||
/dequal@2.0.3:
|
||||
@@ -2409,6 +2419,11 @@ packages:
|
||||
/estree-walker@2.0.2:
|
||||
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
||||
|
||||
/estree-walker@3.0.3:
|
||||
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
|
||||
dependencies:
|
||||
'@types/estree': 1.0.4
|
||||
|
||||
/eventemitter3@5.0.1:
|
||||
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
|
||||
|
||||
@@ -2674,7 +2689,7 @@ packages:
|
||||
hasBin: true
|
||||
dependencies:
|
||||
colorette: 2.0.20
|
||||
defu: 6.1.3
|
||||
defu: 6.1.4
|
||||
https-proxy-agent: 7.0.2
|
||||
mri: 1.2.0
|
||||
node-fetch-native: 1.4.1
|
||||
@@ -2791,14 +2806,12 @@ packages:
|
||||
resolution: {integrity: sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==}
|
||||
dev: false
|
||||
|
||||
/happy-dom@12.10.3:
|
||||
resolution: {integrity: sha512-JzUXOh0wdNGY54oKng5hliuBkq/+aT1V3YpTM+lrN/GoLQTANZsMaIvmHiHe612rauHvPJnDZkZ+5GZR++1Abg==}
|
||||
/happy-dom@13.3.8:
|
||||
resolution: {integrity: sha512-RAbq4oYfJNkVan1m1F3jfA4YEyRY0/ASoNvZsNJbuX85jIypidmsz9jQZD7Tqz0VXA2MhAGfcsh5oshwmwNYSg==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
dependencies:
|
||||
css.escape: 1.5.1
|
||||
entities: 4.5.0
|
||||
iconv-lite: 0.6.3
|
||||
webidl-conversions: 7.0.0
|
||||
whatwg-encoding: 2.0.0
|
||||
whatwg-mimetype: 3.0.0
|
||||
|
||||
/has-bigints@1.0.2:
|
||||
@@ -2852,6 +2865,10 @@ packages:
|
||||
resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
|
||||
dev: false
|
||||
|
||||
/hookable@5.5.3:
|
||||
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
|
||||
dev: false
|
||||
|
||||
/hosted-git-info@2.8.9:
|
||||
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
|
||||
dev: true
|
||||
@@ -2908,12 +2925,6 @@ packages:
|
||||
resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
|
||||
engines: {node: '>=16.17.0'}
|
||||
|
||||
/iconv-lite@0.6.3:
|
||||
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
||||
/ieee754@1.2.1:
|
||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||
dev: false
|
||||
@@ -2934,10 +2945,6 @@ packages:
|
||||
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
|
||||
dev: false
|
||||
|
||||
/immer@10.0.3:
|
||||
resolution: {integrity: sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==}
|
||||
dev: false
|
||||
|
||||
/immutable@4.3.4:
|
||||
resolution: {integrity: sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==}
|
||||
|
||||
@@ -3550,10 +3557,6 @@ packages:
|
||||
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
|
||||
dev: false
|
||||
|
||||
/manage-path@2.0.0:
|
||||
resolution: {integrity: sha512-NJhyB+PJYTpxhxZJ3lecIGgh4kwIY2RAh44XvAz9UlqthlQwtPBf62uBVR8XaD8CRuSjQ6TnZH2lNJkbLPZM2A==}
|
||||
dev: false
|
||||
|
||||
/mark.js@8.11.1:
|
||||
resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==}
|
||||
dev: true
|
||||
@@ -4120,6 +4123,15 @@ packages:
|
||||
nanoid: 3.3.7
|
||||
picocolors: 1.0.0
|
||||
source-map-js: 1.0.2
|
||||
dev: true
|
||||
|
||||
/postcss@8.4.35:
|
||||
resolution: {integrity: sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
dependencies:
|
||||
nanoid: 3.3.7
|
||||
picocolors: 1.0.0
|
||||
source-map-js: 1.0.2
|
||||
|
||||
/preact@10.18.1:
|
||||
resolution: {integrity: sha512-mKUD7RRkQQM6s7Rkmi7IFkoEHjuFqRQUaXamO61E6Nn7vqF/bo7EZCmSyrUnp2UWHw0O7XjZ2eeXis+m7tf4lg==}
|
||||
@@ -4172,9 +4184,9 @@ packages:
|
||||
sade: 1.8.1
|
||||
dev: true
|
||||
|
||||
/publish-browser-extension@2.1.0:
|
||||
resolution: {integrity: sha512-OdnhaGTZ5d3PPNh8gUjTvLhrjH4H0lawe+y5gUvGHAt3hVS33FBTt0r/NyP5qgNtN9TM3iHfTduN4GydXiXt8w==}
|
||||
engines: {node: '18', pnpm: '8'}
|
||||
/publish-browser-extension@2.1.3:
|
||||
resolution: {integrity: sha512-qisnXUUwjvu5kMvObfG7UQ9rPU3t0XfbKdCdCrwAXaLUySdC25nlM4gxi+CBvL7LiwvK494GJyEK/weQBhIyTQ==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
@@ -4226,7 +4238,7 @@ packages:
|
||||
/rc9@2.1.1:
|
||||
resolution: {integrity: sha512-lNeOl38Ws0eNxpO3+wD1I9rkHGQyj1NU1jlzv4go2CtEnEQEUfqnIvZG7W+bC/aXdJ27n5x/yUjb6RoT9tko+Q==}
|
||||
dependencies:
|
||||
defu: 6.1.3
|
||||
defu: 6.1.4
|
||||
destr: 2.0.2
|
||||
flat: 5.0.2
|
||||
dev: false
|
||||
@@ -4388,22 +4400,6 @@ packages:
|
||||
glob: 7.1.6
|
||||
dev: false
|
||||
|
||||
/rollup-plugin-visualizer@5.9.3:
|
||||
resolution: {integrity: sha512-ieGM5UAbMVqThX67GCuFHu/GkaSXIUZwFKJsSzE+7+k9fibU/6gbUz7SL+9BBzNtv5bIFHj7kEu0TWcqEnT/sQ==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
rollup: 2.x || 3.x || 4.x
|
||||
peerDependenciesMeta:
|
||||
rollup:
|
||||
optional: true
|
||||
dependencies:
|
||||
open: 8.4.2
|
||||
picomatch: 2.3.1
|
||||
source-map: 0.7.4
|
||||
yargs: 17.7.2
|
||||
dev: false
|
||||
|
||||
/rollup@4.6.1:
|
||||
resolution: {integrity: sha512-jZHaZotEHQaHLgKr8JnQiDT1rmatjgKlMekyksz+yk9jt/8z9quNjnKNRoaM0wd9DC2QKXjmWWuDYtM3jfF8pQ==}
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
@@ -4460,9 +4456,6 @@ packages:
|
||||
is-regex: 1.1.4
|
||||
dev: true
|
||||
|
||||
/safer-buffer@2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
|
||||
/sass@1.69.5:
|
||||
resolution: {integrity: sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@@ -4899,8 +4892,8 @@ packages:
|
||||
/tinybench@2.5.1:
|
||||
resolution: {integrity: sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg==}
|
||||
|
||||
/tinypool@0.8.1:
|
||||
resolution: {integrity: sha512-zBTCK0cCgRROxvs9c0CGK838sPkeokNGdQVUUwHAbynHFlmyJYj825f/oRs528HaIJ97lo0pLIlDUzwN+IorWg==}
|
||||
/tinypool@0.8.2:
|
||||
resolution: {integrity: sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
/tinyspy@2.2.0:
|
||||
@@ -4941,12 +4934,12 @@ packages:
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/ts-essentials@9.3.2(typescript@5.3.2):
|
||||
/ts-essentials@9.3.2(typescript@5.3.3):
|
||||
resolution: {integrity: sha512-JxKJzuWqH1MmH4ZFHtJzGEhkfN3QvVR3C3w+4BIoWeoY68UVVoA2Np/Bca9z0IPSErVCWhv439aT0We4Dks8kQ==}
|
||||
peerDependencies:
|
||||
typescript: '>=4.1.0'
|
||||
dependencies:
|
||||
typescript: 5.3.2
|
||||
typescript: 5.3.3
|
||||
dev: true
|
||||
|
||||
/ts-interface-checker@0.1.13:
|
||||
@@ -4957,7 +4950,7 @@ packages:
|
||||
resolution: {integrity: sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==}
|
||||
dev: false
|
||||
|
||||
/tsup@8.0.1(typescript@5.3.2):
|
||||
/tsup@8.0.1(typescript@5.3.3):
|
||||
resolution: {integrity: sha512-hvW7gUSG96j53ZTSlT4j/KL0q1Q2l6TqGBFc6/mu/L46IoNWqLLUzLRLP1R8Q7xrJTmkDxxDoojV5uCVs1sVOg==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
@@ -4990,7 +4983,7 @@ packages:
|
||||
source-map: 0.8.0-beta.0
|
||||
sucrase: 3.32.0
|
||||
tree-kill: 1.2.2
|
||||
typescript: 5.3.2
|
||||
typescript: 5.3.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- ts-node
|
||||
@@ -5048,7 +5041,7 @@ packages:
|
||||
peerDependencies:
|
||||
typedoc: '>=0.25.0'
|
||||
dependencies:
|
||||
typedoc: 0.25.4(typescript@5.3.2)
|
||||
typedoc: 0.25.4(typescript@5.3.3)
|
||||
dev: true
|
||||
|
||||
/typedoc-vitepress-theme@1.0.0-next.3(typedoc-plugin-markdown@4.0.0-next.23):
|
||||
@@ -5059,7 +5052,7 @@ packages:
|
||||
typedoc-plugin-markdown: 4.0.0-next.23(typedoc@0.25.4)
|
||||
dev: true
|
||||
|
||||
/typedoc@0.25.4(typescript@5.3.2):
|
||||
/typedoc@0.25.4(typescript@5.3.3):
|
||||
resolution: {integrity: sha512-Du9ImmpBCw54bX275yJrxPVnjdIyJO/84co0/L9mwe0R3G4FSR6rQ09AlXVRvZEGMUg09+z/usc8mgygQ1aidA==}
|
||||
engines: {node: '>= 16'}
|
||||
hasBin: true
|
||||
@@ -5070,11 +5063,11 @@ packages:
|
||||
marked: 4.3.0
|
||||
minimatch: 9.0.3
|
||||
shiki: 0.14.5
|
||||
typescript: 5.3.2
|
||||
typescript: 5.3.3
|
||||
dev: true
|
||||
|
||||
/typescript@5.3.2:
|
||||
resolution: {integrity: sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==}
|
||||
/typescript@5.3.3:
|
||||
resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
@@ -5192,8 +5185,8 @@ packages:
|
||||
spdx-expression-parse: 3.0.1
|
||||
dev: true
|
||||
|
||||
/vite-node@1.1.0(@types/node@20.10.3)(sass@1.69.5):
|
||||
resolution: {integrity: sha512-jV48DDUxGLEBdHCQvxL1mEh7+naVy+nhUUUaPAZLd3FJgXuxQiewHcfeZebbJ6onDqNGkP4r3MhQ342PRlG81Q==}
|
||||
/vite-node@1.2.2(@types/node@20.10.3)(sass@1.69.5):
|
||||
resolution: {integrity: sha512-1as4rDTgVWJO3n1uHmUYqq7nsFgINQ9u+mRcXpjeOMJUmviqNKjcZB7UfRZrlM7MjYXMKpuWp5oGkjaFLnjawg==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
@@ -5201,7 +5194,7 @@ packages:
|
||||
debug: 4.3.4
|
||||
pathe: 1.1.1
|
||||
picocolors: 1.0.0
|
||||
vite: 5.0.12(@types/node@20.10.3)(sass@1.69.5)
|
||||
vite: 5.1.3(@types/node@20.10.3)(sass@1.69.5)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- less
|
||||
@@ -5212,8 +5205,8 @@ packages:
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
/vite@5.0.12(@types/node@20.10.3)(sass@1.69.5):
|
||||
resolution: {integrity: sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==}
|
||||
/vite@5.1.3(@types/node@20.10.3)(sass@1.69.5):
|
||||
resolution: {integrity: sha512-UfmUD36DKkqhi/F75RrxvPpry+9+tTkrXfMNZD+SboZqBCMsxKtO52XeGzzuh7ioz+Eo/SYDBbdb0Z7vgcDJew==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -5242,13 +5235,13 @@ packages:
|
||||
dependencies:
|
||||
'@types/node': 20.10.3
|
||||
esbuild: 0.19.11
|
||||
postcss: 8.4.32
|
||||
postcss: 8.4.35
|
||||
rollup: 4.6.1
|
||||
sass: 1.69.5
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
/vitepress@1.0.0-rc.34(@types/node@20.10.3)(typescript@5.3.2):
|
||||
/vitepress@1.0.0-rc.34(@types/node@20.10.3)(typescript@5.3.3):
|
||||
resolution: {integrity: sha512-TUbTiSdAZFni2XlHlpx61KikgkQ5uG4Wtmw2R0SXhIOG6qGqzDJczAFjkMc4i45I9c3KyatwOYe8oEfCnzVYwQ==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -5263,7 +5256,7 @@ packages:
|
||||
'@docsearch/css': 3.5.2
|
||||
'@docsearch/js': 3.5.2
|
||||
'@types/markdown-it': 13.0.7
|
||||
'@vitejs/plugin-vue': 5.0.2(vite@5.0.12)(vue@3.4.3)
|
||||
'@vitejs/plugin-vue': 5.0.2(vite@5.1.3)(vue@3.4.3)
|
||||
'@vue/devtools-api': 6.5.1
|
||||
'@vueuse/core': 10.7.1(vue@3.4.3)
|
||||
'@vueuse/integrations': 10.7.1(focus-trap@7.5.4)(vue@3.4.3)
|
||||
@@ -5274,8 +5267,8 @@ packages:
|
||||
shikiji: 0.9.16
|
||||
shikiji-core: 0.9.16
|
||||
shikiji-transformers: 0.9.16
|
||||
vite: 5.0.12(@types/node@20.10.3)(sass@1.69.5)
|
||||
vue: 3.4.3(typescript@5.3.2)
|
||||
vite: 5.1.3(@types/node@20.10.3)(sass@1.69.5)
|
||||
vue: 3.4.3(typescript@5.3.3)
|
||||
transitivePeerDependencies:
|
||||
- '@algolia/client-search'
|
||||
- '@types/node'
|
||||
@@ -5304,19 +5297,19 @@ packages:
|
||||
- universal-cookie
|
||||
dev: true
|
||||
|
||||
/vitest-mock-extended@1.3.1(typescript@5.3.2)(vitest@1.1.0):
|
||||
/vitest-mock-extended@1.3.1(typescript@5.3.3)(vitest@1.2.2):
|
||||
resolution: {integrity: sha512-OpghYjh4BDuQ/Mzs3lFMQ1QRk9D8/2O9T47MLUA5eLn7K4RWIy+MfIivYOWEyxjTENjsBnzgMihDjyNalN/K0Q==}
|
||||
peerDependencies:
|
||||
typescript: 3.x || 4.x || 5.x
|
||||
vitest: '>=0.31.1'
|
||||
dependencies:
|
||||
ts-essentials: 9.3.2(typescript@5.3.2)
|
||||
typescript: 5.3.2
|
||||
vitest: 1.1.0(@types/node@20.10.3)(happy-dom@12.10.3)(sass@1.69.5)
|
||||
ts-essentials: 9.3.2(typescript@5.3.3)
|
||||
typescript: 5.3.3
|
||||
vitest: 1.2.2(@types/node@20.10.3)(happy-dom@13.3.8)(sass@1.69.5)
|
||||
dev: true
|
||||
|
||||
/vitest@1.1.0(@types/node@20.10.3)(happy-dom@12.10.3)(sass@1.69.5):
|
||||
resolution: {integrity: sha512-oDFiCrw7dd3Jf06HoMtSRARivvyjHJaTxikFxuqJjO76U436PqlVw1uLn7a8OSPrhSfMGVaRakKpA2lePdw79A==}
|
||||
/vitest@1.2.2(@types/node@20.10.3)(happy-dom@13.3.8)(sass@1.69.5):
|
||||
resolution: {integrity: sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -5341,17 +5334,17 @@ packages:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@types/node': 20.10.3
|
||||
'@vitest/expect': 1.1.0
|
||||
'@vitest/runner': 1.1.0
|
||||
'@vitest/snapshot': 1.1.0
|
||||
'@vitest/spy': 1.1.0
|
||||
'@vitest/utils': 1.1.0
|
||||
acorn-walk: 8.3.0
|
||||
'@vitest/expect': 1.2.2
|
||||
'@vitest/runner': 1.2.2
|
||||
'@vitest/snapshot': 1.2.2
|
||||
'@vitest/spy': 1.2.2
|
||||
'@vitest/utils': 1.2.2
|
||||
acorn-walk: 8.3.2
|
||||
cac: 6.7.14
|
||||
chai: 4.3.10
|
||||
debug: 4.3.4
|
||||
execa: 8.0.1
|
||||
happy-dom: 12.10.3
|
||||
happy-dom: 13.3.8
|
||||
local-pkg: 0.5.0
|
||||
magic-string: 0.30.5
|
||||
pathe: 1.1.1
|
||||
@@ -5359,9 +5352,9 @@ packages:
|
||||
std-env: 3.6.0
|
||||
strip-literal: 1.3.0
|
||||
tinybench: 2.5.1
|
||||
tinypool: 0.8.1
|
||||
vite: 5.0.12(@types/node@20.10.3)(sass@1.69.5)
|
||||
vite-node: 1.1.0(@types/node@20.10.3)(sass@1.69.5)
|
||||
tinypool: 0.8.2
|
||||
vite: 5.1.3(@types/node@20.10.3)(sass@1.69.5)
|
||||
vite-node: 1.2.2(@types/node@20.10.3)(sass@1.69.5)
|
||||
why-is-node-running: 2.2.2
|
||||
transitivePeerDependencies:
|
||||
- less
|
||||
@@ -5392,10 +5385,10 @@ packages:
|
||||
'@vue/composition-api':
|
||||
optional: true
|
||||
dependencies:
|
||||
vue: 3.4.3(typescript@5.3.2)
|
||||
vue: 3.4.3(typescript@5.3.3)
|
||||
dev: true
|
||||
|
||||
/vue@3.3.10(typescript@5.3.2):
|
||||
/vue@3.3.10(typescript@5.3.3):
|
||||
resolution: {integrity: sha512-zg6SIXZdTBwiqCw/1p+m04VyHjLfwtjwz8N57sPaBhEex31ND0RYECVOC1YrRwMRmxFf5T1dabl6SGUbMKKuVw==}
|
||||
peerDependencies:
|
||||
typescript: '*'
|
||||
@@ -5408,10 +5401,10 @@ packages:
|
||||
'@vue/runtime-dom': 3.3.10
|
||||
'@vue/server-renderer': 3.3.10(vue@3.3.10)
|
||||
'@vue/shared': 3.3.10
|
||||
typescript: 5.3.2
|
||||
typescript: 5.3.3
|
||||
dev: true
|
||||
|
||||
/vue@3.4.3(typescript@5.3.2):
|
||||
/vue@3.4.3(typescript@5.3.3):
|
||||
resolution: {integrity: sha512-GjN+culMAGv/mUbkIv8zMKItno8npcj5gWlXkSxf1SPTQf8eJ4A+YfHIvQFyL1IfuJcMl3soA7SmN1fRxbf/wA==}
|
||||
peerDependencies:
|
||||
typescript: '*'
|
||||
@@ -5424,7 +5417,7 @@ packages:
|
||||
'@vue/runtime-dom': 3.4.3
|
||||
'@vue/server-renderer': 3.4.3(vue@3.4.3)
|
||||
'@vue/shared': 3.4.3
|
||||
typescript: 5.3.2
|
||||
typescript: 5.3.3
|
||||
dev: true
|
||||
|
||||
/watchpack@2.4.0:
|
||||
@@ -5496,12 +5489,6 @@ packages:
|
||||
resolution: {integrity: sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==}
|
||||
dev: false
|
||||
|
||||
/whatwg-encoding@2.0.0:
|
||||
resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
iconv-lite: 0.6.3
|
||||
|
||||
/whatwg-mimetype@3.0.0:
|
||||
resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
+469
-413
@@ -1,7 +1,14 @@
|
||||
import { fakeBrowser } from '@webext-core/fake-browser';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, vi, expectTypeOf } from 'vitest';
|
||||
import { browser } from '~/browser';
|
||||
import { storage } from '~/storage';
|
||||
import { WxtStorageItem, storage } from '~/storage';
|
||||
|
||||
async function triggerUpdate() {
|
||||
await fakeBrowser.runtime.onInstalled.trigger({
|
||||
reason: 'update',
|
||||
temporary: false,
|
||||
});
|
||||
}
|
||||
|
||||
describe('Storage Utils', () => {
|
||||
beforeEach(() => {
|
||||
@@ -384,417 +391,466 @@ describe('Storage Utils', () => {
|
||||
expect(cb).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineItem', () => {
|
||||
describe('versioning', () => {
|
||||
/**
|
||||
* This works because fakeBrowser is syncrounous, and is will finish any number of chained
|
||||
* calls within a single tick of the event loop, ie: a timeout of 0.
|
||||
*/
|
||||
async function waitForMigrations() {
|
||||
return new Promise((res) => setTimeout(res));
|
||||
}
|
||||
|
||||
it('should migrate values to the latest when a version upgrade is detected', async () => {
|
||||
await fakeBrowser.storage[storageArea].set({
|
||||
count: 2,
|
||||
count$: { v: 1 },
|
||||
});
|
||||
const migrateToV2 = vi.fn((oldCount) => oldCount * 2);
|
||||
const migrateToV3 = vi.fn((oldCount) => oldCount * 3);
|
||||
|
||||
const item = storage.defineItem<number, { v: number }>(
|
||||
`${storageArea}:count`,
|
||||
{
|
||||
version: 3,
|
||||
migrations: {
|
||||
2: migrateToV2,
|
||||
3: migrateToV3,
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitForMigrations();
|
||||
|
||||
const actualValue = await item.getValue();
|
||||
const actualMeta = await item.getMeta();
|
||||
|
||||
expect(actualValue).toEqual(12);
|
||||
expect(actualMeta).toEqual({ v: 3 });
|
||||
|
||||
expect(migrateToV2).toBeCalledTimes(1);
|
||||
expect(migrateToV2).toBeCalledWith(2);
|
||||
|
||||
expect(migrateToV3).toBeCalledTimes(1);
|
||||
expect(migrateToV3).toBeCalledWith(4);
|
||||
});
|
||||
|
||||
it("should not run migrations if the value doesn't exist yet", async () => {
|
||||
const migrateToV2 = vi.fn((oldCount) => oldCount * 2);
|
||||
const migrateToV3 = vi.fn((oldCount) => oldCount * 3);
|
||||
|
||||
const item = storage.defineItem<number, { v: number }>(
|
||||
`${storageArea}:count`,
|
||||
{
|
||||
version: 3,
|
||||
migrations: {
|
||||
2: migrateToV2,
|
||||
3: migrateToV3,
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitForMigrations();
|
||||
|
||||
const actualValue = await item.getValue();
|
||||
const actualMeta = await item.getMeta();
|
||||
|
||||
expect(actualValue).toBeNull();
|
||||
expect(actualMeta).toEqual({});
|
||||
|
||||
expect(migrateToV2).not.toBeCalled();
|
||||
expect(migrateToV3).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should run the v2 migration when converting an unversioned item to a versioned one', async () => {
|
||||
await fakeBrowser.storage[storageArea].set({
|
||||
count: 2,
|
||||
});
|
||||
const migrateToV2 = vi.fn((oldCount) => oldCount * 2);
|
||||
|
||||
const item = storage.defineItem<number, { v: number }>(
|
||||
`${storageArea}:count`,
|
||||
{
|
||||
version: 2,
|
||||
migrations: {
|
||||
2: migrateToV2,
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitForMigrations();
|
||||
|
||||
const actualValue = await item.getValue();
|
||||
const actualMeta = await item.getMeta();
|
||||
|
||||
expect(actualValue).toEqual(4);
|
||||
expect(actualMeta).toEqual({ v: 2 });
|
||||
|
||||
expect(migrateToV2).toBeCalledTimes(1);
|
||||
expect(migrateToV2).toBeCalledWith(2);
|
||||
});
|
||||
|
||||
it('Should not run old migrations if the version is unchanged', async () => {
|
||||
await fakeBrowser.storage[storageArea].set({
|
||||
count: 2,
|
||||
count$: { v: 3 },
|
||||
});
|
||||
const migrateToV2 = vi.fn((oldCount) => oldCount * 2);
|
||||
const migrateToV3 = vi.fn((oldCount) => oldCount * 3);
|
||||
|
||||
storage.defineItem<number, { v: number }>(`${storageArea}:count`, {
|
||||
version: 3,
|
||||
migrations: {
|
||||
2: migrateToV2,
|
||||
3: migrateToV3,
|
||||
},
|
||||
});
|
||||
await waitForMigrations();
|
||||
|
||||
expect(migrateToV2).not.toBeCalled();
|
||||
expect(migrateToV3).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should skip missing migration functions', async () => {
|
||||
await fakeBrowser.storage[storageArea].set({
|
||||
count: 2,
|
||||
count$: { v: 0 },
|
||||
});
|
||||
const migrateToV1 = vi.fn((oldCount) => oldCount * 1);
|
||||
const migrateToV3 = vi.fn((oldCount) => oldCount * 3);
|
||||
|
||||
const item = storage.defineItem<number, { v: number }>(
|
||||
`${storageArea}:count`,
|
||||
{
|
||||
version: 3,
|
||||
migrations: {
|
||||
1: migrateToV1,
|
||||
3: migrateToV3,
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitForMigrations();
|
||||
|
||||
const actualValue = await item.getValue();
|
||||
const actualMeta = await item.getMeta();
|
||||
|
||||
expect(actualValue).toEqual(6);
|
||||
expect(actualMeta).toEqual({ v: 3 });
|
||||
|
||||
expect(migrateToV1).toBeCalledTimes(1);
|
||||
expect(migrateToV1).toBeCalledWith(2);
|
||||
|
||||
expect(migrateToV3).toBeCalledTimes(1);
|
||||
expect(migrateToV3).toBeCalledWith(2);
|
||||
});
|
||||
|
||||
it('should throw an error if the new version is less than the previous version', async () => {
|
||||
const prevVersion = 2;
|
||||
const nextVersion = 1;
|
||||
await fakeBrowser.storage[storageArea].set({
|
||||
count: 0,
|
||||
count$: { v: prevVersion },
|
||||
});
|
||||
|
||||
const item = storage.defineItem(`${storageArea}:count`, {
|
||||
version: nextVersion,
|
||||
});
|
||||
|
||||
// @ts-expect-error: _migrationsCompleted is returned, but untyped
|
||||
await expect(item._migrationsCompleted).rejects.toThrow(
|
||||
'version downgrade detected',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getValue', () => {
|
||||
it('should return the value from storage', async () => {
|
||||
const expected = 2;
|
||||
const item = storage.defineItem<number>(`${storageArea}:count`);
|
||||
await fakeBrowser.storage[storageArea].set({ count: expected });
|
||||
|
||||
const actual = await item.getValue();
|
||||
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it('should return null if missing', async () => {
|
||||
const item = storage.defineItem<number>(`${storageArea}:count`);
|
||||
|
||||
const actual = await item.getValue();
|
||||
|
||||
expect(actual).toBeNull();
|
||||
});
|
||||
|
||||
it('should return the provided default value if missing', async () => {
|
||||
const expected = 0;
|
||||
const item = storage.defineItem(`${storageArea}:count`, {
|
||||
defaultValue: expected,
|
||||
});
|
||||
|
||||
const actual = await item.getValue();
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMeta', () => {
|
||||
it('should return the value from storage at key+$', async () => {
|
||||
const expected = { v: 2 };
|
||||
const item = storage.defineItem<number, { v: number }>(
|
||||
`${storageArea}:count`,
|
||||
);
|
||||
await fakeBrowser.storage[storageArea].set({ count$: expected });
|
||||
|
||||
const actual = await item.getMeta();
|
||||
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it('should return an empty object if missing', async () => {
|
||||
const expected = {};
|
||||
const item = storage.defineItem<number, { v: number }>(
|
||||
`${storageArea}:count`,
|
||||
);
|
||||
|
||||
const actual = await item.getMeta();
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setValue', () => {
|
||||
it('should set the value in storage', async () => {
|
||||
const expected = 1;
|
||||
const item = storage.defineItem<number>(`${storageArea}:count`);
|
||||
|
||||
await item.setValue(expected);
|
||||
const actual = await item.getValue();
|
||||
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it.each([undefined, null])(
|
||||
'should remove the value in storage when %s is passed in',
|
||||
async (value) => {
|
||||
const item = storage.defineItem<number>(`${storageArea}:count`);
|
||||
|
||||
// @ts-expect-error: undefined is not assignable to null, but we're testing that case on purpose
|
||||
await item.setValue(value);
|
||||
const actual = await item.getValue();
|
||||
|
||||
expect(actual).toBeNull();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('setMeta', () => {
|
||||
it('should set metadata at key+$', async () => {
|
||||
const expected = { date: Date.now() };
|
||||
const item = storage.defineItem<number, { date: number }>(
|
||||
`${storageArea}:count`,
|
||||
);
|
||||
|
||||
await item.setMeta(expected);
|
||||
const actual = await item.getMeta();
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should add to metadata if already present', async () => {
|
||||
const existing = { v: 2 };
|
||||
const newFields = { date: Date.now() };
|
||||
const expected = { ...existing, ...newFields };
|
||||
const item = storage.defineItem<
|
||||
number,
|
||||
{ date: number; v: number }
|
||||
>(`${storageArea}:count`);
|
||||
await fakeBrowser.storage[storageArea].set({
|
||||
count$: existing,
|
||||
});
|
||||
|
||||
await item.setMeta(newFields);
|
||||
const actual = await item.getMeta();
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeValue', () => {
|
||||
it('should remove the key from storage', async () => {
|
||||
const item = storage.defineItem(`${storageArea}:count`);
|
||||
await fakeBrowser.storage[storageArea].set({ count: 456 });
|
||||
|
||||
await item.removeValue();
|
||||
const actual = await item.getValue();
|
||||
|
||||
expect(actual).toBeNull();
|
||||
});
|
||||
|
||||
it('should not remove the metadata by default', async () => {
|
||||
const item = storage.defineItem(`${storageArea}:count`);
|
||||
const expected = { v: 1 };
|
||||
await fakeBrowser.storage[storageArea].set({
|
||||
count$: expected,
|
||||
count: 3,
|
||||
});
|
||||
|
||||
await item.removeValue();
|
||||
const actual = await item.getMeta();
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should remove the metadata when requested', async () => {
|
||||
const item = storage.defineItem(`${storageArea}:count`);
|
||||
await fakeBrowser.storage[storageArea].set({
|
||||
count$: { v: 1 },
|
||||
count: 3,
|
||||
});
|
||||
|
||||
await item.removeValue({ removeMeta: true });
|
||||
const actual = await item.getMeta();
|
||||
|
||||
expect(actual).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeMeta', () => {
|
||||
it('should remove all metadata', async () => {
|
||||
const item = storage.defineItem<number, { v: number }>(
|
||||
`${storageArea}:count`,
|
||||
);
|
||||
await fakeBrowser.storage[storageArea].set({ count$: { v: 4 } });
|
||||
|
||||
await item.removeMeta();
|
||||
const actual = await item.getMeta();
|
||||
|
||||
expect(actual).toEqual({});
|
||||
});
|
||||
|
||||
it('should only remove specific properties', async () => {
|
||||
const item = storage.defineItem<number, { v: number; d: number }>(
|
||||
`${storageArea}:count`,
|
||||
);
|
||||
await fakeBrowser.storage[storageArea].set({
|
||||
count$: { v: 4, d: Date.now() },
|
||||
});
|
||||
|
||||
await item.removeMeta(['d']);
|
||||
const actual = await item.getMeta();
|
||||
|
||||
expect(actual).toEqual({ v: 4 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('watch', () => {
|
||||
it("should not trigger if the changed key is different from the item's key", async () => {
|
||||
const item = storage.defineItem(`${storageArea}:key`);
|
||||
const cb = vi.fn();
|
||||
|
||||
item.watch(cb);
|
||||
await storage.setItem(`${storageArea}:not-the-key`, '123');
|
||||
|
||||
expect(cb).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("should not trigger if the value doesn't change", async () => {
|
||||
const item = storage.defineItem(`${storageArea}:key`);
|
||||
const cb = vi.fn();
|
||||
const value = '123';
|
||||
|
||||
await item.setValue(value);
|
||||
item.watch(cb);
|
||||
await item.setValue(value);
|
||||
|
||||
expect(cb).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should call the callback when the value changes', async () => {
|
||||
const item = storage.defineItem(`${storageArea}:key`);
|
||||
const cb = vi.fn();
|
||||
const newValue = '123';
|
||||
const oldValue = null;
|
||||
|
||||
item.watch(cb);
|
||||
await item.setValue(newValue);
|
||||
|
||||
expect(cb).toBeCalledTimes(1);
|
||||
expect(cb).toBeCalledWith(newValue, oldValue);
|
||||
});
|
||||
|
||||
it('should remove the listener when calling the returned function', async () => {
|
||||
const item = storage.defineItem(`${storageArea}:key`);
|
||||
const cb = vi.fn();
|
||||
|
||||
const unwatch = item.watch(cb);
|
||||
unwatch();
|
||||
await item.setValue('123');
|
||||
|
||||
expect(cb).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('unwatch', () => {
|
||||
it('should remove all watch listeners', async () => {
|
||||
const item = storage.defineItem(`${storageArea}:key`);
|
||||
const cb = vi.fn();
|
||||
|
||||
item.watch(cb);
|
||||
storage.unwatch();
|
||||
await item.setValue('123');
|
||||
|
||||
expect(cb).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
describe('defineItem', () => {
|
||||
describe('versioning', () => {
|
||||
it('should migrate values to the latest when a version upgrade is detected', async () => {
|
||||
await fakeBrowser.storage.local.set({
|
||||
count: 2,
|
||||
count$: { v: 1 },
|
||||
});
|
||||
const migrateToV2 = vi.fn((oldCount) => oldCount * 2);
|
||||
const migrateToV3 = vi.fn((oldCount) => oldCount * 3);
|
||||
|
||||
const item = storage.defineItem<number, { v: number }>(`local:count`, {
|
||||
defaultValue: 0,
|
||||
version: 3,
|
||||
migrations: {
|
||||
2: migrateToV2,
|
||||
3: migrateToV3,
|
||||
},
|
||||
});
|
||||
await triggerUpdate();
|
||||
|
||||
const actualValue = await item.getValue();
|
||||
const actualMeta = await item.getMeta();
|
||||
|
||||
expect(actualValue).toEqual(12);
|
||||
expect(actualMeta).toEqual({ v: 3 });
|
||||
|
||||
expect(migrateToV2).toBeCalledTimes(1);
|
||||
expect(migrateToV2).toBeCalledWith(2);
|
||||
|
||||
expect(migrateToV3).toBeCalledTimes(1);
|
||||
expect(migrateToV3).toBeCalledWith(4);
|
||||
});
|
||||
|
||||
it("should not run migrations if the value doesn't exist yet", async () => {
|
||||
const migrateToV2 = vi.fn((oldCount) => oldCount * 2);
|
||||
const migrateToV3 = vi.fn((oldCount) => oldCount * 3);
|
||||
|
||||
const item = storage.defineItem<number, { v: number }>(`local:count`, {
|
||||
defaultValue: 0,
|
||||
version: 3,
|
||||
migrations: {
|
||||
2: migrateToV2,
|
||||
3: migrateToV3,
|
||||
},
|
||||
});
|
||||
await triggerUpdate();
|
||||
|
||||
const actualValue = await item.getValue();
|
||||
const actualMeta = await item.getMeta();
|
||||
|
||||
expect(actualValue).toEqual(0);
|
||||
expect(actualMeta).toEqual({});
|
||||
|
||||
expect(migrateToV2).not.toBeCalled();
|
||||
expect(migrateToV3).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should run the v2 migration when converting an unversioned item to a versioned one', async () => {
|
||||
await fakeBrowser.storage.local.set({
|
||||
count: 2,
|
||||
});
|
||||
const migrateToV2 = vi.fn((oldCount) => oldCount * 2);
|
||||
|
||||
const item = storage.defineItem<number, { v: number }>(`local:count`, {
|
||||
defaultValue: 0,
|
||||
version: 2,
|
||||
migrations: {
|
||||
2: migrateToV2,
|
||||
},
|
||||
});
|
||||
await triggerUpdate();
|
||||
|
||||
const actualValue = await item.getValue();
|
||||
const actualMeta = await item.getMeta();
|
||||
|
||||
expect(actualValue).toEqual(4);
|
||||
expect(actualMeta).toEqual({ v: 2 });
|
||||
|
||||
expect(migrateToV2).toBeCalledTimes(1);
|
||||
expect(migrateToV2).toBeCalledWith(2);
|
||||
});
|
||||
|
||||
it('should not run old migrations if the version is unchanged', async () => {
|
||||
await fakeBrowser.storage.local.set({
|
||||
count: 2,
|
||||
count$: { v: 3 },
|
||||
});
|
||||
const migrateToV2 = vi.fn((oldCount) => oldCount * 2);
|
||||
const migrateToV3 = vi.fn((oldCount) => oldCount * 3);
|
||||
|
||||
storage.defineItem<number, { v: number }>(`local:count`, {
|
||||
defaultValue: 0,
|
||||
version: 3,
|
||||
migrations: {
|
||||
2: migrateToV2,
|
||||
3: migrateToV3,
|
||||
},
|
||||
});
|
||||
await triggerUpdate();
|
||||
|
||||
expect(migrateToV2).not.toBeCalled();
|
||||
expect(migrateToV3).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should skip missing migration functions', async () => {
|
||||
await fakeBrowser.storage.local.set({
|
||||
count: 2,
|
||||
count$: { v: 0 },
|
||||
});
|
||||
const migrateToV1 = vi.fn((oldCount) => oldCount * 1);
|
||||
const migrateToV3 = vi.fn((oldCount) => oldCount * 3);
|
||||
|
||||
const item = storage.defineItem<number, { v: number }>(`local:count`, {
|
||||
defaultValue: 0,
|
||||
version: 3,
|
||||
migrations: {
|
||||
1: migrateToV1,
|
||||
3: migrateToV3,
|
||||
},
|
||||
});
|
||||
await triggerUpdate();
|
||||
|
||||
const actualValue = await item.getValue();
|
||||
const actualMeta = await item.getMeta();
|
||||
|
||||
expect(actualValue).toEqual(6);
|
||||
expect(actualMeta).toEqual({ v: 3 });
|
||||
|
||||
expect(migrateToV1).toBeCalledTimes(1);
|
||||
expect(migrateToV1).toBeCalledWith(2);
|
||||
|
||||
expect(migrateToV3).toBeCalledTimes(1);
|
||||
expect(migrateToV3).toBeCalledWith(2);
|
||||
});
|
||||
|
||||
it('should throw an error if the new version is less than the previous version', async () => {
|
||||
const prevVersion = 2;
|
||||
const nextVersion = 1;
|
||||
await fakeBrowser.storage.local.set({
|
||||
count: 0,
|
||||
count$: { v: prevVersion },
|
||||
});
|
||||
|
||||
const item = storage.defineItem(`local:count`, {
|
||||
defaultValue: 0,
|
||||
version: nextVersion,
|
||||
});
|
||||
await triggerUpdate();
|
||||
|
||||
await expect(item.migrate()).rejects.toThrow(
|
||||
'Version downgrade detected (v2 -> v1) for "local:count"',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getValue', () => {
|
||||
it('should return the value from storage', async () => {
|
||||
const expected = 2;
|
||||
const item = storage.defineItem<number>(`local:count`);
|
||||
await fakeBrowser.storage.local.set({ count: expected });
|
||||
|
||||
const actual = await item.getValue();
|
||||
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it('should return null if missing', async () => {
|
||||
const item = storage.defineItem<number>(`local:count`);
|
||||
|
||||
const actual = await item.getValue();
|
||||
|
||||
expect(actual).toBeNull();
|
||||
});
|
||||
|
||||
it('should return the provided default value if missing', async () => {
|
||||
const expected = 0;
|
||||
const item = storage.defineItem(`local:count`, {
|
||||
defaultValue: expected,
|
||||
});
|
||||
|
||||
const actual = await item.getValue();
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMeta', () => {
|
||||
it('should return the value from storage at key+$', async () => {
|
||||
const expected = { v: 2 };
|
||||
const item = storage.defineItem<number, { v: number }>(`local:count`);
|
||||
await fakeBrowser.storage.local.set({ count$: expected });
|
||||
|
||||
const actual = await item.getMeta();
|
||||
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it('should return an empty object if missing', async () => {
|
||||
const expected = {};
|
||||
const item = storage.defineItem<number, { v: number }>(`local:count`);
|
||||
|
||||
const actual = await item.getMeta();
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setValue', () => {
|
||||
it('should set the value in storage', async () => {
|
||||
const expected = 1;
|
||||
const item = storage.defineItem<number>(`local:count`);
|
||||
|
||||
await item.setValue(expected);
|
||||
const actual = await item.getValue();
|
||||
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it.each([undefined, null])(
|
||||
'should remove the value in storage when %s is passed in',
|
||||
async (value) => {
|
||||
const item = storage.defineItem<number>(`local:count`);
|
||||
|
||||
// @ts-expect-error: undefined is not assignable to null, but we're testing that case on purpose
|
||||
await item.setValue(value);
|
||||
const actual = await item.getValue();
|
||||
|
||||
expect(actual).toBeNull();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('setMeta', () => {
|
||||
it('should set metadata at key+$', async () => {
|
||||
const expected = { date: Date.now() };
|
||||
const item = storage.defineItem<number, { date: number }>(
|
||||
`local:count`,
|
||||
);
|
||||
|
||||
await item.setMeta(expected);
|
||||
const actual = await item.getMeta();
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should add to metadata if already present', async () => {
|
||||
const existing = { v: 2 };
|
||||
const newFields = { date: Date.now() };
|
||||
const expected = { ...existing, ...newFields };
|
||||
const item = storage.defineItem<number, { date: number; v: number }>(
|
||||
`local:count`,
|
||||
);
|
||||
await fakeBrowser.storage.local.set({
|
||||
count$: existing,
|
||||
});
|
||||
|
||||
await item.setMeta(newFields);
|
||||
const actual = await item.getMeta();
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeValue', () => {
|
||||
it('should remove the key from storage', async () => {
|
||||
const item = storage.defineItem(`local:count`);
|
||||
await fakeBrowser.storage.local.set({ count: 456 });
|
||||
|
||||
await item.removeValue();
|
||||
const actual = await item.getValue();
|
||||
|
||||
expect(actual).toBeNull();
|
||||
});
|
||||
|
||||
it('should not remove the metadata by default', async () => {
|
||||
const item = storage.defineItem(`local:count`);
|
||||
const expected = { v: 1 };
|
||||
await fakeBrowser.storage.local.set({
|
||||
count$: expected,
|
||||
count: 3,
|
||||
});
|
||||
|
||||
await item.removeValue();
|
||||
const actual = await item.getMeta();
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should remove the metadata when requested', async () => {
|
||||
const item = storage.defineItem(`local:count`);
|
||||
await fakeBrowser.storage.local.set({
|
||||
count$: { v: 1 },
|
||||
count: 3,
|
||||
});
|
||||
|
||||
await item.removeValue({ removeMeta: true });
|
||||
const actual = await item.getMeta();
|
||||
|
||||
expect(actual).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeMeta', () => {
|
||||
it('should remove all metadata', async () => {
|
||||
const item = storage.defineItem<number, { v: number }>(`local:count`);
|
||||
await fakeBrowser.storage.local.set({ count$: { v: 4 } });
|
||||
|
||||
await item.removeMeta();
|
||||
const actual = await item.getMeta();
|
||||
|
||||
expect(actual).toEqual({});
|
||||
});
|
||||
|
||||
it('should only remove specific properties', async () => {
|
||||
const item = storage.defineItem<number, { v: number; d: number }>(
|
||||
`local:count`,
|
||||
);
|
||||
await fakeBrowser.storage.local.set({
|
||||
count$: { v: 4, d: Date.now() },
|
||||
});
|
||||
|
||||
await item.removeMeta(['d']);
|
||||
const actual = await item.getMeta();
|
||||
|
||||
expect(actual).toEqual({ v: 4 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('watch', () => {
|
||||
it("should not trigger if the changed key is different from the item's key", async () => {
|
||||
const item = storage.defineItem(`local:key`);
|
||||
const cb = vi.fn();
|
||||
|
||||
item.watch(cb);
|
||||
await storage.setItem(`local:not-the-key`, '123');
|
||||
|
||||
expect(cb).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("should not trigger if the value doesn't change", async () => {
|
||||
const item = storage.defineItem(`local:key`);
|
||||
const cb = vi.fn();
|
||||
const value = '123';
|
||||
|
||||
await item.setValue(value);
|
||||
item.watch(cb);
|
||||
await item.setValue(value);
|
||||
|
||||
expect(cb).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should call the callback when the value changes', async () => {
|
||||
const item = storage.defineItem(`local:key`);
|
||||
const cb = vi.fn();
|
||||
const newValue = '123';
|
||||
const oldValue = null;
|
||||
|
||||
item.watch(cb);
|
||||
await item.setValue(newValue);
|
||||
|
||||
expect(cb).toBeCalledTimes(1);
|
||||
expect(cb).toBeCalledWith(newValue, oldValue);
|
||||
});
|
||||
|
||||
it('should use the default value for the newValue when the item is removed', async () => {
|
||||
const defaultValue = 'default';
|
||||
const item = storage.defineItem<string>(`local:key`, {
|
||||
defaultValue,
|
||||
});
|
||||
const cb = vi.fn();
|
||||
const oldValue = '123';
|
||||
await item.setValue(oldValue);
|
||||
|
||||
item.watch(cb);
|
||||
await item.removeValue();
|
||||
|
||||
expect(cb).toBeCalledTimes(1);
|
||||
expect(cb).toBeCalledWith(defaultValue, oldValue);
|
||||
});
|
||||
|
||||
it("should use the default value for the oldItem when the item didn't exist in storage yet", async () => {
|
||||
const defaultValue = 'default';
|
||||
const item = storage.defineItem<string>(`local:key`, {
|
||||
defaultValue,
|
||||
});
|
||||
const cb = vi.fn();
|
||||
const newValue = '123';
|
||||
await item.removeValue();
|
||||
|
||||
item.watch(cb);
|
||||
await item.setValue(newValue);
|
||||
|
||||
expect(cb).toBeCalledTimes(1);
|
||||
expect(cb).toBeCalledWith(newValue, defaultValue);
|
||||
});
|
||||
|
||||
it('should remove the listener when calling the returned function', async () => {
|
||||
const item = storage.defineItem(`local:key`);
|
||||
const cb = vi.fn();
|
||||
|
||||
const unwatch = item.watch(cb);
|
||||
unwatch();
|
||||
await item.setValue('123');
|
||||
|
||||
expect(cb).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('unwatch', () => {
|
||||
it('should remove all watch listeners', async () => {
|
||||
const item = storage.defineItem(`local:key`);
|
||||
const cb = vi.fn();
|
||||
|
||||
item.watch(cb);
|
||||
storage.unwatch();
|
||||
await item.setValue('123');
|
||||
|
||||
expect(cb).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('defaultValue', () => {
|
||||
it('should return the default value when provided', () => {
|
||||
const defaultValue = 123;
|
||||
const item = storage.defineItem(`local:test`, {
|
||||
defaultValue,
|
||||
});
|
||||
|
||||
expect(item.defaultValue).toBe(defaultValue);
|
||||
});
|
||||
|
||||
it('should return null when not provided', () => {
|
||||
const item = storage.defineItem<number>(`local:test`);
|
||||
|
||||
expect(item.defaultValue).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('types', () => {
|
||||
it('should define a nullable value when options are not passed', () => {
|
||||
const item = storage.defineItem<number>(`local:test`);
|
||||
expectTypeOf(item).toEqualTypeOf<WxtStorageItem<number | null, {}>>();
|
||||
});
|
||||
|
||||
it('should define a non-null value when options are passed with a nullish default value', () => {
|
||||
const item = storage.defineItem(`local:test`, {
|
||||
defaultValue: 123,
|
||||
});
|
||||
expectTypeOf(item).toEqualTypeOf<WxtStorageItem<number, {}>>();
|
||||
});
|
||||
|
||||
it('should define a nullable value when options are passed with null default value', () => {
|
||||
const item = storage.defineItem<number | null>(`local:test`, {
|
||||
defaultValue: null,
|
||||
});
|
||||
expectTypeOf(item).toEqualTypeOf<WxtStorageItem<number | null, {}>>();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+13
-4
@@ -1,10 +1,9 @@
|
||||
import { CAC, Command } from 'cac';
|
||||
import consola, { LogLevels } from 'consola';
|
||||
import { getInternalConfig } from '~/core/utils/building';
|
||||
import { exec } from '~/core/utils/exec';
|
||||
import { printHeader } from '~/core/utils/log';
|
||||
import { formatDuration } from '~/core/utils/time';
|
||||
import { ValidationError } from '~/core/utils/validation';
|
||||
import { registerWxt } from '~/core/wxt';
|
||||
|
||||
/**
|
||||
* Wrap an action handler to add a timer, error handling, and maybe enable debug mode.
|
||||
@@ -63,10 +62,18 @@ export function getArrayFromFlags<T>(
|
||||
}
|
||||
|
||||
const aliasCommandNames = new Set<string>();
|
||||
/**
|
||||
* @param base Command to add this one to
|
||||
* @param name The command name to add
|
||||
* @param alias The CLI tool being aliased
|
||||
* @param bin The CLI tool binary name. Usually the same as the alias
|
||||
* @param docsUrl URL to the docs for the aliased CLI tool
|
||||
*/
|
||||
export function createAliasedCommand(
|
||||
base: CAC,
|
||||
name: string,
|
||||
alias: string,
|
||||
bin: string,
|
||||
docsUrl: string,
|
||||
) {
|
||||
const aliasedCommand = base
|
||||
@@ -74,11 +81,13 @@ export function createAliasedCommand(
|
||||
.allowUnknownOptions()
|
||||
.action(async () => {
|
||||
try {
|
||||
const config = await getInternalConfig({}, 'build');
|
||||
await registerWxt('build');
|
||||
|
||||
const args = process.argv.slice(
|
||||
process.argv.indexOf(aliasedCommand.name) + 1,
|
||||
);
|
||||
await exec(config, alias, args, {
|
||||
const { execa } = await import('execa');
|
||||
await execa(bin, args, {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
} catch {
|
||||
|
||||
@@ -140,6 +140,7 @@ createAliasedCommand(
|
||||
cli,
|
||||
'submit',
|
||||
'publish-extension',
|
||||
'wxt-publish-extension',
|
||||
'https://www.npmjs.com/publish-browser-extension',
|
||||
);
|
||||
|
||||
|
||||
@@ -367,7 +367,7 @@ describe('Content Script UIs', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe.todo('after', () => {
|
||||
describe('after', () => {
|
||||
it('should append the UI after the anchor', () => {
|
||||
const ui = createIntegratedUi(ctx, {
|
||||
position: 'inline',
|
||||
|
||||
@@ -119,19 +119,18 @@ export async function createShadowRootUi<TMounted>(
|
||||
let mounted: TMounted;
|
||||
|
||||
const mount = () => {
|
||||
// Mount UI inside shadow root
|
||||
mounted = options.onMount(uiContainer, shadow, shadowHost);
|
||||
|
||||
// Add shadow root element to DOM
|
||||
mountUi(shadowHost, options);
|
||||
applyPosition(shadowHost, shadow.querySelector('html'), options);
|
||||
// Mount UI inside shadow root
|
||||
mounted = options.onMount(uiContainer, shadow, shadowHost);
|
||||
};
|
||||
|
||||
const remove = () => {
|
||||
// Detatch shadow root from DOM
|
||||
shadowHost.remove();
|
||||
// Cleanup mounted state
|
||||
options.onRemove?.(mounted);
|
||||
// Detatch shadow root from DOM
|
||||
shadowHost.remove();
|
||||
// Remove children from uiContainer
|
||||
while (uiContainer.lastChild)
|
||||
uiContainer.removeChild(uiContainer.lastChild);
|
||||
@@ -211,20 +210,16 @@ function mountUi(
|
||||
anchor.append(root);
|
||||
break;
|
||||
case 'first':
|
||||
if (anchor.firstChild) {
|
||||
anchor.insertBefore(root, anchor.firstChild);
|
||||
} else {
|
||||
anchor.append(root);
|
||||
}
|
||||
anchor.prepend(root);
|
||||
break;
|
||||
case 'replace':
|
||||
anchor.replaceWith(root);
|
||||
break;
|
||||
case 'after':
|
||||
anchor.replaceWith(anchor, root);
|
||||
anchor.parentElement?.insertBefore(root, anchor.nextElementSibling);
|
||||
break;
|
||||
case 'before':
|
||||
anchor.replaceWith(root, anchor);
|
||||
anchor.parentElement?.insertBefore(root, anchor);
|
||||
break;
|
||||
default:
|
||||
options.append(anchor, root);
|
||||
|
||||
+5
-3
@@ -1,5 +1,6 @@
|
||||
import { BuildOutput, InlineConfig } from '~/types';
|
||||
import { getInternalConfig, internalBuild } from './utils/building';
|
||||
import { internalBuild } from './utils/building';
|
||||
import { registerWxt } from './wxt';
|
||||
|
||||
/**
|
||||
* Bundles the extension for production. Returns a promise of the build result. Discovers the `wxt.config.ts` file in
|
||||
@@ -15,6 +16,7 @@ import { getInternalConfig, internalBuild } from './utils/building';
|
||||
* })
|
||||
*/
|
||||
export async function build(config?: InlineConfig): Promise<BuildOutput> {
|
||||
const internalConfig = await getInternalConfig(config ?? {}, 'build');
|
||||
return await internalBuild(internalConfig);
|
||||
await registerWxt('build', config);
|
||||
|
||||
return await internalBuild();
|
||||
}
|
||||
|
||||
@@ -3,19 +3,22 @@ import {
|
||||
BuildStepOutput,
|
||||
Entrypoint,
|
||||
InlineConfig,
|
||||
InternalConfig,
|
||||
ResolvedConfig,
|
||||
UserConfig,
|
||||
VirtualEntrypointType,
|
||||
WxtBuilder,
|
||||
WxtBuilderServer,
|
||||
} from '~/types';
|
||||
import * as wxtPlugins from './plugins';
|
||||
import { getEntrypointBundlePath } from '~/core/utils/entrypoints';
|
||||
import {
|
||||
getEntrypointBundlePath,
|
||||
isHtmlEntrypoint,
|
||||
} from '~/core/utils/entrypoints';
|
||||
|
||||
export async function createViteBuilder(
|
||||
inlineConfig: InlineConfig,
|
||||
userConfig: UserConfig,
|
||||
wxtConfig: Omit<InternalConfig, 'builder'>,
|
||||
wxtConfig: Omit<ResolvedConfig, 'builder'>,
|
||||
): Promise<WxtBuilder> {
|
||||
const vite = await import('vite');
|
||||
|
||||
@@ -63,9 +66,10 @@ export async function createViteBuilder(
|
||||
wxtPlugins.noopBackground(),
|
||||
wxtPlugins.globals(wxtConfig),
|
||||
wxtPlugins.excludeBrowserPolyfill(wxtConfig),
|
||||
wxtPlugins.defineImportMeta(),
|
||||
);
|
||||
if (wxtConfig.analysis.enabled) {
|
||||
config.plugins.push(wxtPlugins.bundleAnalysis());
|
||||
config.plugins.push(wxtPlugins.bundleAnalysis(wxtConfig));
|
||||
}
|
||||
|
||||
return config;
|
||||
@@ -132,6 +136,9 @@ export async function createViteBuilder(
|
||||
* Return the basic config for building multiple entrypoints in [multi-page mode](https://vitejs.dev/guide/build.html#multi-page-app).
|
||||
*/
|
||||
const getMultiPageConfig = (entrypoints: Entrypoint[]): vite.InlineConfig => {
|
||||
const htmlEntrypoints = new Set(
|
||||
entrypoints.filter(isHtmlEntrypoint).map((e) => e.name),
|
||||
);
|
||||
return {
|
||||
mode: wxtConfig.mode,
|
||||
plugins: [
|
||||
@@ -147,10 +154,12 @@ export async function createViteBuilder(
|
||||
output: {
|
||||
// Include a hash to prevent conflicts
|
||||
chunkFileNames: 'chunks/[name]-[hash].js',
|
||||
// Place JS entrypoints in main directory without a hash. (popup.html & popup.js are
|
||||
// next to each other). The unique entrypoint name requirement prevents conflicts with
|
||||
// scripts of the same name
|
||||
entryFileNames: '[name].js',
|
||||
entryFileNames: ({ name }) => {
|
||||
// HTML main JS files go in the chunks folder
|
||||
if (htmlEntrypoints.has(name)) return 'chunks/[name]-[hash].js';
|
||||
// Scripts are output in the root folder
|
||||
return '[name].js';
|
||||
},
|
||||
// We can't control the "name", so we need a hash to prevent conflicts
|
||||
assetFileNames: 'assets/[name]-[hash].[ext]',
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Window } from 'happy-dom';
|
||||
import { pointToDevServer } from '../devHtmlPrerender';
|
||||
import {
|
||||
fakeDevServer,
|
||||
fakeInternalConfig,
|
||||
fakeResolvedConfig,
|
||||
} from '~/core/utils/testing/fake-objects';
|
||||
import { normalizePath } from '~/core/utils/paths';
|
||||
import { resolve } from 'node:path';
|
||||
@@ -36,7 +36,7 @@ describe('Dev HTML Prerender Plugin', () => {
|
||||
url: 'http://localhost',
|
||||
});
|
||||
const root = '/some/root';
|
||||
const config = fakeInternalConfig({
|
||||
const config = fakeResolvedConfig({
|
||||
root,
|
||||
alias: {
|
||||
'~local': '.',
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
import type * as vite from 'vite';
|
||||
import { visualizer } from 'rollup-plugin-visualizer';
|
||||
import { visualizer } from '@aklinker1/rollup-plugin-visualizer';
|
||||
import { ResolvedConfig } from '~/types';
|
||||
import path from 'node:path';
|
||||
|
||||
let increment = 0;
|
||||
|
||||
export function bundleAnalysis(): vite.Plugin {
|
||||
export function bundleAnalysis(
|
||||
config: Omit<ResolvedConfig, 'builder'>,
|
||||
): vite.Plugin {
|
||||
return visualizer({
|
||||
emitFile: true,
|
||||
template: 'raw-data',
|
||||
filename: `stats-${increment++}.json`,
|
||||
}) as vite.Plugin;
|
||||
filename: path.resolve(
|
||||
config.analysis.outputDir,
|
||||
`${config.analysis.outputName}-${increment++}.json`,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated FOR TESTING ONLY.
|
||||
*/
|
||||
export function resetBundleIncrement() {
|
||||
increment = 0;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type * as vite from 'vite';
|
||||
import { Entrypoint, InternalConfig } from '~/types';
|
||||
import { Entrypoint, ResolvedConfig } from '~/types';
|
||||
import { getEntrypointBundlePath } from '~/core/utils/entrypoints';
|
||||
|
||||
/**
|
||||
@@ -14,7 +14,7 @@ import { getEntrypointBundlePath } from '~/core/utils/entrypoints';
|
||||
*/
|
||||
export function cssEntrypoints(
|
||||
entrypoint: Entrypoint,
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
config: Omit<ResolvedConfig, 'builder'>,
|
||||
): vite.Plugin {
|
||||
return {
|
||||
name: 'wxt:css-entrypoint',
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Overrides definitions for `import.meta.*`
|
||||
*
|
||||
* - `import.meta.url`: Without this, background service workers crash trying to access
|
||||
* `document.location`, see https://github.com/wxt-dev/wxt/issues/392
|
||||
*/
|
||||
export function defineImportMeta() {
|
||||
return {
|
||||
name: 'wxt:define',
|
||||
config() {
|
||||
return {
|
||||
define: {
|
||||
// This works for all extension contexts, including background service worker
|
||||
'import.meta.url': 'self.location.href',
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type * as vite from 'vite';
|
||||
import { InternalConfig, WxtDevServer } from '~/types';
|
||||
import { ResolvedConfig, WxtDevServer } from '~/types';
|
||||
import { getEntrypointName } from '~/core/utils/entrypoints';
|
||||
import { parseHTML } from 'linkedom';
|
||||
import { dirname, relative, resolve } from 'node:path';
|
||||
@@ -12,7 +12,7 @@ let reactRefreshPreamble = '';
|
||||
* Pre-renders the HTML entrypoints when building the extension to connect to the dev server.
|
||||
*/
|
||||
export function devHtmlPrerender(
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
config: Omit<ResolvedConfig, 'builder'>,
|
||||
): vite.PluginOption {
|
||||
const htmlReloadId = '@wxt/reload-html';
|
||||
const resolvedHtmlReloadId = resolve(
|
||||
@@ -134,7 +134,7 @@ export function devHtmlPrerender(
|
||||
}
|
||||
|
||||
export function pointToDevServer(
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
config: Omit<ResolvedConfig, 'builder'>,
|
||||
server: WxtDevServer,
|
||||
id: string,
|
||||
document: Document,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Plugin } from 'vite';
|
||||
import { InternalConfig } from '~/types';
|
||||
import { ResolvedConfig } from '~/types';
|
||||
|
||||
/**
|
||||
* Defines global constants about the dev server. Helps scripts connect to the server's web socket.
|
||||
*/
|
||||
export function devServerGlobals(
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
config: Omit<ResolvedConfig, 'builder'>,
|
||||
): Plugin {
|
||||
return {
|
||||
name: 'wxt:dev-server-globals',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Plugin } from 'vite';
|
||||
import { InternalConfig } from '~/types';
|
||||
import { ResolvedConfig } from '~/types';
|
||||
import { fetchCached } from '~/core/utils/network';
|
||||
|
||||
/**
|
||||
@@ -9,7 +9,7 @@ import { fetchCached } from '~/core/utils/network';
|
||||
* @example
|
||||
* import "url:https://google-tagmanager.com/gtag?id=XYZ";
|
||||
*/
|
||||
export function download(config: Omit<InternalConfig, 'builder'>): Plugin {
|
||||
export function download(config: Omit<ResolvedConfig, 'builder'>): Plugin {
|
||||
return {
|
||||
name: 'wxt:download',
|
||||
resolveId(id) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { InternalConfig } from '~/types';
|
||||
import { ResolvedConfig } from '~/types';
|
||||
import type * as vite from 'vite';
|
||||
|
||||
/**
|
||||
@@ -7,7 +7,7 @@ import type * as vite from 'vite';
|
||||
* virtual module.
|
||||
*/
|
||||
export function excludeBrowserPolyfill(
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
config: Omit<ResolvedConfig, 'builder'>,
|
||||
): vite.Plugin {
|
||||
const virtualId = 'virtual:wxt-webextension-polyfill-disabled';
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type * as vite from 'vite';
|
||||
import { InternalConfig } from '~/types';
|
||||
import { ResolvedConfig } from '~/types';
|
||||
import { getGlobals } from '~/core/utils/globals';
|
||||
|
||||
export function globals(
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
config: Omit<ResolvedConfig, 'builder'>,
|
||||
): vite.PluginOption {
|
||||
return {
|
||||
name: 'wxt:globals',
|
||||
|
||||
@@ -12,3 +12,4 @@ export * from './globals';
|
||||
export * from './webextensionPolyfillMock';
|
||||
export * from './excludeBrowserPolyfill';
|
||||
export * from './entrypointGroupGlobals';
|
||||
export * from './defineImportMeta';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type * as vite from 'vite';
|
||||
import { Entrypoint, InternalConfig } from '~/types';
|
||||
import { Entrypoint, ResolvedConfig } from '~/types';
|
||||
import { dirname, extname, resolve, join } from 'node:path';
|
||||
import { getEntrypointBundlePath } from '~/core/utils/entrypoints';
|
||||
import fs, { ensureDir } from 'fs-extra';
|
||||
@@ -20,7 +20,7 @@ import { normalizePath } from '~/core/utils/paths';
|
||||
*/
|
||||
export function multipageMove(
|
||||
entrypoints: Entrypoint[],
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
config: Omit<ResolvedConfig, 'builder'>,
|
||||
): vite.Plugin {
|
||||
return {
|
||||
name: 'wxt:multipage-move',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { InternalConfig } from '~/types';
|
||||
import { ResolvedConfig } from '~/types';
|
||||
import type * as vite from 'vite';
|
||||
|
||||
export function tsconfigPaths(
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
config: Omit<ResolvedConfig, 'builder'>,
|
||||
): vite.Plugin {
|
||||
return {
|
||||
name: 'wxt:aliases',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { createUnimport } from 'unimport';
|
||||
import { InternalConfig } from '~/types';
|
||||
import { getUnimportOptions } from '~/core/utils/unimport';
|
||||
import { ResolvedConfig } from '~/types';
|
||||
import type * as vite from 'vite';
|
||||
import { extname } from 'path';
|
||||
|
||||
@@ -17,9 +16,9 @@ const ENABLED_EXTENSIONS = new Set([
|
||||
* Inject any global imports defined by unimport
|
||||
*/
|
||||
export function unimport(
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
config: Omit<ResolvedConfig, 'builder'>,
|
||||
): vite.PluginOption {
|
||||
const options = getUnimportOptions(config);
|
||||
const options = config.imports;
|
||||
if (options === false) return [];
|
||||
|
||||
const unimport = createUnimport(options);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Plugin } from 'vite';
|
||||
import { InternalConfig, VirtualEntrypointType } from '~/types';
|
||||
import { ResolvedConfig, VirtualEntrypointType } from '~/types';
|
||||
import fs from 'fs-extra';
|
||||
import { resolve } from 'path';
|
||||
import { normalizePath } from '~/core/utils/paths';
|
||||
@@ -9,7 +9,7 @@ import { normalizePath } from '~/core/utils/paths';
|
||||
*/
|
||||
export function virtualEntrypoint(
|
||||
type: VirtualEntrypointType,
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
config: Omit<ResolvedConfig, 'builder'>,
|
||||
): Plugin {
|
||||
const virtualId = `virtual:wxt-${type}?`;
|
||||
const resolvedVirtualId = `\0${virtualId}`;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import path from 'node:path';
|
||||
import type * as vite from 'vite';
|
||||
import { InternalConfig } from '~/types';
|
||||
import { ResolvedConfig } from '~/types';
|
||||
|
||||
/**
|
||||
* Mock `webextension-polyfill` by inlining all dependencies that import it and adding a custom
|
||||
@@ -16,7 +16,7 @@ import { InternalConfig } from '~/types';
|
||||
* `npm list` and inline them automatically.
|
||||
*/
|
||||
export function webextensionPolyfillMock(
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
config: Omit<ResolvedConfig, 'builder'>,
|
||||
): vite.PluginOption {
|
||||
return {
|
||||
name: 'wxt:testing-inline-deps',
|
||||
|
||||
+72
-71
@@ -2,13 +2,12 @@ import {
|
||||
BuildStepOutput,
|
||||
EntrypointGroup,
|
||||
InlineConfig,
|
||||
InternalConfig,
|
||||
ServerInfo,
|
||||
WxtDevServer,
|
||||
} from '~/types';
|
||||
import {
|
||||
getEntrypointBundlePath,
|
||||
resolvePerBrowserOption,
|
||||
isHtmlEntrypoint,
|
||||
} from '~/core/utils/entrypoints';
|
||||
import {
|
||||
getContentScriptCssFiles,
|
||||
@@ -16,7 +15,6 @@ import {
|
||||
} from '~/core/utils/manifest';
|
||||
import {
|
||||
internalBuild,
|
||||
getInternalConfig,
|
||||
detectDevChanges,
|
||||
rebuild,
|
||||
findEntrypoints,
|
||||
@@ -26,6 +24,9 @@ import { consola } from 'consola';
|
||||
import { Mutex } from 'async-mutex';
|
||||
import pc from 'picocolors';
|
||||
import { relative } from 'node:path';
|
||||
import { registerWxt, wxt } from './wxt';
|
||||
import { unnormalizePath } from './utils/paths';
|
||||
import { mapWxtOptionsToRegisteredContentScript } from './utils/content-scripts';
|
||||
|
||||
/**
|
||||
* Creates a dev server and pre-builds all the files that need to exist before loading the extension.
|
||||
@@ -50,10 +51,18 @@ export async function createServer(
|
||||
|
||||
const buildAndOpenBrowser = async () => {
|
||||
// Build after starting the dev server so it can be used to transform HTML files
|
||||
server.currentOutput = await internalBuild(config);
|
||||
server.currentOutput = await internalBuild();
|
||||
|
||||
// Add file watchers for files not loaded by the dev server. See
|
||||
// https://github.com/wxt-dev/wxt/issues/428#issuecomment-1944731870
|
||||
try {
|
||||
server.watcher.add(getExternalOutputDependencies(server));
|
||||
} catch (err) {
|
||||
wxt.config.logger.warn('Failed to register additional file paths:', err);
|
||||
}
|
||||
|
||||
// Open browser after everything is ready to go.
|
||||
await runner.openBrowser(config);
|
||||
await runner.openBrowser();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -61,8 +70,8 @@ export async function createServer(
|
||||
*/
|
||||
const closeAndRecreateRunner = async () => {
|
||||
await runner.closeBrowser();
|
||||
config = await getLatestConfig();
|
||||
runner = await createExtensionRunner(config);
|
||||
await wxt.reloadConfig();
|
||||
runner = await createExtensionRunner();
|
||||
};
|
||||
|
||||
// Server instance must be created first so its reference can be added to the internal config used
|
||||
@@ -78,7 +87,7 @@ export async function createServer(
|
||||
currentOutput: undefined,
|
||||
async start() {
|
||||
await builderServer.listen();
|
||||
config.logger.success(`Started dev server @ ${serverInfo.origin}`);
|
||||
wxt.logger.success(`Started dev server @ ${serverInfo.origin}`);
|
||||
await buildAndOpenBrowser();
|
||||
},
|
||||
async stop() {
|
||||
@@ -92,8 +101,8 @@ export async function createServer(
|
||||
transformHtml(url, html, originalUrl) {
|
||||
return builderServer.transformHtml(url, html, originalUrl);
|
||||
},
|
||||
reloadContentScript(contentScript) {
|
||||
server.ws.send('wxt:reload-content-script', contentScript);
|
||||
reloadContentScript(payload) {
|
||||
server.ws.send('wxt:reload-content-script', payload);
|
||||
},
|
||||
reloadPage(path) {
|
||||
server.ws.send('wxt:reload-page', path);
|
||||
@@ -103,34 +112,26 @@ export async function createServer(
|
||||
},
|
||||
async restartBrowser() {
|
||||
await closeAndRecreateRunner();
|
||||
await runner.openBrowser(config);
|
||||
await runner.openBrowser();
|
||||
},
|
||||
};
|
||||
|
||||
const getLatestConfig = () =>
|
||||
getInternalConfig(inlineConfig ?? {}, 'serve', server);
|
||||
let config = await getLatestConfig();
|
||||
await registerWxt('serve', inlineConfig, server);
|
||||
|
||||
let [runner, builderServer] = await Promise.all([
|
||||
createExtensionRunner(config),
|
||||
config.builder.createServer(server),
|
||||
createExtensionRunner(),
|
||||
wxt.config.builder.createServer(server),
|
||||
]);
|
||||
|
||||
// Register content scripts for the first time after the background starts up since they're not
|
||||
// listed in the manifest
|
||||
server.ws.on('wxt:background-initialized', () => {
|
||||
if (server.currentOutput == null) return;
|
||||
reloadContentScripts(server.currentOutput.steps, config, server);
|
||||
reloadContentScripts(server.currentOutput.steps, server);
|
||||
});
|
||||
|
||||
// Listen for file changes and reload different parts of the extension accordingly
|
||||
const reloadOnChange = createFileReloader({
|
||||
server,
|
||||
getLatestConfig,
|
||||
updateConfig(newConfig) {
|
||||
config = newConfig;
|
||||
},
|
||||
});
|
||||
const reloadOnChange = createFileReloader(server);
|
||||
server.watcher.on('all', reloadOnChange);
|
||||
|
||||
return server;
|
||||
@@ -145,21 +146,15 @@ async function getPort(): Promise<number> {
|
||||
* Returns a function responsible for reloading different parts of the extension when a file
|
||||
* changes.
|
||||
*/
|
||||
function createFileReloader(options: {
|
||||
server: WxtDevServer;
|
||||
getLatestConfig: () => Promise<InternalConfig>;
|
||||
updateConfig: (config: InternalConfig) => void;
|
||||
}) {
|
||||
const { server, getLatestConfig, updateConfig } = options;
|
||||
function createFileReloader(server: WxtDevServer) {
|
||||
const fileChangedMutex = new Mutex();
|
||||
const changeQueue: Array<[string, string]> = [];
|
||||
|
||||
return async (event: string, path: string) => {
|
||||
const config = await getLatestConfig();
|
||||
updateConfig(config);
|
||||
await wxt.reloadConfig();
|
||||
|
||||
// Here, "path" is a non-normalized path (ie: C:\\users\\... instead of C:/users/...)
|
||||
if (path.startsWith(config.outBaseDir)) return;
|
||||
if (path.startsWith(wxt.config.outBaseDir)) return;
|
||||
changeQueue.push([event, path]);
|
||||
|
||||
await fileChangedMutex.runExclusive(async () => {
|
||||
@@ -170,36 +165,31 @@ function createFileReloader(options: {
|
||||
.map(([_, file]) => file);
|
||||
if (fileChanges.length === 0) return;
|
||||
|
||||
const changes = detectDevChanges(
|
||||
config,
|
||||
fileChanges,
|
||||
server.currentOutput,
|
||||
);
|
||||
const changes = detectDevChanges(fileChanges, server.currentOutput);
|
||||
if (changes.type === 'no-change') return;
|
||||
|
||||
if (changes.type === 'full-restart') {
|
||||
config.logger.info('Config changed, restarting server...');
|
||||
wxt.logger.info('Config changed, restarting server...');
|
||||
server.restart();
|
||||
return;
|
||||
}
|
||||
|
||||
if (changes.type === 'browser-restart') {
|
||||
config.logger.info('Runner config changed, restarting browser...');
|
||||
wxt.logger.info('Runner config changed, restarting browser...');
|
||||
server.restartBrowser();
|
||||
return;
|
||||
}
|
||||
|
||||
// Log the entrypoints that were effected
|
||||
config.logger.info(
|
||||
wxt.logger.info(
|
||||
`Changed: ${Array.from(new Set(fileChanges))
|
||||
.map((file) => pc.dim(relative(config.root, file)))
|
||||
.map((file) => pc.dim(relative(wxt.config.root, file)))
|
||||
.join(', ')}`,
|
||||
);
|
||||
|
||||
// Rebuild entrypoints on change
|
||||
const allEntrypoints = await findEntrypoints(config);
|
||||
const allEntrypoints = await findEntrypoints();
|
||||
const { output: newOutput } = await rebuild(
|
||||
config,
|
||||
allEntrypoints,
|
||||
// TODO: this excludes new entrypoints, so they're not built until the dev command is restarted
|
||||
changes.rebuildGroups,
|
||||
@@ -217,12 +207,11 @@ function createFileReloader(options: {
|
||||
const { reloadedNames } = reloadHtmlPages(
|
||||
changes.rebuildGroups,
|
||||
server,
|
||||
config,
|
||||
);
|
||||
consola.success(`Reloaded: ${getFilenameList(reloadedNames)}`);
|
||||
break;
|
||||
case 'content-script-reload':
|
||||
reloadContentScripts(changes.changedSteps, config, server);
|
||||
reloadContentScripts(changes.changedSteps, server);
|
||||
const rebuiltNames = changes.rebuildGroups
|
||||
.flat()
|
||||
.map((entry) => entry.name);
|
||||
@@ -236,37 +225,25 @@ function createFileReloader(options: {
|
||||
/**
|
||||
* From the server, tell the client to reload content scripts from the provided build step outputs.
|
||||
*/
|
||||
function reloadContentScripts(
|
||||
steps: BuildStepOutput[],
|
||||
config: InternalConfig,
|
||||
server: WxtDevServer,
|
||||
) {
|
||||
if (config.manifestVersion === 3) {
|
||||
function reloadContentScripts(steps: BuildStepOutput[], server: WxtDevServer) {
|
||||
if (wxt.config.manifestVersion === 3) {
|
||||
steps.forEach((step) => {
|
||||
if (server.currentOutput == null) return;
|
||||
|
||||
const entry = step.entrypoints;
|
||||
if (Array.isArray(entry) || entry.type !== 'content-script') return;
|
||||
|
||||
const js = [getEntrypointBundlePath(entry, config.outDir, '.js')];
|
||||
const js = [getEntrypointBundlePath(entry, wxt.config.outDir, '.js')];
|
||||
const cssMap = getContentScriptsCssMap(server.currentOutput, [entry]);
|
||||
const css = getContentScriptCssFiles([entry], cssMap);
|
||||
|
||||
server.reloadContentScript({
|
||||
allFrames: resolvePerBrowserOption(
|
||||
entry.options.allFrames,
|
||||
config.browser,
|
||||
registration: entry.options.registration,
|
||||
contentScript: mapWxtOptionsToRegisteredContentScript(
|
||||
entry.options,
|
||||
js,
|
||||
css,
|
||||
),
|
||||
excludeMatches: resolvePerBrowserOption(
|
||||
entry.options.excludeMatches,
|
||||
config.browser,
|
||||
),
|
||||
matches: resolvePerBrowserOption(entry.options.matches, config.browser),
|
||||
runAt: resolvePerBrowserOption(entry.options.runAt, config.browser),
|
||||
// @ts-expect-error: Chrome accepts this, not typed in webextension-polyfill (https://developer.chrome.com/docs/extensions/reference/scripting/#type-RegisteredContentScript)
|
||||
world: resolvePerBrowserOption(entry.options.world, config.browser),
|
||||
js,
|
||||
css,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
@@ -277,15 +254,12 @@ function reloadContentScripts(
|
||||
function reloadHtmlPages(
|
||||
groups: EntrypointGroup[],
|
||||
server: WxtDevServer,
|
||||
config: InternalConfig,
|
||||
): { reloadedNames: string[] } {
|
||||
// groups might contain other files like background/content scripts, and we only care about the HTMl pages
|
||||
const htmlEntries = groups
|
||||
.flat()
|
||||
.filter((entry) => entry.inputPath.endsWith('.html'));
|
||||
const htmlEntries = groups.flat().filter(isHtmlEntrypoint);
|
||||
|
||||
htmlEntries.forEach((entry) => {
|
||||
const path = getEntrypointBundlePath(entry, config.outDir, '.html');
|
||||
const path = getEntrypointBundlePath(entry, wxt.config.outDir, '.html');
|
||||
server.reloadPage(path);
|
||||
});
|
||||
|
||||
@@ -301,3 +275,30 @@ function getFilenameList(names: string[]): string {
|
||||
})
|
||||
.join(pc.dim(', '));
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on the current build output, return a list of files that are:
|
||||
* 1. Not in node_modules
|
||||
* 2. Not inside project root
|
||||
*/
|
||||
function getExternalOutputDependencies(server: WxtDevServer) {
|
||||
return (
|
||||
server.currentOutput?.steps
|
||||
.flatMap((step, i) => {
|
||||
if (Array.isArray(step.entrypoints) && i === 0) {
|
||||
// Dev server is already watching all HTML/esm files
|
||||
return [];
|
||||
}
|
||||
|
||||
return step.chunks.flatMap((chunk) => {
|
||||
if (chunk.type === 'asset') return [];
|
||||
return chunk.moduleIds;
|
||||
});
|
||||
})
|
||||
.filter(
|
||||
(file) => !file.includes('node_modules') && !file.startsWith('\x00'),
|
||||
)
|
||||
.map(unnormalizePath)
|
||||
.filter((file) => !file.startsWith(wxt.config.root)) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
+16
-17
@@ -90,26 +90,25 @@ interface Template {
|
||||
|
||||
async function listTemplates(): Promise<Template[]> {
|
||||
try {
|
||||
const res = await fetch(
|
||||
'https://api.github.com/repos/wxt-dev/wxt/contents/templates',
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
},
|
||||
},
|
||||
);
|
||||
const res = await fetch('https://ungh.cc/repos/wxt-dev/wxt/files/main');
|
||||
if (res.status >= 300)
|
||||
throw Error(`Request failed with status ${res.status} ${res.statusText}`);
|
||||
|
||||
const data = (await res.json()) as Array<{
|
||||
type: 'file' | 'dir';
|
||||
name: string;
|
||||
path: string;
|
||||
}>;
|
||||
return data
|
||||
.filter((item: any) => item.type === 'dir')
|
||||
.map((item) => ({ name: item.name, path: item.path }))
|
||||
const data = (await res.json()) as {
|
||||
meta: {
|
||||
sha: string;
|
||||
};
|
||||
files: Array<{
|
||||
path: string;
|
||||
mode: string;
|
||||
sha: string;
|
||||
size: number;
|
||||
}>;
|
||||
};
|
||||
return data.files
|
||||
.map((item) => item.path.match(/templates\/(.+)\/package\.json/)?.[1])
|
||||
.filter((name) => name != null)
|
||||
.map((name) => ({ name: name!, path: `templates/${name}` }))
|
||||
.sort((l, r) => {
|
||||
const lWeight = TEMPLATE_SORT_WEIGHT[l.name] ?? Number.MAX_SAFE_INTEGER;
|
||||
const rWeight = TEMPLATE_SORT_WEIGHT[r.name] ?? Number.MAX_SAFE_INTEGER;
|
||||
|
||||
+6
-10
@@ -1,15 +1,11 @@
|
||||
import { InlineConfig } from '~/types';
|
||||
import {
|
||||
findEntrypoints,
|
||||
generateTypesDir,
|
||||
getInternalConfig,
|
||||
} from '~/core/utils/building';
|
||||
import { findEntrypoints, generateTypesDir } from '~/core/utils/building';
|
||||
import { registerWxt, wxt } from './wxt';
|
||||
|
||||
export async function prepare(config: InlineConfig) {
|
||||
const internalConfig = await getInternalConfig(config, 'build');
|
||||
await registerWxt('build', config);
|
||||
wxt.logger.info('Generating types...');
|
||||
|
||||
internalConfig.logger.info('Generating types...');
|
||||
|
||||
const entrypoints = await findEntrypoints(internalConfig);
|
||||
await generateTypesDir(entrypoints, internalConfig);
|
||||
const entrypoints = await findEntrypoints();
|
||||
await generateTypesDir(entrypoints);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { createExtensionRunner } from '..';
|
||||
import { fakeInternalConfig } from '~/core/utils/testing/fake-objects';
|
||||
import { setFakeWxt } from '~/core/utils/testing/fake-objects';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { createSafariRunner } from '../safari';
|
||||
import { createWslRunner } from '../wsl';
|
||||
@@ -26,54 +26,62 @@ const createWebExtRunnerMock = vi.mocked(createWebExtRunner);
|
||||
|
||||
describe('createExtensionRunner', () => {
|
||||
it('should return a Safari runner when browser is "safari"', async () => {
|
||||
const config = fakeInternalConfig({
|
||||
browser: 'safari',
|
||||
setFakeWxt({
|
||||
config: {
|
||||
browser: 'safari',
|
||||
},
|
||||
});
|
||||
const safariRunner = mock<ExtensionRunner>();
|
||||
createSafariRunnerMock.mockReturnValue(safariRunner);
|
||||
|
||||
await expect(createExtensionRunner(config)).resolves.toBe(safariRunner);
|
||||
await expect(createExtensionRunner()).resolves.toBe(safariRunner);
|
||||
});
|
||||
|
||||
it('should return a WSL runner when `is-wsl` is true', async () => {
|
||||
isWslMock.mockResolvedValueOnce(true);
|
||||
const config = fakeInternalConfig({
|
||||
browser: 'chrome',
|
||||
setFakeWxt({
|
||||
config: {
|
||||
browser: 'chrome',
|
||||
},
|
||||
});
|
||||
const wslRunner = mock<ExtensionRunner>();
|
||||
createWslRunnerMock.mockReturnValue(wslRunner);
|
||||
|
||||
await expect(createExtensionRunner(config)).resolves.toBe(wslRunner);
|
||||
await expect(createExtensionRunner()).resolves.toBe(wslRunner);
|
||||
});
|
||||
|
||||
it('should return a manual runner when `runner.disabled` is true', async () => {
|
||||
isWslMock.mockResolvedValueOnce(false);
|
||||
const config = fakeInternalConfig({
|
||||
browser: 'chrome',
|
||||
runnerConfig: {
|
||||
config: {
|
||||
disabled: true,
|
||||
setFakeWxt({
|
||||
config: {
|
||||
browser: 'chrome',
|
||||
runnerConfig: {
|
||||
config: {
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const manualRunner = mock<ExtensionRunner>();
|
||||
createManualRunnerMock.mockReturnValue(manualRunner);
|
||||
|
||||
await expect(createExtensionRunner(config)).resolves.toBe(manualRunner);
|
||||
await expect(createExtensionRunner()).resolves.toBe(manualRunner);
|
||||
});
|
||||
|
||||
it('should return a web-ext runner otherwise', async () => {
|
||||
const config = fakeInternalConfig({
|
||||
browser: 'chrome',
|
||||
runnerConfig: {
|
||||
config: {
|
||||
disabled: undefined,
|
||||
setFakeWxt({
|
||||
config: {
|
||||
browser: 'chrome',
|
||||
runnerConfig: {
|
||||
config: {
|
||||
disabled: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const manualRunner = mock<ExtensionRunner>();
|
||||
createWebExtRunnerMock.mockReturnValue(manualRunner);
|
||||
|
||||
await expect(createExtensionRunner(config)).resolves.toBe(manualRunner);
|
||||
await expect(createExtensionRunner()).resolves.toBe(manualRunner);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { InternalConfig, ExtensionRunner } from '~/types';
|
||||
import { ExtensionRunner } from '~/types';
|
||||
import { createWslRunner } from './wsl';
|
||||
import { createWebExtRunner } from './web-ext';
|
||||
import { createSafariRunner } from './safari';
|
||||
import { createManualRunner } from './manual';
|
||||
import { isWsl } from '~/core/utils/wsl';
|
||||
import { wxt } from '../wxt';
|
||||
|
||||
export async function createExtensionRunner(
|
||||
config: InternalConfig,
|
||||
): Promise<ExtensionRunner> {
|
||||
if (config.browser === 'safari') return createSafariRunner();
|
||||
export async function createExtensionRunner(): Promise<ExtensionRunner> {
|
||||
if (wxt.config.browser === 'safari') return createSafariRunner();
|
||||
|
||||
if (await isWsl()) return createWslRunner();
|
||||
if (config.runnerConfig.config?.disabled) return createManualRunner();
|
||||
if (wxt.config.runnerConfig.config?.disabled) return createManualRunner();
|
||||
|
||||
return createWebExtRunner();
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { ExtensionRunner } from '~/types';
|
||||
import { relative } from 'node:path';
|
||||
import { wxt } from '../wxt';
|
||||
|
||||
/**
|
||||
* The manual runner tells the user to load the unpacked extension manually.
|
||||
*/
|
||||
export function createManualRunner(): ExtensionRunner {
|
||||
return {
|
||||
async openBrowser(config) {
|
||||
config.logger.info(
|
||||
async openBrowser() {
|
||||
wxt.logger.info(
|
||||
`Load "${relative(
|
||||
process.cwd(),
|
||||
config.outDir,
|
||||
wxt.config.outDir,
|
||||
)}" as an unpacked extension manually`,
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { ExtensionRunner } from '~/types';
|
||||
import { relative } from 'node:path';
|
||||
import { wxt } from '../wxt';
|
||||
|
||||
/**
|
||||
* The Safari runner just logs a warning message because `web-ext` doesn't work with Safari.
|
||||
*/
|
||||
export function createSafariRunner(): ExtensionRunner {
|
||||
return {
|
||||
async openBrowser(config) {
|
||||
config.logger.warn(
|
||||
async openBrowser() {
|
||||
wxt.logger.warn(
|
||||
`Cannot Safari using web-ext. Load "${relative(
|
||||
process.cwd(),
|
||||
config.outDir,
|
||||
wxt.config.outDir,
|
||||
)}" as an unpacked extension manually`,
|
||||
);
|
||||
},
|
||||
|
||||
+17
-12
@@ -2,6 +2,7 @@ import type { WebExtRunInstance } from 'web-ext-run';
|
||||
import { ExtensionRunner } from '~/types';
|
||||
import { formatDuration } from '../utils/time';
|
||||
import defu from 'defu';
|
||||
import { wxt } from '../wxt';
|
||||
|
||||
/**
|
||||
* Create an `ExtensionRunner` backed by `web-ext`.
|
||||
@@ -10,10 +11,13 @@ export function createWebExtRunner(): ExtensionRunner {
|
||||
let runner: WebExtRunInstance | undefined;
|
||||
|
||||
return {
|
||||
async openBrowser(config) {
|
||||
async openBrowser() {
|
||||
const startTime = Date.now();
|
||||
|
||||
if (config.browser === 'firefox' && config.manifestVersion === 3) {
|
||||
if (
|
||||
wxt.config.browser === 'firefox' &&
|
||||
wxt.config.manifestVersion === 3
|
||||
) {
|
||||
throw Error(
|
||||
'Dev mode does not support Firefox MV3. For alternatives, see https://github.com/wxt-dev/wxt/issues/230#issuecomment-1806881653',
|
||||
);
|
||||
@@ -22,16 +26,16 @@ export function createWebExtRunner(): ExtensionRunner {
|
||||
// Use WXT's logger instead of web-ext's built-in one.
|
||||
const webExtLogger = await import('web-ext-run/util/logger');
|
||||
webExtLogger.consoleStream.write = ({ level, msg, name }) => {
|
||||
if (level >= ERROR_LOG_LEVEL) config.logger.error(name, msg);
|
||||
if (level >= WARN_LOG_LEVEL) config.logger.warn(msg);
|
||||
if (level >= ERROR_LOG_LEVEL) wxt.logger.error(name, msg);
|
||||
if (level >= WARN_LOG_LEVEL) wxt.logger.warn(msg);
|
||||
};
|
||||
|
||||
const wxtUserConfig = config.runnerConfig.config;
|
||||
const wxtUserConfig = wxt.config.runnerConfig.config;
|
||||
const userConfig = {
|
||||
console: wxtUserConfig?.openConsole,
|
||||
devtools: wxtUserConfig?.openDevtools,
|
||||
startUrl: wxtUserConfig?.startUrls,
|
||||
...(config.browser === 'firefox'
|
||||
...(wxt.config.browser === 'firefox'
|
||||
? {
|
||||
firefox: wxtUserConfig?.binaries?.firefox,
|
||||
firefoxProfile: wxtUserConfig?.firefoxProfile,
|
||||
@@ -39,7 +43,7 @@ export function createWebExtRunner(): ExtensionRunner {
|
||||
args: wxtUserConfig?.firefoxArgs,
|
||||
}
|
||||
: {
|
||||
chromiumBinary: wxtUserConfig?.binaries?.[config.browser],
|
||||
chromiumBinary: wxtUserConfig?.binaries?.[wxt.config.browser],
|
||||
chromiumProfile: wxtUserConfig?.chromiumProfile,
|
||||
chromiumPref: defu(
|
||||
wxtUserConfig?.chromiumPref,
|
||||
@@ -51,8 +55,9 @@ export function createWebExtRunner(): ExtensionRunner {
|
||||
|
||||
const finalConfig = {
|
||||
...userConfig,
|
||||
target: config.browser === 'firefox' ? 'firefox-desktop' : 'chromium',
|
||||
sourceDir: config.outDir,
|
||||
target:
|
||||
wxt.config.browser === 'firefox' ? 'firefox-desktop' : 'chromium',
|
||||
sourceDir: wxt.config.outDir,
|
||||
// WXT handles reloads, so disable auto-reload behaviors in web-ext
|
||||
noReload: true,
|
||||
noInput: true,
|
||||
@@ -61,14 +66,14 @@ export function createWebExtRunner(): ExtensionRunner {
|
||||
// Don't call `process.exit(0)` after starting web-ext
|
||||
shouldExitProgram: false,
|
||||
};
|
||||
config.logger.debug('web-ext config:', finalConfig);
|
||||
config.logger.debug('web-ext options:', options);
|
||||
wxt.logger.debug('web-ext config:', finalConfig);
|
||||
wxt.logger.debug('web-ext options:', options);
|
||||
|
||||
const webExt = await import('web-ext-run');
|
||||
runner = await webExt.default.cmd.run(finalConfig, options);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
config.logger.success(`Opened browser in ${formatDuration(duration)}`);
|
||||
wxt.logger.success(`Opened browser in ${formatDuration(duration)}`);
|
||||
},
|
||||
|
||||
async closeBrowser() {
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { ExtensionRunner } from '~/types';
|
||||
import { relative } from 'node:path';
|
||||
import { wxt } from '../wxt';
|
||||
|
||||
/**
|
||||
* The WSL runner just logs a warning message because `web-ext` doesn't work in WSL.
|
||||
*/
|
||||
export function createWslRunner(): ExtensionRunner {
|
||||
return {
|
||||
async openBrowser(config) {
|
||||
config.logger.warn(
|
||||
async openBrowser() {
|
||||
wxt.logger.warn(
|
||||
`Cannot open browser when using WSL. Load "${relative(
|
||||
process.cwd(),
|
||||
config.outDir,
|
||||
wxt.config.outDir,
|
||||
)}" as an unpacked extension manually`,
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { describe, expect, it, beforeEach } from 'vitest';
|
||||
import { hashContentScriptOptions } from '~/core/utils/content-scripts';
|
||||
import { fakeInternalConfig } from '~/core/utils/testing/fake-objects';
|
||||
import { setFakeWxt } from '~/core/utils/testing/fake-objects';
|
||||
|
||||
describe('Content Script Utils', () => {
|
||||
beforeEach(() => {
|
||||
setFakeWxt();
|
||||
});
|
||||
|
||||
describe('hashContentScriptOptions', () => {
|
||||
it('should return a string containing all the options with defaults applied', () => {
|
||||
const hash = hashContentScriptOptions(
|
||||
{ matches: [] },
|
||||
fakeInternalConfig(),
|
||||
);
|
||||
const hash = hashContentScriptOptions({ matches: [] });
|
||||
|
||||
expect(hash).toMatchInlineSnapshot(
|
||||
`"[["all_frames",false],["exclude_globs",[]],["exclude_matches",[]],["include_globs",[]],["match_about_blank",false],["match_origin_as_fallback",false],["matches",[]],["run_at","document_idle"],["world","ISOLATED"]]"`,
|
||||
@@ -16,21 +17,15 @@ describe('Content Script Utils', () => {
|
||||
});
|
||||
|
||||
it('should be consistent regardless of the object ordering and default values', () => {
|
||||
const hash1 = hashContentScriptOptions(
|
||||
{
|
||||
allFrames: true,
|
||||
matches: ['*://google.com/*', '*://duckduckgo.com/*'],
|
||||
matchAboutBlank: false,
|
||||
},
|
||||
fakeInternalConfig(),
|
||||
);
|
||||
const hash2 = hashContentScriptOptions(
|
||||
{
|
||||
matches: ['*://duckduckgo.com/*', '*://google.com/*'],
|
||||
allFrames: true,
|
||||
},
|
||||
fakeInternalConfig(),
|
||||
);
|
||||
const hash1 = hashContentScriptOptions({
|
||||
allFrames: true,
|
||||
matches: ['*://google.com/*', '*://duckduckgo.com/*'],
|
||||
matchAboutBlank: false,
|
||||
});
|
||||
const hash2 = hashContentScriptOptions({
|
||||
matches: ['*://duckduckgo.com/*', '*://google.com/*'],
|
||||
allFrames: true,
|
||||
});
|
||||
|
||||
expect(hash1).toBe(hash2);
|
||||
});
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { generateManifest, stripPathFromMatchPattern } from '../manifest';
|
||||
import {
|
||||
fakeArray,
|
||||
fakeBackgroundEntrypoint,
|
||||
fakeBuildOutput,
|
||||
fakeContentScriptEntrypoint,
|
||||
fakeEntrypoint,
|
||||
fakeInternalConfig,
|
||||
fakeManifestCommand,
|
||||
fakeOptionsEntrypoint,
|
||||
fakePopupEntrypoint,
|
||||
fakeSidepanelEntrypoint,
|
||||
setFakeWxt,
|
||||
} from '../testing/fake-objects';
|
||||
import { Manifest } from 'webextension-polyfill';
|
||||
import {
|
||||
@@ -17,11 +19,16 @@ import {
|
||||
Entrypoint,
|
||||
OutputAsset,
|
||||
} from '~/types';
|
||||
import { wxt } from '../../wxt';
|
||||
|
||||
const outDir = '/output';
|
||||
const contentScriptOutDir = '/output/content-scripts';
|
||||
|
||||
describe('Manifest Utils', () => {
|
||||
beforeEach(() => {
|
||||
setFakeWxt();
|
||||
});
|
||||
|
||||
describe('generateManifest', () => {
|
||||
describe('popup', () => {
|
||||
type ActionType = 'browser_action' | 'page_action';
|
||||
@@ -41,9 +48,12 @@ describe('Manifest Utils', () => {
|
||||
it('should include an action for mv3', async () => {
|
||||
const popup = popupEntrypoint();
|
||||
const buildOutput = fakeBuildOutput();
|
||||
const config = fakeInternalConfig({
|
||||
manifestVersion: 3,
|
||||
outDir,
|
||||
|
||||
setFakeWxt({
|
||||
config: {
|
||||
manifestVersion: 3,
|
||||
outDir,
|
||||
},
|
||||
});
|
||||
const expected: Partial<Manifest.WebExtensionManifest> = {
|
||||
action: {
|
||||
@@ -56,7 +66,6 @@ describe('Manifest Utils', () => {
|
||||
const { manifest: actual } = await generateManifest(
|
||||
[popup],
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
|
||||
expect(actual).toMatchObject(expected);
|
||||
@@ -74,9 +83,11 @@ describe('Manifest Utils', () => {
|
||||
async ({ inputType, expectedType }) => {
|
||||
const popup = popupEntrypoint(inputType);
|
||||
const buildOutput = fakeBuildOutput();
|
||||
const config = fakeInternalConfig({
|
||||
manifestVersion: 2,
|
||||
outDir,
|
||||
setFakeWxt({
|
||||
config: {
|
||||
manifestVersion: 2,
|
||||
outDir,
|
||||
},
|
||||
});
|
||||
const expected = {
|
||||
default_icon: popup.options.defaultIcon,
|
||||
@@ -87,7 +98,6 @@ describe('Manifest Utils', () => {
|
||||
const { manifest: actual } = await generateManifest(
|
||||
[popup],
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
|
||||
expect(actual[expectedType]).toEqual(expected);
|
||||
@@ -98,24 +108,22 @@ describe('Manifest Utils', () => {
|
||||
describe('action without popup', () => {
|
||||
it('should respect the action field in the manifest without a popup', async () => {
|
||||
const buildOutput = fakeBuildOutput();
|
||||
const config = fakeInternalConfig({
|
||||
outDir,
|
||||
manifest: {
|
||||
action: {
|
||||
default_icon: 'icon-16.png',
|
||||
default_title: 'Example title',
|
||||
setFakeWxt({
|
||||
config: {
|
||||
outDir,
|
||||
manifest: {
|
||||
action: {
|
||||
default_icon: 'icon-16.png',
|
||||
default_title: 'Example title',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const expected: Partial<Manifest.WebExtensionManifest> = {
|
||||
action: config.manifest.action,
|
||||
action: wxt.config.manifest.action,
|
||||
};
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
[],
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
const { manifest: actual } = await generateManifest([], buildOutput);
|
||||
|
||||
expect(actual).toMatchObject(expected);
|
||||
});
|
||||
@@ -132,10 +140,12 @@ describe('Manifest Utils', () => {
|
||||
});
|
||||
|
||||
it('should include a options_ui and chrome_style for chrome', async () => {
|
||||
const config = fakeInternalConfig({
|
||||
manifestVersion: 3,
|
||||
outDir,
|
||||
browser: 'chrome',
|
||||
setFakeWxt({
|
||||
config: {
|
||||
manifestVersion: 3,
|
||||
outDir,
|
||||
browser: 'chrome',
|
||||
},
|
||||
});
|
||||
const buildOutput = fakeBuildOutput();
|
||||
const expected = {
|
||||
@@ -147,17 +157,18 @@ describe('Manifest Utils', () => {
|
||||
const { manifest: actual } = await generateManifest(
|
||||
[options],
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
|
||||
expect(actual.options_ui).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should include a options_ui and browser_style for firefox', async () => {
|
||||
const config = fakeInternalConfig({
|
||||
manifestVersion: 3,
|
||||
browser: 'firefox',
|
||||
outDir,
|
||||
setFakeWxt({
|
||||
config: {
|
||||
manifestVersion: 3,
|
||||
browser: 'firefox',
|
||||
outDir,
|
||||
},
|
||||
});
|
||||
const buildOutput = fakeBuildOutput();
|
||||
const expected = {
|
||||
@@ -169,7 +180,6 @@ describe('Manifest Utils', () => {
|
||||
const { manifest: actual } = await generateManifest(
|
||||
[options],
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
|
||||
expect(actual.options_ui).toEqual(expected);
|
||||
@@ -189,10 +199,12 @@ describe('Manifest Utils', () => {
|
||||
it.each(['chrome', 'safari'])(
|
||||
'should include a service worker and type for %s',
|
||||
async (browser) => {
|
||||
const config = fakeInternalConfig({
|
||||
outDir,
|
||||
manifestVersion: 3,
|
||||
browser,
|
||||
setFakeWxt({
|
||||
config: {
|
||||
outDir,
|
||||
manifestVersion: 3,
|
||||
browser,
|
||||
},
|
||||
});
|
||||
const buildOutput = fakeBuildOutput();
|
||||
const expected = {
|
||||
@@ -203,7 +215,6 @@ describe('Manifest Utils', () => {
|
||||
const { manifest: actual } = await generateManifest(
|
||||
[background],
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
|
||||
expect(actual.background).toEqual(expected);
|
||||
@@ -211,10 +222,12 @@ describe('Manifest Utils', () => {
|
||||
);
|
||||
|
||||
it('should include a background script and type for firefox', async () => {
|
||||
const config = fakeInternalConfig({
|
||||
outDir,
|
||||
manifestVersion: 3,
|
||||
browser: 'firefox',
|
||||
setFakeWxt({
|
||||
config: {
|
||||
outDir,
|
||||
manifestVersion: 3,
|
||||
browser: 'firefox',
|
||||
},
|
||||
});
|
||||
const buildOutput = fakeBuildOutput();
|
||||
const expected = {
|
||||
@@ -225,7 +238,6 @@ describe('Manifest Utils', () => {
|
||||
const { manifest: actual } = await generateManifest(
|
||||
[background],
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
|
||||
expect(actual.background).toEqual(expected);
|
||||
@@ -236,10 +248,12 @@ describe('Manifest Utils', () => {
|
||||
it.each(['chrome', 'safari'])(
|
||||
'should include scripts and persistent for %s',
|
||||
async (browser) => {
|
||||
const config = fakeInternalConfig({
|
||||
outDir,
|
||||
manifestVersion: 2,
|
||||
browser,
|
||||
setFakeWxt({
|
||||
config: {
|
||||
outDir,
|
||||
manifestVersion: 2,
|
||||
browser,
|
||||
},
|
||||
});
|
||||
const buildOutput = fakeBuildOutput();
|
||||
const expected = {
|
||||
@@ -250,7 +264,6 @@ describe('Manifest Utils', () => {
|
||||
const { manifest: actual } = await generateManifest(
|
||||
[background],
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
|
||||
expect(actual.background).toEqual(expected);
|
||||
@@ -258,10 +271,12 @@ describe('Manifest Utils', () => {
|
||||
);
|
||||
|
||||
it('should include a background script and persistent for firefox mv2', async () => {
|
||||
const config = fakeInternalConfig({
|
||||
outDir,
|
||||
manifestVersion: 2,
|
||||
browser: 'firefox',
|
||||
setFakeWxt({
|
||||
config: {
|
||||
outDir,
|
||||
manifestVersion: 2,
|
||||
browser: 'firefox',
|
||||
},
|
||||
});
|
||||
const buildOutput = fakeBuildOutput();
|
||||
const expected = {
|
||||
@@ -272,7 +287,6 @@ describe('Manifest Utils', () => {
|
||||
const { manifest: actual } = await generateManifest(
|
||||
[background],
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
|
||||
expect(actual.background).toEqual(expected);
|
||||
@@ -293,12 +307,10 @@ describe('Manifest Utils', () => {
|
||||
{ type: 'asset', fileName: 'icons/128x128.png' },
|
||||
],
|
||||
});
|
||||
const config = fakeInternalConfig();
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
|
||||
expect(actual.icons).toEqual({
|
||||
@@ -319,12 +331,10 @@ describe('Manifest Utils', () => {
|
||||
{ type: 'asset', fileName: 'icon-16.jpeg' },
|
||||
],
|
||||
});
|
||||
const config = fakeInternalConfig();
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
|
||||
expect(actual.icons).toBeUndefined();
|
||||
@@ -346,16 +356,17 @@ describe('Manifest Utils', () => {
|
||||
32: 'logo-32.png',
|
||||
48: 'logo-48.png',
|
||||
};
|
||||
const config = fakeInternalConfig({
|
||||
manifest: {
|
||||
icons: expected,
|
||||
setFakeWxt({
|
||||
config: {
|
||||
manifest: {
|
||||
icons: expected,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
|
||||
expect(actual.icons).toEqual(expected);
|
||||
@@ -440,10 +451,12 @@ describe('Manifest Utils', () => {
|
||||
};
|
||||
|
||||
const entrypoints = [cs1, cs2, cs3, cs4, cs5];
|
||||
const config = fakeInternalConfig({
|
||||
command: 'build',
|
||||
outDir,
|
||||
manifestVersion: 3,
|
||||
setFakeWxt({
|
||||
config: {
|
||||
command: 'build',
|
||||
outDir,
|
||||
manifestVersion: 3,
|
||||
},
|
||||
});
|
||||
const buildOutput: Omit<BuildOutput, 'manifest'> = {
|
||||
publicAssets: [],
|
||||
@@ -459,7 +472,6 @@ describe('Manifest Utils', () => {
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
|
||||
expect(actual.content_scripts).toContainEqual({
|
||||
@@ -509,18 +521,19 @@ describe('Manifest Utils', () => {
|
||||
|
||||
const entrypoints = [cs];
|
||||
const buildOutput = fakeBuildOutput();
|
||||
const config = fakeInternalConfig({
|
||||
outDir,
|
||||
command: 'build',
|
||||
manifest: {
|
||||
content_scripts: [userContentScript],
|
||||
setFakeWxt({
|
||||
config: {
|
||||
outDir,
|
||||
command: 'build',
|
||||
manifest: {
|
||||
content_scripts: [userContentScript],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
|
||||
expect(actual.content_scripts).toContainEqual(userContentScript);
|
||||
@@ -552,15 +565,16 @@ describe('Manifest Utils', () => {
|
||||
publicAssets: [],
|
||||
steps: [{ entrypoints: cs, chunks: [styles] }],
|
||||
};
|
||||
const config = fakeInternalConfig({
|
||||
outDir,
|
||||
command: 'build',
|
||||
setFakeWxt({
|
||||
config: {
|
||||
outDir,
|
||||
command: 'build',
|
||||
},
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
|
||||
expect(actual.content_scripts).toEqual([
|
||||
@@ -597,15 +611,16 @@ describe('Manifest Utils', () => {
|
||||
publicAssets: [],
|
||||
steps: [{ entrypoints: cs, chunks: [styles] }],
|
||||
};
|
||||
const config = fakeInternalConfig({
|
||||
outDir,
|
||||
command: 'build',
|
||||
setFakeWxt({
|
||||
config: {
|
||||
outDir,
|
||||
command: 'build',
|
||||
},
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
|
||||
expect(actual.content_scripts).toEqual([
|
||||
@@ -639,16 +654,17 @@ describe('Manifest Utils', () => {
|
||||
publicAssets: [],
|
||||
steps: [{ entrypoints: cs, chunks: [styles] }],
|
||||
};
|
||||
const config = fakeInternalConfig({
|
||||
outDir,
|
||||
command: 'build',
|
||||
manifestVersion: 3,
|
||||
setFakeWxt({
|
||||
config: {
|
||||
outDir,
|
||||
command: 'build',
|
||||
manifestVersion: 3,
|
||||
},
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
|
||||
expect(actual.web_accessible_resources).toEqual([
|
||||
@@ -681,16 +697,17 @@ describe('Manifest Utils', () => {
|
||||
publicAssets: [],
|
||||
steps: [{ entrypoints: cs, chunks: [styles] }],
|
||||
};
|
||||
const config = fakeInternalConfig({
|
||||
outDir,
|
||||
command: 'build',
|
||||
manifestVersion: 2,
|
||||
setFakeWxt({
|
||||
config: {
|
||||
outDir,
|
||||
command: 'build',
|
||||
manifestVersion: 2,
|
||||
},
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
|
||||
expect(actual.web_accessible_resources).toEqual([
|
||||
@@ -720,16 +737,17 @@ describe('Manifest Utils', () => {
|
||||
publicAssets: [],
|
||||
steps: [{ entrypoints: cs, chunks: [styles] }],
|
||||
};
|
||||
const config = fakeInternalConfig({
|
||||
outDir,
|
||||
command: 'build',
|
||||
manifestVersion: 3,
|
||||
setFakeWxt({
|
||||
config: {
|
||||
outDir,
|
||||
command: 'build',
|
||||
manifestVersion: 3,
|
||||
},
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
|
||||
expect(actual.web_accessible_resources).toEqual([
|
||||
@@ -740,6 +758,136 @@ describe('Manifest Utils', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('registration', () => {
|
||||
it('should throw an error when registration=runtime for MV2', async () => {
|
||||
const cs: ContentScriptEntrypoint = fakeContentScriptEntrypoint({
|
||||
options: {
|
||||
registration: 'runtime',
|
||||
},
|
||||
});
|
||||
|
||||
const entrypoints = [cs];
|
||||
const buildOutput: Omit<BuildOutput, 'manifest'> = {
|
||||
publicAssets: [],
|
||||
steps: [{ entrypoints: cs, chunks: [] }],
|
||||
};
|
||||
setFakeWxt({
|
||||
config: {
|
||||
manifestVersion: 2,
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
generateManifest(entrypoints, buildOutput),
|
||||
).rejects.toThrowError();
|
||||
});
|
||||
|
||||
it('should add host_permissions instead of content_scripts when registration=runtime', async () => {
|
||||
const cs: ContentScriptEntrypoint = {
|
||||
type: 'content-script',
|
||||
name: 'one',
|
||||
inputPath: 'entrypoints/one.content.ts',
|
||||
outputDir: contentScriptOutDir,
|
||||
options: {
|
||||
matches: ['*://google.com/*'],
|
||||
registration: 'runtime',
|
||||
},
|
||||
skipped: false,
|
||||
};
|
||||
const styles: OutputAsset = {
|
||||
type: 'asset',
|
||||
fileName: 'content-scripts/one.css',
|
||||
};
|
||||
|
||||
const entrypoints = [cs];
|
||||
const buildOutput: Omit<BuildOutput, 'manifest'> = {
|
||||
publicAssets: [],
|
||||
steps: [{ entrypoints: cs, chunks: [styles] }],
|
||||
};
|
||||
setFakeWxt({
|
||||
config: {
|
||||
manifestVersion: 3,
|
||||
outDir,
|
||||
command: 'build',
|
||||
},
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
);
|
||||
|
||||
expect(actual.content_scripts).toEqual([]);
|
||||
expect(actual.host_permissions).toEqual(['*://google.com/*']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('sidepanel', () => {
|
||||
it.each(['chrome', 'safari', 'edge'])(
|
||||
'should include a side_panel ignoring all options for %s',
|
||||
async (browser) => {
|
||||
const sidepanel = fakeSidepanelEntrypoint({
|
||||
outputDir: outDir,
|
||||
});
|
||||
const buildOutput = fakeBuildOutput();
|
||||
|
||||
setFakeWxt({
|
||||
config: {
|
||||
manifestVersion: 3,
|
||||
browser,
|
||||
outDir,
|
||||
},
|
||||
});
|
||||
const expected = {
|
||||
side_panel: {
|
||||
default_path: 'sidepanel.html',
|
||||
},
|
||||
};
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
[sidepanel],
|
||||
buildOutput,
|
||||
);
|
||||
|
||||
expect(actual).toMatchObject(expected);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(['firefox'])(
|
||||
'should include a sidebar_action for %s',
|
||||
async (browser) => {
|
||||
const sidepanel = fakeSidepanelEntrypoint({
|
||||
outputDir: outDir,
|
||||
});
|
||||
const buildOutput = fakeBuildOutput();
|
||||
|
||||
setFakeWxt({
|
||||
config: {
|
||||
manifestVersion: 3,
|
||||
browser,
|
||||
outDir,
|
||||
},
|
||||
});
|
||||
const expected = {
|
||||
sidebar_action: {
|
||||
default_panel: 'sidepanel.html',
|
||||
open_at_install: sidepanel.options.openAtInstall,
|
||||
default_title: sidepanel.options.defaultTitle,
|
||||
default_icon: sidepanel.options.defaultIcon,
|
||||
browser_style: sidepanel.options.browserStyle,
|
||||
},
|
||||
};
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
[sidepanel],
|
||||
buildOutput,
|
||||
);
|
||||
|
||||
expect(actual).toMatchObject(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('web_accessible_resources', () => {
|
||||
@@ -765,21 +913,22 @@ describe('Manifest Utils', () => {
|
||||
publicAssets: [],
|
||||
steps: [{ entrypoints: cs, chunks: [styles] }],
|
||||
};
|
||||
const config = fakeInternalConfig({
|
||||
outDir,
|
||||
command: 'build',
|
||||
manifestVersion: 3,
|
||||
manifest: {
|
||||
web_accessible_resources: [
|
||||
{ resources: ['one.png'], matches: ['*://one.com/*'] },
|
||||
],
|
||||
setFakeWxt({
|
||||
config: {
|
||||
outDir,
|
||||
command: 'build',
|
||||
manifestVersion: 3,
|
||||
manifest: {
|
||||
web_accessible_resources: [
|
||||
{ resources: ['one.png'], matches: ['*://one.com/*'] },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
|
||||
expect(actual.web_accessible_resources).toEqual([
|
||||
@@ -813,19 +962,20 @@ describe('Manifest Utils', () => {
|
||||
publicAssets: [],
|
||||
steps: [{ entrypoints: cs, chunks: [styles] }],
|
||||
};
|
||||
const config = fakeInternalConfig({
|
||||
outDir,
|
||||
command: 'build',
|
||||
manifestVersion: 2,
|
||||
manifest: {
|
||||
web_accessible_resources: ['one.png'],
|
||||
setFakeWxt({
|
||||
config: {
|
||||
outDir,
|
||||
command: 'build',
|
||||
manifestVersion: 2,
|
||||
manifest: {
|
||||
web_accessible_resources: ['one.png'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
|
||||
expect(actual.web_accessible_resources).toEqual([
|
||||
@@ -833,6 +983,55 @@ describe('Manifest Utils', () => {
|
||||
'content-scripts/one.css',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should convert mv3 items to mv2 strings automatically', async () => {
|
||||
setFakeWxt({
|
||||
config: {
|
||||
outDir,
|
||||
manifestVersion: 2,
|
||||
manifest: {
|
||||
web_accessible_resources: [
|
||||
{
|
||||
matches: ['*://*/*'],
|
||||
resources: ['/icon-128.png'],
|
||||
},
|
||||
{
|
||||
matches: ['https://google.com'],
|
||||
resources: ['/icon-128.png', '/icon-32.png'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
[],
|
||||
fakeBuildOutput(),
|
||||
);
|
||||
|
||||
expect(actual.web_accessible_resources).toEqual([
|
||||
'/icon-128.png',
|
||||
'/icon-32.png',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should convert mv2 strings to mv3 items with a warning automatically', async () => {
|
||||
setFakeWxt({
|
||||
config: {
|
||||
outDir,
|
||||
manifestVersion: 3,
|
||||
manifest: {
|
||||
web_accessible_resources: ['/icon.svg'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(() =>
|
||||
generateManifest([], fakeBuildOutput()),
|
||||
).rejects.toThrow(
|
||||
'Non-MV3 web_accessible_resources detected: ["/icon.svg"]. When manually defining web_accessible_resources, define them as MV3 objects ({ matches: [...], resources: [...] }), and WXT will automatically convert them to MV2 when necessary.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformManifest option', () => {
|
||||
@@ -840,9 +1039,11 @@ describe('Manifest Utils', () => {
|
||||
const entrypoints: Entrypoint[] = [];
|
||||
const buildOutput = fakeBuildOutput();
|
||||
const newAuthor = 'Custom Author';
|
||||
const config = fakeInternalConfig({
|
||||
transformManifest(manifest: any) {
|
||||
manifest.author = newAuthor;
|
||||
setFakeWxt({
|
||||
config: {
|
||||
transformManifest(manifest: any) {
|
||||
manifest.author = newAuthor;
|
||||
},
|
||||
},
|
||||
});
|
||||
const expected = {
|
||||
@@ -852,7 +1053,6 @@ describe('Manifest Utils', () => {
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
|
||||
expect(actual).toMatchObject(expected);
|
||||
@@ -867,18 +1067,19 @@ describe('Manifest Utils', () => {
|
||||
const versionName = '1.0.0-alpha1';
|
||||
const entrypoints: Entrypoint[] = [];
|
||||
const buildOutput = fakeBuildOutput();
|
||||
const config = fakeInternalConfig({
|
||||
browser,
|
||||
manifest: {
|
||||
version,
|
||||
version_name: versionName,
|
||||
setFakeWxt({
|
||||
config: {
|
||||
browser,
|
||||
manifest: {
|
||||
version,
|
||||
version_name: versionName,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
|
||||
expect(actual.version).toBe(version);
|
||||
@@ -893,18 +1094,19 @@ describe('Manifest Utils', () => {
|
||||
const versionName = '1.0.0-alpha1';
|
||||
const entrypoints: Entrypoint[] = [];
|
||||
const buildOutput = fakeBuildOutput();
|
||||
const config = fakeInternalConfig({
|
||||
browser,
|
||||
manifest: {
|
||||
version,
|
||||
version_name: versionName,
|
||||
setFakeWxt({
|
||||
config: {
|
||||
browser,
|
||||
manifest: {
|
||||
version,
|
||||
version_name: versionName,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
|
||||
expect(actual.version).toBe(version);
|
||||
@@ -918,18 +1120,19 @@ describe('Manifest Utils', () => {
|
||||
const version = '1.0.0';
|
||||
const entrypoints: Entrypoint[] = [];
|
||||
const buildOutput = fakeBuildOutput();
|
||||
const config = fakeInternalConfig({
|
||||
browser,
|
||||
manifest: {
|
||||
version,
|
||||
version_name: version,
|
||||
setFakeWxt({
|
||||
config: {
|
||||
browser,
|
||||
manifest: {
|
||||
version,
|
||||
version_name: version,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
|
||||
expect(actual.version).toBe(version);
|
||||
@@ -940,23 +1143,24 @@ describe('Manifest Utils', () => {
|
||||
it('should log a warning if the version could not be detected', async () => {
|
||||
const entrypoints: Entrypoint[] = [];
|
||||
const buildOutput = fakeBuildOutput();
|
||||
const config = fakeInternalConfig({
|
||||
manifest: {
|
||||
// @ts-ignore: Purposefully removing version from fake object
|
||||
version: null,
|
||||
setFakeWxt({
|
||||
config: {
|
||||
manifest: {
|
||||
// @ts-ignore: Purposefully removing version from fake object
|
||||
version: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
|
||||
expect(actual.version).toBe('0.0.0');
|
||||
expect(actual.version_name).toBeUndefined();
|
||||
expect(config.logger.warn).toBeCalledTimes(1);
|
||||
expect(config.logger.warn).toBeCalledWith(
|
||||
expect(wxt.logger.warn).toBeCalledTimes(1);
|
||||
expect(wxt.logger.warn).toBeCalledWith(
|
||||
expect.stringContaining('Extension version not found'),
|
||||
);
|
||||
});
|
||||
@@ -972,14 +1176,15 @@ describe('Manifest Utils', () => {
|
||||
};
|
||||
|
||||
it('should include a command for reloading the extension during development', async () => {
|
||||
const config = fakeInternalConfig({ command: 'serve' });
|
||||
setFakeWxt({
|
||||
config: { command: 'serve' },
|
||||
});
|
||||
const output = fakeBuildOutput();
|
||||
const entrypoints = fakeArray(fakeEntrypoint);
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
output,
|
||||
config,
|
||||
);
|
||||
|
||||
expect(actual.commands).toEqual({
|
||||
@@ -988,10 +1193,12 @@ describe('Manifest Utils', () => {
|
||||
});
|
||||
|
||||
it('should customize the reload commands key binding if passing a custom command', async () => {
|
||||
const config = fakeInternalConfig({
|
||||
command: 'serve',
|
||||
dev: {
|
||||
reloadCommand: 'Ctrl+E',
|
||||
setFakeWxt({
|
||||
config: {
|
||||
command: 'serve',
|
||||
dev: {
|
||||
reloadCommand: 'Ctrl+E',
|
||||
},
|
||||
},
|
||||
});
|
||||
const output = fakeBuildOutput();
|
||||
@@ -1000,7 +1207,6 @@ describe('Manifest Utils', () => {
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
output,
|
||||
config,
|
||||
);
|
||||
|
||||
expect(actual.commands).toEqual({
|
||||
@@ -1014,10 +1220,12 @@ describe('Manifest Utils', () => {
|
||||
});
|
||||
|
||||
it("should not include the reload command when it's been disabled", async () => {
|
||||
const config = fakeInternalConfig({
|
||||
command: 'serve',
|
||||
dev: {
|
||||
reloadCommand: false,
|
||||
setFakeWxt({
|
||||
config: {
|
||||
command: 'serve',
|
||||
dev: {
|
||||
reloadCommand: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
const output = fakeBuildOutput();
|
||||
@@ -1026,7 +1234,6 @@ describe('Manifest Utils', () => {
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
output,
|
||||
config,
|
||||
);
|
||||
|
||||
expect(actual.commands).toBeUndefined();
|
||||
@@ -1035,11 +1242,13 @@ describe('Manifest Utils', () => {
|
||||
it('should not override any existing commands when adding the one to reload the extension', async () => {
|
||||
const customCommandName = 'custom-command';
|
||||
const customCommand = fakeManifestCommand();
|
||||
const config = fakeInternalConfig({
|
||||
command: 'serve',
|
||||
manifest: {
|
||||
commands: {
|
||||
[customCommandName]: customCommand,
|
||||
setFakeWxt({
|
||||
config: {
|
||||
command: 'serve',
|
||||
manifest: {
|
||||
commands: {
|
||||
[customCommandName]: customCommand,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1049,7 +1258,6 @@ describe('Manifest Utils', () => {
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
output,
|
||||
config,
|
||||
);
|
||||
|
||||
expect(actual.commands).toEqual({
|
||||
@@ -1065,9 +1273,11 @@ describe('Manifest Utils', () => {
|
||||
command3: fakeManifestCommand(),
|
||||
command4: fakeManifestCommand(),
|
||||
};
|
||||
const config = fakeInternalConfig({
|
||||
command: 'serve',
|
||||
manifest: { commands },
|
||||
setFakeWxt({
|
||||
config: {
|
||||
command: 'serve',
|
||||
manifest: { commands },
|
||||
},
|
||||
});
|
||||
const output = fakeBuildOutput();
|
||||
const entrypoints = fakeArray(fakeEntrypoint);
|
||||
@@ -1075,7 +1285,6 @@ describe('Manifest Utils', () => {
|
||||
const { manifest: actual, warnings } = await generateManifest(
|
||||
entrypoints,
|
||||
output,
|
||||
config,
|
||||
);
|
||||
|
||||
expect(actual.commands).toEqual(commands);
|
||||
@@ -1083,14 +1292,15 @@ describe('Manifest Utils', () => {
|
||||
});
|
||||
|
||||
it('should not include the command when building an extension', async () => {
|
||||
const config = fakeInternalConfig({ command: 'build' });
|
||||
setFakeWxt({
|
||||
config: { command: 'build' },
|
||||
});
|
||||
const output = fakeBuildOutput();
|
||||
const entrypoints = fakeArray(fakeEntrypoint);
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
output,
|
||||
config,
|
||||
);
|
||||
|
||||
expect(actual.commands).toBeUndefined();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { getPackageJson } from '../package';
|
||||
import { fakeInternalConfig } from '../testing/fake-objects';
|
||||
import { setFakeWxt } from '../testing/fake-objects';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { Logger } from '~/types';
|
||||
|
||||
@@ -8,7 +8,11 @@ describe('Package JSON Utils', () => {
|
||||
describe('getPackageJson', () => {
|
||||
it('should return the package.json inside <root>/package.json', async () => {
|
||||
const root = process.cwd(); // WXT project directory
|
||||
const actual = await getPackageJson(fakeInternalConfig({ root }));
|
||||
setFakeWxt({
|
||||
config: { root },
|
||||
});
|
||||
|
||||
const actual = await getPackageJson();
|
||||
|
||||
expect(actual).toMatchObject({
|
||||
name: 'wxt',
|
||||
@@ -18,7 +22,12 @@ describe('Package JSON Utils', () => {
|
||||
it("should return an empty object when <root>/package.json doesn't exist", async () => {
|
||||
const root = '/some/path/that/does/not/exist';
|
||||
const logger = mock<Logger>();
|
||||
const actual = await getPackageJson(fakeInternalConfig({ root, logger }));
|
||||
setFakeWxt({
|
||||
config: { root, logger },
|
||||
logger,
|
||||
});
|
||||
|
||||
const actual = await getPackageJson();
|
||||
|
||||
expect(actual).toEqual({});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { DevModeChange, detectDevChanges } from '~/core/utils/building';
|
||||
import {
|
||||
fakeBackgroundEntrypoint,
|
||||
@@ -10,14 +10,19 @@ import {
|
||||
fakePopupEntrypoint,
|
||||
fakeOutputAsset,
|
||||
fakeOutputChunk,
|
||||
fakeInternalConfig,
|
||||
fakeWxt,
|
||||
setFakeWxt,
|
||||
} from '~/core/utils/testing/fake-objects';
|
||||
import { BuildOutput, BuildStepOutput } from '~/types';
|
||||
import { setWxtForTesting } from '../../../wxt';
|
||||
|
||||
describe('Detect Dev Changes', () => {
|
||||
beforeEach(() => {
|
||||
setWxtForTesting(fakeWxt());
|
||||
});
|
||||
|
||||
describe('No changes', () => {
|
||||
it("should return 'no-change' when the changed file isn't used by any of the entrypoints", () => {
|
||||
const config = fakeInternalConfig();
|
||||
const changes = ['/some/path.ts'];
|
||||
const currentOutput: BuildOutput = {
|
||||
manifest: fakeManifest(),
|
||||
@@ -34,7 +39,7 @@ describe('Detect Dev Changes', () => {
|
||||
],
|
||||
};
|
||||
|
||||
const actual = detectDevChanges(config, changes, currentOutput);
|
||||
const actual = detectDevChanges(changes, currentOutput);
|
||||
|
||||
expect(actual).toEqual({ type: 'no-change' });
|
||||
});
|
||||
@@ -43,9 +48,11 @@ describe('Detect Dev Changes', () => {
|
||||
describe('wxt.config.ts', () => {
|
||||
it("should return 'full-restart' when one of the changed files is the config file", () => {
|
||||
const configFile = '/root/wxt.config.ts';
|
||||
const config = fakeInternalConfig({
|
||||
userConfigMetadata: {
|
||||
configFile,
|
||||
setFakeWxt({
|
||||
config: {
|
||||
userConfigMetadata: {
|
||||
configFile,
|
||||
},
|
||||
},
|
||||
});
|
||||
const changes = ['/root/src/public/image.svg', configFile];
|
||||
@@ -58,7 +65,7 @@ describe('Detect Dev Changes', () => {
|
||||
type: 'full-restart',
|
||||
};
|
||||
|
||||
const actual = detectDevChanges(config, changes, currentOutput);
|
||||
const actual = detectDevChanges(changes, currentOutput);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
@@ -67,9 +74,11 @@ describe('Detect Dev Changes', () => {
|
||||
describe('web-ext.config.ts', () => {
|
||||
it("should return 'browser-restart' when one of the changed files is the config file", () => {
|
||||
const runnerFile = '/root/web-ext.config.ts';
|
||||
const config = fakeInternalConfig({
|
||||
runnerConfig: {
|
||||
configFile: runnerFile,
|
||||
setFakeWxt({
|
||||
config: {
|
||||
runnerConfig: {
|
||||
configFile: runnerFile,
|
||||
},
|
||||
},
|
||||
});
|
||||
const changes = ['/root/src/public/image.svg', runnerFile];
|
||||
@@ -82,7 +91,7 @@ describe('Detect Dev Changes', () => {
|
||||
type: 'browser-restart',
|
||||
};
|
||||
|
||||
const actual = detectDevChanges(config, changes, currentOutput);
|
||||
const actual = detectDevChanges(changes, currentOutput);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
@@ -90,7 +99,6 @@ describe('Detect Dev Changes', () => {
|
||||
|
||||
describe('Public Assets', () => {
|
||||
it("should return 'extension-reload' without any groups to rebuild when the changed file is a public asset", () => {
|
||||
const config = fakeInternalConfig();
|
||||
const changes = ['/root/src/public/image.svg'];
|
||||
const asset1 = fakeOutputAsset({
|
||||
fileName: 'image.svg',
|
||||
@@ -112,7 +120,7 @@ describe('Detect Dev Changes', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const actual = detectDevChanges(config, changes, currentOutput);
|
||||
const actual = detectDevChanges(changes, currentOutput);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
@@ -120,7 +128,6 @@ describe('Detect Dev Changes', () => {
|
||||
|
||||
describe('Background', () => {
|
||||
it("should rebuild the background and reload the extension when the changed file in it's chunks' `moduleIds` field", () => {
|
||||
const config = fakeInternalConfig();
|
||||
const changedPath = '/root/utils/shared.ts';
|
||||
const contentScript = fakeContentScriptEntrypoint({
|
||||
inputPath: '/root/overlay.content.ts',
|
||||
@@ -160,7 +167,7 @@ describe('Detect Dev Changes', () => {
|
||||
rebuildGroups: [background],
|
||||
};
|
||||
|
||||
const actual = detectDevChanges(config, [changedPath], currentOutput);
|
||||
const actual = detectDevChanges([changedPath], currentOutput);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
@@ -168,7 +175,6 @@ describe('Detect Dev Changes', () => {
|
||||
|
||||
describe('HTML Pages', () => {
|
||||
it('should detect changes to entrypoints/<name>.html files', async () => {
|
||||
const config = fakeInternalConfig();
|
||||
const changedPath = '/root/page1.html';
|
||||
const htmlPage1 = fakePopupEntrypoint({
|
||||
inputPath: changedPath,
|
||||
@@ -212,13 +218,12 @@ describe('Detect Dev Changes', () => {
|
||||
rebuildGroups: [[htmlPage1, htmlPage2]],
|
||||
};
|
||||
|
||||
const actual = detectDevChanges(config, [changedPath], currentOutput);
|
||||
const actual = detectDevChanges([changedPath], currentOutput);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should detect changes to entrypoints/<name>/index.html files', async () => {
|
||||
const config = fakeInternalConfig();
|
||||
const changedPath = '/root/page1/index.html';
|
||||
const htmlPage1 = fakePopupEntrypoint({
|
||||
inputPath: changedPath,
|
||||
@@ -262,7 +267,7 @@ describe('Detect Dev Changes', () => {
|
||||
rebuildGroups: [[htmlPage1, htmlPage2]],
|
||||
};
|
||||
|
||||
const actual = detectDevChanges(config, [changedPath], currentOutput);
|
||||
const actual = detectDevChanges([changedPath], currentOutput);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
@@ -270,7 +275,6 @@ describe('Detect Dev Changes', () => {
|
||||
|
||||
describe('Content Scripts', () => {
|
||||
it('should rebuild then reload only the effected content scripts', async () => {
|
||||
const config = fakeInternalConfig();
|
||||
const changedPath = '/root/utils/shared.ts';
|
||||
const script1 = fakeContentScriptEntrypoint({
|
||||
inputPath: '/root/overlay1.content/index.ts',
|
||||
@@ -322,7 +326,7 @@ describe('Detect Dev Changes', () => {
|
||||
rebuildGroups: [script1, script3],
|
||||
};
|
||||
|
||||
const actual = detectDevChanges(config, [changedPath], currentOutput);
|
||||
const actual = detectDevChanges([changedPath], currentOutput);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import {
|
||||
BackgroundEntrypoint,
|
||||
BackgroundEntrypointOptions,
|
||||
BaseEntrypointOptions,
|
||||
ContentScriptEntrypoint,
|
||||
GenericEntrypoint,
|
||||
OptionsEntrypoint,
|
||||
PopupEntrypoint,
|
||||
SidepanelEntrypoint,
|
||||
} from '~/types';
|
||||
import { resolve } from 'path';
|
||||
import { findEntrypoints } from '../find-entrypoints';
|
||||
import fs from 'fs-extra';
|
||||
import { importEntrypointFile } from '../import-entrypoint';
|
||||
import glob from 'fast-glob';
|
||||
import { fakeInternalConfig } from '~/core/utils/testing/fake-objects';
|
||||
import {
|
||||
fakeResolvedConfig,
|
||||
setFakeWxt,
|
||||
} from '~/core/utils/testing/fake-objects';
|
||||
import { unnormalizePath } from '~/core/utils/paths';
|
||||
|
||||
vi.mock('../import-entrypoint');
|
||||
@@ -26,7 +32,7 @@ const readFileMock = vi.mocked(
|
||||
);
|
||||
|
||||
describe('findEntrypoints', () => {
|
||||
const config = fakeInternalConfig({
|
||||
const config = fakeResolvedConfig({
|
||||
manifestVersion: 3,
|
||||
root: '/',
|
||||
entrypointsDir: resolve('/src/entrypoints'),
|
||||
@@ -34,6 +40,10 @@ describe('findEntrypoints', () => {
|
||||
command: 'build',
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
setFakeWxt({ config });
|
||||
});
|
||||
|
||||
it.each<[string, string, PopupEntrypoint]>([
|
||||
[
|
||||
'popup.html',
|
||||
@@ -83,7 +93,7 @@ describe('findEntrypoints', () => {
|
||||
globMock.mockResolvedValueOnce([path]);
|
||||
readFileMock.mockResolvedValueOnce(content);
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
expect(entrypoints).toHaveLength(1);
|
||||
expect(entrypoints[0]).toEqual(expected);
|
||||
@@ -136,7 +146,7 @@ describe('findEntrypoints', () => {
|
||||
globMock.mockResolvedValueOnce([path]);
|
||||
readFileMock.mockResolvedValueOnce(content);
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
expect(entrypoints).toHaveLength(1);
|
||||
expect(entrypoints[0]).toEqual(expected);
|
||||
@@ -203,14 +213,11 @@ describe('findEntrypoints', () => {
|
||||
globMock.mockResolvedValueOnce([path]);
|
||||
importEntrypointFileMock.mockResolvedValue(options);
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
expect(entrypoints).toHaveLength(1);
|
||||
expect(entrypoints[0]).toEqual({ ...expected, options });
|
||||
expect(importEntrypointFileMock).toBeCalledWith(
|
||||
expected.inputPath,
|
||||
config,
|
||||
);
|
||||
expect(importEntrypointFileMock).toBeCalledWith(expected.inputPath);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -238,56 +245,138 @@ describe('findEntrypoints', () => {
|
||||
])(
|
||||
'should find and load background entrypoint config from %s',
|
||||
async (path, expected) => {
|
||||
const options: BackgroundEntrypoint['options'] = {
|
||||
const options: BackgroundEntrypointOptions = {
|
||||
type: 'module',
|
||||
};
|
||||
globMock.mockResolvedValueOnce([path]);
|
||||
importEntrypointFileMock.mockResolvedValue(options);
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
expect(entrypoints).toHaveLength(1);
|
||||
expect(entrypoints[0]).toEqual({ ...expected, options });
|
||||
expect(importEntrypointFileMock).toBeCalledWith(
|
||||
expected.inputPath,
|
||||
config,
|
||||
);
|
||||
expect(importEntrypointFileMock).toBeCalledWith(expected.inputPath);
|
||||
},
|
||||
);
|
||||
|
||||
it.each<[string, string, SidepanelEntrypoint]>([
|
||||
[
|
||||
'sidepanel.html',
|
||||
`
|
||||
<html>
|
||||
<head>
|
||||
<title>Default Title</title>
|
||||
<meta name="manifest.default_icon" content="{ '16': '/icon/16.png' }" />
|
||||
<meta name="manifest.open_at_install" content="true" />
|
||||
</head>
|
||||
</html>
|
||||
`,
|
||||
{
|
||||
type: 'sidepanel',
|
||||
name: 'sidepanel',
|
||||
inputPath: resolve(config.entrypointsDir, 'sidepanel.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {
|
||||
defaultTitle: 'Default Title',
|
||||
defaultIcon: { '16': '/icon/16.png' },
|
||||
openAtInstall: true,
|
||||
},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
'sidepanel/index.html',
|
||||
`<html></html>`,
|
||||
{
|
||||
type: 'sidepanel',
|
||||
name: 'sidepanel',
|
||||
inputPath: resolve(config.entrypointsDir, 'sidepanel/index.html'),
|
||||
options: {},
|
||||
outputDir: config.outDir,
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
'named.sidepanel.html',
|
||||
`<html></html>`,
|
||||
{
|
||||
type: 'sidepanel',
|
||||
name: 'named',
|
||||
inputPath: resolve(config.entrypointsDir, 'named.sidepanel.html'),
|
||||
options: {},
|
||||
outputDir: config.outDir,
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
'named.sidepanel/index.html',
|
||||
`<html></html>`,
|
||||
{
|
||||
type: 'sidepanel',
|
||||
name: 'named',
|
||||
inputPath: resolve(config.entrypointsDir, 'named.sidepanel/index.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
])(
|
||||
'should find and load sidepanel entrypoint config from %s',
|
||||
async (path, content, expected) => {
|
||||
globMock.mockResolvedValueOnce([path]);
|
||||
readFileMock.mockResolvedValueOnce(content);
|
||||
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
expect(entrypoints).toHaveLength(1);
|
||||
expect(entrypoints[0]).toEqual(expected);
|
||||
},
|
||||
);
|
||||
|
||||
it('should remove type=module from MV2 background scripts', async () => {
|
||||
const config = fakeInternalConfig({ manifestVersion: 2 });
|
||||
const options: BackgroundEntrypoint['options'] = {
|
||||
setFakeWxt({
|
||||
config: {
|
||||
manifestVersion: 2,
|
||||
},
|
||||
});
|
||||
const options: BackgroundEntrypointOptions = {
|
||||
type: 'module',
|
||||
};
|
||||
globMock.mockResolvedValueOnce(['background.ts']);
|
||||
importEntrypointFileMock.mockResolvedValue(options);
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
expect(entrypoints[0].options).toEqual({});
|
||||
});
|
||||
|
||||
it('should allow type=module for MV3 background service workers', async () => {
|
||||
const config = fakeInternalConfig({ manifestVersion: 3 });
|
||||
const options: BackgroundEntrypoint['options'] = {
|
||||
setFakeWxt({
|
||||
config: {
|
||||
manifestVersion: 3,
|
||||
},
|
||||
});
|
||||
const options: BackgroundEntrypointOptions = {
|
||||
type: 'module',
|
||||
};
|
||||
globMock.mockResolvedValueOnce(['background.ts']);
|
||||
importEntrypointFileMock.mockResolvedValue(options);
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
expect(entrypoints[0].options).toEqual(options);
|
||||
});
|
||||
|
||||
it("should include a virtual background script so dev reloading works when there isn't a background entrypoint defined by the user", async () => {
|
||||
setFakeWxt({
|
||||
config: {
|
||||
...config,
|
||||
command: 'serve',
|
||||
},
|
||||
});
|
||||
globMock.mockResolvedValueOnce(['popup.html']);
|
||||
|
||||
const entrypoints = await findEntrypoints({
|
||||
...config,
|
||||
command: 'serve',
|
||||
});
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
expect(entrypoints).toHaveLength(2);
|
||||
expect(entrypoints).toContainEqual({
|
||||
@@ -319,18 +408,15 @@ describe('findEntrypoints', () => {
|
||||
outputDir: config.outDir,
|
||||
skipped: false,
|
||||
};
|
||||
const options: GenericEntrypoint['options'] = {};
|
||||
const options: BaseEntrypointOptions = {};
|
||||
globMock.mockResolvedValueOnce([path]);
|
||||
importEntrypointFileMock.mockResolvedValue(options);
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
expect(entrypoints).toHaveLength(1);
|
||||
expect(entrypoints[0]).toEqual({ ...expected, options });
|
||||
expect(importEntrypointFileMock).toBeCalledWith(
|
||||
expected.inputPath,
|
||||
config,
|
||||
);
|
||||
expect(importEntrypointFileMock).toBeCalledWith(expected.inputPath);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -453,52 +539,6 @@ describe('findEntrypoints', () => {
|
||||
},
|
||||
],
|
||||
|
||||
// sidepanel
|
||||
[
|
||||
'sidepanel.html',
|
||||
{
|
||||
type: 'sidepanel',
|
||||
name: 'sidepanel',
|
||||
inputPath: resolve(config.entrypointsDir, 'sidepanel.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
'sidepanel/index.html',
|
||||
{
|
||||
type: 'sidepanel',
|
||||
name: 'sidepanel',
|
||||
inputPath: resolve(config.entrypointsDir, 'sidepanel/index.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
'named.sidepanel.html',
|
||||
{
|
||||
type: 'sidepanel',
|
||||
name: 'named',
|
||||
inputPath: resolve(config.entrypointsDir, 'named.sidepanel.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
'named.sidepanel/index.html',
|
||||
{
|
||||
type: 'sidepanel',
|
||||
name: 'named',
|
||||
inputPath: resolve(config.entrypointsDir, 'named.sidepanel/index.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
|
||||
// devtools
|
||||
[
|
||||
'devtools.html',
|
||||
@@ -619,7 +659,7 @@ describe('findEntrypoints', () => {
|
||||
])('should find entrypoint for %s', async (path, expected) => {
|
||||
globMock.mockResolvedValueOnce([path]);
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
expect(entrypoints).toHaveLength(1);
|
||||
expect(entrypoints[0]).toEqual(expected);
|
||||
@@ -635,7 +675,7 @@ describe('findEntrypoints', () => {
|
||||
'ui.html',
|
||||
]);
|
||||
|
||||
await expect(() => findEntrypoints(config)).rejects.toThrowError(
|
||||
await expect(() => findEntrypoints()).rejects.toThrowError(
|
||||
[
|
||||
'Multiple entrypoints with the same name detected, only one entrypoint for each name is allowed.',
|
||||
'',
|
||||
@@ -653,7 +693,7 @@ describe('findEntrypoints', () => {
|
||||
it('throw an error if there are no entrypoints', async () => {
|
||||
globMock.mockResolvedValueOnce([]);
|
||||
|
||||
await expect(() => findEntrypoints(config)).rejects.toThrowError(
|
||||
await expect(() => findEntrypoints()).rejects.toThrowError(
|
||||
`No entrypoints found in ${unnormalizePath(config.entrypointsDir)}`,
|
||||
);
|
||||
});
|
||||
@@ -665,7 +705,7 @@ describe('findEntrypoints', () => {
|
||||
include: ['not' + config.browser],
|
||||
});
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
expect(entrypoints).toEqual([]);
|
||||
});
|
||||
@@ -676,7 +716,7 @@ describe('findEntrypoints', () => {
|
||||
include: ['not' + config.browser],
|
||||
});
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
expect(entrypoints).toEqual([]);
|
||||
});
|
||||
@@ -693,7 +733,7 @@ describe('findEntrypoints', () => {
|
||||
</html>`,
|
||||
);
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
expect(entrypoints).toEqual([]);
|
||||
});
|
||||
@@ -710,7 +750,7 @@ describe('findEntrypoints', () => {
|
||||
</html>`,
|
||||
);
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
expect(entrypoints).toEqual([]);
|
||||
});
|
||||
@@ -727,7 +767,7 @@ describe('findEntrypoints', () => {
|
||||
</html>`,
|
||||
);
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
expect(entrypoints).toEqual([]);
|
||||
});
|
||||
@@ -740,7 +780,7 @@ describe('findEntrypoints', () => {
|
||||
exclude: [config.browser],
|
||||
});
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
expect(entrypoints).toEqual([]);
|
||||
});
|
||||
@@ -751,7 +791,7 @@ describe('findEntrypoints', () => {
|
||||
exclude: [config.browser],
|
||||
});
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
expect(entrypoints).toEqual([]);
|
||||
});
|
||||
@@ -766,7 +806,7 @@ describe('findEntrypoints', () => {
|
||||
</html>`,
|
||||
);
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
expect(entrypoints).toEqual([]);
|
||||
});
|
||||
@@ -781,7 +821,7 @@ describe('findEntrypoints', () => {
|
||||
</html>`,
|
||||
);
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
expect(entrypoints).toEqual([]);
|
||||
});
|
||||
@@ -796,7 +836,7 @@ describe('findEntrypoints', () => {
|
||||
</html>`,
|
||||
);
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
expect(entrypoints).toEqual([]);
|
||||
});
|
||||
@@ -812,15 +852,17 @@ describe('findEntrypoints', () => {
|
||||
]);
|
||||
importEntrypointFileMock.mockResolvedValue({});
|
||||
const filterEntrypoints = ['popup', 'ui'];
|
||||
const config = fakeInternalConfig({
|
||||
root: '/',
|
||||
entrypointsDir: resolve('/src/entrypoints'),
|
||||
outDir: resolve('.output'),
|
||||
command: 'build',
|
||||
filterEntrypoints: new Set(filterEntrypoints),
|
||||
setFakeWxt({
|
||||
config: {
|
||||
root: '/',
|
||||
entrypointsDir: resolve('/src/entrypoints'),
|
||||
outDir: resolve('.output'),
|
||||
command: 'build',
|
||||
filterEntrypoints: new Set(filterEntrypoints),
|
||||
},
|
||||
});
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
const entrypoints = await findEntrypoints();
|
||||
const names = entrypoints.map((item) => item.name);
|
||||
expect(names).toHaveLength(2);
|
||||
expect(names).toEqual(filterEntrypoints);
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { importEntrypointFile } from '~/core/utils/building';
|
||||
import { fakeInternalConfig } from '~/core/utils/testing/fake-objects';
|
||||
import { resolve } from 'node:path';
|
||||
import { unnormalizePath } from '../../paths';
|
||||
import { setFakeWxt } from '../../testing/fake-objects';
|
||||
|
||||
const entrypointPath = (filename: string) =>
|
||||
resolve('src/core/utils/__tests__/test-entrypoints', filename);
|
||||
|
||||
const config = fakeInternalConfig({
|
||||
imports: false,
|
||||
debug: false,
|
||||
// Run inside the demo folder so that wxt is in the node_modules
|
||||
// WXT must also be built for these tests to pass
|
||||
root: 'demo',
|
||||
});
|
||||
|
||||
describe('importEntrypointFile', () => {
|
||||
beforeEach(() => {
|
||||
setFakeWxt({
|
||||
config: {
|
||||
imports: false,
|
||||
debug: false,
|
||||
// Run inside the demo folder so that wxt is in the node_modules
|
||||
// WXT must also be built for these tests to pass
|
||||
root: 'demo',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
['background.ts', { main: expect.any(Function) }],
|
||||
['content.ts', { main: expect.any(Function), matches: ['<all_urls>'] }],
|
||||
@@ -25,7 +29,7 @@ describe('importEntrypointFile', () => {
|
||||
])(
|
||||
'should return the default export of test-entrypoints/%s',
|
||||
async (file, expected) => {
|
||||
const actual = await importEntrypointFile(entrypointPath(file), config);
|
||||
const actual = await importEntrypointFile(entrypointPath(file));
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
},
|
||||
@@ -34,7 +38,6 @@ describe('importEntrypointFile', () => {
|
||||
it('should return undefined when there is no default export', async () => {
|
||||
const actual = await importEntrypointFile(
|
||||
entrypointPath('no-default-export.ts'),
|
||||
config,
|
||||
);
|
||||
|
||||
expect(actual).toBeUndefined();
|
||||
@@ -45,7 +48,7 @@ describe('importEntrypointFile', () => {
|
||||
'../src/core/utils/__tests__/test-entrypoints/imported-option.ts',
|
||||
);
|
||||
await expect(() =>
|
||||
importEntrypointFile(entrypointPath('imported-option.ts'), config),
|
||||
importEntrypointFile(entrypointPath('imported-option.ts')),
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`[Error: ${filePath}: Cannot use imported variable "faker" outside the main function. See https://wxt.dev/guide/entrypoints.html#side-effects]`,
|
||||
);
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
import {
|
||||
BuildOutput,
|
||||
BuildStepOutput,
|
||||
EntrypointGroup,
|
||||
InternalConfig,
|
||||
} from '~/types';
|
||||
import { BuildOutput, BuildStepOutput, EntrypointGroup } from '~/types';
|
||||
import { getPublicFiles } from '~/core/utils/fs';
|
||||
import fs from 'fs-extra';
|
||||
import { dirname, resolve } from 'path';
|
||||
import type { Ora } from 'ora';
|
||||
import pc from 'picocolors';
|
||||
import { wxt } from '../../wxt';
|
||||
|
||||
export async function buildEntrypoints(
|
||||
groups: EntrypointGroup[],
|
||||
config: InternalConfig,
|
||||
spinner: Ora,
|
||||
): Promise<Omit<BuildOutput, 'manifest'>> {
|
||||
const steps: BuildStepOutput[] = [];
|
||||
@@ -23,26 +18,25 @@ export async function buildEntrypoints(
|
||||
spinner.text =
|
||||
pc.dim(`[${i + 1}/${groups.length}]`) + ` ${groupNameColored}`;
|
||||
try {
|
||||
steps.push(await config.builder.build(group));
|
||||
steps.push(await wxt.config.builder.build(group));
|
||||
} catch (err) {
|
||||
wxt.logger.error(err);
|
||||
throw Error(`Failed to build ${groupNames.join(', ')}`, { cause: err });
|
||||
}
|
||||
}
|
||||
const publicAssets = await copyPublicDirectory(config);
|
||||
const publicAssets = await copyPublicDirectory();
|
||||
|
||||
return { publicAssets, steps };
|
||||
}
|
||||
|
||||
async function copyPublicDirectory(
|
||||
config: InternalConfig,
|
||||
): Promise<BuildOutput['publicAssets']> {
|
||||
const files = await getPublicFiles(config);
|
||||
async function copyPublicDirectory(): Promise<BuildOutput['publicAssets']> {
|
||||
const files = await getPublicFiles();
|
||||
if (files.length === 0) return [];
|
||||
|
||||
const publicAssets: BuildOutput['publicAssets'] = [];
|
||||
for (const file of files) {
|
||||
const srcPath = resolve(config.publicDir, file);
|
||||
const outPath = resolve(config.outDir, file);
|
||||
const srcPath = resolve(wxt.config.publicDir, file);
|
||||
const outPath = resolve(wxt.config.outDir, file);
|
||||
|
||||
await fs.ensureDir(dirname(outPath));
|
||||
await fs.copyFile(srcPath, outPath);
|
||||
|
||||
@@ -2,12 +2,12 @@ import {
|
||||
BuildOutput,
|
||||
BuildStepOutput,
|
||||
EntrypointGroup,
|
||||
InternalConfig,
|
||||
OutputAsset,
|
||||
OutputFile,
|
||||
} from '~/types';
|
||||
import { every, some } from '~/core/utils/arrays';
|
||||
import { normalizePath } from '~/core/utils/paths';
|
||||
import { wxt } from '../../wxt';
|
||||
|
||||
/**
|
||||
* Compare the changed files vs the build output and determine what kind of reload needs to happen:
|
||||
@@ -30,19 +30,18 @@ import { normalizePath } from '~/core/utils/paths';
|
||||
* - Config file changed (wxt.config.ts, .env, web-ext.config.ts, etc)
|
||||
*/
|
||||
export function detectDevChanges(
|
||||
config: InternalConfig,
|
||||
changedFiles: string[],
|
||||
currentOutput: BuildOutput,
|
||||
): DevModeChange {
|
||||
const isConfigChange = some(
|
||||
changedFiles,
|
||||
(file) => file === config.userConfigMetadata.configFile,
|
||||
(file) => file === wxt.config.userConfigMetadata.configFile,
|
||||
);
|
||||
if (isConfigChange) return { type: 'full-restart' };
|
||||
|
||||
const isRunnerChange = some(
|
||||
changedFiles,
|
||||
(file) => file === config.runnerConfig.configFile,
|
||||
(file) => file === wxt.config.runnerConfig.configFile,
|
||||
);
|
||||
if (isRunnerChange) return { type: 'browser-restart' };
|
||||
|
||||
|
||||
@@ -7,10 +7,13 @@ import {
|
||||
ContentScriptEntrypoint,
|
||||
Entrypoint,
|
||||
GenericEntrypoint,
|
||||
InternalConfig,
|
||||
OptionsEntrypoint,
|
||||
PopupEntrypoint,
|
||||
UnlistedScriptDefinition,
|
||||
PopupEntrypointOptions,
|
||||
OptionsEntrypointOptions,
|
||||
SidepanelEntrypoint,
|
||||
SidepanelEntrypointOptions,
|
||||
} from '~/types';
|
||||
import fs from 'fs-extra';
|
||||
import { minimatch } from 'minimatch';
|
||||
@@ -20,20 +23,19 @@ import { importEntrypointFile } from '~/core/utils/building';
|
||||
import glob from 'fast-glob';
|
||||
import {
|
||||
getEntrypointName,
|
||||
resolvePerBrowserOption,
|
||||
resolvePerBrowserOptions,
|
||||
} from '~/core/utils/entrypoints';
|
||||
import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '~/core/utils/constants';
|
||||
import { CSS_EXTENSIONS_PATTERN } from '~/core/utils/paths';
|
||||
import pc from 'picocolors';
|
||||
import { wxt } from '../../wxt';
|
||||
|
||||
/**
|
||||
* Return entrypoints and their configuration by looking through the project's files.
|
||||
*/
|
||||
export async function findEntrypoints(
|
||||
config: InternalConfig,
|
||||
): Promise<Entrypoint[]> {
|
||||
export async function findEntrypoints(): Promise<Entrypoint[]> {
|
||||
const relativePaths = await glob(Object.keys(PATH_GLOB_TO_TYPE_MAP), {
|
||||
cwd: config.entrypointsDir,
|
||||
cwd: wxt.config.entrypointsDir,
|
||||
});
|
||||
// Ensure consistent output
|
||||
relativePaths.sort();
|
||||
@@ -42,8 +44,8 @@ export async function findEntrypoints(
|
||||
const entrypointInfos: EntrypointInfo[] = relativePaths.reduce<
|
||||
EntrypointInfo[]
|
||||
>((results, relativePath) => {
|
||||
const inputPath = resolve(config.entrypointsDir, relativePath);
|
||||
const name = getEntrypointName(config.entrypointsDir, inputPath);
|
||||
const inputPath = resolve(wxt.config.entrypointsDir, relativePath);
|
||||
const name = getEntrypointName(wxt.config.entrypointsDir, inputPath);
|
||||
const matchingGlob = pathGlobs.find((glob) =>
|
||||
minimatch(relativePath, glob),
|
||||
);
|
||||
@@ -54,16 +56,16 @@ export async function findEntrypoints(
|
||||
inputPath,
|
||||
type,
|
||||
skipped:
|
||||
config.filterEntrypoints != null &&
|
||||
!config.filterEntrypoints.has(name),
|
||||
wxt.config.filterEntrypoints != null &&
|
||||
!wxt.config.filterEntrypoints.has(name),
|
||||
});
|
||||
}
|
||||
return results;
|
||||
}, []);
|
||||
|
||||
// Validation
|
||||
preventNoEntrypoints(config, entrypointInfos);
|
||||
preventDuplicateEntrypointNames(config, entrypointInfos);
|
||||
preventNoEntrypoints(entrypointInfos);
|
||||
preventDuplicateEntrypointNames(entrypointInfos);
|
||||
|
||||
// Import entrypoints to get their config
|
||||
let hasBackground = false;
|
||||
@@ -72,23 +74,25 @@ export async function findEntrypoints(
|
||||
const { type } = info;
|
||||
switch (type) {
|
||||
case 'popup':
|
||||
return await getPopupEntrypoint(config, info);
|
||||
return await getPopupEntrypoint(info);
|
||||
case 'sidepanel':
|
||||
return await getSidepanelEntrypoint(info);
|
||||
case 'options':
|
||||
return await getOptionsEntrypoint(config, info);
|
||||
return await getOptionsEntrypoint(info);
|
||||
case 'background':
|
||||
hasBackground = true;
|
||||
return await getBackgroundEntrypoint(config, info);
|
||||
return await getBackgroundEntrypoint(info);
|
||||
case 'content-script':
|
||||
return await getContentScriptEntrypoint(config, info);
|
||||
return await getContentScriptEntrypoint(info);
|
||||
case 'unlisted-page':
|
||||
return await getUnlistedPageEntrypoint(config, info);
|
||||
return await getUnlistedPageEntrypoint(info);
|
||||
case 'unlisted-script':
|
||||
return await getUnlistedScriptEntrypoint(config, info);
|
||||
return await getUnlistedScriptEntrypoint(info);
|
||||
case 'content-script-style':
|
||||
return {
|
||||
...info,
|
||||
type,
|
||||
outputDir: resolve(config.outDir, CONTENT_SCRIPT_OUT_DIR),
|
||||
outputDir: resolve(wxt.config.outDir, CONTENT_SCRIPT_OUT_DIR),
|
||||
options: {
|
||||
include: undefined,
|
||||
exclude: undefined,
|
||||
@@ -98,7 +102,7 @@ export async function findEntrypoints(
|
||||
return {
|
||||
...info,
|
||||
type,
|
||||
outputDir: config.outDir,
|
||||
outputDir: wxt.config.outDir,
|
||||
options: {
|
||||
include: undefined,
|
||||
exclude: undefined,
|
||||
@@ -108,9 +112,9 @@ export async function findEntrypoints(
|
||||
}),
|
||||
);
|
||||
|
||||
if (config.command === 'serve' && !hasBackground) {
|
||||
if (wxt.config.command === 'serve' && !hasBackground) {
|
||||
entrypoints.push(
|
||||
await getBackgroundEntrypoint(config, {
|
||||
await getBackgroundEntrypoint({
|
||||
inputPath: VIRTUAL_NOOP_BACKGROUND_MODULE_ID,
|
||||
name: 'background',
|
||||
type: 'background',
|
||||
@@ -119,12 +123,12 @@ export async function findEntrypoints(
|
||||
);
|
||||
}
|
||||
|
||||
config.logger.debug('All entrypoints:', entrypoints);
|
||||
wxt.logger.debug('All entrypoints:', entrypoints);
|
||||
const skippedEntrypointNames = entrypointInfos
|
||||
.filter((item) => item.skipped)
|
||||
.map((item) => item.name);
|
||||
if (skippedEntrypointNames.length) {
|
||||
config.logger.warn(
|
||||
wxt.logger.warn(
|
||||
`Filter excluded the following entrypoints:\n${skippedEntrypointNames
|
||||
.map((item) => `${pc.dim('-')} ${pc.cyan(item)}`)
|
||||
.join('\n')}`,
|
||||
@@ -133,16 +137,16 @@ export async function findEntrypoints(
|
||||
const targetEntrypoints = entrypoints.filter((entry) => {
|
||||
const { include, exclude } = entry.options;
|
||||
if (include?.length && exclude?.length) {
|
||||
config.logger.warn(
|
||||
wxt.logger.warn(
|
||||
`The ${entry.name} entrypoint lists both include and exclude, but only one can be used per entrypoint. Entrypoint ignored.`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (exclude?.length && !include?.length) {
|
||||
return !exclude.includes(config.browser);
|
||||
return !exclude.includes(wxt.config.browser);
|
||||
}
|
||||
if (include?.length && !exclude?.length) {
|
||||
return include.includes(config.browser);
|
||||
return include.includes(wxt.config.browser);
|
||||
}
|
||||
if (skippedEntrypointNames.includes(entry.name)) {
|
||||
return false;
|
||||
@@ -150,7 +154,9 @@ export async function findEntrypoints(
|
||||
|
||||
return true;
|
||||
});
|
||||
config.logger.debug(`${config.browser} entrypoints:`, targetEntrypoints);
|
||||
wxt.logger.debug(`${wxt.config.browser} entrypoints:`, targetEntrypoints);
|
||||
await wxt.hooks.callHook('entrypoints:resolved', wxt, targetEntrypoints);
|
||||
|
||||
return targetEntrypoints;
|
||||
}
|
||||
|
||||
@@ -164,10 +170,7 @@ interface EntrypointInfo {
|
||||
skipped: boolean;
|
||||
}
|
||||
|
||||
function preventDuplicateEntrypointNames(
|
||||
config: InternalConfig,
|
||||
files: EntrypointInfo[],
|
||||
) {
|
||||
function preventDuplicateEntrypointNames(files: EntrypointInfo[]) {
|
||||
const namesToPaths = files.reduce<Record<string, string[]>>(
|
||||
(map, { name, inputPath }) => {
|
||||
map[name] ??= [];
|
||||
@@ -181,7 +184,7 @@ function preventDuplicateEntrypointNames(
|
||||
if (absolutePaths.length > 1) {
|
||||
lines.push(`- ${name}`);
|
||||
absolutePaths.forEach((absolutePath) => {
|
||||
lines.push(` - ${relative(config.root, absolutePath)}`);
|
||||
lines.push(` - ${relative(wxt.config.root, absolutePath)}`);
|
||||
});
|
||||
}
|
||||
return lines;
|
||||
@@ -196,194 +199,119 @@ function preventDuplicateEntrypointNames(
|
||||
}
|
||||
}
|
||||
|
||||
function preventNoEntrypoints(config: InternalConfig, files: EntrypointInfo[]) {
|
||||
function preventNoEntrypoints(files: EntrypointInfo[]) {
|
||||
if (files.length === 0) {
|
||||
throw Error(`No entrypoints found in ${config.entrypointsDir}`);
|
||||
throw Error(`No entrypoints found in ${wxt.config.entrypointsDir}`);
|
||||
}
|
||||
}
|
||||
|
||||
function getHtmlBaseOptions(document: Document): BaseEntrypointOptions {
|
||||
const options: BaseEntrypointOptions = {};
|
||||
|
||||
const includeContent = document
|
||||
.querySelector("meta[name='manifest.include']")
|
||||
?.getAttribute('content');
|
||||
if (includeContent) {
|
||||
options.include = JSON5.parse(includeContent);
|
||||
}
|
||||
|
||||
const excludeContent = document
|
||||
.querySelector("meta[name='manifest.exclude']")
|
||||
?.getAttribute('content');
|
||||
if (excludeContent) {
|
||||
options.exclude = JSON5.parse(excludeContent);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path Absolute path to the popup HTML file.
|
||||
* @param content String contents of the file at the path.
|
||||
*/
|
||||
async function getPopupEntrypoint(
|
||||
config: InternalConfig,
|
||||
{ inputPath, name, skipped }: EntrypointInfo,
|
||||
info: EntrypointInfo,
|
||||
): Promise<PopupEntrypoint> {
|
||||
const content = await fs.readFile(inputPath, 'utf-8');
|
||||
const { document } = parseHTML(content);
|
||||
|
||||
const options: PopupEntrypoint['options'] = getHtmlBaseOptions(document);
|
||||
|
||||
const title = document.querySelector('title');
|
||||
if (title != null) options.defaultTitle = title.textContent ?? undefined;
|
||||
|
||||
const defaultIconContent = document
|
||||
.querySelector("meta[name='manifest.default_icon']")
|
||||
?.getAttribute('content');
|
||||
if (defaultIconContent) {
|
||||
try {
|
||||
options.defaultIcon = JSON5.parse(defaultIconContent);
|
||||
} catch (err) {
|
||||
config.logger.fatal(
|
||||
`Failed to parse default_icon meta tag content as JSON5. content=${defaultIconContent}`,
|
||||
err,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mv2TypeContent = document
|
||||
.querySelector("meta[name='manifest.type']")
|
||||
?.getAttribute('content');
|
||||
if (mv2TypeContent) {
|
||||
options.mv2Key =
|
||||
mv2TypeContent === 'page_action' ? 'page_action' : 'browser_action';
|
||||
}
|
||||
|
||||
const browserStyleContent = document
|
||||
.querySelector("meta[name='manifest.browser_style']")
|
||||
?.getAttribute('content');
|
||||
if (browserStyleContent) {
|
||||
options.browserStyle = browserStyleContent === 'true';
|
||||
}
|
||||
const options = await getHtmlEntrypointOptions<PopupEntrypointOptions>(
|
||||
info,
|
||||
{
|
||||
browserStyle: 'browse_style',
|
||||
exclude: 'exclude',
|
||||
include: 'include',
|
||||
defaultIcon: 'default_icon',
|
||||
defaultTitle: 'default_title',
|
||||
mv2Key: 'type',
|
||||
},
|
||||
{
|
||||
defaultTitle: (document) =>
|
||||
document.querySelector('title')?.textContent || undefined,
|
||||
},
|
||||
{
|
||||
defaultTitle: (content) => content,
|
||||
mv2Key: (content) =>
|
||||
content === 'page_action' ? 'page_action' : 'browser_action',
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
type: 'popup',
|
||||
name: 'popup',
|
||||
options,
|
||||
inputPath,
|
||||
outputDir: config.outDir,
|
||||
skipped,
|
||||
options: resolvePerBrowserOptions(options, wxt.config.browser),
|
||||
inputPath: info.inputPath,
|
||||
outputDir: wxt.config.outDir,
|
||||
skipped: info.skipped,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path Absolute path to the options HTML file.
|
||||
* @param content String contents of the file at the path.
|
||||
*/
|
||||
async function getOptionsEntrypoint(
|
||||
config: InternalConfig,
|
||||
{ inputPath, name, skipped }: EntrypointInfo,
|
||||
info: EntrypointInfo,
|
||||
): Promise<OptionsEntrypoint> {
|
||||
const content = await fs.readFile(inputPath, 'utf-8');
|
||||
const { document } = parseHTML(content);
|
||||
|
||||
const options: OptionsEntrypoint['options'] = getHtmlBaseOptions(document);
|
||||
|
||||
const openInTabContent = document
|
||||
.querySelector("meta[name='manifest.open_in_tab']")
|
||||
?.getAttribute('content');
|
||||
if (openInTabContent) {
|
||||
options.openInTab = openInTabContent === 'true';
|
||||
}
|
||||
|
||||
const chromeStyleContent = document
|
||||
.querySelector("meta[name='manifest.chrome_style']")
|
||||
?.getAttribute('content');
|
||||
if (chromeStyleContent) {
|
||||
options.chromeStyle = chromeStyleContent === 'true';
|
||||
}
|
||||
|
||||
const browserStyleContent = document
|
||||
.querySelector("meta[name='manifest.browser_style']")
|
||||
?.getAttribute('content');
|
||||
if (browserStyleContent) {
|
||||
options.browserStyle = browserStyleContent === 'true';
|
||||
}
|
||||
|
||||
const options = await getHtmlEntrypointOptions<OptionsEntrypointOptions>(
|
||||
info,
|
||||
{
|
||||
browserStyle: 'browse_style',
|
||||
chromeStyle: 'chrome_style',
|
||||
exclude: 'exclude',
|
||||
include: 'include',
|
||||
openInTab: 'open_in_tab',
|
||||
},
|
||||
);
|
||||
return {
|
||||
type: 'options',
|
||||
name: 'options',
|
||||
options,
|
||||
inputPath,
|
||||
outputDir: config.outDir,
|
||||
skipped,
|
||||
options: resolvePerBrowserOptions(options, wxt.config.browser),
|
||||
inputPath: info.inputPath,
|
||||
outputDir: wxt.config.outDir,
|
||||
skipped: info.skipped,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path Absolute path to the HTML file.
|
||||
* @param content String contents of the file at the path.
|
||||
*/
|
||||
async function getUnlistedPageEntrypoint(
|
||||
config: InternalConfig,
|
||||
{ inputPath, name, skipped }: EntrypointInfo,
|
||||
info: EntrypointInfo,
|
||||
): Promise<GenericEntrypoint> {
|
||||
const content = await fs.readFile(inputPath, 'utf-8');
|
||||
const { document } = parseHTML(content);
|
||||
const options = await getHtmlEntrypointOptions<BaseEntrypointOptions>(info, {
|
||||
exclude: 'exclude',
|
||||
include: 'include',
|
||||
});
|
||||
|
||||
return {
|
||||
type: 'unlisted-page',
|
||||
name: getEntrypointName(config.entrypointsDir, inputPath),
|
||||
inputPath,
|
||||
outputDir: config.outDir,
|
||||
options: getHtmlBaseOptions(document),
|
||||
skipped,
|
||||
name: info.name,
|
||||
inputPath: info.inputPath,
|
||||
outputDir: wxt.config.outDir,
|
||||
options,
|
||||
skipped: info.skipped,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path Absolute path to the script's file.
|
||||
* @param content String contents of the file at the path.
|
||||
*/
|
||||
async function getUnlistedScriptEntrypoint(
|
||||
config: InternalConfig,
|
||||
{ inputPath, name, skipped }: EntrypointInfo,
|
||||
): Promise<GenericEntrypoint> {
|
||||
const defaultExport = await importEntrypointFile<UnlistedScriptDefinition>(
|
||||
inputPath,
|
||||
config,
|
||||
);
|
||||
async function getUnlistedScriptEntrypoint({
|
||||
inputPath,
|
||||
name,
|
||||
skipped,
|
||||
}: EntrypointInfo): Promise<GenericEntrypoint> {
|
||||
const defaultExport =
|
||||
await importEntrypointFile<UnlistedScriptDefinition>(inputPath);
|
||||
if (defaultExport == null) {
|
||||
throw Error(
|
||||
`${name}: Default export not found, did you forget to call "export default defineUnlistedScript(...)"?`,
|
||||
);
|
||||
}
|
||||
const { main: _, ...moduleOptions } = defaultExport;
|
||||
const options: Omit<UnlistedScriptDefinition, 'main'> = moduleOptions;
|
||||
const { main: _, ...options } = defaultExport;
|
||||
return {
|
||||
type: 'unlisted-script',
|
||||
name,
|
||||
inputPath,
|
||||
outputDir: config.outDir,
|
||||
options,
|
||||
outputDir: wxt.config.outDir,
|
||||
options: resolvePerBrowserOptions(options, wxt.config.browser),
|
||||
skipped,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path Absolute path to the background's TS file.
|
||||
*/
|
||||
async function getBackgroundEntrypoint(
|
||||
config: InternalConfig,
|
||||
{ inputPath, name, skipped }: EntrypointInfo,
|
||||
): Promise<BackgroundEntrypoint> {
|
||||
async function getBackgroundEntrypoint({
|
||||
inputPath,
|
||||
name,
|
||||
skipped,
|
||||
}: EntrypointInfo): Promise<BackgroundEntrypoint> {
|
||||
let options: Omit<BackgroundDefinition, 'main'> = {};
|
||||
if (inputPath !== VIRTUAL_NOOP_BACKGROUND_MODULE_ID) {
|
||||
const defaultExport = await importEntrypointFile<BackgroundDefinition>(
|
||||
inputPath,
|
||||
config,
|
||||
);
|
||||
const defaultExport =
|
||||
await importEntrypointFile<BackgroundDefinition>(inputPath);
|
||||
if (defaultExport == null) {
|
||||
throw Error(
|
||||
`${name}: Default export not found, did you forget to call "export default defineBackground(...)"?`,
|
||||
@@ -393,7 +321,7 @@ async function getBackgroundEntrypoint(
|
||||
options = moduleOptions;
|
||||
}
|
||||
|
||||
if (config.manifestVersion !== 3) {
|
||||
if (wxt.config.manifestVersion !== 3) {
|
||||
delete options.type;
|
||||
}
|
||||
|
||||
@@ -401,25 +329,19 @@ async function getBackgroundEntrypoint(
|
||||
type: 'background',
|
||||
name,
|
||||
inputPath,
|
||||
outputDir: config.outDir,
|
||||
options: {
|
||||
...options,
|
||||
type: resolvePerBrowserOption(options.type, config.browser),
|
||||
persistent: resolvePerBrowserOption(options.persistent, config.browser),
|
||||
},
|
||||
outputDir: wxt.config.outDir,
|
||||
options: resolvePerBrowserOptions(options, wxt.config.browser),
|
||||
skipped,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path Absolute path to the content script's TS file.
|
||||
*/
|
||||
async function getContentScriptEntrypoint(
|
||||
config: InternalConfig,
|
||||
{ inputPath, name, skipped }: EntrypointInfo,
|
||||
): Promise<ContentScriptEntrypoint> {
|
||||
async function getContentScriptEntrypoint({
|
||||
inputPath,
|
||||
name,
|
||||
skipped,
|
||||
}: EntrypointInfo): Promise<ContentScriptEntrypoint> {
|
||||
const { main: _, ...options } =
|
||||
await importEntrypointFile<ContentScriptDefinition>(inputPath, config);
|
||||
await importEntrypointFile<ContentScriptDefinition>(inputPath);
|
||||
if (options == null) {
|
||||
throw Error(
|
||||
`${name}: Default export not found, did you forget to call "export default defineContentScript(...)"?`,
|
||||
@@ -429,12 +351,88 @@ async function getContentScriptEntrypoint(
|
||||
type: 'content-script',
|
||||
name,
|
||||
inputPath,
|
||||
outputDir: resolve(config.outDir, CONTENT_SCRIPT_OUT_DIR),
|
||||
options,
|
||||
outputDir: resolve(wxt.config.outDir, CONTENT_SCRIPT_OUT_DIR),
|
||||
options: resolvePerBrowserOptions(options, wxt.config.browser),
|
||||
skipped,
|
||||
};
|
||||
}
|
||||
|
||||
async function getSidepanelEntrypoint(
|
||||
info: EntrypointInfo,
|
||||
): Promise<SidepanelEntrypoint> {
|
||||
const options = await getHtmlEntrypointOptions<SidepanelEntrypointOptions>(
|
||||
info,
|
||||
{
|
||||
browserStyle: 'browse_style',
|
||||
exclude: 'exclude',
|
||||
include: 'include',
|
||||
defaultIcon: 'default_icon',
|
||||
defaultTitle: 'default_title',
|
||||
openAtInstall: 'open_at_install',
|
||||
},
|
||||
{
|
||||
defaultTitle: (document) =>
|
||||
document.querySelector('title')?.textContent || undefined,
|
||||
},
|
||||
{
|
||||
defaultTitle: (content) => content,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
type: 'sidepanel',
|
||||
name: info.name,
|
||||
options: resolvePerBrowserOptions(options, wxt.config.browser),
|
||||
inputPath: info.inputPath,
|
||||
outputDir: wxt.config.outDir,
|
||||
skipped: info.skipped,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the HTML tags to extract options from them.
|
||||
*/
|
||||
async function getHtmlEntrypointOptions<T extends BaseEntrypointOptions>(
|
||||
info: EntrypointInfo,
|
||||
keyMap: Record<keyof T, string>,
|
||||
queries?: Partial<{
|
||||
[key in keyof T]: (
|
||||
document: Document,
|
||||
manifestKey: string,
|
||||
) => string | undefined;
|
||||
}>,
|
||||
parsers?: Partial<{ [key in keyof T]: (content: string) => T[key] }>,
|
||||
): Promise<T> {
|
||||
const content = await fs.readFile(info.inputPath, 'utf-8');
|
||||
const { document } = parseHTML(content);
|
||||
|
||||
const options = {} as T;
|
||||
|
||||
const defaultQuery = (manifestKey: string) =>
|
||||
document
|
||||
.querySelector(`meta[name='manifest.${manifestKey}']`)
|
||||
?.getAttribute('content');
|
||||
|
||||
Object.entries(keyMap).forEach(([_key, manifestKey]) => {
|
||||
const key = _key as keyof T;
|
||||
const content = queries?.[key]
|
||||
? queries[key]!(document, manifestKey)
|
||||
: defaultQuery(manifestKey);
|
||||
if (content) {
|
||||
try {
|
||||
options[key] = (parsers?.[key] ?? JSON5.parse)(content);
|
||||
} catch (err) {
|
||||
wxt.logger.fatal(
|
||||
`Failed to parse meta tag content. Usually this means you have invalid JSON5 content (content=${content})`,
|
||||
err,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
const PATH_GLOB_TO_TYPE_MAP: Record<string, Entrypoint['type']> = {
|
||||
'sandbox.html': 'sandbox',
|
||||
'sandbox/index.html': 'sandbox',
|
||||
|
||||
@@ -1,48 +1,53 @@
|
||||
import { UnimportOptions, createUnimport } from 'unimport';
|
||||
import { Entrypoint, InternalConfig } from '~/types';
|
||||
import { Unimport, createUnimport } from 'unimport';
|
||||
import {
|
||||
EslintGlobalsPropValue,
|
||||
Entrypoint,
|
||||
WxtResolvedUnimportOptions,
|
||||
} from '~/types';
|
||||
import fs from 'fs-extra';
|
||||
import { relative, resolve } from 'path';
|
||||
import { getEntrypointBundlePath } from '~/core/utils/entrypoints';
|
||||
import { getUnimportOptions } from '~/core/utils/unimport';
|
||||
import {
|
||||
getEntrypointBundlePath,
|
||||
isHtmlEntrypoint,
|
||||
} from '~/core/utils/entrypoints';
|
||||
import { getEntrypointGlobals, getGlobals } from '~/core/utils/globals';
|
||||
import { normalizePath } from '~/core/utils/paths';
|
||||
import path from 'node:path';
|
||||
import { Message, parseI18nMessages } from '~/core/utils/i18n';
|
||||
import { writeFileIfDifferent, getPublicFiles } from '~/core/utils/fs';
|
||||
import { wxt } from '../../wxt';
|
||||
|
||||
/**
|
||||
* Generate and write all the files inside the `InternalConfig.typesDir` directory.
|
||||
*/
|
||||
export async function generateTypesDir(
|
||||
entrypoints: Entrypoint[],
|
||||
config: InternalConfig,
|
||||
): Promise<void> {
|
||||
await fs.ensureDir(config.typesDir);
|
||||
await fs.ensureDir(wxt.config.typesDir);
|
||||
|
||||
const references: string[] = [];
|
||||
|
||||
const imports = getUnimportOptions(config);
|
||||
if (imports !== false) {
|
||||
references.push(await writeImportsDeclarationFile(config, imports));
|
||||
if (wxt.config.imports !== false) {
|
||||
const unimport = createUnimport(wxt.config.imports);
|
||||
references.push(await writeImportsDeclarationFile(unimport));
|
||||
if (wxt.config.imports.eslintrc.enabled) {
|
||||
await writeImportsEslintFile(unimport, wxt.config.imports);
|
||||
}
|
||||
}
|
||||
|
||||
references.push(await writePathsDeclarationFile(entrypoints, config));
|
||||
references.push(await writeI18nDeclarationFile(config));
|
||||
references.push(await writeGlobalsDeclarationFile(config));
|
||||
references.push(await writePathsDeclarationFile(entrypoints));
|
||||
references.push(await writeI18nDeclarationFile());
|
||||
references.push(await writeGlobalsDeclarationFile());
|
||||
|
||||
const mainReference = await writeMainDeclarationFile(references, config);
|
||||
await writeTsConfigFile(mainReference, config);
|
||||
const mainReference = await writeMainDeclarationFile(references);
|
||||
await writeTsConfigFile(mainReference);
|
||||
}
|
||||
|
||||
async function writeImportsDeclarationFile(
|
||||
config: InternalConfig,
|
||||
unimportOptions: Partial<UnimportOptions>,
|
||||
): Promise<string> {
|
||||
const filePath = resolve(config.typesDir, 'imports.d.ts');
|
||||
const unimport = createUnimport(unimportOptions);
|
||||
async function writeImportsDeclarationFile(unimport: Unimport) {
|
||||
const filePath = resolve(wxt.config.typesDir, 'imports.d.ts');
|
||||
|
||||
// Load project imports into unimport memory so they are output via generateTypeDeclarations
|
||||
await unimport.scanImportsFromDir(undefined, { cwd: config.srcDir });
|
||||
await unimport.scanImportsFromDir(undefined, { cwd: wxt.config.srcDir });
|
||||
|
||||
await writeFileIfDifferent(
|
||||
filePath,
|
||||
@@ -54,20 +59,36 @@ async function writeImportsDeclarationFile(
|
||||
return filePath;
|
||||
}
|
||||
|
||||
async function writeImportsEslintFile(
|
||||
unimport: Unimport,
|
||||
options: WxtResolvedUnimportOptions,
|
||||
) {
|
||||
const globals: Record<string, EslintGlobalsPropValue> = {};
|
||||
const eslintrc = { globals };
|
||||
|
||||
(await unimport.getImports())
|
||||
.map((i) => i.as ?? i.name)
|
||||
.filter(Boolean)
|
||||
.sort()
|
||||
.forEach((name) => {
|
||||
eslintrc.globals[name] = options.eslintrc.globalsPropValue;
|
||||
});
|
||||
await fs.writeJson(options.eslintrc.filePath, eslintrc, { spaces: 2 });
|
||||
}
|
||||
|
||||
async function writePathsDeclarationFile(
|
||||
entrypoints: Entrypoint[],
|
||||
config: InternalConfig,
|
||||
): Promise<string> {
|
||||
const filePath = resolve(config.typesDir, 'paths.d.ts');
|
||||
const filePath = resolve(wxt.config.typesDir, 'paths.d.ts');
|
||||
const unions = entrypoints
|
||||
.map((entry) =>
|
||||
getEntrypointBundlePath(
|
||||
entry,
|
||||
config.outDir,
|
||||
entry.inputPath.endsWith('.html') ? '.html' : '.js',
|
||||
wxt.config.outDir,
|
||||
isHtmlEntrypoint(entry) ? '.html' : '.js',
|
||||
),
|
||||
)
|
||||
.concat(await getPublicFiles(config))
|
||||
.concat(await getPublicFiles())
|
||||
.map(normalizePath)
|
||||
.map((path) => ` | "/${path}"`)
|
||||
.sort()
|
||||
@@ -95,11 +116,9 @@ declare module "wxt/browser" {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
async function writeI18nDeclarationFile(
|
||||
config: InternalConfig,
|
||||
): Promise<string> {
|
||||
const filePath = resolve(config.typesDir, 'i18n.d.ts');
|
||||
const defaultLocale = config.manifest.default_locale;
|
||||
async function writeI18nDeclarationFile(): Promise<string> {
|
||||
const filePath = resolve(wxt.config.typesDir, 'i18n.d.ts');
|
||||
const defaultLocale = wxt.config.manifest.default_locale;
|
||||
const template = `// Generated by wxt
|
||||
import "wxt/browser";
|
||||
|
||||
@@ -123,7 +142,7 @@ declare module "wxt/browser" {
|
||||
let messages: Message[];
|
||||
if (defaultLocale) {
|
||||
const defaultLocalePath = path.resolve(
|
||||
config.publicDir,
|
||||
wxt.config.publicDir,
|
||||
'_locales',
|
||||
defaultLocale,
|
||||
'messages.json',
|
||||
@@ -154,11 +173,9 @@ declare module "wxt/browser" {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
async function writeGlobalsDeclarationFile(
|
||||
config: InternalConfig,
|
||||
): Promise<string> {
|
||||
const filePath = resolve(config.typesDir, 'globals.d.ts');
|
||||
const globals = [...getGlobals(config), ...getEntrypointGlobals('')];
|
||||
async function writeGlobalsDeclarationFile(): Promise<string> {
|
||||
const filePath = resolve(wxt.config.typesDir, 'globals.d.ts');
|
||||
const globals = [...getGlobals(wxt.config), ...getEntrypointGlobals('')];
|
||||
await writeFileIfDifferent(
|
||||
filePath,
|
||||
[
|
||||
@@ -175,11 +192,8 @@ async function writeGlobalsDeclarationFile(
|
||||
return filePath;
|
||||
}
|
||||
|
||||
async function writeMainDeclarationFile(
|
||||
references: string[],
|
||||
config: InternalConfig,
|
||||
): Promise<string> {
|
||||
const dir = config.wxtDir;
|
||||
async function writeMainDeclarationFile(references: string[]): Promise<string> {
|
||||
const dir = wxt.config.wxtDir;
|
||||
const filePath = resolve(dir, 'wxt.d.ts');
|
||||
await writeFileIfDifferent(
|
||||
filePath,
|
||||
@@ -195,13 +209,10 @@ async function writeMainDeclarationFile(
|
||||
return filePath;
|
||||
}
|
||||
|
||||
async function writeTsConfigFile(
|
||||
mainReference: string,
|
||||
config: InternalConfig,
|
||||
) {
|
||||
const dir = config.wxtDir;
|
||||
async function writeTsConfigFile(mainReference: string) {
|
||||
const dir = wxt.config.wxtDir;
|
||||
const getTsconfigPath = (path: string) => normalizePath(relative(dir, path));
|
||||
const paths = Object.entries(config.alias)
|
||||
const paths = Object.entries(wxt.config.alias)
|
||||
.flatMap(([alias, absolutePath]) => {
|
||||
const aliasPath = getTsconfigPath(absolutePath);
|
||||
return [
|
||||
@@ -229,10 +240,10 @@ ${paths}
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"${getTsconfigPath(config.root)}/**/*",
|
||||
"${getTsconfigPath(wxt.config.root)}/**/*",
|
||||
"./${getTsconfigPath(mainReference)}"
|
||||
],
|
||||
"exclude": ["${getTsconfigPath(config.outBaseDir)}"]
|
||||
"exclude": ["${getTsconfigPath(wxt.config.outBaseDir)}"]
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import createJITI, { TransformOptions as JitiTransformOptions } from 'jiti';
|
||||
import { InternalConfig } from '~/types';
|
||||
import { createUnimport } from 'unimport';
|
||||
import fs from 'fs-extra';
|
||||
import { relative, resolve } from 'node:path';
|
||||
import { getUnimportOptions } from '~/core/utils/unimport';
|
||||
import { removeProjectImportStatements } from '~/core/utils/strings';
|
||||
import { normalizePath } from '~/core/utils/paths';
|
||||
import { TransformOptions, transformSync } from 'esbuild';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { wxt } from '../../wxt';
|
||||
|
||||
/**
|
||||
* Get the value from the default export of a `path`.
|
||||
@@ -24,16 +23,13 @@ import { fileURLToPath } from 'node:url';
|
||||
* Downside is that code cannot be executed outside of the main fucntion for the entrypoint,
|
||||
* otherwise you will see "xxx is not defined" errors for any imports used outside of main function.
|
||||
*/
|
||||
export async function importEntrypointFile<T>(
|
||||
path: string,
|
||||
config: InternalConfig,
|
||||
): Promise<T> {
|
||||
config.logger.debug('Loading file metadata:', path);
|
||||
export async function importEntrypointFile<T>(path: string): Promise<T> {
|
||||
wxt.logger.debug('Loading file metadata:', path);
|
||||
// JITI & Babel uses normalized paths.
|
||||
const normalPath = normalizePath(path);
|
||||
|
||||
const unimport = createUnimport({
|
||||
...getUnimportOptions(config),
|
||||
...wxt.config.imports,
|
||||
// Only allow specific imports, not all from the project
|
||||
dirs: [],
|
||||
});
|
||||
@@ -42,7 +38,7 @@ export async function importEntrypointFile<T>(
|
||||
const text = await fs.readFile(path, 'utf-8');
|
||||
const textNoImports = removeProjectImportStatements(text);
|
||||
const { code } = await unimport.injectImports(textNoImports);
|
||||
config.logger.debug(
|
||||
wxt.logger.debug(
|
||||
['Text:', text, 'No imports:', textNoImports, 'Code:', code].join('\n'),
|
||||
);
|
||||
|
||||
@@ -52,11 +48,11 @@ export async function importEntrypointFile<T>(
|
||||
: fileURLToPath(import.meta.url),
|
||||
{
|
||||
cache: false,
|
||||
debug: config.debug,
|
||||
debug: wxt.config.debug,
|
||||
esmResolve: true,
|
||||
alias: {
|
||||
'webextension-polyfill': resolve(
|
||||
config.root,
|
||||
wxt.config.root,
|
||||
'node_modules/wxt/dist/virtual/mock-browser.js',
|
||||
),
|
||||
},
|
||||
@@ -89,7 +85,7 @@ export async function importEntrypointFile<T>(
|
||||
const res = await jiti(path);
|
||||
return res.default;
|
||||
} catch (err) {
|
||||
const filePath = relative(config.root, path);
|
||||
const filePath = relative(wxt.config.root, path);
|
||||
if (err instanceof ReferenceError) {
|
||||
// "XXX is not defined" - usually due to WXT removing imports
|
||||
const variableName = err.message.replace(' is not defined', '');
|
||||
@@ -98,7 +94,7 @@ export async function importEntrypointFile<T>(
|
||||
{ cause: err },
|
||||
);
|
||||
} else {
|
||||
config.logger.error(err);
|
||||
wxt.logger.error(err);
|
||||
throw Error(`Failed to load entrypoint: ${filePath}`, { cause: err });
|
||||
}
|
||||
}
|
||||
@@ -109,6 +105,13 @@ function getEsbuildOptions(opts: JitiTransformOptions): TransformOptions {
|
||||
return {
|
||||
format: 'cjs',
|
||||
loader: isJsx ? 'tsx' : 'ts',
|
||||
jsx: isJsx ? 'automatic' : undefined,
|
||||
...(isJsx
|
||||
? {
|
||||
// `h` and `Fragment` are undefined, but that's OK because JSX is never evaluated while
|
||||
// grabbing the entrypoint's options.
|
||||
jsxFactory: 'h',
|
||||
jsxFragment: 'Fragment',
|
||||
}
|
||||
: undefined),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ export * from './build-entrypoints';
|
||||
export * from './detect-dev-changes';
|
||||
export * from './find-entrypoints';
|
||||
export * from './generate-wxt-dir';
|
||||
export * from './get-internal-config';
|
||||
export * from './resolve-config';
|
||||
export * from './group-entrypoints';
|
||||
export * from './import-entrypoint';
|
||||
export * from './internal-build';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { findEntrypoints } from './find-entrypoints';
|
||||
import { InternalConfig, BuildOutput, Entrypoint } from '~/types';
|
||||
import { BuildOutput, Entrypoint } from '~/types';
|
||||
import pc from 'picocolors';
|
||||
import fs from 'fs-extra';
|
||||
import { groupEntrypoints } from './group-entrypoints';
|
||||
@@ -16,7 +16,8 @@ import {
|
||||
validateEntrypoints,
|
||||
} from '../validation';
|
||||
import consola from 'consola';
|
||||
import { exec } from '../exec';
|
||||
import { wxt } from '../../wxt';
|
||||
import { mergeJsonOutputs } from '@aklinker1/rollup-plugin-visualizer';
|
||||
|
||||
/**
|
||||
* Builds the extension based on an internal config. No more config discovery is performed, the
|
||||
@@ -28,28 +29,28 @@ import { exec } from '../exec';
|
||||
* `rebuild` for more details)
|
||||
* 3. Prints the summary
|
||||
*/
|
||||
export async function internalBuild(
|
||||
config: InternalConfig,
|
||||
): Promise<BuildOutput> {
|
||||
const verb = config.command === 'serve' ? 'Pre-rendering' : 'Building';
|
||||
const target = `${config.browser}-mv${config.manifestVersion}`;
|
||||
config.logger.info(
|
||||
`${verb} ${pc.cyan(target)} for ${pc.cyan(config.mode)} with ${pc.green(
|
||||
`${config.builder.name} ${config.builder.version}`,
|
||||
export async function internalBuild(): Promise<BuildOutput> {
|
||||
await wxt.hooks.callHook('build:before', wxt);
|
||||
|
||||
const verb = wxt.config.command === 'serve' ? 'Pre-rendering' : 'Building';
|
||||
const target = `${wxt.config.browser}-mv${wxt.config.manifestVersion}`;
|
||||
wxt.logger.info(
|
||||
`${verb} ${pc.cyan(target)} for ${pc.cyan(wxt.config.mode)} with ${pc.green(
|
||||
`${wxt.config.builder.name} ${wxt.config.builder.version}`,
|
||||
)}`,
|
||||
);
|
||||
const startTime = Date.now();
|
||||
|
||||
// Cleanup
|
||||
await fs.rm(config.outDir, { recursive: true, force: true });
|
||||
await fs.ensureDir(config.outDir);
|
||||
await fs.rm(wxt.config.outDir, { recursive: true, force: true });
|
||||
await fs.ensureDir(wxt.config.outDir);
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
config.logger.debug('Detected entrypoints:', entrypoints);
|
||||
const entrypoints = await findEntrypoints();
|
||||
wxt.logger.debug('Detected entrypoints:', entrypoints);
|
||||
|
||||
const validationResults = validateEntrypoints(entrypoints);
|
||||
if (validationResults.errorCount + validationResults.warningCount > 0) {
|
||||
printValidationResults(config, validationResults);
|
||||
printValidationResults(validationResults);
|
||||
}
|
||||
if (validationResults.errorCount > 0) {
|
||||
throw new ValidationError(`Entrypoint validation failed`, {
|
||||
@@ -58,55 +59,57 @@ export async function internalBuild(
|
||||
}
|
||||
|
||||
const groups = groupEntrypoints(entrypoints);
|
||||
const { output, warnings } = await rebuild(
|
||||
config,
|
||||
entrypoints,
|
||||
groups,
|
||||
undefined,
|
||||
);
|
||||
await wxt.hooks.callHook('entrypoints:grouped', wxt, groups);
|
||||
|
||||
const { output, warnings } = await rebuild(entrypoints, groups, undefined);
|
||||
await wxt.hooks.callHook('build:done', wxt, output);
|
||||
|
||||
// Post-build
|
||||
await printBuildSummary(
|
||||
config.logger.success,
|
||||
wxt.logger.success,
|
||||
`Built extension in ${formatDuration(Date.now() - startTime)}`,
|
||||
output,
|
||||
config,
|
||||
);
|
||||
|
||||
for (const warning of warnings) {
|
||||
config.logger.warn(...warning);
|
||||
wxt.logger.warn(...warning);
|
||||
}
|
||||
|
||||
if (config.analysis.enabled) {
|
||||
await combineAnalysisStats(config);
|
||||
config.logger.info(
|
||||
`Analysis complete:\n ${pc.gray('└─')} ${pc.yellow('stats.html')}`,
|
||||
if (wxt.config.analysis.enabled) {
|
||||
await combineAnalysisStats();
|
||||
const statsPath = relative(wxt.config.root, wxt.config.analysis.outputFile);
|
||||
wxt.logger.info(
|
||||
`Analysis complete:\n ${pc.gray('└─')} ${pc.yellow(statsPath)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
async function combineAnalysisStats(config: InternalConfig): Promise<void> {
|
||||
const unixFiles = await glob(`stats-*.json`, {
|
||||
cwd: config.outDir,
|
||||
async function combineAnalysisStats(): Promise<void> {
|
||||
const unixFiles = await glob(`${wxt.config.analysis.outputName}-*.json`, {
|
||||
cwd: wxt.config.analysis.outputDir,
|
||||
absolute: true,
|
||||
});
|
||||
const absolutePaths = unixFiles.map(unnormalizePath);
|
||||
|
||||
await exec(
|
||||
config,
|
||||
'rollup-plugin-visualizer',
|
||||
[...absolutePaths, '--template', config.analysis.template],
|
||||
{ cwd: config.root, stdio: 'inherit' },
|
||||
);
|
||||
await mergeJsonOutputs({
|
||||
inputs: absolutePaths,
|
||||
template: wxt.config.analysis.template,
|
||||
filename: wxt.config.analysis.outputFile,
|
||||
});
|
||||
|
||||
if (!wxt.config.analysis.keepArtifacts) {
|
||||
await Promise.all(absolutePaths.map((statsFile) => fs.remove(statsFile)));
|
||||
}
|
||||
}
|
||||
|
||||
function printValidationResults(
|
||||
config: InternalConfig,
|
||||
{ errorCount, errors, warningCount }: ValidationResults,
|
||||
) {
|
||||
(errorCount > 0 ? config.logger.error : config.logger.warn)(
|
||||
function printValidationResults({
|
||||
errorCount,
|
||||
errors,
|
||||
warningCount,
|
||||
}: ValidationResults) {
|
||||
(errorCount > 0 ? wxt.logger.error : wxt.logger.warn)(
|
||||
`Entrypoint validation failed: ${errorCount} error${
|
||||
errorCount === 1 ? '' : 's'
|
||||
}, ${warningCount} warning${warningCount === 1 ? '' : 's'}`,
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import type { Manifest } from '~/browser';
|
||||
import {
|
||||
BuildOutput,
|
||||
Entrypoint,
|
||||
EntrypointGroup,
|
||||
InternalConfig,
|
||||
} from '~/types';
|
||||
import { BuildOutput, Entrypoint, EntrypointGroup } from '~/types';
|
||||
import { generateTypesDir } from './generate-wxt-dir';
|
||||
import { buildEntrypoints } from './build-entrypoints';
|
||||
import { generateManifest, writeManifest } from '~/core/utils/manifest';
|
||||
import { wxt } from '../../wxt';
|
||||
|
||||
/**
|
||||
* Given a configuration, list of entrypoints, and an existing, partial output, build the
|
||||
@@ -26,7 +22,6 @@ import { generateManifest, writeManifest } from '~/core/utils/manifest';
|
||||
* this is the first build.
|
||||
*/
|
||||
export async function rebuild(
|
||||
config: InternalConfig,
|
||||
allEntrypoints: Entrypoint[],
|
||||
entrypointGroups: EntrypointGroup[],
|
||||
existingOutput: Omit<BuildOutput, 'manifest'> = {
|
||||
@@ -42,28 +37,28 @@ export async function rebuild(
|
||||
const spinner = ora(`Preparing...`).start();
|
||||
|
||||
// Update types directory with new files and types
|
||||
await generateTypesDir(allEntrypoints, config).catch((err) => {
|
||||
config.logger.warn('Failed to update .wxt directory:', err);
|
||||
await generateTypesDir(allEntrypoints).catch((err) => {
|
||||
wxt.logger.warn('Failed to update .wxt directory:', err);
|
||||
// Throw the error if doing a regular build, don't for dev mode.
|
||||
if (config.command === 'build') throw err;
|
||||
if (wxt.config.command === 'build') throw err;
|
||||
});
|
||||
|
||||
// Build and merge the outputs
|
||||
const newOutput = await buildEntrypoints(entrypointGroups, config, spinner);
|
||||
const newOutput = await buildEntrypoints(entrypointGroups, spinner);
|
||||
const mergedOutput: Omit<BuildOutput, 'manifest'> = {
|
||||
steps: [...existingOutput.steps, ...newOutput.steps],
|
||||
publicAssets: [...existingOutput.publicAssets, ...newOutput.publicAssets],
|
||||
};
|
||||
|
||||
const { manifest: newManifest, warnings: manifestWarnings } =
|
||||
await generateManifest(allEntrypoints, mergedOutput, config);
|
||||
await generateManifest(allEntrypoints, mergedOutput);
|
||||
const finalOutput: BuildOutput = {
|
||||
manifest: newManifest,
|
||||
...newOutput,
|
||||
};
|
||||
|
||||
// Write manifest
|
||||
await writeManifest(newManifest, finalOutput, config);
|
||||
await writeManifest(newManifest, finalOutput);
|
||||
|
||||
// Stop the spinner and remove it from the CLI output
|
||||
spinner.clear().stop();
|
||||
|
||||
+75
-10
@@ -1,13 +1,15 @@
|
||||
import { loadConfig } from 'c12';
|
||||
import {
|
||||
InlineConfig,
|
||||
InternalConfig,
|
||||
ResolvedConfig,
|
||||
UserConfig,
|
||||
ConfigEnv,
|
||||
UserManifestFn,
|
||||
UserManifest,
|
||||
ExtensionRunnerConfig,
|
||||
WxtDevServer,
|
||||
WxtResolvedUnimportOptions,
|
||||
Logger,
|
||||
} from '~/types';
|
||||
import path from 'node:path';
|
||||
import { createFsCache } from '~/core/utils/cache';
|
||||
@@ -15,6 +17,7 @@ import consola, { LogLevels } from 'consola';
|
||||
import { createViteBuilder } from '~/core/builders/vite';
|
||||
import defu from 'defu';
|
||||
import { NullablyRequired } from '../types';
|
||||
import { isModuleInstalled } from '../package';
|
||||
|
||||
/**
|
||||
* Given an inline config, discover the config file if necessary, merge the results, resolve any
|
||||
@@ -23,17 +26,18 @@ import { NullablyRequired } from '../types';
|
||||
* Inline config always has priority over user config. Cli flags are passed as inline config if set.
|
||||
* If unset, undefined is passed in, letting this function decide default values.
|
||||
*/
|
||||
export async function getInternalConfig(
|
||||
export async function resolveConfig(
|
||||
inlineConfig: InlineConfig,
|
||||
command: 'build' | 'serve',
|
||||
server?: WxtDevServer,
|
||||
): Promise<InternalConfig> {
|
||||
): Promise<ResolvedConfig> {
|
||||
// Load user config
|
||||
|
||||
let userConfig: UserConfig = {};
|
||||
let userConfigMetadata: InternalConfig['userConfigMetadata'] | undefined;
|
||||
let userConfigMetadata: ResolvedConfig['userConfigMetadata'] | undefined;
|
||||
if (inlineConfig.configFile !== false) {
|
||||
const { config: loadedConfig, ...metadata } = await loadConfig<UserConfig>({
|
||||
configFile: inlineConfig.configFile,
|
||||
name: 'wxt',
|
||||
cwd: inlineConfig.root ?? process.cwd(),
|
||||
rcFile: false,
|
||||
@@ -100,7 +104,14 @@ export async function getInternalConfig(
|
||||
}).map(([key, value]) => [key, path.resolve(root, value)]),
|
||||
);
|
||||
|
||||
const finalConfig: Omit<InternalConfig, 'builder'> = {
|
||||
const analysisOutputFile = path.resolve(
|
||||
root,
|
||||
mergedConfig.analysis?.outputFile ?? 'stats.html',
|
||||
);
|
||||
const analysisOutputDir = path.dirname(analysisOutputFile);
|
||||
const analysisOutputName = path.parse(analysisOutputFile).name;
|
||||
|
||||
const finalConfig: Omit<ResolvedConfig, 'builder'> = {
|
||||
browser,
|
||||
command,
|
||||
debug,
|
||||
@@ -108,7 +119,7 @@ export async function getInternalConfig(
|
||||
filterEntrypoints,
|
||||
env,
|
||||
fsCache: createFsCache(wxtDir),
|
||||
imports: mergedConfig.imports ?? {},
|
||||
imports: await getUnimportOptions(wxtDir, logger, mergedConfig),
|
||||
logger,
|
||||
manifest: await resolveManifestConfig(env, mergedConfig.manifest),
|
||||
manifestVersion,
|
||||
@@ -129,6 +140,10 @@ export async function getInternalConfig(
|
||||
analysis: {
|
||||
enabled: mergedConfig.analysis?.enabled ?? false,
|
||||
template: mergedConfig.analysis?.template ?? 'treemap',
|
||||
outputFile: analysisOutputFile,
|
||||
outputDir: analysisOutputDir,
|
||||
outputName: analysisOutputName,
|
||||
keepArtifacts: mergedConfig.analysis?.keepArtifacts ?? false,
|
||||
},
|
||||
userConfigMetadata: userConfigMetadata ?? {},
|
||||
alias,
|
||||
@@ -140,6 +155,7 @@ export async function getInternalConfig(
|
||||
dev: {
|
||||
reloadCommand,
|
||||
},
|
||||
hooks: mergedConfig.hooks ?? {},
|
||||
};
|
||||
|
||||
const builder = await createViteBuilder(
|
||||
@@ -191,6 +207,10 @@ function mergeInlineConfig(
|
||||
inlineConfig.zip ?? {},
|
||||
userConfig.zip ?? {},
|
||||
);
|
||||
const hooks: InlineConfig['hooks'] = defu(
|
||||
inlineConfig.hooks ?? {},
|
||||
userConfig.hooks ?? {},
|
||||
);
|
||||
|
||||
return {
|
||||
root: inlineConfig.root ?? userConfig.root,
|
||||
@@ -211,9 +231,8 @@ function mergeInlineConfig(
|
||||
outDir: inlineConfig.outDir ?? userConfig.outDir,
|
||||
zip,
|
||||
analysis: {
|
||||
enabled: inlineConfig.analysis?.enabled ?? userConfig.analysis?.enabled,
|
||||
template:
|
||||
inlineConfig.analysis?.template ?? userConfig.analysis?.template,
|
||||
...userConfig.analysis,
|
||||
...inlineConfig.analysis,
|
||||
},
|
||||
alias: {
|
||||
...userConfig.alias,
|
||||
@@ -229,13 +248,14 @@ function mergeInlineConfig(
|
||||
...userConfig.dev,
|
||||
...inlineConfig.dev,
|
||||
},
|
||||
hooks,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveInternalZipConfig(
|
||||
root: string,
|
||||
mergedConfig: InlineConfig,
|
||||
): NullablyRequired<InternalConfig['zip']> {
|
||||
): NullablyRequired<ResolvedConfig['zip']> {
|
||||
return {
|
||||
name: undefined,
|
||||
sourcesTemplate: '{{name}}-{{version}}-sources.zip',
|
||||
@@ -257,3 +277,48 @@ function resolveInternalZipConfig(
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
async function getUnimportOptions(
|
||||
wxtDir: string,
|
||||
logger: Logger,
|
||||
config: InlineConfig,
|
||||
): Promise<WxtResolvedUnimportOptions | false> {
|
||||
if (config.imports === false) return false;
|
||||
|
||||
const enabledConfig = config.imports?.eslintrc?.enabled;
|
||||
let enabled: boolean;
|
||||
switch (enabledConfig) {
|
||||
case undefined:
|
||||
case 'auto':
|
||||
enabled = await isModuleInstalled('eslint');
|
||||
break;
|
||||
default:
|
||||
enabled = enabledConfig;
|
||||
}
|
||||
|
||||
const defaultOptions: WxtResolvedUnimportOptions = {
|
||||
debugLog: logger.debug,
|
||||
imports: [
|
||||
{ name: 'defineConfig', from: 'wxt' },
|
||||
{ name: 'fakeBrowser', from: 'wxt/testing' },
|
||||
],
|
||||
presets: [
|
||||
{ package: 'wxt/client' },
|
||||
{ package: 'wxt/browser' },
|
||||
{ package: 'wxt/sandbox' },
|
||||
{ package: 'wxt/storage' },
|
||||
],
|
||||
warn: logger.warn,
|
||||
dirs: ['components', 'composables', 'hooks', 'utils'],
|
||||
eslintrc: {
|
||||
enabled,
|
||||
filePath: path.resolve(wxtDir, 'eslintrc-auto-import.json'),
|
||||
globalsPropValue: true,
|
||||
},
|
||||
};
|
||||
|
||||
return defu<WxtResolvedUnimportOptions, [WxtResolvedUnimportOptions]>(
|
||||
config.imports ?? {},
|
||||
defaultOptions,
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Manifest } from '~/browser';
|
||||
import { ContentScriptEntrypoint, InternalConfig } from '~/types';
|
||||
import { resolvePerBrowserOption } from './entrypoints';
|
||||
import type { Manifest, Scripting } from '~/browser';
|
||||
import { ContentScriptEntrypoint } from '~/types';
|
||||
|
||||
/**
|
||||
* Returns a unique and consistent string hash based on a content scripts options.
|
||||
@@ -9,9 +8,12 @@ import { resolvePerBrowserOption } from './entrypoints';
|
||||
*/
|
||||
export function hashContentScriptOptions(
|
||||
options: ContentScriptEntrypoint['options'],
|
||||
config: InternalConfig,
|
||||
): string {
|
||||
const simplifiedOptions = mapWxtOptionsToContentScript(options, config);
|
||||
const simplifiedOptions = mapWxtOptionsToContentScript(
|
||||
options,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
|
||||
// Remove undefined fields and use defaults to generate hash
|
||||
Object.keys(simplifiedOptions).forEach((key) => {
|
||||
@@ -45,34 +47,39 @@ export function hashContentScriptOptions(
|
||||
|
||||
export function mapWxtOptionsToContentScript(
|
||||
options: ContentScriptEntrypoint['options'],
|
||||
config: InternalConfig,
|
||||
): Omit<Manifest.ContentScript, 'js' | 'css'> {
|
||||
js: string[] | undefined,
|
||||
css: string[] | undefined,
|
||||
): Manifest.ContentScript {
|
||||
return {
|
||||
matches: resolvePerBrowserOption(options.matches, config.browser),
|
||||
all_frames: resolvePerBrowserOption(options.allFrames, config.browser),
|
||||
match_about_blank: resolvePerBrowserOption(
|
||||
options.matchAboutBlank,
|
||||
config.browser,
|
||||
),
|
||||
exclude_globs: resolvePerBrowserOption(
|
||||
options.excludeGlobs,
|
||||
config.browser,
|
||||
),
|
||||
exclude_matches: resolvePerBrowserOption(
|
||||
options.excludeMatches,
|
||||
config.browser,
|
||||
),
|
||||
include_globs: resolvePerBrowserOption(
|
||||
options.includeGlobs,
|
||||
config.browser,
|
||||
),
|
||||
run_at: resolvePerBrowserOption(options.runAt, config.browser),
|
||||
matches: options.matches,
|
||||
all_frames: options.allFrames,
|
||||
match_about_blank: options.matchAboutBlank,
|
||||
exclude_globs: options.excludeGlobs,
|
||||
exclude_matches: options.excludeMatches,
|
||||
include_globs: options.includeGlobs,
|
||||
run_at: options.runAt,
|
||||
css,
|
||||
js,
|
||||
|
||||
// @ts-expect-error: untyped chrome options
|
||||
match_origin_as_fallback: resolvePerBrowserOption(
|
||||
options.matchOriginAsFallback,
|
||||
config.browser,
|
||||
),
|
||||
match_origin_as_fallback: options.matchOriginAsFallback,
|
||||
world: options.world,
|
||||
};
|
||||
}
|
||||
|
||||
export function mapWxtOptionsToRegisteredContentScript(
|
||||
options: ContentScriptEntrypoint['options'],
|
||||
js: string[] | undefined,
|
||||
css: string[] | undefined,
|
||||
): Omit<Scripting.RegisteredContentScript, 'id'> {
|
||||
return {
|
||||
allFrames: options.allFrames,
|
||||
excludeMatches: options.excludeMatches,
|
||||
matches: options.matches,
|
||||
runAt: options.runAt,
|
||||
js,
|
||||
css,
|
||||
// @ts-expect-error: Chrome accepts this, not typed in webextension-polyfill (https://developer.chrome.com/docs/extensions/reference/scripting/#type-RegisteredContentScript)
|
||||
world: options.world,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { Entrypoint, PerBrowserOption, TargetBrowser } from '~/types';
|
||||
import {
|
||||
Entrypoint,
|
||||
PerBrowserOption,
|
||||
ResolvedPerBrowserOptions,
|
||||
TargetBrowser,
|
||||
} from '~/types';
|
||||
import path, { relative, resolve } from 'node:path';
|
||||
import { normalizePath } from './paths';
|
||||
|
||||
@@ -46,3 +51,31 @@ export function resolvePerBrowserOption<T>(
|
||||
return (option as any)[browser];
|
||||
return option;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an entrypoint option, resolve it's value based on a target browser.
|
||||
*
|
||||
* defaultIcon is special, it's the only key that's a record, which can confuse this function. So
|
||||
* it's been manually excluded from resolution.
|
||||
*/
|
||||
export function resolvePerBrowserOptions<
|
||||
T extends Record<string, any>,
|
||||
TKeys extends keyof T,
|
||||
>(options: T, browser: TargetBrowser): ResolvedPerBrowserOptions<T, TKeys> {
|
||||
// @ts-expect-error: Object.entries is untyped.
|
||||
return Object.fromEntries(
|
||||
Object.entries(options).map(([key, value]) => [
|
||||
key,
|
||||
key === 'defaultIcon' ? value : resolvePerBrowserOption(value, browser),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when the entrypoint is an HTML entrypoint.
|
||||
*
|
||||
* Naively just checking the file extension of the input path.
|
||||
*/
|
||||
export function isHtmlEntrypoint(entrypoint: Entrypoint): boolean {
|
||||
return entrypoint.inputPath.endsWith('.html');
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import type { Options } from 'execa';
|
||||
import managePath from 'manage-path';
|
||||
import { resolve } from 'node:path';
|
||||
import { InternalConfig } from '~/types';
|
||||
|
||||
const managedPath = managePath(process.env);
|
||||
|
||||
/**
|
||||
* Wrapper around `execa` with a modified `PATH` variable containing CLI tools from WXT's dependencies.
|
||||
*/
|
||||
export const exec = async (
|
||||
config: InternalConfig,
|
||||
file: string,
|
||||
args?: readonly string[],
|
||||
options?: Options,
|
||||
) => {
|
||||
// Reset so the same path isn't added multiple times
|
||||
managedPath.restore();
|
||||
|
||||
// Add subdependency path for PNPM shamefully-hoist=false
|
||||
managedPath.push(resolve(config.root, 'node_modules/wxt/node_modules/.bin'));
|
||||
|
||||
const { execa } = await import('execa');
|
||||
return await execa(file, args, options);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { InternalConfig } from '~/types';
|
||||
import fs from 'fs-extra';
|
||||
import glob from 'fast-glob';
|
||||
import { unnormalizePath } from './paths';
|
||||
import { wxt } from '../wxt';
|
||||
|
||||
/**
|
||||
* Only write the contents to a file if it results in a change. This prevents unnecessary file
|
||||
@@ -27,11 +27,9 @@ export async function writeFileIfDifferent(
|
||||
* Get all the files in the project's public directory. Returned paths are relative to the
|
||||
* `config.publicDir`.
|
||||
*/
|
||||
export async function getPublicFiles(
|
||||
config: InternalConfig,
|
||||
): Promise<string[]> {
|
||||
if (!(await fs.exists(config.publicDir))) return [];
|
||||
export async function getPublicFiles(): Promise<string[]> {
|
||||
if (!(await fs.exists(wxt.config.publicDir))) return [];
|
||||
|
||||
const files = await glob('**/*', { cwd: config.publicDir });
|
||||
const files = await glob('**/*', { cwd: wxt.config.publicDir });
|
||||
return files.map(unnormalizePath);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { InternalConfig } from '~/types';
|
||||
import { ResolvedConfig } from '~/types';
|
||||
|
||||
export function getGlobals(
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
config: Omit<ResolvedConfig, 'builder'>,
|
||||
): Array<{ name: string; value: any; type: string }> {
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { resolve } from 'path';
|
||||
import { BuildOutput, InternalConfig } from '~/types';
|
||||
import { BuildOutput } from '~/types';
|
||||
import { printFileList } from './printFileList';
|
||||
import { wxt } from '../../wxt';
|
||||
|
||||
export async function printBuildSummary(
|
||||
log: (...args: any[]) => void,
|
||||
header: string,
|
||||
output: BuildOutput,
|
||||
config: InternalConfig,
|
||||
) {
|
||||
const chunks = [
|
||||
...output.steps.flatMap((step) => step.chunks),
|
||||
@@ -19,8 +19,10 @@ export async function printBuildSummary(
|
||||
return l.fileName.localeCompare(r.fileName);
|
||||
});
|
||||
|
||||
const files = chunks.map((chunk) => resolve(config.outDir, chunk.fileName));
|
||||
await printFileList(log, header, config.outDir, files);
|
||||
const files = chunks.map((chunk) =>
|
||||
resolve(wxt.config.outDir, chunk.fileName),
|
||||
);
|
||||
await printFileList(log, header, wxt.config.outDir, files);
|
||||
}
|
||||
|
||||
const DEFAULT_SORT_WEIGHT = 100;
|
||||
|
||||
+166
-107
@@ -4,16 +4,13 @@ import {
|
||||
BackgroundEntrypoint,
|
||||
BuildOutput,
|
||||
ContentScriptEntrypoint,
|
||||
InternalConfig,
|
||||
OptionsEntrypoint,
|
||||
PopupEntrypoint,
|
||||
SidepanelEntrypoint,
|
||||
} from '~/types';
|
||||
import fs from 'fs-extra';
|
||||
import { resolve } from 'path';
|
||||
import {
|
||||
getEntrypointBundlePath,
|
||||
resolvePerBrowserOption,
|
||||
} from './entrypoints';
|
||||
import { getEntrypointBundlePath } from './entrypoints';
|
||||
import { ContentSecurityPolicy } from './content-security-policy';
|
||||
import {
|
||||
hashContentScriptOptions,
|
||||
@@ -22,8 +19,8 @@ import {
|
||||
import { getPackageJson } from './package';
|
||||
import { normalizePath } from './paths';
|
||||
import { writeFileIfDifferent } from './fs';
|
||||
import { produce } from 'immer';
|
||||
import defu from 'defu';
|
||||
import { wxt } from '../wxt';
|
||||
|
||||
/**
|
||||
* Writes the manifest to the output directory and the build output.
|
||||
@@ -31,15 +28,14 @@ import defu from 'defu';
|
||||
export async function writeManifest(
|
||||
manifest: Manifest.WebExtensionManifest,
|
||||
output: BuildOutput,
|
||||
config: InternalConfig,
|
||||
): Promise<void> {
|
||||
const str =
|
||||
config.mode === 'production'
|
||||
wxt.config.mode === 'production'
|
||||
? JSON.stringify(manifest)
|
||||
: JSON.stringify(manifest, null, 2);
|
||||
|
||||
await fs.ensureDir(config.outDir);
|
||||
await writeFileIfDifferent(resolve(config.outDir, 'manifest.json'), str);
|
||||
await fs.ensureDir(wxt.config.outDir);
|
||||
await writeFileIfDifferent(resolve(wxt.config.outDir, 'manifest.json'), str);
|
||||
|
||||
output.publicAssets.unshift({
|
||||
type: 'asset',
|
||||
@@ -53,38 +49,39 @@ export async function writeManifest(
|
||||
export async function generateManifest(
|
||||
entrypoints: Entrypoint[],
|
||||
buildOutput: Omit<BuildOutput, 'manifest'>,
|
||||
config: InternalConfig,
|
||||
): Promise<{ manifest: Manifest.WebExtensionManifest; warnings: any[][] }> {
|
||||
const warnings: any[][] = [];
|
||||
const pkg = await getPackageJson(config);
|
||||
const pkg = await getPackageJson();
|
||||
|
||||
let versionName =
|
||||
config.manifest.version_name ?? config.manifest.version ?? pkg?.version;
|
||||
wxt.config.manifest.version_name ??
|
||||
wxt.config.manifest.version ??
|
||||
pkg?.version;
|
||||
if (versionName == null) {
|
||||
versionName = '0.0.0';
|
||||
config.logger.warn(
|
||||
wxt.logger.warn(
|
||||
'Extension version not found, defaulting to "0.0.0". Add a version to your `package.json` or `wxt.config.ts` file. For more details, see: https://wxt.dev/guide/manifest.html#version-and-version-name',
|
||||
);
|
||||
}
|
||||
const version = config.manifest.version ?? simplifyVersion(versionName);
|
||||
const version = wxt.config.manifest.version ?? simplifyVersion(versionName);
|
||||
|
||||
const baseManifest: Manifest.WebExtensionManifest = {
|
||||
manifest_version: config.manifestVersion,
|
||||
manifest_version: wxt.config.manifestVersion,
|
||||
name: pkg?.name,
|
||||
description: pkg?.description,
|
||||
version,
|
||||
short_name: pkg?.shortName,
|
||||
icons: discoverIcons(buildOutput),
|
||||
};
|
||||
const userManifest = config.manifest;
|
||||
const userManifest = wxt.config.manifest;
|
||||
|
||||
const manifest = defu(
|
||||
let manifest = defu(
|
||||
userManifest,
|
||||
baseManifest,
|
||||
) as Manifest.WebExtensionManifest;
|
||||
|
||||
// Add reload command in dev mode
|
||||
if (config.command === 'serve' && config.dev.reloadCommand) {
|
||||
if (wxt.config.command === 'serve' && wxt.config.dev.reloadCommand) {
|
||||
if (manifest.commands && Object.keys(manifest.commands).length >= 4) {
|
||||
warnings.push([
|
||||
"Extension already has 4 registered commands, WXT's reload command is disabled",
|
||||
@@ -94,7 +91,7 @@ export async function generateManifest(
|
||||
manifest.commands['wxt:reload-extension'] = {
|
||||
description: 'Reload the extension during development',
|
||||
suggested_key: {
|
||||
default: config.dev.reloadCommand,
|
||||
default: wxt.config.dev.reloadCommand,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -104,29 +101,38 @@ export async function generateManifest(
|
||||
manifest.version = version;
|
||||
manifest.version_name =
|
||||
// Firefox doesn't support version_name
|
||||
config.browser === 'firefox' || versionName === version
|
||||
wxt.config.browser === 'firefox' || versionName === version
|
||||
? undefined
|
||||
: versionName;
|
||||
|
||||
addEntrypoints(manifest, entrypoints, buildOutput, config);
|
||||
addEntrypoints(manifest, entrypoints, buildOutput);
|
||||
|
||||
if (config.command === 'serve') addDevModeCsp(manifest, config);
|
||||
if (config.command === 'serve') addDevModePermissions(manifest, config);
|
||||
if (wxt.config.command === 'serve') addDevModeCsp(manifest);
|
||||
if (wxt.config.command === 'serve') addDevModePermissions(manifest);
|
||||
|
||||
const finalManifest = produce(manifest, config.transformManifest);
|
||||
// TODO: Remove in v1
|
||||
wxt.config.transformManifest(manifest);
|
||||
await wxt.hooks.callHook('build:manifestGenerated', wxt, manifest);
|
||||
|
||||
if (finalManifest.name == null)
|
||||
if (wxt.config.manifestVersion === 2)
|
||||
convertWebAccessibleResourcesToMv2(manifest);
|
||||
|
||||
if (wxt.config.manifestVersion === 3) {
|
||||
validateMv3WebAccessbileResources(manifest);
|
||||
}
|
||||
|
||||
if (manifest.name == null)
|
||||
throw Error(
|
||||
"Manifest 'name' is missing. Either:\n1. Set the name in your <rootDir>/package.json\n2. Set a name via the manifest option in your wxt.config.ts",
|
||||
);
|
||||
if (finalManifest.version == null) {
|
||||
if (manifest.version == null) {
|
||||
throw Error(
|
||||
"Manifest 'version' is missing. Either:\n1. Add a version in your <rootDir>/package.json\n2. Pass the version via the manifest option in your wxt.config.ts",
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
manifest: finalManifest,
|
||||
manifest,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
@@ -153,7 +159,6 @@ function addEntrypoints(
|
||||
manifest: Manifest.WebExtensionManifest,
|
||||
entrypoints: Entrypoint[],
|
||||
buildOutput: Omit<BuildOutput, 'manifest'>,
|
||||
config: InternalConfig,
|
||||
): void {
|
||||
const entriesByType = entrypoints.reduce<
|
||||
Partial<Record<Entrypoint['type'], Entrypoint[]>>
|
||||
@@ -178,16 +183,22 @@ function addEntrypoints(
|
||||
| undefined;
|
||||
const popup = entriesByType['popup']?.[0] as PopupEntrypoint | undefined;
|
||||
const sandboxes = entriesByType['sandbox'];
|
||||
const sidepanels = entriesByType['sidepanel'];
|
||||
const sidepanels = entriesByType['sidepanel'] as
|
||||
| SidepanelEntrypoint[]
|
||||
| undefined;
|
||||
|
||||
if (background) {
|
||||
const script = getEntrypointBundlePath(background, config.outDir, '.js');
|
||||
if (config.browser === 'firefox' && config.manifestVersion === 3) {
|
||||
const script = getEntrypointBundlePath(
|
||||
background,
|
||||
wxt.config.outDir,
|
||||
'.js',
|
||||
);
|
||||
if (wxt.config.browser === 'firefox' && wxt.config.manifestVersion === 3) {
|
||||
manifest.background = {
|
||||
type: background.options.type,
|
||||
scripts: [script],
|
||||
};
|
||||
} else if (config.manifestVersion === 3) {
|
||||
} else if (wxt.config.manifestVersion === 3) {
|
||||
manifest.background = {
|
||||
type: background.options.type,
|
||||
service_worker: script,
|
||||
@@ -201,8 +212,8 @@ function addEntrypoints(
|
||||
}
|
||||
|
||||
if (bookmarks) {
|
||||
if (config.browser === 'firefox') {
|
||||
config.logger.warn(
|
||||
if (wxt.config.browser === 'firefox') {
|
||||
wxt.logger.warn(
|
||||
'Bookmarks are not supported by Firefox. chrome_url_overrides.bookmarks was not added to the manifest',
|
||||
);
|
||||
} else {
|
||||
@@ -210,15 +221,15 @@ function addEntrypoints(
|
||||
// @ts-expect-error: bookmarks is untyped in webextension-polyfill, but supported by chrome
|
||||
manifest.chrome_url_overrides.bookmarks = getEntrypointBundlePath(
|
||||
bookmarks,
|
||||
config.outDir,
|
||||
wxt.config.outDir,
|
||||
'.html',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (history) {
|
||||
if (config.browser === 'firefox') {
|
||||
config.logger.warn(
|
||||
if (wxt.config.browser === 'firefox') {
|
||||
wxt.logger.warn(
|
||||
'Bookmarks are not supported by Firefox. chrome_url_overrides.history was not added to the manifest',
|
||||
);
|
||||
} else {
|
||||
@@ -226,7 +237,7 @@ function addEntrypoints(
|
||||
// @ts-expect-error: history is untyped in webextension-polyfill, but supported by chrome
|
||||
manifest.chrome_url_overrides.history = getEntrypointBundlePath(
|
||||
history,
|
||||
config.outDir,
|
||||
wxt.config.outDir,
|
||||
'.html',
|
||||
);
|
||||
}
|
||||
@@ -236,7 +247,7 @@ function addEntrypoints(
|
||||
manifest.chrome_url_overrides ??= {};
|
||||
manifest.chrome_url_overrides.newtab = getEntrypointBundlePath(
|
||||
newtab,
|
||||
config.outDir,
|
||||
wxt.config.outDir,
|
||||
'.html',
|
||||
);
|
||||
}
|
||||
@@ -244,7 +255,7 @@ function addEntrypoints(
|
||||
if (popup) {
|
||||
const default_popup = getEntrypointBundlePath(
|
||||
popup,
|
||||
config.outDir,
|
||||
wxt.config.outDir,
|
||||
'.html',
|
||||
);
|
||||
const options: Manifest.ActionManifest = {};
|
||||
@@ -273,33 +284,37 @@ function addEntrypoints(
|
||||
if (devtools) {
|
||||
manifest.devtools_page = getEntrypointBundlePath(
|
||||
devtools,
|
||||
config.outDir,
|
||||
wxt.config.outDir,
|
||||
'.html',
|
||||
);
|
||||
}
|
||||
|
||||
if (options) {
|
||||
const page = getEntrypointBundlePath(options, config.outDir, '.html');
|
||||
const page = getEntrypointBundlePath(options, wxt.config.outDir, '.html');
|
||||
manifest.options_ui = {
|
||||
open_in_tab: options.options.openInTab,
|
||||
browser_style:
|
||||
config.browser === 'firefox' ? options.options.browserStyle : undefined,
|
||||
wxt.config.browser === 'firefox'
|
||||
? options.options.browserStyle
|
||||
: undefined,
|
||||
chrome_style:
|
||||
config.browser !== 'firefox' ? options.options.chromeStyle : undefined,
|
||||
wxt.config.browser !== 'firefox'
|
||||
? options.options.chromeStyle
|
||||
: undefined,
|
||||
page,
|
||||
};
|
||||
}
|
||||
|
||||
if (sandboxes?.length) {
|
||||
if (config.browser === 'firefox') {
|
||||
config.logger.warn(
|
||||
if (wxt.config.browser === 'firefox') {
|
||||
wxt.logger.warn(
|
||||
'Sandboxed pages not supported by Firefox. sandbox.pages was not added to the manifest',
|
||||
);
|
||||
} else {
|
||||
// @ts-expect-error: sandbox not typed
|
||||
manifest.sandbox = {
|
||||
pages: sandboxes.map((entry) =>
|
||||
getEntrypointBundlePath(entry, config.outDir, '.html'),
|
||||
getEntrypointBundlePath(entry, wxt.config.outDir, '.html'),
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -310,23 +325,25 @@ function addEntrypoints(
|
||||
sidepanels.find((entry) => entry.name === 'sidepanel') ?? sidepanels[0];
|
||||
const page = getEntrypointBundlePath(
|
||||
defaultSidepanel,
|
||||
config.outDir,
|
||||
wxt.config.outDir,
|
||||
'.html',
|
||||
);
|
||||
|
||||
if (config.browser === 'firefox') {
|
||||
if (wxt.config.browser === 'firefox') {
|
||||
manifest.sidebar_action = {
|
||||
// TODO: Add options to side panel
|
||||
// ...defaultSidepanel.options,
|
||||
default_panel: page,
|
||||
browser_style: defaultSidepanel.options.browserStyle,
|
||||
default_icon: defaultSidepanel.options.defaultIcon,
|
||||
default_title: defaultSidepanel.options.defaultTitle,
|
||||
open_at_install: defaultSidepanel.options.openAtInstall,
|
||||
};
|
||||
} else if (config.manifestVersion === 3) {
|
||||
} else if (wxt.config.manifestVersion === 3) {
|
||||
// @ts-expect-error: Untyped
|
||||
manifest.side_panel = {
|
||||
default_path: page,
|
||||
};
|
||||
} else {
|
||||
config.logger.warn(
|
||||
wxt.logger.warn(
|
||||
'Side panel not supported by Chromium using MV2. side_panel.default_path was not added to the manifest',
|
||||
);
|
||||
}
|
||||
@@ -337,45 +354,58 @@ function addEntrypoints(
|
||||
|
||||
// Don't add content scripts to the manifest in dev mode for MV3 - they're managed and reloaded
|
||||
// at runtime
|
||||
if (config.command === 'serve' && config.manifestVersion === 3) {
|
||||
const hostPermissions = new Set<string>(manifest.host_permissions ?? []);
|
||||
if (wxt.config.command === 'serve' && wxt.config.manifestVersion === 3) {
|
||||
contentScripts.forEach((script) => {
|
||||
const matches = resolvePerBrowserOption(
|
||||
script.options.matches,
|
||||
config.browser,
|
||||
);
|
||||
matches.forEach((matchPattern) => {
|
||||
hostPermissions.add(matchPattern);
|
||||
script.options.matches.forEach((matchPattern) => {
|
||||
addHostPermission(manifest, matchPattern);
|
||||
});
|
||||
});
|
||||
hostPermissions.forEach((permission) =>
|
||||
addHostPermission(manifest, permission),
|
||||
);
|
||||
} else {
|
||||
const hashToEntrypointsMap = contentScripts.reduce((map, script) => {
|
||||
const hash = hashContentScriptOptions(script.options, config);
|
||||
if (map.has(hash)) map.get(hash)?.push(script);
|
||||
else map.set(hash, [script]);
|
||||
return map;
|
||||
}, new Map<string, ContentScriptEntrypoint[]>());
|
||||
|
||||
const newContentScripts = Array.from(hashToEntrypointsMap.entries()).map(
|
||||
([, scripts]) => ({
|
||||
...mapWxtOptionsToContentScript(scripts[0].options, config),
|
||||
css: getContentScriptCssFiles(scripts, cssMap),
|
||||
js: scripts.map((entry) =>
|
||||
getEntrypointBundlePath(entry, config.outDir, '.js'),
|
||||
// Manifest scripts
|
||||
const hashToEntrypointsMap = contentScripts
|
||||
.filter((cs) => cs.options.registration !== 'runtime')
|
||||
.reduce((map, script) => {
|
||||
const hash = hashContentScriptOptions(script.options);
|
||||
if (map.has(hash)) map.get(hash)?.push(script);
|
||||
else map.set(hash, [script]);
|
||||
return map;
|
||||
}, new Map<string, ContentScriptEntrypoint[]>());
|
||||
const manifestContentScripts = Array.from(
|
||||
hashToEntrypointsMap.values(),
|
||||
).map((scripts) =>
|
||||
mapWxtOptionsToContentScript(
|
||||
scripts[0].options,
|
||||
scripts.map((entry) =>
|
||||
getEntrypointBundlePath(entry, wxt.config.outDir, '.js'),
|
||||
),
|
||||
}),
|
||||
getContentScriptCssFiles(scripts, cssMap),
|
||||
),
|
||||
);
|
||||
if (newContentScripts.length >= 0) {
|
||||
if (manifestContentScripts.length >= 0) {
|
||||
manifest.content_scripts ??= [];
|
||||
manifest.content_scripts.push(...newContentScripts);
|
||||
manifest.content_scripts.push(...manifestContentScripts);
|
||||
}
|
||||
|
||||
// Runtime content scripts
|
||||
const runtimeContentScripts = contentScripts.filter(
|
||||
(cs) => cs.options.registration === 'runtime',
|
||||
);
|
||||
if (
|
||||
runtimeContentScripts.length > 0 &&
|
||||
wxt.config.manifestVersion === 2
|
||||
) {
|
||||
throw Error(
|
||||
'Cannot use `registration: "runtime"` with MV2 content scripts, it is a MV3-only feature.',
|
||||
);
|
||||
}
|
||||
runtimeContentScripts.forEach((script) => {
|
||||
script.options.matches.forEach((matchPattern) => {
|
||||
addHostPermission(manifest, matchPattern);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const contentScriptCssResources = getContentScriptCssWebAccessibleResources(
|
||||
config,
|
||||
contentScripts,
|
||||
cssMap,
|
||||
);
|
||||
@@ -420,12 +450,9 @@ function discoverIcons(
|
||||
return icons.length > 0 ? Object.fromEntries(icons) : undefined;
|
||||
}
|
||||
|
||||
function addDevModeCsp(
|
||||
manifest: Manifest.WebExtensionManifest,
|
||||
config: InternalConfig,
|
||||
): void {
|
||||
const permission = `http://${config.server?.hostname ?? ''}/*`;
|
||||
const allowedCsp = config.server?.origin ?? 'http://localhost:*';
|
||||
function addDevModeCsp(manifest: Manifest.WebExtensionManifest): void {
|
||||
const permission = `http://${wxt.config.server?.hostname ?? ''}/*`;
|
||||
const allowedCsp = wxt.config.server?.origin ?? 'http://localhost:*';
|
||||
|
||||
if (manifest.manifest_version === 3) {
|
||||
addHostPermission(manifest, permission);
|
||||
@@ -442,7 +469,7 @@ function addDevModeCsp(
|
||||
"script-src 'self'; object-src 'self';", // default CSP for MV2
|
||||
);
|
||||
|
||||
if (config.server) csp.add('script-src', allowedCsp);
|
||||
if (wxt.config.server) csp.add('script-src', allowedCsp);
|
||||
|
||||
if (manifest.manifest_version === 3) {
|
||||
manifest.content_security_policy ??= {};
|
||||
@@ -453,15 +480,12 @@ function addDevModeCsp(
|
||||
}
|
||||
}
|
||||
|
||||
function addDevModePermissions(
|
||||
manifest: Manifest.WebExtensionManifest,
|
||||
config: InternalConfig,
|
||||
) {
|
||||
function addDevModePermissions(manifest: Manifest.WebExtensionManifest) {
|
||||
// For reloading the page
|
||||
addPermission(manifest, 'tabs');
|
||||
|
||||
// For registering content scripts
|
||||
if (config.manifestVersion === 3) addPermission(manifest, 'scripting');
|
||||
if (wxt.config.manifestVersion === 3) addPermission(manifest, 'scripting');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -497,11 +521,11 @@ export function getContentScriptCssFiles(
|
||||
* added to.
|
||||
*/
|
||||
export function getContentScriptCssWebAccessibleResources(
|
||||
config: InternalConfig,
|
||||
contentScripts: ContentScriptEntrypoint[],
|
||||
contentScriptCssMap: Record<string, string | undefined>,
|
||||
): any[] {
|
||||
const resources: any[] = [];
|
||||
const resources: Manifest.WebExtensionManifestWebAccessibleResourcesC2ItemType[] =
|
||||
[];
|
||||
|
||||
contentScripts.forEach((script) => {
|
||||
if (script.options.cssInjectionMode !== 'ui') return;
|
||||
@@ -509,17 +533,12 @@ export function getContentScriptCssWebAccessibleResources(
|
||||
const cssFile = contentScriptCssMap[script.name];
|
||||
if (cssFile == null) return;
|
||||
|
||||
if (config.manifestVersion === 2) {
|
||||
resources.push(cssFile);
|
||||
} else {
|
||||
resources.push({
|
||||
resources: [cssFile],
|
||||
matches: resolvePerBrowserOption(
|
||||
script.options.matches,
|
||||
config.browser,
|
||||
).map((matchPattern) => stripPathFromMatchPattern(matchPattern)),
|
||||
});
|
||||
}
|
||||
resources.push({
|
||||
resources: [cssFile],
|
||||
matches: script.options.matches.map((matchPattern) =>
|
||||
stripPathFromMatchPattern(matchPattern),
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
return resources;
|
||||
@@ -573,3 +592,43 @@ export function stripPathFromMatchPattern(pattern: string) {
|
||||
const startOfPath = pattern.indexOf('/', protocolSepIndex + 3);
|
||||
return pattern.substring(0, startOfPath) + '/*';
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts all MV3 web accessible resources to their MV2 forms. MV3 web accessible resources are
|
||||
* generated in this file, and may be defined by the user in their manifest. In both cases, when
|
||||
* targetting MV2, automatically convert their definitions down to the basic MV2 array.
|
||||
*/
|
||||
export function convertWebAccessibleResourcesToMv2(
|
||||
manifest: Manifest.WebExtensionManifest,
|
||||
): void {
|
||||
if (manifest.web_accessible_resources == null) return;
|
||||
|
||||
manifest.web_accessible_resources = Array.from(
|
||||
new Set(
|
||||
manifest.web_accessible_resources.flatMap((item) => {
|
||||
if (typeof item === 'string') return item;
|
||||
return item.resources;
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure all resources are in MV3 format. If not, add a wanring
|
||||
*/
|
||||
export function validateMv3WebAccessbileResources(
|
||||
manifest: Manifest.WebExtensionManifest,
|
||||
): void {
|
||||
if (manifest.web_accessible_resources == null) return;
|
||||
|
||||
const stringResources = manifest.web_accessible_resources.filter(
|
||||
(item) => typeof item === 'string',
|
||||
);
|
||||
if (stringResources.length > 0) {
|
||||
throw Error(
|
||||
`Non-MV3 web_accessible_resources detected: ${JSON.stringify(
|
||||
stringResources,
|
||||
)}. When manually defining web_accessible_resources, define them as MV3 objects ({ matches: [...], resources: [...] }), and WXT will automatically convert them to MV2 when necessary.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import dns from 'node:dns';
|
||||
import { InternalConfig } from '~/types';
|
||||
import { ResolvedConfig } from '~/types';
|
||||
import { withTimeout } from './time';
|
||||
|
||||
function isOffline(): Promise<boolean> {
|
||||
@@ -26,7 +26,7 @@ export async function isOnline(): Promise<boolean> {
|
||||
*/
|
||||
export async function fetchCached(
|
||||
url: string,
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
config: Omit<ResolvedConfig, 'builder'>,
|
||||
): Promise<string> {
|
||||
let content: string = '';
|
||||
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
import { resolve } from 'node:path';
|
||||
import { InternalConfig } from '~/types';
|
||||
import fs from 'fs-extra';
|
||||
import { wxt } from '../wxt';
|
||||
|
||||
/**
|
||||
* Read the project's package.json.
|
||||
*
|
||||
* TODO: look in root and up directories until it's found
|
||||
*/
|
||||
export async function getPackageJson(
|
||||
config: InternalConfig,
|
||||
): Promise<Partial<Record<string, any>> | undefined> {
|
||||
const file = resolve(config.root, 'package.json');
|
||||
export async function getPackageJson(): Promise<
|
||||
Partial<Record<string, any>> | undefined
|
||||
> {
|
||||
const file = resolve(wxt.config.root, 'package.json');
|
||||
try {
|
||||
return await fs.readJson(file);
|
||||
} catch (err) {
|
||||
config.logger.debug(
|
||||
wxt.logger.debug(
|
||||
`Failed to read package.json at: ${file}. Returning undefined.`,
|
||||
);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export function isModuleInstalled(name: string) {
|
||||
return import(name).then(() => true).catch(() => false);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import merge from 'lodash.merge';
|
||||
import { Commands, type Manifest } from '~/browser';
|
||||
import {
|
||||
FsCache,
|
||||
InternalConfig,
|
||||
ResolvedConfig,
|
||||
WxtDevServer,
|
||||
BackgroundEntrypoint,
|
||||
ContentScriptEntrypoint,
|
||||
@@ -20,13 +20,20 @@ import {
|
||||
BuildOutput,
|
||||
BuildStepOutput,
|
||||
UserManifest,
|
||||
Wxt,
|
||||
SidepanelEntrypoint,
|
||||
} from '~/types';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { vi } from 'vitest';
|
||||
import { setWxtForTesting } from '~/core/wxt';
|
||||
|
||||
faker.seed(__TEST_SEED__);
|
||||
|
||||
type DeepPartial<T> = { [key in keyof T]+?: Partial<T[key]> };
|
||||
type DeepPartial<T> = T extends object
|
||||
? {
|
||||
[P in keyof T]?: DeepPartial<T[P]>;
|
||||
}
|
||||
: T;
|
||||
function fakeObjectCreator<T>(base: () => T) {
|
||||
return (overrides?: DeepPartial<T>): T => merge(base(), overrides);
|
||||
}
|
||||
@@ -142,6 +149,30 @@ export const fakePopupEntrypoint = fakeObjectCreator<PopupEntrypoint>(() => ({
|
||||
skipped: faker.datatype.boolean(),
|
||||
}));
|
||||
|
||||
export const fakeSidepanelEntrypoint = fakeObjectCreator<SidepanelEntrypoint>(
|
||||
() => ({
|
||||
type: 'sidepanel',
|
||||
inputPath: 'entrypoints/sidepanel.html',
|
||||
name: 'sidepanel',
|
||||
outputDir: fakeDir('.output'),
|
||||
options: {
|
||||
defaultTitle: faker.helpers.arrayElement([
|
||||
faker.person.fullName(),
|
||||
undefined,
|
||||
]),
|
||||
defaultIcon: faker.helpers.arrayElement([
|
||||
{
|
||||
'16': 'icon/16.png',
|
||||
'24': 'icon/24.png',
|
||||
'64': 'icon/64.png',
|
||||
},
|
||||
]),
|
||||
openAtInstall: faker.helpers.arrayElement([true, false, undefined]),
|
||||
},
|
||||
skipped: faker.datatype.boolean(),
|
||||
}),
|
||||
);
|
||||
|
||||
export const fakeGenericEntrypoint = fakeObjectCreator<GenericEntrypoint>(
|
||||
() => ({
|
||||
type: faker.helpers.arrayElement([
|
||||
@@ -149,7 +180,6 @@ export const fakeGenericEntrypoint = fakeObjectCreator<GenericEntrypoint>(
|
||||
'bookmarks',
|
||||
'history',
|
||||
'newtab',
|
||||
'sidepanel',
|
||||
'devtools',
|
||||
'unlisted-page',
|
||||
'unlisted-script',
|
||||
@@ -198,7 +228,7 @@ export function fakeArray<T>(createItem: () => T, count = 3): T[] {
|
||||
return array;
|
||||
}
|
||||
|
||||
export const fakeInternalConfig = fakeObjectCreator<InternalConfig>(() => {
|
||||
export const fakeResolvedConfig = fakeObjectCreator<ResolvedConfig>(() => {
|
||||
const browser = faker.helpers.arrayElement(['chrome', 'firefox']);
|
||||
const command = faker.helpers.arrayElement(['build', 'serve'] as const);
|
||||
const manifestVersion = faker.helpers.arrayElement([2, 3] as const);
|
||||
@@ -210,7 +240,20 @@ export const fakeInternalConfig = fakeObjectCreator<InternalConfig>(() => {
|
||||
entrypointsDir: fakeDir(),
|
||||
env: { browser, command, manifestVersion, mode },
|
||||
fsCache: mock<FsCache>(),
|
||||
imports: {},
|
||||
imports: {
|
||||
eslintrc: {
|
||||
enabled: faker.datatype.boolean(),
|
||||
filePath: fakeFile(),
|
||||
globalsPropValue: faker.helpers.arrayElement([
|
||||
true,
|
||||
false,
|
||||
'readable',
|
||||
'readonly',
|
||||
'writable',
|
||||
'writeable',
|
||||
] as const),
|
||||
},
|
||||
},
|
||||
logger: mock(),
|
||||
manifest: fakeUserManifest(),
|
||||
manifestVersion,
|
||||
@@ -230,6 +273,10 @@ export const fakeInternalConfig = fakeObjectCreator<InternalConfig>(() => {
|
||||
analysis: {
|
||||
enabled: false,
|
||||
template: 'treemap',
|
||||
outputFile: fakeFile(),
|
||||
outputDir: fakeDir(),
|
||||
outputName: 'stats',
|
||||
keepArtifacts: false,
|
||||
},
|
||||
zip: {
|
||||
artifactTemplate: '{{name}}-{{version}}.zip',
|
||||
@@ -249,9 +296,22 @@ export const fakeInternalConfig = fakeObjectCreator<InternalConfig>(() => {
|
||||
dev: {
|
||||
reloadCommand: 'Alt+R',
|
||||
},
|
||||
hooks: {},
|
||||
};
|
||||
});
|
||||
|
||||
export const fakeWxt = fakeObjectCreator<Wxt>(() => ({
|
||||
config: fakeResolvedConfig(),
|
||||
hooks: mock(),
|
||||
logger: mock(),
|
||||
reloadConfig: vi.fn(),
|
||||
}));
|
||||
|
||||
export function setFakeWxt(overrides?: DeepPartial<Wxt>) {
|
||||
const wxt = fakeWxt(overrides);
|
||||
setWxtForTesting(wxt);
|
||||
}
|
||||
|
||||
export const fakeBuildOutput = fakeObjectCreator<BuildOutput>(() => ({
|
||||
manifest: fakeManifest(),
|
||||
publicAssets: fakeArray(fakeOutputAsset),
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { UnimportOptions } from 'unimport';
|
||||
import { InternalConfig } from '~/types';
|
||||
import { defu } from 'defu';
|
||||
|
||||
export function getUnimportOptions(
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
): Partial<UnimportOptions | false> {
|
||||
if (config.imports === false) return false;
|
||||
|
||||
const defaultOptions: Partial<UnimportOptions> = {
|
||||
debugLog: config.logger.debug,
|
||||
imports: [
|
||||
{ name: 'defineConfig', from: 'wxt' },
|
||||
{ name: 'fakeBrowser', from: 'wxt/testing' },
|
||||
],
|
||||
presets: [
|
||||
{ package: 'wxt/client' },
|
||||
{ package: 'wxt/browser' },
|
||||
{ package: 'wxt/sandbox' },
|
||||
{ package: 'wxt/storage' },
|
||||
],
|
||||
warn: config.logger.warn,
|
||||
dirs: ['components', 'composables', 'hooks', 'utils'],
|
||||
};
|
||||
|
||||
return defu(config.imports, defaultOptions);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { InlineConfig, Wxt, WxtDevServer, WxtHooks } from '~/types';
|
||||
import { resolveConfig } from './utils/building';
|
||||
import { createHooks } from 'hookable';
|
||||
|
||||
/**
|
||||
* Global variable set once `createWxt` is called once. Since this variable is used everywhere, this
|
||||
* global can be used instead of passing the variable as a function parameter everywhere.
|
||||
*/
|
||||
export let wxt: Wxt;
|
||||
|
||||
/**
|
||||
* Create and register a global instance of the Wxt interface for use throughout the project.
|
||||
*/
|
||||
export async function registerWxt(
|
||||
command: 'build' | 'serve',
|
||||
inlineConfig: InlineConfig = {},
|
||||
server?: WxtDevServer,
|
||||
): Promise<void> {
|
||||
const config = await resolveConfig(inlineConfig, command, server);
|
||||
const hooks = createHooks<WxtHooks>();
|
||||
|
||||
wxt = {
|
||||
config,
|
||||
hooks,
|
||||
get logger() {
|
||||
return config.logger;
|
||||
},
|
||||
async reloadConfig() {
|
||||
wxt.config = await resolveConfig(inlineConfig, command, server);
|
||||
},
|
||||
};
|
||||
|
||||
// Initialize hooks
|
||||
wxt.hooks.addHooks(config.hooks);
|
||||
await wxt.hooks.callHook('ready', wxt);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal ONLY USE FOR TESTING.
|
||||
*
|
||||
* @example
|
||||
* setWxtForTesting(fakeWxt({ ... }));
|
||||
* // Or use the shorthand
|
||||
* setFakeWxt({ ... })
|
||||
*/
|
||||
export function setWxtForTesting(testInstance: Wxt) {
|
||||
wxt = testInstance;
|
||||
}
|
||||
+22
-26
@@ -7,7 +7,8 @@ import { getPackageJson } from '~/core/utils/package';
|
||||
import { minimatch } from 'minimatch';
|
||||
import { formatDuration } from '~/core/utils/time';
|
||||
import { printFileList } from '~/core/utils/log/printFileList';
|
||||
import { getInternalConfig, internalBuild } from '~/core/utils/building';
|
||||
import { internalBuild } from '~/core/utils/building';
|
||||
import { registerWxt, wxt } from './wxt';
|
||||
|
||||
/**
|
||||
* Build and zip the extension for distribution.
|
||||
@@ -15,59 +16,54 @@ import { getInternalConfig, internalBuild } from '~/core/utils/building';
|
||||
* @returns A list of all files included in the ZIP.
|
||||
*/
|
||||
export async function zip(config?: InlineConfig): Promise<string[]> {
|
||||
const internalConfig = await getInternalConfig(config ?? {}, 'build');
|
||||
const output = await internalBuild(internalConfig);
|
||||
await registerWxt('build', config);
|
||||
const output = await internalBuild();
|
||||
|
||||
const start = Date.now();
|
||||
internalConfig.logger.info('Zipping extension...');
|
||||
wxt.logger.info('Zipping extension...');
|
||||
const zipFiles: string[] = [];
|
||||
|
||||
const projectName =
|
||||
internalConfig.zip.name ??
|
||||
wxt.config.zip.name ??
|
||||
kebabCaseAlphanumeric(
|
||||
(await getPackageJson(internalConfig))?.name || dirname(process.cwd()),
|
||||
(await getPackageJson())?.name || dirname(process.cwd()),
|
||||
);
|
||||
const applyTemplate = (template: string): string =>
|
||||
template
|
||||
.replaceAll('{{name}}', projectName)
|
||||
.replaceAll('{{browser}}', internalConfig.browser)
|
||||
.replaceAll('{{browser}}', wxt.config.browser)
|
||||
.replaceAll(
|
||||
'{{version}}',
|
||||
output.manifest.version_name ?? output.manifest.version,
|
||||
)
|
||||
.replaceAll('{{manifestVersion}}', `mv${internalConfig.manifestVersion}`);
|
||||
.replaceAll('{{manifestVersion}}', `mv${wxt.config.manifestVersion}`);
|
||||
|
||||
await fs.ensureDir(internalConfig.outBaseDir);
|
||||
await fs.ensureDir(wxt.config.outBaseDir);
|
||||
|
||||
// ZIP output directory
|
||||
|
||||
const outZipFilename = applyTemplate(internalConfig.zip.artifactTemplate);
|
||||
const outZipPath = resolve(internalConfig.outBaseDir, outZipFilename);
|
||||
await zipdir(internalConfig.outDir, {
|
||||
const outZipFilename = applyTemplate(wxt.config.zip.artifactTemplate);
|
||||
const outZipPath = resolve(wxt.config.outBaseDir, outZipFilename);
|
||||
await zipdir(wxt.config.outDir, {
|
||||
saveTo: outZipPath,
|
||||
});
|
||||
zipFiles.push(outZipPath);
|
||||
|
||||
// ZIP sources for Firefox
|
||||
|
||||
if (internalConfig.browser === 'firefox') {
|
||||
const sourcesZipFilename = applyTemplate(
|
||||
internalConfig.zip.sourcesTemplate,
|
||||
);
|
||||
const sourcesZipPath = resolve(
|
||||
internalConfig.outBaseDir,
|
||||
sourcesZipFilename,
|
||||
);
|
||||
await zipdir(internalConfig.zip.sourcesRoot, {
|
||||
if (wxt.config.browser === 'firefox') {
|
||||
const sourcesZipFilename = applyTemplate(wxt.config.zip.sourcesTemplate);
|
||||
const sourcesZipPath = resolve(wxt.config.outBaseDir, sourcesZipFilename);
|
||||
await zipdir(wxt.config.zip.sourcesRoot, {
|
||||
saveTo: sourcesZipPath,
|
||||
filter(path) {
|
||||
const relativePath = relative(internalConfig.zip.sourcesRoot, path);
|
||||
const relativePath = relative(wxt.config.zip.sourcesRoot, path);
|
||||
|
||||
return (
|
||||
internalConfig.zip.includeSources.some((pattern) =>
|
||||
wxt.config.zip.includeSources.some((pattern) =>
|
||||
minimatch(relativePath, pattern),
|
||||
) ||
|
||||
!internalConfig.zip.excludeSources.some((pattern) =>
|
||||
!wxt.config.zip.excludeSources.some((pattern) =>
|
||||
minimatch(relativePath, pattern),
|
||||
)
|
||||
);
|
||||
@@ -77,9 +73,9 @@ export async function zip(config?: InlineConfig): Promise<string[]> {
|
||||
}
|
||||
|
||||
await printFileList(
|
||||
internalConfig.logger.success,
|
||||
wxt.logger.success,
|
||||
`Zipped extension in ${formatDuration(Date.now() - start)}`,
|
||||
internalConfig.outBaseDir,
|
||||
wxt.config.outBaseDir,
|
||||
zipFiles,
|
||||
);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user