@gjsify/cli 0.26.0 → 0.27.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 +184 -182
- package/lib/cli-app.js +3 -1
- package/lib/commands/affected.js +146 -15
- package/lib/commands/build.js +21 -14
- package/lib/commands/check.js +40 -4
- package/lib/commands/clear.d.ts +8 -0
- package/lib/commands/clear.js +90 -0
- package/lib/commands/copy.d.ts +8 -0
- package/lib/commands/copy.js +87 -0
- package/lib/commands/dlx.js +8 -1
- package/lib/commands/index.d.ts +2 -0
- package/lib/commands/index.js +2 -0
- package/lib/commands/install.d.ts +4 -0
- package/lib/commands/install.js +164 -15
- package/lib/commands/onboard.js +17 -14
- package/lib/commands/publish.js +18 -4
- package/lib/commands/storybook.js +5 -1
- package/lib/commands/test.js +29 -2
- package/lib/commands/uninstall.js +44 -2
- package/lib/commands/upgrade.js +1 -4
- package/lib/templates/install.mjs.tmpl +110 -19
- package/lib/utils/bin-shim.d.ts +73 -0
- package/lib/utils/bin-shim.js +97 -2
- package/lib/utils/build-cache.d.ts +7 -27
- package/lib/utils/build-cache.js +34 -4
- package/lib/utils/check-system-deps.d.ts +15 -1
- package/lib/utils/check-system-deps.js +126 -2
- package/lib/utils/clear-targets.d.ts +30 -0
- package/lib/utils/clear-targets.js +114 -0
- package/lib/utils/copy-targets.d.ts +41 -0
- package/lib/utils/copy-targets.js +145 -0
- package/lib/utils/detect-native-packages.d.ts +160 -5
- package/lib/utils/detect-native-packages.js +326 -50
- package/lib/utils/dir-link.d.ts +86 -0
- package/lib/utils/dir-link.js +140 -0
- package/lib/utils/dlx-cache.d.ts +30 -3
- package/lib/utils/dlx-cache.js +61 -7
- package/lib/utils/install-backend-native.d.ts +109 -1
- package/lib/utils/install-backend-native.js +622 -59
- package/lib/utils/install-backend.js +17 -15
- package/lib/utils/install-global.js +6 -34
- package/lib/utils/install-lock.js +1 -1
- package/lib/utils/install-packument-cache.d.ts +51 -9
- package/lib/utils/install-packument-cache.js +88 -25
- package/lib/utils/oxc-resolve.d.ts +27 -1
- package/lib/utils/oxc-resolve.js +53 -26
- package/lib/utils/pkg-json.d.ts +24 -0
- package/lib/utils/pkg-json.js +27 -1
- package/lib/utils/platform-check.d.ts +215 -0
- package/lib/utils/platform-check.js +377 -0
- package/lib/utils/resolve-npm-package.js +9 -2
- package/lib/utils/run-gjs.d.ts +1 -1
- package/lib/utils/run-gjs.js +19 -3
- package/lib/utils/spawn.js +50 -2
- package/lib/utils/win32-command.d.ts +54 -0
- package/lib/utils/win32-command.js +140 -0
- package/package.json +19 -20
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// `gjsify copy <sources…> <dest>` — portable file staging for package scripts.
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS EXISTS
|
|
4
|
+
//
|
|
5
|
+
// The sibling of `gjsify clear`, and the second half of the same finding. npm
|
|
6
|
+
// runs package scripts through `cmd.exe` on Windows, which has no `cp`, no
|
|
7
|
+
// `mkdir -p` and expands no glob. `clear` closed the 224 `rm -rf` scripts
|
|
8
|
+
// (issue #914); what remained were 34 scripts that stage static assets with
|
|
9
|
+
// `mkdir -p X && cp -r …` — `build:assets` (24), `build:public` (5), and one
|
|
10
|
+
// each of `build:views`, `sync:theme`, `prebuild:test`, `prebuild:test:fixtures`.
|
|
11
|
+
//
|
|
12
|
+
// They were not merely a hygiene item. `showcases/node/express-webserver` is the
|
|
13
|
+
// one showcase that runs on Windows at all — its `--app node` bundle needs no
|
|
14
|
+
// gjs — and `build:public` is what puts `index.html` and `style.css` next to it.
|
|
15
|
+
// Without this command the showcase builds and then serves 404s for its own
|
|
16
|
+
// frontend, which is a worse failure than not building: the server starts.
|
|
17
|
+
//
|
|
18
|
+
// WHAT IT IS NOT
|
|
19
|
+
//
|
|
20
|
+
// Not a `cp` clone. Two deliberate differences, both because a build step is
|
|
21
|
+
// re-run:
|
|
22
|
+
//
|
|
23
|
+
// - `mkdir -p` is implied. Every one of the replaced scripts paired the two,
|
|
24
|
+
// and a copy that fails because the output directory does not exist yet is
|
|
25
|
+
// not a distinction worth preserving.
|
|
26
|
+
// - directory-vs-path is decided by the ARGUMENTS, never by what is on disk —
|
|
27
|
+
// see `isDirectoryDestination`. `cp -r src/assets dist/res` means two
|
|
28
|
+
// different things on the first and second run; this does not.
|
|
29
|
+
//
|
|
30
|
+
// Overwriting is unconditional (`force: true`), which is what the `-f` in most
|
|
31
|
+
// of the replaced scripts asked for and what the rest wanted without saying so.
|
|
32
|
+
import { cp, mkdir } from 'node:fs/promises';
|
|
33
|
+
import { readdirSync } from 'node:fs';
|
|
34
|
+
import { dirname, relative } from 'node:path';
|
|
35
|
+
import { planCopy } from '../utils/copy-targets.js';
|
|
36
|
+
export const copyCommand = {
|
|
37
|
+
command: 'copy [paths..]',
|
|
38
|
+
description: 'Copy files and directories into the build output. Portable replacement for `mkdir -p` + `cp -r` in package scripts.',
|
|
39
|
+
// No `normalize: true` — see the comment block in `commands/build.ts`. It is
|
|
40
|
+
// meaningless for a path resolved here anyway, and on win32 it is what would
|
|
41
|
+
// backslash a wildcard segment before it can be matched.
|
|
42
|
+
builder: (yargs) => {
|
|
43
|
+
return yargs
|
|
44
|
+
.positional('paths', {
|
|
45
|
+
description: 'One or more sources followed by the destination. The destination is a DIRECTORY when it ends with "/", when several sources are given, or when a source carries a wildcard; otherwise it is the exact target path. Missing parent directories are created. `*` and `?` are honoured in the last segment of a source.',
|
|
46
|
+
type: 'string',
|
|
47
|
+
array: true,
|
|
48
|
+
})
|
|
49
|
+
.option('dry-run', {
|
|
50
|
+
description: 'Print what would be copied and exit without touching anything.',
|
|
51
|
+
type: 'boolean',
|
|
52
|
+
default: false,
|
|
53
|
+
})
|
|
54
|
+
.option('verbose', {
|
|
55
|
+
alias: 'v',
|
|
56
|
+
description: 'Print each path as it is copied.',
|
|
57
|
+
type: 'boolean',
|
|
58
|
+
default: false,
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
handler: async (args) => {
|
|
62
|
+
const cwd = process.cwd();
|
|
63
|
+
const paths = args.paths ?? [];
|
|
64
|
+
if (paths.length < 2) {
|
|
65
|
+
throw new Error('gjsify copy: needs at least one source and a destination. Usage: gjsify copy <sources…> <dest>');
|
|
66
|
+
}
|
|
67
|
+
// Planning validates every argument — and every destination against the
|
|
68
|
+
// package boundary — before a single byte is written.
|
|
69
|
+
const ops = planCopy(paths, { cwd, readdir: readdirSync });
|
|
70
|
+
const show = (p) => {
|
|
71
|
+
const rel = relative(cwd, p);
|
|
72
|
+
// A source may legitimately live in a sibling package; showing it as
|
|
73
|
+
// `../../../packages/…` is more useful than an absolute path there.
|
|
74
|
+
return rel === '' ? '.' : rel;
|
|
75
|
+
};
|
|
76
|
+
for (const { from, to } of ops) {
|
|
77
|
+
if (args.dryRun) {
|
|
78
|
+
console.log(`gjsify copy: would copy ${show(from)} -> ${show(to)}`);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
await mkdir(dirname(to), { recursive: true });
|
|
82
|
+
await cp(from, to, { recursive: true, force: true });
|
|
83
|
+
if (args.verbose)
|
|
84
|
+
console.log(`gjsify copy: ${show(from)} -> ${show(to)}`);
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
};
|
package/lib/commands/dlx.js
CHANGED
|
@@ -89,7 +89,14 @@ export const dlxCommand = {
|
|
|
89
89
|
const { binName, extraArgs } = splitBinAndArgs(pkgDir, args.binOrArg, extraArgsCombined);
|
|
90
90
|
const entry = resolveGjsEntry(pkgDir, binName);
|
|
91
91
|
if (entry.fromFallback) {
|
|
92
|
-
|
|
92
|
+
// Name the key that is actually missing. The condition is
|
|
93
|
+
// `resolveGjsEntry` finding no `gjsify.main` (or no matching
|
|
94
|
+
// `gjsify.bin`), but the text said "no `gjsify` field" — and two
|
|
95
|
+
// showcases DO carry a `gjsify` object (tier + example) without a
|
|
96
|
+
// `main`, so the warning sent the reader looking for a field that
|
|
97
|
+
// was right there. Measured running `gjsify showcase
|
|
98
|
+
// webrtc-loopback` on a Node-less device.
|
|
99
|
+
console.warn(`[gjsify dlx] package "${cachedPkgName ?? parsed.kind}" declares no \`gjsify.main\` — falling back to package.json#main. Add \`gjsify.main\` to silence.`);
|
|
93
100
|
}
|
|
94
101
|
// Terminal call — exit on success, or the GJS main loop parks this
|
|
95
102
|
// process forever (see RunGjsBundleOptions.exitOnSuccess).
|
package/lib/commands/index.d.ts
CHANGED
package/lib/commands/index.js
CHANGED
|
@@ -12,6 +12,10 @@ interface InstallOptions {
|
|
|
12
12
|
progress?: boolean;
|
|
13
13
|
backend?: 'native' | 'npm';
|
|
14
14
|
timeout: number;
|
|
15
|
+
os?: string;
|
|
16
|
+
cpu?: string;
|
|
17
|
+
libc?: string;
|
|
18
|
+
force?: boolean;
|
|
15
19
|
}
|
|
16
20
|
export declare const installCommand: Command<unknown, InstallOptions>;
|
|
17
21
|
export declare function buildBinShim(wsLocation: string, nodeTarget?: string, gjsTarget?: string, nativePrebuildDirs?: string[], platform?: string, scanRoot?: string): string;
|
package/lib/commands/install.js
CHANGED
|
@@ -124,8 +124,29 @@ export const installCommand = {
|
|
|
124
124
|
description: 'Overall install wall-clock timeout in ms (default 1800000 = 30 min). On timeout, all in-flight registry fetches are aborted and the install exits non-zero with a clear "install timed out — likely a registry slowdown" message. Per-request timeouts in @gjsify/npm-registry (default 30s) still apply within this budget. Set to 0 to disable the overall budget.',
|
|
125
125
|
type: 'number',
|
|
126
126
|
default: DEFAULT_INSTALL_TIMEOUT_MS,
|
|
127
|
+
})
|
|
128
|
+
.option('os', {
|
|
129
|
+
description: 'Resolve/install for this OS instead of the running one (npm config key `os`, e.g. darwin, win32, linux). Affects which packages pass the os/cpu/libc compatibility check; the lockfile stays platform-independent either way.',
|
|
130
|
+
type: 'string',
|
|
131
|
+
})
|
|
132
|
+
.option('cpu', {
|
|
133
|
+
description: 'Resolve/install for this CPU architecture instead of the running one (npm config key `cpu`, e.g. x64, arm64).',
|
|
134
|
+
type: 'string',
|
|
135
|
+
})
|
|
136
|
+
.option('libc', {
|
|
137
|
+
description: 'Resolve/install for this libc family instead of probing the host (npm config key `libc`: glibc or musl). Only meaningful with --os=linux. A package declaring a libc is incompatible when the target libc is unknown.',
|
|
138
|
+
type: 'string',
|
|
139
|
+
})
|
|
140
|
+
.option('force', {
|
|
141
|
+
description: 'Install a REQUIRED dependency even when its os/cpu/libc excludes the target, instead of failing with EBADPLATFORM (npm config key `force`). Incompatible OPTIONAL dependencies stay skipped — npm ignores --force for those too, and a binary that cannot load is not worth downloading.',
|
|
142
|
+
type: 'boolean',
|
|
143
|
+
default: false,
|
|
127
144
|
}),
|
|
128
145
|
handler: async (args) => {
|
|
146
|
+
// Publish `--os/--cpu/--libc/--force` as npm CONFIG KEYS before anything
|
|
147
|
+
// resolves — see applyPlatformConfigFromFlags for why the environment is
|
|
148
|
+
// the right channel for them rather than a parameter.
|
|
149
|
+
applyPlatformConfigFromFlags(args);
|
|
129
150
|
// --immutable is incompatible with explicit `<pkg>` adds and with
|
|
130
151
|
// `--global` (which has no lockfile concept). Matches yarn's
|
|
131
152
|
// behavior: `yarn add --immutable` is a hard error.
|
|
@@ -213,12 +234,15 @@ export const installCommand = {
|
|
|
213
234
|
// Do NOT assert a cause here — this timer cannot tell the
|
|
214
235
|
// two shapes apart, and claiming the rarer one sends the
|
|
215
236
|
// next reader after a bug that isn't there. Measured on a
|
|
216
|
-
// cold tree of this workspace
|
|
217
|
-
// extracted, of which ~3.4 GB
|
|
218
|
-
// optional deps (`os`/`cpu`
|
|
219
|
-
// would never place.
|
|
220
|
-
//
|
|
221
|
-
//
|
|
237
|
+
// cold tree of this workspace WHEN THIS TIMER WAS WRITTEN:
|
|
238
|
+
// 1597 packages / ~4.8 GB extracted, of which ~3.4 GB were
|
|
239
|
+
// foreign-platform optional deps (`os`/`cpu` excluding the
|
|
240
|
+
// host) that npm would never place. Those are no longer
|
|
241
|
+
// downloaded — the resolver now records `os`/`cpu`/`libc`
|
|
242
|
+
// and marks incompatible optional nodes inert
|
|
243
|
+
// (`utils/platform-check.ts` + `applyPlatformFilter`), which
|
|
244
|
+
// leaves ~1.4 GB of that same tree to extract. The budget
|
|
245
|
+
// still exists for the same reason: extraction has
|
|
222
246
|
// multi-second SYNCHRONOUS stretches (tar parse + file
|
|
223
247
|
// writes) and 16 run concurrently, so the clean abort path
|
|
224
248
|
// can easily miss the grace window with nothing wedged.
|
|
@@ -261,6 +285,43 @@ export const installCommand = {
|
|
|
261
285
|
}
|
|
262
286
|
},
|
|
263
287
|
};
|
|
288
|
+
/**
|
|
289
|
+
* Hand the platform target + force bypass to the install backend.
|
|
290
|
+
*
|
|
291
|
+
* `os`, `cpu`, `libc` and `force` are npm CONFIG KEYS, and npm's own transport
|
|
292
|
+
* for a config key is the environment: `--libc=musl` and `npm_config_libc=musl`
|
|
293
|
+
* are the same input, which is why supporting the flags gives users the env
|
|
294
|
+
* spelling for free. That is also why this is not threaded through as a
|
|
295
|
+
* parameter: the config channel is already what this installer reads
|
|
296
|
+
* (`loadNpmrc` honours `npm_config_registry` exactly so), one write reaches
|
|
297
|
+
* EVERY prefix an invocation may install into (project, workspace root, `-g`,
|
|
298
|
+
* the `dlx` cache) instead of one call site each, and a child process inherits
|
|
299
|
+
* it — so a nested install can't quietly resolve for a different machine than
|
|
300
|
+
* the run that spawned it.
|
|
301
|
+
*
|
|
302
|
+
* Empty/whitespace values are ignored rather than written as the empty string:
|
|
303
|
+
* `--os=` must not mean "the nameless platform" (`readPlatformOverrides` drops
|
|
304
|
+
* them on the read side too, so the two ends agree).
|
|
305
|
+
*/
|
|
306
|
+
function applyPlatformConfigFromFlags(args) {
|
|
307
|
+
const keys = [
|
|
308
|
+
['os', 'npm_config_os'],
|
|
309
|
+
['cpu', 'npm_config_cpu'],
|
|
310
|
+
['libc', 'npm_config_libc'],
|
|
311
|
+
];
|
|
312
|
+
for (const [flag, envKey] of keys) {
|
|
313
|
+
const value = args[flag];
|
|
314
|
+
if (typeof value !== 'string')
|
|
315
|
+
continue;
|
|
316
|
+
const trimmed = value.trim();
|
|
317
|
+
if (trimmed !== '')
|
|
318
|
+
process.env[envKey] = trimmed;
|
|
319
|
+
}
|
|
320
|
+
// Only ever SET: a `false` flag is the default, and clearing the key would
|
|
321
|
+
// override an `npm_config_force` the user deliberately exported.
|
|
322
|
+
if (args.force)
|
|
323
|
+
process.env.npm_config_force = 'true';
|
|
324
|
+
}
|
|
264
325
|
/**
|
|
265
326
|
* Heuristic: was this error raised because the overall-install AbortSignal
|
|
266
327
|
* fired? The signal's `reason` is the sentinel `Error('install-overall-timeout')`
|
|
@@ -376,6 +437,36 @@ function isWorkspaceRoot(cwd) {
|
|
|
376
437
|
return false;
|
|
377
438
|
return pkg.workspaces !== undefined;
|
|
378
439
|
}
|
|
440
|
+
/**
|
|
441
|
+
* Which dependency NAMES are optional across the given manifests.
|
|
442
|
+
*
|
|
443
|
+
* The specs handed to the install backend are flat `"<name>@<range>"` strings —
|
|
444
|
+
* `projectSpecsFromPackageJson` merges the three dependency blocks — so the kind
|
|
445
|
+
* has to travel beside them. It matters for exactly one decision: an
|
|
446
|
+
* incompatible `os`/`cpu`/`libc` skips an OPTIONAL dependency and fails a
|
|
447
|
+
* REQUIRED one (see `NativeInstallOptions.optionalSpecs`).
|
|
448
|
+
*
|
|
449
|
+
* REQUIRED WINS. In a workspace, the same package can be a plain dependency of
|
|
450
|
+
* one member and an optionalDependency of another; if anything is not allowed to
|
|
451
|
+
* be missing, the whole install is not allowed to silently miss it. Subtracting
|
|
452
|
+
* rather than "last block seen" also makes the answer independent of manifest
|
|
453
|
+
* iteration order.
|
|
454
|
+
*/
|
|
455
|
+
function optionalDependencyNames(manifests) {
|
|
456
|
+
const optional = new Set();
|
|
457
|
+
const required = new Set();
|
|
458
|
+
for (const manifest of manifests) {
|
|
459
|
+
for (const name of Object.keys(manifest.optionalDependencies ?? {}))
|
|
460
|
+
optional.add(name);
|
|
461
|
+
for (const kind of ['dependencies', 'devDependencies']) {
|
|
462
|
+
for (const name of Object.keys(manifest[kind] ?? {}))
|
|
463
|
+
required.add(name);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
for (const name of required)
|
|
467
|
+
optional.delete(name);
|
|
468
|
+
return optional;
|
|
469
|
+
}
|
|
379
470
|
function depKindFromArgs(args) {
|
|
380
471
|
if (args['save-dev'])
|
|
381
472
|
return 'devDependencies';
|
|
@@ -469,7 +560,11 @@ async function projectInstallNative(args, signal) {
|
|
|
469
560
|
// with ours. `installPackages` re-acquires the same lock re-entrantly.
|
|
470
561
|
const lock = await acquireInstallLock(cwd, { signal });
|
|
471
562
|
try {
|
|
472
|
-
|
|
563
|
+
// Typed as NativeInstallOptions so the native-backend-only
|
|
564
|
+
// `optionalSpecs` is CHECKED at this call site instead of being smuggled
|
|
565
|
+
// past an object-literal excess-property check; `installPackages`
|
|
566
|
+
// forwards the object to that backend unchanged.
|
|
567
|
+
const nativeOpts = {
|
|
473
568
|
prefix: cwd,
|
|
474
569
|
specs,
|
|
475
570
|
verbose: args.verbose,
|
|
@@ -480,7 +575,15 @@ async function projectInstallNative(args, signal) {
|
|
|
480
575
|
refreshLockfile: args['refresh-lockfile'],
|
|
481
576
|
signal,
|
|
482
577
|
progress,
|
|
483
|
-
|
|
578
|
+
// `gjsify install -O <pkg>` saves to optionalDependencies, but the
|
|
579
|
+
// manifest is only written AFTER this install — so the flag, not the
|
|
580
|
+
// manifest, is what makes this run treat the added specs as optional.
|
|
581
|
+
optionalSpecs: new Set([
|
|
582
|
+
...optionalDependencyNames(pkg ? [pkg] : []),
|
|
583
|
+
...(args['save-optional'] ? (args.packages ?? []).map((s) => parseSpec(s).name) : []),
|
|
584
|
+
]),
|
|
585
|
+
};
|
|
586
|
+
const result = await installPackages(nativeOpts);
|
|
484
587
|
// Update package.json only when the user passed explicit packages
|
|
485
588
|
// (the `gjsify install <pkg>...` add-a-dep flow). The no-args refresh
|
|
486
589
|
// flow doesn't mutate manifest entries.
|
|
@@ -844,7 +947,7 @@ async function workspaceInstallLocked(cwd, args, signal) {
|
|
|
844
947
|
const progress = makeProgressReporter({
|
|
845
948
|
enabled: !args.verbose && !args.quiet && args.progress !== false,
|
|
846
949
|
});
|
|
847
|
-
|
|
950
|
+
const rootOpts = {
|
|
848
951
|
prefix: cwd,
|
|
849
952
|
specs: [...externalSpecs],
|
|
850
953
|
verbose: args.verbose,
|
|
@@ -860,7 +963,12 @@ async function workspaceInstallLocked(cwd, args, signal) {
|
|
|
860
963
|
// npm and is pinned transitively in the lockfile).
|
|
861
964
|
workspaceNames: new Set(byName.keys()),
|
|
862
965
|
specOrigins: new Map([...specOrigins].map(([k, v]) => [k, [...v]])),
|
|
863
|
-
|
|
966
|
+
// Aggregated over EVERY member: the specs are aggregated the same
|
|
967
|
+
// way, so the optional-vs-required answer has to be too (a name that
|
|
968
|
+
// any member declares as a plain dependency stays required).
|
|
969
|
+
optionalSpecs: optionalDependencyNames(workspaces.map((w) => w.manifest)),
|
|
970
|
+
};
|
|
971
|
+
await installPackages(rootOpts);
|
|
864
972
|
}
|
|
865
973
|
else if (args.verbose) {
|
|
866
974
|
console.log('gjsify install: no external deps to fetch');
|
|
@@ -876,7 +984,7 @@ async function workspaceInstallLocked(cwd, args, signal) {
|
|
|
876
984
|
if (args.verbose) {
|
|
877
985
|
console.log(`gjsify install: ${wsName} — installing ${specSet.size} scoped-override spec(s) into ${wsLocation}/node_modules/`);
|
|
878
986
|
}
|
|
879
|
-
|
|
987
|
+
const scopedOpts = {
|
|
880
988
|
prefix: wsLocation,
|
|
881
989
|
specs: [...specSet],
|
|
882
990
|
verbose: args.verbose,
|
|
@@ -886,7 +994,12 @@ async function workspaceInstallLocked(cwd, args, signal) {
|
|
|
886
994
|
frozen: args.immutable,
|
|
887
995
|
signal,
|
|
888
996
|
workspaceNames: new Set(byName.keys()),
|
|
889
|
-
|
|
997
|
+
// Same aggregated answer as the root install: these specs are a
|
|
998
|
+
// SUBSET of the root's, so classifying them differently here would
|
|
999
|
+
// make one dep optional at the root and required in a member.
|
|
1000
|
+
optionalSpecs: optionalDependencyNames(workspaces.map((w) => w.manifest)),
|
|
1001
|
+
};
|
|
1002
|
+
await installPackages(scopedOpts);
|
|
890
1003
|
}
|
|
891
1004
|
// Re-wire workspace symlinks once more now that the external + scoped
|
|
892
1005
|
// installs are done. The EARLY pass above already created them so workspace
|
|
@@ -1080,11 +1193,47 @@ export function buildBinShim(wsLocation, nodeTarget, gjsTarget, nativePrebuildDi
|
|
|
1080
1193
|
// package and oversubscribes CI's few cores. Verified: the faithful
|
|
1081
1194
|
// `foreach build -tp --jobs 4` over @gjsify/* with a piped stdout
|
|
1082
1195
|
// completes in ~83s under GJS (was a multi-hour hang).
|
|
1083
|
-
|
|
1084
|
-
|
|
1196
|
+
// …EXCEPT on macOS and Windows, where the order is inverted — and not
|
|
1197
|
+
// as a preference but as a capability. GJS is a GNOME-desktop host: on
|
|
1198
|
+
// those two platforms it is at best a Homebrew/MSYS afterthought, while
|
|
1199
|
+
// Node is what a developer already has. The tooling agrees:
|
|
1200
|
+
//
|
|
1201
|
+
// - macOS: the only bundler engine under GJS is
|
|
1202
|
+
// `@gjsify/rolldown-native` (npm `rolldown` is a Rust napi crate
|
|
1203
|
+
// GJS cannot load), and it declares `linux-x64`/`linux-arm64` only.
|
|
1204
|
+
// So the gjs branch cannot run `gjsify build`, nor any script that
|
|
1205
|
+
// ends in one — it dies with "no usable bundler engine under GJS",
|
|
1206
|
+
// whose own closing line is "Running the build under Node also
|
|
1207
|
+
// works". Preferring gjs routed the user into a dead end the error
|
|
1208
|
+
// message tells them to leave while the shim offered no way out.
|
|
1209
|
+
// Measured on a cold macOS tree: root `build:infra` fails at the
|
|
1210
|
+
// first `gjsify build --library` (`@gjsify/utils`) with the
|
|
1211
|
+
// gjs-first shim and completes with this one.
|
|
1212
|
+
// - Windows: there is no prebuilt `libgjs` at all (see
|
|
1213
|
+
// `website/src/content/docs/platform-support.md`), so the gjs probe
|
|
1214
|
+
// can never succeed. This also makes the `sh` shim — the git-bash /
|
|
1215
|
+
// MSYS entry point — agree with the `.cmd`/`.ps1` companions
|
|
1216
|
+
// {@link buildLauncherShims} already writes as Node-only, instead
|
|
1217
|
+
// of leading with a branch that is dead by construction.
|
|
1218
|
+
//
|
|
1219
|
+
// The probe stays symmetric with the gjs one — `command -v node`, not a
|
|
1220
|
+
// bare file test — so a host carrying the bundle but no Node still
|
|
1221
|
+
// falls through to gjs and keeps every non-build command working. That
|
|
1222
|
+
// is what keeps GJS on these platforms supported-and-tested rather than
|
|
1223
|
+
// merely absent. Revert a platform to the shared gjs-first form once it
|
|
1224
|
+
// has a working engine (macOS: status/open-todos.md,
|
|
1225
|
+
// "@gjsify/rolldown-native's darwin-arm64 leg is PROVEN but not
|
|
1226
|
+
// promoted").
|
|
1227
|
+
const nodeFirstPlatform = platform === 'darwin' || platform === 'win32';
|
|
1228
|
+
const gjsFirst = `if command -v gjs >/dev/null 2>&1 && [ -f "${gjsAbs}" ]; then\n` +
|
|
1085
1229
|
`${gjsPreamble}exec gjs -m "${gjsAbs}" "$@"\n` +
|
|
1086
1230
|
`fi\n` +
|
|
1087
|
-
`exec node "${nodeAbs}" "$@"\n
|
|
1231
|
+
`exec node "${nodeAbs}" "$@"\n`;
|
|
1232
|
+
const nodeFirst = `if command -v node >/dev/null 2>&1 && [ -f "${nodeAbs}" ]; then\n` +
|
|
1233
|
+
`exec node "${nodeAbs}" "$@"\n` +
|
|
1234
|
+
`fi\n` +
|
|
1235
|
+
`${gjsPreamble}exec gjs -m "${gjsAbs}" "$@"\n`;
|
|
1236
|
+
return `#!/bin/sh\n` + (nodeFirstPlatform ? nodeFirst : gjsFirst);
|
|
1088
1237
|
}
|
|
1089
1238
|
if (nodeAbs)
|
|
1090
1239
|
return `#!/bin/sh\nexec node "${nodeAbs}" "$@"\n`;
|
package/lib/commands/onboard.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
//
|
|
25
25
|
// Re-running when everything is already published + trusted does nothing and
|
|
26
26
|
// exits 0. `--dry-run` reports the plan and changes nothing.
|
|
27
|
-
import {
|
|
27
|
+
import { spawnSync } from 'node:child_process';
|
|
28
28
|
import { DEFAULT_REGISTRY, whoami } from '@gjsify/npm-registry';
|
|
29
29
|
import { discoverWorkspaces, filterWorkspaces } from '@gjsify/workspace';
|
|
30
30
|
import { mergePublishables } from '../utils/publishable-packages.js';
|
|
@@ -34,6 +34,7 @@ import { publishWorkspace } from './publish.js';
|
|
|
34
34
|
import { createTrustRequester } from './trust.js';
|
|
35
35
|
import { runLogin, LoginError } from './login.js';
|
|
36
36
|
import { detectPackageManager } from './workspace.js';
|
|
37
|
+
import { spawnToCompletion } from '../utils/spawn.js';
|
|
37
38
|
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
38
39
|
import { githubTrustBody, normalizeWorkflowFile, parseRepoFromGitRemote, validateRepository, } from '../utils/trust-registry.js';
|
|
39
40
|
import { DEFAULT_PROBE_CONCURRENCY, probeAllTrustStates } from '../utils/onboard-probe.js';
|
|
@@ -379,20 +380,22 @@ async function buildIfPresent(ws, log) {
|
|
|
379
380
|
const pm = detectPackageManager();
|
|
380
381
|
const argv = ['run', 'build'];
|
|
381
382
|
log(` $ ${pm} ${argv.join(' ')}`);
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
383
|
+
// Through `spawnToCompletion`, which resolves the package manager the way
|
|
384
|
+
// the rest of the CLI does. `detectPackageManager()` returns `npm` on every
|
|
385
|
+
// non-GJS host, and a bare `spawn('npm', …)` is ENOENT on Windows — npm is
|
|
386
|
+
// a `.cmd` shim that `CreateProcess` never finds (see
|
|
387
|
+
// utils/win32-command.ts). This is the pre-publish build, so the break
|
|
388
|
+
// landed between "about to publish" and the publish itself.
|
|
389
|
+
//
|
|
390
|
+
// `completion: 'return'` — this resolves to a caller that goes on to
|
|
391
|
+
// publish, so under GJS it must not leave a main loop armed.
|
|
392
|
+
const { code } = await spawnToCompletion(pm, argv, {
|
|
393
|
+
completion: 'return',
|
|
394
|
+
cwd: ws.location,
|
|
395
|
+
env: { ...process.env },
|
|
395
396
|
});
|
|
397
|
+
if (code !== 0)
|
|
398
|
+
throw new Error(`build exited with code ${code}`);
|
|
396
399
|
}
|
|
397
400
|
/** Human-readable reason for a failed publish outcome. */
|
|
398
401
|
function describePublishFailure(pub) {
|
package/lib/commands/publish.js
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
// and npm's @npmcli/registry-fetch internals — verified against npm's
|
|
35
35
|
// in-the-wild publish payloads.
|
|
36
36
|
import { readFileSync } from 'node:fs';
|
|
37
|
+
import { tmpdir } from 'node:os';
|
|
37
38
|
import { join, resolve } from 'node:path';
|
|
38
39
|
import { DEFAULT_REGISTRY, registryFor } from '@gjsify/npm-registry';
|
|
39
40
|
import { buildPublishHeaders, escapePackageName } from '../utils/publish-headers.js';
|
|
@@ -477,7 +478,12 @@ async function packWorkspaceToBytes(wsDir) {
|
|
|
477
478
|
// scripts are already run by the outer publish flow's first pack
|
|
478
479
|
// call — passing `[]` here skips re-running them (idempotent for
|
|
479
480
|
// most projects but a needless cost otherwise).
|
|
480
|
-
|
|
481
|
+
// `tmpdir()`, not a literal `/tmp`. `pack` resolves its destination and
|
|
482
|
+
// creates it recursively, so on Windows the literal produced `C:\tmp\…` at
|
|
483
|
+
// the DRIVE ROOT — created on every publish, and only the inner directory
|
|
484
|
+
// was removed afterwards, so `C:\tmp` was left behind for good. On an image
|
|
485
|
+
// where the drive root is not writable it failed outright.
|
|
486
|
+
const tmp = join(tmpdir(), `gjsify-publish-${process.pid}-${Date.now()}`);
|
|
481
487
|
const res = await packWorkspace(wsDir, {
|
|
482
488
|
destination: tmp,
|
|
483
489
|
dryRun: false,
|
|
@@ -504,9 +510,14 @@ async function loadRewrittenManifest(wsDir, pkg) {
|
|
|
504
510
|
// Pack + re-read the tarball's package.json. Easier than duplicating the
|
|
505
511
|
// rewrite logic — pack already does it correctly, including handling
|
|
506
512
|
// workspace:^ patterns we'd otherwise have to reimplement here.
|
|
507
|
-
|
|
513
|
+
// Only the DIRECTORY is passed to pack, so build it directly instead of
|
|
514
|
+
// composing a filename and slicing it back off at the last `/` — which on
|
|
515
|
+
// Windows found no separator in `C:\Users\…\gjsify-publish-manifest-…` and
|
|
516
|
+
// handed pack a truncated path. Same `/tmp`-at-the-drive-root problem as
|
|
517
|
+
// above on top of it.
|
|
518
|
+
const dest = join(tmpdir(), `gjsify-publish-manifest-${process.pid}-${Date.now()}`);
|
|
508
519
|
const res = await packWorkspace(wsDir, {
|
|
509
|
-
destination:
|
|
520
|
+
destination: dest,
|
|
510
521
|
dryRun: false,
|
|
511
522
|
});
|
|
512
523
|
const { rmSync } = await import('node:fs');
|
|
@@ -514,7 +525,10 @@ async function loadRewrittenManifest(wsDir, pkg) {
|
|
|
514
525
|
throw new Error('gjsify publish: pack did not produce a file');
|
|
515
526
|
const { gunzip, parseTar } = await import('@gjsify/tar');
|
|
516
527
|
const bytes = new Uint8Array(readFileSync(res.absolutePath));
|
|
517
|
-
|
|
528
|
+
// The whole per-call directory, not just the tarball inside it — the
|
|
529
|
+
// destination is now unique per invocation, so leaving it behind would grow
|
|
530
|
+
// the temp directory once per publish.
|
|
531
|
+
rmSync(dest, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
518
532
|
const tar = await gunzip(bytes);
|
|
519
533
|
for (const entry of parseTar(tar)) {
|
|
520
534
|
if (entry.name === 'package/package.json' && entry.body) {
|
|
@@ -107,7 +107,11 @@ export const storybookCommand = {
|
|
|
107
107
|
type: 'boolean',
|
|
108
108
|
default: false,
|
|
109
109
|
})
|
|
110
|
-
.option('build-only', {
|
|
110
|
+
.option('build-only', {
|
|
111
|
+
description: 'Build the bundle but do not launch it',
|
|
112
|
+
type: 'boolean',
|
|
113
|
+
default: false,
|
|
114
|
+
}),
|
|
111
115
|
handler: async (args) => {
|
|
112
116
|
const cwd = process.cwd();
|
|
113
117
|
const pkg = readPackage(cwd);
|
package/lib/commands/test.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
import { existsSync, statSync, readdirSync } from 'node:fs';
|
|
9
9
|
import { join, dirname, resolve, relative } from 'node:path';
|
|
10
10
|
import { spawn } from 'node:child_process';
|
|
11
|
+
import { isRuntimeAvailable, RUNTIMES } from '../utils/runtimes.js';
|
|
12
|
+
import { nodeBinary } from '../utils/run-node.js';
|
|
11
13
|
import { Config } from '../config.js';
|
|
12
14
|
import { BuildAction } from '../actions/build.js';
|
|
13
15
|
import { runGjsBundle } from '../utils/run-gjs.js';
|
|
@@ -64,6 +66,16 @@ export const testCommand = {
|
|
|
64
66
|
// handler would try to build the missing entry anyway.
|
|
65
67
|
return process.exit(1);
|
|
66
68
|
}
|
|
69
|
+
// EXPLICIT (`--runtime node`) vs DEFAULT (`all`, or a package's declared
|
|
70
|
+
// set) is the whole distinction here, and it decides what an absent
|
|
71
|
+
// runtime means. Asked for by name, its absence is the answer to the
|
|
72
|
+
// question and must fail loudly. Reached only because it is in the
|
|
73
|
+
// default set, its absence is a fact about the host — and failing there
|
|
74
|
+
// reports a PASSING suite as a failed run: on a Node-less GJS host
|
|
75
|
+
// (postmarketOS/aarch64, gjs 1.88, no node) `gjsify test` printed
|
|
76
|
+
// `✅ gjs (267ms) ❌ node (27ms) — spawn node ENOENT` for a suite whose
|
|
77
|
+
// every assertion passed.
|
|
78
|
+
const explicit = args.runtime === 'gjs' || args.runtime === 'node';
|
|
67
79
|
const requested = args.runtime === 'gjs'
|
|
68
80
|
? ['gjs']
|
|
69
81
|
: args.runtime === 'node'
|
|
@@ -71,8 +83,19 @@ export const testCommand = {
|
|
|
71
83
|
: testCfg.runtimes && testCfg.runtimes.length > 0
|
|
72
84
|
? testCfg.runtimes
|
|
73
85
|
: ['gjs', 'node'];
|
|
86
|
+
// Skipping is NOT silent — a skipped runtime is reported, so "tests
|
|
87
|
+
// passed" never quietly means "on fewer runtimes than you think".
|
|
88
|
+
const runnable = explicit ? requested : requested.filter((rt) => isRuntimeAvailable(rt));
|
|
89
|
+
for (const rt of requested.filter((rt) => !runnable.includes(rt))) {
|
|
90
|
+
console.log(`[gjsify test] skipping ${rt} — no \`${RUNTIMES[rt].probe}\` on PATH (not requested explicitly)`);
|
|
91
|
+
}
|
|
92
|
+
if (runnable.length === 0) {
|
|
93
|
+
console.error(`[gjsify test] none of the default runtimes (${requested.join(', ')}) is available on this host.\n` +
|
|
94
|
+
` Install one, or pin the set: \`gjsify test --runtime <rt>\` / \`gjsify.test.runtimes\` in package.json.`);
|
|
95
|
+
return process.exit(1);
|
|
96
|
+
}
|
|
74
97
|
const results = [];
|
|
75
|
-
for (const runtime of
|
|
98
|
+
for (const runtime of runnable) {
|
|
76
99
|
const outfile = join(outdir, `test.${runtime}.mjs`);
|
|
77
100
|
// Build stage (skip if --no-build OR (not --rebuild AND outfile fresher than src)).
|
|
78
101
|
if (args.build !== false) {
|
|
@@ -171,7 +194,11 @@ async function runTestBundle(outfile, runtime) {
|
|
|
171
194
|
return;
|
|
172
195
|
}
|
|
173
196
|
await new Promise((resolvePromise, reject) => {
|
|
174
|
-
|
|
197
|
+
// `nodeBinary()`, never a bare `'node'`: under the committed GJS bundle
|
|
198
|
+
// `process.execPath` is the bundle itself, and on a host without Node a
|
|
199
|
+
// bare literal dies `spawn node ENOENT` — which is how a fully passing
|
|
200
|
+
// GJS suite was reported as a failed run.
|
|
201
|
+
const child = spawn(nodeBinary(), [outfile], { stdio: 'inherit' });
|
|
175
202
|
child.on('error', reject);
|
|
176
203
|
child.on('exit', (code) => {
|
|
177
204
|
if (code === 0)
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// <pkg>` without -g) is a separate workstream — it needs to rewrite
|
|
15
15
|
// package.json + refresh the lockfile, which install -g doesn't touch.
|
|
16
16
|
import { existsSync, readFileSync, readdirSync, rmSync, statSync, unlinkSync } from 'node:fs';
|
|
17
|
-
import { join } from 'node:path';
|
|
17
|
+
import { isAbsolute, join, relative, sep } from 'node:path';
|
|
18
18
|
import { defaultGlobalLayout, specToPackageName } from '../utils/install-global.js';
|
|
19
19
|
export const uninstallCommand = {
|
|
20
20
|
command: 'uninstall <packages..>',
|
|
@@ -109,7 +109,42 @@ export const uninstallCommand = {
|
|
|
109
109
|
* string in the file, which with the preamble is the prebuild dir list) and
|
|
110
110
|
* check whether it's under `pkgDir`. Non-shim files (e.g. unrelated binaries
|
|
111
111
|
* the user installed via `npm install -g`) are skipped silently.
|
|
112
|
+
*
|
|
113
|
+
* WINDOWS
|
|
114
|
+
*
|
|
115
|
+
* `linkGlobalBins` writes THREE files there — the `sh` launcher above plus the
|
|
116
|
+
* `.cmd` and `.ps1` siblings cmd.exe and pwsh need, because Windows runs neither
|
|
117
|
+
* an extension-less file nor a `#!` line. Two bugs stacked on that:
|
|
118
|
+
*
|
|
119
|
+
* • the containment test was `target.startsWith(pkgDir + '/')`, while both
|
|
120
|
+
* `pkgDir` and the baked `targetAbs` use `\` there — so the sh launcher
|
|
121
|
+
* never matched either;
|
|
122
|
+
* • nothing looked for the `.cmd`/`.ps1` siblings at all — they do not begin
|
|
123
|
+
* `#!/bin/sh`, so the scan above skips them by construction.
|
|
124
|
+
*
|
|
125
|
+
* Net: `gjsify uninstall -g <pkg>` on Windows deleted the package and left all
|
|
126
|
+
* three launchers pointing at a deleted path. The user then got a crash rather
|
|
127
|
+
* than "command not found", and a later install of a different package with the
|
|
128
|
+
* same bin name stayed shadowed by the stale `.cmd`.
|
|
129
|
+
*
|
|
130
|
+
* The sh launcher stays the ANCHOR — it is written on every platform, so it is
|
|
131
|
+
* the one shape worth parsing — and its siblings are collected by name. That
|
|
132
|
+
* avoids teaching this function to read batch and PowerShell as well.
|
|
112
133
|
*/
|
|
134
|
+
/**
|
|
135
|
+
* Is `target` `dir` itself, or under it?
|
|
136
|
+
*
|
|
137
|
+
* Asked through `path.relative` rather than a string prefix, so it holds
|
|
138
|
+
* whatever separator either side happens to be spelled with — the string form
|
|
139
|
+
* was `\`-vs-`/` blind on Windows. It also stops `/opt/foo-backup` from reading
|
|
140
|
+
* as inside `/opt/foo`, which a bare `startsWith(dir)` would have allowed.
|
|
141
|
+
*/
|
|
142
|
+
function isInside(dir, target) {
|
|
143
|
+
const rel = relative(dir, target);
|
|
144
|
+
if (rel === '')
|
|
145
|
+
return true;
|
|
146
|
+
return rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
147
|
+
}
|
|
113
148
|
function findBinShimsForPackage(binDir, pkgDir, verbose) {
|
|
114
149
|
if (!existsSync(binDir))
|
|
115
150
|
return [];
|
|
@@ -140,8 +175,15 @@ function findBinShimsForPackage(binDir, pkgDir, verbose) {
|
|
|
140
175
|
if (!m)
|
|
141
176
|
continue;
|
|
142
177
|
const target = m[1];
|
|
143
|
-
if (
|
|
178
|
+
if (isInside(pkgDir, target)) {
|
|
144
179
|
matches.push(fullPath);
|
|
180
|
+
// The Windows companions, when present. `existsSync` rather
|
|
181
|
+
// than a platform check: what decides is whether they were
|
|
182
|
+
// written, and a tree can be inspected from another OS.
|
|
183
|
+
for (const ext of ['.cmd', '.ps1']) {
|
|
184
|
+
if (existsSync(fullPath + ext))
|
|
185
|
+
matches.push(fullPath + ext);
|
|
186
|
+
}
|
|
145
187
|
}
|
|
146
188
|
}
|
|
147
189
|
catch (err) {
|