UNPKG

ng-openapi

Version:

Generate Angular services and TypeScript types from OpenAPI/Swagger specifications

1,597 lines (1,561 loc) 82.1 kB
#!/usr/bin/env node "use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); // src/lib/cli.ts var import_commander = require("commander"); var path11 = __toESM(require("path")); var fs4 = __toESM(require("fs")); // src/lib/core/generator.ts var import_ts_morph6 = require("ts-morph"); // src/lib/generators/type/type.generator.ts var import_ts_morph = require("ts-morph"); // ../shared/src/utils/string.utils.ts function camelCase(str) { const cleaned = str.replace(/[-_](\w)/g, (_, c) => c.toUpperCase()); return cleaned.charAt(0).toLowerCase() + cleaned.slice(1); } __name(camelCase, "camelCase"); function pascalCase(str) { return str.replace(/(?:^|[-_])([a-z])/g, (_, char) => char.toUpperCase()); } __name(pascalCase, "pascalCase"); // ../shared/src/utils/type.utils.ts function getTypeScriptType(schemaOrType, config, formatOrNullable, isNullable, context = "type") { let schema; let nullable; if (typeof schemaOrType === "string" || schemaOrType === void 0) { schema = { type: schemaOrType, format: typeof formatOrNullable === "string" ? formatOrNullable : void 0 }; nullable = typeof formatOrNullable === "boolean" ? formatOrNullable : isNullable; } else { schema = schemaOrType; nullable = typeof formatOrNullable === "boolean" ? formatOrNullable : schema.nullable; } if (!schema) { return "any"; } if (schema.$ref) { const refName = schema.$ref.split("/").pop(); return nullableType(pascalCase(refName), nullable); } if (schema.type === "array") { const itemType = schema.items ? getTypeScriptType(schema.items, config, void 0, void 0, context) : "unknown"; return nullable ? `(${itemType}[] | null)` : `${itemType}[]`; } switch (schema.type) { case "string": if (schema.enum) { return schema.enum.map((value) => typeof value === "string" ? `'${escapeString(value)}'` : String(value)).join(" | "); } if (schema.format === "date" || schema.format === "date-time") { const dateType = config.options.dateType === "Date" ? "Date" : "string"; return nullableType(dateType, nullable); } if (schema.format === "binary") { const binaryType = context === "type" ? "Blob" : "File"; return nullableType(binaryType, nullable); } if (schema.format === "uuid" || schema.format === "email" || schema.format === "uri" || schema.format === "hostname" || schema.format === "ipv4" || schema.format === "ipv6") { return nullableType("string", nullable); } return nullableType("string", nullable); case "number": case "integer": return nullableType("number", nullable); case "boolean": return nullableType("boolean", nullable); case "object": return nullableType(context === "type" ? "Record<string, unknown>" : "any", nullable); case "null": return "null"; default: console.warn(`Unknown swagger type: ${schema.type}`); return nullableType("any", nullable); } } __name(getTypeScriptType, "getTypeScriptType"); function nullableType(type, isNullable) { return type + (isNullable ? " | null" : ""); } __name(nullableType, "nullableType"); function escapeString(str) { return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'"); } __name(escapeString, "escapeString"); // ../shared/src/utils/functions/collect-used-types.ts function collectUsedTypes(operations) { const usedTypes = /* @__PURE__ */ new Set(); operations.forEach((operation) => { operation.parameters?.forEach((param) => { collectTypesFromSchema(param.schema || param, usedTypes); }); if (operation.requestBody) { collectTypesFromRequestBody(operation.requestBody, usedTypes); } if (operation.responses) { Object.values(operation.responses).forEach((response) => { collectTypesFromResponse(response, usedTypes); }); } }); return usedTypes; } __name(collectUsedTypes, "collectUsedTypes"); function collectTypesFromSchema(schema, usedTypes) { if (!schema) return; if (schema.$ref) { const refName = schema.$ref.split("/").pop(); if (refName) { usedTypes.add(pascalCase(refName)); } } if (schema.type === "array" && schema.items) { collectTypesFromSchema(schema.items, usedTypes); } if (schema.type === "object" && schema.properties) { Object.values(schema.properties).forEach((prop) => { collectTypesFromSchema(prop, usedTypes); }); } if (schema.allOf) { schema.allOf.forEach((subSchema) => { collectTypesFromSchema(subSchema, usedTypes); }); } if (schema.oneOf) { schema.oneOf.forEach((subSchema) => { collectTypesFromSchema(subSchema, usedTypes); }); } if (schema.anyOf) { schema.anyOf.forEach((subSchema) => { collectTypesFromSchema(subSchema, usedTypes); }); } } __name(collectTypesFromSchema, "collectTypesFromSchema"); function collectTypesFromRequestBody(requestBody, usedTypes) { const content = requestBody.content || {}; Object.values(content).forEach((mediaType) => { if (mediaType.schema) { collectTypesFromSchema(mediaType.schema, usedTypes); } }); } __name(collectTypesFromRequestBody, "collectTypesFromRequestBody"); function collectTypesFromResponse(response, usedTypes) { const content = response.content || {}; Object.values(content).forEach((mediaType) => { if (mediaType.schema) { collectTypesFromSchema(mediaType.schema, usedTypes); } }); } __name(collectTypesFromResponse, "collectTypesFromResponse"); // ../shared/src/utils/functions/token-names.ts function getClientContextTokenName(clientName = "default") { const clientSuffix = clientName.toUpperCase().replace(/[^A-Z0-9]/g, "_"); return `CLIENT_CONTEXT_TOKEN_${clientSuffix}`; } __name(getClientContextTokenName, "getClientContextTokenName"); function getBasePathTokenName(clientName = "default") { const clientSuffix = clientName.toUpperCase().replace(/[^A-Z0-9]/g, "_"); return `BASE_PATH_${clientSuffix}`; } __name(getBasePathTokenName, "getBasePathTokenName"); // ../shared/src/utils/functions/duplicate-function-name.ts function hasDuplicateFunctionNames(arr) { return new Set(arr.map((fn) => fn.getName())).size !== arr.length; } __name(hasDuplicateFunctionNames, "hasDuplicateFunctionNames"); // ../shared/src/utils/functions/extract-paths.ts function extractPaths(swaggerPaths = {}, methods = [ "get", "post", "put", "patch", "delete", "options", "head" ]) { const paths = []; Object.entries(swaggerPaths).forEach(([path12, pathItem]) => { methods.forEach((method) => { if (pathItem[method]) { const operation = pathItem[method]; paths.push({ path: path12, method: method.toUpperCase(), operationId: operation.operationId, summary: operation.summary, description: operation.description, tags: operation.tags || [], parameters: parseParameters(operation.parameters || [], pathItem.parameters || []), requestBody: operation.requestBody, responses: operation.responses || {} }); } }); }); return paths; } __name(extractPaths, "extractPaths"); function parseParameters(operationParams, pathParams) { const allParams = [ ...pathParams, ...operationParams ]; return allParams.map((param) => ({ name: param.name, in: param.in, required: param.required || param.in === "path", schema: param.schema, type: param.type, format: param.format, description: param.description })); } __name(parseParameters, "parseParameters"); // ../shared/src/utils/functions/extract-swagger-response-type.ts function getResponseTypeFromResponse(response, responseTypeMapping) { const content = response.content || {}; if (Object.keys(content).length === 0) { return "json"; } const responseTypes = []; for (const [contentType, mediaType] of Object.entries(content)) { const schema = mediaType?.schema; const mapping = responseTypeMapping || {}; if (mapping[contentType]) { responseTypes.push({ type: mapping[contentType], priority: 1, contentType }); continue; } if (schema?.format === "binary" || schema?.format === "byte") { responseTypes.push({ type: "blob", priority: 2, contentType }); continue; } if (schema?.type === "string" && (schema?.format === "binary" || schema?.format === "byte")) { responseTypes.push({ type: "blob", priority: 2, contentType }); continue; } const isPrimitive = isPrimitiveType(schema); const inferredType = inferResponseTypeFromContentType(contentType); let priority = 3; let finalType = inferredType; if (inferredType === "json" && isPrimitive) { finalType = "text"; priority = 2; } else if (inferredType === "json") { priority = 2; } responseTypes.push({ type: finalType, priority, contentType, isPrimitive }); } responseTypes.sort((a, b) => a.priority - b.priority); return responseTypes[0]?.type || "json"; } __name(getResponseTypeFromResponse, "getResponseTypeFromResponse"); function isPrimitiveType(schema) { if (!schema) return false; const primitiveTypes = [ "string", "number", "integer", "boolean" ]; if (primitiveTypes.includes(schema.type)) { return true; } if (schema.type === "array") { return false; } if (schema.type === "object" || schema.properties) { return false; } if (schema.$ref) { return false; } if (schema.allOf || schema.oneOf || schema.anyOf) { return false; } return false; } __name(isPrimitiveType, "isPrimitiveType"); function inferResponseTypeFromContentType(contentType) { const normalizedType = contentType.split(";")[0].trim().toLowerCase(); if (normalizedType.includes("json") || normalizedType === "application/ld+json" || normalizedType === "application/hal+json" || normalizedType === "application/vnd.api+json") { return "json"; } if (normalizedType.includes("xml") || normalizedType === "application/soap+xml" || normalizedType === "application/atom+xml" || normalizedType === "application/rss+xml") { return "text"; } if (normalizedType.startsWith("text/")) { const binaryTextTypes = [ "text/rtf", "text/cache-manifest", "text/vcard", "text/calendar" ]; if (binaryTextTypes.includes(normalizedType)) { return "blob"; } return "text"; } if (normalizedType === "application/x-www-form-urlencoded" || normalizedType === "multipart/form-data") { return "text"; } if (normalizedType === "application/javascript" || normalizedType === "application/typescript" || normalizedType === "application/css" || normalizedType === "application/yaml" || normalizedType === "application/x-yaml" || normalizedType === "application/toml") { return "text"; } if (normalizedType.startsWith("image/") || normalizedType.startsWith("audio/") || normalizedType.startsWith("video/") || normalizedType === "application/pdf" || normalizedType === "application/zip" || normalizedType.includes("octet-stream")) { return "arraybuffer"; } return "blob"; } __name(inferResponseTypeFromContentType, "inferResponseTypeFromContentType"); function getResponseType(response, config) { const responseType = getResponseTypeFromResponse(response); switch (responseType) { case "blob": return "Blob"; case "arraybuffer": return "ArrayBuffer"; case "text": return "string"; case "json": { const content = response.content || {}; for (const [contentType, mediaType] of Object.entries(content)) { if (inferResponseTypeFromContentType(contentType) === "json" && mediaType?.schema) { return getTypeScriptType(mediaType.schema, config, mediaType.schema.nullable); } } return "any"; } default: return "any"; } } __name(getResponseType, "getResponseType"); // ../shared/src/config/constants.ts var disableLinting = `/* @ts-nocheck */ /* eslint-disable */ /* @noformat */ /* @formatter:off */ `; var authorComment = `/** * Generated by ng-openapi `; var defaultHeaderComment = disableLinting + authorComment; var TYPE_GENERATOR_HEADER_COMMENT = defaultHeaderComment + `* Generated TypeScript interfaces from Swagger specification * Do not edit this file manually */ `; var SERVICE_INDEX_GENERATOR_HEADER_COMMENT = defaultHeaderComment + `* Generated service exports * Do not edit this file manually */ `; var SERVICE_GENERATOR_HEADER_COMMENT = /* @__PURE__ */ __name((controllerName) => defaultHeaderComment + `* Generated Angular service for ${controllerName} controller * Do not edit this file manually */ `, "SERVICE_GENERATOR_HEADER_COMMENT"); var MAIN_INDEX_GENERATOR_HEADER_COMMENT = defaultHeaderComment + `* Entrypoint for the client * Do not edit this file manually */ `; var PROVIDER_GENERATOR_HEADER_COMMENT = defaultHeaderComment + `* Generated provider functions for easy setup * Do not edit this file manually */ `; var BASE_INTERCEPTOR_HEADER_COMMENT = /* @__PURE__ */ __name((clientName) => defaultHeaderComment + `* Generated Base Interceptor for client ${clientName} * Do not edit this file manually */ `, "BASE_INTERCEPTOR_HEADER_COMMENT"); // ../shared/src/core/swagger-parser.ts var fs = __toESM(require("fs")); var path = __toESM(require("path")); var yaml = __toESM(require("js-yaml")); // ../shared/src/utils/functions/is-url.ts function isUrl(input) { try { const url = new URL(input); return [ "http:", "https:" ].includes(url.protocol); } catch { return false; } } __name(isUrl, "isUrl"); // ../shared/src/core/swagger-parser.ts var SwaggerParser = class _SwaggerParser { static { __name(this, "SwaggerParser"); } spec; constructor(spec, config) { const isInputValid = config.validateInput?.(spec) ?? true; if (!isInputValid) { throw new Error("Swagger spec is not valid. Check your `validateInput` condition."); } this.spec = spec; } static async create(swaggerPathOrUrl, config) { const swaggerContent = await _SwaggerParser.loadContent(swaggerPathOrUrl); const spec = _SwaggerParser.parseSpecContent(swaggerContent, swaggerPathOrUrl); return new _SwaggerParser(spec, config); } static async loadContent(pathOrUrl) { if (isUrl(pathOrUrl)) { return await _SwaggerParser.fetchUrlContent(pathOrUrl); } else { return fs.readFileSync(pathOrUrl, "utf8"); } } static async fetchUrlContent(url) { try { const response = await fetch(url, { method: "GET", headers: { Accept: "application/json, application/yaml, text/yaml, text/plain, */*", "User-Agent": "ng-openapi" }, // 30 second timeout signal: AbortSignal.timeout(3e4) }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const content = await response.text(); if (!content || content.trim() === "") { throw new Error(`Empty response from URL: ${url}`); } return content; } catch (error) { let errorMessage = `Failed to fetch content from URL: ${url}`; if (error.name === "AbortError") { errorMessage += " - Request timeout (30s)"; } else if (error.message) { errorMessage += ` - ${error.message}`; } throw new Error(errorMessage); } } static parseSpecContent(content, pathOrUrl) { let format; if (isUrl(pathOrUrl)) { const urlPath = new URL(pathOrUrl).pathname.toLowerCase(); if (urlPath.endsWith(".json")) { format = "json"; } else if (urlPath.endsWith(".yaml") || urlPath.endsWith(".yml")) { format = "yaml"; } else { format = _SwaggerParser.detectFormat(content); } } else { const extension = path.extname(pathOrUrl).toLowerCase(); switch (extension) { case ".json": format = "json"; break; case ".yaml": format = "yaml"; break; case ".yml": format = "yml"; break; default: format = _SwaggerParser.detectFormat(content); } } try { switch (format) { case "json": return JSON.parse(content); case "yaml": case "yml": return yaml.load(content); default: throw new Error(`Unable to determine format for: ${pathOrUrl}`); } } catch (error) { throw new Error(`Failed to parse ${format.toUpperCase()} content from: ${pathOrUrl}. Error: ${error instanceof Error ? error.message : error}`); } } static detectFormat(content) { const trimmed = content.trim(); if (trimmed.startsWith("{") || trimmed.startsWith("[")) { return "json"; } if (trimmed.includes("openapi:") || trimmed.includes("swagger:") || trimmed.includes("---") || /^[a-zA-Z][a-zA-Z0-9_]*\s*:/.test(trimmed)) { return "yaml"; } return "json"; } getDefinitions() { return this.spec.definitions || this.spec.components?.schemas || {}; } getDefinition(name) { const definitions = this.getDefinitions(); return definitions[name]; } resolveReference(ref) { const parts = ref.split("/"); const definitionName = parts[parts.length - 1]; return this.getDefinition(definitionName); } getAllDefinitionNames() { return Object.keys(this.getDefinitions()); } getSpec() { return this.spec; } getPaths() { return this.spec.paths || {}; } isValidSpec() { return !!(this.spec.swagger && this.spec.swagger.startsWith("2.") || this.spec.openapi && this.spec.openapi.startsWith("3.")); } getSpecVersion() { if (this.spec.swagger) { return { type: "swagger", version: this.spec.swagger }; } if (this.spec.openapi) { return { type: "openapi", version: this.spec.openapi }; } return null; } }; // src/lib/generators/type/type.generator.ts var TypeGenerator = class _TypeGenerator { static { __name(this, "TypeGenerator"); } project; parser; sourceFile; generatedTypes = /* @__PURE__ */ new Set(); config; constructor(parser, outputRoot, config) { this.config = config; const outputPath = outputRoot + "/models/index.ts"; this.project = new import_ts_morph.Project({ compilerOptions: { declaration: true, target: import_ts_morph.ScriptTarget.ES2022, module: import_ts_morph.ModuleKind.Preserve, strict: true, ...this.config.compilerOptions } }); this.parser = parser; this.sourceFile = this.project.createSourceFile(outputPath, "", { overwrite: true }); } static async create(swaggerPathOrUrl, outputRoot, config) { const parser = await SwaggerParser.create(swaggerPathOrUrl, config); return new _TypeGenerator(parser, outputRoot, config); } generate() { try { const definitions = this.parser.getDefinitions(); if (!definitions || Object.keys(definitions).length === 0) { console.warn("No definitions found in swagger file"); return; } this.sourceFile.insertText(0, TYPE_GENERATOR_HEADER_COMMENT); Object.entries(definitions).forEach(([name, definition]) => { this.generateInterface(name, definition); }); this.sourceFile.formatText(); this.sourceFile.saveSync(); } catch (error) { console.error("Error in generate():", error); throw new Error(`Failed to generate types: ${error instanceof Error ? error.message : "Unknown error"}`); } } generateInterface(name, definition) { const interfaceName = this.pascalCaseForEnums(name); if (this.generatedTypes.has(interfaceName)) { return; } this.generatedTypes.add(interfaceName); if (definition.enum) { this.generateEnum(interfaceName, definition); return; } if (definition.allOf) { this.generateCompositeType(interfaceName, definition); return; } const interfaceDeclaration = this.sourceFile.addInterface({ name: interfaceName, isExported: true, docs: definition.description ? [ definition.description ] : void 0 }); this.addInterfaceProperties(interfaceDeclaration, definition); } generateEnum(name, definition) { if (!definition.enum?.length) return; const isStringEnum = definition.enum.some((value) => typeof value === "string"); if (isStringEnum) { const unionType = definition.enum.map((value) => typeof value === "string" ? `'${this.escapeString(value)}'` : String(value)).join(" | "); this.sourceFile.addTypeAlias({ name, type: unionType, isExported: true, docs: definition.description ? [ definition.description ] : void 0 }); } else if (definition.description && this.config.options.generateEnumBasedOnDescription) { const enumDeclaration = this.sourceFile.addEnum({ name, isExported: true }); try { const enumValueObjects = JSON.parse(definition.description); enumValueObjects.forEach((enumValueObject) => { enumDeclaration.addMember({ name: enumValueObject.Name, value: enumValueObject.Value }); }); } catch (e) { console.error(`Failed to parse enum description for ${name}`); definition.enum.forEach((value) => { const enumKey = this.toEnumKey(value); enumDeclaration.addMember({ name: enumKey, value }); }); } } else { const enumDeclaration = this.sourceFile.addEnum({ name, isExported: true, docs: definition.description ? [ definition.description ] : void 0 }); definition.enum.forEach((value) => { const enumKey = this.toEnumKey(value); enumDeclaration.addMember({ name: enumKey, value }); }); } } generateCompositeType(name, definition) { let typeExpression = ""; if (definition.allOf) { const types = definition.allOf.map((def) => this.resolveSwaggerType(def)).filter((type) => type !== "any" && type !== "unknown"); typeExpression = types.length > 0 ? types.join(" & ") : "Record<string, unknown>"; } this.sourceFile.addTypeAlias({ name, type: typeExpression, isExported: true, docs: definition.description ? [ definition.description ] : void 0 }); } addInterfaceProperties(interfaceDeclaration, definition) { if (!definition.properties && definition.additionalProperties === false) { interfaceDeclaration.addIndexSignature({ keyName: "key", keyType: "string", returnType: "never" }); return; } if (!definition.properties && definition.additionalProperties === true) { interfaceDeclaration.addIndexSignature({ keyName: "key", keyType: "string", returnType: "any" }); return; } if (!definition.properties) { console.warn(`No properties found for interface ${interfaceDeclaration.getName()}`); return; } Object.entries(definition.properties).forEach(([propertyName, property]) => { const isRequired = definition.required?.includes(propertyName) ?? false; const isReadOnly = property.readOnly; const propertyType = this.resolveSwaggerType(property); const sanitizedName = this.sanitizePropertyName(propertyName); interfaceDeclaration.addProperty({ name: sanitizedName, type: propertyType, isReadonly: isReadOnly, hasQuestionToken: !isRequired, docs: property.description ? [ property.description ] : void 0 }); }); } resolveSwaggerType(schema) { if (schema.$ref) { return this.resolveReference(schema.$ref); } if (schema.enum) { return schema.enum.map((value) => typeof value === "string" ? `'${this.escapeString(value)}'` : String(value)).join(" | "); } if (schema.allOf) { return schema.allOf.map((def) => this.resolveSwaggerType(def)).filter((type) => type !== "any" && type !== "unknown").join(" & ") || "Record"; } if (schema.oneOf) { return schema.oneOf.map((def) => this.resolveSwaggerType(def)).filter((type) => type !== "any" && type !== "unknown").join(" | ") || "unknown"; } if (schema.anyOf) { return schema.anyOf.map((def) => this.resolveSwaggerType(def)).filter((type) => type !== "any" && type !== "unknown").join(" | ") || "unknown"; } if (schema.type === "array") { const itemType = schema.items ? this.getArrayItemType(schema.items) : "unknown"; return `${itemType}[]`; } if (schema.type === "object") { if (schema.properties) { return this.generateInlineObjectType(schema); } if (schema.additionalProperties) { const valueType = typeof schema.additionalProperties === "object" ? this.resolveSwaggerType(schema.additionalProperties) : "unknown"; return `Record<string, ${valueType}>`; } return "Record<string, unknown>"; } return this.mapSwaggerTypeToTypeScript(schema.type, schema.format, schema.nullable); } generateInlineObjectType(definition) { if (!definition.properties) { if (definition.additionalProperties) { const additionalType = typeof definition.additionalProperties === "object" ? this.resolveSwaggerType(definition.additionalProperties) : "unknown"; return `Record<string, ${additionalType}>`; } return "Record<string, unknown>"; } const properties = Object.entries(definition.properties).map(([key, prop]) => { const isRequired = definition.required?.includes(key) ?? false; const questionMark = isRequired ? "" : "?"; const sanitizedKey = this.sanitizePropertyName(key); return `${sanitizedKey}${questionMark}: ${this.resolveSwaggerType(prop)}`; }).join("; "); return `{ ${properties} }`; } resolveReference(ref) { try { const refDefinition = this.parser.resolveReference(ref); const refName = ref.split("/").pop(); if (!refName) { console.warn(`Invalid reference format: ${ref}`); return "unknown"; } const typeName = this.pascalCaseForEnums(refName); if (refDefinition && !this.generatedTypes.has(typeName)) { this.generateInterface(refName, refDefinition); } return typeName; } catch (error) { console.warn(`Failed to resolve reference ${ref}:`, error); return "unknown"; } } mapSwaggerTypeToTypeScript(type, format, isNullable) { if (!type) return "unknown"; switch (type) { case "string": if (format === "date" || format === "date-time") { const dateType = this.config.options.dateType === "Date" ? "Date" : "string"; return this.nullableType(dateType, isNullable); } if (format === "binary") return "Blob"; if (format === "uuid") return "string"; if (format === "email") return "string"; if (format === "uri") return "string"; return this.nullableType("string", isNullable); case "number": case "integer": return this.nullableType("number", isNullable); case "boolean": return this.nullableType("boolean", isNullable); case "array": return this.nullableType("unknown[]", isNullable); case "object": return this.nullableType("Record<string, unknown>", isNullable); case "null": return this.nullableType("null", isNullable); default: console.warn(`Unknown swagger type: ${type}`); return this.nullableType("unknown", isNullable); } } nullableType(type, isNullable) { return type + (isNullable ? " | null" : ""); } pascalCaseForEnums(str) { return str.replace(/[^a-zA-Z0-9]/g, "_").replace(/(?:^|_)([a-z])/g, (_, char) => char.toUpperCase()).replace(/^([0-9])/, "_$1"); } sanitizePropertyName(name) { if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name)) { return `"${name}"`; } return name; } toEnumKey(value) { return value.toString().replace(/[^a-zA-Z0-9]/g, "_").replace(/^([0-9])/, "_$1").toUpperCase(); } getArrayItemType(items) { if (Array.isArray(items)) { const types = items.map((item) => this.resolveSwaggerType(item)); return `[${types.join(", ")}]`; } else { return this.resolveSwaggerType(items); } } escapeString(str) { return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'"); } }; // src/lib/generators/utility/token.generator.ts var import_ts_morph2 = require("ts-morph"); var path2 = __toESM(require("path")); var TokenGenerator = class { static { __name(this, "TokenGenerator"); } project; clientName; constructor(project, clientName = "default") { this.project = project; this.clientName = clientName; } generate(outputDir) { const tokensDir = path2.join(outputDir, "tokens"); const filePath = path2.join(tokensDir, "index.ts"); const sourceFile = this.project.createSourceFile(filePath, "", { overwrite: true }); sourceFile.addImportDeclaration({ namedImports: [ "InjectionToken" ], moduleSpecifier: "@angular/core" }); sourceFile.addImportDeclaration({ namedImports: [ "HttpInterceptor", "HttpContextToken" ], moduleSpecifier: "@angular/common/http" }); const basePathTokenName = this.getBasePathTokenName(); const interceptorsTokenName = this.getInterceptorsTokenName(); const clientContextTokenName = this.getClientContextTokenName(); sourceFile.addVariableStatement({ isExported: true, declarationKind: import_ts_morph2.VariableDeclarationKind.Const, declarations: [ { name: basePathTokenName, initializer: `new InjectionToken<string>('${basePathTokenName}', { providedIn: 'root', factory: () => '/api', // Default fallback })` } ], leadingTrivia: `/** * Injection token for the ${this.clientName} client base API path */ ` }); sourceFile.addVariableStatement({ isExported: true, declarationKind: import_ts_morph2.VariableDeclarationKind.Const, declarations: [ { name: interceptorsTokenName, initializer: `new InjectionToken<HttpInterceptor[]>('${interceptorsTokenName}', { providedIn: 'root', factory: () => [], // Default empty array })` } ], leadingTrivia: `/** * Injection token for the ${this.clientName} client HTTP interceptor instances */ ` }); sourceFile.addVariableStatement({ isExported: true, declarationKind: import_ts_morph2.VariableDeclarationKind.Const, declarations: [ { name: clientContextTokenName, initializer: `new HttpContextToken<string>(() => '${this.clientName}')` } ], leadingTrivia: `/** * HttpContext token to identify requests belonging to the ${this.clientName} client */ ` }); if (this.clientName === "default") { sourceFile.addVariableStatement({ isExported: true, declarationKind: import_ts_morph2.VariableDeclarationKind.Const, declarations: [ { name: "BASE_PATH", initializer: basePathTokenName } ], leadingTrivia: `/** * @deprecated Use ${basePathTokenName} instead */ ` }); sourceFile.addVariableStatement({ isExported: true, declarationKind: import_ts_morph2.VariableDeclarationKind.Const, declarations: [ { name: "CLIENT_CONTEXT_TOKEN", initializer: clientContextTokenName } ], leadingTrivia: `/** * @deprecated Use ${clientContextTokenName} instead */ ` }); } sourceFile.formatText(); sourceFile.saveSync(); } getBasePathTokenName() { const clientSuffix = this.clientName.toUpperCase().replace(/[^A-Z0-9]/g, "_"); return `BASE_PATH_${clientSuffix}`; } getInterceptorsTokenName() { const clientSuffix = this.clientName.toUpperCase().replace(/[^A-Z0-9]/g, "_"); return `HTTP_INTERCEPTORS_${clientSuffix}`; } getClientContextTokenName() { const clientSuffix = this.clientName.toUpperCase().replace(/[^A-Z0-9]/g, "_"); return `CLIENT_CONTEXT_TOKEN_${clientSuffix}`; } }; // src/lib/generators/utility/file-download.generator.ts var path3 = __toESM(require("path")); var FileDownloadGenerator = class { static { __name(this, "FileDownloadGenerator"); } project; constructor(project) { this.project = project; } generate(outputDir) { const utilsDir = path3.join(outputDir, "utils"); const filePath = path3.join(utilsDir, "file-download.ts"); const sourceFile = this.project.createSourceFile(filePath, "", { overwrite: true }); sourceFile.addImportDeclaration({ namedImports: [ "Observable" ], moduleSpecifier: "rxjs" }); sourceFile.addImportDeclaration({ namedImports: [ "tap" ], moduleSpecifier: "rxjs/operators" }); sourceFile.addFunction({ name: "downloadFile", isExported: true, parameters: [ { name: "blob", type: "Blob" }, { name: "filename", type: "string" }, { name: "mimeType", type: "string", hasQuestionToken: true } ], returnType: "void", statements: ` // Create a temporary URL for the blob const url = window.URL.createObjectURL(blob); // Create a temporary anchor element and trigger download const link = document.createElement('a'); link.href = url; link.download = filename; // Append to body, click, and remove document.body.appendChild(link); link.click(); document.body.removeChild(link); // Clean up the URL window.URL.revokeObjectURL(url);` }); sourceFile.addFunction({ name: "downloadFileOperator", isExported: true, typeParameters: [ { name: "T", constraint: "Blob" } ], parameters: [ { name: "filename", type: "string | ((blob: T) => string)" }, { name: "mimeType", type: "string", hasQuestionToken: true } ], returnType: "(source: Observable<T>) => Observable<T>", statements: ` return (source: Observable<T>) => { return source.pipe( tap((blob: T) => { const actualFilename = typeof filename === 'function' ? filename(blob) : filename; downloadFile(blob, actualFilename, mimeType); }) ); };` }); sourceFile.addFunction({ name: "extractFilenameFromContentDisposition", isExported: true, parameters: [ { name: "contentDisposition", type: "string | null" }, { name: "fallbackFilename", type: "string", initializer: '"download"' } ], returnType: "string", statements: ` if (!contentDisposition) { return fallbackFilename; } // Try to extract filename from Content-Disposition header // Supports both "filename=" and "filename*=" formats const filenameMatch = contentDisposition.match(/filename\\*?=['"]?([^'"\\n;]+)['"]?/i); if (filenameMatch && filenameMatch[1]) { // Decode if it's RFC 5987 encoded (filename*=UTF-8''...) const filename = filenameMatch[1]; if (filename.includes("''")) { const parts = filename.split("''"); if (parts.length === 2) { try { return decodeURIComponent(parts[1]); } catch { return parts[1]; } } } return filename; } return fallbackFilename;` }); sourceFile.formatText(); sourceFile.saveSync(); } }; // src/lib/generators/utility/date-transformer.generator.ts var import_ts_morph3 = require("ts-morph"); var path4 = __toESM(require("path")); var DateTransformerGenerator = class { static { __name(this, "DateTransformerGenerator"); } project; constructor(project) { this.project = project; } generate(outputDir) { const utilsDir = path4.join(outputDir, "utils"); const filePath = path4.join(utilsDir, "date-transformer.ts"); const sourceFile = this.project.createSourceFile(filePath, "", { overwrite: true }); sourceFile.addImportDeclaration({ namedImports: [ "HttpInterceptor", "HttpRequest", "HttpHandler", "HttpEvent", "HttpResponse" ], moduleSpecifier: "@angular/common/http" }); sourceFile.addImportDeclaration({ namedImports: [ "Injectable" ], moduleSpecifier: "@angular/core" }); sourceFile.addImportDeclaration({ namedImports: [ "Observable" ], moduleSpecifier: "rxjs" }); sourceFile.addImportDeclaration({ namedImports: [ "map" ], moduleSpecifier: "rxjs/operators" }); sourceFile.addVariableStatement({ isExported: true, declarationKind: import_ts_morph3.VariableDeclarationKind.Const, declarations: [ { name: "ISO_DATE_REGEX", initializer: "/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{3})?Z?$/" } ] }); sourceFile.addFunction({ name: "transformDates", isExported: true, parameters: [ { name: "obj", type: "any" } ], returnType: "any", statements: ` if (obj === null || obj === undefined || typeof obj !== 'object') { return obj; } if (obj instanceof Date) { return obj; } if (Array.isArray(obj)) { return obj.map(item => transformDates(item)); } if (typeof obj === 'object') { const transformed: any = {}; for (const key of Object.keys(obj)) { const value = obj[key]; if (typeof value === 'string' && ISO_DATE_REGEX.test(value)) { transformed[key] = new Date(value); } else { transformed[key] = transformDates(value); } } return transformed; } return obj;` }); sourceFile.addClass({ name: "DateInterceptor", isExported: true, decorators: [ { name: "Injectable", arguments: [] } ], implements: [ "HttpInterceptor" ], methods: [ { name: "intercept", parameters: [ { name: "req", type: "HttpRequest<any>" }, { name: "next", type: "HttpHandler" } ], returnType: "Observable<HttpEvent<any>>", statements: ` return next.handle(req).pipe( map(event => { if (event instanceof HttpResponse && event.body) { return event.clone({ body: transformDates(event.body) }); } return event; }) );` } ] }); sourceFile.formatText(); sourceFile.saveSync(); } }; // src/lib/generators/utility/main-index.generator.ts var path5 = __toESM(require("path")); var MainIndexGenerator = class { static { __name(this, "MainIndexGenerator"); } project; config; constructor(project, config) { this.project = project; this.config = config; } generateMainIndex(outputRoot) { const indexPath = path5.join(outputRoot, "index.ts"); const sourceFile = this.project.createSourceFile(indexPath, "", { overwrite: true }); sourceFile.insertText(0, MAIN_INDEX_GENERATOR_HEADER_COMMENT); sourceFile.addExportDeclaration({ moduleSpecifier: "./models" }); if (this.config.options.generateServices !== false) { sourceFile.addExportDeclaration({ moduleSpecifier: "./tokens" }); sourceFile.addExportDeclaration({ moduleSpecifier: "./providers" }); sourceFile.addExportDeclaration({ moduleSpecifier: "./services" }); sourceFile.addExportDeclaration({ moduleSpecifier: "./utils/file-download" }); if (this.config.options.dateType === "Date") { sourceFile.addExportDeclaration({ moduleSpecifier: "./utils/date-transformer" }); } } sourceFile.formatText(); sourceFile.saveSync(); } }; // src/lib/generators/utility/provider.generator.ts var path6 = __toESM(require("path")); var ProviderGenerator = class { static { __name(this, "ProviderGenerator"); } project; config; clientName; constructor(project, config) { this.project = project; this.config = config; this.clientName = config.clientName || "default"; } generate(outputDir) { const filePath = path6.join(outputDir, "providers.ts"); const sourceFile = this.project.createSourceFile(filePath, "", { overwrite: true }); sourceFile.insertText(0, PROVIDER_GENERATOR_HEADER_COMMENT); const basePathTokenName = this.getBasePathTokenName(); const interceptorsTokenName = this.getInterceptorsTokenName(); const baseInterceptorClassName = `${this.capitalizeFirst(this.clientName)}BaseInterceptor`; sourceFile.addImportDeclarations([ { namedImports: [ "EnvironmentProviders", "Provider", "makeEnvironmentProviders" ], moduleSpecifier: "@angular/core" }, { namedImports: [ "HTTP_INTERCEPTORS", "HttpInterceptor" ], moduleSpecifier: "@angular/common/http" }, { namedImports: [ basePathTokenName, interceptorsTokenName ], moduleSpecifier: "./tokens" }, { namedImports: [ baseInterceptorClassName ], moduleSpecifier: "./utils/base-interceptor" } ]); if (this.config.options.dateType === "Date") { sourceFile.addImportDeclaration({ namedImports: [ "DateInterceptor" ], moduleSpecifier: "./utils/date-transformer" }); } sourceFile.addInterface({ name: `${this.capitalizeFirst(this.clientName)}Config`, isExported: true, docs: [ `Configuration options for ${this.clientName} client` ], properties: [ { name: "basePath", type: "string", docs: [ "Base API URL" ] }, { name: "enableDateTransform", type: "boolean", hasQuestionToken: true, docs: [ "Enable automatic date transformation (default: true)" ] }, { name: "interceptors", type: "(new (...args: HttpInterceptor[]) => HttpInterceptor)[]", hasQuestionToken: true, docs: [ "Array of HTTP interceptor classes to apply to this client" ] } ] }); this.addMainProviderFunction(sourceFile, basePathTokenName, interceptorsTokenName, baseInterceptorClassName); sourceFile.formatText(); sourceFile.saveSync(); } addMainProviderFunction(sourceFile, basePathTokenName, interceptorsTokenName, baseInterceptorClassName) { const hasDateInterceptor = this.config.options.dateType === "Date"; const functionName = `provide${this.capitalizeFirst(this.clientName)}Client`; const configTypeName = `${this.capitalizeFirst(this.clientName)}Config`; const functionBody = ` const providers: Provider[] = [ // Base path token for this client { provide: ${basePathTokenName}, useValue: config.basePath }, // Base interceptor that handles client-specific interceptors { provide: HTTP_INTERCEPTORS, useClass: ${baseInterceptorClassName}, multi: true } ]; // Add client-specific interceptor instances if (config.interceptors && config.interceptors.length > 0) { const interceptorInstances = config.interceptors.map(InterceptorClass => new InterceptorClass()); ${hasDateInterceptor ? `// Add date interceptor if enabled (default: true) if (config.enableDateTransform !== false) { interceptorInstances.unshift(new DateInterceptor()); }` : `// Date transformation not available (dateType: 'string' was used in generation)`} providers.push({ provide: ${interceptorsTokenName}, useValue: interceptorInstances }); } ${hasDateInterceptor ? `else if (config.enableDateTransform !== false) { // Only date interceptor enabled providers.push({ provide: ${interceptorsTokenName}, useValue: [new DateInterceptor()] }); }` : ``} else { // No interceptors providers.push({ provide: ${interceptorsTokenName}, useValue: [] }); } return makeEnvironmentProviders(providers);`; sourceFile.addFunction({ name: functionName, isExported: true, docs: [ `Provides configuration for ${this.clientName} client`, "", "@example", "```typescript", "// In your app.config.ts", `import { ${functionName} } from './api/providers';`, "", "export const appConfig: ApplicationConfig = {", " providers: [", ` ${functionName}({`, " basePath: 'https://api.example.com',", " interceptors: [AuthInterceptor, LoggingInterceptor] // Classes, not instances", " }),", " // other providers...", " ]", "};", "```" ], parameters: [ { name: "config", type: configTypeName } ], returnType: "EnvironmentProviders", statements: functionBody }); if (this.clientName === "default") { sourceFile.addFunction({ name: "provideNgOpenapi", isExported: true, docs: [ "@deprecated Use provideDefaultClient instead for better clarity", "Provides configuration for the default client" ], parameters: [ { name: "config", type: configTypeName } ], returnType: "EnvironmentProviders", statements: `return ${functionName}(config);` }); } } getBasePathTokenName() { const clientSuffix = this.clientName.toUpperCase().replace(/[^A-Z0-9]/g, "_"); return `BASE_PATH_${clientSuffix}`; } getInterceptorsTokenName() { const clientSuffix = this.clientName.toUpperCase().replace(/[^A-Z0-9]/g, "_"); return `HTTP_INTERCEPTORS_${clientSuffix}`; } capitalizeFirst(str) { return str.charAt(0).toUpperCase() + str.slice(1); } }; // src/lib/generators/utility/base-interceptor.generator.ts var import_ts_morph4 = require("ts-morph"); var path7 = __toESM(require("path")); var BaseInterceptorGenerator = class { static { __name(this, "BaseInterceptorGenerator"); } #project; #clientName; constructor(project, clientName = "default") { this.#project = project; this.#clientName = clientName; } generate(outputDir) { const utilsDir = path7.join(outputDir, "utils"); const filePath = path7.join(utilsDir, "base-interceptor.ts"); const sourceFile = this.#project.createSourceFile(filePath, "", { overwrite: true }); sourceFile.insertText(0, BASE_INTERCEPTOR_HEADER_COMMENT(this.#clientName)); const interceptorsTokenName = this.getInterceptorsTokenName(); const clientContextTokenName = this.getClientContextTokenName(); sourceFile.addImportDeclarations([ { namedImports: [ "HttpEvent", "HttpHandler", "HttpInterceptor", "HttpRequest", "HttpContextToken" ], moduleSpecifier: "@angular/common/http" }, { namedImports: [ "inject", "Injectable" ], moduleSpecifier: "@angular/core" }, { namedImports: [ "Observable" ], moduleSpecifier: "rxjs" }, {