@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,490 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const { IndentationText, Node, Project, QuoteKind, SyntaxKind } = require('ts-morph');
4
+
5
+ const projectCache = new Map();
6
+
7
+ const ROUTES_FACTORY = 'craftRoutes';
8
+ const ROUTE_FN = 'craftRoute';
9
+ const PENDING_PROP = 'pendingComponent';
10
+ const VT_PROP = 'withLoaderViewTransitionImage';
11
+ const CHECK_TYPE = 'RouteCheckedDI';
12
+ const CASCADE_TYPE = 'ValidateCascadeRoutesFile';
13
+ const CAN_RUN = 'CanRun';
14
+
15
+ /**
16
+ * Ensures every route with a lazy `pendingComponent: () => import('./x')` is
17
+ * verified with the per-component, O(1) `RouteCheckedDI` check (setup.md
18
+ * "Escape hatch"). The aggregated cascade never sees the pending component, so
19
+ * its DI must be checked directly — and `RouteCheckedDI` is used (not a second
20
+ * aggregated pass) to stay off the instantiation-count budget.
21
+ *
22
+ * Autofix generates the whole block:
23
+ *
24
+ * type _Check<Feature>PendingDI = RouteCheckedDI<
25
+ * import('./x').GenDeps_<DefaultExportClass>,
26
+ * '<CollectionParam>Params' | '<Collection><Route>ViewTransition', // route auto-provides these
27
+ * <ParentValues>, // borrowed from the cascade check
28
+ * 'pending component: <path>'
29
+ * >;
30
+ * type _CanRun<Feature>Pending = CanRun<_Check<Feature>PendingDI>;
31
+ *
32
+ * The `GenDeps_*` alias is read from the skeleton module, the available service
33
+ * names are derived from the route's path params + (when present) its
34
+ * `withLoaderViewTransitionImage` payload, and the parent context is taken from
35
+ * the collection's own `ValidateCascadeRoutesFile<…, typeof xRoutes>`. If that
36
+ * cascade check is absent (so the context can't be inferred), the rule reports
37
+ * without a fix.
38
+ */
39
+ module.exports = {
40
+ meta: {
41
+ type: 'problem',
42
+ docs: {
43
+ description:
44
+ 'Ensure every route with a lazy pendingComponent is verified with RouteCheckedDI(...).',
45
+ },
46
+ fixable: 'code',
47
+ schema: [],
48
+ },
49
+ create(context) {
50
+ return {
51
+ 'Program:exit'() {
52
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
53
+ const filePath = getFilePath(context);
54
+ if (!filePath || !filePath.endsWith('.ts')) {
55
+ return;
56
+ }
57
+
58
+ const text = sourceCode.getText();
59
+ if (!text.includes(PENDING_PROP)) {
60
+ return;
61
+ }
62
+
63
+ const sourceFile = getProjectSourceFile(
64
+ getProject(getCwd(context)),
65
+ filePath,
66
+ text,
67
+ );
68
+
69
+ const collections = collectCollections(sourceFile, filePath);
70
+ const pendingRoutes = collections.flatMap((c) => c.pendingRoutes);
71
+ if (pendingRoutes.length === 0) {
72
+ return;
73
+ }
74
+
75
+ const checkedFirstArgs = collectCheckedGenDeps(sourceFile);
76
+ const cascadeContexts = collectCascadeContexts(sourceFile);
77
+
78
+ const issues = pendingRoutes.filter(
79
+ (route) => !checkedFirstArgs.has(normalize(route.genDepsRef)),
80
+ );
81
+ if (issues.length === 0) {
82
+ return;
83
+ }
84
+
85
+ const reportLoc = getNodeLoc(sourceCode, issues[0].reportNode);
86
+
87
+ let fixable = true;
88
+ for (const issue of issues) {
89
+ const ctx = cascadeContexts.get(`typeof ${issue.routesName}`);
90
+ if (!ctx) {
91
+ fixable = false;
92
+ continue;
93
+ }
94
+ appendCheck(sourceFile, issue, ctx);
95
+ }
96
+
97
+ if (fixable) {
98
+ ensureImports(sourceFile, [CHECK_TYPE, CAN_RUN]);
99
+ }
100
+
101
+ const fixedText = sourceFile.getFullText();
102
+ const message = `craftRoute(s) with a pendingComponent must be verified with ${CHECK_TYPE}(): ${issues
103
+ .map((i) => i.path)
104
+ .join(', ')}`;
105
+
106
+ if (!fixable || fixedText === text) {
107
+ context.report({ loc: reportLoc, message });
108
+ return;
109
+ }
110
+
111
+ context.report({
112
+ loc: reportLoc,
113
+ message,
114
+ fix(fixer) {
115
+ return fixer.replaceTextRange([0, text.length], fixedText);
116
+ },
117
+ });
118
+ },
119
+ };
120
+ },
121
+ };
122
+
123
+ function collectCollections(sourceFile, filePath) {
124
+ const collections = [];
125
+
126
+ for (const call of sourceFile.getDescendantsOfKind(
127
+ SyntaxKind.CallExpression,
128
+ )) {
129
+ if (call.getExpression().getText() !== ROUTES_FACTORY) {
130
+ continue;
131
+ }
132
+
133
+ const collectionName = getStringArg(call, 0);
134
+ const routesArray = call.getArguments()[1];
135
+ if (!collectionName || !routesArray) {
136
+ continue;
137
+ }
138
+
139
+ const routesName = getRoutesBindingName(call);
140
+ if (!routesName) {
141
+ continue;
142
+ }
143
+
144
+ const pendingRoutes = [];
145
+ for (const element of getArrayElements(routesArray)) {
146
+ const route = readRoute(element);
147
+ if (!route) {
148
+ continue;
149
+ }
150
+
151
+ const importSpecifier = getLazyImportSpecifier(route.object, PENDING_PROP);
152
+ if (!importSpecifier) {
153
+ continue;
154
+ }
155
+
156
+ const genDepsName = resolveGenDepsName(filePath, importSpecifier);
157
+ if (!genDepsName) {
158
+ continue;
159
+ }
160
+
161
+ pendingRoutes.push({
162
+ routesName,
163
+ path: route.path,
164
+ genDepsRef: `import('${importSpecifier}').${genDepsName}`,
165
+ availableNames: deriveAvailableNames(
166
+ collectionName,
167
+ route.path,
168
+ hasProperty(route.object, VT_PROP),
169
+ ),
170
+ suffix: toPascalCase(routesName.replace(/Routes$/, '')),
171
+ reportNode: getPropertyAssignment(route.object, PENDING_PROP),
172
+ });
173
+ }
174
+
175
+ collections.push({ collectionName, routesName, pendingRoutes });
176
+ }
177
+
178
+ return collections;
179
+ }
180
+
181
+ // A route element is either `craftRoute('<path>', { … })` or a `{ path: '<path>', … }`
182
+ // object literal. Returns the path string and the route's object literal.
183
+ function readRoute(element) {
184
+ if (Node.isCallExpression(element) && element.getExpression().getText() === ROUTE_FN) {
185
+ const pathArg = getStringArg(element, 0);
186
+ const object = element.getArguments()[1];
187
+ if (pathArg !== undefined && object && Node.isObjectLiteralExpression(object)) {
188
+ return { path: pathArg, object };
189
+ }
190
+ return undefined;
191
+ }
192
+
193
+ if (Node.isObjectLiteralExpression(element)) {
194
+ const pathProp = getPropertyAssignment(element, 'path');
195
+ const initializer = pathProp?.getInitializer();
196
+ if (initializer && Node.isStringLiteral(initializer)) {
197
+ return { path: initializer.getLiteralText(), object: element };
198
+ }
199
+ }
200
+
201
+ return undefined;
202
+ }
203
+
204
+ // `${Collection}${Param}Params` for each `:param`, plus
205
+ // `${Collection}${RouteBase}ViewTransition` when the route declares a payload.
206
+ function deriveAvailableNames(collectionName, routePath, hasViewTransition) {
207
+ const collection = toPascalCase(collectionName);
208
+ const names = [];
209
+
210
+ for (const segment of routePath.split('/')) {
211
+ if (segment.startsWith(':')) {
212
+ const param = segment.slice(1).replace(/\?$/, '');
213
+ names.push(`${collection}${toPascalCase(param)}Params`);
214
+ }
215
+ }
216
+
217
+ if (hasViewTransition) {
218
+ names.push(`${collection}${routeBaseServiceName(routePath)}ViewTransition`);
219
+ }
220
+
221
+ return names;
222
+ }
223
+
224
+ function routeBaseServiceName(routePath) {
225
+ const name = routePath
226
+ .split('/')
227
+ .filter(Boolean)
228
+ .map((segment) =>
229
+ segment === '**'
230
+ ? 'Wildcard'
231
+ : toPascalCase(segment.replace(/^:/, '').replace(/\?$/, '')),
232
+ )
233
+ .join('');
234
+ return name || 'Root';
235
+ }
236
+
237
+ function getArrayElements(node) {
238
+ return Node.isArrayLiteralExpression(node) ? node.getElements() : [];
239
+ }
240
+
241
+ function getStringArg(call, index) {
242
+ const arg = call.getArguments()[index];
243
+ return arg && Node.isStringLiteral(arg) ? arg.getLiteralText() : undefined;
244
+ }
245
+
246
+ function getPropertyAssignment(objectLiteral, name) {
247
+ if (!Node.isObjectLiteralExpression(objectLiteral)) {
248
+ return undefined;
249
+ }
250
+ const property = objectLiteral
251
+ .getProperties()
252
+ .find(
253
+ (candidate) =>
254
+ Node.isPropertyAssignment(candidate) && candidate.getName() === name,
255
+ );
256
+ return Node.isPropertyAssignment(property) ? property : undefined;
257
+ }
258
+
259
+ function hasProperty(objectLiteral, name) {
260
+ return getPropertyAssignment(objectLiteral, name) !== undefined;
261
+ }
262
+
263
+ function getLazyImportSpecifier(objectLiteral, propertyName) {
264
+ const property = getPropertyAssignment(objectLiteral, propertyName);
265
+ const initializer = property?.getInitializer();
266
+ if (!initializer || !Node.isArrowFunction(initializer)) {
267
+ return undefined;
268
+ }
269
+
270
+ const body = initializer.getBody();
271
+ const callExpression = Node.isCallExpression(body) ? body : undefined;
272
+ if (
273
+ !callExpression ||
274
+ callExpression.getExpression().getKind() !== SyntaxKind.ImportKeyword
275
+ ) {
276
+ return undefined;
277
+ }
278
+
279
+ const [argument] = callExpression.getArguments();
280
+ return argument && Node.isStringLiteral(argument)
281
+ ? argument.getLiteralText()
282
+ : undefined;
283
+ }
284
+
285
+ function getRoutesBindingName(call) {
286
+ const declaration = call.getFirstAncestorByKind(
287
+ SyntaxKind.VariableDeclaration,
288
+ );
289
+ const nameNode = declaration?.getNameNode();
290
+ if (!nameNode || nameNode.getKind() !== SyntaxKind.ObjectBindingPattern) {
291
+ return undefined;
292
+ }
293
+
294
+ const elements = nameNode.getElements();
295
+ const expected =
296
+ getStringArg(call, 0) !== undefined ? `${getStringArg(call, 0)}Routes` : undefined;
297
+ if (expected) {
298
+ const match = elements.find(
299
+ (element) =>
300
+ (element.getPropertyNameNode()?.getText() ?? element.getName()) ===
301
+ expected,
302
+ );
303
+ if (match) {
304
+ return match.getName();
305
+ }
306
+ }
307
+
308
+ const routesElements = elements.filter((element) =>
309
+ (element.getPropertyNameNode()?.getText() ?? element.getName()).endsWith(
310
+ 'Routes',
311
+ ),
312
+ );
313
+ return routesElements.length === 1 ? routesElements[0].getName() : undefined;
314
+ }
315
+
316
+ // First type-argument text of every `RouteCheckedDI<First, …>` already present.
317
+ function collectCheckedGenDeps(sourceFile) {
318
+ const checked = new Set();
319
+ for (const reference of sourceFile.getDescendantsOfKind(
320
+ SyntaxKind.TypeReference,
321
+ )) {
322
+ if (reference.getTypeName().getText() !== CHECK_TYPE) {
323
+ continue;
324
+ }
325
+ const first = reference.getTypeArguments()[0];
326
+ if (first) {
327
+ checked.add(normalize(first.getText()));
328
+ }
329
+ }
330
+ return checked;
331
+ }
332
+
333
+ // `typeof xRoutes` -> "ParentNames, ParentValues" from the cascade check.
334
+ function collectCascadeContexts(sourceFile) {
335
+ const contexts = new Map();
336
+ for (const reference of sourceFile.getDescendantsOfKind(
337
+ SyntaxKind.TypeReference,
338
+ )) {
339
+ if (reference.getTypeName().getText() !== CASCADE_TYPE) {
340
+ continue;
341
+ }
342
+ const args = reference.getTypeArguments();
343
+ if (args.length >= 3) {
344
+ contexts.set(args[2].getText(), {
345
+ names: args[0].getText(),
346
+ values: args[1].getText(),
347
+ });
348
+ }
349
+ }
350
+ return contexts;
351
+ }
352
+
353
+ function appendCheck(sourceFile, issue, parentContext) {
354
+ const quoted = issue.availableNames.map((name) => `'${name}'`);
355
+ const available =
356
+ parentContext.names === 'never'
357
+ ? quoted.join(' | ') || 'never'
358
+ : [parentContext.names, ...quoted].join(' | ');
359
+
360
+ sourceFile.addStatements((writer) => {
361
+ writer.blankLine();
362
+ writer.writeLine(`type _Check${issue.suffix}PendingDI = ${CHECK_TYPE}<`);
363
+ writer.writeLine(` ${issue.genDepsRef},`);
364
+ writer.writeLine(` ${available},`);
365
+ writer.writeLine(` ${parentContext.values},`);
366
+ writer.writeLine(` 'pending component: ${issue.path}'`);
367
+ writer.writeLine('>;');
368
+ writer.writeLine(
369
+ `type _CanRun${issue.suffix}Pending = ${CAN_RUN}<_Check${issue.suffix}PendingDI>;`,
370
+ );
371
+ });
372
+ }
373
+
374
+ function resolveGenDepsName(fromFilePath, importSpecifier) {
375
+ if (!importSpecifier.startsWith('.')) {
376
+ return undefined;
377
+ }
378
+ const resolvedBase = path.resolve(path.dirname(fromFilePath), importSpecifier);
379
+ const candidates = [
380
+ `${resolvedBase}.ts`,
381
+ `${resolvedBase}.tsx`,
382
+ path.join(resolvedBase, 'index.ts'),
383
+ ];
384
+ const targetPath = candidates.find((candidate) => fs.existsSync(candidate));
385
+ if (!targetPath) {
386
+ return undefined;
387
+ }
388
+
389
+ const targetText = fs.readFileSync(targetPath, 'utf8');
390
+ const names = [...targetText.matchAll(/export\s+type\s+(GenDeps_\w+)/g)].map(
391
+ (match) => match[1],
392
+ );
393
+ if (names.length === 0) {
394
+ return undefined;
395
+ }
396
+
397
+ const defaultClass = targetText.match(/export\s+default\s+class\s+(\w+)/);
398
+ if (defaultClass) {
399
+ const preferred = `GenDeps_${defaultClass[1]}`;
400
+ if (names.includes(preferred)) {
401
+ return preferred;
402
+ }
403
+ }
404
+ return names[0];
405
+ }
406
+
407
+ function ensureImports(sourceFile, names) {
408
+ const craftImport = sourceFile
409
+ .getImportDeclarations()
410
+ .find((imp) => imp.getModuleSpecifierValue() === '@craft-ts/core');
411
+ if (!craftImport) {
412
+ sourceFile.addImportDeclaration({
413
+ moduleSpecifier: '@craft-ts/core',
414
+ namedImports: names.map((name) => ({ name, isTypeOnly: true })),
415
+ });
416
+ return;
417
+ }
418
+ const existing = new Set(
419
+ craftImport.getNamedImports().map((ni) => ni.getName()),
420
+ );
421
+ for (const name of names) {
422
+ if (!existing.has(name)) {
423
+ craftImport.addNamedImport({ name, isTypeOnly: true });
424
+ }
425
+ }
426
+ }
427
+
428
+ function getProject(cwd) {
429
+ let project = projectCache.get(cwd);
430
+ if (project) {
431
+ return project;
432
+ }
433
+ const manipulationSettings = {
434
+ indentationText: IndentationText.TwoSpaces,
435
+ quoteKind: QuoteKind.Single,
436
+ };
437
+ const tsConfigFilePath = path.join(cwd, 'tsconfig.json');
438
+ project = fs.existsSync(tsConfigFilePath)
439
+ ? new Project({ tsConfigFilePath, manipulationSettings })
440
+ : new Project({
441
+ compilerOptions: { experimentalDecorators: true, target: 9 },
442
+ manipulationSettings,
443
+ });
444
+ projectCache.set(cwd, project);
445
+ return project;
446
+ }
447
+
448
+ function getProjectSourceFile(project, filePath, text) {
449
+ const normalizedPath = path.resolve(filePath);
450
+ const existing = project.getSourceFile(normalizedPath);
451
+ if (existing) {
452
+ existing.replaceWithText(text);
453
+ return existing;
454
+ }
455
+ const added = project.addSourceFileAtPathIfExists(normalizedPath);
456
+ if (added) {
457
+ added.replaceWithText(text);
458
+ return added;
459
+ }
460
+ return project.createSourceFile(normalizedPath, text, { overwrite: true });
461
+ }
462
+
463
+ function getFilePath(context) {
464
+ const filePath = context.filename ?? context.getFilename();
465
+ return !filePath || filePath === '<input>' ? undefined : filePath;
466
+ }
467
+
468
+ function getCwd(context) {
469
+ return context.cwd ?? process.cwd();
470
+ }
471
+
472
+ function getNodeLoc(sourceCode, node) {
473
+ return {
474
+ start: sourceCode.getLocFromIndex(node.getStart()),
475
+ end: sourceCode.getLocFromIndex(node.getEnd()),
476
+ };
477
+ }
478
+
479
+ function toPascalCase(value) {
480
+ return value
481
+ .replace(/^:/, '')
482
+ .split(/[^a-zA-Z0-9]+/)
483
+ .filter(Boolean)
484
+ .map((segment) => segment[0].toUpperCase() + segment.slice(1))
485
+ .join('');
486
+ }
487
+
488
+ function normalize(text) {
489
+ return text.replace(/\s+/g, ' ').trim();
490
+ }
@@ -0,0 +1,156 @@
1
+ const PRIMITIVES = new Set([
2
+ 'asyncProcess',
3
+ 'craftPipe',
4
+ 'mutation',
5
+ 'query',
6
+ 'queryParam',
7
+ 'queryParams',
8
+ 'state',
9
+ ]);
10
+
11
+ const ALLOWED_HOSTS = new Set([
12
+ 'craftComponent',
13
+ 'craftGen',
14
+ 'craftRoute',
15
+ 'craftRoutes',
16
+ 'craftService',
17
+ 'toCraftService',
18
+ ]);
19
+
20
+ const CORE_IMPORTS = new Set([
21
+ 'asyncProcess',
22
+ 'craftGen',
23
+ 'craftPipe',
24
+ 'craftRoute',
25
+ 'craftRoutes',
26
+ 'craftService',
27
+ 'mutation',
28
+ 'query',
29
+ 'queryParam',
30
+ 'queryParams',
31
+ 'state',
32
+ 'toCraftService',
33
+ ]);
34
+
35
+ const COMPONENT_IMPORTS = new Set(['craftComponent']);
36
+ const CORE_MODULES = new Set(['@craft-ts/core']);
37
+ const COMPONENT_MODULES = new Set(['@craft-ts/component']);
38
+
39
+ module.exports = {
40
+ meta: {
41
+ type: 'problem',
42
+ docs: {
43
+ description:
44
+ 'Require Craft primitive creation calls to run inside an Angular-aware Craft host.',
45
+ },
46
+ schema: [],
47
+ messages: {
48
+ primitiveContext:
49
+ "'{{name}}(...)' must be created inside craftComponent, craftService, craftGen, craftRoute, or craftRoutes; module-level primitive creation can fail without an injection context.",
50
+ },
51
+ },
52
+ create(context) {
53
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
54
+
55
+ return {
56
+ CallExpression(node) {
57
+ if (
58
+ node.callee.type !== 'Identifier' ||
59
+ node.arguments.length === 0 ||
60
+ !PRIMITIVES.has(node.callee.name) ||
61
+ !resolvesToImportedName(
62
+ node.callee,
63
+ sourceCode,
64
+ CORE_IMPORTS,
65
+ CORE_MODULES,
66
+ ) ||
67
+ isInsideAllowedHost(node, sourceCode)
68
+ ) {
69
+ return;
70
+ }
71
+
72
+ context.report({
73
+ node,
74
+ messageId: 'primitiveContext',
75
+ data: { name: node.callee.name },
76
+ });
77
+ },
78
+ };
79
+ },
80
+ };
81
+
82
+ function isInsideAllowedHost(node, sourceCode) {
83
+ let current = node.parent;
84
+ while (current) {
85
+ if (
86
+ current.type === 'CallExpression' &&
87
+ current.callee.type === 'Identifier' &&
88
+ isAllowedHostImport(current.callee, sourceCode)
89
+ ) {
90
+ return true;
91
+ }
92
+ current = current.parent;
93
+ }
94
+ return false;
95
+ }
96
+
97
+ function isAllowedHostImport(identifier, sourceCode) {
98
+ const allowedNames =
99
+ identifier.name === 'craftComponent'
100
+ ? COMPONENT_IMPORTS
101
+ : CORE_IMPORTS;
102
+ const allowedModules =
103
+ identifier.name === 'craftComponent'
104
+ ? COMPONENT_MODULES
105
+ : CORE_MODULES;
106
+ return (
107
+ ALLOWED_HOSTS.has(identifier.name) &&
108
+ resolvesToImportedName(
109
+ identifier,
110
+ sourceCode,
111
+ allowedNames,
112
+ allowedModules,
113
+ )
114
+ );
115
+ }
116
+
117
+ function resolvesToImportedName(
118
+ identifier,
119
+ sourceCode,
120
+ importedNames,
121
+ moduleNames,
122
+ ) {
123
+ const scope = sourceCode.getScope(identifier);
124
+ let current = scope;
125
+
126
+ while (current) {
127
+ const variable = current.variables.find(
128
+ (candidate) => candidate.name === identifier.name,
129
+ );
130
+ if (variable) {
131
+ return variable.defs.some(
132
+ (definition) =>
133
+ definition.type === 'ImportBinding' &&
134
+ importedNames.has(importedName(definition)) &&
135
+ moduleNames.has(definition.parent?.source?.value),
136
+ );
137
+ }
138
+ current = current.upper;
139
+ }
140
+
141
+ return false;
142
+ }
143
+
144
+ function importedName(definition) {
145
+ if (definition.node?.type === 'ImportSpecifier') {
146
+ return definition.node.imported.type === 'Identifier'
147
+ ? definition.node.imported.name
148
+ : definition.node.imported.value;
149
+ }
150
+
151
+ if (definition.node?.type === 'ImportDefaultSpecifier') {
152
+ return 'default';
153
+ }
154
+
155
+ return undefined;
156
+ }