Compare commits

..

39 Commits

Author SHA1 Message Date
GitHub Actions 4e2dd5d618 chore(release): v0.11.0 2023-12-06 17:24:11 +00:00
Aaron 1a39a0d153 feat!: Vite 5 support (#261) 2023-12-06 11:18:52 -06:00
Aaron f922648dd7 chore: Abstract vite from WXT's core logic (#242) 2023-12-05 16:55:22 -06:00
Ítalo Brasil e6142e3608 docs: Adds tl;dv to homepage (#260) 2023-12-05 12:30:57 -06:00
Aaron ed73451fd0 chore: Speed up CI using pnpm instead of npm (#259) 2023-12-04 14:14:44 -06:00
GitHub Actions 8a25a2f60f chore(release): v0.10.4 2023-12-04 15:54:42 +00:00
Aaron 397e9a8b45 feat: Add config to customize outDir (#258) 2023-12-04 09:50:24 -06:00
dependabot[bot] 7cef7680de chore(deps): bump esbuild from 0.19.5 to 0.19.8 (#253)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-01 15:23:31 -06:00
dependabot[bot] 0c57375758 chore(deps-dev): bump tsx from 3.14.0 to 4.6.1 (#252)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-01 15:09:19 -06:00
dependabot[bot] 2c6af84165 chore(deps): bump rollup-plugin-visualizer from 5.9.2 to 5.9.3 (#256)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-01 15:08:06 -06:00
dependabot[bot] 58e1371701 chore(deps-dev): bump @types/lodash.merge from 4.6.8 to 4.6.9 (#255)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-01 15:06:58 -06:00
dependabot[bot] 0d8e7463b8 chore(deps-dev): bump prettier from 3.0.3 to 3.1.0 (#254)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-01 15:06:24 -06:00
Aaron 77eeacaf0f docs: Update sidepanel availability (#250) 2023-11-29 17:12:37 -06:00
冯不游 e80c134150 docs: Add Doozy to homepage (#249) 2023-11-29 07:21:46 -06:00
GitHub Actions 03c8ab4d6f chore(release): v0.10.3 2023-11-28 18:18:59 +00:00
Aaron df13b9705c fix(auto-imports): Don't add imports to node_module dependencies (#247) 2023-11-28 12:15:05 -06:00
Aaron 762ba0080d chore(deps): Upgrade to typescript 5.3 (#245) 2023-11-27 10:03:03 -06:00
Aaron Klinker 317b1b6dcc docs: Fix typo 2023-11-27 09:57:09 -06:00
Aaron 93175a6477 chore: Use defu for merging some config objects (#243) 2023-11-26 12:23:10 -06:00
Aaron 60d6707b11 chore: Use normalize-path instead of vite.normalizePath (#244) 2023-11-26 11:36:42 -06:00
Aaron Klinker 97cbda3dab ci: only print response headers from docs webhook 2023-11-25 14:57:04 -06:00
Aaron Klinker 1611c1dba6 ci: Publish docs on push to main 2023-11-25 14:51:30 -06:00
Aaron Klinker b59252284a Print verbose webhook 2023-11-25 14:48:05 -06:00
Aaron Klinker 742b99657a chore: Trigger docs upgrade via webhook 2023-11-25 14:45:41 -06:00
Aaron Klinker e2997a43e0 Fix docker push command 2023-11-24 14:18:27 -06:00
Aaron Klinker 21dead60fd Self-host docs 2023-11-24 14:16:18 -06:00
GitHub Actions 3d1bc0a12c chore(release): v0.10.2 2023-11-20 19:52:55 +00:00
Aaron Klinker 4b24bee1fb Update demo app logs 2023-11-20 13:47:31 -06:00
Aaron Klinker 82ed821eb5 fix: Apply mode option to build steps correctly 2023-11-20 13:47:18 -06:00
Aaron 60625280c2 chore: Upgrade templates to v0.10 (#239) 2023-11-16 16:50:30 -06:00
GitHub Actions 083792b5f0 chore(release): v0.10.1 2023-11-16 17:46:36 +00:00
Aaron Klinker 3da3e07bda fix: Remove WXT global to remove unused modules from production builds 2023-11-16 11:35:20 -06:00
Aaron Klinker 64d61eb3f1 Update changelog 2023-11-14 23:26:27 -06:00
GitHub Actions aa6009e476 chore(release): v0.10.0 2023-11-15 05:21:53 +00:00
Aaron d2cb8f9416 feat: Add wxt/storage API (#234) 2023-11-14 23:07:45 -06:00
Nenad Novaković 046a4809d3 feat!: Allow plural directory and only png's for manifest icons (#237)
BREAKING CHANGE: No longer discover icons with extensions other than `.png`. If you previously used `.jpg`, `.jpeg`, `.bmp`, or `.svg`, you'll need to convert your icons to `.png` files or manually add them to the manifest inside your `wxt.config.ts` file.
2023-11-14 23:07:09 -06:00
Aaron Klinker 207b750d4e docs: Update main README links 2023-11-12 10:37:56 -06:00
Aaron 077fa9939e feat: List bun as an experimental option in wxt init (#233) 2023-11-11 15:45:29 -06:00
Aaron 44d715a29e fix: Don't use bun to load entrypoint config (#232) 2023-11-11 14:46:13 -06:00
77 changed files with 2780 additions and 1103 deletions
+27
View File
@@ -0,0 +1,27 @@
name: Publish Docs
on:
push:
branches:
- main
workflow_dispatch:
inputs:
tag:
description: Docker Image Tag
required: true
default: latest
jobs:
publish:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- uses: docker/login-action@v3
with:
registry: https://${{ secrets.DOCKER_REGISTRY_HOSTNAME }}
username: ${{ secrets.DOCKER_REGISTRY_USERNAME }}
password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
- run: pnpm docs:build
- run: docker build docs/.vitepress -t ${{ secrets.DOCKER_REGISTRY_HOSTNAME }}/wxt/docs:${{ github.event.inputs.tag || 'latest' }}
- run: docker push ${{ secrets.DOCKER_REGISTRY_HOSTNAME }}/wxt/docs:${{ github.event.inputs.tag || 'latest' }}
- run: curl -X POST -i ${{ secrets.UPDATE_DOCS_WEBHOOK }}
+4 -4
View File
@@ -54,7 +54,7 @@ jobs:
matrix:
template:
- react
- solid
# - solid
- svelte
- vanilla
- vue
@@ -66,11 +66,11 @@ jobs:
working-directory: templates/${{ matrix.template }}
- run: npm i -D ../../wxt-*.tgz
working-directory: templates/${{ matrix.template }}
- run: npm run compile
- run: pnpm compile
if: matrix.template != 'svelte'
working-directory: templates/${{ matrix.template }}
- run: npm run check
- run: pnpm check
if: matrix.template == 'svelte'
working-directory: templates/${{ matrix.template }}
- run: npm run build
- run: pnpm build
working-directory: templates/${{ matrix.template }}
+117
View File
@@ -1,5 +1,122 @@
# Changelog
## v0.11.0
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.10.4...v0.11.0)
### 🚀 Enhancements
- ⚠️ Vite 5 support ([#261](https://github.com/wxt-dev/wxt/pull/261))
### 📖 Documentation
- Adds tl;dv to homepage ([#260](https://github.com/wxt-dev/wxt/pull/260))
### 🏡 Chore
- Speed up CI using `pnpm` instead of `npm` ([#259](https://github.com/wxt-dev/wxt/pull/259))
- Abstract vite from WXT's core logic ([#242](https://github.com/wxt-dev/wxt/pull/242))
#### ⚠️ Breaking Changes
- ⚠️ Vite 5 support ([#261](https://github.com/wxt-dev/wxt/pull/261))
### ❤️ Contributors
- Ítalo Brasil ([@italodeverdade](http://github.com/italodeverdade))
## v0.10.4
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.10.3...v0.10.4)
### 🚀 Enhancements
- Add config to customize `outDir` ([#258](https://github.com/wxt-dev/wxt/pull/258))
### 📖 Documentation
- Add Doozy to homepage ([#249](https://github.com/wxt-dev/wxt/pull/249))
- Update sidepanel availability ([#250](https://github.com/wxt-dev/wxt/pull/250))
### 🏡 Chore
- **deps-dev:** Bump prettier from 3.0.3 to 3.1.0 ([#254](https://github.com/wxt-dev/wxt/pull/254))
- **deps-dev:** Bump @types/lodash.merge from 4.6.8 to 4.6.9 ([#255](https://github.com/wxt-dev/wxt/pull/255))
- **deps-dev:** Bump tsx from 3.14.0 to 4.6.1 ([#252](https://github.com/wxt-dev/wxt/pull/252))
### ❤️ Contributors
- 冯不游
## v0.10.3
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.10.2...v0.10.3)
### 🩹 Fixes
- **auto-imports:** Don't add imports to `node_module` dependencies ([#247](https://github.com/wxt-dev/wxt/pull/247))
### 📖 Documentation
- Fix typo ([317b1b6](https://github.com/wxt-dev/wxt/commit/317b1b6))
### 🏡 Chore
- Trigger docs upgrade via webhook ([742b996](https://github.com/wxt-dev/wxt/commit/742b996))
- Use `normalize-path` instead of `vite.normalizePath` ([#244](https://github.com/wxt-dev/wxt/pull/244))
- Use `defu` for merging some config objects ([#243](https://github.com/wxt-dev/wxt/pull/243))
### 🤖 CI
- Publish docs on push to main ([1611c1d](https://github.com/wxt-dev/wxt/commit/1611c1d))
- Only print response headers from docs webhook ([97cbda3](https://github.com/wxt-dev/wxt/commit/97cbda3))
## v0.10.2
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.10.1...v0.10.2)
### 🩹 Fixes
- Apply `mode` option to build steps correctly ([82ed821](https://github.com/wxt-dev/wxt/commit/82ed821))
### 🏡 Chore
- Upgrade templates to v0.10 ([#239](https://github.com/wxt-dev/wxt/pull/239))
## v0.10.1
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.10.0...v0.10.1)
### 🩹 Fixes
- Remove WXT global to remove unused modules from production builds ([3da3e07](https://github.com/wxt-dev/wxt/commit/3da3e07))
## v0.10.0
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.9.2...v0.10.0)
### 🚀 Enhancements
- List `bun` as an experimental option in `wxt init` ([#233](https://github.com/wxt-dev/wxt/pull/233))
- ⚠️ Allow plural directory and only png's for manifest icons ([#237](https://github.com/wxt-dev/wxt/pull/237))
- Add `wxt/storage` API ([#234](https://github.com/wxt-dev/wxt/pull/234))
### 🩹 Fixes
- Don't use `bun` to load entrypoint config ([#232](https://github.com/wxt-dev/wxt/pull/232))
### 📖 Documentation
- Update main README links ([207b750](https://github.com/wxt-dev/wxt/commit/207b750))
#### ⚠️ Breaking Changes
- ⚠️ No longer discover icons with extensions other than `.png`. If you previously used `.jpg`, `.jpeg`, `.bmp`, or `.svg`, you'll need to convert your icons to `.png` files or manually add them to the manifest inside your `wxt.config.ts` file ([#237](https://github.com/wxt-dev/wxt/pull/237))
### ❤️ Contributors
- Nenad Novaković ([@dvlden](https://github.com/dvlden))
## v0.9.2
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.9.1...v0.9.2)
+1 -1
View File
@@ -86,7 +86,7 @@ Note that templates are hardcoded to a specific version of `wxt` from NPM, they
```diff
"devDependencies": {
"typescript": "^5.1.6",
"typescript": "^5.3.2",
"vite-plugin-solid": "^2.7.0",
- "wxt": "^0.8.0"
+ "wxt": "../.."
+2 -2
View File
@@ -27,9 +27,9 @@
&bull;
<a href="https://wxt.dev/guide/installation.html" target="_blank">Installation</a>
&bull;
<a href="https://wxt.dev/entrypoints/background.html" target="_blank">Entrypoints</a>
&bull;
<a href="https://wxt.dev/api/config.html" target="_blank">Configuration</a>
&bull;
<a href="https://wxt.dev/examples.html" target="_blank">Examples</a>
</p>
![Example CLI Output](./docs/assets/cli-output.png)
+7 -1
View File
@@ -23,5 +23,11 @@ export default defineBackground(() => {
browser.i18n.getMessage('bye', ['Aaron']);
browser.i18n.getMessage('@@extension_id');
console.log('WXT MODE:', import.meta.env.MODE);
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());
});
+5 -1
View File
@@ -6,7 +6,11 @@ export default defineContentScript({
console.log(browser.runtime.id);
logId();
console.log('WXT MODE:', import.meta.env.MODE);
console.log('WXT MODE:', {
MODE: import.meta.env.MODE,
DEV: import.meta.env.DEV,
PROD: import.meta.env.PROD,
});
const n = (Math.random() * 100).toFixed(1);
ctx.setInterval(() => {
+5 -1
View File
@@ -4,4 +4,8 @@ console.log(browser.runtime.id);
logId();
console.log(2);
console.log('WXT MODE:', import.meta.env.MODE);
console.log('WXT MODE:', {
MODE: import.meta.env.MODE,
DEV: import.meta.env.DEV,
PROD: import.meta.env.PROD,
});
+11
View File
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sidebar</title>
</head>
<body>
<p>Example</p>
</body>
</html>
+1
View File
@@ -3,6 +3,7 @@ import { defineConfig } from 'wxt';
export default defineConfig({
srcDir: 'src',
manifest: {
permissions: ['storage'],
default_locale: 'en',
web_accessible_resources: [
{
+2
View File
@@ -0,0 +1,2 @@
FROM lipanski/docker-static-website:latest
COPY dist .
@@ -10,6 +10,8 @@ const chromeExtensionIds = [
'mgmdkjcljneegjfajchedjpdhbadklcf', // Anime Skip Player
'bfbnagnphiehemkdgmmficmjfddgfhpl', // UltraWideo
'elfaihghhjjoknimpccccmkioofjjfkf', // StayFree - Website Blocker & Web Analytics
'okifoaikfmpfcamplcfjkpdnhfodpkil', // Doozy: Ai Made Easy
'lknmjhcajhfbbglglccadlfdjbaiifig' // tl;dv - Record, Transcribe & ChatGPT for Google Meet
];
const { data } = useListExtensionDetails(chromeExtensionIds);
+1
View File
@@ -90,6 +90,7 @@ export default defineConfig({
{ text: 'Auto-imports', link: '/guide/auto-imports.md' },
{ text: 'Manifest.json', link: '/guide/manifest.md' },
{ text: 'Extension APIs', link: '/guide/extension-apis.md' },
{ text: 'Storage', link: '/guide/storage.md' },
{ text: 'Remote Code', link: '/guide/remote-code.md' },
{ text: 'Development', link: '/guide/development.md' },
{ text: 'Testing', link: '/guide/testing.md' },
+3 -3
View File
@@ -1,9 +1,9 @@
# Side Panel
[Chrome Docs](https://developer.chrome.com/docs/extensions/reference/sidePanel/)
[Chrome Docs](https://developer.chrome.com/docs/extensions/reference/sidePanel/) &bull; [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/user_interface/Sidebars)
:::tip Chromium Only
Firefox does not support sidepanel pages.
:::warning
Chrome added support for sidepanels in Manifest V3, they are not available in Manfiest V2.
:::
## Filenames
+1 -1
View File
@@ -27,7 +27,7 @@ Lets compare the features of WXT vs [Plasmo](https://docs.plasmo.com/framework),
| Reload Content Scripts on Change | ✅ | 🟡 Reloads entire extension |
| Reload Background on Change | 🟡 Reloads entire extension | 🟡 Reloads entire extension |
| <strong style="opacity: 50%">Built-in Utils</strong> | | |
| Storage | 🟡 Coming soon | ✅ |
| Storage | | ✅ |
| Messaging | 🟡 Coming soon | ✅ |
| Content Script UI | ✅ | ✅ |
+1 -1
View File
@@ -2,7 +2,7 @@
The manifest.json is generated at build-time based on files in your `entrypoints` directory and your `wxt.config.ts`.
## Confiuration
## Configuration
While entrypoints are generated and added to the manifest at build-time, you can customize or add to your `manifest.json` in the config file.
+36
View File
@@ -0,0 +1,36 @@
# Storage API
WXT's storage API is powered by `unstorage`. See [their docs](https://unstorage.unjs.io/usage#usage-1) for more details.
## Overview
:::code-group
```ts [native]
const { installDate } = await browser.storage.local.get('installDate');
await browser.storage.local.set({ key: 'value' });
```
```ts [wxt/browser]
const installDate = await storage.get('local:installDate');
await storage.setItem('key', 'value');
```
:::
Use the `"local:"`, `"session:"`, `"sync:"`, and `"managed:"` prefixes to specify which storage area to use.
## Customization
WXT also provides a driver for `unstorage`. To customize the `storage` object's setup, like removing the prefixes and using a single storage area, you can create your own storage:
```ts
// storage.ts
export default createStorage({
driver: webExtensionDriver({ storageArea: 'local' }),
});
```
:::note
`wxt/browser` re-exports all of `unstorage`, which is where `createStorage` comes from.
:::
+1
View File
@@ -4,6 +4,7 @@
"../src/client",
"../src/browser.ts",
"../src/sandbox",
"../src/storage.ts",
"../src/testing"
],
"plugin": ["typedoc-plugin-markdown", "typedoc-vitepress-theme"],
+25 -14
View File
@@ -18,13 +18,24 @@ describe('Auto Imports', () => {
declare global {
const ContentScriptContext: typeof import('wxt/client')['ContentScriptContext']
const browser: typeof import('wxt/browser')['browser']
const builtinDrivers: typeof import('wxt/storage')['builtinDrivers']
const createContentScriptIframe: typeof import('wxt/client')['createContentScriptIframe']
const createContentScriptUi: typeof import('wxt/client')['createContentScriptUi']
const createStorage: typeof import('wxt/storage')['createStorage']
const defineBackground: typeof import('wxt/client')['defineBackground']
const defineConfig: typeof import('wxt')['defineConfig']
const defineContentScript: typeof import('wxt/client')['defineContentScript']
const defineDriver: typeof import('wxt/storage')['defineDriver']
const defineUnlistedScript: typeof import('wxt/sandbox')['defineUnlistedScript']
const fakeBrowser: typeof import('wxt/testing')['fakeBrowser']
const joinKeys: typeof import('wxt/storage')['joinKeys']
const normalizeBaseKey: typeof import('wxt/storage')['normalizeBaseKey']
const normalizeKey: typeof import('wxt/storage')['normalizeKey']
const prefixStorage: typeof import('wxt/storage')['prefixStorage']
const restoreSnapshot: typeof import('wxt/storage')['restoreSnapshot']
const snapshot: typeof import('wxt/storage')['snapshot']
const storage: typeof import('wxt/storage')['storage']
const webExtensionDriver: typeof import('wxt/storage')['webExtensionDriver']
}
"
`);
@@ -38,16 +49,16 @@ describe('Auto Imports', () => {
expect(await project.serializeFile('.wxt/wxt.d.ts'))
.toMatchInlineSnapshot(`
".wxt/wxt.d.ts
----------------------------------------
// Generated by wxt
/// <reference types=\\"vite/client\\" />
/// <reference types=\\"./types/imports.d.ts\\" />
/// <reference types=\\"./types/paths.d.ts\\" />
/// <reference types=\\"./types/i18n.d.ts\\" />
/// <reference types=\\"./types/globals.d.ts\\" />
"
`);
".wxt/wxt.d.ts
----------------------------------------
// Generated by wxt
/// <reference types="vite/client" />
/// <reference types="./types/imports.d.ts" />
/// <reference types="./types/paths.d.ts" />
/// <reference types="./types/i18n.d.ts" />
/// <reference types="./types/globals.d.ts" />
"
`);
});
});
@@ -80,10 +91,10 @@ describe('Auto Imports', () => {
".wxt/wxt.d.ts
----------------------------------------
// Generated by wxt
/// <reference types=\\"vite/client\\" />
/// <reference types=\\"./types/paths.d.ts\\" />
/// <reference types=\\"./types/i18n.d.ts\\" />
/// <reference types=\\"./types/globals.d.ts\\" />
/// <reference types="vite/client" />
/// <reference types="./types/paths.d.ts" />
/// <reference types="./types/i18n.d.ts" />
/// <reference types="./types/globals.d.ts" />
"
`,
);
+24 -24
View File
@@ -175,22 +175,22 @@ describe('Manifest Content', () => {
it('should auto-discover icons with the correct name', async () => {
const project = new TestProject();
project.addFile('public/icon-16.png');
project.addFile('public/icon/32.jpeg');
project.addFile('public/icon@48w.jpg');
project.addFile('public/icon-64x64.gif');
project.addFile('public/icon@96.bmp');
project.addFile('public/icon/128x128.ico');
project.addFile('public/icon/32.png');
project.addFile('public/icon@48w.png');
project.addFile('public/icon-64x64.png');
project.addFile('public/icon@96.png');
project.addFile('public/icons/128x128.png');
await project.build();
const manifest = await project.getOutputManifest();
expect(manifest.icons).toEqual({
'16': 'icon-16.png',
'32': 'icon/32.jpeg',
'48': 'icon@48w.jpg',
'64': 'icon-64x64.gif',
'96': 'icon@96.bmp',
'128': 'icon/128x128.ico',
'32': 'icon/32.png',
'48': 'icon@48w.png',
'64': 'icon-64x64.png',
'96': 'icon@96.png',
'128': 'icons/128x128.png',
});
});
@@ -358,7 +358,7 @@ describe('Manifest Content', () => {
project.addFile(
'entrypoints/content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["https://*.google.com/*"],
@@ -371,7 +371,7 @@ describe('Manifest Content', () => {
.toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"css\\":[\\"content-scripts/content.css\\"],\\"js\\":[\\"content-scripts/content.js\\"]}]}"
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","content_scripts":[{"matches":["https://*.google.com/*"],"css":["content-scripts/content.css"],"js":["content-scripts/content.js"]}]}"
`);
});
@@ -384,7 +384,7 @@ describe('Manifest Content', () => {
project.addFile(
'entrypoints/content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["https://*.google.com/*"],
cssInjectionMode: "manifest",
@@ -398,7 +398,7 @@ describe('Manifest Content', () => {
.toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"css\\":[\\"content-scripts/content.css\\"],\\"js\\":[\\"content-scripts/content.js\\"]}]}"
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","content_scripts":[{"matches":["https://*.google.com/*"],"css":["content-scripts/content.css"],"js":["content-scripts/content.js"]}]}"
`);
});
@@ -411,7 +411,7 @@ describe('Manifest Content', () => {
project.addFile(
'entrypoints/content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["https://*.google.com/*"],
cssInjectionMode: "manual",
@@ -425,7 +425,7 @@ describe('Manifest Content', () => {
.toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"js\\":[\\"content-scripts/content.js\\"]}]}"
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","content_scripts":[{"matches":["https://*.google.com/*"],"js":["content-scripts/content.js"]}]}"
`);
});
@@ -438,7 +438,7 @@ describe('Manifest Content', () => {
project.addFile(
'entrypoints/content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["https://*.google.com/*"],
cssInjectionMode: "ui",
@@ -454,7 +454,7 @@ describe('Manifest Content', () => {
.toMatchInlineSnapshot(`
".output/chrome-mv2/manifest.json
----------------------------------------
{\\"manifest_version\\":2,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"js\\":[\\"content-scripts/content.js\\"]}],\\"web_accessible_resources\\":[\\"content-scripts/content.css\\"]}"
{"manifest_version":2,"name":"E2E Extension","description":"Example description","version":"0.0.0","content_scripts":[{"matches":["https://*.google.com/*"],"js":["content-scripts/content.js"]}],"web_accessible_resources":["content-scripts/content.css"]}"
`);
});
@@ -467,7 +467,7 @@ describe('Manifest Content', () => {
project.addFile(
'entrypoints/content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["https://*.google.com/*"],
cssInjectionMode: "ui",
@@ -483,7 +483,7 @@ describe('Manifest Content', () => {
.toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"js\\":[\\"content-scripts/content.js\\"]}],\\"web_accessible_resources\\":[{\\"resources\\":[\\"content-scripts/content.css\\"],\\"matches\\":[\\"https://*.google.com/*\\"]}]}"
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","content_scripts":[{"matches":["https://*.google.com/*"],"js":["content-scripts/content.js"]}],"web_accessible_resources":[{"resources":["content-scripts/content.css"],"matches":["https://*.google.com/*"]}]}"
`);
});
});
@@ -497,7 +497,7 @@ describe('Manifest Content', () => {
project.addFile(
'entrypoints/content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["https://*.google.com/*"],
cssInjectionMode: "ui",
@@ -518,7 +518,7 @@ describe('Manifest Content', () => {
.toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"web_accessible_resources\\":[{\\"resources\\":[\\"one.png\\"],\\"matches\\":[\\"https://one.com/*\\"]},{\\"resources\\":[\\"content-scripts/content.css\\"],\\"matches\\":[\\"https://*.google.com/*\\"]}],\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"js\\":[\\"content-scripts/content.js\\"]}]}"
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","web_accessible_resources":[{"resources":["one.png"],"matches":["https://one.com/*"]},{"resources":["content-scripts/content.css"],"matches":["https://*.google.com/*"]}],"content_scripts":[{"matches":["https://*.google.com/*"],"js":["content-scripts/content.js"]}]}"
`);
});
@@ -527,7 +527,7 @@ describe('Manifest Content', () => {
project.addFile(
'wxt.config.ts',
`import { defineConfig } from 'wxt';
export default defineConfig({
transformManifest(manifest) {
manifest.author = "Custom Author"
@@ -543,7 +543,7 @@ describe('Manifest Content', () => {
expect(output).toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"author\\":\\"Custom Author\\"}"
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","author":"Custom Author"}"
`);
});
+17 -4
View File
@@ -13,7 +13,7 @@ describe('Output Directory Structure', () => {
expect(await project.serializeOutput()).toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\"}"
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0"}"
`);
});
@@ -69,7 +69,7 @@ describe('Output Directory Structure', () => {
================================================================================
.output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"content_scripts\\":[{\\"matches\\":[\\"*://*/*\\"],\\"css\\":[\\"content-scripts/one.css\\",\\"content-scripts/two.css\\"],\\"js\\":[\\"content-scripts/one.js\\",\\"content-scripts/two.js\\"]}]}"
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","content_scripts":[{"matches":["*://*/*"],"css":["content-scripts/one.css","content-scripts/two.css"],"js":["content-scripts/one.js","content-scripts/two.js"]}]}"
`);
});
@@ -96,7 +96,7 @@ describe('Output Directory Structure', () => {
================================================================================
.output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"content_scripts\\":[{\\"matches\\":[\\"*://*/*\\"],\\"js\\":[\\"content-scripts/overlay-one.js\\"]}]}"
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","content_scripts":[{"matches":["*://*/*"],"js":["content-scripts/overlay-one.js"]}]}"
`);
});
@@ -197,7 +197,7 @@ describe('Output Directory Structure', () => {
.toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"background\\":{\\"service_worker\\":\\"background.js\\"},\\"content_scripts\\":[{\\"matches\\":[\\"*://*.google.com/*\\"],\\"js\\":[\\"content-scripts/content.js\\"]},{\\"matches\\":[\\"*://*.duckduckgo.com/*\\"],\\"js\\":[\\"content-scripts/named.js\\"]}]}"
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","background":{"service_worker":"background.js"},"content_scripts":[{"matches":["*://*.google.com/*"],"js":["content-scripts/content.js"]},{"matches":["*://*.duckduckgo.com/*"],"js":["content-scripts/named.js"]}]}"
`);
expect(await project.fileExists('.output/chrome-mv3/background.js'));
expect(
@@ -208,4 +208,17 @@ describe('Output Directory Structure', () => {
);
expect(await project.fileExists('.output/chrome-mv3/unlisted.js'));
});
it("should output to a custom directory when overriding 'outDir'", async () => {
const project = new TestProject();
project.setConfigFileConfig({
outDir: 'dist',
});
await project.build();
expect(await project.fileExists('dist/chrome-mv3/manifest.json')).toBe(
true,
);
});
});
+1 -1
View File
@@ -37,7 +37,7 @@ describe('React', () => {
.toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"content_scripts\\":[{\\"matches\\":\\"<all_urls>\\",\\"js\\":[\\"content-scripts/demo.js\\"]}]}"
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","content_scripts":[{"matches":"<all_urls>","js":["content-scripts/demo.js"]}]}"
`);
});
});
+106 -106
View File
@@ -21,7 +21,7 @@ describe('TypeScript Project', () => {
const __IS_SAFARI__: boolean;
const __IS_EDGE__: boolean;
const __IS_OPERA__: boolean;
const __COMMAND__: \\"build\\" | \\"serve\\";
const __COMMAND__: "build" | "serve";
const __ENTRYPOINT__: string;
}
"
@@ -41,13 +41,13 @@ describe('TypeScript Project', () => {
".wxt/types/paths.d.ts
----------------------------------------
// Generated by wxt
import \\"wxt/browser\\";
import "wxt/browser";
declare module \\"wxt/browser\\" {
declare module "wxt/browser" {
export type PublicPath =
| \\"/options.html\\"
| \\"/popup.html\\"
| \\"/sandbox.html\\"
| "/options.html"
| "/popup.html"
| "/sandbox.html"
export interface WxtRuntime extends Runtime.Static {
getURL(path: PublicPath): string;
}
@@ -103,9 +103,9 @@ describe('TypeScript Project', () => {
".wxt/types/i18n.d.ts
----------------------------------------
// Generated by wxt
import \\"wxt/browser\\";
import "wxt/browser";
declare module \\"wxt/browser\\" {
declare module "wxt/browser" {
/**
* See https://developer.chrome.com/docs/extensions/reference/i18n/#method-getMessage
*/
@@ -121,90 +121,90 @@ describe('TypeScript Project', () => {
* The extension or app ID; you might use this string to construct URLs for resources inside the extension. Even unlocalized extensions can use this message.
Note: You can't use this message in a manifest file.
*
* \\"<browser.runtime.id>\\"
* "<browser.runtime.id>"
*/
getMessage(
messageName: \\"@@extension_id\\",
messageName: "@@extension_id",
substitutions?: string | string[],
options?: GetMessageOptions,
): string;
/**
*
*
* \\"<browser.i18n.getUiLocale()>\\"
* "<browser.i18n.getUiLocale()>"
*/
getMessage(
messageName: \\"@@ui_locale\\",
messageName: "@@ui_locale",
substitutions?: string | string[],
options?: GetMessageOptions,
): string;
/**
* The text direction for the current locale, either \\"ltr\\" for left-to-right languages such as English or \\"rtl\\" for right-to-left languages such as Japanese.
* The text direction for the current locale, either "ltr" for left-to-right languages such as English or "rtl" for right-to-left languages such as Japanese.
*
* \\"<ltr|rtl>\\"
* "<ltr|rtl>"
*/
getMessage(
messageName: \\"@@bidi_dir\\",
messageName: "@@bidi_dir",
substitutions?: string | string[],
options?: GetMessageOptions,
): string;
/**
* If the @@bidi_dir is \\"ltr\\", then this is \\"rtl\\"; otherwise, it's \\"ltr\\".
* If the @@bidi_dir is "ltr", then this is "rtl"; otherwise, it's "ltr".
*
* \\"<rtl|ltr>\\"
* "<rtl|ltr>"
*/
getMessage(
messageName: \\"@@bidi_reversed_dir\\",
messageName: "@@bidi_reversed_dir",
substitutions?: string | string[],
options?: GetMessageOptions,
): string;
/**
* If the @@bidi_dir is \\"ltr\\", then this is \\"left\\"; otherwise, it's \\"right\\".
* If the @@bidi_dir is "ltr", then this is "left"; otherwise, it's "right".
*
* \\"<left|right>\\"
* "<left|right>"
*/
getMessage(
messageName: \\"@@bidi_start_edge\\",
messageName: "@@bidi_start_edge",
substitutions?: string | string[],
options?: GetMessageOptions,
): string;
/**
* If the @@bidi_dir is \\"ltr\\", then this is \\"right\\"; otherwise, it's \\"left\\".
* If the @@bidi_dir is "ltr", then this is "right"; otherwise, it's "left".
*
* \\"<right|left>\\"
* "<right|left>"
*/
getMessage(
messageName: \\"@@bidi_end_edge\\",
messageName: "@@bidi_end_edge",
substitutions?: string | string[],
options?: GetMessageOptions,
): string;
/**
* Ask for the user's name
*
* \\"What's your name?\\"
* "What's your name?"
*/
getMessage(
messageName: \\"prompt_for_name\\",
messageName: "prompt_for_name",
substitutions?: string | string[],
options?: GetMessageOptions,
): string;
/**
* Greet the user
*
* \\"Hello, $USER$\\"
* "Hello, $USER$"
*/
getMessage(
messageName: \\"hello\\",
messageName: "hello",
substitutions?: string | string[],
options?: GetMessageOptions,
): string;
/**
* Say goodbye to the user
*
* \\"Goodbye, $USER$. Come back to $OUR_SITE$ soon!\\"
* "Goodbye, $USER$. Come back to $OUR_SITE$ soon!"
*/
getMessage(
messageName: \\"bye\\",
messageName: "bye",
substitutions?: string | string[],
options?: GetMessageOptions,
): string;
@@ -224,11 +224,11 @@ describe('TypeScript Project', () => {
".wxt/wxt.d.ts
----------------------------------------
// Generated by wxt
/// <reference types=\\"vite/client\\" />
/// <reference types=\\"./types/imports.d.ts\\" />
/// <reference types=\\"./types/paths.d.ts\\" />
/// <reference types=\\"./types/i18n.d.ts\\" />
/// <reference types=\\"./types/globals.d.ts\\" />
/// <reference types="vite/client" />
/// <reference types="./types/imports.d.ts" />
/// <reference types="./types/paths.d.ts" />
/// <reference types="./types/i18n.d.ts" />
/// <reference types="./types/globals.d.ts" />
"
`);
});
@@ -243,32 +243,32 @@ describe('TypeScript Project', () => {
".wxt/tsconfig.json
----------------------------------------
{
\\"compilerOptions\\": {
\\"target\\": \\"ESNext\\",
\\"module\\": \\"ESNext\\",
\\"moduleResolution\\": \\"Bundler\\",
\\"noEmit\\": true,
\\"esModuleInterop\\": true,
\\"forceConsistentCasingInFileNames\\": true,
\\"resolveJsonModule\\": true,
\\"strict\\": true,
\\"skipLibCheck\\": true,
\\"paths\\": {
\\"@\\": [\\"..\\"],
\\"@/*\\": [\\"../*\\"],
\\"~\\": [\\"..\\"],
\\"~/*\\": [\\"../*\\"],
\\"@@\\": [\\"..\\"],
\\"@@/*\\": [\\"../*\\"],
\\"~~\\": [\\"..\\"],
\\"~~/*\\": [\\"../*\\"]
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"noEmit": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"strict": true,
"skipLibCheck": true,
"paths": {
"@": [".."],
"@/*": ["../*"],
"~": [".."],
"~/*": ["../*"],
"@@": [".."],
"@@/*": ["../*"],
"~~": [".."],
"~~/*": ["../*"]
}
},
\\"include\\": [
\\"../**/*\\",
\\"./wxt.d.ts\\"
"include": [
"../**/*",
"./wxt.d.ts"
],
\\"exclude\\": [\\"../.output\\"]
"exclude": ["../.output"]
}"
`);
});
@@ -286,32 +286,32 @@ describe('TypeScript Project', () => {
".wxt/tsconfig.json
----------------------------------------
{
\\"compilerOptions\\": {
\\"target\\": \\"ESNext\\",
\\"module\\": \\"ESNext\\",
\\"moduleResolution\\": \\"Bundler\\",
\\"noEmit\\": true,
\\"esModuleInterop\\": true,
\\"forceConsistentCasingInFileNames\\": true,
\\"resolveJsonModule\\": true,
\\"strict\\": true,
\\"skipLibCheck\\": true,
\\"paths\\": {
\\"@\\": [\\"../src\\"],
\\"@/*\\": [\\"../src/*\\"],
\\"~\\": [\\"../src\\"],
\\"~/*\\": [\\"../src/*\\"],
\\"@@\\": [\\"..\\"],
\\"@@/*\\": [\\"../*\\"],
\\"~~\\": [\\"..\\"],
\\"~~/*\\": [\\"../*\\"]
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"noEmit": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"strict": true,
"skipLibCheck": true,
"paths": {
"@": ["../src"],
"@/*": ["../src/*"],
"~": ["../src"],
"~/*": ["../src/*"],
"@@": [".."],
"@@/*": ["../*"],
"~~": [".."],
"~~/*": ["../*"]
}
},
\\"include\\": [
\\"../**/*\\",
\\"./wxt.d.ts\\"
"include": [
"../**/*",
"./wxt.d.ts"
],
\\"exclude\\": [\\"../.output\\"]
"exclude": ["../.output"]
}"
`);
});
@@ -333,34 +333,34 @@ describe('TypeScript Project', () => {
".wxt/tsconfig.json
----------------------------------------
{
\\"compilerOptions\\": {
\\"target\\": \\"ESNext\\",
\\"module\\": \\"ESNext\\",
\\"moduleResolution\\": \\"Bundler\\",
\\"noEmit\\": true,
\\"esModuleInterop\\": true,
\\"forceConsistentCasingInFileNames\\": true,
\\"resolveJsonModule\\": true,
\\"strict\\": true,
\\"skipLibCheck\\": true,
\\"paths\\": {
\\"example\\": [\\"../example\\"],
\\"example/*\\": [\\"../example/*\\"],
\\"@\\": [\\"../src\\"],
\\"@/*\\": [\\"../src/*\\"],
\\"~\\": [\\"../src\\"],
\\"~/*\\": [\\"../src/*\\"],
\\"@@\\": [\\"..\\"],
\\"@@/*\\": [\\"../*\\"],
\\"~~\\": [\\"..\\"],
\\"~~/*\\": [\\"../*\\"]
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"noEmit": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"strict": true,
"skipLibCheck": true,
"paths": {
"example": ["../example"],
"example/*": ["../example/*"],
"@": ["../src"],
"@/*": ["../src/*"],
"~": ["../src"],
"~/*": ["../src/*"],
"@@": [".."],
"@@/*": ["../*"],
"~~": [".."],
"~~/*": ["../*"]
}
},
\\"include\\": [
\\"../**/*\\",
\\"./wxt.d.ts\\"
"include": [
"../**/*",
"./wxt.d.ts"
],
\\"exclude\\": [\\"../.output\\"]
"exclude": ["../.output"]
}"
`);
});
+3 -3
View File
@@ -29,7 +29,7 @@ describe('User Config', () => {
================================================================================
.output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"background\\":{\\"service_worker\\":\\"background.js\\"}}"
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","background":{"service_worker":"background.js"}}"
`);
});
@@ -55,7 +55,7 @@ describe('User Config', () => {
================================================================================
.output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"background\\":{\\"service_worker\\":\\"background.js\\"}}"
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","background":{"service_worker":"background.js"}}"
`);
});
@@ -83,7 +83,7 @@ describe('User Config', () => {
expect(output).toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"example_customization\\":[\\"production\\",\\"chrome\\",\\"3\\",\\"build\\"]}"
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","example_customization":["3","build","production","chrome"]}"
`);
});
+3 -1
View File
@@ -77,7 +77,9 @@ export class TestProject {
await fs.writeFile(filePath, content ?? '', 'utf-8');
}
await execaCommand('npm i --ignore-scripts', { cwd: this.root });
await execaCommand('pnpm --ignore-workspace i --ignore-scripts', {
cwd: this.root,
});
await build({ ...config, root: this.root });
}
+30 -20
View File
@@ -1,7 +1,7 @@
{
"name": "wxt",
"type": "module",
"version": "0.9.2",
"version": "0.11.0",
"description": "Next gen framework for developing web extensions",
"engines": {
"node": ">=18",
@@ -56,6 +56,11 @@
"require": "./dist/testing.cjs",
"import": "./dist/testing.js",
"types": "./dist/testing.d.ts"
},
"./storage": {
"require": "./dist/storage.cjs",
"import": "./dist/storage.js",
"types": "./dist/storage.d.ts"
}
},
"scripts": {
@@ -76,13 +81,15 @@
},
"dependencies": {
"@types/webextension-polyfill": "^0.10.5",
"@webext-core/fake-browser": "^1.2.2",
"@webext-core/fake-browser": "^1.3.1",
"@webext-core/isolated-element": "^1.0.4",
"@webext-core/match-patterns": "^1.0.2",
"async-mutex": "^0.4.0",
"c12": "^1.5.1",
"cac": "^6.7.14",
"chokidar": "^3.5.3",
"consola": "^3.2.3",
"defu": "^6.1.3",
"esbuild": "^0.19.5",
"fast-glob": "^3.3.1",
"filesize": "^10.0.8",
@@ -95,41 +102,44 @@
"json5": "^2.2.3",
"linkedom": "^0.16.1",
"minimatch": "^9.0.3",
"normalize-path": "^3.0.0",
"ora": "^7.0.1",
"picocolors": "^1.0.0",
"prompts": "^2.4.2",
"rollup-plugin-visualizer": "^5.9.2",
"unimport": "^3.4.0",
"vite": "^4.0.0 || ^5.0.0-0",
"unstorage": "^1.9.0",
"vite": "^5.0.0",
"web-ext-run": "^0.1.0",
"webextension-polyfill": "^0.10.0",
"zip-dir": "^2.0.0"
},
"devDependencies": {
"@faker-js/faker": "^8.2.0",
"@types/fs-extra": "^11.0.3",
"@types/lodash.merge": "^4.6.8",
"@types/node": "^20.8.10",
"@types/prompts": "^2.4.7",
"@vitest/coverage-v8": "^0.34.6",
"@faker-js/faker": "^8.3.1",
"@types/fs-extra": "^11.0.4",
"@types/lodash.merge": "^4.6.9",
"@types/node": "^20.10.3",
"@types/normalize-path": "^3.0.2",
"@types/prompts": "^2.4.9",
"@vitest/coverage-v8": "^1.0.1",
"execa": "^8.0.1",
"happy-dom": "^12.4.0",
"lint-staged": "^15.0.2",
"happy-dom": "^12.10.3",
"lint-staged": "^15.2.0",
"lodash.merge": "^4.6.2",
"npm-run-all": "^4.1.5",
"p-map": "^6.0.0",
"prettier": "^3.0.1",
"p-map": "^7.0.0",
"prettier": "^3.1.0",
"simple-git-hooks": "^2.9.0",
"tsup": "^7.2.0",
"tsx": "^3.12.7",
"typedoc": "^0.25.3",
"tsup": "^8.0.1",
"tsx": "^4.6.2",
"typedoc": "^0.25.4",
"typedoc-plugin-markdown": "4.0.0-next.23",
"typedoc-vitepress-theme": "1.0.0-next.3",
"typescript": "^5.2.2",
"vitepress": "1.0.0-rc.24",
"vitest": "^0.34.6",
"typescript": "^5.3.2",
"vitepress": "1.0.0-rc.31",
"vitest": "^1.0.0",
"vitest-mock-extended": "^1.3.1",
"vue": "^3.3.7"
"vue": "^3.3.10"
},
"packageManager": "pnpm@8.6.3",
"simple-git-hooks": {
+1416 -361
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -57,6 +57,7 @@ const config: tsup.Options[] = [
entry: {
index: 'src/index.ts',
testing: 'src/testing/index.ts',
storage: 'src/storage.ts',
},
format: ['cjs', 'esm'],
clean: true,
+70
View File
@@ -0,0 +1,70 @@
import { fakeBrowser } from '@webext-core/fake-browser';
import { describe, it, expect, beforeEach } from 'vitest';
import { browser } from '~/browser';
import { storage } from '~/storage';
describe('Storage Utils', () => {
beforeEach(() => {
fakeBrowser.reset();
});
describe.each(['local', 'sync', 'managed', 'session'] as const)(
'storage - %s',
(storageArea) => {
describe('getItem', () => {
it('should return the value from the correct storage area', async () => {
const expected = 123;
await fakeBrowser.storage[storageArea].set({ count: expected });
expect(await storage.getItem(`${storageArea}:count`)).toBe(expected);
});
it("should return null if the value doesn't exist", async () => {
expect(await storage.getItem(`${storageArea}:count`)).toBeNull();
});
});
describe('getItems', () => {
it('should return an array of values in the same order as the keys passed in', async () => {
const expected = [
{ key: `${storageArea}:count`, value: 234 },
{ key: `${storageArea}:installDate`, value: null },
];
const keys = expected.map((item) => item.key);
await fakeBrowser.storage[storageArea].set({
count: expected[0].value,
});
const actual = await storage.getItems(keys);
expect(actual).toEqual(expected);
});
});
describe('setItem', () => {
it('should set the value in the correct storage area', () => {});
it.each([undefined, null])(
'should remove the item from storage when setting the value to %s',
async (value) => {
await fakeBrowser.storage[storageArea].set({ count: 345 });
await storage.setItem(`${storageArea}:count`, value as null);
// For some reason storage sets the value to "null" instead of deleting it. So using
// fakeBrowser during the expect fails. Using storage works. I've confirmed that this
// doesn't happen in a real extension environment.
expect(await storage.getItem(`${storageArea}:count`)).toBeNull();
},
);
});
describe('removeItem', () => {
it('should remove the key from storage', async () => {
await fakeBrowser.storage[storageArea].set({ count: 456 });
await storage.removeItem(`${storageArea}:count`);
expect(await browser.storage[storageArea].get()).toEqual({});
});
});
},
);
});
+244
View File
@@ -0,0 +1,244 @@
import type * as vite from 'vite';
import {
BuildStepOutput,
Entrypoint,
InlineConfig,
InternalConfig,
UserConfig,
WxtBuilder,
WxtBuilderServer,
} from '~/types';
import * as wxtPlugins from './plugins';
import { getEntrypointBundlePath } from '~/core/utils/entrypoints';
export async function craeteViteBuilder(
inlineConfig: InlineConfig,
userConfig: UserConfig,
wxtConfig: Omit<InternalConfig, 'builder'>,
): Promise<WxtBuilder> {
const vite = await import('vite');
/**
* Returns the base vite config shared by all builds based on the inline and user config.
*/
const getBaseConfig = async () => {
const resolvedInlineConfig =
(await inlineConfig.vite?.(wxtConfig.env)) ?? {};
const resolvedUserConfig = (await userConfig.vite?.(wxtConfig.env)) ?? {};
const config: vite.InlineConfig = vite.mergeConfig(
resolvedUserConfig,
resolvedInlineConfig,
);
config.root = wxtConfig.root;
config.configFile = false;
config.logLevel = 'warn';
config.mode = wxtConfig.mode;
config.build ??= {};
config.build.outDir = wxtConfig.outDir;
config.build.emptyOutDir = false;
config.plugins ??= [];
config.plugins.push(
wxtPlugins.download(wxtConfig),
wxtPlugins.devHtmlPrerender(wxtConfig),
wxtPlugins.unimport(wxtConfig),
wxtPlugins.virtualEntrypoint('background', wxtConfig),
wxtPlugins.virtualEntrypoint('content-script', wxtConfig),
wxtPlugins.virtualEntrypoint('unlisted-script', wxtConfig),
wxtPlugins.devServerGlobals(wxtConfig),
wxtPlugins.tsconfigPaths(wxtConfig),
wxtPlugins.noopBackground(),
wxtPlugins.globals(wxtConfig),
wxtPlugins.excludeBrowserPolyfill(wxtConfig),
);
if (wxtConfig.analysis.enabled) {
config.plugins.push(wxtPlugins.bundleAnalysis());
}
return config;
};
/**
* Return the basic config for building an entrypoint in [lib mode](https://vitejs.dev/guide/build.html#library-mode).
*/
const getLibModeConfig = (entrypoint: Entrypoint): vite.InlineConfig => {
const isVirtual = [
'background',
'content-script',
'unlisted-script',
].includes(entrypoint.type);
const entry = isVirtual
? `virtual:wxt-${entrypoint.type}?${entrypoint.inputPath}`
: entrypoint.inputPath;
const plugins: NonNullable<vite.UserConfig['plugins']> = [
wxtPlugins.entrypointGroupGlobals(entrypoint),
];
if (
entrypoint.type === 'content-script-style' ||
entrypoint.type === 'unlisted-style'
) {
plugins.push(wxtPlugins.cssEntrypoints(entrypoint, wxtConfig));
}
const libMode: vite.UserConfig = {
mode: wxtConfig.mode,
plugins,
build: {
lib: {
entry,
formats: ['iife'],
name: '_',
fileName: entrypoint.name,
},
rollupOptions: {
output: {
// There's only a single output for this build, so we use the desired bundle path for the
// entry output (like "content-scripts/overlay.js")
entryFileNames: getEntrypointBundlePath(
entrypoint,
wxtConfig.outDir,
'.js',
),
// Output content script CSS to `content-scripts/`, but all other scripts are written to
// `assets/`.
assetFileNames: ({ name }) => {
if (
entrypoint.type === 'content-script' &&
name?.endsWith('css')
) {
return `content-scripts/${entrypoint.name}.[ext]`;
} else {
return `assets/${entrypoint.name}.[ext]`;
}
},
},
},
},
define: {
// See https://github.com/aklinker1/vite-plugin-web-extension/issues/96
'process.env.NODE_ENV': JSON.stringify(wxtConfig.mode),
},
};
return libMode;
};
/**
* Return the basic config for building multiple entrypoints in [multi-page mode](https://vitejs.dev/guide/build.html#multi-page-app).
*/
const getMultiPageConfig = (entrypoints: Entrypoint[]): vite.InlineConfig => {
return {
mode: wxtConfig.mode,
plugins: [
wxtPlugins.multipageMove(entrypoints, wxtConfig),
wxtPlugins.entrypointGroupGlobals(entrypoints),
],
build: {
rollupOptions: {
input: entrypoints.reduce<Record<string, string>>((input, entry) => {
input[entry.name] = entry.inputPath;
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',
// We can't control the "name", so we need a hash to prevent conflicts
assetFileNames: 'assets/[name]-[hash].[ext]',
},
},
},
};
};
/**
* Return the basic config for building a sinlge CSS entrypoint in [multi-page mode](https://vitejs.dev/guide/build.html#multi-page-app).
*/
const getCssConfig = (entrypoint: Entrypoint): vite.InlineConfig => {
return {
mode: wxtConfig.mode,
plugins: [wxtPlugins.entrypointGroupGlobals(entrypoint)],
build: {
rollupOptions: {
input: {
[entrypoint.name]: entrypoint.inputPath,
},
output: {
assetFileNames: () => {
if (entrypoint.type === 'content-script-style') {
return `content-scripts/${entrypoint.name}.[ext]`;
} else {
return `assets/${entrypoint.name}.[ext]`;
}
},
},
},
},
};
};
return {
name: 'Vite',
version: vite.version,
async build(group) {
let entryConfig;
if (Array.isArray(group)) entryConfig = getMultiPageConfig(group);
else if (group.inputPath.endsWith('.css'))
entryConfig = getCssConfig(group);
else entryConfig = getLibModeConfig(group);
const buildConfig = vite.mergeConfig(await getBaseConfig(), entryConfig);
const result = await vite.build(buildConfig);
return {
entrypoints: group,
chunks: getBuildOutputChunks(result),
};
},
async createServer(info) {
const serverConfig: vite.InlineConfig = {
server: {
port: info.port,
strictPort: true,
host: info.hostname,
origin: info.origin,
},
};
const baseConfig = await getBaseConfig();
const viteServer = await vite.createServer(
vite.mergeConfig(baseConfig, serverConfig),
);
const server: WxtBuilderServer = {
async listen() {
await viteServer.listen(info.port);
},
transformHtml(...args) {
return viteServer.transformIndexHtml(...args);
},
ws: {
send(message, payload) {
return viteServer.ws.send(message, payload);
},
on(message, cb) {
viteServer.ws.on(message, cb);
},
},
watcher: viteServer.watcher,
};
return server;
},
};
}
function getBuildOutputChunks(
result: Awaited<ReturnType<typeof vite.build>>,
): BuildStepOutput['chunks'] {
if ('on' in result) throw Error('wxt does not support vite watch mode.');
if (Array.isArray(result)) return result.flatMap(({ output }) => output);
return result.output;
}
@@ -14,7 +14,7 @@ import { getEntrypointBundlePath } from '~/core/utils/entrypoints';
*/
export function cssEntrypoints(
entrypoint: Entrypoint,
config: InternalConfig,
config: Omit<InternalConfig, 'builder'>,
): vite.Plugin {
return {
name: 'wxt:css-entrypoint',
@@ -2,7 +2,7 @@ import * as vite from 'vite';
import { InternalConfig } from '~/types';
import { getEntrypointName } from '~/core/utils/entrypoints';
import { parseHTML } from 'linkedom';
import { dirname, isAbsolute, relative, resolve } from 'path';
import { dirname, isAbsolute, relative, resolve } from 'node:path';
// Cache the preamble script for all devHtmlPrerender plugins, not just one
let reactRefreshPreamble = '';
@@ -10,7 +10,9 @@ let reactRefreshPreamble = '';
/**
* Pre-renders the HTML entrypoints when building the extension to connect to the dev server.
*/
export function devHtmlPrerender(config: InternalConfig): vite.PluginOption {
export function devHtmlPrerender(
config: Omit<InternalConfig, 'builder'>,
): vite.PluginOption {
const htmlReloadId = '@wxt/reload-html';
const resolvedHtmlReloadId = resolve(
config.root,
@@ -86,11 +88,7 @@ export function devHtmlPrerender(config: InternalConfig): vite.PluginOption {
const originalUrl = `${server.origin}${ctx.path}`;
const name = getEntrypointName(config.entrypointsDir, ctx.filename);
const url = `${server.origin}/${name}.html`;
const serverHtml = await server.transformIndexHtml(
url,
html,
originalUrl,
);
const serverHtml = await server.transformHtml(url, html, originalUrl);
const { document } = parseHTML(serverHtml);
// React pages include a preamble as an unsafe-inline type="module" script to enable fast refresh, as shown here:
@@ -4,20 +4,19 @@ import { InternalConfig } from '~/types';
/**
* Defines global constants about the dev server. Helps scripts connect to the server's web socket.
*/
export function devServerGlobals(internalConfig: InternalConfig): Plugin {
export function devServerGlobals(
config: Omit<InternalConfig, 'builder'>,
): Plugin {
return {
name: 'wxt:dev-server-globals',
config() {
if (internalConfig.server == null || internalConfig.command == 'build')
return;
if (config.server == null || config.command == 'build') return;
return {
define: {
__DEV_SERVER_PROTOCOL__: JSON.stringify('ws:'),
__DEV_SERVER_HOSTNAME__: JSON.stringify(
internalConfig.server.hostname,
),
__DEV_SERVER_PORT__: JSON.stringify(internalConfig.server.port),
__DEV_SERVER_HOSTNAME__: JSON.stringify(config.server.hostname),
__DEV_SERVER_PORT__: JSON.stringify(config.server.port),
},
};
},
@@ -9,7 +9,7 @@ import { fetchCached } from '~/core/utils/network';
* @example
* import "url:https://google-tagmanager.com/gtag?id=XYZ";
*/
export function download(config: InternalConfig): Plugin {
export function download(config: Omit<InternalConfig, 'builder'>): Plugin {
return {
name: 'wxt:download',
resolveId(id) {
@@ -0,0 +1,24 @@
import * as vite from 'vite';
import { EntrypointGroup } from '~/types';
import { getEntrypointGlobals } from '~/core/utils/globals';
/**
* Define a set of global variables specific to an entrypoint.
*/
export function entrypointGroupGlobals(
entrypointGroup: EntrypointGroup,
): vite.PluginOption {
return {
name: 'wxt:entrypoint-group-globals',
config() {
const define: vite.InlineConfig['define'] = {};
let name = Array.isArray(entrypointGroup) ? 'html' : entrypointGroup.name;
for (const global of getEntrypointGlobals(name)) {
define[global.name] = JSON.stringify(global.value);
}
return {
define,
};
},
};
}
@@ -6,7 +6,9 @@ import * as vite from 'vite';
* `webextension-polyfill` module to a virtual module and exporting the `chrome` global from the
* virtual module.
*/
export function excludeBrowserPolyfill(config: InternalConfig): vite.Plugin {
export function excludeBrowserPolyfill(
config: Omit<InternalConfig, 'builder'>,
): vite.Plugin {
const virtualId = 'virtual:wxt-webextension-polyfill-disabled';
return {
@@ -1,8 +1,10 @@
import * as vite from 'vite';
import { InternalConfig } from '~/types';
import { getGlobals } from '../utils/globals';
import { getGlobals } from '~/core/utils/globals';
export function globals(config: InternalConfig): vite.PluginOption {
export function globals(
config: Omit<InternalConfig, 'builder'>,
): vite.PluginOption {
return {
name: 'wxt:globals',
config() {
@@ -12,3 +12,4 @@ export * from './globals';
export * from './webextensionPolyfillAlias';
export * from './webextensionPolyfillInlineDeps';
export * from './excludeBrowserPolyfill';
export * from './entrypointGroupGlobals';
@@ -1,6 +1,6 @@
import * as vite from 'vite';
import { Entrypoint, InternalConfig } from '~/types';
import { dirname, extname, resolve } from 'node:path';
import { dirname, extname, resolve, join } from 'node:path';
import { getEntrypointBundlePath } from '~/core/utils/entrypoints';
import fs, { ensureDir } from 'fs-extra';
import { normalizePath } from '~/core/utils/paths';
@@ -20,7 +20,7 @@ import { normalizePath } from '~/core/utils/paths';
*/
export function multipageMove(
entrypoints: Entrypoint[],
config: InternalConfig,
config: Omit<InternalConfig, 'builder'>,
): vite.Plugin {
return {
name: 'wxt:multipage-move',
@@ -67,6 +67,30 @@ export function multipageMove(
delete bundle[oldBundlePath];
bundle[newBundlePath] = renamedChunk;
}
// Remove directories that were created
// TODO: Optimize and only delete old path directories
removeEmptyDirs(config.outDir);
},
};
}
/**
* Recursively remove all directories that are empty/
*/
export async function removeEmptyDirs(dir: string): Promise<void> {
const files = await fs.readdir(dir);
for (const file of files) {
const filePath = join(dir, file);
const stats = await fs.stat(filePath);
if (stats.isDirectory()) {
await removeEmptyDirs(filePath);
}
}
try {
await fs.rmdir(dir);
} catch {
// noop on failure - this means the directory was not empty.
}
}
@@ -1,4 +1,5 @@
import { Plugin } from 'vite';
import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '~/core/utils/constants';
/**
* In dev mode, if there's not a background script listed, we need to add one.
@@ -20,5 +21,3 @@ export function noopBackground(): Plugin {
},
};
}
export const VIRTUAL_NOOP_BACKGROUND_MODULE_ID = 'virtual:user-background';
@@ -1,7 +1,9 @@
import { InternalConfig } from '~/types';
import * as vite from 'vite';
export function tsconfigPaths(config: InternalConfig): vite.Plugin {
export function tsconfigPaths(
config: Omit<InternalConfig, 'builder'>,
): vite.Plugin {
return {
name: 'wxt:aliases',
async config() {
@@ -4,19 +4,21 @@ import { getUnimportOptions } from '~/core/utils/unimport';
import * as vite from 'vite';
import { extname } from 'path';
const ENABLED_EXTENSIONS: Record<string, boolean | undefined> = {
'.js': true,
'.jsx': true,
'.ts': true,
'.tsx': true,
'.vue': true,
'.svelte': true,
};
const ENABLED_EXTENSIONS = new Set([
'.js',
'.jsx',
'.ts',
'.tsx',
'.vue',
'.svelte',
]);
/**
* Inject any global imports defined by unimport
*/
export function unimport(config: InternalConfig): vite.PluginOption {
export function unimport(
config: Omit<InternalConfig, 'builder'>,
): vite.PluginOption {
const options = getUnimportOptions(config);
if (options === false) return [];
@@ -28,8 +30,13 @@ export function unimport(config: InternalConfig): vite.PluginOption {
await unimport.scanImportsFromDir(undefined, { cwd: config.srcDir });
},
async transform(code, id) {
const ext = extname(id);
if (ENABLED_EXTENSIONS[ext]) return unimport.injectImports(code, id);
// Don't transform dependencies
if (id.includes('node_modules')) return;
// Don't transform non-js files
if (!ENABLED_EXTENSIONS.has(extname(id))) return;
return unimport.injectImports(code, id);
},
};
}
@@ -9,7 +9,7 @@ import { normalizePath } from '~/core/utils/paths';
*/
export function virtualEntrypoint(
type: Entrypoint['type'],
config: InternalConfig,
config: Omit<InternalConfig, 'builder'>,
): Plugin {
const virtualId = `virtual:wxt-${type}?`;
const resolvedVirtualId = `\0${virtualId}`;
@@ -8,7 +8,7 @@ import { InternalConfig } from '~/types';
* This should only be used during tests.
*/
export function webextensionPolyfillAlias(
config: InternalConfig,
config: Omit<InternalConfig, 'builder'>,
): vite.PluginOption {
return {
name: 'wxt:webextension-polyfill-test-alias',
+96 -114
View File
@@ -3,10 +3,9 @@ import {
EntrypointGroup,
InlineConfig,
InternalConfig,
ServerInfo,
WxtDevServer,
} from '~/types';
import * as vite from 'vite';
import type { Scripting } from 'webextension-polyfill';
import {
getEntrypointBundlePath,
getEntrypointOutputFile,
@@ -38,35 +37,103 @@ import { relative } from 'node:path';
* await server.start();
*/
export async function createServer(
config?: InlineConfig,
inlineConfig?: InlineConfig,
): Promise<WxtDevServer> {
const serverInfo = await getServerInfo();
const getLatestInternalConfig = async () => {
return getInternalConfig(
{
...config,
vite: () => serverInfo.viteServerConfig,
},
'serve',
);
const port = await getPort();
const hostname = 'localhost';
const origin = `http://${hostname}:${port}`;
const serverInfo: ServerInfo = {
port,
hostname,
origin,
};
let internalConfig = await getLatestInternalConfig();
const server = await setupServer(serverInfo, internalConfig);
internalConfig.server = server;
// Server instance must be created first so its reference can be added to the internal config used
// to pre-render entrypoints
const server: WxtDevServer = {
...serverInfo,
watcher: undefined as any, // Filled out later down below
ws: undefined as any, // Filled out later down below
currentOutput: undefined as any, // Filled out later down below
async start() {
await builderServer.listen();
config.logger.success(`Started dev server @ ${serverInfo.origin}`);
// Build after starting the dev server so it can be used to transform HTML files
server.currentOutput = await internalBuild(config);
// Open browser after everything is ready to go.
await runner.openBrowser(config);
},
transformHtml(url, html, originalUrl) {
return builderServer.transformHtml(url, html, originalUrl);
},
reloadContentScript(contentScript) {
server.ws.send('wxt:reload-content-script', contentScript);
},
reloadPage(path) {
server.ws.send('wxt:reload-page', path);
},
reloadExtension() {
server.ws.send('wxt:reload-extension');
},
};
const getLatestConfig = () =>
getInternalConfig(inlineConfig ?? {}, 'serve', server);
let config = await getLatestConfig();
const [runner, builderServer] = await Promise.all([
createExtensionRunner(config),
config.builder.createServer(server),
]);
server.watcher = builderServer.watcher;
server.ws = builderServer.ws;
// Register content scripts for the first time after the background starts up since they're not
// listed in the manifest
server.ws.on('wxt:background-initialized', () => {
reloadContentScripts(server.currentOutput.steps, config, server);
});
// Listen for file changes and reload different parts of the extension accordingly
const reloadOnChange = createFileReloader({
server,
getLatestConfig,
updateConfig(newConfig) {
config = newConfig;
},
});
server.watcher.on('all', reloadOnChange);
return server;
}
async function getPort(): Promise<number> {
const { default: getPort, portNumbers } = await import('get-port');
return await getPort({ port: portNumbers(3000, 3010) });
}
/**
* Returns a function responsible for reloading different parts of the extension when a file
* changes.
*/
function createFileReloader(options: {
server: WxtDevServer;
getLatestConfig: () => Promise<InternalConfig>;
updateConfig: (config: InternalConfig) => void;
}) {
const { server, getLatestConfig, updateConfig } = options;
const fileChangedMutex = new Mutex();
const changeQueue: Array<[string, string]> = [];
server.ws.on('wxt:background-initialized', () => {
// Register content scripts for the first time since they're not listed in the manifest
reloadContentScripts(server.currentOutput.steps, internalConfig, server);
});
return async (event: string, path: string) => {
const config = await getLatestConfig();
updateConfig(config);
server.watcher.on('all', async (event, path, _stats) => {
// Here, "path" is a non-normalized path (ie: C:\\users\\... instead of C:/users/...)
if (path.startsWith(internalConfig.outBaseDir)) return;
if (path.startsWith(config.outBaseDir)) return;
changeQueue.push([event, path]);
await fileChangedMutex.runExclusive(async () => {
@@ -77,25 +144,23 @@ export async function createServer(
if (changes.type === 'no-change') return;
// Log the entrypoints that were effected
internalConfig.logger.info(
config.logger.info(
`Changed: ${Array.from(new Set(fileChanges.map((change) => change[1])))
.map((file) => pc.dim(relative(internalConfig.root, file)))
.map((file) => pc.dim(relative(config.root, file)))
.join(', ')}`,
);
const rebuiltNames = changes.rebuildGroups
.flat()
.map((entry) => {
return pc.cyan(
relative(internalConfig.outDir, getEntrypointOutputFile(entry, '')),
relative(config.outDir, getEntrypointOutputFile(entry, '')),
);
})
.join(pc.dim(', '));
// Get latest config and Rebuild groups with changes
internalConfig = await getLatestInternalConfig();
internalConfig.server = server;
// Rebuild entrypoints on change
const { output: newOutput } = await rebuild(
internalConfig,
config,
// TODO: this excludes new entrypoints, so they're not built until the dev command is restarted
changes.rebuildGroups,
changes.cachedOutput,
@@ -108,91 +173,15 @@ export async function createServer(
server.reloadExtension();
break;
case 'html-reload':
reloadHtmlPages(changes.rebuildGroups, server, internalConfig);
reloadHtmlPages(changes.rebuildGroups, server, config);
break;
case 'content-script-reload':
reloadContentScripts(changes.changedSteps, internalConfig, server);
reloadContentScripts(changes.changedSteps, config, server);
break;
}
consola.success(`Reloaded: ${rebuiltNames}`);
});
});
return server;
}
async function getServerInfo(): Promise<ServerInfo> {
const { default: getPort, portNumbers } = await import('get-port');
const port = await getPort({ port: portNumbers(3000, 3010) });
const hostname = 'localhost';
const origin = `http://${hostname}:${port}`;
const serverConfig: vite.InlineConfig = {
server: {
origin,
},
};
return {
port,
hostname,
origin,
viteServerConfig: serverConfig,
};
}
async function setupServer(
serverInfo: ServerInfo,
config: InternalConfig,
): Promise<WxtDevServer> {
const runner = await createExtensionRunner(config);
const viteServer = await vite.createServer(
vite.mergeConfig(serverInfo, await config.vite(config.env)),
);
const start = async () => {
await viteServer.listen(server.port);
config.logger.success(`Started dev server @ ${serverInfo.origin}`);
server.currentOutput = await internalBuild(config);
await runner.openBrowser(config);
};
const reloadExtension = () => {
viteServer.ws.send('wxt:reload-extension');
};
const reloadPage = (path: string) => {
// Can't use Vite's built-in "full-reload" event because it doesn't like our paths, it expects
// paths ending in "/index.html"
viteServer.ws.send('wxt:reload-page', path);
};
const reloadContentScript = (
contentScript: Omit<Scripting.RegisteredContentScript, 'id'>,
) => {
viteServer.ws.send('wxt:reload-content-script', contentScript);
};
const server: WxtDevServer = {
...viteServer,
start,
currentOutput: {
manifest: {
manifest_version: 3,
name: '',
version: '',
},
publicAssets: [],
steps: [],
},
port: serverInfo.port,
hostname: serverInfo.hostname,
origin: serverInfo.origin,
reloadExtension,
reloadPage,
reloadContentScript,
};
return server;
}
/**
@@ -244,10 +233,3 @@ function reloadHtmlPages(
server.reloadPage(path);
});
}
interface ServerInfo {
port: number;
hostname: string;
origin: string;
viteServerConfig: vite.InlineConfig;
}
+7 -3
View File
@@ -41,9 +41,13 @@ export async function initialize(options: {
type: () => (options.packageManager == null ? 'select' : undefined),
message: 'Package Manager',
choices: [
{ title: 'npm', value: 'npm' },
{ title: 'pnpm', value: 'pnpm' },
{ title: 'yarn', value: 'yarn' },
{ title: pc.red('npm'), value: 'npm' },
{ title: pc.yellow('pnpm'), value: 'pnpm' },
{ title: pc.cyan('yarn'), value: 'yarn' },
{
title: `${pc.magenta('bun')}${pc.gray(' (experimental)')}`,
value: 'bun',
},
],
},
],
@@ -11,7 +11,7 @@ describe('Content Script Utils', () => {
);
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\\"]]"',
`"[["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"]]"`,
);
});
+24
View File
@@ -0,0 +1,24 @@
import { describe, expect, it } from 'vitest';
import { normalizePath } from '../paths';
describe('Path Utils', () => {
describe('normalizePath', () => {
it.each([
// Relative paths
['../test.sh', '../test.sh'],
['..\\test.sh', '../test.sh'],
['test.png', 'test.png'],
// Absolute paths
['C:\\\\path\\to\\file', 'C:/path/to/file'],
['/path/to/file', '/path/to/file'],
// Strip trailing slash
['C:\\\\path\\to\\folder\\', 'C:/path/to/folder'],
['/path/to/folder/', '/path/to/folder'],
// Dedupe slashes
['path\\\\\\file', 'path/file'],
['path//file', 'path/file'],
])('should normalize "%s" to "%s"', (input, expected) => {
expect(normalizePath(input)).toBe(expected);
});
});
});
@@ -8,8 +8,8 @@ import {
fakeManifest,
fakeOptionsEntrypoint,
fakePopupEntrypoint,
fakeRollupOutputAsset,
fakeRollupOutputChunk,
fakeOutputAsset,
fakeOutputChunk,
} from '~/core/utils/testing/fake-objects';
import { BuildOutput, BuildStepOutput } from '~/types';
@@ -32,15 +32,11 @@ describe('Detect Dev Changes', () => {
steps: [
{
entrypoints: fakeContentScriptEntrypoint(),
chunks: [fakeRollupOutputChunk(), fakeRollupOutputChunk()],
chunks: [fakeOutputChunk(), fakeOutputChunk()],
},
{
entrypoints: fakeContentScriptEntrypoint(),
chunks: [
fakeRollupOutputChunk(),
fakeRollupOutputChunk(),
fakeRollupOutputChunk(),
],
chunks: [fakeOutputChunk(), fakeOutputChunk(), fakeOutputChunk()],
},
],
};
@@ -57,10 +53,10 @@ describe('Detect Dev Changes', () => {
'unknown',
'/root/src/public/image.svg',
];
const asset1 = fakeRollupOutputAsset({
const asset1 = fakeOutputAsset({
fileName: 'image.svg',
});
const asset2 = fakeRollupOutputAsset({
const asset2 = fakeOutputAsset({
fileName: 'some-other-image.svg',
});
const currentOutput: BuildOutput = {
@@ -96,7 +92,7 @@ describe('Detect Dev Changes', () => {
const step1: BuildStepOutput = {
entrypoints: contentScript,
chunks: [
fakeRollupOutputChunk({
fakeOutputChunk({
moduleIds: [fakeFile(), fakeFile()],
}),
],
@@ -104,7 +100,7 @@ describe('Detect Dev Changes', () => {
const step2: BuildStepOutput = {
entrypoints: background,
chunks: [
fakeRollupOutputChunk({
fakeOutputChunk({
moduleIds: [fakeFile(), changedPath, fakeFile()],
}),
],
@@ -150,7 +146,7 @@ describe('Detect Dev Changes', () => {
const step1: BuildStepOutput = {
entrypoints: [htmlPage1, htmlPage2],
chunks: [
fakeRollupOutputChunk({
fakeOutputChunk({
moduleIds: [fakeFile(), changedPath],
}),
],
@@ -158,7 +154,7 @@ describe('Detect Dev Changes', () => {
const step2: BuildStepOutput = {
entrypoints: [htmlPage3],
chunks: [
fakeRollupOutputChunk({
fakeOutputChunk({
moduleIds: [fakeFile(), fakeFile(), fakeFile()],
}),
],
@@ -203,7 +199,7 @@ describe('Detect Dev Changes', () => {
const step1: BuildStepOutput = {
entrypoints: script1,
chunks: [
fakeRollupOutputChunk({
fakeOutputChunk({
moduleIds: [fakeFile(), changedPath],
}),
],
@@ -211,7 +207,7 @@ describe('Detect Dev Changes', () => {
const step2: BuildStepOutput = {
entrypoints: script2,
chunks: [
fakeRollupOutputChunk({
fakeOutputChunk({
moduleIds: [fakeFile(), fakeFile(), fakeFile()],
}),
],
@@ -219,7 +215,7 @@ describe('Detect Dev Changes', () => {
const step3: BuildStepOutput = {
entrypoints: script3,
chunks: [
fakeRollupOutputChunk({
fakeOutputChunk({
moduleIds: [changedPath, fakeFile(), fakeFile()],
}),
],
+6 -150
View File
@@ -1,18 +1,12 @@
import * as vite from 'vite';
import {
BuildOutput,
BuildStepOutput,
Entrypoint,
EntrypointGroup,
InternalConfig,
} from '~/types';
import * as wxtPlugins from '~/core/vite-plugins';
import { removeEmptyDirs } from '~/core/utils/fs';
import { getPublicFiles } from '~/core/utils/fs';
import { getEntrypointBundlePath } from '~/core/utils/entrypoints';
import fs from 'fs-extra';
import { dirname, resolve } from 'path';
import { getEntrypointGlobals } from '~/core/utils/globals';
import type { Ora } from 'ora';
import pc from 'picocolors';
@@ -24,153 +18,18 @@ export async function buildEntrypoints(
const steps: BuildStepOutput[] = [];
for (let i = 0; i < groups.length; i++) {
const group = groups[i];
spinner.text =
pc.dim(`[${i + 1}/${groups.length}]`) +
` ${[group]
.flat()
.map((e) => e.name)
.join(pc.dim(', '))}`;
const step = Array.isArray(group)
? await buildMultipleEntrypoints(group, config)
: await buildSingleEntrypoint(group, config);
steps.push(step);
const groupNames = [group]
.flat()
.map((e) => e.name)
.join(pc.dim(', '));
spinner.text = pc.dim(`[${i + 1}/${groups.length}]`) + ` ${groupNames}`;
steps.push(await config.builder.build(group));
}
const publicAssets = await copyPublicDirectory(config);
// Remove any empty directories from moving outputs around
await removeEmptyDirs(config.outDir);
return { publicAssets, steps };
}
/**
* Use Vite's lib mode + IIFE format to bundle the entrypoint to a single file.
*/
async function buildSingleEntrypoint(
entrypoint: Entrypoint,
config: InternalConfig,
): Promise<BuildStepOutput> {
// Should this entrypoint be wrapped by the vite-plugins/virtualEntrypoint plugin?
const isVirtual = [
'background',
'content-script',
'unlisted-script',
].includes(entrypoint.type);
const entry = isVirtual
? `virtual:wxt-${entrypoint.type}?${entrypoint.inputPath}`
: entrypoint.inputPath;
const plugins: NonNullable<vite.UserConfig['plugins']> = [];
if (
entrypoint.type === 'content-script-style' ||
entrypoint.type === 'unlisted-style'
) {
plugins.push(wxtPlugins.cssEntrypoints(entrypoint, config));
}
const libMode: vite.UserConfig = {
plugins,
build: {
lib: {
entry,
formats: ['iife'],
name: '_',
fileName: entrypoint.name,
},
rollupOptions: {
output: {
// There's only a single output for this build, so we use the desired bundle path for the
// entry output (like "content-scripts/overlay.js")
entryFileNames: getEntrypointBundlePath(
entrypoint,
config.outDir,
'.js',
),
// Output content script CSS to `content-scripts/`, but all other scripts are written to
// `assets/`.
assetFileNames: ({ name }) => {
if (entrypoint.type === 'content-script' && name?.endsWith('css')) {
return `content-scripts/${entrypoint.name}.[ext]`;
} else {
return `assets/${entrypoint.name}.[ext]`;
}
},
},
},
},
define: {
// See https://github.com/aklinker1/vite-plugin-web-extension/issues/96
'process.env.NODE_ENV': JSON.stringify(config.mode),
},
};
for (const global of getEntrypointGlobals(config, entrypoint.name)) {
libMode.define![global.name] = JSON.stringify(global.value);
}
const entryConfig = vite.mergeConfig(
libMode,
await config.vite(config.env),
) as vite.InlineConfig;
const result = await vite.build(entryConfig);
return {
entrypoints: entrypoint,
chunks: getBuildOutputChunks(result),
};
}
/**
* Use Vite's multipage build to bundle all the entrypoints in a single step.
*/
async function buildMultipleEntrypoints(
entrypoints: Entrypoint[],
config: InternalConfig,
): Promise<BuildStepOutput> {
const multiPage: vite.UserConfig = {
plugins: [wxtPlugins.multipageMove(entrypoints, config)],
build: {
rollupOptions: {
input: entrypoints.reduce<Record<string, string>>((input, entry) => {
input[entry.name] = entry.inputPath;
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',
// We can't control the "name", so we need a hash to prevent conflicts
assetFileNames: 'assets/[name]-[hash].[ext]',
},
},
},
define: {},
};
for (const global of getEntrypointGlobals(config, 'html')) {
multiPage.define![global.name] = JSON.stringify(global.value);
}
const entryConfig = vite.mergeConfig(
multiPage,
await config.vite(config.env),
) as vite.UserConfig;
const result = await vite.build(entryConfig);
return {
entrypoints,
chunks: getBuildOutputChunks(result),
};
}
function getBuildOutputChunks(
result: Awaited<ReturnType<typeof vite.build>>,
): BuildStepOutput['chunks'] {
if ('on' in result) throw Error('wxt does not support vite watch mode.');
if (Array.isArray(result)) return result.flatMap(({ output }) => output);
return result.output;
}
async function copyPublicDirectory(
config: InternalConfig,
): Promise<BuildOutput['publicAssets']> {
@@ -187,9 +46,6 @@ async function copyPublicDirectory(
publicAssets.push({
type: 'asset',
fileName: file,
name: file,
needsCodeReference: false,
source: await fs.readFile(srcPath),
});
}
@@ -1,5 +1,10 @@
import { BuildOutput, BuildStepOutput, EntrypointGroup } from '~/types';
import * as vite from 'vite';
import {
BuildOutput,
BuildStepOutput,
EntrypointGroup,
OutputAsset,
OutputFile,
} from '~/types';
import { every } from '~/core/utils/arrays';
import { normalizePath } from '~/core/utils/paths';
@@ -105,9 +110,7 @@ function findEffectedSteps(
const changes: DetectedChange[] = [];
const changedPath = normalizePath(changedFile[1]);
const isChunkEffected = (
chunk: vite.Rollup.OutputChunk | vite.Rollup.OutputAsset,
): boolean =>
const isChunkEffected = (chunk: OutputFile): boolean =>
// If it's an HTML file with the same path, is is effected because HTML files need to be pre-rendered
// fileName is normalized, relative bundle path
(chunk.type === 'asset' && changedPath.endsWith(chunk.fileName)) ||
@@ -176,4 +179,4 @@ interface ContentScriptReload extends RebuildChange {
* directory asset that was changed. It doesn't know what type of change is required yet. Just an
* intermediate type.
*/
type DetectedChange = BuildStepOutput | vite.Rollup.OutputAsset;
type DetectedChange = BuildStepOutput | OutputAsset;
+1 -1
View File
@@ -22,7 +22,7 @@ import {
getEntrypointName,
resolvePerBrowserOption,
} from '~/core/utils/entrypoints';
import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '~/core/vite-plugins/noopBackground';
import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '~/core/utils/constants';
import { CSS_EXTENSIONS_PATTERN } from '~/core/utils/paths';
/**
+1 -1
View File
@@ -156,7 +156,7 @@ async function writeGlobalsDeclarationFile(
config: InternalConfig,
): Promise<string> {
const filePath = resolve(config.typesDir, 'globals.d.ts');
const globals = [...getGlobals(config), ...getEntrypointGlobals(config, '')];
const globals = [...getGlobals(config), ...getEntrypointGlobals('')];
await writeFileIfDifferent(
filePath,
[
+29 -69
View File
@@ -6,14 +6,15 @@ import {
ConfigEnv,
UserManifestFn,
UserManifest,
WxtViteConfig,
ExtensionRunnerConfig,
WxtDevServer,
} from '~/types';
import path from 'node:path';
import * as vite from 'vite';
import { createFsCache } from '~/core/utils/cache';
import consola, { LogLevels } from 'consola';
import * as plugins from '~/core/vite-plugins';
import { craeteViteBuilder } from '~/core/builders/vite';
import defu from 'defu';
import { NullablyRequired } from '../types';
/**
* Given an inline config, discover the config file if necessary, merge the results, resolve any
@@ -25,6 +26,7 @@ import * as plugins from '~/core/vite-plugins';
export async function getInternalConfig(
inlineConfig: InlineConfig,
command: 'build' | 'serve',
server?: WxtDevServer,
): Promise<InternalConfig> {
// Load user config
@@ -69,7 +71,7 @@ export async function getInternalConfig(
);
const publicDir = path.resolve(srcDir, mergedConfig.publicDir ?? 'public');
const typesDir = path.resolve(wxtDir, 'types');
const outBaseDir = path.resolve(root, '.output');
const outBaseDir = path.resolve(root, mergedConfig.outDir ?? '.output');
const outDir = path.resolve(outBaseDir, `${browser}-mv${manifestVersion}`);
const runnerConfig = await loadConfig<ExtensionRunnerConfig>({
@@ -91,7 +93,7 @@ export async function getInternalConfig(
}).map(([key, value]) => [key, path.resolve(root, value)]),
);
const finalConfig: InternalConfig = {
const finalConfig: Omit<InternalConfig, 'builder'> = {
browser,
command,
debug,
@@ -110,7 +112,6 @@ export async function getInternalConfig(
runnerConfig,
srcDir,
typesDir,
vite: () => ({}), // Real value added after this object is initialized.
wxtDir,
zip: resolveInternalZipConfig(root, mergedConfig),
transformManifest(manifest) {
@@ -127,12 +128,19 @@ export async function getInternalConfig(
includeBrowserPolyfill:
mergedConfig.experimental?.includeBrowserPolyfill ?? true,
},
server,
};
finalConfig.vite = (env) =>
resolveInternalViteConfig(env, mergedConfig, finalConfig);
const builder = await craeteViteBuilder(
inlineConfig,
userConfig,
finalConfig,
);
return finalConfig;
return {
...finalConfig,
builder,
};
}
async function resolveManifestConfig(
@@ -150,35 +158,27 @@ async function resolveManifestConfig(
function mergeInlineConfig(
inlineConfig: InlineConfig,
userConfig: UserConfig,
): InlineConfig {
): NullablyRequired<InlineConfig> {
let imports: InlineConfig['imports'];
if (inlineConfig.imports === false || userConfig.imports === false) {
imports = false;
} else if (userConfig.imports == null && inlineConfig.imports == null) {
imports = undefined;
} else {
imports = vite.mergeConfig(
userConfig.imports ?? {},
inlineConfig.imports ?? {},
);
imports = defu(inlineConfig.imports ?? {}, userConfig.imports ?? {});
}
const manifest: UserManifestFn = async (env) => {
const user = await resolveManifestConfig(env, userConfig.manifest);
const inline = await resolveManifestConfig(env, inlineConfig.manifest);
return vite.mergeConfig(user, inline);
return defu(inline, user);
};
const viteConfig = async (env: ConfigEnv): Promise<WxtViteConfig> => {
const user = await userConfig.vite?.(env);
const inline = await inlineConfig.vite?.(env);
return vite.mergeConfig(user ?? {}, inline ?? {});
};
const runner: InlineConfig['runner'] = vite.mergeConfig(
userConfig.runner ?? {},
const runner: InlineConfig['runner'] = defu(
inlineConfig.runner ?? {},
userConfig.runner ?? {},
);
const zip: InlineConfig['zip'] = vite.mergeConfig(
userConfig.zip ?? {},
const zip: InlineConfig['zip'] = defu(
inlineConfig.zip ?? {},
userConfig.zip ?? {},
);
return {
@@ -195,7 +195,7 @@ function mergeInlineConfig(
publicDir: inlineConfig.publicDir ?? userConfig.publicDir,
runner,
srcDir: inlineConfig.srcDir ?? userConfig.srcDir,
vite: viteConfig,
outDir: inlineConfig.outDir ?? userConfig.outDir,
zip,
analysis: {
enabled: inlineConfig.analysis?.enabled ?? userConfig.analysis?.enabled,
@@ -210,14 +210,17 @@ function mergeInlineConfig(
...userConfig.experimental,
...inlineConfig.experimental,
},
vite: undefined,
transformManifest: undefined,
};
}
function resolveInternalZipConfig(
root: string,
mergedConfig: InlineConfig,
): InternalConfig['zip'] {
): NullablyRequired<InternalConfig['zip']> {
return {
name: undefined,
sourcesTemplate: '{{name}}-{{version}}-sources.zip',
artifactTemplate: '{{name}}-{{version}}-{{browser}}.zip',
sourcesRoot: root,
@@ -236,46 +239,3 @@ function resolveInternalZipConfig(
],
};
}
async function resolveInternalViteConfig(
env: ConfigEnv,
mergedConfig: InlineConfig,
finalConfig: InternalConfig,
) {
const internalVite: vite.InlineConfig =
(await mergedConfig.vite?.(env)) ?? {};
internalVite.root = finalConfig.root;
internalVite.configFile = false;
internalVite.logLevel = 'warn';
internalVite.mode = env.mode;
internalVite.build ??= {};
internalVite.build.outDir = finalConfig.outDir;
internalVite.build.emptyOutDir = false;
internalVite.plugins ??= [];
internalVite.plugins.push(plugins.download(finalConfig));
internalVite.plugins.push(plugins.devHtmlPrerender(finalConfig));
internalVite.plugins.push(plugins.unimport(finalConfig));
internalVite.plugins.push(
plugins.virtualEntrypoint('background', finalConfig),
);
internalVite.plugins.push(
plugins.virtualEntrypoint('content-script', finalConfig),
);
internalVite.plugins.push(
plugins.virtualEntrypoint('unlisted-script', finalConfig),
);
internalVite.plugins.push(plugins.devServerGlobals(finalConfig));
internalVite.plugins.push(plugins.tsconfigPaths(finalConfig));
internalVite.plugins.push(plugins.noopBackground());
if (finalConfig.analysis.enabled) {
internalVite.plugins.push(plugins.bundleAnalysis());
}
internalVite.plugins.push(plugins.globals(finalConfig));
internalVite.plugins.push(plugins.excludeBrowserPolyfill(finalConfig));
return internalVite;
}
@@ -55,6 +55,9 @@ export async function importEntrypointFile<T>(
'node_modules/wxt/dist/virtual/mock-browser.js',
),
},
// Continue using node to load TS files even if `bun run --bun` is detected. Jiti does not
// respect the custom transform function when using it's native bun option.
experimentalBun: false,
// List of extensions to transform with esbuild
extensions: ['.ts', '.cts', '.mts', '.tsx', '.js', '.cjs', '.mjs', '.jsx'],
transform(opts) {
+1 -2
View File
@@ -1,7 +1,6 @@
import { findEntrypoints } from './find-entrypoints';
import { InternalConfig, BuildOutput } from '~/types';
import pc from 'picocolors';
import * as vite from 'vite';
import fs from 'fs-extra';
import { groupEntrypoints } from './group-entrypoints';
import { formatDuration } from '~/core/utils/time';
@@ -27,7 +26,7 @@ export async function internalBuild(
const target = `${config.browser}-mv${config.manifestVersion}`;
config.logger.info(
`${verb} ${pc.cyan(target)} for ${pc.cyan(config.mode)} with ${pc.green(
`Vite ${vite.version}`,
`${config.builder.name} ${config.builder.version}`,
)}`,
);
const startTime = Date.now();
+5
View File
@@ -0,0 +1,5 @@
/**
* Module ID used to build the background in dev mode if the extension doesn't include a background
* script/service worker.
*/
export const VIRTUAL_NOOP_BACKGROUND_MODULE_ID = 'virtual:user-background';
-21
View File
@@ -2,7 +2,6 @@ import { InternalConfig } from '~/types';
import fs from 'fs-extra';
import glob from 'fast-glob';
import { unnormalizePath } from './paths';
import path from 'node:path';
/**
* Only write the contents to a file if it results in a change. This prevents unnecessary file
@@ -36,23 +35,3 @@ export async function getPublicFiles(
const files = await glob('**/*', { cwd: config.publicDir });
return files.map(unnormalizePath);
}
/**
* Recursively remove all directories that are empty/
*/
export async function removeEmptyDirs(dir: string): Promise<void> {
const files = await fs.readdir(dir);
for (const file of files) {
const filePath = path.join(dir, file);
const stats = await fs.stat(filePath);
if (stats.isDirectory()) {
await removeEmptyDirs(filePath);
}
}
try {
await fs.rmdir(dir);
} catch {
// noop on failure - this means the directory was not empty.
}
}
+2 -5
View File
@@ -1,7 +1,7 @@
import { InternalConfig } from '~/types';
export function getGlobals(
config: InternalConfig,
config: Omit<InternalConfig, 'builder'>,
): Array<{ name: string; value: any; type: string }> {
return [
{
@@ -47,10 +47,7 @@ export function getGlobals(
];
}
export function getEntrypointGlobals(
config: InternalConfig,
entrypointName: string,
) {
export function getEntrypointGlobals(entrypointName: string) {
return [
{
name: surroundInUnderscore('ENTRYPOINT'),
+10 -13
View File
@@ -23,7 +23,7 @@ import { getPackageJson } from './package';
import { normalizePath } from './paths';
import { writeFileIfDifferent } from './fs';
import { produce } from 'immer';
import * as vite from 'vite';
import defu from 'defu';
/**
* Writes the manifest to the output directory and the build output.
@@ -44,9 +44,6 @@ export async function writeManifest(
output.publicAssets.unshift({
type: 'asset',
fileName: 'manifest.json',
name: 'manifest',
needsCodeReference: false,
source: str,
});
}
@@ -78,9 +75,9 @@ export async function generateMainfest(
};
const userManifest = config.manifest;
const manifest = vite.mergeConfig(
baseManifest,
const manifest = defu(
userManifest,
baseManifest,
) as Manifest.WebExtensionManifest;
addEntrypoints(manifest, entrypoints, buildOutput, config);
@@ -365,13 +362,13 @@ function discoverIcons(
// prettier-ignore
// #region snippet
const iconRegex = [
/^icon-([0-9]+)\.(png|bmp|jpeg|jpg|ico|gif)$/, // icon-16.png
/^icon-([0-9]+)x[0-9]+\.(png|bmp|jpeg|jpg|ico|gif)$/, // icon-16x16.png
/^icon@([0-9]+)w\.(png|bmp|jpeg|jpg|ico|gif)$/, // icon@16w.png
/^icon@([0-9]+)h\.(png|bmp|jpeg|jpg|ico|gif)$/, // icon@16h.png
/^icon@([0-9]+)\.(png|bmp|jpeg|jpg|ico|gif)$/, // icon@16.png
/^icon[\/\\]([0-9]+)\.(png|bmp|jpeg|jpg|ico|gif)$/, // icon/16.png
/^icon[\/\\]([0-9]+)x[0-9]+\.(png|bmp|jpeg|jpg|ico|gif)$/, // icon/16x16.png
/^icon-([0-9]+)\.png$/, // icon-16.png
/^icon-([0-9]+)x[0-9]+\.png$/, // icon-16x16.png
/^icon@([0-9]+)w\.png$/, // icon@16w.png
/^icon@([0-9]+)h\.png$/, // icon@16h.png
/^icon@([0-9]+)\.png$/, // icon@16.png
/^icons?[\/\\]([0-9]+)\.png$/, // icon/16.png | icons/16.png
/^icons?[\/\\]([0-9]+)x[0-9]+\.png$/, // icon/16x16.png | icons/16x16.png
];
// #endregion snippet
+1 -1
View File
@@ -26,7 +26,7 @@ export async function isOnline(): Promise<boolean> {
*/
export async function fetchCached(
url: string,
config: InternalConfig,
config: Omit<InternalConfig, 'builder'>,
): Promise<string> {
let content: string = '';
+4 -4
View File
@@ -1,19 +1,19 @@
import nodePath from 'node:path';
import * as vite from 'vite';
import systemPath from 'node:path';
import normalize from 'normalize-path';
/**
* Converts system paths to normalized bundler path. On windows and unix, this returns paths with /
* instead of \.
*/
export function normalizePath(path: string): string {
return vite.normalizePath(path);
return normalize(path);
}
/**
* Given a normalized path, convert it to the system path style. On Windows, switch to \, otherwise use /.
*/
export function unnormalizePath(path: string): string {
return nodePath.normalize(path);
return systemPath.normalize(path);
}
export const CSS_EXTENSIONS = ['css', 'scss', 'sass', 'less', 'styl', 'stylus'];
+15 -43
View File
@@ -4,7 +4,6 @@
import { resolve } from 'path';
import { faker } from '@faker-js/faker';
import merge from 'lodash.merge';
import { Rollup } from 'vite';
import type { Manifest } from 'webextension-polyfill';
import {
FsCache,
@@ -15,6 +14,9 @@ import {
GenericEntrypoint,
OptionsEntrypoint,
PopupEntrypoint,
OutputChunk,
OutputFile,
OutputAsset,
} from '~/types';
import { mock } from 'vitest-mock-extended';
@@ -140,49 +142,19 @@ export const fakeGenericEntrypoint = fakeObjectCreator<GenericEntrypoint>(
}),
);
export const fakeRollupOutputChunk = fakeObjectCreator<Rollup.OutputChunk>(
() => ({
type: 'chunk',
code: '',
dynamicImports: [],
exports: [],
facadeModuleId: faker.helpers.arrayElement([null, fakeFile()]),
fileName: faker.string.alphanumeric(),
implicitlyLoadedBefore: [],
importedBindings: {},
imports: [],
isDynamicEntry: faker.datatype.boolean(),
isEntry: faker.datatype.boolean(),
isImplicitEntry: faker.datatype.boolean(),
map: null,
moduleIds: [],
modules: {},
name: faker.string.alpha(),
referencedFiles: [],
viteMetadata: {
importedAssets: new Set(),
importedCss: new Set(),
},
preliminaryFileName: faker.string.alphanumeric(),
sourcemapFileName: null,
}),
);
export const fakeOutputChunk = fakeObjectCreator<OutputChunk>(() => ({
type: 'chunk',
fileName: faker.string.alphanumeric(),
moduleIds: [],
}));
export const fakeRollupOutputAsset = fakeObjectCreator<Rollup.OutputAsset>(
() => ({
type: 'asset',
fileName: fakeFileName(),
name: faker.string.alpha(),
needsCodeReference: faker.datatype.boolean(),
source: '',
}),
);
export const fakeOutputAsset = fakeObjectCreator<OutputAsset>(() => ({
type: 'asset',
fileName: fakeFileName(),
}));
export function fakeRollupOutput(): Rollup.OutputAsset | Rollup.OutputChunk {
return faker.helpers.arrayElement([
fakeRollupOutputAsset(),
fakeRollupOutputChunk(),
]);
export function fakeOutputFile(): OutputFile {
return faker.helpers.arrayElement([fakeOutputAsset(), fakeOutputChunk()]);
}
export const fakeManifest = fakeObjectCreator<Manifest.WebExtensionManifest>(
@@ -228,7 +200,6 @@ export const fakeInternalConfig = fakeObjectCreator<InternalConfig>(() => {
debug: faker.datatype.boolean(),
srcDir: fakeDir(),
typesDir: fakeDir(),
vite: () => ({}),
wxtDir: fakeDir(),
server: mock<WxtDevServer>(),
analysis: {
@@ -248,5 +219,6 @@ export const fakeInternalConfig = fakeObjectCreator<InternalConfig>(() => {
experimental: {
includeBrowserPolyfill: true,
},
builder: mock(),
};
});
+8
View File
@@ -0,0 +1,8 @@
/**
* Remove optional from key, but keep undefined if present
*
* @example
* type Test = NullablyRequired<{a?: string, b: number}>
* // type Test = {a: string | undefined, b: number}
*/
export type NullablyRequired<T> = { [K in keyof Required<T>]: T[K] };
+4 -6
View File
@@ -1,9 +1,9 @@
import { UnimportOptions } from 'unimport';
import { InternalConfig } from '~/types';
import { mergeConfig } from 'vite';
import { defu } from 'defu';
export function getUnimportOptions(
config: InternalConfig,
config: Omit<InternalConfig, 'builder'>,
): Partial<UnimportOptions | false> {
if (config.imports === false) return false;
@@ -17,13 +17,11 @@ export function getUnimportOptions(
{ package: 'wxt/client' },
{ package: 'wxt/browser' },
{ package: 'wxt/sandbox' },
{ package: 'wxt/storage' },
],
warn: config.logger.warn,
dirs: ['components', 'composables', 'hooks', 'utils'],
};
return mergeConfig(
defaultOptions,
config.imports,
) as Partial<UnimportOptions>;
return defu(config.imports, defaultOptions);
}
+118
View File
@@ -0,0 +1,118 @@
/**
* @module wxt/storage
*/
import {
Driver,
WatchCallback,
createStorage,
defineDriver,
Storage,
} from 'unstorage';
import browser, { Storage as BrowserStorage } from 'webextension-polyfill';
export interface WebExtensionDriverOptions {
storageArea: 'sync' | 'local' | 'managed' | 'session';
}
export const webExtensionDriver: (opts: WebExtensionDriverOptions) => Driver =
defineDriver((opts) => {
const checkPermission = () => {
if (browser.storage == null)
throw Error(
"You must request the 'storage' permission to use webExtensionDriver",
);
};
const _storageListener: (
changes: BrowserStorage.StorageAreaSyncOnChangedChangesType,
) => void = (changes) => {
Object.entries(changes).forEach(([key, { newValue }]) => {
_listeners.forEach((callback) => {
callback(newValue ? 'update' : 'remove', key);
});
});
};
const _listeners = new Set<WatchCallback>();
return {
name: 'web-extension:' + opts.storageArea,
async hasItem(key) {
checkPermission();
const res = await browser.storage[opts.storageArea].get(key);
return res[key] != null;
},
async getItem(key) {
checkPermission();
const res = await browser.storage[opts.storageArea].get(key);
return res[key] ?? null;
},
async getItems(items) {
checkPermission();
const res = await browser.storage[opts.storageArea].get(
items.map((item) => item.key),
);
return items.map((item) => ({
key: item.key,
value: res[item.key] ?? null,
}));
},
async setItem(key, value) {
checkPermission();
await browser.storage[opts.storageArea].set({ [key]: value ?? null });
},
async setItems(items) {
checkPermission();
const map = items.reduce<Record<string, any>>((map, item) => {
map[item.key] = item.value ?? null;
return map;
}, {});
await browser.storage[opts.storageArea].set(map);
},
async removeItem(key) {
checkPermission();
await browser.storage[opts.storageArea].remove(key);
},
async getKeys() {
checkPermission();
const all = await browser.storage[opts.storageArea].get();
return Object.keys(all);
},
async clear() {
checkPermission();
await browser.storage[opts.storageArea].clear();
},
watch(callback) {
checkPermission();
_listeners.add(callback);
if (_listeners.size === 1) {
browser.storage[opts.storageArea].onChanged.addListener(
_storageListener,
);
}
return () => {
_listeners.delete(callback);
if (_listeners.size === 0) {
browser.storage[opts.storageArea].onChanged.removeListener(
_storageListener,
);
}
};
},
};
});
function createWebExtensionStorage() {
const storage = createStorage();
storage.mount('local', webExtensionDriver({ storageArea: 'local' }));
storage.mount('session', webExtensionDriver({ storageArea: 'session' }));
storage.mount('sync', webExtensionDriver({ storageArea: 'sync' }));
storage.mount('managed', webExtensionDriver({ storageArea: 'managed' }));
return storage;
}
export type StorageValue = null | string | number | boolean | object;
export const storage: Storage<StorageValue> = createWebExtensionStorage();
export * from 'unstorage';
+1 -1
View File
@@ -6,7 +6,7 @@ import {
globals,
webextensionPolyfillAlias,
webextensionPolyfillInlineDeps,
} from '../core/vite-plugins';
} from '../core/builders/vite/plugins';
import { getInternalConfig } from '~/core/utils/building';
import { InlineConfig } from '../types';
+138 -40
View File
@@ -1,9 +1,10 @@
import * as vite from 'vite';
import type * as vite from 'vite';
import type { Manifest, Scripting } from 'webextension-polyfill';
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 {
/**
@@ -30,6 +31,12 @@ export interface InlineConfig {
* @default "${config.srcDir}/entrypoints"
*/
entrypointsDir?: 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
*
@@ -83,17 +90,6 @@ export interface InlineConfig {
* consola
*/
logger?: Logger;
/**
* 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>;
/**
* Customize the `manifest.json` output. Can be an object, promise, or function that returns an
* object or promise.
@@ -194,10 +190,6 @@ export interface InlineConfig {
* 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.
*
* Passed into Vite's
* [`resolve.alias`](https://vitejs.dev/config/shared-options.html#resolve-alias) option and used
* to generate the `.wxt/tsconfig.json`.
*
* The key is the import alias and the value is either a relative path to the root directory or an absolute path.
*
* @example
@@ -233,43 +225,83 @@ export interface InlineConfig {
};
}
export interface WxtInlineViteConfig
extends Omit<vite.InlineConfig, 'root' | 'configFile' | 'mode' | 'build'> {
build?: Omit<vite.BuildOptions, 'outDir'>;
// 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: vite.Rollup.OutputAsset[];
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: (vite.Rollup.OutputChunk | vite.Rollup.OutputAsset)[];
chunks: OutputFile[];
}
export interface WxtDevServer extends vite.ViteDevServer {
/**
* Ex: `3000`
*/
port: number;
/**
* Ex: `"localhost"`
*/
hostname: string;
/**
* Ex: `"http://localhost:3000"`
*/
origin: string;
export interface WxtDevServer
extends Omit<WxtBuilderServer, 'listen'>,
ServerInfo {
/**
* Stores the current build output of the server.
*/
currentOutput: BuildOutput;
/**
* Start the server on the first open port.
* Start the server.
*/
start(): 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`.
*/
@@ -327,7 +359,7 @@ export interface BaseEntrypoint {
* - `named.sandbox.html` &rarr; `named`
* - `named.sandbox/index.html` &rarr; `named`
* - `sandbox.html` &rarr; `sandbox`
* - `sandbox.index.html` &rarr; `sandbox`
* - `sandbox/index.html` &rarr; `sandbox`
* - `overlay.content.ts` &rarr; `overlay`
* - `overlay.content/index.ts` &rarr; `overlay`
*
@@ -590,7 +622,73 @@ export interface ExtensionRunnerConfig {
startUrls?: string[];
}
export type WxtViteConfig = Omit<
vite.UserConfig,
'root' | 'configFile' | 'mode'
>;
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>;
/**
* 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;
}
+2 -2
View File
@@ -1,4 +1,3 @@
import * as vite from 'vite';
import {
WxtDevServer,
Logger,
@@ -8,6 +7,7 @@ import {
ExtensionRunnerConfig,
ConfigEnv,
UserConfig,
WxtBuilder,
} from './external';
import { UnimportOptions } from 'unimport';
import { ResolvedConfig } from 'c12';
@@ -31,7 +31,6 @@ export interface InternalConfig {
env: ConfigEnv;
logger: Logger;
imports: false | Partial<UnimportOptions>;
vite: (env: ConfigEnv) => Promise<vite.InlineConfig> | vite.InlineConfig;
manifest: UserManifest;
fsCache: FsCache;
server?: WxtDevServer;
@@ -56,6 +55,7 @@ export interface InternalConfig {
experimental: {
includeBrowserPolyfill: boolean;
};
builder: WxtBuilder;
}
export interface FsCache {
+3 -3
View File
@@ -21,8 +21,8 @@
"devDependencies": {
"@types/react": "^18.2.14",
"@types/react-dom": "^18.2.6",
"@vitejs/plugin-react": "^4.0.3",
"typescript": "^5.1.6",
"wxt": "^0.9.0"
"@vitejs/plugin-react": "^4.2.1",
"typescript": "^5.3.2",
"wxt": "^0.10.0"
}
}
+3 -2
View File
@@ -18,8 +18,9 @@
"solid-js": "^1.7.8"
},
"devDependencies": {
"typescript": "^5.1.6",
"typescript": "^5.3.2",
"vite": "^4.0.0",
"vite-plugin-solid": "^2.7.0",
"wxt": "^0.9.0"
"wxt": "^0.10.0"
}
}
+3 -3
View File
@@ -14,12 +14,12 @@
"postinstall": "wxt prepare"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^2.4.2",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@tsconfig/svelte": "^5.0.0",
"svelte": "^4.0.5",
"svelte-check": "^3.4.6",
"tslib": "^2.6.0",
"typescript": "^5.1.6",
"wxt": "^0.9.0"
"typescript": "^5.3.2",
"wxt": "^0.10.0"
}
}
+2 -2
View File
@@ -15,7 +15,7 @@
"postinstall": "wxt prepare"
},
"devDependencies": {
"typescript": "^5.1.6",
"wxt": "^0.9.0"
"typescript": "^5.3.2",
"wxt": "^0.10.0"
}
}
+4 -4
View File
@@ -18,9 +18,9 @@
"vue": "^3.3.4"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.2.3",
"typescript": "^5.1.6",
"vue-tsc": "^1.8.4",
"wxt": "^0.9.0"
"@vitejs/plugin-vue": "^4.5.1",
"typescript": "^5.3.2",
"vue-tsc": "^1.8.25",
"wxt": "^0.10.0"
}
}
+4 -3
View File
@@ -1,7 +1,8 @@
import { defineWorkspace } from 'vitest/config';
import fs from 'fs-extra';
import pc from 'picocolors';
import { Plugin } from 'vite';
import type { Plugin } from 'vite';
import path from 'node:path';
const seed = Math.round(Math.random() * Number.MAX_SAFE_INTEGER);
console.info('Test seed: ' + pc.cyan(seed));
@@ -17,8 +18,8 @@ const testSeed = (): Plugin => ({
const resolve = {
alias: {
'~': 'src',
'webextension-polyfill': './src/virtual/mock-browser',
'~': path.resolve('src'),
'webextension-polyfill': path.resolve('src/virtual/mock-browser'),
},
};