@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/node.d.ts CHANGED
@@ -1,27 +1,20 @@
1
1
  import type { RolldownOptions, RolldownPluginOption } from 'rolldown';
2
2
  import type { PluginOptions } from '../types/plugin-options.js';
3
3
  /**
4
- * Reverse-bridge register routing: a node build of a GENUINE GJS SOURCE needs
5
- * the REAL `@gjsify/*` register bodies (document, HTMLCanvasElement, the `'2d'`
6
- * context factory, matchMedia, …) running over `@gjsify/node-gi`, not the
7
- * default `@gjsify/empty` stubs that keep CROSS-PLATFORM node bundles loadable
8
- * on plain Node.
9
- *
10
- * Applied by `setupForNode` when `isGjsSourceBuild` is true — see the comment
11
- * on that flag for why BOTH reverse-bridge signals qualify, not just the
12
- * explicit `--globals` stub.
4
+ * Reverse-bridge register routing: a node build of a GENUINE GJS SOURCE needs the
5
+ * REAL `@gjsify/*` register bodies (document, HTMLCanvasElement, the `'2d'` context
6
+ * factory, matchMedia, …) over `@gjsify/node-gi`, not the default `@gjsify/empty`
7
+ * stubs that keep CROSS-PLATFORM node bundles loadable on plain Node.
13
8
  *
14
9
  * Two adjustments over the standard node alias map:
15
- * 1. every `<pkg>/register…` → `@gjsify/empty` entry is DROPPED so the
16
- * register import resolves to its real body;
17
- * 2. the gjs target's bare→scoped register routes (`fetch/register/fetch` →
18
- * `@gjsify/fetch/register/fetch`, `xmlhttprequest/register` →
10
+ * 1. every `<pkg>/register…` → `@gjsify/empty` entry is DROPPED so the register
11
+ * import resolves to its real body;
12
+ * 2. the gjs target's bare→scoped register routes (`xmlhttprequest/register` →
19
13
  * `@gjsify/fetch/register/xhr`, …) are merged in so the inject stub's bare
20
14
  * specifiers resolve on node exactly as they do on gjs.
21
15
  *
22
- * Applied to the BASE map only — `pluginOptions.aliases` / user aliases still
23
- * override. On a non-reverse-bridge build the map is untouched (the plain-Node
24
- * loadability guarantee for cross-platform packages is not regressed).
16
+ * Applied to the BASE map only, when `isGjsSourceBuild` — `pluginOptions.aliases` /
17
+ * user aliases still override, and a non-reverse-bridge build is untouched.
25
18
  */
26
19
  export declare function enableGjsRegistersForNode(baseAliases: Record<string, string>): Record<string, string>;
27
20
  /**
@@ -30,34 +23,31 @@ export declare function enableGjsRegistersForNode(baseAliases: Record<string, st
30
23
  *
31
24
  * Two signals qualify, and BOTH must gate the same things:
32
25
  * - `nodeGiGlobalsInject` — the CLI's post-tree-shake detection found bare GJS
33
- * ambient globals (`print`/`imports`/`ARGV`/…) OR a static
34
- * `@gjsify/node-gi/*` import (the externalised spelling of the bare
35
- * `system`/`gettext`/`cairo` built-ins) in the bundled output;
36
- * - `registerInject` (the `--globals` auto/explicit inject stub) — the user
37
- * asked for GJS registers on the node target explicitly.
26
+ * ambient globals (`print`/`imports`/`ARGV`/…) or a static `@gjsify/node-gi/*`
27
+ * import (the externalised spelling of the bare built-ins) in the output;
28
+ * - `registerInject` — the `--globals` inject stub, i.e. the user asked for GJS
29
+ * registers on the node target explicitly.
38
30
  *
39
- * This predicate exists as ONE named function because the two consumers used to
40
- * ask the question separately and disagree: `emitGirs` took the union while the
41
- * register-alias lift took only the explicit stub. A reverse-bridge build then
42
- * got its `@girs/*` routed through to `requireGi` while a `/register`
43
- * side-effect import in the SAME graph was still emptied to `@gjsify/empty` —
44
- * silently dropping `@gjsify/dom-elements/register/canvas`'s `'2d'` context
45
- * factory, so `@gjsify/canvas2d`'s `Canvas2DBridge` never fired `onReady`.
31
+ * ONE named function because the two consumers used to ask separately and
32
+ * disagree: with `emitGirs` taking the union and the register-alias lift taking
33
+ * only the explicit stub, a reverse-bridge build got `@girs/*` routed to
34
+ * `requireGi` while a `/register` import in the SAME graph was still emptied to
35
+ * `@gjsify/empty` — silently dropping `@gjsify/dom-elements/register/canvas`'s
36
+ * `'2d'` context factory, so `@gjsify/canvas2d`'s `Canvas2DBridge` never fired
37
+ * `onReady`.
46
38
  *
47
- * A build with NEITHER signal is byte-unchanged: the `@gjsify/empty` register
48
- * routing and the `@girs/*` emptying both stay, so a cross-platform package's
49
- * node bundle keeps loading on plain Node without node-gi installed.
39
+ * A build with NEITHER signal is byte-unchanged: register routing and `@girs/*`
40
+ * emptying both stay, so a cross-platform package's node bundle keeps loading on
41
+ * plain Node without node-gi installed.
50
42
  *
51
- * KNOWN NARROWNESS (pre-dates this predicate, tracked in status/open-todos.md): the
52
- * `nodeGiGlobalsInject` detector keys on bare ambient globals + static
53
- * `@gjsify/node-gi/*` imports, so a genuine GJS source whose ONLY platform
54
- * reach is `gi://` (no ambient global, no bare `system`/`gettext`/`cairo`) is
55
- * still not recognised — its `@girs/*` AND its registers are both emptied. A
56
- * surviving `gi://` import CANNOT simply become a third signal: its shim loads
57
- * node-gi lazily precisely so a gjs-gated `gi://` import in a cross-platform
58
- * package keeps that package's node bundle loadable on plain Node (#641,
59
- * pinned by `tests/e2e/node-gi-globals-inject`), and injecting the eager
60
- * globals shim for it would break exactly that.
43
+ * KNOWN NARROWNESS (tracked in status/open-todos.md): a genuine GJS source whose
44
+ * ONLY platform reach is `gi://` (no ambient global, no bare built-in) is not
45
+ * recognised — its `@girs/*` and its registers are both emptied. A surviving
46
+ * `gi://` import cannot simply become a third signal: its shim loads node-gi
47
+ * lazily precisely so a gjs-gated `gi://` import keeps a cross-platform package's
48
+ * node bundle loadable on plain Node (#641, pinned by
49
+ * `tests/e2e/node-gi-globals-inject`), and injecting the eager globals shim would
50
+ * break exactly that.
61
51
  */
