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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +843 -0
  2. package/README.md +28 -4
  3. package/design/interactive-styles.ts +5 -0
  4. package/design/theme-css.js +392 -0
  5. package/design/theme-css.ts +547 -0
  6. package/fesm2022/ethlete-core.mjs +9338 -5667
  7. package/fesm2022/ethlete-core.mjs.map +1 -1
  8. package/generators/devtools-about/generator.js +122 -0
  9. package/generators/devtools-about/schema.json +25 -0
  10. package/generators/generators.json +41 -5
  11. package/generators/migrate-color-theme-variants/color-theme-variants.js +136 -0
  12. package/generators/migrate-color-theme-variants/migration.js +46 -0
  13. package/generators/migrate-color-theme-variants/schema.json +26 -0
  14. package/generators/migrate-create-destroy/create-destroy.js +317 -0
  15. package/generators/migrate-create-destroy/migration.js +46 -0
  16. package/generators/migrate-create-destroy/schema.json +26 -0
  17. package/generators/migrate-legacy-color-themes/legacy-color-themes.js +123 -0
  18. package/generators/migrate-legacy-color-themes/migration.js +93 -0
  19. package/generators/migrate-legacy-color-themes/schema.json +26 -0
  20. package/generators/migrate-provider-shape/migration-scope.js +35 -0
  21. package/generators/migrate-provider-shape/migration.js +50 -0
  22. package/generators/migrate-provider-shape/provider-shape.js +284 -0
  23. package/generators/migrate-provider-shape/schema.json +26 -0
  24. package/generators/migrate-surface-interaction-swatch/interaction-swatch.js +63 -0
  25. package/generators/migrate-surface-interaction-swatch/migration.js +36 -0
  26. package/generators/migrate-surface-interaction-swatch/schema.json +26 -0
  27. package/generators/migrate-to-v5/apply-replacements.js +14 -0
  28. package/generators/migrate-to-v5/color-naming.js +124 -0
  29. package/generators/migrate-to-v5/create-provider.js +67 -55
  30. package/generators/migrate-to-v5/migration-files.js +13 -0
  31. package/generators/migrate-to-v5/migration.js +33 -19
  32. package/generators/migrate-to-v5/removed-exports.js +161 -0
  33. package/generators/migrate-to-v5/router-state-service.js +125 -330
  34. package/generators/migrate-to-v5/schema.json +22 -0
  35. package/generators/migrate-to-v5/service-classes.js +108 -0
  36. package/generators/migrate-to-v5/sync-observable-consumers.js +80 -0
  37. package/generators/migrate-to-v5/viewport-service.js +226 -491
  38. package/generators/tailwind-4-color-theme/generator.js +411 -0
  39. package/generators/tailwind-4-color-theme/schema.json +44 -0
  40. package/generators/tailwind-4-surface-theme/generator.js +379 -0
  41. package/generators/tailwind-4-surface-theme/schema.json +48 -0
  42. package/migrations/legacy-color-themes.md +38 -0
  43. package/migrations/seo-directive-removed.md +81 -0
  44. package/migrations/surface-theme-regenerate.md +20 -0
  45. package/migrations.json +59 -0
  46. package/package.json +38 -21
  47. package/src/lib/animations/easing-tokens-styles.component.css +155 -0
  48. package/src/lib/providers/visually-hidden-styles.component.css +13 -0
  49. package/src/lib/resize-handles/resize-handles.component.css +137 -0
  50. package/src/lib/theming/color-interactive-styles.component.css +147 -0
  51. package/src/lib/theming/surface-interactive-styles.component.css +42 -0
  52. package/types/ethlete-core.d.ts +3842 -2205
  53. package/generators/tailwind-4-theme/generator.js +0 -514
  54. package/generators/tailwind-4-theme/schema.json +0 -32
@@ -1,74 +1,41 @@
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 { findSyncObservableConsumers } from './sync-observable-consumers.js';
5
+ import { applyReplacementsInClass, collectClasses, findServiceClasses, getServiceImport, isServiceParameter, isServiceProperty, isServiceStillUsed, removeServiceImport, } from './service-classes.js';
6
+ export default async function migrateViewportService(tree, scope) {
7
+ const tsFiles = collectFiles(tree, scope, ['.ts']);
8
+ const review = [];
33
9
  let filesModified = 0;
34
10
  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
11
+ const externalTemplateMigrations = new Map();
39
12
  for (const filePath of tsFiles) {
40
13
  const content = tree.read(filePath, 'utf-8');
41
14
  if (!content)
42
15
  continue;
43
- // Skip files that don't use ViewportService
44
16
  if (!content.includes('ViewportService'))
45
17
  continue;
46
- logger.log(`Processing: ${filePath}`);
47
18
  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);
19
+ const serviceImport = getServiceImport(sourceFile, 'ViewportService');
20
+ const viewportPackage = serviceImport?.packageName;
21
+ if (!serviceImport || (viewportPackage !== '@ethlete/core' && !viewportPackage?.startsWith('@fifa-gg/uikit'))) {
22
+ continue;
23
+ }
24
+ const serviceName = serviceImport.localName;
25
+ const inlineResult = handleInlineInjectPatterns(sourceFile, content, serviceName);
56
26
  let updatedContent = inlineResult.content;
57
- // Re-parse if content changed
58
27
  const updatedSourceFile = updatedContent !== content
59
28
  ? ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true)
