@craft-ts/mcp 0.8.0 → 0.8.2
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/content/best-practices.md +1 -1
- package/content/docs-index.json +34 -19
- package/package.json +2 -2
- package/skills/craft-ts-architecture-tests/SKILL.md +1 -1
- package/skills/craft-ts-routes/SKILL.md +29 -33
- package/skills/craft-ts-routes/references/di-checks.md +34 -80
- package/skills/craft-ts-routes/references/eslint-workflow.md +19 -34
- package/skills/craft-ts-routes/references/pending-and-exceptions.md +4 -10
- package/skills/craft-ts-routes/references/scaling-and-pitfalls.md +34 -87
- package/skills/craft-ts-service-migration/SKILL.md +5 -4
- package/skills/migrate-to-craft-ts/SKILL.md +1 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@craft-ts/mcp",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "MCP server, Agent Skills, and LLM files for coding agents using @craft-ts/core",
|
|
5
5
|
"author": "Romain Geffrault",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"test": "vitest run --config vitest.config.mts"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@craft-ts/dev-tools": "^0.8.
|
|
38
|
+
"@craft-ts/dev-tools": "^0.8.2",
|
|
39
39
|
"@modelcontextprotocol/sdk": "1.26.0",
|
|
40
40
|
"zod": "4.3.6"
|
|
41
41
|
},
|
|
@@ -28,7 +28,7 @@ generates the application, API/page example, routes, ESLint, unit tests,
|
|
|
28
28
|
architecture suite and Playwright commands together:
|
|
29
29
|
|
|
30
30
|
```shell
|
|
31
|
-
npx craft create my-app --effect=none --agents=codex,cursor,
|
|
31
|
+
npx craft create my-app --effect=none --agents=codex,cursor,claude-code
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
For an Effect v4 starter:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: craft-ts-routes
|
|
3
|
-
description: Best practices for creating and maintaining type-safe routes in Angular apps that use @craft-ts/core (craft-ts). Use this whenever you create or edit a routes file, call craftRoutes(...) / route(...), declare componentDeps, wire loadChildren, add a pendingComponent or a view-transition payload,
|
|
3
|
+
description: Best practices for creating and maintaining type-safe routes in Angular apps that use @craft-ts/core (craft-ts). Use this whenever you create or edit a routes file, call craftRoutes(...) / route(...), declare componentDeps, wire loadChildren, add a pendingComponent or a view-transition payload, or need a per-route DI check (RouteCheckedDI). Apply it even when the user only says "add a route", "split this route file", "fix this routing type error", or "the injectXxx helper is missing" — getting the DI checks, the loadChildren split, and exhaustiveness right is non-obvious and easy to skip, and skipping it silently disables type-safe DI.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Creating craft-ts routes
|
|
@@ -9,9 +9,9 @@ description: Best practices for creating and maintaining type-safe routes in Ang
|
|
|
9
9
|
|
|
10
10
|
Produce route files that keep **compile-time dependency-injection safety** intact. In craft-ts a route
|
|
11
11
|
is not just a path → component map: each route declares the dependencies its component injects
|
|
12
|
-
(`componentDeps`), and a per-
|
|
12
|
+
(`componentDeps`), and a per-route check turns any unmet dependency into a TypeScript error. The whole
|
|
13
13
|
value proposition collapses the moment a route ships without its check, so the rules below are about
|
|
14
|
-
never letting that happen
|
|
14
|
+
never letting that happen.
|
|
15
15
|
|
|
16
16
|
If the public API names here don't match the installed version, confirm against the app's own routes and
|
|
17
17
|
`node_modules/@craft-ts/core`; prefer the patterns already used in the repo.
|
|
@@ -22,23 +22,19 @@ If the public API names here don't match the installed version, confirm against
|
|
|
22
22
|
component route a `componentDeps: {} as import('./x').GenDeps_X` line. That line is the wire between
|
|
23
23
|
the component's generated dependency type and the route check; without it the route's DI is invisible.
|
|
24
24
|
|
|
25
|
-
2. **Every
|
|
25
|
+
2. **Every routed component carries its own DI check.** Add, in the route file:
|
|
26
26
|
```ts
|
|
27
|
-
type _CheckXDI =
|
|
27
|
+
type _CheckXDI = RouteCheckedDI<ComponentDeps, ParentNames, ParentValues, 'route'>;
|
|
28
28
|
type _CanRunX = CanRun<_CheckXDI>;
|
|
29
29
|
```
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
unchecked. A missing provider then surfaces as `Injected SomeService is not provided in path: "…"`.
|
|
33
|
-
Architecture tests (`assertRouteDiProofs`) fail CI if a collection ships without an armed check.
|
|
30
|
+
A missing provider then surfaces as `Injected SomeService is not provided in path: "…"`.
|
|
31
|
+
Architecture tests (`assertRouteDiProofs`) fail CI if a routed component ships without an armed check.
|
|
34
32
|
That helper belongs in the app's `architecture/` suite — load `craft-ts-architecture-tests` to
|
|
35
33
|
scaffold or keep it armed. Do not add an architecture rule for the feature.
|
|
36
34
|
|
|
37
|
-
3. **
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
architectural, not a tweak: move routes into lazy child collections joined by `loadChildren`, each with
|
|
41
|
-
its own check. See [references/scaling-and-pitfalls.md](references/scaling-and-pitfalls.md).
|
|
35
|
+
3. **Keep route ownership clear — split with `loadChildren`.** Move a feature into a lazy child collection
|
|
36
|
+
when it deserves a separate loading or ownership boundary. Each routed component in the child still
|
|
37
|
+
carries its own `RouteCheckedDI` check. See [references/scaling-and-pitfalls.md](references/scaling-and-pitfalls.md).
|
|
42
38
|
|
|
43
39
|
4. **Keep exception handling exhaustive.** A route whose `canActivate` / `canMatch` / `resolve` can throw
|
|
44
40
|
a typed `craftException` must handle exactly those codes. Use the 3-arg `route(path, def, handlers)`
|
|
@@ -62,11 +58,11 @@ import {
|
|
|
62
58
|
route,
|
|
63
59
|
assertExhaustiveRouteExceptions,
|
|
64
60
|
type CanRun,
|
|
65
|
-
type
|
|
61
|
+
type RouteCheckedDI,
|
|
66
62
|
} from '@craft-ts/core';
|
|
67
63
|
import type { Router } from '@angular/router';
|
|
68
64
|
|
|
69
|
-
export const { featureRoutes,
|
|
65
|
+
export const { featureRoutes, FeatureUserIdParams } = craftRoutes('feature', [
|
|
70
66
|
route(':userId', {
|
|
71
67
|
componentDeps: {} as import('./user-detail').GenDeps_UserDetailComponent,
|
|
72
68
|
loadComponent: () => import('./user-detail'),
|
|
@@ -77,8 +73,13 @@ export const { featureRoutes, injectFeatureUserIdParams } = craftRoutes('feature
|
|
|
77
73
|
// Exhaustive over canActivate ∪ canMatch ∪ resolve for the whole collection.
|
|
78
74
|
assertExhaustiveRouteExceptions(featureRoutes);
|
|
79
75
|
|
|
80
|
-
// DI safety for
|
|
81
|
-
type _CheckFeatureDI =
|
|
76
|
+
// DI safety for this routed component.
|
|
77
|
+
type _CheckFeatureDI = RouteCheckedDI<
|
|
78
|
+
import('./user-detail').GenDeps_UserDetailComponent,
|
|
79
|
+
never,
|
|
80
|
+
Router,
|
|
81
|
+
'feature/:userId'
|
|
82
|
+
>;
|
|
82
83
|
type _CanRunFeature = CanRun<_CheckFeatureDI>;
|
|
83
84
|
```
|
|
84
85
|
|
|
@@ -97,18 +98,17 @@ The parent registers the child as a near-free lazy entry:
|
|
|
97
98
|
- **Adding a component route** → add `loadComponent` (or `component`) **and** `componentDeps:
|
|
98
99
|
{} as import('./x').GenDeps_X`. If `GenDeps_X` doesn't exist yet, generate it (ESLint Quick Fix
|
|
99
100
|
`brand-angular-gen-deps-required`, or `craft:brand`), don't write it by hand.
|
|
100
|
-
- **A
|
|
101
|
-
|
|
101
|
+
- **A feature needs a separate loading or ownership boundary** → split it into a new `craftRoutes(...)`
|
|
102
|
+
file mounted via `loadChildren`, and give every routed component in that file its own check.
|
|
102
103
|
- **A lazy child only makes sense under one specific path** (it relies on that route's params, payload, or
|
|
103
104
|
providers) → pin it with `.withParent<ParentRoutes<'path'>>()` and enforce placement in the parent with
|
|
104
105
|
`assertChildRouteMounts(parentRoutes)`. See the scaling reference.
|
|
105
106
|
- **Slow guard/resolve + you want a skeleton** → add `pendingComponent: () => import('./skeleton')`. The
|
|
106
|
-
|
|
107
|
+
route check does not cover the skeleton, so verify its DI separately with a per-component `RouteCheckedDI`. For a
|
|
107
108
|
shared-element morph, declare the payload with `viewTransitionPayload<T>()`. See
|
|
108
109
|
[references/pending-and-exceptions.md](references/pending-and-exceptions.md).
|
|
109
|
-
- **A single file genuinely must hold a big flat list**
|
|
110
|
-
|
|
111
|
-
thousands). See the DI-checks reference.
|
|
110
|
+
- **A single file genuinely must hold a big flat list** → use one `RouteCheckedDI` / `CanRun` pair per
|
|
111
|
+
routed component. The checks do not recurse through sibling routes.
|
|
112
112
|
- **You changed a component's `inject`/`imports`/`providers`** → regenerate its `GenDeps_*` (Quick Fix /
|
|
113
113
|
`craft:brand`) so the route check sees the new shape, then re-run ESLint `--fix` on the routes file.
|
|
114
114
|
|
|
@@ -116,19 +116,15 @@ The parent registers the child as a near-free lazy entry:
|
|
|
116
116
|
|
|
117
117
|
- Export a plain `Routes` array, or omit `componentDeps` on a routed component — the route's DI becomes
|
|
118
118
|
unchecked.
|
|
119
|
-
- Ship a
|
|
120
|
-
`loadChildren` child — it does not.
|
|
121
|
-
- "Fix" a `TS2589` by deleting the check or casting to `any` — that hides every DI error in the file.
|
|
122
|
-
Split with `loadChildren` instead.
|
|
119
|
+
- Ship a routed component without its own check, or assume a parent's check covers a `loadChildren` child.
|
|
123
120
|
- Hand-edit the generated `_Check*` / `_CanRun*` blocks or `GenDeps_*` aliases — run ESLint `--fix`.
|
|
124
121
|
|
|
125
122
|
## References
|
|
126
123
|
|
|
127
|
-
- [references/di-checks.md](references/di-checks.md) — the per-
|
|
128
|
-
|
|
129
|
-
- [references/scaling-and-pitfalls.md](references/scaling-and-pitfalls.md) — the
|
|
130
|
-
|
|
131
|
-
tempting but don't work (so you don't re-derive them).
|
|
124
|
+
- [references/di-checks.md](references/di-checks.md) — the per-route check, parent DI context, and
|
|
125
|
+
`GenDeps_*` regeneration.
|
|
126
|
+
- [references/scaling-and-pitfalls.md](references/scaling-and-pitfalls.md) — the loadChildren tree,
|
|
127
|
+
`.withParent` + `assertChildRouteMounts`, and route ownership pitfalls.
|
|
132
128
|
- [references/pending-and-exceptions.md](references/pending-and-exceptions.md) — `pendingComponent` DI
|
|
133
129
|
verification, `viewTransitionPayload`, `handleExceptions`, and exhaustiveness.
|
|
134
130
|
- [references/eslint-workflow.md](references/eslint-workflow.md) — the ESLint rules that keep all of the
|
|
@@ -1,101 +1,55 @@
|
|
|
1
|
-
# Per-
|
|
1
|
+
# Per-route DI checks
|
|
2
2
|
|
|
3
3
|
## What the check does
|
|
4
4
|
|
|
5
|
-
`
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
(params, `data`, `queryParams`, guarded/resolved data), the route `providers`, and the **parent context**
|
|
9
|
-
you pass in. Any gap becomes a TypeScript error on `_CanRun*`:
|
|
10
|
-
|
|
11
|
-
```
|
|
12
|
-
Injected SomeService is not provided in path: "some/path"
|
|
13
|
-
Input "userId" is not provided in path: "some/path"
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
`CanRun<Check>` is what turns the check result into a hard compile error — always pair them:
|
|
5
|
+
`RouteCheckedDI<ComponentDeps, ParentNames, ParentValues, Context, RouteInputs>` compares one
|
|
6
|
+
routed component's dependencies against the providers available at its mount point, the route, and
|
|
7
|
+
the component itself. Any gap becomes a TypeScript error when the result is consumed by `CanRun`:
|
|
17
8
|
|
|
18
9
|
```ts
|
|
19
|
-
type _CheckXDI =
|
|
10
|
+
type _CheckXDI = RouteCheckedDI<
|
|
11
|
+
import('./detail').GenDeps_DetailComponent,
|
|
12
|
+
AppProvidedNames,
|
|
13
|
+
AppProvidedValues,
|
|
14
|
+
'detail/:id',
|
|
15
|
+
'id'
|
|
16
|
+
>;
|
|
20
17
|
type _CanRunX = CanRun<_CheckXDI>;
|
|
21
18
|
```
|
|
22
19
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
The check reads only the **current** collection's metadata. It does **not** descend into `loadChildren`.
|
|
26
|
-
So a lazy child loaded from another file is invisible to the parent's check — that child file must
|
|
27
|
-
declare its own, or its components are never verified. This is the single most common way DI safety
|
|
28
|
-
silently disappears.
|
|
20
|
+
Typical errors are:
|
|
29
21
|
|
|
30
22
|
```
|
|
31
|
-
|
|
32
|
-
|
|
23
|
+
Injected SomeService is not provided in path: "detail/:id"
|
|
24
|
+
Input "id" is not provided in path: "detail/:id"
|
|
33
25
|
```
|
|
34
26
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
`ParentNames` / `ParentValues` describe everything provided **at the collection's mount point** — app
|
|
38
|
-
providers **plus** every ancestor route's `providers`.
|
|
39
|
-
|
|
40
|
-
- **App-level / no ancestor providers** (the common case): `<never, Router>`. No extra named providers;
|
|
41
|
-
`Router` is provided by value via `provideCraftRouter` / `provideRouter`. This pair is identical in
|
|
42
|
-
every file mounted directly under the app.
|
|
43
|
-
|
|
44
|
-
```ts
|
|
45
|
-
type _Check = ValidateCascadeRoutesFile<never, Router, typeof featureRoutes>;
|
|
46
|
-
```
|
|
27
|
+
The `CanRun` alias is what turns the result into a hard compile error. Keep both aliases next to the
|
|
28
|
+
route metadata.
|
|
47
29
|
|
|
48
|
-
|
|
49
|
-
context next to the route that adds it, and union your own onto it:
|
|
30
|
+
## One check per routed component
|
|
50
31
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
export type BillingChildValues = AppProvidedValues;
|
|
32
|
+
`RouteCheckedDI` does not recurse through sibling routes or lazy route collections. Every routed
|
|
33
|
+
component, including components in a `loadChildren` child file, therefore needs its own check. This
|
|
34
|
+
per-route shape keeps the cost stable as a route file grows and makes the failing route easy to find.
|
|
55
35
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
```
|
|
36
|
+
The architecture assertion `assertRouteDiProofs` catches a missing or unarmed check in CI. TypeScript
|
|
37
|
+
still validates the dependency semantics; the architecture suite validates that the check was invoked.
|
|
59
38
|
|
|
60
|
-
|
|
61
|
-
missing provider can slip through, or a provided service gets flagged as missing. Keep the re-export
|
|
62
|
-
beside the route that adds the providers.
|
|
63
|
-
|
|
64
|
-
> Note: deriving the names automatically (e.g. `AppProvidedServiceNamesOf<typeof appConfig>`) can itself
|
|
65
|
-
> hit TS2589 when app providers are complex (function wrappers, monitoring, …). Listing the value types
|
|
66
|
-
> explicitly (`<never, Router, …>`) is the reliable workaround.
|
|
67
|
-
|
|
68
|
-
## The `RouteCheckedDI` escape hatch (O(1) per route)
|
|
69
|
-
|
|
70
|
-
When a single file genuinely must hold a large **flat** list with no natural `loadChildren` boundary,
|
|
71
|
-
switch from the aggregated cascade to the per-route check. It validates one component at a time with **no
|
|
72
|
-
recursion between routes**, so it never hits the depth ceiling and scales to thousands of routes — at the
|
|
73
|
-
cost of one block per component instead of one per file:
|
|
74
|
-
|
|
75
|
-
```ts
|
|
76
|
-
import { type CanRun, type RouteCheckedDI } from '@craft-ts/core';
|
|
77
|
-
|
|
78
|
-
type _CheckItem0 = RouteCheckedDI<
|
|
79
|
-
import('./item-0').GenDeps_Item0Component,
|
|
80
|
-
AppProvidedNames, // available provider names (parent context)
|
|
81
|
-
AppProvidedValues, // provided value types
|
|
82
|
-
'Item0Component' // label shown in the error
|
|
83
|
-
>;
|
|
84
|
-
type _CanRunItem0 = CanRun<_CheckItem0>;
|
|
85
|
-
// …one pair per route component
|
|
86
|
-
```
|
|
39
|
+
## Threading the parent DI context
|
|
87
40
|
|
|
88
|
-
|
|
89
|
-
|
|
41
|
+
`ParentNames` / `ParentValues` describe everything provided at the route's mount point — app providers
|
|
42
|
+
plus ancestor route providers.
|
|
90
43
|
|
|
91
|
-
|
|
44
|
+
- **App-level / no ancestor providers:** use the app's named provider union and value type.
|
|
45
|
+
- **Mounted under a route that adds providers:** re-export the ancestor's cumulative context next to
|
|
46
|
+
the route that adds them and pass it to the child route checks.
|
|
92
47
|
|
|
93
|
-
|
|
94
|
-
|
|
48
|
+
Forgetting an ancestor provider can either hide a missing-provider error or report a provided service
|
|
49
|
+
as missing, so keep the context definition beside the provider boundary.
|
|
95
50
|
|
|
96
|
-
|
|
97
|
-
- changing component `imports`, `providers`, or `viewProviders`
|
|
51
|
+
## Pending and error components
|
|
98
52
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
53
|
+
Pending components and error-component descriptors are not the routed target. Verify them independently
|
|
54
|
+
with `RouteCheckedDI` or `RouteExceptionComponentCheckedDI`, then consume each result with `CanRun`.
|
|
55
|
+
The ESLint rules for pending and exception components maintain those independent blocks.
|
|
@@ -1,52 +1,37 @@
|
|
|
1
|
-
# ESLint workflow —
|
|
1
|
+
# ESLint workflow — keep route checks in sync
|
|
2
2
|
|
|
3
|
-
The route
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
touching routes or a component's DI shape, run `--fix` and trust the rules to regenerate the bookkeeping.
|
|
3
|
+
The route checks, dependency metadata, exception asserts and their imports are maintained by the
|
|
4
|
+
project's ESLint rules. An ESLint error is not a TypeScript compile error, so run ESLint after editing
|
|
5
|
+
routes or a component's dependency shape.
|
|
7
6
|
|
|
8
|
-
##
|
|
7
|
+
## Routing-relevant rules
|
|
9
8
|
|
|
10
|
-
Enable these in the flat ESLint config (plugin exposed as `@craft-ts/dev-tools/eslint-rules
|
|
11
|
-
under the `craft-ts/` namespace):
|
|
9
|
+
Enable these in the flat ESLint config (plugin exposed as `@craft-ts/dev-tools/eslint-rules`):
|
|
12
10
|
|
|
13
11
|
| Rule | Keeps in sync |
|
|
14
12
|
| --- | --- |
|
|
15
|
-
| `
|
|
16
|
-
| `brand-angular-deps-match` | refreshes an existing `GenDeps_*` when the component's DI changes |
|
|
17
|
-
| `require-assert-exhaustive-route-exceptions` | adds `assertExhaustiveRouteExceptions(xRoutes)` (+ import) per collection |
|
|
13
|
+
| `require-assert-exhaustive-route-exceptions` | adds `assertExhaustiveRouteExceptions(xRoutes)` per collection |
|
|
18
14
|
| `require-pending-component-di-check` | generates/refreshes the `RouteCheckedDI` block for a `pendingComponent` |
|
|
19
|
-
| `require-
|
|
15
|
+
| `require-exception-component-di-check` | generates/refreshes `RouteExceptionComponentCheckedDI` blocks |
|
|
16
|
+
| `require-child-route-mount-check` | adds `assertChildRouteMounts(xRoutes)` for collections mounting lazy `loadChildren` |
|
|
20
17
|
| `global-exception-registry-match` | mirrors `globalError()` codes into `CraftGlobalExceptionRegistry` |
|
|
21
18
|
|
|
22
|
-
> The aggregated `ValidateCascadeRoutesFile<…>` check itself is **not** auto-generated (its parent context
|
|
23
|
-
> can't be guessed across files). Write that one block by hand per file — see di-checks.md — and let the
|
|
24
|
-
> rules above handle everything else.
|
|
25
|
-
|
|
26
19
|
## The loop
|
|
27
20
|
|
|
28
|
-
1. **
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
3. **Read the remaining TypeScript errors** — those are the real DI gaps (`Injected X is not provided…`,
|
|
34
|
-
`Input "y" is not provided…`). Fix by providing the service / adding the input / correcting the route.
|
|
21
|
+
1. **Edit a routed component or route** → keep its `RouteCheckedDI` / `CanRun` block beside the route.
|
|
22
|
+
2. **Run ESLint `--fix`** on the changed route file. Exception, pending-component and child-mount
|
|
23
|
+
bookkeeping is added or refreshed.
|
|
24
|
+
3. **Read the remaining TypeScript errors** — these are the real DI gaps (`Injected X is not provided…`,
|
|
25
|
+
`Input "y" is not provided…`).
|
|
35
26
|
|
|
36
27
|
```bash
|
|
37
|
-
# one file
|
|
38
28
|
eslint --fix src/app/feature/feature.routes.ts
|
|
39
|
-
# the component whose DI changed
|
|
40
|
-
eslint --fix src/app/feature/feature-detail.ts
|
|
41
29
|
```
|
|
42
30
|
|
|
43
31
|
## Gotchas
|
|
44
32
|
|
|
45
|
-
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
-
|
|
49
|
-
|
|
50
|
-
root).
|
|
51
|
-
- **Don't commit a green build with a red ESLint.** Because the ESLint error isn't a compile error, a route
|
|
52
|
-
can build fine while its check is stale — CI should run both.
|
|
33
|
+
- Checks are per-file and per-route; a parent file does not cover a component loaded through
|
|
34
|
+
`loadChildren`.
|
|
35
|
+
- Quick fixes are per-file. Run them on each route file you changed.
|
|
36
|
+
- Do not commit a green build with a red ESLint result: a missing or unarmed check can otherwise pass
|
|
37
|
+
TypeScript while the architecture test catches it only later.
|
|
@@ -15,17 +15,12 @@ route('user/:userId', {
|
|
|
15
15
|
}),
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
### Verifying the skeleton's DI
|
|
18
|
+
### Verifying the skeleton's DI
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
monitoring), so verify it **directly** with the per-component `RouteCheckedDI` (not a second aggregated
|
|
23
|
-
pass — that would add to the budget the cascade already spends):
|
|
20
|
+
The skeleton is a real component that injects things (route params, a payload, monitoring), so verify it
|
|
21
|
+
independently with the per-component `RouteCheckedDI`:
|
|
24
22
|
|
|
25
23
|
```ts
|
|
26
|
-
type _CheckTargetDI = ValidateCascadeRoutesFile<AppNames, AppValues, typeof userRoutes>;
|
|
27
|
-
type _CanRunTarget = CanRun<_CheckTargetDI>;
|
|
28
|
-
|
|
29
24
|
// The skeleton injects the route-auto-provided :userId param — list its service name as available.
|
|
30
25
|
type _CheckPendingDI = RouteCheckedDI<
|
|
31
26
|
import('./user-skeleton').GenDeps_UserSkeletonComponent,
|
|
@@ -38,8 +33,7 @@ type _CanRunPending = CanRun<_CheckPendingDI>;
|
|
|
38
33
|
|
|
39
34
|
The ESLint rule `require-pending-component-di-check` **generates and refreshes this whole block** on
|
|
40
35
|
`--fix` — deriving the skeleton's `GenDeps_*`, the auto-provided service names from the route's path params
|
|
41
|
-
(+ view-transition payload), and the parent context
|
|
42
|
-
Don't hand-maintain it.
|
|
36
|
+
(+ view-transition payload), and the parent context. Don't hand-maintain it.
|
|
43
37
|
|
|
44
38
|
## View transitions (shared-element morph across a slow chain)
|
|
45
39
|
|
|
@@ -1,111 +1,58 @@
|
|
|
1
|
-
# Scaling routes
|
|
1
|
+
# Scaling routes and avoiding type blow-ups
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`RouteCheckedDI` checks one routed component at a time. It does not recurse through sibling routes,
|
|
4
|
+
so the DI proof cost remains local to the component being checked.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
**deep** it will recurse and how many instantiations it will do in **total**, so a single collection has a
|
|
7
|
-
finite budget — in practice a few dozen routes, sooner if routes carry guards / `resolve` /
|
|
8
|
-
`handleExceptions` (each costs several times more than a trivial route).
|
|
6
|
+
## Use a route tree when ownership or loading requires it
|
|
9
7
|
|
|
10
|
-
|
|
8
|
+
Split features into child collections joined by `loadChildren` when that improves code-splitting or
|
|
9
|
+
ownership:
|
|
11
10
|
|
|
12
11
|
```
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
app.routes.ts
|
|
13
|
+
├── billing.routes.ts # each routed component has RouteCheckedDI + CanRun
|
|
14
|
+
├── admin.routes.ts # each routed component has RouteCheckedDI + CanRun
|
|
15
|
+
└── reporting.routes.ts
|
|
15
16
|
```
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
which poisons inference of neighbouring `const`s in the *same file*. The symptoms are misleading:
|
|
19
|
-
`route(...)` calls degrade to a bare builder type, the `craftRoutes(...)` helpers go missing
|
|
20
|
-
(`Property 'injectXxx' does not exist`), `craftRouterLink` targets type as `never`. The root cause is the
|
|
21
|
-
overflowing check — fix that, not the symptoms.
|
|
22
|
-
|
|
23
|
-
## The fix is architectural: a tree of `loadChildren`
|
|
24
|
-
|
|
25
|
-
Don't shrink the check or cast to `any`. Move routes into lazy child collections joined by `loadChildren`
|
|
26
|
-
(which you want anyway for code-splitting), each with **its own** check:
|
|
18
|
+
The child returns its named collection from `loadChildren`:
|
|
27
19
|
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
20
|
+
```ts
|
|
21
|
+
{
|
|
22
|
+
path: 'billing',
|
|
23
|
+
loadChildren: ({ withRetry }) =>
|
|
24
|
+
withRetry(import('./billing.routes')).then((m) => m.billingRoutes),
|
|
25
|
+
}
|
|
33
26
|
```
|
|
34
27
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
- Each feature file pays only for its own leaves. ~500 routes ÷ ~17 per file ≈ ~30 files; two levels are
|
|
38
|
-
plenty and you can nest further without limit.
|
|
39
|
-
- The takeaway: **DI is always verified — never drop the check; move it next to the routes it covers.**
|
|
28
|
+
A parent check does not cover components in the lazy child. Keep each child's route checks beside
|
|
29
|
+
the components they validate.
|
|
40
30
|
|
|
41
|
-
|
|
31
|
+
## Thread the parent DI context
|
|
42
32
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
```
|
|
33
|
+
`ParentNames` / `ParentValues` passed to `RouteCheckedDI` describe everything provided at the route's
|
|
34
|
+
mount point: app providers plus ancestor route providers. Re-export the cumulative context when an
|
|
35
|
+
ancestor adds route providers, then pass it to the child checks.
|
|
47
36
|
|
|
48
|
-
##
|
|
37
|
+
## Pin a child to its mount
|
|
49
38
|
|
|
50
|
-
A child whose components rely on a
|
|
51
|
-
|
|
39
|
+
A child whose components rely on a specific mount (a route param, a view-transition payload, or an
|
|
40
|
+
ancestor provider) can declare `.withParent<ParentRoutes<'path'>>()`. Enforce placement in the parent
|
|
41
|
+
with `assertChildRouteMounts(parentRoutes)`:
|
|
52
42
|
|
|
53
43
|
```ts
|
|
54
|
-
// view-transitions.routes.ts — the child declares where it belongs
|
|
55
|
-
import { craftRoutes, route, type ParentRoutes } from '@craft-ts/core';
|
|
56
|
-
|
|
57
44
|
export const { viewTransitionsRoutes } = craftRoutes('viewTransitions', [
|
|
58
|
-
|
|
45
|
+
craftRoute(':photoId', { /* … */ }),
|
|
59
46
|
]).withParent<ParentRoutes<'view-transitions'>>();
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
```ts
|
|
63
|
-
// app.routes.ts — the parent enforces placement (scoped to this file)
|
|
64
|
-
import { assertChildRouteMounts, craftRoutes } from '@craft-ts/core';
|
|
65
|
-
|
|
66
|
-
export const { demoRoutes } = craftRoutes('demo', [
|
|
67
|
-
{ path: 'view-transitions', loadChildren: () => import('./view-transitions.routes').then((m) => m.viewTransitionsRoutes) },
|
|
68
|
-
]);
|
|
69
47
|
|
|
70
48
|
assertChildRouteMounts(demoRoutes);
|
|
71
49
|
```
|
|
72
50
|
|
|
73
|
-
|
|
51
|
+
`assertChildRouteMounts` reads only the parent's own routes and does not re-validate the child.
|
|
74
52
|
|
|
75
|
-
|
|
76
|
-
craftRoutes(...).withParent<ParentRoutes<'view-transitions'>>() must be
|
|
77
|
-
loadChildren-mounted under the route with path 'view-transitions', not 'admin'
|
|
78
|
-
```
|
|
53
|
+
## Pitfalls
|
|
79
54
|
|
|
80
|
-
-
|
|
81
|
-
|
|
82
|
-
-
|
|
83
|
-
|
|
84
|
-
- **Type-only.** `.withParent<…>()` returns the same object at runtime; `ParentRoutes<'path'>` carries only
|
|
85
|
-
the path string, so it creates no runtime coupling.
|
|
86
|
-
- **ESLint** (`require-child-route-mount-check`) adds the missing `assertChildRouteMounts(...)` + import on
|
|
87
|
-
`--fix`. Whether a child opts in with `.withParent` stays your call.
|
|
88
|
-
|
|
89
|
-
## Pitfalls — approaches that look tempting but don't work
|
|
90
|
-
|
|
91
|
-
These were tried and rejected; recording them so they aren't re-derived.
|
|
92
|
-
|
|
93
|
-
- **Casting away a `TS2589`** (`as any`, deleting the check, `@ts-ignore`). It silences the error but
|
|
94
|
-
disables DI checking for the whole file — the opposite of the goal. Always split with `loadChildren`.
|
|
95
|
-
|
|
96
|
-
- **Enforcing child placement inside `craftRoutes(...)` itself** (weaving a mount check into the `routes`
|
|
97
|
-
argument type so a wrong mount errors at the literal). It type-checks, but the extra per-collection
|
|
98
|
-
instantiation tips already-at-ceiling files into `TS2589` — even a 2-route child with no `loadChildren`
|
|
99
|
-
pays the cost, because *every* collection runs the same inference. Placement checks must be **scoped to
|
|
100
|
-
the parent** that mounts children (a standalone `assertChildRouteMounts`), not folded into the hot
|
|
101
|
-
`craftRoutes` path every file pays.
|
|
102
|
-
|
|
103
|
-
- **A `loadChildrenType: {} as typeof import('./x').xRoutes` carrier** to skip inferring the child through
|
|
104
|
-
`import().then()`. In isolation it builds; applied broadly it **materialises the child's full type
|
|
105
|
-
(components included)**, creating a **circular reference** for any child whose components inject the
|
|
106
|
-
*parent's* route data (`TS2615` "circularly references itself" + `TS2589`). The dynamic-import resolution
|
|
107
|
-
it replaced is cycle-safe and, measured against build-time noise, no slower — so don't add such a carrier.
|
|
108
|
-
|
|
109
|
-
- **Auto-deriving the parent context** from the app config in a generic constraint
|
|
110
|
-
(`AppProvidedServiceNamesOf<typeof appConfig>`). It overflows when app providers are complex. List the
|
|
111
|
-
value types explicitly: `<never, Router, …>`.
|
|
55
|
+
- Do not cast away a DI error with `any` or `@ts-ignore`.
|
|
56
|
+
- Do not assume a parent route check covers a `loadChildren` child.
|
|
57
|
+
- Do not omit `CanRun`: the type alias is what consumes the check and turns a mismatch into a compile
|
|
58
|
+
error.
|
|
@@ -44,7 +44,7 @@ Use this skill when a task touches dependency injection, service architecture, o
|
|
|
44
44
|
|
|
45
45
|
6. Refresh generated artifacts:
|
|
46
46
|
- Run ESLint `--fix` for `brand-angular-gen-deps-required` and `brand-angular-deps-match`.
|
|
47
|
-
- Check route files still have `componentDeps` and `
|
|
47
|
+
- Check route files still have `componentDeps` and per-route `RouteCheckedDI` checks.
|
|
48
48
|
- If direct Angular tokens disappeared, remove them from `ProvidedValues`; prefer `never`.
|
|
49
49
|
|
|
50
50
|
## Route DI Rule
|
|
@@ -52,10 +52,11 @@ Use this skill when a task touches dependency injection, service architecture, o
|
|
|
52
52
|
For migrated features, prefer:
|
|
53
53
|
|
|
54
54
|
```ts
|
|
55
|
-
type _CheckFeatureDI =
|
|
55
|
+
type _CheckFeatureDI = RouteCheckedDI<
|
|
56
|
+
ComponentDeps,
|
|
56
57
|
AppProvidedNames,
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
AppProvidedValues,
|
|
59
|
+
'feature'
|
|
59
60
|
>;
|
|
60
61
|
type _CanRunFeature = CanRun<_CheckFeatureDI>;
|
|
61
62
|
```
|
|
@@ -113,8 +113,7 @@ cannot be inferred safely. Preserve unrelated user changes.
|
|
|
113
113
|
|
|
114
114
|
- Complete unresolved guards, redirects, lazy collections, inherited
|
|
115
115
|
providers, and dynamic route diagnostics manually.
|
|
116
|
-
- Keep `componentDeps` and
|
|
117
|
-
`RouteCheckedDI` checks.
|
|
116
|
+
- Keep `componentDeps` and per-route `RouteCheckedDI` checks.
|
|
118
117
|
- Prefer provider names and `ProvidedValues = never` after direct Angular DI is
|
|
119
118
|
removed.
|
|
120
119
|
- Run ESLint fixes to regenerate `GenDeps_*` aliases after dependency changes.
|