toolaby 1.6.3 → 1.7.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/README.md +4 -1
- package/bin.mjs +178 -57
- package/lib/check.js +42 -6
- package/lib/manifest.js +111 -10
- package/lib/project.js +2 -2
- package/lib/safe.js +344 -32
- package/lib/summary.js +73 -0
- package/package.json +1 -1
package/lib/manifest.js
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* the storage permission; the platform's address as a host permission and
|
|
7
7
|
* for externally_connectable (the site's sign-in and purchase handoffs); a
|
|
8
8
|
* module service worker when the manifest names none, or names one that is
|
|
9
|
-
* not there
|
|
9
|
+
* not there — and in front of the extension's own background, the Wall's,
|
|
10
|
+
* which runs it and then starts the platform (wallWorkerSource), so nothing
|
|
11
|
+
* of theirs is edited; the Wall's popup in front of the extension's own — and its
|
|
10
12
|
* side panel in front of the extension's side panel — which the
|
|
11
13
|
* configuration then remembers; and the extension's identity — the `key`
|
|
12
14
|
* that fixes its id, which the tool key is bound to (identity.ts) — unless
|
|
@@ -18,6 +20,55 @@
|
|
|
18
20
|
export const WALL_POPUP = 'toolaby-popup.html';
|
|
19
21
|
export const WALL_SIDE_PANEL = 'toolaby-sidepanel.html';
|
|
20
22
|
export const WORKER_LINES = "import { toolaby } from './toolaby.js';\n\ntoolaby.startBackground();\n";
|
|
23
|
+
/** The Wall's background: the one it writes when the manifest names none, and the one it stands in front of theirs. */
|
|
24
|
+
export const WALL_WORKER = 'toolaby.background.js';
|
|
25
|
+
/** The client as a classic script (a global `Toolaby`): what a classic background loads with importScripts(). */
|
|
26
|
+
export const CLASSIC_CLIENT = 'toolaby-client.js';
|
|
27
|
+
/** The line in the Wall's background that names the extension's own, which it runs: read back when wired again. */
|
|
28
|
+
const WRAPS = /^\/\/ toolaby-wraps: (.+)$/m;
|
|
29
|
+
/** A background the Wall's can name in the code it writes: a path inside the folder, plain characters, a script. */
|
|
30
|
+
const SCRIPT_PATH = /^(?!.*\.\.)(?!.*\/\/)[A-Za-z0-9_][A-Za-z0-9_\-./]*\.m?js$/;
|
|
31
|
+
/** Code without its comments, strings kept whole, so a `//` inside one is not taken for a comment. */
|
|
32
|
+
export function withoutComments(text) {
|
|
33
|
+
return text.replace(/("(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'|`(?:[^`\\]|\\.)*`)|\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, (m, str) => (str ? m : ''));
|
|
34
|
+
}
|
|
35
|
+
/** Whether a background already starts the Wall itself: then it is left as it is. */
|
|
36
|
+
export const startsPlatform = (source) => /\b(?:startBackground|ensureBackground)\s*\(/.test(withoutComments(source));
|
|
37
|
+
/** The extension's own background the Wall's runs, read from the Wall's; null for any other file. */
|
|
38
|
+
export const wrappedBy = (source) => source?.match(WRAPS)?.[1]?.trim() || null;
|
|
39
|
+
/**
|
|
40
|
+
* The Wall's background in front of the extension's own (`file`, beside `wraps`, so a relative importScripts() of
|
|
41
|
+
* theirs still finds its files): it runs theirs as before, then starts the Wall unless theirs did — ensureBackground(),
|
|
42
|
+
* so a background that starts it with handlers of its own keeps them. A module background gets a module; a classic
|
|
43
|
+
* one a classic script, which loads the client as one (CLASSIC_CLIENT), so its importScripts() work as they did. The
|
|
44
|
+
* classic one carries the configuration it starts with — the key and the pages of toolaby.config.js — which wiring
|
|
45
|
+
* again rewrites with it.
|
|
46
|
+
*/
|
|
47
|
+
export function wallWorkerSource(b, config) {
|
|
48
|
+
const depth = b.file.split('/').length - 1;
|
|
49
|
+
const up = '../'.repeat(depth);
|
|
50
|
+
const own = b.wraps.slice(b.wraps.lastIndexOf('/') + 1);
|
|
51
|
+
const head = `// The Wall's background, in front of yours: ${b.wraps} runs as it did, then the Wall starts.\n// Written by the Wall's wiring (toolaby wire): keep it, never edit it. Your code stays in ${b.wraps}.\n// toolaby-wraps: ${b.wraps}\n`;
|
|
52
|
+
if (!b.classic)
|
|
53
|
+
return `${head}import { toolaby } from '${up || './'}toolaby.js';\nimport './${own}';\n\ntoolaby.ensureBackground();\n`;
|
|
54
|
+
const c = config ?? { key: '', appPopup: '', appSidePanel: '' };
|
|
55
|
+
return `${head}importScripts('${up}${CLASSIC_CLIENT}');\nself.toolaby = Toolaby(${JSON.stringify({ key: c.key, appPopup: c.appPopup, appSidePanel: c.appSidePanel })});\nimportScripts('${own}');\ntoolaby.ensureBackground();\n`;
|
|
56
|
+
}
|
|
57
|
+
/** What toolaby.config.js starts the Wall with, read from its text: what a classic background of the Wall's carries. */
|
|
58
|
+
export function configValues(text) {
|
|
59
|
+
const code = withoutComments(text);
|
|
60
|
+
const get = (name) => code.match(new RegExp(`${name}:\\s*'([^']*)'`))?.[1];
|
|
61
|
+
const key = get('key');
|
|
62
|
+
return key ? { key, appPopup: get('appPopup') ?? '', appSidePanel: get('appSidePanel') ?? '' } : null;
|
|
63
|
+
}
|
|
64
|
+
/** A name a background can be loaded by, for one that has none: its characters kept where they can be, the rest a dash. */
|
|
65
|
+
export const loadableName = (file) => {
|
|
66
|
+
const clean = file.split('/').filter((p) => p && p !== '.' && p !== '..').map((p) => p.replace(/[^A-Za-z0-9_\-.]+/g, '-').replace(/^[^A-Za-z0-9_]+/, '')).filter(Boolean).join('/');
|
|
67
|
+
const js = /\.m?js$/.test(clean) ? clean : `${clean.replace(/\.[^./]*$/, '') || 'background'}.js`;
|
|
68
|
+
return js.replace(/-+(?=\.m?js$)/, '');
|
|
69
|
+
};
|
|
70
|
+
/** The background the manifest names, as the folder spells it: "./background.js" is background.js. */
|
|
71
|
+
export const namedWorker = (manifest) => typeof manifest?.background?.service_worker === 'string' ? manifest.background.service_worker.trim().replace(/^(\.\/)+/, '') || null : null;
|
|
21
72
|
/**
|
|
22
73
|
* Whether an earlier wiring left the extension's own pages drawing the panel: a configuration that says '' for both
|
|
23
74
|
* pages while the manifest names none of the Wall's. That is what `create` makes, and the "your popup first" shape
|
|
@@ -37,11 +88,19 @@ export function drawsItself(manifest, oldConfig) {
|
|
|
37
88
|
export function wireManifest(input, o) {
|
|
38
89
|
const manifest = JSON.parse(JSON.stringify(input));
|
|
39
90
|
const did = [];
|
|
91
|
+
const added = [];
|
|
92
|
+
const host = (() => { try {
|
|
93
|
+
return new URL(o.site).host;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return o.site;
|
|
97
|
+
} })();
|
|
40
98
|
// The identity: the manifest's own key stays (the platform adopts it); otherwise the platform's goes in, and the unpacked extension's id is known before it loads.
|
|
41
99
|
const own = typeof manifest.key === 'string' && manifest.key.trim() ? manifest.key.trim() : null;
|
|
42
100
|
if (!own && o.manifestKey) {
|
|
43
101
|
manifest.key = o.manifestKey;
|
|
44
102
|
did.push('a "key", so the extension keeps one id across reloads and machines');
|
|
103
|
+
added.push('key');
|
|
45
104
|
}
|
|
46
105
|
const manifestKey = own ?? o.manifestKey ?? null;
|
|
47
106
|
const add = (list, value) => { const arr = Array.isArray(list) ? [...list] : []; if (!arr.includes(value))
|
|
@@ -49,15 +108,20 @@ export function wireManifest(input, o) {
|
|
|
49
108
|
if (!(Array.isArray(manifest.permissions) && manifest.permissions.includes('storage'))) {
|
|
50
109
|
manifest.permissions = add(manifest.permissions, 'storage');
|
|
51
110
|
did.push('the "storage" permission');
|
|
111
|
+
added.push('storage');
|
|
52
112
|
}
|
|
53
113
|
if (!(Array.isArray(manifest.host_permissions) && manifest.host_permissions.includes(`${o.site}/*`))) {
|
|
54
114
|
manifest.host_permissions = add(manifest.host_permissions, `${o.site}/*`);
|
|
55
115
|
did.push(`a host permission for ${o.site}`);
|
|
116
|
+
if (!added.includes(host))
|
|
117
|
+
added.push(host);
|
|
56
118
|
}
|
|
57
119
|
const ec = manifest.externally_connectable ?? {};
|
|
58
120
|
if (!(Array.isArray(ec.matches) && ec.matches.includes(`${o.site}/*`))) {
|
|
59
121
|
manifest.externally_connectable = { ...ec, matches: add(ec.matches, `${o.site}/*`) };
|
|
60
122
|
did.push(`externally_connectable for ${o.site}`);
|
|
123
|
+
if (!added.includes(host))
|
|
124
|
+
added.push(host);
|
|
61
125
|
}
|
|
62
126
|
// The extension's own popup: the manifest's, or the one remembered when the manifest already names the Wall's.
|
|
63
127
|
const declared = typeof manifest.action?.default_popup === 'string' ? manifest.action.default_popup : null;
|
|
@@ -67,6 +131,11 @@ export function wireManifest(input, o) {
|
|
|
67
131
|
manifest.action = { ...manifest.action, default_popup: WALL_POPUP };
|
|
68
132
|
did.push(`the Wall's popup in front of ${appPopup}: it opens on to yours while the device may, and shows the panel when it may not`);
|
|
69
133
|
}
|
|
134
|
+
// Asked to keep the extension's own in front after a wiring stood the Wall's there: theirs goes back.
|
|
135
|
+
if (o.ownInFront && declared === WALL_POPUP && ownPopup) {
|
|
136
|
+
manifest.action = { ...manifest.action, default_popup: ownPopup };
|
|
137
|
+
did.push(`${ownPopup} back in front: toolaby.showPaywall() opens the Wall's popup`);
|
|
138
|
+
}
|
|
70
139
|
// The extension's own side panel, the same way: the Wall's in front, with the permission a side panel needs.
|
|
71
140
|
// Chrome spells it side_panel.default_path, Firefox sidebar_action.default_panel; a manifest may carry both.
|
|
72
141
|
const theirs = (v) => (typeof v === 'string' && v !== WALL_SIDE_PANEL ? v : null);
|
|
@@ -75,7 +144,15 @@ export function wireManifest(input, o) {
|
|
|
75
144
|
const ours = declaredPanel === WALL_SIDE_PANEL || declaredSidebar === WALL_SIDE_PANEL;
|
|
76
145
|
const ownSidePanel = theirs(declaredPanel) ?? theirs(declaredSidebar) ?? (ours ? o.rememberedSidePanel || null : null);
|
|
77
146
|
const appSidePanel = o.ownInFront ? null : ownSidePanel;
|
|
78
|
-
if (o.ownInFront) {
|
|
147
|
+
if (o.ownInFront) {
|
|
148
|
+
// The extension's own side panel stays in front, as its popup does — and goes back there after a wiring put the Wall's.
|
|
149
|
+
if (declaredPanel === WALL_SIDE_PANEL && ownSidePanel) {
|
|
150
|
+
manifest.side_panel = { ...manifest.side_panel, default_path: ownSidePanel };
|
|
151
|
+
did.push(`${ownSidePanel} back in front: toolaby.showPaywall() opens the Wall's side panel`);
|
|
152
|
+
}
|
|
153
|
+
if (declaredSidebar === WALL_SIDE_PANEL && ownSidePanel)
|
|
154
|
+
manifest.sidebar_action = { ...manifest.sidebar_action, default_panel: ownSidePanel };
|
|
155
|
+
}
|
|
79
156
|
else if (theirs(declaredPanel)) {
|
|
80
157
|
manifest.side_panel = { ...manifest.side_panel, default_path: WALL_SIDE_PANEL };
|
|
81
158
|
if (!(Array.isArray(manifest.permissions) && manifest.permissions.includes('sidePanel')))
|
|
@@ -87,16 +164,40 @@ export function wireManifest(input, o) {
|
|
|
87
164
|
manifest.sidebar_action = { ...manifest.sidebar_action, default_panel: WALL_SIDE_PANEL };
|
|
88
165
|
did.push(`the Wall's side panel in front of ${declaredSidebar} in Firefox's sidebar_action too`);
|
|
89
166
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
167
|
+
// The background. None named, or named and not there: one of the Wall's alone, a module. The extension's own: left as
|
|
168
|
+
// it is, and the Wall's stood in front of it — beside it, a module for a module and a classic script for a classic
|
|
169
|
+
// one — unless it starts the Wall already. Wired again, the Wall's is written again for the one it names.
|
|
170
|
+
const named = namedWorker(manifest);
|
|
171
|
+
let background;
|
|
172
|
+
let workerMoved = false;
|
|
93
173
|
if (!named || !o.has(named)) {
|
|
94
|
-
|
|
95
|
-
manifest.background = { service_worker:
|
|
96
|
-
did.push(`a service worker, ${
|
|
174
|
+
const file = named ?? (o.has('background.js') ? WALL_WORKER : 'background.js');
|
|
175
|
+
manifest.background = { service_worker: file, type: 'module' };
|
|
176
|
+
did.push(`a service worker, ${file} (two lines: it starts the platform)`);
|
|
177
|
+
background = { kind: 'new', file };
|
|
97
178
|
}
|
|
98
179
|
else {
|
|
99
|
-
|
|
180
|
+
const source = o.workerSource ?? '';
|
|
181
|
+
const wraps = wrappedBy(source);
|
|
182
|
+
const theirs = wraps ?? named;
|
|
183
|
+
const classic = manifest.background?.type !== 'module';
|
|
184
|
+
if (!wraps && startsPlatform(source))
|
|
185
|
+
background = { kind: 'started', file: named };
|
|
186
|
+
else if (wraps && !o.has(wraps))
|
|
187
|
+
background = { kind: 'gone', file: named, wraps };
|
|
188
|
+
else if (/\.(ts|tsx|mts|cts|jsx)$/.test(theirs))
|
|
189
|
+
background = { kind: 'lines', file: theirs };
|
|
190
|
+
else if (!SCRIPT_PATH.test(theirs))
|
|
191
|
+
background = { kind: 'rename', file: theirs, to: loadableName(theirs) };
|
|
192
|
+
else {
|
|
193
|
+
const file = wraps ? named : `${theirs.includes('/') ? theirs.slice(0, theirs.lastIndexOf('/') + 1) : ''}${WALL_WORKER}`;
|
|
194
|
+
if (!wraps) {
|
|
195
|
+
manifest.background = { ...manifest.background, service_worker: file };
|
|
196
|
+
workerMoved = true;
|
|
197
|
+
}
|
|
198
|
+
background = { kind: 'wrap', file, wraps: theirs, classic };
|
|
199
|
+
}
|
|
100
200
|
}
|
|
101
|
-
|
|
201
|
+
const changed = did.length > 0 || workerMoved;
|
|
202
|
+
return { manifest, did, added, changed, background, appPopup, appSidePanel, manifestKey, ownPopup, ownSidePanel };
|
|
102
203
|
}
|
package/lib/project.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Generated from wall/src/packages/wiring/project.ts by wall/scripts/pack-create.mjs — edit there.
|
|
2
2
|
/** What to tell a developer whose build ships the extension — the Wire button, the command and the Set up page all say it in these words. */
|
|
3
|
-
export const BUILD_COPY_NOTE = 'A build ships this extension: make sure it copies
|
|
3
|
+
export const BUILD_COPY_NOTE = 'A build ships this extension: make sure it copies every toolaby* file to its output — toolaby.js, toolaby.config.js, toolaby-popup.html and the rest — the way it copies your popup.html.';
|
|
4
4
|
/** Always written, where the build imports from: the platform as one file (the client and the popup UI), its config, its types. */
|
|
5
5
|
export const CODE_FILES = ['toolaby.config.js', 'toolaby.js', 'toolaby.d.ts'];
|
|
6
6
|
/** Beside the client when the build generates the manifest: the fragment it spreads in, derived from the key (boilerplate/toolaby.manifest.js). */
|
|
@@ -161,7 +161,7 @@ export function detectProject(o) {
|
|
|
161
161
|
const declared = typeof manifest?.action?.default_popup === 'string' ? manifest.action.default_popup : null;
|
|
162
162
|
// Chrome's side_panel, else Firefox's sidebar_action: the same page, two spellings.
|
|
163
163
|
const declaredPanel = typeof manifest?.side_panel?.default_path === 'string' ? manifest.side_panel.default_path : typeof manifest?.sidebar_action?.default_panel === 'string' ? manifest.sidebar_action.default_panel : null;
|
|
164
|
-
const named = typeof manifest?.background?.service_worker === 'string' ? manifest.background.service_worker : null;
|
|
164
|
+
const named = typeof manifest?.background?.service_worker === 'string' ? manifest.background.service_worker.trim().replace(/^(\.\/)+/, '') || null : null;
|
|
165
165
|
const workerFile = named && has(named) ? named : 'background.js';
|
|
166
166
|
return {
|
|
167
167
|
kind: crxjs ? 'crxjs' : 'plain', label: crxjs ? 'CRXJS' : 'a plain extension', codeDir: '', popupDir: '', manifestFile: 'manifest.json', wrapper: true, built: crxjs ? false : built, foundManifest: null,
|