Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ae8d6bc46 | |||
| de7dd09f60 | |||
| 1649785826 | |||
| 91fc41cb15 | |||
| 9c80ad832b | |||
| 86b7162d99 | |||
| b8f8acd33d |
+23
-1
@@ -1,5 +1,27 @@
|
||||
# Changelog
|
||||
|
||||
## 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
|
||||
|
||||
- **storage:** ⚠️ Improved support for default values on storage items ([#477](https://github.com/wxt-dev/wxt/pull/477))
|
||||
- **storage:** ⚠️ Only run migrations when the extension is updated ([#478](https://github.com/wxt-dev/wxt/pull/478))
|
||||
|
||||
## v0.16.11
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.10...v0.16.11)
|
||||
@@ -15,7 +37,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
|
||||
|
||||
|
||||
+37
-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,30 @@ 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.
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wxt",
|
||||
"type": "module",
|
||||
"version": "0.16.11",
|
||||
"version": "0.17.0",
|
||||
"description": "Next gen framework for developing web extensions",
|
||||
"engines": {
|
||||
"node": ">=18",
|
||||
|
||||
+469
-413
@@ -1,7 +1,14 @@
|
||||
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';
|
||||
|
||||
async function triggerUpdate() {
|
||||
await fakeBrowser.runtime.onInstalled.trigger({
|
||||
reason: 'update',
|
||||
temporary: false,
|
||||
});
|
||||
}
|
||||
|
||||
describe('Storage Utils', () => {
|
||||
beforeEach(() => {
|
||||
@@ -384,417 +391,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 triggerUpdate();
|
||||
|
||||
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 triggerUpdate();
|
||||
|
||||
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 triggerUpdate();
|
||||
|
||||
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 triggerUpdate();
|
||||
|
||||
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 triggerUpdate();
|
||||
|
||||
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 triggerUpdate();
|
||||
|
||||
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, {}>>();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
+50
-15
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
import { Storage, browser } from '~/browser';
|
||||
import { dequal } from 'dequal/lite';
|
||||
import { logger } from './sandbox/utils/logger';
|
||||
|
||||
export const storage = createStorage();
|
||||
|
||||
@@ -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,7 +257,7 @@ function createStorage(): WxtStorage {
|
||||
'Storage item version cannot be less than 1. Initial versions should be set to 1, not 0.',
|
||||
);
|
||||
}
|
||||
const runMigrations = async () => {
|
||||
const migrate = async () => {
|
||||
const [value, meta] = await Promise.all([
|
||||
// TODO: Optimize with getItems
|
||||
getItem(driver, driverKey, undefined),
|
||||
@@ -267,10 +268,13 @@ function createStorage(): WxtStorage {
|
||||
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,
|
||||
@@ -286,18 +290,37 @@ function createStorage(): WxtStorage {
|
||||
setItem(driver, driverKey, migratedValue),
|
||||
setMeta(driver, driverKey, { v: targetVersion }),
|
||||
]);
|
||||
logger.debug(
|
||||
`Storage migration completed for ${key} v${targetVersion}`,
|
||||
{ migratedValue },
|
||||
);
|
||||
};
|
||||
let _migrationsCompleted = runMigrations();
|
||||
browser.runtime.onInstalled.addListener(async ({ reason }) => {
|
||||
if (reason !== 'update') return;
|
||||
try {
|
||||
await migrate();
|
||||
} catch (err) {
|
||||
logger.error(`Migration failed for ${key}`, err);
|
||||
}
|
||||
});
|
||||
|
||||
const getDefaultValue = () => opts?.defaultValue ?? null;
|
||||
|
||||
return {
|
||||
_migrationsCompleted,
|
||||
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),
|
||||
watch: (cb) => watch(driver, driverKey, cb),
|
||||
watch: (cb) =>
|
||||
watch(driver, driverKey, (newValue, oldValue) =>
|
||||
cb(newValue ?? getDefaultValue(), oldValue ?? getDefaultValue()),
|
||||
),
|
||||
migrate,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -482,20 +505,23 @@ export interface WxtStorage {
|
||||
/**
|
||||
* 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 +534,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 +542,7 @@ export interface WxtStorageItem<
|
||||
TValue,
|
||||
TMetadata extends Record<string, unknown>,
|
||||
> {
|
||||
defaultValue: TValue;
|
||||
/**
|
||||
* Get the latest value from storage.
|
||||
*/
|
||||
@@ -527,7 +554,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 +571,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 +604,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 +627,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
|
||||
*/
|
||||
|
||||
+7
-4
@@ -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;
|
||||
|
||||
|
||||
@@ -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 reloadRuntimeContentScriptMv3(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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user