@arraypress/extension-kit 0.0.0-stage → 1.1.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.
- package/LICENSE +21 -0
- package/README.md +217 -2
- package/bin/extension-kit.js +35 -0
- package/package.json +67 -4
- package/src/about/links.d.ts +51 -0
- package/src/about/links.js +48 -0
- package/src/build/duplication.d.ts +116 -0
- package/src/build/duplication.js +119 -0
- package/src/build/index.d.ts +11 -0
- package/src/build/index.js +12 -0
- package/src/build/release-checks.d.ts +36 -0
- package/src/build/release-checks.js +61 -0
- package/src/build/toolbar-icons.d.ts +96 -0
- package/src/build/toolbar-icons.js +101 -0
- package/src/build/vendor.d.ts +26 -0
- package/src/build/vendor.js +43 -0
- package/src/build/zip.d.ts +31 -0
- package/src/build/zip.js +47 -0
- package/src/commands/shortcuts.d.ts +39 -0
- package/src/commands/shortcuts.js +31 -0
- package/src/dom/i18n.d.ts +39 -0
- package/src/dom/i18n.js +49 -0
- package/src/dom/page.d.ts +59 -0
- package/src/dom/page.js +93 -0
- package/src/index.d.ts +21 -0
- package/src/index.js +24 -0
- package/src/settings/saver.d.ts +76 -0
- package/src/settings/saver.js +98 -0
- package/src/settings/storage.d.ts +91 -0
- package/src/settings/storage.js +76 -0
- package/src/settings/store.d.ts +138 -0
- package/src/settings/store.js +173 -0
- package/src/styles/chrome-ui.css +1094 -0
- package/src/testing/e2e.d.ts +222 -0
- package/src/testing/e2e.js +327 -0
- package/src/testing/fake-storage.d.ts +71 -0
- package/src/testing/fake-storage.js +145 -0
- package/src/testing/index.d.ts +29 -0
- package/src/testing/index.js +24 -0
- package/src/testing/package.d.ts +39 -0
- package/src/testing/package.js +96 -0
- package/src/testing/server.d.ts +47 -0
- package/src/testing/server.js +90 -0
- package/src/toolbar/offscreen.d.ts +30 -0
- package/src/toolbar/offscreen.js +37 -0
- package/src/toolbar/scheme.d.ts +81 -0
- package/src/toolbar/scheme.js +87 -0
- package/src/toolbar/theme.d.ts +187 -0
- package/src/toolbar/theme.js +176 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A settings store on `chrome.storage.sync`, one item per setting, like
|
|
3
|
+
* Chrome's own settings: they follow the user to their other computers
|
|
4
|
+
* through their own Chrome Sync, and the developer receives nothing.
|
|
5
|
+
*
|
|
6
|
+
* Settings tied to one computer (say, ones that need a site permission Chrome
|
|
7
|
+
* grants per computer) are named in `localKeys` and kept in
|
|
8
|
+
* `chrome.storage.local` instead. Local also holds any write sync refused
|
|
9
|
+
* (too many writes a minute, or an item over its size limit), kept under
|
|
10
|
+
* `unsyncedKey`, read as if synced, and retried, so a change is never lost.
|
|
11
|
+
*
|
|
12
|
+
* Each extension supplies its schema: the defaults and one normaliser that
|
|
13
|
+
* coerces anything read from storage or a form into valid settings.
|
|
14
|
+
*
|
|
15
|
+
* @module settings/store
|
|
16
|
+
*/
|
|
17
|
+
import type { StorageChange, SyncedStorage } from './storage.js';
|
|
18
|
+
export type SettingsSchema<S extends Record<string, unknown>> = {
|
|
19
|
+
/**
|
|
20
|
+
* Every setting and its default value.
|
|
21
|
+
*/
|
|
22
|
+
defaults: Readonly<S>;
|
|
23
|
+
/**
|
|
24
|
+
* Coerce anything
|
|
25
|
+
* (stored, typed, from a newer version) into valid settings, falling back
|
|
26
|
+
* to the default for what it can't read. Must handle `{}`.
|
|
27
|
+
*/
|
|
28
|
+
normalize: (raw: Record<string, unknown>) => S;
|
|
29
|
+
/**
|
|
30
|
+
* Settings kept in
|
|
31
|
+
* `chrome.storage.local`, tied to this computer. Default none.
|
|
32
|
+
*/
|
|
33
|
+
localKeys?: readonly (keyof S & string)[];
|
|
34
|
+
/**
|
|
35
|
+
* The local item holding writes sync
|
|
36
|
+
* refused, `{key: value}`. Default `'unsyncedSettings'`.
|
|
37
|
+
*/
|
|
38
|
+
unsyncedKey?: string;
|
|
39
|
+
};
|
|
40
|
+
export type SettingsStore<S extends Record<string, unknown>> = {
|
|
41
|
+
/**
|
|
42
|
+
* The schema's defaults.
|
|
43
|
+
*/
|
|
44
|
+
defaults: Readonly<S>;
|
|
45
|
+
/**
|
|
46
|
+
* Settings kept per computer.
|
|
47
|
+
*/
|
|
48
|
+
localKeys: readonly (keyof S & string)[];
|
|
49
|
+
/**
|
|
50
|
+
* Settings kept in Chrome Sync.
|
|
51
|
+
*/
|
|
52
|
+
syncedKeys: readonly (keyof S & string)[];
|
|
53
|
+
/**
|
|
54
|
+
* The local item holding writes sync refused.
|
|
55
|
+
*/
|
|
56
|
+
unsyncedKey: string;
|
|
57
|
+
/**
|
|
58
|
+
* The
|
|
59
|
+
* schema's normaliser; `undefined` or `null` read as `{}`.
|
|
60
|
+
*/
|
|
61
|
+
normalize: (raw?: Record<string, unknown> | null) => S;
|
|
62
|
+
/**
|
|
63
|
+
* Read every setting.
|
|
64
|
+
*/
|
|
65
|
+
load: (storage: SyncedStorage) => Promise<S>;
|
|
66
|
+
/**
|
|
67
|
+
* Write synced settings, then retry earlier refusals; resolves to whether
|
|
68
|
+
* everything reached sync.
|
|
69
|
+
*/
|
|
70
|
+
writeSynced: (storage: SyncedStorage, values: Partial<S>) => Promise<boolean>;
|
|
71
|
+
/**
|
|
72
|
+
* Try again to put earlier refusals into sync.
|
|
73
|
+
*/
|
|
74
|
+
retryUnsynced: (storage: SyncedStorage) => Promise<boolean>;
|
|
75
|
+
/**
|
|
76
|
+
* Save the settings `changes` names, normalised.
|
|
77
|
+
*/
|
|
78
|
+
save: (storage: SyncedStorage, changes: Partial<S>) => Promise<{
|
|
79
|
+
settings: S;
|
|
80
|
+
synced: boolean;
|
|
81
|
+
}>;
|
|
82
|
+
/**
|
|
83
|
+
* The settings a `chrome.storage.onChanged` event touches.
|
|
84
|
+
*/
|
|
85
|
+
changed: (changes: Record<string, StorageChange>, area: string) => (keyof S & string)[];
|
|
86
|
+
};
|
|
87
|
+
/** @import { StorageChange, SyncedStorage } from './storage.js' */
|
|
88
|
+
/**
|
|
89
|
+
* What an extension's settings are.
|
|
90
|
+
*
|
|
91
|
+
* @template {Record<string, unknown>} S
|
|
92
|
+
* @typedef {object} SettingsSchema
|
|
93
|
+
* @property {Readonly<S>} defaults Every setting and its default value.
|
|
94
|
+
* @property {(raw: Record<string, unknown>) => S} normalize Coerce anything
|
|
95
|
+
* (stored, typed, from a newer version) into valid settings, falling back
|
|
96
|
+
* to the default for what it can't read. Must handle `{}`.
|
|
97
|
+
* @property {readonly (keyof S & string)[]} [localKeys] Settings kept in
|
|
98
|
+
* `chrome.storage.local`, tied to this computer. Default none.
|
|
99
|
+
* @property {string} [unsyncedKey] The local item holding writes sync
|
|
100
|
+
* refused, `{key: value}`. Default `'unsyncedSettings'`.
|
|
101
|
+
*/
|
|
102
|
+
/**
|
|
103
|
+
* A settings store made by {@link createSettingsStore}.
|
|
104
|
+
*
|
|
105
|
+
* @template {Record<string, unknown>} S
|
|
106
|
+
* @typedef {object} SettingsStore
|
|
107
|
+
* @property {Readonly<S>} defaults The schema's defaults.
|
|
108
|
+
* @property {readonly (keyof S & string)[]} localKeys Settings kept per computer.
|
|
109
|
+
* @property {readonly (keyof S & string)[]} syncedKeys Settings kept in Chrome Sync.
|
|
110
|
+
* @property {string} unsyncedKey The local item holding writes sync refused.
|
|
111
|
+
* @property {(raw?: Record<string, unknown> | null) => S} normalize The
|
|
112
|
+
* schema's normaliser; `undefined` or `null` read as `{}`.
|
|
113
|
+
* @property {(storage: SyncedStorage) => Promise<S>} load Read every setting.
|
|
114
|
+
* @property {(storage: SyncedStorage, values: Partial<S>) => Promise<boolean>} writeSynced
|
|
115
|
+
* Write synced settings, then retry earlier refusals; resolves to whether
|
|
116
|
+
* everything reached sync.
|
|
117
|
+
* @property {(storage: SyncedStorage) => Promise<boolean>} retryUnsynced
|
|
118
|
+
* Try again to put earlier refusals into sync.
|
|
119
|
+
* @property {(storage: SyncedStorage, changes: Partial<S>) => Promise<{settings: S, synced: boolean}>} save
|
|
120
|
+
* Save the settings `changes` names, normalised.
|
|
121
|
+
* @property {(changes: Record<string, StorageChange>, area: string) => (keyof S & string)[]} changed
|
|
122
|
+
* The settings a `chrome.storage.onChanged` event touches.
|
|
123
|
+
*/
|
|
124
|
+
/**
|
|
125
|
+
* Make a settings store for an extension's schema.
|
|
126
|
+
*
|
|
127
|
+
* @template {Record<string, unknown>} S
|
|
128
|
+
* @param {SettingsSchema<S>} schema - The defaults, the normaliser and where each setting lives.
|
|
129
|
+
* @returns {SettingsStore<S>} The store; every method takes `chrome.storage` (or a fake).
|
|
130
|
+
* @example
|
|
131
|
+
* const store = createSettingsStore({
|
|
132
|
+
* defaults: { days: 7 },
|
|
133
|
+
* normalize: (raw) => ({ days: Number.isInteger(raw.days) ? raw.days : 7 }),
|
|
134
|
+
* });
|
|
135
|
+
* const settings = await store.load(chrome.storage);
|
|
136
|
+
* await store.save(chrome.storage, { days: 14 });
|
|
137
|
+
*/
|
|
138
|
+
export declare function createSettingsStore<S extends Record<string, unknown>>({ defaults, normalize: normalizeRaw, localKeys, unsyncedKey }: SettingsSchema<S>): SettingsStore<S>;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A settings store on `chrome.storage.sync`, one item per setting, like
|
|
3
|
+
* Chrome's own settings: they follow the user to their other computers
|
|
4
|
+
* through their own Chrome Sync, and the developer receives nothing.
|
|
5
|
+
*
|
|
6
|
+
* Settings tied to one computer (say, ones that need a site permission Chrome
|
|
7
|
+
* grants per computer) are named in `localKeys` and kept in
|
|
8
|
+
* `chrome.storage.local` instead. Local also holds any write sync refused
|
|
9
|
+
* (too many writes a minute, or an item over its size limit), kept under
|
|
10
|
+
* `unsyncedKey`, read as if synced, and retried, so a change is never lost.
|
|
11
|
+
*
|
|
12
|
+
* Each extension supplies its schema: the defaults and one normaliser that
|
|
13
|
+
* coerces anything read from storage or a form into valid settings.
|
|
14
|
+
*
|
|
15
|
+
* @module settings/store
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { MAX_SYNC_ITEM_BYTES, isEmpty, pick, syncItemBytes } from './storage.js';
|
|
19
|
+
|
|
20
|
+
/** @import { StorageChange, SyncedStorage } from './storage.js' */
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* What an extension's settings are.
|
|
24
|
+
*
|
|
25
|
+
* @template {Record<string, unknown>} S
|
|
26
|
+
* @typedef {object} SettingsSchema
|
|
27
|
+
* @property {Readonly<S>} defaults Every setting and its default value.
|
|
28
|
+
* @property {(raw: Record<string, unknown>) => S} normalize Coerce anything
|
|
29
|
+
* (stored, typed, from a newer version) into valid settings, falling back
|
|
30
|
+
* to the default for what it can't read. Must handle `{}`.
|
|
31
|
+
* @property {readonly (keyof S & string)[]} [localKeys] Settings kept in
|
|
32
|
+
* `chrome.storage.local`, tied to this computer. Default none.
|
|
33
|
+
* @property {string} [unsyncedKey] The local item holding writes sync
|
|
34
|
+
* refused, `{key: value}`. Default `'unsyncedSettings'`.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A settings store made by {@link createSettingsStore}.
|
|
39
|
+
*
|
|
40
|
+
* @template {Record<string, unknown>} S
|
|
41
|
+
* @typedef {object} SettingsStore
|
|
42
|
+
* @property {Readonly<S>} defaults The schema's defaults.
|
|
43
|
+
* @property {readonly (keyof S & string)[]} localKeys Settings kept per computer.
|
|
44
|
+
* @property {readonly (keyof S & string)[]} syncedKeys Settings kept in Chrome Sync.
|
|
45
|
+
* @property {string} unsyncedKey The local item holding writes sync refused.
|
|
46
|
+
* @property {(raw?: Record<string, unknown> | null) => S} normalize The
|
|
47
|
+
* schema's normaliser; `undefined` or `null` read as `{}`.
|
|
48
|
+
* @property {(storage: SyncedStorage) => Promise<S>} load Read every setting.
|
|
49
|
+
* @property {(storage: SyncedStorage, values: Partial<S>) => Promise<boolean>} writeSynced
|
|
50
|
+
* Write synced settings, then retry earlier refusals; resolves to whether
|
|
51
|
+
* everything reached sync.
|
|
52
|
+
* @property {(storage: SyncedStorage) => Promise<boolean>} retryUnsynced
|
|
53
|
+
* Try again to put earlier refusals into sync.
|
|
54
|
+
* @property {(storage: SyncedStorage, changes: Partial<S>) => Promise<{settings: S, synced: boolean}>} save
|
|
55
|
+
* Save the settings `changes` names, normalised.
|
|
56
|
+
* @property {(changes: Record<string, StorageChange>, area: string) => (keyof S & string)[]} changed
|
|
57
|
+
* The settings a `chrome.storage.onChanged` event touches.
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Make a settings store for an extension's schema.
|
|
62
|
+
*
|
|
63
|
+
* @template {Record<string, unknown>} S
|
|
64
|
+
* @param {SettingsSchema<S>} schema - The defaults, the normaliser and where each setting lives.
|
|
65
|
+
* @returns {SettingsStore<S>} The store; every method takes `chrome.storage` (or a fake).
|
|
66
|
+
* @example
|
|
67
|
+
* const store = createSettingsStore({
|
|
68
|
+
* defaults: { days: 7 },
|
|
69
|
+
* normalize: (raw) => ({ days: Number.isInteger(raw.days) ? raw.days : 7 }),
|
|
70
|
+
* });
|
|
71
|
+
* const settings = await store.load(chrome.storage);
|
|
72
|
+
* await store.save(chrome.storage, { days: 14 });
|
|
73
|
+
*/
|
|
74
|
+
export function createSettingsStore({ defaults, normalize: normalizeRaw, localKeys = [], unsyncedKey = 'unsyncedSettings' }) {
|
|
75
|
+
const allKeys = /** @type {(keyof S & string)[]} */ (Object.keys(defaults));
|
|
76
|
+
const syncedKeys = Object.freeze(allKeys.filter((key) => !localKeys.includes(key)));
|
|
77
|
+
|
|
78
|
+
/** @param {Record<string, unknown> | null} [raw] @returns {S} */
|
|
79
|
+
const normalize = (raw) => normalizeRaw(raw ?? {});
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Start one sync write of `values` at once; resolves to what sync refused.
|
|
83
|
+
* An item too big for sync isn't even tried.
|
|
84
|
+
*
|
|
85
|
+
* @param {SyncedStorage} storage
|
|
86
|
+
* @param {Record<string, unknown>} values
|
|
87
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
88
|
+
*/
|
|
89
|
+
function send(storage, values) {
|
|
90
|
+
const entries = Object.entries(values);
|
|
91
|
+
const fits = Object.fromEntries(entries.filter(([key, value]) => syncItemBytes(key, value) <= MAX_SYNC_ITEM_BYTES));
|
|
92
|
+
const refused = Object.fromEntries(entries.filter(([key]) => !(key in fits)));
|
|
93
|
+
if (isEmpty(fits)) return Promise.resolve(refused);
|
|
94
|
+
return storage.sync.set(fits).then(() => refused, (/** @type {unknown} */ error) => {
|
|
95
|
+
console.warn('Settings: Chrome Sync refused the change; keeping it on this computer and trying again later.',
|
|
96
|
+
/** @type {Error | undefined} */ (error)?.message ?? error);
|
|
97
|
+
return { ...refused, ...fits };
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** @type {SettingsStore<S>['load']} */
|
|
102
|
+
async function load(storage) {
|
|
103
|
+
const [synced, local] = await Promise.all([
|
|
104
|
+
storage.sync.get([...syncedKeys]),
|
|
105
|
+
storage.local.get([...localKeys, unsyncedKey]),
|
|
106
|
+
]);
|
|
107
|
+
const { [unsyncedKey]: unsynced = {}, ...here } = local;
|
|
108
|
+
return normalize({ ...synced, ...pick(unsynced, syncedKeys), ...here });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* One sync write for all of `values` (sync allows 120 a minute), then any
|
|
113
|
+
* that sync refused before. What sync refuses is kept in local, where
|
|
114
|
+
* `load` still reads it, and goes up with the next write or
|
|
115
|
+
* `retryUnsynced`.
|
|
116
|
+
*
|
|
117
|
+
* @type {SettingsStore<S>['writeSynced']}
|
|
118
|
+
*/
|
|
119
|
+
async function writeSynced(storage, values) {
|
|
120
|
+
// Sent before anything is awaited, so a page that's closing still gets it out.
|
|
121
|
+
const sending = send(storage, values);
|
|
122
|
+
const { [unsyncedKey]: held } = await storage.local.get([unsyncedKey]);
|
|
123
|
+
let refused = await sending;
|
|
124
|
+
const older = Object.fromEntries(Object.entries(pick(held ?? {}, syncedKeys)).filter(([key]) => !(key in values)));
|
|
125
|
+
if (!isEmpty(older)) refused = { ...(await send(storage, older)), ...refused };
|
|
126
|
+
if (!isEmpty(refused)) await storage.local.set({ [unsyncedKey]: refused });
|
|
127
|
+
else if (held) await storage.local.remove(unsyncedKey);
|
|
128
|
+
return isEmpty(refused);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Only the settings `changes` names are written, so a change made meanwhile
|
|
133
|
+
* on another computer to a different setting survives. Each setting
|
|
134
|
+
* normalises on its own, so nothing is read first and the writes start at
|
|
135
|
+
* once.
|
|
136
|
+
*
|
|
137
|
+
* @type {SettingsStore<S>['save']}
|
|
138
|
+
*/
|
|
139
|
+
async function save(storage, changes) {
|
|
140
|
+
const values = pick(normalize(changes), Object.keys(changes));
|
|
141
|
+
const local = pick(values, localKeys);
|
|
142
|
+
const writingLocal = isEmpty(local) ? null : storage.local.set(local);
|
|
143
|
+
const synced = await writeSynced(storage, pick(values, syncedKeys));
|
|
144
|
+
await writingLocal;
|
|
145
|
+
return { settings: await load(storage), synced };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Includes settings held for sync in local, which `load` reads too.
|
|
150
|
+
*
|
|
151
|
+
* @type {SettingsStore<S>['changed']}
|
|
152
|
+
*/
|
|
153
|
+
function changed(changes, area) {
|
|
154
|
+
if (area === 'sync') return syncedKeys.filter((key) => key in changes);
|
|
155
|
+
if (area !== 'local') return [];
|
|
156
|
+
const heldChange = changes[unsyncedKey];
|
|
157
|
+
const held = heldChange ? Object.keys({ ...(/** @type {object} */ (heldChange.oldValue)), ...(/** @type {object} */ (heldChange.newValue)) }) : [];
|
|
158
|
+
return allKeys.filter((key) => (localKeys.includes(key) ? key in changes : held.includes(key)));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return Object.freeze({
|
|
162
|
+
defaults,
|
|
163
|
+
localKeys: Object.freeze([...localKeys]),
|
|
164
|
+
syncedKeys,
|
|
165
|
+
unsyncedKey,
|
|
166
|
+
normalize,
|
|
167
|
+
load,
|
|
168
|
+
writeSynced,
|
|
169
|
+
retryUnsynced: (/** @type {SyncedStorage} */ storage) => writeSynced(storage, {}),
|
|
170
|
+
save,
|
|
171
|
+
changed,
|
|
172
|
+
});
|
|
173
|
+
}
|