62
52
  export declare function isGjsSourceBuild(options: {
63
53
  nodeGiGlobalsInject?: boolean;
package/lib/app/node.js CHANGED
@@ -1,9 +1,4 @@
1
1
  // `--app node` Rolldown configuration factory.
2
- //
3
- // Same external set + alias map as the esbuild predecessor. The
4
- // `createRequire` banner that esbuild needed for ESM-output CJS interop
5
- // translates to Rolldown's `output.banner` directly — Rolldown itself does
6
- // not synthesise a `require()` shim for ESM consumers of bundled CJS code.
7
2
  import { aliasPlugin } from '../plugins/alias.js';
8
3
  import blueprintPlugin from '@gjsify/vite-plugin-blueprint';
9
4
  import { deepkitPlugin } from '@gjsify/rolldown-plugin-deepkit';
@@ -18,60 +13,46 @@ import { unresolvedWorkspaceImportPlugin } from '../plugins/unresolved-workspace
18
13
  import { wrapInputWithSideEffects } from '../utils/entry-wrapper.js';
19
14
  /**
20
15
  * Side-effect specifier the node target injects when GJS ambient globals
21
- * (`print`/`imports`/…) are detected in the bundled output. Importing it seeds
22
- * those globals on `globalThis` via the node-gi backend. Kept EXTERNAL (see the
23
- * external set below) — it loads the native addon and must never be bundled.
16
+ * (`print`/`imports`/…) are detected in the bundled output; importing it seeds
17
+ * them on `globalThis` via the node-gi backend. Kept EXTERNAL — it loads the
18
+ * native addon and must never be bundled.
24
19
  */
25
20
  const NODE_GI_GLOBALS_SPECIFIER = '@gjsify/node-gi/globals';
26
21
  /**
27
- * The node-gi standalone GJS built-in modules. The bare `system` / `gettext` /
28
- * `cairo` specifiers a GJS source imports are aliased to these (via
29
- * `ALIASES_GJS_FOR_NODE`, applied node-target-only by `gjsBuiltinModulesNodePlugin`)
30
- * and, like `@gjsify/node-gi/gi`, kept EXTERNAL — they load the native node-gi
31
- * backend and must resolve at runtime against the consumer's node_modules, never
32
- * be bundled.
22
+ * The node-gi standalone GJS built-in modules — the targets `ALIASES_GJS_FOR_NODE`
23
+ * rewrites a GJS source's bare `system`/`gettext`/`cairo` imports to (node-target
24
+ * only, via `gjsBuiltinModulesNodePlugin`). Kept EXTERNAL like
25
+ * `@gjsify/node-gi/gi`: they load the native node-gi backend and must resolve at
26
+ * runtime against the consumer's node_modules.
33
27
  *
34
- * DERIVED from `ALIASES_GJS_FOR_NODE`'s VALUES so this set can never drift from
35
- * the alias map again — a new bare built-in added there is externalised
36
- * automatically. That completeness is LOAD-BEARING under `@gjsify/rolldown-native`
37
- * (the GJS bundler engine): its JSON options boundary silently DROPS the resolveId
38
- * `{ external: true }` flag that `gjsBuiltinModulesNodePlugin` returns (only the
39
- * plain `external` string array survives serialization — the same reason
40
- * `app/gjs.ts` routes its externals through a resolveId hook AND filters the array).
41
- * So the rewritten `@gjsify/node-gi/<mod>` target stays external ONLY if it is in
42
- * this array. `@gjsify/node-gi/cairo` was previously MISSING here (only `system` +
43
- * `gettext` were listed), so under the GJS bundler `cairo` was left as a bare,
44
- * unresolvable specifier — breaking every Cairo/PangoCairo consumer
45
- * (`@gjsify/canvas2d-core`) built via the reverse bridge. Under npm `rolldown`
46
- * (Node) the resolveId `external` flag IS honoured, so the gap was invisible until
47
- * a Cairo consumer was built on the GJS bundler. e2e: `tests/e2e/node-gi-build`;
48
- * unit: `packages/infra/cli/src/node-gi-externals.spec.ts`.
28
+ * DERIVED from the alias map's VALUES so the two cannot drift. That completeness
29
+ * is LOAD-BEARING under `@gjsify/rolldown-native`: its JSON options boundary drops
30
+ * the resolveId `{ external: true }` flag `gjsBuiltinModulesNodePlugin` returns
31
+ * (only the plain `external` string array survives), so a rewritten
32
+ * `@gjsify/node-gi/<mod>` stays external ONLY if it is in this array. A hardcoded
33
+ * list missing `cairo` left it bare and unresolvable under the GJS bundler,
34
+ * breaking every Cairo/PangoCairo reverse-bridge consumer — invisible under npm
35
+ * `rolldown`, which does honour the flag. e2e `tests/e2e/node-gi-build`; unit
36
+ * `packages/infra/cli/src/node-gi-externals.spec.ts`.
49
37
  */
50
38
  const NODE_GI_BARE_MODULE_SPECIFIERS = Object.values(ALIASES_GJS_FOR_NODE);
51
39
  /** Matches a register subpath in an alias key: `<pkg>/register` or `<pkg>/register/<feature>`. */
52
40
  const REGISTER_SUBPATH_RE = /\/register(\/|$)/;
53
41
  /**
54
- * Reverse-bridge register routing: a node build of a GENUINE GJS SOURCE needs
55
- * the REAL `@gjsify/*` register bodies (document, HTMLCanvasElement, the `'2d'`
56
- * context factory, matchMedia, …) running over `@gjsify/node-gi`, not the
57
- * default `@gjsify/empty` stubs that keep CROSS-PLATFORM node bundles loadable
58
- * on plain Node.
59
- *
60
- * Applied by `setupForNode` when `isGjsSourceBuild` is true — see the comment
61
- * on that flag for why BOTH reverse-bridge signals qualify, not just the
62
- * explicit `--globals` stub.
42
+ * Reverse-bridge register routing: a node build of a GENUINE GJS SOURCE needs the
43
+ * REAL `@gjsify/*` register bodies (document, HTMLCanvasElement, the `'2d'` context
44
+ * factory, matchMedia, …) over `@gjsify/node-gi`, not the default `@gjsify/empty`
45
+ * stubs that keep CROSS-PLATFORM node bundles loadable on plain Node.
63
46
  *
64
47
  * Two adjustments over the standard node alias map:
65
- * 1. every `<pkg>/register…` → `@gjsify/empty` entry is DROPPED so the
66
- * register import resolves to its real body;
67
- * 2. the gjs target's bare→scoped register routes (`fetch/register/fetch` →
68
- * `@gjsify/fetch/register/fetch`, `xmlhttprequest/register` →
48
+ * 1. every `<pkg>/register…` → `@gjsify/empty` entry is DROPPED so the register
49
+ * import resolves to its real body;
50
+ * 2. the gjs target's bare→scoped register routes (`xmlhttprequest/register` →
69
51
  * `@gjsify/fetch/register/xhr`, …) are merged in so the inject stub's bare
70
52
  * specifiers resolve on node exactly as they do on gjs.
71
53
  *
72
- * Applied to the BASE map only — `pluginOptions.aliases` / user aliases still
73
- * override. On a non-reverse-bridge build the map is untouched (the plain-Node
74
- * loadability guarantee for cross-platform packages is not regressed).
54
+ * Applied to the BASE map only, when `isGjsSourceBuild` — `pluginOptions.aliases` /
55
+ * user aliases still override, and a non-reverse-bridge build is untouched.
75
56
  */
76
57
  export function enableGjsRegistersForNode(baseAliases) {
77
58
  const out = {};
@@ -92,74 +73,53 @@ export function enableGjsRegistersForNode(baseAliases) {
92
73
  *
93
74
  * Two signals qualify, and BOTH must gate the same things:
94
75
  * - `nodeGiGlobalsInject` — the CLI's post-tree-shake detection found bare GJS
95
- * ambient globals (`print`/`imports`/`ARGV`/…) OR a static
96
- * `@gjsify/node-gi/*` import (the externalised spelling of the bare
97
- * `system`/`gettext`/`cairo` built-ins) in the bundled output;
98
- * - `registerInject` (the `--globals` auto/explicit inject stub) — the user
99
- * asked for GJS registers on the node target explicitly.
76
+ * ambient globals (`print`/`imports`/`ARGV`/…) or a static `@gjsify/node-gi/*`
77
+ * import (the externalised spelling of the bare built-ins) in the output;
78
+ * - `registerInject` — the `--globals` inject stub, i.e. the user asked for GJS
79
+ * registers on the node target explicitly.
100
80
  *
101
- * This predicate exists as ONE named function because the two consumers used to
102
- * ask the question separately and disagree: `emitGirs` took the union while the
103
- * register-alias lift took only the explicit stub. A reverse-bridge build then
104
- * got its `@girs/*` routed through to `requireGi` while a `/register`
105
- * side-effect import in the SAME graph was still emptied to `@gjsify/empty` —
106
- * silently dropping `@gjsify/dom-elements/register/canvas`'s `'2d'` context
107
- * factory, so `@gjsify/canvas2d`'s `Canvas2DBridge` never fired `onReady`.
81
+ * ONE named function because the two consumers used to ask separately and
82
+ * disagree: with `emitGirs` taking the union and the register-alias lift taking
83
+ * only the explicit stub, a reverse-bridge build got `@girs/*` routed to
84
+ * `requireGi` while a `/register` import in the SAME graph was still emptied to
85
+ * `@gjsify/empty` — silently dropping `@gjsify/dom-elements/register/canvas`'s
86
+ * `'2d'` context factory, so `@gjsify/canvas2d`'s `Canvas2DBridge` never fired
87
+ * `onReady`.
108
88
  *
109
- * A build with NEITHER signal is byte-unchanged: the `@gjsify/empty` register
110
- * routing and the `@girs/*` emptying both stay, so a cross-platform package's
111
- * node bundle keeps loading on plain Node without node-gi installed.
89
+ * A build with NEITHER signal is byte-unchanged: register routing and `@girs/*`
90
+ * emptying both stay, so a cross-platform package's node bundle keeps loading on
91
+ * plain Node without node-gi installed.
112
92
  *
113
- * KNOWN NARROWNESS (pre-dates this predicate, tracked in status/open-todos.md): the
114
- * `nodeGiGlobalsInject` detector keys on bare ambient globals + static
115
- * `@gjsify/node-gi/*` imports, so a genuine GJS source whose ONLY platform
116
- * reach is `gi://` (no ambient global, no bare `system`/`gettext`/`cairo`) is
117
- * still not recognised — its `@girs/*` AND its registers are both emptied. A
118
- * surviving `gi://` import CANNOT simply become a third signal: its shim loads
119
- * node-gi lazily precisely so a gjs-gated `gi://` import in a cross-platform
120
- * package keeps that package's node bundle loadable on plain Node (#641,
121
- * pinned by `tests/e2e/node-gi-globals-inject`), and injecting the eager
122
- * globals shim for it would break exactly that.
93
+ * KNOWN NARROWNESS (tracked in status/open-todos.md): a genuine GJS source whose
94
+ * ONLY platform reach is `gi://` (no ambient global, no bare built-in) is not
95
+ * recognised — its `@girs/*` and its registers are both emptied. A surviving
96
+ * `gi://` import cannot simply become a third signal: its shim loads node-gi
97
+ * lazily precisely so a gjs-gated `gi://` import keeps a cross-platform package's
98
+ * node bundle loadable on plain Node (#641, pinned by
99
+ * `tests/e2e/node-gi-globals-inject`), and injecting the eager globals shim would
100
+ * break exactly that.
123
101
  */
124
102
  export function isGjsSourceBuild(options) {
125
103
  return Boolean(options.nodeGiGlobalsInject || options.registerInject);
126
104
  }
127
105
  export const setupForNode = async (input) => {
128
106
  const userExternal = input.userExternal ?? [];
129
- // node-datachannel is a native C++ addon that cannot be bundled — its
130
- // `require('../build/Release/node_datachannel.node')` must resolve at
131
- // runtime against the real node_modules tree.
107
+ // node-datachannel and `@gjsify/node-gi` are native addons that cannot be
108
+ // bundled — their loaders resolve a `.node` binary relative to their own
109
+ // installed location, so both must stay external.
132
110
  //
133
- // GJS-specific specifiers (`gi://*`, `@girs/*`) are NOT externalised —
134
- // `gi://*` is rewritten to `requireGi` by `gjsGiNodePlugin`, and `@girs/*`
135
- // is handled by `gjsGiNodePlugin` + `gjsImportsEmptyPlugin` (both in the
136
- // plugins array below). Marking them external would leave bare
137
- // `import 'gi://Gio?version=2.0'` strings in the output that Node's default
138
- // ESM loader rejects with `ERR_UNSUPPORTED_ESM_URL_SCHEME`. For a
139
- // cross-platform package the `@girs/*` empty-module redirect makes node
140
- // bundles (which transitively import @girs/* via *.gjs.spec / direct
141
- // internal imports) loadable on Node — the GJS-only code paths are still
142
- // gated at runtime by `on('Gjs', …)` or `typeof globalThis.imports` guards.
143
- // For a GENUINE GJS source built via the node-gi reverse bridge (detected
144
- // by `nodeGiGlobalsInject`) the `@girs/*` empty redirect is OFF so the
145
- // namespace resolves through to `requireGi` — see the plugins array.
146
- // `@gjsify/node-gi` is the Axis-5 GI runtime the `gjsGiNodePlugin` rewrites
147
- // `gi://` onto. It is a native (node-gyp) addon whose loader resolves its
148
- // `.node` binary relative to its own installed location, so it must NOT be
149
- // bundled — keep it external so the `import … from '@gjsify/node-gi/gi'` the
150
- // virtual shim emits resolves at runtime against the consumer's node_modules.
111
+ // GJS-specific specifiers (`gi://*`, `@girs/*`) are deliberately NOT
112
+ // externalised: `gjsGiNodePlugin` rewrites `gi://` to `requireGi`, and
113
+ // `@girs/*` is handled by `gjsGiNodePlugin` + `gjsImportsEmptyPlugin`.
114
+ // Externalising them would leave bare `import 'gi://Gio?version=2.0'` in the
115
+ // output, which Node's ESM loader rejects with
116
+ // `ERR_UNSUPPORTED_ESM_URL_SCHEME`.
151
117
  const exactExternal = [
152
118
  ...EXTERNALS_NODE,
153
119
  'node-datachannel',
154
120
  '@gjsify/node-gi',
155
121
  '@gjsify/node-gi/gi',
156
- // The GJS-ambient-globals shim (auto-injected below when `print`/
157
- // `imports`/… are referenced). Like `@gjsify/node-gi/gi` it loads the
158
- // native addon and must resolve at runtime, never be bundled.
159
122
  NODE_GI_GLOBALS_SPECIFIER,
160
- // The node-gi standalone GJS built-in modules (`system`/`gettext`),
161
- // reached when the alias layer rewrites the bare specifiers. Kept
162
- // external for the same reason as the gi/globals shims above.
163
123
  ...NODE_GI_BARE_MODULE_SPECIFIERS,
164
124
  ...userExternal,
165
125
  ];
@@ -173,16 +133,11 @@ export const setupForNode = async (input) => {
173
133
  const format = input.pluginOptions.format ?? 'esm';
174
134
  const exclude = input.pluginOptions.exclude ?? [];
175
135
  const entryPoints = await globToEntryPoints(input.input, exclude);
176
- // When the CLI's pre-build detection (`detectNodeGiGlobals`) recognised a
177
- // genuine GJS source (bare ambient globals OR a static `@gjsify/node-gi/*`
178
- // import in the bundled output), wrap the entry so it side-effect imports
179
- // the node-gi globals shim BEFORE the user code runs. The import is
180
- // injected ONLY when detected — a bundle that neither touches
181
- // `print`/`imports` nor retains a bare `system`/`gettext`/`cairo` import
182
- // stays free of `@gjsify/node-gi/globals`, so it loads on plain Node
183
- // without node-gi installed (the eager-native-load regression guard, same
184
- // lesson as the lazy `gi://` shim in #641). Reuses the SAME virtual-entry
185
- // wrapper the GJS `--globals auto` machinery uses.
136
+ // When `detectNodeGiGlobals` recognised a genuine GJS source, wrap the entry
137
+ // so it side-effect imports the node-gi globals shim before user code runs —
138
+ // ONLY then, so an undetected bundle stays free of `@gjsify/node-gi/globals`
139
+ // and loads on plain Node without node-gi installed (the eager-native-load
140
+ // guard, same lesson as the lazy `gi://` shim in #641).
186
141
  const sideEffectImports = [];
187
142
  if (input.pluginOptions.nodeGiGlobalsInject)
188
143
  sideEffectImports.push(NODE_GI_GLOBALS_SPECIFIER);
@@ -194,35 +149,9 @@ export const setupForNode = async (input) => {
194
149
  sideEffectImports.push(registerInject);
195
150
  const virtualEntries = wrapInputWithSideEffects(entryPoints, sideEffectImports);
196
151
  const finalInput = virtualEntries.input;
197
- /**
198
- * Is this a GENUINE GJS SOURCE built via the node-gi reverse bridge (as
199
- * opposed to a cross-platform package's plain-Node bundle)?
200
- *
201
- * TWO signals answer that ONE question and both must count:
202
- *
203
- * - `nodeGiGlobalsInject` — the CLI detected BARE GJS ambient globals
204
- * (`print`/`imports`/`ARGV`) or a static `@gjsify/node-gi/*` import
205
- * (a retained bare `system`/`gettext`/`cairo` built-in) in the
206
- * tree-shaken output. Only genuine GJS source has those (the detector
207
- * deliberately ignores the `globalThis.imports` isomorphic-guard shape).
208
- * - `autoGlobalsInject` — the user named EXPLICIT `--globals` (e.g.
209
- * `--globals auto,dom`), i.e. asked for the DOM surface on Node.
210
- *
211
- * Both already gate `emptyGirs` below. The register routing used to consult
212
- * ONLY the explicit stub, and that asymmetry was a
213
- * bug: a reverse-bridge build got its `@girs/*` value imports routed
214
- * through to `requireGi` while a `/register` side-effect import in the very
215
- * same graph was still emptied to `@gjsify/empty`. It surfaced as
216
- * `@gjsify/canvas2d`'s `Canvas2DBridge` silently losing the `'2d'` context
217
- * factory (`@gjsify/dom-elements/register/canvas` → empty → `getContext('2d')`
218
- * null → `onReady` never fires) on a plain `gjsify build … --app node`, with
219
- * no `--globals` flag anywhere — guarded by the node-gi
220
- * `test/canvas2d-bridge.test.mjs` golden.
221
- *
222
- * A build with NEITHER signal is byte-unchanged: the `@gjsify/empty`
223
- * register routing stays, so a cross-platform package's node bundle keeps
224
- * loading on plain Node without node-gi installed.
225
- */
152
+ // Gates both the register-alias lift and `emptyGirs` below — see the
153
+ // predicate's doc comment. The node-gi golden `test/canvas2d-bridge.test.mjs`
154
+ // pins the asymmetry it fixed.
226
155
  const gjsSourceBuild = isGjsSourceBuild({ ...input.pluginOptions, registerInject });
227
156
  const baseAliases = getAliasesForNode({ external });
228
157
  const aliasMap = {
@@ -235,42 +164,29 @@ export const setupForNode = async (input) => {
235
164
  // promise the alias layer made.
236
165
  const aliasEntries = flattenAliases(aliasMap);
237
166
  const bundleDir = getBundleDirFromOutput(input.output);
238
- // Rolldown's CJS interop wraps bundled CJS via `__commonJSMin` and
239
- // routes external Node-builtin `require()` through `__require` —
240
- // both injected internally. Unlike esbuild we therefore don't need a
241
- // top-of-bundle `const require = createRequire(...)` shim. Keeping
242
- // one collides with bundled CJS sources that declare their own
243
- // `const require = createRequire(...)` (e.g. yargs's ESM platform
244
- // shim) — `SyntaxError: Identifier 'require' has already been
245
- // declared`.
167
+ // No banner on purpose: Rolldown injects its own CJS interop (`__commonJSMin`,
168
+ // `__require`), so a top-of-bundle `const require = createRequire(...)` is both
169
+ // unnecessary and harmful — it collides with bundled CJS sources declaring
170
+ // their own (yargs's ESM platform shim): `SyntaxError: Identifier 'require'
171
+ // has already been declared`.
246
172
  const banner = undefined;
247
173
  const options = {
248
174
  input: finalInput,
249
175
  platform: 'node',
250
- // Pass the EXACT-MATCH external set as a plain string array, NOT the
251
- // `external` predicate function. `@gjsify/rolldown-native` ships the
252
- // whole options object to its Rust core via `JSON.stringify` — a
253
- // function value does not survive serialization (it is silently
254
- // dropped), so a function `external` is honoured under npm rolldown
255
- // (Node) but IGNORED under native rolldown (GJS), leaving
256
- // `node-datachannel` (and the rest of EXTERNALS_NODE) bundled. That
257
- // descends into node-datachannel's `require('…/node_datachannel.node')`
258
- // and fails the `--app node` build with `Module not found`. The
259
- // predicate here is pure exact membership, so the array is
260
- // behaviourally identical under both engines AND JSON-serializable.
261
- // (The gjs target follows the same rule: exact names as an array,
262
- // plus an `externalsPlugin` resolveId hook for its gi://-prefix and
263
- // register-subpath shape rules — see app/gjs.ts. The browser target
264
- // already uses a plain array.)
265
- // The function form is still used for `getAliasesForNode({ external })`
266
- // above — that runs in-process and is never serialized.
176
+ // A plain string array, never the `external` predicate function:
177
+ // `@gjsify/rolldown-native` JSON.stringify's the options to its Rust core,
178
+ // so a function value is silently dropped and every `EXTERNALS_NODE` entry
179
+ // gets bundled — the build then descends into node-datachannel's
180
+ // `require('…/node_datachannel.node')` and fails with `Module not found`.
181
+ // Exact membership makes the array behaviourally identical on both engines.
182
+ // The function form survives only for `getAliasesForNode({ external })`,
183
+ // which runs in-process and is never serialized.
267
184
  external: exactExternal,
268
185
  resolve: {
269
186
  mainFields: format === 'esm' ? ['module', 'main', 'browser'] : ['main', 'module', 'browser'],
270
- // CJS-priority conditions for Node bundles. Rolldown uses the first
271
- // matching key, so including 'import' would route packages like ws
272
- // v8 (whose exports map lists 'import' before 'require') through
273
- // their incomplete ESM wrapper.
187
+ // CJS-priority conditions. Rolldown takes the package's first matching
188
+ // key, so adding 'import' would route ws v8 (which lists 'import'
189
+ // before 'require') through its incomplete ESM wrapper.
274
190
  conditionNames: format === 'esm' ? ['require', 'node', 'module'] : ['require'],
275
191
  },
276
192
  transform: {
@@ -285,76 +201,57 @@ export const setupForNode = async (input) => {
285
201
  format,
286
202
  sourcemap: false,
287
203
  banner,
288
- // Single-bundle output. `codeSplitting: false` replaces the
289
- // deprecated `inlineDynamicImports: true`.
204
+ // Single-bundle output.
290
205
  codeSplitting: false,
291
206
  },
292
207
  treeshake: true,
293
208
  };
294
209
  const plugins = [
295
- // Virtual-entry plugin runs FIRST so its resolveId/load match the
296
- // synthetic `\0gjsify-entry:` ids that `wrapInputWithSideEffects`
297
- // produces (no-op when nothing was injected).
210
+ // Virtual-entry plugin runs FIRST so its resolveId/load match the synthetic
211
+ // `\0gjsify-entry:` ids `wrapInputWithSideEffects` produces (no-op when
212
+ // nothing was injected).
298
213
  ...(virtualEntries.plugin ? [virtualEntries.plugin] : []),
299
- // gjsGiNodePlugin runs FIRST (resolveId order 'pre' + array order): it
300
- // claims `gi://Ns?version=X` and rewrites it to the `@gjsify/node-gi`
301
- // runtime, so a real GJS/GI source builds + runs on Node. It returns
302
- // null for `@girs/*`.
214
+ // Claims `gi://Ns?version=X` (resolveId `pre` + array order) and rewrites it
215
+ // onto the `@gjsify/node-gi` runtime so a real GJS/GI source builds and runs
216
+ // on Node. Returns null for `@girs/*`.
303
217
  gjsGiNodePlugin(),
304
- // Bare GJS built-in modules (`system`/`gettext`/`cairo`): claimed BEFORE
305
- // the aliasPlugin so they resolve to the EXTERNAL `@gjsify/node-gi/<mod>`
306
- // shims without a disk resolution (works without node-gi installed,
307
- // like gi://). Node-target-only — never active on gjs/browser/ns. The
308
- // externalisation itself rides `NODE_GI_BARE_MODULE_SPECIFIERS` in the
309
- // `exactExternal` array (the resolveId `external` flag is dropped under
310
- // @gjsify/rolldown-native — see that const's doc-comment).
218
+ // Bare GJS built-ins: claimed BEFORE aliasPlugin so they resolve to the
219
+ // EXTERNAL `@gjsify/node-gi/<mod>` shims without touching disk (works
220
+ // without node-gi installed, like gi://). Node-target-only. The
221
+ // externalisation itself rides `NODE_GI_BARE_MODULE_SPECIFIERS` in
222
+ // `exactExternal` — see that const's doc comment.
311
223
  gjsBuiltinModulesNodePlugin(ALIASES_GJS_FOR_NODE),
312
- // gjsImportsEmptyPlugin then decides the fate of `@girs/*` before
313
- // `aliasPlugin` and the default resolver. Same composition order as
314
- // `app/browser.ts`. `emptyGirs` is GATED on `nodeGiGlobalsInject`:
315
- // • false-flag (default Node build) → `@girs/*` → empty module. A
316
- // cross-platform polyfill package whose GJS-only code paths import
317
- // `@girs/*` transitively (e.g. via `@gjsify/unit`) stays loadable on
318
- // plain Node WITHOUT node-gi installed — exactly the existing
319
- // behaviour, never regressed.
320
- // • node-gi build (`nodeGiGlobalsInject` true — the genuine-GJS-source
321
- // signal) → `@girs/<ns>-<ver>` falls through to its real package
322
- // body (`import Adw from 'gi://Adw?version=1'; export default Adw`),
323
- // whose inner `gi://` is rewritten to `requireGi` by gjsGiNodePlugin
324
- // above. This routes `@girs/adw-1` → `requireGi('Adw','1')` WITHOUT
325
- // a lowercased-pkg→namespace map (the proper-cased namespace comes
326
- // from the @girs package's own `gi://` specifier). `gi://*` is
327
- // already claimed by gjsGiNodePlugin, so this plugin's `gi://`
328
- // branch is dead on the node target either way.
329
- // `emptyGirs` also lifts for the explicit-register reverse-bridge case:
330
- // the injected register bodies (dom-elements, canvas2d, …) are genuine
331
- // GJS code whose `@girs/*` value imports must route through to
332
- // `requireGi`, exactly like the ambient-globals case. Same predicate as
333
- // the register-alias lift above — one question, one flag.
224
+ // Decides the fate of `@girs/*` before `aliasPlugin` and the default
225
+ // resolver (same composition order as `app/browser.ts`). `emptyGirs` is
226
+ // gated on `gjsSourceBuild`:
227
+ // • plain Node build → `@girs/*` → empty module, so a cross-platform
228
+ // polyfill whose GJS-only paths import `@girs/*` transitively (e.g. via
229
+ // `@gjsify/unit`) stays loadable without node-gi installed.
230
+ // • reverse-bridge build → `@girs/<ns>-<ver>` falls through to its real
231
+ // package body (`import Adw from 'gi://Adw?version=1'`), whose inner
232
+ // `gi://` gjsGiNodePlugin already rewrote — so `@girs/adw-1` reaches
233
+ // `requireGi('Adw','1')` with no lowercased-pkg→namespace map needed.
234
+ // This plugin's own `gi://` branch is dead on the node target.
334
235
  gjsImportsEmptyPlugin({ emptyGirs: !gjsSourceBuild }),
335
236
  aliasPlugin({ entries: aliasEntries }),
336
- // Blueprint (.blp → XML string): the reverse bridge runs REAL GTK on
337
- // Node, so a GJS app entry (composite-template windows) must build for
338
- // `--app node` too. Only claims `.blp` files — inert for plain-Node
339
- // bundles. Same plugin the gjs/browser targets compose.
237
+ // Blueprint (.blp → XML string): the reverse bridge runs REAL GTK on Node,
238
+ // so a GJS app entry with composite-template windows must build for
239
+ // `--app node` too. Claims only `.blp` — inert for plain-Node bundles.
340
240
  blueprintPlugin(),
341
241
  deepkitPlugin({ reflection: input.pluginOptions.reflection }),
342
- // CSS-as-string. For a node-gi build (the reverse bridge runs REAL GTK on
343
- // Node — `nodeGiGlobalsInject`, the same signal that keeps `@girs/*` bodies
344
- // above), lower the CSS to GTK 4's subset with `firefox: 60 << 16` — exactly
345
- // as `--app gjs` does — so modern authoring (nesting, `&`, …) is FLATTENED
346
- // before it reaches `Gtk.CssProvider.load_from_string`. Without this GTK 4's
347
- // CSS parser rejects nested rules at runtime ("Expected ';'/an identifier"
348
- // theme-parser warnings, styles silently dropped — the Learn6502 app-gnome
349
- // symptom). A plain-Node bundle (no node-gi) keeps the CSS pristine.
242
+ // On a node-gi build (REAL GTK on Node) lower the CSS to GTK 4's subset just
243
+ // as `--app gjs` does, so nesting/`&` is FLATTENED before it reaches
244
+ // `Gtk.CssProvider.load_from_string` — GTK 4's parser otherwise rejects
245
+ // nested rules at runtime and silently drops the styles (the Learn6502
246
+ // app-gnome symptom). A plain-Node bundle keeps the CSS pristine.
350
247
  cssAsStringPlugin(input.pluginOptions.nodeGiGlobalsInject ? { targets: { firefox: 60 << 16 } } : {}),
351
248
  nodeModulesPathRewritePlugin({ bundleDir }),
352
- // `order: 'post'` — fires only for ids nothing above claimed, i.e. the
353
- // ones headed for Rolldown's unresolved-import → external fallback. The
354
- // node target has no `node:`-shaped symptom for the emit-time guard to
355
- // detect (its `node:` builtins ARE external by policy), so this is the
356
- // ONLY thing standing between an unbuilt `@gjsify/*` workspace edge and
357
- // a bundle that re-imports the bare specifier at runtime.
249
+ // `order: 'post'` — fires only for ids headed to Rolldown's
250
+ // unresolved-import → external fallback. The node target has no
251
+ // `node:`-shaped symptom the emit-time guard could detect (its `node:`
252
+ // builtins ARE external by policy), so this is the ONLY thing between an
253
+ // unbuilt `@gjsify/*` workspace edge and a bundle that re-imports the bare
254
+ // specifier at runtime.
358
255
  unresolvedWorkspaceImportPlugin({ target: 'node', aliases: aliasEntries, isExternal: external }),
359
256
  ];
360
257
  return { options, plugins };
@@ -331,8 +331,16 @@ let _sassPromise = null;
331
331
  async function compileSass(filename) {
332
332
  if (!_sassPromise) {
333
333
  // Lazy + indirect specifier so `sass` is only resolved when a Sass file
334
- // is actually imported (mirrors the lazy lightningcss backend). The
335
- // `dart-sass` JS API is pure JS, so it loads under GJS + Node alike.
334
+ // is actually imported (mirrors the lazy lightningcss backend).
335
+ //
336
+ // NODE ONLY — this comment used to claim "loads under GJS + Node alike".
337
+ // Measured on 0.37.0: under GJS it rejects with `Module not found: sass` — a
338
+ // BARE specifier resolved at RUNTIME is what GJS's ESM loader cannot do, so
339
+ // `import './x.scss'` fails with UNLOADABLE_DEPENDENCY. Resolving it to a file
340
+ // first (`tryLoadNativeBundler`'s trick) does not help: `sass.default.js` then
341
+ // imports a bare `immutable`. Only INLINING dart-sass works, and 3.6 MB minified
342
+ // onto a 6.6 MB CLI bundle is a carrier-package decision — status/open-todos.md
343
+ // (#1053). dart-sass DOES run under GJS: `adwaita-web`'s `build:scss` does.
336
344
  _sassPromise = import(/* @vite-ignore */ 'sass');
337
345
  }
338
346
  try {