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/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; the Wall's popup in front of the extension's own — and its
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) { /* the extension's own side panel stays in front, as its popup does */ }
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
- const named = typeof manifest.background?.service_worker === 'string' && manifest.background.service_worker.trim() ? manifest.background.service_worker.trim() : null;
91
- let writeWorker = null;
92
- let ownWorker = null;
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
- writeWorker = named ?? (o.has('background.js') ? 'toolaby.background.js' : 'background.js');
95
- manifest.background = { service_worker: writeWorker, type: 'module' };
96
- did.push(`a service worker, ${writeWorker} (two lines: it starts the platform)`);
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
- ownWorker = { file: named, classic: manifest.background?.type !== 'module' };
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
- return { manifest, did, writeWorker, ownWorker, appPopup, appSidePanel, manifestKey, ownPopup, ownSidePanel };
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 the platform\'s files to its output — toolaby.js, toolaby.config.js and toolaby-popup.html (toolaby-sidepanel.html too, with a side panel) — the way it copies your popup.html.';
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,