Compare commits
116 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a30dc46fa | |||
| b75c553e51 | |||
| a56face03d | |||
| 19756c61ce | |||
| 5f54b4de17 | |||
| ea8935c47c | |||
| e37f73880d | |||
| 2e24b9e18a | |||
| f8a0fb37a2 | |||
| dd26b99027 | |||
| d9e9b43f8d | |||
| d580083727 | |||
| 5a70d9e57d | |||
| 1b1af245bd | |||
| 874a531a62 | |||
| a3d409f250 | |||
| b6758ca9fa | |||
| 2672308946 | |||
| 7ac171ed3b | |||
| 6a93f20bb3 | |||
| 921af6a5a1 | |||
| 21ebeacd20 | |||
| 527600031f | |||
| 91a804c492 | |||
| 54f3785063 | |||
| 44e4bc5295 | |||
| f464d7d33f | |||
| d0672739f0 | |||
| 44464f914f | |||
| 8940c41bdb | |||
| 446f265b6c | |||
| 7a465684c0 | |||
| 0369316463 | |||
| 8b74291d18 | |||
| 739b738100 | |||
| 95442dd39b | |||
| 4f82645c08 | |||
| d200c376f8 | |||
| b58fb02016 | |||
| e1bab6c746 | |||
| d10c22fbd9 | |||
| da1f7f3ea5 | |||
| 2659272f8a | |||
| 868fd27804 | |||
| d9fdcb5b33 | |||
| 580793158f | |||
| f58d69dc5f | |||
| c9028dd335 | |||
| 6342f59c5e | |||
| 2d91898452 | |||
| 4acefd601c | |||
| 9a2e71b481 | |||
| 6e578f3156 | |||
| 474de83e28 | |||
| 65fcfc0064 | |||
| 2d4983e88b | |||
| 4150e42c05 | |||
| 8d7150653a | |||
| 61e57b7b7c | |||
| 83add72312 | |||
| 3c723d2c92 | |||
| 1eb35c7207 | |||
| 725ecf7c64 | |||
| 3847092df4 | |||
| 19b11c236f | |||
| 4b2012c489 | |||
| 0591050f31 | |||
| b15dc7fc11 | |||
| 89d15babc2 | |||
| c616125689 | |||
| e97071b7d7 | |||
| 1fa049c5a7 | |||
| b48cee9715 | |||
| 24e69fe1bf | |||
| 41e154992a | |||
| 55707932a9 | |||
| 08115a40ae | |||
| 96be879918 | |||
| 1fc4ada66d | |||
| d27f299641 | |||
| 7183114370 | |||
| 7bd940450f | |||
| cf5a7d1411 | |||
| 5482b2f934 | |||
| c4a6ff928d | |||
| 455e7f3765 | |||
| 25b6ab92f5 | |||
| 50f6289ac5 | |||
| a0e1b4741e | |||
| ab672e9dbd | |||
| 7b3ea52e02 | |||
| e0929a68ba | |||
| 4c430725a5 | |||
| 7d745f6ec7 | |||
| 10091d7769 | |||
| 2e142b3038 | |||
| 361bb2dcc8 | |||
| 26fca5c0a2 | |||
| 30a61f4384 | |||
| 8e0a189d77 | |||
| cae44c9732 | |||
| fba8f0d017 | |||
| 3f260ee777 | |||
| 1f6a931b02 | |||
| 07891d028b | |||
| 1f448d1bf0 | |||
| 7722986537 | |||
| 4b73168618 | |||
| 16cebc538f | |||
| e4aaba98ab | |||
| c190d8c44c | |||
| 5801f96f79 | |||
| 92039b855e | |||
| f145e00cc9 | |||
| 87e8df90f5 | |||
| 419fab8193 |
@@ -0,0 +1,8 @@
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
informational: true
|
||||
patch:
|
||||
default:
|
||||
informational: true
|
||||
@@ -7,13 +7,9 @@ on:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
wxt:
|
||||
name: WXT
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
checks:
|
||||
name: Checks
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
@@ -46,11 +42,41 @@ jobs:
|
||||
pnpm build:all:firefox-mv3
|
||||
pnpm tsc --noEmit
|
||||
pnpm wxt zip
|
||||
pnpm vitest run
|
||||
working-directory: demo
|
||||
|
||||
- name: Tests
|
||||
run: pnpm test:coverage --reporter=default --reporter=hanging-process
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
windows-tests:
|
||||
name: Windows Tests
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup PNPM
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- name: Setup NodeJS
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Tests
|
||||
run: pnpm test:coverage --reporter=default --reporter=hanging-process
|
||||
|
||||
project-templates:
|
||||
name: Project Templates
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
+3
-1
@@ -17,4 +17,6 @@ web-ext.config.ts
|
||||
templates/*/pnpm-lock.yaml
|
||||
templates/*/yarn.lock
|
||||
templates/*/package-lock.json
|
||||
docs/config.md
|
||||
docs/api
|
||||
stats.html
|
||||
.tool-versions
|
||||
|
||||
@@ -10,3 +10,7 @@ CHANGELOG.md
|
||||
.prettierignore
|
||||
*.png
|
||||
*.svg
|
||||
*.txt
|
||||
_gitignore
|
||||
_redirects
|
||||
*.svelte
|
||||
|
||||
+322
-96
@@ -1,5 +1,326 @@
|
||||
# Changelog
|
||||
|
||||
## v0.8.6
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.8.5...v0.8.6)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Inline WXT modules inside `WxtVitest` plugin ([b75c553](https://github.com/wxt-dev/wxt/commit/b75c553))
|
||||
|
||||
## v0.8.5
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.8.4...v0.8.5)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Refactor project structure to export `initialize`, `prepare`, and `zip` functions ([#182](https://github.com/wxt-dev/wxt/pull/182))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Enable Vue SFC auto-imports in `vue` template ([f8a0fb3](https://github.com/wxt-dev/wxt/commit/f8a0fb3))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Improve `runner.binaries` documentation ([d9e9b43](https://github.com/wxt-dev/wxt/commit/d9e9b43))
|
||||
- Update auto-imports.md ([#186](https://github.com/wxt-dev/wxt/pull/186))
|
||||
- Add `test.server.deps.inline` to Vitest guide ([19756c6](https://github.com/wxt-dev/wxt/commit/19756c6))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Update template docs ([2e24b9e](https://github.com/wxt-dev/wxt/commit/2e24b9e))
|
||||
- Reduce package size by 70%, 1.92 MB to 590 kB ([#190](https://github.com/wxt-dev/wxt/pull/190))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Nenad Novaković
|
||||
|
||||
## v0.8.4
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.8.3...v0.8.4)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Allow actions without a popup ([#181](https://github.com/wxt-dev/wxt/pull/181))
|
||||
|
||||
## v0.8.3
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.8.2...v0.8.3)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add testing utils under `wxt/testing` ([#178](https://github.com/wxt-dev/wxt/pull/178))
|
||||
|
||||
## v0.8.2
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.8.1...v0.8.2)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- **firefox:** Stop extending `AbortController` to fix crash in content scripts ([#176](https://github.com/wxt-dev/wxt/pull/176))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Improve output consistency ([#175](https://github.com/wxt-dev/wxt/pull/175))
|
||||
|
||||
## v0.8.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.8.0...v0.8.1)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Output `action.browser_style` correctly ([6a93f20](https://github.com/wxt-dev/wxt/commit/6a93f20))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Generate full API docs with typedoc ([#174](https://github.com/wxt-dev/wxt/pull/174))
|
||||
|
||||
## v0.8.0
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.7.5...v0.8.0)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- ⚠️ Use `defineUnlistedScript` to define unlisted scripts ([#167](https://github.com/wxt-dev/wxt/pull/167))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Fix wrong links ([#166](https://github.com/wxt-dev/wxt/pull/166))
|
||||
|
||||
### 🌊 Types
|
||||
|
||||
- ⚠️ Rename `BackgroundScriptDefintition` to `BackgroundDefinition` ([446f265](https://github.com/wxt-dev/wxt/commit/446f265))
|
||||
|
||||
#### ⚠️ Breaking Changes
|
||||
|
||||
- ⚠️ Unlisted scripts must now `export default defineUnlistedScript(...)` ([#167](https://github.com/wxt-dev/wxt/pull/167))
|
||||
- ⚠️ Rename `BackgroundScriptDefintition` to `BackgroundDefinition` ([446f265](https://github.com/wxt-dev/wxt/commit/446f265))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- 渣渣120 <WOSHIZHAZHA120@qq.com>
|
||||
|
||||
## v0.7.5
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.7.4...v0.7.5)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- More consistent `version_name` generation between browsers ([#163](https://github.com/wxt-dev/wxt/pull/163))
|
||||
- Ignore non-manifest fields when merging content script entries ([#164](https://github.com/wxt-dev/wxt/pull/164))
|
||||
- Add `browser_style` to popup options ([#165](https://github.com/wxt-dev/wxt/pull/165))
|
||||
|
||||
## v0.7.4
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.7.3...v0.7.4)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Support `react-refresh` when pre-rendering HTML pages in dev mode ([#158](https://github.com/wxt-dev/wxt/pull/158))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Add migration guides ([b58fb02](https://github.com/wxt-dev/wxt/commit/b58fb02))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Upgrade templates to v0.7 ([#156](https://github.com/wxt-dev/wxt/pull/156))
|
||||
|
||||
## v0.7.3
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.7.2...v0.7.3)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Support JS entrypoints ([#155](https://github.com/wxt-dev/wxt/pull/155))
|
||||
|
||||
## v0.7.2
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.7.1...v0.7.2)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Allow customizing entrypoint options per browser ([#154](https://github.com/wxt-dev/wxt/pull/154))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Default safari to MV2 ([5807931](https://github.com/wxt-dev/wxt/commit/5807931))
|
||||
- Add missing `persistent` type to `defineBackgroundScript` ([d9fdcb5](https://github.com/wxt-dev/wxt/commit/d9fdcb5))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Restructure website to improve UX ([#149](https://github.com/wxt-dev/wxt/pull/149))
|
||||
- Add docs for development and testing ([f58d69d](https://github.com/wxt-dev/wxt/commit/f58d69d))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **deps-dev:** Bump @types/fs-extra from 11.0.1 to 11.0.2 ([#144](https://github.com/wxt-dev/wxt/pull/144))
|
||||
- **deps-dev:** Bump @faker-js/faker from 8.0.2 to 8.1.0 ([#146](https://github.com/wxt-dev/wxt/pull/146))
|
||||
- **deps-dev:** Bump vitest-mock-extended from 1.2.1 to 1.3.0 ([#147](https://github.com/wxt-dev/wxt/pull/147))
|
||||
- **deps-dev:** Bump vitest from 0.34.3 to 0.34.6 ([#145](https://github.com/wxt-dev/wxt/pull/145))
|
||||
- **deps-dev:** Bump typescript from 5.1 to 5.2 ([#148](https://github.com/wxt-dev/wxt/pull/148))
|
||||
|
||||
## v0.7.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.7.0...v0.7.1)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- `createContentScriptUi` helper ([#143](https://github.com/wxt-dev/wxt/pull/143))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Add docs for `createContentScriptUi` ([65fcfc0](https://github.com/wxt-dev/wxt/commit/65fcfc0))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **release:** V0.7.1-alpha1 ([2d4983e](https://github.com/wxt-dev/wxt/commit/2d4983e))
|
||||
|
||||
## v0.7.0
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.6.6...v0.7.0)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Content script `cssInjectionMode` ([#141](https://github.com/wxt-dev/wxt/pull/141))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Validate transformed manifest correctly ([4b2012c](https://github.com/wxt-dev/wxt/commit/4b2012c))
|
||||
- ⚠️ Output content script CSS to `content-scripts/<name>.css` ([#140](https://github.com/wxt-dev/wxt/pull/140))
|
||||
- Reorder typescript paths to give priority to `@` and `~` over `@@` and `~~` ([#142](https://github.com/wxt-dev/wxt/pull/142))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Store user config metadata in memory ([0591050](https://github.com/wxt-dev/wxt/commit/0591050))
|
||||
|
||||
#### ⚠️ Breaking Changes
|
||||
|
||||
- ⚠️ Content script CSS used to be output to `assets/<name>.css`, but is now `content-scripts/<name>.css` to match the docs. ([#140](https://github.com/wxt-dev/wxt/pull/140))
|
||||
|
||||
## v0.6.6
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.6.5...v0.6.6)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Disable opening browser automatically during dev mode ([#136](https://github.com/wxt-dev/wxt/pull/136))
|
||||
|
||||
## v0.6.5
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.6.4...v0.6.5)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Don't crash when `<all_urls>` matches is used in dev mode ([b48cee9](https://github.com/wxt-dev/wxt/commit/b48cee9))
|
||||
- Support loading `tsx` entrypoints ([#134](https://github.com/wxt-dev/wxt/pull/134))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Add tags for SEO and socials ([96be879](https://github.com/wxt-dev/wxt/commit/96be879))
|
||||
- Add more content to the homepage ([5570793](https://github.com/wxt-dev/wxt/commit/5570793))
|
||||
- Fix DX section sizing ([41e1549](https://github.com/wxt-dev/wxt/commit/41e1549))
|
||||
- Add link to update extensions using WXT ([24e69fe](https://github.com/wxt-dev/wxt/commit/24e69fe))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Code coverage improvements ([#131](https://github.com/wxt-dev/wxt/pull/131))
|
||||
|
||||
## v0.6.4
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.6.3...v0.6.4)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- **content-scripts:** Don't throw an error when including `include` or `exclude` options on a content script ([455e7f3](https://github.com/wxt-dev/wxt/commit/455e7f3))
|
||||
- Use `execaCommand` instead of `node:child_process` ([#130](https://github.com/wxt-dev/wxt/pull/130))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **templates:** Add `.wxt` directory to gitignore ([#129](https://github.com/wxt-dev/wxt/pull/129))
|
||||
- Increase E2E test timeout ([5482b2f](https://github.com/wxt-dev/wxt/commit/5482b2f))
|
||||
|
||||
## v0.6.3
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.6.2...v0.6.3)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- **client:** Add `block` and `addEventListener` utils to `ContentScriptContext` ([#128](https://github.com/wxt-dev/wxt/pull/128))
|
||||
|
||||
## v0.6.2
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.6.1...v0.6.2)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- `--analyze` build flag ([#125](https://github.com/wxt-dev/wxt/pull/125))
|
||||
- Show spinner when building entrypoints ([#126](https://github.com/wxt-dev/wxt/pull/126))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Fix import typo ([4c43072](https://github.com/wxt-dev/wxt/commit/4c43072))
|
||||
- Update vite docs to use function ([e0929a6](https://github.com/wxt-dev/wxt/commit/e0929a6))
|
||||
|
||||
## v0.6.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.6.0...v0.6.1)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add `transformManifest` option ([#124](https://github.com/wxt-dev/wxt/pull/124))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Don't open browser during development when using WSL ([#123](https://github.com/wxt-dev/wxt/pull/123))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Load extension details from CWS ([8e0a189](https://github.com/wxt-dev/wxt/commit/8e0a189))
|
||||
|
||||
## v0.6.0
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.6...v0.6.0)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Export `ContentScriptContext` from `wxt/client` ([1f448d1](https://github.com/wxt-dev/wxt/commit/1f448d1))
|
||||
- ⚠️ Require a function for `vite` configuration ([#121](https://github.com/wxt-dev/wxt/pull/121))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Use the same mode for each build step ([1f6a931](https://github.com/wxt-dev/wxt/commit/1f6a931))
|
||||
- Disable dev logs in production ([3f260ee](https://github.com/wxt-dev/wxt/commit/3f260ee))
|
||||
|
||||
#### ⚠️ Breaking Changes
|
||||
|
||||
- ⚠️ The `vite` config option must now be a function. If you were using an object before, change it from `vite: { ... }` to `vite: () => ({ ... })`. ([#121](https://github.com/wxt-dev/wxt/pull/121))
|
||||
|
||||
## v0.5.6
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.5...v0.5.6)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add `ContentScriptContext` util for stopping invalidated content scripts ([#120](https://github.com/wxt-dev/wxt/pull/120))
|
||||
|
||||
## v0.5.5
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.4...v0.5.5)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Automatically replace vite's `process.env.NODE_ENV` output in lib mode with the mode ([92039b8](https://github.com/wxt-dev/wxt/commit/92039b8))
|
||||
|
||||
## v0.5.4
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.3...v0.5.4)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Recognize `background/index.ts` as an entrypoint ([419fab8](https://github.com/wxt-dev/wxt/commit/419fab8))
|
||||
- Don't warn about deep entrypoint subdirectories not being recognized ([87e8df9](https://github.com/wxt-dev/wxt/commit/87e8df9))
|
||||
|
||||
## v0.5.3
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.2...v0.5.3)
|
||||
@@ -12,11 +333,6 @@
|
||||
|
||||
- Refactor how config is resolved ([#118](https://github.com/wxt-dev/wxt/pull/118))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron <aaronklinker1@gmail.com>
|
||||
- Aaron Klinker <aaronklinker1@gmail.com>
|
||||
|
||||
## v0.5.2
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.1...v0.5.2)
|
||||
@@ -25,10 +341,6 @@
|
||||
|
||||
- Import client utils when getting entrypoint config ([#117](https://github.com/wxt-dev/wxt/pull/117))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron <aaronklinker1@gmail.com>
|
||||
|
||||
## v0.5.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.0...v0.5.1)
|
||||
@@ -42,11 +354,6 @@
|
||||
|
||||
- Allow any string for target browser ([b4de93d](https://github.com/wxt-dev/wxt/commit/b4de93d))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron <aaronklinker1@gmail.com>
|
||||
- Aaron Klinker <aaronklinker1@gmail.com>
|
||||
|
||||
## v0.5.0
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.4.1...v0.5.0)
|
||||
@@ -76,10 +383,6 @@
|
||||
|
||||
- ⚠️ Change default `publicDir` to `<srcDir>/public` ([5f15f9c](https://github.com/wxt-dev/wxt/commit/5f15f9c))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.4.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.4.0...v0.4.1)
|
||||
@@ -92,10 +395,6 @@
|
||||
|
||||
- **init:** Don't show `cd .` when initializing the current directory ([e086374](https://github.com/wxt-dev/wxt/commit/e086374))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.4.0
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.3.2...v0.4.0)
|
||||
@@ -145,10 +444,6 @@
|
||||
|
||||
- ⚠️ Use relative path aliases inside `.wxt/tsconfig.json` ([#102](https://github.com/wxt-dev/wxt/pull/102))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.3.2
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.3.1...v0.3.2)
|
||||
@@ -178,10 +473,6 @@
|
||||
- List vite version when validating project templates ([ef140dc](https://github.com/wxt-dev/wxt/commit/ef140dc))
|
||||
- Validate templates using tarball to avoid version conflicts within the `wxt/node_modules` directory ([edfa075](https://github.com/wxt-dev/wxt/commit/edfa075))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.3.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.3.0...v0.3.1)
|
||||
@@ -213,9 +504,8 @@
|
||||
- Speed up demo validation ([3a9fd39](https://github.com/wxt-dev/wxt/commit/3a9fd39))
|
||||
- Fix flakey failure when validating templates ([25677ba](https://github.com/wxt-dev/wxt/commit/25677ba))
|
||||
|
||||
### ❤️ Contributors
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
- BeanWei ([@BeanWei](https://github.com/BeanWei))
|
||||
|
||||
## v0.3.0
|
||||
@@ -249,10 +539,6 @@
|
||||
- ⚠️ Add type safety to `browser.runtime.getURL` ([58a84ec](https://github.com/wxt-dev/wxt/commit/58a84ec))
|
||||
- ⚠️ Change default `publicDir` to `<rootDir>/public` ([19c0948](https://github.com/wxt-dev/wxt/commit/19c0948))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.2.5
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.2.4...v0.2.5)
|
||||
@@ -275,10 +561,6 @@
|
||||
|
||||
- Validate project templates ([9ac756f](https://github.com/wxt-dev/wxt/commit/9ac756f))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.2.4
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.2.3...v0.2.4)
|
||||
@@ -287,10 +569,6 @@
|
||||
|
||||
- Add `wxt zip` command ([#47](https://github.com/wxt-dev/wxt/pull/47))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.2.3
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.2.2...v0.2.3)
|
||||
@@ -301,10 +579,6 @@
|
||||
- Read boolean maniest options from meta tags correctly ([495c5c8](https://github.com/wxt-dev/wxt/commit/495c5c8))
|
||||
- Some fields cannot be overridden from `config.manifest` ([#46](https://github.com/wxt-dev/wxt/pull/46))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.2.2
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.2.1...v0.2.2)
|
||||
@@ -313,10 +587,6 @@
|
||||
|
||||
- Register content scripts correctly in dev mode ([2fb5a54](https://github.com/wxt-dev/wxt/commit/2fb5a54))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.2.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.2.0...v0.2.1)
|
||||
@@ -329,10 +599,6 @@
|
||||
|
||||
- Remove HMR log ([90fa6bf](https://github.com/wxt-dev/wxt/commit/90fa6bf))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.2.0
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.1.6...v0.2.0)
|
||||
@@ -357,10 +623,6 @@
|
||||
|
||||
- ⚠️ Rename `defineBackgroundScript` to `defineBackground` ([5b48ae9](https://github.com/wxt-dev/wxt/commit/5b48ae9))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.1.6
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.1.5...v0.1.6)
|
||||
@@ -375,10 +637,6 @@
|
||||
- React and Vue starter templates ([#33](https://github.com/wxt-dev/wxt/pull/33))
|
||||
- Svelte template ([#34](https://github.com/wxt-dev/wxt/pull/34))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.1.5
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.1.4...v0.1.5)
|
||||
@@ -387,10 +645,6 @@
|
||||
|
||||
- Include `vite/client` types ([371be99](https://github.com/wxt-dev/wxt/commit/371be99))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.1.4
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.1.3...v0.1.4)
|
||||
@@ -408,10 +662,6 @@
|
||||
|
||||
- Update prettier ignore ([68611ae](https://github.com/wxt-dev/wxt/commit/68611ae))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.1.3
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.1.2...v0.1.3)
|
||||
@@ -433,10 +683,6 @@
|
||||
|
||||
- Upgrade to pnpm 8 ([0ce7c9d](https://github.com/wxt-dev/wxt/commit/0ce7c9d))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.1.2
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.1.1...v0.1.2)
|
||||
@@ -455,10 +701,6 @@
|
||||
|
||||
- Change `config.vite` to `UserConfig` ([ef6001e](https://github.com/wxt-dev/wxt/commit/ef6001e))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.1.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.1.0...v0.1.1)
|
||||
@@ -468,10 +710,6 @@
|
||||
- Allow dashes in entrypoint names ([2e51e73](https://github.com/wxt-dev/wxt/commit/2e51e73))
|
||||
- Unable to read entrypoint options ([#28](https://github.com/wxt-dev/wxt/pull/28))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.1.0
|
||||
|
||||
Initial release of WXT. Full support for production builds and initial toolkit for development:
|
||||
@@ -495,10 +733,6 @@ Initial release of WXT. Full support for production builds and initial toolkit f
|
||||
|
||||
- Create github release ([b7c078f](https://github.com/wxt-dev/wxt/commit/b7c078f))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.0.2
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.0.1...v0.0.2)
|
||||
@@ -529,10 +763,6 @@ Initial release of WXT. Full support for production builds and initial toolkit f
|
||||
- Rename `templates` to `virtual-modules` ([#24](https://github.com/wxt-dev/wxt/pull/24))
|
||||
- Update cli screenshot ([54eb118](https://github.com/wxt-dev/wxt/commit/54eb118))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.0.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.0.0...v0.0.1)
|
||||
@@ -602,7 +832,3 @@ Initial release of WXT. Full support for production builds and initial toolkit f
|
||||
|
||||
- Create validation workflow ([#12](https://github.com/wxt-dev/wxt/pull/12))
|
||||
- Create release workflow ([#13](https://github.com/wxt-dev/wxt/pull/13))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
@@ -70,3 +70,37 @@ pnpm test manifest-contents
|
||||
Unit and E2E tests are ran together via [Vitest workspaces](https://vitest.dev/guide/#workspaces-support).
|
||||
|
||||
If you want to manually test a change, you can modify the demo project for your test, but please don't leave those changes committed once you open a PR.
|
||||
|
||||
## Templates
|
||||
|
||||
Each directory inside `templates/` is it's own standalone project. Simply `cd` into the directory you're updating, install dependencies with `npm` (NOT `pnpm`), and run the relevant commands
|
||||
|
||||
```sh
|
||||
cd templates/vue
|
||||
npm i
|
||||
npm run dev
|
||||
npm run build
|
||||
```
|
||||
|
||||
Note that templates are hardcoded to a specific version of `wxt` from NPM, they do not use the local version. PR checks will test your changes against the templates, but if you want to manually do it, update the package.json dependency:
|
||||
|
||||
```diff
|
||||
"devDependencies": {
|
||||
"typescript": "^5.1.6",
|
||||
"vite-plugin-solid": "^2.7.0",
|
||||
- "wxt": "^0.8.0"
|
||||
+ "wxt": "../.."
|
||||
}
|
||||
```
|
||||
|
||||
Then run `npm i` again.
|
||||
|
||||
### Adding Templates
|
||||
|
||||
To add a template, copy the vanilla template and give it a new name.
|
||||
|
||||
```sh
|
||||
cp -r templates/vailla templates/<new-template-name>
|
||||
```
|
||||
|
||||
That's it. Once your template is merged, it will be available inside `wxt init` immediately. You don't need to release a new version of WXT to release a new template.
|
||||
|
||||
@@ -3,6 +3,21 @@
|
||||
<span>WXT</span>
|
||||
</h1>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/package/wxt" target="_blank">
|
||||
<img alt="npm" src="https://img.shields.io/npm/v/wxt?labelColor=black&color=%234fa048">
|
||||
</a>
|
||||
<span> </span>
|
||||
<img alt="npm" src="https://img.shields.io/npm/dm/wxt?labelColor=black&color=%234fa048">
|
||||
<span> </span>
|
||||
<img alt="NPM" src="https://img.shields.io/npm/l/wxt?labelColor=black&color=%234fa048">
|
||||
<!-- Hide code coverage while it's broken -->
|
||||
<!-- <span> </span>
|
||||
<a href="https://codecov.io/github/wxt-dev/wxt" target="_blank">
|
||||
<img alt="Codecov" src="https://img.shields.io/codecov/c/github/wxt-dev/wxt?labelColor=black&color=%234fa048">
|
||||
</a> -->
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<span>Next-gen framework for developing web extensions.</span>
|
||||
<br/>
|
||||
@@ -14,11 +29,11 @@
|
||||
<p align="center">
|
||||
<a href="https://wxt.dev" target="_blank">Get Started</a>
|
||||
•
|
||||
<a href="https://wxt.dev/get-started/installation.html" target="_blank">Installation</a>
|
||||
<a href="https://wxt.dev/guide/installation.html" target="_blank">Installation</a>
|
||||
•
|
||||
<a href="https://wxt.dev/guide/background.html" target="_blank">Entrypoints</a>
|
||||
<a href="https://wxt.dev/entrypoints/background.html" target="_blank">Entrypoints</a>
|
||||
•
|
||||
<a href="https://wxt.dev/config.html" target="_blank">Configuration</a>
|
||||
<a href="https://wxt.dev/api/config.html" target="_blank">Configuration</a>
|
||||
</p>
|
||||
|
||||

|
||||
@@ -31,7 +46,7 @@ Bootstrap a new project:
|
||||
pnpx wxt@latest init <project-name>
|
||||
```
|
||||
|
||||
Or see the [installation guide](https://wxt.dev/get-started/installation.html) to get started with WXT.
|
||||
Or see the [installation guide](https://wxt.dev/guide/installation.html) to get started with WXT.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -44,10 +59,10 @@ Or see the [installation guide](https://wxt.dev/get-started/installation.html) t
|
||||
- ⬇️ Download and bundle remote URL imports
|
||||
- 🎨 Frontend framework agnostic: works with Vue, React, Svelte, etc
|
||||
- 🖍️ Quickly bootstrap a new project
|
||||
- 📏 Bundle analysis
|
||||
|
||||
### Coming Soon
|
||||
|
||||
- 📏 Bundle analysis
|
||||
- 🤖 Automated publishing
|
||||
|
||||
## Contributors
|
||||
|
||||
@@ -11,14 +11,20 @@
|
||||
"build:all:chrome-mv2": "wxt build --mv2",
|
||||
"build:all:firefox-mv3": "wxt build -b firefox --mv3",
|
||||
"build:all:firefox-mv2": "wxt build -b firefox",
|
||||
"test": "pnpm -w build && vitest",
|
||||
"zip": "pnpm -w build && wxt zip",
|
||||
"compile": "pnpm -w build && tsc --noEmit",
|
||||
"postinstall": "pnpm -w build && wxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"vitest": "^0.34.6",
|
||||
"webextension-polyfill": "^0.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.14",
|
||||
"@types/react-dom": "^18.2.6",
|
||||
"@types/webextension-polyfill": "^0.10.0",
|
||||
"sass": "^1.64.0",
|
||||
"wxt": "workspace:*"
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import background from '../background';
|
||||
|
||||
browser.i18n.getMessage = () => 'fake-message';
|
||||
|
||||
const logMock = vi.fn();
|
||||
console.log = logMock;
|
||||
|
||||
describe('Background Entrypoint', () => {
|
||||
it("should log the extenion's runtime ID", () => {
|
||||
const id = 'some-id';
|
||||
fakeBrowser.runtime.id = id;
|
||||
|
||||
background.main();
|
||||
|
||||
expect(logMock).toBeCalledWith(id);
|
||||
});
|
||||
});
|
||||
@@ -19,4 +19,6 @@ export default defineBackground(() => {
|
||||
browser.i18n.getMessage('hello', 'Aaron');
|
||||
browser.i18n.getMessage('bye', ['Aaron']);
|
||||
browser.i18n.getMessage('@@extension_id');
|
||||
|
||||
console.log('WXT MODE:', import.meta.env.MODE);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['<all_urls>'],
|
||||
async main(ctx) {
|
||||
console.log(browser.runtime.id);
|
||||
logId();
|
||||
|
||||
console.log('WXT MODE:', import.meta.env.MODE);
|
||||
|
||||
const n = (Math.random() * 100).toFixed(1);
|
||||
ctx.setInterval(() => {
|
||||
console.log(n, browser.runtime.id);
|
||||
}, 1e3);
|
||||
|
||||
const container = document.createElement('div');
|
||||
document.body.append(container);
|
||||
|
||||
ReactDOM.createRoot(container).render(<SomeComponent />);
|
||||
},
|
||||
});
|
||||
|
||||
function SomeComponent() {
|
||||
return <div>Some component</div>;
|
||||
}
|
||||
@@ -3,3 +3,5 @@ import 'url:https://code.jquery.com/jquery-3.7.1.slim.min.js';
|
||||
console.log(browser.runtime.id);
|
||||
logId();
|
||||
console.log(2);
|
||||
|
||||
console.log('WXT MODE:', import.meta.env.MODE);
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import '../../common/style.css';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['*://*/*'],
|
||||
async main() {
|
||||
console.log(browser.runtime.id);
|
||||
logId();
|
||||
mountContentScriptUi();
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import '../../common/style.css';
|
||||
import './style.css';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['https://*.duckduckgo.com/*'],
|
||||
cssInjectionMode: 'ui',
|
||||
|
||||
async main(ctx) {
|
||||
const ui = await createContentScriptUi(ctx, {
|
||||
name: 'demo-ui',
|
||||
type: 'inline',
|
||||
append: 'before',
|
||||
anchor: 'form[role=search]',
|
||||
mount: (container) => {
|
||||
const app = document.createElement('div');
|
||||
app.textContent = 'Custom content script UI';
|
||||
container.append(app);
|
||||
},
|
||||
});
|
||||
ui.mount();
|
||||
|
||||
setTimeout(ui.remove, 5000);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
color: indianred;
|
||||
}
|
||||
html {
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
div {
|
||||
padding: 16px;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default defineUnlistedScript(() => {
|
||||
console.log('injected');
|
||||
});
|
||||
+5
-1
@@ -1,3 +1,7 @@
|
||||
{
|
||||
"extends": ["../tsconfig.base.json", "./.wxt/tsconfig.json"]
|
||||
"extends": ["../tsconfig.base.json", "./.wxt/tsconfig.json"],
|
||||
"compilerOptions": {
|
||||
"allowImportingTsExtensions": true,
|
||||
"jsx": "react-jsx"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { WxtVitest } from 'wxt/testing';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
mockReset: true,
|
||||
restoreMocks: true,
|
||||
},
|
||||
plugins: [WxtVitest()],
|
||||
});
|
||||
@@ -1,26 +1,68 @@
|
||||
<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',
|
||||
},
|
||||
import { computed } from 'vue';
|
||||
import useListExtensionDetails, {
|
||||
ChromeExtension,
|
||||
} from '../composables/useListExtensionDetails';
|
||||
|
||||
// Add extension IDs here. Order doesn't matter, will be sorted by weekly active users
|
||||
const chromeExtensionIds = [
|
||||
'ocfdgncpifmegplaglcnglhioflaimkd', // GitHub: Better Line Counts
|
||||
'mgmdkjcljneegjfajchedjpdhbadklcf', // Anime Skip Player
|
||||
];
|
||||
|
||||
const { data } = useListExtensionDetails(chromeExtensionIds);
|
||||
const sortedExtensions = computed(() =>
|
||||
!data.value
|
||||
? undefined
|
||||
: [...data.value].sort((l, r) => r.weeklyActiveUsers - l.weeklyActiveUsers),
|
||||
);
|
||||
|
||||
function getStoreUrl(extension: ChromeExtension) {
|
||||
const url = new URL(extension.storeUrl);
|
||||
url.searchParams.set('utm_source', 'wxt.dev');
|
||||
return url.href;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="vp-doc">
|
||||
<div class="container">
|
||||
<h2>Who's Using WXT?</h2>
|
||||
<p>
|
||||
Battle tested and ready for production. Explore chrome extensions made
|
||||
with WXT.
|
||||
</p>
|
||||
<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 v-for="extension of sortedExtensions">
|
||||
<img
|
||||
:src="extension.iconUrl"
|
||||
:alt="`${extension.name} icon`"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
<div>
|
||||
<a
|
||||
:href="getStoreUrl(extension)"
|
||||
target="_blank"
|
||||
:title="extension.name"
|
||||
>{{ extension.name }}</a
|
||||
>
|
||||
<p class="description" :title="extension.shortDescription">
|
||||
{{ extension.shortDescription }}
|
||||
</p>
|
||||
<p class="user-count">
|
||||
{{ extension.weeklyActiveUsers.toLocaleString() }} users
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<p>Open a PR to add your extension to the list!</p>
|
||||
<p class="centered pr">
|
||||
<a
|
||||
href="https://github.com/wxt-dev/wxt/edit/main/docs/.vitepress/components/UsingWxtSection.vue"
|
||||
target="_blank"
|
||||
>Open a PR</a
|
||||
>
|
||||
to add your extension to the list!
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -30,10 +72,6 @@ const extensions = [
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.vp-doc {
|
||||
padding: 0 48px;
|
||||
@@ -53,57 +91,86 @@ h2 {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
margin-bottom: 16px;
|
||||
li img {
|
||||
width: 116px;
|
||||
height: 116px;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
background-color: var(--vp-c-default-soft);
|
||||
}
|
||||
|
||||
ul {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
align-items: stretch;
|
||||
gap: 16px;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
margin: 16px 0;
|
||||
padding: 0;
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
ul {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
ul {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 0 !important;
|
||||
padding: 12px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
flex: 1;
|
||||
}
|
||||
a,
|
||||
small {
|
||||
text-align: center;
|
||||
}
|
||||
small {
|
||||
opacity: 50%;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
p {
|
||||
.centered {
|
||||
text-align: center;
|
||||
opacity: 50%;
|
||||
}
|
||||
a {
|
||||
color: var(--vp-c-text-1);
|
||||
|
||||
li a,
|
||||
li .user-count,
|
||||
li .description {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
li .user-count {
|
||||
opacity: 70%;
|
||||
text-align: right;
|
||||
width: 100%;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
li a {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
li a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
li div {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
li .description {
|
||||
opacity: 90%;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.pr {
|
||||
opacity: 70%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
export interface ChromeExtension {
|
||||
id: string;
|
||||
name: string;
|
||||
iconUrl: string;
|
||||
weeklyActiveUsers: number;
|
||||
shortDescription: string;
|
||||
storeUrl: string;
|
||||
}
|
||||
|
||||
const operationName = 'WxtDocsUsedBy';
|
||||
const query = `query ${operationName}($ids:[String!]!) {
|
||||
chromeExtensions(ids: $ids) {
|
||||
id
|
||||
name
|
||||
iconUrl
|
||||
weeklyActiveUsers
|
||||
shortDescription
|
||||
storeUrl
|
||||
}
|
||||
}`;
|
||||
|
||||
export default function (ids: string[]) {
|
||||
const data = ref<ChromeExtension[]>();
|
||||
const err = ref<unknown>();
|
||||
|
||||
fetch('https://queue.wxt.dev/api', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
operationName,
|
||||
query,
|
||||
variables: { ids },
|
||||
}),
|
||||
})
|
||||
.then(async (res) => {
|
||||
const {
|
||||
data: { chromeExtensions },
|
||||
} = await res.json();
|
||||
data.value = chromeExtensions;
|
||||
err.value = undefined;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
data.value = undefined;
|
||||
err.value = error;
|
||||
});
|
||||
|
||||
return {
|
||||
data,
|
||||
err,
|
||||
};
|
||||
}
|
||||
+103
-38
@@ -1,19 +1,66 @@
|
||||
import { defineConfig } from 'vitepress';
|
||||
import { generateConfigDocs } from './plugins/generate-config-docs';
|
||||
import { DefaultTheme, defineConfig } from 'vitepress';
|
||||
import { generateCliDocs } from './plugins/generate-cli-docs';
|
||||
import typedocSidebar from '../api/typedoc-sidebar.json';
|
||||
|
||||
const filteredTypedocSidebar = typedocSidebar.filter(
|
||||
(item) => item.text !== 'API',
|
||||
);
|
||||
// Typedoc's markdown theme adds collapse: true to all our items, event ones without any children,
|
||||
// so they need to be removed.
|
||||
function removeCollapsedWithNoItems(items: DefaultTheme.SidebarItem[]) {
|
||||
for (const item of items) {
|
||||
if (item.items) removeCollapsedWithNoItems(item.items);
|
||||
else delete item.collapsed;
|
||||
}
|
||||
}
|
||||
removeCollapsedWithNoItems(filteredTypedocSidebar);
|
||||
|
||||
const title = 'Next-gen Web Extension Framework';
|
||||
const titleSuffix = ' – WXT';
|
||||
|
||||
const description =
|
||||
"WXT provides the best developer experience, making it quick, easy, and fun to develop chrome extensions for all browsers. With built-in utilties for building, zipping, and publishing your extension, it's easy to get started.";
|
||||
const ogTitle = `${title}${titleSuffix}`;
|
||||
const ogUrl = 'https://wxt.dev';
|
||||
const ogImage = 'https://wxt.dev/social-preview.png';
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
titleTemplate: `:title${titleSuffix}`,
|
||||
title: 'WXT',
|
||||
description,
|
||||
vite: {
|
||||
clearScreen: false,
|
||||
plugins: [generateConfigDocs()],
|
||||
plugins: [generateCliDocs()],
|
||||
},
|
||||
description: 'Next gen framework for developing web extensions',
|
||||
lastUpdated: true,
|
||||
sitemap: {
|
||||
hostname: 'https://wxt.dev',
|
||||
},
|
||||
|
||||
head: [
|
||||
['meta', { property: 'og:type', content: 'website' }],
|
||||
['meta', { property: 'og:title', content: ogTitle }],
|
||||
['meta', { property: 'og:image', content: ogImage }],
|
||||
['meta', { property: 'og:url', content: ogUrl }],
|
||||
['meta', { property: 'og:description', content: description }],
|
||||
['meta', { name: 'twitter:card', content: 'summary_large_image' }],
|
||||
[
|
||||
'script',
|
||||
{
|
||||
async: '',
|
||||
'data-website-id': 'c1840c18-a12c-4a45-a848-55ae85ef7915',
|
||||
src: 'https://umami.aklinker1.io/script.js',
|
||||
},
|
||||
],
|
||||
],
|
||||
|
||||
themeConfig: {
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
logo: '/logo.svg',
|
||||
logo: {
|
||||
src: '/logo.svg',
|
||||
alt: 'WXT logo',
|
||||
},
|
||||
editLink: {
|
||||
pattern: 'https://github.com/wxt-dev/wxt/edit/main/docs/:path',
|
||||
},
|
||||
@@ -22,56 +69,74 @@ export default defineConfig({
|
||||
},
|
||||
|
||||
nav: [
|
||||
{ text: 'Get Started', link: '/get-started/installation.md' },
|
||||
{ text: 'Guide', link: '/guide/auto-imports.md' },
|
||||
{ text: 'Config', link: '/config.md' },
|
||||
{ text: 'API', link: '/api.md' },
|
||||
{ text: 'Guide', link: '/guide/installation.md' },
|
||||
{ text: 'Entrypoints', link: '/entrypoints/background.md' },
|
||||
{ text: 'API', link: '/api/cli.md' },
|
||||
],
|
||||
|
||||
sidebar: {
|
||||
'/get-started/': [
|
||||
{
|
||||
text: 'Get Started',
|
||||
items: [
|
||||
{ text: 'Introduction', link: '/get-started/introduction.md' },
|
||||
{ text: 'Installation', link: '/get-started/installation.md' },
|
||||
{ text: 'Configuration', link: '/get-started/configuration.md' },
|
||||
{ text: 'Entrypoints', link: '/get-started/entrypoints.md' },
|
||||
{ text: 'Assets', link: '/get-started/assets.md' },
|
||||
{ text: 'Build Targets', link: '/get-started/build-targets.md' },
|
||||
{ text: 'Publishing', link: '/get-started/publishing.md' },
|
||||
{ text: 'Testing', link: '/get-started/testing.md' },
|
||||
{ text: 'Compare', link: '/get-started/compare.md' },
|
||||
],
|
||||
},
|
||||
],
|
||||
'/guide/': [
|
||||
{
|
||||
text: 'Guide',
|
||||
items: [
|
||||
{ text: 'Introduction', link: '/guide/introduction.md' },
|
||||
{ text: 'Installation', link: '/guide/installation.md' },
|
||||
{ text: 'Configuration', link: '/guide/configuration.md' },
|
||||
{ text: 'Entrypoints', link: '/guide/entrypoints.md' },
|
||||
{ text: 'Assets', link: '/guide/assets.md' },
|
||||
{ text: 'Multiple Browsers', link: '/guide/multiple-browsers.md' },
|
||||
{ text: 'Publishing', link: '/guide/publishing.md' },
|
||||
{ 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: 'Development', link: '/guide/development.md' },
|
||||
{ text: 'Testing', link: '/guide/testing.md' },
|
||||
{ text: 'Vite', link: '/guide/vite.md' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Other',
|
||||
items: [
|
||||
{ text: 'Migrate to WXT', link: '/guide/migrate-to-wxt.md' },
|
||||
{ text: 'Compare', link: '/guide/compare.md' },
|
||||
],
|
||||
},
|
||||
],
|
||||
'/entrypoints/': [
|
||||
{
|
||||
text: 'Entrypoints',
|
||||
items: [
|
||||
{ text: 'Background', link: '/guide/background.md' },
|
||||
{ text: 'Bookmarks', link: '/guide/bookmarks.md' },
|
||||
{ text: 'Content Scripts', link: '/guide/content-scripts.md' },
|
||||
{ text: 'CSS', link: '/guide/css.md' },
|
||||
{ text: 'Devtools', link: '/guide/devtools.md' },
|
||||
{ text: 'History', link: '/guide/history.md' },
|
||||
{ text: 'Newtab', link: '/guide/newtab.md' },
|
||||
{ text: 'Options', link: '/guide/options.md' },
|
||||
{ text: 'Popup', link: '/guide/popup.md' },
|
||||
{ text: 'Sandbox', link: '/guide/sandbox.md' },
|
||||
{ text: 'Sidepanel', link: '/guide/sidepanel.md' },
|
||||
{ text: 'Unlisted Pages', link: '/guide/unlisted-pages.md' },
|
||||
{ text: 'Unlisted Scripts', link: '/guide/unlisted-scripts.md' },
|
||||
{ text: 'Background', link: '/entrypoints/background.md' },
|
||||
{ text: 'Bookmarks', link: '/entrypoints/bookmarks.md' },
|
||||
{
|
||||
text: 'Content Scripts',
|
||||
link: '/entrypoints/content-scripts.md',
|
||||
},
|
||||
{ text: 'CSS', link: '/entrypoints/css.md' },
|
||||
{ text: 'Devtools', link: '/entrypoints/devtools.md' },
|
||||
{ text: 'History', link: '/entrypoints/history.md' },
|
||||
{ text: 'Newtab', link: '/entrypoints/newtab.md' },
|
||||
{ text: 'Options', link: '/entrypoints/options.md' },
|
||||
{ text: 'Popup', link: '/entrypoints/popup.md' },
|
||||
{ text: 'Sandbox', link: '/entrypoints/sandbox.md' },
|
||||
{ text: 'Sidepanel', link: '/entrypoints/sidepanel.md' },
|
||||
{ text: 'Unlisted Pages', link: '/entrypoints/unlisted-pages.md' },
|
||||
{
|
||||
text: 'Unlisted Scripts',
|
||||
link: '/entrypoints/unlisted-scripts.md',
|
||||
},
|
||||
].sort((l, r) => l.text.localeCompare(r.text)),
|
||||
},
|
||||
],
|
||||
'/api/': [
|
||||
{
|
||||
items: [
|
||||
{ text: 'CLI', link: '/api/cli.md' },
|
||||
{
|
||||
text: 'Modules',
|
||||
items: filteredTypedocSidebar,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# CLI Reference
|
||||
|
||||
> Reference generated from `wxt <command> --help`
|
||||
|
||||
{{ DOCS }}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { relative, resolve } from 'node:path';
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { format } from 'prettier';
|
||||
import consola from 'consola';
|
||||
import { execaCommand } from 'execa';
|
||||
|
||||
let hasGenerated = false;
|
||||
|
||||
const cliDir = resolve('src/cli/commands');
|
||||
const cliDirGlob = resolve(cliDir, '**');
|
||||
const cliTemplatePath = resolve('docs/.vitepress/plugins/cli.tpl.md');
|
||||
const cliPath = resolve('docs/api/cli.md');
|
||||
|
||||
const PREFACE = `<!--
|
||||
DO NOT EDIT
|
||||
Generated by \`wxt/docs/.vitepress/plugins/generate-config-docs.ts\`
|
||||
To make changes to the config reference, update the JSDoc in \`src/core/types/external.ts\`.
|
||||
-->`;
|
||||
|
||||
export function generateCliDocs() {
|
||||
writeFileSync(cliPath, '');
|
||||
|
||||
const generateDocs = async () => {
|
||||
consola.info(`Generating ${relative(process.cwd(), cliPath)}`);
|
||||
try {
|
||||
const res = await execaCommand('pnpm -s wxt --help');
|
||||
const dev = splitInfo(res.stdout);
|
||||
const lines: Array<string | string[]> = [
|
||||
`## \`wxt\``,
|
||||
'```sh',
|
||||
dev.rest,
|
||||
'```',
|
||||
];
|
||||
|
||||
const commands = await Promise.all(
|
||||
extractCommands(dev.info).map(async (command) => {
|
||||
const res = await execaCommand(`pnpm -s wxt ${command} --help`);
|
||||
const { rest: docs } = splitInfo(res.stdout);
|
||||
return [`## \`wxt ${command}\``, '```sh', docs, '```'];
|
||||
}),
|
||||
);
|
||||
lines.push(...commands);
|
||||
|
||||
const text = await format(
|
||||
PREFACE +
|
||||
'\n\n' +
|
||||
readFileSync(cliTemplatePath, 'utf-8').replace(
|
||||
'{{ DOCS }}',
|
||||
lines.flat().join('\n'),
|
||||
),
|
||||
{ parser: 'markdown' },
|
||||
);
|
||||
|
||||
writeFileSync(cliPath, text);
|
||||
consola.success(`Generated ${relative(process.cwd(), cliPath)}`);
|
||||
} catch (err) {
|
||||
consola.fail(`Failed to generate ${relative(process.cwd(), cliPath)}`);
|
||||
consola.error(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
name: 'docs:generate-cli-docs',
|
||||
async config() {
|
||||
if (!hasGenerated) {
|
||||
hasGenerated = true;
|
||||
await generateDocs();
|
||||
}
|
||||
},
|
||||
configureServer(server: any) {
|
||||
server.watcher.add(cliDirGlob);
|
||||
},
|
||||
async handleHotUpdate(ctx: { file: string }) {
|
||||
if (ctx.file === cliTemplatePath || ctx.file.includes(cliDir)) {
|
||||
await generateDocs();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function splitInfo(text: string): { info: string; rest: string } {
|
||||
const infoStart = text.indexOf('For more info,');
|
||||
const infoEnd = text.indexOf('\n\nOptions:');
|
||||
|
||||
if (infoStart === -1 || infoEnd === -1) {
|
||||
return { info: '', rest: text };
|
||||
}
|
||||
|
||||
const info = text.substring(infoStart, infoEnd).trim();
|
||||
const rest = text.replace(info, '').trim().replace('\n\n\n\n', '\n\n');
|
||||
|
||||
return { info, rest };
|
||||
}
|
||||
|
||||
function extractCommands(info: string): string[] {
|
||||
const commands: string[] = [];
|
||||
|
||||
// Split the info by line and iterate through each line
|
||||
info.split('\n').forEach((line) => {
|
||||
// Use regex to capture the command after "$ wxt " and before "--help"
|
||||
const match = line.match(/\$ wxt (\w+) --help/);
|
||||
if (match && match[1]) {
|
||||
commands.push(match[1]);
|
||||
}
|
||||
});
|
||||
|
||||
return commands;
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
import { resolve } from 'node:path';
|
||||
import { Project, ts, Type, Node, JSDocableNode } from 'ts-morph';
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { format } from 'prettier';
|
||||
import consola from 'consola';
|
||||
|
||||
let hasGenerated = false;
|
||||
|
||||
const externalTypesPath = resolve('src/core/types/external.ts');
|
||||
const configTemplatePath = resolve('docs/config.tpl.md');
|
||||
const configPath = resolve('docs/config.md');
|
||||
|
||||
const PREFACE = `<!--
|
||||
DO NOT EDIT
|
||||
Generated by \`wxt/docs/.vitepress/plugins/generate-config-docs.ts\`
|
||||
To make changes to the config reference, update the JSDoc in \`src/core/types/external.ts\`.
|
||||
-->`;
|
||||
|
||||
/**
|
||||
* Custom property paths that should not be recursively inspected. Usually 3rd party types.
|
||||
*/
|
||||
const LEAF_PATHS = ['imports', 'vite', 'server'];
|
||||
|
||||
/**
|
||||
* Override any types that resolve to `import(...)` instead of their type names when calling
|
||||
* `type.getText()`.
|
||||
*
|
||||
* This also stops any further type inspection for objects, meaning the object will be documented,
|
||||
* not it's properties.
|
||||
*/
|
||||
const CUSTOM_TYPES = {
|
||||
manifest:
|
||||
'Manifest | Promise<Manifest> | () => Manifest | () => Promise<Manifest>',
|
||||
imports: "false | Partial<import('unimport').UnimportOptions>",
|
||||
vite: "Omit<import('vite').UserConfig, 'root' | 'configFile' | 'mode'>",
|
||||
};
|
||||
|
||||
export function generateConfigDocs() {
|
||||
writeFileSync(configPath, '');
|
||||
|
||||
const generateDocs = async () => {
|
||||
consola.info('Generating /config.md');
|
||||
try {
|
||||
const project = new Project({
|
||||
tsConfigFilePath: resolve('tsconfig.json'),
|
||||
});
|
||||
|
||||
// Load file containing "UserConfig"
|
||||
const externalTypesFile = project.addSourceFileAtPath(externalTypesPath);
|
||||
project.resolveSourceFileDependencies();
|
||||
|
||||
const inlineConfigInterface =
|
||||
externalTypesFile.getInterfaceOrThrow('InlineConfig');
|
||||
|
||||
const getDocsFor = (path: string[], node: Node<ts.Node>): string[] => {
|
||||
const pathStr = path.join('.');
|
||||
|
||||
let type: Type<ts.Type>;
|
||||
if (node.isKind(ts.SyntaxKind.InterfaceDeclaration)) {
|
||||
type = node.getType();
|
||||
} else if (node.isKind(ts.SyntaxKind.PropertySignature)) {
|
||||
type = node.getTypeNodeOrThrow()?.getType();
|
||||
} else if (node.isKind(ts.SyntaxKind.MethodSignature)) {
|
||||
type = node.getType();
|
||||
} else {
|
||||
throw Error('Unsupported type node: ' + node.getKindName());
|
||||
}
|
||||
|
||||
if (type.isObject() && !type.isArray() && !CUSTOM_TYPES[pathStr]) {
|
||||
return type.getProperties().flatMap((property) => {
|
||||
const childPath = [...path, property.getName()];
|
||||
|
||||
return getDocsFor(childPath, property.getDeclarations()[0]);
|
||||
});
|
||||
}
|
||||
|
||||
if ('getJsDocs' in node) {
|
||||
const lines: string[] = [];
|
||||
const docs = (node as unknown as JSDocableNode).getJsDocs();
|
||||
let typeText: string;
|
||||
if (CUSTOM_TYPES[pathStr]) {
|
||||
typeText = CUSTOM_TYPES[pathStr];
|
||||
} else if (type.isUnion() && !type.isBoolean()) {
|
||||
typeText = type
|
||||
.getUnionTypes()
|
||||
.map((type) => type.getText())
|
||||
.join(' | ');
|
||||
} else {
|
||||
typeText = type.getText();
|
||||
}
|
||||
const defaultValue = docs
|
||||
.flatMap((doc) => doc.getTags())
|
||||
.find((tag) => tag.getTagName() === 'default')
|
||||
?.getCommentText();
|
||||
lines.push(
|
||||
'',
|
||||
`## ${pathStr}`,
|
||||
'',
|
||||
`- **Type**: \`${typeText}\``,
|
||||
`- **Default**: \`${defaultValue}\``,
|
||||
...docs.flatMap((doc) => doc.getDescription()),
|
||||
);
|
||||
return lines;
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
const lines = getDocsFor([], inlineConfigInterface);
|
||||
const text = await format(
|
||||
PREFACE +
|
||||
'\n\n' +
|
||||
readFileSync(configTemplatePath, 'utf-8').replace(
|
||||
'{{ DOCS }}',
|
||||
lines.join('\n'),
|
||||
),
|
||||
{ parser: 'markdown' },
|
||||
);
|
||||
|
||||
writeFileSync(configPath, text);
|
||||
consola.success('Generated /config.md');
|
||||
} catch (err) {
|
||||
consola.fail('Failed to generate /config.md');
|
||||
consola.error(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
name: 'docs:generate-config-docs',
|
||||
async config() {
|
||||
if (!hasGenerated) {
|
||||
hasGenerated = true;
|
||||
await generateDocs();
|
||||
}
|
||||
},
|
||||
configureServer(server: any) {
|
||||
server.watcher.add(externalTypesPath);
|
||||
},
|
||||
async handleHotUpdate(ctx: { file: string }) {
|
||||
if ([externalTypesPath, configTemplatePath].includes(ctx.file)) {
|
||||
await generateDocs();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
# API Reference
|
||||
|
||||
:::warning 🚧 Under construction
|
||||
This documentation does not exist yet.
|
||||
:::
|
||||
@@ -1,5 +0,0 @@
|
||||
# Config Reference
|
||||
|
||||
Discover all the options you can use in your `wxt.config.ts` file.
|
||||
|
||||
{{ DOCS }}
|
||||
@@ -8,7 +8,8 @@ For MV2, the background is added as a script to the background page. For MV3, th
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['background.ts', 'background.js'],
|
||||
['background.[jt]s', 'background.js'],
|
||||
['background/index.[jt]s', 'background.js'],
|
||||
]"
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
# 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.[jt]sx?', 'content-scripts/content.js'],
|
||||
['content/index.[jt]sx?', 'content-scripts/content.js'],
|
||||
['<name>.content.[jt]sx?', 'content-scripts/<name>.js'],
|
||||
['<name>.content/index.[jt]sx?', 'content-scripts/<name>.js'],
|
||||
]"
|
||||
/>
|
||||
|
||||
## 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',
|
||||
|
||||
// Set include/exclude if the background should be removed from some builds
|
||||
include: undefined | string[],
|
||||
exclude: undefined | string[],
|
||||
|
||||
// Configure how CSS is injected onto the page
|
||||
cssInjectionMode: undefined | "manifest" | "manual" | "ui",
|
||||
|
||||
main(ctx) {
|
||||
// Executed when content script is loaded
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
> 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.
|
||||
|
||||
## Context
|
||||
|
||||
Old content scripts are not automatically stopped when an extension updates and reloads. Often, this leads to "Invalidated context" errors in production when a content script from an old version of your extension tries to use a extension API.
|
||||
|
||||
WXT provides a utility for managing this process: `ContentScriptContext`. An instance of this class is provided to you automatically inside the `main` function of your content script.
|
||||
|
||||
```ts
|
||||
export default defineContentScript({
|
||||
// ...
|
||||
main(ctx: ContentScriptContext) {
|
||||
// Add custom listeners for stopping work
|
||||
ctx.onInvalidated(() => {
|
||||
// ...
|
||||
});
|
||||
|
||||
// Stop fetch requests
|
||||
fetch('...url', { signal: ctx.signal });
|
||||
|
||||
// Timeout utilities
|
||||
ctx.setTimeout(() => {
|
||||
// ...
|
||||
}, 5e3);
|
||||
ctx.setInterval(() => {
|
||||
// ...
|
||||
}, 60e3);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The class extends [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) and provides other utilities for stopping a content script's logic once it becomes invalidated.
|
||||
|
||||
:::tip
|
||||
When working with content scripts, **you should always use the `ctx` object to stop any async or future work.**
|
||||
|
||||
This prevents old content scripts from interfering with new content scripts, and prevents error messages from the console in production.
|
||||
:::
|
||||
|
||||
## CSS
|
||||
|
||||
To include CSS with your content script, import the CSS file at the top of your entrypoint.
|
||||
|
||||
```
|
||||
|
||||
<srcDir>
|
||||
└─ entrypoints/
|
||||
└─ overlay.content/
|
||||
├─ index.ts
|
||||
└─ style.css
|
||||
```
|
||||
|
||||
```ts
|
||||
// entrypoints/overlay.content/index.ts
|
||||
import './style.css';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['*://google.com/*', '*://duckduckgo.com/*'],
|
||||
|
||||
main(ctx) {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Any styles imported in your content script will be added to that content script's `css` array in your `manifest.json`:
|
||||
|
||||
```json
|
||||
// .output/chrome-mv3/manifest.json
|
||||
{
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["*://google.com/*", "*://duckduckgo.com/*"],
|
||||
"js": ["content-scripts/overlay.js"],
|
||||
"css": ["content-scripts/overlay.css"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
To disable this behavior, set `cssInjectionMode` to `"manual"` or `"ui"`.
|
||||
|
||||
```ts
|
||||
export default defineContentScript({
|
||||
matches: ['*://google.com/*', '*://duckduckgo.com/*'],
|
||||
cssInjectionMode: 'manual',
|
||||
|
||||
main(ctx) {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## UI
|
||||
|
||||
WXT provides a utility function, `createContentScriptUi` to simplify mounting a UI from a content script. Internally, it uses the `ShadowRoot` API to isolate your CSS from the webpages.
|
||||
|
||||
`createContentScriptUi` requires a `ContentScriptContext` so that when the context is invalidated, the UI is automatically removed from the webpage.
|
||||
|
||||
:::details When to use `createContentScriptUi`
|
||||
You should only use `createContentScriptUi` if you want your UI's styles isolated from the webpages. If you want to create a more "integrated" UI that uses the page's styles, you can just use the regular JS API's to append your UI to the page.
|
||||
|
||||
```ts
|
||||
const ui = document.createElement('div');
|
||||
const anchor = document.querySelector('#anchor-selector');
|
||||
anchor.append(ui);
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### Usage
|
||||
|
||||
To use `createContentScriptUi`, follow these steps:
|
||||
|
||||
1. Import your CSS file at the top of your content script
|
||||
2. Set `cssInjectionMode: "ui"` inside `defineContentScript`
|
||||
3. Call `createContentScriptUi`
|
||||
4. Call `ui.mount()` to add the UI to the webpage
|
||||
|
||||
Here's a basic example:
|
||||
|
||||
```ts
|
||||
// entrypoints/ui.content/index.ts
|
||||
import './style.css';
|
||||
|
||||
export default defineContentScript({
|
||||
// ...
|
||||
cssInjectionMode: 'ui',
|
||||
|
||||
async main(ctx) {
|
||||
const ui = await createContentScriptUi(ctx, {
|
||||
name: 'example-ui',
|
||||
type: 'inline',
|
||||
anchor: '#some-element',
|
||||
append: 'after',
|
||||
mount(container) {
|
||||
// Mount UI inside `container`...
|
||||
},
|
||||
});
|
||||
|
||||
// Yoy must call `mount` to add the UI to the page.
|
||||
ui.mount();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
If you're using a frontend framework, you'll also need to include an `onRemoved` callback:
|
||||
|
||||
:::code-group
|
||||
|
||||
```ts [Vue]
|
||||
import { createApp } from 'vue';
|
||||
|
||||
createContentScriptUi(ctx, {
|
||||
// ...
|
||||
mount(container) {
|
||||
// Create a new app and mount it inside the container
|
||||
const app = createApp(...);
|
||||
app.mount(container);
|
||||
return app;
|
||||
},
|
||||
onRemove(app) {
|
||||
// When the UI is removed from the DOM, call unmount to stop the app
|
||||
app.unmount();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts [React]
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
createContentScriptUi(ctx, {
|
||||
// ...
|
||||
mount(container) {
|
||||
// Create a root using the container and render your app
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(...);
|
||||
return root;
|
||||
},
|
||||
onRemove(root) {
|
||||
// When the UI is removed from the DOM, call unmount to stop the app
|
||||
root.unmount();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts [Svelte]
|
||||
import App from './App.svelte';
|
||||
|
||||
createContentScriptUi(ctx, {
|
||||
// ...
|
||||
mount(container) {
|
||||
// Mount your app component inside the container
|
||||
return new App({
|
||||
target: container,
|
||||
});
|
||||
},
|
||||
onRemove(app) {
|
||||
// When the UI is removed from the DOM, call $destroy to stop the app
|
||||
app.$destroy();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts [Solid]
|
||||
import { render } from 'solid-js/web';
|
||||
|
||||
createContentScriptUi(ctx, {
|
||||
// ...
|
||||
mount(container) {
|
||||
// Render your app component into the container
|
||||
return render(() => ..., container)
|
||||
},
|
||||
onRemove(unmount) {
|
||||
// When the UI is removed from the DOM, call unmount to stop the app
|
||||
unmount();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### `anchor`
|
||||
|
||||
The anchor dictates where the UI will be mounted.
|
||||
|
||||
### `append`
|
||||
|
||||
Customize where the UI get's appended to the DOM, relative to the `anchor` element.
|
||||
|
||||
### `type`
|
||||
|
||||
There are 3 types of UI's you can mount.
|
||||
|
||||
- `inline`: Shows up inline based on the `anchor` and `append` options
|
||||
- `overlay`: Shows up inline, but styled to be 0px by 0px, with overflow visible. This causes the UI to overlay on top of the webpage's content
|
||||
- `modal`: A fullscreen overlay that covers the entire screen, regardless of where it's anchored.
|
||||
|
||||
> TODO: Add visualization of the different UI types.
|
||||
|
||||
### Overlay `alignment`
|
||||
|
||||
Because the overlay UI type results in a 0px by 0px container being added to the webpage, the `alignment` option allows you to configure which corner of your UI is aligned with the 0x0 element.
|
||||
|
||||
> TODO: Add visualization of the different alignments.
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
WXT can build CSS entrypoints individually. CSS entrypoints are always unlisted.
|
||||
|
||||
See [Content Script CSS](/guide/content-scripts.md#css) documentation for the recomended approach to include CSS with a content script.
|
||||
See [Content Script CSS](/entrypoints/content-scripts#css) documentation for the recomended approach to include CSS with a content script.
|
||||
|
||||
:::info
|
||||
If the recommended approach doesn't work for your use case, you can use any of the filename patterns below to build the styles separate from the JS and use the [`transformManifest` hook](/config.md#transformmanifest) to manually add your CSS file to the manifest.
|
||||
If the recommended approach doesn't work for your use case, you can use any of the filename patterns below to build the styles separate from the JS and use the [`transformManifest` hook](/api/wxt/interfaces/InlineConfig#transformmanifest) to manually add your CSS file to the manifest.
|
||||
:::
|
||||
|
||||
## Filenames
|
||||
@@ -29,6 +29,7 @@
|
||||
}"
|
||||
/>
|
||||
<meta name="manifest.type" content="page_action|browser_action" />
|
||||
<meta name="manifest.browser_style" content="true|false" />
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
@@ -0,0 +1,37 @@
|
||||
# Unlisted Scripts
|
||||
|
||||
TypeScript files that are built, but are not included in the manifest.
|
||||
|
||||
You are responsible for loading/running these scripts where needed.
|
||||
|
||||
## Filenames
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['<name>.[jt]sx?', '<name>.js'],
|
||||
['<name>/index.[jt]sx?', '<name>.js'],
|
||||
]"
|
||||
/>
|
||||
|
||||
## Definition
|
||||
|
||||
```ts
|
||||
export default defineUnlistedScript(() => {
|
||||
// Executed when script is loaded
|
||||
});
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```ts
|
||||
export default defineUnlistedScript({
|
||||
// Set include/exclude if the script should be removed from some builds
|
||||
include: undefined | string[],
|
||||
exclude: undefined | string[],
|
||||
|
||||
// Executed when script is loaded
|
||||
main() {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -1,46 +0,0 @@
|
||||
# Build Targets
|
||||
|
||||
You can build an extension for any combination of browser and manifest verison. Different browsers and manifest versions support different APIs and entrypoints, so be sure to check that your extension functions as expected for each target.
|
||||
|
||||
Separate build targets are written to their own output directories:
|
||||
|
||||
```
|
||||
<rootDir>
|
||||
└─ .output
|
||||
├─ chrome-mv3
|
||||
├─ firefox-mv2
|
||||
├─ edge-mv3
|
||||
└─ ...
|
||||
```
|
||||
|
||||
## Target Browser
|
||||
|
||||
To build for a specific browser, pass the `-b --browser` flag from the CLI:
|
||||
|
||||
```
|
||||
|
||||
wxt --browser firefox
|
||||
wxt build --browser firefox
|
||||
|
||||
```
|
||||
|
||||
By default, it will build for `chrome`. When excluding the [`--mv2` or `--mv3` flags](#target-manifest-version), it will default to the commonly accepted manifest version used with that browser.
|
||||
|
||||
| Browser | Default Manifest Version |
|
||||
| ---------------- | :----------------------: |
|
||||
| `chrome` | 3 |
|
||||
| `firefox` | 2 |
|
||||
| `safari` | 2 |
|
||||
| `edge` | 3 |
|
||||
| Any other string | 3 |
|
||||
|
||||
## Target Manifest Version
|
||||
|
||||
To build for a specific manifest version, pass either the `--mv2` flag or `--mv3` flag from the CLI.
|
||||
|
||||
```sh
|
||||
wxt --mv2
|
||||
wxt build --mv2
|
||||
```
|
||||
|
||||
When the `-b --browser` flag is not passed, it defaults to `chrome`. So here, we're targetting MV2 for Chrome.
|
||||
@@ -1,5 +0,0 @@
|
||||
# Testing
|
||||
|
||||
:::warning 🚧 Testing utils are not implemented yet!
|
||||
Eventually, the plan is to have an integration with Vitest.
|
||||
:::
|
||||
@@ -3,18 +3,21 @@
|
||||
WXT uses the same tool as Nuxt for auto-imports, [`unimport`](https://github.com/unjs/unimport).
|
||||
|
||||
:::info Testing
|
||||
To setup your test environment for auto-imports, see [Testing](/get-started/testing.md).
|
||||
To setup your test environment for auto-imports, see [Testing](/guide/testing).
|
||||
:::
|
||||
|
||||
## WXT Auto-imports
|
||||
|
||||
Some WXT APIs can be used without importing them:
|
||||
|
||||
- [`browser`](/config.md#browser) from `wxt/browser`, a small wrapper around `webextension-polyfill`
|
||||
- [`defineContentScript`](/config.md#defiencontentscript) from `wxt/client`
|
||||
- [`defineBackground`](/config.md#definebackgroundscript) from `wxt/client`
|
||||
- [`browser`](/api/wxt/browser/variables/browser) from `wxt/browser`, a small wrapper around `webextension-polyfill`
|
||||
- [`defineContentScript`](/api/wxt/client/functions/defineContentScript) from `wxt/client`
|
||||
- [`defineBackground`](/api/wxt/client/functions/defineBackground) from `wxt/client`
|
||||
- [`createContentScriptUi`](/api/wxt/client/functions/createContentScriptUi) from `wxt/client`
|
||||
- [`defineUnlistedScript`](/api/wxt/sandbox/functions/defineUnlistedScript) from `wxt/sandbox`
|
||||
- [`fakeBrowser`](/api/wxt/testing/variables/fakeBrowser) from `wxt/testing`
|
||||
|
||||
And more. All [`wxt/client`](/config.md#wxtclient) APIs can be used without imports.
|
||||
And more. All `wxt/*` APIs can be used without imports.
|
||||
|
||||
## Project Auto-imports
|
||||
|
||||
@@ -61,7 +64,7 @@ import { defineConfig } from 'wxt';
|
||||
export default defineConfig({
|
||||
imports: {
|
||||
// Add auto-imports for vue fuctions like createApp, ref, computed, watch, toRaw, etc...
|
||||
preset: ['vue'],
|
||||
presets: ['vue'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -15,7 +15,7 @@ export default defineConfig({
|
||||
```
|
||||
|
||||
:::info
|
||||
See the [Config reference](/config.md) for a full list of options.
|
||||
See the [Config reference](/api/wxt/interfaces/InlineConfig) for a full list of options.
|
||||
:::
|
||||
|
||||
## Directory Config
|
||||
@@ -25,7 +25,7 @@ WXT allows you to edit several directories to your liking:
|
||||
- `root` (default: `process.cwd()`) - Root of the WXT project
|
||||
- `srcDir` (default: `<rootDir>`) - Location of all your source code
|
||||
- `entrypointsDir` (default: `<srcDir>/entrypoints`) - Folder containing all the entrypoints.
|
||||
- `publicDir` (default: `<srcDir>/public`) - Folder containing [public assets](/get-started/assets.md)
|
||||
- `publicDir` (default: `<srcDir>/public`) - Folder containing [public assets](/guide/assets)
|
||||
|
||||
### Example
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
# 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',
|
||||
|
||||
// Set include/exclude if the background should be removed from some builds
|
||||
include: undefined | string[],
|
||||
exclude: undefined | string[],
|
||||
|
||||
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,59 @@
|
||||
# Development
|
||||
|
||||
WXT's main goal is providing the best DX it possibly can. When running your extension in dev mode, each part of your extension is reloaded separately when possible.
|
||||
|
||||
| | HMR | Reloaded individually | Reload extension | Restart browser |
|
||||
| ------------------- | :-: | :-------------------: | :--------------: | :----------------------------------------------------: |
|
||||
| HTML File | | ✅ |
|
||||
| HTML Dependency | ✅ |
|
||||
| MV3 Content Script | | ✅ |
|
||||
| MV2 Content Script | | | ✅ |
|
||||
| Background | | | ✅ |
|
||||
| manifest.json | | | | 🟡 See [#16](https://github.com/wxt-dev/wxt/issues/16) |
|
||||
| `wxt.config.ts` | | | | 🟡 See [#10](https://github.com/wxt-dev/wxt/issues/10) |
|
||||
| `web-ext.config.ts` | | | | 🟡 See [#10](https://github.com/wxt-dev/wxt/issues/10) |
|
||||
|
||||
## Configure Browser Startup
|
||||
|
||||
WXT uses [`web-ext` by Mozilla](https://github.com/mozilla/web-ext) to automatically open a browser with the extension installed. You can configure the runner's behavior via the [`runner`](/api/wxt/interfaces/InlineConfig#runner) option, or in a separate gitignored file, `web-ext.config.ts`.
|
||||
|
||||
:::code-group
|
||||
|
||||
```ts [wxt.config.ts]
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
runner: {
|
||||
// Runner config
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts [web-ext.config.ts]
|
||||
import { defineRunnerConfig } from 'wxt';
|
||||
|
||||
export default defineRunnerConfig({
|
||||
// Runner config
|
||||
});
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
`web-ext`'s browser discovery is very limitted. By default, it only guesses at where Chrome and Firefox are installed. If you've customized your install locations, you may need to tell `web-ext` where the binaries/executables are located using the [`binaries` option](/api/wxt/interfaces/ExtensionRunnerConfig#binaries). For other Chromium based browsers, like Edge or Opera, you'll need to explicitly list them in the `binaries` option as well, otherwise they will open in Chrome by default.
|
||||
|
||||
```ts
|
||||
// ~/web-ext.config.ts
|
||||
import { defineRunnerConfig } from 'wxt';
|
||||
|
||||
export default defineRunnerConfig({
|
||||
binaries: {
|
||||
chrome: '/path/to/chrome-beta', // Use Chrome Beta instead of regular Chrome
|
||||
firefox: 'firefoxdeveloperedition', // Use Firefox Developer Edition instead of regular Firefox
|
||||
edge: '/path/to/edge', // Open MS Edge when running "wxt -b edge"
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
:::tip
|
||||
When configuring browser binaries, it's helpful to put them in `~/web-ext.config.ts` instead of the project directory's `web-ext.config.ts` file. When placed in your home directory (`~/`), this config will be used by all WXT projects, so you only need to configure the binaries once.
|
||||
:::
|
||||
@@ -42,7 +42,7 @@ 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:
|
||||
If a file uses a special name recognized by WXT, it will be added to the manifest. In this case:
|
||||
|
||||
- `popup.html` → `action.default_popup`
|
||||
- `content.ts` → `content_scripts.0.js.0`
|
||||
@@ -53,7 +53,7 @@ But not all entrypoints are added to the `manifest.json`. If the filename is not
|
||||
In this case, `injected.ts` gets output to `<outdir>/injected.js` and is accessible via `browser.runtime.getURL("/injected.js")`.
|
||||
|
||||
:::info
|
||||
See [`/entrypoints` folder](/guide/background.md) documentation for a full list of recognized entrypoint filenames.
|
||||
See [`/entrypoints` folder](/entrypoints/background) documentation for a full list of recognized entrypoint filenames.
|
||||
:::
|
||||
|
||||
## Entrypoint Options
|
||||
@@ -84,5 +84,5 @@ export default defineContentScript({
|
||||
```
|
||||
|
||||
:::info
|
||||
For a full list of entrypoints and each of their options, see the [`/entrypoints` folder](/guide/background.md) documentation.
|
||||
For a full list of entrypoints and each of their options, see the [`/entrypoints` folder](/entrypoints/background) documentation.
|
||||
:::
|
||||
@@ -11,10 +11,10 @@ And that's it! Your extension now supports Chrome, Firefox, Safari, Edge, and ot
|
||||
|
||||
## Basic Usage
|
||||
|
||||
The `browser` variable is available globally via [auto-imports](/guide/auto-imports.md), or it can be imported manually.
|
||||
The `browser` variable is available globally via [auto-imports](/guide/auto-imports), or it can be imported manually.
|
||||
|
||||
```ts
|
||||
import browser from 'wxt/browser';
|
||||
import { browser } from 'wxt/browser';
|
||||
```
|
||||
|
||||
The `wxt/browser` module exports a customized version of `webextension-polyfill`'s browser with improved typing.
|
||||
@@ -79,8 +79,8 @@ There are a number of message passing libraries you can use to improve the messa
|
||||
|
||||
Here are some that are compatible with WXT (because they are based off `webextension-polyfill` as well):
|
||||
|
||||
- [`@webext-core/messaging`](https://webext-core.aklinker1.io/guide/proxy-service/) - "A light-weight, type-safe wrapper around the `browser.runtime` messaging APIs"
|
||||
- [`@webext-core/proxy-service`](https://webext-core.aklinker1.io/guide/messaging/) - "Create TRPC-like services that can be called from anywhere but run in the background"
|
||||
- [`@webext-core/messaging`](https://webext-core.aklinker1.io/guide/messaging/) - "A light-weight, type-safe wrapper around the `browser.runtime` messaging APIs"
|
||||
- [`@webext-core/proxy-service`](https://webext-core.aklinker1.io/guide/proxy-service/) - "Create TRPC-like services that can be called from anywhere but run in the background"
|
||||
- [`webext-bridge`](https://github.com/zikaari/webext-bridge) - "Messaging in Web Extensions made super easy. Out of the box."
|
||||
|
||||
## Browser Differences
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
# Installation
|
||||
|
||||
Bootstrap a new project, start from scratch, or migrate an existing project.
|
||||
|
||||
:::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/wxt-dev/wxt/issues/55) to track progress.
|
||||
|
||||
In the meantime, you can use `cmd` instead.
|
||||
:::
|
||||
Bootstrap a new project, start from scratch, or [migrate an existing project](/guide/migrate-to-wxt).
|
||||
|
||||
## Bootstrap Project
|
||||
|
||||
@@ -32,7 +26,9 @@ There are several starting templates available.
|
||||
| <Icon name="Svelte" /> [`svelte`](https://github.com/wxt-dev/wxt/tree/main/templates/svelte) |
|
||||
| <Icon name="Solid" icon="https://www.solidjs.com/img/favicons/favicon-32x32.png" /> [`solid`](https://github.com/wxt-dev/wxt/tree/main/templates/solid) |
|
||||
|
||||
> All templates are in TypeScript. WXT does not support JS at this time.
|
||||
:::info
|
||||
All templates default to TypeScript. Rename the file extensions to `.js` to use JavaScript instead.
|
||||
:::
|
||||
|
||||
## From Scratch
|
||||
|
||||
@@ -148,6 +144,6 @@ If you're an experienced web extension developer and think the dev manifest look
|
||||
|
||||
You're ready to build your web extension!
|
||||
|
||||
- Learn how to [add entrypoints](./entrypoints.md) like the popup, options page, or content scripts
|
||||
- [Configure WXT](./configuration.md) by creating a `wxt.config.ts` file
|
||||
- Learn how to [add entrypoints](./entrypoints) like the popup, options page, or content scripts
|
||||
- [Configure WXT](./configuration) by creating a `wxt.config.ts` file
|
||||
- Checkout [example projects](https://github.com/wxt-dev/wxt-examples) to see how to perfom common tasks with WXT
|
||||
@@ -1,3 +1,10 @@
|
||||
---
|
||||
head:
|
||||
- - link
|
||||
- rel: canonical
|
||||
href: https://wxt.dev
|
||||
---
|
||||
|
||||
# Introduction
|
||||
|
||||
WXT is a free and open source framework for building web extensions in an conventional, intuative, and safe way **_for all browsers_**.
|
||||
@@ -28,5 +35,5 @@ Production builds are optimized for store review, changing as few files as possi
|
||||
In addition, WXT fully supports Firefox's source code requirements when using a bundler. It will automatically create and upload a ZIP file of your source code.
|
||||
|
||||
:::info
|
||||
See [Publishing](./publishing.md) for more info around production builds.
|
||||
See [Publishing](./publishing) for more info around production builds.
|
||||
:::
|
||||
@@ -49,7 +49,7 @@ The [manifest's `version` and `version_name`](https://developer.chrome.com/docs/
|
||||
|
||||
## `icons`
|
||||
|
||||
By default, WXT will discover icons in your [`public` directory](/get-started/assets#public-directory) and use them for the [manifest's `icons`](https://developer.chrome.com/docs/extensions/mv3/manifest/icons/).
|
||||
By default, WXT will discover icons in your [`public` directory](/guide/assets#public-directory) and use them for the [manifest's `icons`](https://developer.chrome.com/docs/extensions/mv3/manifest/icons/).
|
||||
|
||||
```
|
||||
public/
|
||||
@@ -94,7 +94,7 @@ export default defineConfig({
|
||||
|
||||
## Localization
|
||||
|
||||
Similar to the icon, the [`_locales` directory](https://developer.chrome.com/docs/extensions/reference/i18n/) should be placed inside the the WXT's [`public` directory](/get-started/assets#public-directory).
|
||||
Similar to the icon, the [`_locales` directory](https://developer.chrome.com/docs/extensions/reference/i18n/) should be placed inside the the WXT's [`public` directory](/guide/assets#public-directory).
|
||||
|
||||
```
|
||||
public/
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Migrate to WXT
|
||||
|
||||
> If you have problems migrating to WXT, feel free to ask for help in GitHub by [starting a discussion](https://github.com/wxt-dev/wxt/discussions/new?category=q-a)!
|
||||
|
||||
## Overview
|
||||
|
||||
Always start by generating a new vanilla project and merging it into your project one file at a time.
|
||||
|
||||
```sh
|
||||
cd path/to/your/project
|
||||
pnpx wxt@latest init example-wxt --template vanilla
|
||||
```
|
||||
|
||||
In general, you'll need to:
|
||||
|
||||
<input type="checkbox" /> Install `wxt`<br />
|
||||
<input type="checkbox" /> Update/create `package.json` scripts to use `wxt` (don't forget about `postinstall`)<br />
|
||||
<input type="checkbox" /> Move entrypoints into `entrypoints/` directory<br />
|
||||
<input type="checkbox" /> Move assets into either the `assets/` or `public/` directories<br />
|
||||
<input type="checkbox" /> Move manifest.json content into `wxt.config.ts`<br />
|
||||
<input type="checkbox" /> Convert custom import syntax to be compatible with Vite<br />
|
||||
<input type="checkbox" /> Add a default export to JS entrypoints<br />
|
||||
<input type="checkbox" /> Use the `browser` global instead of `chrome`<br />
|
||||
<input type="checkbox" /> Compare final `manifest.json` files, making sure permissions and host permissions are unchanged<br />
|
||||
<input type="checkbox" /> Extension output by `wxt build` works the same way as before the migration<br />
|
||||
|
||||
Every project is different, so there's no one-solution-fits-all to migrating your project. Just make sure `wxt dev` runs, `wxt build` results in a working extension, and the list of permissions in the `manifest.json` hasn't changed. If all that looks good, you've finished migrating your extension!
|
||||
|
||||
## Popular Tools/Frameworks
|
||||
|
||||
Here's specific steps for other popuplar frameworks/build tools.
|
||||
|
||||
### `vite-plugin-web-extension`
|
||||
|
||||
Since you're already using Vite, it's a simple refactor.
|
||||
|
||||
1. Install `wxt`
|
||||
2. Move and refactor your entrypoints to WXT's style (with a default export)
|
||||
3. Update package.json scripts to use `wxt`
|
||||
4. Add `"postinstall": "wxt prepare"` script
|
||||
5. Move the `manifest.json` into `wxt.config.ts`
|
||||
6. Move any custom settings from `vite.config.ts` into `wxt.config.ts`'s
|
||||
7. Compare `dist/manifest.json` to `.output/*/manifest.json`, they should have the same content as before. If not, tweak your entrypoints and config to get as close as possible.
|
||||
|
||||
### `plasmo`
|
||||
|
||||
1. Install `wxt`
|
||||
2. Move entrypoints into `entrypoints/` directory, merging the named exports used to configure your JS entrypoints into WXT's default export
|
||||
3. Move public `assets/*` into the `public/` directory
|
||||
4. If you use CSUI, migrate to WXT's `createContentScriptUi`
|
||||
5. Convert Plasmo's custom import resolutions to Vite's
|
||||
6. If importing remote code via a URL, add a `url:` prefix so it works with WXT
|
||||
7. Compare your output `manifest.json` files from before the migration to after the migration. They should have the same content. If not, tweak your entrypoints and config to get as close as possible.
|
||||
@@ -0,0 +1,114 @@
|
||||
# Multiple Browsers
|
||||
|
||||
You can build an extension for any combination of browser and manifest verison. Different browsers and manifest versions support different APIs and entrypoints, so be sure to check that your extension functions as expected for each target.
|
||||
|
||||
Separate build targets are written to their own output directories:
|
||||
|
||||
```
|
||||
<rootDir>
|
||||
└─ .output
|
||||
├─ chrome-mv3
|
||||
├─ firefox-mv2
|
||||
├─ edge-mv3
|
||||
└─ ...
|
||||
```
|
||||
|
||||
## Target Browser
|
||||
|
||||
To build for a specific browser, pass the `-b --browser` flag from the CLI:
|
||||
|
||||
```
|
||||
|
||||
wxt --browser firefox
|
||||
wxt build --browser firefox
|
||||
|
||||
```
|
||||
|
||||
By default, it will build for `chrome`. When excluding the [manifest version flags](#target-manifest-version), it will default to the commonly accepted manifest version for that browser.
|
||||
|
||||
| Browser | Default Manifest Version |
|
||||
| ---------------- | :----------------------: |
|
||||
| `chrome` | 3 |
|
||||
| `firefox` | 2 |
|
||||
| `safari` | 2 |
|
||||
| `edge` | 3 |
|
||||
| Any other string | 3 |
|
||||
|
||||
:::tip
|
||||
To configure which browser is opened when running dev mode via `wxt -b <browser>`, see the [Development docs](/guide/development#configure-browser-startup) docs.
|
||||
:::
|
||||
|
||||
## Target Manifest Version
|
||||
|
||||
To build for a specific manifest version, pass either the `--mv2` flag or `--mv3` flag from the CLI.
|
||||
|
||||
```sh
|
||||
wxt --mv2
|
||||
wxt build --mv2
|
||||
```
|
||||
|
||||
When the `-b --browser` flag is not passed, it defaults to `chrome`. So here, we're targetting MV2 for Chrome.
|
||||
|
||||
## Customizing Entrypoints
|
||||
|
||||
There are several ways to customize entrypoint definitions per browser.
|
||||
|
||||
First, you can use either the `include` or `exclude` option to include or exclude the entrypoint from specific browsers. Here are some examples
|
||||
|
||||
:::code-group
|
||||
|
||||
```ts [Background]
|
||||
export default defineBackground({
|
||||
// Only include a background script when targeting chrome
|
||||
include: ['chrome'],
|
||||
});
|
||||
```
|
||||
|
||||
```ts [Content Script]
|
||||
export default defineContentScript({
|
||||
// Do not add this content script to the manifest when targeting firefox
|
||||
exclude: ['firefox'],
|
||||
});
|
||||
```
|
||||
|
||||
```html [HTML page]
|
||||
<!-- entrypoints/options.html -->
|
||||
<html>
|
||||
<head>
|
||||
<!-- Don't include the options page for safari -->
|
||||
<meta name="manifest.exclude" content="['safari']" />
|
||||
</head>
|
||||
</html>
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
Second, you can change individual options per-browser:
|
||||
|
||||
:::code-group
|
||||
|
||||
```ts [Background]
|
||||
export default defineBackground({
|
||||
persistent: {
|
||||
// Use a non-persistent background script for just safari
|
||||
safari: false,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts [Content Script]
|
||||
export default defineContentScript({
|
||||
matches: {
|
||||
// Run the content script on different pages for each browser
|
||||
chrome: ['*://*.google.com/*'],
|
||||
firefox: ['*://*.duckduckgo.com/*'],
|
||||
edge: ['*://*.bing.com/*'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::warning
|
||||
Only `defineBackground` and `defineContentScript` support per-browser options right now.
|
||||
:::
|
||||
@@ -0,0 +1,162 @@
|
||||
# Testing
|
||||
|
||||
WXT provides several utils for writing tests.
|
||||
|
||||
## Unit tests
|
||||
|
||||
If you're using auto-imports (enabled by default), [Vitest](https://vitest.dev/) is the only testing framework that supports them.
|
||||
|
||||
If you want to use a different testing library/framework (like Jest, mocha, node:test, etc), you can keep using it, but you have two options:
|
||||
|
||||
1. Switch to Vitest (recommended)
|
||||
2. Configure the testing library manually
|
||||
- Disable auto-imports by setting `imports: false` in your `wxt.config.ts` file
|
||||
- Manually add globals normally provided by WXT (like `__BROWSER__`) that you consume to the global scope before accessing them (`globalThis.__BROWSER__ = "chrome"`)
|
||||
|
||||
### Vitest Setup
|
||||
|
||||
Install vitest and add the `WxtVitest` plugin to your `vitest.config.ts` file.
|
||||
|
||||
```sh
|
||||
pnpm i -D vitest
|
||||
```
|
||||
|
||||
```ts
|
||||
// <root>/vitest.config.ts
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { WxtVitest } from 'wxt/testing';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [WxtVitest()],
|
||||
test: {
|
||||
server: {
|
||||
deps: {
|
||||
// Add any dependencies that import webextension-polyfill here, otherwise tests will attempt to import the real polyfill, breaking the
|
||||
// TODO: Auto-detect these dependencies inside `WxtVitest` so maintaining this list manually isn't necessary
|
||||
inline: [...],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
And that's it. You're ready to start writing tests.
|
||||
|
||||
### Writing Tests
|
||||
|
||||
Here's a very basic test, written with a few different testing libraries, with a few different approaches for mocking the `browser` global.
|
||||
|
||||
:::code-group
|
||||
|
||||
```ts [Vitest]
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
function logRuntimeId() {
|
||||
// Vitest automatically mocks "browser" with "fakeBrowser"
|
||||
console.log(browser.runtime.id);
|
||||
}
|
||||
|
||||
describe('logRuntimeId', () => {
|
||||
it("should log the extension's runtime ID", () => {
|
||||
// Set a known ID on fakeBrowser for the test
|
||||
const id = 'some-runtime-id';
|
||||
fakeBrowser.runtime.id = id;
|
||||
const logSpy = vi.spyOn(console, 'log');
|
||||
|
||||
logRuntimeId();
|
||||
|
||||
expect(logSpy).toBeCalledWith(id);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
```ts [Jest - Manual Mock]
|
||||
import { fakeBrowser } from 'wxt/testing';
|
||||
import { browser } from 'wxt/browser';
|
||||
|
||||
function logRuntimeId() {
|
||||
console.log(browser.runtime.id);
|
||||
}
|
||||
|
||||
// Manually mock
|
||||
jest.mock('wxt/browser', () => {
|
||||
const { fakeBrowser } = require('wxt/testing');
|
||||
return { browser: fakeBrowser };
|
||||
});
|
||||
|
||||
describe('logRuntimeId', () => {
|
||||
it("should log the extension's runtime ID", () => {
|
||||
// Set a known ID on fakeBrowser for the test
|
||||
const id = 'some-runtime-id';
|
||||
fakeBrowser.runtime.id = id;
|
||||
const logSpy = jest.spyOn(console, 'log');
|
||||
|
||||
logRuntimeId();
|
||||
|
||||
expect(logSpy).toBeCalledWith(id);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
```ts [node:test - Parameterized]
|
||||
import { describe, it, mock } from 'node:test';
|
||||
import { assert } from 'node:assert';
|
||||
import { fakeBrowser } from 'wxt/testing';
|
||||
import { browser } from 'wxt/browser';
|
||||
|
||||
// Add browser as a parameter so fakeBrowser can be passed instead of browser
|
||||
function logRuntimeId(browser = browser) {
|
||||
console.log(browser.runtime.id);
|
||||
}
|
||||
|
||||
describe('logRuntimeId', () => {
|
||||
it("should log the extension's runtime ID", () => {
|
||||
// Set a known ID on fakeBrowser for the test
|
||||
const id = 'some-runtime-id';
|
||||
fakeBrowser.runtime.id = id;
|
||||
console.log = mock.fn();
|
||||
|
||||
// pass in fakeBrowser during tests
|
||||
logRuntimeId(fakeBrowser);
|
||||
|
||||
assert.deepStrictEqual(console.log.mock.calls[0].arguments, [id]);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::warning
|
||||
Without mocking the `browser` variable, you'll see errors like this:
|
||||
|
||||
```
|
||||
This script should only be loaded in a browser extension.
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
WXT provides an in-memory, partial implementation of `browser`, [`fakeBrowser`](/api/wxt/testing/variables/fakeBrowser), from the [`@webext-core/fake-browser`](https://webext-core.aklinker1.io/guide/fake-browser/) package. `fakeBrowser` works with all testing frameworks/libraries. See their docs for a list of [implemented APIs](https://webext-core.aklinker1.io/guide/fake-browser/implemented-apis.html) and more example tests.
|
||||
|
||||
## E2E Tests
|
||||
|
||||
WXT does not provide any utils for running E2E tests. There are two libraries you can use to run E2E tests for any chrome extension.
|
||||
|
||||
- [`playwright`](https://playwright.dev/docs/chrome-extensions) (recommended) - "A high-level API to automate web browsers"
|
||||
- [`puppeteer`](https://pptr.dev/guides/chrome-extensions) - "A high-level API to control headless Chrome over the DevTools Protocol"
|
||||
|
||||
:::info
|
||||
Note that both only support running tests on Chrome.
|
||||
:::
|
||||
|
||||
Before running tests with either of these tools, you must build the extension with `wxt build` and then load the extension from the output directory in a new tab.
|
||||
|
||||
To test an extension's UI, like the popup or options page, you'll need to know the extension's ID to open the URL directly.
|
||||
|
||||
> _chrome-extension://`browser.runtime.id`/popup.html_
|
||||
|
||||
- Playwright provides an API to get your extension ID after it has been installed. [See their docs](https://playwright.dev/docs/chrome-extensions#testing).
|
||||
- Puppeteer requires you know the ID before installing the extension, so you can hard code it into the URLs you open. Follow [Chrome's guide](https://developer.chrome.com/docs/extensions/mv3/manifest/key/) to setup a consistent runtime id.
|
||||
|
||||
:::info
|
||||
You cannot test popups in their normal popup window, you have to open them in a tab.
|
||||
:::
|
||||
@@ -1,20 +0,0 @@
|
||||
# Unlisted Scripts
|
||||
|
||||
TypeScript files that are built, but are not included in the manifest.
|
||||
|
||||
## Filenames
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['<name>.(ts|tsx)', '<name>.js'],
|
||||
['<name>/index.(ts|tsx)', '<name>.js'],
|
||||
]"
|
||||
/>
|
||||
|
||||
## Definition
|
||||
|
||||
Unlike the background or content scripts, you can define this script's logic in the top level scope.
|
||||
|
||||
```ts
|
||||
// Code goes here
|
||||
```
|
||||
+4
-4
@@ -10,9 +10,9 @@ All of Vite's config can be customized by setting the `vite` configuration in yo
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
vite: {
|
||||
vite: () => ({
|
||||
// Same as `defineConfig({ ... })` inside vite.config.ts
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
@@ -26,10 +26,10 @@ All plugins should work in WXT, but it is worth pointing out that since WXT orch
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
vite: {
|
||||
vite: () => ({
|
||||
plugins: [
|
||||
// ...
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
+55
-15
@@ -1,57 +1,97 @@
|
||||
---
|
||||
# https://vitepress.dev/reference/default-theme-home-page
|
||||
layout: home
|
||||
titleTemplate: 'Next Generation Web Extension Framework'
|
||||
title: Next-gen Web Extension Framework
|
||||
|
||||
hero:
|
||||
name: WXT
|
||||
text: Next-gen Web Extension Framework
|
||||
tagline: Powered by Vite, inspired by Nuxt.
|
||||
tagline: An open source tool that makes Chrome Extension devlopment faster than ever before.
|
||||
image:
|
||||
src: /hero-logo.svg
|
||||
alt: WXT
|
||||
actions:
|
||||
- theme: brand
|
||||
text: Get Started
|
||||
link: /get-started/installation
|
||||
link: /guide/installation
|
||||
- theme: alt
|
||||
text: Learn More
|
||||
link: /get-started/introduction
|
||||
link: /guide/introduction
|
||||
|
||||
features:
|
||||
- icon: 🌐
|
||||
title: Supported Browsers
|
||||
details: Chrome, Firefox, Edge, Safari, and any Chromium based browser.
|
||||
details: WXT will build extensions for Chrome, Firefox, Edge, Safari, and any Chromium based browser.
|
||||
link: /guide/multiple-browsers
|
||||
linkText: Read docs
|
||||
- icon: ✅
|
||||
title: MV2 and MV3
|
||||
details: Supports both manifest versions for each browser.
|
||||
details: Build Manifest V2 or V3 extensions for any browser using the same codebase.
|
||||
link: /guide/multiple-browsers#target-manifest-version
|
||||
linkText: Read docs
|
||||
- icon: ⚡
|
||||
title: Fast Dev Mode
|
||||
details: HMR for UIs and fast reload for background and content scripts.
|
||||
details: Lighting fast HMR for UI development and fast reloads for content/background scripts enables faster iterations.
|
||||
link: /guide/development.html
|
||||
linkText: Learn more
|
||||
- icon: 📂
|
||||
title: File Based Entrypoints
|
||||
details: Manifest is generated based on files inside the project.
|
||||
details: Manifest is generated based on files in the project with inline configuration.
|
||||
link: /guide/entrypoints
|
||||
linkText: See project structure
|
||||
- icon: 🚔
|
||||
title: TypeScript
|
||||
details: Scale projects with full TS support.
|
||||
details: Create large projects with confidence using TS by default.
|
||||
- icon: 🦾
|
||||
title: Auto-imports
|
||||
details: Nuxt-like auto-imports to speed up development.
|
||||
link: /guide/auto-imports
|
||||
linkText: Read docs
|
||||
- icon: ⬇️
|
||||
title: Bundle Remote Code
|
||||
details: Downloads and bundles remote code imported from URLs.
|
||||
link: /guide/remote-code
|
||||
linkText: Read docs
|
||||
- icon: 🎨
|
||||
title: Frontend Framework Agnostic
|
||||
details: Works with any front-end framework with a Vite plugin.
|
||||
link: /guide/configuration#frontend-frameworks
|
||||
linkText: Add a framework
|
||||
- icon: 🖍️
|
||||
title: Bootstrap a New Project
|
||||
details: Comes with starter templates for all major frontend frameworks.
|
||||
- icon: 🤖
|
||||
title: Automated Publishing
|
||||
details: 'TODO: Automatically zip, upload, and release extensions.'
|
||||
details: Get started quickly with several awesome project templates.
|
||||
link: /guide/installation#bootstrap-project
|
||||
linkText: See templates
|
||||
- icon: 📏
|
||||
title: Bundle Analysis
|
||||
details: 'TODO: Tools for analyizing the final extension bundle.'
|
||||
details: Tools for analyizing the final extension bundle and minimizing your extension's size.
|
||||
- icon: 🤖
|
||||
title: Automated Publishing
|
||||
details: 'Coming soon. Automatically zip, upload, and release extensions.'
|
||||
---
|
||||
|
||||
<UsingWxtSection />
|
||||
<section class="vp-doc">
|
||||
<div class="container">
|
||||
<h2>Put <span style="color: var(--vp-c-brand-1)">Developer Experience</span> First</h2>
|
||||
<p>
|
||||
WXT's simplifies the chrome extension development process by providing tools for zipping and publishing, the best-in-class dev mode, an opinionated project structure, and more. Iterate faster, develop features not build scripts, and use everything the JS ecosystem has to offer.
|
||||
</p>
|
||||
<div style="margin: auto; width: 100%; max-width: 700px; text-align: center">
|
||||
<img src="./assets/cli-output.png" alt="Screenshot of WXT's build output" />
|
||||
<small>
|
||||
And who doesn't appreciate a beautiful CLI?
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<ClientOnly>
|
||||
<UsingWxtSection />
|
||||
</ClientOnly>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
max-width: 1152px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Netlify Redirects File
|
||||
# https://docs.netlify.com/routing/redirects/
|
||||
|
||||
# Old URLs -> New URLs
|
||||
/config.html /api/wxt/interfaces/InlineConfig.html
|
||||
/api/config.html /api/wxt/interfaces/InlineConfig.html
|
||||
/entrypoints /entrypoints/background.html
|
||||
/get-started/assets.html /guide/assets.html
|
||||
/get-started/build-targets.html /guide/multiple-browsers.html
|
||||
/get-started/compare.html /guide/compare.html
|
||||
/get-started/configuration.html /guide/configuration.html
|
||||
/get-started/entrypoints.html /guide/entrypoints.html
|
||||
/get-started/installation.html /guide/installation.html
|
||||
/get-started/introduction.html /guide/introduction.html
|
||||
/get-started/publishing.html /guide/publishing.html
|
||||
/get-started/testing.html /guide/testing.html
|
||||
/guide/background.html /entrypoints/background.html
|
||||
/guide/bookmarks.html /entrypoints/bookmarks.html
|
||||
/guide/content-scripts.html /entrypoints/content-scripts.html
|
||||
/guide/css.html /entrypoints/css.html
|
||||
/guide/devtools.html /entrypoints/devtools.html
|
||||
/guide/history.html /entrypoints/history.html
|
||||
/guide/manifest.html /entrypoints/manifest.html
|
||||
/guide/newtab.html /entrypoints/newtab.html
|
||||
/guide/options.html /entrypoints/options.html
|
||||
/guide/popup.html /entrypoints/popup.html
|
||||
/guide/sandbox.html /entrypoints/sandbox.html
|
||||
/guide/sidepanel.html /entrypoints/sidepanel.html
|
||||
/guide/unlisted-pages.html /entrypoints/unlisted-pages.html
|
||||
/guide/unlisted-scripts.html /entrypoints/unlisted-scripts.html
|
||||
/guide/build-targets.html /guide/multiple-browsers.html
|
||||
@@ -0,0 +1,5 @@
|
||||
User-agent: *
|
||||
Disallow: /api.html
|
||||
Disallow: /config.html
|
||||
|
||||
Sitemap: https://wxt.dev/sitemap.xml
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"entryPoints": [
|
||||
"../src",
|
||||
"../src/client",
|
||||
"../src/browser.ts",
|
||||
"../src/sandbox",
|
||||
"../src/testing"
|
||||
],
|
||||
"plugin": ["typedoc-plugin-markdown", "typedoc-vitepress-theme"],
|
||||
"out": "./api",
|
||||
"githubPages": false,
|
||||
"excludePrivate": true,
|
||||
"excludeProtected": true,
|
||||
"excludeInternal": true,
|
||||
"readme": "none"
|
||||
}
|
||||
@@ -16,11 +16,14 @@ describe('Auto Imports', () => {
|
||||
// Generated by wxt
|
||||
export {}
|
||||
declare global {
|
||||
const ContentScriptContext: typeof import('wxt/client')['ContentScriptContext']
|
||||
const browser: typeof import('wxt/browser')['browser']
|
||||
const createContentScriptUi: typeof import('wxt/client')['createContentScriptUi']
|
||||
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']
|
||||
const defineUnlistedScript: typeof import('wxt/sandbox')['defineUnlistedScript']
|
||||
const fakeBrowser: typeof import('wxt/testing')['fakeBrowser']
|
||||
}
|
||||
"
|
||||
`);
|
||||
|
||||
@@ -40,5 +40,5 @@ describe('Init command', () => {
|
||||
"wxt.config.ts",
|
||||
]
|
||||
`);
|
||||
}, 30e3);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -193,117 +193,435 @@ describe('Manifest Content', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should group content scripts and styles together based on their matches and run_at', async () => {
|
||||
describe('content_scripts', () => {
|
||||
it('should group content scripts and styles together based on their manifest properties', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'entrypoints/one.content/index.ts',
|
||||
`import "./style.css";
|
||||
export default defineContentScript({
|
||||
matches: ["*://google.com/*"],
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/one.content/style.css',
|
||||
`body { color: red }`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/two.content/index.ts',
|
||||
`import "./style.css";
|
||||
export default defineContentScript({
|
||||
matches: ["*://google.com/*"],
|
||||
runAt: "document_end",
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/two.content/style.css',
|
||||
`body { color: green }`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/three.content/index.ts',
|
||||
`import "./style.css";
|
||||
export default defineContentScript({
|
||||
matches: ["*://google.com/*"],
|
||||
runAt: "document_end",
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/three.content/style.css',
|
||||
`body { color: blue }`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/four.content/index.ts',
|
||||
`import "./style.css";
|
||||
export default defineContentScript({
|
||||
matches: ["*://duckduckgo.com/*"],
|
||||
runAt: "document_end",
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/four.content/style.css',
|
||||
`body { color: yellow }`,
|
||||
);
|
||||
|
||||
await project.build();
|
||||
|
||||
const manifest = await project.getOutputManifest();
|
||||
|
||||
expect(manifest.content_scripts).toContainEqual({
|
||||
matches: ['*://duckduckgo.com/*'],
|
||||
run_at: 'document_end',
|
||||
css: ['content-scripts/four.css'],
|
||||
js: ['content-scripts/four.js'],
|
||||
});
|
||||
expect(manifest.content_scripts).toContainEqual({
|
||||
matches: ['*://google.com/*'],
|
||||
run_at: 'document_end',
|
||||
css: ['content-scripts/three.css', 'content-scripts/two.css'],
|
||||
js: ['content-scripts/three.js', 'content-scripts/two.js'],
|
||||
});
|
||||
expect(manifest.content_scripts).toContainEqual({
|
||||
matches: ['*://google.com/*'],
|
||||
css: ['content-scripts/one.css'],
|
||||
js: ['content-scripts/one.js'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should add to any content scripts declared in wxt.config.ts', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'entrypoints/one.content/index.ts',
|
||||
`export default defineContentScript({
|
||||
matches: ["*://google.com/*"],
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/two.content/style.css',
|
||||
`body {
|
||||
background-color: red;
|
||||
}`,
|
||||
);
|
||||
project.setConfigFileConfig({
|
||||
manifest: {
|
||||
content_scripts: [
|
||||
{
|
||||
css: ['content-scripts/two.css'],
|
||||
matches: ['*://*.google.com/*'],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await project.build();
|
||||
|
||||
const manifest = await project.getOutputManifest();
|
||||
|
||||
expect(manifest.content_scripts).toContainEqual({
|
||||
css: ['content-scripts/two.css'],
|
||||
matches: ['*://*.google.com/*'],
|
||||
});
|
||||
expect(manifest.content_scripts).toContainEqual({
|
||||
matches: ['*://google.com/*'],
|
||||
js: ['content-scripts/one.js'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should add a CSS entry when cssInjectionMode is undefined', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'entrypoints/content/style.css',
|
||||
'body { background-color: red; }',
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/content/index.ts',
|
||||
`import "./style.css";
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ["https://*.google.com/*"],
|
||||
|
||||
main() {},
|
||||
});`,
|
||||
);
|
||||
await project.build();
|
||||
|
||||
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
|
||||
.toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"css\\":[\\"content-scripts/content.css\\"],\\"js\\":[\\"content-scripts/content.js\\"]}]}"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should add a CSS entry when cssInjectionMode is "manifest"', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'entrypoints/content/style.css',
|
||||
'body { background-color: red; }',
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/content/index.ts',
|
||||
`import "./style.css";
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ["https://*.google.com/*"],
|
||||
cssInjectionMode: "manifest",
|
||||
|
||||
main() {},
|
||||
});`,
|
||||
);
|
||||
await project.build();
|
||||
|
||||
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
|
||||
.toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"css\\":[\\"content-scripts/content.css\\"],\\"js\\":[\\"content-scripts/content.js\\"]}]}"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not add an entry for CSS when cssInjectionMode is "manual"', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'entrypoints/content/style.css',
|
||||
'body { background-color: red; }',
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/content/index.ts',
|
||||
`import "./style.css";
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ["https://*.google.com/*"],
|
||||
cssInjectionMode: "manual",
|
||||
|
||||
main() {},
|
||||
});`,
|
||||
);
|
||||
await project.build();
|
||||
|
||||
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
|
||||
.toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"js\\":[\\"content-scripts/content.js\\"]}]}"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not add a content script entry for CSS when cssInjectionMode is "ui", but add a web_accessible_resources entry for MV2', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'entrypoints/content/style.css',
|
||||
'body { background-color: red; }',
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/content/index.ts',
|
||||
`import "./style.css";
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ["https://*.google.com/*"],
|
||||
cssInjectionMode: "ui",
|
||||
|
||||
main() {},
|
||||
});`,
|
||||
);
|
||||
await project.build({
|
||||
manifestVersion: 2,
|
||||
});
|
||||
|
||||
expect(await project.serializeFile('.output/chrome-mv2/manifest.json'))
|
||||
.toMatchInlineSnapshot(`
|
||||
".output/chrome-mv2/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":2,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"js\\":[\\"content-scripts/content.js\\"]}],\\"web_accessible_resources\\":[\\"content-scripts/content.css\\"]}"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not add a content script entry for CSS when cssInjectionMode is "ui", but add a web_accessible_resources entry for MV3', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'entrypoints/content/style.css',
|
||||
'body { background-color: red; }',
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/content/index.ts',
|
||||
`import "./style.css";
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ["https://*.google.com/*"],
|
||||
cssInjectionMode: "ui",
|
||||
|
||||
main() {},
|
||||
});`,
|
||||
);
|
||||
await project.build({
|
||||
manifestVersion: 3,
|
||||
});
|
||||
|
||||
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
|
||||
.toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"js\\":[\\"content-scripts/content.js\\"]}],\\"web_accessible_resources\\":[{\\"resources\\":[\\"content-scripts/content.css\\"],\\"matches\\":[\\"https://*.google.com/*\\"]}]}"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
it('should combine web accessible resources', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'entrypoints/one.content/index.ts',
|
||||
'entrypoints/content/style.css',
|
||||
'body { background-color: red; }',
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/content/index.ts',
|
||||
`import "./style.css";
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ["*://google.com/*"],
|
||||
main: () => {},
|
||||
matches: ["https://*.google.com/*"],
|
||||
cssInjectionMode: "ui",
|
||||
|
||||
main() {},
|
||||
});`,
|
||||
);
|
||||
project.setConfigFileConfig({
|
||||
manifest: {
|
||||
web_accessible_resources: [
|
||||
{ resources: ['one.png'], matches: ['https://one.com/*'] },
|
||||
],
|
||||
},
|
||||
});
|
||||
await project.build();
|
||||
|
||||
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
|
||||
.toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"web_accessible_resources\\":[{\\"resources\\":[\\"one.png\\"],\\"matches\\":[\\"https://one.com/*\\"]},{\\"resources\\":[\\"content-scripts/content.css\\"],\\"matches\\":[\\"https://*.google.com/*\\"]}],\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"js\\":[\\"content-scripts/content.js\\"]}]}"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should respect the transformManifest option', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'wxt.config.ts',
|
||||
`import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
transformManifest(manifest) {
|
||||
manifest.author = "Custom Author"
|
||||
}
|
||||
})`,
|
||||
);
|
||||
project.addFile('entrypoints/one.content/style.css', `body { color: red }`);
|
||||
project.addFile(
|
||||
'entrypoints/two.content/index.ts',
|
||||
`import "./style.css";
|
||||
export default defineContentScript({
|
||||
matches: ["*://google.com/*"],
|
||||
runAt: "document_end",
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/two.content/style.css',
|
||||
`body { color: green }`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/three.content/index.ts',
|
||||
`import "./style.css";
|
||||
export default defineContentScript({
|
||||
matches: ["*://google.com/*"],
|
||||
runAt: "document_end",
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/three.content/style.css',
|
||||
`body { color: blue }`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/four.content/index.ts',
|
||||
`import "./style.css";
|
||||
export default defineContentScript({
|
||||
matches: ["*://duckduckgo.com/*"],
|
||||
runAt: "document_end",
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/four.content/style.css',
|
||||
`body { color: yellow }`,
|
||||
);
|
||||
|
||||
await project.build();
|
||||
|
||||
const manifest = await project.getOutputManifest();
|
||||
|
||||
expect(manifest.content_scripts).toContainEqual({
|
||||
matches: ['*://duckduckgo.com/*'],
|
||||
run_at: 'document_end',
|
||||
css: ['assets/four.css'],
|
||||
js: ['content-scripts/four.js'],
|
||||
});
|
||||
expect(manifest.content_scripts).toContainEqual({
|
||||
matches: ['*://google.com/*'],
|
||||
run_at: 'document_end',
|
||||
css: ['assets/three.css', 'assets/two.css'],
|
||||
js: ['content-scripts/three.js', 'content-scripts/two.js'],
|
||||
});
|
||||
expect(manifest.content_scripts).toContainEqual({
|
||||
matches: ['*://google.com/*'],
|
||||
css: ['assets/one.css'],
|
||||
js: ['content-scripts/one.js'],
|
||||
});
|
||||
const output = await project.serializeFile(
|
||||
'.output/chrome-mv3/manifest.json',
|
||||
);
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"author\\":\\"Custom Author\\"}"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should add to any content scripts declared in wxt.config.ts', async () => {
|
||||
it.each([
|
||||
{ browser: undefined, outDir: 'chrome-mv3', expected: undefined },
|
||||
{ browser: 'chrome', outDir: 'chrome-mv3', expected: undefined },
|
||||
{ browser: 'firefox', outDir: 'firefox-mv2', expected: true },
|
||||
{ browser: 'safari', outDir: 'safari-mv2', expected: false },
|
||||
])(
|
||||
'should respect the per-browser entrypoint option with %j',
|
||||
async ({ browser, expected, outDir }) => {
|
||||
const project = new TestProject();
|
||||
|
||||
project.addFile(
|
||||
'entrypoints/background.ts',
|
||||
`export default defineBackground({
|
||||
persistent: {
|
||||
firefox: true,
|
||||
safari: false,
|
||||
},
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
await project.build({ browser });
|
||||
|
||||
const safariManifest = await project.getOutputManifest(
|
||||
`.output/${outDir}/manifest.json`,
|
||||
);
|
||||
expect(safariManifest.background.persistent).toBe(expected);
|
||||
},
|
||||
);
|
||||
|
||||
describe('versions', () => {
|
||||
it.each([
|
||||
['chrome', 3] as const,
|
||||
['safari', 2] as const,
|
||||
['edge', 3] as const,
|
||||
])(
|
||||
'should include version_name on %s when it needs simplified',
|
||||
async (browser, manifestVersion) => {
|
||||
const project = new TestProject({
|
||||
version: '1.0.0-alpha1',
|
||||
});
|
||||
|
||||
await project.build({ browser, manifestVersion });
|
||||
const manifest = await project.getOutputManifest(
|
||||
`.output/${browser}-mv${manifestVersion}/manifest.json`,
|
||||
);
|
||||
|
||||
expect(manifest.version).toBe('1.0.0');
|
||||
expect(manifest.version_name).toBe('1.0.0-alpha1');
|
||||
},
|
||||
);
|
||||
|
||||
it.each([['firefox', 2] as const])(
|
||||
"should not include a version_name on %s because the browser doesn't support it",
|
||||
async (browser, manifestVersion) => {
|
||||
const project = new TestProject({
|
||||
version: '1.0.0-alpha1',
|
||||
});
|
||||
|
||||
await project.build({ browser, manifestVersion });
|
||||
const manifest = await project.getOutputManifest(
|
||||
`.output/${browser}-mv${manifestVersion}/manifest.json`,
|
||||
);
|
||||
|
||||
expect(manifest.version).toBe('1.0.0');
|
||||
expect(manifest.version_name).toBeUndefined();
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
['chrome', 3] as const,
|
||||
['firefox', 2] as const,
|
||||
['safari', 3] as const,
|
||||
['edge', 3] as const,
|
||||
])(
|
||||
'should not include the version_name if it is equal to version',
|
||||
async (browser, manifestVersion) => {
|
||||
const project = new TestProject({
|
||||
version: '1.0.0.1',
|
||||
});
|
||||
|
||||
await project.build({ browser, manifestVersion });
|
||||
const manifest = await project.getOutputManifest(
|
||||
`.output/${browser}-mv${manifestVersion}/manifest.json`,
|
||||
);
|
||||
|
||||
expect(manifest.version).toBe('1.0.0.1');
|
||||
expect(manifest.version_name).toBeUndefined();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should respect the action field in the manifest without a popup', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'entrypoints/one.content/index.ts',
|
||||
`export default defineContentScript({
|
||||
matches: ["*://google.com/*"],
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/two.content/style.css',
|
||||
`body {
|
||||
background-color: red;
|
||||
}`,
|
||||
);
|
||||
project.setConfigFileConfig({
|
||||
manifest: {
|
||||
content_scripts: [
|
||||
{
|
||||
css: ['content-scripts/two.css'],
|
||||
matches: ['*://*.google.com/*'],
|
||||
},
|
||||
],
|
||||
action: {
|
||||
default_title: 'Hello world',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await project.build();
|
||||
|
||||
const manifest = await project.getOutputManifest();
|
||||
|
||||
expect(manifest.content_scripts).toContainEqual({
|
||||
css: ['content-scripts/two.css'],
|
||||
matches: ['*://*.google.com/*'],
|
||||
});
|
||||
expect(manifest.content_scripts).toContainEqual({
|
||||
matches: ['*://google.com/*'],
|
||||
js: ['content-scripts/one.js'],
|
||||
});
|
||||
expect(await project.getOutputManifest()).toMatchInlineSnapshot(`
|
||||
{
|
||||
"action": {
|
||||
"default_title": "Hello world",
|
||||
},
|
||||
"description": "Example description",
|
||||
"manifest_version": 3,
|
||||
"name": "E2E Extension",
|
||||
"version": "0.0.0",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ describe('Output Directory Structure', () => {
|
||||
expect(await project.serializeOutput()).toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\"}"
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\"}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -43,30 +43,33 @@ describe('Output Directory Structure', () => {
|
||||
|
||||
await project.build();
|
||||
|
||||
expect(await project.serializeOutput()).toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/assets/one.css
|
||||
expect(
|
||||
await project.serializeOutput([
|
||||
'.output/chrome-mv3/content-scripts/one.js',
|
||||
'.output/chrome-mv3/content-scripts/two.js',
|
||||
]),
|
||||
).toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/content-scripts/one.css
|
||||
----------------------------------------
|
||||
body{color:#00f}
|
||||
|
||||
================================================================================
|
||||
.output/chrome-mv3/assets/two.css
|
||||
.output/chrome-mv3/content-scripts/one.js
|
||||
----------------------------------------
|
||||
<contents-ignored>
|
||||
================================================================================
|
||||
.output/chrome-mv3/content-scripts/two.css
|
||||
----------------------------------------
|
||||
body{color:red}
|
||||
|
||||
================================================================================
|
||||
.output/chrome-mv3/content-scripts/one.js
|
||||
----------------------------------------
|
||||
(function(){\\"use strict\\";function i(n){return n}const s=\\"\\",o={matches:[\\"*://*/*\\"],main:()=>{}};function t(n,...e){if(typeof e[0]==\\"string\\"){const c=e.shift();n(\`[wxt] \${c}\`,...e)}else n(\\"[wxt]\\",...e)}var r={debug:(...n)=>t(console.debug,...n),log:(...n)=>t(console.log,...n),warn:(...n)=>t(console.warn,...n),error:(...n)=>t(console.error,...n)};(async()=>{try{await o.main()}catch(n){r.error(\\"The content script crashed on startup!\\",n)}})()})();
|
||||
|
||||
================================================================================
|
||||
.output/chrome-mv3/content-scripts/two.js
|
||||
----------------------------------------
|
||||
(function(){\\"use strict\\";function i(n){return n}const s=\\"\\",o={matches:[\\"*://*/*\\"],main:()=>{}};function t(n,...e){if(typeof e[0]==\\"string\\"){const c=e.shift();n(\`[wxt] \${c}\`,...e)}else n(\\"[wxt]\\",...e)}var r={debug:(...n)=>t(console.debug,...n),log:(...n)=>t(console.log,...n),warn:(...n)=>t(console.warn,...n),error:(...n)=>t(console.error,...n)};(async()=>{try{await o.main()}catch(n){r.error(\\"The content script crashed on startup!\\",n)}})()})();
|
||||
|
||||
<contents-ignored>
|
||||
================================================================================
|
||||
.output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":[\\"*://*/*\\"],\\"css\\":[\\"assets/one.css\\",\\"assets/two.css\\"],\\"js\\":[\\"content-scripts/one.js\\",\\"content-scripts/two.js\\"]}]}"
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"content_scripts\\":[{\\"matches\\":[\\"*://*/*\\"],\\"css\\":[\\"content-scripts/one.css\\",\\"content-scripts/two.css\\"],\\"js\\":[\\"content-scripts/one.js\\",\\"content-scripts/two.js\\"]}]}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -82,15 +85,18 @@ describe('Output Directory Structure', () => {
|
||||
|
||||
await project.build();
|
||||
|
||||
expect(await project.serializeOutput()).toMatchInlineSnapshot(`
|
||||
expect(
|
||||
await project.serializeOutput([
|
||||
'.output/chrome-mv3/content-scripts/overlay-one.js',
|
||||
]),
|
||||
).toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/content-scripts/overlay-one.js
|
||||
----------------------------------------
|
||||
(function(){\\"use strict\\";function i(n){return n}const o={matches:[\\"*://*/*\\"],main:()=>{}};function t(n,...e){if(typeof e[0]==\\"string\\"){const c=e.shift();n(\`[wxt] \${c}\`,...e)}else n(\\"[wxt]\\",...e)}var r={debug:(...n)=>t(console.debug,...n),log:(...n)=>t(console.log,...n),warn:(...n)=>t(console.warn,...n),error:(...n)=>t(console.error,...n)};(async()=>{try{await o.main()}catch(n){r.error(\\"The content script crashed on startup!\\",n)}})()})();
|
||||
|
||||
<contents-ignored>
|
||||
================================================================================
|
||||
.output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":[\\"*://*/*\\"],\\"js\\":[\\"content-scripts/overlay-one.js\\"]}]}"
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"content_scripts\\":[{\\"matches\\":[\\"*://*/*\\"],\\"js\\":[\\"content-scripts/overlay-one.js\\"]}]}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -133,4 +139,73 @@ describe('Output Directory Structure', () => {
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should generate a stats file when analyzing the bundle', async () => {
|
||||
const project = new TestProject();
|
||||
project.setConfigFileConfig({
|
||||
analysis: {
|
||||
enabled: true,
|
||||
template: 'sunburst',
|
||||
},
|
||||
});
|
||||
project.addFile(
|
||||
'entrypoints/background.ts',
|
||||
`export default defineBackground(() => {});`,
|
||||
);
|
||||
project.addFile('entrypoints/popup.html', '<html></html>');
|
||||
project.addFile(
|
||||
'entrypoints/overlay.content.html',
|
||||
`export default defineContentScript({
|
||||
matches: [],
|
||||
main() {},
|
||||
});`,
|
||||
);
|
||||
|
||||
await project.build();
|
||||
|
||||
expect(await project.fileExists('stats.html')).toBe(true);
|
||||
});
|
||||
|
||||
it('should support JavaScript entrypoints', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'entrypoints/background.js',
|
||||
`export default defineBackground(() => {});`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/unlisted.js',
|
||||
`export default defineUnlistedScript(() => {})`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/content.js',
|
||||
`export default defineContentScript({
|
||||
matches: ["*://*.google.com/*"],
|
||||
main() {},
|
||||
})`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/named.content.jsx',
|
||||
`export default defineContentScript({
|
||||
matches: ["*://*.duckduckgo.com/*"],
|
||||
main() {},
|
||||
})`,
|
||||
);
|
||||
|
||||
await project.build();
|
||||
|
||||
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
|
||||
.toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"background\\":{\\"service_worker\\":\\"background.js\\"},\\"content_scripts\\":[{\\"matches\\":[\\"*://*.google.com/*\\"],\\"js\\":[\\"content-scripts/content.js\\"]},{\\"matches\\":[\\"*://*.duckduckgo.com/*\\"],\\"js\\":[\\"content-scripts/named.js\\"]}]}"
|
||||
`);
|
||||
expect(await project.fileExists('.output/chrome-mv3/background.js'));
|
||||
expect(
|
||||
await project.fileExists('.output/chrome-mv3/content-scripts/content.js'),
|
||||
);
|
||||
expect(
|
||||
await project.fileExists('.output/chrome-mv3/content-scripts/named.js'),
|
||||
);
|
||||
expect(await project.fileExists('.output/chrome-mv3/unlisted.js'));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { TestProject } from '../utils';
|
||||
|
||||
describe('React', () => {
|
||||
it('should prepare and build an project with a tsx entrypoint', async () => {
|
||||
const project = new TestProject({
|
||||
dependencies: {
|
||||
react: '^18.2.0',
|
||||
'react-dom': '^18.2.0',
|
||||
},
|
||||
devDependencies: {
|
||||
'@types/react': '^18.2.14',
|
||||
'@types/react-dom': '^18.2.6',
|
||||
},
|
||||
});
|
||||
project.addFile(
|
||||
'entrypoints/demo.content.tsx',
|
||||
`import ReactDOM from 'react-dom/client';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: "<all_urls>",
|
||||
main() {
|
||||
const container = document.createElement("div");
|
||||
document.body.append(container)
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(<h1>Hello, world!</h1>);
|
||||
}
|
||||
})`,
|
||||
);
|
||||
|
||||
await project.build();
|
||||
|
||||
expect(
|
||||
await project.fileExists('.output/chrome-mv3/content-scripts/demo.js'),
|
||||
).toBe(true);
|
||||
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
|
||||
.toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"content_scripts\\":[{\\"matches\\":\\"<all_urls>\\",\\"js\\":[\\"content-scripts/demo.js\\"]}]}"
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,11 @@ describe('Remote Code', () => {
|
||||
it('should download "url:*" modules and include them in the final bundle', async () => {
|
||||
const url = 'https://code.jquery.com/jquery-3.7.1.slim.min.js';
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.ts', `import "url:${url}"`);
|
||||
project.addFile(
|
||||
'entrypoints/popup.ts',
|
||||
`import "url:${url}"
|
||||
export default defineUnlistedScript(() => {})`,
|
||||
);
|
||||
|
||||
await project.build();
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ describe('TypeScript Project', () => {
|
||||
const __IS_EDGE__: boolean;
|
||||
const __IS_OPERA__: boolean;
|
||||
const __COMMAND__: \\"build\\" | \\"serve\\";
|
||||
const __ENTRYPOINT__: string;
|
||||
}
|
||||
"
|
||||
`);
|
||||
@@ -254,14 +255,14 @@ describe('TypeScript Project', () => {
|
||||
\\"lib\\": [\\"DOM\\", \\"WebWorker\\"],
|
||||
\\"skipLibCheck\\": true,
|
||||
\\"paths\\": {
|
||||
\\"@@\\": [\\"..\\"],
|
||||
\\"@@/*\\": [\\"../*\\"],
|
||||
\\"~~\\": [\\"..\\"],
|
||||
\\"~~/*\\": [\\"../*\\"],
|
||||
\\"@\\": [\\"..\\"],
|
||||
\\"@/*\\": [\\"../*\\"],
|
||||
\\"~\\": [\\"..\\"],
|
||||
\\"~/*\\": [\\"../*\\"]
|
||||
\\"~/*\\": [\\"../*\\"],
|
||||
\\"@@\\": [\\"..\\"],
|
||||
\\"@@/*\\": [\\"../*\\"],
|
||||
\\"~~\\": [\\"..\\"],
|
||||
\\"~~/*\\": [\\"../*\\"]
|
||||
}
|
||||
},
|
||||
\\"include\\": [
|
||||
@@ -298,14 +299,14 @@ describe('TypeScript Project', () => {
|
||||
\\"lib\\": [\\"DOM\\", \\"WebWorker\\"],
|
||||
\\"skipLibCheck\\": true,
|
||||
\\"paths\\": {
|
||||
\\"@@\\": [\\"..\\"],
|
||||
\\"@@/*\\": [\\"../*\\"],
|
||||
\\"~~\\": [\\"..\\"],
|
||||
\\"~~/*\\": [\\"../*\\"],
|
||||
\\"@\\": [\\"../src\\"],
|
||||
\\"@/*\\": [\\"../src/*\\"],
|
||||
\\"~\\": [\\"../src\\"],
|
||||
\\"~/*\\": [\\"../src/*\\"]
|
||||
\\"~/*\\": [\\"../src/*\\"],
|
||||
\\"@@\\": [\\"..\\"],
|
||||
\\"@@/*\\": [\\"../*\\"],
|
||||
\\"~~\\": [\\"..\\"],
|
||||
\\"~~/*\\": [\\"../*\\"]
|
||||
}
|
||||
},
|
||||
\\"include\\": [
|
||||
|
||||
File diff suppressed because one or more lines are too long
+38
-17
@@ -1,32 +1,36 @@
|
||||
import { dirname, join, relative, resolve } from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import glob from 'fast-glob';
|
||||
import { execSync } from 'child_process';
|
||||
import { execaCommand } from 'execa';
|
||||
import { InlineConfig, UserConfig, build } from '../src';
|
||||
import { normalizePath } from '../src/core/utils/paths';
|
||||
import merge from 'lodash.merge';
|
||||
|
||||
export class TestProject {
|
||||
files: Array<[string, string]> = [];
|
||||
config: UserConfig | undefined;
|
||||
readonly root: string;
|
||||
|
||||
constructor(root = 'e2e/dist') {
|
||||
constructor(packageJson: any = {}) {
|
||||
// We can't put each test's project inside e2e/dist directly, otherwise the wxt.config.ts
|
||||
// file is cached and cannot be different between each test. Instead, we add a random ID to the
|
||||
// end to make each test's path unique.
|
||||
const id = Math.random().toString(32).substring(3);
|
||||
this.root = join(root, id);
|
||||
this.root = join('e2e/dist', id);
|
||||
this.files.push([
|
||||
'package.json',
|
||||
JSON.stringify(
|
||||
{
|
||||
name: 'E2E Extension',
|
||||
description: 'Example description',
|
||||
version: '0.0.0-test',
|
||||
dependencies: {
|
||||
wxt: '../../..',
|
||||
merge(
|
||||
{
|
||||
name: 'E2E Extension',
|
||||
description: 'Example description',
|
||||
version: '0.0.0',
|
||||
dependencies: {
|
||||
wxt: '../../..',
|
||||
},
|
||||
},
|
||||
},
|
||||
packageJson,
|
||||
),
|
||||
null,
|
||||
2,
|
||||
),
|
||||
@@ -72,17 +76,20 @@ export class TestProject {
|
||||
await fs.ensureDir(fileDir);
|
||||
await fs.writeFile(filePath, content ?? '', 'utf-8');
|
||||
}
|
||||
execSync('npm i --ignore-scripts', { cwd: this.root });
|
||||
|
||||
await execaCommand('npm i --ignore-scripts', { cwd: this.root });
|
||||
await build({ ...config, root: this.root });
|
||||
}
|
||||
|
||||
/**
|
||||
* Read all the files from the test project's `.output` directory and combine them into a string
|
||||
* that can be used in a snapshot.
|
||||
*
|
||||
* Optionally, provide a list of filenames whose content is not printed (because it's inconsistent
|
||||
* or not relevant to a test).
|
||||
*/
|
||||
serializeOutput(): Promise<string> {
|
||||
return this.serializeDir('.output');
|
||||
serializeOutput(ignoreContentsOfFilenames?: string[]): Promise<string> {
|
||||
return this.serializeDir('.output', ignoreContentsOfFilenames);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,7 +102,16 @@ export class TestProject {
|
||||
);
|
||||
}
|
||||
|
||||
private async serializeDir(dir: string): Promise<string> {
|
||||
/**
|
||||
* Deeply print the filename and contents of all files in a directory.
|
||||
*
|
||||
* Optionally, provide a list of filenames whose content is not printed (because it's inconsistent
|
||||
* or not relevant to a test).
|
||||
*/
|
||||
private async serializeDir(
|
||||
dir: string,
|
||||
ignoreContentsOfFilenames?: string[],
|
||||
): Promise<string> {
|
||||
const outputFiles = await glob('**/*', {
|
||||
cwd: resolve(this.root, dir),
|
||||
ignore: ['**/node_modules', '**/.output'],
|
||||
@@ -104,19 +120,24 @@ export class TestProject {
|
||||
const fileContents = [];
|
||||
for (const file of outputFiles) {
|
||||
const path = resolve(this.root, dir, file);
|
||||
fileContents.push(await this.serializeFile(path));
|
||||
const isContentIgnored = !!ignoreContentsOfFilenames?.find(
|
||||
(ignoredFile) => normalizePath(path).endsWith(ignoredFile),
|
||||
);
|
||||
fileContents.push(await this.serializeFile(path, isContentIgnored));
|
||||
}
|
||||
return fileContents.join(`\n${''.padEnd(80, '=')}\n`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path An abosolute path to a file or a path relative to the root.
|
||||
* @param ignoreContents An optional boolean that, when true, causes this function to not print
|
||||
* the file contents.
|
||||
*/
|
||||
async serializeFile(path: string): Promise<string> {
|
||||
async serializeFile(path: string, ignoreContents?: boolean): Promise<string> {
|
||||
const absolutePath = resolve(this.root, path);
|
||||
return [
|
||||
normalizePath(relative(this.root, absolutePath)),
|
||||
await fs.readFile(absolutePath),
|
||||
ignoreContents ? '<contents-ignored>' : await fs.readFile(absolutePath),
|
||||
].join(`\n${''.padEnd(40, '-')}\n`);
|
||||
}
|
||||
|
||||
|
||||
+42
-18
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "wxt",
|
||||
"type": "module",
|
||||
"version": "0.5.3",
|
||||
"version": "0.8.6",
|
||||
"description": "Next gen framework for developing web extensions",
|
||||
"engines": {
|
||||
"node": ">=18.16.0",
|
||||
"node": ">=18",
|
||||
"pnpm": ">=8"
|
||||
},
|
||||
"repository": {
|
||||
@@ -44,13 +44,22 @@
|
||||
"import": "./dist/client.js",
|
||||
"types": "./dist/client.d.ts"
|
||||
},
|
||||
"./sandbox": {
|
||||
"import": "./dist/sandbox.js",
|
||||
"types": "./dist/sandbox.d.ts"
|
||||
},
|
||||
"./browser": {
|
||||
"import": "./dist/browser.js",
|
||||
"types": "./dist/browser.d.ts"
|
||||
},
|
||||
"./testing": {
|
||||
"require": "./dist/testing.cjs",
|
||||
"import": "./dist/testing.js",
|
||||
"types": "./dist/testing.d.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"wxt": "tsx src/cli/index.ts",
|
||||
"wxt": "tsx src/cli.ts",
|
||||
"build": "tsx scripts/build.ts",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --write .",
|
||||
@@ -59,32 +68,39 @@
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"prepare": "simple-git-hooks",
|
||||
"prepublish": "pnpm -s build",
|
||||
"docs:dev": "vitepress dev docs",
|
||||
"docs:build": "vitepress build docs",
|
||||
"docs:preview": "vitepress preview docs",
|
||||
"sync-releases": "pnpx changelogen@latest gh release all"
|
||||
"docs:gen": "typedoc --options docs/typedoc.json",
|
||||
"docs:dev": "pnpm -s docs:gen && vitepress dev docs",
|
||||
"docs:build": "pnpm -s docs:gen && vitepress build docs",
|
||||
"docs:preview": "pnpm -s docs:gen && vitepress preview docs",
|
||||
"sync-releases": "pnpx changelogen@latest gh release"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/webextension-polyfill": "^0.10.1",
|
||||
"@webext-core/fake-browser": "^1.2.2",
|
||||
"@webext-core/match-patterns": "^1.0.1",
|
||||
"@webext-core/isolated-element": "^1.0.4",
|
||||
"@webext-core/match-patterns": "^1.0.2",
|
||||
"async-mutex": "^0.4.0",
|
||||
"c12": "^1.4.2",
|
||||
"cac": "^6.7.14",
|
||||
"consola": "^3.2.3",
|
||||
"esbuild": "^0.19.4",
|
||||
"fast-glob": "^3.3.1",
|
||||
"filesize": "^10.0.8",
|
||||
"fs-extra": "^11.1.1",
|
||||
"get-port": "^7.0.0",
|
||||
"giget": "^1.1.2",
|
||||
"immer": "^10.0.2",
|
||||
"is-wsl": "^3.0.0",
|
||||
"jiti": "^1.19.1",
|
||||
"json5": "^2.2.3",
|
||||
"linkedom": "^0.15.1",
|
||||
"minimatch": "^9.0.3",
|
||||
"ora": "^7.0.1",
|
||||
"picocolors": "^1.0.0",
|
||||
"prompts": "^2.4.2",
|
||||
"rollup-plugin-visualizer": "^5.9.2",
|
||||
"unimport": "^3.1.0",
|
||||
"vite": "^4.4.7",
|
||||
"vite": "^4.4.11",
|
||||
"web-ext-run": "^0.1.0",
|
||||
"webextension-polyfill": "^0.10.0",
|
||||
"zip-dir": "^2.0.0"
|
||||
@@ -93,24 +109,27 @@
|
||||
"@faker-js/faker": "^8.0.2",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/lodash.merge": "^4.6.7",
|
||||
"@types/node": "^20.4.5",
|
||||
"@types/prettier": "^3.0.0",
|
||||
"@types/node": "^20.8.6",
|
||||
"@types/prompts": "^2.4.4",
|
||||
"@vitest/coverage-v8": "^0.34.1",
|
||||
"@vitest/coverage-v8": "^0.34.6",
|
||||
"execa": "^7.2.0",
|
||||
"happy-dom": "^12.4.0",
|
||||
"lint-staged": "^14.0.0",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"ora": "^7.0.1",
|
||||
"p-map": "^6.0.0",
|
||||
"prettier": "^3.0.1",
|
||||
"simple-git-hooks": "^2.9.0",
|
||||
"ts-morph": "^19.0.0",
|
||||
"tsup": "^7.1.0",
|
||||
"tsup": "^7.2.0",
|
||||
"tsx": "^3.12.7",
|
||||
"typescript": "^5.1.6",
|
||||
"typedoc": "^0.25.2",
|
||||
"typedoc-plugin-markdown": "4.0.0-next.23",
|
||||
"typedoc-vitepress-theme": "1.0.0-next.3",
|
||||
"typescript": "^5.2.2",
|
||||
"vitepress": "1.0.0-rc.10",
|
||||
"vitest": "^0.34.1",
|
||||
"vitest-mock-extended": "^1.1.4",
|
||||
"vitest": "^0.34.6",
|
||||
"vitest-mock-extended": "^1.3.1",
|
||||
"vue": "^3.3.4",
|
||||
"webextension-polyfill": "^0.10.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -123,6 +142,11 @@
|
||||
"lint-staged": {
|
||||
"*": "prettier --write"
|
||||
},
|
||||
"changelog": {
|
||||
"excludeAuthors": [
|
||||
"aaronklinker1@gmail.com"
|
||||
]
|
||||
},
|
||||
"pnpm": {
|
||||
"peerDependencyRules": {
|
||||
"ignoreMissing": [
|
||||
|
||||
Generated
+585
-349
File diff suppressed because it is too large
Load Diff
+83
-63
@@ -1,82 +1,102 @@
|
||||
import tsup from 'tsup';
|
||||
import glob from 'fast-glob';
|
||||
import { printFileList } from '../src/core/log/printFileList';
|
||||
import { formatDuration } from '../src/core/utils/formatDuration';
|
||||
import { printFileList } from '~/core/utils/log';
|
||||
import { formatDuration } from '~/core/utils/time';
|
||||
import ora from 'ora';
|
||||
import fs from 'fs-extra';
|
||||
import { consola } from 'consola';
|
||||
import pMap from 'p-map';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
const spinner = ora('Building WXT').start();
|
||||
const spinnerText = 'Building WXT';
|
||||
const spinner = ora(spinnerText).start();
|
||||
|
||||
const startTime = Date.now();
|
||||
const outDir = 'dist';
|
||||
const virtualEntrypoints = ['background', 'content-script'];
|
||||
await fs.rm(path.join(outDir, '*'), { recursive: true, force: true });
|
||||
|
||||
await fs.rm(outDir, { recursive: true, force: true });
|
||||
const preset: tsup.Options = {
|
||||
dts: true,
|
||||
silent: true,
|
||||
sourcemap: false,
|
||||
external: [
|
||||
'vite',
|
||||
'virtual:user-unlisted-script',
|
||||
'virtual:user-content-script',
|
||||
'virtual:user-background',
|
||||
],
|
||||
};
|
||||
|
||||
await Promise.all([
|
||||
tsup.build({
|
||||
function spinnerPMap(configs: tsup.Options[]) {
|
||||
let completed = 0;
|
||||
const updateSpinner = () => {
|
||||
spinner.text = `${spinnerText} [${completed}/${configs.length}]`;
|
||||
};
|
||||
updateSpinner();
|
||||
|
||||
return pMap(
|
||||
config,
|
||||
async (config) => {
|
||||
const res = await tsup.build(config);
|
||||
completed++;
|
||||
updateSpinner();
|
||||
return res;
|
||||
},
|
||||
{
|
||||
stopOnError: true,
|
||||
concurrency: process.env.CI === 'true' ? os.cpus().length : Infinity,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const config: tsup.Options[] = [
|
||||
// CJS/ESM
|
||||
{
|
||||
...preset,
|
||||
entry: {
|
||||
index: 'src/index.ts',
|
||||
testing: 'src/testing/index.ts',
|
||||
},
|
||||
format: ['cjs', 'esm'],
|
||||
sourcemap: true,
|
||||
dts: true,
|
||||
silent: true,
|
||||
external: ['vite'],
|
||||
}),
|
||||
tsup.build({
|
||||
entry: { cli: 'src/cli/index.ts' },
|
||||
clean: true,
|
||||
},
|
||||
// ESM-only
|
||||
{
|
||||
...preset,
|
||||
entry: {
|
||||
browser: 'src/browser.ts',
|
||||
sandbox: 'src/sandbox/index.ts',
|
||||
client: 'src/client/index.ts',
|
||||
},
|
||||
format: ['esm'],
|
||||
},
|
||||
{
|
||||
...preset,
|
||||
entry: {
|
||||
'virtual/background-entrypoint': 'src/virtual/background-entrypoint.ts',
|
||||
'virtual/content-script-entrypoint':
|
||||
'src/virtual/content-script-entrypoint.ts',
|
||||
'virtual/mock-browser': 'src/virtual/mock-browser.ts',
|
||||
'virtual/reload-html': 'src/virtual/reload-html.ts',
|
||||
'virtual/unlisted-script-entrypoint':
|
||||
'src/virtual/unlisted-script-entrypoint.ts',
|
||||
},
|
||||
format: ['esm'],
|
||||
splitting: false,
|
||||
dts: false,
|
||||
},
|
||||
// CJS-only
|
||||
{
|
||||
...preset,
|
||||
entry: {
|
||||
cli: 'src/cli.ts',
|
||||
},
|
||||
format: ['cjs'],
|
||||
sourcemap: 'inline',
|
||||
silent: true,
|
||||
external: ['vite'],
|
||||
}),
|
||||
tsup.build({
|
||||
entry: { client: 'src/client/index.ts' },
|
||||
format: ['esm'],
|
||||
sourcemap: 'inline',
|
||||
dts: true,
|
||||
silent: true,
|
||||
external: ['vite'],
|
||||
}),
|
||||
tsup.build({
|
||||
entry: { browser: 'src/client/browser.ts' },
|
||||
format: ['esm'],
|
||||
sourcemap: 'inline',
|
||||
dts: true,
|
||||
silent: true,
|
||||
external: ['vite'],
|
||||
}),
|
||||
...virtualEntrypoints.map((entryName) =>
|
||||
tsup.build({
|
||||
entry: {
|
||||
[`virtual-modules/${entryName}-entrypoint`]: `src/client/virtual-modules/${entryName}-entrypoint.ts`,
|
||||
},
|
||||
format: ['esm'],
|
||||
sourcemap: true,
|
||||
silent: true,
|
||||
external: [`virtual:user-${entryName}`, 'vite'],
|
||||
}),
|
||||
),
|
||||
tsup.build({
|
||||
entry: {
|
||||
'virtual-modules/reload-html': `src/client/virtual-modules/reload-html.ts`,
|
||||
},
|
||||
format: ['esm'],
|
||||
sourcemap: true,
|
||||
silent: true,
|
||||
external: ['vite'],
|
||||
}),
|
||||
tsup.build({
|
||||
entry: {
|
||||
'virtual-modules/fake-browser': `src/client/virtual-modules/fake-browser.ts`,
|
||||
},
|
||||
format: ['esm', 'cjs'],
|
||||
silent: true,
|
||||
external: ['vite'],
|
||||
}),
|
||||
]).catch((err) => {
|
||||
},
|
||||
];
|
||||
|
||||
await spinnerPMap(config).catch((err) => {
|
||||
spinner.fail();
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
|
||||
Vendored
-16
@@ -1,16 +0,0 @@
|
||||
declare module 'zip-dir' {
|
||||
// Represents the options object for zipdir function
|
||||
interface ZipDirOptions {
|
||||
saveTo?: string;
|
||||
filter?: (path: string, stat: import('fs').Stats) => boolean;
|
||||
each?: (path: string) => void;
|
||||
}
|
||||
|
||||
function zipdir(
|
||||
dirPath: string,
|
||||
options?: ZipDirOptions,
|
||||
callback?: (error: Error | null, buffer: Buffer) => void,
|
||||
): Promise<Buffer>;
|
||||
|
||||
export = zipdir;
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* @module wxt/browser
|
||||
*/
|
||||
import originalBrowser, { Browser, Runtime, I18n } from 'webextension-polyfill';
|
||||
|
||||
export interface AugmentedBrowser extends Browser {
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
import cac from 'cac';
|
||||
import { version } from '~/version';
|
||||
import { build, clean, createServer, initialize, prepare, zip } from '~/core';
|
||||
import consola, { LogLevels } from 'consola';
|
||||
import { printHeader } from '~/core/utils/log';
|
||||
import { formatDuration } from '~/core/utils/time';
|
||||
|
||||
const cli = cac('wxt');
|
||||
cli.help();
|
||||
cli.version(version);
|
||||
|
||||
cli.option('--debug', 'enable debug mode');
|
||||
|
||||
// DEV
|
||||
cli
|
||||
.command('[root]', 'start dev server')
|
||||
.option('-c, --config <file>', 'use specified config file')
|
||||
.option('-m, --mode <mode>', 'set env mode')
|
||||
.option('-b, --browser <browser>', 'specify a browser')
|
||||
.option('--mv3', 'target manifest v3')
|
||||
.option('--mv2', 'target manifest v2')
|
||||
.action(
|
||||
wrapAction(async (root, flags) => {
|
||||
const server = await createServer({
|
||||
root,
|
||||
mode: flags.mode,
|
||||
browser: flags.browser,
|
||||
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
|
||||
configFile: flags.config,
|
||||
debug: flags.debug,
|
||||
});
|
||||
await server.start();
|
||||
return { isOngoing: true };
|
||||
}),
|
||||
);
|
||||
|
||||
// BUILD
|
||||
cli
|
||||
.command('build [root]', 'build for production')
|
||||
.option('-c, --config <file>', 'use specified config file')
|
||||
.option('-m, --mode <mode>', 'set env mode')
|
||||
.option('-b, --browser <browser>', 'specify a browser')
|
||||
.option('--mv3', 'target manifest v3')
|
||||
.option('--mv2', 'target manifest v2')
|
||||
.option('--analyze', 'visualize extension bundle')
|
||||
.action(
|
||||
wrapAction(async (root, flags) => {
|
||||
await build({
|
||||
root,
|
||||
mode: flags.mode,
|
||||
browser: flags.browser,
|
||||
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
|
||||
configFile: flags.config,
|
||||
debug: flags.debug,
|
||||
analysis: {
|
||||
enabled: flags.analyze,
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// ZIP
|
||||
cli
|
||||
.command('zip [root]', 'build for production and zip output')
|
||||
.option('-c, --config <file>', 'use specified config file')
|
||||
.option('-m, --mode <mode>', 'set env mode')
|
||||
.option('-b, --browser <browser>', 'specify a browser')
|
||||
.option('--mv3', 'target manifest v3')
|
||||
.option('--mv2', 'target manifest v2')
|
||||
.action(
|
||||
wrapAction(async (root, flags) => {
|
||||
await zip({
|
||||
root,
|
||||
mode: flags.mode,
|
||||
browser: flags.browser,
|
||||
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
|
||||
configFile: flags.config,
|
||||
debug: flags.debug,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// PREPARE
|
||||
cli
|
||||
.command('prepare [root]', 'prepare typescript project')
|
||||
.option('-c, --config <file>', 'use specified config file')
|
||||
.action(
|
||||
wrapAction(async (root, flags) => {
|
||||
await prepare({
|
||||
root,
|
||||
configFile: flags.config,
|
||||
debug: flags.debug,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// CLEAN
|
||||
cli
|
||||
.command('clean [root]', 'clean generated files and caches')
|
||||
.alias('cleanup')
|
||||
.action(
|
||||
wrapAction(async (root, flags) => {
|
||||
await clean(root);
|
||||
}),
|
||||
);
|
||||
|
||||
// INIT
|
||||
cli
|
||||
.command('init [directory]', 'initialize a new project')
|
||||
.option('-t, --template <template>', 'template to use')
|
||||
.option('--pm <packageManager>', 'which package manager to use')
|
||||
.action(
|
||||
wrapAction(
|
||||
async (directory, flags) => {
|
||||
await initialize({
|
||||
directory,
|
||||
template: flags.template,
|
||||
packageManager: flags.pm,
|
||||
});
|
||||
},
|
||||
{ disableFinishedLog: true },
|
||||
),
|
||||
);
|
||||
|
||||
cli.parse();
|
||||
|
||||
/**
|
||||
* Wrap an action handler to add a timer, error handling, and maybe enable debug mode.
|
||||
*/
|
||||
function wrapAction(
|
||||
cb: (
|
||||
...args: any[]
|
||||
) => void | { isOngoing?: boolean } | Promise<void | { isOngoing?: boolean }>,
|
||||
options?: {
|
||||
disableFinishedLog?: boolean;
|
||||
},
|
||||
) {
|
||||
return async (...args: any[]) => {
|
||||
// Enable consola's debug mode globally at the start of all commands when the `--debug` flag is
|
||||
// passed
|
||||
const isDebug = !!args.find((arg) => arg?.debug);
|
||||
if (isDebug) {
|
||||
consola.level = LogLevels.debug;
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
printHeader();
|
||||
|
||||
const status = await cb(...args);
|
||||
|
||||
if (!status?.isOngoing && !options?.disableFinishedLog)
|
||||
consola.success(
|
||||
`Finished in ${formatDuration(Date.now() - startTime)}`,
|
||||
);
|
||||
} catch (err) {
|
||||
consola.fail(
|
||||
`Command failed after ${formatDuration(Date.now() - startTime)}`,
|
||||
);
|
||||
consola.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import * as wxt from '../..';
|
||||
import { defineCommand } from '../utils/defineCommand';
|
||||
|
||||
export const build = defineCommand<
|
||||
[
|
||||
root: string | undefined,
|
||||
flags: {
|
||||
mode?: string;
|
||||
config?: string;
|
||||
browser?: wxt.TargetBrowser;
|
||||
mv3?: boolean;
|
||||
mv2?: boolean;
|
||||
debug?: boolean;
|
||||
},
|
||||
]
|
||||
>(async (root, flags) => {
|
||||
const cliConfig: wxt.InlineConfig = {
|
||||
root,
|
||||
mode: flags.mode,
|
||||
browser: flags.browser,
|
||||
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
|
||||
configFile: flags.config,
|
||||
debug: flags.debug,
|
||||
};
|
||||
|
||||
await wxt.build(cliConfig);
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import { defineCommand } from '../utils/defineCommand';
|
||||
import * as wxt from '../..';
|
||||
|
||||
export const clean = defineCommand<
|
||||
[
|
||||
root: string | undefined,
|
||||
flags: {
|
||||
debug?: boolean;
|
||||
},
|
||||
]
|
||||
>(wxt.clean);
|
||||
@@ -1,30 +0,0 @@
|
||||
import * as wxt from '../..';
|
||||
import { defineCommand } from '../utils/defineCommand';
|
||||
|
||||
export const dev = defineCommand<
|
||||
[
|
||||
root: string | undefined,
|
||||
flags: {
|
||||
mode?: string;
|
||||
config?: string;
|
||||
browser?: wxt.TargetBrowser;
|
||||
mv3?: boolean;
|
||||
mv2?: boolean;
|
||||
debug?: boolean;
|
||||
},
|
||||
]
|
||||
>(async (root, flags) => {
|
||||
const cliConfig: wxt.InlineConfig = {
|
||||
root,
|
||||
mode: flags.mode,
|
||||
browser: flags.browser,
|
||||
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
|
||||
configFile: flags.config,
|
||||
debug: flags.debug,
|
||||
};
|
||||
|
||||
const server = await wxt.createServer(cliConfig);
|
||||
await server.start();
|
||||
|
||||
return true;
|
||||
});
|
||||
@@ -1,7 +0,0 @@
|
||||
export * from './build';
|
||||
export * from './dev';
|
||||
export * from './init';
|
||||
export * from './prepare';
|
||||
export * from './publish';
|
||||
export * from './zip';
|
||||
export * from './clean';
|
||||
@@ -1,27 +0,0 @@
|
||||
import { getInternalConfig } from '../../core/utils/getInternalConfig';
|
||||
import { findEntrypoints } from '../../core/build/findEntrypoints';
|
||||
import { generateTypesDir } from '../../core/build/generateTypesDir';
|
||||
import { defineCommand } from '../utils/defineCommand';
|
||||
import * as wxt from '../..';
|
||||
|
||||
export const prepare = defineCommand<
|
||||
[
|
||||
root: string | undefined,
|
||||
flags: {
|
||||
config?: string;
|
||||
debug?: boolean;
|
||||
},
|
||||
]
|
||||
>(async (root, flags) => {
|
||||
const cliConfig: wxt.InlineConfig = {
|
||||
root,
|
||||
configFile: flags.config,
|
||||
debug: flags.debug,
|
||||
};
|
||||
const config = await getInternalConfig(cliConfig, 'build');
|
||||
|
||||
config.logger.info('Generating types...');
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
await generateTypesDir(entrypoints, config);
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import { consola } from 'consola';
|
||||
import { defineCommand } from '../utils/defineCommand';
|
||||
|
||||
export const publish = defineCommand(
|
||||
async (
|
||||
root: any,
|
||||
{ config: configFile, debug }: { config?: string; debug?: string },
|
||||
) => {
|
||||
consola.warn('wxt publish: Not implemented');
|
||||
},
|
||||
);
|
||||
@@ -1,32 +0,0 @@
|
||||
import * as wxt from '../..';
|
||||
import { buildInternal } from '../../core/build';
|
||||
import { getInternalConfig } from '../../core/utils/getInternalConfig';
|
||||
import { zipExtension } from '../../core/zip';
|
||||
import { defineCommand } from '../utils/defineCommand';
|
||||
|
||||
export const zip = defineCommand<
|
||||
[
|
||||
root: string | undefined,
|
||||
flags: {
|
||||
mode?: string;
|
||||
config?: string;
|
||||
browser?: wxt.TargetBrowser;
|
||||
mv3?: boolean;
|
||||
mv2?: boolean;
|
||||
debug?: boolean;
|
||||
},
|
||||
]
|
||||
>(async (root, flags) => {
|
||||
const cliConfig: wxt.InlineConfig = {
|
||||
root,
|
||||
mode: flags.mode,
|
||||
browser: flags.browser,
|
||||
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
|
||||
configFile: flags.config,
|
||||
debug: flags.debug,
|
||||
};
|
||||
|
||||
const config = await getInternalConfig(cliConfig, 'build');
|
||||
const output = await buildInternal(config);
|
||||
await zipExtension(config, output);
|
||||
});
|
||||
@@ -1,63 +0,0 @@
|
||||
import cac from 'cac';
|
||||
import { version } from '../../package.json';
|
||||
import * as commands from './commands';
|
||||
|
||||
const cli = cac('wxt');
|
||||
cli.help();
|
||||
cli.version(version);
|
||||
|
||||
cli.option('--debug', 'enable debug mode');
|
||||
|
||||
// DEV
|
||||
cli
|
||||
.command('[root]', 'start dev server')
|
||||
.option('-c, --config <file>', 'use specified config file')
|
||||
.option('-m, --mode <mode>', 'set env mode')
|
||||
.option('-b, --browser <browser>', 'specify a browser')
|
||||
.option('--mv3', 'target manifest v3')
|
||||
.option('--mv2', 'target manifest v2')
|
||||
.action(commands.dev);
|
||||
|
||||
// BUILD
|
||||
cli
|
||||
.command('build [root]', 'build for production')
|
||||
.option('-c, --config <file>', 'use specified config file')
|
||||
.option('-m, --mode <mode>', 'set env mode')
|
||||
.option('-b, --browser <browser>', 'specify a browser')
|
||||
.option('--mv3', 'target manifest v3')
|
||||
.option('--mv2', 'target manifest v2')
|
||||
.action(commands.build);
|
||||
|
||||
// ZIP
|
||||
cli
|
||||
.command('zip [root]', 'build for production and zip output')
|
||||
.option('-c, --config <file>', 'use specified config file')
|
||||
.option('-m, --mode <mode>', 'set env mode')
|
||||
.option('-b, --browser <browser>', 'specify a browser')
|
||||
.option('--mv3', 'target manifest v3')
|
||||
.option('--mv2', 'target manifest v2')
|
||||
.action(commands.zip);
|
||||
|
||||
// PREPARE
|
||||
cli
|
||||
.command('prepare [root]', 'prepare typescript project')
|
||||
.option('-c, --config <file>', 'use specified config file')
|
||||
.action(commands.prepare);
|
||||
|
||||
// CLEAN
|
||||
cli
|
||||
.command('clean [root]', 'clean generated files and caches')
|
||||
.alias('cleanup')
|
||||
.action(commands.clean);
|
||||
|
||||
// PUBLISH
|
||||
cli.command('publish [root]', 'publish to stores').action(commands.publish);
|
||||
|
||||
// INIT
|
||||
cli
|
||||
.command('init [directory]', 'initialize a new project')
|
||||
.option('-t, --template <template>', 'template to use')
|
||||
.option('--pm <packageManager>', 'which package manager to use')
|
||||
.action(commands.init);
|
||||
|
||||
cli.parse();
|
||||
@@ -0,0 +1,25 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { defineBackground } from '~/client/define-background';
|
||||
import { BackgroundDefinition } from '~/types';
|
||||
|
||||
describe('defineBackground', () => {
|
||||
it('should return the object definition when given an object', () => {
|
||||
const definition: BackgroundDefinition = {
|
||||
include: [''],
|
||||
persistent: false,
|
||||
main: vi.fn(),
|
||||
};
|
||||
|
||||
const actual = defineBackground(definition);
|
||||
|
||||
expect(actual).toEqual(definition);
|
||||
});
|
||||
|
||||
it('should return the object definition when given a main function', () => {
|
||||
const main = vi.fn();
|
||||
|
||||
const actual = defineBackground(main);
|
||||
|
||||
expect(actual).toEqual({ main });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,335 @@
|
||||
/** @vitest-environment happy-dom */
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { createContentScriptUi } from '~/client/content-scripts/content-script-ui';
|
||||
import { ContentScriptContext } from '~/client/content-scripts/content-script-context';
|
||||
import { createIsolatedElement } from '@webext-core/isolated-element';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { browser } from '~/browser';
|
||||
|
||||
vi.mock('@webext-core/isolated-element', async () => {
|
||||
const { vi } = await import('vitest');
|
||||
return {
|
||||
createIsolatedElement: vi.fn(),
|
||||
};
|
||||
});
|
||||
const createIsolatedElementMock = vi.mocked(createIsolatedElement);
|
||||
|
||||
const testApp = (container: Element) => {
|
||||
const app = document.createElement('div');
|
||||
app.textContent = 'App';
|
||||
container.append(app);
|
||||
};
|
||||
|
||||
const createCtx = () => new ContentScriptContext('test');
|
||||
|
||||
const fetch = vi.fn();
|
||||
|
||||
describe('createContentScriptUi', () => {
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = `
|
||||
<div id="parent">
|
||||
<p id="one">one</p>
|
||||
<p id="two">two</p>
|
||||
<p id="three"></p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
window.fetch = fetch;
|
||||
fetch.mockResolvedValue({ text: () => Promise.resolve('') });
|
||||
|
||||
createIsolatedElementMock.mockImplementation(async (config) => {
|
||||
const parentElement = document.createElement(config.name);
|
||||
const isolatedElement = document.createElement('html');
|
||||
parentElement.append(isolatedElement);
|
||||
|
||||
return {
|
||||
isolatedElement,
|
||||
parentElement: parentElement,
|
||||
shadow: mock<ShadowRoot>({
|
||||
querySelector: (selector: string) => {
|
||||
if (selector === 'html') return isolatedElement;
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
describe('css', () => {
|
||||
it('should load the CSS for the current entrypoint when cssInjectionMode=ui', async () => {
|
||||
fetch.mockResolvedValue({ text: () => Promise.resolve('body {}') });
|
||||
const ctx = new ContentScriptContext('test', {
|
||||
matches: [],
|
||||
cssInjectionMode: 'ui',
|
||||
});
|
||||
|
||||
await createContentScriptUi(ctx, {
|
||||
name: 'test',
|
||||
type: 'inline',
|
||||
mount: testApp,
|
||||
});
|
||||
|
||||
expect(fetch).toBeCalledTimes(1);
|
||||
expect(fetch).toBeCalledWith(
|
||||
`chrome-extension://${browser.runtime.id}/content-scripts/${__ENTRYPOINT__}.css`,
|
||||
);
|
||||
expect(createIsolatedElementMock).toBeCalledTimes(1);
|
||||
expect(createIsolatedElementMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
css: {
|
||||
textContent: 'body {}',
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should still load the UI when fetch fails to load CSS file when cssInjectionMode=ui', async () => {
|
||||
const error = Error('Test fetch error');
|
||||
fetch.mockRejectedValue(error);
|
||||
const ctx = new ContentScriptContext('test', {
|
||||
matches: [],
|
||||
cssInjectionMode: 'ui',
|
||||
});
|
||||
|
||||
await createContentScriptUi(ctx, {
|
||||
name: 'test',
|
||||
type: 'inline',
|
||||
mount: testApp,
|
||||
});
|
||||
|
||||
expect(fetch).toBeCalledTimes(1);
|
||||
expect(fetch).toBeCalledWith(
|
||||
`chrome-extension://${browser.runtime.id}/content-scripts/${__ENTRYPOINT__}.css`,
|
||||
);
|
||||
expect(createIsolatedElementMock).toBeCalledTimes(1);
|
||||
expect(createIsolatedElementMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
css: {
|
||||
textContent: '',
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.each(['manifest', 'manual'] as const)(
|
||||
'should not fetch CSS when cssInjectionMode=%s',
|
||||
async (cssInjectionMode) => {
|
||||
const ctx = new ContentScriptContext('test', {
|
||||
matches: [],
|
||||
cssInjectionMode,
|
||||
});
|
||||
|
||||
await createContentScriptUi(ctx, {
|
||||
name: 'test',
|
||||
type: 'inline',
|
||||
mount: testApp,
|
||||
});
|
||||
|
||||
expect(fetch).not.toBeCalled();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('mount', () => {
|
||||
describe('append option', () => {
|
||||
it.each([undefined, 'last' as const])(
|
||||
'should append the element as the last child to the anchor when append=%s',
|
||||
async (append) => {
|
||||
const ui = await createContentScriptUi(createCtx(), {
|
||||
name: 'test-app',
|
||||
type: 'inline',
|
||||
anchor: '#parent',
|
||||
append,
|
||||
mount: testApp,
|
||||
});
|
||||
ui.mount();
|
||||
|
||||
expect(
|
||||
document.querySelector('#parent > :nth-child(4)')?.tagName,
|
||||
).toEqual('TEST-APP');
|
||||
},
|
||||
);
|
||||
|
||||
it('should append the element as the first child to the anchor when append=first', async () => {
|
||||
const ui = await createContentScriptUi(createCtx(), {
|
||||
name: 'test-app',
|
||||
type: 'inline',
|
||||
anchor: '#parent',
|
||||
append: 'first',
|
||||
mount: testApp,
|
||||
});
|
||||
ui.mount();
|
||||
|
||||
expect(
|
||||
document.querySelector('#parent > :nth-child(1)')?.tagName,
|
||||
).toEqual('TEST-APP');
|
||||
});
|
||||
|
||||
it('should append the element normally when append=first but there are no other children', async () => {
|
||||
const ui = await createContentScriptUi(createCtx(), {
|
||||
name: 'test-app',
|
||||
type: 'inline',
|
||||
anchor: '#three',
|
||||
append: 'first',
|
||||
mount: testApp,
|
||||
});
|
||||
ui.mount();
|
||||
|
||||
expect(
|
||||
document.querySelector('#three > :nth-child(1)')?.tagName,
|
||||
).toEqual('TEST-APP');
|
||||
});
|
||||
|
||||
it('should replace the anchor with the element when append=replace', async () => {
|
||||
const ui = await createContentScriptUi(createCtx(), {
|
||||
name: 'test-app',
|
||||
type: 'inline',
|
||||
anchor: '#two',
|
||||
append: 'replace',
|
||||
mount: testApp,
|
||||
});
|
||||
ui.mount();
|
||||
|
||||
expect(
|
||||
document.querySelector('#parent > :nth-child(2)')?.tagName,
|
||||
).toEqual('TEST-APP');
|
||||
});
|
||||
|
||||
it('should append the element after the anchor when append=before', async () => {
|
||||
const ui = await createContentScriptUi(createCtx(), {
|
||||
name: 'test-app',
|
||||
type: 'inline',
|
||||
anchor: '#two',
|
||||
append: 'before',
|
||||
mount: testApp,
|
||||
});
|
||||
ui.mount();
|
||||
|
||||
expect(
|
||||
document.querySelector('#parent > :nth-child(2)')?.tagName,
|
||||
).toEqual('TEST-APP');
|
||||
});
|
||||
|
||||
it('should append the element after the anchor when append=after', async () => {
|
||||
const ui = await createContentScriptUi(createCtx(), {
|
||||
name: 'test-app',
|
||||
type: 'inline',
|
||||
anchor: '#two',
|
||||
append: 'after',
|
||||
mount: testApp,
|
||||
});
|
||||
ui.mount();
|
||||
|
||||
// Happy DOM doesn't work in this case, so we just make sure the element is added.
|
||||
// expect(
|
||||
// document.querySelector('#parent > :nth-child(3)')?.tagName,
|
||||
// ).toEqual('TEST-APP');
|
||||
expect(document.querySelector('test-app')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should apply a custom function', async () => {
|
||||
const ui = await createContentScriptUi(createCtx(), {
|
||||
name: 'test-app',
|
||||
type: 'inline',
|
||||
anchor: '#three',
|
||||
append: (anchor, ui) => anchor.replaceWith(ui),
|
||||
mount: testApp,
|
||||
});
|
||||
ui.mount();
|
||||
|
||||
expect(
|
||||
document.querySelector('#parent > :nth-child(3)')?.tagName,
|
||||
).toEqual('TEST-APP');
|
||||
});
|
||||
});
|
||||
|
||||
it('should default the anchor to the body when unset', async () => {
|
||||
const ui = await createContentScriptUi(createCtx(), {
|
||||
name: 'test-app',
|
||||
type: 'inline',
|
||||
mount: testApp,
|
||||
});
|
||||
ui.mount();
|
||||
|
||||
expect(document.querySelector('body > :nth-child(2)')?.tagName).toEqual(
|
||||
'TEST-APP',
|
||||
);
|
||||
});
|
||||
|
||||
it.each(['#four', () => document.querySelector('#four')])(
|
||||
"should throw an error if the anchor doesn't exist",
|
||||
async (anchor) => {
|
||||
const ui = await createContentScriptUi(createCtx(), {
|
||||
name: 'test',
|
||||
type: 'inline',
|
||||
anchor,
|
||||
mount: testApp,
|
||||
});
|
||||
|
||||
expect(ui.mount).toThrow(
|
||||
'Failed to mount content script ui: could not find anchor element',
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it("should not fail if the ui hasn't been mounted", async () => {
|
||||
const ui = await createContentScriptUi(createCtx(), {
|
||||
name: 'test-app',
|
||||
type: 'inline',
|
||||
mount: testApp,
|
||||
});
|
||||
ui.remove();
|
||||
});
|
||||
|
||||
it('should automatically remove the UI when the context is invalidated', async () => {
|
||||
const ctx = createCtx();
|
||||
const ui = await createContentScriptUi(ctx, {
|
||||
name: 'test-app',
|
||||
type: 'inline',
|
||||
mount: testApp,
|
||||
});
|
||||
ui.mount();
|
||||
|
||||
expect(document.querySelector('test-app')).toBeDefined();
|
||||
|
||||
ctx.abort();
|
||||
expect(document.querySelector('test-app')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('type', () => {
|
||||
it.each(['inline', 'overlay', 'modal'] as const)(
|
||||
'should render type=%s',
|
||||
async (type) => {
|
||||
const ui = await createContentScriptUi(createCtx(), {
|
||||
name: 'test-app',
|
||||
type,
|
||||
mount: testApp,
|
||||
});
|
||||
ui.mount();
|
||||
|
||||
expect(document.querySelector('test-app')).toBeDefined();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('anchor', () => {
|
||||
it.each([
|
||||
() => '#two',
|
||||
() => () => '#two',
|
||||
() => document.querySelector('#two'),
|
||||
() => () => document.querySelector('#two'),
|
||||
])('should render anchor=%s', async (getAnchor) => {
|
||||
const ui = await createContentScriptUi(createCtx(), {
|
||||
name: 'test-app',
|
||||
type: 'inline',
|
||||
anchor: getAnchor(),
|
||||
mount: testApp,
|
||||
});
|
||||
ui.mount();
|
||||
|
||||
expect(document.querySelector('test-app')).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { defineContentScript } from '~/client/content-scripts/define-content-script';
|
||||
import { ContentScriptDefinition } from '~/types';
|
||||
|
||||
describe('defineContentScript', () => {
|
||||
it('should return the object passed in', () => {
|
||||
const definition: ContentScriptDefinition = {
|
||||
matches: [],
|
||||
include: [''],
|
||||
main: vi.fn(),
|
||||
};
|
||||
|
||||
const actual = defineContentScript(definition);
|
||||
|
||||
expect(actual).toEqual(definition);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,195 @@
|
||||
import { ContentScriptDefinition } from '~/types';
|
||||
import { browser } from '~/browser';
|
||||
import { logger } from '~/client/utils/logger';
|
||||
|
||||
/**
|
||||
* Implements [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController).
|
||||
* Used to detect and stop content script code when the script is invalidated.
|
||||
*
|
||||
* It also provides several utilities like `ctx.setTimeout` and `ctx.setInterval` that should be used in
|
||||
* content scripts instead of `window.setTimeout` or `window.setInterval`.
|
||||
*/
|
||||
export class ContentScriptContext implements AbortController {
|
||||
private static SCRIPT_STARTED_MESSAGE_TYPE = 'wxt:content-script-started';
|
||||
|
||||
#isTopFrame = window.self === window.top;
|
||||
#abortController: AbortController;
|
||||
|
||||
constructor(
|
||||
private readonly contentScriptName: string,
|
||||
public readonly options?: Omit<ContentScriptDefinition, 'main'>,
|
||||
) {
|
||||
this.#abortController = new AbortController();
|
||||
if (this.#isTopFrame) {
|
||||
this.#stopOldScripts();
|
||||
}
|
||||
this.setTimeout(() => {
|
||||
// Run on next tick so the listener it adds isn't triggered by stopOldScript
|
||||
this.#listenForNewerScripts();
|
||||
});
|
||||
}
|
||||
|
||||
get signal() {
|
||||
return this.#abortController.signal;
|
||||
}
|
||||
|
||||
abort(reason?: any): void {
|
||||
return this.#abortController.abort(reason);
|
||||
}
|
||||
|
||||
get isInvalid(): boolean {
|
||||
if (browser.runtime.id == null) {
|
||||
this.notifyInvalidated(); // Sets `signal.aborted` to true
|
||||
}
|
||||
return this.signal.aborted;
|
||||
}
|
||||
|
||||
get isValid(): boolean {
|
||||
return !this.isInvalid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a listener that is called when the content script's context is invalidated.
|
||||
*
|
||||
* @returns A function to remove the listener.
|
||||
*
|
||||
* @example
|
||||
* browser.runtime.onMessage.addListener(cb);
|
||||
* const removeInvalidatedListener = ctx.onInvalidated(() => {
|
||||
* browser.runtime.onMessage.removeListener(cb);
|
||||
* })
|
||||
* // ...
|
||||
* removeInvalidatedListener();
|
||||
*/
|
||||
onInvalidated(cb: () => void): () => void {
|
||||
this.signal.addEventListener('abort', cb);
|
||||
return () => this.signal.removeEventListener('abort', cb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a promise that never resolves. Useful if you have an async function that shouldn't run
|
||||
* after the context is expired.
|
||||
*
|
||||
* @example
|
||||
* const getValueFromStorage = async () => {
|
||||
* if (ctx.isInvalid) return ctx.block();
|
||||
*
|
||||
* // ...
|
||||
* }
|
||||
*/
|
||||
block<T>(): Promise<T> {
|
||||
return new Promise(() => {
|
||||
// noop
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around `window.setInterval` that automatically clears the interval when invalidated.
|
||||
*/
|
||||
setInterval(handler: () => void, timeout?: number): number {
|
||||
const id = setInterval(() => {
|
||||
if (this.isValid) handler();
|
||||
}, timeout) as unknown as number;
|
||||
this.onInvalidated(() => clearInterval(id));
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around `window.setTimeout` that automatically clears the interval when invalidated.
|
||||
*/
|
||||
setTimeout(handler: () => void, timeout?: number): number {
|
||||
const id = setTimeout(() => {
|
||||
if (this.isValid) handler();
|
||||
}, timeout) as unknown as number;
|
||||
this.onInvalidated(() => clearTimeout(id));
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around `window.requestAnimationFrame` that automatically cancels the request when
|
||||
* invalidated.
|
||||
*/
|
||||
requestAnimationFrame(callback: FrameRequestCallback): number {
|
||||
const id = requestAnimationFrame((...args) => {
|
||||
if (this.isValid) callback(...args);
|
||||
});
|
||||
|
||||
this.onInvalidated(() => cancelAnimationFrame(id));
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around `window.requestIdleCallback` that automatically cancels the request when
|
||||
* invalidated.
|
||||
*/
|
||||
requestIdleCallback(
|
||||
callback: IdleRequestCallback,
|
||||
options?: IdleRequestOptions,
|
||||
): number {
|
||||
const id = requestIdleCallback((...args) => {
|
||||
if (!this.signal.aborted) callback(...args);
|
||||
}, options);
|
||||
|
||||
this.onInvalidated(() => cancelIdleCallback(id));
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call `target.addEventListener` and remove the event listener when the context is invalidated.
|
||||
*
|
||||
* @example
|
||||
* ctx.addEventListener(window, "mousemove", () => {
|
||||
* // ...
|
||||
* });
|
||||
* ctx.addEventListener(document, "visibilitychange", () => {
|
||||
* // ...
|
||||
* });
|
||||
*/
|
||||
addEventListener(
|
||||
target: any,
|
||||
type: string,
|
||||
handler: (event: Event) => void,
|
||||
options?: AddEventListenerOptions,
|
||||
) {
|
||||
target.addEventListener?.(type, handler, options);
|
||||
this.onInvalidated(
|
||||
() => target.removeEventListener?.(type, handler, options),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Abort the abort controller and execute all `onInvalidated` listeners.
|
||||
*/
|
||||
notifyInvalidated() {
|
||||
this.abort('Content script context invalidated');
|
||||
logger.debug(
|
||||
`Content script "${this.contentScriptName}" context invalidated`,
|
||||
);
|
||||
}
|
||||
|
||||
#stopOldScripts() {
|
||||
// Use postMessage so it get's sent to all the frames of the page.
|
||||
window.postMessage(
|
||||
{
|
||||
event: ContentScriptContext.SCRIPT_STARTED_MESSAGE_TYPE,
|
||||
contentScriptName: this.contentScriptName,
|
||||
},
|
||||
'*',
|
||||
);
|
||||
}
|
||||
|
||||
#listenForNewerScripts() {
|
||||
const cb = (event: MessageEvent) => {
|
||||
if (
|
||||
event.data?.type === ContentScriptContext.SCRIPT_STARTED_MESSAGE_TYPE &&
|
||||
event.data?.contentScriptName === this.contentScriptName
|
||||
) {
|
||||
this.notifyInvalidated();
|
||||
}
|
||||
};
|
||||
|
||||
addEventListener('message', cb);
|
||||
this.onInvalidated(() => removeEventListener('message', cb));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
import { createIsolatedElement } from '@webext-core/isolated-element';
|
||||
import { browser } from '~/browser';
|
||||
import { logger } from '~/client/utils/logger';
|
||||
import { ContentScriptContext } from './content-script-context';
|
||||
|
||||
/**
|
||||
* Utility for mounting content script UI's with isolated styles. Automatically removed from the DOM
|
||||
* when the content script's context is invalidated.
|
||||
*
|
||||
* See https://wxt.dev/entrypoints/content-scripts.html#ui for full documentation.
|
||||
*
|
||||
* @example
|
||||
* // entrypoints/example-ui.content/index.ts
|
||||
* import "./style.css"
|
||||
*
|
||||
* export default defineContentScript({
|
||||
* matches: ["*://*.google.com/*"],
|
||||
* cssInjectionMode: "ui",
|
||||
*
|
||||
* async main(ctx) {
|
||||
* const ui = await createContentScriptUi(ctx, {
|
||||
* name: "example-overlay",
|
||||
* type: "modal",
|
||||
* mount(container) {
|
||||
* const app = document.createElement("div");
|
||||
* app.textContent = "Content Script UI";
|
||||
* container.append(app);
|
||||
* }
|
||||
* })
|
||||
* ui.mount();
|
||||
* }
|
||||
* })
|
||||
*/
|
||||
export async function createContentScriptUi<TApp>(
|
||||
ctx: ContentScriptContext,
|
||||
options: ContentScriptUiOptions<TApp>,
|
||||
): Promise<ContentScriptUi<TApp>> {
|
||||
const css = [options.css ?? ''];
|
||||
if (ctx.options?.cssInjectionMode === 'ui') {
|
||||
css.push(await loadCss());
|
||||
}
|
||||
|
||||
const {
|
||||
isolatedElement: uiContainer,
|
||||
parentElement: shadowHost,
|
||||
shadow,
|
||||
} = await createIsolatedElement({
|
||||
name: options.name,
|
||||
css: {
|
||||
textContent: css.join('\n').trim(),
|
||||
},
|
||||
mode: 'open',
|
||||
});
|
||||
|
||||
const getAnchor = (): Element | undefined => {
|
||||
if (options.anchor == null) return document.body;
|
||||
|
||||
let resolved =
|
||||
typeof options.anchor === 'function' ? options.anchor() : options.anchor;
|
||||
if (typeof resolved === 'string')
|
||||
return document.querySelector<Element>(resolved) ?? undefined;
|
||||
return resolved ?? undefined;
|
||||
};
|
||||
|
||||
let mounted: TApp;
|
||||
|
||||
const mount = () => {
|
||||
const anchor = getAnchor();
|
||||
if (anchor == null)
|
||||
throw Error(
|
||||
'Failed to mount content script ui: could not find anchor element',
|
||||
);
|
||||
|
||||
// Mount UI inside shadow root
|
||||
mounted = options.mount(uiContainer);
|
||||
|
||||
// Add shadow root element to DOM
|
||||
switch (options.append) {
|
||||
case undefined:
|
||||
case 'last':
|
||||
anchor.append(shadowHost);
|
||||
break;
|
||||
case 'first':
|
||||
if (anchor.firstChild) {
|
||||
anchor.insertBefore(shadowHost, anchor.firstChild);
|
||||
} else {
|
||||
anchor.append(shadowHost);
|
||||
}
|
||||
break;
|
||||
case 'replace':
|
||||
anchor.replaceWith(shadowHost);
|
||||
break;
|
||||
case 'after':
|
||||
anchor.replaceWith(anchor, shadowHost);
|
||||
break;
|
||||
case 'before':
|
||||
anchor.replaceWith(shadowHost, anchor);
|
||||
break;
|
||||
default:
|
||||
options.append(anchor, shadowHost);
|
||||
break;
|
||||
}
|
||||
|
||||
// Apply types
|
||||
if (options.type !== 'inline') {
|
||||
if (options.zIndex != null)
|
||||
shadowHost.style.zIndex = String(options.zIndex);
|
||||
|
||||
shadowHost.style.overflow = 'visible';
|
||||
shadowHost.style.position = 'relative';
|
||||
shadowHost.style.width = '0';
|
||||
shadowHost.style.height = '0';
|
||||
shadowHost.style.display = 'block';
|
||||
|
||||
const html = shadow.querySelector('html')!;
|
||||
// HTML doesn't exist in tests
|
||||
if (options.type === 'overlay') {
|
||||
html.style.position = 'absolute';
|
||||
if (options.alignment?.startsWith('bottom-')) html.style.bottom = '0';
|
||||
else html.style.top = '0';
|
||||
|
||||
if (options.alignment?.endsWith('-right')) html.style.right = '0';
|
||||
else html.style.left = '0';
|
||||
} else {
|
||||
html.style.position = 'fixed';
|
||||
html.style.top = '0';
|
||||
html.style.bottom = '0';
|
||||
html.style.left = '0';
|
||||
html.style.right = '0';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const remove = () => {
|
||||
// Detatch shadow root from DOM
|
||||
shadowHost.remove();
|
||||
// Cleanup mounted state
|
||||
options.onRemove?.(mounted);
|
||||
// Remove children from uiContainer
|
||||
while (uiContainer.lastChild)
|
||||
uiContainer.removeChild(uiContainer.lastChild);
|
||||
};
|
||||
|
||||
ctx.onInvalidated(remove);
|
||||
|
||||
return {
|
||||
shadow,
|
||||
shadowHost,
|
||||
uiContainer,
|
||||
mount,
|
||||
remove,
|
||||
mounted: mounted!,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the CSS for the current entrypoint.
|
||||
*/
|
||||
async function loadCss(): Promise<string> {
|
||||
const url = browser.runtime.getURL(`/content-scripts/${__ENTRYPOINT__}.css`);
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
const css = await res.text();
|
||||
|
||||
// Replace :root selectors with :host since we're in a shadow root
|
||||
return css.replaceAll(':root', ':host');
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
`Failed to load styles @ ${url}. Did you forget to import the stylesheet in your entrypoint?`,
|
||||
err,
|
||||
);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export interface ContentScriptUi<TApp> {
|
||||
/**
|
||||
* The `HTMLElement` hosting the shadow root used to isolate the UI's styles. This is the element
|
||||
* that get's added to the DOM. This element's style is not isolated from the webpage.
|
||||
*/
|
||||
shadowHost: HTMLElement;
|
||||
/**
|
||||
* The container element inside the `ShadowRoot` whose styles are isolated. The UI is mounted
|
||||
* inside this `HTMLElement`.
|
||||
*/
|
||||
uiContainer: HTMLElement;
|
||||
/**
|
||||
* The shadow root performing the isolation.
|
||||
*/
|
||||
shadow: ShadowRoot;
|
||||
/**
|
||||
* Custom data returned from the `options.mount` function.
|
||||
*/
|
||||
mounted: TApp;
|
||||
/**
|
||||
* Function that mounts or remounts the UI on the page.
|
||||
*/
|
||||
mount: () => void;
|
||||
/**
|
||||
* Function that removes the UI from the webpage.
|
||||
*/
|
||||
remove: () => void;
|
||||
}
|
||||
|
||||
export interface BaseContentScriptUiOptions<TApp> {
|
||||
/**
|
||||
* The name of the custom component used to host the ShadowRoot. Must be kebab-case.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* In combination with `anchor`, decide how to add the UI to the DOM.
|
||||
*
|
||||
* - `"last"` (default) - Add the UI as the last child of the `anchor` element
|
||||
* - `"first"` - Add the UI as the last child of the `anchor` element
|
||||
* - `"replace"` - Replace the `anchor` element with the UI.
|
||||
* - `"before"` - Add the UI as the sibling before the `anchor` element
|
||||
* - `"after"` - Add the UI as the sibling after the `anchor` element
|
||||
* - `(anchor, ui) => void` - Customizable function that let's you add the UI to the DOM
|
||||
*/
|
||||
append?: ContentScriptAppendMode | ((anchor: Element, ui: Element) => void);
|
||||
/**
|
||||
* A CSS selector, element, or function that returns one of the two. Along with `append`, the
|
||||
* `anchor` dictates where in the page the UI will be added.
|
||||
*/
|
||||
anchor?:
|
||||
| string
|
||||
| Element
|
||||
| null
|
||||
| undefined
|
||||
| (() => string | Element | null | undefined);
|
||||
/**
|
||||
* Callback executed when mounting the UI. This function should create and append the UI to the
|
||||
* `container` element. It is called every time `ui.mount()` is called
|
||||
*
|
||||
* Optionally return a value that can be accessed at `ui.mounted` or in the `onRemove` callback.
|
||||
*/
|
||||
mount: (container: Element) => TApp;
|
||||
/**
|
||||
* Callback called when the UI is removed from the webpage. Use to cleanup your UI, like
|
||||
* unmounting your vue or react apps.
|
||||
*/
|
||||
onRemove?: (mounted: TApp) => void;
|
||||
/**
|
||||
* Custom CSS text to apply to the UI. If your content script imports/generates CSS and you've
|
||||
* set `cssInjectionMode: "ui"`, the imported CSS will be included automatically. You do not need
|
||||
* to pass those styles in here. This is for any additional styles not in the imported CSS.
|
||||
*
|
||||
* See https://wxt.dev/entrypoints/content-scripts.html#ui for more info.
|
||||
*/
|
||||
css?: string;
|
||||
}
|
||||
|
||||
export type OverlayContentScriptUiOptions<TApp> =
|
||||
BaseContentScriptUiOptions<TApp> & {
|
||||
type: 'overlay';
|
||||
/**
|
||||
* When using `type: "overlay"`, the mounted element is 0px by 0px in size. Alignment specifies
|
||||
* which corner is aligned with that 0x0 pixel space.
|
||||
*
|
||||
* @default "top-left"
|
||||
*/
|
||||
alignment?: ContentScriptUiOverlayAlignment;
|
||||
/**
|
||||
* The `z-index` used on the `shadowHost`. Set to a positive number to show your UI over website
|
||||
* content.
|
||||
*/
|
||||
zIndex?: number;
|
||||
};
|
||||
|
||||
export type ModalContentScriptUiOptions<TApp> =
|
||||
BaseContentScriptUiOptions<TApp> & {
|
||||
type: 'modal';
|
||||
/**
|
||||
* The `z-index` used on the `shadowHost`. Set to a positive number to show your UI over website
|
||||
* content.
|
||||
*/
|
||||
zIndex?: number;
|
||||
};
|
||||
|
||||
export type InlineContentScriptUiOptions<TApp> =
|
||||
BaseContentScriptUiOptions<TApp> & {
|
||||
type: 'inline';
|
||||
};
|
||||
|
||||
export type ContentScriptUiOverlayAlignment =
|
||||
| 'top-left'
|
||||
| 'top-right'
|
||||
| 'bottom-left'
|
||||
| 'bottom-right';
|
||||
|
||||
export type ContentScriptAppendMode =
|
||||
| 'last'
|
||||
| 'first'
|
||||
| 'replace'
|
||||
| 'before'
|
||||
| 'after';
|
||||
|
||||
export type ContentScriptUiOptions<TApp> =
|
||||
| OverlayContentScriptUiOptions<TApp>
|
||||
| ModalContentScriptUiOptions<TApp>
|
||||
| InlineContentScriptUiOptions<TApp>;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { ContentScriptDefinition } from '../core/types';
|
||||
import { ContentScriptDefinition } from '~/types';
|
||||
|
||||
export function defineContentScript(
|
||||
definition: ContentScriptDefinition,
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './content-script-context';
|
||||
export * from './content-script-ui';
|
||||
export * from './define-content-script';
|
||||
@@ -0,0 +1,12 @@
|
||||
import { BackgroundDefinition } from '~/types';
|
||||
|
||||
export function defineBackground(main: () => void): BackgroundDefinition;
|
||||
export function defineBackground(
|
||||
definition: BackgroundDefinition,
|
||||
): BackgroundDefinition;
|
||||
export function defineBackground(
|
||||
arg: (() => void) | BackgroundDefinition,
|
||||
): BackgroundDefinition {
|
||||
if (typeof arg === 'function') return { main: arg };
|
||||
return arg;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { BackgroundScriptDefintition } from '..';
|
||||
|
||||
export function defineBackground(main: () => void): BackgroundScriptDefintition;
|
||||
export function defineBackground(
|
||||
definition: BackgroundScriptDefintition,
|
||||
): BackgroundScriptDefintition;
|
||||
export function defineBackground(
|
||||
arg: (() => void) | BackgroundScriptDefintition,
|
||||
): BackgroundScriptDefintition {
|
||||
if (typeof arg === 'function') return { main: arg };
|
||||
return arg;
|
||||
}
|
||||
+5
-3
@@ -1,3 +1,5 @@
|
||||
export * from './defineContentScript';
|
||||
export * from './defineBackground';
|
||||
export * from './mountContentScriptUi';
|
||||
/**
|
||||
* @module wxt/client
|
||||
*/
|
||||
export * from './define-background';
|
||||
export * from './content-scripts';
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export function mountContentScriptUi() {
|
||||
throw Error('Not implemented: mountContentScriptUi');
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
function print(method: (...args: any[]) => void, ...args: any[]) {
|
||||
if (import.meta.env.MODE === 'production') return;
|
||||
|
||||
if (typeof args[0] === 'string') {
|
||||
const message = args.shift();
|
||||
method(`[wxt] ${message}`, ...args);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user