UNPKG

@moccona/apicodegen

Version:

A powerful OpenAPI code generator that automatically generates TypeScript API client code from OpenAPI specifications.

1,171 lines 77 kB
import { Agent, request } from "undici"; import { NodeFlags, SyntaxKind, addSyntheticLeadingComment, createPrinter, factory } from "typescript"; import { writeFile } from "node:fs/promises"; import { format } from "prettier"; import path from "node:path"; import fs from "fs-extra"; import { createScopedLogger } from "@moccona/logger"; //#region src/core/base/Adaptor.ts /** * Base adapter for tool * This abstract class serves as the foundation for implementing adapters for different code generation tools */ var Adapter = class {}; //#endregion //#region src/core/constants/keywords.ts const typescriptKeywords = new Set([ "break", "case", "catch", "class", "const", "continue", "debugger", "default", "delete", "do", "else", "enum", "export", "extends", "false", "finally", "for", "function", "if", "import", "in", "instanceof", "new", "null", "return", "super", "switch", "this", "throw", "true", "try", "typeof", "var", "void", "while", "with", "as", "implements", "interface", "let", "package", "private", "protected", "public", "static", "yield", "abstract", "any", "async", "await", "constructor", "declare", "from", "get", "is", "module", "namespace", "never", "require", "set", "type", "unknown", "readonly", "of", "asserts", "infer", "keyof", "boolean", "number", "string", "symbol", "object", "undefined", "bigint" ]); //#endregion //#region src/core/interface.ts let SchemaType = /* @__PURE__ */ function(SchemaType) { SchemaType["schemas"] = "schemas"; SchemaType["parameters"] = "parameters"; SchemaType["responses"] = "responses"; SchemaType["requestBodies"] = "requestBodies"; return SchemaType; }({}); let NonArraySchemaType = /* @__PURE__ */ function(NonArraySchemaType) { NonArraySchemaType["object"] = "object"; NonArraySchemaType["string"] = "string"; NonArraySchemaType["number"] = "number"; NonArraySchemaType["boolean"] = "boolean"; NonArraySchemaType["integer"] = "integer"; NonArraySchemaType["enum"] = "enum"; NonArraySchemaType["file"] = "file"; return NonArraySchemaType; }({}); let ArraySchemaType = /* @__PURE__ */ function(ArraySchemaType) { ArraySchemaType["array"] = "array"; return ArraySchemaType; }({}); let SchemaFormatType = /* @__PURE__ */ function(SchemaFormatType) { SchemaFormatType["string"] = "string"; SchemaFormatType["number"] = "number"; SchemaFormatType["boolean"] = "boolean"; SchemaFormatType["file"] = "file"; SchemaFormatType["binary"] = "binary"; SchemaFormatType["blob"] = "blob"; return SchemaFormatType; }({}); let ParameterIn = /* @__PURE__ */ function(ParameterIn) { ParameterIn["header"] = "header"; ParameterIn["body"] = "body"; ParameterIn["query"] = "query"; ParameterIn["cookie"] = "cookie"; ParameterIn["path"] = "path"; ParameterIn["formData"] = "formData"; return ParameterIn; }({}); let MediaTypes = /* @__PURE__ */ function(MediaTypes) { MediaTypes["JSON"] = "application/json"; MediaTypes["EVENT_STREAM"] = "text/event-stream"; MediaTypes["TEXT"] = "text"; MediaTypes["IMAGE"] = "image"; MediaTypes["AUDIO"] = "audio"; MediaTypes["VIDEO"] = "video"; return MediaTypes; }({}); let HttpMethods = /* @__PURE__ */ function(HttpMethods) { HttpMethods["GET"] = "get"; HttpMethods["PUT"] = "put"; HttpMethods["POST"] = "post"; HttpMethods["DELETE"] = "delete"; HttpMethods["OPTIONS"] = "options"; HttpMethods["HEAD"] = "head"; HttpMethods["PATCH"] = "patch"; HttpMethods["TRACE"] = "trace"; return HttpMethods; }({}); let Adaptors = /* @__PURE__ */ function(Adaptors) { Adaptors["fetch"] = "fetch"; Adaptors["axios"] = "axios"; return Adaptors; }({}); //#endregion //#region src/core/base/Base.ts /** * @file Base class implementation * @author wp.l * @description Base utility class providing common methods for code generation and API handling */ /** * Represents success HTTP status codes. * Each key is a string representation of a success HTTP status code. */ const SuccessHttpStatusCode = { "200": "200", "201": "201", "202": "202", "203": "203", "204": "204", "205": "205", "206": "206", "207": "207", "208": "208", "226": "226" }; /** * Base abstract class providing common utility methods. */ var Base = class Base { constructor() { if (new.target === Base) throw new Error("Cannot instantiate abstract class"); } /** * Converts a reference string to a meaningful name. * @param ref - The reference string to process. * @param [doc] - Optional document reference for context. * @returns - The processed name. */ static ref2name(ref, doc) { const paths = ref.replace(/^#/, "").split("/").filter(Boolean); if (!doc) return paths.slice(-1)[0]; let temporary = doc; let lastPath = ""; for (const path of paths) { const adjustedPath = path.replaceAll("~1", "/"); temporary = temporary[adjustedPath]; lastPath = adjustedPath; } if (!temporary) return "unknown"; return temporary.$ref ? Base.ref2name(temporary.$ref, doc) : lastPath; } /** * Converts an API path to a function name. * @param path - The API endpoint path. * @param [method] - The HTTP method (e.g., GET, POST). * @param [operationId] - Unique identifier for the operation. * @returns - The generated function name. */ static pathToFnName(path, method, _operationId = "") { return Base.normalize(Base.camelCase(Base.normalize(path))) + (method ? Base.capitalize(Base.upperCamelCase(`using_${method}`)) : ""); } /** * Normalizes a string by replacing special characters and avoiding TypeScript keywords. * @param text - Input text to normalize. * @returns - The normalized string. */ static normalize(text) { if (typescriptKeywords.has(text)) text += "_"; return text.replace(/[/\-_{}():\s`,*<>$#.]/gm, "_").replace(/^\d./gm, "").replaceAll("...", ""); } /** * Capitalizes the first character of a string. * @param text - Input string. * @returns - Capitalized string. */ static capitalize(text) { text = text.trim(); return `${text.charAt(0).toUpperCase()}${text.slice(1)}`; } /** * Converts a string to camelCase. * @param text - Input string. * @returns - CamelCase string. */ static camelCase(text) { text = text.trim(); const parts = text.split("_").filter(Boolean); while (parts[0]?.match(/^\d/)) parts.shift(); return parts.map((t, index) => index === 0 ? t : Base.capitalize(t)).join(""); } /** * Converts a string to UpperCamelCase. * @param text - Input string. * @returns - UpperCamelCase string. */ static upperCamelCase(text) { return Base.normalize(text).replaceAll("...", "").split("_").filter(Boolean).map(Base.capitalize).join(""); } /** * Fetches documentation from a given URL. * @param url - The URL to fetch the documentation from. * @param requestInit - Additional request parameters. * @returns - A promise resolving to the fetched documentation data. */ static async fetchDoc(url, requestInit = {}) { const { body, statusCode } = await request(url, { method: "GET", dispatcher: new Agent({ connect: { rejectUnauthorized: false } }), ...requestInit }); if (statusCode >= 400) throw new Error(`Failed to fetch OpenAPI documentation from ${url}: HTTP ${statusCode}`); try { return body.json(); } catch (error) { throw new Error(`Failed to parse JSON response from ${url}: ${error instanceof Error ? error.message : String(error)}`); } } /** * Determines the media type from a given media type string. * @param mediaType - The media type string to evaluate. * @returns - The matched MediaTypes or null. */ static getMediaType(mediaType) { return Object.values(MediaTypes).find((type) => mediaType.includes(type)); } /** * Checks if a schema is a valid enum type that isn't boolean. * @param a - The schema object to evaluate. * @returns - True if the schema is a valid non-boolean enum. */ static isValidEnumType(a) { return a.type !== "boolean" && !Base.isBooleanEnum(a); } /** * Checks if a schema represents a boolean enum. * @param a - The schema object to evaluate. * @returns - True if the schema is a boolean enum. */ static isBooleanEnum(a) { return a.type === "boolean" || !!a.enum?.some((member) => typeof member === "boolean"); } /** * Checks if two enum schemas are identical. * @param a - First enum schema to compare. * @param b - Second enum schema to compare. * @returns - True if the enums are identical. */ static isSameEnum(a, b) { return a.enum.length === b.enum.length && a.enum.sort().every((v, index) => v === b.enum.sort()[index]); } /** * Filters out duplicate enum schemas from an array. * @param enums - Array of enum schemas to process. * @returns - Array of unique enum schemas. */ static uniqueEnums(enums) { const enumMap = /* @__PURE__ */ new Map(); for (const e of enums) { const existing = enumMap.get(e.name); if (existing) for (const value of e.enum) existing.add(value); else enumMap.set(e.name, new Set(e.enum)); } return Array.from(enumMap.entries()).map(([name, values]) => ({ name, enum: Array.from(values) })); } /** * Finds the first occurrence of a matching enum schema in an array. * @param a - The enum schema to find. * @param enums - Array of enum schemas to search. * @returns - The found schema or undefined. */ static findSameSchema(a, enums) { return enums.find((b) => Base.isSameEnum(b, a)); } /** * Checks if an object is a reference object. * @param schema - The object to check. * @returns - True if the object is a reference. */ static isRef(schema) { return typeof schema === "object" && schema !== null && "$ref" in schema && typeof schema.$ref === "string"; } }; //#endregion //#region src/core/base/Provider.ts /** * Abstract Provider Class. * * The Provider class is designed to be extended by specific implementations (e.g., OpenAPI 2 provider, OpenAPI 3 provider). * It handles the initialization of the provider and the parsing of documentation into structured data. * * @example * * ```ts * /// Example of how this class might be used by a subclass: * class OpenAPIProvider extends Provider { * /// Implement the parse method to handle OpenAPI-specific documentation parsing. * parse(doc: unknown): ProviderInitResult { * /// Implementation details... * } * } * * /// Initializing a provider with configuration and documentation data: * const initOptions: ProviderInitOptions = { * docURL: "https://example.com/api/swagger.json", * baseURL: "https://api.example.com", * output: "./generated", * requestOptions: { * headers: { "Content-Type": "application/json" }, * }, * importClientSource: "generated/client", * }; * * const docData = fetchSwaggerDoc(); * const provider = new OpenAPIProvider(initOptions, docData); * ``` */ var Provider = class { /** collection of enum schemas */ enums = []; /** collection of schemas indexed by name */ schemas = {}; /** collection of parameters indexed by name */ parameters = {}; /** collection of API responses indexed by name */ responses = {}; /** collection of request bodies indexed by name */ requestBodies = {}; /** collection of API endpoints (operations) indexed by path */ apis = {}; /** URL for fetching API documentation */ docURL; /** base URL for API endpoints */ baseURL; /** output directory for generated code */ output; /** request options for API documentation fetch */ requestOptions; /** source path for imported client */ importClientSource; /** * Provider Constructor. * @param {ProviderInitOptions} initOptions - Initial configuration for the provider. * @param {unknown} doc - Raw API documentation data to be parsed. */ constructor(initOptions, doc) { this.docURL = initOptions.docURL; this.baseURL = initOptions.baseURL ?? ""; this.output = initOptions.output ?? "."; this.requestOptions = initOptions.requestOptions ?? {}; this.importClientSource = initOptions.importClientSource ?? ""; const { enums, schemas, requestBodies, responses, parameters, apis } = this.parse(doc); this.enums = enums; this.schemas = schemas; this.responses = responses; this.parameters = parameters; this.requestBodies = requestBodies; this.apis = apis; } }; //#endregion //#region src/core/errors.ts /** * Error handling utilities for api-codegen */ const ErrorCodes = { SPEC_NOT_FOUND: "E_SPEC_NOT_FOUND", SPEC_FETCH_FAILED: "E_SPEC_FETCH_FAILED", SPEC_PARSE_FAILED: "E_SPEC_PARSE_FAILED", OUTPUT_DIR_MISSING: "E_OUTPUT_DIR_MISSING", CONFIG_INVALID: "E_CONFIG_INVALID", VALIDATION_FAILED: "E_VALIDATION_FAILED", GENERATION_FAILED: "E_GENERATION_FAILED", TYPE_CHECK_FAILED: "E_TYPE_CHECK_FAILED" }; /** * Custom error class for api-codegen with rich context */ var ApicodegenError = class ApicodegenError extends Error { code; location; line; column; path; suggestions; cause; constructor(context) { super(context.message); this.name = "ApicodegenError"; this.code = context.code; this.location = context.location; this.line = context.line; this.column = context.column; this.path = context.path; this.suggestions = context.suggestions || []; this.cause = context.cause; if (Error.captureStackTrace) Error.captureStackTrace(this, ApicodegenError); } /** * Convert error to formatted string for CLI output */ toString(verbose = false) { const lines = []; lines.push(`\x1b[1;31mError [${this.code}]\x1b[0m ${this.message}`); if (this.location) lines.push(` \x1b[36m→ Location:\x1b[0m ${this.location}`); if (this.path) lines.push(` \x1b[36m→ Path:\x1b[0m ${this.path}`); if (this.line !== void 0) { let lineInfo = ` \x1b[36m→ Line:\x1b[0m ${this.line}`; if (this.column !== void 0) lineInfo += `, Column: ${this.column}`; lines.push(lineInfo); } if (this.suggestions.length > 0) for (const suggestion of this.suggestions) lines.push(` \x1b[32m→ Suggestion:\x1b[0m ${suggestion}`); if (verbose && this.cause) { lines.push(`\n \x1b[90mOriginal Error:\x1b[0m ${this.cause.message}`); if (this.stack) { const stackLines = this.stack.split("\n").slice(1).join("\n"); lines.push(`\x1b[90m${stackLines}\x1b[0m`); } } return lines.join("\n"); } /** * Convert to JSON-serializable object */ toJSON() { return { name: this.name, code: this.code, message: this.message, location: this.location, line: this.line, column: this.column, path: this.path, suggestions: this.suggestions, cause: this.cause?.message }; } }; /** * ANSI color codes for terminal output */ const Colors = { reset: "\x1B[0m", bold: "\x1B[1m", red: "\x1B[31m", green: "\x1B[32m", yellow: "\x1B[33m", blue: "\x1B[34m", cyan: "\x1B[36m", gray: "\x1B[90m", brightRed: "\x1B[91m", brightGreen: "\x1B[92m" }; /** * Format error for CLI output */ function formatError(error, verbose = false) { if (error instanceof ApicodegenError) return error.toString(verbose); if (error instanceof Error) return `${Colors.red}${Colors.bold}Error${Colors.reset}: ${error.message}${verbose && error.stack ? `\n\n${Colors.gray}${error.stack}${Colors.reset}` : ""}`; return `${Colors.red}${Colors.bold}Error${Colors.reset}: ${String(error)}`; } /** * Print error to console with formatting */ function printError(error, verbose = false, stream = process.stderr) { stream.write(formatError(error, verbose)); stream.write("\n"); } /** * Create error with common patterns */ const createErrors = { specNotFound(path, cause) { return new ApicodegenError({ code: ErrorCodes.SPEC_NOT_FOUND, message: "OpenAPI spec file not found", location: path, suggestions: [ "Check if the file exists using 'ls -la'", "Use --spec to provide the correct path", "For remote specs, ensure the URL is accessible" ], cause }); }, specFetchFailed(url, statusCode, cause) { const message = statusCode ? `Failed to fetch OpenAPI spec (HTTP ${statusCode})` : "Failed to fetch OpenAPI spec from URL"; return new ApicodegenError({ code: ErrorCodes.SPEC_FETCH_FAILED, message, location: url, suggestions: [ "Check if the URL is accessible in a browser", "Download the spec file locally and use the local path", "Verify CORS settings if fetching from a different origin" ], cause }); }, specParseFailed(path, line, column, cause) { return new ApicodegenError({ code: ErrorCodes.SPEC_PARSE_FAILED, message: "Failed to parse OpenAPI spec (invalid JSON or YAML)", location: path, line, column, suggestions: [ "Validate JSON syntax using jsonlint.com", "For YAML specs, ensure proper indentation", "Check for trailing commas or unquoted special characters" ], cause }); }, outputDirMissing(path, cause) { return new ApicodegenError({ code: ErrorCodes.OUTPUT_DIR_MISSING, message: "Output directory does not exist", location: path, suggestions: ["Create the directory: mkdir -p $(dirname <output>)", "Check if the path is correct"], cause }); }, configInvalid(path, cause) { return new ApicodegenError({ code: ErrorCodes.CONFIG_INVALID, message: "Invalid configuration file", location: path, suggestions: ["Validate JSON syntax in the config file", "Check for required fields (spec, output)"], cause }); }, validationFailed(path, details, cause) { return new ApicodegenError({ code: ErrorCodes.VALIDATION_FAILED, message: "OpenAPI spec validation failed", location: path, path: details, suggestions: [ "Check OpenAPI spec structure at the specified path", "Ensure all required fields are present", "Validate using swagger.io editor" ], cause }); }, generationFailed(cause) { return new ApicodegenError({ code: ErrorCodes.GENERATION_FAILED, message: "Code generation failed", suggestions: [ "Check for unsupported OpenAPI features", "Ensure spec follows OpenAPI 2.0, 3.0, or 3.1 specification", "Use --verbose for more details" ], cause }); }, typeCheckFailed(path, _errors, cause) { return new ApicodegenError({ code: ErrorCodes.TYPE_CHECK_FAILED, message: "TypeScript type check failed", location: path, suggestions: [ "Review type errors above", "Check for schema inconsistencies", "Update generated types or fix source schema" ], cause }); }, missingRequiredField(field, context) { return new ApicodegenError({ code: ErrorCodes.VALIDATION_FAILED, message: `Missing required field: ${field}`, path: context, suggestions: [`Add the '${field}' field to your configuration`] }); } }; /** * Wrap unknown error in ApicodegenError if needed */ function wrapError(error, context) { if (error instanceof ApicodegenError) return error; if (error instanceof Error) return new ApicodegenError({ code: context?.code || ErrorCodes.GENERATION_FAILED, message: context?.message || error.message, location: context?.location, suggestions: context?.suggestions, cause: error }); return new ApicodegenError({ code: context?.code || ErrorCodes.GENERATION_FAILED, message: String(error), suggestions: context?.suggestions }); } /** * Check if error is an ApicodegenError */ function isApicodegenError(error) { return error instanceof ApicodegenError; } //#endregion //#region src/core/generator/index.ts var Generator = class Generator { /** * Converts an array of TypeScript statements into a formatted string of code. * * @param statements - The array of TypeScript statement nodes. * @returns Formatted code as a string. * @throws {Error} If no valid statements are provided. */ static toCode(statements) { if (statements.length === 0) return "// No api declaration found."; const sourceFile = factory.createSourceFile(statements, factory.createToken(SyntaxKind.EndOfFileToken), NodeFlags.None); return createPrinter().printFile(sourceFile); } static async write(code, filepath) { const { mkdir } = await import("node:fs/promises"); const { dirname } = await import("node:path"); try { await mkdir(dirname(filepath), { recursive: true }); await writeFile(filepath, code); } catch (error) { throw new ApicodegenError({ code: ErrorCodes.OUTPUT_DIR_MISSING, message: "Failed to write generated code to output file", location: filepath, cause: error instanceof Error ? error : new Error(String(error)), suggestions: ["Verify the output directory path is writable", "Check that the parent directory exists or can be created"] }); } } /** * Converts a path string with parameters into a TypeScript template expression. * Handles query parameters and path placeholders. * * @param path - The base path string containing placeholders. * @param parameters - Array of parameter objects defining the parameters. * @param basePath - Optional base path to prepend (default: ""). * @returns A TypeScript template expressi */ static toUrlTemplate(path, parameters, basePath = "") { const queryParameters = parameters.filter((p) => p.in === "query"); if (queryParameters.length > 0) { const queryString = queryParameters.map((qp, index) => `${index === 0 ? "?" : "&"}${encodeURIComponent(qp.name)}={${Base.normalize(qp.name)}}`).join(""); path += queryString; } const pathSegments = path.replaceAll("{", "${").split("$").filter(Boolean); if (pathSegments.length === 1) return factory.createNoSubstitutionTemplateLiteral(basePath + path); return factory.createTemplateExpression(factory.createTemplateHead(basePath + pathSegments[0]), pathSegments.slice(1).map((segment, index) => { const match = /^{(.+)}(.+)?/gm.exec(segment); const isLastSegment = index === pathSegments.length - 2; if (!match) throw new Error(`Invalid path segment: ${segment}`); return factory.createTemplateSpan(factory.createIdentifier(Base.normalize(match[1])), !isLastSegment ? factory.createTemplateMiddle(match[2]) : factory.createTemplateTail(match[2] || "")); })); } /** * Adds synthetic comments to a TypeScript AST node. * * @param node - The target AST node. * @param comments - Array of comment objects to add. */ static addComments(node, comments) { if (!Array.isArray(comments) || comments.filter(Boolean).length === 0) return; const formatComment = (comment) => { if (comment.tag === "returns") return `* @returns {${comment.type}} ${comment.comment ?? ""}`; if (comment.tag === "param") return comment.comment ? `* @param ${comment.paramName} - ${comment.comment}` : `* @param ${comment.paramName}`; if (comment.tag) return `* @${comment.tag} ${comment.comment ?? ""}`; return `* ${comment.comment}`; }; const formattedComments = comments.map(formatComment).join("\n").trim() + "\n"; addSyntheticLeadingComment(node, SyntaxKind.MultiLineCommentTrivia, formattedComments, true); } /** * Checks if a schema represents a binary type. * * @param schema - The schema object to check. * @returns true if the schema is a binary type, false otherwise. */ static isBinarySchema(schema) { if (schema.type === "array") { const arraySchema = schema; return Generator.isBinarySchema(arraySchema.items); } const nonArraySchema = schema; return nonArraySchema.format === "blob" || nonArraySchema.format === "binary" || nonArraySchema.type === "file"; } static schemaToTypeString(schema) { if (schema.type === "array") { const arraySchema = schema; return arraySchema.items ? `${Generator.schemaToTypeString(arraySchema.items)}[]` : "unknown"; } const singleSchema = schema; if (schema.type === "string") return "string"; if (schema.type === "number" || schema.type === "integer") return "number"; if (schema.type === "boolean") return "boolean"; if (schema.type === "object" || schema.properties) return "object"; if (singleSchema.format === "binary" || singleSchema.type === "file") return "Blob"; if (singleSchema.format === "blob") return "Blob"; if (singleSchema.ref) return singleSchema.ref; return "unknown"; } static generateParamTags(parameters, requestBody) { const tags = []; for (const p of parameters) { const paramName = Base.normalize(p.name); let paramType = "unknown"; if (p.schema) paramType = Generator.schemaToTypeString(p.schema); const isOptional = p.required === false; tags.push({ tag: "param", paramName, type: `${paramType}${isOptional ? " | undefined" : ""}`, comment: p.description ?? "" }); } if (requestBody?.schema && "properties" in requestBody.schema) { const properties = requestBody.schema.properties; const required = requestBody.schema.required; const requiredArray = Array.isArray(required) ? required : []; for (const [key, schema] of Object.entries(properties ?? {})) { const paramName = `req.${key}`; const paramType = Generator.schemaToTypeString(schema); const isOptional = !requiredArray.includes(key); tags.push({ tag: "param", paramName, type: `${paramType}${isOptional ? " | undefined" : ""}`, comment: schema.description ?? "" }); } } return tags; } static toRequestBodyTypeNode(schema) { return factory.createParameterDeclaration(void 0, void 0, factory.createIdentifier("req"), void 0, Generator.toTypeNode(schema)); } static toTypeNode(schema) { const { type, ref } = schema; if (ref) { const identify = Base.ref2name(ref); return factory.createTypeReferenceNode(factory.createIdentifier(identify === "unknown" ? identify : Base.upperCamelCase(identify))); } switch (type) { case "array": { const { items } = schema; return factory.createArrayTypeNode(Generator.toTypeNode(items)); } case "object": { const propsCount = Object.keys(schema.properties ?? {}).length; if (!schema.properties || propsCount === 0) return factory.createTypeReferenceNode(factory.createIdentifier("Record"), [factory.createToken(SyntaxKind.StringKeyword), factory.createToken(SyntaxKind.UnknownKeyword)]); const props = Object.keys(schema.properties); return factory.createTypeLiteralNode(props.map((propKey) => { const propSchema = schema.properties[propKey]; return factory.createPropertySignature(void 0, factory.createStringLiteral(propKey), schema.required || schema.ref || Generator.isBinarySchema(schema) ? void 0 : factory.createToken(SyntaxKind.QuestionToken), Generator.toTypeNode(propSchema)); })); } case "integer": case "number": if (schema.enum) return factory.createUnionTypeNode(schema.enum.map((e) => factory.createLiteralTypeNode(factory.createNumericLiteral(e)))); return factory.createToken(SyntaxKind.NumberKeyword); case "boolean": return factory.createToken(SyntaxKind.BooleanKeyword); case "file": return factory.createTypeReferenceNode(factory.createIdentifier("Blob")); default: { const { format, oneOf, allOf, anyOf, type, enum: enum_ } = schema; switch (format) { case "number": return factory.createToken(SyntaxKind.NumberKeyword); case "string": return factory.createToken(SyntaxKind.StringKeyword); case "boolean": return factory.createToken(SyntaxKind.BooleanKeyword); case "blob": case "binary": return factory.createTypeReferenceNode(factory.createIdentifier("Blob")); default: } if (enum_) return factory.createUnionTypeNode(enum_.map((e) => factory.createLiteralTypeNode(factory.createStringLiteral(e)))); if (type === "string") return factory.createToken(SyntaxKind.StringKeyword); if (oneOf) return factory.createUnionTypeNode(oneOf.map((schema) => Generator.toTypeNode(schema))); if (anyOf) return factory.createUnionTypeNode(anyOf.map((schema) => Generator.toTypeNode(schema))); if (allOf) return factory.createIntersectionTypeNode(allOf.map((schema) => Generator.toTypeNode(schema))); if (type && typeof type === "string") return factory.createTypeReferenceNode(type !== "unknown" && type !== "null" ? factory.createIdentifier(Base.upperCamelCase(type)) : type); } } return factory.createToken(SyntaxKind.UnknownKeyword); } static toDeclarationNodes(parameters) { const objectElements = []; const typeObjectElements = []; const refParameters = []; for (const parameter of parameters) if (parameter.ref) { const refName = Base.ref2name(parameter.ref); refParameters.push(factory.createParameterDeclaration(void 0, void 0, factory.createIdentifier(Base.normalize(refName)), void 0, factory.createTypeReferenceNode(factory.createIdentifier(Base.upperCamelCase(Base.normalize(refName)))), void 0)); } else { const { name, schema, required } = parameter; objectElements.push(factory.createBindingElement(void 0, void 0, factory.createIdentifier(Base.normalize(name)))); typeObjectElements.push(factory.createPropertySignature([], factory.createIdentifier(Base.normalize(name)), required ? void 0 : factory.createToken(SyntaxKind.QuestionToken), !schema ? factory.createToken(SyntaxKind.UnknownKeyword) : Generator.toTypeNode(schema))); } if (objectElements.length > 0) return [factory.createParameterDeclaration(void 0, void 0, factory.createObjectBindingPattern(objectElements), void 0, factory.createTypeLiteralNode(typeObjectElements), void 0), ...refParameters]; return refParameters; } static toFormDataStatement(parameters, requestBody) { const statements = []; const fdDeclaration = factory.createVariableStatement(void 0, factory.createVariableDeclarationList([factory.createVariableDeclaration(factory.createIdentifier("fd"), void 0, void 0, factory.createNewExpression(factory.createIdentifier("FormData"), void 0, []))], NodeFlags.Const)); statements.push(fdDeclaration); parameters.forEach((parameter) => { statements.push(factory.createExpressionStatement(factory.createBinaryExpression(factory.createIdentifier(parameter.name), factory.createToken(SyntaxKind.AmpersandAmpersandToken), factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("fd"), factory.createIdentifier("append")), void 0, [factory.createStringLiteral(parameter.name), factory.createIdentifier(parameter.name)])))); }); if (requestBody && requestBody.type === "object" && requestBody.properties && Object.keys(requestBody.properties).length !== 0) Object.keys(requestBody.properties).forEach((key) => { const schemaByKey = requestBody.properties[key]; if (schemaByKey.type === "array" && Generator.isBinarySchema(schemaByKey)) statements.push(factory.createForOfStatement(void 0, factory.createVariableDeclarationList([factory.createVariableDeclaration("file")], NodeFlags.Const), factory.createElementAccessExpression(factory.createIdentifier("req"), factory.createStringLiteral(key)), factory.createBlock([factory.createExpressionStatement(factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("fd"), factory.createIdentifier("append")), [], [ factory.createStringLiteral(key), factory.createIdentifier("file"), factory.createPropertyAccessExpression(factory.createAsExpression(factory.createIdentifier("file"), factory.createTypeReferenceNode(factory.createIdentifier("File"), void 0)), factory.createIdentifier("name")) ]))]))); else if (schemaByKey.required) statements.push(factory.createExpressionStatement(factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("fd"), factory.createIdentifier("append")), void 0, [factory.createStringLiteral(key), schemaByKey.type === "string" || Generator.isBinarySchema(schemaByKey) || schemaByKey.isRef ? factory.createElementAccessExpression(factory.createIdentifier("req"), factory.createStringLiteral(key)) : schemaByKey.type === "array" || schemaByKey.type === "object" ? factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("JSON"), factory.createIdentifier("stringify")), void 0, [factory.createElementAccessExpression(factory.createIdentifier("req"), factory.createStringLiteral(key))]) : factory.createCallExpression(factory.createIdentifier("String"), void 0, [factory.createElementAccessExpression(factory.createIdentifier("req"), factory.createStringLiteral(key))])]))); else statements.push(factory.createExpressionStatement(factory.createBinaryExpression(factory.createElementAccessExpression(factory.createIdentifier("req"), factory.createStringLiteral(key)), factory.createToken(SyntaxKind.AmpersandAmpersandToken), factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("fd"), factory.createIdentifier("append")), void 0, [factory.createStringLiteral(key), schemaByKey.type === "string" || Generator.isBinarySchema(schemaByKey) || schemaByKey.isRef ? factory.createElementAccessExpression(factory.createIdentifier("req"), factory.createStringLiteral(key)) : schemaByKey.type === "array" || schemaByKey.type === "object" ? factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("JSON"), factory.createIdentifier("stringify")), void 0, [factory.createElementAccessExpression(factory.createIdentifier("req"), factory.createStringLiteral(key))]) : factory.createCallExpression(factory.createIdentifier("String"), void 0, [factory.createElementAccessExpression(factory.createIdentifier("req"), factory.createStringLiteral(key))])])))); }); return statements; } static bodyBlock(uri, method, parameters, requestBody, response, adapter) { const isFormDataRequest = requestBody && ["multipart/form-data", "application/x-www-form-urlencoded"].includes(requestBody.type); const shouldParseResponseToJSON = "application/json" === response?.type; const isEventStream = response?.type === "text/event-stream"; const isRequestBodyBinary = requestBody?.schema && requestBody.schema.type === "array" && Generator.isBinarySchema(requestBody.schema); const parametersShouldPutInFormData = parameters.filter((p) => p.in === "formData" || p.schema && Generator.isBinarySchema(p.schema)); const parametersShouldNotPutInFormData = parameters.filter((p) => !parametersShouldPutInFormData.includes(p)); const isRequestBodyContainsBinary = requestBody?.schema && "properties" in requestBody.schema && Object.values(requestBody.schema?.properties ?? {}).some((p) => Generator.isBinarySchema(p)); const hasBinaryInParameters = parameters.some((p) => p?.schema && Generator.isBinarySchema(p.schema)); const shouldPutParametersOrBodyInFormData = !!isFormDataRequest && (isRequestBodyBinary || hasBinaryInParameters || isRequestBodyContainsBinary || parametersShouldPutInFormData.length > 0); return factory.createBlock([...shouldPutParametersOrBodyInFormData ? Generator.toFormDataStatement(parametersShouldPutInFormData, requestBody?.schema) : [], ...adapter.client(uri, method, parametersShouldNotPutInFormData, requestBody, response, adapter, shouldPutParametersOrBodyInFormData, shouldParseResponseToJSON, isEventStream)]); } static schemaToStatemets(parsedDoc, adaptor, options) { const statements = []; const { apis, schemas = {}, enums } = parsedDoc; const enumNames = []; for (const enumObject of enums) { enumNames.push(Base.upperCamelCase(enumObject.name)); statements.push(factory.createEnumDeclaration([factory.createToken(SyntaxKind.ExportKeyword)], factory.createIdentifier(Base.upperCamelCase(enumObject.name)), enumObject.enum.map((member) => { return factory.createEnumMember(factory.createStringLiteral(typeof member === "string" ? member : `${member}_`), typeof member === "string" ? factory.createStringLiteral(member) : factory.createNumericLiteral(member)); }))); } for (const schemaKey in schemas) if (Object.hasOwn(schemas, schemaKey) && !enumNames.includes(Base.upperCamelCase(schemaKey))) { const schema = schemas[schemaKey]; statements.push(factory.createTypeAliasDeclaration([factory.createModifier(SyntaxKind.ExportKeyword)], factory.createIdentifier(Base.upperCamelCase(schemaKey)), void 0, Generator.toTypeNode(schema))); } for (const uri in apis) { const operations = apis[uri]; for (const operation of operations) { const { method, operationId, requestBody = [], responses = [], summary, deprecated, description } = operation; let { parameters = [] } = operation; parameters = parameters.filter((p) => p.in !== "cookie"); if (requestBody.length === 0) requestBody.push({ type: "application/json" }); const shouldAddExtraMethodNameSuffix = requestBody.length > 1; for (const req of requestBody) { const statement = factory.createFunctionDeclaration([factory.createModifier(SyntaxKind.ExportKeyword), factory.createModifier(SyntaxKind.AsyncKeyword)], void 0, Base.pathToFnName(uri, method, operationId) + (shouldAddExtraMethodNameSuffix ? Base.capitalize(req.type.split("/")[1]) : ""), void 0, [...parameters.length > 0 ? Generator.toDeclarationNodes(parameters) : [], ...req?.schema ? [Generator.toRequestBodyTypeNode(req.schema)] : []].filter(Boolean), void 0, Generator.bodyBlock(options.baseURL + uri, method, parameters, req, responses[0], adaptor)); const mergedDescription = [description, summary].filter(Boolean).join(". "); Generator.addComments(statement, [ mergedDescription && { comment: mergedDescription }, deprecated && { tag: "deprecated" }, ...Generator.generateParamTags(parameters, req) ].filter(Boolean)); statements.push(statement); } } } return statements; } static async prettier(code) { return await format(code, { parser: "typescript" }); } static async genCode(schema, initOptions, adaptor) { const { importClientSource } = initOptions; const statements = Generator.schemaToStatemets(schema, adaptor, { baseURL: initOptions.baseURL ?? "" }); let code = Generator.toCode(statements); if (importClientSource) code = importClientSource + "\n\n" + code; return await Generator.prettier(code); } }; //#endregion //#region src/core/client/axios.ts /** * Adapter class implementing support for generating code that makes use of the Axios HTTP client library. * This class defines custom behavior and field mappings specific to the Axios client. */ var AxiosAdapter = class extends Adapter { /** * Name of the field used to specify the HTTP method in the request configuration. */ methodFieldName = "method"; /** * Name of the field used to specify the request body (data) in the request configuration. */ bodyFieldName = "data"; /** * Name of the field used to specify the request headers in the request configuration. */ headersFieldName = "headers"; /** * Name of the field used to specify the query parameters in the request configuration. */ queryFieldName = "params"; /** * The name of the client this adapter is configured for, which is 'axios' in this case. */ name = "axios"; /** * Generates client code for making API requests using Axios. * @param uri - The API endpoint URI * @param method - The HTTP method (GET, POST, etc.) * @param parameters - Array of parameters to include in the request * @param requestBody - The request body media type definition * @param response - The response media type definition * @param adapter - The adapter instance * @param shouldUseFormData - Flag to use FormData for the request body * @param shouldUseJSONResponse - Unused by AxiosAdapter; present to align with the abstract signature so positional args bind correctly * @param isEventStream - Flag indicating a text/event-stream response; when true the raw AxiosResponse is returned without JSON parsing * @return - An array of generated TypeScript statements */ client(uri, method, parameters, requestBody, response, adapter, shouldUseFormData, _shouldUseJSONResponse, isEventStream) { const statements = []; const inBody = parameters.filter((p) => !p.in || p.in === "body"); const inHeader = parameters.filter((p) => p.in === "header"); /** * Creates the literal object expression for fetch options * including method, headers, and body. * @returns - The constructed fetch options object */ const toLiterlExpression = (extraProperties = []) => { return factory.createObjectLiteralExpression([factory.createPropertyAssignment(factory.createIdentifier(adapter.methodFieldName), factory.createStringLiteral(method.toUpperCase()))].concat(inHeader.length > 0 ? factory.createPropertyAssignment(factory.createIdentifier(adapter.headersFieldName), factory.createObjectLiteralExpression(inHeader.map((p) => factory.createPropertyAssignment(factory.createStringLiteral(p.name), factory.createCallExpression(factory.createIdentifier("encodeURIComponent"), void 0, [factory.createCallExpression(factory.createIdentifier("String"), void 0, [factory.createIdentifier(Base.normalize(p.name))])]))))) : []).concat(shouldUseFormData || inBody.length > 0 || requestBody?.schema ? factory.createPropertyAssignment(factory.createIdentifier(adapter.bodyFieldName), shouldUseFormData ? factory.createIdentifier("fd") : inBody.length > 0 || requestBody?.schema && !Generator.isBinarySchema(requestBody.schema) ? factory.createIdentifier("req") : factory.createIdentifier("req")) : []).concat(extraProperties), true); }; if (isEventStream) { statements.push(factory.createReturnStatement(factory.createCallExpression(factory.createIdentifier(adapter.name), void 0, [Generator.toUrlTemplate(uri, parameters), toLiterlExpression([factory.createPropertyAssignment(factory.createIdentifier("adapter"), factory.createStringLiteral("fetch")), factory.createPropertyAssignment(factory.createIdentifier("responseType"), factory.createStringLiteral("stream"))])]))); return statements; } statements.push(factory.createReturnStatement(factory.createCallExpression(factory.createIdentifier(adapter.name), response?.schema ? [Generator.toTypeNode(response.schema)] : void 0, [Generator.toUrlTemplate(uri, parameters), toLiterlExpression()]))); return statements; } }; //#endregion //#region src/core/client/fetch.ts /** * FetchAdapter is an adapter class that generates client-side fetch requests. * It handles parameters, headers, and request bodies to construct proper fetch calls. */ var FetchAdapter = class extends Adapter { methodFieldName = "method"; bodyFieldName = "body"; headersFieldName = "headers"; queryFieldName = ""; name = "fetch"; /** * Generates client code for making API requests using the Fetch API. * @param uri - The API endpoint URI * @param method - The HTTP method (GET, POST, etc.) * @param parameters - Array of parameters to include in the request * @param requestBody - The request body media type definition * @param response - The response media type definition * @param adapter - The adapter instance * @param shouldUseFormData - Flag to use FormData for the request body * @param shouldUseJSONResponse - Flag to use JSON parsing for the response * @param isEventStream - Flag indicating a text/event-stream response; when true the raw Response is returned unparsed * @return - An array of generated TypeScript statements */ client(uri, method, parameters, requestBody, response, adapter, shouldUseFormData, shouldUseJSONResponse, isEventStream) { const statements = []; const inBody = parameters.filter((p) => !p.in || p.in === "body"); const inHeader = parameters.filter((p) => p.in === "header"); /** * Creates the literal object expression for fetch options * including method, headers, and body. * @returns - The constructed fetch options object */ const toLiterlExpression = () => { return factory.createObjectLiteralExpression([factory.createPropertyAssignment(factory.createIdentifier(adapter.methodFieldName), factory.createStringLiteral(method.toUpperCase()))].concat(inHeader.length > 0 ? factory.createPropertyAssignment(factory.createIdentifier(adapter.headersFieldName), factory.createObjectLiteralExpression(inHeader.map((p) => factory.createPropertyAssignment(factory.createStringLiteral(p.name), factory.createCallExpression(factory.createIdentifier("encodeURIComponent"), void 0, [factory.createCallExpression(factory.createIdentifier("String"), void 0, [factory.createIdentifier(Base.normalize(p.name))])]))))) : []).concat(shouldUseFormData || inBody.length > 0 || requestBody?.schema ? factory.createPropertyAssignment(factory.createIdentifier(adapter.bodyFieldName), shouldUseFormData ? factory.createIdentifier("fd") : inBody.length > 0 || requestBody?.schema && !Generator.isBinarySchema(requestBody.schema) ? factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("JSON"), factory.createIdentifier("stringify")), [], [requestBody ? factory.createIdentifier("req") : factory.createObjectLiteralExpression(inBody.map((b) => factory.createShorthandPropertyAssignment(factory.createIdentifier(b.name))), true)]) : factory.createIdentifier("req")) : []), true); }; if (isEventStream) { statements.push(factory.createReturnStatement(factory.createCallExpression(factory.createIdentifier(adapter.name), void 0, [Generator.toUrlTemplate(uri, parameters), toLiterlExpression()]))); return statements; } statements.push(factory.createReturnStatement(shouldUseJSONResponse ? factory.createCallExpression(factory.createPropertyAccessExpression(factory.createCallExpression(factory.createIdentifier(adapter.name), void 0, [Generator.toUrlTemplate(uri, parameters), toLiterlExpression()]), factory.createIdentifier("then")), void 0, [factory.createArrowFunction([factory.createModifier(SyntaxKind.AsyncKeyword)], [], [factory.createParameterDeclaration(void 0, void 0, factory.createIdentifier("response"))], void 0, factory.createToken(SyntaxKind.EqualsGreaterThanToken), response?.schema ? factory.createAsExpression(factory.createParenthesizedExpression(factory.createAwaitExpression(factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("response"), factory.createIdentifier("json")), void 0, []))), response?.schema ? Generator.toTypeNode(response.schema) : factory.createToken(SyntaxKind.UnknownKeyword)) : factory.createParenthesizedExpression(factory.createAwaitExpression(factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("response"), factory.createIdentifier("json")), void 0, []))))]) : factory.createCallExpression(factory.createIdentifier(adapter.name), void 0, [Generator.toUrlTemplate(uri, parameters), toLiterlExpression()]))); return statements; } }; //#endregion //#region src/core/config.ts /** * Environment variable mappings */ const ENV_MAPPINGS = { APICODEGEN_SPEC: "spec", APICODEGEN_OUTPUT: "output", APICODEGEN_BASE_URL: "baseURL", APICODEGEN_ADAPTOR: "adaptor", APICODEGEN_VERBOSE: "verbose", APICODEGEN_WATCH: "watch", APICODEGEN_TYPE_CHECK: "typeCheck" }; /** * Load config from environment variables */ function loadFromEnv() { const config = {}; for (const [envKey, configKey] of Object.entries(ENV_MAPPINGS)) { const value = process.env[envKey]; if (value !== void 0) switch (configKey) { case "verbose": case "watch": case "typeCheck": config[configKey] = value === "true" || value === "1"; break; case "adaptor": config[configKey] = value; break; default: config[configKey] = value; } } return config; } /** * Load config from a file */ async function loadFromFile(filePath) { const ext = path.extname(filePath).toLowerCase(); try { if (ext === ".json" || ext === ".jsonc") { const content = await fs.readFile(filePath, "utf-8"); return JSON.parse(content); } if (ext === ".js" || ext === ".cjs" || ext === ".mjs") { const mod = await import(filePath); return mod.default || mod; } if (ext === ".ts") { const content = await fs.readFile(filePath, "utf-8"); try { return JSON.parse(content); } catch { const jsonMatch = content.match(/export\s+default\s+(\{.+\})/s); if (jsonMatch) return JSON.parse(jsonMatch[1]); } } const content = await fs.readFile(filePath, "utf-8"); return JSON.parse(content); } catch (error) { throw new Error(`Failed to load config from ${filePath}: ${error}`); } } /** * Find config file in project root */ async function findConfigFile(cwd) { for (const fileName of [ "apicodegen.config.json", "apicodegen.config.js", "apicodegen.config.mjs", ".apicodegenrc", ".apicodegenrc.json", ".apicodegenrc.js", ".apicodegenrc.mjs" ]) { const filePath = path.join(cwd, fileName); if (await fs.pathExists(filePath)) return filePath; } const packageJsonPath = path.join(cwd, "package.json"); if (await fs.pathExists(packageJsonPath)) try { const pkg = JSON.parse(await fs.readFile(packageJsonPath, "utf-8")); if (pkg.apicodegen && typeof pkg.apicodegen === "string") return path.resolve(cwd, pkg.apicodegen); } catch {} return null; } /** * Merge multiple config sources with priority * Priority: defaults < env vars < config file < CLI args */ function mergeConfigs(base, ...sources) { const result = { ...base }; for (const source of sources) { if (!source) continue; for (const [key, value] of Object.entries(source)) if (value !== void 0) result[key] = value; } return result; } /** * Validate config has required fields */ function validateConfig(config) { if (!config.spec) throw new Error("Missing required field: spec (OpenAPI spec file path or URL)"); return true; } /** * Load and resolve config from multiple sources */ async function loadConfig(options = {}) { const cwd = options.cwd || process.cwd(); const cliOptions = options.cliOptions || {}; const envConfig = loadFromEnv(); let fileConfig = {}; let configFilePath; if (options.configFile) { configFilePath = path.resolve(cwd, options.configFile); fileConfig = await loadFromFile(configFilePath); } else { const foundPath = await findConfigFile(cwd); if (foundPath) { configFilePath = foundPath; fileConfig = await loadFromFile(foundPath); } } const packageJsonPath = path.join(cwd, "package.json"); let inlineConfig = {}; if (await fs.pathExists(packageJsonPath)) try { const pkg = JSON.parse(await fs.readFile(packageJsonPath, "utf-8")); if (pkg.apicodegen && typeof pkg.apicodegen === "object") inlineConfig =