60
29
  : sourceFile;
61
- // Find all ViewportService variables (injected or constructor params)
62
- const viewportServiceVars = findViewportServiceVariables(updatedSourceFile);
30
+ const viewportServiceVars = findViewportServiceVariables(updatedSourceFile, serviceName);
31
+ const serviceClasses = findServiceClasses(updatedSourceFile, serviceName);
63
32
  if (viewportServiceVars.length === 0 && inlineResult.importsNeeded.size === 0) {
64
33
  continue;
65
34
  }
66
- viewportServiceUsed = true;
67
35
  const allImportsNeeded = {
68
36
  '@ethlete/core': new Set(),
69
37
  '@angular/core/rxjs-interop': new Set(),
70
38
  };
71
- // Merge imports from inline patterns
72
39
  for (const importName of inlineResult.importsNeeded) {
73
40
  if (importName === 'toObservable' || importName === 'toSignal') {
74
41
  allImportsNeeded['@angular/core/rxjs-interop'].add(importName);
@@ -77,270 +44,174 @@ export default async function migrateViewportService(tree) {
77
44
  allImportsNeeded['@ethlete/core'].add(importName);
78
45
  }
79
46
  }
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');
47
+ for (const { index, fields } of serviceClasses) {
48
+ for (const viewportServiceVar of fields) {
49
+ const currentSourceFile = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
50
+ const classNode = collectClasses(currentSourceFile)[index];
51
+ const context = analyzeClassMigration(currentSourceFile, classNode, viewportServiceVar);
52
+ context.importsNeeded.forEach((imp) => allImportsNeeded['@ethlete/core'].add(imp));
53
+ context.membersToAdd.forEach((member) => {
54
+ allImportsNeeded['@ethlete/core'].add(member.injectFn);
55
+ if (member.type === 'observable' && !member.wrappedInToSignal) {
56
+ allImportsNeeded['@angular/core/rxjs-interop'].add('toObservable');
57
+ }
58
+ });
59
+ for (const replacement of context.replacements.values()) {
60
+ if (replacement.includes('toObservable(')) {
61
+ allImportsNeeded['@angular/core/rxjs-interop'].add('toObservable');
62
+ }
63
+ if (replacement.includes('toSignal(')) {
64
+ allImportsNeeded['@angular/core/rxjs-interop'].add('toSignal');
65
+ }
103
66
  }
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);
67
+ updatedContent = applyReplacementsInClass(updatedContent, currentSourceFile, classNode, context.replacements);
68
+ if (context.membersToAdd.length > 0) {
69
+ const sourceFileUpdated = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
70
+ updatedContent = addMembersToClass(sourceFileUpdated, updatedContent, collectClasses(sourceFileUpdated)[index], context.membersToAdd, viewportServiceVar);
116
71
  }
117
72
  }
118
73
  }
119
- // Handle monitorViewport migrations
120
74
  const sourceFileAfterReplacements = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
121
- const monitorViewportResult = migrateMonitorViewport(sourceFileAfterReplacements, updatedContent, viewportServiceVars, cssVariablesUsed);
75
+ const monitorViewportResult = migrateMonitorViewport(sourceFileAfterReplacements, updatedContent, viewportServiceVars);
122
76
  updatedContent = monitorViewportResult.content;
123
77
  monitorViewportResult.imports.forEach((imp) => allImportsNeeded['@ethlete/core'].add(imp));
124
- // Add necessary imports
125
78
  for (const [packageName, importsSet] of Object.entries(allImportsNeeded)) {
126
79
  if (importsSet.size > 0) {
127
80
  const sourceFileUpdated = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
128
81
  updatedContent = addImportsToPackage(sourceFileUpdated, updatedContent, importsSet, packageName);
129
82
  }
130
83
  }
131
- // Remove ViewportService injection (properties and constructor params)
132
84
  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
85
+ updatedContent = removeViewportServiceInjection(sourceFileFinal, updatedContent, serviceName, filePath);
135
86
  const sourceFileAfterRemoval = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
