Skip to content

Commit 7232e8a

Browse files
committed
examples: add Node.js 26 runtime support
Allow examples to run under Node 26 with --experimental-ffi in addition to Bun. Replace some Bun-specific APIs with standard Node equivalents, lazy-load demos that still require Bun (WebGPU, sprites, shaders), and import Rapier via its ESM subpath.
1 parent de9ea24 commit 7232e8a

13 files changed

Lines changed: 282 additions & 109 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
3030
# caches
3131
.eslintcache
3232
.cache
33+
.node
3334
*.tsbuildinfo
3435

3536
# IntelliJ based IDEs

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"build": "cd packages/core && bun run build && cd ../three && bun run build && cd ../solid && bun run build && cd ../react && bun run build && cd ../keymap && bun run build",
1010
"build:examples": "cd packages/examples && bun run build",
1111
"clean": "bun scripts/clean.ts",
12+
"examples:node": "node packages/examples/scripts/run-node26.mjs",
1213
"fmt": "oxfmt --write .",
1314
"fmt:check": "oxfmt --check .",
1415
"lint": "oxlint .",

packages/examples/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
"scripts": {
1616
"dev": "bun src/index.ts",
1717
"build": "bun scripts/build.ts",
18-
"build:host": "bun scripts/build.ts --host"
18+
"build:host": "bun scripts/build.ts --host",
19+
"download:node26": "node scripts/download-node26.mjs",
20+
"dev:node": "node scripts/run-node26.mjs"
1921
},
2022
"dependencies": {
2123
"@dimforge/rapier2d-simd-compat": "^0.17.3",
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { ensureNode26 } from "./node26.mjs"
2+
3+
console.log(ensureNode26())
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { spawnSync } from "node:child_process"
2+
import { existsSync, mkdirSync } from "node:fs"
3+
import { dirname, resolve } from "node:path"
4+
import { fileURLToPath } from "node:url"
5+
6+
export const NODE26_VERSION = "v26.1.0"
7+
8+
const scriptDir = dirname(fileURLToPath(import.meta.url))
9+
const packageRoot = resolve(scriptDir, "..")
10+
const cacheDir = resolve(packageRoot, ".cache/node26")
11+
12+
function getTarget() {
13+
if (process.arch !== "x64") {
14+
throw new Error(`This examples runner downloads the x64 Node 26 builds, not ${process.platform}-${process.arch}.`)
15+
}
16+
17+
switch (process.platform) {
18+
case "linux":
19+
return {
20+
archiveName: `node-${NODE26_VERSION}-linux-x64.tar.xz`,
21+
directoryName: `node-${NODE26_VERSION}-linux-x64`,
22+
nodeRelativePath: "bin/node",
23+
url: `https://nodejs.org/dist/${NODE26_VERSION}/node-${NODE26_VERSION}-linux-x64.tar.xz`,
24+
}
25+
case "darwin":
26+
return {
27+
archiveName: `node-${NODE26_VERSION}-darwin-x64.tar.gz`,
28+
directoryName: `node-${NODE26_VERSION}-darwin-x64`,
29+
nodeRelativePath: "bin/node",
30+
url: `https://nodejs.org/dist/${NODE26_VERSION}/node-${NODE26_VERSION}-darwin-x64.tar.gz`,
31+
}
32+
case "win32":
33+
return {
34+
archiveName: `node-${NODE26_VERSION}-win-x64.zip`,
35+
directoryName: `node-${NODE26_VERSION}-win-x64`,
36+
nodeRelativePath: "node.exe",
37+
url: `https://nodejs.org/dist/${NODE26_VERSION}/node-${NODE26_VERSION}-win-x64.zip`,
38+
}
39+
default:
40+
throw new Error(`This examples runner does not have a Node 26 download for ${process.platform}-${process.arch}.`)
41+
}
42+
}
43+
44+
export function getNode26Path() {
45+
const target = getTarget()
46+
return resolve(cacheDir, target.directoryName, target.nodeRelativePath)
47+
}
48+
49+
export function ensureNode26() {
50+
const target = getTarget()
51+
const archivePath = resolve(cacheDir, target.archiveName)
52+
const nodeDir = resolve(cacheDir, target.directoryName)
53+
const nodePath = resolve(nodeDir, target.nodeRelativePath)
54+
55+
if (existsSync(nodePath)) {
56+
return nodePath
57+
}
58+
59+
mkdirSync(cacheDir, { recursive: true })
60+
61+
if (!existsSync(archivePath)) {
62+
run("curl", ["-L", target.url, "-o", archivePath])
63+
}
64+
65+
extractArchive(archivePath, cacheDir)
66+
67+
if (!existsSync(nodePath)) {
68+
throw new Error(`Downloaded Node 26 archive did not produce ${nodePath}`)
69+
}
70+
71+
return nodePath
72+
}
73+
74+
function extractArchive(archivePath, outputDir) {
75+
if (archivePath.endsWith(".zip")) {
76+
if (process.platform === "win32") {
77+
run("powershell", [
78+
"-NoProfile",
79+
"-Command",
80+
`Expand-Archive -Path '${archivePath}' -DestinationPath '${outputDir}' -Force`,
81+
])
82+
return
83+
}
84+
85+
run("unzip", ["-q", archivePath, "-d", outputDir])
86+
return
87+
}
88+
89+
run("tar", ["-xf", archivePath, "-C", outputDir])
90+
}
91+
92+
function run(command, args) {
93+
const result = spawnSync(command, args, { stdio: "inherit" })
94+
95+
if (result.error) {
96+
throw result.error
97+
}
98+
99+
if (result.status !== 0) {
100+
process.exit(result.status ?? 1)
101+
}
102+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { spawnSync } from "node:child_process"
2+
import { cpSync, mkdirSync, rmSync } from "node:fs"
3+
import { dirname, resolve } from "node:path"
4+
import { fileURLToPath } from "node:url"
5+
6+
import { ensureNode26 } from "./node26.mjs"
7+
8+
const scriptDir = dirname(fileURLToPath(import.meta.url))
9+
const packageRoot = resolve(scriptDir, "..")
10+
const repoRoot = resolve(packageRoot, "../..")
11+
const coreRoot = resolve(repoRoot, "packages/core")
12+
const nodePath = ensureNode26()
13+
const bundleDir = resolve(packageRoot, ".node")
14+
const bundleEntry = resolve(bundleDir, "index.js")
15+
16+
ensureNativePackage()
17+
buildNodeExamples()
18+
19+
const result = spawnSync(nodePath, ["--experimental-ffi", "--no-warnings", bundleEntry], {
20+
cwd: packageRoot,
21+
stdio: "inherit",
22+
env: process.env,
23+
})
24+
25+
if (result.error) {
26+
throw result.error
27+
}
28+
29+
process.exit(result.status ?? 0)
30+
31+
function buildNodeExamples() {
32+
rmSync(bundleDir, { recursive: true, force: true })
33+
34+
run(
35+
"bun",
36+
[
37+
"build",
38+
"src/index.ts",
39+
"--target=node",
40+
"--format=esm",
41+
"--splitting",
42+
"--outdir",
43+
".node",
44+
"--define",
45+
"OPENTUI_BUN_ONLY_EXAMPLES=false",
46+
],
47+
packageRoot,
48+
)
49+
}
50+
51+
function ensureNativePackage() {
52+
const nativePackageName = `core-${process.platform === "win32" ? "win32" : process.platform}-${process.arch}`
53+
const sourceNativeDir = resolve(coreRoot, "node_modules", "@opentui", nativePackageName)
54+
const targetNativeDir = resolve(packageRoot, "node_modules", "@opentui", nativePackageName)
55+
56+
run("bun", ["run", "build:native"], coreRoot)
57+
58+
mkdirSync(resolve(packageRoot, "node_modules", "@opentui"), { recursive: true })
59+
rmSync(targetNativeDir, { recursive: true, force: true })
60+
cpSync(sourceNativeDir, targetNativeDir, { recursive: true, dereference: true })
61+
}
62+
63+
function run(command, args, cwd) {
64+
const result = spawnSync(command, args, {
65+
cwd,
66+
stdio: "inherit",
67+
})
68+
69+
if (result.error) {
70+
throw result.error
71+
}
72+
73+
if (result.status !== 0) {
74+
process.exit(result.status ?? 1)
75+
}
76+
}

packages/examples/src/hast-syntax-highlighting-demo.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import { setupCommonDemoKeys } from "./lib/standalone-keys.js"
33
import { parseColor } from "@opentui/core"
44
import { hastToStyledText, type HASTElement } from "@opentui/core"
55
import { SyntaxStyle } from "@opentui/core"
6+
import exampleHAST from "./assets/hast-example.json" with { type: "json" }
67

7-
const exampleHAST: HASTElement = (await import("./assets/hast-example.json", { with: { type: "json" } })) as HASTElement
8+
const typedExampleHAST = exampleHAST as HASTElement
89

910
let renderer: CliRenderer | null = null
1011
let keyboardHandler: ((key: KeyEvent) => void) | null = null
@@ -67,7 +68,7 @@ export function run(rendererInstance: CliRenderer): void {
6768
default: { fg: parseColor("#FFFFFF") },
6869
})
6970
const transformStart = performance.now()
70-
const styledText = hastToStyledText(exampleHAST, syntaxStyle)
71+
const styledText = hastToStyledText(typedExampleHAST, syntaxStyle)
7172
const transformEnd = performance.now()
7273
const transformTime = (transformEnd - transformStart).toFixed(2)
7374

@@ -93,7 +94,7 @@ export function run(rendererInstance: CliRenderer): void {
9394
syntaxStyle.clearCache()
9495

9596
const retransformStart = performance.now()
96-
const newStyledText = hastToStyledText(exampleHAST, syntaxStyle)
97+
const newStyledText = hastToStyledText(typedExampleHAST, syntaxStyle)
9798
const retransformEnd = performance.now()
9899
const newTransformTime = (retransformEnd - retransformStart).toFixed(2)
99100

0 commit comments

Comments
 (0)