@craft-ts/mcp 0.7.0-beta.13

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.
Files changed (33) hide show
  1. package/README.md +71 -0
  2. package/content/agents.md +35 -0
  3. package/content/best-practices.md +79 -0
  4. package/content/docs-index.json +658 -0
  5. package/dist/catalog.d.ts +27 -0
  6. package/dist/catalog.js +67 -0
  7. package/dist/catalog.js.map +1 -0
  8. package/dist/main.d.ts +2 -0
  9. package/dist/main.js +13 -0
  10. package/dist/main.js.map +1 -0
  11. package/dist/mcp-server.d.ts +3 -0
  12. package/dist/mcp-server.js +144 -0
  13. package/dist/mcp-server.js.map +1 -0
  14. package/dist/resources.d.ts +10 -0
  15. package/dist/resources.js +63 -0
  16. package/dist/resources.js.map +1 -0
  17. package/mcp.json +10 -0
  18. package/package.json +62 -0
  19. package/plugin.json +19 -0
  20. package/skills/craft-ts/SKILL.md +43 -0
  21. package/skills/craft-ts-architecture-tests/SKILL.md +110 -0
  22. package/skills/craft-ts-effect-v4/SKILL.md +31 -0
  23. package/skills/craft-ts-routes/SKILL.md +135 -0
  24. package/skills/craft-ts-routes/references/di-checks.md +101 -0
  25. package/skills/craft-ts-routes/references/eslint-workflow.md +52 -0
  26. package/skills/craft-ts-routes/references/pending-and-exceptions.md +93 -0
  27. package/skills/craft-ts-routes/references/scaling-and-pitfalls.md +111 -0
  28. package/skills/craft-ts-service-migration/SKILL.md +86 -0
  29. package/skills/migrate-to-craft-ts/SKILL.md +135 -0
  30. package/skills/translate-spec-to-craft-ts/SKILL.md +86 -0
  31. package/skills/translate-spec-to-craft-ts/references/lexical-map.md +322 -0
  32. package/skills/translate-spec-to-craft-ts/references/pattern-recipes.md +123 -0
  33. package/skills/translate-spec-to-craft-ts/references/project-index.md +52 -0
