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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +714 -0
  2. package/README.md +28 -4
  3. package/fesm2022/ethlete-core.mjs +8061 -3502
  4. package/fesm2022/ethlete-core.mjs.map +1 -1
  5. package/generators/devtools-about/generator.js +122 -0
  6. package/generators/devtools-about/schema.json +25 -0
  7. package/generators/generators.json +26 -5
  8. package/generators/migrate-provider-shape/migration-scope.js +35 -0
  9. package/generators/migrate-provider-shape/migration.js +50 -0
  10. package/generators/migrate-provider-shape/provider-shape.js +284 -0
  11. package/generators/migrate-provider-shape/schema.json +26 -0
  12. package/generators/migrate-surface-interaction-swatch/interaction-swatch.js +63 -0
  13. package/generators/migrate-surface-interaction-swatch/migration.js +36 -0
  14. package/generators/migrate-surface-interaction-swatch/schema.json +26 -0
  15. package/generators/migrate-to-v5/apply-replacements.js +14 -0
  16. package/generators/migrate-to-v5/color-naming.js +124 -0
  17. package/generators/migrate-to-v5/create-provider.js +67 -55
  18. package/generators/migrate-to-v5/migration-files.js +13 -0
  19. package/generators/migrate-to-v5/migration.js +33 -19
  20. package/generators/migrate-to-v5/removed-exports.js +73 -0
  21. package/generators/migrate-to-v5/router-state-service.js +123 -330
  22. package/generators/migrate-to-v5/schema.json +22 -0
  23. package/generators/migrate-to-v5/service-classes.js +108 -0
  24. package/generators/migrate-to-v5/viewport-service.js +224 -491
  25. package/generators/tailwind-4-color-theme/generator.js +594 -0
  26. package/generators/tailwind-4-color-theme/schema.json +44 -0
  27. package/generators/tailwind-4-surface-theme/generator.js +599 -0
  28. package/generators/tailwind-4-surface-theme/schema.json +48 -0
  29. package/migrations/seo-directive-removed.md +80 -0
  30. package/migrations/surface-theme-regenerate.md +20 -0
  31. package/migrations.json +43 -0
  32. package/package.json +25 -14
  33. package/types/ethlete-core.d.ts +3337 -1775
  34. package/generators/tailwind-4-theme/generator.js +0 -490
  35. package/generators/tailwind-4-theme/schema.json +0 -32
@@ -1,74 +1,40 @@
1
- /* eslint-disable @typescript-eslint/no-unused-vars */
2
- /* eslint-disable @typescript-eslint/no-non-null-assertion */
3
1
  import { logger } from '@nx/devkit';
4
2
  import * as ts from 'typescript';
5
- export default async function migrateViewportService(tree) {
6
- logger.log('\nšŸ”„ Migrating ViewportService to standalone utilities...\n');
7
- const tsFiles = [];
8
- const styleFiles = [];
9
- // Collect all TypeScript and style files
10
- function findFiles(dir) {
11
- const children = tree.children(dir);
12
- for (const child of children) {
13
- const path = dir === '.' ? child : `${dir}/${child}`;
14
- if (tree.isFile(path)) {
15
- if (path.endsWith('.ts') && !path.includes('node_modules') && !path.includes('.spec.ts')) {
16
- tsFiles.push(path);
17
- }
18
- else if (path.endsWith('.css') ||
19
- path.endsWith('.scss') ||
20
- path.endsWith('.sass') ||
21
- path.endsWith('.less')) {
22
- styleFiles.push(path);
23
- }
24
- }
25
- else {
26
- findFiles(path);
27
- }
28
- }
29
- }
30
- findFiles('.');
31
- // Detect which CSS variables are being used across the codebase
32
- 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 = [];
33
8
  let filesModified = 0;
34
9
  let templatesModified = 0;
35
- let viewportServiceUsed = false;
36
- // Track which properties became signals for template migration
37
- const componentTemplateMigrations = new Map();
38
- // Process each TypeScript file
10
+ const externalTemplateMigrations = new Map();
39
11
  for (const filePath of tsFiles) {
40
12
  const content = tree.read(filePath, 'utf-8');
41
13
  if (!content)
42
14
  continue;
43
- // Skip files that don't use ViewportService
44
15
  if (!content.includes('ViewportService'))
45
16
  continue;
46
- logger.log(`Processing: ${filePath}`);
47
17
  const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
48
- // Check if this is a component with a template
49
- const templatePath = findTemplateForComponent(tree, filePath, sourceFile);
50
- // Track signal properties for template migration
51
- const templateMigrationInfo = {
52
- signalProperties: new Set(),
53
- };
54
- // First handle inline inject patterns (e.g., toSignal(inject(ViewportService).isXs$))
55
- const inlineResult = handleInlineInjectPatterns(sourceFile, content);
18
+ const serviceImport = getServiceImport(sourceFile, 'ViewportService');
19
+ const viewportPackage = serviceImport?.packageName;
20
+ if (!serviceImport || (viewportPackage !== '@ethlete/core' && !viewportPackage?.startsWith('@fifa-gg/uikit'))) {
21
+ continue;
22
+ }
23
+ const serviceName = serviceImport.localName;
24
+ const inlineResult = handleInlineInjectPatterns(sourceFile, content, serviceName);
56
25
  let updatedContent = inlineResult.content;
57
- // Re-parse if content changed
58
26
  const updatedSourceFile = updatedContent !== content
59
27
  ? ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true)
60
28
  : sourceFile;
61
- // Find all ViewportService variables (injected or constructor params)
62
- const viewportServiceVars = findViewportServiceVariables(updatedSourceFile);
29
+ const viewportServiceVars = findViewportServiceVariables(updatedSourceFile, serviceName);
30
+ const serviceClasses = findServiceClasses(updatedSourceFile, serviceName);
63
31
  if (viewportServiceVars.length === 0 && inlineResult.importsNeeded.size === 0) {
64
32
  continue;
65
33
  }
66
- viewportServiceUsed = true;
67
34
  const allImportsNeeded = {
68
35
  '@ethlete/core': new Set(),
69
36
  '@angular/core/rxjs-interop': new Set(),
70
37
  };
71
- // Merge imports from inline patterns
72
38
  for (const importName of inlineResult.importsNeeded) {
73
39
  if (importName === 'toObservable' || importName === 'toSignal') {
74
40
  allImportsNeeded['@angular/core/rxjs-interop'].add(importName);
@@ -77,270 +43,173 @@ export default async function migrateViewportService(tree) {
77
43
  allImportsNeeded['@ethlete/core'].add(importName);
78
44
  }
79
45
  }
80
- // Process each ViewportService variable found in the file
81
- for (const viewportServiceVar of viewportServiceVars) {
82
- const classNode = findClassForViewportService(updatedSourceFile, viewportServiceVar);
83
- if (!classNode)
84
- continue;
85
- // Analyze what needs to be migrated BEFORE making any changes
86
- const context = analyzeClassMigration(updatedSourceFile, classNode, viewportServiceVar);
87
- // Track imports needed from direct replacements
88
- context.importsNeeded.forEach((imp) => allImportsNeeded['@ethlete/core'].add(imp));
89
- // Track imports needed from new members
90
- context.membersToAdd.forEach((member) => {
91
- allImportsNeeded['@ethlete/core'].add(member.injectFn);
92
- if (member.type === 'observable' && !member.wrappedInToSignal) {
93
- allImportsNeeded['@angular/core/rxjs-interop'].add('toObservable');
94
- }
95
- });
96
- // Check if any replacements use toObservable or toSignal
97
- for (const [original, replacement] of context.replacements) {
98
- if (replacement.includes('toObservable(')) {
99
- allImportsNeeded['@angular/core/rxjs-interop'].add('toObservable');
100
- }
101
- if (replacement.includes('toSignal(')) {
102
- 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
+ }
103
65
  }
104
- }
105
- // Apply replacements FIRST (before adding members)
106
- for (const [original, replacement] of context.replacements) {
107
- const regex = new RegExp(escapeRegExp(original), 'g');
108
- updatedContent = updatedContent.replace(regex, replacement);
109
- }
110
- // Then add new members to the updated content
111
- if (context.membersToAdd.length > 0) {
112
- const sourceFileUpdated = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
113
- const classNodeUpdated = findClassForViewportService(sourceFileUpdated, viewportServiceVar);
114
- if (classNodeUpdated) {
115
- 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);
116
70
  }
117
71
  }
118
72
  }
