@gjsify/cli 0.36.0 → 0.37.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 +3 -3
- package/lib/bundler-pick.js +61 -87
- package/lib/cli-fail.d.ts +4 -7
- package/lib/cli-fail.js +29 -55
- package/lib/commands/affected.js +161 -286
- package/lib/commands/install.d.ts +63 -66
- package/lib/commands/install.js +372 -536
- package/lib/templates/install.mjs.tmpl +38 -68
- package/lib/types/cli-build-options.d.ts +33 -67
- package/lib/types/command.d.ts +9 -12
- package/lib/types/config-data.d.ts +125 -221
- package/lib/utils/auth-npmrc.d.ts +5 -10
- package/lib/utils/auth-npmrc.js +11 -21
- package/lib/utils/clear-targets.d.ts +8 -11
- package/lib/utils/clear-targets.js +22 -29
- package/lib/utils/copy-targets.d.ts +13 -19
- package/lib/utils/copy-targets.js +31 -44
- package/lib/utils/dep-aggregation.d.ts +7 -15
- package/lib/utils/dep-aggregation.js +9 -22
- package/lib/utils/detect-native-packages.d.ts +83 -116
- package/lib/utils/detect-native-packages.js +111 -149
- package/lib/utils/dir-link.d.ts +29 -48
- package/lib/utils/dir-link.js +33 -56
- package/lib/utils/discover-showcases.d.ts +6 -7
- package/lib/utils/discover-showcases.js +11 -15
- package/lib/utils/dlx-cache.d.ts +29 -38
- package/lib/utils/dlx-cache.js +53 -63
- package/lib/utils/force-exit.d.ts +0 -3
- package/lib/utils/force-exit.js +13 -24
- package/lib/utils/gi-search-path.d.ts +16 -24
- package/lib/utils/gi-search-path.js +54 -82
- package/lib/utils/gjs-bundle-guard.d.ts +5 -13
- package/lib/utils/gjs-bundle-guard.js +35 -57
- package/lib/utils/gjs-source-escape.d.ts +2 -2
- package/lib/utils/gjs-source-escape.js +21 -39
- package/lib/utils/gjsify-shim.d.ts +17 -20
- package/lib/utils/gjsify-shim.js +80 -120
- package/lib/utils/html-entry.d.ts +8 -12
- package/lib/utils/html-entry.js +19 -28
- package/lib/utils/install-backend-native.d.ts +123 -161
- package/lib/utils/install-backend-native.js +536 -737
- package/lib/utils/install-backend.d.ts +42 -57
- package/lib/utils/install-backend.js +16 -21
- package/lib/utils/install-cache-fs.d.ts +10 -15
- package/lib/utils/install-cache-fs.js +18 -26
- package/lib/utils/install-lock.d.ts +5 -6
- package/lib/utils/install-lock.js +46 -65
- package/lib/utils/install-packument-cache.d.ts +27 -33
- package/lib/utils/install-packument-cache.js +44 -57
- package/lib/utils/install-progress.d.ts +2 -9
- package/lib/utils/install-progress.js +14 -39
- package/lib/utils/install-tarball-cache.d.ts +14 -20
- package/lib/utils/install-tarball-cache.js +41 -59
- package/lib/utils/library-output.d.ts +9 -22
- package/lib/utils/library-output.js +19 -37
- package/lib/utils/load-npmrc.d.ts +5 -6
- package/lib/utils/load-npmrc.js +18 -27
- package/lib/utils/node-bundle-guard.d.ts +4 -8
- package/lib/utils/node-bundle-guard.js +21 -35
- package/lib/utils/node-script.js +33 -54
- package/lib/utils/node-version.d.ts +5 -14
- package/lib/utils/node-version.js +15 -24
- package/lib/utils/normalize-bundler-options.d.ts +7 -11
- package/lib/utils/normalize-bundler-options.js +24 -42
- package/lib/utils/npm-oidc.d.ts +23 -37
- package/lib/utils/npm-oidc.js +28 -53
- package/lib/utils/npm-otp-cache.d.ts +4 -4
- package/lib/utils/npm-otp-cache.js +20 -25
- package/lib/utils/npm-otp.d.ts +20 -30
- package/lib/utils/npm-otp.js +32 -51
- package/lib/utils/onboard-probe.d.ts +7 -13
- package/lib/utils/onboard-probe.js +16 -27
- package/lib/utils/parse-spec.d.ts +3 -3
- package/lib/utils/parse-spec.js +4 -5
- package/lib/utils/pkg-json-edit.d.ts +7 -12
- package/lib/utils/pkg-json-edit.js +13 -26
- package/lib/utils/pkg-json.d.ts +11 -18
- package/lib/utils/pkg-json.js +19 -28
- package/lib/utils/prefixed-output.d.ts +3 -4
- package/lib/utils/prefixed-output.js +13 -19
- package/lib/utils/prompt.d.ts +3 -4
- package/lib/utils/prompt.js +14 -18
- package/lib/utils/publish-diagnose.d.ts +11 -16
- package/lib/utils/publish-diagnose.js +15 -28
- package/lib/utils/publish-headers.d.ts +7 -11
- package/lib/utils/publish-headers.js +36 -65
- package/lib/utils/publishable-packages.d.ts +4 -6
- package/lib/utils/publishable-packages.js +14 -18
- package/lib/utils/resolve-gjs-entry.d.ts +5 -8
- package/lib/utils/resolve-gjs-entry.js +10 -27
- package/lib/utils/resolve-npm-package.d.ts +6 -11
- package/lib/utils/resolve-npm-package.js +29 -63
- package/lib/utils/resolve-plugin-by-name.d.ts +11 -19
- package/lib/utils/resolve-plugin-by-name.js +9 -14
- package/lib/utils/run-gjs.d.ts +16 -21
- package/lib/utils/run-gjs.js +31 -56
- package/lib/utils/run-lifecycle-script.d.ts +5 -9
- package/lib/utils/run-lifecycle-script.js +23 -42
- package/lib/utils/run-node.d.ts +21 -33
- package/lib/utils/run-node.js +43 -64
- package/lib/utils/runtimes.d.ts +25 -35
- package/lib/utils/runtimes.js +34 -46
- package/lib/utils/spawn.d.ts +32 -48
- package/lib/utils/spawn.js +51 -84
- package/lib/utils/stdin.d.ts +2 -2
- package/lib/utils/stdin.js +10 -25
- package/lib/utils/suggest.d.ts +6 -7
- package/lib/utils/suggest.js +8 -10
- package/lib/utils/system-gi.d.ts +18 -27
- package/lib/utils/system-gi.js +60 -114
- package/lib/utils/trust-registry.d.ts +4 -8
- package/lib/utils/trust-registry.js +7 -15
- package/lib/utils/win32-command.d.ts +8 -14
- package/lib/utils/win32-command.js +29 -43
- package/lib/utils/workspace-root.js +7 -12
- package/package.json +18 -18
package/lib/commands/install.js
CHANGED
|
@@ -1,32 +1,21 @@
|
|
|
1
|
-
// `gjsify install [pkg...]` — install
|
|
1
|
+
// `gjsify install [pkg...]` — project install, `<pkg>` add, or `-g` user-global
|
|
2
|
+
// install (XDG prefix + GJS-runnable bin), plus gjsify-aware post-checks.
|
|
2
3
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// All three modes route through `installPackagesNative`
|
|
5
|
+
// (`@gjsify/{semver,npm-registry,tar}`) — no Node/npm at runtime.
|
|
6
|
+
// `--backend=npm` / `GJSIFY_INSTALL_BACKEND=npm` is the escape hatch for what
|
|
7
|
+
// the native backend does not model (Yarn PnP, lifecycle scripts).
|
|
7
8
|
//
|
|
8
|
-
//
|
|
9
|
-
// `
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// (Yarn PnP repos, lifecycle scripts, npm's `overrides` quirks).
|
|
9
|
+
// A workspace install hoists every member's externals into the root
|
|
10
|
+
// `node_modules/` and symlinks `workspace:` refs to their source. Members with
|
|
11
|
+
// conflicting ranges of the same external still share ONE hoisted copy; the
|
|
12
|
+
// resolver warns naming both ranges, both requesters and the winner (per-member
|
|
13
|
+
// dedup is the open Phase D.8 item in status/open-todos.md).
|
|
14
14
|
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
// external dep from different workspaces still share ONE hoisted root copy
|
|
20
|
-
// (a real per-workspace dedup pass is Phase D.8, see status/open-todos.md), but since
|
|
21
|
-
// ADR 0001 step 3 the resolver surfaces every such conflict loudly —
|
|
22
|
-
// `[gjsify] warning: version conflict for <pkg> …` names both ranges, the
|
|
23
|
-
// workspaces that requested them, and the version that actually won.
|
|
24
|
-
//
|
|
25
|
-
// Concurrency (ADR 0001 step 2): every mutation of an install prefix —
|
|
26
|
-
// project node_modules/, gjsify-lock.json, the user-global prefix — runs
|
|
27
|
-
// under a per-prefix cross-process lock (utils/install-lock.ts). Installs
|
|
28
|
-
// into different prefixes stay parallel; the shared XDG tarball/packument
|
|
29
|
-
// caches need no lock because their writes are atomic tmp+rename.
|
|
15
|
+
// Concurrency (ADR 0001): every prefix mutation runs under a per-prefix
|
|
16
|
+
// cross-process lock (utils/install-lock.ts), so installs into different
|
|
17
|
+
// prefixes stay parallel. The shared XDG tarball/packument caches need no lock
|
|
18
|
+
// because their writes are atomic tmp+rename.
|
|
30
19
|
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
31
20
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
32
21
|
import { fileURLToPath } from 'node:url';
|
|
@@ -47,30 +36,25 @@ import { addDependencyEntry, defaultRangeFromVersion, parseSpec, projectSpecsFro
|
|
|
47
36
|
/**
|
|
48
37
|
* Link type for the workspace↔workspace directory links.
|
|
49
38
|
*
|
|
50
|
-
* Windows cannot create a
|
|
51
|
-
* Mode (`EPERM`), but any user can create an NTFS
|
|
52
|
-
*
|
|
53
|
-
* no try/catch, so a non-elevated Windows workspace install hard-failed here.
|
|
39
|
+
* Windows cannot create a directory *symlink* without elevation or Developer
|
|
40
|
+
* Mode (`EPERM`), but any user can create an NTFS junction — which is why npm
|
|
41
|
+
* and yarn both use junctions for workspace links.
|
|
54
42
|
*/
|
|
55
43
|
const WORKSPACE_LINK_TYPE = process.platform === 'win32' ? 'junction' : undefined;
|
|
56
44
|
/**
|
|
57
45
|
* The `target` argument for a workspace link.
|
|
58
46
|
*
|
|
59
|
-
* Node normalises a `'junction'` target with `path.resolve()` —
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* moved.
|
|
47
|
+
* Node normalises a `'junction'` target with `path.resolve()` — against the
|
|
48
|
+
* process CWD, not the link's own directory — so a junction must be given an
|
|
49
|
+
* absolute path. POSIX symlinks stay relative so the tree survives a move.
|
|
63
50
|
*/
|
|
64
51
|
function workspaceLinkTarget(linkPath, absTarget) {
|
|
65
52
|
return WORKSPACE_LINK_TYPE === 'junction' ? resolve(absTarget) : relative(dirname(linkPath), absTarget);
|
|
66
53
|
}
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
// flows. Per-fetch timeout (30s, retried) catches the truly stuck case
|
|
72
|
-
// inside this budget. Set --timeout 0 to disable the wall-clock guard
|
|
73
|
-
// entirely.
|
|
54
|
+
// 30 min, not minutes: this monorepo (212+ members × 600+ externals) legitimately
|
|
55
|
+
// takes 10-20 min on a fresh CI install behind a slow npm CDN, so a tighter
|
|
56
|
+
// default would false-positive. The per-fetch timeout (30s, retried) catches the
|
|
57
|
+
// truly stuck case inside this budget.
|
|
74
58
|
const DEFAULT_INSTALL_TIMEOUT_MS = 1_800_000;
|
|
75
59
|
export const installCommand = {
|
|
76
60
|
command: 'install [packages..]',
|
|
@@ -143,37 +127,33 @@ export const installCommand = {
|
|
|
143
127
|
default: false,
|
|
144
128
|
}),
|
|
145
129
|
handler: async (args) => {
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
// the right channel for them rather than a parameter.
|
|
130
|
+
// Must run before anything resolves — `--os/--cpu/--libc/--force` are npm
|
|
131
|
+
// config keys, see applyPlatformConfigFromFlags for why via the environment.
|
|
149
132
|
applyPlatformConfigFromFlags(args);
|
|
150
|
-
// --immutable
|
|
151
|
-
//
|
|
152
|
-
// behavior: `yarn add --immutable` is a hard error.
|
|
133
|
+
// --immutable rejects `<pkg>` adds and `--global` (no lockfile concept),
|
|
134
|
+
// matching yarn, where `yarn add --immutable` is a hard error.
|
|
153
135
|
if (args.immutable) {
|
|
154
136
|
if (args.packages && args.packages.length > 0) {
|
|
155
137
|
console.error('gjsify install --immutable does not accept package arguments. ' +
|
|
156
138
|
'Remove the package names or drop --immutable.');
|
|
157
|
-
// `return` — a bare `process.exit()` is deferred under GJS and
|
|
158
|
-
//
|
|
139
|
+
// `return` — a bare `process.exit()` is deferred under GJS, and the
|
|
140
|
+
// install would proceed past a refused flag combination. Same for
|
|
141
|
+
// every `return process.exit()` below.
|
|
159
142
|
return process.exit(1);
|
|
160
143
|
}
|
|
161
144
|
if (args.global) {
|
|
162
145
|
console.error('gjsify install --immutable is incompatible with --global.');
|
|
163
|
-
// `return` — see the deferred-exit note above.
|
|
164
146
|
return process.exit(1);
|
|
165
147
|
}
|
|
166
148
|
if (args['refresh-lockfile']) {
|
|
167
149
|
console.error('gjsify install --immutable is incompatible with --refresh-lockfile ' +
|
|
168
150
|
'(--immutable forbids rewriting the lockfile). Drop one.');
|
|
169
|
-
// `return` — see the deferred-exit note above.
|
|
170
151
|
return process.exit(1);
|
|
171
152
|
}
|
|
172
153
|
}
|
|
173
154
|
if (args.global) {
|
|
174
155
|
if (!args.packages || args.packages.length === 0) {
|
|
175
156
|
console.error('gjsify install --global requires at least one <pkg> argument.');
|
|
176
|
-
// `return` — see the deferred-exit note above.
|
|
177
157
|
return process.exit(1);
|
|
178
158
|
}
|
|
179
159
|
for (const flag of ['save-dev', 'save-peer', 'save-optional']) {
|
|
@@ -184,68 +164,43 @@ export const installCommand = {
|
|
|
184
164
|
await installGlobalAndLink(args.packages, { verbose: args.verbose });
|
|
185
165
|
return;
|
|
186
166
|
}
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
// of step with the @gjsify/cli the workspace pins is the root cause of
|
|
190
|
-
// both the mysterious install stall (a mismatched CLI resolves a
|
|
191
|
-
// differently-shaped lock → full fresh resolve → a wedge-prone extract)
|
|
192
|
-
// and silent-wrong-builds (the stale CLI builds with stale semantics).
|
|
167
|
+
// Before the heavy install, so the skew is visible even if a later phase
|
|
168
|
+
// is slow — it is the root cause the slow phase would otherwise mask.
|
|
193
169
|
warnOnCliVersionSkew(process.cwd());
|
|
194
|
-
// Backend selection (in precedence order):
|
|
195
|
-
// 1. --backend flag (explicit user choice)
|
|
196
|
-
// 2. GJSIFY_INSTALL_BACKEND env (back-compat shape from pre-flag era)
|
|
197
|
-
// 3. native (default)
|
|
198
170
|
const backend = args.backend ?? process.env.GJSIFY_INSTALL_BACKEND ?? 'native';
|
|
199
171
|
if (backend === 'npm') {
|
|
200
172
|
await projectInstallViaNpm(args);
|
|
201
173
|
await runPostInstallChecks(args);
|
|
202
174
|
return;
|
|
203
175
|
}
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
// signal-aware await rejects and the process exits cleanly with an
|
|
208
|
-
// actionable message. Per-request timeouts inside @gjsify/npm-registry
|
|
209
|
-
// (default 30s, retried) still apply within this budget.
|
|
176
|
+
// On timeout this controller aborts every in-flight registry fetch, so a
|
|
177
|
+
// signal-aware await rejects and the process exits with an actionable
|
|
178
|
+
// message.
|
|
210
179
|
//
|
|
211
|
-
//
|
|
212
|
-
// the fresh-resolve path do not —
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
// residual never-settling await into a clean failure instead of a
|
|
219
|
-
// silent 0%-CPU hang (the observed "it never completed; I killed it"
|
|
220
|
-
// pathology). `extractOne` additionally caps each extract with its own
|
|
221
|
-
// stall timeout so the common case fails fast, long before this.
|
|
180
|
+
// But the abort only rescues awaits that OBSERVE the signal, and some on
|
|
181
|
+
// the fresh-resolve path do not — notably tarball extraction (a Gio-backed
|
|
182
|
+
// decompress whose stream close-event can be dropped under GJS) and the
|
|
183
|
+
// workspace symlink fs-pool. `hardExitTimerId` below is the backstop that
|
|
184
|
+
// turns any residual never-settling await into a non-zero exit instead of a
|
|
185
|
+
// silent 0%-CPU hang. `extractOne` also caps each extract with its own
|
|
186
|
+
// stall timeout, so the common case fails long before either.
|
|
222
187
|
const overallTimeoutMs = args.timeout > 0 ? args.timeout : 0;
|
|
223
188
|
const overallController = overallTimeoutMs > 0 ? new AbortController() : null;
|
|
224
189
|
const overallTimerId = overallController !== null
|
|
225
190
|
? setTimeout(() => overallController.abort(new Error('install-overall-timeout')), overallTimeoutMs)
|
|
226
191
|
: null;
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
// short enough that a genuinely-wedged await doesn't hang for minutes.
|
|
192
|
+
// Long enough for a signal-aware reject to propagate through the catch (the
|
|
193
|
+
// clean path), short enough that a wedged await does not hang for minutes.
|
|
230
194
|
const HARD_EXIT_GRACE_MS = 10_000;
|
|
231
195
|
const hardExitTimerId = overallTimeoutMs > 0
|
|
232
196
|
? setTimeout(() => {
|
|
233
197
|
const secs = Math.round(overallTimeoutMs / 100) / 10;
|
|
234
|
-
// Do NOT assert a cause here
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
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
|
|
246
|
-
// multi-second SYNCHRONOUS stretches (tar parse + file
|
|
247
|
-
// writes) and 16 run concurrently, so the clean abort path
|
|
248
|
-
// can easily miss the grace window with nothing wedged.
|
|
198
|
+
// Do NOT assert a cause here: this timer cannot tell the two
|
|
199
|
+
// shapes apart, and naming the rarer one sends the next reader
|
|
200
|
+
// after a bug that is not there. Extraction has multi-second
|
|
201
|
+
// SYNCHRONOUS stretches (tar parse + file writes) and 16 run
|
|
202
|
+
// concurrently, so a healthy install can miss the grace window
|
|
203
|
+
// with nothing wedged — measured: ~1.4 GB on a cold tree here.
|
|
249
204
|
console.error(`gjsify install: the ${secs}s budget elapsed and the abort did not unwind within ` +
|
|
250
205
|
`${Math.round(HARD_EXIT_GRACE_MS / 1000)}s — forcing exit. That means one of two ` +
|
|
251
206
|
`things, and this timer cannot distinguish them: the install was still WORKING ` +
|
|
@@ -257,9 +212,8 @@ export const installCommand = {
|
|
|
257
212
|
forceExit(1);
|
|
258
213
|
}, overallTimeoutMs + HARD_EXIT_GRACE_MS)
|
|
259
214
|
: null;
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
// process end the instant the install returns; `finally` clears it too.
|
|
215
|
+
// Node keeps the loop up while a timer is armed, so `unref` lets the process
|
|
216
|
+
// end the instant the install returns.
|
|
263
217
|
hardExitTimerId?.unref?.();
|
|
264
218
|
try {
|
|
265
219
|
await projectInstallNative(args, overallController?.signal);
|
|
@@ -270,9 +224,8 @@ export const installCommand = {
|
|
|
270
224
|
const secs = Math.round(overallTimeoutMs / 100) / 10;
|
|
271
225
|
console.error(`gjsify install: timed out after ${secs}s — likely a registry slowdown or a wedged extract.\n` +
|
|
272
226
|
`Re-run, or override with --timeout <ms> (set --timeout 0 to disable the overall budget).`);
|
|
273
|
-
// `return` — the deferred GJS exit otherwise fell through into
|
|
274
|
-
//
|
|
275
|
-
// `finally` still runs on the way out and clears the timers.
|
|
227
|
+
// `return` — the deferred GJS exit otherwise fell through into the
|
|
228
|
+
// rethrow below and reported the timeout twice.
|
|
276
229
|
return process.exit(1);
|
|
277
230
|
}
|
|
278
231
|
throw err;
|
|
@@ -288,20 +241,16 @@ export const installCommand = {
|
|
|
288
241
|
/**
|
|
289
242
|
* Hand the platform target + force bypass to the install backend.
|
|
290
243
|
*
|
|
291
|
-
* `os`, `cpu`, `libc` and `force` are npm CONFIG KEYS, and npm's
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
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.
|
|
244
|
+
* `os`, `cpu`, `libc` and `force` are npm CONFIG KEYS, and npm's transport for a
|
|
245
|
+
* config key is the environment (`--libc=musl` ≡ `npm_config_libc=musl`), so the
|
|
246
|
+
* flags come with the env spelling for free. Not threaded as a parameter because
|
|
247
|
+
* one env write reaches EVERY prefix an invocation may install into (project,
|
|
248
|
+
* workspace root, `-g`, the `dlx` cache) and is inherited by children, so a
|
|
249
|
+
* nested install cannot quietly resolve for a different machine than its parent.
|
|
301
250
|
*
|
|
302
|
-
* Empty/whitespace values are ignored rather than written as the empty string
|
|
303
|
-
* `--os=` must not mean "the nameless platform"
|
|
304
|
-
* them on the read side too
|
|
251
|
+
* Empty/whitespace values are ignored rather than written as the empty string —
|
|
252
|
+
* `--os=` must not mean "the nameless platform", and `readPlatformOverrides`
|
|
253
|
+
* drops them on the read side too.
|
|
305
254
|
*/
|
|
306
255
|
function applyPlatformConfigFromFlags(args) {
|
|
307
256
|
const keys = [
|
|
@@ -323,12 +272,10 @@ function applyPlatformConfigFromFlags(args) {
|
|
|
323
272
|
process.env.npm_config_force = 'true';
|
|
324
273
|
}
|
|
325
274
|
/**
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
* We match permissively because intermediate layers (fetch, GJS Soup, our
|
|
331
|
-
* own delay()) re-wrap the reason in their own AbortError instances.
|
|
275
|
+
* Was this error raised because the overall-install AbortSignal fired? Matched
|
|
276
|
+
* permissively on purpose: intermediate layers (fetch, GJS Soup, our own
|
|
277
|
+
* `delay()`) re-wrap the `install-overall-timeout` reason in their own
|
|
278
|
+
* AbortError instances.
|
|
332
279
|
*/
|
|
333
280
|
function isAbortedFromOverallTimeout(err) {
|
|
334
281
|
if (!err || typeof err !== 'object')
|
|
@@ -361,9 +308,9 @@ function readCliVersionFrom(pkgJsonPath) {
|
|
|
361
308
|
return null;
|
|
362
309
|
}
|
|
363
310
|
/**
|
|
364
|
-
* Version of the
|
|
365
|
-
*
|
|
366
|
-
*
|
|
311
|
+
* Version of the `@gjsify/cli` that is ACTUALLY running: walks up from
|
|
312
|
+
* `import.meta.url` to the nearest `@gjsify/cli` package.json, the same
|
|
313
|
+
* discovery as `self-update`'s `readCurrentVersion`.
|
|
367
314
|
*/
|
|
368
315
|
function readRunningCliVersion() {
|
|
369
316
|
try {
|
|
@@ -381,24 +328,20 @@ function readRunningCliVersion() {
|
|
|
381
328
|
return null;
|
|
382
329
|
}
|
|
383
330
|
/**
|
|
384
|
-
* Version of the
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
* in-repo source so the check also fires on a fresh, not-yet-installed
|
|
388
|
-
* checkout of the gjsify monorepo itself.
|
|
331
|
+
* Version of the `@gjsify/cli` the workspace at `cwd` pins. Prefers the resolved
|
|
332
|
+
* install, falling back to the in-repo source so the check also fires on a fresh,
|
|
333
|
+
* not-yet-installed checkout of the gjsify monorepo itself.
|
|
389
334
|
*/
|
|
390
335
|
function readWorkspaceCliVersion(cwd) {
|
|
391
336
|
return (readCliVersionFrom(join(cwd, 'node_modules', CLI_PACKAGE_NAME, 'package.json')) ??
|
|
392
337
|
readCliVersionFrom(join(cwd, 'packages', 'infra', 'cli', 'package.json')));
|
|
393
338
|
}
|
|
394
339
|
/**
|
|
395
|
-
* Warn (never block) when the running gjsify CLI
|
|
396
|
-
* the
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
400
|
-
* single actionable line pointing at `gjsify self-update` beats both silently.
|
|
401
|
-
* Suppress with GJSIFY_NO_VERSION_SKEW_WARNING=1.
|
|
340
|
+
* Warn (never block) when the running gjsify CLI differs from the `@gjsify/cli`
|
|
341
|
+
* the workspace pins. A mismatched CLI is the measured root of two
|
|
342
|
+
* hard-to-diagnose failures: install stalls (the lock is discarded → full fresh
|
|
343
|
+
* resolve → wedge-prone extract) and silent-wrong-builds (stale bundle
|
|
344
|
+
* semantics). Suppress with `GJSIFY_NO_VERSION_SKEW_WARNING=1`.
|
|
402
345
|
*/
|
|
403
346
|
function warnOnCliVersionSkew(cwd) {
|
|
404
347
|
if (process.env.GJSIFY_NO_VERSION_SKEW_WARNING === '1')
|
|
@@ -425,26 +368,23 @@ function isWorkspaceRoot(cwd) {
|
|
|
425
368
|
/**
|
|
426
369
|
* Which dependency NAMES are optional across the given manifests.
|
|
427
370
|
*
|
|
428
|
-
* The specs handed to the
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
* REQUIRED one (see `NativeInstallOptions.optionalSpecs`).
|
|
371
|
+
* The specs handed to the backend are flat `"<name>@<range>"` strings, so the
|
|
372
|
+
* kind has to travel beside them. It decides exactly one thing: an incompatible
|
|
373
|
+
* `os`/`cpu`/`libc` skips an OPTIONAL dependency and fails a REQUIRED one (see
|
|
374
|
+
* `NativeInstallOptions.optionalSpecs`).
|
|
433
375
|
*
|
|
434
|
-
* REQUIRED WINS ACROSS MANIFESTS, OPTIONAL WINS WITHIN ONE —
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
* `requiredDepEntries` fixes: `optionalDependencies: { fsevents }` alongside a
|
|
447
|
-
* `dependencies: { fsevents }` line would fail every Linux install with
|
|
376
|
+
* REQUIRED WINS ACROSS MANIFESTS, OPTIONAL WINS WITHIN ONE — two rules with two
|
|
377
|
+
* reasons, neither a special case of the other:
|
|
378
|
+
* - Across manifests: one package can be a plain dependency of one member and
|
|
379
|
+
* an optionalDependency of another. Those are two real edges, and an edge
|
|
380
|
+
* that may not be missing must not be silently missed. Subtracting rather
|
|
381
|
+
* than "last block seen" also makes the answer order-independent.
|
|
382
|
+
* - Within one manifest: a name in BOTH blocks is OPTIONAL because npm says so
|
|
383
|
+
* ("entries in optionalDependencies will override entries of the same name
|
|
384
|
+
* in dependencies") — one edge, and the optional block names its kind.
|
|
385
|
+
* Treating it as required is the root-edge twin of the defect
|
|
386
|
+
* `requiredDepEntries` fixes: `optionalDependencies: { fsevents }` beside a
|
|
387
|
+
* `dependencies: { fsevents }` line failed every Linux install with
|
|
448
388
|
* EBADPLATFORM over a package npm never installs there.
|
|
449
389
|
*/
|
|
450
390
|
export function optionalDependencyNames(manifests) {
|
|
@@ -456,9 +396,8 @@ export function optionalDependencyNames(manifests) {
|
|
|
456
396
|
optional.add(name);
|
|
457
397
|
for (const kind of ['dependencies', 'devDependencies']) {
|
|
458
398
|
for (const name of Object.keys(manifest[kind] ?? {})) {
|
|
459
|
-
// Overridden by THIS manifest's own optional block — not a
|
|
460
|
-
//
|
|
461
|
-
// declaration either.
|
|
399
|
+
// Overridden by THIS manifest's own optional block — not a required
|
|
400
|
+
// edge, so it must not veto another manifest's optional declaration.
|
|
462
401
|
if (optionalHere.has(name))
|
|
463
402
|
continue;
|
|
464
403
|
required.add(name);
|
|
@@ -481,23 +420,18 @@ function depKindFromArgs(args) {
|
|
|
481
420
|
async function projectInstallNative(args, signal) {
|
|
482
421
|
const cwd = process.cwd();
|
|
483
422
|
const pkgPath = join(cwd, 'package.json');
|
|
484
|
-
//
|
|
485
|
-
//
|
|
486
|
-
// tree
|
|
487
|
-
// install, and the two resolution strategies cannot coexist — a leftover
|
|
488
|
-
// `.pnp.cjs` makes Node ignore the node_modules/ tree we write, leaving a
|
|
489
|
-
// silently broken project.
|
|
423
|
+
// This is a node_modules-linker installer, and the two resolution strategies
|
|
424
|
+
// cannot coexist: a leftover `.pnp.cjs` makes Node ignore the node_modules/
|
|
425
|
+
// tree we write, leaving a silently broken project.
|
|
490
426
|
//
|
|
491
|
-
// Native PnP *generation*
|
|
492
|
-
//
|
|
493
|
-
//
|
|
494
|
-
//
|
|
495
|
-
// only ever match a single yarn release. So we stop here with actionable
|
|
496
|
-
// guidance instead of half-doing it.
|
|
427
|
+
// Native PnP *generation* is out of scope on purpose — it would mean
|
|
428
|
+
// replicating yarn's libzip-deterministic archives, SHA-512 cache keys,
|
|
429
|
+
// cache-version tracking and virtual-locator synthesis, and would only ever
|
|
430
|
+
// match one yarn release. Better to stop with actionable guidance.
|
|
497
431
|
//
|
|
498
|
-
//
|
|
499
|
-
//
|
|
500
|
-
//
|
|
432
|
+
// The error deliberately does NOT point at `GJSIFY_INSTALL_BACKEND=npm`: that
|
|
433
|
+
// backend rejects `workspace:` specs (`EUNSUPPORTEDPROTOCOL`), which is exactly
|
|
434
|
+
// what the repos reaching this branch have.
|
|
501
435
|
const pnpResidue = ['.pnp.cjs', '.pnp.loader.mjs'].filter((f) => existsSync(join(cwd, f)));
|
|
502
436
|
if (pnpResidue.length > 0) {
|
|
503
437
|
throw new Error(`gjsify install uses the node_modules linker and cannot run in a Yarn PnP ` +
|
|
@@ -509,14 +443,10 @@ async function projectInstallNative(args, signal) {
|
|
|
509
443
|
`• If instead you want Yarn to manage dependencies, set \`nodeLinker: node-modules\` ` +
|
|
510
444
|
`in .yarnrc.yml and run \`yarn install\` (not \`gjsify install\`).`);
|
|
511
445
|
}
|
|
512
|
-
// Workspace install (no args)
|
|
513
|
-
//
|
|
514
|
-
//
|
|
515
|
-
//
|
|
516
|
-
// workspace there. Without this, a child install treats the child as the
|
|
517
|
-
// root and tries to resolve its `workspace:`/sibling deps from the registry.
|
|
518
|
-
// Project-local `gjsify install <pkg>` inside a child still adds the dep to
|
|
519
|
-
// that child via the single-package path below (this branch is no-args only).
|
|
446
|
+
// Workspace install (no args), from the root OR from any member — npm/yarn/pnpm
|
|
447
|
+
// all resolve the monorepo root when you `install` inside a member. Without the
|
|
448
|
+
// walk up, a child install treats the child as the root and tries to resolve
|
|
449
|
+
// its `workspace:`/sibling deps from the registry.
|
|
520
450
|
if (!args.packages || args.packages.length === 0) {
|
|
521
451
|
const wsRoot = isWorkspaceRoot(cwd) ? cwd : findWorkspaceRoot(cwd);
|
|
522
452
|
if (wsRoot) {
|
|
@@ -531,10 +461,9 @@ async function projectInstallNative(args, signal) {
|
|
|
531
461
|
const pkg = readPackageJson(pkgPath);
|
|
532
462
|
const existingSpecs = pkg ? projectSpecsFromPackageJson(pkg) : [];
|
|
533
463
|
if (args.packages && args.packages.length > 0) {
|
|
534
|
-
// Combine new specs with existing manifest deps
|
|
535
|
-
//
|
|
536
|
-
//
|
|
537
|
-
// same name as an existing dep override.
|
|
464
|
+
// Combine new specs with the existing manifest deps: resolving the new ones
|
|
465
|
+
// alone would drop every previously-pinned lockfile entry. A new spec
|
|
466
|
+
// overrides an existing dep of the same name.
|
|
538
467
|
const newNames = new Set(args.packages.map((s) => parseSpec(s).name));
|
|
539
468
|
const carryover = existingSpecs.filter((s) => !newNames.has(parseSpec(s).name));
|
|
540
469
|
specs = [...carryover, ...args.packages];
|
|
@@ -550,28 +479,24 @@ async function projectInstallNative(args, signal) {
|
|
|
550
479
|
}
|
|
551
480
|
}
|
|
552
481
|
mkdirSync(cwd, { recursive: true });
|
|
553
|
-
// Progress bar is auto-enabled when stderr is a TTY (and `--verbose` /
|
|
554
|
-
// `--quiet` / `--no-progress` aren't set). When piped to a log file the
|
|
555
|
-
// reporter falls back to one line per phase begin/end.
|
|
556
482
|
const progress = makeProgressReporter({
|
|
557
483
|
enabled: !args.verbose && !args.quiet && args.progress !== false,
|
|
558
484
|
});
|
|
559
|
-
//
|
|
560
|
-
//
|
|
561
|
-
//
|
|
562
|
-
//
|
|
485
|
+
// Held across the install AND the manifest/lockfile sync that follows, so a
|
|
486
|
+
// concurrent `gjsify install <pkg>` in the same project cannot interleave its
|
|
487
|
+
// package.json / gjsify-lock.json writes with ours. `installPackages`
|
|
488
|
+
// re-acquires the same lock re-entrantly.
|
|
563
489
|
const lock = await acquireInstallLock(cwd, { signal });
|
|
564
490
|
try {
|
|
565
|
-
// Typed as NativeInstallOptions so the native-backend-only
|
|
566
|
-
//
|
|
567
|
-
//
|
|
568
|
-
// forwards the object to that backend unchanged.
|
|
491
|
+
// Typed as NativeInstallOptions so the native-backend-only `optionalSpecs`
|
|
492
|
+
// is CHECKED here instead of smuggled past an excess-property check;
|
|
493
|
+
// `installPackages` forwards the object to that backend unchanged.
|
|
569
494
|
const nativeOpts = {
|
|
570
495
|
prefix: cwd,
|
|
571
496
|
specs,
|
|
572
497
|
verbose: args.verbose,
|
|
573
|
-
// --immutable consumes the lockfile verbatim and must NOT rewrite
|
|
574
|
-
//
|
|
498
|
+
// --immutable consumes the lockfile verbatim and must NOT rewrite it:
|
|
499
|
+
// byte-stability under CI is the point.
|
|
575
500
|
lockfile: !args.immutable,
|
|
576
501
|
frozen: args.immutable,
|
|
577
502
|
refreshLockfile: args['refresh-lockfile'],
|
|
@@ -586,9 +511,8 @@ async function projectInstallNative(args, signal) {
|
|
|
586
511
|
]),
|
|
587
512
|
};
|
|
588
513
|
const result = await installPackages(nativeOpts);
|
|
589
|
-
//
|
|
590
|
-
//
|
|
591
|
-
// flow doesn't mutate manifest entries.
|
|
514
|
+
// Only the `gjsify install <pkg>...` add-a-dep flow mutates the manifest;
|
|
515
|
+
// the no-args refresh must not.
|
|
592
516
|
if (args.packages && args.packages.length > 0 && pkg) {
|
|
593
517
|
const kind = depKindFromArgs(args);
|
|
594
518
|
for (const spec of args.packages) {
|
|
@@ -598,12 +522,10 @@ async function projectInstallNative(args, signal) {
|
|
|
598
522
|
addDependencyEntry(pkg, name, finalRange, kind);
|
|
599
523
|
}
|
|
600
524
|
writePackageJson(pkgPath, pkg);
|
|
601
|
-
// Re-sync
|
|
602
|
-
// `
|
|
603
|
-
//
|
|
604
|
-
//
|
|
605
|
-
// --immutable` (reads package.json → spec `"foo@^1.2.3"`) would
|
|
606
|
-
// surface a spurious drift error.
|
|
525
|
+
// Re-sync `requested` with what `projectSpecsFromPackageJson()` returns
|
|
526
|
+
// next time. Without this, `gjsify install foo` (bare name, lockfile
|
|
527
|
+
// records `"foo"`) followed by `gjsify install --immutable` (reads
|
|
528
|
+
// package.json → `"foo@^1.2.3"`) reports a spurious drift error.
|
|
607
529
|
if (!args.immutable) {
|
|
608
530
|
syncLockfileRequested(cwd, projectSpecsFromPackageJson(pkg));
|
|
609
531
|
}
|
|
@@ -625,40 +547,31 @@ function syncLockfileRequested(cwd, specs) {
|
|
|
625
547
|
return; // Already in sync; preserve byte-stability.
|
|
626
548
|
}
|
|
627
549
|
lock.requested = specs;
|
|
628
|
-
// Atomic tmp+rename
|
|
629
|
-
// writer: a torn gjsify-lock.json breaks the next `--immutable`.
|
|
550
|
+
// Atomic tmp+rename: a torn gjsify-lock.json breaks the next `--immutable`.
|
|
630
551
|
atomicWriteStrict(lockPath, JSON.stringify(lock, null, 2) + '\n');
|
|
631
552
|
}
|
|
632
553
|
catch {
|
|
633
|
-
// Best-effort
|
|
634
|
-
// non-immutable install
|
|
554
|
+
// Best-effort; a malformed lockfile is rewritten from scratch by the next
|
|
555
|
+
// non-immutable install.
|
|
635
556
|
}
|
|
636
557
|
}
|
|
637
558
|
/**
|
|
638
|
-
* Workspace-aware install
|
|
639
|
-
*
|
|
640
|
-
*
|
|
641
|
-
*
|
|
642
|
-
* externals land in a single `node_modules/` (poor-man's hoisting —
|
|
643
|
-
* workspaces with conflicting ranges of the same dep share one hoisted
|
|
644
|
-
* copy; the resolver emits a loud `[gjsify] warning: version conflict`
|
|
645
|
-
* naming both sides and the version that won).
|
|
646
|
-
* 4. For every `workspace:` reference, symlink the target workspace's
|
|
647
|
-
* directory into the requesting workspace's `node_modules/<dep>`
|
|
648
|
-
* so `import '@gjsify/utils'` resolves to the local source.
|
|
559
|
+
* Workspace-aware install, mirroring `yarn install` at a monorepo root: discover
|
|
560
|
+
* the members, aggregate their external (non-`workspace:`) deps, install them
|
|
561
|
+
* ONCE at the root prefix, and symlink every workspace-named dep to the local
|
|
562
|
+
* source so `import '@gjsify/utils'` resolves there.
|
|
649
563
|
*
|
|
650
|
-
* Hoisting
|
|
651
|
-
*
|
|
652
|
-
* Phase D.8
|
|
564
|
+
* Hoisting is deliberately minimal — members with conflicting ranges of the same
|
|
565
|
+
* dep share one hoisted copy and the resolver warns. Per-member dedup and nested
|
|
566
|
+
* `node_modules/` are the open Phase D.8 item in status/open-todos.md.
|
|
653
567
|
*/
|
|
654
568
|
async function workspaceInstall(cwd, args, signal) {
|
|
655
|
-
//
|
|
656
|
-
//
|
|
657
|
-
//
|
|
658
|
-
//
|
|
659
|
-
//
|
|
660
|
-
//
|
|
661
|
-
// in root→child order, so there is no lock-ordering cycle.
|
|
569
|
+
// Held for the WHOLE flow: bin shims, workspace symlinks and the external
|
|
570
|
+
// install all mutate the root node_modules/, and a second concurrent workspace
|
|
571
|
+
// install interleaving its rm+symlink/extract steps corrupts both. The inner
|
|
572
|
+
// `installPackages` calls re-acquire this lock re-entrantly, or take the
|
|
573
|
+
// child-workspace lock for scoped overrides — always root→child, so there is
|
|
574
|
+
// no lock-ordering cycle.
|
|
662
575
|
const lock = await acquireInstallLock(cwd, { signal });
|
|
663
576
|
try {
|
|
664
577
|
await workspaceInstallLocked(cwd, args, signal);
|
|
@@ -675,9 +588,8 @@ async function workspaceInstallLocked(cwd, args, signal) {
|
|
|
675
588
|
}
|
|
676
589
|
const byName = new Map(workspaces.map((w) => [w.name, w]));
|
|
677
590
|
const externalSpecs = new Set();
|
|
678
|
-
//
|
|
679
|
-
//
|
|
680
|
-
// can attribute both sides of a cross-workspace range conflict.
|
|
591
|
+
// `"<name>@<range>"` → the members that declared it. Feeds the resolver's
|
|
592
|
+
// version-conflict warning so it can attribute both sides of a conflict.
|
|
681
593
|
const specOrigins = new Map();
|
|
682
594
|
const symlinks = [];
|
|
683
595
|
for (const ws of workspaces) {
|
|
@@ -689,23 +601,16 @@ async function workspaceInstallLocked(cwd, args, signal) {
|
|
|
689
601
|
for (const [depName, spec] of Object.entries(block)) {
|
|
690
602
|
if (typeof spec !== 'string')
|
|
691
603
|
continue;
|
|
692
|
-
// A
|
|
693
|
-
//
|
|
694
|
-
//
|
|
695
|
-
//
|
|
696
|
-
//
|
|
697
|
-
//
|
|
698
|
-
//
|
|
699
|
-
// against — `extractOne` would `rmSync` + drop the published
|
|
700
|
-
// tarball over the workspace's OWN source tree (the published
|
|
701
|
-
// tarball ships only `files`, so `src/**` gets wiped). Symlink
|
|
702
|
-
// it instead, exactly like a `workspace:` ref.
|
|
604
|
+
// A dep whose NAME matches a local workspace is always satisfied by
|
|
605
|
+
// that workspace, never by a same-named package on npm — whatever
|
|
606
|
+
// the spec form (`workspace:`, `^1.2.3`, a dist-tag), because that
|
|
607
|
+
// is what yarn/npm do. Routing one into the fetch/extract queue is a
|
|
608
|
+
// DATA-LOSS bug: `extractOne` would `rmSync` the workspace's own
|
|
609
|
+
// source tree and unpack the published tarball over it, and that
|
|
610
|
+
// tarball ships only `files`, so `src/**` is wiped.
|
|
703
611
|
const localWorkspace = byName.get(depName);
|
|
704
612
|
if (localWorkspace) {
|
|
705
|
-
// Only an EXPLICIT out-of-workspace protocol
|
|
706
|
-
// portal:/git+/http(s):) opts out of the local workspace. Any
|
|
707
|
-
// other shape — `workspace:` or a plain semver range/dist-tag
|
|
708
|
-
// (`^1.2.3`, `*`, `latest`) — resolves to the local package.
|
|
613
|
+
// Only an EXPLICIT out-of-workspace protocol opts out.
|
|
709
614
|
const explicitOverride = /^(link|file|portal|git\+|https?):/.test(spec);
|
|
710
615
|
if (!explicitOverride) {
|
|
711
616
|
symlinks.push({
|
|
@@ -715,11 +620,10 @@ async function workspaceInstallLocked(cwd, args, signal) {
|
|
|
715
620
|
});
|
|
716
621
|
continue;
|
|
717
622
|
}
|
|
718
|
-
// explicit override → fall through to the existing handling.
|
|
719
623
|
}
|
|
720
624
|
if (spec.startsWith('workspace:')) {
|
|
721
|
-
// `workspace:` against a name that is
|
|
722
|
-
//
|
|
625
|
+
// `workspace:` against a name that is not a discovered workspace
|
|
626
|
+
// is a hard error (typo / missing package).
|
|
723
627
|
throw new Error(`gjsify install: ${ws.name} declares "${depName}: ${spec}" but ` +
|
|
724
628
|
`no workspace with that name exists`);
|
|
725
629
|
}
|
|
@@ -737,31 +641,21 @@ async function workspaceInstallLocked(cwd, args, signal) {
|
|
|
737
641
|
}
|
|
738
642
|
}
|
|
739
643
|
console.log(`gjsify install: ${workspaces.length} workspace(s), ${externalSpecs.size} external dep spec(s), ${symlinks.length} workspace symlink(s)`);
|
|
740
|
-
//
|
|
741
|
-
//
|
|
742
|
-
//
|
|
743
|
-
//
|
|
744
|
-
//
|
|
745
|
-
//
|
|
746
|
-
// tree refreshes a stale shim even when a later phase aborts. The same
|
|
747
|
-
// shims are re-written at the end of this function so the GJS-preamble
|
|
748
|
-
// (GI_TYPELIB_PATH for native prebuilds) reflects the freshly installed
|
|
749
|
-
// tree — see the final `writeWorkspaceBinShims` call below.
|
|
644
|
+
// EARLY, before the download/extract phase: the shims derive from workspace
|
|
645
|
+
// manifests alone, while the heavy phase that follows can die mid-way (network,
|
|
646
|
+
// timeout, crash). Writing them up front means a failed install can never leave
|
|
647
|
+
// the workspace without its runner shim (`node_modules/.bin/gjsify`), and a
|
|
648
|
+
// stale shim is refreshed even when a later phase aborts. Rewritten at the end
|
|
649
|
+
// of this function so the GJS preamble reflects the installed tree.
|
|
750
650
|
writeWorkspaceBinShims(cwd, workspaces);
|
|
751
|
-
//
|
|
752
|
-
//
|
|
753
|
-
//
|
|
754
|
-
//
|
|
755
|
-
// the hoist skips entries that already exist), so it's safe to run more
|
|
756
|
-
// than once.
|
|
651
|
+
// Per-requester links (`<requester>/node_modules/<dep>`) plus a root hoist of
|
|
652
|
+
// every workspace into `node_modules/<name>`, so transitive workspace deps
|
|
653
|
+
// resolve by Node's parent-walk from any descendant. Idempotent, hence callable
|
|
654
|
+
// more than once.
|
|
757
655
|
const wireWorkspaceSymlinks = async () => {
|
|
758
|
-
//
|
|
759
|
-
//
|
|
760
|
-
//
|
|
761
|
-
// concurrency. Pure sync loops here used to dominate the tail of large
|
|
762
|
-
// installs (~793 symlinks × ~10ms each for mkdir+rm+symlink = ~24s of
|
|
763
|
-
// serial syscalls). With async + a 32-wide pool the same set lands in
|
|
764
|
-
// 1-2s.
|
|
656
|
+
// Bounded-concurrency async, because the sync version dominated the tail of
|
|
657
|
+
// large installs: ~793 symlinks × ~10 ms of mkdir+rm+symlink is ~24 s of
|
|
658
|
+
// serial syscalls, versus 1-2 s with a 32-wide pool.
|
|
765
659
|
if (symlinks.length > 0) {
|
|
766
660
|
const fsp = await import('node:fs/promises');
|
|
767
661
|
const parentDirs = new Set();
|
|
@@ -775,25 +669,20 @@ async function workspaceInstallLocked(cwd, args, signal) {
|
|
|
775
669
|
const relTarget = workspaceLinkTarget(linkPath, link.targetLocation);
|
|
776
670
|
plans.push({ linkPath, relTarget });
|
|
777
671
|
}
|
|
778
|
-
//
|
|
672
|
+
// One mkdir per unique parent — ~213 instead of ~793.
|
|
779
673
|
await Promise.all([...parentDirs].map((dir) => fsp.mkdir(dir, { recursive: true })));
|
|
780
|
-
//
|
|
781
|
-
// keeps the concurrent in-flight count bounded so we don't blow up
|
|
782
|
-
// the file-descriptor table on huge monorepos.
|
|
674
|
+
// Bounded so a huge monorepo cannot blow up the fd table.
|
|
783
675
|
const SYMLINK_CONCURRENCY = 32;
|
|
784
676
|
let cursor = 0;
|
|
785
677
|
const workers = [];
|
|
786
678
|
const wireOne = async (linkPath, relTarget) => {
|
|
787
|
-
//
|
|
788
|
-
//
|
|
789
|
-
// `{ recursive: true, force: true }` handles every shape (rm
|
|
790
|
-
// no-ops on missing paths under force; recursive covers dirs).
|
|
679
|
+
// Clear any prior entry of any shape — dir, file, broken or live
|
|
680
|
+
// symlink; `{ recursive, force }` covers all of them.
|
|
791
681
|
try {
|
|
792
682
|
await fsp.rm(linkPath, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
|
793
683
|
}
|
|
794
684
|
catch {
|
|
795
|
-
/*
|
|
796
|
-
cause if it persists. */
|
|
685
|
+
/* the symlink call below surfaces the real cause if it persists */
|
|
797
686
|
}
|
|
798
687
|
await fsp.symlink(relTarget, linkPath, WORKSPACE_LINK_TYPE);
|
|
799
688
|
};
|
|
@@ -813,37 +702,27 @@ async function workspaceInstallLocked(cwd, args, signal) {
|
|
|
813
702
|
await Promise.all(workers);
|
|
814
703
|
console.log(`gjsify install: wired ${symlinks.length} workspace symlink(s)`);
|
|
815
704
|
}
|
|
816
|
-
// Hoist EVERY workspace package to the
|
|
817
|
-
//
|
|
818
|
-
//
|
|
819
|
-
//
|
|
820
|
-
//
|
|
821
|
-
//
|
|
822
|
-
//
|
|
823
|
-
// when the consumer didn't declare a direct dep on it (auto-globals
|
|
824
|
-
// injection at build time).
|
|
825
|
-
//
|
|
826
|
-
// Without this hoist, each workspace's `node_modules/` only contains
|
|
827
|
-
// its direct declared deps, and any auto-injected register import for
|
|
828
|
-
// a workspace package the consumer didn't list as a dep externalises
|
|
829
|
-
// and the bundle fails at runtime with `Module not found`.
|
|
705
|
+
// Hoist EVERY workspace package to the root `node_modules/`, like yarn's
|
|
706
|
+
// `nodeLinker: node-modules`, so the whole graph is reachable from any
|
|
707
|
+
// descendant by parent-walk. Without it a member's `node_modules/` holds
|
|
708
|
+
// only its declared deps, and an auto-injected register import for an
|
|
709
|
+
// undeclared workspace package (auto-globals injection at build time, e.g.
|
|
710
|
+
// `@gjsify/abort-controller/register`) externalises and the bundle fails at
|
|
711
|
+
// runtime with `Module not found`.
|
|
830
712
|
const rootBinDir = join(cwd, 'node_modules');
|
|
831
713
|
let rootHoisted = 0;
|
|
832
714
|
for (const ws of workspaces) {
|
|
833
|
-
//
|
|
834
|
-
// symlink itself into its own node_modules).
|
|
715
|
+
// The root workspace cannot symlink itself into its own node_modules.
|
|
835
716
|
if (ws.location === cwd)
|
|
836
717
|
continue;
|
|
837
718
|
if (!ws.name)
|
|
838
719
|
continue;
|
|
839
720
|
const linkPath = join(rootBinDir, ws.name);
|
|
840
|
-
//
|
|
841
|
-
//
|
|
842
|
-
//
|
|
843
|
-
//
|
|
844
|
-
//
|
|
845
|
-
// EEXIST. A real directory at this path is also left alone —
|
|
846
|
-
// someone else (npm, yarn) seeded it and we shouldn't clobber.
|
|
721
|
+
// An existing symlink here (from the per-requester loop above) already
|
|
722
|
+
// points at the right place. Deliberately not remove+recreate: under
|
|
723
|
+
// GJS's Gio-backed fs polyfill `rmSync` on a symlink can race
|
|
724
|
+
// `symlinkSync` and surface EEXIST. A real directory is left alone too —
|
|
725
|
+
// npm or yarn seeded it and clobbering it is not ours to do.
|
|
847
726
|
let existsHere = false;
|
|
848
727
|
try {
|
|
849
728
|
lstatSync(linkPath);
|
|
@@ -862,35 +741,23 @@ async function workspaceInstallLocked(cwd, args, signal) {
|
|
|
862
741
|
console.log(`gjsify install: hoisted ${rootHoisted} workspace(s) to root node_modules/`);
|
|
863
742
|
}
|
|
864
743
|
};
|
|
865
|
-
//
|
|
866
|
-
//
|
|
867
|
-
//
|
|
868
|
-
//
|
|
869
|
-
//
|
|
870
|
-
// (
|
|
871
|
-
//
|
|
872
|
-
// makes workspace resolution survive a failed external phase. Safe to run
|
|
873
|
-
// before `installPackages`: that phase only fetches NON-workspace names
|
|
874
|
-
// (workspace-named deps are routed to symlinks and excluded from
|
|
875
|
-
// `externalSpecs`), and `extractOne` only `rm`s the individual external
|
|
876
|
-
// package dest it is about to write — never a workspace symlink.
|
|
744
|
+
// EARLY too, for the same reason as the bin shims: these links point at local
|
|
745
|
+
// source and never depend on the external download, but when they ran only at
|
|
746
|
+
// the tail, an interrupted external fetch left the tree with NO workspace
|
|
747
|
+
// symlinks, so cross-package imports failed to resolve even with every package
|
|
748
|
+
// on disk. Safe here because `installPackages` only fetches NON-workspace names
|
|
749
|
+
// (workspace-named deps are excluded from `externalSpecs`), and `extractOne`
|
|
750
|
+
// only `rm`s the one external dest it is about to write.
|
|
877
751
|
await wireWorkspaceSymlinks();
|
|
878
|
-
//
|
|
879
|
-
//
|
|
880
|
-
//
|
|
881
|
-
//
|
|
882
|
-
// workspace-local installs that place the overridden dep inside that
|
|
883
|
-
// workspace's own `node_modules/`. Lets a monorepo pin one workspace to
|
|
884
|
-
// an older `typescript` (e.g. a downstream integration test) without
|
|
885
|
-
// forcing the rest of the tree to the same version.
|
|
752
|
+
// `overrides` (npm) / `resolutions` (yarn). Flat `name → range` entries apply to
|
|
753
|
+
// every member; nested `<workspace> → {dep → range}` entries become
|
|
754
|
+
// workspace-local installs, so a monorepo can pin one member to an older
|
|
755
|
+
// `typescript` without dragging the rest of the tree along.
|
|
886
756
|
const rootManifest = workspaces.find((w) => w.location === cwd)?.manifest;
|
|
887
757
|
const extracted = extractOverrides(rootManifest);
|
|
888
758
|
const overrides = extracted?.global;
|
|
889
|
-
//
|
|
890
|
-
//
|
|
891
|
-
// specs will be installed into the workspace's own `node_modules/` after
|
|
892
|
-
// the root install completes, so the resolver in the root pass does NOT
|
|
893
|
-
// see the conflicting versions.
|
|
759
|
+
// Move specs carrying a workspace-scoped override out of `externalSpecs` into a
|
|
760
|
+
// per-workspace map, so the root resolver never sees the conflicting versions.
|
|
894
761
|
const wsLocalSpecs = new Map(); // wsLocation → name@range set
|
|
895
762
|
const droppedFromExternal = new Set();
|
|
896
763
|
if (extracted && extracted.scoped.size > 0) {
|
|
@@ -904,7 +771,6 @@ async function workspaceInstallLocked(cwd, args, signal) {
|
|
|
904
771
|
const override = scopedOverrideFor(ws, depName, extracted);
|
|
905
772
|
if (!override)
|
|
906
773
|
continue;
|
|
907
|
-
// Re-route this dep to the workspace's own install
|
|
908
774
|
const targetRange = override;
|
|
909
775
|
const wsKey = ws.location;
|
|
910
776
|
let bucket = wsLocalSpecs.get(wsKey);
|
|
@@ -913,17 +779,14 @@ async function workspaceInstallLocked(cwd, args, signal) {
|
|
|
913
779
|
wsLocalSpecs.set(wsKey, bucket);
|
|
914
780
|
}
|
|
915
781
|
bucket.add(`${depName}@${targetRange}`);
|
|
916
|
-
//
|
|
917
|
-
// the
|
|
918
|
-
// resolution. Note we only drop the EXACT `name@spec` the
|
|
919
|
-
// workspace declared; other workspaces' instances of the
|
|
920
|
-
// same name+spec stay in the root set.
|
|
782
|
+
// Only the EXACT `name@spec` this member declared is dropped from
|
|
783
|
+
// the root set; other members' instances of the same pair stay.
|
|
921
784
|
droppedFromExternal.add(`${depName}@${spec}`);
|
|
922
785
|
}
|
|
923
786
|
}
|
|
924
787
|
}
|
|
925
|
-
//
|
|
926
|
-
//
|
|
788
|
+
// Only drop a spec no OTHER member declared — otherwise it still has
|
|
789
|
+
// legitimate root requesters.
|
|
927
790
|
const stillNeeded = new Set();
|
|
928
791
|
for (const ws of workspaces) {
|
|
929
792
|
for (const kind of ['dependencies', 'devDependencies', 'optionalDependencies']) {
|
|
@@ -959,15 +822,14 @@ async function workspaceInstallLocked(cwd, args, signal) {
|
|
|
959
822
|
overrides,
|
|
960
823
|
signal,
|
|
961
824
|
progress,
|
|
962
|
-
// Workspace-named packages are symlinked above
|
|
963
|
-
//
|
|
964
|
-
//
|
|
965
|
-
//
|
|
825
|
+
// Workspace-named packages are symlinked above; the backend must never
|
|
826
|
+
// fetch a same-named published version over those symlinks (e.g. a
|
|
827
|
+
// `types-dev/<lib>` workspace that also exists on npm and is pinned
|
|
828
|
+
// transitively in the lockfile).
|
|
966
829
|
workspaceNames: new Set(byName.keys()),
|
|
967
830
|
specOrigins: new Map([...specOrigins].map(([k, v]) => [k, [...v]])),
|
|
968
|
-
// Aggregated over EVERY member
|
|
969
|
-
//
|
|
970
|
-
// any member declares as a plain dependency stays required).
|
|
831
|
+
// Aggregated over EVERY member, because the specs are: a name any member
|
|
832
|
+
// declares as a plain dependency stays required.
|
|
971
833
|
optionalSpecs: optionalDependencyNames(workspaces.map((w) => w.manifest)),
|
|
972
834
|
};
|
|
973
835
|
await installPackages(rootOpts);
|
|
@@ -975,10 +837,8 @@ async function workspaceInstallLocked(cwd, args, signal) {
|
|
|
975
837
|
else if (args.verbose) {
|
|
976
838
|
console.log('gjsify install: no external deps to fetch');
|
|
977
839
|
}
|
|
978
|
-
//
|
|
979
|
-
// `
|
|
980
|
-
// `node_modules/<dep>` shadows the root-hoisted version via standard
|
|
981
|
-
// Node parent-walk resolution.
|
|
840
|
+
// Scoped overrides, one `installPackages` per member: the resulting
|
|
841
|
+
// `node_modules/<dep>` shadows the root-hoisted version by parent-walk.
|
|
982
842
|
for (const [wsLocation, specSet] of wsLocalSpecs) {
|
|
983
843
|
if (specSet.size === 0)
|
|
984
844
|
continue;
|
|
@@ -990,34 +850,26 @@ async function workspaceInstallLocked(cwd, args, signal) {
|
|
|
990
850
|
prefix: wsLocation,
|
|
991
851
|
specs: [...specSet],
|
|
992
852
|
verbose: args.verbose,
|
|
993
|
-
//
|
|
994
|
-
//
|
|
853
|
+
// A thin lockfile next to the member's package.json, same `--immutable`
|
|
854
|
+
// semantics as the root install.
|
|
995
855
|
lockfile: !args.immutable,
|
|
996
856
|
frozen: args.immutable,
|
|
997
857
|
signal,
|
|
998
858
|
workspaceNames: new Set(byName.keys()),
|
|
999
|
-
// Same aggregated answer as the root install: these specs are a
|
|
1000
|
-
//
|
|
1001
|
-
//
|
|
859
|
+
// Same aggregated answer as the root install: these specs are a SUBSET of
|
|
860
|
+
// the root's, so classifying them differently would make one dep optional
|
|
861
|
+
// at the root and required in a member.
|
|
1002
862
|
optionalSpecs: optionalDependencyNames(workspaces.map((w) => w.manifest)),
|
|
1003
863
|
};
|
|
1004
864
|
await installPackages(scopedOpts);
|
|
1005
865
|
}
|
|
1006
|
-
//
|
|
1007
|
-
// installs are done. The EARLY pass above already created them so workspace
|
|
1008
|
-
// resolution survives a failed external phase; this final idempotent pass
|
|
1009
|
-
// is a cheap safety net in case any later step disturbed one (the per-link
|
|
1010
|
-
// rm+symlink re-establishes them, the root hoist skips entries that already
|
|
1011
|
-
// exist — so it's a near no-op when nothing changed).
|
|
866
|
+
// A near no-op safety net in case a later step disturbed a link.
|
|
1012
867
|
await wireWorkspaceSymlinks();
|
|
1013
|
-
//
|
|
1014
|
-
//
|
|
1015
|
-
//
|
|
1016
|
-
//
|
|
1017
|
-
//
|
|
1018
|
-
// packages are extracted). Running last also keeps workspace shims
|
|
1019
|
-
// authoritative over any same-named external bin linked by the install
|
|
1020
|
-
// backend (yarn semantics: workspace bins win).
|
|
868
|
+
// Now that the tree is materialised, refresh the shims' GJS preamble with the
|
|
869
|
+
// native prebuild dirs that only became discoverable after the install (on a
|
|
870
|
+
// fresh checkout `detectNativePackages` finds nothing before extraction).
|
|
871
|
+
// Running last also keeps workspace shims authoritative over any same-named
|
|
872
|
+
// external bin the backend linked — yarn semantics: workspace bins win.
|
|
1021
873
|
const wsBinsCreated = writeWorkspaceBinShims(cwd, workspaces);
|
|
1022
874
|
if (wsBinsCreated > 0) {
|
|
1023
875
|
console.log(`gjsify install: linked ${wsBinsCreated} workspace bin(s) into node_modules/.bin/`);
|
|
@@ -1025,47 +877,32 @@ async function workspaceInstallLocked(cwd, args, signal) {
|
|
|
1025
877
|
}
|
|
1026
878
|
/**
|
|
1027
879
|
* (Re)write the shell shims for every workspace-declared bin into the root
|
|
1028
|
-
* `node_modules/.bin/`. Without
|
|
1029
|
-
* `node_modules/.bin`-PATH consumer
|
|
1030
|
-
*
|
|
880
|
+
* `node_modules/.bin/`. Without them `npm run <script>` — or any
|
|
881
|
+
* `node_modules/.bin`-on-PATH consumer — cannot find `gjsify` on a fresh
|
|
882
|
+
* checkout; yarn writes these at install time and we match.
|
|
1031
883
|
*
|
|
1032
|
-
*
|
|
1033
|
-
*
|
|
1034
|
-
*
|
|
1035
|
-
* versus the `bin` field which typically points at `lib/index.js`
|
|
1036
|
-
* (a build artifact that may not yet exist). The shim wraps the
|
|
1037
|
-
* target in a shell script that picks the right interpreter (`gjs -m`
|
|
1038
|
-
* for `.mjs` bundles, `node` for `.js` files).
|
|
884
|
+
* `gjsify.bin` is preferred for the GJS target because it names the committed
|
|
885
|
+
* `dist/cli.gjs.mjs` bundle, which exists on a fresh checkout, while `bin`
|
|
886
|
+
* typically names `lib/index.js`, a build artifact that may not.
|
|
1039
887
|
*
|
|
1040
|
-
* IDEMPOTENT + UNCONDITIONAL: shims are derived artifacts
|
|
1041
|
-
*
|
|
1042
|
-
*
|
|
1043
|
-
* once BEFORE the download/extract phase (so a failed/aborted install can
|
|
1044
|
-
* never leave the workspace without its runner shim, and a stale shim is
|
|
1045
|
-
* refreshed even when a later phase dies) and once after (to pick up
|
|
1046
|
-
* native prebuild dirs that only exist post-extract).
|
|
888
|
+
* IDEMPOTENT + UNCONDITIONAL: shims are derived artifacts, always regenerated,
|
|
889
|
+
* never skipped for existing and never fatal for missing. `workspaceInstall`
|
|
890
|
+
* calls this before AND after the download/extract phase.
|
|
1047
891
|
*/
|
|
1048
892
|
function writeWorkspaceBinShims(cwd, workspaces) {
|
|
1049
893
|
const wsBinDir = join(cwd, 'node_modules', '.bin');
|
|
1050
|
-
//
|
|
1051
|
-
//
|
|
1052
|
-
//
|
|
1053
|
-
//
|
|
1054
|
-
// directly, with no chance to set env after the fact, so without this
|
|
1055
|
-
// preamble `imports.gi.GjsifyTerminal` etc. fail and process.stdout
|
|
1056
|
-
// collapses to no-color, 80-col defaults.
|
|
894
|
+
// The shim invokes the CLI bundle via `gjs -m` directly, with no chance to set
|
|
895
|
+
// env after the fact, so GI_TYPELIB_PATH / LD_LIBRARY_PATH have to be in its
|
|
896
|
+
// preamble — otherwise `imports.gi.GjsifyTerminal` and friends fail and
|
|
897
|
+
// process.stdout collapses to no-color, 80-col defaults.
|
|
1057
898
|
const nativePrebuildDirs = detectNativePackages(cwd).map((p) => p.prebuildsDir);
|
|
1058
899
|
let wsBinsCreated = 0;
|
|
1059
900
|
for (const ws of workspaces) {
|
|
1060
901
|
const m = ws.manifest;
|
|
1061
902
|
const gjsifyBin = m.gjsify?.bin;
|
|
1062
903
|
const nodeBin = m.bin;
|
|
1063
|
-
//
|
|
1064
|
-
//
|
|
1065
|
-
// because Node's child_process is more reliable than GJS's
|
|
1066
|
-
// Gio.Subprocess polyfill (parallel-spawn close-event delivery
|
|
1067
|
-
// races under heavy concurrency); GJS is the fallback for fresh
|
|
1068
|
-
// checkouts where the Node target hasn't been built yet.
|
|
904
|
+
// Collect both the Node and GJS target per bin name when they exist; which
|
|
905
|
+
// one the shim tries first is `buildBinShim`'s per-platform decision.
|
|
1069
906
|
const merged = mergeWorkspaceBins(ws.name, gjsifyBin, nodeBin);
|
|
1070
907
|
if (merged.size === 0)
|
|
1071
908
|
continue;
|
|
@@ -1082,10 +919,10 @@ function writeWorkspaceBinShims(cwd, workspaces) {
|
|
|
1082
919
|
}
|
|
1083
920
|
writeFileSync(linkPath, buildBinShim(ws.location, nodeTarget, gjsTarget, nativePrebuildDirs, process.platform, cwd), { mode: 0o755 });
|
|
1084
921
|
chmodSync(linkPath, 0o755);
|
|
1085
|
-
// Windows executes neither an extension-less file nor a `#!` line
|
|
1086
|
-
//
|
|
1087
|
-
//
|
|
1088
|
-
// npm's cmd-shim
|
|
922
|
+
// Windows executes neither an extension-less file nor a `#!` line, so the
|
|
923
|
+
// sh shim above is reachable only from git-bash/MSYS/WSL. The
|
|
924
|
+
// `.cmd`/`.ps1` companions are what cmd.exe and pwsh need, mirroring
|
|
925
|
+
// npm's cmd-shim.
|
|
1089
926
|
if (process.platform === 'win32') {
|
|
1090
927
|
for (const [suffix, contents] of Object.entries(buildWorkspaceBinShimsForWindows(ws.location, nodeTarget, gjsTarget, nativePrebuildDirs))) {
|
|
1091
928
|
writeFileSync(`${linkPath}.${suffix}`, contents, { mode: 0o755 });
|
|
@@ -1096,6 +933,19 @@ function writeWorkspaceBinShims(cwd, workspaces) {
|
|
|
1096
933
|
}
|
|
1097
934
|
return wsBinsCreated;
|
|
1098
935
|
}
|
|
936
|
+
/**
|
|
937
|
+
* Flatten npm `overrides` / yarn `resolutions` into a bare name → range map,
|
|
938
|
+
* accepting both `{ "typescript": "~5.9.2" }` and yarn's pattern form
|
|
939
|
+
* `{ "typescript@*": "~5.9.2" }`.
|
|
940
|
+
*
|
|
941
|
+
* Pattern keys with a version glob are normalised to the bare name because the
|
|
942
|
+
* resolver has no per-incoming-range scoping. npm's per-parent nested shape
|
|
943
|
+
* (`"foo": { ".": "1.0", "bar": "2.0" }`) is warned about rather than
|
|
944
|
+
* half-applied, since without per-parent support it would misbehave silently.
|
|
945
|
+
*
|
|
946
|
+
* Keys beginning with `_` are skipped — the in-the-wild convention for
|
|
947
|
+
* documentation entries like `"_comment_typescript"`.
|
|
948
|
+
*/
|
|
1099
949
|
function extractOverrides(rootManifest) {
|
|
1100
950
|
if (!rootManifest)
|
|
1101
951
|
return undefined;
|
|
@@ -1108,9 +958,8 @@ function extractOverrides(rootManifest) {
|
|
|
1108
958
|
if (key.startsWith('_'))
|
|
1109
959
|
continue;
|
|
1110
960
|
if (typeof value === 'string') {
|
|
1111
|
-
//
|
|
1112
|
-
//
|
|
1113
|
-
// leading `@`.
|
|
961
|
+
// Normalise pattern keys (`name@*`, `name@^range`) to the bare name,
|
|
962
|
+
// preserving the leading `@` of a scoped package.
|
|
1114
963
|
let name = key;
|
|
1115
964
|
const atIdx = key.startsWith('@') ? key.indexOf('@', 1) : key.indexOf('@');
|
|
1116
965
|
if (atIdx > 0)
|
|
@@ -1119,9 +968,8 @@ function extractOverrides(rootManifest) {
|
|
|
1119
968
|
continue;
|
|
1120
969
|
}
|
|
1121
970
|
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
1122
|
-
//
|
|
1123
|
-
//
|
|
1124
|
-
// selectors collapsed to per-workspace level.
|
|
971
|
+
// `<workspace> → {dep → range}`: npm's nested overrides shape and
|
|
972
|
+
// yarn's resolutions selectors, collapsed to per-workspace level.
|
|
1125
973
|
const sub = {};
|
|
1126
974
|
for (const [depKey, depValue] of Object.entries(value)) {
|
|
1127
975
|
if (depKey.startsWith('_'))
|
|
@@ -1151,10 +999,7 @@ function extractOverrides(rootManifest) {
|
|
|
1151
999
|
return undefined;
|
|
1152
1000
|
return { global, scoped };
|
|
1153
1001
|
}
|
|
1154
|
-
/**
|
|
1155
|
-
* Look up the scoped override for a given workspace + dep. Matches by
|
|
1156
|
-
* workspace name OR relative location (both accepted for readability).
|
|
1157
|
-
*/
|
|
1002
|
+
/** Scoped override for a workspace + dep, matched by name OR relative location. */
|
|
1158
1003
|
function scopedOverrideFor(ws, depName, extracted) {
|
|
1159
1004
|
if (!extracted || extracted.scoped.size === 0)
|
|
1160
1005
|
return undefined;
|
|
@@ -1166,66 +1011,45 @@ function scopedOverrideFor(ws, depName, extracted) {
|
|
|
1166
1011
|
}
|
|
1167
1012
|
return undefined;
|
|
1168
1013
|
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Build the `sh` shim for one bin. Which interpreter it tries first is decided
|
|
1016
|
+
* per platform below; the probe is per-INVOCATION, not per-install, so the same
|
|
1017
|
+
* shim works before and after the workspace's `lib/` has been built.
|
|
1018
|
+
*
|
|
1019
|
+
* Both targets are absolute so the shim survives the different cwds consumers
|
|
1020
|
+
* (`yarn run`, `npm run`, a direct PATH invocation) call it from.
|
|
1021
|
+
*/
|
|
1169
1022
|
export function buildBinShim(wsLocation, nodeTarget, gjsTarget, nativePrebuildDirs = [], platform = process.platform, scanRoot = wsLocation) {
|
|
1170
1023
|
const nodeAbs = nodeTarget ? join(wsLocation, nodeTarget) : null;
|
|
1171
1024
|
const gjsAbs = gjsTarget ? join(wsLocation, gjsTarget) : null;
|
|
1172
|
-
//
|
|
1173
|
-
// export to the gjs branch, keeping the shim minimal when no native pkgs
|
|
1174
|
-
// exist or only the Node bin is in play.
|
|
1025
|
+
// Scoped to the gjs branch because Node ignores GI_TYPELIB_PATH.
|
|
1175
1026
|
//
|
|
1176
|
-
// `scanRoot` is the INSTALL prefix (the tree holding `node_modules/.bin`),
|
|
1177
|
-
//
|
|
1178
|
-
//
|
|
1179
|
-
// written, so `gjsify install <
|
|
1180
|
-
// a re-link. See `buildNativeEnvPreamble`.
|
|
1027
|
+
// `scanRoot` is the INSTALL prefix (the tree holding `node_modules/.bin`), not
|
|
1028
|
+
// the workspace package dir: `buildNativeEnvPreamble` globs its `node_modules`
|
|
1029
|
+
// at LAUNCH time rather than embedding what was installed the day the shim was
|
|
1030
|
+
// written, so a later `gjsify install <x>-native` is picked up without a re-link.
|
|
1181
1031
|
const gjsPreamble = buildNativeEnvPreamble(scanRoot, nativePrebuildDirs, { platform });
|
|
1182
1032
|
if (nodeAbs && gjsAbs) {
|
|
1183
|
-
// GJS-FIRST:
|
|
1184
|
-
//
|
|
1185
|
-
//
|
|
1186
|
-
//
|
|
1033
|
+
// GJS-FIRST by default: gjsify is a GJS-first toolchain that stays fully
|
|
1034
|
+
// runnable under Node. This is only safe because `gjsify run` dispatches a
|
|
1035
|
+
// single `gjsify <cmd>` IN-PROCESS under GJS (see cli-app.ts) instead of
|
|
1036
|
+
// spawning another gjs per script — the earlier gjs-first attempt chained ~5
|
|
1037
|
+
// heavyweight gjs per package and turned a ~83s build into a multi-hour hang
|
|
1038
|
+
// on CI's few cores.
|
|
1187
1039
|
//
|
|
1188
|
-
//
|
|
1189
|
-
//
|
|
1190
|
-
//
|
|
1191
|
-
//
|
|
1192
|
-
//
|
|
1193
|
-
//
|
|
1194
|
-
//
|
|
1195
|
-
//
|
|
1196
|
-
//
|
|
1197
|
-
// completes in ~83s under GJS (was a multi-hour hang).
|
|
1198
|
-
// …EXCEPT on macOS and Windows, where the order is inverted — and not
|
|
1199
|
-
// as a preference but as a capability. GJS is a GNOME-desktop host: on
|
|
1200
|
-
// those two platforms it is at best a Homebrew/MSYS afterthought, while
|
|
1201
|
-
// Node is what a developer already has. The tooling agrees:
|
|
1040
|
+
// macOS and Windows invert the order. Windows is a capability limit: there is
|
|
1041
|
+
// no prebuilt `libgjs` at all (website/src/content/docs/platform-support.md),
|
|
1042
|
+
// so the gjs probe can never succeed, and leading with Node also makes the
|
|
1043
|
+
// `sh` shim agree with the Node-only `.cmd`/`.ps1` companions
|
|
1044
|
+
// {@link buildLauncherShims} writes. macOS is NOT: `@gjsify/rolldown-native`
|
|
1045
|
+
// now ships darwin-arm64 + darwin-x64 prebuilds, so the original reason (no
|
|
1046
|
+
// bundler engine under GJS there, npm `rolldown` being a Rust napi crate GJS
|
|
1047
|
+
// cannot load) has expired, and only the "Homebrew gjs is an afterthought"
|
|
1048
|
+
// preference is left holding `darwin` in this branch.
|
|
1202
1049
|
//
|
|
1203
|
-
//
|
|
1204
|
-
//
|
|
1205
|
-
//
|
|
1206
|
-
// So the gjs branch cannot run `gjsify build`, nor any script that
|
|
1207
|
-
// ends in one — it dies with "no usable bundler engine under GJS",
|
|
1208
|
-
// whose own closing line is "Running the build under Node also
|
|
1209
|
-
// works". Preferring gjs routed the user into a dead end the error
|
|
1210
|
-
// message tells them to leave while the shim offered no way out.
|
|
1211
|
-
// Measured on a cold macOS tree: root `build:infra` fails at the
|
|
1212
|
-
// first `gjsify build --library` (`@gjsify/utils`) with the
|
|
1213
|
-
// gjs-first shim and completes with this one.
|
|
1214
|
-
// - Windows: there is no prebuilt `libgjs` at all (see
|
|
1215
|
-
// `website/src/content/docs/platform-support.md`), so the gjs probe
|
|
1216
|
-
// can never succeed. This also makes the `sh` shim — the git-bash /
|
|
1217
|
-
// MSYS entry point — agree with the `.cmd`/`.ps1` companions
|
|
1218
|
-
// {@link buildLauncherShims} already writes as Node-only, instead
|
|
1219
|
-
// of leading with a branch that is dead by construction.
|
|
1220
|
-
//
|
|
1221
|
-
// The probe stays symmetric with the gjs one — `command -v node`, not a
|
|
1222
|
-
// bare file test — so a host carrying the bundle but no Node still
|
|
1223
|
-
// falls through to gjs and keeps every non-build command working. That
|
|
1224
|
-
// is what keeps GJS on these platforms supported-and-tested rather than
|
|
1225
|
-
// merely absent. Revert a platform to the shared gjs-first form once it
|
|
1226
|
-
// has a working engine (macOS: status/open-todos.md,
|
|
1227
|
-
// "@gjsify/rolldown-native's darwin-arm64 leg is PROVEN but not
|
|
1228
|
-
// promoted").
|
|
1050
|
+
// The Node probe stays symmetric with the gjs one — `command -v node`, not a
|
|
1051
|
+
// bare file test — so a host carrying the bundle but no Node still falls
|
|
1052
|
+
// through to gjs and keeps every non-build command working.
|
|
1229
1053
|
const nodeFirstPlatform = platform === 'darwin' || platform === 'win32';
|
|
1230
1054
|
const gjsFirst = `if command -v gjs >/dev/null 2>&1 && [ -f "${gjsAbs}" ]; then\n` +
|
|
1231
1055
|
`${gjsPreamble}exec gjs -m "${gjsAbs}" "$@"\n` +
|
|
@@ -1246,10 +1070,9 @@ export function buildBinShim(wsLocation, nodeTarget, gjsTarget, nativePrebuildDi
|
|
|
1246
1070
|
/**
|
|
1247
1071
|
* `.cmd` / `.ps1` companions for {@link buildBinShim}.
|
|
1248
1072
|
*
|
|
1249
|
-
*
|
|
1250
|
-
*
|
|
1251
|
-
*
|
|
1252
|
-
* both targets resolves to Node here, and a GJS-only workspace gets an honest
|
|
1073
|
+
* There is no GJS host on Windows at all (no prebuilt `libgjs` — see
|
|
1074
|
+
* `website/src/content/docs/platform-support.md`), so a workspace shipping both
|
|
1075
|
+
* targets resolves to Node here, and a GJS-only workspace gets an honest
|
|
1253
1076
|
* `gjs`-invoking shim that fails exactly the way the `sh` one does.
|
|
1254
1077
|
*
|
|
1255
1078
|
* Exported + platform-free so it is unit-tested from Linux.
|
|
@@ -1260,10 +1083,9 @@ export function buildWorkspaceBinShimsForWindows(wsLocation, nodeTarget, gjsTarg
|
|
|
1260
1083
|
if (nodeAbs)
|
|
1261
1084
|
return buildLauncherShims({ interpreter: 'node', target: nodeAbs });
|
|
1262
1085
|
if (gjsAbs) {
|
|
1263
|
-
// Windows has no dedicated library-path variable — LoadLibrary searches
|
|
1264
|
-
//
|
|
1265
|
-
//
|
|
1266
|
-
// GI_TYPELIB_PATH).
|
|
1086
|
+
// Windows has no dedicated library-path variable — LoadLibrary searches PATH
|
|
1087
|
+
// — so the prebuild dirs go there, `;`-separated, which is also GLib's
|
|
1088
|
+
// G_SEARCHPATH_SEPARATOR on Windows, hence the same join for GI_TYPELIB_PATH.
|
|
1267
1089
|
const joined = nativePrebuildDirs.join(';');
|
|
1268
1090
|
return buildLauncherShims({
|
|
1269
1091
|
interpreter: 'gjs',
|
|
@@ -1275,10 +1097,8 @@ export function buildWorkspaceBinShimsForWindows(wsLocation, nodeTarget, gjsTarg
|
|
|
1275
1097
|
throw new Error('buildWorkspaceBinShimsForWindows: either nodeTarget or gjsTarget must be provided');
|
|
1276
1098
|
}
|
|
1277
1099
|
/**
|
|
1278
|
-
*
|
|
1279
|
-
*
|
|
1280
|
-
* shim built from this picks Node at runtime when its target exists,
|
|
1281
|
-
* GJS otherwise.
|
|
1100
|
+
* Merge a workspace's `bin` (Node) and `gjsify.bin` (GJS) declarations into one
|
|
1101
|
+
* `<binName> → {nodeTarget?, gjsTarget?}` map for {@link buildBinShim}.
|
|
1282
1102
|
*/
|
|
1283
1103
|
function mergeWorkspaceBins(pkgName, gjsifyBin, nodeBin) {
|
|
1284
1104
|
const out = new Map();
|
|
@@ -1327,11 +1147,10 @@ async function projectInstallViaNpm(args) {
|
|
|
1327
1147
|
await spawnNpm(npmArgs);
|
|
1328
1148
|
}
|
|
1329
1149
|
async function spawnNpm(npmArgs) {
|
|
1330
|
-
// `completion: 'return'` — the `--backend npm` branch
|
|
1331
|
-
//
|
|
1332
|
-
//
|
|
1333
|
-
//
|
|
1334
|
-
// blocking path costs nothing in practice.
|
|
1150
|
+
// `completion: 'return'` — the `--backend npm` branch RETURNS after
|
|
1151
|
+
// `runPostInstallChecks()` instead of exiting, so under GJS it must not leave the
|
|
1152
|
+
// main loop arms `spawn()` parks (see utils/spawn.ts). The npm backend needs a
|
|
1153
|
+
// Node host anyway, so the blocking path costs nothing here.
|
|
1335
1154
|
return spawnToCompletion('npm', npmArgs, {
|
|
1336
1155
|
completion: 'return',
|
|
1337
1156
|
notFound: () => new Error('npm not found on PATH — install Node.js first.'),
|
|
@@ -1341,9 +1160,8 @@ async function spawnNpm(npmArgs) {
|
|
|
1341
1160
|
throw new Error(`npm install exited with code ${code}`);
|
|
1342
1161
|
})
|
|
1343
1162
|
.catch((err) => {
|
|
1344
|
-
// A raw spawn failure still carries an errno `code`; the mapped
|
|
1345
|
-
//
|
|
1346
|
-
// read as finished sentences.
|
|
1163
|
+
// A raw spawn failure still carries an errno `code`; the mapped not-found
|
|
1164
|
+
// hint and the non-zero-exit error do not, and already read as sentences.
|
|
1347
1165
|
const errno = err.code;
|
|
1348
1166
|
console.error(errno === undefined ? err.message : `npm install failed: ${err.message}`);
|
|
1349
1167
|
process.exit(1);
|
|
@@ -1354,11 +1172,10 @@ async function installGlobalAndLink(specs, opts) {
|
|
|
1354
1172
|
mkdirSync(layout.prefix, { recursive: true });
|
|
1355
1173
|
console.log(`gjsify install --global → ${layout.prefix}`);
|
|
1356
1174
|
console.log(` bins → ${layout.binDir}`);
|
|
1357
|
-
// The global prefix is SHARED user-wide state
|
|
1358
|
-
//
|
|
1359
|
-
//
|
|
1360
|
-
//
|
|
1361
|
-
// the install, the engine-package top-up AND the bin linking (ADR 0001).
|
|
1175
|
+
// The global prefix is SHARED user-wide state: two concurrent `gjsify install -g`
|
|
1176
|
+
// runs interleaved rm+extract on the same package dirs and raced the launcher
|
|
1177
|
+
// writes in ~/.local/bin. The lock spans the install, the engine top-up AND the
|
|
1178
|
+
// bin linking (ADR 0001).
|
|
1362
1179
|
const lock = await acquireInstallLock(layout.prefix, {});
|
|
1363
1180
|
try {
|
|
1364
1181
|
const result = await installPackages({
|
|
@@ -1367,17 +1184,16 @@ async function installGlobalAndLink(specs, opts) {
|
|
|
1367
1184
|
verbose: opts.verbose,
|
|
1368
1185
|
});
|
|
1369
1186
|
const packageNames = specs.map(specToPackageName);
|
|
1370
|
-
// A global
|
|
1371
|
-
//
|
|
1372
|
-
//
|
|
1373
|
-
//
|
|
1374
|
-
//
|
|
1375
|
-
//
|
|
1376
|
-
//
|
|
1377
|
-
//
|
|
1378
|
-
//
|
|
1379
|
-
//
|
|
1380
|
-
// engine's prebuild dirs into the wrapper's GI_TYPELIB_PATH/LD_LIBRARY_PATH.
|
|
1187
|
+
// A global install of `@gjsify/cli` must also lay down the GJS bundler engine
|
|
1188
|
+
// + the sibling format/CSS bridges. They are OPTIONAL PEERS of the CLI (so a
|
|
1189
|
+
// plain `npm install @gjsify/cli` on Node does not force a prebuild) and the
|
|
1190
|
+
// native backend does not resolve peerDependencies at all, so without this
|
|
1191
|
+
// they never arrive and `gjsify build` hard-fails under GJS with "no usable
|
|
1192
|
+
// bundler engine". Pinned to the CLI's resolved version so they move in
|
|
1193
|
+
// lockstep with the bundle; a platform with no published prebuild warns.
|
|
1194
|
+
//
|
|
1195
|
+
// Must run BEFORE linkGlobalBins, so the launcher's detectNativePackages()
|
|
1196
|
+
// bakes the engine's prebuild dirs into GI_TYPELIB_PATH/LD_LIBRARY_PATH.
|
|
1381
1197
|
if (packageNames.includes('@gjsify/cli')) {
|
|
1382
1198
|
const cliVersion = result.installed.find((r) => r.name === '@gjsify/cli')?.version ?? 'latest';
|
|
1383
1199
|
await installGjsEnginePackages(layout.prefix, cliVersion, { verbose: opts.verbose });
|
|
@@ -1404,6 +1220,42 @@ function reportLinkedBins(created, binDir) {
|
|
|
1404
1220
|
`Add it to your shell rc file:\n export PATH="${binDir}:$PATH"`);
|
|
1405
1221
|
}
|
|
1406
1222
|
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Lay the GJS bundler engine down after a PROJECT install, when the host will need
|
|
1225
|
+
* it and does not have it (#1005).
|
|
1226
|
+
*
|
|
1227
|
+
* The engine is an OPTIONAL PEER of `@gjsify/cli` on purpose — a plain
|
|
1228
|
+
* `npm install @gjsify/cli` on Node must not fetch prebuilds — but nobody resolves
|
|
1229
|
+
* it for a project: npm 7+ skips optional peers and the native backend does not
|
|
1230
|
+
* resolve `peerDependencies` at all. Under GJS there is no npm `rolldown`
|
|
1231
|
+
* fallback, so `gjsify build` had nothing to load.
|
|
1232
|
+
*
|
|
1233
|
+
* Four conditions, and (b) is the one worth reading:
|
|
1234
|
+
*
|
|
1235
|
+
* (a) `@gjsify/cli` is in this tree — otherwise the engine has no caller.
|
|
1236
|
+
* (b) THE HOST CAN RUN GJS, which is NOT the `isGjs()` question. `isGjs()` asks
|
|
1237
|
+
* which runtime is executing the installer, but the launcher prefers the GJS
|
|
1238
|
+
* bundle whenever `command -v gjs` succeeds, so a tree installed BY NODE is
|
|
1239
|
+
* routinely built BY GJS. Gating on the interpreter reproduces #1005 for
|
|
1240
|
+
* every contributor who installs on Node and builds through the shim.
|
|
1241
|
+
* `runMinimalChecks()` already probed for gjs, so this costs no extra spawn.
|
|
1242
|
+
* (c) No engine is already reachable — asked with the same walk the BUILD uses.
|
|
1243
|
+
* (d) Not `--immutable`: a frozen install cannot acquire what its lockfile does
|
|
1244
|
+
* not name, so that case warns and installs nothing.
|
|
1245
|
+
*
|
|
1246
|
+
* NOT gated on `gjsify.app` or on "declares a build script": neither failing
|
|
1247
|
+
* consumer declares the former, and the latter means pattern-matching script
|
|
1248
|
+
* bodies, the drifting hand-maintained judgement ADR 0017 rejects by name.
|
|
1249
|
+
* ADR 0020 records the simpler `optionalDependencies` shape that would retire
|
|
1250
|
+
* this policy entirely.
|
|
1251
|
+
*
|
|
1252
|
+
* NOT recorded in the lockfile — `installPackages` writes one only when both
|
|
1253
|
+
* `resolved` and `opts.lockfile` hold — so a consumer's `gjsify-lock.json` is
|
|
1254
|
+
* never rewritten by this; an e2e row pins it. Ordering is free here, unlike the
|
|
1255
|
+
* global path: `buildNativeEnvPreamble` scans the prefix at LAUNCH time, so a shim
|
|
1256
|
+
* written before the engine arrives still exports its dir
|
|
1257
|
+
* (`tests/e2e/global-install-engine/`).
|
|
1258
|
+
*/
|
|
1407
1259
|
export async function ensureProjectGjsEngine(args, gjsFound, deps = {}) {
|
|
1408
1260
|
const cwd = deps.cwd ?? process.cwd();
|
|
1409
1261
|
const hasEngine = deps.hasEngineFn ?? hasBundlerEngineInstalled;
|
|
@@ -1414,9 +1266,8 @@ export async function ensureProjectGjsEngine(args, gjsFound, deps = {}) {
|
|
|
1414
1266
|
if (hasEngine(cwd))
|
|
1415
1267
|
return;
|
|
1416
1268
|
if (args.immutable) {
|
|
1417
|
-
// Name the DURABLE fix
|
|
1418
|
-
//
|
|
1419
|
-
// to put the engine in the lockfile.
|
|
1269
|
+
// Name the DURABLE fix: a frozen CI install cannot be repaired by anything
|
|
1270
|
+
// happening now, so the answer is to put the engine in the lockfile.
|
|
1420
1271
|
console.warn(`\nWarning: \`gjsify build\` will fail under GJS — ${BUNDLER_ENGINE_PACKAGE} is not installed, and\n` +
|
|
1421
1272
|
' --immutable cannot add it (the lockfile does not name it). Declare it so the lockfile carries it:\n' +
|
|
1422
1273
|
` gjsify install ${BUNDLER_ENGINE_PACKAGE}\n` +
|
|
@@ -1432,7 +1283,6 @@ export async function ensureProjectGjsEngine(args, gjsFound, deps = {}) {
|
|
|
1432
1283
|
}
|
|
1433
1284
|
async function runPostInstallChecks(args) {
|
|
1434
1285
|
console.log('\n--- gjsify post-install checks ---');
|
|
1435
|
-
// 1. System deps that GJS apps typically need.
|
|
1436
1286
|
const results = runMinimalChecks();
|
|
1437
1287
|
const missing = results.filter((r) => !r.found && r.severity === 'required');
|
|
1438
1288
|
if (missing.length > 0) {
|
|
@@ -1448,14 +1298,9 @@ async function runPostInstallChecks(args) {
|
|
|
1448
1298
|
else {
|
|
1449
1299
|
console.log('System dependencies OK.');
|
|
1450
1300
|
}
|
|
1451
|
-
//
|
|
1452
|
-
//
|
|
1301
|
+
// Before the prebuild report, so the report reflects what the tree ACTUALLY has:
|
|
1302
|
+
// an engine installed a moment ago belongs in the "Detected N …" list.
|
|
1453
1303
|
await ensureProjectGjsEngine(args, results.find((r) => r.id === 'gjs')?.found ?? false);
|
|
1454
|
-
// 3. Surface @gjsify/* packages with native prebuilds — `gjsify run`
|
|
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.
|
|
1459
1304
|
const native = detectNativePackages(process.cwd());
|
|
1460
1305
|
if (native.length > 0) {
|
|
1461
1306
|
console.log(`\nDetected ${native.length} @gjsify/* package(s) with native prebuilds:`);
|
|
@@ -1464,37 +1309,28 @@ async function runPostInstallChecks(args) {
|
|
|
1464
1309
|
}
|
|
1465
1310
|
console.log('\nUse `gjsify run <bundle>` to launch with LD_LIBRARY_PATH/GI_TYPELIB_PATH set.');
|
|
1466
1311
|
}
|
|
1467
|
-
// 3. Install workspace git hooks (only fires inside the gjsify monorepo
|
|
1468
|
-
// itself, NOT in consumer projects that depend on @gjsify/cli — gated
|
|
1469
|
-
// by the presence of `scripts/install-git-hooks.mjs` + a `.git`
|
|
1470
|
-
// checkout). Idempotent; safe to re-run on every install.
|
|
1471
1312
|
maybeInstallGitHooks();
|
|
1472
1313
|
}
|
|
1473
1314
|
/**
|
|
1474
|
-
* Wire `core.hooksPath = .githooks` when
|
|
1475
|
-
*
|
|
1476
|
-
*
|
|
1477
|
-
*
|
|
1478
|
-
*
|
|
1479
|
-
* The script itself handles its own no-op cases (extracted tarball, already
|
|
1480
|
-
* configured, SKIP_GJSIFY_HOOKS=1).
|
|
1315
|
+
* Wire `core.hooksPath = .githooks` when `gjsify install` runs inside a git
|
|
1316
|
+
* checkout that ships `scripts/install-git-hooks.mjs` — i.e. the gjsify monorepo
|
|
1317
|
+
* and not a consumer project, which has no hooks to install. Idempotent; the
|
|
1318
|
+
* script handles its own no-op cases (extracted tarball, already configured,
|
|
1319
|
+
* `SKIP_GJSIFY_HOOKS=1`).
|
|
1481
1320
|
*/
|
|
1482
1321
|
function maybeInstallGitHooks() {
|
|
1483
1322
|
const cwd = process.cwd();
|
|
1484
1323
|
const scriptPath = join(cwd, 'scripts', 'install-git-hooks.mjs');
|
|
1485
1324
|
if (!existsSync(scriptPath))
|
|
1486
1325
|
return;
|
|
1487
|
-
//
|
|
1488
|
-
// avoids spawning a process when we know the answer.
|
|
1326
|
+
// The script also checks, but skipping here avoids a spawn.
|
|
1489
1327
|
if (!existsSync(join(cwd, '.git')))
|
|
1490
1328
|
return;
|
|
1491
1329
|
try {
|
|
1492
1330
|
// `nodeBinary()`, NOT `process.execPath`: under the committed GJS bundle
|
|
1493
|
-
// `process.execPath` is `gjs`, so `spawnSync(process.execPath, [script])`
|
|
1494
|
-
//
|
|
1495
|
-
//
|
|
1496
|
-
// (PATH `node` under GJS, the current node otherwise). Best-effort —
|
|
1497
|
-
// wrapped in try/catch, so a missing node just warns and continues.
|
|
1331
|
+
// `process.execPath` is `gjs`, so `spawnSync(process.execPath, [script])` ran
|
|
1332
|
+
// `gjs cli.gjs.mjs install-git-hooks.mjs` — wrong argv. The hook installer is
|
|
1333
|
+
// a plain Node ESM script and needs a real node binary.
|
|
1498
1334
|
const result = spawnSync(nodeBinary(), [scriptPath, '--quiet'], {
|
|
1499
1335
|
cwd,
|
|
1500
1336
|
stdio: 'inherit',
|
|
@@ -1505,8 +1341,8 @@ function maybeInstallGitHooks() {
|
|
|
1505
1341
|
}
|
|
1506
1342
|
}
|
|
1507
1343
|
catch (err) {
|
|
1508
|
-
//
|
|
1509
|
-
//
|
|
1344
|
+
// A quality-of-life touchup, not an install requirement — never let it abort
|
|
1345
|
+
// the surrounding install.
|
|
1510
1346
|
console.warn(`[gjsify install] git hook installation skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
1511
1347
|
}
|
|
1512
1348
|
}
|