@gjsify/cli 0.51.1 → 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 (126) hide show
  1. package/dist/cli.gjs.mjs +539 -493
  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 +7 -2
  7. package/lib/commands/affected-classify.d.ts +7 -1
  8. package/lib/commands/affected-classify.js +66 -9
  9. package/lib/commands/affected-script-refs.d.ts +20 -0
  10. package/lib/commands/affected-script-refs.js +218 -0
  11. package/lib/commands/affected.js +24 -2
  12. package/lib/commands/env.d.ts +31 -0
  13. package/lib/commands/env.js +114 -0
  14. package/lib/commands/exec.d.ts +10 -0
  15. package/lib/commands/exec.js +208 -0
  16. package/lib/commands/flatpak/diff.js +8 -2
  17. package/lib/commands/flatpak/init.js +4 -1
  18. package/lib/commands/flatpak/sources.js +4 -3
  19. package/lib/commands/flatpak/sync-flathub.d.ts +2 -0
  20. package/lib/commands/flatpak/sync-flathub.js +149 -7
  21. package/lib/commands/flatpak/utils.d.ts +10 -0
  22. package/lib/commands/flatpak/utils.js +10 -0
  23. package/lib/commands/foreach.js +5 -97
  24. package/lib/commands/format.js +26 -6
  25. package/lib/commands/index.d.ts +4 -0
  26. package/lib/commands/index.js +4 -0
  27. package/lib/commands/install.d.ts +2 -2
  28. package/lib/commands/install.js +81 -5
  29. package/lib/commands/link.d.ts +13 -0
  30. package/lib/commands/link.js +243 -0
  31. package/lib/commands/onboard.js +18 -2
  32. package/lib/commands/publish.d.ts +34 -0
  33. package/lib/commands/publish.js +153 -8
  34. package/lib/commands/self-update.js +2 -1
  35. package/lib/commands/ship.js +19 -1
  36. package/lib/commands/test.d.ts +18 -9
  37. package/lib/commands/test.js +56 -11
  38. package/lib/commands/tsc.js +4 -4
  39. package/lib/commands/upgrade.d.ts +15 -0
  40. package/lib/commands/upgrade.js +76 -2
  41. package/lib/commands/webext/index.d.ts +2 -0
  42. package/lib/commands/webext/index.js +15 -0
  43. package/lib/commands/webext/subcommands.d.ts +16 -0
  44. package/lib/commands/webext/subcommands.js +204 -0
  45. package/lib/types/config-data.d.ts +2 -2
  46. package/lib/utils/bin-shim.d.ts +8 -2
  47. package/lib/utils/bin-shim.js +6 -5
  48. package/lib/utils/bundle-inputs.d.ts +84 -0
  49. package/lib/utils/bundle-inputs.js +268 -0
  50. package/lib/utils/check-system-deps.d.ts +5 -19
  51. package/lib/utils/check-system-deps.js +31 -94
  52. package/lib/utils/dev-link.d.ts +225 -0
  53. package/lib/utils/dev-link.js +684 -0
  54. package/lib/utils/exec-bin.d.ts +137 -0
  55. package/lib/utils/exec-bin.js +321 -0
  56. package/lib/utils/gi-runtime-paths.js +3 -2
  57. package/lib/utils/gitignore.d.ts +15 -0
  58. package/lib/utils/gitignore.js +178 -0
  59. package/lib/utils/gjsify-shim.d.ts +47 -0
  60. package/lib/utils/gjsify-shim.js +97 -8
  61. package/lib/utils/hang-watchdog.d.ts +95 -0
  62. package/lib/utils/hang-watchdog.js +194 -0
  63. package/lib/utils/install-backend-native.d.ts +33 -1
  64. package/lib/utils/install-backend-native.js +184 -20
  65. package/lib/utils/install-extraneous.d.ts +90 -0
  66. package/lib/utils/install-extraneous.js +120 -0
  67. package/lib/utils/install-lock.js +4 -25
  68. package/lib/utils/install-tarball-cache.d.ts +37 -9
  69. package/lib/utils/install-tarball-cache.js +59 -14
  70. package/lib/utils/native-skip-scan.d.ts +20 -0
  71. package/lib/utils/native-skip-scan.js +343 -0
  72. package/lib/utils/node-script.js +1 -1
  73. package/lib/utils/normalize-bundler-options.js +11 -0
  74. package/lib/utils/oxc-resolve.js +42 -3
  75. package/lib/utils/process-table.d.ts +22 -0
  76. package/lib/utils/process-table.js +188 -0
  77. package/lib/utils/prune-prefix.js +1 -1
  78. package/lib/utils/publish-pins.d.ts +38 -0
  79. package/lib/utils/publish-pins.js +47 -0
  80. package/lib/utils/publish-readback.d.ts +103 -17
  81. package/lib/utils/publish-readback.js +326 -47
  82. package/lib/utils/run-gjs.d.ts +15 -1
  83. package/lib/utils/run-gjs.js +98 -13
  84. package/lib/utils/run-node.d.ts +2 -0
  85. package/lib/utils/run-node.js +2 -1
  86. package/lib/utils/runtimes.js +14 -13
  87. package/lib/utils/ship/gzip.js +1 -1
  88. package/lib/utils/ship/icns.d.ts +14 -0
  89. package/lib/utils/ship/icns.js +83 -0
  90. package/lib/utils/ship/ico.d.ts +25 -0
  91. package/lib/utils/ship/ico.js +120 -0
  92. package/lib/utils/ship/icons.d.ts +197 -0
  93. package/lib/utils/ship/icons.js +399 -0
  94. package/lib/utils/ship/launcher.js +16 -1
  95. package/lib/utils/ship/layout.d.ts +32 -0
  96. package/lib/utils/ship/layout.js +112 -38
  97. package/lib/utils/ship/msi.d.ts +1 -1
  98. package/lib/utils/ship/msi.js +30 -5
  99. package/lib/utils/ship/pe-launcher.d.ts +18 -0
  100. package/lib/utils/ship/pe-launcher.js +125 -4
  101. package/lib/utils/ship/plan.d.ts +1 -34
  102. package/lib/utils/ship/plan.js +7 -56
  103. package/lib/utils/ship/plist.d.ts +20 -10
  104. package/lib/utils/ship/plist.js +29 -12
  105. package/lib/utils/ship/settings.js +3 -2
  106. package/lib/utils/spawn.d.ts +13 -0
  107. package/lib/utils/spawn.js +10 -5
  108. package/lib/utils/watch-loop.d.ts +15 -1
  109. package/lib/utils/watch-loop.js +9 -5
  110. package/lib/utils/webext/build.d.ts +42 -0
  111. package/lib/utils/webext/build.js +120 -0
  112. package/lib/utils/webext/config.d.ts +72 -0
  113. package/lib/utils/webext/config.js +234 -0
  114. package/lib/utils/webext/icons.d.ts +24 -0
  115. package/lib/utils/webext/icons.js +83 -0
  116. package/lib/utils/webext/launch.d.ts +32 -0
  117. package/lib/utils/webext/launch.js +77 -0
  118. package/lib/utils/webext/manifest.d.ts +53 -0
  119. package/lib/utils/webext/manifest.js +219 -0
  120. package/lib/utils/webext/pages.d.ts +26 -0
  121. package/lib/utils/webext/pages.js +61 -0
  122. package/lib/utils/webext/targets.d.ts +13 -0
  123. package/lib/utils/webext/targets.js +36 -0
  124. package/lib/utils/win32-command.d.ts +17 -0
  125. package/lib/utils/win32-command.js +21 -6
  126. package/package.json +20 -20
