feat: embed WebUI in binary with proxy flags (#19299)
Co-authored-by: BlankParticle <blankparticle@gmail.com>
This commit is contained in:
@@ -63,6 +63,26 @@ console.log(`Loaded ${migrations.length} migrations`)
|
|||||||
const singleFlag = process.argv.includes("--single")
|
const singleFlag = process.argv.includes("--single")
|
||||||
const baselineFlag = process.argv.includes("--baseline")
|
const baselineFlag = process.argv.includes("--baseline")
|
||||||
const skipInstall = process.argv.includes("--skip-install")
|
const skipInstall = process.argv.includes("--skip-install")
|
||||||
|
const skipEmbedWebUi = process.argv.includes("--skip-embed-web-ui")
|
||||||
|
|
||||||
|
|
||||||
|
const createEmbeddedWebUIBundle = async()=>{
|
||||||
|
console.log(`Building Web UI to embed in the binary`);
|
||||||
|
const appDir = path.join(import.meta.dirname, "../../app")
|
||||||
|
await $`bun run --cwd ${appDir} build`;
|
||||||
|
const allFiles = await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: path.join(appDir, "dist")}));
|
||||||
|
const fileMap = `
|
||||||
|
// Import all files as file_$i with type: "file"
|
||||||
|
${allFiles.map((filePath, i) => `import file_${i} from "${path.join(appDir, "dist", filePath)}" with { type: "file" };`).join("\n")}
|
||||||
|
// Export with original mappings
|
||||||
|
export default {
|
||||||
|
${allFiles.map((filePath, i)=>`"${filePath}": file_${i},`).join("\n")}
|
||||||
|
}
|
||||||
|
`.trim()
|
||||||
|
return fileMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
const embeddedFileMap = skipEmbedWebUi ? null : await createEmbeddedWebUIBundle();
|
||||||
|
|
||||||
const allTargets: {
|
const allTargets: {
|
||||||
os: string
|
os: string
|
||||||
@@ -192,7 +212,10 @@ for (const item of targets) {
|
|||||||
execArgv: [`--user-agent=opencode/${Script.version}`, "--use-system-ca", "--"],
|
execArgv: [`--user-agent=opencode/${Script.version}`, "--use-system-ca", "--"],
|
||||||
windows: {},
|
windows: {},
|
||||||
},
|
},
|
||||||
entrypoints: ["./src/index.ts", parserWorker, workerPath],
|
files: {
|
||||||
|
...(embeddedFileMap ? { "opencode-web-ui.gen.ts": embeddedFileMap } : {}),
|
||||||
|
},
|
||||||
|
entrypoints: ["./src/index.ts", parserWorker, workerPath, ...(embeddedFileMap ? ["opencode-web-ui.gen.ts"] : [])],
|
||||||
define: {
|
define: {
|
||||||
OPENCODE_VERSION: `'${Script.version}'`,
|
OPENCODE_VERSION: `'${Script.version}'`,
|
||||||
OPENCODE_MIGRATIONS: JSON.stringify(migrations),
|
OPENCODE_MIGRATIONS: JSON.stringify(migrations),
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ export namespace Flag {
|
|||||||
export const OPENCODE_EXPERIMENTAL_MARKDOWN = !falsy("OPENCODE_EXPERIMENTAL_MARKDOWN")
|
export const OPENCODE_EXPERIMENTAL_MARKDOWN = !falsy("OPENCODE_EXPERIMENTAL_MARKDOWN")
|
||||||
export const OPENCODE_MODELS_URL = process.env["OPENCODE_MODELS_URL"]
|
export const OPENCODE_MODELS_URL = process.env["OPENCODE_MODELS_URL"]
|
||||||
export const OPENCODE_MODELS_PATH = process.env["OPENCODE_MODELS_PATH"]
|
export const OPENCODE_MODELS_PATH = process.env["OPENCODE_MODELS_PATH"]
|
||||||
|
export const OPENCODE_DISABLE_EMBEDDED_WEB_UI = truthy("OPENCODE_DISABLE_EMBEDDED_WEB_UI")
|
||||||
export const OPENCODE_DB = process.env["OPENCODE_DB"]
|
export const OPENCODE_DB = process.env["OPENCODE_DB"]
|
||||||
export const OPENCODE_DISABLE_CHANNEL_DB = truthy("OPENCODE_DISABLE_CHANNEL_DB")
|
export const OPENCODE_DISABLE_CHANNEL_DB = truthy("OPENCODE_DISABLE_CHANNEL_DB")
|
||||||
export const OPENCODE_SKIP_MIGRATIONS = truthy("OPENCODE_SKIP_MIGRATIONS")
|
export const OPENCODE_SKIP_MIGRATIONS = truthy("OPENCODE_SKIP_MIGRATIONS")
|
||||||
|
|||||||
@@ -56,6 +56,12 @@ initProjectors()
|
|||||||
|
|
||||||
export namespace Server {
|
export namespace Server {
|
||||||
const log = Log.create({ service: "server" })
|
const log = Log.create({ service: "server" })
|
||||||
|
const DEFAULT_CSP =
|
||||||
|
"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:"
|
||||||
|
const embeddedUIPromise = Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI
|
||||||
|
? Promise.resolve(null)
|
||||||
|
: // @ts-expect-error - generated file at build time
|
||||||
|
import("opencode-web-ui.gen.ts").then((module) => module.default as Record<string, string>).catch(() => null)
|
||||||
|
|
||||||
export const Default = lazy(() => createApp({}))
|
export const Default = lazy(() => createApp({}))
|
||||||
|
|
||||||
@@ -504,24 +510,40 @@ export namespace Server {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.all("/*", async (c) => {
|
.all("/*", async (c) => {
|
||||||
|
const embeddedWebUI = await embeddedUIPromise
|
||||||
const path = c.req.path
|
const path = c.req.path
|
||||||
|
|
||||||
const response = await proxy(`https://app.opencode.ai${path}`, {
|
if (embeddedWebUI) {
|
||||||
...c.req,
|
const match = embeddedWebUI[path.replace(/^\//, "")] ?? embeddedWebUI["index.html"] ?? null
|
||||||
headers: {
|
if (!match) return c.json({ error: "Not Found" }, 404)
|
||||||
...c.req.raw.headers,
|
const file = Bun.file(match)
|
||||||
host: "app.opencode.ai",
|
if (await file.exists()) {
|
||||||
},
|
c.header("Content-Type", file.type)
|
||||||
})
|
if (file.type.startsWith("text/html")) {
|
||||||
const match = response.headers.get("content-type")?.includes("text/html")
|
c.header("Content-Security-Policy", DEFAULT_CSP)
|
||||||
? (await response.clone().text()).match(
|
}
|
||||||
/<script\b(?![^>]*\bsrc\s*=)[^>]*\bid=(['"])oc-theme-preload-script\1[^>]*>([\s\S]*?)<\/script>/i,
|
return c.body(await file.arrayBuffer())
|
||||||
)
|
} else {
|
||||||
: undefined
|
return c.json({ error: "Not Found" }, 404)
|
||||||
const hash = match ? createHash("sha256").update(match[2]).digest("base64") : ""
|
}
|
||||||
response.headers.set("Content-Security-Policy", csp(hash))
|
} else {
|
||||||
return response
|
const response = await proxy(`https://app.opencode.ai${path}`, {
|
||||||
})
|
...c.req,
|
||||||
|
headers: {
|
||||||
|
...c.req.raw.headers,
|
||||||
|
host: "app.opencode.ai",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const match = response.headers.get("content-type")?.includes("text/html")
|
||||||
|
? (await response.clone().text()).match(
|
||||||
|
/<script\b(?![^>]*\bsrc\s*=)[^>]*\bid=(['"])oc-theme-preload-script\1[^>]*>([\s\S]*?)<\/script>/i,
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
const hash = match ? createHash("sha256").update(match[2]).digest("base64") : ""
|
||||||
|
response.headers.set("Content-Security-Policy", csp(hash))
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
}) as unknown as Hono
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openapi() {
|
export async function openapi() {
|
||||||
|
|||||||
Reference in New Issue
Block a user