react-native-gtkx 0.1.0-alpha.4 → 0.2.0-alpha.2
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/README.md +22 -9
- package/dist/adw/index.d.ts +3 -0
- package/dist/adw/index.js +41 -0
- package/dist/adw/index.js.map +1 -0
- package/dist/adw/widgets.generated.d.ts +63 -0
- package/dist/adw/widgets.generated.js +125 -0
- package/dist/adw/widgets.generated.js.map +1 -0
- package/dist/animated/create-animated.d.ts +2 -0
- package/dist/animated/create-animated.js +2 -0
- package/dist/animated/create-animated.js.map +1 -1
- package/dist/animated/index.d.ts +2 -0
- package/dist/animated/index.js +1 -0
- package/dist/animated/index.js.map +1 -1
- package/dist/animated/value-xy.d.ts +41 -0
- package/dist/animated/value-xy.js +90 -0
- package/dist/animated/value-xy.js.map +1 -0
- package/dist/animated/value.d.ts +9 -0
- package/dist/animated/value.js +30 -2
- package/dist/animated/value.js.map +1 -1
- package/dist/common/content.d.ts +69 -0
- package/dist/common/content.js +84 -0
- package/dist/common/content.js.map +1 -0
- package/dist/common/icon.d.ts +23 -0
- package/dist/common/icon.js +14 -0
- package/dist/common/icon.js.map +1 -0
- package/dist/common/index.d.ts +5 -0
- package/dist/common/index.js +21 -0
- package/dist/common/index.js.map +1 -0
- package/dist/common/list.d.ts +82 -0
- package/dist/common/list.js +166 -0
- package/dist/common/list.js.map +1 -0
- package/dist/common/navigation-stack.d.ts +84 -0
- package/dist/common/navigation-stack.js +313 -0
- package/dist/common/navigation-stack.js.map +1 -0
- package/dist/common/widget.d.ts +62 -0
- package/dist/common/widget.js +209 -0
- package/dist/common/widget.js.map +1 -0
- package/dist/components/animated.d.ts +7 -4
- package/dist/components/animated.js +88 -67
- package/dist/components/animated.js.map +1 -1
- package/dist/components/app-registry.d.ts +36 -1
- package/dist/components/app-registry.js +8 -3
- package/dist/components/app-registry.js.map +1 -1
- package/dist/components/host-node.d.ts +7 -0
- package/dist/components/host-node.js +26 -1
- package/dist/components/host-node.js.map +1 -1
- package/dist/components/index.d.ts +6 -2
- package/dist/components/index.js +2 -1
- package/dist/components/index.js.map +1 -1
- package/dist/components/measure.d.ts +13 -0
- package/dist/components/measure.js +51 -0
- package/dist/components/measure.js.map +1 -0
- package/dist/components/modal.d.ts +1 -1
- package/dist/components/press-event.d.ts +28 -0
- package/dist/components/press-event.js +46 -0
- package/dist/components/press-event.js.map +1 -0
- package/dist/components/pressable.d.ts +2 -6
- package/dist/components/pressable.js +119 -20
- package/dist/components/pressable.js.map +1 -1
- package/dist/components/rect-store.d.ts +11 -1
- package/dist/components/rect-store.js +27 -3
- package/dist/components/rect-store.js.map +1 -1
- package/dist/components/scroll-view.d.ts +4 -0
- package/dist/components/scroll-view.js +8 -2
- package/dist/components/scroll-view.js.map +1 -1
- package/dist/components/svg/animated-support.d.ts +19 -0
- package/dist/components/svg/animated-support.js +64 -0
- package/dist/components/svg/animated-support.js.map +1 -0
- package/dist/components/svg/context.d.ts +8 -0
- package/dist/components/svg/context.js +15 -0
- package/dist/components/svg/context.js.map +1 -0
- package/dist/components/svg/gradients.d.ts +31 -0
- package/dist/components/svg/gradients.js +93 -0
- package/dist/components/svg/gradients.js.map +1 -0
- package/dist/components/svg/group.d.ts +8 -0
- package/dist/components/svg/group.js +30 -0
- package/dist/components/svg/group.js.map +1 -0
- package/dist/components/svg/index.d.ts +6 -0
- package/dist/components/svg/index.js +5 -0
- package/dist/components/svg/index.js.map +1 -0
- package/dist/components/svg/node.d.ts +7 -0
- package/dist/components/svg/node.js +7 -0
- package/dist/components/svg/node.js.map +1 -0
- package/dist/components/svg/paint.d.ts +24 -0
- package/dist/components/svg/paint.js +57 -0
- package/dist/components/svg/paint.js.map +1 -0
- package/dist/components/svg/shapes.d.ts +47 -0
- package/dist/components/svg/shapes.js +51 -0
- package/dist/components/svg/shapes.js.map +1 -0
- package/dist/components/svg/svg.d.ts +14 -0
- package/dist/components/svg/svg.js +48 -0
- package/dist/components/svg/svg.js.map +1 -0
- package/dist/components/svg/use-shape-node.d.ts +12 -0
- package/dist/components/svg/use-shape-node.js +52 -0
- package/dist/components/svg/use-shape-node.js.map +1 -0
- package/dist/components/text-input.js +1 -1
- package/dist/components/text-input.js.map +1 -1
- package/dist/components/text.js +36 -3
- package/dist/components/text.js.map +1 -1
- package/dist/components/use-layout-child.js +50 -4
- package/dist/components/use-layout-child.js.map +1 -1
- package/dist/components/view.d.ts +9 -4
- package/dist/components/view.js +8 -2
- package/dist/components/view.js.map +1 -1
- package/dist/contracts.d.ts +31 -4
- package/dist/contracts.js.map +1 -1
- package/dist/gtk/index.d.ts +7 -0
- package/dist/gtk/index.js +46 -0
- package/dist/gtk/index.js.map +1 -0
- package/dist/gtk/widgets.generated.d.ts +103 -0
- package/dist/gtk/widgets.generated.js +205 -0
- package/dist/gtk/widgets.generated.js.map +1 -0
- package/dist/gtk/window-actions.d.ts +77 -0
- package/dist/gtk/window-actions.js +193 -0
- package/dist/gtk/window-actions.js.map +1 -0
- package/dist/gtkx/bridge/geometry.d.ts +15 -0
- package/dist/gtkx/bridge/geometry.js +38 -0
- package/dist/gtkx/bridge/geometry.js.map +1 -0
- package/dist/gtkx/bridge/index.d.ts +11 -4
- package/dist/gtkx/bridge/index.js +47 -6
- package/dist/gtkx/bridge/index.js.map +1 -1
- package/dist/gtkx/bridge/layout-manager.d.ts +2 -1
- package/dist/gtkx/bridge/layout-manager.js +31 -9
- package/dist/gtkx/bridge/layout-manager.js.map +1 -1
- package/dist/gtkx/bridge/slot-portal.d.ts +12 -0
- package/dist/gtkx/bridge/slot-portal.js +17 -0
- package/dist/gtkx/bridge/slot-portal.js.map +1 -0
- package/dist/gtkx/bridge/svg-node.d.ts +79 -0
- package/dist/gtkx/bridge/svg-node.js +323 -0
- package/dist/gtkx/bridge/svg-node.js.map +1 -0
- package/dist/gtkx/bridge/use-signal.js +19 -9
- package/dist/gtkx/bridge/use-signal.js.map +1 -1
- package/dist/gtkx/bridge/widgets.generated.d.ts +2 -0
- package/dist/gtkx/bridge/widgets.generated.js +14 -0
- package/dist/gtkx/bridge/widgets.generated.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/bin.d.ts +2 -0
- package/dist/mcp/bin.js +17 -0
- package/dist/mcp/bin.js.map +1 -0
- package/dist/mcp/data/generated.d.ts +1304 -0
- package/dist/mcp/data/generated.js +1179 -0
- package/dist/mcp/data/generated.js.map +1 -0
- package/dist/mcp/index.d.ts +5 -0
- package/dist/mcp/index.js +9 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/list.d.ts +4 -0
- package/dist/mcp/list.js +56 -0
- package/dist/mcp/list.js.map +1 -0
- package/dist/mcp/resolve.d.ts +55 -0
- package/dist/mcp/resolve.js +151 -0
- package/dist/mcp/resolve.js.map +1 -0
- package/dist/mcp/search.d.ts +13 -0
- package/dist/mcp/search.js +48 -0
- package/dist/mcp/search.js.map +1 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.js +135 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tool.d.ts +21 -0
- package/dist/mcp/tool.js +36 -0
- package/dist/mcp/tool.js.map +1 -0
- package/dist/metro/index.js +10 -0
- package/dist/metro/index.js.map +1 -1
- package/dist/navigation/index.d.ts +50 -13
- package/dist/navigation/index.js +183 -148
- package/dist/navigation/index.js.map +1 -1
- package/dist/navigation/option-warnings.js +9 -1
- package/dist/navigation/option-warnings.js.map +1 -1
- package/dist/navigation/sidebar.d.ts +207 -9
- package/dist/navigation/sidebar.js +294 -10
- package/dist/navigation/sidebar.js.map +1 -1
- package/dist/responder/system.d.ts +27 -0
- package/dist/responder/system.js +184 -0
- package/dist/responder/system.js.map +1 -0
- package/dist/responder/touch-history.d.ts +10 -0
- package/dist/responder/touch-history.js +78 -0
- package/dist/responder/touch-history.js.map +1 -0
- package/dist/responder/types.d.ts +63 -0
- package/dist/responder/types.js +29 -0
- package/dist/responder/types.js.map +1 -0
- package/dist/responder/use-responder.d.ts +12 -0
- package/dist/responder/use-responder.js +98 -0
- package/dist/responder/use-responder.js.map +1 -0
- package/dist/runner/host-dev.js +3 -1
- package/dist/runner/host-dev.js.map +1 -1
- package/dist/runner/host.js +5 -1
- package/dist/runner/host.js.map +1 -1
- package/dist/runner/index.d.ts +22 -2
- package/dist/runner/index.js +163 -13
- package/dist/runner/index.js.map +1 -1
- package/dist/sea/assemble.d.ts +29 -0
- package/dist/sea/assemble.js +129 -0
- package/dist/sea/assemble.js.map +1 -0
- package/dist/sea/bundle.d.ts +61 -0
- package/dist/sea/bundle.js +202 -0
- package/dist/sea/bundle.js.map +1 -0
- package/dist/sea/gtkx-config-module.d.ts +18 -0
- package/dist/sea/gtkx-config-module.js +67 -0
- package/dist/sea/gtkx-config-module.js.map +1 -0
- package/dist/sea/native-shim.d.ts +40 -0
- package/dist/sea/native-shim.js +121 -0
- package/dist/sea/native-shim.js.map +1 -0
- package/dist/sea/react-anchor-plugin.d.ts +2 -0
- package/dist/sea/react-anchor-plugin.js +30 -0
- package/dist/sea/react-anchor-plugin.js.map +1 -0
- package/dist/style/box-shadow.d.ts +7 -0
- package/dist/style/box-shadow.js +197 -0
- package/dist/style/box-shadow.js.map +1 -0
- package/dist/style/index.d.ts +3 -0
- package/dist/style/index.js +3 -0
- package/dist/style/index.js.map +1 -1
- package/dist/style/split-style.d.ts +5 -2
- package/dist/style/split-style.js +33 -5
- package/dist/style/split-style.js.map +1 -1
- package/dist/style/text-decoration.d.ts +15 -0
- package/dist/style/text-decoration.js +26 -0
- package/dist/style/text-decoration.js.map +1 -0
- package/dist/style/transform.d.ts +22 -0
- package/dist/style/transform.js +137 -0
- package/dist/style/transform.js.map +1 -0
- package/dist/style/visual-css.js +40 -3
- package/dist/style/visual-css.js.map +1 -1
- package/dist/svg/geometry.d.ts +19 -0
- package/dist/svg/geometry.js +80 -0
- package/dist/svg/geometry.js.map +1 -0
- package/dist/svg/gradient-geometry.d.ts +15 -0
- package/dist/svg/gradient-geometry.js +15 -0
- package/dist/svg/gradient-geometry.js.map +1 -0
- package/dist/svg/transform.d.ts +22 -0
- package/dist/svg/transform.js +74 -0
- package/dist/svg/transform.js.map +1 -0
- package/dist/svg/view-box.d.ts +34 -0
- package/dist/svg/view-box.js +108 -0
- package/dist/svg/view-box.js.map +1 -0
- package/dist/svg-compat/index.d.ts +1 -0
- package/dist/svg-compat/index.js +16 -0
- package/dist/svg-compat/index.js.map +1 -0
- package/dist/testing/index.d.ts +14 -0
- package/dist/testing/index.js +14 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/vendor/react-native/pan-responder.d.ts +102 -0
- package/dist/vendor/react-native/pan-responder.js +196 -0
- package/dist/vendor/react-native/pan-responder.js.map +1 -0
- package/dist/vendor/react-native/touch-history-math.d.ts +18 -0
- package/dist/vendor/react-native/touch-history-math.js +102 -0
- package/dist/vendor/react-native/touch-history-math.js.map +1 -0
- package/dist/vite/index.d.ts +7 -3
- package/dist/vite/index.js +16 -4
- package/dist/vite/index.js.map +1 -1
- package/dist/vitest/index.d.ts +52 -0
- package/dist/vitest/index.js +58 -0
- package/dist/vitest/index.js.map +1 -0
- package/dist/vitest/setup.d.ts +4 -0
- package/dist/vitest/setup.js +22 -0
- package/dist/vitest/setup.js.map +1 -0
- package/package.json +52 -13
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Turns a Metro-built app into ONE executable file: bundle everything into
|
|
2
|
+
// a CJS entry (./bundle.ts), generate a SEA blob from it, then inject that
|
|
3
|
+
// blob into a copy of the `node` binary. This is the whole of what "SEA"
|
|
4
|
+
// means — Node's Single Executable Application format is literally a copy
|
|
5
|
+
// of the node binary with a V8 code-cache blob appended, which is why the
|
|
6
|
+
// result necessarily carries Node's own weight (~117 MB on linux-arm64,
|
|
7
|
+
// against <7 MB for the app code and its native addon).
|
|
8
|
+
//
|
|
9
|
+
// Follows gtkx's own tutorial (gtkx-org/gtkx examples/tutorial/scripts/
|
|
10
|
+
// build-sea.sh) for these mechanics; ./bundle.ts's header documents where
|
|
11
|
+
// the bundling half deliberately diverges.
|
|
12
|
+
//
|
|
13
|
+
// Kept out of ../runner/index.ts and reached through a dynamic import so
|
|
14
|
+
// the bundler is only loaded when a SEA is actually asked for; the
|
|
15
|
+
// ordinary `build-linux` path never touches it.
|
|
16
|
+
import { spawnSync } from "node:child_process";
|
|
17
|
+
import { chmodSync, copyFileSync, mkdirSync, statSync, writeFileSync, } from "node:fs";
|
|
18
|
+
import { dirname, join } from "node:path";
|
|
19
|
+
import { bundleMetroSea } from "./bundle.js";
|
|
20
|
+
/** The fuse postject looks for inside the node binary to place the blob. */
|
|
21
|
+
const SENTINEL_FUSE = "NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2";
|
|
22
|
+
/** Pinned: postject has no stable release, and the SEA docs name this one. */
|
|
23
|
+
const POSTJECT = "postject@1.0.0-alpha.6";
|
|
24
|
+
const run = (command, args) => {
|
|
25
|
+
const result = spawnSync(command, args, { stdio: "inherit" });
|
|
26
|
+
if (result.error) {
|
|
27
|
+
throw result.error;
|
|
28
|
+
}
|
|
29
|
+
if (result.status !== 0) {
|
|
30
|
+
throw new Error(`${command} ${args.join(" ")} exited with ${result.status}`);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Drops the debug symbols from the copied Node binary. Worth a step of its
|
|
35
|
+
* own because the saving is not marginal: the `node` binary distributed by
|
|
36
|
+
* NodeSource for Ubuntu ships `with debug_info, not stripped` — measured at
|
|
37
|
+
* 117 MB, and 98 MB after `strip --strip-all`. Nothing in a SEA needs those
|
|
38
|
+
* symbols; the only cost is that a crash inside Node's own C++ produces an
|
|
39
|
+
* unsymbolicated backtrace.
|
|
40
|
+
*
|
|
41
|
+
* Runs BEFORE postject, never after: `--strip-all` removes non-allocated
|
|
42
|
+
* sections, which is exactly what the injected NODE_SEA_BLOB is.
|
|
43
|
+
*
|
|
44
|
+
* Best-effort — a build machine without binutils still produces a working
|
|
45
|
+
* (larger) executable rather than failing, so this cannot become a new
|
|
46
|
+
* install requirement for a build that worked yesterday.
|
|
47
|
+
*/
|
|
48
|
+
const stripSymbols = (file) => {
|
|
49
|
+
const before = statSync(file).size;
|
|
50
|
+
const result = spawnSync("strip", ["--strip-all", file], {
|
|
51
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
52
|
+
});
|
|
53
|
+
if (result.error || result.status !== 0) {
|
|
54
|
+
console.warn("[react-native-gtkx] skipped stripping symbols (no usable `strip`) — " +
|
|
55
|
+
"the executable will be tens of MB larger than it needs to be");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const saved = (before - statSync(file).size) / 1024 / 1024;
|
|
59
|
+
console.warn(`[react-native-gtkx] stripped debug symbols (-${saved.toFixed(0)} MB)`);
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Produces the middle artifact: one self-contained CJS file with the app,
|
|
63
|
+
* its whole node_modules closure and the native addon inlined, run by a
|
|
64
|
+
* system `node`. Everything the SEA is except the embedded Node runtime —
|
|
65
|
+
* so ~120 MB smaller, at the cost of a `nodejs` dependency to declare.
|
|
66
|
+
* Returns its size in bytes.
|
|
67
|
+
*/
|
|
68
|
+
export const bundleStandalone = async (options) => {
|
|
69
|
+
const { appRoot, jsbundlePath, outFile } = options;
|
|
70
|
+
mkdirSync(dirname(outFile), { recursive: true });
|
|
71
|
+
console.warn("[react-native-gtkx] bundling a standalone script…");
|
|
72
|
+
await bundleMetroSea({
|
|
73
|
+
appRoot,
|
|
74
|
+
jsbundlePath,
|
|
75
|
+
outFile,
|
|
76
|
+
nativeAddonSource: "inline",
|
|
77
|
+
});
|
|
78
|
+
return statSync(outFile).size;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Produces a single executable at {@link AssembleSeaOptions.outFile} and
|
|
82
|
+
* returns its size in bytes. Requires network access on first run unless
|
|
83
|
+
* postject is already in the npx cache.
|
|
84
|
+
*/
|
|
85
|
+
export const assembleSea = async (options) => {
|
|
86
|
+
const { appRoot, jsbundlePath, outFile } = options;
|
|
87
|
+
const workDir = options.workDir ?? dirname(jsbundlePath);
|
|
88
|
+
mkdirSync(workDir, { recursive: true });
|
|
89
|
+
mkdirSync(dirname(outFile), { recursive: true });
|
|
90
|
+
console.warn("[react-native-gtkx] bundling for a single executable…");
|
|
91
|
+
const bundlePath = join(workDir, "sea-bundle.cjs");
|
|
92
|
+
const nativeAddon = await bundleMetroSea({
|
|
93
|
+
appRoot,
|
|
94
|
+
jsbundlePath,
|
|
95
|
+
outFile: bundlePath,
|
|
96
|
+
});
|
|
97
|
+
console.warn(`[react-native-gtkx] native addon: ${nativeAddon.path}`);
|
|
98
|
+
const configPath = join(workDir, "sea-config.json");
|
|
99
|
+
writeFileSync(configPath, `${JSON.stringify({
|
|
100
|
+
main: bundlePath,
|
|
101
|
+
output: join(workDir, "sea-prep.blob"),
|
|
102
|
+
disableExperimentalSEAWarning: true,
|
|
103
|
+
assets: { [nativeAddon.key]: nativeAddon.path },
|
|
104
|
+
}, null, 2)}\n`);
|
|
105
|
+
console.warn("[react-native-gtkx] generating the SEA blob…");
|
|
106
|
+
run(process.execPath, ["--experimental-sea-config", configPath]);
|
|
107
|
+
console.warn("[react-native-gtkx] copying the node binary…");
|
|
108
|
+
copyFileSync(process.execPath, outFile);
|
|
109
|
+
if (process.platform === "darwin") {
|
|
110
|
+
// The copy inherits a signature that no longer matches once the blob
|
|
111
|
+
// is injected; macOS refuses to exec it until the signature is gone.
|
|
112
|
+
run("codesign", ["--remove-signature", outFile]);
|
|
113
|
+
}
|
|
114
|
+
stripSymbols(outFile);
|
|
115
|
+
console.warn("[react-native-gtkx] injecting the SEA blob…");
|
|
116
|
+
run("npx", [
|
|
117
|
+
"--yes",
|
|
118
|
+
"--ignore-scripts",
|
|
119
|
+
POSTJECT,
|
|
120
|
+
outFile,
|
|
121
|
+
"NODE_SEA_BLOB",
|
|
122
|
+
join(workDir, "sea-prep.blob"),
|
|
123
|
+
"--sentinel-fuse",
|
|
124
|
+
SENTINEL_FUSE,
|
|
125
|
+
]);
|
|
126
|
+
chmodSync(outFile, 0o755);
|
|
127
|
+
return statSync(outFile).size;
|
|
128
|
+
};
|
|
129
|
+
//# sourceMappingURL=assemble.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assemble.js","sourceRoot":"","sources":["../../src/sea/assemble.ts"],"names":[],"mappings":"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","sourcesContent":["// Turns a Metro-built app into ONE executable file: bundle everything into\n// a CJS entry (./bundle.ts), generate a SEA blob from it, then inject that\n// blob into a copy of the `node` binary. This is the whole of what \"SEA\"\n// means — Node's Single Executable Application format is literally a copy\n// of the node binary with a V8 code-cache blob appended, which is why the\n// result necessarily carries Node's own weight (~117 MB on linux-arm64,\n// against <7 MB for the app code and its native addon).\n//\n// Follows gtkx's own tutorial (gtkx-org/gtkx examples/tutorial/scripts/\n// build-sea.sh) for these mechanics; ./bundle.ts's header documents where\n// the bundling half deliberately diverges.\n//\n// Kept out of ../runner/index.ts and reached through a dynamic import so\n// the bundler is only loaded when a SEA is actually asked for; the\n// ordinary `build-linux` path never touches it.\nimport { spawnSync } from \"node:child_process\"\nimport {\n chmodSync,\n copyFileSync,\n mkdirSync,\n statSync,\n writeFileSync,\n} from \"node:fs\"\nimport { dirname, join } from \"node:path\"\nimport { bundleMetroSea } from \"./bundle.js\"\n\n/** The fuse postject looks for inside the node binary to place the blob. */\nconst SENTINEL_FUSE = \"NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2\"\n\n/** Pinned: postject has no stable release, and the SEA docs name this one. */\nconst POSTJECT = \"postject@1.0.0-alpha.6\"\n\nexport type AssembleSeaOptions = {\n /** The app root — where gtkx.config.ts and package.json live. */\n appRoot: string\n /** The release jsbundle `build-linux` just wrote. */\n jsbundlePath: string\n /** Where the finished executable is written. */\n outFile: string\n /** Scratch directory for the intermediate CJS bundle, the SEA config and\n * the prep blob (default: alongside the jsbundle). */\n workDir?: string\n}\n\nconst run = (command: string, args: string[]): void => {\n const result = spawnSync(command, args, { stdio: \"inherit\" })\n if (result.error) {\n throw result.error\n }\n if (result.status !== 0) {\n throw new Error(`${command} ${args.join(\" \")} exited with ${result.status}`)\n }\n}\n\n/**\n * Drops the debug symbols from the copied Node binary. Worth a step of its\n * own because the saving is not marginal: the `node` binary distributed by\n * NodeSource for Ubuntu ships `with debug_info, not stripped` — measured at\n * 117 MB, and 98 MB after `strip --strip-all`. Nothing in a SEA needs those\n * symbols; the only cost is that a crash inside Node's own C++ produces an\n * unsymbolicated backtrace.\n *\n * Runs BEFORE postject, never after: `--strip-all` removes non-allocated\n * sections, which is exactly what the injected NODE_SEA_BLOB is.\n *\n * Best-effort — a build machine without binutils still produces a working\n * (larger) executable rather than failing, so this cannot become a new\n * install requirement for a build that worked yesterday.\n */\nconst stripSymbols = (file: string): void => {\n const before = statSync(file).size\n const result = spawnSync(\"strip\", [\"--strip-all\", file], {\n stdio: [\"ignore\", \"ignore\", \"pipe\"],\n })\n if (result.error || result.status !== 0) {\n console.warn(\n \"[react-native-gtkx] skipped stripping symbols (no usable `strip`) — \" +\n \"the executable will be tens of MB larger than it needs to be\",\n )\n return\n }\n const saved = (before - statSync(file).size) / 1024 / 1024\n console.warn(\n `[react-native-gtkx] stripped debug symbols (-${saved.toFixed(0)} MB)`,\n )\n}\n\n/**\n * Produces the middle artifact: one self-contained CJS file with the app,\n * its whole node_modules closure and the native addon inlined, run by a\n * system `node`. Everything the SEA is except the embedded Node runtime —\n * so ~120 MB smaller, at the cost of a `nodejs` dependency to declare.\n * Returns its size in bytes.\n */\nexport const bundleStandalone = async (options: {\n appRoot: string\n jsbundlePath: string\n outFile: string\n}): Promise<number> => {\n const { appRoot, jsbundlePath, outFile } = options\n mkdirSync(dirname(outFile), { recursive: true })\n console.warn(\"[react-native-gtkx] bundling a standalone script…\")\n await bundleMetroSea({\n appRoot,\n jsbundlePath,\n outFile,\n nativeAddonSource: \"inline\",\n })\n return statSync(outFile).size\n}\n\n/**\n * Produces a single executable at {@link AssembleSeaOptions.outFile} and\n * returns its size in bytes. Requires network access on first run unless\n * postject is already in the npx cache.\n */\nexport const assembleSea = async (\n options: AssembleSeaOptions,\n): Promise<number> => {\n const { appRoot, jsbundlePath, outFile } = options\n const workDir = options.workDir ?? dirname(jsbundlePath)\n mkdirSync(workDir, { recursive: true })\n mkdirSync(dirname(outFile), { recursive: true })\n\n console.warn(\"[react-native-gtkx] bundling for a single executable…\")\n const bundlePath = join(workDir, \"sea-bundle.cjs\")\n const nativeAddon = await bundleMetroSea({\n appRoot,\n jsbundlePath,\n outFile: bundlePath,\n })\n console.warn(`[react-native-gtkx] native addon: ${nativeAddon.path}`)\n\n const configPath = join(workDir, \"sea-config.json\")\n writeFileSync(\n configPath,\n `${JSON.stringify(\n {\n main: bundlePath,\n output: join(workDir, \"sea-prep.blob\"),\n disableExperimentalSEAWarning: true,\n assets: { [nativeAddon.key]: nativeAddon.path },\n },\n null,\n 2,\n )}\\n`,\n )\n\n console.warn(\"[react-native-gtkx] generating the SEA blob…\")\n run(process.execPath, [\"--experimental-sea-config\", configPath])\n\n console.warn(\"[react-native-gtkx] copying the node binary…\")\n copyFileSync(process.execPath, outFile)\n if (process.platform === \"darwin\") {\n // The copy inherits a signature that no longer matches once the blob\n // is injected; macOS refuses to exec it until the signature is gone.\n run(\"codesign\", [\"--remove-signature\", outFile])\n }\n stripSymbols(outFile)\n\n console.warn(\"[react-native-gtkx] injecting the SEA blob…\")\n run(\"npx\", [\n \"--yes\",\n \"--ignore-scripts\",\n POSTJECT,\n outFile,\n \"NODE_SEA_BLOB\",\n join(workDir, \"sea-prep.blob\"),\n \"--sentinel-fuse\",\n SENTINEL_FUSE,\n ])\n\n chmodSync(outFile, 0o755)\n return statSync(outFile).size\n}\n"]}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type NativeAddonSource } from "./native-shim.js";
|
|
2
|
+
export type MetroSeaBundleOptions = {
|
|
3
|
+
/** The app root — where gtkx.config.ts lives and package.json resolves
|
|
4
|
+
* HOST_MODULE_EXTERNALS' bare specifiers (react, @gtkx/*, ...) from. */
|
|
5
|
+
appRoot: string;
|
|
6
|
+
/** Path to the release jsbundle produced by `build-linux`. */
|
|
7
|
+
jsbundlePath: string;
|
|
8
|
+
/** Where to write the bundled CJS entry (e.g. dist/bundle.cjs). */
|
|
9
|
+
outFile: string;
|
|
10
|
+
/** Where the native addon's bytes come from at runtime — "sea-asset"
|
|
11
|
+
* for a real single executable, "inline" for a self-contained .cjs run
|
|
12
|
+
* by a system Node (default: "sea-asset"). */
|
|
13
|
+
nativeAddonSource?: NativeAddonSource;
|
|
14
|
+
};
|
|
15
|
+
export type NativeAddonAsset = {
|
|
16
|
+
/** Absolute path to the platform's @gtkx/native-<platform>-<libc>.node
|
|
17
|
+
* that npm installed for this machine. */
|
|
18
|
+
path: string;
|
|
19
|
+
/** The SEA asset key it must be embedded under. */
|
|
20
|
+
key: string;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Locates the ONE native addon file npm installed for this machine.
|
|
24
|
+
* @gtkx/native picks exactly one optionalDependency per platform/libc at
|
|
25
|
+
* install time (see @gtkx/native/index.js's NAPI-RS-generated loader) —
|
|
26
|
+
* failing loudly on anything other than exactly one match matters here:
|
|
27
|
+
* silently picking the wrong one would embed an addon for the wrong
|
|
28
|
+
* architecture with no error until the app actually launches.
|
|
29
|
+
*/
|
|
30
|
+
export declare const resolveNativeAddon: (appRoot: string) => NativeAddonAsset;
|
|
31
|
+
/**
|
|
32
|
+
* Builds the synthetic SEA entry source: every HOST_MODULE_EXTERNALS name
|
|
33
|
+
* loaded through a dynamic `import()` (mirroring runner/host.ts's own
|
|
34
|
+
* `load()`, which does the same against a real node_modules at runtime),
|
|
35
|
+
* assembled into globalThis.__hostModules, then the jsbundle text executed
|
|
36
|
+
* the same way host.ts does.
|
|
37
|
+
*
|
|
38
|
+
* Dynamic import, not static, throughout: a Node SEA's main script is
|
|
39
|
+
* CommonJS only — confirmed empirically, Node 24 executes the embedded
|
|
40
|
+
* main as CJS regardless of a "type": "module" field in sea-config.json
|
|
41
|
+
* or an .mjs extension — and top-level await is a hard error under a "cjs"
|
|
42
|
+
* output format REGARDLESS of whether the module is reached via a static
|
|
43
|
+
* or dynamic import (the restriction is format-wide, not per-module —
|
|
44
|
+
* verified: switching yoga-layout's static import to a dynamic one alone
|
|
45
|
+
* did not clear the error). yoga-layout's own entry point has exactly this
|
|
46
|
+
* top-level await (it loads its WASM binary asynchronously — see
|
|
47
|
+
* node_modules/yoga-layout/dist/src/index.js: `const Yoga =
|
|
48
|
+
* wrapAssembly(await loadYoga())` at module scope). The actual fix: import
|
|
49
|
+
* "yoga-layout/load" instead, which exports the same `await`, just inside
|
|
50
|
+
* an async FUNCTION (`export async function loadYoga()`), not at the
|
|
51
|
+
* file's top level — bundleable, and awaited here like everything else.
|
|
52
|
+
*/
|
|
53
|
+
export declare const buildEntrySource: (jsbundlePath: string) => string;
|
|
54
|
+
/**
|
|
55
|
+
* Produces `outFile`, a single CJS file with the jsbundle, its
|
|
56
|
+
* HOST_MODULE_EXTERNALS and gtkx.config.ts's resolved values all inlined,
|
|
57
|
+
* and the native addon import redirected to the SEA-asset extraction
|
|
58
|
+
* shim. Returns the native addon that must be embedded as a SEA asset
|
|
59
|
+
* under {@link NativeAddonAsset.key} for the result to run.
|
|
60
|
+
*/
|
|
61
|
+
export declare const bundleMetroSea: (options: MetroSeaBundleOptions) => Promise<NativeAddonAsset>;
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// Bundles a Metro-built react-native-gtkx app (main.jsbundle) into a
|
|
2
|
+
// single CJS file suitable for Node's --experimental-sea-config, undoing
|
|
3
|
+
// exactly the externalization HOST_MODULE_EXTERNALS performs at Metro
|
|
4
|
+
// build time.
|
|
5
|
+
//
|
|
6
|
+
// Design, and where this follows/diverges from gtkx's own tutorial
|
|
7
|
+
// (gtkx-org/gtkx examples/tutorial/scripts/bundle.ts and
|
|
8
|
+
// bundle-postject.ts — read before any of this was written):
|
|
9
|
+
//
|
|
10
|
+
// - Bundling mechanics (bundle → single minified CJS file) and the
|
|
11
|
+
// SEA/postject pipeline itself (./assemble.ts) follow the tutorial
|
|
12
|
+
// closely. The bundler is rolldown rather than the tutorial's esbuild —
|
|
13
|
+
// NOT a preference: rolldown is vite's own engine (vite 8 depends on it
|
|
14
|
+
// outright, esbuild is only an optional peer there), and vite is a hard
|
|
15
|
+
// dependency of @gtkx/cli, which is a hard dependency of this package.
|
|
16
|
+
// So rolldown is already installed for every consumer, and esbuild would
|
|
17
|
+
// have been the one genuinely new bundler in the tree.
|
|
18
|
+
// - Native addon: the tutorial's shim assumes the addon file sits BESIDE
|
|
19
|
+
// the built executable (require resolved from
|
|
20
|
+
// dirname(process.execPath)). That is two files, not one — the actual
|
|
21
|
+
// central design decision this epic exists to make — so this diverges:
|
|
22
|
+
// the addon is embedded IN the artifact (as a SEA asset, or as a base64
|
|
23
|
+
// literal for the --standalone .cjs) and extracted to a per-user cache
|
|
24
|
+
// directory on first run. See ./native-shim.ts's header for the full
|
|
25
|
+
// reasoning.
|
|
26
|
+
// - Metro's externals: the tutorial has no equivalent problem, its source
|
|
27
|
+
// is already a single vite/rollup bundle. Metro deliberately
|
|
28
|
+
// externalizes @gtkx/*, react and yoga-layout (../metro/index.ts,
|
|
29
|
+
// HOST_MODULE_EXTERNALS) so main.jsbundle expects
|
|
30
|
+
// globalThis.__hostModules/__hostRequire to exist before it runs (see
|
|
31
|
+
// ../runner/host.ts). This module is a third host implementation
|
|
32
|
+
// (host.ts and host-dev.ts are the other two — see host-dev.ts's header
|
|
33
|
+
// for why they don't share code) that builds those globals from real,
|
|
34
|
+
// STATICALLY imported modules — every name in HOST_MODULE_EXTERNALS
|
|
35
|
+
// becomes a literal `import * as` statement in the generated entry — so
|
|
36
|
+
// the bundler can inline them instead of the app needing a runtime
|
|
37
|
+
// node_modules to dynamically load them from. gtkx.config.ts is also
|
|
38
|
+
// resolved once here, at bundle time (see ./gtkx-config-module.ts).
|
|
39
|
+
//
|
|
40
|
+
// Not attempted here: the vite path. Investigated, not a drop-in of this
|
|
41
|
+
// same technique — see docs/getting-started.md's "Shipping an app"
|
|
42
|
+
// section for the concrete blocker found empirically while building this:
|
|
43
|
+
// the vite bundle loads the native addon through a DYNAMICALLY obtained
|
|
44
|
+
// require (`createRequire(import.meta.url)("./gtkx.node")`), which a
|
|
45
|
+
// bundler does not intercept the way it intercepts a static import
|
|
46
|
+
// (verified: the resolve hook never fires for it), and which is unlikely
|
|
47
|
+
// to resolve at all once running from inside a real SEA blob, where
|
|
48
|
+
// import.meta.url no longer points at a file with real siblings on disk.
|
|
49
|
+
import { mkdirSync, readdirSync, readFileSync } from "node:fs";
|
|
50
|
+
import { createRequire } from "node:module";
|
|
51
|
+
import { dirname, join } from "node:path";
|
|
52
|
+
import { rolldown } from "rolldown";
|
|
53
|
+
import { HOST_MODULE_EXTERNALS } from "../metro/index.js";
|
|
54
|
+
import { buildGtkxConfigModule, virtualConfigModulePlugin, } from "./gtkx-config-module.js";
|
|
55
|
+
import { NATIVE_ASSET_KEY, nativeAddonShimPlugin, } from "./native-shim.js";
|
|
56
|
+
import { reactAnchorPlugin } from "./react-anchor-plugin.js";
|
|
57
|
+
/**
|
|
58
|
+
* Locates the ONE native addon file npm installed for this machine.
|
|
59
|
+
* @gtkx/native picks exactly one optionalDependency per platform/libc at
|
|
60
|
+
* install time (see @gtkx/native/index.js's NAPI-RS-generated loader) —
|
|
61
|
+
* failing loudly on anything other than exactly one match matters here:
|
|
62
|
+
* silently picking the wrong one would embed an addon for the wrong
|
|
63
|
+
* architecture with no error until the app actually launches.
|
|
64
|
+
*/
|
|
65
|
+
export const resolveNativeAddon = (appRoot) => {
|
|
66
|
+
const appRequire = createRequire(join(appRoot, "package.json"));
|
|
67
|
+
const nativePackageJson = appRequire.resolve("@gtkx/native/package.json");
|
|
68
|
+
// .../node_modules/@gtkx/native/package.json -> .../node_modules/@gtkx
|
|
69
|
+
const scopeDir = dirname(dirname(nativePackageJson));
|
|
70
|
+
const candidates = readdirSync(scopeDir, { withFileTypes: true })
|
|
71
|
+
.filter((entry) => entry.isDirectory() && entry.name.startsWith("native-"))
|
|
72
|
+
.flatMap((entry) => {
|
|
73
|
+
const dir = join(scopeDir, entry.name);
|
|
74
|
+
return readdirSync(dir)
|
|
75
|
+
.filter((file) => file.endsWith(".node"))
|
|
76
|
+
.map((file) => join(dir, file));
|
|
77
|
+
});
|
|
78
|
+
const [first, ...rest] = candidates;
|
|
79
|
+
if (!first || rest.length > 0) {
|
|
80
|
+
throw new Error(`expected exactly one @gtkx/native-*/*.node under ${scopeDir}, found ` +
|
|
81
|
+
`${candidates.length} (${candidates.join(", ") || "none"}) — ` +
|
|
82
|
+
"reinstall targeting a single platform.");
|
|
83
|
+
}
|
|
84
|
+
return { path: first, key: NATIVE_ASSET_KEY };
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Builds the synthetic SEA entry source: every HOST_MODULE_EXTERNALS name
|
|
88
|
+
* loaded through a dynamic `import()` (mirroring runner/host.ts's own
|
|
89
|
+
* `load()`, which does the same against a real node_modules at runtime),
|
|
90
|
+
* assembled into globalThis.__hostModules, then the jsbundle text executed
|
|
91
|
+
* the same way host.ts does.
|
|
92
|
+
*
|
|
93
|
+
* Dynamic import, not static, throughout: a Node SEA's main script is
|
|
94
|
+
* CommonJS only — confirmed empirically, Node 24 executes the embedded
|
|
95
|
+
* main as CJS regardless of a "type": "module" field in sea-config.json
|
|
96
|
+
* or an .mjs extension — and top-level await is a hard error under a "cjs"
|
|
97
|
+
* output format REGARDLESS of whether the module is reached via a static
|
|
98
|
+
* or dynamic import (the restriction is format-wide, not per-module —
|
|
99
|
+
* verified: switching yoga-layout's static import to a dynamic one alone
|
|
100
|
+
* did not clear the error). yoga-layout's own entry point has exactly this
|
|
101
|
+
* top-level await (it loads its WASM binary asynchronously — see
|
|
102
|
+
* node_modules/yoga-layout/dist/src/index.js: `const Yoga =
|
|
103
|
+
* wrapAssembly(await loadYoga())` at module scope). The actual fix: import
|
|
104
|
+
* "yoga-layout/load" instead, which exports the same `await`, just inside
|
|
105
|
+
* an async FUNCTION (`export async function loadYoga()`), not at the
|
|
106
|
+
* file's top level — bundleable, and awaited here like everything else.
|
|
107
|
+
*/
|
|
108
|
+
export const buildEntrySource = (jsbundlePath) => {
|
|
109
|
+
const jsbundleSource = readFileSync(jsbundlePath, "utf8");
|
|
110
|
+
const loadLines = HOST_MODULE_EXTERNALS.map((name) => {
|
|
111
|
+
if (name === "yoga-layout") {
|
|
112
|
+
return [
|
|
113
|
+
` {`,
|
|
114
|
+
` const { loadYoga, ...rest } = await import("yoga-layout/load");`,
|
|
115
|
+
` hostModules["yoga-layout"] = __interopHostModule({ ...rest, default: await loadYoga() });`,
|
|
116
|
+
` }`,
|
|
117
|
+
].join("\n");
|
|
118
|
+
}
|
|
119
|
+
return ` hostModules[${JSON.stringify(name)}] = __interopHostModule(await import(${JSON.stringify(name)}));`;
|
|
120
|
+
});
|
|
121
|
+
return [
|
|
122
|
+
`async function __gtkxSeaMain() {`,
|
|
123
|
+
// Mirrors runner/host.ts's `load()` interop: import() handles both ESM
|
|
124
|
+
// and CJS, merge a `default` for Babel's default-import interop when
|
|
125
|
+
// the module has none of its own.
|
|
126
|
+
` const __interopHostModule = (ns) => {`,
|
|
127
|
+
` const merged = Object.assign({ __esModule: true }, ns);`,
|
|
128
|
+
` if (!("default" in ns)) merged.default = ns;`,
|
|
129
|
+
` return merged;`,
|
|
130
|
+
` };`,
|
|
131
|
+
` const hostModules = {};`,
|
|
132
|
+
...loadLines,
|
|
133
|
+
` globalThis.__hostModules = hostModules;`,
|
|
134
|
+
// Node builtin proxies resolve lazily through the host's own require —
|
|
135
|
+
// real here, no createRequire indirection needed (see host.ts for why
|
|
136
|
+
// it needs one: this file's import.meta.url is never used for
|
|
137
|
+
// anything relative, only require() of bare/builtin specifiers).
|
|
138
|
+
` globalThis.__hostRequire = require;`,
|
|
139
|
+
` require("node:vm").runInThisContext(${JSON.stringify(jsbundleSource)}, { filename: ${JSON.stringify(jsbundlePath)} });`,
|
|
140
|
+
`}`,
|
|
141
|
+
`__gtkxSeaMain().catch((error) => { console.error(error); process.exitCode = 1; });`,
|
|
142
|
+
].join("\n");
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Produces `outFile`, a single CJS file with the jsbundle, its
|
|
146
|
+
* HOST_MODULE_EXTERNALS and gtkx.config.ts's resolved values all inlined,
|
|
147
|
+
* and the native addon import redirected to the SEA-asset extraction
|
|
148
|
+
* shim. Returns the native addon that must be embedded as a SEA asset
|
|
149
|
+
* under {@link NativeAddonAsset.key} for the result to run.
|
|
150
|
+
*/
|
|
151
|
+
export const bundleMetroSea = async (options) => {
|
|
152
|
+
const { appRoot, jsbundlePath, outFile } = options;
|
|
153
|
+
const nativeAddonSource = options.nativeAddonSource ?? "sea-asset";
|
|
154
|
+
const configModuleSource = await buildGtkxConfigModule(appRoot);
|
|
155
|
+
const nativeAddon = resolveNativeAddon(appRoot);
|
|
156
|
+
const entrySource = buildEntrySource(jsbundlePath);
|
|
157
|
+
mkdirSync(dirname(outFile), { recursive: true });
|
|
158
|
+
// The entry is given a path inside appRoot that does not exist on disk,
|
|
159
|
+
// rather than a `\0`-prefixed virtual id: every bare specifier it
|
|
160
|
+
// imports (react, @gtkx/*, yoga-layout) must resolve from the app's own
|
|
161
|
+
// node_modules, and a real directory is what makes that happen.
|
|
162
|
+
const entryId = join(appRoot, "__gtkx-sea-entry.js");
|
|
163
|
+
const bundle = await rolldown({
|
|
164
|
+
input: entryId,
|
|
165
|
+
cwd: appRoot,
|
|
166
|
+
platform: "node",
|
|
167
|
+
plugins: [
|
|
168
|
+
{
|
|
169
|
+
name: "gtkx-sea-entry",
|
|
170
|
+
resolveId: (source) => (source === entryId ? entryId : null),
|
|
171
|
+
load: (id) => (id === entryId ? entrySource : null),
|
|
172
|
+
},
|
|
173
|
+
reactAnchorPlugin(appRoot),
|
|
174
|
+
nativeAddonShimPlugin({
|
|
175
|
+
specifier: "@gtkx/native",
|
|
176
|
+
appRoot,
|
|
177
|
+
source: nativeAddonSource,
|
|
178
|
+
assetKey: nativeAddon.key,
|
|
179
|
+
addonBytes: nativeAddonSource === "inline"
|
|
180
|
+
? readFileSync(nativeAddon.path)
|
|
181
|
+
: undefined,
|
|
182
|
+
}),
|
|
183
|
+
virtualConfigModulePlugin(configModuleSource, appRoot),
|
|
184
|
+
],
|
|
185
|
+
});
|
|
186
|
+
try {
|
|
187
|
+
await bundle.write({
|
|
188
|
+
file: outFile,
|
|
189
|
+
format: "cjs",
|
|
190
|
+
minify: true,
|
|
191
|
+
// Every HOST_MODULE_EXTERNALS name is reached through a dynamic
|
|
192
|
+
// import; without this each one becomes its own chunk and the
|
|
193
|
+
// "single file" the whole build exists to produce is a directory.
|
|
194
|
+
codeSplitting: false,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
await bundle.close();
|
|
199
|
+
}
|
|
200
|
+
return nativeAddon;
|
|
201
|
+
};
|
|
202
|
+
//# sourceMappingURL=bundle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundle.js","sourceRoot":"","sources":["../../src/sea/bundle.ts"],"names":[],"mappings":"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","sourcesContent":["// Bundles a Metro-built react-native-gtkx app (main.jsbundle) into a\n// single CJS file suitable for Node's --experimental-sea-config, undoing\n// exactly the externalization HOST_MODULE_EXTERNALS performs at Metro\n// build time.\n//\n// Design, and where this follows/diverges from gtkx's own tutorial\n// (gtkx-org/gtkx examples/tutorial/scripts/bundle.ts and\n// bundle-postject.ts — read before any of this was written):\n//\n// - Bundling mechanics (bundle → single minified CJS file) and the\n// SEA/postject pipeline itself (./assemble.ts) follow the tutorial\n// closely. The bundler is rolldown rather than the tutorial's esbuild —\n// NOT a preference: rolldown is vite's own engine (vite 8 depends on it\n// outright, esbuild is only an optional peer there), and vite is a hard\n// dependency of @gtkx/cli, which is a hard dependency of this package.\n// So rolldown is already installed for every consumer, and esbuild would\n// have been the one genuinely new bundler in the tree.\n// - Native addon: the tutorial's shim assumes the addon file sits BESIDE\n// the built executable (require resolved from\n// dirname(process.execPath)). That is two files, not one — the actual\n// central design decision this epic exists to make — so this diverges:\n// the addon is embedded IN the artifact (as a SEA asset, or as a base64\n// literal for the --standalone .cjs) and extracted to a per-user cache\n// directory on first run. See ./native-shim.ts's header for the full\n// reasoning.\n// - Metro's externals: the tutorial has no equivalent problem, its source\n// is already a single vite/rollup bundle. Metro deliberately\n// externalizes @gtkx/*, react and yoga-layout (../metro/index.ts,\n// HOST_MODULE_EXTERNALS) so main.jsbundle expects\n// globalThis.__hostModules/__hostRequire to exist before it runs (see\n// ../runner/host.ts). This module is a third host implementation\n// (host.ts and host-dev.ts are the other two — see host-dev.ts's header\n// for why they don't share code) that builds those globals from real,\n// STATICALLY imported modules — every name in HOST_MODULE_EXTERNALS\n// becomes a literal `import * as` statement in the generated entry — so\n// the bundler can inline them instead of the app needing a runtime\n// node_modules to dynamically load them from. gtkx.config.ts is also\n// resolved once here, at bundle time (see ./gtkx-config-module.ts).\n//\n// Not attempted here: the vite path. Investigated, not a drop-in of this\n// same technique — see docs/getting-started.md's \"Shipping an app\"\n// section for the concrete blocker found empirically while building this:\n// the vite bundle loads the native addon through a DYNAMICALLY obtained\n// require (`createRequire(import.meta.url)(\"./gtkx.node\")`), which a\n// bundler does not intercept the way it intercepts a static import\n// (verified: the resolve hook never fires for it), and which is unlikely\n// to resolve at all once running from inside a real SEA blob, where\n// import.meta.url no longer points at a file with real siblings on disk.\nimport { mkdirSync, readdirSync, readFileSync } from \"node:fs\"\nimport { createRequire } from \"node:module\"\nimport { dirname, join } from \"node:path\"\nimport { rolldown } from \"rolldown\"\nimport { HOST_MODULE_EXTERNALS } from \"../metro/index.js\"\nimport {\n buildGtkxConfigModule,\n virtualConfigModulePlugin,\n} from \"./gtkx-config-module.js\"\nimport {\n NATIVE_ASSET_KEY,\n nativeAddonShimPlugin,\n type NativeAddonSource,\n} from \"./native-shim.js\"\nimport { reactAnchorPlugin } from \"./react-anchor-plugin.js\"\n\nexport type MetroSeaBundleOptions = {\n /** The app root — where gtkx.config.ts lives and package.json resolves\n * HOST_MODULE_EXTERNALS' bare specifiers (react, @gtkx/*, ...) from. */\n appRoot: string\n /** Path to the release jsbundle produced by `build-linux`. */\n jsbundlePath: string\n /** Where to write the bundled CJS entry (e.g. dist/bundle.cjs). */\n outFile: string\n /** Where the native addon's bytes come from at runtime — \"sea-asset\"\n * for a real single executable, \"inline\" for a self-contained .cjs run\n * by a system Node (default: \"sea-asset\"). */\n nativeAddonSource?: NativeAddonSource\n}\n\nexport type NativeAddonAsset = {\n /** Absolute path to the platform's @gtkx/native-<platform>-<libc>.node\n * that npm installed for this machine. */\n path: string\n /** The SEA asset key it must be embedded under. */\n key: string\n}\n\n/**\n * Locates the ONE native addon file npm installed for this machine.\n * @gtkx/native picks exactly one optionalDependency per platform/libc at\n * install time (see @gtkx/native/index.js's NAPI-RS-generated loader) —\n * failing loudly on anything other than exactly one match matters here:\n * silently picking the wrong one would embed an addon for the wrong\n * architecture with no error until the app actually launches.\n */\nexport const resolveNativeAddon = (appRoot: string): NativeAddonAsset => {\n const appRequire = createRequire(join(appRoot, \"package.json\"))\n const nativePackageJson = appRequire.resolve(\"@gtkx/native/package.json\")\n // .../node_modules/@gtkx/native/package.json -> .../node_modules/@gtkx\n const scopeDir = dirname(dirname(nativePackageJson))\n const candidates = readdirSync(scopeDir, { withFileTypes: true })\n .filter((entry) => entry.isDirectory() && entry.name.startsWith(\"native-\"))\n .flatMap((entry) => {\n const dir = join(scopeDir, entry.name)\n return readdirSync(dir)\n .filter((file) => file.endsWith(\".node\"))\n .map((file) => join(dir, file))\n })\n const [first, ...rest] = candidates\n if (!first || rest.length > 0) {\n throw new Error(\n `expected exactly one @gtkx/native-*/*.node under ${scopeDir}, found ` +\n `${candidates.length} (${candidates.join(\", \") || \"none\"}) — ` +\n \"reinstall targeting a single platform.\",\n )\n }\n return { path: first, key: NATIVE_ASSET_KEY }\n}\n\n/**\n * Builds the synthetic SEA entry source: every HOST_MODULE_EXTERNALS name\n * loaded through a dynamic `import()` (mirroring runner/host.ts's own\n * `load()`, which does the same against a real node_modules at runtime),\n * assembled into globalThis.__hostModules, then the jsbundle text executed\n * the same way host.ts does.\n *\n * Dynamic import, not static, throughout: a Node SEA's main script is\n * CommonJS only — confirmed empirically, Node 24 executes the embedded\n * main as CJS regardless of a \"type\": \"module\" field in sea-config.json\n * or an .mjs extension — and top-level await is a hard error under a \"cjs\"\n * output format REGARDLESS of whether the module is reached via a static\n * or dynamic import (the restriction is format-wide, not per-module —\n * verified: switching yoga-layout's static import to a dynamic one alone\n * did not clear the error). yoga-layout's own entry point has exactly this\n * top-level await (it loads its WASM binary asynchronously — see\n * node_modules/yoga-layout/dist/src/index.js: `const Yoga =\n * wrapAssembly(await loadYoga())` at module scope). The actual fix: import\n * \"yoga-layout/load\" instead, which exports the same `await`, just inside\n * an async FUNCTION (`export async function loadYoga()`), not at the\n * file's top level — bundleable, and awaited here like everything else.\n */\nexport const buildEntrySource = (jsbundlePath: string): string => {\n const jsbundleSource = readFileSync(jsbundlePath, \"utf8\")\n const loadLines = HOST_MODULE_EXTERNALS.map((name) => {\n if (name === \"yoga-layout\") {\n return [\n ` {`,\n ` const { loadYoga, ...rest } = await import(\"yoga-layout/load\");`,\n ` hostModules[\"yoga-layout\"] = __interopHostModule({ ...rest, default: await loadYoga() });`,\n ` }`,\n ].join(\"\\n\")\n }\n return ` hostModules[${JSON.stringify(name)}] = __interopHostModule(await import(${JSON.stringify(name)}));`\n })\n return [\n `async function __gtkxSeaMain() {`,\n // Mirrors runner/host.ts's `load()` interop: import() handles both ESM\n // and CJS, merge a `default` for Babel's default-import interop when\n // the module has none of its own.\n ` const __interopHostModule = (ns) => {`,\n ` const merged = Object.assign({ __esModule: true }, ns);`,\n ` if (!(\"default\" in ns)) merged.default = ns;`,\n ` return merged;`,\n ` };`,\n ` const hostModules = {};`,\n ...loadLines,\n ` globalThis.__hostModules = hostModules;`,\n // Node builtin proxies resolve lazily through the host's own require —\n // real here, no createRequire indirection needed (see host.ts for why\n // it needs one: this file's import.meta.url is never used for\n // anything relative, only require() of bare/builtin specifiers).\n ` globalThis.__hostRequire = require;`,\n ` require(\"node:vm\").runInThisContext(${JSON.stringify(jsbundleSource)}, { filename: ${JSON.stringify(jsbundlePath)} });`,\n `}`,\n `__gtkxSeaMain().catch((error) => { console.error(error); process.exitCode = 1; });`,\n ].join(\"\\n\")\n}\n\n/**\n * Produces `outFile`, a single CJS file with the jsbundle, its\n * HOST_MODULE_EXTERNALS and gtkx.config.ts's resolved values all inlined,\n * and the native addon import redirected to the SEA-asset extraction\n * shim. Returns the native addon that must be embedded as a SEA asset\n * under {@link NativeAddonAsset.key} for the result to run.\n */\nexport const bundleMetroSea = async (\n options: MetroSeaBundleOptions,\n): Promise<NativeAddonAsset> => {\n const { appRoot, jsbundlePath, outFile } = options\n const nativeAddonSource = options.nativeAddonSource ?? \"sea-asset\"\n const configModuleSource = await buildGtkxConfigModule(appRoot)\n const nativeAddon = resolveNativeAddon(appRoot)\n const entrySource = buildEntrySource(jsbundlePath)\n\n mkdirSync(dirname(outFile), { recursive: true })\n\n // The entry is given a path inside appRoot that does not exist on disk,\n // rather than a `\\0`-prefixed virtual id: every bare specifier it\n // imports (react, @gtkx/*, yoga-layout) must resolve from the app's own\n // node_modules, and a real directory is what makes that happen.\n const entryId = join(appRoot, \"__gtkx-sea-entry.js\")\n\n const bundle = await rolldown({\n input: entryId,\n cwd: appRoot,\n platform: \"node\",\n plugins: [\n {\n name: \"gtkx-sea-entry\",\n resolveId: (source) => (source === entryId ? entryId : null),\n load: (id) => (id === entryId ? entrySource : null),\n },\n reactAnchorPlugin(appRoot),\n nativeAddonShimPlugin({\n specifier: \"@gtkx/native\",\n appRoot,\n source: nativeAddonSource,\n assetKey: nativeAddon.key,\n addonBytes:\n nativeAddonSource === \"inline\"\n ? readFileSync(nativeAddon.path)\n : undefined,\n }),\n virtualConfigModulePlugin(configModuleSource, appRoot),\n ],\n })\n try {\n await bundle.write({\n file: outFile,\n format: \"cjs\",\n minify: true,\n // Every HOST_MODULE_EXTERNALS name is reached through a dynamic\n // import; without this each one becomes its own chunk and the\n // \"single file\" the whole build exists to produce is a directory.\n codeSplitting: false,\n })\n } finally {\n await bundle.close()\n }\n\n return nativeAddon\n}\n"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Plugin } from "rolldown";
|
|
2
|
+
export declare const CONFIG_REQUIRED: string;
|
|
3
|
+
/**
|
|
4
|
+
* Loads and validates the app's gtkx.config.ts, returning the source of a
|
|
5
|
+
* virtual:gtkx-config module with the resolved values baked in as literals.
|
|
6
|
+
*/
|
|
7
|
+
export declare const buildGtkxConfigModule: (appRoot: string) => Promise<string>;
|
|
8
|
+
/**
|
|
9
|
+
* Resolves the bare "virtual:gtkx-config" specifier @gtkx/react imports to
|
|
10
|
+
* the module source built by {@link buildGtkxConfigModule}, so the bundler
|
|
11
|
+
* can inline it like any other static import — no runtime loader hook
|
|
12
|
+
* needed.
|
|
13
|
+
*
|
|
14
|
+
* The module is given an id inside `appRoot` rather than a `\0`-prefixed
|
|
15
|
+
* virtual one because it re-exports "@gtkx/jsx/metadata": that bare
|
|
16
|
+
* specifier needs a real directory to resolve from.
|
|
17
|
+
*/
|
|
18
|
+
export declare const virtualConfigModulePlugin: (moduleSource: string, appRoot: string) => Plugin;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// @gtkx/react imports the bare specifier "virtual:gtkx-config" for JSX
|
|
2
|
+
// metadata plus the app's applicationId/userEventSignals/lazy elements.
|
|
3
|
+
// runner/host.ts resolves it at every process start through a loader hook
|
|
4
|
+
// (registerHooks), reading gtkx.config.ts from the current working
|
|
5
|
+
// directory. A SEA has no "app root" to read a config file from at
|
|
6
|
+
// runtime — there is no cwd convention once the app is a single file
|
|
7
|
+
// copied anywhere — so this bakes the same values in at BUILD time
|
|
8
|
+
// instead, the same way the vite path already does (its CLI plugin serves
|
|
9
|
+
// virtual:gtkx-config from the build-time config, never a runtime read).
|
|
10
|
+
//
|
|
11
|
+
// Deliberately duplicated validation logic rather than shared with
|
|
12
|
+
// host.ts: see host-dev.ts's header comment for why the hosts in this
|
|
13
|
+
// package are self-contained twins rather than sharing relative imports —
|
|
14
|
+
// this is a third twin, for the same reason (host.ts must keep resolving
|
|
15
|
+
// its config at runtime for ordinary `run-linux`/installed-app use; this
|
|
16
|
+
// one resolves it once, at bundle time, for the SEA artifact only).
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { createConfigLoader } from "@gtkx/config/internal";
|
|
19
|
+
export const CONFIG_REQUIRED = "gtkx.config.ts with an applicationId is required in the app root " +
|
|
20
|
+
'(export default defineConfig({ applicationId: "...", libraries: [...] })).';
|
|
21
|
+
/**
|
|
22
|
+
* Loads and validates the app's gtkx.config.ts, returning the source of a
|
|
23
|
+
* virtual:gtkx-config module with the resolved values baked in as literals.
|
|
24
|
+
*/
|
|
25
|
+
export const buildGtkxConfigModule = async (appRoot) => {
|
|
26
|
+
const loader = createConfigLoader();
|
|
27
|
+
const config = await loader.resolve(appRoot).catch((error) => {
|
|
28
|
+
throw new Error(`${CONFIG_REQUIRED}\n${String(error)}`);
|
|
29
|
+
});
|
|
30
|
+
if (!config.applicationId) {
|
|
31
|
+
throw new Error(CONFIG_REQUIRED);
|
|
32
|
+
}
|
|
33
|
+
// A behaviors module would have to be bundled and merged through
|
|
34
|
+
// @gtkx/react/config; the react-native surface owns element behaviors,
|
|
35
|
+
// so no app declares one — refuse rather than silently drop it (mirrors
|
|
36
|
+
// host.ts's runtime check).
|
|
37
|
+
if (config.elements !== null) {
|
|
38
|
+
throw new Error("gtkx.config.ts `elements.behaviors` is not supported on the linux " +
|
|
39
|
+
"platform — react-native-gtkx owns element behaviors.");
|
|
40
|
+
}
|
|
41
|
+
const lazyElements = Object.fromEntries(config.lazyElements.map((type) => [type, { lazy: true }]));
|
|
42
|
+
return [
|
|
43
|
+
`export * from "@gtkx/jsx/metadata";`,
|
|
44
|
+
`export const applicationId = ${JSON.stringify(config.applicationId)};`,
|
|
45
|
+
`export const userEventSignals = ${JSON.stringify(config.userEventSignals)};`,
|
|
46
|
+
`export const elements = ${JSON.stringify(lazyElements)};`,
|
|
47
|
+
].join("\n");
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Resolves the bare "virtual:gtkx-config" specifier @gtkx/react imports to
|
|
51
|
+
* the module source built by {@link buildGtkxConfigModule}, so the bundler
|
|
52
|
+
* can inline it like any other static import — no runtime loader hook
|
|
53
|
+
* needed.
|
|
54
|
+
*
|
|
55
|
+
* The module is given an id inside `appRoot` rather than a `\0`-prefixed
|
|
56
|
+
* virtual one because it re-exports "@gtkx/jsx/metadata": that bare
|
|
57
|
+
* specifier needs a real directory to resolve from.
|
|
58
|
+
*/
|
|
59
|
+
export const virtualConfigModulePlugin = (moduleSource, appRoot) => {
|
|
60
|
+
const configId = join(appRoot, "__gtkx-sea-config.js");
|
|
61
|
+
return {
|
|
62
|
+
name: "gtkx-virtual-config",
|
|
63
|
+
resolveId: (source) => (source === "virtual:gtkx-config" ? configId : null),
|
|
64
|
+
load: (id) => (id === configId ? moduleSource : null),
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
//# sourceMappingURL=gtkx-config-module.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gtkx-config-module.js","sourceRoot":"","sources":["../../src/sea/gtkx-config-module.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,wEAAwE;AACxE,0EAA0E;AAC1E,mEAAmE;AACnE,mEAAmE;AACnE,qEAAqE;AACrE,mEAAmE;AACnE,0EAA0E;AAC1E,yEAAyE;AACzE,EAAE;AACF,mEAAmE;AACnE,sEAAsE;AACtE,0EAA0E;AAC1E,yEAAyE;AACzE,yEAAyE;AACzE,oEAAoE;AACpE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAG1D,MAAM,CAAC,MAAM,eAAe,GAC1B,mEAAmE;IACnE,4EAA4E,CAAA;AAE9E;;;GAGG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,KAAK,EACxC,OAAe,EACE,EAAE;IACnB,MAAM,MAAM,GAAG,kBAAkB,EAAE,CAAA;IACnC,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;QACpE,MAAM,IAAI,KAAK,CAAC,GAAG,eAAe,KAAK,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;IACzD,CAAC,CAAC,CAAA;IACF,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,eAAe,CAAC,CAAA;IAClC,CAAC;IACD,iEAAiE;IACjE,uEAAuE;IACvE,wEAAwE;IACxE,4BAA4B;IAC5B,IAAI,MAAM,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CACb,oEAAoE;YAClE,sDAAsD,CACzD,CAAA;IACH,CAAC;IACD,MAAM,YAAY,GAAG,MAAM,CAAC,WAAW,CACrC,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAC1D,CAAA;IACD,OAAO;QACL,qCAAqC;QACrC,gCAAgC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,aAAa,CAAC,GAAG;QACvE,mCAAmC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,gBAAgB,CAAC,GAAG;QAC7E,2BAA2B,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,GAAG;KAC3D,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACd,CAAC,CAAA;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CACvC,YAAoB,EACpB,OAAe,EACP,EAAE;IACV,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,EAAE,sBAAsB,CAAC,CAAA;IACtD,OAAO;QACL,IAAI,EAAE,qBAAqB;QAC3B,SAAS,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,KAAK,qBAAqB,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC;QAC3E,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC;KACtD,CAAA;AACH,CAAC,CAAA","sourcesContent":["// @gtkx/react imports the bare specifier \"virtual:gtkx-config\" for JSX\n// metadata plus the app's applicationId/userEventSignals/lazy elements.\n// runner/host.ts resolves it at every process start through a loader hook\n// (registerHooks), reading gtkx.config.ts from the current working\n// directory. A SEA has no \"app root\" to read a config file from at\n// runtime — there is no cwd convention once the app is a single file\n// copied anywhere — so this bakes the same values in at BUILD time\n// instead, the same way the vite path already does (its CLI plugin serves\n// virtual:gtkx-config from the build-time config, never a runtime read).\n//\n// Deliberately duplicated validation logic rather than shared with\n// host.ts: see host-dev.ts's header comment for why the hosts in this\n// package are self-contained twins rather than sharing relative imports —\n// this is a third twin, for the same reason (host.ts must keep resolving\n// its config at runtime for ordinary `run-linux`/installed-app use; this\n// one resolves it once, at bundle time, for the SEA artifact only).\nimport { join } from \"node:path\"\nimport { createConfigLoader } from \"@gtkx/config/internal\"\nimport type { Plugin } from \"rolldown\"\n\nexport const CONFIG_REQUIRED =\n \"gtkx.config.ts with an applicationId is required in the app root \" +\n '(export default defineConfig({ applicationId: \"...\", libraries: [...] })).'\n\n/**\n * Loads and validates the app's gtkx.config.ts, returning the source of a\n * virtual:gtkx-config module with the resolved values baked in as literals.\n */\nexport const buildGtkxConfigModule = async (\n appRoot: string,\n): Promise<string> => {\n const loader = createConfigLoader()\n const config = await loader.resolve(appRoot).catch((error: unknown) => {\n throw new Error(`${CONFIG_REQUIRED}\\n${String(error)}`)\n })\n if (!config.applicationId) {\n throw new Error(CONFIG_REQUIRED)\n }\n // A behaviors module would have to be bundled and merged through\n // @gtkx/react/config; the react-native surface owns element behaviors,\n // so no app declares one — refuse rather than silently drop it (mirrors\n // host.ts's runtime check).\n if (config.elements !== null) {\n throw new Error(\n \"gtkx.config.ts `elements.behaviors` is not supported on the linux \" +\n \"platform — react-native-gtkx owns element behaviors.\",\n )\n }\n const lazyElements = Object.fromEntries(\n config.lazyElements.map((type) => [type, { lazy: true }]),\n )\n return [\n `export * from \"@gtkx/jsx/metadata\";`,\n `export const applicationId = ${JSON.stringify(config.applicationId)};`,\n `export const userEventSignals = ${JSON.stringify(config.userEventSignals)};`,\n `export const elements = ${JSON.stringify(lazyElements)};`,\n ].join(\"\\n\")\n}\n\n/**\n * Resolves the bare \"virtual:gtkx-config\" specifier @gtkx/react imports to\n * the module source built by {@link buildGtkxConfigModule}, so the bundler\n * can inline it like any other static import — no runtime loader hook\n * needed.\n *\n * The module is given an id inside `appRoot` rather than a `\\0`-prefixed\n * virtual one because it re-exports \"@gtkx/jsx/metadata\": that bare\n * specifier needs a real directory to resolve from.\n */\nexport const virtualConfigModulePlugin = (\n moduleSource: string,\n appRoot: string,\n): Plugin => {\n const configId = join(appRoot, \"__gtkx-sea-config.js\")\n return {\n name: \"gtkx-virtual-config\",\n resolveId: (source) => (source === \"virtual:gtkx-config\" ? configId : null),\n load: (id) => (id === configId ? moduleSource : null),\n }\n}\n"]}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Plugin } from "rolldown";
|
|
2
|
+
export declare const NATIVE_ASSET_KEY = "gtkx-native.node";
|
|
3
|
+
/**
|
|
4
|
+
* Where the shim gets the addon's bytes from — the only difference between
|
|
5
|
+
* the two single-file artifacts:
|
|
6
|
+
*
|
|
7
|
+
* - `"sea-asset"`: from the SEA blob, via node:sea. Only works inside a
|
|
8
|
+
* real single executable.
|
|
9
|
+
* - `"inline"`: from a base64 literal in the bundle itself, so the CJS
|
|
10
|
+
* file is self-contained under a plain `node app.cjs` — the same
|
|
11
|
+
* artifact minus the embedded Node runtime. Costs ~4/3 of the addon's
|
|
12
|
+
* size in source text (the addon is ~1.6 MB) and is parsed once at
|
|
13
|
+
* startup; the extraction and dlopen below are shared.
|
|
14
|
+
*/
|
|
15
|
+
export type NativeAddonSource = "sea-asset" | "inline";
|
|
16
|
+
export type NativeAddonShimOptions = {
|
|
17
|
+
/** The bare specifier to replace, in practice "@gtkx/native". */
|
|
18
|
+
specifier: string;
|
|
19
|
+
/** The app root — the shim's id lives here, see above. */
|
|
20
|
+
appRoot: string;
|
|
21
|
+
/** Where the addon's bytes come from at runtime. */
|
|
22
|
+
source: NativeAddonSource;
|
|
23
|
+
/** SEA asset key; unused when `source` is "inline". */
|
|
24
|
+
assetKey?: string;
|
|
25
|
+
/** The addon's bytes; required when `source` is "inline". */
|
|
26
|
+
addonBytes?: Buffer;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Intercepts every import/require of `specifier` (an exact bare module
|
|
30
|
+
* specifier, e.g. "@gtkx/native") and replaces it with the extraction shim
|
|
31
|
+
* above. The real module never reaches the bundle graph — only its
|
|
32
|
+
* re-exported surface, loaded from the extracted file at runtime.
|
|
33
|
+
*
|
|
34
|
+
* The shim is given an id inside `appRoot` with a `.cjs` extension rather
|
|
35
|
+
* than a `\0`-prefixed virtual one: the shim IS CommonJS (it assigns
|
|
36
|
+
* `module.exports` so consumers' NAMED imports of the addon still resolve
|
|
37
|
+
* through the bundler's interop), and a real directory is what lets any
|
|
38
|
+
* specifier inside it resolve normally.
|
|
39
|
+
*/
|
|
40
|
+
export declare const nativeAddonShimPlugin: (options: NativeAddonShimOptions) => Plugin;
|