Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions devtools/projects/ng-devtools-backend/src/lib/router-tree.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ describe('parseRoutes', () => {
path: 'redirect',
redirectTo: 'redirectTo',
},
{
path: 'redirect-fn',
redirectTo: () => '/target',
},
],
};
const parsedRoutes = parseRoutes(nestedRouter as any);
Expand All @@ -107,7 +111,6 @@ describe('parseRoutes', () => {
'canDeactivateGuards': [],
'providers': [],
'path': '/(outlet:component-one)',
'title': '[Function]',
'pathMatch': undefined,
'data': [],
'isAux': true,
Expand All @@ -123,7 +126,6 @@ describe('parseRoutes', () => {
'canDeactivateGuards': [],
'providers': [],
'path': '/component-two',
'title': '[Function]',
'pathMatch': undefined,
'data': [{'key': 'name', 'value': 'component-two'}],
'isAux': false,
Expand All @@ -139,7 +141,6 @@ describe('parseRoutes', () => {
'canDeactivateGuards': [],
'providers': [],
'path': '/component-two/component-two-two',
'title': '[Function]',
'pathMatch': undefined,
'data': [],
'isAux': false,
Expand All @@ -157,7 +158,6 @@ describe('parseRoutes', () => {
'canDeactivateGuards': [],
'providers': [],
'path': '/lazy',
'title': '[Function]',
'pathMatch': undefined,
'data': [],
'isAux': false,
Expand All @@ -166,20 +166,36 @@ describe('parseRoutes', () => {
'isRedirect': false,
},
{
'component': 'redirect -> redirecting to -> "redirectTo"',
'component': 'no-name-route',
'canActivateGuards': [],
'canActivateChildGuards': [],
'canMatchGuards': [],
'canDeactivateGuards': [],
'providers': [],
'path': '/redirect',
'title': '[Function]',
'pathMatch': undefined,
'data': [],
'isAux': false,
'isLazy': false,
'isActive': undefined,
'isRedirect': true,
'redirectTo': 'redirectTo',
},
{
'component': 'no-name-route',
'canActivateGuards': [],
'canActivateChildGuards': [],
'canMatchGuards': [],
'canDeactivateGuards': [],
'providers': [],
'path': '/redirect-fn',
'pathMatch': undefined,
'data': [],
'isAux': false,
'isLazy': false,
'isActive': undefined,
'isRedirect': true,
'redirectTo': '[Function]',
},
],
'isAux': false,
Expand Down
81 changes: 70 additions & 11 deletions devtools/projects/ng-devtools-backend/src/lib/router-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
import {Route} from '../../../protocol';
import type {Route as AngularRoute} from '@angular/router';

export type RoutePropertyType = RouteGuard | 'providers' | 'component';
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;

Expand Down Expand Up @@ -70,7 +72,6 @@ function assignChildrenToParent(
const isActive = currentUrl?.startsWith(pathWithoutParams);

const routeConfig: Route = {
title: typeof child.title === 'string' ? child.title : '[Function]',
pathMatch: child.pathMatch,
component: childName,
canActivateGuards: getGuardNames(child, 'canActivate'),
Expand All @@ -86,6 +87,14 @@ function assignChildrenToParent(
isRedirect,
};

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);
}
Expand All @@ -105,21 +114,53 @@ function assignChildrenToParent(
});
}

/**
* 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 isArrowWithNoName = !fn.hasOwnProperty('prototype') && fn.name === '';

if (isArrowWithNoName) {
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 if (child.redirectTo) {
return `${child.path} -> redirecting to -> "${child.redirectTo}"`;
} 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)
* @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
Expand All @@ -130,12 +171,30 @@ export function getElementRefByName(
name: string,
): any | null {
for (const element of routes) {
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;
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;
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {RouterTreeNode} from './router-tree-fns';
export type RowType = 'text' | 'chip' | 'flag' | 'list';

@Component({
standalone: true,
selector: '[ng-route-details-row]',
templateUrl: './route-details-row.component.html',
styleUrls: ['./route-details-row.component.scss'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,17 @@ <h2 class="router-title">Routes Details</h2>
(btnClick)="navigateRoute(route)"
></tr>

<tr
ng-route-details-row
label="Component"
type="chip"
dataKey="component"
[data]="data"
(btnClick)="viewComponentSource(data.component)"
></tr>
@if (!data.isRedirect) {
<tr
ng-route-details-row
label="Component"
type="chip"
dataKey="component"
[data]="data"
(btnClick)="viewComponentSource(data.component)"
></tr>
}

@if (data.pathMatch) {
<tr ng-route-details-row label="Path Match" dataKey="pathMatch" [data]="data"></tr>
}
Expand Down Expand Up @@ -129,7 +132,14 @@ <h2 class="router-title">Routes Details</h2>
></tr>
}
@if (data.title) {
<tr ng-route-details-row label="Title" dataKey="title" [data]="data"></tr>
<tr
ng-route-details-row
type="chip"
label="Title"
dataKey="title"
[data]="data"
(btnClick)="viewFunctionSource(data.title, 'title')"
></tr>
}
<tr
ng-route-details-row
Expand All @@ -153,6 +163,17 @@ <h2 class="router-title">Routes Details</h2>
dataKey="isRedirect"
[data]="data"
></tr>

@if (data.redirectTo) {
<tr
ng-route-details-row
label="Redirect to"
type="chip"
dataKey="redirectTo"
[data]="data"
(btnClick)="viewFunctionSource(data.redirectTo, 'redirectTo')"
></tr>
}
</table>
</div>
</as-split-area>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,7 @@ import {ApplicationOperations} from '../../application-operations/index';
import {RouteDetailsRowComponent} from './route-details-row.component';
import {FrameManager} from '../../application-services/frame_manager';
import {Events, MessageBus, Route} from '../../../../../protocol';
import {
SvgD3Node,
SvgD3Link,
TreeVisualizerConfig,
} from '../../shared/tree-visualizer/tree-visualizer';
import {SvgD3Node, TreeVisualizerConfig} from '../../shared/tree-visualizer/tree-visualizer';
import {
RouterTreeD3Node,
transformRoutesIntoVisTree,
Expand Down Expand Up @@ -148,6 +144,17 @@ export class RouterTreeComponent {
);
}

viewFunctionSource(functionName: string, type: 'title' | 'redirectTo'): void {
if (functionName === '[Function]') {
const message =
'Cannot view the source of redirect functions defined inline (arrow or anonymous).';
this.snackBar.open(message, 'Dismiss', {duration: 5000, horizontalPosition: 'left'});
return;
}

this.appOperations.viewSourceFromRouter(functionName, type, this.frameManager.selectedFrame()!);
}

navigateRoute(route: any): void {
this.messageBus.emit('navigateRoute', [route.data.path]);
}
Expand Down
1 change: 1 addition & 0 deletions devtools/projects/protocol/src/lib/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ export interface Route {
data?: any;
path: string;
component: string;
redirectTo?: string;
isActive: boolean;
isAux: boolean;
isLazy: boolean;
Expand Down