@gjsify/cli 0.52.0 → 0.54.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 +351 -305
- package/lib/actions/build.d.ts +22 -0
- package/lib/actions/build.js +37 -9
- package/lib/bundler-pick.d.ts +72 -1
- package/lib/bundler-pick.js +175 -17
- package/lib/cli-app.js +7 -2
- package/lib/commands/blueprint/index.d.ts +4 -0
- package/lib/commands/blueprint/index.js +15 -0
- package/lib/commands/blueprint/types.d.ts +8 -0
- package/lib/commands/blueprint/types.js +124 -0
- package/lib/commands/devtools.d.ts +21 -0
- package/lib/commands/devtools.js +146 -0
- 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 +5 -0
- package/lib/commands/index.js +5 -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/ship.js +51 -8
- 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 +46 -5
- 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/app-runtime.d.ts +7 -5
- package/lib/utils/ship/app-runtime.js +11 -8
- package/lib/utils/ship/appimage.d.ts +9 -0
- package/lib/utils/ship/appimage.js +2 -1
- package/lib/utils/ship/deb.d.ts +10 -0
- package/lib/utils/ship/deb.js +7 -0
- package/lib/utils/ship/depends.d.ts +37 -3
- package/lib/utils/ship/depends.js +21 -9
- package/lib/utils/ship/gi-namespaces.d.ts +16 -0
- package/lib/utils/ship/gi-namespaces.js +64 -1
- 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/ship/launcher.js +2 -2
- package/lib/utils/ship/layout.js +25 -2
- package/lib/utils/ship/rpm.d.ts +5 -0
- package/lib/utils/ship/rpm.js +42 -0
- package/lib/utils/ship/settings.js +6 -0
- package/lib/utils/ship/signing.d.ts +7 -2
- package/lib/utils/ship/signing.js +59 -32
- package/lib/utils/ship/stage-manifest.d.ts +14 -0
- package/lib/utils/ship/stage-manifest.js +7 -0
- 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 +48 -0
- package/lib/utils/win32-command.d.ts +17 -0
- package/lib/utils/win32-command.js +21 -6
- package/package.json +20 -19
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// The SECOND half of "what are this bundle's build inputs" — what the BUNDLER
|
|
3
|
+
// read, recorded at build time beside the bundle it produced.
|
|
4
|
+
//
|
|
5
|
+
// `package-inputs.ts` answers the other half by walking the package, and that
|
|
6
|
+
// walk cannot see outside the package. A consumer's test bundle routinely reads
|
|
7
|
+
// outside it: a workspace package reached through `node_modules/<name>` — a
|
|
8
|
+
// SYMLINK into a sibling directory — is a file the build read and the walk
|
|
9
|
+
// never enumerates. An edit there left `gjsify test` judging a fresh bundle and
|
|
10
|
+
// rerunning it: a stale GREEN, the worst kind of wrong, and the only remedy a
|
|
11
|
+
// consumer had was `rm -rf dist`.
|
|
12
|
+
//
|
|
13
|
+
// The set is recorded from the bundler's own module graph rather than guessed
|
|
14
|
+
// from a directory list: a bare specifier's resolution is known to the bundler
|
|
15
|
+
// and to nobody else, and a list of "likely dirs" is wrong for every layout
|
|
16
|
+
// nobody anticipated — the same failure `package-inputs.ts` documents twice.
|
|
17
|
+
//
|
|
18
|
+
// TWO halves, because the bundler is not the only reader: a `transform`
|
|
19
|
+
// observer sees every MODULE, and the engine's watch list carries what a plugin
|
|
20
|
+
// read and declared itself (a stylesheet's `@import` chain is read by
|
|
21
|
+
// `gjsify-css-as-string`, not by the bundler, so no graph names it). Neither
|
|
22
|
+
// engine reports both, so the recorder takes the union. Deciding freshness
|
|
23
|
+
// stays cheap: the manifest is written once per build and read as `stat` calls.
|
|
24
|
+
//
|
|
25
|
+
// `package-inputs.ts` is not replaced by this. Its walk answers "what MAY this
|
|
26
|
+
// package's build read", which covers a file the current build does not import
|
|
27
|
+
// yet; this answers "what DID this build read", which reaches outside the
|
|
28
|
+
// package. The union is the honest input set, and both halves must exist.
|
|
29
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
30
|
+
import { dirname, join, sep } from 'node:path';
|
|
31
|
+
/**
|
|
32
|
+
* Bumped when the shape changes; a manifest of any other version is ignored.
|
|
33
|
+
*
|
|
34
|
+
* v2 adds `watchList`. A v1 manifest says nothing about whether the engine that
|
|
35
|
+
* wrote it could report a watch list at all, and the safe reading of "unknown"
|
|
36
|
+
* is "do not trust it": see {@link readBundleInputs}.
|
|
37
|
+
*/
|
|
38
|
+
export const BUNDLE_INPUTS_VERSION = 2;
|
|
39
|
+
/** The manifest that belongs to `outfile`. */
|
|
40
|
+
export function bundleInputsPath(outfile) {
|
|
41
|
+
return `${outfile}.inputs.json`;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* A module id as a path, or `null` when it names no file we can stat.
|
|
45
|
+
*
|
|
46
|
+
* A query or a hash is PART OF THE ID, not of the path: `@scope/pkg/x.css?raw`
|
|
47
|
+
* and `main-window.blp?shared-tree` (the blueprint plugin's shared-tree exit)
|
|
48
|
+
* both stat as ENOENT, and dropping the id silently would drop the file with
|
|
49
|
+
* it. `\0` is the bundler's virtual-module marker — no file, ever.
|
|
50
|
+
*/
|
|
51
|
+
function idToPath(id) {
|
|
52
|
+
if (id === '' || id.startsWith('\0'))
|
|
53
|
+
return null;
|
|
54
|
+
const cut = id.search(/[?#]/);
|
|
55
|
+
if (cut === -1)
|
|
56
|
+
return id;
|
|
57
|
+
const stripped = id.slice(0, cut);
|
|
58
|
+
// The fallback is not a nicety: `#` is LEGAL in a Linux filename, and
|
|
59
|
+
// `a#b.blp` is that file's whole name. Stripping turned it into `a`, which
|
|
60
|
+
// does not exist, and the input was dropped. So the unstripped id gets its
|
|
61
|
+
// turn — and when neither is a file, `inputs()` drops it as before.
|
|
62
|
+
return isFile(stripped) ? stripped : id;
|
|
63
|
+
}
|
|
64
|
+
function isFile(abs) {
|
|
65
|
+
try {
|
|
66
|
+
return statSync(abs).isFile();
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Files the TOOLCHAIN writes and deletes between builds, which therefore must
|
|
74
|
+
* never enter the input set.
|
|
75
|
+
*
|
|
76
|
+
* Not a filter on staleness but on membership: a vanished recorded input forces
|
|
77
|
+
* a rebuild (see {@link newestBundleInputMtimeMs}), so recording the
|
|
78
|
+
* globals-inject stub — `node_modules/.cache/gjsify/auto-globals-<hash>.mjs`,
|
|
79
|
+
* rewritten per build and DELETED when its config changes — would rebuild every
|
|
80
|
+
* run, forever. The same directory holds the cached GJS plugin bundles, which
|
|
81
|
+
* carry their own freshness rule (`bundleFileForGjsCached`).
|
|
82
|
+
*
|
|
83
|
+
* **Not a `tmpdir()` test**, which is the obvious wrong answer and the one this
|
|
84
|
+
* started as: the stub is written into the project's own cache directory
|
|
85
|
+
* (`utils/scan-globals.ts`), never to a temp dir, while a PROJECT under
|
|
86
|
+
* `/tmp` is a project — a CI job, a container, this suite's own fixtures. A
|
|
87
|
+
* freshness check that goes blind in those trees is the defect being fixed,
|
|
88
|
+
* reached from the other side.
|
|
89
|
+
*/
|
|
90
|
+
function isToolchainScratch(abs) {
|
|
91
|
+
return abs.includes(`${sep}node_modules${sep}.cache${sep}`);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The resolution files of the package a recorded input belongs to: the nearest
|
|
95
|
+
* `package.json` above it, and the `tsconfig.json` beside it when there is one.
|
|
96
|
+
*
|
|
97
|
+
* Neither is ever a module. A workspace package reached through a symlink
|
|
98
|
+
* contributes its files, but a `package.json#exports` edit — which moves what
|
|
99
|
+
* every bare specifier in the app resolves to — or a `tsconfig.json` that turns
|
|
100
|
+
* on a `paths` mapping changes the output with no file of its own in the graph.
|
|
101
|
+
* Per package, deduplicated, so the walk is a handful of stats however many
|
|
102
|
+
* modules that package contributed.
|
|
103
|
+
*/
|
|
104
|
+
function resolutionFilesFor(abs) {
|
|
105
|
+
const found = [];
|
|
106
|
+
for (let dir = dirname(abs);; dir = dirname(dir)) {
|
|
107
|
+
const manifest = join(dir, 'package.json');
|
|
108
|
+
if (existsSync(manifest)) {
|
|
109
|
+
found.push(manifest);
|
|
110
|
+
const tsconfig = join(dir, 'tsconfig.json');
|
|
111
|
+
if (existsSync(tsconfig))
|
|
112
|
+
found.push(tsconfig);
|
|
113
|
+
return found;
|
|
114
|
+
}
|
|
115
|
+
const parent = dirname(dir);
|
|
116
|
+
if (parent === dir)
|
|
117
|
+
return found;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* A plugin that records every module the build transforms, plus the writer for
|
|
122
|
+
* the manifest.
|
|
123
|
+
*
|
|
124
|
+
* `transform` rather than a graph API (`getModuleIds`/`getModuleInfo`) because
|
|
125
|
+
* it is the one hook both engines run and the native bridge translates
|
|
126
|
+
* (`bundler-pick.ts` picks it verbatim). Every module that reaches the
|
|
127
|
+
* transform stage was LOADED from disk, so the observed set is what the build
|
|
128
|
+
* read — the modules it then tree-shook away are still inputs of the next
|
|
129
|
+
* build, which is the direction that errs safely.
|
|
130
|
+
*
|
|
131
|
+
* The observation is KEPT alongside {@link BundleInputsRecorder.addWatchFiles}
|
|
132
|
+
* rather than replaced by it, and neither engine is the reason: the native
|
|
133
|
+
* engine has no watch list to hand over (`BundleResult` carries warnings and
|
|
134
|
+
* output only, and its JS context had no `addWatchFile` at all until
|
|
135
|
+
* `rolldown-native` grew one), so on GJS the observed set is the only account
|
|
136
|
+
* of the modules; on npm the watch list is a superset of it. Union: the cost of
|
|
137
|
+
* a redundant path is a `stat`, the cost of a missing one is a stale green.
|
|
138
|
+
*/
|
|
139
|
+
export function createBundleInputsRecorder(outfile) {
|
|
140
|
+
const seen = new Set();
|
|
141
|
+
// Sticky, and pessimistic: ONE pass without a watch list is enough to know
|
|
142
|
+
// the build cannot see a plugin-read input, and the artifact must not be
|
|
143
|
+
// judged fresh on the strength of the passes that could.
|
|
144
|
+
let watchList = true;
|
|
145
|
+
const record = (raw) => {
|
|
146
|
+
const abs = idToPath(raw);
|
|
147
|
+
if (abs !== null)
|
|
148
|
+
seen.add(abs);
|
|
149
|
+
};
|
|
150
|
+
const plugin = {
|
|
151
|
+
name: 'gjsify:bundle-inputs',
|
|
152
|
+
transform(_code, id) {
|
|
153
|
+
record(id);
|
|
154
|
+
return undefined;
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
const inputs = () => {
|
|
158
|
+
const out = [];
|
|
159
|
+
const packages = new Set();
|
|
160
|
+
for (const abs of seen) {
|
|
161
|
+
let isFile;
|
|
162
|
+
try {
|
|
163
|
+
isFile = statSync(abs).isFile();
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (!isFile || isToolchainScratch(abs))
|
|
169
|
+
continue;
|
|
170
|
+
out.push(abs);
|
|
171
|
+
// `resolutionFilesFor` walks to the package root; one call per
|
|
172
|
+
// DIRECTORY keeps a 1000-module bundle from walking 1000 times.
|
|
173
|
+
const dir = dirname(abs);
|
|
174
|
+
if (packages.has(dir))
|
|
175
|
+
continue;
|
|
176
|
+
packages.add(dir);
|
|
177
|
+
for (const meta of resolutionFilesFor(abs))
|
|
178
|
+
out.push(meta);
|
|
179
|
+
}
|
|
180
|
+
return [...new Set(out)].sort();
|
|
181
|
+
};
|
|
182
|
+
return {
|
|
183
|
+
plugin,
|
|
184
|
+
addWatchFiles(watch) {
|
|
185
|
+
if (!watch.reportedByEngine)
|
|
186
|
+
watchList = false;
|
|
187
|
+
for (const id of watch.files)
|
|
188
|
+
record(id);
|
|
189
|
+
},
|
|
190
|
+
inputs,
|
|
191
|
+
write() {
|
|
192
|
+
const path = bundleInputsPath(outfile);
|
|
193
|
+
const manifest = { version: BUNDLE_INPUTS_VERSION, inputs: inputs(), watchList };
|
|
194
|
+
const tmp = `${path}.tmp`;
|
|
195
|
+
try {
|
|
196
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
197
|
+
// Rename, not write: a manifest is read by the NEXT process, and
|
|
198
|
+
// a half-written one is a parse error there — which reads as "no
|
|
199
|
+
// manifest" and silently degrades the check to the package walk.
|
|
200
|
+
writeFileSync(tmp, `${JSON.stringify(manifest, null, 2)}\n`, 'utf-8');
|
|
201
|
+
renameSync(tmp, path);
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
try {
|
|
205
|
+
rmSync(tmp, { force: true });
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// Nothing left to clean up — the write never got that far.
|
|
209
|
+
}
|
|
210
|
+
console.warn(`[gjsify test] could not record the build inputs beside ${path} (${err.message}) — ` +
|
|
211
|
+
'the next run judges freshness by the package walk alone, which cannot see outside this package.');
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* The inputs recorded for `outfile` by the build that wrote it, or `null` when
|
|
218
|
+
* there is none to trust — never written, unreadable, or written by another
|
|
219
|
+
* version of this shape. `null` is the fallback the caller must handle, not an
|
|
220
|
+
* error: a bundle built by an older CLI, or copied in from elsewhere, has no
|
|
221
|
+
* manifest and is judged by the package walk alone.
|
|
222
|
+
*/
|
|
223
|
+
export function readBundleInputs(outfile) {
|
|
224
|
+
let parsed;
|
|
225
|
+
try {
|
|
226
|
+
parsed = JSON.parse(readFileSync(bundleInputsPath(outfile), 'utf-8'));
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
if (parsed?.version !== BUNDLE_INPUTS_VERSION || !Array.isArray(parsed.inputs))
|
|
232
|
+
return null;
|
|
233
|
+
return {
|
|
234
|
+
inputs: parsed.inputs.filter((abs) => typeof abs === 'string'),
|
|
235
|
+
// v2 only, so a missing flag cannot be v2 — but the pessimistic reading
|
|
236
|
+
// is kept anyway: a manifest that does not vouch for its engine's watch
|
|
237
|
+
// list is not one to judge freshness on.
|
|
238
|
+
watchList: parsed.watchList === true,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Newest mtime (ms) among recorded inputs, or 0 for an empty set.
|
|
243
|
+
*
|
|
244
|
+
* A recorded input that has since VANISHED counts as CHANGED, exactly as the
|
|
245
|
+
* package walk counts one that vanished mid-walk: an input we cannot read is
|
|
246
|
+
* not evidence that the artifact is current. A workspace-package source deleted
|
|
247
|
+
* while the bundle still imports it is precisely that case, and skipping it
|
|
248
|
+
* would rerun a bundle built from a file that no longer exists.
|
|
249
|
+
*
|
|
250
|
+
* Safe only because the toolchain's own scratch is never RECORDED
|
|
251
|
+
* ({@link isToolchainScratch}): the globals-inject stub and the cached plugin
|
|
252
|
+
* bundles are written and deleted between builds, and treating their absence as
|
|
253
|
+
* a change would rebuild on every run, forever.
|
|
254
|
+
*/
|
|
255
|
+
export function newestBundleInputMtimeMs(inputs) {
|
|
256
|
+
let newest = 0;
|
|
257
|
+
for (const abs of inputs) {
|
|
258
|
+
try {
|
|
259
|
+
const { mtimeMs } = statSync(abs);
|
|
260
|
+
if (mtimeMs > newest)
|
|
261
|
+
newest = mtimeMs;
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
return Number.POSITIVE_INFINITY;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return newest;
|
|
268
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Win32ResolveContext } from './win32-command.js';
|
|
1
2
|
export type DepSeverity = 'required' | 'optional';
|
|
2
3
|
export interface DepCheck {
|
|
3
4
|
/** Stable key used for install command lookup, e.g. "gjs" */
|
|
@@ -15,7 +16,8 @@ export interface DepCheck {
|
|
|
15
16
|
export type PackageManager = 'apt' | 'dnf' | 'pacman' | 'zypper' | 'apk' | 'brew' | 'winget' | 'unknown';
|
|
16
17
|
/**
|
|
17
18
|
* True when `cmd` is on PATH. Walks `process.env.PATH` with `existsSync`
|
|
18
|
-
* instead of shelling out to `which`(1)
|
|
19
|
+
* instead of shelling out to `which`(1), honouring `PATHEXT` on win32 — see
|
|
20
|
+
* `findOnPath` in `win32-command.ts`, which owns the walk.
|
|
19
21
|
*
|
|
20
22
|
* `which` is NOT a probe you can rely on: the Fedora 43/44 minimal containers
|
|
21
23
|
* CI runs in ship without it (only the `which-2.x` rpm provides it), so a
|
|
@@ -31,7 +33,7 @@ export type PackageManager = 'apt' | 'dnf' | 'pacman' | 'zypper' | 'apk' | 'brew
|
|
|
31
33
|
* reason. It stays a second copy on purpose: it is `.mjs` under `tests/` and
|
|
32
34
|
* cannot be imported from the shipped CLI.
|
|
33
35
|
*/
|
|
34
|
-
export declare function isOnPath(cmd: string): boolean;
|
|
36
|
+
export declare function isOnPath(cmd: string, ctx?: Win32ResolveContext): boolean;
|
|
35
37
|
/**
|
|
36
38
|
* The package manager whose install command we may suggest.
|
|
37
39
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// System dependency checker for gjsify CLI.
|
|
2
|
-
// Uses
|
|
2
|
+
// Uses spawnSync with explicit argument arrays — no shell injection possible.
|
|
3
3
|
// All binary names are hardcoded constants, never derived from user input.
|
|
4
4
|
//
|
|
5
5
|
// Severity model:
|
|
@@ -10,25 +10,31 @@
|
|
|
10
10
|
// optional system deps are only checked if the corresponding @gjsify/* package
|
|
11
11
|
// is in the project's dependency tree. A user with only @gjsify/fs in their
|
|
12
12
|
// project never sees a warning about libmanette.
|
|
13
|
-
import {
|
|
13
|
+
import { spawnSync } from 'node:child_process';
|
|
14
14
|
import { dirname, join, resolve } from 'node:path';
|
|
15
15
|
import { createRequire } from 'node:module';
|
|
16
16
|
import { pathToFileURL } from 'node:url';
|
|
17
17
|
import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
|
18
18
|
import { isNode } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
19
19
|
import { findSystemTypelib } from './gi-typelib.js';
|
|
20
|
+
import { toSpawnable } from './spawn.js';
|
|
21
|
+
import { findOnPath } from './win32-command.js';
|
|
20
22
|
/** Run a binary and return its stdout trimmed, or null if it fails. */
|
|
21
23
|
function tryExecFile(binary, args) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
// `spawnSync`, not `execFileSync`: only the former takes the verbatim-argv
|
|
25
|
+
// flag the cmd.exe route of `toSpawnable` needs.
|
|
26
|
+
const inv = toSpawnable(binary, args);
|
|
27
|
+
const r = spawnSync(inv.cmd, inv.args, {
|
|
28
|
+
encoding: 'utf-8',
|
|
29
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
30
|
+
windowsVerbatimArguments: inv.windowsVerbatimArguments,
|
|
31
|
+
});
|
|
32
|
+
return r.error || r.status !== 0 ? null : r.stdout.trim();
|
|
28
33
|
}
|
|
29
34
|
/**
|
|
30
35
|
* True when `cmd` is on PATH. Walks `process.env.PATH` with `existsSync`
|
|
31
|
-
* instead of shelling out to `which`(1)
|
|
36
|
+
* instead of shelling out to `which`(1), honouring `PATHEXT` on win32 — see
|
|
37
|
+
* `findOnPath` in `win32-command.ts`, which owns the walk.
|
|
32
38
|
*
|
|
33
39
|
* `which` is NOT a probe you can rely on: the Fedora 43/44 minimal containers
|
|
34
40
|
* CI runs in ship without it (only the `which-2.x` rpm provides it), so a
|
|
@@ -44,23 +50,12 @@ function tryExecFile(binary, args) {
|
|
|
44
50
|
* reason. It stays a second copy on purpose: it is `.mjs` under `tests/` and
|
|
45
51
|
* cannot be imported from the shipped CLI.
|
|
46
52
|
*/
|
|
47
|
-
export function isOnPath(cmd) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (!dir)
|
|
54
|
-
continue;
|
|
55
|
-
try {
|
|
56
|
-
if (existsSync(join(dir, cmd)))
|
|
57
|
-
return true;
|
|
58
|
-
}
|
|
59
|
-
catch {
|
|
60
|
-
// An inaccessible PATH entry is not an answer about `cmd`.
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return false;
|
|
53
|
+
export function isOnPath(cmd, ctx = hostPathContext()) {
|
|
54
|
+
return findOnPath(cmd, ctx) !== undefined;
|
|
55
|
+
}
|
|
56
|
+
/** This process's PATH, filesystem and platform — what {@link isOnPath} asks by default. */
|
|
57
|
+
function hostPathContext() {
|
|
58
|
+
return { platform: process.platform, env: process.env, exists: existsSync, join };
|
|
64
59
|
}
|
|
65
60
|
/** Check if a binary exists and optionally capture its version output. */
|
|
66
61
|
function checkBinary(id, name, binary, versionArgs, severity, parseVersion, requiredBy) {
|
package/lib/utils/dev-link.d.ts
CHANGED
|
@@ -184,11 +184,14 @@ export declare function assertDevLinksBuilt(links: readonly DevLink[]): void;
|
|
|
184
184
|
/**
|
|
185
185
|
* Make `.gjsify-link.json` invisible to git WITHOUT editing a tracked file.
|
|
186
186
|
*
|
|
187
|
-
*
|
|
188
|
-
* not shared, which is exactly the lifetime of the thing it
|
|
189
|
-
* `.gitignore` instead would put a developer's local override
|
|
190
|
-
* else's diff — the manifest edit this whole feature exists to
|
|
191
|
-
* over.
|
|
187
|
+
* `info/exclude` is git's own per-clone ignore list: it is not committed and
|
|
188
|
+
* not shared between clones, which is exactly the lifetime of the thing it
|
|
189
|
+
* hides. Writing to `.gitignore` instead would put a developer's local override
|
|
190
|
+
* into everyone else's diff — the manifest edit this whole feature exists to
|
|
191
|
+
* avoid, one file over. A WORKTREE is the case to get right: the file is
|
|
192
|
+
* written under the repository's COMMON dir, because `info/exclude` is shared
|
|
193
|
+
* across a repository's worktrees and the worktree's own gitdir is not read
|
|
194
|
+
* (see {@link resolveGitDir}).
|
|
192
195
|
*
|
|
193
196
|
* Returns what it did, so the caller can say so. A consumer that is not a git
|
|
194
197
|
* repository is not an error: nothing can commit the file there either. A
|
|
@@ -199,7 +202,7 @@ export declare function ensureLocallyIgnored(consumerRoot: string): 'added' | 'a
|
|
|
199
202
|
/**
|
|
200
203
|
* Undo {@link ensureLocallyIgnored} — the other half of `unlink`.
|
|
201
204
|
*
|
|
202
|
-
* Measured: the
|
|
205
|
+
* Measured: the `info/exclude` block outlived `gjsify unlink` (idempotently,
|
|
203
206
|
* so it never grew — it just never left). A command that promises to undo itself
|
|
204
207
|
* and leaves a line behind is not an undo, and the leftover is a rule about a file
|
|
205
208
|
* that no longer exists, which the next reader has to research to delete.
|
package/lib/utils/dev-link.js
CHANGED
|
@@ -484,11 +484,14 @@ const IGNORE_COMMENT = '# gjsify link — local development override, never comm
|
|
|
484
484
|
/**
|
|
485
485
|
* Make `.gjsify-link.json` invisible to git WITHOUT editing a tracked file.
|
|
486
486
|
*
|
|
487
|
-
*
|
|
488
|
-
* not shared, which is exactly the lifetime of the thing it
|
|
489
|
-
* `.gitignore` instead would put a developer's local override
|
|
490
|
-
* else's diff — the manifest edit this whole feature exists to
|
|
491
|
-
* over.
|
|
487
|
+
* `info/exclude` is git's own per-clone ignore list: it is not committed and
|
|
488
|
+
* not shared between clones, which is exactly the lifetime of the thing it
|
|
489
|
+
* hides. Writing to `.gitignore` instead would put a developer's local override
|
|
490
|
+
* into everyone else's diff — the manifest edit this whole feature exists to
|
|
491
|
+
* avoid, one file over. A WORKTREE is the case to get right: the file is
|
|
492
|
+
* written under the repository's COMMON dir, because `info/exclude` is shared
|
|
493
|
+
* across a repository's worktrees and the worktree's own gitdir is not read
|
|
494
|
+
* (see {@link resolveGitDir}).
|
|
492
495
|
*
|
|
493
496
|
* Returns what it did, so the caller can say so. A consumer that is not a git
|
|
494
497
|
* repository is not an error: nothing can commit the file there either. A
|
|
@@ -497,12 +500,12 @@ const IGNORE_COMMENT = '# gjsify link — local development override, never comm
|
|
|
497
500
|
*/
|
|
498
501
|
export function ensureLocallyIgnored(consumerRoot) {
|
|
499
502
|
const found = resolveGitDir(consumerRoot);
|
|
500
|
-
if (!found.
|
|
503
|
+
if (!found.commonDir)
|
|
501
504
|
return found.why;
|
|
502
505
|
const gitignore = join(consumerRoot, '.gitignore');
|
|
503
506
|
if (existsSync(gitignore) && fileMentionsPattern(gitignore, DEV_LINK_FILE))
|
|
504
507
|
return 'already-ignored';
|
|
505
|
-
const excludePath = join(found.
|
|
508
|
+
const excludePath = join(found.commonDir, 'info', 'exclude');
|
|
506
509
|
if (existsSync(excludePath) && fileMentionsPattern(excludePath, DEV_LINK_FILE))
|
|
507
510
|
return 'already-ignored';
|
|
508
511
|
mkdirSync(dirname(excludePath), { recursive: true });
|
|
@@ -514,7 +517,7 @@ export function ensureLocallyIgnored(consumerRoot) {
|
|
|
514
517
|
/**
|
|
515
518
|
* Undo {@link ensureLocallyIgnored} — the other half of `unlink`.
|
|
516
519
|
*
|
|
517
|
-
* Measured: the
|
|
520
|
+
* Measured: the `info/exclude` block outlived `gjsify unlink` (idempotently,
|
|
518
521
|
* so it never grew — it just never left). A command that promises to undo itself
|
|
519
522
|
* and leaves a line behind is not an undo, and the leftover is a rule about a file
|
|
520
523
|
* that no longer exists, which the next reader has to research to delete.
|
|
@@ -525,9 +528,9 @@ export function ensureLocallyIgnored(consumerRoot) {
|
|
|
525
528
|
*/
|
|
526
529
|
export function removeLocalIgnore(consumerRoot) {
|
|
527
530
|
const found = resolveGitDir(consumerRoot);
|
|
528
|
-
if (!found.
|
|
531
|
+
if (!found.commonDir)
|
|
529
532
|
return found.why;
|
|
530
|
-
const excludePath = join(found.
|
|
533
|
+
const excludePath = join(found.commonDir, 'info', 'exclude');
|
|
531
534
|
let text;
|
|
532
535
|
try {
|
|
533
536
|
text = readFileSync(excludePath, 'utf-8');
|
|
@@ -568,10 +571,21 @@ function fileMentionsPattern(file, pattern) {
|
|
|
568
571
|
});
|
|
569
572
|
}
|
|
570
573
|
/**
|
|
571
|
-
* The
|
|
572
|
-
*
|
|
573
|
-
*
|
|
574
|
-
*
|
|
574
|
+
* The COMMON dir of `root`'s repository — the one whose `info/exclude` git
|
|
575
|
+
* actually reads. Two indirections get there, and in a worktree neither the
|
|
576
|
+
* repository root nor the gitdir it points at IS that directory.
|
|
577
|
+
*
|
|
578
|
+
* The first is `.git`: a directory in a clone, a `gitdir:` pointer file in a
|
|
579
|
+
* submodule, and a `gitdir:` pointer to `<common>/worktrees/<name>` in a worktree.
|
|
580
|
+
* Skipping that indirection would write an `info/exclude` inside a plain file's
|
|
581
|
+
* parent and silently ignore nothing at all.
|
|
582
|
+
*
|
|
583
|
+
* The second is `commondir`, and it is the one that was missed: `info/exclude` is
|
|
584
|
+
* SHARED across a repository's worktrees (git-worktree(1)), so in a worktree the
|
|
585
|
+
* pointer leads AWAY from the file git reads. Measured: with the entry written
|
|
586
|
+
* under `<common>/worktrees/<name>/info/exclude`, `git status` in the worktree
|
|
587
|
+
* still printed `?? .gjsify-link.json` and `git check-ignore` named no rule — one
|
|
588
|
+
* `git add -A` from committing the override this feature exists to keep out.
|
|
575
589
|
*
|
|
576
590
|
* TWO outcomes, not one. "There is no repository here" and "there is one and its
|
|
577
591
|
* pointer does not parse" call for opposite reactions — the first is fine, the
|
|
@@ -585,22 +599,60 @@ function resolveGitDir(root) {
|
|
|
585
599
|
stat = statSync(dotGit);
|
|
586
600
|
}
|
|
587
601
|
catch {
|
|
588
|
-
return {
|
|
602
|
+
return { commonDir: null, why: 'no-git' };
|
|
589
603
|
}
|
|
590
604
|
if (stat.isDirectory())
|
|
591
|
-
return {
|
|
605
|
+
return { commonDir: dotGit, why: 'no-git' };
|
|
592
606
|
let pointer;
|
|
593
607
|
try {
|
|
594
608
|
pointer = readFileSync(dotGit, 'utf-8').trim();
|
|
595
609
|
}
|
|
596
610
|
catch {
|
|
597
|
-
return {
|
|
611
|
+
return { commonDir: null, why: 'unreadable-git' };
|
|
598
612
|
}
|
|
599
613
|
const match = /^gitdir:\s*(.+)$/.exec(pointer);
|
|
600
614
|
const target = match?.[1]?.trim();
|
|
601
615
|
if (!target)
|
|
602
|
-
return {
|
|
603
|
-
return
|
|
616
|
+
return { commonDir: null, why: 'unreadable-git' };
|
|
617
|
+
return followCommondir(isAbsolute(target) ? target : resolve(root, target));
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* The shared directory of the gitdir a `gitdir:` pointer named.
|
|
621
|
+
*
|
|
622
|
+
* A plain clone and a submodule have no `commondir` file: there the gitdir IS the
|
|
623
|
+
* common dir, and nothing changes. A worktree has one — `../..`, relative to the
|
|
624
|
+
* worktree gitdir, is what `git worktree add` writes — and an absolute path is
|
|
625
|
+
* honored rather than resolved against the gitdir, where it would name a
|
|
626
|
+
* directory that does not exist.
|
|
627
|
+
*
|
|
628
|
+
* A `commondir` that names NOTHING is not read as absent, which is the decision
|
|
629
|
+
* this shape exists to force. Measured against real git, all three such shapes
|
|
630
|
+
* exit 128: blank → "error reading …/commondir", a directory → the same, a path
|
|
631
|
+
* that is not there → "not a git repository". So there is no answer to fall back
|
|
632
|
+
* to, and `no-git` would announce "nothing here can commit the file" about a
|
|
633
|
+
* repository that just refused to be one — the same "I could not tell" that is not
|
|
634
|
+
* a "yes", as everywhere else in this file.
|
|
635
|
+
*/
|
|
636
|
+
function followCommondir(gitDir) {
|
|
637
|
+
const file = join(gitDir, 'commondir');
|
|
638
|
+
if (!existsSync(file))
|
|
639
|
+
return { commonDir: gitDir, why: 'no-git' };
|
|
640
|
+
let raw;
|
|
641
|
+
try {
|
|
642
|
+
raw = readFileSync(file, 'utf-8');
|
|
643
|
+
}
|
|
644
|
+
catch {
|
|
645
|
+
return { commonDir: null, why: 'unreadable-git' };
|
|
646
|
+
}
|
|
647
|
+
const target = raw.trim();
|
|
648
|
+
if (!target)
|
|
649
|
+
return { commonDir: null, why: 'unreadable-git' };
|
|
650
|
+
const commonDir = isAbsolute(target) ? target : resolve(gitDir, target);
|
|
651
|
+
// The third shape git refuses: writing an `info/exclude` under a directory that
|
|
652
|
+
// is not there would create a tree that nothing ever reads.
|
|
653
|
+
if (!existsSync(commonDir))
|
|
654
|
+
return { commonDir: null, why: 'unreadable-git' };
|
|
655
|
+
return { commonDir, why: 'no-git' };
|
|
604
656
|
}
|
|
605
657
|
/**
|
|
606
658
|
* `--immutable` must never accept an active override — FAIL CLOSED.
|