Compare commits

..

15 Commits

Author SHA1 Message Date
GitHub Actions 8f3a90eeb2 chore(release): @wxt-dev/module-react v1.1.2
vhs / vhs (push) Waiting to run
2024-11-15 15:38:36 +00:00
GitHub Actions a360965d92 chore(release): @wxt-dev/module-vue v1.0.2
vhs / vhs (push) Waiting to run
2024-11-15 15:28:53 +00:00
Aaron eb1ace6d20 fix: Use config:resolved hook to update config instead of ready (#1178) 2024-11-15 09:08:14 -06:00
GitHub Actions 497c902283 chore(release): wxt v0.19.16
vhs / vhs (push) Waiting to run
2024-11-15 14:57:29 +00:00
Aaron 4d4453b830 feat(hooks): Add new config:resolved hook (#1177) 2024-11-15 08:52:22 -06:00
GitHub Actions ef53580ed1 chore(release): wxt v0.19.15
vhs / vhs (push) Waiting to run
2024-11-14 16:41:09 +00:00
Aaron 52e5387d47 fix: Add extension ID to event used to invalidate ContentScriptContext (#1175) 2024-11-14 10:30:51 -06:00
Aaron f5b7f7e0e5 docs: Cleanup changelog 2024-11-14 10:11:39 -06:00
Aaron 592bdbe4d9 chore: Speed up sync-releases workflow by not building packages 2024-11-14 10:09:40 -06:00
Aaron 6fc227bad6 chore: Init changelog for storage package 2024-11-14 10:03:55 -06:00
Aaron d74f6b0735 feat: Extract wxt/storage to its own package, @wxt-dev/storage (#1129)
vhs / vhs (push) Waiting to run
2024-11-14 10:00:08 -06:00
dsrrlldr 62ea796045 docs: Add "Invoice Generator" and "Monthly Bill Tracker" to homepage (#1174) 2024-11-14 08:01:11 -06:00
Nishu 064704c9b7 fix: Add "/" to PublicPath and browser.runtime.getURL (#1171)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2024-11-14 02:02:00 -06:00
Qiwei Yang c846a1febf docs: Add "Text Search Pro" to homepage (#1172) 2024-11-14 01:07:53 -06:00
Aaron c3959e8427 fix: Respect configOrPath for dev server (#1169) 2024-11-13 16:25:03 -06:00
39 changed files with 1306 additions and 932 deletions
+4 -1
View File
@@ -4,6 +4,9 @@ inputs:
install:
default: 'true'
description: Whether or not to run 'pnpm install'
installArgs:
default: ''
description: Additional args to append to "pnpm install"
runs:
using: composite
steps:
@@ -17,4 +20,4 @@ runs:
- name: Install Dependencies
if: ${{ inputs.install == 'true' }}
shell: bash
run: pnpm install
run: pnpm install ${{ inputs.installArgs }}
+1
View File
@@ -13,6 +13,7 @@ on:
- module-solid
- module-svelte
- module-vue
- storage
- unocss
- wxt
+8 -5
View File
@@ -7,13 +7,14 @@ on:
default: wxt
type: choice
options:
- wxt
- module-react
- module-vue
- module-svelte
- module-solid
- auto-icons
- i18n
- module-react
- module-solid
- module-svelte
- module-vue
- storage
- wxt
jobs:
sync:
@@ -21,6 +22,8 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup
with:
installArgs: --ignore-scripts
- run: pnpm tsx scripts/sync-releases.ts ${{ inputs.package }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -53,6 +53,9 @@ const chromeExtensionIds = [
'nmldnjcblcihmegipecakhmnieiofmgl', // Keyword Rank Checker
'gppllamhaciichleihemgilcpledblpn', // YouTube Simple View - Hide distractions & more
'pccbghdfdnnkkbcdcibchpbffdgednkf', // Propbar - Property Data Enhancer
'lfknakglefggmdkjdfhhofkjnnolffkh', // Text Search Pro - Search by case and whole-word match!
'mbenhbocjckkbaojacmaepiameldglij', // Invoice Generator
'phlfhkmdofajnbhgmbmjkbkdgppgoppb', // Monthly Bill Tracker
];
const { data, err, isLoading } = useListExtensionDetails(chromeExtensionIds);
+2 -1
View File
@@ -13,6 +13,7 @@ import { version as wxtVersion } from '../../packages/wxt/package.json';
import { version as i18nVersion } from '../../packages/i18n/package.json';
import { version as autoIconsVersion } from '../../packages/auto-icons/package.json';
import { version as unocssVersion } from '../../packages/unocss/package.json';
import { version as storageVersion } from '../../packages/storage/package.json';
const title = 'Next-gen Web Extension Framework';
const titleSuffix = ' WXT';
@@ -87,7 +88,7 @@ export default defineConfig({
),
]),
navItem('Other Packages', [
navItem(`wxt/storage — ${wxtVersion}`, '/storage'),
navItem(`@wxt-dev/storage — ${storageVersion}`, '/storage'),
navItem(`@wxt-dev/auto-icons — ${autoIconsVersion}`, '/auto-icons'),
navItem(`@wxt-dev/i18n — ${i18nVersion}`, '/i18n'),
navItem(`@wxt-dev/unocss — ${unocssVersion}`, '/unocss'),
+1 -1
View File
@@ -75,7 +75,7 @@ import { defineWxtModule } from 'wxt/modules';
export default defineWxtModule({
setup(wxt) {
wxt.hook('ready', () => {
wxt.hook('config:resolved', () => {
wxt.config.outDir = 'dist';
});
},
+39 -15
View File
@@ -6,23 +6,47 @@ outline: deep
[Changelog](https://github.com/wxt-dev/wxt/blob/main/packages/wxt/CHANGELOG.md)
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:
A simplified wrapper around the extension storage APIs.
## Installation
### With WXT
This module is built-in to WXT, so you don't need to install anything.
```ts
import { storage } from 'wxt/storage';
```
> [!IMPORTANT]
> To use the `wxt/storage` API, the `"storage"` permission must be added to the manifest:
>
> ```ts
> // wxt.config.ts
> export default defineConfig({
> manifest: {
> permissions: ['storage'],
> },
> });
> ```
If you use auto-imports, `storage` is auto-imported for you, so you don't even need to import it!
### Without WXT
Install the NPM package:
```sh
npm i @wxt-dev/storage
pnpm add @wxt-dev/storage
yarn add @wxt-dev/storage
bun add @wxt-dev/storage
```
```ts
import { storage } from '@wxt-dev/storage';
```
## Storage Permission
To use the `wxt/storage` API, the `"storage"` permission must be added to the manifest:
```ts
// wxt.config.ts
export default defineConfig({
manifest: {
permissions: ['storage'],
},
});
```
## Basic Usage
@@ -51,7 +75,7 @@ await storage.watch<number>(
await storage.getMeta<{ v: number }>('local:installDate');
```
For a full list of methods available, see the [API reference](/api/reference/wxt/storage/interfaces/WxtStorage).
For a full list of methods available, see the [API reference](/api/reference/@wxt-dev/storage/interfaces/WxtStorage).
## Watchers
@@ -134,7 +158,7 @@ const unwatch = showChangelogOnUpdate.watch((newValue) => {
});
```
For a full list of properties and methods available, see the [API reference](/api/reference/wxt/storage/interfaces/WxtStorageItem).
For a full list of properties and methods available, see the [API reference](/api/reference/@wxt-dev/storage/interfaces/WxtStorageItem).
### Versioning
@@ -330,4 +354,4 @@ await storage.setItems([
]);
```
Refer to the [API Reference](/api/reference/wxt/storage/interfaces/WxtStorage) for types and examples of how to use all the bulk APIs.
Refer to the [API Reference](/api/reference/@wxt-dev/storage/interfaces/WxtStorage) for types and examples of how to use all the bulk APIs.
+18
View File
@@ -1,5 +1,23 @@
# Changelog
## v1.1.2
[compare changes](https://github.com/wxt-dev/wxt/compare/module-react-v1.1.1...module-react-v1.1.2)
### 🩹 Fixes
- Use `config:resolved` hook to update config instead of `ready` ([#1178](https://github.com/wxt-dev/wxt/pull/1178))
### 🏡 Chore
- Add `oxlint` for linting ([#947](https://github.com/wxt-dev/wxt/pull/947))
- Upgrade all non-major dependencies ([#1040](https://github.com/wxt-dev/wxt/pull/1040))
- **deps:** Upgrade all non-major dependencies ([#1164](https://github.com/wxt-dev/wxt/pull/1164))
### ❤️ Contributors
- Aaron ([@aklinker1](http://github.com/aklinker1))
## v1.1.1
[compare changes](https://github.com/wxt-dev/wxt/compare/module-react-v1.1.0...module-react-v1.1.1)
+1 -1
View File
@@ -15,7 +15,7 @@ export default defineWxtModule<ReactModuleOptions>({
addImportPreset(wxt, 'react');
// Enable auto-imports for JSX files
wxt.hooks.hook('ready', (wxt) => {
wxt.hook('config:resolved', (wxt) => {
if (wxt.config.imports === false) return;
wxt.config.imports.dirsScanOptions ??= {};
+2 -2
View File
@@ -17,7 +17,7 @@
"email": "aaronklinker1+wxt@gmail.com"
},
"license": "MIT",
"version": "1.1.1",
"version": "1.1.2",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
@@ -44,7 +44,7 @@
"prepare": "buildc --deps-only -- wxt prepare"
},
"peerDependencies": {
"wxt": ">=0.18.6"
"wxt": ">=0.19.16"
},
"dependencies": {
"@vitejs/plugin-react": "^4.3.3"
+1 -1
View File
@@ -18,7 +18,7 @@ export default defineWxtModule<SolidModuleOptions>({
addImportPreset(wxt, 'solid-js');
// Enable auto-imports for JSX files
wxt.hooks.hook('ready', (wxt) => {
wxt.hook('config:resolved', (wxt) => {
if (wxt.config.imports === false) return;
wxt.config.imports.dirsScanOptions ??= {};
+1 -1
View File
@@ -44,7 +44,7 @@
"prepare": "buildc --deps-only -- wxt prepare"
},
"peerDependencies": {
"wxt": ">=0.18.6"
"wxt": ">=0.19.16"
},
"dependencies": {
"vite-plugin-solid": "^2.10.2"
+19
View File
@@ -1,5 +1,24 @@
# Changelog
## v1.0.2
[compare changes](https://github.com/wxt-dev/wxt/compare/module-vue-v1.0.1...module-vue-v1.0.2)
### 🩹 Fixes
- Use `config:resolved` hook to update config instead of `ready` ([#1178](https://github.com/wxt-dev/wxt/pull/1178))
### 🏡 Chore
- Add `oxlint` for linting ([#947](https://github.com/wxt-dev/wxt/pull/947))
- **deps:** Bump @vitejs/plugin-vue from 5.1.1 to 5.1.4 ([#1020](https://github.com/wxt-dev/wxt/pull/1020))
- Upgrade all non-major dependencies ([#1040](https://github.com/wxt-dev/wxt/pull/1040))
- **deps:** Upgrade all non-major dependencies ([#1164](https://github.com/wxt-dev/wxt/pull/1164))
### ❤️ Contributors
- Aaron ([@aklinker1](http://github.com/aklinker1))
## v1.0.1
[compare changes](https://github.com/wxt-dev/wxt/compare/module-vue-v1.0.0...module-vue-v1.0.1)
+2 -2
View File
@@ -17,7 +17,7 @@
"email": "aaronklinker1+wxt@gmail.com"
},
"license": "MIT",
"version": "1.0.1",
"version": "1.0.2",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
@@ -42,7 +42,7 @@
"check": "pnpm build && check"
},
"peerDependencies": {
"wxt": ">=0.18.6"
"wxt": ">=0.19.16"
},
"dependencies": {
"@vitejs/plugin-vue": "^5.2.0"
+1 -1
View File
@@ -19,7 +19,7 @@ export default defineWxtModule<VueModuleOptions>({
}));
// Enable auto-imports in template files
wxt.hooks.hook('ready', (wxt) => {
wxt.hook('config:resolved', (wxt) => {
if (!wxt.config.imports) return;
wxt.config.imports.addons ??= {};
+14
View File
@@ -0,0 +1,14 @@
# Changelog
## v1.0.0
Extracted `wxt/storage` into it's own package, `@wxt-dev/storage`!
It's still shipped inside WXT and accessible via `wxt/storage`, but now:
- Non-WXT projects can use the storage wrapper.
- We can make breaking changes to the API separately.
[Read the docs](https://wxt.dev/storage.html) for more details.
> This is apart of the v1.0 initiative for WXT.
+36
View File
@@ -0,0 +1,36 @@
# WXT Storage
[Changelog](https://github.com/wxt-dev/wxt/blob/main/packages/storage/CHANGELOG.md) &bull; [Docs](https://wxt.dev/storage.html)
A simplified wrapper around the extension storage APIs.
## Installation
### With WXT
This module is built-in to WXT, so you don't need to install anything.
```ts
import { storage } from 'wxt/storage';
```
If you use auto-imports, `storage` is auto-imported for you, so you don't even need to import it!
### Without WXT
Install the NPM package:
```sh
npm i @wxt-dev/storage
pnpm add @wxt-dev/storage
yarn add @wxt-dev/storage
bun add @wxt-dev/storage
```
```ts
import { storage } from '@wxt-dev/storage';
```
## Usage
Read full docs on the [documentation website](https://wxt.dev/storage.html).
+56
View File
@@ -0,0 +1,56 @@
{
"name": "@wxt-dev/storage",
"description": "Web extension storage API provided by WXT, supports all browsers.",
"version": "1.0.0",
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/wxt-dev/wxt.git",
"directory": "packages/storage"
},
"homepage": "https://wxt.dev/storage.html",
"keywords": [
"wxt",
"storage",
"extension",
"addon",
"chrome",
"firefox",
"edge"
],
"author": {
"name": "Aaron Klinker",
"email": "aaronklinker1+wxt@gmail.com"
},
"license": "MIT",
"scripts": {
"build": "buildc -- unbuild",
"check": "buildc --deps-only -- check",
"test": "buildc --deps-only -- vitest"
},
"dependencies": {
"async-mutex": "^0.5.0",
"dequal": "^2.0.3"
},
"devDependencies": {
"@aklinker1/check": "^1.4.5",
"@types/chrome": "^0.0.268",
"@webext-core/fake-browser": "^1.3.1",
"oxlint": "^0.9.9",
"publint": "^0.2.11",
"typescript": "^5.6.2",
"unbuild": "^2.0.0",
"vitest": "^2.0.0"
},
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs"
}
},
"files": [
"dist"
]
}
@@ -1,7 +1,6 @@
import { fakeBrowser } from '@webext-core/fake-browser';
import { describe, it, expect, beforeEach, vi, expectTypeOf } from 'vitest';
import { browser } from 'wxt/browser';
import { MigrationError, WxtStorageItem, storage } from '../storage';
import { MigrationError, type WxtStorageItem, storage } from '../index';
/**
* This works because fakeBrowser is synchronous, and is will finish any number of chained
@@ -223,7 +222,7 @@ describe('Storage Utils', () => {
describe('setMeta', () => {
it('should set metadata at key+$', async () => {
const existing = { v: 1 };
await browser.storage[storageArea].set({ count$: existing });
await chrome.storage[storageArea].set({ count$: existing });
const newValues = {
date: Date.now(),
};
@@ -239,7 +238,7 @@ describe('Storage Utils', () => {
'should remove any properties set to %s',
async (version) => {
const existing = { v: 1 };
await browser.storage[storageArea].set({ count$: existing });
await chrome.storage[storageArea].set({ count$: existing });
const expected = {};
await storage.setMeta(`${storageArea}:count`, { v: version });
@@ -1163,7 +1162,7 @@ describe('Storage Utils', () => {
await item.removeValue();
// Make sure it's actually blank before running the test
expect(await browser.storage.local.get()).toEqual({});
expect(await chrome.storage.local.get()).toEqual({});
init.mockClear();
const [value1, value2] = await Promise.all([
+871
View File
@@ -0,0 +1,871 @@
/// <reference types="chrome" />
/**
* Simplified storage APIs with support for versioned fields, snapshots, metadata, and item definitions.
*
* See [the guide](https://wxt.dev/storage.html) for more information.
* @module @wxt-dev/storage
*/
import { dequal } from 'dequal/lite';
import { Mutex } from 'async-mutex';
export const storage = createStorage();
function createStorage(): WxtStorage {
const drivers: Record<StorageArea, WxtStorageDriver> = {
local: createDriver('local'),
session: createDriver('session'),
sync: createDriver('sync'),
managed: createDriver('managed'),
};
const getDriver = (area: StorageArea) => {
const driver = drivers[area];
if (driver == null) {
const areaNames = Object.keys(drivers).join(', ');
throw Error(`Invalid area "${area}". Options: ${areaNames}`);
}
return driver;
};
const resolveKey = (key: StorageItemKey) => {
const deliminatorIndex = key.indexOf(':');
const driverArea = key.substring(0, deliminatorIndex) as StorageArea;
const driverKey = key.substring(deliminatorIndex + 1);
if (driverKey == null)
throw Error(
`Storage key should be in the form of "area:key", but received "${key}"`,
);
return {
driverArea,
driverKey,
driver: getDriver(driverArea),
};
};
const getMetaKey = (key: string) => key + '$';
const mergeMeta = (oldMeta: any, newMeta: any): any => {
const newFields = { ...oldMeta };
Object.entries(newMeta).forEach(([key, value]) => {
if (value == null) delete newFields[key];
else newFields[key] = value;
});
return newFields;
};
const getValueOrFallback = (value: any, fallback: any) =>
value ?? fallback ?? null;
const getMetaValue = (properties: any) =>
typeof properties === 'object' && !Array.isArray(properties)
? properties
: {};
const getItem = async (
driver: WxtStorageDriver,
driverKey: string,
opts: GetItemOptions<any> | undefined,
) => {
const res = await driver.getItem<any>(driverKey);
return getValueOrFallback(res, opts?.fallback ?? opts?.defaultValue);
};
const getMeta = async (driver: WxtStorageDriver, driverKey: string) => {
const metaKey = getMetaKey(driverKey);
const res = await driver.getItem<any>(metaKey);
return getMetaValue(res);
};
const setItem = async (
driver: WxtStorageDriver,
driverKey: string,
value: any,
) => {
await driver.setItem(driverKey, value ?? null);
};
const setMeta = async (
driver: WxtStorageDriver,
driverKey: string,
properties: any | undefined,
) => {
const metaKey = getMetaKey(driverKey);
const existingFields = getMetaValue(await driver.getItem(metaKey));
await driver.setItem(metaKey, mergeMeta(existingFields, properties));
};
const removeItem = async (
driver: WxtStorageDriver,
driverKey: string,
opts: RemoveItemOptions | undefined,
) => {
await driver.removeItem(driverKey);
if (opts?.removeMeta) {
const metaKey = getMetaKey(driverKey);
await driver.removeItem(metaKey);
}
};
const removeMeta = async (
driver: WxtStorageDriver,
driverKey: string,
properties: string | string[] | undefined,
) => {
const metaKey = getMetaKey(driverKey);
if (properties == null) {
await driver.removeItem(metaKey);
} else {
const newFields = getMetaValue(await driver.getItem(metaKey));
[properties].flat().forEach((field) => delete newFields[field]);
await driver.setItem(metaKey, newFields);
}
};
const watch = (
driver: WxtStorageDriver,
driverKey: string,
cb: WatchCallback<any>,
) => {
return driver.watch(driverKey, cb);
};
const storage: WxtStorage = {
getItem: async (key, opts) => {
const { driver, driverKey } = resolveKey(key);
return await getItem(driver, driverKey, opts);
},
getItems: async (keys) => {
const areaToKeyMap = new Map<StorageArea, string[]>();
const keyToOptsMap = new Map<string, GetItemOptions<any> | undefined>();
const orderedKeys: StorageItemKey[] = [];
keys.forEach((key) => {
let keyStr: StorageItemKey;
let opts: GetItemOptions<any> | undefined;
if (typeof key === 'string') {
// key: string
keyStr = key;
} else if ('getValue' in key) {
// key: WxtStorageItem
keyStr = key.key;
opts = { fallback: key.fallback };
} else {
// key: { key, options }
keyStr = key.key;
opts = key.options;
}
orderedKeys.push(keyStr);
const { driverArea, driverKey } = resolveKey(keyStr);
const areaKeys = areaToKeyMap.get(driverArea) ?? [];
areaToKeyMap.set(driverArea, areaKeys.concat(driverKey));
keyToOptsMap.set(keyStr, opts);
});
const resultsMap = new Map<StorageItemKey, any>();
await Promise.all(
Array.from(areaToKeyMap.entries()).map(async ([driverArea, keys]) => {
const driverResults = await drivers[driverArea].getItems(keys);
driverResults.forEach((driverResult) => {
const key = `${driverArea}:${driverResult.key}` as StorageItemKey;
const opts = keyToOptsMap.get(key);
const value = getValueOrFallback(
driverResult.value,
opts?.fallback ?? opts?.defaultValue,
);
resultsMap.set(key, value);
});
}),
);
return orderedKeys.map((key) => ({
key,
value: resultsMap.get(key),
}));
},
getMeta: async (key) => {
const { driver, driverKey } = resolveKey(key);
return await getMeta(driver, driverKey);
},
getMetas: async (args) => {
const keys = args.map((arg) => {
const key = typeof arg === 'string' ? arg : arg.key;
const { driverArea, driverKey } = resolveKey(key);
return {
key,
driverArea,
driverKey,
driverMetaKey: getMetaKey(driverKey),
};
});
const areaToDriverMetaKeysMap = keys.reduce<
Partial<Record<StorageArea, (typeof keys)[number][]>>
>((map, key) => {
map[key.driverArea] ??= [];
map[key.driverArea]!.push(key);
return map;
}, {});
const resultsMap: Record<string, any> = {};
await Promise.all(
Object.entries(areaToDriverMetaKeysMap).map(async ([area, keys]) => {
const areaRes = await chrome.storage[area as StorageArea].get(
keys.map((key) => key.driverMetaKey),
);
keys.forEach((key) => {
resultsMap[key.key] = areaRes[key.driverMetaKey] ?? {};
});
}),
);
return keys.map((key) => ({
key: key.key,
meta: resultsMap[key.key],
}));
},
setItem: async (key, value) => {
const { driver, driverKey } = resolveKey(key);
await setItem(driver, driverKey, value);
},
setItems: async (items) => {
const areaToKeyValueMap: Partial<
Record<StorageArea, Array<{ key: string; value: any }>>
> = {};
items.forEach((item) => {
const { driverArea, driverKey } = resolveKey(
'key' in item ? item.key : item.item.key,
);
areaToKeyValueMap[driverArea] ??= [];
areaToKeyValueMap[driverArea].push({
key: driverKey,
value: item.value,
});
});
await Promise.all(
Object.entries(areaToKeyValueMap).map(async ([driverArea, values]) => {
const driver = getDriver(driverArea as StorageArea);
await driver.setItems(values);
}),
);
},
setMeta: async (key, properties) => {
const { driver, driverKey } = resolveKey(key);
await setMeta(driver, driverKey, properties);
},
setMetas: async (items) => {
const areaToMetaUpdatesMap: Partial<
Record<StorageArea, { key: string; properties: any }[]>
> = {};
items.forEach((item) => {
const { driverArea, driverKey } = resolveKey(
'key' in item ? item.key : item.item.key,
);
areaToMetaUpdatesMap[driverArea] ??= [];
areaToMetaUpdatesMap[driverArea].push({
key: driverKey,
properties: item.meta,
});
});
await Promise.all(
Object.entries(areaToMetaUpdatesMap).map(
async ([storageArea, updates]) => {
const driver = getDriver(storageArea as StorageArea);
const metaKeys = updates.map(({ key }) => getMetaKey(key));
console.log(storageArea, metaKeys);
const existingMetas = await driver.getItems(metaKeys);
const existingMetaMap = Object.fromEntries(
existingMetas.map(({ key, value }) => [key, getMetaValue(value)]),
);
const metaUpdates = updates.map(({ key, properties }) => {
const metaKey = getMetaKey(key);
return {
key: metaKey,
value: mergeMeta(existingMetaMap[metaKey] ?? {}, properties),
};
});
await driver.setItems(metaUpdates);
},
),
);
},
removeItem: async (key, opts) => {
const { driver, driverKey } = resolveKey(key);
await removeItem(driver, driverKey, opts);
},
removeItems: async (keys) => {
const areaToKeysMap: Partial<Record<StorageArea, string[]>> = {};
keys.forEach((key) => {
let keyStr: StorageItemKey;
let opts: RemoveItemOptions | undefined;
if (typeof key === 'string') {
// key: string
keyStr = key;
} else if ('getValue' in key) {
// key: WxtStorageItem
keyStr = key.key;
} else if ('item' in key) {
// key: { item, options }
keyStr = key.item.key;
opts = key.options;
} else {
// key: { key, options }
keyStr = key.key;
opts = key.options;
}
const { driverArea, driverKey } = resolveKey(keyStr);
areaToKeysMap[driverArea] ??= [];
areaToKeysMap[driverArea].push(driverKey);
if (opts?.removeMeta) {
areaToKeysMap[driverArea].push(getMetaKey(driverKey));
}
});
await Promise.all(
Object.entries(areaToKeysMap).map(async ([driverArea, keys]) => {
const driver = getDriver(driverArea as StorageArea);
await driver.removeItems(keys);
}),
);
},
removeMeta: async (key, properties) => {
const { driver, driverKey } = resolveKey(key);
await removeMeta(driver, driverKey, properties);
},
snapshot: async (base, opts) => {
const driver = getDriver(base);
const data = await driver.snapshot();
opts?.excludeKeys?.forEach((key) => {
delete data[key];
delete data[getMetaKey(key)];
});
return data;
},
restoreSnapshot: async (base, data) => {
const driver = getDriver(base);
await driver.restoreSnapshot(data);
},
watch: (key, cb) => {
const { driver, driverKey } = resolveKey(key);
return watch(driver, driverKey, cb);
},
unwatch() {
Object.values(drivers).forEach((driver) => {
driver.unwatch();
});
},
defineItem: (key, opts?: WxtStorageItemOptions<any>) => {
const { driver, driverKey } = resolveKey(key);
const { version: targetVersion = 1, migrations = {} } = opts ?? {};
if (targetVersion < 1) {
throw Error(
'Storage item version cannot be less than 1. Initial versions should be set to 1, not 0.',
);
}
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;
if (currentVersion > targetVersion) {
throw Error(
`Version downgrade detected (v${currentVersion} -> v${targetVersion}) for "${key}"`,
);
}
console.debug(
`[@wxt-dev/storage] Running storage migration for ${key}: v${currentVersion} -> v${targetVersion}`,
);
const migrationsToRun = Array.from(
{ length: targetVersion - currentVersion },
(_, i) => currentVersion + i + 1,
);
let migratedValue = value;
for (const migrateToVersion of migrationsToRun) {
try {
migratedValue =
(await migrations?.[migrateToVersion]?.(migratedValue)) ??
migratedValue;
} catch (err) {
throw Error(`v${migrateToVersion} migration failed for "${key}"`, {
cause: err,
});
}
}
await driver.setItems([
{ key: driverKey, value: migratedValue },
{ key: driverMetaKey, value: { ...meta, v: targetVersion } },
]);
console.debug(
`[@wxt-dev/storage] Storage migration completed for ${key} v${targetVersion}`,
{ migratedValue },
);
};
const migrationsDone =
opts?.migrations == null
? Promise.resolve()
: migrate().catch((err) => {
console.error(
`[@wxt-dev/storage] Migration failed for ${key}`,
err,
);
});
const initMutex = new Mutex();
const getFallback = () => opts?.fallback ?? opts?.defaultValue ?? null;
const getOrInitValue = () =>
initMutex.runExclusive(async () => {
const value = await driver.getItem<any>(driverKey);
// Don't init value if it already exists or the init function isn't provided
if (value != null || opts?.init == null) return value;
const newValue = await opts.init();
await driver.setItem<any>(driverKey, newValue);
return newValue;
});
// Initialize the value once migrations have finished
migrationsDone.then(getOrInitValue);
return {
key,
get defaultValue() {
return getFallback();
},
get fallback() {
return getFallback();
},
getValue: async () => {
await migrationsDone;
if (opts?.init) {
return await getOrInitValue();
} else {
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 ?? getFallback(), oldValue ?? getFallback()),
),
migrate,
};
},
};
return storage;
}
function createDriver(storageArea: StorageArea): WxtStorageDriver {
const getStorageArea = () => {
if (chrome.runtime == null) {
throw Error(
[
"'wxt/storage' must be loaded in a web extension environment",
'\n - If thrown during a build, see https://github.com/wxt-dev/wxt/issues/371',
" - If thrown during tests, mock 'wxt/browser' correctly. See https://wxt.dev/guide/go-further/testing.html\n",
].join('\n'),
);
}
if (chrome.storage == null) {
throw Error(
"You must add the 'storage' permission to your manifest to use 'wxt/storage'",
);
}
const area = chrome.storage[storageArea];
if (area == null)
throw Error(`"chrome.storage.${storageArea}" is undefined`);
return area;
};
const watchListeners = new Set<(changes: StorageAreaChanges) => void>();
return {
getItem: async (key) => {
const res = await getStorageArea().get(key);
return res[key];
},
getItems: async (keys) => {
const result = await getStorageArea().get(keys);
return keys.map((key) => ({ key, value: result[key] ?? null }));
},
setItem: async (key, value) => {
if (value == null) {
await getStorageArea().remove(key);
} else {
await getStorageArea().set({ [key]: value });
}
},
setItems: async (values) => {
const map = values.reduce<Record<string, unknown>>(
(map, { key, value }) => {
map[key] = value;
return map;
},
{},
);
await getStorageArea().set(map);
},
removeItem: async (key) => {
await getStorageArea().remove(key);
},
removeItems: async (keys) => {
await getStorageArea().remove(keys);
},
snapshot: async () => {
return await getStorageArea().get();
},
restoreSnapshot: async (data) => {
await getStorageArea().set(data);
},
watch(key, cb) {
const listener = (changes: StorageAreaChanges) => {
const change = changes[key];
if (change == null) return;
if (dequal(change.newValue, change.oldValue)) return;
cb(change.newValue ?? null, change.oldValue ?? null);
};
getStorageArea().onChanged.addListener(listener);
watchListeners.add(listener);
return () => {
getStorageArea().onChanged.removeListener(listener);
watchListeners.delete(listener);
};
},
unwatch() {
watchListeners.forEach((listener) => {
getStorageArea().onChanged.removeListener(listener);
});
watchListeners.clear();
},
};
}
export interface WxtStorage {
/**
* Get an item from storage, or return `null` if it doesn't exist.
*
* @example
* await storage.getItem<number>("local:installDate");
*/
getItem<TValue>(
key: StorageItemKey,
opts: GetItemOptions<TValue> & { fallback: TValue },
): Promise<TValue>;
getItem<TValue>(
key: StorageItemKey,
opts?: GetItemOptions<TValue>,
): Promise<TValue | null>;
/**
* 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"]);
*/
getItems(
keys: Array<
| StorageItemKey
| WxtStorageItem<any, any>
| { key: StorageItemKey; options?: GetItemOptions<any> }
>,
): Promise<Array<{ key: StorageItemKey; value: any }>>;
/**
* Return an object containing metadata about the key. Object is stored at `key + "$"`. If value
* is not an object, it returns an empty object.
*
* @example
* await storage.getMeta("local:installDate");
*/
getMeta<T extends Record<string, unknown>>(key: StorageItemKey): Promise<T>;
/**
* Get the metadata of multiple storage items.
*
* @param items List of keys or items to get the metadata of.
* @returns An array containing storage keys and their metadata.
*/
getMetas(
keys: Array<StorageItemKey | WxtStorageItem<any, any>>,
): Promise<Array<{ key: StorageItemKey; meta: any }>>;
/**
* Set a value in storage. Setting a value to `null` or `undefined` is equivalent to calling
* `removeItem`.
*
* @example
* await storage.setItem<number>("local:installDate", Date.now());
*/
setItem<T>(key: StorageItemKey, value: T | null): Promise<void>;
/**
* Set multiple values in storage. If a value is set to `null` or `undefined`, the key is removed.
*
* @example
* await storage.setItem([
* { key: "local:installDate", value: Date.now() },
* { key: "session:someCounter, value: 5 },
* ]);
*/
setItems(
values: Array<
| { key: StorageItemKey; value: any }
| { item: WxtStorageItem<any, any>; value: any }
>,
): Promise<void>;
/**
* Sets metadata properties. If some properties are already set, but are not included in the
* `properties` parameter, they will not be removed.
*
* @example
* await storage.setMeta("local:installDate", { appVersion });
*/
setMeta<T extends Record<string, unknown>>(
key: StorageItemKey,
properties: T | null,
): Promise<void>;
/**
* Set the metadata of multiple storage items.
*
* @param items List of storage keys or items and metadata to set for each.
*/
setMetas(
metas: Array<
| { key: StorageItemKey; meta: Record<string, any> }
| { item: WxtStorageItem<any, any>; meta: Record<string, any> }
>,
): Promise<void>;
/**
* Removes an item from storage.
*
* @example
* await storage.removeItem("local:installDate");
*/
removeItem(key: StorageItemKey, opts?: RemoveItemOptions): Promise<void>;
/**
* Remove a list of keys from storage.
*/
removeItems(
keys: Array<
| StorageItemKey
| WxtStorageItem<any, any>
| { key: StorageItemKey; options?: RemoveItemOptions }
| { item: WxtStorageItem<any, any>; options?: RemoveItemOptions }
>,
): Promise<void>;
/**
* Remove the entire metadata for a key, or specific properties by name.
*
* @example
* // Remove all metadata properties from the item
* await storage.removeMeta("local:installDate");
*
* // Remove only specific the "v" field
* await storage.removeMeta("local:installDate", "v")
*/
removeMeta(
key: StorageItemKey,
properties?: string | string[],
): Promise<void>;
/**
* Return all the items in storage.
*/
snapshot(
base: StorageArea,
opts?: SnapshotOptions,
): 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 overridden.
*/
restoreSnapshot(base: StorageArea, data: any): Promise<void>;
/**
* Watch for changes to a specific key in storage.
*/
watch<T>(key: StorageItemKey, cb: WatchCallback<T | null>): Unwatch;
/**
* Remove all watch listeners.
*/
unwatch(): void;
/**
* Define a storage item with a default value, type, or versioning.
*
* Read full docs: https://wxt.dev/guide/extension-apis/storage.html#defining-storage-items
*/
defineItem<TValue, TMetadata extends Record<string, unknown> = {}>(
key: StorageItemKey,
): WxtStorageItem<TValue | null, TMetadata>;
defineItem<TValue, TMetadata extends Record<string, unknown> = {}>(
key: StorageItemKey,
options: WxtStorageItemOptions<TValue>,
): WxtStorageItem<TValue, TMetadata>;
}
interface WxtStorageDriver {
getItem<T>(key: string): Promise<T | null>;
getItems(keys: string[]): Promise<{ key: string; value: any }[]>;
setItem<T>(key: string, value: T | null): Promise<void>;
setItems(values: Array<{ key: string; value: any }>): Promise<void>;
removeItem(key: string): Promise<void>;
removeItems(keys: string[]): Promise<void>;
snapshot(): Promise<Record<string, unknown>>;
restoreSnapshot(data: Record<string, unknown>): Promise<void>;
watch<T>(key: string, cb: WatchCallback<T | null>): Unwatch;
unwatch(): void;
}
export interface WxtStorageItem<
TValue,
TMetadata extends Record<string, unknown>,
> {
/**
* The storage key passed when creating the storage item.
*/
key: StorageItemKey;
/**
* @deprecated Renamed to fallback, use it instead.
*/
defaultValue: TValue;
/**
* The value provided by the `fallback` option.
*/
fallback: TValue;
/**
* Get the latest value from storage.
*/
getValue(): Promise<TValue>;
/**
* Get metadata.
*/
getMeta(): Promise<NullablePartial<TMetadata>>;
/**
* Set the value in storage.
*/
setValue(value: TValue): Promise<void>;
/**
* Set metadata properties.
*/
setMeta(properties: NullablePartial<TMetadata>): Promise<void>;
/**
* Remove the value from storage.
*/
removeValue(opts?: RemoveItemOptions): Promise<void>;
/**
* Remove all metadata or certain properties from metadata.
*/
removeMeta(properties?: string[]): Promise<void>;
/**
* 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 type StorageArea = 'local' | 'session' | 'sync' | 'managed';
export type StorageItemKey = `${StorageArea}:${string}`;
export interface GetItemOptions<T> {
/**
* @deprecated Renamed to `fallback`, use it instead.
*/
defaultValue?: T;
/**
* Default value returned when `getItem` would otherwise return `null`.
*/
fallback?: T;
}
export interface RemoveItemOptions {
/**
* Optionally remove metadata when deleting a key.
*
* @default false
*/
removeMeta?: boolean;
}
export interface SnapshotOptions {
/**
* Exclude a list of keys. The storage area prefix should be removed since the snapshot is for a
* specific storage area already.
*/
excludeKeys?: string[];
}
export interface WxtStorageItemOptions<T> {
/**
* @deprecated Renamed to `fallback`, use it instead.
*/
defaultValue?: T;
/**
* Default value returned when `getValue` would otherwise return `null`.
*/
fallback?: T;
/**
* If passed, a value in storage will be initialized immediately after
* defining the storage item. This function returns the value that will be
* saved to storage during the initialization process if a value doesn't
* already exist.
*/
init?: () => T | Promise<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.
*/
version?: number;
/**
* A map of version numbers to the functions used to migrate the data to that version.
*/
migrations?: Record<number, (oldValue: any) => any>;
}
export type StorageAreaChanges = {
[key: string]: chrome.storage.StorageChange;
};
/**
* Same as `Partial`, but includes `| null`. It makes all the properties of an object optional and
* nullable.
*/
type NullablePartial<T> = {
[key in keyof T]+?: T[key] | undefined | null;
};
/**
* Callback called when a value in storage is changed.
*/
export type WatchCallback<T> = (newValue: T, oldValue: T) => void;
/**
* Call to remove a watch listener
*/
export type Unwatch = () => void;
export class MigrationError extends Error {
constructor(
public key: string,
public version: number,
options?: ErrorOptions,
) {
super(`v${version} migration failed for "${key}"`, options);
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"verbatimModuleSyntax": true,
"types": ["chrome"]
},
"exclude": ["node_modules/**", "dist/**"]
}
+9
View File
@@ -0,0 +1,9 @@
import { defineProject } from 'vitest/config';
export default defineProject({
test: {
mockReset: true,
restoreMocks: true,
setupFiles: ['vitest.setup.ts'],
},
});
+4
View File
@@ -0,0 +1,4 @@
import { fakeBrowser } from '@webext-core/fake-browser';
import { vi } from 'vitest';
vi.stubGlobal('chrome', fakeBrowser);
+1 -1
View File
@@ -28,7 +28,7 @@ export default defineWxtModule<UnoCSSOptions>({
}
wxt.hooks.hook('vite:devServer:extendConfig', (config) => {
config.plugins?.push(UnoCSS());
config.plugins?.push(UnoCSS(resolvedOptions.configOrPath));
});
wxt.hooks.hook('vite:build:extendConfig', async (entries, config) => {
@@ -14,12 +14,13 @@ export default defineBackground({
console.log(useAppConfig());
// @ts-expect-error: should only accept entrypoints or public assets
browser.runtime.getURL('/');
browser.runtime.getURL('/background.js');
browser.runtime.getURL('/icons/128.png');
browser.runtime.getURL('/example.html#hash');
browser.runtime.getURL('/example.html?query=param');
// @ts-expect-error: should only accept entrypoints or public assets
browser.runtime.getURL('/unknown');
// @ts-expect-error: should only allow hashes/query params on HTML files
browser.runtime.getURL('/icon-128.png?query=param');
+30
View File
@@ -1,5 +1,35 @@
# Changelog
## v0.19.16
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.15...wxt-v0.19.16)
### 🚀 Enhancements
- **hooks:** Add new `config:resolved` hook ([#1177](https://github.com/wxt-dev/wxt/pull/1177))
### ❤️ Contributors
- Aaron ([@aklinker1](http://github.com/aklinker1))
## v0.19.15
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.14...wxt-v0.19.15)
### 🚀 Enhancements
- Extract `wxt/storage` to its own package, `@wxt-dev/storage` ([#1129](https://github.com/wxt-dev/wxt/pull/1129))
### 🩹 Fixes
- Add "/" to `PublicPath` and `browser.runtime.getURL` ([#1171](https://github.com/wxt-dev/wxt/pull/1171))
- Add extension ID to event used to invalidate `ContentScriptContext` ([#1175](https://github.com/wxt-dev/wxt/pull/1175))
### ❤️ Contributors
- Aaron ([@aklinker1](http://github.com/aklinker1))
- Nishu ([@nishu-murmu](http://github.com/nishu-murmu))
## v0.19.14
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.13...wxt-v0.19.14)
+6
View File
@@ -4,6 +4,7 @@ import { WxtHooks } from '../../src/types';
const hooks: WxtHooks = {
ready: vi.fn(),
'config:resolved': vi.fn(),
'prepare:types': vi.fn(),
'prepare:publicPaths': vi.fn(),
'build:before': vi.fn(),
@@ -49,6 +50,7 @@ describe('Hooks', () => {
expectHooksToBeCalled({
ready: true,
'config:resolved': true,
'prepare:types': true,
'prepare:publicPaths': true,
'build:before': false,
@@ -76,6 +78,7 @@ describe('Hooks', () => {
expectHooksToBeCalled({
ready: true,
'config:resolved': true,
'prepare:types': true,
'prepare:publicPaths': true,
'build:before': true,
@@ -103,6 +106,7 @@ describe('Hooks', () => {
expectHooksToBeCalled({
ready: true,
'config:resolved': true,
'prepare:types': true,
'prepare:publicPaths': true,
'build:before': true,
@@ -130,6 +134,7 @@ describe('Hooks', () => {
expectHooksToBeCalled({
ready: true,
'config:resolved': true,
'prepare:types': true,
'prepare:publicPaths': true,
'build:before': true,
@@ -163,6 +168,7 @@ describe('Hooks', () => {
expectHooksToBeCalled({
ready: true,
'config:resolved': true,
'prepare:types': true,
'prepare:publicPaths': true,
'build:before': true,
@@ -48,6 +48,7 @@ describe('TypeScript Project', () => {
declare module "wxt/browser" {
export type PublicPath =
| "/"
| "/options.html"
| "/popup.html"
| "/sandbox.html"
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "wxt",
"type": "module",
"version": "0.19.14",
"version": "0.19.16",
"description": "Next gen framework for developing web extensions",
"repository": {
"type": "git",
@@ -87,6 +87,7 @@
"@webext-core/fake-browser": "^1.3.1",
"@webext-core/isolated-element": "^1.1.2",
"@webext-core/match-patterns": "^1.0.3",
"@wxt-dev/storage": "workspace:^1.0.0",
"async-mutex": "^0.5.0",
"c12": "^1.11.2",
"cac": "^6.7.14",
@@ -94,7 +95,6 @@
"ci-info": "^4.1.0",
"consola": "^3.2.3",
"defu": "^6.1.4",
"dequal": "^2.0.3",
"dotenv": "^16.4.5",
"esbuild": "^0.21.5",
"fast-glob": "^3.3.2",
+5 -5
View File
@@ -15,7 +15,7 @@ describe('Module Utilities', () => {
wxt.config.vite = () => Promise.resolve(userConfig);
addViteConfig(wxt, () => moduleConfig);
await wxt.hooks.callHook('ready', wxt);
await wxt.hooks.callHook('config:resolved', wxt);
const actual = await wxt.config.vite(wxt.config.env);
expect(actual).toEqual(expected);
@@ -31,7 +31,7 @@ describe('Module Utilities', () => {
wxt.config.vite = () => userConfig;
addViteConfig(wxt, () => moduleConfig);
await wxt.hooks.callHook('ready', wxt);
await wxt.hooks.callHook('config:resolved', wxt);
const actual = await wxt.config.vite(wxt.config.env);
expect(actual).toEqual(expected);
@@ -44,7 +44,7 @@ describe('Module Utilities', () => {
const wxt = fakeWxt({ hooks: createHooks() });
addImportPreset(wxt, preset);
await wxt.hooks.callHook('ready', wxt);
await wxt.hooks.callHook('config:resolved', wxt);
expect(wxt.config.imports && wxt.config.imports.presets).toContain(
preset,
@@ -63,7 +63,7 @@ describe('Module Utilities', () => {
});
addImportPreset(wxt, preset);
await wxt.hooks.callHook('ready', wxt);
await wxt.hooks.callHook('config:resolved', wxt);
expect(wxt.config.imports && wxt.config.imports.presets).toHaveLength(2);
});
@@ -78,7 +78,7 @@ describe('Module Utilities', () => {
});
addImportPreset(wxt, preset);
await wxt.hooks.callHook('ready', wxt);
await wxt.hooks.callHook('config:resolved', wxt);
expect(wxt.config.imports).toBe(false);
});
+4 -4
View File
@@ -18,7 +18,7 @@ export default defineWxtModule({
let unimport: Unimport;
// Add user module imports to config
wxt.hooks.hook('ready', () => {
wxt.hooks.hook('config:resolved', () => {
const addModuleImports = (module: WxtModule<any>) => {
if (!module.imports) return;
@@ -30,10 +30,10 @@ export default defineWxtModule({
wxt.config.userModules.forEach(addModuleImports);
});
// Create unimport instance AFTER "ready" so any modifications to the
// config inside "ready" are applied.
// Create unimport instance AFTER "config:resolved" so any modifications to the
// config inside "config:resolved" are applied.
wxt.hooks.afterEach((event) => {
if (event.name === 'ready') {
if (event.name === 'config:resolved') {
unimport = createUnimport(options);
}
});
@@ -35,7 +35,9 @@ import { createLocationWatcher } from './location-watcher';
* ```
*/
export class ContentScriptContext implements AbortController {
private static SCRIPT_STARTED_MESSAGE_TYPE = 'wxt:content-script-started';
private static SCRIPT_STARTED_MESSAGE_TYPE = getUniqueEventName(
'wxt:content-script-started',
);
private isTopFrame = window.self === window.top;
private abortController: AbortController;
+2 -1
View File
@@ -78,6 +78,7 @@ async function getPathsDeclarationEntry(
isHtmlEntrypoint(entry) ? '.html' : '.js',
),
)
.concat([''])
.concat(await getPublicFiles());
await wxt.hooks.callHook('prepare:publicPaths', wxt, paths);
@@ -176,7 +177,7 @@ declare module "wxt/browser" {
message.message,
),
),
// Include a final union-based override so TS accepts valid string templates or concatinations
// Include a final union-based override so TS accepts valid string templates or concatenations
// ie: browser.i18n.getMessage(`some_enum_${enumValue}`)
renderGetMessageOverload(
messages.map((message) => `"${message.name}"`).join(' | '),
+2
View File
@@ -47,6 +47,7 @@ export async function registerWxt(
},
async reloadConfig() {
wxt.config = await resolveConfig(inlineConfig, command);
await wxt.hooks.callHook('config:resolved', wxt);
},
pm,
builder,
@@ -82,6 +83,7 @@ export async function registerWxt(
}
await wxt.hooks.callHook('ready', wxt);
await wxt.hooks.callHook('config:resolved', wxt);
}
/**
+8 -7
View File
@@ -28,7 +28,7 @@ export function defineWxtModule<TOptions extends WxtModuleOptions>(
/**
* Adds a TS/JS file as an entrypoint to the project. This file will be bundled
* along with the other entrypoints.
*
* If you're publishing the module to NPM, you should probably pre-build the
* entrypoint and use `addPublicAssets` instead to copy pre-bundled assets into
* the output directory. This will speed up project builds since it just has to
@@ -53,7 +53,7 @@ export function defineWxtModule<TOptions extends WxtModuleOptions>(
* });
*/
export function addEntrypoint(wxt: Wxt, entrypoint: Entrypoint): void {
wxt.hooks.hook('entrypoints:resolved', (wxt, entrypoints) => {
wxt.hooks.hook('entrypoints:resolved', (_, entrypoints) => {
entrypoints.push(entrypoint);
});
}
@@ -106,7 +106,7 @@ export function addViteConfig(
wxt: Wxt,
viteConfig: (env: vite.ConfigEnv) => vite.UserConfig | undefined,
): void {
wxt.hooks.hook('ready', (wxt) => {
wxt.hooks.hook('config:resolved', (wxt) => {
const userVite = wxt.config.vite;
wxt.config.vite = async (env) => {
const fromUser = await userVite(env);
@@ -130,7 +130,7 @@ export function addViteConfig(
* });
*/
export function addWxtPlugin(wxt: Wxt, plugin: string): void {
wxt.hooks.hook('ready', (wxt) => {
wxt.hooks.hook('config:resolved', (wxt) => {
wxt.config.plugins.push(plugin);
});
}
@@ -166,7 +166,7 @@ export function addImportPreset(
wxt: Wxt,
preset: UnimportOptions['presets'][0],
): void {
wxt.hooks.hook('ready', (wxt) => {
wxt.hooks.hook('config:resolved', (wxt) => {
if (!wxt.config.imports) return;
wxt.config.imports.presets ??= [];
@@ -205,12 +205,13 @@ export function addImportPreset(
* });
*/
export function addAlias(wxt: Wxt, alias: string, path: string) {
wxt.hooks.hook('ready', (wxt) => {
wxt.hooks.hook('config:resolved', (wxt) => {
const target = resolve(wxt.config.root, path);
if (wxt.config.alias[alias] != null) {
if (wxt.config.alias[alias] != null && wxt.config.alias[alias] !== target) {
wxt.logger.warn(
`Skipped adding alias (${alias} => ${target}) because an alias with the same name already exists: ${alias} => ${wxt.config.alias[alias]}`,
);
return;
}
wxt.config.alias[alias] = target;
});
+2 -867
View File
@@ -1,869 +1,4 @@
/**
* Simplified storage APIs with support for versioned fields, snapshots, metadata, and item definitions.
*
* See [the guide](https://wxt.dev/guide/extension-apis/storage.html) for more information.
*
* @module wxt/storage
* @module @wxt-dev/storage
*/
import { Storage, browser } from 'wxt/browser';
import { dequal } from 'dequal/lite';
import { logger } from './sandbox/utils/logger';
import { toArray } from './core/utils/arrays';
import { Mutex } from 'async-mutex';
export const storage = createStorage();
function createStorage(): WxtStorage {
const drivers: Record<StorageArea, WxtStorageDriver> = {
local: createDriver('local'),
session: createDriver('session'),
sync: createDriver('sync'),
managed: createDriver('managed'),
};
const getDriver = (area: StorageArea) => {
const driver = drivers[area];
if (driver == null) {
const areaNames = Object.keys(drivers).join(', ');
throw Error(`Invalid area "${area}". Options: ${areaNames}`);
}
return driver;
};
const resolveKey = (key: StorageItemKey) => {
const deliminatorIndex = key.indexOf(':');
const driverArea = key.substring(0, deliminatorIndex) as StorageArea;
const driverKey = key.substring(deliminatorIndex + 1);
if (driverKey == null)
throw Error(
`Storage key should be in the form of "area:key", but received "${key}"`,
);
return {
driverArea,
driverKey,
driver: getDriver(driverArea),
};
};
const getMetaKey = (key: string) => key + '$';
const mergeMeta = (oldMeta: any, newMeta: any): any => {
const newFields = { ...oldMeta };
Object.entries(newMeta).forEach(([key, value]) => {
if (value == null) delete newFields[key];
else newFields[key] = value;
});
return newFields;
};
const getValueOrFallback = (value: any, fallback: any) =>
value ?? fallback ?? null;
const getMetaValue = (properties: any) =>
typeof properties === 'object' && !Array.isArray(properties)
? properties
: {};
const getItem = async (
driver: WxtStorageDriver,
driverKey: string,
opts: GetItemOptions<any> | undefined,
) => {
const res = await driver.getItem<any>(driverKey);
return getValueOrFallback(res, opts?.fallback ?? opts?.defaultValue);
};
const getMeta = async (driver: WxtStorageDriver, driverKey: string) => {
const metaKey = getMetaKey(driverKey);
const res = await driver.getItem<any>(metaKey);
return getMetaValue(res);
};
const setItem = async (
driver: WxtStorageDriver,
driverKey: string,
value: any,
) => {
await driver.setItem(driverKey, value ?? null);
};
const setMeta = async (
driver: WxtStorageDriver,
driverKey: string,
properties: any | undefined,
) => {
const metaKey = getMetaKey(driverKey);
const existingFields = getMetaValue(await driver.getItem(metaKey));
await driver.setItem(metaKey, mergeMeta(existingFields, properties));
};
const removeItem = async (
driver: WxtStorageDriver,
driverKey: string,
opts: RemoveItemOptions | undefined,
) => {
await driver.removeItem(driverKey);
if (opts?.removeMeta) {
const metaKey = getMetaKey(driverKey);
await driver.removeItem(metaKey);
}
};
const removeMeta = async (
driver: WxtStorageDriver,
driverKey: string,
properties: string | string[] | undefined,
) => {
const metaKey = getMetaKey(driverKey);
if (properties == null) {
await driver.removeItem(metaKey);
} else {
const newFields = getMetaValue(await driver.getItem(metaKey));
toArray(properties).forEach((field) => delete newFields[field]);
await driver.setItem(metaKey, newFields);
}
};
const watch = (
driver: WxtStorageDriver,
driverKey: string,
cb: WatchCallback<any>,
) => {
return driver.watch(driverKey, cb);
};
const storage: WxtStorage = {
getItem: async (key, opts) => {
const { driver, driverKey } = resolveKey(key);
return await getItem(driver, driverKey, opts);
},
getItems: async (keys) => {
const areaToKeyMap = new Map<StorageArea, string[]>();
const keyToOptsMap = new Map<string, GetItemOptions<any> | undefined>();
const orderedKeys: StorageItemKey[] = [];
keys.forEach((key) => {
let keyStr: StorageItemKey;
let opts: GetItemOptions<any> | undefined;
if (typeof key === 'string') {
// key: string
keyStr = key;
} else if ('getValue' in key) {
// key: WxtStorageItem
keyStr = key.key;
opts = { fallback: key.fallback };
} else {
// key: { key, options }
keyStr = key.key;
opts = key.options;
}
orderedKeys.push(keyStr);
const { driverArea, driverKey } = resolveKey(keyStr);
const areaKeys = areaToKeyMap.get(driverArea) ?? [];
areaToKeyMap.set(driverArea, areaKeys.concat(driverKey));
keyToOptsMap.set(keyStr, opts);
});
const resultsMap = new Map<StorageItemKey, any>();
await Promise.all(
Array.from(areaToKeyMap.entries()).map(async ([driverArea, keys]) => {
const driverResults = await drivers[driverArea].getItems(keys);
driverResults.forEach((driverResult) => {
const key = `${driverArea}:${driverResult.key}` as StorageItemKey;
const opts = keyToOptsMap.get(key);
const value = getValueOrFallback(
driverResult.value,
opts?.fallback ?? opts?.defaultValue,
);
resultsMap.set(key, value);
});
}),
);
return orderedKeys.map((key) => ({
key,
value: resultsMap.get(key),
}));
},
getMeta: async (key) => {
const { driver, driverKey } = resolveKey(key);
return await getMeta(driver, driverKey);
},
getMetas: async (args) => {
const keys = args.map((arg) => {
const key = typeof arg === 'string' ? arg : arg.key;
const { driverArea, driverKey } = resolveKey(key);
return {
key,
driverArea,
driverKey,
driverMetaKey: getMetaKey(driverKey),
};
});
const areaToDriverMetaKeysMap = keys.reduce<
Partial<Record<StorageArea, (typeof keys)[number][]>>
>((map, key) => {
map[key.driverArea] ??= [];
map[key.driverArea]!.push(key);
return map;
}, {});
const resultsMap: Record<string, any> = {};
await Promise.all(
Object.entries(areaToDriverMetaKeysMap).map(async ([area, keys]) => {
const areaRes = await browser.storage[area as StorageArea].get(
keys.map((key) => key.driverMetaKey),
);
keys.forEach((key) => {
resultsMap[key.key] = areaRes[key.driverMetaKey] ?? {};
});
}),
);
return keys.map((key) => ({
key: key.key,
meta: resultsMap[key.key],
}));
},
setItem: async (key, value) => {
const { driver, driverKey } = resolveKey(key);
await setItem(driver, driverKey, value);
},
setItems: async (items) => {
const areaToKeyValueMap: Partial<
Record<StorageArea, Array<{ key: string; value: any }>>
> = {};
items.forEach((item) => {
const { driverArea, driverKey } = resolveKey(
'key' in item ? item.key : item.item.key,
);
areaToKeyValueMap[driverArea] ??= [];
areaToKeyValueMap[driverArea].push({
key: driverKey,
value: item.value,
});
});
await Promise.all(
Object.entries(areaToKeyValueMap).map(async ([driverArea, values]) => {
const driver = getDriver(driverArea as StorageArea);
await driver.setItems(values);
}),
);
},
setMeta: async (key, properties) => {
const { driver, driverKey } = resolveKey(key);
await setMeta(driver, driverKey, properties);
},
setMetas: async (items) => {
const areaToMetaUpdatesMap: Partial<
Record<StorageArea, { key: string; properties: any }[]>
> = {};
items.forEach((item) => {
const { driverArea, driverKey } = resolveKey(
'key' in item ? item.key : item.item.key,
);
areaToMetaUpdatesMap[driverArea] ??= [];
areaToMetaUpdatesMap[driverArea].push({
key: driverKey,
properties: item.meta,
});
});
await Promise.all(
Object.entries(areaToMetaUpdatesMap).map(
async ([storageArea, updates]) => {
const driver = getDriver(storageArea as StorageArea);
const metaKeys = updates.map(({ key }) => getMetaKey(key));
console.log(storageArea, metaKeys);
const existingMetas = await driver.getItems(metaKeys);
const existingMetaMap = Object.fromEntries(
existingMetas.map(({ key, value }) => [key, getMetaValue(value)]),
);
const metaUpdates = updates.map(({ key, properties }) => {
const metaKey = getMetaKey(key);
return {
key: metaKey,
value: mergeMeta(existingMetaMap[metaKey] ?? {}, properties),
};
});
await driver.setItems(metaUpdates);
},
),
);
},
removeItem: async (key, opts) => {
const { driver, driverKey } = resolveKey(key);
await removeItem(driver, driverKey, opts);
},
removeItems: async (keys) => {
const areaToKeysMap: Partial<Record<StorageArea, string[]>> = {};
keys.forEach((key) => {
let keyStr: StorageItemKey;
let opts: RemoveItemOptions | undefined;
if (typeof key === 'string') {
// key: string
keyStr = key;
} else if ('getValue' in key) {
// key: WxtStorageItem
keyStr = key.key;
} else if ('item' in key) {
// key: { item, options }
keyStr = key.item.key;
opts = key.options;
} else {
// key: { key, options }
keyStr = key.key;
opts = key.options;
}
const { driverArea, driverKey } = resolveKey(keyStr);
areaToKeysMap[driverArea] ??= [];
areaToKeysMap[driverArea].push(driverKey);
if (opts?.removeMeta) {
areaToKeysMap[driverArea].push(getMetaKey(driverKey));
}
});
await Promise.all(
Object.entries(areaToKeysMap).map(async ([driverArea, keys]) => {
const driver = getDriver(driverArea as StorageArea);
await driver.removeItems(keys);
}),
);
},
removeMeta: async (key, properties) => {
const { driver, driverKey } = resolveKey(key);
await removeMeta(driver, driverKey, properties);
},
snapshot: async (base, opts) => {
const driver = getDriver(base);
const data = await driver.snapshot();
opts?.excludeKeys?.forEach((key) => {
delete data[key];
delete data[getMetaKey(key)];
});
return data;
},
restoreSnapshot: async (base, data) => {
const driver = getDriver(base);
await driver.restoreSnapshot(data);
},
watch: (key, cb) => {
const { driver, driverKey } = resolveKey(key);
return watch(driver, driverKey, cb);
},
unwatch() {
Object.values(drivers).forEach((driver) => {
driver.unwatch();
});
},
defineItem: (key, opts?: WxtStorageItemOptions<any>) => {
const { driver, driverKey } = resolveKey(key);
const { version: targetVersion = 1, migrations = {} } = opts ?? {};
if (targetVersion < 1) {
throw Error(
'Storage item version cannot be less than 1. Initial versions should be set to 1, not 0.',
);
}
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;
if (currentVersion > targetVersion) {
throw Error(
`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,
);
let migratedValue = value;
for (const migrateToVersion of migrationsToRun) {
try {
migratedValue =
(await migrations?.[migrateToVersion]?.(migratedValue)) ??
migratedValue;
} catch (err) {
throw Error(`v${migrateToVersion} migration failed for "${key}"`, {
cause: err,
});
}
}
await driver.setItems([
{ key: driverKey, value: migratedValue },
{ key: driverMetaKey, value: { ...meta, v: targetVersion } },
]);
logger.debug(
`Storage migration completed for ${key} v${targetVersion}`,
{ migratedValue },
);
};
const migrationsDone =
opts?.migrations == null
? Promise.resolve()
: migrate().catch((err) => {
logger.error(`Migration failed for ${key}`, err);
});
const initMutex = new Mutex();
const getFallback = () => opts?.fallback ?? opts?.defaultValue ?? null;
const getOrInitValue = () =>
initMutex.runExclusive(async () => {
const value = await driver.getItem<any>(driverKey);
// Don't init value if it already exists or the init function isn't provided
if (value != null || opts?.init == null) return value;
const newValue = await opts.init();
await driver.setItem<any>(driverKey, newValue);
return newValue;
});
// Initialize the value once migrations have finished
migrationsDone.then(getOrInitValue);
return {
key,
get defaultValue() {
return getFallback();
},
get fallback() {
return getFallback();
},
getValue: async () => {
await migrationsDone;
if (opts?.init) {
return await getOrInitValue();
} else {
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 ?? getFallback(), oldValue ?? getFallback()),
),
migrate,
};
},
};
return storage;
}
function createDriver(storageArea: StorageArea): WxtStorageDriver {
const getStorageArea = () => {
if (browser.runtime == null) {
throw Error(
[
"'wxt/storage' must be loaded in a web extension environment",
'\n - If thrown during a build, see https://github.com/wxt-dev/wxt/issues/371',
" - If thrown during tests, mock 'wxt/browser' correctly. See https://wxt.dev/guide/go-further/testing.html\n",
].join('\n'),
);
}
if (browser.storage == null) {
throw Error(
"You must add the 'storage' permission to your manifest to use 'wxt/storage'",
);
}
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
>();
return {
getItem: async (key) => {
const res = await getStorageArea().get(key);
return res[key] as any;
},
getItems: async (keys) => {
const result = await getStorageArea().get(keys);
return keys.map((key) => ({ key, value: result[key] ?? null }));
},
setItem: async (key, value) => {
if (value == null) {
await getStorageArea().remove(key);
} else {
await getStorageArea().set({ [key]: value });
}
},
setItems: async (values) => {
const map = values.reduce<Record<string, unknown>>(
(map, { key, value }) => {
map[key] = value;
return map;
},
{},
);
await getStorageArea().set(map);
},
removeItem: async (key) => {
await getStorageArea().remove(key);
},
removeItems: async (keys) => {
await getStorageArea().remove(keys);
},
snapshot: async () => {
return await getStorageArea().get();
},
restoreSnapshot: async (data) => {
await getStorageArea().set(data);
},
watch(key, cb) {
const listener = (changes: Storage.StorageAreaOnChangedChangesType) => {
const change = changes[key] as { newValue: any; oldValue: any };
if (change == null) return;
if (dequal(change.newValue, change.oldValue)) return;
cb(change.newValue ?? null, change.oldValue ?? null);
};
getStorageArea().onChanged.addListener(listener);
watchListeners.add(listener);
return () => {
getStorageArea().onChanged.removeListener(listener);
watchListeners.delete(listener);
};
},
unwatch() {
watchListeners.forEach((listener) => {
getStorageArea().onChanged.removeListener(listener);
});
watchListeners.clear();
},
};
}
export interface WxtStorage {
/**
* Get an item from storage, or return `null` if it doesn't exist.
*
* @example
* await storage.getItem<number>("local:installDate");
*/
getItem<TValue>(
key: StorageItemKey,
opts: GetItemOptions<TValue> & { fallback: TValue },
): Promise<TValue>;
getItem<TValue>(
key: StorageItemKey,
opts?: GetItemOptions<TValue>,
): Promise<TValue | null>;
/**
* 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"]);
*/
getItems(
keys: Array<
| StorageItemKey
| WxtStorageItem<any, any>
| { key: StorageItemKey; options?: GetItemOptions<any> }
>,
): Promise<Array<{ key: StorageItemKey; value: any }>>;
/**
* Return an object containing metadata about the key. Object is stored at `key + "$"`. If value
* is not an object, it returns an empty object.
*
* @example
* await storage.getMeta("local:installDate");
*/
getMeta<T extends Record<string, unknown>>(key: StorageItemKey): Promise<T>;
/**
* Get the metadata of multiple storage items.
*
* @param items List of keys or items to get the metadata of.
* @returns An array containing storage keys and their metadata.
*/
getMetas(
keys: Array<StorageItemKey | WxtStorageItem<any, any>>,
): Promise<Array<{ key: StorageItemKey; meta: any }>>;
/**
* Set a value in storage. Setting a value to `null` or `undefined` is equivalent to calling
* `removeItem`.
*
* @example
* await storage.setItem<number>("local:installDate", Date.now());
*/
setItem<T>(key: StorageItemKey, value: T | null): Promise<void>;
/**
* Set multiple values in storage. If a value is set to `null` or `undefined`, the key is removed.
*
* @example
* await storage.setItem([
* { key: "local:installDate", value: Date.now() },
* { key: "session:someCounter, value: 5 },
* ]);
*/
setItems(
values: Array<
| { key: StorageItemKey; value: any }
| { item: WxtStorageItem<any, any>; value: any }
>,
): Promise<void>;
/**
* Sets metadata properties. If some properties are already set, but are not included in the
* `properties` parameter, they will not be removed.
*
* @example
* await storage.setMeta("local:installDate", { appVersion });
*/
setMeta<T extends Record<string, unknown>>(
key: StorageItemKey,
properties: T | null,
): Promise<void>;
/**
* Set the metadata of multiple storage items.
*
* @param items List of storage keys or items and metadata to set for each.
*/
setMetas(
metas: Array<
| { key: StorageItemKey; meta: Record<string, any> }
| { item: WxtStorageItem<any, any>; meta: Record<string, any> }
>,
): Promise<void>;
/**
* Removes an item from storage.
*
* @example
* await storage.removeItem("local:installDate");
*/
removeItem(key: StorageItemKey, opts?: RemoveItemOptions): Promise<void>;
/**
* Remove a list of keys from storage.
*/
removeItems(
keys: Array<
| StorageItemKey
| WxtStorageItem<any, any>
| { key: StorageItemKey; options?: RemoveItemOptions }
| { item: WxtStorageItem<any, any>; options?: RemoveItemOptions }
>,
): Promise<void>;
/**
* Remove the entire metadata for a key, or specific properties by name.
*
* @example
* // Remove all metadata properties from the item
* await storage.removeMeta("local:installDate");
*
* // Remove only specific the "v" field
* await storage.removeMeta("local:installDate", "v")
*/
removeMeta(
key: StorageItemKey,
properties?: string | string[],
): Promise<void>;
/**
* Return all the items in storage.
*/
snapshot(
base: StorageArea,
opts?: SnapshotOptions,
): 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 overridden.
*/
restoreSnapshot(base: StorageArea, data: any): Promise<void>;
/**
* Watch for changes to a specific key in storage.
*/
watch<T>(key: StorageItemKey, cb: WatchCallback<T | null>): Unwatch;
/**
* Remove all watch listeners.
*/
unwatch(): void;
/**
* Define a storage item with a default value, type, or versioning.
*
* Read full docs: https://wxt.dev/guide/extension-apis/storage.html#defining-storage-items
*/
defineItem<TValue, TMetadata extends Record<string, unknown> = {}>(
key: StorageItemKey,
): WxtStorageItem<TValue | null, TMetadata>;
defineItem<TValue, TMetadata extends Record<string, unknown> = {}>(
key: StorageItemKey,
options: WxtStorageItemOptions<TValue>,
): WxtStorageItem<TValue, TMetadata>;
}
interface WxtStorageDriver {
getItem<T>(key: string): Promise<T | null>;
getItems(keys: string[]): Promise<{ key: string; value: any }[]>;
setItem<T>(key: string, value: T | null): Promise<void>;
setItems(values: Array<{ key: string; value: any }>): Promise<void>;
removeItem(key: string): Promise<void>;
removeItems(keys: string[]): Promise<void>;
snapshot(): Promise<Record<string, unknown>>;
restoreSnapshot(data: Record<string, unknown>): Promise<void>;
watch<T>(key: string, cb: WatchCallback<T | null>): Unwatch;
unwatch(): void;
}
export interface WxtStorageItem<
TValue,
TMetadata extends Record<string, unknown>,
> {
/**
* The storage key passed when creating the storage item.
*/
key: StorageItemKey;
/**
* @deprecated Renamed to fallback, use it instead.
*/
defaultValue: TValue;
/**
* The value provided by the `fallback` option.
*/
fallback: TValue;
/**
* Get the latest value from storage.
*/
getValue(): Promise<TValue>;
/**
* Get metadata.
*/
getMeta(): Promise<NullablePartial<TMetadata>>;
/**
* Set the value in storage.
*/
setValue(value: TValue): Promise<void>;
/**
* Set metadata properties.
*/
setMeta(properties: NullablePartial<TMetadata>): Promise<void>;
/**
* Remove the value from storage.
*/
removeValue(opts?: RemoveItemOptions): Promise<void>;
/**
* Remove all metadata or certain properties from metadata.
*/
removeMeta(properties?: string[]): Promise<void>;
/**
* 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 type StorageArea = 'local' | 'session' | 'sync' | 'managed';
export type StorageItemKey = `${StorageArea}:${string}`;
export interface GetItemOptions<T> {
/**
* @deprecated Renamed to `fallback`, use it instead.
*/
defaultValue?: T;
/**
* Default value returned when `getItem` would otherwise return `null`.
*/
fallback?: T;
}
export interface RemoveItemOptions {
/**
* Optionally remove metadata when deleting a key.
*
* @default false
*/
removeMeta?: boolean;
}
export interface SnapshotOptions {
/**
* Exclude a list of keys. The storage area prefix should be removed since the snapshot is for a
* specific storage area already.
*/
excludeKeys?: string[];
}
export interface WxtStorageItemOptions<T> {
/**
* @deprecated Renamed to `fallback`, use it instead.
*/
defaultValue?: T;
/**
* Default value returned when `getValue` would otherwise return `null`.
*/
fallback?: T;
/**
* If passed, a value in storage will be initialized immediately after
* defining the storage item. This function returns the value that will be
* saved to storage during the initialization process if a value doesn't
* already exist.
*/
init?: () => T | Promise<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.
*/
version?: number;
/**
* A map of version numbers to the functions used to migrate the data to that version.
*/
migrations?: Record<number, (oldValue: any) => any>;
}
/**
* Same as `Partial`, but includes `| null`. It makes all the properties of an object optional and
* nullable.
*/
export type NullablePartial<T> = {
[key in keyof T]+?: T[key] | undefined | null;
};
/**
* Callback called when a value in storage is changed.
*/
export type WatchCallback<T> = (newValue: T, oldValue: T) => void;
/**
* Call to remove a watch listener
*/
export type Unwatch = () => void;
export class MigrationError extends Error {
constructor(
public key: string,
public version: number,
options?: ErrorOptions,
) {
super(`v${version} migration failed for "${key}"`, options);
}
}
export * from '@wxt-dev/storage';
+6 -2
View File
@@ -1100,11 +1100,15 @@ export type HookResult = Promise<void> | void;
export interface WxtHooks {
/**
* Called after WXT initialization, when the WXT instance is ready to work.
* Called only one time after WXT initialization, when the WXT instance is ready to work.
* @param wxt The configured WXT object
* @returns Promise
*/
ready: (wxt: Wxt) => HookResult;
/**
* Called whenever config is loaded or reloaded. Use this hook to modify config by modifying `wxt.config`.
* @param wxt The configured WXT object
*/
'config:resolved': (wxt: Wxt) => HookResult;
/**
* Called before WXT writes .wxt/tsconfig.json and .wxt/wxt.d.ts, allowing
* addition of custom references and declarations in wxt.d.ts, or directly
+2 -2
View File
@@ -6,7 +6,7 @@
"src/browser/index.ts",
"src/browser/chrome.ts",
"src/index.ts",
"src/storage.ts",
"src/modules.ts"
"src/modules.ts",
"src/storage.ts"
]
}
+125 -3
View File
@@ -260,6 +260,40 @@ importers:
specifier: workspace:*
version: link:../wxt
packages/storage:
dependencies:
async-mutex:
specifier: ^0.5.0
version: 0.5.0
dequal:
specifier: ^2.0.3
version: 2.0.3
devDependencies:
'@aklinker1/check':
specifier: ^1.4.5
version: 1.4.5(typescript@5.6.3)
'@types/chrome':
specifier: ^0.0.268
version: 0.0.268
'@webext-core/fake-browser':
specifier: ^1.3.1
version: 1.3.1
oxlint:
specifier: ^0.9.9
version: 0.9.10
publint:
specifier: ^0.2.11
version: 0.2.12
typescript:
specifier: ^5.6.2
version: 5.6.3
unbuild:
specifier: ^2.0.0
version: 2.0.0(sass@1.80.7)(typescript@5.6.3)
vitest:
specifier: ^2.0.0
version: 2.1.4(@types/node@20.17.6)(happy-dom@15.11.4)(sass@1.80.7)
packages/unocss:
dependencies:
defu:
@@ -311,6 +345,9 @@ importers:
'@webext-core/match-patterns':
specifier: ^1.0.3
version: 1.0.3
'@wxt-dev/storage':
specifier: workspace:^1.0.0
version: link:../storage
async-mutex:
specifier: ^0.5.0
version: 0.5.0
@@ -332,9 +369,6 @@ importers:
defu:
specifier: ^6.1.4
version: 6.1.4
dequal:
specifier: ^2.0.3
version: 2.0.3
dotenv:
specifier: ^16.4.5
version: 16.4.5
@@ -1455,41 +1489,81 @@ packages:
cpu: [arm64]
os: [darwin]
'@oxlint/darwin-arm64@0.9.10':
resolution: {integrity: sha512-eOXKZYq5bnCSgDefgM5bzAg+4Fc//Rc4yjgKN8iDWUARweCaChiQXb6TXX8MfEfs6qayEMy6yVj0pqoFz0B1aw==}
cpu: [arm64]
os: [darwin]
'@oxlint/darwin-x64@0.11.1':
resolution: {integrity: sha512-LPuF0D8uu30KIVEeVuGwIPwHwJRQ1i1otwFFH7tRsNXPgMgZJ4VgriyH22i6RWwBtclJoCSBLtGK6gLZ0oZBvw==}
cpu: [x64]
os: [darwin]
'@oxlint/darwin-x64@0.9.10':
resolution: {integrity: sha512-UeYICDvLUaUOcY+0ugZUEmBMRLP+x8iTgL7TeY6BlpGw2ahbtUOTbyIIRWtr/0O++TnjZ+v8TzhJ9crw6Ij6dg==}
cpu: [x64]
os: [darwin]
'@oxlint/linux-arm64-gnu@0.11.1':
resolution: {integrity: sha512-CYBE+GRIPs5e+raD2pdicuBn6Y6E1xAnyWQ/kHE4GEWDAQZY0Um2VYEUTGH2ObwJ3uXr6jeJ16HOKJvr0S8a8w==}
cpu: [arm64]
os: [linux]
'@oxlint/linux-arm64-gnu@0.9.10':
resolution: {integrity: sha512-0Zn+vqHhrZyufFBfq9WOgiIool0gCR14BLsdS+0Dwd9o+kNxPGA5q7erQFkiC4rpkxtfBHeD3iIKMMt7d29Kyw==}
cpu: [arm64]
os: [linux]
'@oxlint/linux-arm64-musl@0.11.1':
resolution: {integrity: sha512-iYXF5N5Gv+lc2wt90kxXy/W0cn7IEWu3UPzewIjPGDH8ajDckvGzZx6pTGYJnTyMh7U6hUKwOBFPVLMWI7UwKQ==}
cpu: [arm64]
os: [linux]
'@oxlint/linux-arm64-musl@0.9.10':
resolution: {integrity: sha512-tkQcWpYwF42bA/uRaV2iMFePHkBjTTgomOgeEaiw6XOSJX4nBEqGIIboqqLBWT4JnKCf/L+IG3y/e1MflhKByw==}
cpu: [arm64]
os: [linux]
'@oxlint/linux-x64-gnu@0.11.1':
resolution: {integrity: sha512-D0tT8X0CsK/bpdkGdLSmsGftG3VndjyAUJuNGt56JYn0UfuPDkhQcLgUlkANHzNRXJ84tLQKhpf/MUDUHPB5cg==}
cpu: [x64]
os: [linux]
'@oxlint/linux-x64-gnu@0.9.10':
resolution: {integrity: sha512-JHbkMUnibqaSMBvLHyqTL5cWxcGW+jw+Ppt2baLISpvo34a6fBR+PI7v/A92sEDWe0W1rPhypzCwA8mKpkQ3DA==}
cpu: [x64]
os: [linux]
'@oxlint/linux-x64-musl@0.11.1':
resolution: {integrity: sha512-WekaLYk8WLT7Di8+nyPvtqs9OlMoO6KjFDMlqqLDWQTk9ffjn8e76PCRigF3w39jQ70qP3c8k8cNKNw5ROuFcg==}
cpu: [x64]
os: [linux]
'@oxlint/linux-x64-musl@0.9.10':
resolution: {integrity: sha512-aBBwN7bQzidwHwEXr7BAdVvMTLWstCy5gikerjLnGDeCSXX9r+o6+yUzTOqZvOo66E+XBgOJaVbY8rsL1MLE0g==}
cpu: [x64]
os: [linux]
'@oxlint/win32-arm64@0.11.1':
resolution: {integrity: sha512-/CN/bFtI33vB8uemOkZxlNRf6Q7CftP2pSO7a6Q2niG4NC99YRPj7ctXcPF0jGR0NQUhGZk7ajM4G/0MKcRdag==}
cpu: [arm64]
os: [win32]
'@oxlint/win32-arm64@0.9.10':
resolution: {integrity: sha512-LXDnk7vKHT3IY6G1jq0O7+XMhtcHOYuxLGIx4KP+4xS6vKgBY+Bsq4xV3AtmtKlvnXkP5FxHpfLmcEtm5AWysA==}
cpu: [arm64]
os: [win32]
'@oxlint/win32-x64@0.11.1':
resolution: {integrity: sha512-0hLl0z6adYTvLIOPC5uyo+EAwNITkzi4AY4xImykQW8H89GhiV9Xl8MPJeZQMWSz7ajI1I2+hRsvA0QAzeBsxA==}
cpu: [x64]
os: [win32]
'@oxlint/win32-x64@0.9.10':
resolution: {integrity: sha512-w5XRAV4bhgwenjjpGYZGglqzG9Wv/sI+cjQWJBQsvfDXsr2w4vOBXzt1j3/Z3EcSqf4KtkCa/IIuAhQyeShUbA==}
cpu: [x64]
os: [win32]
'@parcel/watcher-android-arm64@2.5.0':
resolution: {integrity: sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==}
engines: {node: '>= 10.0.0'}
@@ -1800,6 +1874,9 @@ packages:
'@types/babel__traverse@7.20.6':
resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==}
'@types/chrome@0.0.268':
resolution: {integrity: sha512-7N1QH9buudSJ7sI8Pe4mBHJr5oZ48s0hcanI9w3wgijAlv1OZNUZve9JR4x42dn5lJ5Sm87V1JNfnoh10EnQlA==}
'@types/chrome@0.0.280':
resolution: {integrity: sha512-AotSmZrL9bcZDDmSI1D9dE7PGbhOur5L0cKxXd7IqbVizQWCY4gcvupPUVsQ4FfDj3V2tt/iOpomT9EY0s+w1g==}
@@ -3790,6 +3867,11 @@ packages:
engines: {node: '>=14.*'}
hasBin: true
oxlint@0.9.10:
resolution: {integrity: sha512-bKiiFN7Hnoaist/rditTRBXz+GXKYuLd53/NB7Q6zHB/bifELJarSoRLkAUGElIJKl4PSr3lTh1g6zehh+rX0g==}
engines: {node: '>=14.*'}
hasBin: true
p-cancelable@3.0.0:
resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==}
engines: {node: '>=12.20'}
@@ -5840,27 +5922,51 @@ snapshots:
'@oxlint/darwin-arm64@0.11.1':
optional: true
'@oxlint/darwin-arm64@0.9.10':
optional: true
'@oxlint/darwin-x64@0.11.1':
optional: true
'@oxlint/darwin-x64@0.9.10':
optional: true
'@oxlint/linux-arm64-gnu@0.11.1':
optional: true
'@oxlint/linux-arm64-gnu@0.9.10':
optional: true
'@oxlint/linux-arm64-musl@0.11.1':
optional: true
'@oxlint/linux-arm64-musl@0.9.10':
optional: true
'@oxlint/linux-x64-gnu@0.11.1':
optional: true
'@oxlint/linux-x64-gnu@0.9.10':
optional: true
'@oxlint/linux-x64-musl@0.11.1':
optional: true
'@oxlint/linux-x64-musl@0.9.10':
optional: true
'@oxlint/win32-arm64@0.11.1':
optional: true
'@oxlint/win32-arm64@0.9.10':
optional: true
'@oxlint/win32-x64@0.11.1':
optional: true
'@oxlint/win32-x64@0.9.10':
optional: true
'@parcel/watcher-android-arm64@2.5.0':
optional: true
@@ -6134,6 +6240,11 @@ snapshots:
dependencies:
'@babel/types': 7.25.6
'@types/chrome@0.0.268':
dependencies:
'@types/filesystem': 0.0.36
'@types/har-format': 1.2.15
'@types/chrome@0.0.280':
dependencies:
'@types/filesystem': 0.0.36
@@ -8350,6 +8461,17 @@ snapshots:
'@oxlint/win32-arm64': 0.11.1
'@oxlint/win32-x64': 0.11.1
oxlint@0.9.10:
optionalDependencies:
'@oxlint/darwin-arm64': 0.9.10
'@oxlint/darwin-x64': 0.9.10
'@oxlint/linux-arm64-gnu': 0.9.10
'@oxlint/linux-arm64-musl': 0.9.10
'@oxlint/linux-x64-gnu': 0.9.10
'@oxlint/linux-x64-musl': 0.9.10
'@oxlint/win32-arm64': 0.9.10
'@oxlint/win32-x64': 0.9.10
p-cancelable@3.0.0: {}
package-json-from-dist@1.0.0: {}