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