@gjsify/rolldown-plugin-gjsify 0.35.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/app/gjs.d.ts CHANGED
@@ -31,68 +31,42 @@ export interface GjsFactoryInput {
31
31
  }
32
32
  export declare const setupForGjs: (input: GjsFactoryInput) => Promise<GjsBuildConfig>;
33
33
  /**
34
- * Build the canonical `--app gjs` externals predicate:
34
+ * The canonical `--app gjs` externals predicate — used both for the in-process
35
+ * alias layer and as the predicate behind `externalsPlugin`. Pinned by
36
+ * `packages/infra/cli/src/auto-globals.spec.ts`.
35
37
  *
36
- * - `@gjsify/<pkg>/register[/<feature>]` (and the bare `<pkg>/register`
37
- * form) MUST NEVER be externalized — these are the side-effect entry
38
- * points `--globals auto` injects to wire up
39
- * `globalThis.{Buffer,fetch,…}`. GJS's native ESM loader has no
40
- * node_modules walker AND does not follow `package.json#exports` maps
41
- * for bare specifiers, so an externalized
42
- * `import '@gjsify/buffer/register/buffer'` throws `Module not found`
43
- * at runtime even when the file is on disk via the exports map.
44
- * Inlining is the only safe option — the exclusion is by SHAPE
45
- * (`isRegisterSubpath`), not an explicit package list, so it scales
46
- * to every package added by the tree-shakeable-globals convention.
47
- * It short-circuits BEFORE the user-external check so `bundler.external`
48
- * can never override it. See AGENTS.md §Tree-shakeable globals.
49
- * - `gi://*` URIs are external by prefix (GJS-native imports).
50
- * - `cairo`/`gettext`/`system` + user externals match by exact name.
51
- *
52
- * Used by `setupForGjs` for the in-process alias layer AND as the
53
- * predicate behind `externalsPlugin` (resolveId `{ external: true }`,
54
- * honoured by both bundler engines). Exported for the regression tests
55
- * in `auto-globals.spec.ts` — canonical contract, change-detector status.
38
+ * - `<pkg>/register[/<feature>]` MUST NEVER be externalized: GJS's ESM loader
39
+ * has no node_modules walker and does not follow `exports` maps for bare
40
+ * specifiers, so an externalized `import '@gjsify/buffer/register/buffer'`
41
+ * throws `Module not found` at runtime even with the file on disk. Matched by
42
+ * SHAPE (`isRegisterSubpath`) so it scales to every package following the
43
+ * tree-shakeable-globals convention, and short-circuited BEFORE the
44
+ * user-external check so `bundler.external` cannot override it.
45
+ * - `gi://*` external by prefix; `cairo`/`gettext`/`system` + user externals by
46
+ * exact name.
56
47
  */
57
48
  export declare function createGjsExternalsPredicate(userExternal?: string[]): (id: string) => boolean;
58
49
  /**
59
- * Recognize the shims this plugin INJECTS into a `--app gjs` bundle
60
- * (`shims/module-resolve`, `shims/console-gjs`, `shims/unicorn-magic`, …), in
61
- * both bare-specifier and resolved-disk-path form.
62
- *
63
- * They must be force-inlined for exactly the reason `/register` subpaths are,
64
- * and the failure is the same one: GJS has no `require` and its ESM loader has
65
- * neither a node_modules walker nor `exports`-map support, so an EXTERNALIZED
66
- * shim aborts the program at load:
67
- *
68
- * Error: Calling `require` for
69
- * "@gjsify/rolldown-plugin-gjsify/shims/module-resolve" in an environment
70
- * that doesn't expose the `require` function
50
+ * Recognize the shims this plugin INJECTS into a `--app gjs` bundle, in both
51
+ * bare-specifier and resolved-disk-path form.
71
52
  *
72
- * The shim is not optional — the bundler injected it because the bundle needs
73
- * it — so externalizing it can only ever produce an unloadable artifact. The
74
- * carve-out short-circuits BEFORE the user-external check so `bundler.external`
75
- * cannot override it, and it matches by SHAPE rather than an explicit list, so
76
- * a shim added later is covered without touching this predicate.
77
- *
78
- * Scope is deliberately narrow: only OUR shim directory. A consumer package
79
- * that happens to have a `shims/` folder is unaffected.
53
+ * Force-inlined for the same reason `/register` subpaths are, with the same
54
+ * failure: GJS has no `require` and its ESM loader follows neither node_modules
55
+ * nor `exports` maps, so an externalized shim aborts at load with `Calling
56
+ * \`require\` for "…/shims/module-resolve" in an environment that doesn't expose
57
+ * the \`require\` function`. The shim is never optional — the bundler injected it
58
+ * because the bundle needs it. Short-circuits BEFORE the user-external check, and
59
+ * matches by SHAPE so a later shim is covered without touching this predicate.
60
+ * Deliberately scoped to OUR shim directory only.
80
61
  */
