Compare commits

...

19 Commits

Author SHA1 Message Date
GitHub Actions 64610bad41 chore(release): v0.17.4 2024-02-28 18:28:34 +00:00
Aaron 45b7d7c67f fix: Resolve the path to node_modules/wxt correctly (#498) 2024-02-28 12:25:04 -06:00
Aaron 0bb2746869 feat: Add ResolvedConfig.wxtModuleDir, resolving the directory once (#497) 2024-02-28 11:52:46 -06:00
Aaron Klinker f9b0aa45f8 chore: Fix tests after template change 2024-02-28 11:34:15 -06:00
btea 6f6ce33bbd feat: Add basic content script to templates (#495)
Co-authored-by: Aaron Klinker <aaronklinker1@gmail.com>
2024-02-28 11:30:53 -06:00
Aaron Klinker e1c6020997 docs: Add testing example for ContentScriptContext
Apart of #491
2024-02-28 11:17:20 -06:00
Aaron Klinker 012bd7e67f docs: Update context usage 2024-02-28 11:16:56 -06:00
Aaron Klinker 35778f70cb docs: Update comparison page 2024-02-28 10:39:47 -06:00
Aaron Klinker cb4f9aa752 docs: Add CRXJS to comparison page 2024-02-28 08:05:39 -06:00
Aaron Klinker f80fb42483 docs: Fix typos 2024-02-28 07:42:22 -06:00
Leo Shklovskii 02c79ca244 docs: Added DocVersionRedirector to "Using WXT" section (#492) 2024-02-27 23:47:10 -06:00
Qiwei Yang 50e8c86ce9 doc: Add markdown sticky ntoes (#488) 2024-02-25 13:37:10 -06:00
GitHub Actions 6fbfc15be7 chore(release): v0.17.3 2024-02-24 16:43:31 +00:00
Aaron c793d3cf6b fix(storage): Revert #478 and run migrations when item is defined and properly wait for migrations before allowing read/writes (#487) 2024-02-24 10:40:03 -06:00
Aaron Klinker b5f4d8cc1b feat(storage): Guarantee storage.getItems returns values in the same order as requested 2024-02-24 08:56:45 -06:00
Aaron Klinker 77e6d1f5a4 fix: Content scripts crash when using storage.defineItem 2024-02-24 08:23:12 -06:00
GitHub Actions 3f149ac4fa chore(release): v0.17.2 2024-02-23 21:49:55 +00:00
Aaron c0dbffaa82 fix: Don't use sub-dependency binaries directly (#482) 2024-02-23 15:45:18 -06:00
Aaron Klinker 707f03a3eb Update changelog 2024-02-23 12:55:55 -06:00
31 changed files with 296 additions and 201 deletions
+53 -1
View File
@@ -1,5 +1,57 @@
# Changelog
## 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)
### 🚀 Enhancements
- **storage:** Guarantee `storage.getItems` returns values in the same order as requested ([b5f4d8c](https://github.com/wxt-dev/wxt/commit/b5f4d8c))
### 🩹 Fixes
- Content scripts crash when using `storage.defineItem` ([77e6d1f](https://github.com/wxt-dev/wxt/commit/77e6d1f))
- **storage:** Revert #478 and run migrations when item is defined and properly wait for migrations before allowing read/writes ([#487](https://github.com/wxt-dev/wxt/pull/487), [#478](https://github.com/wxt-dev/wxt/issues/478))
## v0.17.2
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.1...v0.17.2)
### 🩹 Fixes
- Don't use sub-dependency binaries directly ([#482](https://github.com/wxt-dev/wxt/pull/482))
## v0.17.1
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.0...v0.17.1)
@@ -14,7 +66,7 @@
### ❤️ Contributors
- Leo Shklovskii <leo@thermopylae.net>
- Leo Shklovskii ([@leos](https://github.com/leos))
## v0.17.0
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env node
/**
* A alias around `publish-extension` that is always installed on the path without having to install
* `publish-browser-extension` as a direct dependency (like for PNPM, which doesn't link
* sub-dependency binaries to "node_modules/.bin")
*/
require('publish-browser-extension/cli');
@@ -18,6 +18,8 @@ const chromeExtensionIds = [
'lknmjhcajhfbbglglccadlfdjbaiifig', // tl;dv - Record, Transcribe & ChatGPT for Google Meet
'youtube中文配音/oglffgiaiekgeicdgkdlnlkhliajdlja', // Youtube中文配音
'agjnjboanicjcpenljmaaigopkgdnihi', // PreMiD
'aiakblgmlabokilgljkglggnpflljdgp', // Markdown Sticky Notes
'nomnkbngkijpffepcgbbofhcnafpkiep', // DocVersionRedirector
];
const { data, err, isLoading } = useListExtensionDetails(chromeExtensionIds);
+2 -2
View File
@@ -19,7 +19,7 @@ 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.";
"WXT provides the best developer experience, making it quick, easy, and fun to develop chrome extensions for all browsers. With built-in utilities 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';
@@ -124,7 +124,7 @@ export default defineConfig({
{ text: 'Options', link: '/entrypoints/options.md' },
{ text: 'Popup', link: '/entrypoints/popup.md' },
{ text: 'Sandbox', link: '/entrypoints/sandbox.md' },
{ text: 'Sidepanel', link: '/entrypoints/sidepanel.md' },
{ text: 'Side Panel', link: '/entrypoints/sidepanel.md' },
{ text: 'Unlisted Pages', link: '/entrypoints/unlisted-pages.md' },
{
text: 'Unlisted Scripts',
+16 -7
View File
@@ -52,9 +52,11 @@ When defining multiple content scripts, content script entrypoints that have the
## Context
Old content scripts are not automatically stopped when an extension updates and reloads. Often, this leads to "Invalidated context" errors in production when a content script from an old version of your extension tries to use a extension API.
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 managing this process: `ContentScriptContext`. An instance of this class is provided to you automatically inside the `main` function of your content script.
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({
@@ -65,26 +67,33 @@ export default defineContentScript({
// ...
});
// Stop fetch requests
fetch('...url', { signal: ctx.signal });
// Timeout utilities
// 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.
:::tip
:::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
+29 -28
View File
@@ -1,39 +1,40 @@
# Compare
Lets compare the features of WXT vs [Plasmo](https://docs.plasmo.com/framework), another web extension framework.
Lets compare the features of WXT vs [Plasmo](https://docs.plasmo.com/framework) (another web extension framework) and [CRXJS](https://crxjs.dev/vite-plugin) (the most popular bundler plugin).
## Overview
| Features | WXT | Plasmo |
| ---------------------------------------------------- | :-------------------------: | :--------------------------------------: |
| Supports all browsers | ✅ | ✅ |
| MV2 Support | ✅ | ✅ |
| MV3 Support | ✅ | ✅ |
| Create Extension ZIPs | ✅ | ✅ |
| Create Firefox Sources ZIP | ✅ | ❌ |
| First-class TypeScript support | ✅ | ✅ |
| File based entrypoint discovery | | |
| Inline entrypoint config | ✅ | ✅ |
| Auto-imports | ✅ | ❌ |
| Supports all frontend frameworks | ✅ | 🟡 Only React, Vue, and Svelte |
| Framework specific entrypoints (like `Popup.tsx`) | 🟡 `.html` `.ts` `.tsx` | ✅ `.html` `.ts` `.tsx` `.vue` `.svelte` |
| Automated publishing | 🟡 Coming soon | ✅ |
| Remote Code Bundling (Google Analytics) | ✅ | ✅ |
| Features | WXT | Plasmo | CRXJS |
| ---------------------------------------------------- | :-------------------------: | :--------------------------------------: | :---------------------------------------------------------------------: |
| Supports all browsers | ✅ | ✅ | ❌ See [#56](https://github.com/crxjs/chrome-extension-tools/issues/56) |
| MV2 Support | ✅ | ✅ | 🟡 Either MV2 or MV3 |
| MV3 Support | ✅ | ✅ | 🟡 Either MV2 or MV3 |
| Create Extension ZIPs | ✅ | ✅ | ❌ |
| Create Firefox Sources ZIP | ✅ | ❌ | ❌ |
| First-class TypeScript support | ✅ | ✅ | ✅ |
| Entrypoint discovery | File based | File based | |
| Inline entrypoint config | ✅ | ✅ | Manifest based |
| Auto-imports | ✅ | ❌ | ❌ |
| Supports all frontend frameworks | ✅ | 🟡 Only React, Vue, and Svelte | ✅ |
| Framework specific entrypoints (like `Popup.tsx`) | 🟡 `.html` `.ts` `.tsx` | ✅ `.html` `.ts` `.tsx` `.vue` `.svelte` | ❌ |
| Automated publishing | | ✅ | ❌ |
| Remote Code Bundling (Google Analytics) | ✅ | ✅ | ❌ |
| <strong style="opacity: 50%">Dev Mode</strong> | | |
| `.env` Files | ✅ | ✅ |
| Opens browser and install extension | ✅ | ❌ |
| HMR for UIs | ✅ | 🟡 React only |
| Reload HTML Files on Change | ✅ | 🟡 Reloads entire extension |
| Reload Content Scripts on Change | ✅ | 🟡 Reloads entire extension |
| Reload Background on Change | 🟡 Reloads entire extension | 🟡 Reloads entire extension |
| <strong style="opacity: 50%">Built-in Utils</strong> | | |
| Storage | ✅ | |
| Messaging | 🟡 Coming soon | ✅ |
| Content Script UI | | |
| `.env` Files | ✅ | ✅ | ✅ |
| Opens browser and install extension | ✅ | ❌ | ❌ |
| HMR for UIs | ✅ | 🟡 React only | ✅ |
| Reload HTML Files on Change | ✅ | 🟡 Reloads entire extension | ✅ |
| Reload Content Scripts on Change | ✅ | 🟡 Reloads entire extension | ✅ |
| Reload Background on Change | 🟡 Reloads entire extension | 🟡 Reloads entire extension | 🟡 Reloads entire extension |
| Respects Content Script `run_at` | ✅ | | ❌ ESM-style loaders run asynchronously |
| <strong style="opacity: 50%">Built-in Utils</strong> | | | |
| Storage | | ✅ | ❌ |
| Messaging | | | |
| Content Script UI | ✅ | ✅ | ❌ |
## Dev Mode
## Why use WXT?
WXT's main goal is improving the development experience (DX) of creating web extensions. There are two things WXT does differently:
WXT's main goal is improving the development experience (DX) of creating web extensions, while not sacrificing support. There are two things WXT does differently:
1. Automatically opens a browser with the extension installed when starting development
2. Reload each part of the extension individually rather than reloading the entire extension
+1 -24
View File
@@ -263,27 +263,4 @@ export const ignoredWebsites = storage.defineItem<IgnoredWebsiteV2[]>( // [!code
### Running Migrations
To run migrations, you have two options:
1. Import all versioned storage items into your background script and they will run automatically whenever your extension updates
2. Manually call `item.migrate()`
The first approach is recommended. To make importing all your storage items easy, you can define all of them in a single file, `utils/storage.ts`, and import that file into your background entrypoint:
```ts
// utils/storage.ts
export countStorage = storage.defineItem(...);
export themeStorage = storage.defineItem(...);
export someOtherStorage = storage.defineItem(...);
```
```ts
// entrypoints/background.ts
import '@/utils/storage'; // This import runs migrations on updates
export default defineBackground({
// ...
});
```
> When you call `storage.defineItem`, a `browser.runtime.onInstalled` listener is added. `onInstalled` listeners are only triggered in the background, which is why you must import them into the background.
As soon as `storage.defineItem` is called, WXT checks if migrations need to be ran, and if so, runs them. Calls to get or update the storage item's value or metadata (`getValue`, `setValue`, `removeValue`, `getMeta`, etc) will automatically wait for the migration process to finish before actually reading or writing values.
+1
View File
@@ -25,6 +25,7 @@ describe('Init command', () => {
"assets/vue.svg",
"components/HelloWorld.vue",
"entrypoints/background.ts",
"entrypoints/content.ts",
"entrypoints/popup/App.vue",
"entrypoints/popup/index.html",
"entrypoints/popup/main.ts",
+7 -5
View File
@@ -1,7 +1,7 @@
{
"name": "wxt",
"type": "module",
"version": "0.17.1",
"version": "0.17.4",
"description": "Next gen framework for developing web extensions",
"engines": {
"node": ">=18",
@@ -30,7 +30,10 @@
"bin",
"dist"
],
"bin": "./bin/wxt.mjs",
"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",
@@ -103,6 +106,7 @@
"sync-releases": "pnpx changelogen@latest gh release"
},
"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",
@@ -125,15 +129,13 @@
"jiti": "^1.21.0",
"json5": "^2.2.3",
"linkedom": "^0.16.1",
"manage-path": "^2.0.0",
"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.2",
"rollup-plugin-visualizer": "^5.9.2",
"publish-browser-extension": "^2.1.3",
"unimport": "^3.4.0",
"vite": "^5.1.3",
"web-ext-run": "^0.2.0",
+23 -30
View File
@@ -8,6 +8,9 @@ importers:
.:
dependencies:
'@aklinker1/rollup-plugin-visualizer':
specifier: 5.12.0
version: 5.12.0
'@types/webextension-polyfill':
specifier: ^0.10.5
version: 0.10.7
@@ -74,9 +77,6 @@ importers:
linkedom:
specifier: ^0.16.1
version: 0.16.1
manage-path:
specifier: ^2.0.0
version: 2.0.0
minimatch:
specifier: ^9.0.3
version: 9.0.3
@@ -96,11 +96,8 @@ importers:
specifier: ^2.4.2
version: 2.4.2
publish-browser-extension:
specifier: ^2.1.2
version: 2.1.2
rollup-plugin-visualizer:
specifier: ^5.9.2
version: 5.12.0
specifier: ^2.1.3
version: 2.1.3
unimport:
specifier: ^3.4.0
version: 3.4.0
@@ -226,6 +223,22 @@ importers:
packages:
/@aklinker1/rollup-plugin-visualizer@5.12.0:
resolution: {integrity: sha512-X24LvEGw6UFmy0lpGJDmXsMyBD58XmX1bbwsaMLhNoM+UMQfQ3b2RtC+nz4b/NoRK5r6QJSKJHBNVeUdwqybaQ==}
engines: {node: '>=14'}
hasBin: true
peerDependencies:
rollup: 2.x || 3.x || 4.x
peerDependenciesMeta:
rollup:
optional: true
dependencies:
open: 8.4.2
picomatch: 2.3.1
source-map: 0.7.4
yargs: 17.7.2
dev: false
/@algolia/autocomplete-core@1.9.3(algoliasearch@4.20.0):
resolution: {integrity: sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw==}
dependencies:
@@ -3544,10 +3557,6 @@ packages:
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
dev: false
/manage-path@2.0.0:
resolution: {integrity: sha512-NJhyB+PJYTpxhxZJ3lecIGgh4kwIY2RAh44XvAz9UlqthlQwtPBf62uBVR8XaD8CRuSjQ6TnZH2lNJkbLPZM2A==}
dev: false
/mark.js@8.11.1:
resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==}
dev: true
@@ -4175,8 +4184,8 @@ packages:
sade: 1.8.1
dev: true
/publish-browser-extension@2.1.2:
resolution: {integrity: sha512-g6+mtdR4Z+GYHPIrfaAwC7Kbt1oQlpJ8r0x1PAytScy33OFdK+HVUeDDYorBpPAiQlmYJRQga7rY9QVTyTw34g==}
/publish-browser-extension@2.1.3:
resolution: {integrity: sha512-qisnXUUwjvu5kMvObfG7UQ9rPU3t0XfbKdCdCrwAXaLUySdC25nlM4gxi+CBvL7LiwvK494GJyEK/weQBhIyTQ==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
dependencies:
@@ -4391,22 +4400,6 @@ packages:
glob: 7.1.6
dev: false
/rollup-plugin-visualizer@5.12.0:
resolution: {integrity: sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ==}
engines: {node: '>=14'}
hasBin: true
peerDependencies:
rollup: 2.x || 3.x || 4.x
peerDependenciesMeta:
rollup:
optional: true
dependencies:
open: 8.4.2
picomatch: 2.3.1
source-map: 0.7.4
yargs: 17.7.2
dev: false
/rollup@4.6.1:
resolution: {integrity: sha512-jZHaZotEHQaHLgKr8JnQiDT1rmatjgKlMekyksz+yk9jt/8z9quNjnKNRoaM0wd9DC2QKXjmWWuDYtM3jfF8pQ==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+13 -15
View File
@@ -3,11 +3,12 @@ import { describe, it, expect, beforeEach, vi, expectTypeOf } from 'vitest';
import { browser } from '~/browser';
import { WxtStorageItem, storage } from '~/storage';
async function triggerUpdate() {
await fakeBrowser.runtime.onInstalled.trigger({
reason: 'update',
temporary: false,
});
/**
* This works because fakeBrowser is synchronous, and is will finish any number of chained
* calls within a single tick of the event loop, ie: a timeout of 0.
*/
async function waitForMigrations() {
return new Promise((res) => setTimeout(res));
}
describe('Storage Utils', () => {
@@ -75,10 +76,7 @@ describe('Storage Utils', () => {
const actual = await storage.getItems(params);
expect(actual).toHaveLength(3);
expected.forEach((item) => {
expect(actual).toContainEqual(item);
});
expect(actual).toEqual(expected);
});
});
@@ -412,7 +410,7 @@ describe('Storage Utils', () => {
3: migrateToV3,
},
});
await triggerUpdate();
await waitForMigrations();
const actualValue = await item.getValue();
const actualMeta = await item.getMeta();
@@ -439,7 +437,7 @@ describe('Storage Utils', () => {
3: migrateToV3,
},
});
await triggerUpdate();
await waitForMigrations();
const actualValue = await item.getValue();
const actualMeta = await item.getMeta();
@@ -464,7 +462,7 @@ describe('Storage Utils', () => {
2: migrateToV2,
},
});
await triggerUpdate();
await waitForMigrations();
const actualValue = await item.getValue();
const actualMeta = await item.getMeta();
@@ -492,7 +490,7 @@ describe('Storage Utils', () => {
3: migrateToV3,
},
});
await triggerUpdate();
await waitForMigrations();
expect(migrateToV2).not.toBeCalled();
expect(migrateToV3).not.toBeCalled();
@@ -514,7 +512,7 @@ describe('Storage Utils', () => {
3: migrateToV3,
},
});
await triggerUpdate();
await waitForMigrations();
const actualValue = await item.getValue();
const actualMeta = await item.getMeta();
@@ -541,7 +539,7 @@ describe('Storage Utils', () => {
defaultValue: 0,
version: nextVersion,
});
await triggerUpdate();
await waitForMigrations();
await expect(item.migrate()).rejects.toThrow(
'Version downgrade detected (v2 -> v1) for "local:count"',
+10 -2
View File
@@ -1,6 +1,5 @@
import { CAC, Command } from 'cac';
import consola, { LogLevels } from 'consola';
import { exec } from '~/core/utils/exec';
import { printHeader } from '~/core/utils/log';
import { formatDuration } from '~/core/utils/time';
import { ValidationError } from '~/core/utils/validation';
@@ -63,10 +62,18 @@ export function getArrayFromFlags<T>(
}
const aliasCommandNames = new Set<string>();
/**
* @param base Command to add this one to
* @param name The command name to add
* @param alias The CLI tool being aliased
* @param bin The CLI tool binary name. Usually the same as the alias
* @param docsUrl URL to the docs for the aliased CLI tool
*/
export function createAliasedCommand(
base: CAC,
name: string,
alias: string,
bin: string,
docsUrl: string,
) {
const aliasedCommand = base
@@ -79,7 +86,8 @@ export function createAliasedCommand(
const args = process.argv.slice(
process.argv.indexOf(aliasedCommand.name) + 1,
);
await exec(alias, args, {
const { execa } = await import('execa');
await execa(bin, args, {
stdio: 'inherit',
});
} catch {
+1
View File
@@ -140,6 +140,7 @@ createAliasedCommand(
cli,
'submit',
'publish-extension',
'wxt-publish-extension',
'https://www.npmjs.com/publish-browser-extension',
);
@@ -10,6 +10,29 @@ import { createLocationWatcher } from './location-watcher';
*
* 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`.
*
* To create context for testing, you can use the class's constructor:
*
* ```ts
* import { ContentScriptContext } from 'wxt/client';
*
* test("storage listener should be removed when context is invalidated", () => {
* const ctx = new ContentScriptContext('test');
* const item = storage.defineItem("local:count", { defaultValue: 0 });
* const watcher = vi.fn();
*
* const unwatch = item.watch(watcher);
* ctx.onInvalidated(unwatch); // Listen for invalidate here
*
* await item.setValue(1);
* expect(watcher).toBeCalledTimes(1);
* expect(watcher).toBeCalledWith(1, 0);
*
* ctx.notifyInvalidated(); // Use this function to invalidate the context
* await item.setValue(2);
* expect(watcher).toBeCalledTimes(1);
* });
* ```
*/
export class ContentScriptContext implements AbortController {
private static SCRIPT_STARTED_MESSAGE_TYPE = 'wxt:content-script-started';
@@ -1,5 +1,5 @@
import type * as vite from 'vite';
import { visualizer } from 'rollup-plugin-visualizer';
import { visualizer } from '@aklinker1/rollup-plugin-visualizer';
import { ResolvedConfig } from '~/types';
import path from 'node:path';
@@ -16,8 +16,8 @@ export function devHtmlPrerender(
): vite.PluginOption {
const htmlReloadId = '@wxt/reload-html';
const resolvedHtmlReloadId = resolve(
config.root,
'node_modules/wxt/dist/virtual/reload-html.js',
config.wxtModuleDir,
'dist/virtual/reload-html.js',
);
const virtualReactRefreshId = '@wxt/virtual-react-refresh';
const resolvedVirtualReactRefreshId = '\0' + virtualReactRefreshId;
@@ -30,10 +30,7 @@ export function virtualEntrypoint(
const inputPath = id.replace(resolvedVirtualId, '');
const template = await fs.readFile(
resolve(
config.root,
`node_modules/wxt/dist/virtual/${type}-entrypoint.js`,
),
resolve(config.wxtModuleDir, `dist/virtual/${type}-entrypoint.js`),
'utf-8',
);
return template.replace(`virtual:user-${type}`, inputPath);
@@ -26,8 +26,8 @@ export function webextensionPolyfillMock(
alias: {
// Alias to use a mocked version of the polyfill
'webextension-polyfill': path.resolve(
config.root,
'node_modules/wxt/dist/virtual/mock-browser',
config.wxtModuleDir,
'dist/virtual/mock-browser',
),
},
},
+2 -2
View File
@@ -52,8 +52,8 @@ export async function importEntrypointFile<T>(path: string): Promise<T> {
esmResolve: true,
alias: {
'webextension-polyfill': resolve(
wxt.config.root,
'node_modules/wxt/dist/virtual/mock-browser.js',
wxt.config.wxtModuleDir,
'dist/virtual/mock-browser.js',
),
},
// Continue using node to load TS files even if `bun run --bun` is detected. Jiti does not
+6 -12
View File
@@ -16,8 +16,8 @@ import {
validateEntrypoints,
} from '../validation';
import consola from 'consola';
import { exec } from '../exec';
import { wxt } from '../../wxt';
import { mergeJsonOutputs } from '@aklinker1/rollup-plugin-visualizer';
/**
* Builds the extension based on an internal config. No more config discovery is performed, the
@@ -93,17 +93,11 @@ async function combineAnalysisStats(): Promise<void> {
});
const absolutePaths = unixFiles.map(unnormalizePath);
await exec(
'rollup-plugin-visualizer',
[
...absolutePaths,
'--template',
wxt.config.analysis.template,
'--filename',
wxt.config.analysis.outputFile,
],
{ cwd: wxt.config.root, stdio: 'inherit' },
);
await mergeJsonOutputs({
inputs: absolutePaths,
template: wxt.config.analysis.template,
filename: wxt.config.analysis.outputFile,
});
if (!wxt.config.analysis.keepArtifacts) {
await Promise.all(absolutePaths.map((statsFile) => fs.remove(statsFile)));
+14
View File
@@ -71,6 +71,7 @@ export async function resolveConfig(
inlineConfig.root ?? userConfig.root ?? process.cwd(),
);
const wxtDir = path.resolve(root, '.wxt');
const wxtModuleDir = await resolveWxtModuleDir();
const srcDir = path.resolve(root, mergedConfig.srcDir ?? root);
const entrypointsDir = path.resolve(
srcDir,
@@ -127,6 +128,7 @@ export async function resolveConfig(
outBaseDir,
outDir,
publicDir,
wxtModuleDir,
root,
runnerConfig,
srcDir,
@@ -322,3 +324,15 @@ async function getUnimportOptions(
defaultOptions,
);
}
/**
* Returns the path to `node_modules/wxt`.
*/
async function resolveWxtModuleDir() {
const requireResolve =
require?.resolve ??
(await import('node:module')).default.createRequire(import.meta.url)
.resolve;
// require.resolve returns the wxt/dist/index file, not the package's root directory, which we want to return
return path.resolve(requireResolve('wxt'), '../..');
}
-26
View File
@@ -1,26 +0,0 @@
import type { Options } from 'execa';
import managePath from 'manage-path';
import { resolve } from 'node:path';
import { wxt } from '../wxt';
const managedPath = managePath(process.env);
/**
* Wrapper around `execa` with a modified `PATH` variable containing CLI tools from WXT's dependencies.
*/
export const exec = async (
file: string,
args?: readonly string[],
options?: Options,
) => {
// Reset so the same path isn't added multiple times
managedPath.restore();
// Add subdependency path for PNPM shamefully-hoist=false
managedPath.push(
resolve(wxt.config.root, 'node_modules/wxt/node_modules/.bin'),
);
const { execa } = await import('execa');
return await execa(file, args, options);
};
+1
View File
@@ -262,6 +262,7 @@ export const fakeResolvedConfig = fakeObjectCreator<ResolvedConfig>(() => {
outDir: fakeDir(),
publicDir: fakeDir(),
root: fakeDir(),
wxtModuleDir: fakeDir(),
runnerConfig: {
config: {},
},
+44 -25
View File
@@ -258,14 +258,14 @@ function createStorage(): WxtStorage {
);
}
const migrate = async () => {
const [value, meta] = await Promise.all([
// TODO: Optimize with getItems
getItem(driver, driverKey, undefined),
getMeta(driver, driverKey),
const driverMetaKey = getMetaKey(driverKey);
const [{ value }, { value: meta }] = await driver.getItems([
driverKey,
driverMetaKey,
]);
if (value == null) return;
const currentVersion = meta.v ?? 1;
const currentVersion = meta?.v ?? 1;
if (currentVersion > targetVersion) {
throw Error(
`Version downgrade detected (v${currentVersion} -> v${targetVersion}) for "${key}"`,
@@ -285,24 +285,21 @@ function createStorage(): WxtStorage {
(await migrations?.[migrateToVersion]?.(migratedValue)) ??
migratedValue;
}
await Promise.all([
// TODO: Optimize with `setItem`
setItem(driver, driverKey, migratedValue),
setMeta(driver, driverKey, { v: targetVersion }),
await driver.setItems([
{ key: driverKey, value: migratedValue },
{ key: driverMetaKey, value: { ...meta, v: targetVersion } },
]);
logger.debug(
`Storage migration completed for ${key} v${targetVersion}`,
{ migratedValue },
);
};
browser.runtime.onInstalled.addListener(async ({ reason }) => {
if (reason !== 'update') return;
try {
await migrate();
} catch (err) {
logger.error(`Migration failed for ${key}`, err);
}
});
const migrationsDone =
opts?.migrations == null
? Promise.resolve()
: migrate().catch((err) => {
logger.error(`Migration failed for ${key}`, err);
});
const getDefaultValue = () => opts?.defaultValue ?? null;
@@ -310,12 +307,30 @@ function createStorage(): WxtStorage {
get defaultValue() {
return getDefaultValue();
},
getValue: () => getItem(driver, driverKey, opts),
getMeta: () => getMeta(driver, driverKey),
setValue: (value) => setItem(driver, driverKey, value),
setMeta: (properties) => setMeta(driver, driverKey, properties),
removeValue: (opts) => removeItem(driver, driverKey, opts),
removeMeta: (properties) => removeMeta(driver, driverKey, properties),
getValue: async () => {
await migrationsDone;
return await getItem(driver, driverKey, opts);
},
getMeta: async () => {
await migrationsDone;
return await getMeta(driver, driverKey);
},
setValue: async (value) => {
await migrationsDone;
return await setItem(driver, driverKey, value);
},
setMeta: async (properties) => {
await migrationsDone;
return await setMeta(driver, driverKey, properties);
},
removeValue: async (opts) => {
await migrationsDone;
return await removeItem(driver, driverKey, opts);
},
removeMeta: async (properties) => {
await migrationsDone;
return await removeMeta(driver, driverKey, properties);
},
watch: (cb) =>
watch(driver, driverKey, (newValue, oldValue) =>
cb(newValue ?? getDefaultValue(), oldValue ?? getDefaultValue()),
@@ -346,7 +361,10 @@ function createDriver(
);
}
return browser.storage[storageArea];
const area = browser.storage[storageArea];
if (area == null)
throw Error(`"browser.storage.${storageArea}" is undefined`);
return area;
};
const watchListeners = new Set<
(changes: Storage.StorageAreaOnChangedChangesType) => void
@@ -421,7 +439,8 @@ export interface WxtStorage {
*/
getItem<T>(key: string, opts?: GetItemOptions<T>): Promise<T | null>;
/**
* Get multiple items from storage. There is no guarantee of order in the returned array.
* Get multiple items from storage. The return order is guaranteed to be the same as the order
* requested.
*
* @example
* await storage.getItems(["local:installDate", "session:someCounter"]);
+5 -1
View File
@@ -3,7 +3,7 @@ import type { Manifest, Scripting } from '~/browser';
import { UnimportOptions } from 'unimport';
import { LogLevel } from 'consola';
import { ContentScriptContext } from '../client/content-scripts/content-script-context';
import type { PluginVisualizerOptions } from 'rollup-plugin-visualizer';
import type { PluginVisualizerOptions } from '@aklinker1/rollup-plugin-visualizer';
import type { FSWatcher } from 'chokidar';
import { ResolvedConfig as C12ResolvedConfig } from 'c12';
import { Hookable, NestedHooks } from 'hookable';
@@ -963,6 +963,10 @@ export interface ResolvedConfig {
outBaseDir: string;
outDir: string;
debug: boolean;
/**
* Directory pointing to `node_modules/wxt`, wherever WXT is installed.
*/
wxtModuleDir: string;
mode: string;
command: 'build' | 'serve';
browser: TargetBrowser;
-12
View File
@@ -50,18 +50,6 @@ declare module 'web-ext-run/util/logger' {
export const consoleStream: IConsoleStream;
}
declare module 'manage-path' {
export interface ManagedPath {
push(...paths: string[]);
push(paths: string[]);
shift(...paths: string[]);
shift(paths: string[]);
get(): string;
restore(): void;
}
export default function managePath(env: object): ManagedPath;
}
declare module 'wxt/browser' {
// Overridden when types are generated per project
export type PublicPath = string;
+6
View File
@@ -0,0 +1,6 @@
export default defineContentScript({
matches: ['*://*.google.com/*'],
main() {
console.log('Hello content.');
},
});
+6
View File
@@ -0,0 +1,6 @@
export default defineContentScript({
matches: ['*://*.google.com/*'],
main() {
console.log('Hello content.');
},
});
@@ -0,0 +1,6 @@
export default defineContentScript({
matches: ['*://*.google.com/*'],
main() {
console.log('Hello content.');
},
});
+6
View File
@@ -0,0 +1,6 @@
export default defineContentScript({
matches: ['*://*.google.com/*'],
main() {
console.log('Hello content.');
},
});
+6
View File
@@ -0,0 +1,6 @@
export default defineContentScript({
matches: ['*://*.google.com/*'],
main() {
console.log('Hello content.');
},
});