@ethlete/core 5.0.0-next.6 → 5.0.0-next.61

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 (35) hide show
  1. package/CHANGELOG.md +714 -0
  2. package/README.md +28 -4
  3. package/fesm2022/ethlete-core.mjs +8061 -3502
  4. package/fesm2022/ethlete-core.mjs.map +1 -1
  5. package/generators/devtools-about/generator.js +122 -0
  6. package/generators/devtools-about/schema.json +25 -0
  7. package/generators/generators.json +26 -5
  8. package/generators/migrate-provider-shape/migration-scope.js +35 -0
  9. package/generators/migrate-provider-shape/migration.js +50 -0
  10. package/generators/migrate-provider-shape/provider-shape.js +284 -0
  11. package/generators/migrate-provider-shape/schema.json +26 -0
  12. package/generators/migrate-surface-interaction-swatch/interaction-swatch.js +63 -0
  13. package/generators/migrate-surface-interaction-swatch/migration.js +36 -0
  14. package/generators/migrate-surface-interaction-swatch/schema.json +26 -0
  15. package/generators/migrate-to-v5/apply-replacements.js +14 -0
  16. package/generators/migrate-to-v5/color-naming.js +124 -0
  17. package/generators/migrate-to-v5/create-provider.js +67 -55
  18. package/generators/migrate-to-v5/migration-files.js +13 -0
  19. package/generators/migrate-to-v5/migration.js +33 -19
  20. package/generators/migrate-to-v5/removed-exports.js +73 -0
  21. package/generators/migrate-to-v5/router-state-service.js +123 -330
  22. package/generators/migrate-to-v5/schema.json +22 -0
  23. package/generators/migrate-to-v5/service-classes.js +108 -0
  24. package/generators/migrate-to-v5/viewport-service.js +224 -491
  25. package/generators/tailwind-4-color-theme/generator.js +594 -0
  26. package/generators/tailwind-4-color-theme/schema.json +44 -0
  27. package/generators/tailwind-4-surface-theme/generator.js +599 -0
  28. package/generators/tailwind-4-surface-theme/schema.json +48 -0
  29. package/migrations/seo-directive-removed.md +80 -0
  30. package/migrations/surface-theme-regenerate.md +20 -0
  31. package/migrations.json +43 -0
  32. package/package.json +25 -14
  33. package/types/ethlete-core.d.ts +3337 -1775
  34. package/generators/tailwind-4-theme/generator.js +0 -490
  35. package/generators/tailwind-4-theme/schema.json +0 -32
@@ -1,6 +1,8 @@
1
+ import { logger } from '@nx/devkit';
1
2
  import * as ts from 'typescript';
3
+ import { collectFiles } from './migration-files.js';
4
+ import { applyReplacementsInClass, collectClasses, findServiceClasses, getServiceImport, isServiceParameter, isServiceProperty, isServiceStillUsed, removeServiceImport, } from './service-classes.js';
2
5
  function getPropertyMaps() {
3
- // Property map for signals (getters for latest value)
4
6
  const signalPropertyMap = {
5
7
  route: 'injectRoute',
6
8
  state: 'injectRouterState',
@@ -11,7 +13,6 @@ function getPropertyMaps() {
11
13
  fragment: 'injectFragment',
12
14
  latestEvent: 'injectRouterEvent',
13
15
  };
14
- // Property map for observables
15
16
  const observablePropertyMap = {
16
17
  route$: 'injectRoute',
17
18
  state$: 'injectRouterState',
@@ -25,17 +26,15 @@ function getPropertyMaps() {
25
26
  };
26
27
  return { signalPropertyMap, observablePropertyMap };
27
28
  }
28
- function findRouterStateServiceVariables(sourceFile) {
29
+ function findRouterStateServiceVariables(sourceFile, serviceName) {
29
30
  const variables = [];
30
31
  function visit(node) {
31
- // Check for inject(RouterStateService) pattern
32
32
  if (ts.isCallExpression(node) &&
33
33
  ts.isIdentifier(node.expression) &&
34
34
  node.expression.text === 'inject' &&
35
35
  node.arguments.length > 0) {
36
36
  const arg = node.arguments[0];
37
- if (ts.isIdentifier(arg) && arg.text === 'RouterStateService') {
38
- // Find the variable name
37
+ if (ts.isIdentifier(arg) && arg.text === serviceName) {
39
38
  let parent = node.parent;
40
39
  while (parent) {
41
40
  if (ts.isPropertyDeclaration(parent) && ts.isIdentifier(parent.name)) {
@@ -50,11 +49,10 @@ function findRouterStateServiceVariables(sourceFile) {
50
49
  }
51
50
  }
52
51
  }
53
- // Check for constructor parameter injection
54
52
  if (ts.isParameter(node)) {
55
53
  const typeNode = node.type;
56
54
  if (typeNode && ts.isTypeReferenceNode(typeNode) && ts.isIdentifier(typeNode.typeName)) {
57
- if (typeNode.typeName.text === 'RouterStateService' && node.name && ts.isIdentifier(node.name)) {
55
+ if (typeNode.typeName.text === serviceName && node.name && ts.isIdentifier(node.name)) {
58
56
  variables.push(node.name.text);
59
57
  }
60
58
  }
@@ -64,29 +62,6 @@ function findRouterStateServiceVariables(sourceFile) {
64
62
  visit(sourceFile);
65
63
  return variables;
66
64
  }
67
- function findClassForRouterStateService(sourceFile, routerStateServiceVar) {
68
- let classNode = null;
69
- function visit(node) {
70
- if (ts.isClassDeclaration(node)) {
71
- const hasRouterStateService = node.members.some((member) => {
72
- if (ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name)) {
73
- return member.name.text === routerStateServiceVar;
74
- }
75
- if (ts.isConstructorDeclaration(member)) {
76
- return member.parameters.some((param) => ts.isIdentifier(param.name) && param.name.text === routerStateServiceVar);
77
- }
78
- return false;
79
- });
80
- if (hasRouterStateService) {
81
- classNode = node;
82
- return;
83
- }
84
- }
85
- ts.forEachChild(node, visit);
86
- }
87
- visit(sourceFile);
88
- return classNode;
89
- }
90
65
  function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
91
66
  const context = {
92
67
  routerStateServiceVar,
@@ -94,15 +69,13 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
94
69
  membersToAdd: [],
95
70
  replacements: new Map(),
96
71
  importsNeeded: new Set(),
97
- constructorCalls: [], // Initialize
72
+ constructorCalls: [],
98
73
  };
99
- // Collect existing member names and detect property initializers
100
74
  const propertyInitializers = new Map();
101
75
  classNode.members.forEach((member) => {
102
76
  if (ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name)) {
103
77
  const memberName = member.name.text;
104
78
  context.existingMembers.add(memberName);
105
- // Check if this property has an initializer that uses RouterStateService
106
79
  if (member.initializer) {
107
80
  const initializerText = member.initializer.getText(sourceFile);
108
81
  if (initializerText.includes(routerStateServiceVar)) {
@@ -115,7 +88,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
115
88
  }
116
89
  });
117
90
  const { signalPropertyMap, observablePropertyMap } = getPropertyMaps();
118
- // Method map
119
91
  const methodMap = {
120
92
  selectQueryParam: { injectFn: 'injectQueryParam', type: 'observable', requiresArgs: true },
121
93
  selectPathParam: { injectFn: 'injectPathParam', type: 'observable', requiresArgs: true },
@@ -127,13 +99,10 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
127
99
  needsInjectionContext: true,
128
100
  },
129
101
  };
130
- // Track usages
131
102
  const usagesWrappedInToSignal = new Map();
132
103
  const usagesOutsideToSignal = new Map();
133
104
  const usagesInPropertyInitializers = new Map();
134
- // First pass: detect toSignal usages, property initializers, and count all usages
135
105
  function detectUsages(node, insideToSignal = false, currentProperty) {
136
- // Handle toSignal wrapper
137
106
  if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'toSignal') {
138
107
  const arg = node.arguments[0];
139
108
  if (arg) {
@@ -141,7 +110,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
141
110
  }
142
111
  return;
143
112
  }
144
- // Track RouterStateService property access
145
113
  if (ts.isPropertyAccessExpression(node)) {
146
114
  const propertyName = node.name.text;
147
115
  const isRouterStateAccess = (ts.isIdentifier(node.expression) && node.expression.text === routerStateServiceVar) ||
@@ -158,7 +126,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
158
126
  usage: fullAccess,
159
127
  propertyDecl,
160
128
  });
161
- // Also track if it's wrapped in toSignal
162
129
  if (insideToSignal) {
163
130
  usagesWrappedInToSignal.set(fullAccess, (usagesWrappedInToSignal.get(fullAccess) || 0) + 1);
164
131
  }
@@ -172,7 +139,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
172
139
  }
173
140
  }
174
141
  }
175
- // Track method calls
176
142
  if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
177
143
  const methodName = node.expression.name.text;
178
144
  if (methodName !== 'pipe' && methodName !== 'subscribe') {
@@ -190,7 +156,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
190
156
  usage: fullCall,
191
157
  propertyDecl,
192
158
  });
193
- // Also track if it's wrapped in toSignal
194
159
  if (insideToSignal) {
195
160
  usagesWrappedInToSignal.set(fullCall, (usagesWrappedInToSignal.get(fullCall) || 0) + 1);
196
161
  }
@@ -207,7 +172,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
207
172
  }
208
173
  ts.forEachChild(node, (child) => detectUsages(child, insideToSignal, currentProperty));
209
174
  }
210
- // Detect usages in property initializers
211
175
  classNode.members.forEach((member) => {
212
176
  if (ts.isPropertyDeclaration(member) && member.initializer && ts.isIdentifier(member.name)) {
213
177
  const propertyName = member.name.text;
@@ -218,17 +182,14 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
218
182
  else if (ts.isPropertyDeclaration(member) &&
219
183
  ts.isIdentifier(member.name) &&
220
184
  member.name.text === routerStateServiceVar) {
221
- // Skip the RouterStateService property itself
222
185
  return;
223
186
  }
224
187
  else {
225
188
  detectUsages(member);
226
189
  }
227
190
  });
228
- // Group property initializer usages by usage pattern
229
191
  const propertyInitializerUsages = new Map();
230
192
  for (const [usage, info] of usagesInPropertyInitializers) {
231
- // Determine if this is a property access or method call
232
193
  const isMethodCall = usage.includes('(');
233
194
  let usageKey;
234
195
  let injectFn;
@@ -236,8 +197,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
236
197
  let args;
237
198
  let genericType = '';
238
199
  if (isMethodCall) {
239
- // Extract method name, generic type, and args
240
- // Remove whitespace and newlines for matching
241
200
  const normalizedUsage = usage.replace(/\s+/g, ' ');
242
201
  const match = normalizedUsage.match(/\.(\w+)(?:<([^>]+)>)?\(([^)]*)\)/);
243
202
  if (!match)
@@ -253,7 +212,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
253
212
  type = methodInfo.type;
254
213
  }
255
214
  else {
256
- // Extract property name
257
215
  const match = usage.match(/\.(\w+\$?)$/);
258
216
  if (!match)
259
217
  continue;
@@ -277,10 +235,8 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
277
235
  wrappedInToSignal,
278
236
  });
279
237
  }
280
- // Process property initializer usages
281
238
  for (const [usageKey, usageInfo] of propertyInitializerUsages) {
282
239
  const { injectFn, args, type, genericType, properties } = usageInfo;
283
- // Check if this same usage is used outside property initializers
284
240
  const isUsedOutsideInitializers = usagesOutsideToSignal.has(usageKey) ||
285
241
  Array.from(usagesOutsideToSignal.keys()).some((key) => {
286
242
  if (args) {
@@ -289,7 +245,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
289
245
  return key.includes(usageKey);
290
246
  });
291
247
  if (isUsedOutsideInitializers && type === 'observable') {
292
- // Create a shared member
293
248
  const baseName = usageKey.endsWith('$') ? usageKey : `${usageKey}$`;
294
249
  const memberName = findAvailableMemberName(baseName, context.existingMembers);
295
250
  const memberInfo = {
@@ -302,10 +257,8 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
302
257
  };
303
258
  context.membersToAdd.push(memberInfo);
304
259
  context.existingMembers.add(memberName);
305
- // For each property, replace the usage with the member reference
306
260
  for (const { propertyDecl } of properties) {
307
261
  const initializerText = propertyDecl.initializer.getText(sourceFile);
308
- // Create pattern to match only the RouterStateService access part
309
262
  const isMethodCall = usageKey.includes('(');
310
263
  if (isMethodCall) {
311
264
  const methodNameOnly = usageKey.split(/[<(]/)[0];
@@ -314,7 +267,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
314
267
  const pattern = new RegExp(`(this\\.)?${escapedVar}\\s*\\.\\s*${escapedMethod}(?:<[^>]+>)?\\s*\\([^)]*\\)`, 'gs');
315
268
  const match = initializerText.match(pattern);
316
269
  if (match && match[0]) {
317
- // Check if the original has 'this.' prefix
318
270
  const hasThisPrefix = match[0].startsWith('this.');
319
271
  const replacement = hasThisPrefix ? `this.${memberName}` : memberName;
320
272
  context.replacements.set(match[0], replacement);
@@ -324,7 +276,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
324
276
  const pattern = new RegExp(`(this\\.)?${escapeRegExp(routerStateServiceVar)}\\.${escapeRegExp(usageKey)}`, 'g');
325
277
  const matches = initializerText.match(pattern);
326
278
  if (matches && matches[0]) {
327
- // Check if the original has 'this.' prefix
328
279
  const hasThisPrefix = matches[0].startsWith('this.');
329
280
  const replacement = hasThisPrefix ? `this.${memberName}` : memberName;
330
281
  context.replacements.set(matches[0], replacement);
@@ -334,19 +285,15 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
334
285
  context.importsNeeded.add(injectFn);
335
286
  }
336
287
  else {
337
- // Replace each property initializer directly
338
288
  for (const { propertyDecl, wrappedInToSignal } of properties) {
339
289
  const initializerText = propertyDecl.initializer.getText(sourceFile);
340
290
  const injectCall = args ? `${injectFn}${genericType || ''}(${args})` : `${injectFn}()`;
341
291
  const propertyName = ts.isIdentifier(propertyDecl.name) ? propertyDecl.name.text : '';
342
292
  const isObservableProperty = propertyName.endsWith('$');
343
- const isPropertyUsedElsewhere = checkIfPropertyIsUsedElsewhere(sourceFile, classNode, propertyName, propertyDecl);
293
+ const isPropertyUsedElsewhere = checkIfPropertyIsUsedElsewhere(classNode, propertyName, propertyDecl);
344
294
  const hasChainedCalls = initializerText.includes('.pipe(') ||
345
295
  initializerText.includes('.subscribe(') ||
346
296
  initializerText.match(/\)\s*\./);
347
- // Determine if we need toObservable wrapper:
348
- // 1. If NOT wrapped in toSignal and type is observable and (property ends with $ or used elsewhere)
349
- // 2. If wrapped in toSignal but has chained calls (pipe/subscribe), we need toObservable
350
297
  const needsToObservable = (type === 'observable' && !wrappedInToSignal && (isObservableProperty || isPropertyUsedElsewhere)) ||
351
298
  (wrappedInToSignal && hasChainedCalls);
352
299
  const wrappedInjectCall = needsToObservable ? `toObservable(${injectCall})` : injectCall;
@@ -377,7 +324,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
377
324
  }
378
325
  }
379
326
  }
380
- // Handle direct usages (not in property initializers)
381
327
  for (const [usage] of usagesWrappedInToSignal) {
382
328
  if (!usagesInPropertyInitializers.has(usage)) {
383
329
  const isMethodCall = usage.includes('(');
@@ -406,9 +352,7 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
406
352
  context.importsNeeded.add(injectFn);
407
353
  }
408
354
  }
409
- // Handle direct usages in method bodies (outside toSignal, not in property initializers)
410
355
  for (const [usage] of usagesOutsideToSignal) {
411
- // Skip if already handled in property initializers
412
356
  if (usagesInPropertyInitializers.has(usage))
413
357
  continue;
414
358
  const isMethodCall = usage.includes('(');
@@ -432,13 +376,8 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
432
376
  type = methodInfo.type;
433
377
  needsInjectionContext = methodInfo.needsInjectionContext || false;
434
378
  if (needsInjectionContext) {
435
- // Add to constructor calls
436
379
  const injectCall = args ? `${injectFn}${genericType}(${args})` : `${injectFn}()`;
437
380
  context.constructorCalls.push(injectCall);
438
- // Create pattern to match the full statement including semicolon and potential whitespace
439
- const methodNameOnly = methodName;
440
- const pattern = new RegExp(`(this\\.)?${escapeRegExp(routerStateServiceVar)}\\s*\\.\\s*${escapeRegExp(methodNameOnly)}(?:<[^>]+>)?\\s*\\([^)]*\\)\\s*;`, 'gs');
441
- // Replace with empty string (removes the entire statement)
442
381
  context.replacements.set(usage + ';', '');
443
382
  context.importsNeeded.add(injectFn);
444
383
  continue; // Don't create a member for this
@@ -452,7 +391,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
452
391
  injectFn = signalPropertyMap[propertyName] || observablePropertyMap[propertyName];
453
392
  type = propertyName.endsWith('$') ? 'observable' : 'signal';
454
393
  }
455
- // For properties used in method bodies, create a shared member
456
394
  const baseName = usage.split('.').pop();
457
395
  const memberName = findAvailableMemberName(baseName, context.existingMembers);
458
396
  const memberInfo = {
@@ -465,10 +403,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
465
403
  };
466
404
  context.membersToAdd.push(memberInfo);
467
405
  context.existingMembers.add(memberName);
468
- // Replace usage with member reference
469
- // For signals (non-observable), we need to call them: this.route()
470
- // For observables, we just reference them: this.route$
471
- // Check if the original usage has 'this.' prefix
472
406
  const hasThisPrefix = usage.includes('this.');
473
407
  const baseReplacement = type === 'signal' ? `${memberName}()` : `${memberName}`;
474
408
  const replacement = hasThisPrefix ? `this.${baseReplacement}` : baseReplacement;
@@ -480,20 +414,17 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
480
414
  function addOrUpdateConstructor(sourceFile, content, classNode, constructorCalls) {
481
415
  if (constructorCalls.length === 0)
482
416
  return content;
483
- // Find existing constructor
484
417
  const existingConstructor = classNode.members.find((member) => ts.isConstructorDeclaration(member));
485
418
  const callStatements = constructorCalls.map((call) => ` ${call};`).join('\n');
486
419
  if (existingConstructor) {
487
- // Add calls to existing constructor
488
420
  const constructorBody = existingConstructor.body;
489
421
  if (!constructorBody)
490
422
  return content;
491
- const insertPos = constructorBody.getStart(sourceFile) + 1; // After opening brace
423
+ const insertPos = constructorBody.getStart(sourceFile) + 1;
492
424
  const indent = '\n ';
493
425
  return content.slice(0, insertPos) + indent + callStatements + content.slice(insertPos);
494
426
  }
495
427
  else {
496
- // Create new constructor
497
428
  const firstMethod = classNode.members.find((member) => ts.isMethodDeclaration(member) || ts.isGetAccessor(member) || ts.isSetAccessor(member));
498
429
  const constructorText = `\n\n constructor() {\n${callStatements}\n }`;
499
430
  if (firstMethod) {
@@ -501,18 +432,11 @@ function addOrUpdateConstructor(sourceFile, content, classNode, constructorCalls
501
432
  return content.slice(0, insertPos) + constructorText + '\n\n ' + content.slice(insertPos);
502
433
  }
503
434
  else {
504
- // Insert at end of class, before closing brace
505
435
  const classEnd = classNode.getEnd() - 1;
506
436
  return content.slice(0, classEnd) + constructorText + '\n' + content.slice(classEnd);
507
437
  }
508
438
  }
509
439
  }
510
- function generateNameFromArgs(usageKey, args, type) {
511
- const baseName = usageKey.split('(')[0].replace(/^\w/, (c) => c.toUpperCase());
512
- const cleanArgs = args.replace(/['"]/g, '').replace(/[^\w]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '');
513
- const name = `${baseName.charAt(0).toLowerCase()}${baseName.slice(1)}${cleanArgs.charAt(0).toUpperCase()}${cleanArgs.slice(1)}`;
514
- return type === 'observable' ? `${name}$` : name;
515
- }
516
440
  function findAvailableMemberName(baseName, existingMembers) {
517
441
  let name = baseName;
518
442
  let counter = 1;
@@ -522,23 +446,7 @@ function findAvailableMemberName(baseName, existingMembers) {
522
446
  }
523
447
  return name;
524
448
  }
525
- function createReplacementsForMember(sourceFile, classNode, routerStateServiceVar, usageKey, memberName, type, context, args) {
526
- // Find all occurrences outside property initializers
527
- classNode.members.forEach((member) => {
528
- if (ts.isMethodDeclaration(member) || ts.isGetAccessor(member) || ts.isSetAccessor(member)) {
529
- // Search in method bodies
530
- const searchPattern = args
531
- ? `${routerStateServiceVar}.${usageKey.split('(')[0]}(${args})`
532
- : `${routerStateServiceVar}.${usageKey}`;
533
- const memberText = member.getText(sourceFile);
534
- if (memberText.includes(searchPattern)) {
535
- context.replacements.set(searchPattern, `this.${memberName}`);
536
- context.replacements.set(`this.${searchPattern}`, `this.${memberName}`);
537
- }
538
- }
539
- });
540
- }
541
- function checkIfPropertyIsUsedElsewhere(sourceFile, classNode, propertyName, propertyDecl) {
449
+ function checkIfPropertyIsUsedElsewhere(classNode, propertyName, propertyDecl) {
542
450
  let usedElsewhere = false;
543
451
  function visit(node) {
544
452
  if (node === propertyDecl)
@@ -560,15 +468,13 @@ function checkIfPropertyIsUsedElsewhere(sourceFile, classNode, propertyName, pro
560
468
  function escapeRegExp(string) {
561
469
  return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
562
470
  }
563
- function addMembersToClass(sourceFile, content, classNode, members, routerStateServiceVar) {
564
- // Find the RouterStateService property to insert after
471
+ function addMembersToClass(content, classNode, members, routerStateServiceVar) {
565
472
  const routerStateProperty = classNode.members.find((member) => ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name) && member.name.text === routerStateServiceVar);
566
473
  if (!routerStateProperty) {
567
- console.warn('Could not find RouterStateService property to insert new members after');
474
+ logger.warn('Could not find RouterStateService property to insert new members after');
568
475
  return content;
569
476
  }
570
477
  const insertPosition = routerStateProperty.getEnd();
571
- // Generate member declarations
572
478
  const memberDeclarations = members.map((member) => {
573
479
  const injectCall = member.args ? `${member.injectFn}(${member.args})` : `${member.injectFn}()`;
574
480
  const value = member.type === 'observable' ? `toObservable(${injectCall})` : injectCall;
@@ -576,11 +482,10 @@ function addMembersToClass(sourceFile, content, classNode, members, routerStateS
576
482
  });
577
483
  return content.slice(0, insertPosition) + memberDeclarations.join('') + content.slice(insertPosition);
578
484
  }
579
- function handleInlineInjectPatterns(sourceFile, content) {
485
+ function handleInlineInjectPatterns(sourceFile, content, serviceName) {
580
486
  const importsNeeded = new Set();
581
- let updatedContent = content;
487
+ const edits = [];
582
488
  const { signalPropertyMap, observablePropertyMap } = getPropertyMaps();
583
- // Method map
584
489
  const methodMap = {
585
490
  selectQueryParam: { injectFn: 'injectQueryParam', type: 'observable', requiresArgs: true },
586
491
  selectPathParam: { injectFn: 'injectPathParam', type: 'observable', requiresArgs: true },
@@ -592,7 +497,6 @@ function handleInlineInjectPatterns(sourceFile, content) {
592
497
  needsInjectionContext: true,
593
498
  },
594
499
  };
595
- // Track which nodes are already inside toSignal to avoid double processing
596
500
  const processedNodes = new Set();
597
501
  function isInsideToSignal(node) {
598
502
  let current = node.parent;
@@ -607,7 +511,6 @@ function handleInlineInjectPatterns(sourceFile, content) {
607
511
  return false;
608
512
  }
609
513
  function visitNode(node) {
610
- // Look for: toSignal(inject(RouterStateService).property)
611
514
  if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'toSignal') {
612
515
  const arg = node.arguments[0];
613
516
  if (arg && ts.isPropertyAccessExpression(arg)) {
@@ -617,20 +520,17 @@ function handleInlineInjectPatterns(sourceFile, content) {
617
520
  arg.expression.expression.text === 'inject' &&
618
521
  arg.expression.arguments.length > 0) {
619
522
  const injectArg = arg.expression.arguments[0];
620
- if (ts.isIdentifier(injectArg) && injectArg.text === 'RouterStateService') {
523
+ if (ts.isIdentifier(injectArg) && injectArg.text === serviceName) {
621
524
  const injectFn = signalPropertyMap[propertyName] || observablePropertyMap[propertyName];
622
525
  if (injectFn) {
623
- const oldText = node.getText(sourceFile);
624
526
  const newText = `${injectFn}()`;
625
- updatedContent = updatedContent.replace(oldText, newText);
527
+ edits.push({ start: node.getStart(sourceFile), end: node.getEnd(), text: newText });
626
528
  importsNeeded.add(injectFn);
627
- // Mark this property access as processed
628
529
  processedNodes.add(arg);
629
530
  }
630
531
  }
631
532
  }
632
533
  }
633
- // NEW: Look for: toSignal(inject(RouterStateService).methodCall(...))
634
534
  if (arg && ts.isCallExpression(arg) && ts.isPropertyAccessExpression(arg.expression)) {
635
535
  const methodName = arg.expression.name.text;
636
536
  const methodInfo = methodMap[methodName];
@@ -640,28 +540,21 @@ function handleInlineInjectPatterns(sourceFile, content) {
640
540
  arg.expression.expression.expression.text === 'inject' &&
641
541
  arg.expression.expression.arguments.length > 0) {
642
542
  const injectArg = arg.expression.expression.arguments[0];
643
- if (ts.isIdentifier(injectArg) && injectArg.text === 'RouterStateService') {
644
- const oldText = node.getText(sourceFile);
645
- // Extract generic type and args
543
+ if (ts.isIdentifier(injectArg) && injectArg.text === serviceName) {
646
544
  const innerCallText = arg.getText(sourceFile);
647
545
  const genericMatch = innerCallText.match(new RegExp(`${methodName}<([^>]+)>`));
648
546
  const genericType = genericMatch ? `<${genericMatch[1]}>` : '';
649
- // Get the arguments from the call
650
547
  const args = arg.arguments.map((a) => a.getText(sourceFile)).join(', ');
651
548
  const injectCall = `${methodInfo.injectFn}${genericType}(${args})`;
652
- // Since it's wrapped in toSignal and returns a signal, no need for toObservable
653
549
  const newText = injectCall;
654
- updatedContent = updatedContent.replace(oldText, newText);
550
+ edits.push({ start: node.getStart(sourceFile), end: node.getEnd(), text: newText });
655
551
  importsNeeded.add(methodInfo.injectFn);
656
- // Mark this call as processed
657
552
  processedNodes.add(arg);
658
553
  }
659
554
  }
660
555
  }
661
556
  }
662
- // Look for: inject(RouterStateService).selectQueryParam() or other method calls (NOT inside toSignal)
663
557
  if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
664
- // Skip if already processed by toSignal handler
665
558
  if (processedNodes.has(node)) {
666
559
  ts.forEachChild(node, visitNode);
667
560
  return;
@@ -674,21 +567,18 @@ function handleInlineInjectPatterns(sourceFile, content) {
674
567
  node.expression.expression.expression.text === 'inject' &&
675
568
  node.expression.expression.arguments.length > 0) {
676
569
  const injectArg = node.expression.expression.arguments[0];
677
- if (ts.isIdentifier(injectArg) && injectArg.text === 'RouterStateService') {
678
- // Check if inside toSignal
570
+ if (ts.isIdentifier(injectArg) && injectArg.text === serviceName) {
679
571
  if (isInsideToSignal(node)) {
680
572
  ts.forEachChild(node, visitNode);
681
573
  return;
682
574
  }
683
575
  const oldText = node.getText(sourceFile);
684
- // Extract generic type and args
685
576
  const genericMatch = oldText.match(new RegExp(`${methodName}<([^>]+)>`));
686
577
  const genericType = genericMatch ? `<${genericMatch[1]}>` : '';
687
- // Get the arguments from the call
688
578
  const args = node.arguments.map((arg) => arg.getText(sourceFile)).join(', ');
689
579
  const injectCall = `${methodInfo.injectFn}${genericType}(${args})`;
690
580
  const newText = methodInfo.type === 'observable' ? `toObservable(${injectCall})` : injectCall;
691
- updatedContent = updatedContent.replace(oldText, newText);
581
+ edits.push({ start: node.getStart(sourceFile), end: node.getEnd(), text: newText });
692
582
  importsNeeded.add(methodInfo.injectFn);
693
583
  if (methodInfo.type === 'observable') {
694
584
  importsNeeded.add('toObservable');
@@ -696,10 +586,7 @@ function handleInlineInjectPatterns(sourceFile, content) {
696
586
  }
697
587
  }
698
588
  }
699
- // Look for: inject(RouterStateService).property (not wrapped in toSignal)
700
- // This handles: inject(RouterStateService).pathParams$.pipe(...)
701
589
  if (ts.isPropertyAccessExpression(node)) {
702
- // Skip if already processed by toSignal handler
703
590
  if (processedNodes.has(node)) {
704
591
  ts.forEachChild(node, visitNode);
705
592
  return;
@@ -710,21 +597,18 @@ function handleInlineInjectPatterns(sourceFile, content) {
710
597
  node.expression.expression.text === 'inject' &&
711
598
  node.expression.arguments.length > 0) {
712
599
  const injectArg = node.expression.arguments[0];
713
- if (ts.isIdentifier(injectArg) && injectArg.text === 'RouterStateService') {
600
+ if (ts.isIdentifier(injectArg) && injectArg.text === serviceName) {
714
601
  const injectFn = signalPropertyMap[propertyName] || observablePropertyMap[propertyName];
715
602
  if (injectFn) {
716
- // Check if this is inside a toSignal wrapper
717
603
  const insideToSignal = isInsideToSignal(node);
718
- // Skip if inside toSignal - it will be handled by the toSignal pattern above
719
604
  if (insideToSignal) {
720
605
  ts.forEachChild(node, visitNode);
721
606
  return;
722
607
  }
723
- const oldText = node.getText(sourceFile);
724
608
  const type = observablePropertyMap[propertyName] ? 'observable' : 'signal';
725
609
  const injectCall = `${injectFn}()`;
726
610
  const newText = type === 'observable' ? `toObservable(${injectCall})` : injectCall;
727
- updatedContent = updatedContent.replace(oldText, newText);
611
+ edits.push({ start: node.getStart(sourceFile), end: node.getEnd(), text: newText });
728
612
  importsNeeded.add(injectFn);
729
613
  if (type === 'observable') {
730
614
  importsNeeded.add('toObservable');
@@ -736,11 +620,10 @@ function handleInlineInjectPatterns(sourceFile, content) {
736
620
  ts.forEachChild(node, visitNode);
737
621
  }
738
622
  sourceFile.forEachChild(visitNode);
739
- return { content: updatedContent, importsNeeded };
623
+ return { content: applyInlineEdits(content, edits), importsNeeded };
740
624
  }
741
625
  function addImportsToPackage(sourceFile, content, imports, packageName) {
742
626
  const importsList = Array.from(imports).sort();
743
- // Check if import already exists
744
627
  let existingImport;
745
628
  sourceFile.forEachChild((node) => {
746
629
  if (ts.isImportDeclaration(node) &&
@@ -750,18 +633,18 @@ function addImportsToPackage(sourceFile, content, imports, packageName) {
750
633
  }
751
634
  });
752
635
  if (existingImport?.importClause?.namedBindings && ts.isNamedImports(existingImport.importClause.namedBindings)) {
753
- // Add to existing import
754
- const existingImports = existingImport.importClause.namedBindings.elements.map((el) => el.name.text);
755
- const newImports = importsList.filter((imp) => !existingImports.includes(imp));
636
+ const existingElements = existingImport.importClause.namedBindings.elements;
637
+ const existingImportedNames = existingElements.map((el) => el.propertyName?.text ?? el.name.text);
638
+ const existingImports = existingElements.map((el) => el.getText(sourceFile));
639
+ const newImports = importsList.filter((imp) => !existingImportedNames.includes(imp));
756
640
  if (newImports.length === 0)
757
641
  return content;
758
642
  const allImports = [...existingImports, ...newImports].sort();
759
643
  const newImportText = `import { ${allImports.join(', ')} } from '${packageName}';`;
760
644
  const oldImportText = existingImport.getText(sourceFile);
761
- return content.replace(oldImportText, newImportText);
645
+ return content.replace(oldImportText, () => newImportText);
762
646
  }
763
647
  else {
764
- // Add new import at the top
765
648
  const newImportText = `import { ${importsList.join(', ')} } from '${packageName}';\n`;
766
649
  const firstImport = sourceFile.statements.find((stmt) => ts.isImportDeclaration(stmt));
767
650
  if (firstImport) {
@@ -773,65 +656,54 @@ function addImportsToPackage(sourceFile, content, imports, packageName) {
773
656
  }
774
657
  }
775
658
  }
776
- function removeRouterStateServiceInjection(sourceFile, content, routerStateServiceVars, filePath) {
659
+ function removeRouterStateServiceInjection(sourceFile, content, serviceName, filePath) {
777
660
  let updatedContent = content;
778
661
  const modifications = [];
779
- sourceFile.forEachChild((node) => {
780
- if (ts.isClassDeclaration(node)) {
781
- // Collect property removals
782
- node.members.forEach((member) => {
783
- if (ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name)) {
784
- const memberName = member.name.text;
785
- if (routerStateServiceVars.includes(memberName)) {
786
- const memberStart = member.getStart(sourceFile, true);
787
- const memberEnd = member.getEnd();
788
- let lineStart = memberStart;
789
- while (lineStart > 0 && content[lineStart - 1] !== '\n') {
790
- lineStart--;
791
- }
792
- let lineEnd = memberEnd;
793
- while (lineEnd < content.length && content[lineEnd] !== '\n') {
794
- lineEnd++;
795
- }
796
- if (content[lineEnd] === '\n') {
797
- lineEnd++;
798
- }
799
- modifications.push({ start: lineStart, end: lineEnd, replacement: '' });
662
+ collectClasses(sourceFile).forEach((node) => {
663
+ node.members.forEach((member) => {
664
+ if (ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name)) {
665
+ if (isServiceProperty(member, serviceName)) {
666
+ const memberStart = member.getStart(sourceFile, true);
667
+ const memberEnd = member.getEnd();
668
+ let lineStart = memberStart;
669
+ while (lineStart > 0 && content[lineStart - 1] !== '\n') {
670
+ lineStart--;
800
671
  }
801
- }
802
- });
803
- // Collect constructor parameter modifications
804
- node.members.forEach((member) => {
805
- if (ts.isConstructorDeclaration(member) && member.parameters.length > 0) {
806
- const newParams = [];
807
- member.parameters.forEach((param) => {
808
- if (ts.isIdentifier(param.name)) {
809
- const paramName = param.name.text;
810
- if (routerStateServiceVars.includes(paramName)) {
811
- return;
812
- }
813
- }
814
- newParams.push(param.getText(sourceFile));
815
- });
816
- const constructorText = member.getText(sourceFile);
817
- const openParenIndex = constructorText.indexOf('(');
818
- const closeParenIndex = findMatchingParen(constructorText, openParenIndex);
819
- if (openParenIndex === -1 || closeParenIndex === -1) {
820
- console.warn(`Could not find constructor parameters in ${filePath}`);
821
- return;
672
+ let lineEnd = memberEnd;
673
+ while (lineEnd < content.length && content[lineEnd] !== '\n') {
674
+ lineEnd++;
675
+ }
676
+ if (content[lineEnd] === '\n') {
677
+ lineEnd++;
822
678
  }
823
- const constructorStart = member.getStart(sourceFile);
824
- const paramListStart = constructorStart + openParenIndex + 1;
825
- const paramListEnd = constructorStart + closeParenIndex;
826
- const newParamsText = newParams.join(', ');
827
- modifications.push({
828
- start: paramListStart,
829
- end: paramListEnd,
830
- replacement: newParamsText,
831
- });
679
+ modifications.push({ start: lineStart, end: lineEnd, replacement: '' });
832
680
  }
833
- });
834
- }
681
+ }
682
+ });
683
+ node.members.forEach((member) => {
684
+ if (ts.isConstructorDeclaration(member) &&
685
+ member.parameters.some((param) => isServiceParameter(param, serviceName))) {
686
+ const newParams = member.parameters
687
+ .filter((param) => !isServiceParameter(param, serviceName))
688
+ .map((param) => param.getText(sourceFile));
689
+ const constructorText = member.getText(sourceFile);
690
+ const openParenIndex = constructorText.indexOf('(');
691
+ const closeParenIndex = findMatchingParen(constructorText, openParenIndex);
692
+ if (openParenIndex === -1 || closeParenIndex === -1) {
693
+ logger.warn(`Could not find constructor parameters in ${filePath}`);
694
+ return;
695
+ }
696
+ const constructorStart = member.getStart(sourceFile);
697
+ const paramListStart = constructorStart + openParenIndex + 1;
698
+ const paramListEnd = constructorStart + closeParenIndex;
699
+ const newParamsText = newParams.join(', ');
700
+ modifications.push({
701
+ start: paramListStart,
702
+ end: paramListEnd,
703
+ replacement: newParamsText,
704
+ });
705
+ }
706
+ });
835
707
  });
836
708
  modifications.sort((a, b) => b.start - a.start);
837
709
  for (const mod of modifications) {
@@ -853,70 +725,8 @@ function findMatchingParen(text, openIndex) {
853
725
  }
854
726
  return depth === 0 ? i - 1 : -1;
855
727
  }
856
- function removeRouterStateServiceImport(sourceFile, content) {
857
- let updatedContent = content;
858
- sourceFile.forEachChild((node) => {
859
- if (ts.isImportDeclaration(node)) {
860
- const moduleSpecifier = node.moduleSpecifier;
861
- if (ts.isStringLiteral(moduleSpecifier)) {
862
- if (!node.importClause?.namedBindings)
863
- return;
864
- if (ts.isNamedImports(node.importClause.namedBindings)) {
865
- const imports = node.importClause.namedBindings.elements;
866
- const routerStateServiceImport = imports.find((imp) => ts.isImportSpecifier(imp) && imp.name.text === 'RouterStateService');
867
- if (!routerStateServiceImport)
868
- return;
869
- if (imports.length === 1) {
870
- const importStart = node.getStart(sourceFile);
871
- let lineEnd = node.getEnd();
872
- while (lineEnd < content.length && content[lineEnd] !== '\n') {
873
- lineEnd++;
874
- }
875
- if (content[lineEnd] === '\n') {
876
- lineEnd++;
877
- }
878
- updatedContent = content.slice(0, importStart) + content.slice(lineEnd);
879
- }
880
- else {
881
- const namedImportsText = node.importClause.namedBindings.getText(sourceFile);
882
- const patterns = [
883
- new RegExp(`RouterStateService,\\s*`, 'g'),
884
- new RegExp(`,\\s*RouterStateService`, 'g'),
885
- new RegExp(`\\s*RouterStateService\\s*`, 'g'),
886
- ];
887
- let newNamedImports = namedImportsText;
888
- for (const pattern of patterns) {
889
- const temp = newNamedImports.replace(pattern, '');
890
- if (temp !== newNamedImports) {
891
- newNamedImports = temp;
892
- break;
893
- }
894
- }
895
- newNamedImports = newNamedImports.replace(/,\s*,/g, ',').replace(/{\s*,/g, '{').replace(/,\s*}/g, '}');
896
- updatedContent = updatedContent.replace(namedImportsText, newNamedImports);
897
- }
898
- }
899
- }
900
- }
901
- });
902
- return updatedContent;
903
- }
904
- function checkIfRouterStateServiceStillUsed(sourceFile, routerStateServiceVars) {
905
- let stillUsed = false;
906
- function visit(node) {
907
- if (ts.isIdentifier(node) && routerStateServiceVars.includes(node.text)) {
908
- stillUsed = true;
909
- }
910
- if (!stillUsed) {
911
- ts.forEachChild(node, visit);
912
- }
913
- }
914
- sourceFile.forEachChild(visit);
915
- return stillUsed;
916
- }
917
728
  function removeUnusedImports(sourceFile, content) {
918
729
  let updatedContent = content;
919
- // Check if toSignal is still used
920
730
  const hasToSignal = content.includes('toSignal(');
921
731
  if (!hasToSignal) {
922
732
  sourceFile.forEachChild((node) => {
@@ -943,44 +753,31 @@ function removeUnusedImports(sourceFile, content) {
943
753
  }
944
754
  return updatedContent;
945
755
  }
946
- export default async function migrateRouterStateService(tree) {
947
- console.log('\n🔄 Migrating RouterStateService usage...\n');
948
- const tsFiles = [];
949
- function findFiles(dir) {
950
- const children = tree.children(dir);
951
- for (const child of children) {
952
- const path = dir === '.' ? child : `${dir}/${child}`;
953
- if (tree.isFile(path)) {
954
- if (path.endsWith('.ts') && !path.includes('node_modules') && !path.includes('.spec.ts')) {
955
- tsFiles.push(path);
956
- }
957
- }
958
- else {
959
- findFiles(path);
960
- }
961
- }
962
- }
963
- findFiles('.');
756
+ export default async function migrateRouterStateService(tree, scope) {
757
+ const tsFiles = collectFiles(tree, scope, ['.ts']);
758
+ const review = [];
964
759
  let filesModified = 0;
965
- let routerStateServiceUsed = false;
966
760
  for (const filePath of tsFiles) {
967
761
  const content = tree.read(filePath, 'utf-8');
968
762
  if (!content)
969
763
  continue;
970
764
  if (!content.includes('RouterStateService'))
971
765
  continue;
972
- console.log(`Processing: ${filePath}`);
973
766
  const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
974
- const inlineResult = handleInlineInjectPatterns(sourceFile, content);
767
+ const serviceImport = getServiceImport(sourceFile, 'RouterStateService');
768
+ if (!serviceImport?.packageName.startsWith('@ethlete/'))
769
+ continue;
770
+ const serviceName = serviceImport.localName;
771
+ const inlineResult = handleInlineInjectPatterns(sourceFile, content, serviceName);
975
772
  let updatedContent = inlineResult.content;
976
773
  const updatedSourceFile = updatedContent !== content
977
774
  ? ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true)
978
775
  : sourceFile;
979
- const routerStateServiceVars = findRouterStateServiceVariables(updatedSourceFile);
776
+ const routerStateServiceVars = findRouterStateServiceVariables(updatedSourceFile, serviceName);
777
+ const serviceClasses = findServiceClasses(updatedSourceFile, serviceName);
980
778
  if (routerStateServiceVars.length === 0 && inlineResult.importsNeeded.size === 0) {
981
779
  continue;
982
780
  }
983
- routerStateServiceUsed = true;
984
781
  const allImportsNeeded = {
985
782
  '@ethlete/core': new Set(),
986
783
  '@angular/core/rxjs-interop': new Set(),
@@ -993,42 +790,34 @@ export default async function migrateRouterStateService(tree) {
993
790
  allImportsNeeded['@ethlete/core'].add(importName);
994
791
  }
995
792
  }
996
- for (const routerStateServiceVar of routerStateServiceVars) {
997
- const classNode = findClassForRouterStateService(updatedSourceFile, routerStateServiceVar);
998
- if (!classNode)
999
- continue;
1000
- const context = analyzeClassMigration(updatedSourceFile, classNode, routerStateServiceVar);
1001
- context.importsNeeded.forEach((imp) => allImportsNeeded['@ethlete/core'].add(imp));
1002
- context.membersToAdd.forEach((member) => {
1003
- allImportsNeeded['@ethlete/core'].add(member.injectFn);
1004
- if (member.type === 'observable' && !member.wrappedInToSignal) {
1005
- allImportsNeeded['@angular/core/rxjs-interop'].add('toObservable');
793
+ for (const { index, fields } of serviceClasses) {
794
+ for (const routerStateServiceVar of fields) {
795
+ const currentSourceFile = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
796
+ const classNode = collectClasses(currentSourceFile)[index];
797
+ const context = analyzeClassMigration(currentSourceFile, classNode, routerStateServiceVar);
798
+ context.importsNeeded.forEach((imp) => allImportsNeeded['@ethlete/core'].add(imp));
799
+ context.membersToAdd.forEach((member) => {
800
+ allImportsNeeded['@ethlete/core'].add(member.injectFn);
801
+ if (member.type === 'observable' && !member.wrappedInToSignal) {
802
+ allImportsNeeded['@angular/core/rxjs-interop'].add('toObservable');
803
+ }
804
+ });
805
+ for (const replacement of context.replacements.values()) {
806
+ if (replacement.includes('toObservable(')) {
807
+ allImportsNeeded['@angular/core/rxjs-interop'].add('toObservable');
808
+ }
809
+ if (replacement.includes('toSignal(')) {
810
+ allImportsNeeded['@angular/core/rxjs-interop'].add('toSignal');
811
+ }
1006
812
  }
1007
- });
1008
- for (const [original, replacement] of context.replacements) {
1009
- if (replacement.includes('toObservable(')) {
1010
- allImportsNeeded['@angular/core/rxjs-interop'].add('toObservable');
813
+ updatedContent = applyReplacementsInClass(updatedContent, currentSourceFile, classNode, context.replacements);
814
+ if (context.membersToAdd.length > 0) {
815
+ const sourceFileUpdated = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
816
+ updatedContent = addMembersToClass(updatedContent, collectClasses(sourceFileUpdated)[index], context.membersToAdd, routerStateServiceVar);
1011
817
  }
1012
- if (replacement.includes('toSignal(')) {
1013
- allImportsNeeded['@angular/core/rxjs-interop'].add('toSignal');
1014
- }
1015
- }
1016
- for (const [original, replacement] of context.replacements) {
1017
- const regex = new RegExp(escapeRegExp(original), 'g');
1018
- updatedContent = updatedContent.replace(regex, replacement);
1019
- }
1020
- if (context.membersToAdd.length > 0) {
1021
- const sourceFileUpdated = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
1022
- const classNodeUpdated = findClassForRouterStateService(sourceFileUpdated, routerStateServiceVar);
1023
- if (classNodeUpdated) {
1024
- updatedContent = addMembersToClass(sourceFileUpdated, updatedContent, classNodeUpdated, context.membersToAdd, routerStateServiceVar);
1025
- }
1026
- }
1027
- if (context.constructorCalls.length > 0) {
1028
- const sourceFileUpdated = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
1029
- const classNodeUpdated = findClassForRouterStateService(sourceFileUpdated, routerStateServiceVar);
1030
- if (classNodeUpdated) {
1031
- updatedContent = addOrUpdateConstructor(sourceFileUpdated, updatedContent, classNodeUpdated, context.constructorCalls);
818
+ if (context.constructorCalls.length > 0) {
819
+ const sourceFileUpdated = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
820
+ updatedContent = addOrUpdateConstructor(sourceFileUpdated, updatedContent, collectClasses(sourceFileUpdated)[index], context.constructorCalls);
1032
821
  }
1033
822
  }
1034
823
  }
@@ -1039,10 +828,13 @@ export default async function migrateRouterStateService(tree) {
1039
828
  }
1040
829
  }
1041
830
  const sourceFileFinal = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
1042
- updatedContent = removeRouterStateServiceInjection(sourceFileFinal, updatedContent, routerStateServiceVars, filePath);
831
+ updatedContent = removeRouterStateServiceInjection(sourceFileFinal, updatedContent, serviceName, filePath);
1043
832
  const sourceFileAfterRemoval = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
1044
- if (!checkIfRouterStateServiceStillUsed(sourceFileAfterRemoval, routerStateServiceVars)) {
1045
- updatedContent = removeRouterStateServiceImport(sourceFileAfterRemoval, updatedContent);
833
+ if (isServiceStillUsed(sourceFileAfterRemoval, serviceName, serviceClasses)) {
834
+ review.push(`${filePath}: RouterStateService is still used. Migrate the remaining usages manually.`);
835
+ }
836
+ else {
837
+ updatedContent = removeServiceImport(sourceFileAfterRemoval, updatedContent, 'RouterStateService');
1046
838
  }
1047
839
  const sourceFileAfterCleanup = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
1048
840
  updatedContent = removeUnusedImports(sourceFileAfterCleanup, updatedContent);
@@ -1051,14 +843,15 @@ export default async function migrateRouterStateService(tree) {
1051
843
  filesModified++;
1052
844
  }
1053
845
  }
1054
- if (filesModified > 0) {
1055
- console.log(`\n✅ Successfully migrated RouterStateService in ${filesModified} file(s)\n`);
1056
- }
1057
- else if (routerStateServiceUsed) {
1058
- console.log('\nℹ️ RouterStateService detected but no migrations needed\n');
1059
- }
1060
- else {
1061
- console.log('\nℹ️ No RouterStateService usage found\n');
846
+ return { filesChanged: filesModified, review };
847
+ }
848
+ function applyInlineEdits(content, edits) {
849
+ const accepted = [];
850
+ for (const edit of [...edits].sort((a, b) => a.start - b.start || b.end - a.end)) {
851
+ const last = accepted.at(-1);
852
+ if (last && edit.start < last.end)
853
+ continue;
854
+ accepted.push(edit);
1062
855
  }
856
+ return accepted.reduceRight((result, edit) => result.slice(0, edit.start) + edit.text + result.slice(edit.end), content);
1063
857
  }
1064
- //# sourceMappingURL=router-state-service.js.map