@gjsify/cli 0.52.0 → 0.53.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 (81) hide show
  1. package/dist/cli.gjs.mjs +329 -298
  2. package/lib/actions/build.d.ts +22 -0
  3. package/lib/actions/build.js +37 -9
  4. package/lib/bundler-pick.d.ts +37 -1
  5. package/lib/bundler-pick.js +108 -17
  6. package/lib/cli-app.js +5 -2
  7. package/lib/commands/env.d.ts +31 -0
  8. package/lib/commands/env.js +114 -0
  9. package/lib/commands/exec.d.ts +10 -0
  10. package/lib/commands/exec.js +208 -0
  11. package/lib/commands/foreach.js +5 -97
  12. package/lib/commands/format.js +26 -6
  13. package/lib/commands/index.d.ts +3 -0
  14. package/lib/commands/index.js +3 -0
  15. package/lib/commands/install.d.ts +2 -2
  16. package/lib/commands/install.js +5 -5
  17. package/lib/commands/self-update.js +2 -1
  18. package/lib/commands/test.d.ts +18 -9
  19. package/lib/commands/test.js +56 -11
  20. package/lib/commands/tsc.js +4 -4
  21. package/lib/commands/upgrade.d.ts +15 -0
  22. package/lib/commands/upgrade.js +76 -2
  23. package/lib/commands/webext/index.d.ts +2 -0
  24. package/lib/commands/webext/index.js +15 -0
  25. package/lib/commands/webext/subcommands.d.ts +16 -0
  26. package/lib/commands/webext/subcommands.js +204 -0
  27. package/lib/types/config-data.d.ts +1 -1
  28. package/lib/utils/bin-shim.d.ts +8 -2
  29. package/lib/utils/bin-shim.js +6 -5
  30. package/lib/utils/bundle-inputs.d.ts +84 -0
  31. package/lib/utils/bundle-inputs.js +268 -0
  32. package/lib/utils/check-system-deps.d.ts +4 -2
  33. package/lib/utils/check-system-deps.js +21 -26
  34. package/lib/utils/dev-link.d.ts +9 -6
  35. package/lib/utils/dev-link.js +71 -19
  36. package/lib/utils/exec-bin.d.ts +137 -0
  37. package/lib/utils/exec-bin.js +321 -0
  38. package/lib/utils/gi-runtime-paths.js +3 -2
  39. package/lib/utils/gitignore.d.ts +15 -0
  40. package/lib/utils/gitignore.js +178 -0
  41. package/lib/utils/gjsify-shim.d.ts +47 -0
  42. package/lib/utils/gjsify-shim.js +97 -8
  43. package/lib/utils/install-backend-native.d.ts +19 -1
  44. package/lib/utils/install-backend-native.js +93 -7
  45. package/lib/utils/install-lock.js +4 -25
  46. package/lib/utils/native-skip-scan.d.ts +20 -0
  47. package/lib/utils/native-skip-scan.js +343 -0
  48. package/lib/utils/node-script.js +1 -1
  49. package/lib/utils/oxc-resolve.js +42 -3
  50. package/lib/utils/process-table.d.ts +22 -0
  51. package/lib/utils/process-table.js +188 -0
  52. package/lib/utils/prune-prefix.js +1 -1
  53. package/lib/utils/run-gjs.d.ts +15 -1
  54. package/lib/utils/run-gjs.js +16 -7
  55. package/lib/utils/run-node.d.ts +2 -0
  56. package/lib/utils/run-node.js +2 -1
  57. package/lib/utils/runtimes.js +14 -13
  58. package/lib/utils/ship/gzip.js +1 -1
  59. package/lib/utils/ship/icons.d.ts +6 -0
  60. package/lib/utils/ship/icons.js +6 -5
  61. package/lib/utils/spawn.d.ts +13 -0
  62. package/lib/utils/spawn.js +10 -5
  63. package/lib/utils/watch-loop.d.ts +15 -1
  64. package/lib/utils/watch-loop.js +9 -5
  65. package/lib/utils/webext/build.d.ts +42 -0
  66. package/lib/utils/webext/build.js +120 -0
  67. package/lib/utils/webext/config.d.ts +72 -0
  68. package/lib/utils/webext/config.js +234 -0
  69. package/lib/utils/webext/icons.d.ts +24 -0
  70. package/lib/utils/webext/icons.js +83 -0
  71. package/lib/utils/webext/launch.d.ts +32 -0
  72. package/lib/utils/webext/launch.js +77 -0
  73. package/lib/utils/webext/manifest.d.ts +53 -0
  74. package/lib/utils/webext/manifest.js +219 -0
  75. package/lib/utils/webext/pages.d.ts +26 -0
  76. package/lib/utils/webext/pages.js +61 -0
  77. package/lib/utils/webext/targets.d.ts +13 -0
  78. package/lib/utils/webext/targets.js +36 -0
  79. package/lib/utils/win32-command.d.ts +17 -0
  80. package/lib/utils/win32-command.js +21 -6
  81. package/package.json +19 -19
