@gjsify/cli 0.31.0 → 0.32.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 +191 -191
- package/lib/commands/self-update.d.ts +27 -0
- package/lib/commands/self-update.js +77 -2
- package/lib/commands/showcase.d.ts +14 -0
- package/lib/commands/showcase.js +130 -10
- package/lib/utils/install-global.d.ts +49 -0
- package/lib/utils/install-global.js +92 -4
- package/package.json +17 -17
|
@@ -6,4 +6,31 @@ interface SelfUpdateOptions {
|
|
|
6
6
|
skipDeps?: boolean;
|
|
7
7
|
}
|
|
8
8
|
export declare const selfUpdateCommand: Command<unknown, SelfUpdateOptions>;
|
|
9
|
+
/** Verdict of the post-install "did this change what runs?" check. */
|
|
10
|
+
export type PathVerdict = {
|
|
11
|
+
ok: true;
|
|
12
|
+
} | {
|
|
13
|
+
ok: false;
|
|
14
|
+
message: string;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Compare where PATH sends `binName` against the `binDir` we just linked into.
|
|
18
|
+
*
|
|
19
|
+
* Pure so the three outcomes are unit-testable off-host — the interesting two
|
|
20
|
+
* are exactly the ones a developer's own machine never produces, since a working
|
|
21
|
+
* install resolves to its own `binDir` by definition.
|
|
22
|
+
*
|
|
23
|
+
* Path comparison is case-INSENSITIVE on win32: `C:\Users\X\.local\bin` and
|
|
24
|
+
* `c:\users\x\.local\bin` are one directory there, and a case-sensitive compare
|
|
25
|
+
* would report a correct install as shadowed.
|
|
26
|
+
*/
|
|
27
|
+
export declare function verifyPathResolution(opts: {
|
|
28
|
+
binName: string;
|
|
29
|
+
binDir: string;
|
|
30
|
+
resolvedBin: string | null;
|
|
31
|
+
/** `null` is what `readCurrentVersion()` returns when it cannot tell. */
|
|
32
|
+
runningVersion: string | null | undefined;
|
|
33
|
+
targetVersion: string;
|
|
34
|
+
platform?: NodeJS.Platform;
|
|
35
|
+
}): PathVerdict;
|
|
9
36
|
export {};
|
|
@@ -11,12 +11,21 @@
|
|
|
11
11
|
// `defaultGlobalLayout().prefix` (i.e. via `gjsify install -g` or via the
|
|
12
12
|
// `install.mjs` bootstrap). Installs from `npm install -g @gjsify/cli` land
|
|
13
13
|
// elsewhere and we don't try to chase them — we print a warning and exit.
|
|
14
|
+
//
|
|
15
|
+
// AND THE WARNING IS NOT ENOUGH ON ITS OWN (#1064). This command's whole job is
|
|
16
|
+
// to change which binary the user's next `gjsify` runs, so success has to be
|
|
17
|
+
// measured on PATH, not on the prefix it wrote. Reported from a Windows host:
|
|
18
|
+
// the pre-install warning fired, the install succeeded, `Updated @gjsify/cli to
|
|
19
|
+
// v0.31.0` printed — and the shell kept resolving a v0.26.0 npm-global install,
|
|
20
|
+
// so the very next command re-hit the `dlx` EPERM fixed three releases earlier.
|
|
21
|
+
// A silent no-op that reports success is worse than a clean failure, so the
|
|
22
|
+
// post-link verification below is fatal.
|
|
14
23
|
import { existsSync, readFileSync } from 'node:fs';
|
|
15
|
-
import { dirname, join, resolve } from 'node:path';
|
|
24
|
+
import { dirname, join, posix, resolve, win32 } from 'node:path';
|
|
16
25
|
import { fileURLToPath } from 'node:url';
|
|
17
26
|
import { fetchPackument } from '@gjsify/npm-registry';
|
|
18
27
|
import { installPackages, makeProgressReporter } from '../utils/install-backend.js';
|
|
19
|
-
import { defaultGlobalLayout, hasBundlerEngineInstalled, installGjsEnginePackages, linkGlobalBins, } from '../utils/install-global.js';
|
|
28
|
+
import { defaultGlobalLayout, globalLayoutIsDefault, hasBundlerEngineInstalled, installGjsEnginePackages, linkGlobalBins, resolveBinOnPath, } from '../utils/install-global.js';
|
|
20
29
|
const PACKAGE_NAME = '@gjsify/cli';
|
|
21
30
|
export const selfUpdateCommand = {
|
|
22
31
|
command: 'self-update',
|
|
@@ -157,6 +166,27 @@ export const selfUpdateCommand = {
|
|
|
157
166
|
console.log(` • ${bin.link} → ${bin.target}`);
|
|
158
167
|
}
|
|
159
168
|
}
|
|
169
|
+
// Did the update change what `gjsify` RUNS? Everything above only proves
|
|
170
|
+
// we wrote the prefix. See the #1064 note at the top of this file.
|
|
171
|
+
//
|
|
172
|
+
// Only for the user's OWN layout: a caller that redirected the install
|
|
173
|
+
// with GJSIFY_GLOBAL_PREFIX/_BIN_DIR manages placement itself, and
|
|
174
|
+
// telling a harness that installed into a temp prefix "your PATH still
|
|
175
|
+
// points elsewhere" is true and useless.
|
|
176
|
+
if (globalLayoutIsDefault()) {
|
|
177
|
+
const verdict = verifyPathResolution({
|
|
178
|
+
binName: BIN_NAME,
|
|
179
|
+
binDir: layout.binDir,
|
|
180
|
+
resolvedBin: resolveBinOnPath(BIN_NAME),
|
|
181
|
+
runningVersion: currentVersion,
|
|
182
|
+
targetVersion: target,
|
|
183
|
+
});
|
|
184
|
+
if (!verdict.ok) {
|
|
185
|
+
console.error(`\nInstalled ${PACKAGE_NAME} v${target} at ${layout.prefix}`);
|
|
186
|
+
console.error(verdict.message);
|
|
187
|
+
return process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
160
190
|
console.log(`\nUpdated ${PACKAGE_NAME} to v${target}.`);
|
|
161
191
|
if (pullDeps) {
|
|
162
192
|
console.log('Runtime dependencies were resolved alongside the bundle.');
|
|
@@ -166,6 +196,51 @@ export const selfUpdateCommand = {
|
|
|
166
196
|
}
|
|
167
197
|
},
|
|
168
198
|
};
|
|
199
|
+
/** The bin `@gjsify/cli` installs, and the name a user types. */
|
|
200
|
+
const BIN_NAME = 'gjsify';
|
|
201
|
+
/**
|
|
202
|
+
* Compare where PATH sends `binName` against the `binDir` we just linked into.
|
|
203
|
+
*
|
|
204
|
+
* Pure so the three outcomes are unit-testable off-host — the interesting two
|
|
205
|
+
* are exactly the ones a developer's own machine never produces, since a working
|
|
206
|
+
* install resolves to its own `binDir` by definition.
|
|
207
|
+
*
|
|
208
|
+
* Path comparison is case-INSENSITIVE on win32: `C:\Users\X\.local\bin` and
|
|
209
|
+
* `c:\users\x\.local\bin` are one directory there, and a case-sensitive compare
|
|
210
|
+
* would report a correct install as shadowed.
|
|
211
|
+
*/
|
|
212
|
+
export function verifyPathResolution(opts) {
|
|
213
|
+
const platform = opts.platform ?? process.platform;
|
|
214
|
+
// The TARGET platform's path module, never the host's — see `pathFor` in
|
|
215
|
+
// `utils/install-global.ts` for why (`resolve('C:\\x')` on posix is wrong).
|
|
216
|
+
const p = platform === 'win32' ? win32 : posix;
|
|
217
|
+
const norm = (v) => (platform === 'win32' ? p.resolve(v).toLowerCase() : p.resolve(v));
|
|
218
|
+
if (opts.resolvedBin === null) {
|
|
219
|
+
return {
|
|
220
|
+
ok: false,
|
|
221
|
+
message: `BUT nothing named \`${opts.binName}\` is on PATH, so the update cannot take effect.\n\n` +
|
|
222
|
+
` Add the bin dir to PATH:\n ${opts.binDir}`,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
if (norm(p.dirname(opts.resolvedBin)) === norm(opts.binDir))
|
|
226
|
+
return { ok: true };
|
|
227
|
+
// Deliberately NOT split into "a rival install" vs "a transient runner"
|
|
228
|
+
// (npx/dlx, whose temp bin wins PATH for the length of the run). Both are the
|
|
229
|
+
// same fact — `gjsify` does not currently mean the tree we just wrote — and
|
|
230
|
+
// guessing which one it is would be a heuristic that can only be wrong. So
|
|
231
|
+
// the finding is stated and both remedies are offered.
|
|
232
|
+
const running = opts.runningVersion ? ` (v${opts.runningVersion})` : '';
|
|
233
|
+
return {
|
|
234
|
+
ok: false,
|
|
235
|
+
message: `BUT \`${opts.binName}\` on PATH resolves to\n ${opts.resolvedBin}${running}\n` +
|
|
236
|
+
`which is NOT the install that was just updated, so the next \`${opts.binName}\`\n` +
|
|
237
|
+
`you run will not be v${opts.targetVersion}.\n\n` +
|
|
238
|
+
` If that is another install, remove it (e.g. \`npm uninstall -g ${PACKAGE_NAME}\`)\n` +
|
|
239
|
+
` or put ${opts.binDir} ahead of it on PATH.\n` +
|
|
240
|
+
` If you ran this through npx/dlx, that temporary copy owns the name only for\n` +
|
|
241
|
+
` this run — open a new shell and check \`${opts.binName} --version\`.`,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
169
244
|
/**
|
|
170
245
|
* Resolve the CLI's own `package.json#version`. Walks up from
|
|
171
246
|
* `import.meta.url` (the bundle file under GJS, or `lib/index.js` under
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Command } from '../types/index.js';
|
|
2
|
+
import { type Packument } from '@gjsify/npm-registry';
|
|
3
|
+
import { type ExampleRuntime } from '../utils/runtimes.js';
|
|
2
4
|
/**
|
|
3
5
|
* Pinned invocations for the three runners that cache an unpinned bin.
|
|
4
6
|
*
|
|
@@ -22,4 +24,16 @@ interface ShowcaseOptions {
|
|
|
22
24
|
runtime?: string;
|
|
23
25
|
}
|
|
24
26
|
export declare const showcaseCommand: Command<unknown, ShowcaseOptions>;
|
|
27
|
+
/**
|
|
28
|
+
* The pure half of {@link preflightDeclaredRuntimes}: pick the version whose
|
|
29
|
+
* declaration decides, and read it.
|
|
30
|
+
*
|
|
31
|
+
* Separate from the fetch so the fail-safe direction is unit-testable — that
|
|
32
|
+
* contract ("never fail a run that would have worked") is the only thing that
|
|
33
|
+
* makes a pre-install check safe, and it lives entirely in which branches return
|
|
34
|
+
* `undefined`.
|
|
35
|
+
*
|
|
36
|
+
* @param cliVersion the CLI's own version; `showcase` pins the showcase to it.
|
|
37
|
+
*/
|
|
38
|
+
export declare function declaredRuntimesFromPackument(packument: Packument, cliVersion: string | undefined): ExampleRuntime[] | null | undefined;
|
|
25
39
|
export {};
|
package/lib/commands/showcase.js
CHANGED
|
@@ -4,6 +4,8 @@ import { createRequire } from 'node:module';
|
|
|
4
4
|
import { dirname } from 'node:path';
|
|
5
5
|
import { existsSync, readFileSync } from 'node:fs';
|
|
6
6
|
import { ensurePkgDir } from './dlx.js';
|
|
7
|
+
import { fetchPackument } from '@gjsify/npm-registry';
|
|
8
|
+
import { loadNpmrc } from '../utils/load-npmrc.js';
|
|
7
9
|
import { parseSpec } from '../utils/parse-spec.js';
|
|
8
10
|
import { resolveNodeEntry } from '../utils/resolve-gjs-entry.js';
|
|
9
11
|
import { runRuntimeBundle } from '../utils/run-node.js';
|
|
@@ -212,15 +214,9 @@ export const showcaseCommand = {
|
|
|
212
214
|
*/
|
|
213
215
|
async function resolveShowcaseDir(showcase, cliVersion, extraSpecs = []) {
|
|
214
216
|
// Local-first: a workspace showcase resolves via its own package name.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (existsSync(localPkgJson))
|
|
219
|
-
return dirname(localPkgJson);
|
|
220
|
-
}
|
|
221
|
-
catch {
|
|
222
|
-
// Not resolvable locally — fall through to dlx install.
|
|
223
|
-
}
|
|
217
|
+
const local = resolveLocalShowcaseDir(showcase);
|
|
218
|
+
if (local)
|
|
219
|
+
return local;
|
|
224
220
|
const spec = cliVersion ? `${showcase.packageName}@${cliVersion}` : showcase.packageName;
|
|
225
221
|
const { pkgDir } = await ensurePkgDir(parseSpec(spec), {
|
|
226
222
|
verbose: false,
|
|
@@ -230,6 +226,117 @@ async function resolveShowcaseDir(showcase, cliVersion, extraSpecs = []) {
|
|
|
230
226
|
});
|
|
231
227
|
return pkgDir;
|
|
232
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* The LOCAL (workspace / node_modules) directory of a showcase, or `null`.
|
|
231
|
+
*
|
|
232
|
+
* Factored out because two callers need the same question answered: the resolver
|
|
233
|
+
* below, and the pre-flight, which must know whether an install is even going to
|
|
234
|
+
* happen before deciding to spend a registry request avoiding it.
|
|
235
|
+
*/
|
|
236
|
+
function resolveLocalShowcaseDir(showcase) {
|
|
237
|
+
try {
|
|
238
|
+
const req = createRequire(import.meta.url);
|
|
239
|
+
const localPkgJson = req.resolve(`${showcase.packageName}/package.json`);
|
|
240
|
+
if (existsSync(localPkgJson))
|
|
241
|
+
return dirname(localPkgJson);
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// Not resolvable locally — the caller falls back to a dlx install.
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* The batteries-included GTK runtime to add to a showcase's dlx tree, if any.
|
|
250
|
+
*
|
|
251
|
+
* WHY THE LAUNCHER AND NOT THE PACKAGE. `@gjsify/node-gi` deliberately declares
|
|
252
|
+
* no bundle: making one its dependency is #910, where a from-source addon met a
|
|
253
|
+
* foreign GTK and produced wrong method entries and a 29-minute timeout. The rule
|
|
254
|
+
* that replaced it is "whoever SHIPS an app declares the bundle" — and for a
|
|
255
|
+
* showcase, the shipper is this command, which assembles the dlx tree. Same
|
|
256
|
+
* reasoning, same place, as the `@gjsify/node-gi` spec beside it.
|
|
257
|
+
*
|
|
258
|
+
* Only where a system GTK is not a given, which is exactly where a bundle is
|
|
259
|
+
* built: Windows has none at all (#1063 — every `gi://` showcase died in
|
|
260
|
+
* `ERR_DLOPEN_FAILED` on a clean host), and macOS has Homebrew's, which is
|
|
261
|
+
* measurably not findable. On Linux the distribution's GTK is the tested
|
|
262
|
+
* combination and node-gi's own policy prefers it, so pulling ~80 MB nobody would
|
|
263
|
+
* use would be waste.
|
|
264
|
+
*
|
|
265
|
+
* The tree only ever holds the PUBLISHED PREBUILD, never a from-source build, so
|
|
266
|
+
* the mismatch #910 hit cannot arise here by construction.
|
|
267
|
+
*/
|
|
268
|
+
function gtkRuntimeSpecs(cliVersion) {
|
|
269
|
+
const target = `${process.platform}-${process.arch}`;
|
|
270
|
+
const shipped = ['win32-x64', 'darwin-arm64', 'darwin-x64'];
|
|
271
|
+
if (!shipped.includes(target))
|
|
272
|
+
return [];
|
|
273
|
+
const name = `@gjsify/gtk-runtime-${target}`;
|
|
274
|
+
return [cliVersion ? `${name}@${cliVersion}` : name];
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Read a showcase's declared runtimes from the REGISTRY, with no install (#1069).
|
|
278
|
+
*
|
|
279
|
+
* WHY. `--runtime node` on a gjs-only showcase used to resolve and download the
|
|
280
|
+
* whole dlx tree — plus the pinned native `@gjsify/node-gi` bridge, added
|
|
281
|
+
* BECAUSE `--runtime node` was requested — and only then read the declaration
|
|
282
|
+
* that rejects the run. Measured for `adwaita-storybook`: a 310 KB showcase with
|
|
283
|
+
* no runtime deps, and 3.77 MB of `node-gi` fetched to support a runtime the
|
|
284
|
+
* package was about to refuse. The full packument carries
|
|
285
|
+
* `gjsify.example.runtimes` verbatim, so one metadata GET answers it.
|
|
286
|
+
*
|
|
287
|
+
* NOT solved by putting `runtimes` in `showcases.json`: that is a second copy of
|
|
288
|
+
* a declaration the package already owns, maintained by hand. See the
|
|
289
|
+
* `needsWebgl` post-mortem in `utils/discover-showcases.ts` — "a field nobody
|
|
290
|
+
* reads is a field nobody maintains", proven by a showcase that shipped
|
|
291
|
+
* `"needsWebgl": false` while its engine was WebGL2-only.
|
|
292
|
+
*
|
|
293
|
+
* FAIL-SAFE DIRECTION, and it is the whole contract: this may only ever turn a
|
|
294
|
+
* run that WOULD have failed into a faster failure. It must never fail a run
|
|
295
|
+
* that would have worked, so every uncertain branch — offline, unpublished
|
|
296
|
+
* version, missing field, malformed document — returns `undefined` and lets the
|
|
297
|
+
* install proceed to the authoritative on-disk check.
|
|
298
|
+
*
|
|
299
|
+
* @returns the declared list, `null` for a package that declares none
|
|
300
|
+
* (unconstrained), or `undefined` when the question could not be answered.
|
|
301
|
+
*/
|
|
302
|
+
async function preflightDeclaredRuntimes(showcase, cliVersion) {
|
|
303
|
+
try {
|
|
304
|
+
// `fullMetadata` is REQUIRED: the abbreviated (corgi) document omits
|
|
305
|
+
// custom fields, so `gjsify.example` is simply absent there — the same
|
|
306
|
+
// shape as the `libc` note in `@gjsify/npm-registry`'s PackumentVersion.
|
|
307
|
+
const packument = await fetchPackument(showcase.packageName, {
|
|
308
|
+
npmrc: await loadNpmrc(process.cwd()),
|
|
309
|
+
fullMetadata: true,
|
|
310
|
+
});
|
|
311
|
+
return declaredRuntimesFromPackument(packument, cliVersion);
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
// Offline, private registry, auth failure, 404 — all "cannot say".
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* The pure half of {@link preflightDeclaredRuntimes}: pick the version whose
|
|
320
|
+
* declaration decides, and read it.
|
|
321
|
+
*
|
|
322
|
+
* Separate from the fetch so the fail-safe direction is unit-testable — that
|
|
323
|
+
* contract ("never fail a run that would have worked") is the only thing that
|
|
324
|
+
* makes a pre-install check safe, and it lives entirely in which branches return
|
|
325
|
+
* `undefined`.
|
|
326
|
+
*
|
|
327
|
+
* @param cliVersion the CLI's own version; `showcase` pins the showcase to it.
|
|
328
|
+
*/
|
|
329
|
+
export function declaredRuntimesFromPackument(packument, cliVersion) {
|
|
330
|
+
// A pinned version that is NOT published is "cannot say", never "ask latest":
|
|
331
|
+
// a different version's declaration is a different answer, and a wrong
|
|
332
|
+
// "unsupported" is precisely the failure this must not introduce. (A dev or
|
|
333
|
+
// pre-release CLI is the normal way to reach this.)
|
|
334
|
+
const version = cliVersion ?? packument['dist-tags']?.latest;
|
|
335
|
+
const entry = version ? packument.versions?.[version] : undefined;
|
|
336
|
+
if (!entry)
|
|
337
|
+
return undefined;
|
|
338
|
+
return readDeclaredRuntimes(entry);
|
|
339
|
+
}
|
|
233
340
|
/**
|
|
234
341
|
* Run a showcase on a non-gjs runtime (node/bun/deno). Resolves the package,
|
|
235
342
|
* validates it against the showcase's `gjsify.example.runtimes` declaration
|
|
@@ -237,6 +344,19 @@ async function resolveShowcaseDir(showcase, cliVersion, extraSpecs = []) {
|
|
|
237
344
|
* node), resolves its `--app node` bundle and launches it via the shared runner.
|
|
238
345
|
*/
|
|
239
346
|
async function runShowcaseOnRuntime(showcase, runtime, cliVersion) {
|
|
347
|
+
// PRE-FLIGHT, before anything is downloaded. Only for the dlx path: a local
|
|
348
|
+
// showcase resolves with no network and no install, so there is nothing to
|
|
349
|
+
// pre-empt there and the on-disk check below is already free.
|
|
350
|
+
if (resolveLocalShowcaseDir(showcase) === null) {
|
|
351
|
+
const preflight = await preflightDeclaredRuntimes(showcase, cliVersion);
|
|
352
|
+
if (preflight !== undefined) {
|
|
353
|
+
const support = checkRuntimeSupported(runtime, preflight, showcase.name);
|
|
354
|
+
if (!support.ok) {
|
|
355
|
+
console.error(support.message);
|
|
356
|
+
return process.exit(1);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
240
360
|
let pkgDir;
|
|
241
361
|
try {
|
|
242
362
|
// `@gjsify/node-gi` is the `gi://` bridge the `--app node` bundle
|
|
@@ -248,7 +368,7 @@ async function runShowcaseOnRuntime(showcase, runtime, cliVersion) {
|
|
|
248
368
|
// it supplies the bridge. Pinned to the CLI's version like the showcase
|
|
249
369
|
// itself, so the bridge and the bundle move together.
|
|
250
370
|
const nodeGiSpec = cliVersion ? `@gjsify/node-gi@${cliVersion}` : '@gjsify/node-gi';
|
|
251
|
-
pkgDir = await resolveShowcaseDir(showcase, cliVersion, [nodeGiSpec]);
|
|
371
|
+
pkgDir = await resolveShowcaseDir(showcase, cliVersion, [nodeGiSpec, ...gtkRuntimeSpecs(cliVersion)]);
|
|
252
372
|
}
|
|
253
373
|
catch (err) {
|
|
254
374
|
console.error(`Could not resolve showcase "${showcase.name}": ${err.message}`);
|
|
@@ -114,8 +114,57 @@ export interface LinkedBin {
|
|
|
114
114
|
* simplicity and so we don't need to discriminate by runtime here.
|
|
115
115
|
*/
|
|
116
116
|
export declare function linkGlobalBins(packageNames: string[], layout: GlobalLayout): LinkedBin[];
|
|
117
|
+
/**
|
|
118
|
+
* Is the global layout the user's own, or has a caller redirected it?
|
|
119
|
+
*
|
|
120
|
+
* `GJSIFY_GLOBAL_PREFIX` / `GJSIFY_GLOBAL_BIN_DIR` are the escape hatches
|
|
121
|
+
* {@link defaultGlobalLayout} documents for tests, and a caller that sets them
|
|
122
|
+
* has taken over placement — so "which `gjsify` will PATH resolve next" stops
|
|
123
|
+
* being a question about THEIR intent. `self-update` gates its fatal PATH check
|
|
124
|
+
* on this: without it, every harness installing into a temp prefix it never put
|
|
125
|
+
* on PATH is told its update did not take effect, which is true and useless.
|
|
126
|
+
* Caught by `tests/e2e/global-install-engine`.
|
|
127
|
+
*
|
|
128
|
+
* @param env injectable so both answers are testable without mutating the real
|
|
129
|
+
* environment mid-suite.
|
|
130
|
+
*/
|
|
131
|
+
export declare function globalLayoutIsDefault(env?: NodeJS.ProcessEnv): boolean;
|
|
117
132
|
/** Returns `true` if `binDir` is on the user's PATH. */
|
|
118
133
|
export declare function binDirOnPath(binDir: string): boolean;
|
|
134
|
+
/** Injection points for {@link resolveBinOnPath} — see its `platform` note. */
|
|
135
|
+
export interface ResolveBinOnPathOptions {
|
|
136
|
+
/** PATH to search. Defaults to `process.env.PATH`. */
|
|
137
|
+
pathValue?: string;
|
|
138
|
+
/** PATHEXT to honour on win32. Defaults to `process.env.PATHEXT` or cmd.exe's set. */
|
|
139
|
+
pathExt?: string;
|
|
140
|
+
/** Host platform. Defaults to `process.platform`. */
|
|
141
|
+
platform?: NodeJS.Platform;
|
|
142
|
+
/** File-existence probe. Defaults to a real `fs.existsSync`. */
|
|
143
|
+
exists?: (file: string) => boolean;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Resolve `binName` the way the user's shell will: the FIRST hit in PATH order.
|
|
147
|
+
*
|
|
148
|
+
* WHY THIS IS NOT `binDirOnPath`. That answers "is our dir on PATH", which is
|
|
149
|
+
* necessary but not sufficient for anything that CHANGES which binary runs:
|
|
150
|
+
* `~/.local/bin` can be on PATH and still lose to an npm-global `gjsify` earlier
|
|
151
|
+
* in it. `self-update` reported "Updated to v0.31.0" on exactly that host while
|
|
152
|
+
* the shell kept resolving a v0.26.0 install five releases behind — and the user
|
|
153
|
+
* then re-hit a bug fixed three releases earlier and reported it as new (#1064).
|
|
154
|
+
* "Which one wins" is the question, so this returns the winning PATH.
|
|
155
|
+
*
|
|
156
|
+
* WINDOWS. An extension-less name is not executable there; cmd.exe and pwsh
|
|
157
|
+
* append each `PATHEXT` entry, which is how the `.cmd` shim `bin-shim.ts` writes
|
|
158
|
+
* gets found. The bare name is probed LAST so a git-bash/MSYS `sh` shim is still
|
|
159
|
+
* reported rather than missed. `platform`/`pathExt`/`exists` are injectable for
|
|
160
|
+
* the same reason `dirLinkTarget` takes `linkType`: nothing in CI resolves a bin
|
|
161
|
+
* on a Windows host, so an injected `'win32'` is the ONLY way that branch is ever
|
|
162
|
+
* EXECUTED — and an unexecutable Windows branch is exactly how the `'dir'` bug in
|
|
163
|
+
* `dir-link.ts` shipped.
|
|
164
|
+
*
|
|
165
|
+
* @returns the absolute path of the winning candidate, or `null` when PATH holds none.
|
|
166
|
+
*/
|
|
167
|
+
export declare function resolveBinOnPath(binName: string, opts?: ResolveBinOnPathOptions): string | null;
|
|
119
168
|
/**
|
|
120
169
|
* Extracts the package name from a spec like `name@1.2`, `@scope/name`,
|
|
121
170
|
* or `@scope/name@latest`. Returns the unchanged string for plain names.
|
|
@@ -297,12 +297,100 @@ function readShebangOf(file) {
|
|
|
297
297
|
function readJson(file) {
|
|
298
298
|
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
299
299
|
}
|
|
300
|
+
/**
|
|
301
|
+
* Is the global layout the user's own, or has a caller redirected it?
|
|
302
|
+
*
|
|
303
|
+
* `GJSIFY_GLOBAL_PREFIX` / `GJSIFY_GLOBAL_BIN_DIR` are the escape hatches
|
|
304
|
+
* {@link defaultGlobalLayout} documents for tests, and a caller that sets them
|
|
305
|
+
* has taken over placement — so "which `gjsify` will PATH resolve next" stops
|
|
306
|
+
* being a question about THEIR intent. `self-update` gates its fatal PATH check
|
|
307
|
+
* on this: without it, every harness installing into a temp prefix it never put
|
|
308
|
+
* on PATH is told its update did not take effect, which is true and useless.
|
|
309
|
+
* Caught by `tests/e2e/global-install-engine`.
|
|
310
|
+
*
|
|
311
|
+
* @param env injectable so both answers are testable without mutating the real
|
|
312
|
+
* environment mid-suite.
|
|
313
|
+
*/
|
|
314
|
+
export function globalLayoutIsDefault(env = process.env) {
|
|
315
|
+
return !env.GJSIFY_GLOBAL_PREFIX && !env.GJSIFY_GLOBAL_BIN_DIR;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* The path module for a TARGET platform, not for the host.
|
|
319
|
+
*
|
|
320
|
+
* Load-bearing for every function below that takes a `platform`. `path.resolve`
|
|
321
|
+
* is host-bound: on Linux `path.resolve('C:\\tools')` returns
|
|
322
|
+
* `<cwd>/C:\tools`, because posix has no notion of a drive letter. So a function
|
|
323
|
+
* that accepts `platform` and then calls the host's `path` is only pretending to
|
|
324
|
+
* be portable — it silently mangles Windows input everywhere except Windows,
|
|
325
|
+
* which is exactly the class of bug an injected platform is supposed to expose.
|
|
326
|
+
* Caught by `resolve-bin-on-path.spec.ts`, which asserts real `C:\…` semantics
|
|
327
|
+
* from Linux.
|
|
328
|
+
*/
|
|
329
|
+
function pathFor(platform) {
|
|
330
|
+
return platform === 'win32' ? path.win32 : path.posix;
|
|
331
|
+
}
|
|
332
|
+
/** PATH split into resolved directory entries, in search order. */
|
|
333
|
+
function searchPathEntries(pathValue, platform) {
|
|
334
|
+
const p = pathFor(platform);
|
|
335
|
+
const sep = platform === 'win32' ? ';' : ':';
|
|
336
|
+
return pathValue
|
|
337
|
+
.split(sep)
|
|
338
|
+
.filter(Boolean)
|
|
339
|
+
.map((entry) => p.resolve(entry));
|
|
340
|
+
}
|
|
300
341
|
/** Returns `true` if `binDir` is on the user's PATH. */
|
|
301
342
|
export function binDirOnPath(binDir) {
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
343
|
+
const want = pathFor(process.platform).resolve(binDir);
|
|
344
|
+
return searchPathEntries(process.env.PATH ?? '', process.platform).includes(want);
|
|
345
|
+
}
|
|
346
|
+
/** cmd.exe's default when PATHEXT is unset. `.CMD` is the one npm-style shims need. */
|
|
347
|
+
const DEFAULT_PATHEXT = '.COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC';
|
|
348
|
+
/**
|
|
349
|
+
* Resolve `binName` the way the user's shell will: the FIRST hit in PATH order.
|
|
350
|
+
*
|
|
351
|
+
* WHY THIS IS NOT `binDirOnPath`. That answers "is our dir on PATH", which is
|
|
352
|
+
* necessary but not sufficient for anything that CHANGES which binary runs:
|
|
353
|
+
* `~/.local/bin` can be on PATH and still lose to an npm-global `gjsify` earlier
|
|
354
|
+
* in it. `self-update` reported "Updated to v0.31.0" on exactly that host while
|
|
355
|
+
* the shell kept resolving a v0.26.0 install five releases behind — and the user
|
|
356
|
+
* then re-hit a bug fixed three releases earlier and reported it as new (#1064).
|
|
357
|
+
* "Which one wins" is the question, so this returns the winning PATH.
|
|
358
|
+
*
|
|
359
|
+
* WINDOWS. An extension-less name is not executable there; cmd.exe and pwsh
|
|
360
|
+
* append each `PATHEXT` entry, which is how the `.cmd` shim `bin-shim.ts` writes
|
|
361
|
+
* gets found. The bare name is probed LAST so a git-bash/MSYS `sh` shim is still
|
|
362
|
+
* reported rather than missed. `platform`/`pathExt`/`exists` are injectable for
|
|
363
|
+
* the same reason `dirLinkTarget` takes `linkType`: nothing in CI resolves a bin
|
|
364
|
+
* on a Windows host, so an injected `'win32'` is the ONLY way that branch is ever
|
|
365
|
+
* EXECUTED — and an unexecutable Windows branch is exactly how the `'dir'` bug in
|
|
366
|
+
* `dir-link.ts` shipped.
|
|
367
|
+
*
|
|
368
|
+
* @returns the absolute path of the winning candidate, or `null` when PATH holds none.
|
|
369
|
+
*/
|
|
370
|
+
export function resolveBinOnPath(binName, opts = {}) {
|
|
371
|
+
const platform = opts.platform ?? process.platform;
|
|
372
|
+
const pathValue = opts.pathValue ?? process.env.PATH ?? '';
|
|
373
|
+
const exists = opts.exists ?? fs.existsSync;
|
|
374
|
+
// On win32 the extensions come FIRST (what cmd.exe/pwsh actually launch) and
|
|
375
|
+
// the bare name last (git-bash). Elsewhere there is only the bare name.
|
|
376
|
+
const suffixes = platform === 'win32'
|
|
377
|
+
? [
|
|
378
|
+
...(opts.pathExt ?? process.env.PATHEXT ?? DEFAULT_PATHEXT)
|
|
379
|
+
.split(';')
|
|
380
|
+
.filter(Boolean)
|
|
381
|
+
.map((ext) => (ext.startsWith('.') ? ext : `.${ext}`)),
|
|
382
|
+
'',
|
|
383
|
+
]
|
|
384
|
+
: [''];
|
|
385
|
+
const p = pathFor(platform);
|
|
386
|
+
for (const dir of searchPathEntries(pathValue, platform)) {
|
|
387
|
+
for (const suffix of suffixes) {
|
|
388
|
+
const candidate = p.join(dir, `${binName}${suffix}`);
|
|
389
|
+
if (exists(candidate))
|
|
390
|
+
return candidate;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return null;
|
|
306
394
|
}
|
|
307
395
|
/**
|
|
308
396
|
* Extracts the package name from a spec like `name@1.2`, `@scope/name`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "CLI for Gjsify",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -134,19 +134,19 @@
|
|
|
134
134
|
"cli"
|
|
135
135
|
],
|
|
136
136
|
"dependencies": {
|
|
137
|
-
"@gjsify/buffer": "^0.
|
|
138
|
-
"@gjsify/create-app": "^0.
|
|
139
|
-
"@gjsify/node-globals": "^0.
|
|
140
|
-
"@gjsify/node-polyfills": "^0.
|
|
141
|
-
"@gjsify/npm-registry": "^0.
|
|
142
|
-
"@gjsify/resolve-npm": "^0.
|
|
143
|
-
"@gjsify/rolldown-plugin-gjsify": "^0.
|
|
144
|
-
"@gjsify/rolldown-plugin-pnp": "^0.
|
|
145
|
-
"@gjsify/semver": "^0.
|
|
146
|
-
"@gjsify/tar": "^0.
|
|
147
|
-
"@gjsify/tsc": "^0.
|
|
148
|
-
"@gjsify/web-polyfills": "^0.
|
|
149
|
-
"@gjsify/workspace": "^0.
|
|
137
|
+
"@gjsify/buffer": "^0.32.0",
|
|
138
|
+
"@gjsify/create-app": "^0.32.0",
|
|
139
|
+
"@gjsify/node-globals": "^0.32.0",
|
|
140
|
+
"@gjsify/node-polyfills": "^0.32.0",
|
|
141
|
+
"@gjsify/npm-registry": "^0.32.0",
|
|
142
|
+
"@gjsify/resolve-npm": "^0.32.0",
|
|
143
|
+
"@gjsify/rolldown-plugin-gjsify": "^0.32.0",
|
|
144
|
+
"@gjsify/rolldown-plugin-pnp": "^0.32.0",
|
|
145
|
+
"@gjsify/semver": "^0.32.0",
|
|
146
|
+
"@gjsify/tar": "^0.32.0",
|
|
147
|
+
"@gjsify/tsc": "^0.32.0",
|
|
148
|
+
"@gjsify/web-polyfills": "^0.32.0",
|
|
149
|
+
"@gjsify/workspace": "^0.32.0",
|
|
150
150
|
"cosmiconfig": "^9.0.2",
|
|
151
151
|
"get-tsconfig": "^4.14.0",
|
|
152
152
|
"pkg-types": "^2.3.1",
|
|
@@ -154,13 +154,13 @@
|
|
|
154
154
|
"yargs": "^18.0.0"
|
|
155
155
|
},
|
|
156
156
|
"devDependencies": {
|
|
157
|
-
"@gjsify/unit": "^0.
|
|
157
|
+
"@gjsify/unit": "^0.32.0",
|
|
158
158
|
"@types/yargs": "^17.0.35",
|
|
159
159
|
"typescript": "^6.0.3"
|
|
160
160
|
},
|
|
161
161
|
"peerDependencies": {
|
|
162
|
-
"@gjsify/oxfmt-native": "^0.
|
|
163
|
-
"@gjsify/rolldown-native": "^0.
|
|
162
|
+
"@gjsify/oxfmt-native": "^0.32.0",
|
|
163
|
+
"@gjsify/rolldown-native": "^0.32.0"
|
|
164
164
|
},
|
|
165
165
|
"peerDependenciesMeta": {
|
|
166
166
|
"@gjsify/oxfmt-native": {
|