UNPKG

@zimic/http

Version:

Next-gen TypeScript-first HTTP utilities

1,216 lines (1,204 loc) 52.6 kB
import path from 'path'; import ts3 from 'typescript'; import color3 from 'picocolors'; import fs from 'fs'; // ../zimic-utils/dist/data.mjs function isDefined(value) { return value !== void 0 && value !== null; } var isDefined_default = isDefined; function isNonEmpty(value) { return isDefined_default(value) && value !== ""; } var isNonEmpty_default = isNonEmpty; function isNeverType(type) { return type.kind === ts3.SyntaxKind.NeverKeyword; } function isUnknownType(type) { return type.kind === ts3.SyntaxKind.UnknownKeyword; } function isNullType(type) { return type.kind === ts3.SyntaxKind.NullKeyword; } function createBlobType() { return ts3.factory.createTypeReferenceNode("Blob"); } function createNullType() { return ts3.factory.createLiteralTypeNode(ts3.factory.createNull()); } function createImportSpecifier(importName) { return ts3.factory.createImportSpecifier(false, void 0, ts3.factory.createIdentifier(importName)); } function createImportDeclaration(importSpecifiers, moduleName, options) { return ts3.factory.createImportDeclaration( void 0, ts3.factory.createImportClause( ts3.SyntaxKind.TypeKeyword , void 0, ts3.factory.createNamedImports(importSpecifiers) ), ts3.factory.createStringLiteral(moduleName) ); } // src/types/schema.ts var HTTP_METHODS = Object.freeze(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]); // ../zimic-utils/dist/logging.mjs var Logger = class _Logger { prefix; raw; constructor(options = {}) { const { prefix } = options; this.prefix = prefix; this.raw = prefix ? new _Logger({ ...options, prefix: void 0 }) : this; } logWithLevel(level, ...messages) { if (this.prefix) { console[level](this.prefix, ...messages); } else { console[level](...messages); } } info(...messages) { this.logWithLevel("log", ...messages); } warn(...messages) { this.logWithLevel("warn", ...messages); } error(...messages) { this.logWithLevel("error", ...messages); } table(headers, rows) { const columnLengths = headers.map((header) => { let maxValueLength = header.title.length; for (const row of rows) { const value = row[header.property]; if (value.length > maxValueLength) { maxValueLength = value.length; } } return maxValueLength; }); const formattedRows = []; const horizontalLine = columnLengths.map((length) => "\u2500".repeat(length)); formattedRows.push(horizontalLine, []); for (let headerIndex = 0; headerIndex < headers.length; headerIndex++) { const header = headers[headerIndex]; const columnLength = columnLengths[headerIndex]; const value = header.title; formattedRows.at(-1)?.push(value.padEnd(columnLength, " ")); } formattedRows.push(horizontalLine); for (const row of rows) { formattedRows.push([]); for (let headerIndex = 0; headerIndex < headers.length; headerIndex++) { const header = headers[headerIndex]; const columnLength = columnLengths[headerIndex]; const value = row[header.property]; formattedRows.at(-1)?.push(value.padEnd(columnLength, " ")); } } formattedRows.push(horizontalLine); const formattedTable = formattedRows.map((row, index) => { const isFirstLine = index === 0; if (isFirstLine) { return `\u250C\u2500${row.join("\u2500\u252C\u2500")}\u2500\u2510`; } const isLineAfterHeaders = index === 2; if (isLineAfterHeaders) { return `\u251C\u2500${row.join("\u2500\u253C\u2500")}\u2500\u2524`; } const isLastLine = index === formattedRows.length - 1; if (isLastLine) { return `\u2514\u2500${row.join("\u2500\u2534\u2500")}\u2500\u2518`; } return `\u2502 ${row.join(" \u2502 ")} \u2502`; }).join("\n"); this.logWithLevel("log", formattedTable); } }; var Logger_default = Logger; var logger = new Logger_default({ prefix: color3.cyan("[@zimic/http]") }); function createOperationsIdentifierText(serviceName) { return `${serviceName}Operations`; } function createOperationsIdentifier(serviceName) { return ts3.factory.createIdentifier(createOperationsIdentifierText(serviceName)); } function isOperationsDeclaration(node) { return node !== void 0 && ts3.isInterfaceDeclaration(node) && node.name.text === "operations"; } function isOperation(node) { return ts3.isPropertySignature(node) && (ts3.isIdentifier(node.name) || ts3.isStringLiteral(node.name)) && node.type !== void 0 && ts3.isTypeLiteralNode(node.type); } function normalizeOperation(operation, context) { if (!isOperation(operation)) { return void 0; } const newType = normalizeTypeLiteralMethodType(operation.type, context); return ts3.factory.updatePropertySignature( operation, operation.modifiers, operation.name, operation.questionToken, newType ); } function normalizeOperations(operations, context) { const newIdentifier = createOperationsIdentifier(context.serviceName); const newMembers = operations.members.map((operation) => normalizeOperation(operation, context)).filter(isDefined_default); return ts3.factory.updateInterfaceDeclaration( operations, operations.modifiers, newIdentifier, operations.typeParameters, operations.heritageClauses, newMembers ); } function removeOperationIfUnreferenced(operation, context) { if (!isOperation(operation)) { return void 0; } const operationName = operation.name.text; const isReferenced = context.referencedTypes.operations.has(operationName); if (isReferenced) { context.referencedTypes.operations.delete(operationName); return operation; } return void 0; } function removeUnreferencedOperations(operations, context) { const newMembers = operations.members.map((operation) => removeOperationIfUnreferenced(operation, context)).filter(isDefined_default); context.referencedTypes.operations.clear(); if (newMembers.length === 0) { return void 0; } return ts3.factory.updateInterfaceDeclaration( operations, operations.modifiers, operations.name, operations.typeParameters, operations.heritageClauses, newMembers ); } // src/typegen/openapi/transform/methods.ts function isMethod(node) { return ts3.isPropertySignature(node) && ts3.isIdentifier(node.name) && node.type !== void 0 && (ts3.isTypeLiteralNode(node.type) || ts3.isIndexedAccessTypeNode(node.type)); } function isMethodMember(node) { return ts3.isPropertySignature(node) && ts3.isIdentifier(node.name) && node.type !== void 0 && (ts3.isTypeLiteralNode(node.type) || ts3.isIndexedAccessTypeNode(node.type) || isNeverType(node.type)); } function isRequestMember(node) { return ts3.isPropertySignature(node) && ts3.isIdentifier(node.name) && node.type !== void 0 && !isNeverType(node.type); } function isRequestHeaders(node) { return isRequestMember(node) && node.name.text === "headers" && ts3.isTypeLiteralNode(node.type); } function isNormalizedRequestHeaders(node) { return isRequestMember(node) && node.name.text === "headers" && ts3.isTypeLiteralNode(node.type); } function isRequestParameters(node) { return isRequestMember(node) && node.name.text === "parameters" && ts3.isTypeLiteralNode(node.type); } function isContentPropertySignature(node) { return isRequestMember(node) && node.name.text === "content" && ts3.isTypeLiteralNode(node.type); } function isContentMember(node) { return ts3.isPropertySignature(node) && (ts3.isIdentifier(node.name) || ts3.isStringLiteral(node.name)) && node.type !== void 0 && !isNeverType(node.type); } function isResponse(node) { return ts3.isPropertySignature(node) && (ts3.isIdentifier(node.name) || ts3.isStringLiteral(node.name) || ts3.isNumericLiteral(node.name)) && node.type !== void 0; } function removeRedundantNullUnionIfNecessary(type) { const containsRedundantNullUnion = ts3.isUnionTypeNode(type) && type.types.some((type2) => { const isNull = ts3.isLiteralTypeNode(type2) && isNullType(type2.literal); return isNull; }) && type.types.some((type2) => { return ts3.isParenthesizedTypeNode(type2) && ts3.isUnionTypeNode(type2.type) && type2.type.types.some((subType) => { const isNull = ts3.isLiteralTypeNode(subType) && isNullType(subType.literal); return isNull; }); }); if (!containsRedundantNullUnion) { return type; } const typesWithoutRedundantNullUnion = type.types.filter((type2) => { const isNull = ts3.isLiteralTypeNode(type2) && isNullType(type2.literal); return !isNull; }).flatMap((type2) => { if (ts3.isParenthesizedTypeNode(type2) && ts3.isUnionTypeNode(type2.type)) { return type2.type.types; } return [type2]; }); return ts3.factory.createUnionTypeNode(typesWithoutRedundantNullUnion); } function wrapFormDataContentType(type, context) { context.typeImports.http.add("HttpFormData"); return ts3.factory.createTypeReferenceNode(ts3.factory.createIdentifier("HttpFormData"), [ renameComponentReferences(type, context) ]); } function wrapURLEncodedContentType(type, context) { context.typeImports.http.add("HttpSearchParams"); return ts3.factory.createTypeReferenceNode(ts3.factory.createIdentifier("HttpSearchParams"), [ renameComponentReferences(type, context) ]); } function normalizeRequestBodyMember(requestBodyMember, context, options) { if (!isContentMember(requestBodyMember)) { return void 0; } const newIdentifier = ts3.factory.createIdentifier("body"); const contentType = requestBodyMember.name.text; let newType = removeRedundantNullUnionIfNecessary(renameComponentReferences(requestBodyMember.type, context)); if (contentType === "multipart/form-data") { newType = wrapFormDataContentType(newType, context); } else if (contentType === "x-www-form-urlencoded") { newType = wrapURLEncodedContentType(newType, context); } return { contentTypeName: contentType, propertySignature: ts3.factory.updatePropertySignature( requestBodyMember, requestBodyMember.modifiers, newIdentifier, options.questionToken, newType ) }; } function normalizeHeaders(headers) { const newHeaderMembers = headers.members.filter((header) => { if (ts3.isIndexSignatureDeclaration(header)) { return false; } if (ts3.isPropertySignature(header)) { return header.type !== void 0 && !isUnknownType(header.type); } return true; }); if (newHeaderMembers.length === 0) { return void 0; } return ts3.factory.updateTypeLiteralNode(headers, ts3.factory.createNodeArray(newHeaderMembers)); } function normalizeRequestHeaders(requestHeader) { if (!isRequestHeaders(requestHeader)) { return void 0; } const newType = normalizeHeaders(requestHeader.type); if (!newType) { return void 0; } return ts3.factory.updatePropertySignature( requestHeader, requestHeader.modifiers, requestHeader.name, requestHeader.questionToken, newType ); } function createHeaderForUnionByContentType(existingHeader, contentTypeName) { const existingHeaderMembers = existingHeader?.type.members ?? []; const contentTypeIdentifier = ts3.factory.createIdentifier('"content-type"'); const contentTypeValue = ts3.factory.createLiteralTypeNode(ts3.factory.createStringLiteral(contentTypeName)); const newHeaderType = ts3.factory.createTypeLiteralNode([ ts3.factory.createPropertySignature(void 0, contentTypeIdentifier, void 0, contentTypeValue), ...existingHeaderMembers ]); return ts3.factory.createPropertySignature( existingHeader?.modifiers, ts3.factory.createIdentifier("headers"), void 0, newHeaderType ); } function normalizeContentType(contentType, context, options) { const { bodyQuestionToken } = options; if (ts3.isIndexedAccessTypeNode(contentType)) { return renameComponentReferences(contentType, context); } if (!ts3.isTypeLiteralNode(contentType)) { return contentType; } const newHeader = contentType.members.map(normalizeRequestHeaders).find(isDefined_default); const newBodyMembers = contentType.members.flatMap((body) => { if (isContentPropertySignature(body)) { return body.type.members.map((member) => normalizeRequestBodyMember(member, context, { questionToken: bodyQuestionToken })).filter(isDefined_default); } return []; }); if (newBodyMembers.length === 0) { const newMembers = [newHeader].filter(isDefined_default); return ts3.factory.updateTypeLiteralNode(contentType, ts3.factory.createNodeArray(newMembers)); } else { const bodyMemberUnionTypes = newBodyMembers.map((bodyMember) => { const headerMember = createHeaderForUnionByContentType(newHeader, bodyMember.contentTypeName); return ts3.factory.createTypeLiteralNode([headerMember, bodyMember.propertySignature]); }); return ts3.factory.createUnionTypeNode(bodyMemberUnionTypes); } } function normalizeRequest(request, context) { const newIdentifier = ts3.factory.createIdentifier("request"); const newType = normalizeContentType(request.type, context, { bodyQuestionToken: request.questionToken }); if (request.questionToken && ts3.isIndexedAccessTypeNode(request.type) && ts3.isLiteralTypeNode(request.type.indexType) && (ts3.isIdentifier(request.type.indexType.literal) || ts3.isStringLiteral(request.type.indexType.literal))) { const referencedComponentName = request.type.indexType.literal.text; context.pendingActions.components.requests.toMarkBodyAsOptional.add(referencedComponentName); } return ts3.factory.updatePropertySignature(request, request.modifiers, newIdentifier, void 0, newType); } function normalizeResponseType(responseType, context, options) { const { questionToken } = options; if (!ts3.isTypeLiteralNode(responseType)) { return responseType; } return normalizeContentType(responseType, context, { bodyQuestionToken: questionToken }); } var NON_NUMERIC_RESPONSE_STATUS_TO_MAPPED_TYPE = { default: "HttpStatusCode", "1xx": "HttpStatusCode.Information", "2xx": "HttpStatusCode.Success", "3xx": "HttpStatusCode.Redirection", "4xx": "HttpStatusCode.ClientError", "5xx": "HttpStatusCode.ServerError" }; function normalizeResponse(response, context, options = {}) { const { isComponent: isComponent2 = false } = options; if (!isResponse(response)) { return void 0; } const newType = normalizeResponseType(response.type, context, { questionToken: response.questionToken }); const statusCodeOrComponentName = response.name.text; const isNumericStatusCode = /^\d+$/.test(statusCodeOrComponentName); const shouldReuseIdentifier = isComponent2 || isNumericStatusCode; let newSignature; if (shouldReuseIdentifier) { newSignature = ts3.factory.updatePropertySignature( response, response.modifiers, response.name, response.questionToken, newType ); } else { const statusCode = statusCodeOrComponentName.toLowerCase(); const mappedType = NON_NUMERIC_RESPONSE_STATUS_TO_MAPPED_TYPE[statusCode]; if (!mappedType) { logger.warn( `Warning: Response has a non-standard status code: ${color3.yellow(response.name.text)}. Consider replacing it with a number (e.g. '200'), a pattern ('1xx', '2xx', '3xx', '4xx', or '5xx'), or 'default'.` ); return void 0; } context.typeImports.http.add("HttpStatusCode"); const newIdentifier = ts3.factory.createIdentifier(`[StatusCode in ${mappedType}]`); newSignature = ts3.factory.updatePropertySignature( response, response.modifiers, newIdentifier, response.questionToken, newType ); } return { newSignature, statusCode: { value: statusCodeOrComponentName, isNumeric: isNumericStatusCode } }; } function normalizeResponses(responses, context) { if (isNeverType(responses.type) || !ts3.isTypeLiteralNode(responses.type)) { return void 0; } const newIdentifier = ts3.factory.createIdentifier("response"); const newQuestionToken = void 0; const newMembers = responses.type.members.map((response) => normalizeResponse(response, context), context).filter(isDefined_default); const sortedNewMembers = Array.from(newMembers).sort((response, otherResponse) => { return response.statusCode.value.localeCompare(otherResponse.statusCode.value); }); const isEveryStatusCodeNumeric = sortedNewMembers.every((response) => response.statusCode.isNumeric); let newType; if (isEveryStatusCodeNumeric) { newType = ts3.factory.updateTypeLiteralNode( responses.type, ts3.factory.createNodeArray(sortedNewMembers.map((response) => response.newSignature)) ); } else { context.typeImports.http.add("MergeHttpResponsesByStatusCode"); const typeMembersToMerge = sortedNewMembers.reduce( (members, response) => { if (response.statusCode.isNumeric) { members.numeric.push(response.newSignature); } else { members.nonNumeric.push(response.newSignature); } return members; }, { numeric: [], nonNumeric: [] } ); const numericTypeLiteral = ts3.factory.createTypeLiteralNode(typeMembersToMerge.numeric); const nonNumericTypeLiterals = typeMembersToMerge.nonNumeric.map( (response) => ts3.factory.createTypeLiteralNode([response]) ); const mergeWrapper = ts3.factory.createIdentifier("MergeHttpResponsesByStatusCode"); newType = ts3.factory.createTypeReferenceNode(mergeWrapper, [ ts3.factory.createTupleTypeNode([numericTypeLiteral, ...nonNumericTypeLiterals]) ]); } return ts3.factory.updatePropertySignature( responses, responses.modifiers, newIdentifier, newQuestionToken, renameComponentReferences(newType, context) ); } function normalizeMethodMember(methodMember, context) { if (isMethodMember(methodMember)) { if (methodMember.name.text === "requestBody") { return normalizeRequest(methodMember, context); } if (methodMember.name.text === "responses") { return normalizeResponses(methodMember, context); } return methodMember; } return void 0; } function normalizeRequestQueryWithParameters(requestMember, context) { const newIdentifier = ts3.factory.createIdentifier("searchParams"); const newQuestionToken = void 0; const newType = renameComponentReferences(requestMember.type, context); return ts3.factory.updatePropertySignature( requestMember, requestMember.modifiers, newIdentifier, newQuestionToken, newType ); } function normalizeRequestHeadersWithParameters(requestMember, context) { const newIdentifier = ts3.factory.createIdentifier("headers"); const newQuestionToken = void 0; const newType = renameComponentReferences(requestMember.type, context); return ts3.factory.updatePropertySignature( requestMember, requestMember.modifiers, newIdentifier, newQuestionToken, newType ); } function normalizeRequestMemberWithParameters(requestMember, context) { if (!isRequestMember(requestMember) || requestMember.name.text === "path") { return void 0; } if (requestMember.name.text === "query") { return normalizeRequestQueryWithParameters(requestMember, context); } if (requestMember.name.text === "header") { return normalizeRequestHeadersWithParameters(requestMember, context); } return requestMember; } function mergeRequestHeadersMember(headers, otherHeaders) { const newType = ts3.factory.updateTypeLiteralNode( headers.type, ts3.factory.createNodeArray([...otherHeaders.type.members, ...headers.type.members]) ); return ts3.factory.updatePropertySignature( headers, headers.modifiers, headers.name, headers.questionToken, newType ); } function mergeRequestHeadersMembers(members) { let mergedHeaders; let firstHeadersIndex; const mergedHeadersMembers = members.map((member, index) => { if (!member || !isNormalizedRequestHeaders(member)) { return member; } if (firstHeadersIndex === void 0 || !mergedHeaders) { firstHeadersIndex = index; mergedHeaders = member; return member; } mergedHeaders = mergeRequestHeadersMember(mergedHeaders, member); return void 0; }); if (firstHeadersIndex !== void 0) { mergedHeadersMembers[firstHeadersIndex] = mergedHeaders; } return mergedHeadersMembers.filter(isDefined_default); } function mergeRequestAndParameterTypes(requestType, methodMembers, context) { const parameters = methodMembers.find(isRequestParameters); const parametersMembers = parameters ? parameters.type.members : []; const requestMembers = ts3.isTypeLiteralNode(requestType) ? requestType.members : []; const newMembers = mergeRequestHeadersMembers( [...parametersMembers, ...requestMembers].map((member) => { return normalizeRequestMemberWithParameters(member, context); }) ); if (newMembers.length === 0) { return void 0; } return ts3.factory.createTypeLiteralNode(newMembers); } function normalizeRequestTypeWithParameters(requestType, methodMembers, context) { if (ts3.isUnionTypeNode(requestType)) { const newTypes = requestType.types.map((type) => normalizeRequestTypeWithParameters(type, methodMembers, context)).filter(isDefined_default); return ts3.factory.updateUnionTypeNode(requestType, ts3.factory.createNodeArray(newTypes)); } if (ts3.isIndexedAccessTypeNode(requestType)) { const newType = normalizeRequestTypeWithParameters(ts3.factory.createTypeLiteralNode([]), methodMembers, context); return ts3.factory.createIntersectionTypeNode([requestType, newType].filter(isDefined_default)); } return mergeRequestAndParameterTypes(requestType, methodMembers, context); } function normalizeMethodMemberWithParameters(methodMember, methodMembers, context) { if (!ts3.isIdentifier(methodMember.name) || !methodMember.type) { return void 0; } if (methodMember.name.text === "request") { const newType = normalizeRequestTypeWithParameters(methodMember.type, methodMembers, context); if (!newType) { return void 0; } return ts3.factory.updatePropertySignature( methodMember, methodMember.modifiers, methodMember.name, void 0, newType ); } if (methodMember.name.text === "response") { return methodMember; } return void 0; } function normalizeTypeLiteralMethodType(methodType, context) { const newMembers = methodType.members.map((member) => normalizeMethodMember(member, context)).filter(isDefined_default).map((member, _index, partialMembers) => normalizeMethodMemberWithParameters(member, partialMembers, context)).filter(isDefined_default); return ts3.factory.updateTypeLiteralNode(methodType, ts3.factory.createNodeArray(newMembers)); } function normalizeIndexedAccessMethodType(methodType, context) { const isOperationsReference = ts3.isTypeReferenceNode(methodType.objectType) && ts3.isIdentifier(methodType.objectType.typeName) && methodType.objectType.typeName.text === "operations"; if (!isOperationsReference) { return methodType; } const newIdentifier = createOperationsIdentifier(context.serviceName); const newObjectType = ts3.factory.createTypeReferenceNode(newIdentifier, methodType.objectType.typeArguments); const hasIndexTypeName = ts3.isLiteralTypeNode(methodType.indexType) && (ts3.isIdentifier(methodType.indexType.literal) || ts3.isStringLiteral(methodType.indexType.literal)); if (hasIndexTypeName) { const operationName = methodType.indexType.literal.text; context.referencedTypes.operations.add(operationName); } return ts3.factory.updateIndexedAccessTypeNode(methodType, newObjectType, methodType.indexType); } function normalizeMethod(method, context, options) { if (!isMethod(method)) { return void 0; } const methodName = method.name.text.toUpperCase(); if (!HTTP_METHODS.includes(methodName)) { return void 0; } const matchesPositiveFilters = context.filters.paths.positive.length === 0 || context.filters.paths.positive.some( (filter) => filter.methodRegex.test(methodName) && filter.pathRegex.test(options.pathName) ); if (!matchesPositiveFilters) { return void 0; } const matchesNegativeFilters = context.filters.paths.negative.length > 0 && context.filters.paths.negative.some( (filter) => filter.methodRegex.test(methodName) && filter.pathRegex.test(options.pathName) ); if (matchesNegativeFilters) { return void 0; } const newIdentifier = ts3.factory.createIdentifier(methodName); const newType = ts3.isTypeLiteralNode(method.type) ? normalizeTypeLiteralMethodType(method.type, context) : normalizeIndexedAccessMethodType(method.type, context); return ts3.factory.updatePropertySignature(method, method.modifiers, newIdentifier, method.questionToken, newType); } function createPathsIdentifier(serviceName) { return ts3.factory.createIdentifier(`${serviceName}Schema`); } function isPathsDeclaration(node) { return node !== void 0 && (ts3.isInterfaceDeclaration(node) || ts3.isTypeAliasDeclaration(node)) && node.name.text === "paths"; } function isPath(node) { return ts3.isPropertySignature(node) && (ts3.isIdentifier(node.name) || ts3.isStringLiteral(node.name)) && node.type !== void 0 && (ts3.isTypeLiteralNode(node.type) || ts3.isIndexedAccessTypeNode(node.type)); } function normalizePathNameWithParameters(pathName) { return pathName.replace(/{([^}]+)}/g, ":$1"); } function normalizePath(path3, context, options = {}) { const { isComponent: isComponent2 = false } = options; if (!isPath(path3)) { return void 0; } const newPathName = isComponent2 ? path3.name.text : normalizePathNameWithParameters(path3.name.text); const newIdentifier = isComponent2 ? path3.name : ts3.factory.createStringLiteral(newPathName); let newType; if (ts3.isTypeLiteralNode(path3.type)) { const newMethods = path3.type.members.map((method) => normalizeMethod(method, context, { pathName: newPathName })).filter(isDefined_default); if (newMethods.length === 0) { return void 0; } newType = ts3.factory.updateTypeLiteralNode(path3.type, ts3.factory.createNodeArray(newMethods)); } else { newType = renameComponentReferences(path3.type, context); } return ts3.factory.updatePropertySignature(path3, path3.modifiers, newIdentifier, path3.questionToken, newType); } function wrapPathsType(type, context) { context.typeImports.http.add("HttpSchema"); const httpSchemaPathsWrapper = ts3.factory.createIdentifier("HttpSchema"); return ts3.factory.createTypeReferenceNode(httpSchemaPathsWrapper, [type]); } function normalizePaths(pathsOrTypeAlias, context) { const newIdentifier = createPathsIdentifier(context.serviceName); const paths = ts3.isTypeAliasDeclaration(pathsOrTypeAlias) ? ts3.factory.createInterfaceDeclaration(pathsOrTypeAlias.modifiers, pathsOrTypeAlias.name, void 0, void 0, []) : pathsOrTypeAlias; const newMembers = paths.members.map((path3) => normalizePath(path3, context)).filter(isDefined_default); const newType = ts3.factory.createTypeLiteralNode(newMembers); return ts3.factory.createTypeAliasDeclaration( paths.modifiers, newIdentifier, paths.typeParameters, wrapPathsType(newType, context) ); } // src/typegen/openapi/transform/components.ts function createComponentsIdentifierText(serviceName) { return `${serviceName}Components`; } function createComponentsIdentifier(serviceName) { return ts3.factory.createIdentifier(createComponentsIdentifierText(serviceName)); } function isComponentsDeclaration(node, context) { const componentIdentifiers = ["components", createComponentsIdentifierText(context.serviceName)]; return node !== void 0 && ts3.isInterfaceDeclaration(node) && componentIdentifiers.includes(node.name.text); } function isComponentGroup(node) { return ts3.isPropertySignature(node) && node.type !== void 0 && ts3.isTypeLiteralNode(node.type) && ts3.isIdentifier(node.name); } function isComponent(node) { return ts3.isPropertySignature(node) && node.type !== void 0 && !isNeverType(node.type) && (ts3.isIdentifier(node.name) || ts3.isStringLiteral(node.name)); } function isRequestComponent(node) { return ts3.isTypeLiteralNode(node.type); } function unchangedIndexedAccessTypeNode(node) { return node; } function visitComponentReferences(node, context, options) { const { onComponentReference, renameComponentReference = unchangedIndexedAccessTypeNode } = options; if (isUnknownType(node)) { return ts3.factory.createKeywordTypeNode(ts3.SyntaxKind.AnyKeyword); } if (ts3.isTypeReferenceNode(node)) { const newTypeArguments = node.typeArguments?.map((type) => visitComponentReferences(type, context, options)); return ts3.factory.updateTypeReferenceNode(node, node.typeName, ts3.factory.createNodeArray(newTypeArguments)); } if (ts3.isArrayTypeNode(node)) { const newElementType = visitComponentReferences(node.elementType, context, options); return ts3.factory.updateArrayTypeNode(node, newElementType); } if (ts3.isTupleTypeNode(node)) { const newElements = node.elements.map((element) => visitComponentReferences(element, context, options)); return ts3.factory.updateTupleTypeNode(node, ts3.factory.createNodeArray(newElements)); } if (ts3.isUnionTypeNode(node)) { const newTypes = node.types.map((type) => visitComponentReferences(type, context, options)); return ts3.factory.updateUnionTypeNode(node, ts3.factory.createNodeArray(newTypes)); } if (ts3.isIntersectionTypeNode(node)) { const newTypes = node.types.map((type) => visitComponentReferences(type, context, options)); return ts3.factory.updateIntersectionTypeNode(node, ts3.factory.createNodeArray(newTypes)); } if (ts3.isParenthesizedTypeNode(node)) { const newType = visitComponentReferences(node.type, context, options); return ts3.factory.updateParenthesizedType(node, newType); } if (ts3.isTypeLiteralNode(node)) { const newMembers = node.members.map((member) => { if (ts3.isPropertySignature(member) && member.type) { const newType = visitComponentReferences(member.type, context, options); return ts3.factory.updatePropertySignature(member, member.modifiers, member.name, member.questionToken, newType); } if (ts3.isIndexSignatureDeclaration(member)) { const newType = visitComponentReferences(member.type, context, options); return ts3.factory.updateIndexSignature(member, member.modifiers, member.parameters, newType); } return member; }); return ts3.factory.updateTypeLiteralNode(node, ts3.factory.createNodeArray(newMembers)); } if (ts3.isIndexedAccessTypeNode(node)) { const isRootIndexedAccess = context.isComponentIndexedAccess ?? true; if (ts3.isIndexedAccessTypeNode(node.objectType)) { const childContext = { ...context, isComponentIndexedAccess: false }; const newObjectType = visitComponentReferences(node.objectType, childContext, options); const newNode = ts3.factory.updateIndexedAccessTypeNode(node, newObjectType, node.indexType); if (childContext.partialComponentPath && childContext.partialComponentPath.length > 0) { const hasIndexTypeName = ts3.isLiteralTypeNode(node.indexType) && (ts3.isIdentifier(node.indexType.literal) || ts3.isStringLiteral(node.indexType.literal)); if (hasIndexTypeName) { const componentName = node.indexType.literal.text; childContext.partialComponentPath.push(componentName); } if (isRootIndexedAccess) { const componentGroupName = childContext.partialComponentPath[0]; const componentName = childContext.partialComponentPath.slice(1).join("."); const componentPath = `${componentGroupName}.${componentName}`; onComponentReference(newNode, componentPath); } } return newNode; } const componentIdentifiers = ["components", createComponentsIdentifierText(context.serviceName)]; const isComponentIndexedAccess = ts3.isTypeReferenceNode(node.objectType) && ts3.isIdentifier(node.objectType.typeName) && componentIdentifiers.includes(node.objectType.typeName.text) && ts3.isLiteralTypeNode(node.indexType) && (ts3.isIdentifier(node.indexType.literal) || ts3.isStringLiteral(node.indexType.literal)); if (isComponentIndexedAccess) { const isRawComponent = node.objectType.typeName.text === "components"; const componentGroupName = node.indexType.literal.text; const newNode = isRawComponent ? renameComponentReference(node, { objectType: node.objectType, indexType: node.indexType, componentGroupName }) : node; const newNodeHasComponentGroupName = ts3.isLiteralTypeNode(newNode.indexType) && (ts3.isIdentifier(newNode.indexType.literal) || ts3.isStringLiteral(newNode.indexType.literal)); if (newNodeHasComponentGroupName) { const newComponentGroupName = newNode.indexType.literal.text; context.partialComponentPath = [newComponentGroupName]; } return newNode; } } return node; } function normalizeComponentGroupName(rawComponentGroupName) { if (rawComponentGroupName === "requestBodies") { return "requests"; } return rawComponentGroupName; } function renameComponentReferences(node, context) { return visitComponentReferences(node, context, { onComponentReference(_node, componentPath) { context.referencedTypes.components.add(componentPath); }, renameComponentReference(node2, { indexType, objectType, componentGroupName }) { const newIdentifier = createComponentsIdentifier(context.serviceName); const newObjectType = ts3.factory.updateTypeReferenceNode(objectType, newIdentifier, objectType.typeArguments); const newComponentGroupName = normalizeComponentGroupName(componentGroupName); const newIndexType = ts3.factory.updateLiteralTypeNode( indexType, ts3.factory.createStringLiteral(newComponentGroupName) ); return ts3.factory.updateIndexedAccessTypeNode(node2, newObjectType, newIndexType); } }); } function processPendingRequestComponentActions(component, context) { const pendingRequestActions = context.pendingActions.components.requests; const componentName = component.name.text; const shouldBeMarkedAsOptional = pendingRequestActions.toMarkBodyAsOptional.has(componentName); const bodyQuestionToken = shouldBeMarkedAsOptional ? ts3.factory.createToken(ts3.SyntaxKind.QuestionToken) : component.questionToken; pendingRequestActions.toMarkBodyAsOptional.delete(componentName); return { bodyQuestionToken }; } function normalizeRequestComponent(component, context) { if (!isRequestComponent(component)) { return void 0; } const { bodyQuestionToken } = processPendingRequestComponentActions(component, context); const newType = normalizeContentType(component.type, context, { bodyQuestionToken }); return ts3.factory.updatePropertySignature( component, component.modifiers, component.name, component.questionToken, newType ); } function normalizeComponent(component, componentGroupName, context) { if (!isComponent(component)) { return void 0; } if (componentGroupName === "requests") { return normalizeRequestComponent(component, context); } if (componentGroupName === "responses") { const responseComponent = normalizeResponse(component, context, { isComponent: true }); return responseComponent?.newSignature; } if (componentGroupName === "pathItems") { return normalizePath(component, context, { isComponent: true }); } return ts3.factory.updatePropertySignature( component, component.modifiers, component.name, component.questionToken, renameComponentReferences(component.type, context) ); } function normalizeComponentGroup(componentGroup, context) { if (!isComponentGroup(componentGroup)) { return void 0; } const componentGroupName = normalizeComponentGroupName(componentGroup.name.text); const newIdentifier = ts3.factory.createIdentifier(componentGroupName); const newComponents = componentGroup.type.members.map((component) => normalizeComponent(component, componentGroupName, context)).filter(isDefined_default); const newType = ts3.factory.updateTypeLiteralNode(componentGroup.type, ts3.factory.createNodeArray(newComponents)); return ts3.factory.updatePropertySignature( componentGroup, componentGroup.modifiers, newIdentifier, componentGroup.questionToken, newType ); } function normalizeComponents(components, context) { const newIdentifier = createComponentsIdentifier(context.serviceName); const newMembers = components.members.map((componentGroup) => normalizeComponentGroup(componentGroup, context)).filter(isDefined_default); return ts3.factory.updateInterfaceDeclaration( components, components.modifiers, newIdentifier, components.typeParameters, components.heritageClauses, newMembers ); } function populateReferencedComponents(components, context) { const pathsToVisit = new Set(context.referencedTypes.components); while (pathsToVisit.size > 0) { const previousPathsToVisit = new Set(pathsToVisit); pathsToVisit.clear(); for (const componentGroup of components.members) { if (!isComponentGroup(componentGroup)) { continue; } const componentGroupName = normalizeComponentGroupName(componentGroup.name.text); for (const component of componentGroup.type.members) { if (!isComponent(component)) { continue; } const componentName = component.name.text; const componentPath = `${componentGroupName}.${componentName}`; const isComponentToVisit = previousPathsToVisit.has(componentPath); if (!isComponentToVisit) { continue; } context.referencedTypes.components.add(componentPath); visitComponentReferences(component.type, context, { onComponentReference(_node, componentPath2) { const isKnownReferencedComponent = context.referencedTypes.components.has(componentPath2); if (!isKnownReferencedComponent) { pathsToVisit.add(componentPath2); } } }); } } } } function removeComponentIfUnreferenced(component, componentGroupName, context) { if (!isComponent(component)) { return void 0; } const componentName = component.name.text; const componentPath = `${componentGroupName}.${componentName}`; if (context.referencedTypes.components.has(componentPath)) { context.referencedTypes.components.delete(componentPath); return component; } return void 0; } function removeUnreferencedComponentsInGroup(componentGroup, context) { if (!isComponentGroup(componentGroup)) { return void 0; } const componentGroupName = normalizeComponentGroupName(componentGroup.name.text); const newComponents = componentGroup.type.members.map((component) => removeComponentIfUnreferenced(component, componentGroupName, context)).filter(isDefined_default); if (newComponents.length === 0) { return void 0; } return ts3.factory.updatePropertySignature( componentGroup, componentGroup.modifiers, componentGroup.name, componentGroup.questionToken, ts3.factory.updateTypeLiteralNode(componentGroup.type, ts3.factory.createNodeArray(newComponents)) ); } function removeUnreferencedComponents(components, context) { const newComponentGroups = components.members.map((componentGroup) => removeUnreferencedComponentsInGroup(componentGroup, context)).filter(isDefined_default); context.referencedTypes.components.clear(); if (newComponentGroups.length === 0) { return void 0; } return ts3.factory.updateInterfaceDeclaration( components, components.modifiers, components.name, components.typeParameters, components.heritageClauses, newComponentGroups ); } // src/utils/strings.ts function convertToPascalCase(value) { return value.replace(/(?:^|[^A-Za-z\d])([A-Za-z\d])/g, (_match, letter) => letter.toUpperCase()); } // ../zimic-utils/dist/url.mjs function createPathCharactersToEscapeRegex() { return /([.(){}+$])/g; } function preparePathForRegex(path3) { const pathURLPrefix = `data:${path3.startsWith("/") ? "" : "/"}`; const pathAsURL = new URL(`${pathURLPrefix}${path3}`); const encodedPath = pathAsURL.href.replace(pathURLPrefix, ""); return encodedPath.replace(/^\/+/g, "").replace(/\/+$/g, "").replace(createPathCharactersToEscapeRegex(), "\\$1"); } function getSingleWildcardPathRegex() { return /(?<wildcardPrefix>^|[^*])(?<escape>\\)?\*(?<wildcardSuffix>[^*]|$)/g; } function getDoubleWildcardPathRegex() { return /(?<escape>\\)?\*\*/g; } function getTripleWildcardPathRegex() { return /(?<escape>\\)?\*\*\/\*(?<wildcardSuffix>[^*]|$)/g; } function createWildcardRegexFromPath(path3) { const pathRegexContent = preparePathForRegex(path3).replace(getTripleWildcardPathRegex(), (_match, escape, wildcardSuffix) => { return escape === "\\" ? `\\*\\*/\\*${wildcardSuffix}` : `**${wildcardSuffix}`; }).replace( getSingleWildcardPathRegex(), (_match, wildcardPrefix, escape, wildcardSuffix) => { return escape === "\\" ? `${wildcardPrefix}\\*${wildcardSuffix}` : `${wildcardPrefix}[^/]*${wildcardSuffix}`; } ).replace(getDoubleWildcardPathRegex(), (_match, escape) => { return escape === "\\" ? "\\*\\*" : ".*"; }); return new RegExp(`^/?${pathRegexContent}/?$`); } var createWildcardRegexFromPath_default = createWildcardRegexFromPath; // ../zimic-utils/dist/chunk-W66SQPEM.mjs function createCachedDynamicImport(importModuleDynamically) { let cachedImportResult; return async function importModuleDynamicallyWithCache() { cachedImportResult ??= await importModuleDynamically(); return cachedImportResult; }; } var createCachedDynamicImport_default = createCachedDynamicImport; async function ensurePathExists(path3, options) { try { await fs.promises.access(path3, fs.constants.R_OK); } catch (accessError) { const error = new Error(options.errorMessage); error.cause = accessError; throw error; } } // src/typegen/openapi/transform/filters.ts var HTTP_METHOD_OPTIONS = HTTP_METHODS.join("|"); var MODIFIER_GROUP = "(?<modifier>!?)"; var METHOD_FILTER_GROUP = `(?<method>(?:\\*|(?:${HTTP_METHOD_OPTIONS})(?:,\\s*(?:${HTTP_METHOD_OPTIONS}))*))`; var PATH_FILTER_GROUP = "(?<path>.+)"; var FILTER_REGEX = new RegExp(`^${MODIFIER_GROUP}\\s*${METHOD_FILTER_GROUP}\\s+${PATH_FILTER_GROUP}$`, "i"); function parseRawFilter(rawFilter) { const filterMatch = rawFilter.match(FILTER_REGEX); const { modifier: filterModifier, method: filteredMethodsOrWildcard, path: filteredPath } = filterMatch?.groups ?? {}; const isValidFilter = !filteredMethodsOrWildcard || !filteredPath; if (isValidFilter) { logger.warn(`Warning: Filter could not be parsed and was ignored: ${color3.yellow(rawFilter)}`); return void 0; } return { methodRegex: new RegExp(`(?:${filteredMethodsOrWildcard.toUpperCase().replace(/,/g, "|").replace(/\*/g, ".*")})`), pathRegex: createWildcardRegexFromPath_default(filteredPath), isNegativeMatch: filterModifier === "!" }; } function groupParsedFiltersByMatch(parsedFilters) { return parsedFilters.reduce( (groupedFilters, filter) => { if (filter) { if (filter.isNegativeMatch) { groupedFilters.negative.push(filter); } else { groupedFilters.positive.push(filter); } } return groupedFilters; }, { positive: [], negative: [] } ); } async function readPathFiltersFromFile(filePath) { await ensurePathExists(filePath, { errorMessage: `Could not read filter file: ${color3.yellow(filePath)}` }); const fileContent = await fs.promises.readFile(filePath, "utf-8"); const fileContentWithoutComments = fileContent.replace(/#.*$/gm, ""); const filters = fileContentWithoutComments.split("\n"); return filters; } function ignoreEmptyFilters(filters) { return filters.map((line) => line.trim()).filter(isNonEmpty_default); } // src/typegen/openapi/transform/context.ts function createTypeTransformationContext(serviceName, rawFilters) { const parsedFilters = rawFilters.map(parseRawFilter); return { serviceName: convertToPascalCase(serviceName), filters: { paths: groupParsedFiltersByMatch(parsedFilters) }, typeImports: { http: /* @__PURE__ */ new Set() }, referencedTypes: { operations: /* @__PURE__ */ new Set(), components: /* @__PURE__ */ new Set() }, pendingActions: { components: { requests: { toMarkBodyAsOptional: /* @__PURE__ */ new Set() } } } }; } // src/typegen/openapi/transform/imports.ts var TYPEGEN_HTTP_IMPORT_MODULE = "@zimic/http"; function createImportDeclarations(context) { const httpTypeImports = Array.from(context.typeImports.http).sort().map(createImportSpecifier); const httpImportDeclaration = createImportDeclaration(httpTypeImports, TYPEGEN_HTTP_IMPORT_MODULE); return [httpImportDeclaration]; } var importOpenapiTypeScript = createCachedDynamicImport_default(() => import('openapi-typescript')); function transformSchemaObject(schemaObject) { if (schemaObject.format === "binary") { const blobType = createBlobType(); if (schemaObject.nullable) { const nullType = createNullType(); return ts3.factory.createUnionTypeNode([blobType, nullType]); } return blobType; } } function convertFilePathOrURLToURL(filePathOrURL) { try { return new URL(filePathOrURL); } catch { return new URL(`file://${path.resolve(filePathOrURL)}`); } } async function importTypesFromOpenAPI(filePathOrURL) { const schemaURL = convertFilePathOrURLToURL(filePathOrURL); const { default: generateTypesFromOpenAPI2 } = await importOpenapiTypeScript(); const rawNodes = await generateTypesFromOpenAPI2(schemaURL, { alphabetize: false, additionalProperties: false, excludeDeprecated: false, propertiesRequiredByDefault: false, defaultNonNullable: true, pathParamsAsTypes: false, emptyObjectsUnknown: true, exportType: false, arrayLength: false, immutable: false, enumValues: false, enum: false, silent: true, transform: transformSchemaObject }); return rawNodes; } async function convertTypesToString(nodes, options) { const { astToString: convertTypeASTToString } = await importOpenapiTypeScript(); const typegenResult = convertTypeASTToString(nodes, { formatOptions: { removeComments: !options.includeComments } }); return typegenResult; } function formatTypegenResultToOutput(typegenResult) { const formattedResult = typegenResult.replace(/^export (\w+)/gm, "\nexport $1").replace(/^( {4})+/gm, (match) => match.replace(/ {4}/g, " ")); return [ "// Auto-generated by @zimic/http.", "// NOTE: Do not manually edit this file. Changes will be overridden.\n", formattedResult ].join("\n"); } async function writeTypegenResultToStandardOutput(typegenResult) { await new Promise((resolve) => { process.stdout.write(typegenResult, "utf-8", resolve); }); } async function writeTypegenResultToFile(outputFilePath, formattedResult) { const outputDirectory = path.dirname(outputFilePath); await fs.promises.mkdir(outputDirectory, { recursive: true }); await fs.promises.writeFile(outputFilePath, formattedResult); } // src/typegen/openapi/generate.ts var RESOURCES_TO_REMOVE_IF_NOT_NORMALIZED = ["paths", "webhooks", "operations", "components", "$defs"]; function removeUnknownResources(node) { const isUnknownResource = !node || (ts3.isTypeAliasDeclaration(node) || ts3.isInterfaceDeclaration(node)) && RESOURCES_TO_REMOVE_IF_NOT_NORMALIZED.includes(node.name.text); if (isUnknownResource) { return void 0; } return node; } function normalizeRawNodes(rawNodes, context, options) { let normalizedNodes = rawNodes.map((node) => isPathsDeclaration(node) ? normalizePaths(node, context) : node); if (options.prune) { normalizedNodes = normalizedNodes.map((node) => isOperationsDeclaration(node) ? removeUnreferencedOperations(node, context) : node).filter(isDefined_default); } normalizedNodes = normalizedNodes.map((node) => isOperationsDeclaration(node) ? normalizeOperations(node, context) : node).filter(isDefined_default); if (options.prune) { for (const node of normalizedNodes) { if (isComponentsDeclaration(node, context)) { populateReferencedComponents(node, context); } } normalizedNodes = normalizedNodes.map((node) => isComponentsDeclaration(node, context) ? removeUnreferencedComponents(node, context) : node).filter(isDefined_default); } normalizedNodes = normalizedNodes.map((node) => isComponentsDeclaration(node, context) ? normalizeComponents(node, context) : node).map(removeUnknownResources).filter(isDefined_default); return normalizedNodes; } async function generateTypesFromOpenAPI({ input: inputFilePathOrURL, output: outputFilePath, serviceName, includeComments, prune, filters: filtersFromArguments = [], filterFile }) { const filtersFromFile = filterFile ? await readPathFiltersFromFile(path.resolve(filterFile)) : []; const filters = ignoreEmptyFilters([...filtersFromFile, ...filtersFromArguments]); const rawNodes = await importTypesFromOpenAPI(inputFilePathOrURL); const context = createTypeTransformationContext(serviceName, filters); const nodes = normalizeRawNodes(rawNodes, context, { prune }); const importDeclarations = createImportDeclarations(context); for (const declaration of importDeclarations) { nodes.unshift(declaration); } const typegenResult = await convertTypesToString(nodes, { includeComments }); const formattedTypegenResult = formatTypegenResultToOutput(typegenResult); con