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