@gjsify/cli 0.21.0 → 0.23.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/README.md +24 -1
- package/dist/cli.gjs.mjs +257 -184
- package/lib/bundler-pick.js +12 -4
- package/lib/commands/affected.js +22 -10
- package/lib/commands/check.js +38 -29
- package/lib/commands/flatpak/build.js +19 -18
- package/lib/commands/foreach.js +48 -41
- package/lib/commands/info.js +11 -5
- package/lib/commands/install.d.ts +16 -0
- package/lib/commands/install.js +103 -32
- package/lib/commands/onboard.js +6 -1
- package/lib/commands/pack.js +69 -4
- package/lib/commands/run.js +26 -14
- package/lib/commands/trust.js +4 -62
- package/lib/commands/workspace.js +49 -30
- package/lib/utils/bin-shim.d.ts +66 -0
- package/lib/utils/bin-shim.js +246 -0
- package/lib/utils/build-cache.d.ts +42 -6
- package/lib/utils/build-cache.js +114 -42
- package/lib/utils/detect-native-packages.d.ts +112 -6
- package/lib/utils/detect-native-packages.js +201 -29
- package/lib/utils/install-backend-native.d.ts +55 -0
- package/lib/utils/install-backend-native.js +152 -13
- package/lib/utils/install-global.d.ts +16 -0
- package/lib/utils/install-global.js +58 -8
- package/lib/utils/oxc-resolve.js +18 -55
- package/lib/utils/publish-headers.d.ts +2 -3
- package/lib/utils/publish-headers.js +64 -3
- package/lib/utils/publishable-packages.d.ts +22 -0
- package/lib/utils/publishable-packages.js +93 -0
- package/lib/utils/run-gjs.d.ts +6 -7
- package/lib/utils/run-gjs.js +4 -3
- package/lib/utils/spawn.d.ts +82 -0
- package/lib/utils/spawn.js +139 -0
- package/lib/utils/stdin.d.ts +25 -0
- package/lib/utils/stdin.js +132 -0
- package/package.json +23 -18
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
import * as fs from 'node:fs';
|
|
24
24
|
import * as os from 'node:os';
|
|
25
25
|
import * as path from 'node:path';
|
|
26
|
-
import { detectNativePackages } from './detect-native-packages.js';
|
|
26
|
+
import { detectNativePackages, libraryPathVar } from './detect-native-packages.js';
|
|
27
|
+
import { buildLauncherShims, parseShebang } from './bin-shim.js';
|
|
27
28
|
/**
|
|
28
29
|
* Compute the canonical global install layout for the current user. Honours
|
|
29
30
|
* `XDG_DATA_HOME` (per the XDG Base Directory Spec) plus
|
|
@@ -174,7 +175,9 @@ export function linkGlobalBins(packageNames, layout) {
|
|
|
174
175
|
/* best effort */
|
|
175
176
|
}
|
|
176
177
|
const linkPath = path.join(layout.binDir, binName);
|
|
177
|
-
|
|
178
|
+
for (const stale of [linkPath, `${linkPath}.cmd`, `${linkPath}.ps1`]) {
|
|
179
|
+
fs.rmSync(stale, { force: true, maxRetries: 10, retryDelay: 100 });
|
|
180
|
+
}
|
|
178
181
|
// Inline `${target}` directly — this file is rewritten on every
|
|
179
182
|
// install, paths are user-owned, and POSIX `sh` quoting via
|
|
180
183
|
// single-quotes plus `'\''` for embedded quotes is well-defined.
|
|
@@ -196,6 +199,36 @@ export function linkGlobalBins(packageNames, layout) {
|
|
|
196
199
|
: `#!/bin/sh\nexec ${shQuote(targetAbs)} "$@"\n`;
|
|
197
200
|
fs.writeFileSync(linkPath, launcher);
|
|
198
201
|
fs.chmodSync(linkPath, 0o755);
|
|
202
|
+
// Windows runs neither an extension-less file nor a `#!` line, so
|
|
203
|
+
// the `sh` launcher above is only reachable from git-bash/MSYS/WSL.
|
|
204
|
+
// Write the `.cmd`/`.ps1` companions cmd.exe and pwsh need — the
|
|
205
|
+
// same three-file layout npm's cmd-shim produces.
|
|
206
|
+
if (process.platform === 'win32') {
|
|
207
|
+
const joined = nativePrebuildDirs.join(';');
|
|
208
|
+
// No dedicated DLL-search variable on Windows: LoadLibrary
|
|
209
|
+
// searches PATH, and GLib's search-path separator is `;` there
|
|
210
|
+
// too — so both variables take the same `;`-joined value.
|
|
211
|
+
const prependEnv = isGjsBundle && nativePrebuildDirs.length > 0 ? { GI_TYPELIB_PATH: joined, PATH: joined } : {};
|
|
212
|
+
// A non-bundle bin is exec'd directly by the sh launcher, which
|
|
213
|
+
// relies on its `#!` line; batch/pwsh have no shebang support,
|
|
214
|
+
// so read the interpreter out of it the way cmd-shim does
|
|
215
|
+
// (defaulting to `node`, what every npm bin declares).
|
|
216
|
+
const shebang = isGjsBundle ? null : readShebangOf(targetAbs);
|
|
217
|
+
const shims = isGjsBundle
|
|
218
|
+
? buildLauncherShims({
|
|
219
|
+
interpreter: 'gjs',
|
|
220
|
+
interpreterArgs: ['-m'],
|
|
221
|
+
target: targetAbs,
|
|
222
|
+
prependEnv,
|
|
223
|
+
})
|
|
224
|
+
: buildLauncherShims({
|
|
225
|
+
interpreter: shebang?.prog || 'node',
|
|
226
|
+
interpreterArgs: shebang?.args.trim() ? shebang.args.trim().split(/\s+/) : [],
|
|
227
|
+
target: targetAbs,
|
|
228
|
+
});
|
|
229
|
+
fs.writeFileSync(`${linkPath}.cmd`, shims.cmd);
|
|
230
|
+
fs.writeFileSync(`${linkPath}.ps1`, shims.ps1);
|
|
231
|
+
}
|
|
199
232
|
created.push({ name: binName, target: targetAbs, link: linkPath });
|
|
200
233
|
}
|
|
201
234
|
}
|
|
@@ -206,20 +239,37 @@ function shQuote(s) {
|
|
|
206
239
|
}
|
|
207
240
|
/**
|
|
208
241
|
* Build the POSIX-sh `export` lines that prepend the given prebuild
|
|
209
|
-
* directories to GI_TYPELIB_PATH and
|
|
210
|
-
* inherited from the user's environment is preserved as
|
|
211
|
-
* user-installed typelibs/libraries still resolve.
|
|
242
|
+
* directories to GI_TYPELIB_PATH and the host's shared-library search path.
|
|
243
|
+
* Any pre-existing value inherited from the user's environment is preserved as
|
|
244
|
+
* a suffix so user-installed typelibs/libraries still resolve.
|
|
245
|
+
*
|
|
246
|
+
* The library variable is host-dependent — `LD_LIBRARY_PATH` on Linux,
|
|
247
|
+
* `DYLD_LIBRARY_PATH` on macOS (dyld never reads the ELF one), so a launcher
|
|
248
|
+
* written on a Mac used to export a variable nothing consults, leaving every
|
|
249
|
+
* `darwin-arm64` prebuild unloadable. `platform` is a parameter so both
|
|
250
|
+
* branches are unit-testable from either host.
|
|
212
251
|
*
|
|
213
252
|
* Returns the empty string when no prebuilds were found — avoids emitting an
|
|
214
253
|
* inert assignment in the launcher.
|
|
215
254
|
*/
|
|
216
|
-
function buildLauncherEnvPreamble(prebuildsDirs) {
|
|
255
|
+
export function buildLauncherEnvPreamble(prebuildsDirs, platform = process.platform) {
|
|
217
256
|
if (prebuildsDirs.length === 0)
|
|
218
257
|
return '';
|
|
258
|
+
const { name: libVar } = libraryPathVar(platform);
|
|
219
259
|
const joined = shQuote(prebuildsDirs.join(':'));
|
|
220
260
|
return (`GI_TYPELIB_PATH=${joined}\${GI_TYPELIB_PATH:+":$GI_TYPELIB_PATH"}\n` +
|
|
221
|
-
|
|
222
|
-
`export GI_TYPELIB_PATH
|
|
261
|
+
`${libVar}=${joined}\${${libVar}:+":$${libVar}"}\n` +
|
|
262
|
+
`export GI_TYPELIB_PATH ${libVar}\n`);
|
|
263
|
+
}
|
|
264
|
+
/** Parse a bin file's `#!` line; `null` when unreadable or not a shebang. */
|
|
265
|
+
function readShebangOf(file) {
|
|
266
|
+
try {
|
|
267
|
+
const head = fs.readFileSync(file, 'utf-8');
|
|
268
|
+
return parseShebang(head.trimStart().split(/\r*\n/, 1)[0] ?? '');
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
223
273
|
}
|
|
224
274
|
function pickBinMap(pkgName, pkgJson) {
|
|
225
275
|
const gjsifyEntry = pkgJson.gjsify;
|
package/lib/utils/oxc-resolve.js
CHANGED
|
@@ -48,12 +48,12 @@
|
|
|
48
48
|
// (tracked in STATUS.md Open TODOs).
|
|
49
49
|
import { existsSync, readFileSync } from 'node:fs';
|
|
50
50
|
import { join, resolve } from 'node:path';
|
|
51
|
-
import { spawn, spawnSync } from 'node:child_process';
|
|
52
51
|
import { createRequire } from 'node:module';
|
|
53
52
|
import { pathToFileURL } from 'node:url';
|
|
54
53
|
import { findWorkspaceRoot } from './workspace-root.js';
|
|
55
54
|
import { resolveNpmPackage } from './resolve-npm-package.js';
|
|
56
55
|
import { nodeBinary } from './run-node.js';
|
|
56
|
+
import { spawnToCompletion } from './spawn.js';
|
|
57
57
|
import { isGjs, gjsExit } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
58
58
|
/**
|
|
59
59
|
* Map Node.js `process.platform` + `arch` (+ musl on Linux) to the
|
|
@@ -298,6 +298,14 @@ export async function runOxc(tool, args, opts = {}) {
|
|
|
298
298
|
* launcher as its first positional (yargs prints top-level help) and the parent
|
|
299
299
|
* hangs waiting on a child that never does the intended work. `nodeBinary()`
|
|
300
300
|
* resolves to PATH `node` under GJS, so the launcher runs on Node as required.
|
|
301
|
+
*
|
|
302
|
+
* `completion: 'return'`: `lint` / `format` / `fix` hand the tool's exit code
|
|
303
|
+
* to `setOxcExitCode`, which deliberately does NOT terminate the process on a
|
|
304
|
+
* clean run — so under GJS this must take `spawnToCompletion`'s blocking path
|
|
305
|
+
* (see `utils/spawn.ts`; the async one would leave the main loop `spawn()`
|
|
306
|
+
* arms parked at 0% CPU on the way out). That path also captures and re-emits
|
|
307
|
+
* the tool's stdout/stderr with a raised `maxBuffer`, which is exactly what a
|
|
308
|
+
* full-workspace lint needs.
|
|
301
309
|
*/
|
|
302
310
|
function spawnOxcLauncher(tool, args, opts = {}) {
|
|
303
311
|
const cwd = opts.cwd ?? process.cwd();
|
|
@@ -306,61 +314,16 @@ function spawnOxcLauncher(tool, args, opts = {}) {
|
|
|
306
314
|
if (opts.verbose) {
|
|
307
315
|
console.log(`[gjsify oxc] ${node} ${launcher} ${args.join(' ')}`);
|
|
308
316
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
// fine (and keeps the event loop free), so it is kept there.
|
|
318
|
-
if (isGjs()) {
|
|
319
|
-
// Capture stdout/stderr (`'inherit'` is not forwarded through the GJS
|
|
320
|
-
// spawnSync path — the tool's diagnostics would be silently lost), then
|
|
321
|
-
// re-emit them so oxlint/oxfmt's own output still reaches the user.
|
|
322
|
-
// maxBuffer generous: a full-workspace lint with many findings can emit
|
|
323
|
-
// well past the 1 MB Node default; overflow would truncate diagnostics.
|
|
324
|
-
const r = spawnSync(node, [launcher, ...args], {
|
|
325
|
-
stdio: ['inherit', 'pipe', 'pipe'],
|
|
326
|
-
cwd,
|
|
327
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
328
|
-
});
|
|
329
|
-
if (r.error) {
|
|
330
|
-
const code = r.error.code;
|
|
331
|
-
if (code === 'ENOENT')
|
|
332
|
-
return Promise.reject(new OxcNotFoundError(tool, cwd));
|
|
333
|
-
return Promise.reject(r.error);
|
|
334
|
-
}
|
|
335
|
-
if (r.stdout && r.stdout.length)
|
|
336
|
-
process.stdout.write(r.stdout);
|
|
337
|
-
if (r.stderr && r.stderr.length)
|
|
338
|
-
process.stderr.write(r.stderr);
|
|
339
|
-
if (r.signal) {
|
|
340
|
-
console.error(`[gjsify oxc] ${tool} terminated by signal ${r.signal}`);
|
|
341
|
-
return Promise.resolve(1);
|
|
317
|
+
return spawnToCompletion(node, [launcher, ...args], {
|
|
318
|
+
completion: 'return',
|
|
319
|
+
cwd,
|
|
320
|
+
notFound: () => new OxcNotFoundError(tool, cwd),
|
|
321
|
+
}).then(({ code, signal }) => {
|
|
322
|
+
if (signal !== null) {
|
|
323
|
+
console.error(`[gjsify oxc] ${tool} terminated by signal ${signal}`);
|
|
324
|
+
return 1;
|
|
342
325
|
}
|
|
343
|
-
return
|
|
344
|
-
}
|
|
345
|
-
return new Promise((res, rej) => {
|
|
346
|
-
const spawnOpts = { stdio: 'inherit', cwd };
|
|
347
|
-
const child = spawn(node, [launcher, ...args], spawnOpts);
|
|
348
|
-
child.on('error', (err) => {
|
|
349
|
-
if (err.code === 'ENOENT') {
|
|
350
|
-
rej(new OxcNotFoundError(tool, cwd));
|
|
351
|
-
}
|
|
352
|
-
else {
|
|
353
|
-
rej(err);
|
|
354
|
-
}
|
|
355
|
-
});
|
|
356
|
-
child.on('exit', (code, signal) => {
|
|
357
|
-
if (signal) {
|
|
358
|
-
console.error(`[gjsify oxc] ${tool} terminated by signal ${signal}`);
|
|
359
|
-
res(1);
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
res(code ?? 0);
|
|
363
|
-
});
|
|
326
|
+
return code ?? 0;
|
|
364
327
|
});
|
|
365
328
|
}
|
|
366
329
|
/** Convenience wrappers. */
|
|
@@ -24,9 +24,8 @@ export interface PublishHeaderOptions {
|
|
|
24
24
|
*/
|
|
25
25
|
authorization?: string;
|
|
26
26
|
/**
|
|
27
|
-
* npm user-agent. Defaults to an npm-CLI-shaped string
|
|
28
|
-
*
|
|
29
|
-
* `gjsify-install/0.3.7` placeholder is not used here.
|
|
27
|
+
* npm user-agent. Defaults to an npm-CLI-shaped string carrying this CLI's
|
|
28
|
+
* real version — never a hard-coded placeholder (see {@link cliVersion}).
|
|
30
29
|
*/
|
|
31
30
|
userAgent?: string;
|
|
32
31
|
}
|
|
@@ -27,8 +27,12 @@
|
|
|
27
27
|
// literal `@`. (gjsify's URL encoding already matches this — it was never
|
|
28
28
|
// the bug.)
|
|
29
29
|
//
|
|
30
|
-
// This module
|
|
31
|
-
// and GJS
|
|
30
|
+
// This module pulls in no `gi://` and no runtime fetch/Soup stack, so it is
|
|
31
|
+
// unit-testable on both Node and GJS. Its one filesystem touch is the
|
|
32
|
+
// manifest walk backing the user-agent version — see `cliVersion()`.
|
|
33
|
+
import { readFileSync } from 'node:fs';
|
|
34
|
+
import { dirname } from 'node:path';
|
|
35
|
+
import { fileURLToPath } from 'node:url';
|
|
32
36
|
import { buildHeaders } from '@gjsify/npm-registry';
|
|
33
37
|
/**
|
|
34
38
|
* Percent-encode a package name for the publish PUT path, matching
|
|
@@ -85,6 +89,63 @@ export function buildPublishHeaders(url, opts) {
|
|
|
85
89
|
headers['authorization'] = opts.authorization;
|
|
86
90
|
return headers;
|
|
87
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* This CLI's own `package.json#version`, memoised. Two layers, because
|
|
94
|
+
* `@gjsify/cli` is a DUAL-ENTRY package and neither layer covers both entries:
|
|
95
|
+
*
|
|
96
|
+
* 1. **`__PACKAGE_VERSION__`** — the compile-time define
|
|
97
|
+
* (`package.json#gjsify.defineFromPackageJson`), the mechanism
|
|
98
|
+
* `@gjsify/npm-registry` uses for the same stale-UA bug. Zero I/O, but the
|
|
99
|
+
* bundler only ever writes `dist/cli.gjs.mjs`: the npm-wired Node bin under
|
|
100
|
+
* `lib/` is emitted by plain `tsc`, which knows nothing about defines. A
|
|
101
|
+
* define alone would therefore have left the Node entry — the one `npm
|
|
102
|
+
* install @gjsify/cli` actually runs — on a permanent placeholder.
|
|
103
|
+
*
|
|
104
|
+
* 2. **an upward manifest walk** for the nearest `package.json` naming
|
|
105
|
+
* `@gjsify/cli`, covering `lib/`. Deliberately NOT a fixed
|
|
106
|
+
* `new URL('../../package.json', import.meta.url)` read: that resolves
|
|
107
|
+
* correctly from `lib/utils/` but points one directory too high from
|
|
108
|
+
* `dist/`, and the build-time `inlineStaticReads` pass that would have
|
|
109
|
+
* frozen it is scoped to `node_modules` paths, so first-party source never
|
|
110
|
+
* gets it. The walk is depth-independent and so works from `src/`, `lib/`,
|
|
111
|
+
* `dist/`, and from an installed `node_modules/@gjsify/cli/**` alike.
|
|
112
|
+
*
|
|
113
|
+
* `GJSIFY_CLI_PACKAGE_JSON` overrides both, matching `self-update.ts`.
|
|
114
|
+
*/
|
|
115
|
+
let cachedCliVersion;
|
|
116
|
+
function cliVersion() {
|
|
117
|
+
if (cachedCliVersion !== undefined)
|
|
118
|
+
return cachedCliVersion;
|
|
119
|
+
cachedCliVersion =
|
|
120
|
+
(typeof __PACKAGE_VERSION__ === 'string' ? __PACKAGE_VERSION__ : '') || walkForCliVersion() || 'unknown';
|
|
121
|
+
return cachedCliVersion;
|
|
122
|
+
}
|
|
123
|
+
/** Nearest ancestor `package.json` whose `name` is `@gjsify/cli`; '' when none. */
|
|
124
|
+
function walkForCliVersion() {
|
|
125
|
+
const override = globalThis.process?.env?.['GJSIFY_CLI_PACKAGE_JSON'];
|
|
126
|
+
let dir;
|
|
127
|
+
try {
|
|
128
|
+
dir = override ? dirname(override) : dirname(fileURLToPath(import.meta.url));
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return '';
|
|
132
|
+
}
|
|
133
|
+
for (let i = 0; i < 12; i++) {
|
|
134
|
+
try {
|
|
135
|
+
const pkg = JSON.parse(readFileSync(`${dir}/package.json`, 'utf8'));
|
|
136
|
+
if (pkg.name === '@gjsify/cli' && typeof pkg.version === 'string')
|
|
137
|
+
return pkg.version;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
/* no manifest here (or unreadable) — keep climbing */
|
|
141
|
+
}
|
|
142
|
+
const parent = dirname(dir);
|
|
143
|
+
if (parent === dir)
|
|
144
|
+
break;
|
|
145
|
+
dir = parent;
|
|
146
|
+
}
|
|
147
|
+
return '';
|
|
148
|
+
}
|
|
88
149
|
/**
|
|
89
150
|
* npm-CLI-shaped user-agent. Format mirrors npm-registry-fetch's
|
|
90
151
|
* `default-opts.js` (`npm@<v>/node@<v>+<arch> (<platform>)`); we substitute the
|
|
@@ -95,5 +156,5 @@ function defaultUserAgent() {
|
|
|
95
156
|
const proc = globalThis.process;
|
|
96
157
|
const arch = proc?.arch ?? 'x64';
|
|
97
158
|
const platform = proc?.platform ?? 'linux';
|
|
98
|
-
return `gjsify-publish
|
|
159
|
+
return `gjsify-publish/${cliVersion()} (${platform} ${arch})`;
|
|
99
160
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Workspace } from '@gjsify/workspace';
|
|
2
|
+
/**
|
|
3
|
+
* Package-group directories holding publishable packages that are NOT root
|
|
4
|
+
* workspaces. Add one here the moment a new non-workspace publishable group
|
|
5
|
+
* appears — this list is the only thing standing between such a package and a
|
|
6
|
+
* silently broken first release.
|
|
7
|
+
*/
|
|
8
|
+
export declare const NON_WORKSPACE_PUBLISHABLE_DIRS: readonly [readonly ["packages", "node-gi"], readonly ["packages", "napi"]];
|
|
9
|
+
/**
|
|
10
|
+
* Publishable packages in {@link NON_WORKSPACE_PUBLISHABLE_DIRS}, shaped as
|
|
11
|
+
* `Workspace` entries so callers can treat them like any other package.
|
|
12
|
+
*
|
|
13
|
+
* Best-effort by design: a missing directory or an unreadable/malformed
|
|
14
|
+
* manifest is skipped rather than thrown, because this runs inside sweeps whose
|
|
15
|
+
* value is covering everything they can reach.
|
|
16
|
+
*/
|
|
17
|
+
export declare function discoverNonWorkspacePublishables(cwd: string): Workspace[];
|
|
18
|
+
/**
|
|
19
|
+
* Merge workspace-discovered packages with the non-workspace publishables,
|
|
20
|
+
* deduped by name. A real workspace always wins over a scanned directory.
|
|
21
|
+
*/
|
|
22
|
+
export declare function mergePublishables(workspaces: Workspace[], cwd: string): Workspace[];
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Publishable-package discovery shared by `gjsify trust` and `gjsify onboard`.
|
|
2
|
+
//
|
|
3
|
+
// `discoverWorkspaces` only sees ROOT WORKSPACES. Some publishable packages
|
|
4
|
+
// deliberately are not: each is an independent native engine with its own CI
|
|
5
|
+
// workflow, kept out of the workspace graph so a full `gjsify foreach` never
|
|
6
|
+
// tries to build it. They still publish through `release.yml`, so every sweep
|
|
7
|
+
// that reasons about "what do we publish" has to include them — otherwise the
|
|
8
|
+
// sweep silently under-reports and the package's first release fails the OIDC
|
|
9
|
+
// exchange with `404 — package not found`.
|
|
10
|
+
//
|
|
11
|
+
// That is not hypothetical: `gjsify onboard` reported 127 packages "already
|
|
12
|
+
// done" on 2026-07-26 while `@gjsify/napi` was neither in the list nor
|
|
13
|
+
// published at all, because only `packages/node-gi/*` was carved out.
|
|
14
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
15
|
+
import { dirname, join } from 'node:path';
|
|
16
|
+
/**
|
|
17
|
+
* Package-group directories holding publishable packages that are NOT root
|
|
18
|
+
* workspaces. Add one here the moment a new non-workspace publishable group
|
|
19
|
+
* appears — this list is the only thing standing between such a package and a
|
|
20
|
+
* silently broken first release.
|
|
21
|
+
*/
|
|
22
|
+
export const NON_WORKSPACE_PUBLISHABLE_DIRS = [
|
|
23
|
+
['packages', 'node-gi'], // @gjsify/node-gi + the @gjsify/gtk-runtime-* bundles
|
|
24
|
+
['packages', 'napi'], // @gjsify/napi (the N-API host in GJS, own napi.yml)
|
|
25
|
+
];
|
|
26
|
+
/**
|
|
27
|
+
* Publishable packages in {@link NON_WORKSPACE_PUBLISHABLE_DIRS}, shaped as
|
|
28
|
+
* `Workspace` entries so callers can treat them like any other package.
|
|
29
|
+
*
|
|
30
|
+
* Best-effort by design: a missing directory or an unreadable/malformed
|
|
31
|
+
* manifest is skipped rather than thrown, because this runs inside sweeps whose
|
|
32
|
+
* value is covering everything they can reach.
|
|
33
|
+
*/
|
|
34
|
+
export function discoverNonWorkspacePublishables(cwd) {
|
|
35
|
+
// Walk up to the repo root. Anchored on the FIRST configured group rather
|
|
36
|
+
// than on `packages/` itself, which also exists inside individual packages.
|
|
37
|
+
let root = cwd;
|
|
38
|
+
const anchor = NON_WORKSPACE_PUBLISHABLE_DIRS[0];
|
|
39
|
+
for (let i = 0; i < 8; i++) {
|
|
40
|
+
if (existsSync(join(root, ...anchor)))
|
|
41
|
+
break;
|
|
42
|
+
const parent = dirname(root);
|
|
43
|
+
if (parent === root)
|
|
44
|
+
return [];
|
|
45
|
+
root = parent;
|
|
46
|
+
}
|
|
47
|
+
const out = [];
|
|
48
|
+
for (const group of NON_WORKSPACE_PUBLISHABLE_DIRS) {
|
|
49
|
+
const base = join(root, ...group);
|
|
50
|
+
if (!existsSync(base))
|
|
51
|
+
continue;
|
|
52
|
+
let entries;
|
|
53
|
+
try {
|
|
54
|
+
entries = readdirSync(base, { withFileTypes: true });
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
if (!entry.isDirectory())
|
|
61
|
+
continue;
|
|
62
|
+
const location = join(base, entry.name);
|
|
63
|
+
const pkgPath = join(location, 'package.json');
|
|
64
|
+
if (!existsSync(pkgPath))
|
|
65
|
+
continue;
|
|
66
|
+
try {
|
|
67
|
+
const manifest = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
68
|
+
if (!manifest || typeof manifest.name !== 'string' || manifest.name.length === 0)
|
|
69
|
+
continue;
|
|
70
|
+
out.push({
|
|
71
|
+
location,
|
|
72
|
+
relativeLocation: join(...group, entry.name),
|
|
73
|
+
name: manifest.name,
|
|
74
|
+
version: typeof manifest.version === 'string' ? manifest.version : '0.0.0',
|
|
75
|
+
manifest,
|
|
76
|
+
private: manifest.private === true,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
/* unreadable / malformed — skip */
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Merge workspace-discovered packages with the non-workspace publishables,
|
|
88
|
+
* deduped by name. A real workspace always wins over a scanned directory.
|
|
89
|
+
*/
|
|
90
|
+
export function mergePublishables(workspaces, cwd) {
|
|
91
|
+
const seen = new Set(workspaces.map((ws) => ws.name));
|
|
92
|
+
return [...workspaces, ...discoverNonWorkspacePublishables(cwd).filter((ws) => !seen.has(ws.name))];
|
|
93
|
+
}
|
package/lib/utils/run-gjs.d.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
+
import { type NativeEnv } from './detect-native-packages.js';
|
|
1
2
|
/**
|
|
2
|
-
* Pure env computation for a given bundle. Returns the
|
|
3
|
-
*
|
|
3
|
+
* Pure env computation for a given bundle. Returns the typelib +
|
|
4
|
+
* shared-library search paths that {@link runGjsBundle} would inject into the
|
|
4
5
|
* spawned `gjs` process, plus the formatted env-prefix string used for the
|
|
5
|
-
* `$ …` echo.
|
|
6
|
+
* `$ …` echo. Which library variable is set is host-dependent
|
|
7
|
+
* (`LD_LIBRARY_PATH` / `DYLD_LIBRARY_PATH` / `PATH`) — see `buildNativeEnv`.
|
|
6
8
|
*/
|
|
7
9
|
export declare function computeNativeEnvForBundle(bundlePath: string, cwd?: string): {
|
|
8
|
-
env:
|
|
9
|
-
LD_LIBRARY_PATH: string;
|
|
10
|
-
GI_TYPELIB_PATH: string;
|
|
11
|
-
};
|
|
10
|
+
env: NativeEnv;
|
|
12
11
|
envPrefix: string;
|
|
13
12
|
};
|
|
14
13
|
/**
|
package/lib/utils/run-gjs.js
CHANGED
|
@@ -20,10 +20,11 @@ import { spawn } from 'node:child_process';
|
|
|
20
20
|
import { dirname, resolve } from 'node:path';
|
|
21
21
|
import { detectNativePackages, buildNativeEnv } from './detect-native-packages.js';
|
|
22
22
|
/**
|
|
23
|
-
* Pure env computation for a given bundle. Returns the
|
|
24
|
-
*
|
|
23
|
+
* Pure env computation for a given bundle. Returns the typelib +
|
|
24
|
+
* shared-library search paths that {@link runGjsBundle} would inject into the
|
|
25
25
|
* spawned `gjs` process, plus the formatted env-prefix string used for the
|
|
26
|
-
* `$ …` echo.
|
|
26
|
+
* `$ …` echo. Which library variable is set is host-dependent
|
|
27
|
+
* (`LD_LIBRARY_PATH` / `DYLD_LIBRARY_PATH` / `PATH`) — see `buildNativeEnv`.
|
|
27
28
|
*/
|
|
28
29
|
export function computeNativeEnvForBundle(bundlePath, cwd = process.cwd()) {
|
|
29
30
|
const resolvedBundle = resolve(bundlePath);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { ChildProcess } from 'node:child_process';
|
|
2
|
+
/**
|
|
3
|
+
* What the calling command does once this spawn settles.
|
|
4
|
+
*
|
|
5
|
+
* - `'exit'` — the command terminates the process itself (`process.exit(…)`,
|
|
6
|
+
* `forceExit(…)`, …). The armed GJS main loop is quit by that exit, so the
|
|
7
|
+
* streaming async `spawn` is safe on both hosts.
|
|
8
|
+
* - `'return'` — the command returns normally and lets the CLI unwind. Under
|
|
9
|
+
* GJS that MUST NOT leave a main loop armed, so the child is run on the
|
|
10
|
+
* blocking `spawnSync` path instead.
|
|
11
|
+
*
|
|
12
|
+
* See the module header for the full rationale.
|
|
13
|
+
*/
|
|
14
|
+
export type SpawnCompletionContract = 'exit' | 'return';
|
|
15
|
+
export interface SpawnToCompletionOptions {
|
|
16
|
+
/** The caller's teardown contract. Required — see {@link SpawnCompletionContract}. */
|
|
17
|
+
completion: SpawnCompletionContract;
|
|
18
|
+
/** Working directory for the child. Defaults to the parent's. */
|
|
19
|
+
cwd?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Environment for the child. Omit to inherit the parent's environment
|
|
22
|
+
* verbatim (what `spawn` does with no `env`).
|
|
23
|
+
*/
|
|
24
|
+
env?: NodeJS.ProcessEnv;
|
|
25
|
+
/**
|
|
26
|
+
* Seed `FORCE_COLOR=1` (on top of `env` ?? `process.env`) unless the user
|
|
27
|
+
* explicitly set `FORCE_COLOR` or `NO_COLOR`, so tools that key on
|
|
28
|
+
* `process.stdout.isTTY` (chalk, picocolors, …) still emit ANSI when their
|
|
29
|
+
* stdout is a pipe. Mirrors yarn / npm. Default `false`.
|
|
30
|
+
*/
|
|
31
|
+
color?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* `'inherit'` (default) forwards the parent's stdio. `'pipe'` gives the
|
|
34
|
+
* caller `child.stdout` / `child.stderr` via {@link onSpawn} — only
|
|
35
|
+
* meaningful together with `completion: 'exit'` (see {@link onSpawn}).
|
|
36
|
+
*/
|
|
37
|
+
stdio?: 'inherit' | 'pipe';
|
|
38
|
+
/**
|
|
39
|
+
* Invoked synchronously with the live `ChildProcess` right after a
|
|
40
|
+
* successful async spawn — the seam for kill-tracking registries and for
|
|
41
|
+
* wiring piped output forwarders.
|
|
42
|
+
*
|
|
43
|
+
* ONLY fires on the streaming path: the blocking `spawnSync` path has no
|
|
44
|
+
* `ChildProcess` handle at all. A site that needs the handle must
|
|
45
|
+
* therefore declare `completion: 'exit'`.
|
|
46
|
+
*/
|
|
47
|
+
onSpawn?: (child: ChildProcess) => void;
|
|
48
|
+
/**
|
|
49
|
+
* Map an `ENOENT` spawn failure (command not on PATH) to a domain error
|
|
50
|
+
* carrying an install hint. Other spawn errors are rejected as-is.
|
|
51
|
+
*/
|
|
52
|
+
notFound?: (err: NodeJS.ErrnoException) => Error;
|
|
53
|
+
/**
|
|
54
|
+
* Capture ceiling for the blocking path. Defaults to 64 MiB — a
|
|
55
|
+
* full-workspace lint with many findings runs well past Node's 1 MB
|
|
56
|
+
* default, and an overflow would silently truncate diagnostics.
|
|
57
|
+
*/
|
|
58
|
+
maxBuffer?: number;
|
|
59
|
+
}
|
|
60
|
+
export interface SpawnCompletion {
|
|
61
|
+
/** The child's exit code, or `null` when it was terminated by a signal. */
|
|
62
|
+
code: number | null;
|
|
63
|
+
/** The terminating signal, or `null` on a normal exit. */
|
|
64
|
+
signal: NodeJS.Signals | null;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Run `cmd args…` to completion and resolve with its exit code and signal.
|
|
68
|
+
*
|
|
69
|
+
* Resolves for ANY child exit — a non-zero code is data, not an error, so each
|
|
70
|
+
* call site keeps its own failure message. Rejects only when the child could
|
|
71
|
+
* not be started at all (mapped through {@link SpawnToCompletionOptions.notFound}
|
|
72
|
+
* for ENOENT).
|
|
73
|
+
*
|
|
74
|
+
* Picks the streaming async path or the blocking captured path per the GJS
|
|
75
|
+
* teardown contract documented at the top of this file.
|
|
76
|
+
*/
|
|
77
|
+
export declare function spawnToCompletion(cmd: string, args: readonly string[], opts: SpawnToCompletionOptions): Promise<SpawnCompletion>;
|
|
78
|
+
/**
|
|
79
|
+
* Format the "child failed" suffix the way the call sites report it: an exit
|
|
80
|
+
* code when the child exited, the signal name when it was killed.
|
|
81
|
+
*/
|
|
82
|
+
export declare function describeExit(result: SpawnCompletion): string;
|