diff --git a/demo/src/entrypoints/background.ts b/demo/src/entrypoints/background.ts
index 0f2b80be..9590abdf 100644
--- a/demo/src/entrypoints/background.ts
+++ b/demo/src/entrypoints/background.ts
@@ -24,4 +24,6 @@ export default defineBackground(() => {
browser.i18n.getMessage('@@extension_id');
console.log('WXT MODE:', import.meta.env.MODE);
+
+ storage.setItem('session:startTime', Date.now());
});
diff --git a/demo/wxt.config.ts b/demo/wxt.config.ts
index 81de7525..8df3dc92 100644
--- a/demo/wxt.config.ts
+++ b/demo/wxt.config.ts
@@ -3,6 +3,7 @@ import { defineConfig } from 'wxt';
export default defineConfig({
srcDir: 'src',
manifest: {
+ permissions: ['storage'],
default_locale: 'en',
web_accessible_resources: [
{
diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index eece29f1..a8eb88e4 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -90,6 +90,7 @@ export default defineConfig({
{ text: 'Auto-imports', link: '/guide/auto-imports.md' },
{ text: 'Manifest.json', link: '/guide/manifest.md' },
{ text: 'Extension APIs', link: '/guide/extension-apis.md' },
+ { text: 'Storage', link: '/guide/storage.md' },
{ text: 'Remote Code', link: '/guide/remote-code.md' },
{ text: 'Development', link: '/guide/development.md' },
{ text: 'Testing', link: '/guide/testing.md' },
diff --git a/docs/guide/compare.md b/docs/guide/compare.md
index 385130d4..2746e464 100644
--- a/docs/guide/compare.md
+++ b/docs/guide/compare.md
@@ -27,7 +27,7 @@ Lets compare the features of WXT vs [Plasmo](https://docs.plasmo.com/framework),
| Reload Content Scripts on Change | ✅ | 🟡 Reloads entire extension |
| Reload Background on Change | 🟡 Reloads entire extension | 🟡 Reloads entire extension |
| Built-in Utils | | |
-| Storage | 🟡 Coming soon | ✅ |
+| Storage | ✅ | ✅ |
| Messaging | 🟡 Coming soon | ✅ |
| Content Script UI | ✅ | ✅ |
diff --git a/docs/guide/storage.md b/docs/guide/storage.md
new file mode 100644
index 00000000..65a0976a
--- /dev/null
+++ b/docs/guide/storage.md
@@ -0,0 +1,36 @@
+# Storage API
+
+WXT's storage API is powered by `unstorage`. See [their docs](https://unstorage.unjs.io/usage#usage-1) for more details.
+
+## Overview
+
+:::code-group
+
+```ts [native]
+const { installDate } = await browser.storage.local.get('installDate');
+await browser.storage.local.set({ key: 'value' });
+```
+
+```ts [wxt/browser]
+const installDate = await storage.get('local:installDate');
+await storage.setItem('key', 'value');
+```
+
+:::
+
+Use the `"local:"`, `"session:"`, `"sync:"`, and `"managed:"` prefixes to specify which storage area to use.
+
+## Customization
+
+WXT also provides a driver for `unstorage`. To customize the `storage` object's setup, like removing the prefixes and using a single storage area, you can create your own storage:
+
+```ts
+// storage.ts
+export default createStorage({
+ driver: webExtensionDriver({ storageArea: 'local' }),
+});
+```
+
+:::note
+`wxt/browser` re-exports all of `unstorage`, which is where `createStorage` comes from.
+:::
diff --git a/docs/typedoc.json b/docs/typedoc.json
index d39d1f3b..52cabaa6 100644
--- a/docs/typedoc.json
+++ b/docs/typedoc.json
@@ -4,6 +4,7 @@
"../src/client",
"../src/browser.ts",
"../src/sandbox",
+ "../src/storage.ts",
"../src/testing"
],
"plugin": ["typedoc-plugin-markdown", "typedoc-vitepress-theme"],
diff --git a/e2e/tests/auto-imports.test.ts b/e2e/tests/auto-imports.test.ts
index aeb192ab..40b74b5a 100644
--- a/e2e/tests/auto-imports.test.ts
+++ b/e2e/tests/auto-imports.test.ts
@@ -18,13 +18,24 @@ describe('Auto Imports', () => {
declare global {
const ContentScriptContext: typeof import('wxt/client')['ContentScriptContext']
const browser: typeof import('wxt/browser')['browser']
+ const builtinDrivers: typeof import('wxt/storage')['builtinDrivers']
const createContentScriptIframe: typeof import('wxt/client')['createContentScriptIframe']
const createContentScriptUi: typeof import('wxt/client')['createContentScriptUi']
+ const createStorage: typeof import('wxt/storage')['createStorage']
const defineBackground: typeof import('wxt/client')['defineBackground']
const defineConfig: typeof import('wxt')['defineConfig']
const defineContentScript: typeof import('wxt/client')['defineContentScript']
+ const defineDriver: typeof import('wxt/storage')['defineDriver']
const defineUnlistedScript: typeof import('wxt/sandbox')['defineUnlistedScript']
const fakeBrowser: typeof import('wxt/testing')['fakeBrowser']
+ const joinKeys: typeof import('wxt/storage')['joinKeys']
+ const normalizeBaseKey: typeof import('wxt/storage')['normalizeBaseKey']
+ const normalizeKey: typeof import('wxt/storage')['normalizeKey']
+ const prefixStorage: typeof import('wxt/storage')['prefixStorage']
+ const restoreSnapshot: typeof import('wxt/storage')['restoreSnapshot']
+ const snapshot: typeof import('wxt/storage')['snapshot']
+ const storage: typeof import('wxt/storage')['storage']
+ const webExtensionDriver: typeof import('wxt/storage')['webExtensionDriver']
}
"
`);
diff --git a/package.json b/package.json
index d74f3870..028e1642 100644
--- a/package.json
+++ b/package.json
@@ -56,6 +56,11 @@
"require": "./dist/testing.cjs",
"import": "./dist/testing.js",
"types": "./dist/testing.d.ts"
+ },
+ "./storage": {
+ "require": "./dist/storage.cjs",
+ "import": "./dist/storage.js",
+ "types": "./dist/storage.d.ts"
}
},
"scripts": {
@@ -76,7 +81,7 @@
},
"dependencies": {
"@types/webextension-polyfill": "^0.10.5",
- "@webext-core/fake-browser": "^1.2.2",
+ "@webext-core/fake-browser": "^1.3.1",
"@webext-core/isolated-element": "^1.0.4",
"@webext-core/match-patterns": "^1.0.2",
"async-mutex": "^0.4.0",
@@ -100,6 +105,7 @@
"prompts": "^2.4.2",
"rollup-plugin-visualizer": "^5.9.2",
"unimport": "^3.4.0",
+ "unstorage": "^1.9.0",
"vite": "^4.0.0 || ^5.0.0-0",
"web-ext-run": "^0.1.0",
"webextension-polyfill": "^0.10.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8766f91b..3960db24 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -12,8 +12,8 @@ importers:
specifier: ^0.10.5
version: 0.10.5
'@webext-core/fake-browser':
- specifier: ^1.2.2
- version: 1.2.2
+ specifier: ^1.3.1
+ version: 1.3.1
'@webext-core/isolated-element':
specifier: ^1.0.4
version: 1.0.4
@@ -83,6 +83,9 @@ importers:
unimport:
specifier: ^3.4.0
version: 3.4.0
+ unstorage:
+ specifier: ^1.9.0
+ version: 1.9.0
vite:
specifier: ^4.0.0 || ^5.0.0-0
version: 4.5.0(@types/node@20.8.10)(sass@1.69.5)
@@ -850,6 +853,10 @@ packages:
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0, npm: '>=6.14.13'}
dev: true
+ /@ioredis/commands@1.2.0:
+ resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==}
+ dev: false
+
/@istanbuljs/schema@0.1.3:
resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==}
engines: {node: '>=8'}
@@ -912,6 +919,148 @@ packages:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.15.0
+ /@parcel/watcher-android-arm64@2.3.0:
+ resolution: {integrity: sha512-f4o9eA3dgk0XRT3XhB0UWpWpLnKgrh1IwNJKJ7UJek7eTYccQ8LR7XUWFKqw6aEq5KUNlCcGvSzKqSX/vtWVVA==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm64]
+ os: [android]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@parcel/watcher-darwin-arm64@2.3.0:
+ resolution: {integrity: sha512-mKY+oijI4ahBMc/GygVGvEdOq0L4DxhYgwQqYAz/7yPzuGi79oXrZG52WdpGA1wLBPrYb0T8uBaGFo7I6rvSKw==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@parcel/watcher-darwin-x64@2.3.0:
+ resolution: {integrity: sha512-20oBj8LcEOnLE3mgpy6zuOq8AplPu9NcSSSfyVKgfOhNAc4eF4ob3ldj0xWjGGbOF7Dcy1Tvm6ytvgdjlfUeow==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [x64]
+ os: [darwin]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@parcel/watcher-freebsd-x64@2.3.0:
+ resolution: {integrity: sha512-7LftKlaHunueAEiojhCn+Ef2CTXWsLgTl4hq0pkhkTBFI3ssj2bJXmH2L67mKpiAD5dz66JYk4zS66qzdnIOgw==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@parcel/watcher-linux-arm-glibc@2.3.0:
+ resolution: {integrity: sha512-1apPw5cD2xBv1XIHPUlq0cO6iAaEUQ3BcY0ysSyD9Kuyw4MoWm1DV+W9mneWI+1g6OeP6dhikiFE6BlU+AToTQ==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm]
+ os: [linux]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@parcel/watcher-linux-arm64-glibc@2.3.0:
+ resolution: {integrity: sha512-mQ0gBSQEiq1k/MMkgcSB0Ic47UORZBmWoAWlMrTW6nbAGoLZP+h7AtUM7H3oDu34TBFFvjy4JCGP43JlylkTQA==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@parcel/watcher-linux-arm64-musl@2.3.0:
+ resolution: {integrity: sha512-LXZAExpepJew0Gp8ZkJ+xDZaTQjLHv48h0p0Vw2VMFQ8A+RKrAvpFuPVCVwKJCr5SE+zvaG+Etg56qXvTDIedw==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@parcel/watcher-linux-x64-glibc@2.3.0:
+ resolution: {integrity: sha512-P7Wo91lKSeSgMTtG7CnBS6WrA5otr1K7shhSjKHNePVmfBHDoAOHYRXgUmhiNfbcGk0uMCHVcdbfxtuiZCHVow==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [x64]
+ os: [linux]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@parcel/watcher-linux-x64-musl@2.3.0:
+ resolution: {integrity: sha512-+kiRE1JIq8QdxzwoYY+wzBs9YbJ34guBweTK8nlzLKimn5EQ2b2FSC+tAOpq302BuIMjyuUGvBiUhEcLIGMQ5g==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [x64]
+ os: [linux]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@parcel/watcher-wasm@2.3.0:
+ resolution: {integrity: sha512-ejBAX8H0ZGsD8lSICDNyMbSEtPMWgDL0WFCt/0z7hyf5v8Imz4rAM8xY379mBsECkq/Wdqa5WEDLqtjZ+6NxfA==}
+ engines: {node: '>= 10.0.0'}
+ dependencies:
+ is-glob: 4.0.3
+ micromatch: 4.0.5
+ napi-wasm: 1.1.0
+ dev: false
+ bundledDependencies:
+ - napi-wasm
+
+ /@parcel/watcher-win32-arm64@2.3.0:
+ resolution: {integrity: sha512-35gXCnaz1AqIXpG42evcoP2+sNL62gZTMZne3IackM+6QlfMcJLy3DrjuL6Iks7Czpd3j4xRBzez3ADCj1l7Aw==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm64]
+ os: [win32]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@parcel/watcher-win32-ia32@2.3.0:
+ resolution: {integrity: sha512-FJS/IBQHhRpZ6PiCjFt1UAcPr0YmCLHRbTc00IBTrelEjlmmgIVLeOx4MSXzx2HFEy5Jo5YdhGpxCuqCyDJ5ow==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [ia32]
+ os: [win32]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@parcel/watcher-win32-x64@2.3.0:
+ resolution: {integrity: sha512-dLx+0XRdMnVI62kU3wbXvbIRhLck4aE28bIGKbRGS7BJNt54IIj9+c/Dkqb+7DJEbHUZAX1bwaoM8PqVlHJmCA==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [x64]
+ os: [win32]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@parcel/watcher@2.3.0:
+ resolution: {integrity: sha512-pW7QaFiL11O0BphO+bq3MgqeX/INAk9jgBldVDYjlQPO4VddoZnF22TcF9onMhnLVHuNqBJeRf+Fj7eezi/+rQ==}
+ engines: {node: '>= 10.0.0'}
+ dependencies:
+ detect-libc: 1.0.3
+ is-glob: 4.0.3
+ micromatch: 4.0.5
+ node-addon-api: 7.0.0
+ optionalDependencies:
+ '@parcel/watcher-android-arm64': 2.3.0
+ '@parcel/watcher-darwin-arm64': 2.3.0
+ '@parcel/watcher-darwin-x64': 2.3.0
+ '@parcel/watcher-freebsd-x64': 2.3.0
+ '@parcel/watcher-linux-arm-glibc': 2.3.0
+ '@parcel/watcher-linux-arm64-glibc': 2.3.0
+ '@parcel/watcher-linux-arm64-musl': 2.3.0
+ '@parcel/watcher-linux-x64-glibc': 2.3.0
+ '@parcel/watcher-linux-x64-musl': 2.3.0
+ '@parcel/watcher-win32-arm64': 2.3.0
+ '@parcel/watcher-win32-ia32': 2.3.0
+ '@parcel/watcher-win32-x64': 2.3.0
+ dev: false
+
/@pnpm/config.env-replace@1.1.0:
resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==}
engines: {node: '>=12.22.0'}
@@ -1296,8 +1445,8 @@ packages:
resolution: {integrity: sha512-loGD63sacRzOzSJgQnB9ZAhaQGkN7wl2Zuw7tsphI5Isa0irijrRo6EnJii/GgjGefIFO8AIO7UivzRhFaEk9w==}
dev: false
- /@webext-core/fake-browser@1.2.2:
- resolution: {integrity: sha512-3w0lU0gAYODoMklGQfBuGwIxR+lakYK8FONRFUg8aEdpHUMMzbf5jPwRaTrnsgqupE0JAoBwk8vF6san/BtDeQ==}
+ /@webext-core/fake-browser@1.3.1:
+ resolution: {integrity: sha512-NpBl0rXL6rT3msdl9Fb1GPLd/MKJEZ3pHpxuMdlu+qKW78T6SWJqDvyAVs8VjAmYs9RHoQJc+yObxQoGWdskXQ==}
dependencies:
lodash.merge: 4.6.2
dev: false
@@ -1411,6 +1560,10 @@ packages:
normalize-path: 3.0.0
picomatch: 2.3.1
+ /arch@2.2.0:
+ resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==}
+ dev: false
+
/array-buffer-byte-length@1.0.0:
resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==}
dependencies:
@@ -1663,6 +1816,12 @@ packages:
engines: {node: '>=8'}
dev: false
+ /citty@0.1.4:
+ resolution: {integrity: sha512-Q3bK1huLxzQrvj7hImJ7Z1vKYJRPQCDnd0EjXfHMidcjecGOMuLrmuQmtWmFkuKLcMThlGh1yCKG8IEc6VeNXQ==}
+ dependencies:
+ consola: 3.2.3
+ dev: false
+
/cli-boxes@3.0.0:
resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==}
engines: {node: '>=10'}
@@ -1687,6 +1846,15 @@ packages:
string-width: 5.1.2
dev: true
+ /clipboardy@3.0.0:
+ resolution: {integrity: sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ dependencies:
+ arch: 2.2.0
+ execa: 5.1.1
+ is-wsl: 2.2.0
+ dev: false
+
/cliui@8.0.1:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
@@ -1696,6 +1864,11 @@ packages:
wrap-ansi: 7.0.0
dev: false
+ /cluster-key-slot@1.1.2:
+ resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
+ engines: {node: '>=0.10.0'}
+ dev: false
+
/color-convert@1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
dependencies:
@@ -1780,6 +1953,10 @@ packages:
resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==}
dev: true
+ /cookie-es@1.0.0:
+ resolution: {integrity: sha512-mWYvfOLrfEc996hlKcdABeIiPHUPC6DM2QYZdGGOvhOTbA3tjm2eBwqlJpoFdjC89NI4Qt6h0Pu06Mp+1Pj5OQ==}
+ dev: false
+
/core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
dev: false
@@ -1802,7 +1979,6 @@ packages:
path-key: 3.1.1
shebang-command: 2.0.0
which: 2.0.2
- dev: true
/crypto-random-string@4.0.0:
resolution: {integrity: sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==}
@@ -1903,10 +2079,21 @@ packages:
resolution: {integrity: sha512-Vy2wmG3NTkmHNg/kzpuvHhkqeIx3ODWqasgCRbKtbXEN0G+HpEEv9BtJLp7ZG1CZloFaC41Ah3ZFbq7aqCqMeQ==}
dev: false
+ /denque@2.1.0:
+ resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
+ engines: {node: '>=0.10'}
+ dev: false
+
/destr@2.0.2:
resolution: {integrity: sha512-65AlobnZMiCET00KaFFjUefxDX0khFA/E4myqZ7a6Sq1yZtR8+FVIvilVX66vF2uobSumxooYZChiRPCKNqhmg==}
dev: false
+ /detect-libc@1.0.3:
+ resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
+ engines: {node: '>=0.10'}
+ hasBin: true
+ dev: false
+
/diff-sequences@29.6.3:
resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -2154,7 +2341,6 @@ packages:
onetime: 5.1.2
signal-exit: 3.0.7
strip-final-newline: 2.0.0
- dev: true
/execa@8.0.1:
resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
@@ -2313,6 +2499,10 @@ packages:
has-symbols: 1.0.3
dev: true
+ /get-port-please@3.1.1:
+ resolution: {integrity: sha512-3UBAyM3u4ZBVYDsxOQfJDxEa6XTbpBDrOjp4mf7ExFRt5BKs/QywQQiJsh2B+hxcZLSapWqCRvElUe8DnKcFHA==}
+ dev: false
+
/get-port@7.0.0:
resolution: {integrity: sha512-mDHFgApoQd+azgMdwylJrv2DX47ywGq1i5VFJE7fZ0dttNq3iQMfsU4IvEgBHojA3KqEudyu7Vq+oN8kNaNkWw==}
engines: {node: '>=16'}
@@ -2451,6 +2641,19 @@ packages:
resolution: {integrity: sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==}
dev: false
+ /h3@1.8.2:
+ resolution: {integrity: sha512-1Ca0orJJlCaiFY68BvzQtP2lKLk46kcLAxVM8JgYbtm2cUg6IY7pjpYgWMwUvDO9QI30N5JAukOKoT8KD3Q0PQ==}
+ dependencies:
+ cookie-es: 1.0.0
+ defu: 6.1.3
+ destr: 2.0.2
+ iron-webcrypto: 0.10.1
+ radix3: 1.1.0
+ ufo: 1.3.1
+ uncrypto: 0.1.3
+ unenv: 1.7.4
+ dev: false
+
/happy-dom@12.10.3:
resolution: {integrity: sha512-JzUXOh0wdNGY54oKng5hliuBkq/+aT1V3YpTM+lrN/GoLQTANZsMaIvmHiHe612rauHvPJnDZkZ+5GZR++1Abg==}
dependencies:
@@ -2534,6 +2737,11 @@ packages:
resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==}
dev: false
+ /http-shutdown@1.2.2:
+ resolution: {integrity: sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw==}
+ engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
+ dev: false
+
/http2-wrapper@2.2.0:
resolution: {integrity: sha512-kZB0wxMo0sh1PehyjJUWRFEd99KC5TLjZ2cULC4f9iqJBAmKQQXEICjxl5iPJRwP40dpeHFqqhm7tYCvODpqpQ==}
engines: {node: '>=10.19.0'}
@@ -2555,7 +2763,6 @@ packages:
/human-signals@2.1.0:
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
engines: {node: '>=10.17.0'}
- dev: true
/human-signals@5.0.0:
resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
@@ -2625,6 +2832,27 @@ packages:
side-channel: 1.0.4
dev: true
+ /ioredis@5.3.2:
+ resolution: {integrity: sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==}
+ engines: {node: '>=12.22.0'}
+ dependencies:
+ '@ioredis/commands': 1.2.0
+ cluster-key-slot: 1.1.2
+ debug: 4.3.4
+ denque: 2.1.0
+ lodash.defaults: 4.2.0
+ lodash.isarguments: 3.1.0
+ redis-errors: 1.2.0
+ redis-parser: 3.0.0
+ standard-as-callback: 2.1.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
+ /iron-webcrypto@0.10.1:
+ resolution: {integrity: sha512-QGOS8MRMnj/UiOa+aMIgfyHcvkhqNUsUxb1XzskENvbo+rEfp6TOwqd1KPuDzXC4OnGHcMSVxDGRoilqB8ViqA==}
+ dev: false
+
/is-absolute@0.1.7:
resolution: {integrity: sha512-Xi9/ZSn4NFapG8RP98iNPMOeaV3mXPisxKxzKtHVqr3g56j/fBn+yZmnxSVAA8lmZbl2J9b/a4kJvfU3hqQYgA==}
engines: {node: '>=0.10.0'}
@@ -2794,7 +3022,6 @@ packages:
/is-stream@2.0.1:
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
engines: {node: '>=8'}
- dev: true
/is-stream@3.0.0:
resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
@@ -3032,6 +3259,29 @@ packages:
- supports-color
dev: true
+ /listhen@1.5.5:
+ resolution: {integrity: sha512-LXe8Xlyh3gnxdv4tSjTjscD1vpr/2PRpzq8YIaMJgyKzRG8wdISlWVWnGThJfHnlJ6hmLt2wq1yeeix0TEbuoA==}
+ hasBin: true
+ dependencies:
+ '@parcel/watcher': 2.3.0
+ '@parcel/watcher-wasm': 2.3.0
+ citty: 0.1.4
+ clipboardy: 3.0.0
+ consola: 3.2.3
+ defu: 6.1.3
+ get-port-please: 3.1.1
+ h3: 1.8.2
+ http-shutdown: 1.2.2
+ jiti: 1.21.0
+ mlly: 1.4.2
+ node-forge: 1.3.1
+ pathe: 1.1.1
+ std-env: 3.4.3
+ ufo: 1.3.1
+ untun: 0.1.2
+ uqr: 0.1.2
+ dev: false
+
/listr2@7.0.2:
resolution: {integrity: sha512-rJysbR9GKIalhTbVL2tYbF2hVyDnrf7pFUZBwjPaMIdadYHmeT+EVi/Bu3qd7ETQPahTotg2WRCatXwRBW554g==}
engines: {node: '>=16.0.0'}
@@ -3063,6 +3313,14 @@ packages:
resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==}
engines: {node: '>=14'}
+ /lodash.defaults@4.2.0:
+ resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
+ dev: false
+
+ /lodash.isarguments@3.1.0:
+ resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
+ dev: false
+
/lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
@@ -3106,6 +3364,13 @@ packages:
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dev: false
+ /lru-cache@10.0.2:
+ resolution: {integrity: sha512-Yj9mA8fPiVgOUpByoTZO5pNrcl5Yk37FcSHsUINpAsaBIEZIuqcCclDZJCVxqQShDsmYX8QG63svJiTbOATZwg==}
+ engines: {node: 14 || >=16.14}
+ dependencies:
+ semver: 7.5.4
+ dev: false
+
/lru-cache@6.0.0:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'}
@@ -3154,7 +3419,6 @@ packages:
/merge-stream@2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
- dev: true
/merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
@@ -3167,6 +3431,12 @@ packages:
braces: 3.0.2
picomatch: 2.3.1
+ /mime@3.0.0:
+ resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
+ engines: {node: '>=10.0.0'}
+ hasBin: true
+ dev: false
+
/mimic-fn@2.1.0:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
@@ -3303,6 +3573,10 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
+ /napi-wasm@1.1.0:
+ resolution: {integrity: sha512-lHwIAJbmLSjF9VDRm9GoVOy9AGp3aIvkjv+Kvz9h16QR3uSVYH78PNQUnT2U4X53mhlnV2M7wrhibQ3GHicDmg==}
+ dev: false
+
/ncp@2.0.0:
resolution: {integrity: sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==}
hasBin: true
@@ -3313,6 +3587,10 @@ packages:
resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==}
dev: true
+ /node-addon-api@7.0.0:
+ resolution: {integrity: sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==}
+ dev: false
+
/node-fetch-native@1.4.1:
resolution: {integrity: sha512-NsXBU0UgBxo2rQLOeWNZqS3fvflWePMECr8CoSWoSTqCqGbVVsvl9vZu1HfQicYN0g5piV9Gh8RTEvo/uP752w==}
dev: false
@@ -3372,7 +3650,6 @@ packages:
engines: {node: '>=8'}
dependencies:
path-key: 3.1.1
- dev: true
/npm-run-path@5.1.0:
resolution: {integrity: sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==}
@@ -3410,6 +3687,14 @@ packages:
object-keys: 1.1.1
dev: true
+ /ofetch@1.3.3:
+ resolution: {integrity: sha512-s1ZCMmQWXy4b5K/TW9i/DtiN8Ku+xCiHcjQ6/J/nDdssirrQNOoB165Zu8EqLMA2lln1JUth9a0aW9Ap2ctrUg==}
+ dependencies:
+ destr: 2.0.2
+ node-fetch-native: 1.4.1
+ ufo: 1.3.1
+ dev: false
+
/ohash@1.1.3:
resolution: {integrity: sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==}
dev: false
@@ -3522,7 +3807,6 @@ packages:
/path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
- dev: true
/path-key@4.0.0:
resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==}
@@ -3676,6 +3960,10 @@ packages:
engines: {node: '>=10'}
dev: false
+ /radix3@1.1.0:
+ resolution: {integrity: sha512-pNsHDxbGORSvuSScqNJ+3Km6QAVqk8CfsCBIEoDgpqLrkD2f3QM4I7d1ozJJ172OmIcoUcerZaNWqtLkRXTV3A==}
+ dev: false
+
/rc9@2.1.1:
resolution: {integrity: sha512-lNeOl38Ws0eNxpO3+wD1I9rkHGQyj1NU1jlzv4go2CtEnEQEUfqnIvZG7W+bC/aXdJ27n5x/yUjb6RoT9tko+Q==}
dependencies:
@@ -3750,6 +4038,18 @@ packages:
dependencies:
picomatch: 2.3.1
+ /redis-errors@1.2.0:
+ resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==}
+ engines: {node: '>=4'}
+ dev: false
+
+ /redis-parser@3.0.0:
+ resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==}
+ engines: {node: '>=4'}
+ dependencies:
+ redis-errors: 1.2.0
+ dev: false
+
/regenerator-runtime@0.14.0:
resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==}
dev: false
@@ -3948,7 +4248,6 @@ packages:
engines: {node: '>=8'}
dependencies:
shebang-regex: 3.0.0
- dev: true
/shebang-regex@1.0.0:
resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==}
@@ -3958,7 +4257,6 @@ packages:
/shebang-regex@3.0.0:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
- dev: true
/shell-quote@1.7.3:
resolution: {integrity: sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==}
@@ -4088,9 +4386,17 @@ packages:
/stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
+ /standard-as-callback@2.1.0:
+ resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
+ dev: false
+
/std-env@3.3.3:
resolution: {integrity: sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg==}
+ /std-env@3.4.3:
+ resolution: {integrity: sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==}
+ dev: false
+
/stdin-discarder@0.1.0:
resolution: {integrity: sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@@ -4195,7 +4501,6 @@ packages:
/strip-final-newline@2.0.0:
resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
engines: {node: '>=6'}
- dev: true
/strip-final-newline@3.0.0:
resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==}
@@ -4480,9 +4785,23 @@ packages:
which-boxed-primitive: 1.0.2
dev: true
+ /uncrypto@0.1.3:
+ resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==}
+ dev: false
+
/undici-types@5.26.5:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
+ /unenv@1.7.4:
+ resolution: {integrity: sha512-fjYsXYi30It0YCQYqLOcT6fHfMXsBr2hw9XC7ycf8rTG7Xxpe3ZssiqUnD0khrjiZEmkBXWLwm42yCSCH46fMw==}
+ dependencies:
+ consola: 3.2.3
+ defu: 6.1.3
+ mime: 3.0.0
+ node-fetch-native: 1.4.1
+ pathe: 1.1.1
+ dev: false
+
/unimport@3.4.0:
resolution: {integrity: sha512-M/lfFEgufIT156QAr/jWHLUn55kEmxBBiQsMxvRSIbquwmeJEyQYgshHDEvQDWlSJrVOOTAgnJ3FvlsrpGkanA==}
dependencies:
@@ -4527,6 +4846,68 @@ packages:
webpack-virtual-modules: 0.5.0
dev: false
+ /unstorage@1.9.0:
+ resolution: {integrity: sha512-VpD8ZEYc/le8DZCrny3bnqKE4ZjioQxBRnWE+j5sGNvziPjeDlaS1NaFFHzl/kkXaO3r7UaF8MGQrs14+1B4pQ==}
+ peerDependencies:
+ '@azure/app-configuration': ^1.4.1
+ '@azure/cosmos': ^3.17.3
+ '@azure/data-tables': ^13.2.2
+ '@azure/identity': ^3.2.3
+ '@azure/keyvault-secrets': ^4.7.0
+ '@azure/storage-blob': ^12.14.0
+ '@capacitor/preferences': ^5.0.0
+ '@planetscale/database': ^1.8.0
+ '@upstash/redis': ^1.22.0
+ '@vercel/kv': ^0.2.2
+ idb-keyval: ^6.2.1
+ peerDependenciesMeta:
+ '@azure/app-configuration':
+ optional: true
+ '@azure/cosmos':
+ optional: true
+ '@azure/data-tables':
+ optional: true
+ '@azure/identity':
+ optional: true
+ '@azure/keyvault-secrets':
+ optional: true
+ '@azure/storage-blob':
+ optional: true
+ '@capacitor/preferences':
+ optional: true
+ '@planetscale/database':
+ optional: true
+ '@upstash/redis':
+ optional: true
+ '@vercel/kv':
+ optional: true
+ idb-keyval:
+ optional: true
+ dependencies:
+ anymatch: 3.1.3
+ chokidar: 3.5.3
+ destr: 2.0.2
+ h3: 1.8.2
+ ioredis: 5.3.2
+ listhen: 1.5.5
+ lru-cache: 10.0.2
+ mri: 1.2.0
+ node-fetch-native: 1.4.1
+ ofetch: 1.3.3
+ ufo: 1.3.1
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
+ /untun@0.1.2:
+ resolution: {integrity: sha512-wLAMWvxfqyTiBODA1lg3IXHQtjggYLeTK7RnSfqtOXixWJ3bAa2kK/HHmOOg19upteqO3muLvN6O/icbyQY33Q==}
+ hasBin: true
+ dependencies:
+ citty: 0.1.4
+ consola: 3.2.3
+ pathe: 1.1.1
+ dev: false
+
/update-notifier@6.0.2:
resolution: {integrity: sha512-EDxhTEVPZZRLWYcJ4ZXjGFN0oP7qYvbXWzEgRm/Yql4dHX5wDbvh89YHP6PK1lzZJYrMtXUuZZz8XGK+U6U1og==}
engines: {node: '>=14.16'}
@@ -4547,6 +4928,10 @@ packages:
xdg-basedir: 5.1.0
dev: false
+ /uqr@0.1.2:
+ resolution: {integrity: sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==}
+ dev: false
+
/util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: false
diff --git a/scripts/build.ts b/scripts/build.ts
index 628bb006..db35122c 100644
--- a/scripts/build.ts
+++ b/scripts/build.ts
@@ -57,6 +57,7 @@ const config: tsup.Options[] = [
entry: {
index: 'src/index.ts',
testing: 'src/testing/index.ts',
+ storage: 'src/storage.ts',
},
format: ['cjs', 'esm'],
clean: true,
diff --git a/src/__tests__/storage.test.ts b/src/__tests__/storage.test.ts
new file mode 100644
index 00000000..3117b8f8
--- /dev/null
+++ b/src/__tests__/storage.test.ts
@@ -0,0 +1,70 @@
+import { fakeBrowser } from '@webext-core/fake-browser';
+import { describe, it, expect, beforeEach } from 'vitest';
+import { browser } from '~/browser';
+import { storage } from '~/storage';
+
+describe('Storage Utils', () => {
+ beforeEach(() => {
+ fakeBrowser.reset();
+ });
+
+ describe.each(['local', 'sync', 'managed', 'session'] as const)(
+ 'storage - %s',
+ (storageArea) => {
+ describe('getItem', () => {
+ it('should return the value from the correct storage area', async () => {
+ const expected = 123;
+ await fakeBrowser.storage[storageArea].set({ count: expected });
+ expect(await storage.getItem(`${storageArea}:count`)).toBe(expected);
+ });
+
+ it("should return null if the value doesn't exist", async () => {
+ expect(await storage.getItem(`${storageArea}:count`)).toBeNull();
+ });
+ });
+
+ describe('getItems', () => {
+ it('should return an array of values in the same order as the keys passed in', async () => {
+ const expected = [
+ { key: `${storageArea}:count`, value: 234 },
+ { key: `${storageArea}:installDate`, value: null },
+ ];
+ const keys = expected.map((item) => item.key);
+ await fakeBrowser.storage[storageArea].set({
+ count: expected[0].value,
+ });
+
+ const actual = await storage.getItems(keys);
+
+ expect(actual).toEqual(expected);
+ });
+ });
+
+ describe('setItem', () => {
+ it('should set the value in the correct storage area', () => {});
+
+ it.each([undefined, null])(
+ 'should remove the item from storage when setting the value to %s',
+ async (value) => {
+ await fakeBrowser.storage[storageArea].set({ count: 345 });
+ await storage.setItem(`${storageArea}:count`, value as null);
+
+ // For some reason storage sets the value to "null" instead of deleting it. So using
+ // fakeBrowser during the expect fails. Using storage works. I've confirmed that this
+ // doesn't happen in a real extension environment.
+ expect(await storage.getItem(`${storageArea}:count`)).toBeNull();
+ },
+ );
+ });
+
+ describe('removeItem', () => {
+ it('should remove the key from storage', async () => {
+ await fakeBrowser.storage[storageArea].set({ count: 456 });
+ await storage.removeItem(`${storageArea}:count`);
+
+ expect(await browser.storage[storageArea].get()).toEqual({});
+ });
+ });
+ },
+ );
+});
diff --git a/src/core/utils/unimport.ts b/src/core/utils/unimport.ts
index d71735e3..07a2d289 100644
--- a/src/core/utils/unimport.ts
+++ b/src/core/utils/unimport.ts
@@ -17,6 +17,7 @@ export function getUnimportOptions(
{ package: 'wxt/client' },
{ package: 'wxt/browser' },
{ package: 'wxt/sandbox' },
+ { package: 'wxt/storage' },
],
warn: config.logger.warn,
dirs: ['components', 'composables', 'hooks', 'utils'],
diff --git a/src/storage.ts b/src/storage.ts
new file mode 100644
index 00000000..61fe9461
--- /dev/null
+++ b/src/storage.ts
@@ -0,0 +1,118 @@
+/**
+ * @module wxt/storage
+ */
+import {
+ Driver,
+ WatchCallback,
+ createStorage,
+ defineDriver,
+ Storage,
+} from 'unstorage';
+import browser, { Storage as BrowserStorage } from 'webextension-polyfill';
+
+export interface WebExtensionDriverOptions {
+ storageArea: 'sync' | 'local' | 'managed' | 'session';
+}
+
+export const webExtensionDriver: (opts: WebExtensionDriverOptions) => Driver =
+ defineDriver((opts) => {
+ const checkPermission = () => {
+ if (browser.storage == null)
+ throw Error(
+ "You must request the 'storage' permission to use webExtensionDriver",
+ );
+ };
+
+ const _storageListener: (
+ changes: BrowserStorage.StorageAreaSyncOnChangedChangesType,
+ ) => void = (changes) => {
+ Object.entries(changes).forEach(([key, { newValue }]) => {
+ _listeners.forEach((callback) => {
+ callback(newValue ? 'update' : 'remove', key);
+ });
+ });
+ };
+ const _listeners = new Set();
+
+ return {
+ name: 'web-extension:' + opts.storageArea,
+ async hasItem(key) {
+ checkPermission();
+ const res = await browser.storage[opts.storageArea].get(key);
+ return res[key] != null;
+ },
+ async getItem(key) {
+ checkPermission();
+ const res = await browser.storage[opts.storageArea].get(key);
+ return res[key] ?? null;
+ },
+ async getItems(items) {
+ checkPermission();
+ const res = await browser.storage[opts.storageArea].get(
+ items.map((item) => item.key),
+ );
+ return items.map((item) => ({
+ key: item.key,
+ value: res[item.key] ?? null,
+ }));
+ },
+ async setItem(key, value) {
+ checkPermission();
+ await browser.storage[opts.storageArea].set({ [key]: value ?? null });
+ },
+ async setItems(items) {
+ checkPermission();
+ const map = items.reduce>((map, item) => {
+ map[item.key] = item.value ?? null;
+ return map;
+ }, {});
+ await browser.storage[opts.storageArea].set(map);
+ },
+ async removeItem(key) {
+ checkPermission();
+ await browser.storage[opts.storageArea].remove(key);
+ },
+ async getKeys() {
+ checkPermission();
+ const all = await browser.storage[opts.storageArea].get();
+ return Object.keys(all);
+ },
+ async clear() {
+ checkPermission();
+ await browser.storage[opts.storageArea].clear();
+ },
+ watch(callback) {
+ checkPermission();
+ _listeners.add(callback);
+ if (_listeners.size === 1) {
+ browser.storage[opts.storageArea].onChanged.addListener(
+ _storageListener,
+ );
+ }
+
+ return () => {
+ _listeners.delete(callback);
+ if (_listeners.size === 0) {
+ browser.storage[opts.storageArea].onChanged.removeListener(
+ _storageListener,
+ );
+ }
+ };
+ },
+ };
+ });
+
+function createWebExtensionStorage() {
+ const storage = createStorage();
+ storage.mount('local', webExtensionDriver({ storageArea: 'local' }));
+ storage.mount('session', webExtensionDriver({ storageArea: 'session' }));
+ storage.mount('sync', webExtensionDriver({ storageArea: 'sync' }));
+ storage.mount('managed', webExtensionDriver({ storageArea: 'managed' }));
+ return storage;
+}
+
+export type StorageValue = null | string | number | boolean | object;
+
+export const storage: Storage = createWebExtensionStorage();
+
+export * from 'unstorage';
diff --git a/src/virtual/background-entrypoint.ts b/src/virtual/background-entrypoint.ts
index fac4bfd4..28b7a8e3 100644
--- a/src/virtual/background-entrypoint.ts
+++ b/src/virtual/background-entrypoint.ts
@@ -2,6 +2,7 @@ import definition from 'virtual:user-background';
import { setupWebSocket } from '../client/utils/setup-web-socket';
import { logger } from '../client/utils/logger';
import browser from 'webextension-polyfill';
+import { storage } from '~/storage';
import { keepServiceWorkerAlive } from '../client/utils/keep-service-worker-alive';
import { reloadContentScript } from '../client/utils/reload-content-scripts';
@@ -29,6 +30,14 @@ if (__COMMAND__ === 'serve') {
}
try {
+ // Add utils to global scope so they're accessible from the console during development
+ if (import.meta.env.DEV) {
+ // @ts-expect-error: Untyped global
+ globalThis.wxt = {
+ storage,
+ };
+ }
+
const res = definition.main();
// @ts-expect-error: res shouldn't be a promise, but we're checking it anyways
if (res instanceof Promise) {