@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,492 @@
1
+ const TEMPLATE_CONTROL_FLOW = new Set([
2
+ 'IfStatement',
3
+ 'SwitchStatement',
4
+ 'ForStatement',
5
+ 'ForInStatement',
6
+ 'ForOfStatement',
7
+ 'WhileStatement',
8
+ 'DoWhileStatement',
9
+ 'TryStatement',
10
+ ]);
11
+
12
+ const DOM_EVENT_NAMES = new Set([
13
+ 'abort',
14
+ 'animationcancel',
15
+ 'animationend',
16
+ 'animationiteration',
17
+ 'animationstart',
18
+ 'auxclick',
19
+ 'beforeinput',
20
+ 'beforematch',
21
+ 'beforetoggle',
22
+ 'blur',
23
+ 'cancel',
24
+ 'canplay',
25
+ 'canplaythrough',
26
+ 'change',
27
+ 'click',
28
+ 'close',
29
+ 'compositionend',
30
+ 'compositionstart',
31
+ 'compositionupdate',
32
+ 'contextmenu',
33
+ 'copy',
34
+ 'cuechange',
35
+ 'cut',
36
+ 'dblclick',
37
+ 'drag',
38
+ 'dragend',
39
+ 'dragenter',
40
+ 'dragleave',
41
+ 'dragover',
42
+ 'dragstart',
43
+ 'drop',
44
+ 'durationchange',
45
+ 'emptied',
46
+ 'ended',
47
+ 'error',
48
+ 'focus',
49
+ 'formdata',
50
+ 'fullscreenchange',
51
+ 'fullscreenerror',
52
+ 'gotpointercapture',
53
+ 'input',
54
+ 'invalid',
55
+ 'keydown',
56
+ 'keypress',
57
+ 'keyup',
58
+ 'load',
59
+ 'loadeddata',
60
+ 'loadedmetadata',
61
+ 'loadstart',
62
+ 'lostpointercapture',
63
+ 'mousedown',
64
+ 'mouseenter',
65
+ 'mouseleave',
66
+ 'mousemove',
67
+ 'mouseout',
68
+ 'mouseover',
69
+ 'mouseup',
70
+ 'paste',
71
+ 'pause',
72
+ 'play',
73
+ 'playing',
74
+ 'pointercancel',
75
+ 'pointerdown',
76
+ 'pointerenter',
77
+ 'pointerleave',
78
+ 'pointermove',
79
+ 'pointerout',
80
+ 'pointerover',
81
+ 'pointerup',
82
+ 'progress',
83
+ 'ratechange',
84
+ 'reset',
85
+ 'resize',
86
+ 'scroll',
87
+ 'scrollend',
88
+ 'securitypolicyviolation',
89
+ 'seeked',
90
+ 'seeking',
91
+ 'select',
92
+ 'selectionchange',
93
+ 'selectstart',
94
+ 'slotchange',
95
+ 'stalled',
96
+ 'submit',
97
+ 'suspend',
98
+ 'timeupdate',
99
+ 'toggle',
100
+ 'touchcancel',
101
+ 'touchend',
102
+ 'touchmove',
103
+ 'touchstart',
104
+ 'transitioncancel',
105
+ 'transitionend',
106
+ 'transitionrun',
107
+ 'transitionstart',
108
+ 'volumechange',
109
+ 'waiting',
110
+ 'wheel',
111
+ ]);
112
+
113
+ module.exports = {
114
+ meta: {
115
+ type: 'problem',
116
+ fixable: 'code',
117
+ docs: {
118
+ description:
119
+ 'Keep Craft component templates declarative by using typed Craft blocks instead of inline logic.',
120
+ },
121
+ schema: [],
122
+ messages: {
123
+ ternary:
124
+ 'Do not use a ternary in a Craft template. Use ifBlock(...) for boolean visibility or matchBlock.exhaustive(...) for a discriminated union.',
125
+ logical:
126
+ 'Do not use a logical expression in a Craft template. Move the derivation to state, query, or craftComputed, then render it with a Craft block.',
127
+ negation:
128
+ 'Do not use negation in a Craft template. Move the boolean derivation to state, query, or craftComputed, then bind the resulting value.',
129
+ controlFlow:
130
+ 'Do not use imperative control flow in a Craft template. Use ifBlock(...), matchBlock.exhaustive(...), each(...), or defer(...) so the render contract stays type-checkable.',
131
+ },
132
+ },
133
+
134
+ create(context) {
135
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
136
+
137
+ return {
138
+ CallExpression(node) {
139
+ if (
140
+ node.callee.type !== 'Identifier' ||
141
+ node.callee.name !== 'craftComponent' ||
142
+ node.arguments.length < 4
143
+ ) {
144
+ return;
145
+ }
146
+
147
+ inspectTemplate(node.arguments[3]);
148
+ },
149
+ };
150
+
151
+ function inspectTemplate(template) {
152
+ walk(template, (node) => {
153
+ if (node !== template && isNestedCraftComponent(node)) {
154
+ return 'skip';
155
+ }
156
+
157
+ if (isTemplatePipe(node)) {
158
+ return 'skip';
159
+ }
160
+
161
+ if (node.type === 'ConditionalExpression') {
162
+ const replacement = conditionalReplacement(node);
163
+ context.report({
164
+ node,
165
+ messageId: 'ternary',
166
+ ...(replacement === undefined
167
+ ? {}
168
+ : {
169
+ fix: (fixer) =>
170
+ [
171
+ fixer.replaceText(replacement.node, replacement.text),
172
+ namedImportFix(fixer, 'ifBlock'),
173
+ ].filter(Boolean),
174
+ }),
175
+ });
176
+ return;
177
+ }
178
+
179
+ if (node.type === 'LogicalExpression') {
180
+ context.report({ node, messageId: 'logical' });
181
+ return;
182
+ }
183
+
184
+ if (node.type === 'UnaryExpression' && node.operator === '!') {
185
+ context.report({ node, messageId: 'negation' });
186
+ return;
187
+ }
188
+
189
+ if (TEMPLATE_CONTROL_FLOW.has(node.type)) {
190
+ const replacement =
191
+ node.type === 'SwitchStatement'
192
+ ? switchReplacement(node)
193
+ : undefined;
194
+ context.report({
195
+ node,
196
+ messageId: 'controlFlow',
197
+ ...(replacement === undefined
198
+ ? {}
199
+ : {
200
+ fix: (fixer) =>
201
+ [
202
+ fixer.replaceText(node, replacement),
203
+ namedImportFix(fixer, 'matchBlock'),
204
+ ].filter(Boolean),
205
+ }),
206
+ });
207
+ }
208
+ });
209
+ }
210
+
211
+ function walk(node, visit) {
212
+ if (!node || typeof node.type !== 'string') {
213
+ return;
214
+ }
215
+
216
+ if (visit(node) === 'skip') {
217
+ return;
218
+ }
219
+
220
+ // Event handlers are imperative by design: they react to a user action
221
+ // after rendering and may legitimately branch before triggering a
222
+ // mutation or another command. Keep the declarative restriction on
223
+ // render-time expressions while leaving event callbacks executable.
224
+ if (isEventProperty(node)) {
225
+ return;
226
+ }
227
+
228
+ const keys = sourceCode.visitorKeys[node.type] ?? [];
229
+ for (const key of keys) {
230
+ const child = node[key];
231
+ if (Array.isArray(child)) {
232
+ for (const item of child) {
233
+ walk(item, visit);
234
+ }
235
+ } else {
236
+ walk(child, visit);
237
+ }
238
+ }
239
+ }
240
+
241
+ function conditionalReplacement(node) {
242
+ const condition = conditionName(node.test);
243
+ if (condition === undefined || !isRenderablePosition(node)) {
244
+ return undefined;
245
+ }
246
+
247
+ const text = `ifBlock(${condition}, () => ${sourceCode.getText(
248
+ node.consequent,
249
+ )}, () => ${sourceCode.getText(node.alternate)})`;
250
+ const parent = node.parent;
251
+ if (
252
+ parent?.type === 'ArrowFunctionExpression' &&
253
+ parent.body === node &&
254
+ isRenderableArrow(parent)
255
+ ) {
256
+ return { node: parent, text };
257
+ }
258
+
259
+ return { node, text };
260
+ }
261
+
262
+ function switchReplacement(node) {
263
+ const match = switchMatch(node);
264
+ if (match === undefined) {
265
+ return undefined;
266
+ }
267
+
268
+ const handlers = match.cases
269
+ .map(
270
+ ({ key, expression }) =>
271
+ `${formatObjectKey(key)}: () => ${expression}`,
272
+ )
273
+ .join(', ');
274
+ return `return matchBlock.exhaustive(${match.source}, ${JSON.stringify(
275
+ match.key,
276
+ )}, { ${handlers} });`;
277
+ }
278
+
279
+ function switchMatch(node) {
280
+ if (node.cases.some((switchCase) => switchCase.test === null)) {
281
+ return undefined;
282
+ }
283
+
284
+ const cases = [];
285
+ for (const switchCase of node.cases) {
286
+ if (
287
+ switchCase.test.type !== 'Literal' ||
288
+ (typeof switchCase.test.value !== 'string' &&
289
+ typeof switchCase.test.value !== 'number')
290
+ ) {
291
+ return undefined;
292
+ }
293
+
294
+ const statements = switchCase.consequent.filter(
295
+ (statement) => statement.type !== 'BreakStatement',
296
+ );
297
+ if (
298
+ statements.length !== 1 ||
299
+ statements[0].type !== 'ReturnStatement' ||
300
+ statements[0].argument === null
301
+ ) {
302
+ return undefined;
303
+ }
304
+
305
+ cases.push({
306
+ key: switchCase.test.value,
307
+ expression: sourceCode.getText(statements[0].argument),
308
+ });
309
+ }
310
+
311
+ const discriminant = node.discriminant;
312
+ if (
313
+ discriminant.type === 'MemberExpression' &&
314
+ !discriminant.computed &&
315
+ discriminant.property.type === 'Identifier'
316
+ ) {
317
+ const object = sourceCode.getText(discriminant.object);
318
+ return {
319
+ source: needsFunctionSource(discriminant.object)
320
+ ? `() => ${object}`
321
+ : object,
322
+ key: discriminant.property.name,
323
+ cases,
324
+ };
325
+ }
326
+
327
+ return {
328
+ source: `() => ({ value: ${sourceCode.getText(discriminant)} })`,
329
+ key: 'value',
330
+ cases,
331
+ };
332
+ }
333
+
334
+ function namedImportFix(fixer, name) {
335
+ const declaration = sourceCode.ast.body.find(
336
+ (statement) =>
337
+ statement.type === 'ImportDeclaration' &&
338
+ statement.importKind !== 'type' &&
339
+ statement.source.value === '@craft-ts/component',
340
+ );
341
+
342
+ if (declaration === undefined) {
343
+ const firstStatement = sourceCode.ast.body[0];
344
+ return fixer.insertTextBefore(
345
+ firstStatement ?? sourceCode.ast,
346
+ `import { ${name} } from '@craft-ts/component';\n`,
347
+ );
348
+ }
349
+
350
+ if (
351
+ declaration.specifiers.some(
352
+ (specifier) =>
353
+ specifier.type === 'ImportSpecifier' &&
354
+ specifier.imported.type === 'Identifier' &&
355
+ specifier.imported.name === name,
356
+ )
357
+ ) {
358
+ return undefined;
359
+ }
360
+
361
+ const namedSpecifiers = declaration.specifiers.filter(
362
+ (specifier) => specifier.type === 'ImportSpecifier',
363
+ );
364
+ if (namedSpecifiers.length > 0) {
365
+ return fixer.insertTextAfter(
366
+ namedSpecifiers[namedSpecifiers.length - 1],
367
+ `, ${name}`,
368
+ );
369
+ }
370
+
371
+ return fixer.insertTextAfter(
372
+ declaration,
373
+ `\nimport { ${name} } from '@craft-ts/component';`,
374
+ );
375
+ }
376
+ },
377
+ };
378
+
379
+ function conditionName(node) {
380
+ if (
381
+ node.type === 'CallExpression' &&
382
+ node.callee.type === 'Identifier' &&
383
+ node.arguments.length === 0
384
+ ) {
385
+ return node.callee.name;
386
+ }
387
+
388
+ return undefined;
389
+ }
390
+
391
+ function isRenderablePosition(node) {
392
+ const parent = node.parent;
393
+ if (parent === undefined) {
394
+ return false;
395
+ }
396
+
397
+ if (parent.type === 'ArrayExpression') {
398
+ return true;
399
+ }
400
+
401
+ if (parent.type === 'ReturnStatement') {
402
+ return true;
403
+ }
404
+
405
+ if (parent.type === 'CallExpression') {
406
+ return parent.arguments.includes(node);
407
+ }
408
+
409
+ return (
410
+ parent.type === 'ArrowFunctionExpression' &&
411
+ parent.body === node &&
412
+ isRenderableArrow(parent)
413
+ );
414
+ }
415
+
416
+ function isRenderableArrow(node) {
417
+ const parent = node.parent;
418
+ if (parent?.type === 'ArrayExpression') {
419
+ return true;
420
+ }
421
+
422
+ if (parent?.type !== 'CallExpression') {
423
+ return false;
424
+ }
425
+
426
+ const callee = parent.callee;
427
+ if (callee.type !== 'Identifier') {
428
+ return false;
429
+ }
430
+
431
+ return !new Set([
432
+ 'each',
433
+ 'defer',
434
+ 'content',
435
+ 'craftTemplate',
436
+ 'renderTemplate',
437
+ 'renderContent',
438
+ ]).has(callee.name);
439
+ }
440
+
441
+ function needsFunctionSource(node) {
442
+ return node.type === 'CallExpression';
443
+ }
444
+
445
+ function formatObjectKey(value) {
446
+ return typeof value === 'string' && /^[A-Za-z_$][\w$]*$/.test(value)
447
+ ? value
448
+ : JSON.stringify(value);
449
+ }
450
+
451
+ function isNestedCraftComponent(node) {
452
+ return (
453
+ node.type === 'CallExpression' &&
454
+ node.callee.type === 'Identifier' &&
455
+ node.callee.name === 'craftComponent'
456
+ );
457
+ }
458
+
459
+ function isEventProperty(node) {
460
+ if (node.type !== 'Property' || node.computed) {
461
+ return false;
462
+ }
463
+
464
+ const propertyName =
465
+ node.key.type === 'Identifier'
466
+ ? node.key.name
467
+ : node.key.type === 'Literal' && typeof node.key.value === 'string'
468
+ ? node.key.value
469
+ : undefined;
470
+
471
+ return Boolean(
472
+ propertyName &&
473
+ (DOM_EVENT_NAMES.has(propertyName) || /^on[A-Z]/.test(propertyName)),
474
+ );
475
+ }
476
+
477
+ function isTemplatePipe(node) {
478
+ if (node.type !== 'CallExpression') {
479
+ return false;
480
+ }
481
+
482
+ if (node.callee.type === 'Identifier') {
483
+ return node.callee.name === 'pipe';
484
+ }
485
+
486
+ return (
487
+ node.callee.type === 'MemberExpression' &&
488
+ !node.callee.computed &&
489
+ node.callee.property.type === 'Identifier' &&
490
+ node.callee.property.name === 'pipe'
491
+ );
492
+ }
@@ -0,0 +1,105 @@
1
+ module.exports = {
2
+ meta: {
3
+ type: 'suggestion',
4
+ docs: {
5
+ description:
6
+ 'Prefer passing a yieldable callback directly when a template generator only delegates to it.',
7
+ },
8
+ fixable: 'code',
9
+ schema: [],
10
+ messages: {
11
+ preferDirect:
12
+ 'Pass the yieldable callback directly instead of wrapping it in a generator.',
13
+ },
14
+ },
15
+
16
+ create(context) {
17
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
18
+
19
+ return {
20
+ CallExpression(node) {
21
+ if (
22
+ node.callee.type !== 'Identifier' ||
23
+ node.callee.name !== 'craftComponent' ||
24
+ node.arguments.length < 4
25
+ ) {
26
+ return;
27
+ }
28
+
29
+ inspectTemplate(node.arguments[3]);
30
+ },
31
+ };
32
+
33
+ function inspectTemplate(template) {
34
+ walk(template, (node) => {
35
+ if (node !== template && isNestedCraftComponent(node)) {
36
+ return 'skip';
37
+ }
38
+
39
+ if (!isRedundantGenerator(node)) {
40
+ return;
41
+ }
42
+
43
+ context.report({
44
+ node,
45
+ messageId: 'preferDirect',
46
+ fix(fixer) {
47
+ const delegatedCall = node.body.body[0].argument.argument;
48
+ return fixer.replaceText(
49
+ node,
50
+ sourceCode.getText(delegatedCall.callee),
51
+ );
52
+ },
53
+ });
54
+ });
55
+ }
56
+
57
+ function isRedundantGenerator(node) {
58
+ if (
59
+ node.type !== 'FunctionExpression' ||
60
+ node.id ||
61
+ !node.generator ||
62
+ node.params.length > 0 ||
63
+ node.body.type !== 'BlockStatement' ||
64
+ node.body.body.length !== 1
65
+ ) {
66
+ return false;
67
+ }
68
+
69
+ const statement = node.body.body[0];
70
+ const delegatedCall = statement.argument?.argument;
71
+
72
+ return Boolean(
73
+ statement.type === 'ReturnStatement' &&
74
+ statement.argument?.type === 'YieldExpression' &&
75
+ statement.argument.delegate &&
76
+ delegatedCall?.type === 'CallExpression' &&
77
+ delegatedCall.arguments.length === 0 &&
78
+ delegatedCall.callee.type === 'Identifier',
79
+ );
80
+ }
81
+
82
+ function isNestedCraftComponent(node) {
83
+ return (
84
+ node.type === 'CallExpression' &&
85
+ node.callee.type === 'Identifier' &&
86
+ node.callee.name === 'craftComponent'
87
+ );
88
+ }
89
+
90
+ function walk(node, visit) {
91
+ if (!node || typeof node.type !== 'string') return;
92
+ if (visit(node) === 'skip') return;
93
+
94
+ const keys = sourceCode.visitorKeys[node.type] ?? [];
95
+ for (const key of keys) {
96
+ const child = node[key];
97
+ if (Array.isArray(child)) {
98
+ child.forEach((item) => walk(item, visit));
99
+ } else {
100
+ walk(child, visit);
101
+ }
102
+ }
103
+ }
104
+ },
105
+ };
@@ -0,0 +1,33 @@
1
+ 'use strict';
2
+
3
+ const { NAMED_HTML_HELPER_SET } = require('./html-helpers.cjs');
4
+ const { parseHyperscriptCall } = require('./hyperscript-walk.cjs');
5
+
6
+ module.exports = {
7
+ meta: {
8
+ type: 'problem',
9
+ docs: {
10
+ description:
11
+ 'Forbid h()/customElement() for tags that have a named Craft HTML helper, so types and a11y rules cannot be bypassed.',
12
+ },
13
+ schema: [],
14
+ messages: {
15
+ preferHelper:
16
+ "Use the named helper {{helper}}(...) instead of {{callee}}('{{tag}}'). Named helpers carry a11y types and lint.",
17
+ },
18
+ },
19
+ create(context) {
20
+ return {
21
+ CallExpression(node) {
22
+ const call = parseHyperscriptCall(node);
23
+ if (!call || (call.via !== 'h' && call.via !== 'customElement')) return;
24
+ if (!call.tag || !NAMED_HTML_HELPER_SET.has(call.tag)) return;
25
+ context.report({
26
+ node: call.tagNode ?? node,
27
+ messageId: 'preferHelper',
28
+ data: { helper: call.tag, callee: call.callee, tag: call.tag },
29
+ });
30
+ },
31
+ };
32
+ },
33
+ };