refactor(server): extract createApp function for server initialization

- Replace Server.App() with Server.Default() for internal server access
- Extract server app creation into Server.createApp(opts) for testability
- Move CORS whitelist from module-level variable to function parameter
- Update all tests to use Server.Default() instead of Server.App()
This commit is contained in:
Dax Raad
2026-03-09 17:13:52 -04:00
parent ee18c9976e
commit 89d6f60d25
6 changed files with 493 additions and 504 deletions
+1 -1
View File
@@ -667,7 +667,7 @@ export const RunCommand = cmd({
await bootstrap(process.cwd(), async () => { await bootstrap(process.cwd(), async () => {
const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => { const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {
const request = new Request(input, init) const request = new Request(input, init)
return Server.App().fetch(request) return Server.Default().fetch(request)
}) as typeof globalThis.fetch }) as typeof globalThis.fetch
const sdk = createOpencodeClient({ baseUrl: "http://opencode.internal", fetch: fetchFn }) const sdk = createOpencodeClient({ baseUrl: "http://opencode.internal", fetch: fetchFn })
await execute(sdk) await execute(sdk)
+2 -2
View File
@@ -54,7 +54,7 @@ const startEventStream = (input: { directory: string; workspaceID?: string }) =>
const request = new Request(input, init) const request = new Request(input, init)
const auth = getAuthorizationHeader() const auth = getAuthorizationHeader()
if (auth) request.headers.set("Authorization", auth) if (auth) request.headers.set("Authorization", auth)
return Server.App().fetch(request) return Server.Default().fetch(request)
}) as typeof globalThis.fetch }) as typeof globalThis.fetch
const sdk = createOpencodeClient({ const sdk = createOpencodeClient({
@@ -110,7 +110,7 @@ export const rpc = {
headers, headers,
body: input.body, body: input.body,
}) })
const response = await Server.App().fetch(request) const response = await Server.Default().fetch(request)
const body = await response.text() const body = await response.text()
return { return {
status: response.status, status: response.status,
+4 -3
View File
@@ -25,8 +25,7 @@ export namespace Plugin {
const client = createOpencodeClient({ const client = createOpencodeClient({
baseUrl: "http://localhost:4096", baseUrl: "http://localhost:4096",
directory: Instance.directory, directory: Instance.directory,
// @ts-ignore - fetch type incompatibility fetch: async (...args) => Server.Default().fetch(...args),
fetch: async (...args) => Server.App().fetch(...args),
}) })
const config = await Config.get() const config = await Config.get()
const hooks: Hooks[] = [] const hooks: Hooks[] = []
@@ -35,7 +34,9 @@ export namespace Plugin {
project: Instance.project, project: Instance.project,
worktree: Instance.worktree, worktree: Instance.worktree,
directory: Instance.directory, directory: Instance.directory,
serverUrl: Server.url(), get serverUrl(): URL {
throw new Error("Server URL is no longer supported in plugins")
},
$: Bun.$, $: Bun.$,
} }
+12 -24
View File
@@ -31,7 +31,6 @@ import { FileRoutes } from "./routes/file"
import { ConfigRoutes } from "./routes/config" import { ConfigRoutes } from "./routes/config"
import { ExperimentalRoutes } from "./routes/experimental" import { ExperimentalRoutes } from "./routes/experimental"
import { ProviderRoutes } from "./routes/provider" import { ProviderRoutes } from "./routes/provider"
import { lazy } from "../util/lazy"
import { InstanceBootstrap } from "../project/bootstrap" import { InstanceBootstrap } from "../project/bootstrap"
import { NotFoundError } from "../storage/db" import { NotFoundError } from "../storage/db"
import type { ContentfulStatusCode } from "hono/utils/http-status" import type { ContentfulStatusCode } from "hono/utils/http-status"
@@ -43,6 +42,7 @@ import { QuestionRoutes } from "./routes/question"
import { PermissionRoutes } from "./routes/permission" import { PermissionRoutes } from "./routes/permission"
import { GlobalRoutes } from "./routes/global" import { GlobalRoutes } from "./routes/global"
import { MDNS } from "./mdns" import { MDNS } from "./mdns"
import { lazy } from "@/util/lazy"
// @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85 // @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
globalThis.AI_SDK_LOG_WARNINGS = false globalThis.AI_SDK_LOG_WARNINGS = false
@@ -50,18 +50,11 @@ globalThis.AI_SDK_LOG_WARNINGS = false
export namespace Server { export namespace Server {
const log = Log.create({ service: "server" }) const log = Log.create({ service: "server" })
let _url: URL | undefined export const Default = lazy(() => createApp({}))
let _corsWhitelist: string[] = []
export function url(): URL {
return _url ?? new URL("http://localhost:4096")
}
export const createApp = (opts: { cors?: string[] }): Hono => {
const app = new Hono() const app = new Hono()
export const App: () => Hono = lazy( return app
() =>
// TODO: Break server.ts into smaller route files to fix type inference
app
.onError((err, c) => { .onError((err, c) => {
log.error("failed", { log.error("failed", {
error: err, error: err,
@@ -124,7 +117,7 @@ export namespace Server {
if (/^https:\/\/([a-z0-9-]+\.)*opencode\.ai$/.test(input)) { if (/^https:\/\/([a-z0-9-]+\.)*opencode\.ai$/.test(input)) {
return input return input
} }
if (_corsWhitelist.includes(input)) { if (opts?.cors?.includes(input)) {
return input return input
} }
@@ -282,8 +275,7 @@ export namespace Server {
"/path", "/path",
describeRoute({ describeRoute({
summary: "Get paths", summary: "Get paths",
description: description: "Retrieve the current working directory and related path information for the OpenCode instance.",
"Retrieve the current working directory and related path information for the OpenCode instance.",
operationId: "path.get", operationId: "path.get",
responses: { responses: {
200: { 200: {
@@ -322,8 +314,7 @@ export namespace Server {
"/vcs", "/vcs",
describeRoute({ describeRoute({
summary: "Get VCS info", summary: "Get VCS info",
description: description: "Retrieve version control system (VCS) information for the current project, such as git branch.",
"Retrieve version control system (VCS) information for the current project, such as git branch.",
operationId: "vcs.get", operationId: "vcs.get",
responses: { responses: {
200: { 200: {
@@ -576,12 +567,12 @@ export namespace Server {
"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:", "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:",
) )
return response return response
}) as unknown as Hono, })
) }
export async function openapi() { export async function openapi() {
// Cast to break excessive type recursion from long route chains // Cast to break excessive type recursion from long route chains
const result = await generateSpecs(App() as Hono, { const result = await generateSpecs(Default(), {
documentation: { documentation: {
info: { info: {
title: "opencode", title: "opencode",
@@ -601,12 +592,11 @@ export namespace Server {
mdnsDomain?: string mdnsDomain?: string
cors?: string[] cors?: string[]
}) { }) {
_corsWhitelist = opts.cors ?? [] const app = createApp(opts)
const args = { const args = {
hostname: opts.hostname, hostname: opts.hostname,
idleTimeout: 0, idleTimeout: 0,
fetch: App().fetch, fetch: app.fetch,
websocket: websocket, websocket: websocket,
} as const } as const
const tryServe = (port: number) => { const tryServe = (port: number) => {
@@ -619,8 +609,6 @@ export namespace Server {
const server = opts.port === 0 ? (tryServe(4096) ?? tryServe(0)) : tryServe(opts.port) const server = opts.port === 0 ? (tryServe(4096) ?? tryServe(0)) : tryServe(opts.port)
if (!server) throw new Error(`Failed to start server on port ${opts.port}`) if (!server) throw new Error(`Failed to start server on port ${opts.port}`)
_url = server.url
const shouldPublishMDNS = const shouldPublishMDNS =
opts.mdns && opts.mdns &&
server.port && server.port &&
@@ -19,7 +19,7 @@ afterEach(async () => {
describe("project.initGit endpoint", () => { describe("project.initGit endpoint", () => {
test("initializes git and reloads immediately", async () => { test("initializes git and reloads immediately", async () => {
await using tmp = await tmpdir() await using tmp = await tmpdir()
const app = Server.App() const app = Server.Default()
const seen: { directory?: string; payload: { type: string } }[] = [] const seen: { directory?: string; payload: { type: string } }[] = []
const fn = (evt: { directory?: string; payload: { type: string } }) => { const fn = (evt: { directory?: string; payload: { type: string } }) => {
seen.push(evt) seen.push(evt)
@@ -75,7 +75,7 @@ describe("project.initGit endpoint", () => {
test("does not reload when the project is already git", async () => { test("does not reload when the project is already git", async () => {
await using tmp = await tmpdir({ git: true }) await using tmp = await tmpdir({ git: true })
const app = Server.App() const app = Server.Default()
const seen: { directory?: string; payload: { type: string } }[] = [] const seen: { directory?: string; payload: { type: string } }[] = []
const fn = (evt: { directory?: string; payload: { type: string } }) => { const fn = (evt: { directory?: string; payload: { type: string } }) => {
seen.push(evt) seen.push(evt)
@@ -17,7 +17,7 @@ describe("tui.selectSession endpoint", () => {
const session = await Session.create({}) const session = await Session.create({})
// #when // #when
const app = Server.App() const app = Server.Default()
const response = await app.request("/tui/select-session", { const response = await app.request("/tui/select-session", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
@@ -42,7 +42,7 @@ describe("tui.selectSession endpoint", () => {
const nonExistentSessionID = "ses_nonexistent123" const nonExistentSessionID = "ses_nonexistent123"
// #when // #when
const app = Server.App() const app = Server.Default()
const response = await app.request("/tui/select-session", { const response = await app.request("/tui/select-session", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
@@ -63,7 +63,7 @@ describe("tui.selectSession endpoint", () => {
const invalidSessionID = "invalid_session_id" const invalidSessionID = "invalid_session_id"
// #when // #when
const app = Server.App() const app = Server.Default()
const response = await app.request("/tui/select-session", { const response = await app.request("/tui/select-session", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },