@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,209 @@
1
+ const PRIMITIVE_METHODS = {
2
+ query: new Set(['call']),
3
+ mutation: new Set(['mutate']),
4
+ asyncProcess: new Set(['method']),
5
+ };
6
+
7
+ const CORE_PACKAGE = '@craft-ts/core';
8
+
9
+ module.exports = {
10
+ meta: {
11
+ type: 'problem',
12
+ docs: {
13
+ description:
14
+ 'Require query, mutation, and asyncProcess triggers to be consumed with yield* inside generator functions.',
15
+ },
16
+ schema: [],
17
+ messages: {
18
+ requireYield:
19
+ '{{primitive}}.{{method}}(...) must be consumed with `yield*` inside a generator function.',
20
+ },
21
+ },
22
+
23
+ create(context) {
24
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
25
+ const imported = new Map();
26
+ const primitiveNames = new Map();
27
+ const primitiveRefs = new Map();
28
+ const calls = [];
29
+
30
+ return {
31
+ ImportDeclaration(node) {
32
+ if (node.source.value !== CORE_PACKAGE) return;
33
+ for (const specifier of node.specifiers) {
34
+ if (specifier.type !== 'ImportSpecifier') continue;
35
+ imported.set(specifier.local.name, specifier.imported.name);
36
+ if (PRIMITIVE_METHODS[specifier.imported.name]) {
37
+ primitiveNames.set(
38
+ specifier.local.name,
39
+ specifier.imported.name,
40
+ );
41
+ }
42
+ }
43
+ },
44
+
45
+ VariableDeclarator(node) {
46
+ if (node.id.type === 'Identifier') {
47
+ const primitive = findPrimitiveCreation(node.init);
48
+ if (primitive) {
49
+ primitiveRefs.set(node.id.name, {
50
+ primitive,
51
+ methods: PRIMITIVE_METHODS[primitive],
52
+ });
53
+ }
54
+ }
55
+
56
+ if (node.id.type !== 'ObjectPattern') return;
57
+ const primitive = findPrimitiveCreation(node.init);
58
+ if (!primitive) return;
59
+
60
+ for (const property of node.id.properties) {
61
+ if (
62
+ property.type !== 'Property' ||
63
+ property.computed ||
64
+ property.value.type !== 'Identifier'
65
+ ) {
66
+ continue;
67
+ }
68
+
69
+ primitiveRefs.set(property.value.name, {
70
+ primitive,
71
+ methods: PRIMITIVE_METHODS[primitive],
72
+ });
73
+ }
74
+ },
75
+
76
+ CallExpression(node) {
77
+ calls.push(node);
78
+ },
79
+
80
+ 'Program:exit'() {
81
+ // Resolve simple aliases after all primitive destructuring declarations
82
+ // have been collected (`const q = users`).
83
+ let changed = true;
84
+ while (changed) {
85
+ changed = false;
86
+ walk(sourceCode.ast, (node) => {
87
+ if (
88
+ node.type !== 'VariableDeclarator' ||
89
+ node.id.type !== 'Identifier' ||
90
+ node.init?.type !== 'Identifier'
91
+ ) {
92
+ return;
93
+ }
94
+ const ref = primitiveRefs.get(node.init.name);
95
+ if (ref && !primitiveRefs.has(node.id.name)) {
96
+ primitiveRefs.set(node.id.name, ref);
97
+ changed = true;
98
+ }
99
+ });
100
+ }
101
+
102
+ for (const node of calls) {
103
+ if (!isInsideGeneratorFunction(node)) continue;
104
+ const trigger = getTrigger(node);
105
+ if (!trigger || isYieldStarConsumed(node)) continue;
106
+
107
+ context.report({
108
+ node,
109
+ messageId: 'requireYield',
110
+ data: trigger,
111
+ });
112
+ }
113
+ },
114
+ };
115
+
116
+ function getTrigger(node) {
117
+ if (node.callee.type !== 'MemberExpression') return null;
118
+ if (node.callee.object.type !== 'Identifier') return null;
119
+
120
+ const ref = primitiveRefs.get(node.callee.object.name);
121
+ if (!ref) return null;
122
+
123
+ const method = node.callee.computed
124
+ ? node.callee.property.type === 'Literal'
125
+ ? String(node.callee.property.value)
126
+ : undefined
127
+ : node.callee.property.name;
128
+ if (!method || !ref.methods.has(method)) return null;
129
+
130
+ return { primitive: ref.primitive, method };
131
+ }
132
+
133
+ function findPrimitiveCreation(node) {
134
+ let current = node;
135
+ while (current) {
136
+ if (current.type === 'YieldExpression') {
137
+ current = current.argument;
138
+ continue;
139
+ }
140
+ if (
141
+ current.type === 'TSAsExpression' ||
142
+ current.type === 'TSTypeAssertion' ||
143
+ current.type === 'ChainExpression'
144
+ ) {
145
+ current = current.expression;
146
+ continue;
147
+ }
148
+ if (current.type !== 'CallExpression') return undefined;
149
+
150
+ if (
151
+ current.callee.type === 'Identifier' &&
152
+ (current.callee.name === 'craftUse' ||
153
+ imported.get(current.callee.name) === 'craftUse')
154
+ ) {
155
+ current = current.arguments[0];
156
+ continue;
157
+ }
158
+
159
+ if (
160
+ current.callee.type === 'Identifier' &&
161
+ primitiveNames.has(current.callee.name)
162
+ ) {
163
+ return primitiveNames.get(current.callee.name);
164
+ }
165
+ return undefined;
166
+ }
167
+ return undefined;
168
+ }
169
+ },
170
+ };
171
+
172
+ function isInsideGeneratorFunction(node) {
173
+ let current = node.parent;
174
+ while (current) {
175
+ if (current.type === 'ArrowFunctionExpression') return false;
176
+ if (
177
+ current.type === 'FunctionDeclaration' ||
178
+ current.type === 'FunctionExpression'
179
+ ) {
180
+ return current.generator === true;
181
+ }
182
+ current = current.parent;
183
+ }
184
+ return false;
185
+ }
186
+
187
+ function isYieldStarConsumed(node) {
188
+ let parent = node.parent;
189
+ while (parent && parent.type === 'ParenthesizedExpression') {
190
+ parent = parent.parent;
191
+ }
192
+ return parent?.type === 'YieldExpression' && parent.delegate === true;
193
+ }
194
+
195
+ function walk(node, visitor) {
196
+ if (!node || typeof node !== 'object') return;
197
+ if (node.type) visitor(node);
198
+
199
+ for (const [key, value] of Object.entries(node)) {
200
+ if (key === 'parent' || key === 'tokens' || key === 'comments') continue;
201
+ if (Array.isArray(value)) {
202
+ for (const child of value) {
203
+ if (child && typeof child.type === 'string') walk(child, visitor);
204
+ }
205
+ } else if (value && typeof value.type === 'string') {
206
+ walk(value, visitor);
207
+ }
208
+ }
209
+ }
@@ -0,0 +1,86 @@
1
+ const ADAPTERS = {
2
+ query: 'queryEffect',
3
+ mutation: 'mutationEffect',
4
+ asyncProcess: 'asyncProcessEffect',
5
+ };
6
+
7
+ function isExemptFile(filename) {
8
+ return (
9
+ /(?:^|[/\\])libs[/\\]effect[/\\]/.test(filename) ||
10
+ /\.(?:spec|test)\.[cm]?[jt]sx?$/.test(filename)
11
+ );
12
+ }
13
+
14
+ module.exports = {
15
+ meta: {
16
+ type: 'problem',
17
+ docs: {
18
+ description:
19
+ 'Require Effect-aware Craft adapters in the demo-effect application.',
20
+ },
21
+ schema: [],
22
+ messages: {
23
+ direct: 'Use {{adapter}}(...) in demo-effect instead of {{primitive}}(...).\nEffect demos must use the Effect-aware CraftTS adapters.',
24
+ },
25
+ },
26
+ create(context) {
27
+ if (isExemptFile(context.filename ?? '')) {
28
+ return {};
29
+ }
30
+
31
+ const directImports = new Map();
32
+ const directCalls = [];
33
+
34
+ return {
35
+ ImportDeclaration(node) {
36
+ if (node.source.value !== '@craft-ts/core') return;
37
+
38
+ for (const specifier of node.specifiers) {
39
+ if (
40
+ specifier.type !== 'ImportSpecifier' ||
41
+ specifier.importKind === 'type'
42
+ ) {
43
+ continue;
44
+ }
45
+
46
+ const imported =
47
+ specifier.imported.type === 'Identifier'
48
+ ? specifier.imported.name
49
+ : specifier.imported.value;
50
+ if (!Object.hasOwn(ADAPTERS, imported)) continue;
51
+
52
+ directImports.set(specifier.local.name, imported);
53
+ context.report({
54
+ node: specifier,
55
+ messageId: 'direct',
56
+ data: { adapter: ADAPTERS[imported], primitive: imported },
57
+ });
58
+ }
59
+ },
60
+
61
+ CallExpression(node) {
62
+ if (
63
+ node.callee.type !== 'Identifier' ||
64
+ !directImports.has(node.callee.name)
65
+ ) {
66
+ return;
67
+ }
68
+
69
+ directCalls.push(node);
70
+ },
71
+
72
+ 'Program:exit'() {
73
+ for (const node of directCalls) {
74
+ const primitive = directImports.get(node.callee.name);
75
+ if (!primitive) continue;
76
+
77
+ context.report({
78
+ node: node.callee,
79
+ messageId: 'direct',
80
+ data: { adapter: ADAPTERS[primitive], primitive },
81
+ });
82
+ }
83
+ },
84
+ };
85
+ },
86
+ };
@@ -0,0 +1,314 @@
1
+ /** Generates O(1) DI checks for route, exception, and route-load error components. */
2
+ module.exports = {
3
+ meta: { type: 'problem', fixable: 'code', schema: [] },
4
+ create(context) {
5
+ const source = context.sourceCode ?? context.getSourceCode();
6
+ return {
7
+ 'Program:exit'(program) {
8
+ const text = source.getText();
9
+ if (
10
+ !/renderComponent|errorComponent|withErrorComponent|withRouteLoadError|provideRouteLoadErrorComponent/.test(
11
+ text,
12
+ )
13
+ )
14
+ return;
15
+
16
+ const cascade = readCascadeContext(text);
17
+ const checks = [];
18
+ walk(program, (node) => {
19
+ if (isCall(node, 'craftRoutes')) {
20
+ collectRouteChecks(node, source, cascade, checks);
21
+ } else if (
22
+ isCall(node, 'withErrorComponent') ||
23
+ isCall(node, 'withRouteLoadError')
24
+ ) {
25
+ const descriptor = node.arguments[0];
26
+ const deps = readProperty(descriptor, 'componentDeps', source);
27
+ if (deps) {
28
+ const routeLoad = isCall(node, 'withRouteLoadError');
29
+ checks.push({
30
+ deps,
31
+ names: joinNames(
32
+ cascade.names,
33
+ routeLoad
34
+ ? ['CraftRouteLoadError', 'CraftRouteLoadRecovery']
35
+ : ['CraftGlobalError'],
36
+ ),
37
+ values: cascade.values,
38
+ label: routeLoad
39
+ ? 'global route load error component'
40
+ : 'global error component',
41
+ });
42
+ }
43
+ }
44
+ });
45
+
46
+ const existingChecks = readExistingCheckDeps(text);
47
+ const missing = checks.filter(
48
+ (check) => !existingChecks.has(normalizeTypeText(check.deps)),
49
+ );
50
+ if (missing.length === 0) return;
51
+
52
+ let fixed = ensureImports(text);
53
+ fixed += missing.map(renderCheck).join('');
54
+ context.report({
55
+ node: program,
56
+ message: `${missing.length} exception component(s) must be checked with RouteExceptionComponentCheckedDI.`,
57
+ fix: (fixer) => fixer.replaceTextRange([0, text.length], fixed),
58
+ });
59
+ },
60
+ };
61
+ },
62
+ };
63
+
64
+ function collectRouteChecks(call, source, cascade, checks) {
65
+ const collection = literal(call.arguments[0]);
66
+ const array = call.arguments[1];
67
+ if (!collection || array?.type !== 'ArrayExpression') return;
68
+
69
+ for (const entry of array.elements) {
70
+ const routeCall = unwrapRouteCall(entry);
71
+ const def = routeCall ? routeCall.arguments[1] : entry;
72
+ const path = routeCall
73
+ ? literal(routeCall.arguments[0])
74
+ : literal(propertyValue(def, 'path'));
75
+ if (path === undefined || def?.type !== 'ObjectExpression') continue;
76
+ const baseNames = routeNames(collection, path, def);
77
+ if (routeCall && routeCall !== entry) {
78
+ walk(entry.arguments[0], (node) => {
79
+ if (
80
+ node?.type === 'CallExpression' &&
81
+ node.callee?.type === 'Identifier' &&
82
+ node.callee.name.startsWith('provide')
83
+ ) {
84
+ baseNames.push(node.callee.name.slice('provide'.length));
85
+ }
86
+ });
87
+ }
88
+
89
+ const routeError = propertyValue(def, 'errorComponent');
90
+ const routeDeps = readProperty(routeError, 'componentDeps', source);
91
+ if (routeDeps) {
92
+ checks.push({
93
+ deps: routeDeps,
94
+ names: joinNames(cascade.names, [...baseNames, 'CraftGlobalError']),
95
+ values: cascade.values,
96
+ label: `error component: ${path}`,
97
+ });
98
+ }
99
+
100
+ const providers = propertyValue(def, 'providers');
101
+ if (providers?.type === 'ArrayExpression') {
102
+ for (const provider of providers.elements) {
103
+ if (!isCall(provider, 'provideRouteLoadErrorComponent')) continue;
104
+ const deps = readProperty(
105
+ provider.arguments[0],
106
+ 'componentDeps',
107
+ source,
108
+ );
109
+ if (!deps) continue;
110
+ checks.push({
111
+ deps,
112
+ names: joinNames(cascade.names, [
113
+ ...baseNames,
114
+ 'CraftRouteLoadError',
115
+ 'CraftRouteLoadRecovery',
116
+ ]),
117
+ values: cascade.values,
118
+ label: `route load error component: ${path}`,
119
+ });
120
+ }
121
+ }
122
+
123
+ const handlers = routeCall
124
+ ? routeCall.arguments[2]
125
+ : propertyValue(def, 'handleExceptions');
126
+ if (handlers?.type !== 'ObjectExpression') continue;
127
+ for (const handler of handlers.properties) {
128
+ if (handler.type !== 'Property') continue;
129
+ const code = propertyName(handler);
130
+ walk(handler.value, (node) => {
131
+ if (!isCall(node, 'renderComponent')) return;
132
+ const deps = readProperty(node.arguments[0], 'componentDeps', source);
133
+ if (!deps) return;
134
+ const helper = `${pascal(collection)}${routeBase(path)}${pascal(code)}Exception`;
135
+ checks.push({
136
+ deps,
137
+ names: joinNames(cascade.names, [...baseNames, helper]),
138
+ values: cascade.values,
139
+ label: `render component: ${path}#${code}`,
140
+ });
141
+ });
142
+ }
143
+ }
144
+ }
145
+
146
+ function unwrapRouteCall(entry) {
147
+ if (isCall(entry, 'craftRoute')) return entry;
148
+ if (
149
+ entry?.type === 'CallExpression' &&
150
+ entry.callee?.type === 'MemberExpression' &&
151
+ entry.callee.property?.type === 'Identifier' &&
152
+ entry.callee.property.name === 'withProviders' &&
153
+ isCall(entry.callee.object, 'craftRoute')
154
+ ) {
155
+ return entry.callee.object;
156
+ }
157
+ return undefined;
158
+ }
159
+
160
+ function routeNames(collection, path, def) {
161
+ const prefix = pascal(collection);
162
+ const base = routeBase(path);
163
+ const names = path
164
+ .split('/')
165
+ .filter((x) => x.startsWith(':'))
166
+ .map((x) => `${prefix}${pascal(x.slice(1).replace(/\?$/, ''))}Params`);
167
+ if (propertyValue(def, 'data')) names.push(`${prefix}${base}Data`);
168
+ if (propertyValue(def, 'queryParams'))
169
+ names.push(`${prefix}${base}QueryParams`);
170
+ if (propertyValue(def, 'withLoaderViewTransitionImage'))
171
+ names.push(`${prefix}${base}ViewTransition`);
172
+ const providers = propertyValue(def, 'providers');
173
+ if (providers?.type === 'ArrayExpression') {
174
+ for (const provider of providers.elements) {
175
+ if (
176
+ provider?.type === 'CallExpression' &&
177
+ provider.callee?.type === 'Identifier' &&
178
+ provider.callee.name.startsWith('provide')
179
+ ) {
180
+ names.push(provider.callee.name.slice('provide'.length));
181
+ }
182
+ }
183
+ }
184
+ return names;
185
+ }
186
+
187
+ function renderCheck(check, index) {
188
+ const id = check.label
189
+ .replace(/[^a-zA-Z0-9]+/g, ' ')
190
+ .trim()
191
+ .split(/\s+/)
192
+ .map(pascal)
193
+ .join('');
194
+ return `\n\ntype _Check${id}${index ?? ''}DI = RouteExceptionComponentCheckedDI<\n ${check.deps},\n ${check.names || 'never'},\n ${check.values},\n '${check.label}',\n>;\ntype _CanRun${id}${index ?? ''} = CanRun<_Check${id}${index ?? ''}DI>;`;
195
+ }
196
+
197
+ function readCascadeContext(text) {
198
+ const match = text.match(
199
+ /ValidateCascadeRoutesFile<\s*([^,]+),\s*([^,]+),\s*typeof\s+\w+\s*>/m,
200
+ );
201
+ return {
202
+ names: match?.[1]?.trim() ?? 'never',
203
+ values: match?.[2]?.trim() ?? 'never',
204
+ };
205
+ }
206
+
207
+ function joinNames(parent, names) {
208
+ return (
209
+ [parent === 'never' ? '' : parent, ...names.map((name) => `'${name}'`)]
210
+ .filter(Boolean)
211
+ .join(' | ') || 'never'
212
+ );
213
+ }
214
+
215
+ function ensureImports(text) {
216
+ const names = ['CanRun', 'RouteExceptionComponentCheckedDI'];
217
+ const match = text.match(
218
+ /import\s*\{([\s\S]*?)\}\s*from\s*(['"]@craft-ts\/core['"])/,
219
+ );
220
+ if (!match)
221
+ return `import type { ${names.join(', ')} } from '@craft-ts/core';\n${text}`;
222
+ const missing = names.filter(
223
+ (name) => !new RegExp(`\\b${name}\\b`).test(match[1]),
224
+ );
225
+ if (!missing.length) return text;
226
+ return text.replace(
227
+ match[0],
228
+ `import {${match[1].trimEnd()},\n type ${missing.join(',\n type ')},\n} from ${match[2]}`,
229
+ );
230
+ }
231
+
232
+ function readExistingCheckDeps(text) {
233
+ const deps = new Set();
234
+ const regex = /RouteExceptionComponentCheckedDI<\s*([\s\S]*?),\s*(?:'[^']*'|never)/g;
235
+ let match;
236
+ while ((match = regex.exec(text))) {
237
+ deps.add(normalizeTypeText(match[1]));
238
+ }
239
+ return deps;
240
+ }
241
+
242
+ function normalizeTypeText(text) {
243
+ return String(text).replace(/\s+/g, '');
244
+ }
245
+
246
+ function readProperty(node, name, source) {
247
+ const value = propertyValue(node, name);
248
+ return value ? readTypeText(value, source) : undefined;
249
+ }
250
+
251
+ function readTypeText(node, source) {
252
+ if (node?.type === 'TSAsExpression') return source.getText(node.typeAnnotation);
253
+ return source.getText(node);
254
+ }
255
+
256
+ function propertyValue(node, name) {
257
+ if (node?.type !== 'ObjectExpression') return undefined;
258
+ return node.properties.find(
259
+ (p) => p.type === 'Property' && propertyName(p) === name,
260
+ )?.value;
261
+ }
262
+ function propertyName(property) {
263
+ return property.key?.type === 'Identifier'
264
+ ? property.key.name
265
+ : (literal(property.key) ?? '');
266
+ }
267
+ function literal(node) {
268
+ return node?.type === 'Literal'
269
+ ? node.value
270
+ : node?.type === 'StringLiteral'
271
+ ? node.value
272
+ : undefined;
273
+ }
274
+ function isCall(node, name) {
275
+ return (
276
+ node?.type === 'CallExpression' &&
277
+ node.callee?.type === 'Identifier' &&
278
+ node.callee.name === name
279
+ );
280
+ }
281
+ function pascal(value) {
282
+ return String(value)
283
+ .split(/[^a-zA-Z0-9]+/)
284
+ .filter(Boolean)
285
+ .map(
286
+ (x) =>
287
+ x[0].toUpperCase() +
288
+ (x === x.toUpperCase() ? x.slice(1).toLowerCase() : x.slice(1)),
289
+ )
290
+ .join('');
291
+ }
292
+ function routeBase(path) {
293
+ return (
294
+ String(path)
295
+ .split('/')
296
+ .filter(Boolean)
297
+ .map((x) => pascal(x.replace(/^:/, '').replace(/\?$/, '')))
298
+ .join('') || 'Root'
299
+ );
300
+ }
301
+ function walk(node, visit) {
302
+ if (!node || typeof node !== 'object') return;
303
+ visit(node);
304
+ for (const [key, value] of Object.entries(node)) {
305
+ if (key === 'parent' || key === 'range' || key === 'loc') continue;
306
+ if (Array.isArray(value)) value.forEach((child) => walk(child, visit));
307
+ else if (
308
+ value &&
309
+ typeof value === 'object' &&
310
+ typeof value.type === 'string'
311
+ )
312
+ walk(value, visit);
313
+ }
314
+ }
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ const { componentInfo } = require('./css-rule-utils.cjs');
4
+
5
+ const INTERACTIVE_SELECTOR = /(^|[\s,>+~])(button|a|input|select|textarea|\[tabindex\b)/i;
6
+
7
+ module.exports = {
8
+ meta: {
9
+ type: 'problem',
10
+ docs: {
11
+ description:
12
+ 'Require :focus-visible (or :focus) when a component styles interactive elements.',
13
+ },
14
+ schema: [],
15
+ messages: {
16
+ missing:
17
+ 'Component styles target {{selector}} but never define :focus-visible or :focus.',
18
+ },
19
+ },
20
+ create(context) {
21
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
22
+ return {
23
+ CallExpression(node) {
24
+ const info = componentInfo(context, sourceCode, node);
25
+ if (!info?.css) return;
26
+ if (!INTERACTIVE_SELECTOR.test(info.css)) return;
27
+ if (/:focus-visible|:focus\b/.test(info.css)) return;
28
+ context.report({
29
+ node: info.styles?.value ?? info.stylesUrl.value,
30
+ messageId: 'missing',
31
+ data: { selector: 'button/a/input/[tabindex]' },
32
+ });
33
+ },
34
+ };
35
+ },
36
+ };