From d2cb8f94163a0002da152629f0b6759e91c3ba90 Mon Sep 17 00:00:00 2001 From: Aaron Date: Tue, 14 Nov 2023 23:07:45 -0600 Subject: [PATCH] feat: Add `wxt/storage` API (#234) --- demo/src/entrypoints/background.ts | 2 + demo/wxt.config.ts | 1 + docs/.vitepress/config.ts | 1 + docs/guide/compare.md | 2 +- docs/guide/storage.md | 36 +++ docs/typedoc.json | 1 + e2e/tests/auto-imports.test.ts | 11 + package.json | 8 +- pnpm-lock.yaml | 413 ++++++++++++++++++++++++++- scripts/build.ts | 1 + src/__tests__/storage.test.ts | 70 +++++ src/core/utils/unimport.ts | 1 + src/storage.ts | 118 ++++++++ src/virtual/background-entrypoint.ts | 9 + 14 files changed, 658 insertions(+), 16 deletions(-) create mode 100644 docs/guide/storage.md create mode 100644 src/__tests__/storage.test.ts create mode 100644 src/storage.ts 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) {