@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/actions/build.js
CHANGED
|
@@ -259,7 +259,15 @@ export class BuildAction {
|
|
|
259
259
|
external: userBundler.external,
|
|
260
260
|
transform: userBundler.transform,
|
|
261
261
|
format,
|
|
262
|
-
}, pluginOpts, gjsifyPluginFactory, verbose,
|
|
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) {
|
package/lib/bundler-pick.d.ts
CHANGED
|
@@ -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;
|
package/lib/bundler-pick.js
CHANGED
|
@@ -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
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
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
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/lib/cli-app.d.ts
ADDED
|
@@ -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
|
+
}
|
package/lib/commands/affected.js
CHANGED
|
@@ -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$/,
|
package/lib/commands/build.js
CHANGED
|
@@ -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).
|
|
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);
|
package/lib/commands/check.js
CHANGED
|
@@ -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
|
-
|
|
48
|
-
|
|
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) =>
|
|
60
|
-
|
|
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 = {
|
package/lib/commands/dlx.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|
package/lib/commands/fix.js
CHANGED
|
@@ -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
|
-
|
|
59
|
+
setOxcExitCode(fmtCode || lintCode);
|
|
60
60
|
}
|
|
61
61
|
catch (err) {
|
|
62
62
|
if (err instanceof OxcNotFoundError) {
|
|
63
63
|
printOxcNotFound(err);
|
|
64
|
-
|
|
64
|
+
setOxcExitCode(1);
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
67
67
|
throw err;
|