summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAlbert O'Shea <[email protected]>2025-11-24 15:54:29 +1100
committerGitHub <[email protected]>2025-11-23 22:54:29 -0600
commita3a239967f170423e91382bfdaf1058755befbb8 (patch)
tree322961f56a32fe8d164fdebc43c3a326c3fa421e
parentb4fd4bb25757250b1bcd4da9e6398011ed07e2f3 (diff)
downloadopencode-a3a239967f170423e91382bfdaf1058755befbb8.tar.gz
opencode-a3a239967f170423e91382bfdaf1058755befbb8.zip
nix: bundle js dist with bun and patch tree-sitter wasm paths (#4644)
Co-authored-by: Aiden Cline <[email protected]> Co-authored-by: Github Action <[email protected]>
-rw-r--r--nix/bundle.ts40
-rw-r--r--nix/opencode.nix93
-rw-r--r--nix/scripts/canonicalize-node-modules.ts31
-rw-r--r--nix/scripts/patch-wasm.ts39
-rw-r--r--packages/opencode/src/cli/cmd/tui/thread.ts14
5 files changed, 168 insertions, 49 deletions
diff --git a/nix/bundle.ts b/nix/bundle.ts
new file mode 100644
index 000000000..effb1dff7
--- /dev/null
+++ b/nix/bundle.ts
@@ -0,0 +1,40 @@
+#!/usr/bin/env bun
+
+import solidPlugin from "./node_modules/@opentui/solid/scripts/solid-plugin"
+import path from "path"
+import fs from "fs"
+
+const dir = process.cwd()
+const parser = fs.realpathSync(path.join(dir, "node_modules/@opentui/core/parser.worker.js"))
+const worker = "./src/cli/cmd/tui/worker.ts"
+const version = process.env.OPENCODE_VERSION ?? "local"
+const channel = process.env.OPENCODE_CHANNEL ?? "local"
+
+fs.rmSync(path.join(dir, "dist"), { recursive: true, force: true })
+
+const result = await Bun.build({
+ entrypoints: ["./src/index.ts", worker, parser],
+ outdir: "./dist",
+ target: "bun",
+ sourcemap: "none",
+ tsconfig: "./tsconfig.json",
+ plugins: [solidPlugin],
+ external: ["@opentui/core"],
+ define: {
+ OPENCODE_VERSION: `'${version}'`,
+ OPENCODE_CHANNEL: `'${channel}'`,
+ // Leave undefined so runtime picks bundled/dist worker or fallback in code.
+ OPENCODE_WORKER_PATH: "undefined",
+ OTUI_TREE_SITTER_WORKER_PATH: 'new URL("./cli/cmd/tui/parser.worker.js", import.meta.url).href',
+ },
+})
+
+if (!result.success) {
+ console.error("bundle failed")
+ for (const log of result.logs) console.error(log)
+ process.exit(1)
+}
+
+const parserOut = path.join(dir, "dist/src/cli/cmd/tui/parser.worker.js")
+fs.mkdirSync(path.dirname(parserOut), { recursive: true })
+await Bun.write(parserOut, Bun.file(parser))
diff --git a/nix/opencode.nix b/nix/opencode.nix
index bec299760..ff536cf8f 100644
--- a/nix/opencode.nix
+++ b/nix/opencode.nix
@@ -1,4 +1,4 @@
-{ lib, stdenv, stdenvNoCC, bun, fzf, ripgrep, makeBinaryWrapper }:
+{ lib, stdenvNoCC, bun, fzf, ripgrep, makeBinaryWrapper }:
args:
let
scripts = args.scripts;
@@ -28,64 +28,89 @@ stdenvNoCC.mkDerivation (finalAttrs: {
makeBinaryWrapper
];
- configurePhase = ''
- runHook preConfigure
- cp -R ${finalAttrs.node_modules}/. .
- runHook postConfigure
- '';
-
env.MODELS_DEV_API_JSON = args.modelsDev;
env.OPENCODE_VERSION = args.version;
env.OPENCODE_CHANNEL = "stable";
+ dontConfigure = true;
buildPhase = ''
runHook preBuild
- cp ${scripts + "/bun-build.ts"} bun-build.ts
+ cp -r ${finalAttrs.node_modules}/node_modules .
+ cp -r ${finalAttrs.node_modules}/packages .
- substituteInPlace bun-build.ts \
- --replace '@VERSION@' "${finalAttrs.version}"
+ (
+ cd packages/opencode
- export BUN_COMPILE_TARGET=${args.target}
- bun --bun bun-build.ts
+ chmod -R u+w ./node_modules
+ mkdir -p ./node_modules/@opencode-ai
+ rm -f ./node_modules/@opencode-ai/{script,sdk,plugin}
+ ln -s $(pwd)/../../packages/script ./node_modules/@opencode-ai/script
+ ln -s $(pwd)/../../packages/sdk/js ./node_modules/@opencode-ai/sdk
+ ln -s $(pwd)/../../packages/plugin ./node_modules/@opencode-ai/plugin
+
+ cp ${./bundle.ts} ./bundle.ts
+ chmod +x ./bundle.ts
+ bun run ./bundle.ts
+ )
runHook postBuild
'';
- dontStrip = true;
-
installPhase = ''
runHook preInstall
cd packages/opencode
- if [ ! -f opencode ]; then
- echo "ERROR: opencode binary not found in $(pwd)"
- ls -la
+ if [ ! -d dist ]; then
+ echo "ERROR: dist directory missing after bundle step"
exit 1
fi
- if [ ! -f opencode-worker.js ]; then
- echo "ERROR: opencode worker bundle not found in $(pwd)"
- ls -la
+
+ mkdir -p $out/lib/opencode
+ cp -r dist $out/lib/opencode/
+ chmod -R u+w $out/lib/opencode/dist
+
+ # Select bundled worker assets deterministically (sorted find output)
+ worker_file=$(find "$out/lib/opencode/dist" -type f \( -path '*/tui/worker.*' -o -name 'worker.*' \) | sort | head -n1)
+ parser_worker_file=$(find "$out/lib/opencode/dist" -type f -name 'parser.worker.*' | sort | head -n1)
+ if [ -z "$worker_file" ]; then
+ echo "ERROR: bundled worker not found"
exit 1
fi
- install -Dm755 opencode $out/bin/opencode
- install -Dm644 opencode-worker.js $out/bin/opencode-worker.js
- if [ -f opencode-assets.manifest ]; then
- while IFS= read -r asset; do
- [ -z "$asset" ] && continue
- if [ ! -f "$asset" ]; then
- echo "ERROR: referenced asset \"$asset\" missing"
- exit 1
- fi
- install -Dm644 "$asset" "$out/bin/$(basename "$asset")"
- done < opencode-assets.manifest
- fi
+ main_wasm=$(printf '%s\n' "$out"/lib/opencode/dist/tree-sitter-*.wasm | sort | head -n1)
+ wasm_list=$(find "$out/lib/opencode/dist" -maxdepth 1 -name 'tree-sitter-*.wasm' -print)
+ for patch_file in "$worker_file" "$parser_worker_file"; do
+ [ -z "$patch_file" ] && continue
+ [ ! -f "$patch_file" ] && continue
+ if [ -n "$wasm_list" ] && grep -q 'tree-sitter' "$patch_file"; then
+ # Rewrite wasm references to absolute store paths to avoid runtime resolve failures.
+ bun --bun ${scripts + "/patch-wasm.ts"} "$patch_file" "$main_wasm" $wasm_list
+ fi
+ done
+
+ mkdir -p $out/lib/opencode/node_modules
+ cp -r ../../node_modules/.bun $out/lib/opencode/node_modules/
+ mkdir -p $out/lib/opencode/node_modules/@opentui
+
+ mkdir -p $out/bin
+ makeWrapper ${bun}/bin/bun $out/bin/opencode \
+ --add-flags "run" \
+ --add-flags "$out/lib/opencode/dist/src/index.js" \
+ --prefix PATH : ${lib.makeBinPath [ fzf ripgrep ]} \
+ --argv0 opencode
+
runHook postInstall
'';
- postFixup = ''
- wrapProgram "$out/bin/opencode" --prefix PATH : ${lib.makeBinPath [ fzf ripgrep ]}
+ postInstall = ''
+ 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
+ if [ -d "$pkg" ]; then
+ pkgName=$(basename "$pkg" | sed 's/@opentui+\([^@]*\)@.*/\1/')
+ ln -sf ../.bun/$(basename "$pkg")/node_modules/@opentui/$pkgName \
+ $out/lib/opencode/node_modules/@opentui/$pkgName
+ fi
+ done
'';
meta = {
diff --git a/nix/scripts/canonicalize-node-modules.ts b/nix/scripts/canonicalize-node-modules.ts
index bb004f3c5..828a18fbc 100644
--- a/nix/scripts/canonicalize-node-modules.ts
+++ b/nix/scripts/canonicalize-node-modules.ts
@@ -24,15 +24,13 @@ for (const entry of directories) {
if (!info.isDirectory()) {
continue
}
- const marker = entry.lastIndexOf("@")
- if (marker <= 0) {
+ const parsed = parseEntry(entry)
+ if (!parsed) {
continue
}
- const slug = entry.slice(0, marker).replace(/\+/g, "/")
- const version = entry.slice(marker + 1)
- const list = versions.get(slug) ?? []
- list.push({ dir: full, version, label: entry })
- versions.set(slug, list)
+ const list = versions.get(parsed.name) ?? []
+ list.push({ dir: full, version: parsed.version, label: entry })
+ versions.set(parsed.name, list)
}
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]
await mkdir(parent, { recursive: true })
const linkPath = join(parent, leaf)
const desired = join(entry.dir, "node_modules", slug)
+ const exists = await lstat(desired)
+ .then(info => info.isDirectory())
+ .catch(() => false)
+ if (!exists) {
+ continue
+ }
const relativeTarget = relative(parent, desired)
const resolved = relativeTarget.length === 0 ? "." : relativeTarget
await rm(linkPath, { recursive: true, force: true })
@@ -94,3 +98,16 @@ for (const line of rewrites.slice(0, 20)) {
if (rewrites.length > 20) {
console.log(" ...")
}
+
+function parseEntry(label: string) {
+ const marker = label.startsWith("@") ? label.indexOf("@", 1) : label.indexOf("@")
+ if (marker <= 0) {
+ return null
+ }
+ const name = label.slice(0, marker).replace(/\+/g, "/")
+ const version = label.slice(marker + 1)
+ if (!name || !version) {
+ return null
+ }
+ return { name, version }
+}
diff --git a/nix/scripts/patch-wasm.ts b/nix/scripts/patch-wasm.ts
new file mode 100644
index 000000000..99f8a40e9
--- /dev/null
+++ b/nix/scripts/patch-wasm.ts
@@ -0,0 +1,39 @@
+#!/usr/bin/env bun
+
+import fs from "fs"
+import path from "path"
+
+/**
+ * Rewrite tree-sitter wasm references inside a JS file to absolute paths.
+ * argv: [node, script, file, mainWasm, ...wasmPaths]
+ */
+const [, , file, mainWasm, ...wasmPaths] = process.argv
+
+if (!file || !mainWasm) {
+ console.error("usage: patch-wasm <file> <mainWasm> [wasmPaths...]")
+ process.exit(1)
+}
+
+const content = fs.readFileSync(file, "utf8")
+const byName = new Map<string, string>()
+
+for (const wasm of wasmPaths) {
+ const name = path.basename(wasm)
+ byName.set(name, wasm)
+}
+
+let next = content
+
+for (const [name, wasmPath] of byName) {
+ next = next.replaceAll(name, wasmPath)
+}
+
+next = next.replaceAll("tree-sitter.wasm", mainWasm).replaceAll("web-tree-sitter/tree-sitter.wasm", mainWasm)
+
+// Collapse any relative prefixes before absolute store paths (e.g., "../../../..//nix/store/...")
+next = next.replace(/(\.\/)+/g, "./")
+next = next.replace(/(\.\.\/)+\/?(\/nix\/store[^"']+)/g, "/$2")
+next = next.replace(/(["'])\/{2,}(\/nix\/store[^"']+)(["'])/g, "$1/$2$3")
+next = next.replace(/(["'])\/\/(nix\/store[^"']+)(["'])/g, "$1/$2$3")
+
+if (next !== content) fs.writeFileSync(file, next)
diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts
index 89d61d0f3..79638c5e8 100644
--- a/packages/opencode/src/cli/cmd/tui/thread.ts
+++ b/packages/opencode/src/cli/cmd/tui/thread.ts
@@ -58,16 +58,14 @@ export const TuiThreadCommand = cmd({
// Resolve relative paths against PWD to preserve behavior when using --cwd flag
const baseCwd = process.env.PWD ?? process.cwd()
const cwd = args.project ? path.resolve(baseCwd, args.project) : process.cwd()
- const defaultWorker = new URL("./worker.ts", import.meta.url)
- // Nix build creates a bundled worker next to the binary; prefer it when present.
+ const localWorker = new URL("./worker.ts", import.meta.url)
+ const distWorker = new URL("./cli/cmd/tui/worker.js", import.meta.url)
const execDir = path.dirname(process.execPath)
- const bundledWorker = path.join(execDir, "opencode-worker.js")
- const hasBundledWorker = await Bun.file(bundledWorker).exists()
- const workerPath = (() => {
+ const workerPath = await iife(async () => {
if (typeof OPENCODE_WORKER_PATH !== "undefined") return OPENCODE_WORKER_PATH
- if (hasBundledWorker) return bundledWorker
- return defaultWorker
- })()
+ if (await Bun.file(distWorker).exists()) return distWorker
+ return localWorker
+ })
try {
process.chdir(cwd)
} catch (e) {