@gjsify/cli 0.30.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 +194 -193
- package/lib/actions/build.d.ts +14 -0
- package/lib/actions/build.js +78 -3
- package/lib/actions/index.d.ts +2 -0
- package/lib/actions/index.js +2 -0
- package/lib/affected-entry.d.ts +1 -0
- package/lib/affected-entry.js +49 -0
- package/lib/bundler-pick.js +84 -19
- package/lib/cli-app.js +20 -0
- package/lib/cli-fail.d.ts +22 -0
- package/lib/cli-fail.js +67 -0
- package/lib/commands/affected.d.ts +2 -2
- package/lib/commands/affected.js +16 -0
- package/lib/commands/install.d.ts +55 -0
- package/lib/commands/install.js +38 -21
- package/lib/commands/run.js +25 -6
- 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/commands/test.js +4 -4
- package/lib/commands/tsc.js +59 -11
- package/lib/commands/workspace.js +13 -0
- package/lib/config.js +6 -2
- package/lib/index.js +18 -1
- package/lib/templates/install.mjs.tmpl +27 -2
- package/lib/types/command.d.ts +18 -0
- package/lib/utils/check-system-deps.d.ts +66 -1
- package/lib/utils/check-system-deps.js +139 -2
- package/lib/utils/detect-native-packages.js +12 -1
- package/lib/utils/discover-showcases.d.ts +0 -2
- package/lib/utils/discover-showcases.js +0 -1
- package/lib/utils/dlx-cache.d.ts +22 -2
- package/lib/utils/dlx-cache.js +62 -6
- package/lib/utils/force-exit.d.ts +10 -0
- package/lib/utils/force-exit.js +43 -0
- package/lib/utils/gi-search-path.d.ts +36 -0
- package/lib/utils/gi-search-path.js +146 -0
- package/lib/utils/gjs-source-escape.d.ts +10 -0
- package/lib/utils/gjs-source-escape.js +49 -0
- package/lib/utils/gjsify-shim.d.ts +29 -2
- package/lib/utils/gjsify-shim.js +131 -22
- package/lib/utils/install-global.d.ts +79 -2
- package/lib/utils/install-global.js +130 -8
- package/lib/utils/oxc-resolve.d.ts +1 -1
- package/lib/utils/oxc-resolve.js +10 -6
- package/lib/utils/stdin.d.ts +3 -13
- package/lib/utils/stdin.js +21 -100
- package/lib/utils/system-gi.js +10 -1
- package/package.json +22 -17
- package/showcases.json +8 -16
package/lib/commands/install.js
CHANGED
|
@@ -31,7 +31,7 @@ import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, rmSync, syml
|
|
|
31
31
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
32
32
|
import { fileURLToPath } from 'node:url';
|
|
33
33
|
import { spawnSync } from 'node:child_process';
|
|
34
|
-
import {
|
|
34
|
+
import { forceExit } from '../utils/force-exit.js';
|
|
35
35
|
import { discoverWorkspaces } from '@gjsify/workspace';
|
|
36
36
|
import { buildInstallCommand, detectPackageManager, runMinimalChecks } from '../utils/check-system-deps.js';
|
|
37
37
|
import { detectNativePackages } from '../utils/detect-native-packages.js';
|
|
@@ -42,7 +42,7 @@ import { acquireInstallLock } from '../utils/install-lock.js';
|
|
|
42
42
|
import { nodeBinary } from '../utils/run-node.js';
|
|
43
43
|
import { spawnToCompletion } from '../utils/spawn.js';
|
|
44
44
|
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
45
|
-
import { binDirOnPath, defaultGlobalLayout, installGjsEnginePackages, linkGlobalBins, specToPackageName, } from '../utils/install-global.js';
|
|
45
|
+
import { binDirOnPath, BUNDLER_ENGINE_PACKAGE, defaultGlobalLayout, hasBundlerEngineInstalled, installGjsEnginePackages, linkGlobalBins, specToPackageName, } from '../utils/install-global.js';
|
|
46
46
|
import { addDependencyEntry, defaultRangeFromVersion, parseSpec, projectSpecsFromPackageJson, readPackageJson, writePackageJson, } from '../utils/pkg-json-edit.js';
|
|
47
47
|
/**
|
|
48
48
|
* Link type for the workspace↔workspace directory links.
|
|
@@ -198,7 +198,7 @@ export const installCommand = {
|
|
|
198
198
|
const backend = args.backend ?? process.env.GJSIFY_INSTALL_BACKEND ?? 'native';
|
|
199
199
|
if (backend === 'npm') {
|
|
200
200
|
await projectInstallViaNpm(args);
|
|
201
|
-
await runPostInstallChecks();
|
|
201
|
+
await runPostInstallChecks(args);
|
|
202
202
|
return;
|
|
203
203
|
}
|
|
204
204
|
// Overall wall-clock budget for the install (default 30 min — big
|
|
@@ -263,7 +263,7 @@ export const installCommand = {
|
|
|
263
263
|
hardExitTimerId?.unref?.();
|
|
264
264
|
try {
|
|
265
265
|
await projectInstallNative(args, overallController?.signal);
|
|
266
|
-
await runPostInstallChecks();
|
|
266
|
+
await runPostInstallChecks(args);
|
|
267
267
|
}
|
|
268
268
|
catch (err) {
|
|
269
269
|
if (overallController !== null && overallController.signal.aborted && isAbortedFromOverallTimeout(err)) {
|
|
@@ -345,21 +345,6 @@ function isAbortedFromOverallTimeout(err) {
|
|
|
345
345
|
return true;
|
|
346
346
|
return false;
|
|
347
347
|
}
|
|
348
|
-
/**
|
|
349
|
-
* Force a non-zero process exit that is honored on BOTH runtimes.
|
|
350
|
-
*
|
|
351
|
-
* Node exits at natural shutdown once `process.exitCode` is set, but GJS has
|
|
352
|
-
* no atexit hook, so `imports.system.exit` is called directly there —
|
|
353
|
-
* SpiderMonkey raises its uncatchable exit exception from any context,
|
|
354
|
-
* including the `setTimeout` continuation the backstop timer runs in. Mirrors
|
|
355
|
-
* the entry wrapper (`index.ts`) and `setOxcExitCode` (`oxc-resolve.ts`).
|
|
356
|
-
*/
|
|
357
|
-
function forceExit(code) {
|
|
358
|
-
process.exitCode = code;
|
|
359
|
-
if (gjsExit(code))
|
|
360
|
-
return;
|
|
361
|
-
process.exit(code);
|
|
362
|
-
}
|
|
363
348
|
const CLI_PACKAGE_NAME = '@gjsify/cli';
|
|
364
349
|
/** Read a `version` from a package.json IFF its `name` is `@gjsify/cli`. */
|
|
365
350
|
function readCliVersionFrom(pkgJsonPath) {
|
|
@@ -1419,7 +1404,33 @@ function reportLinkedBins(created, binDir) {
|
|
|
1419
1404
|
`Add it to your shell rc file:\n export PATH="${binDir}:$PATH"`);
|
|
1420
1405
|
}
|
|
1421
1406
|
}
|
|
1422
|
-
async function
|
|
1407
|
+
export async function ensureProjectGjsEngine(args, gjsFound, deps = {}) {
|
|
1408
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
1409
|
+
const hasEngine = deps.hasEngineFn ?? hasBundlerEngineInstalled;
|
|
1410
|
+
if (!existsSync(join(cwd, 'node_modules', '@gjsify', 'cli', 'package.json')))
|
|
1411
|
+
return;
|
|
1412
|
+
if (!gjsFound)
|
|
1413
|
+
return;
|
|
1414
|
+
if (hasEngine(cwd))
|
|
1415
|
+
return;
|
|
1416
|
+
if (args.immutable) {
|
|
1417
|
+
// Name the DURABLE fix, not a transient one: a frozen CI install cannot
|
|
1418
|
+
// be repaired by anything that happens at this moment, so the answer is
|
|
1419
|
+
// to put the engine in the lockfile.
|
|
1420
|
+
console.warn(`\nWarning: \`gjsify build\` will fail under GJS — ${BUNDLER_ENGINE_PACKAGE} is not installed, and\n` +
|
|
1421
|
+
' --immutable cannot add it (the lockfile does not name it). Declare it so the lockfile carries it:\n' +
|
|
1422
|
+
` gjsify install ${BUNDLER_ENGINE_PACKAGE}\n` +
|
|
1423
|
+
' Under Node the npm `rolldown` engine is used instead and this does not apply.');
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
const cliVersion = readCliVersionFrom(join(cwd, 'node_modules', '@gjsify', 'cli', 'package.json')) ?? 'latest';
|
|
1427
|
+
console.log(`\n${BUNDLER_ENGINE_PACKAGE} is missing and this host can run gjs, so \`gjsify build\` would have no\n` +
|
|
1428
|
+
` bundler engine (there is no npm \`rolldown\` fallback under GJS). Installing the GJS engine set at\n` +
|
|
1429
|
+
` ${cliVersion}, in lockstep with the CLI.`);
|
|
1430
|
+
const install = deps.installFn ?? installGjsEnginePackages;
|
|
1431
|
+
await install(cwd, cliVersion, { verbose: args.verbose });
|
|
1432
|
+
}
|
|
1433
|
+
async function runPostInstallChecks(args) {
|
|
1423
1434
|
console.log('\n--- gjsify post-install checks ---');
|
|
1424
1435
|
// 1. System deps that GJS apps typically need.
|
|
1425
1436
|
const results = runMinimalChecks();
|
|
@@ -1437,8 +1448,14 @@ async function runPostInstallChecks() {
|
|
|
1437
1448
|
else {
|
|
1438
1449
|
console.log('System dependencies OK.');
|
|
1439
1450
|
}
|
|
1440
|
-
// 2.
|
|
1451
|
+
// 2. Close #1005 before reporting the prebuild set, so the report reflects
|
|
1452
|
+
// what the tree ACTUALLY has rather than what it had a moment ago.
|
|
1453
|
+
await ensureProjectGjsEngine(args, results.find((r) => r.id === 'gjs')?.found ?? false);
|
|
1454
|
+
// 3. Surface @gjsify/* packages with native prebuilds — `gjsify run`
|
|
1441
1455
|
// will set LD_LIBRARY_PATH / GI_TYPELIB_PATH for these automatically.
|
|
1456
|
+
// Runs AFTER step 2 on purpose: an engine installed a moment ago belongs
|
|
1457
|
+
// in this list, and a consumer reading "Detected N …" needs it to be the
|
|
1458
|
+
// tree's current state rather than its state on entry.
|
|
1442
1459
|
const native = detectNativePackages(process.cwd());
|
|
1443
1460
|
if (native.length > 0) {
|
|
1444
1461
|
console.log(`\nDetected ${native.length} @gjsify/* package(s) with native prebuilds:`);
|
package/lib/commands/run.js
CHANGED
|
@@ -296,12 +296,15 @@ async function runScript(script, extraArgs, cwd = process.cwd()) {
|
|
|
296
296
|
if (monorepoRoot && monorepoRoot !== cwd) {
|
|
297
297
|
binDirs.push(join(monorepoRoot, 'node_modules', '.bin'));
|
|
298
298
|
}
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
//
|
|
299
|
+
// The self-shim (see `ensureGjsifyShimOnPath`) MUST shadow the workspace
|
|
300
|
+
// `node_modules/.bin/gjsify`, so put its dir ahead of the `.bin` dirs in the
|
|
301
|
+
// child's PATH — otherwise a compound script like `gjsify run a && gjsify
|
|
302
|
+
// run b` resolves `gjsify` to the workspace bin. Two trees need that:
|
|
303
|
+
// under GJS the bin is the Node entry and is unusable in a node-free
|
|
304
|
+
// sandbox, and under a BOOTSTRAP Node CLI (npx / global install) the bin
|
|
305
|
+
// dispatches to build outputs that a cold tree has not produced yet.
|
|
306
|
+
// `GJSIFY_SHIM_DIR` is unset whenever the tree's own CLI is the one
|
|
307
|
+
// running, which keeps that case byte-for-byte unchanged.
|
|
305
308
|
if (process.env.GJSIFY_SHIM_DIR) {
|
|
306
309
|
binDirs.unshift(process.env.GJSIFY_SHIM_DIR);
|
|
307
310
|
}
|
|
@@ -342,6 +345,22 @@ async function runScript(script, extraArgs, cwd = process.cwd()) {
|
|
|
342
345
|
process.env.npm_package_version = env.npm_package_version;
|
|
343
346
|
if (env.FORCE_COLOR !== undefined)
|
|
344
347
|
process.env.FORCE_COLOR = env.FORCE_COLOR;
|
|
348
|
+
// …and the working directory, for the same reason. The spawn path below
|
|
349
|
+
// passes `cwd` to `spawn()`; this path used to pass it nowhere, so a
|
|
350
|
+
// `gjsify run -w <ws> <script>` resolved the WORKSPACE and then ran the
|
|
351
|
+
// command in the CALLER's directory. Every relative path in the script
|
|
352
|
+
// — `src/app/main.ts`, `dist/…`, `tests/test.mts` — then resolved
|
|
353
|
+
// against the monorepo root.
|
|
354
|
+
//
|
|
355
|
+
// Only this branch was affected, which is why it survived: the fast path
|
|
356
|
+
// fires exactly when the script is a SINGLE `gjsify <subcommand>`, so a
|
|
357
|
+
// compound script (`a && b`) took the shell path and worked. Measured on
|
|
358
|
+
// the two consumers: bauplaner's cli scripts are all single commands and
|
|
359
|
+
// it documents the gap in its AGENTS.md, sidestepping it with
|
|
360
|
+
// `cd cli && …`; buchhaltung's one `-w` use is `build:meson`, a compound
|
|
361
|
+
// command, and it works.
|
|
362
|
+
if (cwd !== process.cwd())
|
|
363
|
+
process.chdir(cwd);
|
|
345
364
|
// Track the failure LOCALLY rather than through `process.exitCode`.
|
|
346
365
|
// Under GJS `process.exit()` is deferred (see the note below), so the
|
|
347
366
|
// `process.exit(1)` that used to live in the catch returned instead of
|
|
@@ -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}`);
|
package/lib/commands/test.js
CHANGED
|
@@ -194,10 +194,10 @@ async function runTestBundle(outfile, runtime) {
|
|
|
194
194
|
return;
|
|
195
195
|
}
|
|
196
196
|
await new Promise((resolvePromise, reject) => {
|
|
197
|
-
// `nodeBinary()`, never a bare `'node'`: under
|
|
198
|
-
// `
|
|
199
|
-
// bare literal dies `spawn node ENOENT` — which is
|
|
200
|
-
// GJS suite was reported as a failed run.
|
|
197
|
+
// `nodeBinary()`, never a bare `'node'`: under GJS `process.execPath` is
|
|
198
|
+
// the `gjs` interpreter, which cannot run a `--app node` bundle, and on a
|
|
199
|
+
// host without Node a bare literal dies `spawn node ENOENT` — which is
|
|
200
|
+
// how a fully passing GJS suite was reported as a failed run.
|
|
201
201
|
const child = spawn(nodeBinary(), [outfile], { stdio: 'inherit' });
|
|
202
202
|
child.on('error', reject);
|
|
203
203
|
child.on('exit', (code) => {
|
package/lib/commands/tsc.js
CHANGED
|
@@ -22,6 +22,10 @@ import { createRequire } from 'node:module';
|
|
|
22
22
|
import { dirname, join } from 'node:path';
|
|
23
23
|
import { pathToFileURL } from 'node:url';
|
|
24
24
|
import { detectNativePackages, buildNativeEnv } from '../utils/detect-native-packages.js';
|
|
25
|
+
import { isOnPath } from '../utils/check-system-deps.js';
|
|
26
|
+
import { existsSync } from 'node:fs';
|
|
27
|
+
import { forceExit } from '../utils/force-exit.js';
|
|
28
|
+
import { nodeBinary } from '../utils/run-node.js';
|
|
25
29
|
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
26
30
|
export const tscCommand = {
|
|
27
31
|
command: 'tsc [tscArgs..]',
|
|
@@ -104,9 +108,12 @@ export const tscCommand = {
|
|
|
104
108
|
if (!bundlePath && !nodeTscPath) {
|
|
105
109
|
console.error('gjsify tsc: neither @gjsify/tsc (GJS bundle) nor npm `typescript` (Node) is installed.');
|
|
106
110
|
console.error(' Install with: gjsify install --save-dev @gjsify/tsc (or add `typescript`).');
|
|
107
|
-
// `return` —
|
|
108
|
-
//
|
|
109
|
-
|
|
111
|
+
// `return` — the handler must not fall through and try to spawn a
|
|
112
|
+
// compiler that is not there. `forceExit` and not `process.exit`
|
|
113
|
+
// because the latter is idle-scheduled under GJS, and NOTHING here
|
|
114
|
+
// armed a main loop to deliver it: the process would reach natural
|
|
115
|
+
// shutdown and report 0 while printing the message above.
|
|
116
|
+
return forceExit(1);
|
|
110
117
|
}
|
|
111
118
|
// Mirror `gjsify run`'s native-env composition so any future
|
|
112
119
|
// typescript-the-bundle code path that reaches a native bridge
|
|
@@ -118,13 +125,54 @@ export const tscCommand = {
|
|
|
118
125
|
...process.env,
|
|
119
126
|
...nativeEnv,
|
|
120
127
|
};
|
|
121
|
-
// Run upstream `typescript` under
|
|
128
|
+
// Run upstream `typescript` under Node — the Node fallback.
|
|
129
|
+
//
|
|
130
|
+
// `nodeBinary()`, NEVER a bare `process.execPath`: under the GJS bundle
|
|
131
|
+
// `process.execPath` is the GJS interpreter (`/proc/self/exe` →
|
|
132
|
+
// `gjs-console`), so `spawn(process.execPath, [tsc.js])` hands
|
|
133
|
+
// TypeScript's CommonJS CLI to GJS, which dies on the first `module`
|
|
134
|
+
// reference — or, where execPath resolves to a gjsify launcher instead,
|
|
135
|
+
// re-executes the CLI with the tsc entry as an unknown argument. Both
|
|
136
|
+
// were reached from a COLD tree: `@gjsify/tsc`'s `dist/tsc.gjs.mjs` is a
|
|
137
|
+
// build output, and `build:infra` builds `@gjsify/create-app` (its first
|
|
138
|
+
// `gjsify tsc` caller) long before `@gjsify/tsc` produces it, so the
|
|
139
|
+
// fallback below is the NORMAL path there, not an edge case.
|
|
140
|
+
// `run-node.ts` owns the one correct answer; every other spawn site in
|
|
141
|
+
// this CLI already routes through it.
|
|
122
142
|
const runNodeTsc = () => {
|
|
123
|
-
const
|
|
124
|
-
|
|
143
|
+
const nodeBin = nodeBinary();
|
|
144
|
+
// PRE-FLIGHT, deliberately NOT the spawn's `'error'` event.
|
|
145
|
+
//
|
|
146
|
+
// Under the GJS bundle that event is not delivered when the program
|
|
147
|
+
// is missing: the handler below never runs, the command returns
|
|
148
|
+
// normally, and `gjsify tsc` exits **0 in total silence** having
|
|
149
|
+
// compiled nothing. Measured in CI by `tests/e2e/tsc-node-fallback`'s
|
|
150
|
+
// "FAILS LOUDLY …" case, whose assertion printed an EMPTY captured
|
|
151
|
+
// output — no stderr at all, so nothing had reported anything.
|
|
152
|
+
//
|
|
153
|
+
// Deciding it here, synchronously, is what makes the failure
|
|
154
|
+
// reportable: the exit happens on the caller's own stack rather than
|
|
155
|
+
// in a continuation that may never be scheduled.
|
|
156
|
+
const runnable = nodeBin.includes('/') || nodeBin.includes('\\') ? existsSync(nodeBin) : isOnPath(nodeBin);
|
|
157
|
+
if (!runnable) {
|
|
158
|
+
console.error(`gjsify tsc: the @gjsify/tsc GJS bundle is unavailable and \`${nodeBin}\` cannot be run, ` +
|
|
159
|
+
'so upstream `typescript` cannot run either.');
|
|
160
|
+
console.error(' Build the bundle with: gjsify workspace @gjsify/tsc build');
|
|
161
|
+
console.error(' …or install Node.js so the upstream `typescript` fallback can be spawned.');
|
|
162
|
+
return forceExit(1);
|
|
163
|
+
}
|
|
164
|
+
const child = spawn(nodeBin, [nodeTscPath, ...tscArgs], { env, stdio: 'inherit' });
|
|
165
|
+
child.on('close', (code) => forceExit(code ?? 1));
|
|
125
166
|
child.on('error', (err) => {
|
|
126
|
-
|
|
127
|
-
|
|
167
|
+
if (err.code === 'ENOENT') {
|
|
168
|
+
console.error('gjsify tsc: the @gjsify/tsc GJS bundle is unavailable and `node` is not on PATH, ' +
|
|
169
|
+
'so upstream `typescript` cannot run either.');
|
|
170
|
+
console.error(' Build the bundle with: gjsify workspace @gjsify/tsc build');
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
console.error(`gjsify tsc (node typescript): ${err.message}`);
|
|
174
|
+
}
|
|
175
|
+
forceExit(1);
|
|
128
176
|
});
|
|
129
177
|
};
|
|
130
178
|
// Prefer GJS — the Node-free @gjsify/tsc bundle — when it resolves. If
|
|
@@ -144,7 +192,7 @@ export const tscCommand = {
|
|
|
144
192
|
child.on('close', (code) => {
|
|
145
193
|
if (gjsSpawnFailed)
|
|
146
194
|
return;
|
|
147
|
-
|
|
195
|
+
forceExit(code ?? 1);
|
|
148
196
|
});
|
|
149
197
|
child.on('error', (err) => {
|
|
150
198
|
gjsSpawnFailed = true;
|
|
@@ -155,11 +203,11 @@ export const tscCommand = {
|
|
|
155
203
|
else if (err.code === 'ENOENT') {
|
|
156
204
|
console.error('gjsify tsc: `gjs` not found on PATH and no npm `typescript` fallback.');
|
|
157
205
|
console.error(' Install GJS (e.g. `dnf install gjs`), or add `typescript` to the project.');
|
|
158
|
-
|
|
206
|
+
forceExit(1);
|
|
159
207
|
}
|
|
160
208
|
else {
|
|
161
209
|
console.error(`gjsify tsc: ${err.message}`);
|
|
162
|
-
|
|
210
|
+
forceExit(1);
|
|
163
211
|
}
|
|
164
212
|
});
|
|
165
213
|
// Never resolves naturally — process.exit() above terminates the
|
|
@@ -24,6 +24,7 @@ import { buildDependencyGraph, discoverWorkspaces, topologicalSort } from '@gjsi
|
|
|
24
24
|
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
25
25
|
import { BuildCacheRunner, buildCacheEnabledByEnv } from '../utils/build-cache.js';
|
|
26
26
|
import { isGjs } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
27
|
+
import { usingSelfShim } from '../utils/gjsify-shim.js';
|
|
27
28
|
import { suggestClosest } from '../utils/suggest.js';
|
|
28
29
|
export const workspaceCommand = {
|
|
29
30
|
command: 'workspace <name> <script> [args..]',
|
|
@@ -279,6 +280,18 @@ export function detectPackageManager() {
|
|
|
279
280
|
// orchestration work in the Flatpak sandbox.
|
|
280
281
|
if (isGjs())
|
|
281
282
|
return 'gjsify';
|
|
283
|
+
// Same answer for a BOOTSTRAP CLI on Node, for a reason one level deeper
|
|
284
|
+
// than it looks. `ensureGjsifyShimOnPath()` puts a working `gjsify` ahead of
|
|
285
|
+
// the tree's `node_modules/.bin` — but npm RE-PREPENDS `node_modules/.bin`
|
|
286
|
+
// for every lifecycle script it starts, so delegating to npm here restores
|
|
287
|
+
// the tree's not-yet-built shim one level down and undoes the repair.
|
|
288
|
+
// Measured on a cold tree: `run build:infra` cleared its first clause and
|
|
289
|
+
// then died inside `npm run build` with `Cannot find module
|
|
290
|
+
// …/packages/infra/cli/lib/index.js`. Running the script ourselves keeps the
|
|
291
|
+
// bootstrap CLI's identity all the way down — which is exactly what the GJS
|
|
292
|
+
// branch above already relies on, and why Linux CI never saw this.
|
|
293
|
+
if (usingSelfShim())
|
|
294
|
+
return 'gjsify';
|
|
282
295
|
const ua = process.env.npm_config_user_agent ?? '';
|
|
283
296
|
if (ua.startsWith('yarn/'))
|
|
284
297
|
return 'yarn';
|