81
62
  export declare function isGjsifyShim(id: string): boolean;
82
63
  /**
83
- * Recognize the `/register` and `/register/<feature>` subpath shapes that
84
- * `--globals auto` injects into the bundle as side-effect imports.
85
- *
86
- * Matches every shape that goes through the alias layer + Rolldown
87
- * resolution chain to a `@gjsify/<pkg>/register*` target:
88
- * - bare: `<pkg>/register`, `<pkg>/register/<feature>`
89
- * - fully qualified `@gjsify/<pkg>/register`, `@gjsify/<pkg>/register/<feature>`
90
- * - resolved disk paths under a real `node_modules/<scope>/<pkg>/lib/esm/register*`
64
+ * Recognize the `/register[/<feature>]` subpath shapes `--globals auto` injects
65
+ * as side-effect imports, in bare, fully-qualified and resolved-disk-path form.
91
66
  *
92
- * Used by the `--app gjs` externals predicate to force-inline these even
93
- * when the user passes them via `bundler.external`. Exported for direct
94
- * use by the regression test in `auto-globals.spec.ts`; this is the
95
- * canonical contract — change-detector status. Keep in sync with the
96
- * `AGENTS.md` §Tree-shakeable globals subpath convention.
67
+ * Used by the `--app gjs` externals predicate to force-inline these even when the
68
+ * user lists them in `bundler.external`. Pinned by
69
+ * `packages/infra/cli/src/auto-globals.spec.ts`; keep in sync with AGENTS.md
70
+ * § Tree-shakeable globals.
97
71
  */
98
72
  export declare function isRegisterSubpath(id: string): boolean;
package/lib/app/gjs.js CHANGED
@@ -1,13 +1,6 @@
1
- // `--app gjs` Rolldown configuration factory.
2
- //
3
- // Mirrors the esbuild predecessor's `setupForGjs` exactly in terms of the
4
- // effective build behaviour: same externals, same alias map, same target
5
- // (firefox140 for JS, firefox60 for CSS), same console-shim injection,
6
- // same process-stub banner, same `random-access-file` fs-backed-fallback.
7
- //
8
- // Returns a partial `RolldownOptions` template plus the plugin array the
9
- // caller should compose with their user-supplied options. Library mode is
10
- // handled separately by `setupLib`.
1
+ // `--app gjs` Rolldown configuration factory: returns a partial
2
+ // `RolldownOptions` template plus the plugin array the caller composes with
3
+ // their user options. Library mode is `setupLib`.
11
4
  import { fileURLToPath } from 'node:url';
12
5
  import { dirname, resolve } from 'node:path';
13
6
  import { existsSync } from 'node:fs';
@@ -29,55 +22,41 @@ const _shimDir = dirname(fileURLToPath(import.meta.url));
29
22
  /**
30
23
  * Resolve one of this package's bundled shims to an absolute path.
31
24
  *
32
- * `createRequire` is a NAMED IMPORT from `node:module`, never a bare
33
- * `require(...)` — this source is ESM (see AGENTS.md "Our source is ESM —
34
- * no bare `require`"). It is used here for its RESOLVER, which is
35
- * `exports`-map aware, and only to answer "where does this subpath live";
36
- * nothing is loaded through it.
25
+ * `createRequire` is used only for its `exports`-map-aware RESOLVER; nothing is
26
+ * loaded through it (this source is ESM — see AGENTS.md "no bare `require`").
37
27
  */
