@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.
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,55 @@ 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
- // ESM: omit 'require' — packages listing 'require' before 'import'
144
- // would silently route through their CJS entry.
107
+ // NEITHER 'import' NOR 'require' belongs here — rolldown adds the
108
+ // one matching each CALL SITE, and naming either explicitly applies
109
+ // it to both kinds. The exports-map resolver takes the PACKAGE's
110
+ // first key our list contains, so `'import'` here made a require()
111
+ // call match an `import` key declared first, handing a CJS consumer
112
+ // an ESM namespace where it expects `module.exports`. MEASURED on the
113
+ // express showcase: `is-promise@4` declares `[{import,require,default}, …]`
114
+ // in that order, so `router`'s `require('is-promise')` bound
115
+ // `{default: fn}` and every request threw `TypeError: n is not a
116
+ // function` — answered 200 anyway, and express logged `err.stack`,
117
+ // which carries no message line on SpiderMonkey, so the flagship
118
+ // slide printed ~59 anonymous frames per request naming nothing.
145
119
  //
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.
158
- conditionNames: format === 'esm' ? ['browser', 'import'] : ['browser', 'require', 'import'],
120
+ // `'node'` is absent for a different reason: it hands
121
+ // `cross-fetch-ponyfill` its Node-only entry, which imports
122
+ // `blobFrom`/`fileFrom` and breaks the bundle at link time. Packages
123
+ // that genuinely need their `node` export under GJS (so far only
124
+ // `unicorn-magic`'s `traversePathUp`) get an explicit alias.
125
+ conditionNames: ['browser'],
159
126
  },
160
127
  transform: {
161
- // Compile target: GJS 1.86 / SpiderMonkey 140 ≈ firefox140.
128
+ // GJS 1.86 / SpiderMonkey 140 ≈ firefox140.
162
129
  target: 'firefox140',
163
130
  define: {
164
131
  global: 'globalThis',
165
132
  window: 'globalThis',
166
133
  'process.env.READABLE_STREAM': '"disable"',
167
134
  },
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.
135
+ // Rewrite bare `console` to a named import from our shim. Rolldown's
136
+ // `inject` (not a register-style global write) because GJS defines
137
+ // `globalThis.console` non-writable + non-configurable, so assigning
138
+ // to it silently no-ops. Safe here where the auto-globals invariant
139
+ // forbids AST rewrites: console is replaced unconditionally, so there
140
+ // is no tree-shake-aware detection to defeat.
176
141
  ...(consoleShimPath ? { inject: { console: [consoleShimPath, 'console'] } } : {}),
177
142
  },
178
143
  output: {
179
144
  ...input.output,
180
145
  format,
181
146
  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.)
147
+ // App builds emit a single bundle file: dynamic imports inline and
148
+ // the whole program lands in one chunk matching `--outfile`.
187
149
  codeSplitting: false,
188
150
  },
189
151
  treeshake: true,
