@moccona/apicodegen
Version:
A powerful OpenAPI code generator that automatically generates TypeScript API client code from OpenAPI specifications.
1,171 lines • 77 kB
JavaScript
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 =