Skip to content

Commit a3a2399

Browse files
Alb-Orekram1-nodeactions-user
authored
nix: bundle js dist with bun and patch tree-sitter wasm paths (#4644)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Github Action <action@github.com>
1 parent b4fd4bb commit a3a2399

File tree

5 files changed

+168
-49
lines changed

5 files changed

+168
-49
lines changed

nix/bundle.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/usr/bin/env bun
2+
3+
import solidPlugin from "./node_modules/@opentui/solid/scripts/solid-plugin"
4+
import path from "path"
5+
import fs from "fs"
6+
7+
const dir = process.cwd()
8+
const parser = fs.realpathSync(path.join(dir, "node_modules/@opentui/core/parser.worker.js"))
9+
const worker = "./src/cli/cmd/tui/worker.ts"
10+
const version = process.env.OPENCODE_VERSION ?? "local"
11+
const channel = process.env.OPENCODE_CHANNEL ?? "local"
12+
13+
fs.rmSync(path.join(dir, "dist"), { recursive: true, force: true })
14+
15+
const result = await Bun.build({
16+
entrypoints: ["./src/index.ts", worker, parser],
17+
outdir: "./dist",
18+
target: "bun",
19+
sourcemap: "none",
20+
tsconfig: "./tsconfig.json",
21+
plugins: [solidPlugin],
22+
external: ["@opentui/core"],
23+
define: {
24+
OPENCODE_VERSION: `'${version}'`,
25+
OPENCODE_CHANNEL: `'${channel}'`,
26+
// Leave undefined so runtime picks bundled/dist worker or fallback in code.
27+
OPENCODE_WORKER_PATH: "undefined",
28+
OTUI_TREE_SITTER_WORKER_PATH: 'new URL("./cli/cmd/tui/parser.worker.js", import.meta.url).href',
29+
},
30+
})
31+
32+
if (!result.success) {
33+
console.error("bundle failed")
34+
for (const log of result.logs) console.error(log)
35+
process.exit(1)
36+
}
37+
38+
const parserOut = path.join(dir, "dist/src/cli/cmd/tui/parser.worker.js")
39+
fs.mkdirSync(path.dirname(parserOut), { recursive: true })
40+
await Bun.write(parserOut, Bun.file(parser))

nix/opencode.nix

Lines changed: 59 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{ lib, stdenv, stdenvNoCC, bun, fzf, ripgrep, makeBinaryWrapper }:
1+
{ lib, stdenvNoCC, bun, fzf, ripgrep, makeBinaryWrapper }:
22
args:
33
let
44
scripts = args.scripts;
@@ -28,64 +28,89 @@ stdenvNoCC.mkDerivation (finalAttrs: {
2828
makeBinaryWrapper
2929
];
3030

31-
configurePhase = ''
32-
runHook preConfigure
33-
cp -R ${finalAttrs.node_modules}/. .
34-
runHook postConfigure
35-
'';
36-
3731
env.MODELS_DEV_API_JSON = args.modelsDev;
3832
env.OPENCODE_VERSION = args.version;
3933
env.OPENCODE_CHANNEL = "stable";
34+
dontConfigure = true;
4035

4136
buildPhase = ''
4237
runHook preBuild
4338
44-
cp ${scripts + "/bun-build.ts"} bun-build.ts
39+
cp -r ${finalAttrs.node_modules}/node_modules .
40+
cp -r ${finalAttrs.node_modules}/packages .
4541
46-
substituteInPlace bun-build.ts \
47-
--replace '@VERSION@' "${finalAttrs.version}"
42+
(
43+
cd packages/opencode
4844
49-
export BUN_COMPILE_TARGET=${args.target}
50-
bun --bun bun-build.ts
45+
chmod -R u+w ./node_modules
46+
mkdir -p ./node_modules/@opencode-ai
47+
rm -f ./node_modules/@opencode-ai/{script,sdk,plugin}
48+
ln -s $(pwd)/../../packages/script ./node_modules/@opencode-ai/script
49+
ln -s $(pwd)/../../packages/sdk/js ./node_modules/@opencode-ai/sdk
50+
ln -s $(pwd)/../../packages/plugin ./node_modules/@opencode-ai/plugin
51+
52+
cp ${./bundle.ts} ./bundle.ts
53+
chmod +x ./bundle.ts
54+
bun run ./bundle.ts
55+
)
5156
5257
runHook postBuild
5358
'';
5459

55-
dontStrip = true;
56-
5760
installPhase = ''
5861
runHook preInstall
5962
6063
cd packages/opencode
61-
if [ ! -f opencode ]; then
62-
echo "ERROR: opencode binary not found in $(pwd)"
63-
ls -la
64+
if [ ! -d dist ]; then
65+
echo "ERROR: dist directory missing after bundle step"
6466
exit 1
6567
fi
66-
if [ ! -f opencode-worker.js ]; then
67-
echo "ERROR: opencode worker bundle not found in $(pwd)"
68-
ls -la
68+
69+
mkdir -p $out/lib/opencode
70+
cp -r dist $out/lib/opencode/
71+
chmod -R u+w $out/lib/opencode/dist
72+
73+
# Select bundled worker assets deterministically (sorted find output)
74+
worker_file=$(find "$out/lib/opencode/dist" -type f \( -path '*/tui/worker.*' -o -name 'worker.*' \) | sort | head -n1)
75+
parser_worker_file=$(find "$out/lib/opencode/dist" -type f -name 'parser.worker.*' | sort | head -n1)
76+
if [ -z "$worker_file" ]; then
77+
echo "ERROR: bundled worker not found"
6978
exit 1
7079
fi
7180
72-
install -Dm755 opencode $out/bin/opencode
73-
install -Dm644 opencode-worker.js $out/bin/opencode-worker.js
74-
if [ -f opencode-assets.manifest ]; then
75-
while IFS= read -r asset; do
76-
[ -z "$asset" ] && continue
77-
if [ ! -f "$asset" ]; then
78-
echo "ERROR: referenced asset \"$asset\" missing"
79-
exit 1
80-
fi
81-
install -Dm644 "$asset" "$out/bin/$(basename "$asset")"
82-
done < opencode-assets.manifest
83-
fi
81+
main_wasm=$(printf '%s\n' "$out"/lib/opencode/dist/tree-sitter-*.wasm | sort | head -n1)
82+
wasm_list=$(find "$out/lib/opencode/dist" -maxdepth 1 -name 'tree-sitter-*.wasm' -print)
83+
for patch_file in "$worker_file" "$parser_worker_file"; do
84+
[ -z "$patch_file" ] && continue
85+
[ ! -f "$patch_file" ] && continue
86+
if [ -n "$wasm_list" ] && grep -q 'tree-sitter' "$patch_file"; then
87+
# Rewrite wasm references to absolute store paths to avoid runtime resolve failures.
88+
bun --bun ${scripts + "/patch-wasm.ts"} "$patch_file" "$main_wasm" $wasm_list
89+
fi
90+
done
91+
92+
mkdir -p $out/lib/opencode/node_modules
93+
cp -r ../../node_modules/.bun $out/lib/opencode/node_modules/
94+
mkdir -p $out/lib/opencode/node_modules/@opentui
95+
96+
mkdir -p $out/bin
97+
makeWrapper ${bun}/bin/bun $out/bin/opencode \
98+
--add-flags "run" \
99+
--add-flags "$out/lib/opencode/dist/src/index.js" \
100+
--prefix PATH : ${lib.makeBinPath [ fzf ripgrep ]} \
101+
--argv0 opencode
102+
84103
runHook postInstall
85104
'';
86105

87-
postFixup = ''
88-
wrapProgram "$out/bin/opencode" --prefix PATH : ${lib.makeBinPath [ fzf ripgrep ]}
106+
postInstall = ''
107+
for pkg in $out/lib/opencode/node_modules/.bun/@opentui+core-* $out/lib/opencode/node_modules/.bun/@opentui+solid-* $out/lib/opencode/node_modules/.bun/@opentui+core@* $out/lib/opencode/node_modules/.bun/@opentui+solid@*; do
108+
if [ -d "$pkg" ]; then
109+
pkgName=$(basename "$pkg" | sed 's/@opentui+\([^@]*\)@.*/\1/')
110+
ln -sf ../.bun/$(basename "$pkg")/node_modules/@opentui/$pkgName \
111+
$out/lib/opencode/node_modules/@opentui/$pkgName
112+
fi
113+
done
89114
'';
90115

91116
meta = {

nix/scripts/canonicalize-node-modules.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,13 @@ for (const entry of directories) {
2424
if (!info.isDirectory()) {
2525
continue
2626
}
27-
const marker = entry.lastIndexOf("@")
28-
if (marker <= 0) {
27+
const parsed = parseEntry(entry)
28+
if (!parsed) {
2929
continue
3030
}
31-
const slug = entry.slice(0, marker).replace(/\+/g, "/")
32-
const version = entry.slice(marker + 1)
33-
const list = versions.get(slug) ?? []
34-
list.push({ dir: full, version, label: entry })
35-
versions.set(slug, list)
31+
const list = versions.get(parsed.name) ?? []
32+
list.push({ dir: full, version: parsed.version, label: entry })
33+
versions.set(parsed.name, list)
3634
}
3735

3836
const semverModule = (await import(join(bunRoot, "node_modules/semver"))) as
@@ -79,6 +77,12 @@ for (const [slug, entry] of Array.from(selections.entries()).sort((a, b) => a[0]
7977
await mkdir(parent, { recursive: true })
8078
const linkPath = join(parent, leaf)
8179
const desired = join(entry.dir, "node_modules", slug)
80+
const exists = await lstat(desired)
81+
.then(info => info.isDirectory())
82+
.catch(() => false)
83+
if (!exists) {
84+
continue
85+
}
8286
const relativeTarget = relative(parent, desired)
8387
const resolved = relativeTarget.length === 0 ? "." : relativeTarget
8488
await rm(linkPath, { recursive: true, force: true })
@@ -94,3 +98,16 @@ for (const line of rewrites.slice(0, 20)) {
9498
if (rewrites.length > 20) {
9599
console.log(" ...")
96100
}
101+
102+
function parseEntry(label: string) {
103+
const marker = label.startsWith("@") ? label.indexOf("@", 1) : label.indexOf("@")
104+
if (marker <= 0) {
105+
return null
106+
}
107+
const name = label.slice(0, marker).replace(/\+/g, "/")
108+
const version = label.slice(marker + 1)
109+
if (!name || !version) {
110+
return null
111+
}
112+
return { name, version }
113+
}

nix/scripts/patch-wasm.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/usr/bin/env bun
2+
3+
import fs from "fs"
4+
import path from "path"
5+
6+
/**
7+
* Rewrite tree-sitter wasm references inside a JS file to absolute paths.
8+
* argv: [node, script, file, mainWasm, ...wasmPaths]
9+
*/
10+
const [, , file, mainWasm, ...wasmPaths] = process.argv
11+
12+
if (!file || !mainWasm) {
13+
console.error("usage: patch-wasm <file> <mainWasm> [wasmPaths...]")
14+
process.exit(1)
15+
}
16+
17+
const content = fs.readFileSync(file, "utf8")
18+
const byName = new Map<string, string>()
19+
20+
for (const wasm of wasmPaths) {
21+
const name = path.basename(wasm)
22+
byName.set(name, wasm)
23+
}
24+
25+
let next = content
26+
27+
for (const [name, wasmPath] of byName) {
28+
next = next.replaceAll(name, wasmPath)
29+
}
30+
31+
next = next.replaceAll("tree-sitter.wasm", mainWasm).replaceAll("web-tree-sitter/tree-sitter.wasm", mainWasm)
32+
33+
// Collapse any relative prefixes before absolute store paths (e.g., "../../../..//nix/store/...")
34+
next = next.replace(/(\.\/)+/g, "./")
35+
next = next.replace(/(\.\.\/)+\/?(\/nix\/store[^"']+)/g, "/$2")
36+
next = next.replace(/(["'])\/{2,}(\/nix\/store[^"']+)(["'])/g, "$1/$2$3")
37+
next = next.replace(/(["'])\/\/(nix\/store[^"']+)(["'])/g, "$1/$2$3")
38+
39+
if (next !== content) fs.writeFileSync(file, next)

packages/opencode/src/cli/cmd/tui/thread.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,16 +58,14 @@ export const TuiThreadCommand = cmd({
5858
// Resolve relative paths against PWD to preserve behavior when using --cwd flag
5959
const baseCwd = process.env.PWD ?? process.cwd()
6060
const cwd = args.project ? path.resolve(baseCwd, args.project) : process.cwd()
61-
const defaultWorker = new URL("./worker.ts", import.meta.url)
62-
// Nix build creates a bundled worker next to the binary; prefer it when present.
61+
const localWorker = new URL("./worker.ts", import.meta.url)
62+
const distWorker = new URL("./cli/cmd/tui/worker.js", import.meta.url)
6363
const execDir = path.dirname(process.execPath)
64-
const bundledWorker = path.join(execDir, "opencode-worker.js")
65-
const hasBundledWorker = await Bun.file(bundledWorker).exists()
66-
const workerPath = (() => {
64+
const workerPath = await iife(async () => {
6765
if (typeof OPENCODE_WORKER_PATH !== "undefined") return OPENCODE_WORKER_PATH
68-
if (hasBundledWorker) return bundledWorker
69-
return defaultWorker
70-
})()
66+
if (await Bun.file(distWorker).exists()) return distWorker
67+
return localWorker
68+
})
7169
try {
7270
process.chdir(cwd)
7371
} catch (e) {

0 commit comments

Comments
 (0)