Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 71743b3bf3 | |||
| 8e3169eb1e | |||
| 4ec904ea3c | |||
| b284305605 | |||
| 234bd39a11 | |||
| b5acea106d | |||
| 33ed04ff6b | |||
| f51bbc0a68 | |||
| 6ee5b22e6c | |||
| b4de93d2e6 | |||
| 2570debc5c |
@@ -51,6 +51,11 @@ jobs:
|
||||
- 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 }}
|
||||
|
||||
project-templates:
|
||||
name: Project Templates
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
@@ -18,3 +18,4 @@ templates/*/pnpm-lock.yaml
|
||||
templates/*/yarn.lock
|
||||
templates/*/package-lock.json
|
||||
docs/config.md
|
||||
stats.html
|
||||
|
||||
@@ -10,3 +10,5 @@ CHANGELOG.md
|
||||
.prettierignore
|
||||
*.png
|
||||
*.svg
|
||||
*.txt
|
||||
_gitignore
|
||||
|
||||
+149
-83
@@ -1,5 +1,153 @@
|
||||
# Changelog
|
||||
|
||||
## 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)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Allow function for vite config ([4ec904e](https://github.com/wxt-dev/wxt/commit/4ec904e))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Refactor how config is resolved ([#118](https://github.com/wxt-dev/wxt/pull/118))
|
||||
|
||||
## v0.5.2
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.1...v0.5.2)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Import client utils when getting entrypoint config ([#117](https://github.com/wxt-dev/wxt/pull/117))
|
||||
|
||||
## v0.5.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.0...v0.5.1)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Allow disabling auto-imports ([#114](https://github.com/wxt-dev/wxt/pull/114))
|
||||
- Include/exclude entrypoints based on target browser ([#115](https://github.com/wxt-dev/wxt/pull/115))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Allow any string for target browser ([b4de93d](https://github.com/wxt-dev/wxt/commit/b4de93d))
|
||||
|
||||
## v0.5.0
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.4.1...v0.5.0)
|
||||
@@ -29,11 +177,6 @@
|
||||
|
||||
- ⚠️ Change default `publicDir` to `<srcDir>/public` ([5f15f9c](https://github.com/wxt-dev/wxt/commit/5f15f9c))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker <aaronklinker1@gmail.com>
|
||||
- Aaron <aaronklinker1@gmail.com>
|
||||
|
||||
## v0.4.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.4.0...v0.4.1)
|
||||
@@ -46,10 +189,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)
|
||||
@@ -99,10 +238,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)
|
||||
@@ -132,10 +267,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)
|
||||
@@ -167,9 +298,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
|
||||
@@ -203,10 +333,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)
|
||||
@@ -229,10 +355,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)
|
||||
@@ -241,10 +363,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)
|
||||
@@ -255,10 +373,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)
|
||||
@@ -267,10 +381,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)
|
||||
@@ -283,10 +393,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)
|
||||
@@ -311,10 +417,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)
|
||||
@@ -329,10 +431,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)
|
||||
@@ -341,10 +439,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)
|
||||
@@ -362,10 +456,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)
|
||||
@@ -387,10 +477,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)
|
||||
@@ -409,10 +495,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)
|
||||
@@ -422,10 +504,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:
|
||||
@@ -449,10 +527,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)
|
||||
@@ -483,10 +557,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)
|
||||
@@ -556,7 +626,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))
|
||||
|
||||
@@ -3,6 +3,20 @@
|
||||
<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=%2353bc4a">
|
||||
</a>
|
||||
<span> </span>
|
||||
<img alt="npm" src="https://img.shields.io/npm/dm/wxt?labelColor=black&color=%2353bc4a">
|
||||
<span> </span>
|
||||
<img alt="NPM" src="https://img.shields.io/npm/l/wxt?labelColor=black&color=%2353bc4a">
|
||||
<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=%2353bc4a">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<span>Next-gen framework for developing web extensions.</span>
|
||||
<br/>
|
||||
@@ -44,10 +58,10 @@ Or see the [installation guide](https://wxt.dev/get-started/installation.html) t
|
||||
- ⬇️ Download and bundle remote URL imports
|
||||
- 🎨 Frontend framework agnostic: works with Vue, React, Svelte, etc
|
||||
- 🖍️ Quickly bootstrap a new project
|
||||
- 📏 Bundle analysis
|
||||
|
||||
### Coming Soon
|
||||
|
||||
- 📏 Bundle analysis
|
||||
- 🤖 Automated publishing
|
||||
|
||||
## Contributors
|
||||
|
||||
+5
-1
@@ -16,9 +16,13 @@
|
||||
"postinstall": "pnpm -w build && wxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"webextension-polyfill": "^0.10.0"
|
||||
"webextension-polyfill": "^0.10.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.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:*"
|
||||
|
||||
@@ -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,15 @@
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['<all_urls>'],
|
||||
async main() {
|
||||
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 +1,18 @@
|
||||
import '../../common/style.css';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['*://*/*'],
|
||||
async main() {
|
||||
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);
|
||||
|
||||
mountContentScriptUi();
|
||||
},
|
||||
});
|
||||
|
||||
+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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,6 @@ import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
srcDir: 'src',
|
||||
storeIds: {
|
||||
chrome: '123',
|
||||
},
|
||||
manifest: {
|
||||
default_locale: 'en',
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -1,19 +1,52 @@
|
||||
import { defineConfig } from 'vitepress';
|
||||
import { generateConfigDocs } from './plugins/generate-config-docs';
|
||||
|
||||
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()],
|
||||
},
|
||||
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',
|
||||
},
|
||||
|
||||
@@ -23,11 +23,16 @@ const LEAF_PATHS = ['imports', 'vite', 'server'];
|
||||
|
||||
/**
|
||||
* Override any types that resolve to `import(...)` instead of their type names when calling
|
||||
* `type.getText()`
|
||||
* `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() {
|
||||
@@ -61,18 +66,12 @@ export function generateConfigDocs() {
|
||||
throw Error('Unsupported type node: ' + node.getKindName());
|
||||
}
|
||||
|
||||
if (type.isObject() && !type.isArray()) {
|
||||
return (
|
||||
type
|
||||
.getProperties()
|
||||
// .sort((l, r) => l.getName().localeCompare(r.getName()))
|
||||
.flatMap((property) => {
|
||||
const childPath = [...path, property.getName()];
|
||||
if (LEAF_PATHS.includes(childPath.join('.'))) return [];
|
||||
if (type.isObject() && !type.isArray() && !CUSTOM_TYPES[pathStr]) {
|
||||
return type.getProperties().flatMap((property) => {
|
||||
const childPath = [...path, property.getName()];
|
||||
|
||||
return getDocsFor(childPath, property.getDeclarations()[0]);
|
||||
})
|
||||
);
|
||||
return getDocsFor(childPath, property.getDeclarations()[0]);
|
||||
});
|
||||
}
|
||||
|
||||
if ('getJsDocs' in node) {
|
||||
|
||||
@@ -2,12 +2,6 @@
|
||||
|
||||
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 Project
|
||||
|
||||
:::code-group
|
||||
|
||||
@@ -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_**.
|
||||
|
||||
@@ -9,6 +9,7 @@ For MV2, the background is added as a script to the background page. For MV3, th
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['background.ts', 'background.js'],
|
||||
['background/index.ts', 'background.js'],
|
||||
]"
|
||||
/>
|
||||
|
||||
@@ -32,6 +33,10 @@ export default defineBackground({
|
||||
persistent: undefined | true | false,
|
||||
type: undefined | 'module',
|
||||
|
||||
// Set include/exclude if the background should be removed from some builds
|
||||
include: undefined | string[],
|
||||
exclude: undefined | string[],
|
||||
|
||||
// Executed when background is loaded
|
||||
main() {
|
||||
// ...
|
||||
|
||||
@@ -13,8 +13,6 @@
|
||||
|
||||
## Definition
|
||||
|
||||
Plain old HTML file.
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
@@ -22,6 +20,9 @@ Plain old HTML file.
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Title</title>
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
|
||||
@@ -30,7 +30,11 @@ export default defineContentScript({
|
||||
matchOriginAsFallback: undefined | true | false,
|
||||
world: undefined | 'ISOLATED' | 'MAIN',
|
||||
|
||||
main() {
|
||||
// Set include/exclude if the background should be removed from some builds
|
||||
include: undefined | string[],
|
||||
exclude: undefined | string[],
|
||||
|
||||
main(ctx) {
|
||||
// Executed when content script is loaded
|
||||
},
|
||||
});
|
||||
@@ -59,7 +63,7 @@ import './style.css';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['*://google.com/*', '*://duckduckgo.com/*'],
|
||||
main() {
|
||||
main(ctx) {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
@@ -79,3 +83,40 @@ Any styles imported in your content script will be added to that content script'
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Context
|
||||
|
||||
Old content scripts are not automatically stopped when an extension updates and restarts. Often, this leads to "Invalidated context" errors in production when a content script from an old version of your extension tries to use a web extension API. Since it's not connected to the latest version of your extension, the browser decides to throw an error.
|
||||
|
||||
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.
|
||||
:::
|
||||
|
||||
@@ -22,6 +22,9 @@ Plain old HTML file.
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Title</title>
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
|
||||
@@ -14,7 +14,7 @@ And that's it! Your extension now supports Chrome, Firefox, Safari, Edge, and ot
|
||||
The `browser` variable is available globally via [auto-imports](/guide/auto-imports.md), or it can be imported manually.
|
||||
|
||||
```ts
|
||||
import browser from 'wxt/browser';
|
||||
import { browser } from 'wxt/browser';
|
||||
```
|
||||
|
||||
The `wxt/browser` module exports a customized version of `webextension-polyfill`'s browser with improved typing.
|
||||
|
||||
@@ -13,8 +13,6 @@
|
||||
|
||||
## Definition
|
||||
|
||||
Plain old HTML file.
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
@@ -22,6 +20,9 @@ Plain old HTML file.
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Title</title>
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
|
||||
@@ -13,8 +13,6 @@
|
||||
|
||||
## Definition
|
||||
|
||||
Plain old HTML file.
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
@@ -22,6 +20,9 @@ Plain old HTML file.
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Title</title>
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
|
||||
@@ -23,6 +23,9 @@
|
||||
<meta name="manifest.open_in_tab" content="true|false" />
|
||||
<meta name="manifest.chrome_style" content="true|false" />
|
||||
<meta name="manifest.browser_style" content="true|false" />
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
}"
|
||||
/>
|
||||
<meta name="manifest.type" content="page_action|browser_action" />
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
|
||||
@@ -19,8 +19,6 @@ Firefox does not support sandboxed pages.
|
||||
|
||||
## Definition
|
||||
|
||||
Plain old HTML file.
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
@@ -28,6 +26,9 @@ Plain old HTML file.
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Title</title>
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
|
||||
@@ -19,8 +19,6 @@ Firefox does not support sidepanel pages.
|
||||
|
||||
## Definition
|
||||
|
||||
Plain old HTML file.
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
@@ -28,6 +26,9 @@ Plain old HTML file.
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Title</title>
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
|
||||
@@ -29,8 +29,6 @@ console.log(url); // "chrome-extension://<id>/<name>.html"
|
||||
|
||||
## Definition
|
||||
|
||||
Plain old HTML file.
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
@@ -38,6 +36,9 @@ Plain old HTML file.
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Title</title>
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
|
||||
+4
-4
@@ -10,9 +10,9 @@ All of Vite's config can be customized by setting the `vite` configuration in yo
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
vite: {
|
||||
vite: () => ({
|
||||
// Same as `defineConfig({ ... })` inside vite.config.ts
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
@@ -26,10 +26,10 @@ All plugins should work in WXT, but it is worth pointing out that since WXT orch
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
vite: {
|
||||
vite: () => ({
|
||||
plugins: [
|
||||
// ...
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
+53
-13
@@ -1,12 +1,12 @@
|
||||
---
|
||||
# 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
|
||||
@@ -21,37 +21,77 @@ hero:
|
||||
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: /get-started/build-targets
|
||||
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: /get-started/build-targets#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: /get-started/introduction.html#development
|
||||
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: /get-started/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: /get-started/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: /get-started/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,5 @@
|
||||
User-agent: *
|
||||
Disallow: /api.html
|
||||
Disallow: /config.html
|
||||
|
||||
Sitemap: https://wxt.dev/sitemap.xml
|
||||
@@ -2,60 +2,88 @@ import { describe, it, expect } from 'vitest';
|
||||
import { TestProject } from '../utils';
|
||||
|
||||
describe('Auto Imports', () => {
|
||||
it('should output types for entrypoint paths', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'entrypoints/background.ts',
|
||||
'export default defineBackground(() => {})',
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/overlay.content.ts',
|
||||
'export default defineContentScript(() => {})',
|
||||
);
|
||||
project.addFile('entrypoints/popup.html', '<html></html>');
|
||||
describe('imports: { ... }', () => {
|
||||
it('should generate a declaration file, imports.d.ts, for auto-imports', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html', `<html></html>`);
|
||||
|
||||
await project.build();
|
||||
await project.build();
|
||||
|
||||
expect(await project.serializeFile('.wxt/types/paths.d.ts'))
|
||||
.toMatchInlineSnapshot(`
|
||||
".wxt/types/paths.d.ts
|
||||
expect(await project.serializeFile('.wxt/types/imports.d.ts'))
|
||||
.toMatchInlineSnapshot(`
|
||||
".wxt/types/imports.d.ts
|
||||
----------------------------------------
|
||||
// Generated by wxt
|
||||
export {}
|
||||
declare global {
|
||||
const ContentScriptContext: typeof import('wxt/client')['ContentScriptContext']
|
||||
const browser: typeof import('wxt/browser')['browser']
|
||||
const defineBackground: typeof import('wxt/client')['defineBackground']
|
||||
const defineConfig: typeof import('wxt')['defineConfig']
|
||||
const defineContentScript: typeof import('wxt/client')['defineContentScript']
|
||||
const mountContentScriptUi: typeof import('wxt/client')['mountContentScriptUi']
|
||||
}
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should include auto-imports in the project', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html', `<html></html>`);
|
||||
|
||||
await project.build();
|
||||
|
||||
expect(await project.serializeFile('.wxt/wxt.d.ts'))
|
||||
.toMatchInlineSnapshot(`
|
||||
".wxt/wxt.d.ts
|
||||
----------------------------------------
|
||||
// Generated by wxt
|
||||
import \\"wxt/browser\\";
|
||||
|
||||
declare module \\"wxt/browser\\" {
|
||||
type PublicPath =
|
||||
| \\"/background.js\\"
|
||||
| \\"/content-scripts/overlay.js\\"
|
||||
| \\"/popup.html\\"
|
||||
export interface WxtRuntime extends Runtime.Static {
|
||||
getURL(path: PublicPath): string;
|
||||
}
|
||||
}
|
||||
/// <reference types=\\"vite/client\\" />
|
||||
/// <reference types=\\"./types/imports.d.ts\\" />
|
||||
/// <reference types=\\"./types/paths.d.ts\\" />
|
||||
/// <reference types=\\"./types/i18n.d.ts\\" />
|
||||
/// <reference types=\\"./types/globals.d.ts\\" />
|
||||
"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
it('should make some client utils auto-importable', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html', `<html></html>`);
|
||||
describe('imports: false', () => {
|
||||
it('should not generate a imports.d.ts file', async () => {
|
||||
const project = new TestProject();
|
||||
project.setConfigFileConfig({
|
||||
imports: false,
|
||||
});
|
||||
project.addFile('entrypoints/popup.html', `<html></html>`);
|
||||
|
||||
await project.build();
|
||||
await project.build();
|
||||
|
||||
expect(await project.serializeFile('.wxt/types/imports.d.ts'))
|
||||
.toMatchInlineSnapshot(`
|
||||
".wxt/types/imports.d.ts
|
||||
expect(await project.fileExists('.wxt/types/imports.d.ts')).toBe(false);
|
||||
});
|
||||
|
||||
it('should not include imports.d.ts in the type references', async () => {
|
||||
const project = new TestProject();
|
||||
project.setConfigFileConfig({
|
||||
imports: false,
|
||||
});
|
||||
project.addFile('entrypoints/popup.html', `<html></html>`);
|
||||
|
||||
await project.build();
|
||||
|
||||
expect(
|
||||
await project.serializeFile('.wxt/wxt.d.ts'),
|
||||
).toMatchInlineSnapshot(
|
||||
`
|
||||
".wxt/wxt.d.ts
|
||||
----------------------------------------
|
||||
// Generated by wxt
|
||||
export {}
|
||||
declare global {
|
||||
const browser: typeof import('wxt/browser')['browser']
|
||||
const defineBackground: typeof import('wxt/client')['defineBackground']
|
||||
const defineConfig: typeof import('wxt')['defineConfig']
|
||||
const defineContentScript: typeof import('wxt/client')['defineContentScript']
|
||||
const mountContentScriptUi: typeof import('wxt/client')['mountContentScriptUi']
|
||||
}
|
||||
/// <reference types=\\"vite/client\\" />
|
||||
/// <reference types=\\"./types/paths.d.ts\\" />
|
||||
/// <reference types=\\"./types/i18n.d.ts\\" />
|
||||
/// <reference types=\\"./types/globals.d.ts\\" />
|
||||
"
|
||||
`);
|
||||
`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,5 +40,5 @@ describe('Init command', () => {
|
||||
"wxt.config.ts",
|
||||
]
|
||||
`);
|
||||
}, 30e3);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -306,4 +306,29 @@ describe('Manifest Content', () => {
|
||||
js: ['content-scripts/one.js'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect the transformManifest option', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'wxt.config.ts',
|
||||
`import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
transformManifest(manifest) {
|
||||
manifest.author = "Custom Author"
|
||||
}
|
||||
})`,
|
||||
);
|
||||
|
||||
await project.build();
|
||||
|
||||
const output = await project.serializeFile(
|
||||
'.output/chrome-mv3/manifest.json',
|
||||
);
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"author\\":\\"Custom Author\\"}"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -43,7 +43,12 @@ describe('Output Directory Structure', () => {
|
||||
|
||||
await project.build();
|
||||
|
||||
expect(await project.serializeOutput()).toMatchInlineSnapshot(`
|
||||
expect(
|
||||
await project.serializeOutput([
|
||||
'.output/chrome-mv3/content-scripts/one.js',
|
||||
'.output/chrome-mv3/content-scripts/two.js',
|
||||
]),
|
||||
).toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/assets/one.css
|
||||
----------------------------------------
|
||||
body{color:#00f}
|
||||
@@ -56,13 +61,11 @@ describe('Output Directory Structure', () => {
|
||||
================================================================================
|
||||
.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)}})()})();
|
||||
|
||||
<contents-ignored>
|
||||
================================================================================
|
||||
.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
|
||||
----------------------------------------
|
||||
@@ -82,15 +85,84 @@ 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\\"]}]}"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not include an entrypoint if the target browser is not in the list of included targets', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/options.html', '<html></html>');
|
||||
project.addFile(
|
||||
'entrypoints/background.ts',
|
||||
`
|
||||
export default defineBackground({
|
||||
include: ["chrome"],
|
||||
main() {},
|
||||
})
|
||||
`,
|
||||
);
|
||||
|
||||
await project.build({ browser: 'firefox' });
|
||||
|
||||
expect(await project.fileExists('.output/firefox-mv2/background.js')).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should not include an entrypoint if the target browser is in the list of excluded targets', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/options.html', '<html></html>');
|
||||
project.addFile(
|
||||
'entrypoints/background.ts',
|
||||
`
|
||||
export default defineBackground({
|
||||
exclude: ["chrome"],
|
||||
main() {},
|
||||
})
|
||||
`,
|
||||
);
|
||||
|
||||
await project.build({ browser: 'chrome' });
|
||||
|
||||
expect(await project.fileExists('.output/firefox-mv2/background.js')).toBe(
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":\\"<all_urls>\\",\\"js\\":[\\"content-scripts/demo.js\\"]}]}"
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -22,28 +22,7 @@ describe('TypeScript Project', () => {
|
||||
const __IS_EDGE__: boolean;
|
||||
const __IS_OPERA__: boolean;
|
||||
const __COMMAND__: \\"build\\" | \\"serve\\";
|
||||
}
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should define auto-import globals', async () => {
|
||||
const project = new TestProject();
|
||||
|
||||
await project.build();
|
||||
|
||||
const output = await project.serializeFile('.wxt/types/imports.d.ts');
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
".wxt/types/imports.d.ts
|
||||
----------------------------------------
|
||||
// Generated by wxt
|
||||
export {}
|
||||
declare global {
|
||||
const browser: typeof import('wxt/browser')['browser']
|
||||
const defineBackground: typeof import('wxt/client')['defineBackground']
|
||||
const defineConfig: typeof import('wxt')['defineConfig']
|
||||
const defineContentScript: typeof import('wxt/client')['defineContentScript']
|
||||
const mountContentScriptUi: typeof import('wxt/client')['mountContentScriptUi']
|
||||
const __ENTRYPOINT__: string;
|
||||
}
|
||||
"
|
||||
`);
|
||||
@@ -295,7 +274,7 @@ describe('TypeScript Project', () => {
|
||||
`);
|
||||
});
|
||||
|
||||
it('should correct path aliases for a custom srcDir', async () => {
|
||||
it('should generate correct path aliases for a custom srcDir', async () => {
|
||||
const project = new TestProject();
|
||||
project.setConfigFileConfig({
|
||||
srcDir: 'src',
|
||||
|
||||
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-test',
|
||||
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`);
|
||||
}
|
||||
|
||||
|
||||
+14
-5
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wxt",
|
||||
"type": "module",
|
||||
"version": "0.5.0",
|
||||
"version": "0.6.5",
|
||||
"description": "Next gen framework for developing web extensions",
|
||||
"engines": {
|
||||
"node": ">=18.16.0",
|
||||
@@ -56,7 +56,7 @@
|
||||
"format:check": "prettier --write .",
|
||||
"compile": "tsc --noEmit",
|
||||
"test": "vitest",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:coverage": "vitest run --coverage.enabled --coverage.exclude=e2e --coverage.exclude=src/testing",
|
||||
"prepare": "simple-git-hooks",
|
||||
"prepublish": "pnpm -s build",
|
||||
"docs:dev": "vitepress dev docs",
|
||||
@@ -67,22 +67,27 @@
|
||||
"dependencies": {
|
||||
"@types/webextension-polyfill": "^0.10.1",
|
||||
"@webext-core/fake-browser": "^1.2.2",
|
||||
"@webext-core/match-patterns": "^1.0.1",
|
||||
"@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",
|
||||
"web-ext-run": "^0.1.0",
|
||||
@@ -94,14 +99,12 @@
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/lodash.merge": "^4.6.7",
|
||||
"@types/node": "^20.4.5",
|
||||
"@types/prettier": "^3.0.0",
|
||||
"@types/prompts": "^2.4.4",
|
||||
"@vitest/coverage-v8": "^0.34.1",
|
||||
"execa": "^7.2.0",
|
||||
"lint-staged": "^14.0.0",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"ora": "^7.0.1",
|
||||
"prettier": "^3.0.1",
|
||||
"simple-git-hooks": "^2.9.0",
|
||||
"ts-morph": "^19.0.0",
|
||||
@@ -111,6 +114,7 @@
|
||||
"vitepress": "1.0.0-rc.10",
|
||||
"vitest": "^0.34.1",
|
||||
"vitest-mock-extended": "^1.1.4",
|
||||
"vue": "^3.3.4",
|
||||
"webextension-polyfill": "^0.10.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -123,6 +127,11 @@
|
||||
"lint-staged": {
|
||||
"*": "prettier --write"
|
||||
},
|
||||
"changelog": {
|
||||
"excludeAuthors": [
|
||||
"aaronklinker1@gmail.com"
|
||||
]
|
||||
},
|
||||
"pnpm": {
|
||||
"peerDependencyRules": {
|
||||
"ignoreMissing": [
|
||||
|
||||
Generated
+360
-166
File diff suppressed because it is too large
Load Diff
@@ -11,17 +11,20 @@ export const build = defineCommand<
|
||||
mv3?: boolean;
|
||||
mv2?: boolean;
|
||||
debug?: boolean;
|
||||
analyze?: boolean;
|
||||
},
|
||||
]
|
||||
>(async (root, flags) => {
|
||||
const mode = flags.mode ?? 'production';
|
||||
const cliConfig: wxt.InlineConfig = {
|
||||
root,
|
||||
mode,
|
||||
mode: flags.mode,
|
||||
browser: flags.browser,
|
||||
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
|
||||
configFile: flags.config,
|
||||
debug: flags.debug,
|
||||
analysis: {
|
||||
enabled: flags.analyze,
|
||||
},
|
||||
};
|
||||
|
||||
await wxt.build(cliConfig);
|
||||
|
||||
@@ -14,10 +14,9 @@ export const dev = defineCommand<
|
||||
},
|
||||
]
|
||||
>(async (root, flags) => {
|
||||
const mode = flags.mode ?? 'development';
|
||||
const cliConfig: wxt.InlineConfig = {
|
||||
root,
|
||||
mode,
|
||||
mode: flags.mode,
|
||||
browser: flags.browser,
|
||||
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
|
||||
configFile: flags.config,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { defineCommand } from '../utils/defineCommand';
|
||||
import prompts from 'prompts';
|
||||
import ora from 'ora';
|
||||
import { consola } from 'consola';
|
||||
import { downloadTemplate } from 'giget';
|
||||
import fs from 'fs-extra';
|
||||
@@ -133,6 +132,7 @@ async function cloneProject({
|
||||
template: Template;
|
||||
packageManager: string;
|
||||
}) {
|
||||
const { default: ora } = await import('ora');
|
||||
const spinner = ora('Downloading template').start();
|
||||
try {
|
||||
// 1. Clone repo
|
||||
|
||||
@@ -17,10 +17,9 @@ export const zip = defineCommand<
|
||||
},
|
||||
]
|
||||
>(async (root, flags) => {
|
||||
const mode = flags.mode ?? 'production';
|
||||
const cliConfig: wxt.InlineConfig = {
|
||||
root,
|
||||
mode,
|
||||
mode: flags.mode,
|
||||
browser: flags.browser,
|
||||
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
|
||||
configFile: flags.config,
|
||||
|
||||
@@ -26,6 +26,7 @@ cli
|
||||
.option('-b, --browser <browser>', 'specify a browser')
|
||||
.option('--mv3', 'target manifest v3')
|
||||
.option('--mv2', 'target manifest v2')
|
||||
.option('--analyze', 'visualize extension bundle')
|
||||
.action(commands.build);
|
||||
|
||||
// ZIP
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './defineContentScript';
|
||||
export * from './defineBackground';
|
||||
export * from './mountContentScriptUi';
|
||||
export * from './utils/ContentScriptContext';
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
import { browser } from '../browser';
|
||||
import { logger } from './logger';
|
||||
|
||||
/**
|
||||
* Extends [`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 extends AbortController {
|
||||
static SCRIPT_STARTED_MESSAGE_TYPE = 'wxt:content-script-started';
|
||||
|
||||
#isTopFrame = window.self === window.top;
|
||||
|
||||
constructor(private readonly contentScriptName: string) {
|
||||
super();
|
||||
|
||||
if (this.#isTopFrame) {
|
||||
this.#stopOldScripts();
|
||||
}
|
||||
this.setTimeout(() => {
|
||||
// Run on next tick so the listener it adds isn't triggered by stopOldScript
|
||||
this.#listenForNewerScripts();
|
||||
});
|
||||
}
|
||||
|
||||
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),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import definition from 'virtual:user-content-script';
|
||||
import { logger } from '../utils/logger';
|
||||
import { ContentScriptContext } from '../utils/ContentScriptContext';
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
await definition.main();
|
||||
const ctx = new ContentScriptContext(__ENTRYPOINT__);
|
||||
|
||||
await definition.main(ctx);
|
||||
} catch (err) {
|
||||
logger.error('The content script crashed on startup!', err);
|
||||
}
|
||||
|
||||
@@ -21,3 +21,4 @@ declare const __DEV_SERVER_PROTOCOL__: string;
|
||||
declare const __DEV_SERVER_HOSTNAME__: string;
|
||||
declare const __DEV_SERVER_PORT__: string;
|
||||
declare const __MANIFEST_VERSION__: 2 | 3;
|
||||
declare const __ENTRYPOINT__: string;
|
||||
|
||||
+32
-1
@@ -11,6 +11,9 @@ import fs from 'fs-extra';
|
||||
import { groupEntrypoints } from './utils/groupEntrypoints';
|
||||
import { formatDuration } from './utils/formatDuration';
|
||||
import { printBuildSummary } from './log/printBuildSummary';
|
||||
import { execaCommand } from 'execa';
|
||||
import glob from 'fast-glob';
|
||||
import { unnormalizePath } from './utils/paths';
|
||||
|
||||
/**
|
||||
* Builds the extension based on an internal config.
|
||||
@@ -50,6 +53,13 @@ export async function buildInternal(
|
||||
config,
|
||||
);
|
||||
|
||||
if (config.analysis.enabled) {
|
||||
await combineAnalysisStats(config);
|
||||
config.logger.info(
|
||||
`Analysis complete:\n ${pc.gray('└─')} ${pc.yellow('stats.html')}`,
|
||||
);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
@@ -71,6 +81,9 @@ export async function rebuild(
|
||||
publicAssets: [],
|
||||
},
|
||||
): Promise<{ output: BuildOutput; manifest: Manifest.WebExtensionManifest }> {
|
||||
const { default: ora } = await import('ora');
|
||||
const spinner = ora(`Preparing...`).start();
|
||||
|
||||
// Update types directory with new files and types
|
||||
const allEntrypoints = await findEntrypoints(config);
|
||||
await generateTypesDir(allEntrypoints, config).catch((err) => {
|
||||
@@ -80,7 +93,7 @@ export async function rebuild(
|
||||
});
|
||||
|
||||
// Build and merge the outputs
|
||||
const newOutput = await buildEntrypoints(entrypointGroups, config);
|
||||
const newOutput = await buildEntrypoints(entrypointGroups, config, spinner);
|
||||
const mergedOutput: Omit<BuildOutput, 'manifest'> = {
|
||||
steps: [...existingOutput.steps, ...newOutput.steps],
|
||||
publicAssets: [...existingOutput.publicAssets, ...newOutput.publicAssets],
|
||||
@@ -99,6 +112,9 @@ export async function rebuild(
|
||||
// Write manifest
|
||||
await writeManifest(newManifest, finalOutput, config);
|
||||
|
||||
// Stop the spinner and remove it from the CLI output
|
||||
spinner.clear().stop();
|
||||
|
||||
return {
|
||||
output: {
|
||||
manifest: newManifest,
|
||||
@@ -111,3 +127,18 @@ export async function rebuild(
|
||||
manifest: newManifest,
|
||||
};
|
||||
}
|
||||
|
||||
async function combineAnalysisStats(config: InternalConfig): Promise<void> {
|
||||
const unixFiles = await glob(`stats-*.json`, {
|
||||
cwd: config.outDir,
|
||||
absolute: true,
|
||||
});
|
||||
const absolutePaths = unixFiles.map(unnormalizePath);
|
||||
|
||||
await execaCommand(
|
||||
`rollup-plugin-visualizer ${absolutePaths.join(' ')} --template ${
|
||||
config.analysis.template
|
||||
}`,
|
||||
{ cwd: config.root, stdio: 'inherit' },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,13 +9,13 @@ import {
|
||||
import { resolve } from 'path';
|
||||
import { findEntrypoints } from '../findEntrypoints';
|
||||
import fs from 'fs-extra';
|
||||
import { importTsFile } from '../../utils/importTsFile';
|
||||
import { importEntrypointFile } from '../../utils/importEntrypointFile';
|
||||
import glob from 'fast-glob';
|
||||
import { fakeInternalConfig } from '../../../testing/fake-objects';
|
||||
import { unnormalizePath } from '../../utils/paths';
|
||||
|
||||
vi.mock('../../utils/importTsFile');
|
||||
const importTsFileMock = vi.mocked(importTsFile);
|
||||
vi.mock('../../utils/importEntrypointFile');
|
||||
const importEntrypointFileMock = vi.mocked(importEntrypointFile);
|
||||
|
||||
vi.mock('fast-glob');
|
||||
const globMock = vi.mocked(glob);
|
||||
@@ -191,13 +191,16 @@ describe('findEntrypoints', () => {
|
||||
matches: ['<all_urls>'],
|
||||
};
|
||||
globMock.mockResolvedValueOnce([path]);
|
||||
importTsFileMock.mockResolvedValue(options);
|
||||
importEntrypointFileMock.mockResolvedValue(options);
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
|
||||
expect(entrypoints).toHaveLength(1);
|
||||
expect(entrypoints[0]).toEqual({ ...expected, options });
|
||||
expect(importTsFileMock).toBeCalledWith(expected.inputPath, config);
|
||||
expect(importEntrypointFileMock).toBeCalledWith(
|
||||
expected.inputPath,
|
||||
config,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -211,6 +214,15 @@ describe('findEntrypoints', () => {
|
||||
outputDir: config.outDir,
|
||||
},
|
||||
],
|
||||
[
|
||||
'background/index.ts',
|
||||
{
|
||||
type: 'background',
|
||||
name: 'background',
|
||||
inputPath: resolve(config.entrypointsDir, 'background/index.ts'),
|
||||
outputDir: config.outDir,
|
||||
},
|
||||
],
|
||||
])(
|
||||
'should find and load background entrypoint config from %s',
|
||||
async (path, expected) => {
|
||||
@@ -218,13 +230,16 @@ describe('findEntrypoints', () => {
|
||||
type: 'module',
|
||||
};
|
||||
globMock.mockResolvedValueOnce([path]);
|
||||
importTsFileMock.mockResolvedValue(options);
|
||||
importEntrypointFileMock.mockResolvedValue(options);
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
|
||||
expect(entrypoints).toHaveLength(1);
|
||||
expect(entrypoints[0]).toEqual({ ...expected, options });
|
||||
expect(importTsFileMock).toBeCalledWith(expected.inputPath, config);
|
||||
expect(importEntrypointFileMock).toBeCalledWith(
|
||||
expected.inputPath,
|
||||
config,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -255,6 +270,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'sandbox',
|
||||
inputPath: resolve(config.entrypointsDir, 'sandbox.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -264,6 +280,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'sandbox',
|
||||
inputPath: resolve(config.entrypointsDir, 'sandbox/index.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -273,6 +290,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'named',
|
||||
inputPath: resolve(config.entrypointsDir, 'named.sandbox.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -282,6 +300,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'named',
|
||||
inputPath: resolve(config.entrypointsDir, 'named.sandbox/index.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -293,6 +312,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'bookmarks',
|
||||
inputPath: resolve(config.entrypointsDir, 'bookmarks.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -302,6 +322,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'bookmarks',
|
||||
inputPath: resolve(config.entrypointsDir, 'bookmarks/index.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -313,6 +334,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'history',
|
||||
inputPath: resolve(config.entrypointsDir, 'history.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -322,6 +344,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'history',
|
||||
inputPath: resolve(config.entrypointsDir, 'history/index.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -333,6 +356,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'newtab',
|
||||
inputPath: resolve(config.entrypointsDir, 'newtab.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -342,6 +366,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'newtab',
|
||||
inputPath: resolve(config.entrypointsDir, 'newtab/index.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -353,6 +378,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'sidepanel',
|
||||
inputPath: resolve(config.entrypointsDir, 'sidepanel.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -362,6 +388,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'sidepanel',
|
||||
inputPath: resolve(config.entrypointsDir, 'sidepanel/index.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -371,6 +398,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'named',
|
||||
inputPath: resolve(config.entrypointsDir, 'named.sidepanel.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -380,6 +408,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'named',
|
||||
inputPath: resolve(config.entrypointsDir, 'named.sidepanel/index.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -391,6 +420,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'devtools',
|
||||
inputPath: resolve(config.entrypointsDir, 'devtools.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -400,6 +430,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'devtools',
|
||||
inputPath: resolve(config.entrypointsDir, 'devtools/index.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -411,6 +442,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'onboarding',
|
||||
inputPath: resolve(config.entrypointsDir, 'onboarding.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -420,6 +452,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'onboarding',
|
||||
inputPath: resolve(config.entrypointsDir, 'onboarding/index.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -431,6 +464,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'injected',
|
||||
inputPath: resolve(config.entrypointsDir, 'injected.ts'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -440,6 +474,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'injected',
|
||||
inputPath: resolve(config.entrypointsDir, 'injected/index.ts'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -451,6 +486,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'iframe',
|
||||
inputPath: resolve(config.entrypointsDir, 'iframe.scss'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -460,6 +496,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'iframe',
|
||||
inputPath: resolve(config.entrypointsDir, 'iframe.css'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -471,6 +508,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'content',
|
||||
inputPath: resolve(config.entrypointsDir, 'content.css'),
|
||||
outputDir: resolve(config.outDir, 'content-scripts'),
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -480,6 +518,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'overlay',
|
||||
inputPath: resolve(config.entrypointsDir, 'overlay.content.css'),
|
||||
outputDir: resolve(config.outDir, 'content-scripts'),
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -489,6 +528,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'content',
|
||||
inputPath: resolve(config.entrypointsDir, 'content/index.css'),
|
||||
outputDir: resolve(config.outDir, 'content-scripts'),
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -498,6 +538,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'overlay',
|
||||
inputPath: resolve(config.entrypointsDir, 'overlay.content/index.css'),
|
||||
outputDir: resolve(config.outDir, 'content-scripts'),
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
])('should find entrypoint for %s', async (path, expected) => {
|
||||
@@ -521,4 +562,148 @@ describe('findEntrypoints', () => {
|
||||
expectedPaths.join(', '),
|
||||
);
|
||||
});
|
||||
|
||||
describe('include option', () => {
|
||||
it("should filter out the background when include doesn't contain the target browser", async () => {
|
||||
globMock.mockResolvedValueOnce(['background.ts']);
|
||||
importEntrypointFileMock.mockResolvedValue({
|
||||
include: ['not' + config.browser],
|
||||
});
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
|
||||
expect(entrypoints).toEqual([]);
|
||||
});
|
||||
|
||||
it("should filter out content scripts when include doesn't contain the target browser", async () => {
|
||||
globMock.mockResolvedValueOnce(['example.content.ts']);
|
||||
importEntrypointFileMock.mockResolvedValue({
|
||||
include: ['not' + config.browser],
|
||||
});
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
|
||||
expect(entrypoints).toEqual([]);
|
||||
});
|
||||
|
||||
it("should filter out the popup when include doesn't contain the target browser", async () => {
|
||||
globMock.mockResolvedValueOnce(['popup.html']);
|
||||
readFileMock.mockResolvedValueOnce(
|
||||
`<html>
|
||||
<head>
|
||||
<meta name="manifest.include" content="['${
|
||||
'not' + config.browser
|
||||
}']" />
|
||||
</head>
|
||||
</html>`,
|
||||
);
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
|
||||
expect(entrypoints).toEqual([]);
|
||||
});
|
||||
|
||||
it("should filter out the options page when include doesn't contain the target browser", async () => {
|
||||
globMock.mockResolvedValueOnce(['options.html']);
|
||||
readFileMock.mockResolvedValueOnce(
|
||||
`<html>
|
||||
<head>
|
||||
<meta name="manifest.include" content="['${
|
||||
'not' + config.browser
|
||||
}']" />
|
||||
</head>
|
||||
</html>`,
|
||||
);
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
|
||||
expect(entrypoints).toEqual([]);
|
||||
});
|
||||
|
||||
it("should filter out an unlisted page when include doesn't contain the target browser", async () => {
|
||||
globMock.mockResolvedValueOnce(['unlisted.html']);
|
||||
readFileMock.mockResolvedValueOnce(
|
||||
`<html>
|
||||
<head>
|
||||
<meta name="manifest.include" content="['${
|
||||
'not' + config.browser
|
||||
}']" />
|
||||
</head>
|
||||
</html>`,
|
||||
);
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
|
||||
expect(entrypoints).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('exclude option', () => {
|
||||
it('should filter out the background when exclude contains the target browser', async () => {
|
||||
globMock.mockResolvedValueOnce(['background.ts']);
|
||||
importEntrypointFileMock.mockResolvedValue({
|
||||
exclude: [config.browser],
|
||||
});
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
|
||||
expect(entrypoints).toEqual([]);
|
||||
});
|
||||
|
||||
it('should filter out content scripts when exclude contains the target browser', async () => {
|
||||
globMock.mockResolvedValueOnce(['example.content.ts']);
|
||||
importEntrypointFileMock.mockResolvedValue({
|
||||
exclude: [config.browser],
|
||||
});
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
|
||||
expect(entrypoints).toEqual([]);
|
||||
});
|
||||
|
||||
it('should filter out the popup when exclude contains the target browser', async () => {
|
||||
globMock.mockResolvedValueOnce(['popup.html']);
|
||||
readFileMock.mockResolvedValueOnce(
|
||||
`<html>
|
||||
<head>
|
||||
<meta name="manifest.exclude" content="['${config.browser}']" />
|
||||
</head>
|
||||
</html>`,
|
||||
);
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
|
||||
expect(entrypoints).toEqual([]);
|
||||
});
|
||||
|
||||
it('should filter out the options page when exclude contains the target browser', async () => {
|
||||
globMock.mockResolvedValueOnce(['options.html']);
|
||||
readFileMock.mockResolvedValueOnce(
|
||||
`<html>
|
||||
<head>
|
||||
<meta name="manifest.exclude" content="['${config.browser}']" />
|
||||
</head>
|
||||
</html>`,
|
||||
);
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
|
||||
expect(entrypoints).toEqual([]);
|
||||
});
|
||||
|
||||
it('should filter out an unlisted page when exclude contains the target browser', async () => {
|
||||
globMock.mockResolvedValueOnce(['unlisted.html']);
|
||||
readFileMock.mockResolvedValueOnce(
|
||||
`<html>
|
||||
<head>
|
||||
<meta name="manifest.exclude" content="['${config.browser}']" />
|
||||
</head>
|
||||
</html>`,
|
||||
);
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
|
||||
expect(entrypoints).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,13 +12,26 @@ import { getEntrypointBundlePath } from '../utils/entrypoints';
|
||||
import fs from 'fs-extra';
|
||||
import { dirname, resolve } from 'path';
|
||||
import { getPublicFiles } from '../utils/public';
|
||||
import { getEntrypointGlobals } from '../utils/globals';
|
||||
import type { Ora } from 'ora';
|
||||
import pc from 'picocolors';
|
||||
|
||||
export async function buildEntrypoints(
|
||||
groups: EntrypointGroup[],
|
||||
config: InternalConfig,
|
||||
spinner: Ora,
|
||||
): Promise<Omit<BuildOutput, 'manifest'>> {
|
||||
const steps: BuildStepOutput[] = [];
|
||||
for (const group of groups) {
|
||||
for (let i = 0; i < groups.length; i++) {
|
||||
const group = groups[i];
|
||||
|
||||
spinner.text =
|
||||
pc.dim(`[${i + 1}/${groups.length}]`) +
|
||||
` ${[group]
|
||||
.flat()
|
||||
.map((e) => e.name)
|
||||
.join(pc.dim(', '))}`;
|
||||
|
||||
const step = Array.isArray(group)
|
||||
? await buildMultipleEntrypoints(group, config)
|
||||
: await buildSingleEntrypoint(group, config);
|
||||
@@ -78,10 +91,17 @@ async function buildSingleEntrypoint(
|
||||
},
|
||||
},
|
||||
},
|
||||
define: {
|
||||
// See https://github.com/aklinker1/vite-plugin-web-extension/issues/96
|
||||
'process.env.NODE_ENV': JSON.stringify(config.mode),
|
||||
},
|
||||
};
|
||||
for (const global of getEntrypointGlobals(config, entrypoint.name)) {
|
||||
libMode.define![global.name] = JSON.stringify(global.value);
|
||||
}
|
||||
const entryConfig = vite.mergeConfig(
|
||||
libMode,
|
||||
config.vite,
|
||||
await config.vite(config.env),
|
||||
) as vite.InlineConfig;
|
||||
|
||||
const result = await vite.build(entryConfig);
|
||||
@@ -116,11 +136,15 @@ async function buildMultipleEntrypoints(
|
||||
},
|
||||
},
|
||||
},
|
||||
define: {},
|
||||
};
|
||||
for (const global of getEntrypointGlobals(config, 'html')) {
|
||||
multiPage.define![global.name] = JSON.stringify(global.value);
|
||||
}
|
||||
|
||||
const entryConfig = vite.mergeConfig(
|
||||
multiPage,
|
||||
config.vite,
|
||||
await config.vite(config.env),
|
||||
) as vite.UserConfig;
|
||||
|
||||
const result = await vite.build(entryConfig);
|
||||
|
||||
@@ -2,9 +2,11 @@ import { relative, resolve } from 'path';
|
||||
import {
|
||||
BackgroundEntrypoint,
|
||||
BackgroundScriptDefintition,
|
||||
BaseEntrypointOptions,
|
||||
ContentScriptDefinition,
|
||||
ContentScriptEntrypoint,
|
||||
Entrypoint,
|
||||
GenericEntrypoint,
|
||||
InternalConfig,
|
||||
OptionsEntrypoint,
|
||||
PopupEntrypoint,
|
||||
@@ -13,7 +15,7 @@ import fs from 'fs-extra';
|
||||
import { minimatch } from 'minimatch';
|
||||
import { parseHTML } from 'linkedom';
|
||||
import JSON5 from 'json5';
|
||||
import { importTsFile } from '../utils/importTsFile';
|
||||
import { importEntrypointFile } from '../utils/importEntrypointFile';
|
||||
import glob from 'fast-glob';
|
||||
import { getEntrypointName } from '../utils/entrypoints';
|
||||
import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '../vite-plugins/noopBackground';
|
||||
@@ -75,12 +77,19 @@ export async function findEntrypoints(
|
||||
path,
|
||||
);
|
||||
break;
|
||||
case 'unlisted-page':
|
||||
entrypoint = await getUnlistedPageEntrypoint(config, path);
|
||||
break;
|
||||
case 'content-script-style':
|
||||
entrypoint = {
|
||||
type,
|
||||
name: getEntrypointName(config.entrypointsDir, path),
|
||||
inputPath: path,
|
||||
outputDir: resolve(config.outDir, CONTENT_SCRIPT_OUT_DIR),
|
||||
options: {
|
||||
include: undefined,
|
||||
exclude: undefined,
|
||||
},
|
||||
};
|
||||
break;
|
||||
default:
|
||||
@@ -89,6 +98,10 @@ export async function findEntrypoints(
|
||||
name: getEntrypointName(config.entrypointsDir, path),
|
||||
inputPath: path,
|
||||
outputDir: config.outDir,
|
||||
options: {
|
||||
include: undefined,
|
||||
exclude: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -112,7 +125,47 @@ export async function findEntrypoints(
|
||||
await getBackgroundEntrypoint(config, VIRTUAL_NOOP_BACKGROUND_MODULE_ID),
|
||||
);
|
||||
}
|
||||
return entrypoints;
|
||||
|
||||
config.logger.debug('All entrypoints:', entrypoints);
|
||||
const targetEntrypoints = entrypoints.filter((entry) => {
|
||||
const { include, exclude } = entry.options;
|
||||
if (include?.length && exclude?.length) {
|
||||
config.logger.warn(
|
||||
`The ${entry.name} entrypoint lists both include and exclude, but only one can be used per entrypoint. Entrypoint ignored.`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (exclude?.length && !include?.length) {
|
||||
return !exclude.includes(config.browser);
|
||||
}
|
||||
if (include?.length && !exclude?.length) {
|
||||
return include.includes(config.browser);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
config.logger.debug(`${config.browser} entrypoints:`, targetEntrypoints);
|
||||
return targetEntrypoints;
|
||||
}
|
||||
|
||||
function getHtmlBaseOptions(document: Document): BaseEntrypointOptions {
|
||||
const options: BaseEntrypointOptions = {};
|
||||
|
||||
const includeContent = document
|
||||
.querySelector("meta[name='manifest.include']")
|
||||
?.getAttribute('content');
|
||||
if (includeContent) {
|
||||
options.include = JSON5.parse(includeContent);
|
||||
}
|
||||
|
||||
const excludeContent = document
|
||||
.querySelector("meta[name='manifest.exclude']")
|
||||
?.getAttribute('content');
|
||||
if (excludeContent) {
|
||||
options.exclude = JSON5.parse(excludeContent);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,11 +176,11 @@ async function getPopupEntrypoint(
|
||||
config: InternalConfig,
|
||||
path: string,
|
||||
): Promise<PopupEntrypoint> {
|
||||
const options: PopupEntrypoint['options'] = {};
|
||||
|
||||
const content = await fs.readFile(path, 'utf-8');
|
||||
const { document } = parseHTML(content);
|
||||
|
||||
const options: PopupEntrypoint['options'] = getHtmlBaseOptions(document);
|
||||
|
||||
const title = document.querySelector('title');
|
||||
if (title != null) options.defaultTitle = title.textContent ?? undefined;
|
||||
|
||||
@@ -170,11 +223,11 @@ async function getOptionsEntrypoint(
|
||||
config: InternalConfig,
|
||||
path: string,
|
||||
): Promise<OptionsEntrypoint> {
|
||||
const options: OptionsEntrypoint['options'] = {};
|
||||
|
||||
const content = await fs.readFile(path, 'utf-8');
|
||||
const { document } = parseHTML(content);
|
||||
|
||||
const options: OptionsEntrypoint['options'] = getHtmlBaseOptions(document);
|
||||
|
||||
const openInTabContent = document
|
||||
.querySelector("meta[name='manifest.open_in_tab']")
|
||||
?.getAttribute('content');
|
||||
@@ -205,6 +258,26 @@ async function getOptionsEntrypoint(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path Absolute path to the HTML file.
|
||||
* @param content String contents of the file at the path.
|
||||
*/
|
||||
async function getUnlistedPageEntrypoint(
|
||||
config: InternalConfig,
|
||||
path: string,
|
||||
): Promise<GenericEntrypoint> {
|
||||
const content = await fs.readFile(path, 'utf-8');
|
||||
const { document } = parseHTML(content);
|
||||
|
||||
return {
|
||||
type: 'unlisted-page',
|
||||
name: getEntrypointName(config.entrypointsDir, path),
|
||||
inputPath: path,
|
||||
outputDir: config.outDir,
|
||||
options: getHtmlBaseOptions(document),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path Absolute path to the background's TS file.
|
||||
*/
|
||||
@@ -214,10 +287,8 @@ async function getBackgroundEntrypoint(
|
||||
): Promise<BackgroundEntrypoint> {
|
||||
let options: Omit<BackgroundScriptDefintition, 'main'> = {};
|
||||
if (path !== VIRTUAL_NOOP_BACKGROUND_MODULE_ID) {
|
||||
const defaultExport = await importTsFile<BackgroundScriptDefintition>(
|
||||
path,
|
||||
config,
|
||||
);
|
||||
const defaultExport =
|
||||
await importEntrypointFile<BackgroundScriptDefintition>(path, config);
|
||||
if (defaultExport == null) {
|
||||
throw Error('Background script does not have a default export');
|
||||
}
|
||||
@@ -241,10 +312,8 @@ async function getContentScriptEntrypoint(
|
||||
name: string,
|
||||
path: string,
|
||||
): Promise<ContentScriptEntrypoint> {
|
||||
const { main: _, ...options } = await importTsFile<ContentScriptDefinition>(
|
||||
path,
|
||||
config,
|
||||
);
|
||||
const { main: _, ...options } =
|
||||
await importEntrypointFile<ContentScriptDefinition>(path, config);
|
||||
if (options == null) {
|
||||
throw Error(`Content script ${name} does not have a default export`);
|
||||
}
|
||||
@@ -281,6 +350,7 @@ const PATH_GLOB_TO_TYPE_MAP: Record<string, Entrypoint['type'] | 'ignored'> = {
|
||||
'devtools/index.html': 'devtools',
|
||||
|
||||
'background.ts': 'background',
|
||||
'background/index.ts': 'background',
|
||||
[VIRTUAL_NOOP_BACKGROUND_MODULE_ID]: 'background',
|
||||
|
||||
'content.ts?(x)': 'content-script',
|
||||
@@ -305,8 +375,8 @@ const PATH_GLOB_TO_TYPE_MAP: Record<string, Entrypoint['type'] | 'ignored'> = {
|
||||
[`*.${CSS_EXTENSIONS_PATTERN}`]: 'unlisted-style',
|
||||
[`*/index.${CSS_EXTENSIONS_PATTERN}`]: 'unlisted-style',
|
||||
|
||||
// Don't warn about any files in subdirectories, like CSS or JS entrypoints for HTML files
|
||||
'*/*': 'ignored',
|
||||
// Don't warn about any files in subdirectories, like CSS or JS entrypoints for HTML files or tests
|
||||
'*/**': 'ignored',
|
||||
};
|
||||
|
||||
const CONTENT_SCRIPT_OUT_DIR = 'content-scripts';
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { createUnimport } from 'unimport';
|
||||
import { UnimportOptions, createUnimport } from 'unimport';
|
||||
import { Entrypoint, InternalConfig } from '../types';
|
||||
import fs from 'fs-extra';
|
||||
import { relative, resolve } from 'path';
|
||||
import { getEntrypointBundlePath } from '../utils/entrypoints';
|
||||
import { getUnimportOptions } from '../utils/auto-imports';
|
||||
import { getGlobals } from '../utils/globals';
|
||||
import { getEntrypointGlobals, getGlobals } from '../utils/globals';
|
||||
import { getPublicFiles } from '../utils/public';
|
||||
import { normalizePath } from '../utils/paths';
|
||||
import path from 'node:path';
|
||||
@@ -21,7 +21,12 @@ export async function generateTypesDir(
|
||||
await fs.ensureDir(config.typesDir);
|
||||
|
||||
const references: string[] = [];
|
||||
references.push(await writeImportsDeclarationFile(config));
|
||||
|
||||
const imports = getUnimportOptions(config);
|
||||
if (imports !== false) {
|
||||
references.push(await writeImportsDeclarationFile(config, imports));
|
||||
}
|
||||
|
||||
references.push(await writePathsDeclarationFile(entrypoints, config));
|
||||
references.push(await writeI18nDeclarationFile(config));
|
||||
references.push(await writeGlobalsDeclarationFile(config));
|
||||
@@ -32,9 +37,10 @@ export async function generateTypesDir(
|
||||
|
||||
async function writeImportsDeclarationFile(
|
||||
config: InternalConfig,
|
||||
unimportOptions: Partial<UnimportOptions>,
|
||||
): Promise<string> {
|
||||
const filePath = resolve(config.typesDir, 'imports.d.ts');
|
||||
const unimport = createUnimport(getUnimportOptions(config));
|
||||
const unimport = createUnimport(unimportOptions);
|
||||
|
||||
// Load project imports into unimport memory so they are output via generateTypeDeclarations
|
||||
await unimport.scanImportsFromDir(undefined, { cwd: config.srcDir });
|
||||
@@ -151,7 +157,7 @@ async function writeGlobalsDeclarationFile(
|
||||
config: InternalConfig,
|
||||
): Promise<string> {
|
||||
const filePath = resolve(config.typesDir, 'globals.d.ts');
|
||||
const globals = getGlobals(config);
|
||||
const globals = [...getGlobals(config), ...getEntrypointGlobals(config, '')];
|
||||
await writeFileIfDifferent(
|
||||
filePath,
|
||||
[
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { InternalConfig } from '../types';
|
||||
import { ExtensionRunner } from './extension-runner';
|
||||
import { createWslRunner } from './wsl';
|
||||
import { createWebExtRunner } from './web-ext';
|
||||
import { createSafariRunner } from './safari';
|
||||
|
||||
export async function createExtensionRunner(
|
||||
config: InternalConfig,
|
||||
): Promise<ExtensionRunner> {
|
||||
if (config.browser === 'safari') return createSafariRunner();
|
||||
|
||||
const { default: isWsl } = await import('is-wsl'); // ESM only, requires dynamic import
|
||||
if (isWsl) return createWslRunner();
|
||||
|
||||
return createWebExtRunner();
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { ExtensionRunner } from './extension-runner';
|
||||
import { relative } from 'node:path';
|
||||
|
||||
/**
|
||||
* The Safari runner just logs a warning message because `web-ext` doesn't work with Safari.
|
||||
*/
|
||||
export function createSafariRunner(): ExtensionRunner {
|
||||
return {
|
||||
async openBrowser(config) {
|
||||
config.logger.warn(
|
||||
`Cannot Safari using web-ext. Load "${relative(
|
||||
process.cwd(),
|
||||
config.outDir,
|
||||
)}" as an unpacked extension manually`,
|
||||
);
|
||||
},
|
||||
async closeBrowser() {
|
||||
// noop
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { WebExtRunInstance } from 'web-ext-run';
|
||||
import { ExtensionRunner } from './ExtensionRunner';
|
||||
import { ExtensionRunner } from './extension-runner';
|
||||
|
||||
/**
|
||||
* Create an `ExtensionRunner` backed by `web-ext`.
|
||||
@@ -9,10 +9,7 @@ export function createWebExtRunner(): ExtensionRunner {
|
||||
|
||||
return {
|
||||
async openBrowser(config) {
|
||||
if (config.browser === 'safari') {
|
||||
config.logger.warn('Cannot open safari automatically.');
|
||||
return;
|
||||
}
|
||||
config.logger.info('Opening browser...');
|
||||
|
||||
// Use the plugin's logger instead of web-ext's built-in one.
|
||||
const webExtLogger = await import('web-ext-run/util/logger');
|
||||
@@ -57,6 +54,8 @@ export function createWebExtRunner(): ExtensionRunner {
|
||||
|
||||
const webExt = await import('web-ext-run');
|
||||
runner = await webExt.default.cmd.run(finalConfig, options);
|
||||
|
||||
config.logger.success('Opened!');
|
||||
},
|
||||
|
||||
async closeBrowser() {
|
||||
@@ -0,0 +1,21 @@
|
||||
import { ExtensionRunner } from './extension-runner';
|
||||
import { relative } from 'node:path';
|
||||
|
||||
/**
|
||||
* The WSL runner just logs a warning message because `web-ext` doesn't work in WSL.
|
||||
*/
|
||||
export function createWslRunner(): ExtensionRunner {
|
||||
return {
|
||||
async openBrowser(config) {
|
||||
config.logger.warn(
|
||||
`Cannot open browser when using WSL. Load "${relative(
|
||||
process.cwd(),
|
||||
config.outDir,
|
||||
)}" as an unpacked extension manually`,
|
||||
);
|
||||
},
|
||||
async closeBrowser() {
|
||||
// noop
|
||||
},
|
||||
};
|
||||
}
|
||||
+6
-6
@@ -8,8 +8,8 @@ import * as vite from 'vite';
|
||||
import { Scripting } from 'webextension-polyfill';
|
||||
import { getEntrypointBundlePath } from './utils/entrypoints';
|
||||
import { getContentScriptCssFiles } from './utils/manifest';
|
||||
import { createWebExtRunner } from './runners/createWebExtRunner';
|
||||
import { buildInternal } from './build';
|
||||
import { createExtensionRunner } from './runners';
|
||||
|
||||
export async function getServerInfo(): Promise<ServerInfo> {
|
||||
const { default: getPort, portNumbers } = await import('get-port');
|
||||
@@ -34,10 +34,10 @@ export async function setupServer(
|
||||
serverInfo: ServerInfo,
|
||||
config: InternalConfig,
|
||||
): Promise<WxtDevServer> {
|
||||
const runner = createWebExtRunner();
|
||||
const runner = await createExtensionRunner(config);
|
||||
|
||||
const viteServer = await vite.createServer(
|
||||
vite.mergeConfig(serverInfo, config.vite),
|
||||
vite.mergeConfig(serverInfo, await config.vite(config.env)),
|
||||
);
|
||||
|
||||
const start = async () => {
|
||||
@@ -45,9 +45,7 @@ export async function setupServer(
|
||||
config.logger.success(`Started dev server @ ${serverInfo.origin}`);
|
||||
|
||||
server.currentOutput = await buildInternal(config);
|
||||
config.logger.info('Opening browser...');
|
||||
await runner.openBrowser(config);
|
||||
config.logger.success('Opened!');
|
||||
};
|
||||
|
||||
const reloadExtension = () => {
|
||||
@@ -103,8 +101,10 @@ export function reloadContentScripts(
|
||||
const js = [getEntrypointBundlePath(entry, config.outDir, '.js')];
|
||||
const css = getContentScriptCssFiles([entry], server.currentOutput);
|
||||
|
||||
const { include: _1, exclude: _2, ...options } = entry.options;
|
||||
|
||||
server.reloadContentScript({
|
||||
...entry.options,
|
||||
...options,
|
||||
js,
|
||||
css,
|
||||
});
|
||||
|
||||
+95
-49
@@ -3,6 +3,8 @@ import { Manifest, Scripting } from 'webextension-polyfill';
|
||||
import { UnimportOptions } from 'unimport';
|
||||
import { EntrypointGroup } from '.';
|
||||
import { LogLevel } from 'consola';
|
||||
import { ContentScriptContext } from '../../client/utils/ContentScriptContext';
|
||||
import type { PluginVisualizerOptions } from 'rollup-plugin-visualizer';
|
||||
|
||||
export interface InlineConfig {
|
||||
/**
|
||||
@@ -32,7 +34,7 @@ export interface InlineConfig {
|
||||
/**
|
||||
* > Only available when using the JS API. Not available in `wxt.config.ts` files
|
||||
*
|
||||
* Path to `"wxt.config.ts"` file or false to disable config file discovery.
|
||||
* Path to `wxt.config.ts` file or `false` to disable config file discovery.
|
||||
*
|
||||
* @default "wxt.config.ts"
|
||||
*/
|
||||
@@ -43,23 +45,25 @@ export interface InlineConfig {
|
||||
* @default false
|
||||
*/
|
||||
debug?: boolean;
|
||||
/**
|
||||
* ID of the extension for each store. Used for publishing.
|
||||
*/
|
||||
storeIds?: {
|
||||
chrome?: string;
|
||||
firefox?: string;
|
||||
edge?: string;
|
||||
};
|
||||
/**
|
||||
* Explicitly set a mode to run in. This will override the default mode for each command, and can
|
||||
* be overridden by the command line `--mode` option.
|
||||
*/
|
||||
mode?: string;
|
||||
/**
|
||||
* Customize auto-import options.
|
||||
* Customize auto-import options. Set to `false` to disable auto-imports.
|
||||
*
|
||||
* For example, to add a directory to auto-import from, you can use:
|
||||
*
|
||||
* ```ts
|
||||
* export default defineConfig({
|
||||
* imports: {
|
||||
* dirs: ["some-directory"]
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
imports?: Partial<UnimportOptions>;
|
||||
imports?: Partial<UnimportOptions> | false;
|
||||
/**
|
||||
* Explicitly set a browser to build for. This will override the default browser for each command,
|
||||
* and can be overridden by the command line `--browser` option.
|
||||
@@ -81,18 +85,21 @@ export interface InlineConfig {
|
||||
*/
|
||||
logger?: Logger;
|
||||
/**
|
||||
* Custom Vite options.
|
||||
* Return custom Vite options from a function. See
|
||||
* <https://vitejs.dev/config/shared-options.html>.
|
||||
*
|
||||
* [`root`](#root), [`configFile`](#configfile), and [`mode`](#mode) should be set in WXT's config
|
||||
* instead of Vite's.
|
||||
*
|
||||
* This is a function because any vite plugins added need to be recreated for each individual
|
||||
* build step, incase they have internal state causing them to fail when reused.
|
||||
*/
|
||||
vite?: Omit<vite.UserConfig, 'root' | 'configFile' | 'mode'>;
|
||||
vite?: (env: ConfigEnv) => WxtViteConfig | Promise<WxtViteConfig>;
|
||||
/**
|
||||
* Customize the `manifest.json` output. Can be an object, promise, or function that returns an
|
||||
* object or promise.
|
||||
*/
|
||||
manifest?: UserManifest | Promise<UserManifest> | UserManifestFn;
|
||||
/**
|
||||
* Custom server options.
|
||||
*/
|
||||
server?: WxtDevServer;
|
||||
/**
|
||||
* Custom runner options. Options set here can be overridden in a `web-ext.config.ts` file.
|
||||
*/
|
||||
@@ -149,6 +156,37 @@ export interface InlineConfig {
|
||||
*/
|
||||
ignoredSources?: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Transform the final manifest before it's written to the file system. Edit the `manifest`
|
||||
* parameter directly, do not return a new object. Return values are ignored.
|
||||
*
|
||||
* @example
|
||||
* defineConfig({
|
||||
* // Add a CSS-only content script.
|
||||
* transformManifest(manifest) {
|
||||
* manifest.content_scripts.push({
|
||||
* matches: ["*://google.com/*"],
|
||||
* css: ["content-scripts/some-example.css"],
|
||||
* });
|
||||
* }
|
||||
* })
|
||||
*/
|
||||
transformManifest?: (manifest: Manifest.WebExtensionManifest) => void;
|
||||
analysis?: {
|
||||
/**
|
||||
* Explicitly include bundle analysis when running `wxt build`. This can be overridden by the
|
||||
* command line `--analysis` option.
|
||||
*/
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* When running `wxt build --analyze` or setting `analysis.enabled` to true, customize how the
|
||||
* bundle will be visualized. See
|
||||
* [`rollup-plugin-visualizer`](https://github.com/btd/rollup-plugin-visualizer#how-to-use-generated-files)
|
||||
* for more details.
|
||||
*/
|
||||
template?: PluginVisualizerOptions['template'];
|
||||
};
|
||||
}
|
||||
|
||||
export interface WxtInlineViteConfig
|
||||
@@ -214,7 +252,7 @@ export interface WxtDevServer extends vite.ViteDevServer {
|
||||
) => void;
|
||||
}
|
||||
|
||||
export type TargetBrowser = 'chrome' | 'firefox' | 'safari' | 'edge' | 'opera';
|
||||
export type TargetBrowser = string;
|
||||
export type TargetManifestVersion = 2 | 3;
|
||||
|
||||
export type UserConfig = Omit<InlineConfig, 'configFile'>;
|
||||
@@ -230,6 +268,11 @@ export interface Logger {
|
||||
level: LogLevel;
|
||||
}
|
||||
|
||||
export interface BaseEntrypointOptions {
|
||||
include?: TargetBrowser[];
|
||||
exclude?: TargetBrowser[];
|
||||
}
|
||||
|
||||
export interface BaseEntrypoint {
|
||||
/**
|
||||
* The entrypoint's name. This is the filename or dirname without the type suffix.
|
||||
@@ -257,6 +300,7 @@ export interface BaseEntrypoint {
|
||||
* subdirectory of it.
|
||||
*/
|
||||
outputDir: string;
|
||||
options: BaseEntrypointOptions;
|
||||
}
|
||||
|
||||
export interface GenericEntrypoint extends BaseEntrypoint {
|
||||
@@ -278,12 +322,12 @@ export interface BackgroundEntrypoint extends BaseEntrypoint {
|
||||
options: {
|
||||
persistent?: boolean;
|
||||
type?: 'module';
|
||||
};
|
||||
} & BaseEntrypointOptions;
|
||||
}
|
||||
|
||||
export interface ContentScriptEntrypoint extends BaseEntrypoint {
|
||||
type: 'content-script';
|
||||
options: Omit<ContentScriptDefinition, 'main'>;
|
||||
options: Omit<ContentScriptDefinition, 'main'> & BaseEntrypointOptions;
|
||||
}
|
||||
|
||||
export interface PopupEntrypoint extends BaseEntrypoint {
|
||||
@@ -295,7 +339,7 @@ export interface PopupEntrypoint extends BaseEntrypoint {
|
||||
mv2Key?: 'browser_action' | 'page_action';
|
||||
defaultIcon?: Record<string, string>;
|
||||
defaultTitle?: string;
|
||||
};
|
||||
} & BaseEntrypointOptions;
|
||||
}
|
||||
|
||||
export interface OptionsEntrypoint extends BaseEntrypoint {
|
||||
@@ -304,7 +348,7 @@ export interface OptionsEntrypoint extends BaseEntrypoint {
|
||||
openInTab?: boolean;
|
||||
browserStyle?: boolean;
|
||||
chromeStyle?: boolean;
|
||||
};
|
||||
} & BaseEntrypointOptions;
|
||||
}
|
||||
|
||||
export type Entrypoint =
|
||||
@@ -316,7 +360,7 @@ export type Entrypoint =
|
||||
|
||||
export type OnContentScriptStopped = (cb: () => void) => void;
|
||||
|
||||
export interface ContentScriptDefinition {
|
||||
export interface ContentScriptDefinition extends ExcludableEntrypoint {
|
||||
matches: Manifest.ContentScript['matches'];
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
|
||||
@@ -361,14 +405,31 @@ export interface ContentScriptDefinition {
|
||||
/**
|
||||
* Main function executed when the content script is loaded.
|
||||
*/
|
||||
main(): void | Promise<void>;
|
||||
main(ctx: ContentScriptContext): void | Promise<void>;
|
||||
}
|
||||
|
||||
export interface BackgroundScriptDefintition {
|
||||
export interface BackgroundScriptDefintition extends ExcludableEntrypoint {
|
||||
type?: 'module';
|
||||
main(): void;
|
||||
}
|
||||
|
||||
export interface ExcludableEntrypoint {
|
||||
/**
|
||||
* List of target browsers to include this entrypoint in. Defaults to being included in all
|
||||
* builds. Cannot be used with `exclude`. You must choose one of the two options.
|
||||
*
|
||||
* @default undefined
|
||||
*/
|
||||
include?: TargetBrowser[];
|
||||
/**
|
||||
* List of target browsers to exclude this entrypoint from. Cannot be used with `include`. You
|
||||
* must choose one of the two options.
|
||||
*
|
||||
* @default undefined
|
||||
*/
|
||||
exclude?: TargetBrowser[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Manifest customization available in the `wxt.config.ts` file. You cannot configure entrypoints
|
||||
* here, they are configured inline.
|
||||
@@ -423,31 +484,11 @@ export interface ExtensionRunnerConfig {
|
||||
openDevtools?: boolean;
|
||||
/**
|
||||
* List of browser names and the binary that should be used to open the browser.
|
||||
*
|
||||
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#chromium-binary
|
||||
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#firefox
|
||||
*/
|
||||
binaries?: {
|
||||
/**
|
||||
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#chromium-binary
|
||||
*/
|
||||
chrome?: string;
|
||||
/**
|
||||
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#chromium-binary
|
||||
*/
|
||||
edge?: string;
|
||||
/**
|
||||
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#chromium-binary
|
||||
*/
|
||||
opera?: string;
|
||||
/**
|
||||
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#firefox
|
||||
*/
|
||||
firefox?:
|
||||
| 'firefox'
|
||||
| 'beta'
|
||||
| 'nightly'
|
||||
| 'deved'
|
||||
| 'firefoxdeveloperedition'
|
||||
| string;
|
||||
};
|
||||
binaries?: Record<string, string>;
|
||||
/**
|
||||
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#firefox-profile
|
||||
*/
|
||||
@@ -473,3 +514,8 @@ export interface ExtensionRunnerConfig {
|
||||
*/
|
||||
startUrls?: string[];
|
||||
}
|
||||
|
||||
export type WxtViteConfig = Omit<
|
||||
vite.UserConfig,
|
||||
'root' | 'configFile' | 'mode'
|
||||
>;
|
||||
|
||||
@@ -7,9 +7,12 @@ import {
|
||||
TargetManifestVersion,
|
||||
UserManifest,
|
||||
ExtensionRunnerConfig,
|
||||
ConfigEnv,
|
||||
} from './external';
|
||||
import { UnimportOptions } from 'unimport';
|
||||
import { ResolvedConfig } from 'c12';
|
||||
import { Manifest } from 'webextension-polyfill';
|
||||
import type { PluginVisualizerOptions } from 'rollup-plugin-visualizer';
|
||||
|
||||
export interface InternalConfig {
|
||||
root: string;
|
||||
@@ -21,18 +24,14 @@ export interface InternalConfig {
|
||||
outBaseDir: string;
|
||||
outDir: string;
|
||||
debug: boolean;
|
||||
storeIds: {
|
||||
chrome?: string;
|
||||
firefox?: string;
|
||||
edge?: string;
|
||||
};
|
||||
mode: string;
|
||||
command: 'build' | 'serve';
|
||||
browser: TargetBrowser;
|
||||
manifestVersion: TargetManifestVersion;
|
||||
env: ConfigEnv;
|
||||
logger: Logger;
|
||||
imports: Partial<UnimportOptions>;
|
||||
vite: vite.InlineConfig;
|
||||
imports: false | Partial<UnimportOptions>;
|
||||
vite: (env: ConfigEnv) => Promise<vite.InlineConfig> | vite.InlineConfig;
|
||||
manifest: UserManifest;
|
||||
fsCache: FsCache;
|
||||
server?: WxtDevServer;
|
||||
@@ -44,6 +43,11 @@ export interface InternalConfig {
|
||||
ignoredSources: string[];
|
||||
sourcesRoot: string;
|
||||
};
|
||||
transformManifest: (manifest: Manifest.WebExtensionManifest) => void;
|
||||
analysis: {
|
||||
enabled: boolean;
|
||||
template: NonNullable<PluginVisualizerOptions['template']>;
|
||||
};
|
||||
}
|
||||
|
||||
export type EntrypointGroup = Entrypoint | Entrypoint[];
|
||||
|
||||
@@ -42,6 +42,7 @@ describe('Entrypoint Utils', () => {
|
||||
inputPath: '...',
|
||||
name,
|
||||
outputDir,
|
||||
options: {},
|
||||
};
|
||||
|
||||
const actual = getEntrypointOutputFile(entrypoint, ext);
|
||||
|
||||
@@ -23,6 +23,7 @@ const unlistedScript: Entrypoint = {
|
||||
name: 'injected',
|
||||
inputPath: '/injected.ts',
|
||||
outputDir: '/.output/injected',
|
||||
options: {},
|
||||
};
|
||||
const popup: Entrypoint = {
|
||||
type: 'popup',
|
||||
@@ -36,6 +37,7 @@ const unlistedPage: Entrypoint = {
|
||||
name: 'onboarding',
|
||||
inputPath: '/onboarding.html',
|
||||
outputDir: '/.output/onboarding',
|
||||
options: {},
|
||||
};
|
||||
const options: Entrypoint = {
|
||||
type: 'options',
|
||||
@@ -49,24 +51,28 @@ const sandbox1: Entrypoint = {
|
||||
name: 'sandbox',
|
||||
inputPath: '/sandbox1.html',
|
||||
outputDir: '/.output/sandbox1',
|
||||
options: {},
|
||||
};
|
||||
const sandbox2: Entrypoint = {
|
||||
type: 'sandbox',
|
||||
name: 'sandbox2',
|
||||
inputPath: '/sandbox2.html',
|
||||
outputDir: '/.output/sandbox2',
|
||||
options: {},
|
||||
};
|
||||
const unlistedStyle: Entrypoint = {
|
||||
type: 'unlisted-style',
|
||||
name: 'injected',
|
||||
inputPath: '/injected.scss',
|
||||
outputDir: '/.output',
|
||||
options: {},
|
||||
};
|
||||
const contentScriptStyle: Entrypoint = {
|
||||
type: 'content-script-style',
|
||||
name: 'injected',
|
||||
inputPath: '/overlay.content.scss',
|
||||
outputDir: '/.output/content-scripts',
|
||||
options: {},
|
||||
};
|
||||
|
||||
describe('groupEntrypoints', () => {
|
||||
|
||||
@@ -4,7 +4,9 @@ import { mergeConfig } from 'vite';
|
||||
|
||||
export function getUnimportOptions(
|
||||
config: InternalConfig,
|
||||
): Partial<UnimportOptions> {
|
||||
): Partial<UnimportOptions | false> {
|
||||
if (config.imports === false) return false;
|
||||
|
||||
const defaultOptions: Partial<UnimportOptions> = {
|
||||
debugLog: config.logger.debug,
|
||||
imports: [{ name: 'defineConfig', from: 'wxt' }],
|
||||
|
||||
+205
-134
@@ -1,176 +1,121 @@
|
||||
import { loadConfig } from 'c12';
|
||||
import {
|
||||
ConfigEnv,
|
||||
ExtensionRunnerConfig,
|
||||
InlineConfig,
|
||||
InternalConfig,
|
||||
UserConfig,
|
||||
UserManifest,
|
||||
ConfigEnv,
|
||||
UserManifestFn,
|
||||
UserManifest,
|
||||
WxtViteConfig,
|
||||
ExtensionRunnerConfig,
|
||||
} from '../types';
|
||||
import path, { resolve } from 'node:path';
|
||||
import path from 'node:path';
|
||||
import * as vite from 'vite';
|
||||
import { LogLevels, consola } from 'consola';
|
||||
import * as plugins from '../vite-plugins';
|
||||
import { createFsCache } from './createFsCache';
|
||||
import consola, { LogLevels } from 'consola';
|
||||
import * as plugins from '../vite-plugins';
|
||||
import { getGlobals } from './globals';
|
||||
import { loadConfig } from 'c12';
|
||||
|
||||
/**
|
||||
* Given an inline config, discover the config file if necessary, merge the results, resolve any
|
||||
* relative paths, and apply any defaults.
|
||||
*
|
||||
* Inline config always has priority over user config. Cli flags are passed as inline config if set.
|
||||
* If unset, undefined is passed in, letting this function decide default values.
|
||||
*/
|
||||
export async function getInternalConfig(
|
||||
config: InlineConfig,
|
||||
inlineConfig: InlineConfig,
|
||||
command: 'build' | 'serve',
|
||||
): Promise<InternalConfig> {
|
||||
// Apply defaults to a base config
|
||||
const root = config.root ? path.resolve(config.root) : process.cwd();
|
||||
const mode =
|
||||
config.mode ?? (command === 'build' ? 'production' : 'development');
|
||||
const browser = config.browser ?? 'chrome';
|
||||
const manifestVersion =
|
||||
config.manifestVersion ?? (browser == 'firefox' ? 2 : 3);
|
||||
const outBaseDir = path.resolve(root, '.output');
|
||||
const outDir = path.resolve(outBaseDir, `${browser}-mv${manifestVersion}`);
|
||||
const logger = config.logger ?? consola;
|
||||
const debug = !!config.debug;
|
||||
if (debug) logger.level = LogLevels.debug;
|
||||
// Load user config
|
||||
|
||||
const baseConfig: InternalConfigNoUserDirs = {
|
||||
root,
|
||||
outDir,
|
||||
outBaseDir,
|
||||
storeIds: config.storeIds ?? {},
|
||||
browser,
|
||||
manifestVersion,
|
||||
mode,
|
||||
command,
|
||||
debug,
|
||||
logger,
|
||||
vite: config.vite ?? {},
|
||||
imports: config.imports ?? {},
|
||||
runnerConfig: await loadConfig<ExtensionRunnerConfig>({
|
||||
name: 'web-ext',
|
||||
cwd: root,
|
||||
globalRc: true,
|
||||
rcFile: '.webextrc',
|
||||
overrides: config.runner,
|
||||
}),
|
||||
};
|
||||
|
||||
// Load user config from file
|
||||
let userConfig: UserConfig = {
|
||||
mode,
|
||||
};
|
||||
if (config.configFile !== false) {
|
||||
let userConfig: UserConfig = {};
|
||||
if (inlineConfig.configFile !== false) {
|
||||
const loaded = await loadConfig<UserConfig>({
|
||||
name: 'wxt',
|
||||
cwd: root,
|
||||
cwd: inlineConfig.root ?? process.cwd(),
|
||||
rcFile: false,
|
||||
});
|
||||
userConfig = loaded.config ?? {};
|
||||
}
|
||||
|
||||
// Merge inline and user configs
|
||||
const merged = vite.mergeConfig(
|
||||
baseConfig,
|
||||
userConfig,
|
||||
) as InternalConfigNoUserDirs;
|
||||
// Merge it into the inline config
|
||||
|
||||
// Apply user config and create final config
|
||||
const srcDir = userConfig.srcDir ? resolve(root, userConfig.srcDir) : root;
|
||||
const entrypointsDir = resolve(
|
||||
srcDir,
|
||||
userConfig.entrypointsDir ?? 'entrypoints',
|
||||
const mergedConfig = mergeInlineConfig(inlineConfig, userConfig);
|
||||
|
||||
// Apply defaults to make internal config.
|
||||
|
||||
const debug = mergedConfig.debug ?? false;
|
||||
const logger = mergedConfig.logger ?? consola;
|
||||
if (debug) logger.level = LogLevels.debug;
|
||||
|
||||
const browser = mergedConfig.browser ?? 'chrome';
|
||||
const manifestVersion =
|
||||
mergedConfig.manifestVersion ?? (browser == 'firefox' ? 2 : 3);
|
||||
const mode =
|
||||
mergedConfig.mode ?? (command === 'build' ? 'production' : 'development');
|
||||
const env: ConfigEnv = { browser, command, manifestVersion, mode };
|
||||
|
||||
const root = path.resolve(
|
||||
inlineConfig.root ?? userConfig.root ?? process.cwd(),
|
||||
);
|
||||
const publicDir = resolve(srcDir, userConfig.publicDir ?? 'public');
|
||||
const wxtDir = resolve(root, '.wxt');
|
||||
const typesDir = resolve(wxtDir, 'types');
|
||||
const wxtDir = path.resolve(root, '.wxt');
|
||||
const srcDir = path.resolve(root, mergedConfig.srcDir ?? root);
|
||||
const entrypointsDir = path.resolve(
|
||||
srcDir,
|
||||
mergedConfig.entrypointsDir ?? 'entrypoints',
|
||||
);
|
||||
const publicDir = path.resolve(srcDir, mergedConfig.publicDir ?? 'public');
|
||||
const typesDir = path.resolve(wxtDir, 'types');
|
||||
const outBaseDir = path.resolve(root, '.output');
|
||||
const outDir = path.resolve(outBaseDir, `${browser}-mv${manifestVersion}`);
|
||||
|
||||
// Merge manifest sources
|
||||
const env: ConfigEnv = { mode, browser, manifestVersion, command };
|
||||
const userManifest = await resolveManifestConfig(env, userConfig.manifest);
|
||||
const inlineManifest = await resolveManifestConfig(env, config.manifest);
|
||||
const manifest = vite.mergeConfig(userManifest, inlineManifest);
|
||||
const runnerConfig = await loadConfig<ExtensionRunnerConfig>({
|
||||
name: 'web-ext',
|
||||
cwd: root,
|
||||
globalRc: true,
|
||||
rcFile: '.webextrc',
|
||||
overrides: mergedConfig.runner,
|
||||
});
|
||||
|
||||
const finalConfig: InternalConfig = {
|
||||
...merged,
|
||||
srcDir,
|
||||
browser,
|
||||
command,
|
||||
debug,
|
||||
entrypointsDir,
|
||||
publicDir,
|
||||
wxtDir: wxtDir,
|
||||
typesDir,
|
||||
env,
|
||||
fsCache: createFsCache(wxtDir),
|
||||
manifest,
|
||||
zip: {
|
||||
sourcesTemplate: '{{name}}-{{version}}-sources.zip',
|
||||
artifactTemplate: '{{name}}-{{version}}-{{browser}}.zip',
|
||||
sourcesRoot: root,
|
||||
...userConfig.zip,
|
||||
...config.zip,
|
||||
ignoredSources: [
|
||||
'**/node_modules',
|
||||
// WXT files
|
||||
'**/web-ext.config.ts',
|
||||
// Hidden files
|
||||
'**/.*',
|
||||
// Tests
|
||||
'**/__tests__/**',
|
||||
'**/*.+(test|spec).?(c|m)+(j|t)s?(x)',
|
||||
// User config
|
||||
...(userConfig.zip?.ignoredSources ?? []),
|
||||
...(config.zip?.ignoredSources ?? []),
|
||||
],
|
||||
imports: mergedConfig.imports ?? {},
|
||||
logger,
|
||||
manifest: await resolveManifestConfig(env, mergedConfig.manifest),
|
||||
manifestVersion,
|
||||
mode,
|
||||
outBaseDir,
|
||||
outDir,
|
||||
publicDir,
|
||||
root,
|
||||
runnerConfig,
|
||||
srcDir,
|
||||
typesDir,
|
||||
vite: () => ({}), // Real value added after this object is initialized.
|
||||
wxtDir,
|
||||
zip: resolveInternalZipConfig(root, mergedConfig),
|
||||
transformManifest(manifest) {
|
||||
userConfig.transformManifest?.(manifest);
|
||||
inlineConfig.transformManifest?.(manifest);
|
||||
},
|
||||
analysis: {
|
||||
enabled: mergedConfig.analysis?.enabled ?? false,
|
||||
template: mergedConfig.analysis?.template ?? 'treemap',
|
||||
},
|
||||
};
|
||||
|
||||
// Customize the default vite config
|
||||
finalConfig.vite.root = root;
|
||||
finalConfig.vite.configFile = false;
|
||||
finalConfig.vite.logLevel = 'warn';
|
||||
|
||||
finalConfig.vite.build ??= {};
|
||||
finalConfig.vite.build.outDir = outDir;
|
||||
finalConfig.vite.build.emptyOutDir = false;
|
||||
|
||||
finalConfig.vite.plugins ??= [];
|
||||
finalConfig.vite.plugins.push(plugins.download(finalConfig));
|
||||
finalConfig.vite.plugins.push(plugins.devHtmlPrerender(finalConfig));
|
||||
finalConfig.vite.plugins.push(plugins.unimport(finalConfig));
|
||||
finalConfig.vite.plugins.push(
|
||||
plugins.virtualEntrypoin('background', finalConfig),
|
||||
);
|
||||
finalConfig.vite.plugins.push(
|
||||
plugins.virtualEntrypoin('content-script', finalConfig),
|
||||
);
|
||||
finalConfig.vite.plugins.push(plugins.devServerGlobals(finalConfig));
|
||||
finalConfig.vite.plugins.push(plugins.tsconfigPaths(finalConfig));
|
||||
finalConfig.vite.plugins.push(plugins.noopBackground());
|
||||
|
||||
finalConfig.vite.define ??= {};
|
||||
getGlobals(finalConfig).forEach((global) => {
|
||||
finalConfig.vite.define![global.name] = JSON.stringify(global.value);
|
||||
});
|
||||
finalConfig.vite = (env) =>
|
||||
resolveInternalViteConfig(env, mergedConfig, finalConfig);
|
||||
|
||||
return finalConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper type for defining a base config, since user-configurable directories must be set after
|
||||
* reading in the user config.
|
||||
*/
|
||||
type InternalConfigNoUserDirs = Omit<
|
||||
InternalConfig,
|
||||
| 'srcDir'
|
||||
| 'publicDir'
|
||||
| 'entrypointsDir'
|
||||
| 'wxtDir'
|
||||
| 'typesDir'
|
||||
| 'fsCache'
|
||||
| 'manifest'
|
||||
| 'zip'
|
||||
>;
|
||||
|
||||
async function resolveManifestConfig(
|
||||
env: ConfigEnv,
|
||||
manifest: UserManifest | Promise<UserManifest> | UserManifestFn | undefined,
|
||||
@@ -179,3 +124,129 @@ async function resolveManifestConfig(
|
||||
? manifest(env)
|
||||
: manifest ?? {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the inline config and user config. Inline config is given priority. Defaults are not applied here.
|
||||
*/
|
||||
function mergeInlineConfig(
|
||||
inlineConfig: InlineConfig,
|
||||
userConfig: UserConfig,
|
||||
): InlineConfig {
|
||||
let imports: InlineConfig['imports'];
|
||||
if (inlineConfig.imports === false || userConfig.imports === false) {
|
||||
imports = false;
|
||||
} else if (userConfig.imports == null && inlineConfig.imports == null) {
|
||||
imports = undefined;
|
||||
} else {
|
||||
imports = vite.mergeConfig(
|
||||
userConfig.imports ?? {},
|
||||
inlineConfig.imports ?? {},
|
||||
);
|
||||
}
|
||||
const manifest: UserManifestFn = async (env) => {
|
||||
const user = await resolveManifestConfig(env, userConfig.manifest);
|
||||
const inline = await resolveManifestConfig(env, inlineConfig.manifest);
|
||||
return vite.mergeConfig(user, inline);
|
||||
};
|
||||
const viteConfig = async (env: ConfigEnv): Promise<WxtViteConfig> => {
|
||||
const user = await userConfig.vite?.(env);
|
||||
const inline = await inlineConfig.vite?.(env);
|
||||
return vite.mergeConfig(user ?? {}, inline ?? {});
|
||||
};
|
||||
const runner: InlineConfig['runner'] = vite.mergeConfig(
|
||||
userConfig.runner ?? {},
|
||||
inlineConfig.runner ?? {},
|
||||
);
|
||||
const zip: InlineConfig['zip'] = vite.mergeConfig(
|
||||
userConfig.zip ?? {},
|
||||
inlineConfig.zip ?? {},
|
||||
);
|
||||
|
||||
return {
|
||||
root: inlineConfig.root ?? userConfig.root,
|
||||
browser: inlineConfig.browser ?? userConfig.browser,
|
||||
manifestVersion: inlineConfig.manifestVersion ?? userConfig.manifestVersion,
|
||||
configFile: inlineConfig.configFile,
|
||||
debug: inlineConfig.debug ?? userConfig.debug,
|
||||
entrypointsDir: inlineConfig.entrypointsDir ?? userConfig.entrypointsDir,
|
||||
imports,
|
||||
logger: inlineConfig.logger ?? userConfig.logger,
|
||||
manifest,
|
||||
mode: inlineConfig.mode ?? userConfig.mode,
|
||||
publicDir: inlineConfig.publicDir ?? userConfig.publicDir,
|
||||
runner,
|
||||
srcDir: inlineConfig.srcDir ?? userConfig.srcDir,
|
||||
vite: viteConfig,
|
||||
zip,
|
||||
analysis: {
|
||||
enabled: inlineConfig.analysis?.enabled ?? userConfig.analysis?.enabled,
|
||||
template:
|
||||
inlineConfig.analysis?.template ?? userConfig.analysis?.template,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function resolveInternalZipConfig(
|
||||
root: string,
|
||||
mergedConfig: InlineConfig,
|
||||
): InternalConfig['zip'] {
|
||||
return {
|
||||
sourcesTemplate: '{{name}}-{{version}}-sources.zip',
|
||||
artifactTemplate: '{{name}}-{{version}}-{{browser}}.zip',
|
||||
sourcesRoot: root,
|
||||
...mergedConfig.zip,
|
||||
ignoredSources: [
|
||||
'**/node_modules',
|
||||
// WXT files
|
||||
'**/web-ext.config.ts',
|
||||
// Hidden files
|
||||
'**/.*',
|
||||
// Tests
|
||||
'**/__tests__/**',
|
||||
'**/*.+(test|spec).?(c|m)+(j|t)s?(x)',
|
||||
// From user
|
||||
...(mergedConfig.zip?.ignoredSources ?? []),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveInternalViteConfig(
|
||||
env: ConfigEnv,
|
||||
mergedConfig: InlineConfig,
|
||||
finalConfig: InternalConfig,
|
||||
) {
|
||||
const internalVite: vite.InlineConfig =
|
||||
(await mergedConfig.vite?.(env)) ?? {};
|
||||
|
||||
internalVite.root = finalConfig.root;
|
||||
internalVite.configFile = false;
|
||||
internalVite.logLevel = 'warn';
|
||||
internalVite.mode = env.mode;
|
||||
|
||||
internalVite.build ??= {};
|
||||
internalVite.build.outDir = finalConfig.outDir;
|
||||
internalVite.build.emptyOutDir = false;
|
||||
|
||||
internalVite.plugins ??= [];
|
||||
internalVite.plugins.push(plugins.download(finalConfig));
|
||||
internalVite.plugins.push(plugins.devHtmlPrerender(finalConfig));
|
||||
internalVite.plugins.push(plugins.unimport(finalConfig));
|
||||
internalVite.plugins.push(
|
||||
plugins.virtualEntrypoin('background', finalConfig),
|
||||
);
|
||||
internalVite.plugins.push(
|
||||
plugins.virtualEntrypoin('content-script', finalConfig),
|
||||
);
|
||||
internalVite.plugins.push(plugins.devServerGlobals(finalConfig));
|
||||
internalVite.plugins.push(plugins.tsconfigPaths(finalConfig));
|
||||
internalVite.plugins.push(plugins.noopBackground());
|
||||
if (finalConfig.analysis.enabled) {
|
||||
internalVite.plugins.push(plugins.bundleAnalysis());
|
||||
}
|
||||
|
||||
internalVite.define ??= {};
|
||||
for (const global of getGlobals(finalConfig)) {
|
||||
internalVite.define[global.name] = JSON.stringify(global.value);
|
||||
}
|
||||
return internalVite;
|
||||
}
|
||||
|
||||
@@ -46,3 +46,16 @@ export function getGlobals(
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function getEntrypointGlobals(
|
||||
config: InternalConfig,
|
||||
entrypointName: string,
|
||||
) {
|
||||
return [
|
||||
{
|
||||
name: '__ENTRYPOINT__',
|
||||
value: entrypointName,
|
||||
type: `string`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import createJITI from 'jiti';
|
||||
import createJITI, { TransformOptions as JitiTransformOptions } from 'jiti';
|
||||
import { InternalConfig } from '../types';
|
||||
import { createUnimport } from 'unimport';
|
||||
import fs from 'fs-extra';
|
||||
import { resolve } from 'path';
|
||||
import transform from 'jiti/dist/babel';
|
||||
import { getUnimportOptions } from './auto-imports';
|
||||
import { removeImportStatements } from './strings';
|
||||
import { removeProjectImportStatements } from './strings';
|
||||
import { normalizePath } from './paths';
|
||||
import { TransformOptions, transformSync } from 'esbuild';
|
||||
|
||||
/**
|
||||
* Get the value from the default export of a `path`.
|
||||
@@ -23,7 +23,7 @@ import { normalizePath } from './paths';
|
||||
* Downside is that code cannot be executed outside of the main fucntion for the entrypoint,
|
||||
* otherwise you will see "xxx is not defined" errors for any imports used outside of main function.
|
||||
*/
|
||||
export async function importTsFile<T>(
|
||||
export async function importEntrypointFile<T>(
|
||||
path: string,
|
||||
config: InternalConfig,
|
||||
): Promise<T> {
|
||||
@@ -39,7 +39,7 @@ export async function importTsFile<T>(
|
||||
await unimport.init();
|
||||
|
||||
const text = await fs.readFile(path, 'utf-8');
|
||||
const textNoImports = removeImportStatements(text);
|
||||
const textNoImports = removeProjectImportStatements(text);
|
||||
const { code } = await unimport.injectImports(textNoImports);
|
||||
config.logger.debug(
|
||||
['Text:', text, 'No imports:', textNoImports, 'Code:', code].join('\n'),
|
||||
@@ -47,6 +47,7 @@ export async function importTsFile<T>(
|
||||
|
||||
const jiti = createJITI(__filename, {
|
||||
cache: false,
|
||||
debug: config.debug,
|
||||
esmResolve: true,
|
||||
interopDefault: true,
|
||||
alias: {
|
||||
@@ -55,10 +56,14 @@ export async function importTsFile<T>(
|
||||
'node_modules/wxt/dist/virtual-modules/fake-browser.js',
|
||||
),
|
||||
},
|
||||
extensions: ['.ts', '.tsx', '.cjs', '.js', '.mjs'],
|
||||
transform(opts) {
|
||||
if (opts.filename === normalPath)
|
||||
return transform({ ...opts, source: code });
|
||||
else return transform(opts);
|
||||
const isEntrypoint = opts.filename === normalPath;
|
||||
return transformSync(
|
||||
// Use modified source code for entrypoints
|
||||
isEntrypoint ? code : opts.source,
|
||||
getEsbuildOptions(opts),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -69,3 +74,12 @@ export async function importTsFile<T>(
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function getEsbuildOptions(opts: JitiTransformOptions): TransformOptions {
|
||||
const isJsx = opts.filename?.endsWith('x');
|
||||
return {
|
||||
format: 'cjs',
|
||||
loader: isJsx ? 'tsx' : 'ts',
|
||||
jsx: isJsx ? 'automatic' : undefined,
|
||||
};
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import { getPackageJson } from './package';
|
||||
import { normalizePath } from './paths';
|
||||
import { writeFileIfDifferent } from './fs';
|
||||
import { produce } from 'immer';
|
||||
|
||||
/**
|
||||
* Writes the manifest to the output directory and the build output.
|
||||
@@ -89,7 +90,7 @@ export async function generateMainfest(
|
||||
);
|
||||
}
|
||||
|
||||
return manifest;
|
||||
return produce(manifest, config.transformManifest);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,3 +15,14 @@ export function removeImportStatements(text: string): string {
|
||||
'',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes imports, ensuring that some of WXT's client imports are present, so that entrypoints can be parsed if auto-imports are disabled.
|
||||
*/
|
||||
export function removeProjectImportStatements(text: string): string {
|
||||
const noImports = removeImportStatements(text);
|
||||
|
||||
return `import { defineContentScript, defineBackground } from 'wxt/client';
|
||||
|
||||
${noImports}`;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import * as vite from 'vite';
|
||||
import { visualizer } from 'rollup-plugin-visualizer';
|
||||
|
||||
let increment = 0;
|
||||
|
||||
export function bundleAnalysis(): vite.Plugin {
|
||||
return visualizer({
|
||||
emitFile: true,
|
||||
template: 'raw-data',
|
||||
filename: `stats-${increment++}.json`,
|
||||
}) as vite.Plugin;
|
||||
}
|
||||
@@ -7,3 +7,4 @@ export * from './virtualEntrypoint';
|
||||
export * from './tsconfigPaths';
|
||||
export * from './noopBackground';
|
||||
export * from './cssEntrypoints';
|
||||
export * from './bundleAnalysis';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createUnimport } from 'unimport';
|
||||
import { InternalConfig } from '../types';
|
||||
import { getUnimportOptions } from '../utils/auto-imports';
|
||||
import { Plugin } from 'vite';
|
||||
import * as vite from 'vite';
|
||||
import { extname } from 'path';
|
||||
|
||||
const ENABLED_EXTENSIONS: Record<string, boolean | undefined> = {
|
||||
@@ -16,8 +16,10 @@ const ENABLED_EXTENSIONS: Record<string, boolean | undefined> = {
|
||||
/**
|
||||
* Inject any global imports defined by unimport
|
||||
*/
|
||||
export function unimport(config: InternalConfig): Plugin {
|
||||
export function unimport(config: InternalConfig): vite.PluginOption {
|
||||
const options = getUnimportOptions(config);
|
||||
if (options === false) return [];
|
||||
|
||||
const unimport = createUnimport(options);
|
||||
|
||||
return {
|
||||
|
||||
+7
-6
@@ -1,7 +1,6 @@
|
||||
import { BuildOutput, WxtDevServer, InlineConfig } from './core/types';
|
||||
import { getInternalConfig } from './core/utils/getInternalConfig';
|
||||
import pc from 'picocolors';
|
||||
import * as vite from 'vite';
|
||||
import { detectDevChanges } from './core/utils/detectDevChanges';
|
||||
import { Mutex } from 'async-mutex';
|
||||
import { consola } from 'consola';
|
||||
@@ -38,12 +37,14 @@ export async function createServer(
|
||||
): Promise<WxtDevServer> {
|
||||
const serverInfo = await getServerInfo();
|
||||
|
||||
const getLatestInternalConfig = () => {
|
||||
const viteConfig: vite.InlineConfig = vite.mergeConfig(
|
||||
serverInfo.viteServerConfig,
|
||||
config?.vite ?? {},
|
||||
const getLatestInternalConfig = async () => {
|
||||
return getInternalConfig(
|
||||
{
|
||||
...config,
|
||||
vite: () => serverInfo.viteServerConfig,
|
||||
},
|
||||
'serve',
|
||||
);
|
||||
return getInternalConfig({ ...config, vite: viteConfig }, 'serve');
|
||||
};
|
||||
|
||||
let internalConfig = await getLatestInternalConfig();
|
||||
|
||||
+46
-32
@@ -77,6 +77,7 @@ export const fakeUnlistedScriptEntrypoint =
|
||||
inputPath: fakeFile('src'),
|
||||
name: faker.string.alpha(),
|
||||
outputDir: fakeDir('.output'),
|
||||
options: {},
|
||||
}));
|
||||
|
||||
export const fakeOptionsEntrypoint = fakeObjectCreator<OptionsEntrypoint>(
|
||||
@@ -133,6 +134,7 @@ export const fakeGenericEntrypoint = fakeObjectCreator<GenericEntrypoint>(
|
||||
inputPath: fakeFile('src'),
|
||||
name: faker.string.alpha(),
|
||||
outputDir: fakeDir('.output'),
|
||||
options: {},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -196,35 +198,47 @@ export function fakeArray<T>(createItem: () => T, count = 3): T[] {
|
||||
return array;
|
||||
}
|
||||
|
||||
export const fakeInternalConfig = fakeObjectCreator<InternalConfig>(() => ({
|
||||
browser: faker.helpers.arrayElement(['chrome', 'firefox']),
|
||||
command: faker.helpers.arrayElement(['build', 'serve']),
|
||||
entrypointsDir: fakeDir(),
|
||||
fsCache: mock<FsCache>(),
|
||||
imports: {},
|
||||
logger: mock(),
|
||||
manifest: fakeManifest(),
|
||||
manifestVersion: faker.helpers.arrayElement([2, 3]),
|
||||
mode: faker.helpers.arrayElement(['development', 'production']),
|
||||
outBaseDir: fakeDir(),
|
||||
outDir: fakeDir(),
|
||||
publicDir: fakeDir(),
|
||||
root: fakeDir(),
|
||||
runnerConfig: {
|
||||
config: {},
|
||||
},
|
||||
debug: faker.datatype.boolean(),
|
||||
srcDir: fakeDir(),
|
||||
storeIds: {},
|
||||
typesDir: fakeDir(),
|
||||
vite: {},
|
||||
wxtDir: fakeDir(),
|
||||
server: mock<WxtDevServer>(),
|
||||
zip: {
|
||||
artifactTemplate: '{{name}}-{{version}}.zip',
|
||||
ignoredSources: [],
|
||||
sourcesRoot: fakeDir(),
|
||||
sourcesTemplate: '{{name}}-sources.zip',
|
||||
name: faker.person.firstName().toLowerCase(),
|
||||
},
|
||||
}));
|
||||
export const fakeInternalConfig = fakeObjectCreator<InternalConfig>(() => {
|
||||
const browser = faker.helpers.arrayElement(['chrome', 'firefox']);
|
||||
const command = faker.helpers.arrayElement(['build', 'serve'] as const);
|
||||
const manifestVersion = faker.helpers.arrayElement([2, 3] as const);
|
||||
const mode = faker.helpers.arrayElement(['development', 'production']);
|
||||
|
||||
return {
|
||||
browser,
|
||||
command,
|
||||
entrypointsDir: fakeDir(),
|
||||
env: { browser, command, manifestVersion, mode },
|
||||
fsCache: mock<FsCache>(),
|
||||
imports: {},
|
||||
logger: mock(),
|
||||
manifest: fakeManifest(),
|
||||
manifestVersion,
|
||||
mode,
|
||||
outBaseDir: fakeDir(),
|
||||
outDir: fakeDir(),
|
||||
publicDir: fakeDir(),
|
||||
root: fakeDir(),
|
||||
runnerConfig: {
|
||||
config: {},
|
||||
},
|
||||
debug: faker.datatype.boolean(),
|
||||
srcDir: fakeDir(),
|
||||
typesDir: fakeDir(),
|
||||
vite: () => ({}),
|
||||
wxtDir: fakeDir(),
|
||||
server: mock<WxtDevServer>(),
|
||||
analysis: {
|
||||
enabled: false,
|
||||
template: 'treemap',
|
||||
},
|
||||
zip: {
|
||||
artifactTemplate: '{{name}}-{{version}}.zip',
|
||||
ignoredSources: [],
|
||||
sourcesRoot: fakeDir(),
|
||||
sourcesTemplate: '{{name}}-sources.zip',
|
||||
name: faker.person.firstName().toLowerCase(),
|
||||
},
|
||||
transformManifest: () => {},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -9,6 +9,8 @@ lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.output
|
||||
stats.html
|
||||
.wxt
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
|
||||
@@ -23,6 +23,6 @@
|
||||
"@types/react-dom": "^18.2.6",
|
||||
"@vitejs/plugin-react": "^4.0.3",
|
||||
"typescript": "^5.1.6",
|
||||
"wxt": "^0.4.0"
|
||||
"wxt": "^0.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import react from '@vitejs/plugin-react';
|
||||
|
||||
// See https://wxt.dev/config.html
|
||||
export default defineConfig({
|
||||
vite: {
|
||||
vite: () => ({
|
||||
plugins: [react()],
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -9,6 +9,8 @@ lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.output
|
||||
stats.html
|
||||
.wxt
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
|
||||
@@ -20,6 +20,6 @@
|
||||
"devDependencies": {
|
||||
"typescript": "^5.1.6",
|
||||
"vite-plugin-solid": "^2.7.0",
|
||||
"wxt": "^0.4.0"
|
||||
"wxt": "^0.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ import Solid from 'vite-plugin-solid';
|
||||
|
||||
// See https://wxt.dev/config.html
|
||||
export default defineConfig({
|
||||
vite: {
|
||||
vite: () => ({
|
||||
build: {
|
||||
target: 'esnext',
|
||||
},
|
||||
plugins: [Solid()],
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -9,6 +9,8 @@ lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.output
|
||||
stats.html
|
||||
.wxt
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
|
||||
@@ -20,6 +20,6 @@
|
||||
"svelte-check": "^3.4.6",
|
||||
"tslib": "^2.6.0",
|
||||
"typescript": "^5.1.6",
|
||||
"wxt": "^0.4.0"
|
||||
"wxt": "^0.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { svelte, vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
// See https://wxt.dev/config.html
|
||||
export default defineConfig({
|
||||
srcDir: 'src',
|
||||
vite: {
|
||||
vite: () => ({
|
||||
plugins: [
|
||||
svelte({
|
||||
// Using a svelte.config.js file causes a segmentation fault when importing the file
|
||||
@@ -12,5 +12,5 @@ export default defineConfig({
|
||||
preprocess: [vitePreprocess()],
|
||||
}),
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -9,6 +9,8 @@ lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.output
|
||||
stats.html
|
||||
.wxt
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
|
||||
@@ -16,6 +16,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.1.6",
|
||||
"wxt": "^0.4.0"
|
||||
"wxt": "^0.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.output
|
||||
stats.html
|
||||
.wxt
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
|
||||
@@ -21,6 +21,6 @@
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"typescript": "^5.1.6",
|
||||
"vue-tsc": "^1.8.4",
|
||||
"wxt": "^0.4.0"
|
||||
"wxt": "^0.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import vue from '@vitejs/plugin-vue';
|
||||
|
||||
// See https://wxt.dev/config.html
|
||||
export default defineConfig({
|
||||
vite: {
|
||||
vite: () => ({
|
||||
plugins: [vue()],
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"github": {
|
||||
"silent": true
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -33,7 +33,7 @@ export default defineWorkspace([
|
||||
name: 'e2e',
|
||||
dir: 'e2e',
|
||||
singleThread: true,
|
||||
testTimeout: 60e3,
|
||||
testTimeout: 120e3,
|
||||
},
|
||||
plugins: [testSeed()],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user