Build HttpApi UI route from services (#25177)
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { Context, Effect, Layer } from "effect"
|
import { Context, Effect, Layer } from "effect"
|
||||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||||
import { FetchHttpClient, HttpMiddleware, HttpRouter, HttpServer } from "effect/unstable/http"
|
import { FetchHttpClient, HttpClient, HttpMiddleware, HttpRouter, HttpServer } from "effect/unstable/http"
|
||||||
import * as Socket from "effect/unstable/socket/Socket"
|
import * as Socket from "effect/unstable/socket/Socket"
|
||||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||||
import { Account } from "@/account/account"
|
import { Account } from "@/account/account"
|
||||||
@@ -121,9 +121,16 @@ const instanceRoutes = Layer.mergeAll(rawInstanceRoutes, instanceApiRoutes).pipe
|
|||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
|
|
||||||
const uiRoute = HttpRouter.add("*", "/*", (request) =>
|
const uiRoute = Layer.effectDiscard(
|
||||||
serveUIEffect(request).pipe(Effect.provide(AppFileSystem.defaultLayer), Effect.provide(FetchHttpClient.layer)),
|
Effect.gen(function* () {
|
||||||
).pipe(Layer.provide(authorizationRouterMiddleware.layer.pipe(Layer.provide(ServerAuthConfig.defaultLayer))))
|
const fs = yield* AppFileSystem.Service
|
||||||
|
const client = yield* HttpClient.HttpClient
|
||||||
|
const router = yield* HttpRouter.HttpRouter
|
||||||
|
yield* router.add("*", "/*", (request) => serveUIEffect(request, { fs, client }))
|
||||||
|
}),
|
||||||
|
).pipe(
|
||||||
|
Layer.provide(authorizationRouterMiddleware.layer.pipe(Layer.provide(ServerAuthConfig.defaultLayer))),
|
||||||
|
)
|
||||||
|
|
||||||
export const routes = Layer.mergeAll(rootApiRoutes, instanceRoutes, uiRoute).pipe(
|
export const routes = Layer.mergeAll(rootApiRoutes, instanceRoutes, uiRoute).pipe(
|
||||||
Layer.provide([
|
Layer.provide([
|
||||||
@@ -162,6 +169,8 @@ export const routes = Layer.mergeAll(rootApiRoutes, instanceRoutes, uiRoute).pip
|
|||||||
Workspace.defaultLayer,
|
Workspace.defaultLayer,
|
||||||
Worktree.defaultLayer,
|
Worktree.defaultLayer,
|
||||||
Bus.layer,
|
Bus.layer,
|
||||||
|
AppFileSystem.defaultLayer,
|
||||||
|
FetchHttpClient.layer,
|
||||||
HttpServer.layerServices,
|
HttpServer.layerServices,
|
||||||
]),
|
]),
|
||||||
Layer.provideMerge(Observability.layer),
|
Layer.provideMerge(Observability.layer),
|
||||||
|
|||||||
@@ -79,7 +79,10 @@ export async function serveUI(request: Request) {
|
|||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serveUIEffect(request: HttpServerRequest.HttpServerRequest) {
|
export function serveUIEffect(
|
||||||
|
request: HttpServerRequest.HttpServerRequest,
|
||||||
|
services: { fs: AppFileSystem.Interface; client: HttpClient.HttpClient },
|
||||||
|
) {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const embeddedWebUI = yield* Effect.promise(() => embeddedUI())
|
const embeddedWebUI = yield* Effect.promise(() => embeddedUI())
|
||||||
const path = new URL(request.url, "http://localhost").pathname
|
const path = new URL(request.url, "http://localhost").pathname
|
||||||
@@ -88,18 +91,17 @@ export function serveUIEffect(request: HttpServerRequest.HttpServerRequest) {
|
|||||||
const match = embeddedWebUI[path.replace(/^\//, "")] ?? embeddedWebUI["index.html"] ?? null
|
const match = embeddedWebUI[path.replace(/^\//, "")] ?? embeddedWebUI["index.html"] ?? null
|
||||||
if (!match) return HttpServerResponse.jsonUnsafe({ error: "Not Found" }, { status: 404 })
|
if (!match) return HttpServerResponse.jsonUnsafe({ error: "Not Found" }, { status: 404 })
|
||||||
|
|
||||||
const fs = yield* AppFileSystem.Service
|
if (yield* services.fs.existsSafe(match)) {
|
||||||
if (yield* fs.existsSafe(match)) {
|
|
||||||
const mime = getMimeType(match) ?? "text/plain"
|
const mime = getMimeType(match) ?? "text/plain"
|
||||||
const headers = new Headers({ "content-type": mime })
|
const headers = new Headers({ "content-type": mime })
|
||||||
if (mime.startsWith("text/html")) headers.set("content-security-policy", DEFAULT_CSP)
|
if (mime.startsWith("text/html")) headers.set("content-security-policy", DEFAULT_CSP)
|
||||||
return HttpServerResponse.raw(yield* fs.readFile(match), { headers })
|
return HttpServerResponse.raw(yield* services.fs.readFile(match), { headers })
|
||||||
}
|
}
|
||||||
|
|
||||||
return HttpServerResponse.jsonUnsafe({ error: "Not Found" }, { status: 404 })
|
return HttpServerResponse.jsonUnsafe({ error: "Not Found" }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = yield* HttpClient.execute(
|
const response = yield* services.client.execute(
|
||||||
HttpClientRequest.make(request.method)(upstreamURL(path), {
|
HttpClientRequest.make(request.method)(upstreamURL(path), {
|
||||||
headers: ProxyUtil.headers(request.headers, { host: UI_UPSTREAM.host }),
|
headers: ProxyUtil.headers(request.headers, { host: UI_UPSTREAM.host }),
|
||||||
body: requestBody(request),
|
body: requestBody(request),
|
||||||
|
|||||||
@@ -74,22 +74,26 @@ function app(input?: { password?: string; username?: string }) {
|
|||||||
|
|
||||||
function uiApp(input?: { password?: string; username?: string; client?: Layer.Layer<HttpClient.HttpClient> }) {
|
function uiApp(input?: { password?: string; username?: string; client?: Layer.Layer<HttpClient.HttpClient> }) {
|
||||||
const handler = HttpRouter.toWebHandler(
|
const handler = HttpRouter.toWebHandler(
|
||||||
HttpRouter.add("*", "/*", (request) =>
|
Layer.effectDiscard(
|
||||||
serveUIEffect(request).pipe(
|
Effect.gen(function* () {
|
||||||
Effect.provide(AppFileSystem.defaultLayer),
|
const fs = yield* AppFileSystem.Service
|
||||||
Effect.provide(input?.client ?? httpClient(new Response("ui"))),
|
const client = yield* HttpClient.HttpClient
|
||||||
),
|
const router = yield* HttpRouter.HttpRouter
|
||||||
|
yield* router.add("*", "/*", (request) => serveUIEffect(request, { fs, client }))
|
||||||
|
}),
|
||||||
).pipe(
|
).pipe(
|
||||||
Layer.provide(authorizationRouterMiddleware.layer.pipe(Layer.provide(ServerAuthConfig.defaultLayer))),
|
Layer.provide(authorizationRouterMiddleware.layer.pipe(Layer.provide(ServerAuthConfig.defaultLayer))),
|
||||||
Layer.provide(HttpServer.layerServices),
|
Layer.provide([
|
||||||
Layer.provide(
|
AppFileSystem.defaultLayer,
|
||||||
|
input?.client ?? httpClient(new Response("ui")),
|
||||||
|
HttpServer.layerServices,
|
||||||
ConfigProvider.layer(
|
ConfigProvider.layer(
|
||||||
ConfigProvider.fromUnknown({
|
ConfigProvider.fromUnknown({
|
||||||
OPENCODE_SERVER_PASSWORD: input?.password,
|
OPENCODE_SERVER_PASSWORD: input?.password,
|
||||||
OPENCODE_SERVER_USERNAME: input?.username,
|
OPENCODE_SERVER_USERNAME: input?.username,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
]),
|
||||||
),
|
),
|
||||||
{ disableLogger: true },
|
{ disableLogger: true },
|
||||||
).handler
|
).handler
|
||||||
@@ -140,26 +144,32 @@ describe("HttpApi UI fallback", () => {
|
|||||||
let proxiedUrl: string | undefined
|
let proxiedUrl: string | undefined
|
||||||
|
|
||||||
const response = await Effect.runPromise(
|
const response = await Effect.runPromise(
|
||||||
serveUIEffect(HttpServerRequest.fromWeb(new Request("http://localhost/assets/app.js"))).pipe(
|
Effect.gen(function* () {
|
||||||
Effect.provide(AppFileSystem.defaultLayer),
|
const fs = yield* AppFileSystem.Service
|
||||||
|
const client = yield* HttpClient.HttpClient
|
||||||
|
return yield* serveUIEffect(HttpServerRequest.fromWeb(new Request("http://localhost/assets/app.js")), { fs, client })
|
||||||
|
}).pipe(
|
||||||
Effect.provide(
|
Effect.provide(
|
||||||
Layer.succeed(
|
Layer.mergeAll(
|
||||||
HttpClient.HttpClient,
|
AppFileSystem.defaultLayer,
|
||||||
HttpClient.make((request) => {
|
Layer.succeed(
|
||||||
proxiedUrl = request.url
|
HttpClient.HttpClient,
|
||||||
return Effect.succeed(
|
HttpClient.make((request) => {
|
||||||
HttpClientResponse.fromWeb(
|
proxiedUrl = request.url
|
||||||
request,
|
return Effect.succeed(
|
||||||
new Response("console.log('ok')", {
|
HttpClientResponse.fromWeb(
|
||||||
headers: {
|
request,
|
||||||
"content-encoding": "br",
|
new Response("console.log('ok')", {
|
||||||
"content-length": "999",
|
headers: {
|
||||||
"content-type": "text/javascript",
|
"content-encoding": "br",
|
||||||
},
|
"content-length": "999",
|
||||||
}),
|
"content-type": "text/javascript",
|
||||||
),
|
},
|
||||||
)
|
}),
|
||||||
}),
|
),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Effect.map(HttpServerResponse.toWeb),
|
Effect.map(HttpServerResponse.toWeb),
|
||||||
|
|||||||
Reference in New Issue
Block a user