@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,630 @@
1
+ import { Node, SyntaxKind, type SourceFile } from 'ts-morph';
2
+ import type {
3
+ DependencyGraphEdge,
4
+ DependencyGraphNode,
5
+ DependencyGraphProof,
6
+ } from './dependency-graph';
7
+
8
+ export interface EffectServiceNodeDetails {
9
+ readonly runtime: 'effect';
10
+ readonly tag: string;
11
+ readonly serviceName: string;
12
+ }
13
+
14
+ export interface EffectServiceRequirementDetails {
15
+ readonly runtime: 'effect';
16
+ readonly selection: 'whole-service' | 'member';
17
+ readonly member?: string;
18
+ }
19
+
20
+ export interface EffectOperationNodeDetails {
21
+ readonly runtime: 'effect';
22
+ readonly operationName: string;
23
+ readonly resolution: 'static' | 'partial' | 'unknown';
24
+ }
25
+
26
+ export interface EffectLayerNodeDetails {
27
+ readonly runtime: 'effect';
28
+ readonly layerName: string;
29
+ readonly providedServices: readonly string[];
30
+ readonly requiredServices: readonly string[];
31
+ readonly compositions: readonly string[];
32
+ readonly resolution: 'complete' | 'partial' | 'unknown';
33
+ }
34
+
35
+ export interface EffectLayerRelationDetails {
36
+ readonly runtime: 'effect';
37
+ readonly service: string;
38
+ readonly resolution: 'complete' | 'partial' | 'unknown';
39
+ }
40
+
41
+ declare module './dependency-graph' {
42
+ interface DependencyGraphNodeRegistry {
43
+ 'effect-service': EffectServiceNodeDetails;
44
+ 'effect-operation': EffectOperationNodeDetails;
45
+ 'effect-layer': EffectLayerNodeDetails;
46
+ }
47
+
48
+ interface DependencyGraphEdgeRegistry {
49
+ 'requires-service': EffectServiceRequirementDetails;
50
+ 'provided-by-layer': EffectLayerRelationDetails;
51
+ 'composes-layer': { readonly runtime: 'effect' };
52
+ }
53
+ }
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Task 3.3 — fine edges for Effect services.
57
+ //
58
+ // The graph already draws `Consumer -> Service.member` for craft services, via
59
+ // the injectX.property() shortcuts. This does the same for Effect ones, so that
60
+ // selecting `({ byId }) => ({ byId })` shows up as `UserStore -> UserApi.byId`
61
+ // rather than the useless `UserStore -> UserApi`.
62
+ //
63
+ // What the edge does NOT mean. A `Layer` builds the whole service regardless;
64
+ // nothing here reduces what Effect constructs at runtime. The edge records what
65
+ // a consumer actually *reaches for*, which is what makes the graph worth
66
+ // reading — and it is the only reason task 3.1's selection exists at all.
67
+ // ---------------------------------------------------------------------------
68
+
69
+ export type EffectServiceInfo = {
70
+ /** The class name, which is also the Context tag's identity. */
71
+ readonly name: string;
72
+ /** The string key given to Context.Service()(...). */
73
+ readonly key: string;
74
+ readonly filePath: string;
75
+ readonly line: number;
76
+ };
77
+
78
+ export type EffectGraphContribution = {
79
+ readonly nodes: readonly DependencyGraphNode[];
80
+ readonly edges: readonly DependencyGraphEdge[];
81
+ };
82
+
83
+ const serviceId = (name: string) => `service:effect:${name}`;
84
+ const effectServiceId = (name: string) => `effect-service:${name}`;
85
+ const memberId = (name: string, member: string) =>
86
+ `property:${serviceId(name)}:${member}`;
87
+
88
+ /**
89
+ * `class UserApi extends Context.Service<UserApi, Shape>()('UserApi') {}`
90
+ *
91
+ * Matched on the *shape* of the heritage clause rather than on an import,
92
+ * because the tag is routinely re-exported through a barrel.
93
+ */
94
+ export function collectEffectServices(
95
+ sourceFiles: readonly SourceFile[],
96
+ ): Map<string, EffectServiceInfo> {
97
+ const services = new Map<string, EffectServiceInfo>();
98
+
99
+ for (const sourceFile of sourceFiles) {
100
+ for (const declaration of sourceFile.getClasses()) {
101
+ const heritage = declaration.getExtends();
102
+ if (!heritage) continue;
103
+
104
+ const expression = heritage.getExpression();
105
+ if (!Node.isCallExpression(expression)) continue;
106
+
107
+ // The outer call is `Context.Service<...>()('Name')`; its callee is the
108
+ // inner `Context.Service<...>()` call.
109
+ const inner = expression.getExpression();
110
+ if (!Node.isCallExpression(inner)) continue;
111
+ if (!/\bContext\.Service\b/.test(inner.getExpression().getText())) continue;
112
+
113
+ const name = declaration.getName();
114
+ if (!name) continue;
115
+
116
+ const keyArgument = expression.getArguments()[0];
117
+ const key =
118
+ keyArgument && Node.isStringLiteral(keyArgument)
119
+ ? keyArgument.getLiteralValue()
120
+ : name;
121
+
122
+ services.set(name, {
123
+ name,
124
+ key,
125
+ filePath: sourceFile.getFilePath(),
126
+ line: declaration.getStartLineNumber(),
127
+ });
128
+ }
129
+ }
130
+
131
+ return services;
132
+ }
133
+
134
+ /** The member names a selector picks: `({ byId }) => ({ byId })` -> ['byId']. */
135
+ export function selectedMemberNames(selector: Node): readonly string[] {
136
+ if (!Node.isArrowFunction(selector) && !Node.isFunctionExpression(selector)) {
137
+ return [];
138
+ }
139
+
140
+ const names = new Set<string>();
141
+
142
+ // Preferred reading: the keys of the returned object literal, which is what
143
+ // the consumer will actually destructure.
144
+ const body = Node.isArrowFunction(selector) ? selector.getBody() : undefined;
145
+ const returned =
146
+ body && Node.isParenthesizedExpression(body)
147
+ ? body.getExpression()
148
+ : body && Node.isObjectLiteralExpression(body)
149
+ ? body
150
+ : undefined;
151
+
152
+ if (returned && Node.isObjectLiteralExpression(returned)) {
153
+ for (const property of returned.getProperties()) {
154
+ if (
155
+ Node.isPropertyAssignment(property) ||
156
+ Node.isShorthandPropertyAssignment(property)
157
+ ) {
158
+ const nameNode = property.getNameNode();
159
+ if (Node.isIdentifier(nameNode)) names.add(nameNode.getText());
160
+ }
161
+ }
162
+ }
163
+
164
+ // Fallback: the destructured parameter, for selectors that rename on the way
165
+ // out (`({ byId }) => ({ fetchUser: byId })`).
166
+ if (names.size === 0) {
167
+ const parameter = selector.getParameters()[0];
168
+ const binding = parameter?.getNameNode();
169
+ if (binding && Node.isObjectBindingPattern(binding)) {
170
+ for (const element of binding.getElements()) {
171
+ const propertyName = element.getPropertyNameNode() ?? element.getNameNode();
172
+ if (Node.isIdentifier(propertyName)) names.add(propertyName.getText());
173
+ }
174
+ }
175
+ }
176
+
177
+ return [...names];
178
+ }
179
+
180
+ export type EffectOperationOwner = {
181
+ readonly name: string;
182
+ readonly node: DependencyGraphNode;
183
+ readonly start: number;
184
+ readonly end: number;
185
+ readonly filePath: string;
186
+ };
187
+
188
+ const operationId = (filePath: string, name: string) =>
189
+ `effect-operation:${filePath}#${name}`;
190
+
191
+ /**
192
+ * Finds named Effect operations around direct service reads. This is the
193
+ * missing owner for standalone functions such as `checkUserAccess`: they are
194
+ * not Craft services or components, but they still form a meaningful graph
195
+ * boundary.
196
+ */
197
+ export function collectEffectOperationOwners(
198
+ sourceFiles: readonly SourceFile[],
199
+ services: ReadonlyMap<string, EffectServiceInfo>,
200
+ ): Map<string, EffectOperationOwner> {
201
+ const owners = new Map<string, EffectOperationOwner>();
202
+
203
+ for (const sourceFile of sourceFiles) {
204
+ const candidates = [
205
+ ...sourceFile.getDescendantsOfKind(SyntaxKind.YieldExpression),
206
+ ...sourceFile
207
+ .getDescendantsOfKind(SyntaxKind.CallExpression)
208
+ .filter((call) => {
209
+ const callee = call.getExpression().getText();
210
+ return callee === 'effectService' || callee.endsWith('.effectService');
211
+ }),
212
+ ].filter((candidate) => {
213
+ if (Node.isYieldExpression(candidate)) {
214
+ const expression = candidate.getExpression();
215
+ return (
216
+ expression !== undefined &&
217
+ Node.isIdentifier(expression) &&
218
+ services.has(expression.getText())
219
+ );
220
+ }
221
+ return true;
222
+ });
223
+
224
+ for (const candidate of candidates) {
225
+ const declaration = namedOperationDeclaration(candidate);
226
+ if (!declaration) continue;
227
+ const name = declaration.getName();
228
+ if (!name) continue;
229
+ const id = operationId(sourceFile.getFilePath(), name);
230
+ if (owners.has(id)) continue;
231
+ const node: DependencyGraphNode = {
232
+ id,
233
+ kind: 'effect-operation',
234
+ label: name,
235
+ filePath: sourceFile.getFilePath(),
236
+ line: declaration.getStartLineNumber(),
237
+ details: {
238
+ runtime: 'effect',
239
+ operationName: name,
240
+ resolution: 'static',
241
+ },
242
+ };
243
+ owners.set(id, {
244
+ name,
245
+ node,
246
+ start: declaration.getStart(),
247
+ end: declaration.getEnd(),
248
+ filePath: sourceFile.getFilePath(),
249
+ });
250
+ }
251
+ }
252
+
253
+ return owners;
254
+ }
255
+
256
+ function namedOperationDeclaration(
257
+ node: Node,
258
+ ): import('ts-morph').VariableDeclaration | import('ts-morph').FunctionDeclaration | undefined {
259
+ let current: Node | undefined = node.getParent();
260
+ while (current) {
261
+ if (Node.isVariableDeclaration(current) && current.getName()) {
262
+ const initializer = current.getInitializer();
263
+ if (
264
+ (Node.isCallExpression(initializer) &&
265
+ initializer.getExpression().getText() === 'Effect.gen') ||
266
+ initializer?.getDescendantsOfKind(SyntaxKind.CallExpression).some(
267
+ (call) => call.getExpression().getText() === 'Effect.gen',
268
+ )
269
+ ) {
270
+ return current;
271
+ }
272
+ }
273
+ if (Node.isFunctionDeclaration(current) && current.getName()) {
274
+ return current;
275
+ }
276
+ current = current.getParent();
277
+ }
278
+ return undefined;
279
+ }
280
+
281
+ export type EffectLayerContribution = EffectGraphContribution;
282
+
283
+ const layerId = (filePath: string, name: string) =>
284
+ `effect-layer:${filePath}#${name}`;
285
+
286
+ /** Collects only Layer forms whose service and composition are statically visible. */
287
+ export function collectEffectLayers(
288
+ sourceFiles: readonly SourceFile[],
289
+ services: ReadonlyMap<string, EffectServiceInfo>,
290
+ ): EffectLayerContribution {
291
+ const nodes = new Map<string, DependencyGraphNode>();
292
+ const edges = new Map<string, DependencyGraphEdge>();
293
+ const layerDeclarations = new Map<string, import('ts-morph').VariableDeclaration>();
294
+ const layerNames = new Map<string, string>();
295
+
296
+ const addNode = (node: DependencyGraphNode) => {
297
+ if (!nodes.has(node.id)) nodes.set(node.id, node);
298
+ };
299
+ const addEdge = (edge: DependencyGraphEdge) => {
300
+ const key = `${edge.from}:${edge.kind}:${edge.to}`;
301
+ if (!edges.has(key)) edges.set(key, edge);
302
+ };
303
+ const proof = (node: Node, pattern: string): DependencyGraphProof => ({
304
+ filePath: node.getSourceFile().getFilePath(),
305
+ line: node.getStartLineNumber(),
306
+ pattern,
307
+ });
308
+
309
+ for (const sourceFile of sourceFiles) {
310
+ for (const declaration of sourceFile.getVariableDeclarations()) {
311
+ const initializer = declaration.getInitializer();
312
+ if (!initializer || !containsLayerCall(initializer)) continue;
313
+ const name = declaration.getName();
314
+ const id = layerId(sourceFile.getFilePath(), name);
315
+ layerDeclarations.set(id, declaration);
316
+ layerNames.set(name, id);
317
+ }
318
+ }
319
+
320
+ for (const [id, declaration] of layerDeclarations) {
321
+ const initializer = declaration.getInitializer();
322
+ if (!initializer) continue;
323
+ const provided: { name: string; node: Node }[] = [];
324
+ const required: { name: string; node: Node }[] = [];
325
+ const compositions: { id: string; node: Node }[] = [];
326
+ const calls = [
327
+ ...(Node.isCallExpression(initializer) ? [initializer] : []),
328
+ ...initializer.getDescendantsOfKind(SyntaxKind.CallExpression),
329
+ ];
330
+
331
+ for (const call of calls) {
332
+ const callee = call.getExpression().getText();
333
+ const method = callee.match(/^Layer\.(succeed|sync|effect|scoped)$/)?.[1];
334
+ if (method) {
335
+ const argument = call.getArguments()[0];
336
+ if (argument && Node.isIdentifier(argument)) {
337
+ const service = services.get(argument.getText());
338
+ if (service) provided.push({ name: service.name, node: call });
339
+ }
340
+ }
341
+ if (callee === 'Layer.provide' || callee.endsWith('.provide')) {
342
+ const argument = call.getArguments()[0];
343
+ if (argument && Node.isIdentifier(argument)) {
344
+ const target = layerNames.get(argument.getText());
345
+ if (target) compositions.push({ id: target, node: argument });
346
+ }
347
+ }
348
+ }
349
+
350
+ for (const yielded of initializer.getDescendantsOfKind(SyntaxKind.YieldExpression)) {
351
+ const expression = yielded.getExpression();
352
+ if (!expression || !Node.isIdentifier(expression)) continue;
353
+ const service = services.get(expression.getText());
354
+ if (service && !provided.some((item) => item.name === service.name)) {
355
+ required.push({ name: service.name, node: yielded });
356
+ }
357
+ }
358
+
359
+ const resolution =
360
+ provided.length > 0 &&
361
+ required.length === 0 &&
362
+ [...initializer.getDescendantsOfKind(SyntaxKind.CallExpression)].every(
363
+ (call) => !isUnknownLayerCall(call),
364
+ )
365
+ ? 'complete'
366
+ : provided.length > 0 || required.length > 0
367
+ ? 'partial'
368
+ : 'unknown';
369
+ const node: DependencyGraphNode = {
370
+ id,
371
+ kind: 'effect-layer',
372
+ label: declaration.getName(),
373
+ filePath: declaration.getSourceFile().getFilePath(),
374
+ line: declaration.getStartLineNumber(),
375
+ details: {
376
+ runtime: 'effect',
377
+ layerName: declaration.getName(),
378
+ providedServices: [...new Set(provided.map((item) => item.name))],
379
+ requiredServices: [...new Set(required.map((item) => item.name))],
380
+ compositions: [...new Set(compositions.map((item) => item.id))],
381
+ resolution,
382
+ },
383
+ };
384
+ addNode(node);
385
+
386
+ for (const item of provided) {
387
+ const target = effectServiceId(item.name);
388
+ addNode({
389
+ id: target,
390
+ kind: 'effect-service',
391
+ label: item.name,
392
+ filePath: services.get(item.name)?.filePath,
393
+ line: services.get(item.name)?.line,
394
+ details: {
395
+ runtime: 'effect',
396
+ tag: services.get(item.name)?.key ?? item.name,
397
+ serviceName: item.name,
398
+ },
399
+ });
400
+ addEdge({
401
+ from: target,
402
+ to: id,
403
+ kind: 'provided-by-layer',
404
+ evidence: 'ast',
405
+ details: { runtime: 'effect', service: item.name, resolution },
406
+ proof: proof(item.node, `Layer provides ${item.name}`),
407
+ });
408
+ }
409
+ for (const item of required) {
410
+ const target = effectServiceId(item.name);
411
+ addEdge({
412
+ from: id,
413
+ to: target,
414
+ kind: 'requires-service',
415
+ evidence: 'ast',
416
+ details: { runtime: 'effect', selection: 'whole-service' },
417
+ proof: proof(item.node, `yield* ${item.name}`),
418
+ });
419
+ }
420
+ for (const item of compositions) {
421
+ addEdge({
422
+ from: id,
423
+ to: item.id,
424
+ kind: 'composes-layer',
425
+ evidence: 'ast',
426
+ details: { runtime: 'effect' },
427
+ proof: proof(item.node, `Layer provides ${item.id}`),
428
+ });
429
+ }
430
+ }
431
+
432
+ return { nodes: [...nodes.values()], edges: [...edges.values()] };
433
+ }
434
+
435
+ function containsLayerCall(node: Node): boolean {
436
+ return [
437
+ ...(Node.isCallExpression(node) ? [node] : []),
438
+ ...node.getDescendantsOfKind(SyntaxKind.CallExpression),
439
+ ].some((call) => call.getExpression().getText().startsWith('Layer.'));
440
+ }
441
+
442
+ function isUnknownLayerCall(call: import('ts-morph').CallExpression): boolean {
443
+ const callee = call.getExpression().getText();
444
+ return callee.startsWith('Layer.') &&
445
+ !/^Layer\.(succeed|sync|effect|scoped|mergeAll|provide)$/.test(callee);
446
+ }
447
+
448
+ /**
449
+ * Walks `effectService(Tag, selector?)` calls and produces the nodes and edges
450
+ * they justify. `ownerIdOf` maps a call site to the consumer node that should
451
+ * own the edge; a call outside any known consumer is skipped rather than
452
+ * attached to something arbitrary.
453
+ */
454
+ export function collectEffectServiceUsage(
455
+ sourceFiles: readonly SourceFile[],
456
+ services: Map<string, EffectServiceInfo>,
457
+ ownerIdOf: (node: Node) => string | undefined,
458
+ includeBackendRelations = false,
459
+ ): EffectGraphContribution {
460
+ const nodes = new Map<string, DependencyGraphNode>();
461
+ const edges = new Map<string, DependencyGraphEdge>();
462
+
463
+ const addNode = (node: DependencyGraphNode) => {
464
+ if (!nodes.has(node.id)) nodes.set(node.id, node);
465
+ };
466
+ const addEffectServiceNode = (service: EffectServiceInfo) => {
467
+ addNode({
468
+ id: effectServiceId(service.name),
469
+ kind: 'effect-service',
470
+ label: service.name,
471
+ filePath: service.filePath,
472
+ line: service.line,
473
+ details: {
474
+ runtime: 'effect',
475
+ tag: service.key,
476
+ serviceName: service.name,
477
+ },
478
+ });
479
+ };
480
+ const proofFor = (node: Node, pattern: string): DependencyGraphProof => ({
481
+ filePath: node.getSourceFile().getFilePath(),
482
+ line: node.getStartLineNumber(),
483
+ pattern,
484
+ });
485
+ const addEdge = (edge: DependencyGraphEdge) => {
486
+ const key = `${edge.from}:${edge.kind}:${edge.to}`;
487
+ if (!edges.has(key)) edges.set(key, edge);
488
+ };
489
+
490
+ for (const [, service] of services) {
491
+ addNode({
492
+ id: serviceId(service.name),
493
+ kind: 'service',
494
+ label: service.name,
495
+ filePath: service.filePath,
496
+ line: service.line,
497
+ details: { runtime: 'effect', tag: service.key },
498
+ });
499
+ addEffectServiceNode(service);
500
+ }
501
+
502
+ for (const sourceFile of sourceFiles) {
503
+ for (const call of sourceFile.getDescendantsOfKind(
504
+ SyntaxKind.CallExpression,
505
+ )) {
506
+ const callee = call.getExpression().getText();
507
+ if (callee !== 'effectService' && !callee.endsWith('.effectService')) {
508
+ continue;
509
+ }
510
+
511
+ const [tagArgument, selectorArgument] = call.getArguments();
512
+ if (!tagArgument) continue;
513
+
514
+ const service = services.get(tagArgument.getText());
515
+ if (!service) continue;
516
+
517
+ const ownerId = ownerIdOf(call);
518
+ if (!ownerId) continue;
519
+
520
+ const members = selectorArgument
521
+ ? selectedMemberNames(selectorArgument)
522
+ : [];
523
+
524
+ if (members.length === 0) {
525
+ // Whole-service dependency: coarse edge, which is the honest drawing
526
+ // when the consumer took the whole thing.
527
+ addEdge({
528
+ from: ownerId,
529
+ to: serviceId(service.name),
530
+ kind: 'depends-on',
531
+ evidence: 'ast',
532
+ details: {
533
+ runtime: 'effect',
534
+ selection: 'whole-service',
535
+ ...(isInsideLoader(call) ? { resourceRole: 'loader' } : {}),
536
+ },
537
+ });
538
+ if (includeBackendRelations) {
539
+ addEdge({
540
+ from: ownerId,
541
+ to: effectServiceId(service.name),
542
+ kind: 'requires-service',
543
+ evidence: 'ast',
544
+ details: {
545
+ runtime: 'effect',
546
+ selection: 'whole-service',
547
+ },
548
+ proof: proofFor(call, call.getExpression().getText()),
549
+ });
550
+ }
551
+ continue;
552
+ }
553
+
554
+ for (const member of members) {
555
+ addNode({
556
+ id: memberId(service.name, member),
557
+ kind: 'property',
558
+ label: `${service.name}.${member}`,
559
+ filePath: service.filePath,
560
+ line: call.getStartLineNumber(),
561
+ details: { runtime: 'effect', member },
562
+ });
563
+ addEdge({
564
+ from: serviceId(service.name),
565
+ to: memberId(service.name, member),
566
+ kind: 'contains',
567
+ evidence: 'ast',
568
+ details: { member },
569
+ });
570
+ addEdge({
571
+ from: ownerId,
572
+ to: memberId(service.name, member),
573
+ kind: 'uses-property',
574
+ evidence: 'ast',
575
+ details: {
576
+ runtime: 'effect',
577
+ property: member,
578
+ ...(isInsideLoader(call) ? { resourceRole: 'loader' } : {}),
579
+ },
580
+ });
581
+ if (includeBackendRelations) {
582
+ addEdge({
583
+ from: ownerId,
584
+ to: effectServiceId(service.name),
585
+ kind: 'requires-service',
586
+ evidence: 'ast',
587
+ details: {
588
+ runtime: 'effect',
589
+ selection: 'member',
590
+ member,
591
+ },
592
+ proof: proofFor(call, call.getExpression().getText()),
593
+ });
594
+ }
595
+ }
596
+ }
597
+
598
+ for (const yielded of includeBackendRelations
599
+ ? sourceFile.getDescendantsOfKind(SyntaxKind.YieldExpression)
600
+ : []) {
601
+ const expression = yielded.getExpression();
602
+ if (!expression || !Node.isIdentifier(expression)) continue;
603
+ const service = services.get(expression.getText());
604
+ if (!service) continue;
605
+ const ownerId = ownerIdOf(yielded);
606
+ if (!ownerId) continue;
607
+ addEdge({
608
+ from: ownerId,
609
+ to: effectServiceId(service.name),
610
+ kind: 'requires-service',
611
+ evidence: 'ast',
612
+ details: { runtime: 'effect', selection: 'whole-service' },
613
+ proof: proofFor(yielded, `yield* ${service.name}`),
614
+ });
615
+ }
616
+ }
617
+
618
+ return { nodes: [...nodes.values()], edges: [...edges.values()] };
619
+ }
620
+
621
+ function isInsideLoader(node: Node): boolean {
622
+ let current: Node | undefined = node.getParent();
623
+ while (current) {
624
+ if (Node.isPropertyAssignment(current) && current.getName() === 'loader') {
625
+ return true;
626
+ }
627
+ current = current.getParent();
628
+ }
629
+ return false;
630
+ }
@@ -0,0 +1,37 @@
1
+ import { type MigratePrimitivesResult } from './primitives/migrate-primitives.js';
2
+ import { type MigrateRoutesResult } from './routes/migrate-routes.js';
3
+ import { type MigrateServicesResult } from './services/migrate-services.js';
4
+ import { type MigrateComponentsResult } from './components/migrate-components.js';
5
+ import { type MigrateArchitectureResult } from './architecture/migrate-architecture.js';
6
+ export type MigrateOptions = {
7
+ rootDir?: string;
8
+ tsConfigFilePath?: string;
9
+ configFilePath?: string;
10
+ files?: readonly string[];
11
+ write?: boolean;
12
+ check?: boolean;
13
+ json?: boolean;
14
+ jsonFilePath?: string;
15
+ failOnManual?: boolean;
16
+ eslint?: boolean;
17
+ collectionName?: string;
18
+ parentMount?: string;
19
+ parentNames?: readonly string[];
20
+ log?: (message: string) => void;
21
+ };
22
+ export type MigrateResult = {
23
+ primitives: MigratePrimitivesResult;
24
+ services: MigrateServicesResult;
25
+ routes: MigrateRoutesResult;
26
+ components: MigrateComponentsResult;
27
+ architecture: MigrateArchitectureResult;
28
+ changedFiles: string[];
29
+ diagnostics: {
30
+ primitives: MigratePrimitivesResult['diagnostics'];
31
+ services: MigrateServicesResult['diagnostics'];
32
+ routes: MigrateRoutesResult['diagnostics'];
33
+ components: MigrateComponentsResult['diagnostics'];
34
+ };
35
+ exitCode: number;
36
+ };
37
+ export declare function runMigration(options?: MigrateOptions): Promise<MigrateResult>;