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