38
28
  function resolveShim(shimName) {
39
- // Preferred: relative to this module's directory. Works under the
40
- // normal Node consumer flow where `_shimDir` = `<pkg>/lib/app/`.
29
+ // Normal Node consumption: `_shimDir` = `<pkg>/lib/app/`.
41
30
  const relative = resolve(_shimDir, `../shims/${shimName}.js`);
42
31
  if (existsSync(relative))
43
32
  return relative;
44
- // Fallback: when the orchestrator is bundled into a single .mjs
45
- // (GJS-CLI self-host loop) `_shimDir` collapses to the bundle's
46
- // own directory and the relative lookup misses. The published subpath
47
- // export `./shims/<name>` resolves under both Node and GJS without
48
- // further walking. Each shim MUST be a `./shims/<name>` subpath export
49
- // in package.json for this fallback to resolve.
33
+ // Fallback for the bundled-orchestrator case (GJS-CLI self-host loop):
34
+ // `_shimDir` collapses to the bundle's own directory and the relative lookup
35
+ // misses. Every shim MUST therefore be a `./shims/<name>` subpath export in
36
+ // package.json.
50
37
  try {
51
38
  return createRequire(import.meta.url).resolve(`@gjsify/rolldown-plugin-gjsify/shims/${shimName}`);
52
39
  }
53
40
  catch (cause) {
54
- // Both lookups failed — the shim is genuinely absent. Returning the
55
- // unverified `relative` here would hand a non-existent path to the
56
- // alias map, and the failure would resurface much later as an
57
- // unrelated "could not resolve" naming a path nobody wrote. Name the
58
- // actual problem, at the site that knows what it was looking for.
41
+ // Throw rather than return the unverified `relative`: a non-existent
42
+ // path in the alias map resurfaces much later as an unrelated "could not
43
+ // resolve" naming a path nobody wrote.
59
44
  throw new Error(`@gjsify/rolldown-plugin-gjsify: cannot locate the "${shimName}" shim. ` +
60
45
  `Tried ${relative} and the "./shims/${shimName}" subpath export.`, { cause });
61
46
  }
62
47
  }
