@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,166 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { walk } = require('./hyperscript-walk.cjs');
6
+
7
+ const ROUTES_FACTORY = 'craftRoutes';
8
+ const HEADING_CALL = /\bheading\s*\(/;
9
+
10
+ function propertyName(property) {
11
+ if (!property || property.computed) return undefined;
12
+ if (property.key.type === 'Identifier') return property.key.name;
13
+ if (property.key.type === 'Literal') return property.key.value;
14
+ return undefined;
15
+ }
16
+
17
+ function isCallNamed(node, name) {
18
+ return (
19
+ !!node &&
20
+ node.type === 'CallExpression' &&
21
+ node.callee.type === 'Identifier' &&
22
+ node.callee.name === name
23
+ );
24
+ }
25
+
26
+ function hasHeadingCall(node, sourceCode) {
27
+ let found = false;
28
+ walk(node, sourceCode, (candidate) => {
29
+ if (isCallNamed(candidate, 'heading')) {
30
+ found = true;
31
+ return 'skip';
32
+ }
33
+ });
34
+ return found;
35
+ }
36
+
37
+ function collectImportSpecifiers(node, sourceCode) {
38
+ const specifiers = [];
39
+ walk(node, sourceCode, (candidate) => {
40
+ if (candidate.type !== 'ImportExpression') return;
41
+ const source = candidate.source;
42
+ if (source.type === 'Literal' && typeof source.value === 'string') {
43
+ specifiers.push(source.value);
44
+ return;
45
+ }
46
+ if (
47
+ source.type === 'TemplateLiteral' &&
48
+ source.expressions.length === 0 &&
49
+ source.quasis.length === 1
50
+ ) {
51
+ specifiers.push(source.quasis[0].value.cooked);
52
+ }
53
+ });
54
+ return specifiers;
55
+ }
56
+
57
+ function fileExists(filePath) {
58
+ try {
59
+ return fs.existsSync(filePath) && fs.statSync(filePath).isFile();
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
64
+
65
+ function resolveImportedFile(fromFile, specifier) {
66
+ if (!fromFile || !specifier || !specifier.startsWith('.')) return undefined;
67
+ if (!fileExists(fromFile)) return undefined;
68
+ const base = path.resolve(path.dirname(fromFile), specifier);
69
+ const candidates = [
70
+ base,
71
+ `${base}.ts`,
72
+ `${base}.tsx`,
73
+ `${base}.js`,
74
+ path.join(base, 'index.ts'),
75
+ path.join(base, 'index.tsx'),
76
+ ];
77
+ return candidates.find((candidate) => fileExists(candidate));
78
+ }
79
+
80
+ function fileHasHeading(filePath) {
81
+ try {
82
+ return HEADING_CALL.test(fs.readFileSync(filePath, 'utf8'));
83
+ } catch {
84
+ return false;
85
+ }
86
+ }
87
+
88
+ function checkLoadFactory(context, sourceCode, factory, reportNode) {
89
+ if (!factory) return;
90
+ const specifiers = collectImportSpecifiers(factory, sourceCode);
91
+ if (specifiers.length > 0) {
92
+ const filename = context.filename ?? context.getFilename?.() ?? '';
93
+ for (const specifier of specifiers) {
94
+ const resolved = resolveImportedFile(filename, specifier);
95
+ if (!resolved) continue;
96
+ if (!fileHasHeading(resolved)) {
97
+ context.report({ node: reportNode, messageId: 'missing' });
98
+ return;
99
+ }
100
+ }
101
+ return;
102
+ }
103
+ if (hasHeadingCall(factory, sourceCode)) return;
104
+ if (
105
+ factory.type === 'ArrowFunctionExpression' ||
106
+ factory.type === 'FunctionExpression'
107
+ ) {
108
+ context.report({ node: reportNode, messageId: 'missing' });
109
+ }
110
+ }
111
+
112
+ function visitRouteConfig(context, sourceCode, node) {
113
+ if (!node) return;
114
+ if (node.type === 'ArrayExpression') {
115
+ for (const element of node.elements) {
116
+ visitRouteConfig(context, sourceCode, element);
117
+ }
118
+ return;
119
+ }
120
+ if (isCallNamed(node, 'craftRoute')) {
121
+ visitRouteConfig(context, sourceCode, node.arguments[1]);
122
+ return;
123
+ }
124
+ if (isCallNamed(node, 'loadCraftComponent')) {
125
+ checkLoadFactory(context, sourceCode, node.arguments[0], node);
126
+ return;
127
+ }
128
+ if (node.type !== 'ObjectExpression') return;
129
+ for (const prop of node.properties) {
130
+ if (prop.type === 'SpreadElement') {
131
+ visitRouteConfig(context, sourceCode, prop.argument);
132
+ continue;
133
+ }
134
+ if (prop.type !== 'Property') continue;
135
+ const name = propertyName(prop);
136
+ if (name === 'loadComponent') {
137
+ checkLoadFactory(context, sourceCode, prop.value, prop.value);
138
+ } else if (name === 'children') {
139
+ visitRouteConfig(context, sourceCode, prop.value);
140
+ }
141
+ }
142
+ }
143
+
144
+ module.exports = {
145
+ meta: {
146
+ type: 'problem',
147
+ docs: {
148
+ description:
149
+ 'Require each craftRoutes loadComponent SFC to call heading() (not headingRoot). Layouts wrap the outlet in headingSection; shells do not put heading() above the outlet.',
150
+ },
151
+ schema: [],
152
+ messages: {
153
+ missing:
154
+ 'Route loadComponent must call heading(...). Layouts wrap CraftRouterOutlet in headingSection(...); shells must not put heading() above the outlet.',
155
+ },
156
+ },
157
+ create(context) {
158
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
159
+ return {
160
+ CallExpression(node) {
161
+ if (!isCallNamed(node, ROUTES_FACTORY)) return;
162
+ visitRouteConfig(context, sourceCode, node.arguments[1]);
163
+ },
164
+ };
165
+ },
166
+ };
@@ -0,0 +1,98 @@
1
+ const WRITE_METHODS = new Set(['set', 'patch', 'update']);
2
+
3
+ module.exports = {
4
+ meta: {
5
+ type: 'problem',
6
+ docs: {
7
+ description:
8
+ 'Require set, patch, and update insertion writes to be delegated with yield* inside generator functions.',
9
+ },
10
+ schema: [],
11
+ messages: {
12
+ requireYield:
13
+ 'Insertion writes through set(...), patch(...), and update(...) must be delegated with `yield*` inside a generator function.',
14
+ },
15
+ },
16
+
17
+ create(context) {
18
+ const insertionWriteBindings = new Set();
19
+
20
+ return {
21
+ FunctionDeclaration: collectInsertionWriteBindings,
22
+ FunctionExpression: collectInsertionWriteBindings,
23
+ ArrowFunctionExpression: collectInsertionWriteBindings,
24
+
25
+ CallExpression(node) {
26
+ if (
27
+ !isInsideGeneratorFunction(node) ||
28
+ isDelegated(node) ||
29
+ !isInsertionWrite(node)
30
+ ) {
31
+ return;
32
+ }
33
+
34
+ context.report({ node, messageId: 'requireYield' });
35
+ },
36
+ };
37
+
38
+ function isInsertionWrite(node) {
39
+ const callee = node.callee;
40
+ if (callee.type === 'Identifier') {
41
+ return insertionWriteBindings.has(callee.name);
42
+ }
43
+ return false;
44
+ }
45
+
46
+ function collectInsertionWriteBindings(node) {
47
+ for (const parameter of node.params) {
48
+ if (parameter.type !== 'ObjectPattern') continue;
49
+
50
+ for (const property of parameter.properties) {
51
+ if (
52
+ property.type !== 'Property' ||
53
+ property.computed ||
54
+ property.key.type !== 'Identifier'
55
+ ) {
56
+ continue;
57
+ }
58
+
59
+ const key = property.key.name;
60
+ if (!WRITE_METHODS.has(key)) continue;
61
+
62
+ if (property.value.type === 'Identifier') {
63
+ insertionWriteBindings.add(property.value.name);
64
+ }
65
+ }
66
+ }
67
+ }
68
+
69
+ },
70
+ };
71
+
72
+ function isInsideGeneratorFunction(node) {
73
+ let current = node.parent;
74
+ while (current) {
75
+ if (current.type === 'ArrowFunctionExpression') return false;
76
+ if (
77
+ current.type === 'FunctionDeclaration' ||
78
+ current.type === 'FunctionExpression'
79
+ ) {
80
+ return current.generator === true;
81
+ }
82
+ current = current.parent;
83
+ }
84
+ return false;
85
+ }
86
+
87
+ function isDelegated(node) {
88
+ let parent = node.parent;
89
+ while (
90
+ parent &&
91
+ ['ChainExpression', 'TSAsExpression', 'TSTypeAssertion'].includes(
92
+ parent.type,
93
+ )
94
+ ) {
95
+ parent = parent.parent;
96
+ }
97
+ return parent?.type === 'YieldExpression' && parent.delegate === true;
98
+ }
@@ -0,0 +1,102 @@
1
+ const REACTIVE_VALUE_BRAND = 'RAW_REACTIVE_VALUE';
2
+
3
+ module.exports = {
4
+ meta: {
5
+ type: 'problem',
6
+ docs: {
7
+ description:
8
+ 'Require Craft reactive readers to be delegated with yield* inside generator functions.',
9
+ },
10
+ schema: [],
11
+ messages: {
12
+ requireYield:
13
+ 'Craft reactive values must be read with `yield*` inside a generator function.',
14
+ requireGenerator:
15
+ 'A function that reads a Craft reactive value must be a generator and delegate the read with `yield*`.',
16
+ missingTypeInfo:
17
+ 'This rule requires TypeScript type information to identify Craft reactive readers.',
18
+ },
19
+ },
20
+
21
+ create(context) {
22
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
23
+ const parserServices = sourceCode.parserServices ?? context.parserServices;
24
+ const checker = parserServices?.program?.getTypeChecker?.();
25
+ const nodeMap = parserServices?.esTreeNodeToTSNodeMap;
26
+ let reportedMissingTypeInfo = false;
27
+
28
+ return {
29
+ Program() {
30
+ if (!checker || !nodeMap) {
31
+ context.report({
32
+ node: sourceCode.ast,
33
+ messageId: 'missingTypeInfo',
34
+ });
35
+ reportedMissingTypeInfo = true;
36
+ }
37
+ },
38
+ CallExpression(node) {
39
+ if (reportedMissingTypeInfo || !checker || !nodeMap) return;
40
+ if (isConsumed(node) || !isReactiveReader(node)) return;
41
+
42
+ const fn = nearestFunction(node);
43
+ context.report({
44
+ node,
45
+ messageId: fn?.generator ? 'requireYield' : 'requireGenerator',
46
+ });
47
+ },
48
+ };
49
+
50
+ function isReactiveReader(node) {
51
+ const tsNode = nodeMap.get(node.callee);
52
+ if (!tsNode) return false;
53
+ return hasBrand(checker.getTypeAtLocation(tsNode), new Set());
54
+ }
55
+
56
+ function hasBrand(type, seen) {
57
+ if (!type || seen.has(type)) return false;
58
+ seen.add(type);
59
+ if (type.isUnion?.() || type.isIntersection?.()) {
60
+ return type.types.some((part) => hasBrand(part, seen));
61
+ }
62
+ return checker
63
+ .getPropertiesOfType(type)
64
+ .some((property) =>
65
+ String(property.escapedName).includes(REACTIVE_VALUE_BRAND),
66
+ );
67
+ }
68
+ },
69
+ };
70
+
71
+ function isConsumed(node) {
72
+ let parent = node.parent;
73
+ while (
74
+ parent &&
75
+ ['ChainExpression', 'TSAsExpression', 'TSTypeAssertion'].includes(
76
+ parent.type,
77
+ )
78
+ ) {
79
+ parent = parent.parent;
80
+ }
81
+ if (parent?.type === 'YieldExpression' && parent.delegate) return true;
82
+ return (
83
+ parent?.type === 'CallExpression' &&
84
+ parent.callee.type === 'Identifier' &&
85
+ parent.callee.name === 'craftUse'
86
+ );
87
+ }
88
+
89
+ function nearestFunction(node) {
90
+ let current = node.parent;
91
+ while (current) {
92
+ if (
93
+ current.type === 'ArrowFunctionExpression' ||
94
+ current.type === 'FunctionExpression' ||
95
+ current.type === 'FunctionDeclaration'
96
+ ) {
97
+ return current;
98
+ }
99
+ current = current.parent;
100
+ }
101
+ return undefined;
102
+ }