@craft-ts/mcp 0.8.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@craft-ts/mcp",
3
- "version": "0.8.1",
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.1",
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,cloud-code
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, see a "TS2589 excessively deep" error in a routes file, or need a per-file DI check (ValidateCascadeRoutesFile / RouteCheckedDI). Apply it even when the user only says "add a route", "split this routes 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.
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-file check turns any unmet dependency into a TypeScript error. The whole
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 — while staying under TypeScript's instantiation ceiling.
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 `craftRoutes(...) file carries its own DI check.`** Add, in the same file:
25
+ 2. **Every routed component carries its own DI check.** Add, in the route file:
26
26
  ```ts
27
- type _CheckXDI = ValidateCascadeRoutesFile<ParentNames, ParentValues, typeof xRoutes>;
27
+ type _CheckXDI = RouteCheckedDI<ComponentDeps, ParentNames, ParentValues, 'route'>;
28
28
  type _CanRunX = CanRun<_CheckXDI>;
29
29
  ```
30
- This is the *iron rule*: the check reads only the current collection and **does not descend into
31
- `loadChildren`**, so each file including lazy children must re-declare it or its components go
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. **Stay under the instantiation ceiling — split with `loadChildren`.** One collection has a finite
38
- route budget; past it TypeScript throws `TS2589: Type instantiation is excessively deep`, which then
39
- **collapses inference for the whole file** (helpers vanish, `route(...)` calls degrade). The fix is
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 ValidateCascadeRoutesFile,
61
+ type RouteCheckedDI,
66
62
  } from '@craft-ts/core';
67
63
  import type { Router } from '@angular/router';
68
64
 
