@compodoc/compodoc
Version:
The missing documentation tool for your Angular application
1,250 lines (1,114 loc) • 74.8 kB
text/typescript
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