Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 64610bad41 | |||
| 45b7d7c67f | |||
| 0bb2746869 | |||
| f9b0aa45f8 | |||
| 6f6ce33bbd | |||
| e1c6020997 | |||
| 012bd7e67f | |||
| 35778f70cb | |||
| cb4f9aa752 | |||
| f80fb42483 | |||
| 02c79ca244 | |||
| 50e8c86ce9 | |||
| 6fbfc15be7 | |||
| c793d3cf6b | |||
| b5f4d8cc1b | |||
| 77e6d1f5a4 |
@@ -1,5 +1,49 @@
|
||||
# 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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -52,9 +52,11 @@ When defining multiple content scripts, content script entrypoints that have the
|
||||
|
||||
## Context
|
||||
|
||||
Old content scripts are not automatically stopped when an extension updates and reloads. Often, this leads to "Invalidated context" errors in production when a content script from an old version of your extension tries to use a extension API.
|
||||
Old content scripts are not automatically stopped when an extension updates and reloads. Often, this leads to "Invalidated context" errors in production when a content script from an old version of your extension tries to use a web extension API (ie, the `browser` or `chrome` globals).
|
||||
|
||||
WXT provides a utility for managing this process: `ContentScriptContext`. An instance of this class is provided to you automatically inside the `main` function of your content script.
|
||||
WXT provides a utility for handling this process: `ContentScriptContext`. An instance of this class is provided to you automatically inside the `main` function of your content script.
|
||||
|
||||
When your extension updates or is uninstalled, the context will become invalidated, and will trigger any `ctx.onInvalidated` listeners you add:
|
||||
|
||||
```ts
|
||||
export default defineContentScript({
|
||||
@@ -65,26 +67,33 @@ export default defineContentScript({
|
||||
// ...
|
||||
});
|
||||
|
||||
// Stop fetch requests
|
||||
fetch('...url', { signal: ctx.signal });
|
||||
|
||||
// Timeout utilities
|
||||
// Timeout utilities that are automatically cleared when invalidated
|
||||
ctx.setTimeout(() => {
|
||||
// ...
|
||||
}, 5e3);
|
||||
ctx.setInterval(() => {
|
||||
// ...
|
||||
}, 60e3);
|
||||
|
||||
// Or add event listeners that get removed when invalidated
|
||||
ctx.addEventListener(document, 'visibilitychange', (event) => {
|
||||
// ...
|
||||
});
|
||||
|
||||
// You can also stop fetch requests
|
||||
fetch('...url', { signal: ctx.signal });
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The class extends [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) and provides other utilities for stopping a content script's logic once it becomes invalidated.
|
||||
|
||||
:::tip
|
||||
:::warning
|
||||
When working with content scripts, **you should always use the `ctx` object to stop any async or future work.**
|
||||
|
||||
This prevents old content scripts from interfering with new content scripts, and prevents error messages from the console in production.
|
||||
|
||||
If you're using a framework like React, Vue, Svelte, etc., make sure you're unmounting your UI properly in the `onRemove` option of [`createShadowRootUi`](https://wxt.dev/guide/content-script-ui.html#shadow-root).
|
||||
:::
|
||||
|
||||
## CSS
|
||||
|
||||
+29
-28
@@ -1,39 +1,40 @@
|
||||
# Compare
|
||||
|
||||
Lets compare the features of WXT vs [Plasmo](https://docs.plasmo.com/framework), another web extension framework.
|
||||
Lets compare the features of WXT vs [Plasmo](https://docs.plasmo.com/framework) (another web extension framework) and [CRXJS](https://crxjs.dev/vite-plugin) (the most popular bundler plugin).
|
||||
|
||||
## Overview
|
||||
|
||||
| Features | WXT | Plasmo |
|
||||
| ---------------------------------------------------- | :-------------------------: | :--------------------------------------: |
|
||||
| Supports all browsers | ✅ | ✅ |
|
||||
| MV2 Support | ✅ | ✅ |
|
||||
| MV3 Support | ✅ | ✅ |
|
||||
| Create Extension ZIPs | ✅ | ✅ |
|
||||
| Create Firefox Sources ZIP | ✅ | ❌ |
|
||||
| First-class TypeScript support | ✅ | ✅ |
|
||||
| File based entrypoint discovery | ✅ | ✅ |
|
||||
| Inline entrypoint config | ✅ | ✅ |
|
||||
| Auto-imports | ✅ | ❌ |
|
||||
| Supports all frontend frameworks | ✅ | 🟡 Only React, Vue, and Svelte |
|
||||
| Framework specific entrypoints (like `Popup.tsx`) | 🟡 `.html` `.ts` `.tsx` | ✅ `.html` `.ts` `.tsx` `.vue` `.svelte` |
|
||||
| Automated publishing | 🟡 Coming soon | ✅ |
|
||||
| Remote Code Bundling (Google Analytics) | ✅ | ✅ |
|
||||
| Features | WXT | Plasmo | CRXJS |
|
||||
| ---------------------------------------------------- | :-------------------------: | :--------------------------------------: | :---------------------------------------------------------------------: |
|
||||
| Supports all browsers | ✅ | ✅ | ❌ See [#56](https://github.com/crxjs/chrome-extension-tools/issues/56) |
|
||||
| MV2 Support | ✅ | ✅ | 🟡 Either MV2 or MV3 |
|
||||
| MV3 Support | ✅ | ✅ | 🟡 Either MV2 or MV3 |
|
||||
| Create Extension ZIPs | ✅ | ✅ | ❌ |
|
||||
| Create Firefox Sources ZIP | ✅ | ❌ | ❌ |
|
||||
| First-class TypeScript support | ✅ | ✅ | ✅ |
|
||||
| Entrypoint discovery | File based | File based | ❌ |
|
||||
| Inline entrypoint config | ✅ | ✅ | Manifest based |
|
||||
| Auto-imports | ✅ | ❌ | ❌ |
|
||||
| Supports all frontend frameworks | ✅ | 🟡 Only React, Vue, and Svelte | ✅ |
|
||||
| Framework specific entrypoints (like `Popup.tsx`) | 🟡 `.html` `.ts` `.tsx` | ✅ `.html` `.ts` `.tsx` `.vue` `.svelte` | ❌ |
|
||||
| Automated publishing | ✅ | ✅ | ❌ |
|
||||
| Remote Code Bundling (Google Analytics) | ✅ | ✅ | ❌ |
|
||||
| <strong style="opacity: 50%">Dev Mode</strong> | | |
|
||||
| `.env` Files | ✅ | ✅ |
|
||||
| Opens browser and install extension | ✅ | ❌ |
|
||||
| HMR for UIs | ✅ | 🟡 React only |
|
||||
| Reload HTML Files on Change | ✅ | 🟡 Reloads entire extension |
|
||||
| Reload Content Scripts on Change | ✅ | 🟡 Reloads entire extension |
|
||||
| Reload Background on Change | 🟡 Reloads entire extension | 🟡 Reloads entire extension |
|
||||
| <strong style="opacity: 50%">Built-in Utils</strong> | | |
|
||||
| Storage | ✅ | ✅ |
|
||||
| Messaging | 🟡 Coming soon | ✅ |
|
||||
| Content Script UI | ✅ | ✅ |
|
||||
| `.env` Files | ✅ | ✅ | ✅ |
|
||||
| Opens browser and install extension | ✅ | ❌ | ❌ |
|
||||
| HMR for UIs | ✅ | 🟡 React only | ✅ |
|
||||
| Reload HTML Files on Change | ✅ | 🟡 Reloads entire extension | ✅ |
|
||||
| Reload Content Scripts on Change | ✅ | 🟡 Reloads entire extension | ✅ |
|
||||
| Reload Background on Change | 🟡 Reloads entire extension | 🟡 Reloads entire extension | 🟡 Reloads entire extension |
|
||||
| Respects Content Script `run_at` | ✅ | ✅ | ❌ ESM-style loaders run asynchronously |
|
||||
| <strong style="opacity: 50%">Built-in Utils</strong> | | | |
|
||||
| Storage | ✅ | ✅ | ❌ |
|
||||
| Messaging | ❌ | ✅ | ❌ |
|
||||
| Content Script UI | ✅ | ✅ | ❌ |
|
||||
|
||||
## Dev Mode
|
||||
## Why use WXT?
|
||||
|
||||
WXT's main goal is improving the development experience (DX) of creating web extensions. There are two things WXT does differently:
|
||||
WXT's main goal is improving the development experience (DX) of creating web extensions, while not sacrificing support. There are two things WXT does differently:
|
||||
|
||||
1. Automatically opens a browser with the extension installed when starting development
|
||||
2. Reload each part of the extension individually rather than reloading the entire extension
|
||||
|
||||
+1
-24
@@ -263,27 +263,4 @@ export const ignoredWebsites = storage.defineItem<IgnoredWebsiteV2[]>( // [!code
|
||||
|
||||
### Running Migrations
|
||||
|
||||
To run migrations, you have two options:
|
||||
|
||||
1. Import all versioned storage items into your background script and they will run automatically whenever your extension updates
|
||||
2. Manually call `item.migrate()`
|
||||
|
||||
The first approach is recommended. To make importing all your storage items easy, you can define all of them in a single file, `utils/storage.ts`, and import that file into your background entrypoint:
|
||||
|
||||
```ts
|
||||
// utils/storage.ts
|
||||
export countStorage = storage.defineItem(...);
|
||||
export themeStorage = storage.defineItem(...);
|
||||
export someOtherStorage = storage.defineItem(...);
|
||||
```
|
||||
|
||||
```ts
|
||||
// entrypoints/background.ts
|
||||
import '@/utils/storage'; // This import runs migrations on updates
|
||||
|
||||
export default defineBackground({
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
> When you call `storage.defineItem`, a `browser.runtime.onInstalled` listener is added. `onInstalled` listeners are only triggered in the background, which is why you must import them into the background.
|
||||
As soon as `storage.defineItem` is called, WXT checks if migrations need to be ran, and if so, runs them. Calls to get or update the storage item's value or metadata (`getValue`, `setValue`, `removeValue`, `getMeta`, etc) will automatically wait for the migration process to finish before actually reading or writing values.
|
||||
|
||||
@@ -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",
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wxt",
|
||||
"type": "module",
|
||||
"version": "0.17.2",
|
||||
"version": "0.17.4",
|
||||
"description": "Next gen framework for developing web extensions",
|
||||
"engines": {
|
||||
"node": ">=18",
|
||||
|
||||
@@ -3,11 +3,12 @@ import { describe, it, expect, beforeEach, vi, expectTypeOf } from 'vitest';
|
||||
import { browser } from '~/browser';
|
||||
import { WxtStorageItem, storage } from '~/storage';
|
||||
|
||||
async function triggerUpdate() {
|
||||
await fakeBrowser.runtime.onInstalled.trigger({
|
||||
reason: 'update',
|
||||
temporary: false,
|
||||
});
|
||||
/**
|
||||
* This works because fakeBrowser is synchronous, and is will finish any number of chained
|
||||
* calls within a single tick of the event loop, ie: a timeout of 0.
|
||||
*/
|
||||
async function waitForMigrations() {
|
||||
return new Promise((res) => setTimeout(res));
|
||||
}
|
||||
|
||||
describe('Storage Utils', () => {
|
||||
@@ -75,10 +76,7 @@ describe('Storage Utils', () => {
|
||||
|
||||
const actual = await storage.getItems(params);
|
||||
|
||||
expect(actual).toHaveLength(3);
|
||||
expected.forEach((item) => {
|
||||
expect(actual).toContainEqual(item);
|
||||
});
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -412,7 +410,7 @@ describe('Storage Utils', () => {
|
||||
3: migrateToV3,
|
||||
},
|
||||
});
|
||||
await triggerUpdate();
|
||||
await waitForMigrations();
|
||||
|
||||
const actualValue = await item.getValue();
|
||||
const actualMeta = await item.getMeta();
|
||||
@@ -439,7 +437,7 @@ describe('Storage Utils', () => {
|
||||
3: migrateToV3,
|
||||
},
|
||||
});
|
||||
await triggerUpdate();
|
||||
await waitForMigrations();
|
||||
|
||||
const actualValue = await item.getValue();
|
||||
const actualMeta = await item.getMeta();
|
||||
@@ -464,7 +462,7 @@ describe('Storage Utils', () => {
|
||||
2: migrateToV2,
|
||||
},
|
||||
});
|
||||
await triggerUpdate();
|
||||
await waitForMigrations();
|
||||
|
||||
const actualValue = await item.getValue();
|
||||
const actualMeta = await item.getMeta();
|
||||
@@ -492,7 +490,7 @@ describe('Storage Utils', () => {
|
||||
3: migrateToV3,
|
||||
},
|
||||
});
|
||||
await triggerUpdate();
|
||||
await waitForMigrations();
|
||||
|
||||
expect(migrateToV2).not.toBeCalled();
|
||||
expect(migrateToV3).not.toBeCalled();
|
||||
@@ -514,7 +512,7 @@ describe('Storage Utils', () => {
|
||||
3: migrateToV3,
|
||||
},
|
||||
});
|
||||
await triggerUpdate();
|
||||
await waitForMigrations();
|
||||
|
||||
const actualValue = await item.getValue();
|
||||
const actualMeta = await item.getMeta();
|
||||
@@ -541,7 +539,7 @@ describe('Storage Utils', () => {
|
||||
defaultValue: 0,
|
||||
version: nextVersion,
|
||||
});
|
||||
await triggerUpdate();
|
||||
await waitForMigrations();
|
||||
|
||||
await expect(item.migrate()).rejects.toThrow(
|
||||
'Version downgrade detected (v2 -> v1) for "local:count"',
|
||||
|
||||
@@ -10,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';
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'), '../..');
|
||||
}
|
||||
|
||||
@@ -262,6 +262,7 @@ export const fakeResolvedConfig = fakeObjectCreator<ResolvedConfig>(() => {
|
||||
outDir: fakeDir(),
|
||||
publicDir: fakeDir(),
|
||||
root: fakeDir(),
|
||||
wxtModuleDir: fakeDir(),
|
||||
runnerConfig: {
|
||||
config: {},
|
||||
},
|
||||
|
||||
+44
-25
@@ -258,14 +258,14 @@ function createStorage(): WxtStorage {
|
||||
);
|
||||
}
|
||||
const migrate = async () => {
|
||||
const [value, meta] = await Promise.all([
|
||||
// TODO: Optimize with getItems
|
||||
getItem(driver, driverKey, undefined),
|
||||
getMeta(driver, driverKey),
|
||||
const driverMetaKey = getMetaKey(driverKey);
|
||||
const [{ value }, { value: meta }] = await driver.getItems([
|
||||
driverKey,
|
||||
driverMetaKey,
|
||||
]);
|
||||
if (value == null) return;
|
||||
|
||||
const currentVersion = meta.v ?? 1;
|
||||
const currentVersion = meta?.v ?? 1;
|
||||
if (currentVersion > targetVersion) {
|
||||
throw Error(
|
||||
`Version downgrade detected (v${currentVersion} -> v${targetVersion}) for "${key}"`,
|
||||
@@ -285,24 +285,21 @@ function createStorage(): WxtStorage {
|
||||
(await migrations?.[migrateToVersion]?.(migratedValue)) ??
|
||||
migratedValue;
|
||||
}
|
||||
await Promise.all([
|
||||
// TODO: Optimize with `setItem`
|
||||
setItem(driver, driverKey, migratedValue),
|
||||
setMeta(driver, driverKey, { v: targetVersion }),
|
||||
await driver.setItems([
|
||||
{ key: driverKey, value: migratedValue },
|
||||
{ key: driverMetaKey, value: { ...meta, v: targetVersion } },
|
||||
]);
|
||||
logger.debug(
|
||||
`Storage migration completed for ${key} v${targetVersion}`,
|
||||
{ migratedValue },
|
||||
);
|
||||
};
|
||||
browser.runtime.onInstalled.addListener(async ({ reason }) => {
|
||||
if (reason !== 'update') return;
|
||||
try {
|
||||
await migrate();
|
||||
} catch (err) {
|
||||
logger.error(`Migration failed for ${key}`, err);
|
||||
}
|
||||
});
|
||||
const migrationsDone =
|
||||
opts?.migrations == null
|
||||
? Promise.resolve()
|
||||
: migrate().catch((err) => {
|
||||
logger.error(`Migration failed for ${key}`, err);
|
||||
});
|
||||
|
||||
const getDefaultValue = () => opts?.defaultValue ?? null;
|
||||
|
||||
@@ -310,12 +307,30 @@ function createStorage(): WxtStorage {
|
||||
get defaultValue() {
|
||||
return getDefaultValue();
|
||||
},
|
||||
getValue: () => getItem(driver, driverKey, opts),
|
||||
getMeta: () => getMeta(driver, driverKey),
|
||||
setValue: (value) => setItem(driver, driverKey, value),
|
||||
setMeta: (properties) => setMeta(driver, driverKey, properties),
|
||||
removeValue: (opts) => removeItem(driver, driverKey, opts),
|
||||
removeMeta: (properties) => removeMeta(driver, driverKey, properties),
|
||||
getValue: async () => {
|
||||
await migrationsDone;
|
||||
return await getItem(driver, driverKey, opts);
|
||||
},
|
||||
getMeta: async () => {
|
||||
await migrationsDone;
|
||||
return await getMeta(driver, driverKey);
|
||||
},
|
||||
setValue: async (value) => {
|
||||
await migrationsDone;
|
||||
return await setItem(driver, driverKey, value);
|
||||
},
|
||||
setMeta: async (properties) => {
|
||||
await migrationsDone;
|
||||
return await setMeta(driver, driverKey, properties);
|
||||
},
|
||||
removeValue: async (opts) => {
|
||||
await migrationsDone;
|
||||
return await removeItem(driver, driverKey, opts);
|
||||
},
|
||||
removeMeta: async (properties) => {
|
||||
await migrationsDone;
|
||||
return await removeMeta(driver, driverKey, properties);
|
||||
},
|
||||
watch: (cb) =>
|
||||
watch(driver, driverKey, (newValue, oldValue) =>
|
||||
cb(newValue ?? getDefaultValue(), oldValue ?? getDefaultValue()),
|
||||
@@ -346,7 +361,10 @@ function createDriver(
|
||||
);
|
||||
}
|
||||
|
||||
return browser.storage[storageArea];
|
||||
const area = browser.storage[storageArea];
|
||||
if (area == null)
|
||||
throw Error(`"browser.storage.${storageArea}" is undefined`);
|
||||
return area;
|
||||
};
|
||||
const watchListeners = new Set<
|
||||
(changes: Storage.StorageAreaOnChangedChangesType) => void
|
||||
@@ -421,7 +439,8 @@ export interface WxtStorage {
|
||||
*/
|
||||
getItem<T>(key: string, opts?: GetItemOptions<T>): Promise<T | null>;
|
||||
/**
|
||||
* Get multiple items from storage. There is no guarantee of order in the returned array.
|
||||
* Get multiple items from storage. The return order is guaranteed to be the same as the order
|
||||
* requested.
|
||||
*
|
||||
* @example
|
||||
* await storage.getItems(["local:installDate", "session:someCounter"]);
|
||||
|
||||
@@ -963,6 +963,10 @@ export interface ResolvedConfig {
|
||||
outBaseDir: string;
|
||||
outDir: string;
|
||||
debug: boolean;
|
||||
/**
|
||||
* Directory pointing to `node_modules/wxt`, wherever WXT is installed.
|
||||
*/
|
||||
wxtModuleDir: string;
|
||||
mode: string;
|
||||
command: 'build' | 'serve';
|
||||
browser: TargetBrowser;
|
||||
|
||||
@@ -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.');
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user