@gjsify/cli 0.40.0 → 0.42.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/README.md +79 -33
- package/dist/cli.gjs.mjs +1043 -404
- package/lib/actions/build.d.ts +14 -0
- package/lib/actions/build.js +54 -7
- package/lib/bundler-pick.js +10 -2
- package/lib/cli-app.js +3 -1
- package/lib/commands/browse.js +1 -1
- package/lib/commands/create.d.ts +3 -0
- package/lib/commands/create.js +49 -6
- package/lib/commands/debug.js +1 -1
- package/lib/commands/dev.d.ts +13 -0
- package/lib/commands/dev.js +132 -0
- package/lib/commands/dlx.js +1 -1
- package/lib/commands/index.d.ts +2 -0
- package/lib/commands/index.js +2 -0
- package/lib/commands/install.d.ts +1 -0
- package/lib/commands/install.js +32 -1
- package/lib/commands/prune.d.ts +11 -0
- package/lib/commands/prune.js +84 -0
- package/lib/commands/run.js +38 -85
- package/lib/commands/self-update.d.ts +1 -0
- package/lib/commands/self-update.js +55 -0
- package/lib/commands/ship.js +6 -1
- package/lib/commands/showcase.js +1 -1
- package/lib/commands/storybook.js +17 -74
- package/lib/commands/test.js +12 -16
- package/lib/commands/tsc.js +96 -45
- package/lib/config.js +4 -0
- package/lib/templates/install.mjs.tmpl +8 -3
- package/lib/types/config-data.d.ts +27 -0
- package/lib/utils/app-metadata.js +27 -6
- package/lib/utils/check-system-deps.d.ts +2 -2
- package/lib/utils/check-system-deps.js +92 -13
- package/lib/utils/daemon-command.d.ts +7 -0
- package/lib/utils/daemon-command.js +30 -0
- package/lib/utils/dev-plan.d.ts +52 -0
- package/lib/utils/dev-plan.js +117 -0
- package/lib/utils/gi-runtime-paths.d.ts +27 -0
- package/lib/utils/gi-runtime-paths.js +84 -0
- package/lib/utils/gi-typelib.d.ts +49 -0
- package/lib/utils/gi-typelib.js +160 -0
- package/lib/utils/install-provenance.d.ts +44 -0
- package/lib/utils/install-provenance.js +106 -0
- package/lib/utils/node-script.d.ts +4 -1
- package/lib/utils/node-script.js +27 -3
- package/lib/utils/npm-oidc.js +8 -5
- package/lib/utils/prune-prefix.d.ts +83 -0
- package/lib/utils/prune-prefix.js +333 -0
- package/lib/utils/run-gjs.d.ts +12 -1
- package/lib/utils/run-gjs.js +24 -31
- package/lib/utils/run-node.d.ts +9 -4
- package/lib/utils/run-node.js +34 -41
- package/lib/utils/ship/depends.d.ts +5 -0
- package/lib/utils/ship/depends.js +38 -0
- package/lib/utils/ship/discover.js +106 -1
- package/lib/utils/ship/launcher.js +27 -4
- package/lib/utils/ship/payload.d.ts +35 -0
- package/lib/utils/ship/payload.js +84 -0
- package/lib/utils/ship/plan.js +20 -0
- package/lib/utils/ship/settings.d.ts +8 -1
- package/lib/utils/ship/settings.js +13 -1
- package/lib/utils/ship/types.d.ts +19 -0
- package/lib/utils/simple-command.d.ts +16 -0
- package/lib/utils/simple-command.js +77 -0
- package/lib/utils/spawn.d.ts +7 -1
- package/lib/utils/spawn.js +34 -7
- package/lib/utils/system-gi.d.ts +24 -0
- package/lib/utils/system-gi.js +14 -3
- package/lib/utils/watch-loop.d.ts +91 -0
- package/lib/utils/watch-loop.js +207 -0
- package/package.json +27 -18
- package/lib/templates/app/desktop.tmpl +0 -10
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// `gjsify prune` — remove installed packages this target cannot use.
|
|
2
|
+
//
|
|
3
|
+
// The repair half of ADR 0025. `install`/`self-update` prune what they would not
|
|
4
|
+
// have placed, but only going forward: a prefix that accreted foreign-platform
|
|
5
|
+
// packages under an older CLI is only reachable by asking. That is this command,
|
|
6
|
+
// and it is why it exists at all rather than the automatic pass alone.
|
|
7
|
+
//
|
|
8
|
+
// `--os/--cpu/--libc` are accepted here and deliberately NOT honoured by the
|
|
9
|
+
// automatic pass: typed on `prune` they are a request ("show me what a darwin-arm64
|
|
10
|
+
// host could not use"), while inherited by an install they would delete the host's
|
|
11
|
+
// own packages. See `automaticPruneRefusal`.
|
|
12
|
+
import { existsSync } from 'node:fs';
|
|
13
|
+
import { acquireInstallLock } from '../utils/install-lock.js';
|
|
14
|
+
import { defaultGlobalLayout } from '../utils/install-global.js';
|
|
15
|
+
import { resolveHostPlatform } from '../utils/platform-check.js';
|
|
16
|
+
import { executePrune, formatPruneReport, planPrune } from '../utils/prune-prefix.js';
|
|
17
|
+
export const pruneCommand = {
|
|
18
|
+
command: 'prune',
|
|
19
|
+
description: 'Remove installed packages this host cannot use (foreign os/cpu/libc). ' +
|
|
20
|
+
'Reads each package manifest — a package that declares no platform is never touched.',
|
|
21
|
+
builder: (yargs) => yargs
|
|
22
|
+
.option('global', {
|
|
23
|
+
description: 'Prune the user-global XDG prefix (the `install -g` target) instead of the project.',
|
|
24
|
+
type: 'boolean',
|
|
25
|
+
alias: 'g',
|
|
26
|
+
default: false,
|
|
27
|
+
})
|
|
28
|
+
.option('dry-run', {
|
|
29
|
+
description: 'Report what would be removed without touching the filesystem.',
|
|
30
|
+
type: 'boolean',
|
|
31
|
+
default: false,
|
|
32
|
+
})
|
|
33
|
+
.option('verbose', {
|
|
34
|
+
description: 'List every package rather than the first few.',
|
|
35
|
+
type: 'boolean',
|
|
36
|
+
default: false,
|
|
37
|
+
})
|
|
38
|
+
.option('os', {
|
|
39
|
+
description: 'Prune AS IF the host were this OS (linux, darwin, win32). Defaults to this host.',
|
|
40
|
+
type: 'string',
|
|
41
|
+
})
|
|
42
|
+
.option('cpu', {
|
|
43
|
+
description: 'Prune AS IF the host were this architecture (x64, arm64, …). Defaults to this host.',
|
|
44
|
+
type: 'string',
|
|
45
|
+
})
|
|
46
|
+
.option('libc', {
|
|
47
|
+
description: 'Prune AS IF the host used this libc (glibc, musl). Defaults to this host.',
|
|
48
|
+
type: 'string',
|
|
49
|
+
}),
|
|
50
|
+
handler: async (args) => {
|
|
51
|
+
const layout = args.global ? defaultGlobalLayout() : null;
|
|
52
|
+
const prefix = layout ? layout.prefix : process.cwd();
|
|
53
|
+
if (!existsSync(`${prefix}/node_modules`)) {
|
|
54
|
+
console.log(`gjsify prune: nothing installed at ${prefix}`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// The flags are read HERE rather than through the npm config keys, so the
|
|
58
|
+
// automatic pass — which refuses to act on an overridden target — cannot be
|
|
59
|
+
// reached by way of this command's own options.
|
|
60
|
+
const target = resolveHostPlatform({
|
|
61
|
+
env: {
|
|
62
|
+
...(args.os ? { npm_config_os: args.os } : {}),
|
|
63
|
+
...(args.cpu ? { npm_config_cpu: args.cpu } : {}),
|
|
64
|
+
...(args.libc ? { npm_config_libc: args.libc } : {}),
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
// A prune racing an install's extract would delete a half-written tree. The
|
|
68
|
+
// lock is the same one every writer of this prefix takes (ADR 0001).
|
|
69
|
+
const lock = await acquireInstallLock(prefix, {});
|
|
70
|
+
let result;
|
|
71
|
+
try {
|
|
72
|
+
result = executePrune(planPrune({ prefix, target }), { dryRun: args.dryRun });
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
lock.release();
|
|
76
|
+
}
|
|
77
|
+
console.log(formatPruneReport(result, { dryRun: args.dryRun, verbose: args.verbose }));
|
|
78
|
+
// Removing nothing is a SUCCESS — idempotent housekeeping, unlike `uninstall`
|
|
79
|
+
// where naming a package that is not there is a mistake worth reporting. Only
|
|
80
|
+
// a removal the user asked for and did not get is a failure.
|
|
81
|
+
if (result.failed.length > 0)
|
|
82
|
+
return process.exit(1);
|
|
83
|
+
},
|
|
84
|
+
};
|
package/lib/commands/run.js
CHANGED
|
@@ -15,13 +15,15 @@
|
|
|
15
15
|
// to disambiguate can pass `./<file>` explicitly.
|
|
16
16
|
import { readFileSync, statSync } from 'node:fs';
|
|
17
17
|
import { basename, delimiter, join, resolve } from 'node:path';
|
|
18
|
-
import {
|
|
18
|
+
import { describeExit, spawnToCompletion } from '../utils/spawn.js';
|
|
19
19
|
import { runGjsBundle } from '../utils/run-gjs.js';
|
|
20
20
|
import { runRuntimeBundle } from '../utils/run-node.js';
|
|
21
21
|
import { readPackageJson } from '../utils/pkg-json-edit.js';
|
|
22
22
|
import { runNodeScript } from '../utils/node-script.js';
|
|
23
23
|
import { nodeShimDir } from '../utils/gjsify-shim.js';
|
|
24
24
|
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
25
|
+
import { gjsifyCommandArgv } from '../utils/simple-command.js';
|
|
26
|
+
import { isDaemonCommand } from '../utils/daemon-command.js';
|
|
25
27
|
import { discoverWorkspaces } from '@gjsify/workspace';
|
|
26
28
|
import { isGjs, hostRuntime } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
27
29
|
import { EXAMPLE_RUNTIMES, isExampleRuntime, readDeclaredRuntimes, checkRuntimeSupported, } from '../utils/runtimes.js';
|
|
@@ -124,7 +126,7 @@ export const runCommand = {
|
|
|
124
126
|
console.error(`gjsify run: --node-script cannot be combined with ${clash}.`);
|
|
125
127
|
return process.exit(1);
|
|
126
128
|
}
|
|
127
|
-
await runNodeScript(target, extraArgs, { exitOnSuccess: true });
|
|
129
|
+
await runNodeScript(target, extraArgs, { completion: 'exit', exitOnSuccess: true });
|
|
128
130
|
return;
|
|
129
131
|
}
|
|
130
132
|
// `--runtime <gjs|node|bun|deno>` (explicit) → run a BUNDLE FILE on the
|
|
@@ -174,10 +176,10 @@ export const runCommand = {
|
|
|
174
176
|
// Terminal calls — exit on success, or the GJS main loop parks
|
|
175
177
|
// this process forever (see RunGjsBundleOptions.exitOnSuccess).
|
|
176
178
|
if (runtime === 'gjs') {
|
|
177
|
-
await runGjsBundle(file, extraArgs, { exitOnSuccess: true });
|
|
179
|
+
await runGjsBundle(file, extraArgs, { completion: 'exit', exitOnSuccess: true });
|
|
178
180
|
}
|
|
179
181
|
else {
|
|
180
|
-
await runRuntimeBundle(runtime, file, extraArgs, { exitOnSuccess: true });
|
|
182
|
+
await runRuntimeBundle(runtime, file, extraArgs, { completion: 'exit', exitOnSuccess: true });
|
|
181
183
|
}
|
|
182
184
|
return;
|
|
183
185
|
}
|
|
@@ -209,10 +211,10 @@ async function runTargetOnRuntime(runtime, target, extraArgs) {
|
|
|
209
211
|
const file = resolve(target);
|
|
210
212
|
// Terminal call — exit on success (the GJS/GLib loop would otherwise park).
|
|
211
213
|
if (runtime === 'gjs') {
|
|
212
|
-
await runGjsBundle(file, extraArgs, { exitOnSuccess: true });
|
|
214
|
+
await runGjsBundle(file, extraArgs, { completion: 'exit', exitOnSuccess: true });
|
|
213
215
|
}
|
|
214
216
|
else {
|
|
215
|
-
await runRuntimeBundle(runtime, file, extraArgs, { exitOnSuccess: true });
|
|
217
|
+
await runRuntimeBundle(runtime, file, extraArgs, { completion: 'exit', exitOnSuccess: true });
|
|
216
218
|
}
|
|
217
219
|
}
|
|
218
220
|
/**
|
|
@@ -420,6 +422,16 @@ async function runScript(script, extraArgs, cwd = process.cwd()) {
|
|
|
420
422
|
console.error(err.message);
|
|
421
423
|
dispatchFailed = true;
|
|
422
424
|
}
|
|
425
|
+
// A watch loop RESOLVES as soon as it is armed and then keeps running,
|
|
426
|
+
// so its resolution is not an ending to exit on: `gjsify run dev` used
|
|
427
|
+
// to take down the loop it had just started, the instant the first
|
|
428
|
+
// build+launch finished. Node never saw it — there the same script takes
|
|
429
|
+
// the shell-spawn path below — and it is the DOCUMENTED entry point:
|
|
430
|
+
// `gjsify create` prints `gjsify run dev`, and every template's `dev`
|
|
431
|
+
// script is a bare `gjsify dev`. Returning leaves the process on the
|
|
432
|
+
// main loop `runWatchLoop` holds, where Ctrl+C ends it.
|
|
433
|
+
if (!dispatchFailed && isDaemonCommand())
|
|
434
|
+
return;
|
|
423
435
|
// A thrown command wins over `process.exitCode`; otherwise honour a
|
|
424
436
|
// non-zero code the handler set instead of throwing (e.g. a failing
|
|
425
437
|
// check) — a bare exit(0) here masked those as success too.
|
|
@@ -436,23 +448,23 @@ async function runScript(script, extraArgs, cwd = process.cwd()) {
|
|
|
436
448
|
return process.exit(dispatchCode);
|
|
437
449
|
}
|
|
438
450
|
const fullCmd = extraArgs.length > 0 ? `${literal} ${extraArgs.map(shellEscape).join(' ')}` : literal;
|
|
439
|
-
// ensureMainLoop() (called inside spawn) keeps
|
|
440
|
-
// child exits
|
|
441
|
-
//
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
reject(new Error(`script "${script}" exited with code ${code}`));
|
|
449
|
-
});
|
|
450
|
-
child.on('error', reject);
|
|
451
|
-
}).catch((err) => {
|
|
451
|
+
// `completion: 'exit'` — ensureMainLoop() (called inside the async spawn) keeps
|
|
452
|
+
// GJS alive after the child exits, and both paths below end in `process.exit`,
|
|
453
|
+
// which is what tears it down. `shell: true` deliberately skips the win32
|
|
454
|
+
// rewrite: this is a command LINE, and Node routes it through %COMSPEC%.
|
|
455
|
+
let result;
|
|
456
|
+
try {
|
|
457
|
+
result = await spawnToCompletion(fullCmd, [], { completion: 'exit', cwd, env, shell: true });
|
|
458
|
+
}
|
|
459
|
+
catch (err) {
|
|
452
460
|
console.error(err.message);
|
|
453
|
-
process.exit(1);
|
|
454
|
-
}
|
|
455
|
-
|
|
461
|
+
return process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
if (result.code !== 0) {
|
|
464
|
+
console.error(`script "${script}" exited with ${describeExit(result)}`);
|
|
465
|
+
return process.exit(1);
|
|
466
|
+
}
|
|
467
|
+
return process.exit(0);
|
|
456
468
|
}
|
|
457
469
|
function shellEscape(arg) {
|
|
458
470
|
if (/^[a-zA-Z0-9_\-./=:@,]+$/.test(arg))
|
|
@@ -463,70 +475,11 @@ function shellEscape(arg) {
|
|
|
463
475
|
* If `literal` is a SINGLE `gjsify <subcommand> …` command with no shell
|
|
464
476
|
* operators / substitutions / unquoted globs, return the argv to feed `runCli`
|
|
465
477
|
* (the subcommand + its args, with `extraArgs` appended). Otherwise null — the
|
|
466
|
-
* caller falls back to the shell spawn path.
|
|
467
|
-
* glob (`'src/**'`) survives as a single literal token, exactly as the shell
|
|
468
|
-
* would hand it to `gjsify` (which does its own glob expansion).
|
|
478
|
+
* caller falls back to the shell spawn path.
|
|
469
479
|
*/
|
|
470
480
|
function gjsifyInProcessArgv(literal, extraArgs) {
|
|
471
|
-
const
|
|
472
|
-
if (!
|
|
481
|
+
const argv = gjsifyCommandArgv(literal);
|
|
482
|
+
if (!argv)
|
|
473
483
|
return null;
|
|
474
|
-
return [...
|
|
475
|
-
}
|
|
476
|
-
/**
|
|
477
|
-
* Quote-aware tokenizer for a SIMPLE command line (no shell features beyond
|
|
478
|
-
* single/double quoting). Returns the tokens, or null if the string contains
|
|
479
|
-
* anything the shell would treat specially — operators (`&& | ; < > &`),
|
|
480
|
-
* substitutions (`$(...)` / backticks / `$VAR`), unquoted globs/expansions
|
|
481
|
-
* (`* ? { } [ ] ~`), comments (`#`), or an unterminated quote — in which case
|
|
482
|
-
* the caller MUST use the real shell instead.
|
|
483
|
-
*/
|
|
484
|
-
function tokenizeSimpleCommand(cmd) {
|
|
485
|
-
const tokens = [];
|
|
486
|
-
let cur = '';
|
|
487
|
-
let has = false;
|
|
488
|
-
let quote = null;
|
|
489
|
-
for (let i = 0; i < cmd.length; i++) {
|
|
490
|
-
const c = cmd[i];
|
|
491
|
-
if (quote === "'") {
|
|
492
|
-
if (c === "'")
|
|
493
|
-
quote = null;
|
|
494
|
-
else
|
|
495
|
-
cur += c;
|
|
496
|
-
continue;
|
|
497
|
-
}
|
|
498
|
-
if (quote === '"') {
|
|
499
|
-
if (c === '"')
|
|
500
|
-
quote = null;
|
|
501
|
-
else if (c === '\\' && (cmd[i + 1] === '"' || cmd[i + 1] === '\\'))
|
|
502
|
-
cur += cmd[++i];
|
|
503
|
-
else if (c === '$' || c === '`')
|
|
504
|
-
return null; // substitution inside "…"
|
|
505
|
-
else
|
|
506
|
-
cur += c;
|
|
507
|
-
continue;
|
|
508
|
-
}
|
|
509
|
-
if (c === "'" || c === '"') {
|
|
510
|
-
quote = c;
|
|
511
|
-
has = true;
|
|
512
|
-
continue;
|
|
513
|
-
}
|
|
514
|
-
if (c === ' ' || c === '\t') {
|
|
515
|
-
if (has) {
|
|
516
|
-
tokens.push(cur);
|
|
517
|
-
cur = '';
|
|
518
|
-
has = false;
|
|
519
|
-
}
|
|
520
|
-
continue;
|
|
521
|
-
}
|
|
522
|
-
if ('|&;<>`$()\\\n\r*?{}[]~#!'.includes(c))
|
|
523
|
-
return null;
|
|
524
|
-
cur += c;
|
|
525
|
-
has = true;
|
|
526
|
-
}
|
|
527
|
-
if (quote)
|
|
528
|
-
return null; // unterminated quote
|
|
529
|
-
if (has)
|
|
530
|
-
tokens.push(cur);
|
|
531
|
-
return tokens;
|
|
484
|
+
return [...argv, ...extraArgs];
|
|
532
485
|
}
|
|
@@ -25,7 +25,10 @@ import { dirname, join, posix, resolve, win32 } from 'node:path';
|
|
|
25
25
|
import { fileURLToPath } from 'node:url';
|
|
26
26
|
import { fetchPackument } from '@gjsify/npm-registry';
|
|
27
27
|
import { installPackages, makeProgressReporter } from '../utils/install-backend.js';
|
|
28
|
+
import { classifyInstall } from '../utils/install-provenance.js';
|
|
28
29
|
import { defaultGlobalLayout, globalLayoutIsDefault, hasBundlerEngineInstalled, installGjsEnginePackages, linkGlobalBins, resolveBinOnPath, } from '../utils/install-global.js';
|
|
30
|
+
import { resolveHostPlatform } from '../utils/platform-check.js';
|
|
31
|
+
import { pruneAfterInstall } from '../utils/prune-prefix.js';
|
|
29
32
|
const PACKAGE_NAME = '@gjsify/cli';
|
|
30
33
|
export const selfUpdateCommand = {
|
|
31
34
|
command: 'self-update',
|
|
@@ -45,6 +48,11 @@ export const selfUpdateCommand = {
|
|
|
45
48
|
description: 'npm dist-tag or pinned version to install (e.g. `latest`, `next`, `0.5.0`).',
|
|
46
49
|
type: 'string',
|
|
47
50
|
default: 'latest',
|
|
51
|
+
})
|
|
52
|
+
.option('prune', {
|
|
53
|
+
description: 'After updating, remove packages an earlier install left behind that this host cannot use (foreign os/cpu/libc). Use --no-prune to disable.',
|
|
54
|
+
type: 'boolean',
|
|
55
|
+
default: true,
|
|
48
56
|
})
|
|
49
57
|
.option('skip-deps', {
|
|
50
58
|
description: 'Update only the @gjsify/cli bundle itself; do not pull its runtime dependencies ' +
|
|
@@ -60,6 +68,24 @@ export const selfUpdateCommand = {
|
|
|
60
68
|
const currentVersion = readCurrentVersion();
|
|
61
69
|
const installedAtPrefix = existsSync(installedPkgJson);
|
|
62
70
|
console.log(`Current ${PACKAGE_NAME}: v${currentVersion ?? '(unknown)'}`);
|
|
71
|
+
// REFUSE, do not warn-and-continue, when something else owns this install.
|
|
72
|
+
//
|
|
73
|
+
// Everything below writes the user-global XDG prefix. Under a `.deb`,
|
|
74
|
+
// `.rpm` or Flatpak that produces a SECOND copy which shadows the one the
|
|
75
|
+
// package manager tracks — so the next `apt upgrade` reverts it, or the two
|
|
76
|
+
// diverge, and the user's report is "I updated and nothing changed". A
|
|
77
|
+
// clean refusal naming the right command is strictly more useful than a
|
|
78
|
+
// successful write to the wrong place; #1064 is the same lesson one layer
|
|
79
|
+
// down, where the write succeeded and PATH still resolved the old binary.
|
|
80
|
+
const provenance = classifyInstall({ selfDir: readSelfDir(), xdgPrefix: layout.prefix });
|
|
81
|
+
if (provenance.managedElsewhere) {
|
|
82
|
+
console.error(`\ngjsify self-update: this gjsify is managed by something else — ${provenance.evidence}.\n` +
|
|
83
|
+
`Updating it from here would write ${layout.prefix} and leave you running two installs.\n\n` +
|
|
84
|
+
`Update with: ${provenance.updateWith}\n`);
|
|
85
|
+
// `return process.exit()` — a bare `process.exit()` is DEFERRED under
|
|
86
|
+
// GJS and the handler would run on into the install it just refused.
|
|
87
|
+
return process.exit(1);
|
|
88
|
+
}
|
|
63
89
|
if (!installedAtPrefix) {
|
|
64
90
|
console.warn(`\nWarning: no @gjsify/cli install found under ${layout.prefix}.\n` +
|
|
65
91
|
`self-update only manages installs created by install.mjs or \`gjsify install -g\`.\n` +
|
|
@@ -157,6 +183,13 @@ export const selfUpdateCommand = {
|
|
|
157
183
|
if (pullDeps) {
|
|
158
184
|
await installGjsEnginePackages(layout.prefix, target, { verbose: false });
|
|
159
185
|
}
|
|
186
|
+
// Same ordering reason as `install -g`: before the LINK, whose launchers bake
|
|
187
|
+
// the resolved prebuild directories into their env.
|
|
188
|
+
if (args.prune !== false) {
|
|
189
|
+
pruneAfterInstall(layout.prefix, resolveHostPlatform({ env: {} }), {
|
|
190
|
+
hint: 'gjsify prune -g --dry-run',
|
|
191
|
+
});
|
|
192
|
+
}
|
|
160
193
|
const linked = linkGlobalBins([PACKAGE_NAME], layout);
|
|
161
194
|
if (linked.length === 0) {
|
|
162
195
|
console.warn('self-update: install completed but no bins were linked — package.json may be missing a `bin` field.');
|
|
@@ -246,6 +279,28 @@ export function verifyPathResolution(opts) {
|
|
|
246
279
|
* `import.meta.url` (the bundle file under GJS, or `lib/index.js` under
|
|
247
280
|
* Node) until it finds a package.json with `name === '@gjsify/cli'`.
|
|
248
281
|
*/
|
|
282
|
+
/**
|
|
283
|
+
* The directory of the running CLI's own package — the input the provenance
|
|
284
|
+
* question is actually about.
|
|
285
|
+
*
|
|
286
|
+
* The same upward walk `readCurrentVersion()` does, and for the same reason: it
|
|
287
|
+
* works from `lib/index.js` under Node and from the published `dist/cli.gjs.mjs`
|
|
288
|
+
* bundle under GJS. Honours the same `GJSIFY_CLI_PACKAGE_JSON` escape hatch, so a
|
|
289
|
+
* test can place a synthetic install anywhere without a real one.
|
|
290
|
+
*/
|
|
291
|
+
function readSelfDir() {
|
|
292
|
+
const override = process.env.GJSIFY_CLI_PACKAGE_JSON;
|
|
293
|
+
if (override)
|
|
294
|
+
return dirname(resolve(override));
|
|
295
|
+
const here = fileURLToPath(import.meta.url);
|
|
296
|
+
let dir = dirname(resolve(here));
|
|
297
|
+
for (let i = 0; i < 8 && dir !== dirname(dir); i++) {
|
|
298
|
+
if (existsSync(join(dir, 'package.json')))
|
|
299
|
+
return dir;
|
|
300
|
+
dir = dirname(dir);
|
|
301
|
+
}
|
|
302
|
+
return dirname(resolve(here));
|
|
303
|
+
}
|
|
249
304
|
function readCurrentVersion() {
|
|
250
305
|
try {
|
|
251
306
|
// Escape hatch for tests: point GJSIFY_CLI_PACKAGE_JSON at a synthetic
|
package/lib/commands/ship.js
CHANGED
|
@@ -20,7 +20,7 @@ import { deriveDepends, warnAboutGjsFloor } from '../utils/ship/depends.js';
|
|
|
20
20
|
import { discoverPayload } from '../utils/ship/discover.js';
|
|
21
21
|
import { resolveFormats, FORMAT_IDS } from '../utils/ship/formats.js';
|
|
22
22
|
import { scanGiNamespaces } from '../utils/ship/gi-namespaces.js';
|
|
23
|
-
import { buildTimestamp, isArchIndependent } from '../utils/ship/payload.js';
|
|
23
|
+
import { assertPayloadMatchesArch, buildTimestamp, isArchIndependent } from '../utils/ship/payload.js';
|
|
24
24
|
import { planOverlay, planStage } from '../utils/ship/plan.js';
|
|
25
25
|
import { renderLauncher } from '../utils/ship/launcher.js';
|
|
26
26
|
import { buildRpm } from '../utils/ship/rpm.js';
|
|
@@ -151,10 +151,15 @@ async function packOne(input) {
|
|
|
151
151
|
hasSchemas: settings.schemaFiles.length > 0,
|
|
152
152
|
extra: settings.extraDepends[format.id],
|
|
153
153
|
typelibPackages: settings.typelibPackages,
|
|
154
|
+
bundledTypelibs: settings.typelibFiles,
|
|
154
155
|
minGjsVersion: settings.minGjsVersion,
|
|
155
156
|
});
|
|
156
157
|
for (const warning of warnAboutGjsFloor(format.id, settings.minGjsVersion))
|
|
157
158
|
console.warn(`${LOG} ${warning}`);
|
|
159
|
+
// Before the label is chosen, not after: `archName` collapses an
|
|
160
|
+
// arch-independent payload to `all`/`noarch`, and a mismatch only matters
|
|
161
|
+
// for a payload that IS architecture-specific.
|
|
162
|
+
assertPayloadMatchesArch(payload, settings.arch);
|
|
158
163
|
const archLabel = format.archName(settings.arch, isArchIndependent(payload));
|
|
159
164
|
const common = { settings, payload, prefix: format.prefix, depends, archLabel, mtime };
|
|
160
165
|
const bytes = format.id === 'deb' ? await buildDeb(common) : await buildRpm(common);
|
package/lib/commands/showcase.js
CHANGED
|
@@ -411,7 +411,7 @@ async function runShowcaseOnRuntime(showcase, runtime, cliVersion) {
|
|
|
411
411
|
// thrown: an exception escaping the handler makes yargs dump the command's
|
|
412
412
|
// full `--help` above the message, which buries it.
|
|
413
413
|
try {
|
|
414
|
-
await runRuntimeBundle(runtime, entry, [], { exitOnSuccess: true });
|
|
414
|
+
await runRuntimeBundle(runtime, entry, [], { completion: 'exit', exitOnSuccess: true });
|
|
415
415
|
}
|
|
416
416
|
catch (err) {
|
|
417
417
|
console.error(`${err.message}\n\n` +
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
// identically under Node and GJS. @gjsify/storybook is resolved from the
|
|
17
17
|
// CONSUMER's node_modules by the generated entry, so it stays out of the CLI's
|
|
18
18
|
// own dependencies.
|
|
19
|
-
import { spawn } from 'node:child_process';
|
|
20
19
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
21
20
|
import { join, relative, resolve } from 'node:path';
|
|
22
21
|
import { hostRuntime } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
@@ -165,18 +164,18 @@ export const storybookCommand = {
|
|
|
165
164
|
const launch = async () => {
|
|
166
165
|
if (runtime === 'gjs') {
|
|
167
166
|
const { runGjsBundle } = await import('../utils/run-gjs.js');
|
|
168
|
-
await runGjsBundle(outPath, [], { exitOnSuccess: true });
|
|
167
|
+
await runGjsBundle(outPath, [], { completion: 'exit', exitOnSuccess: true });
|
|
169
168
|
}
|
|
170
169
|
else if (runtime === 'node') {
|
|
171
170
|
const { runNodeBundle } = await import('../utils/run-node.js');
|
|
172
|
-
await runNodeBundle(outPath, [], { exitOnSuccess: true });
|
|
171
|
+
await runNodeBundle(outPath, [], { completion: 'exit', exitOnSuccess: true });
|
|
173
172
|
}
|
|
174
173
|
else {
|
|
175
174
|
// bun / deno — run the SAME `--app node` bundle via the shared
|
|
176
175
|
// runner (Node-API is their common ABI). node-gi is required
|
|
177
176
|
// because the storybook uses gi:// (checked in runRuntimeBundle).
|
|
178
177
|
const { runRuntimeBundle } = await import('../utils/run-node.js');
|
|
179
|
-
await runRuntimeBundle(runtime, outPath, [], { exitOnSuccess: true });
|
|
178
|
+
await runRuntimeBundle(runtime, outPath, [], { completion: 'exit', exitOnSuccess: true });
|
|
180
179
|
}
|
|
181
180
|
};
|
|
182
181
|
if (!args.watch) {
|
|
@@ -186,77 +185,21 @@ export const storybookCommand = {
|
|
|
186
185
|
await launch();
|
|
187
186
|
return;
|
|
188
187
|
}
|
|
189
|
-
// Watch mode: rebuild + relaunch on any change under the stories dir
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
// typelib env wired like `gjsify run`. All three need @gjsify/node-gi
|
|
200
|
-
// (the storybook uses gi://), kept external by the bundler.
|
|
201
|
-
const { computeNativeEnvForBundle } = await import('../utils/run-gjs.js');
|
|
202
|
-
const { resolveNodeGiForBundle, nodeBinary } = await import('../utils/run-node.js');
|
|
203
|
-
const { RUNTIMES } = await import('../utils/runtimes.js');
|
|
204
|
-
// Same resolution base as `runRuntimeBundle`: the bundle's own dir
|
|
205
|
-
// first (that is where the runtime resolves the external specifier
|
|
206
|
-
// from), cwd as the fallback.
|
|
207
|
-
if (!resolveNodeGiForBundle(outPath, cwd)) {
|
|
208
|
-
throw new Error(`Cannot run the storybook on ${runtime}: \`@gjsify/node-gi\` is not installed. ` +
|
|
209
|
-
`Add @gjsify/node-gi as a dependency to run the storybook on ${runtime}.`);
|
|
210
|
-
}
|
|
211
|
-
const { env: nativeEnv } = computeNativeEnvForBundle(outPath, cwd);
|
|
212
|
-
const env = { ...process.env, ...nativeEnv };
|
|
213
|
-
// `node` uses the resolved node binary (under a GJS-hosted CLI
|
|
214
|
-
// `process.execPath` is gjs); bun/deno launch via the shared spec
|
|
215
|
-
// (deno's `--node-modules-dir=manual`, etc.).
|
|
216
|
-
if (runtime === 'node') {
|
|
217
|
-
return spawn(nodeBinary(), [outPath], { stdio: 'inherit', env });
|
|
218
|
-
}
|
|
219
|
-
const [cmd, launchArgs] = RUNTIMES[runtime].launch(outPath);
|
|
220
|
-
return spawn(cmd, launchArgs, { stdio: 'inherit', env });
|
|
221
|
-
};
|
|
222
|
-
let child = null;
|
|
223
|
-
let rebuilding = false;
|
|
224
|
-
let pending = false;
|
|
225
|
-
const rebuild = async () => {
|
|
226
|
-
if (rebuilding) {
|
|
227
|
-
pending = true;
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
rebuilding = true;
|
|
231
|
-
try {
|
|
188
|
+
// Watch mode: rebuild + relaunch on any change under the stories dir,
|
|
189
|
+
// through the loop shared with `gjsify dev` (`utils/watch-loop.ts`).
|
|
190
|
+
const { runWatchLoop, spawnBundleSupervised } = await import('../utils/watch-loop.js');
|
|
191
|
+
await runWatchLoop({
|
|
192
|
+
dir: storiesDir,
|
|
193
|
+
dirLabel: relative(cwd, storiesDir) || '.',
|
|
194
|
+
label: 'storybook',
|
|
195
|
+
// Stories are DISCOVERED, so the generated entry is re-derived on
|
|
196
|
+
// every pass — a new `*.story.ts` must show up without a restart.
|
|
197
|
+
prepare: () => {
|
|
232
198
|
writeEntry();
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
await buildBundle();
|
|
238
|
-
if (!args.buildOnly) {
|
|
239
|
-
child = await spawnChild();
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
catch (err) {
|
|
243
|
-
console.error(`[storybook] rebuild failed: ${err.message}`);
|
|
244
|
-
}
|
|
245
|
-
finally {
|
|
246
|
-
rebuilding = false;
|
|
247
|
-
if (pending) {
|
|
248
|
-
pending = false;
|
|
249
|
-
void rebuild();
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
};
|
|
253
|
-
await rebuild();
|
|
254
|
-
let debounce = null;
|
|
255
|
-
watch(storiesDir, { recursive: true }, () => {
|
|
256
|
-
if (debounce)
|
|
257
|
-
clearTimeout(debounce);
|
|
258
|
-
debounce = setTimeout(() => void rebuild(), 200);
|
|
199
|
+
},
|
|
200
|
+
build: buildBundle,
|
|
201
|
+
spawnChild: args.buildOnly ? null : () => spawnBundleSupervised(runtime, outPath, cwd, 'the storybook'),
|
|
202
|
+
output: outPath,
|
|
259
203
|
});
|
|
260
|
-
console.log(`[storybook] watching ${relative(cwd, storiesDir) || '.'} — press Ctrl+C to stop`);
|
|
261
204
|
},
|
|
262
205
|
};
|
package/lib/commands/test.js
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
// exit codes.
|
|
8
8
|
import { existsSync, statSync, readdirSync } from 'node:fs';
|
|
9
9
|
import { join, dirname, resolve, relative } from 'node:path';
|
|
10
|
-
import { spawn } from 'node:child_process';
|
|
11
10
|
import { isRuntimeAvailable, RUNTIMES } from '../utils/runtimes.js';
|
|
12
11
|
import { nodeBinary } from '../utils/run-node.js';
|
|
12
|
+
import { describeExit, spawnToCompletion } from '../utils/spawn.js';
|
|
13
13
|
import { Config } from '../config.js';
|
|
14
14
|
import { BuildAction } from '../actions/build.js';
|
|
15
15
|
import { runGjsBundle } from '../utils/run-gjs.js';
|
|
@@ -190,23 +190,19 @@ async function buildTestBundle(entry, outfile, runtime, verbose) {
|
|
|
190
190
|
/** Run a single test bundle and reject on non-zero exit. */
|
|
191
191
|
async function runTestBundle(outfile, runtime) {
|
|
192
192
|
if (runtime === 'gjs') {
|
|
193
|
-
|
|
193
|
+
// `completion: 'exit'` (this handler ends in `process.exit`) WITHOUT
|
|
194
|
+
// `exitOnSuccess`: the two answer different questions, and exiting here
|
|
195
|
+
// would truncate the multi-runtime loop after the first bundle.
|
|
196
|
+
await runGjsBundle(outfile, [], { completion: 'exit' });
|
|
194
197
|
return;
|
|
195
198
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
child.on('exit', (code) => {
|
|
204
|
-
if (code === 0)
|
|
205
|
-
resolvePromise();
|
|
206
|
-
else
|
|
207
|
-
reject(new Error(`node exited with code ${code}`));
|
|
208
|
-
});
|
|
209
|
-
});
|
|
199
|
+
// `nodeBinary()`, never a bare `'node'`: under GJS `process.execPath` is
|
|
200
|
+
// the `gjs` interpreter, which cannot run a `--app node` bundle, and on a
|
|
201
|
+
// host without Node a bare literal dies `spawn node ENOENT` — which is
|
|
202
|
+
// how a fully passing GJS suite was reported as a failed run.
|
|
203
|
+
const result = await spawnToCompletion(nodeBinary(), [outfile], { completion: 'exit' });
|
|
204
|
+
if (result.code !== 0)
|
|
205
|
+
throw new Error(`node exited with ${describeExit(result)}`);
|
|
210
206
|
}
|
|
211
207
|
/** True when `outfile` exists and is newer than every `.ts`/`.mts` file under the entry's directory tree. */
|
|
212
208
|
function isFresh(outfile, entry) {
|