@@ -0,0 +1,135 @@
1
+ ---
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.
4
+ ---
5
+
6
+ # Creating craft-ts routes
7
+
8
+ ## Objective
9
+
10
+ Produce route files that keep **compile-time dependency-injection safety** intact. In craft-ts a route
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
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.
15
+
16
+ If the public API names here don't match the installed version, confirm against the app's own routes and
17
+ `node_modules/@craft-ts/core`; prefer the patterns already used in the repo.
18
+
19
+ ## The rules you must not skip
20
+
21
+ 1. **Wrap routes in `craftRoutes(name, [...])`** — never export a plain Angular `Routes` array. Give every
22
+ component route a `componentDeps: {} as import('./x').GenDeps_X` line. That line is the wire between
23
+ the component's generated dependency type and the route check; without it the route's DI is invisible.
24
+
25
+ 2. **Every `craftRoutes(...) file carries its own DI check.`** Add, in the same file:
26
+ ```ts
27
+ type _CheckXDI = ValidateCascadeRoutesFile<ParentNames, ParentValues, typeof xRoutes>;
28
+ type _CanRunX = CanRun<_CheckXDI>;
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.
34
+ That helper belongs in the app's `architecture/` suite — load `craft-ts-architecture-tests` to
35
+ scaffold or keep it armed. Do not add an architecture rule for the feature.
36
+
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).
42
+
43
+ 4. **Keep exception handling exhaustive.** A route whose `canActivate` / `canMatch` / `resolve` can throw
44
+ a typed `craftException` must handle exactly those codes. Use the 3-arg `route(path, def, handlers)`
45
+ form, or assert the whole collection with `assertExhaustiveRouteExceptions(xRoutes)`.
46
+
47
+ 5. **Let ESLint do the bookkeeping — don't hand-maintain the checks.** The check blocks, the `GenDeps_*`
48
+ aliases, the asserts and imports are all generated/refreshed by ESLint `--fix`. Run it after editing
49
+ routes or a component's DI shape rather than editing those blocks by hand (an ESLint error is *not* a
50
+ compile error, so a stale check silently hides real DI bugs). See
51
+ [references/eslint-workflow.md](references/eslint-workflow.md).
52
+
53
+ ## Anatomy of a correct routes file
54
+
55
+ Use this as the template for a new collection. The app-level parent context is `<never, Router>` (no
56
+ extra named providers; `Router` is provided by value). For a child mounted under a route that adds
57
+ `providers`, thread the cumulative context instead — see the DI-checks reference.
58
+
59
+ ```ts
60
+ import {
61
+ craftRoutes,
62
+ route,
63
+ assertExhaustiveRouteExceptions,
64
+ type CanRun,
65
+ type ValidateCascadeRoutesFile,
66
+ } from '@craft-ts/core';
67
+ import type { Router } from '@angular/router';
68
+
69
+ export const { featureRoutes, injectFeatureUserIdParams } = craftRoutes('feature', [
70
+ route(':userId', {
71
+ componentDeps: {} as import('./user-detail').GenDeps_UserDetailComponent,
72
+ loadComponent: () => import('./user-detail'),
73
+ // guards / resolve / providers / queryParams as needed
74
+ }),
75
+ ]);
76
+
77
+ // Exhaustive over canActivate ∪ canMatch ∪ resolve for the whole collection.
78
+ assertExhaustiveRouteExceptions(featureRoutes);
79
+
80
+ // DI safety for THIS collection — the parent's cascade does NOT cover loadChildren.
81
+ type _CheckFeatureDI = ValidateCascadeRoutesFile<never, Router, typeof featureRoutes>;
82
+ type _CanRunFeature = CanRun<_CheckFeatureDI>;
83
+ ```
84
+
85
+ The parent registers the child as a near-free lazy entry:
86
+
87
+ ```ts
88
+ // app.routes.ts
89
+ {
90
+ path: 'feature',
91
+ loadChildren: () => import('./feature.routes').then((m) => m.featureRoutes),
92
+ },
93
+ ```
94
+
95
+ ## Decision rules
96
+
97
+ - **Adding a component route** → add `loadComponent` (or `component`) **and** `componentDeps:
98
+ {} as import('./x').GenDeps_X`. If `GenDeps_X` doesn't exist yet, generate it (ESLint Quick Fix
99
+ `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.
102
+ - **A lazy child only makes sense under one specific path** (it relies on that route's params, payload, or
103
+ providers) → pin it with `.withParent<ParentRoutes<'path'>>()` and enforce placement in the parent with
104
+ `assertChildRouteMounts(parentRoutes)`. See the scaling reference.
105
+ - **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
+ shared-element morph, declare the payload with `viewTransitionPayload<T>()`. See
108
+ [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.
112
+ - **You changed a component's `inject`/`imports`/`providers`** → regenerate its `GenDeps_*` (Quick Fix /
113
+ `craft:brand`) so the route check sees the new shape, then re-run ESLint `--fix` on the routes file.
114
+
115
+ ## Never do this
116
+
117
+ - Export a plain `Routes` array, or omit `componentDeps` on a routed component — the route's DI becomes
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.
123
+ - Hand-edit the generated `_Check*` / `_CanRun*` blocks or `GenDeps_*` aliases — run ESLint `--fix`.
124
+
125
+ ## References
126
+
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).
132
+ - [references/pending-and-exceptions.md](references/pending-and-exceptions.md) — `pendingComponent` DI
133
+ verification, `viewTransitionPayload`, `handleExceptions`, and exhaustiveness.
134
+ - [references/eslint-workflow.md](references/eslint-workflow.md) — the ESLint rules that keep all of the
135
+ above in sync, and the `--fix` workflow.
@@ -0,0 +1,101 @@
1
+ # Per-file DI checks
2
+
3
+ ## What the check does
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:
17
+
18
+ ```ts
19
+ type _CheckXDI = ValidateCascadeRoutesFile<ParentNames, ParentValues, typeof xRoutes>;
20
+ type _CanRunX = CanRun<_CheckXDI>;
21
+ ```
22
+
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.
29
+
30
+ ```
31
+ app.routes.ts → ValidateCascadeRoutesFile<…, typeof appRoutes> (checks its own leaves)
32
+ └── loadChildren → feature.routes.ts → ValidateCascadeRoutesFile<…, typeof featureRoutes> (MUST have its own)
33
+ ```
34
+
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
+ ```
47
+
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:
50
+
51
+ ```ts
52
+ // billing.routes.ts
53
+ export type BillingChildNames = AppProvidedNames | 'BillingService';
54
+ export type BillingChildValues = AppProvidedValues;
55
+
56
+ // sub-billing.routes.ts
57
+ type _Check = ValidateCascadeRoutesFile<BillingChildNames, BillingChildValues, typeof subRoutes>;
58
+ ```
59
+
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
+ ```
87
+
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`.
90
+
91
+ ## `GenDeps_*` — keep it fresh
92
+
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:
95
+
96
+ - adding/removing `inject(...)` or constructor injection
97
+ - changing component `imports`, `providers`, or `viewProviders`
98
+
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.
@@ -0,0 +1,52 @@
1
+ # ESLint workflow — let the rules keep the checks in sync
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.
7
+
8
+ ## The routing-relevant rules
9
+
10
+ Enable these in the flat ESLint config (plugin exposed as `@craft-ts/dev-tools/eslint-rules`, registered
11
+ under the `craft-ts/` namespace):
12
+
13
+ | Rule | Keeps in sync |
14
+ | --- | --- |
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 |
18
+ | `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` |
20
+ | `global-exception-registry-match` | mirrors `globalError()` codes into `CraftGlobalExceptionRegistry` |
21
+
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
+ ## The loop
27
+
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.
35
+
36
+ ```bash
37
+ # one file
38
+ eslint --fix src/app/feature/feature.routes.ts
39
+ # the component whose DI changed
40
+ eslint --fix src/app/feature/feature-detail.ts
41
+ ```
42
+
43
+ ## Gotchas
44
+
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.
@@ -0,0 +1,93 @@
1
+ # Pending UI, view transitions & exhaustive exceptions
2
+
3
+ ## Non-blocking navigation + pending component
4
+
5
+ With `<craft-router-outlet>` (wired via `provideCraftRouter(...)`) navigation is non-blocking: the URL
6
+ commits immediately, a `pendingComponent` (skeleton) shows while a slow guard/resolve chain is in flight,
7
+ and the target mounts only on success. Add one per route as a craft-only field:
8
+
9
+ ```ts
10
+ route('user/:userId', {
11
+ componentDeps: {} as import('./user').GenDeps_UserComponent,
12
+ loadComponent: () => import('./user'),
13
+ pendingComponent: () => import('./user-skeleton'),
14
+ canActivate: craftCanActivate(/* slow guard */),
15
+ }),
16
+ ```
17
+
18
+ ### Verifying the skeleton's DI (the cascade can't see it)
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):
24
+
25
+ ```ts
26
+ type _CheckTargetDI = ValidateCascadeRoutesFile<AppNames, AppValues, typeof userRoutes>;
27
+ type _CanRunTarget = CanRun<_CheckTargetDI>;
28
+
29
+ // The skeleton injects the route-auto-provided :userId param — list its service name as available.
30
+ type _CheckPendingDI = RouteCheckedDI<
31
+ import('./user-skeleton').GenDeps_UserSkeletonComponent,
32
+ 'UserUserIdParams', // route-auto-provided names the skeleton may inject
33
+ AppValues, // same parent context the cascade uses
34
+ 'pending component: user/:userId'
35
+ >;
36
+ type _CanRunPending = CanRun<_CheckPendingDI>;
37
+ ```
38
+
39
+ The ESLint rule `require-pending-component-di-check` **generates and refreshes this whole block** on
40
+ `--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.
43
+
44
+ ## View transitions (shared-element morph across a slow chain)
45
+
46
+ For a shared-element morph to survive a slow navigation, the route **declares the payload shape** with
47
+ `viewTransitionPayload<T>()` — the view-transition analogue of how `queryParams` declares query-params shape:
48
+
49
+ ```ts
50
+ export const { photosRoutes, injectPhotosPhotoIdViewTransition } = craftRoutes('photos', [
51
+ route(':photoId', {
52
+ componentDeps: {} as import('./photo-detail').GenDeps_PhotoDetailComponent,
53
+ loadComponent: () => import('./photo-detail'),
54
+ withLoaderViewTransitionImage: viewTransitionPayload<{ name: string; image: string | null }>(),
55
+ pendingComponent: () => import('./photo-skeleton'),
56
+ }),
57
+ ]).withParent<ParentRoutes<'photos'>>();
58
+ ```
59
+
60
+ This makes a typed `viewTransition: T | null` **required** on every `craftRouterLink` / `navigate` targeting
61
+ the route, and exposes a route-generated `injectPhotosPhotoIdViewTransition(): Signal<T | null>` helper the
62
+ skeleton/target reads to wear the matching `view-transition-name`. Use `withCraftViewTransitions()` (not
63
+ Angular's `withViewTransitions()`) so the morph is driven by the outlet's own swaps. The payload travels in
64
+ navigation `state`, so it is lost on reload/direct URL — the app stays functional without the preview image.
65
+
66
+ ## Exhaustive exception handling
67
+
68
+ A route whose `canActivate` / `canMatch` / `resolve` can short-circuit with a typed `craftException({ code })`
69
+ must handle exactly those codes — no missing, no extra. Two equivalent ways:
70
+
71
+ - **3-arg `route(path, def, handlers)`** — enforces exhaustiveness at the call site:
72
+
73
+ ```ts
74
+ route(':photoId', {
75
+ canActivate: craftCanActivate(/* may craftException({ code: 'DENIED' }) */),
76
+ // …
77
+ }, {
78
+ DENIED: ({ redirect }) => redirect('/photos'),
79
+ }),
80
+ ```
81
+
82
+ - **`handleExceptions` field + a collection-level assert** as a safety net for 2-arg routes:
83
+
84
+ ```ts
85
+ assertExhaustiveRouteExceptions(photosRoutes);
86
+ ```
87
+
88
+ A route that can throw but was written with the 2-arg form surfaces its unhandled codes here. The ESLint
89
+ rule `require-assert-exhaustive-route-exceptions` adds this assert (+ import) on `--fix`.
90
+
91
+ Handlers receive helpers like `redirect('/path')` and `globalError()` (delegates to the global error
92
+ component). Codes routed to `globalError()` are mirrored in the `CraftGlobalExceptionRegistry` augmentation
93
+ by the `global-exception-registry-match` ESLint autofix — don't edit that by hand either.
@@ -0,0 +1,111 @@
1
+ # Scaling routes & the instantiation ceiling
2
+
3
+ ## The ceiling, and why it's loud
4
+
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).
9
+
10
+ Past the budget:
11
+
12
+ ```
13
+ TS2589: Type instantiation is excessively deep and possibly infinite.
14
+ app.routes.ts → ValidateCascadeRoutesFile<never, Router, typeof appRoutes>
15
+ ```
16
+
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:
27
+
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+)
33
+ ```
34
+
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.**
40
+
41
+ The child returns its named collection from `loadChildren`:
42
+
43
+ ```ts
44
+ // app.routes.ts
45
+ { path: 'billing', loadChildren: () => import('./billing.routes').then((m) => m.billingRoutes) },
46
+ ```
47
+
48
+ ## Pinning a child to its mount — `.withParent` + `assertChildRouteMounts`
49
+
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:
52
+
53
+ ```ts
54
+ // view-transitions.routes.ts — the child declares where it belongs
55
+ import { craftRoutes, route, type ParentRoutes } from '@craft-ts/core';
56
+
57
+ export const { viewTransitionsRoutes } = craftRoutes('viewTransitions', [
58
+ route(':photoId', { /* … */ }),
59
+ ]).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
+
70
+ assertChildRouteMounts(demoRoutes);
71
+ ```
72
+
73
+ Mount the pinned collection elsewhere and the **parent file** fails to compile:
74
+
75
+ ```
76
+ craftRoutes(...).withParent<ParentRoutes<'view-transitions'>>() must be
77
+ loadChildren-mounted under the route with path 'view-transitions', not 'admin'
78
+ ```
79
+
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, …>`.
@@ -0,0 +1,86 @@
1
+ ---
2
+ name: craft-ts-service-migration
3
+ description: Guides migration of existing Angular services and direct Angular DI toward @craft-ts/core craftService/toCraftService patterns. Use when editing or reviewing Angular services, @Injectable classes, inject(...), InjectionToken adapters, HttpClient/Router/Dialog usage, service tests, or route DI checks affected by service migration.
4
+ ---
5
+
6
+ # craft-ts Service Migration
7
+
8
+ Use this skill when a task touches dependency injection, service architecture, or migration from an existing Angular codebase to `@craft-ts/core`.
9
+
10
+ ## Target Architecture
11
+
12
+ - Application/domain services are authored with `craftService(...)`.
13
+ - Existing Angular tokens, framework services, third-party services, and `InjectionToken`s are adapted once with `toCraftService(...)`.
14
+ - Components, route guards, resolvers, primitives, and other craft services consume the generated `X()` helper, not Angular `inject(Token)`.
15
+ - Direct Angular DI is treated as legacy at the app boundary. Do not add new direct `inject(...)` calls.
16
+ - Route DI checks should rely on craft service provider names. Keep `ProvidedValues = never` unless supporting legacy direct Angular tokens is explicitly required.
17
+
18
+ ## Migration Workflow
19
+
20
+ 1. Inventory the current dependency:
21
+ - Find `@Injectable`, `inject(...)`, constructor injection, `InjectionToken`, `HttpClient`, `Router`, `Dialog`, `Title`, and app-specific API services.
22
+ - Classify each dependency as authored business logic, external Angular/framework token, or third-party adapter.
23
+
24
+ 2. Choose the right primitive:
25
+ - Business/domain logic -> `craftService({ name, scope }, function* () { ... })`.
26
+ - Existing Angular/third-party token -> `toCraftService({ name, scope, token })`.
27
+ - Existing token requiring `inject(...)` internally -> `toCraftService({ name, scope: 'global', inject: () => inject(TOKEN) })`.
28
+ - Dependencies that must be provided per feature/route/test -> `scope: 'toProvide'` or `scope: 'manuallyProvidedAtRoot'`.
29
+
30
+ 3. Preserve the public interface deliberately:
31
+ - Expose the smallest useful surface from adapters.
32
+ - Prefer yielding a narrowed method/property map: `yield* Router(undefined, ({ navigateByUrl }) => ({ navigateByUrl }))`.
33
+ - Avoid pass-through wrappers that expose the whole Angular service unless the service is intentionally an adapter.
34
+
35
+ 4. Replace consumption sites:
36
+ - In components/pages and craft service generators: use the generated `X()` helper, typically as `yield* X(...)`.
37
+ - In guards/resolvers/route helpers: use craft helpers and keep thrown exceptions typed.
38
+ - Remove direct Angular `inject(...)` and constructor injection from migrated code.
39
+
40
+ 5. Update DI registration:
41
+ - Add `provideX(...)` where the selected scope requires it.
42
+ - Keep route-level providers close to the route that owns the instance.
43
+ - Re-export cumulative route provider names only when child route files need them.
44
+
45
+ 6. Refresh generated artifacts:
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`.
48
+ - If direct Angular tokens disappeared, remove them from `ProvidedValues`; prefer `never`.
49
+
50
+ ## Route DI Rule
51
+
52
+ For migrated features, prefer:
53
+
54
+ ```ts
55
+ type _CheckFeatureDI = ValidateCascadeRoutesFile<
56
+ AppProvidedNames,
57
+ never,
58
+ typeof featureRoutes
59
+ >;
60
+ type _CanRunFeature = CanRun<_CheckFeatureDI>;
61
+ ```
62
+
63
+ Use `ProvidedValues` only as a temporary legacy bridge for direct Angular tokens still injected by components. Treat unions like `Router | Dialog | Title | typeof SomeApi` in route files as migration debt unless the user explicitly wants legacy support.
64
+
65
+ ## ESLint Expectations
66
+
67
+ Ensure these rules are enabled for migrated projects:
68
+
69
+ - `craft-ts/prefer-craft-service`: blocks new `@Injectable` / `@Service` app services.
70
+ - `craft-ts/prefer-craft-http-client`: keeps HTTP access inside craft-compatible APIs.
71
+ - `craft-ts/brand-angular-gen-deps-required` and `craft-ts/brand-angular-deps-match`: keep `GenDeps_*` current.
72
+ - Route rules: `require-assert-exhaustive-route-exceptions`, `require-pending-component-di-check`, and `require-child-route-mount-check`.
73
+
74
+ ## Review Checklist
75
+
76
+ - [ ] No new direct Angular `inject(...)` in app code.
77
+ - [ ] No new app-authored `@Injectable` service where `craftService` fits.
78
+ - [ ] Angular/framework tokens adapted exactly once with `toCraftService`.
79
+ - [ ] Consumption sites use the generated `X()` helper.
80
+ - [ ] Route checks use provider names, with `ProvidedValues = never` after migration.
81
+ - [ ] `GenDeps_*` aliases are regenerated after dependency changes.
82
+ - [ ] Tests use craft service testing helpers instead of TestBed-only service wiring where possible.
83
+
84
+ ## When To Push Back
85
+
86
+ Push back when a change adds a direct Angular token to `ProvidedValues` instead of adapting it with `toCraftService`, introduces a pass-through `craftService` with no meaningful interface, or disables DI checks to avoid fixing provider registration.