@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.
Files changed (107) hide show
  1. package/dist/cli.gjs.mjs +351 -305
  2. package/lib/actions/build.d.ts +22 -0
  3. package/lib/actions/build.js +37 -9
  4. package/lib/bundler-pick.d.ts +72 -1
  5. package/lib/bundler-pick.js +175 -17
  6. package/lib/cli-app.js +7 -2
  7. package/lib/commands/blueprint/index.d.ts +4 -0
  8. package/lib/commands/blueprint/index.js +15 -0
  9. package/lib/commands/blueprint/types.d.ts +8 -0
  10. package/lib/commands/blueprint/types.js +124 -0
  11. package/lib/commands/devtools.d.ts +21 -0
  12. package/lib/commands/devtools.js +146 -0
  13. package/lib/commands/env.d.ts +31 -0
  14. package/lib/commands/env.js +114 -0
  15. package/lib/commands/exec.d.ts +10 -0
  16. package/lib/commands/exec.js +208 -0
  17. package/lib/commands/foreach.js +5 -97
  18. package/lib/commands/format.js +26 -6
  19. package/lib/commands/index.d.ts +5 -0
  20. package/lib/commands/index.js +5 -0
  21. package/lib/commands/install.d.ts +2 -2
  22. package/lib/commands/install.js +5 -5
  23. package/lib/commands/self-update.js +2 -1
  24. package/lib/commands/ship.js +51 -8
  25. package/lib/commands/test.d.ts +18 -9
  26. package/lib/commands/test.js +56 -11
  27. package/lib/commands/tsc.js +4 -4
  28. package/lib/commands/upgrade.d.ts +15 -0
  29. package/lib/commands/upgrade.js +76 -2
  30. package/lib/commands/webext/index.d.ts +2 -0
  31. package/lib/commands/webext/index.js +15 -0
  32. package/lib/commands/webext/subcommands.d.ts +16 -0
  33. package/lib/commands/webext/subcommands.js +204 -0
  34. package/lib/types/config-data.d.ts +1 -1
  35. package/lib/utils/bin-shim.d.ts +8 -2
  36. package/lib/utils/bin-shim.js +6 -5
  37. package/lib/utils/bundle-inputs.d.ts +84 -0
  38. package/lib/utils/bundle-inputs.js +268 -0
  39. package/lib/utils/check-system-deps.d.ts +4 -2
  40. package/lib/utils/check-system-deps.js +21 -26
  41. package/lib/utils/dev-link.d.ts +9 -6
  42. package/lib/utils/dev-link.js +71 -19
  43. package/lib/utils/exec-bin.d.ts +137 -0
  44. package/lib/utils/exec-bin.js +321 -0
  45. package/lib/utils/gi-runtime-paths.js +3 -2
  46. package/lib/utils/gitignore.d.ts +15 -0
  47. package/lib/utils/gitignore.js +178 -0
  48. package/lib/utils/gjsify-shim.d.ts +47 -0
  49. package/lib/utils/gjsify-shim.js +97 -8
  50. package/lib/utils/install-backend-native.d.ts +19 -1
  51. package/lib/utils/install-backend-native.js +93 -7
  52. package/lib/utils/install-lock.js +4 -25
  53. package/lib/utils/native-skip-scan.d.ts +20 -0
  54. package/lib/utils/native-skip-scan.js +343 -0
  55. package/lib/utils/node-script.js +1 -1
  56. package/lib/utils/oxc-resolve.js +46 -5
  57. package/lib/utils/process-table.d.ts +22 -0
  58. package/lib/utils/process-table.js +188 -0
  59. package/lib/utils/prune-prefix.js +1 -1
  60. package/lib/utils/run-gjs.d.ts +15 -1
  61. package/lib/utils/run-gjs.js +16 -7
  62. package/lib/utils/run-node.d.ts +2 -0
  63. package/lib/utils/run-node.js +2 -1
  64. package/lib/utils/runtimes.js +14 -13
  65. package/lib/utils/ship/app-runtime.d.ts +7 -5
  66. package/lib/utils/ship/app-runtime.js +11 -8
  67. package/lib/utils/ship/appimage.d.ts +9 -0
  68. package/lib/utils/ship/appimage.js +2 -1
  69. package/lib/utils/ship/deb.d.ts +10 -0
  70. package/lib/utils/ship/deb.js +7 -0
  71. package/lib/utils/ship/depends.d.ts +37 -3
  72. package/lib/utils/ship/depends.js +21 -9
  73. package/lib/utils/ship/gi-namespaces.d.ts +16 -0
  74. package/lib/utils/ship/gi-namespaces.js +64 -1
  75. package/lib/utils/ship/gzip.js +1 -1
  76. package/lib/utils/ship/icons.d.ts +6 -0
  77. package/lib/utils/ship/icons.js +6 -5
  78. package/lib/utils/ship/launcher.js +2 -2
  79. package/lib/utils/ship/layout.js +25 -2
  80. package/lib/utils/ship/rpm.d.ts +5 -0
  81. package/lib/utils/ship/rpm.js +42 -0
  82. package/lib/utils/ship/settings.js +6 -0
  83. package/lib/utils/ship/signing.d.ts +7 -2
  84. package/lib/utils/ship/signing.js +59 -32
  85. package/lib/utils/ship/stage-manifest.d.ts +14 -0
  86. package/lib/utils/ship/stage-manifest.js +7 -0
  87. package/lib/utils/spawn.d.ts +13 -0
  88. package/lib/utils/spawn.js +10 -5
  89. package/lib/utils/watch-loop.d.ts +15 -1
  90. package/lib/utils/watch-loop.js +9 -5
  91. package/lib/utils/webext/build.d.ts +42 -0
  92. package/lib/utils/webext/build.js +120 -0
  93. package/lib/utils/webext/config.d.ts +72 -0
  94. package/lib/utils/webext/config.js +234 -0
  95. package/lib/utils/webext/icons.d.ts +24 -0
  96. package/lib/utils/webext/icons.js +83 -0
  97. package/lib/utils/webext/launch.d.ts +32 -0
  98. package/lib/utils/webext/launch.js +77 -0
  99. package/lib/utils/webext/manifest.d.ts +53 -0
  100. package/lib/utils/webext/manifest.js +219 -0
  101. package/lib/utils/webext/pages.d.ts +26 -0
  102. package/lib/utils/webext/pages.js +61 -0
  103. package/lib/utils/webext/targets.d.ts +13 -0
  104. package/lib/utils/webext/targets.js +48 -0
  105. package/lib/utils/win32-command.d.ts +17 -0
  106. package/lib/utils/win32-command.js +21 -6
  107. 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 child's `PATH`, trying each `PATHEXT` extension per
