@craft-ng/dev-tools 0.5.0-beta.4 → 0.5.1-beta.0

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 (67) hide show
  1. package/README.md +132 -23
  2. package/package.json +6 -2
  3. package/src/bin/craft-migrate-primitives.d.ts +2 -0
  4. package/src/bin/craft-migrate-primitives.js +71 -0
  5. package/src/bin/craft-migrate-primitives.js.map +1 -0
  6. package/src/bin/craft-migrate-routes.d.ts +2 -0
  7. package/src/bin/craft-migrate-routes.js +77 -0
  8. package/src/bin/craft-migrate-routes.js.map +1 -0
  9. package/src/bin/craft-migrate-services.d.ts +2 -0
  10. package/src/bin/craft-migrate-services.js +75 -0
  11. package/src/bin/craft-migrate-services.js.map +1 -0
  12. package/src/bin/craft-migrate.d.ts +2 -0
  13. package/src/bin/craft-migrate.js +90 -0
  14. package/src/bin/craft-migrate.js.map +1 -0
  15. package/src/eslint-rules/global-exception-registry-match.cjs +4 -4
  16. package/src/eslint-rules/index.cjs +6 -0
  17. package/src/eslint-rules/require-craft-exception-handler.cjs +142 -0
  18. package/src/eslint-rules/require-exception-component-di-check.cjs +314 -0
  19. package/src/eslint-rules/require-lazy-load-with-retry.cjs +148 -0
  20. package/src/eslint-rules/require-pending-component-di-check.cjs +3 -3
  21. package/src/index.d.ts +8 -0
  22. package/src/index.js +8 -0
  23. package/src/index.js.map +1 -1
  24. package/src/scripts/angular-brand-codemod.d.ts +17 -0
  25. package/src/scripts/angular-brand-codemod.js +44 -8
  26. package/src/scripts/angular-brand-codemod.js.map +1 -1
  27. package/src/scripts/angular-brand-codemod.spec.ts +3 -3
  28. package/src/scripts/angular-brand-codemod.ts +78 -7
  29. package/src/scripts/migrate.d.ts +32 -0
  30. package/src/scripts/migrate.js +67 -0
  31. package/src/scripts/migrate.js.map +1 -0
  32. package/src/scripts/migrate.ts +128 -0
  33. package/src/scripts/migration-workspace.d.ts +2 -0
  34. package/src/scripts/migration-workspace.js +72 -0
  35. package/src/scripts/migration-workspace.js.map +1 -0
  36. package/src/scripts/migration-workspace.ts +93 -0
  37. package/src/scripts/primitives/migrate-primitives.d.ts +27 -0
  38. package/src/scripts/primitives/migrate-primitives.js +354 -0
  39. package/src/scripts/primitives/migrate-primitives.js.map +1 -0
  40. package/src/scripts/primitives/migrate-primitives.spec.ts +279 -0
  41. package/src/scripts/primitives/migrate-primitives.ts +543 -0
  42. package/src/scripts/primitives/migration-diagnostic.d.ts +8 -0
  43. package/src/scripts/primitives/migration-diagnostic.js +2 -0
  44. package/src/scripts/primitives/migration-diagnostic.js.map +1 -0
  45. package/src/scripts/primitives/migration-diagnostic.ts +13 -0
  46. package/src/scripts/routes/migrate-routes.d.ts +34 -0
  47. package/src/scripts/routes/migrate-routes.js +669 -0
  48. package/src/scripts/routes/migrate-routes.js.map +1 -0
  49. package/src/scripts/routes/migrate-routes.spec.ts +264 -0
  50. package/src/scripts/routes/migrate-routes.ts +897 -0
  51. package/src/scripts/routes/migration-diagnostic.d.ts +16 -0
  52. package/src/scripts/routes/migration-diagnostic.js +2 -0
  53. package/src/scripts/routes/migration-diagnostic.js.map +1 -0
  54. package/src/scripts/routes/migration-diagnostic.ts +25 -0
  55. package/src/scripts/services/config.d.ts +2 -0
  56. package/src/scripts/services/config.js +39 -0
  57. package/src/scripts/services/config.js.map +1 -0
  58. package/src/scripts/services/config.ts +60 -0
  59. package/src/scripts/services/migrate-services.d.ts +29 -0
  60. package/src/scripts/services/migrate-services.js +895 -0
  61. package/src/scripts/services/migrate-services.js.map +1 -0
  62. package/src/scripts/services/migrate-services.spec.ts +719 -0
  63. package/src/scripts/services/migrate-services.ts +1282 -0
  64. package/src/scripts/services/migration-diagnostic.d.ts +8 -0
  65. package/src/scripts/services/migration-diagnostic.js +2 -0
  66. package/src/scripts/services/migration-diagnostic.js.map +1 -0
  67. package/src/scripts/services/migration-diagnostic.ts +27 -0
