@gjsify/cli 0.4.44 → 0.5.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 +214 -215
- package/lib/actions/build.js +9 -1
- package/lib/bundler-pick.d.ts +56 -0
- package/lib/bundler-pick.js +153 -16
- package/lib/cli-app.d.ts +10 -0
- package/lib/cli-app.js +112 -0
- package/lib/commands/affected.js +4 -0
- package/lib/commands/build.js +5 -1
- package/lib/commands/check.js +21 -13
- package/lib/commands/dlx.js +3 -1
- package/lib/commands/fix.js +3 -3
- package/lib/commands/foreach.js +269 -31
- package/lib/commands/format.js +3 -3
- package/lib/commands/install.js +68 -16
- package/lib/commands/lint.js +3 -3
- package/lib/commands/publish.js +18 -2
- package/lib/commands/run.js +132 -2
- package/lib/commands/test.js +5 -0
- package/lib/commands/tsc.js +78 -24
- package/lib/index.js +7 -96
- package/lib/utils/npm-oidc.d.ts +23 -1
- package/lib/utils/npm-oidc.js +44 -6
- package/lib/utils/oxc-resolve.d.ts +36 -5
- package/lib/utils/oxc-resolve.js +132 -6
- package/lib/utils/prefixed-output.d.ts +7 -0
- package/lib/utils/prefixed-output.js +52 -0
- package/lib/utils/run-gjs.d.ts +15 -1
- package/lib/utils/run-gjs.js +8 -7
- package/package.json +22 -18
package/lib/utils/oxc-resolve.js
CHANGED
|
@@ -28,10 +28,31 @@
|
|
|
28
28
|
// We still reuse Biome's platform/arch/musl detection — it's used to name the
|
|
29
29
|
// expected NAPI binding package in the not-found install hint, so the user
|
|
30
30
|
// knows exactly which optionalDependency npm failed to place.
|
|
31
|
+
//
|
|
32
|
+
// NATIVE ENGINE (GJS) — `@gjsify/oxfmt-native`
|
|
33
|
+
// ────────────────────────────────────────────
|
|
34
|
+
// Under GJS the Node launcher path is a dead end: there is no Node host to
|
|
35
|
+
// spawn. For oxfmt the dual-engine pick mirrors `bundler-pick.ts`'s
|
|
36
|
+
// native/npm pattern exactly: `shouldUseNativeOxfmt()` prefers the
|
|
37
|
+
// `@gjsify/oxfmt-native` GI bridge (Vala/Rust prebuild wrapping the full
|
|
38
|
+
// pure-Rust oxfmt CLI — config resolution, ignore handling, file walking,
|
|
39
|
+
// --write/--check/--list-different) when running under GJS and the prebuild
|
|
40
|
+
// is loadable, and falls back to spawning the Node launcher otherwise.
|
|
41
|
+
// `GJSIFY_OXFMT=native|npm` is the explicit override (native throws when the
|
|
42
|
+
// prebuild isn't loadable instead of silently switching engines).
|
|
43
|
+
//
|
|
44
|
+
// oxlint has NO native path yet: its JS-plugin host (the internal
|
|
45
|
+
// `gjsify/register-class-order` rule wired via `.oxlintrc.json` jsPlugins)
|
|
46
|
+
// lives in the Node launcher, so a native oxlint bridge could only run the
|
|
47
|
+
// Rust rule subset. Lint stays Node-spawned until that trade-off is decided
|
|
48
|
+
// (tracked in STATUS.md Open TODOs).
|
|
31
49
|
import { existsSync, readFileSync } from 'node:fs';
|
|
32
50
|
import { join, resolve } from 'node:path';
|
|
33
51
|
import { spawn } from 'node:child_process';
|
|
52
|
+
import { createRequire } from 'node:module';
|
|
53
|
+
import { pathToFileURL } from 'node:url';
|
|
34
54
|
import { findWorkspaceRoot } from './workspace-root.js';
|
|
55
|
+
import { resolveNpmPackage } from './resolve-npm-package.js';
|
|
35
56
|
/**
|
|
36
57
|
* Map Node.js `process.platform` + `arch` (+ musl on Linux) to the
|
|
37
58
|
* napi-rs binding-package suffix oxc uses, e.g. `linux-x64-gnu`,
|
|
@@ -178,17 +199,99 @@ function findConfigFile(cwd, names) {
|
|
|
178
199
|
}
|
|
179
200
|
return null;
|
|
180
201
|
}
|
|
202
|
+
let _nativeOxfmtProbe = null;
|
|
203
|
+
/**
|
|
204
|
+
* Try to load `@gjsify/oxfmt-native` (GJS only). Same multi-anchor
|
|
205
|
+
* resolution as `bundler-pick.ts`'s `tryLoadNative()` — GJS's native ESM
|
|
206
|
+
* loader has no node_modules walker, so bare specifiers must be resolved
|
|
207
|
+
* to a `file://` URL via createRequire/resolveNpmPackage first.
|
|
208
|
+
*/
|
|
209
|
+
async function tryLoadNativeOxfmt() {
|
|
210
|
+
if (_nativeOxfmtProbe)
|
|
211
|
+
return _nativeOxfmtProbe;
|
|
212
|
+
_nativeOxfmtProbe = (async () => {
|
|
213
|
+
const isGjs = typeof globalThis.imports?.gi !== 'undefined';
|
|
214
|
+
if (!isGjs)
|
|
215
|
+
return null;
|
|
216
|
+
try {
|
|
217
|
+
const specifier = '@gjsify/oxfmt-native';
|
|
218
|
+
const resolved = resolveNpmPackage(specifier, { bundleUrl: import.meta.url }) ??
|
|
219
|
+
createRequire(import.meta.url).resolve(specifier);
|
|
220
|
+
const target = pathToFileURL(resolved).href;
|
|
221
|
+
const mod = (await import(/* @vite-ignore */ target));
|
|
222
|
+
if (!mod.hasNativeOxfmt())
|
|
223
|
+
return null;
|
|
224
|
+
return mod;
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
})();
|
|
230
|
+
return _nativeOxfmtProbe;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Engine pick for oxfmt — mirrors `bundler-pick.ts`'s `shouldUseNative()`:
|
|
234
|
+
* - `GJSIFY_OXFMT=npm` forces the Node launcher.
|
|
235
|
+
* - `GJSIFY_OXFMT=native` forces the GI bridge and throws when it is not
|
|
236
|
+
* loadable (instead of silently switching engines).
|
|
237
|
+
* - Default: native under GJS when loadable (the Node launcher cannot run
|
|
238
|
+
* without a Node host), npm launcher on Node.
|
|
239
|
+
*/
|
|
240
|
+
export async function shouldUseNativeOxfmt() {
|
|
241
|
+
const env = globalThis.process?.env ?? {};
|
|
242
|
+
const choice = env.GJSIFY_OXFMT;
|
|
243
|
+
if (choice === 'npm')
|
|
244
|
+
return false;
|
|
245
|
+
if (choice === 'native') {
|
|
246
|
+
const native = await tryLoadNativeOxfmt();
|
|
247
|
+
if (!native) {
|
|
248
|
+
throw new Error('GJSIFY_OXFMT=native but @gjsify/oxfmt-native is not loadable (no prebuild for this architecture, or not running under GJS).');
|
|
249
|
+
}
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
const isGjs = typeof globalThis.imports?.gi !== 'undefined';
|
|
253
|
+
if (!isGjs)
|
|
254
|
+
return false;
|
|
255
|
+
return (await tryLoadNativeOxfmt()) !== null;
|
|
256
|
+
}
|
|
181
257
|
/**
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
258
|
+
* Run an oxc tool (oxlint / oxfmt).
|
|
259
|
+
*
|
|
260
|
+
* oxfmt: dual-engine — under GJS the `@gjsify/oxfmt-native` GI bridge runs
|
|
261
|
+
* the full oxfmt CLI in-process (Node-free); otherwise the npm Node ESM
|
|
262
|
+
* launcher is spawned. `GJSIFY_OXFMT=native|npm` overrides the pick.
|
|
263
|
+
*
|
|
264
|
+
* oxlint: always spawned via its Node ESM launcher with the current Node
|
|
265
|
+
* executable (`process.execPath`) so the JS plugin host is available —
|
|
266
|
+
* required for the `jsPlugins`-wired `gjsify/register-class-order` rule.
|
|
185
267
|
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
268
|
+
* Inherits stdio so the tool's own output (diagnostics, reformatted files,
|
|
269
|
+
* summary lines) reaches the user.
|
|
188
270
|
*
|
|
189
271
|
* Returns the exit code; never throws on non-zero exit (callers check it).
|
|
190
272
|
*/
|
|
191
|
-
export function runOxc(tool, args, opts = {}) {
|
|
273
|
+
export async function runOxc(tool, args, opts = {}) {
|
|
274
|
+
if (tool === 'oxfmt' && (await shouldUseNativeOxfmt())) {
|
|
275
|
+
const native = await tryLoadNativeOxfmt();
|
|
276
|
+
if (!native) {
|
|
277
|
+
// Unreachable: shouldUseNativeOxfmt() returned true above.
|
|
278
|
+
throw new Error('@gjsify/oxfmt-native not loadable');
|
|
279
|
+
}
|
|
280
|
+
if (opts.verbose) {
|
|
281
|
+
console.log(`[gjsify oxc] @gjsify/oxfmt-native (in-process) ${args.join(' ')}`);
|
|
282
|
+
}
|
|
283
|
+
// The native runner walks from the process working directory; the
|
|
284
|
+
// command handlers always pass cwd = process.cwd(), so no chdir is
|
|
285
|
+
// needed here.
|
|
286
|
+
return native.runOxfmt(args);
|
|
287
|
+
}
|
|
288
|
+
return spawnOxcLauncher(tool, args, opts);
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Spawn an oxc tool via its Node ESM launcher (`node_modules/<tool>/bin/…`)
|
|
292
|
+
* with the current Node executable.
|
|
293
|
+
*/
|
|
294
|
+
function spawnOxcLauncher(tool, args, opts = {}) {
|
|
192
295
|
const cwd = opts.cwd ?? process.cwd();
|
|
193
296
|
const launcher = findOxcLauncher(tool, cwd);
|
|
194
297
|
const node = process.execPath || 'node';
|
|
@@ -242,6 +345,29 @@ export function loadOxfmtTemplate() {
|
|
|
242
345
|
export function printOxcNotFound(err) {
|
|
243
346
|
console.error(err.message);
|
|
244
347
|
}
|
|
348
|
+
/**
|
|
349
|
+
* Propagate an oxc tool's exit code from a command handler.
|
|
350
|
+
*
|
|
351
|
+
* On Node, setting `process.exitCode` is the gentle idiom (streams flush,
|
|
352
|
+
* the process ends naturally). Under GJS there is no atexit hook, so
|
|
353
|
+
* `process.exitCode` alone is NOT honored at natural shutdown — the gjs
|
|
354
|
+
* process would exit 0 after a failed `--check`. Call `imports.system.exit`
|
|
355
|
+
* directly for non-zero codes there: SpiderMonkey raises its uncatchable
|
|
356
|
+
* exit exception from any context — including the yargs `parseAsync`
|
|
357
|
+
* microtask continuation this runs in — and stdout writes are synchronous
|
|
358
|
+
* under GJS, so the tool's report is already flushed. (`process.exit`'s
|
|
359
|
+
* idle-scheduled path is NOT used here: the scheduled idle never fires from
|
|
360
|
+
* this continuation — the module's top-level await is still pending and gjs
|
|
361
|
+
* exits 0 before the hook-registered loop runs.)
|
|
362
|
+
*/
|
|
363
|
+
export function setOxcExitCode(code) {
|
|
364
|
+
process.exitCode = code;
|
|
365
|
+
if (code === 0)
|
|
366
|
+
return;
|
|
367
|
+
const gjs = globalThis.imports;
|
|
368
|
+
if (gjs?.system?.exit)
|
|
369
|
+
gjs.system.exit(code);
|
|
370
|
+
}
|
|
245
371
|
/**
|
|
246
372
|
* Has the given oxc tool's npm package (or its companion) been declared in the
|
|
247
373
|
* project's dependencies? Useful as a cheap pre-flight check — `gjsify flatpak
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prefix every line of `src` with `prefix` and route it to `sink`. When
|
|
3
|
+
* `buffered` is false (tty), lines are written live. When true (non-tty /
|
|
4
|
+
* CI pipe), they are accumulated and emitted only by the returned `flush()`
|
|
5
|
+
* — call it once the child has closed.
|
|
6
|
+
*/
|
|
7
|
+
export declare function prefixLines(src: NodeJS.ReadableStream, sink: NodeJS.WritableStream, prefix: string, buffered: boolean): () => void;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Shared line-prefixed child-output forwarding for the parallel runners
|
|
2
|
+
// (`gjsify foreach`, `gjsify check`).
|
|
3
|
+
//
|
|
4
|
+
// Under GJS, `process.stdout.write` is a BLOCKING `Gio.write_all`. Writing
|
|
5
|
+
// each prefixed line LIVE during a PARALLEL run into a backpressuring pipe
|
|
6
|
+
// (a CI log collector that drains slowly) stalls the single GLib main loop
|
|
7
|
+
// on a full pipe → every parallel child's pipe backs up → their reads stall
|
|
8
|
+
// → the whole run HANGS. (A tty or file sink never backpressures, which is
|
|
9
|
+
// why this only ever bites in CI.) On a NON-tty sink we therefore BUFFER
|
|
10
|
+
// each child's prefixed output and flush it as ONE write when that child
|
|
11
|
+
// closes: the child is already done by then, so its own read can't stall,
|
|
12
|
+
// and concurrent flushes serialize into brief loop stalls instead of a
|
|
13
|
+
// deadlock. On a tty (interactive) lines are written live for responsive
|
|
14
|
+
// output.
|
|
15
|
+
//
|
|
16
|
+
// This helper is the single home for that logic — `foreach` shipped the fix
|
|
17
|
+
// first; `check` had an unbuffered copy that kept the hang class alive.
|
|
18
|
+
/**
|
|
19
|
+
* Prefix every line of `src` with `prefix` and route it to `sink`. When
|
|
20
|
+
* `buffered` is false (tty), lines are written live. When true (non-tty /
|
|
21
|
+
* CI pipe), they are accumulated and emitted only by the returned `flush()`
|
|
22
|
+
* — call it once the child has closed.
|
|
23
|
+
*/
|
|
24
|
+
export function prefixLines(src, sink, prefix, buffered) {
|
|
25
|
+
let buf = '';
|
|
26
|
+
let acc = '';
|
|
27
|
+
const emit = (line) => {
|
|
28
|
+
if (buffered)
|
|
29
|
+
acc += line;
|
|
30
|
+
else
|
|
31
|
+
sink.write(line);
|
|
32
|
+
};
|
|
33
|
+
src.setEncoding('utf-8');
|
|
34
|
+
src.on('data', (chunk) => {
|
|
35
|
+
buf += chunk;
|
|
36
|
+
let idx;
|
|
37
|
+
while ((idx = buf.indexOf('\n')) !== -1) {
|
|
38
|
+
emit(prefix + buf.slice(0, idx + 1));
|
|
39
|
+
buf = buf.slice(idx + 1);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
src.on('end', () => {
|
|
43
|
+
if (buf.length > 0)
|
|
44
|
+
emit(prefix + buf + '\n');
|
|
45
|
+
});
|
|
46
|
+
return () => {
|
|
47
|
+
if (acc.length > 0) {
|
|
48
|
+
sink.write(acc);
|
|
49
|
+
acc = '';
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
package/lib/utils/run-gjs.d.ts
CHANGED
|
@@ -16,4 +16,18 @@ export declare function computeNativeEnvForBundle(bundlePath: string, cwd?: stri
|
|
|
16
16
|
* for any installed native gjsify packages discoverable from either the CWD
|
|
17
17
|
* or the bundle's own node_modules tree.
|
|
18
18
|
*/
|
|
19
|
-
export
|
|
19
|
+
export interface RunGjsBundleOptions {
|
|
20
|
+
/**
|
|
21
|
+
* Exit this process with code 0 once the child succeeds. Under GJS,
|
|
22
|
+
* `ensureMainLoop()` (armed by spawn) keeps the parent's GLib loop alive
|
|
23
|
+
* after the child exits — a TERMINAL caller (`gjsify run <file>`, `dlx`)
|
|
24
|
+
* must opt in here or it parks forever (this exact gap hung CI's
|
|
25
|
+
* "Test WebGL conformance" for 83 min: the suite finished in 1.35 s, the
|
|
26
|
+
* parent `gjsify run conformance.gjs.js` never exited). Callers that
|
|
27
|
+
* continue after the bundle (e.g. `gjsify test`'s multi-runtime
|
|
28
|
+
* aggregation loop) MUST leave this false — an unconditional exit here
|
|
29
|
+
* truncated `gjsify test` after the first gjs bundle.
|
|
30
|
+
*/
|
|
31
|
+
exitOnSuccess?: boolean;
|
|
32
|
+
}
|
|
33
|
+
export declare function runGjsBundle(bundlePath: string, extraArgs?: string[], options?: RunGjsBundleOptions): Promise<void>;
|
package/lib/utils/run-gjs.js
CHANGED
|
@@ -38,12 +38,7 @@ export function computeNativeEnvForBundle(bundlePath, cwd = process.cwd()) {
|
|
|
38
38
|
.join(' ');
|
|
39
39
|
return { env, envPrefix };
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
* Run a GJS bundle, automatically setting LD_LIBRARY_PATH and GI_TYPELIB_PATH
|
|
43
|
-
* for any installed native gjsify packages discoverable from either the CWD
|
|
44
|
-
* or the bundle's own node_modules tree.
|
|
45
|
-
*/
|
|
46
|
-
export async function runGjsBundle(bundlePath, extraArgs = []) {
|
|
41
|
+
export async function runGjsBundle(bundlePath, extraArgs = [], options = {}) {
|
|
47
42
|
const { env: nativeEnv, envPrefix } = computeNativeEnvForBundle(bundlePath);
|
|
48
43
|
const env = {
|
|
49
44
|
...process.env,
|
|
@@ -53,8 +48,10 @@ export async function runGjsBundle(bundlePath, extraArgs = []) {
|
|
|
53
48
|
// Print the exact command being executed so users can copy-paste it to
|
|
54
49
|
// run gjs directly without the wrapper. Env vars are only shown if we
|
|
55
50
|
// actually set any (i.e. native gjsify packages were detected).
|
|
51
|
+
// Use stderr so that children speaking a protocol on stdout (e.g. an
|
|
52
|
+
// MCP stdio server) receive an uncontaminated stdout stream.
|
|
56
53
|
const gjsCommand = ['gjs', ...gjsArgs.map((a) => (a.includes(' ') ? `"${a}"` : a))].join(' ');
|
|
57
|
-
console.
|
|
54
|
+
console.error(`$ ${envPrefix ? `${envPrefix} ` : ''}${gjsCommand}`);
|
|
58
55
|
const child = spawn('gjs', gjsArgs, { env, stdio: 'inherit' });
|
|
59
56
|
await new Promise((resolvePromise, reject) => {
|
|
60
57
|
child.on('close', (code) => {
|
|
@@ -70,4 +67,8 @@ export async function runGjsBundle(bundlePath, extraArgs = []) {
|
|
|
70
67
|
console.error(err.message);
|
|
71
68
|
process.exit(1);
|
|
72
69
|
});
|
|
70
|
+
// See RunGjsBundleOptions.exitOnSuccess — terminal callers opt in,
|
|
71
|
+
// mid-flow callers (test.ts's runtime loop) keep the process alive.
|
|
72
|
+
if (options.exitOnSuccess)
|
|
73
|
+
process.exit(0);
|
|
73
74
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "CLI for Gjsify",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -120,34 +120,38 @@
|
|
|
120
120
|
"cli"
|
|
121
121
|
],
|
|
122
122
|
"dependencies": {
|
|
123
|
-
"@gjsify/buffer": "^0.
|
|
124
|
-
"@gjsify/create-app": "^0.
|
|
125
|
-
"@gjsify/node-globals": "^0.
|
|
126
|
-
"@gjsify/node-polyfills": "^0.
|
|
127
|
-
"@gjsify/npm-registry": "^0.
|
|
128
|
-
"@gjsify/resolve-npm": "^0.
|
|
129
|
-
"@gjsify/rolldown-plugin-gjsify": "^0.
|
|
130
|
-
"@gjsify/rolldown-plugin-pnp": "^0.
|
|
131
|
-
"@gjsify/semver": "^0.
|
|
132
|
-
"@gjsify/tar": "^0.
|
|
133
|
-
"@gjsify/tsc": "^0.
|
|
134
|
-
"@gjsify/web-polyfills": "^0.
|
|
135
|
-
"@gjsify/workspace": "^0.
|
|
136
|
-
"cosmiconfig": "^9.0.
|
|
123
|
+
"@gjsify/buffer": "^0.5.0",
|
|
124
|
+
"@gjsify/create-app": "^0.5.0",
|
|
125
|
+
"@gjsify/node-globals": "^0.5.0",
|
|
126
|
+
"@gjsify/node-polyfills": "^0.5.0",
|
|
127
|
+
"@gjsify/npm-registry": "^0.5.0",
|
|
128
|
+
"@gjsify/resolve-npm": "^0.5.0",
|
|
129
|
+
"@gjsify/rolldown-plugin-gjsify": "^0.5.0",
|
|
130
|
+
"@gjsify/rolldown-plugin-pnp": "^0.5.0",
|
|
131
|
+
"@gjsify/semver": "^0.5.0",
|
|
132
|
+
"@gjsify/tar": "^0.5.0",
|
|
133
|
+
"@gjsify/tsc": "^0.5.0",
|
|
134
|
+
"@gjsify/web-polyfills": "^0.5.0",
|
|
135
|
+
"@gjsify/workspace": "^0.5.0",
|
|
136
|
+
"cosmiconfig": "^9.0.2",
|
|
137
137
|
"get-tsconfig": "^4.14.0",
|
|
138
138
|
"pkg-types": "^2.3.1",
|
|
139
|
-
"rolldown": "^1.0
|
|
139
|
+
"rolldown": "^1.1.0",
|
|
140
140
|
"yargs": "^18.0.0"
|
|
141
141
|
},
|
|
142
142
|
"devDependencies": {
|
|
143
|
-
"@gjsify/unit": "^0.
|
|
143
|
+
"@gjsify/unit": "^0.5.0",
|
|
144
144
|
"@types/yargs": "^17.0.35",
|
|
145
145
|
"typescript": "^6.0.3"
|
|
146
146
|
},
|
|
147
147
|
"peerDependencies": {
|
|
148
|
-
"@gjsify/
|
|
148
|
+
"@gjsify/oxfmt-native": "^0.5.0",
|
|
149
|
+
"@gjsify/rolldown-native": "^0.5.0"
|
|
149
150
|
},
|
|
150
151
|
"peerDependenciesMeta": {
|
|
152
|
+
"@gjsify/oxfmt-native": {
|
|
153
|
+
"optional": true
|
|
154
|
+
},
|
|
151
155
|
"@gjsify/rolldown-native": {
|
|
152
156
|
"optional": true
|
|
153
157
|
}
|