@gjsify/cli 0.52.0 → 0.53.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.
Files changed (81) hide show
  1. package/dist/cli.gjs.mjs +329 -298
  2. package/lib/actions/build.d.ts +22 -0
  3. package/lib/actions/build.js +37 -9
  4. package/lib/bundler-pick.d.ts +37 -1
  5. package/lib/bundler-pick.js +108 -17
  6. package/lib/cli-app.js +5 -2
  7. package/lib/commands/env.d.ts +31 -0
  8. package/lib/commands/env.js +114 -0
  9. package/lib/commands/exec.d.ts +10 -0
  10. package/lib/commands/exec.js +208 -0
  11. package/lib/commands/foreach.js +5 -97
  12. package/lib/commands/format.js +26 -6
  13. package/lib/commands/index.d.ts +3 -0
  14. package/lib/commands/index.js +3 -0
  15. package/lib/commands/install.d.ts +2 -2
  16. package/lib/commands/install.js +5 -5
  17. package/lib/commands/self-update.js +2 -1
  18. package/lib/commands/test.d.ts +18 -9
  19. package/lib/commands/test.js +56 -11
  20. package/lib/commands/tsc.js +4 -4
  21. package/lib/commands/upgrade.d.ts +15 -0
  22. package/lib/commands/upgrade.js +76 -2
  23. package/lib/commands/webext/index.d.ts +2 -0
  24. package/lib/commands/webext/index.js +15 -0
  25. package/lib/commands/webext/subcommands.d.ts +16 -0
  26. package/lib/commands/webext/subcommands.js +204 -0
  27. package/lib/types/config-data.d.ts +1 -1
  28. package/lib/utils/bin-shim.d.ts +8 -2
  29. package/lib/utils/bin-shim.js +6 -5
  30. package/lib/utils/bundle-inputs.d.ts +84 -0
  31. package/lib/utils/bundle-inputs.js +268 -0
  32. package/lib/utils/check-system-deps.d.ts +4 -2
  33. package/lib/utils/check-system-deps.js +21 -26
  34. package/lib/utils/dev-link.d.ts +9 -6
  35. package/lib/utils/dev-link.js +71 -19
  36. package/lib/utils/exec-bin.d.ts +137 -0
  37. package/lib/utils/exec-bin.js +321 -0
  38. package/lib/utils/gi-runtime-paths.js +3 -2
  39. package/lib/utils/gitignore.d.ts +15 -0
  40. package/lib/utils/gitignore.js +178 -0
  41. package/lib/utils/gjsify-shim.d.ts +47 -0
  42. package/lib/utils/gjsify-shim.js +97 -8
  43. package/lib/utils/install-backend-native.d.ts +19 -1
  44. package/lib/utils/install-backend-native.js +93 -7
  45. package/lib/utils/install-lock.js +4 -25
  46. package/lib/utils/native-skip-scan.d.ts +20 -0
  47. package/lib/utils/native-skip-scan.js +343 -0
  48. package/lib/utils/node-script.js +1 -1
  49. package/lib/utils/oxc-resolve.js +42 -3
  50. package/lib/utils/process-table.d.ts +22 -0
  51. package/lib/utils/process-table.js +188 -0
  52. package/lib/utils/prune-prefix.js +1 -1
  53. package/lib/utils/run-gjs.d.ts +15 -1
  54. package/lib/utils/run-gjs.js +16 -7
  55. package/lib/utils/run-node.d.ts +2 -0
  56. package/lib/utils/run-node.js +2 -1
  57. package/lib/utils/runtimes.js +14 -13
  58. package/lib/utils/ship/gzip.js +1 -1
  59. package/lib/utils/ship/icons.d.ts +6 -0
  60. package/lib/utils/ship/icons.js +6 -5
  61. package/lib/utils/spawn.d.ts +13 -0
  62. package/lib/utils/spawn.js +10 -5
  63. package/lib/utils/watch-loop.d.ts +15 -1
  64. package/lib/utils/watch-loop.js +9 -5
  65. package/lib/utils/webext/build.d.ts +42 -0
  66. package/lib/utils/webext/build.js +120 -0
  67. package/lib/utils/webext/config.d.ts +72 -0
  68. package/lib/utils/webext/config.js +234 -0
  69. package/lib/utils/webext/icons.d.ts +24 -0
  70. package/lib/utils/webext/icons.js +83 -0
  71. package/lib/utils/webext/launch.d.ts +32 -0
  72. package/lib/utils/webext/launch.js +77 -0
  73. package/lib/utils/webext/manifest.d.ts +53 -0
  74. package/lib/utils/webext/manifest.js +219 -0
  75. package/lib/utils/webext/pages.d.ts +26 -0
  76. package/lib/utils/webext/pages.js +61 -0
  77. package/lib/utils/webext/targets.d.ts +13 -0
  78. package/lib/utils/webext/targets.js +36 -0
  79. package/lib/utils/win32-command.d.ts +17 -0
  80. package/lib/utils/win32-command.js +21 -6
  81. package/package.json +19 -19
@@ -1,6 +1,7 @@
1
1
  import type { ConfigData } from '../types/index.js';
