@craft-ts/dev-tools 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 (237) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -0
  3. package/generators.json +26 -0
  4. package/package.json +119 -0
  5. package/src/bin/craft-architecture-check.d.ts +2 -0
  6. package/src/bin/craft-architecture-check.js +51 -0
  7. package/src/bin/craft-architecture-check.js.map +1 -0
  8. package/src/bin/craft-brand.d.ts +2 -0
  9. package/src/bin/craft-brand.js +9 -0
  10. package/src/bin/craft-brand.js.map +1 -0
  11. package/src/bin/craft-graph.d.ts +2 -0
  12. package/src/bin/craft-graph.js +72 -0
  13. package/src/bin/craft-graph.js.map +1 -0
  14. package/src/bin/craft-migrate-architecture.d.ts +2 -0
  15. package/src/bin/craft-migrate-architecture.js +64 -0
  16. package/src/bin/craft-migrate-architecture.js.map +1 -0
  17. package/src/bin/craft-migrate-components.d.ts +2 -0
  18. package/src/bin/craft-migrate-components.js +67 -0
  19. package/src/bin/craft-migrate-components.js.map +1 -0
  20. package/src/bin/craft-migrate-primitives.d.ts +2 -0
  21. package/src/bin/craft-migrate-primitives.js +71 -0
  22. package/src/bin/craft-migrate-primitives.js.map +1 -0
  23. package/src/bin/craft-migrate-routes.d.ts +2 -0
  24. package/src/bin/craft-migrate-routes.js +77 -0
  25. package/src/bin/craft-migrate-routes.js.map +1 -0
  26. package/src/bin/craft-migrate-services.d.ts +2 -0
  27. package/src/bin/craft-migrate-services.js +75 -0
  28. package/src/bin/craft-migrate-services.js.map +1 -0
  29. package/src/bin/craft-migrate-template.d.ts +2 -0
  30. package/src/bin/craft-migrate-template.js +68 -0
  31. package/src/bin/craft-migrate-template.js.map +1 -0
  32. package/src/bin/craft-migrate.d.ts +2 -0
  33. package/src/bin/craft-migrate.js +91 -0
  34. package/src/bin/craft-migrate.js.map +1 -0
  35. package/src/bin/craft.d.ts +2 -0
  36. package/src/bin/craft.js +318 -0
  37. package/src/bin/craft.js.map +1 -0
  38. package/src/eslint-rules/anchor-has-href.cjs +35 -0
  39. package/src/eslint-rules/app-start-registry-match.cjs +355 -0
  40. package/src/eslint-rules/button-has-type.cjs +50 -0
  41. package/src/eslint-rules/control-has-accessible-name.cjs +56 -0
  42. package/src/eslint-rules/craft-component-name-match.cjs +8 -0
  43. package/src/eslint-rules/craft-computed-name-match.cjs +63 -0
  44. package/src/eslint-rules/craft-css-token-registry.cjs +17 -0
  45. package/src/eslint-rules/craft-css-var-naming.cjs +18 -0
  46. package/src/eslint-rules/craft-css-vars-contract.cjs +34 -0
  47. package/src/eslint-rules/craft-directive-name-match.cjs +8 -0
  48. package/src/eslint-rules/craft-method-name-match.cjs +8 -0
  49. package/src/eslint-rules/craft-name-match-utils.cjs +179 -0
  50. package/src/eslint-rules/craft-primitive-context.cjs +46 -0
  51. package/src/eslint-rules/craft-signal-source-name-match.cjs +8 -0
  52. package/src/eslint-rules/craft-source-name-match.cjs +8 -0
  53. package/src/eslint-rules/craft-styles-scope-safe.cjs +25 -0
  54. package/src/eslint-rules/css-rule-utils.cjs +109 -0
  55. package/src/eslint-rules/global-exception-registry-match.cjs +562 -0
  56. package/src/eslint-rules/heading-has-content.cjs +31 -0
  57. package/src/eslint-rules/html-helpers.cjs +115 -0
  58. package/src/eslint-rules/hyperscript-walk.cjs +249 -0
  59. package/src/eslint-rules/iframe-has-title.cjs +26 -0
  60. package/src/eslint-rules/img-has-alt.cjs +28 -0
  61. package/src/eslint-rules/index.cjs +209 -0
  62. package/src/eslint-rules/index.d.cts +5 -0
  63. package/src/eslint-rules/label-has-associated-control.cjs +67 -0
  64. package/src/eslint-rules/no-async-await.cjs +46 -0
  65. package/src/eslint-rules/no-craft-computed-side-effects.cjs +301 -0
  66. package/src/eslint-rules/no-craft-use-in-template.cjs +89 -0
  67. package/src/eslint-rules/no-craft-use.cjs +42 -0
  68. package/src/eslint-rules/no-direct-temporal-globals.cjs +73 -0
  69. package/src/eslint-rules/no-effect-in-params.cjs +265 -0
  70. package/src/eslint-rules/no-effect-outside-loaders.cjs +3 -0
  71. package/src/eslint-rules/no-ephemeral-template-form-state.cjs +131 -0
  72. package/src/eslint-rules/no-hardcoded-design-values.cjs +15 -0
  73. package/src/eslint-rules/no-heading-level-skip.cjs +101 -0
  74. package/src/eslint-rules/no-imperative-craft-resource-trigger.cjs +291 -0
  75. package/src/eslint-rules/no-important-in-component-styles.cjs +12 -0
  76. package/src/eslint-rules/no-injection-token.cjs +173 -0
  77. package/src/eslint-rules/no-invalid-insertion-pipe.cjs +162 -0
  78. package/src/eslint-rules/no-manual-route-provider-list.cjs +85 -0
  79. package/src/eslint-rules/no-noninteractive-element-interactions.cjs +66 -0
  80. package/src/eslint-rules/no-positive-tabindex.cjs +44 -0
  81. package/src/eslint-rules/no-redundant-primitive-insertion.cjs +361 -0
  82. package/src/eslint-rules/no-render-writes.cjs +106 -0
  83. package/src/eslint-rules/no-throw.cjs +132 -0
  84. package/src/eslint-rules/no-transition-actions.cjs +133 -0
  85. package/src/eslint-rules/no-type-assertions-in-template.cjs +211 -0
  86. package/src/eslint-rules/no-widened-route-provider-context.cjs +39 -0
  87. package/src/eslint-rules/prefer-browser-boundaries.cjs +92 -0
  88. package/src/eslint-rules/prefer-craft-http-transport.cjs +103 -0
  89. package/src/eslint-rules/prefer-craft-template-blocks.cjs +492 -0
  90. package/src/eslint-rules/prefer-direct-yieldable-callback.cjs +105 -0
  91. package/src/eslint-rules/prefer-named-html-helpers.cjs +33 -0
  92. package/src/eslint-rules/prefer-query-method-over-state-trigger.cjs +282 -0
  93. package/src/eslint-rules/prefer-relative-heading.cjs +43 -0
  94. package/src/eslint-rules/provide-host-name-match-component.cjs +455 -0
  95. package/src/eslint-rules/recommended-config.cjs +77 -0
  96. package/src/eslint-rules/require-assert-exhaustive-route-exceptions.cjs +259 -0
  97. package/src/eslint-rules/require-cascade-route-di-check.cjs +223 -0
  98. package/src/eslint-rules/require-child-route-mount-check.cjs +224 -0
  99. package/src/eslint-rules/require-component-monitoring.cjs +212 -0
  100. package/src/eslint-rules/require-craft-exception-handler.cjs +142 -0
  101. package/src/eslint-rules/require-craft-method-for-yieldable-callback.cjs +236 -0
  102. package/src/eslint-rules/require-craft-resource-trigger-yield.cjs +209 -0
  103. package/src/eslint-rules/require-effect-adapters.cjs +86 -0
  104. package/src/eslint-rules/require-exception-component-di-check.cjs +314 -0
  105. package/src/eslint-rules/require-focus-visible.cjs +36 -0
  106. package/src/eslint-rules/require-interactive-local-name.cjs +105 -0
  107. package/src/eslint-rules/require-lazy-load-with-retry.cjs +148 -0
  108. package/src/eslint-rules/require-outlet-heading-section.cjs +70 -0
  109. package/src/eslint-rules/require-pending-component-di-check.cjs +490 -0
  110. package/src/eslint-rules/require-primitive-context.cjs +156 -0
  111. package/src/eslint-rules/require-primitive-derived-property.cjs +598 -0
  112. package/src/eslint-rules/require-primitive-generator-unwrap.cjs +274 -0
  113. package/src/eslint-rules/require-reactive-template-bindings.cjs +305 -0
  114. package/src/eslint-rules/require-reduced-motion.cjs +38 -0
  115. package/src/eslint-rules/require-route-heading-outline.cjs +166 -0
  116. package/src/eslint-rules/require-yieldable-insertion-write.cjs +98 -0
  117. package/src/eslint-rules/require-yieldable-reactive-read.cjs +102 -0
  118. package/src/eslint-rules/require-yieldable-template-method.cjs +424 -0
  119. package/src/eslint-rules/role-has-required-aria.cjs +38 -0
  120. package/src/eslint-rules/server-function-client-match.cjs +283 -0
  121. package/src/eslint-rules/target-blank-noopener.cjs +31 -0
  122. package/src/eslint-rules/template-element-name-unique.cjs +124 -0
  123. package/src/eslint-rules/valid-aria.cjs +38 -0
  124. package/src/generators/route/compat.d.ts +3 -0
  125. package/src/generators/route/compat.js +6 -0
  126. package/src/generators/route/compat.js.map +1 -0
  127. package/src/generators/route/generator.d.ts +31 -0
  128. package/src/generators/route/generator.js +402 -0
  129. package/src/generators/route/generator.js.map +1 -0
  130. package/src/generators/route/schema.json +50 -0
  131. package/src/generators/route/split-schema.json +19 -0
  132. package/src/index.d.ts +21 -0
  133. package/src/index.js +23 -0
  134. package/src/index.js.map +1 -0
  135. package/src/scripts/angular-brand-codemod.d.ts +139 -0
  136. package/src/scripts/angular-brand-codemod.js +2318 -0
  137. package/src/scripts/angular-brand-codemod.js.map +1 -0
  138. package/src/scripts/angular-brand-codemod.ts +3985 -0
  139. package/src/scripts/architecture/migrate-architecture.d.ts +14 -0
  140. package/src/scripts/architecture/migrate-architecture.js +534 -0
  141. package/src/scripts/architecture/migrate-architecture.js.map +1 -0
  142. package/src/scripts/architecture/migrate-architecture.ts +653 -0
  143. package/src/scripts/architecture-graph.d.ts +368 -0
  144. package/src/scripts/architecture-graph.js +2191 -0
  145. package/src/scripts/architecture-graph.js.map +1 -0
  146. package/src/scripts/architecture-graph.ts +3136 -0
  147. package/src/scripts/components/migrate-components.d.ts +20 -0
  148. package/src/scripts/components/migrate-components.js +163 -0
  149. package/src/scripts/components/migrate-components.js.map +1 -0
  150. package/src/scripts/components/migrate-components.ts +227 -0
  151. package/src/scripts/components/migration-diagnostic.d.ts +7 -0
  152. package/src/scripts/components/migration-diagnostic.js +2 -0
  153. package/src/scripts/components/migration-diagnostic.js.map +1 -0
  154. package/src/scripts/components/migration-diagnostic.ts +8 -0
  155. package/src/scripts/create/create-project.d.ts +20 -0
  156. package/src/scripts/create/create-project.js +783 -0
  157. package/src/scripts/create/create-project.js.map +1 -0
  158. package/src/scripts/create/create-project.ts +849 -0
  159. package/src/scripts/data-flow-graph.d.ts +36 -0
  160. package/src/scripts/data-flow-graph.js +186 -0
  161. package/src/scripts/data-flow-graph.js.map +1 -0
  162. package/src/scripts/data-flow-graph.ts +240 -0
  163. package/src/scripts/dependency-graph.d.ts +152 -0
  164. package/src/scripts/dependency-graph.js +4456 -0
  165. package/src/scripts/dependency-graph.js.map +1 -0
  166. package/src/scripts/dependency-graph.ts +5908 -0
  167. package/src/scripts/effect-dependency-graph.d.ts +89 -0
  168. package/src/scripts/effect-dependency-graph.js +487 -0
  169. package/src/scripts/effect-dependency-graph.js.map +1 -0
  170. package/src/scripts/effect-dependency-graph.ts +630 -0
  171. package/src/scripts/migrate.d.ts +37 -0
  172. package/src/scripts/migrate.js +98 -0
  173. package/src/scripts/migrate.js.map +1 -0
  174. package/src/scripts/migrate.ts +162 -0
  175. package/src/scripts/migration-workspace.d.ts +2 -0
  176. package/src/scripts/migration-workspace.js +67 -0
  177. package/src/scripts/migration-workspace.js.map +1 -0
  178. package/src/scripts/migration-workspace.ts +93 -0
  179. package/src/scripts/primitives/migrate-named-primitives.d.ts +50 -0
  180. package/src/scripts/primitives/migrate-named-primitives.js +487 -0
  181. package/src/scripts/primitives/migrate-named-primitives.js.map +1 -0
  182. package/src/scripts/primitives/migrate-named-primitives.ts +627 -0
  183. package/src/scripts/primitives/migrate-primitive-generators.d.ts +30 -0
  184. package/src/scripts/primitives/migrate-primitive-generators.js +309 -0
  185. package/src/scripts/primitives/migrate-primitive-generators.js.map +1 -0
  186. package/src/scripts/primitives/migrate-primitive-generators.ts +410 -0
  187. package/src/scripts/primitives/migrate-primitives.d.ts +27 -0
  188. package/src/scripts/primitives/migrate-primitives.js +427 -0
  189. package/src/scripts/primitives/migrate-primitives.js.map +1 -0
  190. package/src/scripts/primitives/migrate-primitives.ts +655 -0
  191. package/src/scripts/primitives/migrate-yieldable-reactive-reads.d.ts +23 -0
  192. package/src/scripts/primitives/migrate-yieldable-reactive-reads.js +382 -0
  193. package/src/scripts/primitives/migrate-yieldable-reactive-reads.js.map +1 -0
  194. package/src/scripts/primitives/migrate-yieldable-reactive-reads.ts +499 -0
  195. package/src/scripts/primitives/migration-diagnostic.d.ts +8 -0
  196. package/src/scripts/primitives/migration-diagnostic.js +2 -0
  197. package/src/scripts/primitives/migration-diagnostic.js.map +1 -0
  198. package/src/scripts/primitives/migration-diagnostic.ts +15 -0
  199. package/src/scripts/routes/migrate-routes.d.ts +34 -0
  200. package/src/scripts/routes/migrate-routes.js +657 -0
  201. package/src/scripts/routes/migrate-routes.js.map +1 -0
  202. package/src/scripts/routes/migrate-routes.ts +895 -0
  203. package/src/scripts/routes/migration-diagnostic.d.ts +16 -0
  204. package/src/scripts/routes/migration-diagnostic.js +2 -0
  205. package/src/scripts/routes/migration-diagnostic.js.map +1 -0
  206. package/src/scripts/routes/migration-diagnostic.ts +25 -0
  207. package/src/scripts/routes/route-command.d.ts +75 -0
  208. package/src/scripts/routes/route-command.js +1169 -0
  209. package/src/scripts/routes/route-command.js.map +1 -0
  210. package/src/scripts/routes/route-command.ts +1807 -0
  211. package/src/scripts/routes/verify-routes.d.ts +60 -0
  212. package/src/scripts/routes/verify-routes.js +1121 -0
  213. package/src/scripts/routes/verify-routes.js.map +1 -0
  214. package/src/scripts/routes/verify-routes.ts +1511 -0
  215. package/src/scripts/services/config.d.ts +2 -0
  216. package/src/scripts/services/config.js +38 -0
  217. package/src/scripts/services/config.js.map +1 -0
  218. package/src/scripts/services/config.ts +60 -0
  219. package/src/scripts/services/migrate-services.d.ts +29 -0
  220. package/src/scripts/services/migrate-services.js +924 -0
  221. package/src/scripts/services/migrate-services.js.map +1 -0
  222. package/src/scripts/services/migrate-services.ts +1348 -0
  223. package/src/scripts/services/migration-diagnostic.d.ts +8 -0
  224. package/src/scripts/services/migration-diagnostic.js +2 -0
  225. package/src/scripts/services/migration-diagnostic.js.map +1 -0
  226. package/src/scripts/services/migration-diagnostic.ts +28 -0
  227. package/src/scripts/template/migrate-template.d.ts +16 -0
  228. package/src/scripts/template/migrate-template.js +349 -0
  229. package/src/scripts/template/migrate-template.js.map +1 -0
  230. package/src/scripts/template/migrate-template.ts +418 -0
  231. package/src/template-migration.d.ts +1 -0
  232. package/src/template-migration.js +2 -0
  233. package/src/template-migration.js.map +1 -0
  234. package/src/testing.d.ts +19 -0
  235. package/src/testing.js +2 -0
  236. package/src/testing.js.map +1 -0
  237. package/src/tsconfig.codemod.json +13 -0
