@gjsify/rolldown-plugin-gjsify 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/app/gjs.js +12 -0
- package/lib/app/index.d.ts +1 -1
- package/lib/app/index.js +1 -1
- package/lib/app/node.d.ts +41 -7
- package/lib/app/node.js +73 -12
- package/lib/index.d.ts +2 -0
- package/lib/index.js +1 -0
- package/lib/plugins/napi-node-addon.d.ts +100 -0
- package/lib/plugins/napi-node-addon.js +631 -0
- package/lib/plugins/process-stub.js +13 -2
- package/package.json +7 -7
package/lib/app/gjs.js
CHANGED
|
@@ -12,6 +12,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
12
12
|
import { dirname, resolve } from 'node:path';
|
|
13
13
|
import { aliasPlugin } from '../plugins/alias.js';
|
|
14
14
|
import { externalsPlugin } from '../plugins/externals.js';
|
|
15
|
+
import { napiNodeAddonPlugin } from '../plugins/napi-node-addon.js';
|
|
15
16
|
import { deepkitPlugin } from '@gjsify/rolldown-plugin-deepkit';
|
|
16
17
|
import blueprintPlugin from '@gjsify/vite-plugin-blueprint';
|
|
17
18
|
import { getAliasesForGjs } from '../utils/alias.js';
|
|
@@ -196,6 +197,17 @@ export const setupForGjs = async (input) => {
|
|
|
196
197
|
...flattenAliases(aliasMap),
|
|
197
198
|
},
|
|
198
199
|
}),
|
|
200
|
+
// Transparent N-API `.node`-addon loader — the forward mirror of
|
|
201
|
+
// `gjsGiNodePlugin`. Its `order:'pre'` resolveId claims a native-addon
|
|
202
|
+
// acquisition (`bindings`/`node-gyp-build`/a direct `.node`/a napi-rs
|
|
203
|
+
// platform sibling) and rewrites it to a virtual module returning
|
|
204
|
+
// `loadAddon('<abs .node>')` from `@gjsify/napi`. Placed AFTER aliasPlugin
|
|
205
|
+
// (so a user alias that pins an addon's native entry applies first) and
|
|
206
|
+
// BEFORE externalsPlugin (so the addon acquisition is claimed before the
|
|
207
|
+
// externals policy sees it). `@gjsify/napi` is BUNDLED, not external.
|
|
208
|
+
// Inert unless such a specifier is in the graph — every other id passes
|
|
209
|
+
// through. Always-on for `--app gjs`; never registered for node/browser/ns.
|
|
210
|
+
napiNodeAddonPlugin(),
|
|
199
211
|
// Enforce the full `--app gjs` externals policy (gi:// prefix,
|
|
200
212
|
// exact names, register-subpath force-inline) via resolveId —
|
|
201
213
|
// the only form BOTH engines honour (the native engine drops
|
package/lib/app/index.d.ts
CHANGED
|
@@ -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, 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
1
|
export { setupForGjs, isRegisterSubpath, createGjsExternalsPredicate } from './gjs.js';
|
|
2
|
-
export { setupForNode, enableGjsRegistersForNode } from './node.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/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
|
@@ -14,6 +14,8 @@ export type { ShebangPluginOptions } from './plugins/shebang.js';
|
|
|
14
14
|
export { gjsImportsEmptyPlugin } from './plugins/gjs-imports-empty.js';
|
|
15
15
|
export { externalsPlugin } from './plugins/externals.js';
|
|
16
16
|
export type { ExternalsPredicate, ExternalsPluginOptions } from './plugins/externals.js';
|
|
17
|
+
export { napiNodeAddonPlugin, resolveAddonPath, nearestPackageRoot, classifySpecifier, directNodeShim, nodeGypBuildShim, bindingsShim, napiRsShim, isNapiRsPackageJson, detectNapiRsEntry, AddonNotBuiltError, } from './plugins/napi-node-addon.js';
|
|
18
|
+
export type { NapiNodeAddonPluginOptions, AddonPackageJson } from './plugins/napi-node-addon.js';
|
|
17
19
|
export { platformResolvePlugin, detectNativescriptPlatform, nativescriptPlatformDefines, } from './plugins/platform-resolve.js';
|
|
18
20
|
export type { PlatformResolvePluginOptions, NativescriptPlatform } from './plugins/platform-resolve.js';
|
|
19
21
|
export * from './plugin.js';
|
package/lib/index.js
CHANGED
|
@@ -10,6 +10,7 @@ export { textLoaderPlugin } from './plugins/text-loader.js';
|
|
|
10
10
|
export { shebangPlugin, GJS_SHEBANG, NODE_SHEBANG, expandEnvTemplate, resolveShebangLine } from './plugins/shebang.js';
|
|
11
11
|
export { gjsImportsEmptyPlugin } from './plugins/gjs-imports-empty.js';
|
|
12
12
|
export { externalsPlugin } from './plugins/externals.js';
|
|
13
|
+
export { napiNodeAddonPlugin, resolveAddonPath, nearestPackageRoot, classifySpecifier, directNodeShim, nodeGypBuildShim, bindingsShim, napiRsShim, isNapiRsPackageJson, detectNapiRsEntry, AddonNotBuiltError, } from './plugins/napi-node-addon.js';
|
|
13
14
|
export { platformResolvePlugin, detectNativescriptPlatform, nativescriptPlatformDefines, } from './plugins/platform-resolve.js';
|
|
14
15
|
export * from './plugin.js';
|
|
15
16
|
import { gjsifyPlugin } from './plugin.js';
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { Plugin } from 'rolldown';
|
|
2
|
+
/** Options for {@link napiNodeAddonPlugin}. */
|
|
3
|
+
export interface NapiNodeAddonPluginOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Emit a one-line warning when a native addon is intercepted but
|
|
6
|
+
* `@gjsify/napi` is not resolvable in the consumer graph. Default `true`.
|
|
7
|
+
*/
|
|
8
|
+
warnOnMissingNapi?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** A native addon package root has no resolvable compiled `.node`. */
|
|
11
|
+
export declare class AddonNotBuiltError extends Error {
|
|
12
|
+
constructor(pkgRoot: string);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Locate the compiled `.node` for an addon package root, matching
|
|
16
|
+
* node-gyp-build's probe order: `build/Release` → `build/Debug` →
|
|
17
|
+
* `prebuilds/<platform>-<arch>/<best tag>`. Throws {@link AddonNotBuiltError}
|
|
18
|
+
* when nothing is found.
|
|
19
|
+
*
|
|
20
|
+
* @param pkgRoot Absolute path to the addon package root (dir with package.json).
|
|
21
|
+
* @param opts.warn Optional sink for a non-fatal warning (ambiguous build dir).
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveAddonPath(pkgRoot: string, opts?: {
|
|
24
|
+
warn?: (msg: string) => void;
|
|
25
|
+
}): string;
|
|
26
|
+
/**
|
|
27
|
+
* Walk up from an importer file to the nearest `package.json` directory (the
|
|
28
|
+
* addon package root). Returns null when none is found (should not happen for a
|
|
29
|
+
* real node_modules dep).
|
|
30
|
+
*/
|
|
31
|
+
export declare function nearestPackageRoot(importerFile: string): string | null;
|
|
32
|
+
/** Direct `.node` import → the addon's exports (ESM default). */
|
|
33
|
+
export declare function directNodeShim(addonPath: string): string;
|
|
34
|
+
/** `node-gyp-build` replacement — a callable `load(dir)` carrying `.path()`. */
|
|
35
|
+
export declare function nodeGypBuildShim(addonPath: string): string;
|
|
36
|
+
/** `bindings` replacement — a callable `bindings(name)` returning the addon. */
|
|
37
|
+
export declare function bindingsShim(addonPath: string): string;
|
|
38
|
+
/** napi-rs sibling → the raw native exports as the module value. */
|
|
39
|
+
export declare function napiRsShim(addonPath: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* Classify a specifier for interception. Pure decision logic (no filesystem
|
|
42
|
+
* probing beyond what `resolveAddonPath` does via the importer) — exported for
|
|
43
|
+
* the unit tests. Returns the shim kind + the package root to probe, or the
|
|
44
|
+
* literal `.node` path for the direct case.
|
|
45
|
+
*/
|
|
46
|
+
export declare function classifySpecifier(source: string): {
|
|
47
|
+
kind: 'node-gyp-build' | 'bindings';
|
|
48
|
+
} | {
|
|
49
|
+
kind: 'direct-node';
|
|
50
|
+
} | {
|
|
51
|
+
kind: 'napi-rs-candidate';
|
|
52
|
+
} | null;
|
|
53
|
+
/** Minimal package.json shape read for napi-rs detection. */
|
|
54
|
+
export interface AddonPackageJson {
|
|
55
|
+
name?: string;
|
|
56
|
+
main?: string;
|
|
57
|
+
browser?: string;
|
|
58
|
+
napi?: unknown;
|
|
59
|
+
optionalDependencies?: Record<string, string>;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* A package.json describes a napi-rs generated-loader package when EITHER
|
|
63
|
+
* signal holds — both are package.json-level (not source sniffing):
|
|
64
|
+
*
|
|
65
|
+
* (a) a top-level `napi` config OBJECT (`{ binaryName, targets, … }`) — the
|
|
66
|
+
* napi-rs CLI's own build block. An ordinary npm package never declares a
|
|
67
|
+
* `napi` object at the manifest root.
|
|
68
|
+
* (b) at least one `optionalDependencies` entry named `<self>-<triple>` — the
|
|
69
|
+
* napi-rs prebuilt-binary convention (`@node-rs/argon2` ships
|
|
70
|
+
* `@node-rs/argon2-linux-x64-gnu`, …). The key MUST start with the
|
|
71
|
+
* package's own name plus `-` AND end in a platform triple, so a normal
|
|
72
|
+
* optional dep (`fsevents`) can't match.
|
|
73
|
+
*
|
|
74
|
+
* Neither fires on a normal package, so pairing this with the "resolved file IS
|
|
75
|
+
* the package's own native main entry" check (see {@link detectNapiRsEntry})
|
|
76
|
+
* makes entry-replacement safe. Exported for the unit tests.
|
|
77
|
+
*/
|
|
78
|
+
export declare function isNapiRsPackageJson(pkg: AddonPackageJson): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Decide whether an absolute file path is the NATIVE generated-loader entry of a
|
|
81
|
+
* napi-rs package. Returns the package root + parsed manifest when it is, else
|
|
82
|
+
* null. "Native entry" = the package's `main` (default `index.js`) — NOT its
|
|
83
|
+
* `browser`/wasm fallback and NOT a deep file — so we only ever replace the one
|
|
84
|
+
* generated loader, never an unrelated module. Filesystem-based (reads the
|
|
85
|
+
* nearest package.json); the plugin memoizes calls. Exported for the unit tests.
|
|
86
|
+
*/
|
|
87
|
+
export declare function detectNapiRsEntry(entryFile: string): {
|
|
88
|
+
pkgRoot: string;
|
|
89
|
+
pkg: AddonPackageJson;
|
|
90
|
+
} | null;
|
|
91
|
+
/**
|
|
92
|
+
* Transparent `.node`-addon loader for `gjsify build --app gjs`.
|
|
93
|
+
*
|
|
94
|
+
* resolveId (`order: 'pre'`, importer-aware) intercepts a native-addon
|
|
95
|
+
* acquisition and rewrites it to a virtual module whose value is
|
|
96
|
+
* `loadAddon('<abs .node>')`. Inert unless such a specifier is in the graph —
|
|
97
|
+
* every other id returns null. Register ONLY for `--app gjs` (the C-ABI runs
|
|
98
|
+
* under `@gjsify/napi`); never for node/browser/nativescript.
|
|
99
|
+
*/
|
|
100
|
+
export declare function napiNodeAddonPlugin(options?: NapiNodeAddonPluginOptions): Plugin;
|
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
// For `--app gjs`: transparently route a compiled N-API `.node` addon through
|
|
2
|
+
// `@gjsify/napi`'s `loadAddon()` so `import Database from 'better-sqlite3'` /
|
|
3
|
+
// `require('bufferutil')` "just works" in a GJS build — no per-project shim, no
|
|
4
|
+
// hand-pinned addon path.
|
|
5
|
+
//
|
|
6
|
+
// This is the FORWARD MIRROR of `gjsGiNodePlugin` (`gjs-gi-node.ts`): that
|
|
7
|
+
// plugin rewrites `gi://Ns` → `requireGi` for `--app node` (run GObject code on
|
|
8
|
+
// Node); this one rewrites a native-addon acquisition → `loadAddon` for
|
|
9
|
+
// `--app gjs` (run Node's `.node` addons on GJS). Both intercept the ONE
|
|
10
|
+
// specifier a bundler can see and replace it with a virtual module whose value
|
|
11
|
+
// is the runtime bridge.
|
|
12
|
+
//
|
|
13
|
+
// The problem is always the same: the compiled `.node` is acquired with a
|
|
14
|
+
// DYNAMIC `require(<computed path>)` no bundler can rewrite. So we intercept the
|
|
15
|
+
// well-known helper the addon uses and replace it with a virtual module that
|
|
16
|
+
// returns `loadAddon('<abs .node>')`. Handled conventions (keyed by the import
|
|
17
|
+
// a bundler DOES see):
|
|
18
|
+
//
|
|
19
|
+
// - direct `.node` — a `source` (or a `@scope/pkg-<triple>` sibling that
|
|
20
|
+
// resolves to a `.node`) ending in `.node`.
|
|
21
|
+
// - `node-gyp-build` — prebuildify layout (bufferutil, utf-8-validate, …).
|
|
22
|
+
// Default export is a `load(dir)` function.
|
|
23
|
+
// - `bindings` — node-bindings layout (better-sqlite3, node-sqlite3).
|
|
24
|
+
// Default export is a `bindings(name)` function.
|
|
25
|
+
// - napi-rs sibling — `@scope/pkg-<triple>` platform package (or local
|
|
26
|
+
// `pkg.<triple>.node` fallback), whose exports ARE the
|
|
27
|
+
// addon API.
|
|
28
|
+
// - napi-rs ENTRY — the napi-rs GENERATED loader index
|
|
29
|
+
// (`@node-rs/argon2/index.js`), detected by a
|
|
30
|
+
// package.json signal and replaced WHOLESALE.
|
|
31
|
+
//
|
|
32
|
+
// napi-rs GENERATED-LOADER ENTRY replacement: a napi-rs generated index
|
|
33
|
+
// (`@node-rs/argon2/index.js`) wraps its acquisition in a
|
|
34
|
+
// `require('node:module')` + `createRequire(__filename)` + runtime
|
|
35
|
+
// `existsSync`/`process` branch chain whose CJS body does NOT survive `--app gjs`
|
|
36
|
+
// bundling (the top-level `require = createRequire(...)` reassignment throws
|
|
37
|
+
// `ReferenceError: require`). The sibling/direct interceptions above correctly
|
|
38
|
+
// LOCATE the `.node` but cannot rescue that body. So when the entry of a napi-rs
|
|
39
|
+
// package resolves, we replace the WHOLE module with
|
|
40
|
+
// `module.exports = loadAddon('<abs platform .node>')` (the `napi-rs-entry`
|
|
41
|
+
// kind) — the platform `.node` is the current-triple sibling package
|
|
42
|
+
// (`@scope/pkg-<triple>`, only the host's optionalDependency is installed) or a
|
|
43
|
+
// local `pkg.<triple>.node`. Detection is CONSERVATIVE (package.json signal +
|
|
44
|
+
// the file must be the package's own native `main` entry + a real host `.node`
|
|
45
|
+
// must resolve) so it never rewrites an unrelated package's entry; when no
|
|
46
|
+
// current-platform `.node` resolves we fall through to normal resolution rather
|
|
47
|
+
// than emit a shim over a missing file. The C/C++ path (node-gyp-build +
|
|
48
|
+
// bindings) and napi-rs are all proven byte-identical in the transparent gate.
|
|
49
|
+
//
|
|
50
|
+
// The addon's compiled `.node` is located by `resolveAddonPath()`, which
|
|
51
|
+
// replicates node-gyp-build's OWN selection algorithm (build/Release →
|
|
52
|
+
// build/Debug → prebuilds/<platform>-<arch>/<best tag>) so the GJS build routes
|
|
53
|
+
// the SAME binary Node would load.
|
|
54
|
+
//
|
|
55
|
+
// KEY: the shims import `@gjsify/napi` by BARE SPECIFIER (`require('@gjsify/napi')`).
|
|
56
|
+
// `@gjsify/napi`'s L1 is a `gjs:polyfill` package reading `imports.gi`; it
|
|
57
|
+
// bundles normally, and its native typelib is auto-added to `GI_TYPELIB_PATH` by
|
|
58
|
+
// the CLI's `detectNativePackages` (because it declares `gjsify.prebuilds`). The
|
|
59
|
+
// callable-helper shims (`node-gyp-build`/`bindings`) stay CJS
|
|
60
|
+
// (`module.exports = fn`) so the consumer's `require('node-gyp-build')(dir)`
|
|
61
|
+
// call site keeps working through gjsify's cjs-compat interop — a callable, not
|
|
62
|
+
// an ESM namespace.
|
|
63
|
+
//
|
|
64
|
+
// Portability note (same as `gjs-gi-node.ts`): the `filter` is a Rolldown
|
|
65
|
+
// fast-path; the internal guard in the handler is the load-bearing check so the
|
|
66
|
+
// plugin is correct even when the filter does not pre-filter.
|
|
67
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
68
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
69
|
+
const NAPI_ADDON_VIRTUAL_PREFIX = '\0gjsify-napi-addon:';
|
|
70
|
+
/** Bare specifier the shims import — resolved + bundled from the consumer graph. */
|
|
71
|
+
const NAPI_BARE_SPECIFIER = '@gjsify/napi';
|
|
72
|
+
/**
|
|
73
|
+
* Platform-triple tail of a napi-rs sibling platform package
|
|
74
|
+
* (`@node-rs/argon2-linux-x64-gnu`) or its local fallback
|
|
75
|
+
* (`argon2.linux-x64-gnu.node`). Deliberately narrow — an interception on a
|
|
76
|
+
* false match is self-correcting (it only fires when the specifier ALSO
|
|
77
|
+
* resolves to a real `.node`), but keeping the shape tight avoids needless
|
|
78
|
+
* `this.resolve` probes on ordinary deps.
|
|
79
|
+
*/
|
|
80
|
+
const NAPI_RS_TRIPLE_RE = /-(?:linux|darwin|win32|freebsd|openbsd|sunos|android|aix)-(?:x64|arm64|arm|ia32|ppc64|s390x|riscv64|loong64)(?:-(?:gnu|musl|msvc|eabi|eabihf|androideabi|gnueabihf))?$/;
|
|
81
|
+
/** Fast-path filter (superset of what `claimSpecifier` claims). Handler re-checks. */
|
|
82
|
+
const ADDON_FILTER_RE = new RegExp([
|
|
83
|
+
/\.node$/.source, // direct .node
|
|
84
|
+
/^node-gyp-build(?:\/index\.js)?$/.source, // node-gyp-build helper
|
|
85
|
+
/^bindings(?:\/bindings\.js)?$/.source, // bindings helper
|
|
86
|
+
NAPI_RS_TRIPLE_RE.source, // napi-rs platform sibling
|
|
87
|
+
// napi-rs generated-loader ENTRY: the conventional package-root
|
|
88
|
+
// `index.{js,cjs,mjs}` a napi-rs build emits. Narrow on purpose (only
|
|
89
|
+
// `index.*`, not every `.js`) — the handler's `detectNapiRsEntry` gate
|
|
90
|
+
// (package.json signal + native-main match) is the load-bearing check.
|
|
91
|
+
/[/\\]index\.[cm]?js$/.source,
|
|
92
|
+
].join('|'));
|
|
93
|
+
function isMusl(platform) {
|
|
94
|
+
// Match node-gyp-build's isAlpine() probe.
|
|
95
|
+
return platform === 'linux' && existsSync('/etc/alpine-release');
|
|
96
|
+
}
|
|
97
|
+
function hostTarget() {
|
|
98
|
+
const platform = process.env.npm_config_platform || process.platform;
|
|
99
|
+
const arch = process.env.npm_config_arch || process.arch;
|
|
100
|
+
const abi = process.versions ? process.versions.modules : undefined;
|
|
101
|
+
const uv = ((process.versions && process.versions.uv) || '').split('.')[0] || '';
|
|
102
|
+
const armv = process.env.ARM_VERSION || (arch === 'arm64' ? '8' : '') || '';
|
|
103
|
+
const libc = process.env.LIBC === 'musl' || isMusl(platform) ? 'musl' : 'glibc';
|
|
104
|
+
return { platform, arch, libc, abi, uv, armv, runtime: 'node' };
|
|
105
|
+
}
|
|
106
|
+
function readdirSafe(dir) {
|
|
107
|
+
try {
|
|
108
|
+
return readdirSync(dir);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** First `*.node` in `dir` (sorted for determinism), or null. */
|
|
115
|
+
function firstNodeFile(dir) {
|
|
116
|
+
const all = readdirSafe(dir)
|
|
117
|
+
.filter((n) => n.endsWith('.node'))
|
|
118
|
+
.sort();
|
|
119
|
+
if (all.length === 0)
|
|
120
|
+
return null;
|
|
121
|
+
return { file: all[0], all };
|
|
122
|
+
}
|
|
123
|
+
function parseTuple(name) {
|
|
124
|
+
const arr = name.split('-');
|
|
125
|
+
if (arr.length !== 2)
|
|
126
|
+
return null;
|
|
127
|
+
const platform = arr[0];
|
|
128
|
+
const architectures = arr[1].split('+');
|
|
129
|
+
if (!platform || architectures.length === 0 || !architectures.every(Boolean))
|
|
130
|
+
return null;
|
|
131
|
+
return { name, platform, architectures };
|
|
132
|
+
}
|
|
133
|
+
function parseTags(file) {
|
|
134
|
+
const arr = file.split('.');
|
|
135
|
+
const extension = arr.pop();
|
|
136
|
+
const tags = { file, specificity: 0 };
|
|
137
|
+
if (extension !== 'node')
|
|
138
|
+
return null;
|
|
139
|
+
for (const tag of arr) {
|
|
140
|
+
if (tag === 'node' || tag === 'electron' || tag === 'node-webkit') {
|
|
141
|
+
tags.runtime = tag;
|
|
142
|
+
}
|
|
143
|
+
else if (tag === 'napi') {
|
|
144
|
+
tags.napi = true;
|
|
145
|
+
}
|
|
146
|
+
else if (tag.slice(0, 3) === 'abi') {
|
|
147
|
+
tags.abi = tag.slice(3);
|
|
148
|
+
}
|
|
149
|
+
else if (tag.slice(0, 2) === 'uv') {
|
|
150
|
+
tags.uv = tag.slice(2);
|
|
151
|
+
}
|
|
152
|
+
else if (tag.slice(0, 4) === 'armv') {
|
|
153
|
+
tags.armv = tag.slice(4);
|
|
154
|
+
}
|
|
155
|
+
else if (tag === 'glibc' || tag === 'musl') {
|
|
156
|
+
tags.libc = tag;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
tags.specificity++;
|
|
162
|
+
}
|
|
163
|
+
return tags;
|
|
164
|
+
}
|
|
165
|
+
function runtimeAgnostic(tags) {
|
|
166
|
+
return tags.runtime === 'node' && tags.napi === true;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Resolve the best `prebuilds/<platform>-<arch>/<file>.node` for `pkgRoot`,
|
|
170
|
+
* ported from node-gyp-build's `resolve(dir)` — tolerant of a missing `abi`.
|
|
171
|
+
*/
|
|
172
|
+
function resolvePrebuild(pkgRoot, host) {
|
|
173
|
+
const prebuildsDir = join(pkgRoot, 'prebuilds');
|
|
174
|
+
const tuple = readdirSafe(prebuildsDir)
|
|
175
|
+
.map(parseTuple)
|
|
176
|
+
.filter((t) => t !== null && t.platform === host.platform && t.architectures.includes(host.arch))
|
|
177
|
+
// Prefer single-arch prebuilds over multi-arch (compareTuples).
|
|
178
|
+
.sort((a, b) => a.architectures.length - b.architectures.length)[0];
|
|
179
|
+
if (!tuple)
|
|
180
|
+
return null;
|
|
181
|
+
const tupleDir = join(prebuildsDir, tuple.name);
|
|
182
|
+
const winner = readdirSafe(tupleDir)
|
|
183
|
+
.map(parseTags)
|
|
184
|
+
.filter((t) => {
|
|
185
|
+
if (t === null)
|
|
186
|
+
return false;
|
|
187
|
+
if (t.runtime && t.runtime !== host.runtime && !runtimeAgnostic(t))
|
|
188
|
+
return false;
|
|
189
|
+
// abi undefined (GJS host): reject an abi-pinned, non-napi prebuild —
|
|
190
|
+
// only runtime-agnostic napi prebuilds legitimately match.
|
|
191
|
+
if (t.abi && t.abi !== host.abi && !t.napi)
|
|
192
|
+
return false;
|
|
193
|
+
if (t.uv && t.uv !== host.uv)
|
|
194
|
+
return false;
|
|
195
|
+
if (t.armv && t.armv !== host.armv)
|
|
196
|
+
return false;
|
|
197
|
+
if (t.libc && t.libc !== host.libc)
|
|
198
|
+
return false;
|
|
199
|
+
return true;
|
|
200
|
+
})
|
|
201
|
+
// compareTags: matching runtime first, abi over napi, then specificity.
|
|
202
|
+
.sort((a, b) => {
|
|
203
|
+
if (a.runtime !== b.runtime)
|
|
204
|
+
return a.runtime === host.runtime ? -1 : 1;
|
|
205
|
+
if (a.abi !== b.abi)
|
|
206
|
+
return a.abi ? -1 : 1;
|
|
207
|
+
if (a.specificity !== b.specificity)
|
|
208
|
+
return a.specificity > b.specificity ? -1 : 1;
|
|
209
|
+
return 0;
|
|
210
|
+
})[0];
|
|
211
|
+
return winner ? join(tupleDir, winner.file) : null;
|
|
212
|
+
}
|
|
213
|
+
/** A native addon package root has no resolvable compiled `.node`. */
|
|
214
|
+
export class AddonNotBuiltError extends Error {
|
|
215
|
+
constructor(pkgRoot) {
|
|
216
|
+
super(`[gjsify-napi-addon] no compiled .node found for '${pkgRoot}'. Build it ` +
|
|
217
|
+
`(node-gyp / prebuildify) or install a prebuild before bundling for --app gjs.`);
|
|
218
|
+
this.name = 'AddonNotBuiltError';
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Locate the compiled `.node` for an addon package root, matching
|
|
223
|
+
* node-gyp-build's probe order: `build/Release` → `build/Debug` →
|
|
224
|
+
* `prebuilds/<platform>-<arch>/<best tag>`. Throws {@link AddonNotBuiltError}
|
|
225
|
+
* when nothing is found.
|
|
226
|
+
*
|
|
227
|
+
* @param pkgRoot Absolute path to the addon package root (dir with package.json).
|
|
228
|
+
* @param opts.warn Optional sink for a non-fatal warning (ambiguous build dir).
|
|
229
|
+
*/
|
|
230
|
+
export function resolveAddonPath(pkgRoot, opts) {
|
|
231
|
+
const host = hostTarget();
|
|
232
|
+
for (const flavor of ['Release', 'Debug']) {
|
|
233
|
+
const dir = join(pkgRoot, 'build', flavor);
|
|
234
|
+
const hit = firstNodeFile(dir);
|
|
235
|
+
if (hit) {
|
|
236
|
+
if (hit.all.length > 1 && opts?.warn) {
|
|
237
|
+
opts.warn(`[gjsify-napi-addon] ${hit.all.length} .node files in ${dir} ` +
|
|
238
|
+
`(${hit.all.join(', ')}); picking '${hit.file}'. If wrong, import the ` +
|
|
239
|
+
`.node directly.`);
|
|
240
|
+
}
|
|
241
|
+
return join(dir, hit.file);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const prebuild = resolvePrebuild(pkgRoot, host);
|
|
245
|
+
if (prebuild)
|
|
246
|
+
return prebuild;
|
|
247
|
+
throw new AddonNotBuiltError(pkgRoot);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Walk up from an importer file to the nearest `package.json` directory (the
|
|
251
|
+
* addon package root). Returns null when none is found (should not happen for a
|
|
252
|
+
* real node_modules dep).
|
|
253
|
+
*/
|
|
254
|
+
export function nearestPackageRoot(importerFile) {
|
|
255
|
+
let dir = isAbsolute(importerFile) ? dirname(importerFile) : dirname(resolve(importerFile));
|
|
256
|
+
// Guard against an infinite loop at the filesystem root.
|
|
257
|
+
for (let i = 0; i < 64; i++) {
|
|
258
|
+
if (existsSync(join(dir, 'package.json')))
|
|
259
|
+
return dir;
|
|
260
|
+
const parent = dirname(dir);
|
|
261
|
+
if (parent === dir)
|
|
262
|
+
break;
|
|
263
|
+
dir = parent;
|
|
264
|
+
}
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
// Shim generators — the virtual-module bodies. All import `@gjsify/napi` by BARE
|
|
269
|
+
// specifier. The callable-helper shims are CJS (`module.exports = fn`) so the
|
|
270
|
+
// consumer's `require('node-gyp-build')(dir)` / `require('bindings')(name)` stays
|
|
271
|
+
// callable through cjs-compat interop.
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
/** Direct `.node` import → the addon's exports (ESM default). */
|
|
274
|
+
export function directNodeShim(addonPath) {
|
|
275
|
+
return (`import { loadAddon } from ${JSON.stringify(NAPI_BARE_SPECIFIER)};\n` +
|
|
276
|
+
`export default loadAddon(${JSON.stringify(addonPath)});\n`);
|
|
277
|
+
}
|
|
278
|
+
/** `node-gyp-build` replacement — a callable `load(dir)` carrying `.path()`. */
|
|
279
|
+
export function nodeGypBuildShim(addonPath) {
|
|
280
|
+
return (`const { loadAddon } = require(${JSON.stringify(NAPI_BARE_SPECIFIER)});\n` +
|
|
281
|
+
`function load() { return loadAddon(${JSON.stringify(addonPath)}); }\n` +
|
|
282
|
+
`load.path = function () { return ${JSON.stringify(addonPath)}; };\n` +
|
|
283
|
+
`load.resolve = load.path;\n` +
|
|
284
|
+
`module.exports = load;\n`);
|
|
285
|
+
}
|
|
286
|
+
/** `bindings` replacement — a callable `bindings(name)` returning the addon. */
|
|
287
|
+
export function bindingsShim(addonPath) {
|
|
288
|
+
return (`const { loadAddon } = require(${JSON.stringify(NAPI_BARE_SPECIFIER)});\n` +
|
|
289
|
+
`function bindings() { return loadAddon(${JSON.stringify(addonPath)}); }\n` +
|
|
290
|
+
`module.exports = bindings;\n`);
|
|
291
|
+
}
|
|
292
|
+
/** napi-rs sibling → the raw native exports as the module value. */
|
|
293
|
+
export function napiRsShim(addonPath) {
|
|
294
|
+
return (`const { loadAddon } = require(${JSON.stringify(NAPI_BARE_SPECIFIER)});\n` +
|
|
295
|
+
`module.exports = loadAddon(${JSON.stringify(addonPath)});\n`);
|
|
296
|
+
}
|
|
297
|
+
function shimFor(kind, addonPath) {
|
|
298
|
+
switch (kind) {
|
|
299
|
+
case 'direct':
|
|
300
|
+
return directNodeShim(addonPath);
|
|
301
|
+
case 'node-gyp-build':
|
|
302
|
+
return nodeGypBuildShim(addonPath);
|
|
303
|
+
case 'bindings':
|
|
304
|
+
return bindingsShim(addonPath);
|
|
305
|
+
case 'napi-rs':
|
|
306
|
+
case 'napi-rs-entry':
|
|
307
|
+
// Both hand back the raw native exports as the module value. The
|
|
308
|
+
// `napi-rs-entry` kind replaces the whole GENERATED loader; `napi-rs`
|
|
309
|
+
// replaces a directly-imported platform sibling. Same body.
|
|
310
|
+
return napiRsShim(addonPath);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function encodeVirtual(kind, addonPath) {
|
|
314
|
+
return `${NAPI_ADDON_VIRTUAL_PREFIX}${kind}:${addonPath}`;
|
|
315
|
+
}
|
|
316
|
+
function decodeVirtual(id) {
|
|
317
|
+
if (!id.startsWith(NAPI_ADDON_VIRTUAL_PREFIX))
|
|
318
|
+
return null;
|
|
319
|
+
const rest = id.slice(NAPI_ADDON_VIRTUAL_PREFIX.length);
|
|
320
|
+
const sep = rest.indexOf(':');
|
|
321
|
+
if (sep === -1)
|
|
322
|
+
return null;
|
|
323
|
+
const kind = rest.slice(0, sep);
|
|
324
|
+
const addonPath = rest.slice(sep + 1);
|
|
325
|
+
return { kind, addonPath };
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Classify a specifier for interception. Pure decision logic (no filesystem
|
|
329
|
+
* probing beyond what `resolveAddonPath` does via the importer) — exported for
|
|
330
|
+
* the unit tests. Returns the shim kind + the package root to probe, or the
|
|
331
|
+
* literal `.node` path for the direct case.
|
|
332
|
+
*/
|
|
333
|
+
export function classifySpecifier(source) {
|
|
334
|
+
if (source === 'node-gyp-build' || source === 'node-gyp-build/index.js')
|
|
335
|
+
return { kind: 'node-gyp-build' };
|
|
336
|
+
if (source === 'bindings' || source === 'bindings/bindings.js')
|
|
337
|
+
return { kind: 'bindings' };
|
|
338
|
+
if (source.endsWith('.node'))
|
|
339
|
+
return { kind: 'direct-node' };
|
|
340
|
+
// A napi-rs platform sibling (`@scope/pkg-<triple>`) — only a CANDIDATE; the
|
|
341
|
+
// handler confirms by resolving it to a real `.node`.
|
|
342
|
+
const last = source.split('/').pop() ?? source;
|
|
343
|
+
if (!source.startsWith('.') && NAPI_RS_TRIPLE_RE.test(last))
|
|
344
|
+
return { kind: 'napi-rs-candidate' };
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* A package.json describes a napi-rs generated-loader package when EITHER
|
|
349
|
+
* signal holds — both are package.json-level (not source sniffing):
|
|
350
|
+
*
|
|
351
|
+
* (a) a top-level `napi` config OBJECT (`{ binaryName, targets, … }`) — the
|
|
352
|
+
* napi-rs CLI's own build block. An ordinary npm package never declares a
|
|
353
|
+
* `napi` object at the manifest root.
|
|
354
|
+
* (b) at least one `optionalDependencies` entry named `<self>-<triple>` — the
|
|
355
|
+
* napi-rs prebuilt-binary convention (`@node-rs/argon2` ships
|
|
356
|
+
* `@node-rs/argon2-linux-x64-gnu`, …). The key MUST start with the
|
|
357
|
+
* package's own name plus `-` AND end in a platform triple, so a normal
|
|
358
|
+
* optional dep (`fsevents`) can't match.
|
|
359
|
+
*
|
|
360
|
+
* Neither fires on a normal package, so pairing this with the "resolved file IS
|
|
361
|
+
* the package's own native main entry" check (see {@link detectNapiRsEntry})
|
|
362
|
+
* makes entry-replacement safe. Exported for the unit tests.
|
|
363
|
+
*/
|
|
364
|
+
export function isNapiRsPackageJson(pkg) {
|
|
365
|
+
if (pkg && typeof pkg.napi === 'object' && pkg.napi !== null)
|
|
366
|
+
return true;
|
|
367
|
+
const self = pkg?.name;
|
|
368
|
+
const opt = pkg?.optionalDependencies;
|
|
369
|
+
if (self && opt) {
|
|
370
|
+
for (const dep of Object.keys(opt)) {
|
|
371
|
+
if (dep.startsWith(`${self}-`) && NAPI_RS_TRIPLE_RE.test(dep))
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
function readPackageJsonSafe(pkgRoot) {
|
|
378
|
+
try {
|
|
379
|
+
return JSON.parse(readFileSync(join(pkgRoot, 'package.json'), 'utf8'));
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Decide whether an absolute file path is the NATIVE generated-loader entry of a
|
|
387
|
+
* napi-rs package. Returns the package root + parsed manifest when it is, else
|
|
388
|
+
* null. "Native entry" = the package's `main` (default `index.js`) — NOT its
|
|
389
|
+
* `browser`/wasm fallback and NOT a deep file — so we only ever replace the one
|
|
390
|
+
* generated loader, never an unrelated module. Filesystem-based (reads the
|
|
391
|
+
* nearest package.json); the plugin memoizes calls. Exported for the unit tests.
|
|
392
|
+
*/
|
|
393
|
+
export function detectNapiRsEntry(entryFile) {
|
|
394
|
+
if (!isAbsolute(entryFile) || !/\.[cm]?js$/.test(entryFile))
|
|
395
|
+
return null;
|
|
396
|
+
const pkgRoot = nearestPackageRoot(entryFile);
|
|
397
|
+
if (pkgRoot === null)
|
|
398
|
+
return null;
|
|
399
|
+
const pkg = readPackageJsonSafe(pkgRoot);
|
|
400
|
+
if (pkg === null || !isNapiRsPackageJson(pkg))
|
|
401
|
+
return null;
|
|
402
|
+
// The resolved file must be the package's OWN native main entry — reject a
|
|
403
|
+
// `browser`/wasm fallback or any deep module inside the package.
|
|
404
|
+
const mainEntry = resolve(pkgRoot, pkg.main ?? 'index.js');
|
|
405
|
+
if (resolve(entryFile) !== mainEntry)
|
|
406
|
+
return null;
|
|
407
|
+
return { pkgRoot, pkg };
|
|
408
|
+
}
|
|
409
|
+
/** napi-rs binaryName (from `napi.binaryName`/`napi.name`, else the unscoped pkg name). */
|
|
410
|
+
function napiBinaryName(pkg) {
|
|
411
|
+
const napi = pkg.napi;
|
|
412
|
+
if (napi && typeof napi === 'object') {
|
|
413
|
+
const cfg = napi;
|
|
414
|
+
const bn = typeof cfg.binaryName === 'string' ? cfg.binaryName : cfg.name;
|
|
415
|
+
if (typeof bn === 'string' && bn)
|
|
416
|
+
return bn;
|
|
417
|
+
}
|
|
418
|
+
if (typeof pkg.name === 'string' && pkg.name)
|
|
419
|
+
return pkg.name.replace(/^@[^/]+\//, '');
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* The napi-rs short platform triple for the CURRENT host (`linux-x64-gnu`,
|
|
424
|
+
* `darwin-arm64`, `win32-x64-msvc`, …) — the tail napi-rs stamps into a sibling
|
|
425
|
+
* package name (`@node-rs/argon2-linux-x64-gnu`) and a local binary
|
|
426
|
+
* (`argon2.linux-x64-gnu.node`). Used ONLY for the deterministic local-file
|
|
427
|
+
* fallback; the sibling path never needs it (npm installs only the matching
|
|
428
|
+
* optionalDependency). Returns null for a host napi-rs doesn't name.
|
|
429
|
+
*/
|
|
430
|
+
function hostNapiRsTriple() {
|
|
431
|
+
const platform = process.platform;
|
|
432
|
+
const arch = process.arch;
|
|
433
|
+
const archTok = {
|
|
434
|
+
x64: 'x64',
|
|
435
|
+
arm64: 'arm64',
|
|
436
|
+
arm: 'arm',
|
|
437
|
+
ia32: 'ia32',
|
|
438
|
+
ppc64: 'ppc64',
|
|
439
|
+
s390x: 's390x',
|
|
440
|
+
riscv64: 'riscv64',
|
|
441
|
+
};
|
|
442
|
+
const a = archTok[arch];
|
|
443
|
+
if (!a)
|
|
444
|
+
return null;
|
|
445
|
+
switch (platform) {
|
|
446
|
+
case 'linux': {
|
|
447
|
+
if (arch === 'arm')
|
|
448
|
+
return 'linux-arm-gnueabihf';
|
|
449
|
+
const libc = process.env.LIBC === 'musl' || isMusl('linux') ? 'musl' : 'gnu';
|
|
450
|
+
return `linux-${a}-${libc}`;
|
|
451
|
+
}
|
|
452
|
+
case 'darwin':
|
|
453
|
+
return `darwin-${a}`;
|
|
454
|
+
case 'win32':
|
|
455
|
+
return `win32-${a}-msvc`;
|
|
456
|
+
case 'freebsd':
|
|
457
|
+
return `freebsd-${a}`;
|
|
458
|
+
case 'android':
|
|
459
|
+
return arch === 'arm' ? 'android-arm-eabi' : `android-${a}`;
|
|
460
|
+
default:
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
/** Decode a napi virtual id back to its raw `.node` path (safety net for a resolve hit). */
|
|
465
|
+
function rawAddonPath(id) {
|
|
466
|
+
const decoded = decodeVirtual(id);
|
|
467
|
+
return decoded ? decoded.addonPath : id;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Resolve the current-platform compiled `.node` for a napi-rs generated-loader
|
|
471
|
+
* package: the current-triple sibling package (`@scope/pkg-<triple>`, whose own
|
|
472
|
+
* `main` IS the `.node` — npm installs ONLY the host's optionalDependency, so
|
|
473
|
+
* the one that resolves is the host's), then a local `pkg.<triple>.node`. Reuses
|
|
474
|
+
* the plugin's `ctx.resolve` sibling-resolution path (`skipSelf` so this
|
|
475
|
+
* plugin's own napi-rs-candidate interception is bypassed → a raw `.node` id).
|
|
476
|
+
* Returns null when no current-platform binary is present — the caller then
|
|
477
|
+
* DOES NOT rewrite, so we never shim over a missing file.
|
|
478
|
+
*/
|
|
479
|
+
async function resolveNapiRsEntryAddon(ctx, pkgRoot, pkg, importer) {
|
|
480
|
+
const self = pkg.name;
|
|
481
|
+
const opt = pkg.optionalDependencies ?? {};
|
|
482
|
+
if (self) {
|
|
483
|
+
for (const dep of Object.keys(opt)) {
|
|
484
|
+
if (!dep.startsWith(`${self}-`) || !NAPI_RS_TRIPLE_RE.test(dep))
|
|
485
|
+
continue;
|
|
486
|
+
const resolved = await ctx.resolve(dep, importer, { skipSelf: true });
|
|
487
|
+
if (!resolved)
|
|
488
|
+
continue;
|
|
489
|
+
const abs = rawAddonPath(resolved.id);
|
|
490
|
+
if (abs.endsWith('.node') && existsSync(abs))
|
|
491
|
+
return abs;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
// Local in-package binary (`<binaryName>.<host-triple>.node`) — deterministic
|
|
495
|
+
// host-triple match so a wrong-platform local file is never picked.
|
|
496
|
+
const binaryName = napiBinaryName(pkg);
|
|
497
|
+
const triple = hostNapiRsTriple();
|
|
498
|
+
if (binaryName && triple) {
|
|
499
|
+
const local = join(pkgRoot, `${binaryName}.${triple}.node`);
|
|
500
|
+
if (existsSync(local))
|
|
501
|
+
return local;
|
|
502
|
+
}
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
/** Emit a non-fatal warning through the context, if it supports it. */
|
|
506
|
+
function warnSafe(ctx, msg) {
|
|
507
|
+
if (typeof ctx.warn === 'function')
|
|
508
|
+
ctx.warn(msg);
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Resolve a direct `.node` specifier to an EXISTING absolute path
|
|
512
|
+
* (relative/bare/abs), or null when it does not resolve to a real file. Null is
|
|
513
|
+
* load-bearing: a napi-rs generated loader statically references a local
|
|
514
|
+
* `./pkg.<triple>.node` fallback that is absent when the binary ships in the
|
|
515
|
+
* sibling platform package — that dead branch must fall through to normal
|
|
516
|
+
* (external) resolution, NOT be rewritten to a shim over a missing file.
|
|
517
|
+
*/
|
|
518
|
+
async function resolveNodeFile(ctx, source, importer) {
|
|
519
|
+
if (isAbsolute(source))
|
|
520
|
+
return existsSync(source) ? source : null;
|
|
521
|
+
if (importer !== undefined) {
|
|
522
|
+
// Importer-relative path math first (a plain `./foo.node`), then the full
|
|
523
|
+
// resolver chain (a bare `pkg/foo.node`). Only claim an EXISTING file.
|
|
524
|
+
const rel = resolve(dirname(importer), source);
|
|
525
|
+
if (existsSync(rel))
|
|
526
|
+
return rel;
|
|
527
|
+
const resolved = await ctx.resolve(source, importer, { skipSelf: true });
|
|
528
|
+
if (resolved && resolved.id.endsWith('.node') && existsSync(resolved.id))
|
|
529
|
+
return resolved.id;
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Transparent `.node`-addon loader for `gjsify build --app gjs`.
|
|
536
|
+
*
|
|
537
|
+
* resolveId (`order: 'pre'`, importer-aware) intercepts a native-addon
|
|
538
|
+
* acquisition and rewrites it to a virtual module whose value is
|
|
539
|
+
* `loadAddon('<abs .node>')`. Inert unless such a specifier is in the graph —
|
|
540
|
+
* every other id returns null. Register ONLY for `--app gjs` (the C-ABI runs
|
|
541
|
+
* under `@gjsify/napi`); never for node/browser/nativescript.
|
|
542
|
+
*/
|
|
543
|
+
export function napiNodeAddonPlugin(options = {}) {
|
|
544
|
+
const warnOnMissingNapi = options.warnOnMissingNapi !== false;
|
|
545
|
+
let missingNapiChecked = false;
|
|
546
|
+
// Memoize napi-rs entry detection per resolved file — the `index.*` filter
|
|
547
|
+
// fires the handler for every package's index entry across every build pass;
|
|
548
|
+
// this bounds the package.json reads to one per unique entry file.
|
|
549
|
+
const napiEntryCache = new Map();
|
|
550
|
+
function detectNapiRsEntryCached(entryFile) {
|
|
551
|
+
const cached = napiEntryCache.get(entryFile);
|
|
552
|
+
if (cached !== undefined)
|
|
553
|
+
return cached;
|
|
554
|
+
const info = detectNapiRsEntry(entryFile);
|
|
555
|
+
napiEntryCache.set(entryFile, info);
|
|
556
|
+
return info;
|
|
557
|
+
}
|
|
558
|
+
async function warnIfNapiMissing(ctx, importer) {
|
|
559
|
+
if (!warnOnMissingNapi || missingNapiChecked)
|
|
560
|
+
return;
|
|
561
|
+
missingNapiChecked = true; // check once per build — interception is rare
|
|
562
|
+
try {
|
|
563
|
+
const found = await ctx.resolve(NAPI_BARE_SPECIFIER, importer, { skipSelf: true });
|
|
564
|
+
if (!found) {
|
|
565
|
+
warnSafe(ctx, `[gjsify-napi-addon] a native .node addon was intercepted but '${NAPI_BARE_SPECIFIER}' ` +
|
|
566
|
+
`is not resolvable — the bundle will fail at load. Install it: gjsify install ${NAPI_BARE_SPECIFIER}`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
catch {
|
|
570
|
+
/* best-effort probe */
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return {
|
|
574
|
+
name: 'gjsify-napi-node-addon',
|
|
575
|
+
resolveId: {
|
|
576
|
+
order: 'pre',
|
|
577
|
+
filter: { id: ADDON_FILTER_RE },
|
|
578
|
+
async handler(source, importer) {
|
|
579
|
+
const ctx = this;
|
|
580
|
+
const cls = classifySpecifier(source);
|
|
581
|
+
if (cls !== null) {
|
|
582
|
+
// Direct `.node` — resolve the file path itself.
|
|
583
|
+
if (cls.kind === 'direct-node') {
|
|
584
|
+
const abs = await resolveNodeFile(ctx, source, importer);
|
|
585
|
+
if (abs === null)
|
|
586
|
+
return null; // unresolvable — let the default chain error
|
|
587
|
+
await warnIfNapiMissing(ctx, importer);
|
|
588
|
+
return { id: encodeVirtual('direct', abs) };
|
|
589
|
+
}
|
|
590
|
+
// napi-rs platform sibling — confirm it resolves to a `.node`.
|
|
591
|
+
if (cls.kind === 'napi-rs-candidate') {
|
|
592
|
+
const resolved = await ctx.resolve(source, importer, { skipSelf: true });
|
|
593
|
+
if (!resolved || !resolved.id.endsWith('.node'))
|
|
594
|
+
return null; // not a native sibling
|
|
595
|
+
await warnIfNapiMissing(ctx, importer);
|
|
596
|
+
return { id: encodeVirtual('napi-rs', resolved.id) };
|
|
597
|
+
}
|
|
598
|
+
// node-gyp-build / bindings — probe the importer's package root.
|
|
599
|
+
if (importer === undefined)
|
|
600
|
+
return null;
|
|
601
|
+
const pkgRoot = nearestPackageRoot(importer);
|
|
602
|
+
if (pkgRoot === null)
|
|
603
|
+
return null;
|
|
604
|
+
const addonPath = resolveAddonPath(pkgRoot, { warn: (m) => warnSafe(ctx, m) }); // throws → build error
|
|
605
|
+
await warnIfNapiMissing(ctx, importer);
|
|
606
|
+
return { id: encodeVirtual(cls.kind, addonPath) };
|
|
607
|
+
}
|
|
608
|
+
// napi-rs GENERATED-LOADER ENTRY (`index.*` filter). Replace the
|
|
609
|
+
// whole module only when the resolved file IS the native `main`
|
|
610
|
+
// of a napi-rs package (package.json signal) AND a current-platform
|
|
611
|
+
// `.node` resolves. Otherwise fall through (never a shim over a
|
|
612
|
+
// missing file, never an unrelated package's entry rewritten).
|
|
613
|
+
const entry = detectNapiRsEntryCached(source);
|
|
614
|
+
if (entry !== null) {
|
|
615
|
+
const addonPath = await resolveNapiRsEntryAddon(ctx, entry.pkgRoot, entry.pkg, source);
|
|
616
|
+
if (addonPath !== null) {
|
|
617
|
+
await warnIfNapiMissing(ctx, importer);
|
|
618
|
+
return { id: encodeVirtual('napi-rs-entry', addonPath) };
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return null;
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
load(id) {
|
|
625
|
+
const decoded = decodeVirtual(id);
|
|
626
|
+
if (decoded === null)
|
|
627
|
+
return null;
|
|
628
|
+
return { code: shimFor(decoded.kind, decoded.addonPath), moduleSideEffects: false };
|
|
629
|
+
},
|
|
630
|
+
};
|
|
631
|
+
}
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import { BUNDLE_URL_BANNER } from './bundle-url-banner.js';
|
|
2
2
|
import { GJS_WELLKNOWN_SYMBOLS_STUB } from './wellknown-symbols-banner.js';
|
|
3
|
+
// Every GJS ambient global is reached through `globalThis.` — NEVER as a bare
|
|
4
|
+
// identifier. The banner shares the module's top-level scope with the bundled
|
|
5
|
+
// code, so a bare `imports` binds to any top-level `const imports` a bundled
|
|
6
|
+
// module declares — and since the banner runs at byte 1, that binding is still
|
|
7
|
+
// in its temporal dead zone: `ReferenceError: can't access lexical declaration
|
|
8
|
+
// 'imports' before initialization`, thrown at load, before a single line of
|
|
9
|
+
// program code runs. `@girs/gjs`'s `gjs.js` declares exactly that
|
|
10
|
+
// (`const imports = globalThis.imports || {}`), so the failure is real and
|
|
11
|
+
// arrives whenever tree-shaking happens to retain that module. It was found
|
|
12
|
+
// via `@gjsify/web-streams`, whose GJS test leg died the moment `@gjsify/utils`
|
|
13
|
+
// became more shakeable — but nothing about it is specific to either package.
|
|
3
14
|
export const GJS_PROCESS_STUB = 'if(typeof globalThis.process==="undefined"){' +
|
|
4
|
-
'const _s=imports.system,_G=imports.gi.GLib;' +
|
|
15
|
+
'const _s=globalThis.imports.system,_G=globalThis.imports.gi.GLib;' +
|
|
5
16
|
// process.hrtime needs a `.bigint` property attached to the function
|
|
6
17
|
// itself (Node API shape: `process.hrtime.bigint()` — used by execa,
|
|
7
18
|
// perf-tracking libs, …). Build it as a named local so we can
|
|
@@ -21,7 +32,7 @@ export const GJS_PROCESS_STUB = 'if(typeof globalThis.process==="undefined"){' +
|
|
|
21
32
|
'versions:{},config:{},' +
|
|
22
33
|
'cwd(){return _G.get_current_dir()||"/"},' +
|
|
23
34
|
'exit(c){_s.exit(c??0)},' +
|
|
24
|
-
'stderr:{write(s){printerr(s)}},stdout:{write(s){print(s)}},stdin:null,' +
|
|
35
|
+
'stderr:{write(s){globalThis.printerr(s)}},stdout:{write(s){globalThis.print(s)}},stdin:null,' +
|
|
25
36
|
'exitCode:undefined,' +
|
|
26
37
|
'nextTick(fn,...a){Promise.resolve().then(()=>fn(...a))},' +
|
|
27
38
|
'hrtime:_h,' +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/rolldown-plugin-gjsify",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.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.
|
|
59
|
-
"@gjsify/resolve-npm": "^0.
|
|
60
|
-
"@gjsify/rolldown-plugin-deepkit": "^0.
|
|
61
|
-
"@gjsify/rolldown-plugin-pnp": "^0.
|
|
62
|
-
"@gjsify/vite-plugin-blueprint": "^0.
|
|
58
|
+
"@gjsify/console": "^0.23.0",
|
|
59
|
+
"@gjsify/resolve-npm": "^0.23.0",
|
|
60
|
+
"@gjsify/rolldown-plugin-deepkit": "^0.23.0",
|
|
61
|
+
"@gjsify/rolldown-plugin-pnp": "^0.23.0",
|
|
62
|
+
"@gjsify/vite-plugin-blueprint": "^0.23.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.
|
|
71
|
+
"@gjsify/lightningcss-native": "^0.23.0",
|
|
72
72
|
"rolldown": "^1.1.4"
|
|
73
73
|
},
|
|
74
74
|
"peerDependenciesMeta": {
|