Compare commits
129 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d5fe4248e2 | |||
| 58140cf3e8 | |||
| 1a39f856af | |||
| 82b769e939 | |||
| e47519fb76 | |||
| 171e32c272 | |||
| 54e5ed0bb4 | |||
| a94d673f31 | |||
| b7f7222c26 | |||
| aa5a9d4fa9 | |||
| 77da2d6ade | |||
| 3119c04e10 | |||
| c09994263e | |||
| 2a4fa9ae5c | |||
| c833d3b15b | |||
| d6e095bfce | |||
| 825160a3cc | |||
| e29ca33f82 | |||
| ca5a52c6f1 | |||
| 982c69f8ae | |||
| 6fc7d9254a | |||
| 2e0e104d48 | |||
| 02430fd7b8 | |||
| 2e6d35d193 | |||
| 7cf412ac1f | |||
| 9398cbb596 | |||
| ca9b96142e | |||
| db57c8e079 | |||
| 7e37e11caf | |||
| bb90029c54 | |||
| d3b14afbce | |||
| ba2197a609 | |||
| 4c558d3eb6 | |||
| dcd175444a | |||
| 16fc58404a | |||
| 28c7e7d9ed | |||
| 5a3b42a4b0 | |||
| c9c5d721ec | |||
| 1556b9ad73 | |||
| ca15b1f321 | |||
| 448436cb08 | |||
| c9ddc3d0f2 | |||
| 6b72f883b6 | |||
| cd3446bd7f | |||
| 829b371bba | |||
| 36b399dd49 | |||
| 08f32aa685 | |||
| 05db264e02 | |||
| de5a01834c | |||
| 1d260dbacb | |||
| 53c63b4556 | |||
| 816878bc15 | |||
| e087a0d6ef | |||
| 56b71491a6 | |||
| 121b521157 | |||
| 16c2e3218b | |||
| d9cba55d34 | |||
| 832cd5d640 | |||
| bb95e08d71 | |||
| 35269dad9e | |||
| 64d1498460 | |||
| 0d540a6df9 | |||
| b47c150daa | |||
| 38345481df | |||
| da92449e0f | |||
| 23b3980645 | |||
| 81b4b629c9 | |||
| 4f7e9cb050 | |||
| b226d9b947 | |||
| aabba41e86 | |||
| f19e6908b0 | |||
| 5f651cc3fe | |||
| 8a6acbaaf2 | |||
| 13ab8e4d01 | |||
| bd039dcbab | |||
| 9cd4e8342b | |||
| c9633cefec | |||
| 1dd1370ed7 | |||
| 9ff90d0acf | |||
| b754435589 | |||
| 195d2cceaf | |||
| 420d78b638 | |||
| b35e499355 | |||
| 41c3d6d015 | |||
| 64610bad41 | |||
| 45b7d7c67f | |||
| 0bb2746869 | |||
| f9b0aa45f8 | |||
| 6f6ce33bbd | |||
| e1c6020997 | |||
| 012bd7e67f | |||
| 35778f70cb | |||
| cb4f9aa752 | |||
| f80fb42483 | |||
| 02c79ca244 | |||
| 50e8c86ce9 | |||
| 6fbfc15be7 | |||
| c793d3cf6b | |||
| b5f4d8cc1b | |||
| 77e6d1f5a4 | |||
| 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 |
@@ -1,2 +1,5 @@
|
||||
* text=auto eol=lf
|
||||
pnpm-lock.yaml linguist-generated
|
||||
package-lock.json linguist-generated
|
||||
bun.lockb linguist-generated
|
||||
yarn.lock linguist-generated
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,6 +5,7 @@ on:
|
||||
jobs:
|
||||
validate:
|
||||
uses: './.github/workflows/validate.yml'
|
||||
secrets: inherit
|
||||
|
||||
publish:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v2
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8
|
||||
- uses: actions/setup-node@v4
|
||||
|
||||
@@ -7,24 +7,12 @@ on:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
formatting:
|
||||
checks:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup
|
||||
- run: pnpm format:check
|
||||
lint:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup
|
||||
- run: pnpm lint
|
||||
type-check:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup
|
||||
- run: pnpm compile
|
||||
- run: pnpm check
|
||||
validate-demo:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
@@ -43,6 +31,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: oven-sh/setup-bun@v1
|
||||
- name: pnpm test:coverage
|
||||
run: pnpm test:coverage --reporter=default --reporter=hanging-process
|
||||
- uses: codecov/codecov-action@v4
|
||||
|
||||
+394
-7
@@ -1,5 +1,392 @@
|
||||
# Changelog
|
||||
|
||||
## v0.17.12
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.11...v0.17.12)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add hooks for extending vite config ([#599](https://github.com/wxt-dev/wxt/pull/599))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- **content-script-ui:** Properly assign and unassign mounted value ([#598](https://github.com/wxt-dev/wxt/pull/598))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Add discord server link ([#593](https://github.com/wxt-dev/wxt/pull/593))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Remove unnecssary 'Omit' types ([db57c8e](https://github.com/wxt-dev/wxt/commit/db57c8e))
|
||||
- **deps-dev:** Bump @aklinker1/check from 1.1.1 to 1.2.0 ([#588](https://github.com/wxt-dev/wxt/pull/588))
|
||||
- **deps-dev:** Bump vue from 3.3.10 to 3.4.21 ([#589](https://github.com/wxt-dev/wxt/pull/589))
|
||||
- **deps-dev:** Bump sass from 1.69.5 to 1.72.0 ([#591](https://github.com/wxt-dev/wxt/pull/591))
|
||||
|
||||
## v0.17.11
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.10...v0.17.11)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Resolve absolute paths from the public directory properly ([#583](https://github.com/wxt-dev/wxt/pull/583))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Fix `zip.includeSources` example ([16fc584](https://github.com/wxt-dev/wxt/commit/16fc584))
|
||||
- Add "GitHub Custom Notifier" to homepage ([#580](https://github.com/wxt-dev/wxt/pull/580))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Simplify virtual module setup ([#581](https://github.com/wxt-dev/wxt/pull/581))
|
||||
- Add some array utils ([#582](https://github.com/wxt-dev/wxt/pull/582))
|
||||
- Extract helper function ([d3b14af](https://github.com/wxt-dev/wxt/commit/d3b14af))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Qiwei Yang ([@qiweiii](http://github.com/qiweiii))
|
||||
|
||||
## v0.17.10
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.9...v0.17.10)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add `dev.server.port` config ([#577](https://github.com/wxt-dev/wxt/pull/577))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Use `@aklinker1/check` to simplify checks setup ([#550](https://github.com/wxt-dev/wxt/pull/550))
|
||||
- Refactor order of config resolution ([#578](https://github.com/wxt-dev/wxt/pull/578))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Zizheng Tai ([@zizhengtai](http://github.com/zizhengtai))
|
||||
|
||||
## v0.17.9
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.8...v0.17.9)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add `{{mode}}` Template Variable ([#566](https://github.com/wxt-dev/wxt/pull/566))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Don't override `wxt.config.ts` options when CLI flags are not passed ([#567](https://github.com/wxt-dev/wxt/pull/567))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Merge user config using `defu` ([#568](https://github.com/wxt-dev/wxt/pull/568))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Guillaume ([@GuiEpi](http://github.com/GuiEpi))
|
||||
|
||||
## v0.17.8
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.7...v0.17.8)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- **analysis:** Open `stats.html` file automatically ([#564](https://github.com/wxt-dev/wxt/pull/564))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- **init:** Better error logging when templates fail to load ([b47c150](https://github.com/wxt-dev/wxt/commit/b47c150))
|
||||
- Remove deprecated extension from Vue template ([#534](https://github.com/wxt-dev/wxt/pull/534))
|
||||
- Append option description error ([#546](https://github.com/wxt-dev/wxt/pull/546))
|
||||
- **init:** Don't overwrite existing files when initializing a new project ([#556](https://github.com/wxt-dev/wxt/pull/556))
|
||||
- **dev:** Don't crash dev mode when rebuild fails ([#565](https://github.com/wxt-dev/wxt/pull/565))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Replace `pnpx` with `pnpm dlx` ([#527](https://github.com/wxt-dev/wxt/pull/527))
|
||||
- Update homepage demo video ([35269da](https://github.com/wxt-dev/wxt/commit/35269da))
|
||||
- Update README demo video ([#539](https://github.com/wxt-dev/wxt/pull/539))
|
||||
- Add Plex skipper to "Using WXT" section ([#541](https://github.com/wxt-dev/wxt/pull/541))
|
||||
- Add documentation for Bun.sh ([#543](https://github.com/wxt-dev/wxt/pull/543))
|
||||
- Mention adding unlisted scripts and pages to `web_accessible_resources` ([121b521](https://github.com/wxt-dev/wxt/commit/121b521))
|
||||
- Add examples for GitHub Actions ([#540](https://github.com/wxt-dev/wxt/pull/540))
|
||||
- Fixed "Reload the Extension" section ([#559](https://github.com/wxt-dev/wxt/pull/559))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Increase unit test timeout ([d9cba55](https://github.com/wxt-dev/wxt/commit/d9cba55))
|
||||
- Increase hook timeout for Windows/NPM tests ([56b7149](https://github.com/wxt-dev/wxt/commit/56b7149))
|
||||
- Switch to seed plugin for testing ([#547](https://github.com/wxt-dev/wxt/pull/547))
|
||||
- **vue-template:** Upgrade to `vue-tsc` v2 ([#549](https://github.com/wxt-dev/wxt/pull/549))
|
||||
- Add a test covering `wxt init` in a non-empty directory ([#563](https://github.com/wxt-dev/wxt/pull/563))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Btea ([@btea](http://github.com/btea))
|
||||
- Vlad Fedosov ([@StyleT](http://github.com/StyleT))
|
||||
- Lpmvb ([@Lpmvb](http://github.com/Lpmvb))
|
||||
- Guillaume ([@GuiEpi](http://github.com/GuiEpi))
|
||||
- Sunshio ([@MPB-Tech](http://github.com/MPB-Tech))
|
||||
- Luca Dalli ([@lucadalli](http://github.com/lucadalli))
|
||||
|
||||
## v0.17.7
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.6...v0.17.7)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- **zip:** List `.wxt/local_modules` overrides relative to the `package.json` they're added to ([#526](https://github.com/wxt-dev/wxt/pull/526))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Bump templates to v0.17 ([#524](https://github.com/wxt-dev/wxt/pull/524))
|
||||
- Increase test timeout for windows NPM tests ([#525](https://github.com/wxt-dev/wxt/pull/525))
|
||||
|
||||
## v0.17.6
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.5...v0.17.6)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add warnings when important directories are missing ([#516](https://github.com/wxt-dev/wxt/pull/516))
|
||||
- Automatically remove top-level MV2-only or MV3-only keys ([#518](https://github.com/wxt-dev/wxt/pull/518))
|
||||
- Automatically generate `browser_action` based on `action` for MV2 ([#519](https://github.com/wxt-dev/wxt/pull/519))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- **zip:** List all private packages correctly in a PNPM workspace ([#520](https://github.com/wxt-dev/wxt/pull/520))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Mentions moving folders into `srcDir` ([9cd4e83](https://github.com/wxt-dev/wxt/commit/9cd4e83))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **deps-dev:** Bump @types/react from 18.2.34 to 18.2.61 ([#510](https://github.com/wxt-dev/wxt/pull/510))
|
||||
|
||||
## v0.17.5
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.4...v0.17.5)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Expose package management utils under `wxt.pm` ([#502](https://github.com/wxt-dev/wxt/pull/502))
|
||||
- Download and override private packages for Firefox code review ([#507](https://github.com/wxt-dev/wxt/pull/507))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Fix typos ([#503](https://github.com/wxt-dev/wxt/pull/503))
|
||||
- Add docs about configuring the manifest as a function ([195d2cc](https://github.com/wxt-dev/wxt/commit/195d2cc))
|
||||
- Fix CLI generation ([b754435](https://github.com/wxt-dev/wxt/commit/b754435))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Use JSZip for `wxt zip`, enabling future features ([#501](https://github.com/wxt-dev/wxt/pull/501))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Btea ([@btea](http://github.com/btea))
|
||||
|
||||
## v0.17.4
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.3...v0.17.4)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add basic content script to templates ([#495](https://github.com/wxt-dev/wxt/pull/495))
|
||||
- Add `ResolvedConfig.wxtModuleDir`, resolving the directory once ([#497](https://github.com/wxt-dev/wxt/pull/497))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Resolve the path to `node_modules/wxt` correctly ([#498](https://github.com/wxt-dev/wxt/pull/498))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Added DocVersionRedirector to "Using WXT" section ([#492](https://github.com/wxt-dev/wxt/pull/492))
|
||||
- Fix typos ([f80fb42](https://github.com/wxt-dev/wxt/commit/f80fb42))
|
||||
- Add CRXJS to comparison page ([cb4f9aa](https://github.com/wxt-dev/wxt/commit/cb4f9aa))
|
||||
- Update comparison page ([35778f7](https://github.com/wxt-dev/wxt/commit/35778f7))
|
||||
- Update context usage ([012bd7e](https://github.com/wxt-dev/wxt/commit/012bd7e))
|
||||
- Add testing example for `ContentScriptContext` ([e1c6020](https://github.com/wxt-dev/wxt/commit/e1c6020))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Fix tests after template change ([f9b0aa4](https://github.com/wxt-dev/wxt/commit/f9b0aa4))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Btea ([@btea](http://github.com/btea))
|
||||
- Leo Shklovskii <leo@thermopylae.net>
|
||||
|
||||
## v0.17.3
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.2...v0.17.3)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- **storage:** Guarantee `storage.getItems` returns values in the same order as requested ([b5f4d8c](https://github.com/wxt-dev/wxt/commit/b5f4d8c))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Content scripts crash when using `storage.defineItem` ([77e6d1f](https://github.com/wxt-dev/wxt/commit/77e6d1f))
|
||||
- **storage:** Revert #478 and run migrations when item is defined and properly wait for migrations before allowing read/writes ([#487](https://github.com/wxt-dev/wxt/pull/487), [#478](https://github.com/wxt-dev/wxt/issues/478))
|
||||
|
||||
## 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)
|
||||
@@ -59,8 +446,8 @@
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Lionelhorn
|
||||
- Okou
|
||||
- Lionelhorn ([@Lionelhorn](https://github.com/Lionelhorn))
|
||||
- Okou ([@ookkoouu](https://github.com/ookkoouu))
|
||||
|
||||
## v0.16.5
|
||||
|
||||
@@ -780,7 +1167,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))
|
||||
|
||||
@@ -1288,8 +1675,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
|
||||
@@ -1349,7 +1736,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
|
||||
|
||||
@@ -1625,7 +2012,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.
|
||||
|
||||
@@ -29,20 +29,22 @@
|
||||
<a href="https://wxt.dev/api/config.html" target="_blank">Configuration</a>
|
||||
•
|
||||
<a href="https://wxt.dev/examples.html" target="_blank">Examples</a>
|
||||
•
|
||||
<a href="https://discord.gg/ZFsZqGery9" target="_blank">Discord</a>
|
||||
</p>
|
||||
|
||||

|
||||
|
||||
## Demo
|
||||
|
||||
https://github.com/wxt-dev/wxt/assets/10101283/07359e53-f491-43b6-8e8f-fae94aec8063
|
||||
https://github.com/wxt-dev/wxt/assets/10101283/4d678939-1bdb-495c-9c36-3aa281d84c94
|
||||
|
||||
## Quick Start
|
||||
|
||||
Bootstrap a new project:
|
||||
|
||||
```sh
|
||||
pnpx wxt@latest init <project-name>
|
||||
pnpm dlx wxt@latest init <project-name>
|
||||
```
|
||||
|
||||
Or see the [installation guide](https://wxt.dev/guide/installation.html) to get started with WXT.
|
||||
|
||||
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,4 +1,4 @@
|
||||
import messages from 'public/_locales/en/messages.json';
|
||||
import messages from '~/public/_locales/en/messages.json';
|
||||
|
||||
export default defineBackground({
|
||||
// type: 'module',
|
||||
|
||||
+5
-2
@@ -12,7 +12,10 @@ export default defineConfig({
|
||||
},
|
||||
],
|
||||
},
|
||||
alias: {
|
||||
public: 'src/public',
|
||||
zip: {
|
||||
downloadPackages: ['sass'],
|
||||
},
|
||||
analysis: {
|
||||
open: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -18,6 +18,13 @@ const chromeExtensionIds = [
|
||||
'lknmjhcajhfbbglglccadlfdjbaiifig', // tl;dv - Record, Transcribe & ChatGPT for Google Meet
|
||||
'youtube中文配音/oglffgiaiekgeicdgkdlnlkhliajdlja', // Youtube中文配音
|
||||
'agjnjboanicjcpenljmaaigopkgdnihi', // PreMiD
|
||||
'aiakblgmlabokilgljkglggnpflljdgp', // Markdown Sticky Notes
|
||||
'nomnkbngkijpffepcgbbofhcnafpkiep', // DocVersionRedirector
|
||||
'ceicccfeikoipigeghddpocceifjelph', // Plex Skipper
|
||||
'aelkipgppclpfimeamgmlonimflbhlgf', // GitHub Custom Notifier
|
||||
'djnlaiohfaaifbibleebjggkghlmcpcj', // Fluent Read
|
||||
'nhclljcpfmmaiojbhhnkpjcfmacfcian', // Facebook Video Controls
|
||||
'mblkhbaakhbhiimkbcnmeciblfhmafna', // ElemSnap - Quick capture of webpage elements and conversion to images
|
||||
];
|
||||
|
||||
const { data, err, isLoading } = useListExtensionDetails(chromeExtensionIds);
|
||||
|
||||
@@ -19,7 +19,7 @@ const title = 'Next-gen Web Extension Framework';
|
||||
const titleSuffix = ' – WXT';
|
||||
|
||||
const description =
|
||||
"WXT provides the best developer experience, making it quick, easy, and fun to develop chrome extensions for all browsers. With built-in utilties for building, zipping, and publishing your extension, it's easy to get started.";
|
||||
"WXT provides the best developer experience, making it quick, easy, and fun to develop chrome extensions for all browsers. With built-in utilities for building, zipping, and publishing your extension, it's easy to get started.";
|
||||
const ogTitle = `${title}${titleSuffix}`;
|
||||
const ogUrl = 'https://wxt.dev';
|
||||
const ogImage = 'https://wxt.dev/social-preview.png';
|
||||
@@ -124,7 +124,7 @@ export default defineConfig({
|
||||
{ text: 'Options', link: '/entrypoints/options.md' },
|
||||
{ text: 'Popup', link: '/entrypoints/popup.md' },
|
||||
{ text: 'Sandbox', link: '/entrypoints/sandbox.md' },
|
||||
{ text: 'Sidepanel', link: '/entrypoints/sidepanel.md' },
|
||||
{ text: 'Side Panel', link: '/entrypoints/sidepanel.md' },
|
||||
{ text: 'Unlisted Pages', link: '/entrypoints/unlisted-pages.md' },
|
||||
{
|
||||
text: 'Unlisted Scripts',
|
||||
@@ -146,6 +146,9 @@ export default defineConfig({
|
||||
],
|
||||
},
|
||||
|
||||
socialLinks: [{ icon: 'github', link: 'https://github.com/wxt-dev/wxt' }],
|
||||
socialLinks: [
|
||||
{ icon: 'discord', link: 'https://discord.gg/ZFsZqGery9' },
|
||||
{ icon: 'github', link: 'https://github.com/wxt-dev/wxt' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -32,14 +32,20 @@ export function generateCliDocs() {
|
||||
'```',
|
||||
];
|
||||
|
||||
const commands = await Promise.all(
|
||||
const commands = await Promise.allSettled(
|
||||
extractCommands(dev.info).map(async (command) => {
|
||||
const res = await execaCommand(`pnpm -s wxt ${command} --help`);
|
||||
const { rest: docs } = splitInfo(res.stdout);
|
||||
return [`## \`wxt ${command}\``, '```sh', docs, '```'];
|
||||
}),
|
||||
);
|
||||
lines.push(...commands);
|
||||
lines.push(
|
||||
...commands
|
||||
.filter((res) => res.status === 'fulfilled')
|
||||
.map(
|
||||
(res) => (res as unknown as PromiseFulfilledResult<string>).value,
|
||||
),
|
||||
);
|
||||
|
||||
const text = await format(
|
||||
PREFACE +
|
||||
|
||||
@@ -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
|
||||
},
|
||||
@@ -49,9 +52,11 @@ When defining multiple content scripts, content script entrypoints that have the
|
||||
|
||||
## Context
|
||||
|
||||
Old content scripts are not automatically stopped when an extension updates and reloads. Often, this leads to "Invalidated context" errors in production when a content script from an old version of your extension tries to use a extension API.
|
||||
Old content scripts are not automatically stopped when an extension updates and reloads. Often, this leads to "Invalidated context" errors in production when a content script from an old version of your extension tries to use a web extension API (ie, the `browser` or `chrome` globals).
|
||||
|
||||
WXT provides a utility for managing this process: `ContentScriptContext`. An instance of this class is provided to you automatically inside the `main` function of your content script.
|
||||
WXT provides a utility for handling this process: `ContentScriptContext`. An instance of this class is provided to you automatically inside the `main` function of your content script.
|
||||
|
||||
When your extension updates or is uninstalled, the context will become invalidated, and will trigger any `ctx.onInvalidated` listeners you add:
|
||||
|
||||
```ts
|
||||
export default defineContentScript({
|
||||
@@ -62,26 +67,33 @@ export default defineContentScript({
|
||||
// ...
|
||||
});
|
||||
|
||||
// Stop fetch requests
|
||||
fetch('...url', { signal: ctx.signal });
|
||||
|
||||
// Timeout utilities
|
||||
// Timeout utilities that are automatically cleared when invalidated
|
||||
ctx.setTimeout(() => {
|
||||
// ...
|
||||
}, 5e3);
|
||||
ctx.setInterval(() => {
|
||||
// ...
|
||||
}, 60e3);
|
||||
|
||||
// Or add event listeners that get removed when invalidated
|
||||
ctx.addEventListener(document, 'visibilitychange', (event) => {
|
||||
// ...
|
||||
});
|
||||
|
||||
// You can also stop fetch requests
|
||||
fetch('...url', { signal: ctx.signal });
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The class extends [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) and provides other utilities for stopping a content script's logic once it becomes invalidated.
|
||||
|
||||
:::tip
|
||||
:::warning
|
||||
When working with content scripts, **you should always use the `ctx` object to stop any async or future work.**
|
||||
|
||||
This prevents old content scripts from interfering with new content scripts, and prevents error messages from the console in production.
|
||||
|
||||
If you're using a framework like React, Vue, Svelte, etc., make sure you're unmounting your UI properly in the `onRemove` option of [`createShadowRootUi`](https://wxt.dev/guide/content-script-ui.html#shadow-root).
|
||||
:::
|
||||
|
||||
## CSS
|
||||
|
||||
@@ -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', ...]" />
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
HTML pages that are bundled and shipped with the extension, but are not included in the manifest.
|
||||
|
||||
If you plan on using the page in an iframe, don't forget to add the page to [`web_accessible_resources`](https://developer.chrome.com/docs/extensions/reference/manifest/web-accessible-resources).
|
||||
|
||||
### Examples
|
||||
|
||||
- Onboarding
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
TypeScript files that are bundled and shipped with the extension, but are not included in the manifest.
|
||||
|
||||
You are responsible for loading/running these scripts where needed.
|
||||
You are responsible for loading/running these scripts where needed. If necessary, don't forget to add the script and/or any related stylesheets to [`web_accessible_resources`](https://developer.chrome.com/docs/extensions/reference/manifest/web-accessible-resources).
|
||||
|
||||
## Filenames
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ Files in the assets directory will be processed by Vite. They are imported in yo
|
||||
<!-- ... -->
|
||||
</head>
|
||||
<body>
|
||||
<img src="~/illustration.svg" />
|
||||
<img src="~/assets/illustration.svg" />
|
||||
<!-- ... -->
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
});
|
||||
|
||||
+30
-29
@@ -1,46 +1,47 @@
|
||||
# Compare
|
||||
|
||||
Lets compare the features of WXT vs [Plasmo](https://docs.plasmo.com/framework), another web extension framework.
|
||||
Lets compare the features of WXT vs [Plasmo](https://docs.plasmo.com/framework) (another web extension framework) and [CRXJS](https://crxjs.dev/vite-plugin) (the most popular bundler plugin).
|
||||
|
||||
## Overview
|
||||
|
||||
| Features | WXT | Plasmo |
|
||||
| ---------------------------------------------------- | :-------------------------: | :--------------------------------------: |
|
||||
| Supports all browsers | ✅ | ✅ |
|
||||
| MV2 Support | ✅ | ✅ |
|
||||
| MV3 Support | ✅ | ✅ |
|
||||
| Create Extension ZIPs | ✅ | ✅ |
|
||||
| Create Firefox Sources ZIP | ✅ | ❌ |
|
||||
| First-class TypeScript support | ✅ | ✅ |
|
||||
| File based entrypoint discovery | ✅ | ✅ |
|
||||
| Inline entrypoint config | ✅ | ✅ |
|
||||
| Auto-imports | ✅ | ❌ |
|
||||
| Supports all frontend frameworks | ✅ | 🟡 Only React, Vue, and Svelte |
|
||||
| Framework specific entrypoints (like `Popup.tsx`) | 🟡 `.html` `.ts` `.tsx` | ✅ `.html` `.ts` `.tsx` `.vue` `.svelte` |
|
||||
| Automated publishing | 🟡 Coming soon | ✅ |
|
||||
| Remote Code Bundling (Google Analytics) | ✅ | ✅ |
|
||||
| Features | WXT | Plasmo | CRXJS |
|
||||
| ---------------------------------------------------- | :-------------------------: | :--------------------------------------: | :---------------------------------------------------------------------: |
|
||||
| Supports all browsers | ✅ | ✅ | ❌ See [#56](https://github.com/crxjs/chrome-extension-tools/issues/56) |
|
||||
| MV2 Support | ✅ | ✅ | 🟡 Either MV2 or MV3 |
|
||||
| MV3 Support | ✅ | ✅ | 🟡 Either MV2 or MV3 |
|
||||
| Create Extension ZIPs | ✅ | ✅ | ❌ |
|
||||
| Create Firefox Sources ZIP | ✅ | ❌ | ❌ |
|
||||
| First-class TypeScript support | ✅ | ✅ | ✅ |
|
||||
| Entrypoint discovery | File based | File based | ❌ |
|
||||
| Inline entrypoint config | ✅ | ✅ | Manifest based |
|
||||
| Auto-imports | ✅ | ❌ | ❌ |
|
||||
| Supports all frontend frameworks | ✅ | 🟡 Only React, Vue, and Svelte | ✅ |
|
||||
| Framework specific entrypoints (like `Popup.tsx`) | 🟡 `.html` `.ts` `.tsx` | ✅ `.html` `.ts` `.tsx` `.vue` `.svelte` | ❌ |
|
||||
| Automated publishing | ✅ | ✅ | ❌ |
|
||||
| Remote Code Bundling (Google Analytics) | ✅ | ✅ | ❌ |
|
||||
| <strong style="opacity: 50%">Dev Mode</strong> | | |
|
||||
| `.env` Files | ✅ | ✅ |
|
||||
| Opens browser and install extension | ✅ | ❌ |
|
||||
| HMR for UIs | ✅ | 🟡 React only |
|
||||
| Reload HTML Files on Change | ✅ | 🟡 Reloads entire extension |
|
||||
| Reload Content Scripts on Change | ✅ | 🟡 Reloads entire extension |
|
||||
| Reload Background on Change | 🟡 Reloads entire extension | 🟡 Reloads entire extension |
|
||||
| <strong style="opacity: 50%">Built-in Utils</strong> | | |
|
||||
| Storage | ✅ | ✅ |
|
||||
| Messaging | 🟡 Coming soon | ✅ |
|
||||
| Content Script UI | ✅ | ✅ |
|
||||
| `.env` Files | ✅ | ✅ | ✅ |
|
||||
| Opens browser and install extension | ✅ | ❌ | ❌ |
|
||||
| HMR for UIs | ✅ | 🟡 React only | ✅ |
|
||||
| Reload HTML Files on Change | ✅ | 🟡 Reloads entire extension | ✅ |
|
||||
| Reload Content Scripts on Change | ✅ | 🟡 Reloads entire extension | ✅ |
|
||||
| Reload Background on Change | 🟡 Reloads entire extension | 🟡 Reloads entire extension | 🟡 Reloads entire extension |
|
||||
| Respects Content Script `run_at` | ✅ | ✅ | ❌ ESM-style loaders run asynchronously |
|
||||
| <strong style="opacity: 50%">Built-in Utils</strong> | | | |
|
||||
| Storage | ✅ | ✅ | ❌ |
|
||||
| Messaging | ❌ | ✅ | ❌ |
|
||||
| Content Script UI | ✅ | ✅ | ❌ |
|
||||
|
||||
## Dev Mode
|
||||
## Why use WXT?
|
||||
|
||||
WXT's main goal is improving the development experience (DX) of creating web extensions. There are two things WXT does differently:
|
||||
WXT's main goal is improving the development experience (DX) of creating web extensions, while not sacrificing support. There are two things WXT does differently:
|
||||
|
||||
1. Automatically opens a browser with the extension installed when starting development
|
||||
2. Reload each part of the extension individually rather than reloading the entire extension
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -157,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
|
||||
|
||||
@@ -168,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
|
||||
|
||||
@@ -253,8 +253,12 @@ export default defineContentScript({
|
||||
name: 'example-ui',
|
||||
position: 'inline',
|
||||
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;
|
||||
},
|
||||
@@ -364,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({
|
||||
|
||||
@@ -15,9 +15,9 @@ WXT's main goal is providing the best DX it possibly can. When running your exte
|
||||
|
||||
## Dev Mode vs Production Builds
|
||||
|
||||
There are some notible differences between the development and production versions of an extension. During development:
|
||||
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 targetting 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.
|
||||
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:
|
||||
|
||||
@@ -59,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
|
||||
@@ -99,7 +99,17 @@ For a full list of options, see the [API Reference](/api/wxt/interfaces/Extensio
|
||||
|
||||
Normally, to manually reload an extension, you have to visit `chrome://extensions` and click the reload button for your 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`.
|
||||
When running `wxt` command to start the dev server, WXT adds a keyboard shortcut `Alt+R`, that reloads the extension when pressed, without visiting `chrome://extensions`. This can also be customized or disabled:
|
||||
|
||||
```ts [wxt.config.ts]
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
dev: {
|
||||
reloadCommand: 'Alt+T', // false, to disable
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
:::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:
|
||||
|
||||
@@ -89,7 +89,7 @@ For a full list of entrypoints and each of their options, see the [`/entrypoints
|
||||
|
||||
### Side Effects
|
||||
|
||||
You cannot use imported variables outside the `main` function is JS entrypoints. This includes options, as shown below:
|
||||
You cannot use imported variables outside the `main` function in JS entrypoints. This includes options, as shown below:
|
||||
|
||||
```ts
|
||||
// entrypoints/content.ts
|
||||
@@ -114,11 +114,11 @@ WXT 0.14.1
|
||||
[8:55:54 AM] ERROR entrypoints/content.ts: Cannot use imported variable "GOOGLE_MATCHES" before main function. See https://wxt.dev/guide/entrypoints.html#side-effects
|
||||
```
|
||||
|
||||
This throws an error because WXT needs to import each entrypoint during the build process to extract its definition (containing the `match`, `run_at`, `include`/`exclude`, etc.) to render the `manifest.json` correctly. Before loading an entrypoint, a transformation is applied to remove all imports. This prevents imported modules (local or NPM) with side-effects from running during the build process, potentially throwing an error.
|
||||
This throws an error because WXT needs to import each entrypoint during the build process to extract its definition (containing the `match`, `runAt`, `include`/`exclude`, etc.) to render the `manifest.json` correctly. Before loading an entrypoint, a transformation is applied to remove all imports. This prevents imported modules (local or NPM) with side-effects from running during the build process, potentially throwing an error.
|
||||
|
||||
:::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
|
||||
|
||||
@@ -39,9 +39,9 @@ Then we can use `browser.storage` to save the install date to local storage.
|
||||
```ts
|
||||
// background.ts
|
||||
export default defineBackground(() => {
|
||||
browser.runtime.onInstall.addEventListener(({ reason }) => {
|
||||
browser.runtime.onInstalled.addListener(({ reason }) => {
|
||||
if (reason === 'install') {
|
||||
browser.storage.local.setItem({ installDate: Date.now() });
|
||||
browser.storage.local.set({ installDate: Date.now() });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,13 +7,17 @@ Bootstrap a new project, start from scratch, or [migrate an existing project](/g
|
||||
:::code-group
|
||||
|
||||
```sh [pnpm]
|
||||
pnpx wxt@latest init <project-name>
|
||||
pnpm dlx wxt@latest init <project-name>
|
||||
```
|
||||
|
||||
```sh [npm]
|
||||
npx wxt@latest init <project-name>
|
||||
```
|
||||
|
||||
```sh [bun]
|
||||
bunx wxt@latest init <project-name>
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
There are several starting templates available.
|
||||
@@ -55,6 +59,12 @@ cd project-name
|
||||
yarn init
|
||||
```
|
||||
|
||||
```sh [bun]
|
||||
mkdir project-name
|
||||
cd project-name
|
||||
bun init
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
Then install `wxt`:
|
||||
@@ -73,6 +83,10 @@ npm i --save-dev wxt
|
||||
yarn add --dev wxt
|
||||
```
|
||||
|
||||
```sh [bun]
|
||||
bun add --dev wxt
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
Add your first entrypoint:
|
||||
@@ -102,7 +116,7 @@ Finally, add scripts to your `package.json`:
|
||||
|
||||
## Migrate an Existing Project
|
||||
|
||||
Before starting the migration, it is recommended to run `pnpx wxt@latest init` to see what a basic project looks like. Once you have an understanding of how WXT projects are structured, you're ready to convert the project, using the initialized project as a reference.
|
||||
Before starting the migration, it is recommended to run `pnpm dlx wxt@latest init` to see what a basic project looks like. Once you have an understanding of how WXT projects are structured, you're ready to convert the project, using the initialized project as a reference.
|
||||
|
||||
Migrating a project to WXT comes down to a few steps:
|
||||
|
||||
@@ -133,7 +147,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 +161,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
|
||||
|
||||
@@ -20,7 +20,7 @@ WXT is an opinionated framework. This helps keep projects consistent and easy to
|
||||
- **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
|
||||
|
||||
|
||||
+116
-6
@@ -1,3 +1,7 @@
|
||||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Manifest.json
|
||||
|
||||
The manifest.json is generated at build-time based on files in the `entrypoints/` directory and `wxt.config.ts`.
|
||||
@@ -7,7 +11,7 @@ The manifest.json is generated at build-time based on files in the `entrypoints/
|
||||
While entrypoints are generated and added to the manifest at build-time, you can customize or add to your `manifest.json` in the config file.
|
||||
|
||||
```ts
|
||||
// wxt.config.tsentrypoint of your extension
|
||||
// wxt.config.ts entrypoint of your extension
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
@@ -20,11 +24,11 @@ export default defineConfig({
|
||||
});
|
||||
```
|
||||
|
||||
## `name`
|
||||
### `name`
|
||||
|
||||
If not provided via the `manifest` config, the [manifest's `name`](https://developer.chrome.com/docs/extensions/mv3/manifest/name/) defaults to your package.json's `name` property.
|
||||
|
||||
## `version` and `version_name`
|
||||
### `version` and `version_name`
|
||||
|
||||
The [manifest's `version` and `version_name`](https://developer.chrome.com/docs/extensions/mv3/manifest/version/) properties are based on the `version` field listed in your `package.json` or `wxt.config.ts`.
|
||||
|
||||
@@ -33,7 +37,7 @@ The [manifest's `version` and `version_name`](https://developer.chrome.com/docs/
|
||||
|
||||
If a version is not found, a warning is logged and the version defaults to `"0.0.0"`.
|
||||
|
||||
### Example
|
||||
#### Example
|
||||
|
||||
```json
|
||||
// package.json
|
||||
@@ -50,7 +54,7 @@ If a version is not found, a warning is logged and the version defaults to `"0.0
|
||||
}
|
||||
```
|
||||
|
||||
## `icons`
|
||||
### `icons`
|
||||
|
||||
By default, WXT will discover icons in your [`public` directory](/guide/assets#public-directory) and use them for the [manifest's `icons`](https://developer.chrome.com/docs/extensions/mv3/manifest/icons/).
|
||||
|
||||
@@ -83,7 +87,7 @@ export default defineConfig({
|
||||
});
|
||||
```
|
||||
|
||||
## `permissions`
|
||||
### `permissions`
|
||||
|
||||
[Permissions](https://developer.chrome.com/docs/extensions/reference/permissions/) must be listed in the manifest config.
|
||||
|
||||
@@ -125,3 +129,109 @@ export default defineConfig({
|
||||
See the official localization examples for more details:
|
||||
|
||||
<ExampleList tag="i18n" />
|
||||
|
||||
## Per-Manifest Version Config
|
||||
|
||||
WXT applies several transformations to your manifest to simplify managing both MV2 and MV3 keys in your `wxt.config.ts` file:
|
||||
|
||||
1. Top level MV2-only or MV3-only keys are stripped from the final manifest when targeting the other manifest version
|
||||
2. Some keys, are automatically converted between versions when possible:
|
||||
- Define `web_accessible_resources` in it's MV3 style and it will be converted to the MV2 style automatically
|
||||
- `action` will automatically be converted to `browser_action` for MV3. To use `page_action` instead, add both `action` and `page_action` entries to your manifest
|
||||
|
||||
For example, a `wxt.config.ts` file that looks like this:
|
||||
|
||||
```ts
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
mainfest: {
|
||||
action: {
|
||||
default_title: 'Some Title',
|
||||
},
|
||||
web_accessible_resources: [
|
||||
{
|
||||
matches: ['*://*.google.com/*'],
|
||||
resources: ['icon/*.png'],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Will be output differently for each manifest version:
|
||||
|
||||
:::code-group
|
||||
|
||||
```json [MV2]
|
||||
{
|
||||
"manifest_version": 2,
|
||||
// ...
|
||||
"browser_action": {
|
||||
"default_title": "Some Title"
|
||||
},
|
||||
"web_accessible_resources": ["icon/*.png"]
|
||||
}
|
||||
```
|
||||
|
||||
```json [MV3]
|
||||
{
|
||||
"manifest_version": 3,
|
||||
// ...
|
||||
"action": {
|
||||
"default_title": "Some Title"
|
||||
},
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"matches": ["*://*.google.com/*"],
|
||||
"resources": ["icon/*.png"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::tip
|
||||
If this isn't enough control for your use-case, remember you can use a function for the `manifest` key and generate it however you'd like, or you can use the `build:manifestGenerated` hook to apply additional transformations.
|
||||
:::
|
||||
|
||||
## Per-Browser Configuration
|
||||
|
||||
The `manifest` field can be a function. If you are building and extension for multiple browsers, and need to modify the manifest per browser, using a function instead of an object is very useful.
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
manifest: ({ browser, manifestVersion, mode, command }) => {
|
||||
return {
|
||||
// Your manifest
|
||||
};
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
:::info
|
||||
The first argument is of type `ConfigEnv`. See the [API reference](/api/wxt/interfaces/ConfigEnv) for info about each property.
|
||||
:::
|
||||
|
||||
For example, say you use OAuth, and you need to provide a different `oauth.client_id` for each browser:
|
||||
|
||||
```ts
|
||||
const clientIds = {
|
||||
chrome: '<your-chrome-client-id>',
|
||||
edge: '<your-edge-client-id>',
|
||||
firefox: '<your-firefox-client-id>',
|
||||
opera: '<your-opera-client-id>',
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
manifest: ({ browser }) => ({
|
||||
oauth: {
|
||||
client_id: clientIds[browser],
|
||||
scopes: [
|
||||
// ...
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
@@ -12,7 +12,7 @@ Always start by generating a new vanilla project and merging it into your projec
|
||||
|
||||
```sh
|
||||
cd path/to/your/project
|
||||
pnpx wxt@latest init example-wxt --template vanilla
|
||||
pnpm dlx wxt@latest init example-wxt --template vanilla
|
||||
```
|
||||
|
||||
In general, you'll need to:
|
||||
@@ -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,12 +64,59 @@ 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 that submits new versions of an extension for review. Ensure that you've added all required secrets used in the workflow to the repo's settings.
|
||||
|
||||
```yml
|
||||
# TODO
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
submit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 'latest'
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Zip extensions
|
||||
run: |
|
||||
pnpm zip
|
||||
pnpm zip:firefox
|
||||
|
||||
- name: Submit to stores
|
||||
run: |
|
||||
pnpm wxt submit \
|
||||
--chrome-zip .output/*-chrome.zip \
|
||||
--firefox-zip .output/*-firefox.zip --firefox-sources-zip .output/*-sources.zip
|
||||
env:
|
||||
CHROME_EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}
|
||||
CHROME_CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }}
|
||||
CHROME_CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }}
|
||||
CHROME_REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }}
|
||||
FIREFOX_EXTENSION_ID: ${{ secrets.FIREFOX_EXTENSION_ID }}
|
||||
FIREFOX_JWT_ISSUER: ${{ secrets.FIREFOX_JWT_ISSUER }}
|
||||
FIREFOX_JWT_SECRET: ${{ secrets.FIREFOX_JWT_SECRET }}
|
||||
```
|
||||
|
||||
The action above lays the foundation for a basic workflow, including `zip` and `submit` steps. To further enhance your GitHub Action and delve into more complex scenarios, consider exploring the following examples from real projects. They introduce advanced features such as version management, changelog generation, and GitHub releases, tailored for different needs:
|
||||
|
||||
- [`aklinker1/github-better-line-counts`](https://github.com/aklinker1/github-better-line-counts/blob/main/.github/workflows/submit.yml) - Conventional commits, automated version bump and changelog generation, triggered manually, optional dry run for testing
|
||||
- [`GuiEpi/plex-skipper`](https://github.com/GuiEpi/plex-skipper/blob/main/.github/workflows/deploy.yml) - Triggered automatically when `package.json` version is changed, creates and uploads artifacts to GitHub release.
|
||||
|
||||
> These examples are designed to provide clear insights and are a good starting point for customizing your own workflows. Feel free to explore and adapt them to your project needs.
|
||||
|
||||
## Stores
|
||||
|
||||
### Chrome Web Store
|
||||
@@ -124,6 +171,11 @@ yarn
|
||||
yarn zip:firefox
|
||||
```
|
||||
|
||||
```sh [bun]
|
||||
bun i
|
||||
bun zip:firefox
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
Make sure the build output is the exact same when running `wxt build -b firefox` in your main project and inside the zipped sources.
|
||||
@@ -136,6 +188,28 @@ See Issue [#377](https://github.com/wxt-dev/wxt/issues/377) for more details.
|
||||
|
||||
Ensure that you have a `README.md` or `SOURCE_CODE_REVIEW.md` file with the above commands so that the Firefox team knows how to build your extension.
|
||||
|
||||
#### Private Packages
|
||||
|
||||
If you use private packages and you don't want to provide your auth token to the Firefox team during the review process, you can use `zip.downloadPackages` to download any private packages and include them in the zip.
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
zip: {
|
||||
downloadPackages: [
|
||||
'@mycompany/some-package',
|
||||
//...
|
||||
],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Depending on your package manager, the `package.json` in the sources zip will be modified to use the downloaded dependencies via the `overrides` or `resolutions` field.
|
||||
|
||||
:::warning
|
||||
WXT uses the command `npm pack <package-name>` to download the package. That means regardless of your package manager, you need to properly setup a `.npmrc` file. NPM and PNPM both respect `.npmrc` files, but Yarn and Bun have their own ways of authorizing private registries, so you'll need to add an `.npmrc` file.
|
||||
:::
|
||||
|
||||
### Safari
|
||||
|
||||
> 🚧 Not supported yet
|
||||
|
||||
+14
-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,7 @@ export const ignoredWebsites = storage.defineItem<IgnoredWebsiteV2[]>( // [!code
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### Running Migrations
|
||||
|
||||
As soon as `storage.defineItem` is called, WXT checks if migrations need to be ran, and if so, runs them. Calls to get or update the storage item's value or metadata (`getValue`, `setValue`, `removeValue`, `getMeta`, etc) will automatically wait for the migration process to finish before actually reading or writing values.
|
||||
|
||||
+3
-3
@@ -62,7 +62,7 @@ features:
|
||||
linkText: See templates
|
||||
- icon: 📏
|
||||
title: Bundle Analysis
|
||||
details: Tools for analyizing the final extension bundle and minimizing your extension's size.
|
||||
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.
|
||||
@@ -70,14 +70,14 @@ features:
|
||||
linkText: Read docs
|
||||
---
|
||||
|
||||
<section class="vp-doc">
|
||||
<section class="vp-doc" style="padding: 0 48px">
|
||||
<div class="container">
|
||||
<h2>Put <span style="color: var(--vp-c-brand-1)">Developer Experience</span> First</h2>
|
||||
<p>
|
||||
WXT's simplifies the chrome extension development process by providing tools for zipping and publishing, the best-in-class dev mode, an opinionated project structure, and more. Iterate faster, develop features not build scripts, and use everything the JS ecosystem has to offer.
|
||||
</p>
|
||||
<div style="margin: auto; width: 100%; max-width: 900px; text-align: center">
|
||||
<video src="https://github.com/wxt-dev/wxt/assets/10101283/b32e6766-ec11-45a4-9677-226ee4718e1c" controls></video>
|
||||
<video src="https://github.com/wxt-dev/wxt/assets/10101283/4d678939-1bdb-495c-9c36-3aa281d84c94" controls></video>
|
||||
<br />
|
||||
<small>
|
||||
And who doesn't appreciate a beautiful CLI?
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { TestProject } from '../utils';
|
||||
import { resetBundleIncrement } from '~/core/builders/vite/plugins';
|
||||
import open from 'open';
|
||||
|
||||
vi.mock('open');
|
||||
const openMock = vi.mocked(open);
|
||||
|
||||
vi.mock('ci-info', () => ({
|
||||
isCI: false,
|
||||
}));
|
||||
|
||||
describe('Analysis', () => {
|
||||
beforeEach(() => {
|
||||
resetBundleIncrement();
|
||||
});
|
||||
|
||||
it('should outptut a stats.html with no part files by default', async () => {
|
||||
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');
|
||||
@@ -84,10 +92,29 @@ describe('Analysis', () => {
|
||||
keepArtifacts: true,
|
||||
},
|
||||
});
|
||||
console.log(project.root);
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
it('should open the stats in the browser 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,
|
||||
open: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(openMock).toBeCalledTimes(1);
|
||||
expect(openMock).toBeCalledWith(project.resolvePath('stats.html'));
|
||||
});
|
||||
});
|
||||
|
||||
+16
-3
@@ -9,16 +9,21 @@ const hooks: WxtHooks = {
|
||||
'build:manifestGenerated': vi.fn(),
|
||||
'entrypoints:resolved': vi.fn(),
|
||||
'entrypoints:grouped': vi.fn(),
|
||||
'vite:build:extendConfig': vi.fn(),
|
||||
'vite:devServer:extendConfig': vi.fn(),
|
||||
};
|
||||
|
||||
function expectHooksToBeCalled(called: Record<keyof WxtHooks, boolean>) {
|
||||
function expectHooksToBeCalled(
|
||||
called: Record<keyof WxtHooks, boolean | number>,
|
||||
) {
|
||||
Object.keys(hooks).forEach((key) => {
|
||||
const hookName = key as keyof WxtHooks;
|
||||
const times = called[hookName] ? 1 : 0;
|
||||
const value = called[hookName];
|
||||
const times = typeof value === 'number' ? value : value ? 1 : 0;
|
||||
expect(
|
||||
hooks[hookName],
|
||||
`Expected "${hookName}" to be called ${times} time(s)`,
|
||||
).toBeCalledTimes(called[hookName] ? 1 : 0);
|
||||
).toBeCalledTimes(times);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -40,6 +45,8 @@ describe('Hooks', () => {
|
||||
'build:manifestGenerated': false,
|
||||
'entrypoints:grouped': false,
|
||||
'entrypoints:resolved': true,
|
||||
'vite:build:extendConfig': false,
|
||||
'vite:devServer:extendConfig': false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -56,6 +63,8 @@ describe('Hooks', () => {
|
||||
'build:manifestGenerated': true,
|
||||
'entrypoints:grouped': true,
|
||||
'entrypoints:resolved': true,
|
||||
'vite:build:extendConfig': 1,
|
||||
'vite:devServer:extendConfig': false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -72,6 +81,8 @@ describe('Hooks', () => {
|
||||
'build:manifestGenerated': true,
|
||||
'entrypoints:grouped': true,
|
||||
'entrypoints:resolved': true,
|
||||
'vite:build:extendConfig': 1,
|
||||
'vite:devServer:extendConfig': false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -94,6 +105,8 @@ describe('Hooks', () => {
|
||||
'build:manifestGenerated': true,
|
||||
'entrypoints:grouped': true,
|
||||
'entrypoints:resolved': true,
|
||||
'vite:build:extendConfig': 2,
|
||||
'vite:devServer:extendConfig': 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest';
|
||||
import { TestProject } from '../utils';
|
||||
import { execaCommand } from 'execa';
|
||||
import glob from 'fast-glob';
|
||||
import { mkdir, writeJson } from 'fs-extra';
|
||||
|
||||
describe('Init command', () => {
|
||||
it('should download and create a template', async () => {
|
||||
@@ -25,6 +26,7 @@ describe('Init command', () => {
|
||||
"assets/vue.svg",
|
||||
"components/HelloWorld.vue",
|
||||
"entrypoints/background.ts",
|
||||
"entrypoints/content.ts",
|
||||
"entrypoints/popup/App.vue",
|
||||
"entrypoints/popup/index.html",
|
||||
"entrypoints/popup/main.ts",
|
||||
@@ -41,4 +43,17 @@ describe('Init command', () => {
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should throw an error if the directory is not empty', async () => {
|
||||
const project = new TestProject();
|
||||
await mkdir(project.root);
|
||||
await writeJson(project.resolvePath('package.json'), {});
|
||||
|
||||
await expect(() =>
|
||||
execaCommand(`pnpm -s wxt init ${project.root} -t vue --pm npm`, {
|
||||
env: { ...process.env, CI: 'true' },
|
||||
stdio: 'ignore',
|
||||
}),
|
||||
).rejects.toThrowError('Command failed with exit code 1:');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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-BsZv2eRn.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 () => {
|
||||
@@ -362,11 +362,13 @@ describe('Output Directory Structure', () => {
|
||||
function logHello(name) {
|
||||
console.log(\`Hello \${name}!\`);
|
||||
}
|
||||
_background;
|
||||
const definition = defineBackground({
|
||||
main() {
|
||||
logHello("background");
|
||||
}
|
||||
});
|
||||
_background;
|
||||
chrome;
|
||||
function print(method, ...args) {
|
||||
return;
|
||||
@@ -381,7 +383,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) {
|
||||
@@ -389,6 +391,7 @@ describe('Output Directory Structure', () => {
|
||||
throw err;
|
||||
}
|
||||
})();
|
||||
_background;
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { TestProject } from '../utils';
|
||||
import extract from 'extract-zip';
|
||||
import { execaCommand } from 'execa';
|
||||
import { readFile, writeFile } from 'fs-extra';
|
||||
|
||||
process.env.WXT_PNPM_IGNORE_WORKSPACE = 'true';
|
||||
|
||||
describe('Zipping', () => {
|
||||
it('should download packages and produce a valid build when zipping sources', async () => {
|
||||
const project = new TestProject({
|
||||
name: 'test',
|
||||
version: '1.0.0',
|
||||
dependencies: {
|
||||
flatten: '1.0.3',
|
||||
},
|
||||
});
|
||||
project.addFile(
|
||||
'entrypoints/background.ts',
|
||||
'export default defineBackground(() => {});',
|
||||
);
|
||||
const unzipDir = project.resolvePath('.output/test-1.0.0-sources');
|
||||
const sourcesZip = project.resolvePath('.output/test-1.0.0-sources.zip');
|
||||
|
||||
await project.zip({
|
||||
browser: 'firefox',
|
||||
zip: { downloadPackages: ['flatten'] },
|
||||
});
|
||||
expect(await project.fileExists('.output/')).toBe(true);
|
||||
|
||||
await extract(sourcesZip, { dir: unzipDir });
|
||||
// Update package json wxt path
|
||||
const packageJsonPath = project.resolvePath(unzipDir, 'package.json');
|
||||
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'));
|
||||
packageJson.dependencies.wxt = '../../../../..';
|
||||
await writeFile(
|
||||
packageJsonPath,
|
||||
JSON.stringify(packageJson, null, 2),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
// Build zipped extension
|
||||
await expect(
|
||||
execaCommand('pnpm i --ignore-workspace --frozen-lockfile false', {
|
||||
cwd: unzipDir,
|
||||
}),
|
||||
).resolves.toMatchObject({ exitCode: 0 });
|
||||
await expect(
|
||||
execaCommand('pnpm wxt build -b firefox', { cwd: unzipDir }),
|
||||
).resolves.toMatchObject({ exitCode: 0 });
|
||||
|
||||
await expect(project.fileExists(unzipDir, '.output')).resolves.toBe(true);
|
||||
expect(
|
||||
await project.serializeFile(
|
||||
project.resolvePath(unzipDir, 'package.json'),
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
".output/test-1.0.0-sources/package.json
|
||||
----------------------------------------
|
||||
{
|
||||
"name": "test",
|
||||
"description": "Example description",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"wxt": "../../../../..",
|
||||
"flatten": "1.0.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"flatten@1.0.3": "file://./.wxt/local_modules/flatten-1.0.3.tgz"
|
||||
}
|
||||
}"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should correctly apply template variables for zip file names based on provided config', async () => {
|
||||
const project = new TestProject({
|
||||
name: 'test',
|
||||
version: '1.0.0',
|
||||
});
|
||||
project.addFile(
|
||||
'entrypoints/background.ts',
|
||||
'export default defineBackground(() => {});',
|
||||
);
|
||||
const artifactZip = '.output/test-1.0.0-firefox-development.zip';
|
||||
const sourcesZip = '.output/test-1.0.0-development-sources.zip';
|
||||
|
||||
await project.zip({
|
||||
browser: 'firefox',
|
||||
mode: 'development',
|
||||
zip: {
|
||||
artifactTemplate: '{{name}}-{{version}}-{{browser}}-{{mode}}.zip',
|
||||
sourcesTemplate: '{{name}}-{{version}}-{{mode}}-sources.zip',
|
||||
},
|
||||
});
|
||||
|
||||
expect(await project.fileExists(artifactZip)).toBe(true);
|
||||
expect(await project.fileExists(sourcesZip)).toBe(true);
|
||||
});
|
||||
});
|
||||
+3
-3
@@ -163,7 +163,7 @@ 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.
|
||||
*/
|
||||
@@ -175,8 +175,8 @@ export class TestProject {
|
||||
].join(`\n${''.padEnd(40, '-')}\n`);
|
||||
}
|
||||
|
||||
fileExists(path: string): Promise<boolean> {
|
||||
return fs.exists(this.resolvePath(path));
|
||||
fileExists(...path: string[]): Promise<boolean> {
|
||||
return fs.exists(this.resolvePath(...path));
|
||||
}
|
||||
|
||||
async getOutputManifest(
|
||||
|
||||
+19
-16
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wxt",
|
||||
"type": "module",
|
||||
"version": "0.16.8",
|
||||
"version": "0.17.13-alpha1",
|
||||
"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",
|
||||
@@ -84,14 +87,9 @@
|
||||
"scripts": {
|
||||
"wxt": "tsx src/cli/index.ts",
|
||||
"build": "tsx scripts/build.ts",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"lint": "run-p -c -s lint:*",
|
||||
"lint:eslint": "echo 'ESLint: TODO'",
|
||||
"lint:package": "publint",
|
||||
"compile": "run-s -c compile:*",
|
||||
"compile:wxt": "tsc --noEmit",
|
||||
"compile:virtual": "tsc --noEmit -p src/virtual",
|
||||
"check": "run-s -c check:*",
|
||||
"check:default": "check",
|
||||
"check:tsc-virtual": "tsc --noEmit -p src/virtual",
|
||||
"test": "vitest",
|
||||
"test:coverage": "vitest run --coverage.enabled \"--coverage.include=src/**\" \"--coverage.exclude=src/core/utils/testing/**\" \"--coverage.exclude=**/*.d.ts\"",
|
||||
"prepare": "simple-git-hooks",
|
||||
@@ -103,6 +101,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",
|
||||
@@ -111,6 +110,7 @@
|
||||
"c12": "^1.5.1",
|
||||
"cac": "^6.7.14",
|
||||
"chokidar": "^3.5.3",
|
||||
"ci-info": "^4.0.0",
|
||||
"consola": "^3.2.3",
|
||||
"defu": "^6.1.3",
|
||||
"dequal": "^2.0.3",
|
||||
@@ -124,23 +124,24 @@
|
||||
"is-wsl": "^3.0.0",
|
||||
"jiti": "^1.21.0",
|
||||
"json5": "^2.2.3",
|
||||
"jszip": "^3.10.1",
|
||||
"linkedom": "^0.16.1",
|
||||
"manage-path": "^2.0.0",
|
||||
"minimatch": "^9.0.3",
|
||||
"natural-compare": "^1.4.0",
|
||||
"normalize-path": "^3.0.0",
|
||||
"nypm": "^0.3.6",
|
||||
"open": "^10.1.0",
|
||||
"ora": "^7.0.1",
|
||||
"picocolors": "^1.0.0",
|
||||
"prompts": "^2.4.2",
|
||||
"publish-browser-extension": "^2.1.2",
|
||||
"rollup-plugin-visualizer": "^5.9.2",
|
||||
"publish-browser-extension": "^2.1.3",
|
||||
"unimport": "^3.4.0",
|
||||
"vite": "^5.1.3",
|
||||
"vite": "^5.2.8",
|
||||
"web-ext-run": "^0.2.0",
|
||||
"webextension-polyfill": "^0.10.0",
|
||||
"zip-dir": "^2.0.0"
|
||||
"webextension-polyfill": "^0.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.1.1",
|
||||
"@faker-js/faker": "^8.3.1",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/lodash.merge": "^4.6.9",
|
||||
@@ -150,6 +151,7 @@
|
||||
"@types/prompts": "^2.4.9",
|
||||
"@vitest/coverage-v8": "^1.0.1",
|
||||
"execa": "^8.0.1",
|
||||
"extract-zip": "^2.0.1",
|
||||
"happy-dom": "^13.3.8",
|
||||
"lint-staged": "^15.2.0",
|
||||
"lodash.merge": "^4.6.2",
|
||||
@@ -167,6 +169,7 @@
|
||||
"vitepress": "1.0.0-rc.34",
|
||||
"vitest": "^1.2.2",
|
||||
"vitest-mock-extended": "^1.3.1",
|
||||
"vitest-plugin-random-seed": "^1.0.2",
|
||||
"vue": "^3.3.10"
|
||||
},
|
||||
"packageManager": "pnpm@8.6.3",
|
||||
|
||||
Generated
+701
-269
File diff suppressed because it is too large
Load Diff
+12
-17
@@ -8,6 +8,10 @@ import { consola } from 'consola';
|
||||
import pMap from 'p-map';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
virtualModuleNames,
|
||||
virtualEntrypointModuleNames,
|
||||
} from '~/core/utils/virtual-modules';
|
||||
|
||||
const spinnerText = 'Building WXT';
|
||||
const spinner = ora(spinnerText).start();
|
||||
@@ -20,12 +24,7 @@ const preset = {
|
||||
dts: true,
|
||||
silent: true,
|
||||
sourcemap: false,
|
||||
external: [
|
||||
'virtual:user-unlisted-script',
|
||||
'virtual:user-content-script-isolated-world',
|
||||
'virtual:user-content-script-main-world',
|
||||
'virtual:user-background',
|
||||
],
|
||||
external: virtualEntrypointModuleNames.map((name) => `virtual:user-${name}`),
|
||||
} satisfies tsup.Options;
|
||||
|
||||
function spinnerPMap(configs: tsup.Options[]) {
|
||||
@@ -74,17 +73,13 @@ const config: tsup.Options[] = [
|
||||
},
|
||||
{
|
||||
...preset,
|
||||
entry: {
|
||||
'virtual/background-entrypoint': 'src/virtual/background-entrypoint.ts',
|
||||
'virtual/content-script-isolated-world-entrypoint':
|
||||
'src/virtual/content-script-isolated-world-entrypoint.ts',
|
||||
'virtual/content-script-main-world-entrypoint':
|
||||
'src/virtual/content-script-main-world-entrypoint.ts',
|
||||
'virtual/mock-browser': 'src/virtual/mock-browser.ts',
|
||||
'virtual/reload-html': 'src/virtual/reload-html.ts',
|
||||
'virtual/unlisted-script-entrypoint':
|
||||
'src/virtual/unlisted-script-entrypoint.ts',
|
||||
},
|
||||
entry: virtualModuleNames.reduce<Record<string, string>>(
|
||||
(acc, moduleName) => {
|
||||
acc[`virtual/${moduleName}`] = `src/virtual/${moduleName}.ts`;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
),
|
||||
format: ['esm'],
|
||||
splitting: false,
|
||||
dts: false,
|
||||
|
||||
+471
-417
@@ -1,7 +1,15 @@
|
||||
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';
|
||||
|
||||
/**
|
||||
* This works because fakeBrowser is synchronous, 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));
|
||||
}
|
||||
|
||||
describe('Storage Utils', () => {
|
||||
beforeEach(() => {
|
||||
@@ -68,10 +76,7 @@ describe('Storage Utils', () => {
|
||||
|
||||
const actual = await storage.getItems(params);
|
||||
|
||||
expect(actual).toHaveLength(3);
|
||||
expected.forEach((item) => {
|
||||
expect(actual).toContainEqual(item);
|
||||
});
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -384,417 +389,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 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 }>(`local:count`, {
|
||||
defaultValue: 0,
|
||||
version: 3,
|
||||
migrations: {
|
||||
2: migrateToV2,
|
||||
3: migrateToV3,
|
||||
},
|
||||
});
|
||||
await waitForMigrations();
|
||||
|
||||
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 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.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 waitForMigrations();
|
||||
|
||||
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 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.local.set({
|
||||
count: 0,
|
||||
count$: { v: prevVersion },
|
||||
});
|
||||
|
||||
const item = storage.defineItem(`local:count`, {
|
||||
defaultValue: 0,
|
||||
version: nextVersion,
|
||||
});
|
||||
await waitForMigrations();
|
||||
|
||||
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, {}>>();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ export type {
|
||||
Identity,
|
||||
Idle,
|
||||
Management,
|
||||
Manifest,
|
||||
Manifest, // TODO: Export custom manifest types that are valid for both Chrome and Firefox.
|
||||
ContextMenus,
|
||||
Menus,
|
||||
NetworkStatus,
|
||||
|
||||
@@ -116,6 +116,20 @@ describe('CLI', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing --port', async () => {
|
||||
const expectedPort = 3100;
|
||||
mockArgv('--port', String(expectedPort));
|
||||
await importCli();
|
||||
|
||||
expect(createServerMock).toBeCalledWith({
|
||||
dev: {
|
||||
server: {
|
||||
port: expectedPort,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing --debug', async () => {
|
||||
mockArgv('--debug');
|
||||
await importCli();
|
||||
|
||||
+13
-4
@@ -1,6 +1,6 @@
|
||||
import { CAC, Command } from 'cac';
|
||||
import consola, { LogLevels } from 'consola';
|
||||
import { exec } from '~/core/utils/exec';
|
||||
import { filterTruthy, toArray } from '~/core/utils/arrays';
|
||||
import { printHeader } from '~/core/utils/log';
|
||||
import { formatDuration } from '~/core/utils/time';
|
||||
import { ValidationError } from '~/core/utils/validation';
|
||||
@@ -57,16 +57,24 @@ export function getArrayFromFlags<T>(
|
||||
flags: any,
|
||||
name: string,
|
||||
): T[] | undefined {
|
||||
const array = [flags[name]].flat() as Array<T | undefined>;
|
||||
const result = array.filter((item) => item != null) as T[];
|
||||
const array = toArray<T | undefined>(flags[name]);
|
||||
const result = filterTruthy(array);
|
||||
return result.length ? result : undefined;
|
||||
}
|
||||
|
||||
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
|
||||
@@ -79,7 +87,8 @@ export function createAliasedCommand(
|
||||
const args = process.argv.slice(
|
||||
process.argv.indexOf(aliasedCommand.name) + 1,
|
||||
);
|
||||
await exec(alias, args, {
|
||||
const { execa } = await import('execa');
|
||||
await execa(bin, args, {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
} catch {
|
||||
|
||||
+17
-1
@@ -16,6 +16,7 @@ cli
|
||||
.option('-c, --config <file>', 'use specified config file')
|
||||
.option('-m, --mode <mode>', 'set env mode')
|
||||
.option('-b, --browser <browser>', 'specify a browser')
|
||||
.option('-p, --port <port>', 'specify a port for the dev server')
|
||||
.option(
|
||||
'-e, --filter-entrypoint <entrypoint>',
|
||||
'only build specific entrypoints',
|
||||
@@ -35,6 +36,14 @@ cli
|
||||
configFile: flags.config,
|
||||
debug: flags.debug,
|
||||
filterEntrypoints: getArrayFromFlags(flags, 'filterEntrypoint'),
|
||||
dev:
|
||||
flags.port == null
|
||||
? undefined
|
||||
: {
|
||||
server: {
|
||||
port: parseInt(flags.port),
|
||||
},
|
||||
},
|
||||
});
|
||||
await server.start();
|
||||
return { isOngoing: true };
|
||||
@@ -57,6 +66,7 @@ cli
|
||||
.option('--mv3', 'target manifest v3')
|
||||
.option('--mv2', 'target manifest v2')
|
||||
.option('--analyze', 'visualize extension bundle')
|
||||
.option('--analyze-open', 'automatically open stats.html in browser')
|
||||
.action(
|
||||
wrapAction(async (root, flags) => {
|
||||
await build({
|
||||
@@ -66,7 +76,12 @@ cli
|
||||
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
|
||||
configFile: flags.config,
|
||||
debug: flags.debug,
|
||||
analysis: flags.analyze ? { enabled: true } : undefined,
|
||||
analysis: flags.analyze
|
||||
? {
|
||||
enabled: true,
|
||||
open: flags.analyzeOpen,
|
||||
}
|
||||
: undefined,
|
||||
filterEntrypoints: getArrayFromFlags(flags, 'filterEntrypoint'),
|
||||
});
|
||||
}),
|
||||
@@ -140,6 +155,7 @@ createAliasedCommand(
|
||||
cli,
|
||||
'submit',
|
||||
'publish-extension',
|
||||
'wxt-publish-extension',
|
||||
'https://www.npmjs.com/publish-browser-extension',
|
||||
);
|
||||
|
||||
|
||||
@@ -10,6 +10,29 @@ import { createLocationWatcher } from './location-watcher';
|
||||
*
|
||||
* It also provides several utilities like `ctx.setTimeout` and `ctx.setInterval` that should be used in
|
||||
* content scripts instead of `window.setTimeout` or `window.setInterval`.
|
||||
*
|
||||
* To create context for testing, you can use the class's constructor:
|
||||
*
|
||||
* ```ts
|
||||
* import { ContentScriptContext } from 'wxt/client';
|
||||
*
|
||||
* test("storage listener should be removed when context is invalidated", () => {
|
||||
* const ctx = new ContentScriptContext('test');
|
||||
* const item = storage.defineItem("local:count", { defaultValue: 0 });
|
||||
* const watcher = vi.fn();
|
||||
*
|
||||
* const unwatch = item.watch(watcher);
|
||||
* ctx.onInvalidated(unwatch); // Listen for invalidate here
|
||||
*
|
||||
* await item.setValue(1);
|
||||
* expect(watcher).toBeCalledTimes(1);
|
||||
* expect(watcher).toBeCalledWith(1, 0);
|
||||
*
|
||||
* ctx.notifyInvalidated(); // Use this function to invalidate the context
|
||||
* await item.setValue(2);
|
||||
* expect(watcher).toBeCalledTimes(1);
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export class ContentScriptContext implements AbortController {
|
||||
private static SCRIPT_STARTED_MESSAGE_TYPE = 'wxt:content-script-started';
|
||||
|
||||
@@ -404,4 +404,62 @@ describe('Content Script UIs', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mounted value', () => {
|
||||
describe('integrated', () => {
|
||||
it('should set the mounted value based on the onMounted return value', () => {
|
||||
const expected = Symbol();
|
||||
|
||||
const ui = createIntegratedUi(new ContentScriptContext('test'), {
|
||||
position: 'inline',
|
||||
onMount: () => expected,
|
||||
});
|
||||
expect(ui.mounted).toBeUndefined();
|
||||
|
||||
ui.mount();
|
||||
expect(ui.mounted).toBe(expected);
|
||||
|
||||
ui.remove();
|
||||
expect(ui.mounted).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('iframe', () => {
|
||||
it('should set the mounted value based on the onMounted return value', async () => {
|
||||
const expected = Symbol();
|
||||
|
||||
const ui = createIframeUi(new ContentScriptContext('test'), {
|
||||
page: '',
|
||||
position: 'inline',
|
||||
onMount: () => expected,
|
||||
});
|
||||
expect(ui.mounted).toBeUndefined();
|
||||
|
||||
ui.mount();
|
||||
expect(ui.mounted).toBe(expected);
|
||||
|
||||
ui.remove();
|
||||
expect(ui.mounted).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('shadow-root', () => {
|
||||
it('should set the mounted value based on the onMounted return value', async () => {
|
||||
const expected = Symbol();
|
||||
|
||||
const ui = await createShadowRootUi(new ContentScriptContext('test'), {
|
||||
name: 'test',
|
||||
position: 'inline',
|
||||
onMount: () => expected,
|
||||
});
|
||||
expect(ui.mounted).toBeUndefined();
|
||||
|
||||
ui.mount();
|
||||
expect(ui.mounted).toBe(expected);
|
||||
|
||||
ui.remove();
|
||||
expect(ui.mounted).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,12 +35,15 @@ export function createIntegratedUi<TMounted>(
|
||||
const remove = () => {
|
||||
options.onRemove?.(mounted);
|
||||
wrapper.remove();
|
||||
mounted = undefined;
|
||||
};
|
||||
|
||||
ctx.onInvalidated(remove);
|
||||
|
||||
return {
|
||||
mounted,
|
||||
get mounted() {
|
||||
return mounted;
|
||||
},
|
||||
wrapper,
|
||||
mount,
|
||||
remove,
|
||||
@@ -71,12 +74,15 @@ export function createIframeUi<TMounted>(
|
||||
const remove = () => {
|
||||
options.onRemove?.(mounted);
|
||||
wrapper.remove();
|
||||
mounted = undefined;
|
||||
};
|
||||
|
||||
ctx.onInvalidated(remove);
|
||||
|
||||
return {
|
||||
mounted,
|
||||
get mounted() {
|
||||
return mounted;
|
||||
},
|
||||
iframe,
|
||||
wrapper,
|
||||
mount,
|
||||
@@ -116,25 +122,26 @@ export async function createShadowRootUi<TMounted>(
|
||||
});
|
||||
shadowHost.setAttribute('data-wxt-shadow-root', '');
|
||||
|
||||
let mounted: TMounted;
|
||||
let mounted: TMounted | undefined;
|
||||
|
||||
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);
|
||||
// Clear mounted value
|
||||
mounted = undefined;
|
||||
};
|
||||
|
||||
ctx.onInvalidated(remove);
|
||||
@@ -145,7 +152,9 @@ export async function createShadowRootUi<TMounted>(
|
||||
uiContainer,
|
||||
mount,
|
||||
remove,
|
||||
mounted: mounted!,
|
||||
get mounted() {
|
||||
return mounted;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -207,7 +207,7 @@ export interface ContentScriptAnchoredOptions {
|
||||
* In combination with `anchor`, decide how to add the UI to the DOM.
|
||||
*
|
||||
* - `"last"` (default) - Add the UI as the last child of the `anchor` element
|
||||
* - `"first"` - Add the UI as the last child of the `anchor` element
|
||||
* - `"first"` - Add the UI as the first child of the `anchor` element
|
||||
* - `"replace"` - Replace the `anchor` element with the UI.
|
||||
* - `"before"` - Add the UI as the sibling before the `anchor` element
|
||||
* - `"after"` - Add the UI as the sibling after the `anchor` element
|
||||
|
||||
@@ -2,20 +2,29 @@ import type * as vite from 'vite';
|
||||
import {
|
||||
BuildStepOutput,
|
||||
Entrypoint,
|
||||
InlineConfig,
|
||||
ResolvedConfig,
|
||||
UserConfig,
|
||||
VirtualEntrypointType,
|
||||
WxtBuilder,
|
||||
WxtBuilderServer,
|
||||
WxtDevServer,
|
||||
WxtHooks,
|
||||
} from '~/types';
|
||||
import * as wxtPlugins from './plugins';
|
||||
import { getEntrypointBundlePath } from '~/core/utils/entrypoints';
|
||||
import {
|
||||
getEntrypointBundlePath,
|
||||
isHtmlEntrypoint,
|
||||
} from '~/core/utils/entrypoints';
|
||||
import {
|
||||
VirtualEntrypointType,
|
||||
VirtualModuleId,
|
||||
} from '~/core/utils/virtual-modules';
|
||||
import { Hookable } from 'hookable';
|
||||
import { toArray } from '~/core/utils/arrays';
|
||||
import { safeVarName } from '~/core/utils/strings';
|
||||
|
||||
export async function createViteBuilder(
|
||||
inlineConfig: InlineConfig,
|
||||
userConfig: UserConfig,
|
||||
wxtConfig: Omit<ResolvedConfig, 'builder'>,
|
||||
wxtConfig: ResolvedConfig,
|
||||
hooks: Hookable<WxtHooks>,
|
||||
server?: WxtDevServer,
|
||||
): Promise<WxtBuilder> {
|
||||
const vite = await import('vite');
|
||||
|
||||
@@ -23,14 +32,7 @@ export async function createViteBuilder(
|
||||
* Returns the base vite config shared by all builds based on the inline and user config.
|
||||
*/
|
||||
const getBaseConfig = async () => {
|
||||
const resolvedInlineConfig =
|
||||
(await inlineConfig.vite?.(wxtConfig.env)) ?? {};
|
||||
const resolvedUserConfig = (await userConfig.vite?.(wxtConfig.env)) ?? {};
|
||||
|
||||
const config: vite.InlineConfig = vite.mergeConfig(
|
||||
resolvedUserConfig,
|
||||
resolvedInlineConfig,
|
||||
);
|
||||
const config: vite.InlineConfig = await wxtConfig.vite(wxtConfig.env);
|
||||
|
||||
config.root = wxtConfig.root;
|
||||
config.configFile = false;
|
||||
@@ -38,6 +40,8 @@ export async function createViteBuilder(
|
||||
config.mode = wxtConfig.mode;
|
||||
|
||||
config.build ??= {};
|
||||
config.publicDir = wxtConfig.publicDir;
|
||||
config.build.copyPublicDir = false;
|
||||
config.build.outDir = wxtConfig.outDir;
|
||||
config.build.emptyOutDir = false;
|
||||
// Disable minification for the dev command
|
||||
@@ -52,13 +56,10 @@ export async function createViteBuilder(
|
||||
config.plugins ??= [];
|
||||
config.plugins.push(
|
||||
wxtPlugins.download(wxtConfig),
|
||||
wxtPlugins.devHtmlPrerender(wxtConfig),
|
||||
wxtPlugins.devHtmlPrerender(wxtConfig, server),
|
||||
wxtPlugins.unimport(wxtConfig),
|
||||
wxtPlugins.virtualEntrypoint('background', wxtConfig),
|
||||
wxtPlugins.virtualEntrypoint('content-script-isolated-world', wxtConfig),
|
||||
wxtPlugins.virtualEntrypoint('content-script-main-world', wxtConfig),
|
||||
wxtPlugins.virtualEntrypoint('unlisted-script', wxtConfig),
|
||||
wxtPlugins.devServerGlobals(wxtConfig),
|
||||
wxtPlugins.resolveVirtualModules(wxtConfig),
|
||||
wxtPlugins.devServerGlobals(wxtConfig, server),
|
||||
wxtPlugins.tsconfigPaths(wxtConfig),
|
||||
wxtPlugins.noopBackground(),
|
||||
wxtPlugins.globals(wxtConfig),
|
||||
@@ -87,14 +88,22 @@ export async function createViteBuilder(
|
||||
plugins.push(wxtPlugins.cssEntrypoints(entrypoint, wxtConfig));
|
||||
}
|
||||
|
||||
const iifeReturnValueName = safeVarName(entrypoint.name);
|
||||
const libMode: vite.UserConfig = {
|
||||
mode: wxtConfig.mode,
|
||||
plugins,
|
||||
esbuild: {
|
||||
// Add a footer with the returned value so it can return values to `scripting.executeScript`
|
||||
// Footer is added apart of esbuild to make sure it's not minified. It
|
||||
// get's removed if added to `build.rollupOptions.output.footer`
|
||||
// See https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/executeScript#return_value
|
||||
footer: iifeReturnValueName + ';',
|
||||
},
|
||||
build: {
|
||||
lib: {
|
||||
entry,
|
||||
formats: ['iife'],
|
||||
name: '_',
|
||||
name: iifeReturnValueName,
|
||||
fileName: entrypoint.name,
|
||||
},
|
||||
rollupOptions: {
|
||||
@@ -133,6 +142,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: [
|
||||
@@ -148,10 +160,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]',
|
||||
},
|
||||
@@ -197,6 +211,11 @@ export async function createViteBuilder(
|
||||
else entryConfig = getLibModeConfig(group);
|
||||
|
||||
const buildConfig = vite.mergeConfig(await getBaseConfig(), entryConfig);
|
||||
await hooks.callHook(
|
||||
'vite:build:extendConfig',
|
||||
toArray(group),
|
||||
buildConfig,
|
||||
);
|
||||
const result = await vite.build(buildConfig);
|
||||
return {
|
||||
entrypoints: group,
|
||||
@@ -213,9 +232,9 @@ export async function createViteBuilder(
|
||||
},
|
||||
};
|
||||
const baseConfig = await getBaseConfig();
|
||||
const viteServer = await vite.createServer(
|
||||
vite.mergeConfig(baseConfig, serverConfig),
|
||||
);
|
||||
const finalConfig = vite.mergeConfig(baseConfig, serverConfig);
|
||||
await hooks.callHook('vite:devServer:extendConfig', finalConfig);
|
||||
const viteServer = await vite.createServer(finalConfig);
|
||||
|
||||
const server: WxtBuilderServer = {
|
||||
async listen() {
|
||||
@@ -269,7 +288,10 @@ function getRollupEntry(entrypoint: Entrypoint): string {
|
||||
: 'content-script-isolated-world';
|
||||
break;
|
||||
}
|
||||
return virtualEntrypointType
|
||||
? `virtual:wxt-${virtualEntrypointType}?${entrypoint.inputPath}`
|
||||
: entrypoint.inputPath;
|
||||
|
||||
if (virtualEntrypointType) {
|
||||
const moduleId: VirtualModuleId = `virtual:wxt-${virtualEntrypointType}-entrypoint`;
|
||||
return `${moduleId}?${entrypoint.inputPath}`;
|
||||
}
|
||||
return entrypoint.inputPath;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
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(
|
||||
config: Omit<ResolvedConfig, 'builder'>,
|
||||
): vite.Plugin {
|
||||
export function bundleAnalysis(config: ResolvedConfig): vite.Plugin {
|
||||
// @ts-expect-error: Vite/Rollup type mismatch, but it's fine.
|
||||
return visualizer({
|
||||
template: 'raw-data',
|
||||
filename: path.resolve(
|
||||
|
||||
@@ -14,7 +14,7 @@ import { getEntrypointBundlePath } from '~/core/utils/entrypoints';
|
||||
*/
|
||||
export function cssEntrypoints(
|
||||
entrypoint: Entrypoint,
|
||||
config: Omit<ResolvedConfig, 'builder'>,
|
||||
config: ResolvedConfig,
|
||||
): vite.Plugin {
|
||||
return {
|
||||
name: 'wxt:css-entrypoint',
|
||||
|
||||
@@ -12,12 +12,13 @@ let reactRefreshPreamble = '';
|
||||
* Pre-renders the HTML entrypoints when building the extension to connect to the dev server.
|
||||
*/
|
||||
export function devHtmlPrerender(
|
||||
config: Omit<ResolvedConfig, 'builder'>,
|
||||
config: ResolvedConfig,
|
||||
server: WxtDevServer | undefined,
|
||||
): vite.PluginOption {
|
||||
const htmlReloadId = '@wxt/reload-html';
|
||||
const resolvedHtmlReloadId = resolve(
|
||||
config.root,
|
||||
'node_modules/wxt/dist/virtual/reload-html.js',
|
||||
config.wxtModuleDir,
|
||||
'dist/virtual/reload-html.js',
|
||||
);
|
||||
const virtualReactRefreshId = '@wxt/virtual-react-refresh';
|
||||
const resolvedVirtualReactRefreshId = '\0' + virtualReactRefreshId;
|
||||
@@ -38,7 +39,6 @@ export function devHtmlPrerender(
|
||||
// Convert scripts like src="./main.tsx" -> src="http://localhost:3000/entrypoints/popup/main.tsx"
|
||||
// before the paths are replaced with their bundled path
|
||||
transform(code, id) {
|
||||
const server = config.server;
|
||||
if (
|
||||
config.command !== 'serve' ||
|
||||
server == null ||
|
||||
@@ -68,7 +68,6 @@ export function devHtmlPrerender(
|
||||
|
||||
// Pass the HTML through the dev server to add dev-mode specific code
|
||||
async transformIndexHtml(html, ctx) {
|
||||
const server = config.server;
|
||||
if (config.command !== 'serve' || server == null) return;
|
||||
|
||||
const originalUrl = `${server.origin}${ctx.path}`;
|
||||
@@ -134,7 +133,7 @@ export function devHtmlPrerender(
|
||||
}
|
||||
|
||||
export function pointToDevServer(
|
||||
config: Omit<ResolvedConfig, 'builder'>,
|
||||
config: ResolvedConfig,
|
||||
server: WxtDevServer,
|
||||
id: string,
|
||||
document: Document,
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import { Plugin } from 'vite';
|
||||
import { ResolvedConfig } from '~/types';
|
||||
import { ResolvedConfig, WxtDevServer } from '~/types';
|
||||
|
||||
/**
|
||||
* Defines global constants about the dev server. Helps scripts connect to the server's web socket.
|
||||
*/
|
||||
export function devServerGlobals(
|
||||
config: Omit<ResolvedConfig, 'builder'>,
|
||||
config: ResolvedConfig,
|
||||
server: WxtDevServer | undefined,
|
||||
): Plugin {
|
||||
return {
|
||||
name: 'wxt:dev-server-globals',
|
||||
config() {
|
||||
if (config.server == null || config.command == 'build') return;
|
||||
if (server == null || config.command == 'build') return;
|
||||
|
||||
return {
|
||||
define: {
|
||||
__DEV_SERVER_PROTOCOL__: JSON.stringify('ws:'),
|
||||
__DEV_SERVER_HOSTNAME__: JSON.stringify(config.server.hostname),
|
||||
__DEV_SERVER_PORT__: JSON.stringify(config.server.port),
|
||||
__DEV_SERVER_HOSTNAME__: JSON.stringify(server.hostname),
|
||||
__DEV_SERVER_PORT__: JSON.stringify(server.port),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -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<ResolvedConfig, 'builder'>): Plugin {
|
||||
export function download(config: ResolvedConfig): Plugin {
|
||||
return {
|
||||
name: 'wxt:download',
|
||||
resolveId(id) {
|
||||
|
||||
@@ -6,9 +6,7 @@ import type * as vite from 'vite';
|
||||
* `webextension-polyfill` module to a virtual module and exporting the `chrome` global from the
|
||||
* virtual module.
|
||||
*/
|
||||
export function excludeBrowserPolyfill(
|
||||
config: Omit<ResolvedConfig, 'builder'>,
|
||||
): vite.Plugin {
|
||||
export function excludeBrowserPolyfill(config: ResolvedConfig): vite.Plugin {
|
||||
const virtualId = 'virtual:wxt-webextension-polyfill-disabled';
|
||||
|
||||
return {
|
||||
|
||||
@@ -2,9 +2,7 @@ import type * as vite from 'vite';
|
||||
import { ResolvedConfig } from '~/types';
|
||||
import { getGlobals } from '~/core/utils/globals';
|
||||
|
||||
export function globals(
|
||||
config: Omit<ResolvedConfig, 'builder'>,
|
||||
): vite.PluginOption {
|
||||
export function globals(config: ResolvedConfig): vite.PluginOption {
|
||||
return {
|
||||
name: 'wxt:globals',
|
||||
config() {
|
||||
|
||||
@@ -3,7 +3,7 @@ export * from './devServerGlobals';
|
||||
export * from './download';
|
||||
export * from './multipageMove';
|
||||
export * from './unimport';
|
||||
export * from './virtualEntrypoint';
|
||||
export * from './resolveVirtualModules';
|
||||
export * from './tsconfigPaths';
|
||||
export * from './noopBackground';
|
||||
export * from './cssEntrypoints';
|
||||
|
||||
@@ -20,7 +20,7 @@ import { normalizePath } from '~/core/utils/paths';
|
||||
*/
|
||||
export function multipageMove(
|
||||
entrypoints: Entrypoint[],
|
||||
config: Omit<ResolvedConfig, 'builder'>,
|
||||
config: ResolvedConfig,
|
||||
): vite.Plugin {
|
||||
return {
|
||||
name: 'wxt:multipage-move',
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Plugin } from 'vite';
|
||||
import { ResolvedConfig } from '~/types';
|
||||
import { normalizePath } from '~/core/utils/paths';
|
||||
import {
|
||||
VirtualModuleId,
|
||||
virtualModuleNames,
|
||||
} from '~/core/utils/virtual-modules';
|
||||
import fs from 'fs-extra';
|
||||
import { resolve } from 'path';
|
||||
|
||||
/**
|
||||
* Resolve all the virtual modules to the `node_modules/wxt/dist/virtual` directory.
|
||||
*/
|
||||
export function resolveVirtualModules(config: ResolvedConfig): Plugin[] {
|
||||
return virtualModuleNames.map((name) => {
|
||||
const virtualId: `${VirtualModuleId}?` = `virtual:wxt-${name}?`;
|
||||
const resolvedVirtualId = '\0' + virtualId;
|
||||
return {
|
||||
name: `wxt:resolve-virtual-${name}`,
|
||||
resolveId(id) {
|
||||
// Id doesn't start with prefix, it looks like this:
|
||||
// /path/to/project/virtual:wxt-background?/path/to/project/entrypoints/background.ts
|
||||
const index = id.indexOf(virtualId);
|
||||
if (index === -1) return;
|
||||
|
||||
const inputPath = normalizePath(id.substring(index + virtualId.length));
|
||||
return resolvedVirtualId + inputPath;
|
||||
},
|
||||
async load(id) {
|
||||
if (!id.startsWith(resolvedVirtualId)) return;
|
||||
|
||||
const inputPath = id.replace(resolvedVirtualId, '');
|
||||
const template = await fs.readFile(
|
||||
resolve(config.wxtModuleDir, `dist/virtual/${name}.js`),
|
||||
'utf-8',
|
||||
);
|
||||
return template.replace(`virtual:user-${name}`, inputPath);
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
import { ResolvedConfig } from '~/types';
|
||||
import type * as vite from 'vite';
|
||||
|
||||
export function tsconfigPaths(
|
||||
config: Omit<ResolvedConfig, 'builder'>,
|
||||
): vite.Plugin {
|
||||
export function tsconfigPaths(config: ResolvedConfig): vite.Plugin {
|
||||
return {
|
||||
name: 'wxt:aliases',
|
||||
async config() {
|
||||
|
||||
@@ -15,9 +15,7 @@ const ENABLED_EXTENSIONS = new Set([
|
||||
/**
|
||||
* Inject any global imports defined by unimport
|
||||
*/
|
||||
export function unimport(
|
||||
config: Omit<ResolvedConfig, 'builder'>,
|
||||
): vite.PluginOption {
|
||||
export function unimport(config: ResolvedConfig): vite.PluginOption {
|
||||
const options = config.imports;
|
||||
if (options === false) return [];
|
||||
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { Plugin } from 'vite';
|
||||
import { ResolvedConfig, VirtualEntrypointType } from '~/types';
|
||||
import fs from 'fs-extra';
|
||||
import { resolve } from 'path';
|
||||
import { normalizePath } from '~/core/utils/paths';
|
||||
|
||||
/**
|
||||
* Wraps a user's entrypoint with a vitual version with additional logic.
|
||||
*/
|
||||
export function virtualEntrypoint(
|
||||
type: VirtualEntrypointType,
|
||||
config: Omit<ResolvedConfig, 'builder'>,
|
||||
): Plugin {
|
||||
const virtualId = `virtual:wxt-${type}?`;
|
||||
const resolvedVirtualId = `\0${virtualId}`;
|
||||
|
||||
return {
|
||||
name: `wxt:virtual-entrypoint`,
|
||||
resolveId(id) {
|
||||
// Id doesn't start with prefix, it looks like this:
|
||||
// /path/to/project/virtual:background?/path/to/project/entrypoints/background.ts
|
||||
const index = id.indexOf(virtualId);
|
||||
if (index === -1) return;
|
||||
|
||||
const inputPath = normalizePath(id.substring(index + virtualId.length));
|
||||
return resolvedVirtualId + inputPath;
|
||||
},
|
||||
async load(id) {
|
||||
if (!id.startsWith(resolvedVirtualId)) return;
|
||||
|
||||
const inputPath = id.replace(resolvedVirtualId, '');
|
||||
const template = await fs.readFile(
|
||||
resolve(
|
||||
config.root,
|
||||
`node_modules/wxt/dist/virtual/${type}-entrypoint.js`,
|
||||
),
|
||||
'utf-8',
|
||||
);
|
||||
return template.replace(`virtual:user-${type}`, inputPath);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import { ResolvedConfig } from '~/types';
|
||||
* `npm list` and inline them automatically.
|
||||
*/
|
||||
export function webextensionPolyfillMock(
|
||||
config: Omit<ResolvedConfig, 'builder'>,
|
||||
config: ResolvedConfig,
|
||||
): vite.PluginOption {
|
||||
return {
|
||||
name: 'wxt:testing-inline-deps',
|
||||
@@ -26,8 +26,8 @@ export function webextensionPolyfillMock(
|
||||
alias: {
|
||||
// Alias to use a mocked version of the polyfill
|
||||
'webextension-polyfill': path.resolve(
|
||||
config.root,
|
||||
'node_modules/wxt/dist/virtual/mock-browser',
|
||||
config.wxtModuleDir,
|
||||
'dist/virtual/mock-browser',
|
||||
),
|
||||
},
|
||||
},
|
||||
|
||||
+100
-110
@@ -7,7 +7,7 @@ import {
|
||||
} from '~/types';
|
||||
import {
|
||||
getEntrypointBundlePath,
|
||||
resolvePerBrowserOption,
|
||||
isHtmlEntrypoint,
|
||||
} from '~/core/utils/entrypoints';
|
||||
import {
|
||||
getContentScriptCssFiles,
|
||||
@@ -26,6 +26,10 @@ import pc from 'picocolors';
|
||||
import { relative } from 'node:path';
|
||||
import { registerWxt, wxt } from './wxt';
|
||||
import { unnormalizePath } from './utils/paths';
|
||||
import {
|
||||
getContentScriptJs,
|
||||
mapWxtOptionsToRegisteredContentScript,
|
||||
} from './utils/content-scripts';
|
||||
|
||||
/**
|
||||
* Creates a dev server and pre-builds all the files that need to exist before loading the extension.
|
||||
@@ -39,14 +43,63 @@ import { unnormalizePath } from './utils/paths';
|
||||
export async function createServer(
|
||||
inlineConfig?: InlineConfig,
|
||||
): Promise<WxtDevServer> {
|
||||
const port = await getPort();
|
||||
const hostname = 'localhost';
|
||||
const origin = `http://${hostname}:${port}`;
|
||||
const serverInfo: ServerInfo = {
|
||||
port,
|
||||
hostname,
|
||||
origin,
|
||||
};
|
||||
await registerWxt('serve', inlineConfig, async (config) => {
|
||||
const { port, hostname } = config.dev.server!;
|
||||
const serverInfo: ServerInfo = {
|
||||
port,
|
||||
hostname,
|
||||
origin: `http://${hostname}:${port}`,
|
||||
};
|
||||
|
||||
// Server instance must be created first so its reference can be added to the internal config used
|
||||
// to pre-render entrypoints
|
||||
const server: WxtDevServer = {
|
||||
...serverInfo,
|
||||
get watcher() {
|
||||
return builderServer.watcher;
|
||||
},
|
||||
get ws() {
|
||||
return builderServer.ws;
|
||||
},
|
||||
currentOutput: undefined,
|
||||
async start() {
|
||||
await builderServer.listen();
|
||||
wxt.logger.success(`Started dev server @ ${serverInfo.origin}`);
|
||||
await buildAndOpenBrowser();
|
||||
},
|
||||
async stop() {
|
||||
await runner.closeBrowser();
|
||||
await builderServer.close();
|
||||
},
|
||||
async restart() {
|
||||
await closeAndRecreateRunner();
|
||||
await buildAndOpenBrowser();
|
||||
},
|
||||
transformHtml(url, html, originalUrl) {
|
||||
return builderServer.transformHtml(url, html, originalUrl);
|
||||
},
|
||||
reloadContentScript(payload) {
|
||||
server.ws.send('wxt:reload-content-script', payload);
|
||||
},
|
||||
reloadPage(path) {
|
||||
server.ws.send('wxt:reload-page', path);
|
||||
},
|
||||
reloadExtension() {
|
||||
server.ws.send('wxt:reload-extension');
|
||||
},
|
||||
async restartBrowser() {
|
||||
await closeAndRecreateRunner();
|
||||
await runner.openBrowser();
|
||||
},
|
||||
};
|
||||
return server;
|
||||
});
|
||||
|
||||
const server = wxt.server!;
|
||||
let [runner, builderServer] = await Promise.all([
|
||||
createExtensionRunner(),
|
||||
wxt.builder.createServer(server),
|
||||
]);
|
||||
|
||||
const buildAndOpenBrowser = async () => {
|
||||
// Build after starting the dev server so it can be used to transform HTML files
|
||||
@@ -73,55 +126,6 @@ export async function createServer(
|
||||
runner = await createExtensionRunner();
|
||||
};
|
||||
|
||||
// Server instance must be created first so its reference can be added to the internal config used
|
||||
// to pre-render entrypoints
|
||||
const server: WxtDevServer = {
|
||||
...serverInfo,
|
||||
get watcher() {
|
||||
return builderServer.watcher;
|
||||
},
|
||||
get ws() {
|
||||
return builderServer.ws;
|
||||
},
|
||||
currentOutput: undefined,
|
||||
async start() {
|
||||
await builderServer.listen();
|
||||
wxt.logger.success(`Started dev server @ ${serverInfo.origin}`);
|
||||
await buildAndOpenBrowser();
|
||||
},
|
||||
async stop() {
|
||||
await runner.closeBrowser();
|
||||
await builderServer.close();
|
||||
},
|
||||
async restart() {
|
||||
await closeAndRecreateRunner();
|
||||
await buildAndOpenBrowser();
|
||||
},
|
||||
transformHtml(url, html, originalUrl) {
|
||||
return builderServer.transformHtml(url, html, originalUrl);
|
||||
},
|
||||
reloadContentScript(contentScript) {
|
||||
server.ws.send('wxt:reload-content-script', contentScript);
|
||||
},
|
||||
reloadPage(path) {
|
||||
server.ws.send('wxt:reload-page', path);
|
||||
},
|
||||
reloadExtension() {
|
||||
server.ws.send('wxt:reload-extension');
|
||||
},
|
||||
async restartBrowser() {
|
||||
await closeAndRecreateRunner();
|
||||
await runner.openBrowser();
|
||||
},
|
||||
};
|
||||
|
||||
await registerWxt('serve', inlineConfig, server);
|
||||
|
||||
let [runner, builderServer] = await Promise.all([
|
||||
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', () => {
|
||||
@@ -136,11 +140,6 @@ export async function createServer(
|
||||
return server;
|
||||
}
|
||||
|
||||
async function getPort(): Promise<number> {
|
||||
const { default: getPort, portNumbers } = await import('get-port');
|
||||
return await getPort({ port: portNumbers(3000, 3010) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a function responsible for reloading different parts of the extension when a file
|
||||
* changes.
|
||||
@@ -188,34 +187,38 @@ function createFileReloader(server: WxtDevServer) {
|
||||
|
||||
// Rebuild entrypoints on change
|
||||
const allEntrypoints = await findEntrypoints();
|
||||
const { output: newOutput } = await rebuild(
|
||||
allEntrypoints,
|
||||
// TODO: this excludes new entrypoints, so they're not built until the dev command is restarted
|
||||
changes.rebuildGroups,
|
||||
changes.cachedOutput,
|
||||
);
|
||||
server.currentOutput = newOutput;
|
||||
try {
|
||||
const { output: newOutput } = await rebuild(
|
||||
allEntrypoints,
|
||||
// TODO: this excludes new entrypoints, so they're not built until the dev command is restarted
|
||||
changes.rebuildGroups,
|
||||
changes.cachedOutput,
|
||||
);
|
||||
server.currentOutput = newOutput;
|
||||
|
||||
// Perform reloads
|
||||
switch (changes.type) {
|
||||
case 'extension-reload':
|
||||
server.reloadExtension();
|
||||
consola.success(`Reloaded extension`);
|
||||
break;
|
||||
case 'html-reload':
|
||||
const { reloadedNames } = reloadHtmlPages(
|
||||
changes.rebuildGroups,
|
||||
server,
|
||||
);
|
||||
consola.success(`Reloaded: ${getFilenameList(reloadedNames)}`);
|
||||
break;
|
||||
case 'content-script-reload':
|
||||
reloadContentScripts(changes.changedSteps, server);
|
||||
const rebuiltNames = changes.rebuildGroups
|
||||
.flat()
|
||||
.map((entry) => entry.name);
|
||||
consola.success(`Reloaded: ${getFilenameList(rebuiltNames)}`);
|
||||
break;
|
||||
// Perform reloads
|
||||
switch (changes.type) {
|
||||
case 'extension-reload':
|
||||
server.reloadExtension();
|
||||
consola.success(`Reloaded extension`);
|
||||
break;
|
||||
case 'html-reload':
|
||||
const { reloadedNames } = reloadHtmlPages(
|
||||
changes.rebuildGroups,
|
||||
server,
|
||||
);
|
||||
consola.success(`Reloaded: ${getFilenameList(reloadedNames)}`);
|
||||
break;
|
||||
case 'content-script-reload':
|
||||
reloadContentScripts(changes.changedSteps, server);
|
||||
const rebuiltNames = changes.rebuildGroups
|
||||
.flat()
|
||||
.map((entry) => entry.name);
|
||||
consola.success(`Reloaded: ${getFilenameList(rebuiltNames)}`);
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
// Catch build errors instead of crashing. Don't log error either, builder should have already logged it
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -232,28 +235,17 @@ function reloadContentScripts(steps: BuildStepOutput[], server: WxtDevServer) {
|
||||
const entry = step.entrypoints;
|
||||
if (Array.isArray(entry) || entry.type !== 'content-script') return;
|
||||
|
||||
const js = [getEntrypointBundlePath(entry, wxt.config.outDir, '.js')];
|
||||
const js = getContentScriptJs(wxt.config, entry);
|
||||
const cssMap = getContentScriptsCssMap(server.currentOutput, [entry]);
|
||||
const css = getContentScriptCssFiles([entry], cssMap);
|
||||
|
||||
server.reloadContentScript({
|
||||
allFrames: resolvePerBrowserOption(
|
||||
entry.options.allFrames,
|
||||
wxt.config.browser,
|
||||
registration: entry.options.registration,
|
||||
contentScript: mapWxtOptionsToRegisteredContentScript(
|
||||
entry.options,
|
||||
js,
|
||||
css,
|
||||
),
|
||||
excludeMatches: resolvePerBrowserOption(
|
||||
entry.options.excludeMatches,
|
||||
wxt.config.browser,
|
||||
),
|
||||
matches: resolvePerBrowserOption(
|
||||
entry.options.matches,
|
||||
wxt.config.browser,
|
||||
),
|
||||
runAt: resolvePerBrowserOption(entry.options.runAt, wxt.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, wxt.config.browser),
|
||||
js,
|
||||
css,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
@@ -266,9 +258,7 @@ function reloadHtmlPages(
|
||||
server: WxtDevServer,
|
||||
): { 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, wxt.config.outDir, '.html');
|
||||
|
||||
+12
-1
@@ -59,6 +59,16 @@ export async function initialize(options: {
|
||||
input.template ??= defaultTemplate;
|
||||
input.packageManager ??= options.packageManager;
|
||||
|
||||
const isExists = await fs.pathExists(input.directory);
|
||||
if (isExists) {
|
||||
const isEmpty = (await fs.readdir(input.directory)).length === 0;
|
||||
if (!isEmpty) {
|
||||
consola.error(
|
||||
`The directory ${path.resolve(input.directory)} is not empty. Aborted.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
await cloneProject(input);
|
||||
|
||||
const cdPath = path.relative(process.cwd(), path.resolve(input.directory));
|
||||
@@ -117,7 +127,8 @@ async function listTemplates(): Promise<Template[]> {
|
||||
return l.name.localeCompare(r.name);
|
||||
});
|
||||
} catch (err) {
|
||||
throw Error(`Cannot load templates: ${JSON.stringify(err, null, 2)}`);
|
||||
consola.error(err);
|
||||
throw Error(`Failed to load templates`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import path from 'node:path';
|
||||
import { bun } from '../bun';
|
||||
|
||||
const cwd = path.resolve(__dirname, 'fixtures/bun-project');
|
||||
|
||||
describe.skipIf(() => process.platform === 'win32')(
|
||||
'Bun Package Management Utils',
|
||||
() => {
|
||||
describe('listDependencies', () => {
|
||||
it('should list direct dependencies', async () => {
|
||||
const actual = await bun.listDependencies({ cwd });
|
||||
expect(actual).toEqual([
|
||||
{ name: 'flatten', version: '1.0.3' },
|
||||
{ name: 'mime-types', version: '2.1.35' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should list all dependencies', async () => {
|
||||
const actual = await bun.listDependencies({ cwd, all: true });
|
||||
expect(actual).toEqual([
|
||||
{ name: 'flatten', version: '1.0.3' },
|
||||
{ name: 'mime-db', version: '1.52.0' },
|
||||
{ name: 'mime-types', version: '2.1.35' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "bun-ls",
|
||||
"dependencies": {
|
||||
"mime-types": "2.1.35"
|
||||
},
|
||||
"devDependencies": {
|
||||
"flatten": "1.0.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "npm-ls",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "npm-ls",
|
||||
"dependencies": {
|
||||
"mime-types": "2.1.35"
|
||||
},
|
||||
"devDependencies": {
|
||||
"flatten": "1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/flatten": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.3.tgz",
|
||||
"integrity": "sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==",
|
||||
"deprecated": "flatten is deprecated in favor of utility frameworks such as lodash.",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "npm-ls",
|
||||
"dependencies": {
|
||||
"mime-types": "2.1.35"
|
||||
},
|
||||
"devDependencies": {
|
||||
"flatten": "1.0.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "pnpm-ls",
|
||||
"dependencies": {
|
||||
"mime-types": "2.1.35"
|
||||
},
|
||||
"devDependencies": {
|
||||
"flatten": "1.0.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
lockfileVersion: '6.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
dependencies:
|
||||
mime-types:
|
||||
specifier: 2.1.35
|
||||
version: 2.1.35
|
||||
|
||||
devDependencies:
|
||||
flatten:
|
||||
specifier: 1.0.3
|
||||
version: 1.0.3
|
||||
|
||||
packages:
|
||||
|
||||
/flatten@1.0.3:
|
||||
resolution: {integrity: sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==}
|
||||
deprecated: flatten is deprecated in favor of utility frameworks such as lodash.
|
||||
dev: true
|
||||
|
||||
/mime-db@1.52.0:
|
||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/mime-types@2.1.35:
|
||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dependencies:
|
||||
mime-db: 1.52.0
|
||||
dev: false
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "yarn-ls",
|
||||
"packageManager": "yarn@1.22.19",
|
||||
"dependencies": {
|
||||
"mime-types": "2.1.35"
|
||||
},
|
||||
"devDependencies": {
|
||||
"flatten": "1.0.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
flatten@1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.3.tgz#c1283ac9f27b368abc1e36d1ff7b04501a30356b"
|
||||
integrity sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==
|
||||
|
||||
mime-db@1.52.0:
|
||||
version "1.52.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
||||
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
||||
|
||||
mime-types@2.1.35:
|
||||
version "2.1.35"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
|
||||
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||
dependencies:
|
||||
mime-db "1.52.0"
|
||||
@@ -0,0 +1,46 @@
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
import path from 'node:path';
|
||||
import { npm } from '../npm';
|
||||
import { execaCommand } from 'execa';
|
||||
import { exists } from 'fs-extra';
|
||||
|
||||
const cwd = path.resolve(__dirname, 'fixtures/npm-project');
|
||||
|
||||
describe('NPM Package Management Utils', () => {
|
||||
beforeAll(async () => {
|
||||
// NPM needs the modules installed for 'npm ls' to work
|
||||
await execaCommand('npm i', { cwd });
|
||||
}, 60e3);
|
||||
|
||||
describe('listDependencies', () => {
|
||||
it('should list direct dependencies', async () => {
|
||||
const actual = await npm.listDependencies({ cwd });
|
||||
expect(actual).toEqual([
|
||||
{ name: 'flatten', version: '1.0.3' },
|
||||
{ name: 'mime-types', version: '2.1.35' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should list all dependencies', async () => {
|
||||
const actual = await npm.listDependencies({ cwd, all: true });
|
||||
expect(actual).toEqual([
|
||||
{ name: 'flatten', version: '1.0.3' },
|
||||
{ name: 'mime-types', version: '2.1.35' },
|
||||
{ name: 'mime-db', version: '1.52.0' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('downloadDependency', () => {
|
||||
it('should download the dependency as a tarball', async () => {
|
||||
const downloadDir = path.resolve(cwd, 'dist');
|
||||
const id = 'mime-db@1.52.0';
|
||||
const expected = path.resolve(downloadDir, 'mime-db-1.52.0.tgz');
|
||||
|
||||
const actual = await npm.downloadDependency(id, downloadDir);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
expect(await exists(actual)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
import path from 'node:path';
|
||||
import { pnpm } from '../pnpm';
|
||||
import { execaCommand } from 'execa';
|
||||
|
||||
const cwd = path.resolve(__dirname, 'fixtures/pnpm-project');
|
||||
|
||||
process.env.WXT_PNPM_IGNORE_WORKSPACE = 'true';
|
||||
|
||||
describe('PNPM Package Management Utils', () => {
|
||||
beforeAll(async () => {
|
||||
// PNPM needs the modules installed, or 'pnpm ls' will return a blank list.
|
||||
await execaCommand('pnpm i --ignore-workspace', { cwd });
|
||||
});
|
||||
|
||||
describe('listDependencies', () => {
|
||||
it('should list direct dependencies', async () => {
|
||||
const actual = await pnpm.listDependencies({ cwd });
|
||||
expect(actual).toEqual([
|
||||
{ name: 'flatten', version: '1.0.3' },
|
||||
{ name: 'mime-types', version: '2.1.35' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should list all dependencies', async () => {
|
||||
const actual = await pnpm.listDependencies({ cwd, all: true });
|
||||
expect(actual).toEqual([
|
||||
{ name: 'flatten', version: '1.0.3' },
|
||||
{ name: 'mime-types', version: '2.1.35' },
|
||||
{ name: 'mime-db', version: '1.52.0' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import path from 'node:path';
|
||||
import { yarn } from '../yarn';
|
||||
|
||||
const cwd = path.resolve(__dirname, 'fixtures/yarn-project');
|
||||
|
||||
describe('Yarn Package Management Utils', () => {
|
||||
describe('listDependencies', () => {
|
||||
it('should list direct dependencies', async () => {
|
||||
const actual = await yarn.listDependencies({ cwd });
|
||||
expect(actual).toEqual([
|
||||
{ name: 'mime-db', version: '1.52.0' },
|
||||
{ name: 'flatten', version: '1.0.3' },
|
||||
{ name: 'mime-types', version: '2.1.35' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should list all dependencies', async () => {
|
||||
const actual = await yarn.listDependencies({ cwd, all: true });
|
||||
expect(actual).toEqual([
|
||||
{ name: 'mime-db', version: '1.52.0' },
|
||||
{ name: 'flatten', version: '1.0.3' },
|
||||
{ name: 'mime-types', version: '2.1.35' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import { dedupeDependencies, npm } from './npm';
|
||||
import { WxtPackageManagerImpl } from './types';
|
||||
|
||||
export const bun: WxtPackageManagerImpl = {
|
||||
overridesKey: 'overrides', // But also supports "resolutions"
|
||||
downloadDependency(...args) {
|
||||
return npm.downloadDependency(...args);
|
||||
},
|
||||
async listDependencies(options) {
|
||||
const args = ['pm', 'ls'];
|
||||
if (options?.all) {
|
||||
args.push('--all');
|
||||
}
|
||||
const { execa } = await import('execa');
|
||||
const res = await execa('bun', args, { cwd: options?.cwd });
|
||||
return dedupeDependencies(
|
||||
res.stdout
|
||||
.split('\n')
|
||||
.slice(1) // Skip the first line, is not a dependency
|
||||
.map((line) => line.trim())
|
||||
.map((line) => /.* (@?\S+)@(\S+)$/.exec(line))
|
||||
// @ts-expect-error: Filtering to known non-null matches
|
||||
.filter<RegExpExecArray>((match) => !!match)
|
||||
.map(([_, name, version]) => ({ name, version })),
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import {
|
||||
detectPackageManager,
|
||||
addDependency,
|
||||
addDevDependency,
|
||||
ensureDependencyInstalled,
|
||||
installDependencies,
|
||||
removeDependency,
|
||||
PackageManager,
|
||||
PackageManagerName,
|
||||
} from 'nypm';
|
||||
import { WxtPackageManager } from '~/types';
|
||||
import { bun } from './bun';
|
||||
import { WxtPackageManagerImpl } from './types';
|
||||
import { yarn } from './yarn';
|
||||
import { pnpm } from './pnpm';
|
||||
import { npm } from './npm';
|
||||
|
||||
export async function createWxtPackageManager(
|
||||
root: string,
|
||||
): Promise<WxtPackageManager> {
|
||||
const pm = await detectPackageManager(root, {
|
||||
includeParentDirs: true,
|
||||
});
|
||||
|
||||
// Use requirePm to prevent throwing errors before the package manager utils are used.
|
||||
const requirePm = <T>(cb: (pm: PackageManager) => T) => {
|
||||
if (pm == null) throw Error('Could not detect package manager');
|
||||
return cb(pm);
|
||||
};
|
||||
|
||||
return {
|
||||
get name() {
|
||||
return requirePm((pm) => pm.name);
|
||||
},
|
||||
get command() {
|
||||
return requirePm((pm) => pm.command);
|
||||
},
|
||||
get version() {
|
||||
return requirePm((pm) => pm.version);
|
||||
},
|
||||
get majorVersion() {
|
||||
return requirePm((pm) => pm.majorVersion);
|
||||
},
|
||||
get lockFile() {
|
||||
return requirePm((pm) => pm.lockFile);
|
||||
},
|
||||
get files() {
|
||||
return requirePm((pm) => pm.files);
|
||||
},
|
||||
addDependency,
|
||||
addDevDependency,
|
||||
ensureDependencyInstalled,
|
||||
installDependencies,
|
||||
removeDependency,
|
||||
get overridesKey() {
|
||||
return requirePm((pm) => packageManagers[pm.name].overridesKey);
|
||||
},
|
||||
downloadDependency(...args) {
|
||||
return requirePm((pm) =>
|
||||
packageManagers[pm.name].downloadDependency(...args),
|
||||
);
|
||||
},
|
||||
listDependencies(...args) {
|
||||
return requirePm((pm) =>
|
||||
packageManagers[pm.name].listDependencies(...args),
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const packageManagers: Record<PackageManagerName, WxtPackageManagerImpl> = {
|
||||
npm,
|
||||
pnpm,
|
||||
bun,
|
||||
yarn,
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Dependency } from '~/types';
|
||||
import { WxtPackageManagerImpl } from './types';
|
||||
import path from 'node:path';
|
||||
import { ensureDir } from 'fs-extra';
|
||||
|
||||
export const npm: WxtPackageManagerImpl = {
|
||||
overridesKey: 'overrides',
|
||||
async downloadDependency(id, downloadDir) {
|
||||
await ensureDir(downloadDir);
|
||||
const { execa } = await import('execa');
|
||||
const res = await execa('npm', ['pack', id, '--json'], {
|
||||
cwd: downloadDir,
|
||||
});
|
||||
const packed: PackedDependency[] = JSON.parse(res.stdout);
|
||||
return path.resolve(downloadDir, packed[0].filename);
|
||||
},
|
||||
async listDependencies(options) {
|
||||
const args = ['ls', '--json'];
|
||||
if (options?.all) {
|
||||
args.push('--depth', 'Infinity');
|
||||
}
|
||||
const { execa } = await import('execa');
|
||||
const res = await execa('npm', args, { cwd: options?.cwd });
|
||||
const project: NpmListProject = JSON.parse(res.stdout);
|
||||
|
||||
return flattenNpmListOutput([project]);
|
||||
},
|
||||
};
|
||||
|
||||
export function flattenNpmListOutput(projects: NpmListProject[]): Dependency[] {
|
||||
const queue: Record<string, NpmListDependency>[] = projects.flatMap(
|
||||
(project) => {
|
||||
const acc: Record<string, NpmListDependency>[] = [];
|
||||
if (project.dependencies) acc.push(project.dependencies);
|
||||
if (project.devDependencies) acc.push(project.devDependencies);
|
||||
return acc;
|
||||
},
|
||||
);
|
||||
const dependencies: Dependency[] = [];
|
||||
while (queue.length > 0) {
|
||||
Object.entries(queue.pop()!).forEach(([name, meta]) => {
|
||||
dependencies.push({
|
||||
name,
|
||||
version: meta.version,
|
||||
});
|
||||
if (meta.dependencies) queue.push(meta.dependencies);
|
||||
if (meta.devDependencies) queue.push(meta.devDependencies);
|
||||
});
|
||||
}
|
||||
return dedupeDependencies(dependencies);
|
||||
}
|
||||
|
||||
export function dedupeDependencies(dependencies: Dependency[]): Dependency[] {
|
||||
const hashes = new Set<string>();
|
||||
return dependencies.filter((dep) => {
|
||||
const hash = `${dep.name}@${dep.version}`;
|
||||
if (hashes.has(hash)) {
|
||||
return false;
|
||||
} else {
|
||||
hashes.add(hash);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export interface NpmListProject {
|
||||
name: string;
|
||||
dependencies?: Record<string, NpmListDependency>;
|
||||
devDependencies?: Record<string, NpmListDependency>;
|
||||
}
|
||||
|
||||
export interface NpmListDependency {
|
||||
version: string;
|
||||
resolved?: string;
|
||||
overridden?: boolean;
|
||||
dependencies?: Record<string, NpmListDependency>;
|
||||
devDependencies?: Record<string, NpmListDependency>;
|
||||
}
|
||||
|
||||
interface PackedDependency {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
filename: string;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { NpmListProject, flattenNpmListOutput, npm } from './npm';
|
||||
import { WxtPackageManagerImpl } from './types';
|
||||
|
||||
export const pnpm: WxtPackageManagerImpl = {
|
||||
overridesKey: 'resolutions', // "pnpm.overrides" has a higher priority, but I don't want to deal with nesting
|
||||
downloadDependency(...args) {
|
||||
return npm.downloadDependency(...args);
|
||||
},
|
||||
async listDependencies(options) {
|
||||
const args = ['ls', '-r', '--json'];
|
||||
if (options?.all) {
|
||||
args.push('--depth', 'Infinity');
|
||||
}
|
||||
// Helper for testing - since WXT uses pnpm workspaces, folders inside it don't behave like
|
||||
// standalone projects unless you pass the --ignore-workspace flag.
|
||||
if (
|
||||
typeof process !== 'undefined' &&
|
||||
process.env.WXT_PNPM_IGNORE_WORKSPACE === 'true'
|
||||
) {
|
||||
args.push('--ignore-workspace');
|
||||
}
|
||||
const { execa } = await import('execa');
|
||||
const res = await execa('pnpm', args, { cwd: options?.cwd });
|
||||
const projects: NpmListProject[] = JSON.parse(res.stdout);
|
||||
|
||||
return flattenNpmListOutput(projects);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import { WxtPackageManager } from '~/types';
|
||||
|
||||
export type WxtPackageManagerImpl = Pick<
|
||||
WxtPackageManager,
|
||||
'downloadDependency' | 'listDependencies' | 'overridesKey'
|
||||
>;
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Dependency } from '~/types';
|
||||
import { WxtPackageManagerImpl } from './types';
|
||||
import { dedupeDependencies, npm } from './npm';
|
||||
|
||||
export const yarn: WxtPackageManagerImpl = {
|
||||
overridesKey: 'resolutions',
|
||||
downloadDependency(...args) {
|
||||
return npm.downloadDependency(...args);
|
||||
},
|
||||
async listDependencies(options) {
|
||||
const args = ['list', '--json'];
|
||||
if (options?.all) {
|
||||
args.push('--depth', 'Infinity');
|
||||
}
|
||||
const { execa } = await import('execa');
|
||||
const res = await execa('yarn', args, { cwd: options?.cwd });
|
||||
const tree = res.stdout
|
||||
.split('\n')
|
||||
.map<JsonLine>((line) => JSON.parse(line))
|
||||
.find((line) => line.type === 'tree')?.data as JsonLineTree | undefined;
|
||||
if (tree == null) throw Error("'yarn list --json' did not output a tree");
|
||||
|
||||
const queue = [...tree.trees];
|
||||
const dependencies: Dependency[] = [];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const { name: treeName, children } = queue.pop()!;
|
||||
const match = /(@?\S+)@(\S+)$/.exec(treeName);
|
||||
if (match) {
|
||||
const [_, name, version] = match;
|
||||
dependencies.push({ name, version });
|
||||
}
|
||||
if (children != null) {
|
||||
queue.push(...children);
|
||||
}
|
||||
}
|
||||
|
||||
return dedupeDependencies(dependencies);
|
||||
},
|
||||
};
|
||||
|
||||
type JsonLine =
|
||||
| { type: unknown; data: unknown }
|
||||
| { type: 'tree'; data: JsonLineTree };
|
||||
|
||||
interface JsonLineTree {
|
||||
type: 'list';
|
||||
trees: Tree[];
|
||||
}
|
||||
|
||||
interface Tree {
|
||||
name: string;
|
||||
children?: Tree[];
|
||||
}
|
||||
@@ -4,10 +4,12 @@ import {
|
||||
fakeArray,
|
||||
fakeBackgroundEntrypoint,
|
||||
fakeBuildOutput,
|
||||
fakeContentScriptEntrypoint,
|
||||
fakeEntrypoint,
|
||||
fakeManifestCommand,
|
||||
fakeOptionsEntrypoint,
|
||||
fakePopupEntrypoint,
|
||||
fakeSidepanelEntrypoint,
|
||||
setFakeWxt,
|
||||
} from '../testing/fake-objects';
|
||||
import { Manifest } from 'webextension-polyfill';
|
||||
@@ -109,6 +111,7 @@ describe('Manifest Utils', () => {
|
||||
setFakeWxt({
|
||||
config: {
|
||||
outDir,
|
||||
manifestVersion: 3,
|
||||
manifest: {
|
||||
action: {
|
||||
default_icon: 'icon-16.png',
|
||||
@@ -117,13 +120,83 @@ describe('Manifest Utils', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const expected: Partial<Manifest.WebExtensionManifest> = {
|
||||
action: wxt.config.manifest.action,
|
||||
};
|
||||
|
||||
const { manifest: actual } = await generateManifest([], buildOutput);
|
||||
|
||||
expect(actual).toMatchObject(expected);
|
||||
expect(actual.action).toEqual(wxt.config.manifest.action);
|
||||
expect(actual.browser_action).toBeUndefined();
|
||||
expect(actual.page_action).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should generate `browser_action` for MV2 when only `action` is defined', async () => {
|
||||
const buildOutput = fakeBuildOutput();
|
||||
setFakeWxt({
|
||||
config: {
|
||||
outDir,
|
||||
manifestVersion: 2,
|
||||
manifest: {
|
||||
action: {
|
||||
default_title: 'Action',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest([], buildOutput);
|
||||
|
||||
expect(actual.action).toBeUndefined();
|
||||
expect(actual.browser_action).toEqual(wxt.config.manifest.action);
|
||||
expect(actual.page_action).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should keep the `page_action` for MV2 when both `action` and `page_action` are defined', async () => {
|
||||
const buildOutput = fakeBuildOutput();
|
||||
setFakeWxt({
|
||||
config: {
|
||||
outDir,
|
||||
manifestVersion: 2,
|
||||
manifest: {
|
||||
action: {
|
||||
default_title: 'Action',
|
||||
},
|
||||
page_action: {
|
||||
default_title: 'Page Action',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest([], buildOutput);
|
||||
|
||||
expect(actual.action).toBeUndefined();
|
||||
expect(actual.browser_action).toBeUndefined();
|
||||
expect(actual.page_action).toEqual(wxt.config.manifest.page_action);
|
||||
});
|
||||
|
||||
it('should keep the custom `browser_action` for MV2 when both `action` and `browser_action` are defined', async () => {
|
||||
const buildOutput = fakeBuildOutput();
|
||||
setFakeWxt({
|
||||
config: {
|
||||
outDir,
|
||||
manifestVersion: 2,
|
||||
manifest: {
|
||||
action: {
|
||||
default_title: 'Action',
|
||||
},
|
||||
browser_action: {
|
||||
default_title: 'Browser Action',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest([], buildOutput);
|
||||
|
||||
expect(actual.action).toBeUndefined();
|
||||
expect(actual.browser_action).toEqual(
|
||||
wxt.config.manifest.browser_action,
|
||||
);
|
||||
expect(actual.page_action).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -756,6 +829,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', () => {
|
||||
@@ -1174,6 +1377,76 @@ describe('Manifest Utils', () => {
|
||||
expect(actual.commands).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Stripping keys', () => {
|
||||
const mv2Manifest = {
|
||||
page_action: {},
|
||||
browser_action: {},
|
||||
automation: {},
|
||||
content_capabilities: {},
|
||||
converted_from_user_script: {},
|
||||
current_locale: {},
|
||||
differential_fingerprint: {},
|
||||
event_rules: {},
|
||||
file_browser_handlers: {},
|
||||
file_system_provider_capabilities: {},
|
||||
input_components: {},
|
||||
nacl_modules: {},
|
||||
natively_connectable: {},
|
||||
offline_enabled: {},
|
||||
platforms: {},
|
||||
replacement_web_app: {},
|
||||
system_indicator: {},
|
||||
user_scripts: {},
|
||||
};
|
||||
const mv3Manifest = {
|
||||
action: {},
|
||||
export: {},
|
||||
optional_host_permissions: {},
|
||||
side_panel: {},
|
||||
};
|
||||
const hostPermissionsManifest = {
|
||||
host_permissions: {},
|
||||
};
|
||||
const manifest: any = {
|
||||
...mv2Manifest,
|
||||
...mv3Manifest,
|
||||
...hostPermissionsManifest,
|
||||
};
|
||||
|
||||
it.each([
|
||||
['firefox', 2, mv2Manifest],
|
||||
['chrome', 2, { ...mv2Manifest, ...hostPermissionsManifest }],
|
||||
['safari', 2, { ...mv2Manifest, ...hostPermissionsManifest }],
|
||||
['edge', 2, { ...mv2Manifest, ...hostPermissionsManifest }],
|
||||
['firefox', 3, { ...mv3Manifest, ...hostPermissionsManifest }],
|
||||
['chrome', 3, { ...mv3Manifest, ...hostPermissionsManifest }],
|
||||
['safari', 3, { ...mv3Manifest, ...hostPermissionsManifest }],
|
||||
['edge', 3, { ...mv3Manifest, ...hostPermissionsManifest }],
|
||||
] as const)(
|
||||
"%s MV%s should only include that version's keys",
|
||||
async (browser, manifestVersion, expected) => {
|
||||
setFakeWxt({
|
||||
config: {
|
||||
browser,
|
||||
manifest,
|
||||
manifestVersion,
|
||||
command: 'build',
|
||||
},
|
||||
});
|
||||
const output = fakeBuildOutput();
|
||||
|
||||
const { manifest: actual } = await generateManifest([], output);
|
||||
|
||||
expect(actual).toEqual({
|
||||
name: expect.any(String),
|
||||
version: expect.any(String),
|
||||
manifest_version: manifestVersion,
|
||||
...expected,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stripPathFromMatchPattern', () => {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { describe, it } from 'vitest';
|
||||
import {
|
||||
VirtualModuleId,
|
||||
VirtualModuleName,
|
||||
VirtualEntrypointType,
|
||||
VirtualEntrypointModuleName,
|
||||
} from '../virtual-modules';
|
||||
|
||||
describe('Virtual Modules', () => {
|
||||
it('should resolve types to litteral values, not string', () => {
|
||||
// @ts-expect-error
|
||||
const _c: VirtualEntrypointType = '';
|
||||
// @ts-expect-error
|
||||
const _d: VirtualEntrypointModuleName = '';
|
||||
// @ts-expect-error
|
||||
const _b: VirtualModuleName = '';
|
||||
// @ts-expect-error
|
||||
const _a: VirtualModuleId = '';
|
||||
});
|
||||
});
|
||||
@@ -21,3 +21,14 @@ export function some<T>(
|
||||
if (predicate(array[i], i)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an item or array to an array.
|
||||
*/
|
||||
export function toArray<T>(a: T | T[]): T[] {
|
||||
return Array.isArray(a) ? a : [a];
|
||||
}
|
||||
|
||||
export function filterTruthy<T>(array: Array<T | undefined>): T[] {
|
||||
return array.filter((item) => !!item) as T[];
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
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';
|
||||
@@ -242,7 +245,7 @@ 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]);
|
||||
@@ -256,13 +259,87 @@ describe('findEntrypoints', () => {
|
||||
},
|
||||
);
|
||||
|
||||
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 () => {
|
||||
setFakeWxt({
|
||||
config: {
|
||||
manifestVersion: 2,
|
||||
},
|
||||
});
|
||||
const options: BackgroundEntrypoint['options'] = {
|
||||
const options: BackgroundEntrypointOptions = {
|
||||
type: 'module',
|
||||
};
|
||||
globMock.mockResolvedValueOnce(['background.ts']);
|
||||
@@ -279,7 +356,7 @@ describe('findEntrypoints', () => {
|
||||
manifestVersion: 3,
|
||||
},
|
||||
});
|
||||
const options: BackgroundEntrypoint['options'] = {
|
||||
const options: BackgroundEntrypointOptions = {
|
||||
type: 'module',
|
||||
};
|
||||
globMock.mockResolvedValueOnce(['background.ts']);
|
||||
@@ -331,7 +408,7 @@ describe('findEntrypoints', () => {
|
||||
outputDir: config.outDir,
|
||||
skipped: false,
|
||||
};
|
||||
const options: GenericEntrypoint['options'] = {};
|
||||
const options: BaseEntrypointOptions = {};
|
||||
globMock.mockResolvedValueOnce([path]);
|
||||
importEntrypointFileMock.mockResolvedValue(options);
|
||||
|
||||
@@ -462,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',
|
||||
|
||||
@@ -5,6 +5,7 @@ import { dirname, resolve } from 'path';
|
||||
import type { Ora } from 'ora';
|
||||
import pc from 'picocolors';
|
||||
import { wxt } from '../../wxt';
|
||||
import { toArray } from '../arrays';
|
||||
|
||||
export async function buildEntrypoints(
|
||||
groups: EntrypointGroup[],
|
||||
@@ -13,13 +14,14 @@ export async function buildEntrypoints(
|
||||
const steps: BuildStepOutput[] = [];
|
||||
for (let i = 0; i < groups.length; i++) {
|
||||
const group = groups[i];
|
||||
const groupNames = [group].flat().map((e) => e.name);
|
||||
const groupNames = toArray(group).map((e) => e.name);
|
||||
const groupNameColored = groupNames.join(pc.dim(', '));
|
||||
spinner.text =
|
||||
pc.dim(`[${i + 1}/${groups.length}]`) + ` ${groupNameColored}`;
|
||||
try {
|
||||
steps.push(await wxt.config.builder.build(group));
|
||||
steps.push(await wxt.builder.build(group));
|
||||
} catch (err) {
|
||||
spinner.stop().clear();
|
||||
wxt.logger.error(err);
|
||||
throw Error(`Failed to build ${groupNames.join(', ')}`, { cause: err });
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@ import {
|
||||
OptionsEntrypoint,
|
||||
PopupEntrypoint,
|
||||
UnlistedScriptDefinition,
|
||||
PopupEntrypointOptions,
|
||||
OptionsEntrypointOptions,
|
||||
SidepanelEntrypoint,
|
||||
SidepanelEntrypointOptions,
|
||||
} from '~/types';
|
||||
import fs from 'fs-extra';
|
||||
import { minimatch } from 'minimatch';
|
||||
@@ -19,7 +23,7 @@ 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';
|
||||
@@ -71,6 +75,8 @@ export async function findEntrypoints(): Promise<Entrypoint[]> {
|
||||
switch (type) {
|
||||
case 'popup':
|
||||
return await getPopupEntrypoint(info);
|
||||
case 'sidepanel':
|
||||
return await getSidepanelEntrypoint(info);
|
||||
case 'options':
|
||||
return await getOptionsEntrypoint(info);
|
||||
case 'background':
|
||||
@@ -199,153 +205,81 @@ function preventNoEntrypoints(files: EntrypointInfo[]) {
|
||||
}
|
||||
}
|
||||
|
||||
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({
|
||||
inputPath,
|
||||
name,
|
||||
skipped,
|
||||
}: 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) {
|
||||
wxt.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';
|
||||
}
|
||||
async function getPopupEntrypoint(
|
||||
info: EntrypointInfo,
|
||||
): Promise<PopupEntrypoint> {
|
||||
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,
|
||||
options: resolvePerBrowserOptions(options, wxt.config.browser),
|
||||
inputPath: info.inputPath,
|
||||
outputDir: wxt.config.outDir,
|
||||
skipped,
|
||||
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({
|
||||
inputPath,
|
||||
name,
|
||||
skipped,
|
||||
}: 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';
|
||||
}
|
||||
|
||||
async function getOptionsEntrypoint(
|
||||
info: EntrypointInfo,
|
||||
): Promise<OptionsEntrypoint> {
|
||||
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,
|
||||
options: resolvePerBrowserOptions(options, wxt.config.browser),
|
||||
inputPath: info.inputPath,
|
||||
outputDir: wxt.config.outDir,
|
||||
skipped,
|
||||
skipped: info.skipped,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path Absolute path to the HTML file.
|
||||
* @param content String contents of the file at the path.
|
||||
*/
|
||||
async function getUnlistedPageEntrypoint({
|
||||
inputPath,
|
||||
name,
|
||||
skipped,
|
||||
}: EntrypointInfo): Promise<GenericEntrypoint> {
|
||||
const content = await fs.readFile(inputPath, 'utf-8');
|
||||
const { document } = parseHTML(content);
|
||||
async function getUnlistedPageEntrypoint(
|
||||
info: EntrypointInfo,
|
||||
): Promise<GenericEntrypoint> {
|
||||
const options = await getHtmlEntrypointOptions<BaseEntrypointOptions>(info, {
|
||||
exclude: 'exclude',
|
||||
include: 'include',
|
||||
});
|
||||
|
||||
return {
|
||||
type: 'unlisted-page',
|
||||
name: getEntrypointName(wxt.config.entrypointsDir, inputPath),
|
||||
inputPath,
|
||||
name: info.name,
|
||||
inputPath: info.inputPath,
|
||||
outputDir: wxt.config.outDir,
|
||||
options: getHtmlBaseOptions(document),
|
||||
skipped,
|
||||
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({
|
||||
inputPath,
|
||||
name,
|
||||
@@ -358,21 +292,17 @@ async function getUnlistedScriptEntrypoint({
|
||||
`${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: wxt.config.outDir,
|
||||
options,
|
||||
options: resolvePerBrowserOptions(options, wxt.config.browser),
|
||||
skipped,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path Absolute path to the background's TS file.
|
||||
*/
|
||||
async function getBackgroundEntrypoint({
|
||||
inputPath,
|
||||
name,
|
||||
@@ -400,21 +330,11 @@ async function getBackgroundEntrypoint({
|
||||
name,
|
||||
inputPath,
|
||||
outputDir: wxt.config.outDir,
|
||||
options: {
|
||||
...options,
|
||||
type: resolvePerBrowserOption(options.type, wxt.config.browser),
|
||||
persistent: resolvePerBrowserOption(
|
||||
options.persistent,
|
||||
wxt.config.browser,
|
||||
),
|
||||
},
|
||||
options: resolvePerBrowserOptions(options, wxt.config.browser),
|
||||
skipped,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path Absolute path to the content script's TS file.
|
||||
*/
|
||||
async function getContentScriptEntrypoint({
|
||||
inputPath,
|
||||
name,
|
||||
@@ -432,11 +352,87 @@ async function getContentScriptEntrypoint({
|
||||
name,
|
||||
inputPath,
|
||||
outputDir: resolve(wxt.config.outDir, CONTENT_SCRIPT_OUT_DIR),
|
||||
options,
|
||||
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',
|
||||
|
||||
@@ -6,7 +6,10 @@ import {
|
||||
} from '~/types';
|
||||
import fs from 'fs-extra';
|
||||
import { relative, resolve } from 'path';
|
||||
import { getEntrypointBundlePath } from '~/core/utils/entrypoints';
|
||||
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';
|
||||
@@ -82,7 +85,7 @@ async function writePathsDeclarationFile(
|
||||
getEntrypointBundlePath(
|
||||
entry,
|
||||
wxt.config.outDir,
|
||||
entry.inputPath.endsWith('.html') ? '.html' : '.js',
|
||||
isHtmlEntrypoint(entry) ? '.html' : '.js',
|
||||
),
|
||||
)
|
||||
.concat(await getPublicFiles())
|
||||
|
||||
@@ -52,8 +52,8 @@ export async function importEntrypointFile<T>(path: string): Promise<T> {
|
||||
esmResolve: true,
|
||||
alias: {
|
||||
'webextension-polyfill': resolve(
|
||||
wxt.config.root,
|
||||
'node_modules/wxt/dist/virtual/mock-browser.js',
|
||||
wxt.config.wxtModuleDir,
|
||||
'dist/virtual/mock-browser.js',
|
||||
),
|
||||
},
|
||||
// Continue using node to load TS files even if `bun run --bun` is detected. Jiti does not
|
||||
@@ -105,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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,8 +16,9 @@ import {
|
||||
validateEntrypoints,
|
||||
} from '../validation';
|
||||
import consola from 'consola';
|
||||
import { exec } from '../exec';
|
||||
import { wxt } from '../../wxt';
|
||||
import { mergeJsonOutputs } from '@aklinker1/rollup-plugin-visualizer';
|
||||
import { isCI } from 'ci-info';
|
||||
|
||||
/**
|
||||
* Builds the extension based on an internal config. No more config discovery is performed, the
|
||||
@@ -36,7 +37,7 @@ export async function internalBuild(): Promise<BuildOutput> {
|
||||
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}`,
|
||||
`${wxt.builder.name} ${wxt.builder.version}`,
|
||||
)}`,
|
||||
);
|
||||
const startTime = Date.now();
|
||||
@@ -81,6 +82,15 @@ export async function internalBuild(): Promise<BuildOutput> {
|
||||
wxt.logger.info(
|
||||
`Analysis complete:\n ${pc.gray('└─')} ${pc.yellow(statsPath)}`,
|
||||
);
|
||||
if (wxt.config.analysis.open) {
|
||||
if (isCI) {
|
||||
wxt.logger.debug(`Skipped opening ${pc.yellow(statsPath)} in CI`);
|
||||
} else {
|
||||
wxt.logger.info(`Opening ${pc.yellow(statsPath)} in browser...`);
|
||||
const { default: open } = await import('open');
|
||||
open(wxt.config.analysis.outputFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
@@ -93,17 +103,11 @@ async function combineAnalysisStats(): Promise<void> {
|
||||
});
|
||||
const absolutePaths = unixFiles.map(unnormalizePath);
|
||||
|
||||
await exec(
|
||||
'rollup-plugin-visualizer',
|
||||
[
|
||||
...absolutePaths,
|
||||
'--template',
|
||||
wxt.config.analysis.template,
|
||||
'--filename',
|
||||
wxt.config.analysis.outputFile,
|
||||
],
|
||||
{ cwd: wxt.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)));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user