webext-storage 2.0.1 → 3.0.0

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.
@@ -7,16 +7,17 @@ export declare class StorageItemMap<
7
7
  Base,
8
8
  /** The return type will be undefined unless you provide a default value */
9
9
  Return = Base | undefined> {
10
+ #private;
10
11
  readonly prefix: `${string}:::`;
11
12
  readonly areaName: chrome.storage.AreaName;
12
13
  readonly defaultValue?: Return;
13
14
  constructor(key: string, { area, defaultValue, }?: StorageItemMapOptions<Exclude<Return, undefined>>);
14
- has: (secondaryKey: string) => Promise<boolean>;
15
- get: (secondaryKey: string) => Promise<Return>;
16
- set: (secondaryKey: string, value: Exclude<Return, undefined>) => Promise<void>;
17
- remove: (secondaryKey: string) => Promise<void>;
15
+ has(secondaryKey: string): Promise<boolean>;
16
+ get(secondaryKey: string): Promise<Return>;
17
+ set(secondaryKey: string, value: Exclude<Return, undefined>): Promise<void>;
18
+ remove(secondaryKey: string): Promise<void>;
18
19
  /** @deprecated Only here to match the Map API; use `remove` instead */
19
- delete: (secondaryKey: string) => Promise<void>;
20
+ delete(secondaryKey: string): Promise<void>;
20
21
  onChanged(callback: (key: string, value: Exclude<Return, undefined>) => void, signal?: AbortSignal): void;
21
22
  private getRawStorageKey;
22
23
  private getSecondaryStorageKey;
@@ -1,55 +1,62 @@
1
- import chromeP from 'webext-polyfill-kinda';
1
+ import { assertChromeStorageAvailable, hasStorageValueChanged } from './utils.js';
2
2
  export class StorageItemMap {
3
3
  prefix;
4
4
  areaName;
5
5
  defaultValue;
6
+ get #storage() {
7
+ assertChromeStorageAvailable();
8
+ return chrome.storage[this.areaName];
9
+ }
6
10
  constructor(key, { area = 'local', defaultValue, } = {}) {
7
11
  this.prefix = `${key}:::`;
8
12
  this.areaName = area;
9
13
  this.defaultValue = defaultValue;
10
14
  }
11
- has = async (secondaryKey) => {
15
+ async has(secondaryKey) {
12
16
  const rawStorageKey = this.getRawStorageKey(secondaryKey);
13
- const result = await chromeP.storage[this.areaName].get(rawStorageKey);
17
+ const result = await this.#storage.get(rawStorageKey);
14
18
  // Do not use Object.hasOwn() due to https://github.com/RickyMarou/jest-webextension-mock/issues/20
15
19
  return result[rawStorageKey] !== undefined;
16
- };
17
- get = async (secondaryKey) => {
20
+ }
21
+ async get(secondaryKey) {
18
22
  const rawStorageKey = this.getRawStorageKey(secondaryKey);
19
- const result = await chromeP.storage[this.areaName].get(rawStorageKey);
23
+ const result = await this.#storage.get(rawStorageKey);
20
24
  // Do not use Object.hasOwn() due to https://github.com/RickyMarou/jest-webextension-mock/issues/20
21
25
  if (result[rawStorageKey] === undefined) {
26
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Assumes the user never uses the Storage API directly for this key
22
27
  return this.defaultValue;
23
28
  }
24
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return -- Assumes the user never uses the Storage API directly for this key
29
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-type-assertion -- Assumes the user never uses the Storage API directly for this key
25
30
  return result[rawStorageKey];
26
- };
27
- set = async (secondaryKey, value) => {
31
+ }
32
+ async set(secondaryKey, value) {
28
33
  const rawStorageKey = this.getRawStorageKey(secondaryKey);
29
34
  // eslint-disable-next-line unicorn/prefer-ternary -- ur rong
30
35
  if (value === undefined) {
31
- await chromeP.storage[this.areaName].remove(rawStorageKey);
36
+ await this.#storage.remove(rawStorageKey);
32
37
  }
33
38
  else {
34
- await chromeP.storage[this.areaName].set({ [rawStorageKey]: value });
39
+ await this.#storage.set({ [rawStorageKey]: value });
35
40
  }
36
- };
37
- remove = async (secondaryKey) => {
41
+ }
42
+ async remove(secondaryKey) {
38
43
  const rawStorageKey = this.getRawStorageKey(secondaryKey);
39
- await chromeP.storage[this.areaName].remove(rawStorageKey);
40
- };
44
+ await this.#storage.remove(rawStorageKey);
45
+ }
41
46
  /** @deprecated Only here to match the Map API; use `remove` instead */
42
- // eslint-disable-next-line @typescript-eslint/member-ordering -- invalid
43
- delete = this.remove;
47
+ async delete(secondaryKey) {
48
+ return this.remove(secondaryKey);
49
+ }
44
50
  onChanged(callback, signal) {
51
+ assertChromeStorageAvailable();
45
52
  const changeHandler = (changes, area) => {
46
53
  if (area !== this.areaName) {
47
54
  return;
48
55
  }
49
56
  for (const rawKey of Object.keys(changes)) {
50
57
  const secondaryKey = this.getSecondaryStorageKey(rawKey);
51
- if (secondaryKey) {
52
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument -- Assumes the user never uses the Storage API directly
58
+ if (secondaryKey && hasStorageValueChanged(changes[rawKey])) {
59
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-type-assertion -- Assumes the user never uses the Storage API directly
53
60
  callback(secondaryKey, changes[rawKey].newValue);
54
61
  }
55
62
  }
@@ -7,15 +7,14 @@ export declare class StorageItem<
7
7
  Base,
8
8
  /** The return type will be undefined unless you provide a default value */
9
9
  Return = Base | undefined> {
10
- readonly key: string;
10
+ #private;
11
11
  readonly area: chrome.storage.AreaName;
12
12
  readonly defaultValue?: Return;
13
- /** @deprecated Use `onChanged` instead */
14
- onChange: (callback: (value: Exclude<Return, undefined>) => void, signal?: AbortSignal) => void;
13
+ readonly key: string;
15
14
  constructor(key: string, { area, defaultValue, }?: StorageItemOptions<Exclude<Return, undefined>>);
16
- get: () => Promise<Return>;
17
- set: (value: Exclude<Return, undefined>) => Promise<void>;
18
- has: () => Promise<boolean>;
19
- remove: () => Promise<void>;
15
+ get(): Promise<Return>;
16
+ set(value: Exclude<Return, undefined>): Promise<void>;
17
+ has(): Promise<boolean>;
18
+ remove(): Promise<void>;
20
19
  onChanged(callback: (value: Exclude<Return, undefined>) => void, signal?: AbortSignal): void;
21
20
  }
@@ -1,46 +1,50 @@
1
- import chromeP from 'webext-polyfill-kinda';
1
+ import { assertChromeStorageAvailable, hasStorageValueChanged } from './utils.js';
2
2
  export class StorageItem {
3
- key;
4
3
  area;
5
4
  defaultValue;
6
- /** @deprecated Use `onChanged` instead */
7
- onChange = this.onChanged;
5
+ key;
6
+ get #storage() {
7
+ assertChromeStorageAvailable();
8
+ return chrome.storage[this.area];
9
+ }
8
10
  constructor(key, { area = 'local', defaultValue, } = {}) {
9
11
  this.key = key;
10
12
  this.area = area;
11
13
  this.defaultValue = defaultValue;
12
14
  }
13
- get = async () => {
14
- const result = await chromeP.storage[this.area].get(this.key);
15
+ async get() {
16
+ const result = await this.#storage.get(this.key);
15
17
  // Do not use Object.hasOwn() due to https://github.com/RickyMarou/jest-webextension-mock/issues/20
16
18
  if (result[this.key] === undefined) {
19
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Assumes the user never uses the Storage API directly
17
20
  return this.defaultValue;
18
21
  }
19
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return -- Assumes the user never uses the Storage API directly
22
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-type-assertion -- Assumes the user never uses the Storage API directly
20
23
  return result[this.key];
21
- };
22
- set = async (value) => {
24
+ }
25
+ async set(value) {
23
26
  // eslint-disable-next-line unicorn/prefer-ternary -- ur rong
24
27
  if (value === undefined) {
25
- await chromeP.storage[this.area].remove(this.key);
28
+ await this.#storage.remove(this.key);
26
29
  }
27
30
  else {
28
- await chromeP.storage[this.area].set({ [this.key]: value });
31
+ await this.#storage.set({ [this.key]: value });
29
32
  }
30
- };
31
- has = async () => {
32
- const result = await chromeP.storage[this.area].get(this.key);
33
+ }
34
+ async has() {
35
+ const result = await this.#storage.get(this.key);
33
36
  // Do not use Object.hasOwn() due to https://github.com/RickyMarou/jest-webextension-mock/issues/20
34
37
  return result[this.key] !== undefined;
35
- };
36
- remove = async () => {
37
- await chromeP.storage[this.area].remove(this.key);
38
- };
38
+ }
39
+ async remove() {
40
+ await this.#storage.remove(this.key);
41
+ }
39
42
  onChanged(callback, signal) {
43
+ assertChromeStorageAvailable();
40
44
  const changeHandler = (changes, area) => {
41
45
  const changedItem = changes[this.key];
42
- if (area === this.area && changedItem) {
43
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument -- Assumes the user never uses the Storage API directly
46
+ if (area === this.area && changedItem && hasStorageValueChanged(changedItem)) {
47
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-type-assertion -- Assumes the user never uses the Storage API directly
44
48
  callback(changedItem.newValue);
45
49
  }
46
50
  };
@@ -1,4 +1,5 @@
1
- /* eslint-disable @typescript-eslint/ban-types */
1
+ /* eslint-disable @typescript-eslint/no-unnecessary-type-arguments -- Explicit type arguments are intentional in tsd assertions */
2
+ /* eslint-disable @typescript-eslint/no-restricted-types -- null is an intentional test value */
2
3
  /* eslint-disable no-new -- Type tests only */
3
4
  import { expectType, expectNotAssignable, expectAssignable } from 'tsd';
4
5
  import { StorageItem } from './storage-item.js';
@@ -0,0 +1,2 @@
1
+ export declare function assertChromeStorageAvailable(): void;
2
+ export declare function hasStorageValueChanged(change: chrome.storage.StorageChange): boolean;
@@ -0,0 +1,12 @@
1
+ export function assertChromeStorageAvailable() {
2
+ if (!globalThis.chrome?.storage) {
3
+ throw new TypeError('`chrome.storage` is not available. Make sure you\'re running in a browser extension context.');
4
+ }
5
+ }
6
+ // Workaround for https://github.com/w3c/webextensions/issues/511
7
+ // Firefox fires onChanged even when set() is called with the same value
8
+ export function hasStorageValueChanged(change) {
9
+ // eslint-disable-next-line n/no-unsupported-features/node-builtins -- browser extension context, not Node.js
10
+ return !globalThis.navigator?.userAgent.includes('Firefox')
11
+ || JSON.stringify(change.newValue) !== JSON.stringify(change.oldValue);
12
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webext-storage",
3
- "version": "2.0.1",
3
+ "version": "3.0.0",
4
4
  "description": "A more usable typed storage API for Web Extensions",
5
5
  "keywords": [
6
6
  "browser",
@@ -31,27 +31,14 @@
31
31
  "test:watch": "vitest",
32
32
  "watch": "tsc --watch"
33
33
  },
34
- "xo": {
35
- "envs": [
36
- "browser"
37
- ],
38
- "globals": [
39
- "chrome"
40
- ]
41
- },
42
- "dependencies": {
43
- "webext-polyfill-kinda": "^1.0.2"
44
- },
45
34
  "devDependencies": {
46
- "@sindresorhus/tsconfig": "^7.0.0",
47
- "@types/chrome": "^0.0.300",
48
- "@types/sinon-chrome": "^2.2.15",
35
+ "@sindresorhus/tsconfig": "^8.1.0",
36
+ "@types/chrome": "^0.1.39",
49
37
  "jest-chrome": "^0.8.0",
50
- "sinon-chrome": "^3.0.1",
51
- "tsd": "^0.31.2",
52
- "typescript": "^5.7.3",
53
- "vitest": "^3.0.4",
54
- "xo": "^0.60.0"
38
+ "tsd": "^0.33.0",
39
+ "typescript": "^6.0.2",
40
+ "vitest": "^4.1.2",
41
+ "xo": "^2.0.2"
55
42
  },
56
43
  "engines": {
57
44
  "node": ">=20"
package/readme.md CHANGED
@@ -60,7 +60,7 @@ The package exports two classes:
60
60
  Support:
61
61
 
62
62
  - Browsers: Chrome, Firefox, and Safari
63
- - Manifest: v2 and v3
63
+ - Manifest: v3
64
64
  - Permissions: `storage` or `unlimitedStorage`
65
65
  - Context: They can be called from any context
66
66