@ethlete/core 4.30.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.
- package/CHANGELOG.md +73 -0
- package/fesm2022/ethlete-core.mjs +4019 -4810
- package/fesm2022/ethlete-core.mjs.map +1 -1
- package/generators/generators.json +14 -0
- package/generators/migrate-to-v5/create-provider.js +158 -0
- package/generators/migrate-to-v5/migration.js +28 -0
- package/generators/migrate-to-v5/router-state-service.js +1064 -0
- package/generators/migrate-to-v5/schema.json +29 -0
- package/generators/migrate-to-v5/viewport-service.js +1678 -0
- package/generators/tailwind-4-theme/generator.js +490 -0
- package/generators/tailwind-4-theme/schema.json +32 -0
- package/package.json +18 -11
- package/types/ethlete-core.d.ts +2161 -0
- package/index.d.ts +0 -1968
|
@@ -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
|