feat(client): add embedded V2 host

This commit is contained in:
Kit Langton
2026-06-23 11:56:06 -04:00
parent 924e00776b
commit e03f5dd331
25 changed files with 507 additions and 432 deletions
+36 -3
View File
@@ -200,7 +200,9 @@ export function emitEffect(contract: Contract): Output {
export function emitEffectImported(
contract: Contract,
options: { readonly module: string; readonly api: string },
options:
| { readonly module: string; readonly api: string }
| { readonly module: string; readonly endpoints: Readonly<Record<string, string>> },
): Output {
return {
operations: operations(contract.groups),
@@ -291,7 +293,9 @@ function renderEffectFiles(groups: ReadonlyArray<Group>): Output["files"] {
function renderImportedEffectFiles(
groups: ReadonlyArray<Group>,
options: { readonly module: string; readonly api: string },
options:
| { readonly module: string; readonly api: string }
| { readonly module: string; readonly endpoints: Readonly<Record<string, string>> },
): Output["files"] {
const adapters = groups.map((group, groupIndex) => {
const rawGroup = group.endpoints[0]?.topLevel ? "RawClient" : `RawClient[${JSON.stringify(group.identifier)}]`
@@ -328,7 +332,15 @@ function renderImportedEffectFiles(
: [`${JSON.stringify(group.identifier)}: adaptGroup${index}(raw[${JSON.stringify(group.identifier)}])`],
)
const usesStream = groups.some((group) => group.endpoints.some((item) => item.operation.success === "stream"))
const client = `// Generated by @opencode-ai/httpapi-codegen. Do not edit.\nimport { Effect${usesStream ? ", Stream" : ""}, Schema } from "effect"\nimport { Sse } from "effect/unstable/encoding"\nimport { HttpClientError } from "effect/unstable/http"\nimport { HttpApiClient } from "effect/unstable/httpapi"\nimport { ${options.api} } from ${JSON.stringify(options.module)}\nimport { ClientError } from "./client-error"\n\ntype RawClient = HttpApiClient.ForApi<typeof ${options.api}>\n\nconst mapClientError = <E>(error: E) => HttpClientError.isHttpClientError(error) || Schema.isSchemaError(error) || Sse.Retry.is(error) ? new ClientError({ cause: error }) : error\n\n${adapters.join("\n\n")}\n\nexport const make = (options?: { readonly baseUrl?: URL | string }) => HttpApiClient.make(${options.api}, options).pipe(Effect.map((raw) => ({ ${fields.join(", ")} })))\n`
const imported = "api" in options
const projection = imported ? undefined : renderImportedProjection(groups, options.endpoints)
const api = imported ? options.api : "Api"
const imports =
projection === undefined
? `import { ${api} } from ${JSON.stringify(options.module)}`
: `import { HttpApi, HttpApiClient, HttpApiGroup } from "effect/unstable/httpapi"\nimport { ${projection.imports.join(", ")} } from ${JSON.stringify(options.module)}`
const httpApiImport = projection === undefined ? 'import { HttpApiClient } from "effect/unstable/httpapi"\n' : ""
const client = `// Generated by @opencode-ai/httpapi-codegen. Do not edit.\nimport { Effect${usesStream ? ", Stream" : ""}, Schema } from "effect"\nimport { Sse } from "effect/unstable/encoding"\nimport { HttpClientError } from "effect/unstable/http"\n${httpApiImport}${imports}\nimport { ClientError } from "./client-error"\n\n${projection?.source ?? ""}type RawClient = HttpApiClient.ForApi<typeof ${api}>\n\nconst mapClientError = <E>(error: E) => HttpClientError.isHttpClientError(error) || Schema.isSchemaError(error) || Sse.Retry.is(error) ? new ClientError({ cause: error }) : error\n\n${adapters.join("\n\n")}\n\nexport const make = (options?: { readonly baseUrl?: URL | string }) => HttpApiClient.make(${api}, options).pipe(Effect.map((raw) => ({ ${fields.join(", ")} })))\n`
return [
{
path: "client-error.ts",
@@ -343,6 +355,27 @@ function renderImportedEffectFiles(
]
}
function renderImportedProjection(groups: ReadonlyArray<Group>, endpoints: Readonly<Record<string, string>>) {
const imports = groups.flatMap((group) =>
group.endpoints.map((endpoint) => {
const name = endpoints[`${group.identifier}.${endpoint.endpoint.name}`]
if (name === undefined) {
throw new GenerationError({
reason: `Missing imported endpoint: ${group.identifier}.${endpoint.endpoint.name}`,
})
}
return name
}),
)
const source = `const Api = HttpApi.make("generated").${groups
.map((group) => {
const options = group.endpoints[0]?.topLevel ? ", { topLevel: true }" : ""
return `add(HttpApiGroup.make(${JSON.stringify(group.identifier)}${options})${group.endpoints.map((endpoint) => `.add(${endpoints[`${group.identifier}.${endpoint.endpoint.name}`]})`).join("")})`
})
.join(".")}\n\n`
return { imports: [...new Set(imports)], source }
}
function renderPromiseTypes(groups: ReadonlyArray<Group>) {
const types = new Map<SchemaAST.AST, string>()
const typeOf = (schema: Schema.Top) => {