From 48051a8fe16032de1741886ee0211d85e720c0e6 Mon Sep 17 00:00:00 2001 From: Maciej Pyrc Date: Wed, 7 Oct 2026 20:25:00 +0200 Subject: [PATCH] =?UTF-8?q?fix(cli):=20=F0=9F=90=9B=20emit=20RequestModel?= =?UTF-8?q?=20schema=20leaves=20and=20type-only=20imports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The generated SDK file now describes each endpoint with `RequestModel<{ ... }>` as documented, instead of a raw `Request<...>` with positional generics. `ClientInstance` and `RequestModel` are imported with `import type`, so the output compiles under `verbatimModuleSyntax`. `SdkSchema` drops its `Client` generic because `createSdk` injects the client into every leaf. Closes #145 Co-Authored-By: Claude Fable 5.1 --- packages/cli/README.md | 18 +++++------ .../codegen/openapi/v3/generator.spec.ts | 7 +++-- packages/cli/src/codegen/openapi/generator.ts | 31 ++++++++++++++----- 3 files changed, 36 insertions(+), 20 deletions(-) diff --git a/packages/cli/README.md b/packages/cli/README.md index a6c0b184..595d8c50 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -152,7 +152,8 @@ The generated file contains: ```ts // What gets generated (simplified) -import { createSdk as coreCreateSdk, ClientInstance, Request } from "@hyper-fetch/core"; +import { createSdk as coreCreateSdk } from "@hyper-fetch/core"; +import type { ClientInstance, RequestModel } from "@hyper-fetch/core"; // Schema types extracted from your OpenAPI document export interface Components { @@ -162,17 +163,16 @@ export interface Components { }; } -// Typed request for each endpoint with operationId -export const getUsers = client.createRequest<{ - response: Components["schemas"]["User"][]; -}>()({ - endpoint: "/users", - method: "GET", -}); +// Request model for each endpoint with operationId +export type SdkSchema = { + users: { + $get: RequestModel<{ response: Components["schemas"]["User"][]; endpoint: "/users" }>; + }; +}; // SDK tree you import and use export const createSdk = (client: Client) => - coreCreateSdk>(client); + coreCreateSdk(client); ``` ## ⚠️ Important Notes diff --git a/packages/cli/__tests__/codegen/openapi/v3/generator.spec.ts b/packages/cli/__tests__/codegen/openapi/v3/generator.spec.ts index 04aa490b..7a7af097 100644 --- a/packages/cli/__tests__/codegen/openapi/v3/generator.spec.ts +++ b/packages/cli/__tests__/codegen/openapi/v3/generator.spec.ts @@ -246,12 +246,13 @@ describe("Generator", () => { const compact = sdkSchema.replaceAll(/\s+/g, " "); expect(compact).toContain( - 'users: { $userId: { postsList: { $get: Request;', + 'users: { $userId: { postsList: { $get: RequestModel<{ response: GetUsers$userIdPostsListResponseType; queryParams: GetUsers$userIdPostsListQueryParams; error: GetUsers$userIdPostsListErrorType; endpoint: "/users/:userId/posts-list" }>;', ); expect(compact).toContain( - '$post: Request;', + '$post: RequestModel<{ response: CreatePostResponseType; payload: CreatePostRequestBody; error: CreatePostErrorType; endpoint: "/users/:userId/posts-list" }>;', ); - expect(compact).toContain('$get: Request;'); + expect(compact).toContain('$get: RequestModel<{ response: GetResponseType; error: GetErrorType; endpoint: "/" }>;'); + expect(compact).toContain("export type SdkSchema = {"); expect(generatedTypes.join("\n")).toContain( "export type GetUsers$userIdPostsListResponseType = Paths.Users$UserIdPostsList.Get.Responses.$200", ); diff --git a/packages/cli/src/codegen/openapi/generator.ts b/packages/cli/src/codegen/openapi/generator.ts index 2d8876a6..58f2d0b3 100644 --- a/packages/cli/src/codegen/openapi/generator.ts +++ b/packages/cli/src/codegen/openapi/generator.ts @@ -47,7 +47,9 @@ export class OpenapiRequestGenerator { const defaultFileName = "openapi.client"; const { schemaTypes, generatedTypes, sdkSchema, createSdkFn } = await this.generateRequestsFromSchema(); const contents = [ - `import { createSdk as coreCreateSdk, ClientInstance, Request } from "@hyper-fetch/core";`, + `import { createSdk as coreCreateSdk } from "@hyper-fetch/core";`, + "\n", + `import type { ClientInstance, RequestModel } from "@hyper-fetch/core";`, "\n\n", schemaTypes, "\n\n", @@ -111,7 +113,7 @@ export class OpenapiRequestGenerator { getAvailableOperations(this.openapiDocument).forEach((operation) => { const meta = OpenapiRequestGenerator.generateMethodMetadata(operation, exportedTypes); const operationTypes = OpenapiRequestGenerator.generateTypes(meta); - const requestInstanceType = OpenapiRequestGenerator.generateRequestInstanceType(meta, operationTypes); + const requestModelType = OpenapiRequestGenerator.generateRequestModelType(meta, operationTypes); generatedTypes.push(Object.values(operationTypes).join("\n")); @@ -138,24 +140,24 @@ export class OpenapiRequestGenerator { currentLevel = currentLevel[key]; } // Prefix method names with $ - currentLevel[`$${method.toLowerCase()}`] = requestInstanceType; + currentLevel[`$${method.toLowerCase()}`] = requestModelType; }); - const sdkSchema = `export type SdkSchema = {\n${formatSchema(schemaTree)}\n}`; + const sdkSchema = `export type SdkSchema = {\n${formatSchema(schemaTree)}\n}`; const createSdkFn = ` export type { Components }; export const createSdk = (client: Client, options?: Parameters[1] | undefined) => { - return coreCreateSdk>(client, options); + return coreCreateSdk(client, options); }; `; return { schemaTypes, generatedTypes, sdkSchema, createSdkFn }; }; - static generateRequestInstanceType( + static generateRequestModelType( { id, path: endpoint, queryParamsRequired }: { id: string; path: string; queryParamsRequired?: boolean }, types: Record, ) { @@ -168,9 +170,22 @@ export const createSdk = (client: Client, options ? `${createTypeBaseName(id)}QueryParams` : undefined; - const QueryParamsGeneric = QueryParams && !queryParamsRequired ? `${QueryParams} | undefined` : QueryParams; + const fields: string[] = []; + if (Response) { + fields.push(`response: ${Response}`); + } + if (Payload) { + fields.push(`payload: ${Payload}`); + } + if (QueryParams) { + fields.push(`${queryParamsRequired ? "queryParams" : "queryParams?"}: ${QueryParams}`); + } + if (LocalError) { + fields.push(`error: ${LocalError}`); + } + fields.push(`endpoint: "${endpoint}"`); - return `Request<${Response}, ${Payload}, ${QueryParamsGeneric}, ${LocalError}, "${endpoint}", Client>`; + return `RequestModel<{ ${fields.join("; ")} }>`; } static generateHyperFetchRequest(