2
2
  import type { App } from '@gjsify/rolldown-plugin-gjsify';
3
3
  import type { RolldownOutput } from 'rolldown';
4
+ import { type RunBundleOptions } from '../bundler-pick.js';
4
5
  export declare class BuildAction {
5
6
  readonly configData: ConfigData;
6
7
  constructor(configData?: ConfigData);
@@ -186,11 +187,31 @@ export declare class BuildAction {
186
187
  */
187
188
  resolveFromToolchain?: boolean;
188
189
  }): Promise<string>;
190
+ /**
191
+ * The uncached half of {@link bundleFileForGjsCached}: bundle `inputPath` to
192
+ * `outfile` as one self-contained `--app gjs` module. `gjsify exec` calls it
193
+ * directly because it keys its artifacts by CONTENT (package version, lockfile,
194
+ * CLI version — `utils/exec-bin.ts`) rather than by mtime.
195
+ */
196
+ static bundleFileForGjs(inputPath: string, outfile: string, opts: {
197
+ verbose?: boolean;
198
+ define?: Record<string, string>;
199
+ preserveDefaultExport?: boolean;
200
+ globals?: string;
201
+ excludeGlobals?: string[];
202
+ /** The running CLI's file, or null — see `resolveFromToolchain` above; the CALLER decides. */
203
+ toolchainAnchor?: string | null;
204
+ /** `silent` drops warnings, never errors — errors reject the build either way. */
205
+ logLevel?: 'silent';
206
+ }): Promise<void>;
207
+ /** The running CLI's own file — the toolchain anchor `gjsify exec` hands {@link bundleFileForGjs}. */
208
+ static runningCliFile(): string | null;
189
209
  /** Application mode */
190
210
  buildApp(app?: App, opts?: {
191
211
  watch?: boolean;
192
212
  preserveDefaultExport?: boolean;
193
213
  toolchainAnchor?: string;
214
+ onWatchFiles?: RunBundleOptions['onWatchFiles'];
194
215
  }): Promise<RolldownOutput[]>;
195
216
  /**
196
217
  * Drive `rolldown.watch(...)`: rebuild on source change, apply the
@@ -203,5 +224,6 @@ export declare class BuildAction {
203
224
  library?: boolean;
204
225
  app?: App;
205
226
  watch?: boolean;
227
+ onWatchFiles?: RunBundleOptions['onWatchFiles'];
206
228
  }): Promise<RolldownOutput[]>;
207
229
  }
@@ -555,6 +555,16 @@ export class BuildAction {
555
555
  }
556
556
  if (opts.verbose)
557
557
  console.debug(`[gjsify] bundling ${inputPath} for GJS → ${outfile}`);
558
+ await BuildAction.bundleFileForGjs(inputPath, outfile, { ...opts, toolchainAnchor: anchorFile });
559
+ return outfile;
560
+ }
561
+ /**
562
+ * The uncached half of {@link bundleFileForGjsCached}: bundle `inputPath` to
563
+ * `outfile` as one self-contained `--app gjs` module. `gjsify exec` calls it
564
+ * directly because it keys its artifacts by CONTENT (package version, lockfile,
565
+ * CLI version — `utils/exec-bin.ts`) rather than by mtime.
566
+ */
567
+ static async bundleFileForGjs(inputPath, outfile, opts) {
558
568
  await mkdir(dirname(outfile), { recursive: true });
559
569
  await new BuildAction({
560
570
  verbose: opts.verbose,
@@ -564,15 +574,18 @@ export class BuildAction {
564
574
  input: inputPath,
565
575
  output: { file: outfile },
566
576
  ...(opts.define ? { transform: { define: opts.define } } : {}),
577
+ ...(opts.logLevel ? { logLevel: opts.logLevel } : {}),
567
578
  },
568
579
  }).buildApp('gjs', {
569
580
  preserveDefaultExport: opts.preserveDefaultExport ?? true,
570
- // See `opts.resolveFromToolchain`: the CALLER decides whether this input is
571
- // toolchain, because this helper alone cannot tell a config file from a
572
- // consumer's `node ./src/main.mjs`.
573
- ...(anchorFile === null ? {} : { toolchainAnchor: anchorFile }),
581
+ // The CALLER decides whether this input is toolchain, because this helper
582
+ // alone cannot tell a config file from a consumer's `node ./src/main.mjs`.
583
+ ...(opts.toolchainAnchor ? { toolchainAnchor: opts.toolchainAnchor } : {}),
574
584
  });
575
- return outfile;
585
+ }
586
+ /** The running CLI's own file — the toolchain anchor `gjsify exec` hands {@link bundleFileForGjs}. */
587
+ static runningCliFile() {
588
+ return runningCliFile();
576
589
  }
577
590
  /** Application mode */