136
- if (!checkIfViewportServiceStillUsed(sourceFileAfterRemoval, viewportServiceVars)) {
137
- updatedContent = removeViewportServiceImport(sourceFileAfterRemoval, updatedContent);
87
+ if (isServiceStillUsed(sourceFileAfterRemoval, serviceName, serviceClasses)) {
88
+ review.push(`${filePath}: ViewportService is still used. Migrate the remaining usages manually.`);
89
+ }
90
+ else {
91
+ updatedContent = removeServiceImport(sourceFileAfterRemoval, updatedContent, 'ViewportService');
138
92
  }
139
- // Clean up unused imports (toSignal, toObservable)
140
93
  const sourceFileAfterCleanup = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
141
94
  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
- });
95
+ review.push(...findSyncObservableConsumers(filePath, content, updatedContent));
96
+ const sourceFileForTemplates = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
97
+ const originalClasses = collectClasses(sourceFile);
98
+ const inlineTemplateEdits = [];
99
+ collectClasses(sourceFileForTemplates).forEach((classNode, index) => {
100
+ if (originalClasses[index]?.getText(sourceFile) === classNode.getText(sourceFileForTemplates))
101
+ return;
102
+ const signalProperties = collectSignalProperties(sourceFileForTemplates, classNode);
103
+ if (signalProperties.size === 0)
104
+ return;
105
+ const template = findComponentTemplate(classNode);
106
+ if (!template)
107
+ return;
108
+ if (template.kind === 'url') {
109
+ const componentDir = filePath.substring(0, filePath.lastIndexOf('/'));
110
+ const templatePath = `${componentDir}/${template.path}`.replace(/\/\.\//g, '/').replace(/\/\//g, '/');
111
+ const existing = externalTemplateMigrations.get(templatePath) ?? new Set();
112
+ signalProperties.forEach((property) => existing.add(property));
113
+ externalTemplateMigrations.set(templatePath, existing);
114
+ return;
213
115
  }
116
+ const initializerText = template.initializer.getText(sourceFileForTemplates);
117
+ const quote = initializerText[0];
118
+ inlineTemplateEdits.push({
119
+ start: template.initializer.getStart(sourceFileForTemplates),
120
+ end: template.initializer.getEnd(),
121
+ replacement: quote + migrateTemplateText(initializerText.slice(1, -1), signalProperties) + quote,
122
+ });
214
123
  });
215
- // Write the updated content if changes were made
124
+ inlineTemplateEdits.sort((a, b) => b.start - a.start);
125
+ for (const edit of inlineTemplateEdits) {
126
+ updatedContent = updatedContent.slice(0, edit.start) + edit.replacement + updatedContent.slice(edit.end);
127
+ }
216
128
  if (updatedContent !== content) {
217
129
  tree.write(filePath, updatedContent);
218
130
  filesModified++;
219
131
  }
220
132
  }
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
- }
133
+ for (const [templatePath, signalProperties] of externalTemplateMigrations) {
134
+ if (tree.exists(templatePath) && migrateTemplateFile(tree, templatePath, { signalProperties })) {
135
+ templatesModified++;
231
136
  }
232
137
  }
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
- }
138
+ return { filesChanged: filesModified + templatesModified, review };
243
139
  }
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
- }
140
+ const SIGNAL_INJECT_CALLS = [
141
+ 'injectIsXs()',
142
+ 'injectIsSm()',
143
+ 'injectIsMd()',
144
+ 'injectIsLg()',
145
+ 'injectIsXl()',
146
+ 'injectIs2Xl()',
147
+ 'injectIsBase()',
148
+ 'injectCurrentBreakpoint()',
149
+ 'injectViewportDimensions()',
150
+ 'injectScrollbarDimensions()',
151
+ 'injectObserveBreakpoint()',
152
+ 'injectBreakpointIsMatched()',
153
+ ];
154
+ function collectSignalProperties(sourceFile, classNode) {
155
+ const signalProperties = new Set();
156
+ for (const member of classNode.members) {
157
+ if (!ts.isPropertyDeclaration(member) || !ts.isIdentifier(member.name) || !member.initializer)
158
+ continue;
159
+ if (member.modifiers?.some((mod) => mod.kind === ts.SyntaxKind.PrivateKeyword))
160
+ continue;
161
+ const initText = member.initializer.getText(sourceFile);
162
+ if (SIGNAL_INJECT_CALLS.some((call) => initText.includes(call)) && !initText.includes('toObservable(')) {
163
+ signalProperties.add(member.name.text);
260
164
  }
261
165
  }
262
- findTsFiles('.');
263
- for (const filePath of allFiles) {
264
- const content = tree.read(filePath, 'utf-8');
265
- if (!content)
166
+ return signalProperties;
167
+ }
168
+ function findComponentTemplate(classNode) {
169
+ for (const decorator of ts.getDecorators(classNode) ?? []) {
170
+ const metadata = ts.isCallExpression(decorator.expression) ? decorator.expression.arguments[0] : undefined;
171
+ if (!metadata || !ts.isObjectLiteralExpression(metadata))
266
172
  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
173
+ for (const prop of metadata.properties) {
174
+ if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name))
175
+ continue;
176
+ if (!ts.isNoSubstitutionTemplateLiteral(prop.initializer) && !ts.isStringLiteral(prop.initializer))
177
+ continue;
178
+ if (prop.name.text === 'template')
179
+ return { kind: 'inline', initializer: prop.initializer };
180
+ if (prop.name.text === 'templateUrl')
181
+ return { kind: 'url', path: prop.initializer.text };
275
182
  }
276
183
  }