119
- // Handle monitorViewport migrations
120
73
  const sourceFileAfterReplacements = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
121
- const monitorViewportResult = migrateMonitorViewport(sourceFileAfterReplacements, updatedContent, viewportServiceVars, cssVariablesUsed);
74
+ const monitorViewportResult = migrateMonitorViewport(sourceFileAfterReplacements, updatedContent, viewportServiceVars);
122
75
  updatedContent = monitorViewportResult.content;
123
76
  monitorViewportResult.imports.forEach((imp) => allImportsNeeded['@ethlete/core'].add(imp));
124
- // Add necessary imports
125
77
  for (const [packageName, importsSet] of Object.entries(allImportsNeeded)) {
126
78
  if (importsSet.size > 0) {
127
79
  const sourceFileUpdated = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
128
80
  updatedContent = addImportsToPackage(sourceFileUpdated, updatedContent, importsSet, packageName);
129
81
  }
130
82
  }
131
- // Remove ViewportService injection (properties and constructor params)
132
83
  const sourceFileFinal = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
133
- updatedContent = removeViewportServiceInjection(sourceFileFinal, updatedContent, viewportServiceVars, filePath);
134
- // Remove ViewportService import if no longer needed
84
+ updatedContent = removeViewportServiceInjection(sourceFileFinal, updatedContent, serviceName, filePath);
135
85
  const sourceFileAfterRemoval = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
136
- if (!checkIfViewportServiceStillUsed(sourceFileAfterRemoval, viewportServiceVars)) {
137
- 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');
138
91
  }
139
- // Clean up unused imports (toSignal, toObservable)
140
92
  const sourceFileAfterCleanup = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
141
93
  updatedContent = removeUnusedImports(sourceFileAfterCleanup, updatedContent);
142
- // Always collect signalProperties for this file
143
- const updatedSourceFileForSignals = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
144
- updatedSourceFileForSignals.forEachChild((node) => {
145
- if (ts.isClassDeclaration(node)) {
146
- node.members.forEach((member) => {
147
- if (ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name)) {
148
- const memberName = member.name.text;
149
- const isPublicOrProtected = !member.modifiers?.some((mod) => mod.kind === ts.SyntaxKind.PrivateKeyword);
150
- if (isPublicOrProtected && member.initializer) {
151
- const initText = member.initializer.getText(updatedSourceFileForSignals);
152
- // Check if this property uses any of our inject functions (which return signals)
153
- const usesSignalInject = initText.includes('injectIsXs()') ||
154
- initText.includes('injectIsSm()') ||
155
- initText.includes('injectIsMd()') ||
156
- initText.includes('injectIsLg()') ||
157
- initText.includes('injectIsXl()') ||
158
- initText.includes('injectIs2Xl()') ||
159
- initText.includes('injectIsBase()') ||
160
- initText.includes('injectCurrentBreakpoint()') ||
161
- initText.includes('injectViewportDimensions()') ||
162
- initText.includes('injectScrollbarDimensions()') ||
163
- initText.includes('injectObserveBreakpoint()') ||
164
- initText.includes('injectBreakpointIsMatched()');
165
- if (usesSignalInject && !initText.includes('toObservable(')) {
166
- templateMigrationInfo.signalProperties.add(memberName);
167
- }
168
- }
169
- }
170
- });
171
- }
172
- });
173
- // Only add to componentTemplateMigrations if there's an external template
174
- if (templatePath && templateMigrationInfo.signalProperties.size > 0) {
175
- componentTemplateMigrations.set(filePath, templateMigrationInfo);
176
- }
177
- const updatedSourceFileForInline = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
178
- updatedSourceFileForInline.forEachChild((node) => {
179
- if (ts.isClassDeclaration(node)) {
180
- const decorators =
181
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
182
- ts.getDecorators?.(node) ?? node.decorators;
183
- decorators?.forEach((decorator) => {
184
- if (ts.isCallExpression(decorator.expression) &&
185
- decorator.expression.arguments.length > 0 &&
186
- ts.isObjectLiteralExpression(decorator.expression.arguments[0])) {
187
- const obj = decorator.expression.arguments[0];
188
- obj.properties.forEach((prop) => {
189
- if (ts.isPropertyAssignment(prop) &&
190
- ts.isIdentifier(prop.name) &&
191
- prop.name.text === 'template' &&
192
- (ts.isNoSubstitutionTemplateLiteral(prop.initializer) || ts.isStringLiteral(prop.initializer))) {
193
- let templateText = prop.initializer.getText();
194
- // Remove the surrounding quotes/backticks
195
- templateText = templateText.slice(1, -1);
196
- // For each signal property, replace usages with ()
197
- for (const propName of templateMigrationInfo.signalProperties) {
198
- const regex = new RegExp(`\\b${propName}(?!\\s*\\()\\b`, 'g');
199
- templateText = templateText.replace(regex, `${propName}()`);
200
- }
201
- // Re-wrap with original quotes/backticks
202
- const quote = prop.initializer.getText()[0];
203
- const newInitializer = quote + templateText + quote;
204
- // Replace in the file content
205
- updatedContent =
206
- updatedContent.slice(0, prop.initializer.getStart(updatedSourceFileForInline)) +
207
- newInitializer +
208
- updatedContent.slice(prop.initializer.getEnd());
209
- }
210
- });
211
- }
212
- });
94
+ const sourceFileForTemplates = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
95
+ const originalClasses = collectClasses(sourceFile);
96
+ const inlineTemplateEdits = [];
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;
213
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
+ });
214
121
  });
