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:
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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.$,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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" },
|
||||||
|
|||||||
Reference in New Issue
Block a user