@@ -0,0 +1,543 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { mkdir, writeFile } from 'node:fs/promises';
4
+ import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
5
+ import { promisify } from 'node:util';
6
+ import {
7
+ CallExpression,
8
+ Node,
9
+ Project,
10
+ QuoteKind,
11
+ SourceFile,
12
+ SyntaxKind,
13
+ } from 'ts-morph';
14
+ import type {
15
+ PrimitiveMigrationDiagnostic,
16
+ PrimitiveMigrationDiagnosticCode,
17
+ } from './migration-diagnostic.js';
18
+
19
+ const execFileAsync = promisify(execFile);
20
+
21
+ export type MigratePrimitivesOptions = {
22
+ rootDir?: string;
23
+ tsConfigFilePath?: string;
24
+ files?: readonly string[];
25
+ write?: boolean;
26
+ check?: boolean;
27
+ json?: boolean;
28
+ jsonFilePath?: string;
29
+ failOnManual?: boolean;
30
+ eslint?: boolean;
31
+ log?: (message: string) => void;
32
+ };
33
+
34
+ export type MigratedPrimitiveFile = {
35
+ filePath: string;
36
+ changed: boolean;
37
+ };
38
+
39
+ export type MigratePrimitivesResult = {
40
+ changedFiles: string[];
41
+ files: MigratedPrimitiveFile[];
42
+ diagnostics: PrimitiveMigrationDiagnostic[];
43
+ remainingAngularSignals: number;
44
+ remainingSignalForms: number;
45
+ eslintRan: boolean;
46
+ exitCode: number;
47
+ };
48
+
49
+ export async function runPrimitivesMigration(
50
+ options: MigratePrimitivesOptions = {},
51
+ ): Promise<MigratePrimitivesResult> {
52
+ const rootDir = resolve(options.rootDir ?? process.cwd());
53
+ const tsConfigFilePath = options.tsConfigFilePath
54
+ ? resolve(options.tsConfigFilePath)
55
+ : defaultTsConfig(rootDir);
56
+ const project = new Project({
57
+ ...(existsSync(tsConfigFilePath) ? { tsConfigFilePath } : {}),
58
+ manipulationSettings: { quoteKind: QuoteKind.Single },
59
+ skipAddingFilesFromTsConfig: false,
60
+ });
61
+ project.addSourceFilesAtPaths([
62
+ join(rootDir, '**/*.ts'),
63
+ `!${join(rootDir, '**/node_modules/**')}`,
64
+ `!${join(rootDir, '**/dist/**')}`,
65
+ `!${join(rootDir, '**/.angular/**')}`,
66
+ `!${join(rootDir, '**/*.d.ts')}`,
67
+ ]);
68
+
69
+ const selected = options.files?.length
70
+ ? new Set(options.files.map((file) => resolve(rootDir, file)))
71
+ : undefined;
72
+ const sourceFiles = project.getSourceFiles().filter((file) => {
73
+ const path = resolve(file.getFilePath());
74
+ return isInside(path, rootDir) && (!selected || selected.has(path));
75
+ });
76
+
77
+ const files = new Map<string, MigratedPrimitiveFile>();
78
+ const touched = new Set<SourceFile>();
79
+ const diagnostics: PrimitiveMigrationDiagnostic[] = [];
80
+
81
+ for (const sourceFile of sourceFiles) {
82
+ const changedSignals = migrateSignalsToState(sourceFile);
83
+ const changedResources = migrateSingleEmissionRxResources(
84
+ sourceFile,
85
+ diagnostics,
86
+ );
87
+ const changedWorkflows = annotateImperativeWorkflows(
88
+ sourceFile,
89
+ diagnostics,
90
+ );
91
+ diagnoseSignalForms(sourceFile, diagnostics);
92
+ if (changedSignals || changedResources || changedWorkflows)
93
+ touched.add(sourceFile);
94
+ if (changedSignals || changedResources || changedWorkflows)
95
+ getFileReport(files, sourceFile.getFilePath()).changed = true;
96
+ }
97
+
98
+ for (const file of touched) file.organizeImports();
99
+
100
+ if (options.write) {
101
+ await Promise.all([...touched].map((file) => file.save()));
102
+ }
103
+
104
+ let eslintRan = false;
105
+ if (options.write && options.eslint !== false && touched.size > 0) {
106
+ await runEslint(
107
+ [...touched].map((file) => file.getFilePath()),
108
+ rootDir,
109
+ );
110
+ eslintRan = true;
111
+ }
112
+
113
+ const remainingAngularSignals = countAngularSignalImports(project, rootDir);
114
+ const remainingSignalForms = countSignalFormImports(project, rootDir);
115
+ const result: MigratePrimitivesResult = {
116
+ changedFiles: [...touched].map((file) => file.getFilePath()),
117
+ files: [...files.values()],
118
+ diagnostics,
119
+ remainingAngularSignals,
120
+ remainingSignalForms,
121
+ eslintRan,
122
+ exitCode:
123
+ (options.check &&
124
+ (remainingAngularSignals > 0 || remainingSignalForms > 0)) ||
125
+ (options.failOnManual && diagnostics.length > 0)
126
+ ? 1
127
+ : 0,
128
+ };
129
+
130
+ if (options.jsonFilePath) {
131
+ const path = resolve(options.jsonFilePath);
132
+ await mkdir(dirname(path), { recursive: true });
133
+ await writeFile(path, `${JSON.stringify(result, null, 2)}\n`, 'utf8');
134
+ }
135
+ const log = options.log ?? console.log;
136
+ if (options.json) log(JSON.stringify(result, null, 2));
137
+ else logSummary(result, log, options.write === true);
138
+ return result;
139
+ }
140
+
141
+ function migrateSignalsToState(sourceFile: SourceFile): boolean {
142
+ const angularCore = sourceFile.getImportDeclaration('@angular/core');
143
+ const signalImport = angularCore
144
+ ?.getNamedImports()
145
+ .find((item) => item.getName() === 'signal');
146
+ if (!signalImport) return false;
147
+
148
+ let changed = false;
149
+ const declarationsToAnnotate = new Set<Node>();
150
+ for (const call of [
151
+ ...sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression),
152
+ ]) {
153
+ if (call.wasForgotten()) continue;
154
+ if (call.getExpression().getText() !== 'signal') continue;
155
+ const typeArgument = call.getTypeArguments()[0];
156
+ const initialValue = call.getArguments()[0];
157
+ if (!initialValue) continue;
158
+ const value = typeArgument
159
+ ? `${initialValue.getText()} as ${typeArgument.getText()} satisfies ${typeArgument.getText()}`
160
+ : initialValue.getText();
161
+ const declaration = findImperativeStateDeclaration(call);
162
+ if (declaration) declarationsToAnnotate.add(declaration);
163
+ call.replaceWithText(
164
+ `state(${value}, ({ set, update }) => ({ set, update }))`,
165
+ );
166
+ changed = true;
167
+ }
168
+ for (const declaration of [...declarationsToAnnotate].sort(
169
+ (left, right) => right.getStart() - left.getStart(),
170
+ )) {
171
+ if (declaration.wasForgotten()) continue;
172
+ declaration.replaceWithText(
173
+ `${IMPERATIVE_STATE_COMMENT}\n${declaration.getText()}`,
174
+ );
175
+ }
176
+
177
+ if (!changed) return false;
178
+ signalImport.remove();
179
+ if (
180
+ angularCore &&
181
+ !angularCore.getDefaultImport() &&
182
+ !angularCore.getNamespaceImport() &&
183
+ angularCore.getNamedImports().length === 0
184
+ )
185
+ angularCore.remove();
186
+ ensureCoreImports(sourceFile, ['state']);
187
+ return true;
188
+ }
189
+
190
+ const IMPERATIVE_STATE_COMMENT =
191
+ '// CRAFT_IMPERATIVE_CODE_DETECTED: imperative code detected, prefer a declarative approach.';
192
+
193
+ function findImperativeStateDeclaration(
194
+ call: CallExpression,
195
+ ): Node | undefined {
196
+ const declaration = call.getFirstAncestor(
197
+ (ancestor) =>
198
+ Node.isPropertyDeclaration(ancestor) ||
199
+ Node.isVariableStatement(ancestor),
200
+ );
201
+ if (
202
+ !declaration ||
203
+ declaration.getFullText().includes('CRAFT_IMPERATIVE_CODE_DETECTED')
204
+ )
205
+ return undefined;
206
+ return declaration;
207
+ }
208
+
209
+ const REACTIVE_WORKFLOW_COMMENT =
210
+ '// CRAFT_REACTIVE_WORKFLOW_RECOMMENDED: workflow impératif détecté...';
211
+ const FIRST_VALUE_FROM_REVIEW_COMMENT =
212
+ '// CRAFT_FIRST_VALUE_FROM_REVIEW: firstValueFrom bridges an Observable temporarily; prefer a Promise-native Craft API when possible.';
213
+
214
+ function migrateSingleEmissionRxResources(
215
+ sourceFile: SourceFile,
216
+ diagnostics: PrimitiveMigrationDiagnostic[],
217
+ ): boolean {
218
+ const rxInterop = sourceFile.getImportDeclaration(
219
+ '@angular/core/rxjs-interop',
220
+ );
221
+ if (
222
+ !rxInterop
223
+ ?.getNamedImports()
224
+ .some((item) => item.getName() === 'rxResource')
225
+ )
226
+ return false;
227
+
228
+ let changed = false;
229
+ for (const call of sourceFile.getDescendantsOfKind(
230
+ SyntaxKind.CallExpression,
231
+ )) {
232
+ if (call.wasForgotten() || call.getExpression().getText() !== 'rxResource')
233
+ continue;
234
+ const config = call.getArguments()[0];
235
+ if (!config || !Node.isObjectLiteralExpression(config)) continue;
236
+ const stream = config.getProperty('stream');
237
+ if (!stream || !Node.isPropertyAssignment(stream)) continue;
238
+
239
+ const converted = convertSingleEmissionStream(
240
+ stream.getInitializer()?.getText() ?? '',
241
+ );
242
+ if (!converted) continue;
243
+
244
+ call.getExpression().replaceWithText('query');
245
+ stream.replaceWithText(`loader: ${converted}`);
246
+ changed = true;
247
+ }
248
+
249
+ if (!changed) return false;
250
+ ensureCoreImports(sourceFile, ['query']);
251
+ ensureNamedImport(sourceFile, 'rxjs', 'firstValueFrom');
252
+ removeNamedImportIfUnused(sourceFile, '@angular/core/rxjs-interop', 'rxResource');
253
+ removeNamedImportIfUnused(sourceFile, 'rxjs', 'from');
254
+ removeNamedImportIfUnused(sourceFile, 'rxjs', 'of');
255
+ removeNamedImportIfUnused(sourceFile, 'rxjs/operators', 'switchMap');
256
+
257
+ if (
258
+ !sourceFile
259
+ .getDescendantsOfKind(SyntaxKind.CallExpression)
260
+ .some((call) => call.getExpression().getText() === 'rxResource')
261
+ ) {
262
+ removeDiagnostic(diagnostics, sourceFile, 'RX_RESOURCE_REQUIRES_QUERY');
263
+ }
264
+ return true;
265
+ }
266
+
267
+ function convertSingleEmissionStream(initializer: string): string | undefined {
268
+ const match = initializer.match(
269
+ /^\s*\(([^)]*)\)\s*=>\s*\{([\s\S]*)\}\s*$/,
270
+ );
271
+ if (!match) return undefined;
272
+ let body = match[2];
273
+ const bridge = body.match(
274
+ /return\s+from\(([\s\S]*?)\)\.pipe\(\s*switchMap\(\s*\((\w+)\)\s*=>\s*([\s\S]*?)\s*\)\s*\)\s*;/,
275
+ );
276
+ if (!bridge) return undefined;
277
+
278
+ body = body.replace(/return\s+of\(([\s\S]*?)\)\s*;/g, 'return $1;');
279
+ body = body.replace(
280
+ bridge[0],
281
+ `${FIRST_VALUE_FROM_REVIEW_COMMENT}\nreturn firstValueFrom((await ${bridge[1]}).${stripReceiver(bridge[3], bridge[2])});`,
282
+ );
283
+ return `async (${match[1]}) => {${body}}`;
284
+ }
285
+
286
+ function stripReceiver(expression: string, receiver: string): string {
287
+ const trimmed = expression.trim();
288
+ const prefix = `${receiver}.`;
289
+ return trimmed.startsWith(prefix) ? trimmed.slice(prefix.length) : trimmed;
290
+ }
291
+
292
+ function ensureNamedImport(
293
+ sourceFile: SourceFile,
294
+ moduleSpecifier: string,
295
+ name: string,
296
+ ): void {
297
+ let declaration = sourceFile.getImportDeclaration(moduleSpecifier);
298
+ if (!declaration)
299
+ declaration = sourceFile.addImportDeclaration({ moduleSpecifier });
300
+ if (
301
+ !declaration
302
+ .getNamedImports()
303
+ .some((namedImport) => namedImport.getName() === name)
304
+ )
305
+ declaration.addNamedImport(name);
306
+ }
307
+
308
+ function removeNamedImportIfUnused(
309
+ sourceFile: SourceFile,
310
+ moduleSpecifier: string,
311
+ name: string,
312
+ ): void {
313
+ const declaration = sourceFile.getImportDeclaration(moduleSpecifier);
314
+ const namedImport = declaration
315
+ ?.getNamedImports()
316
+ .find((item) => item.getName() === name);
317
+ if (!namedImport) return;
318
+ const stillUsed = sourceFile
319
+ .getDescendantsOfKind(SyntaxKind.Identifier)
320
+ .some(
321
+ (identifier) =>
322
+ identifier.getText() === name &&
323
+ identifier.getFirstAncestorByKind(SyntaxKind.ImportDeclaration) !==
324
+ declaration,
325
+ );
326
+ if (stillUsed) return;
327
+ namedImport.remove();
328
+ if (
329
+ declaration &&
330
+ !declaration.getDefaultImport() &&
331
+ !declaration.getNamespaceImport() &&
332
+ declaration.getNamedImports().length === 0
333
+ )
334
+ declaration.remove();
335
+ }
336
+
337
+ function removeDiagnostic(
338
+ diagnostics: PrimitiveMigrationDiagnostic[],
339
+ sourceFile: SourceFile,
340
+ code: PrimitiveMigrationDiagnosticCode,
341
+ ): void {
342
+ const index = diagnostics.findIndex(
343
+ (diagnostic) =>
344
+ diagnostic.code === code &&
345
+ diagnostic.filePath === sourceFile.getFilePath(),
346
+ );
347
+ if (index >= 0) diagnostics.splice(index, 1);
348
+ }
349
+
350
+ function annotateImperativeWorkflows(
351
+ sourceFile: SourceFile,
352
+ diagnostics: PrimitiveMigrationDiagnostic[],
353
+ ): boolean {
354
+ let changed = false;
355
+ const functions = [
356
+ ...sourceFile.getDescendantsOfKind(SyntaxKind.FunctionDeclaration),
357
+ ...sourceFile.getDescendantsOfKind(SyntaxKind.MethodDeclaration),
358
+ ];
359
+ for (const functionLike of functions) {
360
+ if (
361
+ functionLike.getFullText().includes('CRAFT_REACTIVE_WORKFLOW_RECOMMENDED')
362
+ )
363
+ continue;
364
+ const calls = functionLike.getDescendantsOfKind(SyntaxKind.CallExpression);
365
+ const hasSubmit = calls.some((call) => {
366
+ const expression = call.getExpression();
367
+ return (
368
+ expression.getText() === 'submit' ||
369
+ (Node.isPropertyAccessExpression(expression) &&
370
+ expression.getName() === 'submit')
371
+ );
372
+ });
373
+ if (!hasSubmit) continue;
374
+ const stateWrites = calls.filter((call) => {
375
+ const expression = call.getExpression();
376
+ return (
377
+ Node.isPropertyAccessExpression(expression) &&
378
+ ['set', 'update'].includes(expression.getName())
379
+ );
380
+ }).length;
381
+ const hasNavigation = calls.some((call) => {
382
+ const expression = call.getExpression();
383
+ return (
384
+ Node.isPropertyAccessExpression(expression) &&
385
+ ['navigate', 'navigateByUrl'].includes(expression.getName())
386
+ );
387
+ });
388
+ if (stateWrites < 2 && !(stateWrites >= 1 && hasNavigation)) continue;
389
+
390
+ functionLike.replaceWithText(
391
+ `${REACTIVE_WORKFLOW_COMMENT}\n${functionLike.getText()}`,
392
+ );
393
+ diagnose(
394
+ diagnostics,
395
+ 'IMPERATIVE_WORKFLOW_REQUIRES_REVIEW',
396
+ sourceFile,
397
+ 'Workflow impératif détecté: préférer insertFormSubmit avec une réaction au statut du formulaire, ou source$ avec on$/effect.',
398
+ );
399
+ changed = true;
400
+ }
401
+ return changed;
402
+ }
403
+
404
+ function diagnoseSignalForms(
405
+ sourceFile: SourceFile,
406
+ diagnostics: PrimitiveMigrationDiagnostic[],
407
+ ): void {
408
+ const formsImport = sourceFile.getImportDeclaration('@angular/forms/signals');
409
+ if (!formsImport) return;
410
+ const names = new Set(formsImport.getNamedImports().map((item) => item.getName()));
411
+ if (names.has('form')) {
412
+ diagnose(
413
+ diagnostics,
414
+ 'SIGNAL_FORM_REQUIRES_INSERT_FORM',
415
+ sourceFile,
416
+ 'Angular signal form `form(...)` doit être migré vers `state(..., insertForm(...))`; les chemins de champs doivent être remappés vers le form tree craft.',
417
+ );
418
+ }
419
+ if (names.has('validateAsync')) {
420
+ for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
421
+ if (!isValidateAsyncRxResourceCall(call)) continue;
422
+ diagnose(
423
+ diagnostics,
424
+ 'ASYNC_VALIDATOR_REQUIRES_QUERY',
425
+ sourceFile,
426
+ '`validateAsync(...)` basé sur `rxResource(...)` doit devenir une `query(...)` locale déclenchée par la valeur du champ, puis `cAsyncValidate(queryRef, ...)`. Dans un craftService, créer la query avec `yield* track(query(...))`.',
427
+ );
428
+ }
429
+ }
430
+ if (sourceFile.getImportDeclaration('@angular/core/rxjs-interop')?.getNamedImports().some((item) => item.getName() === 'rxResource')) {
431
+ diagnose(
432
+ diagnostics,
433
+ 'RX_RESOURCE_REQUIRES_QUERY',
434
+ sourceFile,
435
+ '`rxResource(...)` doit être remplacé par `query(...)` ou `mutation(...)` selon l’intention; dans un validateur async, préférer `query(...) + cAsyncValidate(...)`. Dans un craftService, englober toute primitive dépendante avec `yield* track(...)`.',
436
+ );
437
+ }
438
+ }
439
+
440
+ function isValidateAsyncRxResourceCall(call: CallExpression): boolean {
441
+ if (call.getExpression().getText() !== 'validateAsync') return false;
442
+ return call
443
+ .getDescendantsOfKind(SyntaxKind.CallExpression)
444
+ .some((inner) => inner.getExpression().getText() === 'rxResource');
445
+ }
446
+
447
+ function ensureCoreImports(file: SourceFile, names: string[]): void {
448
+ let declaration = file.getImportDeclaration('@craft-ng/core');
449
+ if (!declaration)
450
+ declaration = file.addImportDeclaration({
451
+ moduleSpecifier: '@craft-ng/core',
452
+ });
453
+ const existing = new Set(
454
+ declaration.getNamedImports().map((item) => item.getName()),
455
+ );
456
+ declaration.addNamedImports(names.filter((name) => !existing.has(name)));
457
+ }
458
+
459
+ function diagnose(
460
+ diagnostics: PrimitiveMigrationDiagnostic[],
461
+ code: PrimitiveMigrationDiagnosticCode,
462
+ sourceFile: SourceFile,
463
+ message: string,
464
+ ): void {
465
+ if (
466
+ diagnostics.some(
467
+ (item) => item.code === code && item.filePath === sourceFile.getFilePath(),
468
+ )
469
+ )
470
+ return;
471
+ diagnostics.push({
472
+ code,
473
+ filePath: sourceFile.getFilePath(),
474
+ message,
475
+ manual: true,
476
+ });
477
+ }
478
+
479
+ function countAngularSignalImports(project: Project, rootDir: string): number {
480
+ return project
481
+ .getSourceFiles()
482
+ .filter((file) => isInside(resolve(file.getFilePath()), rootDir))
483
+ .filter((file) =>
484
+ file
485
+ .getImportDeclaration('@angular/core')
486
+ ?.getNamedImports()
487
+ .some((item) => item.getName() === 'signal'),
488
+ ).length;
489
+ }
490
+
491
+ function countSignalFormImports(project: Project, rootDir: string): number {
492
+ return project
493
+ .getSourceFiles()
494
+ .filter((file) => isInside(resolve(file.getFilePath()), rootDir))
495
+ .filter((file) => Boolean(file.getImportDeclaration('@angular/forms/signals')))
496
+ .length;
497
+ }
498
+
499
+ function getFileReport(
500
+ map: Map<string, MigratedPrimitiveFile>,
501
+ filePath: string,
502
+ ): MigratedPrimitiveFile {
503
+ let report = map.get(filePath);
504
+ if (!report) {
505
+ report = { filePath, changed: false };
506
+ map.set(filePath, report);
507
+ }
508
+ return report;
509
+ }
510
+
511
+ function isInside(filePath: string, rootDir: string): boolean {
512
+ const path = relative(rootDir, filePath);
513
+ return path === '' || (!path.startsWith('..') && !isAbsolute(path));
514
+ }
515
+
516
+ function defaultTsConfig(rootDir: string): string {
517
+ for (const name of ['tsconfig.app.json', 'tsconfig.json']) {
518
+ const candidate = join(rootDir, name);
519
+ if (existsSync(candidate)) return candidate;
520
+ }
521
+ return join(rootDir, 'tsconfig.json');
522
+ }
523
+
524
+ async function runEslint(files: string[], cwd: string): Promise<void> {
525
+ const local = join(cwd, 'node_modules', '.bin', 'eslint');
526
+ await execFileAsync(
527
+ existsSync(local) ? local : 'eslint',
528
+ ['--fix', ...files],
529
+ { cwd },
530
+ );
531
+ }
532
+
533
+ function logSummary(
534
+ result: MigratePrimitivesResult,
535
+ log: (message: string) => void,
536
+ wrote: boolean,
537
+ ): void {
538
+ log(
539
+ `${wrote ? 'Migrated' : 'Would migrate'} ${result.changedFiles.length} file(s); ${result.diagnostics.length} manual diagnostic(s).`,
540
+ );
541
+ for (const diagnostic of result.diagnostics)
542
+ log(`[${diagnostic.code}] ${diagnostic.filePath}: ${diagnostic.message}`);
543
+ }
@@ -0,0 +1,8 @@
1
+ export type PrimitiveMigrationDiagnosticCode = 'SIGNAL_FORM_REQUIRES_INSERT_FORM' | 'ASYNC_VALIDATOR_REQUIRES_QUERY' | 'RX_RESOURCE_REQUIRES_QUERY' | 'IMPERATIVE_WORKFLOW_REQUIRES_REVIEW';
2
+ export type PrimitiveMigrationDiagnostic = {
3
+ code: PrimitiveMigrationDiagnosticCode;
4
+ filePath: string;
5
+ symbol?: string;
6
+ message: string;
7
+ manual: true;
8
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=migration-diagnostic.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migration-diagnostic.js","sourceRoot":"","sources":["../../../../../../libs/dev-tools/src/scripts/primitives/migration-diagnostic.ts"],"names":[],"mappings":""}
@@ -0,0 +1,13 @@
1
+ export type PrimitiveMigrationDiagnosticCode =
2
+ | 'SIGNAL_FORM_REQUIRES_INSERT_FORM'
3
+ | 'ASYNC_VALIDATOR_REQUIRES_QUERY'
4
+ | 'RX_RESOURCE_REQUIRES_QUERY'
5
+ | 'IMPERATIVE_WORKFLOW_REQUIRES_REVIEW';
6
+
7
+ export type PrimitiveMigrationDiagnostic = {
8
+ code: PrimitiveMigrationDiagnosticCode;
9
+ filePath: string;
10
+ symbol?: string;
11
+ message: string;
12
+ manual: true;
13
+ };
@@ -0,0 +1,34 @@
1
+ import { SourceFile } from 'ts-morph';
2
+ import { RouteMigrationDiagnostic } from './migration-diagnostic.js';
3
+ export type MigrateRoutesOptions = {
4
+ rootDir?: string;
5
+ tsConfigFilePath?: string;
6
+ files?: readonly string[];
7
+ write?: boolean;
8
+ check?: boolean;
9
+ collectionName?: string;
10
+ parentMount?: string;
11
+ parentNames?: readonly string[];
12
+ jsonFilePath?: string;
13
+ failOnManual?: boolean;
14
+ log?: (message: string) => void;
15
+ };
16
+ export type MigratedRoutesFile = {
17
+ filePath: string;
18
+ changed: boolean;
19
+ collections: string[];
20
+ };
21
+ export type MigrateRoutesResult = {
22
+ changedFiles: string[];
23
+ files: MigratedRoutesFile[];
24
+ diagnostics: RouteMigrationDiagnostic[];
25
+ remainingLegacyCollections: number;
26
+ exitCode: number;
27
+ };
28
+ export declare function runRoutesMigration(options?: MigrateRoutesOptions): Promise<MigrateRoutesResult>;
29
+ export declare function migrateApplicationConfigSourceFile(sourceFile: SourceFile): boolean;
30
+ export declare function migrateBootstrapSourceFile(sourceFile: SourceFile): boolean;
31
+ export declare function migrateRoutesSourceFile(sourceFile: SourceFile, options?: Pick<MigrateRoutesOptions, 'collectionName' | 'parentMount' | 'parentNames'> & {
32
+ diagnostics?: RouteMigrationDiagnostic[];
33
+ generatedComponentFiles?: Set<SourceFile>;
34
+ }): string[];