578
591
  async buildApp(app = 'gjs', opts = {}) {
@@ -712,7 +725,13 @@ export class BuildAction {
712
725
  // unresolvable import would hard-crash the analysis pass.
713
726
  // Mirrors the explicit-globals path (resolveGlobalsInject)
714
727
  // and resolveUserPlugins above, which already anchor on cwd.
715
- { extraGlobalsList: extras, excludeGlobals, cwd: process.cwd() }, bundleToChunks);
728
+ {
729
+ extraGlobalsList: extras,
730
+ excludeGlobals,
731
+ cwd: process.cwd(),
732
+ // The same fallback the resolver gets, or the gate drops what it would rescue.
733
+ ...(opts.toolchainAnchor !== undefined ? { toolchainDir: dirname(opts.toolchainAnchor) } : {}),
734
+ }, bundleToChunks);
716
735
  pluginOpts.autoGlobalsInject = injectPath;
717
736
  }
718
737
  else if (app === 'node' && autoMode) {
@@ -803,7 +822,7 @@ export class BuildAction {
803
822
  await this.runWatchLoop(finalOpts, app, outfile, outdir, verbose);
804
823
  return [];
805
824
  }
806
- const writeResult = await runBundle(finalOpts);
825
+ const writeResult = await runBundle(finalOpts, { onWatchFiles: opts.onWatchFiles });
807
826
  // GJS truncates module source at a raw U+0000 (it is handed to SpiderMonkey as a
808
827
  // NUL-terminated C string), so escape any the minifier emitted before anything else
809
828
  // touches the file. See utils/gjs-source-escape.ts.
@@ -920,14 +939,23 @@ export class BuildAction {
920
939
  }
921
940
  await closed;
922
941
  }
923
- async start(buildType = { app: 'gjs' }) {
942
+ async start(buildType = {
943
+ app: 'gjs',
944
+ }) {
924
945
  if (buildType.library) {
946
+ // `onWatchFiles` has no consumer here and is deliberately NOT
947
+ // forwarded: a `--library` build emits a published package, and
948
+ // nothing asks whether it is stale (`gjsify test` is the only
949
+ // freshness check, and it never builds a library).
925
950
  if (buildType.watch) {
926
951
  throw new Error('gjsify build: --watch is not supported with --library (library mode would emit watcher rebuilds for every produced format; use --app gjs|node|browser instead).');
927
952
  }
928
953
  return await this.buildLibrary();
929
954
  }
930
- return await this.buildApp(buildType.app, { watch: buildType.watch });
955
+ return await this.buildApp(buildType.app, {
956
+ watch: buildType.watch,
957
+ onWatchFiles: buildType.onWatchFiles,
958
+ });
931
959
  }
932
960
  }
933
961
  async function runOneLibraryBuild(args) {
@@ -8,6 +8,20 @@ export interface NativePluginContext {
8
8
  id: string;
9
9
  external: boolean;
10
10
  } | null>;
11
+ /**
12
+ * rolldown's `this.addWatchFile`, collected by the native facade into
13
+ * `BundleResult.watchedFiles` — the native half of what npm rolldown reports
14
+ * as `RolldownBuild.watchFiles`.
15
+ *
16
+ * OPTIONAL here on purpose. The engine is resolved through several anchors
17
+ * and a global-prefix copy can answer first (measured 2026-09-29 on a dev
18
+ * host whose project-local engine was the current one), so a plugin cannot
19
+ * assume the method exists: it is this bridge's documented stance that an
20
+ * unimplemented context method FAILS at hook-call time, and a build must
21
+ * not fail over bookkeeping. `gjsify-css-as-string` therefore
22
+ * feature-detects and warns once.
23
+ */
24
+ addWatchFile(id: string): void;
11
25
  warn(message: string): void;
12
26
  error(message: string): never;
13
27
  }
