import * as path from 'path' ; import * as fs from 'fs' ; import * as ts from 'typescript' ; import { NodeTransformer , walker } from './util' ; console . log ( '[power-di] load transformer: class metadata.' ) ; export default function transformer ( program ) { const typeChecker = program . getTypeChecker ( ) ; return { before ( ctx ) { return ( sourceFile ) => { return walker ( sourceFile , ctx , before ( ctx , sourceFile , typeChecker ) ) ; } ; } , } ; } export function before ( ctx , sf , typeChecker ) { const pkg = findPkg ( sf . fileName ) ; if ( ! pkg ) { console . log ( 'no pkg' , sf . fileName ) ; } return ( node ) => { if ( ! pkg ) { return node ; } if ( ! ts . isDecorator ( node ) ) { return node ; } if ( [ 'inject' , 'getContributions' , 'getExtensions' , 'getPlugins' ] . includes ( `${ getDecoratorName ( node ) }` ) ) { return processInject ( node , sf , typeChecker ) ; } if ( [ 'injectable' , 'plugin' , 'extension' , 'contribution' ] . includes ( `${ getDecoratorName ( node ) }` ) ) { return processClassInfo ( node , pkg , sf , typeChecker ) ; } return node ; } ; } function processClassInfo ( node , pkg , sourceFile , typeChecker ) { const clsNode = node . parent ; if ( ! ts . isClassDeclaration ( clsNode ) || ! ts . isCallExpression ( node . expression ) ) { return node ; } const decoratorFactory = node . expression ; if ( decoratorFactory . arguments && decoratorFactory . arguments [ 0 ] && ! ts . isObjectLiteralExpression ( decoratorFactory . arguments [ 0 ] ) ) { console . warn ( '[power-di] class metadata transformer fail!' , `@${ getDecoratorName ( node ) } of class [${ clsNode . name . escapedText }] param is not a ObjectLiteral.` ) ; return node ; } const oldArg = decoratorFactory . arguments . length && decoratorFactory . arguments [ 0 ] ; const oldArgObj = oldArg && ts . isObjectLiteralExpression ( oldArg ) && oldArg ; const impls = ! hasField ( oldArgObj , 'implements' ) && clsNode . heritageClauses ?. find ( hc => hc . token === ts . SyntaxKind . ImplementsKeyword ) ; impls && impls . types . forEach ( typeNode => { const type = typeChecker . getTypeFromTypeNode ( typeNode ) ; const symbol = type . getSymbol ( ) ; fixedImport ( symbol ?. name || typeNode . getText ( ) , sourceFile ) ; } ) ; const info = [ ts . createPropertyAssignment ( 'pkg' , ts . createStringLiteral ( pkg . name ) ) , ts . createPropertyAssignment ( 'version' , ts . createStringLiteral ( pkg . version ) ) , impls && ts . createPropertyAssignment ( 'implements' , ts . createArrayLiteral ( impls . types . map ( type => type . expression ) ) ) , ] . filter ( s => s ) ; const config = oldArgObj ? ts . updateObjectLiteral ( oldArgObj , ts . createNodeArray ( [ ... info , ... oldArgObj . properties . filter ( p => p . name && ts . isIdentifier ( p . name ) && ! [ 'pkg' , 'version' , impls ? 'implements' : undefined ] . filter ( s => s ) . includes ( `${ p . name . escapedText }` ) ) , ] ) ) : ts . createObjectLiteral ( ts . createNodeArray ( info ) , false ) ; return ts . updateDecorator ( node , ts . updateCall ( decoratorFactory , decoratorFactory . expression , decoratorFactory . typeArguments , [ config , ] ) ) ; } function processInject ( node , sourceFile , typeChecker ) { const propertyNode = node . parent ; if ( ! ts . isPropertyDeclaration ( propertyNode ) || ! ts . isCallExpression ( node . expression ) ) { return node ; } const decoratorFactory = node . expression ; if ( decoratorFactory . arguments && decoratorFactory . arguments [ 0 ] && ! ts . isObjectLiteralExpression ( decoratorFactory . arguments [ 0 ] ) ) { console . warn ( '[power-di] class metadata transformer fail!' , `@${ getDecoratorName ( node ) } of class [${ propertyNode . name . getText ( ) }] param is not a ObjectLiteral.` ) ; return node ; } const oldArg = decoratorFactory . arguments . length && decoratorFactory . arguments [ 0 ] ; const oldArgObj = oldArg && ts . isObjectLiteralExpression ( oldArg ) && oldArg ; if ( oldArgObj && oldArgObj . properties . some ( p => p . name && ts . isIdentifier ( p . name ) && 'type' === `${ p . name . escapedText }` ) ) { return node ; } const refType = propertyNode . type ? ts . isTypeReferenceNode ( propertyNode . type ) ? propertyNode . type : ts . isArrayTypeNode ( propertyNode . type ) ? ts . isTypeReferenceNode ( propertyNode . type . elementType ) ? propertyNode . type . elementType : undefined : undefined : undefined ; if ( ! refType ) { return node ; } let identifier = refType . typeName as ts . Identifier ; if ( identifier . escapedText === 'Array' ) { identifier = ( refType . typeArguments [ 0 ] as ts . TypeReferenceNode ) ?. typeName as ts . Identifier ; if ( ! identifier ) { console . warn ( '[power-di] class metadata transformer fail!' , getDecoratorName ( node ) , propertyNode . name . getText ( ) , refType . getText ( ) ) ; return node ; } } fixedImport ( identifier . escapedText , sourceFile ) ; const info = [ refType && ts . createPropertyAssignment ( 'type' , identifier ) ] ; const config = oldArgObj ? ts . updateObjectLiteral ( oldArgObj , ts . createNodeArray ( [ ... info , ... oldArgObj . properties ] ) ) : ts . createObjectLiteral ( ts . createNodeArray ( info ) , false ) ; return ts . updateDecorator ( node , ts . updateCall ( decoratorFactory , decoratorFactory . expression , decoratorFactory . typeArguments , [ config , ] ) ) ; } const pkgCache = [ ] ; function findPkg ( filePath ) { if ( filePath === '/' ) { return ; } const parsed = path . parse ( filePath ) ; const cache = pkgCache . sort ( ( a , b ) => b . path . length - a . path . length ) . find ( pc => parsed . dir === pc . path ) ; if ( cache ) { return cache . pkg ; } const pkgPath = path . join ( parsed . dir , 'package.json' ) ; if ( ! fs . existsSync ( pkgPath ) ) { return findPkg ( parsed . dir ) ; } const pkgData = JSON . parse ( fs . readFileSync ( pkgPath , 'utf8' ) ) ; pkgCache . push ( { path : parsed . dir , pkg : pkgData } ) ; return pkgData ; } function getDecoratorName ( node ) { if ( ! node ) { return '' ; } return ts . isIdentifier ( node . expression ) ? node . expression . escapedText : ts . isCallExpression ( node . expression ) && ts . isIdentifier ( node . expression . expression ) ? node . expression . expression . escapedText : '' ; } function getField ( config , fieldName ) { const prop = config && ts . isObjectLiteralExpression ( config ) && config . properties . find ( p => { return p . name && ts . isIdentifier ( p . name ) && p . name . escapedText === fieldName ; } ) ; return prop && ts . isPropertyAssignment ( prop ) && prop ; } function hasField ( config , fieldName ) { return ! ! getField ( config , fieldName ) ; } function fixedImport ( escapedText , sourceFile ) { escapedText = `${ escapedText }` . split ( '<' ) [ 0 ] ; sourceFile . statements . filter ( n => ts . isImportDeclaration ( n ) ) . forEach ( ( im ) => { const nb = im . importClause ?. namedBindings as ts . NamedImports ; const el = nb ?. elements ?. find ( el => { return el . name . escapedText === escapedText ; } ) ; if ( el ) { im . flags = im . flags | ts . NodeFlags . Synthesized ; } } ) ; }