Skip to content

Commit ec20efc

Browse files
thdxrBlankParticle
andauthored
feat: embed WebUI in binary with proxy flags (#19299)
Co-authored-by: BlankParticle <[email protected]>
1 parent 83ed1c4 commit ec20efc

3 files changed

Lines changed: 63 additions & 17 deletions

File tree

packages/opencode/script/build.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,26 @@ console.log(`Loaded ${migrations.length} migrations`)
6363
const singleFlag = process.argv.includes("--single")
6464
const baselineFlag = process.argv.includes("--baseline")
6565
const skipInstall = process.argv.includes("--skip-install")
66+
const skipEmbedWebUi = process.argv.includes("--skip-embed-web-ui")
67+
68+
69+
const createEmbeddedWebUIBundle = async()=>{
70+
console.log(`Building Web UI to embed in the binary`);
71+
const appDir = path.join(import.meta.dirname, "../../app")
72+
await $`bun run --cwd ${appDir} build`;
73+
const allFiles = await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: path.join(appDir, "dist")}));
74+
const fileMap = `
75+
// Import all files as file_$i with type: "file"
76+
${allFiles.map((filePath, i) => `import file_${i} from "${path.join(appDir, "dist", filePath)}" with { type: "file" };`).join("\n")}
77+
// Export with original mappings
78+
export default {
79+
${allFiles.map((filePath, i)=>`"${filePath}": file_${i},`).join("\n")}
80+
}
81+
`.trim()
82+
return fileMap;
83+
}
84+
85+
const embeddedFileMap = skipEmbedWebUi ? null : await createEmbeddedWebUIBundle();
6686

6787
const allTargets: {
6888
os: string
@@ -192,7 +212,10 @@ for (const item of targets) {
192212
execArgv: [`--user-agent=opencode/${Script.version}`, "--use-system-ca", "--"],
193213
windows: {},
194214
},
195-
entrypoints: ["./src/index.ts", parserWorker, workerPath],
215+
files: {
216+
...(embeddedFileMap ? { "opencode-web-ui.gen.ts": embeddedFileMap } : {}),
217+
},
218+
entrypoints: ["./src/index.ts", parserWorker, workerPath, ...(embeddedFileMap ? ["opencode-web-ui.gen.ts"] : [])],
196219
define: {
197220
OPENCODE_VERSION: `'${Script.version}'`,
198221
OPENCODE_MIGRATIONS: JSON.stringify(migrations),

packages/opencode/src/flag/flag.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export namespace Flag {
7070
export const OPENCODE_EXPERIMENTAL_MARKDOWN = !falsy("OPENCODE_EXPERIMENTAL_MARKDOWN")
7171
export const OPENCODE_MODELS_URL = process.env["OPENCODE_MODELS_URL"]
7272
export const OPENCODE_MODELS_PATH = process.env["OPENCODE_MODELS_PATH"]
73+
export const OPENCODE_DISABLE_EMBEDDED_WEB_UI = truthy("OPENCODE_DISABLE_EMBEDDED_WEB_UI")
7374
export const OPENCODE_DB = process.env["OPENCODE_DB"]
7475
export const OPENCODE_DISABLE_CHANNEL_DB = truthy("OPENCODE_DISABLE_CHANNEL_DB")
7576
export const OPENCODE_SKIP_MIGRATIONS = truthy("OPENCODE_SKIP_MIGRATIONS")

packages/opencode/src/server/server.ts

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ initProjectors()
5656

5757
export namespace Server {
5858
const log = Log.create({ service: "server" })
59+
const DEFAULT_CSP =
60+
"default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src 'self' data:"
61+
const embeddedUIPromise = Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI
62+
? Promise.resolve(null)
63+
: // @ts-expect-error - generated file at build time
64+
import("opencode-web-ui.gen.ts").then((module) => module.default as Record<string, string>).catch(() => null)
5965

6066
export const Default = lazy(() => createApp({}))
6167

@@ -504,24 +510,40 @@ export namespace Server {
504510
},
505511
)
506512
.all("/*", async (c) => {
513+
const embeddedWebUI = await embeddedUIPromise
507514
const path = c.req.path
508515

509-
const response = await proxy(`https://app.opencode.ai${path}`, {
510-
...c.req,
511-
headers: {
512-
...c.req.raw.headers,
513-
host: "app.opencode.ai",
514-
},
515-
})
516-
const match = response.headers.get("content-type")?.includes("text/html")
517-
? (await response.clone().text()).match(
518-
/<script\b(?![^>]*\bsrc\s*=)[^>]*\bid=(['"])oc-theme-preload-script\1[^>]*>([\s\S]*?)<\/script>/i,
519-
)
520-
: undefined
521-
const hash = match ? createHash("sha256").update(match[2]).digest("base64") : ""
522-
response.headers.set("Content-Security-Policy", csp(hash))
523-
return response
524-
})
516+
if (embeddedWebUI) {
517+
const match = embeddedWebUI[path.replace(/^\//, "")] ?? embeddedWebUI["index.html"] ?? null
518+
if (!match) return c.json({ error: "Not Found" }, 404)
519+
const file = Bun.file(match)
520+
if (await file.exists()) {
521+
c.header("Content-Type", file.type)
522+
if (file.type.startsWith("text/html")) {
523+
c.header("Content-Security-Policy", DEFAULT_CSP)
524+
}
525+
return c.body(await file.arrayBuffer())
526+
} else {
527+
return c.json({ error: "Not Found" }, 404)
528+
}
529+
} else {
530+
const response = await proxy(`https://app.opencode.ai${path}`, {
531+
...c.req,
532+
headers: {
533+
...c.req.raw.headers,
534+
host: "app.opencode.ai",
535+
},
536+
})
537+
const match = response.headers.get("content-type")?.includes("text/html")
538+
? (await response.clone().text()).match(
539+
/<script\b(?![^>]*\bsrc\s*=)[^>]*\bid=(['"])oc-theme-preload-script\1[^>]*>([\s\S]*?)<\/script>/i,
540+
)
541+
: undefined
542+
const hash = match ? createHash("sha256").update(match[2]).digest("base64") : ""
543+
response.headers.set("Content-Security-Policy", csp(hash))
544+
return response
545+
}
546+
}) as unknown as Hono
525547
}
526548

527549
export async function openapi() {

0 commit comments

Comments
 (0)