Compare commits

...

99 Commits

Author SHA1 Message Date
GitHub Actions 8a25a2f60f chore(release): v0.10.4 2023-12-04 15:54:42 +00:00
Aaron 397e9a8b45 feat: Add config to customize outDir (#258) 2023-12-04 09:50:24 -06:00
dependabot[bot] 7cef7680de chore(deps): bump esbuild from 0.19.5 to 0.19.8 (#253)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-01 15:23:31 -06:00
dependabot[bot] 0c57375758 chore(deps-dev): bump tsx from 3.14.0 to 4.6.1 (#252)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-01 15:09:19 -06:00
dependabot[bot] 2c6af84165 chore(deps): bump rollup-plugin-visualizer from 5.9.2 to 5.9.3 (#256)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-01 15:08:06 -06:00
dependabot[bot] 58e1371701 chore(deps-dev): bump @types/lodash.merge from 4.6.8 to 4.6.9 (#255)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-01 15:06:58 -06:00
dependabot[bot] 0d8e7463b8 chore(deps-dev): bump prettier from 3.0.3 to 3.1.0 (#254)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-01 15:06:24 -06:00
Aaron 77eeacaf0f docs: Update sidepanel availability (#250) 2023-11-29 17:12:37 -06:00
冯不游 e80c134150 docs: Add Doozy to homepage (#249) 2023-11-29 07:21:46 -06:00
GitHub Actions 03c8ab4d6f chore(release): v0.10.3 2023-11-28 18:18:59 +00:00
Aaron df13b9705c fix(auto-imports): Don't add imports to node_module dependencies (#247) 2023-11-28 12:15:05 -06:00
Aaron 762ba0080d chore(deps): Upgrade to typescript 5.3 (#245) 2023-11-27 10:03:03 -06:00
Aaron Klinker 317b1b6dcc docs: Fix typo 2023-11-27 09:57:09 -06:00
Aaron 93175a6477 chore: Use defu for merging some config objects (#243) 2023-11-26 12:23:10 -06:00
Aaron 60d6707b11 chore: Use normalize-path instead of vite.normalizePath (#244) 2023-11-26 11:36:42 -06:00
Aaron Klinker 97cbda3dab ci: only print response headers from docs webhook 2023-11-25 14:57:04 -06:00
Aaron Klinker 1611c1dba6 ci: Publish docs on push to main 2023-11-25 14:51:30 -06:00
Aaron Klinker b59252284a Print verbose webhook 2023-11-25 14:48:05 -06:00
Aaron Klinker 742b99657a chore: Trigger docs upgrade via webhook 2023-11-25 14:45:41 -06:00
Aaron Klinker e2997a43e0 Fix docker push command 2023-11-24 14:18:27 -06:00
Aaron Klinker 21dead60fd Self-host docs 2023-11-24 14:16:18 -06:00
GitHub Actions 3d1bc0a12c chore(release): v0.10.2 2023-11-20 19:52:55 +00:00
Aaron Klinker 4b24bee1fb Update demo app logs 2023-11-20 13:47:31 -06:00
Aaron Klinker 82ed821eb5 fix: Apply mode option to build steps correctly 2023-11-20 13:47:18 -06:00
Aaron 60625280c2 chore: Upgrade templates to v0.10 (#239) 2023-11-16 16:50:30 -06:00
GitHub Actions 083792b5f0 chore(release): v0.10.1 2023-11-16 17:46:36 +00:00
Aaron Klinker 3da3e07bda fix: Remove WXT global to remove unused modules from production builds 2023-11-16 11:35:20 -06:00
Aaron Klinker 64d61eb3f1 Update changelog 2023-11-14 23:26:27 -06:00
GitHub Actions aa6009e476 chore(release): v0.10.0 2023-11-15 05:21:53 +00:00
Aaron d2cb8f9416 feat: Add wxt/storage API (#234) 2023-11-14 23:07:45 -06:00
Nenad Novaković 046a4809d3 feat!: Allow plural directory and only png's for manifest icons (#237)
BREAKING CHANGE: No longer discover icons with extensions other than `.png`. If you previously used `.jpg`, `.jpeg`, `.bmp`, or `.svg`, you'll need to convert your icons to `.png` files or manually add them to the manifest inside your `wxt.config.ts` file.
2023-11-14 23:07:09 -06:00
Aaron Klinker 207b750d4e docs: Update main README links 2023-11-12 10:37:56 -06:00
Aaron 077fa9939e feat: List bun as an experimental option in wxt init (#233) 2023-11-11 15:45:29 -06:00
Aaron 44d715a29e fix: Don't use bun to load entrypoint config (#232) 2023-11-11 14:46:13 -06:00
GitHub Actions 3ab9fe4ae3 chore(release): v0.9.2 2023-11-11 19:57:25 +00:00
Aaron 91b28c2cb9 feat: Experimental option to exclude webextension-polyfill (#231) 2023-11-11 13:53:47 -06:00
Aaron Klinker d1b523061f ci: Fix sync-release workflow 2023-11-11 12:49:40 -06:00
Aaron Klinker c69ea3967f Fix changelog 2023-11-11 12:47:32 -06:00
GitHub Actions 41527a2200 chore(release): v0.9.1 2023-11-11 18:27:09 +00:00
Aaron 281f28192d fix: Generate valid manifest for Firefox MV3 (#229) 2023-11-11 12:22:46 -06:00
Aaron 67ffa44f9c docs: Update feature comparison 2023-11-11 08:53:59 -06:00
Aaron Klinker 421c0e412d docs: Add StayFree to homepage (#221) 2023-11-08 10:51:42 -06:00
Aaron d21ee08eb6 docs: Add UltraWideo to homepage (#193) 2023-11-08 09:26:51 -06:00
Aaron 7fa150dfae docs: Improve the "Used By" section on homepage (#220) 2023-11-08 08:27:58 -06:00
Aaron 7ba52b18fd ci: Automatically sync GitHub releases with CHANGELOG.md on push (#218) 2023-11-07 13:08:46 -06:00
Aaron 66e5079b84 feat: Add alias config for customizing path aliases (#216) 2023-11-07 11:38:48 -06:00
Aaron Klinker f1e8084be8 chore: Update Vite dependency range to ^4.0.0 || ^5.0.0-0 2023-11-07 10:42:36 -06:00
Aaron Klinker 609ae2ae37 fix: Move webextension-polyfill from peer to regular dependencies
This closes https://github.com/wxt-dev/wxt/issues/215
2023-11-07 10:38:24 -06:00
Aaron Klinker c81dfff37c docs: Add examples 2023-11-05 11:35:59 -06:00
Aaron 9c27820add chore: Upgrade templates to wxt@^0.9.0 (#214) 2023-11-04 13:01:05 -05:00
Aaron 22f8e10918 chore: Remove whitespace from genearted .wxt files (#211) 2023-11-04 12:28:31 -05:00
Aaron Klinker 96d41f8b7c Update changelog 2023-11-04 11:17:32 -05:00
GitHub Actions c4ce44e001 chore(release): v0.9.0 2023-11-04 15:56:43 +00:00
yyyanghj 6641ffaeed fix!: Remove lib from .wxt/tsconfig.json (#209)
Co-authored-by: Aaron Klinker <aaronklinker1@gmail.com>
2023-11-04 10:50:52 -05:00
Aaron 450dc0975f Add CLI screenshot back to README 2023-11-04 09:12:06 -05:00
Aaron d343555f41 Update MP4 title 2023-11-04 01:34:25 -05:00
Aaron 7993908c6c docs: Add demo video (#208) 2023-11-04 01:32:19 -05:00
yyyanghj 94e1dbce24 chore: Fix Svelte and React template READMEs (#207) 2023-11-03 12:59:35 -05:00
Aaron d14215946a Cleanup README badge links 2023-11-03 11:46:14 -05:00
Aaron Klinker 345406f02a docs: Fix heading 2023-11-03 09:39:09 -05:00
GitHub Actions 6b689bab23 chore(release): v0.8.7 2023-11-03 14:36:36 +00:00
Aaron fcdf0dcbe9 feat: createContentScriptIframe utility (#206) 2023-11-03 09:31:16 -05:00
Aaron bd35acdae1 chore: Run E2E tests in parallel (#204) 2023-11-02 11:15:07 -05:00
Aaron 21c35ee593 ci: Separate validation into multiple jobs (#203) 2023-11-02 11:08:19 -05:00
Aaron Klinker 0f09cbee0e chore: Improve prettier git hook 2023-11-02 10:31:54 -05:00
Aaron Klinker 183bb02e29 chore(deps-dev): Update sass to 1.69.5 2023-11-02 10:26:14 -05:00
Aaron 0bf4ea0897 chore(deps): Upgrade vite to 4.5.0 (#202) 2023-11-02 10:24:50 -05:00
Aaron Klinker cd4d00e23e chore(deps-dev): Update @type/* packages for demo 2023-11-02 10:16:52 -05:00
Aaron Klinker 5de18e5371 chore(deps-dev): Upgrade vitepress to 1.0.0-rc.24 2023-11-02 10:04:33 -05:00
Aaron af823418ee chore(deps): Upgrade giget to ^1.1.3 (#199) 2023-11-02 09:11:17 -05:00
Aaron Klinker 0b8d101c33 chore(deps-dev): Upgrade vue to 3.3.7 2023-11-02 08:58:25 -05:00
Aaron b587849126 chore(deps-dev): Upgrade typedoc to ^0.25.3 (#201) 2023-11-02 08:57:16 -05:00
Aaron 2a35ce0a1e chore(deps-dev): Upgrade execa to ^8.0.1 (#200) 2023-11-02 08:57:06 -05:00
Aaron Klinker c390b70f76 chore(deps): Upgrade unimport to 3.4.0 2023-11-02 08:54:24 -05:00
Aaron Klinker c9fd739128 chore(deps): Upgrade linkedom to 0.16.1 2023-11-02 08:52:22 -05:00
Aaron Klinker 0806c06042 chore(deps): Upgrade jiti to 1.21.0 2023-11-02 08:51:14 -05:00
Aaron Klinker 5f74a544a4 chore(deps-dev): Upgrade lint-staged to ^15.0.2 2023-11-02 08:47:53 -05:00
Aaron Klinker 121778894c chore(deps): Upgrade immer to ^10.0.3 2023-11-02 08:46:25 -05:00
Aaron Klinker 102c72a03c chore(deps): Upgrade esbuild to ^0.19.5 2023-11-02 08:42:16 -05:00
Aaron Klinker a0507866db chore(deps): Upgrade c12 to ^1.5.1 2023-11-02 08:41:05 -05:00
Aaron Klinker f3874da780 chore: Upgrade types 2023-11-02 08:39:47 -05:00
dependabot[bot] cc5d24ec92 chore(deps): bump @types/webextension-polyfill from 0.10.0 to 0.10.5 (#196)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-02 08:05:50 -05:00
dependabot[bot] 08760015da chore(deps-dev): bump tsx from 3.12.8 to 3.14.0 (#198)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-02 08:02:53 -05:00
dependabot[bot] 5368371020 chore(deps): bump filesize from 10.0.12 to 10.1.0 (#197)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-02 08:01:50 -05:00
dependabot[bot] 08d62a437a chore(deps): bump is-wsl from 3.0.0 to 3.1.0 (#195)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-02 08:00:52 -05:00
dependabot[bot] 37e2348d79 chore(deps-dev): bump happy-dom from 12.4.0 to 12.10.3 (#194)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-02 07:59:55 -05:00
GitHub Actions 6a30dc46fa chore(release): v0.8.6 2023-10-30 15:41:46 +00:00
Aaron Klinker b75c553e51 fix: Inline WXT modules inside WxtVitest plugin 2023-10-30 10:25:22 -05:00
GitHub Actions a56face03d chore(release): v0.8.5 2023-10-30 14:54:59 +00:00
Aaron Klinker 19756c61ce docs: Add test.server.deps.inline to Vitest guide 2023-10-30 09:48:52 -05:00
Aaron 5f54b4de17 chore: Reduce package size by 70%, 1.92 MB to 590 kB (#190) 2023-10-29 23:00:10 -05:00
Aaron Klinker ea8935c47c Hide code coverage badge while it's broken 2023-10-29 21:24:21 -05:00
Aaron e37f73880d feat: Refactor project structure to export initialize, prepare, and zip functions (#182) 2023-10-29 21:19:31 -05:00
Aaron Klinker 2e24b9e18a chore: Update template docs 2023-10-29 20:24:38 -05:00
Aaron Klinker f8a0fb37a2 fix: Enable Vue SFC auto-imports in vue template 2023-10-29 20:24:27 -05:00
Nenad Novaković dd26b99027 docs: Update auto-imports.md (#186) 2023-10-29 08:29:54 -05:00
Aaron Klinker d9e9b43f8d docs: Improve runner.binaries documentation 2023-10-25 03:14:53 -05:00
GitHub Actions d580083727 chore(release): v0.8.4 2023-10-20 15:26:38 +00:00
Aaron 5a70d9e57d fix: Allow actions without a popup (#181) 2023-10-20 10:20:40 -05:00
181 changed files with 4091 additions and 2531 deletions
+17
View File
@@ -0,0 +1,17 @@
name: Basic Setup
description: Install PNPM, Node, and dependencies
runs:
using: composite
steps:
- name: Setup PNPM
uses: pnpm/action-setup@v2
with:
version: 8
- name: Setup NodeJS
uses: actions/setup-node@v4
with:
node-version: 18
cache: pnpm
- name: Install Dependencies
shell: bash
run: pnpm install
+27
View File
@@ -0,0 +1,27 @@
name: Publish Docs
on:
push:
branches:
- main
workflow_dispatch:
inputs:
tag:
description: Docker Image Tag
required: true
default: latest
jobs:
publish:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- uses: docker/login-action@v3
with:
registry: https://${{ secrets.DOCKER_REGISTRY_HOSTNAME }}
username: ${{ secrets.DOCKER_REGISTRY_USERNAME }}
password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
- run: pnpm docs:build
- run: docker build docs/.vitepress -t ${{ secrets.DOCKER_REGISTRY_HOSTNAME }}/wxt/docs:${{ github.event.inputs.tag || 'latest' }}
- run: docker push ${{ secrets.DOCKER_REGISTRY_HOSTNAME }}/wxt/docs:${{ github.event.inputs.tag || 'latest' }}
- run: curl -X POST -i ${{ secrets.UPDATE_DOCS_WEBHOOK }}
+2 -15
View File
@@ -11,24 +11,11 @@ jobs:
needs:
- validate
steps:
- name: Checkout
uses: actions/checkout@v3
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup PNPM
uses: pnpm/action-setup@v2
with:
version: 8
- name: Setup NodeJS
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- uses: ./.github/actions/setup
- name: Bump and Tag
run: |
+22
View File
@@ -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 }}
+56 -127
View File
@@ -7,141 +7,70 @@ on:
- main
jobs:
checks:
name: Checks
formatting:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup PNPM
uses: pnpm/action-setup@v2
with:
version: 8
- name: Setup NodeJS
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Formatting
run: pnpm format:check
- name: Type Check
run: pnpm compile
- name: Build Demo
run: |
pnpm build:all:chrome-mv2
pnpm build:all:chrome-mv3
pnpm build:all:firefox-mv2
pnpm build:all:firefox-mv3
pnpm tsc --noEmit
pnpm wxt zip
pnpm vitest run
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- run: pnpm format:check
type-check:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- run: pnpm compile
validate-demo:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- run: pnpm build:all
working-directory: demo
- name: Tests
- run: pnpm tsc --noEmit
working-directory: demo
- run: pnpm wxt zip
working-directory: demo
- run: pnpm vitest run
working-directory: demo
tests:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- name: pnpm test:coverage
run: pnpm test:coverage --reporter=default --reporter=hanging-process
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
- uses: codecov/codecov-action@v3
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
windows-tests:
name: Windows Tests
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup PNPM
uses: pnpm/action-setup@v2
with:
version: 8
- name: Setup NodeJS
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Tests
run: pnpm test:coverage --reporter=default --reporter=hanging-process
project-templates:
name: Project Templates
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- run: pnpm test run
template:
runs-on: ubuntu-22.04
strategy:
matrix:
template:
- react
- solid
- svelte
- vanilla
- vue
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup PNPM
uses: pnpm/action-setup@v2
with:
version: 8
- name: Setup NodeJS
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Build Local Tarball
run: pnpm pack
- name: Validate Vanilla
working-directory: templates/vanilla
run: |
npm i
npm i -D ../../wxt-*.tgz
npm ls vite
npm run build
npm run compile
- name: Validate Vue
working-directory: templates/vue
run: |
npm i
npm i -D ../../wxt-*.tgz
npm ls vite
npm run build
npm run compile
- name: Validate React
working-directory: templates/react
run: |
npm i
npm i -D ../../wxt-*.tgz
npm ls vite
npm run build
npm run compile
- name: Validate Svelte
working-directory: templates/svelte
run: |
npm i
npm i -D ../../wxt-*.tgz
npm ls vite
npm run build
npm run check
- name: Validate Solid
working-directory: templates/solid
run: |
npm i
npm i -D ../../wxt-*.tgz
npm ls vite
npm run build
npm run compile
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- run: pnpm pack
- run: npm i
working-directory: templates/${{ matrix.template }}
- run: npm i -D ../../wxt-*.tgz
working-directory: templates/${{ matrix.template }}
- run: npm run compile
if: matrix.template != 'svelte'
working-directory: templates/${{ matrix.template }}
- run: npm run check
if: matrix.template == 'svelte'
working-directory: templates/${{ matrix.template }}
- run: npm run build
working-directory: templates/${{ matrix.template }}
-9
View File
@@ -5,12 +5,3 @@ dist
docs/.vitepress/cache
pnpm-lock.yaml
CHANGELOG.md
.gitattributes
.gitignore
.prettierignore
*.png
*.svg
*.txt
_gitignore
_redirects
*.svelte
+243 -1
View File
@@ -1,5 +1,247 @@
# Changelog
## 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)
### 🚀 Enhancements
- `createContentScriptIframe` utility ([#206](https://github.com/wxt-dev/wxt/pull/206))
### 🏡 Chore
- **deps-dev:** Bump happy-dom from 12.4.0 to 12.10.3 ([#194](https://github.com/wxt-dev/wxt/pull/194))
- **deps-dev:** Bump tsx from 3.12.8 to 3.14.0 ([#198](https://github.com/wxt-dev/wxt/pull/198))
- Upgrade types ([f3874da](https://github.com/wxt-dev/wxt/commit/f3874da))
- **deps-dev:** Upgrade `lint-staged` to `^15.0.2` ([5f74a54](https://github.com/wxt-dev/wxt/commit/5f74a54))
- **deps-dev:** Upgrade `execa` to `^8.0.1` ([#200](https://github.com/wxt-dev/wxt/pull/200))
- **deps-dev:** Upgrade `typedoc` to `^0.25.3` ([#201](https://github.com/wxt-dev/wxt/pull/201))
- **deps-dev:** Upgrade `vue` to `3.3.7` ([0b8d101](https://github.com/wxt-dev/wxt/commit/0b8d101))
- **deps-dev:** Upgrade `vitepress` to `1.0.0-rc.24` ([5de18e5](https://github.com/wxt-dev/wxt/commit/5de18e5))
- **deps-dev:** Update `@type/*` packages for demo ([cd4d00e](https://github.com/wxt-dev/wxt/commit/cd4d00e))
- **deps-dev:** Update `sass` to `1.69.5` ([183bb02](https://github.com/wxt-dev/wxt/commit/183bb02))
- Improve prettier git hook ([0f09cbe](https://github.com/wxt-dev/wxt/commit/0f09cbe))
- Run E2E tests in parallel ([#204](https://github.com/wxt-dev/wxt/pull/204))
### 🤖 CI
- Separate validation into multiple jobs ([#203](https://github.com/wxt-dev/wxt/pull/203))
## v0.8.6
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.8.5...v0.8.6)
### 🩹 Fixes
- Inline WXT modules inside `WxtVitest` plugin ([b75c553](https://github.com/wxt-dev/wxt/commit/b75c553))
## v0.8.5
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.8.4...v0.8.5)
### 🚀 Enhancements
- Refactor project structure to export `initialize`, `prepare`, and `zip` functions ([#182](https://github.com/wxt-dev/wxt/pull/182))
### 🩹 Fixes
- Enable Vue SFC auto-imports in `vue` template ([f8a0fb3](https://github.com/wxt-dev/wxt/commit/f8a0fb3))
### 📖 Documentation
- Improve `runner.binaries` documentation ([d9e9b43](https://github.com/wxt-dev/wxt/commit/d9e9b43))
- Update auto-imports.md ([#186](https://github.com/wxt-dev/wxt/pull/186))
- Add `test.server.deps.inline` to Vitest guide ([19756c6](https://github.com/wxt-dev/wxt/commit/19756c6))
### 🏡 Chore
- Update template docs ([2e24b9e](https://github.com/wxt-dev/wxt/commit/2e24b9e))
- Reduce package size by 70%, 1.92 MB to 590 kB ([#190](https://github.com/wxt-dev/wxt/pull/190))
### ❤️ Contributors
- Nenad Novaković ([@dvlden](https://github.com/dvlden))
## v0.8.4
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.8.3...v0.8.4)
### 🩹 Fixes
- Allow actions without a popup ([#181](https://github.com/wxt-dev/wxt/pull/181))
## v0.8.3
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.8.2...v0.8.3)
@@ -55,7 +297,7 @@
### ❤️ Contributors
- 渣渣120 <WOSHIZHAZHA120@qq.com>
- 渣渣120 [@WOSHIZHAZHA120](https://github.com/WOSHIZHAZHA120)
## v0.7.5
+34
View File
@@ -70,3 +70,37 @@ pnpm test manifest-contents
Unit and E2E tests are ran together via [Vitest workspaces](https://vitest.dev/guide/#workspaces-support).
If you want to manually test a change, you can modify the demo project for your test, but please don't leave those changes committed once you open a PR.
## Templates
Each directory inside `templates/` is it's own standalone project. Simply `cd` into the directory you're updating, install dependencies with `npm` (NOT `pnpm`), and run the relevant commands
```sh
cd templates/vue
npm i
npm run dev
npm run build
```
Note that templates are hardcoded to a specific version of `wxt` from NPM, they do not use the local version. PR checks will test your changes against the templates, but if you want to manually do it, update the package.json dependency:
```diff
"devDependencies": {
"typescript": "^5.3.2",
"vite-plugin-solid": "^2.7.0",
- "wxt": "^0.8.0"
+ "wxt": "../.."
}
```
Then run `npm i` again.
### Adding Templates
To add a template, copy the vanilla template and give it a new name.
```sh
cp -r templates/vailla templates/<new-template-name>
```
That's it. Once your template is merged, it will be available inside `wxt init` immediately. You don't need to release a new version of WXT to release a new template.
+12 -11
View File
@@ -4,17 +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">
<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://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> -->
</p>
<p align="center">
@@ -30,13 +27,17 @@
&bull;
<a href="https://wxt.dev/guide/installation.html" target="_blank">Installation</a>
&bull;
<a href="https://wxt.dev/entrypoints/background.html" target="_blank">Entrypoints</a>
&bull;
<a href="https://wxt.dev/api/config.html" target="_blank">Configuration</a>
&bull;
<a href="https://wxt.dev/examples.html" target="_blank">Examples</a>
</p>
![Example CLI Output](./docs/assets/cli-output.png)
## Demo
https://github.com/wxt-dev/wxt/assets/10101283/07359e53-f491-43b6-8e8f-fae94aec8063
## Quick Start
Bootstrap a new project:
+6 -6
View File
@@ -1,12 +1,12 @@
{
"name": "WXT Demo",
"name": "demo",
"version": "1.0.0",
"description": "Demo extension for WXT",
"type": "module",
"scripts": {
"dev": "pnpm -w build && wxt",
"build": "pnpm -w build && wxt build",
"build:all": "pnpm -w build && run-s -s build:all:*",
"build:all": "pnpm -w build && run-s -s 'build:all:*'",
"build:all:chrome-mv3": "wxt build",
"build:all:chrome-mv2": "wxt build --mv2",
"build:all:firefox-mv3": "wxt build -b firefox --mv3",
@@ -23,10 +23,10 @@
"webextension-polyfill": "^0.10.0"
},
"devDependencies": {
"@types/react": "^18.2.14",
"@types/react-dom": "^18.2.6",
"@types/webextension-polyfill": "^0.10.0",
"sass": "^1.64.0",
"@types/react": "^18.2.34",
"@types/react-dom": "^18.2.14",
"@types/webextension-polyfill": "^0.10.5",
"sass": "^1.69.5",
"wxt": "workspace:*"
}
}
+10 -1
View File
@@ -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());
});
+5 -1
View File
@@ -6,7 +6,11 @@ export default defineContentScript({
console.log(browser.runtime.id);
logId();
console.log('WXT MODE:', import.meta.env.MODE);
console.log('WXT MODE:', {
MODE: import.meta.env.MODE,
DEV: import.meta.env.DEV,
PROD: import.meta.env.PROD,
});
const n = (Math.random() * 100).toFixed(1);
ctx.setInterval(() => {
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Iframe Example</title>
</head>
<body>
<p>Hello iframe page!</p>
<script type="module" src="./main.ts"></script>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
console.log('iframe 2');
+13
View File
@@ -0,0 +1,13 @@
export default defineContentScript({
matches: ['*://*.google.com/*'],
main(ctx) {
const ui = createContentScriptIframe(ctx, {
page: '/iframe-src.html',
type: 'overlay',
anchor: 'form[action="/search"]',
});
ui.mount();
console.log('Mounted iframe');
},
});
+5 -1
View File
@@ -4,4 +4,8 @@ console.log(browser.runtime.id);
logId();
console.log(2);
console.log('WXT MODE:', import.meta.env.MODE);
console.log('WXT MODE:', {
MODE: import.meta.env.MODE,
DEV: import.meta.env.DEV,
PROD: import.meta.env.PROD,
});
+11
View File
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sidebar</title>
</head>
<body>
<p>Example</p>
</body>
</html>
+10
View File
@@ -3,6 +3,16 @@ import { defineConfig } from 'wxt';
export default defineConfig({
srcDir: 'src',
manifest: {
permissions: ['storage'],
default_locale: 'en',
web_accessible_resources: [
{
resources: ['/iframe-src.html'],
matches: ['*://*.google.com/*'],
},
],
},
alias: {
public: 'src/public',
},
});
+2
View File
@@ -0,0 +1,2 @@
FROM lipanski/docker-static-website:latest
COPY dist .
+43 -17
View File
@@ -8,14 +8,23 @@ 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
];
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 +36,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 +139,8 @@ li {
background-color: var(--vp-c-bg-soft);
border-radius: 12px;
flex: 1;
gap: 16px;
gap: 24px;
align-items: center;
}
.centered {
@@ -136,9 +155,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 +177,6 @@ li a:hover {
li div {
flex: 1;
display: flex;
flex-direction: column;
}
li .description {
@@ -167,10 +185,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
}
}`;
+2
View File
@@ -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' },
+45 -2
View File
@@ -153,6 +153,8 @@ const anchor = document.querySelector('#anchor-selector');
anchor.append(ui);
```
You can try out [`createContentScriptIframe`](#createcontentscriptiframe) as an alternative solution
:::
### Usage
@@ -162,7 +164,7 @@ To use `createContentScriptUi`, follow these steps:
1. Import your CSS file at the top of your content script
2. Set `cssInjectionMode: "ui"` inside `defineContentScript`
3. Call `createContentScriptUi`
4. Call `ui.mount()` to add the UI to the webpage
4. Call `mount` to add the UI to the webpage
Here's a basic example:
@@ -185,7 +187,7 @@ export default defineContentScript({
},
});
// Yoy must call `mount` to add the UI to the page.
// You must call `mount` to add the UI to the page.
ui.mount();
},
});
@@ -290,3 +292,44 @@ There are 3 types of UI's you can mount.
Because the overlay UI type results in a 0px by 0px container being added to the webpage, the `alignment` option allows you to configure which corner of your UI is aligned with the 0x0 element.
> TODO: Add visualization of the different alignments.
## 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.
`createContentScriptIframe` requires a `ContentScriptContext` so that when the context is invalidated, the UI is automatically removed from the webpage.
### Usage
To use `createContentScriptIframe`, follow these steps:
1. Create an unlisted HTML page that will be loaded into your iframe
1. Add unlisted page to the manifest's `web_accessible_resouces`
1. Call `createContentScriptUi`
1. Call `mount` to add the UI to the webpage
Here's a basic example:
```ts
export default defineContentScript({
// ...
async main(ctx) {
const ui = await createContentScriptIframe(ctx, {
page: '/your-unlisted-page.html',
type: 'inline',
anchor: '#some-element',
append: 'after',
});
// You must call `mount` to add the UI to the page.
ui.mount();
},
});
```
The options, other than `page`, are the same as [`createContentScriptUi`](#anchor).
### `page`
The HTML page you want to load inside the iframe. This string will be passed into `browser.runtime.getURL` to resolve the full path of to your HTML page.
+3 -3
View File
@@ -1,9 +1,9 @@
# Side Panel
[Chrome Docs](https://developer.chrome.com/docs/extensions/reference/sidePanel/)
[Chrome Docs](https://developer.chrome.com/docs/extensions/reference/sidePanel/) &bull; [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/user_interface/Sidebars)
:::tip Chromium Only
Firefox does not support sidepanel pages.
:::warning
Chrome added support for sidepanels in Manifest V3, they are not available in Manfiest V2.
:::
## Filenames
+27
View File
@@ -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)
+1 -1
View File
@@ -64,7 +64,7 @@ import { defineConfig } from 'wxt';
export default defineConfig({
imports: {
// Add auto-imports for vue fuctions like createApp, ref, computed, watch, toRaw, etc...
preset: ['vue'],
presets: ['vue'],
},
});
```
+3 -3
View File
@@ -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
+8 -2
View File
@@ -39,7 +39,7 @@ export default defineRunnerConfig({
:::
You may also setup default for your entire computer by creating a `web-ext.config.ts` file in your home directory. This is useful if you want to specify config for all project on your computer, like that you want to use Chrome Beta instead of Chrome.
`web-ext`'s browser discovery is very limitted. By default, it only guesses at where Chrome and Firefox are installed. If you've customized your install locations, you may need to tell `web-ext` where the binaries/executables are located using the [`binaries` option](/api/wxt/interfaces/ExtensionRunnerConfig#binaries). For other Chromium based browsers, like Edge or Opera, you'll need to explicitly list them in the `binaries` option as well, otherwise they will open in Chrome by default.
```ts
// ~/web-ext.config.ts
@@ -47,7 +47,13 @@ import { defineRunnerConfig } from 'wxt';
export default defineRunnerConfig({
binaries: {
chrome: '/path/to/chrome-beta',
chrome: '/path/to/chrome-beta', // Use Chrome Beta instead of regular Chrome
firefox: 'firefoxdeveloperedition', // Use Firefox Developer Edition instead of regular Firefox
edge: '/path/to/edge', // Open MS Edge when running "wxt -b edge"
},
});
```
:::tip
When configuring browser binaries, it's helpful to put them in `~/web-ext.config.ts` instead of the project directory's `web-ext.config.ts` file. When placed in your home directory (`~/`), this config will be used by all WXT projects, so you only need to configure the binaries once.
:::
+1 -1
View File
@@ -2,7 +2,7 @@
The manifest.json is generated at build-time based on files in your `entrypoints` directory and your `wxt.config.ts`.
## Confiuration
## Configuration
While entrypoints are generated and added to the manifest at build-time, you can customize or add to your `manifest.json` in the config file.
+5 -1
View File
@@ -24,7 +24,7 @@ wxt build --browser firefox
```
By default, it will build for `chrome`. When excluding the [`--mv2` or `--mv3` flags](#target-manifest-version), it will default to the commonly accepted manifest version used with that browser.
By default, it will build for `chrome`. When excluding the [manifest version flags](#target-manifest-version), it will default to the commonly accepted manifest version for that browser.
| Browser | Default Manifest Version |
| ---------------- | :----------------------: |
@@ -34,6 +34,10 @@ By default, it will build for `chrome`. When excluding the [`--mv2` or `--mv3` f
| `edge` | 3 |
| Any other string | 3 |
:::tip
To configure which browser is opened when running dev mode via `wxt -b <browser>`, see the [Development docs](/guide/development#configure-browser-startup) docs.
:::
## Target Manifest Version
To build for a specific manifest version, pass either the `--mv2` flag or `--mv3` flag from the CLI.
+36
View File
@@ -0,0 +1,36 @@
# Storage API
WXT's storage API is powered by `unstorage`. See [their docs](https://unstorage.unjs.io/usage#usage-1) for more details.
## Overview
:::code-group
```ts [native]
const { installDate } = await browser.storage.local.get('installDate');
await browser.storage.local.set({ key: 'value' });
```
```ts [wxt/browser]
const installDate = await storage.get('local:installDate');
await storage.setItem('key', 'value');
```
:::
Use the `"local:"`, `"session:"`, `"sync:"`, and `"managed:"` prefixes to specify which storage area to use.
## Customization
WXT also provides a driver for `unstorage`. To customize the `storage` object's setup, like removing the prefixes and using a single storage area, you can create your own storage:
```ts
// storage.ts
export default createStorage({
driver: webExtensionDriver({ storageArea: 'local' }),
});
```
:::note
`wxt/browser` re-exports all of `unstorage`, which is where `createStorage` comes from.
:::
+9
View File
@@ -28,6 +28,15 @@ import { WxtVitest } from 'wxt/testing';
export default defineConfig({
plugins: [WxtVitest()],
test: {
server: {
deps: {
// Add any dependencies that import webextension-polyfill here, otherwise tests will attempt to import the real polyfill, breaking the
// TODO: Auto-detect these dependencies inside `WxtVitest` so maintaining this list manually isn't necessary
inline: [...],
},
},
},
});
```
+3 -2
View File
@@ -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>
+3 -2
View File
@@ -2,8 +2,9 @@
"entryPoints": [
"../src",
"../src/client",
"../src/client/browser.ts",
"../src/client/sandbox",
"../src/browser.ts",
"../src/sandbox",
"../src/storage.ts",
"../src/testing"
],
"plugin": ["typedoc-plugin-markdown", "typedoc-vitepress-theme"],
+12
View File
@@ -18,12 +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']
}
"
`);
+87 -24
View File
@@ -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/*"],
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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"
@@ -599,4 +637,29 @@ describe('Manifest Content', () => {
},
);
});
it('should respect the action field in the manifest without a popup', async () => {
const project = new TestProject();
project.setConfigFileConfig({
manifest: {
action: {
default_title: 'Hello world',
},
},
});
await project.build();
expect(await project.getOutputManifest()).toMatchInlineSnapshot(`
{
"action": {
"default_title": "Hello world",
},
"description": "Example description",
"manifest_version": 3,
"name": "E2E Extension",
"version": "0.0.0",
}
`);
});
});
+13
View File
@@ -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,
);
});
});
+59 -12
View File
@@ -44,7 +44,7 @@ describe('TypeScript Project', () => {
import \\"wxt/browser\\";
declare module \\"wxt/browser\\" {
type PublicPath =
export type PublicPath =
| \\"/options.html\\"
| \\"/popup.html\\"
| \\"/sandbox.html\\"
@@ -120,7 +120,7 @@ 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>\\"
*/
getMessage(
@@ -130,7 +130,7 @@ describe('TypeScript Project', () => {
): string;
/**
*
*
*
* \\"<browser.i18n.getUiLocale()>\\"
*/
getMessage(
@@ -140,7 +140,7 @@ describe('TypeScript Project', () => {
): 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>\\"
*/
getMessage(
@@ -150,7 +150,7 @@ describe('TypeScript Project', () => {
): string;
/**
* If the @@bidi_dir is \\"ltr\\", then this is \\"rtl\\"; otherwise, it's \\"ltr\\".
*
*
* \\"<rtl|ltr>\\"
*/
getMessage(
@@ -160,7 +160,7 @@ describe('TypeScript Project', () => {
): string;
/**
* If the @@bidi_dir is \\"ltr\\", then this is \\"left\\"; otherwise, it's \\"right\\".
*
*
* \\"<left|right>\\"
*/
getMessage(
@@ -170,7 +170,7 @@ describe('TypeScript Project', () => {
): string;
/**
* If the @@bidi_dir is \\"ltr\\", then this is \\"right\\"; otherwise, it's \\"left\\".
*
*
* \\"<right|left>\\"
*/
getMessage(
@@ -180,7 +180,7 @@ describe('TypeScript Project', () => {
): string;
/**
* Ask for the user's name
*
*
* \\"What's your name?\\"
*/
getMessage(
@@ -190,7 +190,7 @@ describe('TypeScript Project', () => {
): string;
/**
* Greet the user
*
*
* \\"Hello, $USER$\\"
*/
getMessage(
@@ -200,7 +200,7 @@ describe('TypeScript Project', () => {
): string;
/**
* Say goodbye to the user
*
*
* \\"Goodbye, $USER$. Come back to $OUR_SITE$ soon!\\"
*/
getMessage(
@@ -252,7 +252,6 @@ describe('TypeScript Project', () => {
\\"forceConsistentCasingInFileNames\\": true,
\\"resolveJsonModule\\": true,
\\"strict\\": true,
\\"lib\\": [\\"DOM\\", \\"WebWorker\\"],
\\"skipLibCheck\\": true,
\\"paths\\": {
\\"@\\": [\\"..\\"],
@@ -296,7 +295,6 @@ describe('TypeScript Project', () => {
\\"forceConsistentCasingInFileNames\\": true,
\\"resolveJsonModule\\": true,
\\"strict\\": true,
\\"lib\\": [\\"DOM\\", \\"WebWorker\\"],
\\"skipLibCheck\\": true,
\\"paths\\": {
\\"@\\": [\\"../src\\"],
@@ -317,4 +315,53 @@ describe('TypeScript Project', () => {
}"
`);
});
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\\"]
}"
`);
});
});
+21
View File
@@ -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.
@@ -85,4 +86,24 @@ describe('User Config', () => {
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"example_customization\\":[\\"production\\",\\"chrome\\",\\"3\\",\\"build\\"]}"
`);
});
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);
});
});
+38 -32
View File
@@ -1,7 +1,7 @@
{
"name": "wxt",
"type": "module",
"version": "0.8.3",
"version": "0.10.4",
"description": "Next gen framework for developing web extensions",
"engines": {
"node": ">=18",
@@ -56,16 +56,21 @@
"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": {
"wxt": "tsx src/cli/index.ts",
"wxt": "tsx src/cli.ts",
"build": "tsx scripts/build.ts",
"format": "prettier --write .",
"format:check": "prettier --write .",
"compile": "tsc --noEmit",
"test": "vitest",
"test:coverage": "vitest run --coverage.enabled --coverage.exclude=e2e --coverage.exclude=src/testing",
"test:coverage": "vitest run --coverage",
"prepare": "simple-git-hooks",
"prepublish": "pnpm -s build",
"docs:gen": "typedoc --options docs/typedoc.json",
@@ -75,71 +80,72 @@
"sync-releases": "pnpx changelogen@latest gh release"
},
"dependencies": {
"@types/webextension-polyfill": "^0.10.1",
"@webext-core/fake-browser": "^1.2.2",
"@types/webextension-polyfill": "^0.10.5",
"@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.4.2",
"c12": "^1.5.1",
"cac": "^6.7.14",
"consola": "^3.2.3",
"esbuild": "^0.19.4",
"defu": "^6.1.3",
"esbuild": "^0.19.5",
"fast-glob": "^3.3.1",
"filesize": "^10.0.8",
"fs-extra": "^11.1.1",
"get-port": "^7.0.0",
"giget": "^1.1.2",
"immer": "^10.0.2",
"giget": "^1.1.3",
"immer": "^10.0.3",
"is-wsl": "^3.0.0",
"jiti": "^1.19.1",
"jiti": "^1.21.0",
"json5": "^2.2.3",
"linkedom": "^0.15.1",
"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.1.0",
"vite": "^4.4.11",
"unimport": "^3.4.0",
"unstorage": "^1.9.0",
"vite": "^4.0.0 || ^5.0.0-0",
"web-ext-run": "^0.1.0",
"webextension-polyfill": "^0.10.0",
"zip-dir": "^2.0.0"
},
"devDependencies": {
"@faker-js/faker": "^8.0.2",
"@types/fs-extra": "^11.0.1",
"@types/lodash.merge": "^4.6.7",
"@types/node": "^20.8.6",
"@types/prompts": "^2.4.4",
"@faker-js/faker": "^8.2.0",
"@types/fs-extra": "^11.0.3",
"@types/lodash.merge": "^4.6.8",
"@types/node": "^20.8.10",
"@types/normalize-path": "^3.0.2",
"@types/prompts": "^2.4.7",
"@vitest/coverage-v8": "^0.34.6",
"execa": "^7.2.0",
"jsdom": "^22.1.0",
"lint-staged": "^14.0.0",
"execa": "^8.0.1",
"happy-dom": "^12.4.0",
"lint-staged": "^15.0.2",
"lodash.merge": "^4.6.2",
"npm-run-all": "^4.1.5",
"p-map": "^6.0.0",
"prettier": "^3.0.1",
"simple-git-hooks": "^2.9.0",
"tsup": "^7.1.0",
"tsx": "^3.12.7",
"typedoc": "^0.25.2",
"tsup": "^7.2.0",
"tsx": "^4.6.1",
"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.10",
"typescript": "^5.3.2",
"vitepress": "1.0.0-rc.24",
"vitest": "^0.34.6",
"vitest-mock-extended": "^1.3.1",
"vue": "^3.3.4",
"webextension-polyfill": "^0.10.0"
},
"peerDependencies": {
"webextension-polyfill": ">=0.10.0"
"vue": "^3.3.7"
},
"packageManager": "pnpm@8.6.3",
"simple-git-hooks": {
"pre-commit": "pnpm lint-staged"
},
"lint-staged": {
"*": "prettier --write"
"*": "prettier --ignore-unknown --write"
},
"changelog": {
"excludeAuthors": [
+1003 -830
View File
File diff suppressed because it is too large Load Diff
+84 -77
View File
@@ -1,96 +1,103 @@
import tsup from 'tsup';
import glob from 'fast-glob';
import { printFileList } from '../src/core/log/printFileList';
import { formatDuration } from '../src/core/utils/formatDuration';
import { printFileList } from '~/core/utils/log';
import { formatDuration } from '~/core/utils/time';
import ora from 'ora';
import fs from 'fs-extra';
import { consola } from 'consola';
import pMap from 'p-map';
import os from 'node:os';
import path from 'node:path';
const spinner = ora('Building WXT').start();
const spinnerText = 'Building WXT';
const spinner = ora(spinnerText).start();
const startTime = Date.now();
const outDir = 'dist';
const virtualEntrypoints = ['background', 'content-script', 'unlisted-script'];
await fs.rm(path.join(outDir, '*'), { recursive: true, force: true });
await fs.rm(outDir, { recursive: true, force: true });
const preset: tsup.Options = {
dts: true,
silent: true,
sourcemap: false,
external: [
'vite',
'virtual:user-unlisted-script',
'virtual:user-content-script',
'virtual:user-background',
],
};
await Promise.all([
tsup.build({
function spinnerPMap(configs: tsup.Options[]) {
let completed = 0;
const updateSpinner = () => {
spinner.text = `${spinnerText} [${completed}/${configs.length}]`;
};
updateSpinner();
return pMap(
config,
async (config) => {
const res = await tsup.build(config);
completed++;
updateSpinner();
return res;
},
{
stopOnError: true,
concurrency: process.env.CI === 'true' ? os.cpus().length : Infinity,
},
);
}
const config: tsup.Options[] = [
// CJS/ESM
{
...preset,
entry: {
index: 'src/index.ts',
testing: 'src/testing/index.ts',
storage: 'src/storage.ts',
},
format: ['cjs', 'esm'],
sourcemap: true,
dts: true,
silent: true,
external: ['vite'],
}),
tsup.build({
entry: { cli: 'src/cli/index.ts' },
clean: true,
},
// ESM-only
{
...preset,
entry: {
browser: 'src/browser.ts',
sandbox: 'src/sandbox/index.ts',
client: 'src/client/index.ts',
},
format: ['esm'],
},
{
...preset,
entry: {
'virtual/background-entrypoint': 'src/virtual/background-entrypoint.ts',
'virtual/content-script-entrypoint':
'src/virtual/content-script-entrypoint.ts',
'virtual/mock-browser': 'src/virtual/mock-browser.ts',
'virtual/reload-html': 'src/virtual/reload-html.ts',
'virtual/unlisted-script-entrypoint':
'src/virtual/unlisted-script-entrypoint.ts',
},
format: ['esm'],
splitting: false,
dts: false,
},
// CJS-only
{
...preset,
entry: {
cli: 'src/cli.ts',
},
format: ['cjs'],
sourcemap: 'inline',
silent: true,
external: ['vite'],
}),
tsup.build({
entry: { client: 'src/client/index.ts' },
format: ['esm'],
sourcemap: 'inline',
dts: true,
silent: true,
external: ['vite'],
}),
tsup.build({
entry: { browser: 'src/client/browser.ts' },
format: ['esm'],
sourcemap: 'inline',
dts: true,
silent: true,
external: ['vite'],
}),
tsup.build({
entry: { sandbox: 'src/client/sandbox/index.ts' },
format: ['esm'],
sourcemap: 'inline',
dts: true,
silent: true,
}),
tsup.build({
entry: { testing: 'src/testing/index.ts' },
format: ['esm', 'cjs'],
sourcemap: 'inline',
dts: true,
silent: true,
}),
...virtualEntrypoints.map((entryName) =>
tsup.build({
entry: {
[`virtual-modules/${entryName}-entrypoint`]: `src/client/virtual-modules/${entryName}-entrypoint.ts`,
},
format: ['esm'],
sourcemap: true,
silent: true,
external: [`virtual:user-${entryName}`, 'vite'],
}),
),
tsup.build({
entry: {
'virtual-modules/reload-html': `src/client/virtual-modules/reload-html.ts`,
},
format: ['esm'],
sourcemap: true,
silent: true,
external: ['vite'],
}),
tsup.build({
entry: {
'virtual-modules/fake-browser': `src/client/virtual-modules/fake-browser.ts`,
},
format: ['esm', 'cjs'],
silent: true,
external: ['vite'],
}),
]).catch((err) => {
},
];
await spinnerPMap(config).catch((err) => {
spinner.fail();
console.error(err);
process.exit(1);
-16
View File
@@ -1,16 +0,0 @@
declare module 'zip-dir' {
// Represents the options object for zipdir function
interface ZipDirOptions {
saveTo?: string;
filter?: (path: string, stat: import('fs').Stats) => boolean;
each?: (path: string) => void;
}
function zipdir(
dirPath: string,
options?: ZipDirOptions,
callback?: (error: Error | null, buffer: Buffer) => void,
): Promise<Buffer>;
export = zipdir;
}
+70
View File
@@ -0,0 +1,70 @@
import { fakeBrowser } from '@webext-core/fake-browser';
import { describe, it, expect, beforeEach } from 'vitest';
import { browser } from '~/browser';
import { storage } from '~/storage';
describe('Storage Utils', () => {
beforeEach(() => {
fakeBrowser.reset();
});
describe.each(['local', 'sync', 'managed', 'session'] as const)(
'storage - %s',
(storageArea) => {
describe('getItem', () => {
it('should return the value from the correct storage area', async () => {
const expected = 123;
await fakeBrowser.storage[storageArea].set({ count: expected });
expect(await storage.getItem(`${storageArea}:count`)).toBe(expected);
});
it("should return null if the value doesn't exist", async () => {
expect(await storage.getItem(`${storageArea}:count`)).toBeNull();
});
});
describe('getItems', () => {
it('should return an array of values in the same order as the keys passed in', async () => {
const expected = [
{ key: `${storageArea}:count`, value: 234 },
{ key: `${storageArea}:installDate`, value: null },
];
const keys = expected.map((item) => item.key);
await fakeBrowser.storage[storageArea].set({
count: expected[0].value,
});
const actual = await storage.getItems(keys);
expect(actual).toEqual(expected);
});
});
describe('setItem', () => {
it('should set the value in the correct storage area', () => {});
it.each([undefined, null])(
'should remove the item from storage when setting the value to %s',
async (value) => {
await fakeBrowser.storage[storageArea].set({ count: 345 });
await storage.setItem(`${storageArea}:count`, value as null);
// For some reason storage sets the value to "null" instead of deleting it. So using
// fakeBrowser during the expect fails. Using storage works. I've confirmed that this
// doesn't happen in a real extension environment.
expect(await storage.getItem(`${storageArea}:count`)).toBeNull();
},
);
});
describe('removeItem', () => {
it('should remove the key from storage', async () => {
await fakeBrowser.storage[storageArea].set({ count: 456 });
await storage.removeItem(`${storageArea}:count`);
expect(await browser.storage[storageArea].get()).toEqual({});
});
});
},
);
});
+164
View File
@@ -0,0 +1,164 @@
import cac from 'cac';
import { version } from '~/version';
import { build, clean, createServer, initialize, prepare, zip } from '~/core';
import consola, { LogLevels } from 'consola';
import { printHeader } from '~/core/utils/log';
import { formatDuration } from '~/core/utils/time';
const cli = cac('wxt');
cli.help();
cli.version(version);
cli.option('--debug', 'enable debug mode');
// DEV
cli
.command('[root]', 'start dev server')
.option('-c, --config <file>', 'use specified config file')
.option('-m, --mode <mode>', 'set env mode')
.option('-b, --browser <browser>', 'specify a browser')
.option('--mv3', 'target manifest v3')
.option('--mv2', 'target manifest v2')
.action(
wrapAction(async (root, flags) => {
const server = await createServer({
root,
mode: flags.mode,
browser: flags.browser,
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
configFile: flags.config,
debug: flags.debug,
});
await server.start();
return { isOngoing: true };
}),
);
// BUILD
cli
.command('build [root]', 'build for production')
.option('-c, --config <file>', 'use specified config file')
.option('-m, --mode <mode>', 'set env mode')
.option('-b, --browser <browser>', 'specify a browser')
.option('--mv3', 'target manifest v3')
.option('--mv2', 'target manifest v2')
.option('--analyze', 'visualize extension bundle')
.action(
wrapAction(async (root, flags) => {
await build({
root,
mode: flags.mode,
browser: flags.browser,
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
configFile: flags.config,
debug: flags.debug,
analysis: {
enabled: flags.analyze,
},
});
}),
);
// ZIP
cli
.command('zip [root]', 'build for production and zip output')
.option('-c, --config <file>', 'use specified config file')
.option('-m, --mode <mode>', 'set env mode')
.option('-b, --browser <browser>', 'specify a browser')
.option('--mv3', 'target manifest v3')
.option('--mv2', 'target manifest v2')
.action(
wrapAction(async (root, flags) => {
await zip({
root,
mode: flags.mode,
browser: flags.browser,
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
configFile: flags.config,
debug: flags.debug,
});
}),
);
// PREPARE
cli
.command('prepare [root]', 'prepare typescript project')
.option('-c, --config <file>', 'use specified config file')
.action(
wrapAction(async (root, flags) => {
await prepare({
root,
configFile: flags.config,
debug: flags.debug,
});
}),
);
// CLEAN
cli
.command('clean [root]', 'clean generated files and caches')
.alias('cleanup')
.action(
wrapAction(async (root, flags) => {
await clean(root);
}),
);
// INIT
cli
.command('init [directory]', 'initialize a new project')
.option('-t, --template <template>', 'template to use')
.option('--pm <packageManager>', 'which package manager to use')
.action(
wrapAction(
async (directory, flags) => {
await initialize({
directory,
template: flags.template,
packageManager: flags.pm,
});
},
{ disableFinishedLog: true },
),
);
cli.parse();
/**
* Wrap an action handler to add a timer, error handling, and maybe enable debug mode.
*/
function wrapAction(
cb: (
...args: any[]
) => void | { isOngoing?: boolean } | Promise<void | { isOngoing?: boolean }>,
options?: {
disableFinishedLog?: boolean;
},
) {
return async (...args: any[]) => {
// Enable consola's debug mode globally at the start of all commands when the `--debug` flag is
// passed
const isDebug = !!args.find((arg) => arg?.debug);
if (isDebug) {
consola.level = LogLevels.debug;
}
const startTime = Date.now();
try {
printHeader();
const status = await cb(...args);
if (!status?.isOngoing && !options?.disableFinishedLog)
consola.success(
`Finished in ${formatDuration(Date.now() - startTime)}`,
);
} catch (err) {
consola.fail(
`Command failed after ${formatDuration(Date.now() - startTime)}`,
);
consola.error(err);
process.exit(1);
}
};
}
-31
View File
@@ -1,31 +0,0 @@
import * as wxt from '../..';
import { defineCommand } from '../utils/defineCommand';
export const build = defineCommand<
[
root: string | undefined,
flags: {
mode?: string;
config?: string;
browser?: wxt.TargetBrowser;
mv3?: boolean;
mv2?: boolean;
debug?: boolean;
analyze?: boolean;
},
]
>(async (root, flags) => {
const cliConfig: wxt.InlineConfig = {
root,
mode: flags.mode,
browser: flags.browser,
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
configFile: flags.config,
debug: flags.debug,
analysis: {
enabled: flags.analyze,
},
};
await wxt.build(cliConfig);
});
-11
View File
@@ -1,11 +0,0 @@
import { defineCommand } from '../utils/defineCommand';
import * as wxt from '../..';
export const clean = defineCommand<
[
root: string | undefined,
flags: {
debug?: boolean;
},
]
>(wxt.clean);
-30
View File
@@ -1,30 +0,0 @@
import * as wxt from '../..';
import { defineCommand } from '../utils/defineCommand';
export const dev = defineCommand<
[
root: string | undefined,
flags: {
mode?: string;
config?: string;
browser?: wxt.TargetBrowser;
mv3?: boolean;
mv2?: boolean;
debug?: boolean;
},
]
>(async (root, flags) => {
const cliConfig: wxt.InlineConfig = {
root,
mode: flags.mode,
browser: flags.browser,
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
configFile: flags.config,
debug: flags.debug,
};
const server = await wxt.createServer(cliConfig);
await server.start();
return true;
});
-7
View File
@@ -1,7 +0,0 @@
export * from './build';
export * from './dev';
export * from './init';
export * from './prepare';
export * from './publish';
export * from './zip';
export * from './clean';
-27
View File
@@ -1,27 +0,0 @@
import { getInternalConfig } from '../../core/utils/getInternalConfig';
import { findEntrypoints } from '../../core/build/findEntrypoints';
import { generateTypesDir } from '../../core/build/generateTypesDir';
import { defineCommand } from '../utils/defineCommand';
import * as wxt from '../..';
export const prepare = defineCommand<
[
root: string | undefined,
flags: {
config?: string;
debug?: boolean;
},
]
>(async (root, flags) => {
const cliConfig: wxt.InlineConfig = {
root,
configFile: flags.config,
debug: flags.debug,
};
const config = await getInternalConfig(cliConfig, 'build');
config.logger.info('Generating types...');
const entrypoints = await findEntrypoints(config);
await generateTypesDir(entrypoints, config);
});
-11
View File
@@ -1,11 +0,0 @@
import { consola } from 'consola';
import { defineCommand } from '../utils/defineCommand';
export const publish = defineCommand(
async (
root: any,
{ config: configFile, debug }: { config?: string; debug?: string },
) => {
consola.warn('wxt publish: Not implemented');
},
);
-32
View File
@@ -1,32 +0,0 @@
import * as wxt from '../..';
import { buildInternal } from '../../core/build';
import { getInternalConfig } from '../../core/utils/getInternalConfig';
import { zipExtension } from '../../core/zip';
import { defineCommand } from '../utils/defineCommand';
export const zip = defineCommand<
[
root: string | undefined,
flags: {
mode?: string;
config?: string;
browser?: wxt.TargetBrowser;
mv3?: boolean;
mv2?: boolean;
debug?: boolean;
},
]
>(async (root, flags) => {
const cliConfig: wxt.InlineConfig = {
root,
mode: flags.mode,
browser: flags.browser,
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
configFile: flags.config,
debug: flags.debug,
};
const config = await getInternalConfig(cliConfig, 'build');
const output = await buildInternal(config);
await zipExtension(config, output);
});
-64
View File
@@ -1,64 +0,0 @@
import cac from 'cac';
import { version } from '../../package.json';
import * as commands from './commands';
const cli = cac('wxt');
cli.help();
cli.version(version);
cli.option('--debug', 'enable debug mode');
// DEV
cli
.command('[root]', 'start dev server')
.option('-c, --config <file>', 'use specified config file')
.option('-m, --mode <mode>', 'set env mode')
.option('-b, --browser <browser>', 'specify a browser')
.option('--mv3', 'target manifest v3')
.option('--mv2', 'target manifest v2')
.action(commands.dev);
// BUILD
cli
.command('build [root]', 'build for production')
.option('-c, --config <file>', 'use specified config file')
.option('-m, --mode <mode>', 'set env mode')
.option('-b, --browser <browser>', 'specify a browser')
.option('--mv3', 'target manifest v3')
.option('--mv2', 'target manifest v2')
.option('--analyze', 'visualize extension bundle')
.action(commands.build);
// ZIP
cli
.command('zip [root]', 'build for production and zip output')
.option('-c, --config <file>', 'use specified config file')
.option('-m, --mode <mode>', 'set env mode')
.option('-b, --browser <browser>', 'specify a browser')
.option('--mv3', 'target manifest v3')
.option('--mv2', 'target manifest v2')
.action(commands.zip);
// PREPARE
cli
.command('prepare [root]', 'prepare typescript project')
.option('-c, --config <file>', 'use specified config file')
.action(commands.prepare);
// CLEAN
cli
.command('clean [root]', 'clean generated files and caches')
.alias('cleanup')
.action(commands.clean);
// PUBLISH
cli.command('publish [root]', 'publish to stores').action(commands.publish);
// INIT
cli
.command('init [directory]', 'initialize a new project')
.option('-t, --template <template>', 'template to use')
.option('--pm <packageManager>', 'which package manager to use')
.action(commands.init);
cli.parse();
@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from 'vitest';
import { defineBackground } from '../defineBackground';
import { BackgroundDefinition } from '../../core/types';
import { defineBackground } from '~/client/define-background';
import { BackgroundDefinition } from '~/types';
describe('defineBackground', () => {
it('should return the object definition when given an object', () => {
@@ -0,0 +1,214 @@
/** @vitest-environment happy-dom */
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { createContentScriptIframe } from '~/client/content-scripts/content-script-iframe';
import { ContentScriptContext } from '~/client/content-scripts/content-script-context';
const createCtx = () => new ContentScriptContext('test');
const fetch = vi.fn();
describe('createContentScriptIframe', () => {
beforeEach(() => {
document.body.innerHTML = `
<div id="parent">
<p id="one">one</p>
<p id="two">two</p>
<p id="three"></p>
</div>
`;
window.fetch = fetch;
fetch.mockResolvedValue({ text: () => Promise.resolve('') });
});
describe('mount', () => {
describe('append option', () => {
it.each([undefined, 'last' as const])(
'should append the element as the last child to the anchor when append=%s',
async (append) => {
const ui = await createContentScriptIframe(createCtx(), {
page: '/test.html',
type: 'inline',
anchor: '#parent',
append,
});
ui.mount();
expect(
document.querySelector('#parent > :nth-child(4)')?.tagName,
).toEqual('DIV');
},
);
it('should append the element as the first child to the anchor when append=first', async () => {
const ui = await createContentScriptIframe(createCtx(), {
page: '/test.html',
type: 'inline',
anchor: '#parent',
append: 'first',
});
ui.mount();
expect(
document.querySelector('#parent > :nth-child(1)')?.tagName,
).toEqual('DIV');
});
it('should append the element normally when append=first but there are no other children', async () => {
const ui = await createContentScriptIframe(createCtx(), {
page: '/test.html',
type: 'inline',
anchor: '#three',
append: 'first',
});
ui.mount();
expect(
document.querySelector('#three > :nth-child(1)')?.tagName,
).toEqual('DIV');
});
it('should replace the anchor with the element when append=replace', async () => {
const ui = await createContentScriptIframe(createCtx(), {
page: '/test.html',
type: 'inline',
anchor: '#two',
append: 'replace',
});
ui.mount();
expect(
document.querySelector('#parent > :nth-child(2)')?.tagName,
).toEqual('DIV');
});
it('should append the element after the anchor when append=before', async () => {
const ui = await createContentScriptIframe(createCtx(), {
page: '/test.html',
type: 'inline',
anchor: '#two',
append: 'before',
});
ui.mount();
expect(
document.querySelector('#parent > :nth-child(2)')?.tagName,
).toEqual('DIV');
});
it('should append the element after the anchor when append=after', async () => {
const ui = await createContentScriptIframe(createCtx(), {
page: '/test.html',
type: 'inline',
anchor: '#two',
append: 'after',
});
ui.mount();
// Happy DOM doesn't work in this case, so we just make sure the element is added.
// expect(
// document.querySelector('#parent > :nth-child(3)')?.tagName,
// ).toEqual('DIV');
expect(document.querySelector('DIV')).toBeDefined();
});
it('should apply a custom function', async () => {
const ui = await createContentScriptIframe(createCtx(), {
page: '/test.html',
type: 'inline',
anchor: '#three',
append: (anchor, ui) => anchor.replaceWith(ui),
});
ui.mount();
expect(
document.querySelector('#parent > :nth-child(3)')?.tagName,
).toEqual('DIV');
});
});
it('should default the anchor to the body when unset', async () => {
const ui = await createContentScriptIframe(createCtx(), {
page: '/test.html',
type: 'inline',
});
ui.mount();
expect(document.querySelector('body > :nth-child(2)')?.tagName).toEqual(
'DIV',
);
});
it.each(['#four', () => document.querySelector('#four')])(
"should throw an error if the anchor doesn't exist",
async (anchor) => {
const ui = await createContentScriptIframe(createCtx(), {
page: '/test.html',
type: 'inline',
anchor,
});
expect(ui.mount).toThrow(
'Failed to mount content script UI: could not find anchor element',
);
},
);
});
describe('remove', () => {
it("should not fail if the ui hasn't been mounted", async () => {
const ui = await createContentScriptIframe(createCtx(), {
page: '/test.html',
type: 'inline',
});
ui.remove();
});
it('should automatically remove the UI when the context is invalidated', async () => {
const ctx = createCtx();
const ui = await createContentScriptIframe(ctx, {
page: '/test.html',
type: 'inline',
});
ui.mount();
expect(document.querySelector('.wxt-iframe-wrapper')).toBeDefined();
ctx.abort();
expect(document.querySelector('.wxt-iframe-wrapper')).toBeNull();
});
});
describe('type', () => {
it.each(['inline', 'overlay', 'modal'] as const)(
'should render type=%s',
async (type) => {
const ui = await createContentScriptIframe(createCtx(), {
page: '/test.html',
type,
});
ui.mount();
expect(document.querySelector('DIV')).toBeDefined();
},
);
});
describe('anchor', () => {
it.each([
() => '#two',
() => () => '#two',
() => document.querySelector('#two'),
() => () => document.querySelector('#two'),
])('should render anchor=%s', async (getAnchor) => {
const ui = await createContentScriptIframe(createCtx(), {
page: '/test.html',
type: 'inline',
anchor: getAnchor(),
});
ui.mount();
expect(document.querySelector('DIV')).toBeDefined();
});
});
});
@@ -1,13 +1,10 @@
/** @vitest-environment jsdom */
/** @vitest-environment happy-dom */
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ContentScriptContext, createContentScriptUi } from '..';
import { createContentScriptUi } from '~/client/content-scripts/content-script-ui';
import { ContentScriptContext } from '~/client/content-scripts/content-script-context';
import { createIsolatedElement } from '@webext-core/isolated-element';
import { mock } from 'vitest-mock-extended';
import { browser } from '../browser';
vi.mock('webextension-polyfill', () =>
vi.importActual('../virtual-modules/fake-browser'),
);
import { browser } from '~/browser';
vi.mock('@webext-core/isolated-element', async () => {
const { vi } = await import('vitest');
@@ -222,9 +219,11 @@ describe('createContentScriptUi', () => {
});
ui.mount();
expect(
document.querySelector('#parent > :nth-child(3)')?.tagName,
).toEqual('TEST-APP');
// Happy DOM doesn't work in this case, so we just make sure the element is added.
// expect(
// document.querySelector('#parent > :nth-child(3)')?.tagName,
// ).toEqual('TEST-APP');
expect(document.querySelector('test-app')).toBeDefined();
});
it('should apply a custom function', async () => {
@@ -267,7 +266,7 @@ describe('createContentScriptUi', () => {
});
expect(ui.mount).toThrow(
'Failed to mount content script ui: could not find anchor element',
'Failed to mount content script UI: could not find anchor element',
);
},
);
@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from 'vitest';
import { defineContentScript } from '../defineContentScript';
import { ContentScriptDefinition } from '../../core/types';
import { defineContentScript } from '~/client/content-scripts/define-content-script';
import { ContentScriptDefinition } from '~/types';
describe('defineContentScript', () => {
it('should return the object passed in', () => {
@@ -1,6 +1,6 @@
import { ContentScriptDefinition } from '../../core/types';
import { browser } from '../browser';
import { logger } from './logger';
import { ContentScriptDefinition } from '~/types';
import { browser } from '~/browser';
import { logger } from '~/client/utils/logger';
/**
* Implements [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController).
@@ -0,0 +1,83 @@
import browser from 'webextension-polyfill';
import {
ContentScriptAnchoredOptions,
ContentScriptPositioningOptions,
applyContentScriptUiPosition,
mountContentScriptUiRoot,
} from '../utils/content-script-ui';
import { ContentScriptContext } from './content-script-context';
/**
* Utility for mounting a content script UI inside an iframe. Automatically removed from the DOM
* when the content script's context is invalidated.
*
* See https://wxt.dev/entrypoints/content-scripts.html#iframe for full documentation.
*
* @example
* export default defineContentScript({
* matches: ["*://*.google.com/*"],
*
* main(ctx) {
* const ui = await createContentScriptIframe(ctx, {
* page: "/content-script-overlay.html",
* type: "modal",
* })
* ui.mount();
* }
* })
*/
export function createContentScriptIframe(
ctx: ContentScriptContext,
options: ContentScriptIframeOptions,
): ContentScriptIframe {
const wrapper = document.createElement('div');
wrapper.classList.add('wxt-iframe-wrapper');
const iframe = document.createElement('iframe');
iframe.src = browser.runtime.getURL(options.page);
wrapper.appendChild(iframe);
const mount = () => {
applyContentScriptUiPosition(wrapper, iframe, options);
mountContentScriptUiRoot(wrapper, options);
};
const remove = () => {
wrapper.remove();
};
ctx.onInvalidated(remove);
return {
iframe,
wrapper,
mount,
remove,
};
}
export interface ContentScriptIframe {
/**
* The iframe added to the DOM.
*/
iframe: HTMLIFrameElement;
/**
* A wrapper div that assists in positioning.
*/
wrapper: HTMLDivElement;
/**
* Function that mounts or remounts the UI on the page.
*/
mount: () => void;
/**
* Function that removes the UI from the webpage.
*/
remove: () => void;
}
export type ContentScriptIframeOptions = ContentScriptPositioningOptions &
ContentScriptAnchoredOptions & {
/**
* The path to the unlisted HTML file to display in the iframe.
*/
page: import('wxt/browser').PublicPath;
};
@@ -0,0 +1,173 @@
import { createIsolatedElement } from '@webext-core/isolated-element';
import { browser } from '~/browser';
import { logger } from '~/client/utils/logger';
import { ContentScriptContext } from './content-script-context';
import {
ContentScriptAnchoredOptions,
ContentScriptPositioningOptions,
applyContentScriptUiPosition,
mountContentScriptUiRoot,
} from '../utils/content-script-ui';
/**
* Utility for mounting content script UI's with isolated styles. Automatically removed from the DOM
* when the content script's context is invalidated.
*
* See https://wxt.dev/entrypoints/content-scripts.html#ui for full documentation.
*
* @example
* // entrypoints/example-ui.content/index.ts
* import "./style.css"
*
* export default defineContentScript({
* matches: ["*://*.google.com/*"],
* cssInjectionMode: "ui",
*
* async main(ctx) {
* const ui = await createContentScriptUi(ctx, {
* name: "example-overlay",
* type: "modal",
* mount(container) {
* const app = document.createElement("div");
* app.textContent = "Content Script UI";
* container.append(app);
* }
* })
* ui.mount();
* }
* })
*/
export async function createContentScriptUi<TApp>(
ctx: ContentScriptContext,
options: ContentScriptUiOptions<TApp>,
): Promise<ContentScriptUi<TApp>> {
const css = [options.css ?? ''];
if (ctx.options?.cssInjectionMode === 'ui') {
css.push(await loadCss());
}
const {
isolatedElement: uiContainer,
parentElement: shadowHost,
shadow,
} = await createIsolatedElement({
name: options.name,
css: {
textContent: css.join('\n').trim(),
},
mode: 'open',
});
let mounted: TApp;
const mount = () => {
// Mount UI inside shadow root
mounted = options.mount(uiContainer);
// Add shadow root element to DOM
mountContentScriptUiRoot(shadowHost, options);
applyContentScriptUiPosition(
shadowHost,
shadow.querySelector('html'),
options,
);
};
const remove = () => {
// Detatch shadow root from DOM
shadowHost.remove();
// Cleanup mounted state
options.onRemove?.(mounted);
// Remove children from uiContainer
while (uiContainer.lastChild)
uiContainer.removeChild(uiContainer.lastChild);
};
ctx.onInvalidated(remove);
return {
shadow,
shadowHost,
uiContainer,
mount,
remove,
mounted: mounted!,
};
}
/**
* Load the CSS for the current entrypoint.
*/
async function loadCss(): Promise<string> {
const url = browser.runtime.getURL(`/content-scripts/${__ENTRYPOINT__}.css`);
try {
const res = await fetch(url);
const css = await res.text();
// Replace :root selectors with :host since we're in a shadow root
return css.replaceAll(':root', ':host');
} catch (err) {
logger.warn(
`Failed to load styles @ ${url}. Did you forget to import the stylesheet in your entrypoint?`,
err,
);
return '';
}
}
export interface ContentScriptUi<TApp> {
/**
* The `HTMLElement` hosting the shadow root used to isolate the UI's styles. This is the element
* that get's added to the DOM. This element's style is not isolated from the webpage.
*/
shadowHost: HTMLElement;
/**
* The container element inside the `ShadowRoot` whose styles are isolated. The UI is mounted
* inside this `HTMLElement`.
*/
uiContainer: HTMLElement;
/**
* The shadow root performing the isolation.
*/
shadow: ShadowRoot;
/**
* Custom data returned from the `options.mount` function.
*/
mounted: TApp;
/**
* Function that mounts or remounts the UI on the page.
*/
mount: () => void;
/**
* Function that removes the UI from the webpage.
*/
remove: () => void;
}
export type ContentScriptUiOptions<TApp> = ContentScriptPositioningOptions &
ContentScriptAnchoredOptions & {
/**
* The name of the custom component used to host the ShadowRoot. Must be kebab-case.
*/
name: string;
/**
* Callback executed when mounting the UI. This function should create and append the UI to the
* `container` element. It is called every time `ui.mount()` is called
*
* Optionally return a value that can be accessed at `ui.mounted` or in the `onRemove` callback.
*/
mount: (container: Element) => TApp;
/**
* Callback called when the UI is removed from the webpage. Use to cleanup your UI, like
* unmounting your vue or react apps.
*/
onRemove?: (mounted: TApp) => void;
/**
* Custom CSS text to apply to the UI. If your content script imports/generates CSS and you've
* set `cssInjectionMode: "ui"`, the imported CSS will be included automatically. You do not need
* to pass those styles in here. This is for any additional styles not in the imported CSS.
*
* See https://wxt.dev/entrypoints/content-scripts.html#ui for more info.
*/
css?: string;
};
@@ -1,4 +1,4 @@
import { ContentScriptDefinition } from '../core/types';
import { ContentScriptDefinition } from '~/types';
export function defineContentScript(
definition: ContentScriptDefinition,
+4
View File
@@ -0,0 +1,4 @@
export * from './content-script-context';
export * from './content-script-ui';
export * from './content-script-iframe';
export * from './define-content-script';
-301
View File
@@ -1,301 +0,0 @@
import { createIsolatedElement } from '@webext-core/isolated-element';
import { browser } from './browser';
import { logger } from './utils/logger';
import { ContentScriptContext } from '.';
/**
* Utility for mounting content script UI's with isolated styles. Automatically removed from the DOM
* when the content script's context is invalidated.
*
* See https://wxt.dev/entrypoints/content-scripts.html#ui for full documentation.
*
* @example
* // entrypoints/example-ui.content/index.ts
* import "./style.css"
*
* export default defineContentScript({
* matches: ["*://*.google.com/*"],
* cssInjectionMode: "ui",
*
* async main(ctx) {
* const ui = await createContentScriptUi(ctx, {
* name: "example-overlay",
* type: "modal",
* mount(container) {
* const app = document.createElement("div");
* app.textContent = "Content Script UI";
* container.append(app);
* }
* })
* ui.mount();
* }
* })
*/
export async function createContentScriptUi<TApp>(
ctx: ContentScriptContext,
options: ContentScriptUiOptions<TApp>,
): Promise<ContentScriptUi<TApp>> {
const css = [options.css ?? ''];
if (ctx.options?.cssInjectionMode === 'ui') {
css.push(await loadCss());
}
const {
isolatedElement: uiContainer,
parentElement: shadowHost,
shadow,
} = await createIsolatedElement({
name: options.name,
css: {
textContent: css.join('\n').trim(),
},
mode: 'open',
});
const getAnchor = (): Element | undefined => {
if (options.anchor == null) return document.body;
let resolved =
typeof options.anchor === 'function' ? options.anchor() : options.anchor;
if (typeof resolved === 'string')
return document.querySelector<Element>(resolved) ?? undefined;
return resolved ?? undefined;
};
let mounted: TApp;
const mount = () => {
const anchor = getAnchor();
if (anchor == null)
throw Error(
'Failed to mount content script ui: could not find anchor element',
);
// Mount UI inside shadow root
mounted = options.mount(uiContainer);
// Add shadow root element to DOM
switch (options.append) {
case undefined:
case 'last':
anchor.append(shadowHost);
break;
case 'first':
if (anchor.firstChild) {
anchor.insertBefore(shadowHost, anchor.firstChild);
} else {
anchor.append(shadowHost);
}
break;
case 'replace':
anchor.replaceWith(shadowHost);
break;
case 'after':
anchor.replaceWith(anchor, shadowHost);
break;
case 'before':
anchor.replaceWith(shadowHost, anchor);
break;
default:
options.append(anchor, shadowHost);
break;
}
// Apply types
if (options.type !== 'inline') {
if (options.zIndex != null)
shadowHost.style.zIndex = String(options.zIndex);
shadowHost.style.overflow = 'visible';
shadowHost.style.position = 'relative';
shadowHost.style.width = '0';
shadowHost.style.height = '0';
shadowHost.style.display = 'block';
const html = shadow.querySelector('html')!;
// HTML doesn't exist in tests
if (options.type === 'overlay') {
html.style.position = 'absolute';
if (options.alignment?.startsWith('bottom-')) html.style.bottom = '0';
else html.style.top = '0';
if (options.alignment?.endsWith('-right')) html.style.right = '0';
else html.style.left = '0';
} else {
html.style.position = 'fixed';
html.style.top = '0';
html.style.bottom = '0';
html.style.left = '0';
html.style.right = '0';
}
}
};
const remove = () => {
// Detatch shadow root from DOM
shadowHost.remove();
// Cleanup mounted state
options.onRemove?.(mounted);
// Remove children from uiContainer
while (uiContainer.lastChild)
uiContainer.removeChild(uiContainer.lastChild);
};
ctx.onInvalidated(remove);
return {
shadow,
shadowHost,
uiContainer,
mount,
remove,
mounted: mounted!,
};
}
/**
* Load the CSS for the current entrypoint.
*/
async function loadCss(): Promise<string> {
const url = browser.runtime.getURL(`/content-scripts/${__ENTRYPOINT__}.css`);
try {
const res = await fetch(url);
const css = await res.text();
// Replace :root selectors with :host since we're in a shadow root
return css.replaceAll(':root', ':host');
} catch (err) {
logger.warn(
`Failed to load styles @ ${url}. Did you forget to import the stylesheet in your entrypoint?`,
err,
);
return '';
}
}
export interface ContentScriptUi<TApp> {
/**
* The `HTMLElement` hosting the shadow root used to isolate the UI's styles. This is the element
* that get's added to the DOM. This element's style is not isolated from the webpage.
*/
shadowHost: HTMLElement;
/**
* The container element inside the `ShadowRoot` whose styles are isolated. The UI is mounted
* inside this `HTMLElement`.
*/
uiContainer: HTMLElement;
/**
* The shadow root performing the isolation.
*/
shadow: ShadowRoot;
/**
* Custom data returned from the `options.mount` function.
*/
mounted: TApp;
/**
* Function that mounts or remounts the UI on the page.
*/
mount: () => void;
/**
* Function that removes the UI from the webpage.
*/
remove: () => void;
}
interface BaseContentScriptUiOptions<TApp> {
/**
* The name of the custom component used to host the ShadowRoot. Must be kebab-case.
*/
name: string;
/**
* In combination with `anchor`, decide how to add the UI to the DOM.
*
* - `"last"` (default) - Add the UI as the last child of the `anchor` element
* - `"first"` - Add the UI as the last child of the `anchor` element
* - `"replace"` - Replace the `anchor` element with the UI.
* - `"before"` - Add the UI as the sibling before the `anchor` element
* - `"after"` - Add the UI as the sibling after the `anchor` element
* - `(anchor, ui) => void` - Customizable function that let's you add the UI to the DOM
*/
append?: ContentScriptAppendMode | ((anchor: Element, ui: Element) => void);
/**
* A CSS selector, element, or function that returns one of the two. Along with `append`, the
* `anchor` dictates where in the page the UI will be added.
*/
anchor?:
| string
| Element
| null
| undefined
| (() => string | Element | null | undefined);
/**
* Callback executed when mounting the UI. This function should create and append the UI to the
* `container` element. It is called every time `ui.mount()` is called
*
* Optionally return a value that can be accessed at `ui.mounted` or in the `onRemove` callback.
*/
mount: (container: Element) => TApp;
/**
* Callback called when the UI is removed from the webpage. Use to cleanup your UI, like
* unmounting your vue or react apps.
*/
onRemove?: (mounted: TApp) => void;
/**
* Custom CSS text to apply to the UI. If your content script imports/generates CSS and you've
* set `cssInjectionMode: "ui"`, the imported CSS will be included automatically. You do not need
* to pass those styles in here. This is for any additional styles not in the imported CSS.
*
* See https://wxt.dev/entrypoints/content-scripts.html#ui for more info.
*/
css?: string;
}
export type OverlayContentScriptUiOptions<TApp> =
BaseContentScriptUiOptions<TApp> & {
type: 'overlay';
/**
* When using `type: "overlay"`, the mounted element is 0px by 0px in size. Alignment specifies
* which corner is aligned with that 0x0 pixel space.
*
* @default "top-left"
*/
alignment?: ContentScriptUiOverlayAlignment;
/**
* The `z-index` used on the `shadowHost`. Set to a positive number to show your UI over website
* content.
*/
zIndex?: number;
};
export type ModalContentScriptUiOptions<TApp> =
BaseContentScriptUiOptions<TApp> & {
type: 'modal';
/**
* The `z-index` used on the `shadowHost`. Set to a positive number to show your UI over website
* content.
*/
zIndex?: number;
};
export type InlineContentScriptUiOptions<TApp> =
BaseContentScriptUiOptions<TApp> & {
type: 'inline';
};
export type ContentScriptUiOverlayAlignment =
| 'top-left'
| 'top-right'
| 'bottom-left'
| 'bottom-right';
export type ContentScriptAppendMode =
| 'last'
| 'first'
| 'replace'
| 'before'
| 'after';
export type ContentScriptUiOptions<TApp> =
| OverlayContentScriptUiOptions<TApp>
| ModalContentScriptUiOptions<TApp>
| InlineContentScriptUiOptions<TApp>;
@@ -1,4 +1,4 @@
import { BackgroundDefinition } from '..';
import { BackgroundDefinition } from '~/types';
export function defineBackground(main: () => void): BackgroundDefinition;
export function defineBackground(
+2 -4
View File
@@ -1,7 +1,5 @@
/**
* @module wxt/client
*/
export * from './defineContentScript';
export * from './defineBackground';
export * from './createContentScriptUi';
export * from './utils/ContentScriptContext';
export * from './define-background';
export * from './content-scripts';
-4
View File
@@ -1,4 +0,0 @@
/**
* @module wxt/sandbox
*/
export * from './defineUnlistedScript';
+145
View File
@@ -0,0 +1,145 @@
export type ContentScriptOverlayAlignment =
| 'top-left'
| 'top-right'
| 'bottom-left'
| 'bottom-right';
export type ContentScriptAppendMode =
| 'last'
| 'first'
| 'replace'
| 'before'
| 'after'
| ((anchor: Element, ui: Element) => void);
export function mountContentScriptUiRoot(
root: HTMLElement,
options: ContentScriptAnchoredOptions,
): void {
const anchor = getAnchor(options);
if (anchor == null)
throw Error(
'Failed to mount content script UI: could not find anchor element',
);
switch (options.append) {
case undefined:
case 'last':
anchor.append(root);
break;
case 'first':
if (anchor.firstChild) {
anchor.insertBefore(root, anchor.firstChild);
} else {
anchor.append(root);
}
break;
case 'replace':
anchor.replaceWith(root);
break;
case 'after':
anchor.replaceWith(anchor, root);
break;
case 'before':
anchor.replaceWith(root, anchor);
break;
default:
options.append(anchor, root);
break;
}
}
export type ContentScriptPositioningOptions =
| { type: 'inline' }
| {
type: 'overlay';
/**
* The `z-index` used on the `shadowHost`. Set to a positive number to show your UI over website
* content.
*/
zIndex?: number;
/**
* When using `type: "overlay"`, the mounted element is 0px by 0px in size. Alignment specifies
* which corner is aligned with that 0x0 pixel space.
*
* @default "top-left"
*/
alignment?: ContentScriptOverlayAlignment;
}
| {
type: 'modal';
/**
* The `z-index` used on the `shadowHost`. Set to a positive number to show your UI over website
* content.
*/
zIndex?: number;
};
export function applyContentScriptUiPosition(
root: HTMLElement,
positionedElement: HTMLElement | undefined | null,
options: ContentScriptPositioningOptions,
): void {
if (options.type !== 'inline') {
if (options.zIndex != null) root.style.zIndex = String(options.zIndex);
root.style.overflow = 'visible';
root.style.position = 'relative';
root.style.width = '0';
root.style.height = '0';
root.style.display = 'block';
if (positionedElement) {
if (options.type === 'overlay') {
positionedElement.style.position = 'absolute';
if (options.alignment?.startsWith('bottom-'))
positionedElement.style.bottom = '0';
else positionedElement.style.top = '0';
if (options.alignment?.endsWith('-right'))
positionedElement.style.right = '0';
else positionedElement.style.left = '0';
} else {
positionedElement.style.position = 'fixed';
positionedElement.style.top = '0';
positionedElement.style.bottom = '0';
positionedElement.style.left = '0';
positionedElement.style.right = '0';
}
}
}
}
function getAnchor(options: ContentScriptAnchoredOptions): Element | undefined {
if (options.anchor == null) return document.body;
let resolved =
typeof options.anchor === 'function' ? options.anchor() : options.anchor;
if (typeof resolved === 'string')
return document.querySelector<Element>(resolved) ?? undefined;
return resolved ?? undefined;
}
export interface ContentScriptAnchoredOptions {
/**
* A CSS selector, element, or function that returns one of the two. Along with `append`, the
* `anchor` dictates where in the page the UI will be added.
*/
anchor?:
| string
| Element
| null
| undefined
| (() => string | Element | null | undefined);
/**
* In combination with `anchor`, decide how to add the UI to the DOM.
*
* - `"last"` (default) - Add the UI as the last child of the `anchor` element
* - `"first"` - Add the UI as the last child of the `anchor` element
* - `"replace"` - Replace the `anchor` element with the UI.
* - `"before"` - Add the UI as the sibling before the `anchor` element
* - `"after"` - Add the UI as the sibling after the `anchor` element
* - `(anchor, ui) => void` - Customizable function that let's you add the UI to the DOM
*/
append?: ContentScriptAppendMode | ((anchor: Element, ui: Element) => void);
}
@@ -1,3 +0,0 @@
import { fakeBrowser as browser } from '../../testing/fake-browser';
export default browser;
+14 -138
View File
@@ -1,144 +1,20 @@
import { Manifest } from 'webextension-polyfill';
import { BuildOutput } from './types';
import { buildEntrypoints } from './build/buildEntrypoints';
import { findEntrypoints } from './build/findEntrypoints';
import { generateTypesDir } from './build/generateTypesDir';
import { InternalConfig, EntrypointGroup } from './types';
import { generateMainfest, writeManifest } from './utils/manifest';
import pc from 'picocolors';
import * as vite from 'vite';
import fs from 'fs-extra';
import { groupEntrypoints } from './utils/groupEntrypoints';
import { formatDuration } from './utils/formatDuration';
import { printBuildSummary } from './log/printBuildSummary';
import { execaCommand } from 'execa';
import glob from 'fast-glob';
import { unnormalizePath } from './utils/paths';
import { BuildOutput, InlineConfig } from '~/types';
import { getInternalConfig, internalBuild } from './utils/building';
/**
* Builds the extension based on an internal config.
* Bundles the extension for production. Returns a promise of the build result. Discovers the `wxt.config.ts` file in
* the root directory, and merges that config with what is passed in.
*
* This function:
* 1. Cleans the output directory
* 2. Executes the rebuild function with a blank previous output so everything is built (see
* `rebuild` for more details)
* 3. Prints the summary
*/
export async function buildInternal(
config: InternalConfig,
): Promise<BuildOutput> {
const verb = config.command === 'serve' ? 'Pre-rendering' : 'Building';
const target = `${config.browser}-mv${config.manifestVersion}`;
config.logger.info(
`${verb} ${pc.cyan(target)} for ${pc.cyan(config.mode)} with ${pc.green(
`Vite ${vite.version}`,
)}`,
);
const startTime = Date.now();
// Cleanup
await fs.rm(config.outDir, { recursive: true, force: true });
await fs.ensureDir(config.outDir);
const entrypoints = await findEntrypoints(config);
config.logger.debug('Detected entrypoints:', entrypoints);
const groups = groupEntrypoints(entrypoints);
const { output } = await rebuild(config, groups, undefined);
// Post-build
await printBuildSummary(
config.logger.success,
`Built extension in ${formatDuration(Date.now() - startTime)}`,
output,
config,
);
if (config.analysis.enabled) {
await combineAnalysisStats(config);
config.logger.info(
`Analysis complete:\n ${pc.gray('└─')} ${pc.yellow('stats.html')}`,
);
}
return output;
}
/**
* Given a configuration, list of entrypoints, and an existing, partial output, build the
* entrypoints and merge the new output with the existing output.
* @example
* // Use config from `wxt.config.ts`
* const res = await build()
*
* This function will:
* 1. Generate the .wxt directory's types
* 2. Build the `entrypointGroups` (and copies public files)
* 3. Generate the latest manifest for all entrypoints
* 4. Write the new manifest to the file system
* // or override config `from wxt.config.ts`
* const res = await build({
* // Override config...
* })
*/
export async function rebuild(
config: InternalConfig,
entrypointGroups: EntrypointGroup[],
existingOutput: Omit<BuildOutput, 'manifest'> = {
steps: [],
publicAssets: [],
},
): Promise<{ output: BuildOutput; manifest: Manifest.WebExtensionManifest }> {
const { default: ora } = await import('ora');
const spinner = ora(`Preparing...`).start();
// Update types directory with new files and types
const allEntrypoints = await findEntrypoints(config);
await generateTypesDir(allEntrypoints, config).catch((err) => {
config.logger.warn('Failed to update .wxt directory:', err);
// Throw the error if doing a regular build, don't for dev mode.
if (config.command === 'build') throw err;
});
// Build and merge the outputs
const newOutput = await buildEntrypoints(entrypointGroups, config, spinner);
const mergedOutput: Omit<BuildOutput, 'manifest'> = {
steps: [...existingOutput.steps, ...newOutput.steps],
publicAssets: [...existingOutput.publicAssets, ...newOutput.publicAssets],
};
const newManifest = await generateMainfest(
allEntrypoints,
mergedOutput,
config,
);
const finalOutput: BuildOutput = {
manifest: newManifest,
...newOutput,
};
// Write manifest
await writeManifest(newManifest, finalOutput, config);
// Stop the spinner and remove it from the CLI output
spinner.clear().stop();
return {
output: {
manifest: newManifest,
steps: [...existingOutput.steps, ...finalOutput.steps],
publicAssets: [
...existingOutput.publicAssets,
...finalOutput.publicAssets,
],
},
manifest: newManifest,
};
}
async function combineAnalysisStats(config: InternalConfig): Promise<void> {
const unixFiles = await glob(`stats-*.json`, {
cwd: config.outDir,
absolute: true,
});
const absolutePaths = unixFiles.map(unnormalizePath);
await execaCommand(
`rollup-plugin-visualizer ${absolutePaths.join(' ')} --template ${
config.analysis.template
}`,
{ cwd: config.root, stdio: 'inherit' },
);
export async function build(config?: InlineConfig): Promise<BuildOutput> {
const internalConfig = await getInternalConfig(config ?? {}, 'build');
return await internalBuild(internalConfig);
}
+253
View File
@@ -0,0 +1,253 @@
import {
BuildStepOutput,
EntrypointGroup,
InlineConfig,
InternalConfig,
WxtDevServer,
} from '~/types';
import * as vite from 'vite';
import type { Scripting } from 'webextension-polyfill';
import {
getEntrypointBundlePath,
getEntrypointOutputFile,
resolvePerBrowserOption,
} from '~/core/utils/entrypoints';
import {
getContentScriptCssFiles,
getContentScriptsCssMap,
} from '~/core/utils/manifest';
import {
internalBuild,
getInternalConfig,
detectDevChanges,
rebuild,
} from '~/core/utils/building';
import { createExtensionRunner } from '~/core/runners';
import { consola } from 'consola';
import { Mutex } from 'async-mutex';
import pc from 'picocolors';
import { relative } from 'node:path';
/**
* Creates a dev server and pre-builds all the files that need to exist before loading the extension.
*
* @example
* const server = await wxt.createServer({
* // Enter config...
* });
* await server.start();
*/
export async function createServer(
config?: InlineConfig,
): Promise<WxtDevServer> {
const serverInfo = await getServerInfo();
const getLatestInternalConfig = async () => {
return getInternalConfig(
{
...config,
vite: () => serverInfo.viteServerConfig,
},
'serve',
);
};
let internalConfig = await getLatestInternalConfig();
const server = await setupServer(serverInfo, internalConfig);
internalConfig.server = server;
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);
});
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;
changeQueue.push([event, path]);
await fileChangedMutex.runExclusive(async () => {
const fileChanges = changeQueue.splice(0, changeQueue.length);
if (fileChanges.length === 0) return;
const changes = detectDevChanges(fileChanges, server.currentOutput);
if (changes.type === 'no-change') return;
// Log the entrypoints that were effected
internalConfig.logger.info(
`Changed: ${Array.from(new Set(fileChanges.map((change) => change[1])))
.map((file) => pc.dim(relative(internalConfig.root, file)))
.join(', ')}`,
);
const rebuiltNames = changes.rebuildGroups
.flat()
.map((entry) => {
return pc.cyan(
relative(internalConfig.outDir, getEntrypointOutputFile(entry, '')),
);
})
.join(pc.dim(', '));
// Get latest config and Rebuild groups with changes
internalConfig = await getLatestInternalConfig();
internalConfig.server = server;
const { output: newOutput } = await rebuild(
internalConfig,
// TODO: this excludes new entrypoints, so they're not built until the dev command is restarted
changes.rebuildGroups,
changes.cachedOutput,
);
server.currentOutput = newOutput;
// Perform reloads
switch (changes.type) {
case 'extension-reload':
server.reloadExtension();
break;
case 'html-reload':
reloadHtmlPages(changes.rebuildGroups, server, internalConfig);
break;
case 'content-script-reload':
reloadContentScripts(changes.changedSteps, internalConfig, 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;
}
/**
* From the server, tell the client to reload content scripts from the provided build step outputs.
*/
function reloadContentScripts(
steps: BuildStepOutput[],
config: InternalConfig,
server: WxtDevServer,
) {
if (config.manifestVersion === 3) {
steps.forEach((step) => {
const entry = step.entrypoints;
if (Array.isArray(entry) || entry.type !== 'content-script') return;
const js = [getEntrypointBundlePath(entry, config.outDir, '.js')];
const cssMap = getContentScriptsCssMap(server.currentOutput, [entry]);
const css = getContentScriptCssFiles([entry], cssMap);
server.reloadContentScript({
allFrames: resolvePerBrowserOption(
entry.options.allFrames,
config.browser,
),
excludeMatches: resolvePerBrowserOption(
entry.options.excludeMatches,
config.browser,
),
matches: resolvePerBrowserOption(entry.options.matches, config.browser),
runAt: resolvePerBrowserOption(entry.options.runAt, config.browser),
// @ts-expect-error: Chrome accepts this, not typed in webextension-polyfill (https://developer.chrome.com/docs/extensions/reference/scripting/#type-RegisteredContentScript)
world: resolvePerBrowserOption(entry.options.world, config.browser),
js,
css,
});
});
} else {
server.reloadExtension();
}
}
function reloadHtmlPages(
groups: EntrypointGroup[],
server: WxtDevServer,
config: InternalConfig,
) {
groups.flat().forEach((entry) => {
const path = getEntrypointBundlePath(entry, config.outDir, '.html');
server.reloadPage(path);
});
}
interface ServerInfo {
port: number;
hostname: string;
origin: string;
viteServerConfig: vite.InlineConfig;
}
@@ -1,4 +1,4 @@
import { UserConfig } from '../types';
import { UserConfig } from '~/types';
export function defineConfig(config: UserConfig): UserConfig {
return config;
@@ -1,4 +1,4 @@
import { ExtensionRunnerConfig } from '../types';
import { ExtensionRunnerConfig } from '~/types';
export function defineRunnerConfig(
config: ExtensionRunnerConfig,
+8
View File
@@ -0,0 +1,8 @@
export * from './build';
export * from './clean';
export * from './define-config';
export * from './define-runner-config';
export * from './create-server';
export * from './initialize';
export * from './prepare';
export * from './zip';
@@ -1,4 +1,3 @@
import { defineCommand } from '../utils/defineCommand';
import prompts from 'prompts';
import { consola } from 'consola';
import { downloadTemplate } from 'giget';
@@ -7,76 +6,76 @@ import path from 'node:path';
import pc from 'picocolors';
import { Formatter } from 'picocolors/types';
export const init = defineCommand<
[
directory: string | undefined,
options: { template?: string; pm?: string; debug?: boolean },
]
>(
async (userDirectory, flags) => {
consola.info('Initalizing new project');
export async function initialize(options: {
directory: string;
template: string;
packageManager: string;
}) {
consola.info('Initalizing new project');
const templates = await listTemplates();
const defaultTemplate = templates.find(
(template) => template.name === flags.template?.toLowerCase().trim(),
);
const templates = await listTemplates();
const defaultTemplate = templates.find(
(template) => template.name === options.template?.toLowerCase().trim(),
);
const input = await prompts(
[
{
name: 'directory',
type: () => (userDirectory == null ? 'text' : undefined),
message: 'Project Directory',
initial: userDirectory,
},
{
name: 'template',
type: () => (defaultTemplate == null ? 'select' : undefined),
message: 'Choose a template',
choices: templates.map((template) => ({
title:
TEMPLATE_COLORS[template.name]?.(template.name) ?? template.name,
value: template,
})),
},
{
name: 'packageManager',
type: () => (flags.pm == null ? 'select' : undefined),
message: 'Package Manager',
choices: [
{ title: 'npm', value: 'npm' },
{ title: 'pnpm', value: 'pnpm' },
{ title: 'yarn', value: 'yarn' },
],
},
],
const input = await prompts(
[
{
onCancel: () => process.exit(1),
name: 'directory',
type: () => (options.directory == null ? 'text' : undefined),
message: 'Project Directory',
initial: options.directory,
},
);
input.directory ??= userDirectory;
input.template ??= defaultTemplate;
input.packageManager ??= flags.pm;
{
name: 'template',
type: () => (defaultTemplate == null ? 'select' : undefined),
message: 'Choose a template',
choices: templates.map((template) => ({
title:
TEMPLATE_COLORS[template.name]?.(template.name) ?? template.name,
value: template,
})),
},
{
name: 'packageManager',
type: () => (options.packageManager == null ? 'select' : undefined),
message: 'Package Manager',
choices: [
{ 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',
},
],
},
],
{
onCancel: () => process.exit(1),
},
);
input.directory ??= options.directory;
input.template ??= defaultTemplate;
input.packageManager ??= options.packageManager;
await cloneProject(input);
await cloneProject(input);
const cdPath = path.relative(process.cwd(), path.resolve(input.directory));
console.log();
consola.log(
`✨ WXT project created with the ${
TEMPLATE_COLORS[input.template.name]?.(input.template.name) ??
input.template.name
} template.`,
);
console.log();
consola.log('Next steps:');
let step = 0;
if (cdPath !== '') consola.log(` ${++step}.`, pc.cyan(`cd ${cdPath}`));
consola.log(` ${++step}.`, pc.cyan(`${input.packageManager} install`));
console.log();
},
{ disableFinishedLog: true },
);
const cdPath = path.relative(process.cwd(), path.resolve(input.directory));
console.log();
consola.log(
`✨ WXT project created with the ${
TEMPLATE_COLORS[input.template.name]?.(input.template.name) ??
input.template.name
} template.`,
);
console.log();
consola.log('Next steps:');
let step = 0;
if (cdPath !== '') consola.log(` ${++step}.`, pc.cyan(`cd ${cdPath}`));
consola.log(` ${++step}.`, pc.cyan(`${input.packageManager} install`));
console.log();
}
interface Template {
/**
+15
View File
@@ -0,0 +1,15 @@
import { InlineConfig } from '~/types';
import {
findEntrypoints,
generateTypesDir,
getInternalConfig,
} from '~/core/utils/building';
export async function prepare(config: InlineConfig) {
const internalConfig = await getInternalConfig(config, 'build');
internalConfig.logger.info('Generating types...');
const entrypoints = await findEntrypoints(internalConfig);
await generateTypesDir(entrypoints, internalConfig);
}
+2 -2
View File
@@ -1,13 +1,13 @@
import { describe, expect, it, vi } from 'vitest';
import { createExtensionRunner } from '..';
import { fakeInternalConfig } from '../../../testing-utils/fake-objects';
import { fakeInternalConfig } from '~/core/utils/testing/fake-objects';
import { mock } from 'vitest-mock-extended';
import { createSafariRunner } from '../safari';
import { ExtensionRunner } from '../extension-runner';
import { createWslRunner } from '../wsl';
import { createManualRunner } from '../manual';
import { isWsl } from '../../utils/wsl';
import { createWebExtRunner } from '../web-ext';
import { ExtensionRunner } from '~/types';
vi.mock('../../utils/wsl');
const isWslMock = vi.mocked(isWsl);
-6
View File
@@ -1,6 +0,0 @@
import { InternalConfig } from '../types';
export interface ExtensionRunner {
openBrowser(config: InternalConfig): Promise<void>;
closeBrowser(): Promise<void>;
}
+2 -3
View File
@@ -1,10 +1,9 @@
import { InternalConfig } from '../types';
import { ExtensionRunner } from './extension-runner';
import { InternalConfig, ExtensionRunner } from '~/types';
import { createWslRunner } from './wsl';
import { createWebExtRunner } from './web-ext';
import { createSafariRunner } from './safari';
import { createManualRunner } from './manual';
import { isWsl } from '../utils/wsl';
import { isWsl } from '~/core/utils/wsl';
export async function createExtensionRunner(
config: InternalConfig,
+1 -1
View File
@@ -1,4 +1,4 @@
import { ExtensionRunner } from './extension-runner';
import { ExtensionRunner } from '~/types';
import { relative } from 'node:path';
/**
+1 -1
View File
@@ -1,4 +1,4 @@
import { ExtensionRunner } from './extension-runner';
import { ExtensionRunner } from '~/types';
import { relative } from 'node:path';
/**
+7 -1
View File
@@ -1,5 +1,5 @@
import type { WebExtRunInstance } from 'web-ext-run';
import { ExtensionRunner } from './extension-runner';
import { ExtensionRunner } from '~/types';
/**
* Create an `ExtensionRunner` backed by `web-ext`.
@@ -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 }) => {
+1 -1
View File
@@ -1,4 +1,4 @@
import { ExtensionRunner } from './extension-runner';
import { ExtensionRunner } from '~/types';
import { relative } from 'node:path';
/**
-149
View File
@@ -1,149 +0,0 @@
import {
BuildStepOutput,
EntrypointGroup,
InternalConfig,
WxtDevServer,
} from './types';
import * as vite from 'vite';
import { Scripting } from 'webextension-polyfill';
import {
getEntrypointBundlePath,
resolvePerBrowserOption,
} from './utils/entrypoints';
import {
getContentScriptCssFiles,
getContentScriptsCssMap,
} from './utils/manifest';
import { buildInternal } from './build';
import { createExtensionRunner } from './runners';
export 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,
};
}
export 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 buildInternal(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;
}
/**
* From the server, tell the client to reload content scripts from the provided build step outputs.
*/
export function reloadContentScripts(
steps: BuildStepOutput[],
config: InternalConfig,
server: WxtDevServer,
) {
if (config.manifestVersion === 3) {
steps.forEach((step) => {
const entry = step.entrypoints;
if (Array.isArray(entry) || entry.type !== 'content-script') return;
const js = [getEntrypointBundlePath(entry, config.outDir, '.js')];
const cssMap = getContentScriptsCssMap(server.currentOutput, [entry]);
const css = getContentScriptCssFiles([entry], cssMap);
server.reloadContentScript({
allFrames: resolvePerBrowserOption(
entry.options.allFrames,
config.browser,
),
excludeMatches: resolvePerBrowserOption(
entry.options.excludeMatches,
config.browser,
),
matches: resolvePerBrowserOption(entry.options.matches, config.browser),
runAt: resolvePerBrowserOption(entry.options.runAt, config.browser),
// @ts-expect-error: Chrome accepts this, not typed in webextension-polyfill (https://developer.chrome.com/docs/extensions/reference/scripting/#type-RegisteredContentScript)
world: resolvePerBrowserOption(entry.options.world, config.browser),
js,
css,
});
});
} else {
server.reloadExtension();
}
}
export function reloadHtmlPages(
groups: EntrypointGroup[],
server: WxtDevServer,
config: InternalConfig,
) {
groups.flat().forEach((entry) => {
const path = getEntrypointBundlePath(entry, config.outDir, '.html');
server.reloadPage(path);
});
}
interface ServerInfo {
port: number;
hostname: string;
origin: string;
viteServerConfig: vite.InlineConfig;
}
+1 -1
View File
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { every } from '../arrays';
import { every } from '~/core/utils/arrays';
describe('Array Utils', () => {
describe('every', () => {
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import { hashContentScriptOptions } from '../content-scripts';
import { fakeInternalConfig } from '../../../testing-utils/fake-objects';
import { hashContentScriptOptions } from '~/core/utils/content-scripts';
import { fakeInternalConfig } from '~/core/utils/testing/fake-objects';
describe('Content Script Utils', () => {
describe('hashContentScriptOptions', () => {
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { ContentSecurityPolicy } from '../ContentSecurityPolicy';
import { ContentSecurityPolicy } from '~/core/utils/content-security-policy';
describe('Content Security Policy Builder', () => {
it('should add values to new directives correctly', () => {
+2 -2
View File
@@ -3,8 +3,8 @@ import {
getEntrypointName,
getEntrypointOutputFile,
resolvePerBrowserOption,
} from '../entrypoints';
import { Entrypoint } from '../../types';
} from '~/core/utils/entrypoints';
import { Entrypoint } from '~/types';
import { resolve } from 'path';
describe('Entrypoint Utils', () => {
+26
View File
@@ -0,0 +1,26 @@
import { describe, it, expect } from 'vitest';
import { getPackageJson } from '../package';
import { fakeInternalConfig } from '../testing/fake-objects';
import { mock } from 'vitest-mock-extended';
import { Logger } from '~/types';
describe('Package JSON Utils', () => {
describe('getPackageJson', () => {
it('should return the package.json inside <root>/package.json', async () => {
const root = process.cwd(); // WXT project directory
const actual = await getPackageJson(fakeInternalConfig({ root }));
expect(actual).toMatchObject({
name: 'wxt',
});
});
it("should return an empty object when <root>/package.json doesn't exist", async () => {
const root = '/some/path/that/does/not/exist';
const logger = mock<Logger>();
const actual = await getPackageJson(fakeInternalConfig({ root, logger }));
expect(actual).toEqual({});
});
});
});
+24
View File
@@ -0,0 +1,24 @@
import { describe, expect, it } from 'vitest';
import { normalizePath } from '../paths';
describe('Path Utils', () => {
describe('normalizePath', () => {
it.each([
// Relative paths
['../test.sh', '../test.sh'],
['..\\test.sh', '../test.sh'],
['test.png', 'test.png'],
// Absolute paths
['C:\\\\path\\to\\file', 'C:/path/to/file'],
['/path/to/file', '/path/to/file'],
// Strip trailing slash
['C:\\\\path\\to\\folder\\', 'C:/path/to/folder'],
['/path/to/folder/', '/path/to/folder'],
// Dedupe slashes
['path\\\\\\file', 'path/file'],
['path//file', 'path/file'],
])('should normalize "%s" to "%s"', (input, expected) => {
expect(normalizePath(input)).toBe(expected);
});
});
});
+4 -1
View File
@@ -1,5 +1,8 @@
import { describe, expect, it } from 'vitest';
import { kebabCaseAlphanumeric, removeImportStatements } from '../strings';
import {
kebabCaseAlphanumeric,
removeImportStatements,
} from '~/core/utils/strings';
describe('String utils', () => {
describe('kebabCaseAlphanumeric', () => {
@@ -1,4 +1,4 @@
import { defineBackground } from '../../../../client';
import { defineBackground } from '~/client';
export default defineBackground({
main() {},
@@ -1,4 +1,4 @@
import { defineContentScript } from '../../../../client';
import { defineContentScript } from '~/client';
export default defineContentScript({
matches: ['<all_urls>'],
@@ -1,3 +1,3 @@
import { defineUnlistedScript } from '../../../../client/sandbox';
import { defineUnlistedScript } from '~/sandbox';
export default defineUnlistedScript(() => {});
@@ -1,3 +1,3 @@
import { defineUnlistedScript } from '../../../../client/sandbox';
import { defineUnlistedScript } from '~/sandbox';
export default defineUnlistedScript(() => {});
@@ -1,4 +1,4 @@
import { defineBackground } from '../../../../client';
import { defineBackground } from '~/client';
export const a = {};

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