@@ -200,32 +162,26 @@ export const setupForGjs = async (input) => {
200
162
  // Virtual-entry plugin runs FIRST so its resolveId/load match the
201
163
  // synthetic input ids that `wrapInputWithSideEffects` produces.
202
164
  ...(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.
165
+ // Strip leading #! from every input module BEFORE bundling: a shebang in
166
+ // e.g. the CLI's own entry would end up mid-chunk after the process-stub
167
+ // banner, and acorn (the auto-globals detector) rejects the `#` byte.
168
+ // The final-output shebang is composed by shebangPlugin's renderChunk.
208
169
  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.
170
+ // random-access-file's 'browser' field maps to a throwing stub; the alias
171
+ // table forces the fs-backed Node entry.
212
172
  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.
173
+ // Transparent N-API `.node`-addon loader (the forward mirror of
174
+ // `gjsGiNodePlugin`): claims a native-addon acquisition
175
+ // (`bindings`/`node-gyp-build`/a direct `.node`/a napi-rs platform
176
+ // sibling) and rewrites it to a virtual module returning `loadAddon()`
177
+ // from `@gjsify/napi`, which is BUNDLED, not external. Order matters:
178
+ // after aliasPlugin so a user alias pinning an addon's native entry wins,
179
+ // before externalsPlugin so the acquisition is claimed first. Inert
180
+ // otherwise; `--app gjs` only.
223
181
  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.
182
+ // Externals policy as a resolveId hook — the only form BOTH engines
183
+ // honour. Runs after the alias plugin's `pre` resolveId so aliases apply
184
+ // first.
229
185
  externalsPlugin(external, { name: 'gjsify-gjs-externalize' }),
230
186
  blueprintPlugin(),
231
187
  deepkitPlugin({ reflection: input.pluginOptions.reflection }),
@@ -236,27 +192,24 @@ export const setupForGjs = async (input) => {
236
192
  cssAsStringPlugin({ targets: { firefox: 60 << 16 } }),
237
193
  nodeModulesPathRewritePlugin({ bundleDir, runtimeResolve: format === 'esm' }),
238
194
  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.
195
+ // resolveShebangLine returns null when disabled, else the resolved line
196
+ // with `${env:…}` expanded.
241
197
  (() => {
242
198
  const line = resolveShebangLine(input.shebang);
243
199
  return shebangPlugin({ enabled: line !== null, line: line ?? undefined });
244
200
  })(),
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.
201
+ // LAST claim on an id nothing else wanted (`order: 'post'`): an
202
+ // unresolvable `@gjsify/*` substitution is a build ERROR, not a silent
203
+ // external. Otherwise Rolldown externalises the ORIGINAL specifier, exits
204
+ // 0, and writes a bundle stock GJS aborts on at load (`ImportError:
205
+ // Unsupported URI scheme for importing: node`) — the same failure
206
+ // `utils/gjs-bundle-guard.ts` catches at emit time, caught here at its
207
+ // source with the importer and cause named.
252
208
  unresolvedWorkspaceImportPlugin({ target: 'gjs', aliases: aliasEntries, isExternal: external }),
253
209
  ];
254
210
  return { options, plugins };
255
211
  };
256
- /**
257
- * Flatten the legacy `Record<string, string>` alias map into the
258
- * `@rollup/plugin-alias` `entries` array shape, dropping empty values.
259
- */
212
+ /** Copy the alias map, dropping entries with an empty target. */
260
213
  function flattenAliases(map) {
261
214
  const out = {};
262
215
  for (const [from, to] of Object.entries(map)) {
@@ -266,28 +219,19 @@ function flattenAliases(map) {
266
219
  return out;
267
220
  }
268
221
  /**
269
- * Build the canonical `--app gjs` externals predicate:
222
+ * The canonical `--app gjs` externals predicate — used both for the in-process
223
+ * alias layer and as the predicate behind `externalsPlugin`. Pinned by
224
+ * `packages/infra/cli/src/auto-globals.spec.ts`.
270
225
  *
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.
226
+ * - `<pkg>/register[/<feature>]` MUST NEVER be externalized: GJS's ESM loader
227
+ * has no node_modules walker and does not follow `exports` maps for bare
228
+ * specifiers, so an externalized `import '@gjsify/buffer/register/buffer'`
229
+ * throws `Module not found` at runtime even with the file on disk. Matched by
230
+ * SHAPE (`isRegisterSubpath`) so it scales to every package following the
231
+ * tree-shakeable-globals convention, and short-circuited BEFORE the
232
+ * user-external check so `bundler.external` cannot override it.
233
+ * - `gi://*` external by prefix; `cairo`/`gettext`/`system` + user externals by
234
+ * exact name.
291
235
  */
292
236
  export function createGjsExternalsPredicate(userExternal = []) {
293
237
  const exact = ['cairo', 'gettext', 'system', ...userExternal];
@@ -302,27 +246,17 @@ export function createGjsExternalsPredicate(userExternal = []) {
302
246
  };
303
247
  }
304
248
  /**
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:
249
+ * Recognize the shims this plugin INJECTS into a `--app gjs` bundle, in both
250
+ * bare-specifier and resolved-disk-path form.
313
251
  *
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.
252
+ * Force-inlined for the same reason `/register` subpaths are, with the same
253
+ * failure: GJS has no `require` and its ESM loader follows neither node_modules
254
+ * nor `exports` maps, so an externalized shim aborts at load with `Calling
255
+ * \`require\` for "…/shims/module-resolve" in an environment that doesn't expose
256
+ * the \`require\` function`. The shim is never optional — the bundler injected it
257
+ * because the bundle needs it. Short-circuits BEFORE the user-external check, and
258
+ * matches by SHAPE so a later shim is covered without touching this predicate.
259
+ * Deliberately scoped to OUR shim directory only.
326
260
  */
327
261
  export function isGjsifyShim(id) {
328
262
  // Bare/fully-qualified: `@gjsify/rolldown-plugin-gjsify/shims/<name>`.
@@ -332,45 +266,24 @@ export function isGjsifyShim(id) {
332
266
  return /[\\/]rolldown-plugin-gjsify[\\/](?:lib|src)[\\/]shims[\\/]/.test(id);
333
267
  }
334
268
  /**
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*`
269
+ * Recognize the `/register[/<feature>]` subpath shapes `--globals auto` injects
270
+ * as side-effect imports, in bare, fully-qualified and resolved-disk-path form.
343
271
  *
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.
272
+ * Used by the `--app gjs` externals predicate to force-inline these even when the
273
+ * user lists them in `bundler.external`. Pinned by
274
+ * `packages/infra/cli/src/auto-globals.spec.ts`; keep in sync with AGENTS.md
275
+ * § Tree-shakeable globals.
349
276
  */
350
277
  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)
278
+ // Source shape. The required leading `/` rules out the bare word `register`
279
+ // and `@scope/unregister`.
364
280
  if (/\/register(?:\/[^?]*)?$/.test(id)) {
365
281
  return true;
366
282
  }
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).
283
+ // Resolved disk-path shape (post alias plugin + node_modules resolver), for
284
+ // both `lib/esm/register/<feature>.js` and a TS-direct `src/register/<feature>.ts` export. The
285
+ // extension is required, so a Rolldown synthetic-id suffix (`?query=1`) falls
286
+ // through to the normal externals path on purpose.
374
287
  if (/[/\\]register(?:[/\\][^/\\]+)?\.(?:[mc]?js|ts)$/.test(id)) {
375
288
  return true;
376
289
  }