@@ -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' | '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,36 @@
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', 'safari'];
8
+ /** What a project gets when it declares no `targets`. */
9
+ export const DEFAULT_WEBEXT_TARGETS = ['chrome-mv3', 'firefox-mv3'];
10
+ /**
11
+ * Chromium-based browsers no longer load Manifest V2 at all, so a `chrome-mv2`
12
+ * folder would build, zip, and then be rejected by the browser and the store.
13
+ */
14
+ const MV2_REMOVED = new Set(['chrome', 'edge']);
15
+ export function parseWebextTarget(id) {
16
+ const match = /^([a-z]+)-mv([23])$/.exec(id);
17
+ const browser = match?.[1];
18
+ if (!match || browser === undefined || !WEBEXT_BROWSERS.includes(browser)) {
19
+ throw new Error(`gjsify webext: unknown target "${id}". A target is <browser>-mv<2|3> with browser one of ` +
20
+ `${WEBEXT_BROWSERS.join(', ')} (e.g. chrome-mv3, firefox-mv2).`);
21
+ }
22
+ const manifestVersion = match[2] === '2' ? 2 : 3;
23
+ if (manifestVersion === 2 && MV2_REMOVED.has(browser)) {
24
+ throw new Error(`gjsify webext: target "${id}" does not exist any more: ${browser} has removed Manifest V2. ` +
25
+ `Use ${browser}-mv3.`);
26
+ }
27
+ return { id, browser, manifestVersion };
28
+ }
29
+ /** Whether `web-ext run` launches this target through its Firefox or its Chromium driver. */
30
+ export function webExtRunTarget(target) {
31
+ if (target.browser === 'firefox')
32
+ return 'firefox-desktop';
33
+ if (target.browser === 'chrome' || target.browser === 'edge')
34
+ return 'chromium';
35
+ return null;
36
+ }
@@ -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.53.0",
4
4
  "description": "CLI for Gjsify",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -142,21 +142,21 @@
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/buffer": "^0.53.0",
146
+ "@gjsify/create-app": "^0.53.0",
147
+ "@gjsify/node-globals": "^0.53.0",
148
+ "@gjsify/node-polyfills": "^0.53.0",
149
+ "@gjsify/npm-registry": "^0.53.0",
150
+ "@gjsify/resolve-npm": "^0.53.0",
151
+ "@gjsify/rolldown-plugin-gjsify": "^0.53.0",
152
+ "@gjsify/vite-plugin-blueprint": "^0.53.0",
153
+ "@gjsify/rolldown-plugin-pnp": "^0.53.0",
154
+ "@gjsify/semver": "^0.53.0",
155
+ "@gjsify/tar": "^0.53.0",
156
+ "@gjsify/tsc": "^0.53.0",
157
+ "@gjsify/utils": "^0.53.0",
158
+ "@gjsify/web-polyfills": "^0.53.0",
159
+ "@gjsify/workspace": "^0.53.0",
160
160
  "acorn": "^8.17.0",
161
161
  "cosmiconfig": "^9.0.2",
162
162
  "get-tsconfig": "^4.14.0",
@@ -165,13 +165,13 @@
165
165
  "yargs": "^18.0.0"
166
166
  },
167
167
  "devDependencies": {
168
- "@gjsify/unit": "^0.52.0",
168
+ "@gjsify/unit": "^0.53.0",
169
169
  "@types/yargs": "^17.0.35",
170
170
  "typescript": "^6.0.3"
171
171
  },
172
172
  "peerDependencies": {
173
- "@gjsify/oxfmt-native": "^0.52.0",
174
- "@gjsify/rolldown-native": "^0.52.0"
173
+ "@gjsify/oxfmt-native": "^0.53.0",
174
+ "@gjsify/rolldown-native": "^0.53.0"
175
175
  },
176
176
  "peerDependenciesMeta": {
177
177
  "@gjsify/oxfmt-native": {