@ethlete/cdk 4.70.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,514 @@
1
+ import { visitNotIgnoredFiles } from '@nx/devkit';
2
+ import * as ts from 'typescript';
3
+ export function migrateEtLet(tree) {
4
+ console.log('\nšŸ”„ Migrating *etLet and *ngLet');
5
+ let filesModified = 0;
6
+ let directivesConverted = 0;
7
+ let importsRemoved = 0;
8
+ const renamedVariables = [];
9
+ visitNotIgnoredFiles(tree, '', (filePath) => {
10
+ if (!filePath.endsWith('.html') && !filePath.endsWith('.component.ts')) {
11
+ return;
12
+ }
13
+ const content = tree.read(filePath, 'utf-8');
14
+ if (!content)
15
+ return;
16
+ let newContent = content;
17
+ let fileModified = false;
18
+ // Migrate directives in templates
19
+ const templateResult = migrateEtLetDirectives(newContent, filePath);
20
+ if (templateResult.content !== newContent) {
21
+ newContent = templateResult.content;
22
+ directivesConverted += templateResult.convertedCount;
23
+ renamedVariables.push(...templateResult.renamedVariables);
24
+ fileModified = true;
25
+ }
26
+ // Remove imports from TypeScript files
27
+ if (filePath.endsWith('.ts')) {
28
+ const importResult = removeLetDirectiveImports(newContent, filePath);
29
+ if (importResult.content !== newContent) {
30
+ newContent = importResult.content;
31
+ importsRemoved += importResult.removedCount;
32
+ fileModified = true;
33
+ }
34
+ }
35
+ if (fileModified) {
36
+ tree.write(filePath, newContent);
37
+ filesModified++;
38
+ }
39
+ });
40
+ function migrateEtLetDirectives(content, filePath) {
41
+ let result = content;
42
+ let converted = 0;
43
+ const renamedVars = [];
44
+ // Handle HTML template files
45
+ if (filePath.endsWith('.html')) {
46
+ const migration = migrateHtmlTemplate(content, filePath);
47
+ result = migration.content;
48
+ converted = migration.convertedCount;
49
+ renamedVars.push(...migration.renamedVariables);
50
+ }
51
+ // Handle inline templates in TypeScript files
52
+ if (filePath.endsWith('.component.ts')) {
53
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
54
+ const replacements = [];
55
+ function visit(node) {
56
+ if (ts.isPropertyAssignment(node)) {
57
+ if (ts.isIdentifier(node.name) &&
58
+ node.name.text === 'template' &&
59
+ (ts.isStringLiteral(node.initializer) || ts.isNoSubstitutionTemplateLiteral(node.initializer))) {
60
+ const templateContent = node.initializer.getText(sourceFile);
61
+ const templateText = templateContent.slice(1, -1); // Remove quotes
62
+ if (templateText.includes('*etLet=') || templateText.includes('*ngLet=')) {
63
+ const migration = migrateHtmlTemplate(templateText, filePath);
64
+ if (migration.convertedCount > 0) {
65
+ const quote = templateContent[0];
66
+ replacements.push({
67
+ start: node.initializer.getStart(sourceFile),
68
+ end: node.initializer.getEnd(),
69
+ replacement: `${quote}${migration.content}${quote}`,
70
+ });
71
+ converted += migration.convertedCount;
72
+ renamedVars.push(...migration.renamedVariables);
73
+ }
74
+ }
75
+ }
76
+ }
77
+ ts.forEachChild(node, visit);
78
+ }
79
+ visit(sourceFile);
80
+ if (replacements.length > 0) {
81
+ replacements.sort((a, b) => b.start - a.start);
82
+ for (const { start, end, replacement } of replacements) {
83
+ result = result.slice(0, start) + replacement + result.slice(end);
84
+ }
85
+ }
86
+ }
87
+ if (converted > 0) {
88
+ console.log(` āœ“ ${filePath}: converted ${converted} directive(s)`);
89
+ }
90
+ return { content: result, convertedCount: converted, renamedVariables: renamedVars };
91
+ }
92
+ function removeLetDirectiveImports(content, filePath) {
93
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
94
+ const replacements = [];
95
+ let removedCount = 0;
96
+ function visit(node) {
97
+ // Remove from imports array in @Component decorator
98
+ if (ts.isDecorator(node)) {
99
+ const expression = node.expression;
100
+ if (ts.isCallExpression(expression) && ts.isIdentifier(expression.expression)) {
101
+ if (expression.expression.text === 'Component' && expression.arguments.length > 0) {
102
+ const arg = expression.arguments[0];
103
+ if (ts.isObjectLiteralExpression(arg)) {
104
+ const importsProperty = arg.properties.find((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === 'imports');
105
+ if (importsProperty && ts.isArrayLiteralExpression(importsProperty.initializer)) {
106
+ const importsArray = importsProperty.initializer;
107
+ const filteredElements = importsArray.elements.filter((el) => {
108
+ if (ts.isIdentifier(el)) {
109
+ return el.text !== 'LetDirective' && el.text !== 'NgLetDirective';
110
+ }
111
+ return true;
112
+ });
113
+ if (filteredElements.length !== importsArray.elements.length) {
114
+ const removedElements = importsArray.elements.length - filteredElements.length;
115
+ removedCount += removedElements;
116
+ if (filteredElements.length === 0) {
117
+ // Remove entire imports property
118
+ const propertyStart = importsProperty.getStart(sourceFile);
119
+ const propertyEnd = importsProperty.getEnd();
120
+ // Check if there's a comma after
121
+ const nextChar = content[propertyEnd];
122
+ const endPos = nextChar === ',' ? propertyEnd + 1 : propertyEnd;
123
+ replacements.push({
124
+ start: propertyStart,
125
+ end: endPos,
126
+ replacement: '',
127
+ });
128
+ }
129
+ else {
130
+ // Replace with filtered array
131
+ const newArray = `imports: [${filteredElements.map((el) => el.getText(sourceFile)).join(', ')}]`;
132
+ replacements.push({
133
+ start: importsProperty.getStart(sourceFile),
134
+ end: importsProperty.getEnd(),
135
+ replacement: newArray,
136
+ });
137
+ }
138
+ }
139
+ }
140
+ }
141
+ }
142
+ }
143
+ }
144
+ // Remove from import statements
145
+ if (ts.isImportDeclaration(node) && node.importClause?.namedBindings) {
146
+ if (ts.isNamedImports(node.importClause.namedBindings)) {
147
+ const elements = node.importClause.namedBindings.elements;
148
+ const filteredElements = elements.filter((el) => el.name.text !== 'LetDirective' && el.name.text !== 'NgLetDirective');
149
+ if (filteredElements.length !== elements.length) {
150
+ const removedElements = elements.length - filteredElements.length;
151
+ removedCount += removedElements;
152
+ if (filteredElements.length === 0) {
153
+ // Remove entire import statement
154
+ const importStart = node.getStart(sourceFile);
155
+ const importEnd = node.getEnd();
156
+ const nextChar = content[importEnd];
157
+ const endPos = nextChar === '\n' ? importEnd + 1 : importEnd;
158
+ replacements.push({
159
+ start: importStart,
160
+ end: endPos,
161
+ replacement: '',
162
+ });
163
+ }
164
+ else {
165
+ // Replace with filtered imports
166
+ const moduleSpecifier = node.moduleSpecifier.text;
167
+ const newImport = `import { ${filteredElements.map((el) => el.name.text).join(', ')} } from '${moduleSpecifier}';`;
168
+ replacements.push({
169
+ start: node.getStart(sourceFile),
170
+ end: node.getEnd(),
171
+ replacement: newImport,
172
+ });
173
+ }
174
+ }
175
+ }
176
+ }
177
+ ts.forEachChild(node, visit);
178
+ }
179
+ visit(sourceFile);
180
+ let result = content;
181
+ if (replacements.length > 0) {
182
+ replacements.sort((a, b) => b.start - a.start);
183
+ for (const { start, end, replacement } of replacements) {
184
+ result = result.slice(0, start) + replacement + result.slice(end);
185
+ }
186
+ }
187
+ if (removedCount > 0) {
188
+ console.log(` āœ“ ${filePath}: removed ${removedCount} directive import(s)`);
189
+ }
190
+ return { content: result, removedCount };
191
+ }
192
+ function migrateHtmlTemplate(html, filePath) {
193
+ const startTime = Date.now();
194
+ let result = html;
195
+ let convertedCount = 0;
196
+ let hasMatches = true;
197
+ let iterations = 0;
198
+ const maxIterations = 10000;
199
+ let lastLogTime = startTime;
200
+ const logInterval = 2000;
201
+ // Track variable names to prevent duplicates
202
+ const usedVariables = new Map(); // variable name -> count
203
+ const renamedVars = [];
204
+ while (hasMatches && iterations < maxIterations) {
205
+ iterations++;
206
+ const currentTime = Date.now();
207
+ if (currentTime - lastLogTime > logInterval) {
208
+ const elapsed = ((currentTime - startTime) / 1000).toFixed(1);
209
+ const fileInfo = filePath ? ` (${filePath})` : '';
210
+ console.log(` ā³ Processing${fileInfo}: ${convertedCount} directives converted, iteration ${iterations}/${maxIterations}, ${elapsed}s elapsed...`);
211
+ lastLogTime = currentTime;
212
+ }
213
+ const letRegex = /\*(etLet|ngLet)="([\s\S]+?)\s+as\s+(\w+)\s*"/;
214
+ const match = letRegex.exec(result);
215
+ if (!match) {
216
+ // eslint-disable-next-line no-useless-assignment
217
+ hasMatches = false;
218
+ break;
219
+ }
220
+ const directive = match[1];
221
+ const rawExpression = match[2];
222
+ const expression = rawExpression.replace(/\s+/g, ' ').trim();
223
+ const originalVariable = match[3];
224
+ let variable = originalVariable;
225
+ const index = match.index;
226
+ // Check if variable name matches the expression (self-reference)
227
+ // e.g., *ngLet="contentOverviewStore as contentOverviewStore"
228
+ // or *ngLet="shareableImage() as shareableImage" (signal call)
229
+ // This would create invalid code: @let contentOverviewStore = contentOverviewStore;
230
+ // or: @let shareableImage = shareableImage();
231
+ const isSelfReference = expression.trim() === variable.trim() || expression.trim() === `${variable.trim()}()`;
232
+ // Check for potential self-reference from expression
233
+ // e.g., *ngLet="competitions?.items as competitions" where 'competitions' might exist from *etQuery
234
+ const expressionStartsWithVariable = expression.trim().startsWith(`${variable.trim()}.`) ||
235
+ expression.trim().startsWith(`${variable.trim()}?.`) ||
236
+ expression.trim().startsWith(`${variable.trim()}[`);
237
+ const potentialExternalConflict = expressionStartsWithVariable && !isSelfReference;
238
+ // Check if this variable name is already used OR if it's a self-reference OR potential conflict
239
+ if (usedVariables.has(variable) || isSelfReference || potentialExternalConflict) {
240
+ const count = usedVariables.get(variable) || 0;
241
+ usedVariables.set(variable, count + 1);
242
+ variable = `${variable}${count + 1}`;
243
+ // Calculate approximate line number
244
+ const lineNumber = result.substring(0, index).split('\n').length;
245
+ renamedVars.push({
246
+ file: filePath || 'inline template',
247
+ original: originalVariable,
248
+ renamed: variable,
249
+ line: lineNumber,
250
+ });
251
+ // Add specific warning for potential external conflicts
252
+ if (potentialExternalConflict) {
253
+ console.warn(` āš ļø Renamed variable at ${filePath || 'template'}:${lineNumber} to avoid potential conflict`);
254
+ console.warn(` Expression "${expression}" references "${originalVariable}"`);
255
+ console.warn(` Renamed to: @let ${variable} = ${expression};`);
256
+ console.warn(` Please verify this variable doesn't conflict with other directives`);
257
+ }
258
+ }
259
+ else {
260
+ usedVariables.set(variable, 1);
261
+ }
262
+ const elementStart = result.lastIndexOf('<', index);
263
+ const elementEnd = result.indexOf('>', index) + 1;
264
+ if (elementStart === -1 || elementEnd === 0) {
265
+ console.warn(` āš ļø Could not find element boundaries for directive at index ${index}`);
266
+ break;
267
+ }
268
+ const element = result.substring(elementStart, elementEnd);
269
+ const isNgContainer = element.trim().startsWith('<ng-container');
270
+ const lineStart = result.lastIndexOf('\n', elementStart);
271
+ const indentation = lineStart === -1 ? result.substring(0, elementStart) : result.substring(lineStart + 1, elementStart);
272
+ if (isNgContainer) {
273
+ let depth = 1;
274
+ let pos = elementEnd;
275
+ let closingTagIndex = -1;
276
+ const len = result.length;
277
+ let openTagsFound = 0;
278
+ let closeTagsFound = 0;
279
+ while (depth > 0 && pos < len) {
280
+ const char = result[pos];
281
+ if (char === '<') {
282
+ if (pos + 1 < len && result[pos + 1] !== '/') {
283
+ if (pos + 13 <= len && result.substring(pos, pos + 13) === '<ng-container') {
284
+ const nextChar = pos + 13 < len ? result[pos + 13] : '';
285
+ if (nextChar === ' ' ||
286
+ nextChar === '>' ||
287
+ nextChar === '\n' ||
288
+ nextChar === '\r' ||
289
+ nextChar === '\t') {
290
+ // Check if this is a self-closing tag
291
+ const tagEnd = result.indexOf('>', pos);
292
+ if (tagEnd !== -1 && result[tagEnd - 1] === '/') {
293
+ // Self-closing tag - don't increment depth
294
+ pos = tagEnd + 1;
295
+ continue;
296
+ }
297
+ depth++;
298
+ openTagsFound++;
299
+ pos += 13;
300
+ continue;
301
+ }
302
+ }
303
+ }
304
+ else if (pos + 1 < len && result[pos + 1] === '/') {
305
+ if (pos + 15 <= len && result.substring(pos, pos + 15) === '</ng-container>') {
306
+ depth--;
307
+ closeTagsFound++;
308
+ if (depth === 0) {
309
+ closingTagIndex = pos;
310
+ break;
311
+ }
312
+ pos += 15;
313
+ continue;
314
+ }
315
+ }
316
+ }
317
+ pos++;
318
+ }
319
+ if (closingTagIndex === -1) {
320
+ console.warn(` āš ļø Could not find matching closing tag for ng-container in ${filePath || 'template'}`);
321
+ console.warn(` Variable: ${variable}, started at position ${elementStart}`);
322
+ console.warn(` Found ${openTagsFound} opening and ${closeTagsFound} closing <ng-container> tags`);
323
+ break;
324
+ }
325
+ let innerContent = result.substring(elementEnd, closingTagIndex);
326
+ // If we renamed the variable, update references in the inner content
327
+ if (variable !== originalVariable) {
328
+ // Pattern 1: Attribute values - match the full pattern [attr]="value"
329
+ // and only replace the variable in the value part, never in attribute names
330
+ innerContent = innerContent.replace(/\[([^\]]+)\]="([^"]*)"/g, (match, attrName, attrValue) => {
331
+ // Don't rename the attribute name, only references in the value
332
+ const pattern = new RegExp(`\\b${originalVariable}\\b`, 'g');
333
+ const newValue = attrValue.replace(pattern, variable);
334
+ return `[${attrName}]="${newValue}"`;
335
+ });
336
+ // Pattern 2: Event bindings like (click)="method(variable)"
337
+ innerContent = innerContent.replace(/\(([^)]+)\)="([^"]*)"/g, (match, eventName, eventHandler) => {
338
+ const pattern = new RegExp(`\\b${originalVariable}\\b`, 'g');
339
+ const newHandler = eventHandler.replace(pattern, variable);
340
+ return `(${eventName})="${newHandler}"`;
341
+ });
342
+ // Pattern 3: Interpolations like {{variable}}
343
+ const interpolationPattern = new RegExp(`(\\{\\{[^}]*?)\\b${originalVariable}\\b([^}]*?\\}\\})`, 'g');
344
+ innerContent = innerContent.replace(interpolationPattern, `$1${variable}$2`);
345
+ // Pattern 4: Control flow (@if, @for, @switch)
346
+ const controlFlowPattern = new RegExp(`(@(?:if|for|switch)\\s*\\([^)]*?)\\b${originalVariable}\\b([^)]*)\\)`, 'g');
347
+ innerContent = innerContent.replace(controlFlowPattern, `$1${variable}$2)`);
348
+ }
349
+ const letStatement = `${indentation}@let ${variable} = ${expression};\n`;
350
+ const replaceStart = lineStart === -1 ? 0 : lineStart + 1;
351
+ const beforeClosingTag = result[closingTagIndex - 1];
352
+ const hasNewlineBeforeClosing = beforeClosingTag === '\n';
353
+ const afterClosingTag = result[closingTagIndex + 15];
354
+ const hasNewlineAfterClosing = afterClosingTag === '\n';
355
+ let replacement = letStatement;
356
+ const trimmedInner = innerContent.trimEnd();
357
+ if (trimmedInner) {
358
+ replacement += trimmedInner;
359
+ if (hasNewlineBeforeClosing) {
360
+ replacement += '\n';
361
+ }
362
+ }
363
+ const skipAfterClosing = hasNewlineAfterClosing ? 16 : 15;
364
+ result = result.substring(0, replaceStart) + replacement + result.substring(closingTagIndex + skipAfterClosing);
365
+ convertedCount++;
366
+ }
367
+ else {
368
+ // For non-ng-container elements, we need to find the closing tag and update inner content too
369
+ const elementTagName = element.match(/<(\w+)/)?.[1];
370
+ let closingTag = -1;
371
+ let innerContent = '';
372
+ if (elementTagName) {
373
+ // Find the matching closing tag with proper depth tracking
374
+ let depth = 1;
375
+ let pos = elementEnd;
376
+ const len = result.length;
377
+ const openingTagPattern = new RegExp(`<${elementTagName}(?:\\s|>|/)`, 'g');
378
+ const closingTagPattern = new RegExp(`</${elementTagName}>`, 'g');
379
+ while (depth > 0 && pos < len) {
380
+ const remaining = result.substring(pos);
381
+ // Find next opening or closing tag
382
+ openingTagPattern.lastIndex = 0;
383
+ closingTagPattern.lastIndex = 0;
384
+ const nextOpening = openingTagPattern.exec(remaining);
385
+ const nextClosing = closingTagPattern.exec(remaining);
386
+ if (!nextClosing) {
387
+ // No more closing tags found
388
+ break;
389
+ }
390
+ const closingPos = pos + nextClosing.index;
391
+ const openingPos = nextOpening ? pos + nextOpening.index : Infinity;
392
+ if (openingPos < closingPos) {
393
+ // Found an opening tag before the closing tag
394
+ // Check if it's self-closing
395
+ const tagEnd = result.indexOf('>', openingPos);
396
+ if (tagEnd !== -1 && result[tagEnd - 1] === '/') {
397
+ // Self-closing, skip it
398
+ pos = tagEnd + 1;
399
+ continue;
400
+ }
401
+ depth++;
402
+ pos = openingPos + elementTagName.length + 1;
403
+ }
404
+ else {
405
+ // Found a closing tag
406
+ depth--;
407
+ if (depth === 0) {
408
+ closingTag = closingPos;
409
+ innerContent = result.substring(elementEnd, closingTag);
410
+ break;
411
+ }
412
+ pos = closingPos + elementTagName.length + 3; // </>
413
+ }
414
+ }
415
+ }
416
+ const letStatement = `${indentation}@let ${variable} = ${expression};\n`;
417
+ // Replace the directive pattern
418
+ const directivePattern = new RegExp(`\\s*\\*${directive}="[\\s\\S]+?\\s+as\\s+\\w+\\s*"\\s*`, 'g');
419
+ let elementWithoutDirective = element
420
+ .replace(directivePattern, ' ')
421
+ .replace(/\s+>/g, '>')
422
+ .replace(/\s{2,}/g, ' ');
423
+ // If we renamed the variable, update references in BOTH element attributes AND inner content
424
+ if (variable !== originalVariable) {
425
+ // We need to be careful to only replace variable references in VALUES, not in attribute NAMES
426
+ // So instead of a simple word-boundary replacement, we use specific patterns
427
+ // Pattern 1: Attribute values like [attr]="variable" or [attr]="variable.prop"
428
+ const attributeValuePattern = new RegExp(`(\\[[^\\]]+\\]\\s*=\\s*")([^"]*?)\\b${originalVariable}\\b([^"]*?")`, 'g');
429
+ elementWithoutDirective = elementWithoutDirective.replace(attributeValuePattern, `$1$2${variable}$3`);
430
+ // Pattern 2: Event bindings like (click)="method(variable)"
431
+ const eventBindingPattern = new RegExp(`(\\([^)]+\\)\\s*=\\s*")([^"]*?)\\b${originalVariable}\\b([^"]*?")`, 'g');
432
+ elementWithoutDirective = elementWithoutDirective.replace(eventBindingPattern, `$1$2${variable}$3`);
433
+ // Pattern 3: Interpolations like {{variable}}
434
+ const interpolationPattern = new RegExp(`(\\{\\{[^}]*?)\\b${originalVariable}\\b([^}]*?\\}\\})`, 'g');
435
+ elementWithoutDirective = elementWithoutDirective.replace(interpolationPattern, `$1${variable}$2`);
436
+ // Update references in inner content using specific patterns only
437
+ if (innerContent) {
438
+ // Pattern 1: Attribute values in inner content
439
+ const innerAttributePattern = new RegExp(`(\\[[^\\]]+\\]\\s*=\\s*")([^"]*?)\\b${originalVariable}\\b([^"]*?")`, 'g');
440
+ innerContent = innerContent.replace(innerAttributePattern, `$1$2${variable}$3`);
441
+ // Pattern 2: Event bindings in inner content
442
+ const innerEventPattern = new RegExp(`(\\([^)]+\\)\\s*=\\s*")([^"]*?)\\b${originalVariable}\\b([^"]*?")`, 'g');
443
+ innerContent = innerContent.replace(innerEventPattern, `$1$2${variable}$3`);
444
+ // Pattern 3: Interpolations in inner content
445
+ const innerInterpolationPattern = new RegExp(`(\\{\\{[^}]*?)\\b${originalVariable}\\b([^}]*?\\}\\})`, 'g');
446
+ innerContent = innerContent.replace(innerInterpolationPattern, `$1${variable}$2`);
447
+ // Pattern 4: Control flow in inner content
448
+ const innerControlFlowPattern = new RegExp(`(@(?:if|for|switch)\\s*\\([^)]*?)\\b${originalVariable}\\b([^)]*)\\)`, 'g');
449
+ innerContent = innerContent.replace(innerControlFlowPattern, `$1${variable}$2)`);
450
+ // Pattern 5: [ngClass] and [ngStyle] in inner content
451
+ const innerNgClassPattern = new RegExp(`(\\[ng(?:Class|Style)\\]\\s*=\\s*"\\{[^}]*?)\\b${originalVariable}\\b([^}]*?\\}")`, 'g');
452
+ innerContent = innerContent.replace(innerNgClassPattern, `$1${variable}$2`);
453
+ }
454
+ }
455
+ const replaceStart = lineStart === -1 ? 0 : lineStart + 1;
456
+ if (closingTag !== -1 && elementTagName) {
457
+ // Replace element with directive + element without directive + updated inner content + closing tag
458
+ const afterClosingTag = closingTag + elementTagName.length + 3; // </tagname>
459
+ result =
460
+ result.substring(0, replaceStart) +
461
+ letStatement +
462
+ indentation +
463
+ elementWithoutDirective +
464
+ innerContent +
465
+ `</${elementTagName}>` +
466
+ result.substring(afterClosingTag);
467
+ }
468
+ else {
469
+ // Fallback for self-closing or elements where we can't find closing tag
470
+ result =
471
+ result.substring(0, replaceStart) +
472
+ letStatement +
473
+ indentation +
474
+ elementWithoutDirective +
475
+ result.substring(elementEnd);
476
+ }
477
+ convertedCount++;
478
+ }
479
+ }
480
+ // Clean up: remove multiple consecutive blank lines after @let statements
481
+ // This groups @let statements together by removing extra blank lines between them
482
+ result = result.replace(/(@let [^;]+;)\n\n+(?=\s*@let)/g, '$1\n');
483
+ const totalTime = ((Date.now() - startTime) / 1000).toFixed(1);
484
+ if (iterations >= maxIterations) {
485
+ console.warn(` āš ļø Maximum iterations reached after ${totalTime}s - converted ${convertedCount} directives but may have more remaining`);
486
+ }
487
+ else if (parseFloat(totalTime) > 5) {
488
+ const fileInfo = filePath ? ` ${filePath}` : '';
489
+ console.log(` ā±ļø Completed${fileInfo} in ${totalTime}s (${iterations} iterations, ${convertedCount} directives)`);
490
+ }
491
+ return { content: result, convertedCount, renamedVariables: renamedVars };
492
+ }
493
+ if (filesModified > 0) {
494
+ const messages = [];
495
+ if (directivesConverted > 0) {
496
+ messages.push(`${directivesConverted} directive(s) converted`);
497
+ }
498
+ if (importsRemoved > 0) {
499
+ messages.push(`${importsRemoved} import(s) removed`);
500
+ }
501
+ console.log(`\nāœ… Migrated ${filesModified} file(s): ${messages.join(', ')}`);
502
+ // Warn about renamed variables
503
+ if (renamedVariables.length > 0) {
504
+ console.log(`\nāš ļø Variable name conflicts resolved - please review:`);
505
+ for (const { file, original, renamed, line } of renamedVariables) {
506
+ console.log(` • ${file}:${line} - "${original}" renamed to "${renamed}"`);
507
+ }
508
+ }
509
+ }
510
+ else {
511
+ console.log('\nāœ… No *etLet or *ngLet directives found that need migration');
512
+ }
513
+ }
514
+ //# sourceMappingURL=et-let.js.map