@@ -0,0 +1,1511 @@
1
+ import { execFile, spawn } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import {
4
+ mkdir,
5
+ mkdtemp,
6
+ readdir,
7
+ readFile,
8
+ rm,
9
+ writeFile,
10
+ } from 'node:fs/promises';
11
+ import { basename, dirname, join, relative, resolve } from 'node:path';
12
+ import { promisify } from 'node:util';
13
+ import { Node, Project, ScriptTarget, SyntaxKind, ts } from 'ts-morph';
14
+ import {
15
+ defineAngularBrandConfig,
16
+ transformSourceFile,
17
+ } from '../angular-brand-codemod.js';
18
+ import { listAngularProjects } from './route-command.js';
19
+
20
+ const execFileAsync = promisify(execFile);
21
+
22
+ export type RouteVerificationOptions = {
23
+ rootDir?: string;
24
+ project?: string;
25
+ tsConfigFilePath?: string;
26
+ json?: boolean;
27
+ keepFixtures?: boolean;
28
+ log?: (message: string) => void;
29
+ };
30
+
31
+ export type RouteVerificationCaseResult = {
32
+ id: string;
33
+ category: string;
34
+ status: 'passed' | 'failed';
35
+ expected: string[];
36
+ matched: string[];
37
+ output?: string;
38
+ };
39
+
40
+ export type RouteVerificationResult = {
41
+ exitCode: number;
42
+ project: string;
43
+ tsConfigFilePath: string;
44
+ sourceRoot: string;
45
+ fixtureDirectory?: string;
46
+ baseline: { status: 'passed' | 'failed'; output?: string };
47
+ routeChecks: {
48
+ status: 'passed' | 'failed' | 'skipped';
49
+ diagnostics?: string[];
50
+ };
51
+ projectLint: {
52
+ status: 'passed' | 'failed' | 'skipped';
53
+ diagnostics?: string[];
54
+ output?: string;
55
+ };
56
+ eslint: {
57
+ status: 'passed' | 'failed' | 'skipped';
58
+ output?: string;
59
+ };
60
+ cases: RouteVerificationCaseResult[];
61
+ diagnostics: string[];
62
+ };
63
+
64
+ export type RouteVerificationFixture = {
65
+ id: string;
66
+ category: string;
67
+ fileName: string;
68
+ source: string;
69
+ expected: string[];
70
+ kind: 'positive' | 'negative';
71
+ };
72
+
73
+ type ProjectContext = {
74
+ rootDir: string;
75
+ project: string;
76
+ tsConfigFilePath: string;
77
+ sourceRoot: string;
78
+ };
79
+
80
+ const serviceName = 'VerifyMissingService';
81
+ const routeContext = 'path: "missing-provider"';
82
+
83
+ /**
84
+ * Runs a compile-time smoke suite against the real application tsconfig.
85
+ *
86
+ * The suite deliberately contains both valid and invalid route definitions. A
87
+ * valid batch must type-check cleanly; an invalid batch must fail with the
88
+ * expected route/pending/exception context. All files are removed in finally.
89
+ */
90
+ export async function runRouteVerification(
91
+ options: RouteVerificationOptions = {},
92
+ ): Promise<RouteVerificationResult> {
93
+ const rootDir = resolve(options.rootDir ?? process.cwd());
94
+ const context = resolveProjectContext(rootDir, options);
95
+ const log = options.log ?? (options.json ? () => undefined : console.log);
96
+ const diagnostics: string[] = [];
97
+ const compiler =
98
+ findLocalBinary(rootDir, 'ngc') ?? findLocalBinary(rootDir, 'tsc');
99
+
100
+ if (!compiler) {
101
+ return failedResult(
102
+ context,
103
+ 'No local ngc or tsc binary was found. Install the project dependencies before running route verification.',
104
+ );
105
+ }
106
+
107
+ log(`[route verify] Project: ${context.tsConfigFilePath}`);
108
+ log('[route verify] 1/7 Checking the existing project...');
109
+ const baselineOutput = await runTypeCheck(compiler, context, log, 'baseline');
110
+ const baseline = {
111
+ status: baselineOutput.ok ? ('passed' as const) : ('failed' as const),
112
+ ...(baselineOutput.output ? { output: baselineOutput.output } : {}),
113
+ };
114
+ if (!baselineOutput.ok) {
115
+ log('[route verify] Baseline failed; fixtures were not created.');
116
+ return {
117
+ exitCode: 1,
118
+ ...context,
119
+ baseline,
120
+ routeChecks: { status: 'skipped' },
121
+ projectLint: { status: 'skipped' },
122
+ eslint: { status: 'skipped' },
123
+ cases: [],
124
+ diagnostics: [
125
+ 'The project already has type-check errors. Fix the baseline before running route verification.',
126
+ ],
127
+ };
128
+ }
129
+
130
+ log('[route verify] 2/7 Checking DI proofs on existing routes...');
131
+ const routeChecks = await auditProjectRouteChecks(context.sourceRoot, log);
132
+ if (routeChecks.status === 'failed') {
133
+ diagnostics.push(...(routeChecks.diagnostics ?? []));
134
+ }
135
+
136
+ log('[route verify] 3/7 Checking existing route exception bookkeeping...');
137
+ const projectLint = await runProjectRouteLint(
138
+ rootDir,
139
+ context.sourceRoot,
140
+ log,
141
+ );
142
+ if (projectLint.status === 'failed') {
143
+ diagnostics.push(...(projectLint.diagnostics ?? []));
144
+ }
145
+
146
+ let fixtureDirectory: string | undefined;
147
+ let eslint: RouteVerificationResult['eslint'] = { status: 'skipped' };
148
+ const cases: RouteVerificationCaseResult[] = [];
149
+
150
+ try {
151
+ log('[route verify] 4/7 Generating temporary fixtures...');
152
+ fixtureDirectory = await mkdtemp(
153
+ join(context.sourceRoot, 'craft-route-verify-'),
154
+ );
155
+ const fixtures = createRouteVerificationFixtures();
156
+ const positive = fixtures.filter((fixture) => fixture.kind === 'positive');
157
+ const negative = fixtures.filter((fixture) => fixture.kind === 'negative');
158
+
159
+ await writeFixtures(fixtureDirectory, positive);
160
+ await prepareAngularBrandFixture(fixtureDirectory);
161
+
162
+ log(
163
+ `[route verify] 5/7 Running ESLint on ${positive.length} positive fixtures...`,
164
+ );
165
+ eslint = await runFixtureLint(
166
+ rootDir,
167
+ context.sourceRoot,
168
+ fixtureDirectory,
169
+ log,
170
+ );
171
+ if (eslint.status === 'failed') {
172
+ diagnostics.push(
173
+ 'The temporary route fixtures failed the ESLint bookkeeping pass.',
174
+ );
175
+ }
176
+
177
+ log('[route verify] 6/7 Compiling valid fixtures...');
178
+ const positiveCheck = await runTypeCheck(
179
+ compiler,
180
+ context,
181
+ log,
182
+ 'valid fixtures',
183
+ );
184
+ for (const fixture of positive) {
185
+ cases.push({
186
+ id: fixture.id,
187
+ category: fixture.category,
188
+ status: positiveCheck.ok ? 'passed' : 'failed',
189
+ expected: [],
190
+ matched: positiveCheck.ok ? [] : [],
191
+ ...(positiveCheck.output ? { output: positiveCheck.output } : {}),
192
+ });
193
+ }
194
+
195
+ if (!positiveCheck.ok) {
196
+ diagnostics.push('Valid route verification fixtures do not compile.');
197
+ }
198
+
199
+ log(
200
+ '[route verify] 7/7 Compiling invalid fixtures and matching expected diagnostics...',
201
+ );
202
+ log(
203
+ '[route verify] The compiler errors printed in this phase are intentional.',
204
+ );
205
+ await writeFixtures(fixtureDirectory, negative);
206
+ const negativeCheck = await runTypeCheck(
207
+ compiler,
208
+ context,
209
+ log,
210
+ 'invalid fixtures',
211
+ );
212
+ const output = negativeCheck.output;
213
+ for (const fixture of negative) {
214
+ const matched = matchRouteVerificationDiagnostics(
215
+ output,
216
+ fixture.expected,
217
+ );
218
+ const passed =
219
+ !negativeCheck.ok && matched.length === fixture.expected.length;
220
+ cases.push({
221
+ id: fixture.id,
222
+ category: fixture.category,
223
+ status: passed ? 'passed' : 'failed',
224
+ expected: fixture.expected,
225
+ matched,
226
+ ...(output ? { output } : {}),
227
+ });
228
+ if (!passed) {
229
+ diagnostics.push(
230
+ `${fixture.id}: expected ${fixture.expected.join(' / ')} in the compiler diagnostics.`,
231
+ );
232
+ }
233
+ }
234
+
235
+ const failedCases = cases.filter((item) => item.status === 'failed');
236
+ const result: RouteVerificationResult = {
237
+ exitCode: diagnostics.length === 0 ? 0 : 1,
238
+ ...context,
239
+ fixtureDirectory: options.keepFixtures ? fixtureDirectory : undefined,
240
+ baseline,
241
+ routeChecks,
242
+ projectLint,
243
+ eslint,
244
+ cases,
245
+ diagnostics,
246
+ };
247
+ if (result.exitCode === 0) {
248
+ log(`[route verify] Passed (${cases.length} fixture(s)).`);
249
+ } else {
250
+ const projectIssueCount =
251
+ (routeChecks.diagnostics?.length ?? 0) +
252
+ (projectLint.diagnostics?.length ?? 0);
253
+ const fixtureIssueCount =
254
+ failedCases.length + (eslint.status === 'failed' ? 1 : 0);
255
+ const issues = [
256
+ projectIssueCount > 0
257
+ ? `${projectIssueCount} project issue${projectIssueCount === 1 ? '' : 's'}`
258
+ : undefined,
259
+ fixtureIssueCount > 0
260
+ ? `${fixtureIssueCount} fixture failure${fixtureIssueCount === 1 ? '' : 's'}`
261
+ : undefined,
262
+ ].filter((issue): issue is string => issue !== undefined);
263
+ log(`[route verify] Failed: ${issues.join(', ')}.`);
264
+ }
265
+ return result;
266
+ } catch (error) {
267
+ const message = error instanceof Error ? error.message : String(error);
268
+ diagnostics.push(message);
269
+ return {
270
+ exitCode: 1,
271
+ ...context,
272
+ fixtureDirectory: options.keepFixtures ? fixtureDirectory : undefined,
273
+ baseline,
274
+ routeChecks,
275
+ projectLint,
276
+ eslint,
277
+ cases,
278
+ diagnostics,
279
+ };
280
+ } finally {
281
+ if (fixtureDirectory && !options.keepFixtures) {
282
+ await rm(fixtureDirectory, { recursive: true, force: true });
283
+ }
284
+ }
285
+ }
286
+
287
+ export function matchRouteVerificationDiagnostics(
288
+ output: string,
289
+ expected: readonly string[],
290
+ ): string[] {
291
+ const normalizedOutput = output.replace(/\\"/g, '"');
292
+ return expected.filter((value) => normalizedOutput.includes(value));
293
+ }
294
+
295
+ function resolveProjectContext(
296
+ rootDir: string,
297
+ options: RouteVerificationOptions,
298
+ ): ProjectContext {
299
+ const tsConfigFilePath = resolveTsConfig(
300
+ rootDir,
301
+ options.tsConfigFilePath ?? options.project,
302
+ );
303
+ const project =
304
+ options.project ?? relative(rootDir, dirname(tsConfigFilePath));
305
+ const projectRoot = dirname(tsConfigFilePath);
306
+ const sourceRoot =
307
+ basename(tsConfigFilePath) === 'tsconfig.app.json'
308
+ ? existsSync(join(projectRoot, 'src'))
309
+ ? join(projectRoot, 'src')
310
+ : projectRoot
311
+ : rootDir;
312
+ return {
313
+ rootDir,
314
+ project: project || '.',
315
+ tsConfigFilePath,
316
+ sourceRoot,
317
+ };
318
+ }
319
+
320
+ function resolveTsConfig(rootDir: string, projectOption?: string): string {
321
+ if (projectOption) {
322
+ const direct = resolve(rootDir, projectOption);
323
+ if (existsSync(direct) && direct.endsWith('.json')) return direct;
324
+ for (const candidate of [
325
+ join(direct, 'tsconfig.app.json'),
326
+ join(rootDir, 'apps', projectOption, 'tsconfig.app.json'),
327
+ join(rootDir, 'projects', projectOption, 'tsconfig.app.json'),
328
+ ]) {
329
+ if (existsSync(candidate)) return candidate;
330
+ }
331
+ }
332
+
333
+ const detected = listAngularProjects(rootDir)
334
+ .map((project) => join(rootDir, project, 'tsconfig.app.json'))
335
+ .filter((candidate) => existsSync(candidate));
336
+ if (detected.length === 1) return detected[0];
337
+ for (const candidate of [
338
+ join(rootDir, 'tsconfig.app.json'),
339
+ join(rootDir, 'tsconfig.json'),
340
+ ]) {
341
+ if (existsSync(candidate)) return candidate;
342
+ }
343
+ return join(rootDir, 'tsconfig.json');
344
+ }
345
+
346
+ async function writeFixtures(
347
+ directory: string,
348
+ fixtures: readonly RouteVerificationFixture[],
349
+ ) {
350
+ await Promise.all(
351
+ fixtures.map(async (fixture) => {
352
+ const target = join(directory, fixture.fileName);
353
+ await mkdir(dirname(target), { recursive: true });
354
+ await writeFile(target, fixture.source.trimStart(), 'utf8');
355
+ }),
356
+ );
357
+ }
358
+
359
+ /** Generate the Angular GenDeps alias using the same codemod as the project. */
360
+ async function prepareAngularBrandFixture(directory: string): Promise<void> {
361
+ const componentPath = join(directory, 'template-missing.component.ts');
362
+ if (!existsSync(componentPath)) return;
363
+ const project = new Project({
364
+ compilerOptions: {
365
+ experimentalDecorators: true,
366
+ module: ts.ModuleKind.Preserve,
367
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
368
+ target: ScriptTarget.ES2022,
369
+ },
370
+ });
371
+ const sourceFile = project.createSourceFile(
372
+ componentPath,
373
+ await readFile(componentPath, 'utf8'),
374
+ { overwrite: true },
375
+ );
376
+ transformSourceFile(sourceFile, {
377
+ config: defineAngularBrandConfig({
378
+ importAugmentations: [
379
+ {
380
+ match: {
381
+ module: './template-pipe',
382
+ symbols: ['VerifyTemplatePipe'],
383
+ metadata: ['imports'],
384
+ },
385
+ deps: [
386
+ {
387
+ key: serviceName,
388
+ symbol: serviceName,
389
+ module: './support',
390
+ typeText: `typeof ${serviceName}`,
391
+ },
392
+ ],
393
+ missingProvider: [
394
+ {
395
+ key: serviceName,
396
+ symbol: serviceName,
397
+ module: './support',
398
+ typeText: `typeof ${serviceName}`,
399
+ },
400
+ ],
401
+ },
402
+ ],
403
+ }),
404
+ });
405
+ const supportImport = sourceFile
406
+ .getImportDeclarations()
407
+ .find(
408
+ (declaration) => declaration.getModuleSpecifierValue() === './support',
409
+ );
410
+ if (supportImport) {
411
+ const hasServiceImport = supportImport
412
+ .getNamedImports()
413
+ .some((namedImport) => namedImport.getName() === serviceName);
414
+ if (!hasServiceImport)
415
+ supportImport.addNamedImports([{ name: serviceName }]);
416
+ } else {
417
+ sourceFile.addImportDeclaration({
418
+ moduleSpecifier: './support',
419
+ namedImports: [{ name: serviceName }],
420
+ });
421
+ }
422
+ sourceFile.insertText(
423
+ 0,
424
+ '/* eslint-disable @typescript-eslint/no-empty-object-type */\n',
425
+ );
426
+ await writeFile(componentPath, sourceFile.getFullText(), 'utf8');
427
+ }
428
+
429
+ async function runFixtureLint(
430
+ rootDir: string,
431
+ sourceRoot: string,
432
+ directory: string,
433
+ log: (message: string) => void,
434
+ ): Promise<RouteVerificationResult['eslint']> {
435
+ const eslint = findLocalBinary(rootDir, 'eslint');
436
+ if (!eslint) {
437
+ log('[route verify] ESLint not found; bookkeeping pass skipped.');
438
+ return { status: 'skipped' };
439
+ }
440
+ const files = await listFiles(
441
+ directory,
442
+ (file) => file.endsWith('.routes.ts') || file.endsWith('.component.ts'),
443
+ );
444
+ if (files.length === 0) return { status: 'skipped' };
445
+ try {
446
+ const { stdout, stderr } = await execFileAsync(
447
+ eslint,
448
+ ['--fix', '--format', 'json', ...files],
449
+ { cwd: sourceRoot, maxBuffer: 20 * 1024 * 1024 },
450
+ );
451
+ log('[route verify] ESLint bookkeeping passed.');
452
+ return {
453
+ status: 'passed',
454
+ output: `${stdout}${stderr}`.trim() || undefined,
455
+ };
456
+ } catch (error) {
457
+ log('[route verify] ESLint bookkeeping reported errors.');
458
+ for (const line of commandOutput(error).split(/\r?\n/)) {
459
+ if (line.trim()) log(`[route verify] eslint: ${line}`);
460
+ }
461
+ return {
462
+ status: 'failed',
463
+ output: commandOutput(error),
464
+ };
465
+ }
466
+ }
467
+
468
+ const projectRouteLintRules = [
469
+ 'craft-ts/require-assert-exhaustive-route-exceptions',
470
+ 'craft-ts/require-craft-exception-handler',
471
+ 'craft-ts/require-pending-component-di-check',
472
+ 'craft-ts/require-exception-component-di-check',
473
+ 'craft-ts/require-lazy-load-with-retry',
474
+ 'craft-ts/global-exception-registry-match',
475
+ ] as const;
476
+
477
+ async function runProjectRouteLint(
478
+ rootDir: string,
479
+ sourceRoot: string,
480
+ log: (message: string) => void,
481
+ ): Promise<RouteVerificationResult['projectLint']> {
482
+ const eslint = findLocalBinary(rootDir, 'eslint');
483
+ if (!eslint) {
484
+ log('[route verify] ESLint not found; existing route checks skipped.');
485
+ return { status: 'skipped' };
486
+ }
487
+
488
+ const files = await listFiles(sourceRoot, (file) =>
489
+ file.endsWith('.routes.ts'),
490
+ );
491
+ if (files.length === 0) {
492
+ log('[route verify] No existing route files found; route lint skipped.');
493
+ return { status: 'skipped' };
494
+ }
495
+
496
+ const args = [
497
+ '--format',
498
+ 'json',
499
+ '--no-warn-ignored',
500
+ ...projectRouteLintRules.flatMap((rule) => ['--rule', `${rule}:error`]),
501
+ ...files,
502
+ ];
503
+
504
+ try {
505
+ const { stdout, stderr } = await execFileAsync(eslint, args, {
506
+ cwd: sourceRoot,
507
+ maxBuffer: 20 * 1024 * 1024,
508
+ });
509
+ const diagnostics = readProjectLintDiagnostics(stdout);
510
+ if (diagnostics.length === 0) {
511
+ log('[route verify] Existing route exception bookkeeping passed.');
512
+ return {
513
+ status: 'passed',
514
+ output: `${stdout}${stderr}`.trim() || undefined,
515
+ };
516
+ }
517
+ reportProjectLintDiagnostics(log, diagnostics);
518
+ return {
519
+ status: 'failed',
520
+ diagnostics,
521
+ output: `${stdout}${stderr}`.trim() || undefined,
522
+ };
523
+ } catch (error) {
524
+ const stdout = commandStdout(error);
525
+ const diagnostics = readProjectLintDiagnostics(stdout);
526
+ if (diagnostics.length === 0) {
527
+ diagnostics.push(
528
+ `Existing route ESLint pass failed: ${commandOutput(error)}`,
529
+ );
530
+ }
531
+ reportProjectLintDiagnostics(log, diagnostics);
532
+ return {
533
+ status: 'failed',
534
+ diagnostics,
535
+ output: commandOutput(error),
536
+ };
537
+ }
538
+ }
539
+
540
+ function readProjectLintDiagnostics(output: string): string[] {
541
+ if (!output.trim()) return [];
542
+ let reports: unknown;
543
+ try {
544
+ reports = JSON.parse(output);
545
+ } catch {
546
+ return [];
547
+ }
548
+ if (!Array.isArray(reports)) return [];
549
+
550
+ const diagnostics: string[] = [];
551
+ for (const report of reports) {
552
+ if (!isRecord(report) || typeof report['filePath'] !== 'string') continue;
553
+ const messages = Array.isArray(report['messages'])
554
+ ? report['messages']
555
+ : [];
556
+ for (const message of messages) {
557
+ if (
558
+ !isRecord(message) ||
559
+ message['severity'] !== 2 ||
560
+ typeof message['ruleId'] !== 'string' ||
561
+ !projectRouteLintRules.includes(
562
+ message['ruleId'] as (typeof projectRouteLintRules)[number],
563
+ ) ||
564
+ typeof message['message'] !== 'string'
565
+ ) {
566
+ continue;
567
+ }
568
+ const line =
569
+ typeof message['line'] === 'number' ? `:${message['line']}` : '';
570
+ diagnostics.push(
571
+ `${relative(process.cwd(), report['filePath'])}${line}: ${message['message']}`,
572
+ );
573
+ }
574
+ }
575
+ return diagnostics;
576
+ }
577
+
578
+ function reportProjectLintDiagnostics(
579
+ log: (message: string) => void,
580
+ diagnostics: readonly string[],
581
+ ): void {
582
+ for (const diagnostic of diagnostics) {
583
+ log(`[route verify] route lint: ${diagnostic}`);
584
+ }
585
+ }
586
+
587
+ function isRecord(value: unknown): value is Record<string, unknown> {
588
+ return typeof value === 'object' && value !== null;
589
+ }
590
+
591
+ function commandStdout(value: unknown): string {
592
+ if (!isRecord(value)) return '';
593
+ return typeof value['stdout'] === 'string' ? value['stdout'] : '';
594
+ }
595
+
596
+ type ProjectRouteCheckResult = RouteVerificationResult['routeChecks'];
597
+
598
+ type ProjectRouteEntry = {
599
+ path: string;
600
+ moduleSpecifiers: string[];
601
+ position: number;
602
+ };
603
+
604
+ async function auditProjectRouteChecks(
605
+ sourceRoot: string,
606
+ log: (message: string) => void,
607
+ ): Promise<ProjectRouteCheckResult> {
608
+ const files = await listFiles(sourceRoot, (file) =>
609
+ file.endsWith('.routes.ts'),
610
+ );
611
+ if (files.length === 0) {
612
+ log('[route verify] No existing route files found; DI audit skipped.');
613
+ return { status: 'skipped' };
614
+ }
615
+
616
+ const diagnostics: string[] = [];
617
+ for (const filePath of files) {
618
+ const source = await readFile(filePath, 'utf8');
619
+ const project = new Project({
620
+ useInMemoryFileSystem: true,
621
+ compilerOptions: {
622
+ module: ts.ModuleKind.Preserve,
623
+ target: ScriptTarget.ES2022,
624
+ },
625
+ });
626
+ const sourceFile = project.createSourceFile(filePath, source, {
627
+ overwrite: true,
628
+ });
629
+
630
+ for (const routesCall of sourceFile
631
+ .getDescendantsOfKind(SyntaxKind.CallExpression)
632
+ .filter((call) => call.getExpression().getText() === 'craftRoutes')) {
633
+ const routesName = getProjectRoutesName(routesCall);
634
+ const routeArray = routesCall.getArguments()[1];
635
+ if (
636
+ !routesName ||
637
+ !routeArray ||
638
+ !Node.isArrayLiteralExpression(routeArray)
639
+ ) {
640
+ continue;
641
+ }
642
+
643
+ const checks = collectProjectCanRunChecks(sourceFile);
644
+ if (hasProjectCascadeCheck(sourceFile, routesName, checks)) continue;
645
+
646
+ for (const element of routeArray.getElements()) {
647
+ const route = readProjectRoute(element);
648
+ if (!route || !routeHasComponent(route.definition)) continue;
649
+
650
+ const covered =
651
+ route.moduleSpecifiers.some((moduleSpecifier) =>
652
+ checks.some((check) => check.includes(moduleSpecifier)),
653
+ ) || checks.some((check) => hasRouteContext(check, route.path));
654
+ if (
655
+ covered ||
656
+ (route.moduleSpecifiers.length > 0 &&
657
+ (await routeHasInternalProviders(
658
+ filePath,
659
+ route.moduleSpecifiers,
660
+ )) &&
661
+ !route.path.includes(':'))
662
+ ) {
663
+ continue;
664
+ }
665
+
666
+ const location = sourceFile.getLineAndColumnAtPos(route.position);
667
+ diagnostics.push(
668
+ `${relative(sourceRoot, filePath)}:${location.line}: route ${JSON.stringify(route.path)} ` +
669
+ 'is missing its route DI check. Add an active CanRun<RouteCheckedDI> proof in this file.',
670
+ );
671
+ }
672
+ }
673
+ }
674
+
675
+ if (diagnostics.length === 0) {
676
+ log('[route verify] Existing route DI checks passed.');
677
+ return { status: 'passed' };
678
+ }
679
+ for (const diagnostic of diagnostics) {
680
+ log(`[route verify] DI: ${diagnostic}`);
681
+ }
682
+ return { status: 'failed', diagnostics };
683
+ }
684
+
685
+ async function routeHasInternalProviders(
686
+ routeFilePath: string,
687
+ moduleSpecifiers: readonly string[],
688
+ ): Promise<boolean> {
689
+ for (const moduleSpecifier of moduleSpecifiers) {
690
+ if (!moduleSpecifier.startsWith('.')) continue;
691
+ const base = resolve(dirname(routeFilePath), moduleSpecifier);
692
+ const target = [`${base}.ts`, `${base}.tsx`, join(base, 'index.ts')].find(
693
+ (candidate) => existsSync(candidate),
694
+ );
695
+ if (!target) continue;
696
+ const source = await readFile(target, 'utf8');
697
+ if (/\bwithProviders\s*\(|\bproviders\s*:/.test(source)) return true;
698
+ }
699
+ return false;
700
+ }
701
+
702
+ function getProjectRoutesName(
703
+ call: import('ts-morph').CallExpression,
704
+ ): string | undefined {
705
+ const declaration = call.getFirstAncestorByKind(
706
+ SyntaxKind.VariableDeclaration,
707
+ );
708
+ const binding = declaration?.getNameNode();
709
+ if (!binding || !Node.isObjectBindingPattern(binding)) return undefined;
710
+ const collection = call.getArguments()[0];
711
+ const expected = Node.isStringLiteral(collection)
712
+ ? `${collection.getLiteralValue()}Routes`
713
+ : undefined;
714
+ const routeBindings = binding
715
+ .getElements()
716
+ .filter((element) =>
717
+ (element.getPropertyNameNode()?.getText() ?? element.getName()).endsWith(
718
+ 'Routes',
719
+ ),
720
+ );
721
+ const match = routeBindings.find(
722
+ (element) =>
723
+ expected !== undefined &&
724
+ (element.getPropertyNameNode()?.getText() ?? element.getName()) ===
725
+ expected,
726
+ );
727
+ return (
728
+ match?.getName() ??
729
+ (routeBindings.length === 1 ? routeBindings[0].getName() : undefined)
730
+ );
731
+ }
732
+
733
+ function collectProjectCanRunChecks(
734
+ sourceFile: import('ts-morph').SourceFile,
735
+ ): string[] {
736
+ const aliases = new Map(
737
+ sourceFile.getTypeAliases().map((alias) => [alias.getName(), alias]),
738
+ );
739
+ const checks: string[] = [];
740
+ for (const alias of sourceFile.getTypeAliases()) {
741
+ const typeNode = alias.getTypeNode();
742
+ if (
743
+ !typeNode ||
744
+ !Node.isTypeReference(typeNode) ||
745
+ typeNode.getTypeName().getText() !== 'CanRun'
746
+ ) {
747
+ continue;
748
+ }
749
+ const checkedName = typeNode.getTypeArguments()[0]?.getText();
750
+ checks.push(
751
+ [
752
+ alias.getText(),
753
+ checkedName ? aliases.get(checkedName)?.getText() : undefined,
754
+ ]
755
+ .filter((text): text is string => text !== undefined)
756
+ .join('\n'),
757
+ );
758
+ }
759
+ return checks;
760
+ }
761
+
762
+ function hasProjectCascadeCheck(
763
+ sourceFile: import('ts-morph').SourceFile,
764
+ routesName: string,
765
+ checks: readonly string[],
766
+ ): boolean {
767
+ const cascadeNames = sourceFile
768
+ .getTypeAliases()
769
+ .filter((alias) => {
770
+ const typeNode = alias.getTypeNode();
771
+ if (!typeNode || !Node.isTypeReference(typeNode)) return false;
772
+ if (typeNode.getTypeName().getText() !== 'ValidateCascadeRoutesFile') {
773
+ return false;
774
+ }
775
+ return (
776
+ typeNode.getTypeArguments()[2]?.getText() === `typeof ${routesName}`
777
+ );
778
+ })
779
+ .map((alias) => alias.getName());
780
+ return cascadeNames.some((name) =>
781
+ checks.some((check) => check.includes(`<${name}>`)),
782
+ );
783
+ }
784
+
785
+ function readProjectRoute(element: import('ts-morph').Node):
786
+ | (ProjectRouteEntry & {
787
+ definition: import('ts-morph').ObjectLiteralExpression;
788
+ })
789
+ | undefined {
790
+ let routeCall: import('ts-morph').CallExpression | undefined;
791
+ if (
792
+ Node.isCallExpression(element) &&
793
+ element.getExpression().getText() === 'craftRoute'
794
+ ) {
795
+ routeCall = element;
796
+ } else if (Node.isCallExpression(element)) {
797
+ const expression = element.getExpression();
798
+ if (!Node.isPropertyAccessExpression(expression)) return undefined;
799
+ if (expression.getName() !== 'withProviders') return undefined;
800
+ const inner = expression.getExpression();
801
+ if (
802
+ Node.isCallExpression(inner) &&
803
+ inner.getExpression().getText() === 'craftRoute'
804
+ ) {
805
+ routeCall = inner;
806
+ }
807
+ }
808
+
809
+ if (routeCall) {
810
+ const path = routeCall.getArguments()[0];
811
+ const definition = routeCall.getArguments()[1];
812
+ if (
813
+ !Node.isStringLiteral(path) ||
814
+ !Node.isObjectLiteralExpression(definition)
815
+ ) {
816
+ return undefined;
817
+ }
818
+ return {
819
+ path: path.getLiteralValue(),
820
+ moduleSpecifiers: projectImportSpecifiers(definition),
821
+ position: routeCall.getStart(),
822
+ definition,
823
+ };
824
+ }
825
+
826
+ if (!Node.isObjectLiteralExpression(element)) return undefined;
827
+ const path = element
828
+ .getProperties()
829
+ .find(
830
+ (property) =>
831
+ Node.isPropertyAssignment(property) && property.getName() === 'path',
832
+ );
833
+ const pathValue = Node.isPropertyAssignment(path)
834
+ ? path.getInitializer()
835
+ : undefined;
836
+ if (!Node.isStringLiteral(pathValue)) return undefined;
837
+ return {
838
+ path: pathValue.getLiteralValue(),
839
+ moduleSpecifiers: projectImportSpecifiers(element),
840
+ position: element.getStart(),
841
+ definition: element,
842
+ };
843
+ }
844
+
845
+ function routeHasComponent(
846
+ definition: import('ts-morph').ObjectLiteralExpression,
847
+ ): boolean {
848
+ if (
849
+ definition
850
+ .getProperties()
851
+ .some(
852
+ (property) =>
853
+ Node.isPropertyAssignment(property) &&
854
+ ['component', 'componentDeps', 'loadComponent'].includes(
855
+ property.getName(),
856
+ ),
857
+ )
858
+ ) {
859
+ return true;
860
+ }
861
+ return definition
862
+ .getDescendantsOfKind(SyntaxKind.CallExpression)
863
+ .some((call) => call.getExpression().getText() === 'loadCraftComponent');
864
+ }
865
+
866
+ function projectImportSpecifiers(node: import('ts-morph').Node): string[] {
867
+ return node
868
+ .getDescendantsOfKind(SyntaxKind.CallExpression)
869
+ .filter((call) => call.getExpression().getText() === 'import')
870
+ .map((call) => call.getArguments()[0])
871
+ .filter(
872
+ (argument): argument is import('ts-morph').StringLiteral =>
873
+ argument !== undefined && Node.isStringLiteral(argument),
874
+ )
875
+ .map((argument) => argument.getLiteralValue());
876
+ }
877
+
878
+ function hasRouteContext(check: string, path: string): boolean {
879
+ return [`path: "${path}"`, `path: '${path}'`].some((context) =>
880
+ check.includes(context),
881
+ );
882
+ }
883
+
884
+ async function runTypeCheck(
885
+ compiler: string,
886
+ context: ProjectContext,
887
+ log: (message: string) => void,
888
+ label: string,
889
+ ): Promise<{ ok: boolean; output: string }> {
890
+ const args = [
891
+ '--project',
892
+ context.tsConfigFilePath,
893
+ '--noEmit',
894
+ '--pretty',
895
+ 'false',
896
+ ];
897
+ return await new Promise((resolveResult) => {
898
+ const child = spawn(compiler, args, { cwd: context.rootDir });
899
+ let stdout = '';
900
+ let stderr = '';
901
+ let pendingLine = '';
902
+
903
+ const stream = (chunk: Buffer, target: 'stdout' | 'stderr') => {
904
+ const text = chunk.toString();
905
+ if (target === 'stdout') stdout += text;
906
+ else stderr += text;
907
+ pendingLine += text;
908
+ const lines = pendingLine.split(/\r?\n/);
909
+ pendingLine = lines.pop() ?? '';
910
+ for (const line of lines) {
911
+ if (line.trim()) log(`[route verify] ${label}: ${line}`);
912
+ }
913
+ };
914
+
915
+ child.stdout.on('data', (chunk: Buffer) => stream(chunk, 'stdout'));
916
+ child.stderr.on('data', (chunk: Buffer) => stream(chunk, 'stderr'));
917
+ child.on('error', (error) => {
918
+ resolveResult({
919
+ ok: false,
920
+ output: [stdout, stderr, error.message].filter(Boolean).join('\n'),
921
+ });
922
+ });
923
+ child.on('close', (code) => {
924
+ if (pendingLine.trim()) log(`[route verify] ${label}: ${pendingLine}`);
925
+ const output = [stdout, stderr].filter(Boolean).join('\n');
926
+ log(
927
+ `[route verify] ${label}: ${code === 0 ? 'passed' : `failed (exit ${code ?? 1})`}`,
928
+ );
929
+ resolveResult({ ok: code === 0, output });
930
+ });
931
+ });
932
+ }
933
+
934
+ async function listFiles(
935
+ directory: string,
936
+ predicate: (filePath: string) => boolean,
937
+ ): Promise<string[]> {
938
+ const result: string[] = [];
939
+ const entries = await readdir(directory, { withFileTypes: true });
940
+ for (const entry of entries) {
941
+ const filePath = join(directory, entry.name);
942
+ if (entry.isDirectory()) {
943
+ result.push(...(await listFiles(filePath, predicate)));
944
+ } else if (entry.isFile() && predicate(filePath)) {
945
+ result.push(filePath);
946
+ }
947
+ }
948
+ return result;
949
+ }
950
+
951
+ function findLocalBinary(rootDir: string, name: string): string | undefined {
952
+ let current = rootDir;
953
+ while (true) {
954
+ const candidate = join(current, 'node_modules', '.bin', name);
955
+ if (existsSync(candidate)) return candidate;
956
+ const parent = dirname(current);
957
+ if (parent === current) return undefined;
958
+ current = parent;
959
+ }
960
+ }
961
+
962
+ function commandOutput(value: unknown): string {
963
+ if (typeof value === 'object' && value !== null) {
964
+ const record = value as {
965
+ stdout?: unknown;
966
+ stderr?: unknown;
967
+ message?: unknown;
968
+ };
969
+ return [record.stdout, record.stderr, record.message]
970
+ .filter(
971
+ (part): part is string => typeof part === 'string' && part.length > 0,
972
+ )
973
+ .join('\n');
974
+ }
975
+ return String(value);
976
+ }
977
+
978
+ function failedResult(
979
+ context: ProjectContext,
980
+ message: string,
981
+ ): RouteVerificationResult {
982
+ return {
983
+ exitCode: 1,
984
+ ...context,
985
+ baseline: { status: 'failed' },
986
+ routeChecks: { status: 'skipped' },
987
+ projectLint: { status: 'skipped' },
988
+ eslint: { status: 'skipped' },
989
+ cases: [],
990
+ diagnostics: [message],
991
+ };
992
+ }
993
+
994
+ export function createRouteVerificationFixtures(): RouteVerificationFixture[] {
995
+ const support = `
996
+ import { craftService, type GetDeps, type GetServiceDependencies } from '@craft-ts/core';
997
+
998
+ export const { VerifyMissingService, provideVerifyMissingService } = craftService(
999
+ { name: '${serviceName}', providedIn: 'toProvide' },
1000
+ () => ({ value: 1 }),
1001
+ );
1002
+
1003
+ export type Router = { readonly __routeVerifierRouter: unique symbol };
1004
+
1005
+ export class VerifyComponent {}
1006
+ export class VerifyPendingComponent {}
1007
+ export class VerifyErrorComponent {}
1008
+
1009
+ export type MissingDeps = GetDeps<{
1010
+ deps: { ${serviceName}: GetServiceDependencies<typeof VerifyMissingService> };
1011
+ provided: {};
1012
+ missingProvider: { ${serviceName}: GetServiceDependencies<typeof VerifyMissingService> };
1013
+ publicProperties: {};
1014
+ }>;
1015
+
1016
+ export type ProvidedDeps = GetDeps<{
1017
+ deps: {};
1018
+ provided: { ${serviceName}: ReturnType<typeof provideVerifyMissingService> };
1019
+ publicProperties: {};
1020
+ }>;
1021
+
1022
+ export type InputDeps = GetDeps<{
1023
+ deps: {};
1024
+ provided: {};
1025
+ publicProperties: { verifyId: () => string };
1026
+ }>;
1027
+ `;
1028
+
1029
+ const validRoutes = `
1030
+ import type { Router } from './support';
1031
+ import {
1032
+ assertExhaustiveRouteExceptions,
1033
+ craftRoute,
1034
+ craftRoutes,
1035
+ type CanRun,
1036
+ type ValidateCascadeRoutesFile,
1037
+ } from '@craft-ts/core';
1038
+ import { ProvidedDeps } from './support';
1039
+
1040
+ export const { validRoutes } = craftRoutes('valid', [
1041
+ craftRoute('app-provider', {
1042
+ loadComponent: ({ withRetry }: import('@craft-ts/core').CraftRouteLazyLoadHelpers) => withRetry(import('./support')).then(({ VerifyComponent }) => VerifyComponent),
1043
+ componentDeps: {} as import('./support').MissingDeps,
1044
+ }),
1045
+ craftRoute('exceptions', {
1046
+ loadComponent: ({ withRetry }: import('@craft-ts/core').CraftRouteLazyLoadHelpers) => withRetry(import('./support')).then(({ VerifyComponent }) => VerifyComponent),
1047
+ componentDeps: {} as ProvidedDeps,
1048
+ }),
1049
+ ]);
1050
+
1051
+ assertExhaustiveRouteExceptions(validRoutes);
1052
+ type _CheckValidDI = ValidateCascadeRoutesFile<'${serviceName}', Router, typeof validRoutes>;
1053
+ type _CanRunValid = CanRun<_CheckValidDI>;
1054
+ `;
1055
+
1056
+ const lazyParent = `
1057
+ import type { Router } from './support';
1058
+ import { craftRoutes, type CanRun, type ValidateCascadeRoutesFile } from '@craft-ts/core';
1059
+
1060
+ export const { lazyParentRoutes } = craftRoutes('lazyParent', [
1061
+ {
1062
+ path: 'lazy',
1063
+ loadChildren: ({ withRetry }) =>
1064
+ withRetry(import('./lazy-child.routes')).then((module) => module.lazyChildRoutes),
1065
+ },
1066
+ ]);
1067
+
1068
+ type _CheckLazyParentDI = ValidateCascadeRoutesFile<never, Router, typeof lazyParentRoutes>;
1069
+ type _CanRunLazyParent = CanRun<_CheckLazyParentDI>;
1070
+ `;
1071
+
1072
+ const routeProvider = `
1073
+ import type { Router } from './support';
1074
+ import { craftRoute, craftRoutes, type CanRun, type RouteCheckedDI } from '@craft-ts/core';
1075
+ import { MissingDeps, provideVerifyMissingService } from './support';
1076
+
1077
+ export const { routeProviderRoutes } = craftRoutes('routeProvider', [
1078
+ craftRoute('provided', {
1079
+ loadComponent: ({ withRetry }: import('@craft-ts/core').CraftRouteLazyLoadHelpers) => withRetry(import('./support')).then(({ VerifyComponent }) => VerifyComponent),
1080
+ componentDeps: {} as MissingDeps,
1081
+ providers: [provideVerifyMissingService()],
1082
+ }),
1083
+ ]);
1084
+
1085
+ type _CheckRouteProviderDI = RouteCheckedDI<
1086
+ MissingDeps,
1087
+ 'VerifyMissingService',
1088
+ Router,
1089
+ 'path: "provided"'
1090
+ >;
1091
+ type _CanRunRouteProvider = CanRun<_CheckRouteProviderDI>;
1092
+ `;
1093
+
1094
+ const lazyChild = `
1095
+ import type { Router } from './support';
1096
+ import { craftRoute, craftRoutes, type CanRun, type ValidateCascadeRoutesFile } from '@craft-ts/core';
1097
+ import { ProvidedDeps } from './support';
1098
+
1099
+ export const { lazyChildRoutes } = craftRoutes('lazyChild', [
1100
+ craftRoute('page', {
1101
+ loadComponent: ({ withRetry }) => withRetry(import('./support')).then(({ VerifyComponent }) => VerifyComponent),
1102
+ componentDeps: {} as ProvidedDeps,
1103
+ }),
1104
+ ]);
1105
+
1106
+ type _CheckLazyChildDI = ValidateCascadeRoutesFile<never, Router, typeof lazyChildRoutes>;
1107
+ type _CanRunLazyChild = CanRun<_CheckLazyChildDI>;
1108
+ `;
1109
+
1110
+ const pending = `
1111
+ import type { Router } from './support';
1112
+ import { craftRoute, craftRoutes, type CanRun, type RouteCheckedDI, type ValidateCascadeRoutesFile } from '@craft-ts/core';
1113
+ import { ProvidedDeps } from './support';
1114
+
1115
+ export const { pendingRoutes } = craftRoutes('pending', [
1116
+ craftRoute('page', {
1117
+ loadComponent: ({ withRetry }) => withRetry(import('./support')).then(({ VerifyComponent }) => VerifyComponent),
1118
+ componentDeps: {} as ProvidedDeps,
1119
+ pendingComponent: () => import('./pending-component'),
1120
+ }),
1121
+ ]);
1122
+
1123
+ type _CheckPendingDI = ValidateCascadeRoutesFile<never, Router, typeof pendingRoutes>;
1124
+ type _CanRunPending = CanRun<_CheckPendingDI>;
1125
+ type _CheckPendingComponentDI = RouteCheckedDI<
1126
+ import('./pending-component').GenDeps_VerifyPendingComponent,
1127
+ never,
1128
+ Router,
1129
+ 'pending component: page'
1130
+ >;
1131
+ type _CanRunPendingComponent = CanRun<_CheckPendingComponentDI>;
1132
+ `;
1133
+
1134
+ const exceptionComponents = `
1135
+ import type { Router } from './support';
1136
+ import {
1137
+ RouteExceptionComponentCheckedDI,
1138
+ craftExceptionHandler,
1139
+ craftRoute,
1140
+ craftRoutes,
1141
+ withErrorComponent,
1142
+ withRouteLoadError,
1143
+ type CanRun,
1144
+ type ValidateCascadeRoutesFile,
1145
+ } from '@craft-ts/core';
1146
+ import { ProvidedDeps, VerifyErrorComponent } from './support';
1147
+
1148
+ export const { exceptionRoutes } = craftRoutes('exception', [
1149
+ craftRoute('page', {
1150
+ loadComponent: ({ withRetry }) => withRetry(import('./support')).then(({ VerifyComponent }) => VerifyComponent),
1151
+ componentDeps: {} as ProvidedDeps,
1152
+ errorComponent: {
1153
+ component: VerifyErrorComponent,
1154
+ componentDeps: {} as ProvidedDeps,
1155
+ },
1156
+ }),
1157
+ ]);
1158
+
1159
+ const globalError = withErrorComponent({
1160
+ component: VerifyErrorComponent,
1161
+ componentDeps: {} as ProvidedDeps,
1162
+ });
1163
+ const routeLoadError = withRouteLoadError({
1164
+ component: VerifyErrorComponent,
1165
+ componentDeps: {} as ProvidedDeps,
1166
+ });
1167
+ const renderedError = craftExceptionHandler(function* ({ renderComponent }) {
1168
+ return renderComponent({
1169
+ loadComponent: () => import('./support').then(({ VerifyErrorComponent }) => VerifyErrorComponent),
1170
+ componentDeps: {} as ProvidedDeps,
1171
+ });
1172
+ });
1173
+ void globalError;
1174
+ void routeLoadError;
1175
+ void renderedError;
1176
+
1177
+ type _CheckExceptionDI = ValidateCascadeRoutesFile<never, Router, typeof exceptionRoutes>;
1178
+ type _CanRunException = CanRun<_CheckExceptionDI>;
1179
+ type _CheckRouteErrorDI = RouteExceptionComponentCheckedDI<
1180
+ ProvidedDeps,
1181
+ 'CraftGlobalError',
1182
+ Router,
1183
+ 'error component: page'
1184
+ >;
1185
+ type _CanRunRouteError = CanRun<_CheckRouteErrorDI>;
1186
+ `;
1187
+
1188
+ const templatePipe = `
1189
+ import type { GetDeps } from '@craft-ts/core';
1190
+
1191
+ export class VerifyTemplatePipe {
1192
+ transform(value: string): string {
1193
+ return value;
1194
+ }
1195
+ }
1196
+
1197
+ export type GenDeps_VerifyTemplatePipe = GetDeps<{
1198
+ deps: {};
1199
+ provided: {};
1200
+ publicProperties: {};
1201
+ }>;
1202
+ `;
1203
+
1204
+ const templateComponent = `
1205
+ /* eslint-disable @typescript-eslint/no-empty-object-type */
1206
+ import type { GetDeps, GetServiceDependencies } from '@craft-ts/core';
1207
+ import { VerifyTemplatePipe } from './template-pipe';
1208
+ import { VerifyMissingService } from './support';
1209
+
1210
+ export class TemplateMissingComponent {
1211
+ readonly value = 'verify';
1212
+ }
1213
+
1214
+ export type GenDeps_TemplateMissingComponent = GetDeps<{
1215
+ deps: { VerifyMissingService: GetServiceDependencies<typeof VerifyMissingService> };
1216
+ provided: {};
1217
+ missingProvider: { VerifyMissingService: GetServiceDependencies<typeof VerifyMissingService> };
1218
+ publicProperties: {};
1219
+ }>;
1220
+ `;
1221
+
1222
+ const templateRoutes = `
1223
+ import type { Router } from './support';
1224
+ import { craftRoute, craftRoutes, type CanRun, type RouteCheckedDI } from '@craft-ts/core';
1225
+
1226
+ export const { templateRoutes } = craftRoutes('template', [
1227
+ craftRoute('missing-template-provider', {
1228
+ loadComponent: ({ withRetry }) => withRetry(import('./template-missing.component')).then(({ TemplateMissingComponent }) => TemplateMissingComponent),
1229
+ componentDeps: {} as import('./template-missing.component').GenDeps_TemplateMissingComponent,
1230
+ }),
1231
+ ]);
1232
+
1233
+ type _CheckTemplateDI = RouteCheckedDI<
1234
+ import('./template-missing.component').GenDeps_TemplateMissingComponent,
1235
+ never,
1236
+ Router,
1237
+ 'path: "missing-template-provider"'
1238
+ >;
1239
+ type _CanRunTemplate = CanRun<_CheckTemplateDI>;
1240
+ `;
1241
+
1242
+ const missingProvider = `
1243
+ import type { Router } from './support';
1244
+ import { craftRoute, craftRoutes, type CanRun, type ValidateCascadeRoutesFile } from '@craft-ts/core';
1245
+ import { MissingDeps } from './support';
1246
+
1247
+ export const { missingProviderRoutes } = craftRoutes('missingProvider', [
1248
+ craftRoute('missing-provider', {
1249
+ loadComponent: ({ withRetry }) => withRetry(import('./support')).then(({ VerifyComponent }) => VerifyComponent),
1250
+ componentDeps: {} as MissingDeps,
1251
+ }),
1252
+ ]);
1253
+
1254
+ type _CheckMissingProviderDI = ValidateCascadeRoutesFile<never, Router, typeof missingProviderRoutes>;
1255
+ type _CanRunMissingProvider = CanRun<_CheckMissingProviderDI>;
1256
+ `;
1257
+
1258
+ const missingInput = `
1259
+ import type { Router } from './support';
1260
+ import { craftRoute, craftRoutes, type CanRun, type ValidateCascadeRoutesFile } from '@craft-ts/core';
1261
+ import { InputDeps } from './support';
1262
+
1263
+ export const { missingInputRoutes } = craftRoutes('missingInput', [
1264
+ craftRoute('missing-input', {
1265
+ loadComponent: ({ withRetry }) => withRetry(import('./support')).then(({ VerifyComponent }) => VerifyComponent),
1266
+ componentDeps: {} as InputDeps,
1267
+ }),
1268
+ ]);
1269
+
1270
+ type _CheckMissingInputDI = ValidateCascadeRoutesFile<never, Router, typeof missingInputRoutes>;
1271
+ type _CanRunMissingInput = CanRun<_CheckMissingInputDI>;
1272
+ `;
1273
+
1274
+ const pendingMissing = `
1275
+ import type { Router } from './support';
1276
+ import { craftRoute, craftRoutes, type CanRun, type RouteCheckedDI, type ValidateCascadeRoutesFile } from '@craft-ts/core';
1277
+ import { ProvidedDeps } from './support';
1278
+
1279
+ export const { pendingMissingRoutes } = craftRoutes('pendingMissing', [
1280
+ craftRoute('pending-missing', {
1281
+ loadComponent: ({ withRetry }) => withRetry(import('./support')).then(({ VerifyComponent }) => VerifyComponent),
1282
+ componentDeps: {} as ProvidedDeps,
1283
+ pendingComponent: () => import('./pending-missing-component'),
1284
+ }),
1285
+ ]);
1286
+
1287
+ type _CheckPendingMissingDI = ValidateCascadeRoutesFile<never, Router, typeof pendingMissingRoutes>;
1288
+ type _CanRunPendingMissing = CanRun<_CheckPendingMissingDI>;
1289
+ type _CheckPendingMissingComponentDI = RouteCheckedDI<
1290
+ import('./pending-missing-component').GenDeps_VerifyPendingMissingComponent,
1291
+ never,
1292
+ Router,
1293
+ 'pending component: pending-missing'
1294
+ >;
1295
+ type _CanRunPendingMissingComponent = CanRun<_CheckPendingMissingComponentDI>;
1296
+ `;
1297
+
1298
+ const exceptionMissing = `
1299
+ import type { Router } from './support';
1300
+ import { RouteExceptionComponentCheckedDI, type CanRun } from '@craft-ts/core';
1301
+ import { MissingDeps } from './support';
1302
+
1303
+ type _CheckExceptionComponentDI = RouteExceptionComponentCheckedDI<
1304
+ MissingDeps,
1305
+ never,
1306
+ Router,
1307
+ 'exception component: missing-render'
1308
+ >;
1309
+ type _CanRunExceptionComponent = CanRun<_CheckExceptionComponentDI>;
1310
+ `;
1311
+
1312
+ const exceptionMissingRoutes = `
1313
+ import type { Router } from './support';
1314
+ import { assertExhaustiveRouteExceptions, craftException, craftGen, craftRoute, craftRoutes, type CanRun, type ValidateCascadeRoutesFile } from '@craft-ts/core';
1315
+ import { ProvidedDeps } from './support';
1316
+
1317
+ const verifyFailure = craftGen(function* () {
1318
+ return craftException({ _tag: 'VERIFY_UNHANDLED' });
1319
+ });
1320
+
1321
+ export const { exceptionMissingRoutes } = craftRoutes('exceptionMissing', [
1322
+ craftRoute('missing-handler', {
1323
+ loadComponent: ({ withRetry }) => withRetry(import('./support')).then(({ VerifyComponent }) => VerifyComponent),
1324
+ componentDeps: {} as ProvidedDeps,
1325
+ canActivate: function* () {
1326
+ return yield* verifyFailure();
1327
+ },
1328
+ }),
1329
+ ]);
1330
+
1331
+ assertExhaustiveRouteExceptions(exceptionMissingRoutes);
1332
+ type _CheckExceptionMissingDI = ValidateCascadeRoutesFile<never, Router, typeof exceptionMissingRoutes>;
1333
+ type _CanRunExceptionMissing = CanRun<_CheckExceptionMissingDI>;
1334
+ `;
1335
+
1336
+ const exceptionExtraRoutes = `
1337
+ import type { Router } from './support';
1338
+ import { assertExhaustiveRouteExceptions, craftException, craftExceptionHandler, craftGen, craftRoute, craftRoutes, type CanRun, type ValidateCascadeRoutesFile } from '@craft-ts/core';
1339
+ import { ProvidedDeps } from './support';
1340
+
1341
+ const verifyFailure = craftGen(function* () {
1342
+ return craftException({ _tag: 'VERIFY_HANDLED' });
1343
+ });
1344
+
1345
+ export const { exceptionExtraRoutes } = craftRoutes('exceptionExtra', [
1346
+ craftRoute('extra-handler', {
1347
+ loadComponent: ({ withRetry }: import('@craft-ts/core').CraftRouteLazyLoadHelpers) => withRetry(import('./support')).then(({ VerifyComponent }) => VerifyComponent),
1348
+ componentDeps: {} as ProvidedDeps,
1349
+ canActivate: function* () {
1350
+ return yield* verifyFailure();
1351
+ },
1352
+ }, {
1353
+ VERIFY_HANDLED: craftExceptionHandler(function* ({ noop }) {
1354
+ return noop();
1355
+ }),
1356
+ VERIFY_EXTRA: craftExceptionHandler(function* ({ noop }) {
1357
+ return noop();
1358
+ }),
1359
+ }),
1360
+ ]);
1361
+
1362
+ assertExhaustiveRouteExceptions(exceptionExtraRoutes);
1363
+ type _CheckExceptionExtraDI = ValidateCascadeRoutesFile<never, Router, typeof exceptionExtraRoutes>;
1364
+ type _CanRunExceptionExtra = CanRun<_CheckExceptionExtraDI>;
1365
+ `;
1366
+
1367
+ const pendingComponent = `
1368
+ import { GetDeps, GetServiceDependencies } from '@craft-ts/core';
1369
+
1370
+ export default class VerifyPendingComponent {}
1371
+ export type GenDeps_VerifyPendingComponent = GetDeps<{
1372
+ deps: {};
1373
+ provided: {};
1374
+ publicProperties: {};
1375
+ }>;
1376
+ `;
1377
+
1378
+ const pendingMissingComponent = `
1379
+ import type { GetDeps, GetServiceDependencies } from '@craft-ts/core';
1380
+
1381
+ export default class VerifyPendingMissingComponent {}
1382
+ export type GenDeps_VerifyPendingMissingComponent = GetDeps<{
1383
+ deps: { ${serviceName}: GetServiceDependencies<typeof import('./support').VerifyMissingService> };
1384
+ provided: {};
1385
+ missingProvider: { ${serviceName}: GetServiceDependencies<typeof import('./support').VerifyMissingService> };
1386
+ publicProperties: {};
1387
+ }>;
1388
+ `;
1389
+
1390
+ return [
1391
+ fixture('support', 'support.ts', support, [], 'positive'),
1392
+ fixture('valid-routes', 'valid.routes.ts', validRoutes, [], 'positive'),
1393
+ fixture(
1394
+ 'route-provider',
1395
+ 'route-provider.routes.ts',
1396
+ routeProvider,
1397
+ [],
1398
+ 'positive',
1399
+ ),
1400
+ fixture('lazy-parent', 'lazy-parent.routes.ts', lazyParent, [], 'positive'),
1401
+ fixture('lazy-child', 'lazy-child.routes.ts', lazyChild, [], 'positive'),
1402
+ fixture('pending', 'pending.routes.ts', pending, [], 'positive'),
1403
+ fixture(
1404
+ 'exception-components',
1405
+ 'exception-components.routes.ts',
1406
+ exceptionComponents,
1407
+ [],
1408
+ 'positive',
1409
+ ),
1410
+ fixture('template-pipe', 'template-pipe.ts', templatePipe, [], 'positive'),
1411
+ fixture(
1412
+ 'template-component',
1413
+ 'template-missing.component.ts',
1414
+ templateComponent,
1415
+ [],
1416
+ 'positive',
1417
+ ),
1418
+ fixture(
1419
+ 'template-routes',
1420
+ 'template.routes.ts',
1421
+ templateRoutes,
1422
+ [
1423
+ `The ${serviceName} service is not provided in path: "missing-template-provider"`,
1424
+ ],
1425
+ 'negative',
1426
+ true,
1427
+ ),
1428
+ fixture(
1429
+ 'missing-provider',
1430
+ 'missing-provider.routes.ts',
1431
+ missingProvider,
1432
+ [`The ${serviceName} service is not provided in ${routeContext}`],
1433
+ 'negative',
1434
+ ),
1435
+ fixture(
1436
+ 'missing-input',
1437
+ 'missing-input.routes.ts',
1438
+ missingInput,
1439
+ ['Input "verifyId" is not provided in path: "missing-input"'],
1440
+ 'negative',
1441
+ ),
1442
+ fixture(
1443
+ 'pending-missing',
1444
+ 'pending-missing.routes.ts',
1445
+ pendingMissing,
1446
+ [
1447
+ `The ${serviceName} service is not provided in pending component: pending-missing`,
1448
+ ],
1449
+ 'negative',
1450
+ ),
1451
+ fixture(
1452
+ 'exception-component-missing',
1453
+ 'exception-component-missing.routes.ts',
1454
+ exceptionMissing,
1455
+ [
1456
+ `The ${serviceName} service is not provided in exception component: missing-render`,
1457
+ ],
1458
+ 'negative',
1459
+ ),
1460
+ fixture(
1461
+ 'exception-missing-handler',
1462
+ 'exception-missing-handler.routes.ts',
1463
+ exceptionMissingRoutes,
1464
+ ['VERIFY_UNHANDLED', 'missingHandlers'],
1465
+ 'negative',
1466
+ ),
1467
+ fixture(
1468
+ 'exception-extra-handler',
1469
+ 'exception-extra-handler.routes.ts',
1470
+ exceptionExtraRoutes,
1471
+ ['VERIFY_EXTRA', 'ERROR_unhandled_or_extra_route_exceptions'],
1472
+ 'negative',
1473
+ ),
1474
+ fixture(
1475
+ 'pending-component',
1476
+ 'pending-component.ts',
1477
+ pendingComponent,
1478
+ [],
1479
+ 'positive',
1480
+ ),
1481
+ fixture(
1482
+ 'pending-missing-component',
1483
+ 'pending-missing-component.ts',
1484
+ pendingMissingComponent,
1485
+ [],
1486
+ 'positive',
1487
+ ),
1488
+ ];
1489
+ }
1490
+
1491
+ function fixture(
1492
+ id: string,
1493
+ fileName: string,
1494
+ source: string,
1495
+ expected: string[],
1496
+ kind: 'positive' | 'negative',
1497
+ template = false,
1498
+ ): RouteVerificationFixture {
1499
+ return {
1500
+ id,
1501
+ category: template
1502
+ ? 'template'
1503
+ : id.includes('exception')
1504
+ ? 'exceptions'
1505
+ : 'routing',
1506
+ fileName,
1507
+ source,
1508
+ expected,
1509
+ kind,
1510
+ };
1511
+ }