Compare commits
133 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d4983e88b | |||
| 4150e42c05 | |||
| 8d7150653a | |||
| 61e57b7b7c | |||
| 83add72312 | |||
| 3c723d2c92 | |||
| 1eb35c7207 | |||
| 725ecf7c64 | |||
| 3847092df4 | |||
| 19b11c236f | |||
| 4b2012c489 | |||
| 0591050f31 | |||
| b15dc7fc11 | |||
| 89d15babc2 | |||
| c616125689 | |||
| e97071b7d7 | |||
| 1fa049c5a7 | |||
| b48cee9715 | |||
| 24e69fe1bf | |||
| 41e154992a | |||
| 55707932a9 | |||
| 08115a40ae | |||
| 96be879918 | |||
| 1fc4ada66d | |||
| d27f299641 | |||
| 7183114370 | |||
| 7bd940450f | |||
| cf5a7d1411 | |||
| 5482b2f934 | |||
| c4a6ff928d | |||
| 455e7f3765 | |||
| 25b6ab92f5 | |||
| 50f6289ac5 | |||
| a0e1b4741e | |||
| ab672e9dbd | |||
| 7b3ea52e02 | |||
| e0929a68ba | |||
| 4c430725a5 | |||
| 7d745f6ec7 | |||
| 10091d7769 | |||
| 2e142b3038 | |||
| 361bb2dcc8 | |||
| 26fca5c0a2 | |||
| 30a61f4384 | |||
| 8e0a189d77 | |||
| cae44c9732 | |||
| fba8f0d017 | |||
| 3f260ee777 | |||
| 1f6a931b02 | |||
| 07891d028b | |||
| 1f448d1bf0 | |||
| 7722986537 | |||
| 4b73168618 | |||
| 16cebc538f | |||
| e4aaba98ab | |||
| c190d8c44c | |||
| 5801f96f79 | |||
| 92039b855e | |||
| f145e00cc9 | |||
| 87e8df90f5 | |||
| 419fab8193 | |||
| 71743b3bf3 | |||
| 8e3169eb1e | |||
| 4ec904ea3c | |||
| b284305605 | |||
| 234bd39a11 | |||
| b5acea106d | |||
| 33ed04ff6b | |||
| f51bbc0a68 | |||
| 6ee5b22e6c | |||
| b4de93d2e6 | |||
| 2570debc5c | |||
| e6a2c15d3c | |||
| dbd84c8815 | |||
| fe966b6ca8 | |||
| e2350fecc6 | |||
| b33b6631a5 | |||
| 5f15f9ccec | |||
| beafa6afc4 | |||
| 46a5036482 | |||
| eaa15e0d57 | |||
| 2c22948d2e | |||
| 3e701e22c9 | |||
| 6ee1fa17a1 | |||
| b76bf2831b | |||
| ac7cbfce1f | |||
| 06f027b889 | |||
| e996686082 | |||
| 1bb3474a91 | |||
| 79426671cf | |||
| 89b34403ee | |||
| 749bc38234 | |||
| e086374828 | |||
| 2c3721e5f4 | |||
| c2946aa1eb | |||
| 4dd6320eae | |||
| a8285fff78 | |||
| d89be2544d | |||
| f234c85357 | |||
| 93bfee0578 | |||
| 2c7922ca4c | |||
| 72ae812c42 | |||
| 2a0842bb62 | |||
| b301ee324e | |||
| 4ef3eeaca1 | |||
| e803bfecb5 | |||
| 0dcf540393 | |||
| 062e25d834 | |||
| b58a15e783 | |||
| 5f5f1d90f8 | |||
| 83e62a1d63 | |||
| b428a62a90 | |||
| 1fe4eedc9a | |||
| 39d3192904 | |||
| de30509d94 | |||
| d5ae2332ca | |||
| f8b367dd7b | |||
| 6bcb71baa0 | |||
| 54bdf54839 | |||
| 83f457f40f | |||
| f9df2d0809 | |||
| 2b079bb7f6 | |||
| 0194448af4 | |||
| b409def211 | |||
| 62bbef3487 | |||
| 87013bc616 | |||
| 4599bf2aa0 | |||
| d629acf518 | |||
| 2bde9175b7 | |||
| 8d5f4f5201 | |||
| d05f126eb3 | |||
| e164bd51fe | |||
| d66293c56e |
@@ -0,0 +1,6 @@
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: 80%
|
||||
threshold: 1%
|
||||
@@ -1,3 +1,2 @@
|
||||
* text=auto eol=lf
|
||||
pnpm-lock.yaml linguist-generated
|
||||
docs/config.md linguist-generated
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Report an issue with WXT
|
||||
title: ''
|
||||
labels: triage
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
### Describe the bug
|
||||
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
### To Reproduce
|
||||
|
||||
Upload a ZIP or share a link to a repo representing the minimal reproduction. **_If you don't upload a minimal reproduction, you bug report will be closed._**
|
||||
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
### Expected behavior
|
||||
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
### Screenshots
|
||||
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
### Environment
|
||||
|
||||
<!--- Run `npx envinfo --system --browsers --binaries --npmPackages wxt,vite` and paste the output below -->
|
||||
|
||||
```
|
||||
Paste output here
|
||||
```
|
||||
|
||||
### Additional context
|
||||
|
||||
Add any other context about the problem here, otherwise you can delete this section.
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for WXT
|
||||
title: ''
|
||||
labels: feature
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
### Feature Request
|
||||
|
||||
Please describe your feature, be clear and concise. If you have a proposal for required type or API changes, list them here.
|
||||
|
||||
#### Is your feature request related to a bug?
|
||||
|
||||
If so, add a link here. If not, write "N/A"
|
||||
|
||||
### What are the alternatives?
|
||||
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
### Additional context
|
||||
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,11 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: '/' # Location of package manifests
|
||||
schedule:
|
||||
interval: 'monthly'
|
||||
@@ -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
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
/docs/.vitepress/cache
|
||||
coverage
|
||||
dist
|
||||
e2e/project
|
||||
node_modules
|
||||
TODOs.md
|
||||
web-ext.config.js
|
||||
@@ -18,3 +17,5 @@ web-ext.config.ts
|
||||
templates/*/pnpm-lock.yaml
|
||||
templates/*/yarn.lock
|
||||
templates/*/package-lock.json
|
||||
docs/config.md
|
||||
stats.html
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
.output
|
||||
coverage
|
||||
dist
|
||||
e2e/project
|
||||
.wxt
|
||||
docs/.vitepress/cache
|
||||
pnpm-lock.yaml
|
||||
CHANGELOG.md
|
||||
.gitattributes
|
||||
.gitignore
|
||||
.prettierignore
|
||||
*.png
|
||||
*.svg
|
||||
*.txt
|
||||
_gitignore
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
singleQuote: true
|
||||
trailingComma: all
|
||||
endOfLine: lf
|
||||
|
||||
@@ -1,328 +1,538 @@
|
||||
# Changelog
|
||||
|
||||
## v0.7.0
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.6.6...v0.7.0)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Content script `cssInjectionMode` ([#141](https://github.com/wxt-dev/wxt/pull/141))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Validate transformed manifest correctly ([4b2012c](https://github.com/wxt-dev/wxt/commit/4b2012c))
|
||||
- ⚠️ Output content script CSS to `content-scripts/<name>.css` ([#140](https://github.com/wxt-dev/wxt/pull/140))
|
||||
- Reorder typescript paths to give priority to `@` and `~` over `@@` and `~~` ([#142](https://github.com/wxt-dev/wxt/pull/142))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Store user config metadata in memory ([0591050](https://github.com/wxt-dev/wxt/commit/0591050))
|
||||
|
||||
#### ⚠️ Breaking Changes
|
||||
|
||||
- ⚠️ Content script CSS used to be output to `assets/<name>.css`, but is now `content-scripts/<name>.css` to match the docs. ([#140](https://github.com/wxt-dev/wxt/pull/140))
|
||||
|
||||
## v0.6.6
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.6.5...v0.6.6)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Disable opening browser automatically during dev mode ([#136](https://github.com/wxt-dev/wxt/pull/136))
|
||||
|
||||
## v0.6.5
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.6.4...v0.6.5)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Don't crash when `<all_urls>` matches is used in dev mode ([b48cee9](https://github.com/wxt-dev/wxt/commit/b48cee9))
|
||||
- Support loading `tsx` entrypoints ([#134](https://github.com/wxt-dev/wxt/pull/134))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Add tags for SEO and socials ([96be879](https://github.com/wxt-dev/wxt/commit/96be879))
|
||||
- Add more content to the homepage ([5570793](https://github.com/wxt-dev/wxt/commit/5570793))
|
||||
- Fix DX section sizing ([41e1549](https://github.com/wxt-dev/wxt/commit/41e1549))
|
||||
- Add link to update extensions using WXT ([24e69fe](https://github.com/wxt-dev/wxt/commit/24e69fe))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Code coverage improvements ([#131](https://github.com/wxt-dev/wxt/pull/131))
|
||||
|
||||
## v0.6.4
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.6.3...v0.6.4)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- **content-scripts:** Don't throw an error when including `include` or `exclude` options on a content script ([455e7f3](https://github.com/wxt-dev/wxt/commit/455e7f3))
|
||||
- Use `execaCommand` instead of `node:child_process` ([#130](https://github.com/wxt-dev/wxt/pull/130))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **templates:** Add `.wxt` directory to gitignore ([#129](https://github.com/wxt-dev/wxt/pull/129))
|
||||
- Increase E2E test timeout ([5482b2f](https://github.com/wxt-dev/wxt/commit/5482b2f))
|
||||
|
||||
## v0.6.3
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.6.2...v0.6.3)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- **client:** Add `block` and `addEventListener` utils to `ContentScriptContext` ([#128](https://github.com/wxt-dev/wxt/pull/128))
|
||||
|
||||
## v0.6.2
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.6.1...v0.6.2)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- `--analyze` build flag ([#125](https://github.com/wxt-dev/wxt/pull/125))
|
||||
- Show spinner when building entrypoints ([#126](https://github.com/wxt-dev/wxt/pull/126))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Fix import typo ([4c43072](https://github.com/wxt-dev/wxt/commit/4c43072))
|
||||
- Update vite docs to use function ([e0929a6](https://github.com/wxt-dev/wxt/commit/e0929a6))
|
||||
|
||||
## v0.6.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.6.0...v0.6.1)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add `transformManifest` option ([#124](https://github.com/wxt-dev/wxt/pull/124))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Don't open browser during development when using WSL ([#123](https://github.com/wxt-dev/wxt/pull/123))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Load extension details from CWS ([8e0a189](https://github.com/wxt-dev/wxt/commit/8e0a189))
|
||||
|
||||
## v0.6.0
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.6...v0.6.0)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Export `ContentScriptContext` from `wxt/client` ([1f448d1](https://github.com/wxt-dev/wxt/commit/1f448d1))
|
||||
- ⚠️ Require a function for `vite` configuration ([#121](https://github.com/wxt-dev/wxt/pull/121))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Use the same mode for each build step ([1f6a931](https://github.com/wxt-dev/wxt/commit/1f6a931))
|
||||
- Disable dev logs in production ([3f260ee](https://github.com/wxt-dev/wxt/commit/3f260ee))
|
||||
|
||||
#### ⚠️ Breaking Changes
|
||||
|
||||
- ⚠️ The `vite` config option must now be a function. If you were using an object before, change it from `vite: { ... }` to `vite: () => ({ ... })`. ([#121](https://github.com/wxt-dev/wxt/pull/121))
|
||||
|
||||
## v0.5.6
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.5...v0.5.6)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add `ContentScriptContext` util for stopping invalidated content scripts ([#120](https://github.com/wxt-dev/wxt/pull/120))
|
||||
|
||||
## v0.5.5
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.4...v0.5.5)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Automatically replace vite's `process.env.NODE_ENV` output in lib mode with the mode ([92039b8](https://github.com/wxt-dev/wxt/commit/92039b8))
|
||||
|
||||
## v0.5.4
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.3...v0.5.4)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Recognize `background/index.ts` as an entrypoint ([419fab8](https://github.com/wxt-dev/wxt/commit/419fab8))
|
||||
- Don't warn about deep entrypoint subdirectories not being recognized ([87e8df9](https://github.com/wxt-dev/wxt/commit/87e8df9))
|
||||
|
||||
## v0.5.3
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.2...v0.5.3)
|
||||
|
||||
### 🩹 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)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- **types:** Don't write to files if nothing changes ([#107](https://github.com/wxt-dev/wxt/pull/107))
|
||||
- ⚠️ Change default `publicDir` to `<srcDir>/public` ([5f15f9c](https://github.com/wxt-dev/wxt/commit/5f15f9c))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Add link to examples repo ([46a5036](https://github.com/wxt-dev/wxt/commit/46a5036))
|
||||
- Fix typos ([beafa6a](https://github.com/wxt-dev/wxt/commit/beafa6a))
|
||||
- Make README pretty ([b33b663](https://github.com/wxt-dev/wxt/commit/b33b663))
|
||||
- Add migration docs ([e2350fe](https://github.com/wxt-dev/wxt/commit/e2350fe))
|
||||
- Add vite customization docs ([fe966b6](https://github.com/wxt-dev/wxt/commit/fe966b6))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Move repo to wxt-dev org ([ac7cbfc](https://github.com/wxt-dev/wxt/commit/ac7cbfc))
|
||||
- **deps-dev:** Bump prettier from 3.0.1 to 3.0.3 ([#111](https://github.com/wxt-dev/wxt/pull/111))
|
||||
- **deps-dev:** Bump tsx from 3.12.7 to 3.12.8 ([#109](https://github.com/wxt-dev/wxt/pull/109))
|
||||
- **deps-dev:** Bump @types/node from 20.5.0 to 20.5.9 ([#110](https://github.com/wxt-dev/wxt/pull/110))
|
||||
- Add entrypoints debug log ([dbd84c8](https://github.com/wxt-dev/wxt/commit/dbd84c8))
|
||||
|
||||
#### ⚠️ Breaking Changes
|
||||
|
||||
- ⚠️ Change default `publicDir` to `<srcDir>/public` ([5f15f9c](https://github.com/wxt-dev/wxt/commit/5f15f9c))
|
||||
|
||||
## v0.4.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.4.0...v0.4.1)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- **cli:** Add `wxt clean` command to delete generated files ([#106](https://github.com/wxt-dev/wxt/pull/106))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- **init:** Don't show `cd .` when initializing the current directory ([e086374](https://github.com/wxt-dev/wxt/commit/e086374))
|
||||
|
||||
## v0.4.0
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.3.2...v0.4.0)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add `--debug` flag for printing debug logs for all CLI commands ([#75](https://github.com/wxt-dev/wxt/pull/75))
|
||||
- Replace `web-ext` with `web-ext-run` ([#101](https://github.com/wxt-dev/wxt/pull/101))
|
||||
- Generate types for `browser.i18n.getMessage` ([#103](https://github.com/wxt-dev/wxt/pull/103))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Allow adding custom content scripts ([b428a62](https://github.com/wxt-dev/wxt/commit/b428a62))
|
||||
- Don't overwrite `wxt.config.ts` content scripts, append entrypoints to it ([5f5f1d9](https://github.com/wxt-dev/wxt/commit/5f5f1d9))
|
||||
- ⚠️ Use relative path aliases inside `.wxt/tsconfig.json` ([#102](https://github.com/wxt-dev/wxt/pull/102))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Add contribution guide ([#76](https://github.com/wxt-dev/wxt/pull/76))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Setup dependabot for upgrading dependencies ([d66293c](https://github.com/wxt-dev/wxt/commit/d66293c))
|
||||
- Update social preview ([e164bd5](https://github.com/wxt-dev/wxt/commit/e164bd5))
|
||||
- Setup bug and feature issue templates ([2bde917](https://github.com/wxt-dev/wxt/commit/2bde917))
|
||||
- Upgrade to prettier 3 ([#77](https://github.com/wxt-dev/wxt/pull/77))
|
||||
- **deps-dev:** Bump vitest from 0.32.4 to 0.34.1 ([#81](https://github.com/wxt-dev/wxt/pull/81))
|
||||
- **deps-dev:** Bump ora from 6.3.1 to 7.0.1 ([#79](https://github.com/wxt-dev/wxt/pull/79))
|
||||
- **deps-dev:** Bump @types/node from 20.4.5 to 20.5.0 ([#78](https://github.com/wxt-dev/wxt/pull/78))
|
||||
- **deps-dev:** Bump tsup from 7.1.0 to 7.2.0 ([#80](https://github.com/wxt-dev/wxt/pull/80))
|
||||
- **deps-dev:** Bump @vitest/coverage-v8 from 0.32.4 to 0.34.1 ([#84](https://github.com/wxt-dev/wxt/pull/84))
|
||||
- **deps-dev:** Bump vitepress from 1.0.0-beta.5 to 1.0.0-rc.4 ([#85](https://github.com/wxt-dev/wxt/pull/85))
|
||||
- **deps-dev:** Bump vitest-mock-extended from 1.1.4 to 1.2.0 ([#87](https://github.com/wxt-dev/wxt/pull/87))
|
||||
- **deps-dev:** Bump lint-staged from 13.3.0 to 14.0.0 ([#89](https://github.com/wxt-dev/wxt/pull/89))
|
||||
- Fix remote code E2E test ([83e62a1](https://github.com/wxt-dev/wxt/commit/83e62a1))
|
||||
- Fix failing demo build ([b58a15e](https://github.com/wxt-dev/wxt/commit/b58a15e))
|
||||
- **deps-dev:** Bump vitest-mock-extended from 1.2.0 to 1.2.1 ([#97](https://github.com/wxt-dev/wxt/pull/97))
|
||||
- **deps-dev:** Bump lint-staged from 14.0.0 to 14.0.1 ([#100](https://github.com/wxt-dev/wxt/pull/100))
|
||||
- **deps-dev:** Bump vitest from 0.34.1 to 0.34.3 ([#99](https://github.com/wxt-dev/wxt/pull/99))
|
||||
- Increase E2E test timeout because GitHub Actions Window runner is slow ([2a0842b](https://github.com/wxt-dev/wxt/commit/2a0842b))
|
||||
- **deps-dev:** Bump vitepress from 1.0.0-rc.4 to 1.0.0-rc.10 ([#96](https://github.com/wxt-dev/wxt/pull/96))
|
||||
- Fix test watcher restarting indefinetly ([2c7922c](https://github.com/wxt-dev/wxt/commit/2c7922c))
|
||||
- Remove explict icon config from templates ([93bfee0](https://github.com/wxt-dev/wxt/commit/93bfee0))
|
||||
- Use import aliases in Vue template ([#104](https://github.com/wxt-dev/wxt/pull/104))
|
||||
|
||||
#### ⚠️ Breaking Changes
|
||||
|
||||
- ⚠️ Use relative path aliases inside `.wxt/tsconfig.json` ([#102](https://github.com/wxt-dev/wxt/pull/102))
|
||||
|
||||
## v0.3.2
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.3.1...v0.3.2)
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.3.1...v0.3.2)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Discover icons from the public directory ([#72](https://github.com/aklinker1/wxt/pull/72))
|
||||
- Don't allow auto-importing from subdirectories ([d54d611](https://github.com/aklinker1/wxt/commit/d54d611))
|
||||
- Discover icons from the public directory ([#72](https://github.com/wxt-dev/wxt/pull/72))
|
||||
- Don't allow auto-importing from subdirectories ([d54d611](https://github.com/wxt-dev/wxt/commit/d54d611))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Document the `url:` import prefix for remote code ([323045a](https://github.com/aklinker1/wxt/commit/323045a))
|
||||
- Fix typos ([97f0938](https://github.com/aklinker1/wxt/commit/97f0938))
|
||||
- Fix capitalization ([39467d1](https://github.com/aklinker1/wxt/commit/39467d1))
|
||||
- Generate markdown for config reference ([#74](https://github.com/aklinker1/wxt/pull/74))
|
||||
- Document the `url:` import prefix for remote code ([323045a](https://github.com/wxt-dev/wxt/commit/323045a))
|
||||
- Fix typos ([97f0938](https://github.com/wxt-dev/wxt/commit/97f0938))
|
||||
- Fix capitalization ([39467d1](https://github.com/wxt-dev/wxt/commit/39467d1))
|
||||
- Generate markdown for config reference ([#74](https://github.com/wxt-dev/wxt/pull/74))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Upgrade dependencies ([798f02f](https://github.com/aklinker1/wxt/commit/798f02f))
|
||||
- Upgrade vite (`v4.3` → `v4.4`) ([547c185](https://github.com/aklinker1/wxt/commit/547c185))
|
||||
- Update templates to work with CSS entrypoints ([7f15305](https://github.com/aklinker1/wxt/commit/7f15305))
|
||||
- Improve file list output in CI ([#73](https://github.com/aklinker1/wxt/pull/73))
|
||||
- Upgrade dependencies ([798f02f](https://github.com/wxt-dev/wxt/commit/798f02f))
|
||||
- Upgrade vite (`v4.3` → `v4.4`) ([547c185](https://github.com/wxt-dev/wxt/commit/547c185))
|
||||
- Update templates to work with CSS entrypoints ([7f15305](https://github.com/wxt-dev/wxt/commit/7f15305))
|
||||
- Improve file list output in CI ([#73](https://github.com/wxt-dev/wxt/pull/73))
|
||||
|
||||
### 🤖 CI
|
||||
|
||||
- Validate templates against `main` ([#66](https://github.com/aklinker1/wxt/pull/66))
|
||||
- List vite version when validating project templates ([ef140dc](https://github.com/aklinker1/wxt/commit/ef140dc))
|
||||
- Validate templates using tarball to avoid version conflicts within the `wxt/node_modules` directory ([edfa075](https://github.com/aklinker1/wxt/commit/edfa075))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron <aaronklinker1@gmail.com>
|
||||
- Aaron Klinker <aaronklinker1@gmail.com>
|
||||
- Validate templates against `main` ([#66](https://github.com/wxt-dev/wxt/pull/66))
|
||||
- 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))
|
||||
|
||||
## v0.3.1
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.3.0...v0.3.1)
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.3.0...v0.3.1)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- CSS entrypoints ([#61](https://github.com/aklinker1/wxt/pull/61))
|
||||
- `init` command for bootstrapping new projects ([#65](https://github.com/aklinker1/wxt/pull/65))
|
||||
- CSS entrypoints ([#61](https://github.com/wxt-dev/wxt/pull/61))
|
||||
- `init` command for bootstrapping new projects ([#65](https://github.com/wxt-dev/wxt/pull/65))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Add zip command to installation scripts ([94a1097](https://github.com/aklinker1/wxt/commit/94a1097))
|
||||
- Add output paths to entrypoint docs ([3a336eb](https://github.com/aklinker1/wxt/commit/3a336eb))
|
||||
- Update installation docs ([aea866c](https://github.com/aklinker1/wxt/commit/aea866c))
|
||||
- Add publishing docs ([4184b05](https://github.com/aklinker1/wxt/commit/4184b05))
|
||||
- Add a section for extensions using WXT ([709b61a](https://github.com/aklinker1/wxt/commit/709b61a))
|
||||
- Add a comparison page to compare and contrast against Plasmo ([38d4f9c](https://github.com/aklinker1/wxt/commit/38d4f9c))
|
||||
- Add zip command to installation scripts ([94a1097](https://github.com/wxt-dev/wxt/commit/94a1097))
|
||||
- Add output paths to entrypoint docs ([3a336eb](https://github.com/wxt-dev/wxt/commit/3a336eb))
|
||||
- Update installation docs ([aea866c](https://github.com/wxt-dev/wxt/commit/aea866c))
|
||||
- Add publishing docs ([4184b05](https://github.com/wxt-dev/wxt/commit/4184b05))
|
||||
- Add a section for extensions using WXT ([709b61a](https://github.com/wxt-dev/wxt/commit/709b61a))
|
||||
- Add a comparison page to compare and contrast against Plasmo ([38d4f9c](https://github.com/wxt-dev/wxt/commit/38d4f9c))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Update template projects to v0.3 ([#56](https://github.com/aklinker1/wxt/pull/56))
|
||||
- Branding and logo ([#60](https://github.com/aklinker1/wxt/pull/60))
|
||||
- Simplify binary setup ([#62](https://github.com/aklinker1/wxt/pull/62))
|
||||
- Add Solid template ([#63](https://github.com/aklinker1/wxt/pull/63))
|
||||
- Increate E2E test timeout to fix flakey test ([dfe424f](https://github.com/aklinker1/wxt/commit/dfe424f))
|
||||
- Update template projects to v0.3 ([#56](https://github.com/wxt-dev/wxt/pull/56))
|
||||
- Branding and logo ([#60](https://github.com/wxt-dev/wxt/pull/60))
|
||||
- Simplify binary setup ([#62](https://github.com/wxt-dev/wxt/pull/62))
|
||||
- Add Solid template ([#63](https://github.com/wxt-dev/wxt/pull/63))
|
||||
- Increate E2E test timeout to fix flakey test ([dfe424f](https://github.com/wxt-dev/wxt/commit/dfe424f))
|
||||
|
||||
### 🤖 CI
|
||||
|
||||
- Speed up demo validation ([3a9fd39](https://github.com/aklinker1/wxt/commit/3a9fd39))
|
||||
- Fix flakey failure when validating templates ([25677ba](https://github.com/aklinker1/wxt/commit/25677ba))
|
||||
- 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
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.2.5...v0.3.0)
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.2.5...v0.3.0)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- ⚠️ Add type safety to `browser.runtime.getURL` ([58a84ec](https://github.com/aklinker1/wxt/commit/58a84ec))
|
||||
- ⚠️ Change default `publicDir` to `<rootDir>/public` ([19c0948](https://github.com/aklinker1/wxt/commit/19c0948))
|
||||
- Windows support ([#50](https://github.com/aklinker1/wxt/pull/50))
|
||||
- ⚠️ 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))
|
||||
- Windows support ([#50](https://github.com/wxt-dev/wxt/pull/50))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Add `WebWorker` lib to generated tsconfig ([2c70246](https://github.com/aklinker1/wxt/commit/2c70246))
|
||||
- Add `WebWorker` lib to generated tsconfig ([2c70246](https://github.com/wxt-dev/wxt/commit/2c70246))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Update entrypoint directory links ([0aebb67](https://github.com/aklinker1/wxt/commit/0aebb67))
|
||||
- Update entrypoint directory links ([0aebb67](https://github.com/wxt-dev/wxt/commit/0aebb67))
|
||||
|
||||
### 🌊 Types
|
||||
|
||||
- Allow any string for the `__BROWSER__` global ([6092235](https://github.com/aklinker1/wxt/commit/6092235))
|
||||
- Allow any string for the `__BROWSER__` global ([6092235](https://github.com/wxt-dev/wxt/commit/6092235))
|
||||
|
||||
### 🤖 CI
|
||||
|
||||
- Improve checks against `demo/` extension ([9cc464f](https://github.com/aklinker1/wxt/commit/9cc464f))
|
||||
- Improve checks against `demo/` extension ([9cc464f](https://github.com/wxt-dev/wxt/commit/9cc464f))
|
||||
|
||||
#### ⚠️ Breaking Changes
|
||||
|
||||
- ⚠️ Add type safety to `browser.runtime.getURL` ([58a84ec](https://github.com/aklinker1/wxt/commit/58a84ec))
|
||||
- ⚠️ Change default `publicDir` to `<rootDir>/public` ([19c0948](https://github.com/aklinker1/wxt/commit/19c0948))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
- ⚠️ 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))
|
||||
|
||||
## v0.2.5
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.2.4...v0.2.5)
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.2.4...v0.2.5)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Auto-import from subdirectories ([547fee0](https://github.com/aklinker1/wxt/commit/547fee0))
|
||||
- Include background script in dev mode if user doesn't define one ([ca20a21](https://github.com/aklinker1/wxt/commit/ca20a21))
|
||||
- Auto-import from subdirectories ([547fee0](https://github.com/wxt-dev/wxt/commit/547fee0))
|
||||
- Include background script in dev mode if user doesn't define one ([ca20a21](https://github.com/wxt-dev/wxt/commit/ca20a21))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Don't crash when generating types in dev mode ([d8c1903](https://github.com/aklinker1/wxt/commit/d8c1903))
|
||||
- Properly load entrypoints that reference `import.meta` ([54b18cc](https://github.com/aklinker1/wxt/commit/54b18cc))
|
||||
- Don't crash when generating types in dev mode ([d8c1903](https://github.com/wxt-dev/wxt/commit/d8c1903))
|
||||
- Properly load entrypoints that reference `import.meta` ([54b18cc](https://github.com/wxt-dev/wxt/commit/54b18cc))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Update templates to wxt@0.2 ([9d00eb2](https://github.com/aklinker1/wxt/commit/9d00eb2))
|
||||
- Update templates to wxt@0.2 ([9d00eb2](https://github.com/wxt-dev/wxt/commit/9d00eb2))
|
||||
|
||||
### 🤖 CI
|
||||
|
||||
- Validate project templates ([9ac756f](https://github.com/aklinker1/wxt/commit/9ac756f))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
- Validate project templates ([9ac756f](https://github.com/wxt-dev/wxt/commit/9ac756f))
|
||||
|
||||
## v0.2.4
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.2.3...v0.2.4)
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.2.3...v0.2.4)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add `wxt zip` command ([#47](https://github.com/aklinker1/wxt/pull/47))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
- Add `wxt zip` command ([#47](https://github.com/wxt-dev/wxt/pull/47))
|
||||
|
||||
## v0.2.3
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.2.2...v0.2.3)
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.2.2...v0.2.3)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Correctly lookup open port ([#45](https://github.com/aklinker1/wxt/pull/45))
|
||||
- Read boolean maniest options from meta tags correctly ([495c5c8](https://github.com/aklinker1/wxt/commit/495c5c8))
|
||||
- Some fields cannot be overridden from `config.manifest` ([#46](https://github.com/aklinker1/wxt/pull/46))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
- Correctly lookup open port ([#45](https://github.com/wxt-dev/wxt/pull/45))
|
||||
- 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))
|
||||
|
||||
## v0.2.2
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.2.1...v0.2.2)
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.2.1...v0.2.2)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Register content scripts correctly in dev mode ([2fb5a54](https://github.com/aklinker1/wxt/commit/2fb5a54))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
- Register content scripts correctly in dev mode ([2fb5a54](https://github.com/wxt-dev/wxt/commit/2fb5a54))
|
||||
|
||||
## v0.2.1
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.2.0...v0.2.1)
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.2.0...v0.2.1)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Support all content script options ([6f5bf89](https://github.com/aklinker1/wxt/commit/6f5bf89))
|
||||
- Support all content script options ([6f5bf89](https://github.com/wxt-dev/wxt/commit/6f5bf89))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Remove HMR log ([90fa6bf](https://github.com/aklinker1/wxt/commit/90fa6bf))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
- Remove HMR log ([90fa6bf](https://github.com/wxt-dev/wxt/commit/90fa6bf))
|
||||
|
||||
## v0.2.0
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.1.6...v0.2.0)
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.1.6...v0.2.0)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- ⚠️ Rename `defineBackgroundScript` to `defineBackground` ([5b48ae9](https://github.com/aklinker1/wxt/commit/5b48ae9))
|
||||
- Recongize unnamed content scripts (`content.ts` and `content/index.ts`) ([3db5cec](https://github.com/aklinker1/wxt/commit/3db5cec))
|
||||
- ⚠️ Rename `defineBackgroundScript` to `defineBackground` ([5b48ae9](https://github.com/wxt-dev/wxt/commit/5b48ae9))
|
||||
- Recongize unnamed content scripts (`content.ts` and `content/index.ts`) ([3db5cec](https://github.com/wxt-dev/wxt/commit/3db5cec))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Update templates ([f28a29e](https://github.com/aklinker1/wxt/commit/f28a29e))
|
||||
- Add docs for each type of entrypoint ([77cbfc1](https://github.com/aklinker1/wxt/commit/77cbfc1))
|
||||
- Add inline JSDoc for public types ([375a2a6](https://github.com/aklinker1/wxt/commit/375a2a6))
|
||||
- Update templates ([f28a29e](https://github.com/wxt-dev/wxt/commit/f28a29e))
|
||||
- Add docs for each type of entrypoint ([77cbfc1](https://github.com/wxt-dev/wxt/commit/77cbfc1))
|
||||
- Add inline JSDoc for public types ([375a2a6](https://github.com/wxt-dev/wxt/commit/375a2a6))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Run `wxt prepare` on `postinstall` ([c1ea9ba](https://github.com/aklinker1/wxt/commit/c1ea9ba))
|
||||
- Don't format lockfile ([5c7e041](https://github.com/aklinker1/wxt/commit/5c7e041))
|
||||
- Run `wxt prepare` on `postinstall` ([c1ea9ba](https://github.com/wxt-dev/wxt/commit/c1ea9ba))
|
||||
- Don't format lockfile ([5c7e041](https://github.com/wxt-dev/wxt/commit/5c7e041))
|
||||
|
||||
#### ⚠️ Breaking Changes
|
||||
|
||||
- ⚠️ Rename `defineBackgroundScript` to `defineBackground` ([5b48ae9](https://github.com/aklinker1/wxt/commit/5b48ae9))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
- ⚠️ Rename `defineBackgroundScript` to `defineBackground` ([5b48ae9](https://github.com/wxt-dev/wxt/commit/5b48ae9))
|
||||
|
||||
## v0.1.6
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.1.5...v0.1.6)
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.1.5...v0.1.6)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Resolve tsconfig paths in vite ([ea92a27](https://github.com/aklinker1/wxt/commit/ea92a27))
|
||||
- Add logs when a hot reload happens ([977246f](https://github.com/aklinker1/wxt/commit/977246f))
|
||||
- Resolve tsconfig paths in vite ([ea92a27](https://github.com/wxt-dev/wxt/commit/ea92a27))
|
||||
- Add logs when a hot reload happens ([977246f](https://github.com/wxt-dev/wxt/commit/977246f))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- React and Vue starter templates ([#33](https://github.com/aklinker1/wxt/pull/33))
|
||||
- Svelte template ([#34](https://github.com/aklinker1/wxt/pull/34))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
- 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))
|
||||
|
||||
## v0.1.5
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.1.4...v0.1.5)
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.1.4...v0.1.5)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Include `vite/client` types ([371be99](https://github.com/aklinker1/wxt/commit/371be99))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
- Include `vite/client` types ([371be99](https://github.com/wxt-dev/wxt/commit/371be99))
|
||||
|
||||
## v0.1.4
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.1.3...v0.1.4)
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.1.3...v0.1.4)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Fix regression where manifest was not listed first in build summary ([fa2b656](https://github.com/aklinker1/wxt/commit/fa2b656))
|
||||
- Fix config hook implementations for vite plugins ([49965e7](https://github.com/aklinker1/wxt/commit/49965e7))
|
||||
- Fix regression where manifest was not listed first in build summary ([fa2b656](https://github.com/wxt-dev/wxt/commit/fa2b656))
|
||||
- Fix config hook implementations for vite plugins ([49965e7](https://github.com/wxt-dev/wxt/commit/49965e7))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Update CLI screenshot ([0a26673](https://github.com/aklinker1/wxt/commit/0a26673))
|
||||
- Update CLI screenshot ([0a26673](https://github.com/wxt-dev/wxt/commit/0a26673))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Update prettier ignore ([68611ae](https://github.com/aklinker1/wxt/commit/68611ae))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
- Update prettier ignore ([68611ae](https://github.com/wxt-dev/wxt/commit/68611ae))
|
||||
|
||||
## v0.1.3
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.1.2...v0.1.3)
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.1.2...v0.1.3)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add tsconfig path aliases ([#32](https://github.com/aklinker1/wxt/pull/32))
|
||||
- Add tsconfig path aliases ([#32](https://github.com/wxt-dev/wxt/pull/32))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Merge `manifest` option from both inline and user config ([05ca998](https://github.com/aklinker1/wxt/commit/05ca998))
|
||||
- Cleanup build summary with sourcemaps ([ac0b28e](https://github.com/aklinker1/wxt/commit/ac0b28e))
|
||||
- Merge `manifest` option from both inline and user config ([05ca998](https://github.com/wxt-dev/wxt/commit/05ca998))
|
||||
- Cleanup build summary with sourcemaps ([ac0b28e](https://github.com/wxt-dev/wxt/commit/ac0b28e))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Create documentation site ([#31](https://github.com/aklinker1/wxt/pull/31))
|
||||
- Create documentation site ([#31](https://github.com/wxt-dev/wxt/pull/31))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Upgrade to pnpm 8 ([0ce7c9d](https://github.com/aklinker1/wxt/commit/0ce7c9d))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
- Upgrade to pnpm 8 ([0ce7c9d](https://github.com/wxt-dev/wxt/commit/0ce7c9d))
|
||||
|
||||
## v0.1.2
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.1.1...v0.1.2)
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.1.1...v0.1.2)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Accept a function for `config.manifest` ([ee49837](https://github.com/aklinker1/wxt/commit/ee49837))
|
||||
- Accept a function for `config.manifest` ([ee49837](https://github.com/wxt-dev/wxt/commit/ee49837))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Add missing types for `webextension-polyfill` and the `manifest` option ([636aa48](https://github.com/aklinker1/wxt/commit/636aa48))
|
||||
- Only add imports to JS files ([b29c3c6](https://github.com/aklinker1/wxt/commit/b29c3c6))
|
||||
- Generate valid type for `EntrypointPath` when there are no entrypoints ([6e7184d](https://github.com/aklinker1/wxt/commit/6e7184d))
|
||||
- Add missing types for `webextension-polyfill` and the `manifest` option ([636aa48](https://github.com/wxt-dev/wxt/commit/636aa48))
|
||||
- Only add imports to JS files ([b29c3c6](https://github.com/wxt-dev/wxt/commit/b29c3c6))
|
||||
- Generate valid type for `EntrypointPath` when there are no entrypoints ([6e7184d](https://github.com/wxt-dev/wxt/commit/6e7184d))
|
||||
|
||||
### 🌊 Types
|
||||
|
||||
- Change `config.vite` to `UserConfig` ([ef6001e](https://github.com/aklinker1/wxt/commit/ef6001e))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
- Change `config.vite` to `UserConfig` ([ef6001e](https://github.com/wxt-dev/wxt/commit/ef6001e))
|
||||
|
||||
## v0.1.1
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.1.0...v0.1.1)
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.1.0...v0.1.1)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Allow dashes in entrypoint names ([2e51e73](https://github.com/aklinker1/wxt/commit/2e51e73))
|
||||
- Unable to read entrypoint options ([#28](https://github.com/aklinker1/wxt/pull/28))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
- 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))
|
||||
|
||||
## v0.1.0
|
||||
|
||||
@@ -333,128 +543,116 @@ Initial release of WXT. Full support for production builds and initial toolkit f
|
||||
- Reload HTML pages when saving them directly
|
||||
- Re-register and reload tabs when content scripts change
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.0.2...v0.1.0)
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.0.2...v0.1.0)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Content scripts reloading ([#25](https://github.com/aklinker1/wxt/pull/25))
|
||||
- Content scripts reloading ([#25](https://github.com/wxt-dev/wxt/pull/25))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Update feature list ([0255028](https://github.com/aklinker1/wxt/commit/0255028))
|
||||
- Update feature list ([0255028](https://github.com/wxt-dev/wxt/commit/0255028))
|
||||
|
||||
### 🤖 CI
|
||||
|
||||
- Create github release ([b7c078f](https://github.com/aklinker1/wxt/commit/b7c078f))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
- Create github release ([b7c078f](https://github.com/wxt-dev/wxt/commit/b7c078f))
|
||||
|
||||
## v0.0.2
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.0.1...v0.0.2)
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.0.1...v0.0.2)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Reload extension when source code is changed ([#17](https://github.com/aklinker1/wxt/pull/17))
|
||||
- Setup background script web socket/reload ([#22](https://github.com/aklinker1/wxt/pull/22))
|
||||
- Reload HTML files individually ([#23](https://github.com/aklinker1/wxt/pull/23))
|
||||
- Reload extension when source code is changed ([#17](https://github.com/wxt-dev/wxt/pull/17))
|
||||
- Setup background script web socket/reload ([#22](https://github.com/wxt-dev/wxt/pull/22))
|
||||
- Reload HTML files individually ([#23](https://github.com/wxt-dev/wxt/pull/23))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Output chunks to a chunks directory ([2dd7a99](https://github.com/aklinker1/wxt/commit/2dd7a99))
|
||||
- Remove hash from content script css outputs ([#20](https://github.com/aklinker1/wxt/pull/20))
|
||||
- Overwrite files with the same name when renaming entrypoints in dev mode ([37986bf](https://github.com/aklinker1/wxt/commit/37986bf))
|
||||
- Separate template builds to prevent sharing chunks ([7f3a1e8](https://github.com/aklinker1/wxt/commit/7f3a1e8))
|
||||
- Show Vite warnings and errors ([c51f0e0](https://github.com/aklinker1/wxt/commit/c51f0e0))
|
||||
- Output chunks to a chunks directory ([2dd7a99](https://github.com/wxt-dev/wxt/commit/2dd7a99))
|
||||
- Remove hash from content script css outputs ([#20](https://github.com/wxt-dev/wxt/pull/20))
|
||||
- Overwrite files with the same name when renaming entrypoints in dev mode ([37986bf](https://github.com/wxt-dev/wxt/commit/37986bf))
|
||||
- Separate template builds to prevent sharing chunks ([7f3a1e8](https://github.com/wxt-dev/wxt/commit/7f3a1e8))
|
||||
- Show Vite warnings and errors ([c51f0e0](https://github.com/wxt-dev/wxt/commit/c51f0e0))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Add milestone progress badge to README ([684197d](https://github.com/aklinker1/wxt/commit/684197d))
|
||||
- Fix milestone link in README ([e14f81d](https://github.com/aklinker1/wxt/commit/e14f81d))
|
||||
- Add milestone progress badge to README ([684197d](https://github.com/wxt-dev/wxt/commit/684197d))
|
||||
- Fix milestone link in README ([e14f81d](https://github.com/wxt-dev/wxt/commit/e14f81d))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Refactor build output type ([#19](https://github.com/aklinker1/wxt/pull/19))
|
||||
- Refactor build outputs to support transpiled templates ([a78aada](https://github.com/aklinker1/wxt/commit/a78aada))
|
||||
- Rename `templates` to `virtual-modules` ([#24](https://github.com/aklinker1/wxt/pull/24))
|
||||
- Update cli screenshot ([54eb118](https://github.com/aklinker1/wxt/commit/54eb118))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
- Refactor build output type ([#19](https://github.com/wxt-dev/wxt/pull/19))
|
||||
- Refactor build outputs to support transpiled templates ([a78aada](https://github.com/wxt-dev/wxt/commit/a78aada))
|
||||
- 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))
|
||||
|
||||
## v0.0.1
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.0.0...v0.0.1)
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.0.0...v0.0.1)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add logger to config ([232ff7a](https://github.com/aklinker1/wxt/commit/232ff7a))
|
||||
- Export and bootstrap the `/client` package ([5b07c95](https://github.com/aklinker1/wxt/commit/5b07c95))
|
||||
- Resolve entrypoints based on filesystem ([a63f061](https://github.com/aklinker1/wxt/commit/a63f061))
|
||||
- Separate output directories for each browser/manifest version ([f09ffbb](https://github.com/aklinker1/wxt/commit/f09ffbb))
|
||||
- Build entrypoints and output `manfiest.json` ([1e7c738](https://github.com/aklinker1/wxt/commit/1e7c738))
|
||||
- Automatically add CSS files to content scripts ([047ce04](https://github.com/aklinker1/wxt/commit/047ce04))
|
||||
- Download and bundle remote URL imports ([523c7df](https://github.com/aklinker1/wxt/commit/523c7df))
|
||||
- Generate type declarations and config for project types and auto-imports ([21debad](https://github.com/aklinker1/wxt/commit/21debad))
|
||||
- Good looking console output ([e2cc995](https://github.com/aklinker1/wxt/commit/e2cc995))
|
||||
- Dev server working and a valid extension is built ([505e419](https://github.com/aklinker1/wxt/commit/505e419))
|
||||
- Virtualized content script entrypoint ([ca29537](https://github.com/aklinker1/wxt/commit/ca29537))
|
||||
- Provide custom, typed globals defined by Vite ([8c59a1c](https://github.com/aklinker1/wxt/commit/8c59a1c))
|
||||
- Copy public directory to outputs ([1a25f2b](https://github.com/aklinker1/wxt/commit/1a25f2b))
|
||||
- Support browser and chrome styles for mv2 popups ([7945c94](https://github.com/aklinker1/wxt/commit/7945c94))
|
||||
- Support browser and chrome styles for mv2 popups ([7abb577](https://github.com/aklinker1/wxt/commit/7abb577))
|
||||
- Support more CLI flags for `build` and `dev` ([#9](https://github.com/aklinker1/wxt/pull/9))
|
||||
- Add more supported browser types ([f114c5b](https://github.com/aklinker1/wxt/commit/f114c5b))
|
||||
- Open browser when starting dev server ([#11](https://github.com/aklinker1/wxt/pull/11))
|
||||
- Add logger to config ([232ff7a](https://github.com/wxt-dev/wxt/commit/232ff7a))
|
||||
- Export and bootstrap the `/client` package ([5b07c95](https://github.com/wxt-dev/wxt/commit/5b07c95))
|
||||
- Resolve entrypoints based on filesystem ([a63f061](https://github.com/wxt-dev/wxt/commit/a63f061))
|
||||
- Separate output directories for each browser/manifest version ([f09ffbb](https://github.com/wxt-dev/wxt/commit/f09ffbb))
|
||||
- Build entrypoints and output `manfiest.json` ([1e7c738](https://github.com/wxt-dev/wxt/commit/1e7c738))
|
||||
- Automatically add CSS files to content scripts ([047ce04](https://github.com/wxt-dev/wxt/commit/047ce04))
|
||||
- Download and bundle remote URL imports ([523c7df](https://github.com/wxt-dev/wxt/commit/523c7df))
|
||||
- Generate type declarations and config for project types and auto-imports ([21debad](https://github.com/wxt-dev/wxt/commit/21debad))
|
||||
- Good looking console output ([e2cc995](https://github.com/wxt-dev/wxt/commit/e2cc995))
|
||||
- Dev server working and a valid extension is built ([505e419](https://github.com/wxt-dev/wxt/commit/505e419))
|
||||
- Virtualized content script entrypoint ([ca29537](https://github.com/wxt-dev/wxt/commit/ca29537))
|
||||
- Provide custom, typed globals defined by Vite ([8c59a1c](https://github.com/wxt-dev/wxt/commit/8c59a1c))
|
||||
- Copy public directory to outputs ([1a25f2b](https://github.com/wxt-dev/wxt/commit/1a25f2b))
|
||||
- Support browser and chrome styles for mv2 popups ([7945c94](https://github.com/wxt-dev/wxt/commit/7945c94))
|
||||
- Support browser and chrome styles for mv2 popups ([7abb577](https://github.com/wxt-dev/wxt/commit/7abb577))
|
||||
- Support more CLI flags for `build` and `dev` ([#9](https://github.com/wxt-dev/wxt/pull/9))
|
||||
- Add more supported browser types ([f114c5b](https://github.com/wxt-dev/wxt/commit/f114c5b))
|
||||
- Open browser when starting dev server ([#11](https://github.com/wxt-dev/wxt/pull/11))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Support `srcDir` config ([739d19f](https://github.com/aklinker1/wxt/commit/739d19f))
|
||||
- Root path customization now works ([4faa3b3](https://github.com/aklinker1/wxt/commit/4faa3b3))
|
||||
- Print durations as ms/s based on total time ([3e37de9](https://github.com/aklinker1/wxt/commit/3e37de9))
|
||||
- Don't print error twice when background crashes ([407627c](https://github.com/aklinker1/wxt/commit/407627c))
|
||||
- Load package.json from root not cwd ([3ca16ee](https://github.com/aklinker1/wxt/commit/3ca16ee))
|
||||
- Only allow a single entrypoint with a given name ([8eb4e86](https://github.com/aklinker1/wxt/commit/8eb4e86))
|
||||
- Respect the mv2 popup type ([0f37ceb](https://github.com/aklinker1/wxt/commit/0f37ceb))
|
||||
- Respect background type and persistent manifest options ([573ef80](https://github.com/aklinker1/wxt/commit/573ef80))
|
||||
- Make content script array orders consistent ([f380378](https://github.com/aklinker1/wxt/commit/f380378))
|
||||
- Firefox manifest warnings in dev mode ([50bb845](https://github.com/aklinker1/wxt/commit/50bb845))
|
||||
- Support `srcDir` config ([739d19f](https://github.com/wxt-dev/wxt/commit/739d19f))
|
||||
- Root path customization now works ([4faa3b3](https://github.com/wxt-dev/wxt/commit/4faa3b3))
|
||||
- Print durations as ms/s based on total time ([3e37de9](https://github.com/wxt-dev/wxt/commit/3e37de9))
|
||||
- Don't print error twice when background crashes ([407627c](https://github.com/wxt-dev/wxt/commit/407627c))
|
||||
- Load package.json from root not cwd ([3ca16ee](https://github.com/wxt-dev/wxt/commit/3ca16ee))
|
||||
- Only allow a single entrypoint with a given name ([8eb4e86](https://github.com/wxt-dev/wxt/commit/8eb4e86))
|
||||
- Respect the mv2 popup type ([0f37ceb](https://github.com/wxt-dev/wxt/commit/0f37ceb))
|
||||
- Respect background type and persistent manifest options ([573ef80](https://github.com/wxt-dev/wxt/commit/573ef80))
|
||||
- Make content script array orders consistent ([f380378](https://github.com/wxt-dev/wxt/commit/f380378))
|
||||
- Firefox manifest warnings in dev mode ([50bb845](https://github.com/wxt-dev/wxt/commit/50bb845))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Update README ([785ea54](https://github.com/aklinker1/wxt/commit/785ea54))
|
||||
- Update README ([99ccadb](https://github.com/aklinker1/wxt/commit/99ccadb))
|
||||
- Update description ([07a262e](https://github.com/aklinker1/wxt/commit/07a262e))
|
||||
- Update README ([58a0ef4](https://github.com/aklinker1/wxt/commit/58a0ef4))
|
||||
- Update README ([23ed6f7](https://github.com/aklinker1/wxt/commit/23ed6f7))
|
||||
- Add initial release milestone link to README ([b400e54](https://github.com/aklinker1/wxt/commit/b400e54))
|
||||
- Fix typo in README ([5590c9d](https://github.com/aklinker1/wxt/commit/5590c9d))
|
||||
- Update README ([785ea54](https://github.com/wxt-dev/wxt/commit/785ea54))
|
||||
- Update README ([99ccadb](https://github.com/wxt-dev/wxt/commit/99ccadb))
|
||||
- Update description ([07a262e](https://github.com/wxt-dev/wxt/commit/07a262e))
|
||||
- Update README ([58a0ef4](https://github.com/wxt-dev/wxt/commit/58a0ef4))
|
||||
- Update README ([23ed6f7](https://github.com/wxt-dev/wxt/commit/23ed6f7))
|
||||
- Add initial release milestone link to README ([b400e54](https://github.com/wxt-dev/wxt/commit/b400e54))
|
||||
- Fix typo in README ([5590c9d](https://github.com/wxt-dev/wxt/commit/5590c9d))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Refactor cli files into their own directory ([e6c0d84](https://github.com/aklinker1/wxt/commit/e6c0d84))
|
||||
- Simplify `BuildOutput` type ([1f6c4a0](https://github.com/aklinker1/wxt/commit/1f6c4a0))
|
||||
- Move `.exvite` directory into `srcDir` instead of `root` ([53fb805](https://github.com/aklinker1/wxt/commit/53fb805))
|
||||
- Refactor CLI commands ([b8952b6](https://github.com/aklinker1/wxt/commit/b8952b6))
|
||||
- Improve build summary sorting ([ec57e8c](https://github.com/aklinker1/wxt/commit/ec57e8c))
|
||||
- Remove comments ([e3e9c0d](https://github.com/aklinker1/wxt/commit/e3e9c0d))
|
||||
- Refactor internal config creation ([7c634f4](https://github.com/aklinker1/wxt/commit/7c634f4))
|
||||
- Check virtual entrypoints feature in README ([70208f4](https://github.com/aklinker1/wxt/commit/70208f4))
|
||||
- Add E2E tests and convert to vitest workspace ([5813302](https://github.com/aklinker1/wxt/commit/5813302))
|
||||
- Rename package to wxt ([51a1072](https://github.com/aklinker1/wxt/commit/51a1072))
|
||||
- Fix header log's timestamp ([8ca5657](https://github.com/aklinker1/wxt/commit/8ca5657))
|
||||
- Fix demo global usage ([1ecfedd](https://github.com/aklinker1/wxt/commit/1ecfedd))
|
||||
- Refactor folder structure ([9ab3953](https://github.com/aklinker1/wxt/commit/9ab3953))
|
||||
- Fix release workflow ([2e94f2a](https://github.com/aklinker1/wxt/commit/2e94f2a))
|
||||
- Refactor cli files into their own directory ([e6c0d84](https://github.com/wxt-dev/wxt/commit/e6c0d84))
|
||||
- Simplify `BuildOutput` type ([1f6c4a0](https://github.com/wxt-dev/wxt/commit/1f6c4a0))
|
||||
- Move `.exvite` directory into `srcDir` instead of `root` ([53fb805](https://github.com/wxt-dev/wxt/commit/53fb805))
|
||||
- Refactor CLI commands ([b8952b6](https://github.com/wxt-dev/wxt/commit/b8952b6))
|
||||
- Improve build summary sorting ([ec57e8c](https://github.com/wxt-dev/wxt/commit/ec57e8c))
|
||||
- Remove comments ([e3e9c0d](https://github.com/wxt-dev/wxt/commit/e3e9c0d))
|
||||
- Refactor internal config creation ([7c634f4](https://github.com/wxt-dev/wxt/commit/7c634f4))
|
||||
- Check virtual entrypoints feature in README ([70208f4](https://github.com/wxt-dev/wxt/commit/70208f4))
|
||||
- Add E2E tests and convert to vitest workspace ([5813302](https://github.com/wxt-dev/wxt/commit/5813302))
|
||||
- Rename package to wxt ([51a1072](https://github.com/wxt-dev/wxt/commit/51a1072))
|
||||
- Fix header log's timestamp ([8ca5657](https://github.com/wxt-dev/wxt/commit/8ca5657))
|
||||
- Fix demo global usage ([1ecfedd](https://github.com/wxt-dev/wxt/commit/1ecfedd))
|
||||
- Refactor folder structure ([9ab3953](https://github.com/wxt-dev/wxt/commit/9ab3953))
|
||||
- Fix release workflow ([2e94f2a](https://github.com/wxt-dev/wxt/commit/2e94f2a))
|
||||
|
||||
### 🤖 CI
|
||||
|
||||
- Create validation workflow ([#12](https://github.com/aklinker1/wxt/pull/12))
|
||||
- Create release workflow ([#13](https://github.com/aklinker1/wxt/pull/13))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
|
||||
- Create validation workflow ([#12](https://github.com/wxt-dev/wxt/pull/12))
|
||||
- Create release workflow ([#13](https://github.com/wxt-dev/wxt/pull/13))
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# Contributing
|
||||
|
||||
Everyone is welcome to contribute to WXT!
|
||||
|
||||
If you are changing the docs or fixing a bug, feel free to fork and open a PR.
|
||||
|
||||
If you want to add a new feature, please create an issue or discussion first so we can decide if the feature is inline with the vision for WXT.
|
||||
|
||||
## Conventional Commits
|
||||
|
||||
This project uses [Conventional Commits](https://www.conventionalcommits.org/en) to automate versioning. If you're a new contributor, don't worry about this. When you open a PR, a maintainer will change the PR's title so it's in the style of conventional commits, but that's all.
|
||||
|
||||
Maintainers, commits to the `main` branch (either directly or via PRs) must be valid conventional commits.
|
||||
|
||||
## Setup
|
||||
|
||||
WXT uses `pnpm`, so make sure you have it installed.
|
||||
|
||||
```sh
|
||||
corepack enable
|
||||
```
|
||||
|
||||
Then, simply run the install command:
|
||||
|
||||
```sh
|
||||
pnpm i
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
Here are some helpful commands:
|
||||
|
||||
```sh
|
||||
# Build WXT package
|
||||
pnpm build
|
||||
```
|
||||
|
||||
```sh
|
||||
# Build WXT package, then build demo extension
|
||||
cd demo
|
||||
pnpm build
|
||||
```
|
||||
|
||||
```sh
|
||||
# Build WXT package, then start the demo extension in dev mode
|
||||
cd demo
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
```sh
|
||||
# Run unit and E2E tests
|
||||
pnpm test
|
||||
```
|
||||
|
||||
```sh
|
||||
# Start the docs website locally
|
||||
pnpm docs:dev
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
WXT has unit and E2E tests. When making a change or adding a feature, make sure to update the tests or add new ones.
|
||||
|
||||
To run tests for a specific file, add the filename at the end of the test command:
|
||||
|
||||
```sh
|
||||
pnpm test manifest-contents
|
||||
```
|
||||
|
||||
Unit and E2E tests are ran together via [Vitest workspaces](https://vitest.dev/guide/#workspaces-support).
|
||||
|
||||
If you want to manually test a change, you can modify the demo project for your test, but please don't leave those changes committed once you open a PR.
|
||||
@@ -1,11 +1,52 @@
|
||||
<h1 align="center">WXT</h1>
|
||||
<h1 align="center">
|
||||
<img style="vertical-align:middle" width="44" src="./docs/public/hero-logo.svg" alt="WXT Logo">
|
||||
<span>WXT</span>
|
||||
</h1>
|
||||
|
||||
<p align="center"><img align="center" width="44" src="./docs/public/hero-logo.svg" alt="WXT Logo"></p>
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/package/wxt" target="_blank">
|
||||
<img alt="npm" src="https://img.shields.io/npm/v/wxt?labelColor=black&color=%234fa048">
|
||||
</a>
|
||||
<span> </span>
|
||||
<img alt="npm" src="https://img.shields.io/npm/dm/wxt?labelColor=black&color=%234fa048">
|
||||
<span> </span>
|
||||
<img alt="NPM" src="https://img.shields.io/npm/l/wxt?labelColor=black&color=%234fa048">
|
||||
<span> </span>
|
||||
<a href="https://codecov.io/github/wxt-dev/wxt" target="_blank">
|
||||
<img alt="Codecov" src="https://img.shields.io/codecov/c/github/wxt-dev/wxt?labelColor=black&color=%234fa048">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center"><i>Next gen framework for developing web extensions.<br/>Powered by <a href="https://vitejs.dev/" target="_blank">Vite</a>. Inspired by <a href="https://nuxt.com/" target="_blank">Nuxt</a>.</i></p>
|
||||
<p align="center">
|
||||
<span>Next-gen framework for developing web extensions.</span>
|
||||
<br/>
|
||||
<span>⚡</span>
|
||||
<br/>
|
||||
<q><i>It's like Nuxt, but for Chrome Extensions</i></q>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://wxt.dev" target="_blank">Get Started</a>
|
||||
•
|
||||
<a href="https://wxt.dev/get-started/installation.html" target="_blank">Installation</a>
|
||||
•
|
||||
<a href="https://wxt.dev/guide/background.html" target="_blank">Entrypoints</a>
|
||||
•
|
||||
<a href="https://wxt.dev/config.html" target="_blank">Configuration</a>
|
||||
</p>
|
||||
|
||||

|
||||
|
||||
## Quick Start
|
||||
|
||||
Bootstrap a new project:
|
||||
|
||||
```sh
|
||||
pnpx wxt@latest init <project-name>
|
||||
```
|
||||
|
||||
Or see the [installation guide](https://wxt.dev/get-started/installation.html) to get started with WXT.
|
||||
|
||||
## Features
|
||||
|
||||
- 🌐 Supports all browsers
|
||||
@@ -17,18 +58,14 @@
|
||||
- ⬇️ Download and bundle remote URL imports
|
||||
- 🎨 Frontend framework agnostic: works with Vue, React, Svelte, etc
|
||||
- 🖍️ Quickly bootstrap a new project
|
||||
|
||||
### Todo
|
||||
|
||||
- 📏 Bundle analysis
|
||||
|
||||
### Coming Soon
|
||||
|
||||
- 🤖 Automated publishing
|
||||
|
||||
## Get Started
|
||||
|
||||
Checkout the [installation guide](https://wxt.dev/get-started/installation.html) to get started with WXT.
|
||||
|
||||
## Contributors
|
||||
|
||||
<a href="https://github.com/aklinker1/wxt/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=aklinker1/wxt" />
|
||||
<a href="https://github.com/wxt-dev/wxt/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=wxt-dev/wxt" />
|
||||
</a>
|
||||
|
||||
@@ -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:*"
|
||||
|
||||
@@ -12,4 +12,13 @@ export default defineBackground(() => {
|
||||
browser.runtime.getURL('/');
|
||||
browser.runtime.getURL('/background.js');
|
||||
browser.runtime.getURL('/icon/128.png');
|
||||
|
||||
// @ts-expect-error: should only accept known message names
|
||||
browser.i18n.getMessage('test');
|
||||
browser.i18n.getMessage('prompt_for_name');
|
||||
browser.i18n.getMessage('hello', 'Aaron');
|
||||
browser.i18n.getMessage('bye', ['Aaron']);
|
||||
browser.i18n.getMessage('@@extension_id');
|
||||
|
||||
console.log('WXT MODE:', import.meta.env.MODE);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['<all_urls>'],
|
||||
async main(ctx) {
|
||||
console.log(browser.runtime.id);
|
||||
logId();
|
||||
|
||||
console.log('WXT MODE:', import.meta.env.MODE);
|
||||
|
||||
const n = (Math.random() * 100).toFixed(1);
|
||||
ctx.setInterval(() => {
|
||||
console.log(n, browser.runtime.id);
|
||||
}, 1e3);
|
||||
|
||||
const container = document.createElement('div');
|
||||
document.body.append(container);
|
||||
|
||||
ReactDOM.createRoot(container).render(<SomeComponent />);
|
||||
},
|
||||
});
|
||||
|
||||
function SomeComponent() {
|
||||
return <div>Some component</div>;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'url:https://stats.aklinker1.io/umami.js';
|
||||
import 'url:https://code.jquery.com/jquery-3.7.1.slim.min.js';
|
||||
|
||||
console.log(browser.runtime.id);
|
||||
logId();
|
||||
console.log(2);
|
||||
|
||||
console.log('WXT MODE:', import.meta.env.MODE);
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import '../../common/style.css';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['*://*/*'],
|
||||
async main() {
|
||||
console.log(browser.runtime.id);
|
||||
logId();
|
||||
mountContentScriptUi();
|
||||
},
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import '../../common/style.css';
|
||||
import './style.css';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['https://*.duckduckgo.com/*'],
|
||||
cssInjectionMode: 'ui',
|
||||
|
||||
async main(ctx) {
|
||||
const ui = await createContentScriptUi(ctx, {
|
||||
name: 'demo-ui',
|
||||
type: 'inline',
|
||||
append: 'before',
|
||||
anchor: 'form[role=search]',
|
||||
mount: (container) => {
|
||||
const app = document.createElement('div');
|
||||
app.textContent = 'Custom content script UI';
|
||||
container.append(app);
|
||||
},
|
||||
});
|
||||
ui.mount();
|
||||
|
||||
setTimeout(ui.remove, 5000);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
color: indianred;
|
||||
}
|
||||
html {
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
div {
|
||||
padding: 16px;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"prompt_for_name": {
|
||||
"message": "What's your name?",
|
||||
"description": "Ask for the user's name"
|
||||
},
|
||||
"hello": {
|
||||
"message": "Hello, $USER$",
|
||||
"description": "Greet the user",
|
||||
"placeholders": {
|
||||
"user": {
|
||||
"content": "$1",
|
||||
"example": "Cira"
|
||||
}
|
||||
}
|
||||
},
|
||||
"bye": {
|
||||
"message": "Goodbye, $USER$. Come back to $OUR_SITE$ soon!",
|
||||
"description": "Say goodbye to the user",
|
||||
"placeholders": {
|
||||
"our_site": {
|
||||
"content": "Example.com"
|
||||
},
|
||||
"user": {
|
||||
"content": "$1",
|
||||
"example": "Cira"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 504 B After Width: | Height: | Size: 504 B |
|
Before Width: | Height: | Size: 936 B After Width: | Height: | Size: 936 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
@@ -1,3 +1,7 @@
|
||||
{
|
||||
"extends": ["../tsconfig.base.json", "./src/.wxt/tsconfig.json"]
|
||||
"extends": ["../tsconfig.base.json", "./.wxt/tsconfig.json"],
|
||||
"compilerOptions": {
|
||||
"allowImportingTsExtensions": true,
|
||||
"jsx": "react-jsx"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,7 @@ import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
srcDir: 'src',
|
||||
storeIds: {
|
||||
chrome: '123',
|
||||
},
|
||||
manifest: {
|
||||
icons: {
|
||||
'16': 'icon/16.png',
|
||||
'32': 'icon/32.png',
|
||||
'48': 'icon/48.png',
|
||||
'96': 'icon/96.png',
|
||||
'128': 'icon/128.png',
|
||||
},
|
||||
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,21 +1,54 @@
|
||||
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/aklinker1/wxt/edit/main/docs/:path',
|
||||
pattern: 'https://github.com/wxt-dev/wxt/edit/main/docs/:path',
|
||||
},
|
||||
search: {
|
||||
provider: 'local',
|
||||
@@ -53,6 +86,7 @@ export default defineConfig({
|
||||
{ text: 'Manifest.json', link: '/guide/manifest.md' },
|
||||
{ text: 'Extension APIs', link: '/guide/extension-apis.md' },
|
||||
{ text: 'Remote Code', link: '/guide/remote-code.md' },
|
||||
{ text: 'Vite', link: '/guide/vite.md' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -76,6 +110,6 @@ export default defineConfig({
|
||||
],
|
||||
},
|
||||
|
||||
socialLinks: [{ icon: 'github', link: 'https://github.com/aklinker1/wxt' }],
|
||||
socialLinks: [{ icon: 'github', link: 'https://github.com/wxt-dev/wxt' }],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { resolve } from 'node:path';
|
||||
import { Project, ts, Type, Node, JSDocableNode } from 'ts-morph';
|
||||
import Ora from 'ora';
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { format } from 'prettier';
|
||||
import consola from 'consola';
|
||||
|
||||
let hasGenerated = false;
|
||||
|
||||
const externalTypesPath = resolve('src/core/types/external.ts');
|
||||
const configTemplatePath = resolve('docs/config.tpl.md');
|
||||
@@ -20,16 +23,23 @@ 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() {
|
||||
const generateDocs = () => {
|
||||
const spinner = Ora('Generating /config.md').start();
|
||||
writeFileSync(configPath, '');
|
||||
|
||||
const generateDocs = async () => {
|
||||
consola.info('Generating /config.md');
|
||||
try {
|
||||
const project = new Project({
|
||||
tsConfigFilePath: resolve('tsconfig.json'),
|
||||
@@ -39,18 +49,10 @@ export function generateConfigDocs() {
|
||||
const externalTypesFile = project.addSourceFileAtPath(externalTypesPath);
|
||||
project.resolveSourceFileDependencies();
|
||||
|
||||
const typeChecker = project.getProgram().getTypeChecker();
|
||||
|
||||
const inlineConfigInterface =
|
||||
externalTypesFile.getInterfaceOrThrow('InlineConfig');
|
||||
|
||||
const getDocsFor = (
|
||||
path: string[],
|
||||
node: Node<ts.Node>,
|
||||
depth = 0,
|
||||
): string[] => {
|
||||
if (depth > 3) throw Error('Recursion to deep for ' + path.join('.'));
|
||||
|
||||
const getDocsFor = (path: string[], node: Node<ts.Node>): string[] => {
|
||||
const pathStr = path.join('.');
|
||||
|
||||
let type: Type<ts.Type>;
|
||||
@@ -64,22 +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],
|
||||
depth + 1,
|
||||
);
|
||||
})
|
||||
);
|
||||
return getDocsFor(childPath, property.getDeclarations()[0]);
|
||||
});
|
||||
}
|
||||
|
||||
if ('getJsDocs' in node) {
|
||||
@@ -115,33 +107,38 @@ export function generateConfigDocs() {
|
||||
};
|
||||
|
||||
const lines = getDocsFor([], inlineConfigInterface);
|
||||
const text =
|
||||
const text = await format(
|
||||
PREFACE +
|
||||
'\n\n' +
|
||||
readFileSync(configTemplatePath, 'utf-8').replace(
|
||||
'{{ DOCS }}',
|
||||
lines.join('\n'),
|
||||
);
|
||||
'\n\n' +
|
||||
readFileSync(configTemplatePath, 'utf-8').replace(
|
||||
'{{ DOCS }}',
|
||||
lines.join('\n'),
|
||||
),
|
||||
{ parser: 'markdown' },
|
||||
);
|
||||
|
||||
writeFileSync(configPath, text);
|
||||
spinner.succeed('Generated /config.md');
|
||||
consola.success('Generated /config.md');
|
||||
} catch (err) {
|
||||
spinner.fail('Failed to generate /config.md');
|
||||
console.error(err.message);
|
||||
consola.fail('Failed to generate /config.md');
|
||||
consola.error(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
name: 'docs:generate-config-docs',
|
||||
buildStart() {
|
||||
generateDocs();
|
||||
async config() {
|
||||
if (!hasGenerated) {
|
||||
hasGenerated = true;
|
||||
await generateDocs();
|
||||
}
|
||||
},
|
||||
configureServer(server: any) {
|
||||
server.watcher.add(externalTypesPath);
|
||||
},
|
||||
handleHotUpdate(ctx: { file: string }) {
|
||||
async handleHotUpdate(ctx: { file: string }) {
|
||||
if ([externalTypesPath, configTemplatePath].includes(ctx.file)) {
|
||||
generateDocs();
|
||||
await generateDocs();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,31 +1,46 @@
|
||||
/* Colors */
|
||||
:root {
|
||||
--wxt-c-green: #53bc4a;
|
||||
--wxt-c-green-light: #67d45e;
|
||||
--wxt-c-green-lighter: #67d45e;
|
||||
--wxt-c-green-dark: #4fa048;
|
||||
--wxt-c-green-darker: #447e3f;
|
||||
--wxt-c-green-1: #67d45e;
|
||||
--wxt-c-green-2: #4fa048;
|
||||
--wxt-c-green-3: #447e3f;
|
||||
}
|
||||
|
||||
/* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css */
|
||||
|
||||
:root {
|
||||
--vp-c-brand: var(--wxt-c-green);
|
||||
--vp-c-brand-light: var(--wxt-c-green-light);
|
||||
--vp-c-brand-lighter: var(--wxt-c-green-lighter);
|
||||
--vp-c-brand-dark: var(--wxt-c-green-dark);
|
||||
--vp-c-brand-darker: var(--wxt-c-green-darker);
|
||||
--vp-c-brand-1: var(--wxt-c-green-1);
|
||||
--vp-c-brand-2: var(--wxt-c-green-2);
|
||||
--vp-c-brand-3: var(--wxt-c-green-3);
|
||||
|
||||
--vp-button-brand-bg: var(--wxt-c-green);
|
||||
--vp-button-brand-hover-bg: var(--wxt-c-green-2);
|
||||
--vp-button-brand-active-bg: var(--wxt-c-green-3);
|
||||
|
||||
--vp-code-link-color: var(--wxt-c-green);
|
||||
|
||||
/* --vp-c-text-1: var(--wxt-c-green-1); */
|
||||
|
||||
--vp-button-brand-text: var(--vp-c-black);
|
||||
--vp-button-brand-hover-text: var(--vp-c-black);
|
||||
--vp-button-brand-active-text: var(--vp-c-black);
|
||||
|
||||
--vp-custom-block-tip-border: var(--wxt-c-green-dark);
|
||||
--vp-custom-block-tip-text: var(--wxt-c-green-dark);
|
||||
--vp-custom-block-tip-border: var(--wxt-c-green);
|
||||
--vp-custom-block-tip-text: var(--wxt-c-green);
|
||||
|
||||
--vp-code-block-bg: #222422;
|
||||
/* --vp-code-block-bg: #222422;
|
||||
--vp-code-copy-code-bg: #313431;
|
||||
--vp-code-copy-code-hover-bg: #3c403c;
|
||||
--vp-code-copy-code-hover-bg: #3c403c; */
|
||||
|
||||
--vp-custom-block-tip-bg: var(--vp-code-block-bg);
|
||||
--vp-custom-block-info-bg: var(--vp-code-block-bg);
|
||||
|
||||
--vp-code-color: #476582;
|
||||
}
|
||||
|
||||
.vp-doc a {
|
||||
color: var(--wxt-c-green);
|
||||
}
|
||||
|
||||
.dark {
|
||||
@@ -38,15 +53,19 @@
|
||||
|
||||
--vp-c-bg-alt: #171817;
|
||||
|
||||
--vp-c-mute: #313136;
|
||||
--vp-c-mute-light: #3a3a3c;
|
||||
--vp-c-mute-lighter: #505053;
|
||||
--vp-c-mute-dark: #2c2c30;
|
||||
--vp-c-mute-darker: #252529;
|
||||
--vp-c-default: #313136;
|
||||
--vp-c-default-1: #3a3a3c;
|
||||
--vp-c-default-2: #505053;
|
||||
--vp-c-default-3: #2c2c30;
|
||||
--vp-c-default-soft: #252529;
|
||||
|
||||
--vp-code-block-bg: #191a19;
|
||||
--vp-code-copy-code-bg: #212321;
|
||||
--vp-code-copy-code-hover-bg: #292d29;
|
||||
|
||||
--vp-custom-block-info-bg: #191a19;
|
||||
|
||||
--vp-code-color: #c9def1;
|
||||
}
|
||||
|
||||
.vp-doc .no-vertical-dividers th,
|
||||
|
||||
|
Before Width: | Height: | Size: 415 KiB After Width: | Height: | Size: 238 KiB |
@@ -1,200 +0,0 @@
|
||||
<!--
|
||||
DO NOT EDIT
|
||||
Generated by `wxt/docs/.vitepress/plugins/generate-config-docs.ts`
|
||||
To make changes to the config reference, update the JSDoc in `src/core/types/external.ts`.
|
||||
-->
|
||||
|
||||
# Config Reference
|
||||
|
||||
Discover all the options you can use in your `wxt.config.ts` file.
|
||||
|
||||
## root
|
||||
|
||||
- **Type**: `string`
|
||||
- **Default**: `process.cwd()`
|
||||
|
||||
Your project's root directory containing the `package.json` used to fill out the
|
||||
`manifest.json`.
|
||||
|
||||
## srcDir
|
||||
|
||||
- **Type**: `string`
|
||||
- **Default**: `config.root`
|
||||
|
||||
Directory containing all source code. Set to `"src"` to move all source code to a `src/`
|
||||
directory.
|
||||
|
||||
## publicDir
|
||||
|
||||
- **Type**: `string`
|
||||
- **Default**: `"${config.root}/public"`
|
||||
|
||||
Directory containing files that will be copied to the output directory as-is.
|
||||
|
||||
## entrypointsDir
|
||||
|
||||
- **Type**: `string`
|
||||
- **Default**: `"${config.srcDir}/entrypoints"`
|
||||
|
||||
## configFile
|
||||
|
||||
- **Type**: `string | false`
|
||||
- **Default**: `"wxt.config.ts"`
|
||||
|
||||
Path to `"wxt.config.ts"` file or false to disable config file discovery.
|
||||
|
||||
## storeIds.chrome
|
||||
|
||||
- **Type**: `string`
|
||||
- **Default**: `undefined`
|
||||
|
||||
## storeIds.firefox
|
||||
|
||||
- **Type**: `string`
|
||||
- **Default**: `undefined`
|
||||
|
||||
## storeIds.edge
|
||||
|
||||
- **Type**: `string`
|
||||
- **Default**: `undefined`
|
||||
|
||||
## mode
|
||||
|
||||
- **Type**: `string`
|
||||
- **Default**: `undefined`
|
||||
|
||||
Explicitly set a mode to run in. This will override the default mode for each command, and can
|
||||
be overridden by the command line `--mode` option.
|
||||
|
||||
## browser
|
||||
|
||||
- **Type**: `"chrome" | "firefox" | "safari" | "edge" | "opera"`
|
||||
- **Default**: `"chrome"`
|
||||
|
||||
Explicitly set a browser to build for. This will override the default browser for each command,
|
||||
and can be overridden by the command line `--browser` option.
|
||||
|
||||
## manifestVersion
|
||||
|
||||
- **Type**: `2 | 3`
|
||||
- **Default**: `undefined`
|
||||
|
||||
Explicitly set a manifest version to target. This will override the default manifest version
|
||||
for each command, and can be overridden by the command line `--mv2` or `--mv3` option.
|
||||
|
||||
## manifest
|
||||
|
||||
- **Type**: `Manifest | Promise<Manifest> | () => Manifest | () => Promise<Manifest>`
|
||||
- **Default**: `undefined`
|
||||
|
||||
Customize the `manifest.json` output. Can be an object, promise, or function that returns an
|
||||
object or promise.
|
||||
|
||||
## runner.openConsole
|
||||
|
||||
- **Type**: `boolean`
|
||||
- **Default**: `undefined`
|
||||
|
||||
## runner.openDevtools
|
||||
|
||||
- **Type**: `boolean`
|
||||
- **Default**: `undefined`
|
||||
|
||||
## runner.binaries.chrome
|
||||
|
||||
- **Type**: `string`
|
||||
- **Default**: `undefined`
|
||||
|
||||
## runner.binaries.edge
|
||||
|
||||
- **Type**: `string`
|
||||
- **Default**: `undefined`
|
||||
|
||||
## runner.binaries.opera
|
||||
|
||||
- **Type**: `string`
|
||||
- **Default**: `undefined`
|
||||
|
||||
## runner.binaries.firefox
|
||||
|
||||
- **Type**: `string`
|
||||
- **Default**: `undefined`
|
||||
|
||||
## runner.firefoxProfile
|
||||
|
||||
- **Type**: `string`
|
||||
- **Default**: `undefined`
|
||||
|
||||
## runner.chromiumProfile
|
||||
|
||||
- **Type**: `string`
|
||||
- **Default**: `undefined`
|
||||
|
||||
## runner.firefoxArgs
|
||||
|
||||
- **Type**: `string[]`
|
||||
- **Default**: `undefined`
|
||||
|
||||
## runner.chromiumArgs
|
||||
|
||||
- **Type**: `string[]`
|
||||
- **Default**: `undefined`
|
||||
|
||||
## runner.startUrls
|
||||
|
||||
- **Type**: `string[]`
|
||||
- **Default**: `undefined`
|
||||
|
||||
## zip.artifactTemplate
|
||||
|
||||
- **Type**: `string`
|
||||
- **Default**: `"{name}-{version}-{browser}.zip"`
|
||||
|
||||
Configure the filename output when zipping files.
|
||||
|
||||
Available template variables:
|
||||
|
||||
- `{name}` - The project's name converted to kebab-case
|
||||
- `{version}` - The version_name or version from the manifest
|
||||
- `{browser}` - The target browser from the `--browser` CLI flag
|
||||
- `{manifestVersion}` - Either "2" or "3"
|
||||
|
||||
## zip.sourcesTemplate
|
||||
|
||||
- **Type**: `string`
|
||||
- **Default**: `"{name}-{version}-sources.zip"`
|
||||
|
||||
Configure the filename output when zipping files.
|
||||
|
||||
Available template variables:
|
||||
|
||||
- `{name}` - The project's name converted to kebab-case
|
||||
- `{version}` - The version_name or version from the manifest
|
||||
- `{browser}` - The target browser from the `--browser` CLI flag
|
||||
- `{manifestVersion}` - Either "2" or "3"
|
||||
|
||||
## zip.name
|
||||
|
||||
- **Type**: `string`
|
||||
- **Default**: `undefined`
|
||||
|
||||
Override the artifactTemplate's `{name}` template variable. Defaults to the `package.json`'s
|
||||
name, or if that doesn't exist, the current working directories name.
|
||||
|
||||
## zip.sourcesRoot
|
||||
|
||||
- **Type**: `string`
|
||||
- **Default**: `config.root`
|
||||
|
||||
Root directory to ZIP when generating the sources ZIP.
|
||||
|
||||
## zip.ignoredSources
|
||||
|
||||
- **Type**: `string[]`
|
||||
- **Default**: `undefined`
|
||||
|
||||
[Minimatch](https://www.npmjs.com/package/minimatch) patterns of files to exclude when
|
||||
creating a ZIP of all your source code for Firfox. Patterns are relative to your
|
||||
`config.zip.sourcesRoot`.
|
||||
|
||||
Hidden files, node_modules, and tests are ignored by default.
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
WXT has two directories for storing assets like CSS, images, or fonts.
|
||||
|
||||
- `<rootDir>/public`: Store files that will be copied into the output directory as-is
|
||||
- `<srcDir>/public`: Store files that will be copied into the output directory as-is
|
||||
- `<srcDir>/assets`: Store files that will be processed by Vite during the build process
|
||||
|
||||
## `/public` Directory
|
||||
|
||||
@@ -45,5 +45,5 @@ Reloading each part of the extension individually improves your iteration speed
|
||||
WXT solves this problem by reloading HTML pages and content scripts individually (when possible) to keep your UIs open while you develop them. This is a MV3 feature, so if you're developing a MV2 extension, you'll get the same dev experience as Plasmo.
|
||||
|
||||
:::info
|
||||
Unfortunately, there isn't an API for reloading the background page/service worker individually, so if you change a file used by the background, the entire extension will reload. See [Issue #53](https://github.com/aklinker1/wxt/issues/53) for more details.
|
||||
Unfortunately, there isn't an API for reloading the background page/service worker individually, so if you change a file used by the background, the entire extension will reload. See [Issue #53](https://github.com/wxt-dev/wxt/issues/53) for more details.
|
||||
:::
|
||||
|
||||
@@ -25,7 +25,7 @@ WXT allows you to edit several directories to your liking:
|
||||
- `root` (default: `process.cwd()`) - Root of the WXT project
|
||||
- `srcDir` (default: `<rootDir>`) - Location of all your source code
|
||||
- `entrypointsDir` (default: `<srcDir>/entrypoints`) - Folder containing all the entrypoints.
|
||||
- `publicDir` (default: `<rootDir>/public`) - Folder containing [public assets](/get-started/assets.md)
|
||||
- `publicDir` (default: `<srcDir>/public`) - Folder containing [public assets](/get-started/assets.md)
|
||||
|
||||
### Example
|
||||
|
||||
|
||||
@@ -12,12 +12,11 @@ For example, a project that looks like this:
|
||||
|
||||
```
|
||||
<rootDir>
|
||||
├─ entrypoints/
|
||||
│ ├─ background.ts
|
||||
│ ├─ content.ts
|
||||
│ ├─ injected.ts
|
||||
│ └─ popup.html
|
||||
└─ wxt.config.ts
|
||||
└─ entrypoints/
|
||||
├─ background.ts
|
||||
├─ content.ts
|
||||
├─ injected.ts
|
||||
└─ popup.html
|
||||
```
|
||||
|
||||
would result in the following `manifest.json`:
|
||||
@@ -49,9 +48,9 @@ If a file uses a [special name recognized by WXT](/get-started/entrypoints.md),
|
||||
- `content.ts` → `content_scripts.0.js.0`
|
||||
- `background.ts` → `background.service_worker`
|
||||
|
||||
But not all entrypoints are added to the `manifest.json`. If they have a name that is not recognized by WXT, they are still built and included in the extension, but they are considered "unlisted" and are not apart of the manifest.
|
||||
But not all entrypoints are added to the `manifest.json`. If the filename is not recognized by WXT, they are still built and included in the extension, but they are considered "unlisted" and are not apart of the manifest.
|
||||
|
||||
In this case, `injected.ts` gets bundled to `<outdir>/injected.js` and is accessible via `browser.runtime.getURL("/injected.js")`.
|
||||
In this case, `injected.ts` gets output to `<outdir>/injected.js` and is accessible via `browser.runtime.getURL("/injected.js")`.
|
||||
|
||||
:::info
|
||||
See [`/entrypoints` folder](/guide/background.md) documentation for a full list of recognized entrypoint filenames.
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
# Installation
|
||||
|
||||
Bootstrap a new project or start from scratch.
|
||||
|
||||
:::warning 🚧 WSL Support
|
||||
**_WXT does not support [Windows Subsystem for Linux](https://learn.microsoft.com/en-us/windows/wsl/) yet_**. See [Issue #55](https://github.com/aklinker1/wxt/issues/55) to track progress.
|
||||
|
||||
In the meantime, you can use `cmd` instead.
|
||||
:::
|
||||
Bootstrap a new project, start from scratch, or migrate an existing project.
|
||||
|
||||
## Bootstrap Project
|
||||
|
||||
@@ -24,13 +18,13 @@ npx wxt@latest init <project-name>
|
||||
|
||||
There are several starting templates available.
|
||||
|
||||
| TypeScript |
|
||||
| --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| <Icon name="TypeScript" /> [`vanilla`](https://github.com/aklinker1/wxt/tree/main/templates/vanilla) |
|
||||
| <Icon name="Vue" /> [`vue`](https://github.com/aklinker1/wxt/tree/main/templates/vue) |
|
||||
| <Icon name="React" /> [`react`](https://github.com/aklinker1/wxt/tree/main/templates/react) |
|
||||
| <Icon name="Svelte" /> [`svelte`](https://github.com/aklinker1/wxt/tree/main/templates/svelte) |
|
||||
| <Icon name="Solid" icon="https://www.solidjs.com/img/favicons/favicon-32x32.png" /> [`solid`](https://github.com/aklinker1/wxt/tree/main/templates/solid) |
|
||||
| TypeScript |
|
||||
| ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| <Icon name="TypeScript" /> [`vanilla`](https://github.com/wxt-dev/wxt/tree/main/templates/vanilla) |
|
||||
| <Icon name="Vue" /> [`vue`](https://github.com/wxt-dev/wxt/tree/main/templates/vue) |
|
||||
| <Icon name="React" /> [`react`](https://github.com/wxt-dev/wxt/tree/main/templates/react) |
|
||||
| <Icon name="Svelte" /> [`svelte`](https://github.com/wxt-dev/wxt/tree/main/templates/svelte) |
|
||||
| <Icon name="Solid" icon="https://www.solidjs.com/img/favicons/favicon-32x32.png" /> [`solid`](https://github.com/wxt-dev/wxt/tree/main/templates/solid) |
|
||||
|
||||
> All templates are in TypeScript. WXT does not support JS at this time.
|
||||
|
||||
@@ -104,6 +98,22 @@ Finally, add scripts to your `package.json`:
|
||||
}
|
||||
```
|
||||
|
||||
## Migrate an Existing Project
|
||||
|
||||
Before starting the migration, it is recommended to run `pnpx wxt@latest init` to see what a basic project looks like. Once you have an understanding of how WXT projects are structured, you're ready to convert the project, using the initialized project as a reference.
|
||||
|
||||
Migrating a project to WXT comes down to a few steps:
|
||||
|
||||
1. Install WXT and remove any old build tools: `pnpm i -D wxt`
|
||||
1. Refactoring/move your entrypoints to the `entrypoints` directory
|
||||
1. Moving public assets to the `public` directory
|
||||
1. Update the dev and build scripts to use WXT
|
||||
1. Ensure project is compatible with Vite, which is used under the hood to bundle your extension
|
||||
|
||||
:::info
|
||||
Since projects vary greatly in setup, [start a discussion on GitHub](https://github.com/wxt-dev/wxt/discussions/new/choose) if you need help migrating your project to WXT.
|
||||
:::
|
||||
|
||||
## Development
|
||||
|
||||
Once you've installed WXT, you can start the development server using the `dev` script.
|
||||
@@ -134,3 +144,4 @@ You're ready to build your web extension!
|
||||
|
||||
- Learn how to [add entrypoints](./entrypoints.md) like the popup, options page, or content scripts
|
||||
- [Configure WXT](./configuration.md) by creating a `wxt.config.ts` file
|
||||
- Checkout [example projects](https://github.com/wxt-dev/wxt-examples) to see how to perfom common tasks with WXT
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
---
|
||||
head:
|
||||
- - link
|
||||
- rel: canonical
|
||||
href: https://wxt.dev
|
||||
---
|
||||
|
||||
# Introduction
|
||||
|
||||
WXT is a free and open source framework for building web extensions in an conventional, intuative, and safe way **_for all browsers_**.
|
||||
|
||||
@@ -16,9 +16,9 @@ wxt zip -b firefox
|
||||
|
||||
Generated ZIP files are stored in the `.output` directory.
|
||||
|
||||
## Setup Automated Submissions
|
||||
## Automation
|
||||
|
||||
To automate submissions, use the [`publish-browser-extension`](https://www.npmjs.com/package/publish-browser-extension) package.
|
||||
To automate releasing updates, use the [`publish-browser-extension`](https://www.npmjs.com/package/publish-browser-extension) package.
|
||||
|
||||
:::info
|
||||
🚧 WXT plans to eventually incorporate the `publish-browser-extension` package into its own `wxt submit` command.
|
||||
@@ -159,7 +159,7 @@ Ensure that you have a `README.md` or `SOURCE_CODE_REVIEW.md` file with the abov
|
||||
🚧 Not automated at this time
|
||||
|
||||
:::warning
|
||||
🚧 WXT does not currently support automated publishing for Safari. Safari extensions require a native MacOS or iOS app wrapper, which WXT isn't configured to create. For now, if you want to publish to Safari, follow this guide:
|
||||
🚧 WXT does not currently support automated publishing for Safari. Safari extensions require a native MacOS or iOS app wrapper, which WXT cannot create at this time. For now, if you want to publish to Safari, follow this guide:
|
||||
|
||||
https://developer.apple.com/documentation/safariservices/safari_web_extensions/distributing_your_safari_web_extension
|
||||
|
||||
|
||||
@@ -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,15 +13,16 @@
|
||||
|
||||
## Definition
|
||||
|
||||
Plain old HTML file.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Title</title>
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
|
||||
@@ -30,7 +30,14 @@ 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[],
|
||||
|
||||
// Configure how CSS is injected onto the page
|
||||
cssInjectionMode: undefined | "manifest" | "manual" | "ui",
|
||||
|
||||
main(ctx) {
|
||||
// Executed when content script is loaded
|
||||
},
|
||||
});
|
||||
@@ -59,7 +66,7 @@ import './style.css';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['*://google.com/*', '*://duckduckgo.com/*'],
|
||||
main() {
|
||||
main(ctx) {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
@@ -79,3 +86,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.
|
||||
:::
|
||||
|
||||
@@ -16,12 +16,15 @@
|
||||
Plain old HTML file.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Title</title>
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
</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,15 +13,16 @@
|
||||
|
||||
## Definition
|
||||
|
||||
Plain old HTML file.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Title</title>
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
|
||||
@@ -13,15 +13,16 @@
|
||||
|
||||
## Definition
|
||||
|
||||
Plain old HTML file.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Title</title>
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
## Definition
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
@@ -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>
|
||||
<!-- ... -->
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
## Definition
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
@@ -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,15 +19,16 @@ Firefox does not support sandboxed pages.
|
||||
|
||||
## Definition
|
||||
|
||||
Plain old HTML file.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Title</title>
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
|
||||
@@ -19,15 +19,16 @@ Firefox does not support sidepanel pages.
|
||||
|
||||
## Definition
|
||||
|
||||
Plain old HTML file.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Title</title>
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
|
||||
@@ -29,15 +29,16 @@ console.log(url); // "chrome-extension://<id>/<name>.html"
|
||||
|
||||
## Definition
|
||||
|
||||
Plain old HTML file.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Title</title>
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# Vite
|
||||
|
||||
Under the hood, WXT uses Vite to bundle your web extension.
|
||||
|
||||
## Basic Vite Configuration
|
||||
|
||||
All of Vite's config can be customized by setting the `vite` configuration in your `wxt.config.ts` file.
|
||||
|
||||
```ts
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
vite: () => ({
|
||||
// Same as `defineConfig({ ... })` inside vite.config.ts
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
## Using Plugins
|
||||
|
||||
Plugins can be passed into the `vite` configuration in you `wxt.config.ts` file, just like any other option.
|
||||
|
||||
All plugins should work in WXT, but it is worth pointing out that since WXT orchestrates multiple vite builds to bundle an extension, plugins will be executed multiple times if necessary.
|
||||
|
||||
```ts
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
vite: () => ({
|
||||
plugins: [
|
||||
// ...
|
||||
],
|
||||
}),
|
||||
});
|
||||
```
|
||||
@@ -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
|
||||
|
Before Width: | Height: | Size: 487 KiB After Width: | Height: | Size: 472 KiB |
@@ -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 createContentScriptUi: typeof import('wxt/client')['createContentScriptUi']
|
||||
const defineBackground: typeof import('wxt/client')['defineBackground']
|
||||
const defineConfig: typeof import('wxt')['defineConfig']
|
||||
const defineContentScript: typeof import('wxt/client')['defineContentScript']
|
||||
}
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
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 ProjectRuntime 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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -193,77 +193,319 @@ describe('Manifest Content', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should group content scripts and styles together based on their matches and run_at', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'entrypoints/one.content/index.ts',
|
||||
`import "./style.css";
|
||||
describe('content_scripts', () => {
|
||||
it('should group content scripts and styles together based on their matches and run_at', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'entrypoints/one.content/index.ts',
|
||||
`import "./style.css";
|
||||
export default defineContentScript({
|
||||
matches: ["*://google.com/*"],
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
project.addFile('entrypoints/one.content/style.css', `body { color: red }`);
|
||||
project.addFile(
|
||||
'entrypoints/two.content/index.ts',
|
||||
`import "./style.css";
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/one.content/style.css',
|
||||
`body { color: red }`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/two.content/index.ts',
|
||||
`import "./style.css";
|
||||
export default defineContentScript({
|
||||
matches: ["*://google.com/*"],
|
||||
runAt: "document_end",
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/two.content/style.css',
|
||||
`body { color: green }`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/three.content/index.ts',
|
||||
`import "./style.css";
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/two.content/style.css',
|
||||
`body { color: green }`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/three.content/index.ts',
|
||||
`import "./style.css";
|
||||
export default defineContentScript({
|
||||
matches: ["*://google.com/*"],
|
||||
runAt: "document_end",
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/three.content/style.css',
|
||||
`body { color: blue }`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/four.content/index.ts',
|
||||
`import "./style.css";
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/three.content/style.css',
|
||||
`body { color: blue }`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/four.content/index.ts',
|
||||
`import "./style.css";
|
||||
export default defineContentScript({
|
||||
matches: ["*://duckduckgo.com/*"],
|
||||
runAt: "document_end",
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/four.content/style.css',
|
||||
`body { color: yellow }`,
|
||||
);
|
||||
|
||||
await project.build();
|
||||
|
||||
const manifest = await project.getOutputManifest();
|
||||
|
||||
expect(manifest.content_scripts).toContainEqual({
|
||||
matches: ['*://duckduckgo.com/*'],
|
||||
run_at: 'document_end',
|
||||
css: ['content-scripts/four.css'],
|
||||
js: ['content-scripts/four.js'],
|
||||
});
|
||||
expect(manifest.content_scripts).toContainEqual({
|
||||
matches: ['*://google.com/*'],
|
||||
run_at: 'document_end',
|
||||
css: ['content-scripts/three.css', 'content-scripts/two.css'],
|
||||
js: ['content-scripts/three.js', 'content-scripts/two.js'],
|
||||
});
|
||||
expect(manifest.content_scripts).toContainEqual({
|
||||
matches: ['*://google.com/*'],
|
||||
css: ['content-scripts/one.css'],
|
||||
js: ['content-scripts/one.js'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should add to any content scripts declared in wxt.config.ts', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'entrypoints/one.content/index.ts',
|
||||
`export default defineContentScript({
|
||||
matches: ["*://google.com/*"],
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/two.content/style.css',
|
||||
`body {
|
||||
background-color: red;
|
||||
}`,
|
||||
);
|
||||
project.setConfigFileConfig({
|
||||
manifest: {
|
||||
content_scripts: [
|
||||
{
|
||||
css: ['content-scripts/two.css'],
|
||||
matches: ['*://*.google.com/*'],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await project.build();
|
||||
|
||||
const manifest = await project.getOutputManifest();
|
||||
|
||||
expect(manifest.content_scripts).toContainEqual({
|
||||
css: ['content-scripts/two.css'],
|
||||
matches: ['*://*.google.com/*'],
|
||||
});
|
||||
expect(manifest.content_scripts).toContainEqual({
|
||||
matches: ['*://google.com/*'],
|
||||
js: ['content-scripts/one.js'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should add a CSS entry when cssInjectionMode is undefined', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'entrypoints/content/style.css',
|
||||
'body { background-color: red; }',
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/content/index.ts',
|
||||
`import "./style.css";
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ["https://*.google.com/*"],
|
||||
|
||||
main() {},
|
||||
});`,
|
||||
);
|
||||
await project.build();
|
||||
|
||||
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
|
||||
.toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"css\\":[\\"content-scripts/content.css\\"],\\"js\\":[\\"content-scripts/content.js\\"]}]}"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should add a CSS entry when cssInjectionMode is "manifest"', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'entrypoints/content/style.css',
|
||||
'body { background-color: red; }',
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/content/index.ts',
|
||||
`import "./style.css";
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ["https://*.google.com/*"],
|
||||
cssInjectionMode: "manifest",
|
||||
|
||||
main() {},
|
||||
});`,
|
||||
);
|
||||
await project.build();
|
||||
|
||||
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
|
||||
.toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"css\\":[\\"content-scripts/content.css\\"],\\"js\\":[\\"content-scripts/content.js\\"]}]}"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not add an entry for CSS when cssInjectionMode is "manual"', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'entrypoints/content/style.css',
|
||||
'body { background-color: red; }',
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/content/index.ts',
|
||||
`import "./style.css";
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ["https://*.google.com/*"],
|
||||
cssInjectionMode: "manual",
|
||||
|
||||
main() {},
|
||||
});`,
|
||||
);
|
||||
await project.build();
|
||||
|
||||
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
|
||||
.toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"js\\":[\\"content-scripts/content.js\\"]}]}"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not add a content script entry for CSS when cssInjectionMode is "ui", but add a web_accessible_resources entry for MV2', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'entrypoints/content/style.css',
|
||||
'body { background-color: red; }',
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/content/index.ts',
|
||||
`import "./style.css";
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ["https://*.google.com/*"],
|
||||
cssInjectionMode: "ui",
|
||||
|
||||
main() {},
|
||||
});`,
|
||||
);
|
||||
await project.build({
|
||||
manifestVersion: 2,
|
||||
});
|
||||
|
||||
expect(await project.serializeFile('.output/chrome-mv2/manifest.json'))
|
||||
.toMatchInlineSnapshot(`
|
||||
".output/chrome-mv2/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":2,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"js\\":[\\"content-scripts/content.js\\"]}],\\"web_accessible_resources\\":[\\"content-scripts/content.css\\"]}"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not add a content script entry for CSS when cssInjectionMode is "ui", but add a web_accessible_resources entry for MV3', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'entrypoints/content/style.css',
|
||||
'body { background-color: red; }',
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/content/index.ts',
|
||||
`import "./style.css";
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ["https://*.google.com/*"],
|
||||
cssInjectionMode: "ui",
|
||||
|
||||
main() {},
|
||||
});`,
|
||||
);
|
||||
await project.build({
|
||||
manifestVersion: 3,
|
||||
});
|
||||
|
||||
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
|
||||
.toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"js\\":[\\"content-scripts/content.js\\"]}],\\"web_accessible_resources\\":[{\\"resources\\":[\\"content-scripts/content.css\\"],\\"matches\\":[\\"https://*.google.com/*\\"]}]}"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
it('should combine web accessible resources', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'entrypoints/content/style.css',
|
||||
'body { background-color: red; }',
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/four.content/style.css',
|
||||
`body { color: yellow }`,
|
||||
'entrypoints/content/index.ts',
|
||||
`import "./style.css";
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ["https://*.google.com/*"],
|
||||
cssInjectionMode: "ui",
|
||||
|
||||
main() {},
|
||||
});`,
|
||||
);
|
||||
project.setConfigFileConfig({
|
||||
manifest: {
|
||||
web_accessible_resources: [
|
||||
{ resources: ['one.png'], matches: ['https://one.com/*'] },
|
||||
],
|
||||
},
|
||||
});
|
||||
await project.build();
|
||||
|
||||
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
|
||||
.toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"web_accessible_resources\\":[{\\"resources\\":[\\"one.png\\"],\\"matches\\":[\\"https://one.com/*\\"]},{\\"resources\\":[\\"content-scripts/content.css\\"],\\"matches\\":[\\"https://*.google.com/*\\"]}],\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"js\\":[\\"content-scripts/content.js\\"]}]}"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should respect the transformManifest option', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'wxt.config.ts',
|
||||
`import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
transformManifest(manifest) {
|
||||
manifest.author = "Custom Author"
|
||||
}
|
||||
})`,
|
||||
);
|
||||
|
||||
await project.build();
|
||||
|
||||
const manifest = await project.getOutputManifest();
|
||||
|
||||
expect(manifest.content_scripts).toContainEqual({
|
||||
matches: ['*://duckduckgo.com/*'],
|
||||
run_at: 'document_end',
|
||||
css: ['assets/four.css'],
|
||||
js: ['content-scripts/four.js'],
|
||||
});
|
||||
expect(manifest.content_scripts).toContainEqual({
|
||||
matches: ['*://google.com/*'],
|
||||
run_at: 'document_end',
|
||||
css: ['assets/three.css', 'assets/two.css'],
|
||||
js: ['content-scripts/three.js', 'content-scripts/two.js'],
|
||||
});
|
||||
expect(manifest.content_scripts).toContainEqual({
|
||||
matches: ['*://google.com/*'],
|
||||
css: ['assets/one.css'],
|
||||
js: ['content-scripts/one.js'],
|
||||
});
|
||||
const output = await project.serializeFile(
|
||||
'.output/chrome-mv3/manifest.json',
|
||||
);
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"author\\":\\"Custom Author\\"}"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -43,30 +43,33 @@ describe('Output Directory Structure', () => {
|
||||
|
||||
await project.build();
|
||||
|
||||
expect(await project.serializeOutput()).toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/assets/one.css
|
||||
expect(
|
||||
await project.serializeOutput([
|
||||
'.output/chrome-mv3/content-scripts/one.js',
|
||||
'.output/chrome-mv3/content-scripts/two.js',
|
||||
]),
|
||||
).toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/content-scripts/one.css
|
||||
----------------------------------------
|
||||
body{color:#00f}
|
||||
|
||||
================================================================================
|
||||
.output/chrome-mv3/assets/two.css
|
||||
.output/chrome-mv3/content-scripts/one.js
|
||||
----------------------------------------
|
||||
<contents-ignored>
|
||||
================================================================================
|
||||
.output/chrome-mv3/content-scripts/two.css
|
||||
----------------------------------------
|
||||
body{color:red}
|
||||
|
||||
================================================================================
|
||||
.output/chrome-mv3/content-scripts/one.js
|
||||
----------------------------------------
|
||||
(function(){\\"use strict\\";function i(n){return n}const s=\\"\\",o={matches:[\\"*://*/*\\"],main:()=>{}};function t(n,...e){if(typeof e[0]==\\"string\\"){const c=e.shift();n(\`[wxt] \${c}\`,...e)}else n(\\"[wxt]\\",...e)}var r={debug:(...n)=>t(console.debug,...n),log:(...n)=>t(console.log,...n),warn:(...n)=>t(console.warn,...n),error:(...n)=>t(console.error,...n)};(async()=>{try{await o.main()}catch(n){r.error(\\"The content script crashed on startup!\\",n)}})()})();
|
||||
|
||||
================================================================================
|
||||
.output/chrome-mv3/content-scripts/two.js
|
||||
----------------------------------------
|
||||
(function(){\\"use strict\\";function i(n){return n}const s=\\"\\",o={matches:[\\"*://*/*\\"],main:()=>{}};function t(n,...e){if(typeof e[0]==\\"string\\"){const c=e.shift();n(\`[wxt] \${c}\`,...e)}else n(\\"[wxt]\\",...e)}var r={debug:(...n)=>t(console.debug,...n),log:(...n)=>t(console.log,...n),warn:(...n)=>t(console.warn,...n),error:(...n)=>t(console.error,...n)};(async()=>{try{await o.main()}catch(n){r.error(\\"The content script crashed on startup!\\",n)}})()})();
|
||||
|
||||
<contents-ignored>
|
||||
================================================================================
|
||||
.output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":[\\"*://*/*\\"],\\"css\\":[\\"assets/one.css\\",\\"assets/two.css\\"],\\"js\\":[\\"content-scripts/one.js\\",\\"content-scripts/two.js\\"]}]}"
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":[\\"*://*/*\\"],\\"css\\":[\\"content-scripts/one.css\\",\\"content-scripts/two.css\\"],\\"js\\":[\\"content-scripts/one.js\\",\\"content-scripts/two.js\\"]}]}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -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\\"]}]}"
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import { TestProject } from '../utils';
|
||||
|
||||
describe('Remote Code', () => {
|
||||
it('should download "url:*" modules and include them in the final bundle', async () => {
|
||||
const url = 'https://stats.aklinker1.io/umami.js';
|
||||
const url = 'https://code.jquery.com/jquery-3.7.1.slim.min.js';
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.ts', `import "url:${url}"`);
|
||||
|
||||
@@ -12,7 +12,7 @@ describe('Remote Code', () => {
|
||||
const output = await project.serializeFile('.output/chrome-mv3/popup.js');
|
||||
expect(output).toContain(
|
||||
// Some text that will hopefully be in future versions of this script
|
||||
'umami.disabled',
|
||||
'jQuery v3.7.1',
|
||||
);
|
||||
expect(output).not.toContain(url);
|
||||
expect(
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { TestProject } from '../utils';
|
||||
|
||||
describe('TypeScript Project', () => {
|
||||
it('should generate defined constants correctly', async () => {
|
||||
const project = new TestProject();
|
||||
|
||||
await project.build();
|
||||
|
||||
const output = await project.serializeFile('.wxt/types/globals.d.ts');
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
".wxt/types/globals.d.ts
|
||||
----------------------------------------
|
||||
// Generated by wxt
|
||||
export {}
|
||||
declare global {
|
||||
const __MANIFEST_VERSION__: 2 | 3;
|
||||
const __BROWSER__: string;
|
||||
const __IS_CHROME__: boolean;
|
||||
const __IS_FIREFOX__: boolean;
|
||||
const __IS_SAFARI__: boolean;
|
||||
const __IS_EDGE__: boolean;
|
||||
const __IS_OPERA__: boolean;
|
||||
const __COMMAND__: \\"build\\" | \\"serve\\";
|
||||
const __ENTRYPOINT__: string;
|
||||
}
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should augment the types for browser.runtime.getURL', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html');
|
||||
project.addFile('entrypoints/options.html');
|
||||
project.addFile('entrypoints/sandbox.html');
|
||||
|
||||
await project.build();
|
||||
|
||||
const output = await project.serializeFile('.wxt/types/paths.d.ts');
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
".wxt/types/paths.d.ts
|
||||
----------------------------------------
|
||||
// Generated by wxt
|
||||
import \\"wxt/browser\\";
|
||||
|
||||
declare module \\"wxt/browser\\" {
|
||||
type PublicPath =
|
||||
| \\"/options.html\\"
|
||||
| \\"/popup.html\\"
|
||||
| \\"/sandbox.html\\"
|
||||
export interface WxtRuntime extends Runtime.Static {
|
||||
getURL(path: PublicPath): string;
|
||||
}
|
||||
}
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should augment the types for browser.i18n.getMessage', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'public/_locales/en/messages.json',
|
||||
JSON.stringify({
|
||||
prompt_for_name: {
|
||||
message: "What's your name?",
|
||||
description: "Ask for the user's name",
|
||||
},
|
||||
hello: {
|
||||
message: 'Hello, $USER$',
|
||||
description: 'Greet the user',
|
||||
placeholders: {
|
||||
user: {
|
||||
content: '$1',
|
||||
example: 'Cira',
|
||||
},
|
||||
},
|
||||
},
|
||||
bye: {
|
||||
message: 'Goodbye, $USER$. Come back to $OUR_SITE$ soon!',
|
||||
description: 'Say goodbye to the user',
|
||||
placeholders: {
|
||||
our_site: {
|
||||
content: 'Example.com',
|
||||
},
|
||||
user: {
|
||||
content: '$1',
|
||||
example: 'Cira',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
project.setConfigFileConfig({
|
||||
manifest: {
|
||||
default_locale: 'en',
|
||||
},
|
||||
});
|
||||
|
||||
await project.build();
|
||||
|
||||
const output = await project.serializeFile('.wxt/types/i18n.d.ts');
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
".wxt/types/i18n.d.ts
|
||||
----------------------------------------
|
||||
// Generated by wxt
|
||||
import \\"wxt/browser\\";
|
||||
|
||||
declare module \\"wxt/browser\\" {
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/reference/i18n/#method-getMessage
|
||||
*/
|
||||
interface GetMessageOptions {
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/reference/i18n/#method-getMessage
|
||||
*/
|
||||
escapeLt?: boolean
|
||||
}
|
||||
|
||||
export interface WxtI18n extends I18n.Static {
|
||||
/**
|
||||
* The extension or app ID; you might use this string to construct URLs for resources inside the extension. Even unlocalized extensions can use this message.
|
||||
Note: You can't use this message in a manifest file.
|
||||
*
|
||||
* \\"<browser.runtime.id>\\"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: \\"@@extension_id\\",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
*
|
||||
*
|
||||
* \\"<browser.i18n.getUiLocale()>\\"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: \\"@@ui_locale\\",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
* The text direction for the current locale, either \\"ltr\\" for left-to-right languages such as English or \\"rtl\\" for right-to-left languages such as Japanese.
|
||||
*
|
||||
* \\"<ltr|rtl>\\"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: \\"@@bidi_dir\\",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
* If the @@bidi_dir is \\"ltr\\", then this is \\"rtl\\"; otherwise, it's \\"ltr\\".
|
||||
*
|
||||
* \\"<rtl|ltr>\\"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: \\"@@bidi_reversed_dir\\",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
* If the @@bidi_dir is \\"ltr\\", then this is \\"left\\"; otherwise, it's \\"right\\".
|
||||
*
|
||||
* \\"<left|right>\\"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: \\"@@bidi_start_edge\\",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
* If the @@bidi_dir is \\"ltr\\", then this is \\"right\\"; otherwise, it's \\"left\\".
|
||||
*
|
||||
* \\"<right|left>\\"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: \\"@@bidi_end_edge\\",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
* Ask for the user's name
|
||||
*
|
||||
* \\"What's your name?\\"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: \\"prompt_for_name\\",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
* Greet the user
|
||||
*
|
||||
* \\"Hello, $USER$\\"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: \\"hello\\",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
* Say goodbye to the user
|
||||
*
|
||||
* \\"Goodbye, $USER$. Come back to $OUR_SITE$ soon!\\"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: \\"bye\\",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
}
|
||||
}
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should reference all the required types in a single declaration file', async () => {
|
||||
const project = new TestProject();
|
||||
|
||||
await project.build();
|
||||
|
||||
const output = await project.serializeFile('.wxt/wxt.d.ts');
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
".wxt/wxt.d.ts
|
||||
----------------------------------------
|
||||
// Generated by wxt
|
||||
/// <reference types=\\"vite/client\\" />
|
||||
/// <reference types=\\"./types/imports.d.ts\\" />
|
||||
/// <reference types=\\"./types/paths.d.ts\\" />
|
||||
/// <reference types=\\"./types/i18n.d.ts\\" />
|
||||
/// <reference types=\\"./types/globals.d.ts\\" />
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should generate a TSConfig file for the project', async () => {
|
||||
const project = new TestProject();
|
||||
|
||||
await project.build();
|
||||
|
||||
const output = await project.serializeFile('.wxt/tsconfig.json');
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
".wxt/tsconfig.json
|
||||
----------------------------------------
|
||||
{
|
||||
\\"compilerOptions\\": {
|
||||
\\"target\\": \\"ESNext\\",
|
||||
\\"module\\": \\"ESNext\\",
|
||||
\\"moduleResolution\\": \\"Bundler\\",
|
||||
\\"noEmit\\": true,
|
||||
\\"esModuleInterop\\": true,
|
||||
\\"forceConsistentCasingInFileNames\\": true,
|
||||
\\"resolveJsonModule\\": true,
|
||||
\\"strict\\": true,
|
||||
\\"lib\\": [\\"DOM\\", \\"WebWorker\\"],
|
||||
\\"skipLibCheck\\": true,
|
||||
\\"paths\\": {
|
||||
\\"@\\": [\\"..\\"],
|
||||
\\"@/*\\": [\\"../*\\"],
|
||||
\\"~\\": [\\"..\\"],
|
||||
\\"~/*\\": [\\"../*\\"],
|
||||
\\"@@\\": [\\"..\\"],
|
||||
\\"@@/*\\": [\\"../*\\"],
|
||||
\\"~~\\": [\\"..\\"],
|
||||
\\"~~/*\\": [\\"../*\\"]
|
||||
}
|
||||
},
|
||||
\\"include\\": [
|
||||
\\"../**/*\\",
|
||||
\\"./wxt.d.ts\\"
|
||||
],
|
||||
\\"exclude\\": [\\"../.output\\"]
|
||||
}"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should generate correct path aliases for a custom srcDir', async () => {
|
||||
const project = new TestProject();
|
||||
project.setConfigFileConfig({
|
||||
srcDir: 'src',
|
||||
});
|
||||
|
||||
await project.build();
|
||||
|
||||
const output = await project.serializeFile('.wxt/tsconfig.json');
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
".wxt/tsconfig.json
|
||||
----------------------------------------
|
||||
{
|
||||
\\"compilerOptions\\": {
|
||||
\\"target\\": \\"ESNext\\",
|
||||
\\"module\\": \\"ESNext\\",
|
||||
\\"moduleResolution\\": \\"Bundler\\",
|
||||
\\"noEmit\\": true,
|
||||
\\"esModuleInterop\\": true,
|
||||
\\"forceConsistentCasingInFileNames\\": true,
|
||||
\\"resolveJsonModule\\": true,
|
||||
\\"strict\\": true,
|
||||
\\"lib\\": [\\"DOM\\", \\"WebWorker\\"],
|
||||
\\"skipLibCheck\\": true,
|
||||
\\"paths\\": {
|
||||
\\"@\\": [\\"../src\\"],
|
||||
\\"@/*\\": [\\"../src/*\\"],
|
||||
\\"~\\": [\\"../src\\"],
|
||||
\\"~/*\\": [\\"../src/*\\"],
|
||||
\\"@@\\": [\\"..\\"],
|
||||
\\"@@/*\\": [\\"../*\\"],
|
||||
\\"~~\\": [\\"..\\"],
|
||||
\\"~~/*\\": [\\"../*\\"]
|
||||
}
|
||||
},
|
||||
\\"include\\": [
|
||||
\\"../**/*\\",
|
||||
\\"./wxt.d.ts\\"
|
||||
],
|
||||
\\"exclude\\": [\\"../.output\\"]
|
||||
}"
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -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/project') {
|
||||
// We can't put each test's project inside e2e/project directly, otherwise the wxt.config.ts
|
||||
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`);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wxt",
|
||||
"type": "module",
|
||||
"version": "0.3.2",
|
||||
"version": "0.7.1-alpha1",
|
||||
"description": "Next gen framework for developing web extensions",
|
||||
"engines": {
|
||||
"node": ">=18.16.0",
|
||||
@@ -9,8 +9,9 @@
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/aklinker1/wxt"
|
||||
"url": "https://github.com/wxt-dev/wxt"
|
||||
},
|
||||
"homepage": "https://wxt.dev",
|
||||
"keywords": [
|
||||
"vite",
|
||||
"chrome",
|
||||
@@ -55,36 +56,42 @@
|
||||
"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",
|
||||
"docs:build": "vitepress build docs",
|
||||
"docs:preview": "vitepress preview docs"
|
||||
"docs:preview": "vitepress preview docs",
|
||||
"sync-releases": "pnpx changelogen@latest gh release"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/webextension-polyfill": "^0.10.1",
|
||||
"@webext-core/fake-browser": "^1.2.2",
|
||||
"@webext-core/match-patterns": "^1.0.1",
|
||||
"@webext-core/isolated-element": "^1.0.4",
|
||||
"@webext-core/match-patterns": "^1.0.2",
|
||||
"async-mutex": "^0.4.0",
|
||||
"c12": "^1.4.2",
|
||||
"cac": "^6.7.14",
|
||||
"consola": "^3.2.3",
|
||||
"esbuild": "^0.19.4",
|
||||
"fast-glob": "^3.3.1",
|
||||
"filesize": "^10.0.8",
|
||||
"fs-extra": "^11.1.1",
|
||||
"get-port": "^7.0.0",
|
||||
"giget": "^1.1.2",
|
||||
"immer": "^10.0.2",
|
||||
"is-wsl": "^3.0.0",
|
||||
"jiti": "^1.19.1",
|
||||
"json5": "^2.2.3",
|
||||
"linkedom": "^0.14.26",
|
||||
"linkedom": "^0.15.1",
|
||||
"minimatch": "^9.0.3",
|
||||
"ora": "^7.0.1",
|
||||
"picocolors": "^1.0.0",
|
||||
"prompts": "^2.4.2",
|
||||
"rollup-plugin-visualizer": "^5.9.2",
|
||||
"unimport": "^3.1.0",
|
||||
"vite": "^4.4.7",
|
||||
"vite-tsconfig-paths": "^4.2.0",
|
||||
"web-ext": "^7.6.2",
|
||||
"web-ext-run": "^0.1.0",
|
||||
"webextension-polyfill": "^0.10.0",
|
||||
"zip-dir": "^2.0.0"
|
||||
},
|
||||
@@ -94,21 +101,22 @@
|
||||
"@types/lodash.merge": "^4.6.7",
|
||||
"@types/node": "^20.4.5",
|
||||
"@types/prompts": "^2.4.4",
|
||||
"@vitest/coverage-v8": "^0.32.4",
|
||||
"@vitest/coverage-v8": "^0.34.1",
|
||||
"execa": "^7.2.0",
|
||||
"jsdom": "^22.1.0",
|
||||
"lint-staged": "^14.0.0",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"ora": "^6.3.1",
|
||||
"prettier": "^2.8.8",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"prettier": "^3.0.1",
|
||||
"simple-git-hooks": "^2.9.0",
|
||||
"ts-morph": "^19.0.0",
|
||||
"tsup": "^7.1.0",
|
||||
"tsx": "^3.12.7",
|
||||
"typescript": "^5.1.6",
|
||||
"vitepress": "1.0.0-beta.5",
|
||||
"vitest": "^0.32.4",
|
||||
"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": {
|
||||
@@ -116,16 +124,21 @@
|
||||
},
|
||||
"packageManager": "pnpm@8.6.3",
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "pnpm pretty-quick --staged"
|
||||
"pre-commit": "pnpm lint-staged"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*": "prettier --write"
|
||||
},
|
||||
"changelog": {
|
||||
"excludeAuthors": [
|
||||
"aaronklinker1@gmail.com"
|
||||
]
|
||||
},
|
||||
"pnpm": {
|
||||
"peerDependencyRules": {
|
||||
"ignoreMissing": [
|
||||
"@algolia/client-search",
|
||||
"search-insights"
|
||||
],
|
||||
"allowAny": [
|
||||
"node-fetch"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
declare module 'web-ext' {
|
||||
declare module 'web-ext-run' {
|
||||
export interface WebExtRunInstance {
|
||||
reloadAllExtensions(): Promise<void>;
|
||||
exit(): Promise<void>;
|
||||
@@ -12,7 +12,7 @@ declare module 'web-ext' {
|
||||
export default webExt;
|
||||
}
|
||||
|
||||
declare module 'web-ext/util/logger' {
|
||||
declare module 'web-ext-run/util/logger' {
|
||||
// https://github.com/mozilla/web-ext/blob/e37e60a2738478f512f1255c537133321f301771/src/util/logger.js#L43
|
||||
export interface IConsoleStream {
|
||||
stopCapturing(): void;
|
||||
@@ -10,16 +10,21 @@ export const build = defineCommand<
|
||||
browser?: wxt.TargetBrowser;
|
||||
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);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { defineCommand } from '../utils/defineCommand';
|
||||
import * as wxt from '../..';
|
||||
|
||||
export const clean = defineCommand<
|
||||
[
|
||||
root: string | undefined,
|
||||
flags: {
|
||||
debug?: boolean;
|
||||
},
|
||||
]
|
||||
>(wxt.clean);
|
||||
@@ -10,16 +10,17 @@ export const dev = defineCommand<
|
||||
browser?: wxt.TargetBrowser;
|
||||
mv3?: boolean;
|
||||
mv2?: boolean;
|
||||
debug?: boolean;
|
||||
},
|
||||
]
|
||||
>(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,
|
||||
debug: flags.debug,
|
||||
};
|
||||
|
||||
const server = await wxt.createServer(cliConfig);
|
||||
|
||||
@@ -4,3 +4,4 @@ export * from './init';
|
||||
export * from './prepare';
|
||||
export * from './publish';
|
||||
export * from './zip';
|
||||
export * from './clean';
|
||||
|
||||
@@ -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';
|
||||
@@ -9,7 +8,10 @@ import pc from 'picocolors';
|
||||
import { Formatter } from 'picocolors/types';
|
||||
|
||||
export const init = defineCommand<
|
||||
[directory: string | undefined, options: { template?: string; pm?: string }]
|
||||
[
|
||||
directory: string | undefined,
|
||||
options: { template?: string; pm?: string; debug?: boolean },
|
||||
]
|
||||
>(
|
||||
async (userDirectory, flags) => {
|
||||
consola.info('Initalizing new project');
|
||||
@@ -69,7 +71,7 @@ export const init = defineCommand<
|
||||
console.log();
|
||||
consola.log('Next steps:');
|
||||
let step = 0;
|
||||
if (cdPath !== '.') consola.log(` ${++step}.`, pc.cyan(`cd ${cdPath}`));
|
||||
if (cdPath !== '') consola.log(` ${++step}.`, pc.cyan(`cd ${cdPath}`));
|
||||
consola.log(` ${++step}.`, pc.cyan(`${input.packageManager} install`));
|
||||
console.log();
|
||||
},
|
||||
@@ -90,7 +92,7 @@ interface Template {
|
||||
async function listTemplates(): Promise<Template[]> {
|
||||
try {
|
||||
const res = await fetch(
|
||||
'https://api.github.com/repos/aklinker1/wxt/contents/templates',
|
||||
'https://api.github.com/repos/wxt-dev/wxt/contents/templates',
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/vnd.github+json',
|
||||
@@ -130,10 +132,11 @@ async function cloneProject({
|
||||
template: Template;
|
||||
packageManager: string;
|
||||
}) {
|
||||
const { default: ora } = await import('ora');
|
||||
const spinner = ora('Downloading template').start();
|
||||
try {
|
||||
// 1. Clone repo
|
||||
await downloadTemplate(`gh:aklinker1/wxt/${template.path}`, {
|
||||
await downloadTemplate(`gh:wxt-dev/wxt/${template.path}`, {
|
||||
dir: directory,
|
||||
force: true,
|
||||
});
|
||||
|
||||
@@ -9,12 +9,14 @@ export const prepare = defineCommand<
|
||||
root: string | undefined,
|
||||
flags: {
|
||||
config?: string;
|
||||
debug?: boolean;
|
||||
},
|
||||
]
|
||||
>(async (root, flags) => {
|
||||
const cliConfig: wxt.InlineConfig = {
|
||||
root,
|
||||
configFile: flags.config,
|
||||
debug: flags.debug,
|
||||
};
|
||||
const config = await getInternalConfig(cliConfig, 'build');
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@ import { consola } from 'consola';
|
||||
import { defineCommand } from '../utils/defineCommand';
|
||||
|
||||
export const publish = defineCommand(
|
||||
async (root: any, { config: configFile }: any) => {
|
||||
async (
|
||||
root: any,
|
||||
{ config: configFile, debug }: { config?: string; debug?: string },
|
||||
) => {
|
||||
consola.warn('wxt publish: Not implemented');
|
||||
},
|
||||
);
|
||||
|
||||
@@ -13,16 +13,17 @@ export const zip = defineCommand<
|
||||
browser?: wxt.TargetBrowser;
|
||||
mv3?: boolean;
|
||||
mv2?: boolean;
|
||||
debug?: 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,
|
||||
};
|
||||
|
||||
const config = await getInternalConfig(cliConfig, 'build');
|
||||
|
||||
@@ -6,6 +6,8 @@ const cli = cac('wxt');
|
||||
cli.help();
|
||||
cli.version(version);
|
||||
|
||||
cli.option('--debug', 'enable debug mode');
|
||||
|
||||
// DEV
|
||||
cli
|
||||
.command('[root]', 'start dev server')
|
||||
@@ -24,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
|
||||
@@ -38,10 +41,16 @@ cli
|
||||
|
||||
// PREPARE
|
||||
cli
|
||||
.command('prepare [root]', 'prepare')
|
||||
.command('prepare [root]', 'prepare typescript project')
|
||||
.option('-c, --config <file>', 'use specified config file')
|
||||
.action(commands.prepare);
|
||||
|
||||
// CLEAN
|
||||
cli
|
||||
.command('clean [root]', 'clean generated files and caches')
|
||||
.alias('cleanup')
|
||||
.action(commands.clean);
|
||||
|
||||
// PUBLISH
|
||||
cli.command('publish [root]', 'publish to stores').action(commands.publish);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { consola } from 'consola';
|
||||
import { LogLevels, consola } from 'consola';
|
||||
import { printHeader } from '../../core/log/printHeader';
|
||||
import { formatDuration } from '../../core/utils/formatDuration';
|
||||
|
||||
@@ -9,6 +9,13 @@ export function defineCommand<TArgs extends any[]>(
|
||||
},
|
||||
) {
|
||||
return async (...args: TArgs) => {
|
||||
// Enable consola's debug mode globally at the start of all commands when the `--debug` flag is
|
||||
// passed
|
||||
const isDebug = !!args.find((arg) => arg?.debug);
|
||||
if (isDebug) {
|
||||
consola.level = LogLevels.debug;
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
printHeader();
|
||||
|
||||
@@ -0,0 +1,336 @@
|
||||
/** @vitest-environment jsdom */
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { ContentScriptContext, createContentScriptUi } from '..';
|
||||
import { createIsolatedElement } from '@webext-core/isolated-element';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { browser } from '../browser';
|
||||
|
||||
vi.mock('webextension-polyfill', () =>
|
||||
vi.importActual('../virtual-modules/fake-browser'),
|
||||
);
|
||||
|
||||
vi.mock('@webext-core/isolated-element', async () => {
|
||||
const { vi } = await import('vitest');
|
||||
return {
|
||||
createIsolatedElement: vi.fn(),
|
||||
};
|
||||
});
|
||||
const createIsolatedElementMock = vi.mocked(createIsolatedElement);
|
||||
|
||||
const testApp = (container: Element) => {
|
||||
const app = document.createElement('div');
|
||||
app.textContent = 'App';
|
||||
container.append(app);
|
||||
};
|
||||
|
||||
const createCtx = () => new ContentScriptContext('test');
|
||||
|
||||
const fetch = vi.fn();
|
||||
|
||||
describe('createContentScriptUi', () => {
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = `
|
||||
<div id="parent">
|
||||
<p id="one">one</p>
|
||||
<p id="two">two</p>
|
||||
<p id="three"></p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
window.fetch = fetch;
|
||||
fetch.mockResolvedValue({ text: () => Promise.resolve('') });
|
||||
|
||||
createIsolatedElementMock.mockImplementation(async (config) => {
|
||||
const parentElement = document.createElement(config.name);
|
||||
const isolatedElement = document.createElement('html');
|
||||
parentElement.append(isolatedElement);
|
||||
|
||||
return {
|
||||
isolatedElement,
|
||||
parentElement: parentElement,
|
||||
shadow: mock<ShadowRoot>({
|
||||
querySelector: (selector: string) => {
|
||||
if (selector === 'html') return isolatedElement;
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
describe('css', () => {
|
||||
it('should load the CSS for the current entrypoint when cssInjectionMode=ui', async () => {
|
||||
fetch.mockResolvedValue({ text: () => Promise.resolve('body {}') });
|
||||
const ctx = new ContentScriptContext('test', {
|
||||
matches: [],
|
||||
cssInjectionMode: 'ui',
|
||||
});
|
||||
|
||||
await createContentScriptUi(ctx, {
|
||||
name: 'test',
|
||||
type: 'inline',
|
||||
mount: testApp,
|
||||
});
|
||||
|
||||
expect(fetch).toBeCalledTimes(1);
|
||||
expect(fetch).toBeCalledWith(
|
||||
`chrome-extension://${browser.runtime.id}/content-scripts/${__ENTRYPOINT__}.css`,
|
||||
);
|
||||
expect(createIsolatedElementMock).toBeCalledTimes(1);
|
||||
expect(createIsolatedElementMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
css: {
|
||||
textContent: 'body {}',
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should still load the UI when fetch fails to load CSS file when cssInjectionMode=ui', async () => {
|
||||
const error = Error('Test fetch error');
|
||||
fetch.mockRejectedValue(error);
|
||||
const ctx = new ContentScriptContext('test', {
|
||||
matches: [],
|
||||
cssInjectionMode: 'ui',
|
||||
});
|
||||
|
||||
await createContentScriptUi(ctx, {
|
||||
name: 'test',
|
||||
type: 'inline',
|
||||
mount: testApp,
|
||||
});
|
||||
|
||||
expect(fetch).toBeCalledTimes(1);
|
||||
expect(fetch).toBeCalledWith(
|
||||
`chrome-extension://${browser.runtime.id}/content-scripts/${__ENTRYPOINT__}.css`,
|
||||
);
|
||||
expect(createIsolatedElementMock).toBeCalledTimes(1);
|
||||
expect(createIsolatedElementMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
css: {
|
||||
textContent: '',
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.each(['manifest', 'manual'] as const)(
|
||||
'should not fetch CSS when cssInjectionMode=%s',
|
||||
async (cssInjectionMode) => {
|
||||
const ctx = new ContentScriptContext('test', {
|
||||
matches: [],
|
||||
cssInjectionMode,
|
||||
});
|
||||
|
||||
await createContentScriptUi(ctx, {
|
||||
name: 'test',
|
||||
type: 'inline',
|
||||
mount: testApp,
|
||||
});
|
||||
|
||||
expect(fetch).not.toBeCalled();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('mount', () => {
|
||||
describe('append option', () => {
|
||||
it.each([undefined, 'last' as const])(
|
||||
'should append the element as the last child to the anchor when append=%s',
|
||||
async (append) => {
|
||||
const ui = await createContentScriptUi(createCtx(), {
|
||||
name: 'test-app',
|
||||
type: 'inline',
|
||||
anchor: '#parent',
|
||||
append,
|
||||
mount: testApp,
|
||||
});
|
||||
ui.mount();
|
||||
|
||||
expect(
|
||||
document.querySelector('#parent > :nth-child(4)')?.tagName,
|
||||
).toEqual('TEST-APP');
|
||||
},
|
||||
);
|
||||
|
||||
it('should append the element as the first child to the anchor when append=first', async () => {
|
||||
const ui = await createContentScriptUi(createCtx(), {
|
||||
name: 'test-app',
|
||||
type: 'inline',
|
||||
anchor: '#parent',
|
||||
append: 'first',
|
||||
mount: testApp,
|
||||
});
|
||||
ui.mount();
|
||||
|
||||
expect(
|
||||
document.querySelector('#parent > :nth-child(1)')?.tagName,
|
||||
).toEqual('TEST-APP');
|
||||
});
|
||||
|
||||
it('should append the element normally when append=first but there are no other children', async () => {
|
||||
const ui = await createContentScriptUi(createCtx(), {
|
||||
name: 'test-app',
|
||||
type: 'inline',
|
||||
anchor: '#three',
|
||||
append: 'first',
|
||||
mount: testApp,
|
||||
});
|
||||
ui.mount();
|
||||
|
||||
expect(
|
||||
document.querySelector('#three > :nth-child(1)')?.tagName,
|
||||
).toEqual('TEST-APP');
|
||||
});
|
||||
|
||||
it('should replace the anchor with the element when append=replace', async () => {
|
||||
const ui = await createContentScriptUi(createCtx(), {
|
||||
name: 'test-app',
|
||||
type: 'inline',
|
||||
anchor: '#two',
|
||||
append: 'replace',
|
||||
mount: testApp,
|
||||
});
|
||||
ui.mount();
|
||||
|
||||
expect(
|
||||
document.querySelector('#parent > :nth-child(2)')?.tagName,
|
||||
).toEqual('TEST-APP');
|
||||
});
|
||||
|
||||
it('should append the element after the anchor when append=before', async () => {
|
||||
const ui = await createContentScriptUi(createCtx(), {
|
||||
name: 'test-app',
|
||||
type: 'inline',
|
||||
anchor: '#two',
|
||||
append: 'before',
|
||||
mount: testApp,
|
||||
});
|
||||
ui.mount();
|
||||
|
||||
expect(
|
||||
document.querySelector('#parent > :nth-child(2)')?.tagName,
|
||||
).toEqual('TEST-APP');
|
||||
});
|
||||
|
||||
it('should append the element after the anchor when append=after', async () => {
|
||||
const ui = await createContentScriptUi(createCtx(), {
|
||||
name: 'test-app',
|
||||
type: 'inline',
|
||||
anchor: '#two',
|
||||
append: 'after',
|
||||
mount: testApp,
|
||||
});
|
||||
ui.mount();
|
||||
|
||||
expect(
|
||||
document.querySelector('#parent > :nth-child(3)')?.tagName,
|
||||
).toEqual('TEST-APP');
|
||||
});
|
||||
|
||||
it('should apply a custom function', async () => {
|
||||
const ui = await createContentScriptUi(createCtx(), {
|
||||
name: 'test-app',
|
||||
type: 'inline',
|
||||
anchor: '#three',
|
||||
append: (anchor, ui) => anchor.replaceWith(ui),
|
||||
mount: testApp,
|
||||
});
|
||||
ui.mount();
|
||||
|
||||
expect(
|
||||
document.querySelector('#parent > :nth-child(3)')?.tagName,
|
||||
).toEqual('TEST-APP');
|
||||
});
|
||||
});
|
||||
|
||||
it('should default the anchor to the body when unset', async () => {
|
||||
const ui = await createContentScriptUi(createCtx(), {
|
||||
name: 'test-app',
|
||||
type: 'inline',
|
||||
mount: testApp,
|
||||
});
|
||||
ui.mount();
|
||||
|
||||
expect(document.querySelector('body > :nth-child(2)')?.tagName).toEqual(
|
||||
'TEST-APP',
|
||||
);
|
||||
});
|
||||
|
||||
it.each(['#four', () => document.querySelector('#four')])(
|
||||
"should throw an error if the anchor doesn't exist",
|
||||
async (anchor) => {
|
||||
const ui = await createContentScriptUi(createCtx(), {
|
||||
name: 'test',
|
||||
type: 'inline',
|
||||
anchor,
|
||||
mount: testApp,
|
||||
});
|
||||
|
||||
expect(ui.mount).toThrow(
|
||||
'Failed to mount content script ui: could not find anchor element',
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it("should not fail if the ui hasn't been mounted", async () => {
|
||||
const ui = await createContentScriptUi(createCtx(), {
|
||||
name: 'test-app',
|
||||
type: 'inline',
|
||||
mount: testApp,
|
||||
});
|
||||
ui.remove();
|
||||
});
|
||||
|
||||
it('should automatically remove the UI when the context is invalidated', async () => {
|
||||
const ctx = createCtx();
|
||||
const ui = await createContentScriptUi(ctx, {
|
||||
name: 'test-app',
|
||||
type: 'inline',
|
||||
mount: testApp,
|
||||
});
|
||||
ui.mount();
|
||||
|
||||
expect(document.querySelector('test-app')).toBeDefined();
|
||||
|
||||
ctx.abort();
|
||||
expect(document.querySelector('test-app')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('type', () => {
|
||||
it.each(['inline', 'overlay', 'modal'] as const)(
|
||||
'should render type=%s',
|
||||
async (type) => {
|
||||
const ui = await createContentScriptUi(createCtx(), {
|
||||
name: 'test-app',
|
||||
type,
|
||||
mount: testApp,
|
||||
});
|
||||
ui.mount();
|
||||
|
||||
expect(document.querySelector('test-app')).toBeDefined();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('anchor', () => {
|
||||
it.each([
|
||||
() => '#two',
|
||||
() => () => '#two',
|
||||
() => document.querySelector('#two'),
|
||||
() => () => document.querySelector('#two'),
|
||||
])('should render anchor=%s', async (getAnchor) => {
|
||||
const ui = await createContentScriptUi(createCtx(), {
|
||||
name: 'test-app',
|
||||
type: 'inline',
|
||||
anchor: getAnchor(),
|
||||
mount: testApp,
|
||||
});
|
||||
ui.mount();
|
||||
|
||||
expect(document.querySelector('test-app')).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,15 @@
|
||||
import originalBrowser, { Browser, Runtime } from 'webextension-polyfill';
|
||||
import originalBrowser, { Browser, Runtime, I18n } from 'webextension-polyfill';
|
||||
|
||||
export interface AugmentedBrowser extends Browser {
|
||||
runtime: ProjectRuntime;
|
||||
runtime: WxtRuntime;
|
||||
i18n: WxtI18n;
|
||||
}
|
||||
|
||||
export interface ProjectRuntime extends Runtime.Static {
|
||||
export interface WxtRuntime extends Runtime.Static {
|
||||
// Overriden per-project
|
||||
}
|
||||
|
||||
export interface WxtI18n extends I18n.Static {
|
||||
// Overriden per-project
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
import { createIsolatedElement } from '@webext-core/isolated-element';
|
||||
import { browser } from './browser';
|
||||
import { logger } from './utils/logger';
|
||||
import { ContentScriptContext } from '.';
|
||||
|
||||
/**
|
||||
* Utility for mounting content script UI's with isolated styles. Automatically removed from the DOM
|
||||
* when the content script's context is invalidated.
|
||||
*
|
||||
* See <https://wxt.dev/guide/content-scripts.html#ui> for full documentation.
|
||||
*
|
||||
* @example
|
||||
* // entrypoints/example-ui.content/index.ts
|
||||
* import "./style.css"
|
||||
*
|
||||
* export default defineContentScript({
|
||||
* matches: ["*://*.google.com/*"],
|
||||
* cssInjectionMode: "ui",
|
||||
*
|
||||
* async main(ctx) {
|
||||
* const ui = await createContentScriptUi(ctx, {
|
||||
* name: "example-overlay",
|
||||
* type: "modal",
|
||||
* mount(container) {
|
||||
* const app = document.createElement("div");
|
||||
* app.textContent = "Content Script UI";
|
||||
* container.append(app);
|
||||
* }
|
||||
* })
|
||||
* ui.mount();
|
||||
* }
|
||||
* })
|
||||
*/
|
||||
export async function createContentScriptUi<T>(
|
||||
ctx: ContentScriptContext,
|
||||
options: ContentScriptUiOptions<T>,
|
||||
): Promise<ContentScriptUi<T>> {
|
||||
const css = [options.css ?? ''];
|
||||
if (ctx.options?.cssInjectionMode === 'ui') {
|
||||
css.push(await loadCss());
|
||||
}
|
||||
|
||||
const {
|
||||
isolatedElement: uiContainer,
|
||||
parentElement: shadowHost,
|
||||
shadow,
|
||||
} = await createIsolatedElement({
|
||||
name: options.name,
|
||||
css: {
|
||||
textContent: css.join('\n').trim(),
|
||||
},
|
||||
mode: 'open',
|
||||
});
|
||||
|
||||
const getAnchor = (): Element | undefined => {
|
||||
if (options.anchor == null) return document.body;
|
||||
|
||||
let resolved =
|
||||
typeof options.anchor === 'function' ? options.anchor() : options.anchor;
|
||||
if (typeof resolved === 'string')
|
||||
return document.querySelector<Element>(resolved) ?? undefined;
|
||||
return resolved ?? undefined;
|
||||
};
|
||||
|
||||
let mounted: T;
|
||||
|
||||
const mount = () => {
|
||||
const anchor = getAnchor();
|
||||
if (anchor == null)
|
||||
throw Error(
|
||||
'Failed to mount content script ui: could not find anchor element',
|
||||
);
|
||||
|
||||
// Mount UI inside shadow root
|
||||
mounted = options.mount(uiContainer);
|
||||
|
||||
// Add shadow root element to DOM
|
||||
switch (options.append) {
|
||||
case undefined:
|
||||
case 'last':
|
||||
anchor.append(shadowHost);
|
||||
break;
|
||||
case 'first':
|
||||
if (anchor.firstChild) {
|
||||
anchor.insertBefore(shadowHost, anchor.firstChild);
|
||||
} else {
|
||||
anchor.append(shadowHost);
|
||||
}
|
||||
break;
|
||||
case 'replace':
|
||||
anchor.replaceWith(shadowHost);
|
||||
break;
|
||||
case 'after':
|
||||
anchor.replaceWith(anchor, shadowHost);
|
||||
break;
|
||||
case 'before':
|
||||
anchor.replaceWith(shadowHost, anchor);
|
||||
break;
|
||||
default:
|
||||
options.append(anchor, shadowHost);
|
||||
break;
|
||||
}
|
||||
|
||||
// Apply types
|
||||
if (options.type !== 'inline') {
|
||||
if (options.zIndex != null)
|
||||
shadowHost.style.zIndex = String(options.zIndex);
|
||||
|
||||
shadowHost.style.overflow = 'visible';
|
||||
shadowHost.style.position = 'relative';
|
||||
shadowHost.style.width = '0';
|
||||
shadowHost.style.height = '0';
|
||||
shadowHost.style.display = 'block';
|
||||
|
||||
const html = shadow.querySelector('html')!;
|
||||
// HTML doesn't exist in tests
|
||||
if (options.type === 'overlay') {
|
||||
html.style.position = 'absolute';
|
||||
if (options.alignment?.startsWith('bottom-')) html.style.bottom = '0';
|
||||
else html.style.top = '0';
|
||||
|
||||
if (options.alignment?.endsWith('-right')) html.style.right = '0';
|
||||
else html.style.left = '0';
|
||||
} else {
|
||||
html.style.position = 'fixed';
|
||||
html.style.top = '0';
|
||||
html.style.bottom = '0';
|
||||
html.style.left = '0';
|
||||
html.style.right = '0';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const remove = () => {
|
||||
// Detatch shadow root from DOM
|
||||
shadowHost.remove();
|
||||
// Cleanup mounted state
|
||||
options.onRemove?.(mounted);
|
||||
// Remove children from uiContainer
|
||||
while (uiContainer.lastChild)
|
||||
uiContainer.removeChild(uiContainer.lastChild);
|
||||
};
|
||||
|
||||
ctx.onInvalidated(remove);
|
||||
|
||||
return {
|
||||
shadow,
|
||||
shadowHost,
|
||||
uiContainer,
|
||||
mount,
|
||||
remove,
|
||||
mounted: mounted!,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the CSS for the current entrypoint.
|
||||
*/
|
||||
async function loadCss(): Promise<string> {
|
||||
const url = browser.runtime.getURL(`/content-scripts/${__ENTRYPOINT__}.css`);
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
const css = await res.text();
|
||||
|
||||
// Replace :root selectors with :host since we're in a shadow root
|
||||
return css.replaceAll(':root', ':host');
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
`Failed to load styles @ ${url}. Did you forget to import the stylesheet in your entrypoint?`,
|
||||
err,
|
||||
);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export interface ContentScriptUi<T> {
|
||||
/**
|
||||
* The `HTMLElement` hosting the shadow root used to isolate the UI's styles. This is the element
|
||||
* that get's added to the DOM. This element's style is not isolated from the webpage.
|
||||
*/
|
||||
shadowHost: HTMLElement;
|
||||
/**
|
||||
* The container element inside the `ShadowRoot` whose styles are isolated. The UI is mounted
|
||||
* inside this `HTMLElement`.
|
||||
*/
|
||||
uiContainer: HTMLElement;
|
||||
/**
|
||||
* The shadow root performing the isolation.
|
||||
*/
|
||||
shadow: ShadowRoot;
|
||||
/**
|
||||
* Custom data returned from the `options.mount` function.
|
||||
*/
|
||||
mounted: T;
|
||||
/**
|
||||
* Function that mounts or remounts the UI on the page.
|
||||
*/
|
||||
mount: () => void;
|
||||
/**
|
||||
* Function that removes the UI from the webpage.
|
||||
*/
|
||||
remove: () => void;
|
||||
}
|
||||
|
||||
interface BaseContentScriptUiOptions<T> {
|
||||
/**
|
||||
* The name of the custom component used to host the ShadowRoot. Must be kebab-case.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* In combination with `anchor`, decide how to add the UI to the DOM.
|
||||
*
|
||||
* - `"last"` (default) - Add the UI as the last child of the `anchor` element
|
||||
* - `"first"` - Add the UI as the last child of the `anchor` element
|
||||
* - `"replace"` - Replace the `anchor` element with the UI.
|
||||
* - `"before"` - Add the UI as the sibling before the `anchor` element
|
||||
* - `"after"` - Add the UI as the sibling after the `anchor` element
|
||||
* - `(anchor, ui) => void` - Customizable function that let's you add the UI to the DOM
|
||||
*/
|
||||
append?: ContentScriptAppendMode | ((anchor: Element, ui: Element) => void);
|
||||
/**
|
||||
* A CSS selector, element, or function that returns one of the two. Along with `append`, the
|
||||
* `anchor` dictates where in the page the UI will be added.
|
||||
*/
|
||||
anchor?:
|
||||
| string
|
||||
| Element
|
||||
| null
|
||||
| undefined
|
||||
| (() => string | Element | null | undefined);
|
||||
/**
|
||||
* Callback executed when mounting the UI. This function should create and append the UI to the
|
||||
* `container` element. It is called every time `ui.mount()` is called
|
||||
*
|
||||
* Optionally return a value that can be accessed at `ui.mounted` or in the `onRemove` callback.
|
||||
*/
|
||||
mount: (container: Element) => T;
|
||||
/**
|
||||
* Callback called when the UI is removed from the webpage. Use to cleanup your UI, like
|
||||
* unmounting your vue or react apps.
|
||||
*/
|
||||
onRemove?: (mounted: T) => void;
|
||||
/**
|
||||
* Custom CSS text to apply to the UI. If your content script imports/generates CSS and you've
|
||||
* set `cssInjectionMode: "ui"`, the imported CSS will be included automatically. You do not need
|
||||
* to pass those styles in here. This is for any additional styles not in the imported CSS.
|
||||
*
|
||||
* See <https://wxt.dev/guide/content-scripts.html#ui> for more info.
|
||||
*/
|
||||
css?: string;
|
||||
}
|
||||
|
||||
export type OverlayContentScriptUiOptions<T> = BaseContentScriptUiOptions<T> & {
|
||||
type: 'overlay';
|
||||
/**
|
||||
* When using `type: "overlay"`, the mounted element is 0px by 0px in size. Alignment specifies
|
||||
* which corner is aligned with that 0x0 pixel space.
|
||||
*
|
||||
* @default "top-left"
|
||||
*/
|
||||
alignment?: ContentScriptUiOverlayAlignment;
|
||||
/**
|
||||
* The `z-index` used on the `shadowHost`. Set to a positive number to show your UI over website
|
||||
* content.
|
||||
*/
|
||||
zIndex?: number;
|
||||
};
|
||||
|
||||
export type ModalContentScriptUiOptions<T> = BaseContentScriptUiOptions<T> & {
|
||||
type: 'modal';
|
||||
/**
|
||||
* The `z-index` used on the `shadowHost`. Set to a positive number to show your UI over website
|
||||
* content.
|
||||
*/
|
||||
zIndex?: number;
|
||||
};
|
||||
|
||||
export type InlineContentScriptUiOptions<T> = BaseContentScriptUiOptions<T> & {
|
||||
type: 'inline';
|
||||
};
|
||||
|
||||
export type ContentScriptUiOverlayAlignment =
|
||||
| 'top-left'
|
||||
| 'top-right'
|
||||
| 'bottom-left'
|
||||
| 'bottom-right';
|
||||
|
||||
export type ContentScriptAppendMode =
|
||||
| 'last'
|
||||
| 'first'
|
||||
| 'replace'
|
||||
| 'before'
|
||||
| 'after';
|
||||
|
||||
export type ContentScriptUiOptions<T> =
|
||||
| OverlayContentScriptUiOptions<T>
|
||||
| ModalContentScriptUiOptions<T>
|
||||
| InlineContentScriptUiOptions<T>;
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './defineContentScript';
|
||||
export * from './defineBackground';
|
||||
export * from './mountContentScriptUi';
|
||||
export * from './createContentScriptUi';
|
||||
export * from './utils/ContentScriptContext';
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export function mountContentScriptUi() {
|
||||
throw Error('Not implemented: mountContentScriptUi');
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
import { ContentScriptDefinition } from '../../core/types';
|
||||
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,
|
||||
public readonly options?: Omit<ContentScriptDefinition, 'main'>,
|
||||
) {
|
||||
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,13 @@
|
||||
import definition from 'virtual:user-content-script';
|
||||
import { logger } from '../utils/logger';
|
||||
import { ContentScriptContext } from '../utils/ContentScriptContext';
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
await definition.main();
|
||||
const { main, ...options } = definition;
|
||||
const ctx = new ContentScriptContext(__ENTRYPOINT__, options);
|
||||
|
||||
await 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;
|
||||
|
||||
@@ -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.
|
||||
@@ -38,6 +41,7 @@ export async function buildInternal(
|
||||
await fs.ensureDir(config.outDir);
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
config.logger.debug('Detected entrypoints:', entrypoints);
|
||||
const groups = groupEntrypoints(entrypoints);
|
||||
const { output } = await rebuild(config, groups, undefined);
|
||||
|
||||
@@ -49,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;
|
||||
}
|
||||
|
||||
@@ -70,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) => {
|
||||
@@ -79,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],
|
||||
@@ -98,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,
|
||||
@@ -110,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);
|
||||
@@ -71,17 +84,29 @@ async function buildSingleEntrypoint(
|
||||
config.outDir,
|
||||
'.js',
|
||||
),
|
||||
// Output content script CSS to assets/ with a hash to prevent conflicts. Defaults to
|
||||
// "[name].[ext]" in lib mode, which usually results in "style.css". That means multiple
|
||||
// content scripts with styles would overwrite each other if it weren't changed below.
|
||||
assetFileNames: `assets/${entrypoint.name}.[ext]`,
|
||||
// Output content script CSS to `content-scripts/`, but all other scripts are written to
|
||||
// `assets/`.
|
||||
assetFileNames: ({ name }) => {
|
||||
if (entrypoint.type === 'content-script' && name?.endsWith('css')) {
|
||||
return `content-scripts/${entrypoint.name}.[ext]`;
|
||||
} else {
|
||||
return `assets/${entrypoint.name}.[ext]`;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
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 +141,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,12 +1,15 @@
|
||||
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';
|
||||
import { Message, parseI18nMessages } from '../utils/i18n';
|
||||
import { writeFileIfDifferent } from '../utils/fs';
|
||||
|
||||
/**
|
||||
* Generate and write all the files inside the `InternalConfig.typesDir` directory.
|
||||
@@ -18,8 +21,14 @@ 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));
|
||||
|
||||
const mainReference = await writeMainDeclarationFile(references, config);
|
||||
@@ -28,14 +37,15 @@ 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 });
|
||||
|
||||
await fs.writeFile(
|
||||
await writeFileIfDifferent(
|
||||
filePath,
|
||||
['// Generated by wxt', await unimport.generateTypeDeclarations()].join(
|
||||
'\n',
|
||||
@@ -70,13 +80,13 @@ import "wxt/browser";
|
||||
declare module "wxt/browser" {
|
||||
type PublicPath =
|
||||
{{ union }}
|
||||
export interface ProjectRuntime extends Runtime.Static {
|
||||
export interface WxtRuntime extends Runtime.Static {
|
||||
getURL(path: PublicPath): string;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
await fs.writeFile(
|
||||
await writeFileIfDifferent(
|
||||
filePath,
|
||||
template.replace('{{ union }}', unions || ' | never'),
|
||||
);
|
||||
@@ -84,12 +94,71 @@ declare module "wxt/browser" {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
async function writeI18nDeclarationFile(
|
||||
config: InternalConfig,
|
||||
): Promise<string> {
|
||||
const filePath = resolve(config.typesDir, 'i18n.d.ts');
|
||||
const defaultLocale = config.manifest.default_locale;
|
||||
const template = `// Generated by wxt
|
||||
import "wxt/browser";
|
||||
|
||||
declare module "wxt/browser" {
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/reference/i18n/#method-getMessage
|
||||
*/
|
||||
interface GetMessageOptions {
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/reference/i18n/#method-getMessage
|
||||
*/
|
||||
escapeLt?: boolean
|
||||
}
|
||||
|
||||
export interface WxtI18n extends I18n.Static {
|
||||
{{ overrides }}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
let messages: Message[];
|
||||
if (defaultLocale) {
|
||||
const defaultLocalePath = path.resolve(
|
||||
config.publicDir,
|
||||
'_locales',
|
||||
defaultLocale,
|
||||
'messages.json',
|
||||
);
|
||||
const content = JSON.parse(await fs.readFile(defaultLocalePath, 'utf-8'));
|
||||
messages = parseI18nMessages(content);
|
||||
} else {
|
||||
messages = parseI18nMessages({});
|
||||
}
|
||||
|
||||
const overrides = messages.map((message) => {
|
||||
return ` /**
|
||||
* ${message.description ?? 'No message description.'}
|
||||
*
|
||||
* "${message.message}"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: "${message.name}",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;`;
|
||||
});
|
||||
await writeFileIfDifferent(
|
||||
filePath,
|
||||
template.replace('{{ overrides }}', overrides.join('\n')),
|
||||
);
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
async function writeGlobalsDeclarationFile(
|
||||
config: InternalConfig,
|
||||
): Promise<string> {
|
||||
const filePath = resolve(config.typesDir, 'globals.d.ts');
|
||||
const globals = getGlobals(config);
|
||||
await fs.writeFile(
|
||||
const globals = [...getGlobals(config), ...getEntrypointGlobals(config, '')];
|
||||
await writeFileIfDifferent(
|
||||
filePath,
|
||||
[
|
||||
'// Generated by wxt',
|
||||
@@ -98,7 +167,6 @@ async function writeGlobalsDeclarationFile(
|
||||
...globals.map((global) => ` const ${global.name}: ${global.type};`),
|
||||
'}',
|
||||
].join('\n') + '\n',
|
||||
'utf-8',
|
||||
);
|
||||
return filePath;
|
||||
}
|
||||
@@ -109,7 +177,7 @@ async function writeMainDeclarationFile(
|
||||
): Promise<string> {
|
||||
const dir = config.wxtDir;
|
||||
const filePath = resolve(dir, 'wxt.d.ts');
|
||||
await fs.writeFile(
|
||||
await writeFileIfDifferent(
|
||||
filePath,
|
||||
[
|
||||
'// Generated by wxt',
|
||||
@@ -128,7 +196,9 @@ async function writeTsConfigFile(
|
||||
config: InternalConfig,
|
||||
) {
|
||||
const dir = config.wxtDir;
|
||||
await fs.writeFile(
|
||||
const rootPath = normalizePath(relative(dir, config.root));
|
||||
const srcPath = normalizePath(relative(dir, config.srcDir));
|
||||
await writeFileIfDifferent(
|
||||
resolve(dir, 'tsconfig.json'),
|
||||
`{
|
||||
"compilerOptions": {
|
||||
@@ -142,16 +212,15 @@ async function writeTsConfigFile(
|
||||
"strict": true,
|
||||
"lib": ["DOM", "WebWorker"],
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": "${normalizePath(relative(dir, config.root))}",
|
||||
"paths": {
|
||||
"@@": ["."],
|
||||
"@@/*": ["./*"],
|
||||
"~~": ["."],
|
||||
"~~/*": ["./*"],
|
||||
"@": ["${normalizePath(relative(config.root, config.srcDir))}"],
|
||||
"@/*": ["${normalizePath(relative(config.root, config.srcDir))}/*"],
|
||||
"~": ["${normalizePath(relative(config.root, config.srcDir))}"],
|
||||
"~/*": ["${normalizePath(relative(config.root, config.srcDir))}/*"]
|
||||
"@": ["${srcPath}"],
|
||||
"@/*": ["${srcPath}/*"],
|
||||
"~": ["${srcPath}"],
|
||||
"~/*": ["${srcPath}/*"],
|
||||
"@@": ["${rootPath}"],
|
||||
"@@/*": ["${rootPath}/*"],
|
||||
"~~": ["${rootPath}"],
|
||||
"~~/*": ["${rootPath}/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import path from 'node:path';
|
||||
import glob from 'fast-glob';
|
||||
import fs from 'fs-extra';
|
||||
import { consola } from 'consola';
|
||||
import pc from 'picocolors';
|
||||
|
||||
/**
|
||||
* Remove generated/temp files from the directory.
|
||||
*
|
||||
* @param root The directory to look for generated/temp files in. Defaults to `process.cwd()`. Can be relative to `process.cwd()` or absolute.
|
||||
*/
|
||||
export async function clean(root = process.cwd()) {
|
||||
consola.info('Cleaning Project');
|
||||
|
||||
const tempDirs = [
|
||||
'node_modules/.vite',
|
||||
'node_modules/.cache',
|
||||
'**/.wxt',
|
||||
'.output/*',
|
||||
];
|
||||
consola.debug('Looking for:', tempDirs.map(pc.cyan).join(', '));
|
||||
const directories = await glob(tempDirs, {
|
||||
cwd: path.resolve(root),
|
||||
absolute: true,
|
||||
onlyDirectories: true,
|
||||
deep: 2,
|
||||
});
|
||||
if (directories.length === 0) {
|
||||
consola.debug('No generated files found.');
|
||||
return;
|
||||
}
|
||||
|
||||
consola.debug(
|
||||
'Found:',
|
||||
directories.map((dir) => pc.cyan(path.relative(root, dir))).join(', '),
|
||||
);
|
||||
for (const directory of directories) {
|
||||
await fs.rm(directory, { force: true, recursive: true });
|
||||
consola.debug('Deleted ' + pc.cyan(path.relative(root, directory)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { createExtensionRunner } from '..';
|
||||
import { fakeInternalConfig } from '../../../testing/fake-objects';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { createSafariRunner } from '../safari';
|
||||
import { ExtensionRunner } from '../extension-runner';
|
||||
import { createWslRunner } from '../wsl';
|
||||
import { createManualRunner } from '../manual';
|
||||
import { isWsl } from '../../utils/wsl';
|
||||
import { createWebExtRunner } from '../web-ext';
|
||||
|
||||
vi.mock('../../utils/wsl');
|
||||
const isWslMock = vi.mocked(isWsl);
|
||||
|
||||
vi.mock('../safari');
|
||||
const createSafariRunnerMock = vi.mocked(createSafariRunner);
|
||||
|
||||
vi.mock('../wsl');
|
||||
const createWslRunnerMock = vi.mocked(createWslRunner);
|
||||
|
||||
vi.mock('../manual');
|
||||
const createManualRunnerMock = vi.mocked(createManualRunner);
|
||||
|
||||
vi.mock('../web-ext');
|
||||
const createWebExtRunnerMock = vi.mocked(createWebExtRunner);
|
||||
|
||||
describe('createExtensionRunner', () => {
|
||||
it('should return a Safari runner when browser is "safari"', async () => {
|
||||
const config = fakeInternalConfig({
|
||||
browser: 'safari',
|
||||
});
|
||||
const safariRunner = mock<ExtensionRunner>();
|
||||
createSafariRunnerMock.mockReturnValue(safariRunner);
|
||||
|
||||
await expect(createExtensionRunner(config)).resolves.toBe(safariRunner);
|
||||
});
|
||||
|
||||
it('should return a WSL runner when `is-wsl` is true', async () => {
|
||||
isWslMock.mockResolvedValueOnce(true);
|
||||
const config = fakeInternalConfig({
|
||||
browser: 'chrome',
|
||||
});
|
||||
const wslRunner = mock<ExtensionRunner>();
|
||||
createWslRunnerMock.mockReturnValue(wslRunner);
|
||||
|
||||
await expect(createExtensionRunner(config)).resolves.toBe(wslRunner);
|
||||
});
|
||||
|
||||
it('should return a manual runner when `runner.disabled` is true', async () => {
|
||||
isWslMock.mockResolvedValueOnce(false);
|
||||
const config = fakeInternalConfig({
|
||||
browser: 'chrome',
|
||||
runnerConfig: {
|
||||
config: {
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
const manualRunner = mock<ExtensionRunner>();
|
||||
createManualRunnerMock.mockReturnValue(manualRunner);
|
||||
|
||||
await expect(createExtensionRunner(config)).resolves.toBe(manualRunner);
|
||||
});
|
||||
|
||||
it('should return a web-ext runner otherwise', async () => {
|
||||
const config = fakeInternalConfig({
|
||||
browser: 'chrome',
|
||||
runnerConfig: {
|
||||
config: {
|
||||
disabled: undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
const manualRunner = mock<ExtensionRunner>();
|
||||
createWebExtRunnerMock.mockReturnValue(manualRunner);
|
||||
|
||||
await expect(createExtensionRunner(config)).resolves.toBe(manualRunner);
|
||||
});
|
||||
});
|
||||