import * as Handlebars from 'handlebars' ; import * as JSON5 from 'json5' ; import * as _ from 'lodash' ; import * as path from 'path' ; import { Project , ts , SourceFile , SyntaxKind , Node } from 'ts-morph' ; import FileEngine from '../app/engines/file.engine' ; import { RoutingGraphNode } from '../app/nodes/routing-graph-node' ; import ImportsUtil from './imports.util' ; import { logger } from './logger' ; const traverse = require ( 'traverse' ) ; const ast = new Project ( ) ; export class RouterParserUtil { private routes : any [ ] = [ ] ; private incompleteRoutes = [ ] ; private modules = [ ] ; private modulesTree ; private rootModule : string ; private cleanModulesTree ; private modulesWithRoutes = [ ] ; private transformAngular8ImportSyntax = /(['"]loadChildren['"]:)\(\)(:[^)]+?)?=>"import\((\\'|'|"|`)([^'"]+?)(\\'|'|"|`)\)\.then\(\(?\w+?\)?=>\S+?\.([^)]+?)\)(\\'|'|")/g ; private transformAngular8ImportSyntaxAsyncAwait = /(['"]loadChildren['"]:)\(\)(:[^)]+?)?=>\("import\((\\'|'|"|`)([^'"]+?)(\\'|'|"|`)\)"\)\.['"]([^)]+?)['"]/g ; private static instance : RouterParserUtil ; private constructor ( ) { } public static getInstance ( ) { if ( ! RouterParserUtil . instance ) { RouterParserUtil . instance = new RouterParserUtil ( ) ; } return RouterParserUtil . instance ; } public addRoute ( route ) { this . routes . push ( route ) ; this . routes = _ . sortBy ( _ . uniqWith ( this . routes , _ . isEqual ) , [ 'name' ] ) ; } public addIncompleteRoute ( route ) { this . incompleteRoutes . push ( route ) ; this . incompleteRoutes = _ . sortBy ( _ . uniqWith ( this . incompleteRoutes , _ . isEqual ) , [ 'name' ] ) ; } public addModuleWithRoutes ( moduleName , moduleImports , filename ) { this . modulesWithRoutes . push ( { name : moduleName , importsNode : moduleImports , filename : filename } ) ; this . modulesWithRoutes = _ . sortBy ( _ . uniqWith ( this . modulesWithRoutes , _ . isEqual ) , [ 'name' ] ) ; } public addModule ( moduleName , moduleImports ) { this . modules . push ( { name : moduleName , importsNode : moduleImports } ) ; this . modules = _ . sortBy ( _ . uniqWith ( this . modules , _ . isEqual ) , [ 'name' ] ) ; } public cleanRawRouteParsed ( route ) { let routesWithoutSpaces = route . replace ( / /gm , '' ) ; const testTrailingComma = routesWithoutSpaces . indexOf ( '},]' ) ; if ( testTrailingComma !== - 1 ) { routesWithoutSpaces = routesWithoutSpaces . replace ( '},]' , '}]' ) ; } routesWithoutSpaces = routesWithoutSpaces . replace ( this . transformAngular8ImportSyntax , '$1"$4#$6"' ) ; routesWithoutSpaces = routesWithoutSpaces . replace ( this . transformAngular8ImportSyntaxAsyncAwait , '$1"$4#$6"' ) ; return JSON5 . parse ( routesWithoutSpaces ) ; } public cleanRawRoute ( route ) { let routesWithoutSpaces = route . replace ( / /gm , '' ) ; let testTrailingComma = routesWithoutSpaces . indexOf ( '},]' ) ; if ( testTrailingComma !== - 1 ) { routesWithoutSpaces = routesWithoutSpaces . replace ( '},]' , '}]' ) ; } routesWithoutSpaces = routesWithoutSpaces . replace ( this . transformAngular8ImportSyntax , '$1"$4#$6"' ) ; routesWithoutSpaces = routesWithoutSpaces . replace ( this . transformAngular8ImportSyntaxAsyncAwait , '$1"$4#$6"' ) ; return routesWithoutSpaces ; } public setRootModule ( module ) { this . rootModule = module ; } public hasRouterModuleInImports ( imports ) { for ( let i = 0 ; i < imports . length ; i ++ ) { if ( imports [ i ] . name . indexOf ( 'RouterModule.forChild' ) !== - 1 || imports [ i ] . name . indexOf ( 'RouterModule.forRoot' ) !== - 1 || imports [ i ] . name . indexOf ( 'RouterModule' ) !== - 1 ) { return true ; } } return false ; } public fixIncompleteRoutes ( miscellaneousVariables ) { let matchingVariables = [ ] ; for ( let i = 0 ; i < this . incompleteRoutes . length ; i ++ ) { for ( let j = 0 ; j < miscellaneousVariables . length ; j ++ ) { if ( this . incompleteRoutes [ i ] . data . indexOf ( miscellaneousVariables [ j ] . name ) !== - 1 ) { console . log ( 'found one misc var inside incompleteRoute' ) ; console . log ( miscellaneousVariables [ j ] . name ) ; matchingVariables . push ( miscellaneousVariables [ j ] ) ; } } this . incompleteRoutes [ i ] . data = this . incompleteRoutes [ i ] . data . replace ( '[' , '' ) ; this . incompleteRoutes [ i ] . data = this . incompleteRoutes [ i ] . data . replace ( ']' , '' ) ; } } public linkModulesAndRoutes ( ) { let i = 0 ; let len = this . modulesWithRoutes . length ; for ( i ; i < len ; i ++ ) { _ . forEach ( this . modulesWithRoutes [ i ] . importsNode , ( node ) => { let initializer = node . initializer as ts . ArrayLiteralExpression ; if ( initializer ) { if ( initializer . elements ) { _ . forEach ( initializer . elements , ( element ) => { if ( element . arguments ) { _ . forEach ( element . arguments , ( argument ) => { _ . forEach ( this . routes , route => { if ( argument . text && route . name === argument . text && route . filename === this . modulesWithRoutes [ i ] . filename ) { route . module = this . modulesWithRoutes [ i ] . name ; } else if ( argument . text && route . name === argument . text && route . filename !== this . modulesWithRoutes [ i ] . filename ) { let argumentImportPath = ImportsUtil . findFilePathOfImportedVariable ( argument . text , this . modulesWithRoutes [ i ] . filename ) ; argumentImportPath = argumentImportPath . replace ( process . cwd ( ) + path . sep , '' ) . replace ( /\\/g , '/' ) ; if ( argument . text && route . name === argument . text && route . filename === argumentImportPath ) { route . module = this . modulesWithRoutes [ i ] . name ; } } } ) ; } ) ; } } ) ; } } /**
                 * direct support of for example
                 * export const HomeRoutingModule: ModuleWithProviders = RouterModule.forChild(HOME_ROUTES);
                 */ if ( ts . isCallExpression ( node ) ) { if ( node . arguments ) { _ . forEach ( node . arguments , ( argument ) => { _ . forEach ( this . routes , route => { if ( argument . text && route . name === argument . text && route . filename === this . modulesWithRoutes [ i ] . filename ) { route . module = this . modulesWithRoutes [ i ] . name ; } } ) ; } ) ; } } } ) ; } } public foundRouteWithModuleName ( moduleName ) { return _ . find ( this . routes , { module : moduleName } ) ; } public foundLazyModuleWithPath ( modulePath ) { let split = modulePath . split ( '#' ) ; let lazyModulePath = split [ 0 ] ; let lazyModuleName = split [ 1 ] ; return lazyModuleName ; } public constructRoutesTree ( ) { traverse ( this . modulesTree ) . forEach ( function ( node ) { if ( node ) { if ( node . parent ) { delete node . parent ; } if ( node . initializer ) { delete node . initializer ; } if ( node . importsNode ) { delete node . importsNode ; } } } ) ; this . cleanModulesTree = _ . cloneDeep ( this . modulesTree ) ; let routesTree = { name : '<root>' , kind : 'module' , className : this . rootModule , children : [ ] } ; let loopModulesParser = node => { if ( node . children && node . children . length > 0 ) { for ( let i in node . children ) { let route = this . foundRouteWithModuleName ( node . children [ i ] . name ) ; if ( route && route . data ) { try { route . children = JSON5 . parse ( route . data ) ; } catch ( e ) { logger . error ( 'Error during generation of routes JSON file, maybe a trailing comma or an external variable inside one route.' ) ; } delete route . data ; route . kind = 'module' ; routesTree . children . push ( route ) ; } if ( node . children [ i ] . children ) { loopModulesParser ( node . children [ i ] ) ; } } } else { let rawRoutes = this . foundRouteWithModuleName ( node . name ) ; if ( rawRoutes ) { let routes = JSON5 . parse ( rawRoutes . data ) ; if ( routes ) { let i = 0 ; let len = routes . length ; let routeAddedOnce = false ; for ( i ; i < len ; i ++ ) { let route = routes [ i ] ; if ( routes [ i ] . component ) { routeAddedOnce = true ; routesTree . children . push ( { kind : 'component' , component : routes [ i ] . component , path : routes [ i ] . path } ) ; } } if ( ! routeAddedOnce ) { routesTree . children = [ ... routesTree . children , ... routes ] ; } } } } } ; let startModule = _ . find ( this . cleanModulesTree , { name : this . rootModule } ) ; if ( startModule ) { loopModulesParser ( startModule ) ; } let cleanedRoutesTree = undefined ; let cleanRoutesTree = route => { for ( let i in route . children ) { let routes = route . children [ i ] . routes ; } return route ; } ; cleanedRoutesTree = cleanRoutesTree ( routesTree ) ; let loopInsideModule = ( mod , _rawModule ) => { if ( mod . children ) { for ( let z in mod . children ) { let route = this . foundRouteWithModuleName ( mod . children [ z ] . name ) ; if ( typeof route !== 'undefined' ) { if ( route . data ) { route . children = JSON5 . parse ( route . data ) ; delete route . data ; route . kind = 'module' ; _rawModule . children . push ( route ) ; } } } } else { let route = this . foundRouteWithModuleName ( mod . name ) ; if ( typeof route !== 'undefined' ) { if ( route . data ) { route . children = JSON5 . parse ( route . data ) ; delete route . data ; route . kind = 'module' ; _rawModule . children . push ( route ) ; } } } } ; let loopRoutesParser = route => { if ( route . children ) { for ( let i in route . children ) { if ( route . children [ i ] . loadChildren ) { let child = this . foundLazyModuleWithPath ( route . children [ i ] . loadChildren ) ; let module = _ . find ( this . cleanModulesTree , { name : child } ) ; if ( module ) { let _rawModule = { } ; _rawModule . kind = 'module' ; _rawModule . children = [ ] ; _rawModule . module = module . name ; loopInsideModule ( module , _rawModule ) ; route . children [ i ] . children = [ ] ; route . children [ i ] . children . push ( _rawModule ) ; } } loopRoutesParser ( route . children [ i ] ) ; } } } ; loopRoutesParser ( cleanedRoutesTree ) ; return cleanedRoutesTree ; } public constructModulesTree ( ) { let getNestedChildren = ( arr , parent ? ) => { let out = [ ] ; for ( let i in arr ) { if ( arr [ i ] . parent === parent ) { let children = getNestedChildren ( arr , arr [ i ] . name ) ; if ( children . length ) { arr [ i ] . children = children ; } out . push ( arr [ i ] ) ; } } return out ; } ; _ . forEach ( this . modules , firstLoopModule => { _ . forEach ( firstLoopModule . importsNode , importNode => { _ . forEach ( this . modules , module => { if ( module . name === importNode . name ) { module . parent = firstLoopModule . name ; } } ) ; } ) ; } ) ; this . modulesTree = getNestedChildren ( this . modules ) ; } public generateRoutesIndex ( outputFolder , routes ) { return FileEngine . get ( __dirname + '/../src/templates/partials/routes-index.hbs' ) . then ( data => { let template = Handlebars . compile ( data ) ; let result = template ( { routes : JSON . stringify ( routes ) } ) ; let testOutputDir = outputFolder . match ( process . cwd ( ) ) ; if ( testOutputDir && testOutputDir . length > 0 ) { outputFolder = outputFolder . replace ( process . cwd ( ) + path . sep , '' ) ; } return FileEngine . write ( outputFolder + path . sep + '/js/routes/routes_index.js' , result ) ; } , err => Promise . reject ( 'Error during routes index generation' ) ) ; } public routesLength ( ) { let _n = 0 ; let routesParser = route => { if ( typeof route . path !== 'undefined' ) { _n += 1 ; } if ( route . children ) { for ( let j in route . children ) { routesParser ( route . children [ j ] ) ; } } } ; for ( let i in this . routes ) { routesParser ( this . routes [ i ] ) ; } return _n ; } public printRoutes ( ) { console . log ( '' ) ; console . log ( 'printRoutes: ' ) ; console . log ( this . routes ) ; } public printModulesRoutes ( ) { console . log ( '' ) ; console . log ( 'printModulesRoutes: ' ) ; console . log ( this . modulesWithRoutes ) ; } public isVariableRoutes ( node ) { let result = false ; if ( node . declarationList && node . declarationList . declarations ) { let i = 0 ; let len = node . declarationList . declarations . length ; for ( i ; i < len ; i ++ ) { if ( node . declarationList . declarations [ i ] . type ) { if ( node . declarationList . declarations [ i ] . type . typeName && node . declarationList . declarations [ i ] . type . typeName . text === 'Routes' ) { result = true ; } } } } return result ; } public cleanFileIdentifiers ( sourceFile ) { let file = sourceFile ; const identifiers = file . getDescendantsOfKind ( SyntaxKind . Identifier ) . filter ( p => { return ( Node . isArrayLiteralExpression ( p . getParentOrThrow ( ) ) || Node . isPropertyAssignment ( p . getParentOrThrow ( ) ) ) ; } ) ; let identifiersInRoutesVariableStatement = [ ] ; for ( const identifier of identifiers ) { let foundParentVariableStatement = false ; let parent = identifier . getParentWhile ( n => { if ( n . getKind ( ) === SyntaxKind . VariableStatement ) { if ( this . isVariableRoutes ( n . compilerNode ) ) { foundParentVariableStatement = true ; } } return true ; } ) ; if ( foundParentVariableStatement ) { identifiersInRoutesVariableStatement . push ( identifier ) ; } } for ( const identifier of identifiersInRoutesVariableStatement ) { const identifierDeclaration = identifier . getSymbolOrThrow ( ) . getValueDeclarationOrThrow ( ) ; if ( ! Node . isPropertyAssignment ( identifierDeclaration ) && Node . isVariableDeclaration ( identifierDeclaration ) && Node . isPropertyAssignment ( identifierDeclaration ) && ! Node . isVariableDeclaration ( identifierDeclaration ) ) { throw new Error ( `Not implemented referenced declaration kind: ${ identifierDeclaration . getKindName ( ) }` ) ; } if ( Node . isVariableDeclaration ( identifierDeclaration ) ) { identifier . replaceWithText ( identifierDeclaration . getInitializerOrThrow ( ) . getText ( ) ) ; } } return file ; } public cleanFileSpreads ( sourceFile ) { let file = sourceFile ; const spreadElements = file . getDescendantsOfKind ( SyntaxKind . SpreadElement ) . filter ( p => Node . isArrayLiteralExpression ( p . getParentOrThrow ( ) ) ) ; let spreadElementsInRoutesVariableStatement = [ ] ; for ( const spreadElement of spreadElements ) { let foundParentVariableStatement = false ; let parent = spreadElement . getParentWhile ( n => { if ( n . getKind ( ) === SyntaxKind . VariableStatement ) { if ( this . isVariableRoutes ( n . compilerNode ) ) { foundParentVariableStatement = true ; } } return true ; } ) ; if ( foundParentVariableStatement ) { spreadElementsInRoutesVariableStatement . push ( spreadElement ) ; } } for ( const spreadElement of spreadElementsInRoutesVariableStatement ) { let spreadElementIdentifier = spreadElement . getExpression ( ) . getText ( ) , searchedImport , aliasOriginalName = '' , foundWithAliasInImports = false , foundWithAlias = false ; const imports = file . getImportDeclarations ( ) ; imports . forEach ( i => { let namedImports = i . getNamedImports ( ) , namedImportsLength = namedImports . length , j = 0 ; if ( namedImportsLength > 0 ) { for ( j ; j < namedImportsLength ; j ++ ) { let importName = namedImports [ j ] . getNameNode ( ) . getText ( ) as string , importAlias ; if ( namedImports [ j ] . getAliasNode ( ) ) { importAlias = namedImports [ j ] . getAliasNode ( ) . getText ( ) ; } if ( importName === spreadElementIdentifier ) { foundWithAliasInImports = true ; searchedImport = i ; break ; } if ( importAlias === spreadElementIdentifier ) { foundWithAliasInImports = true ; foundWithAlias = true ; aliasOriginalName = importName ; searchedImport = i ; break ; } } } } ) ; let referencedDeclaration ; if ( foundWithAliasInImports ) { if ( typeof searchedImport !== 'undefined' ) { const routePathIsBad = path => { return typeof ast . getSourceFile ( path ) == 'undefined' ; } ; const getIndicesOf = ( searchStr , str , caseSensitive ) => { var searchStrLen = searchStr . length ; if ( searchStrLen == 0 ) { return [ ] ; } var startIndex = 0 , index , indices = [ ] ; if ( ! caseSensitive ) { str = str . toLowerCase ( ) ; searchStr = searchStr . toLowerCase ( ) ; } while ( ( index = str . indexOf ( searchStr , startIndex ) ) > - 1 ) { indices . push ( index ) ; startIndex = index + searchStrLen ; } return indices ; } ; const dirNamePath = path . dirname ( file . getFilePath ( ) ) ; const searchedImportPath = searchedImport . getModuleSpecifierValue ( ) ; const leadingFilePath = searchedImportPath . split ( '/' ) . shift ( ) ; let importPath = path . resolve ( dirNamePath + '/' + searchedImport . getModuleSpecifierValue ( ) + '.ts' ) ; if ( routePathIsBad ( importPath ) ) { let leadingIndices = getIndicesOf ( leadingFilePath , importPath , true ) ; if ( leadingIndices . length > 1 ) { let startIndex = leadingIndices [ 0 ] ; let endIndex = leadingIndices [ leadingIndices . length - 1 ] ; importPath = importPath . slice ( 0 , startIndex ) + importPath . slice ( endIndex ) ; } else { importPath = path . dirname ( dirNamePath ) + '/' + searchedImportPath + '.ts' ; } } const sourceFileImport = typeof ast . getSourceFile ( importPath ) !== 'undefined' ? ast . getSourceFile ( importPath ) : ast . addSourceFileAtPath ( importPath ) ; if ( sourceFileImport ) { let variableName = foundWithAlias ? aliasOriginalName : spreadElementIdentifier ; referencedDeclaration = sourceFileImport . getVariableDeclaration ( variableName ) ; } } } else { referencedDeclaration = spreadElement . getExpression ( ) . getSymbolOrThrow ( ) . getValueDeclarationOrThrow ( ) ; } if ( ! Node . isVariableDeclaration ( referencedDeclaration ) ) { throw new Error ( `Not implemented referenced declaration kind: ${ referencedDeclaration . getKindName ( ) }` ) ; } const referencedArray = referencedDeclaration . getInitializerIfKindOrThrow ( SyntaxKind . ArrayLiteralExpression ) ; const spreadElementArray = spreadElement . getParentIfKindOrThrow ( SyntaxKind . ArrayLiteralExpression ) ; const insertIndex = spreadElementArray . getElements ( ) . indexOf ( spreadElement ) ; spreadElementArray . removeElement ( spreadElement ) ; spreadElementArray . insertElements ( insertIndex , referencedArray . getElements ( ) . map ( e => e . getText ( ) ) ) ; } return file ; } public cleanFileDynamics ( sourceFile ) { let file = sourceFile ; const propertyAccessExpressions = file . getDescendantsOfKind ( SyntaxKind . PropertyAccessExpression ) . filter ( p => ! Node . isPropertyAccessExpression ( p . getParentOrThrow ( ) ) ) ; let propertyAccessExpressionsInRoutesVariableStatement = [ ] ; for ( const propertyAccessExpression of propertyAccessExpressions ) { let foundParentVariableStatement = false ; let parent = propertyAccessExpression . getParentWhile ( n => { if ( n . getKind ( ) === SyntaxKind . VariableStatement ) { if ( this . isVariableRoutes ( n . compilerNode ) ) { foundParentVariableStatement = true ; } } return true ; } ) ; if ( foundParentVariableStatement ) { propertyAccessExpressionsInRoutesVariableStatement . push ( propertyAccessExpression ) ; } } for ( const propertyAccessExpression of propertyAccessExpressionsInRoutesVariableStatement ) { const propertyAccessExpressionNodeName = propertyAccessExpression . getNameNode ( ) ; if ( propertyAccessExpressionNodeName ) { try { const propertyAccessExpressionNodeNameSymbol = propertyAccessExpressionNodeName . getSymbolOrThrow ( ) ; if ( propertyAccessExpressionNodeNameSymbol ) { const referencedDeclaration = propertyAccessExpressionNodeNameSymbol . getValueDeclarationOrThrow ( ) ; if ( ! Node . isPropertyAssignment ( referencedDeclaration ) && Node . isEnumMember ( referencedDeclaration ) && Node . isPropertyAssignment ( referencedDeclaration ) && ! Node . isEnumMember ( referencedDeclaration ) ) { throw new Error ( `Not implemented referenced declaration kind: ${ referencedDeclaration . getKindName ( ) }` ) ; } if ( typeof referencedDeclaration . getInitializerOrThrow !== 'undefined' ) { propertyAccessExpression . replaceWithText ( referencedDeclaration . getInitializerOrThrow ( ) . getText ( ) ) ; } } } catch ( e ) { } } } return file ; } param sourceFile public cleanCallExpressions ( sourceFile ) { let file = sourceFile ; const variableStatements = sourceFile . getVariableDeclaration ( v => { let result = false ; const type = v . compilerNode . type ; if ( typeof type !== 'undefined' && typeof type . typeName !== 'undefined' ) { result = type . typeName . text === 'Routes' ; } return result ; } ) ; const initializer = variableStatements . getInitializer ( ) ; for ( const callExpr of initializer . getDescendantsOfKind ( SyntaxKind . CallExpression ) ) { if ( callExpr . wasForgotten ( ) ) { continue ; } callExpr . replaceWithText ( writer => writer . quote ( callExpr . getText ( ) ) ) ; } return file ; } param { ts . Node } initializer return { ts . Node } public cleanRoutesDefinitionWithImport ( initializer , node , sourceFile ) { initializer . elements . forEach ( ( element ) => { element . properties . forEach ( ( property ) => { let propertyName = property . name . getText ( ) , propertyInitializer = property . initializer ; switch ( propertyName ) { case 'path' : case 'redirectTo' : case 'outlet' : case 'pathMatch' : if ( propertyInitializer ) { if ( propertyInitializer . kind !== SyntaxKind . StringLiteral ) { if ( propertyInitializer . kind === SyntaxKind . PropertyAccessExpression ) { let lastObjectLiteralAttributeName = propertyInitializer . name . getText ( ) , firstObjectLiteralAttributeName ; if ( propertyInitializer . expression ) { firstObjectLiteralAttributeName = propertyInitializer . expression . getText ( ) ; let result = ImportsUtil . findPropertyValueInImportOrLocalVariables ( firstObjectLiteralAttributeName + '.' + lastObjectLiteralAttributeName , sourceFile ) ; if ( result !== '' ) { propertyInitializer . kind = 9 ; propertyInitializer . text = result ; } } } } } break ; } } ) ; } ) ; return initializer ; } } export default RouterParserUtil . getInstance ( ) ;