@gjsify/cli 0.52.0 → 0.54.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 (107) hide show
  1. package/dist/cli.gjs.mjs +351 -305
  2. package/lib/actions/build.d.ts +22 -0
  3. package/lib/actions/build.js +37 -9
  4. package/lib/bundler-pick.d.ts +72 -1
  5. package/lib/bundler-pick.js +175 -17
  6. package/lib/cli-app.js +7 -2
  7. package/lib/commands/blueprint/index.d.ts +4 -0
  8. package/lib/commands/blueprint/index.js +15 -0
  9. package/lib/commands/blueprint/types.d.ts +8 -0
  10. package/lib/commands/blueprint/types.js +124 -0
  11. package/lib/commands/devtools.d.ts +21 -0
  12. package/lib/commands/devtools.js +146 -0
  13. package/lib/commands/env.d.ts +31 -0
  14. package/lib/commands/env.js +114 -0
  15. package/lib/commands/exec.d.ts +10 -0
  16. package/lib/commands/exec.js +208 -0
  17. package/lib/commands/foreach.js +5 -97
  18. package/lib/commands/format.js +26 -6
  19. package/lib/commands/index.d.ts +5 -0
  20. package/lib/commands/index.js +5 -0
  21. package/lib/commands/install.d.ts +2 -2
  22. package/lib/commands/install.js +5 -5
  23. package/lib/commands/self-update.js +2 -1
  24. package/lib/commands/ship.js +51 -8
  25. package/lib/commands/test.d.ts +18 -9
  26. package/lib/commands/test.js +56 -11
  27. package/lib/commands/tsc.js +4 -4
  28. package/lib/commands/upgrade.d.ts +15 -0
  29. package/lib/commands/upgrade.js +76 -2
  30. package/lib/commands/webext/index.d.ts +2 -0
  31. package/lib/commands/webext/index.js +15 -0
  32. package/lib/commands/webext/subcommands.d.ts +16 -0
  33. package/lib/commands/webext/subcommands.js +204 -0
  34. package/lib/types/config-data.d.ts +1 -1
  35. package/lib/utils/bin-shim.d.ts +8 -2
  36. package/lib/utils/bin-shim.js +6 -5
  37. package/lib/utils/bundle-inputs.d.ts +84 -0
  38. package/lib/utils/bundle-inputs.js +268 -0
  39. package/lib/utils/check-system-deps.d.ts +4 -2
  40. package/lib/utils/check-system-deps.js +21 -26
  41. package/lib/utils/dev-link.d.ts +9 -6
  42. package/lib/utils/dev-link.js +71 -19
  43. package/lib/utils/exec-bin.d.ts +137 -0
  44. package/lib/utils/exec-bin.js +321 -0
  45. package/lib/utils/gi-runtime-paths.js +3 -2
  46. package/lib/utils/gitignore.d.ts +15 -0
  47. package/lib/utils/gitignore.js +178 -0
  48. package/lib/utils/gjsify-shim.d.ts +47 -0
  49. package/lib/utils/gjsify-shim.js +97 -8
  50. package/lib/utils/install-backend-native.d.ts +19 -1
  51. package/lib/utils/install-backend-native.js +93 -7
  52. package/lib/utils/install-lock.js +4 -25
  53. package/lib/utils/native-skip-scan.d.ts +20 -0
  54. package/lib/utils/native-skip-scan.js +343 -0
  55. package/lib/utils/node-script.js +1 -1
  56. package/lib/utils/oxc-resolve.js +46 -5
  57. package/lib/utils/process-table.d.ts +22 -0
  58. package/lib/utils/process-table.js +188 -0
  59. package/lib/utils/prune-prefix.js +1 -1
  60. package/lib/utils/run-gjs.d.ts +15 -1
  61. package/lib/utils/run-gjs.js +16 -7
  62. package/lib/utils/run-node.d.ts +2 -0
  63. package/lib/utils/run-node.js +2 -1
  64. package/lib/utils/runtimes.js +14 -13
  65. package/lib/utils/ship/app-runtime.d.ts +7 -5
  66. package/lib/utils/ship/app-runtime.js +11 -8
  67. package/lib/utils/ship/appimage.d.ts +9 -0
  68. package/lib/utils/ship/appimage.js +2 -1
  69. package/lib/utils/ship/deb.d.ts +10 -0
  70. package/lib/utils/ship/deb.js +7 -0
  71. package/lib/utils/ship/depends.d.ts +37 -3
  72. package/lib/utils/ship/depends.js +21 -9
  73. package/lib/utils/ship/gi-namespaces.d.ts +16 -0
  74. package/lib/utils/ship/gi-namespaces.js +64 -1
  75. package/lib/utils/ship/gzip.js +1 -1
  76. package/lib/utils/ship/icons.d.ts +6 -0
  77. package/lib/utils/ship/icons.js +6 -5
  78. package/lib/utils/ship/launcher.js +2 -2
  79. package/lib/utils/ship/layout.js +25 -2
  80. package/lib/utils/ship/rpm.d.ts +5 -0
  81. package/lib/utils/ship/rpm.js +42 -0
  82. package/lib/utils/ship/settings.js +6 -0
  83. package/lib/utils/ship/signing.d.ts +7 -2
  84. package/lib/utils/ship/signing.js +59 -32
  85. package/lib/utils/ship/stage-manifest.d.ts +14 -0
  86. package/lib/utils/ship/stage-manifest.js +7 -0
  87. package/lib/utils/spawn.d.ts +13 -0
  88. package/lib/utils/spawn.js +10 -5
  89. package/lib/utils/watch-loop.d.ts +15 -1
  90. package/lib/utils/watch-loop.js +9 -5
  91. package/lib/utils/webext/build.d.ts +42 -0
  92. package/lib/utils/webext/build.js +120 -0
  93. package/lib/utils/webext/config.d.ts +72 -0
  94. package/lib/utils/webext/config.js +234 -0
  95. package/lib/utils/webext/icons.d.ts +24 -0
  96. package/lib/utils/webext/icons.js +83 -0
  97. package/lib/utils/webext/launch.d.ts +32 -0
  98. package/lib/utils/webext/launch.js +77 -0
  99. package/lib/utils/webext/manifest.d.ts +53 -0
  100. package/lib/utils/webext/manifest.js +219 -0
  101. package/lib/utils/webext/pages.d.ts +26 -0
  102. package/lib/utils/webext/pages.js +61 -0
  103. package/lib/utils/webext/targets.d.ts +13 -0
  104. package/lib/utils/webext/targets.js +48 -0
  105. package/lib/utils/win32-command.d.ts +17 -0
  106. package/lib/utils/win32-command.js +21 -6
  107. package/package.json +20 -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) {
@@ -1,5 +1,6 @@
1
1
  import type { RolldownOutput, InputOptions, RolldownWatcher } from 'rolldown';
2
2
  import type { BundlerOptions } from './types/index.js';
3
+ import { type ResolveNpmPackageOptions } from './utils/resolve-npm-package.js';
3
4
  export interface NativePluginContext {
4
5
  resolve(specifier: string, importer?: string, opts?: {
5
6
  skipSelf?: boolean;
@@ -8,6 +9,20 @@ export interface NativePluginContext {
8
9
  id: string;
9
10
  external: boolean;
10
11
  } | null>;
12
+ /**
13
+ * rolldown's `this.addWatchFile`, collected by the native facade into
14
+ * `BundleResult.watchedFiles` — the native half of what npm rolldown reports
15
+ * as `RolldownBuild.watchFiles`.
16
+ *
17
+ * OPTIONAL here on purpose. The engine is resolved through several anchors
18
+ * and a global-prefix copy can answer first (measured 2026-09-29 on a dev
19
+ * host whose project-local engine was the current one), so a plugin cannot
20
+ * assume the method exists: it is this bridge's documented stance that an
21
+ * unimplemented context method FAILS at hook-call time, and a build must
22
+ * not fail over bookkeeping. `gjsify-css-as-string` therefore
23
+ * feature-detects and warns once.
24
+ */
25
+ addWatchFile(id: string): void;
11
26
  warn(message: string): void;
12
27
  error(message: string): never;
13
28
  }
@@ -72,11 +87,67 @@ export declare function bundleToChunks(input: {
72
87
  * and is responsible for invoking `watcher.close()` on shutdown.
73
88
  */
74
89
  export declare function runWatch(finalOpts: BundlerOptions): Promise<RolldownWatcher>;
90
+ /** What one build reported about the files its output depends on. */
91
+ export interface BuildWatchList {
92
+ /**
93
+ * The engine's watch list: npm rolldown's `RolldownBuild.watchFiles` (its
94
+ * module set plus every `this.addWatchFile`), the native facade's
95
+ * `watchedFiles` (the declared half — the native engine reports no modules,
96
+ * hence the two halves rather than either). Read BEFORE `close()`, which is
97
+ * where the npm getter is still alive.
98
+ */
99
+ files: readonly string[];
100
+ /**
101
+ * `false` when the engine CANNOT report one at all (a `@gjsify/rolldown-native`
102
+ * older than `addWatchFile`). A caller that judges a build's inputs on this
103
+ * list has to refuse to trust it — the file a plugin read itself is then
104
+ * invisible, and a stale bundle would read as current.
105
+ */
106
+ reportedByEngine: boolean;
107
+ }
108
+ /** Where a build's own input list goes. Absent → the caller does not want it. */
109
+ export interface RunBundleOptions {
110
+ onWatchFiles?: (watch: BuildWatchList) => void;
111
+ }
75
112
  /**
76
113
  * Run a bundle with the picked engine. Drop-in replacement for the
77
114
  * `rolldown(opts).write(opts.output)` flow used directly in build.ts.
78
115
  */
79
- export declare function runBundle(finalOpts: BundlerOptions): Promise<RolldownOutput>;
116
+ export declare function runBundle(finalOpts: BundlerOptions, options?: RunBundleOptions): Promise<RolldownOutput>;
117
+ /**
118
+ * The file `@gjsify/utils/native-library` resolves to, or THROWS when no anchor
119
+ * reaches it.
120
+ *
121
+ * `resolveNpmPackage` first, the same resolver — and the same `?? createRequire`
122
+ * tail — the ENGINE loader below uses, for the same reason: ONE
123
+ * `createRequire(import.meta.url)` anchor walks exactly one node_modules chain,
124
+ * the one rooted at the BUNDLE. v0.53.0's release `publish-napi` job runs the
125
+ * published bundle from `~/.cache/gjsify/bootstrap/`, which is in no workspace's
126
+ * chain, so the bundle-only anchor found nothing — and since #1901 moved
127
+ * `probeNativeLibrary()` onto `tryLoadNative()`'s SUCCESS path, that is not a
128
+ * degraded diagnostic but NO ENGINE: every `gjsify build` under GJS died with
129
+ * "no usable bundler engine", reported by a diagnostic that resolves the same
130
+ * specifier and so could not name the cause. MEASURED on the release job's own
131
+ * shape: with `@gjsify/utils` FULLY BUILT, anchoring only at the bundle still
132
+ * fails with `Cannot find module "@gjsify/utils/native-library" - not found in
133
+ * any node_modules directory`; anchoring at cwd resolves it. `main.yml` never
134
+ * saw this because it exports `XDG_CACHE_HOME=$PWD/.gjsify-cache` and therefore
135
+ * caches the bundle INSIDE the workspace, where the bundle-only anchor happens
136
+ * to work.
137
+ *
138
+ * Two independent defects, so both are fixed where they live: this anchor, and
139
+ * the `lib/esm` emit it then needs on a cold tree (`@gjsify/utils build:esm`
140
+ * before the first bundler clause, rule 5 of
141
+ * `scripts/check-build-infra-order.mjs`).
142
+ *
143
+ * SCOPE, since it decides what this unblocks: the release job runs `build:infra`
144
+ * with the PUBLISHED bundle, which is fixed code this change cannot reach — so on
145
+ * its own it does NOT unblock v0.53.0. What does is the `XDG_CACHE_HOME` line in
146
+ * release.yml's `publish-napi` job, which puts that bundle inside the workspace
147
+ * where ANY anchor finds this tree. This fix is what stops the next release from
148
+ * depending on that line.
149
+ */
150
+ export declare function resolveNativeLibraryModule(opts?: ResolveNpmPackageOptions): string;
80
151
  export declare function shouldUseNative(): Promise<boolean>;
81
152
  /**
82
153
  * 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,112 @@ 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
+ /**
322
+ * The file `@gjsify/utils/native-library` resolves to, or THROWS when no anchor
323
+ * reaches it.
324
+ *
325
+ * `resolveNpmPackage` first, the same resolver — and the same `?? createRequire`
326
+ * tail — the ENGINE loader below uses, for the same reason: ONE
327
+ * `createRequire(import.meta.url)` anchor walks exactly one node_modules chain,
328
+ * the one rooted at the BUNDLE. v0.53.0's release `publish-napi` job runs the
329
+ * published bundle from `~/.cache/gjsify/bootstrap/`, which is in no workspace's
330
+ * chain, so the bundle-only anchor found nothing — and since #1901 moved
331
+ * `probeNativeLibrary()` onto `tryLoadNative()`'s SUCCESS path, that is not a
332
+ * degraded diagnostic but NO ENGINE: every `gjsify build` under GJS died with
333
+ * "no usable bundler engine", reported by a diagnostic that resolves the same
334
+ * specifier and so could not name the cause. MEASURED on the release job's own
335
+ * shape: with `@gjsify/utils` FULLY BUILT, anchoring only at the bundle still
336
+ * fails with `Cannot find module "@gjsify/utils/native-library" - not found in
337
+ * any node_modules directory`; anchoring at cwd resolves it. `main.yml` never
338
+ * saw this because it exports `XDG_CACHE_HOME=$PWD/.gjsify-cache` and therefore
339
+ * caches the bundle INSIDE the workspace, where the bundle-only anchor happens
340
+ * to work.
341
+ *
342
+ * Two independent defects, so both are fixed where they live: this anchor, and
343
+ * the `lib/esm` emit it then needs on a cold tree (`@gjsify/utils build:esm`
344
+ * before the first bundler clause, rule 5 of
345
+ * `scripts/check-build-infra-order.mjs`).
346
+ *
347
+ * SCOPE, since it decides what this unblocks: the release job runs `build:infra`
348
+ * with the PUBLISHED bundle, which is fixed code this change cannot reach — so on
349
+ * its own it does NOT unblock v0.53.0. What does is the `XDG_CACHE_HOME` line in
350
+ * release.yml's `publish-napi` job, which puts that bundle inside the workspace
351
+ * where ANY anchor finds this tree. This fix is what stops the next release from
352
+ * depending on that line.
353
+ */
354
+ export function resolveNativeLibraryModule(opts = {}) {
355
+ const specifier = '@gjsify/utils/native-library';
356
+ return resolveNpmPackage(specifier, opts) ?? createRequire(opts.bundleUrl ?? import.meta.url).resolve(specifier);
357
+ }
358
+ async function utilsCore() {
359
+ if (_utilsCore === null) {
360
+ // `./native-library`, not `./core`: `core` re-exports `main-loop`, which
361
+ // holds a module-level singleton. The GJS bundle inlines utils already,
362
+ // so loading `core` off disk would give the process a SECOND copy of that
363
+ // singleton — inert today because only pure probe functions are read
364
+ // through this edge, and a trap the first time that stops being true.
365
+ //
366
+ // Off disk, so the resolution's ANCHORS matter — see
367
+ // `resolveNativeLibraryModule()` — and on a cold tree the WORKSPACE copy
368
+ // it finds is itself a build output, which is what
369
+ // `@gjsify/utils build:esm` orders (rule 5 of
370
+ // `scripts/check-build-infra-order.mjs`). Two independent preconditions,
371
+ // and v0.53.0's release `publish-napi` job needed both: a bundle-only
372
+ // anchor resolved nothing when the bundle was the published
373
+ // `cli.gjs.mjs` — one loose file in `<cache>/gjsify/bootstrap/`, no
374
+ // `node_modules` above it. So a throw here is "not measured" (see
375
+ // `tryLoadNative`), never "no engine".
376
+ const href = pathToFileURL(resolveNativeLibraryModule({ bundleUrl: import.meta.url })).href;
377
+ _utilsCore = (await import(/* @vite-ignore */ href));
378
+ }
379
+ return _utilsCore;
380
+ }
276
381
  export async function shouldUseNative() {
277
382
  const env = globalThis.process?.env ?? {};
278
383
  const choice = env.GJSIFY_BUNDLER;
@@ -282,7 +387,8 @@ export async function shouldUseNative() {
282
387
  if (choice === 'native') {
283
388
  const native = await tryLoadNative();
284
389
  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).');
390
+ throw new Error('GJSIFY_BUNDLER=native but @gjsify/rolldown-native is not loadable (no prebuild for this architecture, or not running under GJS).' +
391
+ (isGjs() ? `\n${diagnoseNativeEngine()}` : ''));
286
392
  }
287
393
  return true;
288
394
  }
@@ -375,7 +481,7 @@ async function tryLoadNative() {
375
481
  // limitations and `findWorkspaceRoot`'s dependency on
376
482
  // `discoverWorkspaces`, both of which can fail to map a
377
483
  // sub-package dir up to the hoisted workspace root.
378
- const bundleDir = path.dirname(new URL(import.meta.url).pathname);
484
+ const bundleDir = path.dirname(fileURLToPath(import.meta.url));
379
485
  const resolvedFromNpm = resolveNpmPackage(specifier, { bundleUrl: import.meta.url });
380
486
  const resolvedFromFs = resolvedFromNpm
381
487
  ? null
@@ -399,6 +505,44 @@ async function tryLoadNative() {
399
505
  const mod = (await import(/* @vite-ignore */ target));
400
506
  if (!mod.hasNativeRolldown())
401
507
  return null;
508
+ // The typelib resolved, which says nothing about its library: that
509
+ // opens at the first class access, and a missing system library
510
+ // (Homebrew json-glib on a Mac) would pass here and fail inside
511
+ // `new BundlerSession()` as the nameless "Unsupported type void" —
512
+ // past `diagnoseNativeEngine()`, which then never runs.
513
+ // Separated from the engine's own failure so that "the engine is
514
+ // absent" and "the measurement could not run" stay distinguishable
515
+ // at the one place that reports them — and only a library the probe
516
+ // MEASURED as broken vetoes the engine. A probe that could not be
517
+ // LOADED does not: the engine's own load already answered twice
518
+ // over (the module imported, `hasNativeRolldown()` resolved its
519
+ // typelib), and under GJS there is no second engine to fall back to
520
+ // — the npm `rolldown` crate is a napi binary — so `null` here
521
+ // turned "nothing was measured" into "there is no bundler".
522
+ //
523
+ // What reaches that verdict is not exotic. `utilsCore()` resolves
524
+ // `@gjsify/utils/native-library` from THIS bundle's own directory,
525
+ // while every other resolution in this function goes through
526
+ // `resolveNpmPackage()` with its cwd/workspace anchors; the bundle
527
+ // that runs a cold `build:infra` is the PUBLISHED `cli.gjs.mjs`,
528
+ // which `install.mjs --fetch-only` caches as one loose file in
529
+ // `<cache>/gjsify/bootstrap/` with no `node_modules` beside it. On
530
+ // the release runner the probe therefore resolved nothing and
531
+ // `@gjsify/napi` did not publish (v0.53.0), and building utils'
532
+ // `lib/esm` first changed nothing — measured, which is why the
533
+ // asymmetry is fixed here and no clause was reordered.
534
+ let surface = null;
535
+ try {
536
+ surface = await utilsCore();
537
+ }
538
+ catch (err) {
539
+ _utilsCoreError = err;
540
+ }
541
+ if (surface !== null) {
542
+ _nativeLibraryFailure = surface.probeNativeLibrary('GjsifyRolldown');
543
+ if (_nativeLibraryFailure)
544
+ return null;
545
+ }
402
546
  return mod;
403
547
  }
404
548
  catch {
@@ -407,7 +551,7 @@ async function tryLoadNative() {
407
551
  })();
408
552
  return _nativeProbe;
409
553
  }
410
- async function runNativeBundle(finalOpts) {
554
+ async function runNativeBundle(finalOpts, options = {}) {
411
555
  const native = await tryLoadNative();
412
556
  if (!native) {
413
557
  throw new Error('@gjsify/rolldown-native not loadable');
@@ -440,6 +584,20 @@ async function runNativeBundle(finalOpts) {
440
584
  }
441
585
  const result = await native.bundleWithPlugins(bundlerOpts, nativePlugins);
442
586
  reportNativeWarnings(result, finalOpts);
587
+ if (options.onWatchFiles) {
588
+ if (result.watchedFiles === undefined) {
589
+ // NOT a silent drop. A version-skewed engine has no `addWatchFile` on
590
+ // its plugin context, so nothing a plugin declares can reach the
591
+ // build's input set — and a caller judging freshness on that set
592
+ // would call a stale bundle current. Say so here, where the engine is
593
+ // in hand, and report `reportedByEngine: false` so the caller refuses.
594
+ warnNoNativeWatchList();
595
+ options.onWatchFiles({ files: [], reportedByEngine: false });
596
+ }
597
+ else {
598
+ options.onWatchFiles({ files: result.watchedFiles, reportedByEngine: true });
599
+ }
600
+ }
443
601
  // The native facade returns the BundleOutput shape but doesn't
444
602
  // write files — replicate `.write()` here so callers see the same
445
603
  // 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, devtoolsCommand as devtools, browseCommand as browse, webextCommand as webext, blueprintCommand as blueprint, } 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)
@@ -133,7 +135,10 @@ export async function runCli(argv) {
133
135
  .command(storybook.command, storybook.description, storybook.builder, storybook.handler)
134
136
  .command(dev.command, dev.description, dev.builder, dev.handler)
135
137
  .command(debug.command, debug.description, debug.builder, debug.handler)
138
+ .command(devtools.command, devtools.description, devtools.builder, devtools.handler)
136
139
  .command(browse.command, browse.description, browse.builder, browse.handler)
140
+ .command(webext.command, webext.description, webext.builder, webext.handler)
141
+ .command(blueprint.command, blueprint.description, blueprint.builder, blueprint.handler)
137
142
  .demandCommand(1)
138
143
  .epilogue(`Running on ${runtimeLabel()}`)
139
144
  .help()
@@ -0,0 +1,4 @@
1
+ import type { Command } from '../../types/index.js';
2
+ import { blueprintTypesCommand } from './types.js';
3
+ export { blueprintTypesCommand };
4
+ export declare const blueprintCommand: Command;
@@ -0,0 +1,15 @@
1
+ // `gjsify blueprint` — yargs subcommand-group dispatcher, the same shape as `flatpak/`.
2
+ //
3
+ // A directory rather than one file because `check-website-cli-reference.mjs` reads every
4
+ // `command:` in a top-level file as a top-level command; a group's subcommands live beside
5
+ // its `index.ts` and are documented as `#### gjsify blueprint <sub>`.
6
+ import { blueprintTypesCommand } from './types.js';
7
+ export { blueprintTypesCommand };
8
+ export const blueprintCommand = {
9
+ command: 'blueprint <subcommand>',
10
+ description: 'Blueprint (.blp) tooling: generate the TypeScript type sidecars a .blp exports (ADR 0088).',
11
+ builder: (yargs) => yargs
12
+ .command(blueprintTypesCommand.command, blueprintTypesCommand.description, blueprintTypesCommand.builder, blueprintTypesCommand.handler)
13
+ .demandCommand(1)
14
+ .strict(),
15
+ };
@@ -0,0 +1,8 @@
1
+ import type { Command } from '../../types/index.js';
2
+ interface BlueprintTypesOptions {
3
+ paths?: string[];
4
+ check?: boolean;
5
+ verbose?: boolean;
6
+ }
7
+ export declare const blueprintTypesCommand: Command<unknown, BlueprintTypesOptions>;
8
+ export {};