@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,212 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const {
4
+ IndentationText,
5
+ Project,
6
+ QuoteKind,
7
+ Scope,
8
+ SyntaxKind,
9
+ } = require('ts-morph');
10
+
11
+ const projectCache = new Map();
12
+
13
+ module.exports = {
14
+ meta: {
15
+ type: 'problem',
16
+ docs: {
17
+ description:
18
+ "Ensure Angular @Component and @Directive classes declare 'private readonly _monitoring = componentMonitoring()'.",
19
+ },
20
+ fixable: 'code',
21
+ schema: [],
22
+ messages: {
23
+ missingMonitoring:
24
+ "Component/Directive '{{className}}' must declare 'private readonly _monitoring = componentMonitoring()'.",
25
+ },
26
+ },
27
+ create(context) {
28
+ return {
29
+ 'Program:exit'() {
30
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
31
+ const filePath = getFilePath(context);
32
+ if (!filePath || !filePath.endsWith('.ts')) {
33
+ return;
34
+ }
35
+
36
+ const text = sourceCode.getText();
37
+ if (!text.includes('@Component') && !text.includes('@Directive')) {
38
+ return;
39
+ }
40
+
41
+ const sourceFile = getProjectSourceFile(
42
+ getProject(getCwd(context)),
43
+ filePath,
44
+ text,
45
+ );
46
+
47
+ const issues = collectMonitoringIssues(sourceFile);
48
+ if (issues.length === 0) {
49
+ return;
50
+ }
51
+
52
+ const reportLoc = getNodeLoc(sourceCode, issues[0].classDeclaration);
53
+
54
+ for (const issue of issues) {
55
+ applyMonitoringFix(issue);
56
+ }
57
+ ensureComponentMonitoringImport(sourceFile);
58
+
59
+ const fixedText = sourceFile.getFullText();
60
+ if (fixedText === text) {
61
+ return;
62
+ }
63
+
64
+ const classNames = issues.map((i) => i.className).join(', ');
65
+
66
+ context.report({
67
+ loc: reportLoc,
68
+ message: `Component(s) missing componentMonitoring(): ${classNames}`,
69
+ fix(fixer) {
70
+ return fixer.replaceTextRange([0, text.length], fixedText);
71
+ },
72
+ });
73
+ },
74
+ };
75
+ },
76
+ };
77
+
78
+ function getProject(cwd) {
79
+ let project = projectCache.get(cwd);
80
+ if (project) {
81
+ return project;
82
+ }
83
+
84
+ const manipulationSettings = {
85
+ indentationText: IndentationText.TwoSpaces,
86
+ quoteKind: QuoteKind.Single,
87
+ };
88
+ const tsConfigFilePath = path.join(cwd, 'tsconfig.json');
89
+ project = fs.existsSync(tsConfigFilePath)
90
+ ? new Project({ tsConfigFilePath, manipulationSettings })
91
+ : new Project({
92
+ compilerOptions: { experimentalDecorators: true, target: 9 },
93
+ manipulationSettings,
94
+ });
95
+
96
+ projectCache.set(cwd, project);
97
+ return project;
98
+ }
99
+
100
+ function getProjectSourceFile(project, filePath, text) {
101
+ const normalizedPath = path.resolve(filePath);
102
+ const existingSourceFile = project.getSourceFile(normalizedPath);
103
+ if (existingSourceFile) {
104
+ existingSourceFile.replaceWithText(text);
105
+ return existingSourceFile;
106
+ }
107
+
108
+ const sourceFile = project.addSourceFileAtPathIfExists(normalizedPath);
109
+ if (sourceFile) {
110
+ sourceFile.replaceWithText(text);
111
+ return sourceFile;
112
+ }
113
+
114
+ return project.createSourceFile(normalizedPath, text, { overwrite: true });
115
+ }
116
+
117
+ function getFilePath(context) {
118
+ const filePath = context.filename ?? context.getFilename();
119
+ if (!filePath || filePath === '<input>') {
120
+ return undefined;
121
+ }
122
+ return filePath;
123
+ }
124
+
125
+ function getCwd(context) {
126
+ return context.cwd ?? process.cwd();
127
+ }
128
+
129
+ function getNodeLoc(sourceCode, node) {
130
+ return {
131
+ start: sourceCode.getLocFromIndex(node.getStart()),
132
+ end: sourceCode.getLocFromIndex(node.getEnd()),
133
+ };
134
+ }
135
+
136
+ function isAngularComponentOrDirective(classDeclaration) {
137
+ for (const decorator of classDeclaration.getDecorators()) {
138
+ const name = decorator.getName();
139
+ if (name === 'Component' || name === 'Directive') {
140
+ return true;
141
+ }
142
+ }
143
+ return false;
144
+ }
145
+
146
+ function hasMonitoringProperty(classDeclaration) {
147
+ for (const prop of classDeclaration.getProperties()) {
148
+ if (
149
+ prop.getName() === '_monitoring' &&
150
+ prop.hasModifier(SyntaxKind.PrivateKeyword) &&
151
+ prop.isReadonly()
152
+ ) {
153
+ const initializer = prop.getInitializer();
154
+ if (
155
+ initializer &&
156
+ initializer.getKindName() === 'CallExpression' &&
157
+ initializer.getText().startsWith('componentMonitoring(')
158
+ ) {
159
+ return true;
160
+ }
161
+ }
162
+ }
163
+ return false;
164
+ }
165
+
166
+ function collectMonitoringIssues(sourceFile) {
167
+ const issues = [];
168
+
169
+ for (const classDeclaration of sourceFile.getClasses()) {
170
+ const className = classDeclaration.getName();
171
+ if (!className) continue;
172
+ if (!isAngularComponentOrDirective(classDeclaration)) continue;
173
+ if (hasMonitoringProperty(classDeclaration)) continue;
174
+
175
+ issues.push({ className, classDeclaration });
176
+ }
177
+
178
+ return issues;
179
+ }
180
+
181
+ function applyMonitoringFix(issue) {
182
+ const { classDeclaration } = issue;
183
+
184
+ // Insert as first property of the class
185
+ classDeclaration.insertProperty(0, {
186
+ name: '_monitoring',
187
+ scope: Scope.Private,
188
+ isReadonly: true,
189
+ initializer: 'componentMonitoring()',
190
+ });
191
+ }
192
+
193
+ function ensureComponentMonitoringImport(sourceFile) {
194
+ const craftTsCoreImport = sourceFile
195
+ .getImportDeclarations()
196
+ .find((imp) => imp.getModuleSpecifierValue() === '@craft-ts/core');
197
+
198
+ if (craftTsCoreImport) {
199
+ const namedImports = craftTsCoreImport.getNamedImports();
200
+ const alreadyImported = namedImports.some(
201
+ (ni) => ni.getName() === 'componentMonitoring',
202
+ );
203
+ if (!alreadyImported) {
204
+ craftTsCoreImport.addNamedImport('componentMonitoring');
205
+ }
206
+ } else {
207
+ sourceFile.addImportDeclaration({
208
+ moduleSpecifier: '@craft-ts/core',
209
+ namedImports: ['componentMonitoring'],
210
+ });
211
+ }
212
+ }
@@ -0,0 +1,142 @@
1
+ /** Enforces craftExceptionHandler(function* (...) {}) in craftRoute() handler maps. */
2
+ module.exports = {
3
+ meta: {
4
+ type: 'problem',
5
+ fixable: 'code',
6
+ schema: [],
7
+ messages: {
8
+ wrap: 'Route exception handlers must use craftExceptionHandler(function* (...) {}).',
9
+ redirect:
10
+ 'Raw redirect(...) is ambiguous: migrate internal routes to redirectTo(...) and opaque URLs to redirectUrl(...).',
11
+ },
12
+ },
13
+ create(context) {
14
+ const source = context.sourceCode ?? context.getSourceCode();
15
+
16
+ return {
17
+ 'Program:exit'(program) {
18
+ const issues = [];
19
+
20
+ for (const statement of program.body) {
21
+ walk(statement, (node) => {
22
+ if (
23
+ node.type !== 'CallExpression' ||
24
+ node.callee?.type !== 'Identifier' ||
25
+ node.callee.name !== 'craftRoute' ||
26
+ node.arguments.length < 3
27
+ ) {
28
+ return;
29
+ }
30
+
31
+ const handlers = node.arguments[2];
32
+ if (handlers?.type !== 'ObjectExpression') return;
33
+
34
+ for (const property of handlers.properties) {
35
+ if (property.type !== 'Property') continue;
36
+ const value = property.value;
37
+ if (
38
+ value.type === 'CallExpression' &&
39
+ value.callee?.type === 'Identifier' &&
40
+ value.callee.name === 'craftExceptionHandler'
41
+ ) {
42
+ continue;
43
+ }
44
+
45
+ if (
46
+ value.type !== 'ArrowFunctionExpression' &&
47
+ value.type !== 'FunctionExpression'
48
+ ) {
49
+ issues.push({ node: value, manual: true });
50
+ continue;
51
+ }
52
+
53
+ const text = source.getText(value);
54
+ issues.push({
55
+ node: value,
56
+ manual: /\bredirect\s*\(/.test(text),
57
+ replacement: wrapHandler(value, source),
58
+ });
59
+ }
60
+ });
61
+ }
62
+
63
+ if (issues.length === 0) return;
64
+ const manual = issues.filter((issue) => issue.manual);
65
+ if (manual.length > 0) {
66
+ for (const issue of manual) {
67
+ context.report({ node: issue.node, messageId: 'redirect' });
68
+ }
69
+ }
70
+
71
+ const fixable = issues.filter((item) => !item.manual);
72
+ if (fixable.length === 0) return;
73
+ const original = source.getText();
74
+ const replacements = fixable
75
+ .map((issue) => ({
76
+ range: issue.node.range,
77
+ text: issue.replacement,
78
+ }))
79
+ .sort((a, b) => b.range[0] - a.range[0]);
80
+ let fixed = original;
81
+ for (const replacement of replacements) {
82
+ fixed =
83
+ fixed.slice(0, replacement.range[0]) +
84
+ replacement.text +
85
+ fixed.slice(replacement.range[1]);
86
+ }
87
+ fixed = ensureImport(fixed);
88
+
89
+ context.report({
90
+ node: fixable[0].node,
91
+ messageId: 'wrap',
92
+ fix: (fixer) => fixer.replaceTextRange([0, original.length], fixed),
93
+ });
94
+ },
95
+ };
96
+ },
97
+ };
98
+
99
+ function wrapHandler(node, source) {
100
+ const params = node.params.map((param) => source.getText(param)).join(', ');
101
+ const body =
102
+ node.body.type === 'BlockStatement'
103
+ ? source.getText(node.body)
104
+ : `{ return ${source.getText(node.body)}; }`;
105
+ return `craftExceptionHandler(function* (${params}) ${body})`;
106
+ }
107
+
108
+ function ensureImport(text) {
109
+ if (
110
+ /\bcraftExceptionHandler\b/.test(
111
+ text.match(/import[\s\S]*?from\s+['"]@craft-ts\/core['"]/g)?.join('\n') ??
112
+ '',
113
+ )
114
+ ) {
115
+ return text;
116
+ }
117
+ const pattern = /import\s*\{([\s\S]*?)\}\s*from\s*(['"]@craft-ts\/core['"])/;
118
+ if (pattern.test(text)) {
119
+ return text.replace(pattern, (_all, names, source) => {
120
+ const separator = names.includes('\n') ? '\n ' : ' ';
121
+ return `import {${names.trimEnd()},${separator}craftExceptionHandler\n} from ${source}`;
122
+ });
123
+ }
124
+ return `import { craftExceptionHandler } from '@craft-ts/core';\n${text}`;
125
+ }
126
+
127
+ function walk(node, visit) {
128
+ if (!node || typeof node !== 'object') return;
129
+ visit(node);
130
+ for (const [key, value] of Object.entries(node)) {
131
+ if (key === 'parent' || key === 'range' || key === 'loc') continue;
132
+ if (Array.isArray(value)) {
133
+ for (const child of value) walk(child, visit);
134
+ } else if (
135
+ value &&
136
+ typeof value === 'object' &&
137
+ typeof value.type === 'string'
138
+ ) {
139
+ walk(value, visit);
140
+ }
141
+ }
142
+ }
@@ -0,0 +1,236 @@
1
+ const YIELDABLE_METHOD_NAME = 'YIELDABLE_METHOD';
2
+
3
+ module.exports = {
4
+ meta: {
5
+ type: 'problem',
6
+ docs: {
7
+ description:
8
+ 'Require callbacks returned from a craftComponent factory to use craftMethod when they call yieldable Craft methods.',
9
+ },
10
+ schema: [],
11
+ messages: {
12
+ requireCraftMethod:
13
+ "Callback '{{name}}' calls a yieldable Craft method and must be created with `craftMethod(...)`.",
14
+ missingTypeInfo:
15
+ 'This rule requires TypeScript type information to identify yieldable Craft methods.',
16
+ },
17
+ },
18
+
19
+ create(context) {
20
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
21
+ const parserServices = sourceCode.parserServices ?? context.parserServices;
22
+ const checker = parserServices?.program?.getTypeChecker?.();
23
+ const esTreeNodeToTSNodeMap = parserServices?.esTreeNodeToTSNodeMap;
24
+ let reportedMissingTypeInfo = false;
25
+
26
+ return {
27
+ Program() {
28
+ if (!checker || !esTreeNodeToTSNodeMap) {
29
+ context.report({
30
+ node: sourceCode.ast,
31
+ messageId: 'missingTypeInfo',
32
+ });
33
+ reportedMissingTypeInfo = true;
34
+ }
35
+ },
36
+
37
+ CallExpression(node) {
38
+ if (
39
+ reportedMissingTypeInfo ||
40
+ !checker ||
41
+ !esTreeNodeToTSNodeMap ||
42
+ node.callee.type !== 'Identifier' ||
43
+ node.callee.name !== 'craftComponent' ||
44
+ node.arguments.length < 4
45
+ ) {
46
+ return;
47
+ }
48
+
49
+ inspectFactory(node.arguments[2]);
50
+ },
51
+ };
52
+
53
+ function inspectFactory(factory) {
54
+ const localFunctions = collectLocalFunctions(factory);
55
+ const returnedObject = findReturnedObject(factory);
56
+
57
+ if (!returnedObject) return;
58
+
59
+ for (const property of returnedObject.properties) {
60
+ if (property.type !== 'Property') continue;
61
+
62
+ const callback = resolveCallback(property.value, localFunctions);
63
+ if (!callback) continue;
64
+
65
+ const yieldableCalls = [];
66
+ walkFunctionBody(callback, (node) => {
67
+ if (node.type === 'CallExpression' && isDirectYieldableCall(node)) {
68
+ yieldableCalls.push(node);
69
+ }
70
+ });
71
+
72
+ if (yieldableCalls.length === 0) continue;
73
+
74
+ context.report({
75
+ node: callback,
76
+ messageId: 'requireCraftMethod',
77
+ data: { name: getPropertyName(property) },
78
+ });
79
+ }
80
+ }
81
+
82
+ function collectLocalFunctions(factory) {
83
+ const localFunctions = new Map();
84
+ if (!factory) return localFunctions;
85
+
86
+ if (
87
+ factory.type === 'ArrowFunctionExpression' &&
88
+ factory.body.type === 'ObjectExpression'
89
+ ) {
90
+ return localFunctions;
91
+ }
92
+
93
+ walkFunctionBody(factory.body, (node) => {
94
+ if (node.type === 'FunctionDeclaration' && node.id) {
95
+ localFunctions.set(node.id.name, node);
96
+ return 'skip';
97
+ }
98
+
99
+ if (
100
+ node.type === 'VariableDeclarator' &&
101
+ node.id.type === 'Identifier' &&
102
+ node.init &&
103
+ isFunctionNode(node.init)
104
+ ) {
105
+ localFunctions.set(node.id.name, node.init);
106
+ }
107
+ });
108
+
109
+ return localFunctions;
110
+ }
111
+
112
+ function findReturnedObject(factory) {
113
+ if (!factory) return undefined;
114
+
115
+ if (
116
+ factory.type === 'ArrowFunctionExpression' &&
117
+ factory.body.type === 'ObjectExpression'
118
+ ) {
119
+ return factory.body;
120
+ }
121
+
122
+ let returnedObject;
123
+ walkFunctionBody(factory.body, (node) => {
124
+ if (
125
+ !returnedObject &&
126
+ node.type === 'ReturnStatement' &&
127
+ node.argument?.type === 'ObjectExpression'
128
+ ) {
129
+ returnedObject = node.argument;
130
+ return 'skip';
131
+ }
132
+ });
133
+
134
+ return returnedObject;
135
+ }
136
+
137
+ function resolveCallback(value, localFunctions) {
138
+ if (isFunctionNode(value)) {
139
+ return value;
140
+ }
141
+
142
+ if (value?.type === 'Identifier') {
143
+ return localFunctions.get(value.name);
144
+ }
145
+
146
+ return undefined;
147
+ }
148
+
149
+ function isDirectYieldableCall(node) {
150
+ const tsNode = esTreeNodeToTSNodeMap.get(node.callee);
151
+ if (!tsNode) return false;
152
+
153
+ const calleeType = checker.getTypeAtLocation(tsNode);
154
+ if (hasYieldableBrand(calleeType, new Set())) {
155
+ return true;
156
+ }
157
+
158
+ const signature = calleeType.getCallSignatures?.()[0];
159
+ return signature ? returnsGenerator(signature.getReturnType()) : false;
160
+ }
161
+
162
+ function hasYieldableBrand(type, seen) {
163
+ if (!type || seen.has(type)) return false;
164
+ seen.add(type);
165
+
166
+ if (type.isUnion?.() || type.isIntersection?.()) {
167
+ return type.types.some((part) => hasYieldableBrand(part, seen));
168
+ }
169
+
170
+ return checker
171
+ .getPropertiesOfType(type)
172
+ .some((property) =>
173
+ String(property.escapedName).includes(YIELDABLE_METHOD_NAME),
174
+ );
175
+ }
176
+
177
+ function returnsGenerator(type) {
178
+ const symbol = type.aliasSymbol ?? type.getSymbol?.();
179
+ const name = symbol && String(symbol.escapedName ?? symbol.name);
180
+ if (name === 'Generator' || name === 'YieldableInvocation') {
181
+ return true;
182
+ }
183
+
184
+ if (type.isUnion?.()) {
185
+ return type.types.every((part) => returnsGenerator(part));
186
+ }
187
+
188
+ return (
189
+ checker
190
+ .getPropertiesOfType(type)
191
+ .some((property) => property.name === 'next') &&
192
+ checker
193
+ .getPropertiesOfType(type)
194
+ .some((property) => property.name === 'return')
195
+ );
196
+ }
197
+
198
+ function getPropertyName(property) {
199
+ if (property.key.type === 'Identifier') return property.key.name;
200
+ if (property.key.type === 'Literal') return String(property.key.value);
201
+ return '<anonymous>';
202
+ }
203
+
204
+ function walkFunctionBody(node, visit) {
205
+ walk(node, (child) => {
206
+ if (child !== node && isFunctionNode(child)) {
207
+ return 'skip';
208
+ }
209
+ return visit(child);
210
+ });
211
+ }
212
+
213
+ function walk(node, visit) {
214
+ if (!node || typeof node.type !== 'string') return;
215
+ if (visit(node) === 'skip') return;
216
+
217
+ const keys = sourceCode.visitorKeys[node.type] ?? [];
218
+ for (const key of keys) {
219
+ const child = node[key];
220
+ if (Array.isArray(child)) {
221
+ child.forEach((item) => walk(item, visit));
222
+ } else {
223
+ walk(child, visit);
224
+ }
225
+ }
226
+ }
227
+
228
+ function isFunctionNode(node) {
229
+ return (
230
+ node?.type === 'ArrowFunctionExpression' ||
231
+ node?.type === 'FunctionExpression' ||
232
+ node?.type === 'FunctionDeclaration'
233
+ );
234
+ }
235
+ },
236
+ };