ng-openapi
Version:
Generate Angular services and TypeScript types from OpenAPI/Swagger specifications
1,597 lines (1,561 loc) • 82.1 kB
JavaScript
#!/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"
},
{