@ethlete/core 4.31.0 → 5.0.0-next.0

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.
@@ -0,0 +1,1678 @@
1
+ /* eslint-disable @typescript-eslint/no-unused-vars */
2
+ /* eslint-disable @typescript-eslint/no-non-null-assertion */
3
+ import { logger } from '@nx/devkit';
4
+ 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);
33
+ let filesModified = 0;
34
+ 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
39
+ for (const filePath of tsFiles) {
40
+ const content = tree.read(filePath, 'utf-8');
41
+ if (!content)
42
+ continue;
43
+ // Skip files that don't use ViewportService
44
+ if (!content.includes('ViewportService'))
45
+ continue;
46
+ logger.log(`Processing: ${filePath}`);
47
+ 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);
56
+ let updatedContent = inlineResult.content;
57
+ // Re-parse if content changed
58
+ const updatedSourceFile = updatedContent !== content
59
+ ? ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true)
60
+ : sourceFile;
61
+ // Find all ViewportService variables (injected or constructor params)
62
+ const viewportServiceVars = findViewportServiceVariables(updatedSourceFile);
63
+ if (viewportServiceVars.length === 0 && inlineResult.importsNeeded.size === 0) {
64
+ continue;
65
+ }
66
+ viewportServiceUsed = true;
67
+ const allImportsNeeded = {
68
+ '@ethlete/core': new Set(),
69
+ '@angular/core/rxjs-interop': new Set(),
70
+ };
71
+ // Merge imports from inline patterns
72
+ for (const importName of inlineResult.importsNeeded) {
73
+ if (importName === 'toObservable' || importName === 'toSignal') {
74
+ allImportsNeeded['@angular/core/rxjs-interop'].add(importName);
75
+ }
76
+ else {
77
+ allImportsNeeded['@ethlete/core'].add(importName);
78
+ }
79
+ }
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');
103
+ }
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);
116
+ }
117
+ }
118
+ }
119
+ // Handle monitorViewport migrations
120
+ const sourceFileAfterReplacements = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
121
+ const monitorViewportResult = migrateMonitorViewport(sourceFileAfterReplacements, updatedContent, viewportServiceVars, cssVariablesUsed);
122
+ updatedContent = monitorViewportResult.content;
123
+ monitorViewportResult.imports.forEach((imp) => allImportsNeeded['@ethlete/core'].add(imp));
124
+ // Add necessary imports
125
+ for (const [packageName, importsSet] of Object.entries(allImportsNeeded)) {
126
+ if (importsSet.size > 0) {
127
+ const sourceFileUpdated = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
128
+ updatedContent = addImportsToPackage(sourceFileUpdated, updatedContent, importsSet, packageName);
129
+ }
130
+ }
131
+ // Remove ViewportService injection (properties and constructor params)
132
+ 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
135
+ const sourceFileAfterRemoval = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
136
+ if (!checkIfViewportServiceStillUsed(sourceFileAfterRemoval, viewportServiceVars)) {
137
+ updatedContent = removeViewportServiceImport(sourceFileAfterRemoval, updatedContent);
138
+ }
139
+ // Clean up unused imports (toSignal, toObservable)
140
+ const sourceFileAfterCleanup = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
141
+ 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
+ });
213
+ }
214
+ });
215
+ // Write the updated content if changes were made
216
+ if (updatedContent !== content) {
217
+ tree.write(filePath, updatedContent);
218
+ filesModified++;
219
+ }
220
+ }
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
+ }
231
+ }
232
+ }
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
+ }
243
+ }
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
+ }
260
+ }
261
+ }
262
+ findTsFiles('.');
263
+ for (const filePath of allFiles) {
264
+ const content = tree.read(filePath, 'utf-8');
265
+ if (!content)
266
+ 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
275
+ }
276
+ }
277
+ return { hasViewportVariables, hasScrollbarVariables };
278
+ }
279
+ function migrateTemplateFile(tree, htmlFilePath, migrationInfo) {
280
+ const content = tree.read(htmlFilePath, 'utf-8');
281
+ if (!content)
282
+ 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
+ }
301
+ if (hasChanges) {
302
+ tree.write(htmlFilePath, updatedContent);
303
+ }
304
+ return hasChanges;
305
+ }
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
+ function getViewportServicePackage(sourceFile) {
339
+ let packageName = null;
340
+ sourceFile.forEachChild((node) => {
341
+ if (ts.isImportDeclaration(node) && node.importClause?.namedBindings) {
342
+ if (ts.isNamedImports(node.importClause.namedBindings)) {
343
+ const hasViewportService = node.importClause.namedBindings.elements.some((el) => el.name.text === 'ViewportService');
344
+ if (hasViewportService && ts.isStringLiteral(node.moduleSpecifier)) {
345
+ packageName = node.moduleSpecifier.text;
346
+ }
347
+ }
348
+ }
349
+ });
350
+ return packageName;
351
+ }
352
+ function getPropertyMaps(packageName) {
353
+ const isEthleteCore = packageName === '@ethlete/core';
354
+ // Property map for signals (boolean getters)
355
+ const signalPropertyMap = {
356
+ isXs: 'injectIsXs',
357
+ isSm: 'injectIsSm',
358
+ isMd: 'injectIsMd',
359
+ isLg: isEthleteCore ? 'injectIsLg' : 'injectIsXl',
360
+ isXl: isEthleteCore ? 'injectIsXl' : 'injectIs2Xl',
361
+ is2Xl: 'injectIs2Xl',
362
+ isBase: isEthleteCore ? 'injectIsBase' : 'injectIsLg',
363
+ viewportSize: 'injectViewportDimensions',
364
+ scrollbarSize: 'injectScrollbarDimensions',
365
+ currentViewport: 'injectCurrentBreakpoint',
366
+ };
367
+ // Property map for observables
368
+ const observablePropertyMap = {
369
+ isXs$: 'injectIsXs',
370
+ isSm$: 'injectIsSm',
371
+ isMd$: 'injectIsMd',
372
+ isLg$: isEthleteCore ? 'injectIsLg' : 'injectIsXl',
373
+ isXl$: isEthleteCore ? 'injectIsXl' : 'injectIs2Xl',
374
+ is2Xl$: 'injectIs2Xl',
375
+ isBase$: 'injectIsLg',
376
+ viewportSize$: 'injectViewportDimensions',
377
+ scrollbarSize$: 'injectScrollbarDimensions',
378
+ currentViewport$: 'injectCurrentBreakpoint',
379
+ };
380
+ return { signalPropertyMap, observablePropertyMap };
381
+ }
382
+ function analyzeClassMigration(sourceFile, classNode, viewportServiceVar) {
383
+ const context = {
384
+ viewportServiceVar,
385
+ existingMembers: new Set(),
386
+ membersToAdd: [],
387
+ replacements: new Map(),
388
+ importsNeeded: new Set(),
389
+ };
390
+ // Collect existing member names and detect property initializers
391
+ const propertyInitializers = new Map();
392
+ classNode.members.forEach((member) => {
393
+ if (ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name)) {
394
+ const memberName = member.name.text;
395
+ context.existingMembers.add(memberName);
396
+ // Check if this property has an initializer that uses ViewportService
397
+ if (member.initializer) {
398
+ const initializerText = member.initializer.getText(sourceFile);
399
+ if (initializerText.includes(viewportServiceVar)) {
400
+ propertyInitializers.set(memberName, member);
401
+ }
402
+ }
403
+ }
404
+ else if (ts.isMethodDeclaration(member) && ts.isIdentifier(member.name)) {
405
+ context.existingMembers.add(member.name.text);
406
+ }
407
+ });
408
+ // Get the ViewportService package to determine mapping
409
+ const packageName = getViewportServicePackage(sourceFile);
410
+ const { signalPropertyMap, observablePropertyMap } = getPropertyMaps(packageName);
411
+ // Method map
412
+ const methodMap = {
413
+ observe: { injectFn: 'injectObserveBreakpoint', type: 'observable' },
414
+ isMatched: { injectFn: 'injectBreakpointIsMatched', type: 'signal' },
415
+ // For even older project based Viewport services
416
+ build: { injectFn: 'injectObserveBreakpoint', type: 'observable' },
417
+ };
418
+ // Track which usages are wrapped in toSignal
419
+ const usagesWrappedInToSignal = new Map();
420
+ const usagesOutsideToSignal = new Map();
421
+ const usagesInPropertyInitializers = new Map();
422
+ // First pass: detect toSignal usages, property initializers, and count all usages
423
+ function detectUsages(node, insideToSignal = false, currentProperty) {
424
+ // Handle toSignal wrapper
425
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'toSignal') {
426
+ const arg = node.arguments[0];
427
+ if (arg) {
428
+ detectUsages(arg, true, currentProperty);
429
+ }
430
+ // Don't traverse children again for toSignal - we already processed the argument
431
+ return;
432
+ }
433
+ // Track ViewportService property access
434
+ if (ts.isPropertyAccessExpression(node)) {
435
+ const propertyName = node.name.text;
436
+ const isViewportAccess = (ts.isIdentifier(node.expression) && node.expression.text === viewportServiceVar) ||
437
+ (ts.isPropertyAccessExpression(node.expression) &&
438
+ node.expression.expression.kind === ts.SyntaxKind.ThisKeyword &&
439
+ node.expression.name.text === viewportServiceVar);
440
+ if (isViewportAccess && (signalPropertyMap[propertyName] || observablePropertyMap[propertyName])) {
441
+ const fullAccess = node.getText(sourceFile);
442
+ if (currentProperty) {
443
+ const propertyDecl = propertyInitializers.get(currentProperty);
444
+ if (propertyDecl) {
445
+ usagesInPropertyInitializers.set(fullAccess, {
446
+ propertyName: currentProperty,
447
+ usage: fullAccess,
448
+ propertyDecl,
449
+ });
450
+ }
451
+ }
452
+ else if (insideToSignal) {
453
+ usagesWrappedInToSignal.set(fullAccess, (usagesWrappedInToSignal.get(fullAccess) || 0) + 1);
454
+ }
455
+ else {
456
+ usagesOutsideToSignal.set(fullAccess, (usagesOutsideToSignal.get(fullAccess) || 0) + 1);
457
+ }
458
+ }
459
+ }
460
+ // Track method calls
461
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
462
+ const methodName = node.expression.name.text;
463
+ if (methodName !== 'monitorViewport' && methodName !== 'pipe' && methodName !== 'subscribe') {
464
+ const isViewportAccess = (ts.isIdentifier(node.expression.expression) && node.expression.expression.text === viewportServiceVar) ||
465
+ (ts.isPropertyAccessExpression(node.expression.expression) &&
466
+ node.expression.expression.expression.kind === ts.SyntaxKind.ThisKeyword &&
467
+ node.expression.expression.name.text === viewportServiceVar);
468
+ if (isViewportAccess && methodMap[methodName]) {
469
+ const fullCall = node.getText(sourceFile);
470
+ if (currentProperty) {
471
+ const propertyDecl = propertyInitializers.get(currentProperty);
472
+ if (propertyDecl) {
473
+ usagesInPropertyInitializers.set(fullCall, {
474
+ propertyName: currentProperty,
475
+ usage: fullCall,
476
+ propertyDecl,
477
+ });
478
+ }
479
+ }
480
+ else if (insideToSignal) {
481
+ usagesWrappedInToSignal.set(fullCall, (usagesWrappedInToSignal.get(fullCall) || 0) + 1);
482
+ }
483
+ else {
484
+ usagesOutsideToSignal.set(fullCall, (usagesOutsideToSignal.get(fullCall) || 0) + 1);
485
+ }
486
+ }
487
+ }
488
+ }
489
+ // ALWAYS traverse children
490
+ ts.forEachChild(node, (child) => detectUsages(child, insideToSignal, currentProperty));
491
+ }
492
+ classNode.members.forEach((member) => {
493
+ if (ts.isPropertyDeclaration(member) && member.initializer && ts.isIdentifier(member.name)) {
494
+ const propertyName = member.name.text;
495
+ detectUsages(member.initializer, false, propertyName);
496
+ }
497
+ else {
498
+ detectUsages(member);
499
+ }
500
+ });
501
+ // Group property initializer usages by their ViewportService usage pattern
502
+ const propertyInitializerUsages = new Map();
503
+ for (const [usageText, { propertyName, usage, propertyDecl }] of usagesInPropertyInitializers) {
504
+ let injectFn;
505
+ let args;
506
+ let type = 'signal';
507
+ let usageKey;
508
+ // Check if it's a method call
509
+ if (usage.includes('(')) {
510
+ const methodMatch = usage.match(/\.(\w+)\((.*)\)/);
511
+ if (methodMatch) {
512
+ const methodName = methodMatch[1];
513
+ args = methodMatch[2];
514
+ const methodInfo = methodMap[methodName];
515
+ if (methodInfo) {
516
+ injectFn = methodInfo.injectFn;
517
+ type = methodInfo.type;
518
+ usageKey = `${methodName}(${args})`;
519
+ }
520
+ }
521
+ }
522
+ else {
523
+ // It's a property access
524
+ const propertyMatch = usage.match(/\.(\w+\$?)$/);
525
+ if (propertyMatch) {
526
+ const propName = propertyMatch[1];
527
+ usageKey = propName;
528
+ if (signalPropertyMap[propName]) {
529
+ injectFn = signalPropertyMap[propName];
530
+ type = 'signal';
531
+ }
532
+ else if (observablePropertyMap[propName]) {
533
+ injectFn = observablePropertyMap[propName];
534
+ type = 'observable';
535
+ }
536
+ }
537
+ }
538
+ // Check if this property initializer wraps the usage in toSignal
539
+ const initializerText = propertyDecl.initializer?.getText(sourceFile) || '';
540
+ const wrappedInToSignal = initializerText.includes('toSignal(');
541
+ if (injectFn && usageKey) {
542
+ if (!propertyInitializerUsages.has(usageKey)) {
543
+ propertyInitializerUsages.set(usageKey, {
544
+ usageKey,
545
+ injectFn,
546
+ args,
547
+ type,
548
+ properties: [],
549
+ });
550
+ }
551
+ propertyInitializerUsages.get(usageKey).properties.push({
552
+ propertyName,
553
+ propertyDecl,
554
+ wrappedInToSignal,
555
+ });
556
+ }
557
+ }
558
+ // Handle property initializers
559
+ for (const [usageKey, usageInfo] of propertyInitializerUsages) {
560
+ const { injectFn, args, type, properties } = usageInfo;
561
+ // Check if this same ViewportService usage is used outside property initializers
562
+ const isUsedOutsideInitializers = usagesOutsideToSignal.has(usageKey) ||
563
+ Array.from(usagesOutsideToSignal.keys()).some((key) => {
564
+ if (args) {
565
+ return key.includes(usageKey.split('(')[0]) && key.includes(args);
566
+ }
567
+ return key.includes(usageKey);
568
+ });
569
+ if (isUsedOutsideInitializers && type === 'observable') {
570
+ // Create a shared member for all properties that use this observable
571
+ const baseName = args ? generateNameFromArgs(usageKey, args, type) : usageKey;
572
+ const memberName = findAvailableMemberName(baseName, context.existingMembers);
573
+ const memberInfo = {
574
+ name: memberName,
575
+ type: 'observable',
576
+ injectFn,
577
+ originalProperty: usageKey.replace(/\(.*\)$/, ''),
578
+ args,
579
+ wrappedInToSignal: false,
580
+ };
581
+ context.membersToAdd.push(memberInfo);
582
+ context.existingMembers.add(memberName);
583
+ // Replace all property initializers to use the shared member
584
+ for (const { propertyDecl, wrappedInToSignal } of properties) {
585
+ const initializerText = propertyDecl.initializer.getText(sourceFile);
586
+ // If the property initializer wrapped it in toSignal, keep that wrapper
587
+ const replacement = wrappedInToSignal ? `toSignal(this.${memberName})` : `this.${memberName}`;
588
+ context.replacements.set(initializerText, replacement);
589
+ }
590
+ // Also create replacements for usages outside initializers
591
+ createReplacementsForMember(sourceFile, classNode, viewportServiceVar, usageKey, memberName, type, context, args);
592
+ }
593
+ else {
594
+ // Replace each property initializer directly
595
+ for (const { propertyDecl } of properties) {
596
+ const initializerText = propertyDecl.initializer.getText(sourceFile);
597
+ const injectCall = args ? `${injectFn}(${args})` : `${injectFn}()`;
598
+ // For observable types, check if property name indicates it's an observable (ends with $)
599
+ // OR if the property is used elsewhere in the class
600
+ const propertyName = ts.isIdentifier(propertyDecl.name) ? propertyDecl.name.text : '';
601
+ const isObservableProperty = propertyName.endsWith('$');
602
+ const isPropertyUsedElsewhere = checkIfPropertyIsUsedElsewhere(sourceFile, classNode, propertyName, propertyDecl);
603
+ // Wrap with toObservable if:
604
+ // 1. It's an observable type AND
605
+ // 2. (Property name ends with $ OR property is used elsewhere)
606
+ const needsToObservable = type === 'observable' && (isObservableProperty || isPropertyUsedElsewhere);
607
+ const wrappedInjectCall = needsToObservable ? `toObservable(${injectCall})` : injectCall;
608
+ // Check if this is a method call or property access
609
+ const isMethodCall = usageKey.includes('(');
610
+ const hasChainedCalls = initializerText.includes('.pipe(') ||
611
+ initializerText.includes('.subscribe(') ||
612
+ initializerText.match(/\)\s*\./);
613
+ if (hasChainedCalls) {
614
+ // Only replace the ViewportService access, preserve the rest
615
+ if (isMethodCall) {
616
+ // For method calls: this._viewportService.observe({ min: 'md' })
617
+ const viewportServiceCallPattern = new RegExp(`(this\\.)?${escapeRegExp(viewportServiceVar)}\\.${escapeRegExp(usageKey.split('(')[0])}\\([^)]*\\)`, 'g');
618
+ const matches = initializerText.match(viewportServiceCallPattern);
619
+ if (matches && matches[0]) {
620
+ context.replacements.set(matches[0], wrappedInjectCall);
621
+ }
622
+ }
623
+ else {
624
+ // For property access: this._viewportService.currentViewport$
625
+ const viewportServicePropertyPattern = new RegExp(`(this\\.)?${escapeRegExp(viewportServiceVar)}\\.${escapeRegExp(usageKey)}`, 'g');
626
+ const matches = initializerText.match(viewportServicePropertyPattern);
627
+ if (matches && matches[0]) {
628
+ context.replacements.set(matches[0], wrappedInjectCall);
629
+ }
630
+ }
631
+ }
632
+ else {
633
+ // Replace the entire initializer
634
+ context.replacements.set(initializerText, wrappedInjectCall);
635
+ }
636
+ context.importsNeeded.add(injectFn);
637
+ }
638
+ }
639
+ }
640
+ // Find all other usages and determine what members we need
641
+ const usagesFound = new Map();
642
+ function visitNode(node) {
643
+ if (ts.isPropertyAccessExpression(node)) {
644
+ const propertyName = node.name.text;
645
+ const isViewportAccess = (ts.isIdentifier(node.expression) && node.expression.text === viewportServiceVar) ||
646
+ (ts.isPropertyAccessExpression(node.expression) &&
647
+ node.expression.expression.kind === ts.SyntaxKind.ThisKeyword &&
648
+ node.expression.name.text === viewportServiceVar);
649
+ if (isViewportAccess) {
650
+ const fullAccess = node.getText(sourceFile);
651
+ // Skip if this is in a property initializer (already handled)
652
+ if (usagesInPropertyInitializers.has(fullAccess)) {
653
+ return;
654
+ }
655
+ const onlyInToSignal = usagesWrappedInToSignal.has(fullAccess) && !usagesOutsideToSignal.has(fullAccess);
656
+ if (signalPropertyMap[propertyName]) {
657
+ usagesFound.set(propertyName, {
658
+ type: 'signal',
659
+ injectFn: signalPropertyMap[propertyName],
660
+ wrappedInToSignal: onlyInToSignal,
661
+ });
662
+ }
663
+ else if (observablePropertyMap[propertyName]) {
664
+ usagesFound.set(propertyName, {
665
+ type: 'observable',
666
+ injectFn: observablePropertyMap[propertyName],
667
+ wrappedInToSignal: onlyInToSignal,
668
+ });
669
+ }
670
+ }
671
+ }
672
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
673
+ const methodName = node.expression.name.text;
674
+ if (methodName === 'monitorViewport') {
675
+ ts.forEachChild(node, visitNode);
676
+ return;
677
+ }
678
+ const isViewportAccess = (ts.isIdentifier(node.expression.expression) && node.expression.expression.text === viewportServiceVar) ||
679
+ (ts.isPropertyAccessExpression(node.expression.expression) &&
680
+ node.expression.expression.expression.kind === ts.SyntaxKind.ThisKeyword &&
681
+ node.expression.expression.name.text === viewportServiceVar);
682
+ if (isViewportAccess && methodMap[methodName]) {
683
+ const methodInfo = methodMap[methodName];
684
+ const args = node.arguments.map((arg) => arg.getText(sourceFile)).join(', ');
685
+ const uniqueKey = `${methodName}(${args})`;
686
+ const fullCall = node.getText(sourceFile);
687
+ // Skip if this is in a property initializer (already handled)
688
+ if (usagesInPropertyInitializers.has(fullCall)) {
689
+ return;
690
+ }
691
+ const onlyInToSignal = usagesWrappedInToSignal.has(fullCall) && !usagesOutsideToSignal.has(fullCall);
692
+ usagesFound.set(uniqueKey, {
693
+ type: methodInfo.type,
694
+ injectFn: methodInfo.injectFn,
695
+ args,
696
+ wrappedInToSignal: onlyInToSignal,
697
+ });
698
+ }
699
+ }
700
+ ts.forEachChild(node, visitNode);
701
+ }
702
+ classNode.members.forEach((member) => {
703
+ if (ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name) && member.name.text === viewportServiceVar) {
704
+ return;
705
+ }
706
+ // Skip property initializers as we already processed them
707
+ if (ts.isPropertyDeclaration(member) && member.initializer) {
708
+ return;
709
+ }
710
+ visitNode(member);
711
+ });
712
+ // Create members to add and replacements for non-property-initializer usages
713
+ for (const [originalProperty, usage] of usagesFound) {
714
+ if (usage.wrappedInToSignal) {
715
+ createDirectReplacementsForToSignal(sourceFile, classNode, viewportServiceVar, originalProperty, usage.injectFn, usage.args, context);
716
+ context.importsNeeded.add(usage.injectFn);
717
+ }
718
+ else {
719
+ let baseName;
720
+ if (usage.args) {
721
+ baseName = generateNameFromArgs(originalProperty, usage.args, usage.type);
722
+ }
723
+ else {
724
+ baseName = usage.type === 'observable' ? originalProperty : originalProperty.replace(/\$$/, '');
725
+ }
726
+ const memberName = findAvailableMemberName(baseName, context.existingMembers);
727
+ const memberInfo = {
728
+ name: memberName,
729
+ type: usage.type,
730
+ injectFn: usage.injectFn,
731
+ originalProperty: originalProperty.replace(/\(.*\)$/, ''),
732
+ args: usage.args,
733
+ wrappedInToSignal: false,
734
+ };
735
+ context.membersToAdd.push(memberInfo);
736
+ context.existingMembers.add(memberName);
737
+ createReplacementsForMember(sourceFile, classNode, viewportServiceVar, originalProperty, memberName, usage.type, context, usage.args);
738
+ }
739
+ }
740
+ return context;
741
+ }
742
+ function createDirectReplacementsForToSignal(sourceFile, classNode, viewportServiceVar, originalProperty, injectFn, args, context) {
743
+ // For observables returned by inject functions, we need to:
744
+ // 1. If it's an observable property (isXs$), replace toSignal(this.viewportService.isXs$) with just injectIsXs()
745
+ // 2. If it's a method call (observe()), check what it returns:
746
+ // - observe() returns Observable<boolean>, so replace toSignal(...observe()) with injectObserveBreakpoint() which returns Signal<boolean>
747
+ const isObservableProperty = originalProperty.endsWith('$');
748
+ const isObserveMethod = originalProperty.startsWith('observe(');
749
+ const isIsMatchedMethod = originalProperty.startsWith('isMatched(');
750
+ function visitNode(node) {
751
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'toSignal') {
752
+ const arg = node.arguments[0];
753
+ if (!arg)
754
+ return;
755
+ const argText = arg.getText(sourceFile);
756
+ // Check if this toSignal wraps our ViewportService usage
757
+ let shouldReplace;
758
+ if (args) {
759
+ // For method calls like observe({ min: 'md' })
760
+ const methodName = originalProperty.replace(/\(.*\)$/, '');
761
+ shouldReplace = argText.includes(`${viewportServiceVar}.${methodName}`) && argText.includes(args);
762
+ }
763
+ else {
764
+ // For property access like isXs$ or isXs
765
+ shouldReplace = argText.includes(`${viewportServiceVar}.${originalProperty}`);
766
+ }
767
+ if (shouldReplace) {
768
+ const toSignalCall = node.getText(sourceFile);
769
+ let replacement;
770
+ // Determine what to replace with based on return type
771
+ if (isObservableProperty || isObserveMethod) {
772
+ // These inject functions return Signals directly, so remove toSignal wrapper
773
+ const injectCall = args ? `${injectFn}(${args})` : `${injectFn}()`;
774
+ replacement = injectCall;
775
+ }
776
+ else if (isIsMatchedMethod) {
777
+ // isMatched also returns a Signal
778
+ const injectCall = args ? `${injectFn}(${args})` : `${injectFn}()`;
779
+ replacement = injectCall;
780
+ }
781
+ else {
782
+ // Fallback: keep toSignal wrapper
783
+ const injectCall = args ? `${injectFn}(${args})` : `${injectFn}()`;
784
+ replacement = `toSignal(${injectCall})`;
785
+ }
786
+ if (!context.replacements.has(toSignalCall)) {
787
+ context.replacements.set(toSignalCall, replacement);
788
+ }
789
+ }
790
+ }
791
+ ts.forEachChild(node, visitNode);
792
+ }
793
+ classNode.members.forEach((member) => {
794
+ visitNode(member);
795
+ });
796
+ }
797
+ function escapeRegExp(string) {
798
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
799
+ }
800
+ function addMembersToClass(sourceFile, content, classNode, membersToAdd, viewportServiceVar) {
801
+ if (membersToAdd.length === 0)
802
+ return content;
803
+ // Find the ViewportService property to determine where to insert
804
+ let viewportServiceProperty;
805
+ let insertAfterViewportService = false;
806
+ for (const member of classNode.members) {
807
+ if (ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name) && member.name.text === viewportServiceVar) {
808
+ viewportServiceProperty = member;
809
+ break;
810
+ }
811
+ }
812
+ let finalInsertPosition;
813
+ let indentation = ' '; // default indentation
814
+ if (viewportServiceProperty) {
815
+ // Insert after the ViewportService property
816
+ const propertyEnd = viewportServiceProperty.getEnd();
817
+ finalInsertPosition = propertyEnd;
818
+ insertAfterViewportService = true;
819
+ // Get indentation from the ViewportService property
820
+ const propertyStart = viewportServiceProperty.getStart(sourceFile, true);
821
+ const lineStart = content.lastIndexOf('\n', propertyStart) + 1;
822
+ const leadingWhitespace = content.slice(lineStart, propertyStart);
823
+ if (leadingWhitespace.trim() === '') {
824
+ indentation = leadingWhitespace;
825
+ }
826
+ }
827
+ else {
828
+ // Insert at the beginning of the class body (after first member or opening brace)
829
+ if (classNode.members.length > 0) {
830
+ // Insert before the first member
831
+ const firstMember = classNode.members[0];
832
+ const memberStart = firstMember.getStart(sourceFile, true);
833
+ finalInsertPosition = memberStart;
834
+ // Get indentation from the first member
835
+ const lineStart = content.lastIndexOf('\n', memberStart) + 1;
836
+ const leadingWhitespace = content.slice(lineStart, memberStart);
837
+ if (leadingWhitespace.trim() === '') {
838
+ indentation = leadingWhitespace;
839
+ }
840
+ }
841
+ else {
842
+ // No members exist, find the opening brace and insert after it
843
+ const classText = classNode.getText(sourceFile);
844
+ const openBraceIndex = classText.indexOf('{');
845
+ if (openBraceIndex === -1) {
846
+ console.warn('Could not find class body opening brace');
847
+ return content;
848
+ }
849
+ const classStart = classNode.getStart(sourceFile);
850
+ finalInsertPosition = classStart + openBraceIndex + 1;
851
+ }
852
+ }
853
+ // Generate new member code
854
+ const newMembers = membersToAdd
855
+ .map((member) => {
856
+ const injectCall = member.args ? `${member.injectFn}(${member.args})` : `${member.injectFn}()`;
857
+ if (member.type === 'signal') {
858
+ return `${indentation}private ${member.name} = ${injectCall};`;
859
+ }
860
+ else if (member.type === 'observable') {
861
+ if (member.wrappedInToSignal) {
862
+ return `${indentation}private ${member.name} = ${injectCall};`;
863
+ }
864
+ else {
865
+ return `${indentation}private ${member.name} = toObservable(${injectCall});`;
866
+ }
867
+ }
868
+ return '';
869
+ })
870
+ .filter(Boolean)
871
+ .join('\n');
872
+ // Insert the new members with proper newlines
873
+ let prefix;
874
+ let suffix;
875
+ if (insertAfterViewportService) {
876
+ // Insert after ViewportService property
877
+ prefix = '\n';
878
+ suffix = '\n';
879
+ }
880
+ else if (classNode.members.length > 0) {
881
+ // Insert before first member
882
+ prefix = '';
883
+ suffix = '\n';
884
+ }
885
+ else {
886
+ // Insert in empty class body
887
+ prefix = '\n';
888
+ suffix = '\n';
889
+ }
890
+ return content.slice(0, finalInsertPosition) + prefix + newMembers + suffix + content.slice(finalInsertPosition);
891
+ }
892
+ function removeViewportServiceInjection(sourceFile, content, viewportServiceVars, filePath) {
893
+ let updatedContent = content;
894
+ 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: '' });
918
+ }
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;
943
+ }
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
+ });
953
+ }
954
+ });
955
+ }
956
+ });
957
+ // Sort modifications by start position (descending) to apply from end to start
958
+ modifications.sort((a, b) => b.start - a.start);
959
+ // Apply modifications from end to start
960
+ for (const mod of modifications) {
961
+ updatedContent = updatedContent.slice(0, mod.start) + mod.replacement + updatedContent.slice(mod.end);
962
+ }
963
+ return updatedContent;
964
+ }
965
+ function findMatchingParen(text, openIndex) {
966
+ let depth = 1;
967
+ let i = openIndex + 1;
968
+ while (i < text.length && depth > 0) {
969
+ if (text[i] === '(') {
970
+ depth++;
971
+ }
972
+ else if (text[i] === ')') {
973
+ depth--;
974
+ }
975
+ i++;
976
+ }
977
+ return depth === 0 ? i - 1 : -1;
978
+ }
979
+ function generateNameFromArgs(methodName, args, type) {
980
+ // Parse the arguments to generate a meaningful name
981
+ // e.g., { min: 'lg' } => isMinLg or isMinLg$
982
+ // e.g., { max: 'sm' } => isMaxSm or isMaxSm$
983
+ try {
984
+ // Simple parsing for common patterns
985
+ const minMatch = args.match(/min:\s*['"](\w+)['"]/);
986
+ const maxMatch = args.match(/max:\s*['"](\w+)['"]/);
987
+ let name = 'breakpoint';
988
+ if (minMatch) {
989
+ const breakpoint = minMatch[1];
990
+ name = `isMin${breakpoint.charAt(0).toUpperCase()}${breakpoint.slice(1)}`;
991
+ }
992
+ else if (maxMatch) {
993
+ const breakpoint = maxMatch[1];
994
+ name = `isMax${breakpoint.charAt(0).toUpperCase()}${breakpoint.slice(1)}`;
995
+ }
996
+ else if (args.includes('min') && args.includes('max')) {
997
+ name = 'isInRange';
998
+ }
999
+ return type === 'observable' ? `${name}$` : name;
1000
+ }
1001
+ catch {
1002
+ // Fallback to generic name
1003
+ return type === 'observable' ? 'breakpoint$' : 'breakpoint';
1004
+ }
1005
+ }
1006
+ function createReplacementsForMember(sourceFile, classNode, viewportServiceVar, originalProperty, memberName, type, context, args) {
1007
+ function visitNode(node) {
1008
+ // Handle property access: this.viewportService.isXs or this._viewportService.isXs$
1009
+ if (ts.isPropertyAccessExpression(node)) {
1010
+ const propertyName = node.name.text;
1011
+ const methodName = originalProperty.replace(/\(.*\)$/, '');
1012
+ // Check if this matches our property (with or without $)
1013
+ const matches = args
1014
+ ? false // For method calls, only match through call expressions
1015
+ : propertyName === methodName || propertyName === methodName.replace(/\$$/, '');
1016
+ if (matches) {
1017
+ // Check if it's accessing the SPECIFIC viewportService variable
1018
+ const isCorrectViewportAccess = (ts.isIdentifier(node.expression) && node.expression.text === viewportServiceVar) ||
1019
+ (ts.isPropertyAccessExpression(node.expression) &&
1020
+ node.expression.expression.kind === ts.SyntaxKind.ThisKeyword &&
1021
+ ts.isIdentifier(node.expression.name) &&
1022
+ node.expression.name.text === viewportServiceVar);
1023
+ if (isCorrectViewportAccess) {
1024
+ const originalText = node.getText(sourceFile);
1025
+ const replacement = type === 'signal' ? `this.${memberName}()` : `this.${memberName}`;
1026
+ // Only add replacement if we haven't already added it
1027
+ if (!context.replacements.has(originalText)) {
1028
+ context.replacements.set(originalText, replacement);
1029
+ }
1030
+ }
1031
+ }
1032
+ }
1033
+ // Handle method calls: this.viewportService.observe(...) or this._viewportService.observe(...)
1034
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
1035
+ const methodName = node.expression.name.text;
1036
+ const callArgs = node.arguments.map((arg) => arg.getText(sourceFile)).join(', ');
1037
+ // Match the original property name and arguments
1038
+ const uniqueKey = `${methodName}(${callArgs})`;
1039
+ if (originalProperty === uniqueKey) {
1040
+ // Check if it's accessing the SPECIFIC viewportService variable
1041
+ const isCorrectViewportAccess = (ts.isIdentifier(node.expression.expression) && node.expression.expression.text === viewportServiceVar) ||
1042
+ (ts.isPropertyAccessExpression(node.expression.expression) &&
1043
+ node.expression.expression.expression.kind === ts.SyntaxKind.ThisKeyword &&
1044
+ ts.isIdentifier(node.expression.expression.name) &&
1045
+ node.expression.expression.name.text === viewportServiceVar);
1046
+ if (isCorrectViewportAccess) {
1047
+ const originalText = node.getText(sourceFile);
1048
+ const replacement = type === 'signal' ? `this.${memberName}()` : `this.${memberName}`;
1049
+ if (!context.replacements.has(originalText)) {
1050
+ context.replacements.set(originalText, replacement);
1051
+ }
1052
+ }
1053
+ }
1054
+ }
1055
+ ts.forEachChild(node, visitNode);
1056
+ }
1057
+ classNode.members.forEach((member) => {
1058
+ if (ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name) && member.name.text === viewportServiceVar) {
1059
+ return;
1060
+ }
1061
+ visitNode(member);
1062
+ });
1063
+ }
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
+ function findAvailableMemberName(baseName, existingMembers) {
1094
+ // baseName already has $ suffix for observables at this point
1095
+ // Try variations: name, _name, #name
1096
+ const candidates = [baseName, `_${baseName}`, `#${baseName}`];
1097
+ for (const candidate of candidates) {
1098
+ if (!existingMembers.has(candidate)) {
1099
+ return candidate;
1100
+ }
1101
+ }
1102
+ // If all taken, add a number suffix
1103
+ let counter = 2;
1104
+ while (existingMembers.has(`${baseName}${counter}`)) {
1105
+ counter++;
1106
+ }
1107
+ return `${baseName}${counter}`;
1108
+ }
1109
+ function migrateMonitorViewport(sourceFile, content, viewportServiceVars, cssVariablesUsed) {
1110
+ let updatedContent = content;
1111
+ const imports = [];
1112
+ const monitorViewportCalls = [];
1113
+ // First pass: find all monitorViewport calls and their containing classes
1114
+ function findMonitorViewportCalls(node, currentClass) {
1115
+ if (ts.isClassDeclaration(node)) {
1116
+ currentClass = node;
1117
+ }
1118
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
1119
+ const methodName = node.expression.name.text;
1120
+ if (methodName === 'monitorViewport') {
1121
+ const expression = node.expression.expression;
1122
+ const isViewportServiceCall = (ts.isPropertyAccessExpression(expression) && viewportServiceVars.includes(expression.name.text)) ||
1123
+ (ts.isIdentifier(expression) && viewportServiceVars.includes(expression.text));
1124
+ if (isViewportServiceCall && currentClass) {
1125
+ // Store the call location
1126
+ const callStart = node.getStart(sourceFile, true);
1127
+ const callEnd = node.getEnd();
1128
+ // Find the line boundaries to remove the entire statement
1129
+ let lineStart = callStart;
1130
+ while (lineStart > 0 && content[lineStart - 1] !== '\n') {
1131
+ lineStart--;
1132
+ }
1133
+ let lineEnd = callEnd;
1134
+ // Look for semicolon
1135
+ while (lineEnd < content.length && content[lineEnd] !== ';' && content[lineEnd] !== '\n') {
1136
+ lineEnd++;
1137
+ }
1138
+ if (content[lineEnd] === ';') {
1139
+ lineEnd++; // Include semicolon
1140
+ }
1141
+ // Include trailing whitespace and newline
1142
+ while (lineEnd < content.length && (content[lineEnd] === ' ' || content[lineEnd] === '\t')) {
1143
+ lineEnd++;
1144
+ }
1145
+ if (content[lineEnd] === '\n') {
1146
+ lineEnd++;
1147
+ }
1148
+ monitorViewportCalls.push({
1149
+ start: lineStart,
1150
+ end: lineEnd,
1151
+ node,
1152
+ classNode: currentClass,
1153
+ });
1154
+ }
1155
+ }
1156
+ }
1157
+ ts.forEachChild(node, (child) => findMonitorViewportCalls(child, currentClass));
1158
+ }
1159
+ sourceFile.forEachChild((node) => findMonitorViewportCalls(node));
1160
+ if (monitorViewportCalls.length === 0) {
1161
+ return { content, imports };
1162
+ }
1163
+ // 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
+ }
1187
+ // Group calls by class
1188
+ const callsByClass = new Map();
1189
+ for (const call of monitorViewportCalls) {
1190
+ if (!callsByClass.has(call.classNode)) {
1191
+ callsByClass.set(call.classNode, []);
1192
+ }
1193
+ callsByClass.get(call.classNode).push(call);
1194
+ }
1195
+ // Process each class
1196
+ const allModifications = [];
1197
+ for (const [classNode, calls] of callsByClass) {
1198
+ // Find or create constructor
1199
+ const constructor = classNode.members.find((member) => ts.isConstructorDeclaration(member));
1200
+ if (constructor) {
1201
+ // Add to existing constructor at the beginning
1202
+ const constructorBody = constructor.body;
1203
+ if (constructorBody) {
1204
+ const bodyStart = constructorBody.getStart(sourceFile);
1205
+ // Find position after opening brace
1206
+ const openBrace = content.indexOf('{', bodyStart);
1207
+ const insertPosition = openBrace + 1;
1208
+ // Get indentation from existing constructor body
1209
+ let indentation = ' '; // default
1210
+ if (constructorBody.statements.length > 0) {
1211
+ const firstStatement = constructorBody.statements[0];
1212
+ const statementStart = firstStatement.getStart(sourceFile, true);
1213
+ const lineStart = content.lastIndexOf('\n', statementStart) + 1;
1214
+ const leadingWhitespace = content.slice(lineStart, statementStart);
1215
+ if (leadingWhitespace.trim() === '') {
1216
+ indentation = leadingWhitespace;
1217
+ }
1218
+ }
1219
+ const statementsText = constructorStatements.map((stmt) => `${indentation}${stmt}`).join('\n');
1220
+ const insertion = `\n${statementsText}\n`;
1221
+ allModifications.push({
1222
+ start: insertPosition,
1223
+ end: insertPosition,
1224
+ replacement: insertion,
1225
+ });
1226
+ }
1227
+ }
1228
+ else {
1229
+ // Create new constructor
1230
+ // Find where to insert it - at the beginning of class body
1231
+ const classStart = classNode.getStart(sourceFile);
1232
+ const classText = classNode.getText(sourceFile);
1233
+ const openBraceIndex = classText.indexOf('{');
1234
+ if (openBraceIndex === -1) {
1235
+ console.warn('Could not find class body opening brace');
1236
+ continue;
1237
+ }
1238
+ const insertPosition = classStart + openBraceIndex + 1;
1239
+ // Determine indentation
1240
+ let indentation = ' ';
1241
+ if (classNode.members.length > 0) {
1242
+ const firstMember = classNode.members[0];
1243
+ const memberStart = firstMember.getStart(sourceFile, true);
1244
+ const lineStart = content.lastIndexOf('\n', memberStart) + 1;
1245
+ const leadingWhitespace = content.slice(lineStart, memberStart);
1246
+ if (leadingWhitespace.trim() === '') {
1247
+ indentation = leadingWhitespace;
1248
+ }
1249
+ }
1250
+ const bodyIndentation = indentation + ' ';
1251
+ const statementsText = constructorStatements.map((stmt) => `${bodyIndentation}${stmt}`).join('\n');
1252
+ const constructorText = `\n${indentation}constructor() {\n${statementsText}\n${indentation}}\n`;
1253
+ allModifications.push({
1254
+ start: insertPosition,
1255
+ end: insertPosition,
1256
+ replacement: constructorText,
1257
+ });
1258
+ }
1259
+ // Mark monitorViewport calls for removal
1260
+ for (const call of calls) {
1261
+ allModifications.push({
1262
+ start: call.start,
1263
+ end: call.end,
1264
+ replacement: '',
1265
+ });
1266
+ }
1267
+ }
1268
+ // Sort modifications by start position (descending) and apply
1269
+ allModifications.sort((a, b) => b.start - a.start);
1270
+ for (const mod of allModifications) {
1271
+ updatedContent = updatedContent.slice(0, mod.start) + mod.replacement + updatedContent.slice(mod.end);
1272
+ }
1273
+ return { content: updatedContent, imports };
1274
+ }
1275
+ function addImportsToPackage(sourceFile, content, neededImports, packageName) {
1276
+ const existingImport = sourceFile.statements.find((statement) => ts.isImportDeclaration(statement) &&
1277
+ ts.isStringLiteral(statement.moduleSpecifier) &&
1278
+ statement.moduleSpecifier.text === packageName);
1279
+ const importsToAdd = Array.from(neededImports).sort();
1280
+ 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));
1283
+ if (newImports.length > 0) {
1284
+ const allImports = [...existingImportNames, ...newImports].sort();
1285
+ const importText = existingImport.getText(sourceFile);
1286
+ const newImportText = `import { ${allImports.join(', ')} } from '${packageName}';`;
1287
+ return content.replace(importText, newImportText);
1288
+ }
1289
+ }
1290
+ else if (!existingImport) {
1291
+ const firstStatement = sourceFile.statements[0];
1292
+ const insertPosition = firstStatement?.getStart(sourceFile) ?? 0;
1293
+ const newImportText = `import { ${importsToAdd.join(', ')} } from '${packageName}';\n`;
1294
+ return content.slice(0, insertPosition) + newImportText + content.slice(insertPosition);
1295
+ }
1296
+ return content;
1297
+ }
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) {
1379
+ const variables = [];
1380
+ function visit(node) {
1381
+ // Check for inject(ViewportService) pattern
1382
+ if (ts.isCallExpression(node) &&
1383
+ ts.isIdentifier(node.expression) &&
1384
+ node.expression.text === 'inject' &&
1385
+ node.arguments.length > 0) {
1386
+ const arg = node.arguments[0];
1387
+ if (ts.isIdentifier(arg) && arg.text === 'ViewportService') {
1388
+ // Find the variable name
1389
+ let parent = node.parent;
1390
+ while (parent) {
1391
+ if (ts.isPropertyDeclaration(parent) && ts.isIdentifier(parent.name)) {
1392
+ variables.push(parent.name.text);
1393
+ break;
1394
+ }
1395
+ if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
1396
+ variables.push(parent.name.text);
1397
+ break;
1398
+ }
1399
+ parent = parent.parent;
1400
+ }
1401
+ }
1402
+ }
1403
+ // Check for constructor parameter injection
1404
+ if (ts.isParameter(node)) {
1405
+ const typeNode = node.type;
1406
+ if (typeNode && ts.isTypeReferenceNode(typeNode) && ts.isIdentifier(typeNode.typeName)) {
1407
+ if (typeNode.typeName.text === 'ViewportService' && node.name && ts.isIdentifier(node.name)) {
1408
+ variables.push(node.name.text);
1409
+ }
1410
+ }
1411
+ }
1412
+ ts.forEachChild(node, visit);
1413
+ }
1414
+ visit(sourceFile);
1415
+ return variables;
1416
+ }
1417
+ function handleInlineInjectPatterns(sourceFile, content) {
1418
+ const importsNeeded = new Set();
1419
+ let updatedContent = content;
1420
+ // Get the ViewportService package to determine mapping
1421
+ const packageName = getViewportServicePackage(sourceFile);
1422
+ const { signalPropertyMap, observablePropertyMap } = getPropertyMaps(packageName);
1423
+ // Method map
1424
+ const methodMap = {
1425
+ observe: { injectFn: 'injectObserveBreakpoint', type: 'observable' },
1426
+ isMatched: { injectFn: 'injectBreakpointIsMatched', type: 'signal' },
1427
+ build: { injectFn: 'injectObserveBreakpoint', type: 'observable' },
1428
+ };
1429
+ // Track processed nodes to avoid double processing
1430
+ const processedNodes = new Set();
1431
+ function isInsideToSignal(node) {
1432
+ let parent = node.parent;
1433
+ while (parent) {
1434
+ if (ts.isCallExpression(parent) && ts.isIdentifier(parent.expression) && parent.expression.text === 'toSignal') {
1435
+ return true;
1436
+ }
1437
+ parent = parent.parent;
1438
+ }
1439
+ return false;
1440
+ }
1441
+ function visitNode(node) {
1442
+ // Look for: toSignal(inject(ViewportService).property)
1443
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'toSignal') {
1444
+ const arg = node.arguments[0];
1445
+ // Handle property access: toSignal(inject(ViewportService).isXs$)
1446
+ if (arg && ts.isPropertyAccessExpression(arg)) {
1447
+ const propertyName = arg.name.text;
1448
+ if (ts.isCallExpression(arg.expression) &&
1449
+ ts.isIdentifier(arg.expression.expression) &&
1450
+ arg.expression.expression.text === 'inject' &&
1451
+ arg.expression.arguments.length > 0) {
1452
+ const injectArg = arg.expression.arguments[0];
1453
+ if (ts.isIdentifier(injectArg) && injectArg.text === 'ViewportService') {
1454
+ const injectFn = signalPropertyMap[propertyName] || observablePropertyMap[propertyName];
1455
+ if (injectFn) {
1456
+ const oldText = node.getText(sourceFile);
1457
+ const newText = `${injectFn}()`;
1458
+ updatedContent = updatedContent.replace(oldText, newText);
1459
+ importsNeeded.add(injectFn);
1460
+ processedNodes.add(arg);
1461
+ }
1462
+ }
1463
+ }
1464
+ }
1465
+ // Handle method calls: toSignal(inject(ViewportService).observe(...))
1466
+ if (arg && ts.isCallExpression(arg) && ts.isPropertyAccessExpression(arg.expression)) {
1467
+ const methodName = arg.expression.name.text;
1468
+ const methodInfo = methodMap[methodName];
1469
+ if (methodInfo &&
1470
+ ts.isCallExpression(arg.expression.expression) &&
1471
+ ts.isIdentifier(arg.expression.expression.expression) &&
1472
+ arg.expression.expression.expression.text === 'inject' &&
1473
+ arg.expression.expression.arguments.length > 0) {
1474
+ const injectArg = arg.expression.expression.arguments[0];
1475
+ if (ts.isIdentifier(injectArg) && injectArg.text === 'ViewportService') {
1476
+ const oldText = node.getText(sourceFile);
1477
+ // Extract arguments from the method call
1478
+ const args = arg.arguments.map((a) => a.getText(sourceFile)).join(', ');
1479
+ // Since it's wrapped in toSignal and our inject functions return signals, remove toSignal
1480
+ const newText = `${methodInfo.injectFn}(${args})`;
1481
+ updatedContent = updatedContent.replace(oldText, newText);
1482
+ importsNeeded.add(methodInfo.injectFn);
1483
+ processedNodes.add(arg);
1484
+ }
1485
+ }
1486
+ }
1487
+ }
1488
+ // Look for: inject(ViewportService).observe(...) (NOT wrapped in toSignal)
1489
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
1490
+ if (processedNodes.has(node)) {
1491
+ ts.forEachChild(node, visitNode);
1492
+ return;
1493
+ }
1494
+ const methodName = node.expression.name.text;
1495
+ const methodInfo = methodMap[methodName];
1496
+ if (methodInfo &&
1497
+ ts.isCallExpression(node.expression.expression) &&
1498
+ ts.isIdentifier(node.expression.expression.expression) &&
1499
+ node.expression.expression.expression.text === 'inject' &&
1500
+ node.expression.expression.arguments.length > 0) {
1501
+ const injectArg = node.expression.expression.arguments[0];
1502
+ if (ts.isIdentifier(injectArg) && injectArg.text === 'ViewportService') {
1503
+ if (isInsideToSignal(node)) {
1504
+ ts.forEachChild(node, visitNode);
1505
+ return;
1506
+ }
1507
+ const oldText = node.getText(sourceFile);
1508
+ const args = node.arguments.map((arg) => arg.getText(sourceFile)).join(', ');
1509
+ // Only wrap with toObservable if the assigned variable/property ends with $
1510
+ let needsToObservable = false;
1511
+ const parent = node.parent;
1512
+ if ((ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name) && parent.name.text.endsWith('$')) ||
1513
+ (ts.isPropertyDeclaration(parent) && ts.isIdentifier(parent.name) && parent.name.text.endsWith('$'))) {
1514
+ needsToObservable = true;
1515
+ }
1516
+ let newText = `${methodInfo.injectFn}(${args})`;
1517
+ if (needsToObservable) {
1518
+ newText = `toObservable(${newText})`;
1519
+ importsNeeded.add('toObservable');
1520
+ }
1521
+ updatedContent = updatedContent.replace(oldText, newText);
1522
+ importsNeeded.add(methodInfo.injectFn);
1523
+ }
1524
+ }
1525
+ }
1526
+ // Look for standalone: inject(ViewportService).property (not wrapped in toSignal)
1527
+ if (ts.isPropertyAccessExpression(node)) {
1528
+ if (processedNodes.has(node)) {
1529
+ ts.forEachChild(node, visitNode);
1530
+ return;
1531
+ }
1532
+ const propertyName = node.name.text;
1533
+ if (ts.isCallExpression(node.expression) &&
1534
+ ts.isIdentifier(node.expression.expression) &&
1535
+ node.expression.expression.text === 'inject' &&
1536
+ node.expression.arguments.length > 0) {
1537
+ const injectArg = node.expression.arguments[0];
1538
+ if (ts.isIdentifier(injectArg) && injectArg.text === 'ViewportService') {
1539
+ const injectFn = signalPropertyMap[propertyName] || observablePropertyMap[propertyName];
1540
+ if (injectFn) {
1541
+ if (isInsideToSignal(node)) {
1542
+ ts.forEachChild(node, visitNode);
1543
+ return;
1544
+ }
1545
+ const oldText = node.getText(sourceFile);
1546
+ // Check if this is an observable property (ends with $)
1547
+ const isObservable = !!observablePropertyMap[propertyName];
1548
+ let newText = `${injectFn}()`;
1549
+ if (isObservable) {
1550
+ newText = `toObservable(${newText})`;
1551
+ importsNeeded.add('toObservable');
1552
+ }
1553
+ updatedContent = updatedContent.replace(oldText, newText);
1554
+ importsNeeded.add(injectFn);
1555
+ }
1556
+ }
1557
+ }
1558
+ }
1559
+ ts.forEachChild(node, visitNode);
1560
+ }
1561
+ sourceFile.forEachChild(visitNode);
1562
+ return { content: updatedContent, importsNeeded };
1563
+ }
1564
+ function removeUnusedImports(sourceFile, content) {
1565
+ const importsToCheck = [
1566
+ { specifier: 'toSignal', package: '@angular/core/rxjs-interop' },
1567
+ { specifier: 'toObservable', package: '@angular/core/rxjs-interop' },
1568
+ ];
1569
+ let updatedContent = content;
1570
+ for (const importToCheck of importsToCheck) {
1571
+ // Check if the import specifier is used anywhere in the code
1572
+ const isUsed = checkIfImportIsUsed(sourceFile, content, importToCheck.specifier);
1573
+ if (!isUsed) {
1574
+ updatedContent = removeImportSpecifier(sourceFile, updatedContent, importToCheck.specifier, importToCheck.package);
1575
+ }
1576
+ }
1577
+ return updatedContent;
1578
+ }
1579
+ function checkIfImportIsUsed(sourceFile, content, specifier) {
1580
+ let isUsed = false;
1581
+ function visit(node) {
1582
+ if (isUsed)
1583
+ return;
1584
+ // Check for identifier usage (not in import declarations)
1585
+ if (ts.isIdentifier(node) && node.text === specifier) {
1586
+ // Make sure it's not part of an import declaration
1587
+ let parent = node.parent;
1588
+ while (parent) {
1589
+ if (ts.isImportDeclaration(parent)) {
1590
+ return;
1591
+ }
1592
+ parent = parent.parent;
1593
+ }
1594
+ isUsed = true;
1595
+ return;
1596
+ }
1597
+ ts.forEachChild(node, visit);
1598
+ }
1599
+ visit(sourceFile);
1600
+ return isUsed;
1601
+ }
1602
+ function removeImportSpecifier(sourceFile, content, specifier, packageName) {
1603
+ let updatedContent = content;
1604
+ sourceFile.forEachChild((node) => {
1605
+ if (ts.isImportDeclaration(node) &&
1606
+ ts.isStringLiteral(node.moduleSpecifier) &&
1607
+ node.moduleSpecifier.text === packageName) {
1608
+ if (node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings)) {
1609
+ const elements = node.importClause.namedBindings.elements;
1610
+ const remainingElements = elements.filter((el) => el.name.text !== specifier);
1611
+ if (remainingElements.length === 0) {
1612
+ // Remove entire import statement
1613
+ const importStart = node.getStart(sourceFile, true);
1614
+ const importEnd = node.getEnd();
1615
+ // Find line boundaries
1616
+ let lineStart = importStart;
1617
+ while (lineStart > 0 && content[lineStart - 1] !== '\n') {
1618
+ lineStart--;
1619
+ }
1620
+ let lineEnd = importEnd;
1621
+ while (lineEnd < content.length && content[lineEnd] !== '\n') {
1622
+ lineEnd++;
1623
+ }
1624
+ if (lineEnd < content.length)
1625
+ lineEnd++; // Include the newline
1626
+ updatedContent = content.slice(0, lineStart) + content.slice(lineEnd);
1627
+ }
1628
+ else if (remainingElements.length < elements.length) {
1629
+ // Remove just the specifier
1630
+ const importText = node.getText(sourceFile);
1631
+ const namedImportsText = node.importClause.namedBindings.getText(sourceFile);
1632
+ // Rebuild named imports
1633
+ const newNamedImports = remainingElements.map((el) => el.getText(sourceFile)).join(', ');
1634
+ const newImportText = importText.replace(namedImportsText, `{ ${newNamedImports} }`);
1635
+ const importStart = node.getStart(sourceFile);
1636
+ const importEnd = node.getEnd();
1637
+ updatedContent = content.slice(0, importStart) + newImportText + content.slice(importEnd);
1638
+ }
1639
+ }
1640
+ }
1641
+ });
1642
+ return updatedContent;
1643
+ }
1644
+ function checkIfPropertyIsUsedElsewhere(sourceFile, classNode, propertyName, propertyDecl) {
1645
+ let isUsed = false;
1646
+ function visit(node) {
1647
+ if (isUsed)
1648
+ return;
1649
+ // Skip the property declaration itself
1650
+ if (node === propertyDecl) {
1651
+ return;
1652
+ }
1653
+ // Check for property access like this.propertyName
1654
+ if (ts.isPropertyAccessExpression(node)) {
1655
+ if (node.expression.kind === ts.SyntaxKind.ThisKeyword &&
1656
+ ts.isIdentifier(node.name) &&
1657
+ node.name.text === propertyName) {
1658
+ isUsed = true;
1659
+ return;
1660
+ }
1661
+ }
1662
+ // Check for identifier usage (in case it's accessed without 'this')
1663
+ if (ts.isIdentifier(node) && node.text === propertyName) {
1664
+ // Make sure it's not the property name in the declaration
1665
+ const parent = node.parent;
1666
+ if (!ts.isPropertyDeclaration(parent) || parent !== propertyDecl) {
1667
+ isUsed = true;
1668
+ return;
1669
+ }
1670
+ }
1671
+ ts.forEachChild(node, visit);
1672
+ }
1673
+ classNode.members.forEach((member) => {
1674
+ visit(member);
1675
+ });
1676
+ return isUsed;
1677
+ }
1678
+ //# sourceMappingURL=viewport-service.js.map