webext-storage 1.2.2 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/distribution/index.d.ts +2 -0
- package/distribution/index.js +2 -0
- package/distribution/storage-item-map.d.ts +22 -0
- package/distribution/storage-item-map.js +63 -0
- package/distribution/storage-item.d.ts +10 -7
- package/distribution/storage-item.test-d.d.ts +1 -0
- package/distribution/storage-item.test-d.js +53 -0
- package/package.json +12 -16
- package/readme.md +28 -41
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type StorageItemMapOptions<T> = {
|
|
2
|
+
area?: chrome.storage.AreaName;
|
|
3
|
+
defaultValue?: T;
|
|
4
|
+
};
|
|
5
|
+
export declare class StorageItemMap<
|
|
6
|
+
/** Only specify this if you don't have a default value */
|
|
7
|
+
Base,
|
|
8
|
+
/** The return type will be undefined unless you provide a default value */
|
|
9
|
+
Return = Base | undefined> {
|
|
10
|
+
readonly prefix: `${string}:::`;
|
|
11
|
+
readonly areaName: chrome.storage.AreaName;
|
|
12
|
+
readonly defaultValue?: Return;
|
|
13
|
+
constructor(key: string, { area, defaultValue, }?: StorageItemMapOptions<Exclude<Return, undefined>>);
|
|
14
|
+
has: (secondaryKey: string) => Promise<boolean>;
|
|
15
|
+
delete: (secondaryKey: string) => Promise<void>;
|
|
16
|
+
get: (secondaryKey: string) => Promise<Return>;
|
|
17
|
+
set: (secondaryKey: string, value: Exclude<Return, undefined>) => Promise<void>;
|
|
18
|
+
remove: (secondaryKey: string) => Promise<void>;
|
|
19
|
+
onChanged(callback: (key: string, value: Exclude<Return, undefined>) => void, signal?: AbortSignal): void;
|
|
20
|
+
private getRawStorageKey;
|
|
21
|
+
private getSecondaryStorageKey;
|
|
22
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import chromeP from 'webext-polyfill-kinda';
|
|
2
|
+
export class StorageItemMap {
|
|
3
|
+
prefix;
|
|
4
|
+
areaName;
|
|
5
|
+
defaultValue;
|
|
6
|
+
constructor(key, { area = 'local', defaultValue, } = {}) {
|
|
7
|
+
this.prefix = `${key}:::`;
|
|
8
|
+
this.areaName = area;
|
|
9
|
+
this.defaultValue = defaultValue;
|
|
10
|
+
}
|
|
11
|
+
has = async (secondaryKey) => {
|
|
12
|
+
const rawStorageKey = this.getRawStorageKey(secondaryKey);
|
|
13
|
+
const result = await chromeP.storage[this.areaName].get(rawStorageKey);
|
|
14
|
+
return Object.hasOwn(result, secondaryKey);
|
|
15
|
+
};
|
|
16
|
+
delete = async (secondaryKey) => {
|
|
17
|
+
const rawStorageKey = this.getRawStorageKey(secondaryKey);
|
|
18
|
+
await chromeP.storage[this.areaName].remove(rawStorageKey);
|
|
19
|
+
};
|
|
20
|
+
get = async (secondaryKey) => {
|
|
21
|
+
const rawStorageKey = this.getRawStorageKey(secondaryKey);
|
|
22
|
+
const result = await chromeP.storage[this.areaName].get(rawStorageKey);
|
|
23
|
+
if (!Object.hasOwn(result, rawStorageKey)) {
|
|
24
|
+
return this.defaultValue;
|
|
25
|
+
}
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return -- Assumes the user never uses the Storage API directly for this key
|
|
27
|
+
return result[rawStorageKey];
|
|
28
|
+
};
|
|
29
|
+
set = async (secondaryKey, value) => {
|
|
30
|
+
const rawStorageKey = this.getRawStorageKey(secondaryKey);
|
|
31
|
+
await chromeP.storage[this.areaName].set({ [rawStorageKey]: value });
|
|
32
|
+
};
|
|
33
|
+
remove = async (secondaryKey) => {
|
|
34
|
+
const rawStorageKey = this.getRawStorageKey(secondaryKey);
|
|
35
|
+
await chromeP.storage[this.areaName].remove(rawStorageKey);
|
|
36
|
+
};
|
|
37
|
+
onChanged(callback, signal) {
|
|
38
|
+
const changeHandler = (changes, area) => {
|
|
39
|
+
if (area !== this.areaName) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
for (const rawKey of Object.keys(changes)) {
|
|
43
|
+
const secondaryKey = this.getSecondaryStorageKey(rawKey);
|
|
44
|
+
if (secondaryKey) {
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument -- Assumes the user never uses the Storage API directly
|
|
46
|
+
callback(secondaryKey, changes[rawKey].newValue);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
chrome.storage.onChanged.addListener(changeHandler);
|
|
51
|
+
signal?.addEventListener('abort', () => {
|
|
52
|
+
chrome.storage.onChanged.removeListener(changeHandler);
|
|
53
|
+
}, {
|
|
54
|
+
once: true,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
getRawStorageKey(secondaryKey) {
|
|
58
|
+
return this.prefix + secondaryKey;
|
|
59
|
+
}
|
|
60
|
+
getSecondaryStorageKey(rawKey) {
|
|
61
|
+
return rawKey.startsWith(this.prefix) && rawKey.slice(this.prefix.length);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
/// <reference types="chrome" />
|
|
2
1
|
export type StorageItemOptions<T> = {
|
|
3
2
|
area?: chrome.storage.AreaName;
|
|
4
3
|
defaultValue?: T;
|
|
5
4
|
};
|
|
6
|
-
export declare class StorageItem<
|
|
5
|
+
export declare class StorageItem<
|
|
6
|
+
/** Only specify this if you don't have a default value */
|
|
7
|
+
Base,
|
|
8
|
+
/** The return type will be undefined unless you provide a default value */
|
|
9
|
+
Return = Base | undefined> {
|
|
7
10
|
readonly key: string;
|
|
8
11
|
readonly area: chrome.storage.AreaName;
|
|
9
|
-
readonly defaultValue?:
|
|
12
|
+
readonly defaultValue?: Return;
|
|
10
13
|
/** @deprecated Use `onChanged` instead */
|
|
11
|
-
onChange: (callback: (value:
|
|
12
|
-
constructor(key: string, { area, defaultValue, }?: StorageItemOptions<
|
|
14
|
+
onChange: (callback: (value: Exclude<Return, undefined>) => void, signal?: AbortSignal) => void;
|
|
15
|
+
constructor(key: string, { area, defaultValue, }?: StorageItemOptions<Exclude<Return, undefined>>);
|
|
13
16
|
get: () => Promise<Return>;
|
|
14
|
-
set: (value:
|
|
17
|
+
set: (value: Exclude<Return, undefined>) => Promise<void>;
|
|
15
18
|
remove: () => Promise<void>;
|
|
16
|
-
onChanged(callback: (value:
|
|
19
|
+
onChanged(callback: (value: Exclude<Return, undefined>) => void, signal?: AbortSignal): void;
|
|
17
20
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/ban-types */
|
|
2
|
+
/* eslint-disable no-new -- Type tests only */
|
|
3
|
+
import { expectType, expectNotAssignable, expectAssignable } from 'tsd';
|
|
4
|
+
import { StorageItem } from './storage-item.js';
|
|
5
|
+
new StorageItem('key', { area: 'local' });
|
|
6
|
+
new StorageItem('key', { area: 'sync' });
|
|
7
|
+
// No type, no default, return `unknown`
|
|
8
|
+
const unknownItem = new StorageItem('key');
|
|
9
|
+
expectAssignable(unknownItem.get());
|
|
10
|
+
await unknownItem.set(1);
|
|
11
|
+
await unknownItem.set(null);
|
|
12
|
+
// Explicit type, no default, return `T | undefined`
|
|
13
|
+
const objectNoDefault = new StorageItem('key');
|
|
14
|
+
expectType(objectNoDefault.get());
|
|
15
|
+
expectType(objectNoDefault.set({ name: 'new name' }));
|
|
16
|
+
// NonNullable from default
|
|
17
|
+
const stringDefault = new StorageItem('key', { defaultValue: 'SMASHING' });
|
|
18
|
+
expectAssignable(stringDefault.get());
|
|
19
|
+
expectNotAssignable(stringDefault.get());
|
|
20
|
+
expectType(stringDefault.get());
|
|
21
|
+
expectType(stringDefault.set('some string'));
|
|
22
|
+
// NonNullable from default, includes broader type as generic
|
|
23
|
+
// The second type parameter must be re-specified because TypeScript stops inferring it
|
|
24
|
+
// https://github.com/microsoft/TypeScript/issues/26242
|
|
25
|
+
const broadGeneric = new StorageItem('key', { defaultValue: { a: 1 } });
|
|
26
|
+
expectAssignable(broadGeneric.get());
|
|
27
|
+
// Allows null as a value via default value
|
|
28
|
+
const storeNull = new StorageItem('key', { defaultValue: null });
|
|
29
|
+
await storeNull.set(null);
|
|
30
|
+
expectType(storeNull.get());
|
|
31
|
+
// Allows null as a value type parameters
|
|
32
|
+
const storeSomeNull = new StorageItem('key');
|
|
33
|
+
await storeSomeNull.set(1);
|
|
34
|
+
await storeSomeNull.set(null);
|
|
35
|
+
expectType(storeSomeNull.get());
|
|
36
|
+
// @ts-expect-error Type is string
|
|
37
|
+
await stringDefault.set(1);
|
|
38
|
+
// @ts-expect-error Type is string
|
|
39
|
+
await stringDefault.set(true);
|
|
40
|
+
// @ts-expect-error Type is string
|
|
41
|
+
await stringDefault.set([true, 'string']);
|
|
42
|
+
// @ts-expect-error Type is string
|
|
43
|
+
await stringDefault.set({ wow: [true, 'string'] });
|
|
44
|
+
// @ts-expect-error Type is string
|
|
45
|
+
await stringDefault.set(1, { days: 1 });
|
|
46
|
+
stringDefault.onChanged(value => {
|
|
47
|
+
expectType(value);
|
|
48
|
+
});
|
|
49
|
+
objectNoDefault.onChanged(value => {
|
|
50
|
+
expectType(value);
|
|
51
|
+
});
|
|
52
|
+
// @ts-expect-error Don't allow mismatched types
|
|
53
|
+
new StorageItem('key', { defaultValue: 'SMASHING' });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webext-storage",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "A more usable typed storage API for Web Extensions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"browser",
|
|
@@ -21,17 +21,13 @@
|
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"author": "Federico Brigante <me@fregante.com> (https://fregante.com)",
|
|
23
23
|
"type": "module",
|
|
24
|
-
"exports": "./distribution/
|
|
25
|
-
"main": "./distribution/
|
|
26
|
-
"types": "./distribution/
|
|
27
|
-
"files": [
|
|
28
|
-
"distribution/storage-item.js",
|
|
29
|
-
"distribution/storage-item.d.ts"
|
|
30
|
-
],
|
|
24
|
+
"exports": "./distribution/index.js",
|
|
25
|
+
"main": "./distribution/index.js",
|
|
26
|
+
"types": "./distribution/index.d.ts",
|
|
31
27
|
"scripts": {
|
|
32
28
|
"build": "tsc",
|
|
33
29
|
"prepack": "tsc --sourceMap false",
|
|
34
|
-
"test": "tsc
|
|
30
|
+
"test": "tsc && xo && tsd && vitest run",
|
|
35
31
|
"test:watch": "vitest",
|
|
36
32
|
"watch": "tsc --watch"
|
|
37
33
|
},
|
|
@@ -47,15 +43,15 @@
|
|
|
47
43
|
"webext-polyfill-kinda": "^1.0.2"
|
|
48
44
|
},
|
|
49
45
|
"devDependencies": {
|
|
50
|
-
"@sindresorhus/tsconfig": "^
|
|
51
|
-
"@types/chrome": "^0.0.
|
|
52
|
-
"@types/sinon-chrome": "^2.2.
|
|
46
|
+
"@sindresorhus/tsconfig": "^7.0.0",
|
|
47
|
+
"@types/chrome": "^0.0.300",
|
|
48
|
+
"@types/sinon-chrome": "^2.2.15",
|
|
53
49
|
"jest-chrome": "^0.8.0",
|
|
54
50
|
"sinon-chrome": "^3.0.1",
|
|
55
|
-
"tsd": "^0.
|
|
56
|
-
"typescript": "^5.
|
|
57
|
-
"vitest": "^0.
|
|
58
|
-
"xo": "^0.
|
|
51
|
+
"tsd": "^0.31.2",
|
|
52
|
+
"typescript": "^5.7.3",
|
|
53
|
+
"vitest": "^3.0.4",
|
|
54
|
+
"xo": "^0.60.0"
|
|
59
55
|
},
|
|
60
56
|
"engines": {
|
|
61
57
|
"node": ">=20"
|
package/readme.md
CHANGED
|
@@ -5,27 +5,13 @@
|
|
|
5
5
|
|
|
6
6
|
> A more usable typed storage API for Web Extensions
|
|
7
7
|
|
|
8
|
-
- Browsers: Chrome, Firefox, and Safari
|
|
9
|
-
- Manifest: v2 and v3
|
|
10
|
-
- Permissions: `storage` or `unlimitedStorage`
|
|
11
|
-
- Context: They can be called from any context
|
|
12
|
-
|
|
13
8
|
**Sponsored by [PixieBrix](https://www.pixiebrix.com)** :tada:
|
|
14
9
|
|
|
15
|
-
`chrome.storage.local.get()` is very inconvenient to use and it
|
|
10
|
+
`chrome.storage.local.get()` is very inconvenient to use and it's not type-safe. This module provides a better API:
|
|
16
11
|
|
|
17
|
-
|
|
18
|
-
// Before
|
|
19
|
-
const storage = await chrome.storage.local.get('user-options');
|
|
20
|
-
const value = storage['user-options']; // The type is `any`
|
|
21
|
-
await chrome.storage.local.set({['user-options']: {color: 'red'}}); // Not type-checked
|
|
22
|
-
chrome.storage.onChanged.addListener((storageArea, change) => {
|
|
23
|
-
if (storageArea === 'local' && change['user-options']) { // Repetitive
|
|
24
|
-
console.log('New options', change['user-options'].newValue)
|
|
25
|
-
}
|
|
26
|
-
});
|
|
12
|
+
<details><summary>Comparison 💥</summary>
|
|
27
13
|
|
|
28
|
-
|
|
14
|
+
```ts
|
|
29
15
|
const options = new StorageItem<Record<string, string>>('user-options');
|
|
30
16
|
const value = await options.get(); // The type is `Record<string, string> | undefined`
|
|
31
17
|
await options.set({color: 'red'}) // Type-checked
|
|
@@ -34,47 +20,48 @@ options.onChanged(newValue => {
|
|
|
34
20
|
});
|
|
35
21
|
```
|
|
36
22
|
|
|
37
|
-
Why this is better:
|
|
38
|
-
|
|
39
23
|
- The storage item is defined in a single place, including its storageArea, its types and default value
|
|
40
|
-
- `get`
|
|
24
|
+
- `item.get()` returns the raw value instead of an object
|
|
41
25
|
- Every `get` and `set` operation is type-safe
|
|
42
26
|
- If you provide a `defaultValue`, the return type will not be ` | undefined`
|
|
43
27
|
- The `onChanged` example speaks for itself
|
|
44
28
|
|
|
29
|
+
Now compare it to the native API:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
const storage = await chrome.storage.local.get('user-options');
|
|
33
|
+
const value = storage['user-options']; // The type is `any`
|
|
34
|
+
await chrome.storage.local.set({['user-options']: {color: 'red'}}); // Not type-checked
|
|
35
|
+
chrome.storage.onChanged.addListener((storageArea, change) => {
|
|
36
|
+
if (storageArea === 'local' && change['user-options']) { // Repetitive
|
|
37
|
+
console.log('New options', change['user-options'].newValue)
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
</details>
|
|
43
|
+
|
|
45
44
|
## Install
|
|
46
45
|
|
|
47
46
|
```sh
|
|
48
47
|
npm install webext-storage
|
|
49
48
|
```
|
|
50
49
|
|
|
51
|
-
Or download the [standalone bundle](https://bundle.fregante.com/?pkg=webext-storage&name=
|
|
50
|
+
Or download the [standalone bundle](https://bundle.fregante.com/?pkg=webext-storage&name=window) to include in your `manifest.json`.
|
|
52
51
|
|
|
53
52
|
## Usage
|
|
54
53
|
|
|
55
|
-
|
|
56
|
-
import {StorageItem} from "webext-storage";
|
|
54
|
+
The package exports two classes:
|
|
57
55
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const username = new StorageItem('username', {defaultValue: 'admin'})
|
|
56
|
+
- [StorageItem](./source/storage-item.md) - Store a single value in storage
|
|
57
|
+
- [StorageItemMap](./source/storage-item-map.md) - Store multiple related values in storage with the same type, similar to `new Map()`
|
|
61
58
|
|
|
62
|
-
|
|
63
|
-
// Promise<void>
|
|
59
|
+
Support:
|
|
64
60
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// Promise<void>
|
|
70
|
-
|
|
71
|
-
await username.set({name: 'Ugo'});
|
|
72
|
-
// TypeScript Error: Argument of type '{ name: string; }' is not assignable to parameter of type 'string'.
|
|
73
|
-
|
|
74
|
-
username.onChanged(newName => {
|
|
75
|
-
console.log('The user’s new name is', newName);
|
|
76
|
-
});
|
|
77
|
-
```
|
|
61
|
+
- Browsers: Chrome, Firefox, and Safari
|
|
62
|
+
- Manifest: v2 and v3
|
|
63
|
+
- Permissions: `storage` or `unlimitedStorage`
|
|
64
|
+
- Context: They can be called from any context
|
|
78
65
|
|
|
79
66
|
## Related
|
|
80
67
|
|