@gjsify/rolldown-plugin-gjsify 0.36.0 → 0.38.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.
@@ -1,85 +1,24 @@
1
- // Resolver-layer guard: an unresolvable `@gjsify/*` edge is a build ERROR,
2
- // never a silent external.
1
+ // Resolver-layer guard: an unresolvable `@gjsify/*` edge is a build ERROR, never a
2
+ // silent external. Full rationale, the measured incident and the legitimate-vs-broken
3
+ // line: ../../AGENTS.md § "Unresolvable `@gjsify/*` edge".
3
4
  //
4
- // ## The failure this closes
5
+ // Why this sits UPSTREAM of `utils/gjs-bundle-guard.ts` (which refuses to EMIT a
6
+ // `--app gjs` bundle still importing a bare `node:` specifier, and stays as the last
7
+ // line of defence). That guard is:
8
+ // - gjs-only — the same silent externalisation happens on `--app node`/`browser`,
9
+ // where a surviving bare `@gjsify/foo` is equally unloadable and no
10
+ // `node:`-shaped symptom exists to detect;
11
+ // - symptom-shaped — it names `node:fs`, not the `@gjsify/fs` edge that failed,
12
+ // and cannot name the importer;
13
+ // - post-tree-shake — an unresolved edge that gets shaken out leaves no trace, so
14
+ // the artefact passes while the build is still wrong.
5
15
  //
6
- // Rolldown (like Rollup) treats an import nothing resolves as EXTERNAL: it
7
- // emits an `UNRESOLVED_IMPORT` **warning**, re-emits the ORIGINAL specifier
8
- // into the output, and exits 0. For a normal bundler that is a reasonable
9
- // default — the specifier might be provided by the host. For `gjsify build` it
10
- // is not, because the `@gjsify/*` edge that failed is precisely the SUBSTITUTION
11
- // that makes the bundle runnable on the target:
12
- //
13
- // import { readFileSync } from 'node:fs'
14
- // → aliasPlugin rewrites to `@gjsify/fs`
15
- // → `@gjsify/fs` cannot be resolved (no `node_modules`, or the workspace
16
- // package has no built `lib/` so its `exports["."]` points at a file
17
- // that does not exist)
18
- // → aliasPlugin falls through, nothing else claims it
19
- // → `node:fs` is externalised, the build "succeeds"
20
- //
21
- // Measured on `packages/infra/cli/dist/cli.gjs.mjs` (repo task #63): 1 bare
22
- // `node:` import on `origin/main` vs 11 from a worktree with no `node_modules`
23
- // — a bundle stock GJS aborts on before one line runs
24
- // (`ImportError: Unsupported URI scheme for importing: node`). The pre-commit
25
- // hook then rebuilt and AUTO-STAGED it, and nothing local objected: tsc, lint,
26
- // format, unit and e2e all run the Node entry, never the emitted GJS bundle.
27
- //
28
- // ## Why this is upstream of the `--app gjs` `node:` guard (PR #828)
29
- //
30
- // `utils/gjs-bundle-guard.ts` reads the finished module graph and refuses to
31
- // EMIT a `--app gjs` bundle that still statically imports a bare `node:`
32
- // specifier. That is the right last line of defence, and it stays. But it is:
33
- //
34
- // - **gjs-only** — the same silent externalisation happens on `--app node`
35
- // and `--app browser`, where a surviving bare `@gjsify/foo` is equally
36
- // unloadable and no `node:`-shaped symptom exists to detect;
37
- // - **symptom-shaped** — it names `node:fs`, not the `@gjsify/fs` edge that
38
- // actually failed, and cannot name the importer;
39
- // - **post-tree-shake** — an unresolved edge that gets shaken out leaves no
40
- // trace at all, so the artefact passes while the build is still wrong.
41
- //
42
- // This guard fires at RESOLUTION time instead: it names the specifier, the
43
- // substitution target, the importer, and the likely cause.
44
- //
45
- // ## The legitimate-vs-broken line
46
- //
47
- // A blanket "fail on any external" would break every real build — externals are
48
- // load-bearing on every target (`gi://*`, `cairo`/`system`/`gettext` on gjs; the
49
- // whole `EXTERNALS_NODE` set plus `@gjsify/node-gi*` and `node-datachannel` on
50
- // node; whatever the user passed to `--external`). Those never reach the
51
- // unresolved fallback by accident — a `resolveId` hook or the `external` option
52
- // CLAIMS them, deliberately. So the distinction drawn here is:
53
- //
54
- // DECLARED external → legitimate, ignored. Asked via the target's own
55
- // externals predicate (`isExternal`), i.e. exactly the
56
- // policy `externalsPlugin` enforces, so the guard and the
57
- // policy cannot drift.
58
- // PROMISED, missing → broken. Either (a) the specifier has an entry in the
59
- // target's substitution table and that target does not
60
- // resolve, or (b) the specifier IS a bare `@gjsify/*`
61
- // workspace package that does not resolve.
62
- // anything else → not our business. A missing `lodash` is a plain
63
- // dependency bug that `UNRESOLVED_IMPORT` already
64
- // reports; widening to every bare specifier would turn a
65
- // genuinely optional peer into a hard build failure, and
66
- // this guard's whole claim is that the `@gjsify/*` edge
67
- // is NOT optional — the target cannot run without it.
68
- //
69
- // On (a): the check is keyed on the ALIAS TARGET being a `@gjsify/*` package,
70
- // not merely on the entry existing. Every value in the curated + derived tables
71
- // is a `@gjsify/*` specifier today, so this is a distinction without a
72
- // difference for them — but it keeps a user `--alias foo=./some/local.js` (or a
73
- // shim path like `random-access-file/index.js`) on the old fall-through
74
- // behaviour instead of promoting a user's own mapping mistake to a fatal error
75
- // in a plugin they did not ask for.
76
- //
77
- // ## Not registered for `--library`
78
- //
79
- // Library mode PRESERVES imports on purpose (`preserveModules`, bare specifiers
80
- // kept so the consumer resolves them). An unresolved `@gjsify/*` there is the
81
- // contract, not a bug, so `library/lib.ts` deliberately does not compose this
82
- // plugin.
16
+ // Case (a) of the fatal line keys on the ALIAS TARGET being a `@gjsify/*` package,
17
+ // not merely on the entry existing. Every value in the curated + derived tables is
18
+ // `@gjsify/*` today, so it makes no difference for them — but it keeps a user
19
+ // `--alias foo=./some/local.js` (or a shim path like `random-access-file/index.js`)
20
+ // on the old fall-through instead of promoting a user's own mapping mistake to a
21
+ // fatal error in a plugin they did not ask for.
83
22
  import { dirname } from 'node:path';
