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
37 changes: 37 additions & 0 deletions app/src/__tests__/router-app-view.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,41 @@ describe('routerAppView', () => {
expect(routerAppView(page, 'cart').loops).toEqual([loop]);
expect(routerAppView(page, 'app-root').loops).toBeUndefined();
});

it("shows a secondary app's own outlets, links and preloads, and the page's guard recording", () => {
const outlet = { outlet: 'primary', activated: true, component: 'Cart' };
const link = { text: 'Basket', element: 'a', devtoolsId: 'd1', href: '/basket' };
const preload = { path: 'checkout', startedAt: 1, ms: 4 };
const page = {
pageId: 'p',
reportedAt: 1,
changedAt: 1,
snapshot: null,
navigations: [],
outlets: [{ outlet: 'primary', activated: true, component: 'Shell' }],
links: [{ text: 'Home', element: 'a', devtoolsId: 'd0', href: '/' }],
preloads: [],
instrumented: true,
apps: [
{ id: 'app-root', name: 'Shell', selector: 'app-root', primary: true },
{
id: 'cart',
name: 'Cart',
selector: 'cart-widget',
navigations: [],
outlets: [outlet],
links: [link],
linksTruncated: 2,
preloads: [preload],
},
],
} as unknown as RouterPage;
const cart = routerAppView(page, 'cart');
expect(cart.outlets).toEqual([outlet]);
expect(cart.links).toEqual([link]);
expect(cart.linksTruncated).toBe(2);
expect(cart.preloads).toEqual([preload]);
expect(cart.instrumented).toBe(true);
expect(routerAppView(page, 'app-root').links?.[0].text).toBe('Home');
});
});
11 changes: 9 additions & 2 deletions app/src/pages/router-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@ export function routerAppsOf(page: RouterPage | null | undefined): RouterApp[] {

/**
* The page as one of its apps sees it, as the server builds it for the agent
* tools: that app's route, navigations, config and setup in place of the first
* app's. Outlets, links, preloads and guard recording belong to the first app.
* tools: that app's route, navigations, config, setup, outlets, links and
* preloads in place of the first app's. Guard recording is one switch for the
* whole page.
*/
export function routerAppView(page: RouterPage, appId: string | null): RouterPage {
const app = routerAppsOf(page).find((a) => a.id === appId);
Expand All @@ -83,6 +84,12 @@ export function routerAppView(page: RouterPage, appId: string | null): RouterPag
configTruncated: app.configTruncated,
activeIds: app.activeIds,
setup: app.setup,
outlets: app.outlets,
links: app.links,
linksTruncated: app.linksTruncated,
linksUnchecked: app.linksUnchecked,
preloads: app.preloads,
instrumented: page.instrumented,
loops: app.loops,
apps: page.apps,
appId: app.id,
Expand Down
15 changes: 8 additions & 7 deletions apps/docs/src/content/agents/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -665,7 +665,7 @@ Stores that report through the Redux DevTools extension API, such as NGXS, Elf a

All router tools read the page, except `scan-render-mode`, which also reads your `*.routes.server.ts` files.

On a page that runs more than one Angular app with a Router, `inspect-route`, `explain-navigation`, `diff-navigations`, `preview-navigation`, `list-routes`, `lint-routes` and `router-config` take an optional `app`: the app's id (the tag of its root, such as `app-root`) or its root component name. Without `app` they answer for the first app, and the answer ends with a line that names the other apps. An unknown `app` is refused with the list of apps. See [Several apps on one page](../inspectors/router.md#several-apps-on-one-page).
On a page that runs more than one Angular app with a Router, `inspect-route`, `explain-navigation`, `diff-navigations`, `follow-navigations`, `preview-navigation`, `list-routes`, `lint-routes` and `router-config` take an optional `app`: the app's id (the tag of its root, such as `app-root`) or its root component name. Without `app` they answer for the first app, and the answer ends with a line that names the other apps. An unknown `app` is refused with the list of apps. See [Several apps on one page](../inspectors/router.md#several-apps-on-one-page).

### Read the current route

Expand All @@ -683,13 +683,14 @@ Use `diff-navigations` for "why did this work last time". Without `b`, it takes

`follow-navigations` returns the navigations that are new or changed since a cursor, oldest first, across every page. Use it to follow a login and redirect flow step by step without calling `explain-navigation` in a loop. Reads: page.

| Argument | Value |
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `since` | The cursor a previous call returned. Without it, the tool lists the last 5 navigations and the current cursor. |
| `waitMs` | With `since`: how long to wait, at most 30000 ms, when no navigation came after the cursor yet. The tool answers as soon as one comes. Default 0. |
| `page` | Only the navigations of this page. |
| Argument | Value |
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `since` | The cursor a previous call returned. Without it, the tool lists the last 5 navigations and the current cursor. |
| `waitMs` | With `since`: how long to wait, at most 30000 ms, when no navigation came after the cursor yet. The tool answers as soon as one comes. Default 0. |
| `page` | Only the navigations of this page. |
| `app` | On a page that runs several Angular apps with a Router, the app to follow: its id or root component name. Without it, the tool follows the first app of each page. |

Each line has the sequence number, the page, the navigation id, the URL, the outcome, the time, a redirect target and the reason it failed. The answer ends with **Next cursor**: pass it as `since` on the next call. A navigation that was still pending comes again with `(updated)` once it ends, is redirected or changes target. Probes are left out. The server keeps the last 500 changes, and says how many a cursor missed. A cursor from before the server started lists everything it kept.
Each line has the sequence number, the page, the app (for an app other than the first), the navigation id, the URL, the outcome, the time, a redirect target and the reason it failed. The answer ends with **Next cursor**: pass it as `since` on the next call. A navigation that was still pending comes again with `(updated)` once it ends, is redirected or changes target. Probes are left out. The server keeps the last 500 changes, and says how many a cursor missed. A cursor from before the server started lists everything it kept.

### Read the route config

Expand Down
22 changes: 12 additions & 10 deletions apps/docs/src/content/inspectors/router.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,18 +179,20 @@ The overlay reads the helper `provideRouter()` publishes (`ng.ɵgetRouterInstanc

A page can bootstrap more than one Angular app, with several `bootstrapApplication()` calls or micro-frontends. Each app root (an element with `ng-version`) that has its own Router counts as an app. Roots that share one Router count once.

When a page has more than one, the **Live router** heading shows an **App** picker. Each app is listed by its root component and tag, such as `ShellApp <app-root>`. The first app is the router that has routes or has navigated, else the first root. Pick another app to see its route, navigations, config, setup and lint. The buttons that act on the router (**Go**, **Read lazy**, **Abort**, **Probe in app** and **Replay**) run on the app you picked. A page with one app shows no picker.
When a page has more than one, the **Live router** heading shows an **App** picker. Each app is listed by its root component and tag, such as `ShellApp <app-root>`. The first app is the router that has routes or has navigated, else the first root. Pick another app to see its route, navigations, config, setup, outlets, links, preloads and lint. The buttons that act on the router (**Go**, **Read lazy**, **Abort**, **Probe in app** and **Replay**) run on the app you picked. A page with one app shows no picker.

Each app has an id: the tag of its root, with `-2`, `-3` and so on for a second root with the same tag. The id stays the same while the root is on the page, and a root that replaces a removed one with the same tag gets the same id back. The router agent tools take that id, or the root component name, as `app`.

Some parts stay with the first app:
The overlay watches each app's Router on its own, so nothing one app does shows under another:

| What | Why |
| ------------------ | --------------------------------------------------------------------------------------------------- |
| Outlets and links | The overlay reads them from the whole page. They show on the first app only. |
| Preloads | The overlay watches the first app's preloader. |
| Guard recording | It wraps the first app's guards. On another app, **Record each guard and resolver** shows an error. |
| Follow navigations | `follow-navigations` streams the first app's navigations. |
| What | Which app it belongs to |
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Navigations and callers | The app whose Router navigated. A `RouterLink` click counts for the app whose Router the link uses. |
| Outlets | The app whose Router fills them. |
| Links | The app whose Router the `RouterLink` or `RouterLinkActive` directive uses. A link that names no Router goes to the first app. Each app lists at most 100 links, and the 2000-element check covers the whole page. |
| Preloads | The app whose preloader ran them. |
| Guard and resolver runs | The app that ran them. A guard two apps share (one class, or one routes array) is wrapped once, and each run goes to the app with a navigation in progress. **Record each guard and resolver** covers every app. |
| History changes made outside the router | The first app. They come from the browser's Navigation API, which the whole page shares. |

### Development builds

Expand Down Expand Up @@ -260,7 +262,7 @@ Without that recording, the guards listed for a navigation are candidates: the `
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pangular:explain-navigation` | Why a navigation failed or redirected, with any loop and the cause of each hop. Pass `url` or `id` to narrow it, `limit` for more than the last 5 (up to `limits.navigations`), `perf` for the slowest ones, or `format: "repro"` for a markdown repro of one navigation. |
| `pangular:diff-navigations` | What changed between two navigations: params, data, guards, resolvers, outlets, components and timing. Pass `a` and `b`, or leave them out to compare the newest navigation with the last one to the same path. |
| `pangular:follow-navigations` | New and changed navigations since a cursor, across pages. Pass the cursor back as `since`, with `waitMs` to wait for the next navigation. |
| `pangular:follow-navigations` | New and changed navigations since a cursor, across pages. Pass the cursor back as `since`, with `waitMs` to wait for the next navigation, and `app` to follow another app. |
| `pangular:preview-navigation` | What going to a URL would do, from the live config and without navigating: redirects, the route chain, guards, lazy chunks, resolvers and components in order, source files and render mode. |
| `pangular:inspect-route` | The route the page is on. Pass `component` (a component class, tag or link text) for the route a component was rendered for, or a link state. |
| `pangular:list-routes` | The live config with source files and example URLs. `match` predicts a URL, `audit` lists the guards of each page, `resolveLazy` loads the routes of a lazy route first. |
Expand All @@ -273,7 +275,7 @@ Without that recording, the guards listed for a navigation are candidates: the `
| `pangular:record-guards` | Turns per-guard and per-resolver recording on or off. |
| `pangular:router` (resource) | The active route tree and recent navigations of each page. |

On a page that runs several apps, `inspect-route`, `explain-navigation`, `diff-navigations`, `preview-navigation`, `list-routes`, `lint-routes` and `router-config` take an optional `app`: the app's id or root component name. Without it they answer for the first app and name the others.
On a page that runs several apps, `inspect-route`, `explain-navigation`, `diff-navigations`, `follow-navigations`, `preview-navigation`, `list-routes`, `lint-routes` and `router-config` take an optional `app`: the app's id or root component name. Without it they answer for the first app and name the others.

`navigate` and `probe-route` only accept same-origin relative URLs that start with `/`. With [`actions.router`](../getting-started/configuration.md#actions) set to `false`, the server drops `navigate` and `probe-route`, and keeps `record-guards` and `list-routes` with `resolveLazy`. See [Tools](../agents/tools.md).

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion extension/ui/index.html

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading