UNPKG

@compodoc/compodoc

Version:

The missing documentation tool for your Angular application

1,080 lines (976 loc) 47.8 kB
const Handlebars = require('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('neotraverse/legacy'); const ast = new Project(); export class RouterParserUtil { public scannedFiles: any[] = []; 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 transformAngular8ImportSyntaxComponent = /(['"]loadComponent['"]:)\(\)(:[^)]+?)?=>"import\((\\'|'|"|`)([^'"]+?)(\\'|'|"|`)\)\.then\(\(?\w+?\)?=>\S+?\.([^)]+?)\)(\\'|'|")/g; private transformAngular8ImportSyntaxAsyncAwait = /(['"]loadChildren['"]:)\(\)(:[^)]+?)?=>\("import\((\\'|'|"|`)([^'"]+?)(\\'|'|"|`)\)"\)\.['"]([^)]+?)['"]/g; private transformAngular8ImportSyntaxComponentAsyncAwait = /(['"]loadComponent['"]:)\(\)(:[^)]+?)?=>\("import\((\\'|'|"|`)([^'"]+?)(\\'|'|"|`)\)"\)\.['"]([^)]+?)['"]/g; private trailingComma = /,\s*([\]})])/g; 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 { let cleaned = route .replace(/\s/g, '') .replace(this.trailingComma, '$1') .replace(this.transformAngular8ImportSyntax, '$1"$4#$6"') .replace(this.transformAngular8ImportSyntaxAsyncAwait, '$1"$4#$6"') .replace(this.transformAngular8ImportSyntaxComponent, '$1"$4#$6"') .replace(this.transformAngular8ImportSyntaxComponentAsyncAwait, '$1"$4#$6"'); // Additional cleaning for special characters that cause JSON5 parsing issues // Handle unescaped characters in string literals cleaned = cleaned // Fix template literal expressions that get converted incorrectly // Convert ${VAR}/something patterns to "VAR/something" format .replace(/\$\{([^}]+)\}\/([^"',}\s]+)/g, '"$1/$2"') .replace(/\$\{([^}]+)\}/g, '"$1"') // Fix malformed string concatenations from template literals .replace(/"([^"]*?)"\/"([^"]*?)"/g, '"$1/$2"') .replace(/"([^"]*?)"\+([^"]*?)\+"([^"]*?)"/g, '"$1+$2+$3"') // Fix double quotes issues in path strings .replace(/""([^"]*?)""/g, '"$1"') // Fix malformed string concatenations .replace(/([^"])"([^"]*?)\.([^"]*?)"([^"])/g, '$1"$2\\.$3"$4') // Fix unescaped plus signs in string literals .replace(/([^"])"([^"]*?)\+([^"]*?)"([^"])/g, '$1"$2\\+$3"$4') // Fix unescaped parentheses in string literals .replace(/([^"])"([^"]*?)\(([^"]*?)"([^"])/g, '$1"$2\\($3"$4') .replace(/([^"])"([^"]*?)\)([^"]*?)"([^"])/g, '$1"$2\\)$3"$4'); return cleaned; } 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; } 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 }; // Process routes data if available to extract components and paths for (const route of this.routes) { try { const routeData = JSON.parse(route.data); for (const routeItem of routeData) { if (routeItem.component && isValidName(routeItem.component)) { validChildren.push({ name: routeItem.component, kind: 'component', path: routeItem.path || '', filename: route.filename }); } if (routeItem.loadChildren) { // Extract module name from loadChildren const moduleMatch = routeItem.loadChildren.match(/#(\w+)/); if (moduleMatch && isValidName(moduleMatch[1])) { validChildren.push({ name: moduleMatch[1], kind: 'module', path: routeItem.path || '', filename: 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*"(\w+Component)"/g); if (componentMatches) { for (const match of componentMatches) { const componentNameMatch = match.match(/"component"\s*:\s*"(\w+Component)"/); 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 redirectTo values with strict validation 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', filename: route.filename }); } } } // Handle static enum values by detecting enum.property patterns const enumMappings = { 'ABOUT_ENUMS.todomvc': 'todomvcinstaticclass', 'APP_ENUM.homeenumimported': 'homeenumimported', 'APP_ENUM.homeenuminfile': 'homeenuminfile' }; for (const [enumPattern, staticValue] of Object.entries(enumMappings)) { // Look for various patterns that might appear in route data: const patterns = [ enumPattern, // ABOUT_ENUMS.todomvc `"${enumPattern.replace('.', '"."')}"`, // "ABOUT_ENUMS"."todomvc" `"${enumPattern.replace('.', '\\"."')}"`, // "ABOUT_ENUMS\."todomvc" enumPattern.replace('.', '"."'), // ABOUT_ENUMS"."todomvc enumPattern.replace('.', '\\"."'), // ABOUT_ENUMS\."todomvc `"${enumPattern.split('.')[0]}"\\."${enumPattern.split('.')[1]}"` // "ABOUT_ENUMS"\."todomvc" ]; let found = false; for (const pattern of patterns) { if (route.data.includes(pattern)) { found = true; break; } } if (found && !validChildren.some(child => child.name === staticValue)) { validChildren.push({ name: staticValue, kind: 'route-path', filename: 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 routesTree = { name: '<root>', kind: 'module', className: this.rootModule, children: validChildren }; 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) { let i = 0; const 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: SourceFile): SourceFile { const file = sourceFile; const identifiers = file.getDescendantsOfKind(SyntaxKind.Identifier).filter(p => { return ( Node.isArrayLiteralExpression(p.getParentOrThrow()) || Node.isPropertyAssignment(p.getParentOrThrow()) ); }); const identifiersInRoutesVariableStatement = []; for (const identifier of identifiers) { // Loop through their parents nodes, and if one is a variableStatement and === 'routes' let foundParentVariableStatement = false; identifier.getParentWhile(n => { if (n.getKind() === SyntaxKind.VariableStatement) { if (this.isVariableRoutes(n.compilerNode)) { foundParentVariableStatement = true; } } return true; }); if (foundParentVariableStatement) { identifiersInRoutesVariableStatement.push(identifier); } } // inline the property access expressions for (const identifier of identifiersInRoutesVariableStatement) { const identifierDeclaration = identifier .getSymbolOrThrow() .getValueDeclarationOrThrow(); if ( !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: SourceFile): SourceFile { const file = sourceFile; const spreadElements = file .getDescendantsOfKind(SyntaxKind.SpreadElement) .filter(p => Node.isArrayLiteralExpression(p.getParentOrThrow())); const spreadElementsInRoutesVariableStatement = []; for (const spreadElement of spreadElements) { // Loop through their parents nodes, and if one is a variableStatement and === 'routes' let foundParentVariableStatement = false; spreadElement.getParentWhile(n => { if (n.getKind() === SyntaxKind.VariableStatement) { if (this.isVariableRoutes(n.compilerNode)) { foundParentVariableStatement = true; } } return true; }); if (foundParentVariableStatement) { spreadElementsInRoutesVariableStatement.push(spreadElement); } } // inline the ArrayLiteralExpression SpreadElements for (const spreadElement of spreadElementsInRoutesVariableStatement) { let spreadElementIdentifier = spreadElement.getExpression().getText(), searchedImport, aliasOriginalName = '', foundWithAliasInImports = false, foundWithAlias = false; // Try to find it in imports 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 => { const result = this.scannedFiles.find( scannedFile => path === scannedFile.path ); return !result; }; const getIndicesOf = (searchStr, str, caseSensitive) => { const searchStrLen = searchStr.length; if (searchStrLen == 0) { return []; } let 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)) { const leadingIndices = getIndicesOf(leadingFilePath, importPath, true); if (leadingIndices.length > 1) { // Nested route fixes const startIndex = leadingIndices[0]; const endIndex = leadingIndices[leadingIndices.length - 1]; importPath = importPath.slice(0, startIndex) + importPath.slice(endIndex); } else { // Top level route fixes importPath = path.dirname(dirNamePath) + '/' + searchedImportPath + '.ts'; } } const sourceFileImport = typeof ast.getSourceFile(importPath) !== 'undefined' ? ast.getSourceFile(importPath) : ast.addSourceFileAtPath(importPath); if (sourceFileImport) { const variableName = foundWithAlias ? aliasOriginalName : spreadElementIdentifier; referencedDeclaration = sourceFileImport.getVariableDeclaration(variableName); } } } else { // if not, try directly in file 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: SourceFile): SourceFile { const file = sourceFile; const propertyAccessExpressions = file .getDescendantsOfKind(SyntaxKind.PropertyAccessExpression) .filter(p => !Node.isPropertyAccessExpression(p.getParentOrThrow())); const propertyAccessExpressionsInRoutesVariableStatement = []; for (const propertyAccessExpression of propertyAccessExpressions) { // Loop through their parents nodes, and if one is a variableStatement and === 'routes' let foundParentVariableStatement = false; propertyAccessExpression.getParentWhile(n => { if (n.getKind() === SyntaxKind.VariableStatement) { if (this.isVariableRoutes(n.compilerNode)) { foundParentVariableStatement = true; } } return true; }); if (foundParentVariableStatement) { propertyAccessExpressionsInRoutesVariableStatement.push(propertyAccessExpression); } } // inline the property access expressions for (const propertyAccessExpression of propertyAccessExpressionsInRoutesVariableStatement) { const propertyAccessExpressionNodeName = propertyAccessExpression.getNameNode(); if (propertyAccessExpressionNodeName) { try { const propertyAccessExpressionNodeNameSymbol = propertyAccessExpressionNodeName.getSymbol(); if (propertyAccessExpressionNodeNameSymbol) { const referencedDeclaration = propertyAccessExpressionNodeNameSymbol.getValueDeclarationOrThrow(); if ( !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() ); } } // If symbol is null/undefined, just skip this property access expression } catch (e) { // Gracefully handle cases where symbols cannot be resolved // This is common with dynamic imports and other runtime expressions // We'll just skip processing this property access expression continue; } } } return file; } /** * replace callexpressions with string : utils.doWork() -> 'utils.doWork()' doWork() -> 'doWork()' * @param sourceFile ts.SourceFile */ public cleanCallExpressions(sourceFile: SourceFile): SourceFile { const file = sourceFile; // Find all variable declarations with Routes type const variableDeclarations = sourceFile.getVariableDeclarations(); const routesVariableDeclarations = variableDeclarations.filter(v => { const type = v.compilerNode.type; if (typeof type !== 'undefined' && ts.isTypeReferenceNode(type) && typeof type.typeName !== 'undefined') { return (type.typeName as ts.Identifier).text === 'Routes'; } return false; }); if (routesVariableDeclarations.length === 0) { return file; } // Process all Routes variable declarations for (const variableDeclaration of routesVariableDeclarations) { const initializer = variableDeclaration.getInitializer(); if (!initializer) { continue; } for (const callExpr of initializer.getDescendantsOfKind(SyntaxKind.CallExpression)) { if (callExpr.wasForgotten()) { continue; } callExpr.replaceWithText(writer => writer.quote(callExpr.getText())); } } return file; } /** * Clean routes definition with imported data, for example path, children, or dynamic stuff inside data * * const MY_ROUTES: Routes = [ * { * path: 'home', * component: HomeComponent * }, * { * path: PATHS.home, * component: HomeComponent * } * ]; * * The initializer is an array (ArrayLiteralExpression - 177 ), it has elements, objects (ObjectLiteralExpression - 178) * with properties (PropertyAssignment - 261) * * For each know property (https://angular.io/api/router/Routes#description), we try to see if we have what we want * * Ex: path and pathMatch want a string, component a component reference. * * It is an imperative approach, not a generic way, parsing all the tree * and find something like this which willl break JSON.stringify : MYIMPORT.path * * @param {ts.Node} initializer The node of routes definition * @return {ts.Node} The edited node */ public cleanRoutesDefinitionWithImport( initializer: ts.ArrayLiteralExpression, node: ts.Node, sourceFile: ts.SourceFile ): ts.Node { initializer.elements.forEach((element: ts.ObjectLiteralExpression) => { element.properties.forEach((property: ts.PropertyAssignment) => { const propertyName = property.name.getText(), propertyInitializer = property.initializer; switch (propertyName) { case 'path': case 'redirectTo': case 'outlet': case 'pathMatch': if (propertyInitializer) { if (propertyInitializer.kind !== SyntaxKind.StringLiteral) { // Identifier(71) won't break parsing, but it will be better to retrive them // PropertyAccessExpression(179) ex: MYIMPORT.path will break it, find it in import if ( propertyInitializer.kind === SyntaxKind.PropertyAccessExpression && ts.isPropertyAccessExpression(propertyInitializer) ) { let lastObjectLiteralAttributeName = propertyInitializer.name.getText(), firstObjectLiteralAttributeName; if (propertyInitializer.expression) { firstObjectLiteralAttributeName = propertyInitializer.expression.getText(); const result = ImportsUtil.findPropertyValueInImportOrLocalVariables( firstObjectLiteralAttributeName + '.' + lastObjectLiteralAttributeName, sourceFile ); // tslint:disable-line if (result !== '') { (propertyInitializer as any).kind = 9; (propertyInitializer as any).text = result; } } } } } break; } }); }); return initializer; } } export default RouterParserUtil.getInstance();