Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 64610bad41 | |||
| 45b7d7c67f | |||
| 0bb2746869 | |||
| f9b0aa45f8 | |||
| 6f6ce33bbd | |||
| e1c6020997 | |||
| 012bd7e67f | |||
| 35778f70cb | |||
| cb4f9aa752 | |||
| f80fb42483 | |||
| 02c79ca244 | |||
| 50e8c86ce9 | |||
| 6fbfc15be7 | |||
| c793d3cf6b | |||
| b5f4d8cc1b | |||
| 77e6d1f5a4 | |||
| 3f149ac4fa | |||
| c0dbffaa82 | |||
| 707f03a3eb | |||
| 8372462041 | |||
| 3fbbe2ccde | |||
| e7880fcde0 | |||
| ee5e76e4b1 | |||
| 1ae8d6bc46 | |||
| de7dd09f60 | |||
| 1649785826 | |||
| 91fc41cb15 | |||
| 9c80ad832b | |||
| 86b7162d99 | |||
| b8f8acd33d |
+154
-6
@@ -1,5 +1,153 @@
|
||||
# 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)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Content scripts not loading in dev mode ([3fbbe2c](https://github.com/wxt-dev/wxt/commit/3fbbe2c))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Lots of small typo fixes ([#480](https://github.com/wxt-dev/wxt/pull/480))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Leo Shklovskii ([@leos](https://github.com/leos))
|
||||
|
||||
## v0.17.0
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.11...v0.17.0)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- **storage:** ⚠️ Improved support for default values on storage items ([#477](https://github.com/wxt-dev/wxt/pull/477))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- **storage:** ⚠️ Only run migrations when the extension is updated ([#478](https://github.com/wxt-dev/wxt/pull/478))
|
||||
- Improve dev mode for content scripts registered at runtime ([#474](https://github.com/wxt-dev/wxt/pull/474))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- **storage:** Update docs ([91fc41c](https://github.com/wxt-dev/wxt/commit/91fc41c))
|
||||
|
||||
#### ⚠️ Breaking Changes
|
||||
|
||||
`v0.17.0` introduces several breaking changes to `wxt/storage`.
|
||||
|
||||
First, if you were using `defineItem` with versioning and no default value, you will need to add `defaultValue: null` to the options and update the first type parameter:
|
||||
|
||||
```ts
|
||||
// < 0.17
|
||||
const item = storage.defineItem<number>("local:count", {
|
||||
version: ...,
|
||||
migrations: ...,
|
||||
})
|
||||
|
||||
// >= 0.17
|
||||
const item = storage.defineItem<number | null>("local:count", {
|
||||
defaultValue: null,
|
||||
version: ...,
|
||||
migrations: ...,
|
||||
})
|
||||
```
|
||||
|
||||
The `defaultValue` property is now required if passing in the second options argument.
|
||||
|
||||
If you exclude the second options argument, it will default to being nullable, as before.
|
||||
|
||||
```ts
|
||||
const item: WxtStorageItem<number | null> = storage.defineItem<number>("local:count");
|
||||
const value: number | null = await item.getValue();
|
||||
```
|
||||
|
||||
> If you don't use typescript, there aren't any breaking changes, this is just a type change.
|
||||
|
||||
For storage items that are not nullable, the `watch` callback types has improved and will use the default value instead of `null` when the value is missing:
|
||||
|
||||
```ts
|
||||
// >=0.17
|
||||
const item = storage.defineItem<number>("local:count", { defaultValue: 0 });
|
||||
item.watch((newValue: number | null, oldValue: number | null) => {
|
||||
// ...
|
||||
});
|
||||
|
||||
// >=0.17
|
||||
const item = storage.defineItem<number>("local:count", { defaultValue: 0 });
|
||||
item.watch((newValue: number, oldValue: number) => {
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
You can also access the default value directly off the item:
|
||||
|
||||
```ts
|
||||
console.log(item.defaultValue); // 0
|
||||
```
|
||||
|
||||
The second breaking change is that migrations for versioned items only run when the extension is updated. Before, they were ran whenever the storage item was created, in any entrypoint (background, popup, content script, etc). Now, in v0.17, storage items will only run migrations when the `browser.runtime.onInstalled` event is fired with `reason = "update"` in the background. See the updated docs to make sure they run correctly: https://wxt.dev/guide/storage.html#running-migrations. TLDR: you need to import all storage items into the background entrypoint for the `onInstalled` hook to fire properly and thus run the migrations.
|
||||
|
||||
To keep the old behavior, call the new `migrate` function to run migrations as soon as an item is defined:
|
||||
|
||||
```ts
|
||||
const item = storage.defineItem(...);
|
||||
item.migrate();
|
||||
```
|
||||
|
||||
## v0.16.11
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.10...v0.16.11)
|
||||
@@ -15,7 +163,7 @@
|
||||
### 🤖 CI
|
||||
|
||||
- Fix codecov warning in release workflow ([7c6973f](https://github.com/wxt-dev/wxt/commit/7c6973f))
|
||||
- Upgrade `pnpm/action-setup` to v2 ([905bfc7](https://github.com/wxt-dev/wxt/commit/905bfc7))
|
||||
- Upgrade `pnpm/action-setup` to v3 ([905bfc7](https://github.com/wxt-dev/wxt/commit/905bfc7))
|
||||
|
||||
## v0.16.10
|
||||
|
||||
@@ -834,7 +982,7 @@ Renamed undocumented constants for detecting the build config at runtime in [#38
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Remove whitespace from genearted `.wxt` files ([#211](https://github.com/wxt-dev/wxt/pull/211))
|
||||
- Remove whitespace from generated `.wxt` files ([#211](https://github.com/wxt-dev/wxt/pull/211))
|
||||
- Upgrade templates to `wxt@^0.9.0` ([#214](https://github.com/wxt-dev/wxt/pull/214))
|
||||
- Update Vite dependency range to `^4.0.0 || ^5.0.0-0` ([f1e8084](https://github.com/wxt-dev/wxt/commit/f1e8084be89e512dde441b9197a99183c497f67d))
|
||||
|
||||
@@ -1342,8 +1490,8 @@ Renamed undocumented constants for detecting the build config at runtime in [#38
|
||||
- **deps-dev:** Bump vitest from 0.34.1 to 0.34.3 ([#99](https://github.com/wxt-dev/wxt/pull/99))
|
||||
- Increase E2E test timeout because GitHub Actions Window runner is slow ([2a0842b](https://github.com/wxt-dev/wxt/commit/2a0842b))
|
||||
- **deps-dev:** Bump vitepress from 1.0.0-rc.4 to 1.0.0-rc.10 ([#96](https://github.com/wxt-dev/wxt/pull/96))
|
||||
- Fix test watcher restarting indefinetly ([2c7922c](https://github.com/wxt-dev/wxt/commit/2c7922c))
|
||||
- Remove explict icon config from templates ([93bfee0](https://github.com/wxt-dev/wxt/commit/93bfee0))
|
||||
- Fix test watcher restarting indefinitely ([2c7922c](https://github.com/wxt-dev/wxt/commit/2c7922c))
|
||||
- Remove explicit icon config from templates ([93bfee0](https://github.com/wxt-dev/wxt/commit/93bfee0))
|
||||
- Use import aliases in Vue template ([#104](https://github.com/wxt-dev/wxt/pull/104))
|
||||
|
||||
#### ⚠️ Breaking Changes
|
||||
@@ -1403,7 +1551,7 @@ Renamed undocumented constants for detecting the build config at runtime in [#38
|
||||
- Branding and logo ([#60](https://github.com/wxt-dev/wxt/pull/60))
|
||||
- Simplify binary setup ([#62](https://github.com/wxt-dev/wxt/pull/62))
|
||||
- Add Solid template ([#63](https://github.com/wxt-dev/wxt/pull/63))
|
||||
- Increate E2E test timeout to fix flakey test ([dfe424f](https://github.com/wxt-dev/wxt/commit/dfe424f))
|
||||
- Increase E2E test timeout to fix flakey test ([dfe424f](https://github.com/wxt-dev/wxt/commit/dfe424f))
|
||||
|
||||
### 🤖 CI
|
||||
|
||||
@@ -1679,7 +1827,7 @@ Initial release of WXT. Full support for production builds and initial toolkit f
|
||||
- Export and bootstrap the `/client` package ([5b07c95](https://github.com/wxt-dev/wxt/commit/5b07c95))
|
||||
- Resolve entrypoints based on filesystem ([a63f061](https://github.com/wxt-dev/wxt/commit/a63f061))
|
||||
- Separate output directories for each browser/manifest version ([f09ffbb](https://github.com/wxt-dev/wxt/commit/f09ffbb))
|
||||
- Build entrypoints and output `manfiest.json` ([1e7c738](https://github.com/wxt-dev/wxt/commit/1e7c738))
|
||||
- Build entrypoints and output `manifest.json` ([1e7c738](https://github.com/wxt-dev/wxt/commit/1e7c738))
|
||||
- Automatically add CSS files to content scripts ([047ce04](https://github.com/wxt-dev/wxt/commit/047ce04))
|
||||
- Download and bundle remote URL imports ([523c7df](https://github.com/wxt-dev/wxt/commit/523c7df))
|
||||
- Generate type declarations and config for project types and auto-imports ([21debad](https://github.com/wxt-dev/wxt/commit/21debad))
|
||||
|
||||
+1
-1
@@ -108,7 +108,7 @@ Then run `npm i` again.
|
||||
To add a template, copy the vanilla template and give it a new name.
|
||||
|
||||
```sh
|
||||
cp -r templates/vailla templates/<new-template-name>
|
||||
cp -r templates/vanilla templates/<new-template-name>
|
||||
```
|
||||
|
||||
That's it. Once your template is merged, it will be available inside `wxt init` immediately. You don't need to release a new version of WXT to release a new template.
|
||||
|
||||
Executable
+7
@@ -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);
|
||||
|
||||
@@ -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 +16,7 @@ For MV2, the background is added as a script to the background page. For MV3, th
|
||||
## Definition
|
||||
|
||||
:::warning
|
||||
The main function of the background **_CANNOT BE ASYNC_**. Event listeners must be added syncronously on background startup. If your main function returns a promise, WXT will log an error.
|
||||
The main function of the background **_CANNOT BE ASYNC_**. Event listeners must be added synchronously on background startup. If your main function returns a promise, WXT will log an error.
|
||||
:::
|
||||
|
||||
```ts
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
WXT can build CSS entrypoints individually. CSS entrypoints are always unlisted.
|
||||
|
||||
See [Content Script CSS](/entrypoints/content-scripts#css) documentation for the recomended approach to include CSS with a content script.
|
||||
See [Content Script CSS](/entrypoints/content-scripts#css) documentation for the recommended approach to include CSS with a content script.
|
||||
|
||||
:::info
|
||||
If the recommended approach doesn't work for your use case, you can use any of the filename patterns below to build the styles separate from the JS and use the [`transformManifest` hook](/api/wxt/interfaces/InlineConfig#transformmanifest) to manually add your CSS file to the manifest.
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
In Chrome, side panels use the "side_panel" API, while Firefox uses the "sidebar_action" API.
|
||||
|
||||
:::warning
|
||||
Chrome added support for sidepanels in Manifest V3, they are not available in Manfiest V2.
|
||||
Chrome added support for sidepanels in Manifest V3, they are not available in Manifest V2.
|
||||
:::
|
||||
|
||||
## Filenames
|
||||
|
||||
@@ -65,7 +65,7 @@ import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
imports: {
|
||||
// Add auto-imports for vue fuctions like createApp, ref, computed, watch, toRaw, etc...
|
||||
// Add auto-imports for vue functions like createApp, ref, computed, watch, toRaw, etc...
|
||||
presets: ['vue'],
|
||||
},
|
||||
});
|
||||
|
||||
+30
-29
@@ -1,46 +1,47 @@
|
||||
# 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
|
||||
|
||||
Opening a browser automatically makes it super easy to start and stop development without having to manually load the extension in your browser.
|
||||
|
||||
Reloading each part of the extension individually improves your iteration speed while developing UIs. This is because reloading the entire extension on every change will close the popup and any tabs open to an extension page, like options. If you save a file associated with a UI and a content script while working on the UI, it will randomly close because it needed to reload the extension when the content script changed. This interupts your development flow and is really annoying.
|
||||
Reloading each part of the extension individually improves your iteration speed while developing UIs. This is because reloading the entire extension on every change will close the popup and any tabs open to an extension page, like options. If you save a file associated with a UI and a content script while working on the UI, it will randomly close because it needed to reload the extension when the content script changed. This interrupts your development flow and is really annoying.
|
||||
|
||||
WXT solves this problem by reloading HTML pages and content scripts individually (when possible) to keep your UIs open while you develop them. This is a MV3 feature, so if you're developing a MV2 extension, you'll get the same dev experience as Plasmo.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Configuration
|
||||
|
||||
WXT's behavior can be configured via the `wxt.config.ts` file. In this file, you can add Vite plugins, change the directory strucutre of your project, and set fields on your `manifest.json`.
|
||||
WXT's behavior can be configured via the `wxt.config.ts` file. In this file, you can add Vite plugins, change the directory structure of your project, and set fields on your `manifest.json`.
|
||||
|
||||
## Config File
|
||||
|
||||
|
||||
@@ -368,7 +368,7 @@ WXT provides a helper function, [`createIframeUi`](/api/wxt/client/functions/cre
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
1. Add the page to the manifest's `web_accessible_resouces`
|
||||
1. Add the page to the manifest's `web_accessible_resources`
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
|
||||
@@ -15,9 +15,9 @@ WXT's main goal is providing the best DX it possibly can. When running your exte
|
||||
|
||||
## Dev Mode vs Production Builds
|
||||
|
||||
There are some notible differences between the development and production versions of an extension. During development:
|
||||
There are some notable differences between the development and production versions of an extension. During development:
|
||||
|
||||
1. **Content scripts are not listed in the `manifest.json`** when targetting MV3. Instead, the [`scripting`](https://developer.chrome.com/docs/extensions/reference/api/scripting) permission is used to register content scripts at runtime so they can be reloaded individually.
|
||||
1. **Content scripts are not listed in the `manifest.json`** when targeting MV3. Instead, the [`scripting`](https://developer.chrome.com/docs/extensions/reference/api/scripting) permission is used to register content scripts at runtime so they can be reloaded individually.
|
||||
|
||||
To get the list of content scripts during development, run the following in the background's console:
|
||||
|
||||
@@ -59,7 +59,7 @@ export default defineRunnerConfig({
|
||||
|
||||
### Browser Binaries
|
||||
|
||||
`web-ext`'s browser discovery is very limitted. By default, it only guesses at where Chrome and Firefox are installed. If you've customized your install locations, you may need to tell `web-ext` where the binaries/executables are located using the [`binaries` option](/api/wxt/interfaces/ExtensionRunnerConfig#binaries). For other Chromium based browsers, like Edge or Opera, you'll need to explicitly list them in the `binaries` option as well, otherwise they will open in Chrome by default.
|
||||
`web-ext`'s browser discovery is very limited. By default, it only guesses at where Chrome and Firefox are installed. If you've customized your install locations, you may need to tell `web-ext` where the binaries/executables are located using the [`binaries` option](/api/wxt/interfaces/ExtensionRunnerConfig#binaries). For other Chromium based browsers, like Edge or Opera, you'll need to explicitly list them in the `binaries` option as well, otherwise they will open in Chrome by default.
|
||||
|
||||
```ts
|
||||
// ~/web-ext.config.ts
|
||||
|
||||
@@ -58,7 +58,7 @@ See [`/entrypoints` folder](/entrypoints/background) documentation for a full li
|
||||
|
||||
## Entrypoint Options
|
||||
|
||||
Some entrypoints, like content scripts, actions, or the background, can recieve additional options.
|
||||
Some entrypoints, like content scripts, actions, or the background, can receive additional options.
|
||||
|
||||
In HTML files, options are listed as `meta` tags:
|
||||
|
||||
@@ -118,7 +118,7 @@ This throws an error because WXT needs to import each entrypoint during the buil
|
||||
|
||||
:::details Why?
|
||||
|
||||
When importing your entrypoint to get its definition, the file is imported in a **_node environement_**, and doesn't have access to the `window`, `chrome`, or `browser` globals a web extension ususally has access to. If WXT doesn't remove all the imports from the file, the imported modules could try and access one of these variables, throwing an error.
|
||||
When importing your entrypoint to get its definition, the file is imported in a **_node environment_**, and doesn't have access to the `window`, `chrome`, or `browser` globals a web extension usually has access to. If WXT doesn't remove all the imports from the file, the imported modules could try and access one of these variables, throwing an error.
|
||||
|
||||
:::
|
||||
|
||||
@@ -126,7 +126,7 @@ When importing your entrypoint to get its definition, the file is imported in a
|
||||
See [`wxt-dev/wxt#336`](https://github.com/wxt-dev/wxt/issues/336) to track the status of this bug.
|
||||
:::
|
||||
|
||||
Usually, this error occurs when you try to extract options into a shared file or try to run code outside the `main` function. To fix the example from above, use litteral values when defining an entrypoint instead of importing them:
|
||||
Usually, this error occurs when you try to extract options into a shared file or try to run code outside the `main` function. To fix the example from above, use literal values when defining an entrypoint instead of importing them:
|
||||
|
||||
```ts
|
||||
import { GOOGLE_MATCHES } from '~/utils/match-patterns'; // [!code --]
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ export default defineBackground({
|
||||
```
|
||||
|
||||
:::warning
|
||||
Only MV3 support ESM background scripts/service workers. When targetting MV2, the `type` option is ignored and the background is always bundled into a single file as IIFE.
|
||||
Only MV3 support ESM background scripts/service workers. When targeting MV2, the `type` option is ignored and the background is always bundled into a single file as IIFE.
|
||||
:::
|
||||
|
||||
## Content Scripts
|
||||
|
||||
@@ -133,7 +133,7 @@ The dev command will build the extension for development, open the browser, and
|
||||
When running the dev command, WXT will make several changes to your `manifest.json` to improve your development experience:
|
||||
|
||||
- If missing, add a background script/service worker to enable fast reloads
|
||||
- Add serveral `permissions` and `host_permissions` to enable HMR and fast reloads
|
||||
- Add several `permissions` and `host_permissions` to enable HMR and fast reloads
|
||||
- Modify the CSP to allow connections with the dev server
|
||||
- Remove `content_scripts` and register them at runtime so they can be easily reloaded when you save a file
|
||||
|
||||
@@ -147,4 +147,4 @@ You're ready to build your web extension!
|
||||
- Learn how to [add entrypoints](./entrypoints) like the popup, options page, or content scripts
|
||||
- Configure your entrypoints to [use ESM](./esm) at runtime
|
||||
- [Configure WXT](./configuration) by creating a `wxt.config.ts` file
|
||||
- Checkout [example projects](https://github.com/wxt-dev/wxt-examples) to see how to perfom common tasks with WXT
|
||||
- Checkout [example projects](https://github.com/wxt-dev/wxt-examples) to see how to perform common tasks with WXT
|
||||
|
||||
@@ -20,7 +20,7 @@ WXT is an opinionated framework. This helps keep projects consistent and easy to
|
||||
- **Generated manifest**: Based on your project's file structure
|
||||
- **Entrypoint configuration**: Configure entrypoints from the same file they're declare in
|
||||
- **Type-safety is a priority**: Out-of-the-box TypeScript support with improved browser API typing
|
||||
- **Simple output file structure**: Ouptut file paths minimize the path at runtime
|
||||
- **Simple output file structure**: Output file paths minimize the path at runtime
|
||||
|
||||
## Development
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ Every project is different, so there's no one-solution-fits-all to migrating you
|
||||
|
||||
## Popular Tools/Frameworks
|
||||
|
||||
Here's specific steps for other popuplar frameworks/build tools.
|
||||
Here's specific steps for other popular frameworks/build tools.
|
||||
|
||||
### `vite-plugin-web-extension`
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Multiple Browsers
|
||||
|
||||
You can build an extension for any combination of browser and manifest verison. Different browsers and manifest versions support different APIs and entrypoints, so be sure to check that your extension functions as expected for each target.
|
||||
You can build an extension for any combination of browser and manifest version. Different browsers and manifest versions support different APIs and entrypoints, so be sure to check that your extension functions as expected for each target.
|
||||
|
||||
Separate build targets are written to their own output directories:
|
||||
|
||||
@@ -45,7 +45,7 @@ wxt --mv2
|
||||
wxt build --mv2
|
||||
```
|
||||
|
||||
When the `-b --browser` flag is not passed, it defaults to `chrome`. So here, we're targetting MV2 for Chrome.
|
||||
When the `-b --browser` flag is not passed, it defaults to `chrome`. So here, we're targeting MV2 for Chrome.
|
||||
|
||||
## Customizing Entrypoints
|
||||
|
||||
@@ -116,7 +116,7 @@ Only `defineBackground` and `defineContentScript` support per-browser options ri
|
||||
To determine the browser or manifest version at runtime, you can use any of the below variables:
|
||||
|
||||
- `import.meta.env.BROWSER`: A string, the target browser, usually equal to the `--browser` flag
|
||||
- `import.meta.env.MANIFEST_VERSION`: A number, either `2` or `3`, depending on the manifest version targetted
|
||||
- `import.meta.env.MANIFEST_VERSION`: A number, either `2` or `3`, depending on the manifest version targeted
|
||||
- `import.meta.env.CHROME`: A boolean equivalent to `import.meta.env.BROWSER === "chrome"`
|
||||
- `import.meta.env.FIREFOX`: A boolean equivalent to `import.meta.env.BROWSER === "firefox"`
|
||||
- `import.meta.env.EDGE`: A boolean equivalent to `import.meta.env.BROWSER === "edge"`
|
||||
|
||||
@@ -64,7 +64,7 @@ See the [Firefox Addon Store](#firefox-addon-store) section for more details abo
|
||||
|
||||
## GitHub Action
|
||||
|
||||
Here's an example of a GitHub Action to automate submiting new versions of your 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
|
||||
# TODO
|
||||
|
||||
+14
-2
@@ -1,3 +1,7 @@
|
||||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Storage API
|
||||
|
||||
WXT provides a simplified API to replace the `browser.storage.*` APIs. Use the `storage` auto-import from `wxt/storage` or import it manually to get started:
|
||||
@@ -35,6 +39,8 @@ await storage.watch<number>(
|
||||
await storage.getMeta<{ v: number }>('local:installDate');
|
||||
```
|
||||
|
||||
For a full list of methods available, see the [API reference](/api/wxt/storage/interfaces/WxtStorage).
|
||||
|
||||
## Watchers
|
||||
|
||||
To listen for storage changes, use the `storage.watch` function. It lets you setup a listener for a single key:
|
||||
@@ -58,7 +64,7 @@ unwatch();
|
||||
|
||||
`wxt/storage` also supports setting metadata for keys, stored at `key + "$"`. Metadata is a collection of properties associated with a key. It might be a version number, last modified date, etc.
|
||||
|
||||
[Other than versioning](#versioning-and-migrations), you are responsible for managing a field's metadata:
|
||||
[Other than versioning](#versioning), you are responsible for managing a field's metadata:
|
||||
|
||||
```ts
|
||||
await Promise.all([
|
||||
@@ -116,7 +122,9 @@ const unwatch = showChangelogOnUpdate.watch(() => {
|
||||
});
|
||||
```
|
||||
|
||||
### Versioning and Migrations
|
||||
For a full list of properties and methods available, see the [API reference](/api/wxt/storage/interfaces/WxtStorageItem).
|
||||
|
||||
### Versioning
|
||||
|
||||
You can add versioning to storage items if you expect them to grow or change over time. When defining the first version of an item, start with version 1.
|
||||
|
||||
@@ -252,3 +260,7 @@ export const ignoredWebsites = storage.defineItem<IgnoredWebsiteV2[]>( // [!code
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### Running Migrations
|
||||
|
||||
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
-1
@@ -62,7 +62,7 @@ features:
|
||||
linkText: See templates
|
||||
- icon: 📏
|
||||
title: Bundle Analysis
|
||||
details: Tools for analyizing the final extension bundle and minimizing your extension's size.
|
||||
details: Tools for analyzing the final extension bundle and minimizing your extension's size.
|
||||
- icon: ⬇️
|
||||
title: Bundle Remote Code
|
||||
details: Downloads and bundles remote code imported from URLs.
|
||||
|
||||
@@ -7,7 +7,7 @@ describe('Analysis', () => {
|
||||
resetBundleIncrement();
|
||||
});
|
||||
|
||||
it('should outptut a stats.html with no part files by default', async () => {
|
||||
it('should output a stats.html with no part files by default', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html');
|
||||
project.addFile('entrypoints/options.html');
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -270,43 +270,43 @@ describe('Output Directory Structure', () => {
|
||||
|
||||
expect(await project.serializeFile('.output/chrome-mv3/background.js'))
|
||||
.toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/background.js
|
||||
----------------------------------------
|
||||
import { l as logHello } from "./chunks/log-bezs0tt4.js";
|
||||
function defineBackground(arg) {
|
||||
if (typeof arg === "function")
|
||||
return { main: arg };
|
||||
return arg;
|
||||
}
|
||||
const definition = defineBackground({
|
||||
type: "module",
|
||||
main() {
|
||||
logHello("background");
|
||||
".output/chrome-mv3/background.js
|
||||
----------------------------------------
|
||||
import { l as logHello } from "./chunks/log-bezs0tt4.js";
|
||||
function defineBackground(arg) {
|
||||
if (typeof arg === "function")
|
||||
return { main: arg };
|
||||
return arg;
|
||||
}
|
||||
});
|
||||
chrome;
|
||||
function print(method, ...args) {
|
||||
return;
|
||||
}
|
||||
var logger = {
|
||||
debug: (...args) => print(console.debug, ...args),
|
||||
log: (...args) => print(console.log, ...args),
|
||||
warn: (...args) => print(console.warn, ...args),
|
||||
error: (...args) => print(console.error, ...args)
|
||||
};
|
||||
try {
|
||||
const res = definition.main();
|
||||
if (res instanceof Promise) {
|
||||
console.warn(
|
||||
"The background's main() function return a promise, but it must be synchonous"
|
||||
);
|
||||
const definition = defineBackground({
|
||||
type: "module",
|
||||
main() {
|
||||
logHello("background");
|
||||
}
|
||||
});
|
||||
chrome;
|
||||
function print(method, ...args) {
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("The background crashed on startup!");
|
||||
throw err;
|
||||
}
|
||||
"
|
||||
`);
|
||||
var logger = {
|
||||
debug: (...args) => print(console.debug, ...args),
|
||||
log: (...args) => print(console.log, ...args),
|
||||
warn: (...args) => print(console.warn, ...args),
|
||||
error: (...args) => print(console.error, ...args)
|
||||
};
|
||||
try {
|
||||
const res = definition.main();
|
||||
if (res instanceof Promise) {
|
||||
console.warn(
|
||||
"The background's main() function return a promise, but it must be synchronous"
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("The background crashed on startup!");
|
||||
throw err;
|
||||
}
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should generate IIFE background script when type=undefined', async () => {
|
||||
@@ -381,7 +381,7 @@ describe('Output Directory Structure', () => {
|
||||
const res = definition.main();
|
||||
if (res instanceof Promise) {
|
||||
console.warn(
|
||||
"The background's main() function return a promise, but it must be synchonous"
|
||||
"The background's main() function return a promise, but it must be synchronous"
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
+1
-1
@@ -163,7 +163,7 @@ export class TestProject {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path An abosolute path to a file or a path relative to the root.
|
||||
* @param path An absolute path to a file or a path relative to the root.
|
||||
* @param ignoreContents An optional boolean that, when true, causes this function to not print
|
||||
* the file contents.
|
||||
*/
|
||||
|
||||
+7
-5
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wxt",
|
||||
"type": "module",
|
||||
"version": "0.16.11",
|
||||
"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",
|
||||
|
||||
Generated
+23
-30
@@ -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'}
|
||||
|
||||
+471
-417
@@ -1,7 +1,15 @@
|
||||
import { fakeBrowser } from '@webext-core/fake-browser';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, vi, expectTypeOf } from 'vitest';
|
||||
import { browser } from '~/browser';
|
||||
import { storage } from '~/storage';
|
||||
import { WxtStorageItem, storage } from '~/storage';
|
||||
|
||||
/**
|
||||
* 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', () => {
|
||||
beforeEach(() => {
|
||||
@@ -68,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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -384,417 +389,466 @@ describe('Storage Utils', () => {
|
||||
expect(cb).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineItem', () => {
|
||||
describe('versioning', () => {
|
||||
/**
|
||||
* This works because fakeBrowser is syncrounous, 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));
|
||||
}
|
||||
|
||||
it('should migrate values to the latest when a version upgrade is detected', async () => {
|
||||
await fakeBrowser.storage[storageArea].set({
|
||||
count: 2,
|
||||
count$: { v: 1 },
|
||||
});
|
||||
const migrateToV2 = vi.fn((oldCount) => oldCount * 2);
|
||||
const migrateToV3 = vi.fn((oldCount) => oldCount * 3);
|
||||
|
||||
const item = storage.defineItem<number, { v: number }>(
|
||||
`${storageArea}:count`,
|
||||
{
|
||||
version: 3,
|
||||
migrations: {
|
||||
2: migrateToV2,
|
||||
3: migrateToV3,
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitForMigrations();
|
||||
|
||||
const actualValue = await item.getValue();
|
||||
const actualMeta = await item.getMeta();
|
||||
|
||||
expect(actualValue).toEqual(12);
|
||||
expect(actualMeta).toEqual({ v: 3 });
|
||||
|
||||
expect(migrateToV2).toBeCalledTimes(1);
|
||||
expect(migrateToV2).toBeCalledWith(2);
|
||||
|
||||
expect(migrateToV3).toBeCalledTimes(1);
|
||||
expect(migrateToV3).toBeCalledWith(4);
|
||||
});
|
||||
|
||||
it("should not run migrations if the value doesn't exist yet", async () => {
|
||||
const migrateToV2 = vi.fn((oldCount) => oldCount * 2);
|
||||
const migrateToV3 = vi.fn((oldCount) => oldCount * 3);
|
||||
|
||||
const item = storage.defineItem<number, { v: number }>(
|
||||
`${storageArea}:count`,
|
||||
{
|
||||
version: 3,
|
||||
migrations: {
|
||||
2: migrateToV2,
|
||||
3: migrateToV3,
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitForMigrations();
|
||||
|
||||
const actualValue = await item.getValue();
|
||||
const actualMeta = await item.getMeta();
|
||||
|
||||
expect(actualValue).toBeNull();
|
||||
expect(actualMeta).toEqual({});
|
||||
|
||||
expect(migrateToV2).not.toBeCalled();
|
||||
expect(migrateToV3).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should run the v2 migration when converting an unversioned item to a versioned one', async () => {
|
||||
await fakeBrowser.storage[storageArea].set({
|
||||
count: 2,
|
||||
});
|
||||
const migrateToV2 = vi.fn((oldCount) => oldCount * 2);
|
||||
|
||||
const item = storage.defineItem<number, { v: number }>(
|
||||
`${storageArea}:count`,
|
||||
{
|
||||
version: 2,
|
||||
migrations: {
|
||||
2: migrateToV2,
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitForMigrations();
|
||||
|
||||
const actualValue = await item.getValue();
|
||||
const actualMeta = await item.getMeta();
|
||||
|
||||
expect(actualValue).toEqual(4);
|
||||
expect(actualMeta).toEqual({ v: 2 });
|
||||
|
||||
expect(migrateToV2).toBeCalledTimes(1);
|
||||
expect(migrateToV2).toBeCalledWith(2);
|
||||
});
|
||||
|
||||
it('Should not run old migrations if the version is unchanged', async () => {
|
||||
await fakeBrowser.storage[storageArea].set({
|
||||
count: 2,
|
||||
count$: { v: 3 },
|
||||
});
|
||||
const migrateToV2 = vi.fn((oldCount) => oldCount * 2);
|
||||
const migrateToV3 = vi.fn((oldCount) => oldCount * 3);
|
||||
|
||||
storage.defineItem<number, { v: number }>(`${storageArea}:count`, {
|
||||
version: 3,
|
||||
migrations: {
|
||||
2: migrateToV2,
|
||||
3: migrateToV3,
|
||||
},
|
||||
});
|
||||
await waitForMigrations();
|
||||
|
||||
expect(migrateToV2).not.toBeCalled();
|
||||
expect(migrateToV3).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should skip missing migration functions', async () => {
|
||||
await fakeBrowser.storage[storageArea].set({
|
||||
count: 2,
|
||||
count$: { v: 0 },
|
||||
});
|
||||
const migrateToV1 = vi.fn((oldCount) => oldCount * 1);
|
||||
const migrateToV3 = vi.fn((oldCount) => oldCount * 3);
|
||||
|
||||
const item = storage.defineItem<number, { v: number }>(
|
||||
`${storageArea}:count`,
|
||||
{
|
||||
version: 3,
|
||||
migrations: {
|
||||
1: migrateToV1,
|
||||
3: migrateToV3,
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitForMigrations();
|
||||
|
||||
const actualValue = await item.getValue();
|
||||
const actualMeta = await item.getMeta();
|
||||
|
||||
expect(actualValue).toEqual(6);
|
||||
expect(actualMeta).toEqual({ v: 3 });
|
||||
|
||||
expect(migrateToV1).toBeCalledTimes(1);
|
||||
expect(migrateToV1).toBeCalledWith(2);
|
||||
|
||||
expect(migrateToV3).toBeCalledTimes(1);
|
||||
expect(migrateToV3).toBeCalledWith(2);
|
||||
});
|
||||
|
||||
it('should throw an error if the new version is less than the previous version', async () => {
|
||||
const prevVersion = 2;
|
||||
const nextVersion = 1;
|
||||
await fakeBrowser.storage[storageArea].set({
|
||||
count: 0,
|
||||
count$: { v: prevVersion },
|
||||
});
|
||||
|
||||
const item = storage.defineItem(`${storageArea}:count`, {
|
||||
version: nextVersion,
|
||||
});
|
||||
|
||||
// @ts-expect-error: _migrationsCompleted is returned, but untyped
|
||||
await expect(item._migrationsCompleted).rejects.toThrow(
|
||||
'version downgrade detected',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getValue', () => {
|
||||
it('should return the value from storage', async () => {
|
||||
const expected = 2;
|
||||
const item = storage.defineItem<number>(`${storageArea}:count`);
|
||||
await fakeBrowser.storage[storageArea].set({ count: expected });
|
||||
|
||||
const actual = await item.getValue();
|
||||
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it('should return null if missing', async () => {
|
||||
const item = storage.defineItem<number>(`${storageArea}:count`);
|
||||
|
||||
const actual = await item.getValue();
|
||||
|
||||
expect(actual).toBeNull();
|
||||
});
|
||||
|
||||
it('should return the provided default value if missing', async () => {
|
||||
const expected = 0;
|
||||
const item = storage.defineItem(`${storageArea}:count`, {
|
||||
defaultValue: expected,
|
||||
});
|
||||
|
||||
const actual = await item.getValue();
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMeta', () => {
|
||||
it('should return the value from storage at key+$', async () => {
|
||||
const expected = { v: 2 };
|
||||
const item = storage.defineItem<number, { v: number }>(
|
||||
`${storageArea}:count`,
|
||||
);
|
||||
await fakeBrowser.storage[storageArea].set({ count$: expected });
|
||||
|
||||
const actual = await item.getMeta();
|
||||
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it('should return an empty object if missing', async () => {
|
||||
const expected = {};
|
||||
const item = storage.defineItem<number, { v: number }>(
|
||||
`${storageArea}:count`,
|
||||
);
|
||||
|
||||
const actual = await item.getMeta();
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setValue', () => {
|
||||
it('should set the value in storage', async () => {
|
||||
const expected = 1;
|
||||
const item = storage.defineItem<number>(`${storageArea}:count`);
|
||||
|
||||
await item.setValue(expected);
|
||||
const actual = await item.getValue();
|
||||
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it.each([undefined, null])(
|
||||
'should remove the value in storage when %s is passed in',
|
||||
async (value) => {
|
||||
const item = storage.defineItem<number>(`${storageArea}:count`);
|
||||
|
||||
// @ts-expect-error: undefined is not assignable to null, but we're testing that case on purpose
|
||||
await item.setValue(value);
|
||||
const actual = await item.getValue();
|
||||
|
||||
expect(actual).toBeNull();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('setMeta', () => {
|
||||
it('should set metadata at key+$', async () => {
|
||||
const expected = { date: Date.now() };
|
||||
const item = storage.defineItem<number, { date: number }>(
|
||||
`${storageArea}:count`,
|
||||
);
|
||||
|
||||
await item.setMeta(expected);
|
||||
const actual = await item.getMeta();
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should add to metadata if already present', async () => {
|
||||
const existing = { v: 2 };
|
||||
const newFields = { date: Date.now() };
|
||||
const expected = { ...existing, ...newFields };
|
||||
const item = storage.defineItem<
|
||||
number,
|
||||
{ date: number; v: number }
|
||||
>(`${storageArea}:count`);
|
||||
await fakeBrowser.storage[storageArea].set({
|
||||
count$: existing,
|
||||
});
|
||||
|
||||
await item.setMeta(newFields);
|
||||
const actual = await item.getMeta();
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeValue', () => {
|
||||
it('should remove the key from storage', async () => {
|
||||
const item = storage.defineItem(`${storageArea}:count`);
|
||||
await fakeBrowser.storage[storageArea].set({ count: 456 });
|
||||
|
||||
await item.removeValue();
|
||||
const actual = await item.getValue();
|
||||
|
||||
expect(actual).toBeNull();
|
||||
});
|
||||
|
||||
it('should not remove the metadata by default', async () => {
|
||||
const item = storage.defineItem(`${storageArea}:count`);
|
||||
const expected = { v: 1 };
|
||||
await fakeBrowser.storage[storageArea].set({
|
||||
count$: expected,
|
||||
count: 3,
|
||||
});
|
||||
|
||||
await item.removeValue();
|
||||
const actual = await item.getMeta();
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should remove the metadata when requested', async () => {
|
||||
const item = storage.defineItem(`${storageArea}:count`);
|
||||
await fakeBrowser.storage[storageArea].set({
|
||||
count$: { v: 1 },
|
||||
count: 3,
|
||||
});
|
||||
|
||||
await item.removeValue({ removeMeta: true });
|
||||
const actual = await item.getMeta();
|
||||
|
||||
expect(actual).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeMeta', () => {
|
||||
it('should remove all metadata', async () => {
|
||||
const item = storage.defineItem<number, { v: number }>(
|
||||
`${storageArea}:count`,
|
||||
);
|
||||
await fakeBrowser.storage[storageArea].set({ count$: { v: 4 } });
|
||||
|
||||
await item.removeMeta();
|
||||
const actual = await item.getMeta();
|
||||
|
||||
expect(actual).toEqual({});
|
||||
});
|
||||
|
||||
it('should only remove specific properties', async () => {
|
||||
const item = storage.defineItem<number, { v: number; d: number }>(
|
||||
`${storageArea}:count`,
|
||||
);
|
||||
await fakeBrowser.storage[storageArea].set({
|
||||
count$: { v: 4, d: Date.now() },
|
||||
});
|
||||
|
||||
await item.removeMeta(['d']);
|
||||
const actual = await item.getMeta();
|
||||
|
||||
expect(actual).toEqual({ v: 4 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('watch', () => {
|
||||
it("should not trigger if the changed key is different from the item's key", async () => {
|
||||
const item = storage.defineItem(`${storageArea}:key`);
|
||||
const cb = vi.fn();
|
||||
|
||||
item.watch(cb);
|
||||
await storage.setItem(`${storageArea}:not-the-key`, '123');
|
||||
|
||||
expect(cb).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("should not trigger if the value doesn't change", async () => {
|
||||
const item = storage.defineItem(`${storageArea}:key`);
|
||||
const cb = vi.fn();
|
||||
const value = '123';
|
||||
|
||||
await item.setValue(value);
|
||||
item.watch(cb);
|
||||
await item.setValue(value);
|
||||
|
||||
expect(cb).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should call the callback when the value changes', async () => {
|
||||
const item = storage.defineItem(`${storageArea}:key`);
|
||||
const cb = vi.fn();
|
||||
const newValue = '123';
|
||||
const oldValue = null;
|
||||
|
||||
item.watch(cb);
|
||||
await item.setValue(newValue);
|
||||
|
||||
expect(cb).toBeCalledTimes(1);
|
||||
expect(cb).toBeCalledWith(newValue, oldValue);
|
||||
});
|
||||
|
||||
it('should remove the listener when calling the returned function', async () => {
|
||||
const item = storage.defineItem(`${storageArea}:key`);
|
||||
const cb = vi.fn();
|
||||
|
||||
const unwatch = item.watch(cb);
|
||||
unwatch();
|
||||
await item.setValue('123');
|
||||
|
||||
expect(cb).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('unwatch', () => {
|
||||
it('should remove all watch listeners', async () => {
|
||||
const item = storage.defineItem(`${storageArea}:key`);
|
||||
const cb = vi.fn();
|
||||
|
||||
item.watch(cb);
|
||||
storage.unwatch();
|
||||
await item.setValue('123');
|
||||
|
||||
expect(cb).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
describe('defineItem', () => {
|
||||
describe('versioning', () => {
|
||||
it('should migrate values to the latest when a version upgrade is detected', async () => {
|
||||
await fakeBrowser.storage.local.set({
|
||||
count: 2,
|
||||
count$: { v: 1 },
|
||||
});
|
||||
const migrateToV2 = vi.fn((oldCount) => oldCount * 2);
|
||||
const migrateToV3 = vi.fn((oldCount) => oldCount * 3);
|
||||
|
||||
const item = storage.defineItem<number, { v: number }>(`local:count`, {
|
||||
defaultValue: 0,
|
||||
version: 3,
|
||||
migrations: {
|
||||
2: migrateToV2,
|
||||
3: migrateToV3,
|
||||
},
|
||||
});
|
||||
await waitForMigrations();
|
||||
|
||||
const actualValue = await item.getValue();
|
||||
const actualMeta = await item.getMeta();
|
||||
|
||||
expect(actualValue).toEqual(12);
|
||||
expect(actualMeta).toEqual({ v: 3 });
|
||||
|
||||
expect(migrateToV2).toBeCalledTimes(1);
|
||||
expect(migrateToV2).toBeCalledWith(2);
|
||||
|
||||
expect(migrateToV3).toBeCalledTimes(1);
|
||||
expect(migrateToV3).toBeCalledWith(4);
|
||||
});
|
||||
|
||||
it("should not run migrations if the value doesn't exist yet", async () => {
|
||||
const migrateToV2 = vi.fn((oldCount) => oldCount * 2);
|
||||
const migrateToV3 = vi.fn((oldCount) => oldCount * 3);
|
||||
|
||||
const item = storage.defineItem<number, { v: number }>(`local:count`, {
|
||||
defaultValue: 0,
|
||||
version: 3,
|
||||
migrations: {
|
||||
2: migrateToV2,
|
||||
3: migrateToV3,
|
||||
},
|
||||
});
|
||||
await waitForMigrations();
|
||||
|
||||
const actualValue = await item.getValue();
|
||||
const actualMeta = await item.getMeta();
|
||||
|
||||
expect(actualValue).toEqual(0);
|
||||
expect(actualMeta).toEqual({});
|
||||
|
||||
expect(migrateToV2).not.toBeCalled();
|
||||
expect(migrateToV3).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should run the v2 migration when converting an unversioned item to a versioned one', async () => {
|
||||
await fakeBrowser.storage.local.set({
|
||||
count: 2,
|
||||
});
|
||||
const migrateToV2 = vi.fn((oldCount) => oldCount * 2);
|
||||
|
||||
const item = storage.defineItem<number, { v: number }>(`local:count`, {
|
||||
defaultValue: 0,
|
||||
version: 2,
|
||||
migrations: {
|
||||
2: migrateToV2,
|
||||
},
|
||||
});
|
||||
await waitForMigrations();
|
||||
|
||||
const actualValue = await item.getValue();
|
||||
const actualMeta = await item.getMeta();
|
||||
|
||||
expect(actualValue).toEqual(4);
|
||||
expect(actualMeta).toEqual({ v: 2 });
|
||||
|
||||
expect(migrateToV2).toBeCalledTimes(1);
|
||||
expect(migrateToV2).toBeCalledWith(2);
|
||||
});
|
||||
|
||||
it('should not run old migrations if the version is unchanged', async () => {
|
||||
await fakeBrowser.storage.local.set({
|
||||
count: 2,
|
||||
count$: { v: 3 },
|
||||
});
|
||||
const migrateToV2 = vi.fn((oldCount) => oldCount * 2);
|
||||
const migrateToV3 = vi.fn((oldCount) => oldCount * 3);
|
||||
|
||||
storage.defineItem<number, { v: number }>(`local:count`, {
|
||||
defaultValue: 0,
|
||||
version: 3,
|
||||
migrations: {
|
||||
2: migrateToV2,
|
||||
3: migrateToV3,
|
||||
},
|
||||
});
|
||||
await waitForMigrations();
|
||||
|
||||
expect(migrateToV2).not.toBeCalled();
|
||||
expect(migrateToV3).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should skip missing migration functions', async () => {
|
||||
await fakeBrowser.storage.local.set({
|
||||
count: 2,
|
||||
count$: { v: 0 },
|
||||
});
|
||||
const migrateToV1 = vi.fn((oldCount) => oldCount * 1);
|
||||
const migrateToV3 = vi.fn((oldCount) => oldCount * 3);
|
||||
|
||||
const item = storage.defineItem<number, { v: number }>(`local:count`, {
|
||||
defaultValue: 0,
|
||||
version: 3,
|
||||
migrations: {
|
||||
1: migrateToV1,
|
||||
3: migrateToV3,
|
||||
},
|
||||
});
|
||||
await waitForMigrations();
|
||||
|
||||
const actualValue = await item.getValue();
|
||||
const actualMeta = await item.getMeta();
|
||||
|
||||
expect(actualValue).toEqual(6);
|
||||
expect(actualMeta).toEqual({ v: 3 });
|
||||
|
||||
expect(migrateToV1).toBeCalledTimes(1);
|
||||
expect(migrateToV1).toBeCalledWith(2);
|
||||
|
||||
expect(migrateToV3).toBeCalledTimes(1);
|
||||
expect(migrateToV3).toBeCalledWith(2);
|
||||
});
|
||||
|
||||
it('should throw an error if the new version is less than the previous version', async () => {
|
||||
const prevVersion = 2;
|
||||
const nextVersion = 1;
|
||||
await fakeBrowser.storage.local.set({
|
||||
count: 0,
|
||||
count$: { v: prevVersion },
|
||||
});
|
||||
|
||||
const item = storage.defineItem(`local:count`, {
|
||||
defaultValue: 0,
|
||||
version: nextVersion,
|
||||
});
|
||||
await waitForMigrations();
|
||||
|
||||
await expect(item.migrate()).rejects.toThrow(
|
||||
'Version downgrade detected (v2 -> v1) for "local:count"',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getValue', () => {
|
||||
it('should return the value from storage', async () => {
|
||||
const expected = 2;
|
||||
const item = storage.defineItem<number>(`local:count`);
|
||||
await fakeBrowser.storage.local.set({ count: expected });
|
||||
|
||||
const actual = await item.getValue();
|
||||
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it('should return null if missing', async () => {
|
||||
const item = storage.defineItem<number>(`local:count`);
|
||||
|
||||
const actual = await item.getValue();
|
||||
|
||||
expect(actual).toBeNull();
|
||||
});
|
||||
|
||||
it('should return the provided default value if missing', async () => {
|
||||
const expected = 0;
|
||||
const item = storage.defineItem(`local:count`, {
|
||||
defaultValue: expected,
|
||||
});
|
||||
|
||||
const actual = await item.getValue();
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMeta', () => {
|
||||
it('should return the value from storage at key+$', async () => {
|
||||
const expected = { v: 2 };
|
||||
const item = storage.defineItem<number, { v: number }>(`local:count`);
|
||||
await fakeBrowser.storage.local.set({ count$: expected });
|
||||
|
||||
const actual = await item.getMeta();
|
||||
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it('should return an empty object if missing', async () => {
|
||||
const expected = {};
|
||||
const item = storage.defineItem<number, { v: number }>(`local:count`);
|
||||
|
||||
const actual = await item.getMeta();
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setValue', () => {
|
||||
it('should set the value in storage', async () => {
|
||||
const expected = 1;
|
||||
const item = storage.defineItem<number>(`local:count`);
|
||||
|
||||
await item.setValue(expected);
|
||||
const actual = await item.getValue();
|
||||
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it.each([undefined, null])(
|
||||
'should remove the value in storage when %s is passed in',
|
||||
async (value) => {
|
||||
const item = storage.defineItem<number>(`local:count`);
|
||||
|
||||
// @ts-expect-error: undefined is not assignable to null, but we're testing that case on purpose
|
||||
await item.setValue(value);
|
||||
const actual = await item.getValue();
|
||||
|
||||
expect(actual).toBeNull();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('setMeta', () => {
|
||||
it('should set metadata at key+$', async () => {
|
||||
const expected = { date: Date.now() };
|
||||
const item = storage.defineItem<number, { date: number }>(
|
||||
`local:count`,
|
||||
);
|
||||
|
||||
await item.setMeta(expected);
|
||||
const actual = await item.getMeta();
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should add to metadata if already present', async () => {
|
||||
const existing = { v: 2 };
|
||||
const newFields = { date: Date.now() };
|
||||
const expected = { ...existing, ...newFields };
|
||||
const item = storage.defineItem<number, { date: number; v: number }>(
|
||||
`local:count`,
|
||||
);
|
||||
await fakeBrowser.storage.local.set({
|
||||
count$: existing,
|
||||
});
|
||||
|
||||
await item.setMeta(newFields);
|
||||
const actual = await item.getMeta();
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeValue', () => {
|
||||
it('should remove the key from storage', async () => {
|
||||
const item = storage.defineItem(`local:count`);
|
||||
await fakeBrowser.storage.local.set({ count: 456 });
|
||||
|
||||
await item.removeValue();
|
||||
const actual = await item.getValue();
|
||||
|
||||
expect(actual).toBeNull();
|
||||
});
|
||||
|
||||
it('should not remove the metadata by default', async () => {
|
||||
const item = storage.defineItem(`local:count`);
|
||||
const expected = { v: 1 };
|
||||
await fakeBrowser.storage.local.set({
|
||||
count$: expected,
|
||||
count: 3,
|
||||
});
|
||||
|
||||
await item.removeValue();
|
||||
const actual = await item.getMeta();
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should remove the metadata when requested', async () => {
|
||||
const item = storage.defineItem(`local:count`);
|
||||
await fakeBrowser.storage.local.set({
|
||||
count$: { v: 1 },
|
||||
count: 3,
|
||||
});
|
||||
|
||||
await item.removeValue({ removeMeta: true });
|
||||
const actual = await item.getMeta();
|
||||
|
||||
expect(actual).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeMeta', () => {
|
||||
it('should remove all metadata', async () => {
|
||||
const item = storage.defineItem<number, { v: number }>(`local:count`);
|
||||
await fakeBrowser.storage.local.set({ count$: { v: 4 } });
|
||||
|
||||
await item.removeMeta();
|
||||
const actual = await item.getMeta();
|
||||
|
||||
expect(actual).toEqual({});
|
||||
});
|
||||
|
||||
it('should only remove specific properties', async () => {
|
||||
const item = storage.defineItem<number, { v: number; d: number }>(
|
||||
`local:count`,
|
||||
);
|
||||
await fakeBrowser.storage.local.set({
|
||||
count$: { v: 4, d: Date.now() },
|
||||
});
|
||||
|
||||
await item.removeMeta(['d']);
|
||||
const actual = await item.getMeta();
|
||||
|
||||
expect(actual).toEqual({ v: 4 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('watch', () => {
|
||||
it("should not trigger if the changed key is different from the item's key", async () => {
|
||||
const item = storage.defineItem(`local:key`);
|
||||
const cb = vi.fn();
|
||||
|
||||
item.watch(cb);
|
||||
await storage.setItem(`local:not-the-key`, '123');
|
||||
|
||||
expect(cb).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("should not trigger if the value doesn't change", async () => {
|
||||
const item = storage.defineItem(`local:key`);
|
||||
const cb = vi.fn();
|
||||
const value = '123';
|
||||
|
||||
await item.setValue(value);
|
||||
item.watch(cb);
|
||||
await item.setValue(value);
|
||||
|
||||
expect(cb).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should call the callback when the value changes', async () => {
|
||||
const item = storage.defineItem(`local:key`);
|
||||
const cb = vi.fn();
|
||||
const newValue = '123';
|
||||
const oldValue = null;
|
||||
|
||||
item.watch(cb);
|
||||
await item.setValue(newValue);
|
||||
|
||||
expect(cb).toBeCalledTimes(1);
|
||||
expect(cb).toBeCalledWith(newValue, oldValue);
|
||||
});
|
||||
|
||||
it('should use the default value for the newValue when the item is removed', async () => {
|
||||
const defaultValue = 'default';
|
||||
const item = storage.defineItem<string>(`local:key`, {
|
||||
defaultValue,
|
||||
});
|
||||
const cb = vi.fn();
|
||||
const oldValue = '123';
|
||||
await item.setValue(oldValue);
|
||||
|
||||
item.watch(cb);
|
||||
await item.removeValue();
|
||||
|
||||
expect(cb).toBeCalledTimes(1);
|
||||
expect(cb).toBeCalledWith(defaultValue, oldValue);
|
||||
});
|
||||
|
||||
it("should use the default value for the oldItem when the item didn't exist in storage yet", async () => {
|
||||
const defaultValue = 'default';
|
||||
const item = storage.defineItem<string>(`local:key`, {
|
||||
defaultValue,
|
||||
});
|
||||
const cb = vi.fn();
|
||||
const newValue = '123';
|
||||
await item.removeValue();
|
||||
|
||||
item.watch(cb);
|
||||
await item.setValue(newValue);
|
||||
|
||||
expect(cb).toBeCalledTimes(1);
|
||||
expect(cb).toBeCalledWith(newValue, defaultValue);
|
||||
});
|
||||
|
||||
it('should remove the listener when calling the returned function', async () => {
|
||||
const item = storage.defineItem(`local:key`);
|
||||
const cb = vi.fn();
|
||||
|
||||
const unwatch = item.watch(cb);
|
||||
unwatch();
|
||||
await item.setValue('123');
|
||||
|
||||
expect(cb).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('unwatch', () => {
|
||||
it('should remove all watch listeners', async () => {
|
||||
const item = storage.defineItem(`local:key`);
|
||||
const cb = vi.fn();
|
||||
|
||||
item.watch(cb);
|
||||
storage.unwatch();
|
||||
await item.setValue('123');
|
||||
|
||||
expect(cb).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('defaultValue', () => {
|
||||
it('should return the default value when provided', () => {
|
||||
const defaultValue = 123;
|
||||
const item = storage.defineItem(`local:test`, {
|
||||
defaultValue,
|
||||
});
|
||||
|
||||
expect(item.defaultValue).toBe(defaultValue);
|
||||
});
|
||||
|
||||
it('should return null when not provided', () => {
|
||||
const item = storage.defineItem<number>(`local:test`);
|
||||
|
||||
expect(item.defaultValue).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('types', () => {
|
||||
it('should define a nullable value when options are not passed', () => {
|
||||
const item = storage.defineItem<number>(`local:test`);
|
||||
expectTypeOf(item).toEqualTypeOf<WxtStorageItem<number | null, {}>>();
|
||||
});
|
||||
|
||||
it('should define a non-null value when options are passed with a nullish default value', () => {
|
||||
const item = storage.defineItem(`local:test`, {
|
||||
defaultValue: 123,
|
||||
});
|
||||
expectTypeOf(item).toEqualTypeOf<WxtStorageItem<number, {}>>();
|
||||
});
|
||||
|
||||
it('should define a nullable value when options are passed with null default value', () => {
|
||||
const item = storage.defineItem<number | null>(`local:test`, {
|
||||
defaultValue: null,
|
||||
});
|
||||
expectTypeOf(item).toEqualTypeOf<WxtStorageItem<number | null, {}>>();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+10
-2
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -101,8 +101,8 @@ export async function createServer(
|
||||
transformHtml(url, html, originalUrl) {
|
||||
return builderServer.transformHtml(url, html, originalUrl);
|
||||
},
|
||||
reloadContentScript(contentScript) {
|
||||
server.ws.send('wxt:reload-content-script', contentScript);
|
||||
reloadContentScript(payload) {
|
||||
server.ws.send('wxt:reload-content-script', payload);
|
||||
},
|
||||
reloadPage(path) {
|
||||
server.ws.send('wxt:reload-page', path);
|
||||
@@ -237,9 +237,14 @@ function reloadContentScripts(steps: BuildStepOutput[], server: WxtDevServer) {
|
||||
const cssMap = getContentScriptsCssMap(server.currentOutput, [entry]);
|
||||
const css = getContentScriptCssFiles([entry], cssMap);
|
||||
|
||||
server.reloadContentScript(
|
||||
mapWxtOptionsToRegisteredContentScript(entry.options, js, css),
|
||||
);
|
||||
server.reloadContentScript({
|
||||
registration: entry.options.registration,
|
||||
contentScript: mapWxtOptionsToRegisteredContentScript(
|
||||
entry.options,
|
||||
js,
|
||||
css,
|
||||
),
|
||||
});
|
||||
});
|
||||
} else {
|
||||
server.reloadExtension();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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'), '../..');
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -262,6 +262,7 @@ export const fakeResolvedConfig = fakeObjectCreator<ResolvedConfig>(() => {
|
||||
outDir: fakeDir(),
|
||||
publicDir: fakeDir(),
|
||||
root: fakeDir(),
|
||||
wxtModuleDir: fakeDir(),
|
||||
runnerConfig: {
|
||||
config: {},
|
||||
},
|
||||
|
||||
+89
-35
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Simplfied storage APIs with support for versioned fields, snapshots, metadata, and item definitions.
|
||||
* Simplified storage APIs with support for versioned fields, snapshots, metadata, and item definitions.
|
||||
*
|
||||
* See [the guide](https://wxt.dev/guide/storage.html) for more information.
|
||||
*
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
import { Storage, browser } from '~/browser';
|
||||
import { dequal } from 'dequal/lite';
|
||||
import { logger } from './sandbox/utils/logger';
|
||||
|
||||
export const storage = createStorage();
|
||||
|
||||
@@ -31,7 +32,7 @@ function createStorage(): WxtStorage {
|
||||
const driverKey = key.substring(deliminatorIndex + 1);
|
||||
if (driverKey == null)
|
||||
throw Error(
|
||||
`Storage key should be in the form of "area:key", but recieved "${key}"`,
|
||||
`Storage key should be in the form of "area:key", but received "${key}"`,
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -247,7 +248,7 @@ function createStorage(): WxtStorage {
|
||||
driver.unwatch();
|
||||
});
|
||||
},
|
||||
defineItem: (key, opts) => {
|
||||
defineItem: (key: string, opts?: WxtStorageItemOptions<any>) => {
|
||||
const { driver, driverKey } = resolveKey(key);
|
||||
|
||||
const { version: targetVersion = 1, migrations = {} } = opts ?? {};
|
||||
@@ -256,21 +257,24 @@ function createStorage(): WxtStorage {
|
||||
'Storage item version cannot be less than 1. Initial versions should be set to 1, not 0.',
|
||||
);
|
||||
}
|
||||
const runMigrations = async () => {
|
||||
const [value, meta] = await Promise.all([
|
||||
// TODO: Optimize with getItems
|
||||
getItem(driver, driverKey, undefined),
|
||||
getMeta(driver, driverKey),
|
||||
const migrate = async () => {
|
||||
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(
|
||||
`[wxt/storage] Migration ignored for "${key}", version downgrade detected (${currentVersion} -> ${targetVersion})`,
|
||||
`Version downgrade detected (v${currentVersion} -> v${targetVersion}) for "${key}"`,
|
||||
);
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Running storage migration for ${key}: v${currentVersion} -> v${targetVersion}`,
|
||||
);
|
||||
const migrationsToRun = Array.from(
|
||||
{ length: targetVersion - currentVersion },
|
||||
(_, i) => currentVersion + i + 1,
|
||||
@@ -281,23 +285,57 @@ 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 },
|
||||
);
|
||||
};
|
||||
let _migrationsCompleted = runMigrations();
|
||||
const migrationsDone =
|
||||
opts?.migrations == null
|
||||
? Promise.resolve()
|
||||
: migrate().catch((err) => {
|
||||
logger.error(`Migration failed for ${key}`, err);
|
||||
});
|
||||
|
||||
const getDefaultValue = () => opts?.defaultValue ?? null;
|
||||
|
||||
return {
|
||||
_migrationsCompleted,
|
||||
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),
|
||||
watch: (cb) => watch(driver, driverKey, cb),
|
||||
get defaultValue() {
|
||||
return getDefaultValue();
|
||||
},
|
||||
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()),
|
||||
),
|
||||
migrate,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -323,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
|
||||
@@ -398,7 +439,8 @@ export interface WxtStorage {
|
||||
*/
|
||||
getItem<T>(key: string, opts?: GetItemOptions<T>): Promise<T | null>;
|
||||
/**
|
||||
* Get multiple items from storage. There is no guarentee 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"]);
|
||||
@@ -476,26 +518,29 @@ export interface WxtStorage {
|
||||
): Promise<Record<string, unknown>>;
|
||||
/**
|
||||
* Restores the results of `snapshot`. If new properties have been saved since the snapshot, they are
|
||||
* not overridden. Only values existing in the snapshot are overritten.
|
||||
* not overridden. Only values existing in the snapshot are overridden.
|
||||
*/
|
||||
restoreSnapshot(base: string, data: any): Promise<void>;
|
||||
/**
|
||||
* Watch for changes to a specific key in storage.
|
||||
*/
|
||||
watch<T>(key: string, cb: WatchCallback<T>): Unwatch;
|
||||
watch<T>(key: string, cb: WatchCallback<T | null>): Unwatch;
|
||||
/**
|
||||
* Remove all watch listeners.
|
||||
*/
|
||||
unwatch(): void;
|
||||
|
||||
/**
|
||||
* Define a constant with utilities for reading/writing to a single value in storage.
|
||||
* Define a storage item with a default value, type, or versioning.
|
||||
*
|
||||
* @example
|
||||
* export const installDate = storage.defineItem<number>("local:installDate");
|
||||
* Read full docs: https://wxt.dev/guide/storage.html#defining-storage-items
|
||||
*/
|
||||
defineItem<TValue, TMetadata extends Record<string, unknown> = {}>(
|
||||
key: string,
|
||||
options?: WxtStorageItemOptions<TValue>,
|
||||
): WxtStorageItem<TValue | null, TMetadata>;
|
||||
defineItem<TValue, TMetadata extends Record<string, unknown> = {}>(
|
||||
key: string,
|
||||
options: WxtStorageItemOptions<TValue>,
|
||||
): WxtStorageItem<TValue, TMetadata>;
|
||||
}
|
||||
|
||||
@@ -508,7 +553,7 @@ interface WxtStorageDriver {
|
||||
removeItems(keys: string[]): Promise<void>;
|
||||
snapshot(): Promise<Record<string, unknown>>;
|
||||
restoreSnapshot(data: Record<string, unknown>): Promise<void>;
|
||||
watch<T>(key: string, cb: WatchCallback<T>): Unwatch;
|
||||
watch<T>(key: string, cb: WatchCallback<T | null>): Unwatch;
|
||||
unwatch(): void;
|
||||
}
|
||||
|
||||
@@ -516,6 +561,7 @@ export interface WxtStorageItem<
|
||||
TValue,
|
||||
TMetadata extends Record<string, unknown>,
|
||||
> {
|
||||
defaultValue: TValue;
|
||||
/**
|
||||
* Get the latest value from storage.
|
||||
*/
|
||||
@@ -527,7 +573,7 @@ export interface WxtStorageItem<
|
||||
/**
|
||||
* Set the value in storage.
|
||||
*/
|
||||
setValue(value: TValue | null): Promise<void>;
|
||||
setValue(value: TValue): Promise<void>;
|
||||
/**
|
||||
* Set metadata properties.
|
||||
*/
|
||||
@@ -544,6 +590,13 @@ export interface WxtStorageItem<
|
||||
* Listen for changes to the value in storage.
|
||||
*/
|
||||
watch(cb: WatchCallback<TValue>): Unwatch;
|
||||
/**
|
||||
* If there are migrations defined on the storage item, migrate to the latest version.
|
||||
*
|
||||
* **This function is ran automatically whenever the extension updates**, so you don't have to call it
|
||||
* manually.
|
||||
*/
|
||||
migrate(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface GetItemOptions<T> {
|
||||
@@ -570,7 +623,8 @@ export interface SnapshotOptions {
|
||||
excludeKeys?: string[];
|
||||
}
|
||||
|
||||
export interface WxtStorageItemOptions<T> extends GetItemOptions<T> {
|
||||
export interface WxtStorageItemOptions<T> {
|
||||
defaultValue: T;
|
||||
/**
|
||||
* Provide a version number for the storage item to enable migrations. When changing the version
|
||||
* in the future, migration functions will be ran on application startup.
|
||||
@@ -592,7 +646,7 @@ export type NullablePartial<T> = {
|
||||
/**
|
||||
* Callback called when a value in storage is changed.
|
||||
*/
|
||||
export type WatchCallback<T> = (newValue: T | null, oldValue: T | null) => void;
|
||||
export type WatchCallback<T> = (newValue: T, oldValue: T) => void;
|
||||
/**
|
||||
* Call to remove a watch listener
|
||||
*/
|
||||
|
||||
+12
-5
@@ -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';
|
||||
@@ -396,17 +396,20 @@ export interface WxtDevServer
|
||||
/**
|
||||
* Tell the extension to restart a content script.
|
||||
*
|
||||
* @param contentScript The manifest definition for a content script
|
||||
* @param payload Information about the content script to reload.
|
||||
*/
|
||||
reloadContentScript: (
|
||||
contentScript: Omit<Scripting.RegisteredContentScript, 'id'>,
|
||||
) => void;
|
||||
reloadContentScript: (payload: ReloadContentScriptPayload) => void;
|
||||
/**
|
||||
* Grab the latest runner config and restart the browser.
|
||||
*/
|
||||
restartBrowser: () => void;
|
||||
}
|
||||
|
||||
export interface ReloadContentScriptPayload {
|
||||
registration?: BaseContentScriptEntrypointOptions['registration'];
|
||||
contentScript: Omit<Scripting.RegisteredContentScript, 'id'>;
|
||||
}
|
||||
|
||||
export type TargetBrowser = string;
|
||||
export type TargetManifestVersion = 2 | 3;
|
||||
|
||||
@@ -960,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;
|
||||
|
||||
Vendored
-12
@@ -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;
|
||||
|
||||
@@ -39,7 +39,7 @@ try {
|
||||
// @ts-expect-error: res shouldn't be a promise, but we're checking it anyways
|
||||
if (res instanceof Promise) {
|
||||
console.warn(
|
||||
"The background's main() function return a promise, but it must be synchonous",
|
||||
"The background's main() function return a promise, but it must be synchronous",
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -7,17 +7,34 @@ interface ContentScript {
|
||||
js?: string[];
|
||||
css?: string[];
|
||||
}
|
||||
interface ReloadContentScriptPayload {
|
||||
registration?: 'manifest' | 'runtime';
|
||||
contentScript: ContentScript;
|
||||
}
|
||||
|
||||
export function reloadContentScript(contentScript: ContentScript) {
|
||||
export function reloadContentScript(payload: ReloadContentScriptPayload) {
|
||||
const manifest = browser.runtime.getManifest();
|
||||
if (manifest.manifest_version == 2) {
|
||||
void reloadContentScriptMv2(contentScript);
|
||||
void reloadContentScriptMv2(payload);
|
||||
} else {
|
||||
void reloadContentScriptMv3(contentScript);
|
||||
void reloadContentScriptMv3(payload);
|
||||
}
|
||||
}
|
||||
|
||||
export async function reloadContentScriptMv3(contentScript: ContentScript) {
|
||||
export async function reloadContentScriptMv3({
|
||||
registration,
|
||||
contentScript,
|
||||
}: ReloadContentScriptPayload) {
|
||||
if (registration === 'runtime') {
|
||||
await reloadRuntimeContentScriptMv3(contentScript);
|
||||
} else {
|
||||
await reloadManifestContentScriptMv3(contentScript);
|
||||
}
|
||||
}
|
||||
|
||||
export async function reloadManifestContentScriptMv3(
|
||||
contentScript: ContentScript,
|
||||
) {
|
||||
const id = `wxt:${contentScript.js![0]}`;
|
||||
logger.log('Reloading content script:', contentScript);
|
||||
const registered = await browser.scripting.getRegisteredContentScripts();
|
||||
@@ -33,6 +50,35 @@ export async function reloadContentScriptMv3(contentScript: ContentScript) {
|
||||
await browser.scripting.registerContentScripts([{ ...contentScript, id }]);
|
||||
}
|
||||
|
||||
await reloadTabsForContentScript(contentScript);
|
||||
}
|
||||
|
||||
export async function reloadRuntimeContentScriptMv3(
|
||||
contentScript: ContentScript,
|
||||
) {
|
||||
logger.log('Reloading content script:', contentScript);
|
||||
const registered = await browser.scripting.getRegisteredContentScripts();
|
||||
logger.debug('Existing scripts:', registered);
|
||||
|
||||
const matches = registered.filter((cs) => {
|
||||
const hasJs = contentScript.js?.find((js) => cs.js?.includes(js));
|
||||
const hasCss = contentScript.css?.find((css) => cs.css?.includes(css));
|
||||
return hasJs || hasCss;
|
||||
});
|
||||
|
||||
if (matches.length === 0) {
|
||||
logger.log(
|
||||
'Content script is not registered yet, nothing to reload',
|
||||
contentScript,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await browser.scripting.updateContentScripts(matches);
|
||||
await reloadTabsForContentScript(contentScript);
|
||||
}
|
||||
|
||||
async function reloadTabsForContentScript(contentScript: ContentScript) {
|
||||
const allTabs = await browser.tabs.query({});
|
||||
const matchPatterns = contentScript.matches.map(
|
||||
(match) => new MatchPattern(match),
|
||||
@@ -45,6 +91,8 @@ export async function reloadContentScriptMv3(contentScript: ContentScript) {
|
||||
await Promise.all(matchingTabs.map((tab) => browser.tabs.reload(tab.id)));
|
||||
}
|
||||
|
||||
export async function reloadContentScriptMv2(contentScript: ContentScript) {
|
||||
export async function reloadContentScriptMv2(
|
||||
_payload: ReloadContentScriptPayload,
|
||||
) {
|
||||
throw Error('TODO: reloadContentScriptMv2');
|
||||
}
|
||||
|
||||
@@ -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.');
|
||||
},
|
||||
});
|
||||
@@ -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.');
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
export default defineContentScript({
|
||||
matches: ['*://*.google.com/*'],
|
||||
main() {
|
||||
console.log('Hello content.');
|
||||
},
|
||||
});
|
||||
+1
-1
@@ -7,7 +7,7 @@ import path from 'node:path';
|
||||
const seed = Math.round(Math.random() * Number.MAX_SAFE_INTEGER);
|
||||
console.info('Test seed: ' + pc.cyan(seed));
|
||||
|
||||
// config.define doesn't work with workspaces, so we have to set it inisde a plugin
|
||||
// config.define doesn't work with workspaces, so we have to set it inside a plugin
|
||||
const testSeed = (): Plugin => ({
|
||||
name: 'test-seed',
|
||||
config(config) {
|
||||
|
||||
Reference in New Issue
Block a user