277
- return { hasViewportVariables, hasScrollbarVariables };
184
+ return null;
278
185
  }
186
+ const migrateTemplateExpression = (expression, signalProperties) => {
187
+ let migrated = expression;
188
+ for (const propName of signalProperties) {
189
+ const regex = new RegExp(`\\b${escapeRegExp(propName)}(?!\\s*\\()\\b`, 'g');
190
+ migrated = migrated.replace(regex, `${propName}()`);
191
+ }
192
+ return migrated;
193
+ };
194
+ const migrateTemplateText = (template, signalProperties) => template
195
+ .replace(/{{([\s\S]*?)}}/g, (_, expression) => `{{${migrateTemplateExpression(expression, signalProperties)}}}`)
196
+ .replace(/((?:\[[^\]]+\]|\([^)]+\)|\*[\w.-]+)\s*=\s*)(["'])([\s\S]*?)\2/g, (_, prefix, quote, expression) => `${prefix}${quote}${migrateTemplateExpression(expression, signalProperties)}${quote}`)
197
+ .replace(/@(if|for|switch|case|defer)\s*\(([^\n{}]*)\)/g, (_, block, expression) => `@${block} (${migrateTemplateExpression(expression, signalProperties)})`);
279
198
  function migrateTemplateFile(tree, htmlFilePath, migrationInfo) {
280
199
  const content = tree.read(htmlFilePath, 'utf-8');
281
200
  if (!content)
282
201
  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
- }
202
+ const updatedContent = migrateTemplateText(content, migrationInfo.signalProperties);
203
+ const hasChanges = updatedContent !== content;
301
204
  if (hasChanges) {
302
205
  tree.write(htmlFilePath, updatedContent);
303
206
  }
304
207
  return hasChanges;
305
208
  }
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
209
  function getViewportServicePackage(sourceFile) {
339
210
  let packageName = null;
340
211
  sourceFile.forEachChild((node) => {
341
212
  if (ts.isImportDeclaration(node) && node.importClause?.namedBindings) {
342
213
  if (ts.isNamedImports(node.importClause.namedBindings)) {
343
- const hasViewportService = node.importClause.namedBindings.elements.some((el) => el.name.text === 'ViewportService');
214
+ const hasViewportService = node.importClause.namedBindings.elements.some((el) => (el.propertyName?.text ?? el.name.text) === 'ViewportService');
344
215
  if (hasViewportService && ts.isStringLiteral(node.moduleSpecifier)) {
345
216
  packageName = node.moduleSpecifier.text;
346
217
  }
@@ -359,7 +230,7 @@ function getPropertyMaps(packageName) {
359
230
  isLg: isEthleteCore ? 'injectIsLg' : 'injectIsXl',
360
231
  isXl: isEthleteCore ? 'injectIsXl' : 'injectIs2Xl',
361
232
  is2Xl: 'injectIs2Xl',
362
- isBase: isEthleteCore ? 'injectIsBase' : 'injectIsLg',
233
+ isBase: 'injectIsLg',
363
234
  viewportSize: 'injectViewportDimensions',
364
235
  scrollbarSize: 'injectScrollbarDimensions',
365
236
  currentViewport: 'injectCurrentBreakpoint',
@@ -500,7 +371,7 @@ function analyzeClassMigration(sourceFile, classNode, viewportServiceVar) {
500
371
  });
501
372
  // Group property initializer usages by their ViewportService usage pattern
502
373
  const propertyInitializerUsages = new Map();
503
- for (const [usageText, { propertyName, usage, propertyDecl }] of usagesInPropertyInitializers) {
374
+ for (const [, { propertyName, usage, propertyDecl }] of usagesInPropertyInitializers) {
504
375
  let injectFn;
505
376
  let args;
506
377
  let type = 'signal';
@@ -568,7 +439,7 @@ function analyzeClassMigration(sourceFile, classNode, viewportServiceVar) {
568
439
  });
569
440
  if (isUsedOutsideInitializers && type === 'observable') {
570
441
  // Create a shared member for all properties that use this observable
571
- const baseName = args ? generateNameFromArgs(usageKey, args, type) : usageKey;
442
+ const baseName = args ? generateNameFromArgs(args, type) : usageKey;
572
443
  const memberName = findAvailableMemberName(baseName, context.existingMembers);
573
444
  const memberInfo = {
574
445
  name: memberName,
@@ -599,7 +470,7 @@ function analyzeClassMigration(sourceFile, classNode, viewportServiceVar) {
599
470
  // OR if the property is used elsewhere in the class
600
471
  const propertyName = ts.isIdentifier(propertyDecl.name) ? propertyDecl.name.text : '';
601
472
  const isObservableProperty = propertyName.endsWith('$');
602
- const isPropertyUsedElsewhere = checkIfPropertyIsUsedElsewhere(sourceFile, classNode, propertyName, propertyDecl);
473
+ const isPropertyUsedElsewhere = checkIfPropertyIsUsedElsewhere(classNode, propertyName, propertyDecl);
603
474
  // Wrap with toObservable if:
604
475
  // 1. It's an observable type AND
605
476
  // 2. (Property name ends with $ OR property is used elsewhere)
@@ -718,7 +589,7 @@ function analyzeClassMigration(sourceFile, classNode, viewportServiceVar) {
718
589
  else {
719
590
  let baseName;
720
591
  if (usage.args) {
721
- baseName = generateNameFromArgs(originalProperty, usage.args, usage.type);
592
+ baseName = generateNameFromArgs(usage.args, usage.type);
722
593
  }
723
594
  else {
724
595
  baseName = usage.type === 'observable' ? originalProperty : originalProperty.replace(/\$$/, '');
@@ -843,7 +714,7 @@ function addMembersToClass(sourceFile, content, classNode, membersToAdd, viewpor
843
714
  const classText = classNode.getText(sourceFile);
844
715
  const openBraceIndex = classText.indexOf('{');
845
716
  if (openBraceIndex === -1) {
846
- console.warn('Could not find class body opening brace');
717
+ logger.warn('Could not find class body opening brace');
847
718
  return content;
848
719
  }
849
720
  const classStart = classNode.getStart(sourceFile);
@@ -889,70 +760,57 @@ function addMembersToClass(sourceFile, content, classNode, membersToAdd, viewpor
889
760
  }
890
761
  return content.slice(0, finalInsertPosition) + prefix + newMembers + suffix + content.slice(finalInsertPosition);
891
762
  }
892
- function removeViewportServiceInjection(sourceFile, content, viewportServiceVars, filePath) {
763
+ function removeViewportServiceInjection(sourceFile, content, serviceName, filePath) {
893
764
  let updatedContent = content;
894
765
  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: '' });
766
+ collectClasses(sourceFile).forEach((node) => {
767
+ node.members.forEach((member) => {
768
+ if (ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name)) {
769
+ if (isServiceProperty(member, serviceName)) {
770
+ const memberStart = member.getStart(sourceFile, true);
771
+ const memberEnd = member.getEnd();
772
+ // Find the line boundaries
773
+ let lineStart = memberStart;
774
+ while (lineStart > 0 && content[lineStart - 1] !== '\n') {
775
+ lineStart--;
918
776
  }
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;
777
+ let lineEnd = memberEnd;
778
+ while (lineEnd < content.length && content[lineEnd] !== '\n') {
779
+ lineEnd++;
943
780
  }
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
- });
781
+ if (content[lineEnd] === '\n') {
782
+ lineEnd++;
783
+ }
784
+ modifications.push({ start: lineStart, end: lineEnd, replacement: '' });
953
785
  }
954
- });
955
- }
786
+ }
787
+ });
788
+ // Collect constructor parameter modifications
789
+ node.members.forEach((member) => {
790
+ if (ts.isConstructorDeclaration(member) &&
791
+ member.parameters.some((param) => isServiceParameter(param, serviceName))) {
792
+ const newParams = member.parameters
793
+ .filter((param) => !isServiceParameter(param, serviceName))
794
+ .map((param) => param.getText(sourceFile));
795
+ // Find parameter list boundaries
796
+ const constructorText = member.getText(sourceFile);
797
+ const openParenIndex = constructorText.indexOf('(');
798
+ const closeParenIndex = findMatchingParen(constructorText, openParenIndex);
799
+ if (openParenIndex === -1 || closeParenIndex === -1) {
800
+ logger.warn(`Could not find constructor parameters in ${filePath}`);
801
+ return;
802
+ }
803
+ const constructorStart = member.getStart(sourceFile);
804
+ const paramListStart = constructorStart + openParenIndex + 1;
805
+ const paramListEnd = constructorStart + closeParenIndex;
806
+ const newParamsText = newParams.join(', ');
807
+ modifications.push({
808
+ start: paramListStart,
809
+ end: paramListEnd,
810
+ replacement: newParamsText,
811
+ });
812
+ }
813
+ });
956
814
  });
957
815
  // Sort modifications by start position (descending) to apply from end to start
958
816
  modifications.sort((a, b) => b.start - a.start);
@@ -976,7 +834,7 @@ function findMatchingParen(text, openIndex) {
976
834
  }
977
835
  return depth === 0 ? i - 1 : -1;
978
836
  }
979
- function generateNameFromArgs(methodName, args, type) {
837
+ function generateNameFromArgs(args, type) {
980
838
  // Parse the arguments to generate a meaningful name
981
839
  // e.g., { min: 'lg' } => isMinLg or isMinLg$
982
840
  // e.g., { max: 'sm' } => isMaxSm or isMaxSm$
@@ -1061,39 +919,10 @@ function createReplacementsForMember(sourceFile, classNode, viewportServiceVar,
1061
919
  visitNode(member);
1062
920
  });
1063
921
  }
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
922
  function findAvailableMemberName(baseName, existingMembers) {
1094
923
  // baseName already has $ suffix for observables at this point
1095
924
  // Try variations: name, _name, #name
1096
- const candidates = [baseName, `_${baseName}`, `#${baseName}`];
925
+ const candidates = [baseName, `_${baseName}`];
1097
926
  for (const candidate of candidates) {
1098
927
  if (!existingMembers.has(candidate)) {
1099
928
  return candidate;
@@ -1106,7 +935,7 @@ function findAvailableMemberName(baseName, existingMembers) {
1106
935
  }
1107
936
  return `${baseName}${counter}`;
1108
937
  }
1109
- function migrateMonitorViewport(sourceFile, content, viewportServiceVars, cssVariablesUsed) {
938
+ function migrateMonitorViewport(sourceFile, content, viewportServiceVars) {
1110
939
  let updatedContent = content;
1111
940
  const imports = [];
1112
941
  const monitorViewportCalls = [];
@@ -1161,29 +990,8 @@ function migrateMonitorViewport(sourceFile, content, viewportServiceVars, cssVar
1161
990
  return { content, imports };
1162
991
  }
1163
992
  // 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
- }
993
+ const constructorStatements = ['writeViewportSizeToCssVariables();', 'writeScrollbarSizeToCssVariables();'];
994
+ imports.push('writeViewportSizeToCssVariables', 'writeScrollbarSizeToCssVariables');
1187
995
  // Group calls by class
1188
996
  const callsByClass = new Map();
1189
997
  for (const call of monitorViewportCalls) {
@@ -1232,7 +1040,7 @@ function migrateMonitorViewport(sourceFile, content, viewportServiceVars, cssVar
1232
1040
  const classText = classNode.getText(sourceFile);
1233
1041
  const openBraceIndex = classText.indexOf('{');
1234
1042
  if (openBraceIndex === -1) {
1235
- console.warn('Could not find class body opening brace');
1043
+ logger.warn('Could not find class body opening brace');
1236
1044
  continue;
1237
1045
  }
1238
1046
  const insertPosition = classStart + openBraceIndex + 1;
@@ -1278,13 +1086,15 @@ function addImportsToPackage(sourceFile, content, neededImports, packageName) {
1278
1086
  statement.moduleSpecifier.text === packageName);
1279
1087
  const importsToAdd = Array.from(neededImports).sort();
1280
1088
  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));
1089
+ const existingElements = existingImport.importClause.namedBindings.elements;
1090
+ const existingImportedNames = existingElements.map((el) => el.propertyName?.text ?? el.name.text);
1091
+ const existingImportNames = existingElements.map((el) => el.getText(sourceFile));
1092
+ const newImports = importsToAdd.filter((imp) => !existingImportedNames.includes(imp));
1283
1093
  if (newImports.length > 0) {
1284
1094
  const allImports = [...existingImportNames, ...newImports].sort();
1285
1095
  const importText = existingImport.getText(sourceFile);
1286
1096
  const newImportText = `import { ${allImports.join(', ')} } from '${packageName}';`;
1287
- return content.replace(importText, newImportText);
1097
+ return content.replace(importText, () => newImportText);
1288
1098
  }
1289
1099
  }
1290
1100
  else if (!existingImport) {
@@ -1295,87 +1105,7 @@ function addImportsToPackage(sourceFile, content, neededImports, packageName) {
1295
1105
  }
1296
1106
  return content;
1297
1107
  }
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) {
1108
+ function findViewportServiceVariables(sourceFile, serviceName) {
1379
1109
  const variables = [];
1380
1110
  function visit(node) {
1381
1111
  // Check for inject(ViewportService) pattern
@@ -1384,7 +1114,7 @@ function findViewportServiceVariables(sourceFile) {
1384
1114
  node.expression.text === 'inject' &&
1385
1115
  node.arguments.length > 0) {
1386
1116
  const arg = node.arguments[0];
1387
- if (ts.isIdentifier(arg) && arg.text === 'ViewportService') {
1117
+ if (ts.isIdentifier(arg) && arg.text === serviceName) {
1388
1118
  // Find the variable name
1389
1119
  let parent = node.parent;
1390
1120
  while (parent) {
@@ -1404,7 +1134,7 @@ function findViewportServiceVariables(sourceFile) {
1404
1134
  if (ts.isParameter(node)) {
1405
1135
  const typeNode = node.type;
1406
1136
  if (typeNode && ts.isTypeReferenceNode(typeNode) && ts.isIdentifier(typeNode.typeName)) {
1407
- if (typeNode.typeName.text === 'ViewportService' && node.name && ts.isIdentifier(node.name)) {
1137
+ if (typeNode.typeName.text === serviceName && node.name && ts.isIdentifier(node.name)) {
1408
1138
  variables.push(node.name.text);
1409
1139
  }
1410
1140
  }
@@ -1414,9 +1144,9 @@ function findViewportServiceVariables(sourceFile) {
1414
1144
  visit(sourceFile);
1415
1145
  return variables;
1416
1146
  }
1417
- function handleInlineInjectPatterns(sourceFile, content) {
1147
+ function handleInlineInjectPatterns(sourceFile, content, serviceName) {
1418
1148
  const importsNeeded = new Set();
1419
- let updatedContent = content;
1149
+ const edits = [];
1420
1150
  // Get the ViewportService package to determine mapping
1421
1151
  const packageName = getViewportServicePackage(sourceFile);
1422
1152
  const { signalPropertyMap, observablePropertyMap } = getPropertyMaps(packageName);
@@ -1450,12 +1180,11 @@ function handleInlineInjectPatterns(sourceFile, content) {
1450
1180
  arg.expression.expression.text === 'inject' &&
1451
1181
  arg.expression.arguments.length > 0) {
1452
1182
  const injectArg = arg.expression.arguments[0];
1453
- if (ts.isIdentifier(injectArg) && injectArg.text === 'ViewportService') {
1183
+ if (ts.isIdentifier(injectArg) && injectArg.text === serviceName) {
1454
1184
  const injectFn = signalPropertyMap[propertyName] || observablePropertyMap[propertyName];
1455
1185
  if (injectFn) {
1456
- const oldText = node.getText(sourceFile);
1457
1186
  const newText = `${injectFn}()`;
1458
- updatedContent = updatedContent.replace(oldText, newText);
1187
+ edits.push({ start: node.getStart(sourceFile), end: node.getEnd(), text: newText });
1459
1188
  importsNeeded.add(injectFn);
1460
1189
  processedNodes.add(arg);
1461
1190
  }
@@ -1472,13 +1201,12 @@ function handleInlineInjectPatterns(sourceFile, content) {
1472
1201
  arg.expression.expression.expression.text === 'inject' &&
1473
1202
  arg.expression.expression.arguments.length > 0) {
1474
1203
  const injectArg = arg.expression.expression.arguments[0];
1475
- if (ts.isIdentifier(injectArg) && injectArg.text === 'ViewportService') {
1476
- const oldText = node.getText(sourceFile);
1204
+ if (ts.isIdentifier(injectArg) && injectArg.text === serviceName) {
1477
1205
  // Extract arguments from the method call
1478
1206
  const args = arg.arguments.map((a) => a.getText(sourceFile)).join(', ');
1479
1207
  // Since it's wrapped in toSignal and our inject functions return signals, remove toSignal
1480
1208
  const newText = `${methodInfo.injectFn}(${args})`;
1481
- updatedContent = updatedContent.replace(oldText, newText);
1209
+ edits.push({ start: node.getStart(sourceFile), end: node.getEnd(), text: newText });
1482
1210
  importsNeeded.add(methodInfo.injectFn);
1483
1211
  processedNodes.add(arg);
1484
1212
  }
@@ -1499,12 +1227,11 @@ function handleInlineInjectPatterns(sourceFile, content) {
1499
1227
  node.expression.expression.expression.text === 'inject' &&
1500
1228
  node.expression.expression.arguments.length > 0) {
1501
1229
  const injectArg = node.expression.expression.arguments[0];
1502
- if (ts.isIdentifier(injectArg) && injectArg.text === 'ViewportService') {
1230
+ if (ts.isIdentifier(injectArg) && injectArg.text === serviceName) {
1503
1231
  if (isInsideToSignal(node)) {
1504
1232
  ts.forEachChild(node, visitNode);
1505
1233
  return;
1506
1234
  }
1507
- const oldText = node.getText(sourceFile);
1508
1235
  const args = node.arguments.map((arg) => arg.getText(sourceFile)).join(', ');
1509
1236
  // Only wrap with toObservable if the assigned variable/property ends with $
1510
1237
  let needsToObservable = false;
@@ -1518,7 +1245,7 @@ function handleInlineInjectPatterns(sourceFile, content) {
1518
1245
  newText = `toObservable(${newText})`;
1519
1246
  importsNeeded.add('toObservable');
1520
1247
  }
1521
- updatedContent = updatedContent.replace(oldText, newText);
1248
+ edits.push({ start: node.getStart(sourceFile), end: node.getEnd(), text: newText });
1522
1249
  importsNeeded.add(methodInfo.injectFn);
1523
1250
  }
1524
1251
  }
@@ -1535,14 +1262,13 @@ function handleInlineInjectPatterns(sourceFile, content) {
1535
1262
  node.expression.expression.text === 'inject' &&
1536
1263
  node.expression.arguments.length > 0) {
1537
1264
  const injectArg = node.expression.arguments[0];
1538
- if (ts.isIdentifier(injectArg) && injectArg.text === 'ViewportService') {
1265
+ if (ts.isIdentifier(injectArg) && injectArg.text === serviceName) {
1539
1266
  const injectFn = signalPropertyMap[propertyName] || observablePropertyMap[propertyName];
1540
1267
  if (injectFn) {
1541
1268
  if (isInsideToSignal(node)) {
1542
1269
  ts.forEachChild(node, visitNode);
1543
1270
  return;
1544
1271
  }
1545
- const oldText = node.getText(sourceFile);
1546
1272
  // Check if this is an observable property (ends with $)
1547
1273
  const isObservable = !!observablePropertyMap[propertyName];
1548
1274
  let newText = `${injectFn}()`;
@@ -1550,7 +1276,7 @@ function handleInlineInjectPatterns(sourceFile, content) {
1550
1276
  newText = `toObservable(${newText})`;
1551
1277
  importsNeeded.add('toObservable');
1552
1278
  }
1553
- updatedContent = updatedContent.replace(oldText, newText);
1279
+ edits.push({ start: node.getStart(sourceFile), end: node.getEnd(), text: newText });
1554
1280
  importsNeeded.add(injectFn);
1555
1281
  }
1556
1282
  }
@@ -1559,7 +1285,7 @@ function handleInlineInjectPatterns(sourceFile, content) {
1559
1285
  ts.forEachChild(node, visitNode);
1560
1286
  }
1561
1287
  sourceFile.forEachChild(visitNode);
1562
- return { content: updatedContent, importsNeeded };
1288
+ return { content: applyInlineEdits(content, edits), importsNeeded };
1563
1289
  }
1564
1290
  function removeUnusedImports(sourceFile, content) {
1565
1291
  const importsToCheck = [
@@ -1569,14 +1295,14 @@ function removeUnusedImports(sourceFile, content) {
1569
1295
  let updatedContent = content;
1570
1296
  for (const importToCheck of importsToCheck) {
1571
1297
  // Check if the import specifier is used anywhere in the code
1572
- const isUsed = checkIfImportIsUsed(sourceFile, content, importToCheck.specifier);
1298
+ const isUsed = checkIfImportIsUsed(sourceFile, importToCheck.specifier);
1573
1299
  if (!isUsed) {
1574
1300
  updatedContent = removeImportSpecifier(sourceFile, updatedContent, importToCheck.specifier, importToCheck.package);
1575
1301
  }
1576
1302
  }
1577
1303
  return updatedContent;
1578
1304
  }
1579
- function checkIfImportIsUsed(sourceFile, content, specifier) {
1305
+ function checkIfImportIsUsed(sourceFile, specifier) {
1580
1306
  let isUsed = false;
1581
1307
  function visit(node) {
1582
1308
  if (isUsed)
@@ -1641,7 +1367,7 @@ function removeImportSpecifier(sourceFile, content, specifier, packageName) {
1641
1367
  });
1642
1368
  return updatedContent;
1643
1369
  }
1644
- function checkIfPropertyIsUsedElsewhere(sourceFile, classNode, propertyName, propertyDecl) {
1370
+ function checkIfPropertyIsUsedElsewhere(classNode, propertyName, propertyDecl) {
1645
1371
  let isUsed = false;
1646
1372
  function visit(node) {
1647
1373
  if (isUsed)
@@ -1675,4 +1401,13 @@ function checkIfPropertyIsUsedElsewhere(sourceFile, classNode, propertyName, pro
1675
1401
  });
1676
1402
  return isUsed;
1677
1403
  }
1678
- //# sourceMappingURL=viewport-service.js.map
1404
+ function applyInlineEdits(content, edits) {
1405
+ const accepted = [];
1406
+ for (const edit of [...edits].sort((a, b) => a.start - b.start || b.end - a.end)) {
1407
+ const last = accepted.at(-1);
1408
+ if (last && edit.start < last.end)
1409
+ continue;
1410
+ accepted.push(edit);
1411
+ }
1412
+ return accepted.reduceRight((result, edit) => result.slice(0, edit.start) + edit.text + result.slice(edit.end), content);
1413
+ }