@gjsify/cli 0.29.0 → 0.31.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 +197 -189
- 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/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/bin-shim.d.ts +39 -0
- package/lib/utils/bin-shim.js +89 -13
- package/lib/utils/check-system-deps.d.ts +88 -1
- package/lib/utils/check-system-deps.js +177 -6
- 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-backend-native.d.ts +8 -0
- package/lib/utils/install-backend-native.js +8 -2
- package/lib/utils/install-global.d.ts +30 -2
- package/lib/utils/install-global.js +51 -5
- package/lib/utils/oxc-resolve.d.ts +1 -1
- package/lib/utils/oxc-resolve.js +10 -6
- package/lib/utils/run-lifecycle-script.js +41 -25
- package/lib/utils/spawn.d.ts +17 -1
- package/lib/utils/spawn.js +23 -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
|
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';
|
package/lib/config.js
CHANGED
|
@@ -223,8 +223,12 @@ export class Config {
|
|
|
223
223
|
const pkg = (await this.readPackageJSON(configFilePath));
|
|
224
224
|
const tsConfig = (await this.readTSConfig(configFilePath));
|
|
225
225
|
tsConfig.reflection ||= cliArgs.reflection;
|
|
226
|
-
//
|
|
227
|
-
|
|
226
|
+
// `--verbose` and `--log-level` are two spellings of one question, and
|
|
227
|
+
// only the boolean reached here — so `--log-level debug` turned the
|
|
228
|
+
// BUNDLER's logging up (see the mapping further down, which sets
|
|
229
|
+
// `bundler.logLevel`) while the CLI's own verbose output stayed off.
|
|
230
|
+
// Either spelling now answers it; neither silences the other.
|
|
231
|
+
configData.verbose = cliArgs.verbose || cliArgs.logLevel === 'debug' || cliArgs.logLevel === 'verbose';
|
|
228
232
|
configData.exclude = cliArgs.exclude || [];
|
|
229
233
|
if (cliArgs.consoleShim !== undefined)
|
|
230
234
|
configData.consoleShim = cliArgs.consoleShim;
|
package/lib/index.js
CHANGED
|
@@ -17,7 +17,24 @@ catch (err) {
|
|
|
17
17
|
// a scary `Gjs-CRITICAL **: Module … threw an exception` that buries the
|
|
18
18
|
// real, already-actionable message (e.g. "gjsify build: no usable bundler
|
|
19
19
|
// engine under GJS — build the facade first or run under Node"). See #597.
|
|
20
|
-
|
|
20
|
+
//
|
|
21
|
+
// The `code` is printed WITH the message, not dropped. Errors in this CLI
|
|
22
|
+
// carry npm's own codes on purpose — `badPlatformError()` documents that it
|
|
23
|
+
// reproduces npm's `EBADPLATFORM` payload "so a consumer's error handling
|
|
24
|
+
// (and a human reading the message) sees what npm reports" — and npm itself
|
|
25
|
+
// prints the code (`npm ERR! code EBADPLATFORM`) rather than only the prose.
|
|
26
|
+
//
|
|
27
|
+
// It has to be explicit here because this catch became the SINGLE printer:
|
|
28
|
+
// before the `.fail()` handler in `cli-app.ts`, yargs logged the error OBJECT
|
|
29
|
+
// and the code came along for free. Printing `err.message` alone silently
|
|
30
|
+
// dropped it for every coded error, which is what turned
|
|
31
|
+
// `tests/e2e/install-optional-both-blocks` red on `main` — it asserts the
|
|
32
|
+
// code is visible, and the prose it still printed did not contain it.
|
|
33
|
+
const code = typeof err === 'object' && err !== null && typeof err.code === 'string'
|
|
34
|
+
? err.code
|
|
35
|
+
: '';
|
|
36
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
37
|
+
console.error(code && !message.includes(code) ? `${code}: ${message}` : message);
|
|
21
38
|
if (process.env.GJSIFY_DEBUG && err instanceof Error && err.stack)
|
|
22
39
|
console.error(err.stack);
|
|
23
40
|
// `process.exitCode` is the gentle Node idiom (streams flush). GJS has no
|
|
@@ -58,8 +58,18 @@ const USER_AGENT = 'gjsify-installer/1.0';
|
|
|
58
58
|
const CACHE_PREFIX = 'cli-';
|
|
59
59
|
const CACHE_SUFFIX = '.gjs.mjs';
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Under `--fetch-only` the ONLY thing on stdout must be the bundle path, so
|
|
63
|
+
* progress goes to stderr instead. A caller does
|
|
64
|
+
* `BOOTSTRAP=$(gjs -m install.mjs --fetch-only)`, and one stray `info()` line
|
|
65
|
+
* would make that a path with a log message glued to the front — a failure that
|
|
66
|
+
* surfaces as a confusing "file not found" rather than as bad output.
|
|
67
|
+
*/
|
|
68
|
+
let logToStderr = false;
|
|
69
|
+
|
|
61
70
|
function info(msg) {
|
|
62
|
-
|
|
71
|
+
if (logToStderr) printerr(`[gjsify] ${msg}`);
|
|
72
|
+
else print(`[gjsify] ${msg}`);
|
|
63
73
|
}
|
|
64
74
|
function error(msg) {
|
|
65
75
|
printerr(`[gjsify] ERROR: ${msg}`);
|
|
@@ -71,6 +81,7 @@ function parseArgs() {
|
|
|
71
81
|
let tag = 'latest';
|
|
72
82
|
let force = false;
|
|
73
83
|
let help = false;
|
|
84
|
+
let fetchOnly = false;
|
|
74
85
|
let bootstrapUrl = GLib.getenv('GJSIFY_INSTALL_BOOTSTRAP_URL') || DEFAULT_BOOTSTRAP_URL;
|
|
75
86
|
let bootstrapSha256Url = GLib.getenv('GJSIFY_INSTALL_BOOTSTRAP_SHA256_URL');
|
|
76
87
|
if (bootstrapSha256Url === null || bootstrapSha256Url === undefined) {
|
|
@@ -87,8 +98,9 @@ function parseArgs() {
|
|
|
87
98
|
else if (a.startsWith('--tag=')) tag = a.slice('--tag='.length);
|
|
88
99
|
else if (a === '--bootstrap-url') bootstrapUrl = argv[++i];
|
|
89
100
|
else if (a.startsWith('--bootstrap-url=')) bootstrapUrl = a.slice('--bootstrap-url='.length);
|
|
101
|
+
else if (a === '--fetch-only') fetchOnly = true;
|
|
90
102
|
}
|
|
91
|
-
return { target, tag, force, help, bootstrapUrl, bootstrapSha256Url };
|
|
103
|
+
return { target, tag, force, help, bootstrapUrl, bootstrapSha256Url, fetchOnly };
|
|
92
104
|
}
|
|
93
105
|
|
|
94
106
|
function printUsage() {
|
|
@@ -102,6 +114,10 @@ Options:
|
|
|
102
114
|
--tag <tag> npm dist-tag or version (default: latest)
|
|
103
115
|
--force, -f Reinstall even when present.
|
|
104
116
|
--bootstrap-url <url> Override the cli.gjs.mjs download URL.
|
|
117
|
+
--fetch-only Download + verify the bootstrap bundle, print its cached
|
|
118
|
+
path to stdout and exit — no install. Lets a caller (CI)
|
|
119
|
+
reuse this downloader instead of growing a second one
|
|
120
|
+
beside it; progress goes to stderr so stdout is the path.
|
|
105
121
|
--help, -h Show this message.
|
|
106
122
|
|
|
107
123
|
Env vars:
|
|
@@ -318,6 +334,9 @@ async function main() {
|
|
|
318
334
|
printUsage();
|
|
319
335
|
exit(0);
|
|
320
336
|
}
|
|
337
|
+
// Set BEFORE the first `info()` — resolveBootstrap logs while downloading,
|
|
338
|
+
// and those lines would otherwise land on stdout ahead of the path.
|
|
339
|
+
logToStderr = opts.fetchOnly;
|
|
321
340
|
checkGjsVersion();
|
|
322
341
|
|
|
323
342
|
const session = new Soup.Session();
|
|
@@ -329,6 +348,12 @@ async function main() {
|
|
|
329
348
|
exit(1);
|
|
330
349
|
}
|
|
331
350
|
|
|
351
|
+
if (opts.fetchOnly) {
|
|
352
|
+
// The one thing on stdout. Everything above went to stderr.
|
|
353
|
+
print(bundlePath);
|
|
354
|
+
exit(0);
|
|
355
|
+
}
|
|
356
|
+
|
|
332
357
|
const spec = buildSpec(opts.target, opts.tag);
|
|
333
358
|
try {
|
|
334
359
|
await runInstall(bundlePath, spec);
|
package/lib/types/command.d.ts
CHANGED
|
@@ -7,3 +7,21 @@ export interface Command<T = unknown, U = T> {
|
|
|
7
7
|
middlewares?: MiddlewareFunction[];
|
|
8
8
|
deprecated?: boolean | string;
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* A command that definitely runs something — the shape of every command except
|
|
12
|
+
* the builder-only parents.
|
|
13
|
+
*
|
|
14
|
+
* WHY IT EXISTS: `Command.handler` has to stay optional for parent commands,
|
|
15
|
+
* but that optionality then lands on every CALL site, including the ones
|
|
16
|
+
* outside `cli-app.ts`'s yargs registration where a missing handler would be a
|
|
17
|
+
* silent no-op rather than a type error. `src/affected-entry.ts` — the entry
|
|
18
|
+
* the committed `dist/affected.gjs.mjs` is built from — invoked
|
|
19
|
+
* `affectedCommand.handler(...)` through a cast that suppressed exactly that,
|
|
20
|
+
* and nothing noticed for as long as the package's tsconfig scoped the compile
|
|
21
|
+
* to the import closure of `src/index.ts`. Annotate a command that a standalone
|
|
22
|
+
* entry invokes directly with this type, and the guarantee is in the type
|
|
23
|
+
* instead of in a cast.
|
|
24
|
+
*/
|
|
25
|
+
export type LeafCommand<T = unknown, U = T> = Command<T, U> & {
|
|
26
|
+
handler: NonNullable<Command<T, U>['handler']>;
|
|
27
|
+
};
|
package/lib/utils/bin-shim.d.ts
CHANGED
|
@@ -61,6 +61,17 @@ export declare function buildLauncherShims(opts: {
|
|
|
61
61
|
* path the prebuilds have to be reachable through.
|
|
62
62
|
*/
|
|
63
63
|
prependEnv?: Readonly<Record<string, string>>;
|
|
64
|
+
/**
|
|
65
|
+
* Second interpreter + script to run when the primary interpreter is not on
|
|
66
|
+
* `PATH`. The batch/PowerShell counterpart of {@link buildShLauncher}'s
|
|
67
|
+
* `nodeFallbackAbs` — a Windows host with no `gjs` is the majority case, and
|
|
68
|
+
* the `.cmd`/`.ps1` members are the ones cmd.exe and pwsh actually reach.
|
|
69
|
+
*/
|
|
70
|
+
fallback?: {
|
|
71
|
+
interpreter: string;
|
|
72
|
+
interpreterArgs?: readonly string[];
|
|
73
|
+
target: string;
|
|
74
|
+
};
|
|
64
75
|
}): {
|
|
65
76
|
cmd: string;
|
|
66
77
|
ps1: string;
|
|
@@ -182,6 +193,26 @@ export declare function normalizeBinMap(pkgName: string, bin: string | Record<st
|
|
|
182
193
|
*
|
|
183
194
|
* `isGjsBin` lets the caller keep plain-Node packages on the plain symlink path
|
|
184
195
|
* — this must not change how `lodash`'s bin is linked.
|
|
196
|
+
*
|
|
197
|
+
* WHY THE npm MAP COMES BACK TOO
|
|
198
|
+
*
|
|
199
|
+
* The reasoning above is sound and INCOMPLETE: it was written from the Node-less
|
|
200
|
+
* GJS host and is blind to the mirror case. `@gjsify/cli` declares BOTH fields,
|
|
201
|
+
* so a host with no `gjs` — macOS and Windows, the two active port targets —
|
|
202
|
+
* got `exec gjs -m <bundle>`, which is exit 127, while a working Node entry sat
|
|
203
|
+
* beside it in the same package. `cli-cross-platform.yml` recorded that on both
|
|
204
|
+
* OSes for three consecutive releases (v0.27.1, v0.28.0, v0.29.0); it is
|
|
205
|
+
* advisory, so it stayed red. The install even KNEW: its own post-install check
|
|
206
|
+
* printed `Missing required system dependencies: ✗ GJS` and then wrote a
|
|
207
|
+
* gjs-only launcher anyway.
|
|
208
|
+
*
|
|
209
|
+
* So `nodeFallback` carries the npm map alongside, and the LAUNCHER decides per
|
|
210
|
+
* invocation. Deliberately not an install-time host probe: an install-time
|
|
211
|
+
* decision is a SNAPSHOT, and this repo has already paid for that exact mistake
|
|
212
|
+
* once — `buildNativeEnvPreamble` used to bake the install-time prebuild scan,
|
|
213
|
+
* so a package installed later was invisible and v0.24.1 shipped a launcher with
|
|
214
|
+
* no preamble at all. A host probe here fails the same way in reverse: install
|
|
215
|
+
* without gjs, install gjs afterwards, shim still points at Node forever.
|
|
185
216
|
*/
|
|
186
217
|
export declare function pickBinMap(pkgName: string, pkgJson: {
|
|
187
218
|
bin?: string | Record<string, string>;
|
|
@@ -191,6 +222,7 @@ export declare function pickBinMap(pkgName: string, pkgJson: {
|
|
|
191
222
|
}): {
|
|
192
223
|
map: Map<string, string>;
|
|
193
224
|
isGjsBin: boolean;
|
|
225
|
+
nodeFallback: Map<string, string> | null;
|
|
194
226
|
} | null;
|
|
195
227
|
/**
|
|
196
228
|
* The POSIX `sh` launcher gjsify writes for a bin it owns.
|
|
@@ -205,10 +237,17 @@ export declare function pickBinMap(pkgName: string, pkgJson: {
|
|
|
205
237
|
* because not every published bundle carries a `#!/usr/bin/env -S gjs -m` line,
|
|
206
238
|
* and direct-exec'ing a shebang-less `.mjs` falls through to `/bin/sh`, which
|
|
207
239
|
* then parses JavaScript as shell.
|
|
240
|
+
*
|
|
241
|
+
* `nodeFallbackAbs` makes the runtime choice per INVOCATION rather than per
|
|
242
|
+
* install — see {@link pickBinMap} for why a host probe at install time is the
|
|
243
|
+
* wrong shape. `gjs` stays first: that is today's behaviour on a host carrying
|
|
244
|
+
* both, and which runtime wins is a separate decision from making the shim
|
|
245
|
+
* runnable at all.
|
|
208
246
|
*/
|
|
209
247
|
export declare function buildShLauncher(targetAbs: string, opts: {
|
|
210
248
|
envPreamble?: string;
|
|
211
249
|
isGjsBundle: boolean;
|
|
250
|
+
nodeFallbackAbs?: string;
|
|
212
251
|
}): string;
|
|
213
252
|
/** True when a bin target is a GJS-runnable bundle rather than a Node script. */
|
|
214
253
|
export declare function isGjsBundlePath(target: string): boolean;
|