@loozo/angular-router 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +565 -80
- package/builders.json +2 -2
- package/fesm2022/loozo-angular-router-_core.mjs +332 -595
- package/fesm2022/loozo-angular-router-_core.mjs.map +1 -1
- package/fesm2022/loozo-angular-router-_generated.mjs +335 -627
- package/fesm2022/loozo-angular-router-_generated.mjs.map +1 -1
- package/fesm2022/loozo-angular-router.mjs +2 -19
- package/node/_core/src/path.d.ts +17 -0
- package/node/src/builders/application.js +19 -7
- package/node/src/builders/options.js +47 -61
- package/node/src/builders/serve.js +66 -16
- package/node/src/generation/anchor.d.ts +13 -0
- package/node/src/generation/anchor.js +92 -0
- package/node/src/generation/compiler.d.ts +6 -0
- package/node/src/generation/compiler.js +68 -0
- package/node/src/generation/emit.d.ts +3 -0
- package/node/src/generation/emit.js +26 -8
- package/node/src/generation/files.d.ts +26 -0
- package/node/src/generation/files.js +14 -3
- package/node/src/generation/filesystem.d.ts +7 -0
- package/node/src/generation/filesystem.js +261 -69
- package/node/src/generation/generate.d.ts +16 -0
- package/node/src/generation/generate.js +10 -4
- package/node/src/generation/index.d.ts +16 -0
- package/node/src/generation/index.js +15 -0
- package/node/src/generation/overlay.d.ts +3 -0
- package/node/src/generation/selectors.d.ts +2 -0
- package/node/src/generation/sources.d.ts +50 -0
- package/node/src/generation/sources.js +57 -12
- package/node/src/generation/validate.d.ts +12 -0
- package/node/src/generation/validate.js +53 -57
- package/package.json +7 -2
- package/schemas/application.json +764 -20
- package/schemas/serve.json +144 -30
- package/snippets/angular.json +29 -0
- package/snippets/generated/destinations.generated.ts +126 -0
- package/snippets/generated/routes.generated.ts +35 -0
- package/snippets/src/catalog.ts +20 -0
- package/snippets/src/docs-url.ts +8 -0
- package/snippets/src/index.html +11 -0
- package/snippets/src/main.ts +6 -0
- package/snippets/src/product-url.ts +8 -0
- package/snippets/src/routes/products/$productId/page.ts +20 -0
- package/snippets/src/routes/products/$productId/route.generated.ts +24 -0
- package/snippets/src/routes/products/$productId/route.ts +7 -0
- package/snippets/src/routes/reports/$reportId/page.ts +9 -0
- package/snippets/src/routes/reports/$reportId/route.generated.ts +24 -0
- package/snippets/src/routes/reports/$reportId/route.ts +6 -0
- package/snippets/src/routes/teams/$teamId/items/$id/inputs.ts +15 -0
- package/snippets/src/routes/teams/$teamId/items/$id/links.ts +13 -0
- package/snippets/src/routes/teams/$teamId/items/$id/page.ts +9 -0
- package/snippets/src/routes/teams/$teamId/items/$id/route.generated.ts +24 -0
- package/snippets/src/routes/teams/$teamId/items/$id/route.ts +18 -0
- package/snippets/src/routes/teams/$teamId/members/$memberId/page.ts +9 -0
- package/snippets/src/routes/teams/$teamId/members/$memberId/route.generated.ts +24 -0
- package/snippets/src/routes/teams/$teamId/members/$memberId/route.ts +7 -0
- package/snippets/src/routes/teams/$teamId/old-items/$id/route.ts +13 -0
- package/snippets/src/routes/teams/$teamId/route.generated.ts +24 -0
- package/snippets/src/routes/teams/$teamId/route.ts +6 -0
- package/snippets/src/shell.ts +9 -0
- package/snippets/src/slug.ts +14 -0
- package/snippets/tsconfig.app.json +14 -0
- package/snippets/tsconfig.routes.json +5 -0
- package/types/loozo-angular-router-_core.d.ts +262 -242
- package/types/loozo-angular-router-_generated.d.ts +155 -116
- package/types/loozo-angular-router.d.ts +2 -1
- package/.npmignore +0 -3
- package/fesm2022/loozo-angular-router.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,144 +1,629 @@
|
|
|
1
1
|
# @loozo/angular-router
|
|
2
2
|
|
|
3
|
-
Typed route
|
|
3
|
+
Typed routes for Angular's own router. Each route is a directory holding a `defineRoute` declaration and, usually, a page. The declaration holds the route's path, its parent, and codecs for its path params, search params and fragments; the directories give the path and parent, and a build step keeps them in step. The build step then turns the declarations into an ordinary Angular `Routes` array plus a small typed module next to each route. Pages read parsed inputs as Angular signals and build links and navigations that TypeScript checks against the target route.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Angular still does all the routing: matching, activation, guards, resolvers, redirects, history and lazy loading. This package doesn't add a second router; it generates the configuration Angular consumes.
|
|
6
|
+
|
|
7
|
+
Use it when you want route params, query params and links to be type-checked end to end in an Angular 22 application, and you're happy to run generation through the provided Angular builders.
|
|
8
|
+
|
|
9
|
+
**Status:** experimental and not yet broadly battle-tested. Version `0.0.4`, MIT license.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
pnpm add @loozo/angular-router
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Peer dependencies:
|
|
18
|
+
|
|
19
|
+
| Package | Range |
|
|
20
|
+
| ----------------- | --------- |
|
|
21
|
+
| `@angular/core` | `^22.0.0` |
|
|
22
|
+
| `@angular/router` | `^22.0.0` |
|
|
23
|
+
| `@angular/build` | `~22.2.0` |
|
|
24
|
+
|
|
25
|
+
The package is tested against Angular 22.2.2. The `@angular/build` range is narrow on purpose: the application builder's options schema is Angular's own application schema at the version the package was built against. An option added in a later Angular minor is rejected until the package is rebuilt against that minor.
|
|
26
|
+
|
|
27
|
+
## Setup
|
|
28
|
+
|
|
29
|
+
Generation runs inside two Angular builders that replace your project's `build` and `serve` builders. Both run the official Angular builders (`@angular/build:application` and `@angular/build:dev-server`) after generating, so everything else about your build stays the same.
|
|
30
|
+
|
|
31
|
+
In `angular.json`:
|
|
32
|
+
|
|
33
|
+
`snippets/angular.json`
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"version": 1,
|
|
38
|
+
"projects": {
|
|
39
|
+
"app": {
|
|
40
|
+
"projectType": "application",
|
|
41
|
+
"root": "",
|
|
42
|
+
"sourceRoot": "src",
|
|
43
|
+
"architect": {
|
|
44
|
+
"build": {
|
|
45
|
+
"builder": "@loozo/angular-router:application",
|
|
46
|
+
"options": {
|
|
47
|
+
"tsconfig": "tsconfig.routes.json",
|
|
48
|
+
"scanDirectories": ["src/routes"],
|
|
49
|
+
"outDirectory": "generated",
|
|
50
|
+
"browser": "src/main.ts",
|
|
51
|
+
"index": "src/index.html",
|
|
52
|
+
"tsConfig": "tsconfig.app.json",
|
|
53
|
+
"outputPath": "dist/app"
|
|
54
|
+
},
|
|
55
|
+
"configurations": { "ci": { "check": true } }
|
|
56
|
+
},
|
|
57
|
+
"serve": {
|
|
58
|
+
"builder": "@loozo/angular-router:serve",
|
|
59
|
+
"options": { "buildTarget": "app:build" }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The application builder takes the router options (`tsconfig`, `scanDirectories`, `outDirectory`, and optionally `check` and `autoDefineRoutes`) next to your normal Angular application options, which it passes through unchanged. The serve builder takes normal dev-server options and reads the router options from its `buildTarget`. See [Builder options](#builder-options).
|
|
68
|
+
|
|
69
|
+
Note that `tsconfig` and `tsConfig` are different options. `tsconfig` is the TypeScript config used to find route declarations; `tsConfig` is Angular's own compilation config. The example app uses a small `tsconfig.routes.json` that extends the app config:
|
|
70
|
+
|
|
71
|
+
`snippets/tsconfig.routes.json`
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"extends": "./tsconfig.app.json",
|
|
76
|
+
"files": [],
|
|
77
|
+
"include": ["src/**/*.ts"]
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Your app's `tsconfig.app.json` must include the generated files, as its `"include"` does here:
|
|
82
|
+
|
|
83
|
+
`snippets/tsconfig.app.json`
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"compilerOptions": {
|
|
88
|
+
"target": "ES2022",
|
|
89
|
+
"module": "ESNext",
|
|
90
|
+
"moduleResolution": "Bundler",
|
|
91
|
+
"strict": true,
|
|
92
|
+
"skipLibCheck": true,
|
|
93
|
+
"lib": ["ES2022", "DOM"],
|
|
94
|
+
"types": []
|
|
95
|
+
},
|
|
96
|
+
"angularCompilerOptions": { "strictTemplates": true },
|
|
97
|
+
"files": ["src/main.ts"],
|
|
98
|
+
"include": ["src/**/*.ts", "generated/**/*.ts"]
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Then install the generated routes with Angular's `provideRouter`:
|
|
103
|
+
|
|
104
|
+
`snippets/src/main.ts`
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { bootstrapApplication } from "@angular/platform-browser";
|
|
108
|
+
import { provideRouter } from "@angular/router";
|
|
109
|
+
import Shell from "./shell.js";
|
|
110
|
+
import { routes } from "../generated/routes.generated.js";
|
|
111
|
+
|
|
112
|
+
void bootstrapApplication(Shell, { providers: [provideRouter(routes)] });
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Use the Angular CLI as usual:
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
ng build # generate, then the official application build
|
|
119
|
+
ng serve # generate and watch, then the official dev server
|
|
120
|
+
ng build --configuration ci # check generated files without writing them, then build
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Commit the generated files along with your route declarations, and run the `ci` configuration in CI so drift fails the build.
|
|
124
|
+
|
|
125
|
+
## Quick start
|
|
126
|
+
|
|
127
|
+
A route is a directory under the scan directory with a page, a `route.ts` declaration, or both. The directories give the path: a page in `src/routes/products/$productId/` is the route `products/$productId`, where `$productId` is a path param. Add the page, and the first `ng build` or `ng serve` creates its declaration with a `stringParam` codec for `productId`. The declaration is yours to edit; here the codec is now `numberParam`, with a search param and fragments added:
|
|
128
|
+
|
|
129
|
+
`snippets/src/routes/products/$productId/route.ts`
|
|
6
130
|
|
|
7
131
|
```ts
|
|
8
|
-
import {
|
|
9
|
-
defineRoute,
|
|
10
|
-
createUrl,
|
|
11
|
-
numberParam,
|
|
12
|
-
enumParam,
|
|
13
|
-
optionalSearchParam,
|
|
14
|
-
} from "@loozo/angular-router";
|
|
132
|
+
import { defineRoute, enumParam, numberParam, withFallback } from "@loozo/angular-router";
|
|
15
133
|
|
|
16
|
-
|
|
134
|
+
export default defineRoute("products/$productId", {
|
|
17
135
|
params: { productId: numberParam },
|
|
18
|
-
search: { tab:
|
|
136
|
+
search: { tab: withFallback(enumParam(["details", "reviews"]), "details") },
|
|
19
137
|
fragments: ["summary", "reviews"],
|
|
20
138
|
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`snippets/src/routes/products/$productId/page.ts`
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
import { Component } from "@angular/core";
|
|
145
|
+
import { RouterLink } from "@angular/router";
|
|
146
|
+
import { injectRoute } from "./route.generated.js";
|
|
147
|
+
|
|
148
|
+
@Component({
|
|
149
|
+
imports: [RouterLink],
|
|
150
|
+
template: `
|
|
151
|
+
<h1>Product {{ route.params().productId }}</h1>
|
|
152
|
+
<p>Tab: {{ route.search().tab }}</p>
|
|
153
|
+
<a [routerLink]="reviews">Reviews</a>
|
|
154
|
+
`,
|
|
155
|
+
})
|
|
156
|
+
export default class Product {
|
|
157
|
+
readonly route = injectRoute();
|
|
158
|
+
readonly reviews = this.route.link({
|
|
159
|
+
params: { productId: 42 },
|
|
160
|
+
search: { tab: "reviews" },
|
|
161
|
+
fragment: "reviews",
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Running `ng build` or `ng serve` also writes `generated/routes.generated.ts`, `generated/destinations.generated.ts` and `src/routes/products/$productId/route.generated.ts`. The page is lazy-loaded with `loadComponent`. See [Filesystem route definitions](#filesystem-route-definitions) for how directories map to paths and parents, and [Explicit authoring](#explicit-authoring) to write every path and parent yourself instead.
|
|
167
|
+
|
|
168
|
+
The template reads the signals directly: they hold the activated route's values from the moment the page is created. A URL such as `/products/abc` doesn't match this route at all: a path param that fails to parse makes Angular try its next route, such as a `$segments[]` not-found route. A missing, repeated or unknown `tab` reads as its fallback, `"details"`.
|
|
169
|
+
|
|
170
|
+
## Declaring routes
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
defineRoute(path, config);
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`path` must be a string literal; TypeScript infers everything else without generic arguments. By default the builder writes `path` and `parent` from the directories, and you write the rest.
|
|
177
|
+
|
|
178
|
+
### Paths
|
|
179
|
+
|
|
180
|
+
Paths are decoded segment text, not URLs:
|
|
181
|
+
|
|
182
|
+
| Path | Meaning |
|
|
183
|
+
| ----------------------- | ------------------------------------------------------------------------ |
|
|
184
|
+
| `""` | Root, or a pathless route when it has a parent |
|
|
185
|
+
| `"products"` | A static segment |
|
|
186
|
+
| `"products/$productId"` | `$name` as a whole segment declares a path param |
|
|
187
|
+
| `"docs/$segments[]"` | A terminal catchall: zero or more remaining segments, parsed as an array |
|
|
188
|
+
| `"$id/compare/$id"` | A repeated param uses the same value in every position |
|
|
189
|
+
|
|
190
|
+
No leading, trailing or repeated slashes. A catchall must be the last segment, a route can have at most one, and a catchall route can't have children. A root `$segments[]` route works as a not-found fallback; ordinary routes still match before it, unless their path params fail to parse.
|
|
191
|
+
|
|
192
|
+
### Config
|
|
193
|
+
|
|
194
|
+
| Key | Purpose |
|
|
195
|
+
| ------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
|
196
|
+
| `parent` | The parent route, imported as the parent declaration's default export |
|
|
197
|
+
| `params` | One codec per path param, keyed by bare name. Required exactly when the path has params |
|
|
198
|
+
| `search` | Codecs for query params |
|
|
199
|
+
| `fragments` | The fragment names this route accepts |
|
|
200
|
+
| `resolve` | Resolvers that receive parsed inputs |
|
|
201
|
+
| `canActivate`, `canActivateChild`, `canMatch` | Guards that receive parsed inputs |
|
|
202
|
+
| `onInvalidParams` | What happens when this route's own path params fail to parse, instead of not matching |
|
|
203
|
+
| `title` | A string, or a callback that receives parsed inputs |
|
|
204
|
+
| `canDeactivate`, `providers`, `data`, `runGuardsAndResolvers` | Passed to Angular unchanged |
|
|
205
|
+
| `redirect` | A typed redirect to another route |
|
|
206
|
+
|
|
207
|
+
Structural keys such as `path`, `children`, `component`, `loadComponent`, `redirectTo`, `pathMatch`, `matcher` and `outlet` are rejected: generation owns them.
|
|
208
|
+
|
|
209
|
+
### Parents
|
|
210
|
+
|
|
211
|
+
`snippets/src/routes/teams/$teamId/route.ts`
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
import { defineRoute, numberParam } from "@loozo/angular-router";
|
|
215
|
+
|
|
216
|
+
export default defineRoute("teams/$teamId", {
|
|
217
|
+
params: { teamId: numberParam },
|
|
218
|
+
search: { score: numberParam },
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`snippets/src/routes/teams/$teamId/members/$memberId/route.ts`
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
import { defineRoute, numberParam } from "@loozo/angular-router";
|
|
226
|
+
import team from "../../route.js";
|
|
227
|
+
|
|
228
|
+
export default defineRoute("members/$memberId", {
|
|
229
|
+
parent: team,
|
|
230
|
+
params: { memberId: numberParam },
|
|
231
|
+
});
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
The member directory is inside the team directory, so the team route is its parent. The team directory has no page, so the team route is componentless: it groups its children and their shared inputs. A child declares only its own params; it inherits its ancestors' params and search params, so the member route above has `params: { teamId, memberId }` and `search: { score }`. A child can't redeclare a param or search key its ancestors own. Pathless routes (`""` with a parent) add no URL segment but still contribute their inputs to descendants. A route with neither a page nor children is an error, unless it's a redirect.
|
|
235
|
+
|
|
236
|
+
### Codecs
|
|
237
|
+
|
|
238
|
+
| Codec | Value |
|
|
239
|
+
| ---------------------------- | ---------------------------------------------------------------------------- |
|
|
240
|
+
| `stringParam` | Any string, including `""` |
|
|
241
|
+
| `numberParam` | A finite number (`" 42 "`, `"1e3"` and `"0x10"` parse; blank and `NaN` fail) |
|
|
242
|
+
| `booleanParam` | Exactly `true` or `false` |
|
|
243
|
+
| `enumParam(values)` | One of the listed strings, typed as their union |
|
|
244
|
+
| `withFallback(codec, value)` | Search only: reads `value` when the key is missing, repeated or invalid |
|
|
245
|
+
|
|
246
|
+
For a catchall, the codec applies to each segment, so `$segments[]` with `stringParam` produces `readonly string[]`.
|
|
247
|
+
|
|
248
|
+
Search params never stop a route from matching, and they never fail. Every search key is optional:
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
search: {
|
|
252
|
+
page: withFallback(numberParam, 1),
|
|
253
|
+
sort: enumParam(["asc", "desc"]),
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
| Search key | Missing | Repeated or invalid |
|
|
258
|
+
| -------------------- | ------------ | ------------------- |
|
|
259
|
+
| Plain codec (`sort`) | Absent | Absent |
|
|
260
|
+
| `withFallback` | The fallback | The fallback |
|
|
261
|
+
|
|
262
|
+
`sort` reads as `"asc" | "desc" | undefined`. A key with a fallback always reads a value (`page` is a `number`, never `undefined`). Links, `createUrl`, navigation and redirects can leave out any search key.
|
|
263
|
+
|
|
264
|
+
A custom codec implements `ParamCodec<Value>`. Both methods are synchronous: `parse` receives the already-decoded text and returns the value, and `stringify` returns the text:
|
|
265
|
+
|
|
266
|
+
`snippets/src/slug.ts`
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
import type { ParamCodec } from "@loozo/angular-router";
|
|
270
|
+
|
|
271
|
+
export const slug: ParamCodec<string> = {
|
|
272
|
+
parse(raw) {
|
|
273
|
+
if (!/^[a-z0-9-]+$/.test(raw)) {
|
|
274
|
+
throw new Error("invalid slug");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return raw;
|
|
278
|
+
},
|
|
279
|
+
stringify(value) {
|
|
280
|
+
return value;
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Parsing should be pure and deterministic. A path codec runs twice per navigation: once while Angular matches routes and again for the activated route. A codec whose `parse` returns a Promise is a type error where you declare it; if one slips through (from JavaScript, say), the navigation fails with a `TypeError` rather than treating the value as invalid. Codecs don't run in an injection context and don't need one: load data in resolvers or guards instead.
|
|
286
|
+
|
|
287
|
+
`defineRoute` never calls parsers, guards, resolvers or other callbacks; it only records them.
|
|
288
|
+
|
|
289
|
+
The package root exports exactly `defineRoute`, `createUrl`, `stringParam`, `numberParam`, `booleanParam`, `enumParam` and `withFallback`, plus the types `Route` and `ParamCodec`. The `_core`, `_generated` and `_generation` subpaths are internal coupling for generated code and `@loozo/angular-launch`, not application APIs.
|
|
290
|
+
|
|
291
|
+
### Fragments
|
|
292
|
+
|
|
293
|
+
`fragments: ["summary", "reviews"]` lists the fragment names you can pass when creating a URL to this route. Without the list (or with an empty list), the `fragment` option is not allowed. The names belong to this route only; children don't inherit them. Incoming hashes aren't validated, and the package doesn't scroll: provide matching element IDs and configure Angular's scrolling yourself.
|
|
294
|
+
|
|
295
|
+
## Building URLs
|
|
296
|
+
|
|
297
|
+
`createUrl` builds a root-relative URL from a declaration, synchronously and without running any parsers:
|
|
298
|
+
|
|
299
|
+
`snippets/src/product-url.ts`
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
import { createUrl } from "@loozo/angular-router";
|
|
303
|
+
import product from "./routes/products/$productId/route.js";
|
|
21
304
|
|
|
22
305
|
createUrl(product, {
|
|
23
306
|
params: { productId: 42 },
|
|
24
307
|
search: { tab: "reviews" },
|
|
25
308
|
fragment: "summary",
|
|
26
|
-
}); // /products/42?tab=reviews#summary
|
|
309
|
+
}); // "/products/42?tab=reviews#summary"
|
|
27
310
|
```
|
|
28
311
|
|
|
29
|
-
|
|
312
|
+
All path params are required, including inherited ones. Search params are all optional: an omitted or `undefined` value leaves the key out. Query keys are sorted, values are percent-encoded, and a `/` inside a param value becomes `%2F`. Catchall values are arrays:
|
|
30
313
|
|
|
31
|
-
|
|
314
|
+
`snippets/src/docs-url.ts`
|
|
32
315
|
|
|
33
|
-
|
|
316
|
+
```ts
|
|
317
|
+
const docs = defineRoute("docs/$segments[]", { params: { segments: stringParam } });
|
|
34
318
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
319
|
+
createUrl(docs, { params: { segments: ["guides", "setup"] } }); // "/docs/guides/setup"
|
|
320
|
+
createUrl(docs, { params: { segments: [] } }); // "/docs"
|
|
321
|
+
```
|
|
38
322
|
|
|
39
|
-
|
|
323
|
+
Pass `{}` for a route without params or search. Wrong values fail to type-check. Unknown keys fail to type-check in an object literal; an object passed through a variable can carry extra keys past TypeScript, but `createUrl` still throws a `TypeError` for them at runtime.
|
|
40
324
|
|
|
41
|
-
|
|
325
|
+
## Reading inputs and navigating
|
|
42
326
|
|
|
43
|
-
|
|
327
|
+
Each route that has a page or descendants with pages gets a colocated `route.generated.ts`; redirect routes, and parents left with only redirects below them, get none. It exports two things:
|
|
44
328
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
329
|
+
- `injectRoute()`: call it in an injection context (a field initializer or constructor) to get the handle for this route.
|
|
330
|
+
- `destination`: an inert reference to this route that other routes can link to.
|
|
331
|
+
|
|
332
|
+
`snippets/src/routes/teams/$teamId/items/$id/inputs.ts`
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
import { injectRoute } from "./route.generated.js";
|
|
336
|
+
|
|
337
|
+
// injectRoute() needs an injection context, such as a component field initializer.
|
|
338
|
+
export function readInputs() {
|
|
339
|
+
const route = injectRoute();
|
|
340
|
+
|
|
341
|
+
route.params(); // { teamId: number; id: number }
|
|
342
|
+
route.search(); // { score?: number }
|
|
343
|
+
route.data(); // { item: Item }, local resolver results only
|
|
344
|
+
}
|
|
59
345
|
```
|
|
60
346
|
|
|
61
|
-
|
|
347
|
+
`params`, `search` and `data` are read-only Angular signals holding the activated route's values. They have a value from the first read, even in the page's constructor, so there's no loading state to check. While a navigation is in progress they keep the values of the page that's still on screen. When a navigation reuses this route with new values (a new id, or new search params), all three change together. When a navigation is cancelled or fails, they keep their values; handle the error where Angular reports it: the rejected `navigate` promise, the `NavigationError` event or `withNavigationErrorHandler`. To show that a navigation is pending, read Angular's `Router.currentNavigation()` signal.
|
|
62
348
|
|
|
63
|
-
|
|
349
|
+
`data` holds this route's own resolver results only, not static `data`, parent data or the title.
|
|
64
350
|
|
|
65
|
-
|
|
351
|
+
Reading `params` or `search` throws only if a path codec rejects a value it accepted while matching, which a pure codec never does.
|
|
66
352
|
|
|
67
|
-
|
|
353
|
+
### Links and navigation
|
|
68
354
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
355
|
+
`link` returns an Angular `UrlTree` for `RouterLink`; `navigate` calls `Router.navigateByUrl` and returns Angular's own promise. Both take the same arguments:
|
|
356
|
+
|
|
357
|
+
`snippets/src/routes/teams/$teamId/items/$id/links.ts`
|
|
358
|
+
|
|
359
|
+
```ts
|
|
360
|
+
import { destination as report } from "../../../../reports/$reportId/route.generated.js";
|
|
361
|
+
import { injectRoute } from "./route.generated.js";
|
|
362
|
+
|
|
363
|
+
// injectRoute() needs an injection context, such as a component field initializer.
|
|
364
|
+
export function itemLinks() {
|
|
365
|
+
const route = injectRoute();
|
|
366
|
+
|
|
367
|
+
route.link({ params: { teamId: 7, id: 8 } }); // this route
|
|
368
|
+
route.link(".", { params: { teamId: 7, id: 8 } }); // same as above
|
|
369
|
+
route.link("/teams/$teamId/items/$id", { params: { teamId: 7, id: 9 } }); // absolute
|
|
370
|
+
route.link("../..", { params: { teamId: 7 } }); // relative
|
|
371
|
+
void route.navigate(report, { params: { reportId: 42 } }); // another route's destination
|
|
372
|
+
}
|
|
73
373
|
```
|
|
74
374
|
|
|
75
|
-
The
|
|
375
|
+
The first argument, when given, is a string literal selecting a route by its full authored path, never a concrete URL. Absolute selectors start with `/`. Relative selectors start with `.`, `./` or `..`; each `..` removes one segment of the path template, not one directory or parent. TypeScript only offers selectors that resolve to a known route, and the values are always typed for the selected route.
|
|
76
376
|
|
|
77
|
-
|
|
377
|
+
Values are never carried over from the current URL. Pass every required param and search value explicitly, and pass `fragment` only when the target declares it. There is no extras argument; use `RouterLink` inputs such as `replaceUrl` or `state` for those.
|
|
78
378
|
|
|
79
|
-
|
|
80
|
-
|
|
379
|
+
A `destination` must come from the same generated route set as the handle. Destinations are for `link` and `navigate` only; `createUrl` takes declarations.
|
|
380
|
+
|
|
381
|
+
## Guards, resolvers and redirects
|
|
382
|
+
|
|
383
|
+
`canActivate`, `canActivateChild` and `canMatch` take arrays of functions whose first argument is the parsed inputs. Angular's native arguments follow:
|
|
384
|
+
|
|
385
|
+
`snippets/src/routes/reports/$reportId/route.ts`
|
|
386
|
+
|
|
387
|
+
```ts
|
|
388
|
+
import { defineRoute, numberParam } from "@loozo/angular-router";
|
|
389
|
+
|
|
390
|
+
export default defineRoute("reports/$reportId", {
|
|
391
|
+
params: { reportId: numberParam },
|
|
392
|
+
canActivate: [({ params }) => params.reportId > 0],
|
|
393
|
+
});
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
| Guard | Inputs |
|
|
397
|
+
| ------------------ | ---------------------------------------------------------------------------- |
|
|
398
|
+
| `canActivate` | This route's params and search, including inherited ones |
|
|
399
|
+
| `canActivateChild` | The declaring (parent) route's params and search |
|
|
400
|
+
| `canMatch` | This route's own path params plus all search params; no ancestor path params |
|
|
401
|
+
| `canDeactivate` | Angular's native form, unchanged; no parsing |
|
|
402
|
+
|
|
403
|
+
Guards return anything Angular accepts (`boolean`, `UrlTree`, `RedirectCommand`, or a Promise or Observable of those). Search values recover before any guard runs (absent, or the fallback). Path params that fail to parse decide whether a route matches, so a guard only sees valid ones. Every callback (guards, resolvers, `title` and `onInvalidParams`) is called synchronously in the injection context Angular provides, so `inject()` works at its start, before any `await`.
|
|
404
|
+
|
|
405
|
+
Every route with path params of its own or with search params gets a generated `canMatch` that parses them, search after the path. Search never decides the match. If a path param fails, the route doesn't match and Angular tries its next route, ending at the closest `$segments[]` catchall, so guards, resolvers and pages never see a value their codec rejects. Your own `canMatch` guards aren't called then. `onInvalidParams` replaces that outcome:
|
|
406
|
+
|
|
407
|
+
```ts
|
|
408
|
+
export default defineRoute("items/$id", {
|
|
409
|
+
params: { id: numberParam },
|
|
410
|
+
onInvalidParams: (error, routeConfig, segments, partialSnapshot) =>
|
|
411
|
+
new RedirectCommand(inject(Router).parseUrl("/items")),
|
|
412
|
+
});
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
It receives the parse error (`source: "path"`, `key`, `reason`, and the codec's error as `cause`) and Angular's `canMatch` arguments, runs in the route's injection context, and returns `false` (try the next route), a `UrlTree` or a `RedirectCommand`, or a Promise or Observable of one. It can't return `true`: that, or throwing, fails the navigation. Write `async () => false as const` rather than `async () => false`, which TypeScript widens to `boolean`.
|
|
416
|
+
|
|
417
|
+
A value that parses but doesn't exist, such as an unknown slug, is still yours to handle, for example with a `canActivate` that returns `new RedirectCommand(notFoundUrl, { browserUrl: state.url })`: Angular renders your not-found route while the address bar keeps the requested URL. The not-found URL must not match the guarded route again, or the redirect loops.
|
|
418
|
+
|
|
419
|
+
Resolvers get the parsed inputs too:
|
|
420
|
+
|
|
421
|
+
`snippets/src/routes/teams/$teamId/items/$id/route.ts`
|
|
422
|
+
|
|
423
|
+
```ts
|
|
424
|
+
import { inject } from "@angular/core";
|
|
425
|
+
import { defineRoute, numberParam } from "@loozo/angular-router";
|
|
426
|
+
import team from "../../route.js";
|
|
427
|
+
import { ItemCatalog } from "../../../../../catalog.js";
|
|
428
|
+
|
|
429
|
+
export default defineRoute("items/$id", {
|
|
430
|
+
parent: team,
|
|
431
|
+
params: { id: numberParam },
|
|
432
|
+
runGuardsAndResolvers: "paramsOrQueryParamsChange",
|
|
433
|
+
title: ({ params }) => `Item ${params.id}`,
|
|
434
|
+
resolve: {
|
|
435
|
+
item: ({ params, search }) => {
|
|
436
|
+
const catalog = inject(ItemCatalog);
|
|
437
|
+
|
|
438
|
+
return catalog({ teamId: params.teamId, id: params.id, score: search.score });
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
});
|
|
81
442
|
```
|
|
82
443
|
|
|
83
|
-
|
|
444
|
+
`title` is a string or a callback like a resolver's that returns the title (or a Promise or Observable of it). Angular runs a title callback alongside the route's resolvers, so it can't read their results.
|
|
445
|
+
|
|
446
|
+
Angular's default rerun policy, `paramsChange`, reruns resolvers and the title callback when the path or its path/matrix params change, but not when only search params change. Set `runGuardsAndResolvers` when a resolver reads search params, as above.
|
|
447
|
+
|
|
448
|
+
A redirect targets another declaration and must say what happens to search and fragment:
|
|
449
|
+
|
|
450
|
+
`snippets/src/routes/teams/$teamId/old-items/$id/route.ts`
|
|
451
|
+
|
|
452
|
+
```ts
|
|
453
|
+
import { defineRoute, numberParam } from "@loozo/angular-router";
|
|
454
|
+
import team from "../../route.js";
|
|
455
|
+
import item from "../../items/$id/route.js";
|
|
456
|
+
|
|
457
|
+
export default defineRoute("old-items/$id", {
|
|
458
|
+
parent: team,
|
|
459
|
+
params: { id: numberParam },
|
|
460
|
+
redirect: {
|
|
461
|
+
to: item,
|
|
462
|
+
search: { mode: "drop" }, // or "preserve", or { mode: "replace", values }
|
|
463
|
+
fragment: { mode: "drop" }, // or "preserve", or { mode: "replace", value }
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
Params with the same name and compatible types are forwarded automatically; otherwise map them with `params: { targetKey: "sourceKey" }`. Values are parsed with the source codecs and written with the target's codecs. A redirect route can't have a page, children, `canActivate`, `canMatch` or `onInvalidParams`; an invalid param in a redirect's source URL fails the navigation.
|
|
469
|
+
|
|
470
|
+
## Filesystem route definitions
|
|
471
|
+
|
|
472
|
+
By default, directories give each route its path and parent. A directory becomes a route when it has a page or a declaration. The builder creates an editable `route.ts` in each directory that has a page but no declaration yet. Every other directory, the scan directory included, isn't a route: it adds its name to the paths of the routes below it. Each route's parent is the nearest route above it.
|
|
473
|
+
|
|
474
|
+
| Directory | Path contribution |
|
|
475
|
+
| ------------------ | --------------------------------------------------------------------------- |
|
|
476
|
+
| `products`, `$id` | Its name |
|
|
477
|
+
| `+layout` | `""`: a pathless route when it has a page or declaration, otherwise nothing |
|
|
478
|
+
| The scan directory | `""`: the root route when it has a page or declaration, otherwise nothing |
|
|
479
|
+
| `-helpers` | Nothing, and never a route; routes inside attach to the nearest route above |
|
|
480
|
+
|
|
481
|
+
For example, with pages in `src/routes/$package/` and `src/routes/$package/guide/$guide/` but none in `guide/`:
|
|
482
|
+
|
|
483
|
+
```text
|
|
484
|
+
src/routes/
|
|
485
|
+
$package/
|
|
486
|
+
page.ts
|
|
487
|
+
route.ts defineRoute("$package", …): no parent
|
|
488
|
+
guide/ no page: adds "guide" to the path below
|
|
489
|
+
$guide/
|
|
490
|
+
page.ts
|
|
491
|
+
route.ts defineRoute("guide/$guide", …): parent is $package
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
The generated declaration for `$package/guide/$guide/page.ts` looks like this:
|
|
495
|
+
|
|
496
|
+
```ts
|
|
497
|
+
import { defineRoute, stringParam } from "@loozo/angular-router";
|
|
498
|
+
import parent from "../../route.js";
|
|
499
|
+
|
|
500
|
+
export default defineRoute("guide/$guide", { parent, params: { guide: stringParam } });
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
Angular gets `{ path: ":package", children: [{ path: "guide/:guide" }] }`, the same routes as writing those two declarations by hand, so `/angular-router/guide` on its own matches neither of them. If a directory without a page needs behavior of its own, such as a guard or resolver shared by the routes below it, add a declaration to it. It then becomes a componentless route and the parent of the routes below it. Angular also matches its bare URL (`/angular-router/guide`) when no child does, and SSR prerendering lists it as a route.
|
|
504
|
+
|
|
505
|
+
These files are yours. Change the codecs, add search params, guards and resolvers; the builder leaves them alone. When you move or rename a directory, or a route above it appears or disappears, the builder updates only the path literal, the `parent` property and its import, and the local params where the change is unambiguous: it renames a single changed param key and keeps its codec, and when params are only added (for example, a declaration moved under a page-less `$team` directory) it keeps every existing codec and adds a `stringParam` codec per new param, also to a declaration that had no params. It reuses your `stringParam` import when the file has one, and otherwise imports it as `stringParam`, or `filesystemStringParam` when the file already uses that name. When it can't make an edit safely (for example, a removed param whose codec you customized, or params spread from another object), generation fails before writing anything. The error names the file, the path the directories give and the params it can't reconcile. Make that file's `params` match the directories, with one key per param in that path (or no `params` at all when the path has none), and the next run updates only the path; or move or rename the directory. Params spread from another object, kept in a shared variable or written as shorthand (`{ id }`) must first be written as plain `name: codec` properties. The builder never removes a param's codec itself, and the path always follows the directories. It never deletes declarations.
|
|
506
|
+
|
|
507
|
+
A new import goes on its own line after the file's last import and any comment on that import's line. When that comment spans lines or carries a directive for the next line (such as `// @ts-expect-error`), the new import goes before the comment instead, so the directive keeps its target. A new parent import is named `parent`, or `filesystemParent` when the file already uses that name. When an existing parent import still points at a route that's no longer the parent, for example because you added a declaration between them, it's repointed and renamed to `parent`. When it no longer resolves, because its file moved or its target was deleted, it's repointed and keeps its name. An import that's used for more than the `parent` property is left alone, and a new parent import is added beside it.
|
|
508
|
+
|
|
509
|
+
A `-` directory can't contain a page or a declaration with behavior.
|
|
84
510
|
|
|
85
|
-
|
|
511
|
+
Set `"autoDefineRoutes": { "filename": "definition.ts" }` on the application target to seed a different file name. It must be a plain `.ts` file name that isn't a generated file name. That only affects new files; existing declarations are found by content, whatever their name.
|
|
86
512
|
|
|
87
|
-
|
|
513
|
+
### Explicit authoring
|
|
514
|
+
|
|
515
|
+
To write every path and parent yourself, turn filesystem definitions off:
|
|
88
516
|
|
|
89
517
|
```json
|
|
90
|
-
{
|
|
518
|
+
"options": {
|
|
91
519
|
"tsconfig": "tsconfig.routes.json",
|
|
92
520
|
"scanDirectories": ["src/routes"],
|
|
93
521
|
"outDirectory": "generated",
|
|
94
|
-
"
|
|
95
|
-
"autoDefineRoutes": {}
|
|
522
|
+
"autoDefineRoutes": false
|
|
96
523
|
}
|
|
97
524
|
```
|
|
98
525
|
|
|
99
|
-
|
|
526
|
+
Declarations look the same, but the builder never creates or edits them, and directory names don't affect paths or parents. File names are up to you: the builder finds declarations by their `export default defineRoute(...)` and pages by their default-exported standalone component, not by name. A page still needs a declaration in its own directory; a page without one fails generation.
|
|
100
527
|
|
|
101
|
-
|
|
528
|
+
## Server rendering and hydration
|
|
102
529
|
|
|
103
|
-
The
|
|
530
|
+
The generated routes work with Angular SSR and `provideClientHydration` without extra setup; there's no transfer cache or custom renderer. Parsing is synchronous and resolvers keep Angular's stability pending, so server-rendered HTML contains parsed values and resolved data.
|
|
104
531
|
|
|
105
|
-
`
|
|
532
|
+
[`examples/angular-router/ssr-proof`](../../examples/angular-router/ssr-proof/README.md) is a working SSR app with a search fallback, a resolver, a redirect and hydration.
|
|
106
533
|
|
|
107
|
-
##
|
|
534
|
+
## What gets generated
|
|
108
535
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
536
|
+
| File | Contents |
|
|
537
|
+
| ------------------------------------------ | ------------------------------------------------------------- |
|
|
538
|
+
| `<outDirectory>/routes.generated.ts` | `routes: Routes` for `provideRouter` |
|
|
539
|
+
| `<outDirectory>/destinations.generated.ts` | Shared typing and navigation data (when any route has a page) |
|
|
540
|
+
| `route.generated.ts` beside each route | `injectRoute()` and `destination`; none for redirect routes |
|
|
112
541
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
542
|
+
Each generated file starts with an ownership marker comment such as `// @loozo/angular-router routegen: owned route.generated.ts`. The builder only replaces or deletes marked files with those exact names. It never overwrites an unmarked file in their place, and it removes stale marked files when their routes disappear. Hand edits to generated files are overwritten.
|
|
543
|
+
|
|
544
|
+
Generation reads your source without importing or running it. It validates everything (declarations, overlapping routes, redirects, file ownership) before writing anything, so a run that fails validation leaves your files as they were. Files are then written one at a time, with no rollback: if writing fails partway, for example on a file that isn't writable, or the run is interrupted, some files may already be updated. The next successful run repairs them. Unchanged files aren't rewritten, so their modification times stay the same.
|
|
545
|
+
|
|
546
|
+
With `check: true`, the builder computes the same result but writes nothing. It fails if any generated file is changed, missing or stale, and the Angular build doesn't run.
|
|
547
|
+
|
|
548
|
+
Generation checks route structure, not your whole program. Type errors elsewhere still show up in the Angular build itself.
|
|
549
|
+
|
|
550
|
+
## Builder options
|
|
551
|
+
|
|
552
|
+
### `@loozo/angular-router:application`
|
|
553
|
+
|
|
554
|
+
All `@angular/build:application` options, plus:
|
|
555
|
+
|
|
556
|
+
| Option | Type | Default | Description |
|
|
557
|
+
| ------------------ | -------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
558
|
+
| `tsconfig` | `string` | required | TypeScript config used to find route declarations. Relative to the workspace root. |
|
|
559
|
+
| `scanDirectories` | `string[]` | required | Exactly one directory containing your routes. Relative to the `tsconfig` file's directory. |
|
|
560
|
+
| `outDirectory` | `string` | required | Where `routes.generated.ts` and `destinations.generated.ts` go. Relative to the workspace root; must be outside the scan directory. |
|
|
561
|
+
| `check` | `boolean` | `false` | Verify generated files without writing them. Fails on any drift. |
|
|
562
|
+
| `autoDefineRoutes` | `false \| { filename?: string }` | `{}` | [Filesystem route definitions](#filesystem-route-definitions), seeding `filename` (default `route.ts`). `false` selects [explicit authoring](#explicit-authoring). |
|
|
119
563
|
|
|
120
|
-
|
|
564
|
+
The builder runs generation once, then the official application build. Invalid options stop the build before generation starts. Generation errors, including drift in check mode, are logged and fail the build before the Angular build runs. `watch: true` is rejected; use the serve builder for watching.
|
|
121
565
|
|
|
122
|
-
|
|
566
|
+
### `@loozo/angular-router:serve`
|
|
123
567
|
|
|
124
|
-
|
|
568
|
+
All `@angular/build:dev-server` options, set in `angular.json` or on the command line (`ng serve --port 4300`), plus:
|
|
125
569
|
|
|
126
|
-
|
|
570
|
+
| Option | Type | Default | Description |
|
|
571
|
+
| ------------- | ----------------- | -------- | ------------------------------------------------------------------------------------ |
|
|
572
|
+
| `buildTarget` | `string` | required | A target that uses `@loozo/angular-router:application`. Router options come from it. |
|
|
573
|
+
| `poll` | `number \| false` | `1000` | File-watch polling interval in milliseconds, or `false` for no default. |
|
|
127
574
|
|
|
128
|
-
|
|
575
|
+
Invalid options stop the serve builder before it generates anything, and so does an invalid build target. It watches the scan directory and regenerates when files are added, changed or removed. The initial generation must succeed before the dev server starts; if it fails, the error is logged and the builder reports a failed build. Later errors are logged, and the server keeps running until you fix them. The build target must not resolve `check: true`, because serving always writes generated files.
|
|
129
576
|
|
|
130
|
-
Angular 22.
|
|
577
|
+
Polling is on by default because Angular 22.2.2's native file watcher misses edits to files inside a renamed directory, which leaves the dev server serving stale output. Polling costs some idle CPU (about 3% of one core on the example app at 1000 ms, against under 0.1% without), and more on larger workspaces.
|
|
131
578
|
|
|
132
|
-
|
|
579
|
+
To go back to native file events, set `"poll": false` in the serve target or one of its configurations. The router then adds no default, and Angular's own configuration applies, including a `poll` set on the build target. A positive number overrides the build target's `poll`. `0` is rejected. On the command line, `--poll` accepts milliseconds only; set `false` in `angular.json`.
|
|
133
580
|
|
|
134
|
-
|
|
581
|
+
## Limitations
|
|
135
582
|
|
|
136
|
-
|
|
583
|
+
- No standalone generation CLI or public route-graph API. Generate through the two builders.
|
|
584
|
+
- Route and page files must be `.ts`; `.mts`, `.cts` and `.tsx` are ignored.
|
|
585
|
+
- Primary outlet only: no named outlets, custom matchers or `loadChildren`.
|
|
586
|
+
- One scan directory per application.
|
|
587
|
+
- No data, URL or transfer caches. Resolver data lives in Angular.
|
|
588
|
+
- Relative navigation from a catchall route isn't supported; use `"."` or an absolute selector.
|
|
589
|
+
- Angular 22.2.2 drops a resolver result named `__proto__`, so a resolver with that name has no value in `data`.
|
|
590
|
+
- Writes are individual file replacements. A write error or an interrupted run can leave mixed output; the next successful run repairs it.
|
|
137
591
|
|
|
138
|
-
##
|
|
592
|
+
## Further reading
|
|
139
593
|
|
|
140
|
-
|
|
594
|
+
- [`SPEC.md`](SPEC.md): the full contract, including every edge case
|
|
595
|
+
- [`examples/angular-router`](../../examples/angular-router/): a minimal app using the builders
|
|
596
|
+
- [`examples/angular-router/ssr-proof`](../../examples/angular-router/ssr-proof/README.md): SSR and hydration
|
|
141
597
|
|
|
142
|
-
|
|
598
|
+
## Developing this package
|
|
143
599
|
|
|
144
|
-
|
|
600
|
+
Requires Node 24.21.0 and pnpm 12.9.1. Install from the workspace root with `pnpm_config_pm_on_fail=error pnpm_config_verify_deps_before_run=error pnpm install --frozen-lockfile`. From this directory:
|
|
601
|
+
|
|
602
|
+
```sh
|
|
603
|
+
pnpm run build # Node builders/generation (tsc), then the Angular package (ng-packagr)
|
|
604
|
+
pnpm run check # formatting and lint
|
|
605
|
+
pnpm run typecheck # strict source typecheck
|
|
606
|
+
pnpm run typecheck:consumers # consumer typecheck against the built package (NodeNext and Bundler)
|
|
607
|
+
pnpm run test # root runtime, published-docs and schema tests
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
`build` regenerates `schemas/application.json` from the installed `@angular/build`; `pnpm test` fails when the committed schema is stale.
|
|
611
|
+
|
|
612
|
+
The browser tests use real Angular builds and Chromium (`/usr/bin/chromium`). Each takes a fresh absolute directory, which must not exist yet, for its evidence:
|
|
613
|
+
|
|
614
|
+
```sh
|
|
615
|
+
pnpm run test:application /absolute/new-directory
|
|
616
|
+
pnpm run test:navigation /absolute/new-directory
|
|
617
|
+
pnpm run test:lifecycle /absolute/new-directory
|
|
618
|
+
pnpm run test:ssr /absolute/new-directory
|
|
619
|
+
pnpm run test:filesystem /absolute/new-directory
|
|
620
|
+
node tests/packed.mjs /absolute/new-directory # packed-package consumer check
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
To run the example app from the workspace root after building the package:
|
|
624
|
+
|
|
625
|
+
```sh
|
|
626
|
+
pnpm --filter @loozo/angular-router run example:build # generate + build
|
|
627
|
+
pnpm --filter @loozo/angular-router run example:check # check generated files + build
|
|
628
|
+
pnpm --filter @loozo/angular-router run example:serve # generate + dev server
|
|
629
|
+
```
|