215
- // Write the updated content if changes were made
122
+ inlineTemplateEdits.sort((a, b) => b.start - a.start);
123
+ for (const edit of inlineTemplateEdits) {
124
+ updatedContent = updatedContent.slice(0, edit.start) + edit.replacement + updatedContent.slice(edit.end);
125
+ }
216
126
  if (updatedContent !== content) {
217
127
  tree.write(filePath, updatedContent);
218
128
  filesModified++;
219
129
  }
220
130
  }
221
- // Migrate templates
222
- for (const [tsFilePath, migrationInfo] of componentTemplateMigrations) {
223
- const sourceFile = ts.createSourceFile(tsFilePath, tree.read(tsFilePath, 'utf-8'), ts.ScriptTarget.Latest, true);
224
- const templatePath = findTemplateForComponent(tree, tsFilePath, sourceFile);
225
- if (templatePath && tree.exists(templatePath)) {
226
- logger.log(`Processing template: ${templatePath}`);
227
- const wasModified = migrateTemplateFile(tree, templatePath, migrationInfo);
228
- if (wasModified) {
229
- templatesModified++;
230
- }
131
+ for (const [templatePath, signalProperties] of externalTemplateMigrations) {
132
+ if (tree.exists(templatePath) && migrateTemplateFile(tree, templatePath, { signalProperties })) {
133
+ templatesModified++;
231
134
  }
232
135
  }
233
- // Log results
234
- if (filesModified > 0 || templatesModified > 0) {
235
- logger.log(`\nāœ… Successfully migrated ViewportService in ${filesModified} TypeScript file(s) and ${templatesModified} template(s)\n`);
236
- }
237
- else if (viewportServiceUsed) {
238
- logger.log('\nā„¹ļø ViewportService detected but no migrations needed\n');
239
- }
240
- else {
241
- logger.log('\nā„¹ļø No ViewportService usage found\n');
242
- }
136
+ return { filesChanged: filesModified + templatesModified, review };
243
137
  }
244
- function detectCssVariableUsage(tree, styleFiles) {
245
- let hasViewportVariables = false;
246
- let hasScrollbarVariables = false;
247
- // Check both style files AND TypeScript files (for inline styles)
248
- const allFiles = [...styleFiles];
249
- // Also check all TypeScript files for inline styles
250
- function findTsFiles(dir) {
251
- const children = tree.children(dir);
252
- for (const child of children) {
253
- const path = dir === '.' ? child : `${dir}/${child}`;
254
- if (tree.isFile(path) && path.endsWith('.ts')) {
255
- allFiles.push(path);
256
- }
257
- else if (!tree.isFile(path)) {
258
- findTsFiles(path);
259
- }
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);
260
162
  }
261
163
  }
262
- findTsFiles('.');
263
- for (const filePath of allFiles) {
264
- const content = tree.read(filePath, 'utf-8');
265
- 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))
266
170
  continue;
267
- if (content.includes('--et-vw') || content.includes('--et-vh')) {
268
- hasViewportVariables = true;
269
- }
270
- if (content.includes('--et-sw') || content.includes('--et-sh')) {
271
- hasScrollbarVariables = true;
272
- }
273
- if (hasViewportVariables && hasScrollbarVariables) {
274
- 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 };
275
180
  }
276
181
  }
277
- return { hasViewportVariables, hasScrollbarVariables };
182
+ return null;
278
183
  }
