UNPKG

@nestjs/swagger

Version:

Nest - modern, fast, powerful node.js web framework (@swagger)

414 lines (413 loc) 14.8 kB
import { head } from 'es-toolkit/compat'; import { createRequire } from 'node:module'; import { isAbsolute, posix } from 'path'; import * as ts from 'typescript'; import { getDecoratorName, getText, getTypeArguments, hasFlag, isArray, isBigInt, isBoolean, isEnum, isInterface, isNumber, isString, isStringLiteral, isStringMapping } from './ast-utils.js'; const moduleRequire = createRequire(import.meta.url); export function getDecoratorOrUndefinedByNames(names, decorators, factory) { return (decorators || factory.createNodeArray()).find((item) => { try { const decoratorName = getDecoratorName(item); return names.includes(decoratorName); } catch { return false; } }); } export function getTypeReferenceAsString(type, typeChecker, arrayDepth = 0) { if (isArray(type)) { const arrayType = getTypeArguments(type)[0]; const { typeName, arrayDepth: depth } = getTypeReferenceAsString(arrayType, typeChecker, arrayDepth + 1); if (!typeName) { return { typeName: undefined }; } return { typeName: `${typeName}`, isArray: true, arrayDepth: depth }; } if (isBoolean(type)) { return { typeName: Boolean.name, arrayDepth }; } if (isNumber(type)) { return { typeName: Number.name, arrayDepth }; } if (isBigInt(type)) { return { typeName: BigInt.name, arrayDepth }; } if (isString(type) || isStringLiteral(type) || isStringMapping(type)) { return { typeName: String.name, arrayDepth }; } if (isPromiseOrObservable(getText(type, typeChecker))) { const typeArguments = getTypeArguments(type); const elementType = getTypeReferenceAsString(head(typeArguments), typeChecker, arrayDepth); return elementType; } if (type.isClass()) { return { typeName: getText(type, typeChecker), arrayDepth }; } try { const text = getText(type, typeChecker); if (text === Date.name) { return { typeName: text, arrayDepth }; } if (isOptionalBoolean(text)) { return { typeName: Boolean.name, arrayDepth }; } if (isAutoGeneratedTypeUnion(type) || isAutoGeneratedEnumUnion(type, typeChecker)) { const types = type.types; return getTypeReferenceAsString(types[types.length - 1], typeChecker, arrayDepth); } if (text === 'any' || text === 'unknown' || text === 'object' || isInterface(type) || (type.isUnionOrIntersection() && !isEnum(type))) { return { typeName: 'Object', arrayDepth }; } if (isEnum(type)) { return { typeName: undefined, arrayDepth }; } if (type.aliasSymbol) { return { typeName: 'Object', arrayDepth }; } if (typeChecker.getApparentType(type).getSymbol().getEscapedName() === 'String') { return { typeName: String.name, arrayDepth }; } return { typeName: undefined }; } catch { return { typeName: undefined }; } } export function getRecordValueType(type, typeChecker) { const indexInfo = typeChecker.getIndexInfoOfType(type, ts.IndexKind.String) ?? typeChecker.getIndexInfoOfType(type, ts.IndexKind.Number); if (indexInfo?.type && type.getProperties().length === 0) { return indexInfo.type; } if (type.aliasSymbol?.getName() === 'Record') { const aliasArgs = type.aliasTypeArguments; if (aliasArgs?.length === 2) { return aliasArgs[1]; } } return undefined; } export function createAdditionalPropertiesValueSchema(valueType, typeChecker, factory) { const text = getText(valueType, typeChecker); const objType = (t) => factory.createObjectLiteralExpression([ factory.createPropertyAssignment('type', factory.createStringLiteral(t)) ], false); if (isString(valueType) || isStringLiteral(valueType) || isStringMapping(valueType)) { return objType('string'); } if (isNumber(valueType) || isBigInt(valueType)) { return objType('number'); } if (isBoolean(valueType)) { return objType('boolean'); } if (text === 'any' || text === 'unknown' || text === 'object') { return factory.createTrue(); } const nested = getRecordValueType(valueType, typeChecker); if (nested) { return factory.createObjectLiteralExpression([ factory.createPropertyAssignment('type', factory.createStringLiteral('object')), factory.createPropertyAssignment('additionalProperties', createAdditionalPropertiesValueSchema(nested, typeChecker, factory)) ], false); } if (isArray(valueType)) { const elementType = getTypeArguments(valueType)[0]; return factory.createObjectLiteralExpression([ factory.createPropertyAssignment('type', factory.createStringLiteral('array')), factory.createPropertyAssignment('items', createAdditionalPropertiesValueSchema(elementType, typeChecker, factory)) ], false); } return factory.createTrue(); } export function isPromiseOrObservable(type) { return /(^|\.)(Promise|Observable)</.test(type); } export function hasPropertyKey(key, properties) { return properties .filter((item) => !isDynamicallyAdded(item)) .some((item) => item.name.getText() === key); } export function getOutputExtension(fileName) { if (fileName.endsWith('.mts')) { return '.mjs'; } else if (fileName.endsWith('.cts')) { return '.cjs'; } else { return '.js'; } } export function replaceImportPath(typeReference, fileName, options) { if (!typeReference.includes('import')) { return { typeReference, importPath: null }; } if (options.esmCompatible) { typeReference = typeReference.replace(', { with: { "resolution-mode": "import" } }', ''); } let importPath = /\(\"([^)]).+(\")/.exec(typeReference)[0]; if (!importPath) { return { typeReference: undefined, importPath: null }; } importPath = convertPath(importPath); importPath = importPath.slice(2, importPath.length - 1); const decodedImportPath = safeDecodeURIComponent(importPath); try { if (isAbsolute(decodedImportPath)) { throw {}; } moduleRequire.resolve(decodedImportPath); if (!options.esmCompatible) { return { typeReference: typeReference.replace('import', 'require'), importPath: null }; } if (options.readonly) { return { typeReference, importPath: null }; } const { typeName, typeImportStatement } = convertToAsyncImport(typeReference); return { typeReference: `(${typeImportStatement}).${typeName}`, typeName, importPath: decodedImportPath }; } catch { const from = options?.readonly ? rebaseToOutputDir(safeDecodeURIComponent(convertPath(options.pathToSource)), options) : getOutputDir(fileName, options); const targetPath = options.outDir && options.rootDir && decodedImportPath.startsWith(convertPath(options.rootDir)) ? posix.join(convertPath(options.outDir), posix.relative(convertPath(options.rootDir), decodedImportPath)) : decodedImportPath; let relativePath = posix.relative(from, targetPath); relativePath = relativePath[0] !== '.' ? './' + relativePath : relativePath; const normalizedPath = normalizePackagePath(relativePath); if (normalizedPath !== relativePath) { relativePath = normalizedPath; } else if (options.esmCompatible) { const extension = getOutputExtension(fileName); relativePath += extension; } typeReference = typeReference.replace(importPath, relativePath); if (options.readonly) { const { typeName, typeImportStatement } = convertToAsyncImport(typeReference); return { typeReference: typeImportStatement, typeName, importPath: relativePath }; } if (options.esmCompatible) { const { typeName, typeImportStatement } = convertToAsyncImport(typeReference); return { typeReference: `(${typeImportStatement}).${typeName}`, typeName, importPath: relativePath }; } return { typeReference: typeReference.replace('import', 'require'), importPath: relativePath }; } } function getOutputDir(fileName, options) { const sourceDir = posix.dirname(safeDecodeURIComponent(convertPath(fileName))); return rebaseToOutputDir(sourceDir, options); } function rebaseToOutputDir(sourceDir, options) { if (options.outDir && options.rootDir) { const outDir = convertPath(options.outDir); const rootDir = convertPath(options.rootDir); return posix.join(outDir, posix.relative(rootDir, sourceDir)); } return sourceDir; } function convertToAsyncImport(typeReference) { const regexp = /import\(.+\).([^\]]+)(\])?/; const match = regexp.exec(typeReference); if (match?.length >= 2) { const importPos = typeReference.indexOf(match[0]); typeReference = typeReference.replace(`.${match[1]}`, ''); return { typeImportStatement: insertAt(typeReference, importPos, 'await '), typeName: match[1] }; } return { typeImportStatement: typeReference }; } export function insertAt(string, index, substring) { return string.slice(0, index) + substring + string.slice(index); } export function isDynamicallyAdded(identifier) { return identifier && !identifier.parent && identifier.pos === -1; } export function isAutoGeneratedEnumUnion(type, typeChecker) { if (type.isUnionOrIntersection() && !isEnum(type)) { if (!type.types) { return undefined; } const undefinedTypeIndex = type.types.findIndex((type) => type.intrinsicName === 'undefined' || type.intrinsicName === 'null'); if (undefinedTypeIndex < 0) { return undefined; } let parentType = undefined; const isParentSymbolEqual = type.types.every((item, index) => { if (index === undefinedTypeIndex) { return true; } if (!item.symbol) { return false; } if (!item.symbol.parent || item.symbol.flags !== ts.SymbolFlags.EnumMember) { return false; } const symbolType = typeChecker.getDeclaredTypeOfSymbol(item.symbol.parent); if (symbolType === parentType || !parentType) { parentType = symbolType; return true; } return false; }); if (isParentSymbolEqual) { return parentType; } } return undefined; } export function isAutoGeneratedTypeUnion(type) { if (type.isUnionOrIntersection() && !isEnum(type)) { if (!type.types) { return false; } const undefinedTypeIndex = type.types.findIndex((type) => type.intrinsicName === 'undefined'); if (type.types.length === 2 && undefinedTypeIndex >= 0) { return true; } } return false; } function isPlainStringLiteralType(type) { return (isStringLiteral(type) && !(type.symbol && (type.symbol.flags & ts.SymbolFlags.EnumMember) !== 0)); } function isPlainNumberLiteralType(type) { return (hasFlag(type, ts.TypeFlags.NumberLiteral) && !(type.symbol && (type.symbol.flags & ts.SymbolFlags.EnumMember) !== 0)); } export function getStringLiteralUnionValues(type) { if (!type.isUnion()) { return undefined; } const isNullable = type.types.some((t) => hasFlag(t, ts.TypeFlags.Null)); const members = type.types.filter((t) => !hasFlag(t, ts.TypeFlags.Null) && !hasFlag(t, ts.TypeFlags.Undefined)); if (members.length === 0) { return undefined; } if (members.every(isPlainStringLiteralType)) { return { values: members.map((t) => t.value), isNullable }; } if (members.every(isPlainNumberLiteralType)) { return { values: members.map((t) => t.value), isNullable }; } return undefined; } export function extractTypeArgumentIfArray(type) { if (isArray(type)) { type = getTypeArguments(type)[0]; if (!type) { return undefined; } return { type, isArray: true }; } return { type, isArray: false }; } function isOptionalBoolean(text) { return typeof text === 'string' && text === 'boolean | undefined'; } export function convertPath(windowsPath) { return windowsPath .replace(/^\\\\\?\\/, '') .replace(/\\/g, '/') .replace(/\/\/+/g, '/'); } export function safeDecodeURIComponent(path) { try { return decodeURIComponent(path); } catch { return path; } } export function normalizePackagePath(importPath) { const nodeModulesText = 'node_modules'; const nodeModulePos = importPath.indexOf(nodeModulesText); if (nodeModulePos < 0) { return importPath; } let packagePath = importPath.slice(nodeModulePos + nodeModulesText.length + 1); const typesText = '@types'; const typesPos = packagePath.indexOf(typesText); if (typesPos >= 0) { packagePath = packagePath.slice(typesPos + typesText.length + 1); } const indexText = '/index'; const indexPos = packagePath.indexOf(indexText); if (indexPos >= 0) { packagePath = packagePath.slice(0, indexPos); } return packagePath; } export function canReferenceNode(node, options) { if (!options.readonly) { return true; } if (ts.isCallExpression(node) || ts.isIdentifier(node)) { return false; } if (ts.isNewExpression(node)) { if (node.expression?.escapedText === 'Date') { return true; } return false; } if (node.kind === ts.SyntaxKind.FalseKeyword || node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.NullKeyword) { return true; } if (ts.isNumericLiteral(node) || ts.isPrefixUnaryExpression(node) || ts.isStringLiteral(node)) { return true; } return false; }