94
- * directory — the order `CreateProcess` itself uses.
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 lookupOnPath(cmd, ctx) {
97
- const dirs = (lookupEnv(ctx.env, 'PATH') ?? '').split(';').filter(Boolean);
98
- const exts = (lookupEnv(ctx.env, 'PATHEXT') ?? DEFAULT_PATHEXT).split(';').filter(Boolean);
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 = lookupOnPath(cmd, ctx);
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.52.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/buffer": "^0.52.0",
146
- "@gjsify/create-app": "^0.52.0",
147
- "@gjsify/node-globals": "^0.52.0",
148
- "@gjsify/node-polyfills": "^0.52.0",
149
- "@gjsify/npm-registry": "^0.52.0",
150
- "@gjsify/resolve-npm": "^0.52.0",
151
- "@gjsify/rolldown-plugin-gjsify": "^0.52.0",
152
- "@gjsify/vite-plugin-blueprint": "^0.52.0",
153
- "@gjsify/rolldown-plugin-pnp": "^0.52.0",
154
- "@gjsify/semver": "^0.52.0",
155
- "@gjsify/tar": "^0.52.0",
156
- "@gjsify/tsc": "^0.52.0",
157
- "@gjsify/utils": "^0.52.0",
158
- "@gjsify/web-polyfills": "^0.52.0",
159
- "@gjsify/workspace": "^0.52.0",
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.52.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.52.0",
174
- "@gjsify/rolldown-native": "^0.52.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": {