Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/opencode/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ dist
gen
app.log
src/provider/models-snapshot.ts
src/server/app-manifest.ts
65 changes: 63 additions & 2 deletions packages/opencode/script/build.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env bun

import { $ } from "bun"
import { $, Glob } from "bun"
import fs from "fs"
import path from "path"
import { fileURLToPath } from "url"
Expand Down Expand Up @@ -56,6 +56,52 @@ const migrations = await Promise.all(
)
console.log(`Loaded ${migrations.length} migrations`)

// Glob web UI build output to embed in binary. The app must be built first
// (turbo ensures this via dependsOn: ["^build"]).
// We generate a manifest module that maps relative URL paths to embedded $bunfs
// asset paths. Each file is imported with { type: "file" } so Bun embeds it as
// an opaque asset. A plugin forces non-JS files through the "file" loader so
// Bun does not try to parse/bundle them.
const appDistDir = path.resolve(dir, "../../packages/app/dist")
const appDistAll = fs.existsSync(appDistDir)
? Array.from(new Glob("**/*").scanSync({ cwd: appDistDir, onlyFiles: true }))
: []

// Exclude optional font files from embedding — they fall through to CDN proxy
// at runtime. Core fonts (Inter UI font + IBM Plex Mono default monospace) are
// kept. This saves ~27MB on the binary since users only use one of 12+ optional
// monospace fonts.
const CORE_FONT = /\/(inter|BlexMono)/i
const OPTIONAL_FONT = /\.(woff2|woff|ttf)$/
const appDistFiles = appDistAll.filter((f) => {
if (!OPTIONAL_FONT.test(f)) return true
return CORE_FONT.test(f)
})

const manifestPath = path.join(dir, "src/server/app-manifest.ts")
if (appDistFiles.length > 0) {
const excluded = appDistAll.length - appDistFiles.length
console.log(
`Embedding ${appDistFiles.length} web UI assets from packages/app/dist (${excluded} optional fonts externalized to CDN)`,
)
const lines = [
`// Auto-generated by build.ts — do not edit`,
`// @ts-nocheck — Bun { type: "file" } imports return strings at runtime but TS doesn't know that`,
`const manifest: Record<string, string> = {}`,
]
for (let i = 0; i < appDistFiles.length; i++) {
const abs = path.join(appDistDir, appDistFiles[i]).replaceAll("\\", "/")
const key = "/" + appDistFiles[i].replaceAll("\\", "/")
lines.push(`import _a${i} from ${JSON.stringify(abs)} with { type: "file" }`)
lines.push(`manifest[${JSON.stringify(key)}] = _a${i}`)
}
lines.push(`export default manifest`)
await Bun.write(manifestPath, lines.join("\n"))
} else {
console.log("Warning: packages/app/dist not found — web UI will not be embedded (CDN proxy fallback)")
await Bun.write(manifestPath, `// No assets found at build time\nexport default {} as Record<string, string>\n`)
}

const singleFlag = process.argv.includes("--single")
const baselineFlag = process.argv.includes("--baseline")
const skipInstall = process.argv.includes("--skip-install")
Expand Down Expand Up @@ -171,7 +217,21 @@ for (const item of targets) {
await Bun.build({
conditions: ["browser"],
tsconfig: "./tsconfig.json",
plugins: [solidPlugin],
plugins: [
solidPlugin,
// Force all non-JS/TS web assets through Bun's "file" loader so they are
// embedded as opaque blobs rather than parsed/bundled. This allows the
// generated app-manifest.ts to import HTML, CSS, images, fonts, etc.
{
name: "static-asset-loader",
setup(build) {
build.onLoad(
{ filter: /\.(html|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|wasm|webmanifest|aac|txt)$/ },
async (args) => ({ contents: new Uint8Array(await Bun.file(args.path).arrayBuffer()), loader: "file" }),
)
},
},
],
sourcemap: "external",
compile: {
autoloadBunfig: false,
Expand All @@ -191,6 +251,7 @@ for (const item of targets) {
OPENCODE_WORKER_PATH: workerPath,
OPENCODE_CHANNEL: `'${Script.channel}'`,
OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "",
OPENCODE_APP_EMBEDDED: appDistFiles.length > 0 ? "true" : "false",
},
})

Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/flag/flag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export namespace Flag {
export declare const OPENCODE_CLIENT: string
export const OPENCODE_SERVER_PASSWORD = process.env["OPENCODE_SERVER_PASSWORD"]
export const OPENCODE_SERVER_USERNAME = process.env["OPENCODE_SERVER_USERNAME"]
export const OPENCODE_APP_DIR = process.env["OPENCODE_APP_DIR"]
export const OPENCODE_ENABLE_QUESTION_TOOL = truthy("OPENCODE_ENABLE_QUESTION_TOOL")

// Experimental
Expand Down
Loading
Loading