refactor(devtools): add debug router APIs (#64411)

This is a patch backport of #63081

PR Close #64411
This commit is contained in:
Matthieu Riegler 2025-10-14 19:58:10 +02:00 committed by Andrew Kushnir
parent 5f4f624477
commit 43cb583f70
4 changed files with 74 additions and 10 deletions

View file

@ -59,8 +59,10 @@ export const GLOBAL_PUBLISH_EXPANDO_KEY = 'ng';
// Typing for externally published global util functions
// Ideally we should be able to use `NgGlobalPublishUtils` using declaration merging but that doesn't work with API extractor yet.
// Have included the typings to have type safety when working with editors that support it (VSCode).
interface NgGlobalPublishUtils {
export interface ExternalGlobalUtils {
ɵgetLoadedRoutes(route: any): any;
ɵnavigateByUrl(router: any, url: string): any;
ɵgetRouterInstance(injector: any): any;
}
const globalUtilsFunctions = {
@ -93,7 +95,6 @@ const globalUtilsFunctions = {
'enableProfiling': enableProfiling,
};
type CoreGlobalUtilsFunctions = keyof typeof globalUtilsFunctions;
type ExternalGlobalUtilsFunctions = keyof NgGlobalPublishUtils;
let _published = false;
/**
@ -148,15 +149,15 @@ export type FrameworkAgnosticGlobalUtils = Omit<
'getDirectiveMetadata'
> & {
getDirectiveMetadata(directiveOrComponentInstance: any): DirectiveDebugMetadata | null;
};
} & ExternalGlobalUtils;
/**
* Publishes the given function to `window.ng` from package other than @angular/core
* So that it can be used from the browser console when an application is not in production.
*/
export function publishExternalGlobalUtil<K extends ExternalGlobalUtilsFunctions>(
export function publishExternalGlobalUtil<K extends keyof ExternalGlobalUtils>(
name: K,
fn: NgGlobalPublishUtils[K],
fn: ExternalGlobalUtils[K],
): void {
publishUtil(name, fn);
}

View file

@ -29,12 +29,13 @@ import {
Type,
ɵperformanceMarkFeature as performanceMarkFeature,
ɵIS_ENABLED_BLOCKING_INITIAL_NAVIGATION as IS_ENABLED_BLOCKING_INITIAL_NAVIGATION,
ɵpublishExternalGlobalUtil,
} from '@angular/core';
import {of, Subject} from 'rxjs';
import {INPUT_BINDER, RoutedComponentInputBinder} from './directives/router_outlet';
import {Event, NavigationError, stringifyEvent} from './events';
import {RedirectCommand, Routes} from './models';
import {RedirectCommand, Route, Routes} from './models';
import {NAVIGATION_ERROR_HANDLER, NavigationTransitions} from './navigation_transition';
import {Router} from './router';
import {InMemoryScrollingOptions, ROUTER_CONFIGURATION, RouterConfigOptions} from './router_config';
@ -50,6 +51,7 @@ import {
VIEW_TRANSITION_OPTIONS,
ViewTransitionsFeatureOptions,
} from './utils/view_transition';
import {getLoadedRoutes, getRouterInstance, navigateByUrl} from './router_devtools';
/**
* Sets up providers necessary to enable `Router` functionality for the application.
@ -88,6 +90,13 @@ import {
* @returns A set of providers to setup a Router.
*/
export function provideRouter(routes: Routes, ...features: RouterFeatures[]): EnvironmentProviders {
if (typeof ngDevMode === 'undefined' || ngDevMode) {
// Publish this util when the router is provided so that the devtools can use it.
ɵpublishExternalGlobalUtil('ɵgetLoadedRoutes', getLoadedRoutes);
ɵpublishExternalGlobalUtil('ɵgetRouterInstance', getRouterInstance);
ɵpublishExternalGlobalUtil('ɵnavigateByUrl', navigateByUrl);
}
return makeEnvironmentProviders([
{provide: ROUTES, multi: true, useValue: routes},
typeof ngDevMode === 'undefined' || ngDevMode

View file

@ -6,11 +6,31 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {ɵpublishExternalGlobalUtil} from '@angular/core';
import {Injector} from '@angular/core';
import {Router} from './router';
import {Route} from './models';
/**
* Returns the loaded routes for a given route.
*/
export function getLoadedRoutes(route: Route): Route[] | undefined {
return route._loadedRoutes;
}
ɵpublishExternalGlobalUtil('ɵgetLoadedRoutes', getLoadedRoutes);
/**
* Returns the Router instance from the given injector, or null if not available.
*/
export function getRouterInstance(injector: Injector): Router | null {
return injector.get(Router, null, {optional: true});
}
/**
* Navigates the given router to the specified URL.
* Throws if the provided router is not an Angular Router.
*/
export function navigateByUrl(router: Router, url: string): Promise<boolean> {
if (!(router instanceof Router)) {
throw new Error('The provided router is not an Angular Router.');
}
return router.navigateByUrl(url);
}

View file

@ -6,10 +6,10 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {Component} from '@angular/core';
import {Component, Injector} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {Router, RouterModule} from '../index';
import {getLoadedRoutes} from '../src/router_devtools';
import {getLoadedRoutes, getRouterInstance, navigateByUrl} from '../src/router_devtools';
@Component({template: '<div>simple standalone</div>'})
export class SimpleStandaloneComponent {}
@ -71,4 +71,38 @@ describe('router_devtools', () => {
const loadedPath = loadedRoutes && loadedRoutes[0].path;
expect(loadedPath).toEqual(undefined);
});
describe('getRouterInstance', () => {
it('should return the Router instance from the injector', () => {
TestBed.configureTestingModule({
imports: [RouterModule.forRoot([])],
});
const injector = TestBed.inject(Injector);
const router = TestBed.inject(Router);
expect(getRouterInstance(injector)).toBe(router);
});
it('should return null if Router is not provided', () => {
const injector = Injector.create({providers: []});
expect(getRouterInstance(injector)).toBeNull();
});
});
describe('navigateByUrl', () => {
it('should navigate to the given url', async () => {
TestBed.configureTestingModule({
imports: [RouterModule.forRoot([{path: 'foo', component: SimpleStandaloneComponent}])],
});
const router = TestBed.inject(Router);
const result = await navigateByUrl(router, '/foo');
expect(result).toBeTrue();
expect(router.url).toBe('/foo');
});
it('should throw if not given a Router instance', async () => {
expect(() => navigateByUrl({} as unknown as Router, '/foo')).toThrowError(
'The provided router is not an Angular Router.',
);
});
});
});