@@ -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, 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';
@@ -93,9 +93,13 @@ export async function runCli(argv) {
93
93
  .wrap(cli.terminalWidth())
94
94
  .command(create.command, create.description, create.builder, create.handler)
95
95
  .command(install.command, install.description, install.builder, install.handler)
96
+ .command(link.command, link.description, link.builder, link.handler)
97
+ .command(unlink.command, unlink.description, unlink.builder, unlink.handler)
96
98
  .command(build.command, build.description, build.builder, build.handler)
97
99
  .command(clear.command, clear.description, clear.builder, clear.handler)
98
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)
99
103
  .command(test.command, test.description, test.builder, test.handler)
100
104
  .command(run.command, run.description, run.builder, run.handler)
101
105
  .command(dlx.command, dlx.description, dlx.builder, dlx.handler)
@@ -132,6 +136,7 @@ export async function runCli(argv) {
132
136
  .command(dev.command, dev.description, dev.builder, dev.handler)
133
137
  .command(debug.command, debug.description, debug.builder, debug.handler)
134
138
  .command(browse.command, browse.description, browse.builder, browse.handler)
139
+ .command(webext.command, webext.description, webext.builder, webext.handler)
135
140
  .demandCommand(1)
136
141
  .epilogue(`Running on ${runtimeLabel()}`)
137
142
  .help()
@@ -1,4 +1,5 @@
1
1
  import { type Workspace } from '@gjsify/workspace';
2
+ import type { ScriptReferrers } from './affected-script-refs.js';
2
3
  export interface ClassifyResult {
3
4
  /** Cannot decide precisely — caller should run full suite. */
4
5
  global: boolean;
@@ -13,4 +14,9 @@ export interface ClassifyResult {
13
14
  /** Diff was empty AND no triggers fired → CI can skip the whole job. */
14
15
  skipAll: boolean;
15
16
  }
16
- export declare function classifyAndExpand(workspaces: readonly Workspace[], changedFiles: readonly string[]): ClassifyResult;
17
+ /**
18
+ * @param readersOf who reads a root `scripts/*` file. Omitted, a changed root script stays
19
+ * `unmatched` and forces the full run — the behaviour before the lookup existed, and what
20
+ * the command falls back to when it cannot list the tree.
21
+ */
22
+ export declare function classifyAndExpand(workspaces: readonly Workspace[], changedFiles: readonly string[], readersOf?: ScriptReferrers): ClassifyResult;
@@ -49,8 +49,10 @@ const GLOBAL_TRIGGERS = [
49
49
  */
50
50
  const OTHER_WORKFLOW_INPUTS = [
51
51
  // Each sibling workflow's own definition. `main.yml` is deliberately absent: it
52
- // is a GLOBAL_TRIGGER instead.
53
- /^\.github\/workflows\/(deploy-docs|commitlint|release|release-cut|audit-runtimes|prebuilds|node-gi|napi|cli-cross-platform|build-ci-image|cancel-pr-runs)\.yml$/,
52
+ // is a GLOBAL_TRIGGER instead. The three OS-suite workflows run unfiltered on every
53
+ // PR, so an edit to one is measured by that run; `webview2-probe.yml` is absent on
54
+ // purpose, being dispatch-only — nothing would measure an edit to it on the PR.
55
+ /^\.github\/workflows\/(deploy-docs|commitlint|release|release-cut|audit-runtimes|prebuilds|node-gi|napi|cli-cross-platform|build-ci-image|cancel-pr-runs|macos-suites|windows-suites|gtk-os-suites)\.yml$/,
54
56
  // `prebuilds.yml`'s toolchain (#838): the QEMU-emulated build script and the
55
57
  // classifier deciding which native packages a prebuild run rebuilds. It is in
56
58
  // both of that workflow's `paths:` filters; `main.yml` builds no prebuild and
@@ -168,7 +170,51 @@ const SCRIPT_COUPLINGS = [
168
170
  why: 'tests/e2e/react-native-devtools is the only external observer of AppRegistry.runApplication',
169
171
  },
170
172
  ];
171
- export function classifyAndExpand(workspaces, changedFiles) {
173
+ /** A root script whose readers can stand in for it (see `affected-script-refs.ts`). */
174
+ const ROOT_SCRIPT = /^scripts\/[^/]+$/;
175
+ function isResolvableScript(f) {
176
+ return (ROOT_SCRIPT.test(f) &&
177
+ !GLOBAL_TRIGGERS.some((re) => re.test(f)) &&
178
+ !SCRIPT_COUPLINGS.some((c) => c.match.test(f)));
179
+ }
180
+ /**
181
+ * Replace each changed root script by the files that read it, recursively through scripts
182
+ * that read scripts. Readers go back through IGNORE here and through GLOBAL / workspace
183
+ * mapping in the caller, so a reader this classifier does not model still forces the full
184
+ * run, and a script a GLOBAL_TRIGGER reads (`audit-runtimes.mjs`) stays in and fires it.
185
+ */
186
+ function resolveRootScripts(files, readersOf) {
187
+ const out = new Set();
188
+ const queue = [];
189
+ for (const f of files) {
190
+ if (isResolvableScript(f))
191
+ queue.push(f);
192
+ else
193
+ out.add(f);
194
+ }
195
+ const seen = new Set();
196
+ while (queue.length > 0) {
197
+ const s = queue.shift();
198
+ if (seen.has(s))
199
+ continue;
200
+ seen.add(s);
201
+ for (const r of readersOf(s)) {
202
+ if (IGNORE.some((re) => re.test(r)))
203
+ continue;
204
+ if (isResolvableScript(r))
205
+ queue.push(r);
206
+ else
207
+ out.add(r);
208
+ }
209
+ }
210
+ return { files: [...out], scripts: seen.size };
211
+ }
212
+ /**
213
+ * @param readersOf who reads a root `scripts/*` file. Omitted, a changed root script stays
214
+ * `unmatched` and forces the full run — the behaviour before the lookup existed, and what
215
+ * the command falls back to when it cannot list the tree.
216
+ */
217
+ export function classifyAndExpand(workspaces, changedFiles, readersOf) {
172
218
  const files = changedFiles.map((f) => f.replace(/\\/g, '/')).filter((f) => f.length > 0);
173
219
  if (files.length === 0) {
174
220
  return {
@@ -182,16 +228,26 @@ export function classifyAndExpand(workspaces, changedFiles) {
182
228
  }
183
229
  // Ignored files go FIRST, before the global-trigger check: ignore wins over
184
230
  // global, so `packages/infra/cli/README.md` must not force a full run.
185
- const remaining = [];
231
+ let remaining = [];
186
232
  for (const f of files) {
187
233
  if (IGNORE.some((re) => re.test(f)))
188
234
  continue;
189
235
  remaining.push(f);
190
236
  }
237
+ // Every verdict below says when it was reached through a script's readers, so a
238
+ // surprising selection can be traced back to the lookup rather than the diff.
239
+ let note = '';
240
+ if (readersOf) {
241
+ const resolved = resolveRootScripts(remaining, readersOf);
242
+ if (resolved.scripts > 0) {
243
+ remaining = resolved.files;
244
+ note = `; ${resolved.scripts} root script(s) resolved to their readers`;
245
+ }
246
+ }
191
247
  if (remaining.length === 0) {
192
248
  return {
193
249
  global: false,
194
- reason: 'ignored-only',
250
+ reason: 'ignored-only' + note,
195
251
  workspaces: [],
196
252
  runE2E: false,
197
253
  runIntegration: false,
@@ -204,7 +260,7 @@ export function classifyAndExpand(workspaces, changedFiles) {
204
260
  if (re.test(f)) {
205
261
  return {
206
262
  global: true,
207
- reason: `global-trigger ${re.source} matched ${f}`,
263
+ reason: `global-trigger ${re.source} matched ${f}${note}`,
208
264
  workspaces: workspaces.map((w) => w.name),
209
265
  runE2E: true,
210
266
  runIntegration: true,
@@ -254,7 +310,7 @@ export function classifyAndExpand(workspaces, changedFiles) {
254
310
  if (stillUnmatched.length > 0) {
255
311
  return {
256
312
  global: true,
257
- reason: `unmatched files (${stillUnmatched.length}): ${stillUnmatched.slice(0, 3).join(', ')}${stillUnmatched.length > 3 ? '…' : ''}`,
313
+ reason: `unmatched files (${stillUnmatched.length}): ${stillUnmatched.slice(0, 3).join(', ')}${stillUnmatched.length > 3 ? '…' : ''}${note}`,
258
314
  workspaces: workspaces.map((w) => w.name),
259
315
  runE2E: true,
260
316
  runIntegration: true,
@@ -274,7 +330,7 @@ export function classifyAndExpand(workspaces, changedFiles) {
274
330
  const touchedIntegration = remaining.some((f) => f.startsWith('tests/integration/'));
275
331
  return {
276
332
  global: false,
277
- reason: `test-only (${remaining.length} file(s) in ${only})`,
333
+ reason: `test-only (${remaining.length} file(s) in ${only})${note}`,
278
334
  workspaces: [only],
279
335
  // `|| couplingRunE2E`: this shortcut skips CLOSURE EXPANSION, and a
280
336
  // declared tier is not part of the closure. Dropping it here made a
@@ -323,7 +379,8 @@ export function classifyAndExpand(workspaces, changedFiles) {
323
379
  return {
324
380
  global: false,
325
381
  reason: `closure (${closure.size} ws from ${matched.size} seed(s))` +
326
- (couplingSeeds.size > 0 ? `, ${couplingSeeds.size} via script-coupling` : ''),
382
+ (couplingSeeds.size > 0 ? `, ${couplingSeeds.size} via script-coupling` : '') +
383
+ note,
327
384
  workspaces: [...closure].sort(),
328
385
  runE2E,
329
386
  runIntegration,
@@ -0,0 +1,20 @@
1
+ /** Every file that names `scriptPath`, as repo-relative paths. */
2
+ export type ScriptReferrers = (scriptPath: string) => readonly string[];
3
+ /**
4
+ * The file-name pattern for a script: `adwaita-elements.mjs` is also read as
5
+ * `adwaita-elements.d.mts` (a TS import of the `.mjs` resolves to its declaration) and a
6
+ * `.d.mts` change is read through its `.mjs`. The leading boundary excludes `-` so
7
+ * `value-types.mjs` does not match `generate-value-types.mjs`.
8
+ */
9
+ export declare function scriptNamePattern(scriptPath: string): RegExp;
10
+ /**
11
+ * Build the reader lookup over `texts` (repo-relative path → raw content). Pure: the command
12
+ * feeds it `git ls-files`, the spec feeds it a fixture map.
13
+ */
14
+ export declare function buildScriptReferrers(texts: ReadonlyMap<string, string>): ScriptReferrers;
15
+ /**
16
+ * Every tracked text file under `rootDir`, for {@link buildScriptReferrers}. Throws when git
17
+ * cannot list the tree — the caller then classifies without the lookup, which is the old,
18
+ * conservative behaviour (a root script lands in `unmatched`).
19
+ */
20
+ export declare function readTrackedTexts(rootDir: string): Map<string, string>;