@gjsify/rolldown-plugin-gjsify 0.22.0 → 0.24.1
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/browser.js +8 -4
- package/lib/app/gjs.d.ts +24 -0
- package/lib/app/gjs.js +74 -22
- package/lib/app/index.d.ts +2 -2
- package/lib/app/index.js +2 -2
- package/lib/app/nativescript.js +15 -4
- package/lib/app/node.d.ts +41 -7
- package/lib/app/node.js +73 -12
- package/lib/index.d.ts +4 -0
- package/lib/index.js +2 -0
- package/lib/plugins/alias.js +36 -0
- package/lib/plugins/css-as-string.js +26 -1
- package/lib/plugins/gjs-gi-node.js +5 -1
- package/lib/plugins/gjs-imports-empty.js +2 -1
- package/lib/plugins/napi-node-addon.d.ts +120 -0
- package/lib/plugins/napi-node-addon.js +888 -0
- package/lib/plugins/process-stub.js +13 -2
- package/lib/utils/auto-globals.js +6 -1
- package/lib/utils/entry-wrapper.js +2 -1
- package/lib/utils/index.d.ts +1 -0
- package/lib/utils/index.js +1 -0
- package/lib/utils/virtual-module-id.d.ts +14 -0
- package/lib/utils/virtual-module-id.js +36 -0
- package/package.json +7 -7
package/lib/app/browser.js
CHANGED
|
@@ -4,10 +4,14 @@
|
|
|
4
4
|
// (they appear transitively via `@gjsify/unit` and similar packages with
|
|
5
5
|
// GJS-specific code paths). Bare Node specifiers + their `node:*` prefix
|
|
6
6
|
// variants are routed to `@gjsify/<X>` via the curated
|
|
7
|
-
// `ALIASES_NODE_FOR_BROWSER` table
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
7
|
+
// `ALIASES_NODE_FOR_BROWSER` table, whose VALUES already carry the
|
|
8
|
+
// per-runtimes-triplet slot routing (`@gjsify/<X>` → `@gjsify/<X>/browser` /
|
|
9
|
+
// `/globals` / `@gjsify/empty`): that table is exported through
|
|
10
|
+
// `withDerivedSlotRouting` in `@gjsify/resolve-npm`. Composition happens THERE,
|
|
11
|
+
// not as a resolver second pass here — `aliasPlugin` resolves an alias target
|
|
12
|
+
// in exactly one hop, because the map it receives is merged flat across tiers
|
|
13
|
+
// and a chain would re-route user `--alias` targets too (full argument in
|
|
14
|
+
// `withDerivedSlotRouting`'s doc comment and `plugins/alias.ts`).
|
|
11
15
|
import { aliasPlugin } from '../plugins/alias.js';
|
|
12
16
|
import { deepkitPlugin } from '@gjsify/rolldown-plugin-deepkit';
|
|
13
17
|
import blueprintPlugin from '@gjsify/vite-plugin-blueprint';
|
package/lib/app/gjs.d.ts
CHANGED
|
@@ -55,6 +55,30 @@ export declare const setupForGjs: (input: GjsFactoryInput) => Promise<GjsBuildCo
|
|
|
55
55
|
* in `auto-globals.spec.ts` — canonical contract, change-detector status.
|
|
56
56
|
*/
|
|
57
57
|
export declare function createGjsExternalsPredicate(userExternal?: string[]): (id: string) => boolean;
|
|
58
|
+
/**
|
|
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
|
|
71
|
+
*
|
|
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.
|
|
80
|
+
*/
|
|
81
|
+
export declare function isGjsifyShim(id: string): boolean;
|
|
58
82
|
/**
|
|
59
83
|
* Recognize the `/register` and `/register/<feature>` subpath shapes that
|
|
60
84
|
* `--globals auto` injects into the bundle as side-effect imports.
|
package/lib/app/gjs.js
CHANGED
|
@@ -10,8 +10,11 @@
|
|
|
10
10
|
// handled separately by `setupLib`.
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
12
|
import { dirname, resolve } from 'node:path';
|
|
13
|
+
import { existsSync } from 'node:fs';
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
13
15
|
import { aliasPlugin } from '../plugins/alias.js';
|
|
14
16
|
import { externalsPlugin } from '../plugins/externals.js';
|
|
17
|
+
import { napiNodeAddonPlugin } from '../plugins/napi-node-addon.js';
|
|
15
18
|
import { deepkitPlugin } from '@gjsify/rolldown-plugin-deepkit';
|
|
16
19
|
import blueprintPlugin from '@gjsify/vite-plugin-blueprint';
|
|
17
20
|
import { getAliasesForGjs } from '../utils/alias.js';
|
|
@@ -22,35 +25,38 @@ import { cssAsStringPlugin } from '../plugins/css-as-string.js';
|
|
|
22
25
|
import { shebangPlugin, resolveShebangLine, inputShebangStripPlugin } from '../plugins/shebang.js';
|
|
23
26
|
import { wrapInputWithSideEffects } from '../utils/entry-wrapper.js';
|
|
24
27
|
const _shimDir = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
/**
|
|
29
|
+
* Resolve one of this package's bundled shims to an absolute path.
|
|
30
|
+
*
|
|
31
|
+
* `createRequire` is a NAMED IMPORT from `node:module`, never a bare
|
|
32
|
+
* `require(...)` — this source is ESM (see AGENTS.md "Our source is ESM —
|
|
33
|
+
* no bare `require`"). It is used here for its RESOLVER, which is
|
|
34
|
+
* `exports`-map aware, and only to answer "where does this subpath live";
|
|
35
|
+
* nothing is loaded through it.
|
|
36
|
+
*/
|
|
25
37
|
function resolveShim(shimName) {
|
|
26
38
|
// Preferred: relative to this module's directory. Works under the
|
|
27
39
|
// normal Node consumer flow where `_shimDir` = `<pkg>/lib/app/`.
|
|
28
40
|
const relative = resolve(_shimDir, `../shims/${shimName}.js`);
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
32
|
-
fs = require('node:fs');
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
return relative;
|
|
36
|
-
}
|
|
37
|
-
if (fs.existsSync(relative))
|
|
41
|
+
if (existsSync(relative))
|
|
38
42
|
return relative;
|
|
39
43
|
// Fallback: when the orchestrator is bundled into a single .mjs
|
|
40
44
|
// (GJS-CLI self-host loop) `_shimDir` collapses to the bundle's
|
|
41
|
-
// own directory and the relative lookup misses.
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
// subpath export in package.json for this fallback to resolve.
|
|
45
|
+
// own directory and the relative lookup misses. The published subpath
|
|
46
|
+
// export `./shims/<name>` resolves under both Node and GJS without
|
|
47
|
+
// further walking. Each shim MUST be a `./shims/<name>` subpath export
|
|
48
|
+
// in package.json for this fallback to resolve.
|
|
46
49
|
try {
|
|
47
|
-
|
|
48
|
-
const Module = require('node:module');
|
|
49
|
-
const require_ = Module.createRequire(import.meta.url);
|
|
50
|
-
return require_.resolve(`@gjsify/rolldown-plugin-gjsify/shims/${shimName}`);
|
|
50
|
+
return createRequire(import.meta.url).resolve(`@gjsify/rolldown-plugin-gjsify/shims/${shimName}`);
|
|
51
51
|
}
|
|
52
|
-
catch {
|
|
53
|
-
|
|
52
|
+
catch (cause) {
|
|
53
|
+
// Both lookups failed — the shim is genuinely absent. Returning the
|
|
54
|
+
// unverified `relative` here would hand a non-existent path to the
|
|
55
|
+
// alias map, and the failure would resurface much later as an
|
|
56
|
+
// unrelated "could not resolve" naming a path nobody wrote. Name the
|
|
57
|
+
// actual problem, at the site that knows what it was looking for.
|
|
58
|
+
throw new Error(`@gjsify/rolldown-plugin-gjsify: cannot locate the "${shimName}" shim. ` +
|
|
59
|
+
`Tried ${relative} and the "./shims/${shimName}" subpath export.`, { cause });
|
|
54
60
|
}
|
|
55
61
|
}
|
|
56
62
|
export const setupForGjs = async (input) => {
|
|
@@ -71,7 +77,12 @@ export const setupForGjs = async (input) => {
|
|
|
71
77
|
//
|
|
72
78
|
// `exactExternal` filters out register subpaths so a user external
|
|
73
79
|
// entry can never override the force-inline carve-out below.
|
|
74
|
-
const exactExternal = [
|
|
80
|
+
const exactExternal = [
|
|
81
|
+
'cairo',
|
|
82
|
+
'gettext',
|
|
83
|
+
'system',
|
|
84
|
+
...userExternal.filter((id) => !isRegisterSubpath(id) && !isGjsifyShim(id)),
|
|
85
|
+
];
|
|
75
86
|
const external = createGjsExternalsPredicate(userExternal);
|
|
76
87
|
const format = input.pluginOptions.format ?? 'esm';
|
|
77
88
|
const exclude = input.pluginOptions.exclude ?? [];
|
|
@@ -196,6 +207,17 @@ export const setupForGjs = async (input) => {
|
|
|
196
207
|
...flattenAliases(aliasMap),
|
|
197
208
|
},
|
|
198
209
|
}),
|
|
210
|
+
// Transparent N-API `.node`-addon loader — the forward mirror of
|
|
211
|
+
// `gjsGiNodePlugin`. Its `order:'pre'` resolveId claims a native-addon
|
|
212
|
+
// acquisition (`bindings`/`node-gyp-build`/a direct `.node`/a napi-rs
|
|
213
|
+
// platform sibling) and rewrites it to a virtual module returning
|
|
214
|
+
// `loadAddon('<abs .node>')` from `@gjsify/napi`. Placed AFTER aliasPlugin
|
|
215
|
+
// (so a user alias that pins an addon's native entry applies first) and
|
|
216
|
+
// BEFORE externalsPlugin (so the addon acquisition is claimed before the
|
|
217
|
+
// externals policy sees it). `@gjsify/napi` is BUNDLED, not external.
|
|
218
|
+
// Inert unless such a specifier is in the graph — every other id passes
|
|
219
|
+
// through. Always-on for `--app gjs`; never registered for node/browser/ns.
|
|
220
|
+
napiNodeAddonPlugin(),
|
|
199
221
|
// Enforce the full `--app gjs` externals policy (gi:// prefix,
|
|
200
222
|
// exact names, register-subpath force-inline) via resolveId —
|
|
201
223
|
// the only form BOTH engines honour (the native engine drops
|
|
@@ -259,7 +281,7 @@ function flattenAliases(map) {
|
|
|
259
281
|
export function createGjsExternalsPredicate(userExternal = []) {
|
|
260
282
|
const exact = ['cairo', 'gettext', 'system', ...userExternal];
|
|
261
283
|
return (id) => {
|
|
262
|
-
if (isRegisterSubpath(id))
|
|
284
|
+
if (isRegisterSubpath(id) || isGjsifyShim(id))
|
|
263
285
|
return false;
|
|
264
286
|
if (id.startsWith('gi://'))
|
|
265
287
|
return true;
|
|
@@ -268,6 +290,36 @@ export function createGjsExternalsPredicate(userExternal = []) {
|
|
|
268
290
|
return false;
|
|
269
291
|
};
|
|
270
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* Recognize the shims this plugin INJECTS into a `--app gjs` bundle
|
|
295
|
+
* (`shims/module-resolve`, `shims/console-gjs`, `shims/unicorn-magic`, …), in
|
|
296
|
+
* both bare-specifier and resolved-disk-path form.
|
|
297
|
+
*
|
|
298
|
+
* They must be force-inlined for exactly the reason `/register` subpaths are,
|
|
299
|
+
* and the failure is the same one: GJS has no `require` and its ESM loader has
|
|
300
|
+
* neither a node_modules walker nor `exports`-map support, so an EXTERNALIZED
|
|
301
|
+
* shim aborts the program at load:
|
|
302
|
+
*
|
|
303
|
+
* Error: Calling `require` for
|
|
304
|
+
* "@gjsify/rolldown-plugin-gjsify/shims/module-resolve" in an environment
|
|
305
|
+
* that doesn't expose the `require` function
|
|
306
|
+
*
|
|
307
|
+
* The shim is not optional — the bundler injected it because the bundle needs
|
|
308
|
+
* it — so externalizing it can only ever produce an unloadable artifact. The
|
|
309
|
+
* carve-out short-circuits BEFORE the user-external check so `bundler.external`
|
|
310
|
+
* cannot override it, and it matches by SHAPE rather than an explicit list, so
|
|
311
|
+
* a shim added later is covered without touching this predicate.
|
|
312
|
+
*
|
|
313
|
+
* Scope is deliberately narrow: only OUR shim directory. A consumer package
|
|
314
|
+
* that happens to have a `shims/` folder is unaffected.
|
|
315
|
+
*/
|
|
316
|
+
export function isGjsifyShim(id) {
|
|
317
|
+
// Bare/fully-qualified: `@gjsify/rolldown-plugin-gjsify/shims/<name>`.
|
|
318
|
+
if (/@gjsify\/rolldown-plugin-gjsify\/shims\/[^?]+$/.test(id))
|
|
319
|
+
return true;
|
|
320
|
+
// Resolved disk path: `…/rolldown-plugin-gjsify/{lib,src}/shims/<name>.js`.
|
|
321
|
+
return /[\\/]rolldown-plugin-gjsify[\\/](?:lib|src)[\\/]shims[\\/]/.test(id);
|
|
322
|
+
}
|
|
271
323
|
/**
|
|
272
324
|
* Recognize the `/register` and `/register/<feature>` subpath shapes that
|
|
273
325
|
* `--globals auto` injects into the bundle as side-effect imports.
|
package/lib/app/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { setupForGjs, isRegisterSubpath, createGjsExternalsPredicate } from './gjs.js';
|
|
1
|
+
export { setupForGjs, isRegisterSubpath, isGjsifyShim, createGjsExternalsPredicate } from './gjs.js';
|
|
2
2
|
export type { GjsBuildConfig, GjsFactoryInput } from './gjs.js';
|
|
3
|
-
export { setupForNode, enableGjsRegistersForNode } from './node.js';
|
|
3
|
+
export { setupForNode, enableGjsRegistersForNode, isGjsSourceBuild } 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
|
-
export { setupForGjs, isRegisterSubpath, createGjsExternalsPredicate } from './gjs.js';
|
|
2
|
-
export { setupForNode, enableGjsRegistersForNode } from './node.js';
|
|
1
|
+
export { setupForGjs, isRegisterSubpath, isGjsifyShim, createGjsExternalsPredicate } from './gjs.js';
|
|
2
|
+
export { setupForNode, enableGjsRegistersForNode, isGjsSourceBuild } from './node.js';
|
|
3
3
|
export { setupForBrowser } from './browser.js';
|
|
4
4
|
export { setupForNativescript } from './nativescript.js';
|
package/lib/app/nativescript.js
CHANGED
|
@@ -16,10 +16,21 @@
|
|
|
16
16
|
// GJS-specific code paths that never execute on NS.
|
|
17
17
|
//
|
|
18
18
|
// Bare Node specifiers + their `node:*` prefix variants are routed to
|
|
19
|
-
// `@gjsify/<X>` via the curated `ALIASES_NODE_FOR_NATIVESCRIPT` table
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
19
|
+
// `@gjsify/<X>` via the curated `ALIASES_NODE_FOR_NATIVESCRIPT` table. Unlike
|
|
20
|
+
// the browser table, this one is NOT passed through `withDerivedSlotRouting`:
|
|
21
|
+
// slot routing would only change 4 of its 122 entries and all four are
|
|
22
|
+
// regressions — `fs`/`fs/promises` (+ their `node:` forms) deliberately point
|
|
23
|
+
// at `@gjsify/native-fs-bridge`, which declares `nativescript: "native"` in the
|
|
24
|
+
// "this package IS the native implementation" sense and therefore routes to a
|
|
25
|
+
// `globals.mjs` it does not ship → `@gjsify/empty`. The slot vocabulary's
|
|
26
|
+
// `native` means "the RUNTIME provides this, use its value", so the bridge
|
|
27
|
+
// packages are mis-declared; that is tracked in STATUS.md and must be settled
|
|
28
|
+
// before this table is composed. A direct `import … from
|
|
29
|
+
// '@gjsify/native-fs-bridge'` already hits the derived map (plain key hit) and
|
|
30
|
+
// is emptied TODAY — same root cause, listed in the same TODO.
|
|
31
|
+
//
|
|
32
|
+
// Derived `@gjsify/<X>` entries are merged into the map below, so an import by
|
|
33
|
+
// PACKAGE NAME still routes per its declared slot in a single hop.
|
|
23
34
|
//
|
|
24
35
|
// No `cssAsStringPlugin` (NativeScript ships its own CSS pipeline as part
|
|
25
36
|
// of `@nativescript/core`) and no `blueprintPlugin` (Blueprint is GTK-only).
|
package/lib/app/node.d.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
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
|
|
5
|
-
* register
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* HTMLCanvasElement, matchMedia, …) running over `@gjsify/node-gi`, not the
|
|
9
|
-
* default `@gjsify/empty` stubs that keep cross-platform node bundles loadable
|
|
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
|
|
10
8
|
* on plain Node.
|
|
11
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.
|
|
13
|
+
*
|
|
12
14
|
* Two adjustments over the standard node alias map:
|
|
13
15
|
* 1. every `<pkg>/register…` → `@gjsify/empty` entry is DROPPED so the
|
|
14
16
|
* register import resolves to its real body;
|
|
@@ -18,10 +20,42 @@ import type { PluginOptions } from '../types/plugin-options.js';
|
|
|
18
20
|
* specifiers resolve on node exactly as they do on gjs.
|
|
19
21
|
*
|
|
20
22
|
* Applied to the BASE map only — `pluginOptions.aliases` / user aliases still
|
|
21
|
-
* override.
|
|
23
|
+
* override. On a non-reverse-bridge build the map is untouched (the plain-Node
|
|
22
24
|
* loadability guarantee for cross-platform packages is not regressed).
|
|
23
25
|
*/
|
|
24
26
|
export declare function enableGjsRegistersForNode(baseAliases: Record<string, string>): Record<string, string>;
|
|
27
|
+
/**
|
|
28
|
+
* Is this `--app node` build a GENUINE GJS SOURCE going through the node-gi
|
|
29
|
+
* reverse bridge, rather than the node target of a cross-platform package?
|
|
30
|
+
*
|
|
31
|
+
* Two signals qualify, and BOTH must gate the same things:
|
|
32
|
+
* - `nodeGiGlobalsInject` — the CLI's post-tree-shake detection found bare GJS
|
|
33
|
+
* ambient globals (`print`/`imports`/`ARGV`/…) in the bundled output;
|
|
34
|
+
* - `registerInject` (the `--globals` auto/explicit inject stub) — the user
|
|
35
|
+
* asked for GJS registers on the node target explicitly.
|
|
36
|
+
*
|
|
37
|
+
* This predicate exists as ONE named function because the two consumers used to
|
|
38
|
+
* ask the question separately and disagree: `emitGirs` took the union while the
|
|
39
|
+
* register-alias lift took only the explicit stub. A reverse-bridge build then
|
|
40
|
+
* got its `@girs/*` routed through to `requireGi` while a `/register`
|
|
41
|
+
* side-effect import in the SAME graph was still emptied to `@gjsify/empty` —
|
|
42
|
+
* silently dropping `@gjsify/dom-elements/register/canvas`'s `'2d'` context
|
|
43
|
+
* factory, so `@gjsify/canvas2d`'s `Canvas2DBridge` never fired `onReady`.
|
|
44
|
+
*
|
|
45
|
+
* A build with NEITHER signal is byte-unchanged: the `@gjsify/empty` register
|
|
46
|
+
* routing and the `@girs/*` emptying both stay, so a cross-platform package's
|
|
47
|
+
* node bundle keeps loading on plain Node without node-gi installed.
|
|
48
|
+
*
|
|
49
|
+
* KNOWN NARROWNESS (pre-dates this predicate, tracked in STATUS.md): the
|
|
50
|
+
* `nodeGiGlobalsInject` detector keys on BARE ambient globals, so a genuine GJS
|
|
51
|
+
* source that uses `gi://` but logs via `console.log` and passes no `--globals`
|
|
52
|
+
* is not recognised — its `@girs/*` AND its registers are both emptied. The two
|
|
53
|
+
* are now at parity; widening the signal itself is the separate fix.
|
|
54
|
+
*/
|
|
55
|
+
export declare function isGjsSourceBuild(options: {
|
|
56
|
+
nodeGiGlobalsInject?: boolean;
|
|
57
|
+
registerInject?: string | undefined;
|
|
58
|
+
}): boolean;
|
|
25
59
|
export interface NodeBuildConfig {
|
|
26
60
|
options: RolldownOptions;
|
|
27
61
|
plugins: RolldownPluginOption[];
|
package/lib/app/node.js
CHANGED
|
@@ -50,14 +50,16 @@ const NODE_GI_BARE_MODULE_SPECIFIERS = Object.values(ALIASES_GJS_FOR_NODE);
|
|
|
50
50
|
/** Matches a register subpath in an alias key: `<pkg>/register` or `<pkg>/register/<feature>`. */
|
|
51
51
|
const REGISTER_SUBPATH_RE = /\/register(\/|$)/;
|
|
52
52
|
/**
|
|
53
|
-
* Reverse-bridge register routing: a node build
|
|
54
|
-
* register
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* HTMLCanvasElement, matchMedia, …) running over `@gjsify/node-gi`, not the
|
|
58
|
-
* default `@gjsify/empty` stubs that keep cross-platform node bundles loadable
|
|
53
|
+
* Reverse-bridge register routing: a node build of a GENUINE GJS SOURCE needs
|
|
54
|
+
* the REAL `@gjsify/*` register bodies (document, HTMLCanvasElement, the `'2d'`
|
|
55
|
+
* context factory, matchMedia, …) running over `@gjsify/node-gi`, not the
|
|
56
|
+
* default `@gjsify/empty` stubs that keep CROSS-PLATFORM node bundles loadable
|
|
59
57
|
* on plain Node.
|
|
60
58
|
*
|
|
59
|
+
* Applied by `setupForNode` when `isGjsSourceBuild` is true — see the comment
|
|
60
|
+
* on that flag for why BOTH reverse-bridge signals qualify, not just the
|
|
61
|
+
* explicit `--globals` stub.
|
|
62
|
+
*
|
|
61
63
|
* Two adjustments over the standard node alias map:
|
|
62
64
|
* 1. every `<pkg>/register…` → `@gjsify/empty` entry is DROPPED so the
|
|
63
65
|
* register import resolves to its real body;
|
|
@@ -67,7 +69,7 @@ const REGISTER_SUBPATH_RE = /\/register(\/|$)/;
|
|
|
67
69
|
* specifiers resolve on node exactly as they do on gjs.
|
|
68
70
|
*
|
|
69
71
|
* Applied to the BASE map only — `pluginOptions.aliases` / user aliases still
|
|
70
|
-
* override.
|
|
72
|
+
* override. On a non-reverse-bridge build the map is untouched (the plain-Node
|
|
71
73
|
* loadability guarantee for cross-platform packages is not regressed).
|
|
72
74
|
*/
|
|
73
75
|
export function enableGjsRegistersForNode(baseAliases) {
|
|
@@ -83,6 +85,37 @@ export function enableGjsRegistersForNode(baseAliases) {
|
|
|
83
85
|
}
|
|
84
86
|
return out;
|
|
85
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Is this `--app node` build a GENUINE GJS SOURCE going through the node-gi
|
|
90
|
+
* reverse bridge, rather than the node target of a cross-platform package?
|
|
91
|
+
*
|
|
92
|
+
* Two signals qualify, and BOTH must gate the same things:
|
|
93
|
+
* - `nodeGiGlobalsInject` — the CLI's post-tree-shake detection found bare GJS
|
|
94
|
+
* ambient globals (`print`/`imports`/`ARGV`/…) in the bundled output;
|
|
95
|
+
* - `registerInject` (the `--globals` auto/explicit inject stub) — the user
|
|
96
|
+
* asked for GJS registers on the node target explicitly.
|
|
97
|
+
*
|
|
98
|
+
* This predicate exists as ONE named function because the two consumers used to
|
|
99
|
+
* ask the question separately and disagree: `emitGirs` took the union while the
|
|
100
|
+
* register-alias lift took only the explicit stub. A reverse-bridge build then
|
|
101
|
+
* got its `@girs/*` routed through to `requireGi` while a `/register`
|
|
102
|
+
* side-effect import in the SAME graph was still emptied to `@gjsify/empty` —
|
|
103
|
+
* silently dropping `@gjsify/dom-elements/register/canvas`'s `'2d'` context
|
|
104
|
+
* factory, so `@gjsify/canvas2d`'s `Canvas2DBridge` never fired `onReady`.
|
|
105
|
+
*
|
|
106
|
+
* A build with NEITHER signal is byte-unchanged: the `@gjsify/empty` register
|
|
107
|
+
* routing and the `@girs/*` emptying both stay, so a cross-platform package's
|
|
108
|
+
* node bundle keeps loading on plain Node without node-gi installed.
|
|
109
|
+
*
|
|
110
|
+
* KNOWN NARROWNESS (pre-dates this predicate, tracked in STATUS.md): the
|
|
111
|
+
* `nodeGiGlobalsInject` detector keys on BARE ambient globals, so a genuine GJS
|
|
112
|
+
* source that uses `gi://` but logs via `console.log` and passes no `--globals`
|
|
113
|
+
* is not recognised — its `@girs/*` AND its registers are both emptied. The two
|
|
114
|
+
* are now at parity; widening the signal itself is the separate fix.
|
|
115
|
+
*/
|
|
116
|
+
export function isGjsSourceBuild(options) {
|
|
117
|
+
return Boolean(options.nodeGiGlobalsInject || options.registerInject);
|
|
118
|
+
}
|
|
86
119
|
export const setupForNode = async (input) => {
|
|
87
120
|
const userExternal = input.userExternal ?? [];
|
|
88
121
|
// node-datachannel is a native C++ addon that cannot be bundled — its
|
|
@@ -151,9 +184,38 @@ export const setupForNode = async (input) => {
|
|
|
151
184
|
sideEffectImports.push(registerInject);
|
|
152
185
|
const virtualEntries = wrapInputWithSideEffects(entryPoints, sideEffectImports);
|
|
153
186
|
const finalInput = virtualEntries.input;
|
|
187
|
+
/**
|
|
188
|
+
* Is this a GENUINE GJS SOURCE built via the node-gi reverse bridge (as
|
|
189
|
+
* opposed to a cross-platform package's plain-Node bundle)?
|
|
190
|
+
*
|
|
191
|
+
* TWO signals answer that ONE question and both must count:
|
|
192
|
+
*
|
|
193
|
+
* - `nodeGiGlobalsInject` — the CLI detected BARE GJS ambient globals
|
|
194
|
+
* (`print`/`imports`/`ARGV`) in the tree-shaken output. Only genuine GJS
|
|
195
|
+
* source has those (the detector deliberately ignores the
|
|
196
|
+
* `globalThis.imports` isomorphic-guard shape).
|
|
197
|
+
* - `autoGlobalsInject` — the user named EXPLICIT `--globals` (e.g.
|
|
198
|
+
* `--globals auto,dom`), i.e. asked for the DOM surface on Node.
|
|
199
|
+
*
|
|
200
|
+
* Both already gate `emptyGirs` below. The register routing used to consult
|
|
201
|
+
* ONLY the explicit stub, and that asymmetry was a
|
|
202
|
+
* bug: a reverse-bridge build got its `@girs/*` value imports routed
|
|
203
|
+
* through to `requireGi` while a `/register` side-effect import in the very
|
|
204
|
+
* same graph was still emptied to `@gjsify/empty`. It surfaced as
|
|
205
|
+
* `@gjsify/canvas2d`'s `Canvas2DBridge` silently losing the `'2d'` context
|
|
206
|
+
* factory (`@gjsify/dom-elements/register/canvas` → empty → `getContext('2d')`
|
|
207
|
+
* null → `onReady` never fires) on a plain `gjsify build … --app node`, with
|
|
208
|
+
* no `--globals` flag anywhere — guarded by the node-gi
|
|
209
|
+
* `test/canvas2d-bridge.test.mjs` golden.
|
|
210
|
+
*
|
|
211
|
+
* A build with NEITHER signal is byte-unchanged: the `@gjsify/empty`
|
|
212
|
+
* register routing stays, so a cross-platform package's node bundle keeps
|
|
213
|
+
* loading on plain Node without node-gi installed.
|
|
214
|
+
*/
|
|
215
|
+
const gjsSourceBuild = isGjsSourceBuild({ ...input.pluginOptions, registerInject });
|
|
154
216
|
const baseAliases = getAliasesForNode({ external });
|
|
155
217
|
const aliasMap = {
|
|
156
|
-
...(
|
|
218
|
+
...(gjsSourceBuild ? enableGjsRegistersForNode(baseAliases) : baseAliases),
|
|
157
219
|
...input.pluginOptions.aliases,
|
|
158
220
|
...input.userAliases,
|
|
159
221
|
};
|
|
@@ -252,10 +314,9 @@ export const setupForNode = async (input) => {
|
|
|
252
314
|
// `emptyGirs` also lifts for the explicit-register reverse-bridge case:
|
|
253
315
|
// the injected register bodies (dom-elements, canvas2d, …) are genuine
|
|
254
316
|
// GJS code whose `@girs/*` value imports must route through to
|
|
255
|
-
// `requireGi`, exactly like the ambient-globals case.
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}),
|
|
317
|
+
// `requireGi`, exactly like the ambient-globals case. Same predicate as
|
|
318
|
+
// the register-alias lift above — one question, one flag.
|
|
319
|
+
gjsImportsEmptyPlugin({ emptyGirs: !gjsSourceBuild }),
|
|
259
320
|
aliasPlugin({ entries: flattenAliases(aliasMap) }),
|
|
260
321
|
// Blueprint (.blp → XML string): the reverse bridge runs REAL GTK on
|
|
261
322
|
// Node, so a GJS app entry (composite-template windows) must build for
|
package/lib/index.d.ts
CHANGED
|
@@ -11,9 +11,13 @@ export { textLoaderPlugin } from './plugins/text-loader.js';
|
|
|
11
11
|
export type { TextLoaderPluginOptions, LoaderKind } from './plugins/text-loader.js';
|
|
12
12
|
export { shebangPlugin, GJS_SHEBANG, NODE_SHEBANG, expandEnvTemplate, resolveShebangLine } from './plugins/shebang.js';
|
|
13
13
|
export type { ShebangPluginOptions } from './plugins/shebang.js';
|
|
14
|
+
export { aliasPlugin } from './plugins/alias.js';
|
|
15
|
+
export type { AliasPluginOptions } from './plugins/alias.js';
|
|
14
16
|
export { gjsImportsEmptyPlugin } from './plugins/gjs-imports-empty.js';
|
|
15
17
|
export { externalsPlugin } from './plugins/externals.js';
|
|
16
18
|
export type { ExternalsPredicate, ExternalsPluginOptions } from './plugins/externals.js';
|
|
19
|
+
export { napiNodeAddonPlugin, resolveAddonPath, nearestPackageRoot, classifySpecifier, directNodeShim, nodeGypBuildShim, bindingsShim, napiRsShim, ADDON_FILTER_RE, isNapiRsPackageJson, isNapiRsSibling, detectNapiRsEntry, AddonNotBuiltError, } from './plugins/napi-node-addon.js';
|
|
20
|
+
export type { NapiNodeAddonPluginOptions, AddonPackageJson } from './plugins/napi-node-addon.js';
|
|
17
21
|
export { platformResolvePlugin, detectNativescriptPlatform, nativescriptPlatformDefines, } from './plugins/platform-resolve.js';
|
|
18
22
|
export type { PlatformResolvePluginOptions, NativescriptPlatform } from './plugins/platform-resolve.js';
|
|
19
23
|
export * from './plugin.js';
|
package/lib/index.js
CHANGED
|
@@ -8,8 +8,10 @@ export { processStubPlugin, GJS_PROCESS_STUB, composeBanner } from './plugins/pr
|
|
|
8
8
|
export { cssAsStringPlugin } from './plugins/css-as-string.js';
|
|
9
9
|
export { textLoaderPlugin } from './plugins/text-loader.js';
|
|
10
10
|
export { shebangPlugin, GJS_SHEBANG, NODE_SHEBANG, expandEnvTemplate, resolveShebangLine } from './plugins/shebang.js';
|
|
11
|
+
export { aliasPlugin } from './plugins/alias.js';
|
|
11
12
|
export { gjsImportsEmptyPlugin } from './plugins/gjs-imports-empty.js';
|
|
12
13
|
export { externalsPlugin } from './plugins/externals.js';
|
|
14
|
+
export { napiNodeAddonPlugin, resolveAddonPath, nearestPackageRoot, classifySpecifier, directNodeShim, nodeGypBuildShim, bindingsShim, napiRsShim, ADDON_FILTER_RE, isNapiRsPackageJson, isNapiRsSibling, detectNapiRsEntry, AddonNotBuiltError, } from './plugins/napi-node-addon.js';
|
|
13
15
|
export { platformResolvePlugin, detectNativescriptPlatform, nativescriptPlatformDefines, } from './plugins/platform-resolve.js';
|
|
14
16
|
export * from './plugin.js';
|
|
15
17
|
import { gjsifyPlugin } from './plugin.js';
|
package/lib/plugins/alias.js
CHANGED
|
@@ -20,6 +20,38 @@
|
|
|
20
20
|
// resolve through the "import" condition (Rolldown's default), bypassing the
|
|
21
21
|
// `cjs-compat.cjs` shim that unwraps named-export ESM modules to their
|
|
22
22
|
// constructor — breaking `util.inherits(Child, Stream)` patterns.
|
|
23
|
+
//
|
|
24
|
+
// ── Two invariants this plugin holds ────────────────────────────────────────
|
|
25
|
+
//
|
|
26
|
+
// 1. **Gjsify-generated modules are NOT aliased.** An import whose importer is
|
|
27
|
+
// a `\0gjsify-*` virtual module (`isGjsifyVirtualModuleId`) is left alone.
|
|
28
|
+
// We wrote that module's source; it names the RUNTIME module it needs and
|
|
29
|
+
// must get exactly that. The motivating failure: `gjsGiNodePlugin`'s
|
|
30
|
+
// `import { createRequire } from 'node:module'` was rewritten onto the
|
|
31
|
+
// `@gjsify/module` polyfill by the consumer harness's
|
|
32
|
+
// `--alias node:module=@gjsify/module`, so the bundle called the polyfill's
|
|
33
|
+
// `createRequire` at top level before its GLib proxy existed
|
|
34
|
+
// (`TypeError: … reading 'filename_from_uri'`) — and the polyfill's own
|
|
35
|
+
// Gio-backed loader can only reach GLib THROUGH the module it was being
|
|
36
|
+
// asked to load. The guard is importer-based rather than a per-specifier
|
|
37
|
+
// carve-out so it covers every current and future virtual module (entry
|
|
38
|
+
// wrapper, gi-node, napi-addon, gjs-imports-empty).
|
|
39
|
+
//
|
|
40
|
+
// 2. **An alias target is TERMINAL — exactly one hop, never a chain.** The
|
|
41
|
+
// `entries` map handed in is already MERGED across tiers (derived slot
|
|
42
|
+
// routing → curated `ALIASES_*` tables → per-target overrides → user
|
|
43
|
+
// `--alias`, later wins, see `app/*.ts`), so the tier a value came from is
|
|
44
|
+
// no longer recoverable here. Re-feeding a target through the table would
|
|
45
|
+
// therefore apply slot routing to USER aliases too, which is precisely what
|
|
46
|
+
// must not happen: `scripts/node-gi-consumer-harness.mjs` builds
|
|
47
|
+
// `--alias node:<x>=@gjsify/<x>` to put the POLYFILL under test behind a
|
|
48
|
+
// Node builtin, and 34 of those polyfills declare `runtimes.node: "native"`
|
|
49
|
+
// — a second hop would silently route them back to `@gjsify/<x>/globals`
|
|
50
|
+
// (i.e. Node's own builtin) and the suite would measure nothing. The
|
|
51
|
+
// "second pass" of the slot-routing model is a COMPOSITION concern, and it
|
|
52
|
+
// is resolved where the tiers are still distinguishable — see
|
|
53
|
+
// `withDerivedSlotRouting` in `@gjsify/resolve-npm`.
|
|
54
|
+
import { isGjsifyVirtualModuleId } from '../utils/virtual-module-id.js';
|
|
23
55
|
export function aliasPlugin(options) {
|
|
24
56
|
const entries = options.entries;
|
|
25
57
|
return {
|
|
@@ -27,6 +59,10 @@ export function aliasPlugin(options) {
|
|
|
27
59
|
resolveId: {
|
|
28
60
|
order: 'pre',
|
|
29
61
|
async handler(source, importer, extraOptions) {
|
|
62
|
+
// Invariant 1 — never rewrite an import that WE generated.
|
|
63
|
+
if (isGjsifyVirtualModuleId(importer)) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
30
66
|
if (!Object.prototype.hasOwnProperty.call(entries, source)) {
|
|
31
67
|
return null;
|
|
32
68
|
}
|
|
@@ -121,7 +121,32 @@ async function tryLoadNativeBundler() {
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
async function loadNpmBundler() {
|
|
124
|
-
|
|
124
|
+
// INDIRECT specifier, for the same reason `tryLoadNativeBundler` uses one:
|
|
125
|
+
// keep this acquisition out of the BUILD graph so it is resolved at
|
|
126
|
+
// runtime only. npm `lightningcss` is a napi-rs package, and a static
|
|
127
|
+
// `import('lightningcss')` here put its generated loader into the module
|
|
128
|
+
// graph of every `--app gjs` bundle — including the CLI's own committed
|
|
129
|
+
// `dist/cli.gjs.mjs`.
|
|
130
|
+
//
|
|
131
|
+
// That is what made the committed bundle ENVIRONMENT-DEPENDENT. With
|
|
132
|
+
// `@gjsify/napi` resolvable, `napiNodeAddonPlugin` rewrites the loader to a
|
|
133
|
+
// `loadAddon()` shim and inlines it; without it the plugin declines and the
|
|
134
|
+
// import stays external. Two different module graphs, so the minifier
|
|
135
|
+
// assigns different variable names, so the artifact stops reproducing from
|
|
136
|
+
// its own source (`verify-committed-bundles` red, first difference at byte
|
|
137
|
+
// 275: `r=` vs `a=` for `globalThis.imports.gi.GLib`). A toolchain artifact
|
|
138
|
+
// must not depend on whether an optional package happened to be installed
|
|
139
|
+
// when it was built.
|
|
140
|
+
//
|
|
141
|
+
// Nothing is lost under GJS: the documented GJS backend is
|
|
142
|
+
// `@gjsify/lightningcss-native` (above), and this npm fallback was never
|
|
143
|
+
// actually usable there — its `.node` cannot load under GJS without a
|
|
144
|
+
// build-time rewrite, and the only arches where that rewrite is possible
|
|
145
|
+
// (`@gjsify/napi`: linux-x64, darwin-arm64) are arches where the native
|
|
146
|
+
// bridge exists anyway. On Node this is unchanged: `lightningcss` is a real
|
|
147
|
+
// `dependency` of this package, so the runtime resolve finds it.
|
|
148
|
+
const specifier = 'lightningcss';
|
|
149
|
+
const { bundleAsync } = (await import(/* @vite-ignore */ specifier));
|
|
125
150
|
return async (filename, targets) => {
|
|
126
151
|
const result = await bundleAsync({
|
|
127
152
|
filename,
|
|
@@ -27,7 +27,11 @@
|
|
|
27
27
|
// Portability note (same as `gjs-imports-empty.ts`): the `filter` is a Rolldown
|
|
28
28
|
// fast-path; the internal guard in the handler is the load-bearing check so the
|
|
29
29
|
// plugin is correct even when the filter does not pre-filter (e.g. under Vite).
|
|
30
|
-
|
|
30
|
+
import { GJSIFY_VIRTUAL_PREFIX } from '../utils/virtual-module-id.js';
|
|
31
|
+
// Derived from the shared prefix so `isGjsifyVirtualModuleId` recognises these
|
|
32
|
+
// ids — that is what keeps the `node:module` import below bound to the RUNTIME's
|
|
33
|
+
// `createRequire` even when the build carries a user `--alias node:module=…`.
|
|
34
|
+
const GI_NODE_VIRTUAL_PREFIX = `${GJSIFY_VIRTUAL_PREFIX}gi-node:`;
|
|
31
35
|
/** Parse a `gi://Ns?version=X` specifier into its namespace + optional version. */
|
|
32
36
|
export function parseGiSpecifier(source) {
|
|
33
37
|
if (!source.startsWith('gi://'))
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
// dev server) the handler may receive ALL ids. The internal guard below makes
|
|
32
32
|
// the plugin correct regardless of whether the filter pre-filtered — it is
|
|
33
33
|
// the load-bearing check; the `filter` is a defense-in-depth optimization.
|
|
34
|
-
|
|
34
|
+
import { GJSIFY_VIRTUAL_PREFIX } from '../utils/virtual-module-id.js';
|
|
35
|
+
const GJSIMPORTS_VIRTUAL_ID = `${GJSIFY_VIRTUAL_PREFIX}empty-gjs-import`;
|
|
35
36
|
export function gjsImportsEmptyPlugin(options = {}) {
|
|
36
37
|
const emptyGirs = options.emptyGirs ?? true;
|
|
37
38
|
// When `@girs/*` is carved out, only `gi://*` reaches the empty redirect.
|