@nestjs/swagger
Version:
Nest - modern, fast, powerful node.js web framework (@swagger)
414 lines (413 loc) • 14.8 kB
JavaScript
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;
}