Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ba41691296 | |||
| 8786aa91b3 | |||
| 0623d10653 | |||
| 85174d4994 | |||
| 4e2dd5d618 | |||
| 1a39a0d153 | |||
| f922648dd7 | |||
| e6142e3608 | |||
| ed73451fd0 | |||
| 8a25a2f60f | |||
| 397e9a8b45 | |||
| 7cef7680de | |||
| 0c57375758 | |||
| 2c6af84165 | |||
| 58e1371701 | |||
| 0d8e7463b8 | |||
| 77eeacaf0f | |||
| e80c134150 | |||
| 03c8ab4d6f | |||
| df13b9705c | |||
| 762ba0080d | |||
| 317b1b6dcc | |||
| 93175a6477 | |||
| 60d6707b11 | |||
| 97cbda3dab | |||
| 1611c1dba6 | |||
| b59252284a | |||
| 742b99657a | |||
| e2997a43e0 | |||
| 21dead60fd | |||
| 3d1bc0a12c | |||
| 4b24bee1fb | |||
| 82ed821eb5 | |||
| 60625280c2 | |||
| 083792b5f0 | |||
| 3da3e07bda | |||
| 64d61eb3f1 | |||
| aa6009e476 | |||
| d2cb8f9416 | |||
| 046a4809d3 | |||
| 207b750d4e | |||
| 077fa9939e | |||
| 44d715a29e | |||
| 3ab9fe4ae3 | |||
| 91b28c2cb9 | |||
| d1b523061f | |||
| c69ea3967f | |||
| 41527a2200 | |||
| 281f28192d | |||
| 67ffa44f9c | |||
| 421c0e412d | |||
| d21ee08eb6 | |||
| 7fa150dfae | |||
| 7ba52b18fd | |||
| 66e5079b84 | |||
| f1e8084be8 | |||
| 609ae2ae37 | |||
| c81dfff37c | |||
| 9c27820add | |||
| 22f8e10918 | |||
| 96d41f8b7c | |||
| c4ce44e001 | |||
| 6641ffaeed | |||
| 450dc0975f | |||
| d343555f41 | |||
| 7993908c6c | |||
| 94e1dbce24 | |||
| d14215946a | |||
| 345406f02a |
@@ -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 }}
|
||||
@@ -0,0 +1,22 @@
|
||||
name: Sync Releases
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- CHANGELOG.md
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
cache: pnpm
|
||||
- run: pnpm sync-releases all --token ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -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 }}
|
||||
|
||||
+211
-2
@@ -1,5 +1,214 @@
|
||||
# Changelog
|
||||
|
||||
## v0.11.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.11.0...v0.11.1)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add util for detecting URL changes in content scripts ([#264](https://github.com/wxt-dev/wxt/pull/264))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Upgrade templates to `wxt@^0.11.0` ([#263](https://github.com/wxt-dev/wxt/pull/263))
|
||||
|
||||
## 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
|
||||
|
||||
- You will need to update any other Vite plugins to a version that supports Vite 5 ([#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)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Experimental option to exclude `webextension-polyfill` ([#231](https://github.com/wxt-dev/wxt/pull/231))
|
||||
|
||||
### 🤖 CI
|
||||
|
||||
- Fix sync-release workflow ([d1b5230](https://github.com/wxt-dev/wxt/commit/d1b5230))
|
||||
|
||||
## v0.9.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.9.0...v0.9.1)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add `alias` config for customizing path aliases ([#216](https://github.com/wxt-dev/wxt/pull/216))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Move `webextension-polyfill` from peer to regular dependencies ([609ae2a](https://github.com/wxt-dev/wxt/commit/609ae2a))
|
||||
- Generate valid manifest for Firefox MV3 ([#229](https://github.com/wxt-dev/wxt/pull/229))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Add examples ([c81dfff](https://github.com/wxt-dev/wxt/commit/c81dfff))
|
||||
- Improve the "Used By" section on homepage ([#220](https://github.com/wxt-dev/wxt/pull/220))
|
||||
- Add UltraWideo to homepage ([#193](https://github.com/wxt-dev/wxt/pull/193))
|
||||
- Add StayFree to homepage ([#221](https://github.com/wxt-dev/wxt/pull/221))
|
||||
- Update feature comparison ([67ffa44](https://github.com/wxt-dev/wxt/commit/67ffa44))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Remove whitespace from genearted `.wxt` files ([#211](https://github.com/wxt-dev/wxt/pull/211))
|
||||
- Upgrade templates to `wxt@^0.9.0` ([#214](https://github.com/wxt-dev/wxt/pull/214))
|
||||
- Update Vite dependency range to `^4.0.0 || ^5.0.0-0` ([f1e8084](https://github.com/wxt-dev/wxt/commit/f1e8084be89e512dde441b9197a99183c497f67d))
|
||||
|
||||
### 🤖 CI
|
||||
|
||||
- Automatically sync GitHub releases with `CHANGELOG.md` on push ([#218](https://github.com/wxt-dev/wxt/pull/218))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aaronklinker-st](http://github.com/aaronklinker-st))
|
||||
|
||||
## v0.9.0
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.8.7...v0.9.0)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- ⚠️ Remove `lib` from `.wxt/tsconfig.json` ([#209](https://github.com/wxt-dev/wxt/pull/209))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Fix heading ([345406f](https://github.com/wxt-dev/wxt/commit/345406f))
|
||||
- Add demo video ([#208](https://github.com/wxt-dev/wxt/pull/208))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Fix Svelte and React template READMEs ([#207](https://github.com/wxt-dev/wxt/pull/207))
|
||||
|
||||
#### ⚠️ Breaking Changes
|
||||
|
||||
- ⚠️ Removed [`"WebWorker"` types](https://www.typescriptlang.org/tsconfig/lib.html) from `.wxt/tsconfig.json` ([#209](https://github.com/wxt-dev/wxt/pull/209)). These types are useful for MV3 projects using a service worker. To add them back to your project, add the following to your project's TSConfig:
|
||||
```diff
|
||||
{
|
||||
"extends": "./.wxt/tsconfig.json",
|
||||
+ "compilerOptions": {
|
||||
+ "lib": ["ESNext", "DOM", "WebWorker"]
|
||||
+ }
|
||||
}
|
||||
```
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- yyyanghj ([@yyyanghj](https://github.com/yyyanghj))
|
||||
|
||||
## v0.8.7
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.8.6...v0.8.7)
|
||||
@@ -60,7 +269,7 @@
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Nenad Novaković
|
||||
- Nenad Novaković ([@dvlden](https://github.com/dvlden))
|
||||
|
||||
## v0.8.4
|
||||
|
||||
@@ -125,7 +334,7 @@
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- 渣渣120 <WOSHIZHAZHA120@qq.com>
|
||||
- 渣渣120 [@WOSHIZHAZHA120](https://github.com/WOSHIZHAZHA120)
|
||||
|
||||
## v0.7.5
|
||||
|
||||
|
||||
+1
-1
@@ -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": "../.."
|
||||
|
||||
@@ -4,18 +4,14 @@
|
||||
</h1>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/package/wxt" target="_blank">
|
||||
<img alt="npm" src="https://img.shields.io/npm/v/wxt?labelColor=black&color=%234fa048">
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/wxt" target="_blank"><img alt="npm" src="https://img.shields.io/npm/v/wxt?labelColor=black&color=%234fa048"></a>
|
||||
<span> </span>
|
||||
<img alt="npm" src="https://img.shields.io/npm/dm/wxt?labelColor=black&color=%234fa048">
|
||||
<a href="https://www.npmjs.com/package/wxt" target="_blank"><img alt="npm" src="https://img.shields.io/npm/dm/wxt?labelColor=black&color=%234fa048"></a>
|
||||
<span> </span>
|
||||
<img alt="NPM" src="https://img.shields.io/npm/l/wxt?labelColor=black&color=%234fa048">
|
||||
<a href="https://github.com/wxt-dev/wxt/blob/main/LICENSE" target="_blank"><img alt="NPM" src="https://img.shields.io/npm/l/wxt?labelColor=black&color=%234fa048"></a>
|
||||
<!-- Hide code coverage while it's broken -->
|
||||
<!-- <span> </span>
|
||||
<a href="https://codecov.io/github/wxt-dev/wxt" target="_blank">
|
||||
<img alt="Codecov" src="https://img.shields.io/codecov/c/github/wxt-dev/wxt?labelColor=black&color=%234fa048">
|
||||
</a> -->
|
||||
<a href="https://codecov.io/github/wxt-dev/wxt" target="_blank"><img alt="Codecov" src="https://img.shields.io/codecov/c/github/wxt-dev/wxt?labelColor=black&color=%234fa048"></a> -->
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -31,13 +27,17 @@
|
||||
•
|
||||
<a href="https://wxt.dev/guide/installation.html" target="_blank">Installation</a>
|
||||
•
|
||||
<a href="https://wxt.dev/entrypoints/background.html" target="_blank">Entrypoints</a>
|
||||
•
|
||||
<a href="https://wxt.dev/api/config.html" target="_blank">Configuration</a>
|
||||
•
|
||||
<a href="https://wxt.dev/examples.html" target="_blank">Examples</a>
|
||||
</p>
|
||||
|
||||

|
||||
|
||||
## Demo
|
||||
|
||||
https://github.com/wxt-dev/wxt/assets/10101283/07359e53-f491-43b6-8e8f-fae94aec8063
|
||||
|
||||
## Quick Start
|
||||
|
||||
Bootstrap a new project:
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import messages from 'public/_locales/en/messages.json';
|
||||
|
||||
export default defineBackground(() => {
|
||||
console.log(browser.runtime.id);
|
||||
logId();
|
||||
@@ -6,6 +8,7 @@ export default defineBackground(() => {
|
||||
chrome: __IS_CHROME__,
|
||||
firefox: __IS_FIREFOX__,
|
||||
manifestVersion: __MANIFEST_VERSION__,
|
||||
messages,
|
||||
});
|
||||
|
||||
// @ts-expect-error: should only accept entrypoints or public assets
|
||||
@@ -20,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());
|
||||
});
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export default defineContentScript({
|
||||
// Site that uses HTML5 history
|
||||
matches: ['*://*.crunchyroll.com/*'],
|
||||
|
||||
main(ctx) {
|
||||
ctx.addEventListener(window, 'wxt:locationchange', ({ newUrl, oldUrl }) => {
|
||||
console.log('Location changed:', newUrl.href, oldUrl.href);
|
||||
});
|
||||
console.log('Watching for location change...');
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
@@ -3,6 +3,7 @@ import { defineConfig } from 'wxt';
|
||||
export default defineConfig({
|
||||
srcDir: 'src',
|
||||
manifest: {
|
||||
permissions: ['storage'],
|
||||
default_locale: 'en',
|
||||
web_accessible_resources: [
|
||||
{
|
||||
@@ -11,4 +12,7 @@ export default defineConfig({
|
||||
},
|
||||
],
|
||||
},
|
||||
alias: {
|
||||
public: 'src/public',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
FROM lipanski/docker-static-website:latest
|
||||
COPY dist .
|
||||
@@ -8,14 +8,24 @@ import useListExtensionDetails, {
|
||||
const chromeExtensionIds = [
|
||||
'ocfdgncpifmegplaglcnglhioflaimkd', // GitHub: Better Line Counts
|
||||
'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);
|
||||
const sortedExtensions = computed(() =>
|
||||
!data.value
|
||||
? undefined
|
||||
: [...data.value].sort((l, r) => r.weeklyActiveUsers - l.weeklyActiveUsers),
|
||||
);
|
||||
const sortedExtensions = computed(() => {
|
||||
if (!data.value?.length) return [];
|
||||
|
||||
return [...data.value]
|
||||
.map((item) => ({
|
||||
...item,
|
||||
// Sort based on the user count weighted by the rating
|
||||
sortKey: ((item.rating ?? 5) / 5) * item.weeklyActiveUsers,
|
||||
}))
|
||||
.sort((l, r) => r.sortKey - l.sortKey);
|
||||
});
|
||||
|
||||
function getStoreUrl(extension: ChromeExtension) {
|
||||
const url = new URL(extension.storeUrl);
|
||||
@@ -27,32 +37,41 @@ function getStoreUrl(extension: ChromeExtension) {
|
||||
<template>
|
||||
<section class="vp-doc">
|
||||
<div class="container">
|
||||
<h2>Who's Using WXT?</h2>
|
||||
<h2 id="whos-using-wxt">Who's Using WXT?</h2>
|
||||
<p>
|
||||
Battle tested and ready for production. Explore chrome extensions made
|
||||
with WXT.
|
||||
</p>
|
||||
<ul>
|
||||
<li v-for="extension of sortedExtensions">
|
||||
<li
|
||||
v-for="extension of sortedExtensions"
|
||||
:key="extension.id"
|
||||
class="relative"
|
||||
>
|
||||
<img
|
||||
:src="extension.iconUrl"
|
||||
:alt="`${extension.name} icon`"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
<div>
|
||||
<div class="relative">
|
||||
<a
|
||||
:href="getStoreUrl(extension)"
|
||||
target="_blank"
|
||||
:title="extension.name"
|
||||
class="extension-name"
|
||||
>{{ extension.name }}</a
|
||||
>
|
||||
<p class="description" :title="extension.shortDescription">
|
||||
{{ extension.shortDescription }}
|
||||
</p>
|
||||
<p class="user-count">
|
||||
{{ extension.weeklyActiveUsers.toLocaleString() }} users
|
||||
</p>
|
||||
</div>
|
||||
<p class="user-count">
|
||||
<span>{{ extension.weeklyActiveUsers.toLocaleString() }} users</span
|
||||
><template v-if="extension.rating != null"
|
||||
>,
|
||||
<span>{{ extension.rating }} stars</span>
|
||||
</template>
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="centered pr">
|
||||
@@ -121,7 +140,8 @@ li {
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
flex: 1;
|
||||
gap: 16px;
|
||||
gap: 24px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.centered {
|
||||
@@ -136,9 +156,10 @@ li .description {
|
||||
}
|
||||
li .user-count {
|
||||
opacity: 70%;
|
||||
text-align: right;
|
||||
width: 100%;
|
||||
font-size: small;
|
||||
position: absolute;
|
||||
bottom: 12px;
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
li a {
|
||||
@@ -157,8 +178,6 @@ li a:hover {
|
||||
|
||||
li div {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
li .description {
|
||||
@@ -167,10 +186,18 @@ li .description {
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
flex-grow: 1;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
li .extension-name {
|
||||
font-size: large;
|
||||
}
|
||||
|
||||
.pr {
|
||||
opacity: 70%;
|
||||
}
|
||||
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface ChromeExtension {
|
||||
weeklyActiveUsers: number;
|
||||
shortDescription: string;
|
||||
storeUrl: string;
|
||||
rating: number | undefined;
|
||||
}
|
||||
|
||||
const operationName = 'WxtDocsUsedBy';
|
||||
@@ -18,6 +19,7 @@ const query = `query ${operationName}($ids:[String!]!) {
|
||||
weeklyActiveUsers
|
||||
shortDescription
|
||||
storeUrl
|
||||
rating
|
||||
}
|
||||
}`;
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ export default defineConfig({
|
||||
nav: [
|
||||
{ text: 'Guide', link: '/guide/installation.md' },
|
||||
{ text: 'Entrypoints', link: '/entrypoints/background.md' },
|
||||
{ text: 'Examples', link: '/examples.md' },
|
||||
{ text: 'API', link: '/api/cli.md' },
|
||||
],
|
||||
|
||||
@@ -89,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' },
|
||||
|
||||
@@ -293,7 +293,7 @@ Because the overlay UI type results in a 0px by 0px container being added to the
|
||||
|
||||
> TODO: Add visualization of the different alignments.
|
||||
|
||||
## UI
|
||||
## IFrame
|
||||
|
||||
WXT provides a utility function, `createContentScriptIframe` to simplify mounting a UI from a content script. It creates an iframe to an unlisted HTML page. Unlike `createContentScriptUi`, this API support HMR.
|
||||
|
||||
|
||||
@@ -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/) • [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
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# Examples
|
||||
|
||||
Simple walkthroughs to accomplish common tasks or patterns with WXT.
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
|
||||
const examples = ref()
|
||||
onMounted(async () => {
|
||||
const res = await fetch("https://raw.githubusercontent.com/wxt-dev/wxt-examples/main/examples.json");
|
||||
examples.value = await res.json();
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<ul>
|
||||
<li v-if="examples == null">
|
||||
Loading...
|
||||
</li>
|
||||
<template v-else>
|
||||
<li v-for="example of examples">
|
||||
<a :href="example.url" target="_blank">{{ example.name }}</a>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
|
||||
> Full code available at [`wxt-dev/wxt-examples`](https://github.com/wxt-dev/wxt-examples)
|
||||
@@ -27,9 +27,9 @@ 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 | ❌ | ✅ |
|
||||
| Messaging | ❌ | ✅ |
|
||||
| Content Script UI | ❌ | ✅ |
|
||||
| Storage | ✅ | ✅ |
|
||||
| Messaging | 🟡 Coming soon | ✅ |
|
||||
| Content Script UI | ✅ | ✅ |
|
||||
|
||||
## Dev Mode
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
:::
|
||||
+3
-2
@@ -76,8 +76,9 @@ features:
|
||||
<p>
|
||||
WXT's simplifies the chrome extension development process by providing tools for zipping and publishing, the best-in-class dev mode, an opinionated project structure, and more. Iterate faster, develop features not build scripts, and use everything the JS ecosystem has to offer.
|
||||
</p>
|
||||
<div style="margin: auto; width: 100%; max-width: 700px; text-align: center">
|
||||
<img src="./assets/cli-output.png" alt="Screenshot of WXT's build output" />
|
||||
<div style="margin: auto; width: 100%; max-width: 900px; text-align: center">
|
||||
<video src="https://github.com/wxt-dev/wxt/assets/10101283/b32e6766-ec11-45a4-9677-226ee4718e1c" controls></video>
|
||||
<br />
|
||||
<small>
|
||||
And who doesn't appreciate a beautiful CLI?
|
||||
</small>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"../src/client",
|
||||
"../src/browser.ts",
|
||||
"../src/sandbox",
|
||||
"../src/storage.ts",
|
||||
"../src/testing"
|
||||
],
|
||||
"plugin": ["typedoc-plugin-markdown", "typedoc-vitepress-theme"],
|
||||
|
||||
@@ -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" />
|
||||
"
|
||||
`,
|
||||
);
|
||||
|
||||
@@ -104,26 +104,64 @@ describe('Manifest Content', () => {
|
||||
})
|
||||
`;
|
||||
|
||||
it('should include a background script for mv2', async () => {
|
||||
it.each(['chrome', 'safari'])(
|
||||
'should include scripts and persistent for %s mv2',
|
||||
async (browser) => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/background.ts', backgroundContent);
|
||||
|
||||
await project.build({ browser, manifestVersion: 2 });
|
||||
const manifest = await project.getOutputManifest(
|
||||
`.output/${browser}-mv2/manifest.json`,
|
||||
);
|
||||
|
||||
expect(manifest.background).toEqual({
|
||||
persistent: true,
|
||||
scripts: ['background.js'],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each(['chrome', 'safari'])(
|
||||
'should include a service worker and type for %s mv3',
|
||||
async (browser) => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/background.ts', backgroundContent);
|
||||
|
||||
await project.build({ browser, manifestVersion: 3 });
|
||||
const manifest = await project.getOutputManifest(
|
||||
`.output/${browser}-mv3/manifest.json`,
|
||||
);
|
||||
|
||||
expect(manifest.background).toEqual({
|
||||
type: 'module',
|
||||
service_worker: 'background.js',
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('should include a background script and type for firefox mv3', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/background.ts', backgroundContent);
|
||||
|
||||
await project.build();
|
||||
const manifest = await project.getOutputManifest();
|
||||
await project.build({ browser: 'firefox', manifestVersion: 3 });
|
||||
const manifest = await project.getOutputManifest(
|
||||
'.output/firefox-mv3/manifest.json',
|
||||
);
|
||||
|
||||
expect(manifest.background).toEqual({
|
||||
type: 'module',
|
||||
service_worker: 'background.js',
|
||||
scripts: ['background.js'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should include a options_ui and browser_style for firefox', async () => {
|
||||
it('should include a background script and persistent for firefox mv2', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/background.ts', backgroundContent);
|
||||
|
||||
await project.build({ manifestVersion: 2 });
|
||||
await project.build({ browser: 'firefox', manifestVersion: 2 });
|
||||
const manifest = await project.getOutputManifest(
|
||||
'.output/chrome-mv2/manifest.json',
|
||||
'.output/firefox-mv2/manifest.json',
|
||||
);
|
||||
|
||||
expect(manifest.background).toEqual({
|
||||
@@ -137,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',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -320,7 +358,7 @@ describe('Manifest Content', () => {
|
||||
project.addFile(
|
||||
'entrypoints/content/index.ts',
|
||||
`import "./style.css";
|
||||
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ["https://*.google.com/*"],
|
||||
|
||||
@@ -333,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"]}]}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -346,7 +384,7 @@ describe('Manifest Content', () => {
|
||||
project.addFile(
|
||||
'entrypoints/content/index.ts',
|
||||
`import "./style.css";
|
||||
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ["https://*.google.com/*"],
|
||||
cssInjectionMode: "manifest",
|
||||
@@ -360,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"]}]}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -373,7 +411,7 @@ describe('Manifest Content', () => {
|
||||
project.addFile(
|
||||
'entrypoints/content/index.ts',
|
||||
`import "./style.css";
|
||||
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ["https://*.google.com/*"],
|
||||
cssInjectionMode: "manual",
|
||||
@@ -387,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"]}]}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -400,7 +438,7 @@ describe('Manifest Content', () => {
|
||||
project.addFile(
|
||||
'entrypoints/content/index.ts',
|
||||
`import "./style.css";
|
||||
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ["https://*.google.com/*"],
|
||||
cssInjectionMode: "ui",
|
||||
@@ -416,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"]}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -429,7 +467,7 @@ describe('Manifest Content', () => {
|
||||
project.addFile(
|
||||
'entrypoints/content/index.ts',
|
||||
`import "./style.css";
|
||||
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ["https://*.google.com/*"],
|
||||
cssInjectionMode: "ui",
|
||||
@@ -445,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/*"]}]}"
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -459,7 +497,7 @@ describe('Manifest Content', () => {
|
||||
project.addFile(
|
||||
'entrypoints/content/index.ts',
|
||||
`import "./style.css";
|
||||
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ["https://*.google.com/*"],
|
||||
cssInjectionMode: "ui",
|
||||
@@ -480,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"]}]}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -489,7 +527,7 @@ describe('Manifest Content', () => {
|
||||
project.addFile(
|
||||
'wxt.config.ts',
|
||||
`import { defineConfig } from 'wxt';
|
||||
|
||||
|
||||
export default defineConfig({
|
||||
transformManifest(manifest) {
|
||||
manifest.author = "Custom Author"
|
||||
@@ -505,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"}"
|
||||
`);
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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"]}]}"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
@@ -120,91 +120,91 @@ 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.
|
||||
*
|
||||
* \\"<ltr|rtl>\\"
|
||||
* 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>"
|
||||
*/
|
||||
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\\".
|
||||
*
|
||||
* \\"<rtl|ltr>\\"
|
||||
* If the @@bidi_dir is "ltr", then this is "rtl"; otherwise, it's "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\\".
|
||||
*
|
||||
* \\"<left|right>\\"
|
||||
* If the @@bidi_dir is "ltr", then this is "left"; otherwise, it's "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\\".
|
||||
*
|
||||
* \\"<right|left>\\"
|
||||
* If the @@bidi_dir is "ltr", then this is "right"; otherwise, it's "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,33 +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,
|
||||
\\"lib\\": [\\"DOM\\", \\"WebWorker\\"],
|
||||
\\"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"]
|
||||
}"
|
||||
`);
|
||||
});
|
||||
@@ -287,33 +286,81 @@ describe('TypeScript Project', () => {
|
||||
".wxt/tsconfig.json
|
||||
----------------------------------------
|
||||
{
|
||||
\\"compilerOptions\\": {
|
||||
\\"target\\": \\"ESNext\\",
|
||||
\\"module\\": \\"ESNext\\",
|
||||
\\"moduleResolution\\": \\"Bundler\\",
|
||||
\\"noEmit\\": true,
|
||||
\\"esModuleInterop\\": true,
|
||||
\\"forceConsistentCasingInFileNames\\": true,
|
||||
\\"resolveJsonModule\\": true,
|
||||
\\"strict\\": true,
|
||||
\\"lib\\": [\\"DOM\\", \\"WebWorker\\"],
|
||||
\\"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"]
|
||||
}"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should add additional path aliases listed in the alias config, preventing defaults from being overridden', async () => {
|
||||
const project = new TestProject();
|
||||
project.setConfigFileConfig({
|
||||
srcDir: 'src',
|
||||
alias: {
|
||||
example: 'example',
|
||||
'@': 'ignored-path',
|
||||
},
|
||||
});
|
||||
|
||||
await project.build();
|
||||
|
||||
const output = await project.serializeFile('.wxt/tsconfig.json');
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
".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/*"],
|
||||
"@@": [".."],
|
||||
"@@/*": ["../*"],
|
||||
"~~": [".."],
|
||||
"~~/*": ["../*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"../**/*",
|
||||
"./wxt.d.ts"
|
||||
],
|
||||
"exclude": ["../.output"]
|
||||
}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { TestProject } from '../utils';
|
||||
import { InlineConfig } from '~/types';
|
||||
|
||||
describe('User Config', () => {
|
||||
// Root directory is tested with all tests.
|
||||
@@ -28,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"}}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -54,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"}}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -82,7 +83,27 @@ 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"]}"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should exclude the polyfill when the experimental setting is set to false', async () => {
|
||||
const buildBackground = async (config?: InlineConfig) => {
|
||||
const background = `export default defineBackground(() => console.log(browser.runtime.id));`;
|
||||
const projectWithPolyfill = new TestProject();
|
||||
projectWithPolyfill.addFile('entrypoints/background.ts', background);
|
||||
await projectWithPolyfill.build(config);
|
||||
return await projectWithPolyfill.serializeFile(
|
||||
'.output/chrome-mv3/background.js',
|
||||
);
|
||||
};
|
||||
|
||||
const withPolyfill = await buildBackground();
|
||||
const withoutPolyfill = await buildBackground({
|
||||
experimental: {
|
||||
includeBrowserPolyfill: false,
|
||||
},
|
||||
});
|
||||
expect(withoutPolyfill).not.toBe(withPolyfill);
|
||||
});
|
||||
});
|
||||
|
||||
+3
-1
@@ -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
-24
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wxt",
|
||||
"type": "module",
|
||||
"version": "0.8.7",
|
||||
"version": "0.11.1",
|
||||
"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,45 +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.5.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",
|
||||
"webextension-polyfill": "^0.10.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"webextension-polyfill": ">=0.10.0"
|
||||
"vue": "^3.3.10"
|
||||
},
|
||||
"packageManager": "pnpm@8.6.3",
|
||||
"simple-git-hooks": {
|
||||
|
||||
Generated
+1385
-475
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
|
||||
@@ -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({});
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ContentScriptDefinition } from '~/types';
|
||||
import { browser } from '~/browser';
|
||||
import { logger } from '~/client/utils/logger';
|
||||
import { WxtLocationChangeEvent, getUniqueEventName } from './custom-events';
|
||||
import { createLocationWatcher } from './location-watcher';
|
||||
|
||||
/**
|
||||
* Implements [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController).
|
||||
@@ -14,6 +16,7 @@ export class ContentScriptContext implements AbortController {
|
||||
|
||||
#isTopFrame = window.self === window.top;
|
||||
#abortController: AbortController;
|
||||
#locationWatcher = createLocationWatcher(this);
|
||||
|
||||
constructor(
|
||||
private readonly contentScriptName: string,
|
||||
@@ -137,23 +140,42 @@ export class ContentScriptContext implements AbortController {
|
||||
/**
|
||||
* Call `target.addEventListener` and remove the event listener when the context is invalidated.
|
||||
*
|
||||
* Includes additional events useful for content scripts:
|
||||
*
|
||||
* - `"wxt:locationchange"` - Triggered when HTML5 history mode is used to change URL. Content
|
||||
* scripts are not reloaded when navigating this way, so this can be used to reset the content
|
||||
* script state on URL change, or run custom code.
|
||||
*
|
||||
* @example
|
||||
* ctx.addEventListener(window, "mousemove", () => {
|
||||
* // ...
|
||||
* });
|
||||
* ctx.addEventListener(document, "visibilitychange", () => {
|
||||
* // ...
|
||||
* });
|
||||
* ctx.addEventListener(document, "wxt:locationchange", () => {
|
||||
* // ...
|
||||
* });
|
||||
*/
|
||||
addEventListener(
|
||||
target: any,
|
||||
type: string,
|
||||
handler: (event: Event) => void,
|
||||
addEventListener<
|
||||
TTarget extends EventTarget,
|
||||
TType extends keyof WxtContentScriptEventMap,
|
||||
>(
|
||||
target: TTarget,
|
||||
type: TType,
|
||||
handler: (event: WxtContentScriptEventMap[TType]) => void,
|
||||
options?: AddEventListenerOptions,
|
||||
) {
|
||||
target.addEventListener?.(type, handler, options);
|
||||
this.onInvalidated(
|
||||
() => target.removeEventListener?.(type, handler, options),
|
||||
if (type === 'wxt:locationchange') {
|
||||
// Start the location watcher when adding the event for the first time
|
||||
if (this.isValid) this.#locationWatcher.run();
|
||||
}
|
||||
|
||||
target.addEventListener?.(
|
||||
type.startsWith('wxt:') ? getUniqueEventName(type) : type,
|
||||
// @ts-expect-error: Event don't match, but that's OK, EventTarget doesn't allow custom types in the callback
|
||||
handler,
|
||||
{
|
||||
...options,
|
||||
signal: this.signal,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -193,3 +215,7 @@ export class ContentScriptContext implements AbortController {
|
||||
this.onInvalidated(() => removeEventListener('message', cb));
|
||||
}
|
||||
}
|
||||
|
||||
interface WxtContentScriptEventMap extends WindowEventMap {
|
||||
'wxt:locationchange': WxtLocationChangeEvent;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { browser } from '~/browser';
|
||||
|
||||
export class WxtLocationChangeEvent extends Event {
|
||||
static EVENT_NAME = getUniqueEventName('wxt:locationchange');
|
||||
|
||||
constructor(
|
||||
readonly newUrl: URL,
|
||||
readonly oldUrl: URL,
|
||||
) {
|
||||
super(WxtLocationChangeEvent.EVENT_NAME, {});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an event name unique to the extension and content script that's running.
|
||||
*/
|
||||
export function getUniqueEventName(eventName: string): string {
|
||||
// During the build process, __ENTRYPOINT__ is not defined when importing entrypoints to get their
|
||||
// metadata.
|
||||
const entrypointName =
|
||||
typeof __ENTRYPOINT__ === 'undefined' ? 'build' : __ENTRYPOINT__;
|
||||
|
||||
return `${browser.runtime.id}:${entrypointName}:${eventName}`;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { ContentScriptContext } from '.';
|
||||
import { WxtLocationChangeEvent } from './custom-events';
|
||||
|
||||
/**
|
||||
* Create a util that watches for URL changes, dispatching the custom event when detected. Stops
|
||||
* watching when content script is invalidated.
|
||||
*/
|
||||
export function createLocationWatcher(ctx: ContentScriptContext) {
|
||||
let interval: number | undefined;
|
||||
let oldUrl: URL;
|
||||
|
||||
return {
|
||||
/**
|
||||
* Ensure the location watcher is actively looking for URL changes. If it's already watching,
|
||||
* this is a noop.
|
||||
*/
|
||||
run() {
|
||||
if (interval != null) return;
|
||||
|
||||
oldUrl = new URL(location.href);
|
||||
interval = ctx.setInterval(() => {
|
||||
let newUrl = new URL(location.href);
|
||||
if (newUrl.href !== oldUrl.href) {
|
||||
window.dispatchEvent(new WxtLocationChangeEvent(newUrl, oldUrl));
|
||||
oldUrl = newUrl;
|
||||
}
|
||||
}, 1e3);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
+1
-1
@@ -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',
|
||||
+5
-7
@@ -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:
|
||||
+6
-7
@@ -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,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { InternalConfig } from '~/types';
|
||||
import * as vite from 'vite';
|
||||
|
||||
/**
|
||||
* Apply the experimental config for disabling the polyfill. It works by aliasing the
|
||||
* `webextension-polyfill` module to a virtual module and exporting the `chrome` global from the
|
||||
* virtual module.
|
||||
*/
|
||||
export function excludeBrowserPolyfill(
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
): vite.Plugin {
|
||||
const virtualId = 'virtual:wxt-webextension-polyfill-disabled';
|
||||
|
||||
return {
|
||||
name: 'wxt:exclude-browser-polyfill',
|
||||
config() {
|
||||
// Only apply the config if we're disabling the polyfill
|
||||
if (config.experimental.includeBrowserPolyfill) return;
|
||||
|
||||
return {
|
||||
resolve: {
|
||||
alias: {
|
||||
'webextension-polyfill': virtualId,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
load(id) {
|
||||
if (id === virtualId) {
|
||||
// Use chrome instead of the polyfill when disabled.
|
||||
return 'export default chrome';
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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() {
|
||||
@@ -11,3 +11,5 @@ export * from './bundleAnalysis';
|
||||
export * from './globals';
|
||||
export * from './webextensionPolyfillAlias';
|
||||
export * from './webextensionPolyfillInlineDeps';
|
||||
export * from './excludeBrowserPolyfill';
|
||||
export * from './entrypointGroupGlobals';
|
||||
+26
-2
@@ -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
-2
@@ -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';
|
||||
@@ -0,0 +1,17 @@
|
||||
import { InternalConfig } from '~/types';
|
||||
import * as vite from 'vite';
|
||||
|
||||
export function tsconfigPaths(
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
): vite.Plugin {
|
||||
return {
|
||||
name: 'wxt:aliases',
|
||||
async config() {
|
||||
return {
|
||||
resolve: {
|
||||
alias: config.alias,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
+1
-1
@@ -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}`;
|
||||
+1
-1
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,6 +11,12 @@ export function createWebExtRunner(): ExtensionRunner {
|
||||
async openBrowser(config) {
|
||||
config.logger.info('Opening browser...');
|
||||
|
||||
if (config.browser === 'firefox' && config.manifestVersion === 3) {
|
||||
throw Error(
|
||||
'Dev mode does not support Firefox MV3. For alternatives, see https://github.com/wxt-dev/wxt/issues/230#issuecomment-1806881653',
|
||||
);
|
||||
}
|
||||
|
||||
// Use the plugin's logger instead of web-ext's built-in one.
|
||||
const webExtLogger = await import('web-ext-run/util/logger');
|
||||
webExtLogger.consoleStream.write = ({ level, msg, name }) => {
|
||||
|
||||
@@ -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"]]"`,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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()],
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
|
||||
@@ -135,7 +135,7 @@ declare module "wxt/browser" {
|
||||
const overrides = messages.map((message) => {
|
||||
return ` /**
|
||||
* ${message.description ?? 'No message description.'}
|
||||
*
|
||||
*
|
||||
* "${message.message}"
|
||||
*/
|
||||
getMessage(
|
||||
@@ -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,
|
||||
[
|
||||
@@ -195,8 +195,17 @@ async function writeTsConfigFile(
|
||||
config: InternalConfig,
|
||||
) {
|
||||
const dir = config.wxtDir;
|
||||
const rootPath = normalizePath(relative(dir, config.root));
|
||||
const srcPath = normalizePath(relative(dir, config.srcDir));
|
||||
const getTsconfigPath = (path: string) => normalizePath(relative(dir, path));
|
||||
const paths = Object.entries(config.alias)
|
||||
.flatMap(([alias, absolutePath]) => {
|
||||
const aliasPath = getTsconfigPath(absolutePath);
|
||||
return [
|
||||
` "${alias}": ["${aliasPath}"]`,
|
||||
` "${alias}/*": ["${aliasPath}/*"]`,
|
||||
];
|
||||
})
|
||||
.join(',\n');
|
||||
|
||||
await writeFileIfDifferent(
|
||||
resolve(dir, 'tsconfig.json'),
|
||||
`{
|
||||
@@ -209,24 +218,16 @@ async function writeTsConfigFile(
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"strict": true,
|
||||
"lib": ["DOM", "WebWorker"],
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"@": ["${srcPath}"],
|
||||
"@/*": ["${srcPath}/*"],
|
||||
"~": ["${srcPath}"],
|
||||
"~/*": ["${srcPath}/*"],
|
||||
"@@": ["${rootPath}"],
|
||||
"@@/*": ["${rootPath}/*"],
|
||||
"~~": ["${rootPath}"],
|
||||
"~~/*": ["${rootPath}/*"]
|
||||
${paths}
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"${normalizePath(relative(dir, config.root))}/**/*",
|
||||
"./${normalizePath(relative(dir, mainReference))}"
|
||||
"${getTsconfigPath(config.root)}/**/*",
|
||||
"./${getTsconfigPath(mainReference)}"
|
||||
],
|
||||
"exclude": ["${normalizePath(relative(dir, config.outBaseDir))}"]
|
||||
"exclude": ["${getTsconfigPath(config.outBaseDir)}"]
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>({
|
||||
@@ -80,8 +82,18 @@ export async function getInternalConfig(
|
||||
overrides: inlineConfig.runner,
|
||||
defaults: userConfig.runner,
|
||||
});
|
||||
// Make sure alias are absolute
|
||||
const alias = Object.fromEntries(
|
||||
Object.entries({
|
||||
...mergedConfig.alias,
|
||||
'@': srcDir,
|
||||
'~': srcDir,
|
||||
'@@': root,
|
||||
'~~': root,
|
||||
}).map(([key, value]) => [key, path.resolve(root, value)]),
|
||||
);
|
||||
|
||||
const finalConfig: InternalConfig = {
|
||||
const finalConfig: Omit<InternalConfig, 'builder'> = {
|
||||
browser,
|
||||
command,
|
||||
debug,
|
||||
@@ -100,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) {
|
||||
@@ -112,12 +123,24 @@ export async function getInternalConfig(
|
||||
template: mergedConfig.analysis?.template ?? 'treemap',
|
||||
},
|
||||
userConfigMetadata: userConfigMetadata ?? {},
|
||||
alias,
|
||||
experimental: {
|
||||
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(
|
||||
@@ -135,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 {
|
||||
@@ -180,21 +195,32 @@ 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,
|
||||
template:
|
||||
inlineConfig.analysis?.template ?? userConfig.analysis?.template,
|
||||
},
|
||||
alias: {
|
||||
...userConfig.alias,
|
||||
...inlineConfig.alias,
|
||||
},
|
||||
experimental: {
|
||||
...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,
|
||||
@@ -213,44 +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));
|
||||
|
||||
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,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();
|
||||
|
||||
@@ -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';
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
|
||||
+16
-14
@@ -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);
|
||||
@@ -154,7 +151,12 @@ function addEntrypoints(
|
||||
|
||||
if (background) {
|
||||
const script = getEntrypointBundlePath(background, config.outDir, '.js');
|
||||
if (manifest.manifest_version === 3) {
|
||||
if (config.browser === 'firefox' && config.manifestVersion === 3) {
|
||||
manifest.background = {
|
||||
type: background.options.type,
|
||||
scripts: [script],
|
||||
};
|
||||
} else if (config.manifestVersion === 3) {
|
||||
manifest.background = {
|
||||
type: background.options.type,
|
||||
service_worker: script,
|
||||
@@ -360,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
|
||||
|
||||
|
||||
@@ -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 = '';
|
||||
|
||||
|
||||
@@ -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'];
|
||||
|
||||
@@ -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: {
|
||||
@@ -244,5 +215,10 @@ export const fakeInternalConfig = fakeObjectCreator<InternalConfig>(() => {
|
||||
},
|
||||
transformManifest: () => {},
|
||||
userConfigMetadata: {},
|
||||
alias: {},
|
||||
experimental: {
|
||||
includeBrowserPolyfill: true,
|
||||
},
|
||||
builder: mock(),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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] };
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { InternalConfig } from '~/types';
|
||||
import * as vite from 'vite';
|
||||
|
||||
export function tsconfigPaths(config: InternalConfig): vite.Plugin {
|
||||
return {
|
||||
name: 'wxt:aliases',
|
||||
async config() {
|
||||
return {
|
||||
resolve: {
|
||||
alias: {
|
||||
'@@': config.root,
|
||||
'~~': config.root,
|
||||
'@': config.srcDir,
|
||||
'~': config.srcDir,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
+118
@@ -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';
|
||||
@@ -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';
|
||||
|
||||
|
||||
+175
-36
@@ -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.
|
||||
@@ -190,45 +186,122 @@ export interface InlineConfig {
|
||||
*/
|
||||
template?: PluginVisualizerOptions['template'];
|
||||
};
|
||||
/**
|
||||
* Add additional paths to the `.wxt/tsconfig.json`. Use this instead of overwriting the `paths`
|
||||
* in the root `tsconfig.json` if you want to add new paths.
|
||||
*
|
||||
* The key is the import alias and the value is either a relative path to the root directory or an absolute path.
|
||||
*
|
||||
* @example
|
||||
* {
|
||||
* "testing": "src/utils/testing.ts"
|
||||
* }
|
||||
*/
|
||||
alias?: Record<string, string>;
|
||||
/**
|
||||
* Experimental settings - use with caution.
|
||||
*/
|
||||
experimental?: {
|
||||
/**
|
||||
* Whether to use [`webextension-polyfill`](https://www.npmjs.com/package/webextension-polyfill)
|
||||
* when importing `browser` from `wxt/browser`.
|
||||
*
|
||||
* When set to `false`, WXT will export the chrome global instead of the polyfill from
|
||||
* `wxt/browser`.
|
||||
*
|
||||
* You should use `browser` to access the web extension APIs.
|
||||
*
|
||||
* @experimental This option will remain experimental until Manifest V2 is dead.
|
||||
*
|
||||
* @default true
|
||||
* @example
|
||||
* export default defineConfig({
|
||||
* experimental: {
|
||||
* includeBrowserPolyfill: false
|
||||
* }
|
||||
* })
|
||||
*/
|
||||
includeBrowserPolyfill?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
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`.
|
||||
*/
|
||||
@@ -286,7 +359,7 @@ export interface BaseEntrypoint {
|
||||
* - `named.sandbox.html` → `named`
|
||||
* - `named.sandbox/index.html` → `named`
|
||||
* - `sandbox.html` → `sandbox`
|
||||
* - `sandbox.index.html` → `sandbox`
|
||||
* - `sandbox/index.html` → `sandbox`
|
||||
* - `overlay.content.ts` → `overlay`
|
||||
* - `overlay.content/index.ts` → `overlay`
|
||||
*
|
||||
@@ -549,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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -49,6 +48,14 @@ export interface InternalConfig {
|
||||
template: NonNullable<PluginVisualizerOptions['template']>;
|
||||
};
|
||||
userConfigMetadata: Omit<ResolvedConfig<UserConfig>, 'config'>;
|
||||
/**
|
||||
* Import aliases to absolute paths.
|
||||
*/
|
||||
alias: Record<string, string>;
|
||||
experimental: {
|
||||
includeBrowserPolyfill: boolean;
|
||||
};
|
||||
builder: WxtBuilder;
|
||||
}
|
||||
|
||||
export interface FsCache {
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
# WXT + Vue 3
|
||||
# WXT + React
|
||||
|
||||
This template should help get you started developing with Vue 3 in WXT.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||
This template should help get you started developing with React in WXT.
|
||||
|
||||
@@ -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.8.0"
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"typescript": "^5.3.2",
|
||||
"wxt": "^0.11.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.8.0"
|
||||
"wxt": "^0.10.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Svelte + Vite
|
||||
# WXT + Svelte
|
||||
|
||||
This template should help get you started developing with Svelte in WXT.
|
||||
|
||||
|
||||
@@ -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.8.0"
|
||||
"typescript": "^5.3.2",
|
||||
"wxt": "^0.11.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"postinstall": "wxt prepare"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.1.6",
|
||||
"wxt": "^0.8.0"
|
||||
"typescript": "^5.3.2",
|
||||
"wxt": "^0.11.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.8.0"
|
||||
"@vitejs/plugin-vue": "^4.5.1",
|
||||
"typescript": "^5.3.2",
|
||||
"vue-tsc": "^1.8.25",
|
||||
"wxt": "^0.11.0"
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -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'),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user