@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.
- package/CHANGELOG.md +36 -0
- package/fesm2022/ethlete-cdk.mjs +11055 -12514
- package/fesm2022/ethlete-cdk.mjs.map +1 -1
- package/generators/generators.json +9 -0
- package/generators/migrate-to-v5/cdk-menu.js +259 -0
- package/generators/migrate-to-v5/color-themes.js +221 -0
- package/generators/migrate-to-v5/combobox.js +186 -0
- package/generators/migrate-to-v5/dialog-bottom-sheet.js +1039 -0
- package/generators/migrate-to-v5/et-let.js +514 -0
- package/generators/migrate-to-v5/is-active-element.js +201 -0
- package/generators/migrate-to-v5/migration.js +53 -0
- package/generators/migrate-to-v5/overlay-positions.js +868 -0
- package/generators/migrate-to-v5/schema.json +49 -0
- package/package.json +20 -14
- package/src/lib/styles/cdk.css +1 -1
- package/{index.d.ts ā types/ethlete-cdk.d.ts} +2248 -3617
|
@@ -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
|