Skip to content

Commit

Permalink
feat(router): add support for visualizing routes for debugging (analo…
Browse files Browse the repository at this point in the history
  • Loading branch information
brandonroberts authored Dec 3, 2024
1 parent 2b18dc2 commit e204a71
Show file tree
Hide file tree
Showing 11 changed files with 261 additions and 16 deletions.
2 changes: 2 additions & 0 deletions apps/analog-app/src/app/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import {
provideFileRouter,
withExtraRoutes,
withDebugRoutes,
requestContextInterceptor,
} from '@analogjs/router';
import { withNavigationErrorHandler } from '@angular/router';
Expand All @@ -23,6 +24,7 @@ export const appConfig: ApplicationConfig = {
providers: [
provideFileRouter(
withNavigationErrorHandler(console.error),
withDebugRoutes(),
withExtraRoutes(fallbackRoutes)
),
provideHttpClient(
Expand Down
8 changes: 8 additions & 0 deletions apps/analog-app/src/app/pages/(auth).page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';

@Component({
imports: [RouterOutlet],
template: ` <router-outlet /> `,
})
export default class AuthLayoutPageComponent {}
6 changes: 6 additions & 0 deletions apps/analog-app/src/app/pages/(auth)/sign-up.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Component } from '@angular/core';

@Component({
template: ` <h2>SignUp</h2> `,
})
export default class SignupPageComponent {}
19 changes: 19 additions & 0 deletions apps/docs-app/docs/features/routing/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -359,3 +359,22 @@ export const appConfig: ApplicationConfig = {
providers: [provideFileRouter(withExtraRoutes(customRoutes))],
};
```

## Visualizing and Debugging Routes

When you are building the pages for your application, it can help to visually see the routes based on the filesystem structure. You can use the `withDebugRoutes()` function to provide a debug route that displays the pages and layouts for your application.

Use the `withDebugRoutes` function in the `app.config.ts`:

```ts
import { ApplicationConfig } from '@angular/core';
import { provideFileRouter, withDebugRoutes } from '@analogjs/router';

export const appConfig: ApplicationConfig = {
providers: [provideFileRouter(withDebugRoutes())],
};
```

Navigate the `__analog/routes` URL in the browser to see the routes table.

![debug routes page](/img/debug-routes.png)
Binary file added apps/docs-app/static/img/debug-routes.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions packages/router/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ export { getLoadResolver } from './lib/get-load-resolver';
export { requestContextInterceptor } from './lib/request-context';
export { injectRouteEndpointURL } from './lib/inject-route-endpoint-url';
export { FormAction } from './lib/form-action.directive';
export { injectDebugRoutes } from './lib/debug/routes';
export { withDebugRoutes } from './lib/debug';
135 changes: 135 additions & 0 deletions packages/router/src/lib/debug/debug.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { Component } from '@angular/core';

import { injectDebugRoutes, DebugRoute } from './routes';

type CollectedRoute = {
path: string;
filename: string;
file: string;
isLayout: boolean;
};

@Component({
selector: 'analogjs-debug-routes-page',
standalone: true,
template: `
<h2>Routes</h2>
<div class="table-container">
<div class="table-header">
<div class="header-cell">Route Path</div>
<div class="header-cell">File</div>
<div class="header-cell">Type</div>
</div>
<div class="table-body">
@for(collectedRoute of collectedRoutes; track collectedRoute.filename) {
<div class="table-row">
<div class="table-cell">{{ collectedRoute.path }}</div>
<div class="table-cell" [title]="collectedRoute.filename">
{{ collectedRoute.file }}
</div>
<div class="table-cell">
{{ collectedRoute.isLayout ? 'Layout' : 'Page' }}
</div>
</div>
}
</div>
</div>
`,
styles: `
:host {
width: 100%;
}
.table-container {
width: 100%;
max-width: 900px;
margin: 0 auto;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.table-header {
display: grid;
grid-template-columns: repeat(3, 1fr);
background: gray;
border-bottom: 2px solid #e5e7eb;
}
.header-cell {
padding: 16px 24px;
font-weight: 600;
text-transform: uppercase;
font-size: 14px;
letter-spacing: 0.05em;
color: white;
}
.table-body {
display: flex;
flex-direction: column;
}
.table-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
border-bottom: 1px solid #e5e7eb;
transition: background-color 0.2s ease;
}
.table-row:last-child {
border-bottom: none;
}
.table-row:hover {
background-color: #f9fafb;
}
.table-cell {
padding: 16px 24px;
font-size: 16px;
color: #4b5563;
}
@media (max-width: 640px) {
.table-container {
border-radius: 0;
margin: 0;
}
.header-cell,
.table-cell {
padding: 12px 16px;
}
}
`,
})
export default class DebugRoutesComponent {
collectedRoutes: CollectedRoute[] = [];
debugRoutes = injectDebugRoutes();

ngOnInit() {
this.traverseRoutes(this.debugRoutes);
}

traverseRoutes(routes: DebugRoute[], parent?: string) {
routes.forEach((route) => {
this.collectedRoutes.push({
path: route.isLayout
? `${parent ? `/${parent}` : ''}${route.path ? `/${route.path}` : ''}`
: `${parent ? `/${parent}` : ''}${
route.path ? `/${route.path}` : '/'
}`,
filename: route.filename,
file: route.filename?.replace(/(^.*)pages\//, '') || '',
isLayout: route.isLayout,
});

if (route.children) {
this.traverseRoutes(route.children, route.path || '');
}
});
}
}
20 changes: 20 additions & 0 deletions packages/router/src/lib/debug/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ROUTES } from '@angular/router';

/**
* Provides routes that provide additional
* pages for displaying and debugging
* routes.
*/
export function withDebugRoutes() {
const routes = [
{
path: '__analog/routes',
loadComponent: () => import('./debug.page'),
},
];

return {
ɵkind: 101 as number,
ɵproviders: [{ provide: ROUTES, useValue: routes, multi: true }],
};
}
37 changes: 37 additions & 0 deletions packages/router/src/lib/debug/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { inject, InjectionToken } from '@angular/core';
import { Route } from '@angular/router';

import {
ANALOG_CONTENT_ROUTE_FILES,
ANALOG_ROUTE_FILES,
createRoutes,
} from '../routes';

export const DEBUG_ROUTES = new InjectionToken(
'@analogjs/router debug routes',
{
providedIn: 'root',
factory() {
const debugRoutes = createRoutes(
{
...ANALOG_ROUTE_FILES,
...ANALOG_CONTENT_ROUTE_FILES,
},
true
);

return debugRoutes as (Route & DebugRoute)[];
},
}
);

export type DebugRoute = {
path: string;
filename: string;
isLayout: boolean;
children?: DebugRoute[];
};

export function injectDebugRoutes() {
return inject(DEBUG_ROUTES);
}
43 changes: 29 additions & 14 deletions packages/router/src/lib/routes.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
/// <reference types="vite/client" />

import type { Route } from '@angular/router';

import type { RouteExport, RouteMeta } from './models';
import { toRouteConfig } from './route-config';
import { toMarkdownModule } from './markdown-helpers';
import { APP_DIR, ENDPOINT_EXTENSION } from './constants';
import { ENDPOINT_EXTENSION } from './constants';
import { ANALOG_META_KEY } from './endpoints';

/**
* This variable reference is replaced with a glob of all page routes.
*/
let ANALOG_ROUTE_FILES = {};
export let ANALOG_ROUTE_FILES = {};

/**
* This variable reference is replaced with a glob of all content routes.
*/
let ANALOG_CONTENT_ROUTE_FILES = {};
export let ANALOG_CONTENT_ROUTE_FILES = {};

export type Files = Record<string, () => Promise<RouteExport | string>>;

Expand All @@ -39,7 +37,7 @@ type RawRouteByLevelMap = Record<number, RawRouteMap>;
* @param files
* @returns Array of routes
*/
export function createRoutes(files: Files): Route[] {
export function createRoutes(files: Files, debug = false): Route[] {
const filenames = Object.keys(files);

if (filenames.length === 0) {
Expand Down Expand Up @@ -115,7 +113,7 @@ export function createRoutes(files: Files): Route[] {
);
sortRawRoutes(rawRoutes);

return toRoutes(rawRoutes, files);
return toRoutes(rawRoutes, files, debug);
}

function toRawPath(filename: string): string {
Expand All @@ -136,23 +134,26 @@ function toSegment(rawSegment: string): string {
.replace(/^\/+|\/+$/g, ''); // remove trailing slashes
}

function toRoutes(rawRoutes: RawRoute[], files: Files): Route[] {
function toRoutes(rawRoutes: RawRoute[], files: Files, debug = false): Route[] {
const routes: Route[] = [];

for (const rawRoute of rawRoutes) {
const children: Route[] | undefined =
rawRoute.children.length > 0
? toRoutes(rawRoute.children, files)
? toRoutes(rawRoute.children, files, debug)
: undefined;
let module: (() => Promise<RouteExport>) | undefined = undefined;
let analogMeta: { endpoint: string; endpointKey: string } | undefined =
undefined;

if (rawRoute.filename) {
const isMarkdownFile = rawRoute.filename.endsWith('.md');
module = isMarkdownFile
? toMarkdownModule(files[rawRoute.filename] as () => Promise<string>)
: (files[rawRoute.filename] as () => Promise<RouteExport>);

if (!debug) {
module = isMarkdownFile
? toMarkdownModule(files[rawRoute.filename] as () => Promise<string>)
: (files[rawRoute.filename] as () => Promise<RouteExport>);
}

const endpointKey = rawRoute.filename.replace(
/\.page\.(ts|analog|ag)$/,
Expand All @@ -176,7 +177,12 @@ function toRoutes(rawRoutes: RawRoute[], files: Files): Route[] {
};
}

const route: Route & { meta?: typeof analogMeta } = module
type DebugRoute = Route & {
filename?: string | null | undefined;
isLayout?: boolean;
};

const route: Route & { meta?: typeof analogMeta } & DebugRoute = module
? {
path: rawRoute.segment,
loadChildren: () =>
Expand All @@ -203,7 +209,16 @@ function toRoutes(rawRoutes: RawRoute[], files: Files): Route[] {
];
}),
}
: { path: rawRoute.segment, children };
: {
path: rawRoute.segment,
...(debug
? {
filename: rawRoute.filename ? rawRoute.filename : undefined,
isLayout: children && children.length > 0 ? true : false,
}
: {}),
children,
};

routes.push(route);
}
Expand Down
5 changes: 3 additions & 2 deletions packages/vite-plugin-angular/src/lib/router-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ export function routerPlugin() {
name: 'analogjs-router-optimization',
enforce: 'pre',
async transform(_code: string, id: string) {
if (id.endsWith('analogjs-router.mjs')) {
const contents = await javascriptTransformer.transformFile(id, false);
if (id.includes('analogjs-') && id.includes('.mjs')) {
const path = id.split('?')[0];
const contents = await javascriptTransformer.transformFile(path);

return {
code: Buffer.from(contents).toString('utf-8'),
Expand Down

0 comments on commit e204a71

Please sign in to comment.