@gjsify/cli 0.22.0 → 0.24.1
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 +28 -1
- package/dist/cli.gjs.mjs +265 -184
- package/lib/actions/build.d.ts +12 -0
- package/lib/actions/build.js +26 -0
- package/lib/bundler-pick.js +56 -7
- package/lib/commands/affected.js +84 -14
- package/lib/commands/check.js +38 -29
- package/lib/commands/flatpak/build.js +19 -18
- package/lib/commands/foreach.js +119 -42
- package/lib/commands/info.js +11 -5
- package/lib/commands/install.d.ts +16 -0
- package/lib/commands/install.js +123 -36
- package/lib/commands/onboard.js +6 -1
- package/lib/commands/pack.js +69 -4
- package/lib/commands/run.js +26 -14
- package/lib/commands/showcase.js +17 -13
- package/lib/commands/storybook.js +5 -2
- package/lib/commands/trust.js +4 -62
- package/lib/commands/workspace.js +49 -30
- package/lib/utils/bin-shim.d.ts +66 -0
- package/lib/utils/bin-shim.js +246 -0
- package/lib/utils/build-cache.d.ts +42 -6
- package/lib/utils/build-cache.js +114 -42
- package/lib/utils/detect-native-packages.d.ts +118 -7
- package/lib/utils/detect-native-packages.js +213 -29
- package/lib/utils/gjs-bundle-guard.d.ts +32 -0
- package/lib/utils/gjs-bundle-guard.js +103 -0
- package/lib/utils/install-backend-native.d.ts +24 -0
- package/lib/utils/install-backend-native.js +89 -16
- package/lib/utils/install-global.d.ts +16 -0
- package/lib/utils/install-global.js +58 -8
- package/lib/utils/oxc-resolve.js +25 -57
- package/lib/utils/publish-headers.d.ts +2 -3
- package/lib/utils/publish-headers.js +64 -3
- package/lib/utils/publishable-packages.d.ts +22 -0
- package/lib/utils/publishable-packages.js +93 -0
- package/lib/utils/run-gjs.d.ts +6 -7
- package/lib/utils/run-gjs.js +4 -3
- package/lib/utils/run-node.d.ts +16 -0
- package/lib/utils/run-node.js +28 -7
- package/lib/utils/runtimes.d.ts +21 -0
- package/lib/utils/runtimes.js +23 -0
- package/lib/utils/spawn.d.ts +82 -0
- package/lib/utils/spawn.js +139 -0
- package/lib/utils/stdin.d.ts +25 -0
- package/lib/utils/stdin.js +132 -0
- package/package.json +23 -18
package/lib/commands/foreach.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// Output is line-prefixed `[<workspace-name>]` when --parallel is set,
|
|
9
9
|
// matching yarn's interactive flow. Exit code is non-zero if any child
|
|
10
10
|
// process failed; first failure's stderr is forwarded.
|
|
11
|
-
import {
|
|
11
|
+
import { spawnToCompletion } from '../utils/spawn.js';
|
|
12
12
|
import { readFileSync, readdirSync } from 'node:fs';
|
|
13
13
|
import { cpus } from 'node:os';
|
|
14
14
|
import { buildDependencyGraph, discoverWorkspaces, filterWorkspaces, topologicalSort, } from '@gjsify/workspace';
|
|
@@ -208,7 +208,7 @@ export const foreachCommand = {
|
|
|
208
208
|
default: false,
|
|
209
209
|
})
|
|
210
210
|
.option('include', {
|
|
211
|
-
description: 'Glob pattern to include workspaces by name (repeatable).',
|
|
211
|
+
description: 'Glob pattern to include workspaces by name (repeatable). A pattern that matches NO discovered workspace is a hard error — a filter selecting nothing is a typo or a caller-side quoting bug, never an intent.',
|
|
212
212
|
type: 'string',
|
|
213
213
|
array: true,
|
|
214
214
|
})
|
|
@@ -295,6 +295,7 @@ export const foreachCommand = {
|
|
|
295
295
|
process.exit(1);
|
|
296
296
|
}
|
|
297
297
|
}
|
|
298
|
+
assertEveryIncludeMatches(allWorkspaces, args.include);
|
|
298
299
|
let selected = filterWorkspaces(allWorkspaces, {
|
|
299
300
|
include: args.include,
|
|
300
301
|
exclude: args.exclude,
|
|
@@ -344,6 +345,12 @@ export const foreachCommand = {
|
|
|
344
345
|
}
|
|
345
346
|
}
|
|
346
347
|
if (selected.length === 0) {
|
|
348
|
+
// Reaching here means every --include pattern DID match something
|
|
349
|
+
// (assertEveryIncludeMatches above is fatal otherwise) and the set
|
|
350
|
+
// was then narrowed to nothing by --exclude, --no-private, the
|
|
351
|
+
// script-presence filter, or --shard. All four are legitimate, so
|
|
352
|
+
// this stays a clean exit-0 no-op — see the assert's doc comment
|
|
353
|
+
// for why the two cases are not the same condition.
|
|
347
354
|
console.log(`gjsify foreach: no workspaces match (${exec ? 'exec' : 'script'}="${cmd}", include=${JSON.stringify(args.include ?? [])}, exclude=${JSON.stringify(args.exclude ?? [])})`);
|
|
348
355
|
return;
|
|
349
356
|
}
|
|
@@ -410,6 +417,69 @@ export const foreachCommand = {
|
|
|
410
417
|
process.exit(0);
|
|
411
418
|
},
|
|
412
419
|
};
|
|
420
|
+
/**
|
|
421
|
+
* A `--include` pattern that matches ZERO of the discovered workspaces is a
|
|
422
|
+
* hard error. This is the load-bearing half of the fix for repo task #75: the
|
|
423
|
+
* CI classifier emitted its filter pre-quoted (`--include '@gjsify/fs'`), the
|
|
424
|
+
* workflow expanded it unquoted inside a `su … -c "… sh -c '…'"` nesting, the
|
|
425
|
+
* apostrophes became part of every glob, and this command matched nothing,
|
|
426
|
+
* printed one line, and exited 0. A build that did nothing was indistinguishable
|
|
427
|
+
* from a build that succeeded, so the bug lived through every selective PR run
|
|
428
|
+
* — the Build step took 0.65 s and every later step inspected whatever the
|
|
429
|
+
* cache had restored. The quoting was the defect; the SILENCE is why nobody saw
|
|
430
|
+
* it.
|
|
431
|
+
*
|
|
432
|
+
* Scope is deliberately "an include PATTERN matched no workspace", NOT "the
|
|
433
|
+
* final selection is empty". The final selection is legitimately empty in
|
|
434
|
+
* several supported situations and must stay exit-0:
|
|
435
|
+
* * `--shard 3/4` over a 2-workspace closure — most shards run nothing;
|
|
436
|
+
* * `--exclude '@gjsify/integration-*'` removing everything the include kept;
|
|
437
|
+
* * script mode dropping workspaces that don't declare `<script>` (yarn does
|
|
438
|
+
* this too, and it is why `foreach check` over a closure of script-less
|
|
439
|
+
* packages is a no-op rather than a failure).
|
|
440
|
+
* All three describe a real, existing set being narrowed to nothing. A pattern
|
|
441
|
+
* that names something the workspace tree does not contain is different in
|
|
442
|
+
* kind: it is a typo, a stale package name, or a caller-side quoting bug, and
|
|
443
|
+
* it can never be what the caller meant. There is no opt-out flag — an escape
|
|
444
|
+
* hatch here would be re-arming exactly the silence this exists to remove.
|
|
445
|
+
*/
|
|
446
|
+
function assertEveryIncludeMatches(allWorkspaces, include) {
|
|
447
|
+
if (!include || include.length === 0)
|
|
448
|
+
return;
|
|
449
|
+
const unmatched = include.filter((pattern) => filterWorkspaces(allWorkspaces, { include: [pattern] }).length === 0);
|
|
450
|
+
if (unmatched.length === 0)
|
|
451
|
+
return;
|
|
452
|
+
const lines = [
|
|
453
|
+
`gjsify foreach: --include matched no workspace (${unmatched.length} of ${include.length} pattern(s)):`,
|
|
454
|
+
];
|
|
455
|
+
for (const pattern of unmatched) {
|
|
456
|
+
lines.push(` ${JSON.stringify(pattern)}`);
|
|
457
|
+
// The task-#75 signature: the pattern only fails because the caller's
|
|
458
|
+
// shell quotes ended up INSIDE the value. Name it outright — that
|
|
459
|
+
// failure reads as "package not found" and sends people hunting in the
|
|
460
|
+
// wrong place.
|
|
461
|
+
const unquoted = stripSurroundingQuotes(pattern);
|
|
462
|
+
if (unquoted !== pattern && filterWorkspaces(allWorkspaces, { include: [unquoted] }).length > 0) {
|
|
463
|
+
lines.push(` → matches as ${JSON.stringify(unquoted)}: the QUOTES are part of the value. A shell expansion`, ` is not re-scanned for quoting, so a pre-quoted filter spliced through \`sh -c\` arrives with`, ` the quotes attached. Pass the bare name and let word splitting do the work.`);
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
const near = allWorkspaces
|
|
467
|
+
.map((w) => w.name)
|
|
468
|
+
.filter((name) => name.toLowerCase().includes(unquoted.replace(/\*/g, '').toLowerCase()))
|
|
469
|
+
.slice(0, 3);
|
|
470
|
+
if (near.length > 0)
|
|
471
|
+
lines.push(` → did you mean: ${near.join(', ')}?`);
|
|
472
|
+
}
|
|
473
|
+
lines.push(` ${allWorkspaces.length} workspace(s) discovered. Refusing to run: a filter that selects nothing would`, ` otherwise exit 0 and look exactly like a successful run over the whole set.`);
|
|
474
|
+
console.error(lines.join('\n'));
|
|
475
|
+
process.exit(1);
|
|
476
|
+
}
|
|
477
|
+
/** Drop ONE matching pair of surrounding `'` or `"` — the quoting-bug probe. */
|
|
478
|
+
function stripSurroundingQuotes(s) {
|
|
479
|
+
if (s.length >= 2 && (s[0] === "'" || s[0] === '"') && s[s.length - 1] === s[0])
|
|
480
|
+
return s.slice(1, -1);
|
|
481
|
+
return s;
|
|
482
|
+
}
|
|
413
483
|
async function runSequential(workspaces, script, args, verbose, exec, cache) {
|
|
414
484
|
for (const ws of workspaces) {
|
|
415
485
|
await runOne(ws, script, args, /* prefixOutput */ false, verbose, exec, cache);
|
|
@@ -602,47 +672,54 @@ function detectPackageManager() {
|
|
|
602
672
|
return 'gjsify';
|
|
603
673
|
return 'npm';
|
|
604
674
|
}
|
|
605
|
-
function spawnPrefixed(cmd, args, cwd, prefix) {
|
|
606
|
-
//
|
|
607
|
-
//
|
|
608
|
-
//
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
675
|
+
async function spawnPrefixed(cmd, args, cwd, prefix) {
|
|
676
|
+
// `completion: 'exit'` — every path out of this command's handler ends in
|
|
677
|
+
// `process.exit(…)` (see the handler's tail), which is what quits the GLib
|
|
678
|
+
// main loop `spawn()` arms under GJS. That keeps foreach on the STREAMING
|
|
679
|
+
// async path: live prefixed output and real parallelism, both of which the
|
|
680
|
+
// blocking path would destroy. See utils/spawn.ts for the full contract.
|
|
681
|
+
let tracked = null;
|
|
682
|
+
const flushers = [];
|
|
683
|
+
try {
|
|
684
|
+
const result = await spawnToCompletion(cmd, args, {
|
|
685
|
+
completion: 'exit',
|
|
612
686
|
cwd,
|
|
613
|
-
stdio: prefix ?
|
|
614
|
-
|
|
687
|
+
stdio: prefix ? 'pipe' : 'inherit',
|
|
688
|
+
// Default FORCE_COLOR=1 unless the user explicitly opted out, so
|
|
689
|
+
// tools that key on `process.stdout.isTTY` (chalk, picocolors, …)
|
|
690
|
+
// still emit ANSI colors under gjsify foreach. Mirrors yarn / npm.
|
|
691
|
+
color: true,
|
|
692
|
+
onSpawn: (child) => {
|
|
693
|
+
tracked = child;
|
|
694
|
+
activeChildren.add(child);
|
|
695
|
+
// Under GJS, `process.stdout.write` is a BLOCKING
|
|
696
|
+
// `Gio.write_all`. Writing each prefixed line LIVE during a
|
|
697
|
+
// PARALLEL foreach to a backpressuring pipe (a CI log
|
|
698
|
+
// collector that drains slowly) stalls the single GLib main
|
|
699
|
+
// loop on a full pipe → every parallel child's pipe backs up →
|
|
700
|
+
// their reads stall → the whole run HANGS. (A tty or a file
|
|
701
|
+
// sink never backpressures, which is why it only bites in CI.)
|
|
702
|
+
// On a NON-tty sink we therefore BUFFER each child's prefixed
|
|
703
|
+
// output and flush it as ONE write when that child closes: the
|
|
704
|
+
// child is already done by then, so its own read can't stall,
|
|
705
|
+
// and concurrent flushes serialize into brief loop stalls
|
|
706
|
+
// instead of a deadlock. On a tty (interactive) we keep live
|
|
707
|
+
// line-prefixing for responsive output.
|
|
708
|
+
const buffered = !process.stdout.isTTY;
|
|
709
|
+
if (prefix && child.stdout && child.stderr) {
|
|
710
|
+
flushers.push(prefixLines(child.stdout, process.stdout, prefix, buffered));
|
|
711
|
+
flushers.push(prefixLines(child.stderr, process.stderr, prefix, buffered));
|
|
712
|
+
}
|
|
713
|
+
},
|
|
615
714
|
});
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
// the single GLib main loop on a full pipe → every parallel child's
|
|
621
|
-
// pipe backs up → their reads stall → the whole run HANGS. (A tty or a
|
|
622
|
-
// file sink never backpressures, which is why it only bites in CI.)
|
|
623
|
-
// On a NON-tty sink we therefore BUFFER each child's prefixed output
|
|
624
|
-
// and flush it as ONE write when that child closes: the child is
|
|
625
|
-
// already done by then, so its own read can't stall, and concurrent
|
|
626
|
-
// flushes serialize into brief loop stalls instead of a deadlock. On a
|
|
627
|
-
// tty (interactive) we keep live line-prefixing for responsive output.
|
|
628
|
-
const buffered = !process.stdout.isTTY;
|
|
629
|
-
const flushers = [];
|
|
630
|
-
if (prefix && child.stdout && child.stderr) {
|
|
631
|
-
flushers.push(prefixLines(child.stdout, process.stdout, prefix, buffered));
|
|
632
|
-
flushers.push(prefixLines(child.stderr, process.stderr, prefix, buffered));
|
|
715
|
+
for (const flush of flushers)
|
|
716
|
+
flush();
|
|
717
|
+
if (result.code !== 0) {
|
|
718
|
+
throw new Error(`${cmd} ${args.join(' ')} exited with code ${result.code}`);
|
|
633
719
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
resolve();
|
|
640
|
-
else
|
|
641
|
-
reject(new Error(`${cmd} ${args.join(' ')} exited with code ${code}`));
|
|
642
|
-
});
|
|
643
|
-
child.on('error', (err) => {
|
|
644
|
-
activeChildren.delete(child);
|
|
645
|
-
reject(err);
|
|
646
|
-
});
|
|
647
|
-
});
|
|
720
|
+
}
|
|
721
|
+
finally {
|
|
722
|
+
if (tracked)
|
|
723
|
+
activeChildren.delete(tracked);
|
|
724
|
+
}
|
|
648
725
|
}
|
package/lib/commands/info.js
CHANGED
|
@@ -20,11 +20,17 @@ export const infoCommand = {
|
|
|
20
20
|
const cwd = process.cwd();
|
|
21
21
|
const file = args.file ? resolve(args.file) : null;
|
|
22
22
|
const nativePackages = detectNativePackages(cwd);
|
|
23
|
-
|
|
23
|
+
// Which variables this yields is host-dependent: GI_TYPELIB_PATH
|
|
24
|
+
// everywhere, plus the loader path variable the host actually consults
|
|
25
|
+
// (LD_LIBRARY_PATH on Linux, DYLD_LIBRARY_PATH on macOS, PATH on
|
|
26
|
+
// Windows). Iterate rather than destructuring two fixed names, or the
|
|
27
|
+
// printed environment is wrong off Linux.
|
|
28
|
+
const nativeEnv = buildNativeEnv(nativePackages);
|
|
29
|
+
const envEntries = Object.entries(nativeEnv).filter(([, value]) => value !== undefined);
|
|
24
30
|
if (args.export) {
|
|
25
31
|
// Machine-readable output for eval
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
for (const [key, value] of envEntries)
|
|
33
|
+
console.log(`export ${key}="${value}"`);
|
|
28
34
|
return;
|
|
29
35
|
}
|
|
30
36
|
// Human-readable output
|
|
@@ -39,8 +45,8 @@ export const infoCommand = {
|
|
|
39
45
|
}
|
|
40
46
|
console.log('');
|
|
41
47
|
console.log('To run your app directly with gjs, set:');
|
|
42
|
-
|
|
43
|
-
|
|
48
|
+
for (const [key, value] of envEntries)
|
|
49
|
+
console.log(` export ${key}="${value}"`);
|
|
44
50
|
if (file) {
|
|
45
51
|
console.log(` gjs -m ${file}`);
|
|
46
52
|
}
|
|
@@ -14,4 +14,20 @@ interface InstallOptions {
|
|
|
14
14
|
timeout: number;
|
|
15
15
|
}
|
|
16
16
|
export declare const installCommand: Command<unknown, InstallOptions>;
|
|
17
|
+
export declare function buildBinShim(wsLocation: string, nodeTarget?: string, gjsTarget?: string, nativePrebuildDirs?: string[], platform?: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* `.cmd` / `.ps1` companions for {@link buildBinShim}.
|
|
20
|
+
*
|
|
21
|
+
* The `sh` shim's gjs-first / node-fallback probe has no Windows counterpart to
|
|
22
|
+
* express: there is no GJS host on Windows at all (no prebuilt `libgjs` — see
|
|
23
|
+
* `website/src/content/docs/platform-support.md`), so a workspace that ships
|
|
24
|
+
* both targets resolves to Node here, and a GJS-only workspace gets an honest
|
|
25
|
+
* `gjs`-invoking shim that fails exactly the way the `sh` one does.
|
|
26
|
+
*
|
|
27
|
+
* Exported + platform-free so it is unit-tested from Linux.
|
|
28
|
+
*/
|
|
29
|
+
export declare function buildWorkspaceBinShimsForWindows(wsLocation: string, nodeTarget?: string, gjsTarget?: string, nativePrebuildDirs?: string[]): {
|
|
30
|
+
cmd: string;
|
|
31
|
+
ps1: string;
|
|
32
|
+
};
|
|
17
33
|
export {};
|
package/lib/commands/install.js
CHANGED
|
@@ -30,18 +30,40 @@
|
|
|
30
30
|
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
31
31
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
32
32
|
import { fileURLToPath } from 'node:url';
|
|
33
|
-
import {
|
|
33
|
+
import { spawnSync } from 'node:child_process';
|
|
34
34
|
import { gjsExit } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
35
35
|
import { discoverWorkspaces } from '@gjsify/workspace';
|
|
36
36
|
import { buildInstallCommand, detectPackageManager, runMinimalChecks } from '../utils/check-system-deps.js';
|
|
37
|
-
import { detectNativePackages } from '../utils/detect-native-packages.js';
|
|
37
|
+
import { detectNativePackages, libraryPathVar } from '../utils/detect-native-packages.js';
|
|
38
|
+
import { buildLauncherShims } from '../utils/bin-shim.js';
|
|
38
39
|
import { installPackages, makeProgressReporter } from '../utils/install-backend.js';
|
|
39
40
|
import { atomicWriteStrict } from '../utils/install-cache-fs.js';
|
|
40
41
|
import { acquireInstallLock } from '../utils/install-lock.js';
|
|
41
42
|
import { nodeBinary } from '../utils/run-node.js';
|
|
43
|
+
import { spawnToCompletion } from '../utils/spawn.js';
|
|
42
44
|
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
43
45
|
import { binDirOnPath, defaultGlobalLayout, installGjsEnginePackages, linkGlobalBins, specToPackageName, } from '../utils/install-global.js';
|
|
44
46
|
import { addDependencyEntry, defaultRangeFromVersion, parseSpec, projectSpecsFromPackageJson, readPackageJson, writePackageJson, } from '../utils/pkg-json-edit.js';
|
|
47
|
+
/**
|
|
48
|
+
* Link type for the workspace↔workspace directory links.
|
|
49
|
+
*
|
|
50
|
+
* Windows cannot create a *directory symlink* without elevation or Developer
|
|
51
|
+
* Mode (`EPERM`), but any user can create an NTFS **junction** — which is why
|
|
52
|
+
* npm and yarn both use junctions for workspace links. These two call sites had
|
|
53
|
+
* no try/catch, so a non-elevated Windows workspace install hard-failed here.
|
|
54
|
+
*/
|
|
55
|
+
const WORKSPACE_LINK_TYPE = process.platform === 'win32' ? 'junction' : undefined;
|
|
56
|
+
/**
|
|
57
|
+
* The `target` argument for a workspace link.
|
|
58
|
+
*
|
|
59
|
+
* Node normalises a `'junction'` target with `path.resolve()` — i.e. against
|
|
60
|
+
* the process CWD, NOT the link's own directory — so a junction MUST be given
|
|
61
|
+
* an absolute path. POSIX symlinks stay relative so the tree survives being
|
|
62
|
+
* moved.
|
|
63
|
+
*/
|
|
64
|
+
function workspaceLinkTarget(linkPath, absTarget) {
|
|
65
|
+
return WORKSPACE_LINK_TYPE === 'junction' ? resolve(absTarget) : relative(dirname(linkPath), absTarget);
|
|
66
|
+
}
|
|
45
67
|
// Default 30min wall-clock budget for the full install. Big workspaces
|
|
46
68
|
// (212+ sub-packages × 600+ external deps in the gjsify monorepo itself)
|
|
47
69
|
// can legitimately take 10-20 min on a fresh CI install when the npm CDN
|
|
@@ -183,10 +205,26 @@ export const installCommand = {
|
|
|
183
205
|
const hardExitTimerId = overallTimeoutMs > 0
|
|
184
206
|
? setTimeout(() => {
|
|
185
207
|
const secs = Math.round(overallTimeoutMs / 100) / 10;
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
208
|
+
// Do NOT assert a cause here — this timer cannot tell the
|
|
209
|
+
// two shapes apart, and claiming the rarer one sends the
|
|
210
|
+
// next reader after a bug that isn't there. Measured on a
|
|
211
|
+
// cold tree of this workspace: 1597 packages / ~4.8 GB
|
|
212
|
+
// extracted, of which ~3.4 GB are foreign-platform
|
|
213
|
+
// optional deps (`os`/`cpu` excludes the host) that npm
|
|
214
|
+
// would never place. That install was still making steady
|
|
215
|
+
// progress when the 30-min budget elapsed, and a re-run
|
|
216
|
+
// finished the remainder in ~2 min. Extraction also has
|
|
217
|
+
// multi-second SYNCHRONOUS stretches (tar parse + file
|
|
218
|
+
// writes) and 16 run concurrently, so the clean abort path
|
|
219
|
+
// can easily miss the grace window with nothing wedged.
|
|
220
|
+
console.error(`gjsify install: the ${secs}s budget elapsed and the abort did not unwind within ` +
|
|
221
|
+
`${Math.round(HARD_EXIT_GRACE_MS / 1000)}s — forcing exit. That means one of two ` +
|
|
222
|
+
`things, and this timer cannot distinguish them: the install was still WORKING ` +
|
|
223
|
+
`(a cold tree here extracts several GB, and extraction blocks the loop in ` +
|
|
224
|
+
`multi-second synchronous stretches), or a step genuinely wedged. Re-running tells ` +
|
|
225
|
+
`you which, and is safe: extraction is idempotent, every package already on disk ` +
|
|
226
|
+
`is skipped, so a second run finishes the remainder in a fraction of the time. A ` +
|
|
227
|
+
`re-run that makes NO further progress is a real wedge — file an issue.`);
|
|
190
228
|
forceExit(1);
|
|
191
229
|
}, overallTimeoutMs + HARD_EXIT_GRACE_MS)
|
|
192
230
|
: null;
|
|
@@ -621,7 +659,7 @@ async function workspaceInstallLocked(cwd, args, signal) {
|
|
|
621
659
|
continue;
|
|
622
660
|
const linkPath = join(target.location, 'node_modules', link.depName);
|
|
623
661
|
parentDirs.add(dirname(linkPath));
|
|
624
|
-
const relTarget =
|
|
662
|
+
const relTarget = workspaceLinkTarget(linkPath, link.targetLocation);
|
|
625
663
|
plans.push({ linkPath, relTarget });
|
|
626
664
|
}
|
|
627
665
|
// Phase 1: one mkdir per unique parent (max ~213 instead of ~793).
|
|
@@ -638,13 +676,13 @@ async function workspaceInstallLocked(cwd, args, signal) {
|
|
|
638
676
|
// `{ recursive: true, force: true }` handles every shape (rm
|
|
639
677
|
// no-ops on missing paths under force; recursive covers dirs).
|
|
640
678
|
try {
|
|
641
|
-
await fsp.rm(linkPath, { recursive: true, force: true });
|
|
679
|
+
await fsp.rm(linkPath, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
|
642
680
|
}
|
|
643
681
|
catch {
|
|
644
682
|
/* unexpected; symlink call below will surface the real
|
|
645
683
|
cause if it persists. */
|
|
646
684
|
}
|
|
647
|
-
await fsp.symlink(relTarget, linkPath);
|
|
685
|
+
await fsp.symlink(relTarget, linkPath, WORKSPACE_LINK_TYPE);
|
|
648
686
|
};
|
|
649
687
|
for (let i = 0; i < Math.min(SYMLINK_CONCURRENCY, plans.length); i++) {
|
|
650
688
|
workers.push((async () => {
|
|
@@ -704,8 +742,7 @@ async function workspaceInstallLocked(cwd, args, signal) {
|
|
|
704
742
|
if (existsHere)
|
|
705
743
|
continue;
|
|
706
744
|
mkdirSync(dirname(linkPath), { recursive: true });
|
|
707
|
-
|
|
708
|
-
symlinkSync(relTarget, linkPath);
|
|
745
|
+
symlinkSync(workspaceLinkTarget(linkPath, ws.location), linkPath, WORKSPACE_LINK_TYPE);
|
|
709
746
|
rootHoisted++;
|
|
710
747
|
}
|
|
711
748
|
if (rootHoisted > 0) {
|
|
@@ -912,16 +949,27 @@ function writeWorkspaceBinShims(cwd, workspaces) {
|
|
|
912
949
|
mkdirSync(wsBinDir, { recursive: true });
|
|
913
950
|
for (const [binName, { nodeTarget, gjsTarget }] of merged) {
|
|
914
951
|
const linkPath = join(wsBinDir, binName);
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
952
|
+
for (const file of [linkPath, `${linkPath}.cmd`, `${linkPath}.ps1`]) {
|
|
953
|
+
try {
|
|
954
|
+
rmSync(file, { force: true, maxRetries: 10, retryDelay: 100 });
|
|
955
|
+
}
|
|
956
|
+
catch {
|
|
957
|
+
/* fine */
|
|
958
|
+
}
|
|
920
959
|
}
|
|
921
960
|
writeFileSync(linkPath, buildBinShim(ws.location, nodeTarget, gjsTarget, nativePrebuildDirs), {
|
|
922
961
|
mode: 0o755,
|
|
923
962
|
});
|
|
924
963
|
chmodSync(linkPath, 0o755);
|
|
964
|
+
// Windows executes neither an extension-less file nor a `#!` line —
|
|
965
|
+
// the sh shim above is only reachable from git-bash/MSYS/WSL. Write
|
|
966
|
+
// the `.cmd`/`.ps1` companions cmd.exe and pwsh need, mirroring what
|
|
967
|
+
// npm's cmd-shim lays down next to every linked bin.
|
|
968
|
+
if (process.platform === 'win32') {
|
|
969
|
+
for (const [suffix, contents] of Object.entries(buildWorkspaceBinShimsForWindows(ws.location, nodeTarget, gjsTarget, nativePrebuildDirs))) {
|
|
970
|
+
writeFileSync(`${linkPath}.${suffix}`, contents, { mode: 0o755 });
|
|
971
|
+
}
|
|
972
|
+
}
|
|
925
973
|
wsBinsCreated++;
|
|
926
974
|
}
|
|
927
975
|
}
|
|
@@ -997,19 +1045,25 @@ function scopedOverrideFor(ws, depName, extracted) {
|
|
|
997
1045
|
}
|
|
998
1046
|
return undefined;
|
|
999
1047
|
}
|
|
1000
|
-
function buildBinShim(wsLocation, nodeTarget, gjsTarget, nativePrebuildDirs = []) {
|
|
1048
|
+
export function buildBinShim(wsLocation, nodeTarget, gjsTarget, nativePrebuildDirs = [], platform = process.platform) {
|
|
1001
1049
|
const nodeAbs = nodeTarget ? join(wsLocation, nodeTarget) : null;
|
|
1002
1050
|
const gjsAbs = gjsTarget ? join(wsLocation, gjsTarget) : null;
|
|
1003
1051
|
// GJS-only env preamble — Node ignores GI_TYPELIB_PATH so we scope the
|
|
1004
1052
|
// export to the gjs branch, keeping the shim minimal when no native pkgs
|
|
1005
1053
|
// exist or only the Node bin is in play.
|
|
1054
|
+
//
|
|
1055
|
+
// The library-path variable is host-dependent: `dyld` on macOS never reads
|
|
1056
|
+
// LD_LIBRARY_PATH, so exporting it there silently produced a launcher that
|
|
1057
|
+
// could not load a single `darwin-arm64` prebuild. `libraryPathVar()` is the
|
|
1058
|
+
// one place that mapping lives.
|
|
1059
|
+
const { name: libVar } = libraryPathVar(platform);
|
|
1006
1060
|
const gjsPreamble = nativePrebuildDirs.length === 0
|
|
1007
1061
|
? ''
|
|
1008
1062
|
: (() => {
|
|
1009
1063
|
const joined = `'${nativePrebuildDirs.join(':').replace(/'/g, `'\\''`)}'`;
|
|
1010
1064
|
return (`GI_TYPELIB_PATH=${joined}\${GI_TYPELIB_PATH:+":$GI_TYPELIB_PATH"}\n` +
|
|
1011
|
-
|
|
1012
|
-
`export GI_TYPELIB_PATH
|
|
1065
|
+
`${libVar}=${joined}\${${libVar}:+":$${libVar}"}\n` +
|
|
1066
|
+
`export GI_TYPELIB_PATH ${libVar}\n`);
|
|
1013
1067
|
})();
|
|
1014
1068
|
if (nodeAbs && gjsAbs) {
|
|
1015
1069
|
// GJS-FIRST: prefer the committed Node-free GJS bundle when `gjs` is on
|
|
@@ -1039,6 +1093,37 @@ function buildBinShim(wsLocation, nodeTarget, gjsTarget, nativePrebuildDirs = []
|
|
|
1039
1093
|
return `#!/bin/sh\n${gjsPreamble}exec gjs -m "${gjsAbs}" "$@"\n`;
|
|
1040
1094
|
throw new Error('buildBinShim: either nodeTarget or gjsTarget must be provided');
|
|
1041
1095
|
}
|
|
1096
|
+
/**
|
|
1097
|
+
* `.cmd` / `.ps1` companions for {@link buildBinShim}.
|
|
1098
|
+
*
|
|
1099
|
+
* The `sh` shim's gjs-first / node-fallback probe has no Windows counterpart to
|
|
1100
|
+
* express: there is no GJS host on Windows at all (no prebuilt `libgjs` — see
|
|
1101
|
+
* `website/src/content/docs/platform-support.md`), so a workspace that ships
|
|
1102
|
+
* both targets resolves to Node here, and a GJS-only workspace gets an honest
|
|
1103
|
+
* `gjs`-invoking shim that fails exactly the way the `sh` one does.
|
|
1104
|
+
*
|
|
1105
|
+
* Exported + platform-free so it is unit-tested from Linux.
|
|
1106
|
+
*/
|
|
1107
|
+
export function buildWorkspaceBinShimsForWindows(wsLocation, nodeTarget, gjsTarget, nativePrebuildDirs = []) {
|
|
1108
|
+
const nodeAbs = nodeTarget ? join(wsLocation, nodeTarget) : null;
|
|
1109
|
+
const gjsAbs = gjsTarget ? join(wsLocation, gjsTarget) : null;
|
|
1110
|
+
if (nodeAbs)
|
|
1111
|
+
return buildLauncherShims({ interpreter: 'node', target: nodeAbs });
|
|
1112
|
+
if (gjsAbs) {
|
|
1113
|
+
// Windows has no dedicated library-path variable — LoadLibrary searches
|
|
1114
|
+
// PATH — so the prebuild dirs go there, `;`-separated (which is also
|
|
1115
|
+
// GLib's G_SEARCHPATH_SEPARATOR on Windows, hence the same join for
|
|
1116
|
+
// GI_TYPELIB_PATH).
|
|
1117
|
+
const joined = nativePrebuildDirs.join(';');
|
|
1118
|
+
return buildLauncherShims({
|
|
1119
|
+
interpreter: 'gjs',
|
|
1120
|
+
interpreterArgs: ['-m'],
|
|
1121
|
+
target: gjsAbs,
|
|
1122
|
+
prependEnv: nativePrebuildDirs.length === 0 ? {} : { GI_TYPELIB_PATH: joined, PATH: joined },
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
throw new Error('buildWorkspaceBinShimsForWindows: either nodeTarget or gjsTarget must be provided');
|
|
1126
|
+
}
|
|
1042
1127
|
/**
|
|
1043
1128
|
* Walk a workspace's `bin` (Node) + `gjsify.bin` (GJS) declarations
|
|
1044
1129
|
* into a unified `<binName> → {nodeTarget?, gjsTarget?}` map. The
|
|
@@ -1092,23 +1177,25 @@ async function projectInstallViaNpm(args) {
|
|
|
1092
1177
|
await spawnNpm(npmArgs);
|
|
1093
1178
|
}
|
|
1094
1179
|
async function spawnNpm(npmArgs) {
|
|
1095
|
-
return
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1180
|
+
// `completion: 'return'` — the `--backend npm` branch of the handler
|
|
1181
|
+
// RETURNS after `runPostInstallChecks()` instead of exiting, so under GJS
|
|
1182
|
+
// this must not leave the main loop `spawn()` arms parked (see
|
|
1183
|
+
// utils/spawn.ts). The npm backend needs a Node host anyway, so the
|
|
1184
|
+
// blocking path costs nothing in practice.
|
|
1185
|
+
return spawnToCompletion('npm', npmArgs, {
|
|
1186
|
+
completion: 'return',
|
|
1187
|
+
notFound: () => new Error('npm not found on PATH — install Node.js first.'),
|
|
1188
|
+
})
|
|
1189
|
+
.then(({ code }) => {
|
|
1190
|
+
if (code !== 0)
|
|
1191
|
+
throw new Error(`npm install exited with code ${code}`);
|
|
1192
|
+
})
|
|
1193
|
+
.catch((err) => {
|
|
1194
|
+
// A raw spawn failure still carries an errno `code`; the mapped
|
|
1195
|
+
// not-found hint and the non-zero-exit error do not, and already
|
|
1196
|
+
// read as finished sentences.
|
|
1197
|
+
const errno = err.code;
|
|
1198
|
+
console.error(errno === undefined ? err.message : `npm install failed: ${err.message}`);
|
|
1112
1199
|
process.exit(1);
|
|
1113
1200
|
});
|
|
1114
1201
|
}
|
package/lib/commands/onboard.js
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
import { spawn, spawnSync } from 'node:child_process';
|
|
28
28
|
import { DEFAULT_REGISTRY, whoami } from '@gjsify/npm-registry';
|
|
29
29
|
import { discoverWorkspaces, filterWorkspaces } from '@gjsify/workspace';
|
|
30
|
+
import { mergePublishables } from '../utils/publishable-packages.js';
|
|
30
31
|
import { hasAnyCredential, loadNpmrc } from '../utils/load-npmrc.js';
|
|
31
32
|
import { OtpProvider } from '../utils/npm-otp.js';
|
|
32
33
|
import { publishWorkspace } from './publish.js';
|
|
@@ -142,7 +143,11 @@ export const onboardCommand = {
|
|
|
142
143
|
const environment = args.environment;
|
|
143
144
|
// 2. Enumerate publishable workspaces (non-private, exclude @girs/*).
|
|
144
145
|
const root = findWorkspaceRoot(cwd) ?? cwd;
|
|
145
|
-
|
|
146
|
+
// Non-workspace publishables (packages/{node-gi,napi}/*) publish through
|
|
147
|
+
// release.yml but `discoverWorkspaces` cannot see them — without this
|
|
148
|
+
// merge the sweep reports "all done" while a package that was never
|
|
149
|
+
// published is simply absent from the list.
|
|
150
|
+
const all = mergePublishables(discoverWorkspaces(root, { includeRoot: true }), cwd);
|
|
146
151
|
const selected = filterWorkspaces(all, { noPrivate: true, exclude: ['@girs/*'] }).filter((ws) => ws.name);
|
|
147
152
|
if (selected.length === 0) {
|
|
148
153
|
const msg = 'gjsify onboard: no publishable workspace packages found.';
|
package/lib/commands/pack.js
CHANGED
|
@@ -29,6 +29,7 @@ import { join, resolve } from 'node:path';
|
|
|
29
29
|
import { createTarball, gzip } from '@gjsify/tar';
|
|
30
30
|
import { discoverWorkspaces } from '@gjsify/workspace';
|
|
31
31
|
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
32
|
+
import { readPackageJson } from '../utils/pkg-json.js';
|
|
32
33
|
import { runLifecycleScript } from '../utils/run-lifecycle-script.js';
|
|
33
34
|
export const packCommand = {
|
|
34
35
|
command: 'pack [path]',
|
|
@@ -488,23 +489,87 @@ function globToRegex(glob) {
|
|
|
488
489
|
pat = pat.replace(/\?/g, '[^/]');
|
|
489
490
|
return new RegExp(`^${pat}($|/)`);
|
|
490
491
|
}
|
|
492
|
+
const DEP_BLOCKS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
|
|
493
|
+
/** Every `<block>.<name>` in `pkg` whose range uses the `workspace:` protocol. */
|
|
494
|
+
function collectWorkspaceProtocolDeps(pkg) {
|
|
495
|
+
const found = [];
|
|
496
|
+
for (const block of DEP_BLOCKS) {
|
|
497
|
+
const deps = pkg[block];
|
|
498
|
+
if (!deps)
|
|
499
|
+
continue;
|
|
500
|
+
for (const [name, range] of Object.entries(deps)) {
|
|
501
|
+
if (typeof range === 'string' && range.startsWith('workspace:'))
|
|
502
|
+
found.push(`${block}.${name}`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return found;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Monorepo root to resolve `workspace:` ranges against.
|
|
509
|
+
*
|
|
510
|
+
* `findWorkspaceRoot` is deliberately strict: it only accepts a candidate root
|
|
511
|
+
* whose OWN `workspaces` globs list `start` as a member (so an unrelated
|
|
512
|
+
* grand-parent monorepo can't be picked up). Packing needs a WIDER answer,
|
|
513
|
+
* because some packages live inside the monorepo on purpose WITHOUT being
|
|
514
|
+
* members — `packages/napi/**` and `packages/node-gi/**` are excluded from
|
|
515
|
+
* `package.json#workspaces` (own CI, own release cadence, see AGENTS.md). For
|
|
516
|
+
* those the strict lookup returns `null`, and the pre-fix code took that as
|
|
517
|
+
* "nothing to do" and shipped the literal `workspace:^` string.
|
|
518
|
+
*
|
|
519
|
+
* So: strict answer first, else the nearest ancestor that declares
|
|
520
|
+
* `workspaces` at all — the monorepo the package physically lives in, whose
|
|
521
|
+
* members are exactly the siblings a `workspace:` range can refer to.
|
|
522
|
+
*/
|
|
523
|
+
function findPackWorkspaceRoot(start) {
|
|
524
|
+
const strict = findWorkspaceRoot(start);
|
|
525
|
+
if (strict)
|
|
526
|
+
return strict;
|
|
527
|
+
let dir = start;
|
|
528
|
+
for (let i = 0; i < 12; i++) {
|
|
529
|
+
const pkgPath = join(dir, 'package.json');
|
|
530
|
+
if (existsSync(pkgPath) && readPackageJson(pkgPath)?.workspaces !== undefined)
|
|
531
|
+
return dir;
|
|
532
|
+
const parent = resolve(dir, '..');
|
|
533
|
+
if (parent === dir)
|
|
534
|
+
break;
|
|
535
|
+
dir = parent;
|
|
536
|
+
}
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
491
539
|
/**
|
|
492
540
|
* Walk pkg.dependencies / devDependencies / peerDependencies /
|
|
493
541
|
* optionalDependencies and replace `workspace:^` / `workspace:~` /
|
|
494
542
|
* `workspace:*` ranges with the resolved npm range based on each sibling
|
|
495
543
|
* workspace's version field.
|
|
544
|
+
*
|
|
545
|
+
* A `workspace:` range that survives into a published tarball is unresolvable
|
|
546
|
+
* by EVERY registry client (`npm install` fails with `Unsupported URL Type
|
|
547
|
+
* "workspace:"`), so there is no such thing as a harmless skip here: this
|
|
548
|
+
* either substitutes or throws. Same failure mode, same reasoning as
|
|
549
|
+
* `expandFilesPatterns` above — a packer that silently produces a broken
|
|
550
|
+
* tarball costs a whole release train (v0.4.20, v0.4.37).
|
|
496
551
|
*/
|
|
497
552
|
function rewriteWorkspaceDeps(pkg, wsDir) {
|
|
498
|
-
const root =
|
|
499
|
-
if (!root)
|
|
553
|
+
const root = findPackWorkspaceRoot(wsDir);
|
|
554
|
+
if (!root) {
|
|
555
|
+
// No enclosing monorepo. Fine for a standalone package — but then a
|
|
556
|
+
// `workspace:` range cannot possibly be resolved, so refuse rather
|
|
557
|
+
// than ship it.
|
|
558
|
+
const unresolvable = collectWorkspaceProtocolDeps(pkg);
|
|
559
|
+
if (unresolvable.length > 0) {
|
|
560
|
+
throw new Error(`gjsify pack: ${String(pkg.name)} declares workspace: ranges (${unresolvable.join(', ')}) ` +
|
|
561
|
+
`but ${wsDir} is not inside a monorepo (no ancestor package.json declares "workspaces"). ` +
|
|
562
|
+
`A workspace: range cannot be published — replace it with a real npm range, or pack from within the monorepo.`);
|
|
563
|
+
}
|
|
500
564
|
return pkg;
|
|
565
|
+
}
|
|
501
566
|
const siblings = new Map();
|
|
502
567
|
for (const ws of discoverWorkspaces(root)) {
|
|
503
568
|
if (ws.name && ws.version)
|
|
504
569
|
siblings.set(ws.name, ws.version);
|
|
505
570
|
}
|
|
506
571
|
const cloned = JSON.parse(JSON.stringify(pkg));
|
|
507
|
-
for (const block of
|
|
572
|
+
for (const block of DEP_BLOCKS) {
|
|
508
573
|
const deps = cloned[block];
|
|
509
574
|
if (!deps)
|
|
510
575
|
continue;
|
|
@@ -513,7 +578,7 @@ function rewriteWorkspaceDeps(pkg, wsDir) {
|
|
|
513
578
|
continue;
|
|
514
579
|
const wsVer = siblings.get(name);
|
|
515
580
|
if (!wsVer) {
|
|
516
|
-
throw new Error(`gjsify pack: ${cloned.name} declares
|
|
581
|
+
throw new Error(`gjsify pack: ${cloned.name} declares ${range} on ${name} (${block}) but no sibling workspace with that name exists in the monorepo at ${root}`);
|
|
517
582
|
}
|
|
518
583
|
const operator = range.slice('workspace:'.length);
|
|
519
584
|
if (operator === '*' || operator === '') {
|