@ethlete/core 5.0.0-next.7 → 5.0.0-next.71

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