184
+ const migrateTemplateExpression = (expression, signalProperties) => {
185
+ let migrated = expression;
186
+ for (const propName of signalProperties) {
187
+ const regex = new RegExp(`\\b${escapeRegExp(propName)}(?!\\s*\\()\\b`, 'g');
188
+ migrated = migrated.replace(regex, `${propName}()`);
189
+ }
190
+ return migrated;
191
+ };
192
+ const migrateTemplateText = (template, signalProperties) => template
193
+ .replace(/{{([\s\S]*?)}}/g, (_, expression) => `{{${migrateTemplateExpression(expression, signalProperties)}}}`)
194
+ .replace(/((?:\[[^\]]+\]|\([^)]+\)|\*[\w.-]+)\s*=\s*)(["'])([\s\S]*?)\2/g, (_, prefix, quote, expression) => `${prefix}${quote}${migrateTemplateExpression(expression, signalProperties)}${quote}`)
195
+ .replace(/@(if|for|switch|case|defer)\s*\(([^\n{}]*)\)/g, (_, block, expression) => `@${block} (${migrateTemplateExpression(expression, signalProperties)})`);
279
196
  function migrateTemplateFile(tree, htmlFilePath, migrationInfo) {
280
197
  const content = tree.read(htmlFilePath, 'utf-8');
281
198
  if (!content)
282
199
  return false;
283
- let updatedContent = content;
284
- let hasChanges = false;
285
- // For each signal property, replace usages with () calls
286
- for (const propName of migrationInfo.signalProperties) {
287
- // Match property usage in various Angular contexts:
288
- // 1. Interpolation: {{ propName }}
289
- // 2. Property binding: [prop]="propName"
290
- // 3. Event binding: (event)="propName === value"
291
- // 4. Structural directives: *ngIf="propName"
292
- // But NOT if already called: propName()
293
- // Create a regex that matches the property but not if followed by (
294
- const regex = new RegExp(`\\b${escapeRegExp(propName)}(?!\\s*\\()\\b`, 'g');
295
- const newContent = updatedContent.replace(regex, `${propName}()`);
296
- if (newContent !== updatedContent) {
297
- updatedContent = newContent;
298
- hasChanges = true;
299
- }
300
- }
200
+ const updatedContent = migrateTemplateText(content, migrationInfo.signalProperties);
201
+ const hasChanges = updatedContent !== content;
301
202
  if (hasChanges) {
302
203
  tree.write(htmlFilePath, updatedContent);
303
204
  }
304
205
  return hasChanges;
305
206
  }
306
- function findTemplateForComponent(tree, tsFilePath, sourceFile) {
307
- let templatePath = null;
308
- function visit(node) {
309
- if (templatePath)
310
- return;
311
- // Look for @Component decorator
312
- if (ts.isDecorator(node)) {
313
- const expression = node.expression;
314
- if (ts.isCallExpression(expression)) {
315
- const args = expression.arguments;
316
- if (args.length > 0 && ts.isObjectLiteralExpression(args[0])) {
317
- const properties = args[0].properties;
318
- for (const prop of properties) {
319
- if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === 'templateUrl') {
320
- if (ts.isStringLiteral(prop.initializer)) {
321
- const templateRelativePath = prop.initializer.text;
322
- // Resolve the template path relative to the component file
323
- const componentDir = tsFilePath.substring(0, tsFilePath.lastIndexOf('/'));
324
- templatePath = `${componentDir}/${templateRelativePath}`;
325
- // Normalize the path
326
- templatePath = templatePath.replace(/\/\.\//g, '/').replace(/\/\//g, '/');
327
- }
328
- }
329
- }
330
- }
331
- }
332
- }
333
- ts.forEachChild(node, visit);
334
- }
335
- visit(sourceFile);
336
- return templatePath;
337
- }
338
207
  function getViewportServicePackage(sourceFile) {
339
208
  let packageName = null;
340
209
  sourceFile.forEachChild((node) => {
341
210
  if (ts.isImportDeclaration(node) && node.importClause?.namedBindings) {
342
211
  if (ts.isNamedImports(node.importClause.namedBindings)) {
343
- const hasViewportService = node.importClause.namedBindings.elements.some((el) => el.name.text === 'ViewportService');
212
+ const hasViewportService = node.importClause.namedBindings.elements.some((el) => (el.propertyName?.text ?? el.name.text) === 'ViewportService');
344
213
  if (hasViewportService && ts.isStringLiteral(node.moduleSpecifier)) {
345
214
  packageName = node.moduleSpecifier.text;
346
215
  }
@@ -359,7 +228,7 @@ function getPropertyMaps(packageName) {
359
228
  isLg: isEthleteCore ? 'injectIsLg' : 'injectIsXl',
360
229
  isXl: isEthleteCore ? 'injectIsXl' : 'injectIs2Xl',
361
230
  is2Xl: 'injectIs2Xl',
362
- isBase: isEthleteCore ? 'injectIsBase' : 'injectIsLg',
231
+ isBase: 'injectIsLg',
363
232
  viewportSize: 'injectViewportDimensions',
364
233
  scrollbarSize: 'injectScrollbarDimensions',
365
234
  currentViewport: 'injectCurrentBreakpoint',
@@ -500,7 +369,7 @@ function analyzeClassMigration(sourceFile, classNode, viewportServiceVar) {
500
369
  });
501
370
  // Group property initializer usages by their ViewportService usage pattern
502
371
  const propertyInitializerUsages = new Map();
503
- for (const [usageText, { propertyName, usage, propertyDecl }] of usagesInPropertyInitializers) {
372
+ for (const [, { propertyName, usage, propertyDecl }] of usagesInPropertyInitializers) {
504
373
  let injectFn;
505
374
  let args;
506
375
  let type = 'signal';
@@ -568,7 +437,7 @@ function analyzeClassMigration(sourceFile, classNode, viewportServiceVar) {
568
437
  });
569
438
  if (isUsedOutsideInitializers && type === 'observable') {
570
439
  // Create a shared member for all properties that use this observable
571
- const baseName = args ? generateNameFromArgs(usageKey, args, type) : usageKey;
440
+ const baseName = args ? generateNameFromArgs(args, type) : usageKey;
572
441
  const memberName = findAvailableMemberName(baseName, context.existingMembers);
573
442
  const memberInfo = {
574
443
  name: memberName,
@@ -599,7 +468,7 @@ function analyzeClassMigration(sourceFile, classNode, viewportServiceVar) {
599
468
  // OR if the property is used elsewhere in the class
600
469
  const propertyName = ts.isIdentifier(propertyDecl.name) ? propertyDecl.name.text : '';
601
470
  const isObservableProperty = propertyName.endsWith('$');
602
- const isPropertyUsedElsewhere = checkIfPropertyIsUsedElsewhere(sourceFile, classNode, propertyName, propertyDecl);
471
+ const isPropertyUsedElsewhere = checkIfPropertyIsUsedElsewhere(classNode, propertyName, propertyDecl);
603
472
  // Wrap with toObservable if:
604
473
  // 1. It's an observable type AND
605
474
  // 2. (Property name ends with $ OR property is used elsewhere)
@@ -718,7 +587,7 @@ function analyzeClassMigration(sourceFile, classNode, viewportServiceVar) {
718
587
  else {
719
588
  let baseName;
720
589
  if (usage.args) {
721
- baseName = generateNameFromArgs(originalProperty, usage.args, usage.type);
590
+ baseName = generateNameFromArgs(usage.args, usage.type);
722
591
  }
723
592
  else {
724
593
  baseName = usage.type === 'observable' ? originalProperty : originalProperty.replace(/\$$/, '');
@@ -843,7 +712,7 @@ function addMembersToClass(sourceFile, content, classNode, membersToAdd, viewpor
843
712
  const classText = classNode.getText(sourceFile);
844
713
  const openBraceIndex = classText.indexOf('{');
845
714
  if (openBraceIndex === -1) {
846
- console.warn('Could not find class body opening brace');
715
+ logger.warn('Could not find class body opening brace');
847
716
  return content;
848
717
  }
849
718
  const classStart = classNode.getStart(sourceFile);
@@ -889,70 +758,57 @@ function addMembersToClass(sourceFile, content, classNode, membersToAdd, viewpor
889
758
  }
890
759
  return content.slice(0, finalInsertPosition) + prefix + newMembers + suffix + content.slice(finalInsertPosition);
891
760
  }
892
- function removeViewportServiceInjection(sourceFile, content, viewportServiceVars, filePath) {
761
+ function removeViewportServiceInjection(sourceFile, content, serviceName, filePath) {
893
762
  let updatedContent = content;
894
763
  const modifications = [];
895
- // Collect all modifications first (in reverse order by position)
896
- sourceFile.forEachChild((node) => {
897
- if (ts.isClassDeclaration(node)) {
898
- // Collect property removals
899
- node.members.forEach((member) => {
900
- if (ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name)) {
901
- const memberName = member.name.text;
902
- if (viewportServiceVars.includes(memberName)) {
903
- const memberStart = member.getStart(sourceFile, true);
904
- const memberEnd = member.getEnd();
905
- // Find the line boundaries
906
- let lineStart = memberStart;
907
- while (lineStart > 0 && content[lineStart - 1] !== '\n') {
908
- lineStart--;
909
- }
910
- let lineEnd = memberEnd;
911
- while (lineEnd < content.length && content[lineEnd] !== '\n') {
912
- lineEnd++;
913
- }
914
- if (content[lineEnd] === '\n') {
915
- lineEnd++;
916
- }
917
- 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--;
918
774
  }
919
- }
920
- });
921
- // Collect constructor parameter modifications
922
- node.members.forEach((member) => {
923
- if (ts.isConstructorDeclaration(member) && member.parameters.length > 0) {
924
- const newParams = [];
925
- member.parameters.forEach((param) => {
926
- if (ts.isIdentifier(param.name)) {
927
- const paramName = param.name.text;
928
- // Skip ViewportService parameters
929
- if (viewportServiceVars.includes(paramName)) {
930
- return;
931
- }
932
- }
933
- // Keep this parameter
934
- newParams.push(param.getText(sourceFile));
935
- });
936
- // Find parameter list boundaries
937
- const constructorText = member.getText(sourceFile);
938
- const openParenIndex = constructorText.indexOf('(');
939
- const closeParenIndex = findMatchingParen(constructorText, openParenIndex);
940
- if (openParenIndex === -1 || closeParenIndex === -1) {
941
- console.warn(`Could not find constructor parameters in ${filePath}`);
942
- return;
775
+ let lineEnd = memberEnd;
776
+ while (lineEnd < content.length && content[lineEnd] !== '\n') {
777
+ lineEnd++;
943
778
  }
944
- const constructorStart = member.getStart(sourceFile);
945
- const paramListStart = constructorStart + openParenIndex + 1;
946
- const paramListEnd = constructorStart + closeParenIndex;
947
- const newParamsText = newParams.join(', ');
948
- modifications.push({
949
- start: paramListStart,
950
- end: paramListEnd,
951
- replacement: newParamsText,
952
- });
779
+ if (content[lineEnd] === '\n') {
780
+ lineEnd++;
781
+ }
782
+ modifications.push({ start: lineStart, end: lineEnd, replacement: '' });
953
783
  }
954
- });
955
- }
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
+ });
956
812
  });
957
813
  // Sort modifications by start position (descending) to apply from end to start
958
814
  modifications.sort((a, b) => b.start - a.start);
@@ -976,7 +832,7 @@ function findMatchingParen(text, openIndex) {
976
832
  }
977
833
  return depth === 0 ? i - 1 : -1;
978
834
  }
979
- function generateNameFromArgs(methodName, args, type) {
835
+ function generateNameFromArgs(args, type) {
980
836
  // Parse the arguments to generate a meaningful name
981
837
  // e.g., { min: 'lg' } => isMinLg or isMinLg$
982
838
  // e.g., { max: 'sm' } => isMaxSm or isMaxSm$
@@ -1061,39 +917,10 @@ function createReplacementsForMember(sourceFile, classNode, viewportServiceVar,
1061
917
  visitNode(member);
1062
918
  });
1063
919
  }
1064
- function findClassForViewportService(sourceFile, viewportServiceVar) {
1065
- let classNode;
1066
- function visit(node) {
1067
- if (classNode)
1068
- return;
1069
- if (ts.isClassDeclaration(node)) {
1070
- // Check if this class has the viewportService member
1071
- const hasMember = node.members.some((member) => {
1072
- if (ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name)) {
1073
- return member.name.text === viewportServiceVar;
1074
- }
1075
- if (ts.isParameter(member) && ts.isIdentifier(member.name)) {
1076
- return member.name.text === viewportServiceVar;
1077
- }
1078
- // Check constructor parameters
1079
- if (ts.isConstructorDeclaration(member)) {
1080
- return member.parameters.some((param) => ts.isIdentifier(param.name) && param.name.text === viewportServiceVar);
1081
- }
1082
- return false;
1083
- });
1084
- if (hasMember) {
1085
- classNode = node;
1086
- }
1087
- }
1088
- ts.forEachChild(node, visit);
1089
- }
1090
- visit(sourceFile);
1091
- return classNode;
1092
- }
1093
920
  function findAvailableMemberName(baseName, existingMembers) {
1094
921
  // baseName already has $ suffix for observables at this point
1095
922
  // Try variations: name, _name, #name
1096
- const candidates = [baseName, `_${baseName}`, `#${baseName}`];
923
+ const candidates = [baseName, `_${baseName}`];
1097
924
  for (const candidate of candidates) {
1098
925
  if (!existingMembers.has(candidate)) {
1099
926
  return candidate;
@@ -1106,7 +933,7 @@ function findAvailableMemberName(baseName, existingMembers) {
1106
933
  }
1107
934
  return `${baseName}${counter}`;
1108
935
  }
1109
- function migrateMonitorViewport(sourceFile, content, viewportServiceVars, cssVariablesUsed) {
936
+ function migrateMonitorViewport(sourceFile, content, viewportServiceVars) {
1110
937
  let updatedContent = content;
1111
938
  const imports = [];
1112
939
  const monitorViewportCalls = [];
@@ -1161,29 +988,8 @@ function migrateMonitorViewport(sourceFile, content, viewportServiceVars, cssVar
1161
988
  return { content, imports };
1162
989
  }
1163
990
  // Determine what to add to constructor
1164
- const constructorStatements = [];
1165
- if (cssVariablesUsed.hasViewportVariables) {
1166
- constructorStatements.push('writeViewportSizeToCssVariables();');
1167
- imports.push('writeViewportSizeToCssVariables');
1168
- }
1169
- if (cssVariablesUsed.hasScrollbarVariables) {
1170
- constructorStatements.push('writeScrollbarSizeToCssVariables();');
1171
- imports.push('writeScrollbarSizeToCssVariables');
1172
- }
1173
- if (constructorStatements.length === 0) {
1174
- // Just remove monitorViewport calls
1175
- const modifications = monitorViewportCalls.map((call) => ({
1176
- start: call.start,
1177
- end: call.end,
1178
- replacement: '',
1179
- }));
1180
- // Apply modifications from end to start
1181
- modifications.sort((a, b) => b.start - a.start);
1182
- for (const mod of modifications) {
1183
- updatedContent = updatedContent.slice(0, mod.start) + mod.replacement + updatedContent.slice(mod.end);
1184
- }
1185
- return { content: updatedContent, imports };
1186
- }
991
+ const constructorStatements = ['writeViewportSizeToCssVariables();', 'writeScrollbarSizeToCssVariables();'];
992
+ imports.push('writeViewportSizeToCssVariables', 'writeScrollbarSizeToCssVariables');
1187
993
  // Group calls by class
1188
994
  const callsByClass = new Map();
1189
995
  for (const call of monitorViewportCalls) {
@@ -1232,7 +1038,7 @@ function migrateMonitorViewport(sourceFile, content, viewportServiceVars, cssVar
1232
1038
  const classText = classNode.getText(sourceFile);
1233
1039
  const openBraceIndex = classText.indexOf('{');
1234
1040
  if (openBraceIndex === -1) {
1235
- console.warn('Could not find class body opening brace');
1041
+ logger.warn('Could not find class body opening brace');
1236
1042
  continue;
1237
1043
  }
1238
1044
  const insertPosition = classStart + openBraceIndex + 1;
@@ -1278,13 +1084,15 @@ function addImportsToPackage(sourceFile, content, neededImports, packageName) {
1278
1084
  statement.moduleSpecifier.text === packageName);
1279
1085
  const importsToAdd = Array.from(neededImports).sort();
1280
1086
  if (existingImport?.importClause?.namedBindings && ts.isNamedImports(existingImport.importClause.namedBindings)) {
1281
- const existingImportNames = existingImport.importClause.namedBindings.elements.map((el) => el.name.text);
1282
- const newImports = importsToAdd.filter((imp) => !existingImportNames.includes(imp));
1087
+ const existingElements = existingImport.importClause.namedBindings.elements;
1088
+ const existingImportedNames = existingElements.map((el) => el.propertyName?.text ?? el.name.text);
1089
+ const existingImportNames = existingElements.map((el) => el.getText(sourceFile));
1090
+ const newImports = importsToAdd.filter((imp) => !existingImportedNames.includes(imp));
1283
1091
  if (newImports.length > 0) {
1284
1092
  const allImports = [...existingImportNames, ...newImports].sort();
1285
1093
  const importText = existingImport.getText(sourceFile);
1286
1094
  const newImportText = `import { ${allImports.join(', ')} } from '${packageName}';`;
1287
- return content.replace(importText, newImportText);
1095
+ return content.replace(importText, () => newImportText);
1288
1096
  }
1289
1097
  }
1290
1098
  else if (!existingImport) {
@@ -1295,87 +1103,7 @@ function addImportsToPackage(sourceFile, content, neededImports, packageName) {
1295
1103
  }
1296
1104
  return content;
1297
1105
  }
1298
- function checkIfViewportServiceStillUsed(sourceFile, viewportServiceVars) {
1299
- let stillUsed = false;
1300
- function visit(node) {
1301
- if (stillUsed || !ts.isPropertyAccessExpression(node)) {
1302
- if (!stillUsed)
1303
- ts.forEachChild(node, visit);
1304
- return;
1305
- }
1306
- const expression = node.expression;
1307
- const isUsed = (ts.isPropertyAccessExpression(expression) && viewportServiceVars.includes(expression.name.text)) ||
1308
- (ts.isIdentifier(expression) && viewportServiceVars.includes(expression.text));
1309
- if (isUsed) {
1310
- stillUsed = true;
1311
- }
1312
- else {
1313
- ts.forEachChild(node, visit);
1314
- }
1315
- }
1316
- visit(sourceFile);
1317
- return stillUsed;
1318
- }
1319
- function removeViewportServiceImport(sourceFile, content) {
1320
- let updatedContent = content;
1321
- sourceFile.forEachChild((node) => {
1322
- if (ts.isImportDeclaration(node)) {
1323
- const moduleSpecifier = node.moduleSpecifier;
1324
- if (ts.isStringLiteral(moduleSpecifier)) {
1325
- const importPath = moduleSpecifier.text;
1326
- // Check if this import contains ViewportService from any package
1327
- // (e.g., '@ethlete/core' or '@fifa-gg/uikit/core')
1328
- if (!node.importClause?.namedBindings)
1329
- return;
1330
- if (ts.isNamedImports(node.importClause.namedBindings)) {
1331
- const imports = node.importClause.namedBindings.elements;
1332
- const viewportServiceImport = imports.find((imp) => ts.isImportSpecifier(imp) && imp.name.text === 'ViewportService');
1333
- if (!viewportServiceImport)
1334
- return;
1335
- // If ViewportService is the only import, remove the entire import statement
1336
- if (imports.length === 1) {
1337
- const importText = node.getText(sourceFile);
1338
- const importStart = node.getStart(sourceFile);
1339
- const importEnd = node.getEnd();
1340
- // Find the end of the line (including newline)
1341
- let lineEnd = importEnd;
1342
- while (lineEnd < content.length && content[lineEnd] !== '\n') {
1343
- lineEnd++;
1344
- }
1345
- if (content[lineEnd] === '\n') {
1346
- lineEnd++;
1347
- }
1348
- updatedContent = content.slice(0, importStart) + content.slice(lineEnd);
1349
- }
1350
- else {
1351
- // Remove only ViewportService from the named imports
1352
- const viewportServiceText = viewportServiceImport.getText(sourceFile);
1353
- const namedImportsText = node.importClause.namedBindings.getText(sourceFile);
1354
- // Handle different formatting cases
1355
- const patterns = [
1356
- new RegExp(`ViewportService,\\s*`, 'g'), // ViewportService at start/middle
1357
- new RegExp(`,\\s*ViewportService`, 'g'), // ViewportService at end
1358
- new RegExp(`\\s*ViewportService\\s*`, 'g'), // ViewportService alone
1359
- ];
1360
- let newNamedImports = namedImportsText;
1361
- for (const pattern of patterns) {
1362
- const temp = newNamedImports.replace(pattern, '');
1363
- if (temp !== newNamedImports) {
1364
- newNamedImports = temp;
1365
- break;
1366
- }
1367
- }
1368
- // Clean up any double commas or spaces
1369
- newNamedImports = newNamedImports.replace(/,\s*,/g, ',').replace(/{\s*,/g, '{').replace(/,\s*}/g, '}');
1370
- updatedContent = updatedContent.replace(namedImportsText, newNamedImports);
1371
- }
1372
- }
1373
- }
1374
- }
1375
- });
1376
- return updatedContent;
1377
- }
1378
- function findViewportServiceVariables(sourceFile) {
1106
+ function findViewportServiceVariables(sourceFile, serviceName) {
1379
1107
  const variables = [];
1380
1108
  function visit(node) {
1381
1109
  // Check for inject(ViewportService) pattern
@@ -1384,7 +1112,7 @@ function findViewportServiceVariables(sourceFile) {
1384
1112
  node.expression.text === 'inject' &&
1385
1113
  node.arguments.length > 0) {
1386
1114
  const arg = node.arguments[0];
1387
- if (ts.isIdentifier(arg) && arg.text === 'ViewportService') {
1115
+ if (ts.isIdentifier(arg) && arg.text === serviceName) {
1388
1116
  // Find the variable name
1389
1117
  let parent = node.parent;
1390
1118
  while (parent) {
@@ -1404,7 +1132,7 @@ function findViewportServiceVariables(sourceFile) {
1404
1132
  if (ts.isParameter(node)) {
1405
1133
  const typeNode = node.type;
1406
1134
  if (typeNode && ts.isTypeReferenceNode(typeNode) && ts.isIdentifier(typeNode.typeName)) {
1407
- if (typeNode.typeName.text === 'ViewportService' && node.name && ts.isIdentifier(node.name)) {
1135
+ if (typeNode.typeName.text === serviceName && node.name && ts.isIdentifier(node.name)) {
1408
1136
  variables.push(node.name.text);
1409
1137
  }
1410
1138
  }
@@ -1414,9 +1142,9 @@ function findViewportServiceVariables(sourceFile) {
1414
1142
  visit(sourceFile);
1415
1143
  return variables;
1416
1144
  }
1417
- function handleInlineInjectPatterns(sourceFile, content) {
1145
+ function handleInlineInjectPatterns(sourceFile, content, serviceName) {
1418
1146
  const importsNeeded = new Set();
1419
- let updatedContent = content;
1147
+ const edits = [];
1420
1148
  // Get the ViewportService package to determine mapping
1421
1149
  const packageName = getViewportServicePackage(sourceFile);
1422
1150
  const { signalPropertyMap, observablePropertyMap } = getPropertyMaps(packageName);
@@ -1450,12 +1178,11 @@ function handleInlineInjectPatterns(sourceFile, content) {
1450
1178
  arg.expression.expression.text === 'inject' &&
1451
1179
  arg.expression.arguments.length > 0) {
1452
1180
  const injectArg = arg.expression.arguments[0];
1453
- if (ts.isIdentifier(injectArg) && injectArg.text === 'ViewportService') {
1181
+ if (ts.isIdentifier(injectArg) && injectArg.text === serviceName) {
1454
1182
  const injectFn = signalPropertyMap[propertyName] || observablePropertyMap[propertyName];
1455
1183
  if (injectFn) {
1456
- const oldText = node.getText(sourceFile);
1457
1184
  const newText = `${injectFn}()`;
1458
- updatedContent = updatedContent.replace(oldText, newText);
1185
+ edits.push({ start: node.getStart(sourceFile), end: node.getEnd(), text: newText });
1459
1186
  importsNeeded.add(injectFn);
1460
1187
  processedNodes.add(arg);
1461
1188
  }
@@ -1472,13 +1199,12 @@ function handleInlineInjectPatterns(sourceFile, content) {
1472
1199
  arg.expression.expression.expression.text === 'inject' &&
1473
1200
  arg.expression.expression.arguments.length > 0) {
1474
1201
  const injectArg = arg.expression.expression.arguments[0];
1475
- if (ts.isIdentifier(injectArg) && injectArg.text === 'ViewportService') {
1476
- const oldText = node.getText(sourceFile);
1202
+ if (ts.isIdentifier(injectArg) && injectArg.text === serviceName) {
1477
1203
  // Extract arguments from the method call
1478
1204
  const args = arg.arguments.map((a) => a.getText(sourceFile)).join(', ');
1479
1205
  // Since it's wrapped in toSignal and our inject functions return signals, remove toSignal
1480
1206
  const newText = `${methodInfo.injectFn}(${args})`;
1481
- updatedContent = updatedContent.replace(oldText, newText);
1207
+ edits.push({ start: node.getStart(sourceFile), end: node.getEnd(), text: newText });
1482
1208
  importsNeeded.add(methodInfo.injectFn);
1483
1209
  processedNodes.add(arg);
1484
1210
  }
@@ -1499,12 +1225,11 @@ function handleInlineInjectPatterns(sourceFile, content) {
1499
1225
  node.expression.expression.expression.text === 'inject' &&
1500
1226
  node.expression.expression.arguments.length > 0) {
1501
1227
  const injectArg = node.expression.expression.arguments[0];
1502
- if (ts.isIdentifier(injectArg) && injectArg.text === 'ViewportService') {
1228
+ if (ts.isIdentifier(injectArg) && injectArg.text === serviceName) {
1503
1229
  if (isInsideToSignal(node)) {
1504
1230
  ts.forEachChild(node, visitNode);
1505
1231
  return;
1506
1232
  }
1507
- const oldText = node.getText(sourceFile);
1508
1233
  const args = node.arguments.map((arg) => arg.getText(sourceFile)).join(', ');
1509
1234
  // Only wrap with toObservable if the assigned variable/property ends with $
1510
1235
  let needsToObservable = false;
@@ -1518,7 +1243,7 @@ function handleInlineInjectPatterns(sourceFile, content) {
1518
1243
  newText = `toObservable(${newText})`;
1519
1244
  importsNeeded.add('toObservable');
1520
1245
  }
1521
- updatedContent = updatedContent.replace(oldText, newText);
1246
+ edits.push({ start: node.getStart(sourceFile), end: node.getEnd(), text: newText });
1522
1247
  importsNeeded.add(methodInfo.injectFn);
1523
1248
  }
1524
1249
  }
@@ -1535,14 +1260,13 @@ function handleInlineInjectPatterns(sourceFile, content) {
1535
1260
  node.expression.expression.text === 'inject' &&
1536
1261
  node.expression.arguments.length > 0) {
1537
1262
  const injectArg = node.expression.arguments[0];
1538
- if (ts.isIdentifier(injectArg) && injectArg.text === 'ViewportService') {
1263
+ if (ts.isIdentifier(injectArg) && injectArg.text === serviceName) {
1539
1264
  const injectFn = signalPropertyMap[propertyName] || observablePropertyMap[propertyName];
1540
1265
  if (injectFn) {
1541
1266
  if (isInsideToSignal(node)) {
1542
1267
  ts.forEachChild(node, visitNode);
1543
1268
  return;
1544
1269
  }
1545
- const oldText = node.getText(sourceFile);
1546
1270
  // Check if this is an observable property (ends with $)
1547
1271
  const isObservable = !!observablePropertyMap[propertyName];
1548
1272
  let newText = `${injectFn}()`;
@@ -1550,7 +1274,7 @@ function handleInlineInjectPatterns(sourceFile, content) {
1550
1274
  newText = `toObservable(${newText})`;
1551
1275
  importsNeeded.add('toObservable');
1552
1276
  }
1553
- updatedContent = updatedContent.replace(oldText, newText);
1277
+ edits.push({ start: node.getStart(sourceFile), end: node.getEnd(), text: newText });
1554
1278
  importsNeeded.add(injectFn);
1555
1279
  }
1556
1280
  }
@@ -1559,7 +1283,7 @@ function handleInlineInjectPatterns(sourceFile, content) {
1559
1283
  ts.forEachChild(node, visitNode);
1560
1284
  }
1561
1285
  sourceFile.forEachChild(visitNode);
1562
- return { content: updatedContent, importsNeeded };
1286
+ return { content: applyInlineEdits(content, edits), importsNeeded };
1563
1287
  }
1564
1288
  function removeUnusedImports(sourceFile, content) {
1565
1289
  const importsToCheck = [
@@ -1569,14 +1293,14 @@ function removeUnusedImports(sourceFile, content) {
1569
1293
  let updatedContent = content;
1570
1294
  for (const importToCheck of importsToCheck) {
1571
1295
  // Check if the import specifier is used anywhere in the code
1572
- const isUsed = checkIfImportIsUsed(sourceFile, content, importToCheck.specifier);
1296
+ const isUsed = checkIfImportIsUsed(sourceFile, importToCheck.specifier);
1573
1297
  if (!isUsed) {
1574
1298
  updatedContent = removeImportSpecifier(sourceFile, updatedContent, importToCheck.specifier, importToCheck.package);
1575
1299
  }
1576
1300
  }
1577
1301
  return updatedContent;
1578
1302
  }
1579
- function checkIfImportIsUsed(sourceFile, content, specifier) {
1303
+ function checkIfImportIsUsed(sourceFile, specifier) {
1580
1304
  let isUsed = false;
1581
1305
  function visit(node) {
1582
1306
  if (isUsed)
@@ -1641,7 +1365,7 @@ function removeImportSpecifier(sourceFile, content, specifier, packageName) {
1641
1365
  });
1642
1366
  return updatedContent;
1643
1367
  }
1644
- function checkIfPropertyIsUsedElsewhere(sourceFile, classNode, propertyName, propertyDecl) {
1368
+ function checkIfPropertyIsUsedElsewhere(classNode, propertyName, propertyDecl) {
1645
1369
  let isUsed = false;
1646
1370
  function visit(node) {
1647
1371
  if (isUsed)
@@ -1675,4 +1399,13 @@ function checkIfPropertyIsUsedElsewhere(sourceFile, classNode, propertyName, pro
1675
1399
  });
1676
1400
  return isUsed;
1677
1401
  }
1678
- //# sourceMappingURL=viewport-service.js.map
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
+ }