From f33ce0c6c12d063cd83d8d98f09cc73ba138f8c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Tue, 29 Oct 2024 14:42:42 +0100 Subject: [PATCH] fix(plugin): support compiler options paths mapping --- lib/plugin/utils/plugin-utils.ts | 24 +++++- .../type-reference-to-identifier.util.ts | 6 +- .../visitors/controller-class.visitor.ts | 22 +++-- lib/plugin/visitors/model-class.visitor.ts | 81 ++++++++++++------- .../project/cats/dto/absolute-owner.dto.ts | 4 + .../project/cats/dto/create-cat.dto.ts | 14 ++++ .../fixtures/project/package-a/owner.ts | 4 + test/plugin/fixtures/project/tsconfig.json | 6 +- .../fixtures/serialized-meta.fixture.ts | 12 +++ 9 files changed, 133 insertions(+), 40 deletions(-) create mode 100644 test/plugin/fixtures/project/cats/dto/absolute-owner.dto.ts create mode 100644 test/plugin/fixtures/project/package-a/owner.ts diff --git a/lib/plugin/utils/plugin-utils.ts b/lib/plugin/utils/plugin-utils.ts index 7d1bd208b..188f72bda 100644 --- a/lib/plugin/utils/plugin-utils.ts +++ b/lib/plugin/utils/plugin-utils.ts @@ -115,7 +115,10 @@ export function getTypeReferenceAsString( if (type.aliasSymbol) { return { typeName: 'Object', arrayDepth }; } - if (typeChecker.getApparentType(type).getSymbol().getEscapedName() === 'String') { + if ( + typeChecker.getApparentType(type).getSymbol().getEscapedName() === + 'String' + ) { return { typeName: String.name, arrayDepth }; } return { typeName: undefined }; @@ -140,7 +143,8 @@ export function hasPropertyKey( export function replaceImportPath( typeReference: string, fileName: string, - options: PluginOptions + options: PluginOptions, + compilerOptionsPaths: ts.MapLike ) { if (!typeReference.includes('import')) { return { typeReference, importPath: null }; @@ -167,6 +171,19 @@ export function replaceImportPath( ? convertPath(options.pathToSource) : posix.dirname(convertPath(fileName)); + for (const [key, value] of Object.entries(compilerOptionsPaths)) { + const keyToMatch = key.replace('*', ''); + if (importPath.includes(keyToMatch)) { + const newImportPath = posix.join( + from, + importPath.replace(keyToMatch, value[0].replace('*', '')) + ); + typeReference = typeReference.replace(importPath, newImportPath); + importPath = newImportPath; + break; + } + } + let relativePath = posix.relative(from, importPath); relativePath = relativePath[0] !== '.' ? './' + relativePath : relativePath; @@ -250,7 +267,8 @@ export function isAutoGeneratedEnumUnion( return undefined; } const undefinedTypeIndex = type.types.findIndex( - (type: any) => type.intrinsicName === 'undefined' || type.intrinsicName === 'null' + (type: any) => + type.intrinsicName === 'undefined' || type.intrinsicName === 'null' ); if (undefinedTypeIndex < 0) { return undefined; diff --git a/lib/plugin/utils/type-reference-to-identifier.util.ts b/lib/plugin/utils/type-reference-to-identifier.util.ts index a4368c58d..993c74630 100644 --- a/lib/plugin/utils/type-reference-to-identifier.util.ts +++ b/lib/plugin/utils/type-reference-to-identifier.util.ts @@ -13,7 +13,8 @@ export function typeReferenceToIdentifier( options: PluginOptions, factory: ts.NodeFactory, type: ts.Type, - typeImports: Record + typeImports: Record, + compilerOptionsPaths: ts.MapLike ) { if (options.readonly) { assertReferenceableType( @@ -27,7 +28,8 @@ export function typeReferenceToIdentifier( const { typeReference, importPath, typeName } = replaceImportPath( typeReferenceDescriptor.typeName, hostFilename, - options + options, + compilerOptionsPaths ); let identifier: ts.Identifier; diff --git a/lib/plugin/visitors/controller-class.visitor.ts b/lib/plugin/visitors/controller-class.visitor.ts index ced3d49ba..70349c312 100644 --- a/lib/plugin/visitors/controller-class.visitor.ts +++ b/lib/plugin/visitors/controller-class.visitor.ts @@ -58,6 +58,7 @@ export class ControllerClassVisitor extends AbstractFileVisitor { options: PluginOptions ) { const typeChecker = program.getTypeChecker(); + const compilerOptionsPaths = program.getCompilerOptions().paths ?? {}; if (!options.readonly) { sourceFile = this.updateImports(sourceFile, ctx.factory, program); } @@ -72,7 +73,8 @@ export class ControllerClassVisitor extends AbstractFileVisitor { typeChecker, options, sourceFile, - metadata + metadata, + compilerOptionsPaths ); if (!options.readonly) { return updatedNode; @@ -121,7 +123,8 @@ export class ControllerClassVisitor extends AbstractFileVisitor { typeChecker: ts.TypeChecker, options: PluginOptions, sourceFile: ts.SourceFile, - metadata: ClassMetadata + metadata: ClassMetadata, + compilerOptionsPaths: ts.MapLike ): ts.MethodDeclaration { const hostFilename = sourceFile.fileName; const decorators = @@ -165,7 +168,8 @@ export class ControllerClassVisitor extends AbstractFileVisitor { factory.createNodeArray(), hostFilename, metadata, - options + options, + compilerOptionsPaths ); const updatedDecorators = [ ...apiOperationDecoratorsArray, @@ -389,7 +393,8 @@ export class ControllerClassVisitor extends AbstractFileVisitor { existingProperties: ts.NodeArray = factory.createNodeArray(), hostFilename: string, metadata: ClassMetadata, - options: PluginOptions + options: PluginOptions, + compilerOptionsPaths: ts.MapLike ): ts.ObjectLiteralExpression { let properties = []; if (!options.readonly) { @@ -405,7 +410,8 @@ export class ControllerClassVisitor extends AbstractFileVisitor { typeChecker, existingProperties, hostFilename, - options + options, + compilerOptionsPaths ) ]); const objectLiteralExpr = factory.createObjectLiteralExpression( @@ -435,7 +441,8 @@ export class ControllerClassVisitor extends AbstractFileVisitor { typeChecker: ts.TypeChecker, existingProperties: ts.NodeArray, hostFilename: string, - options: PluginOptions + options: PluginOptions, + compilerOptionsPaths: ts.MapLike ) { if (hasPropertyKey('type', existingProperties)) { return undefined; @@ -458,7 +465,8 @@ export class ControllerClassVisitor extends AbstractFileVisitor { options, factory, type, - this._typeImports + this._typeImports, + compilerOptionsPaths ); return factory.createPropertyAssignment('type', identifier); } diff --git a/lib/plugin/visitors/model-class.visitor.ts b/lib/plugin/visitors/model-class.visitor.ts index 69ece62d0..e66770005 100644 --- a/lib/plugin/visitors/model-class.visitor.ts +++ b/lib/plugin/visitors/model-class.visitor.ts @@ -3,6 +3,10 @@ import { posix } from 'path'; import * as ts from 'typescript'; import { factory, PropertyAssignment } from 'typescript'; import { ApiHideProperty, ApiProperty } from '../../decorators'; +import { + decoratorsProperties, + decoratorsPropertiesMappingType +} from '../../services/decorators-properties'; import { PluginOptions } from '../merge-options'; import { METADATA_FACTORY_NAME } from '../plugin-constants'; import { pluginDebugLogger } from '../plugin-debug-logger'; @@ -16,6 +20,10 @@ import { getTsDocTagsOfNode, isEnum } from '../utils/ast-utils'; +import { + getExternalImports, + replaceExternalImportsInTypeReference +} from '../utils/external-imports.util'; import { canReferenceNode, convertPath, @@ -28,14 +36,6 @@ import { } from '../utils/plugin-utils'; import { typeReferenceToIdentifier } from '../utils/type-reference-to-identifier.util'; import { AbstractFileVisitor } from './abstract.visitor'; -import { - getExternalImports, - replaceExternalImportsInTypeReference -} from '../utils/external-imports.util'; -import { - decoratorsProperties, - decoratorsPropertiesMappingType -} from '../../services/decorators-properties'; type ClassMetadata = Record; @@ -72,6 +72,7 @@ export class ModelClassVisitor extends AbstractFileVisitor { ) { const externalImports = getExternalImports(sourceFile); const typeChecker = program.getTypeChecker(); + const compilerOptionsPaths = program.getCompilerOptions().paths ?? {}; sourceFile = this.updateImports(sourceFile, ctx.factory, program); const propertyNodeVisitorFactory = @@ -86,7 +87,8 @@ export class ModelClassVisitor extends AbstractFileVisitor { options, sourceFile, metadata, - externalImports + externalImports, + compilerOptionsPaths ); } else if ( options.parameterProperties && @@ -98,7 +100,8 @@ export class ModelClassVisitor extends AbstractFileVisitor { options, sourceFile, metadata, - externalImports + externalImports, + compilerOptionsPaths ); } return node; @@ -165,7 +168,8 @@ export class ModelClassVisitor extends AbstractFileVisitor { options: PluginOptions, sourceFile: ts.SourceFile, metadata: ClassMetadata, - externalImports: Record + externalImports: Record, + compilerOptionsPaths: ts.MapLike ) { const isPropertyStatic = (node.modifiers || []).some( (modifier: ts.Modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword @@ -213,7 +217,8 @@ export class ModelClassVisitor extends AbstractFileVisitor { sourceFile.fileName, sourceFile, metadata, - externalImports + externalImports, + compilerOptionsPaths ); } catch (err) { return node; @@ -226,7 +231,8 @@ export class ModelClassVisitor extends AbstractFileVisitor { options: PluginOptions, sourceFile: ts.SourceFile, metadata: ClassMetadata, - externalImports: Record + externalImports: Record, + compilerOptionsPaths: ts.MapLike ) { constructorNode.forEachChild((node) => { if ( @@ -248,7 +254,8 @@ export class ModelClassVisitor extends AbstractFileVisitor { options, sourceFile.fileName, sourceFile, - externalImports + externalImports, + compilerOptionsPaths ); const propertyName = node.name.getText(); @@ -315,7 +322,8 @@ export class ModelClassVisitor extends AbstractFileVisitor { hostFilename: string, sourceFile: ts.SourceFile, metadata: ClassMetadata, - externalImports: Record + externalImports: Record, + compilerOptionsPaths: ts.MapLike ) { const objectLiteralExpr = this.createDecoratorObjectLiteralExpr( factory, @@ -325,7 +333,8 @@ export class ModelClassVisitor extends AbstractFileVisitor { options, hostFilename, sourceFile, - externalImports + externalImports, + compilerOptionsPaths ); this.addClassMetadata( compilerNode, @@ -346,7 +355,8 @@ export class ModelClassVisitor extends AbstractFileVisitor { options: PluginOptions = {}, hostFilename = '', sourceFile?: ts.SourceFile, - externalImports: Record = {} + externalImports: Record = {}, + compilerOptionsPaths: ts.MapLike = {} ): ts.ObjectLiteralExpression { const isRequired = !node.questionToken; @@ -363,7 +373,9 @@ export class ModelClassVisitor extends AbstractFileVisitor { typeChecker, existingProperties, hostFilename, - options + options, + externalImports, + compilerOptionsPaths ), ...this.createDescriptionAndTsDocTagPropertyAssignments( factory, @@ -386,7 +398,8 @@ export class ModelClassVisitor extends AbstractFileVisitor { existingProperties, hostFilename, options, - externalImports + externalImports, + compilerOptionsPaths ) ]; if ( @@ -411,7 +424,9 @@ export class ModelClassVisitor extends AbstractFileVisitor { typeChecker: ts.TypeChecker, existingProperties: ts.NodeArray, hostFilename: string, - options: PluginOptions + options: PluginOptions, + externalImports: Record, + compilerOptionsPaths: ts.MapLike ): ts.PropertyAssignment[] { const key = 'type'; if (hasPropertyKey(key, existingProperties)) { @@ -426,7 +441,9 @@ export class ModelClassVisitor extends AbstractFileVisitor { typeChecker, existingProperties, hostFilename, - options + options, + externalImports, + compilerOptionsPaths ); return [factory.createPropertyAssignment(key, initializer)]; } else if (ts.isUnionTypeNode(node)) { @@ -443,7 +460,9 @@ export class ModelClassVisitor extends AbstractFileVisitor { typeChecker, existingProperties, hostFilename, - options + options, + externalImports, + compilerOptionsPaths ); if (!isNullable) { return propertyAssignments; @@ -475,7 +494,8 @@ export class ModelClassVisitor extends AbstractFileVisitor { options, factory, type, - this._typeImports + this._typeImports, + compilerOptionsPaths ); const initializer = factory.createArrowFunction( @@ -495,7 +515,9 @@ export class ModelClassVisitor extends AbstractFileVisitor { typeChecker: ts.TypeChecker, existingProperties: ts.NodeArray, hostFilename: string, - options: PluginOptions + options: PluginOptions, + externalImports: Record, + compilerOptionsPaths: ts.MapLike ) { const propertyAssignments = Array.from(node.members || []).map((member) => { const literalExpr = this.createDecoratorObjectLiteralExpr( @@ -504,7 +526,10 @@ export class ModelClassVisitor extends AbstractFileVisitor { typeChecker, existingProperties, options, - hostFilename + hostFilename, + undefined, + externalImports, + compilerOptionsPaths ); return factory.createPropertyAssignment( factory.createIdentifier(member.name.getText()), @@ -544,7 +569,8 @@ export class ModelClassVisitor extends AbstractFileVisitor { existingProperties: ts.NodeArray, hostFilename: string, options: PluginOptions, - externalImports: Record + externalImports: Record, + compilerOptionsPaths: ts.MapLike ) { const key = 'enum'; if (hasPropertyKey(key, existingProperties)) { @@ -592,7 +618,8 @@ export class ModelClassVisitor extends AbstractFileVisitor { options, factory, type, - this._typeImports + this._typeImports, + compilerOptionsPaths ); const enumProperty = factory.createPropertyAssignment(key, enumIdentifier); diff --git a/test/plugin/fixtures/project/cats/dto/absolute-owner.dto.ts b/test/plugin/fixtures/project/cats/dto/absolute-owner.dto.ts new file mode 100644 index 000000000..a2e265690 --- /dev/null +++ b/test/plugin/fixtures/project/cats/dto/absolute-owner.dto.ts @@ -0,0 +1,4 @@ +export enum AbsoluteOwner { + YES = 'YES', + NO = 'NO' +} diff --git a/test/plugin/fixtures/project/cats/dto/create-cat.dto.ts b/test/plugin/fixtures/project/cats/dto/create-cat.dto.ts index 63fb9032f..60b4e2d47 100644 --- a/test/plugin/fixtures/project/cats/dto/create-cat.dto.ts +++ b/test/plugin/fixtures/project/cats/dto/create-cat.dto.ts @@ -13,6 +13,8 @@ import { ApiExtraModels, ApiProperty } from '../../../../lib'; import { ExtraModel } from './extra-model.dto'; import { LettersEnum } from './pagination-query.dto'; import { TagDto } from './tag.dto'; +import { Owner } from '@package-a/owner'; +import { AbsoluteOwner } from 'different-cats/dto/absolute-owner.dto'; enum NonExportedEnum { YES = 'YES', @@ -105,6 +107,18 @@ export class CreateCatDto { }) externalEnum: HttpStatus; + @ApiProperty({ + enum: Owner, + enumName: 'Owner' + }) + customPathImportOwner: Owner; + + @ApiProperty({ + enum: AbsoluteOwner, + enumName: 'AbsoluteOwner' + }) + absoluteImportOwner: AbsoluteOwner; + /** * Available language in the application * @example FR diff --git a/test/plugin/fixtures/project/package-a/owner.ts b/test/plugin/fixtures/project/package-a/owner.ts new file mode 100644 index 000000000..6fb850050 --- /dev/null +++ b/test/plugin/fixtures/project/package-a/owner.ts @@ -0,0 +1,4 @@ +export enum Owner { + YES = 'YES', + NO = 'NO' +} diff --git a/test/plugin/fixtures/project/tsconfig.json b/test/plugin/fixtures/project/tsconfig.json index 2f6b08ab5..28e7bc5d6 100644 --- a/test/plugin/fixtures/project/tsconfig.json +++ b/test/plugin/fixtures/project/tsconfig.json @@ -11,7 +11,11 @@ "outDir": "./dist", "baseUrl": "./", "incremental": true, - "skipLibCheck": true + "skipLibCheck": true, + "paths": { + "@package-a/owner": ["package-a/owner"], + "different-cats/*": ["./cats/*"] + } }, "include": ["./**/*"] } diff --git a/test/plugin/fixtures/serialized-meta.fixture.ts b/test/plugin/fixtures/serialized-meta.fixture.ts index d8d9eecff..772520131 100644 --- a/test/plugin/fixtures/serialized-meta.fixture.ts +++ b/test/plugin/fixtures/serialized-meta.fixture.ts @@ -4,6 +4,10 @@ export default async () => { ['./cats/dto/pagination-query.dto']: await import( './cats/dto/pagination-query.dto' ), + ['./package-a/owner']: await import('./package-a/owner'), + ['./cats/dto/absolute-owner.dto']: await import( + './cats/dto/absolute-owner.dto' + ), ['./cats/dto/create-cat.dto']: await import('./cats/dto/create-cat.dto'), ['./cats/dto/tag.dto']: await import('./cats/dto/tag.dto'), ['./cats/classes/cat.class']: await import('./cats/classes/cat.class') @@ -143,6 +147,14 @@ export default async () => { required: true, enum: require('@nestjs/common').HttpStatus }, + customPathImportOwner: { + required: true, + enum: t['./package-a/owner'].Owner + }, + absoluteImportOwner: { + required: true, + enum: t['./cats/dto/absolute-owner.dto'].AbsoluteOwner + }, state: { required: false, description: 'Available language in the application',