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.
@@ -0,0 +1,2 @@
1
+ export * from './storage-item.js';
2
+ export * from './storage-item-map.js';
@@ -0,0 +1,2 @@
1
+ export * from './storage-item.js';
2
+ export * from './storage-item-map.js';
@@ -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<Base, Default = Base | undefined, Return = Default extends undefined ? Base : Default> {
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?: Default;
12
+ readonly defaultValue?: Return;
10
13
  /** @deprecated Use `onChanged` instead */
11
- onChange: (callback: (value: NonNullable<Default>) => void, signal?: AbortSignal) => void;
12
- constructor(key: string, { area, defaultValue, }?: StorageItemOptions<NonNullable<Default>>);
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: NonNullable<Default>) => Promise<void>;
17
+ set: (value: Exclude<Return, undefined>) => Promise<void>;
15
18
  remove: () => Promise<void>;
16
- onChanged(callback: (value: NonNullable<Default>) => void, signal?: AbortSignal): void;
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.2.2",
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/storage-item.js",
25
- "main": "./distribution/storage-item.js",
26
- "types": "./distribution/storage-item.d.ts",
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 --noEmit && xo && tsd && vitest run",
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": "^5.0.0",
51
- "@types/chrome": "^0.0.248",
52
- "@types/sinon-chrome": "^2.2.13",
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.29.0",
56
- "typescript": "^5.2.2",
57
- "vitest": "^0.34.6",
58
- "xo": "^0.56.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 does not provide type safety. This module provides a better API:
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
- ```ts
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
- // After
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` only is only `.get()` instead of the awkward post-get object access that
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=StorageItem) to include in your `manifest.json`.
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
- ```ts
56
- import {StorageItem} from "webext-storage";
54
+ The package exports two classes:
57
55
 
58
- const username = new StorageItem<string>('username')
59
- // Or
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
- await username.set('Ugo');
63
- // Promise<void>
59
+ Support:
64
60
 
65
- await username.get();
66
- // Promise<string>
67
-
68
- await username.remove();
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