Compare commits

..

2 Commits

Author SHA1 Message Date
Aaron Klinker 55a03a1c9e Bump version for testing 2024-02-28 11:54:52 -06:00
Aaron Klinker d26381caea fix: Resolve the path to node_modules/wxt correctly 2024-02-28 11:53:18 -06:00
258 changed files with 1343 additions and 4027 deletions
-3
View File
@@ -1,5 +1,2 @@
* text=auto eol=lf
pnpm-lock.yaml linguist-generated
package-lock.json linguist-generated
bun.lockb linguist-generated
yarn.lock linguist-generated
-3
View File
@@ -25,14 +25,11 @@ jobs:
pnpx changelogen@latest --release
git push
git push --tags
working-directory: packages/wxt
- name: NPM
run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_AUTH_TOKEN }}" > ~/.npmrc
pnpm publish
working-directory: packages/wxt
- name: GitHub Release
run: pnpx changelogen@latest gh release --token ${{ secrets.GITHUB_TOKEN }}
working-directory: packages/wxt
+22 -8
View File
@@ -7,27 +7,42 @@ on:
- main
jobs:
checks:
formatting:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup
- run: pnpm check
build-demo:
- run: pnpm format:check
lint:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup
- run: pnpm lint
type-check:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup
- run: pnpm compile
validate-demo:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup
- run: pnpm tsc --noEmit
working-directory: demo
- run: pnpm build:all
working-directory: packages/wxt-demo
working-directory: demo
- run: pnpm wxt zip
working-directory: packages/wxt-demo
working-directory: demo
- run: pnpm vitest run
working-directory: demo
tests:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup
- uses: oven-sh/setup-bun@v1
- name: pnpm test:coverage
run: pnpm test:coverage --reporter=default --reporter=hanging-process
- uses: codecov/codecov-action@v4
@@ -53,10 +68,9 @@ jobs:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup
- run: pnpm pack
working-directory: packages/wxt
- run: npm i
working-directory: templates/${{ matrix.template }}
- run: npm i -D ../../packages/wxt/wxt-*.tgz
- run: npm i -D ../../wxt-*.tgz
working-directory: templates/${{ matrix.template }}
- run: pnpm compile
if: matrix.template != 'svelte'
+1
View File
@@ -0,0 +1 @@
ignore-workspace-root-check=true
-269
View File
@@ -1,274 +1,5 @@
# Changelog
## v0.18.0
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.12...v0.18.0)
### 🚀 Enhancements
- Add zip compression settings ([#605](https://github.com/wxt-dev/wxt/pull/605))
- Support returning values from scripts executed with the scripting API ([#624](https://github.com/wxt-dev/wxt/pull/624))
- **experimental:** Load entrypoint options with Vite Runtime API ([#648](https://github.com/wxt-dev/wxt/pull/648))
### 🩹 Fixes
- ⚠️ Automatically move `host_permissions` to `permissions` for MV2 ([#626](https://github.com/wxt-dev/wxt/pull/626))
- **dep:** Upgrade `@webext-core/isolated-element` to v1.1.2 ([#625](https://github.com/wxt-dev/wxt/pull/625))
### 📖 Documentation
- Add Fluent Read to homepage ([#600](https://github.com/wxt-dev/wxt/pull/600))
- Fix typo on example for wxt.config.ts. ([#609](https://github.com/wxt-dev/wxt/pull/609))
- Tix typo in `entrypoints.md` ([#614](https://github.com/wxt-dev/wxt/pull/614))
- Add Facebook Video Controls to homepage ([#615](https://github.com/wxt-dev/wxt/pull/615))
- Fix typo in assets page ([a94d673](https://github.com/wxt-dev/wxt/commit/a94d673))
- Add ElemSnap to homepage ([#621](https://github.com/wxt-dev/wxt/pull/621))
- Update content script registration JSDoc ([e47519f](https://github.com/wxt-dev/wxt/commit/e47519f))
- Add docs about handling updates ([acb7554](https://github.com/wxt-dev/wxt/commit/acb7554))
- Add MS Edge TTS to homepage ([#647](https://github.com/wxt-dev/wxt/pull/647))
- Document required permission for storage API ([#632](https://github.com/wxt-dev/wxt/pull/632))
### 🏡 Chore
- Update vue template config ([#607](https://github.com/wxt-dev/wxt/pull/607))
- **deps-dev:** Bump lint-staged from 15.2.1 to 15.2.2 ([#637](https://github.com/wxt-dev/wxt/pull/637))
- **deps-dev:** Bump publint from 0.2.6 to 0.2.7 ([#639](https://github.com/wxt-dev/wxt/pull/639))
- **deps-dev:** Bump simple-git-hooks from 2.9.0 to 2.11.1 ([#640](https://github.com/wxt-dev/wxt/pull/640))
- Refactor repo to a standard monorepo ([#646](https://github.com/wxt-dev/wxt/pull/646))
- Fix formatting after monorepo refactor ([6ca3767](https://github.com/wxt-dev/wxt/commit/6ca3767))
#### ⚠️ Breaking Changes
- ⚠️ Automatically move `host_permissions` to `permissions` for MV2 ([#626](https://github.com/wxt-dev/wxt/pull/626))
### ❤️ Contributors
- Aaron <aaronklinker1@gmail.com>
- Alegal200 <alex.gallez11@gmail.com>
- Yacine-bens ([@yacine-bens](http://github.com/yacine-bens))
- Ayden <itxcc420@gmail.com>
- Wuzequanyouzi ([@wuzequanyouzi](http://github.com/wuzequanyouzi))
- Can Rau ([@CanRau](http://github.com/CanRau))
- 日高 凌 ([@ryohidaka](http://github.com/ryohidaka))
- Bas Van Zanten <bas.van.zanten007@gmail.com>
- ThinkStu ([@Bistutu](http://github.com/Bistutu))
## v0.17.12
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.11...v0.17.12)
### 🚀 Enhancements
- Add hooks for extending vite config ([#599](https://github.com/wxt-dev/wxt/pull/599))
### 🩹 Fixes
- **content-script-ui:** Properly assign and unassign mounted value ([#598](https://github.com/wxt-dev/wxt/pull/598))
### 📖 Documentation
- Add discord server link ([#593](https://github.com/wxt-dev/wxt/pull/593))
### 🏡 Chore
- Remove unnecssary 'Omit' types ([db57c8e](https://github.com/wxt-dev/wxt/commit/db57c8e))
- **deps-dev:** Bump @aklinker1/check from 1.1.1 to 1.2.0 ([#588](https://github.com/wxt-dev/wxt/pull/588))
- **deps-dev:** Bump vue from 3.3.10 to 3.4.21 ([#589](https://github.com/wxt-dev/wxt/pull/589))
- **deps-dev:** Bump sass from 1.69.5 to 1.72.0 ([#591](https://github.com/wxt-dev/wxt/pull/591))
## v0.17.11
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.10...v0.17.11)
### 🩹 Fixes
- Resolve absolute paths from the public directory properly ([#583](https://github.com/wxt-dev/wxt/pull/583))
### 📖 Documentation
- Fix `zip.includeSources` example ([16fc584](https://github.com/wxt-dev/wxt/commit/16fc584))
- Add "GitHub Custom Notifier" to homepage ([#580](https://github.com/wxt-dev/wxt/pull/580))
### 🏡 Chore
- Simplify virtual module setup ([#581](https://github.com/wxt-dev/wxt/pull/581))
- Add some array utils ([#582](https://github.com/wxt-dev/wxt/pull/582))
- Extract helper function ([d3b14af](https://github.com/wxt-dev/wxt/commit/d3b14af))
### ❤️ Contributors
- Qiwei Yang ([@qiweiii](http://github.com/qiweiii))
## v0.17.10
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.9...v0.17.10)
### 🚀 Enhancements
- Add `dev.server.port` config ([#577](https://github.com/wxt-dev/wxt/pull/577))
### 🏡 Chore
- Use `@aklinker1/check` to simplify checks setup ([#550](https://github.com/wxt-dev/wxt/pull/550))
- Refactor order of config resolution ([#578](https://github.com/wxt-dev/wxt/pull/578))
### ❤️ Contributors
- Zizheng Tai ([@zizhengtai](http://github.com/zizhengtai))
## v0.17.9
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.8...v0.17.9)
### 🚀 Enhancements
- Add `{{mode}}` Template Variable ([#566](https://github.com/wxt-dev/wxt/pull/566))
### 🩹 Fixes
- Don't override `wxt.config.ts` options when CLI flags are not passed ([#567](https://github.com/wxt-dev/wxt/pull/567))
### 🏡 Chore
- Merge user config using `defu` ([#568](https://github.com/wxt-dev/wxt/pull/568))
### ❤️ Contributors
- Guillaume ([@GuiEpi](http://github.com/GuiEpi))
## v0.17.8
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.7...v0.17.8)
### 🚀 Enhancements
- **analysis:** Open `stats.html` file automatically ([#564](https://github.com/wxt-dev/wxt/pull/564))
### 🩹 Fixes
- **init:** Better error logging when templates fail to load ([b47c150](https://github.com/wxt-dev/wxt/commit/b47c150))
- Remove deprecated extension from Vue template ([#534](https://github.com/wxt-dev/wxt/pull/534))
- Append option description error ([#546](https://github.com/wxt-dev/wxt/pull/546))
- **init:** Don't overwrite existing files when initializing a new project ([#556](https://github.com/wxt-dev/wxt/pull/556))
- **dev:** Don't crash dev mode when rebuild fails ([#565](https://github.com/wxt-dev/wxt/pull/565))
### 📖 Documentation
- Replace `pnpx` with `pnpm dlx` ([#527](https://github.com/wxt-dev/wxt/pull/527))
- Update homepage demo video ([35269da](https://github.com/wxt-dev/wxt/commit/35269da))
- Update README demo video ([#539](https://github.com/wxt-dev/wxt/pull/539))
- Add Plex skipper to "Using WXT" section ([#541](https://github.com/wxt-dev/wxt/pull/541))
- Add documentation for Bun.sh ([#543](https://github.com/wxt-dev/wxt/pull/543))
- Mention adding unlisted scripts and pages to `web_accessible_resources` ([121b521](https://github.com/wxt-dev/wxt/commit/121b521))
- Add examples for GitHub Actions ([#540](https://github.com/wxt-dev/wxt/pull/540))
- Fixed "Reload the Extension" section ([#559](https://github.com/wxt-dev/wxt/pull/559))
### 🏡 Chore
- Increase unit test timeout ([d9cba55](https://github.com/wxt-dev/wxt/commit/d9cba55))
- Increase hook timeout for Windows/NPM tests ([56b7149](https://github.com/wxt-dev/wxt/commit/56b7149))
- Switch to seed plugin for testing ([#547](https://github.com/wxt-dev/wxt/pull/547))
- **vue-template:** Upgrade to `vue-tsc` v2 ([#549](https://github.com/wxt-dev/wxt/pull/549))
- Add a test covering `wxt init` in a non-empty directory ([#563](https://github.com/wxt-dev/wxt/pull/563))
### ❤️ Contributors
- Btea ([@btea](http://github.com/btea))
- Vlad Fedosov ([@StyleT](http://github.com/StyleT))
- Lpmvb ([@Lpmvb](http://github.com/Lpmvb))
- Guillaume ([@GuiEpi](http://github.com/GuiEpi))
- Sunshio ([@MPB-Tech](http://github.com/MPB-Tech))
- Luca Dalli ([@lucadalli](http://github.com/lucadalli))
## v0.17.7
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.6...v0.17.7)
### 🩹 Fixes
- **zip:** List `.wxt/local_modules` overrides relative to the `package.json` they're added to ([#526](https://github.com/wxt-dev/wxt/pull/526))
### 🏡 Chore
- Bump templates to v0.17 ([#524](https://github.com/wxt-dev/wxt/pull/524))
- Increase test timeout for windows NPM tests ([#525](https://github.com/wxt-dev/wxt/pull/525))
## v0.17.6
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.5...v0.17.6)
### 🚀 Enhancements
- Add warnings when important directories are missing ([#516](https://github.com/wxt-dev/wxt/pull/516))
- Automatically remove top-level MV2-only or MV3-only keys ([#518](https://github.com/wxt-dev/wxt/pull/518))
- Automatically generate `browser_action` based on `action` for MV2 ([#519](https://github.com/wxt-dev/wxt/pull/519))
### 🩹 Fixes
- **zip:** List all private packages correctly in a PNPM workspace ([#520](https://github.com/wxt-dev/wxt/pull/520))
### 📖 Documentation
- Mentions moving folders into `srcDir` ([9cd4e83](https://github.com/wxt-dev/wxt/commit/9cd4e83))
### 🏡 Chore
- **deps-dev:** Bump @types/react from 18.2.34 to 18.2.61 ([#510](https://github.com/wxt-dev/wxt/pull/510))
## v0.17.5
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.4...v0.17.5)
### 🚀 Enhancements
- Expose package management utils under `wxt.pm` ([#502](https://github.com/wxt-dev/wxt/pull/502))
- Download and override private packages for Firefox code review ([#507](https://github.com/wxt-dev/wxt/pull/507))
### 📖 Documentation
- Fix typos ([#503](https://github.com/wxt-dev/wxt/pull/503))
- Add docs about configuring the manifest as a function ([195d2cc](https://github.com/wxt-dev/wxt/commit/195d2cc))
- Fix CLI generation ([b754435](https://github.com/wxt-dev/wxt/commit/b754435))
### 🏡 Chore
- Use JSZip for `wxt zip`, enabling future features ([#501](https://github.com/wxt-dev/wxt/pull/501))
### ❤️ Contributors
- Btea ([@btea](http://github.com/btea))
## v0.17.4
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.3...v0.17.4)
### 🚀 Enhancements
- Add basic content script to templates ([#495](https://github.com/wxt-dev/wxt/pull/495))
- Add `ResolvedConfig.wxtModuleDir`, resolving the directory once ([#497](https://github.com/wxt-dev/wxt/pull/497))
### 🩹 Fixes
- Resolve the path to `node_modules/wxt` correctly ([#498](https://github.com/wxt-dev/wxt/pull/498))
### 📖 Documentation
- Added DocVersionRedirector to "Using WXT" section ([#492](https://github.com/wxt-dev/wxt/pull/492))
- Fix typos ([f80fb42](https://github.com/wxt-dev/wxt/commit/f80fb42))
- Add CRXJS to comparison page ([cb4f9aa](https://github.com/wxt-dev/wxt/commit/cb4f9aa))
- Update comparison page ([35778f7](https://github.com/wxt-dev/wxt/commit/35778f7))
- Update context usage ([012bd7e](https://github.com/wxt-dev/wxt/commit/012bd7e))
- Add testing example for `ContentScriptContext` ([e1c6020](https://github.com/wxt-dev/wxt/commit/e1c6020))
### 🏡 Chore
- Fix tests after template change ([f9b0aa4](https://github.com/wxt-dev/wxt/commit/f9b0aa4))
### ❤️ Contributors
- Btea ([@btea](http://github.com/btea))
- Leo Shklovskii <leo@thermopylae.net>
## v0.17.3
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.2...v0.17.3)
+2 -4
View File
@@ -29,22 +29,20 @@
<a href="https://wxt.dev/api/config.html" target="_blank">Configuration</a>
&bull;
<a href="https://wxt.dev/examples.html" target="_blank">Examples</a>
&bull;
<a href="https://discord.gg/ZFsZqGery9" target="_blank">Discord</a>
</p>
![Example CLI Output](./docs/assets/cli-output.png)
## Demo
https://github.com/wxt-dev/wxt/assets/10101283/4d678939-1bdb-495c-9c36-3aa281d84c94
https://github.com/wxt-dev/wxt/assets/10101283/07359e53-f491-43b6-8e8f-fae94aec8063
## Quick Start
Bootstrap a new project:
```sh
pnpm dlx wxt@latest init <project-name>
pnpx wxt@latest init <project-name>
```
Or see the [installation guide](https://wxt.dev/guide/installation.html) to get started with WXT.
+30
View File
@@ -0,0 +1,30 @@
{
"name": "demo",
"version": "1.0.0",
"description": "Demo extension for WXT",
"type": "module",
"scripts": {
"dev": "pnpm -w build && wxt",
"build": "pnpm -w build && wxt build",
"build:all": "pnpm -w build && run-s -s 'build:all:*'",
"build:all:chrome-mv3": "wxt build",
"build:all:chrome-mv2": "wxt build --mv2",
"build:all:firefox-mv3": "wxt build -b firefox --mv3",
"build:all:firefox-mv2": "wxt build -b firefox",
"test": "pnpm -w build && vitest",
"zip": "pnpm -w build && wxt zip",
"compile": "pnpm -w build && tsc --noEmit",
"postinstall": "pnpm -w build && wxt prepare"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"vitest": "^1.2.2"
},
"devDependencies": {
"@types/react": "^18.2.34",
"@types/react-dom": "^18.2.14",
"sass": "^1.69.5",
"wxt": "workspace:*"
}
}
@@ -1,4 +1,4 @@
import messages from '~/public/_locales/en/messages.json';
import messages from 'public/_locales/en/messages.json';
export default defineBackground({
// type: 'module',

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,5 +1,5 @@
{
"extends": ["../../tsconfig.base.json", "./.wxt/tsconfig.json"],
"extends": ["../tsconfig.base.json", "./.wxt/tsconfig.json"],
"compilerOptions": {
"allowImportingTsExtensions": true,
"jsx": "react-jsx"
@@ -1,7 +1,7 @@
import { defineProject } from 'vitest/config';
import { defineConfig } from 'vitest/config';
import { WxtVitest } from 'wxt/testing';
export default defineProject({
export default defineConfig({
test: {
mockReset: true,
restoreMocks: true,
@@ -12,13 +12,7 @@ export default defineConfig({
},
],
},
zip: {
downloadPackages: ['sass'],
},
analysis: {
open: true,
},
experimental: {
viteRuntime: true,
alias: {
public: 'src/public',
},
});
@@ -20,12 +20,6 @@ const chromeExtensionIds = [
'agjnjboanicjcpenljmaaigopkgdnihi', // PreMiD
'aiakblgmlabokilgljkglggnpflljdgp', // Markdown Sticky Notes
'nomnkbngkijpffepcgbbofhcnafpkiep', // DocVersionRedirector
'ceicccfeikoipigeghddpocceifjelph', // Plex Skipper
'aelkipgppclpfimeamgmlonimflbhlgf', // GitHub Custom Notifier
'djnlaiohfaaifbibleebjggkghlmcpcj', // Fluent Read
'nhclljcpfmmaiojbhhnkpjcfmacfcian', // Facebook Video Controls
'mblkhbaakhbhiimkbcnmeciblfhmafna', // ElemSnap - Quick capture of webpage elements and conversion to images,
'oajalfneblkfiejoadecnmodfpnaeblh', // MS Edge TTS (Text to Speech)
];
const { data, err, isLoading } = useListExtensionDetails(chromeExtensionIds);
+1 -5
View File
@@ -95,7 +95,6 @@ export default defineConfig({
{ text: 'Vite', link: '/guide/vite.md' },
{ text: 'Remote Code', link: '/guide/remote-code.md' },
{ text: 'Publishing', link: '/guide/publishing.md' },
{ text: 'Handling Updates', link: '/guide/handling-updates.md' },
{ text: 'Development', link: '/guide/development.md' },
{ text: 'Testing', link: '/guide/testing.md' },
],
@@ -147,9 +146,6 @@ export default defineConfig({
],
},
socialLinks: [
{ icon: 'discord', link: 'https://discord.gg/ZFsZqGery9' },
{ icon: 'github', link: 'https://github.com/wxt-dev/wxt' },
],
socialLinks: [{ icon: 'github', link: 'https://github.com/wxt-dev/wxt' }],
},
});
+5 -15
View File
@@ -6,7 +6,7 @@ import { execaCommand } from 'execa';
let hasGenerated = false;
const cliDir = resolve('packages/wxt/src/cli/commands');
const cliDir = resolve('src/cli/commands');
const cliDirGlob = resolve(cliDir, '**');
const cliTemplatePath = resolve('docs/.vitepress/plugins/cli.tpl.md');
const cliPath = resolve('docs/api/cli.md');
@@ -23,9 +23,7 @@ export function generateCliDocs() {
const generateDocs = async () => {
consola.info(`Generating ${relative(process.cwd(), cliPath)}`);
try {
const res = await execaCommand('pnpm -s wxt --help', {
cwd: 'packages/wxt',
});
const res = await execaCommand('pnpm -s wxt --help');
const dev = splitInfo(res.stdout);
const lines: Array<string | string[]> = [
`## \`wxt\``,
@@ -34,22 +32,14 @@ export function generateCliDocs() {
'```',
];
const commands = await Promise.allSettled(
const commands = await Promise.all(
extractCommands(dev.info).map(async (command) => {
const res = await execaCommand(`pnpm -s wxt ${command} --help`, {
cwd: 'packages/wxt',
});
const res = await execaCommand(`pnpm -s wxt ${command} --help`);
const { rest: docs } = splitInfo(res.stdout);
return [`## \`wxt ${command}\``, '```sh', docs, '```'];
}),
);
lines.push(
...commands
.filter((res) => res.status === 'fulfilled')
.map(
(res) => (res as unknown as PromiseFulfilledResult<string>).value,
),
);
lines.push(...commands);
const text = await format(
PREFACE +
+47 -1
View File
@@ -40,7 +40,7 @@ export default defineContentScript({
// Configure how/when content script will be registered
registration: undefined | "manifest" | "runtime",
main(ctx: ContentScriptContext) {
main(ctx) {
// Executed when content script is loaded
},
});
@@ -50,6 +50,52 @@ export default defineContentScript({
When defining multiple content scripts, content script entrypoints that have the same set of options will be merged into a single `content_script` item in the manifest.
## Context
Old content scripts are not automatically stopped when an extension updates and reloads. Often, this leads to "Invalidated context" errors in production when a content script from an old version of your extension tries to use a web extension API (ie, the `browser` or `chrome` globals).
WXT provides a utility for handling this process: `ContentScriptContext`. An instance of this class is provided to you automatically inside the `main` function of your content script.
When your extension updates or is uninstalled, the context will become invalidated, and will trigger any `ctx.onInvalidated` listeners you add:
```ts
export default defineContentScript({
// ...
main(ctx: ContentScriptContext) {
// Add custom listeners for stopping work
ctx.onInvalidated(() => {
// ...
});
// Timeout utilities that are automatically cleared when invalidated
ctx.setTimeout(() => {
// ...
}, 5e3);
ctx.setInterval(() => {
// ...
}, 60e3);
// Or add event listeners that get removed when invalidated
ctx.addEventListener(document, 'visibilitychange', (event) => {
// ...
});
// You can also stop fetch requests
fetch('...url', { signal: ctx.signal });
},
});
```
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.
:::warning
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.
If you're using a framework like React, Vue, Svelte, etc., make sure you're unmounting your UI properly in the `onRemove` option of [`createShadowRootUi`](https://wxt.dev/guide/content-script-ui.html#shadow-root).
:::
## CSS
To include CSS with your content script, import the CSS file at the top of your entrypoint.
-2
View File
@@ -2,8 +2,6 @@
HTML pages that are bundled and shipped with the extension, but are not included in the manifest.
If you plan on using the page in an iframe, don't forget to add the page to [`web_accessible_resources`](https://developer.chrome.com/docs/extensions/reference/manifest/web-accessible-resources).
### Examples
- Onboarding
+1 -1
View File
@@ -2,7 +2,7 @@
TypeScript files that are bundled and shipped with the extension, but are not included in the manifest.
You are responsible for loading/running these scripts where needed. If necessary, don't forget to add the script and/or any related stylesheets to [`web_accessible_resources`](https://developer.chrome.com/docs/extensions/reference/manifest/web-accessible-resources).
You are responsible for loading/running these scripts where needed.
## Filenames
+1 -1
View File
@@ -63,7 +63,7 @@ Files in the assets directory will be processed by Vite. They are imported in yo
<!-- ... -->
</head>
<body>
<img src="~/assets/illustration.svg" />
<img src="~/illustration.svg" />
<!-- ... -->
</body>
</html>
+2 -12
View File
@@ -89,7 +89,7 @@ import { defineRunnerConfig } from 'wxt';
export default defineRunnerConfig({
startUrls: ['https://google.com', 'https://duckduckgo.com'],
chromiumProfile: '/path/to/profile/to/use',
chromiumArgs: ['--window-size=400,300'],
chromiumArgs: ['--window-size=400x300'],
});
```
@@ -99,17 +99,7 @@ For a full list of options, see the [API Reference](/api/wxt/interfaces/Extensio
Normally, to manually reload an extension, you have to visit `chrome://extensions` and click the reload button for your extension.
When running `wxt` command to start the dev server, WXT adds a keyboard shortcut `Alt+R`, that reloads the extension when pressed, without visiting `chrome://extensions`. This can also be customized or disabled:
```ts [wxt.config.ts]
import { defineConfig } from 'wxt';
export default defineConfig({
dev: {
reloadCommand: 'Alt+T', // false, to disable
},
});
```
When running `wxt` command to start the dev server, WXT adds a keyboard shortcut, `ctrl+E` for Windows/Linux and `cmd+E` for Mac, that reloads the extension when pressed, without visiting `chrome://extensions`.
:::info
This shortcut is only available during development, and is not be added to your extension when running `wxt build` or `wxt-zip`.
+2 -2
View File
@@ -89,7 +89,7 @@ For a full list of entrypoints and each of their options, see the [`/entrypoints
### Side Effects
You cannot use imported variables outside the `main` function in JS entrypoints. This includes options, as shown below:
You cannot use imported variables outside the `main` function is JS entrypoints. This includes options, as shown below:
```ts
// entrypoints/content.ts
@@ -114,7 +114,7 @@ WXT 0.14.1
[8:55:54 AM] ERROR entrypoints/content.ts: Cannot use imported variable "GOOGLE_MATCHES" before main function. See https://wxt.dev/guide/entrypoints.html#side-effects
```
This throws an error because WXT needs to import each entrypoint during the build process to extract its definition (containing the `match`, `runAt`, `include`/`exclude`, etc.) to render the `manifest.json` correctly. Before loading an entrypoint, a transformation is applied to remove all imports. This prevents imported modules (local or NPM) with side-effects from running during the build process, potentially throwing an error.
This throws an error because WXT needs to import each entrypoint during the build process to extract its definition (containing the `match`, `run_at`, `include`/`exclude`, etc.) to render the `manifest.json` correctly. Before loading an entrypoint, a transformation is applied to remove all imports. This prevents imported modules (local or NPM) with side-effects from running during the build process, potentially throwing an error.
:::details Why?
+2 -2
View File
@@ -39,9 +39,9 @@ Then we can use `browser.storage` to save the install date to local storage.
```ts
// background.ts
export default defineBackground(() => {
browser.runtime.onInstalled.addListener(({ reason }) => {
browser.runtime.onInstall.addEventListener(({ reason }) => {
if (reason === 'install') {
browser.storage.local.set({ installDate: Date.now() });
browser.storage.local.setItem({ installDate: Date.now() });
}
});
});
-116
View File
@@ -1,116 +0,0 @@
# Handling Extension Updates
When releasing an update to your extension, there's a couple of things you need to keep in mind:
[[toc]]
## Content Script Cleanup
Old content scripts are not automatically stopped when an extension updates and reloads. Often, this leads to "Invalidated context" errors in production when a content script from an old version of your extension tries to use a web extension API (ie, the `browser` or `chrome` globals).
WXT provides a utility for handling this process: `ContentScriptContext`. An instance of this class is provided to you automatically inside the `main` function of each content script.
When your extension updates or reloads, the context will become invalidated, and will trigger any `ctx.onInvalidated` listeners you add:
```ts
export default defineContentScript({
main(ctx) {
ctx.onInvalidated(() => {
// Do something
});
},
)
```
The `ctx` also provides other convenient APIs for stopping your content script without manually calling `onInvalidated` to add a listener:
1. Setting timers:
```ts
ctx.setTimeout(() => { ... }, ...);
ctx.setInterval(() => { ... }, ...);
ctx.requestAnimationFrame(() => { ... });
```
1. Adding DOM events:
```ts
ctx.addEventListener(window, "mousemove", (event) => { ... });
```
1. Implements [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) for canceling standard APIs:
```ts
fetch('...', {
signal: ctx.signal,
});
```
Other WXT APIs require a `ctx` object so they can clean themselves up. For example, [`createIntegratedUi`](/guide/content-script-ui#integrated), [`createShadowRootUi`](/guide/content-script-ui#shadow-root), and [`createIframeUi`](/guide/content-script-ui#iframe) automatically unmount and stop a UI when the script is invalidated.
:::warning
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 being logged to the console in production.
:::
## Testing Permission Changes
When `permissions`/`host_permissions` change during an update, depending on what exactly changed, Chrome will disable your extension until the user accepts the new permissions.
It is possible to test this before you release an update, but it's not a simple process:
1. Get 2 ZIPs of your extension, both generated by `wxt zip`. The first contains a previous version of your extension, the second contains the latest version. Make sure the second ZIP's version is higher than the first's.
2. Unzip the two ZIP files somewhere next to each other that's easy to locate.
3. In Chrome, open `chrome://extensions` and make sure developer mode is enabled
4. Pack the first extension into a CRX, generating a new private key:
1. Click "Pack Extension" in the top left
2. For "Extension root directory", enter the path to the first extracted zip directory. The directory should contain a `manifest.json` file
3. Leave "Private key file" blank
4. Click "Pack Extension". This will generate a `.crx` and `.pem` file
5. Pack the second extension into a CRX, reusing the private key generated by the previous step
1. Click "Pack Extension" in the top left
2. For "Extension root directory", enter the path to the second extracted zip directory.
3. For "Private key file", enter the path to the generated `.pem` private key file
4. Click "Pack Extension". This will generate a second `.crx` file.
6. Install the first CRX file by dragging and dropping it onto the `chrome://extensions` page
7. Install the second CRX file by dragging and dropping it onthe the `chrome://extensions` page
If a new permission must be accepted, you'll be prompted to accept it after dropping the second CRX file onto the page.
:::Info Note
Note: Chrome no longer allows self-signed CRX extensions to run, but that's OK for this test case. We're still prompted to accept new permissions, even if we can't interact with the installed extension.
To validate this, you can create a third ZIP file with a rare permission like `geolocation` in the manifest, that's guarenteed to reprompt permissions when added.
:::
## Update Event
You can setup a callback that runs after your extension updates like so:
```ts
browser.runtime.onInstalled.addEventListener(({ reason }) => {
if (reason === 'update') {
// Do something
}
});
```
If the logic is simple, write a unit test to cover this logic. If you feel the need to manually test this callback, you can either:
1. In dev mode, remove the `if` statement and reload the extension from `chrome://extensions`
2. Build two ZIPs with the same runtime ID and actually update the extension
The first approach is very straightforward. The second is more complicated...
Here are the steps:
So the steps:
1. Checkout an old commit.
2. [Add a `key`](https://developer.chrome.com/docs/extensions/reference/manifest/key#keep-consistent-id) to the `manifest` in your `wxt.config.ts`.
3. Run `wxt zip` to create the first ZIP.
4. Stash or reset changes and checkout your latest code.
5. Add back the same `key` to your manifest.
6. Make sure the extension's version is higher than the first zip. It can be any version that's higher, since you won't be releasing this version.
7. Run `wxt zip` to create the second ZIP.
8. In a fresh chrome profile, go to `chrome://extensions`, enable dev mode, and drag and drop the first zip onto the page to install it.
9. In the extension, play around and setup your test case.
10. Back on `chrome://extensions`, drag and drop your second zip onto the page.
If you setup the `key` correctly, it will cause the extension to act like it was updated instead of installing a second version of your extension.
+2 -16
View File
@@ -7,17 +7,13 @@ Bootstrap a new project, start from scratch, or [migrate an existing project](/g
:::code-group
```sh [pnpm]
pnpm dlx wxt@latest init <project-name>
pnpx wxt@latest init <project-name>
```
```sh [npm]
npx wxt@latest init <project-name>
```
```sh [bun]
bunx wxt@latest init <project-name>
```
:::
There are several starting templates available.
@@ -59,12 +55,6 @@ cd project-name
yarn init
```
```sh [bun]
mkdir project-name
cd project-name
bun init
```
:::
Then install `wxt`:
@@ -83,10 +73,6 @@ npm i --save-dev wxt
yarn add --dev wxt
```
```sh [bun]
bun add --dev wxt
```
:::
Add your first entrypoint:
@@ -116,7 +102,7 @@ Finally, add scripts to your `package.json`:
## Migrate an Existing Project
Before starting the migration, it is recommended to run `pnpm dlx 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.
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:
+7 -117
View File
@@ -1,7 +1,3 @@
---
outline: deep
---
# Manifest.json
The manifest.json is generated at build-time based on files in the `entrypoints/` directory and `wxt.config.ts`.
@@ -11,7 +7,7 @@ The manifest.json is generated at build-time based on files in the `entrypoints/
While entrypoints are generated and added to the manifest at build-time, you can customize or add to your `manifest.json` in the config file.
```ts
// wxt.config.ts entrypoint of your extension
// wxt.config.tsentrypoint of your extension
import { defineConfig } from 'wxt';
export default defineConfig({
@@ -24,11 +20,11 @@ export default defineConfig({
});
```
### `name`
## `name`
If not provided via the `manifest` config, the [manifest's `name`](https://developer.chrome.com/docs/extensions/mv3/manifest/name/) defaults to your package.json's `name` property.
### `version` and `version_name`
## `version` and `version_name`
The [manifest's `version` and `version_name`](https://developer.chrome.com/docs/extensions/mv3/manifest/version/) properties are based on the `version` field listed in your `package.json` or `wxt.config.ts`.
@@ -37,7 +33,7 @@ The [manifest's `version` and `version_name`](https://developer.chrome.com/docs/
If a version is not found, a warning is logged and the version defaults to `"0.0.0"`.
#### Example
### Example
```json
// package.json
@@ -54,7 +50,7 @@ If a version is not found, a warning is logged and the version defaults to `"0.0
}
```
### `icons`
## `icons`
By default, WXT will discover icons in your [`public` directory](/guide/assets#public-directory) and use them for the [manifest's `icons`](https://developer.chrome.com/docs/extensions/mv3/manifest/icons/).
@@ -69,7 +65,7 @@ public/
Icon files need to match the following regex to be automatically included in the manifest. Most design software can output icons in one of these formats
<<< @/../packages/wxt/src/core/utils/manifest.ts#snippet
<<< @/../src/core/utils/manifest.ts#snippet
If you prefer to use filenames in a different format, you can add the icons manually in your `wxt.config.ts` file:
@@ -87,7 +83,7 @@ export default defineConfig({
});
```
### `permissions`
## `permissions`
[Permissions](https://developer.chrome.com/docs/extensions/reference/permissions/) must be listed in the manifest config.
@@ -129,109 +125,3 @@ export default defineConfig({
See the official localization examples for more details:
<ExampleList tag="i18n" />
## Per-Manifest Version Config
WXT applies several transformations to your manifest to simplify managing both MV2 and MV3 keys in your `wxt.config.ts` file:
1. Top level MV2-only or MV3-only keys are stripped from the final manifest when targeting the other manifest version
2. Some keys, are automatically converted between versions when possible:
- Define `web_accessible_resources` in it's MV3 style and it will be converted to the MV2 style automatically
- `action` will automatically be converted to `browser_action` for MV3. To use `page_action` instead, add both `action` and `page_action` entries to your manifest
For example, a `wxt.config.ts` file that looks like this:
```ts
import { defineConfig } from 'wxt';
export default defineConfig({
mainfest: {
action: {
default_title: 'Some Title',
},
web_accessible_resources: [
{
matches: ['*://*.google.com/*'],
resources: ['icon/*.png'],
},
],
},
});
```
Will be output differently for each manifest version:
:::code-group
```json [MV2]
{
"manifest_version": 2,
// ...
"browser_action": {
"default_title": "Some Title"
},
"web_accessible_resources": ["icon/*.png"]
}
```
```json [MV3]
{
"manifest_version": 3,
// ...
"action": {
"default_title": "Some Title"
},
"web_accessible_resources": [
{
"matches": ["*://*.google.com/*"],
"resources": ["icon/*.png"]
}
]
}
```
:::
:::tip
If this isn't enough control for your use-case, remember you can use a function for the `manifest` key and generate it however you'd like, or you can use the `build:manifestGenerated` hook to apply additional transformations.
:::
## Per-Browser Configuration
The `manifest` field can be a function. If you are building and extension for multiple browsers, and need to modify the manifest per browser, using a function instead of an object is very useful.
```ts
export default defineConfig({
manifest: ({ browser, manifestVersion, mode, command }) => {
return {
// Your manifest
};
},
});
```
:::info
The first argument is of type `ConfigEnv`. See the [API reference](/api/wxt/interfaces/ConfigEnv) for info about each property.
:::
For example, say you use OAuth, and you need to provide a different `oauth.client_id` for each browser:
```ts
const clientIds = {
chrome: '<your-chrome-client-id>',
edge: '<your-edge-client-id>',
firefox: '<your-firefox-client-id>',
opera: '<your-opera-client-id>',
};
export default defineConfig({
manifest: ({ browser }) => ({
oauth: {
client_id: clientIds[browser],
scopes: [
// ...
],
},
}),
});
```
+1 -1
View File
@@ -12,7 +12,7 @@ Always start by generating a new vanilla project and merging it into your projec
```sh
cd path/to/your/project
pnpm dlx wxt@latest init example-wxt --template vanilla
pnpx wxt@latest init example-wxt --template vanilla
```
In general, you'll need to:
+2 -76
View File
@@ -64,59 +64,12 @@ See the [Firefox Addon Store](#firefox-addon-store) section for more details abo
## GitHub Action
Here's an example of a GitHub Action that submits new versions of an extension for review. Ensure that you've added all required secrets used in the workflow to the repo's settings.
Here's an example of a GitHub Action to automate submitting new versions of your extension for review. Ensure that you've added all required secrets used in the workflow to the repo's settings.
```yml
name: Release
on:
workflow_dispatch:
jobs:
submit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: 'latest'
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Zip extensions
run: |
pnpm zip
pnpm zip:firefox
- name: Submit to stores
run: |
pnpm wxt submit \
--chrome-zip .output/*-chrome.zip \
--firefox-zip .output/*-firefox.zip --firefox-sources-zip .output/*-sources.zip
env:
CHROME_EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}
CHROME_CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }}
CHROME_CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }}
CHROME_REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }}
FIREFOX_EXTENSION_ID: ${{ secrets.FIREFOX_EXTENSION_ID }}
FIREFOX_JWT_ISSUER: ${{ secrets.FIREFOX_JWT_ISSUER }}
FIREFOX_JWT_SECRET: ${{ secrets.FIREFOX_JWT_SECRET }}
# TODO
```
The action above lays the foundation for a basic workflow, including `zip` and `submit` steps. To further enhance your GitHub Action and delve into more complex scenarios, consider exploring the following examples from real projects. They introduce advanced features such as version management, changelog generation, and GitHub releases, tailored for different needs:
- [`aklinker1/github-better-line-counts`](https://github.com/aklinker1/github-better-line-counts/blob/main/.github/workflows/submit.yml) - Conventional commits, automated version bump and changelog generation, triggered manually, optional dry run for testing
- [`GuiEpi/plex-skipper`](https://github.com/GuiEpi/plex-skipper/blob/main/.github/workflows/deploy.yml) - Triggered automatically when `package.json` version is changed, creates and uploads artifacts to GitHub release.
> These examples are designed to provide clear insights and are a good starting point for customizing your own workflows. Feel free to explore and adapt them to your project needs.
## Stores
### Chrome Web Store
@@ -171,11 +124,6 @@ yarn
yarn zip:firefox
```
```sh [bun]
bun i
bun zip:firefox
```
:::
Make sure the build output is the exact same when running `wxt build -b firefox` in your main project and inside the zipped sources.
@@ -188,28 +136,6 @@ See Issue [#377](https://github.com/wxt-dev/wxt/issues/377) for more details.
Ensure that you have a `README.md` or `SOURCE_CODE_REVIEW.md` file with the above commands so that the Firefox team knows how to build your extension.
#### Private Packages
If you use private packages and you don't want to provide your auth token to the Firefox team during the review process, you can use `zip.downloadPackages` to download any private packages and include them in the zip.
```ts
// wxt.config.ts
export default defineConfig({
zip: {
downloadPackages: [
'@mycompany/some-package',
//...
],
},
});
```
Depending on your package manager, the `package.json` in the sources zip will be modified to use the downloaded dependencies via the `overrides` or `resolutions` field.
:::warning
WXT uses the command `npm pack <package-name>` to download the package. That means regardless of your package manager, you need to properly setup a `.npmrc` file. NPM and PNPM both respect `.npmrc` files, but Yarn and Bun have their own ways of authorizing private registries, so you'll need to add an `.npmrc` file.
:::
### Safari
> 🚧 Not supported yet
+1 -16
View File
@@ -10,21 +10,6 @@ WXT provides a simplified API to replace the `browser.storage.*` APIs. Use the `
import { storage } from 'wxt/storage';
```
:::warning
To use the `wxt/storage` API, the `"storage"` permission must be added to the manifest:
```ts
// wxt.config.ts
export default defineConfig({
manifest: {
permissions: ['storage'],
},
});
```
More info on permissions [here](/guide/manifest#permissions).
:::
[[toc]]
## Basic Usage
@@ -132,7 +117,7 @@ Now, instead of using the `storage` variable, you can use the helper functions o
await showChangelogOnUpdate.getValue();
await showChangelogOnUpdate.setValue(false);
await showChangelogOnUpdate.removeValue();
const unwatch = showChangelogOnUpdate.watch((newValue) => {
const unwatch = showChangelogOnUpdate.watch(() => {
// ...
});
```
+2 -2
View File
@@ -70,14 +70,14 @@ features:
linkText: Read docs
---
<section class="vp-doc" style="padding: 0 48px">
<section class="vp-doc">
<div class="container">
<h2>Put <span style="color: var(--vp-c-brand-1)">Developer Experience</span> First</h2>
<p>
WXT's simplifies the chrome extension development process by providing tools for zipping and publishing, the best-in-class dev mode, an opinionated project structure, and more. Iterate faster, develop features not build scripts, and use everything the JS ecosystem has to offer.
</p>
<div style="margin: auto; width: 100%; max-width: 900px; text-align: center">
<video src="https://github.com/wxt-dev/wxt/assets/10101283/4d678939-1bdb-495c-9c36-3aa281d84c94" controls></video>
<video src="https://github.com/wxt-dev/wxt/assets/10101283/b32e6766-ec11-45a4-9677-226ee4718e1c" controls></video>
<br />
<small>
And who doesn't appreciate a beautiful CLI?
+8 -3
View File
@@ -1,7 +1,12 @@
{
"$schema": "https://typedoc.org/schema.json",
"entryPointStrategy": "packages",
"entryPoints": ["../packages/wxt"],
"entryPoints": [
"../src",
"../src/client",
"../src/browser.ts",
"../src/sandbox",
"../src/storage.ts",
"../src/testing"
],
"plugin": ["typedoc-plugin-markdown", "typedoc-vitepress-theme"],
"out": "./api",
"githubPages": false,
@@ -1,14 +1,6 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { describe, it, expect, beforeEach } from 'vitest';
import { TestProject } from '../utils';
import { resetBundleIncrement } from '~/core/builders/vite/plugins';
import open from 'open';
vi.mock('open');
const openMock = vi.mocked(open);
vi.mock('ci-info', () => ({
isCI: false,
}));
describe('Analysis', () => {
beforeEach(() => {
@@ -97,24 +89,4 @@ describe('Analysis', () => {
expect(await project.fileExists('stats/bundle-0.json')).toBe(true);
expect(await project.fileExists('stats/bundle-1.json')).toBe(true);
});
it('should open the stats in the browser when requested', async () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html');
project.addFile('entrypoints/options.html');
project.addFile(
'entrypoints/background.ts',
'export default defineBackground(() => {});',
);
await project.build({
analysis: {
enabled: true,
open: true,
},
});
expect(openMock).toBeCalledTimes(1);
expect(openMock).toBeCalledWith(project.resolvePath('stats.html'));
});
});
@@ -9,21 +9,16 @@ const hooks: WxtHooks = {
'build:manifestGenerated': vi.fn(),
'entrypoints:resolved': vi.fn(),
'entrypoints:grouped': vi.fn(),
'vite:build:extendConfig': vi.fn(),
'vite:devServer:extendConfig': vi.fn(),
};
function expectHooksToBeCalled(
called: Record<keyof WxtHooks, boolean | number>,
) {
function expectHooksToBeCalled(called: Record<keyof WxtHooks, boolean>) {
Object.keys(hooks).forEach((key) => {
const hookName = key as keyof WxtHooks;
const value = called[hookName];
const times = typeof value === 'number' ? value : value ? 1 : 0;
const times = called[hookName] ? 1 : 0;
expect(
hooks[hookName],
`Expected "${hookName}" to be called ${times} time(s)`,
).toBeCalledTimes(times);
).toBeCalledTimes(called[hookName] ? 1 : 0);
});
}
@@ -45,8 +40,6 @@ describe('Hooks', () => {
'build:manifestGenerated': false,
'entrypoints:grouped': false,
'entrypoints:resolved': true,
'vite:build:extendConfig': false,
'vite:devServer:extendConfig': false,
});
});
@@ -63,8 +56,6 @@ describe('Hooks', () => {
'build:manifestGenerated': true,
'entrypoints:grouped': true,
'entrypoints:resolved': true,
'vite:build:extendConfig': 1,
'vite:devServer:extendConfig': false,
});
});
@@ -81,8 +72,6 @@ describe('Hooks', () => {
'build:manifestGenerated': true,
'entrypoints:grouped': true,
'entrypoints:resolved': true,
'vite:build:extendConfig': 1,
'vite:devServer:extendConfig': false,
});
});
@@ -105,8 +94,6 @@ describe('Hooks', () => {
'build:manifestGenerated': true,
'entrypoints:grouped': true,
'entrypoints:resolved': true,
'vite:build:extendConfig': 2,
'vite:devServer:extendConfig': 1,
});
});
});
@@ -2,8 +2,6 @@ import { describe, it, expect } from 'vitest';
import { TestProject } from '../utils';
import { execaCommand } from 'execa';
import glob from 'fast-glob';
import { mkdir, writeJson } from 'fs-extra';
import { WXT_PACKAGE_DIR } from '../utils';
describe('Init command', () => {
it('should download and create a template', async () => {
@@ -12,7 +10,6 @@ describe('Init command', () => {
await execaCommand(`pnpm -s wxt init ${project.root} -t vue --pm npm`, {
env: { ...process.env, CI: 'true' },
stdio: 'ignore',
cwd: WXT_PACKAGE_DIR,
});
const files = await glob('**/*', {
cwd: project.root,
@@ -45,18 +42,4 @@ describe('Init command', () => {
]
`);
});
it('should throw an error if the directory is not empty', async () => {
const project = new TestProject();
await mkdir(project.root, { recursive: true });
await writeJson(project.resolvePath('package.json'), {});
await expect(() =>
execaCommand(`pnpm -s wxt init ${project.root} -t vue --pm npm`, {
env: { ...process.env, CI: 'true' },
stdio: 'ignore',
cwd: WXT_PACKAGE_DIR,
}),
).rejects.toThrowError('Command failed with exit code 1:');
});
});
+33
View File
@@ -0,0 +1,33 @@
import { describe, it, expect } from 'vitest';
import { TestProject } from '../utils';
describe('Manifest Content', () => {
it.each([
{ browser: undefined, outDir: 'chrome-mv3', expected: undefined },
{ browser: 'chrome', outDir: 'chrome-mv3', expected: undefined },
{ browser: 'firefox', outDir: 'firefox-mv2', expected: true },
{ browser: 'safari', outDir: 'safari-mv2', expected: false },
])(
'should respect the per-browser entrypoint option with %j',
async ({ browser, expected, outDir }) => {
const project = new TestProject();
project.addFile(
'entrypoints/background.ts',
`export default defineBackground({
persistent: {
firefox: true,
safari: false,
},
main: () => {},
})`,
);
await project.build({ browser });
const safariManifest = await project.getOutputManifest(
`.output/${outDir}/manifest.json`,
);
expect(safariManifest.background.persistent).toBe(expected);
},
);
});
@@ -272,7 +272,7 @@ describe('Output Directory Structure', () => {
.toMatchInlineSnapshot(`
".output/chrome-mv3/background.js
----------------------------------------
import { l as logHello } from "./chunks/log-BsZv2eRn.js";
import { l as logHello } from "./chunks/log-bezs0tt4.js";
function defineBackground(arg) {
if (typeof arg === "function")
return { main: arg };
@@ -362,13 +362,11 @@ describe('Output Directory Structure', () => {
function logHello(name) {
console.log(\`Hello \${name}!\`);
}
_background;
const definition = defineBackground({
main() {
logHello("background");
}
});
_background;
chrome;
function print(method, ...args) {
return;
@@ -391,7 +389,6 @@ describe('Output Directory Structure', () => {
throw err;
}
})();
_background;
"
`);
});
@@ -119,7 +119,7 @@ describe('User Config', () => {
project.addFile(
'test.config.ts',
`import { defineConfig } from 'wxt';
export default defineConfig({
outDir: ".custom-output",
srcDir: "src",
+8 -16
View File
@@ -1,5 +1,5 @@
import { dirname, relative, resolve } from 'path';
import fs, { mkdir } from 'fs-extra';
import { dirname, join, relative, resolve } from 'path';
import fs from 'fs-extra';
import glob from 'fast-glob';
import { execaCommand } from 'execa';
import {
@@ -13,13 +13,6 @@ import {
import { normalizePath } from '../src/core/utils/paths';
import merge from 'lodash.merge';
// Run "pnpm wxt" to use the "wxt" dev script, not the "wxt" binary from the
// wxt package. This uses the TS files instead of the compiled JS package
// files.
export const WXT_PACKAGE_DIR = resolve(__dirname, '..');
export const E2E_DIR = resolve(WXT_PACKAGE_DIR, 'e2e');
export class TestProject {
files: Array<[string, string]> = [];
config: UserConfig | undefined;
@@ -30,7 +23,7 @@ export class TestProject {
// 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 = resolve(E2E_DIR, 'dist', id);
this.root = join('e2e/dist', id);
this.files.push([
'package.json',
JSON.stringify(
@@ -120,9 +113,6 @@ export class TestProject {
await execaCommand('pnpm --ignore-workspace i --ignore-scripts', {
cwd: this.root,
});
await mkdir(resolve(this.root, 'public'), { recursive: true }).catch(
() => {},
);
}
/**
@@ -141,7 +131,9 @@ export class TestProject {
* that can be used in a snapshot.
*/
serializeWxtDir(): Promise<string> {
return this.serializeDir(resolve(this.root, '.wxt/types'));
return this.serializeDir(
resolve(this.config?.srcDir ?? this.root, '.wxt/types'),
);
}
/**
@@ -183,8 +175,8 @@ export class TestProject {
].join(`\n${''.padEnd(40, '-')}\n`);
}
fileExists(...path: string[]): Promise<boolean> {
return fs.exists(this.resolvePath(...path));
fileExists(path: string): Promise<boolean> {
return fs.exists(this.resolvePath(path));
}
async getOutputManifest(
+145 -11
View File
@@ -1,15 +1,102 @@
{
"private": true,
"name": "wxt",
"type": "module",
"version": "0.17.4-alpha1",
"description": "Next gen framework for developing web extensions",
"engines": {
"node": ">=18",
"pnpm": ">=8"
},
"packageManager": "pnpm@8.6.3",
"repository": {
"type": "git",
"url": "https://github.com/wxt-dev/wxt"
},
"homepage": "https://wxt.dev",
"keywords": [
"vite",
"chrome",
"web",
"extension",
"browser",
"bundler",
"framework"
],
"author": {
"name": "Aaron Klinker",
"email": "aaronklinker1+wxt@gmail.com"
},
"license": "MIT",
"files": [
"bin",
"dist"
],
"bin": {
"wxt": "./bin/wxt.mjs",
"wxt-publish-extension": "./bin/wxt-publish-extension.cjs"
},
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"./client": {
"types": "./dist/client.d.ts",
"import": "./dist/client.js"
},
"./sandbox": {
"types": "./dist/sandbox.d.ts",
"import": "./dist/sandbox.js"
},
"./browser": {
"types": "./dist/browser.d.ts",
"import": "./dist/browser.js"
},
"./testing": {
"import": {
"types": "./dist/testing.d.ts",
"default": "./dist/testing.js"
},
"require": {
"types": "./dist/testing.d.cts",
"default": "./dist/testing.cjs"
}
},
"./storage": {
"import": {
"types": "./dist/storage.d.ts",
"default": "./dist/storage.js"
},
"require": {
"types": "./dist/storage.d.cts",
"default": "./dist/storage.cjs"
}
},
"./vite-builder-env": {
"types": "./dist/vite-builder-env.d.ts"
}
},
"scripts": {
"check": "check && pnpm -r run check",
"wxt": "tsx src/cli/index.ts",
"build": "tsx scripts/build.ts",
"format": "prettier --write .",
"format:check": "prettier --check .",
"lint": "run-p -c -s lint:*",
"lint:eslint": "echo 'ESLint: TODO'",
"lint:package": "publint",
"compile": "run-s -c compile:*",
"compile:wxt": "tsc --noEmit",
"compile:virtual": "tsc --noEmit -p src/virtual",
"test": "vitest",
"test:coverage": "vitest run --coverage.enabled \"--coverage.include=packages/wxt/src/**\" \"--coverage.exclude=packages/wxt/src/core/utils/testing/**\" \"--coverage.exclude=**/*.d.ts\" \"--coverage.exclude=**/fixtures/**\"",
"test:coverage": "vitest run --coverage.enabled \"--coverage.include=src/**\" \"--coverage.exclude=src/core/utils/testing/**\" \"--coverage.exclude=**/*.d.ts\"",
"prepare": "simple-git-hooks",
"prepublish": "pnpm -s build",
"docs:gen": "typedoc --options docs/typedoc.json",
@@ -18,26 +105,73 @@
"docs:preview": "pnpm -s docs:gen && vitepress preview docs",
"sync-releases": "pnpx changelogen@latest gh release"
},
"devDependencies": {
"@aklinker1/check": "^1.1.1",
"@vitest/coverage-v8": "^1.0.1",
"dependencies": {
"@aklinker1/rollup-plugin-visualizer": "5.12.0",
"@types/webextension-polyfill": "^0.10.5",
"@webext-core/fake-browser": "^1.3.1",
"@webext-core/isolated-element": "^1.1.1",
"@webext-core/match-patterns": "^1.0.3",
"async-mutex": "^0.4.0",
"c12": "^1.5.1",
"cac": "^6.7.14",
"chokidar": "^3.5.3",
"consola": "^3.2.3",
"defu": "^6.1.3",
"dequal": "^2.0.3",
"esbuild": "^0.19.5",
"fast-glob": "^3.3.1",
"filesize": "^10.0.8",
"fs-extra": "^11.1.1",
"get-port": "^7.0.0",
"giget": "^1.1.3",
"hookable": "^5.5.3",
"is-wsl": "^3.0.0",
"jiti": "^1.21.0",
"json5": "^2.2.3",
"linkedom": "^0.16.1",
"minimatch": "^9.0.3",
"natural-compare": "^1.4.0",
"normalize-path": "^3.0.0",
"ora": "^7.0.1",
"picocolors": "^1.0.0",
"prompts": "^2.4.2",
"publish-browser-extension": "^2.1.3",
"unimport": "^3.4.0",
"vite": "^5.1.3",
"web-ext-run": "^0.2.0",
"webextension-polyfill": "^0.10.0",
"zip-dir": "^2.0.0"
},
"devDependencies": {
"@faker-js/faker": "^8.3.1",
"@types/fs-extra": "^11.0.4",
"@types/lodash.merge": "^4.6.9",
"@types/natural-compare": "^1.4.3",
"@types/node": "^20.10.3",
"@types/normalize-path": "^3.0.2",
"@types/prompts": "^2.4.9",
"@vitest/coverage-v8": "^1.0.1",
"execa": "^8.0.1",
"happy-dom": "^13.3.8",
"lint-staged": "^15.2.0",
"lodash.merge": "^4.6.2",
"npm-run-all": "^4.1.5",
"p-map": "^7.0.0",
"prettier": "^3.1.0",
"publint": "^0.2.6",
"simple-git-hooks": "^2.9.0",
"tsup": "^8.0.1",
"tsx": "^4.6.2",
"typedoc": "^0.25.4",
"typedoc-plugin-markdown": "4.0.0-next.23",
"typedoc-vitepress-theme": "1.0.0-next.3",
"typescript": "^5.3.2",
"vitepress": "1.0.0-rc.34",
"vitest": "^1.5.3",
"vitest": "^1.2.2",
"vitest-mock-extended": "^1.3.1",
"vitest-plugin-random-seed": "^1.0.2",
"vue": "^3.3.10",
"wxt": "workspace:*"
"vue": "^3.3.10"
},
"packageManager": "pnpm@8.6.3",
"simple-git-hooks": {
"pre-commit": "pnpm lint-staged"
},
-31
View File
@@ -1,31 +0,0 @@
{
"name": "wxt-demo",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "pnpm build:deps && wxt",
"build:deps": "pnpm --filter wxt build",
"build": "pnpm build:deps && wxt build",
"build:all": "pnpm build:deps && run-s -s 'build:all:*'",
"build:all:chrome-mv3": "wxt build",
"build:all:chrome-mv2": "wxt build --mv2",
"build:all:firefox-mv3": "wxt build -b firefox --mv3",
"build:all:firefox-mv2": "wxt build -b firefox",
"test": "pnpm build:deps && vitest",
"zip": "pnpm build:deps && wxt zip",
"check": "pnpm build:deps && check",
"postinstall": "pnpm build:deps && wxt prepare"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.34",
"@types/react-dom": "^18.2.14",
"sass": "^1.69.5",
"typescript": "^5.3.2",
"wxt": "workspace:*"
}
}
@@ -1,36 +0,0 @@
import { describe, it, expect } from 'vitest';
import { TestProject } from '../utils';
describe.each([true, false])(
'Manifest Content (Vite runtime? %s)',
(viteRuntime) => {
it.each([
{ browser: undefined, outDir: 'chrome-mv3', expected: undefined },
{ browser: 'chrome', outDir: 'chrome-mv3', expected: undefined },
{ browser: 'firefox', outDir: 'firefox-mv2', expected: true },
{ browser: 'safari', outDir: 'safari-mv2', expected: false },
])(
'should respect the per-browser entrypoint option with %j',
async ({ browser, expected, outDir }) => {
const project = new TestProject();
project.addFile(
'entrypoints/background.ts',
`export default defineBackground({
persistent: {
firefox: true,
safari: false,
},
main: () => {},
})`,
);
await project.build({ browser, experimental: { viteRuntime } });
const safariManifest = await project.getOutputManifest(
`.output/${outDir}/manifest.json`,
);
expect(safariManifest.background.persistent).toBe(expected);
},
);
},
);
-99
View File
@@ -1,99 +0,0 @@
import { describe, expect, it } from 'vitest';
import { TestProject } from '../utils';
import extract from 'extract-zip';
import { execaCommand } from 'execa';
import { readFile, writeFile } from 'fs-extra';
process.env.WXT_PNPM_IGNORE_WORKSPACE = 'true';
describe('Zipping', () => {
it('should download packages and produce a valid build when zipping sources', async () => {
const project = new TestProject({
name: 'test',
version: '1.0.0',
dependencies: {
flatten: '1.0.3',
},
});
project.addFile(
'entrypoints/background.ts',
'export default defineBackground(() => {});',
);
const unzipDir = project.resolvePath('.output/test-1.0.0-sources');
const sourcesZip = project.resolvePath('.output/test-1.0.0-sources.zip');
await project.zip({
browser: 'firefox',
zip: { downloadPackages: ['flatten'] },
});
expect(await project.fileExists('.output/')).toBe(true);
await extract(sourcesZip, { dir: unzipDir });
// Update package json wxt path
const packageJsonPath = project.resolvePath(unzipDir, 'package.json');
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'));
packageJson.dependencies.wxt = '../../../../..';
await writeFile(
packageJsonPath,
JSON.stringify(packageJson, null, 2),
'utf-8',
);
// Build zipped extension
await expect(
execaCommand('pnpm i --ignore-workspace --frozen-lockfile false', {
cwd: unzipDir,
}),
).resolves.toMatchObject({ exitCode: 0 });
await expect(
execaCommand('pnpm wxt build -b firefox', { cwd: unzipDir }),
).resolves.toMatchObject({ exitCode: 0 });
await expect(project.fileExists(unzipDir, '.output')).resolves.toBe(true);
expect(
await project.serializeFile(
project.resolvePath(unzipDir, 'package.json'),
),
).toMatchInlineSnapshot(`
".output/test-1.0.0-sources/package.json
----------------------------------------
{
"name": "test",
"description": "Example description",
"version": "1.0.0",
"dependencies": {
"wxt": "../../../../..",
"flatten": "1.0.3"
},
"resolutions": {
"flatten@1.0.3": "file://./.wxt/local_modules/flatten-1.0.3.tgz"
}
}"
`);
});
it('should correctly apply template variables for zip file names based on provided config', async () => {
const project = new TestProject({
name: 'test',
version: '1.0.0',
});
project.addFile(
'entrypoints/background.ts',
'export default defineBackground(() => {});',
);
const artifactZip = '.output/test-1.0.0-firefox-development.zip';
const sourcesZip = '.output/test-1.0.0-development-sources.zip';
await project.zip({
browser: 'firefox',
mode: 'development',
zip: {
artifactTemplate: '{{name}}-{{version}}-{{browser}}-{{mode}}.zip',
sourcesTemplate: '{{name}}-{{version}}-{{mode}}-sources.zip',
},
});
expect(await project.fileExists(artifactZip)).toBe(true);
expect(await project.fileExists(sourcesZip)).toBe(true);
});
});
-152
View File
@@ -1,152 +0,0 @@
{
"name": "wxt",
"type": "module",
"version": "0.18.0",
"description": "Next gen framework for developing web extensions",
"repository": {
"type": "git",
"url": "https://github.com/wxt-dev/wxt"
},
"homepage": "https://wxt.dev",
"keywords": [
"vite",
"chrome",
"web",
"extension",
"browser",
"bundler",
"framework"
],
"author": {
"name": "Aaron Klinker",
"email": "aaronklinker1+wxt@gmail.com"
},
"license": "MIT",
"files": [
"bin",
"dist"
],
"bin": {
"wxt": "./bin/wxt.mjs",
"wxt-publish-extension": "./bin/wxt-publish-extension.cjs"
},
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"./client": {
"types": "./dist/client.d.ts",
"import": "./dist/client.js"
},
"./sandbox": {
"types": "./dist/sandbox.d.ts",
"import": "./dist/sandbox.js"
},
"./browser": {
"types": "./dist/browser.d.ts",
"import": "./dist/browser.js"
},
"./testing": {
"import": {
"types": "./dist/testing.d.ts",
"default": "./dist/testing.js"
},
"require": {
"types": "./dist/testing.d.cts",
"default": "./dist/testing.cjs"
}
},
"./storage": {
"import": {
"types": "./dist/storage.d.ts",
"default": "./dist/storage.js"
},
"require": {
"types": "./dist/storage.d.cts",
"default": "./dist/storage.cjs"
}
},
"./vite-builder-env": {
"types": "./dist/vite-builder-env.d.ts"
}
},
"scripts": {
"wxt": "tsx src/cli/index.ts",
"build": "tsx scripts/build.ts",
"check": "run-s -c check:*",
"check:default": "check",
"check:tsc-virtual": "tsc --noEmit -p src/virtual",
"test": "vitest",
"test:e2e": "vitest -r e2e",
"test:e2e:2": "vitest --config e2e/vitest.config.ts"
},
"dependencies": {
"@aklinker1/rollup-plugin-visualizer": "5.12.0",
"@types/webextension-polyfill": "^0.10.5",
"@webext-core/fake-browser": "^1.3.1",
"@webext-core/isolated-element": "^1.1.2",
"@webext-core/match-patterns": "^1.0.3",
"async-mutex": "^0.4.0",
"c12": "^1.5.1",
"cac": "^6.7.14",
"chokidar": "^3.5.3",
"ci-info": "^4.0.0",
"defu": "^6.1.3",
"dequal": "^2.0.3",
"esbuild": "^0.19.5",
"fast-glob": "^3.3.1",
"filesize": "^10.0.8",
"fs-extra": "^11.1.1",
"get-port": "^7.0.0",
"giget": "^1.1.3",
"hookable": "^5.5.3",
"is-wsl": "^3.0.0",
"jiti": "^1.21.0",
"json5": "^2.2.3",
"jszip": "^3.10.1",
"linkedom": "^0.16.1",
"magicast": "^0.3.4",
"minimatch": "^9.0.3",
"natural-compare": "^1.4.0",
"normalize-path": "^3.0.0",
"nypm": "^0.3.6",
"open": "^10.1.0",
"ora": "^7.0.1",
"picocolors": "^1.0.0",
"prompts": "^2.4.2",
"publish-browser-extension": "^2.1.3",
"unimport": "^3.4.0",
"vite": "^5.2.8",
"web-ext-run": "^0.2.0",
"webextension-polyfill": "^0.10.0"
},
"devDependencies": {
"@aklinker1/check": "^1.1.1",
"@faker-js/faker": "^8.3.1",
"@types/fs-extra": "^11.0.4",
"@types/lodash.merge": "^4.6.9",
"@types/natural-compare": "^1.4.3",
"@types/node": "^20.10.3",
"@types/normalize-path": "^3.0.2",
"@types/prompts": "^2.4.9",
"execa": "^8.0.1",
"extract-zip": "^2.0.1",
"happy-dom": "^13.3.8",
"lodash.merge": "^4.6.2",
"p-map": "^7.0.0",
"publint": "^0.2.6",
"tsup": "^8.0.1",
"tsx": "^4.6.2",
"typescript": "^5.3.2"
}
}
@@ -1,12 +0,0 @@
import { a } from './test';
function defineSomething<T>(config: T): T {
return config;
}
export default defineSomething({
option: 'some value',
main: () => {
console.log('main', a);
},
});
@@ -1,2 +0,0 @@
console.log('Side-effect in test.ts');
export const a = 'a';
@@ -1,22 +0,0 @@
import { describe, it, expect } from 'vitest';
import { createViteBuilder } from '../index';
import { fakeResolvedConfig } from '~/core/utils/testing/fake-objects';
import { createHooks } from 'hookable';
describe('Vite Builder', () => {
describe('importEntrypoint', () => {
it('should import entrypoints, removing runtime values (like the main function)', async () => {
const {
default: { main: _, ...expected },
} = await import('./fixtures/module');
const builder = await createViteBuilder(
fakeResolvedConfig({ root: __dirname }),
createHooks(),
);
const actual = await builder.importEntrypoint<{ default: any }>(
'./fixtures/module.ts',
);
expect(actual).toEqual(expected);
});
});
});
@@ -1,21 +0,0 @@
import { ResolvedConfig } from '~/types';
import * as vite from 'vite';
import { normalizePath } from '~/core/utils/paths';
import { removeMainFunctionCode } from '~/core/utils/transform';
import { resolve } from 'node:path';
/**
* Transforms entrypoints, removing the main function from the entrypoint if it exists.
*/
export function removeEntrypointMainFunction(
config: ResolvedConfig,
path: string,
): vite.Plugin {
const absPath = normalizePath(resolve(config.root, path));
return {
name: 'wxt:remove-entrypoint-main-function',
transform(code, id) {
if (id === absPath) return removeMainFunctionCode(code);
},
};
}
@@ -1,41 +0,0 @@
import { Plugin } from 'vite';
import { ResolvedConfig } from '~/types';
import { normalizePath } from '~/core/utils/paths';
import {
VirtualModuleId,
virtualModuleNames,
} from '~/core/utils/virtual-modules';
import fs from 'fs-extra';
import { resolve } from 'path';
/**
* Resolve all the virtual modules to the `node_modules/wxt/dist/virtual` directory.
*/
export function resolveVirtualModules(config: ResolvedConfig): Plugin[] {
return virtualModuleNames.map((name) => {
const virtualId: `${VirtualModuleId}?` = `virtual:wxt-${name}?`;
const resolvedVirtualId = '\0' + virtualId;
return {
name: `wxt:resolve-virtual-${name}`,
resolveId(id) {
// Id doesn't start with prefix, it looks like this:
// /path/to/project/virtual:wxt-background?/path/to/project/entrypoints/background.ts
const index = id.indexOf(virtualId);
if (index === -1) return;
const inputPath = normalizePath(id.substring(index + virtualId.length));
return resolvedVirtualId + inputPath;
},
async load(id) {
if (!id.startsWith(resolvedVirtualId)) return;
const inputPath = id.replace(resolvedVirtualId, '');
const template = await fs.readFile(
resolve(config.wxtModuleDir, `dist/virtual/${name}.js`),
'utf-8',
);
return template.replace(`virtual:user-${name}`, inputPath);
},
};
});
}
@@ -1,29 +0,0 @@
import { describe, expect, it } from 'vitest';
import path from 'node:path';
import { bun } from '../bun';
const cwd = path.resolve(__dirname, 'fixtures/bun-project');
describe.skipIf(() => process.platform === 'win32')(
'Bun Package Management Utils',
() => {
describe('listDependencies', () => {
it('should list direct dependencies', async () => {
const actual = await bun.listDependencies({ cwd });
expect(actual).toEqual([
{ name: 'flatten', version: '1.0.3' },
{ name: 'mime-types', version: '2.1.35' },
]);
});
it('should list all dependencies', async () => {
const actual = await bun.listDependencies({ cwd, all: true });
expect(actual).toEqual([
{ name: 'flatten', version: '1.0.3' },
{ name: 'mime-db', version: '1.52.0' },
{ name: 'mime-types', version: '2.1.35' },
]);
});
});
},
);
@@ -1,9 +0,0 @@
{
"name": "bun-ls",
"dependencies": {
"mime-types": "2.1.35"
},
"devDependencies": {
"flatten": "1.0.3"
}
}
@@ -1,42 +0,0 @@
{
"name": "npm-ls",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "npm-ls",
"dependencies": {
"mime-types": "2.1.35"
},
"devDependencies": {
"flatten": "1.0.3"
}
},
"node_modules/flatten": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.3.tgz",
"integrity": "sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==",
"deprecated": "flatten is deprecated in favor of utility frameworks such as lodash.",
"dev": true
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
}
}
}
@@ -1,9 +0,0 @@
{
"name": "npm-ls",
"dependencies": {
"mime-types": "2.1.35"
},
"devDependencies": {
"flatten": "1.0.3"
}
}
@@ -1,9 +0,0 @@
{
"name": "pnpm-ls",
"dependencies": {
"mime-types": "2.1.35"
},
"devDependencies": {
"flatten": "1.0.3"
}
}
@@ -1,34 +0,0 @@
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies:
mime-types:
specifier: 2.1.35
version: 2.1.35
devDependencies:
flatten:
specifier: 1.0.3
version: 1.0.3
packages:
/flatten@1.0.3:
resolution: {integrity: sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==}
deprecated: flatten is deprecated in favor of utility frameworks such as lodash.
dev: true
/mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
dev: false
/mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
dependencies:
mime-db: 1.52.0
dev: false
@@ -1,10 +0,0 @@
{
"name": "yarn-ls",
"packageManager": "yarn@1.22.19",
"dependencies": {
"mime-types": "2.1.35"
},
"devDependencies": {
"flatten": "1.0.3"
}
}
@@ -1,20 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
flatten@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.3.tgz#c1283ac9f27b368abc1e36d1ff7b04501a30356b"
integrity sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==
mime-db@1.52.0:
version "1.52.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
mime-types@2.1.35:
version "2.1.35"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
dependencies:
mime-db "1.52.0"
@@ -1,46 +0,0 @@
import { beforeAll, describe, expect, it } from 'vitest';
import path from 'node:path';
import { npm } from '../npm';
import { execaCommand } from 'execa';
import { exists } from 'fs-extra';
const cwd = path.resolve(__dirname, 'fixtures/npm-project');
describe('NPM Package Management Utils', () => {
beforeAll(async () => {
// NPM needs the modules installed for 'npm ls' to work
await execaCommand('npm i', { cwd });
}, 60e3);
describe('listDependencies', () => {
it('should list direct dependencies', async () => {
const actual = await npm.listDependencies({ cwd });
expect(actual).toEqual([
{ name: 'flatten', version: '1.0.3' },
{ name: 'mime-types', version: '2.1.35' },
]);
});
it('should list all dependencies', async () => {
const actual = await npm.listDependencies({ cwd, all: true });
expect(actual).toEqual([
{ name: 'flatten', version: '1.0.3' },
{ name: 'mime-types', version: '2.1.35' },
{ name: 'mime-db', version: '1.52.0' },
]);
});
});
describe('downloadDependency', () => {
it('should download the dependency as a tarball', async () => {
const downloadDir = path.resolve(cwd, 'dist');
const id = 'mime-db@1.52.0';
const expected = path.resolve(downloadDir, 'mime-db-1.52.0.tgz');
const actual = await npm.downloadDependency(id, downloadDir);
expect(actual).toEqual(expected);
expect(await exists(actual)).toBe(true);
});
});
});
@@ -1,34 +0,0 @@
import { beforeAll, describe, expect, it } from 'vitest';
import path from 'node:path';
import { pnpm } from '../pnpm';
import { execaCommand } from 'execa';
const cwd = path.resolve(__dirname, 'fixtures/pnpm-project');
process.env.WXT_PNPM_IGNORE_WORKSPACE = 'true';
describe('PNPM Package Management Utils', () => {
beforeAll(async () => {
// PNPM needs the modules installed, or 'pnpm ls' will return a blank list.
await execaCommand('pnpm i --ignore-workspace', { cwd });
});
describe('listDependencies', () => {
it('should list direct dependencies', async () => {
const actual = await pnpm.listDependencies({ cwd });
expect(actual).toEqual([
{ name: 'flatten', version: '1.0.3' },
{ name: 'mime-types', version: '2.1.35' },
]);
});
it('should list all dependencies', async () => {
const actual = await pnpm.listDependencies({ cwd, all: true });
expect(actual).toEqual([
{ name: 'flatten', version: '1.0.3' },
{ name: 'mime-types', version: '2.1.35' },
{ name: 'mime-db', version: '1.52.0' },
]);
});
});
});
@@ -1,27 +0,0 @@
import { describe, expect, it } from 'vitest';
import path from 'node:path';
import { yarn } from '../yarn';
const cwd = path.resolve(__dirname, 'fixtures/yarn-project');
describe('Yarn Package Management Utils', () => {
describe('listDependencies', () => {
it('should list direct dependencies', async () => {
const actual = await yarn.listDependencies({ cwd });
expect(actual).toEqual([
{ name: 'mime-db', version: '1.52.0' },
{ name: 'flatten', version: '1.0.3' },
{ name: 'mime-types', version: '2.1.35' },
]);
});
it('should list all dependencies', async () => {
const actual = await yarn.listDependencies({ cwd, all: true });
expect(actual).toEqual([
{ name: 'mime-db', version: '1.52.0' },
{ name: 'flatten', version: '1.0.3' },
{ name: 'mime-types', version: '2.1.35' },
]);
});
});
});
@@ -1,27 +0,0 @@
import { dedupeDependencies, npm } from './npm';
import { WxtPackageManagerImpl } from './types';
export const bun: WxtPackageManagerImpl = {
overridesKey: 'overrides', // But also supports "resolutions"
downloadDependency(...args) {
return npm.downloadDependency(...args);
},
async listDependencies(options) {
const args = ['pm', 'ls'];
if (options?.all) {
args.push('--all');
}
const { execa } = await import('execa');
const res = await execa('bun', args, { cwd: options?.cwd });
return dedupeDependencies(
res.stdout
.split('\n')
.slice(1) // Skip the first line, is not a dependency
.map((line) => line.trim())
.map((line) => /.* (@?\S+)@(\S+)$/.exec(line))
// @ts-expect-error: Filtering to known non-null matches
.filter<RegExpExecArray>((match) => !!match)
.map(([_, name, version]) => ({ name, version })),
);
},
};
@@ -1,76 +0,0 @@
import {
detectPackageManager,
addDependency,
addDevDependency,
ensureDependencyInstalled,
installDependencies,
removeDependency,
PackageManager,
PackageManagerName,
} from 'nypm';
import { WxtPackageManager } from '~/types';
import { bun } from './bun';
import { WxtPackageManagerImpl } from './types';
import { yarn } from './yarn';
import { pnpm } from './pnpm';
import { npm } from './npm';
export async function createWxtPackageManager(
root: string,
): Promise<WxtPackageManager> {
const pm = await detectPackageManager(root, {
includeParentDirs: true,
});
// Use requirePm to prevent throwing errors before the package manager utils are used.
const requirePm = <T>(cb: (pm: PackageManager) => T) => {
if (pm == null) throw Error('Could not detect package manager');
return cb(pm);
};
return {
get name() {
return requirePm((pm) => pm.name);
},
get command() {
return requirePm((pm) => pm.command);
},
get version() {
return requirePm((pm) => pm.version);
},
get majorVersion() {
return requirePm((pm) => pm.majorVersion);
},
get lockFile() {
return requirePm((pm) => pm.lockFile);
},
get files() {
return requirePm((pm) => pm.files);
},
addDependency,
addDevDependency,
ensureDependencyInstalled,
installDependencies,
removeDependency,
get overridesKey() {
return requirePm((pm) => packageManagers[pm.name].overridesKey);
},
downloadDependency(...args) {
return requirePm((pm) =>
packageManagers[pm.name].downloadDependency(...args),
);
},
listDependencies(...args) {
return requirePm((pm) =>
packageManagers[pm.name].listDependencies(...args),
);
},
};
}
const packageManagers: Record<PackageManagerName, WxtPackageManagerImpl> = {
npm,
pnpm,
bun,
yarn,
};
@@ -1,85 +0,0 @@
import { Dependency } from '~/types';
import { WxtPackageManagerImpl } from './types';
import path from 'node:path';
import { ensureDir } from 'fs-extra';
export const npm: WxtPackageManagerImpl = {
overridesKey: 'overrides',
async downloadDependency(id, downloadDir) {
await ensureDir(downloadDir);
const { execa } = await import('execa');
const res = await execa('npm', ['pack', id, '--json'], {
cwd: downloadDir,
});
const packed: PackedDependency[] = JSON.parse(res.stdout);
return path.resolve(downloadDir, packed[0].filename);
},
async listDependencies(options) {
const args = ['ls', '--json'];
if (options?.all) {
args.push('--depth', 'Infinity');
}
const { execa } = await import('execa');
const res = await execa('npm', args, { cwd: options?.cwd });
const project: NpmListProject = JSON.parse(res.stdout);
return flattenNpmListOutput([project]);
},
};
export function flattenNpmListOutput(projects: NpmListProject[]): Dependency[] {
const queue: Record<string, NpmListDependency>[] = projects.flatMap(
(project) => {
const acc: Record<string, NpmListDependency>[] = [];
if (project.dependencies) acc.push(project.dependencies);
if (project.devDependencies) acc.push(project.devDependencies);
return acc;
},
);
const dependencies: Dependency[] = [];
while (queue.length > 0) {
Object.entries(queue.pop()!).forEach(([name, meta]) => {
dependencies.push({
name,
version: meta.version,
});
if (meta.dependencies) queue.push(meta.dependencies);
if (meta.devDependencies) queue.push(meta.devDependencies);
});
}
return dedupeDependencies(dependencies);
}
export function dedupeDependencies(dependencies: Dependency[]): Dependency[] {
const hashes = new Set<string>();
return dependencies.filter((dep) => {
const hash = `${dep.name}@${dep.version}`;
if (hashes.has(hash)) {
return false;
} else {
hashes.add(hash);
return true;
}
});
}
export interface NpmListProject {
name: string;
dependencies?: Record<string, NpmListDependency>;
devDependencies?: Record<string, NpmListDependency>;
}
export interface NpmListDependency {
version: string;
resolved?: string;
overridden?: boolean;
dependencies?: Record<string, NpmListDependency>;
devDependencies?: Record<string, NpmListDependency>;
}
interface PackedDependency {
id: string;
name: string;
version: string;
filename: string;
}
@@ -1,28 +0,0 @@
import { NpmListProject, flattenNpmListOutput, npm } from './npm';
import { WxtPackageManagerImpl } from './types';
export const pnpm: WxtPackageManagerImpl = {
overridesKey: 'resolutions', // "pnpm.overrides" has a higher priority, but I don't want to deal with nesting
downloadDependency(...args) {
return npm.downloadDependency(...args);
},
async listDependencies(options) {
const args = ['ls', '-r', '--json'];
if (options?.all) {
args.push('--depth', 'Infinity');
}
// Helper for testing - since WXT uses pnpm workspaces, folders inside it don't behave like
// standalone projects unless you pass the --ignore-workspace flag.
if (
typeof process !== 'undefined' &&
process.env.WXT_PNPM_IGNORE_WORKSPACE === 'true'
) {
args.push('--ignore-workspace');
}
const { execa } = await import('execa');
const res = await execa('pnpm', args, { cwd: options?.cwd });
const projects: NpmListProject[] = JSON.parse(res.stdout);
return flattenNpmListOutput(projects);
},
};
@@ -1,6 +0,0 @@
import { WxtPackageManager } from '~/types';
export type WxtPackageManagerImpl = Pick<
WxtPackageManager,
'downloadDependency' | 'listDependencies' | 'overridesKey'
>;
@@ -1,54 +0,0 @@
import { Dependency } from '~/types';
import { WxtPackageManagerImpl } from './types';
import { dedupeDependencies, npm } from './npm';
export const yarn: WxtPackageManagerImpl = {
overridesKey: 'resolutions',
downloadDependency(...args) {
return npm.downloadDependency(...args);
},
async listDependencies(options) {
const args = ['list', '--json'];
if (options?.all) {
args.push('--depth', 'Infinity');
}
const { execa } = await import('execa');
const res = await execa('yarn', args, { cwd: options?.cwd });
const tree = res.stdout
.split('\n')
.map<JsonLine>((line) => JSON.parse(line))
.find((line) => line.type === 'tree')?.data as JsonLineTree | undefined;
if (tree == null) throw Error("'yarn list --json' did not output a tree");
const queue = [...tree.trees];
const dependencies: Dependency[] = [];
while (queue.length > 0) {
const { name: treeName, children } = queue.pop()!;
const match = /(@?\S+)@(\S+)$/.exec(treeName);
if (match) {
const [_, name, version] = match;
dependencies.push({ name, version });
}
if (children != null) {
queue.push(...children);
}
}
return dedupeDependencies(dependencies);
},
};
type JsonLine =
| { type: unknown; data: unknown }
| { type: 'tree'; data: JsonLineTree };
interface JsonLineTree {
type: 'list';
trees: Tree[];
}
interface Tree {
name: string;
children?: Tree[];
}
@@ -1,51 +0,0 @@
import { describe, it, expect } from 'vitest';
import { removeMainFunctionCode } from '../transform';
describe('Transform Utils', () => {
describe('removeMainFunctionCode', () => {
it.each(['defineBackground', 'defineUnlistedScript'])(
'should remove the first arrow function argument for %s',
(def) => {
const input = `export default ${def}(() => {
console.log();
})`;
const expected = `export default ${def}(() => {})`;
const actual = removeMainFunctionCode(input).code;
expect(actual).toEqual(expected);
},
);
it.each(['defineBackground', 'defineUnlistedScript'])(
'should remove the first function argument for %s',
(def) => {
const input = `export default ${def}(function () {
console.log();
})`;
const expected = `export default ${def}(function () {})`;
const actual = removeMainFunctionCode(input).code;
expect(actual).toEqual(expected);
},
);
it.each([
'defineBackground',
'defineContentScript',
'defineUnlistedScript',
])('should remove the main field from %s', (def) => {
const input = `export default ${def}({
asdf: "asdf",
main: () => {},
})`;
const expected = `export default ${def}({
asdf: "asdf"
})`;
const actual = removeMainFunctionCode(input).code;
expect(actual).toEqual(expected);
});
});
});
@@ -1,20 +0,0 @@
import { describe, it } from 'vitest';
import {
VirtualModuleId,
VirtualModuleName,
VirtualEntrypointType,
VirtualEntrypointModuleName,
} from '../virtual-modules';
describe('Virtual Modules', () => {
it('should resolve types to litteral values, not string', () => {
// @ts-expect-error
const _c: VirtualEntrypointType = '';
// @ts-expect-error
const _d: VirtualEntrypointModuleName = '';
// @ts-expect-error
const _b: VirtualModuleName = '';
// @ts-expect-error
const _a: VirtualModuleId = '';
});
});
-33
View File
@@ -1,33 +0,0 @@
import { ProxifiedModule, parseModule } from 'magicast';
/**
* Removes any code used at runtime related to an entrypoint's main function.
* - Removes or clears out `main` function from returned object
* - TODO: Removes unused imports after main function has been removed to prevent importing runtime modules
*/
export function removeMainFunctionCode(code: string): {
code: string;
map?: string;
} {
const mod = parseModule(code);
emptyMainFunction(mod);
return mod.generate();
}
function emptyMainFunction(mod: ProxifiedModule) {
if (mod.exports?.default?.$type === 'function-call') {
if (mod.exports.default.$ast?.arguments?.[0]?.body) {
// Remove body from function
// ex: "fn(() => { ... })" to "fn(() => {})"
// ex: "fn(function () { ... })" to "fn(function () {})"
mod.exports.default.$ast.arguments[0].body.body = [];
} else if (mod.exports.default.$ast?.arguments?.[0]?.properties) {
// Remove main field from object
// ex: "fn({ ..., main: () => {} })" to "fn({ ... })"
mod.exports.default.$ast.arguments[0].properties =
mod.exports.default.$ast.arguments[0].properties.filter(
(prop: any) => prop.key.name !== 'main',
);
}
}
}

Some files were not shown because too many files have changed in this diff Show More