UNPKG

@moznion/openapi-fetch-gen

Version:

Generate TypeScript API client from OpenAPI TypeScript interface definitions created by openapi-typescript

285 lines (284 loc) 11.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.generateClient = generateClient; const node_child_process_1 = require("node:child_process"); const node_fs_1 = __importDefault(require("node:fs")); const node_path_1 = __importDefault(require("node:path")); const tmp_1 = __importDefault(require("tmp")); const ts_morph_1 = require("ts-morph"); function generateClient(schemaFilePath, options = {}) { const project = new ts_morph_1.Project({ compilerOptions: { target: ts_morph_1.ScriptTarget.Latest, module: ts_morph_1.ModuleKind.ESNext, }, }); const schemaFile = project.addSourceFileAtPath(schemaFilePath); const pathsInterface = findInterface(schemaFile, "paths"); if (!pathsInterface) { throw new Error(`Interface "paths" not found in ${schemaFilePath}`); } const componentsInterface = findInterface(schemaFile, "components"); return generateClientCode(pathsInterface, componentsInterface, node_path_1.default.basename(schemaFilePath), options); } function findInterface(sourceFile, interfaceName) { return sourceFile.getInterfaces().find((i) => i.getName() === interfaceName); } function generateClientCode(pathsInterface, componentsInterface, schemaFileName, options = {}) { const endpoints = extractEndpointsInfo(pathsInterface); const clientClass = generateClientClass(endpoints, options); const code = [ "// THIS FILE IS AUTO-GENERATED BY openapi-fetch-gen.", "// DO NOT EDIT THIS FILE MANUALLY.", "// See Also: https://github.com/moznion/openapi-fetch-gen", `import createClient, { type ClientOptions } from "openapi-fetch";`, `import type { components, paths } from "./${schemaFileName}"; // generated by openapi-typescript`, "", clientClass, "", ...generateSchemaTypes(componentsInterface), ].join("\n"); const tempFile = tmp_1.default.fileSync({ postfix: ".ts" }).name; node_fs_1.default.writeFileSync(tempFile, code); (0, node_child_process_1.execSync)(`npx biome check --write ${tempFile}`); return node_fs_1.default.readFileSync(tempFile, "utf-8"); } function generateSchemaTypes(componentsInterface) { const schemasProperty = componentsInterface?.getProperty("schemas"); const types = []; if (schemasProperty) { const schemasType = schemasProperty.getType(); for (const schema of schemasType.getProperties()) { const schemaType = schema.getValueDeclaration()?.getType(); if (schemaType) { const schemaName = schema.getName(); const schemaTypeName = (schemaName + (schemaName === "Client" ? "Schema" : "")).replaceAll(/-/g, "_"); types.push(`export type ${schemaTypeName} = components["schemas"]["${schema.getName()}"];`); } } } return types; } function extractEndpointsInfo(pathsInterface) { const endpoints = []; for (const property of pathsInterface.getProperties()) { const path = property.getName().replace(/['"]/g, ""); const propertyType = property.getType(); const httpMethods = [ "get", "post", "put", "delete", "patch", "head", "options", ]; for (const httpMethod of httpMethods) { const methodProperty = propertyType.getProperty(httpMethod); if (!methodProperty) { continue; } if (methodProperty.getTypeAtLocation(property).getText() === "never") { continue; } const commentLines = []; let paramsType = null; let requestBodyType = null; const declarations = methodProperty.getDeclarations(); if (declarations.length <= 0) { continue; } const decl = declarations[0]; if (!ts_morph_1.Node.isPropertySignature(decl)) { continue; } const typeNode = decl.getTypeNodeOrThrow(); const operationId = (() => { if (ts_morph_1.Node.isIndexedAccessTypeNode(typeNode) && typeNode.getObjectTypeNode().getText() === "operations") { const indexTypeNode = typeNode.getIndexTypeNode(); if (ts_morph_1.Node.isLiteralTypeNode(indexTypeNode) && indexTypeNode.getLiteral().getKind() === ts_morph_1.SyntaxKind.StringLiteral) { return sanitizeForOperation(indexTypeNode.getLiteral().getText().slice(1, -1)); } } return null; })(); const paramProp = decl.getType().getPropertyOrThrow("parameters"); const paramTypes = paramProp .getTypeAtLocation(decl) .getProperties() .map((prop) => { const propType = prop.getTypeAtLocation(decl); const text = propType.getText(); const name = prop.getName(); if (text === "never") { return { name, text: "" }; } return { name, text }; }) .filter((kv) => kv["text"] !== ""); const headerType = paramTypes.find((kv) => kv.name === "header"); if (headerType) { const nonHeaderParams = paramTypes .filter((kv) => kv.name !== "header") .map((kv) => `${kv.name}: ${kv.text}`) .join("\n"); paramsType = `[ Exclude< // Missed Header Keys for default headers keyof ${headerType["text"]}, Extract< // Provided header keys by default headers' keys keyof HT, keyof ${headerType["text"]} > >, ] extends [never] ? ` + `{ header?: ${headerType["text"]}, ${nonHeaderParams} } : ` + `{ header: | (Pick< // Pick the header keys that are not in the default headers ${headerType["text"]}, Exclude< // Missed Header Keys for default headers keyof ${headerType["text"]}, Extract< // Provided header keys by default headers' keys keyof HT, keyof ${headerType["text"]} > > > & Partial< // Disallow default headers' keys to be in the header param Record< Extract< // Provided header keys by default headers' keys keyof HT, keyof ${headerType["text"]} >, never > >) | ${headerType["text"]}, ${nonHeaderParams} } `; } else { const params = paramTypes .map((kv) => `${kv.name}: ${kv.text}`) .join("\n"); if (params !== "") { paramsType = `{${params}}`; } } const requestBodyProp = decl.getType().getProperty("requestBody"); if (requestBodyProp) { const t = requestBodyProp.getTypeAtLocation(decl); if (t.getText() !== "never") { const contentProp = t.getPropertyOrThrow("content"); const contentType = contentProp.getTypeAtLocation(decl); const contentTypeProps = contentType.getProperties(); if (contentTypeProps.length > 0 && contentTypeProps[0]) { requestBodyType = contentTypeProps[0] .getTypeAtLocation(decl) .getText(); } } } if ("getJsDocs" in decl && typeof decl.getJsDocs === "function") { const jsDocs = decl.getJsDocs(); for (const d of jsDocs) { const description = d.getDescription().trim(); if (description) { commentLines.push(`* ${description}`); } } } const sanitizedPath = sanitizeForOperation(path); const pathSegments = sanitizedPath.split("_"); const camelCasePath = pathSegments .map((segment, index) => { if (index === 0) return segment.toLowerCase(); return (segment.charAt(0).toUpperCase() + segment.slice(1).toLowerCase()); }) .join(""); const defaultOperationName = `${httpMethod}${camelCasePath.charAt(0).toUpperCase()}${camelCasePath.slice(1)}`; endpoints.push({ path, httpMethod, operationName: defaultOperationName, operationId, commentLines, paramsType, bodyType: requestBodyType, headerType: headerType ? headerType["text"] : null, }); } } return endpoints; } function generateClientClass(endpoints, options = {}) { const classCode = [ `export class Client<HT extends Record<string, string>> { private readonly client; private readonly defaultHeaders: HT; constructor(clientOptions: ClientOptions, defaultHeaders?: HT) { this.client = createClient<paths>(clientOptions); this.defaultHeaders = defaultHeaders ?? ({} as HT); } `, ]; for (const endpoint of endpoints) { const { path, httpMethod, operationName, operationId, commentLines, paramsType, bodyType, } = endpoint; const methodName = options.useOperationId && operationId ? operationId : operationName; if (commentLines.length > 0) { classCode.push(" /**"); for (const line of commentLines) { classCode.push(` ${line}`); } classCode.push(" */"); } classCode.push(` async ${methodName}(`); const paramsList = []; if (paramsType) { paramsList.push(`params: ${paramsType}`); } if (bodyType) { paramsList.push(`body: ${bodyType}`); } if (paramsList.length > 0) { classCode.push(paramsList.join(",\n")); } classCode.push(" ) {"); classCode.push(` return await this.client.${httpMethod.toUpperCase()}("${path}", {`); if (paramsType) { if (endpoint.headerType) { classCode.push(`params: { ...params, header: {...this.defaultHeaders, ...params.header} as ${endpoint.headerType}, },`); } else { classCode.push("params,"); } } if (bodyType) { classCode.push(" body,"); } classCode.push(" });"); classCode.push(" }"); classCode.push(""); } classCode.push("}"); classCode.push(""); return classCode.join("\n"); } function sanitizeForOperation(operation) { return operation .replace(/[{}]/g, "") .replace(/[-/.]/g, "_") .replace(/^_/, ""); } //# sourceMappingURL=index.js.map