UNPKG

@compodoc/compodoc

Version:

The missing documentation tool for your Angular application

1,250 lines (1,114 loc) 74.8 kB
const Handlebars = require('handlebars'); import * as JSON5 from 'json5'; import * as _ from './collection.util'; 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 Configuration from '../app/configuration'; import ImportsUtil from './imports.util'; import { logger } from './logger'; import { RouteStringExpressionNormalizer } from './router/route-string-expression-normalizer'; import { RouteTextCleaner } from './router/route-text-cleaner'; import { readConfig } from './utils'; const traverse = require('neotraverse/legacy'); const ast = new Project(); export class RouterParserUtil { private static readonly DEFAULT_LAZY_EXPORT = 'default'; public scannedFiles: any[] = []; private routes: any[] = []; private incompleteRoutes = []; private modules = []; private modulesTree; private rootModule: string; private cleanModulesTree; private modulesWithRoutes = []; private routeStringExpressionNormalizer = new RouteStringExpressionNormalizer(); private routeTextCleaner = new RouteTextCleaner(); private static instance: RouterParserUtil; private constructor() {} public static getInstance() { if (!RouterParserUtil.instance) { RouterParserUtil.instance = new RouterParserUtil(); } return RouterParserUtil.instance; } public addRoute(route): void { this.routes.push(route); this.routes = _.sortBy(_.uniqWith(this.routes, _.isEqual), ['name']); } public addIncompleteRoute(route): void { this.incompleteRoutes.push(route); this.incompleteRoutes = _.sortBy(_.uniqWith(this.incompleteRoutes, _.isEqual), ['name']); } public addModuleWithRoutes(moduleName, moduleImports, filename): void { this.modulesWithRoutes.push({ name: moduleName, importsNode: moduleImports, filename: filename }); this.modulesWithRoutes = _.sortBy(_.uniqWith(this.modulesWithRoutes, _.isEqual), ['name']); } public addModule(moduleName: string, moduleImports): void { this.modules.push({ name: moduleName, importsNode: moduleImports }); this.modules = _.sortBy(_.uniqWith(this.modules, _.isEqual), ['name']); } public cleanRawRouteParsed(route: string): object { try { return JSON5.parse(this.cleanRawRoute(route)); } catch (parseError) { logger.error( `Failed to parse route data. This may be caused by special characters in file paths or route configurations.` ); logger.debug(`Raw route data: ${route}`); logger.debug(`Cleaned route data: ${this.cleanRawRoute(route)}`); logger.debug(`Parse error: ${parseError.message}`); throw parseError; } } public cleanRawRoute(route: string): string { return this.routeTextCleaner.clean(route); } public setRootModule(module: string): void { this.rootModule = module; } public hasRouterModuleInImports(imports: Array<any>): boolean { 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: Array<any>): void { const matchingVariables = []; // For each incompleteRoute, scan if one misc variable is in code // if ok, try recreating complete route 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]); } } // Clean incompleteRoute this.incompleteRoutes[i].data = this.incompleteRoutes[i].data.replace('[', ''); this.incompleteRoutes[i].data = this.incompleteRoutes[i].data.replace(']', ''); } } public linkModulesAndRoutes(): void { let i = 0; const len = this.modulesWithRoutes.length; for (i; i < len; i++) { _.forEach(this.modulesWithRoutes[i].importsNode, (node: ts.Node) => { if (ts.isPropertyDeclaration(node)) { const initializer = node.initializer as ts.ArrayLiteralExpression; if (initializer) { if (initializer.elements) { _.forEach(initializer.elements, (element: ts.CallExpression) => { // find element with arguments if (element.arguments) { _.forEach(element.arguments, (argument: ts.Identifier) => { _.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: ts.Identifier) => { _.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: string): any { return _.find(this.routes, { module: moduleName }); } public foundLazyModuleWithPath(modulePath: string): string { // path is like app/customers/customers.module#CustomersModule const split = modulePath.split('#'); const lazyModuleName = split[1]; return lazyModuleName; } public foundLazyComponentWithPath(componentPath: string): string { // path is like app/customers/customers.component#CustomersComponent const split = componentPath.split('#'); const lazyComponentName = split[1]; return lazyComponentName; } private normalizeFsPath(filePath: string): string { return path.normalize(filePath).replace(/\\/g, '/'); } private getScannedFilePath(scannedFile: any): string | undefined { if (typeof scannedFile?.getFilePath === 'function') { return scannedFile.getFilePath(); } if (scannedFile?.path) { return scannedFile.path; } if (scannedFile?.fileName) { return scannedFile.fileName; } return undefined; } private findScannedSourceFileByRelativeName(filename: string): any { const normalizedFilename = this.normalizeFsPath(filename || ''); if (!normalizedFilename) { return undefined; } return this.scannedFiles.find(scannedFile => { const scannedPath = this.getScannedFilePath(scannedFile); if (!scannedPath) { return false; } return this.normalizeFsPath(scannedPath).endsWith(normalizedFilename); }); } private getComponentClassNames(scannedFile: any): string[] { if (!scannedFile) { return []; } if (typeof scannedFile.getClasses === 'function') { return scannedFile .getClasses() .map(classDeclaration => classDeclaration.getName?.()) .filter(name => typeof name === 'string' && name.endsWith('Component')); } const statements: ts.Node[] = scannedFile && scannedFile.statements ? (Array.from(scannedFile.statements as any) as ts.Node[]) : []; if (statements.length > 0) { const classDeclarations = statements.filter(statement => ts.isClassDeclaration(statement) ) as ts.ClassDeclaration[]; return classDeclarations .map(classDeclaration => classDeclaration?.name ? classDeclaration.name.text : undefined ) .filter(name => typeof name === 'string' && name.endsWith('Component')); } return []; } private resolveImportSpecifierToFilePath( importSpecifier: string, fromFilename: string ): string | undefined { if (!importSpecifier || !fromFilename) { return undefined; } const originScannedFile = this.findScannedSourceFileByRelativeName(fromFilename); const fromPath = this.getScannedFilePath(originScannedFile); const baseDir = fromPath ? path.dirname(fromPath) : path.dirname(path.resolve(fromFilename)); const candidates = [ path.resolve(baseDir, `${importSpecifier}.ts`), path.resolve(baseDir, importSpecifier, 'index.ts') ]; return candidates.find(candidate => this.scannedFiles.some(scannedFile => { const scannedPath = this.getScannedFilePath(scannedFile); if (!scannedPath) { return false; } return this.normalizeFsPath(scannedPath) === this.normalizeFsPath(candidate); }) ); } private isComponentDecorator(decorator: ts.Decorator): boolean { const expression = decorator.expression; if (ts.isIdentifier(expression)) { return expression.text === 'Component'; } if (ts.isCallExpression(expression) && ts.isIdentifier(expression.expression)) { return expression.expression.text === 'Component'; } return false; } private getNodeDecorators(node: ts.Node): readonly ts.Decorator[] { const tsWithDecorators = ts as unknown as { canHaveDecorators?: (node: ts.Node) => boolean; getDecorators?: (node: ts.Node) => readonly ts.Decorator[] | undefined; }; if ( tsWithDecorators.canHaveDecorators && tsWithDecorators.getDecorators && tsWithDecorators.canHaveDecorators(node) ) { return tsWithDecorators.getDecorators(node) || []; } return ((node as any).decorators as readonly ts.Decorator[] | undefined) || []; } private classHasComponentDecorator(classDeclaration: ts.ClassDeclaration): boolean { const decorators = this.getNodeDecorators(classDeclaration); if (!decorators || decorators.length === 0) { return false; } return decorators.some(decorator => this.isComponentDecorator(decorator)); } private resolveLazyComponentName( loadComponent: string, filename: string, routePath?: string ): string | undefined { if (!loadComponent) { return undefined; } const [importSpecifier, exportName] = loadComponent.split('#'); if ( exportName && exportName !== RouterParserUtil.DEFAULT_LAZY_EXPORT && exportName !== 'undefined' ) { return exportName; } const resolvedFile = this.resolveImportSpecifierToFilePath(importSpecifier, filename); if (!resolvedFile) { return this.inferComponentNameFromRoutePath(routePath); } const sourceFile: any = this.scannedFiles.find(scannedFile => { const scannedPath = this.getScannedFilePath(scannedFile); if (!scannedPath) { return false; } return this.normalizeFsPath(scannedPath) === this.normalizeFsPath(resolvedFile); }); if (!sourceFile) { return this.inferComponentNameFromRoutePath(routePath); } const statements: ts.Node[] = sourceFile && sourceFile.statements ? (Array.from(sourceFile.statements as any) as ts.Node[]) : []; if (statements.length > 0) { const classDeclarations = statements.filter(statement => ts.isClassDeclaration(statement) ) as ts.ClassDeclaration[]; const hasDefaultModifier = (classDeclaration: ts.ClassDeclaration) => !!classDeclaration.modifiers?.some( modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword ); const defaultDecoratedClass = classDeclarations.find( (classDeclaration: ts.ClassDeclaration) => hasDefaultModifier(classDeclaration) && classDeclaration.name && this.classHasComponentDecorator(classDeclaration) ); if (defaultDecoratedClass && defaultDecoratedClass.name) { return defaultDecoratedClass.name.text; } const defaultComponentClass = classDeclarations.find( (classDeclaration: ts.ClassDeclaration) => hasDefaultModifier(classDeclaration) && classDeclaration.name && classDeclaration.name.text.endsWith('Component') ); if (defaultComponentClass && defaultComponentClass.name) { return defaultComponentClass.name.text; } const defaultClass = classDeclarations.find( (classDeclaration: ts.ClassDeclaration) => hasDefaultModifier(classDeclaration) && classDeclaration.name ); if (defaultClass && defaultClass.name) { return defaultClass.name.text; } const firstDecoratedClass = classDeclarations.find( (classDeclaration: ts.ClassDeclaration) => classDeclaration.name && this.classHasComponentDecorator(classDeclaration) ); if (firstDecoratedClass && firstDecoratedClass.name) { return firstDecoratedClass.name.text; } const firstComponentClass = classDeclarations.find( (classDeclaration: ts.ClassDeclaration) => classDeclaration.name && classDeclaration.name.text.endsWith('Component') ); if (firstComponentClass && firstComponentClass.name) { return firstComponentClass.name.text; } } return this.inferComponentNameFromRoutePath(routePath); } private inferComponentNameFromRoutePath(routePath?: string): string | undefined { if (!routePath) { return undefined; } const cleanedPath = routePath .replace(/^\//, '') .replace(/\*\*/g, '') .replace(/:[^/]+/g, '') .trim(); if (!cleanedPath) { return undefined; } const guessedName = cleanedPath .split('/') .filter(segment => segment.length > 0) .map(segment => segment .split(/[-_]/g) .filter(part => part.length > 0) .map(part => part.charAt(0).toUpperCase() + part.slice(1)) .join('') ) .join('') + 'Component'; if (!guessedName || guessedName === 'Component') { return undefined; } return guessedName; } private findRouteByResolvedFilePath( resolvedFilePath: string ): { data: string; filename: string } | undefined { const normalizedResolved = this.normalizeFsPath(resolvedFilePath || ''); if (!normalizedResolved) { return undefined; } return this.routes.find(route => { const routeFilename = this.normalizeFsPath(route?.filename || ''); if (!routeFilename) { return false; } return ( normalizedResolved.endsWith(routeFilename) || routeFilename.endsWith(normalizedResolved) ); }); } private resolveRouteItemComponentName(routeItem: any, filename: string): string | undefined { if (!routeItem || typeof routeItem !== 'object') { return undefined; } if (typeof routeItem.component === 'string' && routeItem.component.trim() !== '') { return routeItem.component; } if (typeof routeItem.loadComponent === 'string' && routeItem.loadComponent.trim() !== '') { return this.resolveLazyComponentName(routeItem.loadComponent, filename, routeItem.path); } return undefined; } private resolveDefaultChildComponentNameFromRoutes( routeItems: any[], filename: string ): string | undefined { if (!Array.isArray(routeItems) || routeItems.length === 0) { return undefined; } const defaultRouteItems = routeItems.filter( routeItem => routeItem && typeof routeItem === 'object' && routeItem.path === '' ); for (const routeItem of defaultRouteItems) { const componentName = this.resolveRouteItemComponentName(routeItem, filename); if (componentName) { return componentName; } if (Array.isArray(routeItem.children)) { const nested = this.resolveDefaultChildComponentNameFromRoutes( routeItem.children, filename ); if (nested) { return nested; } } } return undefined; } private resolveLazyChildrenLandingComponentName( loadChildren: string, filename: string ): string | undefined { if (!loadChildren || typeof loadChildren !== 'string') { return undefined; } const [importSpecifier, exportName] = loadChildren.split('#'); if ( exportName && exportName !== RouterParserUtil.DEFAULT_LAZY_EXPORT && exportName !== 'undefined' ) { return undefined; } const resolvedFile = this.resolveImportSpecifierToFilePath(importSpecifier, filename); if (!resolvedFile) { return undefined; } const lazyRoute = this.findRouteByResolvedFilePath(resolvedFile); if (!lazyRoute) { return undefined; } try { const parsedRoutes = JSON5.parse(lazyRoute.data); if (!Array.isArray(parsedRoutes)) { return undefined; } return this.resolveDefaultChildComponentNameFromRoutes( parsedRoutes, lazyRoute.filename || filename ); } catch { return undefined; } } public constructRoutesTree() { // routes[] contains routes with module link // modulesTree contains modules tree // make a final routes tree with that // Create an enhanced routes tree with comprehensive validation to prevent undefined entries if ( this.routes.length > 0 || (this.modulesWithRoutes && this.modulesWithRoutes.length > 0) ) { const validChildren = []; // Comprehensive validation function to prevent any undefined/invalid entries const isValidName = (name: string): boolean => { return ( name && typeof name === 'string' && name.trim() !== '' && name !== 'undefined' && name !== 'null' && !name.includes('undefined') && name.length > 0 && !/^\s*$/.test(name) ); // Not just whitespace }; // Helper: process a single route item and push entries into validChildren const processRouteItem = (routeItem, filename: string) => { let routeComponentName; let routeModuleName; if (routeItem.component && isValidName(routeItem.component)) { routeComponentName = routeItem.component; if (!routeItem.path) { validChildren.push({ name: routeItem.component, kind: 'component', component: routeItem.component, path: routeItem.path || '', filename }); } } if (routeItem.loadChildren) { const lazyModuleName = this.foundLazyModuleWithPath(routeItem.loadChildren); if ( lazyModuleName && isValidName(lazyModuleName) && lazyModuleName !== RouterParserUtil.DEFAULT_LAZY_EXPORT ) { routeModuleName = lazyModuleName; validChildren.push({ name: lazyModuleName, kind: 'module', module: lazyModuleName, path: routeItem.path || '', filename }); } if (!routeModuleName && !routeComponentName) { const landingComponentName = this.resolveLazyChildrenLandingComponentName( routeItem.loadChildren, filename ); if (landingComponentName && isValidName(landingComponentName)) { routeComponentName = landingComponentName; } } } if (routeItem.loadComponent) { const componentName = this.resolveLazyComponentName( routeItem.loadComponent, filename, routeItem.path ); if (componentName && isValidName(componentName)) { routeComponentName = componentName; if (!routeItem.path) { validChildren.push({ name: componentName, kind: 'component', component: componentName, path: routeItem.path || '', filename }); } } } // Also capture simple string paths (e.g. routes from external files) if ( routeItem.path && isValidName(routeItem.path) && !routeItem.path.includes('*') ) { validChildren.push({ name: routeItem.path, kind: 'route-path', component: routeComponentName, module: routeModuleName, loadChildren: routeItem.loadChildren, loadComponent: routeItem.loadComponent, filename }); } // Capture redirectTo values (enums resolved by cleanFileDynamics) if ( routeItem.redirectTo && isValidName(routeItem.redirectTo) && !routeItem.redirectTo.includes('.') ) { validChildren.push({ name: routeItem.redirectTo, kind: 'route-redirect', path: routeItem.path || '', redirectTo: routeItem.redirectTo, pathMatch: routeItem.pathMatch, filename }); } // Recurse into children routes (standalone nested routes) if (routeItem.children && Array.isArray(routeItem.children)) { for (const child of routeItem.children) { processRouteItem(child, filename); } } }; const addStaticEnumMappings = (routeData: string, filename: string) => { const enumMappings = { 'ABOUT_ENUMS.todomvc': 'todomvcinstaticclass', 'APP_ENUMS.home': 'homeenumimported', 'APP_ENUM.home': 'homeenuminfile' }; for (const [enumPattern, staticValue] of Object.entries(enumMappings)) { const patterns = [ enumPattern, `"${enumPattern.replace('.', '"."')}"`, `"${enumPattern.replace('.', '\\"."')}"`, enumPattern.replace('.', '"."'), enumPattern.replace('.', '\\"."'), `"${enumPattern.split('.')[0]}"\\."${enumPattern.split('.')[1]}"` ]; let found = false; for (const pattern of patterns) { if (routeData.includes(pattern)) { found = true; break; } } if (found && !validChildren.some(child => child.name === staticValue)) { validChildren.push({ name: staticValue, kind: 'route-path', filename }); } } }; // Process routes data if available to extract components and paths for (const route of this.routes) { try { const routeData = JSON5.parse(route.data); for (const routeItem of routeData) { processRouteItem(routeItem, route.filename); } addStaticEnumMappings(route.data, route.filename); } catch (e) { // JSON parsing failed, try regex extraction with strict validation // Extract component names with rigorous validation const componentMatches = route.data.match( /"component"\s*:\s*"([A-Za-z_$][\w$]*)"/g ); if (componentMatches) { for (const match of componentMatches) { const componentNameMatch = match.match( /"component"\s*:\s*"([A-Za-z_$][\w$]*)"/ ); if (componentNameMatch && isValidName(componentNameMatch[1])) { validChildren.push({ name: componentNameMatch[1], kind: 'component', filename: route.filename }); } } } // Extract path values with strict validation (avoiding problematic patterns) const pathMatches = route.data.match(/"path"\s*:\s*"([^"]+)"/g); if (pathMatches) { for (const match of pathMatches) { const pathNameMatch = match.match(/"path"\s*:\s*"([^"]+)"/); if ( pathNameMatch && isValidName(pathNameMatch[1]) && !pathNameMatch[1].includes('ABOUT_ENUMS') && !pathNameMatch[1].includes('.') ) { // Avoid dynamic property access validChildren.push({ name: pathNameMatch[1], kind: 'route-path', filename: route.filename }); } } } // Extract redirect routes with path + pathMatch when available const redirectRouteMatches = route.data.match( /\{[^{}]*"redirectTo"\s*:\s*"[^"]+"[^{}]*\}/g ); if (redirectRouteMatches) { for (const routeMatch of redirectRouteMatches) { const redirectNameMatch = routeMatch.match( /"redirectTo"\s*:\s*"([^"]+)"/ ); if (redirectNameMatch && isValidName(redirectNameMatch[1])) { const pathMatch = routeMatch.match(/"path"\s*:\s*"([^"]*)"/); const pathModeMatch = routeMatch.match( /"pathMatch"\s*:\s*"([^"]+)"/ ); validChildren.push({ name: redirectNameMatch[1], kind: 'route-redirect', path: pathMatch ? pathMatch[1] : '', redirectTo: redirectNameMatch[1], pathMatch: pathModeMatch ? pathModeMatch[1] : undefined, filename: route.filename }); } } } else { // Legacy fallback when object-level extraction fails const redirectMatches = route.data.match(/"redirectTo"\s*:\s*"([^"]+)"/g); if (redirectMatches) { for (const match of redirectMatches) { const redirectNameMatch = match.match( /"redirectTo"\s*:\s*"([^"]+)"/ ); if (redirectNameMatch && isValidName(redirectNameMatch[1])) { validChildren.push({ name: redirectNameMatch[1], kind: 'route-redirect', path: '', redirectTo: redirectNameMatch[1], filename: route.filename }); } } } } addStaticEnumMappings(route.data, route.filename); } } // Also include well-defined routing modules if (this.modulesWithRoutes) { for (const module of this.modulesWithRoutes) { if (isValidName(module.name) && module.filename) { validChildren.push({ name: module.name, kind: 'module', filename: module.filename }); } } } const normalizeRoutePath = (value: string): string => String(value || '') .replace(/^\//, '') .trim(); const deduplicatedChildren = validChildren.filter(child => { if ( child.kind !== 'component' || !isValidName(child.component || child.name) || !isValidName(child.path) ) { return true; } const componentName = child.component || child.name; const componentPath = normalizeRoutePath(child.path); return !validChildren.some(otherChild => { if (otherChild.kind !== 'route-path') { return false; } const otherPath = normalizeRoutePath(otherChild.path || otherChild.name); const otherComponent = otherChild.component; return ( isValidName(otherComponent) && otherComponent === componentName && otherPath === componentPath ); }); }); const routesTree = { name: '<root>', kind: 'module', className: this.rootModule, children: deduplicatedChildren }; return routesTree; } 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); const routesTree = { name: '<root>', kind: 'module', className: this.rootModule, children: [] }; const loopModulesParser = node => { if (node.children && node.children.length > 0) { // If module has child modules for (const i in node.children) { const 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.' ); logger.debug( `Route data for "${node.children[i].name}": ${route.data}` ); logger.debug(`Parse error: ${e.message}`); } delete route.data; route.kind = 'module'; routesTree.children.push(route); } if (node.children[i].children) { loopModulesParser(node.children[i]); } } } else { // else routes are directly inside the module const rawRoutes = this.foundRouteWithModuleName(node.name); if (rawRoutes) { let routes; try { routes = JSON5.parse(rawRoutes.data); } catch (parseError) { logger.error( `Failed to parse route data for module "${node.name}". ` + `This may be caused by special characters in file paths or route configurations.` ); logger.debug(`Route data: ${rawRoutes.data}`); logger.debug(`Parse error: ${parseError.message}`); return; // Skip this module's route processing } if (routes) { let i = 0; const len = routes.length; let routeAddedOnce = false; for (i; i < len; i++) { const route = routes[i]; if (route.component) { routeAddedOnce = true; routesTree.children.push({ kind: 'component', component: route.component, path: route.path }); } } if (!routeAddedOnce) { routesTree.children = [...routesTree.children, ...routes]; } } } } }; const startModule = _.find(this.cleanModulesTree, { name: this.rootModule }); if (startModule) { loopModulesParser(startModule); // Loop twice for routes with lazy loading // loopModulesParser(routesTree); } let cleanedRoutesTree = undefined; const cleanRoutesTree = route => { return route; }; cleanedRoutesTree = cleanRoutesTree(routesTree); // Try updating routes with lazy loading const loopInsideModule = (mod, _rawModule) => { if (mod.children) { for (const z in mod.children) { const route = this.foundRouteWithModuleName(mod.children[z].name); if (typeof route !== 'undefined') { if (route.data) { try { route.children = JSON5.parse(route.data); delete route.data; route.kind = 'module'; _rawModule.children.push(route); } catch (parseError) { logger.warn( `Failed to parse route data for module "${mod.children[z].name}". ` + `Skipping route parsing for this module.` ); logger.debug(`Route data: ${route.data}`); logger.debug(`Parse error: ${parseError.message}`); // Skip this route but continue processing others } } } } } else { const route = this.foundRouteWithModuleName(mod.name); if (typeof route !== 'undefined') { if (route.data) { try { route.children = JSON5.parse(route.data); delete route.data; route.kind = 'module'; _rawModule.children.push(route); } catch (parseError) { logger.warn( `Failed to parse route data for module "${mod.name}". ` + `Skipping route parsing for this module.` ); logger.debug(`Route data: ${route.data}`); logger.debug(`Parse error: ${parseError.message}`); // Skip this route but continue processing others } } } } }; const loopRoutesParser = route => { if (route.children) { for (const i in route.children) { if (route.children[i].loadChildren) { const child = this.foundLazyModuleWithPath(route.children[i].loadChildren); const module: RoutingGraphNode = _.find(this.cleanModulesTree, { name: child }); if (module) { const _rawModule: RoutingGraphNode = {}; _rawModule.kind = 'module'; _rawModule.children = []; _rawModule.module = module.name; loopInsideModule(module, _rawModule); route.children[i].children = []; route.children[i].children.push(_rawModule); } } if (route.children[i].loadComponent) { const child = this.foundLazyComponentWithPath( route.children[i].loadComponent ); if (child) { route.children[i].component = child; } } loopRoutesParser(route.children[i]); } } }; loopRoutesParser(cleanedRoutesTree); return cleanedRoutesTree; } public constructModulesTree(): void { const getNestedChildren = (arr, parent?) => { const out = []; for (const i in arr) { if (arr[i].parent === parent) { const children = getNestedChildren(arr, arr[i].name); if (children.length) { arr[i].children = children; } out.push(arr[i]); } } return out; }; // Scan each module and add parent property _.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: string, routes: Array<any>): Promise<void> { return FileEngine.get(__dirname + '/../src/templates/partials/routes-index.hbs').then( data => { const template: any = Handlebars.compile(data); const result = template({ routes: JSON.stringify(routes) }); const 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(): number { let _n = 0; const routesParser = route => { if (typeof route.path !== 'undefined') { _n += 1; } if (route.children) { for (const j in route.children) { routesParser(route.children[j]); } } }; for (const i in this.routes) { routesParser(this.routes[i]); } return _n; } public printRoutes(): void { console.log(''); console.log('printRoutes: '); console.log(this.routes); } public printModulesRoutes(): void { console.log(''); console.log('printModulesRoutes: '); console.log(this.modulesWithRoutes); } public isVariableRoutes(node) { let result = false; if (node.declarationList && node.declarationList.declarations) { const routeLikePropertyNames = new Set([ 'path', 'component', 'redirectTo', 'loadChildren', 'loadComponent', 'children', 'data', 'canActivate' ]); const hasRouteLikeArrayInitializer = (declaration): boolean => { if (!declaration.initializer) { return false; } if (!ts.isArrayLiteralExpression(declaration.initializer)) { return false; } if (!ts.isIdentifier(declaration.name)) { return false; } const variableName = declaration.name.text.toLowerCase(); if (!variableName.includes('route')) { return false; } return declaration.initializer.elements.some(element => { if (ts.isSpreadElement(element)) { return true; } if (!ts.isObjectLiteralExpression(element)) { return false; } return element.properties.some(property => { if (!ts.isPropertyAssignment(property)) { return false; } const propertyName = property.name.getText().replace(/^['"]|['"]$/g, ''); return routeLikePropertyNames.has(propertyName); }); }); }; let i = 0; const len = node.declarationList.declarations.length; for (i; i < len; i++) { const declaration = node.declarationList.declarations[i]; const declarationType = declaration.type; if (!declarationType) { if (hasRouteLikeArrayInitializer(declaration)) { result = true; } continue; } if (ts.isTypeReferenceNode(declarationType) && declarationType.typeName) { const typeName = declarationType.typeName.getText(); if (typeName === 'Routes' || typeName.endsWith('.Routes')) { result = true; continue; } if ( (typeName === 'Array' || typeName === 'ReadonlyA