84
23
  /** Bare `@gjsify/*` package specifier — the workspace edge this guard protects. */
85
24
  const WORKSPACE_SCOPE = '@gjsify/';
@@ -90,41 +29,33 @@ const WORKSPACE_SCOPE = '@gjsify/';
90
29
  */
91
30
  const MAX_REPORTED_ALIAS_SOURCES = 4;
92
31
  /**
93
- * Is `id` a bare `@gjsify/*` package specifier (root or subpath)?
94
- *
95
- * Deliberately excludes disk paths and synthetic ids: a resolved
96
- * `/…/node_modules/@gjsify/fs/lib/esm/index.js` is already resolved, and a
97
- * `\0gjsify-*` virtual id belongs to the plugin that emitted it.
32
+ * Is `id` a bare `@gjsify/*` package specifier (root or subpath)? Excludes disk
33
+ * paths and synthetic ids: a resolved `…/node_modules/@gjsify/fs/…` is already
34
+ * resolved, and a `\0gjsify-*` virtual id belongs to the plugin that emitted it.
98
35
  */
99
36
  export function isWorkspaceSpecifier(id) {
100
37
  if (!id.startsWith(WORKSPACE_SCOPE))
101
38
  return false;
102
39
  if (id.includes('\0'))
103
40
  return false;
104
- // `@gjsify/fs` and `@gjsify/fs/register/x` qualify; `@gjsify/` and
105
- // `@gjsify//x` do not (no package name). A resolved disk path can never
106
- // reach here — it is absolute or `./`-relative, so it never starts with the
107
- // scope in the first place.
41
+ // `@gjsify/fs/register/x` qualifies; `@gjsify/` and `@gjsify//x` do not.
108
42
  const rest = id.slice(WORKSPACE_SCOPE.length);
109
43
  return rest.length > 0 && !rest.startsWith('/');
110
44
  }
111
45
  /**
112
- * Pure decision: does this import edge have to resolve, and to what?
113
- *
114
- * Split out from the plugin so the legitimate-vs-broken line is unit-testable
115
- * without a bundler (`packages/infra/cli/src/unresolved-workspace-import.spec.ts`).
46
+ * Pure decision: does this import edge have to resolve, and to what? Split out from
47
+ * the plugin so the legitimate-vs-broken line is unit-testable without a bundler
48
+ * (`packages/infra/cli/src/unresolved-workspace-import.spec.ts`).
116
49
  */
