@ethlete/core 5.0.0-next.60 → 5.0.0-next.62

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,78 +1,40 @@
1
- /* eslint-disable @typescript-eslint/no-unused-vars */
2
1
  import { logger } from '@nx/devkit';
3
2
  import * as ts from 'typescript';
4
- export default async function migrateViewportService(tree) {
5
- logger.log('\nšŸ”„ Migrating ViewportService to standalone utilities...\n');
6
- const tsFiles = [];
7
- const styleFiles = [];
8
- // Collect all TypeScript and style files
9
- function findFiles(dir) {
10
- const children = tree.children(dir);
11
- for (const child of children) {
12
- const path = dir === '.' ? child : `${dir}/${child}`;
13
- if (tree.isFile(path)) {
14
- if (path.endsWith('.ts') && !path.includes('node_modules')) {
15
- tsFiles.push(path);
16
- }
17
- else if (path.endsWith('.css') ||
18
- path.endsWith('.scss') ||
19
- path.endsWith('.sass') ||
20
- path.endsWith('.less')) {
21
- styleFiles.push(path);
22
- }
23
- }
24
- else {
25
- if (child === 'node_modules')
26
- continue;
27
- findFiles(path);
28
- }
29
- }
30
- }
31
- findFiles('.');
32
- // Detect which CSS variables are being used across the codebase
33
- const cssVariablesUsed = detectCssVariableUsage(tree, styleFiles);
3
+ import { collectFiles } from './migration-files.js';
4
+ import { applyReplacementsInClass, collectClasses, findServiceClasses, getServiceImport, isServiceParameter, isServiceProperty, isServiceStillUsed, removeServiceImport, } from './service-classes.js';
5
+ export default async function migrateViewportService(tree, scope) {
6
+ const tsFiles = collectFiles(tree, scope, ['.ts']);
7
+ const review = [];
34
8
  let filesModified = 0;
35
9
  let templatesModified = 0;
36
- let viewportServiceUsed = false;
37
- // Track which properties became signals for template migration
38
- const componentTemplateMigrations = new Map();
39
- // Process each TypeScript file
10
+ const externalTemplateMigrations = new Map();
40
11
  for (const filePath of tsFiles) {
41
12
  const content = tree.read(filePath, 'utf-8');
42
13
  if (!content)
43
14
  continue;
44
- // Skip files that don't use ViewportService
45
15
  if (!content.includes('ViewportService'))
46
16
  continue;
47
- logger.log(`Processing: ${filePath}`);
48
17
  const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
49
- const viewportPackage = getViewportServicePackage(sourceFile);
50
- if (viewportPackage !== '@ethlete/core' && !viewportPackage?.startsWith('@fifa-gg/uikit'))
18
+ const serviceImport = getServiceImport(sourceFile, 'ViewportService');
19
+ const viewportPackage = serviceImport?.packageName;
20
+ if (!serviceImport || (viewportPackage !== '@ethlete/core' && !viewportPackage?.startsWith('@fifa-gg/uikit'))) {
51
21
  continue;
52
- // Check if this is a component with a template
53
- const templatePath = findTemplateForComponent(tree, filePath, sourceFile);
54
- // Track signal properties for template migration
55
- const templateMigrationInfo = {
56
- signalProperties: new Set(),
57
- };
58
- // First handle inline inject patterns (e.g., toSignal(inject(ViewportService).isXs$))
59
- const inlineResult = handleInlineInjectPatterns(sourceFile, content);
22
+ }
23
+ const serviceName = serviceImport.localName;
24
+ const inlineResult = handleInlineInjectPatterns(sourceFile, content, serviceName);
60
25
  let updatedContent = inlineResult.content;
61
- // Re-parse if content changed
62
26
  const updatedSourceFile = updatedContent !== content
63
27
  ? ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true)
64
28
  : sourceFile;
65
- // Find all ViewportService variables (injected or constructor params)
66
- const viewportServiceVars = findViewportServiceVariables(updatedSourceFile);
29
+ const viewportServiceVars = findViewportServiceVariables(updatedSourceFile, serviceName);
30
+ const serviceClasses = findServiceClasses(updatedSourceFile, serviceName);
67
31
  if (viewportServiceVars.length === 0 && inlineResult.importsNeeded.size === 0) {
68
32
  continue;
69
33
  }
70
- viewportServiceUsed = true;
71
34
  const allImportsNeeded = {
72
35
  '@ethlete/core': new Set(),
73
36
  '@angular/core/rxjs-interop': new Set(),
74
37
  };
75
- // Merge imports from inline patterns
76
38
  for (const importName of inlineResult.importsNeeded) {
77
39
  if (importName === 'toObservable' || importName === 'toSignal') {
78
40
  allImportsNeeded['@angular/core/rxjs-interop'].add(importName);
@@ -81,205 +43,143 @@ export default async function migrateViewportService(tree) {
81
43
  allImportsNeeded['@ethlete/core'].add(importName);
82
44
  }
83
45
  }
84
- // Process each ViewportService variable found in the file
85
- for (const viewportServiceVar of viewportServiceVars) {
86
- const classNode = findClassForViewportService(updatedSourceFile, viewportServiceVar);
87
- if (!classNode)
88
- continue;
89
- // Analyze what needs to be migrated BEFORE making any changes
90
- const context = analyzeClassMigration(updatedSourceFile, classNode, viewportServiceVar);
91
- // Track imports needed from direct replacements
92
- context.importsNeeded.forEach((imp) => allImportsNeeded['@ethlete/core'].add(imp));
93
- // Track imports needed from new members
94
- context.membersToAdd.forEach((member) => {
95
- allImportsNeeded['@ethlete/core'].add(member.injectFn);
96
- if (member.type === 'observable' && !member.wrappedInToSignal) {
97
- allImportsNeeded['@angular/core/rxjs-interop'].add('toObservable');
98
- }
99
- });
100
- // Check if any replacements use toObservable or toSignal
101
- for (const [original, replacement] of [...context.replacements].sort((a, b) => b[0].length - a[0].length)) {
102
- if (replacement.includes('toObservable(')) {
103
- allImportsNeeded['@angular/core/rxjs-interop'].add('toObservable');
104
- }
105
- if (replacement.includes('toSignal(')) {
106
- allImportsNeeded['@angular/core/rxjs-interop'].add('toSignal');
46
+ for (const { index, fields } of serviceClasses) {
47
+ for (const viewportServiceVar of fields) {
48
+ const currentSourceFile = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
49
+ const classNode = collectClasses(currentSourceFile)[index];
50
+ const context = analyzeClassMigration(currentSourceFile, classNode, viewportServiceVar);
51
+ context.importsNeeded.forEach((imp) => allImportsNeeded['@ethlete/core'].add(imp));
52
+ context.membersToAdd.forEach((member) => {
53
+ allImportsNeeded['@ethlete/core'].add(member.injectFn);
54
+ if (member.type === 'observable' && !member.wrappedInToSignal) {
55
+ allImportsNeeded['@angular/core/rxjs-interop'].add('toObservable');
56
+ }
57
+ });
58
+ for (const replacement of context.replacements.values()) {
59
+ if (replacement.includes('toObservable(')) {
60
+ allImportsNeeded['@angular/core/rxjs-interop'].add('toObservable');
61
+ }
62
+ if (replacement.includes('toSignal(')) {
63
+ allImportsNeeded['@angular/core/rxjs-interop'].add('toSignal');
64
+ }
107
65
  }
108
- }
109
- // Apply replacements FIRST (before adding members)
110
- for (const [original, replacement] of context.replacements) {
111
- const regex = new RegExp(escapeRegExp(original), 'g');
112
- updatedContent = updatedContent.replace(regex, replacement);
113
- }
114
- // Then add new members to the updated content
115
- if (context.membersToAdd.length > 0) {
116
- const sourceFileUpdated = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
117
- const classNodeUpdated = findClassForViewportService(sourceFileUpdated, viewportServiceVar);
118
- if (classNodeUpdated) {
119
- updatedContent = addMembersToClass(sourceFileUpdated, updatedContent, classNodeUpdated, context.membersToAdd, viewportServiceVar);
66
+ updatedContent = applyReplacementsInClass(updatedContent, currentSourceFile, classNode, context.replacements);
67
+ if (context.membersToAdd.length > 0) {
68
+ const sourceFileUpdated = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
69
+ updatedContent = addMembersToClass(sourceFileUpdated, updatedContent, collectClasses(sourceFileUpdated)[index], context.membersToAdd, viewportServiceVar);
120
70
  }
121
71
  }
122
72
  }
123
- // Handle monitorViewport migrations
124
73
  const sourceFileAfterReplacements = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
125
- const monitorViewportResult = migrateMonitorViewport(sourceFileAfterReplacements, updatedContent, viewportServiceVars, cssVariablesUsed);
74
+ const monitorViewportResult = migrateMonitorViewport(sourceFileAfterReplacements, updatedContent, viewportServiceVars);
126
75
  updatedContent = monitorViewportResult.content;
127
76
  monitorViewportResult.imports.forEach((imp) => allImportsNeeded['@ethlete/core'].add(imp));
128
- // Add necessary imports
129
77
  for (const [packageName, importsSet] of Object.entries(allImportsNeeded)) {
130
78
  if (importsSet.size > 0) {
131
79
  const sourceFileUpdated = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
132
80
  updatedContent = addImportsToPackage(sourceFileUpdated, updatedContent, importsSet, packageName);
133
81
  }
134
82
  }
135
- // Remove ViewportService injection (properties and constructor params)
136
83
  const sourceFileFinal = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
137
- updatedContent = removeViewportServiceInjection(sourceFileFinal, updatedContent, viewportServiceVars, filePath);
138
- // Remove ViewportService import if no longer needed
84
+ updatedContent = removeViewportServiceInjection(sourceFileFinal, updatedContent, serviceName, filePath);
139
85
  const sourceFileAfterRemoval = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
140
- if (!checkIfViewportServiceStillUsed(sourceFileAfterRemoval, viewportServiceVars)) {
141
- updatedContent = removeViewportServiceImport(sourceFileAfterRemoval, updatedContent);
86
+ if (isServiceStillUsed(sourceFileAfterRemoval, serviceName, serviceClasses)) {
87
+ review.push(`${filePath}: ViewportService is still used. Migrate the remaining usages manually.`);
88
+ }
89
+ else {
90
+ updatedContent = removeServiceImport(sourceFileAfterRemoval, updatedContent, 'ViewportService');
142
91
  }
143
- // Clean up unused imports (toSignal, toObservable)
144
92
  const sourceFileAfterCleanup = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
145
93
  updatedContent = removeUnusedImports(sourceFileAfterCleanup, updatedContent);
146
- // Always collect signalProperties for this file
147
- const updatedSourceFileForSignals = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
148
- updatedSourceFileForSignals.forEachChild((node) => {
149
- if (ts.isClassDeclaration(node)) {
150
- node.members.forEach((member) => {
151
- if (ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name)) {
152
- const memberName = member.name.text;
153
- const isPublicOrProtected = !member.modifiers?.some((mod) => mod.kind === ts.SyntaxKind.PrivateKeyword);
154
- if (isPublicOrProtected && member.initializer) {
155
- const initText = member.initializer.getText(updatedSourceFileForSignals);
156
- // Check if this property uses any of our inject functions (which return signals)
157
- const usesSignalInject = initText.includes('injectIsXs()') ||
158
- initText.includes('injectIsSm()') ||
159
- initText.includes('injectIsMd()') ||
160
- initText.includes('injectIsLg()') ||
161
- initText.includes('injectIsXl()') ||
162
- initText.includes('injectIs2Xl()') ||
163
- initText.includes('injectIsBase()') ||
164
- initText.includes('injectCurrentBreakpoint()') ||
165
- initText.includes('injectViewportDimensions()') ||
166
- initText.includes('injectScrollbarDimensions()') ||
167
- initText.includes('injectObserveBreakpoint()') ||
168
- initText.includes('injectBreakpointIsMatched()');
169
- if (usesSignalInject && !initText.includes('toObservable(')) {
170
- templateMigrationInfo.signalProperties.add(memberName);
171
- }
172
- }
173
- }
174
- });
175
- }
176
- });
177
- // Only add to componentTemplateMigrations if there's an external template
178
- if (templatePath && templateMigrationInfo.signalProperties.size > 0) {
179
- componentTemplateMigrations.set(filePath, templateMigrationInfo);
180
- }
181
- const updatedSourceFileForInline = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
94
+ const sourceFileForTemplates = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
95
+ const originalClasses = collectClasses(sourceFile);
182
96
  const inlineTemplateEdits = [];
183
- updatedSourceFileForInline.forEachChild((node) => {
184
- if (ts.isClassDeclaration(node)) {
185
- const decorators =
186
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
187
- ts.getDecorators?.(node) ?? node.decorators;
188
- decorators?.forEach((decorator) => {
189
- if (ts.isCallExpression(decorator.expression) &&
190
- decorator.expression.arguments.length > 0 &&
191
- ts.isObjectLiteralExpression(decorator.expression.arguments[0])) {
192
- const obj = decorator.expression.arguments[0];
193
- obj.properties.forEach((prop) => {
194
- if (ts.isPropertyAssignment(prop) &&
195
- ts.isIdentifier(prop.name) &&
196
- prop.name.text === 'template' &&
197
- (ts.isNoSubstitutionTemplateLiteral(prop.initializer) || ts.isStringLiteral(prop.initializer))) {
198
- let templateText = prop.initializer.getText();
199
- // Remove the surrounding quotes/backticks
200
- templateText = templateText.slice(1, -1);
201
- templateText = migrateTemplateText(templateText, templateMigrationInfo.signalProperties);
202
- // Re-wrap with original quotes/backticks
203
- const quote = prop.initializer.getText()[0];
204
- const newInitializer = quote + templateText + quote;
205
- inlineTemplateEdits.push({
206
- start: prop.initializer.getStart(updatedSourceFileForInline),
207
- end: prop.initializer.getEnd(),
208
- replacement: newInitializer,
209
- });
210
- }
211
- });
212
- }
213
- });
97
+ collectClasses(sourceFileForTemplates).forEach((classNode, index) => {
98
+ if (originalClasses[index]?.getText(sourceFile) === classNode.getText(sourceFileForTemplates))
99
+ return;
100
+ const signalProperties = collectSignalProperties(sourceFileForTemplates, classNode);
101
+ if (signalProperties.size === 0)
102
+ return;
103
+ const template = findComponentTemplate(classNode);
104
+ if (!template)
105
+ return;
106
+ if (template.kind === 'url') {
107
+ const componentDir = filePath.substring(0, filePath.lastIndexOf('/'));
108
+ const templatePath = `${componentDir}/${template.path}`.replace(/\/\.\//g, '/').replace(/\/\//g, '/');
109
+ const existing = externalTemplateMigrations.get(templatePath) ?? new Set();
110
+ signalProperties.forEach((property) => existing.add(property));
111
+ externalTemplateMigrations.set(templatePath, existing);
112
+ return;
214
113
  }
114
+ const initializerText = template.initializer.getText(sourceFileForTemplates);
115
+ const quote = initializerText[0];
116
+ inlineTemplateEdits.push({
117
+ start: template.initializer.getStart(sourceFileForTemplates),
118
+ end: template.initializer.getEnd(),
119
+ replacement: quote + migrateTemplateText(initializerText.slice(1, -1), signalProperties) + quote,
120
+ });
215
121
  });
216
122
  inlineTemplateEdits.sort((a, b) => b.start - a.start);
217
123
  for (const edit of inlineTemplateEdits) {
218
124
  updatedContent = updatedContent.slice(0, edit.start) + edit.replacement + updatedContent.slice(edit.end);
219
125
  }
220
- // Write the updated content if changes were made
221
126
  if (updatedContent !== content) {
222
127
  tree.write(filePath, updatedContent);
223
128
  filesModified++;
224
129
  }
225
130
  }
226
- // Migrate templates
227
- for (const [tsFilePath, migrationInfo] of componentTemplateMigrations) {
228
- const sourceFile = ts.createSourceFile(tsFilePath, tree.read(tsFilePath, 'utf-8'), ts.ScriptTarget.Latest, true);
229
- const templatePath = findTemplateForComponent(tree, tsFilePath, sourceFile);
230
- if (templatePath && tree.exists(templatePath)) {
231
- logger.log(`Processing template: ${templatePath}`);
232
- const wasModified = migrateTemplateFile(tree, templatePath, migrationInfo);
233
- if (wasModified) {
234
- templatesModified++;
235
- }
131
+ for (const [templatePath, signalProperties] of externalTemplateMigrations) {
132
+ if (tree.exists(templatePath) && migrateTemplateFile(tree, templatePath, { signalProperties })) {
133
+ templatesModified++;
236
134
  }
237
135
  }
238
- // Log results
239
- if (filesModified > 0 || templatesModified > 0) {
240
- logger.log(`\nāœ… Successfully migrated ViewportService in ${filesModified} TypeScript file(s) and ${templatesModified} template(s)\n`);
241
- }
242
- else if (viewportServiceUsed) {
243
- logger.log('\nā„¹ļø ViewportService detected but no migrations needed\n');
244
- }
245
- else {
246
- logger.log('\nā„¹ļø No ViewportService usage found\n');
247
- }
136
+ return { filesChanged: filesModified + templatesModified, review };
248
137
  }
249
- function detectCssVariableUsage(tree, styleFiles) {
250
- let hasViewportVariables = false;
251
- let hasScrollbarVariables = false;
252
- // Check both style files AND TypeScript files (for inline styles)
253
- const allFiles = [...styleFiles];
254
- // Also check all TypeScript files for inline styles
255
- function findTsFiles(dir) {
256
- const children = tree.children(dir);
257
- for (const child of children) {
258
- const path = dir === '.' ? child : `${dir}/${child}`;
259
- if (tree.isFile(path) && path.endsWith('.ts')) {
260
- allFiles.push(path);
261
- }
262
- else if (!tree.isFile(path)) {
263
- findTsFiles(path);
264
- }
138
+ const SIGNAL_INJECT_CALLS = [
139
+ 'injectIsXs()',
140
+ 'injectIsSm()',
141
+ 'injectIsMd()',
142
+ 'injectIsLg()',
143
+ 'injectIsXl()',
144
+ 'injectIs2Xl()',
145
+ 'injectIsBase()',
146
+ 'injectCurrentBreakpoint()',
147
+ 'injectViewportDimensions()',
148
+ 'injectScrollbarDimensions()',
149
+ 'injectObserveBreakpoint()',
150
+ 'injectBreakpointIsMatched()',
151
+ ];
152
+ function collectSignalProperties(sourceFile, classNode) {
153
+ const signalProperties = new Set();
154
+ for (const member of classNode.members) {
155
+ if (!ts.isPropertyDeclaration(member) || !ts.isIdentifier(member.name) || !member.initializer)
156
+ continue;
157
+ if (member.modifiers?.some((mod) => mod.kind === ts.SyntaxKind.PrivateKeyword))
158
+ continue;
159
+ const initText = member.initializer.getText(sourceFile);
160
+ if (SIGNAL_INJECT_CALLS.some((call) => initText.includes(call)) && !initText.includes('toObservable(')) {
161
+ signalProperties.add(member.name.text);
265
162
  }
266
163
  }
267
- findTsFiles('.');
268
- for (const filePath of allFiles) {
269
- const content = tree.read(filePath, 'utf-8');
270
- if (!content)
164
+ return signalProperties;
165
+ }
166
+ function findComponentTemplate(classNode) {
167
+ for (const decorator of ts.getDecorators(classNode) ?? []) {
168
+ const metadata = ts.isCallExpression(decorator.expression) ? decorator.expression.arguments[0] : undefined;
169
+ if (!metadata || !ts.isObjectLiteralExpression(metadata))
271
170
  continue;
272
- if (content.includes('--et-vw') || content.includes('--et-vh')) {
273
- hasViewportVariables = true;
274
- }
275
- if (content.includes('--et-sw') || content.includes('--et-sh')) {
276
- hasScrollbarVariables = true;
277
- }
278
- if (hasViewportVariables && hasScrollbarVariables) {
279
- break; // Found both, no need to continue
171
+ for (const prop of metadata.properties) {
172
+ if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name))
173
+ continue;
174
+ if (!ts.isNoSubstitutionTemplateLiteral(prop.initializer) && !ts.isStringLiteral(prop.initializer))
175
+ continue;
176
+ if (prop.name.text === 'template')
177
+ return { kind: 'inline', initializer: prop.initializer };
178
+ if (prop.name.text === 'templateUrl')
179
+ return { kind: 'url', path: prop.initializer.text };
280
180
  }
281
181
  }
282
- return { hasViewportVariables, hasScrollbarVariables };
182
+ return null;
283
183
  }
284
184
  const migrateTemplateExpression = (expression, signalProperties) => {
285
185
  let migrated = expression;
@@ -304,38 +204,6 @@ function migrateTemplateFile(tree, htmlFilePath, migrationInfo) {
304
204
  }
305
205
  return hasChanges;
306
206
  }
307
- function findTemplateForComponent(tree, tsFilePath, sourceFile) {
308
- let templatePath = null;
309
- function visit(node) {
310
- if (templatePath)
311
- return;
312
- // Look for @Component decorator
313
- if (ts.isDecorator(node)) {
314
- const expression = node.expression;
315
- if (ts.isCallExpression(expression)) {
316
- const args = expression.arguments;
317
- if (args.length > 0 && ts.isObjectLiteralExpression(args[0])) {
318
- const properties = args[0].properties;
319
- for (const prop of properties) {
320
- if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === 'templateUrl') {
321
- if (ts.isStringLiteral(prop.initializer)) {
322
- const templateRelativePath = prop.initializer.text;
323
- // Resolve the template path relative to the component file
324
- const componentDir = tsFilePath.substring(0, tsFilePath.lastIndexOf('/'));
325
- templatePath = `${componentDir}/${templateRelativePath}`;
326
- // Normalize the path
327
- templatePath = templatePath.replace(/\/\.\//g, '/').replace(/\/\//g, '/');
328
- }
329
- }
330
- }
331
- }
332
- }
333
- }
334
- ts.forEachChild(node, visit);
335
- }
336
- visit(sourceFile);
337
- return templatePath;
338
- }
339
207
  function getViewportServicePackage(sourceFile) {
340
208
  let packageName = null;
341
209
  sourceFile.forEachChild((node) => {
@@ -501,7 +369,7 @@ function analyzeClassMigration(sourceFile, classNode, viewportServiceVar) {
501
369
  });
502
370
  // Group property initializer usages by their ViewportService usage pattern
503
371
  const propertyInitializerUsages = new Map();
504
- for (const [usageText, { propertyName, usage, propertyDecl }] of usagesInPropertyInitializers) {
372
+ for (const [, { propertyName, usage, propertyDecl }] of usagesInPropertyInitializers) {
505
373
  let injectFn;
506
374
  let args;
507
375
  let type = 'signal';
@@ -569,7 +437,7 @@ function analyzeClassMigration(sourceFile, classNode, viewportServiceVar) {
569
437
  });
570
438
  if (isUsedOutsideInitializers && type === 'observable') {
571
439
  // Create a shared member for all properties that use this observable
572
- const baseName = args ? generateNameFromArgs(usageKey, args, type) : usageKey;
440
+ const baseName = args ? generateNameFromArgs(args, type) : usageKey;
573
441
  const memberName = findAvailableMemberName(baseName, context.existingMembers);
574
442
  const memberInfo = {
575
443
  name: memberName,
@@ -600,7 +468,7 @@ function analyzeClassMigration(sourceFile, classNode, viewportServiceVar) {
600
468
  // OR if the property is used elsewhere in the class
601
469
  const propertyName = ts.isIdentifier(propertyDecl.name) ? propertyDecl.name.text : '';
602
470
  const isObservableProperty = propertyName.endsWith('$');
603
- const isPropertyUsedElsewhere = checkIfPropertyIsUsedElsewhere(sourceFile, classNode, propertyName, propertyDecl);
471
+ const isPropertyUsedElsewhere = checkIfPropertyIsUsedElsewhere(classNode, propertyName, propertyDecl);
604
472
  // Wrap with toObservable if:
605
473
  // 1. It's an observable type AND
606
474
  // 2. (Property name ends with $ OR property is used elsewhere)
@@ -719,7 +587,7 @@ function analyzeClassMigration(sourceFile, classNode, viewportServiceVar) {
719
587
  else {
720
588
  let baseName;
721
589
  if (usage.args) {
722
- baseName = generateNameFromArgs(originalProperty, usage.args, usage.type);
590
+ baseName = generateNameFromArgs(usage.args, usage.type);
723
591
  }
724
592
  else {
725
593
  baseName = usage.type === 'observable' ? originalProperty : originalProperty.replace(/\$$/, '');
@@ -844,7 +712,7 @@ function addMembersToClass(sourceFile, content, classNode, membersToAdd, viewpor
844
712
  const classText = classNode.getText(sourceFile);
845
713
  const openBraceIndex = classText.indexOf('{');
846
714
  if (openBraceIndex === -1) {
847
- console.warn('Could not find class body opening brace');
715
+ logger.warn('Could not find class body opening brace');
848
716
  return content;
849
717
  }
850
718
  const classStart = classNode.getStart(sourceFile);
@@ -890,70 +758,57 @@ function addMembersToClass(sourceFile, content, classNode, membersToAdd, viewpor
890
758
  }
891
759
  return content.slice(0, finalInsertPosition) + prefix + newMembers + suffix + content.slice(finalInsertPosition);
892
760
  }
893
- function removeViewportServiceInjection(sourceFile, content, viewportServiceVars, filePath) {
761
+ function removeViewportServiceInjection(sourceFile, content, serviceName, filePath) {
894
762
  let updatedContent = content;
895
763
  const modifications = [];
896
- // Collect all modifications first (in reverse order by position)
897
- sourceFile.forEachChild((node) => {
898
- if (ts.isClassDeclaration(node)) {
899
- // Collect property removals
900
- node.members.forEach((member) => {
901
- if (ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name)) {
902
- const memberName = member.name.text;
903
- if (viewportServiceVars.includes(memberName)) {
904
- const memberStart = member.getStart(sourceFile, true);
905
- const memberEnd = member.getEnd();
906
- // Find the line boundaries
907
- let lineStart = memberStart;
908
- while (lineStart > 0 && content[lineStart - 1] !== '\n') {
909
- lineStart--;
910
- }
911
- let lineEnd = memberEnd;
912
- while (lineEnd < content.length && content[lineEnd] !== '\n') {
913
- lineEnd++;
914
- }
915
- if (content[lineEnd] === '\n') {
916
- lineEnd++;
917
- }
918
- modifications.push({ start: lineStart, end: lineEnd, replacement: '' });
764
+ collectClasses(sourceFile).forEach((node) => {
765
+ node.members.forEach((member) => {
766
+ if (ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name)) {
767
+ if (isServiceProperty(member, serviceName)) {
768
+ const memberStart = member.getStart(sourceFile, true);
769
+ const memberEnd = member.getEnd();
770
+ // Find the line boundaries
771
+ let lineStart = memberStart;
772
+ while (lineStart > 0 && content[lineStart - 1] !== '\n') {
773
+ lineStart--;
919
774
  }
920
- }
921
- });
922
- // Collect constructor parameter modifications
923
- node.members.forEach((member) => {
924
- if (ts.isConstructorDeclaration(member) && member.parameters.length > 0) {
925
- const newParams = [];
926
- member.parameters.forEach((param) => {
927
- if (ts.isIdentifier(param.name)) {
928
- const paramName = param.name.text;
929
- // Skip ViewportService parameters
930
- if (viewportServiceVars.includes(paramName)) {
931
- return;
932
- }
933
- }
934
- // Keep this parameter
935
- newParams.push(param.getText(sourceFile));
936
- });
937
- // Find parameter list boundaries
938
- const constructorText = member.getText(sourceFile);
939
- const openParenIndex = constructorText.indexOf('(');
940
- const closeParenIndex = findMatchingParen(constructorText, openParenIndex);
941
- if (openParenIndex === -1 || closeParenIndex === -1) {
942
- console.warn(`Could not find constructor parameters in ${filePath}`);
943
- return;
775
+ let lineEnd = memberEnd;
776
+ while (lineEnd < content.length && content[lineEnd] !== '\n') {
777
+ lineEnd++;
944
778
  }
945
- const constructorStart = member.getStart(sourceFile);
946
- const paramListStart = constructorStart + openParenIndex + 1;
947
- const paramListEnd = constructorStart + closeParenIndex;
948
- const newParamsText = newParams.join(', ');
949
- modifications.push({
950
- start: paramListStart,
951
- end: paramListEnd,
952
- replacement: newParamsText,
953
- });
779
+ if (content[lineEnd] === '\n') {
780
+ lineEnd++;
781
+ }
782
+ modifications.push({ start: lineStart, end: lineEnd, replacement: '' });
954
783
  }
955
- });
956
- }
784
+ }
785
+ });
786
+ // Collect constructor parameter modifications
787
+ node.members.forEach((member) => {
788
+ if (ts.isConstructorDeclaration(member) &&
789
+ member.parameters.some((param) => isServiceParameter(param, serviceName))) {
790
+ const newParams = member.parameters
791
+ .filter((param) => !isServiceParameter(param, serviceName))
792
+ .map((param) => param.getText(sourceFile));
793
+ // Find parameter list boundaries
794
+ const constructorText = member.getText(sourceFile);
795
+ const openParenIndex = constructorText.indexOf('(');
796
+ const closeParenIndex = findMatchingParen(constructorText, openParenIndex);
797
+ if (openParenIndex === -1 || closeParenIndex === -1) {
798
+ logger.warn(`Could not find constructor parameters in ${filePath}`);
799
+ return;
800
+ }
801
+ const constructorStart = member.getStart(sourceFile);
802
+ const paramListStart = constructorStart + openParenIndex + 1;
803
+ const paramListEnd = constructorStart + closeParenIndex;
804
+ const newParamsText = newParams.join(', ');
805
+ modifications.push({
806
+ start: paramListStart,
807
+ end: paramListEnd,
808
+ replacement: newParamsText,
809
+ });
810
+ }
811
+ });
957
812
  });
958
813
  // Sort modifications by start position (descending) to apply from end to start
959
814
  modifications.sort((a, b) => b.start - a.start);
@@ -977,7 +832,7 @@ function findMatchingParen(text, openIndex) {
977
832
  }
978
833
  return depth === 0 ? i - 1 : -1;
979
834
  }
980
- function generateNameFromArgs(methodName, args, type) {
835
+ function generateNameFromArgs(args, type) {
981
836
  // Parse the arguments to generate a meaningful name
982
837
  // e.g., { min: 'lg' } => isMinLg or isMinLg$
983
838
  // e.g., { max: 'sm' } => isMaxSm or isMaxSm$
@@ -1062,35 +917,6 @@ function createReplacementsForMember(sourceFile, classNode, viewportServiceVar,
1062
917
  visitNode(member);
1063
918
  });
1064
919
  }
1065
- function findClassForViewportService(sourceFile, viewportServiceVar) {
1066
- let classNode;
1067
- function visit(node) {
1068
- if (classNode)
1069
- return;
1070
- if (ts.isClassDeclaration(node)) {
1071
- // Check if this class has the viewportService member
1072
- const hasMember = node.members.some((member) => {
1073
- if (ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name)) {
1074
- return member.name.text === viewportServiceVar;
1075
- }
1076
- if (ts.isParameter(member) && ts.isIdentifier(member.name)) {
1077
- return member.name.text === viewportServiceVar;
1078
- }
1079
- // Check constructor parameters
1080
- if (ts.isConstructorDeclaration(member)) {
1081
- return member.parameters.some((param) => ts.isIdentifier(param.name) && param.name.text === viewportServiceVar);
1082
- }
1083
- return false;
1084
- });
1085
- if (hasMember) {
1086
- classNode = node;
1087
- }
1088
- }
1089
- ts.forEachChild(node, visit);
1090
- }
1091
- visit(sourceFile);
1092
- return classNode;
1093
- }
1094
920
  function findAvailableMemberName(baseName, existingMembers) {
1095
921
  // baseName already has $ suffix for observables at this point
1096
922
  // Try variations: name, _name, #name
@@ -1107,7 +933,7 @@ function findAvailableMemberName(baseName, existingMembers) {
1107
933
  }
1108
934
  return `${baseName}${counter}`;
1109
935
  }
1110
- function migrateMonitorViewport(sourceFile, content, viewportServiceVars, cssVariablesUsed) {
936
+ function migrateMonitorViewport(sourceFile, content, viewportServiceVars) {
1111
937
  let updatedContent = content;
1112
938
  const imports = [];
1113
939
  const monitorViewportCalls = [];
@@ -1212,7 +1038,7 @@ function migrateMonitorViewport(sourceFile, content, viewportServiceVars, cssVar
1212
1038
  const classText = classNode.getText(sourceFile);
1213
1039
  const openBraceIndex = classText.indexOf('{');
1214
1040
  if (openBraceIndex === -1) {
1215
- console.warn('Could not find class body opening brace');
1041
+ logger.warn('Could not find class body opening brace');
1216
1042
  continue;
1217
1043
  }
1218
1044
  const insertPosition = classStart + openBraceIndex + 1;
@@ -1266,7 +1092,7 @@ function addImportsToPackage(sourceFile, content, neededImports, packageName) {
1266
1092
  const allImports = [...existingImportNames, ...newImports].sort();
1267
1093
  const importText = existingImport.getText(sourceFile);
1268
1094
  const newImportText = `import { ${allImports.join(', ')} } from '${packageName}';`;
1269
- return content.replace(importText, newImportText);
1095
+ return content.replace(importText, () => newImportText);
1270
1096
  }
1271
1097
  }
1272
1098
  else if (!existingImport) {
@@ -1277,87 +1103,7 @@ function addImportsToPackage(sourceFile, content, neededImports, packageName) {
1277
1103
  }
1278
1104
  return content;
1279
1105
  }
1280
- function checkIfViewportServiceStillUsed(sourceFile, viewportServiceVars) {
1281
- let stillUsed = false;
1282
- function visit(node) {
1283
- if (stillUsed || !ts.isPropertyAccessExpression(node)) {
1284
- if (!stillUsed)
1285
- ts.forEachChild(node, visit);
1286
- return;
1287
- }
1288
- const expression = node.expression;
1289
- const isUsed = (ts.isPropertyAccessExpression(expression) && viewportServiceVars.includes(expression.name.text)) ||
1290
- (ts.isIdentifier(expression) && viewportServiceVars.includes(expression.text));
1291
- if (isUsed) {
1292
- stillUsed = true;
1293
- }
1294
- else {
1295
- ts.forEachChild(node, visit);
1296
- }
1297
- }
1298
- visit(sourceFile);
1299
- return stillUsed;
1300
- }
1301
- function removeViewportServiceImport(sourceFile, content) {
1302
- let updatedContent = content;
1303
- sourceFile.forEachChild((node) => {
1304
- if (ts.isImportDeclaration(node)) {
1305
- const moduleSpecifier = node.moduleSpecifier;
1306
- if (ts.isStringLiteral(moduleSpecifier)) {
1307
- const importPath = moduleSpecifier.text;
1308
- // Check if this import contains ViewportService from any package
1309
- // (e.g., '@ethlete/core' or '@fifa-gg/uikit/core')
1310
- if (!node.importClause?.namedBindings)
1311
- return;
1312
- if (ts.isNamedImports(node.importClause.namedBindings)) {
1313
- const imports = node.importClause.namedBindings.elements;
1314
- const viewportServiceImport = imports.find((imp) => ts.isImportSpecifier(imp) && imp.name.text === 'ViewportService');
1315
- if (!viewportServiceImport)
1316
- return;
1317
- // If ViewportService is the only import, remove the entire import statement
1318
- if (imports.length === 1) {
1319
- const importText = node.getText(sourceFile);
1320
- const importStart = node.getStart(sourceFile);
1321
- const importEnd = node.getEnd();
1322
- // Find the end of the line (including newline)
1323
- let lineEnd = importEnd;
1324
- while (lineEnd < content.length && content[lineEnd] !== '\n') {
1325
- lineEnd++;
1326
- }
1327
- if (content[lineEnd] === '\n') {
1328
- lineEnd++;
1329
- }
1330
- updatedContent = content.slice(0, importStart) + content.slice(lineEnd);
1331
- }
1332
- else {
1333
- // Remove only ViewportService from the named imports
1334
- const viewportServiceText = viewportServiceImport.getText(sourceFile);
1335
- const namedImportsText = node.importClause.namedBindings.getText(sourceFile);
1336
- // Handle different formatting cases
1337
- const patterns = [
1338
- new RegExp(`ViewportService,\\s*`, 'g'), // ViewportService at start/middle
1339
- new RegExp(`,\\s*ViewportService`, 'g'), // ViewportService at end
1340
- new RegExp(`\\s*ViewportService\\s*`, 'g'), // ViewportService alone
1341
- ];
1342
- let newNamedImports = namedImportsText;
1343
- for (const pattern of patterns) {
1344
- const temp = newNamedImports.replace(pattern, '');
1345
- if (temp !== newNamedImports) {
1346
- newNamedImports = temp;
1347
- break;
1348
- }
1349
- }
1350
- // Clean up any double commas or spaces
1351
- newNamedImports = newNamedImports.replace(/,\s*,/g, ',').replace(/{\s*,/g, '{').replace(/,\s*}/g, '}');
1352
- updatedContent = updatedContent.replace(namedImportsText, newNamedImports);
1353
- }
1354
- }
1355
- }
1356
- }
1357
- });
1358
- return updatedContent;
1359
- }
1360
- function findViewportServiceVariables(sourceFile) {
1106
+ function findViewportServiceVariables(sourceFile, serviceName) {
1361
1107
  const variables = [];
1362
1108
  function visit(node) {
1363
1109
  // Check for inject(ViewportService) pattern
@@ -1366,7 +1112,7 @@ function findViewportServiceVariables(sourceFile) {
1366
1112
  node.expression.text === 'inject' &&
1367
1113
  node.arguments.length > 0) {
1368
1114
  const arg = node.arguments[0];
1369
- if (ts.isIdentifier(arg) && arg.text === 'ViewportService') {
1115
+ if (ts.isIdentifier(arg) && arg.text === serviceName) {
1370
1116
  // Find the variable name
1371
1117
  let parent = node.parent;
1372
1118
  while (parent) {
@@ -1386,7 +1132,7 @@ function findViewportServiceVariables(sourceFile) {
1386
1132
  if (ts.isParameter(node)) {
1387
1133
  const typeNode = node.type;
1388
1134
  if (typeNode && ts.isTypeReferenceNode(typeNode) && ts.isIdentifier(typeNode.typeName)) {
1389
- if (typeNode.typeName.text === 'ViewportService' && node.name && ts.isIdentifier(node.name)) {
1135
+ if (typeNode.typeName.text === serviceName && node.name && ts.isIdentifier(node.name)) {
1390
1136
  variables.push(node.name.text);
1391
1137
  }
1392
1138
  }
@@ -1396,9 +1142,9 @@ function findViewportServiceVariables(sourceFile) {
1396
1142
  visit(sourceFile);
1397
1143
  return variables;
1398
1144
  }
1399
- function handleInlineInjectPatterns(sourceFile, content) {
1145
+ function handleInlineInjectPatterns(sourceFile, content, serviceName) {
1400
1146
  const importsNeeded = new Set();
1401
- let updatedContent = content;
1147
+ const edits = [];
1402
1148
  // Get the ViewportService package to determine mapping
1403
1149
  const packageName = getViewportServicePackage(sourceFile);
1404
1150
  const { signalPropertyMap, observablePropertyMap } = getPropertyMaps(packageName);
@@ -1432,12 +1178,11 @@ function handleInlineInjectPatterns(sourceFile, content) {
1432
1178
  arg.expression.expression.text === 'inject' &&
1433
1179
  arg.expression.arguments.length > 0) {
1434
1180
  const injectArg = arg.expression.arguments[0];
1435
- if (ts.isIdentifier(injectArg) && injectArg.text === 'ViewportService') {
1181
+ if (ts.isIdentifier(injectArg) && injectArg.text === serviceName) {
1436
1182
  const injectFn = signalPropertyMap[propertyName] || observablePropertyMap[propertyName];
1437
1183
  if (injectFn) {
1438
- const oldText = node.getText(sourceFile);
1439
1184
  const newText = `${injectFn}()`;
1440
- updatedContent = updatedContent.replace(oldText, newText);
1185
+ edits.push({ start: node.getStart(sourceFile), end: node.getEnd(), text: newText });
1441
1186
  importsNeeded.add(injectFn);
1442
1187
  processedNodes.add(arg);
1443
1188
  }
@@ -1454,13 +1199,12 @@ function handleInlineInjectPatterns(sourceFile, content) {
1454
1199
  arg.expression.expression.expression.text === 'inject' &&
1455
1200
  arg.expression.expression.arguments.length > 0) {
1456
1201
  const injectArg = arg.expression.expression.arguments[0];
1457
- if (ts.isIdentifier(injectArg) && injectArg.text === 'ViewportService') {
1458
- const oldText = node.getText(sourceFile);
1202
+ if (ts.isIdentifier(injectArg) && injectArg.text === serviceName) {
1459
1203
  // Extract arguments from the method call
1460
1204
  const args = arg.arguments.map((a) => a.getText(sourceFile)).join(', ');
1461
1205
  // Since it's wrapped in toSignal and our inject functions return signals, remove toSignal
1462
1206
  const newText = `${methodInfo.injectFn}(${args})`;
1463
- updatedContent = updatedContent.replace(oldText, newText);
1207
+ edits.push({ start: node.getStart(sourceFile), end: node.getEnd(), text: newText });
1464
1208
  importsNeeded.add(methodInfo.injectFn);
1465
1209
  processedNodes.add(arg);
1466
1210
  }
@@ -1481,12 +1225,11 @@ function handleInlineInjectPatterns(sourceFile, content) {
1481
1225
  node.expression.expression.expression.text === 'inject' &&
1482
1226
  node.expression.expression.arguments.length > 0) {
1483
1227
  const injectArg = node.expression.expression.arguments[0];
1484
- if (ts.isIdentifier(injectArg) && injectArg.text === 'ViewportService') {
1228
+ if (ts.isIdentifier(injectArg) && injectArg.text === serviceName) {
1485
1229
  if (isInsideToSignal(node)) {
1486
1230
  ts.forEachChild(node, visitNode);
1487
1231
  return;
1488
1232
  }
1489
- const oldText = node.getText(sourceFile);
1490
1233
  const args = node.arguments.map((arg) => arg.getText(sourceFile)).join(', ');
1491
1234
  // Only wrap with toObservable if the assigned variable/property ends with $
1492
1235
  let needsToObservable = false;
@@ -1500,7 +1243,7 @@ function handleInlineInjectPatterns(sourceFile, content) {
1500
1243
  newText = `toObservable(${newText})`;
1501
1244
  importsNeeded.add('toObservable');
1502
1245
  }
1503
- updatedContent = updatedContent.replace(oldText, newText);
1246
+ edits.push({ start: node.getStart(sourceFile), end: node.getEnd(), text: newText });
1504
1247
  importsNeeded.add(methodInfo.injectFn);
1505
1248
  }
1506
1249
  }
@@ -1517,14 +1260,13 @@ function handleInlineInjectPatterns(sourceFile, content) {
1517
1260
  node.expression.expression.text === 'inject' &&
1518
1261
  node.expression.arguments.length > 0) {
1519
1262
  const injectArg = node.expression.arguments[0];
1520
- if (ts.isIdentifier(injectArg) && injectArg.text === 'ViewportService') {
1263
+ if (ts.isIdentifier(injectArg) && injectArg.text === serviceName) {
1521
1264
  const injectFn = signalPropertyMap[propertyName] || observablePropertyMap[propertyName];
1522
1265
  if (injectFn) {
1523
1266
  if (isInsideToSignal(node)) {
1524
1267
  ts.forEachChild(node, visitNode);
1525
1268
  return;
1526
1269
  }
1527
- const oldText = node.getText(sourceFile);
1528
1270
  // Check if this is an observable property (ends with $)
1529
1271
  const isObservable = !!observablePropertyMap[propertyName];
1530
1272
  let newText = `${injectFn}()`;
@@ -1532,7 +1274,7 @@ function handleInlineInjectPatterns(sourceFile, content) {
1532
1274
  newText = `toObservable(${newText})`;
1533
1275
  importsNeeded.add('toObservable');
1534
1276
  }
1535
- updatedContent = updatedContent.replace(oldText, newText);
1277
+ edits.push({ start: node.getStart(sourceFile), end: node.getEnd(), text: newText });
1536
1278
  importsNeeded.add(injectFn);
1537
1279
  }
1538
1280
  }
@@ -1541,7 +1283,7 @@ function handleInlineInjectPatterns(sourceFile, content) {
1541
1283
  ts.forEachChild(node, visitNode);
1542
1284
  }
1543
1285
  sourceFile.forEachChild(visitNode);
1544
- return { content: updatedContent, importsNeeded };
1286
+ return { content: applyInlineEdits(content, edits), importsNeeded };
1545
1287
  }
1546
1288
  function removeUnusedImports(sourceFile, content) {
1547
1289
  const importsToCheck = [
@@ -1551,14 +1293,14 @@ function removeUnusedImports(sourceFile, content) {
1551
1293
  let updatedContent = content;
1552
1294
  for (const importToCheck of importsToCheck) {
1553
1295
  // Check if the import specifier is used anywhere in the code
1554
- const isUsed = checkIfImportIsUsed(sourceFile, content, importToCheck.specifier);
1296
+ const isUsed = checkIfImportIsUsed(sourceFile, importToCheck.specifier);
1555
1297
  if (!isUsed) {
1556
1298
  updatedContent = removeImportSpecifier(sourceFile, updatedContent, importToCheck.specifier, importToCheck.package);
1557
1299
  }
1558
1300
  }
1559
1301
  return updatedContent;
1560
1302
  }
1561
- function checkIfImportIsUsed(sourceFile, content, specifier) {
1303
+ function checkIfImportIsUsed(sourceFile, specifier) {
1562
1304
  let isUsed = false;
1563
1305
  function visit(node) {
1564
1306
  if (isUsed)
@@ -1623,7 +1365,7 @@ function removeImportSpecifier(sourceFile, content, specifier, packageName) {
1623
1365
  });
1624
1366
  return updatedContent;
1625
1367
  }
1626
- function checkIfPropertyIsUsedElsewhere(sourceFile, classNode, propertyName, propertyDecl) {
1368
+ function checkIfPropertyIsUsedElsewhere(classNode, propertyName, propertyDecl) {
1627
1369
  let isUsed = false;
1628
1370
  function visit(node) {
1629
1371
  if (isUsed)
@@ -1657,3 +1399,13 @@ function checkIfPropertyIsUsedElsewhere(sourceFile, classNode, propertyName, pro
1657
1399
  });
1658
1400
  return isUsed;
1659
1401
  }
1402
+ function applyInlineEdits(content, edits) {
1403
+ const accepted = [];
1404
+ for (const edit of [...edits].sort((a, b) => a.start - b.start || b.end - a.end)) {
1405
+ const last = accepted.at(-1);
1406
+ if (last && edit.start < last.end)
1407
+ continue;
1408
+ accepted.push(edit);
1409
+ }
1410
+ return accepted.reduceRight((result, edit) => result.slice(0, edit.start) + edit.text + result.slice(edit.end), content);
1411
+ }