63
48
  export const setupForGjs = async (input) => {
64
49
  const userExternal = input.userExternal ?? [];
65
- // Externals policy — enforced in TWO serializable forms so it is
66
- // identical under npm rolldown (Node) AND `@gjsify/rolldown-native`
67
- // (the GJS-default engine, which JSON.stringify's its options to the
68
- // Rust core and silently DROPS function values — the bug class that
69
- // already shipped via app/node.ts and library/lib.ts predicates):
70
- //
71
- // 1. `options.external` = a plain string array of EXACT names
72
- // (`cairo`/`gettext`/`system` + user `bundler.external` entries).
73
- // Arrays serialize fine on both engines.
74
- // 2. `externalsPlugin(external)` in the plugin chain for the SHAPE
75
- // rules an exact array can't express (`gi://` prefix match) —
76
- // a resolveId hook returning `{ external: true }` is honoured by
77
- // both engines via `normalizeResolveIdResult`.
78
- //
79
- // `exactExternal` filters out register subpaths so a user external
80
- // entry can never override the force-inline carve-out below.
50
+ // Externals policy in TWO forms, because `@gjsify/rolldown-native` (the
51
+ // GJS-default engine) JSON.stringify's its options to the Rust core and
52
+ // silently DROPS function values:
53
+ // 1. `options.external` = a plain string array of EXACT names — arrays
54
+ // serialize on both engines.
55
+ // 2. `externalsPlugin(external)` for the SHAPE rules an exact array can't
56
+ // express (`gi://` prefix) — a resolveId `{ external: true }` is
57
+ // honoured by both engines.
58
+ // `exactExternal` filters out register subpaths so a user external entry can
59
+ // never override the force-inline carve-out below.
81
60
  const exactExternal = [
82
61
  'cairo',
83
62
  'gettext',
@@ -88,12 +67,9 @@ export const setupForGjs = async (input) => {
88
67
  const format = input.pluginOptions.format ?? 'esm';
89
68
  const exclude = input.pluginOptions.exclude ?? [];
90
69
  const entryPoints = await globToEntryPoints(input.input, exclude);
91
- // unicorn-magic gates its full API behind the "node" conditional
92
- // exports. We deliberately omit `node` from conditionNames (some
93
- // packages ship genuinely Node-only code there — see comment
94
- // around `conditionNames` below). Route the package to our
95
- // bundled shim so the API is reachable under --app gjs without
96
- // turning on the node condition globally.
70
+ // unicorn-magic gates its full API behind the "node" condition, which we
71
+ // deliberately omit globally (see `conditionNames` below). Route it to our
72
+ // bundled shim instead of turning the condition on for every package.
97
73
  const unicornMagicShim = resolveShim('unicorn-magic');
98
74
  const aliasMap = {
99
75
  ...getAliasesForGjs({ external }),
@@ -103,22 +79,14 @@ export const setupForGjs = async (input) => {
103
79
  };
104
80
  // The console shim replaces all `console` references with print()/printerr()-
105
81
  // based implementations that bypass GLib.log_structured() — no prefix,
106
- // ANSI codes work. Disabled via `pluginOptions.consoleShim === false`.
107
- //
108
- // Path resolution: `resolve(_shimDir, '../shims/...')` works in normal
109
- // Node consumption (_shimDir = `<pkg>/lib/app/`). When the CLI is
110
- // bundled into a single .mjs (e.g. the GJS-CLI self-host loop),
111
- // `import.meta.url` collapses to the bundle's path and the relative
112
- // resolution lands at a non-existent location. Walk up via
113
- // createRequire's node_modules-aware resolver as a fallback.
82
+ // ANSI codes work.
114
83
  const consoleShimEnabled = input.pluginOptions.consoleShim !== false;
115
84
  const consoleShimPath = consoleShimEnabled ? resolveShim('console-gjs') : null;
116
- // The auto-globals inject stub (when present) is side-effect-imported
117
- // via a virtual entry — its register modules write to globalThis, so
118
- // the import chain matters but no name binding does. We can't use
119
- // Rolldown's `inject` for this because the auto-globals invariant
120
- // forbids source-AST rewrites for global identifiers (false positives
121
- // from isomorphic guards / bracket access — see AGENTS.md).
85
+ // The auto-globals inject stub is side-effect-imported via a virtual entry:
86
+ // its register modules write to globalThis, so the import chain matters but
87
+ // no name binding does. Rolldown's `inject` is not usable here — the
88
+ // auto-globals invariant forbids source-AST rewrites for global identifiers
89
+ // (see AGENTS.md § `--globals` modes).
122
90
  const sideEffectImports = [];
123
91
  if (input.pluginOptions.autoGlobalsInject)
124
92
  sideEffectImports.push(input.pluginOptions.autoGlobalsInject);
@@ -129,61 +97,47 @@ export const setupForGjs = async (input) => {
129
97
  const options = {
130
98
  input: finalInput,
131
99
  platform: 'neutral',
132
- // EXACT names only — the `gi://` prefix + register-subpath shape
133
- // rules live in `externalsPlugin` below (see the policy note at
134
- // the top of this function). A function predicate here would be
135
- // silently dropped by the native engine's JSON options boundary.
100
+ // EXACT names only — see the externals policy note above.
136
101
  external: exactExternal,
137
- // 'browser' field is needed so packages like create-hash, create-hmac,
138
- // randombytes use their pure-JS browser entry instead of index.js
139
- // (which does require('crypto') and causes circular dependencies via
140
- // the crypto → @gjsify/crypto alias).
102
+ // 'browser' first so packages like create-hash/create-hmac/randombytes
103
+ // take their pure-JS browser entry instead of an index.js that
104
+ // `require('crypto')`s and cycles back through the @gjsify/crypto alias.
141
105
  resolve: {
142
106
  mainFields: format === 'esm' ? ['browser', 'module', 'main'] : ['browser', 'main', 'module'],
143
107
  // ESM: omit 'require' — packages listing 'require' before 'import'
144
108
  // would silently route through their CJS entry.
145
109
  //
146
- // We deliberately do NOT add `'node'` here. Per Node's exports-map
147
- // spec the resolver iterates keys in DECLARATION ORDER and picks
148
- // the first one whose name is in `conditionNames` — the order of
149
- // conditionNames itself is irrelevant. Packages like
150
- // `cross-fetch-ponyfill` declare `"node"` first in their exports
151
- // map and ship a Node-only entry that imports `blobFrom`/
152
- // `fileFrom` (from native `node:fetch`). With `node` enabled,
153
- // the resolver picks that branch over `browser` and the bundle
154
- // breaks at link time. Packages that genuinely need their `node`
155
- // export under GJS (rare — only one known case so far,
156
- // `unicorn-magic`'s `traversePathUp`) are handled with explicit
157
- // resolve aliases instead.
110
+ // `'node'` is deliberately absent. The exports-map resolver iterates
111
+ // the PACKAGE's keys in declaration order and takes the first one
112
+ // present in `conditionNames` (our order is irrelevant), so enabling
113
+ // `node` hands `cross-fetch-ponyfill` its Node-only entry — it
114
+ // imports `blobFrom`/`fileFrom` and the bundle breaks at link time.
115
+ // Packages that genuinely need their `node` export under GJS (so far
116
+ // only `unicorn-magic`'s `traversePathUp`) get an explicit alias.
158
117
  conditionNames: format === 'esm' ? ['browser', 'import'] : ['browser', 'require', 'import'],
159
118
  },
160
119
  transform: {
161
- // Compile target: GJS 1.86 / SpiderMonkey 140 ≈ firefox140.
120
+ // GJS 1.86 / SpiderMonkey 140 ≈ firefox140.
162
121
  target: 'firefox140',
163
122
  define: {
164
123
  global: 'globalThis',
165
124
  window: 'globalThis',
166
125
  'process.env.READABLE_STREAM': '"disable"',
167
126
  },
168
- // Console shim: rewrite bare `console` references to a named
169
- // import from our shim module. We use Rolldown's `inject`
170
- // (Oxc-driven, lives under `transform`) because:
171
- // 1. `globalThis.console` is non-configurable on SpiderMonkey
172
- // 128 so a register-style global write throws.
173
- // 2. We're replacing console unconditionally — there's no
174
- // tree-shake-aware detection concern that motivated the
175
- // auto-globals invariant.
127
+ // Rewrite bare `console` to a named import from our shim. Rolldown's
128
+ // `inject` (not a register-style global write) because GJS defines
129
+ // `globalThis.console` non-writable + non-configurable, so assigning
130
+ // to it silently no-ops. Safe here where the auto-globals invariant
131
+ // forbids AST rewrites: console is replaced unconditionally, so there
132
+ // is no tree-shake-aware detection to defeat.
176
133
  ...(consoleShimPath ? { inject: { console: [consoleShimPath, 'console'] } } : {}),
177
134
  },
178
135
  output: {
179
136
  ...input.output,
180
137
  format,
181
138
  sourcemap: false,
182
- // App builds emit a single bundle file. Disable code-splitting
183
- // so dynamic imports get inlined and the entire program lands
184
- // in one chunk that matches `gjsify build --outfile foo.js`.
185
- // (`codeSplitting: false` replaces the deprecated
186
- // `inlineDynamicImports: true` in Rolldown ≥ 1.0-rc.18.)
139
+ // App builds emit a single bundle file: dynamic imports inline and
140
+ // the whole program lands in one chunk matching `--outfile`.
187
141
  codeSplitting: false,
188
142
  },
189
143
  treeshake: true,
@@ -200,32 +154,26 @@ export const setupForGjs = async (input) => {
200
154
  // Virtual-entry plugin runs FIRST so its resolveId/load match the
201
155
  // synthetic input ids that `wrapInputWithSideEffects` produces.
202
156
  ...(virtualEntries.plugin ? [virtualEntries.plugin] : []),
203
- // Strip leading #! from any input module BEFORE bundling — otherwise
204
- // a shebang in e.g. the CLI's own entry file ends up embedded
205
- // mid-chunk after our process-stub banner, and acorn (auto-globals
206
- // detector) rejects the `#` byte. Final-output shebang is composed
207
- // by shebangPlugin's renderChunk hook.
157
+ // Strip leading #! from every input module BEFORE bundling: a shebang in
158
+ // e.g. the CLI's own entry would end up mid-chunk after the process-stub
159
+ // banner, and acorn (the auto-globals detector) rejects the `#` byte.
160
+ // The final-output shebang is composed by shebangPlugin's renderChunk.
208
161
  inputShebangStripPlugin(),
209
- // random-access-file's 'browser' field maps to a throwing stub; force
210
- // the fs-backed Node entry. Implemented via the gjsify alias plugin
211
- // as a direct entry-table override.
162
+ // random-access-file's 'browser' field maps to a throwing stub; the alias
163
+ // table forces the fs-backed Node entry.
212
164
  aliasPlugin({ entries: aliasEntries }),
213
- // Transparent N-API `.node`-addon loader — the forward mirror of
214
- // `gjsGiNodePlugin`. Its `order:'pre'` resolveId claims a native-addon
215
- // acquisition (`bindings`/`node-gyp-build`/a direct `.node`/a napi-rs
216
- // platform sibling) and rewrites it to a virtual module returning
217
- // `loadAddon('<abs .node>')` from `@gjsify/napi`. Placed AFTER aliasPlugin
218
- // (so a user alias that pins an addon's native entry applies first) and
219
- // BEFORE externalsPlugin (so the addon acquisition is claimed before the
220
- // externals policy sees it). `@gjsify/napi` is BUNDLED, not external.
221
- // Inert unless such a specifier is in the graph — every other id passes
222
- // through. Always-on for `--app gjs`; never registered for node/browser/ns.
165
+ // Transparent N-API `.node`-addon loader (the forward mirror of
166
+ // `gjsGiNodePlugin`): claims a native-addon acquisition
167
+ // (`bindings`/`node-gyp-build`/a direct `.node`/a napi-rs platform
168
+ // sibling) and rewrites it to a virtual module returning `loadAddon()`
169
+ // from `@gjsify/napi`, which is BUNDLED, not external. Order matters:
170
+ // after aliasPlugin so a user alias pinning an addon's native entry wins,
171
+ // before externalsPlugin so the acquisition is claimed first. Inert
172
+ // otherwise; `--app gjs` only.
223
173
  napiNodeAddonPlugin(),
224
- // Enforce the full `--app gjs` externals policy (gi:// prefix,
225
- // exact names, register-subpath force-inline) via resolveId —
226
- // the only form BOTH engines honour (the native engine drops
227
- // function `external` options at its JSON boundary). Runs after
228
- // the alias plugin's `pre`-order resolveId so aliases apply first.
174
+ // Externals policy as a resolveId hook — the only form BOTH engines
175
+ // honour. Runs after the alias plugin's `pre` resolveId so aliases apply
176
+ // first.
229
177
  externalsPlugin(external, { name: 'gjsify-gjs-externalize' }),
230
178
  blueprintPlugin(),
231
179
  deepkitPlugin({ reflection: input.pluginOptions.reflection }),
@@ -236,27 +184,24 @@ export const setupForGjs = async (input) => {
236
184
  cssAsStringPlugin({ targets: { firefox: 60 << 16 } }),
237
185
  nodeModulesPathRewritePlugin({ bundleDir, runtimeResolve: format === 'esm' }),
238
186
  processStubPlugin({ userBanner: input.userBanner, captureBundleUrl: format === 'esm' }),
239
- // resolveShebangLine returns null when disabled (false/undefined) and
240
- // the resolved line otherwise — also handles `${env:…}` expansion.
187
+ // resolveShebangLine returns null when disabled, else the resolved line
188
+ // with `${env:…}` expanded.
241
189
  (() => {
242
190
  const line = resolveShebangLine(input.shebang);
243
191
  return shebangPlugin({ enabled: line !== null, line: line ?? undefined });
244
192
  })(),
245
- // LAST claim on an id nothing else wanted (`order: 'post'`): a
246
- // `@gjsify/*` substitution that cannot be resolved is a build ERROR, not
247
- // a silent external. Without it Rolldown externalises the ORIGINAL
248
- // specifier, exits 0, and writes a bundle stock GJS aborts on at load
249
- // (`ImportError: Unsupported URI scheme for importing: node`) — the
250
- // failure `utils/gjs-bundle-guard.ts` catches at emit time, caught here
251
- // at its source instead, with the importer and the cause named.
193
+ // LAST claim on an id nothing else wanted (`order: 'post'`): an
194
+ // unresolvable `@gjsify/*` substitution is a build ERROR, not a silent
195
+ // external. Otherwise Rolldown externalises the ORIGINAL specifier, exits
196
+ // 0, and writes a bundle stock GJS aborts on at load (`ImportError:
197
+ // Unsupported URI scheme for importing: node`) — the same failure
198
+ // `utils/gjs-bundle-guard.ts` catches at emit time, caught here at its
199
+ // source with the importer and cause named.
252
200
  unresolvedWorkspaceImportPlugin({ target: 'gjs', aliases: aliasEntries, isExternal: external }),
253
201
  ];
254
202
  return { options, plugins };
255
203
  };
256
- /**
257
- * Flatten the legacy `Record<string, string>` alias map into the
258
- * `@rollup/plugin-alias` `entries` array shape, dropping empty values.
259
- */
204
+ /** Copy the alias map, dropping entries with an empty target. */
260
205
  function flattenAliases(map) {
261
206
  const out = {};
262
207
  for (const [from, to] of Object.entries(map)) {
@@ -266,28 +211,19 @@ function flattenAliases(map) {
266
211
  return out;
267
212
  }
268
213
  /**
269
- * Build the canonical `--app gjs` externals predicate:
214
+ * The canonical `--app gjs` externals predicate — used both for the in-process
215
+ * alias layer and as the predicate behind `externalsPlugin`. Pinned by
216
+ * `packages/infra/cli/src/auto-globals.spec.ts`.
270
217
  *
271
- * - `@gjsify/<pkg>/register[/<feature>]` (and the bare `<pkg>/register`
272
- * form) MUST NEVER be externalized — these are the side-effect entry
273
- * points `--globals auto` injects to wire up
274
- * `globalThis.{Buffer,fetch,…}`. GJS's native ESM loader has no
275
- * node_modules walker AND does not follow `package.json#exports` maps
276
- * for bare specifiers, so an externalized
277
- * `import '@gjsify/buffer/register/buffer'` throws `Module not found`
278
- * at runtime even when the file is on disk via the exports map.
279
- * Inlining is the only safe option — the exclusion is by SHAPE
280
- * (`isRegisterSubpath`), not an explicit package list, so it scales
281
- * to every package added by the tree-shakeable-globals convention.
282
- * It short-circuits BEFORE the user-external check so `bundler.external`
283
- * can never override it. See AGENTS.md §Tree-shakeable globals.
284
- * - `gi://*` URIs are external by prefix (GJS-native imports).
285
- * - `cairo`/`gettext`/`system` + user externals match by exact name.
286
- *
287
- * Used by `setupForGjs` for the in-process alias layer AND as the
288
- * predicate behind `externalsPlugin` (resolveId `{ external: true }`,
289
- * honoured by both bundler engines). Exported for the regression tests
290
- * in `auto-globals.spec.ts` — canonical contract, change-detector status.
218
+ * - `<pkg>/register[/<feature>]` MUST NEVER be externalized: GJS's ESM loader
219
+ * has no node_modules walker and does not follow `exports` maps for bare
220
+ * specifiers, so an externalized `import '@gjsify/buffer/register/buffer'`
221
+ * throws `Module not found` at runtime even with the file on disk. Matched by
222
+ * SHAPE (`isRegisterSubpath`) so it scales to every package following the
223
+ * tree-shakeable-globals convention, and short-circuited BEFORE the
224
+ * user-external check so `bundler.external` cannot override it.
225
+ * - `gi://*` external by prefix; `cairo`/`gettext`/`system` + user externals by
226
+ * exact name.
291
227
  */
292
228
  export function createGjsExternalsPredicate(userExternal = []) {
293
229
  const exact = ['cairo', 'gettext', 'system', ...userExternal];
@@ -302,27 +238,17 @@ export function createGjsExternalsPredicate(userExternal = []) {
302
238
  };
303
239
  }
304
240
  /**
305
- * Recognize the shims this plugin INJECTS into a `--app gjs` bundle
306
- * (`shims/module-resolve`, `shims/console-gjs`, `shims/unicorn-magic`, …), in
307
- * both bare-specifier and resolved-disk-path form.
308
- *
309
- * They must be force-inlined for exactly the reason `/register` subpaths are,
310
- * and the failure is the same one: GJS has no `require` and its ESM loader has
311
- * neither a node_modules walker nor `exports`-map support, so an EXTERNALIZED
312
- * shim aborts the program at load:
241
+ * Recognize the shims this plugin INJECTS into a `--app gjs` bundle, in both
242
+ * bare-specifier and resolved-disk-path form.
313
243
  *
314
- * Error: Calling `require` for
315
- * "@gjsify/rolldown-plugin-gjsify/shims/module-resolve" in an environment
316
- * that doesn't expose the `require` function
317
- *
318
- * The shim is not optional — the bundler injected it because the bundle needs
319
- * it — so externalizing it can only ever produce an unloadable artifact. The
320
- * carve-out short-circuits BEFORE the user-external check so `bundler.external`
321
- * cannot override it, and it matches by SHAPE rather than an explicit list, so
322
- * a shim added later is covered without touching this predicate.
323
- *
324
- * Scope is deliberately narrow: only OUR shim directory. A consumer package
325
- * that happens to have a `shims/` folder is unaffected.
244
+ * Force-inlined for the same reason `/register` subpaths are, with the same
245
+ * failure: GJS has no `require` and its ESM loader follows neither node_modules
246
+ * nor `exports` maps, so an externalized shim aborts at load with `Calling
247
+ * \`require\` for "…/shims/module-resolve" in an environment that doesn't expose
248
+ * the \`require\` function`. The shim is never optional — the bundler injected it
249
+ * because the bundle needs it. Short-circuits BEFORE the user-external check, and
250
+ * matches by SHAPE so a later shim is covered without touching this predicate.
251
+ * Deliberately scoped to OUR shim directory only.
326
252
  */
327
253
  export function isGjsifyShim(id) {
328
254
  // Bare/fully-qualified: `@gjsify/rolldown-plugin-gjsify/shims/<name>`.
@@ -332,45 +258,24 @@ export function isGjsifyShim(id) {
332
258
  return /[\\/]rolldown-plugin-gjsify[\\/](?:lib|src)[\\/]shims[\\/]/.test(id);
333
259
  }
334
260
  /**
335
- * Recognize the `/register` and `/register/<feature>` subpath shapes that
336
- * `--globals auto` injects into the bundle as side-effect imports.
337
- *
338
- * Matches every shape that goes through the alias layer + Rolldown
339
- * resolution chain to a `@gjsify/<pkg>/register*` target:
340
- * - bare: `<pkg>/register`, `<pkg>/register/<feature>`
341
- * - fully qualified `@gjsify/<pkg>/register`, `@gjsify/<pkg>/register/<feature>`
342
- * - resolved disk paths under a real `node_modules/<scope>/<pkg>/lib/esm/register*`
261
+ * Recognize the `/register[/<feature>]` subpath shapes `--globals auto` injects
262
+ * as side-effect imports, in bare, fully-qualified and resolved-disk-path form.
343
263
  *
344
- * Used by the `--app gjs` externals predicate to force-inline these even
345
- * when the user passes them via `bundler.external`. Exported for direct
346
- * use by the regression test in `auto-globals.spec.ts`; this is the
347
- * canonical contract — change-detector status. Keep in sync with the
348
- * `AGENTS.md` §Tree-shakeable globals subpath convention.
264
+ * Used by the `--app gjs` externals predicate to force-inline these even when the
265
+ * user lists them in `bundler.external`. Pinned by
266
+ * `packages/infra/cli/src/auto-globals.spec.ts`; keep in sync with AGENTS.md
267
+ * § Tree-shakeable globals.
349
268
  */
350
269
  export function isRegisterSubpath(id) {
351
- // Source-shape: a bare or fully-qualified specifier ending in
352
- // `/register` or `/register/<feature>`. The leading `/` rules out
353
- // false positives like `register` (the bare word) or
354
- // `@scope/unregister`.
355
- //
356
- // ✓ `fetch/register`
357
- // ✓ `@gjsify/buffer/register`
358
- // ✓ `@gjsify/node-globals/register/buffer`
359
- // ✗ `register` (no `/` prefix)
360
- // ✗ `@scope/unregister` (the `/` is followed by `un`, not `register`)
361
- // ✗ `foo/register.js?query=1` (query-suffix → treat as resolved-path,
362
- // caught by the second branch only when
363
- // the file extension is intact)
270
+ // Source shape. The required leading `/` rules out the bare word `register`
271
+ // and `@scope/unregister`.
364
272
  if (/\/register(?:\/[^?]*)?$/.test(id)) {
365
273
  return true;
366
274
  }
367
- // Resolved disk-path shape — Rolldown sees these after the alias
368
- // plugin + node_modules resolver run. Matches both ESM build output
369
- // (`lib/esm/register/<feature>.js`) and any future TS-direct setup
370
- // that points the export at `src/register/<feature>.ts`. Strictly
371
- // requires the file extension at the end — a Rolldown synthetic-id
372
- // suffix like `?query=1` therefore does NOT match (those callers
373
- // expect to flow through the normal externals path).
275
+ // Resolved disk-path shape (post alias plugin + node_modules resolver), for
276
+ // both `lib/esm/register/<feature>.js` and a TS-direct `src/register/<feature>.ts` export. The
277
+ // extension is required, so a Rolldown synthetic-id suffix (`?query=1`) falls
278
+ // through to the normal externals path on purpose.
374
279
  if (/[/\\]register(?:[/\\][^/\\]+)?\.(?:[mc]?js|ts)$/.test(id)) {
375
280
  return true;
376
281
  }