117
50
  export function classifyImport(input) {
118
51
  const { source, importer, aliases, isExternal } = input;
119
- // Entry modules are files the caller named; they are resolved by the
120
- // orchestrator, never externalised.
52
+ // Entry modules are files the caller named — resolved by the orchestrator, never
53
+ // externalised.
121
54
  if (importer === undefined)
122
55
  return { verdict: 'ignore', reason: 'entry' };
123
- // Relative / absolute / synthetic ids are not workspace edges.
124
56
  if (source.startsWith('.') || source.startsWith('/') || source.startsWith('\0')) {
125
57
  return { verdict: 'ignore', reason: 'not-bare' };
126
58
  }
127
- // A specifier the target DECLARES external is a deliberate external.
128
59
  if (isExternal?.(source))
129
60
  return { verdict: 'ignore', reason: 'declared-external' };
130
61
  const aliasTarget = aliases?.[source];
@@ -148,22 +79,17 @@ export class UnresolvedWorkspaceImportError extends Error {
148
79
  }
149
80
  }
150
81
  /**
151
- * The actionable message. Names the specifier, the importer and the likely
152
- * cause — the three things `UNRESOLVED_IMPORT` (and the post-hoc `node:` guard)
153
- * cannot tell you.
82
+ * The actionable message: names the specifier, the importer and the likely cause —
83
+ * the three things `UNRESOLVED_IMPORT` and the post-hoc `node:` guard cannot.
154
84
  */
