Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f3a90eeb2 | |||
| a360965d92 | |||
| eb1ace6d20 | |||
| 497c902283 | |||
| 4d4453b830 | |||
| ef53580ed1 | |||
| 52e5387d47 | |||
| f5b7f7e0e5 | |||
| 592bdbe4d9 | |||
| 6fc227bad6 | |||
| d74f6b0735 | |||
| 62ea796045 | |||
| 064704c9b7 | |||
| c846a1febf | |||
| c3959e8427 | |||
| e770f9b6b7 | |||
| 67f35615a2 | |||
| 5c7b6098a2 | |||
| f7d12b7486 | |||
| 3481313859 | |||
| 2f23556893 | |||
| 330e39159a | |||
| aa14676f91 | |||
| 199f330324 | |||
| 63dee97ac3 | |||
| b121ed2fe2 | |||
| 607e1162e5 | |||
| b4d569a915 | |||
| 4fac364542 | |||
| 51c7ee5fa8 | |||
| 9b780f2d00 | |||
| f5aec7ec9b | |||
| 5d991eda31 | |||
| 0318a34046 | |||
| e7c66304d3 | |||
| 4603ebb511 | |||
| ea570c12b7 | |||
| c17ce34a27 | |||
| 0e4d3ad8ab | |||
| 4304f71df2 | |||
| bb5ea34396 |
@@ -0,0 +1,15 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [wxt-dev] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
polar: # Replace with a single Polar username
|
||||
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
||||
thanks_dev: # Replace with a single thanks.dev username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
@@ -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 }}
|
||||
|
||||
@@ -13,6 +13,7 @@ on:
|
||||
- module-solid
|
||||
- module-svelte
|
||||
- module-vue
|
||||
- storage
|
||||
- unocss
|
||||
- wxt
|
||||
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -32,6 +32,7 @@ Here are some helpful commands:
|
||||
|
||||
```sh
|
||||
# Build WXT package
|
||||
cd packages/wxt
|
||||
pnpm build
|
||||
```
|
||||
|
||||
|
||||
@@ -50,6 +50,12 @@ const chromeExtensionIds = [
|
||||
'fcphghnknhkimeagdglkljinmpbagone', // YouTube Auto HD + FPS
|
||||
'lpomjgbicdemjkgmbnkjncgdebogkhlb', // MultiViewer Companion
|
||||
'ggiafipgeeaaahnjamgpjcgkdpanhddg', // Sync Watch - Watch videos together on any site
|
||||
'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);
|
||||
|
||||
@@ -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'),
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 164 KiB |
@@ -62,7 +62,7 @@ import { resolve } from 'node:path';
|
||||
|
||||
export default defineRunnerConfig({
|
||||
// On Windows, the path must be absolute
|
||||
chromiumArgs: [`--user-data-dir="${resolve(".wxt/chrome-data")}"`
|
||||
chromiumArgs: [`--user-data-dir="${resolve('.wxt/chrome-data')}"`],
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -56,3 +56,26 @@ Vite provides two other environment variables, but they aren't useful in WXT pro
|
||||
- `import.meta.env.BASE_URL`: Use `browser.runtime.getURL` instead.
|
||||
- `import.meta.env.SSR`: Always `false`.
|
||||
:::
|
||||
|
||||
## Manifest
|
||||
|
||||
To use environment variables in the manifest, you need to use the function syntax:
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
extensionApi: 'chrome',
|
||||
modules: ['@wxt-dev/module-vue'],
|
||||
manifest: { // [!code --]
|
||||
oauth2: { // [!code --]
|
||||
client_id: import.meta.env.WXT_APP_CLIENT_ID // [!code --]
|
||||
} // [!code --]
|
||||
} // [!code --]
|
||||
manifest: () => ({ // [!code ++]
|
||||
oauth2: { // [!code ++]
|
||||
client_id: import.meta.env.WXT_APP_CLIENT_ID // [!code ++]
|
||||
} // [!code ++]
|
||||
}), // [!code ++]
|
||||
});
|
||||
```
|
||||
|
||||
WXT can't load your `.env` files until after the config file has been loaded. So by using the function syntax for `manifest`, it defers creating the object until after the `.env` files are loaded into the process.
|
||||
|
||||
@@ -239,6 +239,7 @@ export default defineContentScript({
|
||||
onMount: (container) => {
|
||||
// Render your app to the UI container
|
||||
const unmount = render(() => <div>...</div>, container);
|
||||
return unmount;
|
||||
},
|
||||
onRemove: (unmount) => {
|
||||
// Unmount the app when the UI is removed
|
||||
@@ -602,7 +603,7 @@ export default defineContentScript({
|
||||
matches: ['*://*.youtube.com/*'],
|
||||
main(ctx) {
|
||||
ctx.addEventListener(window, 'wxt:locationchange', ({ newUrl }) => {
|
||||
if (watchPattern.matches(newUrl)) mainWatch(ctx);
|
||||
if (watchPattern.includes(newUrl)) mainWatch(ctx);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
});
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@ WXT is a modern, open-source framework for building web extensions. Inspired by
|
||||
- Provide an awesome [DX](https://about.gitlab.com/topics/devops/what-is-developer-experience/)
|
||||
- Provide first-class support for all major browsers
|
||||
|
||||
Check out the [comparison](/guide/resources/compare) to see how WXT compares to other tools for building web extnesions.
|
||||
Check out the [comparison](/guide/resources/compare) to see how WXT compares to other tools for building web extensions.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
|
||||
@@ -8,9 +8,7 @@ Commonly asked questions about how to use WXT or why it behaves the way it does.
|
||||
|
||||
[[toc]]
|
||||
|
||||
---
|
||||
|
||||
### Why are content scripts not showing up in the manifest?
|
||||
## Why aren't content scripts added to the manifest?
|
||||
|
||||
During development, WXT registers content scripts dynamically so they can be reloaded individually when a file is saved without reloading your entire extension.
|
||||
|
||||
@@ -19,3 +17,50 @@ To list the content scripts registered during development, open the service work
|
||||
```js
|
||||
await chrome.scripting.getRegisteredContentScripts();
|
||||
```
|
||||
|
||||
## My component library doesn't work in content scripts!
|
||||
|
||||
Component libraries place their CSS in the document's `<head>` by default. When using `createShadowRoot`, your UI is isolated from the document's styles because it's inside a ShadowRoot.
|
||||
|
||||
To fix this, you need to tell your component library to insert it's CSS inside the shadow root. Here's the docs for a couple of popular libraries:
|
||||
|
||||
- React
|
||||
- Ant Design: [`StyleProvider`](https://ant.design/docs/react/compatible-style#shadow-dom-usage)
|
||||
- Mantine: [`MantineProvider#getRootElement` and `MantineProvider#cssVariablesSelector`](https://mantine.dev/theming/mantine-provider/)
|
||||
|
||||
> If your library isn't listed above, try searching it's docs/issues for "shadow root", "shadow dom", or "css container".
|
||||
|
||||
`createShadowRoot` provides it's own `<head>` element inside the shadow root, so that were you should tell the library to add the CSS. Here's an example with Ant Design:
|
||||
|
||||
```tsx
|
||||
import { StyleProvider } from '@ant-design/cssinjs'; // [!code ++]
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
|
||||
const ui = await createShadowRootUi(ctx, {
|
||||
name: 'example-ui',
|
||||
position: 'inline',
|
||||
anchor: 'body',
|
||||
onMount: (container) => { // [!code --]
|
||||
onMount: (container, shadow) => { // [!code ++]
|
||||
const cssContainer = shadow.querySelector("head")!; // [!code ++]
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(
|
||||
<StyleProvider container={cssContainer}> // [!code ++]
|
||||
<App />
|
||||
</StyleProvider> // [!code ++]
|
||||
);
|
||||
return root;
|
||||
},
|
||||
onRemove: (root) => {
|
||||
root?.unmount();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Note that this doesn't effect all component libraries, just ones that inject CSS themselves rather than having you import their CSS. This approach is more prevailent in the React community, but not limited to it. That's why only React libraries are listed above. Vuetify, for example, works just fine because you import its CSS - WXT picks up on this and the CSS is added inside the shadow root automatically:
|
||||
|
||||
```ts
|
||||
import 'vuetify/styles'; // <-- This line imports the CSS, just like importing a .css file
|
||||
import { createVuetify } from 'vuetify';
|
||||
```
|
||||
|
||||
+69
-22
@@ -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,11 +75,11 @@ 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
|
||||
|
||||
To listen for storage changes, use the `storage.watch` function. It lets you setup a listener for a single key:
|
||||
To listen for storage changes, use the `storage.watch` function. It lets you set up a listener for a single key:
|
||||
|
||||
```ts
|
||||
const unwatch = storage.watch<number>('local:counter', (newCount, oldCount) => {
|
||||
@@ -100,7 +124,7 @@ You can remove all metadata associated with a key, or just specific properties:
|
||||
// Remove all properties
|
||||
await storage.removeMeta('local:preference');
|
||||
|
||||
// Remove one property
|
||||
// Remove only the "lastModified" property
|
||||
await storage.removeMeta('local:preference', 'lastModified');
|
||||
|
||||
// Remove multiple properties
|
||||
@@ -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
|
||||
|
||||
@@ -225,13 +249,13 @@ export const ignoredWebsites = storage.defineItem<IgnoredWebsiteV3[]>( // [!code
|
||||
Internally, this uses a metadata property called `v` to track the value's current version.
|
||||
:::
|
||||
|
||||
In this case, we thought that the ignored website list might change in the future, and were able to setup a versioned storage item from the start.
|
||||
In this case, we thought that the ignored website list might change in the future, and were able to set up a versioned storage item from the start.
|
||||
|
||||
Realistically, you won't know a item needs versioned until you need to change it's schema. Thankfully, it's simple to add versioning to an unversioned storage item.
|
||||
Realistically, you won't know an item needs versioning until you need to change its schema. Thankfully, it's simple to add versioning to an unversioned storage item.
|
||||
|
||||
When a previous version isn't found, WXT assumes the version was `1`. That means you just need to set `version: 2` and add a migration for `2`, and it will just work!
|
||||
|
||||
Lets look at the same ignored websites example from before, but start with an unversioned item this time:
|
||||
Let's look at the same ignored websites example from before, but start with an unversioned item this time:
|
||||
|
||||
:::code-group
|
||||
|
||||
@@ -275,13 +299,13 @@ export const ignoredWebsites = storage.defineItem<IgnoredWebsiteV2[]>( // [!code
|
||||
|
||||
### Running Migrations
|
||||
|
||||
As soon as `storage.defineItem` is called, WXT checks if migrations need to be ran, and if so, runs them. Calls to get or update the storage item's value or metadata (`getValue`, `setValue`, `removeValue`, `getMeta`, etc) will automatically wait for the migration process to finish before actually reading or writing values.
|
||||
As soon as `storage.defineItem` is called, WXT checks if migrations need to be run, and if so, runs them. Calls to get or update the storage item's value or metadata (`getValue`, `setValue`, `removeValue`, `getMeta`, etc.) will automatically wait for the migration process to finish before actually reading or writing values.
|
||||
|
||||
### Default Values
|
||||
|
||||
With `storage.defineItem`, there are multiple ways of defining default values:
|
||||
|
||||
1. `fallback` - Return this value from `getValue` instead of `null` if the value is missing.
|
||||
1. **`fallback`** - Return this value from `getValue` instead of `null` if the value is missing.
|
||||
|
||||
This option is great for providing default values for settings:
|
||||
|
||||
@@ -294,7 +318,7 @@ With `storage.defineItem`, there are multiple ways of defining default values:
|
||||
});
|
||||
```
|
||||
|
||||
2. `init` - Initialize and save a value in storage if it is not already saved.
|
||||
2. **`init`** - Initialize and save a value in storage if it is not already saved.
|
||||
|
||||
This is great for values that need to be initialized or set once:
|
||||
|
||||
@@ -308,3 +332,26 @@ With `storage.defineItem`, there are multiple ways of defining default values:
|
||||
```
|
||||
|
||||
The value is initialized in storage immediately.
|
||||
|
||||
## Bulk Operations
|
||||
|
||||
When getting or setting multiple values in storage, you can perform bulk operations to improve performance by reducing the number of individual storage calls. The `storage` API provides several methods for performing bulk operations:
|
||||
|
||||
- **`getItems`** - Get multiple values at once.
|
||||
- **`getMetas`** - Get metadata for multiple items at once.
|
||||
- **`setItems`** - Set multiple values at once.
|
||||
- **`setMetas`** - Set metadata for multiple items at once.
|
||||
- **`removeItems`** - Remove multiple values (and optionally metadata) at once.
|
||||
|
||||
All these APIs support both string keys and defined storage items:
|
||||
|
||||
```ts
|
||||
const userId = storage.defineItem('local:userId');
|
||||
|
||||
await storage.setItems([
|
||||
{ key: 'local:installDate', value: Date.now() },
|
||||
{ item: userId, value: generateUserId() },
|
||||
]);
|
||||
```
|
||||
|
||||
Refer to the [API Reference](/api/reference/@wxt-dev/storage/interfaces/WxtStorage) for types and examples of how to use all the bulk APIs.
|
||||
|
||||
+6
-6
@@ -4,7 +4,7 @@
|
||||
"engines": {
|
||||
"node": ">=18.20.3"
|
||||
},
|
||||
"packageManager": "pnpm@9.12.0",
|
||||
"packageManager": "pnpm@9.13.0",
|
||||
"scripts": {
|
||||
"check": "check && pnpm -r --sequential run check",
|
||||
"test": "pnpm -r --sequential run test run",
|
||||
@@ -20,24 +20,24 @@
|
||||
"@aklinker1/buildc": "^1.1.4",
|
||||
"@aklinker1/check": "^1.4.5",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@vitest/coverage-v8": "^2.1.2",
|
||||
"@vitest/coverage-v8": "^2.1.4",
|
||||
"changelogen": "^0.5.7",
|
||||
"consola": "^3.2.3",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fs-extra": "^11.2.0",
|
||||
"lint-staged": "^15.2.10",
|
||||
"markdown-it-footnote": "^4.0.0",
|
||||
"nano-spawn": "^0.1.0",
|
||||
"nano-spawn": "^0.2.0",
|
||||
"prettier": "^3.3.3",
|
||||
"simple-git-hooks": "^2.11.1",
|
||||
"tsx": "4.15.7",
|
||||
"typedoc": "^0.25.4",
|
||||
"typedoc-plugin-markdown": "4.0.0-next.23",
|
||||
"typedoc-vitepress-theme": "1.0.0-next.3",
|
||||
"typescript": "^5.6.2",
|
||||
"vitepress": "^1.3.4",
|
||||
"typescript": "^5.6.3",
|
||||
"vitepress": "^1.5.0",
|
||||
"vitest-mock-extended": "^2.0.2",
|
||||
"vue": "^3.5.11",
|
||||
"vue": "^3.5.12",
|
||||
"wxt": "workspace:*"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
|
||||
@@ -46,9 +46,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.4.5",
|
||||
"oxlint": "^0.9.9",
|
||||
"publint": "^0.2.11",
|
||||
"typescript": "^5.6.2",
|
||||
"oxlint": "^0.11.1",
|
||||
"publint": "^0.2.12",
|
||||
"typescript": "^5.6.3",
|
||||
"unbuild": "^2.0.0",
|
||||
"wxt": "workspace:*"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
# Changelog
|
||||
|
||||
## v0.2.3
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/i18n-v0.2.2...i18n-v0.2.3)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Prevent app crashes from parse errors due to incomplete file saves ([#1114](https://github.com/wxt-dev/wxt/pull/1114))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Cleanup typos and broken links ([bb5ea34](https://github.com/wxt-dev/wxt/commit/bb5ea34))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Bread Grocery <breadgrocery@gmail.com>
|
||||
- Aaron ([@aklinker1](http://github.com/aklinker1))
|
||||
|
||||
## v0.2.2
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/i18n-v0.2.1...i18n-v0.2.2)
|
||||
|
||||
@@ -240,11 +240,11 @@ A key is treated as "verbose" when it is:
|
||||
{
|
||||
"appName": {
|
||||
"message": "GitHub - Better Line Counts",
|
||||
"description": "The app's name, should not be translated",
|
||||
"description": "The app's name, should not be translated"
|
||||
},
|
||||
"ok": "OK",
|
||||
"deleteConfirmation": {
|
||||
"title": "Delete XYZ?"
|
||||
"title": "Delete XYZ?",
|
||||
"message": "You cannot undo this action once taken."
|
||||
}
|
||||
}
|
||||
@@ -304,7 +304,7 @@ const messages = {
|
||||
// ...
|
||||
};
|
||||
|
||||
// Generate JSON files for the browser
|
||||
// Generate JSON files for the extension
|
||||
await generateChromeMessagesFile('dist/_locales/en/messages.json', messages.en);
|
||||
await generateChromeMessagesFile('dist/_locales/de/messages.json', messages.de);
|
||||
// ...
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@wxt-dev/i18n",
|
||||
"description": "Type-safe wrapper around browser.i18n.getMessage with additional features",
|
||||
"version": "0.2.2",
|
||||
"version": "0.2.3",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -26,7 +26,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"chokidar": "^3.6.0",
|
||||
"confbox": "^0.1.7",
|
||||
"confbox": "^0.1.8",
|
||||
"fast-glob": "^3.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -39,13 +39,13 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.4.5",
|
||||
"@types/chrome": "^0.0.268",
|
||||
"@types/node": "^20.16.10",
|
||||
"oxlint": "^0.9.9",
|
||||
"publint": "^0.2.11",
|
||||
"typescript": "^5.6.2",
|
||||
"@types/chrome": "^0.0.280",
|
||||
"@types/node": "^20.17.6",
|
||||
"oxlint": "^0.11.1",
|
||||
"publint": "^0.2.12",
|
||||
"typescript": "^5.6.3",
|
||||
"unbuild": "^2.0.0",
|
||||
"vitest": "^2.0.0",
|
||||
"vitest": "^2.1.4",
|
||||
"vitest-plugin-random-seed": "^1.1.0",
|
||||
"wxt": "workspace:*"
|
||||
},
|
||||
|
||||
@@ -158,7 +158,9 @@ export { type GeneratedI18nStructure }
|
||||
if (wxt.config.command === 'serve') {
|
||||
wxt.hooks.hookOnce('build:done', () => {
|
||||
const watcher = watch(localesDir);
|
||||
watcher.on('change', updateLocalizations);
|
||||
watcher.on('change', (path) => {
|
||||
updateLocalizations(path).catch(wxt.logger.error);
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 ??= {};
|
||||
|
||||
@@ -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,19 +44,19 @@
|
||||
"prepare": "buildc --deps-only -- wxt prepare"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"wxt": ">=0.18.6"
|
||||
"wxt": ">=0.19.16"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-react": "^4.3.2"
|
||||
"@vitejs/plugin-react": "^4.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.4.5",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"publint": "^0.2.11",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"publint": "^0.2.12",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"typescript": "^5.6.2",
|
||||
"typescript": "^5.6.3",
|
||||
"unbuild": "^2.0.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
|
||||
@@ -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 ??= {};
|
||||
|
||||
@@ -44,16 +44,16 @@
|
||||
"prepare": "buildc --deps-only -- wxt prepare"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"wxt": ">=0.18.6"
|
||||
"wxt": ">=0.19.16"
|
||||
},
|
||||
"dependencies": {
|
||||
"vite-plugin-solid": "^2.10.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.4.5",
|
||||
"publint": "^0.2.11",
|
||||
"solid-js": "^1.9.1",
|
||||
"typescript": "^5.6.2",
|
||||
"publint": "^0.2.12",
|
||||
"solid-js": "^1.9.3",
|
||||
"typescript": "^5.6.3",
|
||||
"unbuild": "^2.0.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# Changelog
|
||||
|
||||
## v2.0.0
|
||||
|
||||
[⚠️ breaking changes](https://wxt.dev/guide/upgrade-guide/wxt) • [compare changes](https://github.com/wxt-dev/wxt/compare/module-svelte-v1.0.1...module-svelte-v2.0.0)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- ⚠️ Svelte 5 support ([#1104](https://github.com/wxt-dev/wxt/pull/1104))
|
||||
|
||||
### 🏡 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))
|
||||
|
||||
#### ⚠️ Breaking Changes
|
||||
|
||||
Upgraded `@sveltejs/vite-plugin-svelte` from v3 to v4. This drops support for Svelte 4 and below. To continue using older versions of Svelte, use v1 of this module.
|
||||
|
||||
To upgrade to svelte 5, just install `svelte@5`.
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](http://github.com/aklinker1))
|
||||
|
||||
## v1.0.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/module-svelte-v1.0.0...module-svelte-v1.0.1)
|
||||
@@ -28,4 +51,4 @@
|
||||
|
||||
## v1.0.0
|
||||
|
||||
Initial release 🎉
|
||||
Initial release 🎉
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"email": "aaronklinker1+wxt@gmail.com"
|
||||
},
|
||||
"license": "MIT",
|
||||
"version": "1.0.1",
|
||||
"version": "2.0.0",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.mjs",
|
||||
@@ -42,15 +42,16 @@
|
||||
"check": "pnpm build && check"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"wxt": ">=0.18.6"
|
||||
"wxt": ">=0.18.6",
|
||||
"svelte": ">=5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.2"
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.4.5",
|
||||
"publint": "^0.2.11",
|
||||
"typescript": "^5.6.2",
|
||||
"publint": "^0.2.12",
|
||||
"typescript": "^5.6.3",
|
||||
"unbuild": "^2.0.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,15 +42,15 @@
|
||||
"check": "pnpm build && check"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"wxt": ">=0.18.6"
|
||||
"wxt": ">=0.19.16"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-vue": "^5.1.4"
|
||||
"@vitejs/plugin-vue": "^5.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.4.5",
|
||||
"publint": "^0.2.11",
|
||||
"typescript": "^5.6.2",
|
||||
"publint": "^0.2.12",
|
||||
"typescript": "^5.6.3",
|
||||
"unbuild": "^2.0.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
|
||||
@@ -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 ??= {};
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,36 @@
|
||||
# WXT Storage
|
||||
|
||||
[Changelog](https://github.com/wxt-dev/wxt/blob/main/packages/storage/CHANGELOG.md) • [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).
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
+501
-40
@@ -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 { 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
|
||||
@@ -38,7 +37,7 @@ describe('Storage Utils', () => {
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it('should return the value if multiple : are use in the key', async () => {
|
||||
it('should return the value if multiple : are used in the key', async () => {
|
||||
const expected = 'value';
|
||||
await fakeBrowser.storage[storageArea].set({ 'some:key': expected });
|
||||
|
||||
@@ -64,27 +63,97 @@ describe('Storage Utils', () => {
|
||||
});
|
||||
|
||||
describe('getItems', () => {
|
||||
it('should return an array of values', async () => {
|
||||
const expected = [
|
||||
{ key: `${storageArea}:count`, value: 234 },
|
||||
{ key: `${storageArea}:installDate`, value: null },
|
||||
{ key: `${storageArea}:otherValue`, value: 345 },
|
||||
] as const;
|
||||
const params = [
|
||||
expected[0].key,
|
||||
expected[1].key,
|
||||
{
|
||||
key: expected[2].key,
|
||||
options: { defaultValue: expected[2].value },
|
||||
},
|
||||
];
|
||||
it('should get values from multiple storage keys', async () => {
|
||||
const item1 = {
|
||||
key: `${storageArea}:one`,
|
||||
expectedValue: 1,
|
||||
} as const;
|
||||
const item2 = {
|
||||
key: `${storageArea}:two`,
|
||||
expectedValue: null,
|
||||
} as const;
|
||||
|
||||
await fakeBrowser.storage[storageArea].set({
|
||||
count: expected[0].value,
|
||||
one: item1.expectedValue,
|
||||
});
|
||||
|
||||
const actual = await storage.getItems(params);
|
||||
const actual = await storage.getItems([item1.key, item2.key]);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
expect(actual).toEqual([
|
||||
{ key: item1.key, value: item1.expectedValue },
|
||||
{ key: item2.key, value: item2.expectedValue },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should get values from multiple storage items', async () => {
|
||||
const item1 = storage.defineItem(`${storageArea}:one`);
|
||||
const expectedValue1 = 1;
|
||||
const item2 = storage.defineItem(`${storageArea}:two`);
|
||||
const expectedValue2 = null;
|
||||
|
||||
await fakeBrowser.storage[storageArea].set({
|
||||
one: expectedValue1,
|
||||
});
|
||||
|
||||
const actual = await storage.getItems([item1, item2]);
|
||||
|
||||
expect(actual).toEqual([
|
||||
{ key: item1.key, value: expectedValue1 },
|
||||
{ key: item2.key, value: expectedValue2 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should get values for a combination of different input types', async () => {
|
||||
const key1 = `${storageArea}:one` as const;
|
||||
const expectedValue1 = 1;
|
||||
const item2 = storage.defineItem<number>(`${storageArea}:two`);
|
||||
const expectedValue2 = 2;
|
||||
|
||||
await fakeBrowser.storage[storageArea].set({
|
||||
one: expectedValue1,
|
||||
two: expectedValue2,
|
||||
});
|
||||
|
||||
const actual = await storage.getItems([key1, item2]);
|
||||
|
||||
expect(actual).toEqual([
|
||||
{ key: key1, value: expectedValue1 },
|
||||
{ key: item2.key, value: expectedValue2 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return fallback values for keys when provided', async () => {
|
||||
const key1 = `${storageArea}:one` as const;
|
||||
const expectedValue1 = null;
|
||||
const key2 = `${storageArea}:two` as const;
|
||||
const fallback2 = 2;
|
||||
const expectedValue2 = fallback2;
|
||||
|
||||
const actual = await storage.getItems([
|
||||
key1,
|
||||
{ key: key2, options: { fallback: fallback2 } },
|
||||
]);
|
||||
|
||||
expect(actual).toEqual([
|
||||
{ key: key1, value: expectedValue1 },
|
||||
{ key: key2, value: expectedValue2 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return fallback values for items when provided', async () => {
|
||||
const item1 = storage.defineItem<number>(`${storageArea}:one`);
|
||||
const expectedValue1 = null;
|
||||
const item2 = storage.defineItem(`${storageArea}:two`, {
|
||||
fallback: 2,
|
||||
});
|
||||
const expectedValue2 = item2.fallback;
|
||||
|
||||
const actual = await storage.getItems([item1, item2]);
|
||||
|
||||
expect(actual).toEqual([
|
||||
{ key: item1.key, value: expectedValue1 },
|
||||
{ key: item2.key, value: expectedValue2 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -153,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(),
|
||||
};
|
||||
@@ -169,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 });
|
||||
@@ -180,6 +249,74 @@ describe('Storage Utils', () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe('setMetas', () => {
|
||||
it('should set key metadata correctly', async () => {
|
||||
const key1 = `${storageArea}:one` as const;
|
||||
const initialMeta1 = {};
|
||||
const setMeta1 = { v: 1 };
|
||||
const expectedMeta1 = setMeta1;
|
||||
|
||||
const key2 = `${storageArea}:two` as const;
|
||||
const initialMeta2 = { v: 1 };
|
||||
const setMeta2 = { v: 2 };
|
||||
const expectedMeta2 = setMeta2;
|
||||
|
||||
const key3 = `${storageArea}:three` as const;
|
||||
const initialMeta3 = { v: 1 };
|
||||
const setMeta3 = { d: Date.now() };
|
||||
const expectedMeta3 = { ...initialMeta3, ...setMeta3 };
|
||||
|
||||
await fakeBrowser.storage[storageArea].set({
|
||||
one$: initialMeta1,
|
||||
two$: initialMeta2,
|
||||
three$: initialMeta3,
|
||||
});
|
||||
|
||||
await storage.setMetas([
|
||||
{ key: key1, meta: setMeta1 },
|
||||
{ key: key2, meta: setMeta2 },
|
||||
{ key: key3, meta: setMeta3 },
|
||||
]);
|
||||
|
||||
expect(await storage.getMeta(key1)).toEqual(expectedMeta1);
|
||||
expect(await storage.getMeta(key2)).toEqual(expectedMeta2);
|
||||
expect(await storage.getMeta(key3)).toEqual(expectedMeta3);
|
||||
});
|
||||
|
||||
it('should set item metadata correctly', async () => {
|
||||
const item1 = storage.defineItem(`${storageArea}:one`);
|
||||
const initialMeta1 = {};
|
||||
const setMeta1 = { v: 1 };
|
||||
const expectedMeta1 = setMeta1;
|
||||
|
||||
const item2 = storage.defineItem(`${storageArea}:two`);
|
||||
const initialMeta2 = { v: 1 };
|
||||
const setMeta2 = { v: 2 };
|
||||
const expectedMeta2 = setMeta2;
|
||||
|
||||
const item3 = storage.defineItem(`${storageArea}:three`);
|
||||
const initialMeta3 = { v: 1 };
|
||||
const setMeta3 = { d: Date.now() };
|
||||
const expectedMeta3 = { ...initialMeta3, ...setMeta3 };
|
||||
|
||||
await fakeBrowser.storage[storageArea].set({
|
||||
one$: initialMeta1,
|
||||
two$: initialMeta2,
|
||||
three$: initialMeta3,
|
||||
});
|
||||
|
||||
await storage.setMetas([
|
||||
{ item: item1, meta: setMeta1 },
|
||||
{ item: item2, meta: setMeta2 },
|
||||
{ item: item3, meta: setMeta3 },
|
||||
]);
|
||||
|
||||
expect(await item1.getMeta()).toEqual(expectedMeta1);
|
||||
expect(await item2.getMeta()).toEqual(expectedMeta2);
|
||||
expect(await item3.getMeta()).toEqual(expectedMeta3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeItem', () => {
|
||||
it('should remove the key from storage', async () => {
|
||||
await fakeBrowser.storage[storageArea].set({ count: 456 });
|
||||
@@ -219,29 +356,136 @@ describe('Storage Utils', () => {
|
||||
});
|
||||
|
||||
describe('removeItems', () => {
|
||||
it('should remove multiple items', async () => {
|
||||
it('should remove multiple keys', async () => {
|
||||
const key1 = `${storageArea}:one` as const;
|
||||
const key2 = `${storageArea}:two` as const;
|
||||
const key3 = `${storageArea}:three` as const;
|
||||
await fakeBrowser.storage[storageArea].set({
|
||||
['one']: '1',
|
||||
['two']: null,
|
||||
['two$']: { v: 1 },
|
||||
['three']: '1',
|
||||
['three$']: { v: 1 },
|
||||
one: 1,
|
||||
two: 2,
|
||||
});
|
||||
|
||||
await storage.removeItems([key1, key2]);
|
||||
|
||||
expect(await storage.getItem(key1)).toBeNull();
|
||||
expect(await storage.getItem(key2)).toBeNull();
|
||||
});
|
||||
|
||||
it('should remove multiple keys and metadata when requested', async () => {
|
||||
const key1 = `${storageArea}:one` as const;
|
||||
const key2 = `${storageArea}:two` as const;
|
||||
await fakeBrowser.storage[storageArea].set({
|
||||
one: 1,
|
||||
one$: { v: 1 },
|
||||
two: 2,
|
||||
two$: { v: 1 },
|
||||
});
|
||||
|
||||
await storage.removeItems([
|
||||
key1,
|
||||
key2,
|
||||
{ key: key3, options: { removeMeta: true } },
|
||||
{ key: key2, options: { removeMeta: true } },
|
||||
]);
|
||||
|
||||
expect(await storage.getItem(key1)).toBeNull();
|
||||
expect(await storage.getMeta(key1)).toEqual({ v: 1 });
|
||||
expect(await storage.getItem(key2)).toBeNull();
|
||||
expect(await storage.getMeta(key2)).toEqual({ v: 1 });
|
||||
expect(await storage.getItem(key3)).toBeNull();
|
||||
expect(await storage.getMeta(key3)).toEqual({});
|
||||
expect(await storage.getMeta(key2)).toEqual({});
|
||||
});
|
||||
|
||||
it('should remove multiple items', async () => {
|
||||
const item1 = storage.defineItem(`${storageArea}:one`);
|
||||
const item2 = storage.defineItem(`${storageArea}:two`);
|
||||
await fakeBrowser.storage[storageArea].set({
|
||||
one: 1,
|
||||
two: 2,
|
||||
});
|
||||
|
||||
await storage.removeItems([item1, item2]);
|
||||
|
||||
expect(await item1.getValue()).toBeNull();
|
||||
expect(await item2.getValue()).toBeNull();
|
||||
});
|
||||
|
||||
it('should remove multiple items and metadata when requested', async () => {
|
||||
const item1 = storage.defineItem(`${storageArea}:one`);
|
||||
const item2 = storage.defineItem(`${storageArea}:two`);
|
||||
await fakeBrowser.storage[storageArea].set({
|
||||
one: 1,
|
||||
one$: { v: 1 },
|
||||
two: 2,
|
||||
two$: { v: 1 },
|
||||
});
|
||||
|
||||
await storage.removeItems([
|
||||
item1,
|
||||
{ item: item2, options: { removeMeta: true } },
|
||||
]);
|
||||
|
||||
expect(await item1.getValue()).toBeNull();
|
||||
expect(await item1.getMeta()).toEqual({ v: 1 });
|
||||
expect(await item2.getValue()).toBeNull();
|
||||
expect(await item2.getMeta()).toEqual({});
|
||||
});
|
||||
|
||||
it('should remove multiple items', async () => {
|
||||
const item1 = storage.defineItem(`${storageArea}:one`);
|
||||
const item2 = storage.defineItem(`${storageArea}:two`);
|
||||
|
||||
await fakeBrowser.storage.local.set({
|
||||
one: 1,
|
||||
two: 2,
|
||||
});
|
||||
|
||||
await storage.removeItems([item1, item2]);
|
||||
|
||||
expect(await item1.getValue()).toBeNull();
|
||||
expect(await item2.getValue()).toBeNull();
|
||||
});
|
||||
|
||||
it('should remove items using { item: WxtStorageItem, options?: RemoveItemOptions } objects', async () => {
|
||||
const item1 = storage.defineItem('local:item1');
|
||||
const item2 = storage.defineItem('session:item2');
|
||||
|
||||
await item1.setValue('value1');
|
||||
await item1.setMeta({ v: 1 });
|
||||
await item2.setValue('value2');
|
||||
await item2.setMeta({ v: 1 });
|
||||
|
||||
await storage.removeItems([
|
||||
{ item: item1, options: { removeMeta: true } },
|
||||
{ item: item2, options: { removeMeta: false } },
|
||||
]);
|
||||
|
||||
expect(await item1.getValue()).toBeNull();
|
||||
expect(await item1.getMeta()).toEqual({});
|
||||
expect(await item2.getValue()).toBeNull();
|
||||
expect(await item2.getMeta()).toEqual({ v: 1 });
|
||||
});
|
||||
|
||||
it('should handle a mix of different input types', async () => {
|
||||
const item1 = storage.defineItem('local:item1');
|
||||
const item2 = storage.defineItem('session:item2');
|
||||
const item3 = storage.defineItem('local:item3');
|
||||
|
||||
await item1.setValue('value1');
|
||||
await item2.setValue('value2');
|
||||
await item3.setValue('value3');
|
||||
|
||||
await storage.removeItems([
|
||||
'local:item1',
|
||||
item2,
|
||||
{ key: 'local:item3', options: { removeMeta: true } },
|
||||
]);
|
||||
|
||||
expect(await storage.getItem('local:item1')).toBeNull();
|
||||
expect(await item2.getValue()).toBeNull();
|
||||
expect(await item3.getValue()).toBeNull();
|
||||
expect(await item3.getMeta()).toEqual({});
|
||||
});
|
||||
|
||||
it('should not throw an error when removing non-existent items', async () => {
|
||||
const item = storage.defineItem('local:non_existent');
|
||||
|
||||
await expect(storage.removeItems([item])).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -400,6 +644,22 @@ describe('Storage Utils', () => {
|
||||
},
|
||||
);
|
||||
|
||||
describe('Invalid storage areas', () => {
|
||||
it('should not accept keys without a valid storage area prefix', async () => {
|
||||
// @ts-expect-error
|
||||
await storage.getItem('test').catch(() => {});
|
||||
// @ts-expect-error
|
||||
await storage.getItem('loca:test').catch(() => {});
|
||||
});
|
||||
|
||||
it('should throw an error when using an invalid storage area', async () => {
|
||||
// @ts-expect-error: Test passes if there is a type error here
|
||||
await expect(storage.getItem('invalidArea:key')).rejects.toThrow(
|
||||
'Invalid area',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineItem', () => {
|
||||
describe('versioning', () => {
|
||||
it('should migrate values to the latest when a version upgrade is detected', async () => {
|
||||
@@ -553,6 +813,28 @@ describe('Storage Utils', () => {
|
||||
'Version downgrade detected (v2 -> v1) for "local:count"',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error when defining an item with an invalid version', () => {
|
||||
expect(() => storage.defineItem('local:key', { version: 0 })).toThrow(
|
||||
'Storage item version cannot be less than 1',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle errors in migration functions', async () => {
|
||||
const cause = Error('Some error');
|
||||
const expectedError = new MigrationError('local:key', 2, { cause });
|
||||
const item = storage.defineItem<number>('local:key', {
|
||||
version: 3,
|
||||
migrations: {
|
||||
2: () => {
|
||||
throw cause;
|
||||
},
|
||||
},
|
||||
});
|
||||
await fakeBrowser.storage.local.set({ key: 1, key$: { v: 1 } });
|
||||
|
||||
await expect(item.migrate()).rejects.toThrow(expectedError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getValue', () => {
|
||||
@@ -872,6 +1154,24 @@ describe('Storage Utils', () => {
|
||||
|
||||
await expect(storage.getItem('local:test')).resolves.toBe(expected);
|
||||
});
|
||||
|
||||
it("should re-initialize a value on the next call to getValue if it's been removed", async () => {
|
||||
const init = vi.fn().mockImplementation(Math.random);
|
||||
const item = storage.defineItem<number>('local:key', { init });
|
||||
await waitForInit();
|
||||
|
||||
await item.removeValue();
|
||||
// Make sure it's actually blank before running the test
|
||||
expect(await chrome.storage.local.get()).toEqual({});
|
||||
init.mockClear();
|
||||
|
||||
const [value1, value2] = await Promise.all([
|
||||
item.getValue(),
|
||||
item.getValue(),
|
||||
]);
|
||||
expect(init).toBeCalledTimes(1);
|
||||
expect(value1).toBe(value2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('types', () => {
|
||||
@@ -896,12 +1196,173 @@ describe('Storage Utils', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('types', () => {
|
||||
it('should not accept keys without a valid storage area prefix', async () => {
|
||||
// @ts-expect-error: Test passes if there is a type error here
|
||||
await storage.getItem('test').catch(() => {});
|
||||
// @ts-expect-error
|
||||
await storage.getItem('loca:test').catch(() => {});
|
||||
describe('Multiple Storage Areas', () => {
|
||||
describe('getItems', () => {
|
||||
it('should get the values of multiple storage items efficiently', async () => {
|
||||
const item1 = storage.defineItem<number>('local:item1');
|
||||
const item2 = storage.defineItem<string>('session:item2');
|
||||
const item3 = storage.defineItem<boolean>('local:item3');
|
||||
|
||||
await item1.setValue(42);
|
||||
await item2.setValue('hello');
|
||||
await item3.setValue(false);
|
||||
|
||||
const localGetSpy = vi.spyOn(fakeBrowser.storage.local, 'get');
|
||||
const sessionGetSpy = vi.spyOn(fakeBrowser.storage.session, 'get');
|
||||
|
||||
const values = await storage.getItems([item1, item2, item3]);
|
||||
|
||||
expect(values).toEqual([
|
||||
{
|
||||
key: 'local:item1',
|
||||
value: 42,
|
||||
},
|
||||
{
|
||||
key: 'session:item2',
|
||||
value: 'hello',
|
||||
},
|
||||
{
|
||||
key: 'local:item3',
|
||||
value: false,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(localGetSpy).toBeCalledTimes(1);
|
||||
expect(localGetSpy).toBeCalledWith(['item1', 'item3']);
|
||||
expect(sessionGetSpy).toBeCalledTimes(1);
|
||||
expect(sessionGetSpy).toBeCalledWith(['item2']);
|
||||
});
|
||||
|
||||
it('should return values in the same order as input', async () => {
|
||||
const item1 = storage.defineItem<number>('local:item1');
|
||||
const item2 = storage.defineItem<string>('session:item2');
|
||||
const item3 = storage.defineItem<boolean>('local:item3');
|
||||
|
||||
expect(await storage.getItems([item1, item2, item3])).toEqual([
|
||||
{ key: item1.key, value: null },
|
||||
{ key: item2.key, value: null },
|
||||
{ key: item3.key, value: null },
|
||||
]);
|
||||
expect(await storage.getItems([item3, item2, item1])).toEqual([
|
||||
{ key: item3.key, value: null },
|
||||
{ key: item2.key, value: null },
|
||||
{ key: item1.key, value: null },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMetas', () => {
|
||||
it('should get the metadata of multiple storage items efficiently', async () => {
|
||||
const item1 = storage.defineItem<number, { v: number }>('local:item1');
|
||||
const item2 = storage.defineItem<string, { date: number }>(
|
||||
'session:item2',
|
||||
);
|
||||
const item3 = storage.defineItem<boolean, { flag: boolean }>(
|
||||
'local:item3',
|
||||
);
|
||||
|
||||
await item1.setMeta({ v: 1 });
|
||||
await item2.setMeta({ date: 1234567890 });
|
||||
// item3 has no meta
|
||||
|
||||
const localGetSpy = vi.spyOn(fakeBrowser.storage.local, 'get');
|
||||
const sessionGetSpy = vi.spyOn(fakeBrowser.storage.session, 'get');
|
||||
|
||||
const metas = await storage.getMetas([item1, item2, item3]);
|
||||
|
||||
expect(metas).toEqual([
|
||||
{ key: item1.key, meta: { v: 1 } },
|
||||
{ key: item2.key, meta: { date: 1234567890 } },
|
||||
{ key: item3.key, meta: {} },
|
||||
]);
|
||||
|
||||
expect(localGetSpy).toBeCalledTimes(1);
|
||||
expect(localGetSpy).toBeCalledWith(['item1$', 'item3$']);
|
||||
expect(sessionGetSpy).toBeCalledTimes(1);
|
||||
expect(sessionGetSpy).toBeCalledWith(['item2$']);
|
||||
});
|
||||
|
||||
it('should return the metadata in the same order as input', async () => {
|
||||
const item1 = storage.defineItem<number, { v: number }>('local:item1');
|
||||
const item2 = storage.defineItem<string, { date: number }>(
|
||||
'session:item2',
|
||||
);
|
||||
const item3 = storage.defineItem<boolean, { flag: boolean }>(
|
||||
'local:item3',
|
||||
);
|
||||
|
||||
expect(await storage.getMetas([item1, item2, item3])).toEqual([
|
||||
{ key: item1.key, meta: {} },
|
||||
{ key: item2.key, meta: {} },
|
||||
{ key: item3.key, meta: {} },
|
||||
]);
|
||||
expect(await storage.getMetas([item3, item2, item1])).toEqual([
|
||||
{ key: item3.key, meta: {} },
|
||||
{ key: item2.key, meta: {} },
|
||||
{ key: item1.key, meta: {} },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setItems', () => {
|
||||
it('should set the values of multiple storage items efficiently', async () => {
|
||||
const item1 = storage.defineItem<number>('local:item1');
|
||||
const value1 = 100;
|
||||
const item2 = storage.defineItem<string>('session:item2');
|
||||
const value2 = 'test';
|
||||
const item3 = storage.defineItem<boolean>('local:item3');
|
||||
const value3 = true;
|
||||
|
||||
const localSetSpy = vi.spyOn(fakeBrowser.storage.local, 'set');
|
||||
const sessionSetSpy = vi.spyOn(fakeBrowser.storage.session, 'set');
|
||||
|
||||
await storage.setItems([
|
||||
{ item: item1, value: value1 },
|
||||
{ item: item2, value: value2 },
|
||||
{ item: item3, value: value3 },
|
||||
]);
|
||||
|
||||
expect(localSetSpy).toBeCalledTimes(1);
|
||||
expect(localSetSpy).toBeCalledWith({ item1: value1, item3: value3 });
|
||||
expect(sessionSetSpy).toBeCalledTimes(1);
|
||||
expect(sessionSetSpy).toBeCalledWith({ item2: value2 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('setMetas', () => {
|
||||
it('should set metadata efficiently', async () => {
|
||||
const item1 = storage.defineItem<number, { v: number }>('local:one');
|
||||
const item2 = storage.defineItem<string, { v: number }>('session:two');
|
||||
const item3 = storage.defineItem<boolean, { v: number }>('local:three');
|
||||
await waitForInit();
|
||||
|
||||
const localGetSpy = vi.spyOn(fakeBrowser.storage.local, 'get');
|
||||
const sessionGetSpy = vi.spyOn(fakeBrowser.storage.session, 'get');
|
||||
const localSetSpy = vi.spyOn(fakeBrowser.storage.local, 'set');
|
||||
const sessionSetSpy = vi.spyOn(fakeBrowser.storage.session, 'set');
|
||||
|
||||
await storage.setMetas([
|
||||
{ item: item1, meta: { v: 1 } },
|
||||
{ item: item2, meta: { v: 2 } },
|
||||
{ item: item3, meta: { v: 3 } },
|
||||
]);
|
||||
|
||||
console.log(localGetSpy.mock.calls);
|
||||
expect(localGetSpy).toBeCalledTimes(1);
|
||||
expect(localGetSpy).toBeCalledWith(['one$', 'three$']);
|
||||
expect(sessionGetSpy).toBeCalledTimes(1);
|
||||
expect(sessionGetSpy).toBeCalledWith(['two$']);
|
||||
|
||||
expect(localSetSpy).toBeCalledTimes(1);
|
||||
expect(localSetSpy).toBeCalledWith({
|
||||
one$: { v: 1 },
|
||||
three$: { v: 3 },
|
||||
});
|
||||
expect(sessionSetSpy).toBeCalledTimes(1);
|
||||
expect(sessionSetSpy).toBeCalledWith({
|
||||
two$: { v: 2 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a nullable type when getItem is called without a fallback', async () => {
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"verbatimModuleSyntax": true,
|
||||
"types": ["chrome"]
|
||||
},
|
||||
"exclude": ["node_modules/**", "dist/**"]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defineProject } from 'vitest/config';
|
||||
|
||||
export default defineProject({
|
||||
test: {
|
||||
mockReset: true,
|
||||
restoreMocks: true,
|
||||
setupFiles: ['vitest.setup.ts'],
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
import { fakeBrowser } from '@webext-core/fake-browser';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
vi.stubGlobal('chrome', fakeBrowser);
|
||||
@@ -41,11 +41,11 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.4.5",
|
||||
"oxlint": "^0.9.9",
|
||||
"publint": "^0.2.11",
|
||||
"typescript": "^5.6.2",
|
||||
"oxlint": "^0.11.1",
|
||||
"publint": "^0.2.12",
|
||||
"typescript": "^5.6.3",
|
||||
"unbuild": "^2.0.0",
|
||||
"unocss": "^0.63.3",
|
||||
"unocss": "^0.64.0",
|
||||
"wxt": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -22,15 +22,15 @@
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chrome": "^0.0.269",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/chrome": "^0.0.280",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@wxt-dev/auto-icons": "workspace:*",
|
||||
"@wxt-dev/unocss": "workspace:*",
|
||||
"sass": "^1.79.4",
|
||||
"typescript": "^5.6.2",
|
||||
"unocss": "^0.63.3",
|
||||
"vitest": "^2.1.2",
|
||||
"sass": "^1.80.7",
|
||||
"typescript": "^5.6.3",
|
||||
"unocss": "^0.64.0",
|
||||
"vitest": "^2.1.4",
|
||||
"vitest-plugin-random-seed": "^1.1.0",
|
||||
"wxt": "workspace:*"
|
||||
},
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -1,5 +1,66 @@
|
||||
# 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)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- **storage:** Support storage items in batch functions ([#990](https://github.com/wxt-dev/wxt/pull/990))
|
||||
- Automatically disable 'Show warning about Self-XSS when pasing code' in new chrome ([#1159](https://github.com/wxt-dev/wxt/pull/1159))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Throw when config file does not exist ([#1156](https://github.com/wxt-dev/wxt/pull/1156))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Cleanup typos and broken links ([bb5ea34](https://github.com/wxt-dev/wxt/commit/bb5ea34))
|
||||
- Fix typo in `popup` and `options` EntrypointOptions ([#1121](https://github.com/wxt-dev/wxt/pull/1121))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **deps:** Upgrade all non-major dependencies ([#1164](https://github.com/wxt-dev/wxt/pull/1164))
|
||||
- **deps:** Bump dev and non-breaking major dependencies ([#1167](https://github.com/wxt-dev/wxt/pull/1167))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](http://github.com/aklinker1))
|
||||
- Rxliuli ([@rxliuli](http://github.com/rxliuli))
|
||||
- Kongmoumou ([@kongmoumou](http://github.com/kongmoumou))
|
||||
- Florian Metz ([@Timeraa](http://github.com/Timeraa))
|
||||
- Bread Grocery <breadgrocery@gmail.com>
|
||||
|
||||
## v0.19.13
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.12...wxt-v0.19.13)
|
||||
@@ -2578,4 +2639,4 @@ Initial release of WXT. Full support for production builds and initial toolkit f
|
||||
### 🤖 CI
|
||||
|
||||
- Create validation workflow ([#12](https://github.com/wxt-dev/wxt/pull/12))
|
||||
- Create release workflow ([#13](https://github.com/wxt-dev/wxt/pull/13))
|
||||
- Create release workflow ([#13](https://github.com/wxt-dev/wxt/pull/13))
|
||||
@@ -8,6 +8,7 @@ exports[`Auto Imports > eslintrc > "enabled: 8" should output a JSON config file
|
||||
"ContentScriptContext": true,
|
||||
"InvalidMatchPattern": true,
|
||||
"MatchPattern": true,
|
||||
"MigrationError": true,
|
||||
"browser": true,
|
||||
"createIframeUi": true,
|
||||
"createIntegratedUi": true,
|
||||
@@ -34,6 +35,7 @@ const globals = {
|
||||
"ContentScriptContext": true,
|
||||
"InvalidMatchPattern": true,
|
||||
"MatchPattern": true,
|
||||
"MigrationError": true,
|
||||
"browser": true,
|
||||
"createIframeUi": true,
|
||||
"createIntegratedUi": true,
|
||||
@@ -68,6 +70,7 @@ exports[`Auto Imports > eslintrc > "enabled: true" should output a JSON config f
|
||||
"ContentScriptContext": true,
|
||||
"InvalidMatchPattern": true,
|
||||
"MatchPattern": true,
|
||||
"MigrationError": true,
|
||||
"browser": true,
|
||||
"createIframeUi": true,
|
||||
"createIntegratedUi": true,
|
||||
@@ -95,6 +98,7 @@ exports[`Auto Imports > eslintrc > should allow customizing the output 1`] = `
|
||||
"ContentScriptContext": "readonly",
|
||||
"InvalidMatchPattern": "readonly",
|
||||
"MatchPattern": "readonly",
|
||||
"MigrationError": "readonly",
|
||||
"browser": "readonly",
|
||||
"createIframeUi": "readonly",
|
||||
"createIntegratedUi": "readonly",
|
||||
|
||||
@@ -20,6 +20,7 @@ describe('Auto Imports', () => {
|
||||
const ContentScriptContext: typeof import('wxt/client')['ContentScriptContext']
|
||||
const InvalidMatchPattern: typeof import('wxt/sandbox')['InvalidMatchPattern']
|
||||
const MatchPattern: typeof import('wxt/sandbox')['MatchPattern']
|
||||
const MigrationError: typeof import('wxt/storage')['MigrationError']
|
||||
const browser: typeof import('wxt/browser')['browser']
|
||||
const createIframeUi: typeof import('wxt/client')['createIframeUi']
|
||||
const createIntegratedUi: typeof import('wxt/client')['createIntegratedUi']
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -157,4 +157,18 @@ describe('User Config', () => {
|
||||
await project.fileExists('.output/test-chrome-mv3-development-dev-build'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw error when config file not exist', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'src/entrypoints/background.ts',
|
||||
`export default defineBackground(
|
||||
() => console.log('Hello background'),
|
||||
);`,
|
||||
);
|
||||
|
||||
await expect(
|
||||
project.build({ configFile: 'foo.config.ts' }),
|
||||
).rejects.toThrowError(/not found/);
|
||||
});
|
||||
});
|
||||
|
||||
+17
-17
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wxt",
|
||||
"type": "module",
|
||||
"version": "0.19.13",
|
||||
"version": "0.19.16",
|
||||
"description": "Next gen framework for developing web extensions",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -82,19 +82,19 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@aklinker1/rollup-plugin-visualizer": "5.12.0",
|
||||
"@types/chrome": "^0.0.269",
|
||||
"@types/webextension-polyfill": "^0.10.7",
|
||||
"@types/chrome": "^0.0.280",
|
||||
"@types/webextension-polyfill": "^0.12.1",
|
||||
"@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",
|
||||
"chokidar": "^3.6.0",
|
||||
"ci-info": "^4.0.0",
|
||||
"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",
|
||||
@@ -110,40 +110,40 @@
|
||||
"linkedom": "^0.18.5",
|
||||
"magicast": "^0.3.5",
|
||||
"minimatch": "^10.0.1",
|
||||
"nano-spawn": "^0.1.0",
|
||||
"nano-spawn": "^0.2.0",
|
||||
"normalize-path": "^3.0.0",
|
||||
"nypm": "^0.3.12",
|
||||
"ohash": "^1.1.4",
|
||||
"open": "^10.1.0",
|
||||
"ora": "^8.1.0",
|
||||
"ora": "^8.1.1",
|
||||
"perfect-debounce": "^1.0.0",
|
||||
"picocolors": "^1.1.0",
|
||||
"picocolors": "^1.1.1",
|
||||
"prompts": "^2.4.2",
|
||||
"publish-browser-extension": "^2.2.2",
|
||||
"scule": "^1.3.0",
|
||||
"unimport": "^3.13.1",
|
||||
"vite": "^5.4.8",
|
||||
"vite-node": "^2.1.2",
|
||||
"vite": "^5.4.11",
|
||||
"vite-node": "^2.1.4",
|
||||
"web-ext-run": "^0.2.1",
|
||||
"webextension-polyfill": "^0.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.4.5",
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@faker-js/faker": "^9.2.0",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/lodash.merge": "^4.6.9",
|
||||
"@types/node": "^20.16.10",
|
||||
"@types/node": "^20.17.6",
|
||||
"@types/normalize-path": "^3.0.2",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"extract-zip": "^2.0.1",
|
||||
"happy-dom": "^14.12.3",
|
||||
"happy-dom": "^15.11.4",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"oxlint": "^0.9.9",
|
||||
"publint": "^0.2.11",
|
||||
"oxlint": "^0.11.1",
|
||||
"publint": "^0.2.12",
|
||||
"tsx": "4.15.7",
|
||||
"typescript": "^5.6.2",
|
||||
"typescript": "^5.6.3",
|
||||
"unbuild": "^2.0.0",
|
||||
"vitest": "^2.1.2",
|
||||
"vitest": "^2.1.4",
|
||||
"vitest-plugin-random-seed": "^1.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -80,7 +80,6 @@ export type {
|
||||
Theme,
|
||||
TopSites,
|
||||
Types,
|
||||
Urlbar,
|
||||
UserScripts,
|
||||
WebNavigation,
|
||||
WebRequest,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -96,7 +96,7 @@ export function createIframeUi<TMounted>(
|
||||
*
|
||||
* > This function is async because it has to load the CSS via a network call.
|
||||
*
|
||||
* @see https://wxt.dev/guide/essentials/content-scripts.html#shadowroot
|
||||
* @see https://wxt.dev/guide/essentials/content-scripts.html#shadow-root
|
||||
*/
|
||||
export async function createShadowRootUi<TMounted>(
|
||||
ctx: ContentScriptContext,
|
||||
|
||||
@@ -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(' | '),
|
||||
|
||||
@@ -53,6 +53,9 @@ export async function resolveConfig(
|
||||
esmResolve: true,
|
||||
},
|
||||
});
|
||||
if (inlineConfig.configFile && metadata.layers?.length === 0) {
|
||||
throw Error(`Config file "${inlineConfig.configFile}" not found`);
|
||||
}
|
||||
userConfig = loadedConfig ?? {};
|
||||
userConfigMetadata = metadata;
|
||||
}
|
||||
|
||||
@@ -50,7 +50,10 @@ export function createWebExtRunner(): ExtensionRunner {
|
||||
wxtUserConfig?.chromiumPref,
|
||||
DEFAULT_CHROMIUM_PREFS,
|
||||
),
|
||||
args: wxtUserConfig?.chromiumArgs,
|
||||
args: [
|
||||
'--unsafely-disable-devtools-self-xss-warnings',
|
||||
...(wxtUserConfig?.chromiumArgs ?? []),
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
|
||||
@@ -228,7 +228,7 @@ async function getPopupEntrypoint(
|
||||
const options = await getHtmlEntrypointOptions<PopupEntrypointOptions>(
|
||||
info,
|
||||
{
|
||||
browserStyle: 'browse_style',
|
||||
browserStyle: 'browser_style',
|
||||
exclude: 'exclude',
|
||||
include: 'include',
|
||||
defaultIcon: 'default_icon',
|
||||
@@ -262,7 +262,7 @@ async function getOptionsEntrypoint(
|
||||
const options = await getHtmlEntrypointOptions<OptionsEntrypointOptions>(
|
||||
info,
|
||||
{
|
||||
browserStyle: 'browse_style',
|
||||
browserStyle: 'browser_style',
|
||||
chromeStyle: 'chrome_style',
|
||||
exclude: 'exclude',
|
||||
include: 'include',
|
||||
|
||||
@@ -29,7 +29,6 @@ export function hashContentScriptOptions(
|
||||
match_about_blank: false,
|
||||
run_at: 'document_idle',
|
||||
all_frames: false,
|
||||
// @ts-expect-error - not in type
|
||||
match_origin_as_fallback: false,
|
||||
world: 'ISOLATED',
|
||||
...simplifiedOptions,
|
||||
@@ -62,7 +61,6 @@ export function mapWxtOptionsToContentScript(
|
||||
css,
|
||||
js,
|
||||
|
||||
// @ts-expect-error: untyped chrome options
|
||||
match_origin_as_fallback: options.matchOriginAsFallback,
|
||||
world: options.world,
|
||||
};
|
||||
@@ -80,7 +78,6 @@ export function mapWxtOptionsToRegisteredContentScript(
|
||||
runAt: options.runAt,
|
||||
js,
|
||||
css,
|
||||
// @ts-expect-error: Chrome accepts this, not typed in webextension-polyfill (https://developer.chrome.com/docs/extensions/reference/scripting/#type-RegisteredContentScript)
|
||||
world: options.world,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
-719
@@ -1,721 +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 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));
|
||||
const newFields = { ...existingFields };
|
||||
Object.entries(properties).forEach(([key, value]) => {
|
||||
if (value == null) {
|
||||
delete newFields[key];
|
||||
} else {
|
||||
newFields[key] = value;
|
||||
}
|
||||
});
|
||||
await driver.setItem(metaKey, newFields);
|
||||
};
|
||||
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>();
|
||||
keys.forEach((key) => {
|
||||
let keyStr: StorageItemKey;
|
||||
let opts: GetItemOptions<any> | undefined;
|
||||
if (typeof key === 'string') {
|
||||
keyStr = key;
|
||||
} else {
|
||||
keyStr = key.key;
|
||||
opts = key.options;
|
||||
}
|
||||
const { driverArea, driverKey } = resolveKey(keyStr);
|
||||
const keys = areaToKeyMap.get(driverArea) ?? [];
|
||||
areaToKeyMap.set(driverArea, keys.concat(driverKey));
|
||||
keyToOptsMap.set(keyStr, opts);
|
||||
});
|
||||
|
||||
const results = await Promise.all(
|
||||
Array.from(areaToKeyMap.entries()).map(async ([driverArea, keys]) => {
|
||||
const driverResults = await drivers[driverArea].getItems(keys);
|
||||
return driverResults.map((driverResult) => {
|
||||
const key = `${driverArea}:${driverResult.key}` as StorageItemKey;
|
||||
const opts = keyToOptsMap.get(key);
|
||||
const value = getValueOrFallback(
|
||||
driverResult.value,
|
||||
opts?.fallback ?? opts?.defaultValue,
|
||||
);
|
||||
return { key, value };
|
||||
});
|
||||
}),
|
||||
);
|
||||
return results.flat();
|
||||
},
|
||||
getMeta: async (key) => {
|
||||
const { driver, driverKey } = resolveKey(key);
|
||||
return await getMeta(driver, driverKey);
|
||||
},
|
||||
setItem: async (key, value) => {
|
||||
const { driver, driverKey } = resolveKey(key);
|
||||
await setItem(driver, driverKey, value);
|
||||
},
|
||||
setItems: async (values) => {
|
||||
const areaToKeyValueMap = new Map<
|
||||
StorageArea,
|
||||
Array<{ key: string; value: any }>
|
||||
>();
|
||||
values.forEach(({ key, value }) => {
|
||||
const { driverArea, driverKey } = resolveKey(key);
|
||||
const values = areaToKeyValueMap.get(driverArea) ?? [];
|
||||
areaToKeyValueMap.set(
|
||||
driverArea,
|
||||
values.concat({ key: driverKey, value }),
|
||||
);
|
||||
});
|
||||
await Promise.all(
|
||||
Array.from(areaToKeyValueMap.entries()).map(
|
||||
async ([driverArea, values]) => {
|
||||
const driver = getDriver(driverArea);
|
||||
await driver.setItems(values);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
setMeta: async (key, properties) => {
|
||||
const { driver, driverKey } = resolveKey(key);
|
||||
await setMeta(driver, driverKey, properties);
|
||||
},
|
||||
removeItem: async (key, opts) => {
|
||||
const { driver, driverKey } = resolveKey(key);
|
||||
await removeItem(driver, driverKey, opts);
|
||||
},
|
||||
removeItems: async (keys) => {
|
||||
const areaToKeysMap = new Map<StorageArea, string[]>();
|
||||
keys.forEach((key) => {
|
||||
let keyStr: StorageItemKey;
|
||||
let opts: RemoveItemOptions | undefined;
|
||||
if (typeof key === 'string') {
|
||||
keyStr = key;
|
||||
} else {
|
||||
keyStr = key.key;
|
||||
opts = key.options;
|
||||
}
|
||||
const { driverArea, driverKey } = resolveKey(keyStr);
|
||||
const areaKeys = areaToKeysMap.get(driverArea) ?? [];
|
||||
areaKeys.push(driverKey);
|
||||
if (opts?.removeMeta) {
|
||||
areaKeys.push(getMetaKey(driverKey));
|
||||
}
|
||||
areaToKeysMap.set(driverArea, areaKeys);
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
Array.from(areaToKeysMap.entries()).map(async ([driverArea, keys]) => {
|
||||
const driver = getDriver(driverArea);
|
||||
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) {
|
||||
migratedValue =
|
||||
(await migrations?.[migrateToVersion]?.(migratedValue)) ??
|
||||
migratedValue;
|
||||
}
|
||||
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 {
|
||||
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];
|
||||
},
|
||||
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];
|
||||
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 | { 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>;
|
||||
/**
|
||||
* 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 }>): 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>;
|
||||
/**
|
||||
* 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 | { key: StorageItemKey; 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>,
|
||||
> {
|
||||
/**
|
||||
* @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 * from '@wxt-dev/storage';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
Generated
+1105
-772
File diff suppressed because it is too large
Load Diff
@@ -7,5 +7,7 @@ export default {
|
||||
'typedoc-vitepress-theme',
|
||||
// Manually manage version so a single version is used:
|
||||
'esbuild',
|
||||
// Maintained manually to match min-node version
|
||||
'@types/node',
|
||||
],
|
||||
};
|
||||
|
||||
@@ -19,10 +19,11 @@
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/chrome": "^0.0.280",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@wxt-dev/module-react": "^1.1.1",
|
||||
"typescript": "^5.6.2",
|
||||
"wxt": "^0.19.11"
|
||||
"typescript": "^5.6.3",
|
||||
"wxt": "^0.19.13"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,11 +15,12 @@
|
||||
"postinstall": "wxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"solid-js": "^1.9.1"
|
||||
"solid-js": "^1.9.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chrome": "^0.0.280",
|
||||
"@wxt-dev/module-solid": "^1.1.2",
|
||||
"typescript": "^5.6.2",
|
||||
"wxt": "^0.19.11"
|
||||
"typescript": "^5.6.3",
|
||||
"wxt": "^0.19.13"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,11 +16,12 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/svelte": "^5.0.4",
|
||||
"@wxt-dev/module-svelte": "^1.0.1",
|
||||
"svelte": "^4.2.19",
|
||||
"svelte-check": "^3.8.6",
|
||||
"tslib": "^2.7.0",
|
||||
"typescript": "^5.6.2",
|
||||
"wxt": "^0.19.11"
|
||||
"@types/chrome": "^0.0.280",
|
||||
"@wxt-dev/module-svelte": "^2.0.0",
|
||||
"svelte": "^5.1.16",
|
||||
"svelte-check": "^4.0.7",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.6.3",
|
||||
"wxt": "^0.19.13"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
"postinstall": "wxt prepare"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.6.2",
|
||||
"wxt": "^0.19.11"
|
||||
"@types/chrome": "^0.0.280",
|
||||
"typescript": "^5.6.3",
|
||||
"wxt": "^0.19.13"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,12 +15,13 @@
|
||||
"postinstall": "wxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.5.11"
|
||||
"vue": "^3.5.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chrome": "^0.0.280",
|
||||
"@wxt-dev/module-vue": "^1.0.1",
|
||||
"typescript": "^5.6.2",
|
||||
"vue-tsc": "^2.1.6",
|
||||
"wxt": "^0.19.11"
|
||||
"typescript": "^5.6.3",
|
||||
"vue-tsc": "^2.1.10",
|
||||
"wxt": "^0.19.13"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user