@gjsify/cli 0.52.0 → 0.53.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 +329 -298
- package/lib/actions/build.d.ts +22 -0
- package/lib/actions/build.js +37 -9
- package/lib/bundler-pick.d.ts +37 -1
- package/lib/bundler-pick.js +108 -17
- package/lib/cli-app.js +5 -2
- package/lib/commands/env.d.ts +31 -0
- package/lib/commands/env.js +114 -0
- package/lib/commands/exec.d.ts +10 -0
- package/lib/commands/exec.js +208 -0
- package/lib/commands/foreach.js +5 -97
- package/lib/commands/format.js +26 -6
- package/lib/commands/index.d.ts +3 -0
- package/lib/commands/index.js +3 -0
- package/lib/commands/install.d.ts +2 -2
- package/lib/commands/install.js +5 -5
- package/lib/commands/self-update.js +2 -1
- package/lib/commands/test.d.ts +18 -9
- package/lib/commands/test.js +56 -11
- package/lib/commands/tsc.js +4 -4
- package/lib/commands/upgrade.d.ts +15 -0
- package/lib/commands/upgrade.js +76 -2
- package/lib/commands/webext/index.d.ts +2 -0
- package/lib/commands/webext/index.js +15 -0
- package/lib/commands/webext/subcommands.d.ts +16 -0
- package/lib/commands/webext/subcommands.js +204 -0
- package/lib/types/config-data.d.ts +1 -1
- package/lib/utils/bin-shim.d.ts +8 -2
- package/lib/utils/bin-shim.js +6 -5
- package/lib/utils/bundle-inputs.d.ts +84 -0
- package/lib/utils/bundle-inputs.js +268 -0
- package/lib/utils/check-system-deps.d.ts +4 -2
- package/lib/utils/check-system-deps.js +21 -26
- package/lib/utils/dev-link.d.ts +9 -6
- package/lib/utils/dev-link.js +71 -19
- package/lib/utils/exec-bin.d.ts +137 -0
- package/lib/utils/exec-bin.js +321 -0
- package/lib/utils/gi-runtime-paths.js +3 -2
- package/lib/utils/gitignore.d.ts +15 -0
- package/lib/utils/gitignore.js +178 -0
- package/lib/utils/gjsify-shim.d.ts +47 -0
- package/lib/utils/gjsify-shim.js +97 -8
- package/lib/utils/install-backend-native.d.ts +19 -1
- package/lib/utils/install-backend-native.js +93 -7
- package/lib/utils/install-lock.js +4 -25
- package/lib/utils/native-skip-scan.d.ts +20 -0
- package/lib/utils/native-skip-scan.js +343 -0
- package/lib/utils/node-script.js +1 -1
- package/lib/utils/oxc-resolve.js +42 -3
- package/lib/utils/process-table.d.ts +22 -0
- package/lib/utils/process-table.js +188 -0
- package/lib/utils/prune-prefix.js +1 -1
- package/lib/utils/run-gjs.d.ts +15 -1
- package/lib/utils/run-gjs.js +16 -7
- package/lib/utils/run-node.d.ts +2 -0
- package/lib/utils/run-node.js +2 -1
- package/lib/utils/runtimes.js +14 -13
- package/lib/utils/ship/gzip.js +1 -1
- package/lib/utils/ship/icons.d.ts +6 -0
- package/lib/utils/ship/icons.js +6 -5
- package/lib/utils/spawn.d.ts +13 -0
- package/lib/utils/spawn.js +10 -5
- package/lib/utils/watch-loop.d.ts +15 -1
- package/lib/utils/watch-loop.js +9 -5
- package/lib/utils/webext/build.d.ts +42 -0
- package/lib/utils/webext/build.js +120 -0
- package/lib/utils/webext/config.d.ts +72 -0
- package/lib/utils/webext/config.js +234 -0
- package/lib/utils/webext/icons.d.ts +24 -0
- package/lib/utils/webext/icons.js +83 -0
- package/lib/utils/webext/launch.d.ts +32 -0
- package/lib/utils/webext/launch.js +77 -0
- package/lib/utils/webext/manifest.d.ts +53 -0
- package/lib/utils/webext/manifest.js +219 -0
- package/lib/utils/webext/pages.d.ts +26 -0
- package/lib/utils/webext/pages.js +61 -0
- package/lib/utils/webext/targets.d.ts +13 -0
- package/lib/utils/webext/targets.js +36 -0
- package/lib/utils/win32-command.d.ts +17 -0
- package/lib/utils/win32-command.js +21 -6
- package/package.json +19 -19
|
@@ -8,6 +8,16 @@
|
|
|
8
8
|
* undoing the repair one level down.
|
|
9
9
|
*/
|
|
10
10
|
export declare function usingSelfShim(): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* `PATH` as the user's shell sees it: `process.env.PATH` without the self-shim dir.
|
|
13
|
+
*
|
|
14
|
+
* The shim is this PROCESS TREE's `gjsify`, never the one the user's next command
|
|
15
|
+
* runs. `self-update` asked `resolveBinOnPath` who wins PATH and got its own shim
|
|
16
|
+
* back, the temp copy of the version it had just replaced, so a successful update
|
|
17
|
+
* inside a workspace reported "`gjsify` on PATH resolves to /tmp/gjsify-shim-…
|
|
18
|
+
* (v0.51.1)" and exited 1.
|
|
19
|
+
*/
|
|
20
|
+
export declare function pathWithoutSelfShim(pathValue?: string, shimDir?: string | undefined, sep?: string): string;
|
|
11
21
|
/**
|
|
12
22
|
* Does this invocation need its own `gjsify` on PATH, rather than the workspace's
|
|
13
23
|
* npm bin?
|
|
@@ -31,6 +41,43 @@ export declare function needsSelfShim(opts: {
|
|
|
31
41
|
* reuse it instead of creating their own.
|
|
32
42
|
*/
|
|
33
43
|
export declare function ensureGjsifyShimOnPath(): void;
|
|
44
|
+
/**
|
|
45
|
+
* The tree whose `node_modules` holds the running CLI and its prebuilds: the
|
|
46
|
+
* install prefix above the first `node_modules` segment, or the workspace root
|
|
47
|
+
* for an in-repo CLI (`packages/infra/cli/dist/cli.gjs.mjs`).
|
|
48
|
+
*/
|
|
49
|
+
export declare function selfShimScanRoot(selfEntry: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* The env preamble for a shim re-invoking `selfEntry` — {@link buildNativeEnvPreamble}
|
|
52
|
+
* with the inputs `gjsify install -g` gives it: the prefix to glob at launch, and
|
|
53
|
+
* the prebuild dirs the walk finds above it (a hoisted ancestor's `node_modules`).
|
|
54
|
+
*/
|
|
55
|
+
export declare function selfShimPreamble(selfEntry: string): string;
|
|
56
|
+
/**
|
|
57
|
+
* The POSIX `sh` body of the self-shim: re-invoke `target` under `interpreter`.
|
|
58
|
+
*
|
|
59
|
+
* On darwin it RE-EXPORTS the running CLI's `DYLD_*` values first. SIP strips every
|
|
60
|
+
* `DYLD_*` variable when a protected binary is exec'd, and `/bin/sh` — this shim —
|
|
61
|
+
* is one, so the child `gjs` otherwise starts without the loader path the install
|
|
62
|
+
* launcher gave the parent (see `buildNativeEnvPreamble`). `GI_TYPELIB_PATH` is not a
|
|
63
|
+
* `DYLD_*` name and survives, so the child finds a prebuild's typelib and then fails
|
|
64
|
+
* the bare-leaf `dlopen` it names: `gjsify workspace <pkg> build` died with
|
|
65
|
+
* `Failed to load shared library 'libgjsifyterminal.dylib'` while the same script run
|
|
66
|
+
* through the tree's own launcher worked.
|
|
67
|
+
*
|
|
68
|
+
* Baking the values is not the snapshot `buildNativeEnvPreamble` warns about: this
|
|
69
|
+
* shim lives in a temp dir for ONE process tree and carries exactly the environment
|
|
70
|
+
* that tree's root was started with.
|
|
71
|
+
*/
|
|
72
|
+
export declare function buildSelfShimScript(opts: {
|
|
73
|
+
interpreter: string;
|
|
74
|
+
interpreterArgs: readonly string[];
|
|
75
|
+
target: string;
|
|
76
|
+
platform?: string;
|
|
77
|
+
env?: Record<string, string | undefined>;
|
|
78
|
+
}): string;
|
|
79
|
+
/** `export` lines restoring each set `DYLD_*` variable on darwin; `''` elsewhere. */
|
|
80
|
+
export declare function dyldCarryPreamble(platform: string, env: Record<string, string | undefined>): string;
|
|
34
81
|
/**
|
|
35
82
|
* Directory carrying a `node` that runs the file through
|
|
36
83
|
* `gjsify run --node-script`, or null when this host needs none.
|
package/lib/utils/gjsify-shim.js
CHANGED
|
@@ -41,10 +41,11 @@
|
|
|
41
41
|
// nested builds off the GJS bundle.
|
|
42
42
|
import { mkdtempSync, mkdirSync, writeFileSync, chmodSync, existsSync } from 'node:fs';
|
|
43
43
|
import { tmpdir } from 'node:os';
|
|
44
|
-
import { join, delimiter, relative, isAbsolute } from 'node:path';
|
|
44
|
+
import { join, delimiter, relative, isAbsolute, dirname } from 'node:path';
|
|
45
45
|
import { fileURLToPath } from 'node:url';
|
|
46
46
|
import { isGjs } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
47
|
-
import { buildLauncherShims } from './bin-shim.js';
|
|
47
|
+
import { buildLauncherShims, buildNativeEnvPreamble, buildShLauncher, shQuoteArg } from './bin-shim.js';
|
|
48
|
+
import { detectNativePackages } from './detect-native-packages.js';
|
|
48
49
|
import { resolveBinOnPath } from './install-global.js';
|
|
49
50
|
import { findWorkspaceRoot } from './workspace-root.js';
|
|
50
51
|
let _ensured = false;
|
|
@@ -61,6 +62,23 @@ let _selfShimActive = false;
|
|
|
61
62
|
export function usingSelfShim() {
|
|
62
63
|
return _selfShimActive;
|
|
63
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* `PATH` as the user's shell sees it: `process.env.PATH` without the self-shim dir.
|
|
67
|
+
*
|
|
68
|
+
* The shim is this PROCESS TREE's `gjsify`, never the one the user's next command
|
|
69
|
+
* runs. `self-update` asked `resolveBinOnPath` who wins PATH and got its own shim
|
|
70
|
+
* back, the temp copy of the version it had just replaced, so a successful update
|
|
71
|
+
* inside a workspace reported "`gjsify` on PATH resolves to /tmp/gjsify-shim-…
|
|
72
|
+
* (v0.51.1)" and exited 1.
|
|
73
|
+
*/
|
|
74
|
+
export function pathWithoutSelfShim(pathValue = process.env.PATH ?? '', shimDir = process.env.GJSIFY_SHIM_DIR, sep = delimiter) {
|
|
75
|
+
if (!shimDir)
|
|
76
|
+
return pathValue;
|
|
77
|
+
return pathValue
|
|
78
|
+
.split(sep)
|
|
79
|
+
.filter((entry) => entry !== shimDir)
|
|
80
|
+
.join(sep);
|
|
81
|
+
}
|
|
64
82
|
/**
|
|
65
83
|
* Does this invocation need its own `gjsify` on PATH, rather than the workspace's
|
|
66
84
|
* npm bin?
|
|
@@ -117,23 +135,90 @@ export function ensureGjsifyShimOnPath() {
|
|
|
117
135
|
// `process.execPath`: the batch member cannot quote an interpreter argument
|
|
118
136
|
// (`<interpreter> "<target>" %*`), so `C:\Program Files\nodejs\node.exe` splits.
|
|
119
137
|
const interpreter = gjs ? process.env.GJS_CONSOLE || 'gjs' : 'node';
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
138
|
+
// Under GJS the shim is the SAME launcher `gjsify install` writes, preamble
|
|
139
|
+
// included (`selfShimPreamble`). A bare `exec gjs -m` is not enough on a Mac:
|
|
140
|
+
// it runs behind `/bin/sh`, and SIP strips every `DYLD_*` from a protected
|
|
141
|
+
// binary's environment, so a nested `gjsify` found its typelibs through the
|
|
142
|
+
// surviving GI_TYPELIB_PATH and none of their libraries. Reported by the
|
|
143
|
+
// beifahrer consumer on macOS: `gjsify workspace <pkg> build` died in
|
|
144
|
+
// `get columns` with "Unsupported type void".
|
|
145
|
+
const envPreamble = gjs ? selfShimPreamble(selfEntry) : '';
|
|
146
|
+
writeFileSync(shim, gjs
|
|
147
|
+
? buildShLauncher(selfEntry, { isGjsBundle: true, envPreamble, gjs: interpreter })
|
|
148
|
+
: `#!/bin/sh\nexec "${interpreter}" "${selfEntry}" "$@"\n`, { mode: 0o755 });
|
|
123
149
|
chmodSync(shim, 0o755);
|
|
124
150
|
// cmd.exe and pwsh cannot run the extension-less member: not on PATHEXT, and
|
|
125
151
|
// Windows has no shebang handling. `buildLauncherShims` ports npm's
|
|
126
152
|
// three-sibling answer, including the IF/ELSE ERRORLEVEL handling that
|
|
127
153
|
// `&&`/`||` chaining gets wrong.
|
|
128
154
|
if (!gjs && process.platform === 'win32') {
|
|
129
|
-
const { cmd, ps1 } = buildLauncherShims({ interpreter, interpreterArgs, target: selfEntry });
|
|
155
|
+
const { cmd, ps1 } = buildLauncherShims({ interpreter, interpreterArgs: [], target: selfEntry });
|
|
130
156
|
writeFileSync(`${shim}.cmd`, cmd);
|
|
131
157
|
writeFileSync(`${shim}.ps1`, ps1);
|
|
132
158
|
}
|
|
133
159
|
_selfShimActive = true;
|
|
134
160
|
process.env.GJSIFY_SHIM_DIR = dir;
|
|
135
161
|
process.env.PATH = dir + delimiter + (process.env.PATH ?? '');
|
|
136
|
-
writeNodeShim(dir, gjs, selfEntry);
|
|
162
|
+
writeNodeShim(dir, gjs, selfEntry, envPreamble);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* The tree whose `node_modules` holds the running CLI and its prebuilds: the
|
|
166
|
+
* install prefix above the first `node_modules` segment, or the workspace root
|
|
167
|
+
* for an in-repo CLI (`packages/infra/cli/dist/cli.gjs.mjs`).
|
|
168
|
+
*/
|
|
169
|
+
export function selfShimScanRoot(selfEntry) {
|
|
170
|
+
// Both separators: the entry is a HOST path, and win32 spells it with a backslash.
|
|
171
|
+
const at = selfEntry.search(/[\\/]node_modules[\\/]/);
|
|
172
|
+
if (at > 0)
|
|
173
|
+
return selfEntry.slice(0, at);
|
|
174
|
+
return findWorkspaceRoot(dirname(selfEntry)) ?? dirname(selfEntry);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* The env preamble for a shim re-invoking `selfEntry` — {@link buildNativeEnvPreamble}
|
|
178
|
+
* with the inputs `gjsify install -g` gives it: the prefix to glob at launch, and
|
|
179
|
+
* the prebuild dirs the walk finds above it (a hoisted ancestor's `node_modules`).
|
|
180
|
+
*/
|
|
181
|
+
export function selfShimPreamble(selfEntry) {
|
|
182
|
+
let baked = [];
|
|
183
|
+
try {
|
|
184
|
+
baked = detectNativePackages(dirname(selfEntry)).map((p) => p.prebuildsDir);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// The walk is filesystem I/O (EACCES on an unreadable node_modules). The
|
|
188
|
+
// launch-time glob of the scan root still runs; only ancestors go unseen.
|
|
189
|
+
}
|
|
190
|
+
return buildNativeEnvPreamble(selfShimScanRoot(selfEntry), baked);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* The POSIX `sh` body of the self-shim: re-invoke `target` under `interpreter`.
|
|
194
|
+
*
|
|
195
|
+
* On darwin it RE-EXPORTS the running CLI's `DYLD_*` values first. SIP strips every
|
|
196
|
+
* `DYLD_*` variable when a protected binary is exec'd, and `/bin/sh` — this shim —
|
|
197
|
+
* is one, so the child `gjs` otherwise starts without the loader path the install
|
|
198
|
+
* launcher gave the parent (see `buildNativeEnvPreamble`). `GI_TYPELIB_PATH` is not a
|
|
199
|
+
* `DYLD_*` name and survives, so the child finds a prebuild's typelib and then fails
|
|
200
|
+
* the bare-leaf `dlopen` it names: `gjsify workspace <pkg> build` died with
|
|
201
|
+
* `Failed to load shared library 'libgjsifyterminal.dylib'` while the same script run
|
|
202
|
+
* through the tree's own launcher worked.
|
|
203
|
+
*
|
|
204
|
+
* Baking the values is not the snapshot `buildNativeEnvPreamble` warns about: this
|
|
205
|
+
* shim lives in a temp dir for ONE process tree and carries exactly the environment
|
|
206
|
+
* that tree's root was started with.
|
|
207
|
+
*/
|
|
208
|
+
export function buildSelfShimScript(opts) {
|
|
209
|
+
const argv = opts.interpreterArgs.length > 0 ? `${opts.interpreterArgs.join(' ')} ` : '';
|
|
210
|
+
const preamble = dyldCarryPreamble(opts.platform ?? process.platform, opts.env ?? process.env);
|
|
211
|
+
return `#!/bin/sh\n${preamble}exec "${opts.interpreter}" ${argv}"${opts.target}" "$@"\n`;
|
|
212
|
+
}
|
|
213
|
+
/** `export` lines restoring each set `DYLD_*` variable on darwin; `''` elsewhere. */
|
|
214
|
+
export function dyldCarryPreamble(platform, env) {
|
|
215
|
+
if (platform !== 'darwin')
|
|
216
|
+
return '';
|
|
217
|
+
return Object.keys(env)
|
|
218
|
+
.filter((name) => /^DYLD_[A-Z_]+$/.test(name) && env[name])
|
|
219
|
+
.sort()
|
|
220
|
+
.map((name) => `${name}=${shQuoteArg(env[name])}\nexport ${name}\n`)
|
|
221
|
+
.join('');
|
|
137
222
|
}
|
|
138
223
|
/** Deliberately NOT the shim dir itself — see {@link nodeShimDir}. */
|
|
139
224
|
const NODE_SHIM_SUBDIR = 'node-shim';
|
|
@@ -183,7 +268,7 @@ export function nodeShimDir() {
|
|
|
183
268
|
* resolves NOWHERE, and it is reachable only from a package script's PATH (see
|
|
184
269
|
* {@link nodeShimDir}).
|
|
185
270
|
*/
|
|
186
|
-
function writeNodeShim(dir, gjs, selfEntry) {
|
|
271
|
+
function writeNodeShim(dir, gjs, selfEntry, envPreamble) {
|
|
187
272
|
if (!gjs)
|
|
188
273
|
return;
|
|
189
274
|
const sub = join(dir, NODE_SHIM_SUBDIR);
|
|
@@ -199,11 +284,15 @@ function writeNodeShim(dir, gjs, selfEntry) {
|
|
|
199
284
|
// `--node-script` can honour, and yargs would take `--test` FOR the script
|
|
200
285
|
// path (`unknown-options-as-args` turns an unknown flag into a positional).
|
|
201
286
|
writeFileSync(shim, '#!/bin/sh\n' +
|
|
287
|
+
// Same SIP hop as the self-shim — see `buildSelfShimScript`.
|
|
288
|
+
dyldCarryPreamble(process.platform, process.env) +
|
|
202
289
|
'case "$1" in\n' +
|
|
203
290
|
' -*) echo "gjsify: this host has no node; the gjsify shim runs a SCRIPT FILE only" >&2\n' +
|
|
204
291
|
' echo "gjsify: got: node $*" >&2\n' +
|
|
205
292
|
' exit 127 ;;\n' +
|
|
206
293
|
'esac\n' +
|
|
294
|
+
// The same hop as the `gjsify` shim, so the same preamble.
|
|
295
|
+
envPreamble +
|
|
207
296
|
`exec "${interpreter}" -m "${selfEntry}" run --node-script "$@"\n`, { mode: 0o755 });
|
|
208
297
|
chmodSync(shim, 0o755);
|
|
209
298
|
}
|
|
@@ -22,6 +22,12 @@ export interface ResolvedNode {
|
|
|
22
22
|
/** Ranges keyed by name, as published. */
|
|
23
23
|
dependencies: Record<string, string>;
|
|
24
24
|
optionalDependencies: Record<string, string>;
|
|
25
|
+
/** As published; absent when the version declares none. Read through {@link requiredPeerEntries}. */
|
|
26
|
+
peerDependencies?: Record<string, string>;
|
|
27
|
+
/** As published — `optional: true` here is what keeps a peer from being installed. */
|
|
28
|
+
peerDependenciesMeta?: Record<string, {
|
|
29
|
+
optional?: boolean;
|
|
30
|
+
}>;
|
|
25
31
|
bin?: string | Record<string, string>;
|
|
26
32
|
/**
|
|
27
33
|
* The version's `os`/`cpu`/`libc` declaration, as published. Recorded here
|
|
@@ -156,6 +162,17 @@ export declare function warnMissingNativeBuilds(nodes: ResolvedNode[], prefix: s
|
|
|
156
162
|
* `dependencies` entry to override), covered by {@link resolveDeps}'s second queue loop.
|
|
157
163
|
*/
|
|
158
164
|
export declare function requiredDepEntries(node: Pick<ResolvedNode, 'dependencies' | 'optionalDependencies'>): [string, string][];
|
|
165
|
+
/**
|
|
166
|
+
* A node's REQUIRED peer edges: its `peerDependencies` minus those
|
|
167
|
+
* `peerDependenciesMeta` marks `optional` (npm ≥ 7 installs the rest and skips
|
|
168
|
+
* these) and minus any name it also lists as a dependency — that edge is walked
|
|
169
|
+
* from the node itself by {@link requiredDepEntries}, or is optional because the
|
|
170
|
+
* publisher said so in `optionalDependencies`.
|
|
171
|
+
*
|
|
172
|
+
* Shared by the resolve walk and the optionality fixpoint, for the reason
|
|
173
|
+
* {@link requiredDepEntries} is: two readings of one manifest are two answers.
|
|
174
|
+
*/
|
|
175
|
+
export declare function requiredPeerEntries(node: Pick<ResolvedNode, 'dependencies' | 'optionalDependencies' | 'peerDependencies' | 'peerDependenciesMeta'>): [string, string][];
|
|
159
176
|
/**
|
|
160
177
|
* Recompute every node's `optional` flag as a FIXPOINT over the placed graph. The
|
|
161
178
|
* only writer of the final flag; whatever the nodes arrive carrying is an input
|
|
@@ -163,7 +180,8 @@ export declare function requiredDepEntries(node: Pick<ResolvedNode, 'dependencie
|
|
|
163
180
|
* file on the lockfile path) and is overwritten unconditionally.
|
|
164
181
|
*
|
|
165
182
|
* DEFINITION: a node is REQUIRED iff reachable from `requiredNames` through
|
|
166
|
-
* REQUIRED edges alone ({@link requiredDepEntries}
|
|
183
|
+
* REQUIRED edges alone ({@link requiredDepEntries} plus {@link requiredPeerEntries} —
|
|
184
|
+
* a peer npm installs is as mandatory as a dependency); every other node is optional.
|
|
167
185
|
* Optionality is therefore INHERITED — a plain dependency OF an optional package is
|
|
168
186
|
* still optional, which is what npm's `optionalSet` computes.
|
|
169
187
|
*
|
|
@@ -45,8 +45,20 @@
|
|
|
45
45
|
// (v4); without it the fresh-resolve and lockfile paths disagree about one
|
|
46
46
|
// tree.
|
|
47
47
|
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
48
|
+
// PEER DEPENDENCIES follow npm ≥ 7: every peer NOT marked optional in
|
|
49
|
+
// `peerDependenciesMeta` is installed where the dependent resolves it — as its
|
|
50
|
+
// SIBLING (queued from the dependent's parent, so the dependent and whatever
|
|
51
|
+
// requires it share one instance), reusing any visible copy whose version
|
|
52
|
+
// satisfies the range. Optional peers are never installed, exactly as npm skips
|
|
53
|
+
// them; ADR 0020's `@gjsify/rolldown-native` depends on that. The defect this
|
|
54
|
+
// closes: `wxt` declares `vite` as a required peer, `gjsify install` exited 0
|
|
55
|
+
// without it, and `wxt prepare` died with "Builder not found". A peer edge is a
|
|
56
|
+
// REQUIRED edge for the optionality fixpoint ({@link requiredPeerEntries}), so
|
|
57
|
+
// the lockfile records the peer maps and marks a peer-aware resolve
|
|
58
|
+
// (`Lockfile.peersResolved` says why that is a marker and not a version bump).
|
|
59
|
+
//
|
|
60
|
+
// Out of scope (still deferred): lifecycle scripts, git/file specs, and npm's
|
|
61
|
+
// strict-peer-deps ERESOLVE (a conflicting peer warns, see `resolveDeps`).
|
|
50
62
|
import * as fs from 'node:fs';
|
|
51
63
|
import * as path from 'node:path';
|
|
52
64
|
import * as os from 'node:os';
|
|
@@ -172,6 +184,7 @@ async function installPackagesNativeLocked(opts, npmrc, log) {
|
|
|
172
184
|
else if (!opts.refreshLockfile &&
|
|
173
185
|
existingLock &&
|
|
174
186
|
existingLock.lockfileVersion === LOCKFILE_VERSION &&
|
|
187
|
+
existingLock.peersResolved === true &&
|
|
175
188
|
lockfileMatchesRequest(existingLock, opts.specs)) {
|
|
176
189
|
log('install: using lockfile (%d package(s))', Object.keys(existingLock.packages).length);
|
|
177
190
|
nodes = lockfileToNodes(existingLock);
|
|
@@ -179,6 +192,9 @@ async function installPackagesNativeLocked(opts, npmrc, log) {
|
|
|
179
192
|
else {
|
|
180
193
|
// A resolve has to run (new/changed/removed dep, or no lockfile yet).
|
|
181
194
|
//
|
|
195
|
+
// So does a lockfile without `peersResolved`: it may lack a required peer
|
|
196
|
+
// (Lockfile.peersResolved).
|
|
197
|
+
//
|
|
182
198
|
// A pre-v4 lockfile lands here too even when it matches the request: each
|
|
183
199
|
// bump added something the verdict is recomputed FROM, so a file lacking it
|
|
184
200
|
// cannot be consumed verbatim — pre-v3 entries carry no platform
|
|
@@ -533,7 +549,7 @@ optionalSpecs) {
|
|
|
533
549
|
// `workspaceInstall` produces when two workspaces declare incompatible ranges of
|
|
534
550
|
// the same external dep) all compete for the single root slot and the resolver
|
|
535
551
|
// silently keeps one version for everyone. Per-workspace dedup is Phase D.8
|
|
536
|
-
// (status/open-todos.md); until it lands the conflict is surfaced loudly after
|
|
552
|
+
// (status/open-todos/README.md); until it lands the conflict is surfaced loudly after
|
|
537
553
|
// the resolve (see emitTopLevelConflictWarnings).
|
|
538
554
|
const topLevelRanges = new Map();
|
|
539
555
|
const queue = specs.map(parseSpec).map((s) => {
|
|
@@ -601,6 +617,23 @@ optionalSpecs) {
|
|
|
601
617
|
// {@link computeOptionalFlags} is the only writer of the final flag.
|
|
602
618
|
continue;
|
|
603
619
|
}
|
|
620
|
+
// A peer must live in ONE directory — beside its dependent. If that slot
|
|
621
|
+
// already holds a version outside the range, a private copy would give the
|
|
622
|
+
// dependent a different instance from its requester, which is the thing a
|
|
623
|
+
// peer exists to prevent. npm ≥ 7 raises ERESOLVE here in strict mode and,
|
|
624
|
+
// non-strict (`--force`, or a conflict below the root), warns "overriding
|
|
625
|
+
// peer dependency" and keeps the existing copy. We take the lenient answer:
|
|
626
|
+
// warn, keep it, install nothing.
|
|
627
|
+
if (edge.peerOf) {
|
|
628
|
+
const slot = edge.from === null ? `node_modules/${edge.name}` : `${edge.from}/node_modules/${edge.name}`;
|
|
629
|
+
const occupant = byPath.get(slot);
|
|
630
|
+
if (occupant) {
|
|
631
|
+
console.warn(`[gjsify] warning: peer dependency conflict: ${edge.peerOf.name}@${edge.peerOf.version} ` +
|
|
632
|
+
`wants ${edge.name}@${edge.range}, but ${occupant.version} is installed at ` +
|
|
633
|
+
`${occupant.installPath} — keeping it (npm's non-strict behaviour).`);
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
604
637
|
// No compatible placement. Resolve a version, preferring one already
|
|
605
638
|
// pinned when it satisfies the range so an add does not bump unchanged
|
|
606
639
|
// deps.
|
|
@@ -629,6 +662,8 @@ optionalSpecs) {
|
|
|
629
662
|
installPath,
|
|
630
663
|
dependencies: v.dependencies ?? {},
|
|
631
664
|
optionalDependencies: v.optionalDependencies ?? {},
|
|
665
|
+
peerDependencies: nonEmpty(v.peerDependencies),
|
|
666
|
+
peerDependenciesMeta: nonEmpty(v.peerDependenciesMeta),
|
|
632
667
|
bin: v.bin,
|
|
633
668
|
// Complete as published: `fetchPkg` reads the FULL document, so all
|
|
634
669
|
// three fields come from one body — no second fetch, no heuristic.
|
|
@@ -653,6 +688,20 @@ optionalSpecs) {
|
|
|
653
688
|
name: `${edge.name}@${version}`,
|
|
654
689
|
});
|
|
655
690
|
if (!skipDeps) {
|
|
691
|
+
// Required peers FIRST, so the peer claims its slot beside the
|
|
692
|
+
// dependent before the dependent's own subtree can hoist an
|
|
693
|
+
// incompatible copy of the same name into it. Queued from the PARENT
|
|
694
|
+
// (`Edge.peerOf`); optional peers are never queued, npm ≥ 7 skips them.
|
|
695
|
+
const parent = parentInstallPath(installPath);
|
|
696
|
+
for (const [peerName, peerRange] of requiredPeerEntries(node)) {
|
|
697
|
+
queue.push({
|
|
698
|
+
from: parent,
|
|
699
|
+
name: peerName,
|
|
700
|
+
range: applyOverride(peerName, peerRange),
|
|
701
|
+
required: !node.optional,
|
|
702
|
+
peerOf: node,
|
|
703
|
+
});
|
|
704
|
+
}
|
|
656
705
|
// REQUIRED edges only — a name this package also lists in
|
|
657
706
|
// `optionalDependencies` is queued once by the loop below, as
|
|
658
707
|
// optional (header note, invariant 3). Queuing it here too would
|
|
@@ -693,7 +742,9 @@ optionalSpecs) {
|
|
|
693
742
|
// `assertRequiredEdgesResolved` re-judges every entry against the
|
|
694
743
|
// fixpoint flags, so a genuinely required dependency cannot go missing
|
|
695
744
|
// just because BFS reached it through an optional edge first.
|
|
696
|
-
|
|
745
|
+
// A peer edge is keyed by its DEPENDENT, not by `from`: the dependent
|
|
746
|
+
// is the requester whose final optionality decides the verdict.
|
|
747
|
+
skippedEdges.add(edgeKey(edge.peerOf?.installPath ?? edge.from, edge.name));
|
|
697
748
|
log('resolve: optional dep %s@%s skipped (%s)', edge.name, edge.range, errMsg(e));
|
|
698
749
|
continue;
|
|
699
750
|
}
|
|
@@ -798,6 +849,27 @@ export function requiredDepEntries(node) {
|
|
|
798
849
|
}
|
|
799
850
|
return entries;
|
|
800
851
|
}
|
|
852
|
+
/**
|
|
853
|
+
* A node's REQUIRED peer edges: its `peerDependencies` minus those
|
|
854
|
+
* `peerDependenciesMeta` marks `optional` (npm ≥ 7 installs the rest and skips
|
|
855
|
+
* these) and minus any name it also lists as a dependency — that edge is walked
|
|
856
|
+
* from the node itself by {@link requiredDepEntries}, or is optional because the
|
|
857
|
+
* publisher said so in `optionalDependencies`.
|
|
858
|
+
*
|
|
859
|
+
* Shared by the resolve walk and the optionality fixpoint, for the reason
|
|
860
|
+
* {@link requiredDepEntries} is: two readings of one manifest are two answers.
|
|
861
|
+
*/
|
|
862
|
+
export function requiredPeerEntries(node) {
|
|
863
|
+
const entries = [];
|
|
864
|
+
for (const [name, range] of Object.entries(node.peerDependencies ?? {})) {
|
|
865
|
+
if (node.peerDependenciesMeta?.[name]?.optional === true)
|
|
866
|
+
continue;
|
|
867
|
+
if (name in node.dependencies || name in node.optionalDependencies)
|
|
868
|
+
continue;
|
|
869
|
+
entries.push([name, range]);
|
|
870
|
+
}
|
|
871
|
+
return entries;
|
|
872
|
+
}
|
|
801
873
|
/**
|
|
802
874
|
* Recompute every node's `optional` flag as a FIXPOINT over the placed graph. The
|
|
803
875
|
* only writer of the final flag; whatever the nodes arrive carrying is an input
|
|
@@ -805,7 +877,8 @@ export function requiredDepEntries(node) {
|
|
|
805
877
|
* file on the lockfile path) and is overwritten unconditionally.
|
|
806
878
|
*
|
|
807
879
|
* DEFINITION: a node is REQUIRED iff reachable from `requiredNames` through
|
|
808
|
-
* REQUIRED edges alone ({@link requiredDepEntries}
|
|
880
|
+
* REQUIRED edges alone ({@link requiredDepEntries} plus {@link requiredPeerEntries} —
|
|
881
|
+
* a peer npm installs is as mandatory as a dependency); every other node is optional.
|
|
809
882
|
* Optionality is therefore INHERITED — a plain dependency OF an optional package is
|
|
810
883
|
* still optional, which is what npm's `optionalSet` computes.
|
|
811
884
|
*
|
|
@@ -861,7 +934,7 @@ export function computeOptionalFlags(nodes, requiredNames, log) {
|
|
|
861
934
|
const node = byPath.get(installPath);
|
|
862
935
|
if (!node)
|
|
863
936
|
continue;
|
|
864
|
-
for (const [depName] of requiredDepEntries(node)) {
|
|
937
|
+
for (const [depName] of [...requiredDepEntries(node), ...requiredPeerEntries(node)]) {
|
|
865
938
|
// Resolve the edge the way the REQUESTER will at runtime — through
|
|
866
939
|
// the ancestor `node_modules` chain — so a nested copy is credited
|
|
867
940
|
// to the requester that nested it and the hoisted one is not
|
|
@@ -952,7 +1025,7 @@ function assertRequiredEdgesResolved(nodes, skippedEdges) {
|
|
|
952
1025
|
* (dist-tags like `latest`) cannot be compared and are skipped.
|
|
953
1026
|
*
|
|
954
1027
|
* This makes the single-root-slot behaviour honest rather than silent; the real fix
|
|
955
|
-
* is a per-workspace dedup pass, Phase D.8 (status/open-todos.md).
|
|
1028
|
+
* is a per-workspace dedup pass, Phase D.8 (status/open-todos/README.md).
|
|
956
1029
|
*/
|
|
957
1030
|
function emitTopLevelConflictWarnings(topLevelRanges, root) {
|
|
958
1031
|
for (const [name, ranges] of topLevelRanges) {
|
|
@@ -1082,6 +1155,11 @@ function findVisible(requesterPath, name, byPath) {
|
|
|
1082
1155
|
}
|
|
1083
1156
|
return null;
|
|
1084
1157
|
}
|
|
1158
|
+
/** The `installPath` whose `node_modules` holds this one; null for a root placement. */
|
|
1159
|
+
function parentInstallPath(installPath) {
|
|
1160
|
+
const idx = installPath.lastIndexOf('/node_modules/');
|
|
1161
|
+
return idx < 0 ? null : installPath.slice(0, idx);
|
|
1162
|
+
}
|
|
1085
1163
|
/**
|
|
1086
1164
|
* Where to install `name@version` for a request from `requesterPath`: hoist to
|
|
1087
1165
|
* `node_modules/<name>` when the root slot is empty or already holds this version,
|
|
@@ -1142,6 +1220,8 @@ function writeLockfile(lockfilePath, specs, nodes) {
|
|
|
1142
1220
|
// `computeOptionalFlags` runs on the lockfile path too and cannot recompute
|
|
1143
1221
|
// what the file does not carry (v4, header note invariant 3).
|
|
1144
1222
|
optionalDependencies: Object.keys(node.optionalDependencies).length > 0 ? node.optionalDependencies : undefined,
|
|
1223
|
+
peerDependencies: nonEmpty(node.peerDependencies),
|
|
1224
|
+
peerDependenciesMeta: nonEmpty(node.peerDependenciesMeta),
|
|
1145
1225
|
bin: node.bin,
|
|
1146
1226
|
// The DECLARATION, never the verdict: `inert` is per-host and deliberately
|
|
1147
1227
|
// absent from the file, since a lockfile recording which packages the
|
|
@@ -1156,12 +1236,16 @@ function writeLockfile(lockfilePath, specs, nodes) {
|
|
|
1156
1236
|
lockfileVersion: LOCKFILE_VERSION,
|
|
1157
1237
|
requested: [...specs],
|
|
1158
1238
|
packages,
|
|
1239
|
+
peersResolved: true,
|
|
1159
1240
|
};
|
|
1160
1241
|
// Atomic tmp+rename so a crash mid-write, or a reader racing the writer, can never
|
|
1161
1242
|
// observe a torn gjsify-lock.json — `--immutable` would otherwise hard-fail on the
|
|
1162
1243
|
// corrupt file with a misleading error.
|
|
1163
1244
|
atomicWriteStrict(lockfilePath, JSON.stringify(lockfile, null, 2) + '\n');
|
|
1164
1245
|
}
|
|
1246
|
+
function nonEmpty(map) {
|
|
1247
|
+
return map && Object.keys(map).length > 0 ? map : undefined;
|
|
1248
|
+
}
|
|
1165
1249
|
/**
|
|
1166
1250
|
* The lockfile-preservation oracle: `name → every version currently pinned` (a name
|
|
1167
1251
|
* can appear at more than one path/version when the tree nested a conflicting copy).
|
|
@@ -1214,6 +1298,8 @@ function lockfileToNodes(lockfile) {
|
|
|
1214
1298
|
// blocks reads as required here. `--immutable` consumes it anyway — see
|
|
1215
1299
|
// READABLE_LOCKFILE_VERSIONS.
|
|
1216
1300
|
optionalDependencies: entry.optionalDependencies ?? {},
|
|
1301
|
+
peerDependencies: entry.peerDependencies,
|
|
1302
|
+
peerDependenciesMeta: entry.peerDependenciesMeta,
|
|
1217
1303
|
bin: entry.bin,
|
|
1218
1304
|
// Rebuilt from the recorded declaration so `applyPlatformFilter` reaches the
|
|
1219
1305
|
// SAME verdict here as on the fresh-resolve path, for THIS host — generally
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
// `owner.json` inside records pid + start time for staleness checks.
|
|
18
18
|
//
|
|
19
19
|
// Staleness — a crashed install cannot release its lock, so contenders steal when
|
|
20
|
-
// (a) the owner pid is provably dead (`/proc/<pid>`
|
|
21
|
-
//
|
|
20
|
+
// (a) the owner pid is provably dead (`isPidAlive` in `process-table.ts`: `/proc/<pid>` where
|
|
21
|
+
// procfs exists, a signal-0 probe elsewhere), or
|
|
22
22
|
// (b) the lock outlives GJSIFY_INSTALL_LOCK_STALE_MS (default 35 min — just above the 30-min
|
|
23
23
|
// install budget, so a budget-bound install always exits before its lock can be stolen out
|
|
24
24
|
// from under it), or
|
|
@@ -31,8 +31,9 @@
|
|
|
31
31
|
//
|
|
32
32
|
// GJSIFY_INSTALL_LOCK=0 disables locking entirely — for a network filesystem with broken
|
|
33
33
|
// rename/mkdir atomicity, or an outer tool that already serializes installs.
|
|
34
|
-
import {
|
|
34
|
+
import { mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
35
35
|
import { dirname, join, resolve } from 'node:path';
|
|
36
|
+
import { isPidAlive } from './process-table.js';
|
|
36
37
|
const LOCK_DIR_NAME = '.gjsify-install-lock';
|
|
37
38
|
const OWNER_FILE = 'owner.json';
|
|
38
39
|
/** Default staleness budget — see the header comment for why 35 min. */
|
|
@@ -60,28 +61,6 @@ function lockingDisabled() {
|
|
|
60
61
|
const trimmed = flag.trim();
|
|
61
62
|
return trimmed === '0' || trimmed === 'false';
|
|
62
63
|
}
|
|
63
|
-
/**
|
|
64
|
-
* Is `pid` a live process on this machine?
|
|
65
|
-
*
|
|
66
|
-
* `/proc/<pid>` is the reliable cross-runtime probe on Linux, identical under Node and GJS;
|
|
67
|
-
* elsewhere a signal-0 probe (ESRCH dead, EPERM alive-but-foreign). The GJS `process.kill`
|
|
68
|
-
* polyfill shells out and cannot report ESRCH, but GJS only runs on /proc platforms in practice,
|
|
69
|
-
* so the fallback branch is Node's. Unknown outcomes count as ALIVE — never steal a lock that
|
|
70
|
-
* cannot be proven abandoned; the mtime staleness budget still applies.
|
|
71
|
-
*/
|
|
72
|
-
function isPidAlive(pid) {
|
|
73
|
-
// `existsSync` never throws by contract: Node returns false on any error, and the GJS shim is
|
|
74
|
-
// a typeof guard + Gio `query_exists`, neither with a throw path in the GIR.
|
|
75
|
-
if (existsSync('/proc/self'))
|
|
76
|
-
return existsSync(`/proc/${pid}`);
|
|
77
|
-
try {
|
|
78
|
-
process.kill(pid, 0);
|
|
79
|
-
return true;
|
|
80
|
-
}
|
|
81
|
-
catch (err) {
|
|
82
|
-
return err.code === 'EPERM';
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
64
|
function mtimeMs(path) {
|
|
86
65
|
try {
|
|
87
66
|
return statSync(path).mtimeMs;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Prettier parser napi oxfmt would format `fileName` with, or `null` when
|
|
3
|
+
* the native build formats it itself or no oxfmt formats it. `svelte` counts
|
|
4
|
+
* only when the config enables it, as in oxfmt.
|
|
5
|
+
*/
|
|
6
|
+
export declare function externalParserFor(fileName: string, svelteEnabled?: boolean): string | null;
|
|
7
|
+
export interface NativeSkipScanOptions {
|
|
8
|
+
/** The `--config` handed to oxfmt. Absent → discovery, as in oxfmt. */
|
|
9
|
+
configPath?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface NativeSkipScanResult {
|
|
12
|
+
/** Files napi oxfmt would format that the native build skips. */
|
|
13
|
+
skipped: string[];
|
|
14
|
+
/** Files the walk hands to oxfmt's classifier. */
|
|
15
|
+
total: number;
|
|
16
|
+
}
|
|
17
|
+
/** Walk `paths` the way oxfmt does and report what the native build skips. */
|
|
18
|
+
export declare function scanForNativeSkips(paths: string[], cwd?: string, options?: NativeSkipScanOptions): NativeSkipScanResult;
|
|
19
|
+
/** JSON with comments and trailing commas; string contents are never touched. */
|
|
20
|
+
export declare function parseJsonc(text: string): unknown;
|