Compare commits

..

41 Commits

Author SHA1 Message Date
GitHub Actions b2dcec2f8a chore(release): v0.16.5 2024-02-09 06:01:00 +00:00
Aaron Klinker e8355029d9 fix: Support node 20 when running wxt submit 2024-02-08 23:55:53 -06:00
Aaron Klinker 2b374b98d2 docs: Remove "coming soon" from automated publishing feature 2024-02-05 23:36:13 -06:00
GitHub Actions d14d76a0c8 chore(release): v0.16.4 2024-02-06 05:21:00 +00:00
Aaron Klinker d0f1c7065c chore: Fix fake path in test data generator 2024-02-05 23:17:14 -06:00
Aaron 3edb66d76d feat: Add option to customize the analysis output filename (#426) 2024-02-05 23:15:15 -06:00
Aaron bba8cdace1 fix: Exclude analysis files from the build summary (#425) 2024-02-05 23:09:30 -06:00
Aaron 22f82a67ee feat: Automatically convert MV3 web_accessible_resources to MV2 (#423) 2024-02-05 22:49:18 -06:00
Aaron b32c60ca73 fix: Don't use immer for transformManifest (#424) 2024-02-05 22:37:26 -06:00
Aaron Klinker c93620cba5 Update changelog 2024-02-04 17:48:26 -06:00
GitHub Actions dd05ed4d4c chore(release): v0.16.3 2024-02-04 23:41:38 +00:00
Aaron 21636defda feat: Hooks (#419)
Deprecated `transformManifest` since a hook now exists for it.
2024-02-04 17:37:31 -06:00
Aaron bb022646e3 chore: Register global wxt instance (#418) 2024-02-04 16:34:10 -06:00
Aaron Klinker 37ad2c7ceb fix(init): Use ungh to prevent rate limits when loading templates 2024-02-04 14:11:41 -06:00
dependabot[bot] b992631e00 chore(deps): bump rollup-plugin-visualizer from 5.9.3 to 5.12.0 (#412)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-04 03:14:47 -06:00
dependabot[bot] 35a5cc6717 chore(deps-dev): bump typescript from 5.3.2 to 5.3.3 (#409)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-04 03:14:15 -06:00
dependabot[bot] 310b56fe5d chore(deps): bump defu from 6.1.3 to 6.1.4 (#413)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-04 03:13:28 -06:00
dependabot[bot] 148234382e chore(deps): bump @types/webextension-polyfill from 0.10.5 to 0.10.7 (#410)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-04 03:13:03 -06:00
dependabot[bot] c1a97b77b0 chore(deps): bump actions/checkout from 3 to 4 (#407)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-04 03:11:32 -06:00
Chen Hua 557ccb4854 docs: fix typo of opinionated (#416) 2024-02-04 03:11:03 -06:00
Chen Hua 83e7e8aab9 docs: Fix typo of intuitive (#415) 2024-02-04 03:10:20 -06:00
dependabot[bot] 7bf490c59d chore(deps-dev): bump happy-dom from 12.10.3 to 13.3.8 (#411)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-04 03:08:24 -06:00
dependabot[bot] 9670babdbb chore(deps): bump codecov/codecov-action from 3 to 4 (#408)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-04 03:06:11 -06:00
Florian Metz 42dfc2d5cf chore: Add dependabot for github actions (#404) 2024-02-03 16:32:46 -06:00
GitHub Actions 1527f0e275 chore(release): v0.16.2 2024-02-03 21:02:28 +00:00
Aaron 66bd957ee0 fix: Don't crash background service worker when using import.meta.url (#402) 2024-02-03 14:57:34 -06:00
GitHub Actions 72e3c5baa2 chore(release): v0.16.1 2024-02-03 19:57:51 +00:00
Aaron Klinker 9318346ab3 fix: Don't require config to run wxt submit init 2024-02-03 13:52:15 -06:00
Aaron Klinker 1d6aceab0a Update changelog 2024-02-03 11:05:16 -06:00
Florian Metz c1b5a9d270 docs: Add premid extension to homepage (#399) 2024-02-02 22:46:46 -06:00
Aaron Klinker f0b2a12724 chore(templates): Upgrade to wxt ^0.16.0 2024-02-02 16:20:24 -06:00
Aaron Klinker c45677ed48 Update changelog 2024-02-02 16:19:33 -06:00
GitHub Actions 7a3f075e21 chore(release): v0.16.0 2024-02-02 16:55:11 +00:00
Aaron Klinker 1e12ce2a0d docs: Document how to opt into ESM 2024-02-02 10:48:00 -06:00
Aaron eca30296a7 feat!: ESM background support (#398)
BREAKING CHANGE: The build output has changed slightly. This isn't a huge deal, and no steps are required for users unless you're doing something weird with the output files after a build.

Before:

```
.output/
  <target>/
    chunks/
      popup-<hash>.js
    popup.html
```

After:

```
.output/
  <target>/
    popup.html
    popup.js
```

This applies for all HTML files, not just the popup.
2024-02-02 10:17:43 -06:00
dependabot[bot] 41fa320815 chore(deps-dev): bump @vitest/coverage-v8 from 1.0.1 to 1.2.2 (#397)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-02 07:07:50 -06:00
dependabot[bot] 873c24fa50 chore(deps-dev): bump p-map from 7.0.0 to 7.0.1 (#396)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-02 07:06:51 -06:00
dependabot[bot] d92db5ba8f chore(deps-dev): bump lint-staged from 15.2.0 to 15.2.1 (#395)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-02 07:06:31 -06:00
dependabot[bot] 1a6fe33f97 chore(deps): bump fast-glob from 3.3.1 to 3.3.2 (#394)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-02 07:05:42 -06:00
GitHub Actions 2db4ea77a9 chore(release): v0.15.4 2024-01-31 17:42:07 +00:00
Aaron ba5349ccaf fix(submit): Load .env.submit automatically when running wxt submit and wxt submit init (#391) 2024-01-31 11:38:11 -06:00
85 changed files with 2711 additions and 1908 deletions
+4
View File
@@ -9,3 +9,7 @@ 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@v3
- uses: actions/checkout@v4
- 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@v3
- uses: actions/checkout@v4
with:
fetch-depth: 0
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
sync:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with:
version: 8
+8 -8
View File
@@ -10,25 +10,25 @@ jobs:
formatting:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: ./.github/actions/setup
- run: pnpm format:check
lint:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: ./.github/actions/setup
- run: pnpm lint
type-check:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: ./.github/actions/setup
- run: pnpm compile
validate-demo:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: ./.github/actions/setup
- run: pnpm tsc --noEmit
working-directory: demo
@@ -41,17 +41,17 @@ jobs:
tests:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: ./.github/actions/setup
- name: pnpm test:coverage
run: pnpm test:coverage --reporter=default --reporter=hanging-process
- uses: codecov/codecov-action@v3
- uses: codecov/codecov-action@v4
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
windows-tests:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: ./.github/actions/setup
- run: pnpm test run
template:
@@ -65,7 +65,7 @@ jobs:
- vanilla
- vue
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: ./.github/actions/setup
- run: pnpm pack
- run: npm i
+138
View File
@@ -1,5 +1,143 @@
# Changelog
## v0.16.5
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.4...v0.16.5)
### 🩹 Fixes
- Support node 20 when running `wxt submit` ([e835502](https://github.com/wxt-dev/wxt/commit/e835502))
### 📖 Documentation
- Remove "coming soon" from automated publishing feature ([2b374b9](https://github.com/wxt-dev/wxt/commit/2b374b9))
## v0.16.4
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.3...v0.16.4)
### 🚀 Enhancements
- Automatically convert MV3 `web_accessible_resources` to MV2 ([#423](https://github.com/wxt-dev/wxt/pull/423))
- Add option to customize the analysis output filename ([#426](https://github.com/wxt-dev/wxt/pull/426))
### 🩹 Fixes
- Don't use immer for `transformManifest` ([#424](https://github.com/wxt-dev/wxt/pull/424))
- Exclude analysis files from the build summary ([#425](https://github.com/wxt-dev/wxt/pull/425))
### 🏡 Chore
- Fix fake path in test data generator ([d0f1c70](https://github.com/wxt-dev/wxt/commit/d0f1c70))
## v0.16.3
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.2...v0.16.3)
### 🚀 Enhancements
- Hooks ([#419](https://github.com/wxt-dev/wxt/pull/419))
### 🩹 Fixes
- **init:** Use `ungh` to prevent rate limits when loading templates ([37ad2c7](https://github.com/wxt-dev/wxt/commit/37ad2c7))
### 📖 Documentation
- Fix typo of intuitive ([#415](https://github.com/wxt-dev/wxt/pull/415))
- Fix typo of opinionated ([#416](https://github.com/wxt-dev/wxt/pull/416))
### 🏡 Chore
- Add dependabot for github actions ([#404](https://github.com/wxt-dev/wxt/pull/404))
- **deps-dev:** Bump happy-dom from 12.10.3 to 13.3.8 ([#411](https://github.com/wxt-dev/wxt/pull/411))
- **deps-dev:** Bump typescript from 5.3.2 to 5.3.3 ([#409](https://github.com/wxt-dev/wxt/pull/409))
- Register global `wxt` instance ([#418](https://github.com/wxt-dev/wxt/pull/418))
### ❤️ Contributors
- Chen Hua ([@hcljsq](https://github.com/hcljsq))
- Florian Metz ([@Timeraa](http://github.com/Timeraa))
## v0.16.2
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.1...v0.16.2)
### 🩹 Fixes
- Don't crash background service worker when using `import.meta.url` ([#402](https://github.com/wxt-dev/wxt/pull/402))
## v0.16.1
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.0...v0.16.1)
### 🩹 Fixes
- Don't require config to run `wxt submit init` ([9318346](https://github.com/wxt-dev/wxt/commit/9318346))
### 📖 Documentation
- Add premid extension to homepage ([#399](https://github.com/wxt-dev/wxt/pull/399))
### 🏡 Chore
- **templates:** Upgrade to wxt `^0.16.0` ([f0b2a12](https://github.com/wxt-dev/wxt/commit/f0b2a12))
### ❤️ Contributors
- Florian Metz ([@Timeraa](http://github.com/Timeraa))
## v0.16.0
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.15.4...v0.16.0)
### 🚀 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)
+2 -5
View File
@@ -55,14 +55,11 @@ Or see the [installation guide](https://wxt.dev/guide/installation.html) to get
- 📂 File based entrypoints
- 🚔 TypeScript
- 🦾 Auto-imports
- ⬇️ Download and bundle remote URL imports
- 🤖 Automated publishing
- 🎨 Frontend framework agnostic: works with Vue, React, Svelte, etc
- 🖍️ Quickly bootstrap a new project
- 📏 Bundle analysis
### Coming Soon
- 🤖 Automated publishing
- ⬇️ Download and bundle remote URL imports
## Contributors
+35 -30
View File
@@ -1,37 +1,42 @@
import messages from 'public/_locales/en/messages.json';
export default defineBackground(() => {
console.log(browser.runtime.id);
logId();
console.log({
browser: import.meta.env.BROWSER,
chrome: import.meta.env.CHROME,
firefox: import.meta.env.FIREFOX,
manifestVersion: import.meta.env.MANIFEST_VERSION,
messages,
});
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');
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');
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,
});
// @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');
// @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('WXT MODE:', {
MODE: import.meta.env.MODE,
DEV: import.meta.env.DEV,
PROD: import.meta.env.PROD,
});
// @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');
storage.setItem('session:startTime', Date.now());
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());
},
});
@@ -17,6 +17,7 @@ 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
View File
@@ -90,6 +90,7 @@ export default defineConfig({
{ text: 'Assets', link: '/guide/assets.md' },
{ text: 'Content Script UI', link: '/guide/content-script-ui.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' },
+38
View File
@@ -0,0 +1,38 @@
# 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.
+1
View File
@@ -145,5 +145,6 @@ 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, intuative, and safe way **_for all browsers_**.
WXT is a free and open source framework for building web extensions in an conventional, intuitive, and safe way **_for all browsers_**.
WXT is based of [Nuxt](https://nuxt.com), and aims to provide the same great DX with TypeScript, auto-imports, and an opinionated project structure.
@@ -15,7 +15,7 @@ WXT is based of [Nuxt](https://nuxt.com), and aims to provide the same great DX
## Conventions
WXT is an optionated framework. This helps keep projects consistent and easy to pick up.
WXT is an opinionated framework. This helps keep projects consistent and easy to pick up.
- **Generated manifest**: Based on your project's file structure
- **Entrypoint configuration**: Configure entrypoints from the same file they're declare in
+8 -8
View File
@@ -47,11 +47,9 @@ features:
details: Nuxt-like auto-imports to speed up development.
link: /guide/auto-imports
linkText: Read docs
- icon: ⬇️
title: Bundle Remote Code
details: Downloads and bundles remote code imported from URLs.
link: /guide/remote-code
linkText: Read docs
- icon: 🤖
title: Automated Publishing
details: Automatically zip, upload, submit, and publish extensions.
- icon: 🎨
title: Frontend Framework Agnostic
details: Works with any front-end framework with a Vite plugin.
@@ -65,9 +63,11 @@ features:
- icon: 📏
title: Bundle Analysis
details: Tools for analyizing the final extension bundle and minimizing your extension's size.
- icon: 🤖
title: Automated Publishing
details: 'Coming soon. Automatically zip, upload, and release extensions.'
- icon: ⬇️
title: Bundle Remote Code
details: Downloads and bundles remote code imported from URLs.
link: /guide/remote-code
linkText: Read docs
---
<section class="vp-doc">
+99
View File
@@ -0,0 +1,99 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { TestProject } from '../utils';
import { WxtHooks } from '~/types';
const hooks: WxtHooks = {
ready: vi.fn(),
'build:before': vi.fn(),
'build:done': vi.fn(),
'build:manifestGenerated': vi.fn(),
'entrypoints:resolved': vi.fn(),
'entrypoints:grouped': vi.fn(),
};
function expectHooksToBeCalled(called: Record<keyof WxtHooks, boolean>) {
Object.keys(hooks).forEach((key) => {
const hookName = key as keyof WxtHooks;
const times = called[hookName] ? 1 : 0;
expect(
hooks[hookName],
`Expected "${hookName}" to be called ${times} time(s)`,
).toBeCalledTimes(called[hookName] ? 1 : 0);
});
}
describe('Hooks', () => {
beforeEach(() => {
Object.values(hooks).forEach((fn) => fn.mockReset());
});
it('prepare should call hooks', async () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html', '<html></html>');
await project.prepare({ hooks });
expectHooksToBeCalled({
ready: true,
'build:before': false,
'build:done': false,
'build:manifestGenerated': false,
'entrypoints:grouped': false,
'entrypoints:resolved': true,
});
});
it('build should call hooks', async () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html', '<html></html>');
await project.build({ hooks });
expectHooksToBeCalled({
ready: true,
'build:before': true,
'build:done': true,
'build:manifestGenerated': true,
'entrypoints:grouped': true,
'entrypoints:resolved': true,
});
});
it('zip should call hooks', async () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html', '<html></html>');
await project.zip({ hooks });
expectHooksToBeCalled({
ready: true,
'build:before': true,
'build:done': true,
'build:manifestGenerated': true,
'entrypoints:grouped': true,
'entrypoints:resolved': true,
});
});
it('server.start should call hooks', async () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html', '<html></html>');
const server = await project.startServer({
hooks,
runner: {
disabled: true,
},
});
await server.stop();
expectHooksToBeCalled({
ready: true,
'build:before': true,
'build:done': true,
'build:manifestGenerated': true,
'entrypoints:grouped': true,
'entrypoints:resolved': true,
});
});
});
+165
View File
@@ -227,4 +227,169 @@ 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;
}
})();
"
`);
});
});
+30 -5
View File
@@ -2,7 +2,14 @@ import { dirname, join, relative, resolve } from 'path';
import fs from 'fs-extra';
import glob from 'fast-glob';
import { execaCommand } from 'execa';
import { InlineConfig, UserConfig, build } from '../src';
import {
InlineConfig,
UserConfig,
build,
createServer,
prepare,
zip,
} from '../src';
import { normalizePath } from '../src/core/utils/paths';
import merge from 'lodash.merge';
@@ -63,10 +70,29 @@ export class TestProject {
if (filename === 'wxt.config.ts') this.config = {};
}
/**
* Write the files to the test directory install dependencies, and build the project.
*/
async prepare(config: InlineConfig = {}) {
await this.writeProjectToDisk();
await prepare({ ...config, root: this.root });
}
async build(config: InlineConfig = {}) {
await this.writeProjectToDisk();
await build({ ...config, root: this.root });
}
async zip(config: InlineConfig = {}) {
await this.writeProjectToDisk();
await zip({ ...config, root: this.root });
}
async startServer(config: InlineConfig = {}) {
await this.writeProjectToDisk();
const server = await createServer({ ...config, root: this.root });
await server.start();
return server;
}
private async writeProjectToDisk() {
if (this.config == null) this.setConfigFileConfig();
for (const file of this.files) {
@@ -80,7 +106,6 @@ export class TestProject {
await execaCommand('pnpm --ignore-workspace i --ignore-scripts', {
cwd: this.root,
});
await build({ ...config, root: this.root });
}
/**
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "wxt",
"type": "module",
"version": "0.15.3",
"version": "0.16.5",
"description": "Next gen framework for developing web extensions",
"engines": {
"node": ">=18",
@@ -120,7 +120,7 @@
"fs-extra": "^11.1.1",
"get-port": "^7.0.0",
"giget": "^1.1.3",
"immer": "^10.0.3",
"hookable": "^5.5.3",
"is-wsl": "^3.0.0",
"jiti": "^1.21.0",
"json5": "^2.2.3",
@@ -132,7 +132,7 @@
"ora": "^7.0.1",
"picocolors": "^1.0.0",
"prompts": "^2.4.2",
"publish-browser-extension": "^2.0.0",
"publish-browser-extension": "^2.1.2",
"rollup-plugin-visualizer": "^5.9.2",
"unimport": "^3.4.0",
"vite": "^5.0.12",
@@ -150,7 +150,7 @@
"@types/prompts": "^2.4.9",
"@vitest/coverage-v8": "^1.0.1",
"execa": "^8.0.1",
"happy-dom": "^12.10.3",
"happy-dom": "^13.3.8",
"lint-staged": "^15.2.0",
"lodash.merge": "^4.6.2",
"npm-run-all": "^4.1.5",
+90 -122
View File
@@ -10,7 +10,7 @@ importers:
dependencies:
'@types/webextension-polyfill':
specifier: ^0.10.5
version: 0.10.5
version: 0.10.7
'@webext-core/fake-browser':
specifier: ^1.3.1
version: 1.3.1
@@ -37,7 +37,7 @@ importers:
version: 3.2.3
defu:
specifier: ^6.1.3
version: 6.1.3
version: 6.1.4
dequal:
specifier: ^2.0.3
version: 2.0.3
@@ -46,7 +46,7 @@ importers:
version: 0.19.8
fast-glob:
specifier: ^3.3.1
version: 3.3.1
version: 3.3.2
filesize:
specifier: ^10.0.8
version: 10.1.0
@@ -59,9 +59,9 @@ importers:
giget:
specifier: ^1.1.3
version: 1.1.3
immer:
specifier: ^10.0.3
version: 10.0.3
hookable:
specifier: ^5.5.3
version: 5.5.3
is-wsl:
specifier: ^3.0.0
version: 3.1.0
@@ -96,11 +96,11 @@ importers:
specifier: ^2.4.2
version: 2.4.2
publish-browser-extension:
specifier: ^2.0.0
version: 2.0.0
specifier: ^2.1.2
version: 2.1.2
rollup-plugin-visualizer:
specifier: ^5.9.2
version: 5.9.3
version: 5.12.0
unimport:
specifier: ^3.4.0
version: 3.4.0
@@ -140,16 +140,16 @@ importers:
version: 2.4.9
'@vitest/coverage-v8':
specifier: ^1.0.1
version: 1.0.1(vitest@1.1.0)
version: 1.2.2(vitest@1.1.0)
execa:
specifier: ^8.0.1
version: 8.0.1
happy-dom:
specifier: ^12.10.3
version: 12.10.3
specifier: ^13.3.8
version: 13.3.8
lint-staged:
specifier: ^15.2.0
version: 15.2.0
version: 15.2.1
lodash.merge:
specifier: ^4.6.2
version: 4.6.2
@@ -158,7 +158,7 @@ importers:
version: 4.1.5
p-map:
specifier: ^7.0.0
version: 7.0.0
version: 7.0.1
prettier:
specifier: ^3.1.0
version: 3.1.1
@@ -170,13 +170,13 @@ importers:
version: 2.9.0
tsup:
specifier: ^8.0.1
version: 8.0.1(typescript@5.3.2)
version: 8.0.1(typescript@5.3.3)
tsx:
specifier: ^4.6.2
version: 4.7.0
typedoc:
specifier: ^0.25.4
version: 0.25.4(typescript@5.3.2)
version: 0.25.4(typescript@5.3.3)
typedoc-plugin-markdown:
specifier: 4.0.0-next.23
version: 4.0.0-next.23(typedoc@0.25.4)
@@ -185,19 +185,19 @@ importers:
version: 1.0.0-next.3(typedoc-plugin-markdown@4.0.0-next.23)
typescript:
specifier: ^5.3.2
version: 5.3.2
version: 5.3.3
vitepress:
specifier: 1.0.0-rc.34
version: 1.0.0-rc.34(@types/node@20.10.3)(typescript@5.3.2)
version: 1.0.0-rc.34(@types/node@20.10.3)(typescript@5.3.3)
vitest:
specifier: ^1.1.0
version: 1.1.0(@types/node@20.10.3)(happy-dom@12.10.3)(sass@1.69.5)
version: 1.1.0(@types/node@20.10.3)(happy-dom@13.3.8)(sass@1.69.5)
vitest-mock-extended:
specifier: ^1.3.1
version: 1.3.1(typescript@5.3.2)(vitest@1.1.0)
version: 1.3.1(typescript@5.3.3)(vitest@1.1.0)
vue:
specifier: ^3.3.10
version: 3.3.10(typescript@5.3.2)
version: 3.3.10(typescript@5.3.3)
demo:
dependencies:
@@ -209,7 +209,7 @@ importers:
version: 18.2.0(react@18.2.0)
vitest:
specifier: ^1.1.0
version: 1.1.0(@types/node@20.10.3)(happy-dom@12.10.3)(sass@1.69.5)
version: 1.1.0(@types/node@20.10.3)(happy-dom@13.3.8)(sass@1.69.5)
devDependencies:
'@types/react':
specifier: ^18.2.34
@@ -399,20 +399,12 @@ packages:
js-tokens: 4.0.0
dev: false
/@babel/parser@7.23.5:
resolution: {integrity: sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==}
engines: {node: '>=6.0.0'}
hasBin: true
dependencies:
'@babel/types': 7.23.5
dev: true
/@babel/parser@7.23.6:
resolution: {integrity: sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==}
engines: {node: '>=6.0.0'}
hasBin: true
dependencies:
'@babel/types': 7.23.5
'@babel/types': 7.23.9
dev: true
/@babel/runtime@7.23.9:
@@ -422,8 +414,8 @@ packages:
regenerator-runtime: 0.14.0
dev: false
/@babel/types@7.23.5:
resolution: {integrity: sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w==}
/@babel/types@7.23.9:
resolution: {integrity: sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/helper-string-parser': 7.23.4
@@ -1164,8 +1156,8 @@ packages:
resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
dev: true
/@types/webextension-polyfill@0.10.5:
resolution: {integrity: sha512-LlmbFLUB7+BDrb9nMuM0wlqtx9LZbBV2x3W98o02cD7Y8i10+sBenTlhG56vr47dzC7WIVXbURii+5jMJsyjLw==}
/@types/webextension-polyfill@0.10.7:
resolution: {integrity: sha512-10ql7A0qzBmFB+F+qAke/nP1PIonS0TXZAOMVOxEUsm+lGSW6uwVcISFNa0I4Oyj0884TZVWGGMIWeXOVSNFHw==}
dev: false
/@types/yauzl@2.10.3:
@@ -1184,11 +1176,11 @@ packages:
vue: ^3.2.25
dependencies:
vite: 5.0.12(@types/node@20.10.3)(sass@1.69.5)
vue: 3.4.3(typescript@5.3.2)
vue: 3.4.3(typescript@5.3.3)
dev: true
/@vitest/coverage-v8@1.0.1(vitest@1.1.0):
resolution: {integrity: sha512-Z4a7ig4VjUCT/P+LRB3IZrBRXb9xWRUM8rSBH9cKgfrU1Oe01/K2WJKtGshOnQwXZoSfQtwCGpbnHmB/qJwjcw==}
/@vitest/coverage-v8@1.2.2(vitest@1.1.0):
resolution: {integrity: sha512-IHyKnDz18SFclIEEAHb9Y4Uxx0sPKC2VO1kdDCs1BF6Ip4S8rQprs971zIsooLUn7Afs71GRxWMWpkCGZpRMhw==}
peerDependencies:
vitest: ^1.0.0
dependencies:
@@ -1200,12 +1192,12 @@ packages:
istanbul-lib-source-maps: 4.0.1
istanbul-reports: 3.1.6
magic-string: 0.30.5
magicast: 0.3.2
magicast: 0.3.3
picocolors: 1.0.0
std-env: 3.6.0
test-exclude: 6.0.0
v8-to-istanbul: 9.2.0
vitest: 1.1.0(@types/node@20.10.3)(happy-dom@12.10.3)(sass@1.69.5)
vitest: 1.1.0(@types/node@20.10.3)(happy-dom@13.3.8)(sass@1.69.5)
transitivePeerDependencies:
- supports-color
dev: true
@@ -1279,7 +1271,7 @@ packages:
/@vue/compiler-sfc@3.3.10:
resolution: {integrity: sha512-xpcTe7Rw7QefOTRFFTlcfzozccvjM40dT45JtrE3onGm/jBLZ0JhpKu3jkV7rbDFLeeagR/5RlJ2Y9SvyS0lAg==}
dependencies:
'@babel/parser': 7.23.5
'@babel/parser': 7.23.6
'@vue/compiler-core': 3.3.10
'@vue/compiler-dom': 3.3.10
'@vue/compiler-ssr': 3.3.10
@@ -1382,7 +1374,7 @@ packages:
dependencies:
'@vue/compiler-ssr': 3.3.10
'@vue/shared': 3.3.10
vue: 3.3.10(typescript@5.3.2)
vue: 3.3.10(typescript@5.3.3)
dev: true
/@vue/server-renderer@3.4.3(vue@3.4.3):
@@ -1392,7 +1384,7 @@ packages:
dependencies:
'@vue/compiler-ssr': 3.4.3
'@vue/shared': 3.4.3
vue: 3.4.3(typescript@5.3.2)
vue: 3.4.3(typescript@5.3.3)
dev: true
/@vue/shared@3.3.10:
@@ -1752,7 +1744,7 @@ packages:
resolution: {integrity: sha512-BWZRJgDEveT8uI+cliCwvYSSSSvb4xKoiiu5S0jaDbKBopQLQF7E+bq9xKk1pTcG+mUa3yXuFO7bD9d8Lr9Xxg==}
dependencies:
chokidar: 3.5.3
defu: 6.1.3
defu: 6.1.4
dotenv: 16.3.1
giget: 1.1.3
jiti: 1.21.0
@@ -2061,9 +2053,6 @@ packages:
engines: {node: '>= 6'}
dev: false
/css.escape@1.5.1:
resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
/cssom@0.5.0:
resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==}
dev: false
@@ -2167,8 +2156,8 @@ packages:
object-keys: 1.1.1
dev: true
/defu@6.1.3:
resolution: {integrity: sha512-Vy2wmG3NTkmHNg/kzpuvHhkqeIx3ODWqasgCRbKtbXEN0G+HpEEv9BtJLp7ZG1CZloFaC41Ah3ZFbq7aqCqMeQ==}
/defu@6.1.4:
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
dev: false
/dequal@2.0.3:
@@ -2243,10 +2232,6 @@ packages:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
dev: false
/emoji-regex@10.2.1:
resolution: {integrity: sha512-97g6QgOk8zlDRdgq1WxwgTMgEWGVAQvB5Fdpgc1MkNy56la5SKP9GsMXKDOdqwn90/41a8yPwIGk1Y6WVbeMQA==}
dev: false
/emoji-regex@10.3.0:
resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==}
@@ -2481,8 +2466,8 @@ packages:
- supports-color
dev: false
/fast-glob@3.3.1:
resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==}
/fast-glob@3.3.2:
resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
engines: {node: '>=8.6.0'}
dependencies:
'@nodelib/fs.stat': 2.0.5
@@ -2686,7 +2671,7 @@ packages:
hasBin: true
dependencies:
colorette: 2.0.20
defu: 6.1.3
defu: 6.1.4
https-proxy-agent: 7.0.2
mri: 1.2.0
node-fetch-native: 1.4.1
@@ -2759,7 +2744,7 @@ packages:
dependencies:
array-union: 2.1.0
dir-glob: 3.0.1
fast-glob: 3.3.1
fast-glob: 3.3.2
ignore: 5.2.4
merge2: 1.4.1
slash: 3.0.0
@@ -2803,14 +2788,12 @@ packages:
resolution: {integrity: sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==}
dev: false
/happy-dom@12.10.3:
resolution: {integrity: sha512-JzUXOh0wdNGY54oKng5hliuBkq/+aT1V3YpTM+lrN/GoLQTANZsMaIvmHiHe612rauHvPJnDZkZ+5GZR++1Abg==}
/happy-dom@13.3.8:
resolution: {integrity: sha512-RAbq4oYfJNkVan1m1F3jfA4YEyRY0/ASoNvZsNJbuX85jIypidmsz9jQZD7Tqz0VXA2MhAGfcsh5oshwmwNYSg==}
engines: {node: '>=16.0.0'}
dependencies:
css.escape: 1.5.1
entities: 4.5.0
iconv-lite: 0.6.3
webidl-conversions: 7.0.0
whatwg-encoding: 2.0.0
whatwg-mimetype: 3.0.0
/has-bigints@1.0.2:
@@ -2864,6 +2847,10 @@ packages:
resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
dev: false
/hookable@5.5.3:
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
dev: false
/hosted-git-info@2.8.9:
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
dev: true
@@ -2920,12 +2907,6 @@ packages:
resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
engines: {node: '>=16.17.0'}
/iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
dependencies:
safer-buffer: 2.1.2
/ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
dev: false
@@ -2946,10 +2927,6 @@ packages:
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
dev: false
/immer@10.0.3:
resolution: {integrity: sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==}
dev: false
/immutable@4.3.4:
resolution: {integrity: sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==}
@@ -3403,8 +3380,8 @@ packages:
uhyphen: 0.2.0
dev: false
/lint-staged@15.2.0:
resolution: {integrity: sha512-TFZzUEV00f+2YLaVPWBWGAMq7So6yQx+GG8YRMDeOEIf95Zn5RyiLMsEiX4KTNl9vq/w+NqRJkLA1kPIo15ufQ==}
/lint-staged@15.2.1:
resolution: {integrity: sha512-dhwAPnM85VdshybV9FWI/9ghTvMLoQLEXgVMx+ua2DN7mdfzd/tRfoU2yhMcBac0RHkofoxdnnJUokr8s4zKmQ==}
engines: {node: '>=18.12.0'}
hasBin: true
dependencies:
@@ -3413,7 +3390,7 @@ packages:
debug: 4.3.4
execa: 8.0.1
lilconfig: 3.0.0
listr2: 8.0.0
listr2: 8.0.1
micromatch: 4.0.5
pidtree: 0.6.0
string-argv: 0.3.2
@@ -3422,8 +3399,8 @@ packages:
- supports-color
dev: true
/listr2@8.0.0:
resolution: {integrity: sha512-u8cusxAcyqAiQ2RhYvV7kRKNLgUvtObIbhOX2NCXqvp1UU32xIg5CT22ykS2TPKJXZWJwtK3IKLiqAGlGNE+Zg==}
/listr2@8.0.1:
resolution: {integrity: sha512-ovJXBXkKGfq+CwmKTjluEqFi3p4h8xvkxGQQAQan22YCgef4KZ1mKGjzfGh6PL6AW5Csw0QiQPNuQyH+6Xk3hA==}
engines: {node: '>=18.0.0'}
dependencies:
cli-truncate: 4.0.0
@@ -3543,11 +3520,11 @@ packages:
dependencies:
'@jridgewell/sourcemap-codec': 1.4.15
/magicast@0.3.2:
resolution: {integrity: sha512-Fjwkl6a0syt9TFN0JSYpOybxiMCkYNEeOTnOTNRbjphirLakznZXAqrXgj/7GG3D1dvETONNwrBfinvAbpunDg==}
/magicast@0.3.3:
resolution: {integrity: sha512-ZbrP1Qxnpoes8sz47AM0z08U+jW6TyRgZzcWy3Ma3vDhJttwMwAFDMMQFobwdBxByBD46JYmxRzeF7w2+wJEuw==}
dependencies:
'@babel/parser': 7.23.5
'@babel/types': 7.23.5
'@babel/parser': 7.23.6
'@babel/types': 7.23.9
source-map-js: 1.0.2
dev: true
@@ -3974,8 +3951,8 @@ packages:
dependencies:
yocto-queue: 1.0.0
/p-map@7.0.0:
resolution: {integrity: sha512-EZl03dLKv3RypkrjlevZoNwQMSy4bAblWcR18zhonktnN4fUs3asFQKSe0awn982omGxamvbejqQKQYDJYHCEg==}
/p-map@7.0.1:
resolution: {integrity: sha512-2wnaR0XL/FDOj+TgpDuRb2KTjLnu3Fma6b1ZUwGY7LcqenMcvP/YFpjpbPKY6WVGsbuJZRuoUz8iPrt8ORnAFw==}
engines: {node: '>=18'}
dev: true
@@ -4184,9 +4161,9 @@ packages:
sade: 1.8.1
dev: true
/publish-browser-extension@2.0.0:
resolution: {integrity: sha512-HzNfnsx2/yN1n288btH6xfmXzYA5e9FpjteJpR68qlevqKMQqbCw9AJOK2WdFKVBy1kuSil8XGuhpQWv3NcrnA==}
engines: {node: '18', pnpm: '8'}
/publish-browser-extension@2.1.2:
resolution: {integrity: sha512-g6+mtdR4Z+GYHPIrfaAwC7Kbt1oQlpJ8r0x1PAytScy33OFdK+HVUeDDYorBpPAiQlmYJRQga7rY9QVTyTw34g==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
dependencies:
cac: 6.7.14
@@ -4238,7 +4215,7 @@ packages:
/rc9@2.1.1:
resolution: {integrity: sha512-lNeOl38Ws0eNxpO3+wD1I9rkHGQyj1NU1jlzv4go2CtEnEQEUfqnIvZG7W+bC/aXdJ27n5x/yUjb6RoT9tko+Q==}
dependencies:
defu: 6.1.3
defu: 6.1.4
destr: 2.0.2
flat: 5.0.2
dev: false
@@ -4400,8 +4377,8 @@ packages:
glob: 7.1.6
dev: false
/rollup-plugin-visualizer@5.9.3:
resolution: {integrity: sha512-ieGM5UAbMVqThX67GCuFHu/GkaSXIUZwFKJsSzE+7+k9fibU/6gbUz7SL+9BBzNtv5bIFHj7kEu0TWcqEnT/sQ==}
/rollup-plugin-visualizer@5.12.0:
resolution: {integrity: sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ==}
engines: {node: '>=14'}
hasBin: true
peerDependencies:
@@ -4472,9 +4449,6 @@ packages:
is-regex: 1.1.4
dev: true
/safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
/sass@1.69.5:
resolution: {integrity: sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==}
engines: {node: '>=14.0.0'}
@@ -4739,7 +4713,7 @@ packages:
engines: {node: '>=16'}
dependencies:
eastasianwidth: 0.2.0
emoji-regex: 10.2.1
emoji-regex: 10.3.0
strip-ansi: 7.1.0
dev: false
@@ -4953,12 +4927,12 @@ packages:
hasBin: true
dev: true
/ts-essentials@9.3.2(typescript@5.3.2):
/ts-essentials@9.3.2(typescript@5.3.3):
resolution: {integrity: sha512-JxKJzuWqH1MmH4ZFHtJzGEhkfN3QvVR3C3w+4BIoWeoY68UVVoA2Np/Bca9z0IPSErVCWhv439aT0We4Dks8kQ==}
peerDependencies:
typescript: '>=4.1.0'
dependencies:
typescript: 5.3.2
typescript: 5.3.3
dev: true
/ts-interface-checker@0.1.13:
@@ -4969,7 +4943,7 @@ packages:
resolution: {integrity: sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==}
dev: false
/tsup@8.0.1(typescript@5.3.2):
/tsup@8.0.1(typescript@5.3.3):
resolution: {integrity: sha512-hvW7gUSG96j53ZTSlT4j/KL0q1Q2l6TqGBFc6/mu/L46IoNWqLLUzLRLP1R8Q7xrJTmkDxxDoojV5uCVs1sVOg==}
engines: {node: '>=18'}
hasBin: true
@@ -5002,7 +4976,7 @@ packages:
source-map: 0.8.0-beta.0
sucrase: 3.32.0
tree-kill: 1.2.2
typescript: 5.3.2
typescript: 5.3.3
transitivePeerDependencies:
- supports-color
- ts-node
@@ -5060,7 +5034,7 @@ packages:
peerDependencies:
typedoc: '>=0.25.0'
dependencies:
typedoc: 0.25.4(typescript@5.3.2)
typedoc: 0.25.4(typescript@5.3.3)
dev: true
/typedoc-vitepress-theme@1.0.0-next.3(typedoc-plugin-markdown@4.0.0-next.23):
@@ -5071,7 +5045,7 @@ packages:
typedoc-plugin-markdown: 4.0.0-next.23(typedoc@0.25.4)
dev: true
/typedoc@0.25.4(typescript@5.3.2):
/typedoc@0.25.4(typescript@5.3.3):
resolution: {integrity: sha512-Du9ImmpBCw54bX275yJrxPVnjdIyJO/84co0/L9mwe0R3G4FSR6rQ09AlXVRvZEGMUg09+z/usc8mgygQ1aidA==}
engines: {node: '>= 16'}
hasBin: true
@@ -5082,11 +5056,11 @@ packages:
marked: 4.3.0
minimatch: 9.0.3
shiki: 0.14.5
typescript: 5.3.2
typescript: 5.3.3
dev: true
/typescript@5.3.2:
resolution: {integrity: sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==}
/typescript@5.3.3:
resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==}
engines: {node: '>=14.17'}
hasBin: true
dev: true
@@ -5115,7 +5089,7 @@ packages:
dependencies:
'@rollup/pluginutils': 5.0.5
escape-string-regexp: 5.0.0
fast-glob: 3.3.1
fast-glob: 3.3.2
local-pkg: 0.4.3
magic-string: 0.30.5
mlly: 1.4.2
@@ -5260,7 +5234,7 @@ packages:
optionalDependencies:
fsevents: 2.3.3
/vitepress@1.0.0-rc.34(@types/node@20.10.3)(typescript@5.3.2):
/vitepress@1.0.0-rc.34(@types/node@20.10.3)(typescript@5.3.3):
resolution: {integrity: sha512-TUbTiSdAZFni2XlHlpx61KikgkQ5uG4Wtmw2R0SXhIOG6qGqzDJczAFjkMc4i45I9c3KyatwOYe8oEfCnzVYwQ==}
hasBin: true
peerDependencies:
@@ -5287,7 +5261,7 @@ packages:
shikiji-core: 0.9.16
shikiji-transformers: 0.9.16
vite: 5.0.12(@types/node@20.10.3)(sass@1.69.5)
vue: 3.4.3(typescript@5.3.2)
vue: 3.4.3(typescript@5.3.3)
transitivePeerDependencies:
- '@algolia/client-search'
- '@types/node'
@@ -5316,18 +5290,18 @@ packages:
- universal-cookie
dev: true
/vitest-mock-extended@1.3.1(typescript@5.3.2)(vitest@1.1.0):
/vitest-mock-extended@1.3.1(typescript@5.3.3)(vitest@1.1.0):
resolution: {integrity: sha512-OpghYjh4BDuQ/Mzs3lFMQ1QRk9D8/2O9T47MLUA5eLn7K4RWIy+MfIivYOWEyxjTENjsBnzgMihDjyNalN/K0Q==}
peerDependencies:
typescript: 3.x || 4.x || 5.x
vitest: '>=0.31.1'
dependencies:
ts-essentials: 9.3.2(typescript@5.3.2)
typescript: 5.3.2
vitest: 1.1.0(@types/node@20.10.3)(happy-dom@12.10.3)(sass@1.69.5)
ts-essentials: 9.3.2(typescript@5.3.3)
typescript: 5.3.3
vitest: 1.1.0(@types/node@20.10.3)(happy-dom@13.3.8)(sass@1.69.5)
dev: true
/vitest@1.1.0(@types/node@20.10.3)(happy-dom@12.10.3)(sass@1.69.5):
/vitest@1.1.0(@types/node@20.10.3)(happy-dom@13.3.8)(sass@1.69.5):
resolution: {integrity: sha512-oDFiCrw7dd3Jf06HoMtSRARivvyjHJaTxikFxuqJjO76U436PqlVw1uLn7a8OSPrhSfMGVaRakKpA2lePdw79A==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
@@ -5363,7 +5337,7 @@ packages:
chai: 4.3.10
debug: 4.3.4
execa: 8.0.1
happy-dom: 12.10.3
happy-dom: 13.3.8
local-pkg: 0.5.0
magic-string: 0.30.5
pathe: 1.1.1
@@ -5404,10 +5378,10 @@ packages:
'@vue/composition-api':
optional: true
dependencies:
vue: 3.4.3(typescript@5.3.2)
vue: 3.4.3(typescript@5.3.3)
dev: true
/vue@3.3.10(typescript@5.3.2):
/vue@3.3.10(typescript@5.3.3):
resolution: {integrity: sha512-zg6SIXZdTBwiqCw/1p+m04VyHjLfwtjwz8N57sPaBhEex31ND0RYECVOC1YrRwMRmxFf5T1dabl6SGUbMKKuVw==}
peerDependencies:
typescript: '*'
@@ -5420,10 +5394,10 @@ packages:
'@vue/runtime-dom': 3.3.10
'@vue/server-renderer': 3.3.10(vue@3.3.10)
'@vue/shared': 3.3.10
typescript: 5.3.2
typescript: 5.3.3
dev: true
/vue@3.4.3(typescript@5.3.2):
/vue@3.4.3(typescript@5.3.3):
resolution: {integrity: sha512-GjN+culMAGv/mUbkIv8zMKItno8npcj5gWlXkSxf1SPTQf8eJ4A+YfHIvQFyL1IfuJcMl3soA7SmN1fRxbf/wA==}
peerDependencies:
typescript: '*'
@@ -5436,7 +5410,7 @@ packages:
'@vue/runtime-dom': 3.4.3
'@vue/server-renderer': 3.4.3(vue@3.4.3)
'@vue/shared': 3.4.3
typescript: 5.3.2
typescript: 5.3.3
dev: true
/watchpack@2.4.0:
@@ -5508,12 +5482,6 @@ packages:
resolution: {integrity: sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==}
dev: false
/whatwg-encoding@2.0.0:
resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==}
engines: {node: '>=12'}
dependencies:
iconv-lite: 0.6.3
/whatwg-mimetype@3.0.0:
resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
engines: {node: '>=12'}
+4 -3
View File
@@ -1,10 +1,10 @@
import { CAC, Command } from 'cac';
import consola, { LogLevels } from 'consola';
import { getInternalConfig } from '~/core/utils/building';
import { exec } from '~/core/utils/exec';
import { printHeader } from '~/core/utils/log';
import { formatDuration } from '~/core/utils/time';
import { ValidationError } from '~/core/utils/validation';
import { registerWxt } from '~/core/wxt';
/**
* Wrap an action handler to add a timer, error handling, and maybe enable debug mode.
@@ -74,11 +74,12 @@ export function createAliasedCommand(
.allowUnknownOptions()
.action(async () => {
try {
const config = await getInternalConfig({}, 'build');
await registerWxt('build');
const args = process.argv.slice(
process.argv.indexOf(aliasedCommand.name) + 1,
);
await exec(config, alias, args, {
await exec(alias, args, {
stdio: 'inherit',
});
} catch {
+5 -3
View File
@@ -1,5 +1,6 @@
import { BuildOutput, InlineConfig } from '~/types';
import { getInternalConfig, internalBuild } from './utils/building';
import { internalBuild } from './utils/building';
import { registerWxt } from './wxt';
/**
* Bundles the extension for production. Returns a promise of the build result. Discovers the `wxt.config.ts` file in
@@ -15,6 +16,7 @@ import { getInternalConfig, internalBuild } from './utils/building';
* })
*/
export async function build(config?: InlineConfig): Promise<BuildOutput> {
const internalConfig = await getInternalConfig(config ?? {}, 'build');
return await internalBuild(internalConfig);
await registerWxt('build', config);
return await internalBuild();
}
+33 -23
View File
@@ -3,7 +3,7 @@ import {
BuildStepOutput,
Entrypoint,
InlineConfig,
InternalConfig,
ResolvedConfig,
UserConfig,
VirtualEntrypointType,
WxtBuilder,
@@ -15,7 +15,7 @@ import { getEntrypointBundlePath } from '~/core/utils/entrypoints';
export async function createViteBuilder(
inlineConfig: InlineConfig,
userConfig: UserConfig,
wxtConfig: Omit<InternalConfig, 'builder'>,
wxtConfig: Omit<ResolvedConfig, 'builder'>,
): Promise<WxtBuilder> {
const vite = await import('vite');
@@ -63,9 +63,10 @@ export async function createViteBuilder(
wxtPlugins.noopBackground(),
wxtPlugins.globals(wxtConfig),
wxtPlugins.excludeBrowserPolyfill(wxtConfig),
wxtPlugins.defineImportMeta(),
);
if (wxtConfig.analysis.enabled) {
config.plugins.push(wxtPlugins.bundleAnalysis());
config.plugins.push(wxtPlugins.bundleAnalysis(wxtConfig));
}
return config;
@@ -75,23 +76,7 @@ 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 => {
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 entry = getRollupEntry(entrypoint);
const plugins: NonNullable<vite.UserConfig['plugins']> = [
wxtPlugins.entrypointGroupGlobals(entrypoint),
];
@@ -157,14 +142,16 @@ export async function createViteBuilder(
build: {
rollupOptions: {
input: entrypoints.reduce<Record<string, string>>((input, entry) => {
input[entry.name] = entry.inputPath;
input[entry.name] = getRollupEntry(entry);
return input;
}, {}),
output: {
// Include a hash to prevent conflicts
chunkFileNames: 'chunks/[name]-[hash].js',
// Include a hash to prevent conflicts
entryFileNames: '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',
// We can't control the "name", so we need a hash to prevent conflicts
assetFileNames: 'assets/[name]-[hash].[ext]',
},
@@ -263,3 +250,26 @@ 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;
}
@@ -3,7 +3,7 @@ import { Window } from 'happy-dom';
import { pointToDevServer } from '../devHtmlPrerender';
import {
fakeDevServer,
fakeInternalConfig,
fakeResolvedConfig,
} from '~/core/utils/testing/fake-objects';
import { normalizePath } from '~/core/utils/paths';
import { resolve } from 'node:path';
@@ -36,7 +36,7 @@ describe('Dev HTML Prerender Plugin', () => {
url: 'http://localhost',
});
const root = '/some/root';
const config = fakeInternalConfig({
const config = fakeResolvedConfig({
root,
alias: {
'~local': '.',
@@ -1,12 +1,15 @@
import type * as vite from 'vite';
import { visualizer } from 'rollup-plugin-visualizer';
import { ResolvedConfig } from '~/types';
import path from 'node:path';
let increment = 0;
export function bundleAnalysis(): vite.Plugin {
export function bundleAnalysis(
config: Omit<ResolvedConfig, 'builder'>,
): vite.Plugin {
return visualizer({
emitFile: true,
template: 'raw-data',
filename: `stats-${increment++}.json`,
filename: path.resolve(config.outDir, `stats-${increment++}.json`),
}) as vite.Plugin;
}
@@ -1,5 +1,5 @@
import type * as vite from 'vite';
import { Entrypoint, InternalConfig } from '~/types';
import { Entrypoint, ResolvedConfig } from '~/types';
import { getEntrypointBundlePath } from '~/core/utils/entrypoints';
/**
@@ -14,7 +14,7 @@ import { getEntrypointBundlePath } from '~/core/utils/entrypoints';
*/
export function cssEntrypoints(
entrypoint: Entrypoint,
config: Omit<InternalConfig, 'builder'>,
config: Omit<ResolvedConfig, 'builder'>,
): vite.Plugin {
return {
name: 'wxt:css-entrypoint',
@@ -0,0 +1,19 @@
/**
* Overrides definitions for `import.meta.*`
*
* - `import.meta.url`: Without this, background service workers crash trying to access
* `document.location`, see https://github.com/wxt-dev/wxt/issues/392
*/
export function defineImportMeta() {
return {
name: 'wxt:define',
config() {
return {
define: {
// This works for all extension contexts, including background service worker
'import.meta.url': 'self.location.href',
},
};
},
};
}
@@ -1,5 +1,5 @@
import type * as vite from 'vite';
import { InternalConfig, WxtDevServer } from '~/types';
import { ResolvedConfig, WxtDevServer } from '~/types';
import { getEntrypointName } from '~/core/utils/entrypoints';
import { parseHTML } from 'linkedom';
import { dirname, relative, resolve } from 'node:path';
@@ -12,7 +12,7 @@ let reactRefreshPreamble = '';
* Pre-renders the HTML entrypoints when building the extension to connect to the dev server.
*/
export function devHtmlPrerender(
config: Omit<InternalConfig, 'builder'>,
config: Omit<ResolvedConfig, 'builder'>,
): vite.PluginOption {
const htmlReloadId = '@wxt/reload-html';
const resolvedHtmlReloadId = resolve(
@@ -134,7 +134,7 @@ export function devHtmlPrerender(
}
export function pointToDevServer(
config: Omit<InternalConfig, 'builder'>,
config: Omit<ResolvedConfig, 'builder'>,
server: WxtDevServer,
id: string,
document: Document,
@@ -1,11 +1,11 @@
import { Plugin } from 'vite';
import { InternalConfig } from '~/types';
import { ResolvedConfig } from '~/types';
/**
* Defines global constants about the dev server. Helps scripts connect to the server's web socket.
*/
export function devServerGlobals(
config: Omit<InternalConfig, 'builder'>,
config: Omit<ResolvedConfig, 'builder'>,
): Plugin {
return {
name: 'wxt:dev-server-globals',
+2 -2
View File
@@ -1,5 +1,5 @@
import { Plugin } from 'vite';
import { InternalConfig } from '~/types';
import { ResolvedConfig } from '~/types';
import { fetchCached } from '~/core/utils/network';
/**
@@ -9,7 +9,7 @@ import { fetchCached } from '~/core/utils/network';
* @example
* import "url:https://google-tagmanager.com/gtag?id=XYZ";
*/
export function download(config: Omit<InternalConfig, 'builder'>): Plugin {
export function download(config: Omit<ResolvedConfig, 'builder'>): Plugin {
return {
name: 'wxt:download',
resolveId(id) {
@@ -1,4 +1,4 @@
import { InternalConfig } from '~/types';
import { ResolvedConfig } from '~/types';
import type * as vite from 'vite';
/**
@@ -7,7 +7,7 @@ import type * as vite from 'vite';
* virtual module.
*/
export function excludeBrowserPolyfill(
config: Omit<InternalConfig, 'builder'>,
config: Omit<ResolvedConfig, 'builder'>,
): vite.Plugin {
const virtualId = 'virtual:wxt-webextension-polyfill-disabled';
+2 -2
View File
@@ -1,9 +1,9 @@
import type * as vite from 'vite';
import { InternalConfig } from '~/types';
import { ResolvedConfig } from '~/types';
import { getGlobals } from '~/core/utils/globals';
export function globals(
config: Omit<InternalConfig, 'builder'>,
config: Omit<ResolvedConfig, 'builder'>,
): vite.PluginOption {
return {
name: 'wxt:globals',
+1
View File
@@ -12,3 +12,4 @@ export * from './globals';
export * from './webextensionPolyfillMock';
export * from './excludeBrowserPolyfill';
export * from './entrypointGroupGlobals';
export * from './defineImportMeta';
@@ -1,5 +1,5 @@
import type * as vite from 'vite';
import { Entrypoint, InternalConfig } from '~/types';
import { Entrypoint, ResolvedConfig } from '~/types';
import { dirname, extname, resolve, join } from 'node:path';
import { getEntrypointBundlePath } from '~/core/utils/entrypoints';
import fs, { ensureDir } from 'fs-extra';
@@ -20,7 +20,7 @@ import { normalizePath } from '~/core/utils/paths';
*/
export function multipageMove(
entrypoints: Entrypoint[],
config: Omit<InternalConfig, 'builder'>,
config: Omit<ResolvedConfig, 'builder'>,
): vite.Plugin {
return {
name: 'wxt:multipage-move',
@@ -1,8 +1,8 @@
import { InternalConfig } from '~/types';
import { ResolvedConfig } from '~/types';
import type * as vite from 'vite';
export function tsconfigPaths(
config: Omit<InternalConfig, 'builder'>,
config: Omit<ResolvedConfig, 'builder'>,
): vite.Plugin {
return {
name: 'wxt:aliases',
+2 -2
View File
@@ -1,5 +1,5 @@
import { createUnimport } from 'unimport';
import { InternalConfig } from '~/types';
import { ResolvedConfig } 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<InternalConfig, 'builder'>,
config: Omit<ResolvedConfig, 'builder'>,
): vite.PluginOption {
const options = getUnimportOptions(config);
if (options === false) return [];
@@ -1,5 +1,5 @@
import { Plugin } from 'vite';
import { InternalConfig, VirtualEntrypointType } from '~/types';
import { ResolvedConfig, VirtualEntrypointType } from '~/types';
import fs from 'fs-extra';
import { resolve } from 'path';
import { normalizePath } from '~/core/utils/paths';
@@ -9,7 +9,7 @@ import { normalizePath } from '~/core/utils/paths';
*/
export function virtualEntrypoint(
type: VirtualEntrypointType,
config: Omit<InternalConfig, 'builder'>,
config: Omit<ResolvedConfig, 'builder'>,
): Plugin {
const virtualId = `virtual:wxt-${type}?`;
const resolvedVirtualId = `\0${virtualId}`;
@@ -1,6 +1,6 @@
import path from 'node:path';
import type * as vite from 'vite';
import { InternalConfig } from '~/types';
import { ResolvedConfig } from '~/types';
/**
* Mock `webextension-polyfill` by inlining all dependencies that import it and adding a custom
@@ -16,7 +16,7 @@ import { InternalConfig } from '~/types';
* `npm list` and inline them automatically.
*/
export function webextensionPolyfillMock(
config: Omit<InternalConfig, 'builder'>,
config: Omit<ResolvedConfig, 'builder'>,
): vite.PluginOption {
return {
name: 'wxt:testing-inline-deps',
+63 -70
View File
@@ -2,13 +2,11 @@ import {
BuildStepOutput,
EntrypointGroup,
InlineConfig,
InternalConfig,
ServerInfo,
WxtDevServer,
} from '~/types';
import {
getEntrypointBundlePath,
getEntrypointOutputFile,
resolvePerBrowserOption,
} from '~/core/utils/entrypoints';
import {
@@ -17,7 +15,6 @@ import {
} from '~/core/utils/manifest';
import {
internalBuild,
getInternalConfig,
detectDevChanges,
rebuild,
findEntrypoints,
@@ -27,6 +24,7 @@ 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.
@@ -51,10 +49,10 @@ export async function createServer(
const buildAndOpenBrowser = async () => {
// Build after starting the dev server so it can be used to transform HTML files
server.currentOutput = await internalBuild(config);
server.currentOutput = await internalBuild();
// Open browser after everything is ready to go.
await runner.openBrowser(config);
await runner.openBrowser();
};
/**
@@ -62,8 +60,8 @@ export async function createServer(
*/
const closeAndRecreateRunner = async () => {
await runner.closeBrowser();
config = await getLatestConfig();
runner = await createExtensionRunner(config);
await wxt.reloadConfig();
runner = await createExtensionRunner();
};
// Server instance must be created first so its reference can be added to the internal config used
@@ -79,7 +77,7 @@ export async function createServer(
currentOutput: undefined,
async start() {
await builderServer.listen();
config.logger.success(`Started dev server @ ${serverInfo.origin}`);
wxt.logger.success(`Started dev server @ ${serverInfo.origin}`);
await buildAndOpenBrowser();
},
async stop() {
@@ -104,34 +102,26 @@ export async function createServer(
},
async restartBrowser() {
await closeAndRecreateRunner();
await runner.openBrowser(config);
await runner.openBrowser();
},
};
const getLatestConfig = () =>
getInternalConfig(inlineConfig ?? {}, 'serve', server);
let config = await getLatestConfig();
await registerWxt('serve', inlineConfig, server);
let [runner, builderServer] = await Promise.all([
createExtensionRunner(config),
config.builder.createServer(server),
createExtensionRunner(),
wxt.config.builder.createServer(server),
]);
// Register content scripts for the first time after the background starts up since they're not
// listed in the manifest
server.ws.on('wxt:background-initialized', () => {
if (server.currentOutput == null) return;
reloadContentScripts(server.currentOutput.steps, config, server);
reloadContentScripts(server.currentOutput.steps, server);
});
// Listen for file changes and reload different parts of the extension accordingly
const reloadOnChange = createFileReloader({
server,
getLatestConfig,
updateConfig(newConfig) {
config = newConfig;
},
});
const reloadOnChange = createFileReloader(server);
server.watcher.on('all', reloadOnChange);
return server;
@@ -146,21 +136,15 @@ async function getPort(): Promise<number> {
* Returns a function responsible for reloading different parts of the extension when a file
* changes.
*/
function createFileReloader(options: {
server: WxtDevServer;
getLatestConfig: () => Promise<InternalConfig>;
updateConfig: (config: InternalConfig) => void;
}) {
const { server, getLatestConfig, updateConfig } = options;
function createFileReloader(server: WxtDevServer) {
const fileChangedMutex = new Mutex();
const changeQueue: Array<[string, string]> = [];
return async (event: string, path: string) => {
const config = await getLatestConfig();
updateConfig(config);
await wxt.reloadConfig();
// Here, "path" is a non-normalized path (ie: C:\\users\\... instead of C:/users/...)
if (path.startsWith(config.outBaseDir)) return;
if (path.startsWith(wxt.config.outBaseDir)) return;
changeQueue.push([event, path]);
await fileChangedMutex.runExclusive(async () => {
@@ -171,45 +155,31 @@ function createFileReloader(options: {
.map(([_, file]) => file);
if (fileChanges.length === 0) return;
const changes = detectDevChanges(
config,
fileChanges,
server.currentOutput,
);
const changes = detectDevChanges(fileChanges, server.currentOutput);
if (changes.type === 'no-change') return;
if (changes.type === 'full-restart') {
config.logger.info('Config changed, restarting server...');
wxt.logger.info('Config changed, restarting server...');
server.restart();
return;
}
if (changes.type === 'browser-restart') {
config.logger.info('Runner config changed, restarting browser...');
wxt.logger.info('Runner config changed, restarting browser...');
server.restartBrowser();
return;
}
// Log the entrypoints that were effected
config.logger.info(
wxt.logger.info(
`Changed: ${Array.from(new Set(fileChanges))
.map((file) => pc.dim(relative(config.root, file)))
.map((file) => pc.dim(relative(wxt.config.root, file)))
.join(', ')}`,
);
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(config);
const allEntrypoints = await findEntrypoints();
const { output: newOutput } = await rebuild(
config,
allEntrypoints,
// TODO: this excludes new entrypoints, so they're not built until the dev command is restarted
changes.rebuildGroups,
@@ -221,15 +191,23 @@ function createFileReloader(options: {
switch (changes.type) {
case 'extension-reload':
server.reloadExtension();
consola.success(`Reloaded extension`);
break;
case 'html-reload':
reloadHtmlPages(changes.rebuildGroups, server, config);
const { reloadedNames } = reloadHtmlPages(
changes.rebuildGroups,
server,
);
consola.success(`Reloaded: ${getFilenameList(reloadedNames)}`);
break;
case 'content-script-reload':
reloadContentScripts(changes.changedSteps, config, server);
reloadContentScripts(changes.changedSteps, server);
const rebuiltNames = changes.rebuildGroups
.flat()
.map((entry) => entry.name);
consola.success(`Reloaded: ${getFilenameList(rebuiltNames)}`);
break;
}
consola.success(`Reloaded: ${rebuiltNames}`);
});
};
}
@@ -237,35 +215,34 @@ function createFileReloader(options: {
/**
* From the server, tell the client to reload content scripts from the provided build step outputs.
*/
function reloadContentScripts(
steps: BuildStepOutput[],
config: InternalConfig,
server: WxtDevServer,
) {
if (config.manifestVersion === 3) {
function reloadContentScripts(steps: BuildStepOutput[], server: WxtDevServer) {
if (wxt.config.manifestVersion === 3) {
steps.forEach((step) => {
if (server.currentOutput == null) return;
const entry = step.entrypoints;
if (Array.isArray(entry) || entry.type !== 'content-script') return;
const js = [getEntrypointBundlePath(entry, config.outDir, '.js')];
const js = [getEntrypointBundlePath(entry, wxt.config.outDir, '.js')];
const cssMap = getContentScriptsCssMap(server.currentOutput, [entry]);
const css = getContentScriptCssFiles([entry], cssMap);
server.reloadContentScript({
allFrames: resolvePerBrowserOption(
entry.options.allFrames,
config.browser,
wxt.config.browser,
),
excludeMatches: resolvePerBrowserOption(
entry.options.excludeMatches,
config.browser,
wxt.config.browser,
),
matches: resolvePerBrowserOption(entry.options.matches, config.browser),
runAt: resolvePerBrowserOption(entry.options.runAt, config.browser),
matches: resolvePerBrowserOption(
entry.options.matches,
wxt.config.browser,
),
runAt: resolvePerBrowserOption(entry.options.runAt, wxt.config.browser),
// @ts-expect-error: Chrome accepts this, not typed in webextension-polyfill (https://developer.chrome.com/docs/extensions/reference/scripting/#type-RegisteredContentScript)
world: resolvePerBrowserOption(entry.options.world, config.browser),
world: resolvePerBrowserOption(entry.options.world, wxt.config.browser),
js,
css,
});
@@ -278,10 +255,26 @@ function reloadContentScripts(
function reloadHtmlPages(
groups: EntrypointGroup[],
server: WxtDevServer,
config: InternalConfig,
) {
groups.flat().forEach((entry) => {
const path = getEntrypointBundlePath(entry, config.outDir, '.html');
): { 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');
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(', '));
}
+16 -17
View File
@@ -90,26 +90,25 @@ interface Template {
async function listTemplates(): Promise<Template[]> {
try {
const res = await fetch(
'https://api.github.com/repos/wxt-dev/wxt/contents/templates',
{
headers: {
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
},
},
);
const res = await fetch('https://ungh.cc/repos/wxt-dev/wxt/files/main');
if (res.status >= 300)
throw Error(`Request failed with status ${res.status} ${res.statusText}`);
const data = (await res.json()) as Array<{
type: 'file' | 'dir';
name: string;
path: string;
}>;
return data
.filter((item: any) => item.type === 'dir')
.map((item) => ({ name: item.name, path: item.path }))
const data = (await res.json()) as {
meta: {
sha: string;
};
files: Array<{
path: string;
mode: string;
sha: string;
size: number;
}>;
};
return data.files
.map((item) => item.path.match(/templates\/(.+)\/package\.json/)?.[1])
.filter((name) => name != null)
.map((name) => ({ name: name!, path: `templates/${name}` }))
.sort((l, r) => {
const lWeight = TEMPLATE_SORT_WEIGHT[l.name] ?? Number.MAX_SAFE_INTEGER;
const rWeight = TEMPLATE_SORT_WEIGHT[r.name] ?? Number.MAX_SAFE_INTEGER;
+6 -10
View File
@@ -1,15 +1,11 @@
import { InlineConfig } from '~/types';
import {
findEntrypoints,
generateTypesDir,
getInternalConfig,
} from '~/core/utils/building';
import { findEntrypoints, generateTypesDir } from '~/core/utils/building';
import { registerWxt, wxt } from './wxt';
export async function prepare(config: InlineConfig) {
const internalConfig = await getInternalConfig(config, 'build');
await registerWxt('build', config);
wxt.logger.info('Generating types...');
internalConfig.logger.info('Generating types...');
const entrypoints = await findEntrypoints(internalConfig);
await generateTypesDir(entrypoints, internalConfig);
const entrypoints = await findEntrypoints();
await generateTypesDir(entrypoints);
}
+27 -19
View File
@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from 'vitest';
import { createExtensionRunner } from '..';
import { fakeInternalConfig } from '~/core/utils/testing/fake-objects';
import { setFakeWxt } from '~/core/utils/testing/fake-objects';
import { mock } from 'vitest-mock-extended';
import { createSafariRunner } from '../safari';
import { createWslRunner } from '../wsl';
@@ -26,54 +26,62 @@ const createWebExtRunnerMock = vi.mocked(createWebExtRunner);
describe('createExtensionRunner', () => {
it('should return a Safari runner when browser is "safari"', async () => {
const config = fakeInternalConfig({
browser: 'safari',
setFakeWxt({
config: {
browser: 'safari',
},
});
const safariRunner = mock<ExtensionRunner>();
createSafariRunnerMock.mockReturnValue(safariRunner);
await expect(createExtensionRunner(config)).resolves.toBe(safariRunner);
await expect(createExtensionRunner()).resolves.toBe(safariRunner);
});
it('should return a WSL runner when `is-wsl` is true', async () => {
isWslMock.mockResolvedValueOnce(true);
const config = fakeInternalConfig({
browser: 'chrome',
setFakeWxt({
config: {
browser: 'chrome',
},
});
const wslRunner = mock<ExtensionRunner>();
createWslRunnerMock.mockReturnValue(wslRunner);
await expect(createExtensionRunner(config)).resolves.toBe(wslRunner);
await expect(createExtensionRunner()).resolves.toBe(wslRunner);
});
it('should return a manual runner when `runner.disabled` is true', async () => {
isWslMock.mockResolvedValueOnce(false);
const config = fakeInternalConfig({
browser: 'chrome',
runnerConfig: {
config: {
disabled: true,
setFakeWxt({
config: {
browser: 'chrome',
runnerConfig: {
config: {
disabled: true,
},
},
},
});
const manualRunner = mock<ExtensionRunner>();
createManualRunnerMock.mockReturnValue(manualRunner);
await expect(createExtensionRunner(config)).resolves.toBe(manualRunner);
await expect(createExtensionRunner()).resolves.toBe(manualRunner);
});
it('should return a web-ext runner otherwise', async () => {
const config = fakeInternalConfig({
browser: 'chrome',
runnerConfig: {
config: {
disabled: undefined,
setFakeWxt({
config: {
browser: 'chrome',
runnerConfig: {
config: {
disabled: undefined,
},
},
},
});
const manualRunner = mock<ExtensionRunner>();
createWebExtRunnerMock.mockReturnValue(manualRunner);
await expect(createExtensionRunner(config)).resolves.toBe(manualRunner);
await expect(createExtensionRunner()).resolves.toBe(manualRunner);
});
});
+5 -6
View File
@@ -1,17 +1,16 @@
import { InternalConfig, ExtensionRunner } from '~/types';
import { ExtensionRunner } from '~/types';
import { createWslRunner } from './wsl';
import { createWebExtRunner } from './web-ext';
import { createSafariRunner } from './safari';
import { createManualRunner } from './manual';
import { isWsl } from '~/core/utils/wsl';
import { wxt } from '../wxt';
export async function createExtensionRunner(
config: InternalConfig,
): Promise<ExtensionRunner> {
if (config.browser === 'safari') return createSafariRunner();
export async function createExtensionRunner(): Promise<ExtensionRunner> {
if (wxt.config.browser === 'safari') return createSafariRunner();
if (await isWsl()) return createWslRunner();
if (config.runnerConfig.config?.disabled) return createManualRunner();
if (wxt.config.runnerConfig.config?.disabled) return createManualRunner();
return createWebExtRunner();
}
+4 -3
View File
@@ -1,16 +1,17 @@
import { ExtensionRunner } from '~/types';
import { relative } from 'node:path';
import { wxt } from '../wxt';
/**
* The manual runner tells the user to load the unpacked extension manually.
*/
export function createManualRunner(): ExtensionRunner {
return {
async openBrowser(config) {
config.logger.info(
async openBrowser() {
wxt.logger.info(
`Load "${relative(
process.cwd(),
config.outDir,
wxt.config.outDir,
)}" as an unpacked extension manually`,
);
},
+4 -3
View File
@@ -1,16 +1,17 @@
import { ExtensionRunner } from '~/types';
import { relative } from 'node:path';
import { wxt } from '../wxt';
/**
* The Safari runner just logs a warning message because `web-ext` doesn't work with Safari.
*/
export function createSafariRunner(): ExtensionRunner {
return {
async openBrowser(config) {
config.logger.warn(
async openBrowser() {
wxt.logger.warn(
`Cannot Safari using web-ext. Load "${relative(
process.cwd(),
config.outDir,
wxt.config.outDir,
)}" as an unpacked extension manually`,
);
},
+17 -12
View File
@@ -2,6 +2,7 @@ import type { WebExtRunInstance } from 'web-ext-run';
import { ExtensionRunner } from '~/types';
import { formatDuration } from '../utils/time';
import defu from 'defu';
import { wxt } from '../wxt';
/**
* Create an `ExtensionRunner` backed by `web-ext`.
@@ -10,10 +11,13 @@ export function createWebExtRunner(): ExtensionRunner {
let runner: WebExtRunInstance | undefined;
return {
async openBrowser(config) {
async openBrowser() {
const startTime = Date.now();
if (config.browser === 'firefox' && config.manifestVersion === 3) {
if (
wxt.config.browser === 'firefox' &&
wxt.config.manifestVersion === 3
) {
throw Error(
'Dev mode does not support Firefox MV3. For alternatives, see https://github.com/wxt-dev/wxt/issues/230#issuecomment-1806881653',
);
@@ -22,16 +26,16 @@ export function createWebExtRunner(): ExtensionRunner {
// Use WXT's logger instead of web-ext's built-in one.
const webExtLogger = await import('web-ext-run/util/logger');
webExtLogger.consoleStream.write = ({ level, msg, name }) => {
if (level >= ERROR_LOG_LEVEL) config.logger.error(name, msg);
if (level >= WARN_LOG_LEVEL) config.logger.warn(msg);
if (level >= ERROR_LOG_LEVEL) wxt.logger.error(name, msg);
if (level >= WARN_LOG_LEVEL) wxt.logger.warn(msg);
};
const wxtUserConfig = config.runnerConfig.config;
const wxtUserConfig = wxt.config.runnerConfig.config;
const userConfig = {
console: wxtUserConfig?.openConsole,
devtools: wxtUserConfig?.openDevtools,
startUrl: wxtUserConfig?.startUrls,
...(config.browser === 'firefox'
...(wxt.config.browser === 'firefox'
? {
firefox: wxtUserConfig?.binaries?.firefox,
firefoxProfile: wxtUserConfig?.firefoxProfile,
@@ -39,7 +43,7 @@ export function createWebExtRunner(): ExtensionRunner {
args: wxtUserConfig?.firefoxArgs,
}
: {
chromiumBinary: wxtUserConfig?.binaries?.[config.browser],
chromiumBinary: wxtUserConfig?.binaries?.[wxt.config.browser],
chromiumProfile: wxtUserConfig?.chromiumProfile,
chromiumPref: defu(
wxtUserConfig?.chromiumPref,
@@ -51,8 +55,9 @@ export function createWebExtRunner(): ExtensionRunner {
const finalConfig = {
...userConfig,
target: config.browser === 'firefox' ? 'firefox-desktop' : 'chromium',
sourceDir: config.outDir,
target:
wxt.config.browser === 'firefox' ? 'firefox-desktop' : 'chromium',
sourceDir: wxt.config.outDir,
// WXT handles reloads, so disable auto-reload behaviors in web-ext
noReload: true,
noInput: true,
@@ -61,14 +66,14 @@ export function createWebExtRunner(): ExtensionRunner {
// Don't call `process.exit(0)` after starting web-ext
shouldExitProgram: false,
};
config.logger.debug('web-ext config:', finalConfig);
config.logger.debug('web-ext options:', options);
wxt.logger.debug('web-ext config:', finalConfig);
wxt.logger.debug('web-ext options:', options);
const webExt = await import('web-ext-run');
runner = await webExt.default.cmd.run(finalConfig, options);
const duration = Date.now() - startTime;
config.logger.success(`Opened browser in ${formatDuration(duration)}`);
wxt.logger.success(`Opened browser in ${formatDuration(duration)}`);
},
async closeBrowser() {
+4 -3
View File
@@ -1,16 +1,17 @@
import { ExtensionRunner } from '~/types';
import { relative } from 'node:path';
import { wxt } from '../wxt';
/**
* The WSL runner just logs a warning message because `web-ext` doesn't work in WSL.
*/
export function createWslRunner(): ExtensionRunner {
return {
async openBrowser(config) {
config.logger.warn(
async openBrowser() {
wxt.logger.warn(
`Cannot open browser when using WSL. Load "${relative(
process.cwd(),
config.outDir,
wxt.config.outDir,
)}" as an unpacked extension manually`,
);
},
@@ -1,14 +1,15 @@
import { describe, expect, it } from 'vitest';
import { describe, expect, it, beforeEach } from 'vitest';
import { hashContentScriptOptions } from '~/core/utils/content-scripts';
import { fakeInternalConfig } from '~/core/utils/testing/fake-objects';
import { setFakeWxt } from '~/core/utils/testing/fake-objects';
describe('Content Script Utils', () => {
beforeEach(() => {
setFakeWxt();
});
describe('hashContentScriptOptions', () => {
it('should return a string containing all the options with defaults applied', () => {
const hash = hashContentScriptOptions(
{ matches: [] },
fakeInternalConfig(),
);
const hash = hashContentScriptOptions({ matches: [] });
expect(hash).toMatchInlineSnapshot(
`"[["all_frames",false],["exclude_globs",[]],["exclude_matches",[]],["include_globs",[]],["match_about_blank",false],["match_origin_as_fallback",false],["matches",[]],["run_at","document_idle"],["world","ISOLATED"]]"`,
@@ -16,21 +17,15 @@ describe('Content Script Utils', () => {
});
it('should be consistent regardless of the object ordering and default values', () => {
const hash1 = hashContentScriptOptions(
{
allFrames: true,
matches: ['*://google.com/*', '*://duckduckgo.com/*'],
matchAboutBlank: false,
},
fakeInternalConfig(),
);
const hash2 = hashContentScriptOptions(
{
matches: ['*://duckduckgo.com/*', '*://google.com/*'],
allFrames: true,
},
fakeInternalConfig(),
);
const hash1 = hashContentScriptOptions({
allFrames: true,
matches: ['*://google.com/*', '*://duckduckgo.com/*'],
matchAboutBlank: false,
});
const hash2 = hashContentScriptOptions({
matches: ['*://duckduckgo.com/*', '*://google.com/*'],
allFrames: true,
});
expect(hash1).toBe(hash2);
});
+241 -163
View File
@@ -1,14 +1,14 @@
import { describe, expect, it } from 'vitest';
import { beforeEach, describe, expect, it } from 'vitest';
import { generateManifest, stripPathFromMatchPattern } from '../manifest';
import {
fakeArray,
fakeBackgroundEntrypoint,
fakeBuildOutput,
fakeEntrypoint,
fakeInternalConfig,
fakeManifestCommand,
fakeOptionsEntrypoint,
fakePopupEntrypoint,
setFakeWxt,
} from '../testing/fake-objects';
import { Manifest } from 'webextension-polyfill';
import {
@@ -17,11 +17,16 @@ import {
Entrypoint,
OutputAsset,
} from '~/types';
import { wxt } from '../../wxt';
const outDir = '/output';
const contentScriptOutDir = '/output/content-scripts';
describe('Manifest Utils', () => {
beforeEach(() => {
setFakeWxt();
});
describe('generateManifest', () => {
describe('popup', () => {
type ActionType = 'browser_action' | 'page_action';
@@ -41,9 +46,12 @@ describe('Manifest Utils', () => {
it('should include an action for mv3', async () => {
const popup = popupEntrypoint();
const buildOutput = fakeBuildOutput();
const config = fakeInternalConfig({
manifestVersion: 3,
outDir,
setFakeWxt({
config: {
manifestVersion: 3,
outDir,
},
});
const expected: Partial<Manifest.WebExtensionManifest> = {
action: {
@@ -56,7 +64,6 @@ describe('Manifest Utils', () => {
const { manifest: actual } = await generateManifest(
[popup],
buildOutput,
config,
);
expect(actual).toMatchObject(expected);
@@ -74,9 +81,11 @@ describe('Manifest Utils', () => {
async ({ inputType, expectedType }) => {
const popup = popupEntrypoint(inputType);
const buildOutput = fakeBuildOutput();
const config = fakeInternalConfig({
manifestVersion: 2,
outDir,
setFakeWxt({
config: {
manifestVersion: 2,
outDir,
},
});
const expected = {
default_icon: popup.options.defaultIcon,
@@ -87,7 +96,6 @@ describe('Manifest Utils', () => {
const { manifest: actual } = await generateManifest(
[popup],
buildOutput,
config,
);
expect(actual[expectedType]).toEqual(expected);
@@ -98,24 +106,22 @@ describe('Manifest Utils', () => {
describe('action without popup', () => {
it('should respect the action field in the manifest without a popup', async () => {
const buildOutput = fakeBuildOutput();
const config = fakeInternalConfig({
outDir,
manifest: {
action: {
default_icon: 'icon-16.png',
default_title: 'Example title',
setFakeWxt({
config: {
outDir,
manifest: {
action: {
default_icon: 'icon-16.png',
default_title: 'Example title',
},
},
},
});
const expected: Partial<Manifest.WebExtensionManifest> = {
action: config.manifest.action,
action: wxt.config.manifest.action,
};
const { manifest: actual } = await generateManifest(
[],
buildOutput,
config,
);
const { manifest: actual } = await generateManifest([], buildOutput);
expect(actual).toMatchObject(expected);
});
@@ -132,10 +138,12 @@ describe('Manifest Utils', () => {
});
it('should include a options_ui and chrome_style for chrome', async () => {
const config = fakeInternalConfig({
manifestVersion: 3,
outDir,
browser: 'chrome',
setFakeWxt({
config: {
manifestVersion: 3,
outDir,
browser: 'chrome',
},
});
const buildOutput = fakeBuildOutput();
const expected = {
@@ -147,17 +155,18 @@ describe('Manifest Utils', () => {
const { manifest: actual } = await generateManifest(
[options],
buildOutput,
config,
);
expect(actual.options_ui).toEqual(expected);
});
it('should include a options_ui and browser_style for firefox', async () => {
const config = fakeInternalConfig({
manifestVersion: 3,
browser: 'firefox',
outDir,
setFakeWxt({
config: {
manifestVersion: 3,
browser: 'firefox',
outDir,
},
});
const buildOutput = fakeBuildOutput();
const expected = {
@@ -169,7 +178,6 @@ describe('Manifest Utils', () => {
const { manifest: actual } = await generateManifest(
[options],
buildOutput,
config,
);
expect(actual.options_ui).toEqual(expected);
@@ -189,10 +197,12 @@ describe('Manifest Utils', () => {
it.each(['chrome', 'safari'])(
'should include a service worker and type for %s',
async (browser) => {
const config = fakeInternalConfig({
outDir,
manifestVersion: 3,
browser,
setFakeWxt({
config: {
outDir,
manifestVersion: 3,
browser,
},
});
const buildOutput = fakeBuildOutput();
const expected = {
@@ -203,7 +213,6 @@ describe('Manifest Utils', () => {
const { manifest: actual } = await generateManifest(
[background],
buildOutput,
config,
);
expect(actual.background).toEqual(expected);
@@ -211,10 +220,12 @@ describe('Manifest Utils', () => {
);
it('should include a background script and type for firefox', async () => {
const config = fakeInternalConfig({
outDir,
manifestVersion: 3,
browser: 'firefox',
setFakeWxt({
config: {
outDir,
manifestVersion: 3,
browser: 'firefox',
},
});
const buildOutput = fakeBuildOutput();
const expected = {
@@ -225,7 +236,6 @@ describe('Manifest Utils', () => {
const { manifest: actual } = await generateManifest(
[background],
buildOutput,
config,
);
expect(actual.background).toEqual(expected);
@@ -236,10 +246,12 @@ describe('Manifest Utils', () => {
it.each(['chrome', 'safari'])(
'should include scripts and persistent for %s',
async (browser) => {
const config = fakeInternalConfig({
outDir,
manifestVersion: 2,
browser,
setFakeWxt({
config: {
outDir,
manifestVersion: 2,
browser,
},
});
const buildOutput = fakeBuildOutput();
const expected = {
@@ -250,7 +262,6 @@ describe('Manifest Utils', () => {
const { manifest: actual } = await generateManifest(
[background],
buildOutput,
config,
);
expect(actual.background).toEqual(expected);
@@ -258,10 +269,12 @@ describe('Manifest Utils', () => {
);
it('should include a background script and persistent for firefox mv2', async () => {
const config = fakeInternalConfig({
outDir,
manifestVersion: 2,
browser: 'firefox',
setFakeWxt({
config: {
outDir,
manifestVersion: 2,
browser: 'firefox',
},
});
const buildOutput = fakeBuildOutput();
const expected = {
@@ -272,7 +285,6 @@ describe('Manifest Utils', () => {
const { manifest: actual } = await generateManifest(
[background],
buildOutput,
config,
);
expect(actual.background).toEqual(expected);
@@ -293,12 +305,10 @@ describe('Manifest Utils', () => {
{ type: 'asset', fileName: 'icons/128x128.png' },
],
});
const config = fakeInternalConfig();
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.icons).toEqual({
@@ -319,12 +329,10 @@ describe('Manifest Utils', () => {
{ type: 'asset', fileName: 'icon-16.jpeg' },
],
});
const config = fakeInternalConfig();
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.icons).toBeUndefined();
@@ -346,16 +354,17 @@ describe('Manifest Utils', () => {
32: 'logo-32.png',
48: 'logo-48.png',
};
const config = fakeInternalConfig({
manifest: {
icons: expected,
setFakeWxt({
config: {
manifest: {
icons: expected,
},
},
});
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.icons).toEqual(expected);
@@ -440,10 +449,12 @@ describe('Manifest Utils', () => {
};
const entrypoints = [cs1, cs2, cs3, cs4, cs5];
const config = fakeInternalConfig({
command: 'build',
outDir,
manifestVersion: 3,
setFakeWxt({
config: {
command: 'build',
outDir,
manifestVersion: 3,
},
});
const buildOutput: Omit<BuildOutput, 'manifest'> = {
publicAssets: [],
@@ -459,7 +470,6 @@ describe('Manifest Utils', () => {
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.content_scripts).toContainEqual({
@@ -509,18 +519,19 @@ describe('Manifest Utils', () => {
const entrypoints = [cs];
const buildOutput = fakeBuildOutput();
const config = fakeInternalConfig({
outDir,
command: 'build',
manifest: {
content_scripts: [userContentScript],
setFakeWxt({
config: {
outDir,
command: 'build',
manifest: {
content_scripts: [userContentScript],
},
},
});
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.content_scripts).toContainEqual(userContentScript);
@@ -552,15 +563,16 @@ describe('Manifest Utils', () => {
publicAssets: [],
steps: [{ entrypoints: cs, chunks: [styles] }],
};
const config = fakeInternalConfig({
outDir,
command: 'build',
setFakeWxt({
config: {
outDir,
command: 'build',
},
});
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.content_scripts).toEqual([
@@ -597,15 +609,16 @@ describe('Manifest Utils', () => {
publicAssets: [],
steps: [{ entrypoints: cs, chunks: [styles] }],
};
const config = fakeInternalConfig({
outDir,
command: 'build',
setFakeWxt({
config: {
outDir,
command: 'build',
},
});
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.content_scripts).toEqual([
@@ -639,16 +652,17 @@ describe('Manifest Utils', () => {
publicAssets: [],
steps: [{ entrypoints: cs, chunks: [styles] }],
};
const config = fakeInternalConfig({
outDir,
command: 'build',
manifestVersion: 3,
setFakeWxt({
config: {
outDir,
command: 'build',
manifestVersion: 3,
},
});
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.web_accessible_resources).toEqual([
@@ -681,16 +695,17 @@ describe('Manifest Utils', () => {
publicAssets: [],
steps: [{ entrypoints: cs, chunks: [styles] }],
};
const config = fakeInternalConfig({
outDir,
command: 'build',
manifestVersion: 2,
setFakeWxt({
config: {
outDir,
command: 'build',
manifestVersion: 2,
},
});
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.web_accessible_resources).toEqual([
@@ -720,16 +735,17 @@ describe('Manifest Utils', () => {
publicAssets: [],
steps: [{ entrypoints: cs, chunks: [styles] }],
};
const config = fakeInternalConfig({
outDir,
command: 'build',
manifestVersion: 3,
setFakeWxt({
config: {
outDir,
command: 'build',
manifestVersion: 3,
},
});
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.web_accessible_resources).toEqual([
@@ -765,21 +781,22 @@ describe('Manifest Utils', () => {
publicAssets: [],
steps: [{ entrypoints: cs, chunks: [styles] }],
};
const config = fakeInternalConfig({
outDir,
command: 'build',
manifestVersion: 3,
manifest: {
web_accessible_resources: [
{ resources: ['one.png'], matches: ['*://one.com/*'] },
],
setFakeWxt({
config: {
outDir,
command: 'build',
manifestVersion: 3,
manifest: {
web_accessible_resources: [
{ resources: ['one.png'], matches: ['*://one.com/*'] },
],
},
},
});
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.web_accessible_resources).toEqual([
@@ -813,19 +830,20 @@ describe('Manifest Utils', () => {
publicAssets: [],
steps: [{ entrypoints: cs, chunks: [styles] }],
};
const config = fakeInternalConfig({
outDir,
command: 'build',
manifestVersion: 2,
manifest: {
web_accessible_resources: ['one.png'],
setFakeWxt({
config: {
outDir,
command: 'build',
manifestVersion: 2,
manifest: {
web_accessible_resources: ['one.png'],
},
},
});
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.web_accessible_resources).toEqual([
@@ -833,6 +851,55 @@ describe('Manifest Utils', () => {
'content-scripts/one.css',
]);
});
it('should convert mv3 items to mv2 strings automatically', async () => {
setFakeWxt({
config: {
outDir,
manifestVersion: 2,
manifest: {
web_accessible_resources: [
{
matches: ['*://*/*'],
resources: ['/icon-128.png'],
},
{
matches: ['https://google.com'],
resources: ['/icon-128.png', '/icon-32.png'],
},
],
},
},
});
const { manifest: actual } = await generateManifest(
[],
fakeBuildOutput(),
);
expect(actual.web_accessible_resources).toEqual([
'/icon-128.png',
'/icon-32.png',
]);
});
it('should convert mv2 strings to mv3 items with a warning automatically', async () => {
setFakeWxt({
config: {
outDir,
manifestVersion: 3,
manifest: {
web_accessible_resources: ['/icon.svg'],
},
},
});
await expect(() =>
generateManifest([], fakeBuildOutput()),
).rejects.toThrow(
'Non-MV3 web_accessible_resources detected: ["/icon.svg"]. When manually defining web_accessible_resources, define them as MV3 objects ({ matches: [...], resources: [...] }), and WXT will automatically convert them to MV2 when necessary.',
);
});
});
describe('transformManifest option', () => {
@@ -840,9 +907,11 @@ describe('Manifest Utils', () => {
const entrypoints: Entrypoint[] = [];
const buildOutput = fakeBuildOutput();
const newAuthor = 'Custom Author';
const config = fakeInternalConfig({
transformManifest(manifest: any) {
manifest.author = newAuthor;
setFakeWxt({
config: {
transformManifest(manifest: any) {
manifest.author = newAuthor;
},
},
});
const expected = {
@@ -852,7 +921,6 @@ describe('Manifest Utils', () => {
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual).toMatchObject(expected);
@@ -867,18 +935,19 @@ describe('Manifest Utils', () => {
const versionName = '1.0.0-alpha1';
const entrypoints: Entrypoint[] = [];
const buildOutput = fakeBuildOutput();
const config = fakeInternalConfig({
browser,
manifest: {
version,
version_name: versionName,
setFakeWxt({
config: {
browser,
manifest: {
version,
version_name: versionName,
},
},
});
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.version).toBe(version);
@@ -893,18 +962,19 @@ describe('Manifest Utils', () => {
const versionName = '1.0.0-alpha1';
const entrypoints: Entrypoint[] = [];
const buildOutput = fakeBuildOutput();
const config = fakeInternalConfig({
browser,
manifest: {
version,
version_name: versionName,
setFakeWxt({
config: {
browser,
manifest: {
version,
version_name: versionName,
},
},
});
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.version).toBe(version);
@@ -918,18 +988,19 @@ describe('Manifest Utils', () => {
const version = '1.0.0';
const entrypoints: Entrypoint[] = [];
const buildOutput = fakeBuildOutput();
const config = fakeInternalConfig({
browser,
manifest: {
version,
version_name: version,
setFakeWxt({
config: {
browser,
manifest: {
version,
version_name: version,
},
},
});
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.version).toBe(version);
@@ -940,23 +1011,24 @@ describe('Manifest Utils', () => {
it('should log a warning if the version could not be detected', async () => {
const entrypoints: Entrypoint[] = [];
const buildOutput = fakeBuildOutput();
const config = fakeInternalConfig({
manifest: {
// @ts-ignore: Purposefully removing version from fake object
version: null,
setFakeWxt({
config: {
manifest: {
// @ts-ignore: Purposefully removing version from fake object
version: null,
},
},
});
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.version).toBe('0.0.0');
expect(actual.version_name).toBeUndefined();
expect(config.logger.warn).toBeCalledTimes(1);
expect(config.logger.warn).toBeCalledWith(
expect(wxt.logger.warn).toBeCalledTimes(1);
expect(wxt.logger.warn).toBeCalledWith(
expect.stringContaining('Extension version not found'),
);
});
@@ -972,14 +1044,15 @@ describe('Manifest Utils', () => {
};
it('should include a command for reloading the extension during development', async () => {
const config = fakeInternalConfig({ command: 'serve' });
setFakeWxt({
config: { command: 'serve' },
});
const output = fakeBuildOutput();
const entrypoints = fakeArray(fakeEntrypoint);
const { manifest: actual } = await generateManifest(
entrypoints,
output,
config,
);
expect(actual.commands).toEqual({
@@ -988,10 +1061,12 @@ describe('Manifest Utils', () => {
});
it('should customize the reload commands key binding if passing a custom command', async () => {
const config = fakeInternalConfig({
command: 'serve',
dev: {
reloadCommand: 'Ctrl+E',
setFakeWxt({
config: {
command: 'serve',
dev: {
reloadCommand: 'Ctrl+E',
},
},
});
const output = fakeBuildOutput();
@@ -1000,7 +1075,6 @@ describe('Manifest Utils', () => {
const { manifest: actual } = await generateManifest(
entrypoints,
output,
config,
);
expect(actual.commands).toEqual({
@@ -1014,10 +1088,12 @@ describe('Manifest Utils', () => {
});
it("should not include the reload command when it's been disabled", async () => {
const config = fakeInternalConfig({
command: 'serve',
dev: {
reloadCommand: false,
setFakeWxt({
config: {
command: 'serve',
dev: {
reloadCommand: false,
},
},
});
const output = fakeBuildOutput();
@@ -1026,7 +1102,6 @@ describe('Manifest Utils', () => {
const { manifest: actual } = await generateManifest(
entrypoints,
output,
config,
);
expect(actual.commands).toBeUndefined();
@@ -1035,11 +1110,13 @@ describe('Manifest Utils', () => {
it('should not override any existing commands when adding the one to reload the extension', async () => {
const customCommandName = 'custom-command';
const customCommand = fakeManifestCommand();
const config = fakeInternalConfig({
command: 'serve',
manifest: {
commands: {
[customCommandName]: customCommand,
setFakeWxt({
config: {
command: 'serve',
manifest: {
commands: {
[customCommandName]: customCommand,
},
},
},
});
@@ -1049,7 +1126,6 @@ describe('Manifest Utils', () => {
const { manifest: actual } = await generateManifest(
entrypoints,
output,
config,
);
expect(actual.commands).toEqual({
@@ -1065,9 +1141,11 @@ describe('Manifest Utils', () => {
command3: fakeManifestCommand(),
command4: fakeManifestCommand(),
};
const config = fakeInternalConfig({
command: 'serve',
manifest: { commands },
setFakeWxt({
config: {
command: 'serve',
manifest: { commands },
},
});
const output = fakeBuildOutput();
const entrypoints = fakeArray(fakeEntrypoint);
@@ -1075,7 +1153,6 @@ describe('Manifest Utils', () => {
const { manifest: actual, warnings } = await generateManifest(
entrypoints,
output,
config,
);
expect(actual.commands).toEqual(commands);
@@ -1083,14 +1160,15 @@ describe('Manifest Utils', () => {
});
it('should not include the command when building an extension', async () => {
const config = fakeInternalConfig({ command: 'build' });
setFakeWxt({
config: { command: 'build' },
});
const output = fakeBuildOutput();
const entrypoints = fakeArray(fakeEntrypoint);
const { manifest: actual } = await generateManifest(
entrypoints,
output,
config,
);
expect(actual.commands).toBeUndefined();
+12 -3
View File
@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest';
import { getPackageJson } from '../package';
import { fakeInternalConfig } from '../testing/fake-objects';
import { setFakeWxt } from '../testing/fake-objects';
import { mock } from 'vitest-mock-extended';
import { Logger } from '~/types';
@@ -8,7 +8,11 @@ describe('Package JSON Utils', () => {
describe('getPackageJson', () => {
it('should return the package.json inside <root>/package.json', async () => {
const root = process.cwd(); // WXT project directory
const actual = await getPackageJson(fakeInternalConfig({ root }));
setFakeWxt({
config: { root },
});
const actual = await getPackageJson();
expect(actual).toMatchObject({
name: 'wxt',
@@ -18,7 +22,12 @@ describe('Package JSON Utils', () => {
it("should return an empty object when <root>/package.json doesn't exist", async () => {
const root = '/some/path/that/does/not/exist';
const logger = mock<Logger>();
const actual = await getPackageJson(fakeInternalConfig({ root, logger }));
setFakeWxt({
config: { root, logger },
logger,
});
const actual = await getPackageJson();
expect(actual).toEqual({});
});
@@ -1,4 +1,4 @@
import { describe, expect, it } from 'vitest';
import { beforeEach, describe, expect, it } from 'vitest';
import { DevModeChange, detectDevChanges } from '~/core/utils/building';
import {
fakeBackgroundEntrypoint,
@@ -10,14 +10,19 @@ import {
fakePopupEntrypoint,
fakeOutputAsset,
fakeOutputChunk,
fakeInternalConfig,
fakeWxt,
setFakeWxt,
} from '~/core/utils/testing/fake-objects';
import { BuildOutput, BuildStepOutput } from '~/types';
import { setWxtForTesting } from '../../../wxt';
describe('Detect Dev Changes', () => {
beforeEach(() => {
setWxtForTesting(fakeWxt());
});
describe('No changes', () => {
it("should return 'no-change' when the changed file isn't used by any of the entrypoints", () => {
const config = fakeInternalConfig();
const changes = ['/some/path.ts'];
const currentOutput: BuildOutput = {
manifest: fakeManifest(),
@@ -34,7 +39,7 @@ describe('Detect Dev Changes', () => {
],
};
const actual = detectDevChanges(config, changes, currentOutput);
const actual = detectDevChanges(changes, currentOutput);
expect(actual).toEqual({ type: 'no-change' });
});
@@ -43,9 +48,11 @@ describe('Detect Dev Changes', () => {
describe('wxt.config.ts', () => {
it("should return 'full-restart' when one of the changed files is the config file", () => {
const configFile = '/root/wxt.config.ts';
const config = fakeInternalConfig({
userConfigMetadata: {
configFile,
setFakeWxt({
config: {
userConfigMetadata: {
configFile,
},
},
});
const changes = ['/root/src/public/image.svg', configFile];
@@ -58,7 +65,7 @@ describe('Detect Dev Changes', () => {
type: 'full-restart',
};
const actual = detectDevChanges(config, changes, currentOutput);
const actual = detectDevChanges(changes, currentOutput);
expect(actual).toEqual(expected);
});
@@ -67,9 +74,11 @@ describe('Detect Dev Changes', () => {
describe('web-ext.config.ts', () => {
it("should return 'browser-restart' when one of the changed files is the config file", () => {
const runnerFile = '/root/web-ext.config.ts';
const config = fakeInternalConfig({
runnerConfig: {
configFile: runnerFile,
setFakeWxt({
config: {
runnerConfig: {
configFile: runnerFile,
},
},
});
const changes = ['/root/src/public/image.svg', runnerFile];
@@ -82,7 +91,7 @@ describe('Detect Dev Changes', () => {
type: 'browser-restart',
};
const actual = detectDevChanges(config, changes, currentOutput);
const actual = detectDevChanges(changes, currentOutput);
expect(actual).toEqual(expected);
});
@@ -90,7 +99,6 @@ describe('Detect Dev Changes', () => {
describe('Public Assets', () => {
it("should return 'extension-reload' without any groups to rebuild when the changed file is a public asset", () => {
const config = fakeInternalConfig();
const changes = ['/root/src/public/image.svg'];
const asset1 = fakeOutputAsset({
fileName: 'image.svg',
@@ -112,7 +120,7 @@ describe('Detect Dev Changes', () => {
},
};
const actual = detectDevChanges(config, changes, currentOutput);
const actual = detectDevChanges(changes, currentOutput);
expect(actual).toEqual(expected);
});
@@ -120,7 +128,6 @@ describe('Detect Dev Changes', () => {
describe('Background', () => {
it("should rebuild the background and reload the extension when the changed file in it's chunks' `moduleIds` field", () => {
const config = fakeInternalConfig();
const changedPath = '/root/utils/shared.ts';
const contentScript = fakeContentScriptEntrypoint({
inputPath: '/root/overlay.content.ts',
@@ -160,7 +167,7 @@ describe('Detect Dev Changes', () => {
rebuildGroups: [background],
};
const actual = detectDevChanges(config, [changedPath], currentOutput);
const actual = detectDevChanges([changedPath], currentOutput);
expect(actual).toEqual(expected);
});
@@ -168,7 +175,6 @@ describe('Detect Dev Changes', () => {
describe('HTML Pages', () => {
it('should detect changes to entrypoints/<name>.html files', async () => {
const config = fakeInternalConfig();
const changedPath = '/root/page1.html';
const htmlPage1 = fakePopupEntrypoint({
inputPath: changedPath,
@@ -212,13 +218,12 @@ describe('Detect Dev Changes', () => {
rebuildGroups: [[htmlPage1, htmlPage2]],
};
const actual = detectDevChanges(config, [changedPath], currentOutput);
const actual = detectDevChanges([changedPath], currentOutput);
expect(actual).toEqual(expected);
});
it('should detect changes to entrypoints/<name>/index.html files', async () => {
const config = fakeInternalConfig();
const changedPath = '/root/page1/index.html';
const htmlPage1 = fakePopupEntrypoint({
inputPath: changedPath,
@@ -262,7 +267,7 @@ describe('Detect Dev Changes', () => {
rebuildGroups: [[htmlPage1, htmlPage2]],
};
const actual = detectDevChanges(config, [changedPath], currentOutput);
const actual = detectDevChanges([changedPath], currentOutput);
expect(actual).toEqual(expected);
});
@@ -270,7 +275,6 @@ describe('Detect Dev Changes', () => {
describe('Content Scripts', () => {
it('should rebuild then reload only the effected content scripts', async () => {
const config = fakeInternalConfig();
const changedPath = '/root/utils/shared.ts';
const script1 = fakeContentScriptEntrypoint({
inputPath: '/root/overlay1.content/index.ts',
@@ -322,7 +326,7 @@ describe('Detect Dev Changes', () => {
rebuildGroups: [script1, script3],
};
const actual = detectDevChanges(config, [changedPath], currentOutput);
const actual = detectDevChanges([changedPath], currentOutput);
expect(actual).toEqual(expected);
});
@@ -1,4 +1,4 @@
import { describe, it, expect, vi } from 'vitest';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import {
BackgroundEntrypoint,
ContentScriptEntrypoint,
@@ -11,7 +11,10 @@ import { findEntrypoints } from '../find-entrypoints';
import fs from 'fs-extra';
import { importEntrypointFile } from '../import-entrypoint';
import glob from 'fast-glob';
import { fakeInternalConfig } from '~/core/utils/testing/fake-objects';
import {
fakeResolvedConfig,
setFakeWxt,
} from '~/core/utils/testing/fake-objects';
import { unnormalizePath } from '~/core/utils/paths';
vi.mock('../import-entrypoint');
@@ -26,13 +29,18 @@ const readFileMock = vi.mocked(
);
describe('findEntrypoints', () => {
const config = fakeInternalConfig({
const config = fakeResolvedConfig({
manifestVersion: 3,
root: '/',
entrypointsDir: resolve('/src/entrypoints'),
outDir: resolve('.output'),
command: 'build',
});
beforeEach(() => {
setFakeWxt({ config });
});
it.each<[string, string, PopupEntrypoint]>([
[
'popup.html',
@@ -82,7 +90,7 @@ describe('findEntrypoints', () => {
globMock.mockResolvedValueOnce([path]);
readFileMock.mockResolvedValueOnce(content);
const entrypoints = await findEntrypoints(config);
const entrypoints = await findEntrypoints();
expect(entrypoints).toHaveLength(1);
expect(entrypoints[0]).toEqual(expected);
@@ -135,7 +143,7 @@ describe('findEntrypoints', () => {
globMock.mockResolvedValueOnce([path]);
readFileMock.mockResolvedValueOnce(content);
const entrypoints = await findEntrypoints(config);
const entrypoints = await findEntrypoints();
expect(entrypoints).toHaveLength(1);
expect(entrypoints[0]).toEqual(expected);
@@ -202,14 +210,11 @@ describe('findEntrypoints', () => {
globMock.mockResolvedValueOnce([path]);
importEntrypointFileMock.mockResolvedValue(options);
const entrypoints = await findEntrypoints(config);
const entrypoints = await findEntrypoints();
expect(entrypoints).toHaveLength(1);
expect(entrypoints[0]).toEqual({ ...expected, options });
expect(importEntrypointFileMock).toBeCalledWith(
expected.inputPath,
config,
);
expect(importEntrypointFileMock).toBeCalledWith(expected.inputPath);
},
);
@@ -243,24 +248,58 @@ describe('findEntrypoints', () => {
globMock.mockResolvedValueOnce([path]);
importEntrypointFileMock.mockResolvedValue(options);
const entrypoints = await findEntrypoints(config);
const entrypoints = await findEntrypoints();
expect(entrypoints).toHaveLength(1);
expect(entrypoints[0]).toEqual({ ...expected, options });
expect(importEntrypointFileMock).toBeCalledWith(
expected.inputPath,
config,
);
expect(importEntrypointFileMock).toBeCalledWith(expected.inputPath);
},
);
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({
...config,
command: 'serve',
});
const entrypoints = await findEntrypoints();
expect(entrypoints).toHaveLength(2);
expect(entrypoints).toContainEqual({
@@ -296,14 +335,11 @@ describe('findEntrypoints', () => {
globMock.mockResolvedValueOnce([path]);
importEntrypointFileMock.mockResolvedValue(options);
const entrypoints = await findEntrypoints(config);
const entrypoints = await findEntrypoints();
expect(entrypoints).toHaveLength(1);
expect(entrypoints[0]).toEqual({ ...expected, options });
expect(importEntrypointFileMock).toBeCalledWith(
expected.inputPath,
config,
);
expect(importEntrypointFileMock).toBeCalledWith(expected.inputPath);
},
);
@@ -592,7 +628,7 @@ describe('findEntrypoints', () => {
])('should find entrypoint for %s', async (path, expected) => {
globMock.mockResolvedValueOnce([path]);
const entrypoints = await findEntrypoints(config);
const entrypoints = await findEntrypoints();
expect(entrypoints).toHaveLength(1);
expect(entrypoints[0]).toEqual(expected);
@@ -608,7 +644,7 @@ describe('findEntrypoints', () => {
'ui.html',
]);
await expect(() => findEntrypoints(config)).rejects.toThrowError(
await expect(() => findEntrypoints()).rejects.toThrowError(
[
'Multiple entrypoints with the same name detected, only one entrypoint for each name is allowed.',
'',
@@ -626,7 +662,7 @@ describe('findEntrypoints', () => {
it('throw an error if there are no entrypoints', async () => {
globMock.mockResolvedValueOnce([]);
await expect(() => findEntrypoints(config)).rejects.toThrowError(
await expect(() => findEntrypoints()).rejects.toThrowError(
`No entrypoints found in ${unnormalizePath(config.entrypointsDir)}`,
);
});
@@ -638,7 +674,7 @@ describe('findEntrypoints', () => {
include: ['not' + config.browser],
});
const entrypoints = await findEntrypoints(config);
const entrypoints = await findEntrypoints();
expect(entrypoints).toEqual([]);
});
@@ -649,7 +685,7 @@ describe('findEntrypoints', () => {
include: ['not' + config.browser],
});
const entrypoints = await findEntrypoints(config);
const entrypoints = await findEntrypoints();
expect(entrypoints).toEqual([]);
});
@@ -666,7 +702,7 @@ describe('findEntrypoints', () => {
</html>`,
);
const entrypoints = await findEntrypoints(config);
const entrypoints = await findEntrypoints();
expect(entrypoints).toEqual([]);
});
@@ -683,7 +719,7 @@ describe('findEntrypoints', () => {
</html>`,
);
const entrypoints = await findEntrypoints(config);
const entrypoints = await findEntrypoints();
expect(entrypoints).toEqual([]);
});
@@ -700,7 +736,7 @@ describe('findEntrypoints', () => {
</html>`,
);
const entrypoints = await findEntrypoints(config);
const entrypoints = await findEntrypoints();
expect(entrypoints).toEqual([]);
});
@@ -713,7 +749,7 @@ describe('findEntrypoints', () => {
exclude: [config.browser],
});
const entrypoints = await findEntrypoints(config);
const entrypoints = await findEntrypoints();
expect(entrypoints).toEqual([]);
});
@@ -724,7 +760,7 @@ describe('findEntrypoints', () => {
exclude: [config.browser],
});
const entrypoints = await findEntrypoints(config);
const entrypoints = await findEntrypoints();
expect(entrypoints).toEqual([]);
});
@@ -739,7 +775,7 @@ describe('findEntrypoints', () => {
</html>`,
);
const entrypoints = await findEntrypoints(config);
const entrypoints = await findEntrypoints();
expect(entrypoints).toEqual([]);
});
@@ -754,7 +790,7 @@ describe('findEntrypoints', () => {
</html>`,
);
const entrypoints = await findEntrypoints(config);
const entrypoints = await findEntrypoints();
expect(entrypoints).toEqual([]);
});
@@ -769,7 +805,7 @@ describe('findEntrypoints', () => {
</html>`,
);
const entrypoints = await findEntrypoints(config);
const entrypoints = await findEntrypoints();
expect(entrypoints).toEqual([]);
});
@@ -785,15 +821,17 @@ describe('findEntrypoints', () => {
]);
importEntrypointFileMock.mockResolvedValue({});
const filterEntrypoints = ['popup', 'ui'];
const config = fakeInternalConfig({
root: '/',
entrypointsDir: resolve('/src/entrypoints'),
outDir: resolve('.output'),
command: 'build',
filterEntrypoints: new Set(filterEntrypoints),
setFakeWxt({
config: {
root: '/',
entrypointsDir: resolve('/src/entrypoints'),
outDir: resolve('.output'),
command: 'build',
filterEntrypoints: new Set(filterEntrypoints),
},
});
const entrypoints = await findEntrypoints(config);
const entrypoints = await findEntrypoints();
const names = entrypoints.map((item) => item.name);
expect(names).toHaveLength(2);
expect(names).toEqual(filterEntrypoints);
@@ -1,6 +1,11 @@
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',
@@ -141,4 +146,29 @@ 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,21 +1,25 @@
import { describe, expect, it } from 'vitest';
import { beforeEach, describe, expect, it } from 'vitest';
import { importEntrypointFile } from '~/core/utils/building';
import { fakeInternalConfig } from '~/core/utils/testing/fake-objects';
import { resolve } from 'node:path';
import { unnormalizePath } from '../../paths';
import { setFakeWxt } from '../../testing/fake-objects';
const entrypointPath = (filename: string) =>
resolve('src/core/utils/__tests__/test-entrypoints', filename);
const config = fakeInternalConfig({
imports: false,
debug: false,
// Run inside the demo folder so that wxt is in the node_modules
// WXT must also be built for these tests to pass
root: 'demo',
});
describe('importEntrypointFile', () => {
beforeEach(() => {
setFakeWxt({
config: {
imports: false,
debug: false,
// Run inside the demo folder so that wxt is in the node_modules
// WXT must also be built for these tests to pass
root: 'demo',
},
});
});
it.each([
['background.ts', { main: expect.any(Function) }],
['content.ts', { main: expect.any(Function), matches: ['<all_urls>'] }],
@@ -25,7 +29,7 @@ describe('importEntrypointFile', () => {
])(
'should return the default export of test-entrypoints/%s',
async (file, expected) => {
const actual = await importEntrypointFile(entrypointPath(file), config);
const actual = await importEntrypointFile(entrypointPath(file));
expect(actual).toEqual(expected);
},
@@ -34,7 +38,6 @@ describe('importEntrypointFile', () => {
it('should return undefined when there is no default export', async () => {
const actual = await importEntrypointFile(
entrypointPath('no-default-export.ts'),
config,
);
expect(actual).toBeUndefined();
@@ -45,7 +48,7 @@ describe('importEntrypointFile', () => {
'../src/core/utils/__tests__/test-entrypoints/imported-option.ts',
);
await expect(() =>
importEntrypointFile(entrypointPath('imported-option.ts'), config),
importEntrypointFile(entrypointPath('imported-option.ts')),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: ${filePath}: Cannot use imported variable "faker" outside the main function. See https://wxt.dev/guide/entrypoints.html#side-effects]`,
);
+9 -15
View File
@@ -1,18 +1,13 @@
import {
BuildOutput,
BuildStepOutput,
EntrypointGroup,
InternalConfig,
} from '~/types';
import { BuildOutput, BuildStepOutput, EntrypointGroup } from '~/types';
import { getPublicFiles } from '~/core/utils/fs';
import fs from 'fs-extra';
import { dirname, resolve } from 'path';
import type { Ora } from 'ora';
import pc from 'picocolors';
import { wxt } from '../../wxt';
export async function buildEntrypoints(
groups: EntrypointGroup[],
config: InternalConfig,
spinner: Ora,
): Promise<Omit<BuildOutput, 'manifest'>> {
const steps: BuildStepOutput[] = [];
@@ -23,26 +18,25 @@ export async function buildEntrypoints(
spinner.text =
pc.dim(`[${i + 1}/${groups.length}]`) + ` ${groupNameColored}`;
try {
steps.push(await config.builder.build(group));
steps.push(await wxt.config.builder.build(group));
} catch (err) {
wxt.logger.error(err);
throw Error(`Failed to build ${groupNames.join(', ')}`, { cause: err });
}
}
const publicAssets = await copyPublicDirectory(config);
const publicAssets = await copyPublicDirectory();
return { publicAssets, steps };
}
async function copyPublicDirectory(
config: InternalConfig,
): Promise<BuildOutput['publicAssets']> {
const files = await getPublicFiles(config);
async function copyPublicDirectory(): Promise<BuildOutput['publicAssets']> {
const files = await getPublicFiles();
if (files.length === 0) return [];
const publicAssets: BuildOutput['publicAssets'] = [];
for (const file of files) {
const srcPath = resolve(config.publicDir, file);
const outPath = resolve(config.outDir, file);
const srcPath = resolve(wxt.config.publicDir, file);
const outPath = resolve(wxt.config.outDir, file);
await fs.ensureDir(dirname(outPath));
await fs.copyFile(srcPath, outPath);
@@ -2,12 +2,12 @@ import {
BuildOutput,
BuildStepOutput,
EntrypointGroup,
InternalConfig,
OutputAsset,
OutputFile,
} from '~/types';
import { every, some } from '~/core/utils/arrays';
import { normalizePath } from '~/core/utils/paths';
import { wxt } from '../../wxt';
/**
* Compare the changed files vs the build output and determine what kind of reload needs to happen:
@@ -30,19 +30,18 @@ import { normalizePath } from '~/core/utils/paths';
* - Config file changed (wxt.config.ts, .env, web-ext.config.ts, etc)
*/
export function detectDevChanges(
config: InternalConfig,
changedFiles: string[],
currentOutput: BuildOutput,
): DevModeChange {
const isConfigChange = some(
changedFiles,
(file) => file === config.userConfigMetadata.configFile,
(file) => file === wxt.config.userConfigMetadata.configFile,
);
if (isConfigChange) return { type: 'full-restart' };
const isRunnerChange = some(
changedFiles,
(file) => file === config.runnerConfig.configFile,
(file) => file === wxt.config.runnerConfig.configFile,
);
if (isRunnerChange) return { type: 'browser-restart' };
+84 -77
View File
@@ -7,7 +7,6 @@ import {
ContentScriptEntrypoint,
Entrypoint,
GenericEntrypoint,
InternalConfig,
OptionsEntrypoint,
PopupEntrypoint,
UnlistedScriptDefinition,
@@ -25,15 +24,14 @@ import {
import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '~/core/utils/constants';
import { CSS_EXTENSIONS_PATTERN } from '~/core/utils/paths';
import pc from 'picocolors';
import { wxt } from '../../wxt';
/**
* Return entrypoints and their configuration by looking through the project's files.
*/
export async function findEntrypoints(
config: InternalConfig,
): Promise<Entrypoint[]> {
export async function findEntrypoints(): Promise<Entrypoint[]> {
const relativePaths = await glob(Object.keys(PATH_GLOB_TO_TYPE_MAP), {
cwd: config.entrypointsDir,
cwd: wxt.config.entrypointsDir,
});
// Ensure consistent output
relativePaths.sort();
@@ -42,8 +40,8 @@ export async function findEntrypoints(
const entrypointInfos: EntrypointInfo[] = relativePaths.reduce<
EntrypointInfo[]
>((results, relativePath) => {
const inputPath = resolve(config.entrypointsDir, relativePath);
const name = getEntrypointName(config.entrypointsDir, inputPath);
const inputPath = resolve(wxt.config.entrypointsDir, relativePath);
const name = getEntrypointName(wxt.config.entrypointsDir, inputPath);
const matchingGlob = pathGlobs.find((glob) =>
minimatch(relativePath, glob),
);
@@ -54,16 +52,16 @@ export async function findEntrypoints(
inputPath,
type,
skipped:
config.filterEntrypoints != null &&
!config.filterEntrypoints.has(name),
wxt.config.filterEntrypoints != null &&
!wxt.config.filterEntrypoints.has(name),
});
}
return results;
}, []);
// Validation
preventNoEntrypoints(config, entrypointInfos);
preventDuplicateEntrypointNames(config, entrypointInfos);
preventNoEntrypoints(entrypointInfos);
preventDuplicateEntrypointNames(entrypointInfos);
// Import entrypoints to get their config
let hasBackground = false;
@@ -72,23 +70,23 @@ export async function findEntrypoints(
const { type } = info;
switch (type) {
case 'popup':
return await getPopupEntrypoint(config, info);
return await getPopupEntrypoint(info);
case 'options':
return await getOptionsEntrypoint(config, info);
return await getOptionsEntrypoint(info);
case 'background':
hasBackground = true;
return await getBackgroundEntrypoint(config, info);
return await getBackgroundEntrypoint(info);
case 'content-script':
return await getContentScriptEntrypoint(config, info);
return await getContentScriptEntrypoint(info);
case 'unlisted-page':
return await getUnlistedPageEntrypoint(config, info);
return await getUnlistedPageEntrypoint(info);
case 'unlisted-script':
return await getUnlistedScriptEntrypoint(config, info);
return await getUnlistedScriptEntrypoint(info);
case 'content-script-style':
return {
...info,
type,
outputDir: resolve(config.outDir, CONTENT_SCRIPT_OUT_DIR),
outputDir: resolve(wxt.config.outDir, CONTENT_SCRIPT_OUT_DIR),
options: {
include: undefined,
exclude: undefined,
@@ -98,7 +96,7 @@ export async function findEntrypoints(
return {
...info,
type,
outputDir: config.outDir,
outputDir: wxt.config.outDir,
options: {
include: undefined,
exclude: undefined,
@@ -108,9 +106,9 @@ export async function findEntrypoints(
}),
);
if (config.command === 'serve' && !hasBackground) {
if (wxt.config.command === 'serve' && !hasBackground) {
entrypoints.push(
await getBackgroundEntrypoint(config, {
await getBackgroundEntrypoint({
inputPath: VIRTUAL_NOOP_BACKGROUND_MODULE_ID,
name: 'background',
type: 'background',
@@ -119,12 +117,12 @@ export async function findEntrypoints(
);
}
config.logger.debug('All entrypoints:', entrypoints);
wxt.logger.debug('All entrypoints:', entrypoints);
const skippedEntrypointNames = entrypointInfos
.filter((item) => item.skipped)
.map((item) => item.name);
if (skippedEntrypointNames.length) {
config.logger.warn(
wxt.logger.warn(
`Filter excluded the following entrypoints:\n${skippedEntrypointNames
.map((item) => `${pc.dim('-')} ${pc.cyan(item)}`)
.join('\n')}`,
@@ -133,16 +131,16 @@ export async function findEntrypoints(
const targetEntrypoints = entrypoints.filter((entry) => {
const { include, exclude } = entry.options;
if (include?.length && exclude?.length) {
config.logger.warn(
wxt.logger.warn(
`The ${entry.name} entrypoint lists both include and exclude, but only one can be used per entrypoint. Entrypoint ignored.`,
);
return false;
}
if (exclude?.length && !include?.length) {
return !exclude.includes(config.browser);
return !exclude.includes(wxt.config.browser);
}
if (include?.length && !exclude?.length) {
return include.includes(config.browser);
return include.includes(wxt.config.browser);
}
if (skippedEntrypointNames.includes(entry.name)) {
return false;
@@ -150,7 +148,9 @@ export async function findEntrypoints(
return true;
});
config.logger.debug(`${config.browser} entrypoints:`, targetEntrypoints);
wxt.logger.debug(`${wxt.config.browser} entrypoints:`, targetEntrypoints);
await wxt.hooks.callHook('entrypoints:resolved', wxt, targetEntrypoints);
return targetEntrypoints;
}
@@ -164,10 +164,7 @@ interface EntrypointInfo {
skipped: boolean;
}
function preventDuplicateEntrypointNames(
config: InternalConfig,
files: EntrypointInfo[],
) {
function preventDuplicateEntrypointNames(files: EntrypointInfo[]) {
const namesToPaths = files.reduce<Record<string, string[]>>(
(map, { name, inputPath }) => {
map[name] ??= [];
@@ -181,7 +178,7 @@ function preventDuplicateEntrypointNames(
if (absolutePaths.length > 1) {
lines.push(`- ${name}`);
absolutePaths.forEach((absolutePath) => {
lines.push(` - ${relative(config.root, absolutePath)}`);
lines.push(` - ${relative(wxt.config.root, absolutePath)}`);
});
}
return lines;
@@ -196,9 +193,9 @@ function preventDuplicateEntrypointNames(
}
}
function preventNoEntrypoints(config: InternalConfig, files: EntrypointInfo[]) {
function preventNoEntrypoints(files: EntrypointInfo[]) {
if (files.length === 0) {
throw Error(`No entrypoints found in ${config.entrypointsDir}`);
throw Error(`No entrypoints found in ${wxt.config.entrypointsDir}`);
}
}
@@ -226,10 +223,11 @@ 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(
config: InternalConfig,
{ inputPath, name, skipped }: EntrypointInfo,
): Promise<PopupEntrypoint> {
async function getPopupEntrypoint({
inputPath,
name,
skipped,
}: EntrypointInfo): Promise<PopupEntrypoint> {
const content = await fs.readFile(inputPath, 'utf-8');
const { document } = parseHTML(content);
@@ -245,7 +243,7 @@ async function getPopupEntrypoint(
try {
options.defaultIcon = JSON5.parse(defaultIconContent);
} catch (err) {
config.logger.fatal(
wxt.logger.fatal(
`Failed to parse default_icon meta tag content as JSON5. content=${defaultIconContent}`,
err,
);
@@ -272,7 +270,7 @@ async function getPopupEntrypoint(
name: 'popup',
options,
inputPath,
outputDir: config.outDir,
outputDir: wxt.config.outDir,
skipped,
};
}
@@ -281,10 +279,11 @@ 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(
config: InternalConfig,
{ inputPath, name, skipped }: EntrypointInfo,
): Promise<OptionsEntrypoint> {
async function getOptionsEntrypoint({
inputPath,
name,
skipped,
}: EntrypointInfo): Promise<OptionsEntrypoint> {
const content = await fs.readFile(inputPath, 'utf-8');
const { document } = parseHTML(content);
@@ -316,7 +315,7 @@ async function getOptionsEntrypoint(
name: 'options',
options,
inputPath,
outputDir: config.outDir,
outputDir: wxt.config.outDir,
skipped,
};
}
@@ -325,18 +324,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(
config: InternalConfig,
{ inputPath, name, skipped }: EntrypointInfo,
): Promise<GenericEntrypoint> {
async function getUnlistedPageEntrypoint({
inputPath,
name,
skipped,
}: EntrypointInfo): Promise<GenericEntrypoint> {
const content = await fs.readFile(inputPath, 'utf-8');
const { document } = parseHTML(content);
return {
type: 'unlisted-page',
name: getEntrypointName(config.entrypointsDir, inputPath),
name: getEntrypointName(wxt.config.entrypointsDir, inputPath),
inputPath,
outputDir: config.outDir,
outputDir: wxt.config.outDir,
options: getHtmlBaseOptions(document),
skipped,
};
@@ -346,14 +346,13 @@ 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(
config: InternalConfig,
{ inputPath, name, skipped }: EntrypointInfo,
): Promise<GenericEntrypoint> {
const defaultExport = await importEntrypointFile<UnlistedScriptDefinition>(
inputPath,
config,
);
async function getUnlistedScriptEntrypoint({
inputPath,
name,
skipped,
}: EntrypointInfo): Promise<GenericEntrypoint> {
const defaultExport =
await importEntrypointFile<UnlistedScriptDefinition>(inputPath);
if (defaultExport == null) {
throw Error(
`${name}: Default export not found, did you forget to call "export default defineUnlistedScript(...)"?`,
@@ -365,7 +364,7 @@ async function getUnlistedScriptEntrypoint(
type: 'unlisted-script',
name,
inputPath,
outputDir: config.outDir,
outputDir: wxt.config.outDir,
options,
skipped,
};
@@ -374,16 +373,15 @@ async function getUnlistedScriptEntrypoint(
/**
* @param path Absolute path to the background's TS file.
*/
async function getBackgroundEntrypoint(
config: InternalConfig,
{ inputPath, name, skipped }: EntrypointInfo,
): Promise<BackgroundEntrypoint> {
async function getBackgroundEntrypoint({
inputPath,
name,
skipped,
}: EntrypointInfo): Promise<BackgroundEntrypoint> {
let options: Omit<BackgroundDefinition, 'main'> = {};
if (inputPath !== VIRTUAL_NOOP_BACKGROUND_MODULE_ID) {
const defaultExport = await importEntrypointFile<BackgroundDefinition>(
inputPath,
config,
);
const defaultExport =
await importEntrypointFile<BackgroundDefinition>(inputPath);
if (defaultExport == null) {
throw Error(
`${name}: Default export not found, did you forget to call "export default defineBackground(...)"?`,
@@ -392,15 +390,23 @@ async function getBackgroundEntrypoint(
const { main: _, ...moduleOptions } = defaultExport;
options = moduleOptions;
}
if (wxt.config.manifestVersion !== 3) {
delete options.type;
}
return {
type: 'background',
name,
inputPath,
outputDir: config.outDir,
outputDir: wxt.config.outDir,
options: {
...options,
type: resolvePerBrowserOption(options.type, config.browser),
persistent: resolvePerBrowserOption(options.persistent, config.browser),
type: resolvePerBrowserOption(options.type, wxt.config.browser),
persistent: resolvePerBrowserOption(
options.persistent,
wxt.config.browser,
),
},
skipped,
};
@@ -409,12 +415,13 @@ async function getBackgroundEntrypoint(
/**
* @param path Absolute path to the content script's TS file.
*/
async function getContentScriptEntrypoint(
config: InternalConfig,
{ inputPath, name, skipped }: EntrypointInfo,
): Promise<ContentScriptEntrypoint> {
async function getContentScriptEntrypoint({
inputPath,
name,
skipped,
}: EntrypointInfo): Promise<ContentScriptEntrypoint> {
const { main: _, ...options } =
await importEntrypointFile<ContentScriptDefinition>(inputPath, config);
await importEntrypointFile<ContentScriptDefinition>(inputPath);
if (options == null) {
throw Error(
`${name}: Default export not found, did you forget to call "export default defineContentScript(...)"?`,
@@ -424,7 +431,7 @@ async function getContentScriptEntrypoint(
type: 'content-script',
name,
inputPath,
outputDir: resolve(config.outDir, CONTENT_SCRIPT_OUT_DIR),
outputDir: resolve(wxt.config.outDir, CONTENT_SCRIPT_OUT_DIR),
options,
skipped,
};
+29 -41
View File
@@ -1,5 +1,5 @@
import { UnimportOptions, createUnimport } from 'unimport';
import { Entrypoint, InternalConfig } from '~/types';
import { Entrypoint } from '~/types';
import fs from 'fs-extra';
import { relative, resolve } from 'path';
import { getEntrypointBundlePath } from '~/core/utils/entrypoints';
@@ -9,40 +9,39 @@ import { normalizePath } from '~/core/utils/paths';
import path from 'node:path';
import { Message, parseI18nMessages } from '~/core/utils/i18n';
import { writeFileIfDifferent, getPublicFiles } from '~/core/utils/fs';
import { wxt } from '../../wxt';
/**
* Generate and write all the files inside the `InternalConfig.typesDir` directory.
*/
export async function generateTypesDir(
entrypoints: Entrypoint[],
config: InternalConfig,
): Promise<void> {
await fs.ensureDir(config.typesDir);
await fs.ensureDir(wxt.config.typesDir);
const references: string[] = [];
const imports = getUnimportOptions(config);
const imports = getUnimportOptions(wxt.config);
if (imports !== false) {
references.push(await writeImportsDeclarationFile(config, imports));
references.push(await writeImportsDeclarationFile(imports));
}
references.push(await writePathsDeclarationFile(entrypoints, config));
references.push(await writeI18nDeclarationFile(config));
references.push(await writeGlobalsDeclarationFile(config));
references.push(await writePathsDeclarationFile(entrypoints));
references.push(await writeI18nDeclarationFile());
references.push(await writeGlobalsDeclarationFile());
const mainReference = await writeMainDeclarationFile(references, config);
await writeTsConfigFile(mainReference, config);
const mainReference = await writeMainDeclarationFile(references);
await writeTsConfigFile(mainReference);
}
async function writeImportsDeclarationFile(
config: InternalConfig,
unimportOptions: Partial<UnimportOptions>,
): Promise<string> {
const filePath = resolve(config.typesDir, 'imports.d.ts');
const filePath = resolve(wxt.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: config.srcDir });
await unimport.scanImportsFromDir(undefined, { cwd: wxt.config.srcDir });
await writeFileIfDifferent(
filePath,
@@ -56,18 +55,17 @@ async function writeImportsDeclarationFile(
async function writePathsDeclarationFile(
entrypoints: Entrypoint[],
config: InternalConfig,
): Promise<string> {
const filePath = resolve(config.typesDir, 'paths.d.ts');
const filePath = resolve(wxt.config.typesDir, 'paths.d.ts');
const unions = entrypoints
.map((entry) =>
getEntrypointBundlePath(
entry,
config.outDir,
wxt.config.outDir,
entry.inputPath.endsWith('.html') ? '.html' : '.js',
),
)
.concat(await getPublicFiles(config))
.concat(await getPublicFiles())
.map(normalizePath)
.map((path) => ` | "/${path}"`)
.sort()
@@ -95,11 +93,9 @@ declare module "wxt/browser" {
return filePath;
}
async function writeI18nDeclarationFile(
config: InternalConfig,
): Promise<string> {
const filePath = resolve(config.typesDir, 'i18n.d.ts');
const defaultLocale = config.manifest.default_locale;
async function writeI18nDeclarationFile(): Promise<string> {
const filePath = resolve(wxt.config.typesDir, 'i18n.d.ts');
const defaultLocale = wxt.config.manifest.default_locale;
const template = `// Generated by wxt
import "wxt/browser";
@@ -123,7 +119,7 @@ declare module "wxt/browser" {
let messages: Message[];
if (defaultLocale) {
const defaultLocalePath = path.resolve(
config.publicDir,
wxt.config.publicDir,
'_locales',
defaultLocale,
'messages.json',
@@ -154,11 +150,9 @@ declare module "wxt/browser" {
return filePath;
}
async function writeGlobalsDeclarationFile(
config: InternalConfig,
): Promise<string> {
const filePath = resolve(config.typesDir, 'globals.d.ts');
const globals = [...getGlobals(config), ...getEntrypointGlobals('')];
async function writeGlobalsDeclarationFile(): Promise<string> {
const filePath = resolve(wxt.config.typesDir, 'globals.d.ts');
const globals = [...getGlobals(wxt.config), ...getEntrypointGlobals('')];
await writeFileIfDifferent(
filePath,
[
@@ -175,11 +169,8 @@ async function writeGlobalsDeclarationFile(
return filePath;
}
async function writeMainDeclarationFile(
references: string[],
config: InternalConfig,
): Promise<string> {
const dir = config.wxtDir;
async function writeMainDeclarationFile(references: string[]): Promise<string> {
const dir = wxt.config.wxtDir;
const filePath = resolve(dir, 'wxt.d.ts');
await writeFileIfDifferent(
filePath,
@@ -195,13 +186,10 @@ async function writeMainDeclarationFile(
return filePath;
}
async function writeTsConfigFile(
mainReference: string,
config: InternalConfig,
) {
const dir = config.wxtDir;
async function writeTsConfigFile(mainReference: string) {
const dir = wxt.config.wxtDir;
const getTsconfigPath = (path: string) => normalizePath(relative(dir, path));
const paths = Object.entries(config.alias)
const paths = Object.entries(wxt.config.alias)
.flatMap(([alias, absolutePath]) => {
const aliasPath = getTsconfigPath(absolutePath);
return [
@@ -229,10 +217,10 @@ ${paths}
}
},
"include": [
"${getTsconfigPath(config.root)}/**/*",
"${getTsconfigPath(wxt.config.root)}/**/*",
"./${getTsconfigPath(mainReference)}"
],
"exclude": ["${getTsconfigPath(config.outBaseDir)}"]
"exclude": ["${getTsconfigPath(wxt.config.outBaseDir)}"]
}`,
);
}
+20 -17
View File
@@ -11,8 +11,11 @@ export function groupEntrypoints(entrypoints: Entrypoint[]): EntrypointGroup[] {
const groups: EntrypointGroup[] = [];
for (const entry of entrypoints) {
const group = ENTRY_TYPE_TO_GROUP_MAP[entry.type];
if (group === 'no-group') {
let group = ENTRY_TYPE_TO_GROUP_MAP[entry.type];
if (entry.type === 'background' && entry.options.type === 'module') {
group = 'esm';
}
if (group === 'individual') {
groups.push(entry);
} else {
let groupIndex = groupIndexMap[group];
@@ -28,22 +31,22 @@ export function groupEntrypoints(entrypoints: Entrypoint[]): EntrypointGroup[] {
}
const ENTRY_TYPE_TO_GROUP_MAP: Record<Entrypoint['type'], Group> = {
sandbox: 'sandbox-page',
sandbox: 'sandboxed-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',
popup: 'esm',
newtab: 'esm',
history: 'esm',
options: 'esm',
devtools: 'esm',
bookmarks: 'esm',
sidepanel: 'esm',
'unlisted-page': 'esm',
background: 'no-group',
'content-script': 'no-group',
'unlisted-script': 'no-group',
'unlisted-style': 'no-group',
'content-script-style': 'no-group',
background: 'individual',
'content-script': 'individual',
'unlisted-script': 'individual',
'unlisted-style': 'individual',
'content-script-style': 'individual',
};
type Group = 'extension-page' | 'sandbox-page' | 'no-group';
type Group = 'esm' | 'sandboxed-esm' | 'individual';
+9 -12
View File
@@ -1,5 +1,4 @@
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';
@@ -8,6 +7,7 @@ import { removeProjectImportStatements } from '~/core/utils/strings';
import { normalizePath } from '~/core/utils/paths';
import { TransformOptions, transformSync } from 'esbuild';
import { fileURLToPath } from 'node:url';
import { wxt } from '../../wxt';
/**
* Get the value from the default export of a `path`.
@@ -24,16 +24,13 @@ import { fileURLToPath } from 'node:url';
* Downside is that code cannot be executed outside of the main fucntion for the entrypoint,
* otherwise you will see "xxx is not defined" errors for any imports used outside of main function.
*/
export async function importEntrypointFile<T>(
path: string,
config: InternalConfig,
): Promise<T> {
config.logger.debug('Loading file metadata:', path);
export async function importEntrypointFile<T>(path: string): Promise<T> {
wxt.logger.debug('Loading file metadata:', path);
// JITI & Babel uses normalized paths.
const normalPath = normalizePath(path);
const unimport = createUnimport({
...getUnimportOptions(config),
...getUnimportOptions(wxt.config),
// Only allow specific imports, not all from the project
dirs: [],
});
@@ -42,7 +39,7 @@ export async function importEntrypointFile<T>(
const text = await fs.readFile(path, 'utf-8');
const textNoImports = removeProjectImportStatements(text);
const { code } = await unimport.injectImports(textNoImports);
config.logger.debug(
wxt.logger.debug(
['Text:', text, 'No imports:', textNoImports, 'Code:', code].join('\n'),
);
@@ -52,11 +49,11 @@ export async function importEntrypointFile<T>(
: fileURLToPath(import.meta.url),
{
cache: false,
debug: config.debug,
debug: wxt.config.debug,
esmResolve: true,
alias: {
'webextension-polyfill': resolve(
config.root,
wxt.config.root,
'node_modules/wxt/dist/virtual/mock-browser.js',
),
},
@@ -89,7 +86,7 @@ export async function importEntrypointFile<T>(
const res = await jiti(path);
return res.default;
} catch (err) {
const filePath = relative(config.root, path);
const filePath = relative(wxt.config.root, path);
if (err instanceof ReferenceError) {
// "XXX is not defined" - usually due to WXT removing imports
const variableName = err.message.replace(' is not defined', '');
@@ -98,7 +95,7 @@ export async function importEntrypointFile<T>(
{ cause: err },
);
} else {
config.logger.error(err);
wxt.logger.error(err);
throw Error(`Failed to load entrypoint: ${filePath}`, { cause: err });
}
}
+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 './get-internal-config';
export * from './resolve-config';
export * from './group-entrypoints';
export * from './import-entrypoint';
export * from './internal-build';
+43 -37
View File
@@ -1,5 +1,5 @@
import { findEntrypoints } from './find-entrypoints';
import { InternalConfig, BuildOutput, Entrypoint } from '~/types';
import { BuildOutput, Entrypoint } from '~/types';
import pc from 'picocolors';
import fs from 'fs-extra';
import { groupEntrypoints } from './group-entrypoints';
@@ -17,6 +17,7 @@ import {
} 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
@@ -28,28 +29,28 @@ import { exec } from '../exec';
* `rebuild` for more details)
* 3. Prints the summary
*/
export async function internalBuild(
config: InternalConfig,
): Promise<BuildOutput> {
const verb = config.command === 'serve' ? 'Pre-rendering' : 'Building';
const target = `${config.browser}-mv${config.manifestVersion}`;
config.logger.info(
`${verb} ${pc.cyan(target)} for ${pc.cyan(config.mode)} with ${pc.green(
`${config.builder.name} ${config.builder.version}`,
export async function internalBuild(): Promise<BuildOutput> {
await wxt.hooks.callHook('build:before', wxt);
const verb = wxt.config.command === 'serve' ? 'Pre-rendering' : 'Building';
const target = `${wxt.config.browser}-mv${wxt.config.manifestVersion}`;
wxt.logger.info(
`${verb} ${pc.cyan(target)} for ${pc.cyan(wxt.config.mode)} with ${pc.green(
`${wxt.config.builder.name} ${wxt.config.builder.version}`,
)}`,
);
const startTime = Date.now();
// Cleanup
await fs.rm(config.outDir, { recursive: true, force: true });
await fs.ensureDir(config.outDir);
await fs.rm(wxt.config.outDir, { recursive: true, force: true });
await fs.ensureDir(wxt.config.outDir);
const entrypoints = await findEntrypoints(config);
config.logger.debug('Detected entrypoints:', entrypoints);
const entrypoints = await findEntrypoints();
wxt.logger.debug('Detected entrypoints:', entrypoints);
const validationResults = validateEntrypoints(entrypoints);
if (validationResults.errorCount + validationResults.warningCount > 0) {
printValidationResults(config, validationResults);
printValidationResults(validationResults);
}
if (validationResults.errorCount > 0) {
throw new ValidationError(`Entrypoint validation failed`, {
@@ -58,55 +59,60 @@ export async function internalBuild(
}
const groups = groupEntrypoints(entrypoints);
const { output, warnings } = await rebuild(
config,
entrypoints,
groups,
undefined,
);
await wxt.hooks.callHook('entrypoints:grouped', wxt, groups);
const { output, warnings } = await rebuild(entrypoints, groups, undefined);
await wxt.hooks.callHook('build:done', wxt, output);
// Post-build
await printBuildSummary(
config.logger.success,
wxt.logger.success,
`Built extension in ${formatDuration(Date.now() - startTime)}`,
output,
config,
);
for (const warning of warnings) {
config.logger.warn(...warning);
wxt.logger.warn(...warning);
}
if (config.analysis.enabled) {
await combineAnalysisStats(config);
config.logger.info(
`Analysis complete:\n ${pc.gray('└─')} ${pc.yellow('stats.html')}`,
if (wxt.config.analysis.enabled) {
await combineAnalysisStats();
const statsPath = relative(wxt.config.root, wxt.config.analysis.outputFile);
wxt.logger.info(
`Analysis complete:\n ${pc.gray('└─')} ${pc.yellow(statsPath)}`,
);
}
return output;
}
async function combineAnalysisStats(config: InternalConfig): Promise<void> {
async function combineAnalysisStats(): Promise<void> {
const unixFiles = await glob(`stats-*.json`, {
cwd: config.outDir,
cwd: wxt.config.outDir,
absolute: true,
});
const absolutePaths = unixFiles.map(unnormalizePath);
await exec(
config,
'rollup-plugin-visualizer',
[...absolutePaths, '--template', config.analysis.template],
{ cwd: config.root, stdio: 'inherit' },
[
...absolutePaths,
'--template',
wxt.config.analysis.template,
'--filename',
wxt.config.analysis.outputFile,
],
{ cwd: wxt.config.root, stdio: 'inherit' },
);
await Promise.all(absolutePaths.map((statsFile) => fs.remove(statsFile)));
}
function printValidationResults(
config: InternalConfig,
{ errorCount, errors, warningCount }: ValidationResults,
) {
(errorCount > 0 ? config.logger.error : config.logger.warn)(
function printValidationResults({
errorCount,
errors,
warningCount,
}: ValidationResults) {
(errorCount > 0 ? wxt.logger.error : wxt.logger.warn)(
`Entrypoint validation failed: ${errorCount} error${
errorCount === 1 ? '' : 's'
}, ${warningCount} warning${warningCount === 1 ? '' : 's'}`,
+8 -13
View File
@@ -1,13 +1,9 @@
import type { Manifest } from '~/browser';
import {
BuildOutput,
Entrypoint,
EntrypointGroup,
InternalConfig,
} from '~/types';
import { BuildOutput, Entrypoint, EntrypointGroup } from '~/types';
import { generateTypesDir } from './generate-wxt-dir';
import { buildEntrypoints } from './build-entrypoints';
import { generateManifest, writeManifest } from '~/core/utils/manifest';
import { wxt } from '../../wxt';
/**
* Given a configuration, list of entrypoints, and an existing, partial output, build the
@@ -26,7 +22,6 @@ import { generateManifest, writeManifest } from '~/core/utils/manifest';
* this is the first build.
*/
export async function rebuild(
config: InternalConfig,
allEntrypoints: Entrypoint[],
entrypointGroups: EntrypointGroup[],
existingOutput: Omit<BuildOutput, 'manifest'> = {
@@ -42,28 +37,28 @@ export async function rebuild(
const spinner = ora(`Preparing...`).start();
// Update types directory with new files and types
await generateTypesDir(allEntrypoints, config).catch((err) => {
config.logger.warn('Failed to update .wxt directory:', err);
await generateTypesDir(allEntrypoints).catch((err) => {
wxt.logger.warn('Failed to update .wxt directory:', err);
// Throw the error if doing a regular build, don't for dev mode.
if (config.command === 'build') throw err;
if (wxt.config.command === 'build') throw err;
});
// Build and merge the outputs
const newOutput = await buildEntrypoints(entrypointGroups, config, spinner);
const newOutput = await buildEntrypoints(entrypointGroups, spinner);
const mergedOutput: Omit<BuildOutput, 'manifest'> = {
steps: [...existingOutput.steps, ...newOutput.steps],
publicAssets: [...existingOutput.publicAssets, ...newOutput.publicAssets],
};
const { manifest: newManifest, warnings: manifestWarnings } =
await generateManifest(allEntrypoints, mergedOutput, config);
await generateManifest(allEntrypoints, mergedOutput);
const finalOutput: BuildOutput = {
manifest: newManifest,
...newOutput,
};
// Write manifest
await writeManifest(newManifest, finalOutput, config);
await writeManifest(newManifest, finalOutput);
// Stop the spinner and remove it from the CLI output
spinner.clear().stop();
@@ -1,7 +1,7 @@
import { loadConfig } from 'c12';
import {
InlineConfig,
InternalConfig,
ResolvedConfig,
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 getInternalConfig(
export async function resolveConfig(
inlineConfig: InlineConfig,
command: 'build' | 'serve',
server?: WxtDevServer,
): Promise<InternalConfig> {
): Promise<ResolvedConfig> {
// Load user config
let userConfig: UserConfig = {};
let userConfigMetadata: InternalConfig['userConfigMetadata'] | undefined;
let userConfigMetadata: ResolvedConfig['userConfigMetadata'] | undefined;
if (inlineConfig.configFile !== false) {
const { config: loadedConfig, ...metadata } = await loadConfig<UserConfig>({
name: 'wxt',
@@ -100,7 +100,7 @@ export async function getInternalConfig(
}).map(([key, value]) => [key, path.resolve(root, value)]),
);
const finalConfig: Omit<InternalConfig, 'builder'> = {
const finalConfig: Omit<ResolvedConfig, 'builder'> = {
browser,
command,
debug,
@@ -129,6 +129,10 @@ export async function getInternalConfig(
analysis: {
enabled: mergedConfig.analysis?.enabled ?? false,
template: mergedConfig.analysis?.template ?? 'treemap',
outputFile: path.resolve(
root,
mergedConfig.analysis?.outputFile ?? 'stats.html',
),
},
userConfigMetadata: userConfigMetadata ?? {},
alias,
@@ -140,6 +144,7 @@ export async function getInternalConfig(
dev: {
reloadCommand,
},
hooks: mergedConfig.hooks ?? {},
};
const builder = await createViteBuilder(
@@ -191,6 +196,10 @@ function mergeInlineConfig(
inlineConfig.zip ?? {},
userConfig.zip ?? {},
);
const hooks: InlineConfig['hooks'] = defu(
inlineConfig.hooks ?? {},
userConfig.hooks ?? {},
);
return {
root: inlineConfig.root ?? userConfig.root,
@@ -211,9 +220,8 @@ function mergeInlineConfig(
outDir: inlineConfig.outDir ?? userConfig.outDir,
zip,
analysis: {
enabled: inlineConfig.analysis?.enabled ?? userConfig.analysis?.enabled,
template:
inlineConfig.analysis?.template ?? userConfig.analysis?.template,
...userConfig.analysis,
...inlineConfig.analysis,
},
alias: {
...userConfig.alias,
@@ -229,13 +237,14 @@ function mergeInlineConfig(
...userConfig.dev,
...inlineConfig.dev,
},
hooks,
};
}
function resolveInternalZipConfig(
root: string,
mergedConfig: InlineConfig,
): NullablyRequired<InternalConfig['zip']> {
): NullablyRequired<ResolvedConfig['zip']> {
return {
name: undefined,
sourcesTemplate: '{{name}}-{{version}}-sources.zip',
+11 -12
View File
@@ -1,6 +1,7 @@
import type { Manifest } from '~/browser';
import { ContentScriptEntrypoint, InternalConfig } from '~/types';
import { ContentScriptEntrypoint } from '~/types';
import { resolvePerBrowserOption } from './entrypoints';
import { wxt } from '../wxt';
/**
* Returns a unique and consistent string hash based on a content scripts options.
@@ -9,9 +10,8 @@ import { resolvePerBrowserOption } from './entrypoints';
*/
export function hashContentScriptOptions(
options: ContentScriptEntrypoint['options'],
config: InternalConfig,
): string {
const simplifiedOptions = mapWxtOptionsToContentScript(options, config);
const simplifiedOptions = mapWxtOptionsToContentScript(options);
// Remove undefined fields and use defaults to generate hash
Object.keys(simplifiedOptions).forEach((key) => {
@@ -45,33 +45,32 @@ export function hashContentScriptOptions(
export function mapWxtOptionsToContentScript(
options: ContentScriptEntrypoint['options'],
config: InternalConfig,
): Omit<Manifest.ContentScript, 'js' | 'css'> {
return {
matches: resolvePerBrowserOption(options.matches, config.browser),
all_frames: resolvePerBrowserOption(options.allFrames, config.browser),
matches: resolvePerBrowserOption(options.matches, wxt.config.browser),
all_frames: resolvePerBrowserOption(options.allFrames, wxt.config.browser),
match_about_blank: resolvePerBrowserOption(
options.matchAboutBlank,
config.browser,
wxt.config.browser,
),
exclude_globs: resolvePerBrowserOption(
options.excludeGlobs,
config.browser,
wxt.config.browser,
),
exclude_matches: resolvePerBrowserOption(
options.excludeMatches,
config.browser,
wxt.config.browser,
),
include_globs: resolvePerBrowserOption(
options.includeGlobs,
config.browser,
wxt.config.browser,
),
run_at: resolvePerBrowserOption(options.runAt, config.browser),
run_at: resolvePerBrowserOption(options.runAt, wxt.config.browser),
// @ts-expect-error: untyped chrome options
match_origin_as_fallback: resolvePerBrowserOption(
options.matchOriginAsFallback,
config.browser,
wxt.config.browser,
),
world: options.world,
};
+4 -3
View File
@@ -1,7 +1,7 @@
import type { Options } from 'execa';
import managePath from 'manage-path';
import { resolve } from 'node:path';
import { InternalConfig } from '~/types';
import { wxt } from '../wxt';
const managedPath = managePath(process.env);
@@ -9,7 +9,6 @@ const managedPath = managePath(process.env);
* Wrapper around `execa` with a modified `PATH` variable containing CLI tools from WXT's dependencies.
*/
export const exec = async (
config: InternalConfig,
file: string,
args?: readonly string[],
options?: Options,
@@ -18,7 +17,9 @@ export const exec = async (
managedPath.restore();
// Add subdependency path for PNPM shamefully-hoist=false
managedPath.push(resolve(config.root, 'node_modules/wxt/node_modules/.bin'));
managedPath.push(
resolve(wxt.config.root, 'node_modules/wxt/node_modules/.bin'),
);
const { execa } = await import('execa');
return await execa(file, args, options);
+4 -6
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,11 +27,9 @@ export async function writeFileIfDifferent(
* Get all the files in the project's public directory. Returned paths are relative to the
* `config.publicDir`.
*/
export async function getPublicFiles(
config: InternalConfig,
): Promise<string[]> {
if (!(await fs.exists(config.publicDir))) return [];
export async function getPublicFiles(): Promise<string[]> {
if (!(await fs.exists(wxt.config.publicDir))) return [];
const files = await glob('**/*', { cwd: config.publicDir });
const files = await glob('**/*', { cwd: wxt.config.publicDir });
return files.map(unnormalizePath);
}
+2 -2
View File
@@ -1,7 +1,7 @@
import { InternalConfig } from '~/types';
import { ResolvedConfig } from '~/types';
export function getGlobals(
config: Omit<InternalConfig, 'builder'>,
config: Omit<ResolvedConfig, 'builder'>,
): Array<{ name: string; value: any; type: string }> {
return [
{
+6 -4
View File
@@ -1,12 +1,12 @@
import { resolve } from 'path';
import { BuildOutput, InternalConfig } from '~/types';
import { BuildOutput } from '~/types';
import { printFileList } from './printFileList';
import { wxt } from '../../wxt';
export async function printBuildSummary(
log: (...args: any[]) => void,
header: string,
output: BuildOutput,
config: InternalConfig,
) {
const chunks = [
...output.steps.flatMap((step) => step.chunks),
@@ -19,8 +19,10 @@ export async function printBuildSummary(
return l.fileName.localeCompare(r.fileName);
});
const files = chunks.map((chunk) => resolve(config.outDir, chunk.fileName));
await printFileList(log, header, config.outDir, files);
const files = chunks.map((chunk) =>
resolve(wxt.config.outDir, chunk.fileName),
);
await printFileList(log, header, wxt.config.outDir, files);
}
const DEFAULT_SORT_WEIGHT = 100;
+122 -78
View File
@@ -4,7 +4,6 @@ import {
BackgroundEntrypoint,
BuildOutput,
ContentScriptEntrypoint,
InternalConfig,
OptionsEntrypoint,
PopupEntrypoint,
} from '~/types';
@@ -22,8 +21,8 @@ import {
import { getPackageJson } from './package';
import { normalizePath } from './paths';
import { writeFileIfDifferent } from './fs';
import { produce } from 'immer';
import defu from 'defu';
import { wxt } from '../wxt';
/**
* Writes the manifest to the output directory and the build output.
@@ -31,15 +30,14 @@ import defu from 'defu';
export async function writeManifest(
manifest: Manifest.WebExtensionManifest,
output: BuildOutput,
config: InternalConfig,
): Promise<void> {
const str =
config.mode === 'production'
wxt.config.mode === 'production'
? JSON.stringify(manifest)
: JSON.stringify(manifest, null, 2);
await fs.ensureDir(config.outDir);
await writeFileIfDifferent(resolve(config.outDir, 'manifest.json'), str);
await fs.ensureDir(wxt.config.outDir);
await writeFileIfDifferent(resolve(wxt.config.outDir, 'manifest.json'), str);
output.publicAssets.unshift({
type: 'asset',
@@ -53,38 +51,39 @@ export async function writeManifest(
export async function generateManifest(
entrypoints: Entrypoint[],
buildOutput: Omit<BuildOutput, 'manifest'>,
config: InternalConfig,
): Promise<{ manifest: Manifest.WebExtensionManifest; warnings: any[][] }> {
const warnings: any[][] = [];
const pkg = await getPackageJson(config);
const pkg = await getPackageJson();
let versionName =
config.manifest.version_name ?? config.manifest.version ?? pkg?.version;
wxt.config.manifest.version_name ??
wxt.config.manifest.version ??
pkg?.version;
if (versionName == null) {
versionName = '0.0.0';
config.logger.warn(
wxt.logger.warn(
'Extension version not found, defaulting to "0.0.0". Add a version to your `package.json` or `wxt.config.ts` file. For more details, see: https://wxt.dev/guide/manifest.html#version-and-version-name',
);
}
const version = config.manifest.version ?? simplifyVersion(versionName);
const version = wxt.config.manifest.version ?? simplifyVersion(versionName);
const baseManifest: Manifest.WebExtensionManifest = {
manifest_version: config.manifestVersion,
manifest_version: wxt.config.manifestVersion,
name: pkg?.name,
description: pkg?.description,
version,
short_name: pkg?.shortName,
icons: discoverIcons(buildOutput),
};
const userManifest = config.manifest;
const userManifest = wxt.config.manifest;
const manifest = defu(
let manifest = defu(
userManifest,
baseManifest,
) as Manifest.WebExtensionManifest;
// Add reload command in dev mode
if (config.command === 'serve' && config.dev.reloadCommand) {
if (wxt.config.command === 'serve' && wxt.config.dev.reloadCommand) {
if (manifest.commands && Object.keys(manifest.commands).length >= 4) {
warnings.push([
"Extension already has 4 registered commands, WXT's reload command is disabled",
@@ -94,7 +93,7 @@ export async function generateManifest(
manifest.commands['wxt:reload-extension'] = {
description: 'Reload the extension during development',
suggested_key: {
default: config.dev.reloadCommand,
default: wxt.config.dev.reloadCommand,
},
};
}
@@ -104,29 +103,38 @@ export async function generateManifest(
manifest.version = version;
manifest.version_name =
// Firefox doesn't support version_name
config.browser === 'firefox' || versionName === version
wxt.config.browser === 'firefox' || versionName === version
? undefined
: versionName;
addEntrypoints(manifest, entrypoints, buildOutput, config);
addEntrypoints(manifest, entrypoints, buildOutput);
if (config.command === 'serve') addDevModeCsp(manifest, config);
if (config.command === 'serve') addDevModePermissions(manifest, config);
if (wxt.config.command === 'serve') addDevModeCsp(manifest);
if (wxt.config.command === 'serve') addDevModePermissions(manifest);
const finalManifest = produce(manifest, config.transformManifest);
// TODO: Remove in v1
wxt.config.transformManifest(manifest);
await wxt.hooks.callHook('build:manifestGenerated', wxt, manifest);
if (finalManifest.name == null)
if (wxt.config.manifestVersion === 2)
convertWebAccessibleResourcesToMv2(manifest);
if (wxt.config.manifestVersion === 3) {
validateMv3WebAccessbileResources(manifest);
}
if (manifest.name == null)
throw Error(
"Manifest 'name' is missing. Either:\n1. Set the name in your <rootDir>/package.json\n2. Set a name via the manifest option in your wxt.config.ts",
);
if (finalManifest.version == null) {
if (manifest.version == null) {
throw Error(
"Manifest 'version' is missing. Either:\n1. Add a version in your <rootDir>/package.json\n2. Pass the version via the manifest option in your wxt.config.ts",
);
}
return {
manifest: finalManifest,
manifest,
warnings,
};
}
@@ -153,7 +161,6 @@ function addEntrypoints(
manifest: Manifest.WebExtensionManifest,
entrypoints: Entrypoint[],
buildOutput: Omit<BuildOutput, 'manifest'>,
config: InternalConfig,
): void {
const entriesByType = entrypoints.reduce<
Partial<Record<Entrypoint['type'], Entrypoint[]>>
@@ -181,13 +188,17 @@ function addEntrypoints(
const sidepanels = entriesByType['sidepanel'];
if (background) {
const script = getEntrypointBundlePath(background, config.outDir, '.js');
if (config.browser === 'firefox' && config.manifestVersion === 3) {
const script = getEntrypointBundlePath(
background,
wxt.config.outDir,
'.js',
);
if (wxt.config.browser === 'firefox' && wxt.config.manifestVersion === 3) {
manifest.background = {
type: background.options.type,
scripts: [script],
};
} else if (config.manifestVersion === 3) {
} else if (wxt.config.manifestVersion === 3) {
manifest.background = {
type: background.options.type,
service_worker: script,
@@ -201,8 +212,8 @@ function addEntrypoints(
}
if (bookmarks) {
if (config.browser === 'firefox') {
config.logger.warn(
if (wxt.config.browser === 'firefox') {
wxt.logger.warn(
'Bookmarks are not supported by Firefox. chrome_url_overrides.bookmarks was not added to the manifest',
);
} else {
@@ -210,15 +221,15 @@ function addEntrypoints(
// @ts-expect-error: bookmarks is untyped in webextension-polyfill, but supported by chrome
manifest.chrome_url_overrides.bookmarks = getEntrypointBundlePath(
bookmarks,
config.outDir,
wxt.config.outDir,
'.html',
);
}
}
if (history) {
if (config.browser === 'firefox') {
config.logger.warn(
if (wxt.config.browser === 'firefox') {
wxt.logger.warn(
'Bookmarks are not supported by Firefox. chrome_url_overrides.history was not added to the manifest',
);
} else {
@@ -226,7 +237,7 @@ function addEntrypoints(
// @ts-expect-error: history is untyped in webextension-polyfill, but supported by chrome
manifest.chrome_url_overrides.history = getEntrypointBundlePath(
history,
config.outDir,
wxt.config.outDir,
'.html',
);
}
@@ -236,7 +247,7 @@ function addEntrypoints(
manifest.chrome_url_overrides ??= {};
manifest.chrome_url_overrides.newtab = getEntrypointBundlePath(
newtab,
config.outDir,
wxt.config.outDir,
'.html',
);
}
@@ -244,7 +255,7 @@ function addEntrypoints(
if (popup) {
const default_popup = getEntrypointBundlePath(
popup,
config.outDir,
wxt.config.outDir,
'.html',
);
const options: Manifest.ActionManifest = {};
@@ -273,33 +284,37 @@ function addEntrypoints(
if (devtools) {
manifest.devtools_page = getEntrypointBundlePath(
devtools,
config.outDir,
wxt.config.outDir,
'.html',
);
}
if (options) {
const page = getEntrypointBundlePath(options, config.outDir, '.html');
const page = getEntrypointBundlePath(options, wxt.config.outDir, '.html');
manifest.options_ui = {
open_in_tab: options.options.openInTab,
browser_style:
config.browser === 'firefox' ? options.options.browserStyle : undefined,
wxt.config.browser === 'firefox'
? options.options.browserStyle
: undefined,
chrome_style:
config.browser !== 'firefox' ? options.options.chromeStyle : undefined,
wxt.config.browser !== 'firefox'
? options.options.chromeStyle
: undefined,
page,
};
}
if (sandboxes?.length) {
if (config.browser === 'firefox') {
config.logger.warn(
if (wxt.config.browser === 'firefox') {
wxt.logger.warn(
'Sandboxed pages not supported by Firefox. sandbox.pages was not added to the manifest',
);
} else {
// @ts-expect-error: sandbox not typed
manifest.sandbox = {
pages: sandboxes.map((entry) =>
getEntrypointBundlePath(entry, config.outDir, '.html'),
getEntrypointBundlePath(entry, wxt.config.outDir, '.html'),
),
};
}
@@ -310,23 +325,23 @@ function addEntrypoints(
sidepanels.find((entry) => entry.name === 'sidepanel') ?? sidepanels[0];
const page = getEntrypointBundlePath(
defaultSidepanel,
config.outDir,
wxt.config.outDir,
'.html',
);
if (config.browser === 'firefox') {
if (wxt.config.browser === 'firefox') {
manifest.sidebar_action = {
// TODO: Add options to side panel
// ...defaultSidepanel.options,
default_panel: page,
};
} else if (config.manifestVersion === 3) {
} else if (wxt.config.manifestVersion === 3) {
// @ts-expect-error: Untyped
manifest.side_panel = {
default_path: page,
};
} else {
config.logger.warn(
wxt.logger.warn(
'Side panel not supported by Chromium using MV2. side_panel.default_path was not added to the manifest',
);
}
@@ -337,12 +352,12 @@ function addEntrypoints(
// Don't add content scripts to the manifest in dev mode for MV3 - they're managed and reloaded
// at runtime
if (config.command === 'serve' && config.manifestVersion === 3) {
if (wxt.config.command === 'serve' && wxt.config.manifestVersion === 3) {
const hostPermissions = new Set<string>(manifest.host_permissions ?? []);
contentScripts.forEach((script) => {
const matches = resolvePerBrowserOption(
script.options.matches,
config.browser,
wxt.config.browser,
);
matches.forEach((matchPattern) => {
hostPermissions.add(matchPattern);
@@ -353,7 +368,7 @@ function addEntrypoints(
);
} else {
const hashToEntrypointsMap = contentScripts.reduce((map, script) => {
const hash = hashContentScriptOptions(script.options, config);
const hash = hashContentScriptOptions(script.options);
if (map.has(hash)) map.get(hash)?.push(script);
else map.set(hash, [script]);
return map;
@@ -361,10 +376,10 @@ function addEntrypoints(
const newContentScripts = Array.from(hashToEntrypointsMap.entries()).map(
([, scripts]) => ({
...mapWxtOptionsToContentScript(scripts[0].options, config),
...mapWxtOptionsToContentScript(scripts[0].options),
css: getContentScriptCssFiles(scripts, cssMap),
js: scripts.map((entry) =>
getEntrypointBundlePath(entry, config.outDir, '.js'),
getEntrypointBundlePath(entry, wxt.config.outDir, '.js'),
),
}),
);
@@ -375,7 +390,6 @@ function addEntrypoints(
}
const contentScriptCssResources = getContentScriptCssWebAccessibleResources(
config,
contentScripts,
cssMap,
);
@@ -420,12 +434,9 @@ function discoverIcons(
return icons.length > 0 ? Object.fromEntries(icons) : undefined;
}
function addDevModeCsp(
manifest: Manifest.WebExtensionManifest,
config: InternalConfig,
): void {
const permission = `http://${config.server?.hostname ?? ''}/*`;
const allowedCsp = config.server?.origin ?? 'http://localhost:*';
function addDevModeCsp(manifest: Manifest.WebExtensionManifest): void {
const permission = `http://${wxt.config.server?.hostname ?? ''}/*`;
const allowedCsp = wxt.config.server?.origin ?? 'http://localhost:*';
if (manifest.manifest_version === 3) {
addHostPermission(manifest, permission);
@@ -442,7 +453,7 @@ function addDevModeCsp(
"script-src 'self'; object-src 'self';", // default CSP for MV2
);
if (config.server) csp.add('script-src', allowedCsp);
if (wxt.config.server) csp.add('script-src', allowedCsp);
if (manifest.manifest_version === 3) {
manifest.content_security_policy ??= {};
@@ -453,15 +464,12 @@ function addDevModeCsp(
}
}
function addDevModePermissions(
manifest: Manifest.WebExtensionManifest,
config: InternalConfig,
) {
function addDevModePermissions(manifest: Manifest.WebExtensionManifest) {
// For reloading the page
addPermission(manifest, 'tabs');
// For registering content scripts
if (config.manifestVersion === 3) addPermission(manifest, 'scripting');
if (wxt.config.manifestVersion === 3) addPermission(manifest, 'scripting');
}
/**
@@ -497,11 +505,11 @@ export function getContentScriptCssFiles(
* added to.
*/
export function getContentScriptCssWebAccessibleResources(
config: InternalConfig,
contentScripts: ContentScriptEntrypoint[],
contentScriptCssMap: Record<string, string | undefined>,
): any[] {
const resources: any[] = [];
const resources: Manifest.WebExtensionManifestWebAccessibleResourcesC2ItemType[] =
[];
contentScripts.forEach((script) => {
if (script.options.cssInjectionMode !== 'ui') return;
@@ -509,17 +517,13 @@ export function getContentScriptCssWebAccessibleResources(
const cssFile = contentScriptCssMap[script.name];
if (cssFile == null) return;
if (config.manifestVersion === 2) {
resources.push(cssFile);
} else {
resources.push({
resources: [cssFile],
matches: resolvePerBrowserOption(
script.options.matches,
config.browser,
).map((matchPattern) => stripPathFromMatchPattern(matchPattern)),
});
}
resources.push({
resources: [cssFile],
matches: resolvePerBrowserOption(
script.options.matches,
wxt.config.browser,
).map((matchPattern) => stripPathFromMatchPattern(matchPattern)),
});
});
return resources;
@@ -573,3 +577,43 @@ export function stripPathFromMatchPattern(pattern: string) {
const startOfPath = pattern.indexOf('/', protocolSepIndex + 3);
return pattern.substring(0, startOfPath) + '/*';
}
/**
* Converts all MV3 web accessible resources to their MV2 forms. MV3 web accessible resources are
* generated in this file, and may be defined by the user in their manifest. In both cases, when
* targetting MV2, automatically convert their definitions down to the basic MV2 array.
*/
export function convertWebAccessibleResourcesToMv2(
manifest: Manifest.WebExtensionManifest,
): void {
if (manifest.web_accessible_resources == null) return;
manifest.web_accessible_resources = Array.from(
new Set(
manifest.web_accessible_resources.flatMap((item) => {
if (typeof item === 'string') return item;
return item.resources;
}),
),
);
}
/**
* Make sure all resources are in MV3 format. If not, add a wanring
*/
export function validateMv3WebAccessbileResources(
manifest: Manifest.WebExtensionManifest,
): void {
if (manifest.web_accessible_resources == null) return;
const stringResources = manifest.web_accessible_resources.filter(
(item) => typeof item === 'string',
);
if (stringResources.length > 0) {
throw Error(
`Non-MV3 web_accessible_resources detected: ${JSON.stringify(
stringResources,
)}. When manually defining web_accessible_resources, define them as MV3 objects ({ matches: [...], resources: [...] }), and WXT will automatically convert them to MV2 when necessary.`,
);
}
}
+2 -2
View File
@@ -1,5 +1,5 @@
import dns from 'node:dns';
import { InternalConfig } from '~/types';
import { ResolvedConfig } from '~/types';
import { withTimeout } from './time';
function isOffline(): Promise<boolean> {
@@ -26,7 +26,7 @@ export async function isOnline(): Promise<boolean> {
*/
export async function fetchCached(
url: string,
config: Omit<InternalConfig, 'builder'>,
config: Omit<ResolvedConfig, 'builder'>,
): Promise<string> {
let content: string = '';
+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(
config: InternalConfig,
): Promise<Partial<Record<string, any>> | undefined> {
const file = resolve(config.root, 'package.json');
export async function getPackageJson(): Promise<
Partial<Record<string, any>> | undefined
> {
const file = resolve(wxt.config.root, 'package.json');
try {
return await fs.readJson(file);
} catch (err) {
config.logger.debug(
wxt.logger.debug(
`Failed to read package.json at: ${file}. Returning undefined.`,
);
return {};
+23 -3
View File
@@ -7,7 +7,7 @@ import merge from 'lodash.merge';
import { Commands, type Manifest } from '~/browser';
import {
FsCache,
InternalConfig,
ResolvedConfig,
WxtDevServer,
BackgroundEntrypoint,
ContentScriptEntrypoint,
@@ -20,13 +20,19 @@ 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> = { [key in keyof T]+?: Partial<T[key]> };
type DeepPartial<T> = T extends object
? {
[P in keyof T]?: DeepPartial<T[P]>;
}
: T;
function fakeObjectCreator<T>(base: () => T) {
return (overrides?: DeepPartial<T>): T => merge(base(), overrides);
}
@@ -198,7 +204,7 @@ export function fakeArray<T>(createItem: () => T, count = 3): T[] {
return array;
}
export const fakeInternalConfig = fakeObjectCreator<InternalConfig>(() => {
export const fakeResolvedConfig = fakeObjectCreator<ResolvedConfig>(() => {
const browser = faker.helpers.arrayElement(['chrome', 'firefox']);
const command = faker.helpers.arrayElement(['build', 'serve'] as const);
const manifestVersion = faker.helpers.arrayElement([2, 3] as const);
@@ -230,6 +236,7 @@ export const fakeInternalConfig = fakeObjectCreator<InternalConfig>(() => {
analysis: {
enabled: false,
template: 'treemap',
outputFile: fakeFile(),
},
zip: {
artifactTemplate: '{{name}}-{{version}}.zip',
@@ -249,9 +256,22 @@ export const fakeInternalConfig = fakeObjectCreator<InternalConfig>(() => {
dev: {
reloadCommand: 'Alt+R',
},
hooks: {},
};
});
export const fakeWxt = fakeObjectCreator<Wxt>(() => ({
config: fakeResolvedConfig(),
hooks: mock(),
logger: mock(),
reloadConfig: vi.fn(),
}));
export function setFakeWxt(overrides?: DeepPartial<Wxt>) {
const wxt = fakeWxt(overrides);
setWxtForTesting(wxt);
}
export const fakeBuildOutput = fakeObjectCreator<BuildOutput>(() => ({
manifest: fakeManifest(),
publicAssets: fakeArray(fakeOutputAsset),
+2 -2
View File
@@ -1,9 +1,9 @@
import { UnimportOptions } from 'unimport';
import { InternalConfig } from '~/types';
import { ResolvedConfig } from '~/types';
import { defu } from 'defu';
export function getUnimportOptions(
config: Omit<InternalConfig, 'builder'>,
config: Omit<ResolvedConfig, 'builder'>,
): Partial<UnimportOptions | false> {
if (config.imports === false) return false;
+48
View File
@@ -0,0 +1,48 @@
import { InlineConfig, Wxt, WxtDevServer, WxtHooks } from '~/types';
import { resolveConfig } from './utils/building';
import { createHooks } from 'hookable';
/**
* Global variable set once `createWxt` is called once. Since this variable is used everywhere, this
* global can be used instead of passing the variable as a function parameter everywhere.
*/
export let wxt: Wxt;
/**
* Create and register a global instance of the Wxt interface for use throughout the project.
*/
export async function registerWxt(
command: 'build' | 'serve',
inlineConfig: InlineConfig = {},
server?: WxtDevServer,
): Promise<void> {
const config = await resolveConfig(inlineConfig, command, server);
const hooks = createHooks<WxtHooks>();
wxt = {
config,
hooks,
get logger() {
return config.logger;
},
async reloadConfig() {
wxt.config = await resolveConfig(inlineConfig, command, server);
},
};
// Initialize hooks
wxt.hooks.addHooks(config.hooks);
await wxt.hooks.callHook('ready', wxt);
}
/**
* @internal ONLY USE FOR TESTING.
*
* @example
* setWxtForTesting(fakeWxt({ ... }));
* // Or use the shorthand
* setFakeWxt({ ... })
*/
export function setWxtForTesting(testInstance: Wxt) {
wxt = testInstance;
}
+22 -26
View File
@@ -7,7 +7,8 @@ import { getPackageJson } from '~/core/utils/package';
import { minimatch } from 'minimatch';
import { formatDuration } from '~/core/utils/time';
import { printFileList } from '~/core/utils/log/printFileList';
import { getInternalConfig, internalBuild } from '~/core/utils/building';
import { internalBuild } from '~/core/utils/building';
import { registerWxt, wxt } from './wxt';
/**
* Build and zip the extension for distribution.
@@ -15,59 +16,54 @@ import { getInternalConfig, internalBuild } from '~/core/utils/building';
* @returns A list of all files included in the ZIP.
*/
export async function zip(config?: InlineConfig): Promise<string[]> {
const internalConfig = await getInternalConfig(config ?? {}, 'build');
const output = await internalBuild(internalConfig);
await registerWxt('build', config);
const output = await internalBuild();
const start = Date.now();
internalConfig.logger.info('Zipping extension...');
wxt.logger.info('Zipping extension...');
const zipFiles: string[] = [];
const projectName =
internalConfig.zip.name ??
wxt.config.zip.name ??
kebabCaseAlphanumeric(
(await getPackageJson(internalConfig))?.name || dirname(process.cwd()),
(await getPackageJson())?.name || dirname(process.cwd()),
);
const applyTemplate = (template: string): string =>
template
.replaceAll('{{name}}', projectName)
.replaceAll('{{browser}}', internalConfig.browser)
.replaceAll('{{browser}}', wxt.config.browser)
.replaceAll(
'{{version}}',
output.manifest.version_name ?? output.manifest.version,
)
.replaceAll('{{manifestVersion}}', `mv${internalConfig.manifestVersion}`);
.replaceAll('{{manifestVersion}}', `mv${wxt.config.manifestVersion}`);
await fs.ensureDir(internalConfig.outBaseDir);
await fs.ensureDir(wxt.config.outBaseDir);
// ZIP output directory
const outZipFilename = applyTemplate(internalConfig.zip.artifactTemplate);
const outZipPath = resolve(internalConfig.outBaseDir, outZipFilename);
await zipdir(internalConfig.outDir, {
const outZipFilename = applyTemplate(wxt.config.zip.artifactTemplate);
const outZipPath = resolve(wxt.config.outBaseDir, outZipFilename);
await zipdir(wxt.config.outDir, {
saveTo: outZipPath,
});
zipFiles.push(outZipPath);
// ZIP sources for Firefox
if (internalConfig.browser === 'firefox') {
const sourcesZipFilename = applyTemplate(
internalConfig.zip.sourcesTemplate,
);
const sourcesZipPath = resolve(
internalConfig.outBaseDir,
sourcesZipFilename,
);
await zipdir(internalConfig.zip.sourcesRoot, {
if (wxt.config.browser === 'firefox') {
const sourcesZipFilename = applyTemplate(wxt.config.zip.sourcesTemplate);
const sourcesZipPath = resolve(wxt.config.outBaseDir, sourcesZipFilename);
await zipdir(wxt.config.zip.sourcesRoot, {
saveTo: sourcesZipPath,
filter(path) {
const relativePath = relative(internalConfig.zip.sourcesRoot, path);
const relativePath = relative(wxt.config.zip.sourcesRoot, path);
return (
internalConfig.zip.includeSources.some((pattern) =>
wxt.config.zip.includeSources.some((pattern) =>
minimatch(relativePath, pattern),
) ||
!internalConfig.zip.excludeSources.some((pattern) =>
!wxt.config.zip.excludeSources.some((pattern) =>
minimatch(relativePath, pattern),
)
);
@@ -77,9 +73,9 @@ export async function zip(config?: InlineConfig): Promise<string[]> {
}
await printFileList(
internalConfig.logger.success,
wxt.logger.success,
`Zipped extension in ${formatDuration(Date.now() - start)}`,
internalConfig.outBaseDir,
wxt.config.outBaseDir,
zipFiles,
);
+1 -1
View File
@@ -2,5 +2,5 @@
* @module wxt
*/
export * from '~/core';
export * from '~/types/external';
export * from '~/types';
export * from '~/version';
+2 -2
View File
@@ -6,7 +6,7 @@ import {
globals,
webextensionPolyfillMock,
} from '../core/builders/vite/plugins';
import { getInternalConfig } from '~/core/utils/building';
import { resolveConfig } from '~/core/utils/building';
import { InlineConfig } from '../types';
/**
@@ -25,7 +25,7 @@ import { InlineConfig } from '../types';
* @param inlineConfig Customize WXT's config for testing. Any config specified here overrides the config from your `wxt.config.ts` file.
*/
export function WxtVitest(inlineConfig?: InlineConfig): vite.PluginOption {
return getInternalConfig(inlineConfig ?? {}, 'serve').then((config) => [
return resolveConfig(inlineConfig ?? {}, 'serve').then((config) => [
webextensionPolyfillMock(config),
unimport(config),
globals(config),
-795
View File
@@ -1,795 +0,0 @@
import type * as vite from 'vite';
import type { Manifest, Scripting } from '~/browser';
import { UnimportOptions } from 'unimport';
import { LogLevel } from 'consola';
import { ContentScriptContext } from '../client/content-scripts/content-script-context';
import type { PluginVisualizerOptions } from 'rollup-plugin-visualizer';
import type { FSWatcher } from 'chokidar';
export interface InlineConfig {
/**
* Your project's root directory containing the `package.json` used to fill out the
* `manifest.json`.
*
* @default process.cwd()
*/
root?: string;
/**
* Directory containing all source code. Set to `"src"` to move all source code to a `src/`
* directory.
*
* @default config.root
*/
srcDir?: string;
/**
* Directory containing files that will be copied to the output directory as-is.
*
* @default "${config.root}/public"
*/
publicDir?: string;
/**
* @default "${config.srcDir}/entrypoints"
*/
entrypointsDir?: string;
/**
* A list of entrypoint names (`"popup"`, `"options"`, etc.) to build. Will speed up the build if
* your extension has lots of entrypoints, and you don't need to build all of them to develop a
* feature.
*/
filterEntrypoints?: string[];
/**
* Output directory that stored build folders and ZIPs.
*
* @default ".output"
*/
outDir?: string;
/**
* > Only available when using the JS API. Not available in `wxt.config.ts` files
*
* Path to `wxt.config.ts` file or `false` to disable config file discovery.
*
* @default "wxt.config.ts"
*/
configFile?: string | false;
/**
* Set to `true` to show debug logs. Overriden by the command line `--debug` option.
*
* @default false
*/
debug?: boolean;
/**
* Explicitly set a mode to run in. This will override the default mode for each command, and can
* be overridden by the command line `--mode` option.
*/
mode?: string;
/**
* Customize auto-import options. Set to `false` to disable auto-imports.
*
* For example, to add a directory to auto-import from, you can use:
*
* ```ts
* export default defineConfig({
* imports: {
* dirs: ["some-directory"]
* }
* })
* ```
*/
imports?: Partial<UnimportOptions> | false;
/**
* Explicitly set a browser to build for. This will override the default browser for each command,
* and can be overridden by the command line `--browser` option.
*
* @default
* "chrome"
*/
browser?: TargetBrowser;
/**
* Explicitly set a manifest version to target. This will override the default manifest version
* for each command, and can be overridden by the command line `--mv2` or `--mv3` option.
*/
manifestVersion?: TargetManifestVersion;
/**
* Override the logger used.
*
* @default
* consola
*/
logger?: Logger;
/**
* Customize the `manifest.json` output. Can be an object, promise, or function that returns an
* object or promise.
*/
manifest?: UserManifest | Promise<UserManifest> | UserManifestFn;
/**
* Custom runner options. Options set here can be overridden in a `web-ext.config.ts` file.
*/
runner?: ExtensionRunnerConfig;
zip?: {
/**
* Configure the filename output when zipping files.
*
* Available template variables:
*
* - `{{name}}` - The project's name converted to kebab-case
* - `{{version}}` - The version_name or version from the manifest
* - `{{browser}}` - The target browser from the `--browser` CLI flag
* - `{{manifestVersion}}` - Either "2" or "3"
*
* @default "{{name}}-{{version}}-{{browser}}.zip"
*/
artifactTemplate?: string;
/**
* Configure the filename output when zipping files.
*
* Available template variables:
*
* - `{{name}}` - The project's name converted to kebab-case
* - `{{version}}` - The version_name or version from the manifest
* - `{{browser}}` - The target browser from the `--browser` CLI flag
* - `{{manifestVersion}}` - Either "2" or "3"
*
* @default "{{name}}-{{version}}-sources.zip"
*/
sourcesTemplate?: string;
/**
* Override the artifactTemplate's `{name}` template variable. Defaults to the `package.json`'s
* name, or if that doesn't exist, the current working directories name.
*/
name?: string;
/**
* Root directory to ZIP when generating the sources ZIP.
*
* @default config.root
*/
sourcesRoot?: string;
/**
* [Minimatch](https://www.npmjs.com/package/minimatch) patterns of files to include when
* creating a ZIP of all your source code for Firefox. Patterns are relative to your
* `config.zip.sourcesRoot`.
*
* This setting overrides `excludeSources`. So if a file matches both lists, it is included in the ZIP.
*
* @example
* [
* "coverage", // Ignore the coverage directory in the `sourcesRoot`
* ]
*/
includeSources?: string[];
/**
* [Minimatch](https://www.npmjs.com/package/minimatch) patterns of files to exclude when
* creating a ZIP of all your source code for Firefox. Patterns are relative to your
* `config.zip.sourcesRoot`.
*
* Hidden files, node_modules, and tests are ignored by default.
*
* @example
* [
* "coverage", // Ignore the coverage directory in the `sourcesRoot`
* ]
*/
excludeSources?: string[];
};
/**
* Transform the final manifest before it's written to the file system. Edit the `manifest`
* parameter directly, do not return a new object. Return values are ignored.
*
* @example
* defineConfig({
* // Add a CSS-only content script.
* transformManifest(manifest) {
* manifest.content_scripts.push({
* matches: ["*://google.com/*"],
* css: ["content-scripts/some-example.css"],
* });
* }
* })
*/
transformManifest?: (manifest: Manifest.WebExtensionManifest) => void;
analysis?: {
/**
* Explicitly include bundle analysis when running `wxt build`. This can be overridden by the
* command line `--analysis` option.
*
* @default false
*/
enabled?: boolean;
/**
* When running `wxt build --analyze` or setting `analysis.enabled` to true, customize how the
* bundle will be visualized. See
* [`rollup-plugin-visualizer`](https://github.com/btd/rollup-plugin-visualizer#how-to-use-generated-files)
* for more details.
*
* @default "treemap"
*/
template?: PluginVisualizerOptions['template'];
};
/**
* Add additional paths to the `.wxt/tsconfig.json`. Use this instead of overwriting the `paths`
* in the root `tsconfig.json` if you want to add new paths.
*
* The key is the import alias and the value is either a relative path to the root directory or an absolute path.
*
* @example
* {
* "testing": "src/utils/testing.ts"
* }
*/
alias?: Record<string, string>;
/**
* Experimental settings - use with caution.
*/
experimental?: {
/**
* Whether to use [`webextension-polyfill`](https://www.npmjs.com/package/webextension-polyfill)
* when importing `browser` from `wxt/browser`.
*
* When set to `false`, WXT will export the chrome global instead of the polyfill from
* `wxt/browser`.
*
* You should use `browser` to access the web extension APIs.
*
* @experimental This option will remain experimental until Manifest V2 is dead.
*
* @default true
* @example
* export default defineConfig({
* experimental: {
* includeBrowserPolyfill: false
* }
* })
*/
includeBrowserPolyfill?: boolean;
};
/**
* Config effecting dev mode only.
*/
dev?: {
/**
* Controls whether a custom keyboard shortcut command, `Alt+R`, is added during dev mode to
* quickly reload the extension.
*
* If false, the shortcut is not added during development.
*
* If set to a custom string, you can override the key combo used. See
* [Chrome's command docs](https://developer.chrome.com/docs/extensions/reference/api/commands)
* for available options.
*
* @default "Alt+R"
*/
reloadCommand?: string | false;
};
}
// TODO: Extract to @wxt/vite-builder and use module augmentation to include the vite field
export interface InlineConfig {
/**
* Return custom Vite options from a function. See
* <https://vitejs.dev/config/shared-options.html>.
*
* [`root`](#root), [`configFile`](#configfile), and [`mode`](#mode) should be set in WXT's config
* instead of Vite's.
*
* This is a function because any vite plugins added need to be recreated for each individual
* build step, incase they have internal state causing them to fail when reused.
*/
vite?: (env: ConfigEnv) => WxtViteConfig | Promise<WxtViteConfig>;
}
// TODO: Move into @wxt/vite-builder
export type WxtViteConfig = Omit<
vite.UserConfig,
'root' | 'configFile' | 'mode'
>;
export interface BuildOutput {
manifest: Manifest.WebExtensionManifest;
publicAssets: OutputAsset[];
steps: BuildStepOutput[];
}
export type OutputFile = OutputChunk | OutputAsset;
export interface OutputChunk {
type: 'chunk';
/**
* Relative, normalized path relative to the output directory.
*
* Ex: "content-scripts/overlay.js"
*/
fileName: string;
/**
* Absolute, normalized paths to all dependencies this chunk relies on.
*/
moduleIds: string[];
}
export interface OutputAsset {
type: 'asset';
/**
* Relative, normalized path relative to the output directory.
*
* Ex: "icons/16.png"
*/
fileName: string;
}
export interface BuildStepOutput {
entrypoints: EntrypointGroup;
chunks: OutputFile[];
}
export interface WxtDevServer
extends Omit<WxtBuilderServer, 'listen' | 'close'>,
ServerInfo {
/**
* Stores the current build output of the server.
*/
currentOutput: BuildOutput | undefined;
/**
* Start the server.
*/
start(): Promise<void>;
/**
* Stop the server.
*/
stop(): Promise<void>;
/**
* Close the browser, stop the server, rebuild the entire extension, and start the server again.
*/
restart(): Promise<void>;
/**
* Transform the HTML for dev mode.
*/
transformHtml(
url: string,
html: string,
originalUrl?: string | undefined,
): Promise<string>;
/**
* Tell the extension to reload by running `browser.runtime.reload`.
*/
reloadExtension: () => void;
/**
* Tell an extension page to reload.
*
* The path is the bundle path, not the input paths, so if the input paths is
* "src/options/index.html", you would pass "options.html" because that's where it is written to
* in the dist directory, and where it's available at in the actual extension.
*
* @example
* server.reloadPage("popup.html")
* server.reloadPage("sandbox.html")
*/
reloadPage: (path: string) => void;
/**
* Tell the extension to restart a content script.
*
* @param contentScript The manifest definition for a content script
*/
reloadContentScript: (
contentScript: Omit<Scripting.RegisteredContentScript, 'id'>,
) => void;
/**
* Grab the latest runner config and restart the browser.
*/
restartBrowser: () => void;
}
export type TargetBrowser = string;
export type TargetManifestVersion = 2 | 3;
export type UserConfig = Omit<InlineConfig, 'configFile'>;
export interface Logger {
debug(...args: any[]): void;
log(...args: any[]): void;
info(...args: any[]): void;
warn(...args: any[]): void;
error(...args: any[]): void;
fatal(...args: any[]): void;
success(...args: any[]): void;
level: LogLevel;
}
export interface BaseEntrypointOptions {
include?: TargetBrowser[];
exclude?: TargetBrowser[];
}
export interface BaseEntrypoint {
/**
* The entrypoint's name. This is the filename or dirname without the type suffix.
*
* Examples:
* - `popup.html` &rarr; `popup`
* - `options/index.html` &rarr; `options`
* - `named.sandbox.html` &rarr; `named`
* - `named.sandbox/index.html` &rarr; `named`
* - `sandbox.html` &rarr; `sandbox`
* - `sandbox/index.html` &rarr; `sandbox`
* - `overlay.content.ts` &rarr; `overlay`
* - `overlay.content/index.ts` &rarr; `overlay`
*
* The name is used when generating an output file:
* `<entrypoint.outputDir>/<entrypoint.name>.<ext>`
*/
name: string;
/**
* Absolute path to the entrypoint's input file.
*/
inputPath: string;
/**
* Absolute path to the entrypoint's output directory. Can be the`InternalConfg.outDir` or a
* subdirectory of it.
*/
outputDir: string;
options: BaseEntrypointOptions;
skipped: boolean;
}
export interface GenericEntrypoint extends BaseEntrypoint {
type:
| 'sandbox'
| 'bookmarks'
| 'history'
| 'newtab'
| 'sidepanel'
| 'devtools'
| 'unlisted-page'
| 'unlisted-script'
| 'unlisted-style'
| 'content-script-style';
}
export interface BackgroundEntrypoint extends BaseEntrypoint {
type: 'background';
options: {
persistent?: boolean;
type?: 'module';
} & BaseEntrypointOptions;
}
export interface ContentScriptEntrypoint extends BaseEntrypoint {
type: 'content-script';
options: Omit<ContentScriptDefinition, 'main'> & BaseEntrypointOptions;
}
export interface PopupEntrypoint extends BaseEntrypoint {
type: 'popup';
options: {
/**
* Defaults to "browser_action" to be equivalent to MV3's "action" key
*/
mv2Key?: 'browser_action' | 'page_action';
defaultIcon?: Record<string, string>;
defaultTitle?: string;
browserStyle?: boolean;
} & BaseEntrypointOptions;
}
export interface OptionsEntrypoint extends BaseEntrypoint {
type: 'options';
options: {
openInTab?: boolean;
browserStyle?: boolean;
chromeStyle?: boolean;
} & BaseEntrypointOptions;
}
export type Entrypoint =
| GenericEntrypoint
| BackgroundEntrypoint
| ContentScriptEntrypoint
| PopupEntrypoint
| OptionsEntrypoint;
export type EntrypointGroup = Entrypoint | Entrypoint[];
export type OnContentScriptStopped = (cb: () => void) => void;
export type ContentScriptDefinition =
| ContentScriptIsolatedWorldDefinition
| ContentScriptMainWorldDefinition;
export interface ContentScriptIsolatedWorldDefinition
extends ContentScriptBaseDefinition {
/**
* See https://developer.chrome.com/docs/extensions/develop/concepts/content-scripts#isolated_world
* @default "ISOLATED"
*/
world?: 'ISOLATED';
/**
* Main function executed when the content script is loaded.
*/
main(ctx: ContentScriptContext): void | Promise<void>;
}
export interface ContentScriptMainWorldDefinition
extends ContentScriptBaseDefinition {
/**
* See https://developer.chrome.com/docs/extensions/develop/concepts/content-scripts#isolated_world
*/
world: 'MAIN';
/**
* Main function executed when the content script is loaded.
*/
main(): void | Promise<void>;
}
export interface ContentScriptBaseDefinition extends ExcludableEntrypoint {
matches: PerBrowserOption<Manifest.ContentScript['matches']>;
/**
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
* @default "documentIdle"
*/
runAt?: PerBrowserOption<Manifest.ContentScript['run_at']>;
/**
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
* @default false
*/
matchAboutBlank?: PerBrowserOption<
Manifest.ContentScript['match_about_blank']
>;
/**
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
* @default []
*/
excludeMatches?: PerBrowserOption<Manifest.ContentScript['exclude_matches']>;
/**
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
* @default []
*/
includeGlobs?: PerBrowserOption<Manifest.ContentScript['include_globs']>;
/**
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
* @default []
*/
excludeGlobs?: PerBrowserOption<Manifest.ContentScript['exclude_globs']>;
/**
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
* @default false
*/
allFrames?: PerBrowserOption<Manifest.ContentScript['all_frames']>;
/**
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
* @default false
*/
matchOriginAsFallback?: PerBrowserOption<boolean>;
/**
* Customize how imported/generated styles are injected with the content script. Regardless of the
* mode selected, CSS will always be built and included in the output directory.
*
* - `"manifest"` - Include the CSS in the manifest, under the content script's `css` array.
* - `"manual"` - Exclude the CSS from the manifest. You are responsible for manually loading it
* onto the page. Use `browser.runtime.getURL("content-scripts/<name>.css")` to get the file's
* URL
* - `"ui"` - Exclude the CSS from the manifest. CSS will be automatically added to your UI when
* calling `createContentScriptUi`
*
* @default "manifest"
*/
cssInjectionMode?: PerBrowserOption<'manifest' | 'manual' | 'ui'>;
}
export interface BackgroundDefinition extends ExcludableEntrypoint {
type?: PerBrowserOption<'module'>;
persistent?: PerBrowserOption<boolean>;
main(): void;
}
export interface UnlistedScriptDefinition extends ExcludableEntrypoint {
/**
* Main function executed when the unlisted script is ran.
*/
main(): void | Promise<void>;
}
export type PerBrowserOption<T> = T | { [browser: TargetBrowser]: T };
export interface ExcludableEntrypoint {
/**
* List of target browsers to include this entrypoint in. Defaults to being included in all
* builds. Cannot be used with `exclude`. You must choose one of the two options.
*
* @default undefined
*/
include?: TargetBrowser[];
/**
* List of target browsers to exclude this entrypoint from. Cannot be used with `include`. You
* must choose one of the two options.
*
* @default undefined
*/
exclude?: TargetBrowser[];
}
/**
* Manifest customization available in the `wxt.config.ts` file. You cannot configure entrypoints
* here, they are configured inline.
*/
export type UserManifest = Partial<
Omit<
Manifest.WebExtensionManifest,
| 'background'
| 'chrome_url_overrides'
| 'devtools_page'
| 'manifest_version'
| 'options_page'
| 'options_ui'
| 'sandbox'
| 'sidepanel'
| 'sidebar_action'
>
>;
export type UserManifestFn = (
env: ConfigEnv,
) => UserManifest | Promise<UserManifest>;
export interface ConfigEnv {
mode: string;
command: 'build' | 'serve';
/**
* Browser passed in from the CLI
*/
browser: TargetBrowser;
/**
* Manifest version passed in from the CLI
*/
manifestVersion: 2 | 3;
}
/**
* Configure how the browser starts up.
*/
export interface ExtensionRunnerConfig {
/**
* Whether or not to open the browser with the extension installed in dev mode.
*
* @default false
*/
disabled?: boolean;
/**
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#browser-console
*/
openConsole?: boolean;
/**
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#devtools
*/
openDevtools?: boolean;
/**
* List of browser names and the binary that should be used to open the browser.
*
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#chromium-binary
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#firefox
*/
binaries?: Record<string, string>;
/**
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#firefox-profile
*/
firefoxProfile?: string;
/**
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#chromium-profile
*/
chromiumProfile?: string;
/**
* An map of chrome preferences from https://chromium.googlesource.com/chromium/src/+/main/chrome/common/pref_names.h
*
* @example
* // change your downloads directory
* {
* download: {
* default_directory: "/my/custom/dir",
* },
* }
*
* @default
* // Enable dev mode and allow content script sourcemaps
* {
* devtools: {
* synced_preferences_sync_disabled: {
* skipContentScripts: false,
* },
* }
* extensions: {
* ui: {
* developer_mode: true,
* },
* }
* }
*/
chromiumPref?: string;
/**
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#pref
*/
firefoxPrefs?: Record<string, string>;
/**
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#args
*/
firefoxArgs?: string[];
/**
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#args
*/
chromiumArgs?: string[];
/**
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#start-url
*/
startUrls?: string[];
}
export interface WxtBuilder {
/**
* Name of tool used to build. Ex: "Vite" or "Webpack".
*/
name: string;
/**
* Version of tool used to build. Ex: "5.0.2"
*/
version: string;
/**
* Build a single entrypoint group. This is effectively one of the multiple "steps" during the
* build process.
*/
build(group: EntrypointGroup): Promise<BuildStepOutput>;
/**
* Start a dev server at the provided port.
*/
createServer(info: ServerInfo): Promise<WxtBuilderServer>;
}
export interface WxtBuilderServer {
/**
* Start the server.
*/
listen(): Promise<void>;
/**
* Stop the server.
*/
close(): Promise<void>;
/**
* Transform the HTML for dev mode.
*/
transformHtml(
url: string,
html: string,
originalUrl?: string | undefined,
): Promise<string>;
/**
* The web socket server used to communicate with the extension.
*/
ws: {
/**
* Send a message via the server's websocket, with an optional payload.
*
* @example
* ws.send("wxt:reload-extension");
* ws.send("wxt:reload-content-script", { ... });
*/
send(message: string, payload?: any): void;
/**
* Listen for messages over the server's websocket.
*/
on(message: string, cb: (payload: any) => void): void;
};
/**
* Chokidar file watcher instance.
*/
watcher: FSWatcher;
}
export interface ServerInfo {
/**
* Ex: `3000`
*/
port: number;
/**
* Ex: `"localhost"`
*/
hostname: string;
/**
* Ex: `"http://localhost:3000"`
*/
origin: string;
}
+936 -2
View File
@@ -1,2 +1,936 @@
export * from './external';
export * from './internal';
import type * as vite from 'vite';
import type { Manifest, Scripting } from '~/browser';
import { UnimportOptions } from 'unimport';
import { LogLevel } from 'consola';
import { ContentScriptContext } from '../client/content-scripts/content-script-context';
import type { PluginVisualizerOptions } from 'rollup-plugin-visualizer';
import type { FSWatcher } from 'chokidar';
import { ResolvedConfig as C12ResolvedConfig } from 'c12';
import { Hookable, NestedHooks } from 'hookable';
export interface InlineConfig {
/**
* Your project's root directory containing the `package.json` used to fill out the
* `manifest.json`.
*
* @default process.cwd()
*/
root?: string;
/**
* Directory containing all source code. Set to `"src"` to move all source code to a `src/`
* directory.
*
* @default config.root
*/
srcDir?: string;
/**
* Directory containing files that will be copied to the output directory as-is.
*
* @default "${config.root}/public"
*/
publicDir?: string;
/**
* @default "${config.srcDir}/entrypoints"
*/
entrypointsDir?: string;
/**
* A list of entrypoint names (`"popup"`, `"options"`, etc.) to build. Will speed up the build if
* your extension has lots of entrypoints, and you don't need to build all of them to develop a
* feature.
*/
filterEntrypoints?: string[];
/**
* Output directory that stored build folders and ZIPs.
*
* @default ".output"
*/
outDir?: string;
/**
* > Only available when using the JS API. Not available in `wxt.config.ts` files
*
* Path to `wxt.config.ts` file or `false` to disable config file discovery.
*
* @default "wxt.config.ts"
*/
configFile?: string | false;
/**
* Set to `true` to show debug logs. Overriden by the command line `--debug` option.
*
* @default false
*/
debug?: boolean;
/**
* Explicitly set a mode to run in. This will override the default mode for each command, and can
* be overridden by the command line `--mode` option.
*/
mode?: string;
/**
* Customize auto-import options. Set to `false` to disable auto-imports.
*
* For example, to add a directory to auto-import from, you can use:
*
* ```ts
* export default defineConfig({
* imports: {
* dirs: ["some-directory"]
* }
* })
* ```
*/
imports?: Partial<UnimportOptions> | false;
/**
* Explicitly set a browser to build for. This will override the default browser for each command,
* and can be overridden by the command line `--browser` option.
*
* @default
* "chrome"
*/
browser?: TargetBrowser;
/**
* Explicitly set a manifest version to target. This will override the default manifest version
* for each command, and can be overridden by the command line `--mv2` or `--mv3` option.
*/
manifestVersion?: TargetManifestVersion;
/**
* Override the logger used.
*
* @default
* consola
*/
logger?: Logger;
/**
* Customize the `manifest.json` output. Can be an object, promise, or function that returns an
* object or promise.
*/
manifest?: UserManifest | Promise<UserManifest> | UserManifestFn;
/**
* Custom runner options. Options set here can be overridden in a `web-ext.config.ts` file.
*/
runner?: ExtensionRunnerConfig;
zip?: {
/**
* Configure the filename output when zipping files.
*
* Available template variables:
*
* - `{{name}}` - The project's name converted to kebab-case
* - `{{version}}` - The version_name or version from the manifest
* - `{{browser}}` - The target browser from the `--browser` CLI flag
* - `{{manifestVersion}}` - Either "2" or "3"
*
* @default "{{name}}-{{version}}-{{browser}}.zip"
*/
artifactTemplate?: string;
/**
* Configure the filename output when zipping files.
*
* Available template variables:
*
* - `{{name}}` - The project's name converted to kebab-case
* - `{{version}}` - The version_name or version from the manifest
* - `{{browser}}` - The target browser from the `--browser` CLI flag
* - `{{manifestVersion}}` - Either "2" or "3"
*
* @default "{{name}}-{{version}}-sources.zip"
*/
sourcesTemplate?: string;
/**
* Override the artifactTemplate's `{name}` template variable. Defaults to the `package.json`'s
* name, or if that doesn't exist, the current working directories name.
*/
name?: string;
/**
* Root directory to ZIP when generating the sources ZIP.
*
* @default config.root
*/
sourcesRoot?: string;
/**
* [Minimatch](https://www.npmjs.com/package/minimatch) patterns of files to include when
* creating a ZIP of all your source code for Firefox. Patterns are relative to your
* `config.zip.sourcesRoot`.
*
* This setting overrides `excludeSources`. So if a file matches both lists, it is included in the ZIP.
*
* @example
* [
* "coverage", // Ignore the coverage directory in the `sourcesRoot`
* ]
*/
includeSources?: string[];
/**
* [Minimatch](https://www.npmjs.com/package/minimatch) patterns of files to exclude when
* creating a ZIP of all your source code for Firefox. Patterns are relative to your
* `config.zip.sourcesRoot`.
*
* Hidden files, node_modules, and tests are ignored by default.
*
* @example
* [
* "coverage", // Ignore the coverage directory in the `sourcesRoot`
* ]
*/
excludeSources?: string[];
};
/**
* @deprecated Use `hooks.build.manifestGenerated` to modify your manifest instead. This option
* will be removed in v1.0
*
* Transform the final manifest before it's written to the file system. Edit the `manifest`
* parameter directly, do not return a new object. Return values are ignored.
*
* @example
* defineConfig({
* // Add a CSS-only content script.
* transformManifest(manifest) {
* manifest.content_scripts.push({
* matches: ["*://google.com/*"],
* css: ["content-scripts/some-example.css"],
* });
* }
* })
*/
transformManifest?: (manifest: Manifest.WebExtensionManifest) => void;
analysis?: {
/**
* Explicitly include bundle analysis when running `wxt build`. This can be overridden by the
* command line `--analysis` option.
*
* @default false
*/
enabled?: boolean;
/**
* When running `wxt build --analyze` or setting `analysis.enabled` to true, customize how the
* bundle will be visualized. See
* [`rollup-plugin-visualizer`](https://github.com/btd/rollup-plugin-visualizer#how-to-use-generated-files)
* for more details.
*
* @default "treemap"
*/
template?: PluginVisualizerOptions['template'];
/**
* Name of the output HTML file. Relative to the project's root directory.
*
* @default "stats.html"
*/
outputFile?: string;
};
/**
* Add additional paths to the `.wxt/tsconfig.json`. Use this instead of overwriting the `paths`
* in the root `tsconfig.json` if you want to add new paths.
*
* The key is the import alias and the value is either a relative path to the root directory or an absolute path.
*
* @example
* {
* "testing": "src/utils/testing.ts"
* }
*/
alias?: Record<string, string>;
/**
* Experimental settings - use with caution.
*/
experimental?: {
/**
* Whether to use [`webextension-polyfill`](https://www.npmjs.com/package/webextension-polyfill)
* when importing `browser` from `wxt/browser`.
*
* When set to `false`, WXT will export the chrome global instead of the polyfill from
* `wxt/browser`.
*
* You should use `browser` to access the web extension APIs.
*
* @experimental This option will remain experimental until Manifest V2 is dead.
*
* @default true
* @example
* export default defineConfig({
* experimental: {
* includeBrowserPolyfill: false
* }
* })
*/
includeBrowserPolyfill?: boolean;
};
/**
* Config effecting dev mode only.
*/
dev?: {
/**
* Controls whether a custom keyboard shortcut command, `Alt+R`, is added during dev mode to
* quickly reload the extension.
*
* If false, the shortcut is not added during development.
*
* If set to a custom string, you can override the key combo used. See
* [Chrome's command docs](https://developer.chrome.com/docs/extensions/reference/api/commands)
* for available options.
*
* @default "Alt+R"
*/
reloadCommand?: string | false;
};
/**
* Project hooks for running logic during the build process.
*/
hooks?: NestedHooks<WxtHooks>;
}
// TODO: Extract to @wxt/vite-builder and use module augmentation to include the vite field
export interface InlineConfig {
/**
* Return custom Vite options from a function. See
* <https://vitejs.dev/config/shared-options.html>.
*
* [`root`](#root), [`configFile`](#configfile), and [`mode`](#mode) should be set in WXT's config
* instead of Vite's.
*
* This is a function because any vite plugins added need to be recreated for each individual
* build step, incase they have internal state causing them to fail when reused.
*/
vite?: (env: ConfigEnv) => WxtViteConfig | Promise<WxtViteConfig>;
}
// TODO: Move into @wxt/vite-builder
export type WxtViteConfig = Omit<
vite.UserConfig,
'root' | 'configFile' | 'mode'
>;
export interface BuildOutput {
manifest: Manifest.WebExtensionManifest;
publicAssets: OutputAsset[];
steps: BuildStepOutput[];
}
export type OutputFile = OutputChunk | OutputAsset;
export interface OutputChunk {
type: 'chunk';
/**
* Relative, normalized path relative to the output directory.
*
* Ex: "content-scripts/overlay.js"
*/
fileName: string;
/**
* Absolute, normalized paths to all dependencies this chunk relies on.
*/
moduleIds: string[];
}
export interface OutputAsset {
type: 'asset';
/**
* Relative, normalized path relative to the output directory.
*
* Ex: "icons/16.png"
*/
fileName: string;
}
export interface BuildStepOutput {
entrypoints: EntrypointGroup;
chunks: OutputFile[];
}
export interface WxtDevServer
extends Omit<WxtBuilderServer, 'listen' | 'close'>,
ServerInfo {
/**
* Stores the current build output of the server.
*/
currentOutput: BuildOutput | undefined;
/**
* Start the server.
*/
start(): Promise<void>;
/**
* Stop the server.
*/
stop(): Promise<void>;
/**
* Close the browser, stop the server, rebuild the entire extension, and start the server again.
*/
restart(): Promise<void>;
/**
* Transform the HTML for dev mode.
*/
transformHtml(
url: string,
html: string,
originalUrl?: string | undefined,
): Promise<string>;
/**
* Tell the extension to reload by running `browser.runtime.reload`.
*/
reloadExtension: () => void;
/**
* Tell an extension page to reload.
*
* The path is the bundle path, not the input paths, so if the input paths is
* "src/options/index.html", you would pass "options.html" because that's where it is written to
* in the dist directory, and where it's available at in the actual extension.
*
* @example
* server.reloadPage("popup.html")
* server.reloadPage("sandbox.html")
*/
reloadPage: (path: string) => void;
/**
* Tell the extension to restart a content script.
*
* @param contentScript The manifest definition for a content script
*/
reloadContentScript: (
contentScript: Omit<Scripting.RegisteredContentScript, 'id'>,
) => void;
/**
* Grab the latest runner config and restart the browser.
*/
restartBrowser: () => void;
}
export type TargetBrowser = string;
export type TargetManifestVersion = 2 | 3;
export type UserConfig = Omit<InlineConfig, 'configFile'>;
export interface Logger {
debug(...args: any[]): void;
log(...args: any[]): void;
info(...args: any[]): void;
warn(...args: any[]): void;
error(...args: any[]): void;
fatal(...args: any[]): void;
success(...args: any[]): void;
level: LogLevel;
}
export interface BaseEntrypointOptions {
include?: TargetBrowser[];
exclude?: TargetBrowser[];
}
export interface BaseEntrypoint {
/**
* The entrypoint's name. This is the filename or dirname without the type suffix.
*
* Examples:
* - `popup.html` &rarr; `popup`
* - `options/index.html` &rarr; `options`
* - `named.sandbox.html` &rarr; `named`
* - `named.sandbox/index.html` &rarr; `named`
* - `sandbox.html` &rarr; `sandbox`
* - `sandbox/index.html` &rarr; `sandbox`
* - `overlay.content.ts` &rarr; `overlay`
* - `overlay.content/index.ts` &rarr; `overlay`
*
* The name is used when generating an output file:
* `<entrypoint.outputDir>/<entrypoint.name>.<ext>`
*/
name: string;
/**
* Absolute path to the entrypoint's input file.
*/
inputPath: string;
/**
* Absolute path to the entrypoint's output directory. Can be the`InternalConfg.outDir` or a
* subdirectory of it.
*/
outputDir: string;
options: BaseEntrypointOptions;
skipped: boolean;
}
export interface GenericEntrypoint extends BaseEntrypoint {
type:
| 'sandbox'
| 'bookmarks'
| 'history'
| 'newtab'
| 'sidepanel'
| 'devtools'
| 'unlisted-page'
| 'unlisted-script'
| 'unlisted-style'
| 'content-script-style';
}
export interface BackgroundEntrypoint extends BaseEntrypoint {
type: 'background';
options: {
persistent?: boolean;
type?: 'module';
} & BaseEntrypointOptions;
}
export interface ContentScriptEntrypoint extends BaseEntrypoint {
type: 'content-script';
options: Omit<ContentScriptDefinition, 'main'> & BaseEntrypointOptions;
}
export interface PopupEntrypoint extends BaseEntrypoint {
type: 'popup';
options: {
/**
* Defaults to "browser_action" to be equivalent to MV3's "action" key
*/
mv2Key?: 'browser_action' | 'page_action';
defaultIcon?: Record<string, string>;
defaultTitle?: string;
browserStyle?: boolean;
} & BaseEntrypointOptions;
}
export interface OptionsEntrypoint extends BaseEntrypoint {
type: 'options';
options: {
openInTab?: boolean;
browserStyle?: boolean;
chromeStyle?: boolean;
} & BaseEntrypointOptions;
}
export type Entrypoint =
| GenericEntrypoint
| BackgroundEntrypoint
| ContentScriptEntrypoint
| PopupEntrypoint
| OptionsEntrypoint;
export type EntrypointGroup = Entrypoint | Entrypoint[];
export type OnContentScriptStopped = (cb: () => void) => void;
export type ContentScriptDefinition =
| ContentScriptIsolatedWorldDefinition
| ContentScriptMainWorldDefinition;
export interface ContentScriptIsolatedWorldDefinition
extends ContentScriptBaseDefinition {
/**
* See https://developer.chrome.com/docs/extensions/develop/concepts/content-scripts#isolated_world
* @default "ISOLATED"
*/
world?: 'ISOLATED';
/**
* Main function executed when the content script is loaded.
*/
main(ctx: ContentScriptContext): void | Promise<void>;
}
export interface ContentScriptMainWorldDefinition
extends ContentScriptBaseDefinition {
/**
* See https://developer.chrome.com/docs/extensions/develop/concepts/content-scripts#isolated_world
*/
world: 'MAIN';
/**
* Main function executed when the content script is loaded.
*/
main(): void | Promise<void>;
}
export interface ContentScriptBaseDefinition extends ExcludableEntrypoint {
matches: PerBrowserOption<Manifest.ContentScript['matches']>;
/**
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
* @default "documentIdle"
*/
runAt?: PerBrowserOption<Manifest.ContentScript['run_at']>;
/**
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
* @default false
*/
matchAboutBlank?: PerBrowserOption<
Manifest.ContentScript['match_about_blank']
>;
/**
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
* @default []
*/
excludeMatches?: PerBrowserOption<Manifest.ContentScript['exclude_matches']>;
/**
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
* @default []
*/
includeGlobs?: PerBrowserOption<Manifest.ContentScript['include_globs']>;
/**
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
* @default []
*/
excludeGlobs?: PerBrowserOption<Manifest.ContentScript['exclude_globs']>;
/**
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
* @default false
*/
allFrames?: PerBrowserOption<Manifest.ContentScript['all_frames']>;
/**
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
* @default false
*/
matchOriginAsFallback?: PerBrowserOption<boolean>;
/**
* Customize how imported/generated styles are injected with the content script. Regardless of the
* mode selected, CSS will always be built and included in the output directory.
*
* - `"manifest"` - Include the CSS in the manifest, under the content script's `css` array.
* - `"manual"` - Exclude the CSS from the manifest. You are responsible for manually loading it
* onto the page. Use `browser.runtime.getURL("content-scripts/<name>.css")` to get the file's
* URL
* - `"ui"` - Exclude the CSS from the manifest. CSS will be automatically added to your UI when
* calling `createContentScriptUi`
*
* @default "manifest"
*/
cssInjectionMode?: PerBrowserOption<'manifest' | 'manual' | 'ui'>;
}
export interface BackgroundDefinition extends ExcludableEntrypoint {
type?: PerBrowserOption<'module'>;
persistent?: PerBrowserOption<boolean>;
main(): void;
}
export interface UnlistedScriptDefinition extends ExcludableEntrypoint {
/**
* Main function executed when the unlisted script is ran.
*/
main(): void | Promise<void>;
}
export type PerBrowserOption<T> = T | { [browser: TargetBrowser]: T };
export interface ExcludableEntrypoint {
/**
* List of target browsers to include this entrypoint in. Defaults to being included in all
* builds. Cannot be used with `exclude`. You must choose one of the two options.
*
* @default undefined
*/
include?: TargetBrowser[];
/**
* List of target browsers to exclude this entrypoint from. Cannot be used with `include`. You
* must choose one of the two options.
*
* @default undefined
*/
exclude?: TargetBrowser[];
}
/**
* Manifest customization available in the `wxt.config.ts` file. You cannot configure entrypoints
* here, they are configured inline.
*/
export type UserManifest = Partial<
Omit<
Manifest.WebExtensionManifest,
| 'background'
| 'chrome_url_overrides'
| 'devtools_page'
| 'manifest_version'
| 'options_page'
| 'options_ui'
| 'sandbox'
| 'sidepanel'
| 'sidebar_action'
>
>;
export type UserManifestFn = (
env: ConfigEnv,
) => UserManifest | Promise<UserManifest>;
export interface ConfigEnv {
mode: string;
command: 'build' | 'serve';
/**
* Browser passed in from the CLI
*/
browser: TargetBrowser;
/**
* Manifest version passed in from the CLI
*/
manifestVersion: 2 | 3;
}
/**
* Configure how the browser starts up.
*/
export interface ExtensionRunnerConfig {
/**
* Whether or not to open the browser with the extension installed in dev mode.
*
* @default false
*/
disabled?: boolean;
/**
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#browser-console
*/
openConsole?: boolean;
/**
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#devtools
*/
openDevtools?: boolean;
/**
* List of browser names and the binary that should be used to open the browser.
*
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#chromium-binary
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#firefox
*/
binaries?: Record<string, string>;
/**
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#firefox-profile
*/
firefoxProfile?: string;
/**
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#chromium-profile
*/
chromiumProfile?: string;
/**
* An map of chrome preferences from https://chromium.googlesource.com/chromium/src/+/main/chrome/common/pref_names.h
*
* @example
* // change your downloads directory
* {
* download: {
* default_directory: "/my/custom/dir",
* },
* }
*
* @default
* // Enable dev mode and allow content script sourcemaps
* {
* devtools: {
* synced_preferences_sync_disabled: {
* skipContentScripts: false,
* },
* }
* extensions: {
* ui: {
* developer_mode: true,
* },
* }
* }
*/
chromiumPref?: string;
/**
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#pref
*/
firefoxPrefs?: Record<string, string>;
/**
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#args
*/
firefoxArgs?: string[];
/**
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#args
*/
chromiumArgs?: string[];
/**
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#start-url
*/
startUrls?: string[];
}
export interface WxtBuilder {
/**
* Name of tool used to build. Ex: "Vite" or "Webpack".
*/
name: string;
/**
* Version of tool used to build. Ex: "5.0.2"
*/
version: string;
/**
* Build a single entrypoint group. This is effectively one of the multiple "steps" during the
* build process.
*/
build(group: EntrypointGroup): Promise<BuildStepOutput>;
/**
* Start a dev server at the provided port.
*/
createServer(info: ServerInfo): Promise<WxtBuilderServer>;
}
export interface WxtBuilderServer {
/**
* Start the server.
*/
listen(): Promise<void>;
/**
* Stop the server.
*/
close(): Promise<void>;
/**
* Transform the HTML for dev mode.
*/
transformHtml(
url: string,
html: string,
originalUrl?: string | undefined,
): Promise<string>;
/**
* The web socket server used to communicate with the extension.
*/
ws: {
/**
* Send a message via the server's websocket, with an optional payload.
*
* @example
* ws.send("wxt:reload-extension");
* ws.send("wxt:reload-content-script", { ... });
*/
send(message: string, payload?: any): void;
/**
* Listen for messages over the server's websocket.
*/
on(message: string, cb: (payload: any) => void): void;
};
/**
* Chokidar file watcher instance.
*/
watcher: FSWatcher;
}
export interface ServerInfo {
/**
* Ex: `3000`
*/
port: number;
/**
* Ex: `"localhost"`
*/
hostname: string;
/**
* Ex: `"http://localhost:3000"`
*/
origin: string;
}
export type HookResult = Promise<void> | void;
export interface WxtHooks {
/**
* Called after WXT initialization, when the WXT instance is ready to work.
* @param wxt The configured WXT object
* @returns Promise
*/
ready: (wxt: Wxt) => HookResult;
/**
* Called before the build is started in both dev mode and build mode.
*
* @param wxt The configured WXT object
*/
'build:before': (wxt: Wxt) => HookResult;
/**
* Called once the build process has finished.
* @param wxt The configured WXT object
* @param output The results of the build
*/
'build:done': (wxt: Wxt, output: Readonly<BuildOutput>) => HookResult;
/**
* Called once the manifest has been generated. Used to transform the manifest by reference before
* it is written to the output directory.
* @param wxt The configured WXT object
* @param manifest The manifest that was generated
*/
'build:manifestGenerated': (
wxt: Wxt,
manifest: Manifest.WebExtensionManifest,
) => HookResult;
/**
* Called once all entrypoints have been loaded from the `entrypointsDir`.
* @param wxt The configured WXT object
* @param entrypoints The list of entrypoints to be built
*/
'entrypoints:resolved': (wxt: Wxt, entrypoints: Entrypoint[]) => HookResult;
/**
* Called once all entrypoints have been grouped into their build groups.
* @param wxt The configured WXT object
* @param entrypoints The list of groups to build in each build step
*/
'entrypoints:grouped': (wxt: Wxt, groups: EntrypointGroup[]) => HookResult;
}
export interface Wxt {
config: ResolvedConfig;
hooks: Hookable<WxtHooks>;
/**
* Alias for config.logger
*/
logger: Logger;
/**
* Reload config file and update the `config` field with the result.
*/
reloadConfig: () => Promise<void>;
}
export interface ResolvedConfig {
root: string;
srcDir: string;
publicDir: string;
wxtDir: string;
typesDir: string;
entrypointsDir: string;
filterEntrypoints?: Set<string>;
outBaseDir: string;
outDir: string;
debug: boolean;
mode: string;
command: 'build' | 'serve';
browser: TargetBrowser;
manifestVersion: TargetManifestVersion;
env: ConfigEnv;
logger: Logger;
imports: false | Partial<UnimportOptions>;
manifest: UserManifest;
fsCache: FsCache;
server?: WxtDevServer;
runnerConfig: C12ResolvedConfig<ExtensionRunnerConfig>;
zip: {
name?: string;
artifactTemplate: string;
sourcesTemplate: string;
includeSources: string[];
excludeSources: string[];
sourcesRoot: string;
};
transformManifest: (manifest: Manifest.WebExtensionManifest) => void;
analysis: {
enabled: boolean;
template: NonNullable<PluginVisualizerOptions['template']>;
/** Absolute file path */
outputFile: string;
};
userConfigMetadata: Omit<C12ResolvedConfig<UserConfig>, 'config'>;
/**
* Import aliases to absolute paths.
*/
alias: Record<string, string>;
experimental: {
includeBrowserPolyfill: boolean;
};
builder: WxtBuilder;
dev: {
reloadCommand: string | false;
};
hooks: Partial<WxtHooks>;
}
export interface FsCache {
set(key: string, value: string): Promise<void>;
get(key: string): Promise<string | undefined>;
}
export interface ExtensionRunner {
openBrowser(): Promise<void>;
closeBrowser(): Promise<void>;
}
export type VirtualEntrypointType =
| 'content-script-main-world'
| 'content-script-isolated-world'
| 'background'
| 'unlisted-script';
-80
View File
@@ -1,80 +0,0 @@
import {
WxtDevServer,
Logger,
TargetBrowser,
TargetManifestVersion,
UserManifest,
ExtensionRunnerConfig,
ConfigEnv,
UserConfig,
WxtBuilder,
} from './external';
import { UnimportOptions } from 'unimport';
import { ResolvedConfig } from 'c12';
import type { Manifest } from '~/browser';
import type { PluginVisualizerOptions } from 'rollup-plugin-visualizer';
export interface InternalConfig {
root: string;
srcDir: string;
publicDir: string;
wxtDir: string;
typesDir: string;
entrypointsDir: string;
filterEntrypoints?: Set<string>;
outBaseDir: string;
outDir: string;
debug: boolean;
mode: string;
command: 'build' | 'serve';
browser: TargetBrowser;
manifestVersion: TargetManifestVersion;
env: ConfigEnv;
logger: Logger;
imports: false | Partial<UnimportOptions>;
manifest: UserManifest;
fsCache: FsCache;
server?: WxtDevServer;
runnerConfig: ResolvedConfig<ExtensionRunnerConfig>;
zip: {
name?: string;
artifactTemplate: string;
sourcesTemplate: string;
includeSources: string[];
excludeSources: string[];
sourcesRoot: string;
};
transformManifest: (manifest: Manifest.WebExtensionManifest) => void;
analysis: {
enabled: boolean;
template: NonNullable<PluginVisualizerOptions['template']>;
};
userConfigMetadata: Omit<ResolvedConfig<UserConfig>, 'config'>;
/**
* Import aliases to absolute paths.
*/
alias: Record<string, string>;
experimental: {
includeBrowserPolyfill: boolean;
};
builder: WxtBuilder;
dev: {
reloadCommand: string | false;
};
}
export interface FsCache {
set(key: string, value: string): Promise<void>;
get(key: string): Promise<string | undefined>;
}
export interface ExtensionRunner {
openBrowser(config: InternalConfig): Promise<void>;
closeBrowser(): Promise<void>;
}
export type VirtualEntrypointType =
| 'content-script-main-world'
| 'content-script-isolated-world'
| 'background'
| 'unlisted-script';
+1 -1
View File
@@ -23,6 +23,6 @@
"@types/react-dom": "^18.2.18",
"@vitejs/plugin-react": "^4.2.1",
"typescript": "^5.3.3",
"wxt": "^0.15.0"
"wxt": "^0.16.0"
}
}
+1 -1
View File
@@ -20,6 +20,6 @@
"devDependencies": {
"typescript": "^5.3.3",
"vite-plugin-solid": "^2.8.0",
"wxt": "^0.15.0"
"wxt": "^0.16.0"
}
}
+1 -1
View File
@@ -20,6 +20,6 @@
"svelte-check": "^3.6.2",
"tslib": "^2.6.2",
"typescript": "^5.3.3",
"wxt": "^0.15.0"
"wxt": "^0.16.0"
}
}
+1 -1
View File
@@ -16,6 +16,6 @@
},
"devDependencies": {
"typescript": "^5.3.3",
"wxt": "^0.15.0"
"wxt": "^0.16.0"
}
}
+1 -1
View File
@@ -21,6 +21,6 @@
"@vitejs/plugin-vue": "^5.0.1",
"typescript": "^5.3.3",
"vue-tsc": "^1.8.27",
"wxt": "^0.15.0"
"wxt": "^0.16.0"
}
}