69
- export const { featureRoutes, injectFeatureUserIdParams } = craftRoutes('feature', [
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 THIS collection — the parent's cascade does NOT cover loadChildren.
81
- type _CheckFeatureDI = ValidateCascadeRoutesFile<never, Router, typeof featureRoutes>;
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 routes file is getting large / hits TS2589** → don't fight the type; split a slice into a new
101
- `craftRoutes(...)` file mounted via `loadChildren`, and give that file its own check. Repeat as a tree.
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
- cascade never sees the skeleton, so verify its DI separately with a per-component `RouteCheckedDI`. For a
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** (no natural `loadChildren` boundary) switch from
110
- `ValidateCascadeRoutesFile` to the per-route `RouteCheckedDI` (no recursion between routes, scales to
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 `craftRoutes(...)` file without its own check, or assume the parent's check covers 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-file check in depth: `ValidateCascadeRoutesFile`,
128
- threading the parent DI context, the `RouteCheckedDI` escape hatch, and `GenDeps_*` regeneration.
129
- - [references/scaling-and-pitfalls.md](references/scaling-and-pitfalls.md) — the TS2589 ceiling, the
130
- loadChildren-tree architecture, `.withParent` + `assertChildRouteMounts`, and approaches that look
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-file DI checks
1
+ # Per-route DI checks
2
2
 
3
3
  ## What the check does
4
4
 
5
- `ValidateCascadeRoutesFile<ParentNames, ParentValues, typeof xRoutes>` walks every route in the
6
- collection at the type level and, for each routed component, compares the dependencies it injects
7
- (`componentDeps` its `GenDeps_*`) against what is provided the route's own auto-provided services
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 = ValidateCascadeRoutesFile<ParentNames, ParentValues, typeof xRoutes>;
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
- ## The iron rule (why every file needs its own)
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
- app.routes.ts → ValidateCascadeRoutesFile<…, typeof appRoutes> (checks its own leaves)
32
- └── loadChildren feature.routes.ts ValidateCascadeRoutesFile<…, typeof featureRoutes> (MUST have its own)
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
- ## Threading the parent DI context
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
- - **Mounted under a route that adds `providers: [provideBilling()]`**: re-export the ancestor's cumulative
49
- context next to the route that adds it, and union your own onto it:
30
+ ## One check per routed component
50
31
 
51
- ```ts
52
- // billing.routes.ts
53
- export type BillingChildNames = AppProvidedNames | 'BillingService';
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
- // sub-billing.routes.ts
57
- type _Check = ValidateCascadeRoutesFile<BillingChildNames, BillingChildValues, typeof subRoutes>;
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
- Forgetting to fold in an ancestor's provider makes the check wrong in **both** directions: a genuinely
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
- Prefer the `loadChildren` tree (it also code-splits); reach for `RouteCheckedDI` only when one big flat
89
- file is unavoidable — or to verify a component the cascade can't see, like a `pendingComponent`.
41
+ `ParentNames` / `ParentValues` describe everything provided at the route's mount point app providers
42
+ plus ancestor route providers.
90
43
 
91
- ## `GenDeps_*` keep it fresh
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
- `componentDeps: {} as import('./x').GenDeps_X` points at a generated alias describing the component's DI
94
- shape. It is **generated, not hand-written**. Regenerate it whenever the component's DI changes:
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
- - adding/removing `inject(...)` or constructor injection
97
- - changing component `imports`, `providers`, or `viewProviders`
51
+ ## Pending and error components
98
52
 
99
- Use the ESLint Quick Fix (`brand-angular-gen-deps-required` to create, `brand-angular-deps-match` to
100
- refresh) or the project's `craft:brand` codemod. A stale `GenDeps_*` makes the route check validate the
101
- wrong shape so after regenerating it, re-run ESLint `--fix` on the routes file.
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 — let the rules keep the checks in sync
1
+ # ESLint workflow — keep route checks in sync
2
2
 
3
- The route DI checks, the `GenDeps_*` aliases, the asserts and their imports are **generated and refreshed
4
- by ESLint**, not hand-written. This matters because an ESLint error is **not** a TypeScript compile error:
5
- a stale or missing check passes the build while silently hiding real DI bugs. So the discipline is: after
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
- ## The routing-relevant rules
7
+ ## Routing-relevant rules
9
8
 
10
- Enable these in the flat ESLint config (plugin exposed as `@craft-ts/dev-tools/eslint-rules`, registered
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
- | `brand-angular-gen-deps-required` | creates a missing `GenDeps_*` alias for a component/directive/pipe |
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-child-route-mount-check` | adds `assertChildRouteMounts(xRoutes)` (+ import) for collections mounting lazy `loadChildren` |
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. **Create/edit a routed component** → run the Quick Fix `brand-angular-gen-deps-required` (or
29
- `brand-angular-deps-match` if it already has a `GenDeps_*`), or the project's `craft:brand` codemod, to
30
- (re)generate the alias.
31
- 2. **Edit the routes file** run ESLint `--fix` on it. The asserts, the pending-component `RouteCheckedDI`
32
- block, the child-mount assert and their imports are added/refreshed.
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
- - **Quick Fix is per-file.** It won't cascade to other files; run it on each file you changed.
46
- - **Rename a component class → rerun the generator** so the `GenDeps_*` alias name stays aligned with the
47
- class name.
48
- - **Flat config resolves rules from the CWD.** If `--fix` seems to do nothing, run it from the directory
49
- whose ESLint config actually enables the `craft-ts/*` rules (often the app project root, not the repo
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 (the cascade can't see it)
18
+ ### Verifying the skeleton's DI
19
19
 
20
- `ValidateCascadeRoutesFile` only checks the **target** component it never descends into
21
- `pendingComponent`. A skeleton is a real component that injects things (route params, a payload,
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 from the collection's own `ValidateCascadeRoutesFile`.
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 & the instantiation ceiling
1
+ # Scaling routes and avoiding type blow-ups
2
2
 
3
- ## The ceiling, and why it's loud
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
- `ValidateCascadeRoutesFile` instantiates a recursive type over the route tuple. TypeScript caps both how
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
- Past the budget:
8
+ Split features into child collections joined by `loadChildren` when that improves code-splitting or
9
+ ownership:
11
10
 
12
11
  ```
13
- TS2589: Type instantiation is excessively deep and possibly infinite.
14
- app.routes.ts ValidateCascadeRoutesFile<never, Router, typeof appRoutes>
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
- **Watch the knock-on collapse.** A `TS2589` makes TypeScript abandon that type and fall back to `any`,
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
- app.routes.ts # "manifest": ~N cheap { path, loadChildren } entries
30
- ├── billing.routes.ts # ~15–20 leaf routes + its own ValidateCascadeRoutesFile
31
- ├── admin.routes.ts # ~15–20 leaf routes + its own check
32
- └── reporting.routes.ts # if itself large → re-split into sub-loadChildren (level 3+)
20
+ ```ts
21
+ {
22
+ path: 'billing',
23
+ loadChildren: ({ withRetry }) =>
24
+ withRetry(import('./billing.routes')).then((m) => m.billingRoutes),
25
+ }
33
26
  ```
34
27
 
35
- - A `{ path, loadChildren }` entry has no `componentDeps`, so it is **nearly free** in the parent's check
36
- the manifest can list dozens.
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
- The child returns its named collection from `loadChildren`:
31
+ ## Thread the parent DI context
42
32
 
43
- ```ts
44
- // app.routes.ts
45
- { path: 'billing', loadChildren: () => import('./billing.routes').then((m) => m.billingRoutes) },
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
- ## Pinning a child to its mount — `.withParent` + `assertChildRouteMounts`
37
+ ## Pin a child to its mount
49
38
 
50
- A child whose components rely on a *specific* mount (its `:param`, a view-transition payload, an ancestor's
51
- providers) is only correct under that one path. Nothing enforces that by default. Pin it:
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
- route(':photoId', { /* … */ }),
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
- Mount the pinned collection elsewhere and the **parent file** fails to compile:
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
- - **Opt-in.** A collection without `.withParent` is unpinned and mountable anywhere (backward compatible).
81
- Pin only the children whose placement actually matters.
82
- - **Scoped to the parent.** `assertChildRouteMounts` reads the parent's own routes it doesn't re-validate
83
- the child, so it adds nothing to the child's budget.
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 `ValidateCascadeRoutesFile` or `RouteCheckedDI`.
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 = ValidateCascadeRoutesFile<
55
+ type _CheckFeatureDI = RouteCheckedDI<
56
+ ComponentDeps,
56
57
  AppProvidedNames,
57
- never,
58
- typeof featureRoutes
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 file-level `ValidateCascadeRoutesFile` or
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.