angular/devtools/projects/ng-devtools-backend/src/lib/router-tree.ts
SkyZeroZx f2ffbe31b0 refactor(devtools): enhance route guard handling and add inline function warning
refactored route guard handling and added warning for inline functions
2025-11-05 15:20:46 -08:00

248 lines
7.7 KiB
TypeScript

/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {Route} from '../../../protocol';
import type {Route as AngularRoute} from '@angular/router';
export type RoutePropertyType = RouteGuard | 'providers' | 'component' | 'redirectTo' | 'title';
export type RouteGuard = 'canActivate' | 'canActivateChild' | 'canDeactivate' | 'canMatch';
const routeGuards = ['canActivate', 'canActivateChild', 'canDeactivate', 'canMatch'];
type Routes = any;
type Router = any;
export function parseRoutes(router: Router): Route {
const currentUrl = router.stateManager?.routerState?.snapshot?.url;
const rootName = 'App Root';
const rootChildren = router.config;
const root: Route = {
component: rootName,
path: rootName,
children: rootChildren ? assignChildrenToParent(null, rootChildren, currentUrl) : [],
isAux: false,
isLazy: false,
isActive: true, // Root is always active.
data: [],
};
return root;
}
function getGuardNames(child: AngularRoute, type: RouteGuard): string[] {
const guards = child?.[type] || [];
const names = guards.map((g: any) => getClassOrFunctionName(g));
return names || [];
}
function getProviderName(child: any): string[] {
const providers = child?.providers || [];
const names = providers.map((p: any) => p.name);
return names || [];
}
function assignChildrenToParent(
parentPath: string | null,
children: Routes,
currentUrl: string,
): Route[] {
return children.map((child: AngularRoute) => {
const childName = childRouteName(child);
const loadedRoutes = (window as any).ng?.ɵgetLoadedRoutes?.(child as any);
const childDescendents: [AngularRoute] = loadedRoutes || child.children;
const pathFragment = child.outlet ? `(${child.outlet}:${child.path})` : child.path;
const routePath = `${parentPath ?? ''}/${pathFragment}`.split('//').join('/');
// only found in aux routes, otherwise property will be undefined
const isAux = Boolean(child.outlet);
const isLazy = Boolean(child.loadChildren || child.loadComponent);
const pathWithoutParams = routePath.split('/:')[0];
const isActive = currentUrl?.startsWith(pathWithoutParams);
const routeConfig: Route = {
pathMatch: child.pathMatch,
component: childName,
canActivateGuards: getGuardNames(child, 'canActivate'),
canActivateChildGuards: getGuardNames(child, 'canActivateChild'),
canMatchGuards: getGuardNames(child, 'canMatch'),
canDeactivateGuards: getGuardNames(child, 'canDeactivate'),
providers: getProviderName(child),
path: routePath,
data: [],
isAux,
isLazy,
isActive,
};
if (child.title) {
routeConfig.title = getPropertyName(child, 'title');
}
if (child.redirectTo) {
routeConfig.redirectTo = getPropertyName(child, 'redirectTo');
}
if (childDescendents) {
routeConfig.children = assignChildrenToParent(routeConfig.path, childDescendents, currentUrl);
}
if (child.data) {
for (const el in child.data) {
if (child.data.hasOwnProperty(el)) {
routeConfig?.data?.push({
key: el,
value: child.data[el],
});
}
}
}
return routeConfig;
});
}
/**
* Get the display name for a function or class.
* @param fn - The function or class to get the name from
* @param defaultName - Optional name to check against. If the function name matches this value,
* '[Function]' is returned instead
* @returns The formatted name: class name, function name with '()', or '[Function]' for anonymous/arrow functions
*/
function getClassOrFunctionName(fn: Function, defaultName?: string) {
const isArrow = !fn.hasOwnProperty('prototype');
const isEmptyName = fn.name === '';
if ((isArrow && isEmptyName) || isEmptyName) {
return '[Function]';
}
const hasDefaultName = fn.name === defaultName;
if (hasDefaultName) {
return '[Function]';
}
// Check if it's a class by examining the function's string representation
const isClass = /^class\s/.test(fn.toString());
// Return class name without parentheses, function name with parentheses
return isClass ? fn.name : `${fn.name}()`;
}
function getPropertyName(child: AngularRoute, property: 'title' | 'redirectTo') {
if (child[property] instanceof Function) {
return getClassOrFunctionName(child[property], property);
}
return child[property];
}
function childRouteName(child: AngularRoute): string {
if (child.component) {
return child.component.name;
} else if (child.loadChildren || child.loadComponent) {
return `${child.path} [Lazy]`;
} else {
return 'no-name-route';
}
}
/**
* Get the element reference by type & name from the routes array. Called recursively to search through all children.
* @param type - type of element to search for (canActivate, canActivateChild, canDeactivate, canLoad, providers, redirectTo , title)
* @param routes - array of routes to search through
* @param name - name of the element to search for refers to the name of the guard or provider
* @returns - the element reference if found, otherwise null
*/
export function getElementRefByName(
type: RoutePropertyType,
routes: AngularRoute[],
name: string,
): any | null {
for (const element of routes) {
if (type === 'title' && element.title instanceof Function) {
const functionName = getClassOrFunctionName(element.title);
//TODO: improve this, not every titleFn has a name property
if (functionName === name) {
return element.title;
}
}
if (type === 'redirectTo' && element.redirectTo instanceof Function) {
const functionName = getClassOrFunctionName(element.redirectTo);
//TODO: improve this, not every redirectToFn has a name property
if (functionName === name) {
return element.redirectTo;
}
}
if (routeGuards.includes(type)) {
const routeGuard = type as RouteGuard;
if (element[routeGuard]) {
for (const guard of element[routeGuard]) {
// TODO: improve this, not every guard has a name property
if ((guard as any).name === name) {
return guard;
}
}
}
}
// _loadedRoutes is internal, we can't acess it with the dot notation
const loadedRoutes = (element as any)?.['_loadedRoutes'] as AngularRoute[] | undefined;
if (loadedRoutes) {
const result = getElementRefByName(type, loadedRoutes, name);
if (result !== null) {
return result;
}
}
if (element?.children) {
const result = getElementRefByName(type, element.children, name);
if (result !== null) {
return result;
}
}
}
}
/**
* Get the componet reference by name from the routes array. Called recursively to search through all children.
* @param routes - array of routes to search through
* @param name - name of the component to search for
* @returns - the element reference if found, otherwise null
*/
export function getComponentRefByName(routes: AngularRoute[], name: string): any | null {
for (const element of routes) {
if (element?.component?.name === name) {
return element.component;
}
// _loadedRoutes is internal, we can't acess it with the dot notation
const loadedRoutes = (element as any)?.['_loadedRoutes'] as AngularRoute[] | undefined;
if (loadedRoutes) {
const result = getComponentRefByName(loadedRoutes, name);
if (result !== null) {
return result;
}
}
if (element?.children) {
const result = getComponentRefByName(element.children, name);
if (result !== null) {
return result;
}
}
}
return null;
}