Compare commits
270 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8687b2980e | |||
| 6f0814bc97 | |||
| 148c0da51f | |||
| ba09f3e0f1 | |||
| 54e24652da | |||
| d6516142d2 | |||
| 460667e3c0 | |||
| 6ed61d0766 | |||
| 971daf2784 | |||
| 89d8ef9a50 | |||
| a329e24818 | |||
| 249cf63da8 | |||
| 70a496108a | |||
| 214889e052 | |||
| b6ab7a9567 | |||
| 1ceb9e2e80 | |||
| 15ea81514f | |||
| 21d4e6dede | |||
| 5343430c0f | |||
| 8c834835c4 | |||
| 9a2cc18f64 | |||
| 8683bd4b77 | |||
| dca52864bb | |||
| 0c5643f648 | |||
| b7cdf1597c | |||
| 2770974261 | |||
| ab6a34afb6 | |||
| b0eb7da9b4 | |||
| 4f649fc872 | |||
| 1af64aa02f | |||
| c35acff259 | |||
| 995d4f8080 | |||
| b7e2aa3d8f | |||
| e347f4529f | |||
| 3d9683418d | |||
| bdce363eed | |||
| e7775f291a | |||
| 59f6a28c65 | |||
| f7534464bc | |||
| 01445fb1da | |||
| 1360eb7c01 | |||
| c99a281169 | |||
| 76e63e24c0 | |||
| f5ba1173da | |||
| 12079e6ffc | |||
| ff6740d429 | |||
| e6657cb533 | |||
| 2ad099bf7e | |||
| 6421ab34a4 | |||
| 8919c4015a | |||
| 712f2ed4ac | |||
| d1b9e5ded6 | |||
| 86182044d1 | |||
| e1f181356c | |||
| 5f241cf407 | |||
| b4a2cf4cb2 | |||
| aea137a3a1 | |||
| ad0e8e4144 | |||
| 7fd5752ef1 | |||
| 1a8b7ff635 | |||
| fe3ca0fcbd | |||
| f49ee9f005 | |||
| 961440c0ac | |||
| 9b562b0ca8 | |||
| 7b2563b2dc | |||
| 63f507ecf5 | |||
| 0a88955394 | |||
| 291d25b675 | |||
| 6f902cc598 | |||
| fde488ac82 | |||
| 0873c24ad8 | |||
| 7c02497148 | |||
| 25441a3b97 | |||
| f1cf866fe1 | |||
| d92a126489 | |||
| bcb93afe4e | |||
| 21cf3642cb | |||
| 2ef28ec0c4 | |||
| 59094c9af3 | |||
| ba41691296 | |||
| 8786aa91b3 | |||
| 0623d10653 | |||
| 85174d4994 | |||
| 4e2dd5d618 | |||
| 1a39a0d153 | |||
| f922648dd7 | |||
| e6142e3608 | |||
| ed73451fd0 | |||
| 8a25a2f60f | |||
| 397e9a8b45 | |||
| 7cef7680de | |||
| 0c57375758 | |||
| 2c6af84165 | |||
| 58e1371701 | |||
| 0d8e7463b8 | |||
| 77eeacaf0f | |||
| e80c134150 | |||
| 03c8ab4d6f | |||
| df13b9705c | |||
| 762ba0080d | |||
| 317b1b6dcc | |||
| 93175a6477 | |||
| 60d6707b11 | |||
| 97cbda3dab | |||
| 1611c1dba6 | |||
| b59252284a | |||
| 742b99657a | |||
| e2997a43e0 | |||
| 21dead60fd | |||
| 3d1bc0a12c | |||
| 4b24bee1fb | |||
| 82ed821eb5 | |||
| 60625280c2 | |||
| 083792b5f0 | |||
| 3da3e07bda | |||
| 64d61eb3f1 | |||
| aa6009e476 | |||
| d2cb8f9416 | |||
| 046a4809d3 | |||
| 207b750d4e | |||
| 077fa9939e | |||
| 44d715a29e | |||
| 3ab9fe4ae3 | |||
| 91b28c2cb9 | |||
| d1b523061f | |||
| c69ea3967f | |||
| 41527a2200 | |||
| 281f28192d | |||
| 67ffa44f9c | |||
| 421c0e412d | |||
| d21ee08eb6 | |||
| 7fa150dfae | |||
| 7ba52b18fd | |||
| 66e5079b84 | |||
| f1e8084be8 | |||
| 609ae2ae37 | |||
| c81dfff37c | |||
| 9c27820add | |||
| 22f8e10918 | |||
| 96d41f8b7c | |||
| c4ce44e001 | |||
| 6641ffaeed | |||
| 450dc0975f | |||
| d343555f41 | |||
| 7993908c6c | |||
| 94e1dbce24 | |||
| d14215946a | |||
| 345406f02a | |||
| 6b689bab23 | |||
| fcdf0dcbe9 | |||
| bd35acdae1 | |||
| 21c35ee593 | |||
| 0f09cbee0e | |||
| 183bb02e29 | |||
| 0bf4ea0897 | |||
| cd4d00e23e | |||
| 5de18e5371 | |||
| af823418ee | |||
| 0b8d101c33 | |||
| b587849126 | |||
| 2a35ce0a1e | |||
| c390b70f76 | |||
| c9fd739128 | |||
| 0806c06042 | |||
| 5f74a544a4 | |||
| 121778894c | |||
| 102c72a03c | |||
| a0507866db | |||
| f3874da780 | |||
| cc5d24ec92 | |||
| 08760015da | |||
| 5368371020 | |||
| 08d62a437a | |||
| 37e2348d79 | |||
| 6a30dc46fa | |||
| b75c553e51 | |||
| a56face03d | |||
| 19756c61ce | |||
| 5f54b4de17 | |||
| ea8935c47c | |||
| e37f73880d | |||
| 2e24b9e18a | |||
| f8a0fb37a2 | |||
| dd26b99027 | |||
| d9e9b43f8d | |||
| d580083727 | |||
| 5a70d9e57d | |||
| 1b1af245bd | |||
| 874a531a62 | |||
| a3d409f250 | |||
| b6758ca9fa | |||
| 2672308946 | |||
| 7ac171ed3b | |||
| 6a93f20bb3 | |||
| 921af6a5a1 | |||
| 21ebeacd20 | |||
| 527600031f | |||
| 91a804c492 | |||
| 54f3785063 | |||
| 44e4bc5295 | |||
| f464d7d33f | |||
| d0672739f0 | |||
| 44464f914f | |||
| 8940c41bdb | |||
| 446f265b6c | |||
| 7a465684c0 | |||
| 0369316463 | |||
| 8b74291d18 | |||
| 739b738100 | |||
| 95442dd39b | |||
| 4f82645c08 | |||
| d200c376f8 | |||
| b58fb02016 | |||
| e1bab6c746 | |||
| d10c22fbd9 | |||
| da1f7f3ea5 | |||
| 2659272f8a | |||
| 868fd27804 | |||
| d9fdcb5b33 | |||
| 580793158f | |||
| f58d69dc5f | |||
| c9028dd335 | |||
| 6342f59c5e | |||
| 2d91898452 | |||
| 4acefd601c | |||
| 9a2e71b481 | |||
| 6e578f3156 | |||
| 474de83e28 | |||
| 65fcfc0064 | |||
| 2d4983e88b | |||
| 4150e42c05 | |||
| 8d7150653a | |||
| 61e57b7b7c | |||
| 83add72312 | |||
| 3c723d2c92 | |||
| 1eb35c7207 | |||
| 725ecf7c64 | |||
| 3847092df4 | |||
| 19b11c236f | |||
| 4b2012c489 | |||
| 0591050f31 | |||
| b15dc7fc11 | |||
| 89d15babc2 | |||
| c616125689 | |||
| e97071b7d7 | |||
| 1fa049c5a7 | |||
| b48cee9715 | |||
| 24e69fe1bf | |||
| 41e154992a | |||
| 55707932a9 | |||
| 08115a40ae | |||
| 96be879918 | |||
| 1fc4ada66d | |||
| d27f299641 | |||
| 7183114370 | |||
| 7bd940450f | |||
| cf5a7d1411 | |||
| 5482b2f934 | |||
| c4a6ff928d | |||
| 455e7f3765 | |||
| 25b6ab92f5 | |||
| 50f6289ac5 | |||
| a0e1b4741e | |||
| ab672e9dbd | |||
| 7b3ea52e02 | |||
| e0929a68ba | |||
| 4c430725a5 | |||
| 7d745f6ec7 | |||
| 10091d7769 | |||
| 2e142b3038 |
@@ -0,0 +1,8 @@
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
informational: true
|
||||
patch:
|
||||
default:
|
||||
informational: true
|
||||
@@ -12,14 +12,22 @@ A clear and concise description of what the bug is.
|
||||
|
||||
### To Reproduce
|
||||
|
||||
Upload a ZIP or share a link to a repo representing the minimal reproduction. **_If you don't upload a minimal reproduction, you bug report will be closed._**
|
||||
Share minimal reproduction. Examples of a minimal reproduction:
|
||||
|
||||
Steps to reproduce the behavior:
|
||||
- PR with a failing test case
|
||||
- ZIP file containing a minimal WXT project (be sure to include your lockfile!!)
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
> **_⚠️ If you don't upload a minimal reproduction, your issue will be closed until a reproduction is added._**
|
||||
>
|
||||
> Why? https://antfu.me/posts/why-reproductions-are-required
|
||||
|
||||
Steps to reproduce the bug using the reproduction:
|
||||
|
||||
1. Install dependencies: `pnpm i`
|
||||
2. Start dev mode: `pnpm dev`
|
||||
3. Click this...
|
||||
4. Do that...
|
||||
5. Etc...
|
||||
|
||||
### Expected behavior
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
@@ -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: |
|
||||
|
||||
@@ -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 }}
|
||||
+71
-110
@@ -7,115 +7,76 @@ on:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
wxt:
|
||||
name: WXT
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
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
|
||||
working-directory: demo
|
||||
|
||||
- name: Tests
|
||||
run: pnpm test:coverage --reporter=default --reporter=hanging-process
|
||||
|
||||
project-templates:
|
||||
name: Project Templates
|
||||
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: 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 format:check
|
||||
lint:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- run: pnpm lint
|
||||
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
|
||||
- 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
|
||||
- uses: codecov/codecov-action@v3
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
windows-tests:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- 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:
|
||||
- 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: pnpm compile
|
||||
if: matrix.template != 'svelte'
|
||||
working-directory: templates/${{ matrix.template }}
|
||||
- run: pnpm check
|
||||
if: matrix.template == 'svelte'
|
||||
working-directory: templates/${{ matrix.template }}
|
||||
- run: pnpm build
|
||||
working-directory: templates/${{ matrix.template }}
|
||||
|
||||
+3
-1
@@ -17,4 +17,6 @@ web-ext.config.ts
|
||||
templates/*/pnpm-lock.yaml
|
||||
templates/*/yarn.lock
|
||||
templates/*/package-lock.json
|
||||
docs/config.md
|
||||
docs/api
|
||||
stats.html
|
||||
.tool-versions
|
||||
|
||||
@@ -5,8 +5,3 @@ dist
|
||||
docs/.vitepress/cache
|
||||
pnpm-lock.yaml
|
||||
CHANGELOG.md
|
||||
.gitattributes
|
||||
.gitignore
|
||||
.prettierignore
|
||||
*.png
|
||||
*.svg
|
||||
|
||||
+775
-115
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
|
||||
@@ -3,6 +3,16 @@
|
||||
<span>WXT</span>
|
||||
</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>
|
||||
<span> </span>
|
||||
<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>
|
||||
<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>
|
||||
<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">
|
||||
<span>Next-gen framework for developing web extensions.</span>
|
||||
<br/>
|
||||
@@ -14,15 +24,19 @@
|
||||
<p align="center">
|
||||
<a href="https://wxt.dev" target="_blank">Get Started</a>
|
||||
•
|
||||
<a href="https://wxt.dev/get-started/installation.html" target="_blank">Installation</a>
|
||||
<a href="https://wxt.dev/guide/installation.html" target="_blank">Installation</a>
|
||||
•
|
||||
<a href="https://wxt.dev/guide/background.html" target="_blank">Entrypoints</a>
|
||||
<a href="https://wxt.dev/api/config.html" target="_blank">Configuration</a>
|
||||
•
|
||||
<a href="https://wxt.dev/config.html" target="_blank">Configuration</a>
|
||||
<a href="https://wxt.dev/examples.html" target="_blank">Examples</a>
|
||||
</p>
|
||||
|
||||

|
||||
|
||||
## Demo
|
||||
|
||||
https://github.com/wxt-dev/wxt/assets/10101283/07359e53-f491-43b6-8e8f-fae94aec8063
|
||||
|
||||
## Quick Start
|
||||
|
||||
Bootstrap a new project:
|
||||
@@ -31,7 +45,7 @@ Bootstrap a new project:
|
||||
pnpx wxt@latest init <project-name>
|
||||
```
|
||||
|
||||
Or see the [installation guide](https://wxt.dev/get-started/installation.html) to get started with WXT.
|
||||
Or see the [installation guide](https://wxt.dev/guide/installation.html) to get started with WXT.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -44,10 +58,10 @@ Or see the [installation guide](https://wxt.dev/get-started/installation.html) t
|
||||
- ⬇️ Download and bundle remote URL imports
|
||||
- 🎨 Frontend framework agnostic: works with Vue, React, Svelte, etc
|
||||
- 🖍️ Quickly bootstrap a new project
|
||||
- 📏 Bundle analysis
|
||||
|
||||
### Coming Soon
|
||||
|
||||
- 📏 Bundle analysis
|
||||
- 🤖 Automated publishing
|
||||
|
||||
## Contributors
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
require('../dist/cli.cjs');
|
||||
Executable
+2
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env node
|
||||
import '../dist/cli.js';
|
||||
+9
-5
@@ -1,26 +1,30 @@
|
||||
{
|
||||
"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",
|
||||
"build:all:firefox-mv2": "wxt build -b firefox",
|
||||
"test": "pnpm -w build && vitest",
|
||||
"zip": "pnpm -w build && wxt zip",
|
||||
"compile": "pnpm -w build && tsc --noEmit",
|
||||
"postinstall": "pnpm -w build && wxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"webextension-polyfill": "^0.10.0"
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"vitest": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/webextension-polyfill": "^0.10.0",
|
||||
"sass": "^1.64.0",
|
||||
"@types/react": "^18.2.34",
|
||||
"@types/react-dom": "^18.2.14",
|
||||
"sass": "^1.69.5",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import background from '../background';
|
||||
|
||||
browser.i18n.getMessage = () => 'fake-message';
|
||||
|
||||
const logMock = vi.fn();
|
||||
console.log = logMock;
|
||||
|
||||
describe('Background Entrypoint', () => {
|
||||
beforeEach(() => {
|
||||
fakeBrowser.reset();
|
||||
});
|
||||
|
||||
it("should log the extenion's runtime ID", () => {
|
||||
const id = 'some-id';
|
||||
fakeBrowser.runtime.id = id;
|
||||
|
||||
background.main();
|
||||
|
||||
expect(logMock).toBeCalledWith(id);
|
||||
});
|
||||
|
||||
it('should set the start time in storage', async () => {
|
||||
background.main();
|
||||
await new Promise((res) => setTimeout(res));
|
||||
|
||||
expect(await storage.getItem('session:startTime')).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,5 @@
|
||||
import messages from 'public/_locales/en/messages.json';
|
||||
|
||||
export default defineBackground(() => {
|
||||
console.log(browser.runtime.id);
|
||||
logId();
|
||||
@@ -6,12 +8,17 @@ export default defineBackground(() => {
|
||||
chrome: __IS_CHROME__,
|
||||
firefox: __IS_FIREFOX__,
|
||||
manifestVersion: __MANIFEST_VERSION__,
|
||||
messages,
|
||||
});
|
||||
|
||||
// @ts-expect-error: should only accept entrypoints or public assets
|
||||
browser.runtime.getURL('/');
|
||||
browser.runtime.getURL('/background.js');
|
||||
browser.runtime.getURL('/icon/128.png');
|
||||
browser.runtime.getURL('/example.html#hash');
|
||||
browser.runtime.getURL('/example.html?query=param');
|
||||
// @ts-expect-error: should only allow hashes/query params on HTML files
|
||||
browser.runtime.getURL('/icon-128.png?query=param');
|
||||
|
||||
// @ts-expect-error: should only accept known message names
|
||||
browser.i18n.getMessage('test');
|
||||
@@ -20,5 +27,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());
|
||||
});
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['<all_urls>'],
|
||||
async main(ctx) {
|
||||
console.log(browser.runtime.id);
|
||||
logId();
|
||||
|
||||
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(() => {
|
||||
console.log(n, browser.runtime.id);
|
||||
}, 1e3);
|
||||
|
||||
const container = document.createElement('div');
|
||||
document.body.append(container);
|
||||
|
||||
ReactDOM.createRoot(container).render(<SomeComponent />);
|
||||
},
|
||||
});
|
||||
|
||||
function SomeComponent() {
|
||||
return <div>Some component</div>;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1 @@
|
||||
console.log('iframe 2');
|
||||
@@ -0,0 +1,13 @@
|
||||
export default defineContentScript({
|
||||
matches: ['*://*.google.com/*'],
|
||||
|
||||
main(ctx) {
|
||||
const ui = createIframeUi(ctx, {
|
||||
page: '/iframe-src.html',
|
||||
position: 'overlay',
|
||||
anchor: 'form[action="/search"]',
|
||||
});
|
||||
ui.mount();
|
||||
console.log('Mounted iframe');
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
export default defineContentScript({
|
||||
// Site that uses HTML5 history
|
||||
matches: ['*://*.crunchyroll.com/*'],
|
||||
|
||||
main(ctx) {
|
||||
ctx.addEventListener(window, 'wxt:locationchange', ({ newUrl, oldUrl }) => {
|
||||
console.log('Location changed:', newUrl.href, oldUrl.href);
|
||||
});
|
||||
console.log('Watching for location change...');
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
export default defineContentScript({
|
||||
matches: ['*://*/*'],
|
||||
world: 'MAIN',
|
||||
|
||||
main() {
|
||||
console.log(`Hello from ${location.hostname}!`);
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import '../../common/style.css';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['*://*/*'],
|
||||
async main(ctx) {
|
||||
console.log(browser.runtime.id);
|
||||
logId();
|
||||
|
||||
console.log('WXT MODE:', import.meta.env.MODE);
|
||||
|
||||
const n = (Math.random() * 100).toFixed(1);
|
||||
ctx.setInterval(() => {
|
||||
console.log(n, browser.runtime.id);
|
||||
}, 1e3);
|
||||
|
||||
mountContentScriptUi();
|
||||
},
|
||||
});
|
||||
@@ -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>
|
||||
@@ -0,0 +1,24 @@
|
||||
import '../../common/style.css';
|
||||
import './style.css';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['https://*.duckduckgo.com/*'],
|
||||
cssInjectionMode: 'ui',
|
||||
|
||||
async main(ctx) {
|
||||
const ui = await createShadowRootUi(ctx, {
|
||||
name: 'demo-ui',
|
||||
position: 'inline',
|
||||
append: 'before',
|
||||
anchor: 'form[role=search]',
|
||||
onMount: (container) => {
|
||||
const app = document.createElement('div');
|
||||
app.textContent = 'Custom content script UI';
|
||||
container.append(app);
|
||||
},
|
||||
});
|
||||
ui.mount();
|
||||
|
||||
setTimeout(ui.remove, 5000);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
color: indianred;
|
||||
}
|
||||
html {
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
div {
|
||||
padding: 16px;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default defineUnlistedScript(() => {
|
||||
console.log('injected');
|
||||
});
|
||||
+5
-1
@@ -1,3 +1,7 @@
|
||||
{
|
||||
"extends": ["../tsconfig.base.json", "./.wxt/tsconfig.json"]
|
||||
"extends": ["../tsconfig.base.json", "./.wxt/tsconfig.json"],
|
||||
"compilerOptions": {
|
||||
"allowImportingTsExtensions": true,
|
||||
"jsx": "react-jsx"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { WxtVitest } from 'wxt/testing';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
mockReset: true,
|
||||
restoreMocks: true,
|
||||
},
|
||||
plugins: [WxtVitest()],
|
||||
});
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
FROM lipanski/docker-static-website:latest
|
||||
COPY dist .
|
||||
@@ -0,0 +1,34 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
tag?: string;
|
||||
}>();
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
const filteredExamples = computed(() => {
|
||||
if (props.tag == null) return examples.value;
|
||||
|
||||
return examples.value.filter((example) => {
|
||||
return example.tags?.includes(props.tag);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul>
|
||||
<li v-if="examples == null">Loading...</li>
|
||||
<template v-else>
|
||||
<li v-for="example of filteredExamples">
|
||||
<a :href="example.url" target="_blank">{{ example.name }}</a>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</template>
|
||||
@@ -1,29 +1,93 @@
|
||||
<script lang="ts" setup>
|
||||
import useListExtensionDetails from '../composables/useListExtensionDetails';
|
||||
import { computed } from 'vue';
|
||||
import useListExtensionDetails, {
|
||||
ChromeExtension,
|
||||
} from '../composables/useListExtensionDetails';
|
||||
|
||||
// Add extension IDs here. Order doesn't matter, will be sorted by weekly active users
|
||||
// During the transition from chrome.google.com/webstore to
|
||||
// chromewebstore.google.com, queue.wxt.dev might return null for your
|
||||
// extension. If it does, use "<slug>/<id>" instead of just the ID. The slug
|
||||
// can be retrieved from the URL of the item on chromewebstore.google.com
|
||||
const chromeExtensionIds = [
|
||||
'ocfdgncpifmegplaglcnglhioflaimkd', // GitHub: Better Line Counts
|
||||
'mgmdkjcljneegjfajchedjpdhbadklcf', // Anime Skip Player
|
||||
'bfbnagnphiehemkdgmmficmjfddgfhpl', // UltraWideo
|
||||
'elfaihghhjjoknimpccccmkioofjjfkf', // StayFree - Website Blocker & Web Analytics
|
||||
'okifoaikfmpfcamplcfjkpdnhfodpkil', // Doozy: Ai Made Easy
|
||||
'lknmjhcajhfbbglglccadlfdjbaiifig', // tl;dv - Record, Transcribe & ChatGPT for Google Meet
|
||||
'youtube中文配音/oglffgiaiekgeicdgkdlnlkhliajdlja', // Youtube中文配音
|
||||
];
|
||||
|
||||
const { data } = useListExtensionDetails(chromeExtensionIds);
|
||||
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,
|
||||
}))
|
||||
.filter((item) => !!item)
|
||||
.sort((l, r) => r.sortKey - l.sortKey);
|
||||
});
|
||||
|
||||
function getStoreUrl(extension: ChromeExtension) {
|
||||
const url = new URL(extension.storeUrl);
|
||||
url.searchParams.set('utm_source', 'wxt.dev');
|
||||
return url.href;
|
||||
}
|
||||
</script>
|
||||
|
||||
<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 data">
|
||||
<li
|
||||
v-for="extension of sortedExtensions"
|
||||
:key="extension.id"
|
||||
class="relative"
|
||||
>
|
||||
<img
|
||||
:src="extension.iconUrl"
|
||||
:alt="`${extension.name} icon`"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
<a :href="extension.storeUrl" target="_blank">{{ extension.name }}</a>
|
||||
<small>{{ extension.shortDescription }}</small>
|
||||
<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>
|
||||
</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>Open a PR to add your extension to the list!</p>
|
||||
<p class="centered pr">
|
||||
<a
|
||||
href="https://github.com/wxt-dev/wxt/edit/main/docs/.vitepress/components/UsingWxtSection.vue"
|
||||
target="_blank"
|
||||
>Open a PR</a
|
||||
>
|
||||
to add your extension to the list!
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -33,10 +97,6 @@ const { data } = useListExtensionDetails(chromeExtensionIds);
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.vp-doc {
|
||||
padding: 0 48px;
|
||||
@@ -56,58 +116,94 @@ h2 {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
margin-bottom: 16px;
|
||||
li img {
|
||||
width: 116px;
|
||||
height: 116px;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
background-color: var(--vp-c-default-soft);
|
||||
}
|
||||
|
||||
ul {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
align-items: stretch;
|
||||
gap: 16px;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
margin: 16px 0;
|
||||
padding: 0;
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
ul {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
ul {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 0 !important;
|
||||
padding: 12px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
flex: 1;
|
||||
}
|
||||
a,
|
||||
small {
|
||||
text-align: center;
|
||||
}
|
||||
small {
|
||||
opacity: 50%;
|
||||
gap: 24px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
p {
|
||||
.centered {
|
||||
text-align: center;
|
||||
opacity: 50%;
|
||||
}
|
||||
a {
|
||||
color: var(--vp-c-text-1);
|
||||
|
||||
li a,
|
||||
li .user-count,
|
||||
li .description {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
li .user-count {
|
||||
opacity: 70%;
|
||||
font-size: small;
|
||||
position: absolute;
|
||||
bottom: 12px;
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
li a {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
li a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
li div {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
li .description {
|
||||
opacity: 90%;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
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
|
||||
}
|
||||
}`;
|
||||
|
||||
|
||||
+101
-44
@@ -1,30 +1,66 @@
|
||||
import { defineConfig } from 'vitepress';
|
||||
import { generateConfigDocs } from './plugins/generate-config-docs';
|
||||
import { DefaultTheme, defineConfig } from 'vitepress';
|
||||
import { generateCliDocs } from './plugins/generate-cli-docs';
|
||||
import typedocSidebar from '../api/typedoc-sidebar.json';
|
||||
|
||||
const filteredTypedocSidebar = typedocSidebar.filter(
|
||||
(item) => item.text !== 'API',
|
||||
);
|
||||
// Typedoc's markdown theme adds collapse: true to all our items, event ones without any children,
|
||||
// so they need to be removed.
|
||||
function removeCollapsedWithNoItems(items: DefaultTheme.SidebarItem[]) {
|
||||
for (const item of items) {
|
||||
if (item.items) removeCollapsedWithNoItems(item.items);
|
||||
else delete item.collapsed;
|
||||
}
|
||||
}
|
||||
removeCollapsedWithNoItems(filteredTypedocSidebar);
|
||||
|
||||
const title = 'Next-gen Web Extension Framework';
|
||||
const titleSuffix = ' – WXT';
|
||||
|
||||
const description =
|
||||
"WXT provides the best developer experience, making it quick, easy, and fun to develop chrome extensions for all browsers. With built-in utilties for building, zipping, and publishing your extension, it's easy to get started.";
|
||||
const ogTitle = `${title}${titleSuffix}`;
|
||||
const ogUrl = 'https://wxt.dev';
|
||||
const ogImage = 'https://wxt.dev/social-preview.png';
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
titleTemplate: `:title${titleSuffix}`,
|
||||
title: 'WXT',
|
||||
description,
|
||||
vite: {
|
||||
clearScreen: false,
|
||||
plugins: [generateConfigDocs()],
|
||||
plugins: [generateCliDocs()],
|
||||
},
|
||||
description: 'Next gen framework for developing web extensions',
|
||||
lastUpdated: true,
|
||||
sitemap: {
|
||||
hostname: 'https://wxt.dev',
|
||||
},
|
||||
|
||||
head: [
|
||||
['meta', { property: 'og:type', content: 'website' }],
|
||||
['meta', { property: 'og:title', content: ogTitle }],
|
||||
['meta', { property: 'og:image', content: ogImage }],
|
||||
['meta', { property: 'og:url', content: ogUrl }],
|
||||
['meta', { property: 'og:description', content: description }],
|
||||
['meta', { name: 'twitter:card', content: 'summary_large_image' }],
|
||||
[
|
||||
"script",
|
||||
'script',
|
||||
{
|
||||
async: "",
|
||||
"data-website-id": "c1840c18-a12c-4a45-a848-55ae85ef7915",
|
||||
src: "https://umami.aklinker1.io/script.js",
|
||||
async: '',
|
||||
'data-website-id': 'c1840c18-a12c-4a45-a848-55ae85ef7915',
|
||||
src: 'https://umami.aklinker1.io/script.js',
|
||||
},
|
||||
],
|
||||
],
|
||||
|
||||
themeConfig: {
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
logo: '/logo.svg',
|
||||
logo: {
|
||||
src: '/logo.svg',
|
||||
alt: 'WXT logo',
|
||||
},
|
||||
editLink: {
|
||||
pattern: 'https://github.com/wxt-dev/wxt/edit/main/docs/:path',
|
||||
},
|
||||
@@ -33,56 +69,77 @@ export default defineConfig({
|
||||
},
|
||||
|
||||
nav: [
|
||||
{ text: 'Get Started', link: '/get-started/installation.md' },
|
||||
{ text: 'Guide', link: '/guide/auto-imports.md' },
|
||||
{ text: 'Config', link: '/config.md' },
|
||||
{ text: 'API', link: '/api.md' },
|
||||
{ text: 'Guide', link: '/guide/installation.md' },
|
||||
{ text: 'Entrypoints', link: '/entrypoints/background.md' },
|
||||
{ text: 'Examples', link: '/examples.md' },
|
||||
{ text: 'API', link: '/api/cli.md' },
|
||||
],
|
||||
|
||||
sidebar: {
|
||||
'/get-started/': [
|
||||
{
|
||||
text: 'Get Started',
|
||||
items: [
|
||||
{ text: 'Introduction', link: '/get-started/introduction.md' },
|
||||
{ text: 'Installation', link: '/get-started/installation.md' },
|
||||
{ text: 'Configuration', link: '/get-started/configuration.md' },
|
||||
{ text: 'Entrypoints', link: '/get-started/entrypoints.md' },
|
||||
{ text: 'Assets', link: '/get-started/assets.md' },
|
||||
{ text: 'Build Targets', link: '/get-started/build-targets.md' },
|
||||
{ text: 'Publishing', link: '/get-started/publishing.md' },
|
||||
{ text: 'Testing', link: '/get-started/testing.md' },
|
||||
{ text: 'Compare', link: '/get-started/compare.md' },
|
||||
],
|
||||
},
|
||||
],
|
||||
'/guide/': [
|
||||
{
|
||||
text: 'Guide',
|
||||
items: [
|
||||
{ text: 'Auto-imports', link: '/guide/auto-imports.md' },
|
||||
{ text: 'Introduction', link: '/guide/introduction.md' },
|
||||
{ text: 'Installation', link: '/guide/installation.md' },
|
||||
{ text: 'Configuration', link: '/guide/configuration.md' },
|
||||
{ text: 'Entrypoints', link: '/guide/entrypoints.md' },
|
||||
{ text: 'Manifest.json', link: '/guide/manifest.md' },
|
||||
{ text: 'Extension APIs', link: '/guide/extension-apis.md' },
|
||||
{ text: 'Remote Code', link: '/guide/remote-code.md' },
|
||||
{ text: 'Storage', link: '/guide/storage.md' },
|
||||
{ text: 'Assets', link: '/guide/assets.md' },
|
||||
{ text: 'Content Script UI', link: '/guide/content-script-ui.md' },
|
||||
{ text: 'Multiple Browsers', link: '/guide/multiple-browsers.md' },
|
||||
{ text: 'Auto-imports', link: '/guide/auto-imports.md' },
|
||||
{ text: 'Vite', link: '/guide/vite.md' },
|
||||
{ text: 'Remote Code', link: '/guide/remote-code.md' },
|
||||
{ text: 'Publishing', link: '/guide/publishing.md' },
|
||||
{ text: 'Development', link: '/guide/development.md' },
|
||||
{ text: 'Testing', link: '/guide/testing.md' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Other',
|
||||
items: [
|
||||
{ text: 'Migrate to WXT', link: '/guide/migrate-to-wxt.md' },
|
||||
{ text: 'Compare', link: '/guide/compare.md' },
|
||||
],
|
||||
},
|
||||
],
|
||||
'/entrypoints/': [
|
||||
{
|
||||
text: 'Entrypoints',
|
||||
items: [
|
||||
{ text: 'Background', link: '/guide/background.md' },
|
||||
{ text: 'Bookmarks', link: '/guide/bookmarks.md' },
|
||||
{ text: 'Content Scripts', link: '/guide/content-scripts.md' },
|
||||
{ text: 'CSS', link: '/guide/css.md' },
|
||||
{ text: 'Devtools', link: '/guide/devtools.md' },
|
||||
{ text: 'History', link: '/guide/history.md' },
|
||||
{ text: 'Newtab', link: '/guide/newtab.md' },
|
||||
{ text: 'Options', link: '/guide/options.md' },
|
||||
{ text: 'Popup', link: '/guide/popup.md' },
|
||||
{ text: 'Sandbox', link: '/guide/sandbox.md' },
|
||||
{ text: 'Sidepanel', link: '/guide/sidepanel.md' },
|
||||
{ text: 'Unlisted Pages', link: '/guide/unlisted-pages.md' },
|
||||
{ text: 'Unlisted Scripts', link: '/guide/unlisted-scripts.md' },
|
||||
{ text: 'Background', link: '/entrypoints/background.md' },
|
||||
{ text: 'Bookmarks', link: '/entrypoints/bookmarks.md' },
|
||||
{
|
||||
text: 'Content Scripts',
|
||||
link: '/entrypoints/content-scripts.md',
|
||||
},
|
||||
{ text: 'CSS', link: '/entrypoints/css.md' },
|
||||
{ text: 'Devtools', link: '/entrypoints/devtools.md' },
|
||||
{ text: 'History', link: '/entrypoints/history.md' },
|
||||
{ text: 'Newtab', link: '/entrypoints/newtab.md' },
|
||||
{ text: 'Options', link: '/entrypoints/options.md' },
|
||||
{ text: 'Popup', link: '/entrypoints/popup.md' },
|
||||
{ text: 'Sandbox', link: '/entrypoints/sandbox.md' },
|
||||
{ text: 'Sidepanel', link: '/entrypoints/sidepanel.md' },
|
||||
{ text: 'Unlisted Pages', link: '/entrypoints/unlisted-pages.md' },
|
||||
{
|
||||
text: 'Unlisted Scripts',
|
||||
link: '/entrypoints/unlisted-scripts.md',
|
||||
},
|
||||
].sort((l, r) => l.text.localeCompare(r.text)),
|
||||
},
|
||||
],
|
||||
'/api/': [
|
||||
{
|
||||
items: [
|
||||
{ text: 'CLI', link: '/api/cli.md' },
|
||||
{
|
||||
text: 'Modules',
|
||||
items: filteredTypedocSidebar,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# CLI Reference
|
||||
|
||||
> Reference generated from `wxt <command> --help`
|
||||
|
||||
{{ DOCS }}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { relative, resolve } from 'node:path';
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { format } from 'prettier';
|
||||
import consola from 'consola';
|
||||
import { execaCommand } from 'execa';
|
||||
|
||||
let hasGenerated = false;
|
||||
|
||||
const cliDir = resolve('src/cli/commands');
|
||||
const cliDirGlob = resolve(cliDir, '**');
|
||||
const cliTemplatePath = resolve('docs/.vitepress/plugins/cli.tpl.md');
|
||||
const cliPath = resolve('docs/api/cli.md');
|
||||
|
||||
const PREFACE = `<!--
|
||||
DO NOT EDIT
|
||||
Generated by \`wxt/docs/.vitepress/plugins/generate-config-docs.ts\`
|
||||
To make changes to the config reference, update the JSDoc in \`src/core/types/external.ts\`.
|
||||
-->`;
|
||||
|
||||
export function generateCliDocs() {
|
||||
writeFileSync(cliPath, '');
|
||||
|
||||
const generateDocs = async () => {
|
||||
consola.info(`Generating ${relative(process.cwd(), cliPath)}`);
|
||||
try {
|
||||
const res = await execaCommand('pnpm -s wxt --help');
|
||||
const dev = splitInfo(res.stdout);
|
||||
const lines: Array<string | string[]> = [
|
||||
`## \`wxt\``,
|
||||
'```sh',
|
||||
dev.rest,
|
||||
'```',
|
||||
];
|
||||
|
||||
const commands = await Promise.all(
|
||||
extractCommands(dev.info).map(async (command) => {
|
||||
const res = await execaCommand(`pnpm -s wxt ${command} --help`);
|
||||
const { rest: docs } = splitInfo(res.stdout);
|
||||
return [`## \`wxt ${command}\``, '```sh', docs, '```'];
|
||||
}),
|
||||
);
|
||||
lines.push(...commands);
|
||||
|
||||
const text = await format(
|
||||
PREFACE +
|
||||
'\n\n' +
|
||||
readFileSync(cliTemplatePath, 'utf-8').replace(
|
||||
'{{ DOCS }}',
|
||||
lines.flat().join('\n'),
|
||||
),
|
||||
{ parser: 'markdown' },
|
||||
);
|
||||
|
||||
writeFileSync(cliPath, text);
|
||||
consola.success(`Generated ${relative(process.cwd(), cliPath)}`);
|
||||
} catch (err) {
|
||||
consola.fail(`Failed to generate ${relative(process.cwd(), cliPath)}`);
|
||||
consola.error(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
name: 'docs:generate-cli-docs',
|
||||
async config() {
|
||||
if (!hasGenerated) {
|
||||
hasGenerated = true;
|
||||
await generateDocs();
|
||||
}
|
||||
},
|
||||
configureServer(server: any) {
|
||||
server.watcher.add(cliDirGlob);
|
||||
},
|
||||
async handleHotUpdate(ctx: { file: string }) {
|
||||
if (ctx.file === cliTemplatePath || ctx.file.includes(cliDir)) {
|
||||
await generateDocs();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function splitInfo(text: string): { info: string; rest: string } {
|
||||
const infoStart = text.indexOf('For more info,');
|
||||
const infoEnd = text.indexOf('\n\nOptions:');
|
||||
|
||||
if (infoStart === -1 || infoEnd === -1) {
|
||||
return { info: '', rest: text };
|
||||
}
|
||||
|
||||
const info = text.substring(infoStart, infoEnd).trim();
|
||||
const rest = text.replace(info, '').trim().replace('\n\n\n\n', '\n\n');
|
||||
|
||||
return { info, rest };
|
||||
}
|
||||
|
||||
function extractCommands(info: string): string[] {
|
||||
const commands: string[] = [];
|
||||
|
||||
// Split the info by line and iterate through each line
|
||||
info.split('\n').forEach((line) => {
|
||||
// Use regex to capture the command after "$ wxt " and before "--help"
|
||||
const match = line.match(/\$ wxt (\w+) --help/);
|
||||
if (match && match[1]) {
|
||||
commands.push(match[1]);
|
||||
}
|
||||
});
|
||||
|
||||
return commands;
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
import { resolve } from 'node:path';
|
||||
import { Project, ts, Type, Node, JSDocableNode } from 'ts-morph';
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { format } from 'prettier';
|
||||
import consola from 'consola';
|
||||
|
||||
let hasGenerated = false;
|
||||
|
||||
const externalTypesPath = resolve('src/core/types/external.ts');
|
||||
const configTemplatePath = resolve('docs/config.tpl.md');
|
||||
const configPath = resolve('docs/config.md');
|
||||
|
||||
const PREFACE = `<!--
|
||||
DO NOT EDIT
|
||||
Generated by \`wxt/docs/.vitepress/plugins/generate-config-docs.ts\`
|
||||
To make changes to the config reference, update the JSDoc in \`src/core/types/external.ts\`.
|
||||
-->`;
|
||||
|
||||
/**
|
||||
* Custom property paths that should not be recursively inspected. Usually 3rd party types.
|
||||
*/
|
||||
const LEAF_PATHS = ['imports', 'vite', 'server'];
|
||||
|
||||
/**
|
||||
* Override any types that resolve to `import(...)` instead of their type names when calling
|
||||
* `type.getText()`.
|
||||
*
|
||||
* This also stops any further type inspection for objects, meaning the object will be documented,
|
||||
* not it's properties.
|
||||
*/
|
||||
const CUSTOM_TYPES = {
|
||||
manifest:
|
||||
'Manifest | Promise<Manifest> | () => Manifest | () => Promise<Manifest>',
|
||||
imports: "false | Partial<import('unimport').UnimportOptions>",
|
||||
vite: "Omit<import('vite').UserConfig, 'root' | 'configFile' | 'mode'>",
|
||||
};
|
||||
|
||||
export function generateConfigDocs() {
|
||||
writeFileSync(configPath, '');
|
||||
|
||||
const generateDocs = async () => {
|
||||
consola.info('Generating /config.md');
|
||||
try {
|
||||
const project = new Project({
|
||||
tsConfigFilePath: resolve('tsconfig.json'),
|
||||
});
|
||||
|
||||
// Load file containing "UserConfig"
|
||||
const externalTypesFile = project.addSourceFileAtPath(externalTypesPath);
|
||||
project.resolveSourceFileDependencies();
|
||||
|
||||
const inlineConfigInterface =
|
||||
externalTypesFile.getInterfaceOrThrow('InlineConfig');
|
||||
|
||||
const getDocsFor = (path: string[], node: Node<ts.Node>): string[] => {
|
||||
const pathStr = path.join('.');
|
||||
|
||||
let type: Type<ts.Type>;
|
||||
if (node.isKind(ts.SyntaxKind.InterfaceDeclaration)) {
|
||||
type = node.getType();
|
||||
} else if (node.isKind(ts.SyntaxKind.PropertySignature)) {
|
||||
type = node.getTypeNodeOrThrow()?.getType();
|
||||
} else if (node.isKind(ts.SyntaxKind.MethodSignature)) {
|
||||
type = node.getType();
|
||||
} else {
|
||||
throw Error('Unsupported type node: ' + node.getKindName());
|
||||
}
|
||||
|
||||
if (type.isObject() && !type.isArray() && !CUSTOM_TYPES[pathStr]) {
|
||||
return type.getProperties().flatMap((property) => {
|
||||
const childPath = [...path, property.getName()];
|
||||
|
||||
return getDocsFor(childPath, property.getDeclarations()[0]);
|
||||
});
|
||||
}
|
||||
|
||||
if ('getJsDocs' in node) {
|
||||
const lines: string[] = [];
|
||||
const docs = (node as unknown as JSDocableNode).getJsDocs();
|
||||
let typeText: string;
|
||||
if (CUSTOM_TYPES[pathStr]) {
|
||||
typeText = CUSTOM_TYPES[pathStr];
|
||||
} else if (type.isUnion() && !type.isBoolean()) {
|
||||
typeText = type
|
||||
.getUnionTypes()
|
||||
.map((type) => type.getText())
|
||||
.join(' | ');
|
||||
} else {
|
||||
typeText = type.getText();
|
||||
}
|
||||
const defaultValue = docs
|
||||
.flatMap((doc) => doc.getTags())
|
||||
.find((tag) => tag.getTagName() === 'default')
|
||||
?.getCommentText();
|
||||
lines.push(
|
||||
'',
|
||||
`## ${pathStr}`,
|
||||
'',
|
||||
`- **Type**: \`${typeText}\``,
|
||||
`- **Default**: \`${defaultValue}\``,
|
||||
...docs.flatMap((doc) => doc.getDescription()),
|
||||
);
|
||||
return lines;
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
const lines = getDocsFor([], inlineConfigInterface);
|
||||
const text = await format(
|
||||
PREFACE +
|
||||
'\n\n' +
|
||||
readFileSync(configTemplatePath, 'utf-8').replace(
|
||||
'{{ DOCS }}',
|
||||
lines.join('\n'),
|
||||
),
|
||||
{ parser: 'markdown' },
|
||||
);
|
||||
|
||||
writeFileSync(configPath, text);
|
||||
consola.success('Generated /config.md');
|
||||
} catch (err) {
|
||||
consola.fail('Failed to generate /config.md');
|
||||
consola.error(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
name: 'docs:generate-config-docs',
|
||||
async config() {
|
||||
if (!hasGenerated) {
|
||||
hasGenerated = true;
|
||||
await generateDocs();
|
||||
}
|
||||
},
|
||||
configureServer(server: any) {
|
||||
server.watcher.add(externalTypesPath);
|
||||
},
|
||||
async handleHotUpdate(ctx: { file: string }) {
|
||||
if ([externalTypesPath, configTemplatePath].includes(ctx.file)) {
|
||||
await generateDocs();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import DefaultTheme from 'vitepress/theme';
|
||||
import Icon from '../components/Icon.vue';
|
||||
import EntrypointPatterns from '../components/EntrypointPatterns.vue';
|
||||
import UsingWxtSection from '../components/UsingWxtSection.vue';
|
||||
import ExampleList from '../components/ExampleList.vue';
|
||||
import './custom.css';
|
||||
|
||||
export default {
|
||||
@@ -10,5 +11,6 @@ export default {
|
||||
ctx.app.component('Icon', Icon);
|
||||
ctx.app.component('EntrypointPatterns', EntrypointPatterns);
|
||||
ctx.app.component('UsingWxtSection', UsingWxtSection);
|
||||
ctx.app.component('ExampleList', ExampleList);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# API Reference
|
||||
|
||||
:::warning 🚧 Under construction
|
||||
This documentation does not exist yet.
|
||||
:::
|
||||
@@ -1,5 +0,0 @@
|
||||
# Config Reference
|
||||
|
||||
Discover all the options you can use in your `wxt.config.ts` file.
|
||||
|
||||
{{ DOCS }}
|
||||
@@ -8,8 +8,8 @@ For MV2, the background is added as a script to the background page. For MV3, th
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['background.ts', 'background.js'],
|
||||
['background/index.ts', 'background.js'],
|
||||
['background.[jt]s', 'background.js'],
|
||||
['background/index.[jt]s', 'background.js'],
|
||||
]"
|
||||
/>
|
||||
|
||||
@@ -8,10 +8,10 @@ When creating content script entrypoints, they are automatically included in the
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['content.(ts|tsx)', 'content-scripts/content.js'],
|
||||
['content/index.(ts|tsx)', 'content-scripts/content.js'],
|
||||
['<name>.content.(ts|tsx)', 'content-scripts/<name>.js'],
|
||||
['<name>.content/index.(ts|tsx)', 'content-scripts/<name>.js'],
|
||||
['content.[jt]sx?', 'content-scripts/content.js'],
|
||||
['content/index.[jt]sx?', 'content-scripts/content.js'],
|
||||
['<name>.content.[jt]sx?', 'content-scripts/<name>.js'],
|
||||
['<name>.content/index.[jt]sx?', 'content-scripts/<name>.js'],
|
||||
]"
|
||||
/>
|
||||
|
||||
@@ -20,7 +20,7 @@ When creating content script entrypoints, they are automatically included in the
|
||||
```ts
|
||||
export default defineContentScript({
|
||||
// Set manifest options
|
||||
matches: ['*://google.com/*', '*://duckduckgo.com/*'],
|
||||
matches: string[],
|
||||
excludeMatches: undefined | [],
|
||||
includeGlobs: undefined | [],
|
||||
excludeGlobs: undefined | [],
|
||||
@@ -34,6 +34,9 @@ export default defineContentScript({
|
||||
include: undefined | string[],
|
||||
exclude: undefined | string[],
|
||||
|
||||
// Configure how CSS is injected onto the page
|
||||
cssInjectionMode: undefined | "manifest" | "manual" | "ui",
|
||||
|
||||
main(ctx) {
|
||||
// Executed when content script is loaded
|
||||
},
|
||||
@@ -44,49 +47,9 @@ export default defineContentScript({
|
||||
|
||||
When defining multiple content scripts, content script entrypoints that have the same set of options will be merged into a single `content_script` item in the manifest.
|
||||
|
||||
## CSS
|
||||
|
||||
To include CSS with your content script, import the CSS file at the top of your entrypoint.
|
||||
|
||||
```
|
||||
|
||||
<srcDir>
|
||||
└─ entrypoints/
|
||||
└─ overlay.content/
|
||||
├─ index.ts
|
||||
└─ style.css
|
||||
```
|
||||
|
||||
```ts
|
||||
// entrypoints/overlay.content/index.ts
|
||||
import './style.css';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['*://google.com/*', '*://duckduckgo.com/*'],
|
||||
main(ctx) {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Any styles imported in your content script will be added to that content script's `css` array in your `manifest.json`:
|
||||
|
||||
```json
|
||||
// .output/chrome-mv3/manifest.json
|
||||
{
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["*://google.com/*", "*://duckduckgo.com/*"],
|
||||
"js": ["content-scripts/overlay.js"],
|
||||
"css": ["content-scripts/overlay.css"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Context
|
||||
|
||||
Old content scripts are not automatically stopped when an extension updates and restarts. Often, this leads to "Invalidated context" errors in production when a content script from an old version of your extension tries to use a web extension API. Since it's not connected to the latest version of your extension, the browser decides to throw an error.
|
||||
Old content scripts are not automatically stopped when an extension updates and reloads. Often, this leads to "Invalidated context" errors in production when a content script from an old version of your extension tries to use a extension API.
|
||||
|
||||
WXT provides a utility for managing this process: `ContentScriptContext`. An instance of this class is provided to you automatically inside the `main` function of your content script.
|
||||
|
||||
@@ -120,3 +83,59 @@ When working with content scripts, **you should always use the `ctx` object to s
|
||||
|
||||
This prevents old content scripts from interfering with new content scripts, and prevents error messages from the console in production.
|
||||
:::
|
||||
|
||||
## CSS
|
||||
|
||||
To include CSS with your content script, import the CSS file at the top of your entrypoint.
|
||||
|
||||
```
|
||||
|
||||
<srcDir>/
|
||||
└─ entrypoints/
|
||||
└─ overlay.content/
|
||||
├─ index.ts
|
||||
└─ style.css
|
||||
```
|
||||
|
||||
```ts
|
||||
// entrypoints/overlay.content/index.ts
|
||||
import './style.css';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['*://google.com/*', '*://duckduckgo.com/*'],
|
||||
|
||||
main(ctx) {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Any styles imported in your content script will be added to that content script's `css` array in your `manifest.json`:
|
||||
|
||||
```json
|
||||
// .output/chrome-mv3/manifest.json
|
||||
{
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["*://google.com/*", "*://duckduckgo.com/*"],
|
||||
"js": ["content-scripts/overlay.js"],
|
||||
"css": ["content-scripts/overlay.css"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
To disable this behavior, set `cssInjectionMode` to `"manual"` or `"ui"`.
|
||||
|
||||
```ts
|
||||
export default defineContentScript({
|
||||
matches: ['*://google.com/*', '*://duckduckgo.com/*'],
|
||||
cssInjectionMode: 'manual',
|
||||
|
||||
main(ctx) {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
See [Content Script UI](/guide/content-script-ui) for more info on creating UIs and including CSS in content scripts.
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
WXT can build CSS entrypoints individually. CSS entrypoints are always unlisted.
|
||||
|
||||
See [Content Script CSS](/guide/content-scripts.md#css) documentation for the recomended approach to include CSS with a content script.
|
||||
See [Content Script CSS](/entrypoints/content-scripts#css) documentation for the recomended approach to include CSS with a content script.
|
||||
|
||||
:::info
|
||||
If the recommended approach doesn't work for your use case, you can use any of the filename patterns below to build the styles separate from the JS and use the [`transformManifest` hook](/config.md#transformmanifest) to manually add your CSS file to the manifest.
|
||||
If the recommended approach doesn't work for your use case, you can use any of the filename patterns below to build the styles separate from the JS and use the [`transformManifest` hook](/api/wxt/interfaces/InlineConfig#transformmanifest) to manually add your CSS file to the manifest.
|
||||
:::
|
||||
|
||||
## Filenames
|
||||
@@ -13,15 +13,12 @@
|
||||
|
||||
## Definition
|
||||
|
||||
Plain old HTML file.
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Title</title>
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
@@ -31,3 +28,13 @@ Plain old HTML file.
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
## Adding UI Elements
|
||||
|
||||
Chrome extensions allow you to add panels and side panes to the devtools window.
|
||||
|
||||

|
||||
|
||||
See the WXT's examples for a full walkthrough of extending the devtools window:
|
||||
|
||||
<ExampleList tag="devtools" />
|
||||
@@ -29,6 +29,7 @@
|
||||
}"
|
||||
/>
|
||||
<meta name="manifest.type" content="page_action|browser_action" />
|
||||
<meta name="manifest.browser_style" content="true|false" />
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
@@ -10,10 +10,10 @@ Firefox does not support sandboxed pages.
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['entrypoints/sandbox.html', 'sandbox.html'],
|
||||
['entrypoints/sandbox/index.html', 'sandbox.html'],
|
||||
['entrypoints/<name>.sandbox.html', '<name>.html` '],
|
||||
['entrypoints/<name>.sandbox/index.html', '<name>.html` '],
|
||||
['sandbox.html', 'sandbox.html'],
|
||||
['sandbox/index.html', 'sandbox.html'],
|
||||
['<name>.sandbox.html', '<name>.html` '],
|
||||
['<name>.sandbox/index.html', '<name>.html` '],
|
||||
]"
|
||||
/>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
# Side Panel
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/reference/sidePanel/)
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/reference/sidePanel/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/user_interface/Sidebars)
|
||||
|
||||
:::tip Chromium Only
|
||||
Firefox does not support sidepanel pages.
|
||||
:::warning
|
||||
Chrome added support for sidepanels in Manifest V3, they are not available in Manfiest V2.
|
||||
:::
|
||||
|
||||
## Filenames
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['entrypoints/sidepanel.html', 'sidepanel.html'],
|
||||
['entrypoints/sidepanel/index.html', 'sidepanel.html'],
|
||||
['entrypoints/<name>.sidepanel.html', '<name>.html` '],
|
||||
['entrypoints/<name>.sidepanel/index.html', '<name>.html` '],
|
||||
['sidepanel.html', 'sidepanel.html'],
|
||||
['sidepanel/index.html', 'sidepanel.html'],
|
||||
['<name>.sidepanel.html', '<name>.html` '],
|
||||
['<name>.sidepanel/index.html', '<name>.html` '],
|
||||
]"
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Unlisted Pages
|
||||
|
||||
HTML pages that are built by Vite, but are not included in the manifest.
|
||||
HTML pages that are bundled and shipped with the extension, but are not included in the manifest.
|
||||
|
||||
### Examples
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# Unlisted Scripts
|
||||
|
||||
TypeScript files that are bundled and shipped with the extension, but are not included in the manifest.
|
||||
|
||||
You are responsible for loading/running these scripts where needed.
|
||||
|
||||
## Filenames
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['<name>.[jt]sx?', '<name>.js'],
|
||||
['<name>/index.[jt]sx?', '<name>.js'],
|
||||
]"
|
||||
/>
|
||||
|
||||
## Definition
|
||||
|
||||
```ts
|
||||
export default defineUnlistedScript(() => {
|
||||
// Executed when script is loaded
|
||||
});
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```ts
|
||||
export default defineUnlistedScript({
|
||||
// Set include/exclude if the script should be removed from some builds
|
||||
include: undefined | string[],
|
||||
exclude: undefined | string[],
|
||||
|
||||
// Executed when script is loaded
|
||||
main() {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,7 @@
|
||||
# Examples
|
||||
|
||||
Simple walkthroughs to accomplish common tasks or patterns with WXT.
|
||||
|
||||
<ExampleList />
|
||||
|
||||
> Full code available at [`wxt-dev/wxt-examples`](https://github.com/wxt-dev/wxt-examples)
|
||||
@@ -1,46 +0,0 @@
|
||||
# Build Targets
|
||||
|
||||
You can build an extension for any combination of browser and manifest verison. Different browsers and manifest versions support different APIs and entrypoints, so be sure to check that your extension functions as expected for each target.
|
||||
|
||||
Separate build targets are written to their own output directories:
|
||||
|
||||
```
|
||||
<rootDir>
|
||||
└─ .output
|
||||
├─ chrome-mv3
|
||||
├─ firefox-mv2
|
||||
├─ edge-mv3
|
||||
└─ ...
|
||||
```
|
||||
|
||||
## Target Browser
|
||||
|
||||
To build for a specific browser, pass the `-b --browser` flag from the CLI:
|
||||
|
||||
```
|
||||
|
||||
wxt --browser firefox
|
||||
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.
|
||||
|
||||
| Browser | Default Manifest Version |
|
||||
| ---------------- | :----------------------: |
|
||||
| `chrome` | 3 |
|
||||
| `firefox` | 2 |
|
||||
| `safari` | 2 |
|
||||
| `edge` | 3 |
|
||||
| Any other string | 3 |
|
||||
|
||||
## Target Manifest Version
|
||||
|
||||
To build for a specific manifest version, pass either the `--mv2` flag or `--mv3` flag from the CLI.
|
||||
|
||||
```sh
|
||||
wxt --mv2
|
||||
wxt build --mv2
|
||||
```
|
||||
|
||||
When the `-b --browser` flag is not passed, it defaults to `chrome`. So here, we're targetting MV2 for Chrome.
|
||||
@@ -1,88 +0,0 @@
|
||||
# Defining Entrypoints
|
||||
|
||||
An "entrypoint" is any HTML, JS, or CSS file that needs to be bundled and included with the extension.
|
||||
|
||||
Entrypoints may or may not be listed in the extension's `manifest.json`.
|
||||
|
||||
## `/entrypoints` Directory
|
||||
|
||||
In WXT, entrypoints are defined by adding a file to the `entrypoints/` directory.
|
||||
|
||||
For example, a project that looks like this:
|
||||
|
||||
```
|
||||
<rootDir>
|
||||
└─ entrypoints/
|
||||
├─ background.ts
|
||||
├─ content.ts
|
||||
├─ injected.ts
|
||||
└─ popup.html
|
||||
```
|
||||
|
||||
would result in the following `manifest.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
// ...
|
||||
"manifest_version": 3,
|
||||
"action": {
|
||||
// ...
|
||||
"default_popup": "popup.html"
|
||||
},
|
||||
"background": {
|
||||
// ...
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
// ...
|
||||
"js": ["content-scripts/content.js"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
If a file uses a [special name recognized by WXT](/get-started/entrypoints.md), it will be added to the manifest. In this case:
|
||||
|
||||
- `popup.html` → `action.default_popup`
|
||||
- `content.ts` → `content_scripts.0.js.0`
|
||||
- `background.ts` → `background.service_worker`
|
||||
|
||||
But not all entrypoints are added to the `manifest.json`. If the filename is not recognized by WXT, they are still built and included in the extension, but they are considered "unlisted" and are not apart of the manifest.
|
||||
|
||||
In this case, `injected.ts` gets output to `<outdir>/injected.js` and is accessible via `browser.runtime.getURL("/injected.js")`.
|
||||
|
||||
:::info
|
||||
See [`/entrypoints` folder](/guide/background.md) documentation for a full list of recognized entrypoint filenames.
|
||||
:::
|
||||
|
||||
## Entrypoint Options
|
||||
|
||||
Some entrypoints, like content scripts, actions, or the background, can recieve additional options.
|
||||
|
||||
In HTML files, options are listed as `meta` tags:
|
||||
|
||||
```html
|
||||
<html>
|
||||
<head>
|
||||
<!-- Defining the popup's "default_icon" field -->
|
||||
<meta name="manifest.default_icon" content="{ '16': '/icon/16.png' }" />
|
||||
</head>
|
||||
</html>
|
||||
```
|
||||
|
||||
In TS files, options are apart of the file's default export:
|
||||
|
||||
```ts
|
||||
export default defineContentScript({
|
||||
matches: ['*://*.google.com/*'],
|
||||
runAt: 'document_start',
|
||||
main() {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
:::info
|
||||
For a full list of entrypoints and each of their options, see the [`/entrypoints` folder](/guide/background.md) documentation.
|
||||
:::
|
||||
@@ -1,5 +0,0 @@
|
||||
# Testing
|
||||
|
||||
:::warning 🚧 Testing utils are not implemented yet!
|
||||
Eventually, the plan is to have an integration with Vitest.
|
||||
:::
|
||||
@@ -3,18 +3,23 @@
|
||||
WXT uses the same tool as Nuxt for auto-imports, [`unimport`](https://github.com/unjs/unimport).
|
||||
|
||||
:::info Testing
|
||||
To setup your test environment for auto-imports, see [Testing](/get-started/testing.md).
|
||||
To setup your test environment for auto-imports, see [Testing](/guide/testing).
|
||||
:::
|
||||
|
||||
## WXT Auto-imports
|
||||
|
||||
Some WXT APIs can be used without importing them:
|
||||
|
||||
- [`browser`](/config.md#browser) from `wxt/browser`, a small wrapper around `webextension-polyfill`
|
||||
- [`defineContentScript`](/config.md#defiencontentscript) from `wxt/client`
|
||||
- [`defineBackground`](/config.md#definebackgroundscript) from `wxt/client`
|
||||
- [`browser`](/api/wxt/browser/variables/browser) from `wxt/browser`, a small wrapper around `webextension-polyfill`
|
||||
- [`defineContentScript`](/api/wxt/sandbox/functions/defineContentScript) from `wxt/sandbox`
|
||||
- [`defineBackground`](/api/wxt/sandbox/functions/defineBackground) from `wxt/sandbox`
|
||||
- [`defineUnlistedScript`](/api/wxt/sandbox/functions/defineUnlistedScript) from `wxt/sandbox`
|
||||
- [`createIntegratedUi`](/api/wxt/client/functions/createIntegratedUi) from `wxt/client`
|
||||
- [`createShadowRootUi`](/api/wxt/client/functions/createShadowRootUi) from `wxt/client`
|
||||
- [`createIframeUi`](/api/wxt/client/functions/createIframeUi) from `wxt/client`
|
||||
- [`fakeBrowser`](/api/wxt/testing/variables/fakeBrowser) from `wxt/testing`
|
||||
|
||||
And more. All [`wxt/client`](/config.md#wxtclient) APIs can be used without imports.
|
||||
And more!
|
||||
|
||||
## Project Auto-imports
|
||||
|
||||
@@ -61,7 +66,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'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -15,7 +15,7 @@ export default defineConfig({
|
||||
```
|
||||
|
||||
:::info
|
||||
See the [Config reference](/config.md) for a full list of options.
|
||||
See the [Config reference](/api/wxt/interfaces/InlineConfig) for a full list of options.
|
||||
:::
|
||||
|
||||
## Directory Config
|
||||
@@ -25,7 +25,7 @@ WXT allows you to edit several directories to your liking:
|
||||
- `root` (default: `process.cwd()`) - Root of the WXT project
|
||||
- `srcDir` (default: `<rootDir>`) - Location of all your source code
|
||||
- `entrypointsDir` (default: `<srcDir>/entrypoints`) - Folder containing all the entrypoints.
|
||||
- `publicDir` (default: `<srcDir>/public`) - Folder containing [public assets](/get-started/assets.md)
|
||||
- `publicDir` (default: `<srcDir>/public`) - Folder containing [public assets](/guide/assets)
|
||||
|
||||
### Example
|
||||
|
||||
@@ -51,12 +51,6 @@ export default defineConfig({
|
||||
});
|
||||
```
|
||||
|
||||
## Vite Config
|
||||
|
||||
[Vite](https://vitejs.dev/) is the bundler used to build each entrypoint of your extension. Vite can be configured via the `vite` option.
|
||||
|
||||
See [Vite's documentation](https://vitejs.dev/config/) for configuring the bundler.
|
||||
|
||||
## Frontend Frameworks
|
||||
|
||||
Adding a framework like Vue, React, or Svelte is easy!
|
||||
@@ -70,9 +64,9 @@ import { defineConfig } from 'wxt';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
|
||||
export default defineConfig({
|
||||
vite: {
|
||||
vite: () => ({
|
||||
plugins: [vue()],
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
@@ -81,9 +75,9 @@ import { defineConfig } from 'wxt';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
vite: {
|
||||
vite: () => ({
|
||||
plugins: [react()],
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
@@ -92,10 +86,14 @@ import { defineConfig } from 'wxt';
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
export default defineConfig({
|
||||
vite: {
|
||||
vite: () => ({
|
||||
plugins: [svelte()],
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::info
|
||||
Internally, WXT runs `vite build` multiple times during a single build. It can't share the same instance of some plugins between each build, so a function is used to return completely new instances of each plugin for each build.
|
||||
:::
|
||||
@@ -0,0 +1,399 @@
|
||||
# Content Script UI
|
||||
|
||||
There are three ways to mount a UI inside a content script:
|
||||
|
||||
[[toc]]
|
||||
|
||||
Each has their own set of advantages and disadvantages.
|
||||
|
||||
| Method | Isolated Styles | Isolated Events | HMR | Use page's context |
|
||||
| ----------- | :-------------: | :-----------------: | :-: | :----------------: |
|
||||
| Integrated | ❌ | ❌ | ❌ | ✅ |
|
||||
| Shadow Root | ✅ | ✅ (off by default) | ❌ | ✅ |
|
||||
| IFrame | ✅ | ✅ | ✅ | ❌ |
|
||||
|
||||
## Integrated
|
||||
|
||||
Integrated content script UIs are injected alongside the content of a page. This means that they are affected by CSS on that page.
|
||||
|
||||
:::code-group
|
||||
|
||||
```ts [Vanilla]
|
||||
// entrypoints/example-ui.content.ts
|
||||
export default defineContentScript({
|
||||
main(ctx) {
|
||||
const ui = createIntegratedUi(ctx, {
|
||||
position: 'inline',
|
||||
anchor: '#anchor',
|
||||
onMount: (container) => {
|
||||
// Append children to the container
|
||||
const app = document.createElement('p');
|
||||
app.textContent = '...';
|
||||
container.append(app);
|
||||
},
|
||||
});
|
||||
|
||||
// Call mount to add the UI to the DOM
|
||||
ui.mount();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts [Vue]
|
||||
// entrypoints/example-ui.content/index.ts
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
|
||||
export default defineContentScript({
|
||||
main(ctx) {
|
||||
const ui = createIntegratedUi(ctx, {
|
||||
position: 'inline',
|
||||
anchor: '#anchor',
|
||||
onMount: (container) => {
|
||||
// Create the app and mount it to the UI container
|
||||
const app = createApp(App);
|
||||
app.mount(container);
|
||||
return app;
|
||||
},
|
||||
onRemove: (app) => {
|
||||
// Unmount the app when the UI is removed
|
||||
app.unmount();
|
||||
},
|
||||
});
|
||||
|
||||
// Call mount to add the UI to the DOM
|
||||
ui.mount();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```tsx [React]
|
||||
// entrypoints/example-ui.content/index.tsx
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
|
||||
export default defineContentScript({
|
||||
main(ctx) {
|
||||
const ui = createIntegratedUi(ctx, {
|
||||
position: 'inline',
|
||||
anchor: '#anchor',
|
||||
onMount: (container) => {
|
||||
// Create a root on the UI container and render a component
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(<App />);
|
||||
return root;
|
||||
},
|
||||
onRemove: (root) => {
|
||||
// Unmount the root when the UI is removed
|
||||
root.unmount();
|
||||
},
|
||||
});
|
||||
|
||||
// Call mount to add the UI to the DOM
|
||||
ui.mount();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts [Svelte]
|
||||
// entrypoints/example-ui.content/index.ts
|
||||
import App from './App.svelte';
|
||||
|
||||
export default defineContentScript({
|
||||
main(ctx) {
|
||||
const ui = createIntegratedUi(ctx, {
|
||||
position: 'inline',
|
||||
anchor: '#anchor',
|
||||
onMount: (container) => {
|
||||
// Create the Svelte app inside the UI container
|
||||
const app = new App({
|
||||
target: container,
|
||||
});
|
||||
return app;
|
||||
},
|
||||
onRemove: (app) => {
|
||||
// Destroy the app when the UI is removed
|
||||
app.$destroy();
|
||||
},
|
||||
});
|
||||
|
||||
// Call mount to add the UI to the DOM
|
||||
ui.mount();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```tsx [Solid]
|
||||
// entrypoints/example-ui.content/index.ts
|
||||
import { render } from 'solid-js/web';
|
||||
|
||||
export default defineContentScript({
|
||||
main(ctx) {
|
||||
const ui = createIntegratedUi(ctx, {
|
||||
position: 'inline',
|
||||
anchor: '#anchor',
|
||||
onMount: (container) => {
|
||||
// Render your app to the UI container
|
||||
const unmount = render(() => <div>...</div>, container);
|
||||
},
|
||||
onRemove: (unmount) => {
|
||||
// Unmount the app when the UI is removed
|
||||
unmount();
|
||||
},
|
||||
});
|
||||
|
||||
// Call mount to add the UI to the DOM
|
||||
ui.mount();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
See the [API Reference](/api/wxt/client/functions/createIntegratedUi) for the complete list of options.
|
||||
|
||||
You can control how CSS is injected for an integrated content script UI with the [`cssInjectionMode`](/api/wxt/interfaces/ContentScriptBaseDefinition#cssinjectionmode) property. Usually, you'll want to leave it as `"manifest"`, the default, so the UI inherits its style from the website's CSS.
|
||||
|
||||
## Shadow Root
|
||||
|
||||
Often in web extensions, you don't want your content script's CSS affecting the page, or vise-versa. The [`ShadowRoot`](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot) API is ideal for this.
|
||||
|
||||
WXT's [`createShadowRootUi`](/api/wxt/client/functions/createShadowRootUi) abstracts all the `ShadowRoot` setup away, making it easy to create UIs with isolated CSS. It also supports an optional `isolateEvents` parameter to further isolate user interactions.
|
||||
|
||||
To use `createShadowRootUi`, follow these steps:
|
||||
|
||||
1. Import your CSS file at the top of your content script
|
||||
2. Set [`cssInjectionMode: "ui"`](/api/wxt/interfaces/ContentScriptBaseDefinition#cssinjectionmode) inside `defineContentScript`
|
||||
3. Define your UI with `createShadowRootUi()`
|
||||
4. Mount the UI so it is visible to users
|
||||
|
||||
:::code-group
|
||||
|
||||
```ts [Vanilla]
|
||||
// 1. Import the style
|
||||
import './style.css';
|
||||
|
||||
export default defineContentScript({
|
||||
// 2. Set cssInjectionMode
|
||||
cssInjectionMode: 'ui',
|
||||
|
||||
async main(ctx) {
|
||||
// 3. Define your UI
|
||||
const ui = await createShadowRootUi(ctx, {
|
||||
name: 'example-ui',
|
||||
anchor: '#anchor',
|
||||
type: 'inline',
|
||||
onMount(container) {
|
||||
// Define how your UI will be mounted inside the container
|
||||
const app = document.createElement('p');
|
||||
app.textContent = 'Hello world!';
|
||||
container.append(app);
|
||||
},
|
||||
});
|
||||
|
||||
// 4. Mount the UI
|
||||
ui.mount();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts [Vue]
|
||||
// 1. Import the style
|
||||
import './style.css';
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
|
||||
export default defineContentScript({
|
||||
// 2. Set cssInjectionMode
|
||||
cssInjectionMode: 'ui',
|
||||
|
||||
async main(ctx) {
|
||||
// 3. Define your UI
|
||||
const ui = await createShadowRootUi(ctx, {
|
||||
name: 'example-ui',
|
||||
anchor: '#anchor',
|
||||
type: 'inline',
|
||||
onMount: (container) => {
|
||||
// Define how your UI will be mounted inside the container
|
||||
const app = createApp(App);
|
||||
app.mount(container);
|
||||
return app;
|
||||
},
|
||||
onRemove: (app) => {
|
||||
// Unmount the app when the UI is removed
|
||||
app.unmount();
|
||||
},
|
||||
});
|
||||
|
||||
// 4. Mount the UI
|
||||
ui.mount();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```tsx [React]
|
||||
// 1. Import the style
|
||||
import './style.css';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
|
||||
export default defineContentScript({
|
||||
// 2. Set cssInjectionMode
|
||||
cssInjectionMode: 'ui',
|
||||
|
||||
main(ctx) {
|
||||
// 3. Define your UI
|
||||
const ui = createShadowRootUi(ctx, {
|
||||
name: 'example-ui',
|
||||
position: 'inline',
|
||||
anchor: '#anchor',
|
||||
onMount: (container) => {
|
||||
// Create a root on the UI container and render a component
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(<App />);
|
||||
return root;
|
||||
},
|
||||
onRemove: (root) => {
|
||||
// Unmount the root when the UI is removed
|
||||
root.unmount();
|
||||
},
|
||||
});
|
||||
|
||||
// 4. Mount the UI
|
||||
ui.mount();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts [Svelte]
|
||||
// 1. Import the style
|
||||
import './style.css';
|
||||
import App from './App.svelte';
|
||||
|
||||
export default defineContentScript({
|
||||
// 2. Set cssInjectionMode
|
||||
cssInjectionMode: 'ui',
|
||||
|
||||
main(ctx) {
|
||||
// 3. Define your UI
|
||||
const ui = createShadowRootUi(ctx, {
|
||||
name: 'example-ui',
|
||||
position: 'inline',
|
||||
anchor: '#anchor',
|
||||
onMount: (container) => {
|
||||
// Create the Svelte app inside the UI container
|
||||
const app = new App({
|
||||
target: container,
|
||||
});
|
||||
return app;
|
||||
},
|
||||
onRemove: (app) => {
|
||||
// Destroy the app when the UI is removed
|
||||
app.$destroy();
|
||||
},
|
||||
});
|
||||
|
||||
// 4. Mount the UI
|
||||
ui.mount();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```tsx [Solid]
|
||||
// 1. Import the style
|
||||
import './style.css';
|
||||
import { render } from 'solid-js/web';
|
||||
|
||||
export default defineContentScript({
|
||||
// 2. Set cssInjectionMode
|
||||
cssInjectionMode: 'ui',
|
||||
|
||||
main(ctx) {
|
||||
// 3. Define your UI
|
||||
const ui = createShadowRootUi(ctx, {
|
||||
name: 'example-ui',
|
||||
position: 'inline',
|
||||
anchor: '#anchor',
|
||||
onMount: (container) => {
|
||||
// Render your app to the UI container
|
||||
const unmount = render(() => <div>...</div>, container);
|
||||
},
|
||||
onRemove: (unmount) => {
|
||||
// Unmount the app when the UI is removed
|
||||
unmount();
|
||||
},
|
||||
});
|
||||
|
||||
// 4. Mount the UI
|
||||
ui.mount();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
See the [API Reference](/api/wxt/client/functions/createShadowRootUi) for the complete list of options.
|
||||
|
||||
:::info TailwindCSS
|
||||
`createShadowRootUi` supports TailwindCSS out of the box! When importing the styles, just import the main CSS file containing the `@tailwind` directives, and everything will just work :+1:.
|
||||
:::
|
||||
|
||||
## IFrame
|
||||
|
||||
If you don't need to run your UI in the same frame as the content script, you can use an IFrame to host your UI instead. Since an IFrame just hosts an HTML page, **_HMR is supported_**.
|
||||
|
||||
WXT provides a helper function, [`createIframeUi`](/api/wxt/client/functions/createIframeUi), which simplifies setting up the IFrame.
|
||||
|
||||
1. Create an HTML page that will be loaded into your IFrame
|
||||
```html
|
||||
<!-- entrypoints/example-iframe.html -->
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Your Content Script IFrame</title>
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
1. Add the page to the manifest's `web_accessible_resouces`
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
manifest: {
|
||||
web_accessible_resources: [
|
||||
{
|
||||
resources: ['example-iframe.html'],
|
||||
matches: [...],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
```
|
||||
1. Create and mount the IFrame
|
||||
|
||||
```ts
|
||||
export default defineContentScript({
|
||||
// ...
|
||||
async main(ctx) {
|
||||
// Define the UI
|
||||
const ui = await createIframeUi(ctx, {
|
||||
page: '/example-iframe.html',
|
||||
anchor: '#anchor',
|
||||
type: 'inline',
|
||||
onMount: (wrapper, iframe) => {
|
||||
// Add styles to the iframe like width
|
||||
iframe.width = 123;
|
||||
},
|
||||
});
|
||||
|
||||
// Show UI to user
|
||||
ui.mount();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
See the [API Reference](/api/wxt/client/functions/createIframeUi) for the complete list of options.
|
||||
@@ -0,0 +1,88 @@
|
||||
# Development
|
||||
|
||||
WXT's main goal is providing the best DX it possibly can. When running your extension in dev mode, each part of your extension is reloaded separately when possible.
|
||||
|
||||
| | HMR | Reloaded individually | Reload extension | Restart browser |
|
||||
| ------------------- | :-: | :-------------------: | :--------------: | :----------------------------------------------------: |
|
||||
| HTML File | | ✅ |
|
||||
| HTML Dependency | ✅ |
|
||||
| MV3 Content Script | | ✅ |
|
||||
| MV2 Content Script | | | ✅ |
|
||||
| Background | | | ✅ |
|
||||
| manifest.json | | | | 🟡 See [#16](https://github.com/wxt-dev/wxt/issues/16) |
|
||||
| `wxt.config.ts` | | | | 🟡 See [#10](https://github.com/wxt-dev/wxt/issues/10) |
|
||||
| `web-ext.config.ts` | | | | 🟡 See [#10](https://github.com/wxt-dev/wxt/issues/10) |
|
||||
|
||||
## Configure Browser Startup
|
||||
|
||||
WXT uses [`web-ext` by Mozilla](https://github.com/mozilla/web-ext) to automatically open a browser with the extension installed. You can configure the runner's behavior via the [`runner`](/api/wxt/interfaces/InlineConfig#runner) option, or in a separate gitignored file, `web-ext.config.ts`.
|
||||
|
||||
:::code-group
|
||||
|
||||
```ts [wxt.config.ts]
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
runner: {
|
||||
// Runner config
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts [web-ext.config.ts]
|
||||
import { defineRunnerConfig } from 'wxt';
|
||||
|
||||
export default defineRunnerConfig({
|
||||
// Runner config
|
||||
});
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### Browser Binaries
|
||||
|
||||
`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
|
||||
import { defineRunnerConfig } from 'wxt';
|
||||
|
||||
export default defineRunnerConfig({
|
||||
binaries: {
|
||||
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.
|
||||
:::
|
||||
|
||||
### Other options
|
||||
|
||||
You can customize other options as well, like startup URLs, profiles, or additional command line arguments:
|
||||
|
||||
```ts
|
||||
// web-ext.config.ts
|
||||
import { defineRunnerConfig } from 'wxt';
|
||||
|
||||
export default defineRunnerConfig({
|
||||
startUrls: ['https://google.com', 'https://duckduckgo.com'],
|
||||
chromiumProfile: '/path/to/profile/to/use',
|
||||
chromiumArgs: ['--window-size=400x300'],
|
||||
});
|
||||
```
|
||||
|
||||
For a full list of options, see the [API Reference](/api/wxt/interfaces/ExtensionRunnerConfig).
|
||||
|
||||
## Reload the Extension
|
||||
|
||||
Normally, to manually reload an extension, you have to visit `chrome://extensions` and click the reload button for your extension.
|
||||
|
||||
When running `wxt` command to start the dev server, WXT adds a keyboard shortcut, `ctrl+E` for Windows/Linux and `cmd+E` for Mac, that reloads the extension when pressed, without visiting `chrome://extensions`.
|
||||
|
||||
:::note
|
||||
This shortcut is only available during development, and is not be added to your extension when running `wxt build` or `wxt-zip`.
|
||||
:::
|
||||
@@ -0,0 +1,141 @@
|
||||
# Defining Entrypoints
|
||||
|
||||
An "entrypoint" is any HTML, JS, or CSS file that needs to be bundled and included with the extension.
|
||||
|
||||
Entrypoints may or may not be listed in the extension's `manifest.json`.
|
||||
|
||||
## `/entrypoints` Directory
|
||||
|
||||
In WXT, entrypoints are defined by adding a file to the `entrypoints/` directory.
|
||||
|
||||
For example, a project that looks like this:
|
||||
|
||||
```
|
||||
<rootDir>
|
||||
└─ entrypoints/
|
||||
├─ background.ts
|
||||
├─ content.ts
|
||||
├─ injected.ts
|
||||
└─ popup.html
|
||||
```
|
||||
|
||||
would result in the following `manifest.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
// ...
|
||||
"manifest_version": 3,
|
||||
"action": {
|
||||
// ...
|
||||
"default_popup": "popup.html"
|
||||
},
|
||||
"background": {
|
||||
// ...
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
// ...
|
||||
"js": ["content-scripts/content.js"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
If a file uses a special name recognized by WXT, it will be added to the manifest. In this case:
|
||||
|
||||
- `popup.html` → `action.default_popup`
|
||||
- `content.ts` → `content_scripts.0.js.0`
|
||||
- `background.ts` → `background.service_worker`
|
||||
|
||||
But not all entrypoints are added to the `manifest.json`. If the filename is not recognized by WXT, they are still built and included in the extension, but they are considered "unlisted" and are not apart of the manifest.
|
||||
|
||||
In this case, `injected.ts` gets output to `<outdir>/injected.js` and is accessible via `browser.runtime.getURL("/injected.js")`.
|
||||
|
||||
:::info
|
||||
See [`/entrypoints` folder](/entrypoints/background) documentation for a full list of recognized entrypoint filenames.
|
||||
:::
|
||||
|
||||
## Entrypoint Options
|
||||
|
||||
Some entrypoints, like content scripts, actions, or the background, can recieve additional options.
|
||||
|
||||
In HTML files, options are listed as `meta` tags:
|
||||
|
||||
```html
|
||||
<html>
|
||||
<head>
|
||||
<!-- Defining the popup's "default_icon" field -->
|
||||
<meta name="manifest.default_icon" content="{ '16': '/icon/16.png' }" />
|
||||
</head>
|
||||
</html>
|
||||
```
|
||||
|
||||
In TS files, options are apart of the file's default export:
|
||||
|
||||
```ts
|
||||
export default defineContentScript({
|
||||
matches: ['*://*.google.com/*'],
|
||||
runAt: 'document_start',
|
||||
main() {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
:::info
|
||||
For a full list of entrypoints and each of their options, see the [`/entrypoints` folder](/entrypoints/background) documentation.
|
||||
:::
|
||||
|
||||
### Side Effects
|
||||
|
||||
You cannot use imported variables outside the `main` function is JS entrypoints. This includes options, as shown below:
|
||||
|
||||
```ts
|
||||
// entrypoints/content.ts
|
||||
import { GOOGLE_MATCHES } from '~/utils/match-patterns';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: GOOGLE_MATCHES,
|
||||
main() {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```
|
||||
$ wxt build
|
||||
wxt build
|
||||
|
||||
WXT 0.14.1
|
||||
ℹ Building chrome-mv3 for production with Vite 5.0.5
|
||||
✖ Command failed after 360 ms
|
||||
|
||||
[8:55:54 AM] ERROR entrypoints/content.ts: Cannot use imported variable "GOOGLE_MATCHES" before main function. See https://wxt.dev/guide/entrypoints.html#side-effects
|
||||
```
|
||||
|
||||
This throws an error because WXT needs to import each entrypoint during the build process to extract its definition (containing the `match`, `run_at`, `include`/`exclude`, etc.) to render the `manifest.json` correctly. Before loading an entrypoint, a transformation is applied to remove all imports. This prevents imported modules (local or NPM) with side-effects from running during the build process, potentially throwing an error.
|
||||
|
||||
:::details Why?
|
||||
|
||||
When importing your entrypoint to get its definition, the file is imported in a **_node environement_**, and doesn't have access to the `window`, `chrome`, or `browser` globals a web extension ususally has access to. If WXT doesn't remove all the imports from the file, the imported modules could try and access one of these variables, throwing an error.
|
||||
|
||||
:::
|
||||
|
||||
:::warning
|
||||
See [`wxt-dev/wxt#336`](https://github.com/wxt-dev/wxt/issues/336) to track the status of this bug.
|
||||
:::
|
||||
|
||||
Usually, this error occurs when you try to extract options into a shared file or try to run code outside the `main` function. To fix the example from above, use litteral values when defining an entrypoint instead of importing them:
|
||||
|
||||
```ts
|
||||
import { GOOGLE_MATCHES } from '~/utils/match-patterns'; // [!code --]
|
||||
|
||||
export default defineContentScript({
|
||||
matches: GOOGLE_MATCHES, // [!code --]
|
||||
matches: ['*//*.google.com/*'], // [!code ++]
|
||||
main() {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -11,10 +11,10 @@ And that's it! Your extension now supports Chrome, Firefox, Safari, Edge, and ot
|
||||
|
||||
## Basic Usage
|
||||
|
||||
The `browser` variable is available globally via [auto-imports](/guide/auto-imports.md), or it can be imported manually.
|
||||
The `browser` variable is available globally via [auto-imports](/guide/auto-imports), or it can be imported manually.
|
||||
|
||||
```ts
|
||||
import browser from 'wxt/browser';
|
||||
import { browser } from 'wxt/browser';
|
||||
```
|
||||
|
||||
The `wxt/browser` module exports a customized version of `webextension-polyfill`'s browser with improved typing.
|
||||
@@ -79,8 +79,8 @@ There are a number of message passing libraries you can use to improve the messa
|
||||
|
||||
Here are some that are compatible with WXT (because they are based off `webextension-polyfill` as well):
|
||||
|
||||
- [`@webext-core/messaging`](https://webext-core.aklinker1.io/guide/proxy-service/) - "A light-weight, type-safe wrapper around the `browser.runtime` messaging APIs"
|
||||
- [`@webext-core/proxy-service`](https://webext-core.aklinker1.io/guide/messaging/) - "Create TRPC-like services that can be called from anywhere but run in the background"
|
||||
- [`@webext-core/messaging`](https://webext-core.aklinker1.io/guide/messaging/) - "A light-weight, type-safe wrapper around the `browser.runtime` messaging APIs"
|
||||
- [`@webext-core/proxy-service`](https://webext-core.aklinker1.io/guide/proxy-service/) - "Create TRPC-like services that can be called from anywhere but run in the background"
|
||||
- [`webext-bridge`](https://github.com/zikaari/webext-bridge) - "Messaging in Web Extensions made super easy. Out of the box."
|
||||
|
||||
## Browser Differences
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Installation
|
||||
|
||||
Bootstrap a new project, start from scratch, or migrate an existing project.
|
||||
Bootstrap a new project, start from scratch, or [migrate an existing project](/guide/migrate-to-wxt).
|
||||
|
||||
## Bootstrap Project
|
||||
|
||||
@@ -26,7 +26,9 @@ There are several starting templates available.
|
||||
| <Icon name="Svelte" /> [`svelte`](https://github.com/wxt-dev/wxt/tree/main/templates/svelte) |
|
||||
| <Icon name="Solid" icon="https://www.solidjs.com/img/favicons/favicon-32x32.png" /> [`solid`](https://github.com/wxt-dev/wxt/tree/main/templates/solid) |
|
||||
|
||||
> All templates are in TypeScript. WXT does not support JS at this time.
|
||||
:::info
|
||||
All templates default to TypeScript. Rename the file extensions to `.js` to use JavaScript instead.
|
||||
:::
|
||||
|
||||
## From Scratch
|
||||
|
||||
@@ -142,6 +144,6 @@ If you're an experienced web extension developer and think the dev manifest look
|
||||
|
||||
You're ready to build your web extension!
|
||||
|
||||
- Learn how to [add entrypoints](./entrypoints.md) like the popup, options page, or content scripts
|
||||
- [Configure WXT](./configuration.md) by creating a `wxt.config.ts` file
|
||||
- Learn how to [add entrypoints](./entrypoints) like the popup, options page, or content scripts
|
||||
- [Configure WXT](./configuration) by creating a `wxt.config.ts` file
|
||||
- Checkout [example projects](https://github.com/wxt-dev/wxt-examples) to see how to perfom common tasks with WXT
|
||||
@@ -1,3 +1,10 @@
|
||||
---
|
||||
head:
|
||||
- - link
|
||||
- rel: canonical
|
||||
href: https://wxt.dev
|
||||
---
|
||||
|
||||
# Introduction
|
||||
|
||||
WXT is a free and open source framework for building web extensions in an conventional, intuative, and safe way **_for all browsers_**.
|
||||
@@ -28,5 +35,5 @@ Production builds are optimized for store review, changing as few files as possi
|
||||
In addition, WXT fully supports Firefox's source code requirements when using a bundler. It will automatically create and upload a ZIP file of your source code.
|
||||
|
||||
:::info
|
||||
See [Publishing](./publishing.md) for more info around production builds.
|
||||
See [Publishing](./publishing) for more info around production builds.
|
||||
:::
|
||||
+13
-6
@@ -1,12 +1,13 @@
|
||||
# Manifest.json
|
||||
|
||||
The manifest.json is generated at build-time based on files in your `entrypoints` directory and your `wxt.config.ts`.
|
||||
The manifest.json is generated at build-time based on files in the `entrypoints/` directory and `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.
|
||||
|
||||
```ts
|
||||
// wxt.config.tsentrypoint of your extension
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
@@ -25,11 +26,13 @@ If not provided via the `manifest` config, the [manifest's `name`](https://devel
|
||||
|
||||
## `version` and `version_name`
|
||||
|
||||
The [manifest's `version` and `version_name`](https://developer.chrome.com/docs/extensions/mv3/manifest/version/) fields are based on your package.json's `version` property.
|
||||
The [manifest's `version` and `version_name`](https://developer.chrome.com/docs/extensions/mv3/manifest/version/) properties are based on the `version` field listed in your `package.json` or `wxt.config.ts`.
|
||||
|
||||
- `version_name` is the exact string listed in your package.json
|
||||
- `version_name` is the exact string listed in your `package.json` or `wxt.config.ts` file
|
||||
- `version` is the string cleaned up, with any invalid suffixes removed
|
||||
|
||||
If a version is not found, a warning is logged and the version defaults to `"0.0.0"`.
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
@@ -49,7 +52,7 @@ The [manifest's `version` and `version_name`](https://developer.chrome.com/docs/
|
||||
|
||||
## `icons`
|
||||
|
||||
By default, WXT will discover icons in your [`public` directory](/get-started/assets#public-directory) and use them for the [manifest's `icons`](https://developer.chrome.com/docs/extensions/mv3/manifest/icons/).
|
||||
By default, WXT will discover icons in your [`public` directory](/guide/assets#public-directory) and use them for the [manifest's `icons`](https://developer.chrome.com/docs/extensions/mv3/manifest/icons/).
|
||||
|
||||
```
|
||||
public/
|
||||
@@ -94,7 +97,7 @@ export default defineConfig({
|
||||
|
||||
## Localization
|
||||
|
||||
Similar to the icon, the [`_locales` directory](https://developer.chrome.com/docs/extensions/reference/i18n/) should be placed inside the the WXT's [`public` directory](/get-started/assets#public-directory).
|
||||
Similar to the icon, the [`_locales` directory](https://developer.chrome.com/docs/extensions/reference/i18n/) should be placed inside the the WXT's [`public` directory](/guide/assets#public-directory).
|
||||
|
||||
```
|
||||
public/
|
||||
@@ -118,3 +121,7 @@ export default defineConfig({
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
See the official localization examples for more details:
|
||||
|
||||
<ExampleList tag="i18n" />
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Migrate to WXT
|
||||
|
||||
> If you have problems migrating to WXT, feel free to ask for help in GitHub by [starting a discussion](https://github.com/wxt-dev/wxt/discussions/new?category=q-a)!
|
||||
|
||||
## Overview
|
||||
|
||||
Always start by generating a new vanilla project and merging it into your project one file at a time.
|
||||
|
||||
```sh
|
||||
cd path/to/your/project
|
||||
pnpx wxt@latest init example-wxt --template vanilla
|
||||
```
|
||||
|
||||
In general, you'll need to:
|
||||
|
||||
<input type="checkbox" /> Install `wxt`<br />
|
||||
<input type="checkbox" /> Update/create `package.json` scripts to use `wxt` (don't forget about `postinstall`)<br />
|
||||
<input type="checkbox" /> Move entrypoints into `entrypoints/` directory<br />
|
||||
<input type="checkbox" /> Move assets into either the `assets/` or `public/` directories<br />
|
||||
<input type="checkbox" /> Move manifest.json content into `wxt.config.ts`<br />
|
||||
<input type="checkbox" /> Convert custom import syntax to be compatible with Vite<br />
|
||||
<input type="checkbox" /> Add a default export to JS entrypoints<br />
|
||||
<input type="checkbox" /> Use the `browser` global instead of `chrome`<br />
|
||||
<input type="checkbox" /> Compare final `manifest.json` files, making sure permissions and host permissions are unchanged<br />
|
||||
<input type="checkbox" /> Extension output by `wxt build` works the same way as before the migration<br />
|
||||
|
||||
Every project is different, so there's no one-solution-fits-all to migrating your project. Just make sure `wxt dev` runs, `wxt build` results in a working extension, and the list of permissions in the `manifest.json` hasn't changed. If all that looks good, you've finished migrating your extension!
|
||||
|
||||
## Popular Tools/Frameworks
|
||||
|
||||
Here's specific steps for other popuplar frameworks/build tools.
|
||||
|
||||
### `vite-plugin-web-extension`
|
||||
|
||||
Since you're already using Vite, it's a simple refactor.
|
||||
|
||||
1. Install `wxt`
|
||||
2. Move and refactor your entrypoints to WXT's style (with a default export)
|
||||
3. Update package.json scripts to use `wxt`
|
||||
4. Add `"postinstall": "wxt prepare"` script
|
||||
5. Move the `manifest.json` into `wxt.config.ts`
|
||||
6. Move any custom settings from `vite.config.ts` into `wxt.config.ts`'s
|
||||
7. Compare `dist/manifest.json` to `.output/*/manifest.json`, they should have the same content as before. If not, tweak your entrypoints and config to get as close as possible.
|
||||
|
||||
### `plasmo`
|
||||
|
||||
1. Install `wxt`
|
||||
2. Move entrypoints into `entrypoints/` directory, merging the named exports used to configure your JS entrypoints into WXT's default export
|
||||
3. Move public `assets/*` into the `public/` directory
|
||||
4. If you use CSUI, migrate to WXT's `createContentScriptUi`
|
||||
5. Convert Plasmo's custom import resolutions to Vite's
|
||||
6. If importing remote code via a URL, add a `url:` prefix so it works with WXT
|
||||
7. Compare your output `manifest.json` files from before the migration to after the migration. They should have the same content. If not, tweak your entrypoints and config to get as close as possible.
|
||||
@@ -0,0 +1,114 @@
|
||||
# Multiple Browsers
|
||||
|
||||
You can build an extension for any combination of browser and manifest verison. Different browsers and manifest versions support different APIs and entrypoints, so be sure to check that your extension functions as expected for each target.
|
||||
|
||||
Separate build targets are written to their own output directories:
|
||||
|
||||
```
|
||||
<rootDir>
|
||||
└─ .output
|
||||
├─ chrome-mv3
|
||||
├─ firefox-mv2
|
||||
├─ edge-mv3
|
||||
└─ ...
|
||||
```
|
||||
|
||||
## Target Browser
|
||||
|
||||
To build for a specific browser, pass the `-b --browser` flag from the CLI:
|
||||
|
||||
```
|
||||
|
||||
wxt --browser firefox
|
||||
wxt build --browser firefox
|
||||
|
||||
```
|
||||
|
||||
By default, it will build for `chrome`. When excluding the [manifest version flags](#target-manifest-version), it will default to the commonly accepted manifest version for that browser.
|
||||
|
||||
| Browser | Default Manifest Version |
|
||||
| ---------------- | :----------------------: |
|
||||
| `chrome` | 3 |
|
||||
| `firefox` | 2 |
|
||||
| `safari` | 2 |
|
||||
| `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.
|
||||
|
||||
```sh
|
||||
wxt --mv2
|
||||
wxt build --mv2
|
||||
```
|
||||
|
||||
When the `-b --browser` flag is not passed, it defaults to `chrome`. So here, we're targetting MV2 for Chrome.
|
||||
|
||||
## Customizing Entrypoints
|
||||
|
||||
There are several ways to customize entrypoint definitions per browser.
|
||||
|
||||
First, you can use either the `include` or `exclude` option to include or exclude the entrypoint from specific browsers. Here are some examples
|
||||
|
||||
:::code-group
|
||||
|
||||
```ts [Background]
|
||||
export default defineBackground({
|
||||
// Only include a background script when targeting chrome
|
||||
include: ['chrome'],
|
||||
});
|
||||
```
|
||||
|
||||
```ts [Content Script]
|
||||
export default defineContentScript({
|
||||
// Do not add this content script to the manifest when targeting firefox
|
||||
exclude: ['firefox'],
|
||||
});
|
||||
```
|
||||
|
||||
```html [HTML page]
|
||||
<!-- entrypoints/options.html -->
|
||||
<html>
|
||||
<head>
|
||||
<!-- Don't include the options page for safari -->
|
||||
<meta name="manifest.exclude" content="['safari']" />
|
||||
</head>
|
||||
</html>
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
Second, you can change individual options per-browser:
|
||||
|
||||
:::code-group
|
||||
|
||||
```ts [Background]
|
||||
export default defineBackground({
|
||||
persistent: {
|
||||
// Use a non-persistent background script for just safari
|
||||
safari: false,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts [Content Script]
|
||||
export default defineContentScript({
|
||||
matches: {
|
||||
// Run the content script on different pages for each browser
|
||||
chrome: ['*://*.google.com/*'],
|
||||
firefox: ['*://*.duckduckgo.com/*'],
|
||||
edge: ['*://*.bing.com/*'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::warning
|
||||
Only `defineBackground` and `defineContentScript` support per-browser options right now.
|
||||
:::
|
||||
@@ -4,7 +4,7 @@ WXT offers several utilities that simplify the publishing process.
|
||||
|
||||
## First Time Publishing
|
||||
|
||||
If you're publishing an extension to a store for the first time, it's recommended that you manually navigate the process. Each store has unique steps and requirements that you need to familiarize yourself with.
|
||||
If you're publishing an extension to a store for the first time, you must manually navigate the process. Each store has unique steps and requirements that you need to familiarize yourself with.
|
||||
|
||||
Each store requires that a ZIP file be uploaded. You can generate these using the `wxt zip` command:
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
# Storage API
|
||||
|
||||
WXT provides a simplified API to replace the `browser.storage.*` APIs. Use the `storage` auto-import from `wxt/storage` or import it manually to get started:
|
||||
|
||||
```ts
|
||||
import { storage } from 'wxt/storage';
|
||||
```
|
||||
|
||||
[[toc]]
|
||||
|
||||
## Basic Usage
|
||||
|
||||
All storage keys must be prefixed by their storage area.
|
||||
|
||||
```ts
|
||||
// ❌ This will throw an error
|
||||
await storage.getItem('installDate');
|
||||
|
||||
// ✅ This is good
|
||||
await storage.getItem('local:installDate');
|
||||
```
|
||||
|
||||
You can use `local:`, `session:`, `sync:`, or `managed:`.
|
||||
|
||||
If you use TypeScript, you can add a type parameter to most methods to specify the expected type of the key's value:
|
||||
|
||||
```ts
|
||||
await storage.getItem<number>('local:installDate');
|
||||
await storage.watch<number>(
|
||||
'local:installDate',
|
||||
(newInstallDate, oldInstallDate) => {
|
||||
// ...
|
||||
},
|
||||
);
|
||||
await storage.getMeta<{ v: number }>('local:installDate');
|
||||
```
|
||||
|
||||
## Watchers
|
||||
|
||||
To listen for storage changes, use the `storage.watch` function. It lets you setup a listener for a single key:
|
||||
|
||||
```ts
|
||||
const unwatch = storage.watch<number>('local:counter', (newCount, oldCount) => {
|
||||
console.log('Count changed:', { newCount, oldCount });
|
||||
});
|
||||
```
|
||||
|
||||
To remove the listener, call the returned `unwatch` function:
|
||||
|
||||
```ts
|
||||
const unwatch = storage.watch(...);
|
||||
|
||||
// Some time later...
|
||||
unwatch();
|
||||
```
|
||||
|
||||
## Metadata
|
||||
|
||||
`wxt/storage` also supports setting metadata for keys, stored at `key + "$"`. Metadata is a collection of properties associated with a key. It might be a version number, last modified date, etc.
|
||||
|
||||
[Other than versioning](#versioning-and-migrations), you are responsible for managing a field's metadata:
|
||||
|
||||
```ts
|
||||
await Promise.all([
|
||||
storage.setItem('local:preference', true),
|
||||
storage.setMeta('local:preference', { lastModified: Date.now() }),
|
||||
]);
|
||||
```
|
||||
|
||||
When setting different properties of metadata from multiple calls, the properties are combined instead of overwritten:
|
||||
|
||||
```ts
|
||||
await storage.setMeta('local:preference', { lastModified: Date.now() });
|
||||
await storage.setMeta('local:preference', { v: 2 });
|
||||
|
||||
await storage.getMeta('local:preference'); // { v: 2, lastModified: 1703690746007 }
|
||||
```
|
||||
|
||||
You can remove all metadata associated with a key, or just specific properties:
|
||||
|
||||
```ts
|
||||
// Remove all properties
|
||||
await storage.removeMeta('local:preference');
|
||||
|
||||
// Remove one property
|
||||
await storage.removeMeta('local:preference', 'lastModified');
|
||||
|
||||
// Remove multiple properties
|
||||
await storage.removeMeta('local:preference', ['lastModified', 'v']);
|
||||
```
|
||||
|
||||
## Defining Storage Items
|
||||
|
||||
Writing the key and type parameter for the same key over and over again can be annoying. As an alternative, you can use `storage.defineItem` to create a "storage item".
|
||||
|
||||
Storage items contain the same APIs as the `storage` variable, but you can configure its type, default value, and more in a single place:
|
||||
|
||||
```ts
|
||||
// utils/storage.ts
|
||||
const showChangelogOnUpdate = storage.defineItem<boolean>(
|
||||
'local:showChangelogOnUpdate',
|
||||
{
|
||||
defaultValue: true,
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
Now, instead of using the `storage` variable, you can use the helper functions on the storage item you created:
|
||||
|
||||
```ts
|
||||
await showChangelogOnUpdate.getValue();
|
||||
await showChangelogOnUpdate.setValue(false);
|
||||
await showChangelogOnUpdate.removeValue();
|
||||
const unwatch = showChangelogOnUpdate.watch(() => {
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
### Versioning and Migrations
|
||||
|
||||
You can add versioning to storage items if you expect them to grow or change over time. When defining the first version of an item, start with version 1.
|
||||
|
||||
For example, consider a storage item that stores a list of websites that are ignored by an extension.
|
||||
|
||||
:::code-group
|
||||
|
||||
```ts [v1]
|
||||
type IgnoredWebsiteV1 = string;
|
||||
|
||||
export const ignoredWebsites = storage.defineItem<IgnoredWebsiteV1[]>(
|
||||
'local:ignoredWebsites',
|
||||
{
|
||||
defaultValue: [],
|
||||
version: 1,
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```ts [v2]
|
||||
import { nanoid } from 'nanoid'; // [!code ++]
|
||||
|
||||
type IgnoredWebsiteV1 = string;
|
||||
interface IgnoredWebsiteV2 { // [!code ++]
|
||||
id: string; // [!code ++]
|
||||
website: string; // [!code ++]
|
||||
} // [!code ++]
|
||||
|
||||
export const ignoredWebsites = storage.defineItem<IgnoredWebsiteV1[]>( // [!code --]
|
||||
export const ignoredWebsites = storage.defineItem<IgnoredWebsiteV2[]>( // [!code ++]
|
||||
'local:ignoredWebsites',
|
||||
{
|
||||
defaultValue: [],
|
||||
version: 1, // [!code --]
|
||||
version: 2, // [!code ++]
|
||||
migrations: { // [!code ++]
|
||||
// Ran when migrating from v1 to v2 // [!code ++]
|
||||
2: (websites: IgnoredWebsiteV1[]): IgnoredWebsiteV2[] => { // [!code ++]
|
||||
return websites.map((website) => ({ id: nanoid(), website })); // [!code ++]
|
||||
}, // [!code ++]
|
||||
}, // [!code ++]
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```ts [v3]
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
type IgnoredWebsiteV1 = string;
|
||||
interface IgnoredWebsiteV2 {
|
||||
id: string;
|
||||
website: string;
|
||||
}
|
||||
interface IgnoredWebsiteV3 { // [!code ++]
|
||||
id: string; // [!code ++]
|
||||
website: string; // [!code ++]
|
||||
enabled: boolean; // [!code ++]
|
||||
} // [!code ++]
|
||||
|
||||
export const ignoredWebsites = storage.defineItem<IgnoredWebsiteV2[]>( // [!code --]
|
||||
export const ignoredWebsites = storage.defineItem<IgnoredWebsiteV3[]>( // [!code ++]
|
||||
'local:ignoredWebsites',
|
||||
{
|
||||
defaultValue: [],
|
||||
version: 2, // [!code --]
|
||||
version: 3, // [!code ++]
|
||||
migrations: {
|
||||
// Ran when migrating from v1 to v2
|
||||
2: (websites: IgnoredWebsiteV1[]): IgnoredWebsiteV2[] => {
|
||||
return websites.map((website) => ({ id: nanoid(), website }));
|
||||
},
|
||||
// Ran when migrating from v2 to v3 // [!code ++]
|
||||
3: (websites: IgnoredWebsiteV2[]): IgnoredWebsiteV3[] => { // [!code ++]
|
||||
return websites.map((website) => ({ ...website, enabled: true })); // [!code ++]
|
||||
}, // [!code ++]
|
||||
},
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::info
|
||||
Internally, this uses a metadata property called `v` to track the value's current version.
|
||||
:::
|
||||
|
||||
In this case, we thought that the ignored website list might change in the future, and were able to setup a versioned storage item from the start.
|
||||
|
||||
Realistically, you won't know a item needs versioned until you need to change it's schema. Thankfully, it's simple to add versioning to an unversioned storage item.
|
||||
|
||||
When a previous version isn't found, WXT assumes the version was `1`. That means you just need to set `version: 2` and add a migration for `2`, and it will just work!
|
||||
|
||||
Lets look at the same ignored websites example from before, but start with an unversioned item this time:
|
||||
|
||||
:::code-group
|
||||
|
||||
```ts [Unversioned]
|
||||
export const ignoredWebsites = storage.defineItem<string[]>(
|
||||
'local:ignoredWebsites',
|
||||
{
|
||||
defaultValue: [],
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```ts [v2]
|
||||
import { nanoid } from 'nanoid'; // [!code ++]
|
||||
|
||||
// Retroactively add a type for the first version // [!code ++]
|
||||
type IgnoredWebsiteV1 = string; // [!code ++]
|
||||
interface IgnoredWebsiteV2 { // [!code ++]
|
||||
id: string; // [!code ++]
|
||||
website: string; // [!code ++]
|
||||
} // [!code ++]
|
||||
|
||||
export const ignoredWebsites = storage.defineItem<string[]>( // [!code --]
|
||||
export const ignoredWebsites = storage.defineItem<IgnoredWebsiteV2[]>( // [!code ++]
|
||||
'local:ignoredWebsites',
|
||||
{
|
||||
defaultValue: [],
|
||||
version: 2, // [!code ++]
|
||||
migrations: { // [!code ++]
|
||||
// Ran when migrating from v1 to v2 // [!code ++]
|
||||
2: (websites: IgnoredWebsiteV1[]): IgnoredWebsiteV2[] => { // [!code ++]
|
||||
return websites.map((website) => ({ id: nanoid(), website })); // [!code ++]
|
||||
}, // [!code ++]
|
||||
}, // [!code ++]
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
:::
|
||||
@@ -0,0 +1,25 @@
|
||||
# Testing
|
||||
|
||||
## Official Frameworks
|
||||
|
||||
WXT officially supports [Vitest](https://vitest.dev/) for unit tests and either [Playwright](https://playwright.dev/) or [Puppeteer](https://pptr.dev/) for E2E tests against Chromium browsers.
|
||||
|
||||
For details setting up each testing framework, see the official examples:
|
||||
|
||||
<ExampleList tag="testing" />
|
||||
|
||||
### Unofficial Frameworks
|
||||
|
||||
Puppeteer and Playwright are the only E2E test runners that support Chrome Extensions. There are no other options at the time of writing.
|
||||
|
||||
There are other options for unit tests however, like [Jest](https://jestjs.io/), [Mocha](https://mochajs.org/), or [`node:test`](https://nodejs.org/api/test.html). **_WXT does not claim to support any of them_** because none of them support all of WXT's features, like TypeScript or auto-imports.
|
||||
|
||||
If you want to try to use a different framework for unit tests, you will need to configure the environment manually:
|
||||
|
||||
- **Auto-imports**: Add `unimport` to your test environment or disable them by setting `imports: false` in your `wxt.config.ts` file
|
||||
- **`browser` mock**: Mock the `webextension-polyfill` module globally with `wxt/dist/virtual/mock-browser.js`
|
||||
- **[Remote Code Bundling](/guide/remote-code)**: If you use it, configure your environment to handle the `url:` module prefix
|
||||
- **Global Variables**: If you consume them, manually define globals provided by WXT (like `__BROWSER__`) by adding them to the global scope before accessing them (`globalThis.__BROWSER__ = "chrome"`)
|
||||
- **Import paths**: If you use the `@/` or `~/` path aliases, add them to your test environment
|
||||
|
||||
[Here's how Vitest is configured](https://github.com/wxt-dev/wxt/blob/main/src/testing/wxt-vitest-plugin.ts) for reference.
|
||||
@@ -1,20 +0,0 @@
|
||||
# Unlisted Scripts
|
||||
|
||||
TypeScript files that are built, but are not included in the manifest.
|
||||
|
||||
## Filenames
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['<name>.(ts|tsx)', '<name>.js'],
|
||||
['<name>/index.(ts|tsx)', '<name>.js'],
|
||||
]"
|
||||
/>
|
||||
|
||||
## Definition
|
||||
|
||||
Unlike the background or content scripts, you can define this script's logic in the top level scope.
|
||||
|
||||
```ts
|
||||
// Code goes here
|
||||
```
|
||||
+4
-4
@@ -10,9 +10,9 @@ All of Vite's config can be customized by setting the `vite` configuration in yo
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
vite: {
|
||||
vite: () => ({
|
||||
// Same as `defineConfig({ ... })` inside vite.config.ts
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
@@ -26,10 +26,10 @@ All plugins should work in WXT, but it is worth pointing out that since WXT orch
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
vite: {
|
||||
vite: () => ({
|
||||
plugins: [
|
||||
// ...
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
+53
-14
@@ -1,59 +1,98 @@
|
||||
---
|
||||
# https://vitepress.dev/reference/default-theme-home-page
|
||||
layout: home
|
||||
titleTemplate: 'Next Generation Web Extension Framework'
|
||||
title: Next-gen Web Extension Framework
|
||||
|
||||
hero:
|
||||
name: WXT
|
||||
text: Next-gen Web Extension Framework
|
||||
tagline: Powered by Vite, inspired by Nuxt.
|
||||
tagline: An open source tool that makes Chrome Extension devlopment faster than ever before.
|
||||
image:
|
||||
src: /hero-logo.svg
|
||||
alt: WXT
|
||||
actions:
|
||||
- theme: brand
|
||||
text: Get Started
|
||||
link: /get-started/installation
|
||||
link: /guide/installation
|
||||
- theme: alt
|
||||
text: Learn More
|
||||
link: /get-started/introduction
|
||||
link: /guide/introduction
|
||||
|
||||
features:
|
||||
- icon: 🌐
|
||||
title: Supported Browsers
|
||||
details: Chrome, Firefox, Edge, Safari, and any Chromium based browser.
|
||||
details: WXT will build extensions for Chrome, Firefox, Edge, Safari, and any Chromium based browser.
|
||||
link: /guide/multiple-browsers
|
||||
linkText: Read docs
|
||||
- icon: ✅
|
||||
title: MV2 and MV3
|
||||
details: Supports both manifest versions for each browser.
|
||||
details: Build Manifest V2 or V3 extensions for any browser using the same codebase.
|
||||
link: /guide/multiple-browsers#target-manifest-version
|
||||
linkText: Read docs
|
||||
- icon: ⚡
|
||||
title: Fast Dev Mode
|
||||
details: HMR for UIs and fast reload for background and content scripts.
|
||||
details: Lighting fast HMR for UI development and fast reloads for content/background scripts enables faster iterations.
|
||||
link: /guide/development.html
|
||||
linkText: Learn more
|
||||
- icon: 📂
|
||||
title: File Based Entrypoints
|
||||
details: Manifest is generated based on files inside the project.
|
||||
details: Manifest is generated based on files in the project with inline configuration.
|
||||
link: /guide/entrypoints
|
||||
linkText: See project structure
|
||||
- icon: 🚔
|
||||
title: TypeScript
|
||||
details: Scale projects with full TS support.
|
||||
details: Create large projects with confidence using TS by default.
|
||||
- icon: 🦾
|
||||
title: Auto-imports
|
||||
details: Nuxt-like auto-imports to speed up development.
|
||||
link: /guide/auto-imports
|
||||
linkText: Read docs
|
||||
- icon: ⬇️
|
||||
title: Bundle Remote Code
|
||||
details: Downloads and bundles remote code imported from URLs.
|
||||
link: /guide/remote-code
|
||||
linkText: Read docs
|
||||
- icon: 🎨
|
||||
title: Frontend Framework Agnostic
|
||||
details: Works with any front-end framework with a Vite plugin.
|
||||
link: /guide/configuration#frontend-frameworks
|
||||
linkText: Add a framework
|
||||
- icon: 🖍️
|
||||
title: Bootstrap a New Project
|
||||
details: Comes with starter templates for all major frontend frameworks.
|
||||
- icon: 🤖
|
||||
title: Automated Publishing
|
||||
details: 'TODO: Automatically zip, upload, and release extensions.'
|
||||
details: Get started quickly with several awesome project templates.
|
||||
link: /guide/installation#bootstrap-project
|
||||
linkText: See templates
|
||||
- icon: 📏
|
||||
title: Bundle Analysis
|
||||
details: 'TODO: Tools for analyizing the final extension bundle.'
|
||||
details: Tools for analyizing the final extension bundle and minimizing your extension's size.
|
||||
- icon: 🤖
|
||||
title: Automated Publishing
|
||||
details: 'Coming soon. Automatically zip, upload, and release extensions.'
|
||||
---
|
||||
|
||||
<section class="vp-doc">
|
||||
<div class="container">
|
||||
<h2>Put <span style="color: var(--vp-c-brand-1)">Developer Experience</span> First</h2>
|
||||
<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: 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>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<ClientOnly>
|
||||
<UsingWxtSection />
|
||||
</ClientOnly>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
max-width: 1152px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Netlify Redirects File
|
||||
# https://docs.netlify.com/routing/redirects/
|
||||
|
||||
# Old URLs -> New URLs
|
||||
/config.html /api/wxt/interfaces/InlineConfig.html
|
||||
/api/config.html /api/wxt/interfaces/InlineConfig.html
|
||||
/entrypoints /entrypoints/background.html
|
||||
/get-started/assets.html /guide/assets.html
|
||||
/get-started/build-targets.html /guide/multiple-browsers.html
|
||||
/get-started/compare.html /guide/compare.html
|
||||
/get-started/configuration.html /guide/configuration.html
|
||||
/get-started/entrypoints.html /guide/entrypoints.html
|
||||
/get-started/installation.html /guide/installation.html
|
||||
/get-started/introduction.html /guide/introduction.html
|
||||
/get-started/publishing.html /guide/publishing.html
|
||||
/get-started/testing.html /guide/testing.html
|
||||
/guide/background.html /entrypoints/background.html
|
||||
/guide/bookmarks.html /entrypoints/bookmarks.html
|
||||
/guide/content-scripts.html /entrypoints/content-scripts.html
|
||||
/guide/css.html /entrypoints/css.html
|
||||
/guide/devtools.html /entrypoints/devtools.html
|
||||
/guide/history.html /entrypoints/history.html
|
||||
/guide/manifest.html /entrypoints/manifest.html
|
||||
/guide/newtab.html /entrypoints/newtab.html
|
||||
/guide/options.html /entrypoints/options.html
|
||||
/guide/popup.html /entrypoints/popup.html
|
||||
/guide/sandbox.html /entrypoints/sandbox.html
|
||||
/guide/sidepanel.html /entrypoints/sidepanel.html
|
||||
/guide/unlisted-pages.html /entrypoints/unlisted-pages.html
|
||||
/guide/unlisted-scripts.html /entrypoints/unlisted-scripts.html
|
||||
/guide/build-targets.html /guide/multiple-browsers.html
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,5 @@
|
||||
User-agent: *
|
||||
Disallow: /api.html
|
||||
Disallow: /config.html
|
||||
|
||||
Sitemap: https://wxt.dev/sitemap.xml
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"entryPoints": [
|
||||
"../src",
|
||||
"../src/client",
|
||||
"../src/browser.ts",
|
||||
"../src/sandbox",
|
||||
"../src/storage.ts",
|
||||
"../src/testing"
|
||||
],
|
||||
"plugin": ["typedoc-plugin-markdown", "typedoc-vitepress-theme"],
|
||||
"out": "./api",
|
||||
"githubPages": false,
|
||||
"excludePrivate": true,
|
||||
"excludeProtected": true,
|
||||
"excludeInternal": true,
|
||||
"readme": "none"
|
||||
}
|
||||
@@ -17,11 +17,18 @@ describe('Auto Imports', () => {
|
||||
export {}
|
||||
declare global {
|
||||
const ContentScriptContext: typeof import('wxt/client')['ContentScriptContext']
|
||||
const InvalidMatchPattern: typeof import('wxt/sandbox')['InvalidMatchPattern']
|
||||
const MatchPattern: typeof import('wxt/sandbox')['MatchPattern']
|
||||
const browser: typeof import('wxt/browser')['browser']
|
||||
const defineBackground: typeof import('wxt/client')['defineBackground']
|
||||
const createIframeUi: typeof import('wxt/client')['createIframeUi']
|
||||
const createIntegratedUi: typeof import('wxt/client')['createIntegratedUi']
|
||||
const createShadowRootUi: typeof import('wxt/client')['createShadowRootUi']
|
||||
const defineBackground: typeof import('wxt/sandbox')['defineBackground']
|
||||
const defineConfig: typeof import('wxt')['defineConfig']
|
||||
const defineContentScript: typeof import('wxt/client')['defineContentScript']
|
||||
const mountContentScriptUi: typeof import('wxt/client')['mountContentScriptUi']
|
||||
const defineContentScript: typeof import('wxt/sandbox')['defineContentScript']
|
||||
const defineUnlistedScript: typeof import('wxt/sandbox')['defineUnlistedScript']
|
||||
const fakeBrowser: typeof import('wxt/testing')['fakeBrowser']
|
||||
const storage: typeof import('wxt/storage')['storage']
|
||||
}
|
||||
"
|
||||
`);
|
||||
@@ -35,16 +42,16 @@ describe('Auto Imports', () => {
|
||||
|
||||
expect(await project.serializeFile('.wxt/wxt.d.ts'))
|
||||
.toMatchInlineSnapshot(`
|
||||
".wxt/wxt.d.ts
|
||||
----------------------------------------
|
||||
// Generated by wxt
|
||||
/// <reference types=\\"vite/client\\" />
|
||||
/// <reference types=\\"./types/imports.d.ts\\" />
|
||||
/// <reference types=\\"./types/paths.d.ts\\" />
|
||||
/// <reference types=\\"./types/i18n.d.ts\\" />
|
||||
/// <reference types=\\"./types/globals.d.ts\\" />
|
||||
"
|
||||
`);
|
||||
".wxt/wxt.d.ts
|
||||
----------------------------------------
|
||||
// Generated by wxt
|
||||
/// <reference types="wxt/vite-builder-env" />
|
||||
/// <reference types="./types/imports.d.ts" />
|
||||
/// <reference types="./types/paths.d.ts" />
|
||||
/// <reference types="./types/i18n.d.ts" />
|
||||
/// <reference types="./types/globals.d.ts" />
|
||||
"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -77,10 +84,10 @@ describe('Auto Imports', () => {
|
||||
".wxt/wxt.d.ts
|
||||
----------------------------------------
|
||||
// Generated by wxt
|
||||
/// <reference types=\\"vite/client\\" />
|
||||
/// <reference types=\\"./types/paths.d.ts\\" />
|
||||
/// <reference types=\\"./types/i18n.d.ts\\" />
|
||||
/// <reference types=\\"./types/globals.d.ts\\" />
|
||||
/// <reference types="wxt/vite-builder-env" />
|
||||
/// <reference types="./types/paths.d.ts" />
|
||||
/// <reference types="./types/i18n.d.ts" />
|
||||
/// <reference types="./types/globals.d.ts" />
|
||||
"
|
||||
`,
|
||||
);
|
||||
|
||||
@@ -40,5 +40,5 @@ describe('Init command', () => {
|
||||
"wxt.config.ts",
|
||||
]
|
||||
`);
|
||||
}, 30e3);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,334 +1,33 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { TestProject } from '../utils';
|
||||
|
||||
// TODO: move to unit tests to speed this up - this doesn't have to be in E2E tests
|
||||
|
||||
describe('Manifest Content', () => {
|
||||
describe('popup', () => {
|
||||
const popupContent = (type?: 'browser_action' | 'page_action') => `
|
||||
<html>
|
||||
<head>
|
||||
${type == null ? '' : `<meta name="manifest.type" content="${type}">`}
|
||||
<meta name="manifest.default_icon" content="{ '16': '/icon/16.png' }">
|
||||
<title>Default Title</title>
|
||||
</head>
|
||||
</html>
|
||||
`;
|
||||
|
||||
it('should include an action for mv3', async () => {
|
||||
it.each([
|
||||
{ browser: undefined, outDir: 'chrome-mv3', expected: undefined },
|
||||
{ browser: 'chrome', outDir: 'chrome-mv3', expected: undefined },
|
||||
{ browser: 'firefox', outDir: 'firefox-mv2', expected: true },
|
||||
{ browser: 'safari', outDir: 'safari-mv2', expected: false },
|
||||
])(
|
||||
'should respect the per-browser entrypoint option with %j',
|
||||
async ({ browser, expected, outDir }) => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html', popupContent());
|
||||
|
||||
await project.build();
|
||||
|
||||
const manifest = await project.getOutputManifest();
|
||||
expect(manifest.action).toEqual({
|
||||
default_icon: { '16': '/icon/16.png' },
|
||||
default_title: 'Default Title',
|
||||
default_popup: 'popup.html',
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
[undefined, 'browser_action'],
|
||||
['browser_action', 'browser_action'],
|
||||
['page_action', 'page_action'],
|
||||
] as const)(
|
||||
'should include a browser_action for mv2',
|
||||
async (type, expectedType) => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html', popupContent(type));
|
||||
|
||||
await project.build({ manifestVersion: 2 });
|
||||
|
||||
const manifest = await project.getOutputManifest(
|
||||
'.output/chrome-mv2/manifest.json',
|
||||
);
|
||||
expect(manifest[expectedType]).toEqual({
|
||||
default_icon: { '16': '/icon/16.png' },
|
||||
default_title: 'Default Title',
|
||||
default_popup: 'popup.html',
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('options', () => {
|
||||
const optionsContent = `
|
||||
<html>
|
||||
<head>
|
||||
<meta name="manifest.open_in_tab" content="false">
|
||||
<meta name="manifest.chrome_style" content="true">
|
||||
<meta name="manifest.browser_style" content="true">
|
||||
</head>
|
||||
</html>
|
||||
`;
|
||||
|
||||
it('should include a options_ui and chrome_style for chrome', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/options.html', optionsContent);
|
||||
|
||||
await project.build();
|
||||
const manifest = await project.getOutputManifest();
|
||||
|
||||
expect(manifest.options_ui).toEqual({
|
||||
open_in_tab: false,
|
||||
chrome_style: true,
|
||||
page: 'options.html',
|
||||
});
|
||||
});
|
||||
|
||||
it('should include a options_ui and browser_style for firefox', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/options.html', optionsContent);
|
||||
|
||||
await project.build({ browser: 'firefox' });
|
||||
const manifest = await project.getOutputManifest(
|
||||
'.output/firefox-mv2/manifest.json',
|
||||
);
|
||||
|
||||
expect(manifest.options_ui).toEqual({
|
||||
open_in_tab: false,
|
||||
browser_style: true,
|
||||
page: 'options.html',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('background', () => {
|
||||
const backgroundContent = `
|
||||
export default defineBackground({
|
||||
persistent: true,
|
||||
type: "module",
|
||||
main: () => {},
|
||||
})
|
||||
`;
|
||||
|
||||
it('should include a background script for mv2', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/background.ts', backgroundContent);
|
||||
|
||||
await project.build();
|
||||
const manifest = await project.getOutputManifest();
|
||||
|
||||
expect(manifest.background).toEqual({
|
||||
type: 'module',
|
||||
service_worker: 'background.js',
|
||||
});
|
||||
});
|
||||
|
||||
it('should include a options_ui and browser_style for firefox', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/background.ts', backgroundContent);
|
||||
|
||||
await project.build({ manifestVersion: 2 });
|
||||
const manifest = await project.getOutputManifest(
|
||||
'.output/chrome-mv2/manifest.json',
|
||||
);
|
||||
|
||||
expect(manifest.background).toEqual({
|
||||
persistent: true,
|
||||
scripts: ['background.js'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('icons', () => {
|
||||
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');
|
||||
|
||||
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',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined when no icons are found', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('public/logo.png');
|
||||
project.addFile('public/icon.jpeg');
|
||||
|
||||
await project.build();
|
||||
const manifest = await project.getOutputManifest();
|
||||
|
||||
expect(manifest.icons).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should allow icons to be overwritten from the wxt.config.ts file', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('public/icon-16.png');
|
||||
project.addFile('public/icon-32.png');
|
||||
project.addFile('public/logo-16.png');
|
||||
project.addFile('public/logo-32.png');
|
||||
project.addFile('public/logo-48.png');
|
||||
|
||||
const icons = {
|
||||
'16': 'logo-16.png',
|
||||
'32': 'logo-32.png',
|
||||
'48': 'logo-48.png',
|
||||
};
|
||||
project.setConfigFileConfig({
|
||||
manifest: {
|
||||
icons,
|
||||
},
|
||||
});
|
||||
|
||||
await project.build();
|
||||
const manifest = await project.getOutputManifest();
|
||||
|
||||
expect(manifest.icons).toEqual(icons);
|
||||
});
|
||||
});
|
||||
|
||||
it('should group content scripts and styles together based on their matches and run_at', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'entrypoints/one.content/index.ts',
|
||||
`import "./style.css";
|
||||
export default defineContentScript({
|
||||
matches: ["*://google.com/*"],
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
project.addFile('entrypoints/one.content/style.css', `body { color: red }`);
|
||||
project.addFile(
|
||||
'entrypoints/two.content/index.ts',
|
||||
`import "./style.css";
|
||||
export default defineContentScript({
|
||||
matches: ["*://google.com/*"],
|
||||
runAt: "document_end",
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/two.content/style.css',
|
||||
`body { color: green }`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/three.content/index.ts',
|
||||
`import "./style.css";
|
||||
export default defineContentScript({
|
||||
matches: ["*://google.com/*"],
|
||||
runAt: "document_end",
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/three.content/style.css',
|
||||
`body { color: blue }`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/four.content/index.ts',
|
||||
`import "./style.css";
|
||||
export default defineContentScript({
|
||||
matches: ["*://duckduckgo.com/*"],
|
||||
runAt: "document_end",
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/four.content/style.css',
|
||||
`body { color: yellow }`,
|
||||
);
|
||||
|
||||
await project.build();
|
||||
|
||||
const manifest = await project.getOutputManifest();
|
||||
|
||||
expect(manifest.content_scripts).toContainEqual({
|
||||
matches: ['*://duckduckgo.com/*'],
|
||||
run_at: 'document_end',
|
||||
css: ['assets/four.css'],
|
||||
js: ['content-scripts/four.js'],
|
||||
});
|
||||
expect(manifest.content_scripts).toContainEqual({
|
||||
matches: ['*://google.com/*'],
|
||||
run_at: 'document_end',
|
||||
css: ['assets/three.css', 'assets/two.css'],
|
||||
js: ['content-scripts/three.js', 'content-scripts/two.js'],
|
||||
});
|
||||
expect(manifest.content_scripts).toContainEqual({
|
||||
matches: ['*://google.com/*'],
|
||||
css: ['assets/one.css'],
|
||||
js: ['content-scripts/one.js'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should add to any content scripts declared in wxt.config.ts', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'entrypoints/one.content/index.ts',
|
||||
`export default defineContentScript({
|
||||
matches: ["*://google.com/*"],
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/two.content/style.css',
|
||||
`body {
|
||||
background-color: red;
|
||||
}`,
|
||||
);
|
||||
project.setConfigFileConfig({
|
||||
manifest: {
|
||||
content_scripts: [
|
||||
{
|
||||
css: ['content-scripts/two.css'],
|
||||
matches: ['*://*.google.com/*'],
|
||||
project.addFile(
|
||||
'entrypoints/background.ts',
|
||||
`export default defineBackground({
|
||||
persistent: {
|
||||
firefox: true,
|
||||
safari: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
await project.build({ browser });
|
||||
|
||||
await project.build();
|
||||
|
||||
const manifest = await project.getOutputManifest();
|
||||
|
||||
expect(manifest.content_scripts).toContainEqual({
|
||||
css: ['content-scripts/two.css'],
|
||||
matches: ['*://*.google.com/*'],
|
||||
});
|
||||
expect(manifest.content_scripts).toContainEqual({
|
||||
matches: ['*://google.com/*'],
|
||||
js: ['content-scripts/one.js'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect the transformManifest option', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'wxt.config.ts',
|
||||
`import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
transformManifest(manifest) {
|
||||
manifest.author = "Custom Author"
|
||||
}
|
||||
})`,
|
||||
);
|
||||
|
||||
await project.build();
|
||||
|
||||
const output = await project.serializeFile(
|
||||
'.output/chrome-mv3/manifest.json',
|
||||
);
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"author\\":\\"Custom Author\\"}"
|
||||
`);
|
||||
});
|
||||
const safariManifest = await project.getOutputManifest(
|
||||
`.output/${outDir}/manifest.json`,
|
||||
);
|
||||
expect(safariManifest.background.persistent).toBe(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -7,13 +7,18 @@ describe('Output Directory Structure', () => {
|
||||
project.addFile('entrypoints/.DS_Store');
|
||||
project.addFile('entrypoints/.hidden1/index.html');
|
||||
project.addFile('entrypoints/.hidden2.html');
|
||||
project.addFile('entrypoints/unlisted.html');
|
||||
|
||||
await project.build();
|
||||
|
||||
expect(await project.serializeOutput()).toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\"}"
|
||||
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0"}
|
||||
================================================================================
|
||||
.output/chrome-mv3/unlisted.html
|
||||
----------------------------------------
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -49,19 +54,19 @@ describe('Output Directory Structure', () => {
|
||||
'.output/chrome-mv3/content-scripts/two.js',
|
||||
]),
|
||||
).toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/assets/one.css
|
||||
".output/chrome-mv3/content-scripts/one.css
|
||||
----------------------------------------
|
||||
body{color:#00f}
|
||||
|
||||
================================================================================
|
||||
.output/chrome-mv3/assets/two.css
|
||||
----------------------------------------
|
||||
body{color:red}
|
||||
|
||||
================================================================================
|
||||
.output/chrome-mv3/content-scripts/one.js
|
||||
----------------------------------------
|
||||
<contents-ignored>
|
||||
================================================================================
|
||||
.output/chrome-mv3/content-scripts/two.css
|
||||
----------------------------------------
|
||||
body{color:red}
|
||||
|
||||
================================================================================
|
||||
.output/chrome-mv3/content-scripts/two.js
|
||||
----------------------------------------
|
||||
@@ -69,7 +74,7 @@ describe('Output Directory Structure', () => {
|
||||
================================================================================
|
||||
.output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":[\\"*://*/*\\"],\\"css\\":[\\"assets/one.css\\",\\"assets/two.css\\"],\\"js\\":[\\"content-scripts/one.js\\",\\"content-scripts/two.js\\"]}]}"
|
||||
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","content_scripts":[{"matches":["*://*/*"],"css":["content-scripts/one.css","content-scripts/two.css"],"js":["content-scripts/one.js","content-scripts/two.js"]}]}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -96,7 +101,7 @@ describe('Output Directory Structure', () => {
|
||||
================================================================================
|
||||
.output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":[\\"*://*/*\\"],\\"js\\":[\\"content-scripts/overlay-one.js\\"]}]}"
|
||||
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","content_scripts":[{"matches":["*://*/*"],"js":["content-scripts/overlay-one.js"]}]}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -139,4 +144,87 @@ describe('Output Directory Structure', () => {
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should generate a stats file when analyzing the bundle', async () => {
|
||||
const project = new TestProject();
|
||||
project.setConfigFileConfig({
|
||||
analysis: {
|
||||
enabled: true,
|
||||
template: 'sunburst',
|
||||
},
|
||||
});
|
||||
project.addFile(
|
||||
'entrypoints/background.ts',
|
||||
`export default defineBackground(() => {});`,
|
||||
);
|
||||
project.addFile('entrypoints/popup.html', '<html></html>');
|
||||
project.addFile(
|
||||
'entrypoints/overlay.content.html',
|
||||
`export default defineContentScript({
|
||||
matches: [],
|
||||
main() {},
|
||||
});`,
|
||||
);
|
||||
|
||||
await project.build();
|
||||
|
||||
expect(await project.fileExists('stats.html')).toBe(true);
|
||||
});
|
||||
|
||||
it('should support JavaScript entrypoints', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'entrypoints/background.js',
|
||||
`export default defineBackground(() => {});`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/unlisted.js',
|
||||
`export default defineUnlistedScript(() => {})`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/content.js',
|
||||
`export default defineContentScript({
|
||||
matches: ["*://*.google.com/*"],
|
||||
main() {},
|
||||
})`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/named.content.jsx',
|
||||
`export default defineContentScript({
|
||||
matches: ["*://*.duckduckgo.com/*"],
|
||||
main() {},
|
||||
})`,
|
||||
);
|
||||
|
||||
await project.build();
|
||||
|
||||
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
|
||||
.toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","background":{"service_worker":"background.js"},"content_scripts":[{"matches":["*://*.google.com/*"],"js":["content-scripts/content.js"]},{"matches":["*://*.duckduckgo.com/*"],"js":["content-scripts/named.js"]}]}"
|
||||
`);
|
||||
expect(await project.fileExists('.output/chrome-mv3/background.js'));
|
||||
expect(
|
||||
await project.fileExists('.output/chrome-mv3/content-scripts/content.js'),
|
||||
);
|
||||
expect(
|
||||
await project.fileExists('.output/chrome-mv3/content-scripts/named.js'),
|
||||
);
|
||||
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.addFile('entrypoints/unlisted.html');
|
||||
project.setConfigFileConfig({
|
||||
outDir: 'dist',
|
||||
});
|
||||
|
||||
await project.build();
|
||||
|
||||
expect(await project.fileExists('dist/chrome-mv3/manifest.json')).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { TestProject } from '../utils';
|
||||
|
||||
describe('React', () => {
|
||||
it('should prepare and build an project with a tsx entrypoint', async () => {
|
||||
const project = new TestProject({
|
||||
dependencies: {
|
||||
react: '^18.2.0',
|
||||
'react-dom': '^18.2.0',
|
||||
},
|
||||
devDependencies: {
|
||||
'@types/react': '^18.2.14',
|
||||
'@types/react-dom': '^18.2.6',
|
||||
},
|
||||
});
|
||||
project.addFile(
|
||||
'entrypoints/demo.content.tsx',
|
||||
`import ReactDOM from 'react-dom/client';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: "<all_urls>",
|
||||
main() {
|
||||
const container = document.createElement("div");
|
||||
document.body.append(container)
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(<h1>Hello, world!</h1>);
|
||||
}
|
||||
})`,
|
||||
);
|
||||
|
||||
await project.build();
|
||||
|
||||
expect(
|
||||
await project.fileExists('.output/chrome-mv3/content-scripts/demo.js'),
|
||||
).toBe(true);
|
||||
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
|
||||
.toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","content_scripts":[{"matches":"<all_urls>","js":["content-scripts/demo.js"]}]}"
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,11 @@ describe('Remote Code', () => {
|
||||
it('should download "url:*" modules and include them in the final bundle', async () => {
|
||||
const url = 'https://code.jquery.com/jquery-3.7.1.slim.min.js';
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.ts', `import "url:${url}"`);
|
||||
project.addFile(
|
||||
'entrypoints/popup.ts',
|
||||
`import "url:${url}"
|
||||
export default defineUnlistedScript(() => {})`,
|
||||
);
|
||||
|
||||
await project.build();
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { TestProject } from '../utils';
|
||||
describe('TypeScript Project', () => {
|
||||
it('should generate defined constants correctly', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/unlisted.html');
|
||||
|
||||
await project.build();
|
||||
|
||||
@@ -21,7 +22,7 @@ describe('TypeScript Project', () => {
|
||||
const __IS_SAFARI__: boolean;
|
||||
const __IS_EDGE__: boolean;
|
||||
const __IS_OPERA__: boolean;
|
||||
const __COMMAND__: \\"build\\" | \\"serve\\";
|
||||
const __COMMAND__: "build" | "serve";
|
||||
const __ENTRYPOINT__: string;
|
||||
}
|
||||
"
|
||||
@@ -41,15 +42,17 @@ describe('TypeScript Project', () => {
|
||||
".wxt/types/paths.d.ts
|
||||
----------------------------------------
|
||||
// Generated by wxt
|
||||
import \\"wxt/browser\\";
|
||||
import "wxt/browser";
|
||||
|
||||
declare module \\"wxt/browser\\" {
|
||||
type PublicPath =
|
||||
| \\"/options.html\\"
|
||||
| \\"/popup.html\\"
|
||||
| \\"/sandbox.html\\"
|
||||
declare module "wxt/browser" {
|
||||
export type PublicPath =
|
||||
| "/options.html"
|
||||
| "/popup.html"
|
||||
| "/sandbox.html"
|
||||
type HtmlPublicPath = Extract<PublicPath, \`\${string}.html\`>
|
||||
export interface WxtRuntime extends Runtime.Static {
|
||||
getURL(path: PublicPath): string;
|
||||
getURL(path: \`\${HtmlPublicPath}\${string}\`): string;
|
||||
}
|
||||
}
|
||||
"
|
||||
@@ -58,6 +61,7 @@ describe('TypeScript Project', () => {
|
||||
|
||||
it('should augment the types for browser.i18n.getMessage', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/unlisted.html');
|
||||
project.addFile(
|
||||
'public/_locales/en/messages.json',
|
||||
JSON.stringify({
|
||||
@@ -103,9 +107,9 @@ describe('TypeScript Project', () => {
|
||||
".wxt/types/i18n.d.ts
|
||||
----------------------------------------
|
||||
// Generated by wxt
|
||||
import \\"wxt/browser\\";
|
||||
import "wxt/browser";
|
||||
|
||||
declare module \\"wxt/browser\\" {
|
||||
declare module "wxt/browser" {
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/reference/i18n/#method-getMessage
|
||||
*/
|
||||
@@ -120,91 +124,91 @@ describe('TypeScript Project', () => {
|
||||
/**
|
||||
* The extension or app ID; you might use this string to construct URLs for resources inside the extension. Even unlocalized extensions can use this message.
|
||||
Note: You can't use this message in a manifest file.
|
||||
*
|
||||
* \\"<browser.runtime.id>\\"
|
||||
*
|
||||
* "<browser.runtime.id>"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: \\"@@extension_id\\",
|
||||
messageName: "@@extension_id",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
*
|
||||
*
|
||||
* \\"<browser.i18n.getUiLocale()>\\"
|
||||
*
|
||||
* "<browser.i18n.getUiLocale()>"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: \\"@@ui_locale\\",
|
||||
messageName: "@@ui_locale",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
* The text direction for the current locale, either \\"ltr\\" for left-to-right languages such as English or \\"rtl\\" for right-to-left languages such as Japanese.
|
||||
*
|
||||
* \\"<ltr|rtl>\\"
|
||||
* The text direction for the current locale, either "ltr" for left-to-right languages such as English or "rtl" for right-to-left languages such as Japanese.
|
||||
*
|
||||
* "<ltr|rtl>"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: \\"@@bidi_dir\\",
|
||||
messageName: "@@bidi_dir",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
* If the @@bidi_dir is \\"ltr\\", then this is \\"rtl\\"; otherwise, it's \\"ltr\\".
|
||||
*
|
||||
* \\"<rtl|ltr>\\"
|
||||
* If the @@bidi_dir is "ltr", then this is "rtl"; otherwise, it's "ltr".
|
||||
*
|
||||
* "<rtl|ltr>"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: \\"@@bidi_reversed_dir\\",
|
||||
messageName: "@@bidi_reversed_dir",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
* If the @@bidi_dir is \\"ltr\\", then this is \\"left\\"; otherwise, it's \\"right\\".
|
||||
*
|
||||
* \\"<left|right>\\"
|
||||
* If the @@bidi_dir is "ltr", then this is "left"; otherwise, it's "right".
|
||||
*
|
||||
* "<left|right>"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: \\"@@bidi_start_edge\\",
|
||||
messageName: "@@bidi_start_edge",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
* If the @@bidi_dir is \\"ltr\\", then this is \\"right\\"; otherwise, it's \\"left\\".
|
||||
*
|
||||
* \\"<right|left>\\"
|
||||
* If the @@bidi_dir is "ltr", then this is "right"; otherwise, it's "left".
|
||||
*
|
||||
* "<right|left>"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: \\"@@bidi_end_edge\\",
|
||||
messageName: "@@bidi_end_edge",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
* Ask for the user's name
|
||||
*
|
||||
* \\"What's your name?\\"
|
||||
*
|
||||
* "What's your name?"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: \\"prompt_for_name\\",
|
||||
messageName: "prompt_for_name",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
* Greet the user
|
||||
*
|
||||
* \\"Hello, $USER$\\"
|
||||
*
|
||||
* "Hello, $USER$"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: \\"hello\\",
|
||||
messageName: "hello",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
* Say goodbye to the user
|
||||
*
|
||||
* \\"Goodbye, $USER$. Come back to $OUR_SITE$ soon!\\"
|
||||
*
|
||||
* "Goodbye, $USER$. Come back to $OUR_SITE$ soon!"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: \\"bye\\",
|
||||
messageName: "bye",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
@@ -216,6 +220,7 @@ describe('TypeScript Project', () => {
|
||||
|
||||
it('should reference all the required types in a single declaration file', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/unlisted.html');
|
||||
|
||||
await project.build();
|
||||
|
||||
@@ -224,17 +229,18 @@ describe('TypeScript Project', () => {
|
||||
".wxt/wxt.d.ts
|
||||
----------------------------------------
|
||||
// Generated by wxt
|
||||
/// <reference types=\\"vite/client\\" />
|
||||
/// <reference types=\\"./types/imports.d.ts\\" />
|
||||
/// <reference types=\\"./types/paths.d.ts\\" />
|
||||
/// <reference types=\\"./types/i18n.d.ts\\" />
|
||||
/// <reference types=\\"./types/globals.d.ts\\" />
|
||||
/// <reference types="wxt/vite-builder-env" />
|
||||
/// <reference types="./types/imports.d.ts" />
|
||||
/// <reference types="./types/paths.d.ts" />
|
||||
/// <reference types="./types/i18n.d.ts" />
|
||||
/// <reference types="./types/globals.d.ts" />
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should generate a TSConfig file for the project', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/unlisted.html');
|
||||
|
||||
await project.build();
|
||||
|
||||
@@ -243,39 +249,39 @@ describe('TypeScript Project', () => {
|
||||
".wxt/tsconfig.json
|
||||
----------------------------------------
|
||||
{
|
||||
\\"compilerOptions\\": {
|
||||
\\"target\\": \\"ESNext\\",
|
||||
\\"module\\": \\"ESNext\\",
|
||||
\\"moduleResolution\\": \\"Bundler\\",
|
||||
\\"noEmit\\": true,
|
||||
\\"esModuleInterop\\": true,
|
||||
\\"forceConsistentCasingInFileNames\\": true,
|
||||
\\"resolveJsonModule\\": true,
|
||||
\\"strict\\": true,
|
||||
\\"lib\\": [\\"DOM\\", \\"WebWorker\\"],
|
||||
\\"skipLibCheck\\": true,
|
||||
\\"paths\\": {
|
||||
\\"@@\\": [\\"..\\"],
|
||||
\\"@@/*\\": [\\"../*\\"],
|
||||
\\"~~\\": [\\"..\\"],
|
||||
\\"~~/*\\": [\\"../*\\"],
|
||||
\\"@\\": [\\"..\\"],
|
||||
\\"@/*\\": [\\"../*\\"],
|
||||
\\"~\\": [\\"..\\"],
|
||||
\\"~/*\\": [\\"../*\\"]
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"@": [".."],
|
||||
"@/*": ["../*"],
|
||||
"~": [".."],
|
||||
"~/*": ["../*"],
|
||||
"@@": [".."],
|
||||
"@@/*": ["../*"],
|
||||
"~~": [".."],
|
||||
"~~/*": ["../*"]
|
||||
}
|
||||
},
|
||||
\\"include\\": [
|
||||
\\"../**/*\\",
|
||||
\\"./wxt.d.ts\\"
|
||||
"include": [
|
||||
"../**/*",
|
||||
"./wxt.d.ts"
|
||||
],
|
||||
\\"exclude\\": [\\"../.output\\"]
|
||||
"exclude": ["../.output"]
|
||||
}"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should generate correct path aliases for a custom srcDir', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('src/entrypoints/unlisted.html');
|
||||
project.setConfigFileConfig({
|
||||
srcDir: 'src',
|
||||
});
|
||||
@@ -287,33 +293,82 @@ describe('TypeScript Project', () => {
|
||||
".wxt/tsconfig.json
|
||||
----------------------------------------
|
||||
{
|
||||
\\"compilerOptions\\": {
|
||||
\\"target\\": \\"ESNext\\",
|
||||
\\"module\\": \\"ESNext\\",
|
||||
\\"moduleResolution\\": \\"Bundler\\",
|
||||
\\"noEmit\\": true,
|
||||
\\"esModuleInterop\\": true,
|
||||
\\"forceConsistentCasingInFileNames\\": true,
|
||||
\\"resolveJsonModule\\": true,
|
||||
\\"strict\\": true,
|
||||
\\"lib\\": [\\"DOM\\", \\"WebWorker\\"],
|
||||
\\"skipLibCheck\\": true,
|
||||
\\"paths\\": {
|
||||
\\"@@\\": [\\"..\\"],
|
||||
\\"@@/*\\": [\\"../*\\"],
|
||||
\\"~~\\": [\\"..\\"],
|
||||
\\"~~/*\\": [\\"../*\\"],
|
||||
\\"@\\": [\\"../src\\"],
|
||||
\\"@/*\\": [\\"../src/*\\"],
|
||||
\\"~\\": [\\"../src\\"],
|
||||
\\"~/*\\": [\\"../src/*\\"]
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"@": ["../src"],
|
||||
"@/*": ["../src/*"],
|
||||
"~": ["../src"],
|
||||
"~/*": ["../src/*"],
|
||||
"@@": [".."],
|
||||
"@@/*": ["../*"],
|
||||
"~~": [".."],
|
||||
"~~/*": ["../*"]
|
||||
}
|
||||
},
|
||||
\\"include\\": [
|
||||
\\"../**/*\\",
|
||||
\\"./wxt.d.ts\\"
|
||||
"include": [
|
||||
"../**/*",
|
||||
"./wxt.d.ts"
|
||||
],
|
||||
\\"exclude\\": [\\"../.output\\"]
|
||||
"exclude": ["../.output"]
|
||||
}"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should add additional path aliases listed in the alias config, preventing defaults from being overridden', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('src/entrypoints/unlisted.html');
|
||||
project.setConfigFileConfig({
|
||||
srcDir: 'src',
|
||||
alias: {
|
||||
example: 'example',
|
||||
'@': 'ignored-path',
|
||||
},
|
||||
});
|
||||
|
||||
await project.build();
|
||||
|
||||
const output = await project.serializeFile('.wxt/tsconfig.json');
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
".wxt/tsconfig.json
|
||||
----------------------------------------
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"example": ["../example"],
|
||||
"example/*": ["../example/*"],
|
||||
"@": ["../src"],
|
||||
"@/*": ["../src/*"],
|
||||
"~": ["../src"],
|
||||
"~/*": ["../src/*"],
|
||||
"@@": [".."],
|
||||
"@@/*": ["../*"],
|
||||
"~~": [".."],
|
||||
"~~/*": ["../*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"../**/*",
|
||||
"./wxt.d.ts"
|
||||
],
|
||||
"exclude": ["../.output"]
|
||||
}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { TestProject } from '../utils';
|
||||
import { InlineConfig } from '~/types';
|
||||
|
||||
describe('User Config', () => {
|
||||
// Root directory is tested with all tests.
|
||||
@@ -28,7 +29,7 @@ describe('User Config', () => {
|
||||
================================================================================
|
||||
.output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"background\\":{\\"service_worker\\":\\"background.js\\"}}"
|
||||
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","background":{"service_worker":"background.js"}}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -54,12 +55,13 @@ describe('User Config', () => {
|
||||
================================================================================
|
||||
.output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"background\\":{\\"service_worker\\":\\"background.js\\"}}"
|
||||
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","background":{"service_worker":"background.js"}}"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should merge inline and user config based manifests', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/unlisted.html');
|
||||
project.addFile(
|
||||
'wxt.config.ts',
|
||||
`import { defineConfig } from 'wxt';
|
||||
@@ -78,11 +80,31 @@ describe('User Config', () => {
|
||||
}),
|
||||
});
|
||||
|
||||
const output = await project.serializeOutput();
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
|
||||
.toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"example_customization\\":[\\"production\\",\\"chrome\\",\\"3\\",\\"build\\"]}"
|
||||
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","example_customization":["3","build","production","chrome"]}"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should exclude the polyfill when the experimental setting is set to false', async () => {
|
||||
const buildBackground = async (config?: InlineConfig) => {
|
||||
const background = `export default defineBackground(() => console.log(browser.runtime.id));`;
|
||||
const projectWithPolyfill = new TestProject();
|
||||
projectWithPolyfill.addFile('entrypoints/background.ts', background);
|
||||
await projectWithPolyfill.build(config);
|
||||
return await projectWithPolyfill.serializeFile(
|
||||
'.output/chrome-mv3/background.js',
|
||||
);
|
||||
};
|
||||
|
||||
const withPolyfill = await buildBackground();
|
||||
const withoutPolyfill = await buildBackground({
|
||||
experimental: {
|
||||
includeBrowserPolyfill: false,
|
||||
},
|
||||
});
|
||||
expect(withoutPolyfill).not.toBe(withPolyfill);
|
||||
});
|
||||
});
|
||||
|
||||
+17
-11
@@ -1,32 +1,36 @@
|
||||
import { dirname, join, relative, resolve } from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import glob from 'fast-glob';
|
||||
import { execSync } from 'child_process';
|
||||
import { execaCommand } from 'execa';
|
||||
import { InlineConfig, UserConfig, build } from '../src';
|
||||
import { normalizePath } from '../src/core/utils/paths';
|
||||
import merge from 'lodash.merge';
|
||||
|
||||
export class TestProject {
|
||||
files: Array<[string, string]> = [];
|
||||
config: UserConfig | undefined;
|
||||
readonly root: string;
|
||||
|
||||
constructor(root = 'e2e/dist') {
|
||||
constructor(packageJson: any = {}) {
|
||||
// We can't put each test's project inside e2e/dist directly, otherwise the wxt.config.ts
|
||||
// file is cached and cannot be different between each test. Instead, we add a random ID to the
|
||||
// end to make each test's path unique.
|
||||
const id = Math.random().toString(32).substring(3);
|
||||
this.root = join(root, id);
|
||||
this.root = join('e2e/dist', id);
|
||||
this.files.push([
|
||||
'package.json',
|
||||
JSON.stringify(
|
||||
{
|
||||
name: 'E2E Extension',
|
||||
description: 'Example description',
|
||||
version: '0.0.0-test',
|
||||
dependencies: {
|
||||
wxt: '../../..',
|
||||
merge(
|
||||
{
|
||||
name: 'E2E Extension',
|
||||
description: 'Example description',
|
||||
version: '0.0.0',
|
||||
dependencies: {
|
||||
wxt: '../../..',
|
||||
},
|
||||
},
|
||||
},
|
||||
packageJson,
|
||||
),
|
||||
null,
|
||||
2,
|
||||
),
|
||||
@@ -72,8 +76,10 @@ export class TestProject {
|
||||
await fs.ensureDir(fileDir);
|
||||
await fs.writeFile(filePath, content ?? '', 'utf-8');
|
||||
}
|
||||
execSync('npm i --ignore-scripts', { cwd: this.root });
|
||||
|
||||
await execaCommand('pnpm --ignore-workspace i --ignore-scripts', {
|
||||
cwd: this.root,
|
||||
});
|
||||
await build({ ...config, root: this.root });
|
||||
}
|
||||
|
||||
|
||||
+103
-51
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "wxt",
|
||||
"type": "module",
|
||||
"version": "0.6.1",
|
||||
"version": "0.14.4",
|
||||
"description": "Next gen framework for developing web extensions",
|
||||
"engines": {
|
||||
"node": ">=18.16.0",
|
||||
"node": ">=18",
|
||||
"pnpm": ">=8"
|
||||
},
|
||||
"repository": {
|
||||
@@ -30,100 +30,152 @@
|
||||
"bin",
|
||||
"dist"
|
||||
],
|
||||
"bin": "./bin/wxt.cjs",
|
||||
"bin": "./bin/wxt.mjs",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"require": "./dist/index.cjs",
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
"import": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/index.d.cts",
|
||||
"default": "./dist/index.cjs"
|
||||
}
|
||||
},
|
||||
"./client": {
|
||||
"import": "./dist/client.js",
|
||||
"types": "./dist/client.d.ts"
|
||||
"types": "./dist/client.d.ts",
|
||||
"import": "./dist/client.js"
|
||||
},
|
||||
"./sandbox": {
|
||||
"types": "./dist/sandbox.d.ts",
|
||||
"import": "./dist/sandbox.js"
|
||||
},
|
||||
"./browser": {
|
||||
"import": "./dist/browser.js",
|
||||
"types": "./dist/browser.d.ts"
|
||||
"types": "./dist/browser.d.ts",
|
||||
"import": "./dist/browser.js"
|
||||
},
|
||||
"./testing": {
|
||||
"import": {
|
||||
"types": "./dist/testing.d.ts",
|
||||
"default": "./dist/testing.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/testing.d.cts",
|
||||
"default": "./dist/testing.cjs"
|
||||
}
|
||||
},
|
||||
"./storage": {
|
||||
"import": {
|
||||
"types": "./dist/storage.d.ts",
|
||||
"default": "./dist/storage.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/storage.d.cts",
|
||||
"default": "./dist/storage.cjs"
|
||||
}
|
||||
},
|
||||
"./vite-builder-env": {
|
||||
"types": "./dist/vite-builder-env.d.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"wxt": "tsx src/cli/index.ts",
|
||||
"wxt": "tsx src/cli.ts",
|
||||
"build": "tsx scripts/build.ts",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --write .",
|
||||
"compile": "tsc --noEmit",
|
||||
"format:check": "prettier --check .",
|
||||
"lint": "run-p -c -s lint:*",
|
||||
"lint:eslint": "echo 'ESLint: TODO'",
|
||||
"lint:package": "publint",
|
||||
"compile": "run-s -c compile:*",
|
||||
"compile:wxt": "tsc --noEmit",
|
||||
"compile:virtual": "tsc --noEmit -p src/virtual",
|
||||
"test": "vitest",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:coverage": "vitest run --coverage.enabled \"--coverage.include=src/**\" \"--coverage.exclude=src/core/utils/testing/**\" \"--coverage.exclude=**/*.d.ts\"",
|
||||
"prepare": "simple-git-hooks",
|
||||
"prepublish": "pnpm -s build",
|
||||
"docs:dev": "vitepress dev docs",
|
||||
"docs:build": "vitepress build docs",
|
||||
"docs:preview": "vitepress preview docs",
|
||||
"sync-releases": "pnpx changelogen@latest gh release all"
|
||||
"docs:gen": "typedoc --options docs/typedoc.json",
|
||||
"docs:dev": "pnpm -s docs:gen && vitepress dev docs",
|
||||
"docs:build": "pnpm -s docs:gen && vitepress build docs",
|
||||
"docs:preview": "pnpm -s docs:gen && vitepress preview docs",
|
||||
"sync-releases": "pnpx changelogen@latest gh release"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/webextension-polyfill": "^0.10.1",
|
||||
"@webext-core/fake-browser": "^1.2.2",
|
||||
"@webext-core/match-patterns": "^1.0.1",
|
||||
"@types/webextension-polyfill": "^0.10.5",
|
||||
"@webext-core/fake-browser": "^1.3.1",
|
||||
"@webext-core/isolated-element": "^1.1.1",
|
||||
"@webext-core/match-patterns": "^1.0.3",
|
||||
"async-mutex": "^0.4.0",
|
||||
"c12": "^1.4.2",
|
||||
"c12": "^1.5.1",
|
||||
"cac": "^6.7.14",
|
||||
"chokidar": "^3.5.3",
|
||||
"consola": "^3.2.3",
|
||||
"defu": "^6.1.3",
|
||||
"dequal": "^2.0.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",
|
||||
"unimport": "^3.1.0",
|
||||
"vite": "^4.4.7",
|
||||
"rollup-plugin-visualizer": "^5.9.2",
|
||||
"unimport": "^3.4.0",
|
||||
"vite": "^5.0.0",
|
||||
"web-ext-run": "^0.1.0",
|
||||
"webextension-polyfill": "^0.10.0",
|
||||
"zip-dir": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^8.0.2",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/lodash.merge": "^4.6.7",
|
||||
"@types/node": "^20.4.5",
|
||||
"@types/prompts": "^2.4.4",
|
||||
"@vitest/coverage-v8": "^0.34.1",
|
||||
"execa": "^7.2.0",
|
||||
"lint-staged": "^14.0.0",
|
||||
"@faker-js/faker": "^8.3.1",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/lodash.merge": "^4.6.9",
|
||||
"@types/node": "^20.10.3",
|
||||
"@types/normalize-path": "^3.0.2",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"@vitest/coverage-v8": "^1.0.1",
|
||||
"execa": "^8.0.1",
|
||||
"happy-dom": "^12.10.3",
|
||||
"lint-staged": "^15.2.0",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"ora": "^7.0.1",
|
||||
"prettier": "^3.0.1",
|
||||
"p-map": "^7.0.0",
|
||||
"prettier": "^3.1.0",
|
||||
"publint": "^0.2.6",
|
||||
"simple-git-hooks": "^2.9.0",
|
||||
"ts-morph": "^19.0.0",
|
||||
"tsup": "^7.1.0",
|
||||
"tsx": "^3.12.7",
|
||||
"typescript": "^5.1.6",
|
||||
"vitepress": "1.0.0-rc.10",
|
||||
"vitest": "^0.34.1",
|
||||
"vitest-mock-extended": "^1.1.4",
|
||||
"vue": "^3.3.4",
|
||||
"webextension-polyfill": "^0.10.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"webextension-polyfill": ">=0.10.0"
|
||||
"tsup": "^8.0.1",
|
||||
"tsx": "^4.6.2",
|
||||
"typedoc": "^0.25.4",
|
||||
"typedoc-plugin-markdown": "4.0.0-next.23",
|
||||
"typedoc-vitepress-theme": "1.0.0-next.3",
|
||||
"typescript": "^5.3.2",
|
||||
"vitepress": "1.0.0-rc.34",
|
||||
"vitest": "^1.1.0",
|
||||
"vitest-mock-extended": "^1.3.1",
|
||||
"vue": "^3.3.10"
|
||||
},
|
||||
"packageManager": "pnpm@8.6.3",
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "pnpm lint-staged"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*": "prettier --write"
|
||||
"*": "prettier --ignore-unknown --write"
|
||||
},
|
||||
"changelog": {
|
||||
"excludeAuthors": [
|
||||
"aaronklinker1@gmail.com"
|
||||
]
|
||||
},
|
||||
"pnpm": {
|
||||
"peerDependencyRules": {
|
||||
|
||||
Generated
+1475
-884
File diff suppressed because it is too large
Load Diff
+91
-59
@@ -1,87 +1,119 @@
|
||||
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'];
|
||||
await fs.rm(path.join(outDir, '*'), { recursive: true, force: true });
|
||||
|
||||
await fs.rm(outDir, { recursive: true, force: true });
|
||||
const preset = {
|
||||
dts: true,
|
||||
silent: true,
|
||||
sourcemap: false,
|
||||
external: [
|
||||
'virtual:user-unlisted-script',
|
||||
'virtual:user-content-script-isolated-world',
|
||||
'virtual:user-content-script-main-world',
|
||||
'virtual:user-background',
|
||||
],
|
||||
} satisfies tsup.Options;
|
||||
|
||||
await Promise.all([
|
||||
tsup.build({
|
||||
function spinnerPMap(configs: tsup.Options[]) {
|
||||
let progress = 1;
|
||||
const updateSpinner = () => {
|
||||
spinner.text = `${spinnerText} [${progress}/${configs.length}]`;
|
||||
};
|
||||
updateSpinner();
|
||||
|
||||
return pMap(
|
||||
config,
|
||||
async (config) => {
|
||||
const res = await tsup.build(config);
|
||||
progress++;
|
||||
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' },
|
||||
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'],
|
||||
}),
|
||||
...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({
|
||||
clean: true,
|
||||
},
|
||||
// ESM-only
|
||||
{
|
||||
...preset,
|
||||
entry: {
|
||||
'virtual-modules/reload-html': `src/client/virtual-modules/reload-html.ts`,
|
||||
browser: 'src/browser.ts',
|
||||
sandbox: 'src/sandbox/index.ts',
|
||||
client: 'src/client/index.ts',
|
||||
},
|
||||
format: ['esm'],
|
||||
sourcemap: true,
|
||||
silent: true,
|
||||
external: ['vite'],
|
||||
}),
|
||||
tsup.build({
|
||||
},
|
||||
{
|
||||
...preset,
|
||||
entry: {
|
||||
'virtual-modules/fake-browser': `src/client/virtual-modules/fake-browser.ts`,
|
||||
'virtual/background-entrypoint': 'src/virtual/background-entrypoint.ts',
|
||||
'virtual/content-script-isolated-world-entrypoint':
|
||||
'src/virtual/content-script-isolated-world-entrypoint.ts',
|
||||
'virtual/content-script-main-world-entrypoint':
|
||||
'src/virtual/content-script-main-world-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', 'cjs'],
|
||||
silent: true,
|
||||
external: ['vite'],
|
||||
}),
|
||||
]).catch((err) => {
|
||||
format: ['esm'],
|
||||
splitting: false,
|
||||
dts: false,
|
||||
external: [...preset.external, 'wxt'],
|
||||
},
|
||||
// CJS-only
|
||||
{
|
||||
...preset,
|
||||
entry: {
|
||||
cli: 'src/cli.ts',
|
||||
},
|
||||
format: ['esm'],
|
||||
banner: {
|
||||
// Fixes dynamic require of nodejs modules. See https://github.com/wxt-dev/wxt/issues/355
|
||||
// https://github.com/evanw/esbuild/issues/1921#issuecomment-1152991694
|
||||
js: "import { createRequire } from 'module';const require = createRequire(import.meta.url);",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
await spinnerPMap(config).catch((err) => {
|
||||
spinner.fail();
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Copy "public" files that need shipped inside WXT
|
||||
await fs.copyFile('src/vite-builder-env.d.ts', 'dist/vite-builder-env.d.ts');
|
||||
|
||||
spinner.clear().stop();
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
Vendored
-16
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
import { describe, it, vi, beforeEach, expect } from 'vitest';
|
||||
import { build } from '../core/build';
|
||||
import { createServer } from '../core/create-server';
|
||||
import { zip } from '../core/zip';
|
||||
import { prepare } from '../core/prepare';
|
||||
import { clean } from '../core/clean';
|
||||
import { initialize } from '../core/initialize';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import consola from 'consola';
|
||||
|
||||
vi.mock('../core/build');
|
||||
const buildMock = vi.mocked(build);
|
||||
|
||||
vi.mock('../core/create-server');
|
||||
const createServerMock = vi.mocked(createServer);
|
||||
|
||||
vi.mock('../core/zip');
|
||||
const zipMock = vi.mocked(zip);
|
||||
|
||||
vi.mock('../core/prepare');
|
||||
const prepareMock = vi.mocked(prepare);
|
||||
|
||||
vi.mock('../core/clean');
|
||||
const cleanMock = vi.mocked(clean);
|
||||
|
||||
vi.mock('../core/initialize');
|
||||
const initializeMock = vi.mocked(initialize);
|
||||
|
||||
consola.wrapConsole();
|
||||
|
||||
const ogArgv = process.argv;
|
||||
|
||||
function mockArgv(...args: string[]) {
|
||||
process.argv = ['/bin/node', 'bin/wxt.mjs', ...args];
|
||||
}
|
||||
|
||||
describe('CLI', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
process.argv = ogArgv;
|
||||
createServerMock.mockResolvedValue(mock());
|
||||
});
|
||||
|
||||
describe('dev', () => {
|
||||
it('should not pass any config when no flags are passed', async () => {
|
||||
mockArgv();
|
||||
await import('../cli');
|
||||
|
||||
expect(createServerMock).toBeCalledWith({});
|
||||
});
|
||||
|
||||
it('should respect passing a custom root', async () => {
|
||||
mockArgv('path/to/root');
|
||||
await import('../cli');
|
||||
|
||||
expect(createServerMock).toBeCalledWith({
|
||||
root: 'path/to/root',
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect a custom config file', async () => {
|
||||
mockArgv('-c', './path/to/config.ts');
|
||||
await import('../cli');
|
||||
|
||||
expect(createServerMock).toBeCalledWith({
|
||||
configFile: './path/to/config.ts',
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing a custom mode', async () => {
|
||||
mockArgv('-m', 'development');
|
||||
await import('../cli');
|
||||
|
||||
expect(createServerMock).toBeCalledWith({
|
||||
mode: 'development',
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing a custom browser', async () => {
|
||||
mockArgv('-b', 'firefox');
|
||||
await import('../cli');
|
||||
|
||||
expect(createServerMock).toBeCalledWith({
|
||||
browser: 'firefox',
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass correct filtered entrypoints', async () => {
|
||||
mockArgv('-e', 'popup', '-e', 'options');
|
||||
await import('../cli');
|
||||
|
||||
expect(createServerMock).toBeCalledWith({
|
||||
filterEntrypoints: ['popup', 'options'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing --mv2', async () => {
|
||||
mockArgv('--mv2');
|
||||
await import('../cli');
|
||||
|
||||
expect(createServerMock).toBeCalledWith({
|
||||
manifestVersion: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing --mv3', async () => {
|
||||
mockArgv('--mv3');
|
||||
await import('../cli');
|
||||
|
||||
expect(createServerMock).toBeCalledWith({
|
||||
manifestVersion: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing --debug', async () => {
|
||||
mockArgv('--debug');
|
||||
await import('../cli');
|
||||
|
||||
expect(createServerMock).toBeCalledWith({
|
||||
debug: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('build', () => {
|
||||
it('should not pass any config when no flags are passed', async () => {
|
||||
mockArgv('build');
|
||||
await import('../cli');
|
||||
|
||||
expect(buildMock).toBeCalledWith({});
|
||||
});
|
||||
|
||||
it('should respect passing a custom root', async () => {
|
||||
mockArgv('build', 'path/to/root');
|
||||
await import('../cli');
|
||||
|
||||
expect(buildMock).toBeCalledWith({
|
||||
root: 'path/to/root',
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect a custom config file', async () => {
|
||||
mockArgv('build', '-c', './path/to/config.ts');
|
||||
await import('../cli');
|
||||
|
||||
expect(buildMock).toBeCalledWith({
|
||||
configFile: './path/to/config.ts',
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing a custom mode', async () => {
|
||||
mockArgv('build', '-m', 'development');
|
||||
await import('../cli');
|
||||
|
||||
expect(buildMock).toBeCalledWith({
|
||||
mode: 'development',
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing a custom browser', async () => {
|
||||
mockArgv('build', '-b', 'firefox');
|
||||
await import('../cli');
|
||||
|
||||
expect(buildMock).toBeCalledWith({
|
||||
browser: 'firefox',
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass correct filtered entrypoints', async () => {
|
||||
mockArgv('build', '-e', 'popup', '-e', 'options');
|
||||
await import('../cli');
|
||||
|
||||
expect(buildMock).toBeCalledWith({
|
||||
filterEntrypoints: ['popup', 'options'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing --mv2', async () => {
|
||||
mockArgv('build', '--mv2');
|
||||
await import('../cli');
|
||||
|
||||
expect(buildMock).toBeCalledWith({
|
||||
manifestVersion: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing --mv3', async () => {
|
||||
mockArgv('build', '--mv3');
|
||||
await import('../cli');
|
||||
|
||||
expect(buildMock).toBeCalledWith({
|
||||
manifestVersion: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it('should include analysis in the build', async () => {
|
||||
mockArgv('build', '--analyze');
|
||||
await import('../cli');
|
||||
|
||||
expect(buildMock).toBeCalledWith({
|
||||
analysis: {
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing --debug', async () => {
|
||||
mockArgv('build', '--debug');
|
||||
await import('../cli');
|
||||
|
||||
expect(buildMock).toBeCalledWith({
|
||||
debug: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('zip', () => {
|
||||
it('should not pass any config when no flags are passed', async () => {
|
||||
mockArgv('zip');
|
||||
await import('../cli');
|
||||
|
||||
expect(zipMock).toBeCalledWith({});
|
||||
});
|
||||
|
||||
it('should respect passing a custom root', async () => {
|
||||
mockArgv('zip', 'path/to/root');
|
||||
await import('../cli');
|
||||
|
||||
expect(zipMock).toBeCalledWith({
|
||||
root: 'path/to/root',
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect a custom config file', async () => {
|
||||
mockArgv('zip', '-c', './path/to/config.ts');
|
||||
await import('../cli');
|
||||
|
||||
expect(zipMock).toBeCalledWith({
|
||||
configFile: './path/to/config.ts',
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing a custom mode', async () => {
|
||||
mockArgv('zip', '-m', 'development');
|
||||
await import('../cli');
|
||||
|
||||
expect(zipMock).toBeCalledWith({
|
||||
mode: 'development',
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing a custom browser', async () => {
|
||||
mockArgv('zip', '-b', 'firefox');
|
||||
await import('../cli');
|
||||
|
||||
expect(zipMock).toBeCalledWith({
|
||||
browser: 'firefox',
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing --mv2', async () => {
|
||||
mockArgv('zip', '--mv2');
|
||||
await import('../cli');
|
||||
|
||||
expect(zipMock).toBeCalledWith({
|
||||
manifestVersion: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing --mv3', async () => {
|
||||
mockArgv('zip', '--mv3');
|
||||
await import('../cli');
|
||||
|
||||
expect(zipMock).toBeCalledWith({
|
||||
manifestVersion: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing --debug', async () => {
|
||||
mockArgv('zip', '--debug');
|
||||
await import('../cli');
|
||||
|
||||
expect(zipMock).toBeCalledWith({
|
||||
debug: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('prepare', () => {
|
||||
it('should not pass any config when no flags are passed', async () => {
|
||||
mockArgv('prepare');
|
||||
await import('../cli');
|
||||
|
||||
expect(prepareMock).toBeCalledWith({});
|
||||
});
|
||||
|
||||
it('should respect passing a custom root', async () => {
|
||||
mockArgv('prepare', 'path/to/root');
|
||||
await import('../cli');
|
||||
|
||||
expect(prepareMock).toBeCalledWith({
|
||||
root: 'path/to/root',
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect a custom config file', async () => {
|
||||
mockArgv('prepare', '-c', './path/to/config.ts');
|
||||
await import('../cli');
|
||||
|
||||
expect(prepareMock).toBeCalledWith({
|
||||
configFile: './path/to/config.ts',
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing --debug', async () => {
|
||||
mockArgv('prepare', '--debug');
|
||||
await import('../cli');
|
||||
|
||||
expect(prepareMock).toBeCalledWith({
|
||||
debug: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('clean', () => {
|
||||
it('should not pass any config when no flags are passed', async () => {
|
||||
mockArgv('clean');
|
||||
await import('../cli');
|
||||
|
||||
expect(cleanMock).toBeCalledWith(undefined);
|
||||
});
|
||||
|
||||
it('should respect passing a custom root', async () => {
|
||||
mockArgv('clean', 'path/to/root');
|
||||
await import('../cli');
|
||||
|
||||
expect(cleanMock).toBeCalledWith('path/to/root');
|
||||
});
|
||||
});
|
||||
|
||||
describe('init', () => {
|
||||
it('should not pass any options when no flags are passed', async () => {
|
||||
mockArgv('init');
|
||||
await import('../cli');
|
||||
|
||||
expect(initializeMock).toBeCalledWith({});
|
||||
});
|
||||
|
||||
it('should respect the provided folder', async () => {
|
||||
mockArgv('init', 'path/to/folder');
|
||||
await import('../cli');
|
||||
|
||||
expect(initializeMock).toBeCalledWith({
|
||||
directory: 'path/to/folder',
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing --template', async () => {
|
||||
mockArgv('init', '-t', 'vue');
|
||||
await import('../cli');
|
||||
|
||||
expect(initializeMock).toBeCalledWith({
|
||||
template: 'vue',
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing --pm', async () => {
|
||||
mockArgv('init', '--pm', 'pnpm');
|
||||
await import('../cli');
|
||||
|
||||
expect(initializeMock).toBeCalledWith({
|
||||
packageManager: 'pnpm',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user