@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.
@@ -259,7 +259,15 @@ export class BuildAction {
259
259
  external: userBundler.external,
260
260
  transform: userBundler.transform,
261
261
  format,
262
- }, pluginOpts, gjsifyPluginFactory, verbose, { extraGlobalsList: extras, excludeGlobals }, bundleToChunks);
262
+ }, pluginOpts, gjsifyPluginFactory, verbose,
263
+ // Pass the project working dir so the resolvability gate in
264
+ // detectAutoGlobals can check whether each detected global's
265
+ // polyfill package is installed before writing a register
266
+ // import. Without `cwd` the gate is bypassed and an
267
+ // unresolvable import would hard-crash the analysis pass.
268
+ // Mirrors the explicit-globals path (resolveGlobalsInject)
269
+ // and resolveUserPlugins above, which already anchor on cwd.
270
+ { extraGlobalsList: extras, excludeGlobals, cwd: process.cwd() }, bundleToChunks);
263
271
  pluginOpts.autoGlobalsInject = injectPath;
264
272
  }
265
273
  else if (extras) {
@@ -78,6 +78,62 @@ export declare function runWatch(finalOpts: BundlerOptions): Promise<RolldownWat
78
78
  */
79
79
  export declare function runBundle(finalOpts: BundlerOptions): Promise<RolldownOutput>;
80
80
  export declare function shouldUseNative(): Promise<boolean>;
81
+ /**
82
+ * Sanitize fields the JSON encoder can't ship to the Rust deserializer.
83
+ *
84
+ * Functions vanish under `JSON.stringify` (the key disappears from the
85
+ * JSON), silently changing build behavior vs npm rolldown — so dropping
86
+ * one now WARNS (once per key). `external` gets a dedicated message: the
87
+ * orchestrators express externals as exact-name arrays + an
88
+ * `externalsPlugin` resolveId hook precisely because a function predicate
89
+ * cannot cross this boundary; a function here means a regression (or a
90
+ * user config) reintroduced the dropped-predicate class.
91
+ *
92
+ * `external` array elements must be plain strings — the Rust
93
+ * `deserialize_external` only accepts `Vec<String>` and a RegExp
94
+ * serializes to `{}`, producing an opaque serde column error. Reject
95
+ * RegExp/non-string elements with a NAMED error instead.
96
+ *
97
+ * `sourcemap` is TRANSLATED, not dropped: the JS API accepts
98
+ * `boolean | 'inline' | 'hidden'` while the Rust `SourceMapType` enum
99
+ * (plain serde derive, no rename_all) only accepts `'File' | 'Inline' |
100
+ * 'Hidden'`. `true` → `'File'`, `'inline'` → `'Inline'`, `'hidden'` →
101
+ * `'Hidden'`; `false` is omitted (same effect).
102
+ *
103
+ * Exported for the unit tests in `bundler-pick.spec.ts`.
104
+ */
105
+ export declare function stripUnserializable<T extends Record<string, unknown>>(opts: T): T;
106
+ /**
107
+ * Map the JS-API `sourcemap` spellings onto the Rust `SourceMapType` enum
108
+ * variants. Returns `undefined` for `false`/unknown shapes (omit the key).
109
+ * Exported for the unit tests in `bundler-pick.spec.ts`.
110
+ */
111
+ export declare function translateSourcemapOption(v: unknown): 'File' | 'Inline' | 'Hidden' | undefined;
112
+ /**
113
+ * Convert npm rolldown's `transform.inject` map shorthand into the array
114
+ * of descriptors the native Rust deserializer expects. The Rust
115
+ * `InjectImport` enum (`tag = "type"`, `rename_all = "camelCase"`) has
116
+ * exactly TWO variants:
117
+ *
118
+ * - `{ type: 'named', imported, alias?, from }`
119
+ * - `{ type: 'namespace', alias, from }`
120
+ *
121
+ * npm rolldown's map spellings translate as:
122
+ *
123
+ * - `{ alias: 'module' }` → default import →
124
+ * `{ type: 'named', imported: 'default', alias, from: 'module' }`
125
+ * (the Rust `InjectImport::default()` constructor encodes default
126
+ * imports as a named import of `default` — there is NO `'default'`
127
+ * variant; emitting `{ type: 'default' }` crashes the deserializer
128
+ * with `unknown variant \`default\``).
129
+ * - `{ alias: ['module', '*'] }` → namespace import →
130
+ * `{ type: 'namespace', alias, from: 'module' }`.
131
+ * - `{ alias: ['module', 'name'] }` → named import →
132
+ * `{ type: 'named', from: 'module', imported: 'name', alias }`.
133
+ *
134
+ * Exported for the unit tests in `bundler-pick.spec.ts`.
135
+ */
136
+ export declare function mapToInjectArray(map: Record<string, string | [string, string]>): Array<Record<string, string>>;
81
137
  export type PluginRecord = {
82
138
  name?: string;
83
139
  [k: string]: unknown;
@@ -39,6 +39,20 @@ import { resolveNpmPackage } from './utils/resolve-npm-package.js';
39
39
  // Dynamic import keeps it off the GJS code path entirely; the native
40
40
  // branch (`@gjsify/rolldown-native`) handles bundling there.
41
41
  async function loadNpmRolldown() {
42
+ // Under GJS the npm crate can NEVER run (its JS wrapper requires a
43
+ // napi binary). Reaching this loader under GJS means the native
44
+ // engine probe failed — historically that surfaced as an opaque
45
+ // `ImportError: Unsupported URI scheme for importing: node` from deep
46
+ // inside the crate's wrapper (e.g. on a truly-cold workspace where
47
+ // `@gjsify/rolldown-native`'s lib/ is not built yet, issue #497).
48
+ // Fail with the actionable diagnosis instead.
49
+ const isGjs = typeof globalThis.imports?.gi !== 'undefined';
50
+ if (isGjs) {
51
+ throw new Error('gjsify build: no usable bundler engine under GJS — `@gjsify/rolldown-native` is not loadable ' +
52
+ '(its JS facade lib/ is not built, or its prebuild typelib is missing from GI_TYPELIB_PATH), ' +
53
+ 'and the npm `rolldown` engine is a Rust napi crate that cannot run under GJS. ' +
54
+ 'Build the facade first (`gjsify workspace @gjsify/rolldown-native build` under Node) or run this build under Node.');
55
+ }
42
56
  // Indirect specifier so Rolldown's static-analysis doesn't try to
43
57
  // bundle the npm crate into a GJS target build.
44
58
  const specifier = 'rolldown';
@@ -102,6 +116,7 @@ export async function bundleToChunks(input) {
102
116
  }));
103
117
  delete opts.plugins;
104
118
  const result = await native.bundleWithPlugins(opts, nativePlugins);
119
+ reportNativeWarnings(result, input.rolldownInput);
105
120
  const codes = [];
106
121
  for (const item of result.output) {
107
122
  if (item.type === 'chunk')
@@ -261,6 +276,7 @@ async function runNativeBundle(finalOpts) {
261
276
  console.error('[gjsify-bundler-pick] native opts JSON:', JSON.stringify(bundlerOpts));
262
277
  }
263
278
  const result = await native.bundleWithPlugins(bundlerOpts, nativePlugins);
279
+ reportNativeWarnings(result, finalOpts);
264
280
  // The native facade returns the BundleOutput shape but doesn't
265
281
  // write files — replicate `.write()` here so callers see the same
266
282
  // on-disk layout whether they used npm rolldown or native.
@@ -282,6 +298,14 @@ async function runNativeBundle(finalOpts) {
282
298
  await fs.mkdir(path.dirname(target), { recursive: true });
283
299
  if (item.type === 'chunk') {
284
300
  await fs.writeFile(target, item.code, 'utf8');
301
+ // `.write()` parity: npm rolldown emits `<fileName>.map` next to
302
+ // each chunk when `sourcemap: 'File'|'Hidden'` is set. The native
303
+ // facade carries the map JSON on the chunk (`map` is undefined for
304
+ // inline/disabled sourcemaps) — write it or sourcemaps would be
305
+ // silently unavailable under the GJS-default engine.
306
+ if (typeof item.map === 'string') {
307
+ await fs.writeFile(`${target}.map`, item.map, 'utf8');
308
+ }
285
309
  }
286
310
  else if (item.sourceText !== undefined) {
287
311
  await fs.writeFile(target, item.sourceText, 'utf8');
@@ -291,28 +315,111 @@ async function runNativeBundle(finalOpts) {
291
315
  return synthRolldownOutput(result);
292
316
  }
293
317
  /**
294
- * Drop fields the JSON encoder can't ship to the Rust deserializer.
295
- * `external` may be a function predicate (npm rolldown supports this);
296
- * functions vanish under `JSON.stringify` and surface as parse errors on
297
- * the Rust side. Strip top-level function values so the option object
298
- * is JSON-clean. (The actual external behavior under native rolldown
299
- * comes from arrays/regex strings, set elsewhere by the orchestrator.)
318
+ * Surface native-engine bundle warnings through the same channel npm
319
+ * rolldown uses (its default `onLog` prints warnings at logLevel 'info').
320
+ * The Rust side collects rolldown warnings + plugin `this.warn()` calls
321
+ * into `BundleResult.warnings`; never reading them is what let the
322
+ * dropped-options hazard class ship unnoticed — UNRESOLVED_IMPORT (the
323
+ * only runtime signal that an import is riding the unresolved-fallback
324
+ * accident) and every plugin warning were invisible on the GJS-default
325
+ * engine. De-duplicated per call; respects `logLevel: 'silent'` (the
326
+ * `--globals auto` analysis passes set it, so analysis stays quiet).
327
+ */
328
+ function reportNativeWarnings(result, opts) {
329
+ if (opts['logLevel'] === 'silent')
330
+ return;
331
+ if (!Array.isArray(result.warnings) || result.warnings.length === 0)
332
+ return;
333
+ const seen = new Set();
334
+ for (const warning of result.warnings) {
335
+ if (seen.has(warning))
336
+ continue;
337
+ seen.add(warning);
338
+ console.warn(`[gjsify-bundler] ${warning}`);
339
+ }
340
+ }
341
+ // Warn-once registry for `stripUnserializable` — one warning per option
342
+ // key per process, so a 3-pass `--globals auto` build doesn't repeat the
343
+ // same message. The silent drop of function-valued options is the
344
+ // recurring footgun behind three shipped/near-shipped bugs (app/node.ts,
345
+ // library/lib.ts, app/gjs.ts external predicates) — never drop silently.
346
+ const warnedDroppedKeys = new Set();
347
+ /**
348
+ * Sanitize fields the JSON encoder can't ship to the Rust deserializer.
300
349
  *
301
- * Also drops `sourcemap: false` — the JS API accepts a boolean while
302
- * the Rust deserializer expects an enum (`'File' | 'Inline' | 'Hidden'`).
303
- * Omitting the field has the same effect as `false`.
350
+ * Functions vanish under `JSON.stringify` (the key disappears from the
351
+ * JSON), silently changing build behavior vs npm rolldown — so dropping
352
+ * one now WARNS (once per key). `external` gets a dedicated message: the
353
+ * orchestrators express externals as exact-name arrays + an
354
+ * `externalsPlugin` resolveId hook precisely because a function predicate
355
+ * cannot cross this boundary; a function here means a regression (or a
356
+ * user config) reintroduced the dropped-predicate class.
357
+ *
358
+ * `external` array elements must be plain strings — the Rust
359
+ * `deserialize_external` only accepts `Vec<String>` and a RegExp
360
+ * serializes to `{}`, producing an opaque serde column error. Reject
361
+ * RegExp/non-string elements with a NAMED error instead.
362
+ *
363
+ * `sourcemap` is TRANSLATED, not dropped: the JS API accepts
364
+ * `boolean | 'inline' | 'hidden'` while the Rust `SourceMapType` enum
365
+ * (plain serde derive, no rename_all) only accepts `'File' | 'Inline' |
366
+ * 'Hidden'`. `true` → `'File'`, `'inline'` → `'Inline'`, `'hidden'` →
367
+ * `'Hidden'`; `false` is omitted (same effect).
368
+ *
369
+ * Exported for the unit tests in `bundler-pick.spec.ts`.
304
370
  */
305
- function stripUnserializable(opts) {
371
+ export function stripUnserializable(opts) {
306
372
  const out = {};
307
373
  for (const [k, v] of Object.entries(opts)) {
308
- if (typeof v === 'function')
374
+ if (typeof v === 'function') {
375
+ if (!warnedDroppedKeys.has(k)) {
376
+ warnedDroppedKeys.add(k);
377
+ const hint = k === 'external'
378
+ ? ' Function `external` predicates are NOT supported by the native engine — ' +
379
+ 'pass exact names as a string array and/or enforce shape rules via a ' +
380
+ 'resolveId plugin returning `{ external: true }` (see externalsPlugin in ' +
381
+ '@gjsify/rolldown-plugin-gjsify).'
382
+ : ' Function-valued options do not survive the JSON boundary to the Rust core.';
383
+ console.warn(`[gjsify-bundler] dropping non-serializable option "${k}" for the native rolldown engine —` +
384
+ ` behavior may diverge from npm rolldown.${hint}`);
385
+ }
309
386
  continue;
310
- if (k === 'sourcemap' && typeof v === 'boolean')
387
+ }
388
+ if (k === 'external' && Array.isArray(v)) {
389
+ const bad = v.find((el) => typeof el !== 'string');
390
+ if (bad !== undefined) {
391
+ throw new Error('gjsify build: `external` entries must be exact string names under the native ' +
392
+ `rolldown engine (got ${bad instanceof RegExp ? bad.toString() : typeof bad}). ` +
393
+ 'RegExp/function externals are only supported by npm rolldown — use exact names ' +
394
+ 'or a resolveId plugin returning `{ external: true }`.');
395
+ }
396
+ out[k] = v;
311
397
  continue;
398
+ }
399
+ if (k === 'sourcemap') {
400
+ const mapped = translateSourcemapOption(v);
401
+ if (mapped !== undefined)
402
+ out[k] = mapped;
403
+ continue;
404
+ }
312
405
  out[k] = v;
313
406
  }
314
407
  return out;
315
408
  }
409
+ /**
410
+ * Map the JS-API `sourcemap` spellings onto the Rust `SourceMapType` enum
411
+ * variants. Returns `undefined` for `false`/unknown shapes (omit the key).
412
+ * Exported for the unit tests in `bundler-pick.spec.ts`.
413
+ */
414
+ export function translateSourcemapOption(v) {
415
+ if (v === true || v === 'file' || v === 'File')
416
+ return 'File';
417
+ if (v === 'inline' || v === 'Inline')
418
+ return 'Inline';
419
+ if (v === 'hidden' || v === 'Hidden')
420
+ return 'Hidden';
421
+ return undefined;
422
+ }
316
423
  /**
317
424
  * The Rust deserializer in the native facade treats `define` and `inject`
318
425
  * as top-level fields on `BundlerOptions`, while npm rolldown's JS API
@@ -345,17 +452,47 @@ function liftTransformExtras(opts) {
345
452
  ...lift,
346
453
  };
347
454
  }
348
- function mapToInjectArray(map) {
455
+ /**
456
+ * Convert npm rolldown's `transform.inject` map shorthand into the array
457
+ * of descriptors the native Rust deserializer expects. The Rust
458
+ * `InjectImport` enum (`tag = "type"`, `rename_all = "camelCase"`) has
459
+ * exactly TWO variants:
460
+ *
461
+ * - `{ type: 'named', imported, alias?, from }`
462
+ * - `{ type: 'namespace', alias, from }`
463
+ *
464
+ * npm rolldown's map spellings translate as:
465
+ *
466
+ * - `{ alias: 'module' }` → default import →
467
+ * `{ type: 'named', imported: 'default', alias, from: 'module' }`
468
+ * (the Rust `InjectImport::default()` constructor encodes default
469
+ * imports as a named import of `default` — there is NO `'default'`
470
+ * variant; emitting `{ type: 'default' }` crashes the deserializer
471
+ * with `unknown variant \`default\``).
472
+ * - `{ alias: ['module', '*'] }` → namespace import →
473
+ * `{ type: 'namespace', alias, from: 'module' }`.
474
+ * - `{ alias: ['module', 'name'] }` → named import →
475
+ * `{ type: 'named', from: 'module', imported: 'name', alias }`.
476
+ *
477
+ * Exported for the unit tests in `bundler-pick.spec.ts`.
478
+ */
479
+ export function mapToInjectArray(map) {
349
480
  const out = [];
350
481
  for (const [alias, value] of Object.entries(map)) {
351
482
  if (typeof value === 'string') {
352
483
  // Default-import binding: `import alias from 'module'`
353
- out.push({ type: 'default', from: value, alias });
484
+ out.push({ type: 'named', imported: 'default', from: value, alias });
354
485
  }
355
486
  else {
356
- // Named-import binding: `import { imported as alias } from 'from'`
357
487
  const [from, imported] = value;
358
- out.push({ type: 'named', from, imported, alias });
488
+ if (imported === '*') {
489
+ // Namespace-import binding: `import * as alias from 'from'`
490
+ out.push({ type: 'namespace', from, alias });
491
+ }
492
+ else {
493
+ // Named-import binding: `import { imported as alias } from 'from'`
494
+ out.push({ type: 'named', from, imported, alias });
495
+ }
359
496
  }
360
497
  }
361
498
  return out;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Build + run the gjsify yargs command surface for the given argv (already
3
+ * stripped of the `node`/`gjs` + script prefix — i.e. what `hideBin` returns).
4
+ *
5
+ * `parseAsync()` (not `.argv`) so the caller's `await` keeps the process alive
6
+ * until command handlers finish — under GJS the script ends as soon as the
7
+ * top-level synchronous flow does, and fire-and-forget handlers would silently
8
+ * exit before any async work runs.
9
+ */
10
+ export declare function runCli(argv: readonly string[]): Promise<void>;
package/lib/cli-app.js ADDED
@@ -0,0 +1,112 @@
1
+ // The gjsify CLI command surface, factored out of `index.ts` so it can be
2
+ // invoked two ways:
3
+ // 1. As the process entry point (`index.ts` → `runCli(hideBin(process.argv))`).
4
+ // 2. IN-PROCESS by `gjsify run` (`commands/run.ts`) to dispatch a script
5
+ // that is a single `gjsify <subcommand>` WITHOUT spawning a fresh gjs.
6
+ //
7
+ // Why (2) matters: under the GJS-first default every nested `gjsify …`
8
+ // invocation is a heavyweight gjs process (startup + bundle loads). A package
9
+ // build chains ~5 of them (`foreach → npm → gjsify run build:gjsify → gjsify
10
+ // build`, and `gjsify run build:types → gjsify tsc → gjs tsc.gjs.mjs`), so on
11
+ // CI's few cores the multiplier oversubscribes and the build thrashes. Letting
12
+ // `gjsify run` dispatch the inner `gjsify <cmd>` in-process collapses two gjs
13
+ // into one. See STATUS.md.
14
+ //
15
+ // This module MUST NOT execute anything at load time (no top-level
16
+ // `parseAsync`) — `run.ts` imports it, and a top-level run would re-dispatch
17
+ // the original argv on import.
18
+ import { readFileSync } from 'node:fs';
19
+ import { dirname, join } from 'node:path';
20
+ import { fileURLToPath } from 'node:url';
21
+ import yargs from 'yargs';
22
+ import { buildCommand as build, testCommand as test, runCommand as run, infoCommand as info, systemCheckCommand as systemCheck, checkCommand as check, showcaseCommand as showcase, createCommand as create, gresourceCommand as gresource, gettextCommand as gettext, gsettingsCommand as gsettings, flatpakCommand as flatpak, dlxCommand as dlx, installCommand as install, foreachCommand as foreach, workspaceCommand as workspace, packCommand as pack, publishCommand as publish, whoamiCommand as whoami, loginCommand as login, logoutCommand as logout, selfUpdateCommand as selfUpdate, generateInstallerCommand as generateInstaller, uninstallCommand as uninstall, formatCommand as format, lintCommand as lint, fixCommand as fix, upgradeCommand as upgrade, barrelsCommand as barrels, tscCommand as tsc, affectedCommand as affected, } from './commands/index.js';
23
+ import { APP_NAME } from './constants.js';
24
+ // Detect which runtime is executing the CLI (GJS or Node.js).
25
+ // GJS MUST be checked first because @gjsify/process sets
26
+ // `process.versions.node = '20.0.0'` for compatibility — a plain
27
+ // `process.versions.node` check would be a false Node positive under GJS.
28
+ function runtimeLabel() {
29
+ try {
30
+ const sys = globalThis.imports?.system;
31
+ if (sys?.version !== undefined) {
32
+ const v = Number(sys.version);
33
+ return `GJS ${Math.floor(v / 10000)}.${Math.floor((v % 10000) / 100)}.${v % 100} (SpiderMonkey)`;
34
+ }
35
+ }
36
+ catch {
37
+ /* not GJS */
38
+ }
39
+ if (typeof process !== 'undefined' && typeof process.versions?.node === 'string') {
40
+ return `Node.js ${process.version}`;
41
+ }
42
+ return 'unknown runtime';
43
+ }
44
+ // Read the version from package.json adjacent to the bundle. yargs's
45
+ // auto-version-discovery (its `pkg-up`-driven default) doesn't reach
46
+ // through the bundled `dist/cli.gjs.mjs` path on GJS — falls back to
47
+ // "unknown". Both layouts are covered:
48
+ // - dev (tsx, `yarn workspace`): src/index.ts → ../package.json
49
+ // - bundled (install -g): dist/cli.gjs.mjs → ../package.json
50
+ function readBundleVersion() {
51
+ try {
52
+ const here = dirname(fileURLToPath(import.meta.url));
53
+ const pkg = JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8'));
54
+ return typeof pkg.version === 'string' ? pkg.version : 'unknown';
55
+ }
56
+ catch {
57
+ return 'unknown';
58
+ }
59
+ }
60
+ /**
61
+ * Build + run the gjsify yargs command surface for the given argv (already
62
+ * stripped of the `node`/`gjs` + script prefix — i.e. what `hideBin` returns).
63
+ *
64
+ * `parseAsync()` (not `.argv`) so the caller's `await` keeps the process alive
65
+ * until command handlers finish — under GJS the script ends as soon as the
66
+ * top-level synchronous flow does, and fire-and-forget handlers would silently
67
+ * exit before any async work runs.
68
+ */
69
+ export async function runCli(argv) {
70
+ const cli = yargs(argv);
71
+ await cli
72
+ .scriptName(APP_NAME)
73
+ .version(readBundleVersion())
74
+ .strict()
75
+ // Use the full terminal width for help (yargs caps at 80 by default).
76
+ .wrap(cli.terminalWidth())
77
+ .command(create.command, create.description, create.builder, create.handler)
78
+ .command(install.command, install.description, install.builder, install.handler)
79
+ .command(build.command, build.description, build.builder, build.handler)
80
+ .command(test.command, test.description, test.builder, test.handler)
81
+ .command(run.command, run.description, run.builder, run.handler)
82
+ .command(dlx.command, dlx.description, dlx.builder, dlx.handler)
83
+ .command(info.command, info.description, info.builder, info.handler)
84
+ .command(systemCheck.command, systemCheck.description, systemCheck.builder, systemCheck.handler)
85
+ .command(check.command, check.description, check.builder, check.handler)
86
+ .command(showcase.command, showcase.description, showcase.builder, showcase.handler)
87
+ .command(gresource.command, gresource.description, gresource.builder, gresource.handler)
88
+ .command(gettext.command, gettext.description, gettext.builder, gettext.handler)
89
+ .command(gsettings.command, gsettings.description, gsettings.builder, gsettings.handler)
90
+ .command(flatpak.command, flatpak.description, flatpak.builder, flatpak.handler)
91
+ .command(foreach.command, foreach.description, foreach.builder, foreach.handler)
92
+ .command(workspace.command, workspace.description, workspace.builder, workspace.handler)
93
+ .command(pack.command, pack.description, pack.builder, pack.handler)
94
+ .command(publish.command, publish.description, publish.builder, publish.handler)
95
+ .command(whoami.command, whoami.description, whoami.builder, whoami.handler)
96
+ .command(login.command, login.description, login.builder, login.handler)
97
+ .command(logout.command, logout.description, logout.builder, logout.handler)
98
+ .command(selfUpdate.command, selfUpdate.description, selfUpdate.builder, selfUpdate.handler)
99
+ .command(generateInstaller.command, generateInstaller.description, generateInstaller.builder, generateInstaller.handler)
100
+ .command(uninstall.command, uninstall.description, uninstall.builder, uninstall.handler)
101
+ .command(upgrade.command, upgrade.description, upgrade.builder, upgrade.handler)
102
+ .command(format.command, format.description, format.builder, format.handler)
103
+ .command(lint.command, lint.description, lint.builder, lint.handler)
104
+ .command(fix.command, fix.description, fix.builder, fix.handler)
105
+ .command(barrels.command, barrels.description, barrels.builder, barrels.handler)
106
+ .command(tsc.command, tsc.description, tsc.builder, tsc.handler)
107
+ .command(affected.command, affected.description, affected.builder, affected.handler)
108
+ .demandCommand(1)
109
+ .epilogue(`Running on ${runtimeLabel()}`)
110
+ .help()
111
+ .parseAsync();
112
+ }
@@ -114,6 +114,10 @@ const IGNORE = [
114
114
  /^docs\//,
115
115
  /^\.github\/workflows\/(deploy-docs|commitlint|release|audit-runtimes|prebuilds)\.yml$/,
116
116
  /^\.githooks\//,
117
+ // Flatpak build/distribution tooling (SDK-extension manifest + metainfo).
118
+ // Like `.githooks/`, it has no package-test consumers; its own
119
+ // `tests/e2e/flatpak-sdk-extension` runs on `tests/e2e/**` / global triggers.
120
+ /^flatpak\//,
117
121
  /^LICENSE/,
118
122
  /^\.gitignore$/,
119
123
  /^\.gjsify-[^/]*\.md$/,
@@ -107,7 +107,7 @@ export const buildCommand = {
107
107
  normalize: true,
108
108
  })
109
109
  .option('external', {
110
- description: 'Module names that should NOT be bundled. Repeat the flag or pass a comma-separated list (e.g. --external typedoc,prettier). Globs are forwarded to esbuild as-is. See https://esbuild.github.io/api/#external',
110
+ description: 'Module names that should NOT be bundled — EXACT package names only, matched literally on both bundler engines (no globs). Repeat the flag or pass a comma-separated list (e.g. --external typedoc,prettier). Appended to the per-app built-in externals; register subpaths (`<pkg>/register[/…]`) are always force-inlined for --app gjs regardless of this flag.',
111
111
  array: true,
112
112
  type: 'string',
113
113
  default: [],
@@ -146,6 +146,10 @@ export const buildCommand = {
146
146
  });
147
147
  },
148
148
  handler: async (args) => {
149
+ // NOTE: `gjsify build` runs under BOTH Node (npm `rolldown`) and GJS
150
+ // (native `@gjsify/rolldown-native`, PR #483). The Node-free release
151
+ // pipeline dogfoods the GJS path — `gjsify run build` under the
152
+ // GJS-bundled CLI rebuilds the bundle. Do NOT gate this on the runtime.
149
153
  const config = new Config();
150
154
  const configData = await config.forBuild(args);
151
155
  const action = new BuildAction(configData);
@@ -15,6 +15,7 @@
15
15
  // are reachable here, the `--json` flag routes through this command first
16
16
  // (since the typescript-check binding is positional `[paths..]`).
17
17
  import { spawn, spawnSync } from 'node:child_process';
18
+ import { prefixLines } from '../utils/prefixed-output.js';
18
19
  import { existsSync, readFileSync } from 'node:fs';
19
20
  import { join } from 'node:path';
20
21
  import { cpus } from 'node:os';
@@ -42,22 +43,29 @@ function runCheck(ws, prefix) {
42
43
  cwd: ws.location,
43
44
  stdio: prefix === null ? 'inherit' : ['ignore', 'pipe', 'pipe'],
44
45
  });
46
+ // Shared with `foreach`: live line-prefixing on a tty, buffered
47
+ // flush-on-close on a non-tty sink. The previous inline forwarder
48
+ // wrote every line live — under GJS `process.stdout.write` is a
49
+ // blocking `Gio.write_all`, so a parallel check into a
50
+ // backpressuring CI pipe could stall the GLib loop the same way
51
+ // the foreach examples build did. See utils/prefixed-output.ts.
52
+ const buffered = !process.stdout.isTTY;
53
+ const flushers = [];
45
54
  if (prefix !== null && child.stdout && child.stderr) {
46
55
  const tag = `[${prefix}] `;
47
- const forward = (stream, dest) => {
48
- stream.on('data', (chunk) => {
49
- const text = chunk.toString('utf-8');
50
- for (const line of text.split('\n')) {
51
- if (line.length > 0)
52
- dest.write(`${tag}${line}\n`);
53
- }
54
- });
55
- };
56
- forward(child.stdout, process.stdout);
57
- forward(child.stderr, process.stderr);
56
+ flushers.push(prefixLines(child.stdout, process.stdout, tag, buffered));
57
+ flushers.push(prefixLines(child.stderr, process.stderr, tag, buffered));
58
58
  }
59
- child.on('close', (code) => resolve(code ?? 1));
60
- child.on('error', () => resolve(1));
59
+ child.on('close', (code) => {
60
+ for (const flush of flushers)
61
+ flush();
62
+ resolve(code ?? 1);
63
+ });
64
+ child.on('error', () => {
65
+ for (const flush of flushers)
66
+ flush();
67
+ resolve(1);
68
+ });
61
69
  });
62
70
  }
63
71
  export const checkCommand = {
@@ -91,7 +91,9 @@ export const dlxCommand = {
91
91
  if (entry.fromFallback) {
92
92
  console.warn(`[gjsify dlx] package "${cachedPkgName ?? parsed.kind}" has no \`gjsify\` field — falling back to package.json#main. Add \`gjsify.main\` to silence.`);
93
93
  }
94
- await runGjsBundle(entry.bundlePath, extraArgs);
94
+ // Terminal call — exit on success, or the GJS main loop parks this
95
+ // process forever (see RunGjsBundleOptions.exitOnSuccess).
96
+ await runGjsBundle(entry.bundlePath, extraArgs, { exitOnSuccess: true });
95
97
  },
96
98
  };
97
99
  async function ensurePkgDir(parsed, opts) {
@@ -7,7 +7,7 @@
7
7
  // Naming: deliberately distinct from `gjsify check` (workspace TS check) and
8
8
  // `gjsify system-check` (system-dependency verifier).
9
9
  import { resolve } from 'node:path';
10
- import { OxcNotFoundError, findOxfmtConfig, findOxlintConfig, printOxcNotFound, runOxfmt, runOxlint, } from '../utils/oxc-resolve.js';
10
+ import { OxcNotFoundError, findOxfmtConfig, findOxlintConfig, printOxcNotFound, runOxfmt, runOxlint, setOxcExitCode, } from '../utils/oxc-resolve.js';
11
11
  export const fixCommand = {
12
12
  command: 'fix [paths..]',
13
13
  description: 'Run oxfmt --write then oxlint --fix — format + safe lint fixes in one pass.',
@@ -56,12 +56,12 @@ export const fixCommand = {
56
56
  lintArgs.push('--config', resolve(lintConfig));
57
57
  lintArgs.push(...paths);
58
58
  const lintCode = await runOxlint(lintArgs, { cwd, verbose });
59
- process.exitCode = fmtCode || lintCode;
59
+ setOxcExitCode(fmtCode || lintCode);
60
60
  }
61
61
  catch (err) {
62
62
  if (err instanceof OxcNotFoundError) {
63
63
  printOxcNotFound(err);
64
- process.exitCode = 1;
64
+ setOxcExitCode(1);
65
65
  return;
66
66
  }
67
67
  throw err;