@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,924 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { mkdir, writeFile } from 'node:fs/promises';
4
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve, } from 'node:path';
5
+ import { promisify } from 'node:util';
6
+ import { Node, Project, QuoteKind, SyntaxKind, } from 'ts-morph';
7
+ import { loadCraftDevToolsConfig } from './config.js';
8
+ import { migrateEslintConfig } from '../migration-workspace.js';
9
+ const execFileAsync = promisify(execFile);
10
+ const MANUAL_SCOPE = 'CRAFT_IMPLEMENTATION_REQUIRED';
11
+ export async function runServicesMigration(options = {}) {
12
+ const rootDir = resolve(options.rootDir ?? process.cwd());
13
+ const tsConfigFilePath = options.tsConfigFilePath
14
+ ? resolve(options.tsConfigFilePath)
15
+ : defaultTsConfig(rootDir);
16
+ const project = new Project({
17
+ ...(existsSync(tsConfigFilePath) ? { tsConfigFilePath } : {}),
18
+ manipulationSettings: { quoteKind: QuoteKind.Single },
19
+ skipAddingFilesFromTsConfig: false,
20
+ });
21
+ project.addSourceFilesAtPaths([
22
+ join(rootDir, '**/*.ts'),
23
+ `!${join(rootDir, '**/node_modules/**')}`,
24
+ `!${join(rootDir, '**/dist/**')}`,
25
+ `!${join(rootDir, '**/.angular/**')}`,
26
+ `!${join(rootDir, '**/*.d.ts')}`,
27
+ ]);
28
+ const config = loadCraftDevToolsConfig(rootDir, options.configFilePath);
29
+ const overrides = config.serviceMigration?.overrides ?? [];
30
+ const selected = options.files?.length
31
+ ? new Set(options.files.map((file) => resolve(rootDir, file)))
32
+ : undefined;
33
+ const projectSourceFiles = project.getSourceFiles().filter((file) => {
34
+ const path = resolve(file.getFilePath());
35
+ return isInside(path, rootDir);
36
+ });
37
+ const sourceFiles = projectSourceFiles.filter((file) => !selected || selected.has(resolve(file.getFilePath())));
38
+ const diagnostics = [];
39
+ const descriptors = sourceFiles.flatMap((sourceFile) => sourceFile
40
+ .getClasses()
41
+ .filter(isLegacyServiceClass)
42
+ .map((classDeclaration) => describeService(classDeclaration, overrides, rootDir, diagnostics)));
43
+ inferFunctionScopes(descriptors, projectSourceFiles);
44
+ diagnoseCycles(descriptors, diagnostics);
45
+ const files = new Map();
46
+ const touched = new Set();
47
+ for (const descriptor of descriptors) {
48
+ const file = descriptor.sourceFile;
49
+ const report = getFileReport(files, file.getFilePath());
50
+ report.symbols.push(descriptor.symbol);
51
+ if (descriptor.strategy === 'ignore')
52
+ continue;
53
+ const unsafe = getUnsafeReason(descriptor);
54
+ if (unsafe) {
55
+ diagnose(diagnostics, unsafe.code, descriptor, unsafe.message);
56
+ const companion = createCompanion(project, descriptor);
57
+ descriptor.helperSourceFile = companion.file;
58
+ report.companions.push(companion.file.getFilePath());
59
+ if (companion.changed)
60
+ touched.add(companion.file);
61
+ continue;
62
+ }
63
+ migrateSimpleClass(descriptor, descriptors, diagnostics);
64
+ touched.add(file);
65
+ }
66
+ rewriteConsumers(sourceFiles, descriptors, diagnostics, touched);
67
+ rewriteHttpOperationSubscribers(sourceFiles, descriptors, diagnostics, touched);
68
+ annotateRemainingExplicitSubscribes(sourceFiles, diagnostics, touched);
69
+ rewriteProviders(sourceFiles, descriptors, diagnostics, touched);
70
+ const eslintConfig = migrateEslintConfig(project, dirname(tsConfigFilePath));
71
+ if (eslintConfig)
72
+ touched.add(eslintConfig);
73
+ for (const file of touched) {
74
+ if (file === eslintConfig)
75
+ file.formatText();
76
+ else
77
+ file.organizeImports();
78
+ getFileReport(files, file.getFilePath()).changed = true;
79
+ }
80
+ if (options.write) {
81
+ await Promise.all([...touched].map((file) => file.save()));
82
+ }
83
+ let eslintRan = false;
84
+ if (options.write && options.eslint !== false && touched.size > 0) {
85
+ await runEslint([...touched].map((file) => file.getFilePath()), rootDir);
86
+ eslintRan = true;
87
+ }
88
+ const remainingLegacyServices = options.write
89
+ ? project
90
+ .getSourceFiles()
91
+ .reduce((count, file) => count + file.getClasses().filter(isLegacyServiceClass).length, 0)
92
+ : descriptors.length;
93
+ const result = {
94
+ changedFiles: [...touched].map((file) => file.getFilePath()),
95
+ files: [...files.values()],
96
+ diagnostics,
97
+ remainingLegacyServices,
98
+ eslintRan,
99
+ exitCode: (options.check && remainingLegacyServices > 0) ||
100
+ (options.failOnManual && diagnostics.length > 0)
101
+ ? 1
102
+ : 0,
103
+ };
104
+ if (options.jsonFilePath) {
105
+ const path = resolve(options.jsonFilePath);
106
+ await mkdir(dirname(path), { recursive: true });
107
+ await writeFile(path, `${JSON.stringify(result, null, 2)}\n`, 'utf8');
108
+ }
109
+ const log = options.log ?? console.log;
110
+ if (options.json)
111
+ log(JSON.stringify(result, null, 2));
112
+ else
113
+ logSummary(result, log, options.write === true);
114
+ return result;
115
+ }
116
+ function describeService(classDeclaration, overrides, rootDir, diagnostics) {
117
+ const sourceFile = classDeclaration.getSourceFile();
118
+ const symbol = classDeclaration.getNameOrThrow();
119
+ const module = relative(rootDir, sourceFile.getFilePath()).replace(/\\/g, '/');
120
+ const override = [...overrides]
121
+ .reverse()
122
+ .find((candidate) => (!candidate.file || matches(candidate.file, module)) &&
123
+ (!candidate.module || matches(candidate.module, module)) &&
124
+ (!candidate.symbol || candidate.symbol === symbol));
125
+ const decorator = classDeclaration
126
+ .getDecorators()
127
+ .find((item) => ['Injectable', 'Service'].includes(item.getName()));
128
+ const argument = decorator.getArguments()[0];
129
+ const text = argument?.getText() ?? '';
130
+ let scope;
131
+ if (classDeclaration.isAbstract())
132
+ scope = 'abstract';
133
+ else if (decorator.getName() === 'Service' &&
134
+ !/autoProvided\s*:\s*false/.test(text))
135
+ scope = 'global';
136
+ else if (/providedIn\s*:\s*['"]root['"]/.test(text))
137
+ scope = 'global';
138
+ else if (/providedIn\s*:/.test(text)) {
139
+ scope = 'toProvide';
140
+ diagnose(diagnostics, 'UNSUPPORTED_PROVIDED_IN', { sourceFile, symbol }, `providedIn de ${symbol} n'est pas transposable automatiquement.`);
141
+ }
142
+ else
143
+ scope = 'toProvide';
144
+ return {
145
+ classDeclaration,
146
+ sourceFile,
147
+ symbol,
148
+ name: override?.name ?? (symbol.replace(/Service$/, '') || symbol),
149
+ scope: override?.providedIn ?? scope,
150
+ strategy: override?.strategy ?? 'craftService',
151
+ override,
152
+ };
153
+ }
154
+ function inferFunctionScopes(descriptors, sourceFiles) {
155
+ const runtimeFiles = sourceFiles.filter((file) => !isTestFile(file));
156
+ for (const descriptor of descriptors) {
157
+ if (descriptor.scope !== 'toProvide' ||
158
+ descriptor.override?.providedIn ||
159
+ descriptor.strategy !== 'craftService' ||
160
+ getUnsafeReason(descriptor))
161
+ continue;
162
+ const injectionSites = runtimeFiles.flatMap((file) => collectInjectionSites(file, descriptor.symbol));
163
+ if (injectionSites.length === 1 && injectionSites[0]?.componentOwned)
164
+ descriptor.scope = 'function';
165
+ }
166
+ }
167
+ function collectInjectionSites(file, symbol) {
168
+ const sites = [];
169
+ for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
170
+ if (call.getExpression().getText() !== 'inject' ||
171
+ call.getArguments()[0]?.getText() !== symbol)
172
+ continue;
173
+ const owner = call.getFirstAncestorByKind(SyntaxKind.ClassDeclaration);
174
+ sites.push({
175
+ node: call,
176
+ componentOwned: Boolean(owner && isComponentOwnedClass(owner)),
177
+ });
178
+ }
179
+ for (const parameter of file.getDescendantsOfKind(SyntaxKind.Parameter)) {
180
+ if (parameter.getTypeNode()?.getText() !== symbol)
181
+ continue;
182
+ const constructorDeclaration = parameter.getFirstAncestorByKind(SyntaxKind.Constructor);
183
+ const owner = parameter.getFirstAncestorByKind(SyntaxKind.ClassDeclaration);
184
+ if (constructorDeclaration)
185
+ sites.push({
186
+ node: parameter,
187
+ componentOwned: Boolean(owner && isComponentOwnedClass(owner)),
188
+ });
189
+ }
190
+ return sites;
191
+ }
192
+ function isComponentOwnedClass(value) {
193
+ return value
194
+ .getDecorators()
195
+ .some((decorator) => ['Component', 'Directive'].includes(decorator.getName()));
196
+ }
197
+ function isTestFile(file) {
198
+ return /\.(?:spec|test)\.ts$/.test(file.getFilePath());
199
+ }
200
+ function getUnsafeReason(descriptor) {
201
+ const value = descriptor.classDeclaration;
202
+ if (descriptor.strategy === 'companion')
203
+ return {
204
+ code: 'NON_CONVERTIBLE_CLASS',
205
+ message: 'Stratégie companion imposée par la configuration.',
206
+ };
207
+ if (value.getExtends())
208
+ return {
209
+ code: 'NON_CONVERTIBLE_CLASS',
210
+ message: `${descriptor.symbol} utilise l'héritage.`,
211
+ };
212
+ if (value.getImplements().length)
213
+ return {
214
+ code: 'FRAMEWORK_CONTRACT',
215
+ message: `${descriptor.symbol} implémente un contrat de framework.`,
216
+ };
217
+ if (value.getGetAccessors().length || value.getSetAccessors().length)
218
+ return {
219
+ code: 'NON_CONVERTIBLE_CLASS',
220
+ message: `${descriptor.symbol} contient des accesseurs.`,
221
+ };
222
+ if (value.getConstructors().some(hasUnsafeConstructorLogic))
223
+ return {
224
+ code: 'NON_CONVERTIBLE_CLASS',
225
+ message: `${descriptor.symbol} contient de la logique constructeur.`,
226
+ };
227
+ if (value.getMethods().some((method) => /^ng[A-Z]/.test(method.getName())))
228
+ return {
229
+ code: 'LIFECYCLE_HOOK',
230
+ message: `${descriptor.symbol} contient un lifecycle Angular.`,
231
+ };
232
+ if (value
233
+ .getDescendantsOfKind(SyntaxKind.CallExpression)
234
+ .some((call) => call.getExpression().getText() === 'injectAsync'))
235
+ return {
236
+ code: 'INJECT_ASYNC',
237
+ message: `${descriptor.symbol} utilise injectAsync().`,
238
+ };
239
+ if (value
240
+ .getDescendantsOfKind(SyntaxKind.CallExpression)
241
+ .some(hasInjectionFlags))
242
+ return {
243
+ code: 'INJECTION_FLAGS',
244
+ message: `${descriptor.symbol} utilise des flags d'injection.`,
245
+ };
246
+ return undefined;
247
+ }
248
+ function hasUnsafeConstructorLogic(constructorDeclaration) {
249
+ return constructorDeclaration
250
+ .getStatements()
251
+ .some((statement) => !isMigratableConstructorStatement(statement));
252
+ }
253
+ function isMigratableConstructorStatement(statement) {
254
+ if (!Node.isExpressionStatement(statement))
255
+ return false;
256
+ const expression = statement.getExpression();
257
+ if (!Node.isCallExpression(expression))
258
+ return false;
259
+ return ['effect'].includes(expression.getExpression().getText());
260
+ }
261
+ function migrateSimpleClass(descriptor, all, diagnostics) {
262
+ const value = descriptor.classDeclaration;
263
+ const file = descriptor.sourceFile;
264
+ const yields = [];
265
+ const declarations = [];
266
+ const exposed = [];
267
+ let hasTrackedPrimitive = false;
268
+ migrateHttpResources(descriptor, diagnostics);
269
+ const httpOperations = collectHttpOperations(descriptor);
270
+ descriptor.httpOperations = httpOperations;
271
+ const propertyNames = new Set(value.getProperties().map((property) => property.getName()));
272
+ for (const parameter of value
273
+ .getConstructors()
274
+ .flatMap((ctor) => ctor.getParameters())) {
275
+ const token = parameter.getTypeNode()?.getText();
276
+ if (!token)
277
+ continue;
278
+ if (token === 'HttpClient' &&
279
+ !hasHttpClientUsageOutsideMigratedMethods(value, parameter.getName(), httpOperations))
280
+ continue;
281
+ yields.push(makeYield(toInternalName(parameter.getName()), token, descriptor, all, diagnostics));
282
+ }
283
+ for (const property of value.getProperties()) {
284
+ const name = property.getName();
285
+ const internalName = toInternalName(name);
286
+ const initializer = property.getInitializer();
287
+ if (!initializer) {
288
+ diagnose(diagnostics, 'NON_CONVERTIBLE_CLASS', descriptor, `La propriété ${name} n'a pas d'initialiseur.`);
289
+ continue;
290
+ }
291
+ if (Node.isCallExpression(initializer) &&
292
+ initializer.getExpression().getText() === 'inject') {
293
+ const token = initializer.getArguments()[0]?.getText();
294
+ if (token === 'HttpClient' &&
295
+ !hasHttpClientUsageOutsideMigratedMethods(value, name, httpOperations))
296
+ continue;
297
+ if (token)
298
+ yields.push(makeYield(internalName, token, descriptor, all, diagnostics));
299
+ }
300
+ else {
301
+ const shouldYield = isPrimitiveCreation(initializer);
302
+ if (shouldYield) {
303
+ hasTrackedPrimitive = true;
304
+ }
305
+ const imperativeStateComment = property
306
+ .getFullText()
307
+ .includes('CRAFT_IMPERATIVE_CODE_DETECTED')
308
+ ? '// CRAFT_IMPERATIVE_CODE_DETECTED: imperative code detected, prefer a declarative approach.\n'
309
+ : '';
310
+ declarations.push(`${imperativeStateComment}const ${internalName} = ${shouldYield ? 'yield* ' : ''}${rewriteThis(initializer.getText(), propertyNames)};`);
311
+ }
312
+ if (property.getScope() !== 'private' &&
313
+ property.getScope() !== 'protected')
314
+ exposed.push(`${name}: ${internalName}`);
315
+ }
316
+ for (const constructorStatement of value
317
+ .getConstructors()
318
+ .flatMap((ctor) => ctor.getStatements())) {
319
+ declarations.push(`${rewriteThis(constructorStatement.getText(), propertyNames)}`);
320
+ }
321
+ for (const method of value.getMethods()) {
322
+ const name = method.getName();
323
+ const httpOperation = httpOperations.find((operation) => operation.methodName === name);
324
+ if (httpOperation) {
325
+ declarations.push(printHttpOperationGenerator(httpOperation));
326
+ if (method.getScope() !== 'private' && method.getScope() !== 'protected')
327
+ exposed.push(name);
328
+ ensureCoreImports(file, ['CraftHttpClient']);
329
+ continue;
330
+ }
331
+ const asyncKeyword = method.isAsync() ? 'async ' : '';
332
+ const star = method.isGenerator() ? '*' : '';
333
+ const typeParameters = method.getTypeParameters().map((parameter) => parameter.getText()).join(', ');
334
+ const params = method
335
+ .getParameters()
336
+ .map((parameter) => parameter.getText())
337
+ .join(', ');
338
+ const containsMigratedQuery = method.getDescendantsOfKind(SyntaxKind.CallExpression)
339
+ .some((call) => call.getExpression().getText() === 'query');
340
+ const returnType = containsMigratedQuery ? undefined : method.getReturnTypeNode()?.getText();
341
+ const body = method.getBodyText() ?? '';
342
+ const imperativeWorkflowComment = getImperativeWorkflowComment(method);
343
+ if (imperativeWorkflowComment) {
344
+ diagnose(diagnostics, 'IMPERATIVE_WORKFLOW_REQUIRES_REVIEW', descriptor, `${descriptor.name}.${name} orchestre impérativement un submit, plusieurs mises à jour d'état et/ou une navigation. Préférer insertFormSubmit avec une réaction au statut du formulaire, ou source$ avec on$/effect.`);
345
+ }
346
+ declarations.push(`${imperativeWorkflowComment ? `${imperativeWorkflowComment}\n` : ''}${asyncKeyword}function${star} ${name}${typeParameters ? `<${typeParameters}>` : ''}(${params})${returnType ? `: ${returnType}` : ''} {${rewriteThis(body, propertyNames)}}`);
347
+ if (method.getScope() !== 'private' && method.getScope() !== 'protected')
348
+ exposed.push(name);
349
+ }
350
+ const contract = `{ ${value
351
+ .getMethods()
352
+ .filter((method) => method.getScope() !== 'private' && method.getScope() !== 'protected')
353
+ .map((method) => `${method.getName()}(${method
354
+ .getParameters()
355
+ .map((parameter) => parameter.getText())
356
+ .join(', ')}): ${method.getReturnTypeNode()?.getText() ?? 'unknown'};`)
357
+ .join(' ')} }`;
358
+ const factory = descriptor.scope === 'abstract'
359
+ ? `abstract<${contract}>()`
360
+ : `${yields.length > 0 || hasTrackedPrimitive ? 'function*' : 'function'} () {\n${[...yields, ...declarations].map((line) => ` ${line}`).join('\n')}\n return { ${exposed.join(', ')} };\n}`;
361
+ const helpers = descriptor.scope === 'abstract'
362
+ ? `${descriptor.name}Requirement, ${descriptor.name}, provide${descriptor.name}`
363
+ : `${descriptor.name}${isProviderCapableScope(descriptor.scope) ? `, provide${descriptor.name}` : ''}`;
364
+ const typeAlias = descriptor.scope === 'abstract'
365
+ ? `\nexport type ${descriptor.symbol} = ${contract};`
366
+ : `\nexport type ${descriptor.symbol} = GetServiceOutput<typeof ${descriptor.name}>;`;
367
+ value.replaceWithText(`export const { ${helpers} } = craftService({ name: '${descriptor.name}', providedIn: '${descriptor.scope}' }, ${factory});${typeAlias}`);
368
+ ensureCoreImports(file, [
369
+ 'craftService',
370
+ ...(descriptor.scope === 'abstract' ? [] : ['GetServiceOutput']),
371
+ ...(descriptor.scope === 'abstract' ? ['abstract'] : []),
372
+ ]);
373
+ removeDecoratorImport(file, 'Injectable');
374
+ removeDecoratorImport(file, 'Service');
375
+ }
376
+ function getImperativeWorkflowComment(method) {
377
+ const calls = method.getDescendantsOfKind(SyntaxKind.CallExpression);
378
+ const hasSubmit = calls.some((call) => {
379
+ const expression = call.getExpression();
380
+ return (expression.getText() === 'submit' ||
381
+ (Node.isPropertyAccessExpression(expression) &&
382
+ expression.getName() === 'submit'));
383
+ });
384
+ if (!hasSubmit)
385
+ return undefined;
386
+ const stateWrites = calls.filter((call) => {
387
+ const expression = call.getExpression();
388
+ return (Node.isPropertyAccessExpression(expression) &&
389
+ ['set', 'update'].includes(expression.getName()));
390
+ }).length;
391
+ const hasNavigation = calls.some((call) => {
392
+ const expression = call.getExpression();
393
+ return (Node.isPropertyAccessExpression(expression) &&
394
+ ['navigate', 'navigateByUrl'].includes(expression.getName()));
395
+ });
396
+ if (stateWrites < 2 && !(stateWrites >= 1 && hasNavigation))
397
+ return undefined;
398
+ return '// CRAFT_REACTIVE_WORKFLOW_RECOMMENDED: workflow impératif détecté...';
399
+ }
400
+ // The generator primitives must always be consumed with `yield*` inside the
401
+ // service factory so their dependency map folds into the service tree.
402
+ function isPrimitiveCreation(initializer) {
403
+ return (Node.isCallExpression(initializer) &&
404
+ ['query', 'mutation', 'asyncProcess', 'state', 'queryParams'].includes(initializer.getExpression().getText()));
405
+ }
406
+ function migrateHttpResources(descriptor, diagnostics) {
407
+ const calls = descriptor.classDeclaration
408
+ .getDescendantsOfKind(SyntaxKind.CallExpression)
409
+ .filter((call) => call.getExpression().getText() === 'httpResource');
410
+ for (const call of calls) {
411
+ const params = call.getArguments()[0];
412
+ if (!params)
413
+ continue;
414
+ let paramsText = params.getText();
415
+ const chainedNames = [
416
+ ...paramsText.matchAll(/\bchain\(\s*([A-Za-z_$][\w$]*)\s*\)/g),
417
+ ].map((match) => match[1]);
418
+ if (/\bchain\s*\(/.test(paramsText)) {
419
+ const rewritten = paramsText
420
+ .replace(/\(\s*\{\s*chain\s*\}\s*\)\s*=>/, '() =>')
421
+ .replace(/\bchain\(\s*([A-Za-z_$][\w$]*)\s*\)\s*;/g, '$1.value();');
422
+ if (/\bchain\s*\(/.test(rewritten)) {
423
+ diagnose(diagnostics, 'RESOURCE_CHAIN_REQUIRES_REWRITE', descriptor, 'httpResource utilise une forme complexe de chain(...); la dépendance entre queries doit être migrée manuellement.');
424
+ continue;
425
+ }
426
+ paramsText = rewritten;
427
+ const methodDeclaration = call.getFirstAncestorByKind(SyntaxKind.MethodDeclaration);
428
+ for (const name of chainedNames) {
429
+ methodDeclaration
430
+ ?.getParameter(name)
431
+ ?.setType('{ readonly value: () => unknown }');
432
+ }
433
+ }
434
+ const stateType = call.getTypeArguments()[0]?.getText() ?? 'unknown';
435
+ const optionsNode = call.getArguments()[1];
436
+ const options = Node.isObjectLiteralExpression(optionsNode)
437
+ ? optionsNode
438
+ .getProperties()
439
+ .map((property) => property.getText())
440
+ .join(',\n ')
441
+ : optionsNode
442
+ ? `...${optionsNode.getText()}`
443
+ : '';
444
+ const method = /\bmethod\s*:\s*['"]([A-Za-z]+)['"]/.exec(paramsText)?.[1]?.toUpperCase() ??
445
+ 'GET';
446
+ call.replaceWithText(`query({
447
+ params: ${paramsText},
448
+ ${options ? `${options},` : ''}
449
+ loader: function* ({ params: request }) {
450
+ return yield* CraftHttpClient.request(({ response }) => ({
451
+ ...(typeof request === 'string' ? { url: request } : request),
452
+ method: '${method}',
453
+ success: response<${stateType}>(),
454
+ }));
455
+ },
456
+ }, ({ set, update }) => ({ set, update }))`);
457
+ ensureCoreImports(descriptor.sourceFile, ['CraftHttpClient', 'query']);
458
+ }
459
+ }
460
+ function collectHttpOperations(descriptor) {
461
+ return descriptor.classDeclaration
462
+ .getMethods()
463
+ .map((method) => collectHttpOperation(method))
464
+ .filter((operation) => Boolean(operation));
465
+ }
466
+ function collectHttpOperation(method) {
467
+ const body = method.getBody();
468
+ const statements = body && Node.isBlock(body) ? body.getStatements() : [];
469
+ if (statements.length !== 1)
470
+ return undefined;
471
+ const statement = statements[0];
472
+ if (!Node.isReturnStatement(statement))
473
+ return undefined;
474
+ const expression = statement.getExpression();
475
+ if (!Node.isCallExpression(expression))
476
+ return undefined;
477
+ const access = expression.getExpression();
478
+ if (!Node.isPropertyAccessExpression(access))
479
+ return undefined;
480
+ const httpMethod = access.getName();
481
+ if (!isWritableHttpMethod(httpMethod))
482
+ return undefined;
483
+ if (!looksLikeHttpClientReceiver(access.getExpression().getText()))
484
+ return undefined;
485
+ const args = expression.getArguments();
486
+ const urlText = args[0]?.getText();
487
+ if (!urlText)
488
+ return undefined;
489
+ const payloadText = httpMethod === 'delete' ? undefined : (args[1]?.getText() ?? '{}');
490
+ const responseType = expression.getTypeArguments()[0]?.getText() ?? 'unknown';
491
+ const typeParametersText = method
492
+ .getTypeParameters()
493
+ .map((parameter) => parameter.getText())
494
+ .join(', ');
495
+ const parametersText = method
496
+ .getParameters()
497
+ .map((parameter) => parameter.getText())
498
+ .join(', ');
499
+ return {
500
+ methodName: method.getName(),
501
+ httpMethod,
502
+ responseType,
503
+ urlText,
504
+ payloadText,
505
+ parameterCount: method.getParameters().length,
506
+ typeParametersText,
507
+ parametersText,
508
+ };
509
+ }
510
+ function isWritableHttpMethod(value) {
511
+ return ['post', 'put', 'patch', 'delete'].includes(value);
512
+ }
513
+ function looksLikeHttpClientReceiver(value) {
514
+ return /(^|\.|_)http(Client)?$/i.test(value);
515
+ }
516
+ function printHttpOperationGenerator(operation) {
517
+ const payload = operation.payloadText === undefined
518
+ ? ''
519
+ : `\n payload: ${operation.payloadText},`;
520
+ return `function* ${operation.methodName}${operation.typeParametersText ? `<${operation.typeParametersText}>` : ''}(${operation.parametersText}) {
521
+ return yield* CraftHttpClient.${operation.httpMethod}(({ response }) => ({
522
+ url: ${operation.urlText},${payload}
523
+ success: response<${operation.responseType}>(),
524
+ }));
525
+ }`;
526
+ }
527
+ function hasHttpClientUsageOutsideMigratedMethods(serviceClass, localName, operations) {
528
+ const migratedMethods = new Set(operations.map((operation) => operation.methodName));
529
+ return serviceClass.getMethods().some((method) => {
530
+ if (migratedMethods.has(method.getName()))
531
+ return false;
532
+ const text = method.getBodyText() ?? '';
533
+ return (new RegExp(`\\bthis\\.${escapeRegex(localName)}\\b`).test(text) ||
534
+ new RegExp(`\\b${escapeRegex(localName)}\\s*\\.`).test(text));
535
+ });
536
+ }
537
+ function makeYield(localName, token, owner, all, diagnostics) {
538
+ const dependency = all.find((item) => item.symbol === token);
539
+ if (token === 'Router') {
540
+ ensureCoreImports(owner.sourceFile, ['CraftRouter']);
541
+ return `const ${localName} = yield* CraftRouter();`;
542
+ }
543
+ if (!dependency) {
544
+ if (token === 'HttpClient') {
545
+ diagnose(diagnostics, 'HTTP_CLIENT_REWRITE_REQUIRED', owner, 'Les appels HttpClient impératifs restants doivent être migrés vers mutation/CraftHttpClient.');
546
+ ensureExternalAdapter(owner, token);
547
+ return `const ${localName} = yield* ${token}(); // HTTP_CLIENT_REWRITE_REQUIRED`;
548
+ }
549
+ diagnose(diagnostics, 'THIRD_PARTY_SCOPE_REQUIRED', owner, `${token} requiert un adapter toCraftService avec scope CRAFT_SCOPE_REQUIRED.`);
550
+ ensureExternalAdapter(owner, token);
551
+ return `const ${localName} = yield* ${token}(); // CRAFT_SCOPE_REQUIRED`;
552
+ }
553
+ ensureRelativeImport(owner.sourceFile, dependency, dependency.name);
554
+ return `const ${localName} = yield* ${dependency.name}();`;
555
+ }
556
+ function ensureExternalAdapter(owner, token) {
557
+ const file = owner.sourceFile;
558
+ if (file.getFullText().includes(`const { ${token} } = toCraftService(`))
559
+ return;
560
+ const statementIndex = file.getStatements().indexOf(owner.classDeclaration);
561
+ file.insertStatements(Math.max(0, statementIndex), `const { ${token} } = toCraftService({ name: '${token}', providedIn: 'global', token: ${token} }); // HTTP_CLIENT_REWRITE_REQUIRED`);
562
+ ensureAngularImports(file, ['toCraftService']);
563
+ }
564
+ function createCompanion(project, descriptor) {
565
+ const originalPath = descriptor.sourceFile.getFilePath();
566
+ const companionPath = originalPath.slice(0, -extname(originalPath).length) + '.craft.ts';
567
+ const module = `./${basename(originalPath, extname(originalPath))}`;
568
+ const companionScope = descriptor.scope === 'abstract' ? 'toProvide' : descriptor.scope;
569
+ const helpers = `${descriptor.name}${isProviderCapableScope(companionScope) ? `, provide${descriptor.name}` : ''}`;
570
+ const provide = isProviderCapableScope(companionScope)
571
+ ? ` provide: () => [${descriptor.symbol}],\n`
572
+ : '';
573
+ const text = `// Generated by craft-migrate-services. ${MANUAL_SCOPE}: keep the legacy class until it can be rewritten.\nimport { toCraftService } from '@craft-ts/angular';\nimport { ${descriptor.symbol} } from '${module}';\n\nexport const { ${helpers} } = toCraftService({\n name: '${descriptor.name}',\n providedIn: '${companionScope}',\n token: ${descriptor.symbol},\n${provide}});\n`;
574
+ const existing = project.getSourceFile(companionPath);
575
+ if (existing) {
576
+ if (existing.getFullText().includes(MANUAL_SCOPE) &&
577
+ existing.getFullText().includes(`token: ${descriptor.symbol}`)) {
578
+ return { file: existing, changed: false };
579
+ }
580
+ existing.replaceWithText(text);
581
+ return { file: existing, changed: true };
582
+ }
583
+ return {
584
+ file: project.createSourceFile(companionPath, text, { overwrite: false }),
585
+ changed: true,
586
+ };
587
+ }
588
+ function rewriteConsumers(files, descriptors, diagnostics, touched) {
589
+ for (const file of files) {
590
+ for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
591
+ if (call.wasForgotten() || call.getExpression().getText() !== 'inject')
592
+ continue;
593
+ const token = call.getArguments()[0]?.getText();
594
+ const service = descriptors.find((item) => item.symbol === token);
595
+ if (!service)
596
+ continue;
597
+ call.replaceWithText(`inject${service.name}()`);
598
+ ensureRelativeImport(file, service, `inject${service.name}`);
599
+ touched.add(file);
600
+ }
601
+ }
602
+ }
603
+ function rewriteHttpOperationSubscribers(files, descriptors, diagnostics, touched) {
604
+ for (const file of files) {
605
+ for (const classDeclaration of file.getClasses()) {
606
+ const injectedServices = collectInjectedCraftServices(classDeclaration, descriptors);
607
+ if (injectedServices.size === 0)
608
+ continue;
609
+ for (const subscribeCall of [
610
+ ...classDeclaration.getDescendantsOfKind(SyntaxKind.CallExpression),
611
+ ]) {
612
+ if (subscribeCall.wasForgotten())
613
+ continue;
614
+ const subscribeAccess = subscribeCall.getExpression();
615
+ if (!Node.isPropertyAccessExpression(subscribeAccess) ||
616
+ subscribeAccess.getName() !== 'subscribe')
617
+ continue;
618
+ const operationCall = subscribeAccess.getExpression();
619
+ if (!Node.isCallExpression(operationCall))
620
+ continue;
621
+ const operationAccess = operationCall.getExpression();
622
+ if (!Node.isPropertyAccessExpression(operationAccess))
623
+ continue;
624
+ const receiverName = readReceiverPropertyName(operationAccess.getExpression().getText());
625
+ if (!receiverName)
626
+ continue;
627
+ const service = injectedServices.get(receiverName);
628
+ if (!service)
629
+ continue;
630
+ const operation = service.httpOperations?.find((item) => item.methodName === operationAccess.getName());
631
+ if (!operation)
632
+ continue;
633
+ if (subscribeCall.getArguments().length > 0 ||
634
+ operation.parameterCount === 0) {
635
+ diagnose(diagnostics, 'MUTATION_SUBSCRIBE_REQUIRES_REWRITE', { ...service, sourceFile: file }, `${service.name}.${operation.methodName} est un appel HTTP mutable avec subscribe complexe; créer une mutation locale et migrer les callbacks manuellement.`);
636
+ continue;
637
+ }
638
+ const mutationName = ensureLocalMutationProperty(classDeclaration, service, operation);
639
+ subscribeCall.replaceWithText(printMutationCall(mutationName, operationCall));
640
+ ensureCoreImports(file, ['mutation']);
641
+ ensureRelativeImport(file, service, service.name);
642
+ touched.add(file);
643
+ }
644
+ }
645
+ }
646
+ }
647
+ const EXPLICIT_SUBSCRIBE_REVIEW_COMMENT = '// CRAFT_EXPLICIT_SUBSCRIBE_REVIEW: subscribe explicite conservé; vérifier si query, mutation, asyncProcess ou le couple source$/on$ permet un workflow Craft déclaratif.';
648
+ function annotateRemainingExplicitSubscribes(files, diagnostics, touched) {
649
+ for (const file of files) {
650
+ const statements = new Set();
651
+ for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
652
+ const expression = call.getExpression();
653
+ if (!Node.isPropertyAccessExpression(expression) ||
654
+ expression.getName() !== 'subscribe')
655
+ continue;
656
+ const statement = call.getFirstAncestor((ancestor) => Node.isExpressionStatement(ancestor) ||
657
+ Node.isReturnStatement(ancestor) ||
658
+ Node.isVariableStatement(ancestor));
659
+ if (statement)
660
+ statements.add(statement);
661
+ }
662
+ if (statements.size === 0)
663
+ continue;
664
+ let annotated = false;
665
+ for (const statement of [...statements].sort((left, right) => right.getStart() - left.getStart())) {
666
+ if (statement.wasForgotten() ||
667
+ statement.getFullText().includes('CRAFT_EXPLICIT_SUBSCRIBE_REVIEW'))
668
+ continue;
669
+ statement.replaceWithText(`${EXPLICIT_SUBSCRIBE_REVIEW_COMMENT}\n${statement.getText()}`);
670
+ annotated = true;
671
+ }
672
+ if (!annotated)
673
+ continue;
674
+ diagnostics.push({
675
+ code: 'EXPLICIT_SUBSCRIBE_REQUIRES_REVIEW',
676
+ filePath: file.getFilePath(),
677
+ message: 'Un subscribe explicite reste à migrer; évaluer query, mutation, asyncProcess ou le couple source$/on$ selon le cycle de vie de l’opération.',
678
+ manual: true,
679
+ });
680
+ touched.add(file);
681
+ }
682
+ }
683
+ function collectInjectedCraftServices(classDeclaration, descriptors) {
684
+ const byInjectHelper = new Map(descriptors.map((descriptor) => [
685
+ `inject${descriptor.name}`,
686
+ descriptor,
687
+ ]));
688
+ const result = new Map();
689
+ for (const property of classDeclaration.getProperties()) {
690
+ const initializer = property.getInitializer();
691
+ if (!initializer ||
692
+ !Node.isCallExpression(initializer) ||
693
+ !Node.isIdentifier(initializer.getExpression()))
694
+ continue;
695
+ const service = byInjectHelper.get(initializer.getExpression().getText());
696
+ if (service)
697
+ result.set(property.getName(), service);
698
+ }
699
+ return result;
700
+ }
701
+ function readReceiverPropertyName(text) {
702
+ const match = /^(?:this\.)?([A-Za-z_$][\w$]*)$/.exec(text.trim());
703
+ return match?.[1];
704
+ }
705
+ function ensureLocalMutationProperty(classDeclaration, service, operation) {
706
+ const baseName = `${operation.methodName}Mutation`;
707
+ const mutationName = uniqueClassPropertyName(classDeclaration, baseName);
708
+ if (classDeclaration.getProperty(mutationName))
709
+ return mutationName;
710
+ const paramsType = `Parameters<GetServiceOutput<typeof ${service.name}>['${operation.methodName}']>`;
711
+ const method = operation.parameterCount === 1
712
+ ? `(params: ${paramsType}[0]) => params`
713
+ : `(params: ${paramsType}) => params`;
714
+ const call = operation.parameterCount === 1
715
+ ? `${toLocalServiceName(service.name)}.${operation.methodName}(params)`
716
+ : `${toLocalServiceName(service.name)}.${operation.methodName}(...params)`;
717
+ classDeclaration.insertProperty(0, {
718
+ name: mutationName,
719
+ isReadonly: true,
720
+ initializer: `mutation({
721
+ method: ${method},
722
+ loader: function* ({ params }) {
723
+ const ${toLocalServiceName(service.name)} = yield* ${service.name}();
724
+ return yield* ${call};
725
+ },
726
+ })`,
727
+ });
728
+ return mutationName;
729
+ }
730
+ function toLocalServiceName(name) {
731
+ return `${name.charAt(0).toLowerCase()}${name.slice(1)}`;
732
+ }
733
+ function uniqueClassPropertyName(classDeclaration, baseName) {
734
+ if (!classDeclaration.getProperty(baseName))
735
+ return baseName;
736
+ let index = 2;
737
+ while (classDeclaration.getProperty(`${baseName}${index}`))
738
+ index += 1;
739
+ return `${baseName}${index}`;
740
+ }
741
+ function printMutationCall(mutationName, operationCall) {
742
+ const args = operationCall.getArguments().map((arg) => arg.getText());
743
+ const params = args.length === 1 ? args[0] : `[${args.join(', ')}]`;
744
+ return `this.${mutationName}.mutate(${params})`;
745
+ }
746
+ function rewriteProviders(files, descriptors, diagnostics, touched) {
747
+ for (const file of files) {
748
+ const assignments = file
749
+ .getDescendantsOfKind(SyntaxKind.PropertyAssignment)
750
+ .filter((property) => property.getName() === 'providers');
751
+ for (const assignment of assignments) {
752
+ const array = assignment.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression);
753
+ if (!array) {
754
+ const owner = descriptors[0];
755
+ if (owner)
756
+ diagnose(diagnostics, 'COMPLEX_PROVIDER', { ...owner, sourceFile: file }, 'Provider non statique.');
757
+ continue;
758
+ }
759
+ for (const element of [...array.getElements()]) {
760
+ if (!Node.isIdentifier(element))
761
+ continue;
762
+ const service = descriptors.find((item) => item.symbol === element.getText());
763
+ if (!service || service.scope === 'global')
764
+ continue;
765
+ if (service.scope === 'function') {
766
+ array.removeElement(element);
767
+ touched.add(file);
768
+ continue;
769
+ }
770
+ element.replaceWithText(`provide${service.name}()`);
771
+ ensureRelativeImport(file, service, `provide${service.name}`);
772
+ touched.add(file);
773
+ }
774
+ }
775
+ }
776
+ }
777
+ function isProviderCapableScope(scope) {
778
+ return [
779
+ 'toProvide',
780
+ 'manuallyProvidedAtRoot',
781
+ 'abstract',
782
+ ].includes(scope);
783
+ }
784
+ function diagnoseCycles(descriptors, diagnostics) {
785
+ const edges = new Map();
786
+ for (const descriptor of descriptors) {
787
+ const deps = descriptor.classDeclaration
788
+ .getDescendantsOfKind(SyntaxKind.CallExpression)
789
+ .filter((call) => call.getExpression().getText() === 'inject')
790
+ .map((call) => call.getArguments()[0]?.getText())
791
+ .filter((value) => Boolean(value));
792
+ edges.set(descriptor.symbol, deps);
793
+ }
794
+ for (const descriptor of descriptors) {
795
+ if (hasPath(edges, descriptor.symbol, descriptor.symbol, new Set(), true)) {
796
+ diagnose(diagnostics, 'DI_CYCLE', descriptor, `Cycle DI impliquant ${descriptor.symbol}.`);
797
+ }
798
+ }
799
+ }
800
+ function hasPath(edges, current, target, seen, first) {
801
+ if (!first && current === target)
802
+ return true;
803
+ if (seen.has(current))
804
+ return false;
805
+ seen.add(current);
806
+ return (edges.get(current) ?? []).some((next) => hasPath(edges, next, target, seen, false));
807
+ }
808
+ function isLegacyServiceClass(value) {
809
+ return value
810
+ .getDecorators()
811
+ .some((decorator) => ['Injectable', 'Service'].includes(decorator.getName()));
812
+ }
813
+ function hasInjectionFlags(call) {
814
+ if (call.getExpression().getText() !== 'inject' ||
815
+ call.getArguments().length < 2)
816
+ return false;
817
+ return /optional|self|host|skipSelf/.test(call.getArguments()[1].getText());
818
+ }
819
+ function ensureCoreImports(file, names) {
820
+ ensureNamedImports(file, '@craft-ts/core', names);
821
+ }
822
+ function ensureAngularImports(file, names) {
823
+ ensureNamedImports(file, '@craft-ts/angular', names);
824
+ }
825
+ function ensureNamedImports(file, moduleSpecifier, names) {
826
+ let declaration = file.getImportDeclaration(moduleSpecifier);
827
+ if (!declaration)
828
+ declaration = file.addImportDeclaration({
829
+ moduleSpecifier,
830
+ });
831
+ const existing = new Set(declaration.getNamedImports().map((item) => item.getName()));
832
+ declaration.addNamedImports(names.filter((name) => !existing.has(name)));
833
+ }
834
+ function ensureRelativeImport(file, service, name) {
835
+ if (file === service.sourceFile)
836
+ return;
837
+ const helperFile = service.helperSourceFile ?? service.sourceFile;
838
+ let specifier = relative(dirname(file.getFilePath()), helperFile.getFilePath())
839
+ .replace(/\\/g, '/')
840
+ .replace(/\.ts$/, '');
841
+ if (!specifier.startsWith('.'))
842
+ specifier = `./${specifier}`;
843
+ let declaration = file.getImportDeclaration(specifier);
844
+ if (!declaration)
845
+ declaration = file.addImportDeclaration({ moduleSpecifier: specifier });
846
+ if (!declaration.getNamedImports().some((item) => item.getName() === name))
847
+ declaration.addNamedImport(name);
848
+ }
849
+ function removeDecoratorImport(file, name) {
850
+ for (const declaration of file.getImportDeclarations()) {
851
+ const imported = declaration
852
+ .getNamedImports()
853
+ .find((item) => item.getName() === name);
854
+ if (!imported)
855
+ continue;
856
+ imported.remove();
857
+ if (!declaration.getDefaultImport() &&
858
+ !declaration.getNamespaceImport() &&
859
+ declaration.getNamedImports().length === 0)
860
+ declaration.remove();
861
+ }
862
+ }
863
+ function diagnose(diagnostics, code, descriptor, message) {
864
+ if (diagnostics.some((item) => item.code === code &&
865
+ item.filePath === descriptor.sourceFile.getFilePath() &&
866
+ item.symbol === descriptor.symbol))
867
+ return;
868
+ diagnostics.push({
869
+ code,
870
+ filePath: descriptor.sourceFile.getFilePath(),
871
+ symbol: descriptor.symbol,
872
+ message,
873
+ manual: true,
874
+ });
875
+ }
876
+ function getFileReport(map, filePath) {
877
+ let report = map.get(filePath);
878
+ if (!report) {
879
+ report = { filePath, changed: false, symbols: [], companions: [] };
880
+ map.set(filePath, report);
881
+ }
882
+ return report;
883
+ }
884
+ function rewriteThis(text, propertyNames) {
885
+ let rewritten = text;
886
+ for (const name of propertyNames) {
887
+ rewritten = rewritten.replace(new RegExp(`\\bthis\\.${escapeRegex(name)}\\b`, 'g'), toInternalName(name));
888
+ }
889
+ return rewritten.replace(/\bthis\./g, '');
890
+ }
891
+ function toInternalName(name) {
892
+ return `_${name}`;
893
+ }
894
+ function matches(pattern, value) {
895
+ if (!pattern.includes('*'))
896
+ return pattern === value || value.endsWith(pattern);
897
+ const regex = new RegExp(`^${pattern.split('*').map(escapeRegex).join('.*')}$`);
898
+ return regex.test(value);
899
+ }
900
+ function escapeRegex(value) {
901
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
902
+ }
903
+ function isInside(filePath, rootDir) {
904
+ const path = relative(rootDir, filePath);
905
+ return path === '' || (!path.startsWith('..') && !isAbsolute(path));
906
+ }
907
+ function defaultTsConfig(rootDir) {
908
+ for (const name of ['tsconfig.app.json', 'tsconfig.json']) {
909
+ const candidate = join(rootDir, name);
910
+ if (existsSync(candidate))
911
+ return candidate;
912
+ }
913
+ return join(rootDir, 'tsconfig.json');
914
+ }
915
+ async function runEslint(files, cwd) {
916
+ const local = join(cwd, 'node_modules', '.bin', 'eslint');
917
+ await execFileAsync(existsSync(local) ? local : 'eslint', ['--fix', ...files], { cwd });
918
+ }
919
+ function logSummary(result, log, wrote) {
920
+ log(`${wrote ? 'Migrated' : 'Would migrate'} ${result.changedFiles.length} file(s); ${result.diagnostics.length} manual diagnostic(s).`);
921
+ for (const diagnostic of result.diagnostics)
922
+ log(`[${diagnostic.code}] ${diagnostic.filePath}: ${diagnostic.message}`);
923
+ }
924
+ //# sourceMappingURL=migrate-services.js.map