@gjsify/cli 0.52.0 → 0.54.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/dist/cli.gjs.mjs +351 -305
- package/lib/actions/build.d.ts +22 -0
- package/lib/actions/build.js +37 -9
- package/lib/bundler-pick.d.ts +72 -1
- package/lib/bundler-pick.js +175 -17
- package/lib/cli-app.js +7 -2
- package/lib/commands/blueprint/index.d.ts +4 -0
- package/lib/commands/blueprint/index.js +15 -0
- package/lib/commands/blueprint/types.d.ts +8 -0
- package/lib/commands/blueprint/types.js +124 -0
- package/lib/commands/devtools.d.ts +21 -0
- package/lib/commands/devtools.js +146 -0
- package/lib/commands/env.d.ts +31 -0
- package/lib/commands/env.js +114 -0
- package/lib/commands/exec.d.ts +10 -0
- package/lib/commands/exec.js +208 -0
- package/lib/commands/foreach.js +5 -97
- package/lib/commands/format.js +26 -6
- package/lib/commands/index.d.ts +5 -0
- package/lib/commands/index.js +5 -0
- package/lib/commands/install.d.ts +2 -2
- package/lib/commands/install.js +5 -5
- package/lib/commands/self-update.js +2 -1
- package/lib/commands/ship.js +51 -8
- package/lib/commands/test.d.ts +18 -9
- package/lib/commands/test.js +56 -11
- package/lib/commands/tsc.js +4 -4
- package/lib/commands/upgrade.d.ts +15 -0
- package/lib/commands/upgrade.js +76 -2
- package/lib/commands/webext/index.d.ts +2 -0
- package/lib/commands/webext/index.js +15 -0
- package/lib/commands/webext/subcommands.d.ts +16 -0
- package/lib/commands/webext/subcommands.js +204 -0
- package/lib/types/config-data.d.ts +1 -1
- package/lib/utils/bin-shim.d.ts +8 -2
- package/lib/utils/bin-shim.js +6 -5
- package/lib/utils/bundle-inputs.d.ts +84 -0
- package/lib/utils/bundle-inputs.js +268 -0
- package/lib/utils/check-system-deps.d.ts +4 -2
- package/lib/utils/check-system-deps.js +21 -26
- package/lib/utils/dev-link.d.ts +9 -6
- package/lib/utils/dev-link.js +71 -19
- package/lib/utils/exec-bin.d.ts +137 -0
- package/lib/utils/exec-bin.js +321 -0
- package/lib/utils/gi-runtime-paths.js +3 -2
- package/lib/utils/gitignore.d.ts +15 -0
- package/lib/utils/gitignore.js +178 -0
- package/lib/utils/gjsify-shim.d.ts +47 -0
- package/lib/utils/gjsify-shim.js +97 -8
- package/lib/utils/install-backend-native.d.ts +19 -1
- package/lib/utils/install-backend-native.js +93 -7
- package/lib/utils/install-lock.js +4 -25
- package/lib/utils/native-skip-scan.d.ts +20 -0
- package/lib/utils/native-skip-scan.js +343 -0
- package/lib/utils/node-script.js +1 -1
- package/lib/utils/oxc-resolve.js +46 -5
- package/lib/utils/process-table.d.ts +22 -0
- package/lib/utils/process-table.js +188 -0
- package/lib/utils/prune-prefix.js +1 -1
- package/lib/utils/run-gjs.d.ts +15 -1
- package/lib/utils/run-gjs.js +16 -7
- package/lib/utils/run-node.d.ts +2 -0
- package/lib/utils/run-node.js +2 -1
- package/lib/utils/runtimes.js +14 -13
- package/lib/utils/ship/app-runtime.d.ts +7 -5
- package/lib/utils/ship/app-runtime.js +11 -8
- package/lib/utils/ship/appimage.d.ts +9 -0
- package/lib/utils/ship/appimage.js +2 -1
- package/lib/utils/ship/deb.d.ts +10 -0
- package/lib/utils/ship/deb.js +7 -0
- package/lib/utils/ship/depends.d.ts +37 -3
- package/lib/utils/ship/depends.js +21 -9
- package/lib/utils/ship/gi-namespaces.d.ts +16 -0
- package/lib/utils/ship/gi-namespaces.js +64 -1
- package/lib/utils/ship/gzip.js +1 -1
- package/lib/utils/ship/icons.d.ts +6 -0
- package/lib/utils/ship/icons.js +6 -5
- package/lib/utils/ship/launcher.js +2 -2
- package/lib/utils/ship/layout.js +25 -2
- package/lib/utils/ship/rpm.d.ts +5 -0
- package/lib/utils/ship/rpm.js +42 -0
- package/lib/utils/ship/settings.js +6 -0
- package/lib/utils/ship/signing.d.ts +7 -2
- package/lib/utils/ship/signing.js +59 -32
- package/lib/utils/ship/stage-manifest.d.ts +14 -0
- package/lib/utils/ship/stage-manifest.js +7 -0
- package/lib/utils/spawn.d.ts +13 -0
- package/lib/utils/spawn.js +10 -5
- package/lib/utils/watch-loop.d.ts +15 -1
- package/lib/utils/watch-loop.js +9 -5
- package/lib/utils/webext/build.d.ts +42 -0
- package/lib/utils/webext/build.js +120 -0
- package/lib/utils/webext/config.d.ts +72 -0
- package/lib/utils/webext/config.js +234 -0
- package/lib/utils/webext/icons.d.ts +24 -0
- package/lib/utils/webext/icons.js +83 -0
- package/lib/utils/webext/launch.d.ts +32 -0
- package/lib/utils/webext/launch.js +77 -0
- package/lib/utils/webext/manifest.d.ts +53 -0
- package/lib/utils/webext/manifest.js +219 -0
- package/lib/utils/webext/pages.d.ts +26 -0
- package/lib/utils/webext/pages.js +61 -0
- package/lib/utils/webext/targets.d.ts +13 -0
- package/lib/utils/webext/targets.js +48 -0
- package/lib/utils/win32-command.d.ts +17 -0
- package/lib/utils/win32-command.js +21 -6
- package/package.json +20 -19
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { WebextTarget } from './targets.js';
|
|
2
|
+
export type Manifest = Record<string, unknown>;
|
|
3
|
+
/** What a manifest module's default-exported function receives. */
|
|
4
|
+
export interface WebextManifestContext {
|
|
5
|
+
target: string;
|
|
6
|
+
browser: WebextTarget['browser'];
|
|
7
|
+
manifestVersion: 2 | 3;
|
|
8
|
+
mode: 'production' | 'development';
|
|
9
|
+
/** `package.json#version`. */
|
|
10
|
+
version: string;
|
|
11
|
+
/** The effective `define` map (config, then `--define`). */
|
|
12
|
+
define: Record<string, string>;
|
|
13
|
+
/** The `{ size: path }` map for a declared icon, as this target receives it. */
|
|
14
|
+
icons(name: string): Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
/** A loaded template: a function of the context, or an object with optional `$targets` overrides. */
|
|
17
|
+
export type ManifestTemplate = ((ctx: WebextManifestContext) => Manifest | Promise<Manifest>) | Manifest;
|
|
18
|
+
/**
|
|
19
|
+
* Deep-merge `over` onto `base`: objects merge, arrays and scalars replace,
|
|
20
|
+
* `null` deletes. Arrays replace because "add a permission for Firefox" and
|
|
21
|
+
* "drop a permission for Firefox" cannot both be spelled by concatenation.
|
|
22
|
+
*/
|
|
23
|
+
export declare function mergeManifest(base: Manifest, over: Manifest): Manifest;
|
|
24
|
+
/** Apply a JSON template's `$targets` block for one target, and drop the block. */
|
|
25
|
+
export declare function applyTargetOverrides(template: Manifest, target: string): Manifest;
|
|
26
|
+
/** Produce the manifest for one target from a loaded template. */
|
|
27
|
+
export declare function composeManifest(template: ManifestTemplate, ctx: WebextManifestContext): Promise<Manifest>;
|
|
28
|
+
/**
|
|
29
|
+
* Load a manifest template. A `.json` is parsed; a module is imported and its
|
|
30
|
+
* default export used.
|
|
31
|
+
*
|
|
32
|
+
* Every load is FRESH, because `webext dev` reloads the template on each
|
|
33
|
+
* rebuild and both hosts cache modules by URL: on Node a query string makes a
|
|
34
|
+
* new URL; under GJS the module is bundled (GJS cannot import `.ts`, nor a
|
|
35
|
+
* module with `node:` imports) and the bundle copied to a unique file name,
|
|
36
|
+
* because whether GJS's loader keeps a query string on a `file:` URL is not a
|
|
37
|
+
* thing to depend on.
|
|
38
|
+
*/
|
|
39
|
+
export declare function loadManifestTemplate(path: string): Promise<ManifestTemplate>;
|
|
40
|
+
/**
|
|
41
|
+
* Every file inside the extension the manifest refers to by path.
|
|
42
|
+
*
|
|
43
|
+
* Deliberately a list of the keys browsers LOAD from, not a walk over every
|
|
44
|
+
* string: `description` or a `commands` label that happens to look like a path
|
|
45
|
+
* is not a reference.
|
|
46
|
+
*/
|
|
47
|
+
export declare function manifestFileReferences(manifest: Manifest): string[];
|
|
48
|
+
/**
|
|
49
|
+
* What is wrong with a composed manifest for the folder it will sit in.
|
|
50
|
+
* Returns problem sentences; empty means the browser has nothing to refuse
|
|
51
|
+
* that this build could have known about.
|
|
52
|
+
*/
|
|
53
|
+
export declare function checkManifest(manifest: Manifest, target: WebextTarget, dir: string): string[];
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// The manifest per target (ADR 0077 § 3): a module or a JSON template,
|
|
2
|
+
// composed WITHOUT converting keys between manifest versions, then checked
|
|
3
|
+
// against the folder it describes.
|
|
4
|
+
//
|
|
5
|
+
// The checks are the ones a browser would otherwise report against the
|
|
6
|
+
// manifest, far from the build that caused them: Chrome refuses to load a
|
|
7
|
+
// folder whose background script is missing ("Could not load background
|
|
8
|
+
// script") or that has `_locales/` without `default_locale`, and the store
|
|
9
|
+
// rejects the same zip for the same reasons, days later.
|
|
10
|
+
import { copyFileSync, existsSync, readFileSync, rmSync, statSync } from 'node:fs';
|
|
11
|
+
import { basename, dirname, join } from 'node:path';
|
|
12
|
+
import { pathToFileURL } from 'node:url';
|
|
13
|
+
import { isGjs } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
14
|
+
function isRecord(value) {
|
|
15
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Deep-merge `over` onto `base`: objects merge, arrays and scalars replace,
|
|
19
|
+
* `null` deletes. Arrays replace because "add a permission for Firefox" and
|
|
20
|
+
* "drop a permission for Firefox" cannot both be spelled by concatenation.
|
|
21
|
+
*/
|
|
22
|
+
export function mergeManifest(base, over) {
|
|
23
|
+
const out = { ...base };
|
|
24
|
+
for (const [key, value] of Object.entries(over)) {
|
|
25
|
+
if (value === null)
|
|
26
|
+
delete out[key];
|
|
27
|
+
else if (isRecord(value) && isRecord(out[key]))
|
|
28
|
+
out[key] = mergeManifest(out[key], value);
|
|
29
|
+
else
|
|
30
|
+
out[key] = value;
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
/** Apply a JSON template's `$targets` block for one target, and drop the block. */
|
|
35
|
+
export function applyTargetOverrides(template, target) {
|
|
36
|
+
const { $targets, ...base } = template;
|
|
37
|
+
if ($targets === undefined)
|
|
38
|
+
return base;
|
|
39
|
+
if (!isRecord($targets))
|
|
40
|
+
throw new Error("gjsify webext: the manifest's `$targets` must be an object.");
|
|
41
|
+
const over = $targets[target];
|
|
42
|
+
if (over === undefined)
|
|
43
|
+
return base;
|
|
44
|
+
if (!isRecord(over))
|
|
45
|
+
throw new Error(`gjsify webext: \`$targets["${target}"]\` in the manifest must be an object.`);
|
|
46
|
+
return mergeManifest(base, over);
|
|
47
|
+
}
|
|
48
|
+
/** Produce the manifest for one target from a loaded template. */
|
|
49
|
+
export async function composeManifest(template, ctx) {
|
|
50
|
+
const raw = typeof template === 'function' ? await template(ctx) : applyTargetOverrides(template, ctx.target);
|
|
51
|
+
if (!isRecord(raw)) {
|
|
52
|
+
throw new Error(`gjsify webext: the manifest for ${ctx.target} is not an object (got ${typeof raw}).`);
|
|
53
|
+
}
|
|
54
|
+
if (raw.version !== undefined)
|
|
55
|
+
return raw;
|
|
56
|
+
// Beside `name`, where a reader of manifest.json looks for it, rather than last.
|
|
57
|
+
const out = {};
|
|
58
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
59
|
+
out[key] = value;
|
|
60
|
+
if (key === 'name')
|
|
61
|
+
out.version = ctx.version;
|
|
62
|
+
}
|
|
63
|
+
if (out.version === undefined)
|
|
64
|
+
out.version = ctx.version;
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
let loadCounter = 0;
|
|
68
|
+
/**
|
|
69
|
+
* Load a manifest template. A `.json` is parsed; a module is imported and its
|
|
70
|
+
* default export used.
|
|
71
|
+
*
|
|
72
|
+
* Every load is FRESH, because `webext dev` reloads the template on each
|
|
73
|
+
* rebuild and both hosts cache modules by URL: on Node a query string makes a
|
|
74
|
+
* new URL; under GJS the module is bundled (GJS cannot import `.ts`, nor a
|
|
75
|
+
* module with `node:` imports) and the bundle copied to a unique file name,
|
|
76
|
+
* because whether GJS's loader keeps a query string on a `file:` URL is not a
|
|
77
|
+
* thing to depend on.
|
|
78
|
+
*/
|
|
79
|
+
export async function loadManifestTemplate(path) {
|
|
80
|
+
if (/\.json$/i.test(path)) {
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
throw new Error(`gjsify webext: ${path} is not valid JSON: ${err.message}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
let url;
|
|
89
|
+
let cleanup = null;
|
|
90
|
+
if (isGjs()) {
|
|
91
|
+
const { BuildAction } = await import('../../actions/build.js');
|
|
92
|
+
const bundled = await BuildAction.bundleFileForGjsCached(path, {
|
|
93
|
+
cacheSubdir: 'webext',
|
|
94
|
+
label: basename(path),
|
|
95
|
+
cache: false,
|
|
96
|
+
define: { 'import.meta.url': JSON.stringify(pathToFileURL(path).href) },
|
|
97
|
+
});
|
|
98
|
+
const copy = join(dirname(bundled), `manifest-${process.pid}-${++loadCounter}.mjs`);
|
|
99
|
+
copyFileSync(bundled, copy);
|
|
100
|
+
url = pathToFileURL(copy).href;
|
|
101
|
+
cleanup = () => rmSync(copy, { force: true });
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
url = `${pathToFileURL(path).href}?t=${statSync(path).mtimeMs}-${++loadCounter}`;
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const mod = (await import(url));
|
|
108
|
+
const template = mod.default;
|
|
109
|
+
if (typeof template !== 'function' && !isRecord(template)) {
|
|
110
|
+
throw new Error(`gjsify webext: ${path} has no default export. Export the manifest object, or a function ` +
|
|
111
|
+
'`(ctx) => manifest` that receives the target.');
|
|
112
|
+
}
|
|
113
|
+
return template;
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
cleanup?.();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/** Keys that name a page, script, stylesheet or image inside the extension. */
|
|
120
|
+
function addPath(out, value) {
|
|
121
|
+
if (typeof value !== 'string' || value === '')
|
|
122
|
+
return;
|
|
123
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(value) || value.includes('*'))
|
|
124
|
+
return;
|
|
125
|
+
out.add(value.replace(/^\//, '').replace(/[?#].*$/, ''));
|
|
126
|
+
}
|
|
127
|
+
function addIconValue(out, value) {
|
|
128
|
+
if (typeof value === 'string')
|
|
129
|
+
addPath(out, value);
|
|
130
|
+
else if (isRecord(value))
|
|
131
|
+
for (const v of Object.values(value))
|
|
132
|
+
addPath(out, v);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Every file inside the extension the manifest refers to by path.
|
|
136
|
+
*
|
|
137
|
+
* Deliberately a list of the keys browsers LOAD from, not a walk over every
|
|
138
|
+
* string: `description` or a `commands` label that happens to look like a path
|
|
139
|
+
* is not a reference.
|
|
140
|
+
*/
|
|
141
|
+
export function manifestFileReferences(manifest) {
|
|
142
|
+
const out = new Set();
|
|
143
|
+
const background = manifest.background;
|
|
144
|
+
if (isRecord(background)) {
|
|
145
|
+
addPath(out, background.service_worker);
|
|
146
|
+
addPath(out, background.page);
|
|
147
|
+
if (Array.isArray(background.scripts))
|
|
148
|
+
for (const s of background.scripts)
|
|
149
|
+
addPath(out, s);
|
|
150
|
+
}
|
|
151
|
+
if (Array.isArray(manifest.content_scripts)) {
|
|
152
|
+
for (const cs of manifest.content_scripts) {
|
|
153
|
+
if (!isRecord(cs))
|
|
154
|
+
continue;
|
|
155
|
+
for (const key of ['js', 'css'])
|
|
156
|
+
if (Array.isArray(cs[key]))
|
|
157
|
+
for (const f of cs[key])
|
|
158
|
+
addPath(out, f);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
for (const key of ['action', 'browser_action', 'page_action', 'sidebar_action']) {
|
|
162
|
+
const action = manifest[key];
|
|
163
|
+
if (!isRecord(action))
|
|
164
|
+
continue;
|
|
165
|
+
addPath(out, action.default_popup);
|
|
166
|
+
addPath(out, action.default_panel);
|
|
167
|
+
addIconValue(out, action.default_icon);
|
|
168
|
+
}
|
|
169
|
+
if (isRecord(manifest.options_ui))
|
|
170
|
+
addPath(out, manifest.options_ui.page);
|
|
171
|
+
addPath(out, manifest.options_page);
|
|
172
|
+
addPath(out, manifest.devtools_page);
|
|
173
|
+
if (isRecord(manifest.side_panel))
|
|
174
|
+
addPath(out, manifest.side_panel.default_path);
|
|
175
|
+
addIconValue(out, manifest.icons);
|
|
176
|
+
if (isRecord(manifest.chrome_url_overrides))
|
|
177
|
+
for (const v of Object.values(manifest.chrome_url_overrides))
|
|
178
|
+
addPath(out, v);
|
|
179
|
+
if (Array.isArray(manifest.web_accessible_resources)) {
|
|
180
|
+
for (const entry of manifest.web_accessible_resources) {
|
|
181
|
+
if (typeof entry === 'string')
|
|
182
|
+
addPath(out, entry);
|
|
183
|
+
else if (isRecord(entry) && Array.isArray(entry.resources))
|
|
184
|
+
for (const r of entry.resources)
|
|
185
|
+
addPath(out, r);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return [...out].sort();
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* What is wrong with a composed manifest for the folder it will sit in.
|
|
192
|
+
* Returns problem sentences; empty means the browser has nothing to refuse
|
|
193
|
+
* that this build could have known about.
|
|
194
|
+
*/
|
|
195
|
+
export function checkManifest(manifest, target, dir) {
|
|
196
|
+
const problems = [];
|
|
197
|
+
if (manifest.manifest_version !== target.manifestVersion) {
|
|
198
|
+
problems.push(`manifest_version is ${JSON.stringify(manifest.manifest_version)}, and target ${target.id} is Manifest V${target.manifestVersion}. ` +
|
|
199
|
+
'gjsify does not convert keys between versions — branch on `ctx.manifestVersion` in the manifest.');
|
|
200
|
+
}
|
|
201
|
+
for (const ref of manifestFileReferences(manifest)) {
|
|
202
|
+
if (!existsSync(join(dir, ref))) {
|
|
203
|
+
problems.push(`the manifest names "${ref}", which the build did not write into ${target.id}/.`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const hasLocales = existsSync(join(dir, '_locales'));
|
|
207
|
+
const defaultLocale = manifest.default_locale;
|
|
208
|
+
if (hasLocales && typeof defaultLocale !== 'string') {
|
|
209
|
+
problems.push('_locales/ exists but the manifest sets no default_locale; the browser refuses to load it.');
|
|
210
|
+
}
|
|
211
|
+
else if (!hasLocales && defaultLocale !== undefined) {
|
|
212
|
+
problems.push(`the manifest sets default_locale "${String(defaultLocale)}" but there is no _locales/ directory.`);
|
|
213
|
+
}
|
|
214
|
+
else if (typeof defaultLocale === 'string' &&
|
|
215
|
+
!existsSync(join(dir, '_locales', defaultLocale, 'messages.json'))) {
|
|
216
|
+
problems.push(`default_locale is "${defaultLocale}" but _locales/${defaultLocale}/messages.json does not exist.`);
|
|
217
|
+
}
|
|
218
|
+
return problems;
|
|
219
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface PageScript {
|
|
2
|
+
/** Absolute path of the source the tag points at. */
|
|
3
|
+
entry: string;
|
|
4
|
+
/** `esm` for a `type="module"` tag, `iife` for a classic one. */
|
|
5
|
+
format: 'esm' | 'iife';
|
|
6
|
+
/** File name written into the target root. */
|
|
7
|
+
output: string;
|
|
8
|
+
}
|
|
9
|
+
export interface PageStylesheet {
|
|
10
|
+
source: string;
|
|
11
|
+
output: string;
|
|
12
|
+
}
|
|
13
|
+
export interface PagePlan {
|
|
14
|
+
html: string;
|
|
15
|
+
scripts: PageScript[];
|
|
16
|
+
stylesheets: PageStylesheet[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Plan one page: which scripts to bundle and which stylesheets to copy, and the
|
|
20
|
+
* page with their references rewritten.
|
|
21
|
+
*
|
|
22
|
+
* Every relative `src` naming a JS/TS file is bundled. A prebuilt script that
|
|
23
|
+
* must load verbatim goes in `public/` and is referenced by an absolute
|
|
24
|
+
* extension path (`/vendor/x.js`), which is left alone.
|
|
25
|
+
*/
|
|
26
|
+
export declare function planPage(name: string, htmlPath: string, html: string): PagePlan;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// An extension page (popup, options, side panel …) as the build sees it: the
|
|
2
|
+
// local scripts it loads, the local stylesheets it links, and the page with
|
|
3
|
+
// both rewritten to point at the built files in the target root.
|
|
4
|
+
//
|
|
5
|
+
// Regex-based on purpose, like `utils/html-entry.ts`: this rewrites attribute
|
|
6
|
+
// values in author-written shell pages, it is not an HTML pipeline. Everything
|
|
7
|
+
// not matched (inline `<style>`, inline scripts, remote URLs) passes through.
|
|
8
|
+
import { basename, dirname, isAbsolute, resolve } from 'node:path';
|
|
9
|
+
const SCRIPT_TAG_RE = /<script\b[^>]*\bsrc\s*=\s*["'][^"']+["'][^>]*>/gi;
|
|
10
|
+
const LINK_TAG_RE = /<link\b[^>]*>/gi;
|
|
11
|
+
const SRC_RE = /\bsrc\s*=\s*(["'])([^"']+)\1/i;
|
|
12
|
+
const HREF_RE = /\bhref\s*=\s*(["'])([^"']+)\1/i;
|
|
13
|
+
const MODULE_RE = /\btype\s*=\s*["']module["']/i;
|
|
14
|
+
const STYLESHEET_RE = /\brel\s*=\s*["'][^"']*\bstylesheet\b[^"']*["']/i;
|
|
15
|
+
/** Remote, protocol-relative, data: and extension-absolute URLs are not ours to bundle. */
|
|
16
|
+
const NOT_LOCAL_RE = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
|
|
17
|
+
const SOURCE_EXT_RE = /\.(?:[cm]?[jt]sx?)$/i;
|
|
18
|
+
/**
|
|
19
|
+
* Plan one page: which scripts to bundle and which stylesheets to copy, and the
|
|
20
|
+
* page with their references rewritten.
|
|
21
|
+
*
|
|
22
|
+
* Every relative `src` naming a JS/TS file is bundled. A prebuilt script that
|
|
23
|
+
* must load verbatim goes in `public/` and is referenced by an absolute
|
|
24
|
+
* extension path (`/vendor/x.js`), which is left alone.
|
|
25
|
+
*/
|
|
26
|
+
export function planPage(name, htmlPath, html) {
|
|
27
|
+
const dir = dirname(htmlPath);
|
|
28
|
+
const scripts = [];
|
|
29
|
+
const stylesheets = [];
|
|
30
|
+
const local = (value) => {
|
|
31
|
+
if (NOT_LOCAL_RE.test(value) || value.startsWith('/'))
|
|
32
|
+
return null;
|
|
33
|
+
const path = value.replace(/[?#].*$/, '');
|
|
34
|
+
return isAbsolute(path) ? path : resolve(dir, path);
|
|
35
|
+
};
|
|
36
|
+
let out = html.replace(SCRIPT_TAG_RE, (tag) => {
|
|
37
|
+
const src = SRC_RE.exec(tag);
|
|
38
|
+
if (!src)
|
|
39
|
+
return tag;
|
|
40
|
+
const entry = local(src[2]);
|
|
41
|
+
if (entry === null || !SOURCE_EXT_RE.test(entry))
|
|
42
|
+
return tag;
|
|
43
|
+
const output = scripts.length === 0 ? `${name}.js` : `${name}-${scripts.length + 1}.js`;
|
|
44
|
+
scripts.push({ entry, format: MODULE_RE.test(tag) ? 'esm' : 'iife', output });
|
|
45
|
+
return tag.replace(SRC_RE, (_whole, quote) => `src=${quote}${output}${quote}`);
|
|
46
|
+
});
|
|
47
|
+
out = out.replace(LINK_TAG_RE, (tag) => {
|
|
48
|
+
if (!STYLESHEET_RE.test(tag))
|
|
49
|
+
return tag;
|
|
50
|
+
const href = HREF_RE.exec(tag);
|
|
51
|
+
if (!href)
|
|
52
|
+
return tag;
|
|
53
|
+
const source = local(href[2]);
|
|
54
|
+
if (source === null)
|
|
55
|
+
return tag;
|
|
56
|
+
const output = basename(source);
|
|
57
|
+
stylesheets.push({ source, output });
|
|
58
|
+
return tag.replace(HREF_RE, (_whole, quote) => `href=${quote}${output}${quote}`);
|
|
59
|
+
});
|
|
60
|
+
return { html: out, scripts, stylesheets };
|
|
61
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type WebextBrowser = 'chrome' | 'edge' | 'firefox' | 'opera' | 'safari';
|
|
2
|
+
export interface WebextTarget {
|
|
3
|
+
/** As spelled in `gjsify.webext.targets` and the output directory name. */
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly browser: WebextBrowser;
|
|
6
|
+
readonly manifestVersion: 2 | 3;
|
|
7
|
+
}
|
|
8
|
+
export declare const WEBEXT_BROWSERS: readonly WebextBrowser[];
|
|
9
|
+
/** What a project gets when it declares no `targets`. */
|
|
10
|
+
export declare const DEFAULT_WEBEXT_TARGETS: readonly string[];
|
|
11
|
+
export declare function parseWebextTarget(id: string): WebextTarget;
|
|
12
|
+
/** Whether `web-ext run` launches this target through its Firefox or its Chromium driver. */
|
|
13
|
+
export declare function webExtRunTarget(target: WebextTarget): 'firefox-desktop' | 'chromium' | null;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// The build targets of `gjsify webext` (ADR 0077 § 2): `<browser>-mv<2|3>`.
|
|
2
|
+
//
|
|
3
|
+
// The vocabulary is mirrored by the `webext` conformance rule in
|
|
4
|
+
// `@gjsify/manifest-conformance` (plain `.mjs`, no import from here), so a
|
|
5
|
+
// target added on one side and not the other is refused by the rule before a
|
|
6
|
+
// build ever sees it.
|
|
7
|
+
export const WEBEXT_BROWSERS = ['chrome', 'edge', 'firefox', 'opera', 'safari'];
|
|
8
|
+
/** What a project gets when it declares no `targets`. */
|
|
9
|
+
export const DEFAULT_WEBEXT_TARGETS = ['chrome-mv3', 'firefox-mv3'];
|
|
10
|
+
/**
|
|
11
|
+
* Why a browser takes no Manifest V2 folder, absent where it still does. Chrome
|
|
12
|
+
* and Edge removed MV2 in the browser, so the folder would build, zip, and then
|
|
13
|
+
* be refused on load. Opera still LOADS an MV2 extension but its store is
|
|
14
|
+
* MV3-only (Opera, 2025-09), so the folder is refused a step later instead. The
|
|
15
|
+
* outcome is the same either way, which is why the sentence is per browser and
|
|
16
|
+
* not one claim the three do not all support.
|
|
17
|
+
*/
|
|
18
|
+
const MV2_UNSUPPORTED = new Map([
|
|
19
|
+
['chrome', 'chrome has removed Manifest V2'],
|
|
20
|
+
['edge', 'edge has removed Manifest V2'],
|
|
21
|
+
['opera', "opera's extension store accepts Manifest V3 only"],
|
|
22
|
+
]);
|
|
23
|
+
export function parseWebextTarget(id) {
|
|
24
|
+
const match = /^([a-z]+)-mv([23])$/.exec(id);
|
|
25
|
+
const browser = match?.[1];
|
|
26
|
+
if (!match || browser === undefined || !WEBEXT_BROWSERS.includes(browser)) {
|
|
27
|
+
throw new Error(`gjsify webext: unknown target "${id}". A target is <browser>-mv<2|3> with browser one of ` +
|
|
28
|
+
`${WEBEXT_BROWSERS.join(', ')} (e.g. chrome-mv3, firefox-mv2).`);
|
|
29
|
+
}
|
|
30
|
+
const manifestVersion = match[2] === '2' ? 2 : 3;
|
|
31
|
+
const why = manifestVersion === 2 ? MV2_UNSUPPORTED.get(browser) : undefined;
|
|
32
|
+
if (why !== undefined) {
|
|
33
|
+
throw new Error(`gjsify webext: target "${id}" does not exist any more: ${why}. Use ${browser}-mv3.`);
|
|
34
|
+
}
|
|
35
|
+
return { id, browser, manifestVersion };
|
|
36
|
+
}
|
|
37
|
+
/** Whether `web-ext run` launches this target through its Firefox or its Chromium driver. */
|
|
38
|
+
export function webExtRunTarget(target) {
|
|
39
|
+
if (target.browser === 'firefox')
|
|
40
|
+
return 'firefox-desktop';
|
|
41
|
+
// By ENGINE, not by brand: the chromium driver's only browser-specific input is the
|
|
42
|
+
// binary, and `--browser-binary` already carries it — the claim branded Chrome and
|
|
43
|
+
// Edge already make. Refusing opera here would be a claim about the operator's
|
|
44
|
+
// binary, which is not what this switch is about.
|
|
45
|
+
if (target.browser === 'chrome' || target.browser === 'edge' || target.browser === 'opera')
|
|
46
|
+
return 'chromium';
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
@@ -38,6 +38,23 @@ export declare function escapeCmdArgument(arg: string, doubleEscapeMetaChars?: b
|
|
|
38
38
|
* `args` are the caller's UNESCAPED arguments; `comspec` is normally `%COMSPEC%`.
|
|
39
39
|
*/
|
|
40
40
|
export declare function buildCmdExeInvocation(commandFile: string, args: readonly string[], comspec?: string): Win32Invocation;
|
|
41
|
+
/**
|
|
42
|
+
* Find `cmd` on `ctx.env.PATH` the way the host's process creation would, or
|
|
43
|
+
* `undefined`.
|
|
44
|
+
*
|
|
45
|
+
* On win32 a bare name is tried with each `PATHEXT` extension per directory — the
|
|
46
|
+
* order `CreateProcess` itself uses — and NEVER as-is: the extensionless member of
|
|
47
|
+
* npm's shim trio (`npm`, `gjsify`) is a `sh` script that exists on disk and cannot
|
|
48
|
+
* be executed, so an existence probe that accepts it answers "yes" for a command
|
|
49
|
+
* that is ENOENT to `spawn`. A name that already carries an extension, and every
|
|
50
|
+
* name off win32, is probed verbatim.
|
|
51
|
+
*
|
|
52
|
+
* Exported because "is X on PATH" is asked outside the spawn path too
|
|
53
|
+
* (`isOnPath` in `check-system-deps.ts`), and a second, PATHEXT-blind walk there
|
|
54
|
+
* reported `signtool` missing on a Windows host with `signtool.exe` on PATH —
|
|
55
|
+
* `gjsify ship win32 --sign` refused to run on the one OS it can sign on.
|
|
56
|
+
*/
|
|
57
|
+
export declare function findOnPath(cmd: string, ctx: Win32ResolveContext): string | undefined;
|
|
41
58
|
/**
|
|
42
59
|
* Rewrite a bare command name into something Windows can actually spawn.
|
|
43
60
|
*
|
|
@@ -90,12 +90,27 @@ function isBareName(cmd) {
|
|
|
90
90
|
return !cmd.includes('/') && !cmd.includes('\\') && !/\.[^.]+$/.test(cmd);
|
|
91
91
|
}
|
|
92
92
|
/**
|
|
93
|
-
* Find `cmd` on the
|
|
94
|
-
*
|
|
93
|
+
* Find `cmd` on `ctx.env.PATH` the way the host's process creation would, or
|
|
94
|
+
* `undefined`.
|
|
95
|
+
*
|
|
96
|
+
* On win32 a bare name is tried with each `PATHEXT` extension per directory — the
|
|
97
|
+
* order `CreateProcess` itself uses — and NEVER as-is: the extensionless member of
|
|
98
|
+
* npm's shim trio (`npm`, `gjsify`) is a `sh` script that exists on disk and cannot
|
|
99
|
+
* be executed, so an existence probe that accepts it answers "yes" for a command
|
|
100
|
+
* that is ENOENT to `spawn`. A name that already carries an extension, and every
|
|
101
|
+
* name off win32, is probed verbatim.
|
|
102
|
+
*
|
|
103
|
+
* Exported because "is X on PATH" is asked outside the spawn path too
|
|
104
|
+
* (`isOnPath` in `check-system-deps.ts`), and a second, PATHEXT-blind walk there
|
|
105
|
+
* reported `signtool` missing on a Windows host with `signtool.exe` on PATH —
|
|
106
|
+
* `gjsify ship win32 --sign` refused to run on the one OS it can sign on.
|
|
95
107
|
*/
|
|
96
|
-
function
|
|
97
|
-
const
|
|
98
|
-
const
|
|
108
|
+
export function findOnPath(cmd, ctx) {
|
|
109
|
+
const win = ctx.platform === 'win32';
|
|
110
|
+
const dirs = (lookupEnv(ctx.env, 'PATH') ?? '').split(win ? ';' : ':').filter(Boolean);
|
|
111
|
+
const exts = win && !/\.[^.\\/]+$/.test(cmd)
|
|
112
|
+
? (lookupEnv(ctx.env, 'PATHEXT') ?? DEFAULT_PATHEXT).split(';').filter(Boolean)
|
|
113
|
+
: [''];
|
|
99
114
|
for (const dir of dirs) {
|
|
100
115
|
for (const ext of exts) {
|
|
101
116
|
const candidate = ctx.join(dir, cmd + ext);
|
|
@@ -115,7 +130,7 @@ function lookupOnPath(cmd, ctx) {
|
|
|
115
130
|
export function resolveWin32Command(cmd, args, ctx) {
|
|
116
131
|
if (ctx.platform !== 'win32' || !isBareName(cmd))
|
|
117
132
|
return undefined;
|
|
118
|
-
const resolved =
|
|
133
|
+
const resolved = findOnPath(cmd, ctx);
|
|
119
134
|
if (!resolved)
|
|
120
135
|
return undefined;
|
|
121
136
|
// `.exe`/`.com` run without an interpreter; spawning the resolved absolute path skips a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.54.0",
|
|
4
4
|
"description": "CLI for Gjsify",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -142,21 +142,22 @@
|
|
|
142
142
|
"cli"
|
|
143
143
|
],
|
|
144
144
|
"dependencies": {
|
|
145
|
-
"@gjsify/
|
|
146
|
-
"@gjsify/
|
|
147
|
-
"@gjsify/
|
|
148
|
-
"@gjsify/node-
|
|
149
|
-
"@gjsify/
|
|
150
|
-
"@gjsify/
|
|
151
|
-
"@gjsify/
|
|
152
|
-
"@gjsify/
|
|
153
|
-
"@gjsify/
|
|
154
|
-
"@gjsify/
|
|
155
|
-
"@gjsify/
|
|
156
|
-
"@gjsify/
|
|
157
|
-
"@gjsify/
|
|
158
|
-
"@gjsify/
|
|
159
|
-
"@gjsify/
|
|
145
|
+
"@gjsify/blueprint": "^0.54.0",
|
|
146
|
+
"@gjsify/buffer": "^0.54.0",
|
|
147
|
+
"@gjsify/create-app": "^0.54.0",
|
|
148
|
+
"@gjsify/node-globals": "^0.54.0",
|
|
149
|
+
"@gjsify/node-polyfills": "^0.54.0",
|
|
150
|
+
"@gjsify/npm-registry": "^0.54.0",
|
|
151
|
+
"@gjsify/resolve-npm": "^0.54.0",
|
|
152
|
+
"@gjsify/rolldown-plugin-gjsify": "^0.54.0",
|
|
153
|
+
"@gjsify/vite-plugin-blueprint": "^0.54.0",
|
|
154
|
+
"@gjsify/rolldown-plugin-pnp": "^0.54.0",
|
|
155
|
+
"@gjsify/semver": "^0.54.0",
|
|
156
|
+
"@gjsify/tar": "^0.54.0",
|
|
157
|
+
"@gjsify/tsc": "^0.54.0",
|
|
158
|
+
"@gjsify/utils": "^0.54.0",
|
|
159
|
+
"@gjsify/web-polyfills": "^0.54.0",
|
|
160
|
+
"@gjsify/workspace": "^0.54.0",
|
|
160
161
|
"acorn": "^8.17.0",
|
|
161
162
|
"cosmiconfig": "^9.0.2",
|
|
162
163
|
"get-tsconfig": "^4.14.0",
|
|
@@ -165,13 +166,13 @@
|
|
|
165
166
|
"yargs": "^18.0.0"
|
|
166
167
|
},
|
|
167
168
|
"devDependencies": {
|
|
168
|
-
"@gjsify/unit": "^0.
|
|
169
|
+
"@gjsify/unit": "^0.54.0",
|
|
169
170
|
"@types/yargs": "^17.0.35",
|
|
170
171
|
"typescript": "^6.0.3"
|
|
171
172
|
},
|
|
172
173
|
"peerDependencies": {
|
|
173
|
-
"@gjsify/oxfmt-native": "^0.
|
|
174
|
-
"@gjsify/rolldown-native": "^0.
|
|
174
|
+
"@gjsify/oxfmt-native": "^0.54.0",
|
|
175
|
+
"@gjsify/rolldown-native": "^0.54.0"
|
|
175
176
|
},
|
|
176
177
|
"peerDependenciesMeta": {
|
|
177
178
|
"@gjsify/oxfmt-native": {
|