@moccona/apicodegen
Version:
A powerful OpenAPI code generator that automatically generates TypeScript API client code from OpenAPI specifications.
1 lines • 170 kB
Source Map (JSON)
{"version":3,"file":"index.mjs","names":["logger","t","t","t"],"sources":["../../src/core/config.ts","../../src/core/errors.ts","../../src/core/logger.ts","../../src/core/base/Adaptor.ts","../../src/core/constants/keywords.ts","../../src/core/interface.ts","../../src/core/base/Base.ts","../../src/core/base/Provider.ts","../../src/core/generator/index.ts","../../src/core/client/axios.ts","../../src/core/client/fetch.ts","../../src/openapi/VersionedProvider.ts","../../src/openapi/V2.ts","../../src/openapi/V3.ts","../../src/openapi/V3_1.ts","../../src/openapi/index.ts","../../src/vite-plugin/index.ts"],"sourcesContent":["import path from 'node:path';\nimport fs from 'fs-extra';\n\nimport type { Adaptors, FetchDocRequestInit } from './interface.js';\n\n/**\n * Adaptor type for HTTP client\n */\nexport type ConfigAdaptor = keyof typeof Adaptors;\n\n/**\n * Shared config interface for CLI and Vite plugin\n */\nexport interface ApicodegenConfig {\n\t/** OpenAPI spec file path or URL (required) */\n\tspec: string;\n\t/** Output file path */\n\toutput: string;\n\t/** HTTP client adaptor (fetch|axios) */\n\tadaptor?: ConfigAdaptor;\n\t/** Base URL for API endpoints */\n\tbaseURL?: string;\n\t/** Custom client import source path */\n\timportClientSource?: string;\n\t/** Enable verbose logging */\n\tverbose?: boolean;\n\t/** Run type check after generation (default: true) */\n\ttypeCheck?: boolean;\n\t/** Watch for file changes */\n\twatch?: boolean;\n\t/** Request options for fetching spec */\n\trequestOptions?: FetchDocRequestInit;\n}\n\n/**\n * Options for loading config\n */\nexport interface LoadConfigOptions {\n\t/** Explicit config file path */\n\tconfigFile?: string;\n\t/** Config file directory (defaults to cwd) */\n\tcwd?: string;\n\t/** CLI overrides */\n\tcliOptions?: Partial<ApicodegenConfig>;\n\t/** Vite plugin options (for name metadata) */\n\tname?: string;\n}\n\n/**\n * Result of config loading\n */\nexport interface ResolvedConfig extends ApicodegenConfig {\n\t/** Config file path if loaded from file */\n\tconfigFilePath?: string;\n\t/** Config name for logging */\n\tname: string;\n}\n\n/**\n * Environment variable mappings\n */\nconst ENV_MAPPINGS: Record<string, keyof ApicodegenConfig> = {\n\tAPICODEGEN_SPEC: 'spec',\n\tAPICODEGEN_OUTPUT: 'output',\n\tAPICODEGEN_BASE_URL: 'baseURL',\n\tAPICODEGEN_ADAPTOR: 'adaptor',\n\tAPICODEGEN_VERBOSE: 'verbose',\n\tAPICODEGEN_WATCH: 'watch',\n\tAPICODEGEN_TYPE_CHECK: 'typeCheck',\n};\n\n/**\n * Load config from environment variables\n */\nfunction loadFromEnv(): Partial<ApicodegenConfig> {\n\tconst config: Partial<ApicodegenConfig> = {};\n\n\tfor (const [envKey, configKey] of Object.entries(ENV_MAPPINGS)) {\n\t\tconst value = process.env[envKey];\n\t\tif (value !== undefined) {\n\t\t\t// Convert string to appropriate type\n\t\t\tswitch (configKey) {\n\t\t\t\tcase 'verbose':\n\t\t\t\tcase 'watch':\n\t\t\t\tcase 'typeCheck':\n\t\t\t\t\tconfig[configKey] = value === 'true' || value === '1';\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'adaptor':\n\t\t\t\t\tconfig[configKey] = value as ConfigAdaptor;\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tconfig[configKey] = value;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn config;\n}\n\n/**\n * Load config from a file\n */\nasync function loadFromFile(\n\tfilePath: string\n): Promise<Partial<ApicodegenConfig>> {\n\tconst ext = path.extname(filePath).toLowerCase();\n\n\ttry {\n\t\tif (ext === '.json' || ext === '.jsonc') {\n\t\t\tconst content = await fs.readFile(filePath, 'utf-8');\n\t\t\treturn JSON.parse(content);\n\t\t}\n\n\t\tif (ext === '.js' || ext === '.cjs' || ext === '.mjs') {\n\t\t\tconst mod = await import(filePath);\n\t\t\treturn mod.default || mod;\n\t\t}\n\n\t\tif (ext === '.ts') {\n\t\t\t// For .ts files, try to load as JSON first\n\t\t\tconst content = await fs.readFile(filePath, 'utf-8');\n\t\t\t// Try parsing as JSON (may work for JSON-like TS files)\n\t\t\ttry {\n\t\t\t\treturn JSON.parse(content);\n\t\t\t} catch {\n\t\t\t\t// For actual TS config, we'd need ts-node or similar\n\t\t\t\t// For now, fall back to looking for JSON export pattern\n\t\t\t\tconst jsonMatch = content.match(/export\\s+default\\s+(\\{.+\\})/s);\n\t\t\t\tif (jsonMatch) {\n\t\t\t\t\treturn JSON.parse(jsonMatch[1]);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Try parsing as JSON for unknown extensions\n\t\tconst content = await fs.readFile(filePath, 'utf-8');\n\t\treturn JSON.parse(content);\n\t} catch (error) {\n\t\tthrow new Error(`Failed to load config from ${filePath}: ${error}`);\n\t}\n}\n\n/**\n * Find config file in project root\n */\nasync function findConfigFile(cwd: string): Promise<string | null> {\n\tconst configFiles = [\n\t\t'apicodegen.config.json',\n\t\t'apicodegen.config.js',\n\t\t'apicodegen.config.mjs',\n\t\t'.apicodegenrc',\n\t\t'.apicodegenrc.json',\n\t\t'.apicodegenrc.js',\n\t\t'.apicodegenrc.mjs',\n\t];\n\n\tfor (const fileName of configFiles) {\n\t\tconst filePath = path.join(cwd, fileName);\n\t\tif (await fs.pathExists(filePath)) {\n\t\t\treturn filePath;\n\t\t}\n\t}\n\n\t// Also check package.json for apicodegen field\n\tconst packageJsonPath = path.join(cwd, 'package.json');\n\tif (await fs.pathExists(packageJsonPath)) {\n\t\ttry {\n\t\t\tconst pkg = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));\n\t\t\tif (pkg.apicodegen && typeof pkg.apicodegen === 'string') {\n\t\t\t\treturn path.resolve(cwd, pkg.apicodegen);\n\t\t\t}\n\t\t} catch {\n\t\t\t// Ignore package.json parse errors\n\t\t}\n\t}\n\n\treturn null;\n}\n\n/**\n * Merge multiple config sources with priority\n * Priority: defaults < env vars < config file < CLI args\n */\nfunction mergeConfigs(\n\tbase: ApicodegenConfig,\n\t...sources: (Partial<ApicodegenConfig> | undefined)[]\n): ApicodegenConfig {\n\tconst result = { ...base };\n\n\tfor (const source of sources) {\n\t\tif (!source) continue;\n\n\t\tfor (const [key, value] of Object.entries(source)) {\n\t\t\t// Only override if value is defined (not undefined)\n\t\t\tif (value !== undefined) {\n\t\t\t\t(result as Record<string, unknown>)[key] = value;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * Validate config has required fields\n */\nfunction validateConfig(\n\tconfig: Partial<ApicodegenConfig>\n): config is ApicodegenConfig {\n\tif (!config.spec) {\n\t\tthrow new Error(\n\t\t\t'Missing required field: spec (OpenAPI spec file path or URL)'\n\t\t);\n\t}\n\treturn true;\n}\n\n/**\n * Load and resolve config from multiple sources\n */\nexport async function loadConfig(\n\toptions: LoadConfigOptions = {}\n): Promise<ResolvedConfig> {\n\tconst cwd = options.cwd || process.cwd();\n\tconst cliOptions = options.cliOptions || {};\n\n\t// 1. Load from environment variables\n\tconst envConfig = loadFromEnv();\n\n\t// 2. Load from config file (if specified or found)\n\tlet fileConfig: Partial<ApicodegenConfig> = {};\n\tlet configFilePath: string | undefined;\n\n\tif (options.configFile) {\n\t\tconfigFilePath = path.resolve(cwd, options.configFile);\n\t\tfileConfig = await loadFromFile(configFilePath);\n\t} else {\n\t\tconst foundPath = await findConfigFile(cwd);\n\t\tif (foundPath) {\n\t\t\tconfigFilePath = foundPath;\n\t\t\tfileConfig = await loadFromFile(foundPath);\n\t\t}\n\t}\n\n\t// 3. Check package.json inline config\n\tconst packageJsonPath = path.join(cwd, 'package.json');\n\tlet inlineConfig: Partial<ApicodegenConfig> = {};\n\tif (await fs.pathExists(packageJsonPath)) {\n\t\ttry {\n\t\t\tconst pkg = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));\n\t\t\tif (pkg.apicodegen && typeof pkg.apicodegen === 'object') {\n\t\t\t\tinlineConfig = pkg.apicodegen as Partial<ApicodegenConfig>;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Ignore\n\t\t}\n\t}\n\n\t// 4. Merge configs with priority\n\tconst merged = mergeConfigs(\n\t\t{ spec: '', output: './output.ts' }, // defaults\n\t\tenvConfig,\n\t\tinlineConfig,\n\t\tfileConfig,\n\t\tcliOptions\n\t);\n\n\t// 5. Validate\n\tvalidateConfig(merged);\n\n\t// 6. Add metadata\n\tconst name = options.name || merged.baseURL || merged.spec;\n\n\treturn {\n\t\t...merged,\n\t\tconfigFilePath,\n\t\tname,\n\t};\n}\n\n/**\n * Convert resolved config to provider options format\n */\nexport function toProviderOptions(config: ResolvedConfig) {\n\treturn {\n\t\tdocURL: config.spec,\n\t\toutput: config.output,\n\t\tadaptor: config.adaptor,\n\t\tbaseURL: config.baseURL,\n\t\timportClientSource: config.importClientSource,\n\t\tverbose: config.verbose,\n\t\trequestOptions: config.requestOptions,\n\t};\n}\n","/**\n * Error handling utilities for api-codegen\n */\n\n// Error codes\nexport const ErrorCodes = {\n\tSPEC_NOT_FOUND: 'E_SPEC_NOT_FOUND',\n\tSPEC_FETCH_FAILED: 'E_SPEC_FETCH_FAILED',\n\tSPEC_PARSE_FAILED: 'E_SPEC_PARSE_FAILED',\n\tOUTPUT_DIR_MISSING: 'E_OUTPUT_DIR_MISSING',\n\tCONFIG_INVALID: 'E_CONFIG_INVALID',\n\tVALIDATION_FAILED: 'E_VALIDATION_FAILED',\n\tGENERATION_FAILED: 'E_GENERATION_FAILED',\n\tTYPE_CHECK_FAILED: 'E_TYPE_CHECK_FAILED',\n} as const;\n\nexport type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];\n\n/**\n * Error context for ApicodegenError\n */\nexport interface ApicodegenErrorContext {\n\t/** Error code */\n\tcode: ErrorCode;\n\t/** Human readable message */\n\tmessage: string;\n\t/** File/URL related to error */\n\tlocation?: string;\n\t/** Line number if applicable */\n\tline?: number;\n\t/** Column number if applicable */\n\tcolumn?: number;\n\t/** Related schema/path if applicable */\n\tpath?: string;\n\t/** Suggested fixes */\n\tsuggestions?: string[];\n\t/** Original error */\n\tcause?: Error;\n}\n\n/**\n * Custom error class for api-codegen with rich context\n */\nexport class ApicodegenError extends Error {\n\treadonly code: ErrorCode;\n\treadonly location?: string;\n\treadonly line?: number;\n\treadonly column?: number;\n\treadonly path?: string;\n\treadonly suggestions: string[];\n\treadonly cause?: Error;\n\n\tconstructor(context: ApicodegenErrorContext) {\n\t\tsuper(context.message);\n\t\tthis.name = 'ApicodegenError';\n\t\tthis.code = context.code;\n\t\tthis.location = context.location;\n\t\tthis.line = context.line;\n\t\tthis.column = context.column;\n\t\tthis.path = context.path;\n\t\tthis.suggestions = context.suggestions || [];\n\t\tthis.cause = context.cause;\n\n\t\t// Maintains proper stack trace in V8 environments\n\t\tif (Error.captureStackTrace) {\n\t\t\tError.captureStackTrace(this, ApicodegenError);\n\t\t}\n\t}\n\n\t/**\n\t * Convert error to formatted string for CLI output\n\t */\n\ttoString(verbose = false): string {\n\t\tconst lines: string[] = [];\n\n\t\t// Error header with code\n\t\tlines.push(`\\x1b[1;31mError [${this.code}]\\x1b[0m ${this.message}`);\n\n\t\t// Location\n\t\tif (this.location) {\n\t\t\tlines.push(` \\x1b[36m→ Location:\\x1b[0m ${this.location}`);\n\t\t}\n\n\t\t// Path\n\t\tif (this.path) {\n\t\t\tlines.push(` \\x1b[36m→ Path:\\x1b[0m ${this.path}`);\n\t\t}\n\n\t\t// Line/column\n\t\tif (this.line !== undefined) {\n\t\t\tlet lineInfo = ` \\x1b[36m→ Line:\\x1b[0m ${this.line}`;\n\t\t\tif (this.column !== undefined) {\n\t\t\t\tlineInfo += `, Column: ${this.column}`;\n\t\t\t}\n\t\t\tlines.push(lineInfo);\n\t\t}\n\n\t\t// Suggestions\n\t\tif (this.suggestions.length > 0) {\n\t\t\tfor (const suggestion of this.suggestions) {\n\t\t\t\tlines.push(` \\x1b[32m→ Suggestion:\\x1b[0m ${suggestion}`);\n\t\t\t}\n\t\t}\n\n\t\t// Stack trace in verbose mode\n\t\tif (verbose && this.cause) {\n\t\t\tlines.push(`\\n \\x1b[90mOriginal Error:\\x1b[0m ${this.cause.message}`);\n\t\t\tif (this.stack) {\n\t\t\t\t// Skip first few lines of stack (our error header)\n\t\t\t\tconst stackLines = this.stack.split('\\n').slice(1).join('\\n');\n\t\t\t\tlines.push(`\\x1b[90m${stackLines}\\x1b[0m`);\n\t\t\t}\n\t\t}\n\n\t\treturn lines.join('\\n');\n\t}\n\n\t/**\n\t * Convert to JSON-serializable object\n\t */\n\ttoJSON(): object {\n\t\treturn {\n\t\t\tname: this.name,\n\t\t\tcode: this.code,\n\t\t\tmessage: this.message,\n\t\t\tlocation: this.location,\n\t\t\tline: this.line,\n\t\t\tcolumn: this.column,\n\t\t\tpath: this.path,\n\t\t\tsuggestions: this.suggestions,\n\t\t\tcause: this.cause?.message,\n\t\t};\n\t}\n}\n\n/**\n * ANSI color codes for terminal output\n */\nexport const Colors = {\n\treset: '\\x1b[0m',\n\tbold: '\\x1b[1m',\n\tred: '\\x1b[31m',\n\tgreen: '\\x1b[32m',\n\tyellow: '\\x1b[33m',\n\tblue: '\\x1b[34m',\n\tcyan: '\\x1b[36m',\n\tgray: '\\x1b[90m',\n\tbrightRed: '\\x1b[91m',\n\tbrightGreen: '\\x1b[92m',\n} as const;\n\n/**\n * Format error for CLI output\n */\nexport function formatError(error: unknown, verbose = false): string {\n\tif (error instanceof ApicodegenError) {\n\t\treturn error.toString(verbose);\n\t}\n\n\tif (error instanceof Error) {\n\t\treturn `${Colors.red}${Colors.bold}Error${Colors.reset}: ${error.message}${verbose && error.stack ? `\\n\\n${Colors.gray}${error.stack}${Colors.reset}` : ''}`;\n\t}\n\n\treturn `${Colors.red}${Colors.bold}Error${Colors.reset}: ${String(error)}`;\n}\n\n/**\n * Print error to console with formatting\n */\nexport function printError(\n\terror: unknown,\n\tverbose = false,\n\tstream: NodeJS.WriteStream = process.stderr\n): void {\n\tstream.write(formatError(error, verbose));\n\tstream.write('\\n');\n}\n\n/**\n * Create error with common patterns\n */\nexport const createErrors = {\n\tspecNotFound(path: string, cause?: Error): ApicodegenError {\n\t\treturn new ApicodegenError({\n\t\t\tcode: ErrorCodes.SPEC_NOT_FOUND,\n\t\t\tmessage: 'OpenAPI spec file not found',\n\t\t\tlocation: path,\n\t\t\tsuggestions: [\n\t\t\t\t\"Check if the file exists using 'ls -la'\",\n\t\t\t\t'Use --spec to provide the correct path',\n\t\t\t\t'For remote specs, ensure the URL is accessible',\n\t\t\t],\n\t\t\tcause,\n\t\t});\n\t},\n\n\tspecFetchFailed(\n\t\turl: string,\n\t\tstatusCode?: number,\n\t\tcause?: Error\n\t): ApicodegenError {\n\t\tconst message = statusCode\n\t\t\t? `Failed to fetch OpenAPI spec (HTTP ${statusCode})`\n\t\t\t: 'Failed to fetch OpenAPI spec from URL';\n\n\t\treturn new ApicodegenError({\n\t\t\tcode: ErrorCodes.SPEC_FETCH_FAILED,\n\t\t\tmessage,\n\t\t\tlocation: url,\n\t\t\tsuggestions: [\n\t\t\t\t'Check if the URL is accessible in a browser',\n\t\t\t\t'Download the spec file locally and use the local path',\n\t\t\t\t'Verify CORS settings if fetching from a different origin',\n\t\t\t],\n\t\t\tcause,\n\t\t});\n\t},\n\n\tspecParseFailed(\n\t\tpath: string,\n\t\tline?: number,\n\t\tcolumn?: number,\n\t\tcause?: Error\n\t): ApicodegenError {\n\t\treturn new ApicodegenError({\n\t\t\tcode: ErrorCodes.SPEC_PARSE_FAILED,\n\t\t\tmessage: 'Failed to parse OpenAPI spec (invalid JSON or YAML)',\n\t\t\tlocation: path,\n\t\t\tline,\n\t\t\tcolumn,\n\t\t\tsuggestions: [\n\t\t\t\t'Validate JSON syntax using jsonlint.com',\n\t\t\t\t'For YAML specs, ensure proper indentation',\n\t\t\t\t'Check for trailing commas or unquoted special characters',\n\t\t\t],\n\t\t\tcause,\n\t\t});\n\t},\n\n\toutputDirMissing(path: string, cause?: Error): ApicodegenError {\n\t\treturn new ApicodegenError({\n\t\t\tcode: ErrorCodes.OUTPUT_DIR_MISSING,\n\t\t\tmessage: 'Output directory does not exist',\n\t\t\tlocation: path,\n\t\t\tsuggestions: [\n\t\t\t\t'Create the directory: mkdir -p $(dirname <output>)',\n\t\t\t\t'Check if the path is correct',\n\t\t\t],\n\t\t\tcause,\n\t\t});\n\t},\n\n\tconfigInvalid(path: string, cause?: Error): ApicodegenError {\n\t\treturn new ApicodegenError({\n\t\t\tcode: ErrorCodes.CONFIG_INVALID,\n\t\t\tmessage: 'Invalid configuration file',\n\t\t\tlocation: path,\n\t\t\tsuggestions: [\n\t\t\t\t'Validate JSON syntax in the config file',\n\t\t\t\t'Check for required fields (spec, output)',\n\t\t\t],\n\t\t\tcause,\n\t\t});\n\t},\n\n\tvalidationFailed(\n\t\tpath: string,\n\t\tdetails: string,\n\t\tcause?: Error\n\t): ApicodegenError {\n\t\treturn new ApicodegenError({\n\t\t\tcode: ErrorCodes.VALIDATION_FAILED,\n\t\t\tmessage: 'OpenAPI spec validation failed',\n\t\t\tlocation: path,\n\t\t\tpath: details,\n\t\t\tsuggestions: [\n\t\t\t\t'Check OpenAPI spec structure at the specified path',\n\t\t\t\t'Ensure all required fields are present',\n\t\t\t\t'Validate using swagger.io editor',\n\t\t\t],\n\t\t\tcause,\n\t\t});\n\t},\n\n\tgenerationFailed(cause?: Error): ApicodegenError {\n\t\treturn new ApicodegenError({\n\t\t\tcode: ErrorCodes.GENERATION_FAILED,\n\t\t\tmessage: 'Code generation failed',\n\t\t\tsuggestions: [\n\t\t\t\t'Check for unsupported OpenAPI features',\n\t\t\t\t'Ensure spec follows OpenAPI 2.0, 3.0, or 3.1 specification',\n\t\t\t\t'Use --verbose for more details',\n\t\t\t],\n\t\t\tcause,\n\t\t});\n\t},\n\n\ttypeCheckFailed(\n\t\tpath: string,\n\t\t_errors: string[],\n\t\tcause?: Error\n\t): ApicodegenError {\n\t\treturn new ApicodegenError({\n\t\t\tcode: ErrorCodes.TYPE_CHECK_FAILED,\n\t\t\tmessage: 'TypeScript type check failed',\n\t\t\tlocation: path,\n\t\t\tsuggestions: [\n\t\t\t\t'Review type errors above',\n\t\t\t\t'Check for schema inconsistencies',\n\t\t\t\t'Update generated types or fix source schema',\n\t\t\t],\n\t\t\tcause,\n\t\t});\n\t},\n\n\tmissingRequiredField(field: string, context?: string): ApicodegenError {\n\t\treturn new ApicodegenError({\n\t\t\tcode: ErrorCodes.VALIDATION_FAILED,\n\t\t\tmessage: `Missing required field: ${field}`,\n\t\t\tpath: context,\n\t\t\tsuggestions: [`Add the '${field}' field to your configuration`],\n\t\t});\n\t},\n};\n\n/**\n * Wrap unknown error in ApicodegenError if needed\n */\nexport function wrapError(\n\terror: unknown,\n\tcontext?: Partial<ApicodegenErrorContext>\n): ApicodegenError {\n\tif (error instanceof ApicodegenError) {\n\t\treturn error;\n\t}\n\n\tif (error instanceof Error) {\n\t\treturn new ApicodegenError({\n\t\t\tcode: context?.code || ErrorCodes.GENERATION_FAILED,\n\t\t\tmessage: context?.message || error.message,\n\t\t\tlocation: context?.location,\n\t\t\tsuggestions: context?.suggestions,\n\t\t\tcause: error,\n\t\t});\n\t}\n\n\treturn new ApicodegenError({\n\t\tcode: context?.code || ErrorCodes.GENERATION_FAILED,\n\t\tmessage: String(error),\n\t\tsuggestions: context?.suggestions,\n\t});\n}\n\n/**\n * Check if error is an ApicodegenError\n */\nexport function isApicodegenError(error: unknown): error is ApicodegenError {\n\treturn error instanceof ApicodegenError;\n}\n","const cyan = (s: string) => `\\x1b[36m${s}\\x1b[0m`;\nconst green = (s: string) => `\\x1b[32m${s}\\x1b[0m`;\nconst red = (s: string) => `\\x1b[31m${s}\\x1b[0m`;\nconst blue = (s: string) => `\\x1b[34m${s}\\x1b[0m`;\nconst yellow = (s: string) => `\\x1b[33m${s}\\x1b[0m`;\nconst magenta = (s: string) => `\\x1b[35m${s}\\x1b[0m`;\nconst gray = (s: string) => `\\x1b[90m${s}\\x1b[0m`;\nconst bold = (s: string) => `\\x1b[1m${s}\\x1b[0m`;\n\nexport const logger = {\n\tsuccess(msg: string): void {\n\t\tconsole.log(`${green('✓')} ${msg}`);\n\t},\n\n\terror(err: unknown, verbose = false): void {\n\t\tif (isApicodegenError(err)) {\n\t\t\tconsole.error(`${red('✗')} ${err.toString(verbose)}`);\n\t\t} else if (err instanceof Error) {\n\t\t\tconst msg = `Error: ${err.message}`;\n\t\t\tconsole.error(\n\t\t\t\t`${red('✗')} ${msg}${verbose && err.stack ? `\\n${gray(err.stack)}` : ''}`\n\t\t\t);\n\t\t} else {\n\t\t\tconsole.error(`${red('✗')} ${String(err)}`);\n\t\t}\n\t},\n\n\tinfo(msg: string): void {\n\t\tconsole.log(`${blue('ℹ')} ${msg}`);\n\t},\n\n\twarn(msg: string): void {\n\t\tconsole.log(`${yellow('⚠')} ${msg}`);\n\t},\n\n\tloading(msg: string): void {\n\t\tconsole.log(`${yellow('🔄')} ${msg}`);\n\t},\n\n\twatching(msg: string): void {\n\t\tconsole.log(`${magenta('⟳')} ${msg}`);\n\t},\n\n\tfileChange(filePath: string): void {\n\t\tconsole.log(`${yellow('↓')} ${filePath}`);\n\t},\n\n\tfileAdd(filePath: string): void {\n\t\tconsole.log(`${green('+')} ${filePath}`);\n\t},\n\n\tshutdown(): void {\n\t\tconsole.log(`\\n${gray('👋 Shutting down...')}`);\n\t},\n\n\tdivider(width = 50): void {\n\t\tconsole.log(`${bold(cyan('─'.repeat(width)))}`);\n\t},\n\n\theading(text: string, mode: string, width = 50): void {\n\t\tconsole.log(`${bold(cyan('─'.repeat(width)))}`);\n\t\tconsole.log(`${bold(cyan(text))}`);\n\t\tconsole.log(`${gray('Mode:')} ${mode || 'unknown'}`);\n\t\tconsole.log(`${bold(cyan('─'.repeat(width)))}`);\n\t},\n\n\titem(label: string, color: 'green' | 'red' | 'yellow' = 'green'): void {\n\t\tconst icon = color === 'green' ? '✓' : color === 'red' ? '✗' : '⚠';\n\t\tconsole.log(\n\t\t\t`${color === 'green' ? green(icon) : color === 'red' ? red(icon) : yellow(icon)} ${label}`\n\t\t);\n\t},\n\n\tsummary(stats: {\n\t\tsucceeded: number;\n\t\tfailed: number;\n\t\tendpoints: number;\n\t\tschemas: number;\n\t\tduration: number;\n\t}): void {\n\t\tconst { succeeded, failed, endpoints, schemas, duration } = stats;\n\t\tconst label = `API Code Gen - Complete (${succeeded} succeeded${\n\t\t\tfailed > 0 ? `, ${failed} failed` : ''\n\t\t}, ${endpoints} endpoints, ${schemas} schemas, ${duration}ms)`;\n\t\tif (failed === 0) {\n\t\t\tconsole.log(`${green('✓')} ${label}`);\n\t\t} else {\n\t\t\tconsole.log(`${yellow('⚠')} ${label}`);\n\t\t}\n\t},\n};\n\nimport { isApicodegenError } from './errors.js';\n","/**\n * @file Adapter abstract class definition\n * @author wp.l\n * @description Base adapter implementation for various code generation tools\n */\n\nimport type { Statement } from 'typescript';\nimport type { MediaTypeObject, ParameterObject } from '../interface.js';\n\n/**\n * Base adapter for tool\n * This abstract class serves as the foundation for implementing adapters for different code generation tools\n */\nexport abstract class Adapter {\n\t/**\n\t * @abstract The unique name/identifier for this adapter implementation\n\t */\n\tabstract readonly name: string;\n\n\t/**\n\t * @abstract The name of the field used to specify the HTTP method in API calls\n\t */\n\tabstract readonly methodFieldName: string;\n\n\t/**\n\t * @abstract The name of the field used to specify the request body in API calls\n\t */\n\tabstract readonly bodyFieldName: string;\n\n\t/**\n\t * @abstract The name of the field used to specify request headers in API calls\n\t */\n\tabstract readonly headersFieldName: string;\n\n\t/**\n\t * @abstract The name of the field used to specify query parameters in API calls\n\t */\n\tabstract readonly queryFieldName: string;\n\n\t/**\n\t * @abstract\n\t * @param {string} uri - The API endpoint URI\n\t * @param {string} method - The HTTP method (e.g., GET, POST, etc.)\n\t * @param {ParameterObject[]} parameters - An array of parameters for the API call\n\t * @param {MediaTypeObject | undefined} requestBody - The request body payload (if applicable)\n\t * @param {MediaTypeObject | undefined} response - The expected response format (if applicable)\n\t * @param {Adapter} adapter - An instance of the adapter being used\n\t * @param {boolean} useFormData - Flag indicating whether to use FormData for the request body\n\t * @param {boolean} useJSONResponse - Flag indicating whether the response should be parsed as JSON\n\t * @param {boolean} isEventStream - Flag indicating whether the response is a text/event-stream (SSE) stream; when true, the adapter must return the raw response without parsing\n\t * @returns {Statement[]} An array of TypeScript AST statements representing the generated code\n\t */\n\tabstract client(\n\t\turi: string,\n\t\tmethod: string,\n\t\tparameters: ParameterObject[],\n\t\trequestBody: MediaTypeObject | undefined,\n\t\tresponse: MediaTypeObject | undefined,\n\t\tadapter: Adapter,\n\t\tuseFormData: boolean,\n\t\tuseJSONResponse: boolean,\n\t\tisEventStream: boolean\n\t): Statement[];\n}\n","export const typescriptKeywords = new Set([\n\t'break',\n\t'case',\n\t'catch',\n\t'class',\n\t'const',\n\t'continue',\n\t'debugger',\n\t'default',\n\t'delete',\n\t'do',\n\t'else',\n\t'enum',\n\t'export',\n\t'extends',\n\t'false',\n\t'finally',\n\t'for',\n\t'function',\n\t'if',\n\t'import',\n\t'in',\n\t'instanceof',\n\t'new',\n\t'null',\n\t'return',\n\t'super',\n\t'switch',\n\t'this',\n\t'throw',\n\t'true',\n\t'try',\n\t'typeof',\n\t'var',\n\t'void',\n\t'while',\n\t'with',\n\t'as',\n\t'implements',\n\t'interface',\n\t'let',\n\t'package',\n\t'private',\n\t'protected',\n\t'public',\n\t'static',\n\t'yield',\n\t'abstract',\n\t'any',\n\t'async',\n\t'await',\n\t'constructor',\n\t'declare',\n\t'from',\n\t'get',\n\t'is',\n\t'module',\n\t'namespace',\n\t'never',\n\t'require',\n\t'set',\n\t'type',\n\t'unknown',\n\t'readonly',\n\t'of',\n\t'asserts',\n\t'infer',\n\t'keyof',\n\t'boolean',\n\t'number',\n\t'string',\n\t'symbol',\n\t'object',\n\t'undefined',\n\t'bigint',\n]);\n","/**\n * Simple represenration for JSON object\n */\nexport type JSONValue = {\n\t[K: string]:\n\t\t| string\n\t\t| number\n\t\t| boolean\n\t\t| JSONValue\n\t\t| (string | number | boolean | JSONValue)[];\n};\n\nexport enum SchemaType {\n\tschemas = 'schemas',\n\tparameters = 'parameters',\n\tresponses = 'responses',\n\trequestBodies = 'requestBodies',\n}\n\nexport enum NonArraySchemaType {\n\tobject = 'object',\n\tstring = 'string',\n\tnumber = 'number',\n\tboolean = 'boolean',\n\tinteger = 'integer',\n\tenum = 'enum',\n\tfile = 'file',\n}\n\nexport enum ArraySchemaType {\n\tarray = 'array',\n}\n\nexport enum SchemaFormatType {\n\tstring = 'string',\n\tnumber = 'number',\n\tboolean = 'boolean',\n\tfile = 'file',\n\tbinary = 'binary',\n\tblob = 'blob',\n}\n\nexport enum ParameterIn {\n\theader = 'header',\n\tbody = 'body',\n\tquery = 'query',\n\tcookie = 'cookie',\n\tpath = 'path',\n\tformData = 'formData',\n}\n\nexport interface ReferenceObject {\n\t$ref: string;\n}\n\nexport interface EnumSchemaObject {\n\tname: string;\n\tenum: (string | number)[];\n}\n\nexport interface SingleTypeSchemaObject {\n\t// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents\n\ttype: keyof typeof NonArraySchemaType | string;\n\tdescription?: string;\n\tallOf?: SchemaObject[];\n\tanyOf?: SchemaObject[];\n\tdeprecated?: boolean;\n\tenum?: (string | number)[];\n\tformat?: keyof typeof SchemaFormatType;\n\toneOf?: SchemaObject[];\n\tproperties?: Record<string, SchemaObject>;\n\treadonly?: boolean;\n\trequired?: string[] | boolean;\n\tref?: string;\n\tisRef?: boolean;\n}\n\nexport interface ArrayTypeSchemaObject {\n\ttype: keyof typeof ArraySchemaType;\n\titems?: SchemaObject;\n\trequired?: boolean;\n\tdescription?: string;\n\tref?: string;\n}\n\nexport type SchemaObject = SingleTypeSchemaObject | ArrayTypeSchemaObject;\n\nexport type ParameterObject = {\n\tname: string;\n\tin: keyof typeof ParameterIn;\n\tschema?: SchemaObject;\n\trequired?: boolean;\n\tdescription?: string;\n\tdeprecated?: boolean;\n\tref?: string;\n};\n\nexport enum MediaTypes {\n\tJSON = 'application/json',\n\tEVENT_STREAM = 'text/event-stream',\n\tTEXT = 'text',\n\tIMAGE = 'image',\n\tAUDIO = 'audio',\n\tVIDEO = 'video',\n}\n\nexport type MediaTypeObject = {\n\ttype: MediaTypes | keyof typeof MediaTypes;\n\tschema?: SchemaObject;\n};\n\nexport type ResponsesObject = Record<string, MediaTypeObject[]>;\n\nexport type RequestBodyObject = ResponsesObject;\n\nexport enum HttpMethods {\n\tGET = 'get',\n\tPUT = 'put',\n\tPOST = 'post',\n\tDELETE = 'delete',\n\tOPTIONS = 'options',\n\tHEAD = 'head',\n\tPATCH = 'patch',\n\tTRACE = 'trace',\n}\n\nexport type OperationObject = {\n\tmethod: string;\n\tsummary?: string;\n\tdescription?: string;\n\toperationId?: string;\n\texternalDocs?: { url: string; description?: string }[];\n\tparameters?: ParameterObject[];\n\trequestBody?: MediaTypeObject[];\n\tresponses: MediaTypeObject[];\n\tdeprecated?: boolean;\n};\n\nexport type PathObject = {\n\tref?: string;\n\tsummary?: string;\n\tdescription?: string;\n\tparameters?: ParameterObject[];\n} & Partial<Record<HttpMethods, OperationObject>>;\n\nexport type PathsObject = Record<string, OperationObject[]>;\n\nexport type FetchDocRequestInit = {\n\tmethod?: string;\n\tbody?: string | FormData;\n\theaders?: Record<string, string>;\n};\n\nexport enum Adaptors {\n\tfetch = 'fetch',\n\taxios = 'axios',\n}\n\nexport type ProviderInitOptions = {\n\tdocURL: string;\n\toutput: string;\n\tbaseURL?: string;\n\timportClientSource?: string;\n\trequestOptions?: FetchDocRequestInit;\n\tverbose?: boolean;\n\tadaptor?: keyof typeof Adaptors;\n};\n\nexport interface ProviderInitResult {\n\treadonly enums: EnumSchemaObject[];\n\treadonly schemas: Record<string, SchemaObject>;\n\treadonly parameters: Record<string, ParameterObject>;\n\treadonly responses: Record<string, ResponsesObject>;\n\treadonly requestBodies: Record<string, RequestBodyObject>;\n\treadonly apis: PathsObject;\n}\n","/**\n * @file Base class implementation\n * @author wp.l\n * @description Base utility class providing common methods for code generation and API handling\n */\n\nimport { Agent, request } from 'undici';\nimport { typescriptKeywords } from '../constants/keywords.js';\nimport type {\n\tEnumSchemaObject,\n\tFetchDocRequestInit,\n\tReferenceObject,\n\tSchemaObject,\n\tSingleTypeSchemaObject,\n} from '../interface.js';\nimport { MediaTypes } from '../interface.js';\n\n/**\n * Represents success HTTP status codes.\n * Each key is a string representation of a success HTTP status code.\n */\nexport const SuccessHttpStatusCode = {\n\t'200': '200', // OK\n\t'201': '201', // Created\n\t'202': '202', // Accepted\n\t'203': '203', // Non-Authoritative Information\n\t'204': '204', // No Content\n\t'205': '205', // Reset Content\n\t'206': '206', // Partial Content\n\t'207': '207', // Multi_Status\n\t'208': '208', // Already_Reported\n\t'226': '226', // IM Used\n};\n\n/**\n * Base abstract class providing common utility methods.\n */\nexport abstract class Base {\n\tprotected constructor() {\n\t\tif (new.target === Base) {\n\t\t\tthrow new Error('Cannot instantiate abstract class');\n\t\t}\n\t}\n\n\t/**\n\t * Converts a reference string to a meaningful name.\n\t * @param ref - The reference string to process.\n\t * @param [doc] - Optional document reference for context.\n\t * @returns - The processed name.\n\t */\n\tstatic ref2name(ref: string, doc?: any): string {\n\t\tconst paths = ref.replace(/^#/, '').split('/').filter(Boolean);\n\n\t\tif (!doc) {\n\t\t\treturn paths.slice(-1)[0];\n\t\t}\n\n\t\tlet temporary = doc as unknown;\n\t\tlet lastPath = '';\n\t\tfor (const path of paths) {\n\t\t\t// For handling path prefix with ~1\n\t\t\tconst adjustedPath = path.replaceAll('~1', '/');\n\t\t\ttemporary = (temporary as Record<string, any>)[adjustedPath];\n\t\t\tlastPath = adjustedPath;\n\t\t}\n\n\t\tif (!temporary) {\n\t\t\treturn 'unknown';\n\t\t}\n\n\t\treturn (temporary as unknown as { $ref: string }).$ref\n\t\t\t? Base.ref2name((temporary as unknown as { $ref: string }).$ref, doc)\n\t\t\t: lastPath;\n\t}\n\n\t/**\n\t * Converts an API path to a function name.\n\t * @param path - The API endpoint path.\n\t * @param [method] - The HTTP method (e.g., GET, POST).\n\t * @param [operationId] - Unique identifier for the operation.\n\t * @returns - The generated function name.\n\t */\n\tstatic pathToFnName(\n\t\tpath: string,\n\t\tmethod?: string,\n\t\t// eslint-disable-next-line @typescript-eslint/no-unused-vars\n\t\t_operationId: string = ''\n\t) {\n\t\tconst name = Base.normalize(Base.camelCase(Base.normalize(path)));\n\t\tconst suffix = method\n\t\t\t? Base.capitalize(Base.upperCamelCase(`using_${method}`))\n\t\t\t: '';\n\n\t\treturn name + suffix;\n\t}\n\n\t/**\n\t * Normalizes a string by replacing special characters and avoiding TypeScript keywords.\n\t * @param text - Input text to normalize.\n\t * @returns - The normalized string.\n\t */\n\tstatic normalize(text: string) {\n\t\tif (typescriptKeywords.has(text)) {\n\t\t\ttext += '_';\n\t\t}\n\t\treturn text\n\t\t\t.replace(/[/\\-_{}():\\s`,*<>$#.]/gm, '_')\n\t\t\t.replace(/^\\d./gm, '')\n\t\t\t.replaceAll('...', '');\n\t}\n\n\t/**\n\t * Capitalizes the first character of a string.\n\t * @param text - Input string.\n\t * @returns - Capitalized string.\n\t */\n\tstatic capitalize(text: string) {\n\t\ttext = text.trim();\n\t\treturn `${text.charAt(0).toUpperCase()}${text.slice(1)}`;\n\t}\n\n\t/**\n\t * Converts a string to camelCase.\n\t * @param text - Input string.\n\t * @returns - CamelCase string.\n\t */\n\tstatic camelCase(text: string) {\n\t\ttext = text.trim();\n\t\tconst parts = text.split('_').filter(Boolean);\n\t\twhile (parts[0]?.match(/^\\d/)) {\n\t\t\tparts.shift();\n\t\t}\n\t\treturn parts\n\t\t\t.map((t, index) => (index === 0 ? t : Base.capitalize(t)))\n\t\t\t.join('');\n\t}\n\n\t/**\n\t * Converts a string to UpperCamelCase.\n\t * @param text - Input string.\n\t * @returns - UpperCamelCase string.\n\t */\n\tstatic upperCamelCase(text: string) {\n\t\treturn Base.normalize(text)\n\t\t\t.replaceAll('...', '')\n\t\t\t.split('_')\n\t\t\t.filter(Boolean)\n\t\t\t.map(Base.capitalize)\n\t\t\t.join('');\n\t}\n\n\t/**\n\t * Fetches documentation from a given URL.\n\t * @param url - The URL to fetch the documentation from.\n\t * @param requestInit - Additional request parameters.\n\t * @returns - A promise resolving to the fetched documentation data.\n\t */\n\tstatic async fetchDoc<T = unknown>(\n\t\turl: string,\n\t\trequestInit: FetchDocRequestInit = {}\n\t): Promise<T> {\n\t\tconst agent = new Agent({\n\t\t\tconnect: { rejectUnauthorized: false },\n\t\t});\n\n\t\tconst { body, statusCode } = await request(url, {\n\t\t\tmethod: 'GET',\n\t\t\tdispatcher: agent,\n\t\t\t...requestInit,\n\t\t});\n\n\t\tif (statusCode >= 400) {\n\t\t\tthrow new Error(\n\t\t\t\t`Failed to fetch OpenAPI documentation from ${url}: HTTP ${statusCode}`\n\t\t\t);\n\t\t}\n\n\t\ttry {\n\t\t\treturn body.json() as T;\n\t\t} catch (error) {\n\t\t\tthrow new Error(\n\t\t\t\t`Failed to parse JSON response from ${url}: ${error instanceof Error ? error.message : String(error)}`\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Determines the media type from a given media type string.\n\t * @param mediaType - The media type string to evaluate.\n\t * @returns - The matched MediaTypes or null.\n\t */\n\tstatic getMediaType(mediaType: string): MediaTypes | undefined {\n\t\tconst mediaTypeValues = Object.values(MediaTypes) as string[];\n\t\tconst found = mediaTypeValues.find((type) => mediaType.includes(type));\n\t\treturn found as MediaTypes | undefined;\n\t}\n\n\t/**\n\t * Checks if a schema is a valid enum type that isn't boolean.\n\t * @param a - The schema object to evaluate.\n\t * @returns - True if the schema is a valid non-boolean enum.\n\t */\n\tstatic isValidEnumType(a: SchemaObject) {\n\t\treturn a.type !== 'boolean' && !Base.isBooleanEnum(a);\n\t}\n\n\t/**\n\t * Checks if a schema represents a boolean enum.\n\t * @param a - The schema object to evaluate.\n\t * @returns - True if the schema is a boolean enum.\n\t */\n\tstatic isBooleanEnum(a: SchemaObject) {\n\t\treturn (\n\t\t\ta.type === 'boolean' ||\n\t\t\t!!(a as SingleTypeSchemaObject).enum?.some(\n\t\t\t\t(member) => typeof member === 'boolean'\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Checks if two enum schemas are identical.\n\t * @param a - First enum schema to compare.\n\t * @param b - Second enum schema to compare.\n\t * @returns - True if the enums are identical.\n\t */\n\tprivate static isSameEnum(a: EnumSchemaObject, b: EnumSchemaObject) {\n\t\treturn (\n\t\t\ta.enum.length === b.enum.length &&\n\t\t\ta.enum.sort().every((v, index) => v === b.enum.sort()[index])\n\t\t);\n\t}\n\n\t/**\n\t * Filters out duplicate enum schemas from an array.\n\t * @param enums - Array of enum schemas to process.\n\t * @returns - Array of unique enum schemas.\n\t */\n\tstatic uniqueEnums(enums: EnumSchemaObject[]): EnumSchemaObject[] {\n\t\tconst enumMap = new Map<string, Set<string | number>>();\n\n\t\tfor (const e of enums) {\n\t\t\tconst existing = enumMap.get(e.name);\n\t\t\tif (existing) {\n\t\t\t\t// Merge enum values with the same name\n\t\t\t\tfor (const value of e.enum) {\n\t\t\t\t\texisting.add(value);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tenumMap.set(e.name, new Set(e.enum));\n\t\t\t}\n\t\t}\n\n\t\t// Convert back to array\n\t\treturn Array.from(enumMap.entries()).map(([name, values]) => ({\n\t\t\tname,\n\t\t\tenum: Array.from(values),\n\t\t}));\n\t}\n\n\t/**\n\t * Finds the first occurrence of a matching enum schema in an array.\n\t * @param a - The enum schema to find.\n\t * @param enums - Array of enum schemas to search.\n\t * @returns - The found schema or undefined.\n\t */\n\tstatic findSameSchema(a: EnumSchemaObject, enums: EnumSchemaObject[]) {\n\t\treturn enums.find((b) => Base.isSameEnum(b, a));\n\t}\n\n\t/**\n\t * Checks if an object is a reference object.\n\t * @param schema - The object to check.\n\t * @returns - True if the object is a reference.\n\t */\n\n\tstatic isRef(schema: unknown): schema is ReferenceObject {\n\t\treturn (\n\t\t\ttypeof schema === 'object' &&\n\t\t\tschema !== null &&\n\t\t\t'$ref' in schema &&\n\t\t\ttypeof (schema as Record<string, unknown>).$ref === 'string'\n\t\t);\n\t}\n}\n","/**\n * @file Base class implementation\n * @author wp.l\n * @description This file defines the `Provider` abstract class, which serves as a base for providers responsible for parsing and processing API documentation.\n */\n\nimport type {\n\tEnumSchemaObject,\n\tFetchDocRequestInit,\n\tOperationObject,\n\tParameterObject,\n\tProviderInitOptions,\n\tProviderInitResult,\n\tRequestBodyObject,\n\tResponsesObject,\n\tSchemaObject,\n} from '../interface.js';\n\n/**\n * Abstract Provider Class.\n *\n * The Provider class is designed to be extended by specific implementations (e.g., OpenAPI 2 provider, OpenAPI 3 provider).\n * It handles the initialization of the provider and the parsing of documentation into structured data.\n *\n * @example\n *\n * ```ts\n * /// Example of how this class might be used by a subclass:\n * class OpenAPIProvider extends Provider {\n * /// Implement the parse method to handle OpenAPI-specific documentation parsing.\n * parse(doc: unknown): ProviderInitResult {\n * /// Implementation details...\n * }\n * }\n *\n * /// Initializing a provider with configuration and documentation data:\n * const initOptions: ProviderInitOptions = {\n * docURL: \"https://example.com/api/swagger.json\",\n * baseURL: \"https://api.example.com\",\n * output: \"./generated\",\n * requestOptions: {\n * headers: { \"Content-Type\": \"application/json\" },\n * },\n * importClientSource: \"generated/client\",\n * };\n *\n * const docData = fetchSwaggerDoc();\n * const provider = new OpenAPIProvider(initOptions, docData);\n * ```\n */\nexport abstract class Provider\n\timplements ProviderInitResult, ProviderInitOptions\n{\n\t/** collection of enum schemas */\n\treadonly enums: EnumSchemaObject[] = [];\n\t/** collection of schemas indexed by name */\n\treadonly schemas: Record<string, SchemaObject> = {};\n\t/** collection of parameters indexed by name */\n\treadonly parameters: Record<string, ParameterObject> = {};\n\t/** collection of API responses indexed by name */\n\treadonly responses: Record<string, ResponsesObject> = {};\n\t/** collection of request bodies indexed by name */\n\treadonly requestBodies: Record<string, RequestBodyObject> = {};\n\t/** collection of API endpoints (operations) indexed by path */\n\treadonly apis: Record<string, OperationObject[]> = {};\n\n\t/** URL for fetching API documentation */\n\treadonly docURL: string;\n\t/** base URL for API endpoints */\n\treadonly baseURL: string;\n\t/** output directory for generated code */\n\treadonly output: string;\n\t/** request options for API documentation fetch */\n\treadonly requestOptions: FetchDocRequestInit;\n\t/** source path for imported client */\n\treadonly importClientSource: string;\n\n\t/**\n\t * Provider Constructor.\n\t * @param {ProviderInitOptions} initOptions - Initial configuration for the provider.\n\t * @param {unknown} doc - Raw API documentation data to be parsed.\n\t */\n\tconstructor(initOptions: ProviderInitOptions, doc: unknown) {\n\t\tthis.docURL = initOptions.docURL;\n\t\tthis.baseURL = initOptions.baseURL ?? '';\n\t\tthis.output = initOptions.output ?? '.';\n\t\tthis.requestOptions = initOptions.requestOptions ?? {};\n\t\tthis.importClientSource = initOptions.importClientSource ?? '';\n\n\t\tconst { enums, schemas, requestBodies, responses, parameters, apis } =\n\t\t\tthis.parse(doc);\n\n\t\tthis.enums = enums;\n\t\tthis.schemas = schemas;\n\t\tthis.responses = responses;\n\t\tthis.parameters = parameters;\n\t\tthis.requestBodies = requestBodies;\n\t\tthis.apis = apis;\n\t}\n\n\t/**\n\t * Abstract Parse Method.\n\t * @abstract\n\t * @param {unknown} doc - Raw API documentation data.\n\t * @returns {ProviderInitResult} - Parsed documentation data.\n\t *\n\t * This method must be implemented by subclasses to parse the raw documentation into structured data.\n\t */\n\tabstract parse(doc: unknown): ProviderInitResult;\n}\n","/* eslint-disable @typescript-eslint/no-unsafe-enum-comparison */\n/* eslint-disable no-case-declarations */\n\nimport { writeFile } from 'node:fs/promises';\nimport { format } from 'prettier';\nimport type {\n\tBindingElement,\n\tBlock,\n\tNode,\n\tParameterDeclaration,\n\tPropertySignature,\n\tStatement,\n\tTypeNode,\n} from 'typescript';\nimport {\n\taddSyntheticLeadingComment,\n\tcreatePrinter,\n\tNodeFlags,\n\tSyntaxKind,\n\tfactory as t,\n} from 'typescript';\nimport type { Adapter } from '../base/Adaptor.js';\nimport { Base } from '../base/Base.js';\nimport { ApicodegenError, ErrorCodes } from '../errors.js';\nimport type {\n\tArrayTypeSchemaObject,\n\tMediaTypeObject,\n\tParameterObject,\n\tProviderInitOptions,\n\tProviderInitResult,\n\tSchemaObject,\n\tSingleTypeSchemaObject,\n} from '../interface.js';\nimport {\n\tArraySchemaType,\n\tMediaTypes,\n\tNonArraySchemaType,\n\tParameterIn,\n\tSchemaFormatType,\n} from '../interface.js';\n\n/**\n * Represents a comment object with optional tag and message.\n */\nexport type CommentObject = {\n\ttag?: 'deprecated' | 'param' | 'returns';\n\tcomment: string;\n\tparamName?: string;\n\ttype?: string;\n};\n\n/**\n * Array of comment objects to be added to the code.\n */\nexport type Comments = CommentObject[];\n\nexport class Generator {\n\t/**\n\t * Converts an array of TypeScript statements into a formatted string of code.\n\t *\n\t * @param statements - The array of TypeScript statement nodes.\n\t * @returns Formatted code as a string.\n\t * @throws {Error} If no valid statements are provided.\n\t */\n\tstatic toCode(statements: Statement[]): string {\n\t\tif (statements.length === 0) {\n\t\t\treturn '// No api declaration found.';\n\t\t}\n\n\t\tconst sourceFile = t.createSourceFile(\n\t\t\tstatements,\n\t\t\tt.createToken(SyntaxKind.EndOfFileToken),\n\t\t\tNodeFlags.None\n\t\t);\n\n\t\treturn createPrinter().printFile(sourceFile);\n\t}\n\n\tstatic async write(code: string, filepath: string) {\n\t\tconst { mkdir } = await import('node:fs/promises');\n\t\tconst { dirname } = await import('node:path');\n\t\ttry {\n\t\t\tawait mkdir(dirname(filepath), { recursive: true });\n\t\t\tawait writeFile(filepath, code);\n\t\t} catch (error) {\n\t\t\tthrow new ApicodegenError({\n\t\t\t\tcode: ErrorCodes.OUTPUT_DIR_MISSING,\n\t\t\t\tmessage: 'Failed to write generated code to output file',\n\t\t\t\tlocation: filepath,\n\t\t\t\tcause: error instanceof Error ? error : new Error(String(error)),\n\t\t\t\tsuggestions: [\n\t\t\t\t\t'Verify the output directory path is writable',\n\t\t\t\t\t'Check that the parent directory exists or can be created',\n\t\t\t\t],\n\t\t\t});\n\t\t}\n\t}\n\n\t/**\n\t * Converts a path string with parameters into a TypeScript template expression.\n\t * Handles query parameters and path placeholders.\n\t *\n\t * @param path - The base path string containing placeholders.\n\t * @param parameters - Array of parameter objects defining the parameters.\n\t * @param basePath - Optional base path to prepend (default: \"\").\n\t * @returns A TypeScript template expressi\n\t */\n\tstatic toUrlTemplate(\n\t\tpath: string,\n\t\tparameters: ParameterObject[],\n\t\tbasePath = ''\n\t) {\n\t\t// Extract query parameters\n\t\tconst queryParameters = parameters.filter(\n\t\t\t(p) => p.in === ParameterIn.query\n\t\t);\n\n\t\tif (queryParameters.length > 0) {\n\t\t\tconst queryString = queryParameters\n\t\t\t\t.map(\n\t\t\t\t\t(qp, index) =>\n\t\t\t\t\t\t`${index === 0 ? '?' : '&'}${encodeURIComponent(qp.name)}={${Base.normalize(qp.name)}}`\n\t\t\t\t)\n\t\t\t\t.join('');\n\t\t\tpath += queryString;\n\t\t}\n\n\t\t// Split the path into segments\n\t\tconst pathSegments = path.replaceAll('{', '${').split('$').filter(Boolean);\n\n\t\t// If path segments only got one item, it means there are no parameters in path. So just return the path literal.\n\t\tif (pathSegments.length === 1) {\n\t\t\treturn t.createNoSubstitutionTemplateLiteral(basePath + path);\n\t\t}\n\n\t\treturn t.createTemplateExpression(\n\t\t\tt.createTemplateHead(basePath + pathSegments[0]),\n\t\t\tpathSegments.slice(1).map((segment, index) => {\n\t\t\t\tconst match = /^{(.+)}(.+)?/gm.exec(segment);\n\t\t\t\tconst isLastSegment = index === pathSegments.length - 2;\n\n\t\t\t\tif (!match) {\n\t\t\t\t\tthrow new Error(`Invalid path segment: ${segment}`);\n\t\t\t\t}\n\n\t\t\t\treturn t.createTemplateSpan(\n\t\t\t\t\tt.createIdentifier(Base.normalize(match[1])),\n\t\t\t\t\t!isLastSegment\n\t\t\t\t\t\t? t.createTemplateMiddle(match[2])\n\t\t\t\t\t\t: t.createTemplateTail(match[2] || '')\n\t\t\t\t);\n\t\t\t})\n\t\t);\n\t}\n\n\t/**\n\t * Adds synthetic comments to a TypeScript AST node.\n\t *\n\t * @param node - The target AST node.\n\t * @param comments - Array of comment objects to add.\n\t */\n\tstatic addComments(node: Node, comments: Comments) {\n\t\tif (!Array.isArray(comments) || comments.filter(Boolean).length === 0)\n\t\t\treturn;\n\n\t\tconst formatComment = (comment: CommentObject): string => {\n\t\t\tif (comment.tag === 'returns') {\n\t\t\t\treturn `* @returns {${comment.type}} ${comment.comment ?? ''}`;\n\t\t\t}\n\t\t\tif (comment.tag === 'param') {\n\t\t\t\treturn comment.comment\n\t\t\t\t\t? `* @param ${comment.paramName} - ${comment.comment}`\n\t\t\t\t\t: `* @param ${comment.paramName}`;\n\t\t\t}\n\t\t\tif (comment.tag) {\n\t\t\t\treturn `* @${comment.tag} ${comment.comment ?? ''}`;\n\t\t\t}\n\t\t\treturn `* ${comment.comment}`;\n\t\t};\n\n\t\tconst formattedComments =\n\t\t\tcomments.map(formatComment).join('\\n').trim() + '\\n';\n\n\t\taddSyntheticLeadingComment(\n\t\t\tnode,\n\t\t\tSyntaxKind.MultiLineCommentTrivia,\n\t\t\tformattedComments,\n\t\t\ttrue\n\t\t);\n\t}\n\n\t/**\n\t * Checks if a schema represents a binary type.\n\t *\n\t * @param schema - The schema object to check.\n\t * @returns true if the schema is a binary type, false otherwise.\n\t */\n\tstatic isBinarySchema(schema: SchemaObject): boolean {\n\t\tif (schema.type === 'array') {\n\t\t\tconst arraySchema = schema as ArrayTypeSchemaObject;\n\t\t\treturn Generator.isBinarySchema(arraySchema.items!);\n\t\t}\n\n\t\tconst nonArraySchema = schema as SingleTypeSchemaObject;\n\t\treturn (\n\t\t\tnonArraySchema.format === SchemaFormatType.blob ||\n\t\t\tnonArraySchema.format === SchemaFormatType.binary ||\n\t\t\tnonArraySchema.type === SchemaFormatType.file\n\t\t);\n\t}\n\n\tstatic schemaToTypeString(schema: SchemaObject): string {\n\t\tif (schema.type === 'array') {\n\t\t\tconst arraySchema = schema as ArrayTypeSchemaObject;\n\t\t\treturn arraySchema.items\n\t\t\t\t? `${Generator.schemaToTypeString(arraySchema.items)}[]`\n\t\t\t\t: 'unknown';\n\t\t}\n\t\tconst singleSchema = schema as SingleTypeSchemaObject;\n\t\tif (schema.type === 'string') return 'string';\n\t\tif (schema.type === 'number' || schema.type === 'integer') return 'number';\n\t\tif (schema.type === 'boolean') return 'boolean';\n\t\tif (\n\t\t\tschema.type === 'object' ||\n\t\t\t(schema as SingleTypeSchemaObject).properties\n\t\t)\n\t\t\treturn 'object';\n\t\tif (singleSchema.format === 'binary' || singleSchema.type === 'file')\n\t\t\treturn 'Blob';\n\t\tif (singleSchema.format === 'blob') return 'Blob';\n\t\tif (singleSchema.ref) return singleSchema.ref;\n\t\treturn 'unknown';\n\t}\n\n\tstatic generateParamTags(\n\t\tparameters: ParameterObject[],\n\t\trequestBody?: MediaTypeObject\n\t): CommentObject[] {\n\t\tconst tags: CommentObject[] = [];\n\n\t\tfor (const p of parameters) {\n\t\t\tconst paramName = Base.normalize(p.name);\n\t\t\tlet paramType = 'unknown';\n\n\t\t\tif (p.schema) {\n\t\t\t\tparamType = Generator.schemaToTypeString(p.schema);\n\t\t\t}\n\n\t\t\tconst isOptional = p.required === false;\n\t\t\ttags.push({\n\t\t\t\ttag: 'param',\n\t\t\t\tparamName: paramName,\n\t\t\t\ttype: `${paramType}${isOptional ? ' | undefined' : ''}`,\n\t\t\t\tcomment: p.description ?? '',\n\t\t\t});\n\t\t}\n\n\t\tif (requestBody?.schema && 'properties' in requestBody.schema) {\n\t\t\tconst properties = requestBody.schema.properties as Record<\n\t\t\t\tstring,\n\t\t\t\tSchemaObject\n\t\t\t>;\n\t\t\tconst required = requestBody.schema.required;\n\t\t\tconst requiredArray = Array.isArray(required) ? required : [];\n\t\t\tfor (const [key, schema] of Object.entries(properties ?? {})) {\n\t\t\t\tconst paramName = `req.${key}`;\n\t\t\t\tconst paramType = Generator.schemaToTypeString(schema);\n\t\t\t\tconst isOptional = !requiredArray.includes(key);\n\t\t\t\ttags.push({\n\t\t\t\t\ttag: 'param',\n\t\t\t\t\tparamName: paramName,\n\t\t\t\t\ttype: `${paramType}${isOptional ? ' | undefined' : ''}`,\n\t\t\t\t\tcomment: schema.description ?? '',\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\treturn tags;\n\t}\n\n\tstatic toRequestBodyTypeNode(schema: SchemaObject) {\n\t\treturn t.createParameterDeclaration(\n\t\t\tundefined,\n\t\t\tundefined,\n\t\t\tt.createIdentifier('req'),\n\t\t\tundefined,\n\t\t\tGenerator.toTypeNode(schema)\n\t\t);\n\t}\n\n\tstatic toTypeNode(schema: SchemaObject): TypeNode {\n\t\tconst { type, ref } = schema;\n\n\t\tif (ref) {\n\t\t\tconst identify = Base.ref2name(ref);\n\t\t\treturn t.createTypeReferenceNode(\n\t\t\t\tt.createIdentifier(\n\t\t\t\t\tidentify === 'unknown' ? identify : Base.upperCamelCase(identify)\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\tswitch (type) {\n\t\t\tcase ArraySchemaType.array: {\n\t\t\t