Compare commits

..

13 Commits

Author SHA1 Message Date
Aaron Klinker c20478cde2 Bump version to 0.14.2-alpha1 2024-01-14 16:19:54 -06:00
Aaron Klinker ebd50079a7 Cleanup unsed imports 2024-01-14 16:18:47 -06:00
Aaron Klinker b17bd87865 Add docs 2024-01-14 16:18:19 -06:00
Aaron Klinker e8b471e23f Improve typing and add tests for it 2024-01-14 15:05:52 -06:00
Aaron Klinker 32ffca4168 Add client tests 2024-01-14 14:49:15 -06:00
Aaron Klinker e9c573d5a1 Implement and generate types for wxt/i18n 2024-01-12 23:58:55 -06:00
Aaron Klinker ae5126d726 Update lockfile 2024-01-12 22:49:13 -06:00
Aaron Klinker 24a77ca9b5 Add i18n module to named export list 2024-01-12 22:49:03 -06:00
Aaron Klinker 174cbbc5eb Add i18n module to auto-imports 2024-01-12 22:45:58 -06:00
Aaron Klinker 607c70dd56 Build wxt/i18n 2024-01-12 22:45:41 -06:00
Aaron Klinker 616d012326 Update demo code for testing 2024-01-12 22:45:18 -06:00
Aaron Klinker 75b6ca7172 Add conversion code and tests 2024-01-12 22:45:04 -06:00
Aaron Klinker 1497b3099c Move demo files 2024-01-12 22:44:33 -06:00
129 changed files with 3050 additions and 5044 deletions
-4
View File
@@ -9,7 +9,3 @@ updates:
directory: '/' # Location of package manifests
schedule:
interval: 'monthly'
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: 'monthly'
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
publish:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- uses: docker/login-action@v3
with:
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
needs:
- validate
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
with:
fetch-depth: 0
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
sync:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8
+10 -10
View File
@@ -10,30 +10,30 @@ jobs:
formatting:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- run: pnpm format:check
lint:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- run: pnpm lint
type-check:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- run: pnpm compile
validate-demo:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- run: pnpm tsc --noEmit
working-directory: demo
- run: pnpm build:all
working-directory: demo
- run: pnpm tsc --noEmit
working-directory: demo
- run: pnpm wxt zip
working-directory: demo
- run: pnpm vitest run
@@ -41,17 +41,17 @@ jobs:
tests:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- name: pnpm test:coverage
run: pnpm test:coverage --reporter=default --reporter=hanging-process
- uses: codecov/codecov-action@v4
- uses: codecov/codecov-action@v3
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
windows-tests:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- run: pnpm test run
template:
@@ -65,7 +65,7 @@ jobs:
- vanilla
- vue
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- run: pnpm pack
- run: npm i
-303
View File
@@ -1,308 +1,5 @@
# Changelog
## v0.16.3
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.2...v0.16.3)
### 🚀 Enhancements
- Hooks ([#419](https://github.com/wxt-dev/wxt/pull/419))
### 🩹 Fixes
- **init:** Use `ungh` to prevent rate limits when loading templates ([37ad2c7](https://github.com/wxt-dev/wxt/commit/37ad2c7))
### 📖 Documentation
- Fix typo of intuitive ([#415](https://github.com/wxt-dev/wxt/pull/415))
- Fix typo of opinionated ([#416](https://github.com/wxt-dev/wxt/pull/416))
### 🏡 Chore
- Add dependabot for github actions ([#404](https://github.com/wxt-dev/wxt/pull/404))
- **deps-dev:** Bump happy-dom from 12.10.3 to 13.3.8 ([#411](https://github.com/wxt-dev/wxt/pull/411))
- **deps-dev:** Bump typescript from 5.3.2 to 5.3.3 ([#409](https://github.com/wxt-dev/wxt/pull/409))
- Register global `wxt` instance ([#418](https://github.com/wxt-dev/wxt/pull/418))
### ❤️ Contributors
- Chen Hua <hcljsq@gmail.com>
- Florian Metz ([@Timeraa](http://github.com/Timeraa))
## v0.16.2
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.1...v0.16.2)
### 🩹 Fixes
- Don't crash background service worker when using `import.meta.url` ([#402](https://github.com/wxt-dev/wxt/pull/402))
## v0.16.1
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.0...v0.16.1)
### 🩹 Fixes
- Don't require config to run `wxt submit init` ([9318346](https://github.com/wxt-dev/wxt/commit/9318346))
### 📖 Documentation
- Add premid extension to homepage ([#399](https://github.com/wxt-dev/wxt/pull/399))
### 🏡 Chore
- **templates:** Upgrade to wxt `^0.16.0` ([f0b2a12](https://github.com/wxt-dev/wxt/commit/f0b2a12))
### ❤️ Contributors
- Florian Metz ([@Timeraa](http://github.com/Timeraa))
## v0.16.0
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.15.4...v0.16.0)
### 🚀 Enhancements
- ⚠️ ESM background support ([#398](https://github.com/wxt-dev/wxt/pull/398))
### 📖 Documentation
- Document how to opt into ESM ([1e12ce2](https://github.com/wxt-dev/wxt/commit/1e12ce2))
### 🏡 Chore
- **deps-dev:** Bump lint-staged from 15.2.0 to 15.2.1 ([#395](https://github.com/wxt-dev/wxt/pull/395))
- **deps-dev:** Bump p-map from 7.0.0 to 7.0.1 ([#396](https://github.com/wxt-dev/wxt/pull/396))
- **deps-dev:** Bump @vitest/coverage-v8 from 1.0.1 to 1.2.2 ([#397](https://github.com/wxt-dev/wxt/pull/397))
#### ⚠️ Breaking Changes
In [#398](https://github.com/wxt-dev/wxt/pull/398), HTML pages' JS entrypoints in the output directory have been moved. Unless you're doing some kind of post-build work referencing files, you don't have to make any changes.
- Before:
```
.output/
<target>/
chunks/
some-shared-chunk-<hash>.js
popup-<hash>.js
popup.html
```
- After:
```
.output/
<target>/
chunks/
some-shared-chunk-<hash>.js
popup.html
popup.js
```
This effects all HTML files, not just the Popup. The hash has been removed, and it's been moved to the root of the build target folder, not inside the `chunks/` directory. Moving files like this has not historically increased review times or triggered in-depth reviews when submitting updates to the stores.
## v0.15.4
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.15.3...v0.15.4)
### 🩹 Fixes
- **submit:** Load `.env.submit` automatically when running `wxt submit` and `wxt submit init` ([#391](https://github.com/wxt-dev/wxt/pull/391))
## v0.15.3
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.15.2...v0.15.3)
### 🩹 Fixes
- **dev:** Reload `<name>/index.html` entrypoints properly on save ([#390](https://github.com/wxt-dev/wxt/pull/390))
## v0.15.2
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.15.1...v0.15.2)
### 🚀 Enhancements
- Add `submit` command ([#370](https://github.com/wxt-dev/wxt/pull/370))
### 🩹 Fixes
- **dev:** Resolve `script` and `link` aliases ([#387](https://github.com/wxt-dev/wxt/pull/387))
### ❤️ Contributors
- Nenad Novaković ([@dvlden](https://github.com/dvlden))
## v0.15.1
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.15.0...v0.15.1)
### 🚀 Enhancements
- Allow passing custom preferences to chrome, enabling dev mode on `chrome://extensions` and allowing content script sourcemaps automatically ([#384](https://github.com/wxt-dev/wxt/pull/384))
### 🩹 Fixes
- **security:** Upgrade to vite@5.0.12 to resolve CVE-2024-23331 ([39b76d3](https://github.com/wxt-dev/wxt/commit/39b76d3))
### 📖 Documentation
- Fixed doc errors on the guide/extension-api page ([#383](https://github.com/wxt-dev/wxt/pull/383))
### 🏡 Chore
- Fix vite version conflicts in demo extension ([98d2792](https://github.com/wxt-dev/wxt/commit/98d2792))
### ❤️ Contributors
- 0x7a7a ([@0x7a7a](https://github.com/0x7a7a))
## v0.15.0
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.14.7...v0.15.0)
### 🚀 Enhancements
- **zip:** ⚠️ Add `includeSources` and rename `ignoredSources` to `excludeSources` ([#378](https://github.com/wxt-dev/wxt/pull/378))
### 🩹 Fixes
- Generate missing sourcemap in `wxt:unimport` plugin ([#381](https://github.com/wxt-dev/wxt/pull/381))
- ⚠️ Move browser constants to `import.meta.env` ([#380](https://github.com/wxt-dev/wxt/pull/380))
- Enable inline sourcemaps by default during development ([#382](https://github.com/wxt-dev/wxt/pull/382))
### 📖 Documentation
- Fix typo ([f9718a1](https://github.com/wxt-dev/wxt/commit/f9718a1))
### 🏡 Chore
- Update contributor docs ([eb758bd](https://github.com/wxt-dev/wxt/commit/eb758bd))
#### ⚠️ Breaking Changes
Renamed `zip.ignoredSources` to `zip.excludeSources` in [#378](https://github.com/wxt-dev/wxt/pull/378)
Renamed undocumented constants for detecting the build config at runtime in [#380](https://github.com/wxt-dev/wxt/pull/380). Now documented here: https://wxt.dev/guide/multiple-browsers.html#runtime
- `__BROWSER__` &rarr; `import.meta.env.BROWSER`
- `__COMMAND__` &rarr; `import.meta.env.COMMAND`
- `__MANIFEST_VERSION__` &rarr; `import.meta.env.MANIFEST_VERSION`
- `__IS_CHROME__` &rarr; `import.meta.env.CHROME`
- `__IS_FIREFOX__` &rarr; `import.meta.env.FIREFOX`
- `__IS_SAFARI__` &rarr; `import.meta.env.SAFARI`
- `__IS_EDGE__` &rarr; `import.meta.env.EDGE`
- `__IS_OPERA__` &rarr; `import.meta.env.OPERA`
### ❤️ Contributors
- Nenad Novaković ([@dvlden](https://github.com/dvlden))
## v0.14.7
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.14.6...v0.14.7)
### 🩹 Fixes
- Improve error messages when importing and building entrypoints ([3b63a51](https://github.com/wxt-dev/wxt/commit/3b63a51))
- **storage:** Throw better error message when importing outside a extension environment ([35865ad](https://github.com/wxt-dev/wxt/commit/35865ad))
- Upgrade `web-ext-run` ([62ecb6f](https://github.com/wxt-dev/wxt/commit/62ecb6f))
### 📖 Documentation
- Add `matches` to content script examples ([dab8efa](https://github.com/wxt-dev/wxt/commit/dab8efa))
- Fix incorrect sample code ([#372](https://github.com/wxt-dev/wxt/pull/372))
- Document defined constants for the build target ([68874e6](https://github.com/wxt-dev/wxt/commit/68874e6))
- Add missing `await` to `createShadowRootUi` examples ([fc45c37](https://github.com/wxt-dev/wxt/commit/fc45c37))
### ❤️ Contributors
- 東奈比 ([@dongnaebi](http://github.com/dongnaebi))
## v0.14.6
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.14.5...v0.14.6)
### 🚀 Enhancements
- Restart dev mode when saving config ([#365](https://github.com/wxt-dev/wxt/pull/365))
- Add basic validation for entrypoint options ([#368](https://github.com/wxt-dev/wxt/pull/368))
### 🩹 Fixes
- Add subdependency bin directory so `wxt build --analyze` works with PNPM ([#363](https://github.com/wxt-dev/wxt/pull/363))
- Sort build output files naturally ([#364](https://github.com/wxt-dev/wxt/pull/364))
### 🤖 CI
- Check for type errors in demo before building ([4b005b4](https://github.com/wxt-dev/wxt/commit/4b005b4))
## v0.14.5
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.14.4...v0.14.5)
### 🚀 Enhancements
- Add `dev.reloadCommand` config ([#362](https://github.com/wxt-dev/wxt/pull/362))
### 🩹 Fixes
- Disable reload dev command when 4 commands are already registered ([#361](https://github.com/wxt-dev/wxt/pull/361))
## v0.14.4
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.14.3...v0.14.4)
### 🩹 Fixes
- Allow requiring built-in node modules from ESM CLI ([#356](https://github.com/wxt-dev/wxt/pull/356))
### 🏡 Chore
- Add unit tests for passing flags via the CLI ([#354](https://github.com/wxt-dev/wxt/pull/354))
## v0.14.3
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.14.2...v0.14.3)
### 🩹 Fixes
- Make `getArrayFromFlags` result can be undefined ([#352](https://github.com/wxt-dev/wxt/pull/352))
### ❤️ Contributors
- Yuns ([@yunsii](http://github.com/yunsii))
## v0.14.2
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.14.1...v0.14.2)
### 🚀 Enhancements
- Add `filterEntrypoints` option to speed up development ([#344](https://github.com/wxt-dev/wxt/pull/344))
### 🔥 Performance
- Only call `findEntrypoint` once per build ([#342](https://github.com/wxt-dev/wxt/pull/342))
### 🩹 Fixes
- Improve error message and document use of imported variables outside an entrypoint's `main` function ([#346](https://github.com/wxt-dev/wxt/pull/346))
- Allow `browser.runtime.getURL` to include hashes and query params for HTML paths ([#350](https://github.com/wxt-dev/wxt/pull/350))
### 📖 Documentation
- Fix typos and outdated ui function usage ([#347](https://github.com/wxt-dev/wxt/pull/347))
### 🏡 Chore
- Update templates to `^0.14.0` ([70a4961](https://github.com/wxt-dev/wxt/commit/70a4961))
- Fix typo in function name ([a329e24](https://github.com/wxt-dev/wxt/commit/a329e24))
### ❤️ Contributors
- Yuns ([@yunsii](http://github.com/yunsii))
- Armin
## v0.14.1
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.14.0...v0.14.1)
+1 -9
View File
@@ -57,17 +57,9 @@ pnpm test
pnpm docs:dev
```
## Updating Docs
Documentation is written with VitePress, and is located in the `docs/` directory.
The API reference is generated from JSDoc comments in the source code. If there's a typo or change you want to make in there, you'll need to update the source code instead of a file in the `docs/` directory.
## Testing
WXT has unit and E2E tests. When making a change or adding a feature, make sure to update the tests or add new ones, if they exist.
> If they don't exist, feel free to create them, but that's a lot for a one-time contributor. A maintainer might add them to your PR though.
WXT has unit and E2E tests. When making a change or adding a feature, make sure to update the tests or add new ones.
To run tests for a specific file, add the filename at the end of the test command:
+37 -36
View File
@@ -1,42 +1,43 @@
import messages from 'public/_locales/en/messages.json';
export default defineBackground(() => {
console.log(browser.runtime.id);
logId();
console.log({
browser: __BROWSER__,
chrome: __IS_CHROME__,
firefox: __IS_FIREFOX__,
manifestVersion: __MANIFEST_VERSION__,
});
export default defineBackground({
// type: 'module',
// @ts-expect-error: should only accept entrypoints or public assets
browser.runtime.getURL('/');
browser.runtime.getURL('/background.js');
browser.runtime.getURL('/icon-128.png');
main() {
console.log(browser.runtime.id);
logId();
console.log({
url: import.meta.url,
browser: import.meta.env.BROWSER,
chrome: import.meta.env.CHROME,
firefox: import.meta.env.FIREFOX,
manifestVersion: import.meta.env.MANIFEST_VERSION,
messages,
});
console.log([
// @ts-expect-error: browser.i18n should only accept known message names
browser.i18n.getMessage('test'),
browser.i18n.getMessage('promptForName'),
browser.i18n.getMessage('hello', ['Aaron']),
browser.i18n.getMessage('bye', ['Aaron']),
browser.i18n.getMessage('@@extension_id'),
browser.i18n.getMessage('nItems'),
]);
// @ts-expect-error: should only accept entrypoints or public assets
browser.runtime.getURL('/');
browser.runtime.getURL('/background.js');
browser.runtime.getURL('/icon/128.png');
browser.runtime.getURL('/example.html#hash');
browser.runtime.getURL('/example.html?query=param');
// @ts-expect-error: should only allow hashes/query params on HTML files
browser.runtime.getURL('/icon-128.png?query=param');
console.log([
// @ts-expect-error: i18n should only accept known message names
i18n.t('test'),
i18n.t('promptForName'),
i18n.t('hello', ['Aaron']),
i18n.t('bye', ['Aaron']),
i18n.t('@@extension_id'),
i18n.tp('nItems', 0, ['0']),
]);
// @ts-expect-error: should only accept known message names
browser.i18n.getMessage('test');
browser.i18n.getMessage('prompt_for_name');
browser.i18n.getMessage('hello', 'Aaron');
browser.i18n.getMessage('bye', ['Aaron']);
browser.i18n.getMessage('@@extension_id');
console.log('WXT MODE:', {
MODE: import.meta.env.MODE,
DEV: import.meta.env.DEV,
PROD: import.meta.env.PROD,
});
console.log('WXT MODE:', {
MODE: import.meta.env.MODE,
DEV: import.meta.env.DEV,
PROD: import.meta.env.PROD,
});
storage.setItem('session:startTime', Date.now());
},
storage.setItem('session:startTime', Date.now());
});
+21
View File
@@ -0,0 +1,21 @@
promptForName: What's your name?
hello:
message: Hello, $USER$
description: Greet the user
placeholders:
user:
content: $1
example: Paul
bye:
message: Goodbye, $USER$. Come back to $OUR_SITE$ soon!
description: Say goodbye to the user
placeholders:
our_site:
content: Example.com
user:
content: $1
example: Paul
nItems:
0: 0 items
1: 1 item
n: $1 items
-29
View File
@@ -1,29 +0,0 @@
{
"prompt_for_name": {
"message": "What's your name?",
"description": "Ask for the user's name"
},
"hello": {
"message": "Hello, $USER$",
"description": "Greet the user",
"placeholders": {
"user": {
"content": "$1",
"example": "Cira"
}
}
},
"bye": {
"message": "Goodbye, $USER$. Come back to $OUR_SITE$ soon!",
"description": "Say goodbye to the user",
"placeholders": {
"our_site": {
"content": "Example.com"
},
"user": {
"content": "$1",
"example": "Cira"
}
}
}
}

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Before

Width:  |  Height:  |  Size: 504 B

After

Width:  |  Height:  |  Size: 504 B

Before

Width:  |  Height:  |  Size: 936 B

After

Width:  |  Height:  |  Size: 936 B

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -17,7 +17,6 @@ const chromeExtensionIds = [
'okifoaikfmpfcamplcfjkpdnhfodpkil', // Doozy: Ai Made Easy
'lknmjhcajhfbbglglccadlfdjbaiifig', // tl;dv - Record, Transcribe & ChatGPT for Google Meet
'youtube中文配音/oglffgiaiekgeicdgkdlnlkhliajdlja', // Youtube中文配音
'agjnjboanicjcpenljmaaigopkgdnihi', // PreMiD
];
const { data } = useListExtensionDetails(chromeExtensionIds);
+1 -1
View File
@@ -89,8 +89,8 @@ export default defineConfig({
{ text: 'Storage', link: '/guide/storage.md' },
{ text: 'Assets', link: '/guide/assets.md' },
{ text: 'Content Script UI', link: '/guide/content-script-ui.md' },
{ text: 'Localization', link: '/guide/localization.md' },
{ text: 'Multiple Browsers', link: '/guide/multiple-browsers.md' },
{ text: 'ES Modules', link: '/guide/esm.md' },
{ text: 'Auto-imports', link: '/guide/auto-imports.md' },
{ text: 'Vite', link: '/guide/vite.md' },
{ text: 'Remote Code', link: '/guide/remote-code.md' },
+12 -31
View File
@@ -21,8 +21,6 @@ Integrated content script UIs are injected alongside the content of a page. This
```ts [Vanilla]
// entrypoints/example-ui.content.ts
export default defineContentScript({
matches: ['<all_urls>'],
main(ctx) {
const ui = createIntegratedUi(ctx, {
position: 'inline',
@@ -47,8 +45,6 @@ import { createApp } from 'vue';
import App from './App.vue';
export default defineContentScript({
matches: ['<all_urls>'],
main(ctx) {
const ui = createIntegratedUi(ctx, {
position: 'inline',
@@ -77,8 +73,6 @@ import ReactDOM from 'react-dom/client';
import App from './App.tsx';
export default defineContentScript({
matches: ['<all_urls>'],
main(ctx) {
const ui = createIntegratedUi(ctx, {
position: 'inline',
@@ -101,13 +95,11 @@ export default defineContentScript({
});
```
```ts [Svelte]
```ts [Svelete]
// entrypoints/example-ui.content/index.ts
import App from './App.svelte';
export default defineContentScript({
matches: ['<all_urls>'],
main(ctx) {
const ui = createIntegratedUi(ctx, {
position: 'inline',
@@ -136,8 +128,6 @@ export default defineContentScript({
import { render } from 'solid-js/web';
export default defineContentScript({
matches: ['<all_urls>'],
main(ctx) {
const ui = createIntegratedUi(ctx, {
position: 'inline',
@@ -184,7 +174,6 @@ To use `createShadowRootUi`, follow these steps:
import './style.css';
export default defineContentScript({
matches: ['<all_urls>'],
// 2. Set cssInjectionMode
cssInjectionMode: 'ui',
@@ -193,7 +182,7 @@ export default defineContentScript({
const ui = await createShadowRootUi(ctx, {
name: 'example-ui',
anchor: '#anchor',
position: 'inline',
type: 'inline',
onMount(container) {
// Define how your UI will be mounted inside the container
const app = document.createElement('p');
@@ -215,7 +204,6 @@ import { createApp } from 'vue';
import App from './App.vue';
export default defineContentScript({
matches: ['<all_urls>'],
// 2. Set cssInjectionMode
cssInjectionMode: 'ui',
@@ -224,7 +212,7 @@ export default defineContentScript({
const ui = await createShadowRootUi(ctx, {
name: 'example-ui',
anchor: '#anchor',
position: 'inline',
type: 'inline',
onMount: (container) => {
// Define how your UI will be mounted inside the container
const app = createApp(App);
@@ -250,14 +238,12 @@ import ReactDOM from 'react-dom/client';
import App from './App.tsx';
export default defineContentScript({
matches: ['<all_urls>'],
// 2. Set cssInjectionMode
cssInjectionMode: 'ui',
async main(ctx) {
main(ctx) {
// 3. Define your UI
const ui = await createShadowRootUi(ctx, {
name: 'example-ui',
const ui = createIntegratedUi(ctx, {
position: 'inline',
anchor: '#anchor',
onMount: (container) => {
@@ -278,20 +264,18 @@ export default defineContentScript({
});
```
```ts [Svelte]
```ts [Svelete]
// 1. Import the style
import './style.css';
import App from './App.svelte';
export default defineContentScript({
matches: ['<all_urls>'],
// 2. Set cssInjectionMode
cssInjectionMode: 'ui',
async main(ctx) {
main(ctx) {
// 3. Define your UI
const ui = await createShadowRootUi(ctx, {
name: 'example-ui',
const ui = createIntegratedUi(ctx, {
position: 'inline',
anchor: '#anchor',
onMount: (container) => {
@@ -319,14 +303,12 @@ import './style.css';
import { render } from 'solid-js/web';
export default defineContentScript({
matches: ['<all_urls>'],
// 2. Set cssInjectionMode
cssInjectionMode: 'ui',
async main(ctx) {
main(ctx) {
// 3. Define your UI
const ui = await createShadowRootUi(ctx, {
name: 'example-ui',
const ui = createIntegratedUi(ctx, {
position: 'inline',
anchor: '#anchor',
onMount: (container) => {
@@ -392,14 +374,13 @@ WXT provides a helper function, [`createIframeUi`](/api/wxt/client/functions/cre
```ts
export default defineContentScript({
matches: ['<all_urls>'],
// ...
async main(ctx) {
// Define the UI
const ui = await createIframeUi(ctx, {
page: '/example-iframe.html',
anchor: '#anchor',
position: 'inline',
type: 'inline',
onMount: (wrapper, iframe) => {
// Add styles to the iframe like width
iframe.width = 123;
-53
View File
@@ -86,56 +86,3 @@ export default defineContentScript({
:::info
For a full list of entrypoints and each of their options, see the [`/entrypoints` folder](/entrypoints/background) documentation.
:::
### Side Effects
You cannot use imported variables outside the `main` function is JS entrypoints. This includes options, as shown below:
```ts
// entrypoints/content.ts
import { GOOGLE_MATCHES } from '~/utils/match-patterns';
export default defineContentScript({
matches: GOOGLE_MATCHES,
main() {
// ...
},
});
```
```
$ wxt build
wxt build
WXT 0.14.1
Building chrome-mv3 for production with Vite 5.0.5
✖ Command failed after 360 ms
[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.
:::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.
:::
:::warning
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:
```ts
import { GOOGLE_MATCHES } from '~/utils/match-patterns'; // [!code --]
export default defineContentScript({
matches: GOOGLE_MATCHES, // [!code --]
matches: ['*//*.google.com/*'], // [!code ++]
main() {
// ...
},
});
```
-38
View File
@@ -1,38 +0,0 @@
# ES Modules
Configure entrypoints to use ESM at runtime.
Currently, ESM entrypoints are opt-in, so you must configure each entrypoint with that in mind.
## HTML Pages <Badge type="warning" text="≥0.0.1" />
In general, you should always make HTML pages import ESM scripts, unless you need to support old browsers.
To make a script ESM, add `type="module"`:
<!-- prettier-ignore -->
```html
<script src="./main.ts"></script> <!-- [!code --] -->
<script src="./main.ts" type="module"></script> <!-- [!code ++] -->
```
## Background <Badge type="warning" text="≥0.16.0" />
In your background script, set `type: "module"`:
```ts
export default defineBackground({
type: 'module', // !code ++
main() {
// ...
},
});
```
:::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.
:::
## Content Scripts
Coming soon. Follow [Content Script ESM Support #357](https://github.com/wxt-dev/wxt/issues/357) for updates.
+9 -7
View File
@@ -57,19 +57,21 @@ Here's a basic request/response example:
// popup/main.ts
const res = await browser.runtime.sendMessage('ping');
console.log(res); // "pong"
console.log('res'); // "pong"
```
```ts
// background.ts
export default defineBackground(() => {
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log(message); // "ping"
browser.runtime.onMessage.addEventListener(
(message, sender, sendResponse) => {
console.log(message); // "ping"
// Wait 1 second and respond with "pong"
setTimeout(() => sendResponse('pong'), 1000);
return true;
});
// Wait 1 second and respond with "pong"
setTimeout(() => sendResponse('pong'), 1000);
return true;
},
);
});
```
-1
View File
@@ -145,6 +145,5 @@ If you're an experienced web extension developer and think the dev manifest look
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
+2 -2
View File
@@ -7,7 +7,7 @@ head:
# Introduction
WXT is a free and open source framework for building web extensions in an conventional, intuitive, and safe way **_for all browsers_**.
WXT is a free and open source framework for building web extensions in an conventional, intuative, and safe way **_for all browsers_**.
WXT is based of [Nuxt](https://nuxt.com), and aims to provide the same great DX with TypeScript, auto-imports, and an opinionated project structure.
@@ -15,7 +15,7 @@ WXT is based of [Nuxt](https://nuxt.com), and aims to provide the same great DX
## Conventions
WXT is an opinionated framework. This helps keep projects consistent and easy to pick up.
WXT is an optionated framework. This helps keep projects consistent and easy to pick up.
- **Generated manifest**: Based on your project's file structure
- **Entrypoint configuration**: Configure entrypoints from the same file they're declare in
+159
View File
@@ -0,0 +1,159 @@
---
outline: deep
---
# Localization
WXT includes a util, [`i18n`](/api/wxt/i18n/), that provides a type-safe feature-rich alternative to `browser.i18n.getMessage`.
It is available automatically when you create a messages file under the `locales/` directory, and add a `default_locale` to the manifest:
```
<srcDir>
└─ locales/
├─ en.json
├─ es.json5
├─ fr.yml
├─ de.yaml
└─ ...
```
> You can use JSON, JSON5, or YAML formats.
```ts
// wxt.config.ts
export default defineConfig({
manifest: {
default_locale: 'en',
},
});
```
## Message File Format
```yml
# Use plain strings
simpleMessage: Hello world!
# Nest strings in objects
popup:
overview:
title: Nested text
# Plural form support
items:
1: 1 item
n: $1 items
# Optionally include a custom string for 0
cartSize:
0: Empty
1: 1 item
n: $1 items
# Or stick with the standard web extension format (with a message, description, and placeholders)
manifestMessage:
message: $THIS$ is translated
description: This is not-translated, helps translators
placeholder:
this:
content: This
```
:::tip
`locales/<code>.json` is 100% compatible with the standard web extension localization format (`_locales/<code>/messages.json`). If you have existing messages files, just move them into the `locales/` directory.
:::
## Usage
`i18n` is auto-imported, but can be manually imported from `wxt/i18n`.
```ts
import { i18n } from 'wxt/i18n';
```
### Basic Usage
You can access messages by their name:
```yml
helloWorld: Hello world!
```
```ts
i18n.t('helloWorld'); // "Hello world!"
```
Nested messages are combined into one string using an `_`.
```yml
popup:
overview:
title: Hello world!
```
```ts
i18n.t('popup_overview_title'); // "Hello world!"
```
If a message is in the standard web extension format, don't include a `_message`, even if it's nested.
```yml
helloWorld:
message: Hello world!
description: Some description
popup:
overview:
title:
message: Nested Title
```
```ts
i18n.t('helloWorld'); // "Hello world!"
i18n.t('popup_overview_title'); // "Nested Title"
```
### Substitutions
To insert a custom string into a translation, pass an array of values as the second parameter of the `i18n.t` function:
```yml
hello: Hello, $1, my name is $2.
```
```ts
i18n.t('hello', ['Aaron', 'Mark']); // "Hello Aaron, my name is Mark."
```
### Plural Form
When getting the translation for text with a plural form, use the `i18n.tp` function.
```yml
friends:
0: I have no friends.
1: I have a friend.
n: I have many friends.
```
```ts
i18n.tp('friends', 0); // "I have no friends."
i18n.tp('friends', 1); // "I have one friend."
i18n.tp('friends', 2); // "I have many friends."
```
The first number is the `count`. It is what decides which form will be used.
Substitutions are not required. But usually, a plural form will look something like this:
```yml
items:
1: 1 item
n: $1 items
```
```ts
i18n.tp('items', 0, ['0']); // "0 items"
i18n.tp('items', 1, ['1']); // "1 item"
i18n.tp('items', 2, ['2']); // "2 items"
i18n.tp('items', 3, ['3']); // "3 items"
```
+3 -22
View File
@@ -17,9 +17,11 @@ Separate build targets are written to their own output directories:
To build for a specific browser, pass the `-b --browser` flag from the CLI:
```sh
```
wxt --browser firefox
wxt build --browser firefox
```
By default, it will build for `chrome`. When excluding the [manifest version flags](#target-manifest-version), it will default to the commonly accepted manifest version for that browser.
@@ -110,24 +112,3 @@ export default defineContentScript({
:::warning
Only `defineBackground` and `defineContentScript` support per-browser options right now.
:::
## Runtime
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.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"`
- `import.meta.env.SAFARI`: A boolean equivalent to `import.meta.env.BROWSER === "safari"`
- `import.meta.env.OPERA`: A boolean equivalent to `import.meta.env.BROWSER === "opera"`
- `import.meta.env.COMMAND`: A string, `"serve"` when running `wxt` for development or `"build"` in all other cases.
:::info
These variables are constants defined at build time based on the build target. They do not actually detect which browser the code is running in.
For example, if you build for `--browser chrome` and publish it on Edge, `import.meta.env.BROWSER` will be `"chrome"`, not `"edge"`. You have to build a separate ZIP for `--browser edge` before `import.meta.env.BROWSER` will be `"edge"`.
If you need to know the actual browser your code is being ran on, you should use a [user agent parser](https://www.npmjs.com/package/ua-parser-js).
:::
+88 -74
View File
@@ -1,66 +1,94 @@
---
outline: deep
---
# Publishing
WXT will help you ZIP your extensions and submit them to the stores for review.
WXT offers several utilities that simplify the publishing process.
## First Time Publishing
If you're publishing an extension to a store for the first time, you must manually navigate the process. WXT doesn't help you create listings, each store has unique steps and requirements that you need to familiarize yourself with.
If you're publishing an extension to a store for the first time, you must manually navigate the process. Each store has unique steps and requirements that you need to familiarize yourself with.
For specific details about each store, see the stores sections below.
- [Chrome Web Store](#chrome-web-store)
- [Firefox Addon Store](#firefox-addon-store)
- [Edge Addons](#edge-addons)
## Automation
WXT provides two commands to help automate the release process:
- `wxt submit`: Submit new versions of your extension for review (and publish them automatically once approved)
- `wxt submit init`: Help setup all the required secrets and options for the `wxt submit` command
To get started, run `wxt submit init` and follow the prompts. Once finished, you should have a `.env.submit` file! WXT will use this file to submit your updates.
> In CI, make sure you add all the environment variables to the submit step.
To release an update, build all the ZIPs you plan on releasing:
Each store requires that a ZIP file be uploaded. You can generate these using the `wxt zip` command:
```sh
wxt zip
wxt zip -b firefox
# etc
```
Then run the `wxt submit` command, passing in all the ZIP files you want to release. In this case, we'll do a release for all 3 major stores: Chrome Web Store, Edge Addons, and Firefox Addons Store.
Generated ZIP files are stored in the `.output` directory.
If it's your first time running the command, you'll want to test your secrets by passing the `--dry-run` flag:
## Automation
```sh
wxt submit --dry-run \
--chrome-zip .output/<your-extension>-<version>-chrome.zip \
--firefox-zip .output/<your-extension>-<version>-firefox.zip --firefox-sources-zip .output/<your-extension>-<version>-sources.zip \
--edge-zip .output/<your-extension>-<version>-chrome.zip
```
To automate releasing updates, use the [`publish-browser-extension`](https://www.npmjs.com/package/publish-browser-extension) package.
If the dry run passes, remove the flag and do the actual release:
```sh
wxt submit \
--chrome-zip .output/<your-extension>-<version>-chrome.zip \
--firefox-zip .output/<your-extension>-<version>-firefox.zip --firefox-sources-zip .output/<your-extension>-<version>-sources.zip \
--edge-zip .output/<your-extension>-<version>-chrome.zip
```
:::tip
If you only need to release to a single store, only pass that store's ZIP flag.
:::info
🚧 WXT plans to eventually incorporate the `publish-browser-extension` package into its own `wxt submit` command.
:::
:::tip
See the [Firefox Addon Store](#firefox-addon-store) section for more details about the `--firefox-sources-zip` option.
:::
1. Install the necessary dependencies:
```sh
pnpm add -D publish-browser-extension env-cmd
```
2. Add scripts to your `package.json` file:
```json
{
"scripts": {
"submit": "env-cmd -f .env.submit -- publish-extension",
"submit:dry": "env-cmd -f .env.submit -- publish-extension --dry-run"
}
}
```
3. Create a `.env.submit` file and include the code below. If you're not publishing to certain stores, simply ignore their respective variables.
```txt
CHROME_EXTENSION_ID=""
CHROME_CLIENT_ID=""
CHROME_CLIENT_SECRET=""
CHROME_REFRESH_TOKEN=""
FIREFOX_EXTENSION_ID=""
FIREFOX_JWT_ISSUER=""
FIREFOX_JWT_SECRET=""
EDGE_PRODUCT_ID=""
EDGE_CLIENT_ID=""
EDGE_CLIENT_SECRET=""
EDGE_ACCESS_TOKEN_URL=""
```
> Each value will be filled in during the next step.
4. Run `npx publish-extension --help` for assistance with filling out all the values. Insert the obtained values within the double quotes.
5. ZIP all the targets you plan to publish, in this case Chrome and Firefox.
```sh
wxt zip
wxt zip -b firefox
```
6. Test your credentials by running the `submit:dry` command:
```sh
pnpm submit:dry \
--chrome-zip .output/your-extension-X.Y.Z-chrome.zip \
--firefox-zip .output/your-extension-X.Y.Z-firefox.zip \
--firefox-sources-zip .output/your-extension-X.Y.Z-sources.zip \
--edge-zip .output/your-extension-X.Y.Z-chrome.zip
```
7. Upload and submit your extension for review:
```sh
pnpm submit \
--chrome-zip .output/your-extension-X.Y.Z-chrome.zip \
--firefox-zip .output/your-extension-X.Y.Z-firefox.zip \
--firefox-sources-zip .output/your-extension-X.Y.Z-sources.zip \
--edge-zip .output/your-extension-X.Y.Z-chrome.zip
```
## GitHub Action
@@ -70,11 +98,9 @@ Here's an example of a GitHub Action to automate submiting new versions of your
# TODO
```
## Stores
## Chrome Web Store
### Chrome Web Store
> ✅ Supported &bull; [Developer Dashboard](https://chrome.google.com/webstore/developer/dashboard) &bull; [Publishing Docs](https://developer.chrome.com/docs/webstore/publish/)
✅ Automated &bull; [Developer Dashboard](https://chrome.google.com/webstore/developer/dashboard) &bull; [Publishing Docs](https://developer.chrome.com/docs/webstore/publish/)
To create a ZIP for Chrome:
@@ -82,15 +108,15 @@ To create a ZIP for Chrome:
wxt zip
```
### Firefox Addon Store
## Firefox Addon Store
> ✅ Supported &bull; [Developer Dashboard](https://addons.mozilla.org/developers/) &bull; [Publishing Docs](https://extensionworkshop.com/documentation/publish/submitting-an-add-on/)
✅ Automated &bull; [Developer Dashboard](https://addons.mozilla.org/developers/) &bull; [Publishing Docs](https://extensionworkshop.com/documentation/publish/submitting-an-add-on/)
Firefox requires you to upload a ZIP of your source code. This allows them to rebuild your extension and review the code in a readable way. More details can be found in [Firefox's docs](https://extensionworkshop.com/documentation/publish/source-code-submission/).
WXT fully supports generating and automatically submitting a source code ZIP.
WXT and `publish-browser-extension` both fully support generating and automatically submitting a source code ZIP.
When you run `wxt zip -b firefox`, your sources are zipped into the `.output` directory alongside the extension. WXT will automatically exclude certain files such as config files, hidden files, and tests. However, it's important to manually check the ZIP to ensure it only contains the files necessary to rebuild your extension.
When you run `wxt zip -b firefox`, your sources are zipped into the `.output` directory along with your built extension. WXT is configured to exclude certain files such as config files, hidden files, and tests. However, it's important to manually check the ZIP to ensure it only contains the files necessary to rebuild your extension.
To customize which files are zipped, add the `zip` option to your config file.
@@ -126,34 +152,22 @@ yarn 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.
:::warning
If you use a `.env` files, they can effect the chunk hashes in the output directory. Either delete the .env file before running `wxt zip -b firefox`, or include it in your sources zip with the [`zip.includeSources`](/api/wxt/interfaces/InlineConfig#includesources) option. Be careful to not include any secrets in your `.env` files.
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.
### Safari
## Safari
> 🚧 Not supported yet
🚧 Not automated at this time
WXT does not currently support automated publishing for Safari. Safari extensions require a native MacOS or iOS app wrapper, which WXT does not create yet. For now, if you want to publish to Safari, follow this guide:
:::warning
🚧 WXT does not currently support automated publishing for Safari. Safari extensions require a native MacOS or iOS app wrapper, which WXT cannot create at this time. For now, if you want to publish to Safari, follow this guide:
- [Converting a web extension for Safari](https://developer.apple.com/documentation/safariservices/safari_web_extensions/converting_a_web_extension_for_safari) - "Convert your existing extension to a Safari web extension using Xcodes command-line tool."
https://developer.apple.com/documentation/safariservices/safari_web_extensions/distributing_your_safari_web_extension
When running the `safari-web-extension-converter` CLI tool, pass the `.output/safari-mv2` or `.output/safari-mv3` directory, not your source code directory.
:::
```sh
pnpm wxt build -b safari
xcrun safari-web-extension-converter .output/safari-mv2
```
## Edge Addons
### Edge Addons
> ✅ Supported &bull; [Developer Dashboard](https://aka.ms/PartnerCenterLogin) &bull; [Publishing Docs](https://learn.microsoft.com/en-us/microsoft-edge/extensions-chromium/publish/publish-extension)
✅ Automated &bull; [Developer Dashboard](https://aka.ms/PartnerCenterLogin) &bull; [Publishing Docs](https://learn.microsoft.com/en-us/microsoft-edge/extensions-chromium/publish/publish-extension)
No need to create a specific ZIP for Edge. If you're already publishing to the Chrome Web Store, you can reuse your Chrome ZIP.
+1 -1
View File
@@ -19,7 +19,7 @@ If you want to try to use a different framework for unit tests, you will need to
- **Auto-imports**: Add `unimport` to your test environment or disable them by setting `imports: false` in your `wxt.config.ts` file
- **`browser` mock**: Mock the `webextension-polyfill` module globally with `wxt/dist/virtual/mock-browser.js`
- **[Remote Code Bundling](/guide/remote-code)**: If you use it, configure your environment to handle the `url:` module prefix
- **Global Variables**: If you consume them, manually define globals provided by WXT (like `import.meta.env.BROWSER`) by adding them to the global scope before accessing them (`import.meta.env.BROWSER = "chrome"`)
- **Global Variables**: If you consume them, manually define globals provided by WXT (like `__BROWSER__`) by adding them to the global scope before accessing them (`globalThis.__BROWSER__ = "chrome"`)
- **Import paths**: If you use the `@/` or `~/` path aliases, add them to your test environment
[Here's how Vitest is configured](https://github.com/wxt-dev/wxt/blob/main/src/testing/wxt-vitest-plugin.ts) for reference.
+2 -1
View File
@@ -5,7 +5,8 @@
"../src/browser.ts",
"../src/sandbox",
"../src/storage.ts",
"../src/testing"
"../src/testing",
"../src/i18n"
],
"plugin": ["typedoc-plugin-markdown", "typedoc-vitepress-theme"],
"out": "./api",
-99
View File
@@ -1,99 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { TestProject } from '../utils';
import { WxtHooks } from '~/types';
const hooks: WxtHooks = {
ready: vi.fn(),
'build:before': vi.fn(),
'build:done': vi.fn(),
'build:manifestGenerated': vi.fn(),
'entrypoints:resolved': vi.fn(),
'entrypoints:grouped': vi.fn(),
};
function expectHooksToBeCalled(called: Record<keyof WxtHooks, boolean>) {
Object.keys(hooks).forEach((key) => {
const hookName = key as keyof WxtHooks;
const times = called[hookName] ? 1 : 0;
expect(
hooks[hookName],
`Expected "${hookName}" to be called ${times} time(s)`,
).toBeCalledTimes(called[hookName] ? 1 : 0);
});
}
describe('Hooks', () => {
beforeEach(() => {
Object.values(hooks).forEach((fn) => fn.mockReset());
});
it('prepare should call hooks', async () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html', '<html></html>');
await project.prepare({ hooks });
expectHooksToBeCalled({
ready: true,
'build:before': false,
'build:done': false,
'build:manifestGenerated': false,
'entrypoints:grouped': false,
'entrypoints:resolved': true,
});
});
it('build should call hooks', async () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html', '<html></html>');
await project.build({ hooks });
expectHooksToBeCalled({
ready: true,
'build:before': true,
'build:done': true,
'build:manifestGenerated': true,
'entrypoints:grouped': true,
'entrypoints:resolved': true,
});
});
it('zip should call hooks', async () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html', '<html></html>');
await project.zip({ hooks });
expectHooksToBeCalled({
ready: true,
'build:before': true,
'build:done': true,
'build:manifestGenerated': true,
'entrypoints:grouped': true,
'entrypoints:resolved': true,
});
});
it('server.start should call hooks', async () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html', '<html></html>');
const server = await project.startServer({
hooks,
runner: {
disabled: true,
},
});
await server.stop();
expectHooksToBeCalled({
ready: true,
'build:before': true,
'build:done': true,
'build:manifestGenerated': true,
'entrypoints:grouped': true,
'entrypoints:resolved': true,
});
});
});
-165
View File
@@ -227,169 +227,4 @@ describe('Output Directory Structure', () => {
true,
);
});
it('should generate ESM background script when type=module', async () => {
const project = new TestProject();
project.addFile(
'utils/log.ts',
`export function logHello(name: string) {
console.log(\`Hello \${name}!\`);
}`,
);
project.addFile(
'entrypoints/background.ts',
`export default defineBackground({
type: "module",
main() {
logHello("background");
},
})`,
);
project.addFile(
'entrypoints/popup/index.html',
`<html>
<head>
<script type="module" src="./main.ts"></script>
</head>
</html>`,
);
project.addFile('entrypoints/popup/main.ts', `logHello('popup')`);
await project.build({
experimental: {
// Simplify the build output for comparison
includeBrowserPolyfill: false,
},
vite: () => ({
build: {
// Make output for snapshot readible
minify: false,
},
}),
});
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");
}
});
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"
);
}
} catch (err) {
logger.error("The background crashed on startup!");
throw err;
}
"
`);
});
it('should generate IIFE background script when type=undefined', async () => {
const project = new TestProject();
project.addFile(
'utils/log.ts',
`export function logHello(name: string) {
console.log(\`Hello \${name}!\`);
}`,
);
project.addFile(
'entrypoints/background.ts',
`export default defineBackground({
main() {
logHello("background");
},
})`,
);
project.addFile(
'entrypoints/popup/index.html',
`<html>
<head>
<script type="module" src="./main.ts"></script>
</head>
</html>`,
);
project.addFile('entrypoints/popup/main.ts', `logHello('popup')`);
await project.build({
experimental: {
// Simplify the build output for comparison
includeBrowserPolyfill: false,
},
vite: () => ({
build: {
// Make output for snapshot readible
minify: false,
},
}),
});
expect(await project.serializeFile('.output/chrome-mv3/background.js'))
.toMatchInlineSnapshot(`
".output/chrome-mv3/background.js
----------------------------------------
(function() {
"use strict";
function defineBackground(arg) {
if (typeof arg === "function")
return { main: arg };
return arg;
}
function logHello(name) {
console.log(\`Hello \${name}!\`);
}
const definition = defineBackground({
main() {
logHello("background");
}
});
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"
);
}
} catch (err) {
logger.error("The background crashed on startup!");
throw err;
}
})();
"
`);
});
});
+11 -16
View File
@@ -14,19 +14,16 @@ describe('TypeScript Project', () => {
----------------------------------------
// Generated by wxt
export {}
interface ImportMetaEnv {
readonly MANIFEST_VERSION: 2 | 3;
readonly BROWSER: string;
readonly CHROME: boolean;
readonly FIREFOX: boolean;
readonly SAFARI: boolean;
readonly EDGE: boolean;
readonly OPERA: boolean;
readonly COMMAND: "build" | "serve";
readonly ENTRYPOINT: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv
declare global {
const __MANIFEST_VERSION__: 2 | 3;
const __BROWSER__: string;
const __IS_CHROME__: boolean;
const __IS_FIREFOX__: boolean;
const __IS_SAFARI__: boolean;
const __IS_EDGE__: boolean;
const __IS_OPERA__: boolean;
const __COMMAND__: "build" | "serve";
const __ENTRYPOINT__: string;
}
"
`);
@@ -52,10 +49,8 @@ describe('TypeScript Project', () => {
| "/options.html"
| "/popup.html"
| "/sandbox.html"
type HtmlPublicPath = Extract<PublicPath, \`\${string}.html\`>
export interface WxtRuntime extends Runtime.Static {
getURL(path: PublicPath): string;
getURL(path: \`\${HtmlPublicPath}\${string}\`): string;
}
}
"
@@ -136,7 +131,7 @@ describe('TypeScript Project', () => {
options?: GetMessageOptions,
): string;
/**
* No message description.
*
*
* "<browser.i18n.getUiLocale()>"
*/
+5 -30
View File
@@ -2,14 +2,7 @@ import { dirname, join, relative, resolve } from 'path';
import fs from 'fs-extra';
import glob from 'fast-glob';
import { execaCommand } from 'execa';
import {
InlineConfig,
UserConfig,
build,
createServer,
prepare,
zip,
} from '../src';
import { InlineConfig, UserConfig, build } from '../src';
import { normalizePath } from '../src/core/utils/paths';
import merge from 'lodash.merge';
@@ -70,29 +63,10 @@ export class TestProject {
if (filename === 'wxt.config.ts') this.config = {};
}
async prepare(config: InlineConfig = {}) {
await this.writeProjectToDisk();
await prepare({ ...config, root: this.root });
}
/**
* Write the files to the test directory install dependencies, and build the project.
*/
async build(config: InlineConfig = {}) {
await this.writeProjectToDisk();
await build({ ...config, root: this.root });
}
async zip(config: InlineConfig = {}) {
await this.writeProjectToDisk();
await zip({ ...config, root: this.root });
}
async startServer(config: InlineConfig = {}) {
await this.writeProjectToDisk();
const server = await createServer({ ...config, root: this.root });
await server.start();
return server;
}
private async writeProjectToDisk() {
if (this.config == null) this.setConfigFileConfig();
for (const file of this.files) {
@@ -106,6 +80,7 @@ export class TestProject {
await execaCommand('pnpm --ignore-workspace i --ignore-scripts', {
cwd: this.root,
});
await build({ ...config, root: this.root });
}
/**
+16 -10
View File
@@ -1,7 +1,7 @@
{
"name": "wxt",
"type": "module",
"version": "0.16.3",
"version": "0.14.2-alpha1",
"description": "Next gen framework for developing web extensions",
"engines": {
"node": ">=18",
@@ -77,12 +77,22 @@
"default": "./dist/storage.cjs"
}
},
"./i18n": {
"import": {
"types": "./dist/i18n.d.ts",
"default": "./dist/i18n.js"
},
"require": {
"types": "./dist/i18n.d.cts",
"default": "./dist/i18n.cjs"
}
},
"./vite-builder-env": {
"types": "./dist/vite-builder-env.d.ts"
}
},
"scripts": {
"wxt": "tsx src/cli/index.ts",
"wxt": "tsx src/cli.ts",
"build": "tsx scripts/build.ts",
"format": "prettier --write .",
"format:check": "prettier --check .",
@@ -120,38 +130,34 @@
"fs-extra": "^11.1.1",
"get-port": "^7.0.0",
"giget": "^1.1.3",
"hookable": "^5.5.3",
"immer": "^10.0.3",
"is-wsl": "^3.0.0",
"jiti": "^1.21.0",
"json5": "^2.2.3",
"linkedom": "^0.16.1",
"manage-path": "^2.0.0",
"minimatch": "^9.0.3",
"natural-compare": "^1.4.0",
"normalize-path": "^3.0.0",
"ora": "^7.0.1",
"picocolors": "^1.0.0",
"prompts": "^2.4.2",
"publish-browser-extension": "^2.1.1",
"rollup-plugin-visualizer": "^5.9.2",
"unimport": "^3.4.0",
"vite": "^5.0.12",
"web-ext-run": "^0.2.0",
"vite": "^5.0.0",
"web-ext-run": "^0.1.0",
"webextension-polyfill": "^0.10.0",
"yaml": "^2.3.4",
"zip-dir": "^2.0.0"
},
"devDependencies": {
"@faker-js/faker": "^8.3.1",
"@types/fs-extra": "^11.0.4",
"@types/lodash.merge": "^4.6.9",
"@types/natural-compare": "^1.4.3",
"@types/node": "^20.10.3",
"@types/normalize-path": "^3.0.2",
"@types/prompts": "^2.4.9",
"@vitest/coverage-v8": "^1.0.1",
"execa": "^8.0.1",
"happy-dom": "^13.3.8",
"happy-dom": "^12.10.3",
"lint-staged": "^15.2.0",
"lodash.merge": "^4.6.2",
"npm-run-all": "^4.1.5",
+212 -502
View File
File diff suppressed because it is too large Load Diff
+2 -6
View File
@@ -58,6 +58,7 @@ const config: tsup.Options[] = [
index: 'src/index.ts',
testing: 'src/testing/index.ts',
storage: 'src/storage.ts',
i18n: 'src/i18n/index.ts',
},
format: ['cjs', 'esm'],
clean: true,
@@ -94,14 +95,9 @@ const config: tsup.Options[] = [
{
...preset,
entry: {
cli: 'src/cli/index.ts',
cli: 'src/cli.ts',
},
format: ['esm'],
banner: {
// Fixes dynamic require of nodejs modules. See https://github.com/wxt-dev/wxt/issues/355
// https://github.com/evanw/esbuild/issues/1921#issuecomment-1152991694
js: "import { createRequire } from 'module';const require = createRequire(import.meta.url);",
},
},
];
+51 -30
View File
@@ -1,12 +1,16 @@
import cac from 'cac';
import { version } from '~/version';
import { build, clean, createServer, initialize, prepare, zip } from '~/core';
import {
createAliasedCommand,
getArrayFromFlags,
wrapAction,
} from './cli-utils';
import consola, { LogLevels } from 'consola';
import { printHeader } from '~/core/utils/log';
import { formatDuration } from '~/core/utils/time';
// TODO: Remove. See https://github.com/wxt-dev/wxt/issues/277
process.env.VITE_CJS_IGNORE_WARNING = 'true';
const cli = cac('wxt');
cli.help();
cli.version(version);
cli.option('--debug', 'enable debug mode');
@@ -16,13 +20,6 @@ cli
.option('-c, --config <file>', 'use specified config file')
.option('-m, --mode <mode>', 'set env mode')
.option('-b, --browser <browser>', 'specify a browser')
.option(
'-e, --filter-entrypoint <entrypoint>',
'only build specific entrypoints',
{
type: [],
},
)
.option('--mv3', 'target manifest v3')
.option('--mv2', 'target manifest v2')
.action(
@@ -34,7 +31,6 @@ cli
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
configFile: flags.config,
debug: flags.debug,
filterEntrypoints: getArrayFromFlags(flags, 'filterEntrypoint'),
});
await server.start();
return { isOngoing: true };
@@ -47,13 +43,6 @@ cli
.option('-c, --config <file>', 'use specified config file')
.option('-m, --mode <mode>', 'set env mode')
.option('-b, --browser <browser>', 'specify a browser')
.option(
'-e, --filter-entrypoint <entrypoint>',
'only build specific entrypoints',
{
type: [],
},
)
.option('--mv3', 'target manifest v3')
.option('--mv2', 'target manifest v2')
.option('--analyze', 'visualize extension bundle')
@@ -66,8 +55,9 @@ cli
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
configFile: flags.config,
debug: flags.debug,
analysis: flags.analyze ? { enabled: true } : undefined,
filterEntrypoints: getArrayFromFlags(flags, 'filterEntrypoint'),
analysis: {
enabled: flags.analyze,
},
});
}),
);
@@ -135,12 +125,43 @@ cli
),
);
// SUBMIT
createAliasedCommand(
cli,
'submit',
'publish-extension',
'https://www.npmjs.com/publish-browser-extension',
);
cli.parse();
export default cli;
/**
* Wrap an action handler to add a timer, error handling, and maybe enable debug mode.
*/
function wrapAction(
cb: (
...args: any[]
) => void | { isOngoing?: boolean } | Promise<void | { isOngoing?: boolean }>,
options?: {
disableFinishedLog?: boolean;
},
) {
return async (...args: any[]) => {
// Enable consola's debug mode globally at the start of all commands when the `--debug` flag is
// passed
const isDebug = !!args.find((arg) => arg?.debug);
if (isDebug) {
consola.level = LogLevels.debug;
}
const startTime = Date.now();
try {
printHeader();
const status = await cb(...args);
if (!status?.isOngoing && !options?.disableFinishedLog)
consola.success(
`Finished in ${formatDuration(Date.now() - startTime)}`,
);
} catch (err) {
consola.fail(
`Command failed after ${formatDuration(Date.now() - startTime)}`,
);
consola.error(err);
process.exit(1);
}
};
}
-380
View File
@@ -1,380 +0,0 @@
import { describe, it, vi, beforeEach, expect } from 'vitest';
import { build } from '~/core/build';
import { createServer } from '~/core/create-server';
import { zip } from '~/core/zip';
import { prepare } from '~/core/prepare';
import { clean } from '~/core/clean';
import { initialize } from '~/core/initialize';
import { mock } from 'vitest-mock-extended';
import consola from 'consola';
vi.mock('~/core/build');
const buildMock = vi.mocked(build);
vi.mock('~/core/create-server');
const createServerMock = vi.mocked(createServer);
vi.mock('~/core/zip');
const zipMock = vi.mocked(zip);
vi.mock('~/core/prepare');
const prepareMock = vi.mocked(prepare);
vi.mock('~/core/clean');
const cleanMock = vi.mocked(clean);
vi.mock('~/core/initialize');
const initializeMock = vi.mocked(initialize);
consola.wrapConsole();
const ogArgv = process.argv;
function mockArgv(...args: string[]) {
process.argv = ['/bin/node', 'bin/wxt.mjs', ...args];
}
async function importCli() {
await import('~/cli');
}
describe('CLI', () => {
beforeEach(() => {
vi.resetModules();
process.argv = ogArgv;
createServerMock.mockResolvedValue(mock());
});
describe('dev', () => {
it('should not pass any config when no flags are passed', async () => {
mockArgv();
await importCli();
expect(createServerMock).toBeCalledWith({});
});
it('should respect passing a custom root', async () => {
mockArgv('path/to/root');
await importCli();
expect(createServerMock).toBeCalledWith({
root: 'path/to/root',
});
});
it('should respect a custom config file', async () => {
mockArgv('-c', './path/to/config.ts');
await importCli();
expect(createServerMock).toBeCalledWith({
configFile: './path/to/config.ts',
});
});
it('should respect passing a custom mode', async () => {
mockArgv('-m', 'development');
await importCli();
expect(createServerMock).toBeCalledWith({
mode: 'development',
});
});
it('should respect passing a custom browser', async () => {
mockArgv('-b', 'firefox');
await importCli();
expect(createServerMock).toBeCalledWith({
browser: 'firefox',
});
});
it('should pass correct filtered entrypoints', async () => {
mockArgv('-e', 'popup', '-e', 'options');
await importCli();
expect(createServerMock).toBeCalledWith({
filterEntrypoints: ['popup', 'options'],
});
});
it('should respect passing --mv2', async () => {
mockArgv('--mv2');
await importCli();
expect(createServerMock).toBeCalledWith({
manifestVersion: 2,
});
});
it('should respect passing --mv3', async () => {
mockArgv('--mv3');
await importCli();
expect(createServerMock).toBeCalledWith({
manifestVersion: 3,
});
});
it('should respect passing --debug', async () => {
mockArgv('--debug');
await importCli();
expect(createServerMock).toBeCalledWith({
debug: true,
});
});
});
describe('build', () => {
it('should not pass any config when no flags are passed', async () => {
mockArgv('build');
await importCli();
expect(buildMock).toBeCalledWith({});
});
it('should respect passing a custom root', async () => {
mockArgv('build', 'path/to/root');
await importCli();
expect(buildMock).toBeCalledWith({
root: 'path/to/root',
});
});
it('should respect a custom config file', async () => {
mockArgv('build', '-c', './path/to/config.ts');
await importCli();
expect(buildMock).toBeCalledWith({
configFile: './path/to/config.ts',
});
});
it('should respect passing a custom mode', async () => {
mockArgv('build', '-m', 'development');
await importCli();
expect(buildMock).toBeCalledWith({
mode: 'development',
});
});
it('should respect passing a custom browser', async () => {
mockArgv('build', '-b', 'firefox');
await importCli();
expect(buildMock).toBeCalledWith({
browser: 'firefox',
});
});
it('should pass correct filtered entrypoints', async () => {
mockArgv('build', '-e', 'popup', '-e', 'options');
await importCli();
expect(buildMock).toBeCalledWith({
filterEntrypoints: ['popup', 'options'],
});
});
it('should respect passing --mv2', async () => {
mockArgv('build', '--mv2');
await importCli();
expect(buildMock).toBeCalledWith({
manifestVersion: 2,
});
});
it('should respect passing --mv3', async () => {
mockArgv('build', '--mv3');
await importCli();
expect(buildMock).toBeCalledWith({
manifestVersion: 3,
});
});
it('should include analysis in the build', async () => {
mockArgv('build', '--analyze');
await importCli();
expect(buildMock).toBeCalledWith({
analysis: {
enabled: true,
},
});
});
it('should respect passing --debug', async () => {
mockArgv('build', '--debug');
await importCli();
expect(buildMock).toBeCalledWith({
debug: true,
});
});
});
describe('zip', () => {
it('should not pass any config when no flags are passed', async () => {
mockArgv('zip');
await importCli();
expect(zipMock).toBeCalledWith({});
});
it('should respect passing a custom root', async () => {
mockArgv('zip', 'path/to/root');
await importCli();
expect(zipMock).toBeCalledWith({
root: 'path/to/root',
});
});
it('should respect a custom config file', async () => {
mockArgv('zip', '-c', './path/to/config.ts');
await importCli();
expect(zipMock).toBeCalledWith({
configFile: './path/to/config.ts',
});
});
it('should respect passing a custom mode', async () => {
mockArgv('zip', '-m', 'development');
await importCli();
expect(zipMock).toBeCalledWith({
mode: 'development',
});
});
it('should respect passing a custom browser', async () => {
mockArgv('zip', '-b', 'firefox');
await importCli();
expect(zipMock).toBeCalledWith({
browser: 'firefox',
});
});
it('should respect passing --mv2', async () => {
mockArgv('zip', '--mv2');
await importCli();
expect(zipMock).toBeCalledWith({
manifestVersion: 2,
});
});
it('should respect passing --mv3', async () => {
mockArgv('zip', '--mv3');
await importCli();
expect(zipMock).toBeCalledWith({
manifestVersion: 3,
});
});
it('should respect passing --debug', async () => {
mockArgv('zip', '--debug');
await importCli();
expect(zipMock).toBeCalledWith({
debug: true,
});
});
});
describe('prepare', () => {
it('should not pass any config when no flags are passed', async () => {
mockArgv('prepare');
await importCli();
expect(prepareMock).toBeCalledWith({});
});
it('should respect passing a custom root', async () => {
mockArgv('prepare', 'path/to/root');
await importCli();
expect(prepareMock).toBeCalledWith({
root: 'path/to/root',
});
});
it('should respect a custom config file', async () => {
mockArgv('prepare', '-c', './path/to/config.ts');
await importCli();
expect(prepareMock).toBeCalledWith({
configFile: './path/to/config.ts',
});
});
it('should respect passing --debug', async () => {
mockArgv('prepare', '--debug');
await importCli();
expect(prepareMock).toBeCalledWith({
debug: true,
});
});
});
describe('clean', () => {
it('should not pass any config when no flags are passed', async () => {
mockArgv('clean');
await importCli();
expect(cleanMock).toBeCalledWith(undefined);
});
it('should respect passing a custom root', async () => {
mockArgv('clean', 'path/to/root');
await importCli();
expect(cleanMock).toBeCalledWith('path/to/root');
});
});
describe('init', () => {
it('should not pass any options when no flags are passed', async () => {
mockArgv('init');
await importCli();
expect(initializeMock).toBeCalledWith({});
});
it('should respect the provided folder', async () => {
mockArgv('init', 'path/to/folder');
await importCli();
expect(initializeMock).toBeCalledWith({
directory: 'path/to/folder',
});
});
it('should respect passing --template', async () => {
mockArgv('init', '-t', 'vue');
await importCli();
expect(initializeMock).toBeCalledWith({
template: 'vue',
});
});
it('should respect passing --pm', async () => {
mockArgv('init', '--pm', 'pnpm');
await importCli();
expect(initializeMock).toBeCalledWith({
packageManager: 'pnpm',
});
});
});
});
-94
View File
@@ -1,94 +0,0 @@
import { CAC, Command } from 'cac';
import consola, { LogLevels } from 'consola';
import { exec } from '~/core/utils/exec';
import { printHeader } from '~/core/utils/log';
import { formatDuration } from '~/core/utils/time';
import { ValidationError } from '~/core/utils/validation';
import { registerWxt } from '~/core/wxt';
/**
* Wrap an action handler to add a timer, error handling, and maybe enable debug mode.
*/
export function wrapAction(
cb: (
...args: any[]
) => void | { isOngoing?: boolean } | Promise<void | { isOngoing?: boolean }>,
options?: {
disableFinishedLog?: boolean;
},
) {
return async (...args: any[]) => {
// Enable consola's debug mode globally at the start of all commands when the `--debug` flag is
// passed
const isDebug = !!args.find((arg) => arg?.debug);
if (isDebug) {
consola.level = LogLevels.debug;
}
const startTime = Date.now();
try {
printHeader();
const status = await cb(...args);
if (!status?.isOngoing && !options?.disableFinishedLog)
consola.success(
`Finished in ${formatDuration(Date.now() - startTime)}`,
);
} catch (err) {
consola.fail(
`Command failed after ${formatDuration(Date.now() - startTime)}`,
);
if (err instanceof ValidationError) {
// Don't log these errors, they've already been logged
} else {
consola.error(err);
}
process.exit(1);
}
};
}
/**
* Array flags, when not passed, are either `undefined` or `[undefined]`. This function filters out
* the
*/
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[];
return result.length ? result : undefined;
}
const aliasCommandNames = new Set<string>();
export function createAliasedCommand(
base: CAC,
name: string,
alias: string,
docsUrl: string,
) {
const aliasedCommand = base
.command(name, `Alias for ${alias} (${docsUrl})`)
.allowUnknownOptions()
.action(async () => {
try {
await registerWxt('build');
const args = process.argv.slice(
process.argv.indexOf(aliasedCommand.name) + 1,
);
await exec(alias, args, {
stdio: 'inherit',
});
} catch {
// Let the other aliased CLI log errors, just exit
process.exit(1);
}
});
aliasCommandNames.add(aliasedCommand.name);
}
export function isAliasedCommand(command: Command | undefined): boolean {
return !!command && aliasCommandNames.has(command.name);
}
-19
View File
@@ -1,19 +0,0 @@
import cli from './commands';
import { version } from '~/version';
import { isAliasedCommand } from './cli-utils';
// TODO: Remove. See https://github.com/wxt-dev/wxt/issues/277
process.env.VITE_CJS_IGNORE_WARNING = 'true';
// Grab the command that we're trying to run
cli.parse(process.argv, { run: false });
// If it's not an alias, add the help and version options, then parse again
if (!isAliasedCommand(cli.matchedCommand)) {
cli.help();
cli.version(version);
cli.parse(process.argv, { run: false });
}
// Run the alias or command
await cli.runMatchedCommand();
+3 -5
View File
@@ -15,12 +15,10 @@ export class WxtLocationChangeEvent extends Event {
* Returns an event name unique to the extension and content script that's running.
*/
export function getUniqueEventName(eventName: string): string {
// During the build process, import.meta.env is not defined when importing
// entrypoints to get their metadata.
// During the build process, __ENTRYPOINT__ is not defined when importing entrypoints to get their
// metadata.
const entrypointName =
typeof import.meta.env === 'undefined'
? 'build'
: import.meta.env.ENTRYPOINT;
typeof __ENTRYPOINT__ === 'undefined' ? 'build' : __ENTRYPOINT__;
return `${browser.runtime.id}:${entrypointName}:${eventName}`;
}
+1 -3
View File
@@ -236,9 +236,7 @@ function mountUi(
* Load the CSS for the current entrypoint.
*/
async function loadCss(): Promise<string> {
const url = browser.runtime.getURL(
`/content-scripts/${import.meta.env.ENTRYPOINT}.css`,
);
const url = browser.runtime.getURL(`/content-scripts/${__ENTRYPOINT__}.css`);
try {
const res = await fetch(url);
return await res.text();
+3 -5
View File
@@ -1,6 +1,5 @@
import { BuildOutput, InlineConfig } from '~/types';
import { internalBuild } from './utils/building';
import { registerWxt } from './wxt';
import { getInternalConfig, internalBuild } from './utils/building';
/**
* Bundles the extension for production. Returns a promise of the build result. Discovers the `wxt.config.ts` file in
@@ -16,7 +15,6 @@ import { registerWxt } from './wxt';
* })
*/
export async function build(config?: InlineConfig): Promise<BuildOutput> {
await registerWxt('build', config);
return await internalBuild();
const internalConfig = await getInternalConfig(config ?? {}, 'build');
return await internalBuild(internalConfig);
}
+22 -39
View File
@@ -3,7 +3,7 @@ import {
BuildStepOutput,
Entrypoint,
InlineConfig,
ResolvedConfig,
InternalConfig,
UserConfig,
VirtualEntrypointType,
WxtBuilder,
@@ -15,7 +15,7 @@ import { getEntrypointBundlePath } from '~/core/utils/entrypoints';
export async function createViteBuilder(
inlineConfig: InlineConfig,
userConfig: UserConfig,
wxtConfig: Omit<ResolvedConfig, 'builder'>,
wxtConfig: Omit<InternalConfig, 'builder'>,
): Promise<WxtBuilder> {
const vite = await import('vite');
@@ -44,10 +44,6 @@ export async function createViteBuilder(
if (config.build.minify == null && wxtConfig.command === 'serve') {
config.build.minify = false;
}
// Enable inline sourcemaps for the dev command (so content scripts have sourcemaps)
if (config.build.sourcemap == null && wxtConfig.command === 'serve') {
config.build.sourcemap = 'inline';
}
config.plugins ??= [];
config.plugins.push(
@@ -63,7 +59,6 @@ export async function createViteBuilder(
wxtPlugins.noopBackground(),
wxtPlugins.globals(wxtConfig),
wxtPlugins.excludeBrowserPolyfill(wxtConfig),
wxtPlugins.defineImportMeta(),
);
if (wxtConfig.analysis.enabled) {
config.plugins.push(wxtPlugins.bundleAnalysis());
@@ -76,7 +71,23 @@ export async function createViteBuilder(
* Return the basic config for building an entrypoint in [lib mode](https://vitejs.dev/guide/build.html#library-mode).
*/
const getLibModeConfig = (entrypoint: Entrypoint): vite.InlineConfig => {
const entry = getRollupEntry(entrypoint);
let virtualEntrypointType: VirtualEntrypointType | undefined;
switch (entrypoint.type) {
case 'background':
case 'unlisted-script':
virtualEntrypointType = entrypoint.type;
break;
case 'content-script':
virtualEntrypointType =
entrypoint.options.world === 'MAIN'
? 'content-script-main-world'
: 'content-script-isolated-world';
break;
}
const entry = virtualEntrypointType
? `virtual:wxt-${virtualEntrypointType}?${entrypoint.inputPath}`
: entrypoint.inputPath;
const plugins: NonNullable<vite.UserConfig['plugins']> = [
wxtPlugins.entrypointGroupGlobals(entrypoint),
];
@@ -142,16 +153,14 @@ export async function createViteBuilder(
build: {
rollupOptions: {
input: entrypoints.reduce<Record<string, string>>((input, entry) => {
input[entry.name] = getRollupEntry(entry);
input[entry.name] = entry.inputPath;
return input;
}, {}),
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',
// Include a hash to prevent conflicts
entryFileNames: 'chunks/[name]-[hash].js',
// We can't control the "name", so we need a hash to prevent conflicts
assetFileNames: 'assets/[name]-[hash].[ext]',
},
@@ -221,9 +230,6 @@ export async function createViteBuilder(
async listen() {
await viteServer.listen(info.port);
},
async close() {
await viteServer.close();
},
transformHtml(...args) {
return viteServer.transformIndexHtml(...args);
},
@@ -250,26 +256,3 @@ function getBuildOutputChunks(
if (Array.isArray(result)) return result.flatMap(({ output }) => output);
return result.output;
}
/**
* Returns the input module ID (virtual or real file) for an entrypoint. The returned string should
* be passed as an input to rollup.
*/
function getRollupEntry(entrypoint: Entrypoint): string {
let virtualEntrypointType: VirtualEntrypointType | undefined;
switch (entrypoint.type) {
case 'background':
case 'unlisted-script':
virtualEntrypointType = entrypoint.type;
break;
case 'content-script':
virtualEntrypointType =
entrypoint.options.world === 'MAIN'
? 'content-script-main-world'
: 'content-script-isolated-world';
break;
}
return virtualEntrypointType
? `virtual:wxt-${virtualEntrypointType}?${entrypoint.inputPath}`
: entrypoint.inputPath;
}
@@ -1,64 +0,0 @@
import { describe, expect, it } from 'vitest';
import { Window } from 'happy-dom';
import { pointToDevServer } from '../devHtmlPrerender';
import {
fakeDevServer,
fakeResolvedConfig,
} from '~/core/utils/testing/fake-objects';
import { normalizePath } from '~/core/utils/paths';
import { resolve } from 'node:path';
describe('Dev HTML Prerender Plugin', () => {
describe('pointToDevServer', () => {
it.each([
// File paths should be resolved
['style.css', 'http://localhost:5173/entrypoints/popup/style.css'],
['./style.css', 'http://localhost:5173/entrypoints/popup/style.css'],
['../style.css', 'http://localhost:5173/entrypoints/style.css'],
['~/assets/style.css', 'http://localhost:5173/assets/style.css'],
['~~/assets/style.css', 'http://localhost:5173/assets/style.css'],
['~local/style.css', 'http://localhost:5173/style.css'],
['~absolute/style.css', 'http://localhost:5173/assets/style.css'],
['~file', 'http://localhost:5173/example.css'],
// Absolute paths are loaded with the `/@fs/` base path
[
'~outside/test.css',
`http://localhost:5173/@fs${
process.platform === 'win32'
? '/' + normalizePath(resolve('/some/non-root/test.css')) // "/D:/some/non-root/test.css"
: '/some/non-root/test.css'
}`,
],
// URLs should not be changed
['https://example.com/style.css', 'https://example.com/style.css'],
])('should transform "%s" into "%s"', (input, expected) => {
const { document } = new Window({
url: 'http://localhost',
});
const root = '/some/root';
const config = fakeResolvedConfig({
root,
alias: {
'~local': '.',
'~absolute': `${root}/assets`,
'~file': `${root}/example.css`,
'~outside': `${root}/../non-root`,
'~~': root,
'~': root,
},
});
const server = fakeDevServer({
hostname: 'localhost',
port: 5173,
origin: 'http://localhost:5173',
});
const id = root + '/entrypoints/popup/index.html';
document.head.innerHTML = `<link rel="stylesheet" href="${input}" />`;
pointToDevServer(config, server, id, document as any, 'link', 'href');
const actual = document.querySelector('link')!;
expect(actual.getAttribute('href')).toBe(expected);
});
});
});
@@ -1,5 +1,5 @@
import type * as vite from 'vite';
import { Entrypoint, ResolvedConfig } from '~/types';
import { Entrypoint, InternalConfig } from '~/types';
import { getEntrypointBundlePath } from '~/core/utils/entrypoints';
/**
@@ -14,7 +14,7 @@ import { getEntrypointBundlePath } from '~/core/utils/entrypoints';
*/
export function cssEntrypoints(
entrypoint: Entrypoint,
config: Omit<ResolvedConfig, 'builder'>,
config: Omit<InternalConfig, 'builder'>,
): vite.Plugin {
return {
name: 'wxt:css-entrypoint',
@@ -1,19 +0,0 @@
/**
* Overrides definitions for `import.meta.*`
*
* - `import.meta.url`: Without this, background service workers crash trying to access
* `document.location`, see https://github.com/wxt-dev/wxt/issues/392
*/
export function defineImportMeta() {
return {
name: 'wxt:define',
config() {
return {
define: {
// This works for all extension contexts, including background service worker
'import.meta.url': 'self.location.href',
},
};
},
};
}
@@ -1,9 +1,8 @@
import type * as vite from 'vite';
import { ResolvedConfig, WxtDevServer } from '~/types';
import { InternalConfig } from '~/types';
import { getEntrypointName } from '~/core/utils/entrypoints';
import { parseHTML } from 'linkedom';
import { dirname, relative, resolve } from 'node:path';
import { normalizePath } from '~/core/utils/paths';
import { dirname, isAbsolute, relative, resolve } from 'node:path';
// Cache the preamble script for all devHtmlPrerender plugins, not just one
let reactRefreshPreamble = '';
@@ -12,7 +11,7 @@ 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: Omit<InternalConfig, 'builder'>,
): vite.PluginOption {
const htmlReloadId = '@wxt/reload-html';
const resolvedHtmlReloadId = resolve(
@@ -48,10 +47,25 @@ export function devHtmlPrerender(
const { document } = parseHTML(code);
const _pointToDevServer = (querySelector: string, attr: string) =>
pointToDevServer(config, server, id, document, querySelector, attr);
_pointToDevServer('script[type=module]', 'src');
_pointToDevServer('link[rel=stylesheet]', 'href');
const pointToDevServer = (
querySelector: string,
attr: string,
): void => {
document.querySelectorAll(querySelector).forEach((element) => {
const src = element.getAttribute(attr);
if (!src) return;
if (isAbsolute(src)) {
element.setAttribute(attr, server.origin + src);
} else if (src.startsWith('.')) {
const abs = resolve(dirname(id), src);
const pathname = relative(config.root, abs);
element.setAttribute(attr, `${server.origin}/${pathname}`);
}
});
};
pointToDevServer('script[type=module]', 'src');
pointToDevServer('link[rel=stylesheet]', 'href');
// Add a script to add page reloading
const reloader = document.createElement('script');
@@ -132,63 +146,3 @@ export function devHtmlPrerender(
},
];
}
export function pointToDevServer(
config: Omit<ResolvedConfig, 'builder'>,
server: WxtDevServer,
id: string,
document: Document,
querySelector: string,
attr: string,
) {
document.querySelectorAll(querySelector).forEach((element) => {
const src = element.getAttribute(attr);
if (!src || isUrl(src)) return;
let resolvedAbsolutePath: string | undefined;
// Check if src uses a project alias
const matchingAlias = Object.entries(config.alias).find(([key]) =>
src.startsWith(key),
);
if (matchingAlias) {
// Matches a import alias
const [alias, replacement] = matchingAlias;
resolvedAbsolutePath = resolve(
config.root,
src.replace(alias, replacement),
);
} else {
// Some file path relative to the HTML file
resolvedAbsolutePath = resolve(dirname(id), src);
}
// Apply the final file path
if (resolvedAbsolutePath) {
const relativePath = normalizePath(
relative(config.root, resolvedAbsolutePath),
);
if (relativePath.startsWith('.')) {
// Outside the config.root directory, serve the absolute path
let path = normalizePath(resolvedAbsolutePath);
// Add "/" to start of windows paths ("D:/some/path" -> "/D:/some/path")
if (!path.startsWith('/')) path = '/' + path;
element.setAttribute(attr, `${server.origin}/@fs${path}`);
} else {
// Inside the project, use relative path
const url = new URL(relativePath, server.origin);
element.setAttribute(attr, url.href);
}
}
});
}
function isUrl(str: string): boolean {
try {
new URL(str);
return true;
} catch {
return false;
}
}
@@ -1,11 +1,11 @@
import { Plugin } from 'vite';
import { ResolvedConfig } from '~/types';
import { InternalConfig } 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: Omit<InternalConfig, 'builder'>,
): Plugin {
return {
name: 'wxt:dev-server-globals',
+2 -2
View File
@@ -1,5 +1,5 @@
import { Plugin } from 'vite';
import { ResolvedConfig } from '~/types';
import { InternalConfig } from '~/types';
import { fetchCached } from '~/core/utils/network';
/**
@@ -9,7 +9,7 @@ import { fetchCached } from '~/core/utils/network';
* @example
* import "url:https://google-tagmanager.com/gtag?id=XYZ";
*/
export function download(config: Omit<ResolvedConfig, 'builder'>): Plugin {
export function download(config: Omit<InternalConfig, 'builder'>): Plugin {
return {
name: 'wxt:download',
resolveId(id) {
@@ -14,7 +14,7 @@ export function entrypointGroupGlobals(
const define: vite.InlineConfig['define'] = {};
let name = Array.isArray(entrypointGroup) ? 'html' : entrypointGroup.name;
for (const global of getEntrypointGlobals(name)) {
define[`import.meta.env.${global.name}`] = JSON.stringify(global.value);
define[global.name] = JSON.stringify(global.value);
}
return {
define,
@@ -1,4 +1,4 @@
import { ResolvedConfig } from '~/types';
import { InternalConfig } from '~/types';
import type * as vite from 'vite';
/**
@@ -7,7 +7,7 @@ import type * as vite from 'vite';
* virtual module.
*/
export function excludeBrowserPolyfill(
config: Omit<ResolvedConfig, 'builder'>,
config: Omit<InternalConfig, 'builder'>,
): vite.Plugin {
const virtualId = 'virtual:wxt-webextension-polyfill-disabled';
+3 -3
View File
@@ -1,16 +1,16 @@
import type * as vite from 'vite';
import { ResolvedConfig } from '~/types';
import { InternalConfig } from '~/types';
import { getGlobals } from '~/core/utils/globals';
export function globals(
config: Omit<ResolvedConfig, 'builder'>,
config: Omit<InternalConfig, 'builder'>,
): vite.PluginOption {
return {
name: 'wxt:globals',
config() {
const define: vite.InlineConfig['define'] = {};
for (const global of getGlobals(config)) {
define[`import.meta.env.${global.name}`] = JSON.stringify(global.value);
define[global.name] = JSON.stringify(global.value);
}
return {
define,
-1
View File
@@ -12,4 +12,3 @@ export * from './globals';
export * from './webextensionPolyfillMock';
export * from './excludeBrowserPolyfill';
export * from './entrypointGroupGlobals';
export * from './defineImportMeta';
@@ -1,5 +1,5 @@
import type * as vite from 'vite';
import { Entrypoint, ResolvedConfig } from '~/types';
import { Entrypoint, InternalConfig } from '~/types';
import { dirname, extname, resolve, join } from 'node:path';
import { getEntrypointBundlePath } from '~/core/utils/entrypoints';
import fs, { ensureDir } from 'fs-extra';
@@ -20,7 +20,7 @@ import { normalizePath } from '~/core/utils/paths';
*/
export function multipageMove(
entrypoints: Entrypoint[],
config: Omit<ResolvedConfig, 'builder'>,
config: Omit<InternalConfig, 'builder'>,
): vite.Plugin {
return {
name: 'wxt:multipage-move',
@@ -1,8 +1,8 @@
import { ResolvedConfig } from '~/types';
import { InternalConfig } from '~/types';
import type * as vite from 'vite';
export function tsconfigPaths(
config: Omit<ResolvedConfig, 'builder'>,
config: Omit<InternalConfig, 'builder'>,
): vite.Plugin {
return {
name: 'wxt:aliases',
+3 -7
View File
@@ -1,5 +1,5 @@
import { createUnimport } from 'unimport';
import { ResolvedConfig } from '~/types';
import { InternalConfig } from '~/types';
import { getUnimportOptions } from '~/core/utils/unimport';
import type * as vite from 'vite';
import { extname } from 'path';
@@ -17,7 +17,7 @@ const ENABLED_EXTENSIONS = new Set([
* Inject any global imports defined by unimport
*/
export function unimport(
config: Omit<ResolvedConfig, 'builder'>,
config: Omit<InternalConfig, 'builder'>,
): vite.PluginOption {
const options = getUnimportOptions(config);
if (options === false) return [];
@@ -36,11 +36,7 @@ export function unimport(
// Don't transform non-js files
if (!ENABLED_EXTENSIONS.has(extname(id))) return;
const injected = await unimport.injectImports(code, id);
return {
code: injected.code,
map: injected.s.generateMap({ hires: 'boundary', source: id }),
};
return unimport.injectImports(code, id);
},
};
}
@@ -1,5 +1,5 @@
import { Plugin } from 'vite';
import { ResolvedConfig, VirtualEntrypointType } from '~/types';
import { InternalConfig, VirtualEntrypointType } from '~/types';
import fs from 'fs-extra';
import { resolve } from 'path';
import { normalizePath } from '~/core/utils/paths';
@@ -9,7 +9,7 @@ import { normalizePath } from '~/core/utils/paths';
*/
export function virtualEntrypoint(
type: VirtualEntrypointType,
config: Omit<ResolvedConfig, 'builder'>,
config: Omit<InternalConfig, 'builder'>,
): Plugin {
const virtualId = `virtual:wxt-${type}?`;
const resolvedVirtualId = `\0${virtualId}`;
@@ -1,6 +1,6 @@
import path from 'node:path';
import type * as vite from 'vite';
import { ResolvedConfig } from '~/types';
import { InternalConfig } from '~/types';
/**
* Mock `webextension-polyfill` by inlining all dependencies that import it and adding a custom
@@ -16,7 +16,7 @@ import { ResolvedConfig } from '~/types';
* `npm list` and inline them automatically.
*/
export function webextensionPolyfillMock(
config: Omit<ResolvedConfig, 'builder'>,
config: Omit<InternalConfig, 'builder'>,
): vite.PluginOption {
return {
name: 'wxt:testing-inline-deps',
+72 -114
View File
@@ -2,11 +2,13 @@ import {
BuildStepOutput,
EntrypointGroup,
InlineConfig,
InternalConfig,
ServerInfo,
WxtDevServer,
} from '~/types';
import {
getEntrypointBundlePath,
getEntrypointOutputFile,
resolvePerBrowserOption,
} from '~/core/utils/entrypoints';
import {
@@ -15,6 +17,7 @@ import {
} from '~/core/utils/manifest';
import {
internalBuild,
getInternalConfig,
detectDevChanges,
rebuild,
findEntrypoints,
@@ -24,7 +27,6 @@ import { consola } from 'consola';
import { Mutex } from 'async-mutex';
import pc from 'picocolors';
import { relative } from 'node:path';
import { registerWxt, wxt } from './wxt';
/**
* Creates a dev server and pre-builds all the files that need to exist before loading the extension.
@@ -47,46 +49,22 @@ export async function createServer(
origin,
};
const buildAndOpenBrowser = async () => {
// Build after starting the dev server so it can be used to transform HTML files
server.currentOutput = await internalBuild();
// Open browser after everything is ready to go.
await runner.openBrowser();
};
/**
* Stops the previous runner, grabs the latest config, and recreates the runner.
*/
const closeAndRecreateRunner = async () => {
await runner.closeBrowser();
await wxt.reloadConfig();
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,
watcher: undefined as any, // Filled out later down below
ws: undefined as any, // Filled out later down below
currentOutput: undefined as any, // Filled out later down below
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();
config.logger.success(`Started dev server @ ${serverInfo.origin}`);
// Build after starting the dev server so it can be used to transform HTML files
server.currentOutput = await internalBuild(config);
// Open browser after everything is ready to go.
await runner.openBrowser(config);
},
transformHtml(url, html, originalUrl) {
return builderServer.transformHtml(url, html, originalUrl);
@@ -100,28 +78,34 @@ export async function createServer(
reloadExtension() {
server.ws.send('wxt:reload-extension');
},
async restartBrowser() {
await closeAndRecreateRunner();
await runner.openBrowser();
},
};
await registerWxt('serve', inlineConfig, server);
const getLatestConfig = () =>
getInternalConfig(inlineConfig ?? {}, 'serve', server);
let config = await getLatestConfig();
let [runner, builderServer] = await Promise.all([
createExtensionRunner(),
wxt.config.builder.createServer(server),
const [runner, builderServer] = await Promise.all([
createExtensionRunner(config),
config.builder.createServer(server),
]);
server.watcher = builderServer.watcher;
server.ws = builderServer.ws;
// Register content scripts for the first time after the background starts up since they're not
// listed in the manifest
server.ws.on('wxt:background-initialized', () => {
if (server.currentOutput == null) return;
reloadContentScripts(server.currentOutput.steps, server);
reloadContentScripts(server.currentOutput.steps, config, server);
});
// Listen for file changes and reload different parts of the extension accordingly
const reloadOnChange = createFileReloader(server);
const reloadOnChange = createFileReloader({
server,
getLatestConfig,
updateConfig(newConfig) {
config = newConfig;
},
});
server.watcher.on('all', reloadOnChange);
return server;
@@ -136,50 +120,49 @@ async function getPort(): Promise<number> {
* Returns a function responsible for reloading different parts of the extension when a file
* changes.
*/
function createFileReloader(server: WxtDevServer) {
function createFileReloader(options: {
server: WxtDevServer;
getLatestConfig: () => Promise<InternalConfig>;
updateConfig: (config: InternalConfig) => void;
}) {
const { server, getLatestConfig, updateConfig } = options;
const fileChangedMutex = new Mutex();
const changeQueue: Array<[string, string]> = [];
return async (event: string, path: string) => {
await wxt.reloadConfig();
const config = await getLatestConfig();
updateConfig(config);
// Here, "path" is a non-normalized path (ie: C:\\users\\... instead of C:/users/...)
if (path.startsWith(wxt.config.outBaseDir)) return;
if (path.startsWith(config.outBaseDir)) return;
changeQueue.push([event, path]);
await fileChangedMutex.runExclusive(async () => {
if (server.currentOutput == null) return;
const fileChanges = changeQueue
.splice(0, changeQueue.length)
.map(([_, file]) => file);
const fileChanges = changeQueue.splice(0, changeQueue.length);
if (fileChanges.length === 0) return;
const changes = detectDevChanges(fileChanges, server.currentOutput);
if (changes.type === 'no-change') return;
if (changes.type === 'full-restart') {
wxt.logger.info('Config changed, restarting server...');
server.restart();
return;
}
if (changes.type === 'browser-restart') {
wxt.logger.info('Runner config changed, restarting browser...');
server.restartBrowser();
return;
}
// Log the entrypoints that were effected
wxt.logger.info(
`Changed: ${Array.from(new Set(fileChanges))
.map((file) => pc.dim(relative(wxt.config.root, file)))
config.logger.info(
`Changed: ${Array.from(new Set(fileChanges.map((change) => change[1])))
.map((file) => pc.dim(relative(config.root, file)))
.join(', ')}`,
);
const rebuiltNames = changes.rebuildGroups
.flat()
.map((entry) => {
return pc.cyan(
relative(config.outDir, getEntrypointOutputFile(entry, '')),
);
})
.join(pc.dim(', '));
// Rebuild entrypoints on change
const allEntrypoints = await findEntrypoints();
const allEntrypoints = await findEntrypoints(config);
const { output: newOutput } = await rebuild(
config,
allEntrypoints,
// TODO: this excludes new entrypoints, so they're not built until the dev command is restarted
changes.rebuildGroups,
@@ -191,23 +174,15 @@ function createFileReloader(server: WxtDevServer) {
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)}`);
reloadHtmlPages(changes.rebuildGroups, server, config);
break;
case 'content-script-reload':
reloadContentScripts(changes.changedSteps, server);
const rebuiltNames = changes.rebuildGroups
.flat()
.map((entry) => entry.name);
consola.success(`Reloaded: ${getFilenameList(rebuiltNames)}`);
reloadContentScripts(changes.changedSteps, config, server);
break;
}
consola.success(`Reloaded: ${rebuiltNames}`);
});
};
}
@@ -215,34 +190,33 @@ function createFileReloader(server: WxtDevServer) {
/**
* From the server, tell the client to reload content scripts from the provided build step outputs.
*/
function reloadContentScripts(steps: BuildStepOutput[], server: WxtDevServer) {
if (wxt.config.manifestVersion === 3) {
function reloadContentScripts(
steps: BuildStepOutput[],
config: InternalConfig,
server: WxtDevServer,
) {
if (config.manifestVersion === 3) {
steps.forEach((step) => {
if (server.currentOutput == null) return;
const entry = step.entrypoints;
if (Array.isArray(entry) || entry.type !== 'content-script') return;
const js = [getEntrypointBundlePath(entry, wxt.config.outDir, '.js')];
const js = [getEntrypointBundlePath(entry, config.outDir, '.js')];
const cssMap = getContentScriptsCssMap(server.currentOutput, [entry]);
const css = getContentScriptCssFiles([entry], cssMap);
server.reloadContentScript({
allFrames: resolvePerBrowserOption(
entry.options.allFrames,
wxt.config.browser,
config.browser,
),
excludeMatches: resolvePerBrowserOption(
entry.options.excludeMatches,
wxt.config.browser,
config.browser,
),
matches: resolvePerBrowserOption(
entry.options.matches,
wxt.config.browser,
),
runAt: resolvePerBrowserOption(entry.options.runAt, wxt.config.browser),
matches: resolvePerBrowserOption(entry.options.matches, config.browser),
runAt: resolvePerBrowserOption(entry.options.runAt, config.browser),
// @ts-expect-error: Chrome accepts this, not typed in webextension-polyfill (https://developer.chrome.com/docs/extensions/reference/scripting/#type-RegisteredContentScript)
world: resolvePerBrowserOption(entry.options.world, wxt.config.browser),
world: resolvePerBrowserOption(entry.options.world, config.browser),
js,
css,
});
@@ -255,26 +229,10 @@ function reloadContentScripts(steps: BuildStepOutput[], server: WxtDevServer) {
function reloadHtmlPages(
groups: EntrypointGroup[],
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'));
htmlEntries.forEach((entry) => {
const path = getEntrypointBundlePath(entry, wxt.config.outDir, '.html');
config: InternalConfig,
) {
groups.flat().forEach((entry) => {
const path = getEntrypointBundlePath(entry, config.outDir, '.html');
server.reloadPage(path);
});
return {
reloadedNames: htmlEntries.map((entry) => entry.name),
};
}
function getFilenameList(names: string[]): string {
return names
.map((name) => {
return pc.cyan(name);
})
.join(pc.dim(', '));
}
+17 -16
View File
@@ -90,25 +90,26 @@ interface Template {
async function listTemplates(): Promise<Template[]> {
try {
const res = await fetch('https://ungh.cc/repos/wxt-dev/wxt/files/main');
const res = await fetch(
'https://api.github.com/repos/wxt-dev/wxt/contents/templates',
{
headers: {
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
},
},
);
if (res.status >= 300)
throw Error(`Request failed with status ${res.status} ${res.statusText}`);
const data = (await res.json()) as {
meta: {
sha: string;
};
files: Array<{
path: string;
mode: string;
sha: string;
size: number;
}>;
};
return data.files
.map((item) => item.path.match(/templates\/(.+)\/package\.json/)?.[1])
.filter((name) => name != null)
.map((name) => ({ name: name!, path: `templates/${name}` }))
const data = (await res.json()) as Array<{
type: 'file' | 'dir';
name: string;
path: string;
}>;
return data
.filter((item: any) => item.type === 'dir')
.map((item) => ({ name: item.name, path: item.path }))
.sort((l, r) => {
const lWeight = TEMPLATE_SORT_WEIGHT[l.name] ?? Number.MAX_SAFE_INTEGER;
const rWeight = TEMPLATE_SORT_WEIGHT[r.name] ?? Number.MAX_SAFE_INTEGER;
+10 -6
View File
@@ -1,11 +1,15 @@
import { InlineConfig } from '~/types';
import { findEntrypoints, generateTypesDir } from '~/core/utils/building';
import { registerWxt, wxt } from './wxt';
import {
findEntrypoints,
generateTypesDir,
getInternalConfig,
} from '~/core/utils/building';
export async function prepare(config: InlineConfig) {
await registerWxt('build', config);
wxt.logger.info('Generating types...');
const internalConfig = await getInternalConfig(config, 'build');
const entrypoints = await findEntrypoints();
await generateTypesDir(entrypoints);
internalConfig.logger.info('Generating types...');
const entrypoints = await findEntrypoints(internalConfig);
await generateTypesDir(entrypoints, internalConfig);
}
+19 -27
View File
@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from 'vitest';
import { createExtensionRunner } from '..';
import { setFakeWxt } from '~/core/utils/testing/fake-objects';
import { fakeInternalConfig } from '~/core/utils/testing/fake-objects';
import { mock } from 'vitest-mock-extended';
import { createSafariRunner } from '../safari';
import { createWslRunner } from '../wsl';
@@ -26,62 +26,54 @@ const createWebExtRunnerMock = vi.mocked(createWebExtRunner);
describe('createExtensionRunner', () => {
it('should return a Safari runner when browser is "safari"', async () => {
setFakeWxt({
config: {
browser: 'safari',
},
const config = fakeInternalConfig({
browser: 'safari',
});
const safariRunner = mock<ExtensionRunner>();
createSafariRunnerMock.mockReturnValue(safariRunner);
await expect(createExtensionRunner()).resolves.toBe(safariRunner);
await expect(createExtensionRunner(config)).resolves.toBe(safariRunner);
});
it('should return a WSL runner when `is-wsl` is true', async () => {
isWslMock.mockResolvedValueOnce(true);
setFakeWxt({
config: {
browser: 'chrome',
},
const config = fakeInternalConfig({
browser: 'chrome',
});
const wslRunner = mock<ExtensionRunner>();
createWslRunnerMock.mockReturnValue(wslRunner);
await expect(createExtensionRunner()).resolves.toBe(wslRunner);
await expect(createExtensionRunner(config)).resolves.toBe(wslRunner);
});
it('should return a manual runner when `runner.disabled` is true', async () => {
isWslMock.mockResolvedValueOnce(false);
setFakeWxt({
config: {
browser: 'chrome',
runnerConfig: {
config: {
disabled: true,
},
const config = fakeInternalConfig({
browser: 'chrome',
runnerConfig: {
config: {
disabled: true,
},
},
});
const manualRunner = mock<ExtensionRunner>();
createManualRunnerMock.mockReturnValue(manualRunner);
await expect(createExtensionRunner()).resolves.toBe(manualRunner);
await expect(createExtensionRunner(config)).resolves.toBe(manualRunner);
});
it('should return a web-ext runner otherwise', async () => {
setFakeWxt({
config: {
browser: 'chrome',
runnerConfig: {
config: {
disabled: undefined,
},
const config = fakeInternalConfig({
browser: 'chrome',
runnerConfig: {
config: {
disabled: undefined,
},
},
});
const manualRunner = mock<ExtensionRunner>();
createWebExtRunnerMock.mockReturnValue(manualRunner);
await expect(createExtensionRunner()).resolves.toBe(manualRunner);
await expect(createExtensionRunner(config)).resolves.toBe(manualRunner);
});
});
+6 -5
View File
@@ -1,16 +1,17 @@
import { ExtensionRunner } from '~/types';
import { InternalConfig, ExtensionRunner } from '~/types';
import { createWslRunner } from './wsl';
import { createWebExtRunner } from './web-ext';
import { createSafariRunner } from './safari';
import { createManualRunner } from './manual';
import { isWsl } from '~/core/utils/wsl';
import { wxt } from '../wxt';
export async function createExtensionRunner(): Promise<ExtensionRunner> {
if (wxt.config.browser === 'safari') return createSafariRunner();
export async function createExtensionRunner(
config: InternalConfig,
): Promise<ExtensionRunner> {
if (config.browser === 'safari') return createSafariRunner();
if (await isWsl()) return createWslRunner();
if (wxt.config.runnerConfig.config?.disabled) return createManualRunner();
if (config.runnerConfig.config?.disabled) return createManualRunner();
return createWebExtRunner();
}
+3 -4
View File
@@ -1,17 +1,16 @@
import { ExtensionRunner } from '~/types';
import { relative } from 'node:path';
import { wxt } from '../wxt';
/**
* The manual runner tells the user to load the unpacked extension manually.
*/
export function createManualRunner(): ExtensionRunner {
return {
async openBrowser() {
wxt.logger.info(
async openBrowser(config) {
config.logger.info(
`Load "${relative(
process.cwd(),
wxt.config.outDir,
config.outDir,
)}" as an unpacked extension manually`,
);
},
+3 -4
View File
@@ -1,17 +1,16 @@
import { ExtensionRunner } from '~/types';
import { relative } from 'node:path';
import { wxt } from '../wxt';
/**
* The Safari runner just logs a warning message because `web-ext` doesn't work with Safari.
*/
export function createSafariRunner(): ExtensionRunner {
return {
async openBrowser() {
wxt.logger.warn(
async openBrowser(config) {
config.logger.warn(
`Cannot Safari using web-ext. Load "${relative(
process.cwd(),
wxt.config.outDir,
config.outDir,
)}" as an unpacked extension manually`,
);
},
+14 -37
View File
@@ -1,8 +1,5 @@
import type { WebExtRunInstance } from 'web-ext-run';
import { ExtensionRunner } from '~/types';
import { formatDuration } from '../utils/time';
import defu from 'defu';
import { wxt } from '../wxt';
/**
* Create an `ExtensionRunner` backed by `web-ext`.
@@ -11,31 +8,28 @@ export function createWebExtRunner(): ExtensionRunner {
let runner: WebExtRunInstance | undefined;
return {
async openBrowser() {
const startTime = Date.now();
async openBrowser(config) {
config.logger.info('Opening browser...');
if (
wxt.config.browser === 'firefox' &&
wxt.config.manifestVersion === 3
) {
if (config.browser === 'firefox' && config.manifestVersion === 3) {
throw Error(
'Dev mode does not support Firefox MV3. For alternatives, see https://github.com/wxt-dev/wxt/issues/230#issuecomment-1806881653',
);
}
// Use WXT's logger instead of web-ext's built-in one.
// Use the plugin's logger instead of web-ext's built-in one.
const webExtLogger = await import('web-ext-run/util/logger');
webExtLogger.consoleStream.write = ({ level, msg, name }) => {
if (level >= ERROR_LOG_LEVEL) wxt.logger.error(name, msg);
if (level >= WARN_LOG_LEVEL) wxt.logger.warn(msg);
if (level >= ERROR_LOG_LEVEL) config.logger.error(name, msg);
if (level >= WARN_LOG_LEVEL) config.logger.warn(msg);
};
const wxtUserConfig = wxt.config.runnerConfig.config;
const wxtUserConfig = config.runnerConfig.config;
const userConfig = {
console: wxtUserConfig?.openConsole,
devtools: wxtUserConfig?.openDevtools,
startUrl: wxtUserConfig?.startUrls,
...(wxt.config.browser === 'firefox'
...(config.browser === 'firefox'
? {
firefox: wxtUserConfig?.binaries?.firefox,
firefoxProfile: wxtUserConfig?.firefoxProfile,
@@ -43,21 +37,16 @@ export function createWebExtRunner(): ExtensionRunner {
args: wxtUserConfig?.firefoxArgs,
}
: {
chromiumBinary: wxtUserConfig?.binaries?.[wxt.config.browser],
chromiumBinary: wxtUserConfig?.binaries?.[config.browser],
chromiumProfile: wxtUserConfig?.chromiumProfile,
chromiumPref: defu(
wxtUserConfig?.chromiumPref,
DEFAULT_CHROMIUM_PREFS,
),
args: wxtUserConfig?.chromiumArgs,
}),
};
const finalConfig = {
...userConfig,
target:
wxt.config.browser === 'firefox' ? 'firefox-desktop' : 'chromium',
sourceDir: wxt.config.outDir,
target: config.browser === 'firefox' ? 'firefox-desktop' : 'chromium',
sourceDir: config.outDir,
// WXT handles reloads, so disable auto-reload behaviors in web-ext
noReload: true,
noInput: true,
@@ -66,14 +55,13 @@ export function createWebExtRunner(): ExtensionRunner {
// Don't call `process.exit(0)` after starting web-ext
shouldExitProgram: false,
};
wxt.logger.debug('web-ext config:', finalConfig);
wxt.logger.debug('web-ext options:', options);
config.logger.debug('web-ext config:', finalConfig);
config.logger.debug('web-ext options:', options);
const webExt = await import('web-ext-run');
runner = await webExt.default.cmd.run(finalConfig, options);
const duration = Date.now() - startTime;
wxt.logger.success(`Opened browser in ${formatDuration(duration)}`);
config.logger.success('Opened!');
},
async closeBrowser() {
@@ -85,14 +73,3 @@ export function createWebExtRunner(): ExtensionRunner {
// https://github.com/mozilla/web-ext/blob/e37e60a2738478f512f1255c537133321f301771/src/util/logger.js#L12
const WARN_LOG_LEVEL = 40;
const ERROR_LOG_LEVEL = 50;
const DEFAULT_CHROMIUM_PREFS = {
devtools: {
synced_preferences_sync_disabled: {
// Remove content scripts from sourcemap debugger ignore list so stack traces
// and log locations show up properly, see:
// https://github.com/wxt-dev/wxt/issues/236#issuecomment-1915364520
skipContentScripts: false,
},
},
};
+3 -4
View File
@@ -1,17 +1,16 @@
import { ExtensionRunner } from '~/types';
import { relative } from 'node:path';
import { wxt } from '../wxt';
/**
* The WSL runner just logs a warning message because `web-ext` doesn't work in WSL.
*/
export function createWslRunner(): ExtensionRunner {
return {
async openBrowser() {
wxt.logger.warn(
async openBrowser(config) {
config.logger.warn(
`Cannot open browser when using WSL. Load "${relative(
process.cwd(),
wxt.config.outDir,
config.outDir,
)}" as an unpacked extension manually`,
);
},
+1 -17
View File
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { every, some } from '~/core/utils/arrays';
import { every } from '~/core/utils/arrays';
describe('Array Utils', () => {
describe('every', () => {
@@ -15,20 +15,4 @@ describe('Array Utils', () => {
expect(every([1, 2, 1], (item) => item === 1)).toBe(false);
});
});
describe('some', () => {
it('should return true if one value returns true', () => {
const array = [1, 2, 3];
const predicate = (item: number) => item === 2;
expect(some(array, predicate)).toBe(true);
});
it('should return false if no values match', () => {
const array = [1, 2, 3];
const predicate = (item: number) => item === 4;
expect(some(array, predicate)).toBe(false);
});
});
});
@@ -1,15 +1,14 @@
import { describe, expect, it, beforeEach } from 'vitest';
import { describe, expect, it } from 'vitest';
import { hashContentScriptOptions } from '~/core/utils/content-scripts';
import { setFakeWxt } from '~/core/utils/testing/fake-objects';
import { fakeInternalConfig } from '~/core/utils/testing/fake-objects';
describe('Content Script Utils', () => {
beforeEach(() => {
setFakeWxt();
});
describe('hashContentScriptOptions', () => {
it('should return a string containing all the options with defaults applied', () => {
const hash = hashContentScriptOptions({ matches: [] });
const hash = hashContentScriptOptions(
{ matches: [] },
fakeInternalConfig(),
);
expect(hash).toMatchInlineSnapshot(
`"[["all_frames",false],["exclude_globs",[]],["exclude_matches",[]],["include_globs",[]],["match_about_blank",false],["match_origin_as_fallback",false],["matches",[]],["run_at","document_idle"],["world","ISOLATED"]]"`,
@@ -17,15 +16,21 @@ describe('Content Script Utils', () => {
});
it('should be consistent regardless of the object ordering and default values', () => {
const hash1 = hashContentScriptOptions({
allFrames: true,
matches: ['*://google.com/*', '*://duckduckgo.com/*'],
matchAboutBlank: false,
});
const hash2 = hashContentScriptOptions({
matches: ['*://duckduckgo.com/*', '*://google.com/*'],
allFrames: true,
});
const hash1 = hashContentScriptOptions(
{
allFrames: true,
matches: ['*://google.com/*', '*://duckduckgo.com/*'],
matchAboutBlank: false,
},
fakeInternalConfig(),
);
const hash2 = hashContentScriptOptions(
{
matches: ['*://duckduckgo.com/*', '*://google.com/*'],
allFrames: true,
},
fakeInternalConfig(),
);
expect(hash1).toBe(hash2);
});
@@ -47,7 +47,6 @@ describe('Entrypoint Utils', () => {
name,
outputDir,
options: {},
skipped: false,
};
const actual = getEntrypointOutputFile(entrypoint, ext);
+165 -341
View File
@@ -1,14 +1,13 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { describe, expect, it } from 'vitest';
import { generateManifest, stripPathFromMatchPattern } from '../manifest';
import {
fakeArray,
fakeBackgroundEntrypoint,
fakeBuildOutput,
fakeEntrypoint,
fakeManifestCommand,
fakeInternalConfig,
fakeOptionsEntrypoint,
fakePopupEntrypoint,
setFakeWxt,
} from '../testing/fake-objects';
import { Manifest } from 'webextension-polyfill';
import {
@@ -17,16 +16,11 @@ import {
Entrypoint,
OutputAsset,
} from '~/types';
import { wxt } from '../../wxt';
const outDir = '/output';
const contentScriptOutDir = '/output/content-scripts';
describe('Manifest Utils', () => {
beforeEach(() => {
setFakeWxt();
});
describe('generateManifest', () => {
describe('popup', () => {
type ActionType = 'browser_action' | 'page_action';
@@ -46,12 +40,9 @@ describe('Manifest Utils', () => {
it('should include an action for mv3', async () => {
const popup = popupEntrypoint();
const buildOutput = fakeBuildOutput();
setFakeWxt({
config: {
manifestVersion: 3,
outDir,
},
const config = fakeInternalConfig({
manifestVersion: 3,
outDir,
});
const expected: Partial<Manifest.WebExtensionManifest> = {
action: {
@@ -61,10 +52,7 @@ describe('Manifest Utils', () => {
},
};
const { manifest: actual } = await generateManifest(
[popup],
buildOutput,
);
const actual = await generateManifest([popup], buildOutput, config);
expect(actual).toMatchObject(expected);
});
@@ -81,11 +69,9 @@ describe('Manifest Utils', () => {
async ({ inputType, expectedType }) => {
const popup = popupEntrypoint(inputType);
const buildOutput = fakeBuildOutput();
setFakeWxt({
config: {
manifestVersion: 2,
outDir,
},
const config = fakeInternalConfig({
manifestVersion: 2,
outDir,
});
const expected = {
default_icon: popup.options.defaultIcon,
@@ -93,10 +79,7 @@ describe('Manifest Utils', () => {
default_popup: 'popup.html',
};
const { manifest: actual } = await generateManifest(
[popup],
buildOutput,
);
const actual = await generateManifest([popup], buildOutput, config);
expect(actual[expectedType]).toEqual(expected);
},
@@ -106,22 +89,20 @@ describe('Manifest Utils', () => {
describe('action without popup', () => {
it('should respect the action field in the manifest without a popup', async () => {
const buildOutput = fakeBuildOutput();
setFakeWxt({
config: {
outDir,
manifest: {
action: {
default_icon: 'icon-16.png',
default_title: 'Example title',
},
const config = fakeInternalConfig({
outDir,
manifest: {
action: {
default_icon: 'icon-16.png',
default_title: 'Example title',
},
},
});
const expected: Partial<Manifest.WebExtensionManifest> = {
action: wxt.config.manifest.action,
action: config.manifest.action,
};
const { manifest: actual } = await generateManifest([], buildOutput);
const actual = await generateManifest([], buildOutput, config);
expect(actual).toMatchObject(expected);
});
@@ -138,12 +119,10 @@ describe('Manifest Utils', () => {
});
it('should include a options_ui and chrome_style for chrome', async () => {
setFakeWxt({
config: {
manifestVersion: 3,
outDir,
browser: 'chrome',
},
const config = fakeInternalConfig({
manifestVersion: 3,
outDir,
browser: 'chrome',
});
const buildOutput = fakeBuildOutput();
const expected = {
@@ -152,21 +131,16 @@ describe('Manifest Utils', () => {
page: 'options.html',
};
const { manifest: actual } = await generateManifest(
[options],
buildOutput,
);
const actual = await generateManifest([options], buildOutput, config);
expect(actual.options_ui).toEqual(expected);
});
it('should include a options_ui and browser_style for firefox', async () => {
setFakeWxt({
config: {
manifestVersion: 3,
browser: 'firefox',
outDir,
},
const config = fakeInternalConfig({
manifestVersion: 3,
browser: 'firefox',
outDir,
});
const buildOutput = fakeBuildOutput();
const expected = {
@@ -175,10 +149,7 @@ describe('Manifest Utils', () => {
page: 'options.html',
};
const { manifest: actual } = await generateManifest(
[options],
buildOutput,
);
const actual = await generateManifest([options], buildOutput, config);
expect(actual.options_ui).toEqual(expected);
});
@@ -197,12 +168,10 @@ describe('Manifest Utils', () => {
it.each(['chrome', 'safari'])(
'should include a service worker and type for %s',
async (browser) => {
setFakeWxt({
config: {
outDir,
manifestVersion: 3,
browser,
},
const config = fakeInternalConfig({
outDir,
manifestVersion: 3,
browser,
});
const buildOutput = fakeBuildOutput();
const expected = {
@@ -210,9 +179,10 @@ describe('Manifest Utils', () => {
service_worker: 'background.js',
};
const { manifest: actual } = await generateManifest(
const actual = await generateManifest(
[background],
buildOutput,
config,
);
expect(actual.background).toEqual(expected);
@@ -220,12 +190,10 @@ describe('Manifest Utils', () => {
);
it('should include a background script and type for firefox', async () => {
setFakeWxt({
config: {
outDir,
manifestVersion: 3,
browser: 'firefox',
},
const config = fakeInternalConfig({
outDir,
manifestVersion: 3,
browser: 'firefox',
});
const buildOutput = fakeBuildOutput();
const expected = {
@@ -233,9 +201,10 @@ describe('Manifest Utils', () => {
scripts: ['background.js'],
};
const { manifest: actual } = await generateManifest(
const actual = await generateManifest(
[background],
buildOutput,
config,
);
expect(actual.background).toEqual(expected);
@@ -246,12 +215,10 @@ describe('Manifest Utils', () => {
it.each(['chrome', 'safari'])(
'should include scripts and persistent for %s',
async (browser) => {
setFakeWxt({
config: {
outDir,
manifestVersion: 2,
browser,
},
const config = fakeInternalConfig({
outDir,
manifestVersion: 2,
browser,
});
const buildOutput = fakeBuildOutput();
const expected = {
@@ -259,9 +226,10 @@ describe('Manifest Utils', () => {
scripts: ['background.js'],
};
const { manifest: actual } = await generateManifest(
const actual = await generateManifest(
[background],
buildOutput,
config,
);
expect(actual.background).toEqual(expected);
@@ -269,12 +237,10 @@ describe('Manifest Utils', () => {
);
it('should include a background script and persistent for firefox mv2', async () => {
setFakeWxt({
config: {
outDir,
manifestVersion: 2,
browser: 'firefox',
},
const config = fakeInternalConfig({
outDir,
manifestVersion: 2,
browser: 'firefox',
});
const buildOutput = fakeBuildOutput();
const expected = {
@@ -282,9 +248,10 @@ describe('Manifest Utils', () => {
scripts: ['background.js'],
};
const { manifest: actual } = await generateManifest(
const actual = await generateManifest(
[background],
buildOutput,
config,
);
expect(actual.background).toEqual(expected);
@@ -305,11 +272,9 @@ describe('Manifest Utils', () => {
{ type: 'asset', fileName: 'icons/128x128.png' },
],
});
const config = fakeInternalConfig();
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
);
const actual = await generateManifest(entrypoints, buildOutput, config);
expect(actual.icons).toEqual({
16: 'icon-16.png',
@@ -329,11 +294,9 @@ describe('Manifest Utils', () => {
{ type: 'asset', fileName: 'icon-16.jpeg' },
],
});
const config = fakeInternalConfig();
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
);
const actual = await generateManifest(entrypoints, buildOutput, config);
expect(actual.icons).toBeUndefined();
});
@@ -354,18 +317,13 @@ describe('Manifest Utils', () => {
32: 'logo-32.png',
48: 'logo-48.png',
};
setFakeWxt({
config: {
manifest: {
icons: expected,
},
const config = fakeInternalConfig({
manifest: {
icons: expected,
},
});
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
);
const actual = await generateManifest(entrypoints, buildOutput, config);
expect(actual.icons).toEqual(expected);
});
@@ -381,7 +339,6 @@ describe('Manifest Utils', () => {
options: {
matches: ['*://google.com/*'],
},
skipped: false,
};
const cs1Styles: OutputAsset = {
type: 'asset',
@@ -396,7 +353,6 @@ describe('Manifest Utils', () => {
matches: ['*://google.com/*'],
runAt: 'document_end',
},
skipped: false,
};
const cs2Styles: OutputAsset = {
type: 'asset',
@@ -411,7 +367,6 @@ describe('Manifest Utils', () => {
matches: ['*://google.com/*'],
runAt: 'document_end',
},
skipped: false,
};
const cs3Styles: OutputAsset = {
type: 'asset',
@@ -426,7 +381,6 @@ describe('Manifest Utils', () => {
matches: ['*://duckduckgo.com/*'],
runAt: 'document_end',
},
skipped: false,
};
const cs4Styles: OutputAsset = {
type: 'asset',
@@ -441,7 +395,6 @@ describe('Manifest Utils', () => {
matches: ['*://google.com/*'],
world: 'MAIN',
},
skipped: false,
};
const cs5Styles: OutputAsset = {
type: 'asset',
@@ -449,12 +402,10 @@ describe('Manifest Utils', () => {
};
const entrypoints = [cs1, cs2, cs3, cs4, cs5];
setFakeWxt({
config: {
command: 'build',
outDir,
manifestVersion: 3,
},
const config = fakeInternalConfig({
command: 'build',
outDir,
manifestVersion: 3,
});
const buildOutput: Omit<BuildOutput, 'manifest'> = {
publicAssets: [],
@@ -467,10 +418,7 @@ describe('Manifest Utils', () => {
],
};
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
);
const actual = await generateManifest(entrypoints, buildOutput, config);
expect(actual.content_scripts).toContainEqual({
matches: ['*://google.com/*'],
@@ -506,7 +454,6 @@ describe('Manifest Utils', () => {
options: {
matches: ['*://google.com/*'],
},
skipped: false,
};
const generatedContentScript = {
matches: ['*://google.com/*'],
@@ -519,20 +466,15 @@ describe('Manifest Utils', () => {
const entrypoints = [cs];
const buildOutput = fakeBuildOutput();
setFakeWxt({
config: {
outDir,
command: 'build',
manifest: {
content_scripts: [userContentScript],
},
const config = fakeInternalConfig({
outDir,
command: 'build',
manifest: {
content_scripts: [userContentScript],
},
});
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
);
const actual = await generateManifest(entrypoints, buildOutput, config);
expect(actual.content_scripts).toContainEqual(userContentScript);
expect(actual.content_scripts).toContainEqual(generatedContentScript);
@@ -551,7 +493,6 @@ describe('Manifest Utils', () => {
matches: ['*://google.com/*'],
cssInjectionMode,
},
skipped: false,
};
const styles: OutputAsset = {
type: 'asset',
@@ -563,16 +504,15 @@ describe('Manifest Utils', () => {
publicAssets: [],
steps: [{ entrypoints: cs, chunks: [styles] }],
};
setFakeWxt({
config: {
outDir,
command: 'build',
},
const config = fakeInternalConfig({
outDir,
command: 'build',
});
const { manifest: actual } = await generateManifest(
const actual = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.content_scripts).toEqual([
@@ -597,7 +537,6 @@ describe('Manifest Utils', () => {
matches: ['*://google.com/*'],
cssInjectionMode,
},
skipped: false,
};
const styles: OutputAsset = {
type: 'asset',
@@ -609,16 +548,15 @@ describe('Manifest Utils', () => {
publicAssets: [],
steps: [{ entrypoints: cs, chunks: [styles] }],
};
setFakeWxt({
config: {
outDir,
command: 'build',
},
const config = fakeInternalConfig({
outDir,
command: 'build',
});
const { manifest: actual } = await generateManifest(
const actual = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.content_scripts).toEqual([
@@ -640,7 +578,6 @@ describe('Manifest Utils', () => {
matches: ['*://google.com/*'],
cssInjectionMode: 'ui',
},
skipped: false,
};
const styles: OutputAsset = {
type: 'asset',
@@ -652,17 +589,16 @@ describe('Manifest Utils', () => {
publicAssets: [],
steps: [{ entrypoints: cs, chunks: [styles] }],
};
setFakeWxt({
config: {
outDir,
command: 'build',
manifestVersion: 3,
},
const config = fakeInternalConfig({
outDir,
command: 'build',
manifestVersion: 3,
});
const { manifest: actual } = await generateManifest(
const actual = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.web_accessible_resources).toEqual([
@@ -683,7 +619,6 @@ describe('Manifest Utils', () => {
matches: ['*://google.com/*'],
cssInjectionMode: 'ui',
},
skipped: false,
};
const styles: OutputAsset = {
type: 'asset',
@@ -695,17 +630,16 @@ describe('Manifest Utils', () => {
publicAssets: [],
steps: [{ entrypoints: cs, chunks: [styles] }],
};
setFakeWxt({
config: {
outDir,
command: 'build',
manifestVersion: 2,
},
const config = fakeInternalConfig({
outDir,
command: 'build',
manifestVersion: 2,
});
const { manifest: actual } = await generateManifest(
const actual = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.web_accessible_resources).toEqual([
@@ -723,7 +657,6 @@ describe('Manifest Utils', () => {
matches: ['*://play.google.com/books/*'],
cssInjectionMode: 'ui',
},
skipped: false,
};
const styles: OutputAsset = {
type: 'asset',
@@ -735,17 +668,16 @@ describe('Manifest Utils', () => {
publicAssets: [],
steps: [{ entrypoints: cs, chunks: [styles] }],
};
setFakeWxt({
config: {
outDir,
command: 'build',
manifestVersion: 3,
},
const config = fakeInternalConfig({
outDir,
command: 'build',
manifestVersion: 3,
});
const { manifest: actual } = await generateManifest(
const actual = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.web_accessible_resources).toEqual([
@@ -769,7 +701,6 @@ describe('Manifest Utils', () => {
matches: ['*://google.com/*'],
cssInjectionMode: 'ui',
},
skipped: false,
};
const styles: OutputAsset = {
type: 'asset',
@@ -781,23 +712,18 @@ describe('Manifest Utils', () => {
publicAssets: [],
steps: [{ entrypoints: cs, chunks: [styles] }],
};
setFakeWxt({
config: {
outDir,
command: 'build',
manifestVersion: 3,
manifest: {
web_accessible_resources: [
{ resources: ['one.png'], matches: ['*://one.com/*'] },
],
},
const config = fakeInternalConfig({
outDir,
command: 'build',
manifestVersion: 3,
manifest: {
web_accessible_resources: [
{ resources: ['one.png'], matches: ['*://one.com/*'] },
],
},
});
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
);
const actual = await generateManifest(entrypoints, buildOutput, config);
expect(actual.web_accessible_resources).toEqual([
{ resources: ['one.png'], matches: ['*://one.com/*'] },
@@ -818,7 +744,6 @@ describe('Manifest Utils', () => {
matches: ['*://google.com/*'],
cssInjectionMode: 'ui',
},
skipped: false,
};
const styles: OutputAsset = {
type: 'asset',
@@ -830,21 +755,16 @@ describe('Manifest Utils', () => {
publicAssets: [],
steps: [{ entrypoints: cs, chunks: [styles] }],
};
setFakeWxt({
config: {
outDir,
command: 'build',
manifestVersion: 2,
manifest: {
web_accessible_resources: ['one.png'],
},
const config = fakeInternalConfig({
outDir,
command: 'build',
manifestVersion: 2,
manifest: {
web_accessible_resources: ['one.png'],
},
});
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
);
const actual = await generateManifest(entrypoints, buildOutput, config);
expect(actual.web_accessible_resources).toEqual([
'one.png',
@@ -858,21 +778,16 @@ describe('Manifest Utils', () => {
const entrypoints: Entrypoint[] = [];
const buildOutput = fakeBuildOutput();
const newAuthor = 'Custom Author';
setFakeWxt({
config: {
transformManifest(manifest: any) {
manifest.author = newAuthor;
},
const config = fakeInternalConfig({
transformManifest(manifest: any) {
manifest.author = newAuthor;
},
});
const expected = {
author: newAuthor,
};
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
);
const actual = await generateManifest(entrypoints, buildOutput, config);
expect(actual).toMatchObject(expected);
});
@@ -886,19 +801,18 @@ describe('Manifest Utils', () => {
const versionName = '1.0.0-alpha1';
const entrypoints: Entrypoint[] = [];
const buildOutput = fakeBuildOutput();
setFakeWxt({
config: {
browser,
manifest: {
version,
version_name: versionName,
},
const config = fakeInternalConfig({
browser,
manifest: {
version,
version_name: versionName,
},
});
const { manifest: actual } = await generateManifest(
const actual = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.version).toBe(version);
@@ -913,19 +827,18 @@ describe('Manifest Utils', () => {
const versionName = '1.0.0-alpha1';
const entrypoints: Entrypoint[] = [];
const buildOutput = fakeBuildOutput();
setFakeWxt({
config: {
browser,
manifest: {
version,
version_name: versionName,
},
const config = fakeInternalConfig({
browser,
manifest: {
version,
version_name: versionName,
},
});
const { manifest: actual } = await generateManifest(
const actual = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.version).toBe(version);
@@ -939,19 +852,18 @@ describe('Manifest Utils', () => {
const version = '1.0.0';
const entrypoints: Entrypoint[] = [];
const buildOutput = fakeBuildOutput();
setFakeWxt({
config: {
browser,
manifest: {
version,
version_name: version,
},
const config = fakeInternalConfig({
browser,
manifest: {
version,
version_name: version,
},
});
const { manifest: actual } = await generateManifest(
const actual = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.version).toBe(version);
@@ -962,24 +874,19 @@ describe('Manifest Utils', () => {
it('should log a warning if the version could not be detected', async () => {
const entrypoints: Entrypoint[] = [];
const buildOutput = fakeBuildOutput();
setFakeWxt({
config: {
manifest: {
// @ts-ignore: Purposefully removing version from fake object
version: null,
},
const config = fakeInternalConfig({
manifest: {
// @ts-ignore: Purposefully removing version from fake object
version: null,
},
});
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
);
const actual = await generateManifest(entrypoints, buildOutput, config);
expect(actual.version).toBe('0.0.0');
expect(actual.version_name).toBeUndefined();
expect(wxt.logger.warn).toBeCalledTimes(1);
expect(wxt.logger.warn).toBeCalledWith(
expect(config.logger.warn).toBeCalledTimes(1);
expect(config.logger.warn).toBeCalledWith(
expect.stringContaining('Extension version not found'),
);
});
@@ -988,139 +895,56 @@ describe('Manifest Utils', () => {
describe('commands', () => {
const reloadCommandName = 'wxt:reload-extension';
const reloadCommand = {
description: expect.any(String),
suggested_key: {
default: 'Alt+R',
},
};
it('should include a command for reloading the extension during development', async () => {
setFakeWxt({
config: { command: 'serve' },
});
const config = fakeInternalConfig({ command: 'serve' });
const output = fakeBuildOutput();
const entrypoints = fakeArray(fakeEntrypoint);
const { manifest: actual } = await generateManifest(
entrypoints,
output,
);
const actual = await generateManifest(entrypoints, output, config);
expect(actual.commands).toEqual({
expect(actual.commands).toMatchObject({
[reloadCommandName]: reloadCommand,
});
});
it('should customize the reload commands key binding if passing a custom command', async () => {
setFakeWxt({
config: {
command: 'serve',
dev: {
reloadCommand: 'Ctrl+E',
},
},
});
const output = fakeBuildOutput();
const entrypoints = fakeArray(fakeEntrypoint);
const { manifest: actual } = await generateManifest(
entrypoints,
output,
);
expect(actual.commands).toEqual({
[reloadCommandName]: {
...reloadCommand,
suggested_key: {
default: 'Ctrl+E',
},
},
});
});
it("should not include the reload command when it's been disabled", async () => {
setFakeWxt({
config: {
command: 'serve',
dev: {
reloadCommand: false,
},
},
});
const output = fakeBuildOutput();
const entrypoints = fakeArray(fakeEntrypoint);
const { manifest: actual } = await generateManifest(
entrypoints,
output,
);
expect(actual.commands).toBeUndefined();
});
it('should not override any existing commands when adding the one to reload the extension', async () => {
const customCommandName = 'custom-command';
const customCommand = fakeManifestCommand();
setFakeWxt({
config: {
command: 'serve',
manifest: {
commands: {
[customCommandName]: customCommand,
},
const customCommand = {
description: 'Some other command',
suggested_key: {
default: 'Ctrl+H',
},
};
const config = fakeInternalConfig({
command: 'serve',
manifest: {
commands: {
[customCommandName]: customCommand,
},
},
});
const output = fakeBuildOutput();
const entrypoints = fakeArray(fakeEntrypoint);
const { manifest: actual } = await generateManifest(
entrypoints,
output,
);
const actual = await generateManifest(entrypoints, output, config);
expect(actual.commands).toEqual({
expect(actual.commands).toMatchObject({
[reloadCommandName]: reloadCommand,
[customCommandName]: customCommand,
});
});
it('should not include the command if there are already 4 others (the max)', async () => {
const commands = {
command1: fakeManifestCommand(),
command2: fakeManifestCommand(),
command3: fakeManifestCommand(),
command4: fakeManifestCommand(),
};
setFakeWxt({
config: {
command: 'serve',
manifest: { commands },
},
});
const output = fakeBuildOutput();
const entrypoints = fakeArray(fakeEntrypoint);
const { manifest: actual, warnings } = await generateManifest(
entrypoints,
output,
);
expect(actual.commands).toEqual(commands);
expect(warnings).toHaveLength(1);
});
it('should not include the command when building an extension', async () => {
setFakeWxt({
config: { command: 'build' },
});
const config = fakeInternalConfig({ command: 'build' });
const output = fakeBuildOutput();
const entrypoints = fakeArray(fakeEntrypoint);
const { manifest: actual } = await generateManifest(
entrypoints,
output,
);
const actual = await generateManifest(entrypoints, output, config);
expect(actual.commands).toBeUndefined();
});
+3 -12
View File
@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest';
import { getPackageJson } from '../package';
import { setFakeWxt } from '../testing/fake-objects';
import { fakeInternalConfig } from '../testing/fake-objects';
import { mock } from 'vitest-mock-extended';
import { Logger } from '~/types';
@@ -8,11 +8,7 @@ describe('Package JSON Utils', () => {
describe('getPackageJson', () => {
it('should return the package.json inside <root>/package.json', async () => {
const root = process.cwd(); // WXT project directory
setFakeWxt({
config: { root },
});
const actual = await getPackageJson();
const actual = await getPackageJson(fakeInternalConfig({ root }));
expect(actual).toMatchObject({
name: 'wxt',
@@ -22,12 +18,7 @@ describe('Package JSON Utils', () => {
it("should return an empty object when <root>/package.json doesn't exist", async () => {
const root = '/some/path/that/does/not/exist';
const logger = mock<Logger>();
setFakeWxt({
config: { root, logger },
logger,
});
const actual = await getPackageJson();
const actual = await getPackageJson(fakeInternalConfig({ root, logger }));
expect(actual).toEqual({});
});
@@ -1,7 +0,0 @@
import { defineContentScript } from '~/sandbox';
import { faker } from '@faker-js/faker';
export default defineContentScript({
matches: [faker.string.nanoid()],
main() {},
});
-100
View File
@@ -1,100 +0,0 @@
import { describe, it, expect } from 'vitest';
import {
fakeArray,
fakeContentScriptEntrypoint,
fakeEntrypoint,
fakeGenericEntrypoint,
} from '../testing/fake-objects';
import { validateEntrypoints } from '../validation';
describe('Validation Utils', () => {
describe('validateEntrypoints', () => {
it('should return no errors when there are no errors', () => {
const entrypoints = fakeArray(fakeEntrypoint);
const expected = {
errors: [],
errorCount: 0,
warningCount: 0,
};
const actual = validateEntrypoints(entrypoints);
expect(actual).toEqual(expected);
});
it('should return an error when exclude is not an array', () => {
const entrypoint = fakeGenericEntrypoint({
options: {
// @ts-expect-error
exclude: 0,
},
});
const expected = {
errors: [
{
type: 'error',
message: '`exclude` must be an array of browser names',
value: 0,
entrypoint,
},
],
errorCount: 1,
warningCount: 0,
};
const actual = validateEntrypoints([entrypoint]);
expect(actual).toEqual(expected);
});
it('should return an error when include is not an array', () => {
const entrypoint = fakeGenericEntrypoint({
options: {
// @ts-expect-error
include: 0,
},
});
const expected = {
errors: [
{
type: 'error',
message: '`include` must be an array of browser names',
value: 0,
entrypoint,
},
],
errorCount: 1,
warningCount: 0,
};
const actual = validateEntrypoints([entrypoint]);
expect(actual).toEqual(expected);
});
it("should return an error when content scripts don't have a matches", () => {
const entrypoint = fakeContentScriptEntrypoint({
options: {
// @ts-expect-error
matches: null,
},
});
const expected = {
errors: [
{
type: 'error',
message: '`matches` is required',
value: null,
entrypoint,
},
],
errorCount: 1,
warningCount: 0,
};
const actual = validateEntrypoints([entrypoint]);
expect(actual).toEqual(expected);
});
});
});
-12
View File
@@ -9,15 +9,3 @@ export function every<T>(
if (!predicate(array[i], i)) return false;
return true;
}
/**
* Returns true when any of the predicates return true;
*/
export function some<T>(
array: T[],
predicate: (item: T, index: number) => boolean,
): boolean {
for (let i = 0; i < array.length; i++)
if (predicate(array[i], i)) return true;
return false;
}
@@ -1,4 +1,4 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { describe, expect, it } from 'vitest';
import { DevModeChange, detectDevChanges } from '~/core/utils/building';
import {
fakeBackgroundEntrypoint,
@@ -10,20 +10,22 @@ import {
fakePopupEntrypoint,
fakeOutputAsset,
fakeOutputChunk,
fakeWxt,
setFakeWxt,
} from '~/core/utils/testing/fake-objects';
import { BuildOutput, BuildStepOutput } from '~/types';
import { setWxtForTesting } from '../../../wxt';
describe('Detect Dev Changes', () => {
beforeEach(() => {
setWxtForTesting(fakeWxt());
});
describe('No changes', () => {
it("should return 'no-change' when a build hasn't finished", () => {
const actual = detectDevChanges(
[['unknown', '/path/to/file.ts']],
undefined,
);
expect(actual).toEqual({ type: 'no-change' });
});
it("should return 'no-change' when the changed file isn't used by any of the entrypoints", () => {
const changes = ['/some/path.ts'];
const change: [string, string] = ['unknown', '/some/path.ts'];
const currentOutput: BuildOutput = {
manifest: fakeManifest(),
publicAssets: [],
@@ -39,67 +41,18 @@ describe('Detect Dev Changes', () => {
],
};
const actual = detectDevChanges(changes, currentOutput);
const actual = detectDevChanges([change], currentOutput);
expect(actual).toEqual({ type: 'no-change' });
});
});
describe('wxt.config.ts', () => {
it("should return 'full-restart' when one of the changed files is the config file", () => {
const configFile = '/root/wxt.config.ts';
setFakeWxt({
config: {
userConfigMetadata: {
configFile,
},
},
});
const changes = ['/root/src/public/image.svg', configFile];
const currentOutput: BuildOutput = {
manifest: fakeManifest(),
publicAssets: [],
steps: [],
};
const expected: DevModeChange = {
type: 'full-restart',
};
const actual = detectDevChanges(changes, currentOutput);
expect(actual).toEqual(expected);
});
});
describe('web-ext.config.ts', () => {
it("should return 'browser-restart' when one of the changed files is the config file", () => {
const runnerFile = '/root/web-ext.config.ts';
setFakeWxt({
config: {
runnerConfig: {
configFile: runnerFile,
},
},
});
const changes = ['/root/src/public/image.svg', runnerFile];
const currentOutput: BuildOutput = {
manifest: fakeManifest(),
publicAssets: [],
steps: [],
};
const expected: DevModeChange = {
type: 'browser-restart',
};
const actual = detectDevChanges(changes, currentOutput);
expect(actual).toEqual(expected);
});
});
describe('Public Assets', () => {
it("should return 'extension-reload' without any groups to rebuild when the changed file is a public asset", () => {
const changes = ['/root/src/public/image.svg'];
const change: [string, string] = [
'unknown',
'/root/src/public/image.svg',
];
const asset1 = fakeOutputAsset({
fileName: 'image.svg',
});
@@ -120,7 +73,7 @@ describe('Detect Dev Changes', () => {
},
};
const actual = detectDevChanges(changes, currentOutput);
const actual = detectDevChanges([change], currentOutput);
expect(actual).toEqual(expected);
});
@@ -167,15 +120,18 @@ describe('Detect Dev Changes', () => {
rebuildGroups: [background],
};
const actual = detectDevChanges([changedPath], currentOutput);
const actual = detectDevChanges(
[['unknown', changedPath]],
currentOutput,
);
expect(actual).toEqual(expected);
});
});
describe('HTML Pages', () => {
it('should detect changes to entrypoints/<name>.html files', async () => {
const changedPath = '/root/page1.html';
it('should rebuild then reload only the effected pages', async () => {
const changedPath = '/root/page1/index.html';
const htmlPage1 = fakePopupEntrypoint({
inputPath: changedPath,
});
@@ -190,16 +146,16 @@ describe('Detect Dev Changes', () => {
const step1: BuildStepOutput = {
entrypoints: [htmlPage1, htmlPage2],
chunks: [
fakeOutputAsset({
fileName: 'page1.html',
fakeOutputChunk({
moduleIds: [fakeFile(), changedPath],
}),
],
};
const step2: BuildStepOutput = {
entrypoints: [htmlPage3],
chunks: [
fakeOutputAsset({
fileName: 'page2.html',
fakeOutputChunk({
moduleIds: [fakeFile(), fakeFile(), fakeFile()],
}),
],
};
@@ -218,56 +174,10 @@ describe('Detect Dev Changes', () => {
rebuildGroups: [[htmlPage1, htmlPage2]],
};
const actual = detectDevChanges([changedPath], currentOutput);
expect(actual).toEqual(expected);
});
it('should detect changes to entrypoints/<name>/index.html files', async () => {
const changedPath = '/root/page1/index.html';
const htmlPage1 = fakePopupEntrypoint({
inputPath: changedPath,
});
const htmlPage2 = fakeOptionsEntrypoint({
inputPath: '/root/page2/index.html',
});
const htmlPage3 = fakeGenericEntrypoint({
type: 'sandbox',
inputPath: '/root/page3/index.html',
});
const step1: BuildStepOutput = {
entrypoints: [htmlPage1, htmlPage2],
chunks: [
fakeOutputAsset({
fileName: 'page1.html',
}),
],
};
const step2: BuildStepOutput = {
entrypoints: [htmlPage3],
chunks: [
fakeOutputAsset({
fileName: 'page2.html',
}),
],
};
const currentOutput: BuildOutput = {
manifest: fakeManifest(),
publicAssets: [],
steps: [step1, step2],
};
const expected: DevModeChange = {
type: 'html-reload',
cachedOutput: {
...currentOutput,
steps: [step2],
},
rebuildGroups: [[htmlPage1, htmlPage2]],
};
const actual = detectDevChanges([changedPath], currentOutput);
const actual = detectDevChanges(
[['unknown', changedPath]],
currentOutput,
);
expect(actual).toEqual(expected);
});
@@ -326,7 +236,10 @@ describe('Detect Dev Changes', () => {
rebuildGroups: [script1, script3],
};
const actual = detectDevChanges([changedPath], currentOutput);
const actual = detectDevChanges(
[['unknown', changedPath]],
currentOutput,
);
expect(actual).toEqual(expected);
});
@@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { describe, it, expect, vi } from 'vitest';
import {
BackgroundEntrypoint,
ContentScriptEntrypoint,
@@ -11,10 +11,7 @@ import { findEntrypoints } from '../find-entrypoints';
import fs from 'fs-extra';
import { importEntrypointFile } from '../import-entrypoint';
import glob from 'fast-glob';
import {
fakeResolvedConfig,
setFakeWxt,
} from '~/core/utils/testing/fake-objects';
import { fakeInternalConfig } from '~/core/utils/testing/fake-objects';
import { unnormalizePath } from '~/core/utils/paths';
vi.mock('../import-entrypoint');
@@ -29,18 +26,13 @@ const readFileMock = vi.mocked(
);
describe('findEntrypoints', () => {
const config = fakeResolvedConfig({
manifestVersion: 3,
const config = fakeInternalConfig({
root: '/',
entrypointsDir: resolve('/src/entrypoints'),
outDir: resolve('.output'),
command: 'build',
});
beforeEach(() => {
setFakeWxt({ config });
});
it.each<[string, string, PopupEntrypoint]>([
[
'popup.html',
@@ -61,7 +53,6 @@ describe('findEntrypoints', () => {
defaultIcon: { '16': '/icon/16.png' },
defaultTitle: 'Default Title',
},
skipped: false,
},
],
[
@@ -81,7 +72,6 @@ describe('findEntrypoints', () => {
options: {
defaultTitle: 'Title',
},
skipped: false,
},
],
])(
@@ -90,7 +80,7 @@ describe('findEntrypoints', () => {
globMock.mockResolvedValueOnce([path]);
readFileMock.mockResolvedValueOnce(content);
const entrypoints = await findEntrypoints();
const entrypoints = await findEntrypoints(config);
expect(entrypoints).toHaveLength(1);
expect(entrypoints[0]).toEqual(expected);
@@ -113,7 +103,6 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'options.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
[
@@ -134,7 +123,6 @@ describe('findEntrypoints', () => {
options: {
openInTab: true,
},
skipped: false,
},
],
])(
@@ -143,7 +131,7 @@ describe('findEntrypoints', () => {
globMock.mockResolvedValueOnce([path]);
readFileMock.mockResolvedValueOnce(content);
const entrypoints = await findEntrypoints();
const entrypoints = await findEntrypoints(config);
expect(entrypoints).toHaveLength(1);
expect(entrypoints[0]).toEqual(expected);
@@ -158,7 +146,6 @@ describe('findEntrypoints', () => {
name: 'content',
inputPath: resolve(config.entrypointsDir, 'content.ts'),
outputDir: resolve(config.outDir, 'content-scripts'),
skipped: false,
},
],
[
@@ -168,7 +155,6 @@ describe('findEntrypoints', () => {
name: 'overlay',
inputPath: resolve(config.entrypointsDir, 'overlay.content.ts'),
outputDir: resolve(config.outDir, 'content-scripts'),
skipped: false,
},
],
[
@@ -178,7 +164,6 @@ describe('findEntrypoints', () => {
name: 'content',
inputPath: resolve(config.entrypointsDir, 'content/index.ts'),
outputDir: resolve(config.outDir, 'content-scripts'),
skipped: false,
},
],
[
@@ -188,7 +173,6 @@ describe('findEntrypoints', () => {
name: 'overlay',
inputPath: resolve(config.entrypointsDir, 'overlay.content/index.ts'),
outputDir: resolve(config.outDir, 'content-scripts'),
skipped: false,
},
],
[
@@ -198,7 +182,6 @@ describe('findEntrypoints', () => {
name: 'overlay',
inputPath: resolve(config.entrypointsDir, 'overlay.content.tsx'),
outputDir: resolve(config.outDir, 'content-scripts'),
skipped: false,
},
],
])(
@@ -210,11 +193,14 @@ describe('findEntrypoints', () => {
globMock.mockResolvedValueOnce([path]);
importEntrypointFileMock.mockResolvedValue(options);
const entrypoints = await findEntrypoints();
const entrypoints = await findEntrypoints(config);
expect(entrypoints).toHaveLength(1);
expect(entrypoints[0]).toEqual({ ...expected, options });
expect(importEntrypointFileMock).toBeCalledWith(expected.inputPath);
expect(importEntrypointFileMock).toBeCalledWith(
expected.inputPath,
config,
);
},
);
@@ -226,7 +212,6 @@ describe('findEntrypoints', () => {
name: 'background',
inputPath: resolve(config.entrypointsDir, 'background.ts'),
outputDir: config.outDir,
skipped: false,
},
],
[
@@ -236,7 +221,6 @@ describe('findEntrypoints', () => {
name: 'background',
inputPath: resolve(config.entrypointsDir, 'background/index.ts'),
outputDir: config.outDir,
skipped: false,
},
],
])(
@@ -248,58 +232,24 @@ describe('findEntrypoints', () => {
globMock.mockResolvedValueOnce([path]);
importEntrypointFileMock.mockResolvedValue(options);
const entrypoints = await findEntrypoints();
const entrypoints = await findEntrypoints(config);
expect(entrypoints).toHaveLength(1);
expect(entrypoints[0]).toEqual({ ...expected, options });
expect(importEntrypointFileMock).toBeCalledWith(expected.inputPath);
expect(importEntrypointFileMock).toBeCalledWith(
expected.inputPath,
config,
);
},
);
it('should remove type=module from MV2 background scripts', async () => {
setFakeWxt({
config: {
manifestVersion: 2,
},
});
const options: BackgroundEntrypoint['options'] = {
type: 'module',
};
globMock.mockResolvedValueOnce(['background.ts']);
importEntrypointFileMock.mockResolvedValue(options);
const entrypoints = await findEntrypoints();
expect(entrypoints[0].options).toEqual({});
});
it('should allow type=module for MV3 background service workers', async () => {
setFakeWxt({
config: {
manifestVersion: 3,
},
});
const options: BackgroundEntrypoint['options'] = {
type: 'module',
};
globMock.mockResolvedValueOnce(['background.ts']);
importEntrypointFileMock.mockResolvedValue(options);
const entrypoints = await findEntrypoints();
expect(entrypoints[0].options).toEqual(options);
});
it("should include a virtual background script so dev reloading works when there isn't a background entrypoint defined by the user", async () => {
setFakeWxt({
config: {
...config,
command: 'serve',
},
});
globMock.mockResolvedValueOnce(['popup.html']);
const entrypoints = await findEntrypoints();
const entrypoints = await findEntrypoints({
...config,
command: 'serve',
});
expect(entrypoints).toHaveLength(2);
expect(entrypoints).toContainEqual({
@@ -308,7 +258,6 @@ describe('findEntrypoints', () => {
name: 'background',
options: {},
outputDir: config.outDir,
skipped: false,
});
});
@@ -329,17 +278,19 @@ describe('findEntrypoints', () => {
name: 'injected',
inputPath: resolve(config.entrypointsDir, path),
outputDir: config.outDir,
skipped: false,
};
const options: GenericEntrypoint['options'] = {};
globMock.mockResolvedValueOnce([path]);
importEntrypointFileMock.mockResolvedValue(options);
const entrypoints = await findEntrypoints();
const entrypoints = await findEntrypoints(config);
expect(entrypoints).toHaveLength(1);
expect(entrypoints[0]).toEqual({ ...expected, options });
expect(importEntrypointFileMock).toBeCalledWith(expected.inputPath);
expect(importEntrypointFileMock).toBeCalledWith(
expected.inputPath,
config,
);
},
);
@@ -353,7 +304,6 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'sandbox.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
[
@@ -364,7 +314,6 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'sandbox/index.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
[
@@ -375,7 +324,6 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'named.sandbox.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
[
@@ -386,7 +334,6 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'named.sandbox/index.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
@@ -399,7 +346,6 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'bookmarks.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
[
@@ -410,7 +356,6 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'bookmarks/index.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
@@ -423,7 +368,6 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'history.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
[
@@ -434,7 +378,6 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'history/index.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
@@ -447,7 +390,6 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'newtab.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
[
@@ -458,7 +400,6 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'newtab/index.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
@@ -471,7 +412,6 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'sidepanel.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
[
@@ -482,7 +422,6 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'sidepanel/index.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
[
@@ -493,7 +432,6 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'named.sidepanel.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
[
@@ -504,7 +442,6 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'named.sidepanel/index.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
@@ -517,7 +454,6 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'devtools.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
[
@@ -528,7 +464,6 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'devtools/index.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
@@ -541,7 +476,6 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'onboarding.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
[
@@ -552,7 +486,6 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'onboarding/index.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
@@ -565,7 +498,6 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'iframe.scss'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
[
@@ -576,7 +508,6 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'iframe.css'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
@@ -589,7 +520,6 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'content.css'),
outputDir: resolve(config.outDir, 'content-scripts'),
options: {},
skipped: false,
},
],
[
@@ -600,7 +530,6 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'overlay.content.css'),
outputDir: resolve(config.outDir, 'content-scripts'),
options: {},
skipped: false,
},
],
[
@@ -611,7 +540,6 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'content/index.css'),
outputDir: resolve(config.outDir, 'content-scripts'),
options: {},
skipped: false,
},
],
[
@@ -622,13 +550,12 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'overlay.content/index.css'),
outputDir: resolve(config.outDir, 'content-scripts'),
options: {},
skipped: false,
},
],
])('should find entrypoint for %s', async (path, expected) => {
globMock.mockResolvedValueOnce([path]);
const entrypoints = await findEntrypoints();
const entrypoints = await findEntrypoints(config);
expect(entrypoints).toHaveLength(1);
expect(entrypoints[0]).toEqual(expected);
@@ -644,7 +571,7 @@ describe('findEntrypoints', () => {
'ui.html',
]);
await expect(() => findEntrypoints()).rejects.toThrowError(
await expect(() => findEntrypoints(config)).rejects.toThrowError(
[
'Multiple entrypoints with the same name detected, only one entrypoint for each name is allowed.',
'',
@@ -662,7 +589,7 @@ describe('findEntrypoints', () => {
it('throw an error if there are no entrypoints', async () => {
globMock.mockResolvedValueOnce([]);
await expect(() => findEntrypoints()).rejects.toThrowError(
await expect(() => findEntrypoints(config)).rejects.toThrowError(
`No entrypoints found in ${unnormalizePath(config.entrypointsDir)}`,
);
});
@@ -674,7 +601,7 @@ describe('findEntrypoints', () => {
include: ['not' + config.browser],
});
const entrypoints = await findEntrypoints();
const entrypoints = await findEntrypoints(config);
expect(entrypoints).toEqual([]);
});
@@ -685,7 +612,7 @@ describe('findEntrypoints', () => {
include: ['not' + config.browser],
});
const entrypoints = await findEntrypoints();
const entrypoints = await findEntrypoints(config);
expect(entrypoints).toEqual([]);
});
@@ -702,7 +629,7 @@ describe('findEntrypoints', () => {
</html>`,
);
const entrypoints = await findEntrypoints();
const entrypoints = await findEntrypoints(config);
expect(entrypoints).toEqual([]);
});
@@ -719,7 +646,7 @@ describe('findEntrypoints', () => {
</html>`,
);
const entrypoints = await findEntrypoints();
const entrypoints = await findEntrypoints(config);
expect(entrypoints).toEqual([]);
});
@@ -736,7 +663,7 @@ describe('findEntrypoints', () => {
</html>`,
);
const entrypoints = await findEntrypoints();
const entrypoints = await findEntrypoints(config);
expect(entrypoints).toEqual([]);
});
@@ -749,7 +676,7 @@ describe('findEntrypoints', () => {
exclude: [config.browser],
});
const entrypoints = await findEntrypoints();
const entrypoints = await findEntrypoints(config);
expect(entrypoints).toEqual([]);
});
@@ -760,7 +687,7 @@ describe('findEntrypoints', () => {
exclude: [config.browser],
});
const entrypoints = await findEntrypoints();
const entrypoints = await findEntrypoints(config);
expect(entrypoints).toEqual([]);
});
@@ -775,7 +702,7 @@ describe('findEntrypoints', () => {
</html>`,
);
const entrypoints = await findEntrypoints();
const entrypoints = await findEntrypoints(config);
expect(entrypoints).toEqual([]);
});
@@ -790,7 +717,7 @@ describe('findEntrypoints', () => {
</html>`,
);
const entrypoints = await findEntrypoints();
const entrypoints = await findEntrypoints(config);
expect(entrypoints).toEqual([]);
});
@@ -805,36 +732,9 @@ describe('findEntrypoints', () => {
</html>`,
);
const entrypoints = await findEntrypoints();
const entrypoints = await findEntrypoints(config);
expect(entrypoints).toEqual([]);
});
});
describe('filterEntrypoints option', () => {
it('should control entrypoints accessible', async () => {
globMock.mockResolvedValue([
'options/index.html',
'popup/index.html',
'ui.content/index.ts',
'injected.content/index.ts',
]);
importEntrypointFileMock.mockResolvedValue({});
const filterEntrypoints = ['popup', 'ui'];
setFakeWxt({
config: {
root: '/',
entrypointsDir: resolve('/src/entrypoints'),
outDir: resolve('.output'),
command: 'build',
filterEntrypoints: new Set(filterEntrypoints),
},
});
const entrypoints = await findEntrypoints();
const names = entrypoints.map((item) => item.name);
expect(names).toHaveLength(2);
expect(names).toEqual(filterEntrypoints);
});
});
});
@@ -1,11 +1,6 @@
import { describe, expect, it } from 'vitest';
import { Entrypoint } from '~/types';
import { groupEntrypoints } from '../group-entrypoints';
import {
fakeBackgroundEntrypoint,
fakeGenericEntrypoint,
fakePopupEntrypoint,
} from '../../testing/fake-objects';
const background: Entrypoint = {
type: 'background',
@@ -13,7 +8,6 @@ const background: Entrypoint = {
inputPath: '/background.ts',
outputDir: '/.output/background',
options: {},
skipped: false,
};
const contentScript: Entrypoint = {
type: 'content-script',
@@ -23,7 +17,6 @@ const contentScript: Entrypoint = {
options: {
matches: ['<all_urls>'],
},
skipped: false,
};
const unlistedScript: Entrypoint = {
type: 'unlisted-script',
@@ -31,7 +24,6 @@ const unlistedScript: Entrypoint = {
inputPath: '/injected.ts',
outputDir: '/.output/injected',
options: {},
skipped: false,
};
const popup: Entrypoint = {
type: 'popup',
@@ -39,7 +31,6 @@ const popup: Entrypoint = {
inputPath: '/popup.html',
outputDir: '/.output/popup',
options: {},
skipped: false,
};
const unlistedPage: Entrypoint = {
type: 'unlisted-page',
@@ -47,7 +38,6 @@ const unlistedPage: Entrypoint = {
inputPath: '/onboarding.html',
outputDir: '/.output/onboarding',
options: {},
skipped: false,
};
const options: Entrypoint = {
type: 'options',
@@ -55,7 +45,6 @@ const options: Entrypoint = {
inputPath: '/options.html',
outputDir: '/.output/options',
options: {},
skipped: false,
};
const sandbox1: Entrypoint = {
type: 'sandbox',
@@ -63,7 +52,6 @@ const sandbox1: Entrypoint = {
inputPath: '/sandbox1.html',
outputDir: '/.output/sandbox1',
options: {},
skipped: false,
};
const sandbox2: Entrypoint = {
type: 'sandbox',
@@ -71,7 +59,6 @@ const sandbox2: Entrypoint = {
inputPath: '/sandbox2.html',
outputDir: '/.output/sandbox2',
options: {},
skipped: false,
};
const unlistedStyle: Entrypoint = {
type: 'unlisted-style',
@@ -79,7 +66,6 @@ const unlistedStyle: Entrypoint = {
inputPath: '/injected.scss',
outputDir: '/.output',
options: {},
skipped: false,
};
const contentScriptStyle: Entrypoint = {
type: 'content-script-style',
@@ -87,7 +73,6 @@ const contentScriptStyle: Entrypoint = {
inputPath: '/overlay.content.scss',
outputDir: '/.output/content-scripts',
options: {},
skipped: false,
};
describe('groupEntrypoints', () => {
@@ -146,29 +131,4 @@ describe('groupEntrypoints', () => {
expect(actual).toEqual(expected);
});
it('should group ESM compatible scripts with extension pages', () => {
const background = fakeBackgroundEntrypoint({
options: {
type: 'module',
},
});
const popup = fakePopupEntrypoint();
const sandbox = fakeGenericEntrypoint({
inputPath: '/entrypoints/sandbox.html',
name: 'sandbox',
type: 'sandbox',
});
const actual = groupEntrypoints([background, popup, sandbox]);
expect(actual).toEqual([[background, popup], [sandbox]]);
});
it.todo(
'should group ESM compatible sandbox scripts with sandbox pages',
() => {
// Main world content scripts
},
);
});
@@ -1,25 +1,20 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { describe, expect, it } from 'vitest';
import { importEntrypointFile } from '~/core/utils/building';
import { fakeInternalConfig } from '~/core/utils/testing/fake-objects';
import { resolve } from 'node:path';
import { unnormalizePath } from '../../paths';
import { setFakeWxt } from '../../testing/fake-objects';
const entrypointPath = (filename: string) =>
resolve('src/core/utils/__tests__/test-entrypoints', filename);
describe('importEntrypointFile', () => {
beforeEach(() => {
setFakeWxt({
config: {
imports: false,
debug: false,
// Run inside the demo folder so that wxt is in the node_modules
// WXT must also be built for these tests to pass
root: 'demo',
},
});
});
const config = fakeInternalConfig({
imports: false,
debug: false,
// Run inside the demo folder so that wxt is in the node_modules
// WXT must also be built for these tests to pass
root: 'demo',
});
describe('importEntrypointFile', () => {
it.each([
['background.ts', { main: expect.any(Function) }],
['content.ts', { main: expect.any(Function), matches: ['<all_urls>'] }],
@@ -29,7 +24,7 @@ describe('importEntrypointFile', () => {
])(
'should return the default export of test-entrypoints/%s',
async (file, expected) => {
const actual = await importEntrypointFile(entrypointPath(file));
const actual = await importEntrypointFile(entrypointPath(file), config);
expect(actual).toEqual(expected);
},
@@ -38,19 +33,9 @@ describe('importEntrypointFile', () => {
it('should return undefined when there is no default export', async () => {
const actual = await importEntrypointFile(
entrypointPath('no-default-export.ts'),
config,
);
expect(actual).toBeUndefined();
});
it('should throw a custom error message when an imported variable is used before main', async () => {
const filePath = unnormalizePath(
'../src/core/utils/__tests__/test-entrypoints/imported-option.ts',
);
await expect(() =>
importEntrypointFile(entrypointPath('imported-option.ts')),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: ${filePath}: Cannot use imported variable "faker" outside the main function. See https://wxt.dev/guide/entrypoints.html#side-effects]`,
);
});
});
+57 -17
View File
@@ -1,41 +1,53 @@
import { BuildOutput, BuildStepOutput, EntrypointGroup } from '~/types';
import {
BuildOutput,
BuildStepOutput,
EntrypointGroup,
InternalConfig,
} from '~/types';
import { getPublicFiles } from '~/core/utils/fs';
import fs from 'fs-extra';
import { dirname, resolve } from 'path';
import { dirname, resolve, extname } from 'node:path';
import type { Ora } from 'ora';
import pc from 'picocolors';
import { wxt } from '../../wxt';
import { unnormalizePath } from '~/core/utils/paths';
import { convertMessagesToManifest, readMessagesFile } from '~/i18n/node';
export async function buildEntrypoints(
groups: EntrypointGroup[],
config: InternalConfig,
spinner: Ora,
): Promise<Omit<BuildOutput, 'manifest'>> {
const steps: BuildStepOutput[] = [];
for (let i = 0; i < groups.length; i++) {
const group = groups[i];
const groupNames = [group].flat().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));
} catch (err) {
throw Error(`Failed to build ${groupNames.join(', ')}`, { cause: err });
}
const groupNames = [group]
.flat()
.map((e) => e.name)
.join(pc.dim(', '));
spinner.text = pc.dim(`[${i + 1}/${groups.length}]`) + ` ${groupNames}`;
steps.push(await config.builder.build(group));
}
const publicAssets = await copyPublicDirectory();
const publicAssets = (
await Promise.all([
copyPublicDirectory(config),
copyLocalesDirectory(config),
])
).flat();
return { publicAssets, steps };
}
async function copyPublicDirectory(): Promise<BuildOutput['publicAssets']> {
const files = await getPublicFiles();
async function copyPublicDirectory(
config: InternalConfig,
): Promise<BuildOutput['publicAssets']> {
const files = await getPublicFiles(config);
if (files.length === 0) return [];
const publicAssets: BuildOutput['publicAssets'] = [];
for (const file of files) {
const srcPath = resolve(wxt.config.publicDir, file);
const outPath = resolve(wxt.config.outDir, file);
const srcPath = resolve(config.publicDir, file);
const outPath = resolve(config.outDir, file);
await fs.ensureDir(dirname(outPath));
await fs.copyFile(srcPath, outPath);
@@ -47,3 +59,31 @@ async function copyPublicDirectory(): Promise<BuildOutput['publicAssets']> {
return publicAssets;
}
async function copyLocalesDirectory(
config: InternalConfig,
): Promise<BuildOutput['publicAssets']> {
const localesExist = await fs.exists(config.localesDir);
if (!localesExist || config.manifest.default_locale == null) return [];
const files = await fs.readdir(config.localesDir);
return await Promise.all(
files.map(async (file) => {
const locale = file.replace(extname(file), '');
const fileName = unnormalizePath(`_locales/${locale}/messages.json`);
const srcPath = resolve(config.localesDir, file);
const outPath = resolve(config.outDir, fileName);
const messages = await readMessagesFile(srcPath);
const json = convertMessagesToManifest(messages);
await fs.ensureDir(dirname(outPath));
await fs.writeJson(outPath, json);
return {
fileName,
type: 'asset',
};
}),
);
}
+13 -34
View File
@@ -5,9 +5,8 @@ import {
OutputAsset,
OutputFile,
} from '~/types';
import { every, some } from '~/core/utils/arrays';
import { every } from '~/core/utils/arrays';
import { normalizePath } from '~/core/utils/paths';
import { wxt } from '../../wxt';
/**
* Compare the changed files vs the build output and determine what kind of reload needs to happen:
@@ -30,20 +29,10 @@ import { wxt } from '../../wxt';
* - Config file changed (wxt.config.ts, .env, web-ext.config.ts, etc)
*/
export function detectDevChanges(
changedFiles: string[],
currentOutput: BuildOutput,
changedFiles: [event: string, path: string][],
currentOutput: BuildOutput | undefined,
): DevModeChange {
const isConfigChange = some(
changedFiles,
(file) => file === wxt.config.userConfigMetadata.configFile,
);
if (isConfigChange) return { type: 'full-restart' };
const isRunnerChange = some(
changedFiles,
(file) => file === wxt.config.runnerConfig.configFile,
);
if (isRunnerChange) return { type: 'browser-restart' };
if (currentOutput == null) return { type: 'no-change' };
const changedSteps = new Set(
changedFiles.flatMap((changedFile) =>
@@ -80,7 +69,7 @@ export function detectDevChanges(
const isOnlyHtmlChanges =
changedFiles.length > 0 &&
every(changedFiles, (file) => file.endsWith('.html'));
every(changedFiles, ([_, file]) => file.endsWith('.html'));
if (isOnlyHtmlChanges) {
return {
type: 'html-reload',
@@ -115,19 +104,18 @@ export function detectDevChanges(
* For a single change, return all the step of the build output that were effected by it.
*/
function findEffectedSteps(
changedFile: string,
changedFile: [event: string, path: string],
currentOutput: BuildOutput,
): DetectedChange[] {
const changes: DetectedChange[] = [];
const changedPath = normalizePath(changedFile);
const changedPath = normalizePath(changedFile[1]);
const isChunkEffected = (chunk: OutputFile): boolean =>
// If it's an HTML file with the same path, is is effected because HTML files need to be re-rendered
// - fileName is normalized, relative bundle path, "<entrypoint-name>.html"
(chunk.type === 'asset' &&
changedPath.replace('/index.html', '.html').endsWith(chunk.fileName)) ||
// If it's an HTML file with the same path, is is effected because HTML files need to be pre-rendered
// fileName is normalized, relative bundle path
(chunk.type === 'asset' && changedPath.endsWith(chunk.fileName)) ||
// If it's a chunk that depends on the changed file, it is effected
// - moduleIds are absolute, normalized paths
// moduleIds are absolute, normalized paths
(chunk.type === 'chunk' && chunk.moduleIds.includes(changedPath));
for (const step of currentOutput.steps) {
@@ -151,9 +139,8 @@ export type DevModeChange =
| NoChange
| HtmlReload
| ExtensionReload
| ContentScriptReload
| FullRestart
| BrowserRestart;
| ContentScriptReload;
// | BrowserRestart
interface NoChange {
type: 'no-change';
@@ -170,14 +157,6 @@ interface RebuildChange {
cachedOutput: BuildOutput;
}
interface FullRestart {
type: 'full-restart';
}
interface BrowserRestart {
type: 'browser-restart';
}
interface HtmlReload extends RebuildChange {
type: 'html-reload';
}
+75 -114
View File
@@ -7,6 +7,7 @@ import {
ContentScriptEntrypoint,
Entrypoint,
GenericEntrypoint,
InternalConfig,
OptionsEntrypoint,
PopupEntrypoint,
UnlistedScriptDefinition,
@@ -23,15 +24,15 @@ import {
} from '~/core/utils/entrypoints';
import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '~/core/utils/constants';
import { CSS_EXTENSIONS_PATTERN } from '~/core/utils/paths';
import pc from 'picocolors';
import { wxt } from '../../wxt';
/**
* Return entrypoints and their configuration by looking through the project's files.
*/
export async function findEntrypoints(): Promise<Entrypoint[]> {
export async function findEntrypoints(
config: InternalConfig,
): Promise<Entrypoint[]> {
const relativePaths = await glob(Object.keys(PATH_GLOB_TO_TYPE_MAP), {
cwd: wxt.config.entrypointsDir,
cwd: config.entrypointsDir,
});
// Ensure consistent output
relativePaths.sort();
@@ -40,28 +41,21 @@ export async function findEntrypoints(): Promise<Entrypoint[]> {
const entrypointInfos: EntrypointInfo[] = relativePaths.reduce<
EntrypointInfo[]
>((results, relativePath) => {
const inputPath = resolve(wxt.config.entrypointsDir, relativePath);
const name = getEntrypointName(wxt.config.entrypointsDir, inputPath);
const inputPath = resolve(config.entrypointsDir, relativePath);
const name = getEntrypointName(config.entrypointsDir, inputPath);
const matchingGlob = pathGlobs.find((glob) =>
minimatch(relativePath, glob),
);
if (matchingGlob) {
const type = PATH_GLOB_TO_TYPE_MAP[matchingGlob];
results.push({
name,
inputPath,
type,
skipped:
wxt.config.filterEntrypoints != null &&
!wxt.config.filterEntrypoints.has(name),
});
results.push({ name, inputPath, type });
}
return results;
}, []);
// Validation
preventNoEntrypoints(entrypointInfos);
preventDuplicateEntrypointNames(entrypointInfos);
preventNoEntrypoints(config, entrypointInfos);
preventDuplicateEntrypointNames(config, entrypointInfos);
// Import entrypoints to get their config
let hasBackground = false;
@@ -70,23 +64,23 @@ export async function findEntrypoints(): Promise<Entrypoint[]> {
const { type } = info;
switch (type) {
case 'popup':
return await getPopupEntrypoint(info);
return await getPopupEntrypoint(config, info);
case 'options':
return await getOptionsEntrypoint(info);
return await getOptionsEntrypoint(config, info);
case 'background':
hasBackground = true;
return await getBackgroundEntrypoint(info);
return await getBackgroundEntrypoint(config, info);
case 'content-script':
return await getContentScriptEntrypoint(info);
return await getContentScriptEntrypoint(config, info);
case 'unlisted-page':
return await getUnlistedPageEntrypoint(info);
return await getUnlistedPageEntrypoint(config, info);
case 'unlisted-script':
return await getUnlistedScriptEntrypoint(info);
return await getUnlistedScriptEntrypoint(config, info);
case 'content-script-style':
return {
...info,
type,
outputDir: resolve(wxt.config.outDir, CONTENT_SCRIPT_OUT_DIR),
outputDir: resolve(config.outDir, CONTENT_SCRIPT_OUT_DIR),
options: {
include: undefined,
exclude: undefined,
@@ -96,7 +90,7 @@ export async function findEntrypoints(): Promise<Entrypoint[]> {
return {
...info,
type,
outputDir: wxt.config.outDir,
outputDir: config.outDir,
options: {
include: undefined,
exclude: undefined,
@@ -106,51 +100,35 @@ export async function findEntrypoints(): Promise<Entrypoint[]> {
}),
);
if (wxt.config.command === 'serve' && !hasBackground) {
if (config.command === 'serve' && !hasBackground) {
entrypoints.push(
await getBackgroundEntrypoint({
await getBackgroundEntrypoint(config, {
inputPath: VIRTUAL_NOOP_BACKGROUND_MODULE_ID,
name: 'background',
type: 'background',
skipped: false,
}),
);
}
wxt.logger.debug('All entrypoints:', entrypoints);
const skippedEntrypointNames = entrypointInfos
.filter((item) => item.skipped)
.map((item) => item.name);
if (skippedEntrypointNames.length) {
wxt.logger.warn(
`Filter excluded the following entrypoints:\n${skippedEntrypointNames
.map((item) => `${pc.dim('-')} ${pc.cyan(item)}`)
.join('\n')}`,
);
}
config.logger.debug('All entrypoints:', entrypoints);
const targetEntrypoints = entrypoints.filter((entry) => {
const { include, exclude } = entry.options;
if (include?.length && exclude?.length) {
wxt.logger.warn(
config.logger.warn(
`The ${entry.name} entrypoint lists both include and exclude, but only one can be used per entrypoint. Entrypoint ignored.`,
);
return false;
}
if (exclude?.length && !include?.length) {
return !exclude.includes(wxt.config.browser);
return !exclude.includes(config.browser);
}
if (include?.length && !exclude?.length) {
return include.includes(wxt.config.browser);
}
if (skippedEntrypointNames.includes(entry.name)) {
return false;
return include.includes(config.browser);
}
return true;
});
wxt.logger.debug(`${wxt.config.browser} entrypoints:`, targetEntrypoints);
await wxt.hooks.callHook('entrypoints:resolved', wxt, targetEntrypoints);
config.logger.debug(`${config.browser} entrypoints:`, targetEntrypoints);
return targetEntrypoints;
}
@@ -158,13 +136,12 @@ interface EntrypointInfo {
name: string;
inputPath: string;
type: Entrypoint['type'];
/**
* @default false
*/
skipped: boolean;
}
function preventDuplicateEntrypointNames(files: EntrypointInfo[]) {
function preventDuplicateEntrypointNames(
config: InternalConfig,
files: EntrypointInfo[],
) {
const namesToPaths = files.reduce<Record<string, string[]>>(
(map, { name, inputPath }) => {
map[name] ??= [];
@@ -178,7 +155,7 @@ function preventDuplicateEntrypointNames(files: EntrypointInfo[]) {
if (absolutePaths.length > 1) {
lines.push(`- ${name}`);
absolutePaths.forEach((absolutePath) => {
lines.push(` - ${relative(wxt.config.root, absolutePath)}`);
lines.push(` - ${relative(config.root, absolutePath)}`);
});
}
return lines;
@@ -193,9 +170,9 @@ function preventDuplicateEntrypointNames(files: EntrypointInfo[]) {
}
}
function preventNoEntrypoints(files: EntrypointInfo[]) {
function preventNoEntrypoints(config: InternalConfig, files: EntrypointInfo[]) {
if (files.length === 0) {
throw Error(`No entrypoints found in ${wxt.config.entrypointsDir}`);
throw Error(`No entrypoints found in ${config.entrypointsDir}`);
}
}
@@ -223,11 +200,10 @@ function getHtmlBaseOptions(document: Document): BaseEntrypointOptions {
* @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> {
async function getPopupEntrypoint(
config: InternalConfig,
{ inputPath, name }: EntrypointInfo,
): Promise<PopupEntrypoint> {
const content = await fs.readFile(inputPath, 'utf-8');
const { document } = parseHTML(content);
@@ -243,7 +219,7 @@ async function getPopupEntrypoint({
try {
options.defaultIcon = JSON5.parse(defaultIconContent);
} catch (err) {
wxt.logger.fatal(
config.logger.fatal(
`Failed to parse default_icon meta tag content as JSON5. content=${defaultIconContent}`,
err,
);
@@ -270,8 +246,7 @@ async function getPopupEntrypoint({
name: 'popup',
options,
inputPath,
outputDir: wxt.config.outDir,
skipped,
outputDir: config.outDir,
};
}
@@ -279,11 +254,10 @@ async function getPopupEntrypoint({
* @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> {
async function getOptionsEntrypoint(
config: InternalConfig,
{ inputPath, name }: EntrypointInfo,
): Promise<OptionsEntrypoint> {
const content = await fs.readFile(inputPath, 'utf-8');
const { document } = parseHTML(content);
@@ -315,8 +289,7 @@ async function getOptionsEntrypoint({
name: 'options',
options,
inputPath,
outputDir: wxt.config.outDir,
skipped,
outputDir: config.outDir,
};
}
@@ -324,21 +297,19 @@ async function getOptionsEntrypoint({
* @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> {
async function getUnlistedPageEntrypoint(
config: InternalConfig,
{ inputPath, name }: EntrypointInfo,
): Promise<GenericEntrypoint> {
const content = await fs.readFile(inputPath, 'utf-8');
const { document } = parseHTML(content);
return {
type: 'unlisted-page',
name: getEntrypointName(wxt.config.entrypointsDir, inputPath),
name: getEntrypointName(config.entrypointsDir, inputPath),
inputPath,
outputDir: wxt.config.outDir,
outputDir: config.outDir,
options: getHtmlBaseOptions(document),
skipped,
};
}
@@ -346,13 +317,14 @@ async function getUnlistedPageEntrypoint({
* @param path Absolute path to the script's file.
* @param content String contents of the file at the path.
*/
async function getUnlistedScriptEntrypoint({
inputPath,
name,
skipped,
}: EntrypointInfo): Promise<GenericEntrypoint> {
const defaultExport =
await importEntrypointFile<UnlistedScriptDefinition>(inputPath);
async function getUnlistedScriptEntrypoint(
config: InternalConfig,
{ inputPath, name }: EntrypointInfo,
): Promise<GenericEntrypoint> {
const defaultExport = await importEntrypointFile<UnlistedScriptDefinition>(
inputPath,
config,
);
if (defaultExport == null) {
throw Error(
`${name}: Default export not found, did you forget to call "export default defineUnlistedScript(...)"?`,
@@ -364,24 +336,24 @@ async function getUnlistedScriptEntrypoint({
type: 'unlisted-script',
name,
inputPath,
outputDir: wxt.config.outDir,
outputDir: config.outDir,
options,
skipped,
};
}
/**
* @param path Absolute path to the background's TS file.
*/
async function getBackgroundEntrypoint({
inputPath,
name,
skipped,
}: EntrypointInfo): Promise<BackgroundEntrypoint> {
async function getBackgroundEntrypoint(
config: InternalConfig,
{ inputPath, name }: EntrypointInfo,
): Promise<BackgroundEntrypoint> {
let options: Omit<BackgroundDefinition, 'main'> = {};
if (inputPath !== VIRTUAL_NOOP_BACKGROUND_MODULE_ID) {
const defaultExport =
await importEntrypointFile<BackgroundDefinition>(inputPath);
const defaultExport = await importEntrypointFile<BackgroundDefinition>(
inputPath,
config,
);
if (defaultExport == null) {
throw Error(
`${name}: Default export not found, did you forget to call "export default defineBackground(...)"?`,
@@ -390,38 +362,28 @@ async function getBackgroundEntrypoint({
const { main: _, ...moduleOptions } = defaultExport;
options = moduleOptions;
}
if (wxt.config.manifestVersion !== 3) {
delete options.type;
}
return {
type: 'background',
name,
inputPath,
outputDir: wxt.config.outDir,
outputDir: config.outDir,
options: {
...options,
type: resolvePerBrowserOption(options.type, wxt.config.browser),
persistent: resolvePerBrowserOption(
options.persistent,
wxt.config.browser,
),
type: resolvePerBrowserOption(options.type, config.browser),
persistent: resolvePerBrowserOption(options.persistent, config.browser),
},
skipped,
};
}
/**
* @param path Absolute path to the content script's TS file.
*/
async function getContentScriptEntrypoint({
inputPath,
name,
skipped,
}: EntrypointInfo): Promise<ContentScriptEntrypoint> {
async function getContentScriptEntrypoint(
config: InternalConfig,
{ inputPath, name }: EntrypointInfo,
): Promise<ContentScriptEntrypoint> {
const { main: _, ...options } =
await importEntrypointFile<ContentScriptDefinition>(inputPath);
await importEntrypointFile<ContentScriptDefinition>(inputPath, config);
if (options == null) {
throw Error(
`${name}: Default export not found, did you forget to call "export default defineContentScript(...)"?`,
@@ -431,9 +393,8 @@ async function getContentScriptEntrypoint({
type: 'content-script',
name,
inputPath,
outputDir: resolve(wxt.config.outDir, CONTENT_SCRIPT_OUT_DIR),
outputDir: resolve(config.outDir, CONTENT_SCRIPT_OUT_DIR),
options,
skipped,
};
}
+80 -50
View File
@@ -1,47 +1,48 @@
import { UnimportOptions, createUnimport } from 'unimport';
import { Entrypoint } from '~/types';
import { Entrypoint, InternalConfig } from '~/types';
import fs from 'fs-extra';
import { relative, resolve } from 'path';
import { getEntrypointBundlePath } from '~/core/utils/entrypoints';
import { getUnimportOptions } from '~/core/utils/unimport';
import { getEntrypointGlobals, getGlobals } from '~/core/utils/globals';
import { normalizePath } from '~/core/utils/paths';
import path from 'node:path';
import { Message, parseI18nMessages } from '~/core/utils/i18n';
import { writeFileIfDifferent, getPublicFiles } from '~/core/utils/fs';
import { wxt } from '../../wxt';
import glob from 'fast-glob';
import { Message, PREDEFINED_MESSAGES, readMessagesFile } from '~/i18n/node';
/**
* Generate and write all the files inside the `InternalConfig.typesDir` directory.
*/
export async function generateTypesDir(
entrypoints: Entrypoint[],
config: InternalConfig,
): Promise<void> {
await fs.ensureDir(wxt.config.typesDir);
await fs.ensureDir(config.typesDir);
const references: string[] = [];
const imports = getUnimportOptions(wxt.config);
const imports = getUnimportOptions(config);
if (imports !== false) {
references.push(await writeImportsDeclarationFile(imports));
references.push(await writeImportsDeclarationFile(config, imports));
}
references.push(await writePathsDeclarationFile(entrypoints));
references.push(await writeI18nDeclarationFile());
references.push(await writeGlobalsDeclarationFile());
references.push(await writePathsDeclarationFile(entrypoints, config));
references.push(await writeI18nDeclarationFile(config));
references.push(await writeGlobalsDeclarationFile(config));
const mainReference = await writeMainDeclarationFile(references);
await writeTsConfigFile(mainReference);
const mainReference = await writeMainDeclarationFile(references, config);
await writeTsConfigFile(mainReference, config);
}
async function writeImportsDeclarationFile(
config: InternalConfig,
unimportOptions: Partial<UnimportOptions>,
): Promise<string> {
const filePath = resolve(wxt.config.typesDir, 'imports.d.ts');
const filePath = resolve(config.typesDir, 'imports.d.ts');
const unimport = createUnimport(unimportOptions);
// Load project imports into unimport memory so they are output via generateTypeDeclarations
await unimport.scanImportsFromDir(undefined, { cwd: wxt.config.srcDir });
await unimport.scanImportsFromDir(undefined, { cwd: config.srcDir });
await writeFileIfDifferent(
filePath,
@@ -55,17 +56,18 @@ async function writeImportsDeclarationFile(
async function writePathsDeclarationFile(
entrypoints: Entrypoint[],
config: InternalConfig,
): Promise<string> {
const filePath = resolve(wxt.config.typesDir, 'paths.d.ts');
const filePath = resolve(config.typesDir, 'paths.d.ts');
const unions = entrypoints
.map((entry) =>
getEntrypointBundlePath(
entry,
wxt.config.outDir,
config.outDir,
entry.inputPath.endsWith('.html') ? '.html' : '.js',
),
)
.concat(await getPublicFiles())
.concat(await getPublicFiles(config))
.map(normalizePath)
.map((path) => ` | "/${path}"`)
.sort()
@@ -77,10 +79,8 @@ import "wxt/browser";
declare module "wxt/browser" {
export type PublicPath =
{{ union }}
type HtmlPublicPath = Extract<PublicPath, \`\${string}.html\`>
export interface WxtRuntime extends Runtime.Static {
getURL(path: PublicPath): string;
getURL(path: \`\${HtmlPublicPath}\${string}\`): string;
}
}
`;
@@ -93,9 +93,11 @@ declare module "wxt/browser" {
return filePath;
}
async function writeI18nDeclarationFile(): Promise<string> {
const filePath = resolve(wxt.config.typesDir, 'i18n.d.ts');
const defaultLocale = wxt.config.manifest.default_locale;
async function writeI18nDeclarationFile(
config: InternalConfig,
): Promise<string> {
const filePath = resolve(config.typesDir, 'i18n.d.ts');
const defaultLocale = config.manifest.default_locale;
const template = `// Generated by wxt
import "wxt/browser";
@@ -111,30 +113,38 @@ declare module "wxt/browser" {
}
export interface WxtI18n extends I18n.Static {
{{ overrides }}
{{ browserOverrides }}
}
}
declare module "wxt/i18n" {
export interface WxtMessageSchema {
t: {
{{ translationTOverrides }}
};
tp: {
{{ translationTpOverrides }}
};
}
}
`;
let messages: Message[];
if (defaultLocale) {
const defaultLocalePath = path.resolve(
wxt.config.publicDir,
'_locales',
defaultLocale,
'messages.json',
);
const content = JSON.parse(await fs.readFile(defaultLocalePath, 'utf-8'));
messages = parseI18nMessages(content);
const [defaultLocalePath] = await glob(`${defaultLocale}.*`, {
cwd: config.localesDir,
absolute: true,
});
messages = await readMessagesFile(defaultLocalePath);
} else {
messages = parseI18nMessages({});
messages = PREDEFINED_MESSAGES;
}
const overrides = messages.map((message) => {
return ` /**
* ${message.description || 'No message description.'}
* ${message.entry.description ?? 'No message description.'}
*
* "${message.message}"
* "${message.entry.message}"
*/
getMessage(
messageName: "${message.name}",
@@ -144,33 +154,50 @@ declare module "wxt/browser" {
});
await writeFileIfDifferent(
filePath,
template.replace('{{ overrides }}', overrides.join('\n')),
template
.replace('{{ browserOverrides }}', overrides.join('\n'))
.replace(
'{{ translationTOverrides }}',
messages
.filter((message) => !message.isPlural)
.map((message) => ` "${message.name}": any;`)
.join('\n'),
)
.replace(
'{{ translationTpOverrides }}',
messages
.filter((message) => message.isPlural)
.map((message) => ` "${message.name}": any;`)
.join('\n'),
),
);
return filePath;
}
async function writeGlobalsDeclarationFile(): Promise<string> {
const filePath = resolve(wxt.config.typesDir, 'globals.d.ts');
const globals = [...getGlobals(wxt.config), ...getEntrypointGlobals('')];
async function writeGlobalsDeclarationFile(
config: InternalConfig,
): Promise<string> {
const filePath = resolve(config.typesDir, 'globals.d.ts');
const globals = [...getGlobals(config), ...getEntrypointGlobals('')];
await writeFileIfDifferent(
filePath,
[
'// Generated by wxt',
'export {}',
'interface ImportMetaEnv {',
...globals.map((global) => ` readonly ${global.name}: ${global.type};`),
'}',
'interface ImportMeta {',
' readonly env: ImportMetaEnv',
'declare global {',
...globals.map((global) => ` const ${global.name}: ${global.type};`),
'}',
].join('\n') + '\n',
);
return filePath;
}
async function writeMainDeclarationFile(references: string[]): Promise<string> {
const dir = wxt.config.wxtDir;
async function writeMainDeclarationFile(
references: string[],
config: InternalConfig,
): Promise<string> {
const dir = config.wxtDir;
const filePath = resolve(dir, 'wxt.d.ts');
await writeFileIfDifferent(
filePath,
@@ -186,10 +213,13 @@ async function writeMainDeclarationFile(references: string[]): Promise<string> {
return filePath;
}
async function writeTsConfigFile(mainReference: string) {
const dir = wxt.config.wxtDir;
async function writeTsConfigFile(
mainReference: string,
config: InternalConfig,
) {
const dir = config.wxtDir;
const getTsconfigPath = (path: string) => normalizePath(relative(dir, path));
const paths = Object.entries(wxt.config.alias)
const paths = Object.entries(config.alias)
.flatMap(([alias, absolutePath]) => {
const aliasPath = getTsconfigPath(absolutePath);
return [
@@ -217,10 +247,10 @@ ${paths}
}
},
"include": [
"${getTsconfigPath(wxt.config.root)}/**/*",
"${getTsconfigPath(config.root)}/**/*",
"./${getTsconfigPath(mainReference)}"
],
"exclude": ["${getTsconfigPath(wxt.config.outBaseDir)}"]
"exclude": ["${getTsconfigPath(config.outBaseDir)}"]
}`,
);
}
@@ -1,7 +1,7 @@
import { loadConfig } from 'c12';
import {
InlineConfig,
ResolvedConfig,
InternalConfig,
UserConfig,
ConfigEnv,
UserManifestFn,
@@ -23,15 +23,15 @@ import { NullablyRequired } from '../types';
* Inline config always has priority over user config. Cli flags are passed as inline config if set.
* If unset, undefined is passed in, letting this function decide default values.
*/
export async function resolveConfig(
export async function getInternalConfig(
inlineConfig: InlineConfig,
command: 'build' | 'serve',
server?: WxtDevServer,
): Promise<ResolvedConfig> {
): Promise<InternalConfig> {
// Load user config
let userConfig: UserConfig = {};
let userConfigMetadata: ResolvedConfig['userConfigMetadata'] | undefined;
let userConfigMetadata: InternalConfig['userConfigMetadata'] | undefined;
if (inlineConfig.configFile !== false) {
const { config: loadedConfig, ...metadata } = await loadConfig<UserConfig>({
name: 'wxt',
@@ -72,14 +72,11 @@ export async function resolveConfig(
srcDir,
mergedConfig.entrypointsDir ?? 'entrypoints',
);
const filterEntrypoints = !!mergedConfig.filterEntrypoints?.length
? new Set(mergedConfig.filterEntrypoints)
: undefined;
const publicDir = path.resolve(srcDir, mergedConfig.publicDir ?? 'public');
const localesDir = path.resolve(srcDir, mergedConfig.localesDir ?? 'locales');
const typesDir = path.resolve(wxtDir, 'types');
const outBaseDir = path.resolve(root, mergedConfig.outDir ?? '.output');
const outDir = path.resolve(outBaseDir, `${browser}-mv${manifestVersion}`);
const reloadCommand = mergedConfig.dev?.reloadCommand ?? 'Alt+R';
const runnerConfig = await loadConfig<ExtensionRunnerConfig>({
name: 'web-ext',
@@ -100,12 +97,11 @@ export async function resolveConfig(
}).map(([key, value]) => [key, path.resolve(root, value)]),
);
const finalConfig: Omit<ResolvedConfig, 'builder'> = {
const finalConfig: Omit<InternalConfig, 'builder'> = {
browser,
command,
debug,
entrypointsDir,
filterEntrypoints,
env,
fsCache: createFsCache(wxtDir),
imports: mergedConfig.imports ?? {},
@@ -116,6 +112,7 @@ export async function resolveConfig(
outBaseDir,
outDir,
publicDir,
localesDir,
root,
runnerConfig,
srcDir,
@@ -137,10 +134,6 @@ export async function resolveConfig(
mergedConfig.experimental?.includeBrowserPolyfill ?? true,
},
server,
dev: {
reloadCommand,
},
hooks: mergedConfig.hooks ?? {},
};
const builder = await createViteBuilder(
@@ -192,10 +185,6 @@ function mergeInlineConfig(
inlineConfig.zip ?? {},
userConfig.zip ?? {},
);
const hooks: InlineConfig['hooks'] = defu(
inlineConfig.hooks ?? {},
userConfig.hooks ?? {},
);
return {
root: inlineConfig.root ?? userConfig.root,
@@ -204,13 +193,12 @@ function mergeInlineConfig(
configFile: inlineConfig.configFile,
debug: inlineConfig.debug ?? userConfig.debug,
entrypointsDir: inlineConfig.entrypointsDir ?? userConfig.entrypointsDir,
filterEntrypoints:
inlineConfig.filterEntrypoints ?? userConfig.filterEntrypoints,
imports,
logger: inlineConfig.logger ?? userConfig.logger,
manifest,
mode: inlineConfig.mode ?? userConfig.mode,
publicDir: inlineConfig.publicDir ?? userConfig.publicDir,
localesDir: inlineConfig.localesDir ?? userConfig.localesDir,
runner,
srcDir: inlineConfig.srcDir ?? userConfig.srcDir,
outDir: inlineConfig.outDir ?? userConfig.outDir,
@@ -230,26 +218,20 @@ function mergeInlineConfig(
},
vite: undefined,
transformManifest: undefined,
dev: {
...userConfig.dev,
...inlineConfig.dev,
},
hooks,
};
}
function resolveInternalZipConfig(
root: string,
mergedConfig: InlineConfig,
): NullablyRequired<ResolvedConfig['zip']> {
): NullablyRequired<InternalConfig['zip']> {
return {
name: undefined,
sourcesTemplate: '{{name}}-{{version}}-sources.zip',
artifactTemplate: '{{name}}-{{version}}-{{browser}}.zip',
sourcesRoot: root,
includeSources: [],
...mergedConfig.zip,
excludeSources: [
ignoredSources: [
'**/node_modules',
// WXT files
'**/web-ext.config.ts',
@@ -259,7 +241,7 @@ function resolveInternalZipConfig(
'**/__tests__/**',
'**/*.+(test|spec).?(c|m)+(j|t)s?(x)',
// From user
...(mergedConfig.zip?.excludeSources ?? []),
...(mergedConfig.zip?.ignoredSources ?? []),
],
};
}
+17 -20
View File
@@ -11,11 +11,8 @@ export function groupEntrypoints(entrypoints: Entrypoint[]): EntrypointGroup[] {
const groups: EntrypointGroup[] = [];
for (const entry of entrypoints) {
let group = ENTRY_TYPE_TO_GROUP_MAP[entry.type];
if (entry.type === 'background' && entry.options.type === 'module') {
group = 'esm';
}
if (group === 'individual') {
const group = ENTRY_TYPE_TO_GROUP_MAP[entry.type];
if (group === 'no-group') {
groups.push(entry);
} else {
let groupIndex = groupIndexMap[group];
@@ -31,22 +28,22 @@ export function groupEntrypoints(entrypoints: Entrypoint[]): EntrypointGroup[] {
}
const ENTRY_TYPE_TO_GROUP_MAP: Record<Entrypoint['type'], Group> = {
sandbox: 'sandboxed-esm',
sandbox: 'sandbox-page',
popup: 'esm',
newtab: 'esm',
history: 'esm',
options: 'esm',
devtools: 'esm',
bookmarks: 'esm',
sidepanel: 'esm',
'unlisted-page': 'esm',
popup: 'extension-page',
newtab: 'extension-page',
history: 'extension-page',
options: 'extension-page',
devtools: 'extension-page',
bookmarks: 'extension-page',
sidepanel: 'extension-page',
'unlisted-page': 'extension-page',
background: 'individual',
'content-script': 'individual',
'unlisted-script': 'individual',
'unlisted-style': 'individual',
'content-script-style': 'individual',
background: 'no-group',
'content-script': 'no-group',
'unlisted-script': 'no-group',
'unlisted-style': 'no-group',
'content-script-style': 'no-group',
};
type Group = 'esm' | 'sandboxed-esm' | 'individual';
type Group = 'extension-page' | 'sandbox-page' | 'no-group';
+13 -20
View File
@@ -1,13 +1,13 @@
import createJITI, { TransformOptions as JitiTransformOptions } from 'jiti';
import { InternalConfig } from '~/types';
import { createUnimport } from 'unimport';
import fs from 'fs-extra';
import { relative, resolve } from 'node:path';
import { resolve } from 'node:path';
import { getUnimportOptions } from '~/core/utils/unimport';
import { removeProjectImportStatements } from '~/core/utils/strings';
import { normalizePath } from '~/core/utils/paths';
import { TransformOptions, transformSync } from 'esbuild';
import { fileURLToPath } from 'node:url';
import { wxt } from '../../wxt';
/**
* Get the value from the default export of a `path`.
@@ -24,13 +24,16 @@ import { wxt } from '../../wxt';
* Downside is that code cannot be executed outside of the main fucntion for the entrypoint,
* otherwise you will see "xxx is not defined" errors for any imports used outside of main function.
*/
export async function importEntrypointFile<T>(path: string): Promise<T> {
wxt.logger.debug('Loading file metadata:', path);
export async function importEntrypointFile<T>(
path: string,
config: InternalConfig,
): Promise<T> {
config.logger.debug('Loading file metadata:', path);
// JITI & Babel uses normalized paths.
const normalPath = normalizePath(path);
const unimport = createUnimport({
...getUnimportOptions(wxt.config),
...getUnimportOptions(config),
// Only allow specific imports, not all from the project
dirs: [],
});
@@ -39,7 +42,7 @@ export async function importEntrypointFile<T>(path: string): Promise<T> {
const text = await fs.readFile(path, 'utf-8');
const textNoImports = removeProjectImportStatements(text);
const { code } = await unimport.injectImports(textNoImports);
wxt.logger.debug(
config.logger.debug(
['Text:', text, 'No imports:', textNoImports, 'Code:', code].join('\n'),
);
@@ -49,11 +52,11 @@ export async function importEntrypointFile<T>(path: string): Promise<T> {
: fileURLToPath(import.meta.url),
{
cache: false,
debug: wxt.config.debug,
debug: config.debug,
esmResolve: true,
alias: {
'webextension-polyfill': resolve(
wxt.config.root,
config.root,
'node_modules/wxt/dist/virtual/mock-browser.js',
),
},
@@ -86,18 +89,8 @@ export async function importEntrypointFile<T>(path: string): Promise<T> {
const res = await jiti(path);
return res.default;
} catch (err) {
const filePath = relative(wxt.config.root, path);
if (err instanceof ReferenceError) {
// "XXX is not defined" - usually due to WXT removing imports
const variableName = err.message.replace(' is not defined', '');
throw Error(
`${filePath}: Cannot use imported variable "${variableName}" outside the main function. See https://wxt.dev/guide/entrypoints.html#side-effects`,
{ cause: err },
);
} else {
wxt.logger.error(err);
throw Error(`Failed to load entrypoint: ${filePath}`, { cause: err });
}
config.logger.error(err);
throw err;
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ export * from './build-entrypoints';
export * from './detect-dev-changes';
export * from './find-entrypoints';
export * from './generate-wxt-dir';
export * from './resolve-config';
export * from './get-internal-config';
export * from './group-entrypoints';
export * from './import-entrypoint';
export * from './internal-build';
+27 -83
View File
@@ -1,5 +1,5 @@
import { findEntrypoints } from './find-entrypoints';
import { BuildOutput, Entrypoint } from '~/types';
import { InternalConfig, BuildOutput } from '~/types';
import pc from 'picocolors';
import fs from 'fs-extra';
import { groupEntrypoints } from './group-entrypoints';
@@ -8,16 +8,6 @@ import { printBuildSummary } from '~/core/utils/log';
import glob from 'fast-glob';
import { unnormalizePath } from '~/core/utils/paths';
import { rebuild } from './rebuild';
import { relative } from 'node:path';
import {
ValidationError,
ValidationResult,
ValidationResults,
validateEntrypoints,
} from '../validation';
import consola from 'consola';
import { exec } from '../exec';
import { wxt } from '../../wxt';
/**
* Builds the extension based on an internal config. No more config discovery is performed, the
@@ -29,55 +19,38 @@ import { wxt } from '../../wxt';
* `rebuild` for more details)
* 3. Prints the summary
*/
export async function internalBuild(): Promise<BuildOutput> {
await wxt.hooks.callHook('build:before', wxt);
const verb = wxt.config.command === 'serve' ? 'Pre-rendering' : 'Building';
const target = `${wxt.config.browser}-mv${wxt.config.manifestVersion}`;
wxt.logger.info(
`${verb} ${pc.cyan(target)} for ${pc.cyan(wxt.config.mode)} with ${pc.green(
`${wxt.config.builder.name} ${wxt.config.builder.version}`,
export async function internalBuild(
config: InternalConfig,
): Promise<BuildOutput> {
const verb = config.command === 'serve' ? 'Pre-rendering' : 'Building';
const target = `${config.browser}-mv${config.manifestVersion}`;
config.logger.info(
`${verb} ${pc.cyan(target)} for ${pc.cyan(config.mode)} with ${pc.green(
`${config.builder.name} ${config.builder.version}`,
)}`,
);
const startTime = Date.now();
// Cleanup
await fs.rm(wxt.config.outDir, { recursive: true, force: true });
await fs.ensureDir(wxt.config.outDir);
const entrypoints = await findEntrypoints();
wxt.logger.debug('Detected entrypoints:', entrypoints);
const validationResults = validateEntrypoints(entrypoints);
if (validationResults.errorCount + validationResults.warningCount > 0) {
printValidationResults(validationResults);
}
if (validationResults.errorCount > 0) {
throw new ValidationError(`Entrypoint validation failed`, {
cause: validationResults,
});
}
await fs.rm(config.outDir, { recursive: true, force: true });
await fs.ensureDir(config.outDir);
const entrypoints = await findEntrypoints(config);
config.logger.debug('Detected entrypoints:', entrypoints);
const groups = groupEntrypoints(entrypoints);
await wxt.hooks.callHook('entrypoints:grouped', wxt, groups);
const { output, warnings } = await rebuild(entrypoints, groups, undefined);
await wxt.hooks.callHook('build:done', wxt, output);
const { output } = await rebuild(config, entrypoints, groups, undefined);
// Post-build
await printBuildSummary(
wxt.logger.success,
config.logger.success,
`Built extension in ${formatDuration(Date.now() - startTime)}`,
output,
config,
);
for (const warning of warnings) {
wxt.logger.warn(...warning);
}
if (wxt.config.analysis.enabled) {
await combineAnalysisStats();
wxt.logger.info(
if (config.analysis.enabled) {
await combineAnalysisStats(config);
config.logger.info(
`Analysis complete:\n ${pc.gray('└─')} ${pc.yellow('stats.html')}`,
);
}
@@ -85,47 +58,18 @@ export async function internalBuild(): Promise<BuildOutput> {
return output;
}
async function combineAnalysisStats(): Promise<void> {
async function combineAnalysisStats(config: InternalConfig): Promise<void> {
const { execaCommand } = await import('execa');
const unixFiles = await glob(`stats-*.json`, {
cwd: wxt.config.outDir,
cwd: config.outDir,
absolute: true,
});
const absolutePaths = unixFiles.map(unnormalizePath);
await exec(
'rollup-plugin-visualizer',
[...absolutePaths, '--template', wxt.config.analysis.template],
{ cwd: wxt.config.root, stdio: 'inherit' },
await execaCommand(
`rollup-plugin-visualizer ${absolutePaths.join(' ')} --template ${
config.analysis.template
}`,
{ cwd: config.root, stdio: 'inherit' },
);
}
function printValidationResults({
errorCount,
errors,
warningCount,
}: ValidationResults) {
(errorCount > 0 ? wxt.logger.error : wxt.logger.warn)(
`Entrypoint validation failed: ${errorCount} error${
errorCount === 1 ? '' : 's'
}, ${warningCount} warning${warningCount === 1 ? '' : 's'}`,
);
const cwd = process.cwd();
const entrypointErrors = errors.reduce((map, error) => {
const entryErrors = map.get(error.entrypoint) ?? [];
entryErrors.push(error);
map.set(error.entrypoint, entryErrors);
return map;
}, new Map<Entrypoint, ValidationResult[]>());
Array.from(entrypointErrors.entries()).forEach(([entrypoint, errors]) => {
consola.log(relative(cwd, entrypoint.inputPath));
console.log();
errors.forEach((err) => {
const type = err.type === 'error' ? pc.red('ERROR') : pc.yellow('WARN');
const recieved = pc.dim(`(recieved: ${JSON.stringify(err.value)})`);
consola.log(` - ${type} ${err.message} ${recieved}`);
});
console.log();
});
}
+18 -15
View File
@@ -1,9 +1,13 @@
import type { Manifest } from '~/browser';
import { BuildOutput, Entrypoint, EntrypointGroup } from '~/types';
import {
BuildOutput,
Entrypoint,
EntrypointGroup,
InternalConfig,
} from '~/types';
import { generateTypesDir } from './generate-wxt-dir';
import { buildEntrypoints } from './build-entrypoints';
import { generateManifest, writeManifest } from '~/core/utils/manifest';
import { wxt } from '../../wxt';
/**
* Given a configuration, list of entrypoints, and an existing, partial output, build the
@@ -22,43 +26,43 @@ import { wxt } from '../../wxt';
* this is the first build.
*/
export async function rebuild(
config: InternalConfig,
allEntrypoints: Entrypoint[],
entrypointGroups: EntrypointGroup[],
existingOutput: Omit<BuildOutput, 'manifest'> = {
steps: [],
publicAssets: [],
},
): Promise<{
output: BuildOutput;
manifest: Manifest.WebExtensionManifest;
warnings: any[][];
}> {
): Promise<{ output: BuildOutput; manifest: Manifest.WebExtensionManifest }> {
const { default: ora } = await import('ora');
const spinner = ora(`Preparing...`).start();
// Update types directory with new files and types
await generateTypesDir(allEntrypoints).catch((err) => {
wxt.logger.warn('Failed to update .wxt directory:', err);
await generateTypesDir(allEntrypoints, config).catch((err) => {
config.logger.warn('Failed to update .wxt directory:', err);
// Throw the error if doing a regular build, don't for dev mode.
if (wxt.config.command === 'build') throw err;
if (config.command === 'build') throw err;
});
// Build and merge the outputs
const newOutput = await buildEntrypoints(entrypointGroups, spinner);
const newOutput = await buildEntrypoints(entrypointGroups, config, spinner);
const mergedOutput: Omit<BuildOutput, 'manifest'> = {
steps: [...existingOutput.steps, ...newOutput.steps],
publicAssets: [...existingOutput.publicAssets, ...newOutput.publicAssets],
};
const { manifest: newManifest, warnings: manifestWarnings } =
await generateManifest(allEntrypoints, mergedOutput);
const newManifest = await generateManifest(
allEntrypoints,
mergedOutput,
config,
);
const finalOutput: BuildOutput = {
manifest: newManifest,
...newOutput,
};
// Write manifest
await writeManifest(newManifest, finalOutput);
await writeManifest(newManifest, finalOutput, config);
// Stop the spinner and remove it from the CLI output
spinner.clear().stop();
@@ -73,6 +77,5 @@ export async function rebuild(
],
},
manifest: newManifest,
warnings: manifestWarnings,
};
}
+12 -11
View File
@@ -1,7 +1,6 @@
import type { Manifest } from '~/browser';
import { ContentScriptEntrypoint } from '~/types';
import { ContentScriptEntrypoint, InternalConfig } from '~/types';
import { resolvePerBrowserOption } from './entrypoints';
import { wxt } from '../wxt';
/**
* Returns a unique and consistent string hash based on a content scripts options.
@@ -10,8 +9,9 @@ import { wxt } from '../wxt';
*/
export function hashContentScriptOptions(
options: ContentScriptEntrypoint['options'],
config: InternalConfig,
): string {
const simplifiedOptions = mapWxtOptionsToContentScript(options);
const simplifiedOptions = mapWxtOptionsToContentScript(options, config);
// Remove undefined fields and use defaults to generate hash
Object.keys(simplifiedOptions).forEach((key) => {
@@ -45,32 +45,33 @@ export function hashContentScriptOptions(
export function mapWxtOptionsToContentScript(
options: ContentScriptEntrypoint['options'],
config: InternalConfig,
): Omit<Manifest.ContentScript, 'js' | 'css'> {
return {
matches: resolvePerBrowserOption(options.matches, wxt.config.browser),
all_frames: resolvePerBrowserOption(options.allFrames, wxt.config.browser),
matches: resolvePerBrowserOption(options.matches, config.browser),
all_frames: resolvePerBrowserOption(options.allFrames, config.browser),
match_about_blank: resolvePerBrowserOption(
options.matchAboutBlank,
wxt.config.browser,
config.browser,
),
exclude_globs: resolvePerBrowserOption(
options.excludeGlobs,
wxt.config.browser,
config.browser,
),
exclude_matches: resolvePerBrowserOption(
options.excludeMatches,
wxt.config.browser,
config.browser,
),
include_globs: resolvePerBrowserOption(
options.includeGlobs,
wxt.config.browser,
config.browser,
),
run_at: resolvePerBrowserOption(options.runAt, wxt.config.browser),
run_at: resolvePerBrowserOption(options.runAt, config.browser),
// @ts-expect-error: untyped chrome options
match_origin_as_fallback: resolvePerBrowserOption(
options.matchOriginAsFallback,
wxt.config.browser,
config.browser,
),
world: options.world,
};
-26
View File
@@ -1,26 +0,0 @@
import type { Options } from 'execa';
import managePath from 'manage-path';
import { resolve } from 'node:path';
import { wxt } from '../wxt';
const managedPath = managePath(process.env);
/**
* Wrapper around `execa` with a modified `PATH` variable containing CLI tools from WXT's dependencies.
*/
export const exec = async (
file: string,
args?: readonly string[],
options?: Options,
) => {
// Reset so the same path isn't added multiple times
managedPath.restore();
// Add subdependency path for PNPM shamefully-hoist=false
managedPath.push(
resolve(wxt.config.root, 'node_modules/wxt/node_modules/.bin'),
);
const { execa } = await import('execa');
return await execa(file, args, options);
};
+6 -4
View File
@@ -1,7 +1,7 @@
import { InternalConfig } from '~/types';
import fs from 'fs-extra';
import glob from 'fast-glob';
import { unnormalizePath } from './paths';
import { wxt } from '../wxt';
/**
* Only write the contents to a file if it results in a change. This prevents unnecessary file
@@ -27,9 +27,11 @@ export async function writeFileIfDifferent(
* Get all the files in the project's public directory. Returned paths are relative to the
* `config.publicDir`.
*/
export async function getPublicFiles(): Promise<string[]> {
if (!(await fs.exists(wxt.config.publicDir))) return [];
export async function getPublicFiles(
config: InternalConfig,
): Promise<string[]> {
if (!(await fs.exists(config.publicDir))) return [];
const files = await glob('**/*', { cwd: wxt.config.publicDir });
const files = await glob('**/*', { cwd: config.publicDir });
return files.map(unnormalizePath);
}
+19 -11
View File
@@ -1,46 +1,46 @@
import { ResolvedConfig } from '~/types';
import { InternalConfig } from '~/types';
export function getGlobals(
config: Omit<ResolvedConfig, 'builder'>,
config: Omit<InternalConfig, 'builder'>,
): Array<{ name: string; value: any; type: string }> {
return [
{
name: 'MANIFEST_VERSION',
name: surroundInUnderscore('MANIFEST_VERSION'),
value: config.manifestVersion,
type: `2 | 3`,
},
{
name: 'BROWSER',
name: surroundInUnderscore('BROWSER'),
value: config.browser,
type: `string`,
},
{
name: 'CHROME',
name: surroundInUnderscore('IS_CHROME'),
value: config.browser === 'chrome',
type: `boolean`,
},
{
name: 'FIREFOX',
name: surroundInUnderscore('IS_FIREFOX'),
value: config.browser === 'firefox',
type: `boolean`,
},
{
name: 'SAFARI',
name: surroundInUnderscore('IS_SAFARI'),
value: config.browser === 'safari',
type: `boolean`,
},
{
name: 'EDGE',
name: surroundInUnderscore('IS_EDGE'),
value: config.browser === 'edge',
type: `boolean`,
},
{
name: 'OPERA',
name: surroundInUnderscore('IS_OPERA'),
value: config.browser === 'opera',
type: `boolean`,
},
{
name: 'COMMAND',
name: surroundInUnderscore('COMMAND'),
value: config.command,
type: `"build" | "serve"`,
},
@@ -50,9 +50,17 @@ export function getGlobals(
export function getEntrypointGlobals(entrypointName: string) {
return [
{
name: 'ENTRYPOINT',
name: surroundInUnderscore('ENTRYPOINT'),
value: entrypointName,
type: `string`,
},
];
}
/**
* Don't hardcode the complete name so that the string litterals in this file aren't replaced during
* tests (which causes syntax errors), only during builds.
*/
function surroundInUnderscore(name: string): string {
return `__${name}__`;
}
-52
View File
@@ -1,52 +0,0 @@
export interface Message {
name: string;
message: string;
description?: string;
}
const predefinedMessages = {
'@@extension_id': {
message: '<browser.runtime.id>',
description:
"The extension or app ID; you might use this string to construct URLs for resources inside the extension. Even unlocalized extensions can use this message.\nNote: You can't use this message in a manifest file.",
},
'@@ui_locale': {
message: '<browser.i18n.getUiLocale()>',
description: '',
},
'@@bidi_dir': {
message: '<ltr|rtl>',
description:
'The text direction for the current locale, either "ltr" for left-to-right languages such as English or "rtl" for right-to-left languages such as Japanese.',
},
'@@bidi_reversed_dir': {
message: '<rtl|ltr>',
description:
'If the @@bidi_dir is "ltr", then this is "rtl"; otherwise, it\'s "ltr".',
},
'@@bidi_start_edge': {
message: '<left|right>',
description:
'If the @@bidi_dir is "ltr", then this is "left"; otherwise, it\'s "right".',
},
'@@bidi_end_edge': {
message: '<right|left>',
description:
'If the @@bidi_dir is "ltr", then this is "right"; otherwise, it\'s "left".',
},
};
/**
* Get a list of all messages and their metadata from JSON file contents.
*
* @param messagesJson The contents of a `_locales/en/messages.json` file.
*/
export function parseI18nMessages(messagesJson: object): Message[] {
return Object.entries({
...predefinedMessages,
...messagesJson,
}).map<Message>(([name, details]) => ({
name,
...details,
}));
}
+4 -6
View File
@@ -1,12 +1,12 @@
import { resolve } from 'path';
import { BuildOutput } from '~/types';
import { BuildOutput, InternalConfig } from '~/types';
import { printFileList } from './printFileList';
import { wxt } from '../../wxt';
export async function printBuildSummary(
log: (...args: any[]) => void,
header: string,
output: BuildOutput,
config: InternalConfig,
) {
const chunks = [
...output.steps.flatMap((step) => step.chunks),
@@ -19,10 +19,8 @@ export async function printBuildSummary(
return l.fileName.localeCompare(r.fileName);
});
const files = chunks.map((chunk) =>
resolve(wxt.config.outDir, chunk.fileName),
);
await printFileList(log, header, wxt.config.outDir, files);
const files = chunks.map((chunk) => resolve(config.outDir, chunk.fileName));
await printFileList(log, header, config.outDir, files);
}
const DEFAULT_SORT_WEIGHT = 100;
+73 -84
View File
@@ -4,6 +4,7 @@ import {
BackgroundEntrypoint,
BuildOutput,
ContentScriptEntrypoint,
InternalConfig,
OptionsEntrypoint,
PopupEntrypoint,
} from '~/types';
@@ -23,7 +24,6 @@ import { normalizePath } from './paths';
import { writeFileIfDifferent } from './fs';
import { produce } from 'immer';
import defu from 'defu';
import { wxt } from '../wxt';
/**
* Writes the manifest to the output directory and the build output.
@@ -31,14 +31,15 @@ import { wxt } from '../wxt';
export async function writeManifest(
manifest: Manifest.WebExtensionManifest,
output: BuildOutput,
config: InternalConfig,
): Promise<void> {
const str =
wxt.config.mode === 'production'
config.mode === 'production'
? JSON.stringify(manifest)
: JSON.stringify(manifest, null, 2);
await fs.ensureDir(wxt.config.outDir);
await writeFileIfDifferent(resolve(wxt.config.outDir, 'manifest.json'), str);
await fs.ensureDir(config.outDir);
await writeFileIfDifferent(resolve(config.outDir, 'manifest.json'), str);
output.publicAssets.unshift({
type: 'asset',
@@ -52,69 +53,59 @@ export async function writeManifest(
export async function generateManifest(
entrypoints: Entrypoint[],
buildOutput: Omit<BuildOutput, 'manifest'>,
): Promise<{ manifest: Manifest.WebExtensionManifest; warnings: any[][] }> {
const warnings: any[][] = [];
const pkg = await getPackageJson();
config: InternalConfig,
): Promise<Manifest.WebExtensionManifest> {
const pkg = await getPackageJson(config);
let versionName =
wxt.config.manifest.version_name ??
wxt.config.manifest.version ??
pkg?.version;
config.manifest.version_name ?? config.manifest.version ?? pkg?.version;
if (versionName == null) {
versionName = '0.0.0';
wxt.logger.warn(
config.logger.warn(
'Extension version not found, defaulting to "0.0.0". Add a version to your `package.json` or `wxt.config.ts` file. For more details, see: https://wxt.dev/guide/manifest.html#version-and-version-name',
);
}
const version = wxt.config.manifest.version ?? simplifyVersion(versionName);
const version = config.manifest.version ?? simplifyVersion(versionName);
const baseManifest: Manifest.WebExtensionManifest = {
manifest_version: wxt.config.manifestVersion,
manifest_version: config.manifestVersion,
name: pkg?.name,
description: pkg?.description,
version,
short_name: pkg?.shortName,
icons: discoverIcons(buildOutput),
};
const userManifest = wxt.config.manifest;
if (config.command === 'serve') {
baseManifest.commands = {
'wxt:reload-extension': {
description: 'Reload the extension during development',
suggested_key: {
default: 'Alt+R',
},
},
};
}
const userManifest = config.manifest;
const manifest = defu(
userManifest,
baseManifest,
) as Manifest.WebExtensionManifest;
// Add reload command in dev mode
if (wxt.config.command === 'serve' && wxt.config.dev.reloadCommand) {
if (manifest.commands && Object.keys(manifest.commands).length >= 4) {
warnings.push([
"Extension already has 4 registered commands, WXT's reload command is disabled",
]);
} else {
manifest.commands ??= {};
manifest.commands['wxt:reload-extension'] = {
description: 'Reload the extension during development',
suggested_key: {
default: wxt.config.dev.reloadCommand,
},
};
}
}
// Apply the final version fields after merging the user manifest
manifest.version = version;
manifest.version_name =
// Firefox doesn't support version_name
wxt.config.browser === 'firefox' || versionName === version
config.browser === 'firefox' || versionName === version
? undefined
: versionName;
addEntrypoints(manifest, entrypoints, buildOutput);
addEntrypoints(manifest, entrypoints, buildOutput, config);
if (wxt.config.command === 'serve') addDevModeCsp(manifest);
if (wxt.config.command === 'serve') addDevModePermissions(manifest);
if (config.command === 'serve') addDevModeCsp(manifest, config);
if (config.command === 'serve') addDevModePermissions(manifest, config);
const finalManifest = produce(manifest, wxt.config.transformManifest);
await wxt.hooks.callHook('build:manifestGenerated', wxt, finalManifest);
const finalManifest = produce(manifest, config.transformManifest);
if (finalManifest.name == null)
throw Error(
@@ -126,10 +117,7 @@ export async function generateManifest(
);
}
return {
manifest: finalManifest,
warnings,
};
return finalManifest;
}
/**
@@ -154,6 +142,7 @@ function addEntrypoints(
manifest: Manifest.WebExtensionManifest,
entrypoints: Entrypoint[],
buildOutput: Omit<BuildOutput, 'manifest'>,
config: InternalConfig,
): void {
const entriesByType = entrypoints.reduce<
Partial<Record<Entrypoint['type'], Entrypoint[]>>
@@ -181,17 +170,13 @@ function addEntrypoints(
const sidepanels = entriesByType['sidepanel'];
if (background) {
const script = getEntrypointBundlePath(
background,
wxt.config.outDir,
'.js',
);
if (wxt.config.browser === 'firefox' && wxt.config.manifestVersion === 3) {
const script = getEntrypointBundlePath(background, config.outDir, '.js');
if (config.browser === 'firefox' && config.manifestVersion === 3) {
manifest.background = {
type: background.options.type,
scripts: [script],
};
} else if (wxt.config.manifestVersion === 3) {
} else if (config.manifestVersion === 3) {
manifest.background = {
type: background.options.type,
service_worker: script,
@@ -205,8 +190,8 @@ function addEntrypoints(
}
if (bookmarks) {
if (wxt.config.browser === 'firefox') {
wxt.logger.warn(
if (config.browser === 'firefox') {
config.logger.warn(
'Bookmarks are not supported by Firefox. chrome_url_overrides.bookmarks was not added to the manifest',
);
} else {
@@ -214,15 +199,15 @@ function addEntrypoints(
// @ts-expect-error: bookmarks is untyped in webextension-polyfill, but supported by chrome
manifest.chrome_url_overrides.bookmarks = getEntrypointBundlePath(
bookmarks,
wxt.config.outDir,
config.outDir,
'.html',
);
}
}
if (history) {
if (wxt.config.browser === 'firefox') {
wxt.logger.warn(
if (config.browser === 'firefox') {
config.logger.warn(
'Bookmarks are not supported by Firefox. chrome_url_overrides.history was not added to the manifest',
);
} else {
@@ -230,7 +215,7 @@ function addEntrypoints(
// @ts-expect-error: history is untyped in webextension-polyfill, but supported by chrome
manifest.chrome_url_overrides.history = getEntrypointBundlePath(
history,
wxt.config.outDir,
config.outDir,
'.html',
);
}
@@ -240,7 +225,7 @@ function addEntrypoints(
manifest.chrome_url_overrides ??= {};
manifest.chrome_url_overrides.newtab = getEntrypointBundlePath(
newtab,
wxt.config.outDir,
config.outDir,
'.html',
);
}
@@ -248,7 +233,7 @@ function addEntrypoints(
if (popup) {
const default_popup = getEntrypointBundlePath(
popup,
wxt.config.outDir,
config.outDir,
'.html',
);
const options: Manifest.ActionManifest = {};
@@ -277,37 +262,33 @@ function addEntrypoints(
if (devtools) {
manifest.devtools_page = getEntrypointBundlePath(
devtools,
wxt.config.outDir,
config.outDir,
'.html',
);
}
if (options) {
const page = getEntrypointBundlePath(options, wxt.config.outDir, '.html');
const page = getEntrypointBundlePath(options, config.outDir, '.html');
manifest.options_ui = {
open_in_tab: options.options.openInTab,
browser_style:
wxt.config.browser === 'firefox'
? options.options.browserStyle
: undefined,
config.browser === 'firefox' ? options.options.browserStyle : undefined,
chrome_style:
wxt.config.browser !== 'firefox'
? options.options.chromeStyle
: undefined,
config.browser !== 'firefox' ? options.options.chromeStyle : undefined,
page,
};
}
if (sandboxes?.length) {
if (wxt.config.browser === 'firefox') {
wxt.logger.warn(
if (config.browser === 'firefox') {
config.logger.warn(
'Sandboxed pages not supported by Firefox. sandbox.pages was not added to the manifest',
);
} else {
// @ts-expect-error: sandbox not typed
manifest.sandbox = {
pages: sandboxes.map((entry) =>
getEntrypointBundlePath(entry, wxt.config.outDir, '.html'),
getEntrypointBundlePath(entry, config.outDir, '.html'),
),
};
}
@@ -318,23 +299,23 @@ function addEntrypoints(
sidepanels.find((entry) => entry.name === 'sidepanel') ?? sidepanels[0];
const page = getEntrypointBundlePath(
defaultSidepanel,
wxt.config.outDir,
config.outDir,
'.html',
);
if (wxt.config.browser === 'firefox') {
if (config.browser === 'firefox') {
manifest.sidebar_action = {
// TODO: Add options to side panel
// ...defaultSidepanel.options,
default_panel: page,
};
} else if (wxt.config.manifestVersion === 3) {
} else if (config.manifestVersion === 3) {
// @ts-expect-error: Untyped
manifest.side_panel = {
default_path: page,
};
} else {
wxt.logger.warn(
config.logger.warn(
'Side panel not supported by Chromium using MV2. side_panel.default_path was not added to the manifest',
);
}
@@ -345,12 +326,12 @@ function addEntrypoints(
// Don't add content scripts to the manifest in dev mode for MV3 - they're managed and reloaded
// at runtime
if (wxt.config.command === 'serve' && wxt.config.manifestVersion === 3) {
if (config.command === 'serve' && config.manifestVersion === 3) {
const hostPermissions = new Set<string>(manifest.host_permissions ?? []);
contentScripts.forEach((script) => {
const matches = resolvePerBrowserOption(
script.options.matches,
wxt.config.browser,
config.browser,
);
matches.forEach((matchPattern) => {
hostPermissions.add(matchPattern);
@@ -361,7 +342,7 @@ function addEntrypoints(
);
} else {
const hashToEntrypointsMap = contentScripts.reduce((map, script) => {
const hash = hashContentScriptOptions(script.options);
const hash = hashContentScriptOptions(script.options, config);
if (map.has(hash)) map.get(hash)?.push(script);
else map.set(hash, [script]);
return map;
@@ -369,10 +350,10 @@ function addEntrypoints(
const newContentScripts = Array.from(hashToEntrypointsMap.entries()).map(
([, scripts]) => ({
...mapWxtOptionsToContentScript(scripts[0].options),
...mapWxtOptionsToContentScript(scripts[0].options, config),
css: getContentScriptCssFiles(scripts, cssMap),
js: scripts.map((entry) =>
getEntrypointBundlePath(entry, wxt.config.outDir, '.js'),
getEntrypointBundlePath(entry, config.outDir, '.js'),
),
}),
);
@@ -383,6 +364,7 @@ function addEntrypoints(
}
const contentScriptCssResources = getContentScriptCssWebAccessibleResources(
config,
contentScripts,
cssMap,
);
@@ -427,9 +409,12 @@ function discoverIcons(
return icons.length > 0 ? Object.fromEntries(icons) : undefined;
}
function addDevModeCsp(manifest: Manifest.WebExtensionManifest): void {
const permission = `http://${wxt.config.server?.hostname ?? ''}/*`;
const allowedCsp = wxt.config.server?.origin ?? 'http://localhost:*';
function addDevModeCsp(
manifest: Manifest.WebExtensionManifest,
config: InternalConfig,
): void {
const permission = `http://${config.server?.hostname ?? ''}/*`;
const allowedCsp = config.server?.origin ?? 'http://localhost:*';
if (manifest.manifest_version === 3) {
addHostPermission(manifest, permission);
@@ -446,7 +431,7 @@ function addDevModeCsp(manifest: Manifest.WebExtensionManifest): void {
"script-src 'self'; object-src 'self';", // default CSP for MV2
);
if (wxt.config.server) csp.add('script-src', allowedCsp);
if (config.server) csp.add('script-src', allowedCsp);
if (manifest.manifest_version === 3) {
manifest.content_security_policy ??= {};
@@ -457,12 +442,15 @@ function addDevModeCsp(manifest: Manifest.WebExtensionManifest): void {
}
}
function addDevModePermissions(manifest: Manifest.WebExtensionManifest) {
function addDevModePermissions(
manifest: Manifest.WebExtensionManifest,
config: InternalConfig,
) {
// For reloading the page
addPermission(manifest, 'tabs');
// For registering content scripts
if (wxt.config.manifestVersion === 3) addPermission(manifest, 'scripting');
if (config.manifestVersion === 3) addPermission(manifest, 'scripting');
}
/**
@@ -498,6 +486,7 @@ export function getContentScriptCssFiles(
* added to.
*/
export function getContentScriptCssWebAccessibleResources(
config: InternalConfig,
contentScripts: ContentScriptEntrypoint[],
contentScriptCssMap: Record<string, string | undefined>,
): any[] {
@@ -509,14 +498,14 @@ export function getContentScriptCssWebAccessibleResources(
const cssFile = contentScriptCssMap[script.name];
if (cssFile == null) return;
if (wxt.config.manifestVersion === 2) {
if (config.manifestVersion === 2) {
resources.push(cssFile);
} else {
resources.push({
resources: [cssFile],
matches: resolvePerBrowserOption(
script.options.matches,
wxt.config.browser,
config.browser,
).map((matchPattern) => stripPathFromMatchPattern(matchPattern)),
});
}
+2 -2
View File
@@ -1,5 +1,5 @@
import dns from 'node:dns';
import { ResolvedConfig } from '~/types';
import { InternalConfig } from '~/types';
import { withTimeout } from './time';
function isOffline(): Promise<boolean> {
@@ -26,7 +26,7 @@ export async function isOnline(): Promise<boolean> {
*/
export async function fetchCached(
url: string,
config: Omit<ResolvedConfig, 'builder'>,
config: Omit<InternalConfig, 'builder'>,
): Promise<string> {
let content: string = '';
+6 -6
View File
@@ -1,20 +1,20 @@
import { resolve } from 'node:path';
import { InternalConfig } from '~/types';
import fs from 'fs-extra';
import { wxt } from '../wxt';
/**
* Read the project's package.json.
*
* TODO: look in root and up directories until it's found
*/
export async function getPackageJson(): Promise<
Partial<Record<string, any>> | undefined
> {
const file = resolve(wxt.config.root, 'package.json');
export async function getPackageJson(
config: InternalConfig,
): Promise<Partial<Record<string, any>> | undefined> {
const file = resolve(config.root, 'package.json');
try {
return await fs.readJson(file);
} catch (err) {
wxt.logger.debug(
config.logger.debug(
`Failed to read package.json at: ${file}. Returning undefined.`,
);
return {};
+11 -65
View File
@@ -4,10 +4,10 @@
import { resolve } from 'path';
import { faker } from '@faker-js/faker';
import merge from 'lodash.merge';
import { Commands, type Manifest } from '~/browser';
import type { Manifest } from '~/browser';
import {
FsCache,
ResolvedConfig,
InternalConfig,
WxtDevServer,
BackgroundEntrypoint,
ContentScriptEntrypoint,
@@ -20,19 +20,12 @@ import {
BuildOutput,
BuildStepOutput,
UserManifest,
Wxt,
} from '~/types';
import { mock } from 'vitest-mock-extended';
import { vi } from 'vitest';
import { setWxtForTesting } from '~/core/wxt';
faker.seed(__TEST_SEED__);
type DeepPartial<T> = T extends object
? {
[P in keyof T]?: DeepPartial<T[P]>;
}
: T;
type DeepPartial<T> = { [key in keyof T]+?: Partial<T[key]> };
function fakeObjectCreator<T>(base: () => T) {
return (overrides?: DeepPartial<T>): T => merge(base(), overrides);
}
@@ -66,7 +59,7 @@ export const fakeContentScriptEntrypoint =
name: faker.string.alpha(),
options: {
matches: [],
matchAboutBlank: faker.datatype.boolean(),
matchAboutBlank: faker.helpers.arrayElement([true, false, undefined]),
matchOriginAsFallback: faker.helpers.arrayElement([
true,
false,
@@ -80,7 +73,6 @@ export const fakeContentScriptEntrypoint =
]),
},
outputDir: fakeDir('.output'),
skipped: faker.datatype.boolean(),
}));
export const fakeBackgroundEntrypoint = fakeObjectCreator<BackgroundEntrypoint>(
@@ -89,11 +81,10 @@ export const fakeBackgroundEntrypoint = fakeObjectCreator<BackgroundEntrypoint>(
inputPath: 'entrypoints/background.ts',
name: 'background',
options: {
persistent: faker.datatype.boolean(),
persistent: faker.helpers.arrayElement([true, false, undefined]),
type: faker.helpers.maybe(() => 'module'),
},
outputDir: fakeDir('.output'),
skipped: faker.datatype.boolean(),
}),
);
@@ -104,7 +95,6 @@ export const fakeUnlistedScriptEntrypoint =
name: faker.string.alpha(),
outputDir: fakeDir('.output'),
options: {},
skipped: faker.datatype.boolean(),
}));
export const fakeOptionsEntrypoint = fakeObjectCreator<OptionsEntrypoint>(
@@ -114,11 +104,10 @@ export const fakeOptionsEntrypoint = fakeObjectCreator<OptionsEntrypoint>(
name: 'options',
outputDir: fakeDir('.output'),
options: {
browserStyle: faker.datatype.boolean(),
chromeStyle: faker.datatype.boolean(),
openInTab: faker.datatype.boolean(),
browserStyle: faker.helpers.arrayElement([true, false, undefined]),
chromeStyle: faker.helpers.arrayElement([true, false, undefined]),
openInTab: faker.helpers.arrayElement([true, false, undefined]),
},
skipped: faker.datatype.boolean(),
}),
);
@@ -145,7 +134,6 @@ export const fakePopupEntrypoint = fakeObjectCreator<PopupEntrypoint>(() => ({
undefined,
]),
},
skipped: faker.datatype.boolean(),
}));
export const fakeGenericEntrypoint = fakeObjectCreator<GenericEntrypoint>(
@@ -164,7 +152,6 @@ export const fakeGenericEntrypoint = fakeObjectCreator<GenericEntrypoint>(
name: faker.string.alpha(),
outputDir: fakeDir('.output'),
options: {},
skipped: faker.datatype.boolean(),
}),
);
@@ -204,7 +191,7 @@ export function fakeArray<T>(createItem: () => T, count = 3): T[] {
return array;
}
export const fakeResolvedConfig = fakeObjectCreator<ResolvedConfig>(() => {
export const fakeInternalConfig = fakeObjectCreator<InternalConfig>(() => {
const browser = faker.helpers.arrayElement(['chrome', 'firefox']);
const command = faker.helpers.arrayElement(['build', 'serve'] as const);
const manifestVersion = faker.helpers.arrayElement([2, 3] as const);
@@ -224,6 +211,7 @@ export const fakeResolvedConfig = fakeObjectCreator<ResolvedConfig>(() => {
outBaseDir: fakeDir(),
outDir: fakeDir(),
publicDir: fakeDir(),
localesDir: fakeDir(),
root: fakeDir(),
runnerConfig: {
config: {},
@@ -239,8 +227,7 @@ export const fakeResolvedConfig = fakeObjectCreator<ResolvedConfig>(() => {
},
zip: {
artifactTemplate: '{{name}}-{{version}}.zip',
includeSources: [],
excludeSources: [],
ignoredSources: [],
sourcesRoot: fakeDir(),
sourcesTemplate: '{{name}}-sources.zip',
name: faker.person.firstName().toLowerCase(),
@@ -252,25 +239,9 @@ export const fakeResolvedConfig = fakeObjectCreator<ResolvedConfig>(() => {
includeBrowserPolyfill: true,
},
builder: mock(),
dev: {
reloadCommand: 'Alt+R',
},
hooks: {},
};
});
export const fakeWxt = fakeObjectCreator<Wxt>(() => ({
config: fakeResolvedConfig(),
hooks: mock(),
logger: mock(),
reloadConfig: vi.fn(),
}));
export function setFakeWxt(overrides?: DeepPartial<Wxt>) {
const wxt = fakeWxt(overrides);
setWxtForTesting(wxt);
}
export const fakeBuildOutput = fakeObjectCreator<BuildOutput>(() => ({
manifest: fakeManifest(),
publicAssets: fakeArray(fakeOutputAsset),
@@ -281,28 +252,3 @@ export const fakeBuildStepOutput = fakeObjectCreator<BuildStepOutput>(() => ({
chunks: fakeArray(fakeOutputChunk),
entrypoints: fakeArray(fakeEntrypoint),
}));
export const fakeManifestCommand = fakeObjectCreator<Commands.Command>(() => ({
description: faker.string.sample(),
shortcut: `${faker.helpers.arrayElement(['ctrl', 'alt'])}+${faker.number.int({
min: 0,
max: 9,
})}`,
}));
export const fakeDevServer = fakeObjectCreator<WxtDevServer>(() => ({
hostname: 'localhost',
origin: 'http://localhost',
port: 5173,
reloadContentScript: vi.fn(),
reloadExtension: vi.fn(),
reloadPage: vi.fn(),
restart: vi.fn(),
restartBrowser: vi.fn(),
stop: vi.fn(),
start: vi.fn(),
watcher: mock(),
transformHtml: vi.fn(),
ws: mock(),
currentOutput: undefined,
}));
+3 -2
View File
@@ -1,9 +1,9 @@
import { UnimportOptions } from 'unimport';
import { ResolvedConfig } from '~/types';
import { InternalConfig } from '~/types';
import { defu } from 'defu';
export function getUnimportOptions(
config: Omit<ResolvedConfig, 'builder'>,
config: Omit<InternalConfig, 'builder'>,
): Partial<UnimportOptions | false> {
if (config.imports === false) return false;
@@ -18,6 +18,7 @@ export function getUnimportOptions(
{ package: 'wxt/browser' },
{ package: 'wxt/sandbox' },
{ package: 'wxt/storage' },
{ package: 'wxt/i18n' },
],
warn: config.logger.warn,
dirs: ['components', 'composables', 'hooks', 'utils'],

Some files were not shown because too many files have changed in this diff Show More