Compare commits
86 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6aa827438c | |||
| 6ee8a43677 | |||
| d54d6111e6 | |||
| aefc8d3167 | |||
| 7ccff533ff | |||
| edfa075030 | |||
| ef140dc7f8 | |||
| 39467d10f3 | |||
| 97f0938c99 | |||
| 18741eae8e | |||
| 3f3ef37c96 | |||
| 323045a7c3 | |||
| 9b34180467 | |||
| 7f15305037 | |||
| 35cc1c94bf | |||
| 547c1850ac | |||
| 798f02f626 | |||
| d5948abfe5 | |||
| ec01f048ed | |||
| 8751c55063 | |||
| dfe424f35d | |||
| 25677ba445 | |||
| 390f65cf39 | |||
| 310f994ccb | |||
| 23e4295e08 | |||
| abdef08aeb | |||
| 38d4f9c879 | |||
| 709b61a174 | |||
| 4184b0529d | |||
| d3ff4c6afe | |||
| 16de4da27f | |||
| 936d83bc08 | |||
| aea866c9cd | |||
| 3107d27184 | |||
| 3a336eba89 | |||
| 044a24fd6a | |||
| 5b269f4369 | |||
| 7d55faff20 | |||
| 386f8db5db | |||
| 94a1097df5 | |||
| be95a778ab | |||
| d3b1536f39 | |||
| 9f2b989a2f | |||
| e621aa8f8c | |||
| 488d7885ca | |||
| 3a9fd3909f | |||
| 2c70246af5 | |||
| a82a66ec37 | |||
| 19c0948d95 | |||
| 04e5400a46 | |||
| 33c1c171db | |||
| 9cc464f48b | |||
| 58a84ec253 | |||
| 609223566c | |||
| 0aebb67b73 | |||
| 5fa5fd01cb | |||
| de16423e02 | |||
| aa4c0449e9 | |||
| 9d00eb2466 | |||
| 9ac756fb43 | |||
| f195aa429c | |||
| ca20a210ea | |||
| 547fee0e0e | |||
| 54b18cc66e | |||
| d8c190365a | |||
| b625f41919 | |||
| 236a135ac2 | |||
| dca43efe0d | |||
| eedb136e13 | |||
| c71b5e12f2 | |||
| 495c5c8e4c | |||
| 63c4359d83 | |||
| efc3230674 | |||
| 69e840b984 | |||
| 2fb5a542d3 | |||
| 84657e035a | |||
| 6f5bf89645 | |||
| 90fa6bfc40 | |||
| 992452cdae | |||
| 5c7e041f7f | |||
| 3db5cec2ca | |||
| 375a2a6640 | |||
| 77cbfc1985 | |||
| 5b48ae99f4 | |||
| c1ea9ba4a3 | |||
| f28a29e118 |
@@ -1,2 +1,3 @@
|
||||
* text=auto eol=lf
|
||||
pnpm-lock.yaml linguist-generated
|
||||
docs/config.md linguist-generated
|
||||
|
||||
@@ -7,7 +7,7 @@ jobs:
|
||||
uses: './.github/workflows/validate.yml'
|
||||
|
||||
publish:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- validate
|
||||
steps:
|
||||
@@ -28,10 +28,7 @@ jobs:
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pnpm install --ignore-scripts
|
||||
pnpm build
|
||||
pnpm install
|
||||
run: pnpm install
|
||||
|
||||
- name: Bump and Tag
|
||||
run: |
|
||||
|
||||
@@ -7,8 +7,13 @@ on:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-20.04
|
||||
wxt:
|
||||
name: WXT
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
@@ -25,10 +30,7 @@ jobs:
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pnpm install --ignore-scripts
|
||||
pnpm build
|
||||
pnpm install
|
||||
run: pnpm install
|
||||
|
||||
- name: Formatting
|
||||
run: pnpm format:check
|
||||
@@ -42,7 +44,78 @@ jobs:
|
||||
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
|
||||
run: pnpm test:coverage --reporter=default --reporter=hanging-process
|
||||
|
||||
project-templates:
|
||||
name: Project Templates
|
||||
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
|
||||
|
||||
@@ -4,3 +4,5 @@ dist
|
||||
e2e/project
|
||||
.wxt
|
||||
docs/.vitepress/cache
|
||||
pnpm-lock.yaml
|
||||
CHANGELOG.md
|
||||
|
||||
@@ -1,5 +1,218 @@
|
||||
# Changelog
|
||||
|
||||
## v0.3.2
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.3.1...v0.3.2)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Discover icons from the public directory ([#72](https://github.com/aklinker1/wxt/pull/72))
|
||||
- Don't allow auto-importing from subdirectories ([d54d611](https://github.com/aklinker1/wxt/commit/d54d611))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Document the `url:` import prefix for remote code ([323045a](https://github.com/aklinker1/wxt/commit/323045a))
|
||||
- Fix typos ([97f0938](https://github.com/aklinker1/wxt/commit/97f0938))
|
||||
- Fix capitalization ([39467d1](https://github.com/aklinker1/wxt/commit/39467d1))
|
||||
- Generate markdown for config reference ([#74](https://github.com/aklinker1/wxt/pull/74))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Upgrade dependencies ([798f02f](https://github.com/aklinker1/wxt/commit/798f02f))
|
||||
- Upgrade vite (`v4.3` → `v4.4`) ([547c185](https://github.com/aklinker1/wxt/commit/547c185))
|
||||
- Update templates to work with CSS entrypoints ([7f15305](https://github.com/aklinker1/wxt/commit/7f15305))
|
||||
- Improve file list output in CI ([#73](https://github.com/aklinker1/wxt/pull/73))
|
||||
|
||||
### 🤖 CI
|
||||
|
||||
- Validate templates against `main` ([#66](https://github.com/aklinker1/wxt/pull/66))
|
||||
- List vite version when validating project templates ([ef140dc](https://github.com/aklinker1/wxt/commit/ef140dc))
|
||||
- Validate templates using tarball to avoid version conflicts within the `wxt/node_modules` directory ([edfa075](https://github.com/aklinker1/wxt/commit/edfa075))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron <aaronklinker1@gmail.com>
|
||||
- Aaron Klinker <aaronklinker1@gmail.com>
|
||||
|
||||
## v0.3.1
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.3.0...v0.3.1)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- CSS entrypoints ([#61](https://github.com/aklinker1/wxt/pull/61))
|
||||
- `init` command for bootstrapping new projects ([#65](https://github.com/aklinker1/wxt/pull/65))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Add zip command to installation scripts ([94a1097](https://github.com/aklinker1/wxt/commit/94a1097))
|
||||
- Add output paths to entrypoint docs ([3a336eb](https://github.com/aklinker1/wxt/commit/3a336eb))
|
||||
- Update installation docs ([aea866c](https://github.com/aklinker1/wxt/commit/aea866c))
|
||||
- Add publishing docs ([4184b05](https://github.com/aklinker1/wxt/commit/4184b05))
|
||||
- Add a section for extensions using WXT ([709b61a](https://github.com/aklinker1/wxt/commit/709b61a))
|
||||
- Add a comparison page to compare and contrast against Plasmo ([38d4f9c](https://github.com/aklinker1/wxt/commit/38d4f9c))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Update template projects to v0.3 ([#56](https://github.com/aklinker1/wxt/pull/56))
|
||||
- Branding and logo ([#60](https://github.com/aklinker1/wxt/pull/60))
|
||||
- Simplify binary setup ([#62](https://github.com/aklinker1/wxt/pull/62))
|
||||
- Add Solid template ([#63](https://github.com/aklinker1/wxt/pull/63))
|
||||
- Increate E2E test timeout to fix flakey test ([dfe424f](https://github.com/aklinker1/wxt/commit/dfe424f))
|
||||
|
||||
### 🤖 CI
|
||||
|
||||
- Speed up demo validation ([3a9fd39](https://github.com/aklinker1/wxt/commit/3a9fd39))
|
||||
- Fix flakey failure when validating templates ([25677ba](https://github.com/aklinker1/wxt/commit/25677ba))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
- BeanWei ([@BeanWei](https://github.com/BeanWei))
|
||||
|
||||
## v0.3.0
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.2.5...v0.3.0)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- ⚠️ Add type safety to `browser.runtime.getURL` ([58a84ec](https://github.com/aklinker1/wxt/commit/58a84ec))
|
||||
- ⚠️ Change default `publicDir` to `<rootDir>/public` ([19c0948](https://github.com/aklinker1/wxt/commit/19c0948))
|
||||
- Windows support ([#50](https://github.com/aklinker1/wxt/pull/50))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Add `WebWorker` lib to generated tsconfig ([2c70246](https://github.com/aklinker1/wxt/commit/2c70246))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Update entrypoint directory links ([0aebb67](https://github.com/aklinker1/wxt/commit/0aebb67))
|
||||
|
||||
### 🌊 Types
|
||||
|
||||
- Allow any string for the `__BROWSER__` global ([6092235](https://github.com/aklinker1/wxt/commit/6092235))
|
||||
|
||||
### 🤖 CI
|
||||
|
||||
- Improve checks against `demo/` extension ([9cc464f](https://github.com/aklinker1/wxt/commit/9cc464f))
|
||||
|
||||
#### ⚠️ Breaking Changes
|
||||
|
||||
- ⚠️ Add type safety to `browser.runtime.getURL` ([58a84ec](https://github.com/aklinker1/wxt/commit/58a84ec))
|
||||
- ⚠️ Change default `publicDir` to `<rootDir>/public` ([19c0948](https://github.com/aklinker1/wxt/commit/19c0948))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.2.5
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.2.4...v0.2.5)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Auto-import from subdirectories ([547fee0](https://github.com/aklinker1/wxt/commit/547fee0))
|
||||
- Include background script in dev mode if user doesn't define one ([ca20a21](https://github.com/aklinker1/wxt/commit/ca20a21))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Don't crash when generating types in dev mode ([d8c1903](https://github.com/aklinker1/wxt/commit/d8c1903))
|
||||
- Properly load entrypoints that reference `import.meta` ([54b18cc](https://github.com/aklinker1/wxt/commit/54b18cc))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Update templates to wxt@0.2 ([9d00eb2](https://github.com/aklinker1/wxt/commit/9d00eb2))
|
||||
|
||||
### 🤖 CI
|
||||
|
||||
- Validate project templates ([9ac756f](https://github.com/aklinker1/wxt/commit/9ac756f))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.2.4
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.2.3...v0.2.4)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add `wxt zip` command ([#47](https://github.com/aklinker1/wxt/pull/47))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.2.3
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.2.2...v0.2.3)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Correctly lookup open port ([#45](https://github.com/aklinker1/wxt/pull/45))
|
||||
- Read boolean maniest options from meta tags correctly ([495c5c8](https://github.com/aklinker1/wxt/commit/495c5c8))
|
||||
- Some fields cannot be overridden from `config.manifest` ([#46](https://github.com/aklinker1/wxt/pull/46))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.2.2
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.2.1...v0.2.2)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Register content scripts correctly in dev mode ([2fb5a54](https://github.com/aklinker1/wxt/commit/2fb5a54))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.2.1
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.2.0...v0.2.1)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Support all content script options ([6f5bf89](https://github.com/aklinker1/wxt/commit/6f5bf89))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Remove HMR log ([90fa6bf](https://github.com/aklinker1/wxt/commit/90fa6bf))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.2.0
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.1.6...v0.2.0)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- ⚠️ Rename `defineBackgroundScript` to `defineBackground` ([5b48ae9](https://github.com/aklinker1/wxt/commit/5b48ae9))
|
||||
- Recongize unnamed content scripts (`content.ts` and `content/index.ts`) ([3db5cec](https://github.com/aklinker1/wxt/commit/3db5cec))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Update templates ([f28a29e](https://github.com/aklinker1/wxt/commit/f28a29e))
|
||||
- Add docs for each type of entrypoint ([77cbfc1](https://github.com/aklinker1/wxt/commit/77cbfc1))
|
||||
- Add inline JSDoc for public types ([375a2a6](https://github.com/aklinker1/wxt/commit/375a2a6))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Run `wxt prepare` on `postinstall` ([c1ea9ba](https://github.com/aklinker1/wxt/commit/c1ea9ba))
|
||||
- Don't format lockfile ([5c7e041](https://github.com/aklinker1/wxt/commit/5c7e041))
|
||||
|
||||
#### ⚠️ Breaking Changes
|
||||
|
||||
- ⚠️ Rename `defineBackgroundScript` to `defineBackground` ([5b48ae9](https://github.com/aklinker1/wxt/commit/5b48ae9))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.1.6
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.1.5...v0.1.6)
|
||||
@@ -16,8 +229,7 @@
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker
|
||||
- Aaron
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.1.5
|
||||
|
||||
@@ -29,7 +241,7 @@
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.1.4
|
||||
|
||||
@@ -50,7 +262,7 @@
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.1.3
|
||||
|
||||
@@ -75,8 +287,7 @@
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron
|
||||
- Aaron Klinker
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.1.2
|
||||
|
||||
@@ -98,7 +309,7 @@
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.1.1
|
||||
|
||||
@@ -111,7 +322,7 @@
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.1.0
|
||||
|
||||
@@ -138,7 +349,7 @@ Initial release of WXT. Full support for production builds and initial toolkit f
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.0.2
|
||||
|
||||
@@ -172,7 +383,7 @@ Initial release of WXT. Full support for production builds and initial toolkit f
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.0.1
|
||||
|
||||
@@ -246,4 +457,4 @@ Initial release of WXT. Full support for production builds and initial toolkit f
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<h1 align="center">WXT</h1>
|
||||
|
||||
<p align="center"><img align="center" width="44" src="./docs/public/hero-logo.svg" alt="WXT Logo"></p>
|
||||
|
||||
<p align="center"><i>Next gen framework for developing web extensions.<br/>Powered by <a href="https://vitejs.dev/" target="_blank">Vite</a>. Inspired by <a href="https://nuxt.com/" target="_blank">Nuxt</a>.</i></p>
|
||||
|
||||

|
||||
@@ -8,16 +10,16 @@
|
||||
|
||||
- 🌐 Supports all browsers
|
||||
- ✅ Supports both MV2 and MV3
|
||||
- ⚡ Dev mode with HMR & auto-reload
|
||||
- ⚡ Dev mode with HMR & fast reload
|
||||
- 📂 File based entrypoints
|
||||
- 🚔 TypeScript
|
||||
- 🦾 Auto-imports
|
||||
- ⬇️ Download and bundle remote URL imports
|
||||
- 🎨 Frontend framework agnostic: works with Vue, React, Svelte, etc
|
||||
- 🖍️ Quickly bootstrap a new project
|
||||
|
||||
### Todo
|
||||
|
||||
- 🖍️ Quickly bootstrap a new project
|
||||
- 📏 Bundle analysis
|
||||
- 🤖 Automated publishing
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env node
|
||||
require('../dist/cli.cjs');
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "Demo",
|
||||
"name": "WXT Demo",
|
||||
"version": "1.0.0",
|
||||
"description": "Demo extension for WXT",
|
||||
"type": "module",
|
||||
@@ -11,13 +11,16 @@
|
||||
"build:all:chrome-mv2": "wxt build --mv2",
|
||||
"build:all:firefox-mv3": "wxt build -b firefox --mv3",
|
||||
"build:all:firefox-mv2": "wxt build -b firefox",
|
||||
"prepare": "pnpm -w build && wxt prepare"
|
||||
"zip": "pnpm -w build && wxt zip",
|
||||
"compile": "pnpm -w build && tsc --noEmit",
|
||||
"postinstall": "pnpm -w build && wxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"webextension-polyfill": "^0.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/webextension-polyfill": "^0.10.0",
|
||||
"sass": "^1.64.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 504 B |
|
After Width: | Height: | Size: 936 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
@@ -1,4 +1,4 @@
|
||||
export default defineBackgroundScript(() => {
|
||||
export default defineBackground(() => {
|
||||
console.log(browser.runtime.id);
|
||||
logId();
|
||||
console.log({
|
||||
@@ -7,4 +7,9 @@ export default defineBackgroundScript(() => {
|
||||
firefox: __IS_FIREFOX__,
|
||||
manifestVersion: __MANIFEST_VERSION__,
|
||||
});
|
||||
|
||||
// @ts-expect-error: should only accept entrypoints or public assets
|
||||
browser.runtime.getURL('/');
|
||||
browser.runtime.getURL('/background.js');
|
||||
browser.runtime.getURL('/icon/128.png');
|
||||
});
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
body {
|
||||
background-color: red;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
body {
|
||||
color: blue;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 698 B |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
@@ -0,0 +1,40 @@
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps<{
|
||||
patterns: Array<[intput: string, output: string]>;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<table class="no-vertical-dividers">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Input Pattern</th>
|
||||
<th></th>
|
||||
<th>Output Path</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="pattern of patterns">
|
||||
<td style="white-space: nowrap">
|
||||
<code>entrypoints/{{ pattern[0] }}</code>
|
||||
</td>
|
||||
<td style="padding: 6px; opacity: 50%">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M4 11v2h12l-5.5 5.5l1.42 1.42L19.84 12l-7.92-7.92L10.5 5.5L16 11H4Z"
|
||||
/>
|
||||
</svg>
|
||||
</td>
|
||||
<td style="white-space: nowrap">
|
||||
<code>/{{ pattern[1] }}</code>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
name: string;
|
||||
icon?: string;
|
||||
}>();
|
||||
|
||||
const src = computed(() => {
|
||||
if (props.icon) return props.icon;
|
||||
return `https://raw.githubusercontent.com/PKief/vscode-material-icon-theme/main/icons/${props.name.toLowerCase()}.svg`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<img :src="src" :alt="`${name} Logo`" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
img {
|
||||
display: inline;
|
||||
transform: translateY(5px);
|
||||
margin-right: 8px;
|
||||
width: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,109 @@
|
||||
<script lang="ts" setup>
|
||||
const extensions = [
|
||||
{
|
||||
name: 'GitHub: Better Line Counts',
|
||||
description: 'Remove generated files from GitHub line counts.',
|
||||
icon: 'https://lh3.googleusercontent.com/GcffNyCJaxT2G9dsQCJHhUEMlu_E0vEzph5cLPrQj7UHKat7QyCzGu69Dmp_DDUL8rY-bPMFJceQarS1wcqdwTalTg=s256',
|
||||
link: 'https://chrome.google.com/webstore/detail/github-better-line-counts/ocfdgncpifmegplaglcnglhioflaimkd',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="vp-doc">
|
||||
<div class="container">
|
||||
<h2>Who's Using WXT?</h2>
|
||||
<ul>
|
||||
<li v-for="extension of extensions">
|
||||
<img :src="extension.icon" :alt="`${extension.name} icon`" />
|
||||
<a :href="extension.link" target="_blank">{{ extension.name }}</a>
|
||||
<small>{{ extension.description }}</small>
|
||||
</li>
|
||||
</ul>
|
||||
<p>Open a PR to add your extension to the list!</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.vp-doc {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.vp-doc {
|
||||
padding: 0 48px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.vp-doc {
|
||||
padding: 0 64px;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1152px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
ul {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
align-items: stretch;
|
||||
gap: 16px;
|
||||
list-style: none;
|
||||
margin: 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);
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 0 !important;
|
||||
padding: 12px;
|
||||
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%;
|
||||
}
|
||||
|
||||
p {
|
||||
text-align: center;
|
||||
opacity: 50%;
|
||||
}
|
||||
a {
|
||||
color: var(--vp-c-text-1);
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -1,21 +1,37 @@
|
||||
import { defineConfig } from 'vitepress';
|
||||
import { generateConfigDocs } from './plugins/generate-config-docs';
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
title: 'WXT',
|
||||
vite: {
|
||||
clearScreen: false,
|
||||
plugins: [generateConfigDocs()],
|
||||
},
|
||||
description: 'Next gen framework for developing web extensions',
|
||||
lastUpdated: true,
|
||||
|
||||
themeConfig: {
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
logo: '/logo.svg',
|
||||
editLink: {
|
||||
pattern: 'https://github.com/aklinker1/wxt/edit/main/docs/:path',
|
||||
},
|
||||
search: {
|
||||
provider: 'local',
|
||||
},
|
||||
|
||||
nav: [
|
||||
{ text: 'Get Started', link: '/get-started/installation.md' },
|
||||
{ text: 'Guide', link: '/guide.md' },
|
||||
{ text: 'Guide', link: '/guide/auto-imports.md' },
|
||||
{ text: 'Config', link: '/config.md' },
|
||||
{ text: 'API', link: '/api.md' },
|
||||
],
|
||||
|
||||
sidebar: {
|
||||
'/get-started/': [
|
||||
{
|
||||
text: 'Guide',
|
||||
text: 'Get Started',
|
||||
items: [
|
||||
{ text: 'Introduction', link: '/get-started/introduction.md' },
|
||||
{ text: 'Installation', link: '/get-started/installation.md' },
|
||||
@@ -25,6 +41,36 @@ export default defineConfig({
|
||||
{ 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: 'Manifest.json', link: '/guide/manifest.md' },
|
||||
{ text: 'Extension APIs', link: '/guide/extension-apis.md' },
|
||||
{ text: 'Remote Code', link: '/guide/remote-code.md' },
|
||||
],
|
||||
},
|
||||
{
|
||||
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' },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import { resolve } from 'node:path';
|
||||
import { Project, ts, Type, Node, JSDocableNode } from 'ts-morph';
|
||||
import Ora from 'ora';
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
|
||||
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()`
|
||||
*/
|
||||
const CUSTOM_TYPES = {
|
||||
manifest:
|
||||
'Manifest | Promise<Manifest> | () => Manifest | () => Promise<Manifest>',
|
||||
};
|
||||
|
||||
export function generateConfigDocs() {
|
||||
const generateDocs = () => {
|
||||
const spinner = Ora('Generating /config.md').start();
|
||||
try {
|
||||
const project = new Project({
|
||||
tsConfigFilePath: resolve('tsconfig.json'),
|
||||
});
|
||||
|
||||
// Load file containing "UserConfig"
|
||||
const externalTypesFile = project.addSourceFileAtPath(externalTypesPath);
|
||||
project.resolveSourceFileDependencies();
|
||||
|
||||
const typeChecker = project.getProgram().getTypeChecker();
|
||||
|
||||
const inlineConfigInterface =
|
||||
externalTypesFile.getInterfaceOrThrow('InlineConfig');
|
||||
|
||||
const getDocsFor = (
|
||||
path: string[],
|
||||
node: Node<ts.Node>,
|
||||
depth = 0,
|
||||
): string[] => {
|
||||
if (depth > 3) throw Error('Recursion to deep for ' + path.join('.'));
|
||||
|
||||
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()) {
|
||||
return (
|
||||
type
|
||||
.getProperties()
|
||||
// .sort((l, r) => l.getName().localeCompare(r.getName()))
|
||||
.flatMap((property) => {
|
||||
const childPath = [...path, property.getName()];
|
||||
if (LEAF_PATHS.includes(childPath.join('.'))) return [];
|
||||
|
||||
return getDocsFor(
|
||||
childPath,
|
||||
property.getDeclarations()[0],
|
||||
depth + 1,
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
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 =
|
||||
PREFACE +
|
||||
'\n\n' +
|
||||
readFileSync(configTemplatePath, 'utf-8').replace(
|
||||
'{{ DOCS }}',
|
||||
lines.join('\n'),
|
||||
);
|
||||
|
||||
writeFileSync(configPath, text);
|
||||
spinner.succeed('Generated /config.md');
|
||||
} catch (err) {
|
||||
spinner.fail('Failed to generate /config.md');
|
||||
console.error(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
name: 'docs:generate-config-docs',
|
||||
buildStart() {
|
||||
generateDocs();
|
||||
},
|
||||
configureServer(server: any) {
|
||||
server.watcher.add(externalTypesPath);
|
||||
},
|
||||
handleHotUpdate(ctx: { file: string }) {
|
||||
if ([externalTypesPath, configTemplatePath].includes(ctx.file)) {
|
||||
generateDocs();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/* Colors */
|
||||
:root {
|
||||
--wxt-c-green: #53bc4a;
|
||||
--wxt-c-green-light: #67d45e;
|
||||
--wxt-c-green-lighter: #67d45e;
|
||||
--wxt-c-green-dark: #4fa048;
|
||||
--wxt-c-green-darker: #447e3f;
|
||||
}
|
||||
|
||||
/* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css */
|
||||
|
||||
:root {
|
||||
--vp-c-brand: var(--wxt-c-green);
|
||||
--vp-c-brand-light: var(--wxt-c-green-light);
|
||||
--vp-c-brand-lighter: var(--wxt-c-green-lighter);
|
||||
--vp-c-brand-dark: var(--wxt-c-green-dark);
|
||||
--vp-c-brand-darker: var(--wxt-c-green-darker);
|
||||
|
||||
--vp-button-brand-text: var(--vp-c-black);
|
||||
--vp-button-brand-hover-text: var(--vp-c-black);
|
||||
--vp-button-brand-active-text: var(--vp-c-black);
|
||||
|
||||
--vp-custom-block-tip-border: var(--wxt-c-green-dark);
|
||||
--vp-custom-block-tip-text: var(--wxt-c-green-dark);
|
||||
|
||||
--vp-code-block-bg: #222422;
|
||||
--vp-code-copy-code-bg: #313431;
|
||||
--vp-code-copy-code-hover-bg: #3c403c;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--vp-c-bg: #131413;
|
||||
|
||||
--vp-c-bg-soft: #1a1b1a;
|
||||
--vp-c-bg-soft-up: #1f201f;
|
||||
--vp-c-bg-soft-down: #262926;
|
||||
--vp-c-bg-soft-mute: #242424;
|
||||
|
||||
--vp-c-bg-alt: #171817;
|
||||
|
||||
--vp-c-mute: #313136;
|
||||
--vp-c-mute-light: #3a3a3c;
|
||||
--vp-c-mute-lighter: #505053;
|
||||
--vp-c-mute-dark: #2c2c30;
|
||||
--vp-c-mute-darker: #252529;
|
||||
|
||||
--vp-code-block-bg: #191a19;
|
||||
--vp-code-copy-code-bg: #212321;
|
||||
--vp-code-copy-code-hover-bg: #292d29;
|
||||
}
|
||||
|
||||
.vp-doc .no-vertical-dividers th,
|
||||
.vp-doc .no-vertical-dividers td {
|
||||
border: none;
|
||||
}
|
||||
.vp-doc .no-vertical-dividers tr {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import DefaultTheme from 'vitepress/theme';
|
||||
import Icon from '../components/Icon.vue';
|
||||
import EntrypointPatterns from '../components/EntrypointPatterns.vue';
|
||||
import UsingWxtSection from '../components/UsingWxtSection.vue';
|
||||
import './custom.css';
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
enhanceApp(ctx) {
|
||||
ctx.app.component('Icon', Icon);
|
||||
ctx.app.component('EntrypointPatterns', EntrypointPatterns);
|
||||
ctx.app.component('UsingWxtSection', UsingWxtSection);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,200 @@
|
||||
<!--
|
||||
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`.
|
||||
-->
|
||||
|
||||
# Config Reference
|
||||
|
||||
Discover all the options you can use in your `wxt.config.ts` file.
|
||||
|
||||
## root
|
||||
|
||||
- **Type**: `string`
|
||||
- **Default**: `process.cwd()`
|
||||
|
||||
Your project's root directory containing the `package.json` used to fill out the
|
||||
`manifest.json`.
|
||||
|
||||
## srcDir
|
||||
|
||||
- **Type**: `string`
|
||||
- **Default**: `config.root`
|
||||
|
||||
Directory containing all source code. Set to `"src"` to move all source code to a `src/`
|
||||
directory.
|
||||
|
||||
## publicDir
|
||||
|
||||
- **Type**: `string`
|
||||
- **Default**: `"${config.root}/public"`
|
||||
|
||||
Directory containing files that will be copied to the output directory as-is.
|
||||
|
||||
## entrypointsDir
|
||||
|
||||
- **Type**: `string`
|
||||
- **Default**: `"${config.srcDir}/entrypoints"`
|
||||
|
||||
## configFile
|
||||
|
||||
- **Type**: `string | false`
|
||||
- **Default**: `"wxt.config.ts"`
|
||||
|
||||
Path to `"wxt.config.ts"` file or false to disable config file discovery.
|
||||
|
||||
## storeIds.chrome
|
||||
|
||||
- **Type**: `string`
|
||||
- **Default**: `undefined`
|
||||
|
||||
## storeIds.firefox
|
||||
|
||||
- **Type**: `string`
|
||||
- **Default**: `undefined`
|
||||
|
||||
## storeIds.edge
|
||||
|
||||
- **Type**: `string`
|
||||
- **Default**: `undefined`
|
||||
|
||||
## mode
|
||||
|
||||
- **Type**: `string`
|
||||
- **Default**: `undefined`
|
||||
|
||||
Explicitly set a mode to run in. This will override the default mode for each command, and can
|
||||
be overridden by the command line `--mode` option.
|
||||
|
||||
## browser
|
||||
|
||||
- **Type**: `"chrome" | "firefox" | "safari" | "edge" | "opera"`
|
||||
- **Default**: `"chrome"`
|
||||
|
||||
Explicitly set a browser to build for. This will override the default browser for each command,
|
||||
and can be overridden by the command line `--browser` option.
|
||||
|
||||
## manifestVersion
|
||||
|
||||
- **Type**: `2 | 3`
|
||||
- **Default**: `undefined`
|
||||
|
||||
Explicitly set a manifest version to target. This will override the default manifest version
|
||||
for each command, and can be overridden by the command line `--mv2` or `--mv3` option.
|
||||
|
||||
## manifest
|
||||
|
||||
- **Type**: `Manifest | Promise<Manifest> | () => Manifest | () => Promise<Manifest>`
|
||||
- **Default**: `undefined`
|
||||
|
||||
Customize the `manifest.json` output. Can be an object, promise, or function that returns an
|
||||
object or promise.
|
||||
|
||||
## runner.openConsole
|
||||
|
||||
- **Type**: `boolean`
|
||||
- **Default**: `undefined`
|
||||
|
||||
## runner.openDevtools
|
||||
|
||||
- **Type**: `boolean`
|
||||
- **Default**: `undefined`
|
||||
|
||||
## runner.binaries.chrome
|
||||
|
||||
- **Type**: `string`
|
||||
- **Default**: `undefined`
|
||||
|
||||
## runner.binaries.edge
|
||||
|
||||
- **Type**: `string`
|
||||
- **Default**: `undefined`
|
||||
|
||||
## runner.binaries.opera
|
||||
|
||||
- **Type**: `string`
|
||||
- **Default**: `undefined`
|
||||
|
||||
## runner.binaries.firefox
|
||||
|
||||
- **Type**: `string`
|
||||
- **Default**: `undefined`
|
||||
|
||||
## runner.firefoxProfile
|
||||
|
||||
- **Type**: `string`
|
||||
- **Default**: `undefined`
|
||||
|
||||
## runner.chromiumProfile
|
||||
|
||||
- **Type**: `string`
|
||||
- **Default**: `undefined`
|
||||
|
||||
## runner.firefoxArgs
|
||||
|
||||
- **Type**: `string[]`
|
||||
- **Default**: `undefined`
|
||||
|
||||
## runner.chromiumArgs
|
||||
|
||||
- **Type**: `string[]`
|
||||
- **Default**: `undefined`
|
||||
|
||||
## runner.startUrls
|
||||
|
||||
- **Type**: `string[]`
|
||||
- **Default**: `undefined`
|
||||
|
||||
## zip.artifactTemplate
|
||||
|
||||
- **Type**: `string`
|
||||
- **Default**: `"{name}-{version}-{browser}.zip"`
|
||||
|
||||
Configure the filename output when zipping files.
|
||||
|
||||
Available template variables:
|
||||
|
||||
- `{name}` - The project's name converted to kebab-case
|
||||
- `{version}` - The version_name or version from the manifest
|
||||
- `{browser}` - The target browser from the `--browser` CLI flag
|
||||
- `{manifestVersion}` - Either "2" or "3"
|
||||
|
||||
## zip.sourcesTemplate
|
||||
|
||||
- **Type**: `string`
|
||||
- **Default**: `"{name}-{version}-sources.zip"`
|
||||
|
||||
Configure the filename output when zipping files.
|
||||
|
||||
Available template variables:
|
||||
|
||||
- `{name}` - The project's name converted to kebab-case
|
||||
- `{version}` - The version_name or version from the manifest
|
||||
- `{browser}` - The target browser from the `--browser` CLI flag
|
||||
- `{manifestVersion}` - Either "2" or "3"
|
||||
|
||||
## zip.name
|
||||
|
||||
- **Type**: `string`
|
||||
- **Default**: `undefined`
|
||||
|
||||
Override the artifactTemplate's `{name}` template variable. Defaults to the `package.json`'s
|
||||
name, or if that doesn't exist, the current working directories name.
|
||||
|
||||
## zip.sourcesRoot
|
||||
|
||||
- **Type**: `string`
|
||||
- **Default**: `config.root`
|
||||
|
||||
Root directory to ZIP when generating the sources ZIP.
|
||||
|
||||
## zip.ignoredSources
|
||||
|
||||
- **Type**: `string[]`
|
||||
- **Default**: `undefined`
|
||||
|
||||
[Minimatch](https://www.npmjs.com/package/minimatch) patterns of files to exclude when
|
||||
creating a ZIP of all your source code for Firfox. Patterns are relative to your
|
||||
`config.zip.sourcesRoot`.
|
||||
|
||||
Hidden files, node_modules, and tests are ignored by default.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Config Reference
|
||||
|
||||
Discover all the options you can use in your `wxt.config.ts` file.
|
||||
|
||||
{{ DOCS }}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
WXT has two directories for storing assets like CSS, images, or fonts.
|
||||
|
||||
- `<srcDir>/public`: Store files that will be copied into the output directory as-is
|
||||
- `<rootDir>/public`: Store files that will be copied into the output directory as-is
|
||||
- `<srcDir>/assets`: Store files that will be processed by Vite during the build process
|
||||
|
||||
## `/public` Directory
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
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 to separate output directories:
|
||||
Separate build targets are written to their own output directories:
|
||||
|
||||
```
|
||||
<root>
|
||||
<rootDir>
|
||||
└─ .output
|
||||
├─ chrome-mv3
|
||||
├─ firefox-mv2
|
||||
@@ -24,7 +24,7 @@ wxt build --browser firefox
|
||||
|
||||
```
|
||||
|
||||
By default, it will build for `chrome`. When excluding the [`--mv2` or `--mv3` flags](#target-manifest-version), it will default to the commonly accepted version used with that browser.
|
||||
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 |
|
||||
| ---------------- | :----------------------: |
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# Compare
|
||||
|
||||
Lets compare the features of WXT vs [Plasmo](https://docs.plasmo.com/framework), another web extension framework.
|
||||
|
||||
## Overview
|
||||
|
||||
| Features | WXT | Plasmo |
|
||||
| ---------------------------------------------------- | :-------------------------: | :--------------------------------------: |
|
||||
| Supports all browsers | ✅ | ✅ |
|
||||
| MV2 Support | ✅ | ✅ |
|
||||
| MV3 Support | ✅ | ✅ |
|
||||
| Create Extension ZIPs | ✅ | ✅ |
|
||||
| Create Firefox Sources ZIP | ✅ | ❌ |
|
||||
| First-class TypeScript support | ✅ | ✅ |
|
||||
| File based entrypoint discovery | ✅ | ✅ |
|
||||
| Inline entrypoint config | ✅ | ✅ |
|
||||
| Auto-imports | ✅ | ❌ |
|
||||
| Supports all frontend frameworks | ✅ | 🟡 Only React, Vue, and Svelte |
|
||||
| Framework specific entrypoints (like `Popup.tsx`) | 🟡 `.html` `.ts` `.tsx` | ✅ `.html` `.ts` `.tsx` `.vue` `.svelte` |
|
||||
| Automated publishing | 🟡 Coming soon | ✅ |
|
||||
| Remote Code Bundling (Google Analytics) | ✅ | ✅ |
|
||||
| <strong style="opacity: 50%">Dev Mode</strong> | | |
|
||||
| `.env` Files | ✅ | ✅ |
|
||||
| Opens browser and install extension | ✅ | ❌ |
|
||||
| HMR for UIs | ✅ | 🟡 React only |
|
||||
| Reload HTML Files on Change | ✅ | 🟡 Reloads entire extension |
|
||||
| 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 | ❌ | ✅ |
|
||||
|
||||
## Dev Mode
|
||||
|
||||
WXT's main goal is improving the development experience (DX) of creating web extensions. There are two things WXT does differently:
|
||||
|
||||
1. Automatically opens a browser with the extension installed when starting development
|
||||
2. Reload each part of the extension individually rather than reloading the entire extension
|
||||
|
||||
Opening a browser automatically makes it super easy to start and stop development without having to manually load the extension in your browser.
|
||||
|
||||
Reloading each part of the extension individually improves your iteration speed while developing UIs. This is because reloading the entire extension on every change will close the popup and any tabs open to an extension page, like options. If you save a file associated with a UI and a content script while working on the UI, it will randomly close because it needed to reload the extension when the content script changed. This interupts your development flow and is really annoying.
|
||||
|
||||
WXT solves this problem by reloading HTML pages and content scripts individually (when possible) to keep your UIs open while you develop them. This is a MV3 feature, so if you're developing a MV2 extension, you'll get the same dev experience as Plasmo.
|
||||
|
||||
:::info
|
||||
Unfortunately, there isn't an API for reloading the background page/service worker individually, so if you change a file used by the background, the entire extension will reload. See [Issue #53](https://github.com/aklinker1/wxt/issues/53) for more details.
|
||||
:::
|
||||
@@ -1,8 +1,6 @@
|
||||
# Configuration
|
||||
|
||||
WXT's behavior can be configured via the `wxt.config.ts` file. In this file, you can add Vite plugins, change the directory strucutre of your project, and provide permissions or other fields to the `<outdir>/manifest.json`.
|
||||
|
||||
However, since WXT is an opinionated framework, some things cannot be configured.
|
||||
WXT's behavior can be configured via the `wxt.config.ts` file. In this file, you can add Vite plugins, change the directory strucutre of your project, and set fields on your `manifest.json`.
|
||||
|
||||
## Config File
|
||||
|
||||
@@ -17,7 +15,7 @@ export default defineConfig({
|
||||
```
|
||||
|
||||
:::info
|
||||
See the [API reference](/api.md) for a full list of options.
|
||||
See the [Config reference](/config.md) for a full list of options.
|
||||
:::
|
||||
|
||||
## Directory Config
|
||||
@@ -25,13 +23,24 @@ See the [API reference](/api.md) for a full list of options.
|
||||
WXT allows you to edit several directories to your liking:
|
||||
|
||||
- `root` (default: `process.cwd()`) - Root of the WXT project
|
||||
- `srcDir` (default: `<root>`) - Location of all your source code
|
||||
- `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: `<rootDir>/public`) - Folder containing [public assets](/get-started/assets.md)
|
||||
|
||||
### Example
|
||||
|
||||
If you want a `src/` directory to contain all your source code, and you want to rename `entrypoints/` to `entries/`, your config would look like this:
|
||||
You want a `src/` directory to contain all your source code, and you want to rename `entrypoints/` → `entries/`:
|
||||
|
||||
```
|
||||
<rootDir>
|
||||
├─ src/
|
||||
│ └─ entries/
|
||||
│ ├─ background.ts
|
||||
│ └─ ...
|
||||
└─ wxt.config.ts
|
||||
```
|
||||
|
||||
Your config would look like this:
|
||||
|
||||
```ts
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Defining Entrypoints
|
||||
|
||||
Entrypoints are any HTML, JS, or CSS file that needs to be bundled and included with the extension.
|
||||
An "entrypoint" is any HTML, JS, or CSS file that needs to be bundled and included with the extension.
|
||||
|
||||
They may or may not be listed in the extension's `manifest.json`.
|
||||
Entrypoints may or may not be listed in the extension's `manifest.json`.
|
||||
|
||||
## `/entrypoints` Directory
|
||||
|
||||
@@ -11,7 +11,7 @@ In WXT, entrypoints are defined by adding a file to the `entrypoints/` directory
|
||||
For example, a project that looks like this:
|
||||
|
||||
```
|
||||
<root>
|
||||
<rootDir>
|
||||
├─ entrypoints/
|
||||
│ ├─ background.ts
|
||||
│ ├─ content.ts
|
||||
@@ -46,15 +46,15 @@ would result in the following `manifest.json`:
|
||||
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.*.js`
|
||||
- `content.ts` → `content_scripts.0.js.0`
|
||||
- `background.ts` → `background.service_worker`
|
||||
|
||||
But not all entrypoints are added to the `manifest.json`. If they have a name that is not recognized by WXT, they are still built and included in the extension, but they are unlisted and do not show up in the manifest.
|
||||
But not all entrypoints are added to the `manifest.json`. If they have a name that 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 bundled to `<outdir>/injected.js` and is accessible via `browser.runtime.getURL("/injected.js")`.
|
||||
|
||||
:::info
|
||||
See [`/entrypoints` folder](/get-started/entrypoints.md) documentation for a full list of recognized entrypoint filenames.
|
||||
See [`/entrypoints` folder](/guide/background.md) documentation for a full list of recognized entrypoint filenames.
|
||||
:::
|
||||
|
||||
## Entrypoint Options
|
||||
@@ -85,5 +85,5 @@ export default defineContentScript({
|
||||
```
|
||||
|
||||
:::info
|
||||
For a full list of entrypoints and each of their options, see the [`/entrypoints` folder](/get-started/entrypoints.md) documentation.
|
||||
For a full list of entrypoints and each of their options, see the [`/entrypoints` folder](/guide/background.md) documentation.
|
||||
:::
|
||||
|
||||
@@ -2,17 +2,14 @@
|
||||
|
||||
Bootstrap a new project or start from scratch.
|
||||
|
||||
## Bootstrap Project
|
||||
|
||||
:::warning 🚧 The `wxt init` command is not implemented yet.
|
||||
|
||||
See [From Scratch](#from-scratch) or reference one of the templates on GitHub:
|
||||
|
||||
- [Vue](https://github.com/aklinker1/wxt/tree/main/templates/vue)
|
||||
- [React](https://github.com/aklinker1/wxt/tree/main/templates/react)
|
||||
:::warning 🚧 WSL Support
|
||||
**_WXT does not support [Windows Subsystem for Linux](https://learn.microsoft.com/en-us/windows/wsl/) yet_**. See [Issue #55](https://github.com/aklinker1/wxt/issues/55) to track progress.
|
||||
|
||||
In the meantime, you can use `cmd` instead.
|
||||
:::
|
||||
|
||||
## Bootstrap Project
|
||||
|
||||
:::code-group
|
||||
|
||||
```sh [pnpm]
|
||||
@@ -25,6 +22,18 @@ npx wxt@latest init <project-name>
|
||||
|
||||
:::
|
||||
|
||||
There are several starting templates available.
|
||||
|
||||
| TypeScript |
|
||||
| --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| <Icon name="TypeScript" /> [`vanilla`](https://github.com/aklinker1/wxt/tree/main/templates/vanilla) |
|
||||
| <Icon name="Vue" /> [`vue`](https://github.com/aklinker1/wxt/tree/main/templates/vue) |
|
||||
| <Icon name="React" /> [`react`](https://github.com/aklinker1/wxt/tree/main/templates/react) |
|
||||
| <Icon name="Svelte" /> [`svelte`](https://github.com/aklinker1/wxt/tree/main/templates/svelte) |
|
||||
| <Icon name="Solid" icon="https://www.solidjs.com/img/favicons/favicon-32x32.png" /> [`solid`](https://github.com/aklinker1/wxt/tree/main/templates/solid) |
|
||||
|
||||
> All templates are in TypeScript. WXT does not support JS at this time.
|
||||
|
||||
## From Scratch
|
||||
|
||||
Create a new NPM project:
|
||||
@@ -32,19 +41,22 @@ Create a new NPM project:
|
||||
:::code-group
|
||||
|
||||
```sh [pnpm]
|
||||
pnpm init <project-name>
|
||||
cd <project-name>
|
||||
mkdir project-name
|
||||
cd project-name
|
||||
pnpm init
|
||||
echo 'shamefully-hoist=true' >> .npmrc
|
||||
```
|
||||
|
||||
```sh [npm]
|
||||
npm init <project-name>
|
||||
cd <project-name>
|
||||
mkdir project-name
|
||||
cd project-name
|
||||
npm init
|
||||
```
|
||||
|
||||
```sh [yarn]
|
||||
yarn init <project-name>
|
||||
cd <project-name>
|
||||
mkdir project-name
|
||||
cd project-name
|
||||
yarn init
|
||||
```
|
||||
|
||||
:::
|
||||
@@ -54,15 +66,15 @@ Then install `wxt`:
|
||||
:::code-group
|
||||
|
||||
```sh [pnpm]
|
||||
pnpm add wxt
|
||||
pnpm add -D wxt
|
||||
```
|
||||
|
||||
```sh [npm]
|
||||
npm i --save wxt
|
||||
npm i --save-dev wxt
|
||||
```
|
||||
|
||||
```sh [yarn]
|
||||
yarn add wxt
|
||||
yarn add --dev wxt
|
||||
```
|
||||
|
||||
:::
|
||||
@@ -71,7 +83,7 @@ Add your first entrypoint:
|
||||
|
||||
```ts
|
||||
// entrypoints/background.ts
|
||||
export default defineBackgroundScript(() => {
|
||||
export default defineBackground(() => {
|
||||
console.log(`Hello from ${browser.runtime.id}!`);
|
||||
});
|
||||
```
|
||||
@@ -84,13 +96,14 @@ Finally, add scripts to your `package.json`:
|
||||
"dev": "wxt", // [!code ++]
|
||||
"dev:firefox": "wxt --browser firefox", // [!code ++]
|
||||
"build": "wxt build", // [!code ++]
|
||||
"build:firefox": "wxt build --browser firefox" // [!code ++]
|
||||
"build:firefox": "wxt build --browser firefox", // [!code ++]
|
||||
"zip": "wxt zip", // [!code ++]
|
||||
"zip:firefox": "wxt zip --browser firefox", // [!code ++]
|
||||
"postinstall": "wxt prepare" // [!code ++]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> You can skip `*:firefox` scripts if you don't want to support Firefox
|
||||
|
||||
## Development
|
||||
|
||||
Once you've installed WXT, you can start the development server using the `dev` script.
|
||||
@@ -104,9 +117,20 @@ pnpm dev
|
||||
The dev command will build the extension for development, open the browser, and reload the different parts of the extension when you save changes.
|
||||
:::
|
||||
|
||||
:::details Development Manifest
|
||||
When running the dev command, WXT will make several changes to your `manifest.json` to improve your development experience:
|
||||
|
||||
- If missing, add a background script/service worker to enable fast reloads
|
||||
- Add serveral `permissions` and `host_permissions` to enable HMR and fast reloads
|
||||
- Modify the CSP to allow connections with the dev server
|
||||
- Remove `content_scripts` and register them at runtime so they can be easily reloaded when you save a file
|
||||
|
||||
If you're an experienced web extension developer and think the dev manifest looks wrong, this is why. Run a production build with `wxt build` to see the unmodified `manifest.json`.
|
||||
:::
|
||||
|
||||
## Next Steps
|
||||
|
||||
You're ready to build a out your web extension!
|
||||
You're ready to build your web extension!
|
||||
|
||||
- Learn how to [add entrypoints](./entrypoints.md) like the popup, background, or content scripts
|
||||
- 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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
WXT is a free and open source framework for building web extensions in an conventional, intuative, and safe way **_for all browsers_**.
|
||||
|
||||
WXT comes with full TypeScript support and auto-imports. Sounds familiar? That's right, **_WXT was based off of Nuxt_** and aims to provide the same greate DX and features.
|
||||
WXT is based of [Nuxt](https://nuxt.com), and aims to provide the same great DX with TypeScript, auto-imports, and an opinionated project structure.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -1,5 +1,178 @@
|
||||
# Publishing
|
||||
|
||||
:::warning 🚧 Not implemented yet!
|
||||
For now, manually zip the output directory and upload to stores by hand.
|
||||
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.
|
||||
|
||||
Each store requires that a ZIP file be uploaded. You can generate these using the `wxt zip` command:
|
||||
|
||||
```sh
|
||||
wxt zip
|
||||
wxt zip -b firefox
|
||||
# etc
|
||||
```
|
||||
|
||||
Generated ZIP files are stored in the `.output` directory.
|
||||
|
||||
## Setup Automated Submissions
|
||||
|
||||
To automate submissions, use the [`publish-browser-extension`](https://www.npmjs.com/package/publish-browser-extension) package.
|
||||
|
||||
:::info
|
||||
🚧 WXT plans to eventually incorporate the `publish-browser-extension` package into its own `wxt submit` command.
|
||||
:::
|
||||
|
||||
1. Install the necessary dependencies:
|
||||
|
||||
```sh
|
||||
pnpm add -D publish-browser-extension env-cmd
|
||||
```
|
||||
|
||||
2. Add scripts to your `package.json` file:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"submit": "env-cmd -f .env.submit -- publish-extension",
|
||||
"submit:dry": "env-cmd -f .env.submit -- publish-extension --dry-run"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. Create a `.env.submit` file and include the code below. If you're not publishing to certain stores, simply ignore their respective variables.
|
||||
|
||||
```txt
|
||||
CHROME_EXTENSION_ID=""
|
||||
CHROME_CLIENT_ID=""
|
||||
CHROME_CLIENT_SECRET=""
|
||||
CHROME_REFRESH_TOKEN=""
|
||||
|
||||
FIREFOX_EXTENSION_ID=""
|
||||
FIREFOX_JWT_ISSUER=""
|
||||
FIREFOX_JWT_SECRET=""
|
||||
|
||||
EDGE_PRODUCT_ID=""
|
||||
EDGE_CLIENT_ID=""
|
||||
EDGE_CLIENT_SECRET=""
|
||||
EDGE_ACCESS_TOKEN_URL=""
|
||||
```
|
||||
|
||||
> Each value will be filled in during the next step.
|
||||
|
||||
4. Run `npx publish-extension --help` for assistance with filling out all the values. Insert the obtained values within the double quotes.
|
||||
|
||||
5. ZIP all the targets you plan to publish, in this case Chrome and Firefox.
|
||||
|
||||
```sh
|
||||
wxt zip
|
||||
wxt zip -b firefox
|
||||
```
|
||||
|
||||
6. Test your credentials by running the `submit:dry` command:
|
||||
|
||||
```sh
|
||||
pnpm submit:dry \
|
||||
--chrome-zip .output/your-extension-X.Y.Z-chrome.zip \
|
||||
--firefox-zip .output/your-extension-X.Y.Z-firefox.zip \
|
||||
--firefox-sources-zip .output/your-extension-X.Y.Z-sources.zip \
|
||||
--edge-zip .output/your-extension-X.Y.Z-chrome.zip
|
||||
```
|
||||
|
||||
7. Upload and submit your extension for review:
|
||||
|
||||
```sh
|
||||
pnpm submit \
|
||||
--chrome-zip .output/your-extension-X.Y.Z-chrome.zip \
|
||||
--firefox-zip .output/your-extension-X.Y.Z-firefox.zip \
|
||||
--firefox-sources-zip .output/your-extension-X.Y.Z-sources.zip \
|
||||
--edge-zip .output/your-extension-X.Y.Z-chrome.zip
|
||||
```
|
||||
|
||||
## GitHub Action
|
||||
|
||||
Here's an example of a GitHub Action to automate submiting new versions of your extension for review. Ensure that you've added all required secrets used in the workflow to the repo's settings.
|
||||
|
||||
```yml
|
||||
# TODO
|
||||
```
|
||||
|
||||
## Chrome Web Store
|
||||
|
||||
✅ Automated • [Developer Dashboard](https://chrome.google.com/webstore/developer/dashboard) • [Publishing Docs](https://developer.chrome.com/docs/webstore/publish/)
|
||||
|
||||
To create a ZIP for Chrome:
|
||||
|
||||
```sh
|
||||
wxt zip
|
||||
```
|
||||
|
||||
## Firefox Addon Store
|
||||
|
||||
✅ Automated • [Developer Dashboard](https://addons.mozilla.org/developers/) • [Publishing Docs](https://extensionworkshop.com/documentation/publish/submitting-an-add-on/)
|
||||
|
||||
Firefox requires you to upload a ZIP of your source code. This allows them to rebuild your extension and review the code in a readable way. More details can be found in [Firefox's docs](https://extensionworkshop.com/documentation/publish/source-code-submission/).
|
||||
|
||||
WXT and `publish-browser-extension` both fully support generating and automatically submitting a source code ZIP.
|
||||
|
||||
When you run `wxt zip -b firefox`, your sources are zipped into the `.output` directory along with your built extension. WXT is configured to exclude certain files such as config files, hidden files, and tests. However, it's important to manually check the ZIP to ensure it only contains the files necessary to rebuild your extension.
|
||||
|
||||
To customize which files are zipped, add the `zip` option to your config file.
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
zip: {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
If it's your first time submitting to the Firefox Addon Store, or if you've updated your project layout, always test your sources ZIP! The commands below should allow you to rebuild your extension from inside the extracted ZIP.
|
||||
|
||||
:::code-group
|
||||
|
||||
```sh [pnpm]
|
||||
pnpm i
|
||||
pnpm zip:firefox
|
||||
```
|
||||
|
||||
```sh [npm]
|
||||
npm i
|
||||
npm run zip:firefox
|
||||
```
|
||||
|
||||
```sh [yarn]
|
||||
yarn
|
||||
yarn zip:firefox
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
Ensure that you have a `README.md` or `SOURCE_CODE_REVIEW.md` file with the above commands so that the Firefox team knows how to build your extension.
|
||||
|
||||
## Safari
|
||||
|
||||
🚧 Not automated at this time
|
||||
|
||||
:::warning
|
||||
🚧 WXT does not currently support automated publishing for Safari. Safari extensions require a native MacOS or iOS app wrapper, which WXT isn't configured to create. For now, if you want to publish to Safari, follow this guide:
|
||||
|
||||
https://developer.apple.com/documentation/safariservices/safari_web_extensions/distributing_your_safari_web_extension
|
||||
|
||||
:::
|
||||
|
||||
## Edge Addons
|
||||
|
||||
✅ Automated • [Developer Dashboard](https://aka.ms/PartnerCenterLogin) • [Publishing Docs](https://learn.microsoft.com/en-us/microsoft-edge/extensions-chromium/publish/publish-extension)
|
||||
|
||||
No need to create a specific ZIP for Edge. If you're already publishing to the Chrome Web Store, you can reuse your Chrome ZIP.
|
||||
|
||||
However, if you have features specifically for Edge, create a separate ZIP with:
|
||||
|
||||
```sh
|
||||
wxt zip -b edge
|
||||
```
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# Guide
|
||||
|
||||
:::warning 🚧 Under construction
|
||||
This documentation does not exist yet.
|
||||
:::
|
||||
@@ -0,0 +1,75 @@
|
||||
# Auto-imports
|
||||
|
||||
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).
|
||||
:::
|
||||
|
||||
## 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`
|
||||
|
||||
And more. All [`wxt/client`](/config.md#wxtclient) APIs can be used without imports.
|
||||
|
||||
## Project Auto-imports
|
||||
|
||||
In addition WXT APIs, default and named exports from inside the following directories can be used without listing them in imports.
|
||||
|
||||
- `<srcDir>/components/*`
|
||||
- `<srcDir>/composables/*`
|
||||
- `<srcDir>/hooks/*`
|
||||
- `<srcDir>/utils/*`
|
||||
|
||||
To add auto-imports from subdirectories, like `utils/api/some-file.ts`, re-export them from the base directory:
|
||||
|
||||
```ts
|
||||
// utils/index.ts
|
||||
export * from './api/some-file.ts';
|
||||
```
|
||||
|
||||
Alternatively, you could add the directory to the list of auto-import directories in your config file.
|
||||
|
||||
## TypeScript
|
||||
|
||||
For TypeScript to work, you need to run the `wxt prepare` command. This will ensure types are generated for auto-imports.
|
||||
|
||||
This should be added to your `postinstall` script:
|
||||
|
||||
```json
|
||||
// package.json
|
||||
{
|
||||
"scripts": {
|
||||
"postinstall": "wxt prepare" // [!code ++]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
You can override the default auto-import behavior in your `wxt.config.ts` file.
|
||||
|
||||
See [`unimport`'s documentation](https://github.com/unjs/unimport#configurations) for a complete list of options.
|
||||
|
||||
```ts
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
imports: {
|
||||
// Add auto-imports for vue fuctions like createApp, ref, computed, watch, toRaw, etc...
|
||||
preset: ['vue'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
To disable auto-imports, set `imports: false`
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
imports: false,
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,42 @@
|
||||
# Background
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/manifest/background/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/background)
|
||||
|
||||
For MV2, the background is added as a script to the background page. For MV3, the background becomes a service worker.
|
||||
|
||||
## Filenames
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['background.ts', 'background.js'],
|
||||
]"
|
||||
/>
|
||||
|
||||
## Definition
|
||||
|
||||
:::warning
|
||||
The main function of the background **_CANNOT BE ASYNC_**. Event listeners must be added syncronously on background startup. If your main function returns a promise, WXT will log an error.
|
||||
:::
|
||||
|
||||
```ts
|
||||
export default defineBackground(() => {
|
||||
// Executed when background is loaded
|
||||
});
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```ts
|
||||
export default defineBackground({
|
||||
// Set manifest options
|
||||
persistent: undefined | true | false,
|
||||
type: undefined | 'module',
|
||||
|
||||
// Executed when background is loaded
|
||||
main() {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
> All manifest options default to `undefined`.
|
||||
@@ -0,0 +1,30 @@
|
||||
# Bookmarks
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/override/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/chrome_url_overrides)
|
||||
|
||||
## Filenames
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['bookmarks.html', 'bookmarks.html'],
|
||||
['bookmarks/index.html', 'bookmarks.html'],
|
||||
]"
|
||||
/>
|
||||
|
||||
## 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>
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
@@ -0,0 +1,81 @@
|
||||
# Content Scripts
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/content_scripts/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_scripts)
|
||||
|
||||
When creating content script entrypoints, they are automatically included in the `manifest.json` along with any CSS files they import.
|
||||
|
||||
## Filenames
|
||||
|
||||
<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'],
|
||||
]"
|
||||
/>
|
||||
|
||||
## Definition
|
||||
|
||||
```ts
|
||||
export default defineContentScript({
|
||||
// Set manifest options
|
||||
matches: ['*://google.com/*', '*://duckduckgo.com/*'],
|
||||
excludeMatches: undefined | [],
|
||||
includeGlobs: undefined | [],
|
||||
excludeGlobs: undefined | [],
|
||||
allFrames: undefined | [],
|
||||
runAt: undefined | 'document_start' | 'document_end' | 'document_idle',
|
||||
matchAboutBlank: undefined | true | false,
|
||||
matchOriginAsFallback: undefined | true | false,
|
||||
world: undefined | 'ISOLATED' | 'MAIN',
|
||||
|
||||
main() {
|
||||
// Executed when content script is loaded
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
> All manifest options default to `undefined`.
|
||||
|
||||
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() {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,44 @@
|
||||
# CSS
|
||||
|
||||
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.
|
||||
|
||||
:::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.
|
||||
:::
|
||||
|
||||
## Filenames
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['<name>.(css|scss|sass|less|styl|stylus)', '<name>.css'],
|
||||
['<name>/index.(css|scss|sass|less|styl|stylus)', '<name>.css'],
|
||||
['content.(css|scss|sass|less|styl|stylus)', 'content-scripts/content.css'],
|
||||
['content/index.(css|scss|sass|less|styl|stylus)', 'content-scripts/content.css'],
|
||||
['<name>.content.(css|scss|sass|less|styl|stylus)', 'content-scripts/<name>.css'],
|
||||
['<name>.content/index.(css|scss|sass|less|styl|stylus)', 'content-scripts/<name>.css'],
|
||||
]"
|
||||
/>
|
||||
|
||||
## Definition
|
||||
|
||||
```css
|
||||
body {
|
||||
/* Plain CSS file */
|
||||
}
|
||||
```
|
||||
|
||||
Follow Vite's guide to setup a preprocessor: https://vitejs.dev/guide/features.html#css-pre-processors
|
||||
|
||||
```sh
|
||||
pnpm i sass
|
||||
```
|
||||
|
||||
```scss
|
||||
body {
|
||||
h1 {
|
||||
/* ...*/
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,30 @@
|
||||
# Devtools
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/devtools/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/devtools_page)
|
||||
|
||||
## Filenames
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['devtools.html', 'devtools.html'],
|
||||
['devtools/index.html', 'devtools.html'],
|
||||
]"
|
||||
/>
|
||||
|
||||
## 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>
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
@@ -0,0 +1,96 @@
|
||||
# Extension APIs
|
||||
|
||||
WXT is built on top of [`webextension-polyfill`](https://www.npmjs.com/package/webextension-polyfill), which uses the standard `browser` global.
|
||||
|
||||
If you're used to Chrome's `chrome` global, its a simple switch:
|
||||
|
||||
1. Replace `chrome` with `browser`
|
||||
2. [Replace callbacks with async/await](https://developer.chrome.com/docs/extensions/mv3/promises/)
|
||||
|
||||
And that's it! Your extension now supports Chrome, Firefox, Safari, Edge, and other Chromium browsers.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
The `browser` variable is available globally via [auto-imports](/guide/auto-imports.md), or it can be imported manually.
|
||||
|
||||
```ts
|
||||
import browser from 'wxt/browser';
|
||||
```
|
||||
|
||||
The `wxt/browser` module exports a customized version of `webextension-polyfill`'s browser with improved typing.
|
||||
|
||||
### Example
|
||||
|
||||
Let's save the date the extension was installed. Just like `chrome`, some APIs require the permission is added to your manifest before the API is defined. Here, we need to add the `storage` permission to your manifest.
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
manifest: {
|
||||
permissions: ['storage'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Then we can use `browser.storage` to save the install date to local storage.
|
||||
|
||||
```ts
|
||||
// background.ts
|
||||
export default defineBackground(() => {
|
||||
browser.runtime.onInstall.addEventListener(({ reason }) => {
|
||||
if (reason === 'install') {
|
||||
browser.storage.local.setItem({ installDate: Date.now() });
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Messaging
|
||||
|
||||
Follow [Chrome's message passing guide](https://developer.chrome.com/docs/extensions/mv3/messaging/) to understand how message passing works in web extensions. In Google's examples, just replace `chrome` with `browser`, and it will work in WXT.
|
||||
|
||||
Here's a basic request/response example:
|
||||
|
||||
```ts
|
||||
// popup/main.ts
|
||||
const res = await browser.runtime.sendMessage('ping');
|
||||
|
||||
console.log('res'); // "pong"
|
||||
```
|
||||
|
||||
```ts
|
||||
// background.ts
|
||||
export default defineBackground(() => {
|
||||
browser.runtime.onMessage.addEventListener(
|
||||
(message, sender, sendResponse) => {
|
||||
console.log(message); // "ping"
|
||||
|
||||
// Wait 1 second and respond with "pong"
|
||||
setTimeout(() => sendResponse('pong'), 1000);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
There are a number of message passing libraries you can use to improve the message passing experience.
|
||||
|
||||
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-bridge`](https://github.com/zikaari/webext-bridge) - "Messaging in Web Extensions made super easy. Out of the box."
|
||||
|
||||
## Browser Differences
|
||||
|
||||
Some APIs are only available on certain browsers or manifest versions. You will have to check if an API exists at runtime if it is not in the `browser` standard.
|
||||
|
||||
```ts
|
||||
if ('session' in browser.storage) {
|
||||
// Do something with the non-standard session storage API
|
||||
}
|
||||
```
|
||||
|
||||
> If you're using TypeScript, knowing what is non-standard is easy! APIs that are not typed are non-standard.
|
||||
@@ -0,0 +1,30 @@
|
||||
# History
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/override/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/chrome_url_overrides)
|
||||
|
||||
## Filenames
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['history.html', 'history.html'],
|
||||
['history/index.html', 'history.html'],
|
||||
]"
|
||||
/>
|
||||
|
||||
## 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>
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
@@ -0,0 +1,120 @@
|
||||
# Manifest.json
|
||||
|
||||
The manifest.json is generated at build-time based on files in your `entrypoints` directory and your `wxt.config.ts`.
|
||||
|
||||
## Confiuration
|
||||
|
||||
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
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
manifest: {
|
||||
host_permissions: ['*://google.com/*'],
|
||||
content_security_policy: {
|
||||
// ...
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## `name`
|
||||
|
||||
If not provided via the `manifest` config, the [manifest's `name`](https://developer.chrome.com/docs/extensions/mv3/manifest/name/) defaults to your package.json's `name` property.
|
||||
|
||||
## `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.
|
||||
|
||||
- `version_name` is the exact string listed in your package.json
|
||||
- `version` is the string cleaned up, with any invalid suffixes removed
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
// package.json
|
||||
{
|
||||
"version": "1.3.0-alpha2"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
// .output/<dir>/manifest.json
|
||||
{
|
||||
"version": "1.3.0",
|
||||
"version_name": "1.3.0-alpha2"
|
||||
}
|
||||
```
|
||||
|
||||
## `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/).
|
||||
|
||||
```
|
||||
public/
|
||||
├─ icon-16.png
|
||||
├─ icon-24.png
|
||||
├─ icon-48.png
|
||||
├─ icon-96.png
|
||||
└─ icon-128.png
|
||||
```
|
||||
|
||||
Icon files need to match the following regex to be automatically included in the manifest. Most design software can output icons in one of these formats
|
||||
|
||||
<<< @/../src/core/utils/manifest.ts#snippet
|
||||
|
||||
If you prefer to use filenames in a different format, you can add the icons manually in your `wxt.config.ts` file:
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
manifest: {
|
||||
icons: {
|
||||
16: '/extension-icon-16.png',
|
||||
24: '/extension-icon-24.png',
|
||||
48: '/extension-icon-48.png',
|
||||
96: '/extension-icon-96.png',
|
||||
128: '/extension-icon-128.png',
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## `permissions`
|
||||
|
||||
[Permissions](https://developer.chrome.com/docs/extensions/reference/permissions/) must be listed in the manifest config.
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
manifest: {
|
||||
permissions: ['storage', 'tabs'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## 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).
|
||||
|
||||
```
|
||||
public/
|
||||
└─ _locales/
|
||||
├─ en/
|
||||
│ └─ messages.json
|
||||
├─ es/
|
||||
│ └─ messages.json
|
||||
└─ ko/
|
||||
└─ messages.json
|
||||
```
|
||||
|
||||
Then you'll need to explicitly override the `name` and `description` properties in your config for them to be localized.
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
manifest: {
|
||||
name: '__MSG_extName__',
|
||||
description: '__MSG_extDescription__',
|
||||
default_locale: 'en',
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,30 @@
|
||||
# Newtab
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/override/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/chrome_url_overrides)
|
||||
|
||||
## Filenames
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['newtab.html', 'newtab.html'],
|
||||
['newtab/index.html', 'newtab.html'],
|
||||
]"
|
||||
/>
|
||||
|
||||
## 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>
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
@@ -0,0 +1,33 @@
|
||||
# Options
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/options/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/options_ui)
|
||||
|
||||
## Filenames
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['options.html', 'options.html'],
|
||||
['options/index.html', 'options.html'],
|
||||
]"
|
||||
/>
|
||||
|
||||
## Definition
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Options Title</title>
|
||||
<meta name="manifest.open_in_tab" content="true|false" />
|
||||
<meta name="manifest.chrome_style" content="true|false" />
|
||||
<meta name="manifest.browser_style" content="true|false" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
> All manifest options default to `undefined` when the `meta` tag is not present.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Popup
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/reference/action/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/action)
|
||||
|
||||
## Filenames
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['popup.html', 'popup.html'],
|
||||
['popup/index.html', 'popup.html'],
|
||||
]"
|
||||
/>
|
||||
|
||||
## Definition
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Default Popup Title</title>
|
||||
<meta
|
||||
name="manifest.default_icon"
|
||||
content="{
|
||||
16: '/icon-16.png',
|
||||
24: '/icon-24.png',
|
||||
...
|
||||
}"
|
||||
/>
|
||||
<meta name="manifest.type" content="page_action|browser_action" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
> All manifest options default to `undefined` when the `meta` tag is not present.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Remote Code
|
||||
|
||||
WXT will automatically download and bundle imports with the `url:` prefix so the extension does not depend of remote code, [a requirement from Google for MV3](https://developer.chrome.com/docs/extensions/migrating/improve-security/#remove-remote-code).
|
||||
|
||||
## Google Analytics
|
||||
|
||||
For example, you can import google analytics:
|
||||
|
||||
```ts
|
||||
// utils/google-analytics.ts
|
||||
import 'url:https://www.googletagmanager.com/gtag/js?id=G-XXXXXX';
|
||||
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
// NOTE: This line is different from Google's documentation
|
||||
window.gtag = function () {
|
||||
dataLayer.push(arguments);
|
||||
};
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-XXXXXX');
|
||||
```
|
||||
|
||||
Then you can import this in your HTML files to enable Google Analytics:
|
||||
|
||||
```ts
|
||||
// popup/main.ts
|
||||
import '~/utils/google-analytics';
|
||||
|
||||
gtag('event', 'event_name', {
|
||||
key: 'value',
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,36 @@
|
||||
# Sandbox
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/manifest/sandbox/)
|
||||
|
||||
:::tip Chromium Only
|
||||
Firefox does not support sandboxed pages.
|
||||
:::
|
||||
|
||||
## Filenames
|
||||
|
||||
<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` '],
|
||||
]"
|
||||
/>
|
||||
|
||||
## 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>
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
@@ -0,0 +1,36 @@
|
||||
# Side Panel
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/reference/sidePanel/)
|
||||
|
||||
:::tip Chromium Only
|
||||
Firefox does not support sidepanel pages.
|
||||
:::
|
||||
|
||||
## 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` '],
|
||||
]"
|
||||
/>
|
||||
|
||||
## 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>
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
@@ -0,0 +1,46 @@
|
||||
# Unlisted Pages
|
||||
|
||||
HTML pages that are built by Vite, but are not included in the manifest.
|
||||
|
||||
### Examples
|
||||
|
||||
- Onboarding
|
||||
- Dashboard
|
||||
- FAQ
|
||||
- Help
|
||||
- Changelog
|
||||
|
||||
## Filenames
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['<name>.html', '<name>.html'],
|
||||
['<name>/index.html', '<name>.html'],
|
||||
]"
|
||||
/>
|
||||
|
||||
Pages are accessible at `'/<name>.html'`:
|
||||
|
||||
```ts
|
||||
const url = browser.runtime.getURL('/<name>.html');
|
||||
|
||||
console.log(url); // "chrome-extension://<id>/<name>.html"
|
||||
```
|
||||
|
||||
## 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>
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
@@ -0,0 +1,20 @@
|
||||
# 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
|
||||
```
|
||||
@@ -5,8 +5,11 @@ titleTemplate: 'Next Generation Web Extension Framework'
|
||||
|
||||
hero:
|
||||
name: WXT
|
||||
text: Next gen framework for web extensions
|
||||
text: Next-gen Web Extension Framework
|
||||
tagline: Powered by Vite, inspired by Nuxt.
|
||||
image:
|
||||
src: /hero-logo.svg
|
||||
alt: WXT
|
||||
actions:
|
||||
- theme: brand
|
||||
text: Get Started
|
||||
@@ -38,12 +41,17 @@ features:
|
||||
title: Bundle Remote Code
|
||||
details: Downloads and bundles remote code imported from URLs.
|
||||
- icon: 🎨
|
||||
title: Frontend framework agnostic
|
||||
title: Frontend Framework Agnostic
|
||||
details: Works with any front-end framework with a Vite plugin.
|
||||
- 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.'
|
||||
- icon: 📏
|
||||
title: Bundle analysis
|
||||
title: Bundle Analysis
|
||||
details: 'TODO: Tools for analyizing the final extension bundle.'
|
||||
---
|
||||
|
||||
<UsingWxtSection />
|
||||
|
||||
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M354.435 418.141C383.217 418.141 406.55 394.808 406.55 366.026V313.91H416.026C444.808 313.91 468.141 290.578 468.141 261.795C468.141 233.013 444.808 209.68 416.026 209.68H406.55V157.565C406.55 128.783 383.217 105.45 354.435 105.45H302.32V95.9745C302.32 67.1921 278.987 43.8594 250.205 43.8594C221.422 43.8594 198.09 67.1921 198.09 95.9745V105.45H145.974C117.192 105.45 93.8594 128.783 93.8594 157.565V209.68H103.335C132.117 209.68 155.45 233.013 155.45 261.795C155.45 290.578 132.117 313.91 103.335 313.91H93.8594V418.141H198.09V408.665C198.09 379.883 221.422 356.55 250.205 356.55C278.987 356.55 302.32 379.883 302.32 408.665V418.141H354.435Z" stroke="#67D55E" stroke-width="20"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 798 B |
@@ -0,0 +1,10 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_305_490)">
|
||||
<path d="M348.608 492C384.905 492 414.329 462.576 414.329 426.279V360.557H426.279C462.576 360.557 492 331.132 492 294.835C492 258.538 462.576 229.114 426.279 229.114H414.329V163.392C414.329 127.095 384.905 97.6709 348.608 97.6709H282.886V85.7215C282.886 49.4245 253.462 20 217.165 20C180.868 20 151.443 49.4245 151.443 85.7215V97.6709H85.7215C49.4245 97.6709 20 127.095 20 163.392V229.114H31.9494C68.2464 229.114 97.6709 258.538 97.6709 294.835C97.6709 331.132 68.2464 360.557 31.9494 360.557H20V492H151.443V480.051C151.443 443.754 180.868 414.329 217.165 414.329C253.462 414.329 282.886 443.754 282.886 480.051V492H348.608Z" stroke="#67D55E" stroke-width="40"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_305_490">
|
||||
<rect width="512" height="512" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 914 B |
|
After Width: | Height: | Size: 487 KiB |
@@ -6,7 +6,7 @@ describe('Auto Imports', () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'entrypoints/background.ts',
|
||||
'export default defineBackgroundScript(() => {})',
|
||||
'export default defineBackground(() => {})',
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/overlay.content.ts',
|
||||
@@ -21,10 +21,17 @@ describe('Auto Imports', () => {
|
||||
".wxt/types/paths.d.ts
|
||||
----------------------------------------
|
||||
// Generated by wxt
|
||||
type EntrypointPath =
|
||||
| \\"/background.js\\"
|
||||
| \\"/content-scripts/overlay.js\\"
|
||||
| \\"/popup.html\\"
|
||||
import \\"wxt/browser\\";
|
||||
|
||||
declare module \\"wxt/browser\\" {
|
||||
type PublicPath =
|
||||
| \\"/background.js\\"
|
||||
| \\"/content-scripts/overlay.js\\"
|
||||
| \\"/popup.html\\"
|
||||
export interface ProjectRuntime extends Runtime.Static {
|
||||
getURL(path: PublicPath): string;
|
||||
}
|
||||
}
|
||||
"
|
||||
`);
|
||||
});
|
||||
@@ -37,18 +44,18 @@ describe('Auto Imports', () => {
|
||||
|
||||
expect(await project.serializeFile('.wxt/types/imports.d.ts'))
|
||||
.toMatchInlineSnapshot(`
|
||||
".wxt/types/imports.d.ts
|
||||
----------------------------------------
|
||||
// Generated by wxt
|
||||
export {}
|
||||
declare global {
|
||||
const browser: typeof import('webextension-polyfill')
|
||||
const defineBackgroundScript: typeof import('wxt/client')['defineBackgroundScript']
|
||||
const defineConfig: typeof import('wxt')['defineConfig']
|
||||
const defineContentScript: typeof import('wxt/client')['defineContentScript']
|
||||
const mountContentScriptUi: typeof import('wxt/client')['mountContentScriptUi']
|
||||
}
|
||||
"
|
||||
`);
|
||||
".wxt/types/imports.d.ts
|
||||
----------------------------------------
|
||||
// Generated by wxt
|
||||
export {}
|
||||
declare global {
|
||||
const browser: typeof import('wxt/browser')['browser']
|
||||
const defineBackground: typeof import('wxt/client')['defineBackground']
|
||||
const defineConfig: typeof import('wxt')['defineConfig']
|
||||
const defineContentScript: typeof import('wxt/client')['defineContentScript']
|
||||
const mountContentScriptUi: typeof import('wxt/client')['mountContentScriptUi']
|
||||
}
|
||||
"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { TestProject } from '../utils';
|
||||
import { execaCommand } from 'execa';
|
||||
import glob from 'fast-glob';
|
||||
|
||||
describe('Init command', () => {
|
||||
it('should download and create a template', async () => {
|
||||
const project = new TestProject();
|
||||
|
||||
await execaCommand(`pnpm -s wxt init ${project.root} -t vue --pm npm`, {
|
||||
env: { ...process.env, CI: 'true' },
|
||||
stdio: 'ignore',
|
||||
});
|
||||
const files = await glob('**/*', {
|
||||
cwd: project.root,
|
||||
onlyFiles: true,
|
||||
dot: true,
|
||||
});
|
||||
|
||||
expect(files.sort()).toMatchInlineSnapshot(`
|
||||
[
|
||||
".gitignore",
|
||||
".vscode/extensions.json",
|
||||
"README.md",
|
||||
"assets/vue.svg",
|
||||
"components/HelloWorld.vue",
|
||||
"entrypoints/background.ts",
|
||||
"entrypoints/popup/App.vue",
|
||||
"entrypoints/popup/index.html",
|
||||
"entrypoints/popup/main.ts",
|
||||
"entrypoints/popup/style.css",
|
||||
"package.json",
|
||||
"public/icon/128.png",
|
||||
"public/icon/16.png",
|
||||
"public/icon/32.png",
|
||||
"public/icon/48.png",
|
||||
"public/icon/96.png",
|
||||
"public/wxt.svg",
|
||||
"tsconfig.json",
|
||||
"wxt.config.ts",
|
||||
]
|
||||
`);
|
||||
}, 30e3);
|
||||
});
|
||||
@@ -57,7 +57,7 @@ describe('Manifest Content', () => {
|
||||
const optionsContent = `
|
||||
<html>
|
||||
<head>
|
||||
<meta name="manifest.open_in_tab" content="true">
|
||||
<meta name="manifest.open_in_tab" content="false">
|
||||
<meta name="manifest.chrome_style" content="true">
|
||||
<meta name="manifest.browser_style" content="true">
|
||||
</head>
|
||||
@@ -72,7 +72,7 @@ describe('Manifest Content', () => {
|
||||
const manifest = await project.getOutputManifest();
|
||||
|
||||
expect(manifest.options_ui).toEqual({
|
||||
open_in_tab: true,
|
||||
open_in_tab: false,
|
||||
chrome_style: true,
|
||||
page: 'options.html',
|
||||
});
|
||||
@@ -88,7 +88,7 @@ describe('Manifest Content', () => {
|
||||
);
|
||||
|
||||
expect(manifest.options_ui).toEqual({
|
||||
open_in_tab: true,
|
||||
open_in_tab: false,
|
||||
browser_style: true,
|
||||
page: 'options.html',
|
||||
});
|
||||
@@ -97,7 +97,7 @@ describe('Manifest Content', () => {
|
||||
|
||||
describe('background', () => {
|
||||
const backgroundContent = `
|
||||
export default defineBackgroundScript({
|
||||
export default defineBackground({
|
||||
persistent: true,
|
||||
type: "module",
|
||||
main: () => {},
|
||||
@@ -133,6 +133,66 @@ describe('Manifest Content', () => {
|
||||
});
|
||||
});
|
||||
|
||||
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(
|
||||
@@ -149,7 +209,7 @@ describe('Manifest Content', () => {
|
||||
`import "./style.css";
|
||||
export default defineContentScript({
|
||||
matches: ["*://google.com/*"],
|
||||
run_at: "document_end",
|
||||
runAt: "document_end",
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
@@ -162,7 +222,7 @@ describe('Manifest Content', () => {
|
||||
`import "./style.css";
|
||||
export default defineContentScript({
|
||||
matches: ["*://google.com/*"],
|
||||
run_at: "document_end",
|
||||
runAt: "document_end",
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
@@ -175,7 +235,7 @@ describe('Manifest Content', () => {
|
||||
`import "./style.css";
|
||||
export default defineContentScript({
|
||||
matches: ["*://duckduckgo.com/*"],
|
||||
run_at: "document_end",
|
||||
runAt: "document_end",
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
|
||||
@@ -13,7 +13,7 @@ describe('Output Directory Structure', () => {
|
||||
expect(await project.serializeOutput()).toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\"}"
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\"}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -66,7 +66,7 @@ describe('Output Directory Structure', () => {
|
||||
================================================================================
|
||||
.output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"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\\",\\"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\\"]}]}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -90,7 +90,7 @@ describe('Output Directory Structure', () => {
|
||||
================================================================================
|
||||
.output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"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\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":[\\"*://*/*\\"],\\"js\\":[\\"content-scripts/overlay-one.js\\"]}]}"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ describe('User Config', () => {
|
||||
});
|
||||
project.addFile(
|
||||
'src/entrypoints/background.ts',
|
||||
`export default defineBackgroundScript(
|
||||
`export default defineBackground(
|
||||
() => console.log('Hello background'),
|
||||
);`,
|
||||
);
|
||||
@@ -27,7 +27,7 @@ describe('User Config', () => {
|
||||
================================================================================
|
||||
.output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"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\\",\\"version_name\\":\\"0.0.0-test\\",\\"background\\":{\\"service_worker\\":\\"background.js\\"}}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -38,7 +38,7 @@ describe('User Config', () => {
|
||||
});
|
||||
project.addFile(
|
||||
'entries/background.ts',
|
||||
`export default defineBackgroundScript(() => console.log('Hello background'));`,
|
||||
`export default defineBackground(() => console.log('Hello background'));`,
|
||||
);
|
||||
|
||||
await project.build();
|
||||
@@ -52,7 +52,7 @@ describe('User Config', () => {
|
||||
================================================================================
|
||||
.output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"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\\",\\"version_name\\":\\"0.0.0-test\\",\\"background\\":{\\"service_worker\\":\\"background.js\\"}}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -80,7 +80,7 @@ describe('User Config', () => {
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"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\\",\\"version_name\\":\\"0.0.0-test\\",\\"example_customization\\":[\\"production\\",\\"chrome\\",\\"3\\",\\"build\\"]}"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,11 +3,12 @@ import fs from 'fs-extra';
|
||||
import glob from 'fast-glob';
|
||||
import { execSync } from 'child_process';
|
||||
import { InlineConfig, UserConfig, build } from '../src';
|
||||
import { normalizePath } from '../src/core/utils/paths';
|
||||
|
||||
export class TestProject {
|
||||
files: Array<[string, string]> = [];
|
||||
config: UserConfig | undefined;
|
||||
private readonly root: string;
|
||||
readonly root: string;
|
||||
|
||||
constructor(root = 'e2e/project') {
|
||||
// We can't put each test's project inside e2e/project directly, otherwise the wxt.config.ts
|
||||
@@ -97,6 +98,7 @@ export class TestProject {
|
||||
private async serializeDir(dir: string): Promise<string> {
|
||||
const outputFiles = await glob('**/*', {
|
||||
cwd: resolve(this.root, dir),
|
||||
ignore: ['**/node_modules', '**/.output'],
|
||||
});
|
||||
outputFiles.sort();
|
||||
const fileContents = [];
|
||||
@@ -113,7 +115,7 @@ export class TestProject {
|
||||
async serializeFile(path: string): Promise<string> {
|
||||
const absolutePath = resolve(this.root, path);
|
||||
return [
|
||||
relative(this.root, absolutePath),
|
||||
normalizePath(relative(this.root, absolutePath)),
|
||||
await fs.readFile(absolutePath),
|
||||
].join(`\n${''.padEnd(40, '-')}\n`);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wxt",
|
||||
"type": "module",
|
||||
"version": "0.1.6",
|
||||
"version": "0.3.2",
|
||||
"description": "Next gen framework for developing web extensions",
|
||||
"engines": {
|
||||
"node": ">=18.16.0",
|
||||
@@ -26,9 +26,10 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"bin",
|
||||
"dist"
|
||||
],
|
||||
"bin": "dist/cli.cjs",
|
||||
"bin": "./bin/wxt.cjs",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -39,9 +40,12 @@
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"./client": {
|
||||
"require": "./dist/client.cjs",
|
||||
"import": "./dist/client.js",
|
||||
"types": "./dist/client.d.ts"
|
||||
},
|
||||
"./browser": {
|
||||
"import": "./dist/browser.js",
|
||||
"types": "./dist/browser.d.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
@@ -59,43 +63,49 @@
|
||||
"docs:preview": "vitepress preview docs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/webextension-polyfill": "^0.10.0",
|
||||
"@types/webextension-polyfill": "^0.10.1",
|
||||
"@webext-core/fake-browser": "^1.2.2",
|
||||
"@webext-core/match-patterns": "^1.0.1",
|
||||
"async-mutex": "^0.4.0",
|
||||
"c12": "^1.4.2",
|
||||
"cac": "^6.7.14",
|
||||
"consola": "^3.1.0",
|
||||
"fast-glob": "^3.2.12",
|
||||
"filesize": "^10.0.7",
|
||||
"consola": "^3.2.3",
|
||||
"fast-glob": "^3.3.1",
|
||||
"filesize": "^10.0.8",
|
||||
"fs-extra": "^11.1.1",
|
||||
"jiti": "^1.18.2",
|
||||
"get-port": "^7.0.0",
|
||||
"giget": "^1.1.2",
|
||||
"jiti": "^1.19.1",
|
||||
"json5": "^2.2.3",
|
||||
"linkedom": "^0.14.26",
|
||||
"minimatch": "^9.0.3",
|
||||
"picocolors": "^1.0.0",
|
||||
"picomatch": "^2.3.1",
|
||||
"unimport": "^3.0.8",
|
||||
"vite": "^4.3.9",
|
||||
"prompts": "^2.4.2",
|
||||
"unimport": "^3.1.0",
|
||||
"vite": "^4.4.7",
|
||||
"vite-tsconfig-paths": "^4.2.0",
|
||||
"web-ext": "^7.6.2",
|
||||
"webextension-polyfill": "^0.10.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.3.1",
|
||||
"@types/picomatch": "^2.3.0",
|
||||
"@vitest/coverage-v8": "^0.32.2",
|
||||
"@types/node": "^20.4.5",
|
||||
"@types/prompts": "^2.4.4",
|
||||
"@vitest/coverage-v8": "^0.32.4",
|
||||
"execa": "^7.2.0",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"ora": "^6.3.1",
|
||||
"prettier": "^2.8.8",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"simple-git-hooks": "^2.8.1",
|
||||
"tsup": "^7.0.0",
|
||||
"simple-git-hooks": "^2.9.0",
|
||||
"ts-morph": "^19.0.0",
|
||||
"tsup": "^7.1.0",
|
||||
"tsx": "^3.12.7",
|
||||
"typescript": "^5.1.3",
|
||||
"typescript": "^5.1.6",
|
||||
"vitepress": "1.0.0-beta.5",
|
||||
"vitest": "^0.32.4",
|
||||
"vitest-mock-extended": "^1.1.4",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import tsup from 'tsup';
|
||||
import glob from 'fast-glob';
|
||||
import pc from 'picocolors';
|
||||
import path from 'path';
|
||||
import { printFileList } from '../src/core/log/printFileList';
|
||||
import { formatDuration } from '../src/core/utils/formatDuration';
|
||||
import ora from 'ora';
|
||||
import fs from 'fs-extra';
|
||||
@@ -24,12 +23,14 @@ await Promise.all([
|
||||
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' },
|
||||
@@ -37,6 +38,15 @@ await Promise.all([
|
||||
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({
|
||||
@@ -46,7 +56,7 @@ await Promise.all([
|
||||
format: ['esm'],
|
||||
sourcemap: true,
|
||||
silent: true,
|
||||
external: [`virtual:user-${entryName}`],
|
||||
external: [`virtual:user-${entryName}`, 'vite'],
|
||||
}),
|
||||
),
|
||||
tsup.build({
|
||||
@@ -56,6 +66,7 @@ await Promise.all([
|
||||
format: ['esm'],
|
||||
sourcemap: true,
|
||||
silent: true,
|
||||
external: ['vite'],
|
||||
}),
|
||||
tsup.build({
|
||||
entry: {
|
||||
@@ -63,6 +74,7 @@ await Promise.all([
|
||||
},
|
||||
format: ['esm', 'cjs'],
|
||||
silent: true,
|
||||
external: ['vite'],
|
||||
}),
|
||||
]).catch((err) => {
|
||||
spinner.fail();
|
||||
@@ -70,21 +82,13 @@ await Promise.all([
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
spinner.succeed();
|
||||
spinner.clear().stop();
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
const outFiles = await glob(`${outDir}/**`);
|
||||
outFiles.forEach((file, i) => {
|
||||
const color = file.endsWith('.map')
|
||||
? pc.dim
|
||||
: file.endsWith('.d.ts')
|
||||
? pc.blue
|
||||
: pc.cyan;
|
||||
const prefix = i === outFiles.length - 1 ? ' └─' : ' ├─';
|
||||
console.log(
|
||||
`${pc.gray(prefix)} ${pc.dim(outDir + path.sep)}${color(
|
||||
path.relative(outDir, file),
|
||||
)}`,
|
||||
);
|
||||
});
|
||||
consola.success(`Finished in ${formatDuration(duration)}`);
|
||||
const outFiles = await glob(`${outDir}/**`, { absolute: true });
|
||||
await printFileList(
|
||||
consola.success,
|
||||
`Built WXT in ${formatDuration(duration)}`,
|
||||
outDir,
|
||||
outFiles,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
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;
|
||||
}
|
||||
@@ -3,3 +3,4 @@ export * from './dev';
|
||||
export * from './init';
|
||||
export * from './prepare';
|
||||
export * from './publish';
|
||||
export * from './zip';
|
||||
|
||||
@@ -1,6 +1,178 @@
|
||||
import { consola } from 'consola';
|
||||
import { defineCommand } from '../utils/defineCommand';
|
||||
import prompts from 'prompts';
|
||||
import ora from 'ora';
|
||||
import { consola } from 'consola';
|
||||
import { downloadTemplate } from 'giget';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'node:path';
|
||||
import pc from 'picocolors';
|
||||
import { Formatter } from 'picocolors/types';
|
||||
|
||||
export const init = defineCommand<[directory?: string]>(async (directory) => {
|
||||
consola.warn('wxt init: Not implemented');
|
||||
});
|
||||
export const init = defineCommand<
|
||||
[directory: string | undefined, options: { template?: string; pm?: string }]
|
||||
>(
|
||||
async (userDirectory, flags) => {
|
||||
consola.info('Initalizing new project');
|
||||
|
||||
const templates = await listTemplates();
|
||||
const defaultTemplate = templates.find(
|
||||
(template) => template.name === flags.template?.toLowerCase().trim(),
|
||||
);
|
||||
|
||||
const input = await prompts(
|
||||
[
|
||||
{
|
||||
name: 'directory',
|
||||
type: () => (userDirectory == null ? 'text' : undefined),
|
||||
message: 'Project Directory',
|
||||
initial: userDirectory,
|
||||
},
|
||||
{
|
||||
name: 'template',
|
||||
type: () => (defaultTemplate == null ? 'select' : undefined),
|
||||
message: 'Choose a template',
|
||||
choices: templates.map((template) => ({
|
||||
title:
|
||||
TEMPLATE_COLORS[template.name]?.(template.name) ?? template.name,
|
||||
value: template,
|
||||
})),
|
||||
},
|
||||
{
|
||||
name: 'packageManager',
|
||||
type: () => (flags.pm == null ? 'select' : undefined),
|
||||
message: 'Package Manager',
|
||||
choices: [
|
||||
{ title: 'npm', value: 'npm' },
|
||||
{ title: 'pnpm', value: 'pnpm' },
|
||||
{ title: 'yarn', value: 'yarn' },
|
||||
],
|
||||
},
|
||||
],
|
||||
{
|
||||
onCancel: () => process.exit(1),
|
||||
},
|
||||
);
|
||||
input.directory ??= userDirectory;
|
||||
input.template ??= defaultTemplate;
|
||||
input.packageManager ??= flags.pm;
|
||||
|
||||
await cloneProject(input);
|
||||
|
||||
const cdPath = path.relative(process.cwd(), path.resolve(input.directory));
|
||||
console.log();
|
||||
consola.log(
|
||||
`✨ WXT project created with the ${
|
||||
TEMPLATE_COLORS[input.template.name]?.(input.template.name) ??
|
||||
input.template.name
|
||||
} template.`,
|
||||
);
|
||||
console.log();
|
||||
consola.log('Next steps:');
|
||||
let step = 0;
|
||||
if (cdPath !== '.') consola.log(` ${++step}.`, pc.cyan(`cd ${cdPath}`));
|
||||
consola.log(` ${++step}.`, pc.cyan(`${input.packageManager} install`));
|
||||
console.log();
|
||||
},
|
||||
{ disableFinishedLog: true },
|
||||
);
|
||||
|
||||
interface Template {
|
||||
/**
|
||||
* Template's name.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Path to template directory in github repo.
|
||||
*/
|
||||
path: string;
|
||||
}
|
||||
|
||||
async function listTemplates(): Promise<Template[]> {
|
||||
try {
|
||||
const res = await fetch(
|
||||
'https://api.github.com/repos/aklinker1/wxt/contents/templates',
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
},
|
||||
},
|
||||
);
|
||||
if (res.status >= 300)
|
||||
throw Error(`Request failed with status ${res.status} ${res.statusText}`);
|
||||
|
||||
const data = (await res.json()) as Array<{
|
||||
type: 'file' | 'dir';
|
||||
name: string;
|
||||
path: string;
|
||||
}>;
|
||||
return data
|
||||
.filter((item: any) => item.type === 'dir')
|
||||
.map((item) => ({ name: item.name, path: item.path }))
|
||||
.sort((l, r) => {
|
||||
const lWeight = TEMPLATE_SORT_WEIGHT[l.name] ?? Number.MAX_SAFE_INTEGER;
|
||||
const rWeight = TEMPLATE_SORT_WEIGHT[r.name] ?? Number.MAX_SAFE_INTEGER;
|
||||
const diff = lWeight - rWeight;
|
||||
if (diff !== 0) return diff;
|
||||
return l.name.localeCompare(r.name);
|
||||
});
|
||||
} catch (err) {
|
||||
throw Error(`Cannot load templates: ${JSON.stringify(err, null, 2)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function cloneProject({
|
||||
directory,
|
||||
template,
|
||||
packageManager,
|
||||
}: {
|
||||
directory: string;
|
||||
template: Template;
|
||||
packageManager: string;
|
||||
}) {
|
||||
const spinner = ora('Downloading template').start();
|
||||
try {
|
||||
// 1. Clone repo
|
||||
await downloadTemplate(`gh:aklinker1/wxt/${template.path}`, {
|
||||
dir: directory,
|
||||
force: true,
|
||||
});
|
||||
|
||||
// 2. Move _gitignore -> .gitignore
|
||||
await fs
|
||||
.move(
|
||||
path.join(directory, '_gitignore'),
|
||||
path.join(directory, '.gitignore'),
|
||||
)
|
||||
.catch((err) =>
|
||||
consola.warn('Failed to move _gitignore to .gitignore:', err),
|
||||
);
|
||||
|
||||
// 3. Add .npmrc for pnpm
|
||||
if (packageManager === 'pnpm') {
|
||||
await fs.writeFile(
|
||||
path.join(directory, '.npmrc'),
|
||||
'shamefully-hoist=true\n',
|
||||
);
|
||||
}
|
||||
|
||||
spinner.succeed();
|
||||
} catch (err) {
|
||||
spinner.fail();
|
||||
throw Error(`Failed to setup new project: ${JSON.stringify(err, null, 2)}`);
|
||||
}
|
||||
}
|
||||
|
||||
const TEMPLATE_COLORS: Record<string, Formatter> = {
|
||||
vanilla: pc.blue,
|
||||
vue: pc.green,
|
||||
react: pc.cyan,
|
||||
svelte: pc.red,
|
||||
solid: pc.blue,
|
||||
};
|
||||
|
||||
const TEMPLATE_SORT_WEIGHT: Record<string, number> = {
|
||||
vanilla: 0,
|
||||
vue: 1,
|
||||
react: 2,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import * as wxt from '../..';
|
||||
import { buildInternal } from '../../core/build';
|
||||
import { getInternalConfig } from '../../core/utils/getInternalConfig';
|
||||
import { zipExtension } from '../../core/zip';
|
||||
import { defineCommand } from '../utils/defineCommand';
|
||||
|
||||
export const zip = defineCommand<
|
||||
[
|
||||
root: string | undefined,
|
||||
flags: {
|
||||
mode?: string;
|
||||
config?: string;
|
||||
browser?: wxt.TargetBrowser;
|
||||
mv3?: boolean;
|
||||
mv2?: boolean;
|
||||
},
|
||||
]
|
||||
>(async (root, flags) => {
|
||||
const mode = flags.mode ?? 'production';
|
||||
const cliConfig: wxt.InlineConfig = {
|
||||
root,
|
||||
mode,
|
||||
browser: flags.browser,
|
||||
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
|
||||
configFile: flags.config,
|
||||
};
|
||||
|
||||
const config = await getInternalConfig(cliConfig, 'build');
|
||||
const output = await buildInternal(config);
|
||||
await zipExtension(config, output);
|
||||
});
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import cac from 'cac';
|
||||
import { version } from '../../package.json';
|
||||
import * as commands from './commands';
|
||||
@@ -28,6 +26,16 @@ cli
|
||||
.option('--mv2', 'target manifest v2')
|
||||
.action(commands.build);
|
||||
|
||||
// ZIP
|
||||
cli
|
||||
.command('zip [root]', 'build for production and zip output')
|
||||
.option('-c, --config <file>', 'use specified config file')
|
||||
.option('-m, --mode <mode>', 'set env mode')
|
||||
.option('-b, --browser <browser>', 'specify a browser')
|
||||
.option('--mv3', 'target manifest v3')
|
||||
.option('--mv2', 'target manifest v2')
|
||||
.action(commands.zip);
|
||||
|
||||
// PREPARE
|
||||
cli
|
||||
.command('prepare [root]', 'prepare')
|
||||
@@ -40,6 +48,8 @@ cli.command('publish [root]', 'publish to stores').action(commands.publish);
|
||||
// INIT
|
||||
cli
|
||||
.command('init [directory]', 'initialize a new project')
|
||||
.option('-t, --template <template>', 'template to use')
|
||||
.option('--pm <packageManager>', 'which package manager to use')
|
||||
.action(commands.init);
|
||||
|
||||
cli.parse();
|
||||
|
||||
@@ -4,6 +4,9 @@ import { formatDuration } from '../../core/utils/formatDuration';
|
||||
|
||||
export function defineCommand<TArgs extends any[]>(
|
||||
cb: (...args: TArgs) => void | boolean | Promise<void | boolean>,
|
||||
options?: {
|
||||
disableFinishedLog?: boolean;
|
||||
},
|
||||
) {
|
||||
return async (...args: TArgs) => {
|
||||
const startTime = Date.now();
|
||||
@@ -12,7 +15,7 @@ export function defineCommand<TArgs extends any[]>(
|
||||
|
||||
const ongoing = await cb(...args);
|
||||
|
||||
if (!ongoing)
|
||||
if (!ongoing && !options?.disableFinishedLog)
|
||||
consola.success(
|
||||
`Finished in ${formatDuration(Date.now() - startTime)}`,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import originalBrowser, { Browser, Runtime } from 'webextension-polyfill';
|
||||
|
||||
export interface AugmentedBrowser extends Browser {
|
||||
runtime: ProjectRuntime;
|
||||
}
|
||||
|
||||
export interface ProjectRuntime extends Runtime.Static {
|
||||
// Overriden per-project
|
||||
}
|
||||
|
||||
export const browser: AugmentedBrowser = originalBrowser;
|
||||
@@ -1,12 +1,10 @@
|
||||
import { BackgroundScriptDefintition } from '..';
|
||||
|
||||
export function defineBackgroundScript(
|
||||
main: () => void,
|
||||
): BackgroundScriptDefintition;
|
||||
export function defineBackgroundScript(
|
||||
export function defineBackground(main: () => void): BackgroundScriptDefintition;
|
||||
export function defineBackground(
|
||||
definition: BackgroundScriptDefintition,
|
||||
): BackgroundScriptDefintition;
|
||||
export function defineBackgroundScript(
|
||||
export function defineBackground(
|
||||
arg: (() => void) | BackgroundScriptDefintition,
|
||||
): BackgroundScriptDefintition {
|
||||
if (typeof arg === 'function') return { main: arg };
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from './defineContentScript';
|
||||
export * from './defineBackgroundScript';
|
||||
export * from './defineBackground';
|
||||
export * from './mountContentScriptUi';
|
||||
|
||||
@@ -39,13 +39,15 @@ export async function buildInternal(
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
const groups = groupEntrypoints(entrypoints);
|
||||
const { output } = await rebuild(config, groups);
|
||||
const { output } = await rebuild(config, groups, undefined);
|
||||
|
||||
// Post-build
|
||||
config.logger.success(
|
||||
await printBuildSummary(
|
||||
config.logger.success,
|
||||
`Built extension in ${formatDuration(Date.now() - startTime)}`,
|
||||
output,
|
||||
config,
|
||||
);
|
||||
await printBuildSummary(output, config);
|
||||
|
||||
return output;
|
||||
}
|
||||
@@ -70,7 +72,11 @@ export async function rebuild(
|
||||
): Promise<{ output: BuildOutput; manifest: Manifest.WebExtensionManifest }> {
|
||||
// Update types directory with new files and types
|
||||
const allEntrypoints = await findEntrypoints(config);
|
||||
await generateTypesDir(allEntrypoints, config);
|
||||
await generateTypesDir(allEntrypoints, config).catch((err) => {
|
||||
config.logger.warn('Failed to update .wxt directory:', err);
|
||||
// Throw the error if doing a regular build, don't for dev mode.
|
||||
if (config.command === 'build') throw err;
|
||||
});
|
||||
|
||||
// Build and merge the outputs
|
||||
const newOutput = await buildEntrypoints(entrypointGroups, config);
|
||||
|
||||
@@ -12,6 +12,7 @@ import fs from 'fs-extra';
|
||||
import { importTsFile } from '../../utils/importTsFile';
|
||||
import glob from 'fast-glob';
|
||||
import { fakeInternalConfig } from '../../../testing/fake-objects';
|
||||
import { unnormalizePath } from '../../utils/paths';
|
||||
|
||||
vi.mock('../../utils/importTsFile');
|
||||
const importTsFileMock = vi.mocked(importTsFile);
|
||||
@@ -29,6 +30,7 @@ describe('findEntrypoints', () => {
|
||||
root: '/',
|
||||
entrypointsDir: resolve('/src/entrypoints'),
|
||||
outDir: resolve('.output'),
|
||||
command: 'build',
|
||||
});
|
||||
|
||||
it.each<[string, string, PopupEntrypoint]>([
|
||||
@@ -137,6 +139,15 @@ describe('findEntrypoints', () => {
|
||||
);
|
||||
|
||||
it.each<[string, Omit<ContentScriptEntrypoint, 'options'>]>([
|
||||
[
|
||||
'content.ts',
|
||||
{
|
||||
type: 'content-script',
|
||||
name: 'content',
|
||||
inputPath: resolve(config.entrypointsDir, 'content.ts'),
|
||||
outputDir: resolve(config.outDir, 'content-scripts'),
|
||||
},
|
||||
],
|
||||
[
|
||||
'overlay.content.ts',
|
||||
{
|
||||
@@ -146,6 +157,15 @@ describe('findEntrypoints', () => {
|
||||
outputDir: resolve(config.outDir, 'content-scripts'),
|
||||
},
|
||||
],
|
||||
[
|
||||
'content/index.ts',
|
||||
{
|
||||
type: 'content-script',
|
||||
name: 'content',
|
||||
inputPath: resolve(config.entrypointsDir, 'content/index.ts'),
|
||||
outputDir: resolve(config.outDir, 'content-scripts'),
|
||||
},
|
||||
],
|
||||
[
|
||||
'overlay.content/index.ts',
|
||||
{
|
||||
@@ -192,10 +212,10 @@ describe('findEntrypoints', () => {
|
||||
},
|
||||
],
|
||||
])(
|
||||
'should find and load content script entrypoint config from %s',
|
||||
'should find and load background entrypoint config from %s',
|
||||
async (path, expected) => {
|
||||
const options: ContentScriptEntrypoint['options'] = {
|
||||
matches: ['<all_urls>'],
|
||||
const options: BackgroundEntrypoint['options'] = {
|
||||
type: 'module',
|
||||
};
|
||||
globMock.mockResolvedValueOnce([path]);
|
||||
importTsFileMock.mockResolvedValue(options);
|
||||
@@ -208,6 +228,24 @@ describe('findEntrypoints', () => {
|
||||
},
|
||||
);
|
||||
|
||||
it("should include a virtual background script so dev reloading works when there isn't a background entrypoint defined by the user", async () => {
|
||||
globMock.mockResolvedValueOnce([]);
|
||||
|
||||
const entrypoints = await findEntrypoints({
|
||||
...config,
|
||||
command: 'serve',
|
||||
});
|
||||
|
||||
expect(entrypoints).toHaveLength(1);
|
||||
expect(entrypoints[0]).toEqual({
|
||||
type: 'background',
|
||||
inputPath: 'virtual:user-background',
|
||||
name: 'background',
|
||||
options: {},
|
||||
outputDir: config.outDir,
|
||||
});
|
||||
});
|
||||
|
||||
it.each<[string, GenericEntrypoint]>([
|
||||
// Sandbox
|
||||
[
|
||||
@@ -395,6 +433,73 @@ describe('findEntrypoints', () => {
|
||||
outputDir: config.outDir,
|
||||
},
|
||||
],
|
||||
[
|
||||
'injected/index.ts',
|
||||
{
|
||||
type: 'unlisted-script',
|
||||
name: 'injected',
|
||||
inputPath: resolve(config.entrypointsDir, 'injected/index.ts'),
|
||||
outputDir: config.outDir,
|
||||
},
|
||||
],
|
||||
|
||||
// unlisted-style
|
||||
[
|
||||
'iframe.scss',
|
||||
{
|
||||
type: 'unlisted-style',
|
||||
name: 'iframe',
|
||||
inputPath: resolve(config.entrypointsDir, 'iframe.scss'),
|
||||
outputDir: config.outDir,
|
||||
},
|
||||
],
|
||||
[
|
||||
'iframe.css',
|
||||
{
|
||||
type: 'unlisted-style',
|
||||
name: 'iframe',
|
||||
inputPath: resolve(config.entrypointsDir, 'iframe.css'),
|
||||
outputDir: config.outDir,
|
||||
},
|
||||
],
|
||||
|
||||
// content-script-style
|
||||
[
|
||||
'content.css',
|
||||
{
|
||||
type: 'content-script-style',
|
||||
name: 'content',
|
||||
inputPath: resolve(config.entrypointsDir, 'content.css'),
|
||||
outputDir: resolve(config.outDir, 'content-scripts'),
|
||||
},
|
||||
],
|
||||
[
|
||||
'overlay.content.css',
|
||||
{
|
||||
type: 'content-script-style',
|
||||
name: 'overlay',
|
||||
inputPath: resolve(config.entrypointsDir, 'overlay.content.css'),
|
||||
outputDir: resolve(config.outDir, 'content-scripts'),
|
||||
},
|
||||
],
|
||||
[
|
||||
'content/index.css',
|
||||
{
|
||||
type: 'content-script-style',
|
||||
name: 'content',
|
||||
inputPath: resolve(config.entrypointsDir, 'content/index.css'),
|
||||
outputDir: resolve(config.outDir, 'content-scripts'),
|
||||
},
|
||||
],
|
||||
[
|
||||
'overlay.content/index.css',
|
||||
{
|
||||
type: 'content-script-style',
|
||||
name: 'overlay',
|
||||
inputPath: resolve(config.entrypointsDir, 'overlay.content/index.css'),
|
||||
outputDir: resolve(config.outDir, 'content-scripts'),
|
||||
},
|
||||
],
|
||||
])('should find entrypoint for %s', async (path, expected) => {
|
||||
globMock.mockResolvedValueOnce([path]);
|
||||
|
||||
@@ -406,9 +511,14 @@ describe('findEntrypoints', () => {
|
||||
|
||||
it('should not allow multiple entrypoints with the same name', async () => {
|
||||
globMock.mockResolvedValueOnce(['popup.html', 'popup/index.html']);
|
||||
const expectedPaths = [
|
||||
'src/entrypoints/popup.html',
|
||||
'src/entrypoints/popup/index.html',
|
||||
].map(unnormalizePath);
|
||||
|
||||
await expect(() => findEntrypoints(config)).rejects.toThrowError(
|
||||
'Multiple entrypoints with the name "popup" detected, but only one is allowed: src/entrypoints/popup.html, src/entrypoints/popup/index.html',
|
||||
'Multiple entrypoints with the name "popup" detected, but only one is allowed: ' +
|
||||
expectedPaths.join(', '),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,12 +6,12 @@ import {
|
||||
EntrypointGroup,
|
||||
InternalConfig,
|
||||
} from '../types';
|
||||
import * as plugins from '../vite-plugins';
|
||||
import * as wxtPlugins from '../vite-plugins';
|
||||
import { removeEmptyDirs } from '../utils/removeEmptyDirs';
|
||||
import { getEntrypointBundlePath } from '../utils/entrypoints';
|
||||
import glob from 'fast-glob';
|
||||
import fs from 'fs-extra';
|
||||
import { dirname, resolve } from 'path';
|
||||
import { getPublicFiles } from '../utils/public';
|
||||
|
||||
export async function buildEntrypoints(
|
||||
groups: EntrypointGroup[],
|
||||
@@ -45,7 +45,16 @@ async function buildSingleEntrypoint(
|
||||
? `virtual:wxt-${entrypoint.type}?${entrypoint.inputPath}`
|
||||
: entrypoint.inputPath;
|
||||
|
||||
const plugins: NonNullable<vite.UserConfig['plugins']> = [];
|
||||
if (
|
||||
entrypoint.type === 'content-script-style' ||
|
||||
entrypoint.type === 'unlisted-style'
|
||||
) {
|
||||
plugins.push(wxtPlugins.cssEntrypoints(entrypoint, config));
|
||||
}
|
||||
|
||||
const libMode: vite.UserConfig = {
|
||||
plugins,
|
||||
build: {
|
||||
lib: {
|
||||
entry,
|
||||
@@ -90,7 +99,7 @@ async function buildMultipleEntrypoints(
|
||||
config: InternalConfig,
|
||||
): Promise<BuildStepOutput> {
|
||||
const multiPage: vite.UserConfig = {
|
||||
plugins: [plugins.multipageMove(entrypoints, config)],
|
||||
plugins: [wxtPlugins.multipageMove(entrypoints, config)],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: entrypoints.reduce<Record<string, string>>((input, entry) => {
|
||||
@@ -132,11 +141,10 @@ function getBuildOutputChunks(
|
||||
async function copyPublicDirectory(
|
||||
config: InternalConfig,
|
||||
): Promise<BuildOutput['publicAssets']> {
|
||||
const files = await getPublicFiles(config);
|
||||
if (files.length === 0) return [];
|
||||
|
||||
const publicAssets: BuildOutput['publicAssets'] = [];
|
||||
if (!(await fs.exists(config.publicDir))) return publicAssets;
|
||||
|
||||
const files = await glob('**/*', { cwd: config.publicDir });
|
||||
|
||||
for (const file of files) {
|
||||
const srcPath = resolve(config.publicDir, file);
|
||||
const outPath = resolve(config.outDir, file);
|
||||
|
||||
@@ -10,15 +10,17 @@ import {
|
||||
PopupEntrypoint,
|
||||
} from '../types';
|
||||
import fs from 'fs-extra';
|
||||
import picomatch from 'picomatch';
|
||||
import { minimatch } from 'minimatch';
|
||||
import { parseHTML } from 'linkedom';
|
||||
import JSON5 from 'json5';
|
||||
import { importTsFile } from '../utils/importTsFile';
|
||||
import glob from 'fast-glob';
|
||||
import { getEntrypointName } from '../utils/entrypoints';
|
||||
import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '../vite-plugins/noopBackground';
|
||||
import { CSS_EXTENSIONS_PATTERN } from '../utils/paths';
|
||||
|
||||
/**
|
||||
* Return entrypoints and their configuration by looking through the
|
||||
* Return entrypoints and their configuration by looking through the project's files.
|
||||
*/
|
||||
export async function findEntrypoints(
|
||||
config: InternalConfig,
|
||||
@@ -33,11 +35,12 @@ export async function findEntrypoints(
|
||||
const existingNames: Record<string, Entrypoint | undefined> = {};
|
||||
|
||||
const entrypoints: Entrypoint[] = [];
|
||||
let hasBackground = false;
|
||||
await Promise.all(
|
||||
relativePaths.map(async (relativePath) => {
|
||||
const path = resolve(config.entrypointsDir, relativePath);
|
||||
const matchingGlob = pathGlobs.find((glob) =>
|
||||
picomatch.isMatch(relativePath, glob),
|
||||
minimatch(relativePath, glob),
|
||||
);
|
||||
|
||||
if (matchingGlob == null) {
|
||||
@@ -63,14 +66,23 @@ export async function findEntrypoints(
|
||||
break;
|
||||
case 'background':
|
||||
entrypoint = await getBackgroundEntrypoint(config, path);
|
||||
hasBackground = true;
|
||||
break;
|
||||
case 'content-script':
|
||||
entrypoint = await getContentScriptEntrypoint(
|
||||
config,
|
||||
relativePath.split('.', 2)[0],
|
||||
getEntrypointName(config.entrypointsDir, path),
|
||||
path,
|
||||
);
|
||||
break;
|
||||
case 'content-script-style':
|
||||
entrypoint = {
|
||||
type,
|
||||
name: getEntrypointName(config.entrypointsDir, path),
|
||||
inputPath: path,
|
||||
outputDir: resolve(config.outDir, CONTENT_SCRIPT_OUT_DIR),
|
||||
};
|
||||
break;
|
||||
default:
|
||||
entrypoint = {
|
||||
type,
|
||||
@@ -95,6 +107,11 @@ export async function findEntrypoints(
|
||||
existingNames[entrypoint.name] = entrypoint;
|
||||
}),
|
||||
);
|
||||
if (config.command === 'serve' && !hasBackground) {
|
||||
entrypoints.push(
|
||||
await getBackgroundEntrypoint(config, VIRTUAL_NOOP_BACKGROUND_MODULE_ID),
|
||||
);
|
||||
}
|
||||
return entrypoints;
|
||||
}
|
||||
|
||||
@@ -162,21 +179,21 @@ async function getOptionsEntrypoint(
|
||||
.querySelector("meta[name='manifest.open_in_tab']")
|
||||
?.getAttribute('content');
|
||||
if (openInTabContent) {
|
||||
options.openInTab = Boolean(openInTabContent);
|
||||
options.openInTab = openInTabContent === 'true';
|
||||
}
|
||||
|
||||
const chromeStyleContent = document
|
||||
.querySelector("meta[name='manifest.chrome_style']")
|
||||
?.getAttribute('content');
|
||||
if (chromeStyleContent) {
|
||||
options.chromeStyle = Boolean(chromeStyleContent);
|
||||
options.chromeStyle = chromeStyleContent === 'true';
|
||||
}
|
||||
|
||||
const browserStyleContent = document
|
||||
.querySelector("meta[name='manifest.browser_style']")
|
||||
?.getAttribute('content');
|
||||
if (browserStyleContent) {
|
||||
options.browserStyle = Boolean(browserStyleContent);
|
||||
options.browserStyle = browserStyleContent === 'true';
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -195,10 +212,17 @@ async function getBackgroundEntrypoint(
|
||||
config: InternalConfig,
|
||||
path: string,
|
||||
): Promise<BackgroundEntrypoint> {
|
||||
const { main: _, ...options } =
|
||||
await importTsFile<BackgroundScriptDefintition>(path, config);
|
||||
if (options == null) {
|
||||
throw Error('Background script does not have a default export');
|
||||
let options: Omit<BackgroundScriptDefintition, 'main'> = {};
|
||||
if (path !== VIRTUAL_NOOP_BACKGROUND_MODULE_ID) {
|
||||
const defaultExport = await importTsFile<BackgroundScriptDefintition>(
|
||||
path,
|
||||
config,
|
||||
);
|
||||
if (defaultExport == null) {
|
||||
throw Error('Background script does not have a default export');
|
||||
}
|
||||
const { main: _, ...moduleOptions } = defaultExport;
|
||||
options = moduleOptions;
|
||||
}
|
||||
return {
|
||||
type: 'background',
|
||||
@@ -228,7 +252,7 @@ async function getContentScriptEntrypoint(
|
||||
type: 'content-script',
|
||||
name: getEntrypointName(config.entrypointsDir, path),
|
||||
inputPath: path,
|
||||
outputDir: resolve(config.outDir, 'content-scripts'),
|
||||
outputDir: resolve(config.outDir, CONTENT_SCRIPT_OUT_DIR),
|
||||
options,
|
||||
};
|
||||
}
|
||||
@@ -257,9 +281,16 @@ const PATH_GLOB_TO_TYPE_MAP: Record<string, Entrypoint['type'] | 'ignored'> = {
|
||||
'devtools/index.html': 'devtools',
|
||||
|
||||
'background.ts': 'background',
|
||||
[VIRTUAL_NOOP_BACKGROUND_MODULE_ID]: 'background',
|
||||
|
||||
'content.ts?(x)': 'content-script',
|
||||
'content/index.ts?(x)': 'content-script',
|
||||
'*.content.ts?(x)': 'content-script',
|
||||
'*.content/index.ts?(x)': 'content-script',
|
||||
[`content.${CSS_EXTENSIONS_PATTERN}`]: 'content-script-style',
|
||||
[`*.content.${CSS_EXTENSIONS_PATTERN}`]: 'content-script-style',
|
||||
[`content/index.${CSS_EXTENSIONS_PATTERN}`]: 'content-script-style',
|
||||
[`*.content/index.${CSS_EXTENSIONS_PATTERN}`]: 'content-script-style',
|
||||
|
||||
'popup.html': 'popup',
|
||||
'popup/index.html': 'popup',
|
||||
@@ -270,7 +301,12 @@ const PATH_GLOB_TO_TYPE_MAP: Record<string, Entrypoint['type'] | 'ignored'> = {
|
||||
'*.html': 'unlisted-page',
|
||||
'*/index.html': 'unlisted-page',
|
||||
'*.ts': 'unlisted-script',
|
||||
'*/index.ts': 'unlisted-script',
|
||||
[`*.${CSS_EXTENSIONS_PATTERN}`]: 'unlisted-style',
|
||||
[`*/index.${CSS_EXTENSIONS_PATTERN}`]: 'unlisted-style',
|
||||
|
||||
// Don't warn about any files in subdirectories, like CSS or JS entrypoints for HTML files
|
||||
'*/*': 'ignored',
|
||||
};
|
||||
|
||||
const CONTENT_SCRIPT_OUT_DIR = 'content-scripts';
|
||||
|
||||
@@ -5,6 +5,8 @@ import { relative, resolve } from 'path';
|
||||
import { getEntrypointBundlePath } from '../utils/entrypoints';
|
||||
import { getUnimportOptions } from '../utils/auto-imports';
|
||||
import { getGlobals } from '../utils/globals';
|
||||
import { getPublicFiles } from '../utils/public';
|
||||
import { normalizePath } from '../utils/paths';
|
||||
|
||||
/**
|
||||
* Generate and write all the files inside the `InternalConfig.typesDir` directory.
|
||||
@@ -49,23 +51,34 @@ async function writePathsDeclarationFile(
|
||||
): Promise<string> {
|
||||
const filePath = resolve(config.typesDir, 'paths.d.ts');
|
||||
const unions = entrypoints
|
||||
.map((entry) => {
|
||||
const path = getEntrypointBundlePath(
|
||||
.map((entry) =>
|
||||
getEntrypointBundlePath(
|
||||
entry,
|
||||
config.outDir,
|
||||
entry.inputPath.endsWith('.html') ? '.html' : '.js',
|
||||
);
|
||||
return ` | "/${path}"`;
|
||||
})
|
||||
.sort();
|
||||
),
|
||||
)
|
||||
.concat(await getPublicFiles(config))
|
||||
.map(normalizePath)
|
||||
.map((path) => ` | "/${path}"`)
|
||||
.sort()
|
||||
.join('\n');
|
||||
|
||||
const template = `// Generated by wxt
|
||||
import "wxt/browser";
|
||||
|
||||
declare module "wxt/browser" {
|
||||
type PublicPath =
|
||||
{{ union }}
|
||||
export interface ProjectRuntime extends Runtime.Static {
|
||||
getURL(path: PublicPath): string;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
await fs.writeFile(
|
||||
filePath,
|
||||
[
|
||||
'// Generated by wxt',
|
||||
'type EntrypointPath =',
|
||||
...(unions.length === 0 ? [' never'] : unions),
|
||||
].join('\n') + '\n',
|
||||
template.replace('{{ union }}', unions || ' | never'),
|
||||
);
|
||||
|
||||
return filePath;
|
||||
@@ -102,7 +115,8 @@ async function writeMainDeclarationFile(
|
||||
'// Generated by wxt',
|
||||
`/// <reference types="vite/client" />`,
|
||||
...references.map(
|
||||
(ref) => `/// <reference types="./${relative(dir, ref)}" />`,
|
||||
(ref) =>
|
||||
`/// <reference types="./${normalizePath(relative(dir, ref))}" />`,
|
||||
),
|
||||
].join('\n') + '\n',
|
||||
);
|
||||
@@ -125,31 +139,26 @@ async function writeTsConfigFile(
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true,
|
||||
|
||||
/* Completeness */
|
||||
"lib": ["DOM", "WebWorker"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Aliases */
|
||||
"baseUrl": "${relative(dir, config.root)}",
|
||||
"baseUrl": "${normalizePath(relative(dir, config.root))}",
|
||||
"paths": {
|
||||
"@@": ["."],
|
||||
"@@/*": ["./*"],
|
||||
"~~": ["."],
|
||||
"~~/*": ["./*"],
|
||||
"@": ["${relative(config.root, config.srcDir)}"],
|
||||
"@/*": ["${relative(config.root, config.srcDir)}/*"],
|
||||
"~": ["${relative(config.root, config.srcDir)}"],
|
||||
"~/*": ["${relative(config.root, config.srcDir)}/*"]
|
||||
"@": ["${normalizePath(relative(config.root, config.srcDir))}"],
|
||||
"@/*": ["${normalizePath(relative(config.root, config.srcDir))}/*"],
|
||||
"~": ["${normalizePath(relative(config.root, config.srcDir))}"],
|
||||
"~/*": ["${normalizePath(relative(config.root, config.srcDir))}/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"${relative(dir, config.root)}/**/*",
|
||||
"./${relative(dir, mainReference)}"
|
||||
"${normalizePath(relative(dir, config.root))}/**/*",
|
||||
"./${normalizePath(relative(dir, mainReference))}"
|
||||
],
|
||||
"exclude": ["${relative(dir, config.outBaseDir)}"]
|
||||
"exclude": ["${normalizePath(relative(dir, config.outBaseDir))}"]
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import path, { relative, resolve } from 'path';
|
||||
import { resolve } from 'path';
|
||||
import { BuildOutput, InternalConfig } from '../types';
|
||||
import { printTable } from './printTable';
|
||||
import pc from 'picocolors';
|
||||
import fs from 'fs-extra';
|
||||
import { filesize } from 'filesize';
|
||||
import { printFileList } from './printFileList';
|
||||
|
||||
export async function printBuildSummary(
|
||||
log: (...args: any[]) => void,
|
||||
header: string,
|
||||
output: BuildOutput,
|
||||
config: InternalConfig,
|
||||
) {
|
||||
@@ -20,31 +19,8 @@ export async function printBuildSummary(
|
||||
return l.fileName.localeCompare(r.fileName);
|
||||
});
|
||||
|
||||
let totalSize = 0;
|
||||
|
||||
const chunkRows: string[][] = await Promise.all(
|
||||
chunks.map(async (chunk, i) => {
|
||||
const file = [
|
||||
relative(process.cwd(), config.outDir) + path.sep,
|
||||
chunk.fileName,
|
||||
];
|
||||
const prefix = i === chunks.length - 1 ? ' └─' : ' ├─';
|
||||
const color = getChunkColor(chunk.fileName);
|
||||
const stats = await fs.lstat(resolve(config.outDir, chunk.fileName));
|
||||
totalSize += stats.size;
|
||||
const size = String(filesize(stats.size));
|
||||
return [
|
||||
`${pc.gray(prefix)} ${pc.dim(file[0])}${color(file[1])}`,
|
||||
pc.dim(size),
|
||||
];
|
||||
}),
|
||||
);
|
||||
|
||||
printTable(config.logger.log, chunkRows);
|
||||
|
||||
config.logger.log(
|
||||
`${pc.cyan('Σ Total size:')} ${String(filesize(totalSize))}`,
|
||||
);
|
||||
const files = chunks.map((chunk) => resolve(config.outDir, chunk.fileName));
|
||||
await printFileList(log, header, config.outDir, files);
|
||||
}
|
||||
|
||||
const DEFAULT_SORT_WEIGHT = 100;
|
||||
@@ -62,17 +38,3 @@ function getChunkSortWeight(filename: string) {
|
||||
)?.[1] ?? DEFAULT_SORT_WEIGHT
|
||||
);
|
||||
}
|
||||
|
||||
const DEFAULT_COLOR = pc.blue;
|
||||
const CHUNK_COLORS: Record<string, (text: string) => string> = {
|
||||
'.js.map': pc.gray,
|
||||
'.html': pc.green,
|
||||
'.css': pc.magenta,
|
||||
'.js': pc.cyan,
|
||||
};
|
||||
function getChunkColor(filename: string) {
|
||||
return (
|
||||
Object.entries(CHUNK_COLORS).find(([key]) => filename.endsWith(key))?.[1] ??
|
||||
DEFAULT_COLOR
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import path from 'node:path';
|
||||
import pc from 'picocolors';
|
||||
import fs from 'fs-extra';
|
||||
import { filesize } from 'filesize';
|
||||
import { printTable } from './printTable';
|
||||
|
||||
export async function printFileList(
|
||||
log: (message: string) => void,
|
||||
header: string,
|
||||
baseDir: string,
|
||||
files: string[],
|
||||
): Promise<void> {
|
||||
let totalSize = 0;
|
||||
|
||||
const fileRows: string[][] = await Promise.all(
|
||||
files.map(async (file, i) => {
|
||||
const parts = [
|
||||
path.relative(process.cwd(), baseDir) + path.sep,
|
||||
path.relative(baseDir, file),
|
||||
];
|
||||
const prefix = i === files.length - 1 ? ' └─' : ' ├─';
|
||||
const color = getChunkColor(file);
|
||||
const stats = await fs.lstat(file);
|
||||
totalSize += stats.size;
|
||||
const size = String(filesize(stats.size));
|
||||
return [
|
||||
`${pc.gray(prefix)} ${pc.dim(parts[0])}${color(parts[1])}`,
|
||||
pc.dim(size),
|
||||
];
|
||||
}),
|
||||
);
|
||||
|
||||
fileRows.push([`${pc.cyan('Σ Total size:')} ${String(filesize(totalSize))}`]);
|
||||
|
||||
printTable(log, header, fileRows);
|
||||
}
|
||||
|
||||
const DEFAULT_COLOR = pc.blue;
|
||||
const CHUNK_COLORS: Record<string, (text: string) => string> = {
|
||||
'.js.map': pc.gray,
|
||||
'.html': pc.green,
|
||||
'.css': pc.magenta,
|
||||
'.js': pc.cyan,
|
||||
'.zip': pc.yellow,
|
||||
};
|
||||
function getChunkColor(filename: string) {
|
||||
return (
|
||||
Object.entries(CHUNK_COLORS).find(([key]) => filename.endsWith(key))?.[1] ??
|
||||
DEFAULT_COLOR
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
export function printTable(
|
||||
log: (message: string) => void,
|
||||
header: string,
|
||||
rows: string[][],
|
||||
gap = 2,
|
||||
): void {
|
||||
@@ -24,5 +25,5 @@ export function printTable(
|
||||
if (i !== rows.length - 1) str += '\n';
|
||||
});
|
||||
|
||||
log(str);
|
||||
log(`${header}\n${str}`);
|
||||
}
|
||||
|
||||
@@ -5,15 +5,15 @@ import {
|
||||
WxtDevServer,
|
||||
} from './types';
|
||||
import * as vite from 'vite';
|
||||
import { findOpenPort } from './utils/findOpenPort';
|
||||
import { Manifest } from 'webextension-polyfill';
|
||||
import { Scripting } from 'webextension-polyfill';
|
||||
import { getEntrypointBundlePath } from './utils/entrypoints';
|
||||
import { getContentScriptCssFiles } from './utils/manifest';
|
||||
import { createWebExtRunner } from './runners/createWebExtRunner';
|
||||
import { buildInternal } from './build';
|
||||
|
||||
export async function getServerInfo(): Promise<ServerInfo> {
|
||||
const port = await findOpenPort(3000, 3010);
|
||||
const { default: getPort, portNumbers } = await import('get-port');
|
||||
const port = await getPort({ port: portNumbers(3000, 3010) });
|
||||
const hostname = 'localhost';
|
||||
const origin = `http://${hostname}:${port}`;
|
||||
const serverConfig: vite.InlineConfig = {
|
||||
@@ -58,7 +58,9 @@ export async function setupServer(
|
||||
// paths ending in "/index.html"
|
||||
viteServer.ws.send('wxt:reload-page', path);
|
||||
};
|
||||
const reloadContentScript = (contentScript: Manifest.ContentScript) => {
|
||||
const reloadContentScript = (
|
||||
contentScript: Omit<Scripting.RegisteredContentScript, 'id'>,
|
||||
) => {
|
||||
viteServer.ws.send('wxt:reload-content-script', contentScript);
|
||||
};
|
||||
|
||||
@@ -102,9 +104,9 @@ export function reloadContentScripts(
|
||||
const css = getContentScriptCssFiles([entry], server.currentOutput);
|
||||
|
||||
server.reloadContentScript({
|
||||
...entry.options,
|
||||
js,
|
||||
css,
|
||||
...entry.options,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -1,28 +1,145 @@
|
||||
import * as vite from 'vite';
|
||||
import { Manifest } from 'webextension-polyfill';
|
||||
import { Manifest, Scripting } from 'webextension-polyfill';
|
||||
import { UnimportOptions } from 'unimport';
|
||||
import { EntrypointGroup } from '.';
|
||||
|
||||
export interface InlineConfig {
|
||||
/**
|
||||
* Your project's root directory containing the `package.json` used to fill out the
|
||||
* `manifest.json`.
|
||||
*
|
||||
* @default process.cwd()
|
||||
*/
|
||||
root?: string;
|
||||
/**
|
||||
* Directory containing all source code. Set to `"src"` to move all source code to a `src/`
|
||||
* directory.
|
||||
*
|
||||
* @default config.root
|
||||
*/
|
||||
srcDir?: string;
|
||||
/**
|
||||
* Directory containing files that will be copied to the output directory as-is.
|
||||
*
|
||||
* @default "${config.root}/public"
|
||||
*/
|
||||
publicDir?: string;
|
||||
/**
|
||||
* @default "${config.srcDir}/entrypoints"
|
||||
*/
|
||||
entrypointsDir?: string;
|
||||
/**
|
||||
* Path to `"wxt.config.ts"` file or false to disable config file discovery.
|
||||
*
|
||||
* @default "wxt.config.ts"
|
||||
*/
|
||||
configFile?: string | false;
|
||||
/**
|
||||
* ID of the extension for each store. Used for publishing.
|
||||
*/
|
||||
storeIds?: {
|
||||
chrome?: string;
|
||||
firefox?: string;
|
||||
edge?: string;
|
||||
};
|
||||
/**
|
||||
* Explicitly set a mode to run in. This will override the default mode for each command, and can
|
||||
* be overridden by the command line `--mode` option.
|
||||
*/
|
||||
mode?: string;
|
||||
/**
|
||||
* Customize auto-import options.
|
||||
*/
|
||||
imports?: Partial<UnimportOptions>;
|
||||
/**
|
||||
* Explicitly set a browser to build for. This will override the default browser for each command,
|
||||
* and can be overridden by the command line `--browser` option.
|
||||
*
|
||||
* @default
|
||||
* "chrome"
|
||||
*/
|
||||
browser?: TargetBrowser;
|
||||
/**
|
||||
* Explicitly set a manifest version to target. This will override the default manifest version
|
||||
* for each command, and can be overridden by the command line `--mv2` or `--mv3` option.
|
||||
*/
|
||||
manifestVersion?: TargetManifestVersion;
|
||||
/**
|
||||
* Override the logger used.
|
||||
*
|
||||
* @default
|
||||
* consola
|
||||
*/
|
||||
logger?: Logger;
|
||||
/**
|
||||
* Custom Vite options.
|
||||
*/
|
||||
vite?: Omit<vite.UserConfig, 'root' | 'configFile' | 'mode'>;
|
||||
/**
|
||||
* Customize the `manifest.json` output. Can be an object, promise, or function that returns an
|
||||
* object or promise.
|
||||
*/
|
||||
manifest?: UserManifest | Promise<UserManifest> | UserManifestFn;
|
||||
/**
|
||||
* Custom server options.
|
||||
*/
|
||||
server?: WxtDevServer;
|
||||
/**
|
||||
* Custom runner options. Options set here can be overridden in a `web-ext.config.ts` file.
|
||||
*/
|
||||
runner?: ExtensionRunnerConfig;
|
||||
zip?: {
|
||||
/**
|
||||
* Configure the filename output when zipping files.
|
||||
*
|
||||
* Available template variables:
|
||||
*
|
||||
* - `{name}` - The project's name converted to kebab-case
|
||||
* - `{version}` - The version_name or version from the manifest
|
||||
* - `{browser}` - The target browser from the `--browser` CLI flag
|
||||
* - `{manifestVersion}` - Either "2" or "3"
|
||||
*
|
||||
* @default "{name}-{version}-{browser}.zip"
|
||||
*/
|
||||
artifactTemplate?: string;
|
||||
/**
|
||||
* Configure the filename output when zipping files.
|
||||
*
|
||||
* Available template variables:
|
||||
*
|
||||
* - `{name}` - The project's name converted to kebab-case
|
||||
* - `{version}` - The version_name or version from the manifest
|
||||
* - `{browser}` - The target browser from the `--browser` CLI flag
|
||||
* - `{manifestVersion}` - Either "2" or "3"
|
||||
*
|
||||
* @default "{name}-{version}-sources.zip"
|
||||
*/
|
||||
sourcesTemplate?: string;
|
||||
/**
|
||||
* Override the artifactTemplate's `{name}` template variable. Defaults to the `package.json`'s
|
||||
* name, or if that doesn't exist, the current working directories name.
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* Root directory to ZIP when generating the sources ZIP.
|
||||
*
|
||||
* @default config.root
|
||||
*/
|
||||
sourcesRoot?: string;
|
||||
/**
|
||||
* [Minimatch](https://www.npmjs.com/package/minimatch) patterns of files to exclude when
|
||||
* creating a ZIP of all your source code for Firfox. Patterns are relative to your
|
||||
* `config.zip.sourcesRoot`.
|
||||
*
|
||||
* Hidden files, node_modules, and tests are ignored by default.
|
||||
*
|
||||
* @example
|
||||
* [
|
||||
* "coverage", // Ignore the coverage directory in the `sourcesRoot`
|
||||
* ]
|
||||
*/
|
||||
ignoredSources?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface WxtInlineViteConfig
|
||||
@@ -83,7 +200,9 @@ export interface WxtDevServer extends vite.ViteDevServer {
|
||||
*
|
||||
* @param contentScript The manifest definition for a content script
|
||||
*/
|
||||
reloadContentScript: (contentScript: Manifest.ContentScript) => void;
|
||||
reloadContentScript: (
|
||||
contentScript: Omit<Scripting.RegisteredContentScript, 'id'>,
|
||||
) => void;
|
||||
}
|
||||
|
||||
export type TargetBrowser = 'chrome' | 'firefox' | 'safari' | 'edge' | 'opera';
|
||||
@@ -139,7 +258,9 @@ export interface GenericEntrypoint extends BaseEntrypoint {
|
||||
| 'sidepanel'
|
||||
| 'devtools'
|
||||
| 'unlisted-page'
|
||||
| 'unlisted-script';
|
||||
| 'unlisted-script'
|
||||
| 'unlisted-style'
|
||||
| 'content-script-style';
|
||||
}
|
||||
|
||||
export interface BackgroundEntrypoint extends BaseEntrypoint {
|
||||
@@ -186,11 +307,50 @@ export type Entrypoint =
|
||||
export type OnContentScriptStopped = (cb: () => void) => void;
|
||||
|
||||
export interface ContentScriptDefinition {
|
||||
matches: string[];
|
||||
runAt?: 'document_start' | 'document_end' | 'document_idle';
|
||||
matchAboutBlank?: boolean;
|
||||
matches: Manifest.ContentScript['matches'];
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
|
||||
* @default "documentIdle"
|
||||
*/
|
||||
runAt?: Manifest.ContentScript['run_at'];
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
|
||||
* @default false
|
||||
*/
|
||||
matchAboutBlank?: Manifest.ContentScript['match_about_blank'];
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
|
||||
* @default []
|
||||
*/
|
||||
excludeMatches?: Manifest.ContentScript['exclude_matches'];
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
|
||||
* @default []
|
||||
*/
|
||||
includeGlobs?: Manifest.ContentScript['include_globs'];
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
|
||||
* @default []
|
||||
*/
|
||||
excludeGlobs?: Manifest.ContentScript['exclude_globs'];
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
|
||||
* @default false
|
||||
*/
|
||||
allFrames?: Manifest.ContentScript['all_frames'];
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
|
||||
* @default false
|
||||
*/
|
||||
matchOriginAsFallback?: boolean;
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
|
||||
* @default "ISOLATED"
|
||||
*/
|
||||
world?: 'ISOLATED' | 'MAIN';
|
||||
/**
|
||||
* Main function executed when the content script is loaded.
|
||||
*/
|
||||
main(): void | Promise<void>;
|
||||
}
|
||||
|
||||
@@ -200,30 +360,27 @@ export interface BackgroundScriptDefintition {
|
||||
}
|
||||
|
||||
/**
|
||||
* Manifest customization available in the `wxt.config.ts` file. Any missing fields like "name"
|
||||
* and "version" are managed automatically, and don't need to be listed here.
|
||||
* Manifest customization available in the `wxt.config.ts` file. You cannot configure entrypoints
|
||||
* here, they are configured inline.
|
||||
*/
|
||||
export type UserManifest = Omit<
|
||||
Manifest.WebExtensionManifest,
|
||||
| 'action'
|
||||
| 'background'
|
||||
| 'browser_action'
|
||||
| 'chrome_url_overrides'
|
||||
| 'content_scripts'
|
||||
| 'description'
|
||||
| 'devtools_page'
|
||||
| 'manifest_version'
|
||||
| 'name'
|
||||
| 'options_page'
|
||||
| 'options_ui'
|
||||
| 'sandbox'
|
||||
| 'page_action'
|
||||
| 'popup'
|
||||
| 'short_name'
|
||||
| 'sidepanel'
|
||||
| 'sidebar_action'
|
||||
| 'version'
|
||||
| 'version_name'
|
||||
export type UserManifest = Partial<
|
||||
Omit<
|
||||
Manifest.WebExtensionManifest,
|
||||
| 'action'
|
||||
| 'background'
|
||||
| 'browser_action'
|
||||
| 'chrome_url_overrides'
|
||||
| 'content_scripts'
|
||||
| 'devtools_page'
|
||||
| 'manifest_version'
|
||||
| 'options_page'
|
||||
| 'options_ui'
|
||||
| 'sandbox'
|
||||
| 'page_action'
|
||||
| 'popup'
|
||||
| 'sidepanel'
|
||||
| 'sidebar_action'
|
||||
>
|
||||
>;
|
||||
|
||||
export type UserManifestFn = (
|
||||
|
||||
@@ -36,6 +36,13 @@ export interface InternalConfig {
|
||||
fsCache: FsCache;
|
||||
server?: WxtDevServer;
|
||||
runnerConfig: ResolvedConfig<ExtensionRunnerConfig>;
|
||||
zip: {
|
||||
name?: string;
|
||||
artifactTemplate: string;
|
||||
sourcesTemplate: string;
|
||||
ignoredSources: string[];
|
||||
sourcesRoot: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type EntrypointGroup = Entrypoint | Entrypoint[];
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { hashContentScriptOptions } from '../content-scripts';
|
||||
|
||||
describe('Content Script Utils', () => {
|
||||
describe('hashContentScriptOptions', () => {
|
||||
it('should return a string containing all the options with defaults applied', () => {
|
||||
const hash = hashContentScriptOptions({ matches: [] });
|
||||
|
||||
expect(hash).toMatchInlineSnapshot(
|
||||
'"[[\\"allFrames\\",false],[\\"excludeGlobs\\",[]],[\\"excludeMatches\\",[]],[\\"includeGlobs\\",[]],[\\"matchAboutBlank\\",false],[\\"matches\\",[]],[\\"matchOriginAsFallback\\",false],[\\"runAt\\",\\"document_idle\\"],[\\"world\\",\\"ISOLATED\\"]]"',
|
||||
);
|
||||
});
|
||||
|
||||
it('should be consistent regardless of the object ordering and default values', () => {
|
||||
const hash1 = hashContentScriptOptions({
|
||||
allFrames: true,
|
||||
matches: ['*://google.com/*', '*://duckduckgo.com/*'],
|
||||
matchAboutBlank: false,
|
||||
});
|
||||
const hash2 = hashContentScriptOptions({
|
||||
matches: ['*://duckduckgo.com/*', '*://google.com/*'],
|
||||
allFrames: true,
|
||||
});
|
||||
|
||||
expect(hash1).toBe(hash2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -56,6 +56,18 @@ const sandbox2: Entrypoint = {
|
||||
inputPath: '/sandbox2.html',
|
||||
outputDir: '/.output/sandbox2',
|
||||
};
|
||||
const unlistedStyle: Entrypoint = {
|
||||
type: 'unlisted-style',
|
||||
name: 'injected',
|
||||
inputPath: '/injected.scss',
|
||||
outputDir: '/.output',
|
||||
};
|
||||
const contentScriptStyle: Entrypoint = {
|
||||
type: 'content-script-style',
|
||||
name: 'injected',
|
||||
inputPath: '/overlay.content.scss',
|
||||
outputDir: '/.output/content-scripts',
|
||||
};
|
||||
|
||||
describe('groupEntrypoints', () => {
|
||||
it('should keep scripts separate', () => {
|
||||
@@ -72,6 +84,19 @@ describe('groupEntrypoints', () => {
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should keep styles separate', () => {
|
||||
const entrypoints: Entrypoint[] = [
|
||||
unlistedStyle,
|
||||
contentScriptStyle,
|
||||
popup,
|
||||
];
|
||||
const expected = [unlistedStyle, contentScriptStyle, [popup]];
|
||||
|
||||
const actual = groupEntrypoints(entrypoints);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should group extension pages together', () => {
|
||||
const entrypoints: Entrypoint[] = [
|
||||
popup,
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { kebabCaseAlphanumeric, removeImportStatements } from '../strings';
|
||||
|
||||
describe('String utils', () => {
|
||||
describe('kebabCaseAlphanumeric', () => {
|
||||
it.each([
|
||||
['HELLO', 'hello'],
|
||||
['Hello, World!', 'hello-world'],
|
||||
['hello123', 'hello123'],
|
||||
['Hello World This Is A Test', 'hello-world-this-is-a-test'],
|
||||
['Hello World', 'hello-world'],
|
||||
['hello-world', 'hello-world'], // Ensure hyphens are preserved
|
||||
])('should convert "%s" to "%s"', (input, expected) => {
|
||||
expect(kebabCaseAlphanumeric(input)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeImportStatements', () => {
|
||||
it('should remove all import formats', () => {
|
||||
const imports = `
|
||||
import { registerGithubService, createGithubApi } from "@/utils/github";
|
||||
import {
|
||||
registerGithubService,
|
||||
createGithubApi
|
||||
} from "@/utils/github";
|
||||
import{ registerGithubService, createGithubApi }from "@/utils/github";
|
||||
import GitHub from "@/utils/github";
|
||||
import "@/utils/github";
|
||||
import '@/utils/github';
|
||||
import"@/utils/github"
|
||||
import'@/utils/github';
|
||||
`;
|
||||
expect(removeImportStatements(imports).trim()).toEqual('');
|
||||
});
|
||||
|
||||
it('should not remove import.meta or inline import statements', () => {
|
||||
const imports = `
|
||||
import.meta.env.DEV
|
||||
const a = await import("example");
|
||||
import("example");
|
||||
`;
|
||||
expect(removeImportStatements(imports)).toEqual(imports);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -7,11 +7,8 @@ export function getUnimportOptions(
|
||||
): Partial<UnimportOptions> {
|
||||
const defaultOptions: Partial<UnimportOptions> = {
|
||||
debugLog: config.logger.debug,
|
||||
imports: [
|
||||
{ name: '*', as: 'browser', from: 'webextension-polyfill' },
|
||||
{ name: 'defineConfig', from: 'wxt' },
|
||||
],
|
||||
presets: [{ package: 'wxt/client' }],
|
||||
imports: [{ name: 'defineConfig', from: 'wxt' }],
|
||||
presets: [{ package: 'wxt/client' }, { package: 'wxt/browser' }],
|
||||
warn: config.logger.warn,
|
||||
dirs: ['components', 'composables', 'hooks', 'utils'],
|
||||
};
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Manifest } from 'webextension-polyfill';
|
||||
import { ContentScriptEntrypoint } from '../types';
|
||||
|
||||
/**
|
||||
* Returns a unique and consistent string hash based on a content scripts options.
|
||||
*
|
||||
* It is able to recognize default values,
|
||||
*/
|
||||
export function hashContentScriptOptions(
|
||||
options: ContentScriptEntrypoint['options'],
|
||||
): string {
|
||||
const withDefaults: ContentScriptEntrypoint['options'] = {
|
||||
excludeGlobs: [],
|
||||
excludeMatches: [],
|
||||
includeGlobs: [],
|
||||
matchAboutBlank: false,
|
||||
matchOriginAsFallback: false,
|
||||
runAt: 'document_idle',
|
||||
allFrames: false,
|
||||
world: 'ISOLATED',
|
||||
// TODO: strip undefined fields from options object to improve content script grouping.
|
||||
...options,
|
||||
};
|
||||
return JSON.stringify(
|
||||
Object.entries(withDefaults)
|
||||
// Sort any arrays so their values are consistent
|
||||
.map<[string, unknown]>(([key, value]) => {
|
||||
if (Array.isArray(value)) return [key, value.sort()];
|
||||
else return [key, value];
|
||||
})
|
||||
// Sort all the fields alphabetically
|
||||
.sort((l, r) => l[0].localeCompare(r[0])),
|
||||
);
|
||||
}
|
||||
|
||||
export function mapWxtOptionsToContentScript(
|
||||
options: ContentScriptEntrypoint['options'],
|
||||
): Omit<Manifest.ContentScript, 'js' | 'css'> {
|
||||
return {
|
||||
matches: options.matches,
|
||||
all_frames: options.allFrames,
|
||||
match_about_blank: options.matchAboutBlank,
|
||||
exclude_globs: options.excludeGlobs,
|
||||
exclude_matches: options.excludeMatches,
|
||||
include_globs: options.includeGlobs,
|
||||
run_at: options.runAt,
|
||||
|
||||
// @ts-expect-error: untyped chrome options
|
||||
match_origin_as_fallback: options.matchOriginAsFallback,
|
||||
world: options.world,
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { BuildOutput, BuildStepOutput, EntrypointGroup } from '../types';
|
||||
import * as vite from 'vite';
|
||||
import { every } from './arrays';
|
||||
import { normalizePath } from './paths';
|
||||
|
||||
/**
|
||||
* Compare the changed files vs the build output and determine what kind of reload needs to happen:
|
||||
@@ -102,15 +103,16 @@ function findEffectedSteps(
|
||||
currentOutput: BuildOutput,
|
||||
): DetectedChange[] {
|
||||
const changes: DetectedChange[] = [];
|
||||
const changedPath = changedFile[1];
|
||||
const changedPath = normalizePath(changedFile[1]);
|
||||
|
||||
const isChunkEffected = (
|
||||
chunk: vite.Rollup.OutputChunk | vite.Rollup.OutputAsset,
|
||||
): boolean =>
|
||||
// If it's an HTML file with the same path, is is effected because HTML files need to be pre-rendered
|
||||
// TODO: use bundle path to support `<name>/index.html`?
|
||||
// fileName is normalized, relative bundle path
|
||||
(chunk.type === 'asset' && changedPath.endsWith(chunk.fileName)) ||
|
||||
// If it's a chunk that depends on the changed file, it is effected
|
||||
// moduleIds are absolute, normalized paths
|
||||
(chunk.type === 'chunk' && chunk.moduleIds.includes(changedPath));
|
||||
|
||||
for (const step of currentOutput.steps) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Entrypoint } from '../types';
|
||||
import path, { relative, resolve } from 'node:path';
|
||||
import { normalizePath } from './paths';
|
||||
|
||||
export function getEntrypointName(
|
||||
entrypointsDir: string,
|
||||
@@ -7,8 +8,8 @@ export function getEntrypointName(
|
||||
// type: Entrypoint['type'],
|
||||
): string {
|
||||
const relativePath = path.relative(entrypointsDir, inputPath);
|
||||
// Grab the string up to the first . or /
|
||||
const name = relativePath.split(/[\.\/]/, 2)[0];
|
||||
// Grab the string up to the first . or / or \\
|
||||
const name = relativePath.split(/[\.\/\\]/, 2)[0];
|
||||
|
||||
return name;
|
||||
}
|
||||
@@ -29,5 +30,7 @@ export function getEntrypointBundlePath(
|
||||
outDir: string,
|
||||
ext: string,
|
||||
): string {
|
||||
return relative(outDir, getEntrypointOutputFile(entrypoint, ext));
|
||||
return normalizePath(
|
||||
relative(outDir, getEntrypointOutputFile(entrypoint, ext)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import net from 'node:net';
|
||||
|
||||
/**
|
||||
* Finds the first open port in a range of ports.
|
||||
*/
|
||||
export function findOpenPort(
|
||||
startPort: number,
|
||||
endPort: number,
|
||||
): Promise<number> {
|
||||
return findOpenPortRecursive(startPort, startPort, endPort);
|
||||
}
|
||||
|
||||
function findOpenPortRecursive(
|
||||
port: number,
|
||||
startPort: number,
|
||||
endPort: number,
|
||||
): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (port > endPort)
|
||||
return reject(
|
||||
Error(`Could not find open port between ${startPort}-${endPort}`),
|
||||
);
|
||||
const server = net.createServer();
|
||||
|
||||
server.listen(port, () => {
|
||||
server.once('close', () => resolve(port));
|
||||
server.close();
|
||||
});
|
||||
server.on('error', () =>
|
||||
resolve(findOpenPortRecursive(port + 1, startPort, endPort)),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -80,7 +80,7 @@ export async function getInternalConfig(
|
||||
srcDir,
|
||||
userConfig.entrypointsDir ?? 'entrypoints',
|
||||
);
|
||||
const publicDir = resolve(srcDir, userConfig.publicDir ?? 'public');
|
||||
const publicDir = resolve(root, userConfig.publicDir ?? 'public');
|
||||
const wxtDir = resolve(srcDir, '.wxt');
|
||||
const typesDir = resolve(wxtDir, 'types');
|
||||
|
||||
@@ -99,6 +99,26 @@ export async function getInternalConfig(
|
||||
typesDir,
|
||||
fsCache: createFsCache(wxtDir),
|
||||
manifest,
|
||||
zip: {
|
||||
sourcesTemplate: '{{name}}-{{version}}-sources.zip',
|
||||
artifactTemplate: '{{name}}-{{version}}-{{browser}}.zip',
|
||||
sourcesRoot: root,
|
||||
...userConfig.zip,
|
||||
...config.zip,
|
||||
ignoredSources: [
|
||||
'**/node_modules',
|
||||
// WXT files
|
||||
'**/web-ext.config.ts',
|
||||
// Hidden files
|
||||
'**/.*',
|
||||
// Tests
|
||||
'**/__tests__/**',
|
||||
'**/*.+(test|spec).?(c|m)+(j|t)s?(x)',
|
||||
// User config
|
||||
...(userConfig.zip?.ignoredSources ?? []),
|
||||
...(config.zip?.ignoredSources ?? []),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// Customize the default vite config
|
||||
@@ -122,7 +142,7 @@ export async function getInternalConfig(
|
||||
);
|
||||
finalConfig.vite.plugins.push(plugins.devServerGlobals(finalConfig));
|
||||
finalConfig.vite.plugins.push(plugins.tsconfigPaths(finalConfig));
|
||||
finalConfig.vite.plugins.push(plugins.hmrLogger(finalConfig));
|
||||
finalConfig.vite.plugins.push(plugins.noopBackground());
|
||||
|
||||
finalConfig.vite.define ??= {};
|
||||
getGlobals(finalConfig).forEach((global) => {
|
||||
@@ -145,6 +165,7 @@ type InternalConfigNoUserDirs = Omit<
|
||||
| 'typesDir'
|
||||
| 'fsCache'
|
||||
| 'manifest'
|
||||
| 'zip'
|
||||
>;
|
||||
|
||||
async function resolveManifestConfig(
|
||||
|
||||
@@ -12,7 +12,7 @@ export function getGlobals(
|
||||
{
|
||||
name: '__BROWSER__',
|
||||
value: config.browser,
|
||||
type: `"chromium" | "firefox"`,
|
||||
type: `string`,
|
||||
},
|
||||
{
|
||||
name: '__IS_CHROME__',
|
||||
|
||||