chore(sdk): add generated effect example

This commit is contained in:
opencode-agent[bot] 2026-06-03 14:34:44 +00:00
commit bdbf2d4102
18 changed files with 2275 additions and 23 deletions

View file

@ -42,23 +42,7 @@ await createClient({
],
})
// Patch a @hey-api/openapi-ts codegen bug: SseFn incorrectly passes the
// endpoint's TError into the second generic of ServerSentEventsResult, which
// is the AsyncGenerator's TReturn slot. Iterator return values have nothing
// to do with HTTP errors, and any consumer that calls `.return()` or returns
// from a mock generator gets type-checked against the wrong shape. Drop the
// arg so TReturn defaults to void.
const sseTypesPath = "./src/v2/gen/client/types.gen.ts"
const sseTypesFile = Bun.file(sseTypesPath)
const sseTypesSource = await sseTypesFile.text()
const sseTypesPatched = sseTypesSource.replace(
"=> Promise<ServerSentEventsResult<TData, TError>>",
"=> Promise<ServerSentEventsResult<TData>>",
)
if (sseTypesPatched === sseTypesSource) {
throw new Error(`SseFn patch did not apply; @hey-api/openapi-ts output may have changed (${sseTypesPath})`)
}
await Bun.write(sseTypesPath, sseTypesPatched)
await patchSseTypes("./src/v2/gen/client/types.gen.ts")
const openapi = await Bun.file("./openapi.json").json()
const effect = generateFiles(await parseSpec("./openapi.json"), effectEmitter, {
@ -72,12 +56,65 @@ for (const file of effect.files) {
await Bun.write(path.join("./src/v2/gen", file.path), file.content)
}
await createClient({
input: "./example/effect-sdk/openapi.json",
output: {
path: "./example/effect-sdk/gen",
tsConfigPath: path.join(dir, "tsconfig.json"),
clean: true,
},
plugins: [
{
name: "@hey-api/typescript",
exportFromIndex: false,
},
{
name: "@hey-api/client-fetch",
exportFromIndex: false,
baseUrl: "https://api.example.com",
},
],
})
await patchSseTypes("./example/effect-sdk/gen/client/types.gen.ts")
const exampleOpenapi = await Bun.file("./example/effect-sdk/openapi.json").json()
const exampleEffect = generateFiles(await parseSpec("./example/effect-sdk/openapi.json"), effectEmitter, {
namespace: "Example",
outputDir: "./example/effect-sdk/gen",
emitterOptions: {
serverSentEvents: serverSentEvents(exampleOpenapi),
},
})
for (const file of exampleEffect.files) {
await Bun.write(path.join("./example/effect-sdk/gen", file.path), file.content)
}
await $`bun prettier --write src/gen`
await $`bun prettier --write src/v2`
await $`bun prettier --write example/effect-sdk/gen`
await $`rm -rf dist`
await $`bun tsc`
await $`rm openapi.json`
async function patchSseTypes(sseTypesPath: string) {
// Patch a @hey-api/openapi-ts codegen bug: SseFn incorrectly passes the
// endpoint's TError into the second generic of ServerSentEventsResult, which
// is the AsyncGenerator's TReturn slot. Iterator return values have nothing
// to do with HTTP errors, and any consumer that calls `.return()` or returns
// from a mock generator gets type-checked against the wrong shape. Drop the
// arg so TReturn defaults to void.
const sseTypesFile = Bun.file(sseTypesPath)
const sseTypesSource = await sseTypesFile.text()
const sseTypesPatched = sseTypesSource.replace(
"=> Promise<ServerSentEventsResult<TData, TError>>",
"=> Promise<ServerSentEventsResult<TData>>",
)
if (sseTypesPatched === sseTypesSource) {
throw new Error(`SseFn patch did not apply; @hey-api/openapi-ts output may have changed (${sseTypesPath})`)
}
await Bun.write(sseTypesPath, sseTypesPatched)
}
function serverSentEvents(spec: unknown) {
if (!spec || typeof spec !== "object" || !("paths" in spec) || !spec.paths || typeof spec.paths !== "object")
return []

View file

@ -57,13 +57,13 @@ function generateEffectClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
.map((name) => ` ${name},`),
`} from "./types.gen.js"`,
``,
`export type OpencodeEffectOptions<TData extends TDataShape, TResponse> = Omit<`,
`export type ${ctx.namespacePascal}EffectOptions<TData extends TDataShape, TResponse> = Omit<`,
` Options<TData, true, TResponse, "data">,`,
` "responseStyle" | "throwOnError"`,
`>`,
``,
`export interface ${ctx.namespacePascal}EffectClient {`,
...spec.services.flatMap((service) => serviceShape(service, sse)),
...spec.services.flatMap((service) => serviceShape(service, sse, `${ctx.namespacePascal}EffectOptions`)),
`}`,
``,
`export function create${ctx.namespacePascal}EffectClient(client: Client): ${ctx.namespacePascal}EffectClient {`,
@ -82,10 +82,13 @@ function generateEffectClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
}
}
function serviceShape(service: Service, sse: Set<string>) {
function serviceShape(service: Service, sse: Set<string>, optionsType: string) {
return [
` ${propertyName(service.name)}: {`,
...service.operations.flatMap((operation) => [doc(operation, " "), ` ${methodSignature(operation, sse)}`]),
...service.operations.flatMap((operation) => [
doc(operation, " "),
` ${methodSignature(operation, sse, optionsType)}`,
]),
` }`,
]
}
@ -100,9 +103,9 @@ function serviceFactory(service: Service, sse: Set<string>) {
]
}
function methodSignature(operation: Operation, sse: Set<string>) {
function methodSignature(operation: Operation, sse: Set<string>, optionsType: string) {
const optional = hasRequiredOptions(operation) ? "" : "?"
return `${propertyName(operation.name)}(options${optional}: OpencodeEffectOptions<${operationType(
return `${propertyName(operation.name)}(options${optional}: ${optionsType}<${operationType(
operation,
)}Data, ${operationType(operation)}Responses>): Effect.Effect<${operationResponse(operation, sse)}, ${operationError(
operation,