UNPKG

typed-openapi

Version:
1,374 lines (1,314 loc) 57.6 kB
import { prettify } from "./chunk-KAEXXJ7X.js"; // src/asserts.ts var isPrimitiveType = (type2) => primitiveTypeList.includes(type2); var primitiveTypeList = ["string", "number", "integer", "boolean", "null"]; // src/is-reference-object.ts function isReferenceObject(obj) { return obj != null && Object.prototype.hasOwnProperty.call(obj, "$ref"); } // src/string-utils.ts import { capitalize, kebabToCamel } from "pastable/server"; function normalizeString(text) { const prefixed = prefixStringStartingWithNumberIfNeeded(text); return prefixed.normalize("NFKD").trim().replace(/\s+/g, "_").replace(/-+/g, "_").replace(/[^\w\-]+/g, "_").replace(/--+/g, "-"); } var onlyWordRegex = /^\w+$/; var wrapWithQuotesIfNeeded = (str) => { if (str[0] === '"' && str[str.length - 1] === '"') return str; if (onlyWordRegex.test(str)) { return str; } return `"${str}"`; }; var prefixStringStartingWithNumberIfNeeded = (str) => { const firstAsNumber = Number(str[0]); if (typeof firstAsNumber === "number" && !Number.isNaN(firstAsNumber)) { return "_" + str; } return str; }; var pathParamWithBracketsRegex = /({\w+})/g; var wordPrecededByNonWordCharacter = /[^\w\-]+/g; var pathToVariableName = (path) => capitalize(kebabToCamel(path).replaceAll("/", "_")).replace(pathParamWithBracketsRegex, (group) => capitalize(group.slice(1, -1))).replace(wordPrecededByNonWordCharacter, "_"); // src/openapi-schema-to-ts.ts var openApiSchemaToTs = ({ schema, meta: _inheritedMeta, ctx }) => { const meta = {}; if (!schema) { throw new Error("Schema is required"); } const t = createBoxFactory(schema, ctx); const getTs = () => { if (isReferenceObject(schema)) { const refInfo = ctx.refs.getInfosByRef(schema.$ref); return t.reference(refInfo.normalized); } if (Array.isArray(schema.type)) { if (schema.type.length === 1) { return openApiSchemaToTs({ schema: { ...schema, type: schema.type[0] }, ctx, meta }); } return t.union(schema.type.map((prop) => openApiSchemaToTs({ schema: { ...schema, type: prop }, ctx, meta }))); } if (schema.type === "null") { return t.literal("null"); } if (schema.oneOf) { if (schema.oneOf.length === 1) { return openApiSchemaToTs({ schema: schema.oneOf[0], ctx, meta }); } return t.union(schema.oneOf.map((prop) => openApiSchemaToTs({ schema: prop, ctx, meta }))); } if (schema.anyOf) { if (schema.anyOf.length === 1) { return openApiSchemaToTs({ schema: schema.anyOf[0], ctx, meta }); } return t.union(schema.anyOf.map((prop) => openApiSchemaToTs({ schema: prop, ctx, meta }))); } if (schema.allOf) { const types = schema.allOf.map((prop) => openApiSchemaToTs({ schema: prop, ctx, meta })); const { allOf, externalDocs, example, examples, description, title, ...rest } = schema; if (Object.keys(rest).length > 0) { types.push(openApiSchemaToTs({ schema: rest, ctx, meta })); } return t.intersection(types); } const schemaType = schema.type ? schema.type.toLowerCase() : void 0; if (schemaType && isPrimitiveType(schemaType)) { if (schema.enum) { if (schema.enum.length === 1) { const value = schema.enum[0]; if (value === null) { return t.literal("null"); } else if (value === true) { return t.literal("true"); } else if (value === false) { return t.literal("false"); } else if (typeof value === "number") { return t.literal(`${value}`); } else { return t.literal(`"${value}"`); } } if (schemaType === "string") { return t.union(schema.enum.map((value) => t.literal(`"${value}"`))); } if (schema.enum.some((e) => typeof e === "string")) { return t.never(); } return t.union(schema.enum.map((value) => t.literal(value === null ? "null" : value))); } if (schemaType === "string") return t.string(); if (schemaType === "boolean") return t.boolean(); if (schemaType === "number" || schemaType === "integer") return t.number(); if (schemaType === "null") return t.literal("null"); } if (!schemaType && schema.enum) { return t.union( schema.enum.map((value) => { if (typeof value === "string") { return t.literal(`"${value}"`); } if (value === null) { return t.literal("null"); } return t.literal(value); }) ); } if (schemaType === "array") { if (schema.items) { let arrayOfType = openApiSchemaToTs({ schema: schema.items, ctx, meta }); if (typeof arrayOfType === "string") { arrayOfType = t.reference(arrayOfType); } return t.array(arrayOfType); } return t.array(t.any()); } if (schemaType === "object" || schema.properties || schema.additionalProperties) { if (!schema.properties) { if (schema.additionalProperties && !isReferenceObject(schema.additionalProperties) && typeof schema.additionalProperties !== "boolean") { const valueSchema = openApiSchemaToTs({ schema: schema.additionalProperties, ctx, meta }); return t.literal(`Record<string, ${valueSchema.value}>`); } return t.literal("Record<string, unknown>"); } let additionalProperties; if (schema.additionalProperties) { let additionalPropertiesType; if (typeof schema.additionalProperties === "boolean" && schema.additionalProperties || typeof schema.additionalProperties === "object" && Object.keys(schema.additionalProperties).length === 0) { additionalPropertiesType = t.any(); } else if (typeof schema.additionalProperties === "object") { additionalPropertiesType = openApiSchemaToTs({ schema: schema.additionalProperties, ctx, meta }); } additionalProperties = t.literal( `Record<string, ${additionalPropertiesType ? additionalPropertiesType.value : t.any().value}>` ); } const hasRequiredArray = schema.required && schema.required.length > 0; const isPartial = !schema.required?.length; const props = Object.fromEntries( Object.entries(schema.properties).map(([prop, propSchema]) => { let propType = openApiSchemaToTs({ schema: propSchema, ctx, meta }); if (typeof propType === "string") { propType = t.reference(propType); } const isRequired = Boolean(isPartial ? true : hasRequiredArray ? schema.required?.includes(prop) : false); const isOptional = !isPartial && !isRequired; return [`${wrapWithQuotesIfNeeded(prop)}`, isOptional ? t.optional(propType) : propType]; }) ); const objectType = additionalProperties ? t.intersection([t.object(props), additionalProperties]) : t.object(props); return isPartial ? t.reference("Partial", [objectType]) : objectType; } if (!schemaType) { const nullableKey = Object.keys(schema).filter( (key) => !["nullable"].includes(key) ); if (nullableKey.length === 0 && schema.nullable) { return t.literal("null"); } return t.unknown(); } throw new Error(`Unsupported schema type: ${schemaType}`); }; let output = getTs(); if (!isReferenceObject(schema)) { if (schema.nullable && output.value !== "null") { output = t.union([output, t.literal("null")]); } } return output; }; // src/box.ts var Box = class _Box { constructor(definition) { this.definition = definition; this.definition = definition; this.type = definition.type; this.value = definition.value; this.params = definition.params; this.schema = definition.schema; this.ctx = definition.ctx; } type; value; params; schema; ctx; toJSON() { return { type: this.type, value: this.value }; } toString() { return JSON.stringify(this.toJSON(), null, 2); } recompute(callback) { return openApiSchemaToTs({ schema: this.schema, ctx: { ...this.ctx, onBox: callback } }); } static fromJSON(json) { return new _Box(JSON.parse(json)); } static isBox(box) { return box instanceof _Box; } static isUnion(box) { return box.type === "union"; } static isIntersection(box) { return box.type === "intersection"; } static isArray(box) { return box.type === "array"; } static isOptional(box) { return box.type === "optional"; } static isReference(box) { return box.type === "ref"; } static isKeyword(box) { return box.type === "keyword"; } static isObject(box) { return box.type === "object"; } static isLiteral(box) { return box.type === "literal"; } }; // src/box-factory.ts var unwrap = (param) => typeof param === "string" ? param : param.value; var createFactory = (f) => f; var createBoxFactory = (schema, ctx) => { const f = typeof ctx.factory === "function" ? ctx.factory(schema, ctx) : ctx.factory; const callback = (box2) => { if (f.callback) { box2 = f.callback(box2); } if (ctx?.onBox) { box2 = ctx.onBox?.(box2); } return box2; }; const box = { union: (types) => callback(new Box({ ctx, schema, type: "union", params: { types }, value: f.union(types) })), intersection: (types) => callback(new Box({ ctx, schema, type: "intersection", params: { types }, value: f.intersection(types) })), array: (type2) => callback(new Box({ ctx, schema, type: "array", params: { type: type2 }, value: f.array(type2) })), optional: (type2) => callback(new Box({ ctx, schema, type: "optional", params: { type: type2 }, value: f.optional(type2) })), reference: (name, generics) => callback( new Box({ ctx, schema, type: "ref", params: generics ? { name, generics } : { name }, value: f.reference(name, generics) }) ), literal: (value) => callback(new Box({ ctx, schema, type: "literal", params: {}, value: f.literal(value) })), string: () => callback(new Box({ ctx, schema, type: "keyword", params: { name: "string" }, value: f.string() })), number: () => callback(new Box({ ctx, schema, type: "keyword", params: { name: "number" }, value: f.number() })), boolean: () => callback(new Box({ ctx, schema, type: "keyword", params: { name: "boolean" }, value: f.boolean() })), unknown: () => callback(new Box({ ctx, schema, type: "keyword", params: { name: "unknown" }, value: f.unknown() })), any: () => callback(new Box({ ctx, schema, type: "keyword", params: { name: "any" }, value: f.any() })), never: () => callback(new Box({ ctx, schema, type: "keyword", params: { name: "never" }, value: f.never() })), object: (props) => callback(new Box({ ctx, schema, type: "object", params: { props }, value: f.object(props) })) }; return box; }; // src/generator.ts import { capitalize as capitalize2, groupBy } from "pastable/server"; import * as Codegen from "@sinclair/typebox-codegen"; import { match } from "ts-pattern"; import { type } from "arktype"; var DEFAULT_SUCCESS_STATUS_CODES = [ 200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 300, 301, 302, 303, 304, 305, 306, 307, 308 ]; var DEFAULT_ERROR_STATUS_CODES = [ 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 421, 422, 423, 424, 425, 426, 428, 429, 431, 451, 500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511 ]; var allowedRuntimes = type("'none' | 'arktype' | 'io-ts' | 'typebox' | 'valibot' | 'yup' | 'zod'"); var runtimeValidationGenerator = { arktype: Codegen.ModelToArkType.Generate, "io-ts": Codegen.ModelToIoTs.Generate, typebox: Codegen.ModelToTypeBox.Generate, valibot: Codegen.ModelToValibot.Generate, yup: Codegen.ModelToYup.Generate, zod: Codegen.ModelToZod.Generate }; var inferByRuntime = { none: (input) => input, arktype: (input) => `${input}["infer"]`, "io-ts": (input) => `t.TypeOf<${input}>`, typebox: (input) => `Static<${input}>`, valibot: (input) => `v.InferOutput<${input}>`, yup: (input) => `y.InferType<${input}>`, zod: (input) => `z.infer<${input}>` }; var methods = ["get", "put", "post", "delete", "options", "head", "patch", "trace"]; var methodsRegex = new RegExp(`(?:${methods.join("|")})_`); var endpointExport = new RegExp(`export (?:type|const) (?:${methodsRegex.source})`); var replacerByRuntime = { yup: (line) => line.replace(/y\.InferType<\s*?typeof (.*?)\s*?>/g, "typeof $1").replace( new RegExp(`(${endpointExport.source})` + new RegExp(/([\s\S]*? )(y\.object)(\()/).source, "g"), "$1$2(" ), zod: (line) => line.replace(/z\.infer<\s*?typeof (.*?)\s*?>/g, "typeof $1").replace( new RegExp(`(${endpointExport.source})` + new RegExp(/([\s\S]*? )(z\.object)(\()/).source, "g"), "$1$2(" ) }; var generateFile = (options) => { const ctx = { ...options, runtime: options.runtime ?? "none", successStatusCodes: options.successStatusCodes ?? DEFAULT_SUCCESS_STATUS_CODES, errorStatusCodes: options.errorStatusCodes ?? DEFAULT_ERROR_STATUS_CODES, includeClient: options.includeClient ?? true }; const schemaList = generateSchemaList(ctx); const endpointSchemaList = options.schemasOnly ? "" : generateEndpointSchemaList(ctx); const endpointByMethod = options.schemasOnly ? "" : generateEndpointByMethod(ctx); const apiClient = options.schemasOnly || !ctx.includeClient ? "" : generateApiClient(ctx); const transform = ctx.runtime === "none" ? (file2) => file2 : (file2) => { const model = Codegen.TypeScriptToModel.Generate(file2); const transformer = runtimeValidationGenerator[ctx.runtime]; const generated = ctx.runtime === "typebox" ? Codegen.TypeScriptToTypeBox.Generate(file2) : transformer(model); let converted = ""; const match3 = generated.match(/(const __ENDPOINTS_START__ =)([\s\S]*?)(export type __ENDPOINTS_END__)/); const content = match3?.[2]; if (content && ctx.runtime in replacerByRuntime) { const before = generated.slice(0, generated.indexOf("export type __ENDPOINTS_START")); converted = before + replacerByRuntime[ctx.runtime]( content.slice(content.indexOf("export")) ); } else { converted = generated; } return converted; }; const file = ` ${transform(schemaList + endpointSchemaList)} ${endpointByMethod} ${apiClient} `; return file; }; var generateSchemaList = ({ refs, runtime }) => { let file = ` ${runtime === "none" ? "export namespace Schemas {" : ""} // <Schemas> `; refs.getOrderedSchemas().forEach(([schema, infos]) => { if (!infos?.name) return; if (infos.kind !== "schemas") return; file += `export type ${infos.normalized} = ${schema.value} `; }); return file + ` // </Schemas> ${runtime === "none" ? "}" : ""} `; }; var parameterObjectToString = (parameters, ctx) => { if (parameters instanceof Box) { if (ctx.runtime === "none") { return parameters.recompute((box) => { if (Box.isReference(box) && !box.params.generics && box.value !== "null") { box.value = `Schemas.${box.value}`; } return box; }).value; } return parameters.value; } let str = "{"; for (const [key, box] of Object.entries(parameters)) { str += `${wrapWithQuotesIfNeeded(key)}${box.type === "optional" ? "?" : ""}: ${box.value}, `; } return str + "}"; }; var responseHeadersObjectToString = (responseHeaders) => { let str = "{"; for (const [key, responseHeader] of Object.entries(responseHeaders)) { str += `${wrapWithQuotesIfNeeded(key.toLowerCase())}: ${responseHeader.value}, `; } return str + "}"; }; var generateResponsesObject = (responses, ctx) => { let str = "{"; for (const [statusCode, responseType] of Object.entries(responses)) { const value = ctx.runtime === "none" ? responseType.recompute((box) => { if (Box.isReference(box) && !box.params.generics && box.value !== "null") { box.value = `Schemas.${box.value}`; } return box; }).value : responseType.value; str += `${wrapWithQuotesIfNeeded(statusCode)}: ${value}, `; } return str + "}"; }; var generateEndpointSchemaList = (ctx) => { let file = ` ${ctx.runtime === "none" ? "export namespace Endpoints {" : ""} // <Endpoints> ${ctx.runtime === "none" ? "" : "type __ENDPOINTS_START__ = {}"} `; ctx.endpointList.map((endpoint) => { const parameters = endpoint.parameters ?? {}; file += `export type ${endpoint.meta.alias} = { method: "${endpoint.method.toUpperCase()}", path: "${endpoint.path}", requestFormat: "${endpoint.requestFormat}", ${endpoint.meta.hasParameters ? `parameters: { ${parameters.query ? `query: ${parameterObjectToString(parameters.query, ctx)},` : ""} ${parameters.path ? `path: ${parameterObjectToString(parameters.path, ctx)},` : ""} ${parameters.header ? `header: ${parameterObjectToString(parameters.header, ctx)},` : ""} ${parameters.body ? `body: ${parameterObjectToString( ctx.runtime === "none" ? parameters.body.recompute((box) => { if (Box.isReference(box) && !box.params.generics) { box.value = `Schemas.${box.value}`; } return box; }) : parameters.body, ctx )},` : ""} }` : "parameters: never,"} ${endpoint.responses ? `responses: ${generateResponsesObject(endpoint.responses, ctx)},` : ""} ${endpoint.responseHeaders ? `responseHeaders: ${responseHeadersObjectToString(endpoint.responseHeaders)},` : ""} } `; }); return file + ` // </Endpoints> ${ctx.runtime === "none" ? "}" : ""} ${ctx.runtime === "none" ? "" : "type __ENDPOINTS_END__ = {}"} `; }; var generateEndpointByMethod = (ctx) => { const { endpointList } = ctx; const byMethods = groupBy(endpointList, "method"); const endpointByMethod = ` // <EndpointByMethod> export ${ctx.runtime === "none" ? "type" : "const"} EndpointByMethod = { ${Object.entries(byMethods).map(([method, list]) => { return `${method}: { ${list.map( (endpoint) => `"${endpoint.path}": ${ctx.runtime === "none" ? "Endpoints." : ""}${endpoint.meta.alias}` ).join(",\n")} }`; }).join(",\n")} } ${ctx.runtime === "none" ? "" : "export type EndpointByMethod = typeof EndpointByMethod;"} // </EndpointByMethod> `; const shorthands = ` // <EndpointByMethod.Shorthands> ${Object.keys(byMethods).map((method) => `export type ${capitalize2(method)}Endpoints = EndpointByMethod["${method}"]`).join("\n")} // </EndpointByMethod.Shorthands> `; return endpointByMethod + shorthands; }; var generateApiClient = (ctx) => { if (!ctx.includeClient) { return ""; } const { endpointList } = ctx; const byMethods = groupBy(endpointList, "method"); const apiClientTypes = ` // <ApiClientTypes> export type EndpointParameters = { body?: unknown; query?: Record<string, unknown>; header?: Record<string, unknown>; path?: Record<string, unknown>; }; export type MutationMethod = "post" | "put" | "patch" | "delete"; export type Method = "get" | "head" | "options" | MutationMethod; type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text"; export type DefaultEndpoint = { parameters?: EndpointParameters | undefined; responses?: Record<string, unknown>; responseHeaders?: Record<string, unknown>; }; export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = { operationId: string; method: Method; path: string; requestFormat: RequestFormat; parameters?: TConfig["parameters"]; meta: { alias: string; hasParameters: boolean; areParametersRequired: boolean; }; responses?: TConfig["responses"]; responseHeaders?: TConfig["responseHeaders"] }; export interface Fetcher { decodePathParams?: (path: string, pathParams: Record<string, string>) => string encodeSearchParams?: (searchParams: Record<string, unknown> | undefined) => URLSearchParams // fetch: (input: { method: Method; url: URL; urlSearchParams?: URLSearchParams | undefined; parameters?: EndpointParameters | undefined; path: string; overrides?: RequestInit; throwOnStatusError?: boolean }) => Promise<Response>; parseResponseData?: (response: Response) => Promise<unknown> } export const successStatusCodes = [${ctx.successStatusCodes.join(",")}] as const; export type SuccessStatusCode = typeof successStatusCodes[number]; export const errorStatusCodes = [${ctx.errorStatusCodes.join(",")}] as const; export type ErrorStatusCode = typeof errorStatusCodes[number]; // Taken from https://github.com/unjs/fetchdts/blob/ec4eaeab5d287116171fc1efd61f4a1ad34e4609/src/fetch.ts#L3 export interface TypedHeaders<TypedHeaderValues extends Record<string, string> | unknown> extends Omit<Headers, 'append' | 'delete' | 'get' | 'getSetCookie' | 'has' | 'set' | 'forEach'> { /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/append) */ append: <Name extends Extract<keyof TypedHeaderValues, string> | string & {}> (name: Name, value: Lowercase<Name> extends keyof TypedHeaderValues ? TypedHeaderValues[Lowercase<Name>] : string) => void /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/delete) */ delete: <Name extends Extract<keyof TypedHeaderValues, string> | string & {}> (name: Name) => void /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/get) */ get: <Name extends Extract<keyof TypedHeaderValues, string> | string & {}> (name: Name) => (Lowercase<Name> extends keyof TypedHeaderValues ? TypedHeaderValues[Lowercase<Name>] : string) | null /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/getSetCookie) */ getSetCookie: () => string[] /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/has) */ has: <Name extends Extract<keyof TypedHeaderValues, string> | string & {}> (name: Name) => boolean /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/set) */ set: <Name extends Extract<keyof TypedHeaderValues, string> | string & {}> (name: Name, value: Lowercase<Name> extends keyof TypedHeaderValues ? TypedHeaderValues[Lowercase<Name>] : string) => void forEach: (callbackfn: (value: TypedHeaderValues[keyof TypedHeaderValues] | string & {}, key: Extract<keyof TypedHeaderValues, string> | string & {}, parent: TypedHeaders<TypedHeaderValues>) => void, thisArg?: any) => void } /** @see https://developer.mozilla.org/en-US/docs/Web/API/Response */ export interface TypedSuccessResponse<TSuccess, TStatusCode, THeaders> extends Omit<Response, "ok" | "status" | "json" | "headers"> { ok: true; status: TStatusCode; headers: never extends THeaders ? Headers : TypedHeaders<THeaders>; data: TSuccess; /** [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Response/json) */ json: () => Promise<TSuccess>; } /** @see https://developer.mozilla.org/en-US/docs/Web/API/Response */ export interface TypedErrorResponse<TData, TStatusCode, THeaders> extends Omit<Response, "ok" | "status" | "json" | "headers"> { ok: false; status: TStatusCode; headers: never extends THeaders ? Headers : TypedHeaders<THeaders>; data: TData; /** [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Response/json) */ json: () => Promise<TData>; } export type TypedApiResponse<TAllResponses extends Record<string | number, unknown> = {}, THeaders = {}> = ({ [K in keyof TAllResponses]: K extends string ? K extends \`\${infer TStatusCode extends number}\` ? TStatusCode extends SuccessStatusCode ? TypedSuccessResponse<TAllResponses[K], TStatusCode, K extends keyof THeaders ? THeaders[K] : never> : TypedErrorResponse<TAllResponses[K], TStatusCode, K extends keyof THeaders ? THeaders[K] : never> : never : K extends number ? K extends SuccessStatusCode ? TypedSuccessResponse<TAllResponses[K], K, K extends keyof THeaders ? THeaders[K] : never> : TypedErrorResponse<TAllResponses[K], K, K extends keyof THeaders ? THeaders[K] : never> : never; }[keyof TAllResponses]); export type SafeApiResponse<TEndpoint> = TEndpoint extends { responses: infer TResponses } ? TResponses extends Record<string, unknown> ? TypedApiResponse<TResponses, TEndpoint extends { responseHeaders: infer THeaders } ? THeaders : never> : never : never export type InferResponseByStatus<TEndpoint, TStatusCode> = Extract<SafeApiResponse<TEndpoint>, { status: TStatusCode }> type RequiredKeys<T> = { [P in keyof T]-?: undefined extends T[P] ? never : P; }[keyof T]; type MaybeOptionalArg<T> = RequiredKeys<T> extends never ? [config?: T] : [config: T]; type NotNever<T> = [T] extends [never] ? false : true; // </ApiClientTypes> `; const infer = inferByRuntime[ctx.runtime]; const InferTEndpoint = match(ctx.runtime).with("zod", "yup", () => infer(`TEndpoint`)).with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`)).otherwise(() => `TEndpoint`); const apiClient = ` // <TypedStatusError> export class TypedStatusError<TData = unknown> extends Error { response: TypedErrorResponse<TData, ErrorStatusCode, unknown>; status: number; constructor(response: TypedErrorResponse<TData, ErrorStatusCode, unknown>) { super(\`HTTP \${response.status}: \${response.statusText}\`); this.name = 'TypedStatusError'; this.response = response; this.status = response.status; } } // </TypedStatusError> // <ApiClient> export class ApiClient { baseUrl: string = ""; successStatusCodes = successStatusCodes; errorStatusCodes = errorStatusCodes; constructor(public fetcher: Fetcher) {} setBaseUrl(baseUrl: string) { this.baseUrl = baseUrl; return this; } /** * Replace path parameters in URL * Supports both OpenAPI format {param} and Express format :param */ defaultDecodePathParams = (url: string, params: Record<string, string>): string => { return url .replace(/{(\\w+)}/g, (_, key: string) => params[key] || \`{\${key}}\`) .replace(/:([a-zA-Z0-9_]+)/g, (_, key: string) => params[key] || \`:\${key}\`); } /** Uses URLSearchParams, skips null/undefined values */ defaultEncodeSearchParams = (queryParams: Record<string, unknown> | undefined): URLSearchParams | undefined => { if (!queryParams) return; const searchParams = new URLSearchParams(); Object.entries(queryParams).forEach(([key, value]) => { if (value != null) { // Skip null/undefined values if (Array.isArray(value)) { value.forEach((val) => val != null && searchParams.append(key, String(val))); } else { searchParams.append(key, String(value)); } } }); return searchParams; } defaultParseResponseData = async (response: Response): Promise<unknown> => { const contentType = response.headers.get("content-type") ?? ""; if (contentType.startsWith("text/")) { return (await response.text()) } if (contentType === "application/octet-stream") { return (await response.arrayBuffer()) } if ( contentType.includes("application/json") || (contentType.includes("application/") && contentType.includes("json")) || contentType === "*/*" ) { try { return await response.json(); } catch { return undefined } } return } ${Object.entries(byMethods).map(([method, endpointByMethod]) => { const capitalizedMethod = capitalize2(method); return endpointByMethod.length ? `// <ApiClient.${method}> ${method}<Path extends keyof ${capitalizedMethod}Endpoints, TEndpoint extends ${capitalizedMethod}Endpoints[Path]>( path: Path, ...params: MaybeOptionalArg< (TEndpoint extends { parameters: infer UParams } ? NotNever<UParams> extends true ? UParams & { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean }) > ): Promise<Extract<InferResponseByStatus<${InferTEndpoint}, SuccessStatusCode>, { data: {} }>["data"]>; ${method}<Path extends keyof ${capitalizedMethod}Endpoints, TEndpoint extends ${capitalizedMethod}Endpoints[Path]>( path: Path, ...params: MaybeOptionalArg< (TEndpoint extends { parameters: infer UParams } ? NotNever<UParams> extends true ? UParams & { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean }) > ): Promise<SafeApiResponse<TEndpoint>>; ${method}<Path extends keyof ${capitalizedMethod}Endpoints, _TEndpoint extends ${capitalizedMethod}Endpoints[Path]>( path: Path, ...params: MaybeOptionalArg<any> ): Promise<any> { return this.request("${method}", path, ...params); } // </ApiClient.${method}> ` : ""; }).join("\n")} // <ApiClient.request> /** * Generic request method with full type-safety for any endpoint */ request< TMethod extends keyof EndpointByMethod, TPath extends keyof EndpointByMethod[TMethod], TEndpoint extends EndpointByMethod[TMethod][TPath] >( method: TMethod, path: TPath, ...params: MaybeOptionalArg< (TEndpoint extends { parameters: infer UParams } ? NotNever<UParams> extends true ? UParams & { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean }) > ): Promise<Extract<InferResponseByStatus<${InferTEndpoint}, SuccessStatusCode>, { data: {} }>["data"]> request< TMethod extends keyof EndpointByMethod, TPath extends keyof EndpointByMethod[TMethod], TEndpoint extends EndpointByMethod[TMethod][TPath] >( method: TMethod, path: TPath, ...params: MaybeOptionalArg< (TEndpoint extends { parameters: infer UParams } ? NotNever<UParams> extends true ? UParams & { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean }) > ): Promise<SafeApiResponse<TEndpoint>>; request< TMethod extends keyof EndpointByMethod, TPath extends keyof EndpointByMethod[TMethod], TEndpoint extends EndpointByMethod[TMethod][TPath] >( method: TMethod, path: TPath, ...params: MaybeOptionalArg<any> ): Promise<any> { const requestParams = params[0]; const withResponse = requestParams?.withResponse; const { withResponse: _, throwOnStatusError = withResponse ? false : true, overrides, ...fetchParams } = requestParams || {}; const parametersToSend: EndpointParameters = {}; if (requestParams?.body !== undefined) (parametersToSend as any).body = requestParams.body; if (requestParams?.query !== undefined) (parametersToSend as any).query = requestParams.query; if (requestParams?.header !== undefined) (parametersToSend as any).header = requestParams.header; if (requestParams?.path !== undefined) (parametersToSend as any).path = requestParams.path; const resolvedPath = (this.fetcher.decodePathParams ?? this.defaultDecodePathParams)(this.baseUrl + (path as string), (parametersToSend.path ?? {}) as Record<string, string>); const url = new URL(resolvedPath); const urlSearchParams = (this.fetcher.encodeSearchParams ?? this.defaultEncodeSearchParams)(parametersToSend.query); const promise = this.fetcher.fetch({ method: method, path: (path as string), url, urlSearchParams, parameters: Object.keys(fetchParams).length ? fetchParams : undefined, overrides, throwOnStatusError }) .then(async (response) => { const data = await (this.fetcher.parseResponseData ?? this.defaultParseResponseData)(response); const typedResponse = Object.assign(response, { data: data, json: () => Promise.resolve(data) }) as SafeApiResponse<TEndpoint>; if (throwOnStatusError && errorStatusCodes.includes(response.status as never)) { throw new TypedStatusError(typedResponse as never); } return withResponse ? typedResponse : data; }); return promise as Extract<InferResponseByStatus<${InferTEndpoint}, SuccessStatusCode>, { data: {} }>["data"] } // </ApiClient.request> } export function createApiClient(fetcher: Fetcher, baseUrl?: string) { return new ApiClient(fetcher).setBaseUrl(baseUrl ?? ""); } /** Example usage: const api = createApiClient((method, url, params) => fetch(url, { method, body: JSON.stringify(params) }).then((res) => res.json()), ); api.get("/users").then((users) => console.log(users)); api.post("/users", { body: { name: "John" } }).then((user) => console.log(user)); api.put("/users/:id", { path: { id: 1 }, body: { name: "John" } }).then((user) => console.log(user)); // With error handling const result = await api.get("/users/{id}", { path: { id: "123" }, withResponse: true }); if (result.ok) { // Access data directly const user = result.data; console.log(user); // Or use the json() method for compatibility const userFromJson = await result.json(); console.log(userFromJson); } else { const error = result.data; console.error(\`Error \${result.status}:\`, error); } */ // </ApiClient> `; return apiClientTypes + apiClient; }; // src/ref-resolver.ts import { get } from "pastable/server"; // src/topological-sort.ts function topologicalSort(graph) { const sorted = [], visited = {}; function visit(name, ancestors) { if (!Array.isArray(ancestors)) ancestors = []; ancestors.push(name); visited[name] = true; const deps = graph.get(name); if (deps) { deps.forEach((dep) => { if (ancestors.includes(dep)) { return; } if (visited[dep]) return; visit(dep, ancestors.slice(0)); }); } if (!sorted.includes(name)) sorted.push(name); } graph.forEach((_, name) => visit(name, [])); return sorted; } // src/sanitize-name.ts var reservedWords = /* @__PURE__ */ new Set([ // TS keywords and built-ins "import", "package", "namespace", "Record", "Partial", "Required", "Readonly", "Pick", "Omit", "String", "Number", "Boolean", "Object", "Array", "Function", "any", "unknown", "never", "void", "extends", "super", "class", "interface", "type", "enum", "const", "let", "var", "if", "else", "for", "while", "do", "switch", "case", "default", "break", "continue", "return", "try", "catch", "finally", "throw", "new", "delete", "in", "instanceof", "typeof", "void", "with", "yield", "await", "static", "public", "private", "protected", "abstract", "as", "asserts", "from", "get", "set", "module", "require", "keyof", "readonly", "global", "symbol", "bigint" ]); function sanitizeName(name, type2) { let n = name.replace(/[\W/]+/g, "_"); if (/^\d/.test(n)) n = "_" + n; if (reservedWords.has(n)) n = (type2 === "schema" ? "Schema_" : "Endpoint_") + n; return n; } // src/ref-resolver.ts var autocorrectRef = (ref) => ref[1] === "/" ? ref : "#/" + ref.slice(1); var componentsWithSchemas = ["schemas", "responses", "parameters", "requestBodies", "headers"]; var createRefResolver = (doc, factory2, nameTransform) => { const nameByRef = /* @__PURE__ */ new Map(); const refByName = /* @__PURE__ */ new Map(); const byRef = /* @__PURE__ */ new Map(); const byNormalized = /* @__PURE__ */ new Map(); const boxByRef = /* @__PURE__ */ new Map(); const getSchemaByRef = (ref) => { const correctRef = autocorrectRef(ref); const split = correctRef.split("/"); const path = split.slice(1, -1).join("/"); const normalizedPath = path.replace("#/", "").replace("#", "").replaceAll("/", "."); const map = get(doc, normalizedPath) ?? {}; const name = split[split.length - 1]; let normalized = normalizeString(name); if (nameTransform?.transformSchemaName) { normalized = nameTransform.transformSchemaName(normalized); } normalized = sanitizeName(normalized, "schema"); nameByRef.set(correctRef, normalized); refByName.set(normalized, correctRef); const infos = { ref: correctRef, name, normalized, kind: normalizedPath.split(".")[1] }; byRef.set(infos.ref, infos); byNormalized.set(infos.normalized, infos); const schema = map[name]; if (!schema) { throw new Error(`Unresolved ref "${name}" not found in "${path}"`); } return schema; }; const getInfosByRef = (ref) => byRef.get(autocorrectRef(ref)); const schemaEntries = Object.entries(doc.components ?? {}).filter(([key]) => componentsWithSchemas.includes(key)); schemaEntries.forEach(([key, component]) => { Object.keys(component).map((name) => { const ref = `#/components/${key}/${name}`; getSchemaByRef(ref); }); }); const directDependencies = /* @__PURE__ */ new Map(); schemaEntries.forEach(([key, component]) => { Object.keys(component).map((name) => { const ref = `#/components/${key}/${name}`; const schema = getSchemaByRef(ref); boxByRef.set(ref, openApiSchemaToTs({ schema, ctx: { factory: factory2, refs: { getInfosByRef } } })); if (!directDependencies.has(ref)) { directDependencies.set(ref, /* @__PURE__ */ new Set()); } setSchemaDependencies(schema, directDependencies.get(ref)); }); }); const transitiveDependencies = getTransitiveDependencies(directDependencies); return { get: getSchemaByRef, unwrap: (component) => { return isReferenceObject(component) ? getSchemaByRef(component.$ref) : component; }, getInfosByRef, infos: byRef, /** * Get the schemas in the order they should be generated, depending on their dependencies * so that a schema is generated before the ones that depend on it */ getOrderedSchemas: () => { const schemaOrderedByDependencies = topologicalSort(transitiveDependencies).map((ref) => { const infos = getInfosByRef(ref); return [boxByRef.get(infos.ref), infos]; }); return schemaOrderedByDependencies; }, directDependencies, transitiveDependencies }; }; var setSchemaDependencies = (schema, deps) => { const visit = (schema2) => { if (!schema2) return; if (isReferenceObject(schema2)) { deps.add(schema2.$ref); return; } if (schema2.allOf) { for (const allOf of schema2.allOf) { visit(allOf); } return; } if (schema2.oneOf) { for (const oneOf of schema2.oneOf) { visit(oneOf); } return; } if (schema2.anyOf) { for (const anyOf of schema2.anyOf) { visit(anyOf); } return; } if (schema2.type === "array") { if (!schema2.items) return; return void visit(schema2.items); } if (schema2.type === "object" || schema2.properties || schema2.additionalProperties) { if (schema2.properties) { for (const property in schema2.properties) { visit(schema2.properties[property]); } } if (schema2.additionalProperties && typeof schema2.additionalProperties === "object") { visit(schema2.additionalProperties); } } }; visit(schema); }; var getTransitiveDependencies = (directDependencies) => { const transitiveDependencies = /* @__PURE__ */ new Map(); const visitedsDeepRefs = /* @__PURE__ */ new Set(); directDependencies.forEach((deps, ref) => { if (!transitiveDependencies.has(ref)) { transitiveDependencies.set(ref, /* @__PURE__ */ new Set()); } const visit = (depRef) => { transitiveDependencies.get(ref).add(depRef); const deps2 = directDependencies.get(depRef); if (deps2 && ref !== depRef) { deps2.forEach((transitive) => { const key = ref + "__" + transitive; if (visitedsDeepRefs.has(key)) return; visitedsDeepRefs.add(key); visit(transitive); }); } }; deps.forEach((dep) => visit(dep)); }); return transitiveDependencies; }; // src/ts-factory.ts var tsFactory = createFactory({ union: (types) => `(${types.map(unwrap).join(" | ")})`, intersection: (types) => `(${types.map(unwrap).join(" & ")})`, array: (type2) => `Array<${unwrap(type2)}>`, optional: (type2) => `${unwrap(type2)} | undefined`, reference: (name, typeArgs) => `${name}${typeArgs ? `<${typeArgs.map(unwrap).join(", ")}>` : ""}`, literal: (value) => value.toString(), string: () => "string", number: () => "number", boolean: () => "boolean", unknown: () => "unknown", any: () => "any", never: () => "never", object: (props) => { const propsString = Object.entries(props).map( ([prop, type2]) => `${wrapWithQuotesIfNeeded(prop)}${typeof type2 !== "string" && Box.isOptional(type2) ? "?" : ""}: ${unwrap( type2 )}` ).join(", "); return `{ ${propsString} }`; } }); // src/map-openapi-endpoints.ts import { capitalize as capitalize3, pick } from "pastable/server"; import { match as match2, P } from "ts-pattern"; var factory = tsFactory; var mapOpenApiEndpoints = (doc, options) => { const refs = createRefResolver(doc, factory); const ctx = { refs, factory }; const endpointList = []; Object.entries(doc.paths ?? {}).forEach(([path, pathItemObj]) => { const pathItem = pick(pathItemObj, ["get", "put", "post", "delete", "options", "head", "patch", "trace"]); Object.entries(pathItem).forEach(([method, operation]) => { if (operation.deprecated) return; let alias = getAlias({ path, method, operation }); if (options?.nameTransform?.transformEndpointName) { alias = options.nameTransform.transformEndpointName({ alias, path, method, operation }); } const endpoint = { operation, method, path, requestFormat: "json", meta: { alias, areParametersRequired: false, hasParameters: false } }; const lists = { query: [], path: [], header: [] }; const paramObjects = [...pathItemObj.parameters ?? [], ...operation.parameters ?? []].reduce( (acc, paramOrRef) => { const param = refs.unwrap(paramOrRef); const schema = openApiSchemaToTs({ schema: refs.unwrap(param.schema ?? {}), ctx }); if (param.required) endpoint.meta.areParametersRequired = true; endpoint.meta.hasParameters = true; if (param.in === "query") { lists.query.push(param); acc.query[param.name] = schema; } if (param.in === "path") { lists.path.push(param); acc.path[param.name] = schema; } if (param.in === "header") { lists.header.push(param); acc.header[param.name] = schema; } return acc; }, { query: {}, path: {}, header: {} } ); const params = Object.entries(paramObjects).reduce( (acc, [key, value]) => { if (Object.keys(value).length) { acc[key] = value; } return acc; }, {} ); if (operation.requestBody) { endpoint.meta.hasParameters = true; const requestBody = refs.unwrap(operation.requestBody ?? {}); const content = requestBody.content; const matchingMediaType = Object.keys(content).find(isAllowedParamMediaTypes); if (matchingMediaType && content[matchingMediaType]) { params.body = openApiSchemaToTs({ schema: content[matchingMediaType]?.schema ?? {}, ctx }); } endpoint.requestFormat = match2(matchingMediaType).with("application/octet-stream", () => "binary").with("multipart/form-data", () => "form-data").with("application/x-www-form-urlencoded", () => "form-url").with(P.string.includes("json"), () => "json").otherwise(() => "text"); } if (params) { const filtered_params = ["query", "path", "header"]; for (const k of filtered_params) { if (params[k] && lists[k].length) { const properties = Object.entries(params[k]).reduce( (acc, [key, value]) => { if (value.schema) acc[key] = value.schema; return acc; }, {} ); const t = createBoxFactory({ type: "object", properties }, ctx); if (lists[k].every((param) => !param.required)) { params[k] = t.reference("Partial", [t.object(params[k])]); } else { for (const p of lists[k]) { if (!p.required) { params[k][p.name] = t.optional(params[k][p.name]); } } } } } endpoint.parameters = Object.keys(params).length ? params : void 0; } const allResponses = {}; const allResponseHeaders = {}; Object.entries(operation.responses ?? {}).map(([status, responseOrRef]) => { const responseObj = refs.unwrap(responseOrRef); const content = responseObj?.content; const mediaTypes = Object.keys(content ?? {}).filter(isResponseMediaType); if (content && mediaTypes.length) { mediaTypes.forEach((mediaType) => { const schema = content[mediaType] ? content[mediaType].schema ?? {} : {}; const mediaTypeResponse = openApiSchemaToTs({ schema, ctx }); if (allResponses[status]) { const t2 = createBoxFactory( { oneOf: [ ...allResponses[status].schema.oneOf ? allResponses[status].schema.oneOf : [allResponses[status].schema], schema ] }, ctx ); allResponses[status] = t2.union([ ...allResponses[status].type === "union" ? allResponses[status].params.types : [allResponses[status]], mediaTypeResponse ]); } else { allResponses[status] = mediaTypeResponse; } }); } else { const schema = {}; const unknown = openApiSchemaToTs({ schema: {}, ctx }); if (allResponses[status]) { const t2 = createBoxFactory( { oneOf: [ ...allResponses[status].schema.oneOf ? allResponses[status].schema.oneOf : [allResponses[status].schema], schema ] }, ctx ); allResponses[status] = t2.union([ ...allResponses[status].type === "union" ? allResponses[status].params.types : [allResponses[status]], unknown ]); } else { allResponses[status] = unknown; } } const headers = responseObj?.headers; const t = createBoxFactory( { type: "object", properties: headers ?? {}, required: Object.keys(headers ?? {}) }, ctx ); if (headers) { const mappedHeaders = Object.entries(headers).reduce( (acc, [name, headerOrRef]) => { const header = refs.unwrap(headerOrRef); const box = openApiSchemaToTs({ schema: header.schema ?? {}, ctx }); acc[name] = box; return acc; }, {} ); if (Object.keys(mappedHeaders).length) { allResponseHeaders[status] = t.object(mappedHeaders); } } }); if (Object.keys(allResponses).length > 0) { endpoint.responses = allResponses; } if (Object.keys(allResponseHeaders).length) { endpoint.responseHeaders = allResponseHeaders; } endpointList.push(endpoint); }); }); return { doc, refs, endpointList, factory }; }; var allowedParamMediaTypes = [ "application/octet-stream", "multipart/form-data", "application/x-www-form-urlencoded", "*/*" ]; var isAllowedParamMediaTypes = (mediaType) => mediaType.includes("application/") && mediaType.includes("json") || allowedParamMediaTypes.includes(mediaType) || mediaType.includes("text/"); var isResponseMediaType = (mediaType) => mediaType === "*/*" || mediaType.includes("application/") && mediaType.includes("json"); var getAlias = ({ path, method, operation }) => sanitizeName( (method + "_" + capitalize3(operation.operationId ?? pathToVariableName(path))).replace(/-/g, "__"), "endpoint" ); // src/tanstack-query.generator.ts import { capitalize as capitalize4 } from "pastable/server"; var generateTanstackQueryFile = async (ctx) => { const endpointMethods = new Set(ctx.endpointList.map((endpoint) => endpoint.method.toLowerCase())); const file = ` im