155
85
  export function formatUnresolvedWorkspaceImport(details) {
156
86
  const { target, source, candidate, aliasTarget, aliasedFrom, importer } = details;
157
- // The specifiers `candidate` was standing in for: `source` itself when IT
158
- // carried the table entry, else whatever the reverse lookup recovered.
159
- //
160
- // The reverse lookup is only reported when its fan-in is SMALL. A shared
161
- // sink inverts to a useless wall of text — `@gjsify/empty` is the browser
162
- // target for 49 specifiers, and printing all of them says nothing about
163
- // which one this importer wrote, while `@gjsify/fs` inverts to exactly
164
- // `['fs','node:fs']` (the two spellings of one builtin), which is the
165
- // information the reader needs. Above the threshold, fall back to naming
166
- // the package alone rather than guessing.
87
+ // The specifiers `candidate` stood in for: `source` when IT carried the table
88
+ // entry, else the reverse lookup — but only when its fan-in is SMALL. A shared
89
+ // sink inverts to a wall of text (`@gjsify/empty` is the browser target for 49
90
+ // specifiers) that says nothing about which one this importer wrote, while
91
+ // `@gjsify/fs` inverts to exactly the two spellings of one builtin. Above the
92
+ // threshold, name the package alone rather than guess.
167
93
  const reversed = aliasedFrom ?? [];
168
94
  const substitutedFor = aliasTarget === undefined ? (reversed.length <= MAX_REPORTED_ALIAS_SOURCES ? reversed : []) : [source];
169
95
  const quoted = substitutedFor.map((s) => `\`${s}\``).join(', ');
@@ -172,10 +98,9 @@ export function formatUnresolvedWorkspaceImport(details) {
172
98
  : `gjsify build --app ${target}: cannot resolve \`${candidate}\`, the \`--app ${target}\` ` +
173
99
  `substitution for ${quoted}.`;
174
100
  // The symptom is a property of the SPECIFIER that would survive, not of the
175
- // target: only a `node:` specifier reaching stock GJS produces the URI-scheme
176
- // abort; a bare `@gjsify/*` or `<pkg>/register` one produces `Module not
177
- // found` (GJS's ESM loader has no node_modules walker and does not follow
178
- // `exports` maps). Naming the wrong one sends the reader hunting the wrong bug.
101
+ // target: only a `node:` specifier reaching stock GJS gives the URI-scheme abort,
102
+ // while a bare `@gjsify/*` gives `Module not found`. Naming the wrong one sends
103
+ // the reader hunting the wrong bug.
179
104
  const surviving = substitutedFor.length === 0 ? [source] : substitutedFor;
180
105
  const symptom = target !== 'gjs'
181
106
  ? ''
@@ -202,12 +127,7 @@ export function formatUnresolvedWorkspaceImport(details) {
202
127
  ` - \`${candidate}\` is genuinely not a dependency of this project — add it.`,
203
128
  ].join('\n');
204
129
  }
205
- /**
206
- * Reverse the substitution table: alias TARGET → the specifiers that route to it.
207
- *
208
- * Built once per plugin instance (the tables run to ~150 entries per target, and
209
- * the guard only consults this on the failure path).
210
- */
130
+ /** Reverse the substitution table: alias TARGET → the specifiers that route to it. */
211
131
  export function buildReverseAliasIndex(aliases) {
212
132
  const index = new Map();
213
133
  if (!aliases)
@@ -226,54 +146,42 @@ export function buildReverseAliasIndex(aliases) {
226
146
  /**
227
147
  * Refuse a build whose `@gjsify/*` substitution could not be resolved.
228
148
  *
229
- * `order: 'post'` so every deliberate claim — `aliasPlugin` (`pre`),
230
- * `napiNodeAddonPlugin` (`pre`), `gjsGiNodePlugin` (`pre`),
231
- * `gjsImportsEmptyPlugin`, `externalsPlugin` — gets first refusal. Only ids that
232
- * NOTHING claimed reach this hook, i.e. exactly the ones headed for the
233
- * unresolved-import fallback.
234
- *
235
- * The hook re-runs the full resolution chain via `this.resolve(…, { skipSelf })`
236
- * — which is what Rolldown's builtin resolver would have done next — and returns
237
- * its result when it succeeds, so a healthy build resolves the specifier exactly
238
- * once and behaves identically. Only a genuine `null` throws.
149
+ * `order: 'post'` so every deliberate claim (alias/napi/gi-node at `pre`,
150
+ * imports-empty/externals at normal) gets first refusal — only ids NOTHING claimed
151
+ * reach this hook. It then re-runs the resolution Rolldown would have run next
152
+ * (`this.resolve(…, { skipSelf })`) and returns that, so a healthy build resolves
153
+ * each specifier exactly once and behaves identically. Only a genuine `null` throws.
239
154
  */
240
155
  export function unresolvedWorkspaceImportPlugin(options) {
241
156
  const { target, aliases, isExternal } = options;
242
- // Re-entrancy belt: `skipSelf: true` is supposed to keep our own
243
- // `this.resolve` from re-entering this hook, but a hang is a far worse
244
- // failure than a missed check, so the in-flight candidate is tracked
245
- // explicitly. Keyed by `candidate\0importer` — the same package legitimately
246
- // resolves from many importers.
157
+ // Re-entrancy belt: `skipSelf: true` should keep our own `this.resolve` out of
158
+ // this hook, but a hang is a far worse failure than a missed check. Keyed by
159
+ // `candidate\0importer` — the same package legitimately resolves from many.
247
160
  const inFlight = new Set();
248
- // Alias TARGET → the specifiers that route to it. Only read on the failure
249
- // path, to recover the `node:fs` behind a failed `@gjsify/fs`.
161
+ // Read only on the failure path, to recover the `node:fs` behind a failed
162
+ // `@gjsify/fs`.
250
163
  const reverseAliases = buildReverseAliasIndex(aliases);
251
- // Successful resolutions, memoized per (candidate, importer DIRECTORY) —
252
- // the pair node resolution actually depends on, so a hit is the answer the
253
- // chain would have recomputed. Without it this hook was the single most
254
- // expensive plugin in a CLI-bundle build (32% of plugin time): `@gjsify/*`
255
- // edges repeat across hundreds of modules, and each miss re-runs the whole
256
- // `pre`-order chain (alias, napi-addon, gi-node) for the same target. Same
257
- // shape + rationale as `napiNodeAddonPlugin`'s `bareEntryCache` (#840).
258
- // Only POSITIVE results are cached: a failure throws, so it is never hot,
259
- // and caching it could outlive a package appearing mid-watch.
164
+ // Successful resolutions memoized per (candidate, importer DIRECTORY) — the pair
165
+ // node resolution actually depends on, so a hit is the answer the chain would have
166
+ // recomputed. Without it this hook was the most expensive plugin in a CLI-bundle
167
+ // build (32% of plugin time): `@gjsify/*` edges repeat across hundreds of modules
168
+ // and each miss re-runs the whole `pre`-order chain. POSITIVE results only — a
169
+ // failure throws, so it is never hot, and caching it could outlive a package
170
+ // appearing mid-watch.
260
171
  const resolvedCache = new Map();
261
172
  return {
262
173
  name: 'gjsify-unresolved-workspace-import',
263
174
  resolveId: {
264
175
  order: 'post',
265
176
  async handler(source, rawImporter, extraOptions) {
266
- // The two engines disagree on "no importer": npm `rolldown`
267
- // passes `undefined`, `@gjsify/rolldown-native` passes `null`
268
- // (its hook payload round-trips through JSON, which has no
269
- // `undefined`). `classifyImport` reads an absent importer as
270
- // "this is an ENTRY, never externalised" via `=== undefined`,
271
- // and `null` fails that test — so under the GJS bundler an
272
- // entry module would have been resolved as a normal edge and
273
- // could throw this plugin's error for a file the caller named
274
- // explicitly. Normalise once, at the boundary — same fix as
275
- // `napiNodeAddonPlugin` (#840), whose `dirname(null)` threw
276
- // and took the whole GJS build down as an UNHANDLEABLE_ERROR.
177
+ // The two engines disagree on "no importer": npm `rolldown` passes
178
+ // `undefined`, `@gjsify/rolldown-native` passes `null` (its hook
179
+ // payload round-trips through JSON, which has no `undefined`).
180
+ // `classifyImport` reads an absent importer as "ENTRY, never
181
+ // externalised" via `=== undefined`, which `null` fails — so under
182
+ // the GJS bundler an entry module got treated as a normal edge and
183
+ // could throw this plugin's error for a file the caller named.
184
+ // Normalise once, at the boundary (same fix as `napiNodeAddonPlugin`).
277
185
  const importer = typeof rawImporter === 'string' ? rawImporter : undefined;
278
186
  if (extraOptions?.isEntry)
279
187
  return null;
@@ -1,27 +1,19 @@
1
1
  // GJS console shim — bundled into GJS user builds via Rolldown's `inject`.
2
- // Uses print()/printerr() on GJS, bypassing GLib.log_structured() — no
3
- // `Gjs-Console-Message:` prefix, ANSI escapes work, output goes to
4
- // stdout/stderr instead of GLib's logging stream.
2
+ // Routes through print()/printerr() rather than GLib.log_structured(): no
3
+ // `Gjs-Console-Message:` prefix, ANSI escapes survive, output on stdout/stderr.
5
4
  //
6
- // `@gjsify/console` is resolved by the user's `gjsify build` Rolldown
7
- // run, NOT by tsc here. The bare specifier survives compilation and only
8
- // gets followed at user-build time, where the CLI's `@gjsify/node-polyfills`
9
- // dep tree has the package. tsc on this package would otherwise need the
10
- // `@gjsify/console` lib to be pre-built (build-order coupling).
5
+ // `globalThis.console` is non-writable AND non-configurable in GJS — re-measured
6
+ // on 1.88/SM 140, so it is a property of the runtime and not of one version — which
7
+ // is why nothing here assigns to it. Rolldown's `inject` rewrites bare `console`
8
+ // references to a named import from this shim instead.
11
9
  //
12
- // We can't reassign `globalThis.console` on SpiderMonkey 128 — the
13
- // property is non-configurable. Rolldown's `inject` option rewrites bare
14
- // `console` references to a named import from this shim instead, leaving
15
- // `globalThis.console` untouched and routing user `console.log(…)` calls
16
- // through our object.
10
+ // The `@gjsify/console` specifier is followed at USER-build time by Rolldown, not by
11
+ // tsc here; resolving it here would make this package depend on that one being built
12
+ // first. Hence the namespace import: a single statement no formatter can wrap, so the
13
+ // `@ts-ignore` stays on the line directly above it. A multi-line named import would
14
+ // let oxfmt detach the suppression from the `from '@gjsify/console'` line.
17
15
  // @ts-ignore — resolved by Rolldown at user-build time, not by tsc here.
18
16
  import * as gjsConsole from '@gjsify/console';
19
- // NOTE: a namespace import is used deliberately. It is a single statement that
20
- // no formatter (oxfmt) can wrap across lines, so the `@ts-ignore` above always
21
- // sits on the line immediately preceding the import and reliably suppresses the
22
- // TS2307 (`@gjsify/console` is resolved by Rolldown at user-build time, not by
23
- // tsc here). A multi-line named import would let the reformatter detach the
24
- // suppression from the offending `from '@gjsify/console'` line.
25
17
  export const console = {
26
18
  log: gjsConsole.log,
27
19
  info: gjsConsole.info,
@@ -1,14 +1,10 @@
1
1
  import type { InputOptions, RolldownPluginOption, TransformOptions } from 'rolldown';
2
2
  /**
3
- * In-memory bundle function — returns the per-entry chunk code strings.
4
- * Implementations: npm rolldown (Node default), `@gjsify/rolldown-native`
5
- * (GJS). Pulled out so auto-globals can run under either engine without
6
- * hardcoding npm rolldown (which can't load under GJS — the Rust prebuild's
7
- * init code uses `require('node:fs')` synchronously).
8
- *
9
- * The default impl below dynamically imports npm rolldown; the CLI
10
- * overrides this from `actions/build.ts` to route via the same engine the
11
- * final build uses.
3
+ * In-memory bundle function returning the per-entry chunk code strings, injected so
4
+ * auto-globals runs under either engine: npm rolldown cannot load under GJS (its Rust
5
+ * prebuild's init code calls `require('node:fs')` synchronously). The default impl
6
+ * dynamically imports npm rolldown; the CLI overrides it from `actions/build.ts` to
7
+ * route via the same engine the final build uses.
12
8
  */
13
9
  export type AnalysisBundler = (input: {
14
10
  rolldownInput: InputOptions;
@@ -16,101 +12,73 @@ export type AnalysisBundler = (input: {
16
12
  }) => Promise<string[]>;
17
13
  import type { PluginOptions } from '../types/plugin-options.js';
18
14
  export interface AutoGlobalsResult {
19
- /** Global identifiers detected in the bundle */
20
15
  detected: Set<string>;
21
- /** Path to the generated inject stub, or undefined if no globals needed */
16
+ /** Path to the generated inject stub; undefined when no globals are needed. */
22
17
  injectPath: string | undefined;
23
18
  }
24
19
  /**
25
- * Build the build-time note for any GI-backed register modules in a final
26
- * inject set, or null when none are GI-backed. Pure (no I/O) so it is
27
- * unit-testable; `emitGiBackedDiagnostic` is the side-effecting wrapper.
28
- *
29
- * `--globals auto` injecting a GI-backed register makes the bundle
30
- * hard-require a GTK/GNOME runtime at load — under a GTK-less host (headless
31
- * type-check container, SSR generator, plain CI) it throws `Typelib … not
32
- * found`. The note names which globals triggered the injection so that
33
- * otherwise-silent runtime crash is traceable at build time.
20
+ * The build-time note for any GI-backed register modules in a final inject set, or
21
+ * null when none are. Pure so it is unit-testable; `emitGiBackedDiagnostic` is the
22
+ * side-effecting wrapper.
34
23
  *
35
- * @param registerPaths the final register paths to be injected
36
- * @param detected the converged detected-global set (for naming trigger refs)
24
+ * Injecting a GI-backed register makes the bundle hard-require a GTK/GNOME runtime at
25
+ * load, so under a GTK-less host (headless type-check container, SSR generator, plain
26
+ * CI) it throws `Typelib … not found`. Naming the triggering globals makes that
27
+ * otherwise-silent runtime crash traceable at build time.
37
28
  */
38
29
  export declare function describeGiBackedInjection(registerPaths: Set<string>, detected: Set<string>): string | null;
39
30
  export interface DetectAutoGlobalsOptions {
40
31
  /**
41
- * Extra explicit identifiers (or group aliases like `dom`/`web`/`node`)
42
- * that should always be injected, in addition to whatever the iterative
43
- * detection finds. Used by `--globals auto,<extras>` for cases where
44
- * the detector cannot statically see a global because it's accessed via
45
- * indirection (e.g. Excalibur's `BrowserComponent.nativeComponent.matchMedia`).
32
+ * Identifiers or group aliases (`dom`/`web`/`node`) always injected on top of what
33
+ * detection finds — `--globals auto,<extras>`, for globals reached through
34
+ * indirection the detector cannot follow statically (Excalibur's
35
+ * `BrowserComponent.nativeComponent.matchMedia`).
46
36
  */
47
37
  extraGlobalsList?: string;
48
38
  /**
49
- * Identifiers to remove from the auto-detected set before writing the
50
- * inject stub. Useful for globals that appear as false positives from
51
- * dead browser-compat code in npm dependencies whose polyfills require
52
- * unavailable native libraries.
39
+ * Identifiers to remove before writing the inject stub — for false positives from
40
+ * dead browser-compat code in deps whose polyfills need unavailable native libs.
53
41
  */
54
42
  excludeGlobals?: string[];
55
43
  /**
56
- * Skip the committed register-globals closure-map expansion and run the
57
- * pure iterative loop. Used by the closure-map GENERATOR
58
- * (`packages/infra/cli/scripts/generate-register-closure.mjs`): computing
59
- * a register module's closure THROUGH the committed map would re-add
60
- * every identifier the OLD entry contained — removed globals could never
61
- * leave the map (a ratchet, with transitive pollution into sibling
44
+ * Skip the closure-map expansion and run the pure iterative loop. Only the
45
+ * closure-map GENERATOR sets this: computing a register's closure THROUGH the
46
+ * committed map would re-add every identifier the OLD entry contained, so removed
47
+ * globals could never leave it (a ratchet, with transitive pollution into sibling
62
48
  * entries) and `--check` would stay green on semantically stale data.
63
- * Builds never set this; the expansion is exactly what makes them
64
- * converge after one detection pass.
65
49
  */
66
50
  disableClosureExpansion?: boolean;
67
51
  /**
68
- * Working directory of the project being built. Used to locate the
69
- * project's `node_modules` when checking whether a detected global's
70
- * polyfill package is actually installed.
71
- *
72
- * When omitted the resolvability gate is BYPASSED (every detected
73
- * register path is written as-is) to preserve the original behaviour
74
- * for callers that don't know the project root. The real CLI build
75
- * (`actions/build.ts`) passes `process.cwd()` so the gate is active in
76
- * a normal `gjsify build`.
52
+ * Project root, used to locate `node_modules` when checking whether a detected
53
+ * global's polyfill package is installed. Omitting it BYPASSES that resolvability
54
+ * gate (every detected register path is written as-is); the real CLI build passes
55
+ * `process.cwd()`.
77
56
  */
78
57
  cwd?: string;
79
58
  }
80
59
  /**
81
- * Build options accepted by the analyser. A subset of Rolldown's
82
- * `InputOptions` plus the `output` shape used by `RolldownBuild.generate`.
83
- *
84
- * The caller passes the same input + output options it would use for the
85
- * final build (input, plugins, external, define, …). We strip output-side
86
- * fields that would force a write-to-disk and replace them with in-memory
87
- * settings.
60
+ * Build options for the analyser: the same input options the caller would use for the
61
+ * final build, with the output-side fields that force a write-to-disk stripped and
62
+ * replaced by in-memory settings.
88
63
  */
89
64
  export interface AnalysisOptions {
90
65
  input: InputOptions['input'];
91
66
  plugins?: RolldownPluginOption[];
92
67
  external?: InputOptions['external'];
93
68
  resolve?: InputOptions['resolve'];
94
- /**
95
- * Pass-through to Rolldown's `transform` (Oxc-driven) — `define`,
96
- * `dropLabels`, `treeShake`, etc. live here in Rolldown's shape.
97
- */
69
+ /** Pass-through to Rolldown's Oxc-driven `transform` (`define`, `treeShake`, …). */
98
70
  transform?: TransformOptions;
99
- /** Format for the analysis bundle output. Use 'esm' to match the final build. */
71
+ /** Use 'esm' to match the final build. */
100
72
  format?: 'esm' | 'cjs' | 'iife';
101
73
  }
102
74
  /**
103
- * Build a `gjsifyPlugin` for the analyser to insert into the plugin array.
104
- * Late-imported via dynamic `await import()` to break the cyclic dep
105
- * between this file and `../plugin.ts`.
106
- *
107
- * Returns either the plugin array directly (legacy form) or the full
108
- * orchestrator config `{ options, plugins }`. The latter shape lets
109
- * `detectAutoGlobals` reuse the orchestrator's `resolve.conditionNames` /
110
- * `mainFields` / `external` / `treeshake` settings for the in-memory
111
- * analysis bundle — without that, native-rolldown and npm-rolldown default
112
- * to different module-resolution conditions and the bundled output (and
113
- * therefore the detected free-global set) diverges between engines.
75
+ * Supplies the gjsify plugin to the analyser; passed in by the caller to break the
76
+ * cyclic dep between this file and `../plugin.ts`. Returns either the plugin array
77
+ * alone or the full orchestrator config `{ options, plugins }` — the latter lets the
78
+ * analysis bundle reuse the orchestrator's `resolve.conditionNames`/`mainFields`/
79
+ * `external`/`treeshake`, without which native-rolldown and npm-rolldown default to
80
+ * different module-resolution conditions and the detected free-global set diverges
81
+ * between engines.
114
82
  */
115
83
  type GjsifyFactoryReturn = RolldownPluginOption | {
116
84
  options: InputOptions;
@@ -118,21 +86,10 @@ type GjsifyFactoryReturn = RolldownPluginOption | {
118
86
  };
119
87
  type GjsifyPluginFactory = (options: PluginOptions) => GjsifyFactoryReturn | Promise<GjsifyFactoryReturn>;
120
88
  /**
121
- * Run an iterative Rolldown build (in-memory) with acorn-based global
122
- * detection. Each pass uses the globals discovered by the previous pass,
123
- * stopping once the detected set is stable (fixpoint reached).
124
- *
125
- * Returns the inject stub path that the caller should pass to the
126
- * final (real) build via `pluginOptions.autoGlobalsInject`.
127
- *
128
- * @param analysisOptions Rolldown input options for the in-memory build.
129
- * @param pluginOptions Gjsify plugin options (without `autoGlobalsInject`,
130
- * which this function computes).
131
- * @param gjsifyPluginFactory Factory returning the gjsify plugin instance
132
- * for a given set of plugin options. Provided by the caller to avoid a
133
- * cyclic import between this module and `../plugin.ts`.
134
- * @param verbose Emit per-iteration debug output to console.
135
- * @param options Optional `extraGlobalsList` / `excludeGlobals`.
89
+ * Run the iterative in-memory build with acorn-based global detection, each pass
90
+ * seeded by the previous pass's globals, until the detected set is stable. Returns the
91
+ * inject stub path the caller passes to the final build via
92
+ * `pluginOptions.autoGlobalsInject`.
136
93
  */
137
94
  export declare function detectAutoGlobals(analysisOptions: AnalysisOptions, pluginOptions: Omit<PluginOptions, 'autoGlobalsInject'>, gjsifyPluginFactory: GjsifyPluginFactory, verbose?: boolean, options?: DetectAutoGlobalsOptions, bundler?: AnalysisBundler): Promise<AutoGlobalsResult>;
138
95
  /**
@@ -145,24 +102,21 @@ export declare function detectAutoGlobals(analysisOptions: AnalysisOptions, plug
145
102
  * 1. a GJS ambient global (`print`/`printerr`/`log`/`logError`/`ARGV`/
146
103
  * `imports`) referenced as a BARE identifier — the legacy GJS-source shape
147
104
  * `@gjsify/node-gi/globals` seeds (`detectGjsAmbientGlobals`);
148
- * 2. a STATIC import of `@gjsify/node-gi` (root or subpath) — the shape the
149
- * bare GJS built-ins (`system`/`gettext`/`cairo`) are externalised to, i.e.
150
- * the PORTABLE GJS-source spelling (`detectNodeGiModuleImports`). A bundle
151
- * retaining one is already bound to the bridge at load, so flipping
152
- * injection cannot cost plain-Node loadability — while a portable GJS app
153
- * with no ambient global at all (e.g. `system.programArgs` instead of
154
- * `ARGV`) would otherwise get its `@girs/*` bodies emptied and its globals
155
- * shim dropped (the `gjsify storybook --runtime node` no-`requireGi`
156
- * regression).
105
+ * 2. a STATIC import of `@gjsify/node-gi` (root or subpath) — the shape the bare GJS
106
+ * built-ins are externalised to, i.e. the PORTABLE GJS-source spelling
107
+ * (`detectNodeGiModuleImports`). A bundle retaining one is already bound to the
108
+ * bridge at load, so flipping injection cannot cost plain-Node loadability, while
109
+ * a portable GJS app with no ambient global at all (`system.programArgs` instead
110
+ * of `ARGV`) would otherwise get its `@girs/*` bodies emptied and its globals shim
111
+ * dropped (the `gjsify storybook --runtime node` no-`requireGi` regression).
157
112
  *
158
- * A SINGLE pass suffices (unlike the GJS register-injection loop): the shim is
159
- * EXTERNAL, so injecting it adds no bundled code that could reference more
160
- * globals — there is no tree-shaking dependency cycle to converge.
113
+ * A SINGLE pass suffices, unlike the GJS register-injection loop: the shim is EXTERNAL,
114
+ * so injecting it adds no bundled code that could reference more globals — no
115
+ * tree-shaking dependency cycle to converge.
161
116
  *
162
- * Critically, detection is on the bundled output AFTER tree-shaking, so a
163
- * `print`/`imports` reference behind statically-dead code does not trigger
164
- * injection — importing the shim eagerly loads the native node-gi addon, which
165
- * crashes on plain Node without node-gi. See `detectGjsAmbientGlobals`.
117
+ * Detection is on the bundled output AFTER tree-shaking, so a `print`/`imports`
118
+ * reference behind statically-dead code does not trigger injection — importing the shim
119
+ * eagerly loads the native node-gi addon, which crashes on plain Node without node-gi.
166
120
  */
167
121
  export declare function detectNodeGiGlobals(analysisOptions: AnalysisOptions, pluginOptions: Omit<PluginOptions, 'nodeGiGlobalsInject'>, gjsifyPluginFactory: GjsifyPluginFactory, bundler?: AnalysisBundler): Promise<boolean>;
168
122
  export {};