@gjsify/rolldown-plugin-gjsify 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  export { setupForGjs, isRegisterSubpath, createGjsExternalsPredicate } from './gjs.js';
2
2
  export type { GjsBuildConfig, GjsFactoryInput } from './gjs.js';
3
- export { setupForNode } from './node.js';
3
+ export { setupForNode, enableGjsRegistersForNode } from './node.js';
4
4
  export type { NodeBuildConfig, NodeFactoryInput } from './node.js';
5
5
  export { setupForBrowser } from './browser.js';
6
6
  export type { BrowserBuildConfig, BrowserFactoryInput } from './browser.js';
package/lib/app/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  export { setupForGjs, isRegisterSubpath, createGjsExternalsPredicate } from './gjs.js';
2
- export { setupForNode } from './node.js';
2
+ export { setupForNode, enableGjsRegistersForNode } from './node.js';
3
3
  export { setupForBrowser } from './browser.js';
4
4
  export { setupForNativescript } from './nativescript.js';
package/lib/app/node.d.ts CHANGED
@@ -1,5 +1,27 @@
1
1
  import type { RolldownOptions, RolldownPluginOption } from 'rolldown';
2
2
  import type { PluginOptions } from '../types/plugin-options.js';
3
+ /**
4
+ * Reverse-bridge register routing: a node build with an EXPLICIT `--globals`
5
+ * register-inject stub (`pluginOptions.autoGlobalsInject`, set by the CLI for
6
+ * `--app node` when the user names globals — e.g. `--globals auto,dom`) is a
7
+ * genuine GJS source that needs the REAL `@gjsify/*` register bodies (document,
8
+ * HTMLCanvasElement, matchMedia, …) running over `@gjsify/node-gi`, not the
9
+ * default `@gjsify/empty` stubs that keep cross-platform node bundles loadable
10
+ * on plain Node.
11
+ *
12
+ * Two adjustments over the standard node alias map:
13
+ * 1. every `<pkg>/register…` → `@gjsify/empty` entry is DROPPED so the
14
+ * register import resolves to its real body;
15
+ * 2. the gjs target's bare→scoped register routes (`fetch/register/fetch` →
16
+ * `@gjsify/fetch/register/fetch`, `xmlhttprequest/register` →
17
+ * `@gjsify/fetch/register/xhr`, …) are merged in so the inject stub's bare
18
+ * specifiers resolve on node exactly as they do on gjs.
19
+ *
20
+ * Applied to the BASE map only — `pluginOptions.aliases` / user aliases still
21
+ * override. Without the inject stub the map is untouched (the plain-Node
22
+ * loadability guarantee for cross-platform packages is not regressed).
23
+ */
24
+ export declare function enableGjsRegistersForNode(baseAliases: Record<string, string>): Record<string, string>;
3
25
  export interface NodeBuildConfig {
4
26
  options: RolldownOptions;
5
27
  plugins: RolldownPluginOption[];
package/lib/app/node.js CHANGED
@@ -5,8 +5,9 @@
5
5
  // translates to Rolldown's `output.banner` directly — Rolldown itself does
6
6
  // not synthesise a `require()` shim for ESM consumers of bundled CJS code.
7
7
  import { aliasPlugin } from '../plugins/alias.js';
8
+ import blueprintPlugin from '@gjsify/vite-plugin-blueprint';
8
9
  import { deepkitPlugin } from '@gjsify/rolldown-plugin-deepkit';
9
- import { EXTERNALS_NODE, ALIASES_GJS_FOR_NODE } from '@gjsify/resolve-npm';
10
+ import { EXTERNALS_NODE, ALIASES_GJS_FOR_NODE, ALIASES_WEB_FOR_GJS } from '@gjsify/resolve-npm';
10
11
  import { getAliasesForNode } from '../utils/alias.js';
11
12
  import { globToEntryPoints } from '../utils/entry-points.js';
12
13
  import { nodeModulesPathRewritePlugin, getBundleDirFromOutput } from '../plugins/rewrite-node-modules-paths.js';
@@ -22,13 +23,66 @@ import { wrapInputWithSideEffects } from '../utils/entry-wrapper.js';
22
23
  */
23
24
  const NODE_GI_GLOBALS_SPECIFIER = '@gjsify/node-gi/globals';
24
25
  /**
25
- * The node-gi standalone GJS built-in modules. The bare `system` / `gettext`
26
- * specifiers a GJS source imports are aliased to these (via `ALIASES_GJS_FOR_NODE`,
27
- * applied node-target-only) and, like `@gjsify/node-gi/gi`, kept EXTERNAL — they
28
- * load the native node-gi backend and must resolve at runtime against the
29
- * consumer's node_modules, never be bundled.
26
+ * The node-gi standalone GJS built-in modules. The bare `system` / `gettext` /
27
+ * `cairo` specifiers a GJS source imports are aliased to these (via
28
+ * `ALIASES_GJS_FOR_NODE`, applied node-target-only by `gjsBuiltinModulesNodePlugin`)
29
+ * and, like `@gjsify/node-gi/gi`, kept EXTERNAL — they load the native node-gi
30
+ * backend and must resolve at runtime against the consumer's node_modules, never
31
+ * be bundled.
32
+ *
33
+ * DERIVED from `ALIASES_GJS_FOR_NODE`'s VALUES so this set can never drift from
34
+ * the alias map again — a new bare built-in added there is externalised
35
+ * automatically. That completeness is LOAD-BEARING under `@gjsify/rolldown-native`
36
+ * (the GJS bundler engine): its JSON options boundary silently DROPS the resolveId
37
+ * `{ external: true }` flag that `gjsBuiltinModulesNodePlugin` returns (only the
38
+ * plain `external` string array survives serialization — the same reason
39
+ * `app/gjs.ts` routes its externals through a resolveId hook AND filters the array).
40
+ * So the rewritten `@gjsify/node-gi/<mod>` target stays external ONLY if it is in
41
+ * this array. `@gjsify/node-gi/cairo` was previously MISSING here (only `system` +
42
+ * `gettext` were listed), so under the GJS bundler `cairo` was left as a bare,
43
+ * unresolvable specifier — breaking every Cairo/PangoCairo consumer
44
+ * (`@gjsify/canvas2d-core`) built via the reverse bridge. Under npm `rolldown`
45
+ * (Node) the resolveId `external` flag IS honoured, so the gap was invisible until
46
+ * a Cairo consumer was built on the GJS bundler. e2e: `tests/e2e/node-gi-build`;
47
+ * unit: `packages/infra/cli/src/node-gi-externals.spec.ts`.
30
48
  */
31
- const NODE_GI_BARE_MODULE_SPECIFIERS = ['@gjsify/node-gi/system', '@gjsify/node-gi/gettext'];
49
+ const NODE_GI_BARE_MODULE_SPECIFIERS = Object.values(ALIASES_GJS_FOR_NODE);
50
+ /** Matches a register subpath in an alias key: `<pkg>/register` or `<pkg>/register/<feature>`. */
51
+ const REGISTER_SUBPATH_RE = /\/register(\/|$)/;
52
+ /**
53
+ * Reverse-bridge register routing: a node build with an EXPLICIT `--globals`
54
+ * register-inject stub (`pluginOptions.autoGlobalsInject`, set by the CLI for
55
+ * `--app node` when the user names globals — e.g. `--globals auto,dom`) is a
56
+ * genuine GJS source that needs the REAL `@gjsify/*` register bodies (document,
57
+ * HTMLCanvasElement, matchMedia, …) running over `@gjsify/node-gi`, not the
58
+ * default `@gjsify/empty` stubs that keep cross-platform node bundles loadable
59
+ * on plain Node.
60
+ *
61
+ * Two adjustments over the standard node alias map:
62
+ * 1. every `<pkg>/register…` → `@gjsify/empty` entry is DROPPED so the
63
+ * register import resolves to its real body;
64
+ * 2. the gjs target's bare→scoped register routes (`fetch/register/fetch` →
65
+ * `@gjsify/fetch/register/fetch`, `xmlhttprequest/register` →
66
+ * `@gjsify/fetch/register/xhr`, …) are merged in so the inject stub's bare
67
+ * specifiers resolve on node exactly as they do on gjs.
68
+ *
69
+ * Applied to the BASE map only — `pluginOptions.aliases` / user aliases still
70
+ * override. Without the inject stub the map is untouched (the plain-Node
71
+ * loadability guarantee for cross-platform packages is not regressed).
72
+ */
73
+ export function enableGjsRegistersForNode(baseAliases) {
74
+ const out = {};
75
+ for (const [key, value] of Object.entries(baseAliases)) {
76
+ if (value === '@gjsify/empty' && REGISTER_SUBPATH_RE.test(key))
77
+ continue;
78
+ out[key] = value;
79
+ }
80
+ for (const [key, value] of Object.entries(ALIASES_WEB_FOR_GJS)) {
81
+ if (REGISTER_SUBPATH_RE.test(key))
82
+ out[key] = value;
83
+ }
84
+ return out;
85
+ }
32
86
  export const setupForNode = async (input) => {
33
87
  const userExternal = input.userExternal ?? [];
34
88
  // node-datachannel is a native C++ addon that cannot be bundled — its
@@ -89,10 +143,17 @@ export const setupForNode = async (input) => {
89
143
  const sideEffectImports = [];
90
144
  if (input.pluginOptions.nodeGiGlobalsInject)
91
145
  sideEffectImports.push(NODE_GI_GLOBALS_SPECIFIER);
146
+ // The explicit `--globals` register-inject stub (reverse bridge, see
147
+ // enableGjsRegistersForNode). Ordered AFTER the ambient-globals shim so
148
+ // registers may rely on `print`/`imports` being installed.
149
+ const registerInject = input.pluginOptions.autoGlobalsInject;
150
+ if (registerInject)
151
+ sideEffectImports.push(registerInject);
92
152
  const virtualEntries = wrapInputWithSideEffects(entryPoints, sideEffectImports);
93
153
  const finalInput = virtualEntries.input;
154
+ const baseAliases = getAliasesForNode({ external });
94
155
  const aliasMap = {
95
- ...getAliasesForNode({ external }),
156
+ ...(registerInject ? enableGjsRegistersForNode(baseAliases) : baseAliases),
96
157
  ...input.pluginOptions.aliases,
97
158
  ...input.userAliases,
98
159
  };
@@ -163,10 +224,13 @@ export const setupForNode = async (input) => {
163
224
  // runtime, so a real GJS/GI source builds + runs on Node. It returns
164
225
  // null for `@girs/*`.
165
226
  gjsGiNodePlugin(),
166
- // Bare GJS built-in modules (`system`/`gettext`): claimed BEFORE the
167
- // aliasPlugin so they resolve to the EXTERNAL `@gjsify/node-gi/<mod>`
227
+ // Bare GJS built-in modules (`system`/`gettext`/`cairo`): claimed BEFORE
228
+ // the aliasPlugin so they resolve to the EXTERNAL `@gjsify/node-gi/<mod>`
168
229
  // shims without a disk resolution (works without node-gi installed,
169
- // like gi://). Node-target-only — never active on gjs/browser/ns.
230
+ // like gi://). Node-target-only — never active on gjs/browser/ns. The
231
+ // externalisation itself rides `NODE_GI_BARE_MODULE_SPECIFIERS` in the
232
+ // `exactExternal` array (the resolveId `external` flag is dropped under
233
+ // @gjsify/rolldown-native — see that const's doc-comment).
170
234
  gjsBuiltinModulesNodePlugin(ALIASES_GJS_FOR_NODE),
171
235
  // gjsImportsEmptyPlugin then decides the fate of `@girs/*` before
172
236
  // `aliasPlugin` and the default resolver. Same composition order as
@@ -185,8 +249,19 @@ export const setupForNode = async (input) => {
185
249
  // from the @girs package's own `gi://` specifier). `gi://*` is
186
250
  // already claimed by gjsGiNodePlugin, so this plugin's `gi://`
187
251
  // branch is dead on the node target either way.
188
- gjsImportsEmptyPlugin({ emptyGirs: !input.pluginOptions.nodeGiGlobalsInject }),
252
+ // `emptyGirs` also lifts for the explicit-register reverse-bridge case:
253
+ // the injected register bodies (dom-elements, canvas2d, …) are genuine
254
+ // GJS code whose `@girs/*` value imports must route through to
255
+ // `requireGi`, exactly like the ambient-globals case.
256
+ gjsImportsEmptyPlugin({
257
+ emptyGirs: !(input.pluginOptions.nodeGiGlobalsInject || registerInject),
258
+ }),
189
259
  aliasPlugin({ entries: flattenAliases(aliasMap) }),
260
+ // Blueprint (.blp → XML string): the reverse bridge runs REAL GTK on
261
+ // Node, so a GJS app entry (composite-template windows) must build for
262
+ // `--app node` too. Only claims `.blp` files — inert for plain-Node
263
+ // bundles. Same plugin the gjs/browser targets compose.
264
+ blueprintPlugin(),
190
265
  deepkitPlugin({ reflection: input.pluginOptions.reflection }),
191
266
  cssAsStringPlugin(),
192
267
  nodeModulesPathRewritePlugin({ bundleDir }),
@@ -9,14 +9,21 @@ export declare function giNodeShimSource(namespace: string, version?: string): s
9
9
  export declare function gjsGiNodePlugin(): Plugin;
10
10
  /**
11
11
  * For `--app node`: rewrite the bare GJS built-in module specifiers (`system`,
12
- * `gettext`) to their `@gjsify/node-gi` reverse-bridge shims, kept EXTERNAL.
12
+ * `gettext`, `cairo`) to their `@gjsify/node-gi` reverse-bridge shims, kept
13
+ * EXTERNAL.
13
14
  *
14
- * GJS exposes `system`/`gettext` as built-in ESM modules
15
+ * GJS exposes `system`/`gettext`/`cairo` as built-in ESM modules
15
16
  * (`import System from 'system'`); Node has no equivalent. This resolves the
16
- * bare specifier to `@gjsify/node-gi/system` / `@gjsify/node-gi/gettext` and
17
- * marks it external so it is NOT bundled — resolved at runtime against the
18
- * consumer's node_modules, exactly like `@gjsify/node-gi/gi` (the `gi://`
19
- * rewrite) and `@gjsify/node-gi/globals`.
17
+ * bare specifier to `@gjsify/node-gi/system` / `@gjsify/node-gi/gettext` /
18
+ * `@gjsify/node-gi/cairo` and marks it external so it is NOT bundled — resolved
19
+ * at runtime against the consumer's node_modules, exactly like
20
+ * `@gjsify/node-gi/gi` (the `gi://` rewrite) and `@gjsify/node-gi/globals`.
21
+ *
22
+ * NOTE: the `{ external: true }` flag returned here is honoured by npm `rolldown`
23
+ * but DROPPED by `@gjsify/rolldown-native` at its JSON options boundary, so
24
+ * `app/node.ts` ALSO lists these targets in its `exactExternal` string array
25
+ * (`NODE_GI_BARE_MODULE_SPECIFIERS`, derived from `ALIASES_GJS_FOR_NODE`) — the
26
+ * array is what actually keeps them external under the GJS bundler engine.
20
27
  *
21
28
  * Returning `{ external: true }` from `resolveId` is the form honoured by BOTH
22
29
  * bundler engines (npm rolldown on Node AND `@gjsify/rolldown-native` on GJS,
@@ -96,14 +96,21 @@ export function gjsGiNodePlugin() {
96
96
  }
97
97
  /**
98
98
  * For `--app node`: rewrite the bare GJS built-in module specifiers (`system`,
99
- * `gettext`) to their `@gjsify/node-gi` reverse-bridge shims, kept EXTERNAL.
99
+ * `gettext`, `cairo`) to their `@gjsify/node-gi` reverse-bridge shims, kept
100
+ * EXTERNAL.
100
101
  *
101
- * GJS exposes `system`/`gettext` as built-in ESM modules
102
+ * GJS exposes `system`/`gettext`/`cairo` as built-in ESM modules
102
103
  * (`import System from 'system'`); Node has no equivalent. This resolves the
103
- * bare specifier to `@gjsify/node-gi/system` / `@gjsify/node-gi/gettext` and
104
- * marks it external so it is NOT bundled — resolved at runtime against the
105
- * consumer's node_modules, exactly like `@gjsify/node-gi/gi` (the `gi://`
106
- * rewrite) and `@gjsify/node-gi/globals`.
104
+ * bare specifier to `@gjsify/node-gi/system` / `@gjsify/node-gi/gettext` /
105
+ * `@gjsify/node-gi/cairo` and marks it external so it is NOT bundled — resolved
106
+ * at runtime against the consumer's node_modules, exactly like
107
+ * `@gjsify/node-gi/gi` (the `gi://` rewrite) and `@gjsify/node-gi/globals`.
108
+ *
109
+ * NOTE: the `{ external: true }` flag returned here is honoured by npm `rolldown`
110
+ * but DROPPED by `@gjsify/rolldown-native` at its JSON options boundary, so
111
+ * `app/node.ts` ALSO lists these targets in its `exactExternal` string array
112
+ * (`NODE_GI_BARE_MODULE_SPECIFIERS`, derived from `ALIASES_GJS_FOR_NODE`) — the
113
+ * array is what actually keeps them external under the GJS bundler engine.
107
114
  *
108
115
  * Returning `{ external: true }` from `resolveId` is the form honoured by BOTH
109
116
  * bundler engines (npm rolldown on Node AND `@gjsify/rolldown-native` on GJS,
@@ -1,18 +1,71 @@
1
+ /**
2
+ * `true` when running under Bun.
3
+ *
4
+ * Bun exposes a `globalThis.Bun` object no other runtime provides. Probed
5
+ * FIRST in {@link hostRuntime} because Bun also fakes `process.versions.node`
6
+ * for npm compatibility — a bare `process.versions.node` read is a false Node
7
+ * positive there.
8
+ */
9
+ export declare function isBun(): boolean;
10
+ /**
11
+ * `true` when running under Deno.
12
+ *
13
+ * Deno exposes a `globalThis.Deno` object no other runtime provides. Like Bun,
14
+ * Deno also fakes `process.versions.node`, so it must be probed before the Node
15
+ * fallback.
16
+ */
17
+ export declare function isDeno(): boolean;
1
18
  /**
2
19
  * `true` when running under GJS (GNOME JavaScript / SpiderMonkey).
3
20
  *
4
21
  * Detected via the GObject-Introspection bridge `globalThis.imports.gi`, which
5
- * only exists under GJS — Node and browsers never expose it. This is the
6
- * canonical gjsify host probe; prefer it over re-inlining the `imports.gi`
22
+ * only exists under GJS — Node, Bun, Deno and browsers never expose it. This is
23
+ * the canonical gjsify host probe; prefer it over re-inlining the `imports.gi`
7
24
  * check.
8
25
  */
9
26
  export declare function isGjs(): boolean;
10
27
  /**
11
- * `true` when running under Node.js.
28
+ * The JS runtime hosting the gjsify tooling: `gjs`, `node`, `bun` or `deno`.
29
+ *
30
+ * The single source of truth for host-derived defaults (the default `--app`
31
+ * build target and the default `--runtime` in `run`/`showcase`/`storybook`).
12
32
  *
13
- * GJS is checked FIRST and short-circuits to `false`: `@gjsify/process` sets
14
- * `process.versions.node` (for npm-package compatibility), so a bare
15
- * `process.versions.node` probe is a false Node positive under GJS. Guarding
16
- * with `isGjs()` keeps the two host predicates mutually exclusive.
33
+ * Probe order mirrors `packages/node-gi/node-gi/index.js`: **Bun and Deno
34
+ * first** because both fake `process.versions.node` for npm compatibility (so a
35
+ * `process.versions.node` read is a false Node positive there), then GJS (its
36
+ * `@gjsify/process` shim likewise fakes `process.versions.node`), then real
37
+ * Node. Falls back to `node` when nothing matches (e.g. an exotic host) — the
38
+ * gjsify tooling only runs on these four.
39
+ */
40
+ export declare function hostRuntime(): 'gjs' | 'node' | 'bun' | 'deno';
41
+ /**
42
+ * `true` when running under Node.js — and NOT Bun or Deno.
43
+ *
44
+ * Bun and Deno both set `process.versions.node` for npm compatibility, so a
45
+ * bare `process.versions.node` probe is a false Node positive on either — the
46
+ * historical bug this guard fixes. GJS (via `@gjsify/process`) does the same.
47
+ * Guarding on all three keeps the four host predicates mutually exclusive: a
48
+ * host is exactly one of gjs / node / bun / deno.
17
49
  */
18
50
  export declare function isNode(): boolean;
51
+ /**
52
+ * The raw GJS runtime version from `imports.system.version` (e.g. `18800` for
53
+ * GJS 1.88.0), or `undefined` when not running under GJS.
54
+ *
55
+ * A distinct probe from {@link isGjs}: use it where the caller needs the numeric
56
+ * version (to format a banner) OR an early GJS short-circuit that must not rely
57
+ * on `@gjsify/process`'s faked `process.versions.node`.
58
+ */
59
+ export declare function gjsSystemVersion(): number | undefined;
60
+ /**
61
+ * Force a hard exit under GJS via `imports.system.exit(code)`.
62
+ *
63
+ * GJS has no atexit hook and would exit 0 at natural shutdown, so a non-zero
64
+ * `process.exitCode` alone does not stick — the CLI must call `system.exit`
65
+ * explicitly there. Node never exposes `imports`, so this is a no-op returning
66
+ * `false`, letting callers fall back to `process.exit`/`process.exitCode`.
67
+ *
68
+ * @returns `true` when the GJS exit was invoked (running under GJS); `false` on
69
+ * Node (the caller owns the Node exit path).
70
+ */
71
+ export declare function gjsExit(code: number): boolean;
@@ -12,28 +12,102 @@
12
12
  // so it stays loadable on every host and so consumers can import it via the
13
13
  // `@gjsify/rolldown-plugin-gjsify/runtime` subpath without pulling the rest of
14
14
  // the plugin (which transitively loads blueprint-compiler, deepkit, etc.).
15
+ /**
16
+ * `true` when running under Bun.
17
+ *
18
+ * Bun exposes a `globalThis.Bun` object no other runtime provides. Probed
19
+ * FIRST in {@link hostRuntime} because Bun also fakes `process.versions.node`
20
+ * for npm compatibility — a bare `process.versions.node` read is a false Node
21
+ * positive there.
22
+ */
23
+ export function isBun() {
24
+ return typeof globalThis.Bun !== 'undefined';
25
+ }
26
+ /**
27
+ * `true` when running under Deno.
28
+ *
29
+ * Deno exposes a `globalThis.Deno` object no other runtime provides. Like Bun,
30
+ * Deno also fakes `process.versions.node`, so it must be probed before the Node
31
+ * fallback.
32
+ */
33
+ export function isDeno() {
34
+ return typeof globalThis.Deno !== 'undefined';
35
+ }
15
36
  /**
16
37
  * `true` when running under GJS (GNOME JavaScript / SpiderMonkey).
17
38
  *
18
39
  * Detected via the GObject-Introspection bridge `globalThis.imports.gi`, which
19
- * only exists under GJS — Node and browsers never expose it. This is the
20
- * canonical gjsify host probe; prefer it over re-inlining the `imports.gi`
40
+ * only exists under GJS — Node, Bun, Deno and browsers never expose it. This is
41
+ * the canonical gjsify host probe; prefer it over re-inlining the `imports.gi`
21
42
  * check.
22
43
  */
23
44
  export function isGjs() {
24
45
  return typeof globalThis.imports?.gi !== 'undefined';
25
46
  }
26
47
  /**
27
- * `true` when running under Node.js.
48
+ * The JS runtime hosting the gjsify tooling: `gjs`, `node`, `bun` or `deno`.
49
+ *
50
+ * The single source of truth for host-derived defaults (the default `--app`
51
+ * build target and the default `--runtime` in `run`/`showcase`/`storybook`).
28
52
  *
29
- * GJS is checked FIRST and short-circuits to `false`: `@gjsify/process` sets
30
- * `process.versions.node` (for npm-package compatibility), so a bare
31
- * `process.versions.node` probe is a false Node positive under GJS. Guarding
32
- * with `isGjs()` keeps the two host predicates mutually exclusive.
53
+ * Probe order mirrors `packages/node-gi/node-gi/index.js`: **Bun and Deno
54
+ * first** because both fake `process.versions.node` for npm compatibility (so a
55
+ * `process.versions.node` read is a false Node positive there), then GJS (its
56
+ * `@gjsify/process` shim likewise fakes `process.versions.node`), then real
57
+ * Node. Falls back to `node` when nothing matches (e.g. an exotic host) — the
58
+ * gjsify tooling only runs on these four.
33
59
  */
34
- export function isNode() {
60
+ export function hostRuntime() {
61
+ if (isBun())
62
+ return 'bun';
63
+ if (isDeno())
64
+ return 'deno';
35
65
  if (isGjs())
66
+ return 'gjs';
67
+ return 'node';
68
+ }
69
+ /**
70
+ * `true` when running under Node.js — and NOT Bun or Deno.
71
+ *
72
+ * Bun and Deno both set `process.versions.node` for npm compatibility, so a
73
+ * bare `process.versions.node` probe is a false Node positive on either — the
74
+ * historical bug this guard fixes. GJS (via `@gjsify/process`) does the same.
75
+ * Guarding on all three keeps the four host predicates mutually exclusive: a
76
+ * host is exactly one of gjs / node / bun / deno.
77
+ */
78
+ export function isNode() {
79
+ if (isBun() || isDeno() || isGjs())
36
80
  return false;
37
81
  const proc = globalThis.process;
38
82
  return typeof proc?.versions?.node === 'string';
39
83
  }
84
+ /**
85
+ * The raw GJS runtime version from `imports.system.version` (e.g. `18800` for
86
+ * GJS 1.88.0), or `undefined` when not running under GJS.
87
+ *
88
+ * A distinct probe from {@link isGjs}: use it where the caller needs the numeric
89
+ * version (to format a banner) OR an early GJS short-circuit that must not rely
90
+ * on `@gjsify/process`'s faked `process.versions.node`.
91
+ */
92
+ export function gjsSystemVersion() {
93
+ return globalThis.imports?.system?.version;
94
+ }
95
+ /**
96
+ * Force a hard exit under GJS via `imports.system.exit(code)`.
97
+ *
98
+ * GJS has no atexit hook and would exit 0 at natural shutdown, so a non-zero
99
+ * `process.exitCode` alone does not stick — the CLI must call `system.exit`
100
+ * explicitly there. Node never exposes `imports`, so this is a no-op returning
101
+ * `false`, letting callers fall back to `process.exit`/`process.exitCode`.
102
+ *
103
+ * @returns `true` when the GJS exit was invoked (running under GJS); `false` on
104
+ * Node (the caller owns the Node exit path).
105
+ */
106
+ export function gjsExit(code) {
107
+ const sys = globalThis.imports?.system;
108
+ if (sys?.exit) {
109
+ sys.exit(code);
110
+ return true;
111
+ }
112
+ return false;
113
+ }
@@ -46,6 +46,19 @@ export declare function isRegisterPathResolvable(registerPath: string, fromDir:
46
46
  * the gap visible so the developer can add the dep explicitly.
47
47
  */
48
48
  export declare function filterResolvableRegisterPaths(registerPaths: Set<string>, fromDir: string): Set<string>;
49
+ /** Options for {@link writeRegisterInjectFile}. */
50
+ export interface WriteRegisterInjectOptions {
51
+ /**
52
+ * Reverse-bridge (`--app node`) force-override: before the injected register
53
+ * bodies run, clear the runtime-native web globals gjsify must OWN on the
54
+ * reverse bridge (the `REVERSE_BRIDGE_NATIVE_OVERRIDE_IDENTS` set — today the
55
+ * `fetch` family). A native is cleared ONLY when its register is actually in
56
+ * `registerPaths`, so no host global is deleted without a gjsify replacement.
57
+ * No-op (and byte-unchanged output) for the GJS target or when none of the
58
+ * override identifiers are injected.
59
+ */
60
+ reverseBridgeOverride?: boolean;
61
+ }
49
62
  /**
50
63
  * Write a stub ESM file with `import` statements for the given register
51
64
  * paths and return its absolute path, suitable for passing to esbuild's
@@ -56,5 +69,13 @@ export declare function filterResolvableRegisterPaths(registerPaths: Set<string>
56
69
  *
57
70
  * The file name is hashed by content so repeated builds with the same
58
71
  * set reuse the same file (no churn, idempotent on disk).
72
+ *
73
+ * On the reverse bridge (`options.reverseBridgeOverride`, `--app node`) the stub
74
+ * is prefixed with a side-effect import of a generated clear-natives module —
75
+ * imported FIRST so, by ESM depth-first source-order evaluation, the native
76
+ * `fetch`/`Headers`/`Request`/`Response` are deleted BEFORE the register bodies
77
+ * run and reinstall gjsify's versions over them. Kept in a separate module
78
+ * (not a leading statement) because ESM hoists the register `import`s above any
79
+ * statement in the same module.
59
80
  */
60
- export declare function writeRegisterInjectFile(registerPaths: Set<string>, cwd?: string): Promise<string | null>;
81
+ export declare function writeRegisterInjectFile(registerPaths: Set<string>, cwd?: string, options?: WriteRegisterInjectOptions): Promise<string | null>;
@@ -14,10 +14,11 @@ import { writeFile, mkdir } from 'node:fs/promises';
14
14
  import { existsSync } from 'node:fs';
15
15
  import { createHash } from 'node:crypto';
16
16
  import { join } from 'node:path';
17
- import { GJS_GLOBALS_MAP, GJS_GLOBALS_GROUPS } from '@gjsify/resolve-npm/globals-map';
17
+ import { GJS_GLOBALS_MAP, GJS_GLOBALS_GROUPS, REVERSE_BRIDGE_NATIVE_OVERRIDE_IDENTS } from '@gjsify/resolve-npm/globals-map';
18
18
  import { ALIASES_WEB_FOR_GJS } from '@gjsify/resolve-npm';
19
19
  const GLOBALS_MAP = GJS_GLOBALS_MAP;
20
20
  const GLOBALS_GROUPS = GJS_GLOBALS_GROUPS;
21
+ const NATIVE_OVERRIDE_IDENTS = REVERSE_BRIDGE_NATIVE_OVERRIDE_IDENTS;
21
22
  /**
22
23
  * Resolve a `--globals` CLI argument into the set of `/register` subpaths
23
24
  * that must be injected into the build.
@@ -166,6 +167,21 @@ export function filterResolvableRegisterPaths(registerPaths, fromDir) {
166
167
  }
167
168
  return out;
168
169
  }
170
+ /**
171
+ * Identifiers whose native global must be cleared before the injected registers
172
+ * run, given the set of register paths being injected. Only identifiers whose
173
+ * register is actually present are returned — clearing a native without a
174
+ * gjsify replacement in the same build would leave a hole.
175
+ */
176
+ function nativeOverrideIdentsFor(registerPaths) {
177
+ const out = [];
178
+ for (const id of NATIVE_OVERRIDE_IDENTS) {
179
+ const registerPath = GLOBALS_MAP[id];
180
+ if (registerPath && registerPaths.has(registerPath))
181
+ out.push(id);
182
+ }
183
+ return out;
184
+ }
169
185
  /**
170
186
  * Write a stub ESM file with `import` statements for the given register
171
187
  * paths and return its absolute path, suitable for passing to esbuild's
@@ -176,15 +192,37 @@ export function filterResolvableRegisterPaths(registerPaths, fromDir) {
176
192
  *
177
193
  * The file name is hashed by content so repeated builds with the same
178
194
  * set reuse the same file (no churn, idempotent on disk).
195
+ *
196
+ * On the reverse bridge (`options.reverseBridgeOverride`, `--app node`) the stub
197
+ * is prefixed with a side-effect import of a generated clear-natives module —
198
+ * imported FIRST so, by ESM depth-first source-order evaluation, the native
199
+ * `fetch`/`Headers`/`Request`/`Response` are deleted BEFORE the register bodies
200
+ * run and reinstall gjsify's versions over them. Kept in a separate module
201
+ * (not a leading statement) because ESM hoists the register `import`s above any
202
+ * statement in the same module.
179
203
  */
180
- export async function writeRegisterInjectFile(registerPaths, cwd = process.cwd()) {
204
+ export async function writeRegisterInjectFile(registerPaths, cwd = process.cwd(), options = {}) {
181
205
  if (registerPaths.size === 0)
182
206
  return null;
183
- const sorted = [...registerPaths].sort();
184
- const content = sorted.map((p) => `import '${p}';`).join('\n') + '\n';
185
- const hash = createHash('sha1').update(content).digest('hex').slice(0, 10);
186
207
  const cacheDir = join(cwd, 'node_modules', '.cache', 'gjsify');
187
208
  await mkdir(cacheDir, { recursive: true });
209
+ const sorted = [...registerPaths].sort();
210
+ const lines = [];
211
+ const overrideIdents = options.reverseBridgeOverride ? nativeOverrideIdentsFor(registerPaths) : [];
212
+ if (overrideIdents.length > 0) {
213
+ const list = JSON.stringify(overrideIdents.sort());
214
+ const clearContent = '// gjsify --app node reverse-bridge: clear runtime-native web globals so the\n' +
215
+ '// injected @gjsify/* register bodies install their GJS-semantics versions.\n' +
216
+ `for (const __k of ${list}) { try { delete globalThis[__k]; } catch {} }\n`;
217
+ const clearHash = createHash('sha1').update(clearContent).digest('hex').slice(0, 10);
218
+ const clearName = `auto-globals-override-${clearHash}.mjs`;
219
+ await writeFile(join(cacheDir, clearName), clearContent, 'utf-8');
220
+ lines.push(`import './${clearName}';`);
221
+ }
222
+ for (const p of sorted)
223
+ lines.push(`import '${p}';`);
224
+ const content = lines.join('\n') + '\n';
225
+ const hash = createHash('sha1').update(content).digest('hex').slice(0, 10);
188
226
  const path = join(cacheDir, `auto-globals-${hash}.mjs`);
189
227
  await writeFile(path, content, 'utf-8');
190
228
  return path;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/rolldown-plugin-gjsify",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "description": "Rolldown / Rollup / Vite plugin orchestrator for GJS, Node, and Browser targets",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -55,11 +55,11 @@
55
55
  ],
56
56
  "license": "MIT",
57
57
  "dependencies": {
58
- "@gjsify/console": "^0.17.0",
59
- "@gjsify/resolve-npm": "^0.17.0",
60
- "@gjsify/rolldown-plugin-deepkit": "^0.17.0",
61
- "@gjsify/rolldown-plugin-pnp": "^0.17.0",
62
- "@gjsify/vite-plugin-blueprint": "^0.17.0",
58
+ "@gjsify/console": "^0.19.0",
59
+ "@gjsify/resolve-npm": "^0.19.0",
60
+ "@gjsify/rolldown-plugin-deepkit": "^0.19.0",
61
+ "@gjsify/rolldown-plugin-pnp": "^0.19.0",
62
+ "@gjsify/vite-plugin-blueprint": "^0.19.0",
63
63
  "@rollup/pluginutils": "^5.4.0",
64
64
  "acorn": "^8.17.0",
65
65
  "acorn-walk": "^8.3.5",
@@ -68,7 +68,7 @@
68
68
  "sass": "^1.101.0"
69
69
  },
70
70
  "peerDependencies": {
71
- "@gjsify/lightningcss-native": "^0.17.0",
71
+ "@gjsify/lightningcss-native": "^0.19.0",
72
72
  "rolldown": "^1.1.4"
73
73
  },
74
74
  "peerDependenciesMeta": {