@@ -72,11 +86,33 @@ export declare function bundleToChunks(input: {
72
86
  * and is responsible for invoking `watcher.close()` on shutdown.
73
87
  */
74
88
  export declare function runWatch(finalOpts: BundlerOptions): Promise<RolldownWatcher>;
89
+ /** What one build reported about the files its output depends on. */
90
+ export interface BuildWatchList {
91
+ /**
92
+ * The engine's watch list: npm rolldown's `RolldownBuild.watchFiles` (its
93
+ * module set plus every `this.addWatchFile`), the native facade's
94
+ * `watchedFiles` (the declared half — the native engine reports no modules,
95
+ * hence the two halves rather than either). Read BEFORE `close()`, which is
96
+ * where the npm getter is still alive.
97
+ */
98
+ files: readonly string[];
99
+ /**
100
+ * `false` when the engine CANNOT report one at all (a `@gjsify/rolldown-native`
101
+ * older than `addWatchFile`). A caller that judges a build's inputs on this
102
+ * list has to refuse to trust it — the file a plugin read itself is then
103
+ * invisible, and a stale bundle would read as current.
104
+ */
105
+ reportedByEngine: boolean;
106
+ }
107
+ /** Where a build's own input list goes. Absent → the caller does not want it. */
108
+ export interface RunBundleOptions {
109
+ onWatchFiles?: (watch: BuildWatchList) => void;
110
+ }
75
111
  /**
76
112
  * Run a bundle with the picked engine. Drop-in replacement for the
77
113
  * `rolldown(opts).write(opts.output)` flow used directly in build.ts.
78
114
  */
79
- export declare function runBundle(finalOpts: BundlerOptions): Promise<RolldownOutput>;
115
+ export declare function runBundle(finalOpts: BundlerOptions, options?: RunBundleOptions): Promise<RolldownOutput>;
80
116
  export declare function shouldUseNative(): Promise<boolean>;
81
117
  /**
82
118
  * Walk parent directories from `startDir` (and optionally a second anchor
@@ -10,7 +10,7 @@
10
10
  // synchronous require of a builtin, so the load dies before any `.node` is opened.
11
11
  // The distinction is not pedantry — it says an N-API host alone would not be
12
12
  // enough; the wrapper has to be bypassed too, which is exactly what
13
- // `napiNodeAddonPlugin` does for napi-rs packages. See `status/open-todos.md`
13
+ // `napiNodeAddonPlugin` does for napi-rs packages. See `status/open-todos/README.md`
14
14
  // § "Can `@gjsify/napi` retire the hand-written `-native` bridges?".
15
15
  //
16
16
  // `GJSIFY_BUNDLER=native|npm` overrides that: `native` throws when the prebuild is
@@ -30,7 +30,7 @@
30
30
  import { promises as fs, existsSync } from 'node:fs';
31
31
  import * as path from 'node:path';
32
32
  import { createRequire } from 'node:module';
33
- import { pathToFileURL } from 'node:url';
33
+ import { fileURLToPath, pathToFileURL } from 'node:url';
34
34
  import { resolveNpmPackage } from './utils/resolve-npm-package.js';
35
35
  // Static, not dynamic: `diagnoseNativeEngine()` is synchronous and called inside a
36
36
  // `throw` expression. Safe because check-system-deps imports only `node:` builtins,
@@ -80,7 +80,8 @@ async function loadNpmRolldown() {
80
80
  function diagnoseNativeEngine() {
81
81
  let pkgDir = null;
82
82
  try {
83
- const bundleDir = path.dirname(new URL(import.meta.url).pathname);
83
+ // Not `new URL(...).pathname`: on win32 that is `/C:/...`, percent-encoded, no path.
84
+ const bundleDir = path.dirname(fileURLToPath(import.meta.url));
84
85
  pkgDir = findRolldownNativeDir(process.cwd(), bundleDir);
85
86
  }
86
87
  catch {
@@ -131,23 +132,39 @@ function diagnoseNativeEngine() {
131
132
  // reads the same tables `gjsify system-check` does, so the answer cannot drift
132
133
  // from the declaration.
133
134
  const missing = missingSystemDepsFor('@gjsify/rolldown-native');
134
- if (missing.length > 0) {
135
- const names = missing.map((d) => d.name).join(', ');
135
+ const names = missing.map((d) => d.name).join(', ');
136
+ const cmd = missing.length > 0 ? buildInstallCommand(detectPackageManager(), missing) : null;
137
+ const installHint = missing.length === 0
138
+ ? undefined
139
+ : cmd
140
+ ? `Install it:\n ${cmd}`
141
+ : `Install ${names} with your system package manager (this host's manager was not recognised, so no command is suggested rather than a wrong one).`;
142
+ // (0) The measurement could not run at all. First, because everything below
143
+ // reasons from a measurement: with no reading in hand, (2) would assert the
144
+ // prebuild IS on girepository's search path — a positive claim this PR
145
+ // exists to make measurable, so its absence is stated rather than papered over.
146
+ if (_utilsCoreError !== null) {
147
+ parts.push(`\nNOT MEASURED — the native-library probe could not be loaded, so nothing below is a reading of this host:\n ${_utilsCoreError instanceof Error ? _utilsCoreError.message : String(_utilsCoreError)}`);
148
+ }
149
+ // (1) The loader's own answer, when `tryLoadNative()` measured one: the file
150
+ // that would not open and the dependency it named. It outranks (2), which only
151
+ // knows what pkg-config can see — the dependency may be one no table declares.
152
+ if (_nativeLibraryFailure && _utilsCore) {
153
+ parts.push(`\nMEASURED CAUSE — ${new _utilsCore.NativeLibraryLoadError(_nativeLibraryFailure, installHint).message}`);
154
+ }
155
+ else if (missing.length > 0) {
136
156
  parts.push(`\nMEASURED CAUSE — a system library the engine's prebuild loads is MISSING: ${names}.\n` +
137
157
  'The prebuild is present but its typelib cannot open its backing library, which GJS reports as ' +
138
158
  '`Unsupported type void, deriving from fundamental void` — a message that names nothing. ' +
139
159
  'This is a system package, not an npm one.');
140
- const cmd = buildInstallCommand(detectPackageManager(), missing);
141
- if (cmd)
142
- parts.push(`Install it:\n ${cmd}`);
143
- else
144
- parts.push(`Install ${names} with your system package manager (this host's manager was not recognised, so no command is suggested rather than a wrong one).`);
160
+ if (installHint)
161
+ parts.push(installHint);
145
162
  }
146
163
  // (2) No prebuild for this architecture — last because it is least actionable.
147
164
  // Read off the set `activateNativePrebuilds()` ACTUALLY put on the search paths:
148
165
  // a memoized cached-array read that cannot throw, and it names the per-target
149
166
  // sibling package really holding the artifact.
150
- if (missing.length === 0) {
167
+ if (missing.length === 0 && !_nativeLibraryFailure) {
151
168
  const engine = activateNativePrebuilds().find((p) => p.name.startsWith('@gjsify/rolldown-native'));
152
169
  parts.push(engine
153
170
  ? `\nEvery declared system library resolves and ${engine.name}'s prebuild (${engine.prebuildsDir}) IS on ` +
@@ -255,24 +272,64 @@ export async function runWatch(finalOpts) {
255
272
  const output = finalOpts.output ?? {};
256
273
  return mod.watch({ ...finalOpts, output });
257
274
  }
275
+ let _warnedNoWatchList = false;
276
+ function warnNoNativeWatchList() {
277
+ if (_warnedNoWatchList)
278
+ return;
279
+ _warnedNoWatchList = true;
280
+ console.warn('[gjsify] this `@gjsify/rolldown-native` build reports no watch list: it predates ' +
281
+ '`addWatchFile`, so a file a plugin read itself (a stylesheet\u2019s `@import` chain, an ' +
282
+ 'inlined `readFileSync`) cannot be declared as a build input. `gjsify test` will rebuild ' +
283
+ 'the affected bundles on every run rather than trust a set it cannot see. Upgrade the ' +
284
+ 'engine (it ships in the same release train as the CLI).');
285
+ }
258
286
  /**
259
287
  * Run a bundle with the picked engine. Drop-in replacement for the
260
288
  * `rolldown(opts).write(opts.output)` flow used directly in build.ts.
261
289
  */
262
- export async function runBundle(finalOpts) {
290
+ export async function runBundle(finalOpts, options = {}) {
263
291
  if (await shouldUseNative()) {
264
- return await runNativeBundle(finalOpts);
292
+ return await runNativeBundle(finalOpts, options);
265
293
  }
266
294
  const rolldown = await loadNpmRolldown();
267
295
  const build = await rolldown(finalOpts);
268
296
  try {
269
- return await build.write(finalOpts.output ?? {});
297
+ const output = await build.write(finalOpts.output ?? {});
298
+ if (options.onWatchFiles) {
299
+ options.onWatchFiles({ files: await build.watchFiles, reportedByEngine: true });
300
+ }
301
+ return output;
270
302
  }
271
303
  finally {
272
304
  await build.close();
273
305
  }
274
306
  }
275
307
  let _nativeProbe = null;
308
+ /** Why the engine's library would not open, when `tryLoadNative()` measured it. */
309
+ let _nativeLibraryFailure = null;
310
+ let _utilsCore = null;
311
+ /**
312
+ * Why the probe itself could not run, when that is the reason. Kept apart from
313
+ * "there is no engine" on purpose: `tryLoadNative()`'s bare `catch` cannot tell
314
+ * the two apart, and `diagnoseNativeEngine()` then answers a question nobody
315
+ * asked — it reports which system libraries resolve, and says the prebuild IS on
316
+ * girepository's search path, which is a positive claim about a measurement that
317
+ * never happened. This PR exists to make such a claim measurable, so a missing
318
+ * measurement is reported as one.
319
+ */
320
+ let _utilsCoreError = null;
321
+ async function utilsCore() {
322
+ if (_utilsCore === null) {
323
+ // `./native-library`, not `./core`: `core` re-exports `main-loop`, which
324
+ // holds a module-level singleton. The GJS bundle inlines utils already,
325
+ // so loading `core` off disk would give the process a SECOND copy of that
326
+ // singleton — inert today because only pure probe functions are read
327
+ // through this edge, and a trap the first time that stops being true.
328
+ const href = pathToFileURL(createRequire(import.meta.url).resolve('@gjsify/utils/native-library')).href;
329
+ _utilsCore = (await import(/* @vite-ignore */ href));
330
+ }
331
+ return _utilsCore;
332
+ }
276
333
  export async function shouldUseNative() {
277
334
  const env = globalThis.process?.env ?? {};
278
335
  const choice = env.GJSIFY_BUNDLER;
@@ -282,7 +339,8 @@ export async function shouldUseNative() {
282
339
  if (choice === 'native') {
283
340
  const native = await tryLoadNative();
284
341
  if (!native) {
285
- throw new Error('GJSIFY_BUNDLER=native but @gjsify/rolldown-native is not loadable (no prebuild for this architecture, or not running under GJS).');
342
+ throw new Error('GJSIFY_BUNDLER=native but @gjsify/rolldown-native is not loadable (no prebuild for this architecture, or not running under GJS).' +
343
+ (isGjs() ? `\n${diagnoseNativeEngine()}` : ''));
286
344
  }
287
345
  return true;
288
346
  }
@@ -375,7 +433,7 @@ async function tryLoadNative() {
375
433
  // limitations and `findWorkspaceRoot`'s dependency on
376
434
  // `discoverWorkspaces`, both of which can fail to map a
377
435
  // sub-package dir up to the hoisted workspace root.
378
- const bundleDir = path.dirname(new URL(import.meta.url).pathname);
436
+ const bundleDir = path.dirname(fileURLToPath(import.meta.url));
379
437
  const resolvedFromNpm = resolveNpmPackage(specifier, { bundleUrl: import.meta.url });
380
438
  const resolvedFromFs = resolvedFromNpm
381
439
  ? null
@@ -399,6 +457,25 @@ async function tryLoadNative() {
399
457
  const mod = (await import(/* @vite-ignore */ target));
400
458
  if (!mod.hasNativeRolldown())
401
459
  return null;
460
+ // The typelib resolved, which says nothing about its library: that
461
+ // opens at the first class access, and a missing system library
462
+ // (Homebrew json-glib on a Mac) would pass here and fail inside
463
+ // `new BundlerSession()` as the nameless "Unsupported type void" —
464
+ // past `diagnoseNativeEngine()`, which then never runs.
465
+ // Separated from the engine's own failure so that "the engine is
466
+ // absent" and "the measurement could not run" stay distinguishable
467
+ // at the one place that reports them.
468
+ let surface;
469
+ try {
470
+ surface = await utilsCore();
471
+ }
472
+ catch (err) {
473
+ _utilsCoreError = err;
474
+ return null;
475
+ }
476
+ _nativeLibraryFailure = surface.probeNativeLibrary('GjsifyRolldown');
477
+ if (_nativeLibraryFailure)
478
+ return null;
402
479
  return mod;
403
480
  }
404
481
  catch {
@@ -407,7 +484,7 @@ async function tryLoadNative() {
407
484
  })();
408
485
  return _nativeProbe;
409
486
  }
410
- async function runNativeBundle(finalOpts) {
487
+ async function runNativeBundle(finalOpts, options = {}) {
411
488
  const native = await tryLoadNative();
412
489
  if (!native) {
413
490
  throw new Error('@gjsify/rolldown-native not loadable');
@@ -440,6 +517,20 @@ async function runNativeBundle(finalOpts) {
440
517
  }
441
518
  const result = await native.bundleWithPlugins(bundlerOpts, nativePlugins);
442
519
  reportNativeWarnings(result, finalOpts);
520
+ if (options.onWatchFiles) {
521
+ if (result.watchedFiles === undefined) {
522
+ // NOT a silent drop. A version-skewed engine has no `addWatchFile` on
523
+ // its plugin context, so nothing a plugin declares can reach the
524
+ // build's input set — and a caller judging freshness on that set
525
+ // would call a stale bundle current. Say so here, where the engine is
526
+ // in hand, and report `reportedByEngine: false` so the caller refuses.
527
+ warnNoNativeWatchList();
528
+ options.onWatchFiles({ files: [], reportedByEngine: false });
529
+ }
530
+ else {
531
+ options.onWatchFiles({ files: result.watchedFiles, reportedByEngine: true });
532
+ }
533
+ }
443
534
  // The native facade returns the BundleOutput shape but doesn't
444
535
  // write files — replicate `.write()` here so callers see the same
445
536
  // on-disk layout whether they used npm rolldown or native.
package/lib/cli-app.js CHANGED
@@ -10,7 +10,7 @@
10
10
  // build`, and `gjsify run build:types → gjsify tsc → gjs tsc.gjs.mjs`), so on
11
11
  // CI's few cores the multiplier oversubscribes and the build thrashes. Letting
12
12
  // `gjsify run` dispatch the inner `gjsify <cmd>` in-process collapses two gjs
13
- // into one. See status/open-todos.md.
13
+ // into one. See status/open-todos/README.md.
14
14
  //
15
15
  // This module MUST NOT execute anything at load time (no top-level
16
16
  // `parseAsync`) — `run.ts` imports it, and a top-level run would re-dispatch
@@ -18,7 +18,7 @@
18
18
  import yargs from 'yargs';
19
19
  import { classifyCliFailure } from './cli-fail.js';
20
20
  import { cliVersion } from './utils/publish-headers.js';
21
- import { buildCommand as build, clearCommand as clear, copyCommand as copy, 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, shipCommand as ship, dlxCommand as dlx, installCommand as install, linkCommand as link, unlinkCommand as unlink, foreachCommand as foreach, workspaceCommand as workspace, packCommand as pack, publishCommand as publish, whoamiCommand as whoami, loginCommand as login, logoutCommand as logout, trustCommand as trust, onboardCommand as onboard, selfUpdateCommand as selfUpdate, generateInstallerCommand as generateInstaller, pruneCommand as prune, uninstallCommand as uninstall, formatCommand as format, lintCommand as lint, fixCommand as fix, upgradeCommand as upgrade, barrelsCommand as barrels, tscCommand as tsc, affectedCommand as affected, storybookCommand as storybook, devCommand as dev, debugCommand as debug, browseCommand as browse, } from './commands/index.js';
21
+ import { buildCommand as build, clearCommand as clear, copyCommand as copy, envCommand as env, execCommand as exec, 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, shipCommand as ship, dlxCommand as dlx, installCommand as install, linkCommand as link, unlinkCommand as unlink, foreachCommand as foreach, workspaceCommand as workspace, packCommand as pack, publishCommand as publish, whoamiCommand as whoami, loginCommand as login, logoutCommand as logout, trustCommand as trust, onboardCommand as onboard, selfUpdateCommand as selfUpdate, generateInstallerCommand as generateInstaller, pruneCommand as prune, uninstallCommand as uninstall, formatCommand as format, lintCommand as lint, fixCommand as fix, upgradeCommand as upgrade, barrelsCommand as barrels, tscCommand as tsc, affectedCommand as affected, storybookCommand as storybook, devCommand as dev, debugCommand as debug, browseCommand as browse, webextCommand as webext, } from './commands/index.js';
22
22
  import { APP_NAME } from './constants.js';
23
23
  import { isBun, isDeno, isNode, gjsSystemVersion } from '@gjsify/rolldown-plugin-gjsify/runtime';
24
24
  import { ensureGjsifyShimOnPath } from './utils/gjsify-shim.js';
@@ -98,6 +98,8 @@ export async function runCli(argv) {
98
98
  .command(build.command, build.description, build.builder, build.handler)
99
99
  .command(clear.command, clear.description, clear.builder, clear.handler)
100
100
  .command(copy.command, copy.description, copy.builder, copy.handler)
101
+ .command(env.command, env.description, env.builder, env.handler)
102
+ .command(exec.command, exec.description, exec.builder, exec.handler)
101
103
  .command(test.command, test.description, test.builder, test.handler)
102
104
  .command(run.command, run.description, run.builder, run.handler)
103
105
  .command(dlx.command, dlx.description, dlx.builder, dlx.handler)
@@ -134,6 +136,7 @@ export async function runCli(argv) {
134
136
  .command(dev.command, dev.description, dev.builder, dev.handler)
135
137
  .command(debug.command, debug.description, debug.builder, debug.handler)
136
138
  .command(browse.command, browse.description, browse.builder, browse.handler)
139
+ .command(webext.command, webext.description, webext.builder, webext.handler)
137
140
  .demandCommand(1)
138
141
  .epilogue(`Running on ${runtimeLabel()}`)
139
142
  .help()
@@ -0,0 +1,31 @@
1
+ import type { Command } from '../types/index.js';
2
+ interface EnvOptions {
3
+ entries: string[];
4
+ }
5
+ export interface EnvInvocation {
6
+ assignments: Array<[name: string, value: string]>;
7
+ command: string;
8
+ args: string[];
9
+ }
10
+ /**
11
+ * Split env(1)-style argv: leading `NAME=VALUE` words, then the command and its
12
+ * arguments. A later `X=y` is an ARGUMENT, as in env(1). `null` when no command
13
+ * follows the assignments.
14
+ */
15
+ export declare function splitEnvArgv(argv: readonly string[]): EnvInvocation | null;
16
+ /**
17
+ * Rejoin the argv yargs split at the first `--`. One AFTER the command belongs to
18
+ * the command, as in env(1) (`gjsify env A=1 node x.mjs -- --flag` hands node the
19
+ * `--`); only one BEFORE it separates (`gjsify env A=1 -- node …`).
20
+ */
21
+ export declare function joinEnvArgv(entries: readonly string[], tail: readonly string[]): string[];
22
+ /**
23
+ * The child's environment: `base` with `assignments` applied.
24
+ *
25
+ * On win32 names are case-insensitive, and a spread of `process.env` keeps the
26
+ * stored spelling (`Path`). Assigning `PATH` beside it would hand the child two
27
+ * variables for one name, so any differently-cased key is dropped first.
28
+ */
29
+ export declare function applyAssignments(base: NodeJS.ProcessEnv, assignments: ReadonlyArray<readonly [string, string]>, platform?: string): NodeJS.ProcessEnv;
30
+ export declare const envCommand: Command<unknown, EnvOptions>;
31
+ export {};
@@ -0,0 +1,114 @@
1
+ // `gjsify env NAME=VALUE… <command> [args…]` — env(1) for package scripts.
2
+ //
3
+ // npm runs package scripts through `cmd.exe` on Windows, and cmd has no form of
4
+ // the POSIX `NAME=value command` prefix: `LC_ALL=C node --test` fails there with
5
+ // "'LC_ALL' is not recognized as an internal or external command" (measured on
6
+ // the win11-gjsify VM for `@gjsify/node-gi`'s `npm test`). The prefix cannot be
7
+ // rewritten into a cmd spelling either, because `set NAME=value && …` leaks into
8
+ // every later clause and keeps the trailing space in the value.
9
+ //
10
+ // Same answer as `gjsify clear`/`gjsify copy` gave `rm -rf`/`cp -r`: one portable
11
+ // command, so the fix is prepending `gjsify env` rather than a platform branch per
12
+ // script. `portable-scripts` rejects the bare prefix so it stays fixed.
13
+ //
14
+ // The child is spawned through `spawnToCompletion`, so a bare `.cmd` shim (`npm`,
15
+ // `tsc`) resolves on Windows the same way it does for every other CLI spawn.
16
+ import { doubleDashArgs } from '../utils/double-dash-args.js';
17
+ import { describeExit, spawnToCompletion } from '../utils/spawn.js';
18
+ /** POSIX `name=value` assignment word: a portable identifier, then `=`. */
19
+ const ASSIGNMENT_RE = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/s;
20
+ /**
21
+ * Split env(1)-style argv: leading `NAME=VALUE` words, then the command and its
22
+ * arguments. A later `X=y` is an ARGUMENT, as in env(1). `null` when no command
23
+ * follows the assignments.
24
+ */
25
+ export function splitEnvArgv(argv) {
26
+ const assignments = [];
27
+ let i = 0;
28
+ for (; i < argv.length; i++) {
29
+ const m = ASSIGNMENT_RE.exec(argv[i]);
30
+ if (!m)
31
+ break;
32
+ assignments.push([m[1], m[2]]);
33
+ }
34
+ if (i >= argv.length)
35
+ return null;
36
+ return { assignments, command: argv[i], args: argv.slice(i + 1) };
37
+ }
38
+ /**
39
+ * Rejoin the argv yargs split at the first `--`. One AFTER the command belongs to
40
+ * the command, as in env(1) (`gjsify env A=1 node x.mjs -- --flag` hands node the
41
+ * `--`); only one BEFORE it separates (`gjsify env A=1 -- node …`).
42
+ */
43
+ export function joinEnvArgv(entries, tail) {
44
+ if (tail.length === 0)
45
+ return [...entries];
46
+ return splitEnvArgv(entries) ? [...entries, '--', ...tail] : [...entries, ...tail];
47
+ }
48
+ /**
49
+ * The child's environment: `base` with `assignments` applied.
50
+ *
51
+ * On win32 names are case-insensitive, and a spread of `process.env` keeps the
52
+ * stored spelling (`Path`). Assigning `PATH` beside it would hand the child two
53
+ * variables for one name, so any differently-cased key is dropped first.
54
+ */
55
+ export function applyAssignments(base, assignments, platform = process.platform) {
56
+ const env = { ...base };
57
+ for (const [name, value] of assignments) {
58
+ if (platform === 'win32') {
59
+ for (const key of Object.keys(env)) {
60
+ if (key !== name && key.toLowerCase() === name.toLowerCase())
61
+ delete env[key];
62
+ }
63
+ }
64
+ env[name] = value;
65
+ }
66
+ return env;
67
+ }
68
+ export const envCommand = {
69
+ command: 'env <entries..>',
70
+ description: 'Run a command with extra environment variables: `gjsify env NAME=VALUE… <command> [args…]`. Portable replacement for the POSIX `NAME=value command` prefix, which cmd.exe cannot run.',
71
+ builder: (yargs) => yargs
72
+ .positional('entries', {
73
+ description: '`NAME=VALUE` assignments, then the command and its arguments.',
74
+ type: 'string',
75
+ array: true,
76
+ })
77
+ // The command owns its whole flag namespace, as with `gjsify run`:
78
+ // `gjsify env LC_ALL=C node --test --help` must reach node.
79
+ .version(false)
80
+ .help(false)
81
+ .parserConfiguration({
82
+ 'populate--': true,
83
+ 'parse-positional-numbers': false,
84
+ 'unknown-options-as-args': true,
85
+ }),
86
+ handler: async (args) => {
87
+ const invocation = splitEnvArgv(joinEnvArgv(args.entries ?? [], doubleDashArgs(args)));
88
+ if (!invocation) {
89
+ console.error('gjsify env: no command given. Usage: gjsify env NAME=VALUE… <command> [args…]');
90
+ return process.exit(1);
91
+ }
92
+ const { assignments, command, args: childArgs } = invocation;
93
+ let result;
94
+ try {
95
+ result = await spawnToCompletion(command, childArgs, {
96
+ completion: 'exit',
97
+ env: applyAssignments(process.env, assignments),
98
+ });
99
+ }
100
+ catch (err) {
101
+ const e = err;
102
+ console.error(e.code === 'ENOENT'
103
+ ? `gjsify env: \`${command}\` not found on PATH`
104
+ : `gjsify env: ${command}: ${e.message}`);
105
+ return process.exit(127);
106
+ }
107
+ if (result.code !== 0) {
108
+ if (result.code === null)
109
+ console.error(`gjsify env: ${command} ended by ${describeExit(result)}`);
110
+ return process.exit(result.code ?? 1);
111
+ }
112
+ return process.exit(0);
113
+ },
114
+ };
@@ -0,0 +1,10 @@
1
+ import type { Command } from '../types/index.js';
2
+ import { type ResolvedBin } from '../utils/exec-bin.js';
3
+ interface ExecOptions {
4
+ entries?: string[];
5
+ }
6
+ export declare class ExecRebuildError extends Error {
7
+ constructor(bin: ResolvedBin, entry: string, cause: unknown, verbose: boolean);
8
+ }
9
+ export declare const execCommand: Command<unknown, ExecOptions>;
10
+ export {};