@ethlete/core 5.0.0-next.53 → 5.0.0-next.54
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 +15 -0
- package/fesm2022/ethlete-core.mjs +638 -542
- package/fesm2022/ethlete-core.mjs.map +1 -1
- package/generators/devtools-about/generator.js +24 -15
- package/generators/generators.json +7 -6
- package/generators/migrate-provider-shape/migration-scope.js +3 -3
- package/generators/migrate-provider-shape/migration.js +0 -1
- package/generators/migrate-provider-shape/provider-shape.js +91 -24
- package/generators/migrate-surface-interaction-swatch/interaction-swatch.js +22 -2
- package/generators/migrate-surface-interaction-swatch/migration.js +8 -1
- package/generators/migrate-to-v5/color-naming.js +15 -20
- package/generators/migrate-to-v5/create-provider.js +14 -10
- package/generators/migrate-to-v5/migration.js +0 -1
- package/generators/migrate-to-v5/router-state-service.js +33 -8
- package/generators/migrate-to-v5/viewport-service.js +41 -59
- package/generators/tailwind-4-color-theme/generator.js +30 -21
- package/generators/tailwind-4-surface-theme/generator.js +60 -30
- package/package.json +2 -10
- package/types/ethlete-core.d.ts +49 -52
|
@@ -67,6 +67,8 @@ function findRouterStateServiceVariables(sourceFile) {
|
|
|
67
67
|
function findClassForRouterStateService(sourceFile, routerStateServiceVar) {
|
|
68
68
|
let classNode = null;
|
|
69
69
|
function visit(node) {
|
|
70
|
+
if (classNode)
|
|
71
|
+
return;
|
|
70
72
|
if (ts.isClassDeclaration(node)) {
|
|
71
73
|
const hasRouterStateService = node.members.some((member) => {
|
|
72
74
|
if (ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name)) {
|
|
@@ -750,9 +752,10 @@ function addImportsToPackage(sourceFile, content, imports, packageName) {
|
|
|
750
752
|
}
|
|
751
753
|
});
|
|
752
754
|
if (existingImport?.importClause?.namedBindings && ts.isNamedImports(existingImport.importClause.namedBindings)) {
|
|
753
|
-
|
|
754
|
-
const
|
|
755
|
-
const
|
|
755
|
+
const existingElements = existingImport.importClause.namedBindings.elements;
|
|
756
|
+
const existingImportedNames = existingElements.map((el) => el.propertyName?.text ?? el.name.text);
|
|
757
|
+
const existingImports = existingElements.map((el) => el.getText(sourceFile));
|
|
758
|
+
const newImports = importsList.filter((imp) => !existingImportedNames.includes(imp));
|
|
756
759
|
if (newImports.length === 0)
|
|
757
760
|
return content;
|
|
758
761
|
const allImports = [...existingImports, ...newImports].sort();
|
|
@@ -782,7 +785,16 @@ function removeRouterStateServiceInjection(sourceFile, content, routerStateServi
|
|
|
782
785
|
node.members.forEach((member) => {
|
|
783
786
|
if (ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name)) {
|
|
784
787
|
const memberName = member.name.text;
|
|
785
|
-
|
|
788
|
+
const injectsRouterState = member.initializer &&
|
|
789
|
+
ts.isCallExpression(member.initializer) &&
|
|
790
|
+
ts.isIdentifier(member.initializer.expression) &&
|
|
791
|
+
member.initializer.expression.text === 'inject' &&
|
|
792
|
+
member.initializer.arguments.some((argument) => ts.isIdentifier(argument) && argument.text === 'RouterStateService');
|
|
793
|
+
const hasRouterStateType = member.type &&
|
|
794
|
+
ts.isTypeReferenceNode(member.type) &&
|
|
795
|
+
ts.isIdentifier(member.type.typeName) &&
|
|
796
|
+
member.type.typeName.text === 'RouterStateService';
|
|
797
|
+
if (routerStateServiceVars.includes(memberName) && (injectsRouterState || hasRouterStateType)) {
|
|
786
798
|
const memberStart = member.getStart(sourceFile, true);
|
|
787
799
|
const memberEnd = member.getEnd();
|
|
788
800
|
let lineStart = memberStart;
|
|
@@ -807,7 +819,11 @@ function removeRouterStateServiceInjection(sourceFile, content, routerStateServi
|
|
|
807
819
|
member.parameters.forEach((param) => {
|
|
808
820
|
if (ts.isIdentifier(param.name)) {
|
|
809
821
|
const paramName = param.name.text;
|
|
810
|
-
|
|
822
|
+
const hasRouterStateType = param.type &&
|
|
823
|
+
ts.isTypeReferenceNode(param.type) &&
|
|
824
|
+
ts.isIdentifier(param.type.typeName) &&
|
|
825
|
+
param.type.typeName.text === 'RouterStateService';
|
|
826
|
+
if (routerStateServiceVars.includes(paramName) && hasRouterStateType) {
|
|
811
827
|
return;
|
|
812
828
|
}
|
|
813
829
|
}
|
|
@@ -951,11 +967,13 @@ export default async function migrateRouterStateService(tree) {
|
|
|
951
967
|
for (const child of children) {
|
|
952
968
|
const path = dir === '.' ? child : `${dir}/${child}`;
|
|
953
969
|
if (tree.isFile(path)) {
|
|
954
|
-
if (path.endsWith('.ts') && !path.includes('node_modules')
|
|
970
|
+
if (path.endsWith('.ts') && !path.includes('node_modules')) {
|
|
955
971
|
tsFiles.push(path);
|
|
956
972
|
}
|
|
957
973
|
}
|
|
958
974
|
else {
|
|
975
|
+
if (child === 'node_modules')
|
|
976
|
+
continue;
|
|
959
977
|
findFiles(path);
|
|
960
978
|
}
|
|
961
979
|
}
|
|
@@ -971,6 +989,14 @@ export default async function migrateRouterStateService(tree) {
|
|
|
971
989
|
continue;
|
|
972
990
|
console.log(`Processing: ${filePath}`);
|
|
973
991
|
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
|
|
992
|
+
const hasEthleteImport = sourceFile.statements.some((statement) => ts.isImportDeclaration(statement) &&
|
|
993
|
+
ts.isStringLiteral(statement.moduleSpecifier) &&
|
|
994
|
+
statement.moduleSpecifier.text.startsWith('@ethlete/') &&
|
|
995
|
+
!!statement.importClause?.namedBindings &&
|
|
996
|
+
ts.isNamedImports(statement.importClause.namedBindings) &&
|
|
997
|
+
statement.importClause.namedBindings.elements.some((element) => (element.propertyName?.text ?? element.name.text) === 'RouterStateService'));
|
|
998
|
+
if (!hasEthleteImport)
|
|
999
|
+
continue;
|
|
974
1000
|
const inlineResult = handleInlineInjectPatterns(sourceFile, content);
|
|
975
1001
|
let updatedContent = inlineResult.content;
|
|
976
1002
|
const updatedSourceFile = updatedContent !== content
|
|
@@ -1005,7 +1031,7 @@ export default async function migrateRouterStateService(tree) {
|
|
|
1005
1031
|
allImportsNeeded['@angular/core/rxjs-interop'].add('toObservable');
|
|
1006
1032
|
}
|
|
1007
1033
|
});
|
|
1008
|
-
for (const [original, replacement] of context.replacements) {
|
|
1034
|
+
for (const [original, replacement] of [...context.replacements].sort((a, b) => b[0].length - a[0].length)) {
|
|
1009
1035
|
if (replacement.includes('toObservable(')) {
|
|
1010
1036
|
allImportsNeeded['@angular/core/rxjs-interop'].add('toObservable');
|
|
1011
1037
|
}
|
|
@@ -1061,4 +1087,3 @@ export default async function migrateRouterStateService(tree) {
|
|
|
1061
1087
|
console.log('\nℹ️ No RouterStateService usage found\n');
|
|
1062
1088
|
}
|
|
1063
1089
|
}
|
|
1064
|
-
//# sourceMappingURL=router-state-service.js.map
|
|
@@ -11,7 +11,7 @@ export default async function migrateViewportService(tree) {
|
|
|
11
11
|
for (const child of children) {
|
|
12
12
|
const path = dir === '.' ? child : `${dir}/${child}`;
|
|
13
13
|
if (tree.isFile(path)) {
|
|
14
|
-
if (path.endsWith('.ts') && !path.includes('node_modules')
|
|
14
|
+
if (path.endsWith('.ts') && !path.includes('node_modules')) {
|
|
15
15
|
tsFiles.push(path);
|
|
16
16
|
}
|
|
17
17
|
else if (path.endsWith('.css') ||
|
|
@@ -22,6 +22,8 @@ export default async function migrateViewportService(tree) {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
else {
|
|
25
|
+
if (child === 'node_modules')
|
|
26
|
+
continue;
|
|
25
27
|
findFiles(path);
|
|
26
28
|
}
|
|
27
29
|
}
|
|
@@ -44,6 +46,9 @@ export default async function migrateViewportService(tree) {
|
|
|
44
46
|
continue;
|
|
45
47
|
logger.log(`Processing: ${filePath}`);
|
|
46
48
|
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
|
|
49
|
+
const viewportPackage = getViewportServicePackage(sourceFile);
|
|
50
|
+
if (viewportPackage !== '@ethlete/core' && !viewportPackage?.startsWith('@fifa-gg/uikit'))
|
|
51
|
+
continue;
|
|
47
52
|
// Check if this is a component with a template
|
|
48
53
|
const templatePath = findTemplateForComponent(tree, filePath, sourceFile);
|
|
49
54
|
// Track signal properties for template migration
|
|
@@ -93,7 +98,7 @@ export default async function migrateViewportService(tree) {
|
|
|
93
98
|
}
|
|
94
99
|
});
|
|
95
100
|
// Check if any replacements use toObservable or toSignal
|
|
96
|
-
for (const [original, replacement] of context.replacements) {
|
|
101
|
+
for (const [original, replacement] of [...context.replacements].sort((a, b) => b[0].length - a[0].length)) {
|
|
97
102
|
if (replacement.includes('toObservable(')) {
|
|
98
103
|
allImportsNeeded['@angular/core/rxjs-interop'].add('toObservable');
|
|
99
104
|
}
|
|
@@ -174,6 +179,7 @@ export default async function migrateViewportService(tree) {
|
|
|
174
179
|
componentTemplateMigrations.set(filePath, templateMigrationInfo);
|
|
175
180
|
}
|
|
176
181
|
const updatedSourceFileForInline = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
|
|
182
|
+
const inlineTemplateEdits = [];
|
|
177
183
|
updatedSourceFileForInline.forEachChild((node) => {
|
|
178
184
|
if (ts.isClassDeclaration(node)) {
|
|
179
185
|
const decorators =
|
|
@@ -192,25 +198,25 @@ export default async function migrateViewportService(tree) {
|
|
|
192
198
|
let templateText = prop.initializer.getText();
|
|
193
199
|
// Remove the surrounding quotes/backticks
|
|
194
200
|
templateText = templateText.slice(1, -1);
|
|
195
|
-
|
|
196
|
-
for (const propName of templateMigrationInfo.signalProperties) {
|
|
197
|
-
const regex = new RegExp(`\\b${propName}(?!\\s*\\()\\b`, 'g');
|
|
198
|
-
templateText = templateText.replace(regex, `${propName}()`);
|
|
199
|
-
}
|
|
201
|
+
templateText = migrateTemplateText(templateText, templateMigrationInfo.signalProperties);
|
|
200
202
|
// Re-wrap with original quotes/backticks
|
|
201
203
|
const quote = prop.initializer.getText()[0];
|
|
202
204
|
const newInitializer = quote + templateText + quote;
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
205
|
+
inlineTemplateEdits.push({
|
|
206
|
+
start: prop.initializer.getStart(updatedSourceFileForInline),
|
|
207
|
+
end: prop.initializer.getEnd(),
|
|
208
|
+
replacement: newInitializer,
|
|
209
|
+
});
|
|
208
210
|
}
|
|
209
211
|
});
|
|
210
212
|
}
|
|
211
213
|
});
|
|
212
214
|
}
|
|
213
215
|
});
|
|
216
|
+
inlineTemplateEdits.sort((a, b) => b.start - a.start);
|
|
217
|
+
for (const edit of inlineTemplateEdits) {
|
|
218
|
+
updatedContent = updatedContent.slice(0, edit.start) + edit.replacement + updatedContent.slice(edit.end);
|
|
219
|
+
}
|
|
214
220
|
// Write the updated content if changes were made
|
|
215
221
|
if (updatedContent !== content) {
|
|
216
222
|
tree.write(filePath, updatedContent);
|
|
@@ -275,28 +281,24 @@ function detectCssVariableUsage(tree, styleFiles) {
|
|
|
275
281
|
}
|
|
276
282
|
return { hasViewportVariables, hasScrollbarVariables };
|
|
277
283
|
}
|
|
284
|
+
const migrateTemplateExpression = (expression, signalProperties) => {
|
|
285
|
+
let migrated = expression;
|
|
286
|
+
for (const propName of signalProperties) {
|
|
287
|
+
const regex = new RegExp(`\\b${escapeRegExp(propName)}(?!\\s*\\()\\b`, 'g');
|
|
288
|
+
migrated = migrated.replace(regex, `${propName}()`);
|
|
289
|
+
}
|
|
290
|
+
return migrated;
|
|
291
|
+
};
|
|
292
|
+
const migrateTemplateText = (template, signalProperties) => template
|
|
293
|
+
.replace(/{{([\s\S]*?)}}/g, (_, expression) => `{{${migrateTemplateExpression(expression, signalProperties)}}}`)
|
|
294
|
+
.replace(/((?:\[[^\]]+\]|\([^)]+\)|\*[\w.-]+)\s*=\s*)(["'])([\s\S]*?)\2/g, (_, prefix, quote, expression) => `${prefix}${quote}${migrateTemplateExpression(expression, signalProperties)}${quote}`)
|
|
295
|
+
.replace(/@(if|for|switch|case|defer)\s*\(([^\n{}]*)\)/g, (_, block, expression) => `@${block} (${migrateTemplateExpression(expression, signalProperties)})`);
|
|
278
296
|
function migrateTemplateFile(tree, htmlFilePath, migrationInfo) {
|
|
279
297
|
const content = tree.read(htmlFilePath, 'utf-8');
|
|
280
298
|
if (!content)
|
|
281
299
|
return false;
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
// For each signal property, replace usages with () calls
|
|
285
|
-
for (const propName of migrationInfo.signalProperties) {
|
|
286
|
-
// Match property usage in various Angular contexts:
|
|
287
|
-
// 1. Interpolation: {{ propName }}
|
|
288
|
-
// 2. Property binding: [prop]="propName"
|
|
289
|
-
// 3. Event binding: (event)="propName === value"
|
|
290
|
-
// 4. Structural directives: *ngIf="propName"
|
|
291
|
-
// But NOT if already called: propName()
|
|
292
|
-
// Create a regex that matches the property but not if followed by (
|
|
293
|
-
const regex = new RegExp(`\\b${escapeRegExp(propName)}(?!\\s*\\()\\b`, 'g');
|
|
294
|
-
const newContent = updatedContent.replace(regex, `${propName}()`);
|
|
295
|
-
if (newContent !== updatedContent) {
|
|
296
|
-
updatedContent = newContent;
|
|
297
|
-
hasChanges = true;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
+
const updatedContent = migrateTemplateText(content, migrationInfo.signalProperties);
|
|
301
|
+
const hasChanges = updatedContent !== content;
|
|
300
302
|
if (hasChanges) {
|
|
301
303
|
tree.write(htmlFilePath, updatedContent);
|
|
302
304
|
}
|
|
@@ -339,7 +341,7 @@ function getViewportServicePackage(sourceFile) {
|
|
|
339
341
|
sourceFile.forEachChild((node) => {
|
|
340
342
|
if (ts.isImportDeclaration(node) && node.importClause?.namedBindings) {
|
|
341
343
|
if (ts.isNamedImports(node.importClause.namedBindings)) {
|
|
342
|
-
const hasViewportService = node.importClause.namedBindings.elements.some((el) => el.name.text === 'ViewportService');
|
|
344
|
+
const hasViewportService = node.importClause.namedBindings.elements.some((el) => (el.propertyName?.text ?? el.name.text) === 'ViewportService');
|
|
343
345
|
if (hasViewportService && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
344
346
|
packageName = node.moduleSpecifier.text;
|
|
345
347
|
}
|
|
@@ -358,7 +360,7 @@ function getPropertyMaps(packageName) {
|
|
|
358
360
|
isLg: isEthleteCore ? 'injectIsLg' : 'injectIsXl',
|
|
359
361
|
isXl: isEthleteCore ? 'injectIsXl' : 'injectIs2Xl',
|
|
360
362
|
is2Xl: 'injectIs2Xl',
|
|
361
|
-
isBase:
|
|
363
|
+
isBase: 'injectIsLg',
|
|
362
364
|
viewportSize: 'injectViewportDimensions',
|
|
363
365
|
scrollbarSize: 'injectScrollbarDimensions',
|
|
364
366
|
currentViewport: 'injectCurrentBreakpoint',
|
|
@@ -1092,7 +1094,7 @@ function findClassForViewportService(sourceFile, viewportServiceVar) {
|
|
|
1092
1094
|
function findAvailableMemberName(baseName, existingMembers) {
|
|
1093
1095
|
// baseName already has $ suffix for observables at this point
|
|
1094
1096
|
// Try variations: name, _name, #name
|
|
1095
|
-
const candidates = [baseName, `_${baseName}
|
|
1097
|
+
const candidates = [baseName, `_${baseName}`];
|
|
1096
1098
|
for (const candidate of candidates) {
|
|
1097
1099
|
if (!existingMembers.has(candidate)) {
|
|
1098
1100
|
return candidate;
|
|
@@ -1160,29 +1162,8 @@ function migrateMonitorViewport(sourceFile, content, viewportServiceVars, cssVar
|
|
|
1160
1162
|
return { content, imports };
|
|
1161
1163
|
}
|
|
1162
1164
|
// Determine what to add to constructor
|
|
1163
|
-
const constructorStatements = [];
|
|
1164
|
-
|
|
1165
|
-
constructorStatements.push('writeViewportSizeToCssVariables();');
|
|
1166
|
-
imports.push('writeViewportSizeToCssVariables');
|
|
1167
|
-
}
|
|
1168
|
-
if (cssVariablesUsed.hasScrollbarVariables) {
|
|
1169
|
-
constructorStatements.push('writeScrollbarSizeToCssVariables();');
|
|
1170
|
-
imports.push('writeScrollbarSizeToCssVariables');
|
|
1171
|
-
}
|
|
1172
|
-
if (constructorStatements.length === 0) {
|
|
1173
|
-
// Just remove monitorViewport calls
|
|
1174
|
-
const modifications = monitorViewportCalls.map((call) => ({
|
|
1175
|
-
start: call.start,
|
|
1176
|
-
end: call.end,
|
|
1177
|
-
replacement: '',
|
|
1178
|
-
}));
|
|
1179
|
-
// Apply modifications from end to start
|
|
1180
|
-
modifications.sort((a, b) => b.start - a.start);
|
|
1181
|
-
for (const mod of modifications) {
|
|
1182
|
-
updatedContent = updatedContent.slice(0, mod.start) + mod.replacement + updatedContent.slice(mod.end);
|
|
1183
|
-
}
|
|
1184
|
-
return { content: updatedContent, imports };
|
|
1185
|
-
}
|
|
1165
|
+
const constructorStatements = ['writeViewportSizeToCssVariables();', 'writeScrollbarSizeToCssVariables();'];
|
|
1166
|
+
imports.push('writeViewportSizeToCssVariables', 'writeScrollbarSizeToCssVariables');
|
|
1186
1167
|
// Group calls by class
|
|
1187
1168
|
const callsByClass = new Map();
|
|
1188
1169
|
for (const call of monitorViewportCalls) {
|
|
@@ -1277,8 +1258,10 @@ function addImportsToPackage(sourceFile, content, neededImports, packageName) {
|
|
|
1277
1258
|
statement.moduleSpecifier.text === packageName);
|
|
1278
1259
|
const importsToAdd = Array.from(neededImports).sort();
|
|
1279
1260
|
if (existingImport?.importClause?.namedBindings && ts.isNamedImports(existingImport.importClause.namedBindings)) {
|
|
1280
|
-
const
|
|
1281
|
-
const
|
|
1261
|
+
const existingElements = existingImport.importClause.namedBindings.elements;
|
|
1262
|
+
const existingImportedNames = existingElements.map((el) => el.propertyName?.text ?? el.name.text);
|
|
1263
|
+
const existingImportNames = existingElements.map((el) => el.getText(sourceFile));
|
|
1264
|
+
const newImports = importsToAdd.filter((imp) => !existingImportedNames.includes(imp));
|
|
1282
1265
|
if (newImports.length > 0) {
|
|
1283
1266
|
const allImports = [...existingImportNames, ...newImports].sort();
|
|
1284
1267
|
const importText = existingImport.getText(sourceFile);
|
|
@@ -1674,4 +1657,3 @@ function checkIfPropertyIsUsedElsewhere(sourceFile, classNode, propertyName, pro
|
|
|
1674
1657
|
});
|
|
1675
1658
|
return isUsed;
|
|
1676
1659
|
}
|
|
1677
|
-
//# sourceMappingURL=viewport-service.js.map
|
|
@@ -62,6 +62,10 @@ export default async function generate(tree, schema) {
|
|
|
62
62
|
// Step 6: Generate Tailwind CSS
|
|
63
63
|
logger.log('\n🎨 Generating Tailwind theme CSS...');
|
|
64
64
|
const css = generateTailwindThemeCss(themes, prefix, runtimePrefix, schema);
|
|
65
|
+
const typesOutputPath = schema.typesOutputPath ?? outputPath.replace(/\.css$/, '.d.ts');
|
|
66
|
+
if (typesOutputPath === outputPath) {
|
|
67
|
+
throw new Error('A custom outputPath must end in .css or be paired with a distinct typesOutputPath.');
|
|
68
|
+
}
|
|
65
69
|
// Step 7: Write the generated CSS file
|
|
66
70
|
const outputDir = outputPath.substring(0, outputPath.lastIndexOf('/'));
|
|
67
71
|
if (outputDir && !tree.exists(outputDir)) {
|
|
@@ -72,7 +76,6 @@ export default async function generate(tree, schema) {
|
|
|
72
76
|
// Step 8: Generate the `EthleteColorThemeNameRegistry` augmentation, so `etProvideColor`
|
|
73
77
|
// (and anything else that accepts a `RegisteredColorThemeName`) is checked/autocompleted
|
|
74
78
|
// against this app's actual theme names, instead of a plain `string`.
|
|
75
|
-
const typesOutputPath = schema.typesOutputPath || outputPath.replace(/\.css$/, '.d.ts');
|
|
76
79
|
const typesDts = generateColorThemeNameTypes(themes, schema);
|
|
77
80
|
tree.write(typesOutputPath, typesDts);
|
|
78
81
|
logger.log(`✅ Generated color theme name types at: ${typesOutputPath}`);
|
|
@@ -131,35 +134,37 @@ function extractThemesFromContent(content, filePath) {
|
|
|
131
134
|
// Get the array elements (these are references to the theme const declarations)
|
|
132
135
|
const elements = initializer.getElements();
|
|
133
136
|
for (const element of elements) {
|
|
134
|
-
|
|
137
|
+
let themeObj;
|
|
138
|
+
let name = '<inline>';
|
|
135
139
|
if (element.isKind(SyntaxKind.Identifier)) {
|
|
136
|
-
|
|
140
|
+
name = element.getText();
|
|
137
141
|
const themeDecl = exportedDeclarations.find((decl) => decl.getName() === name);
|
|
138
142
|
if (!themeDecl) {
|
|
139
143
|
logger.warn(`⚠️ Could not find declaration for theme: ${name}`);
|
|
140
144
|
continue;
|
|
141
145
|
}
|
|
142
|
-
|
|
146
|
+
themeObj = themeDecl.getInitializer();
|
|
143
147
|
if (!themeObj) {
|
|
144
148
|
logger.warn(`⚠️ Theme ${name} has no initializer`);
|
|
145
149
|
continue;
|
|
146
150
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
themeObj = element;
|
|
154
|
+
}
|
|
155
|
+
while (themeObj.isKind(SyntaxKind.AsExpression) || themeObj.isKind(SyntaxKind.SatisfiesExpression)) {
|
|
156
|
+
themeObj = themeObj.getExpression();
|
|
157
|
+
}
|
|
158
|
+
if (!themeObj.isKind(SyntaxKind.ObjectLiteralExpression)) {
|
|
159
|
+
logger.warn(`⚠️ Theme ${name} is not an object literal`);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
const theme = parseThemeObject(themeObj, sourceFile);
|
|
164
|
+
themes.push(theme);
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
logger.warn(`⚠️ Failed to parse theme ${name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
163
168
|
}
|
|
164
169
|
}
|
|
165
170
|
if (themes.length === 0) {
|
|
@@ -282,6 +287,11 @@ function parseThemeSwatch(obj, sourceFile) {
|
|
|
282
287
|
const COLOR_MAP_KEYS = ['default', 'hover', 'focus', 'active', 'disabled'];
|
|
283
288
|
const isColorMapKey = (name) => COLOR_MAP_KEYS.some((key) => key === name);
|
|
284
289
|
function parseColorMap(initializer, sourceFile) {
|
|
290
|
+
if (initializer.isKind(SyntaxKind.Identifier)) {
|
|
291
|
+
const declaration = sourceFile.getVariableDeclarations().find((decl) => decl.getName() === initializer.getText());
|
|
292
|
+
const referencedInitializer = declaration?.getInitializer();
|
|
293
|
+
return referencedInitializer ? parseColorMap(referencedInitializer, sourceFile) : null;
|
|
294
|
+
}
|
|
285
295
|
// Handle spread expressions by resolving references
|
|
286
296
|
if (initializer.isKind(SyntaxKind.ObjectLiteralExpression)) {
|
|
287
297
|
const colorMap = {};
|
|
@@ -596,4 +606,3 @@ function findMainStylesFile(tree) {
|
|
|
596
606
|
return potentialFiles;
|
|
597
607
|
}
|
|
598
608
|
//#endregion
|
|
599
|
-
//# sourceMappingURL=generator.js.map
|
|
@@ -61,12 +61,15 @@ export default async function generate(tree, schema) {
|
|
|
61
61
|
}
|
|
62
62
|
logger.log('\n🎨 Generating Tailwind surface theme CSS...');
|
|
63
63
|
const css = generateSurfaceThemeCss(themes, prefix, runtimePrefix, schema);
|
|
64
|
+
const typesOutputPath = schema.typesOutputPath ?? outputPath.replace(/\.css$/, '.d.ts');
|
|
65
|
+
if (typesOutputPath === outputPath) {
|
|
66
|
+
throw new Error('A custom outputPath must end in .css or be paired with a distinct typesOutputPath.');
|
|
67
|
+
}
|
|
64
68
|
tree.write(outputPath, css);
|
|
65
69
|
logger.log(`✅ Generated Tailwind surface themes at: ${outputPath}`);
|
|
66
70
|
// Generate the `EthleteSurfaceThemeNameRegistry` augmentation, so `etProvideSurface` (and
|
|
67
71
|
// anything else that accepts a `RegisteredSurfaceThemeName`) is checked/autocompleted
|
|
68
72
|
// against this app's actual surface theme names, instead of a plain `string`.
|
|
69
|
-
const typesOutputPath = schema.typesOutputPath || outputPath.replace(/\.css$/, '.d.ts');
|
|
70
73
|
const typesDts = generateSurfaceThemeNameTypes(themes, schema);
|
|
71
74
|
tree.write(typesOutputPath, typesDts);
|
|
72
75
|
logger.log(`✅ Generated surface theme name types at: ${typesOutputPath}`);
|
|
@@ -119,34 +122,37 @@ function extractSurfaceThemesFromContent(content, filePath) {
|
|
|
119
122
|
}
|
|
120
123
|
const elements = initializer.getElements();
|
|
121
124
|
for (const element of elements) {
|
|
125
|
+
let themeObj;
|
|
126
|
+
let name = '<inline>';
|
|
122
127
|
if (element.isKind(SyntaxKind.Identifier)) {
|
|
123
|
-
|
|
128
|
+
name = element.getText();
|
|
124
129
|
const themeDecl = exportedDeclarations.find((decl) => decl.getName() === name);
|
|
125
130
|
if (!themeDecl) {
|
|
126
131
|
logger.warn(`⚠️ Could not find declaration for surface theme: ${name}`);
|
|
127
132
|
continue;
|
|
128
133
|
}
|
|
129
|
-
|
|
134
|
+
themeObj = themeDecl.getInitializer();
|
|
130
135
|
if (!themeObj) {
|
|
131
136
|
logger.warn(`⚠️ Surface theme ${name} has no initializer`);
|
|
132
137
|
continue;
|
|
133
138
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
themeObj = element;
|
|
142
|
+
}
|
|
143
|
+
while (themeObj.isKind(SyntaxKind.AsExpression) || themeObj.isKind(SyntaxKind.SatisfiesExpression)) {
|
|
144
|
+
themeObj = themeObj.getExpression();
|
|
145
|
+
}
|
|
146
|
+
if (!themeObj.isKind(SyntaxKind.ObjectLiteralExpression)) {
|
|
147
|
+
logger.warn(`⚠️ Surface theme ${name} is not an object literal`);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
const theme = parseSurfaceThemeObject(themeObj, sourceFile);
|
|
152
|
+
themes.push(theme);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
logger.warn(`⚠️ Failed to parse surface theme ${name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
150
156
|
}
|
|
151
157
|
}
|
|
152
158
|
if (themes.length === 0) {
|
|
@@ -160,6 +166,20 @@ const INTERACTION_KEYS = ['default', 'hover', 'focus', 'active', 'disabled'];
|
|
|
160
166
|
const isInteractionKey = (name) => INTERACTION_KEYS.some((key) => key === name);
|
|
161
167
|
const SWATCH_KEYS = ['color', 'onColor', 'inkColor'];
|
|
162
168
|
const isSwatchKey = (name) => SWATCH_KEYS.some((key) => key === name);
|
|
169
|
+
const resolveObjectLiteral = (expression, sourceFile) => {
|
|
170
|
+
let resolved = expression;
|
|
171
|
+
while (resolved.isKind(SyntaxKind.AsExpression) || resolved.isKind(SyntaxKind.SatisfiesExpression)) {
|
|
172
|
+
resolved = resolved.getExpression();
|
|
173
|
+
}
|
|
174
|
+
if (resolved.isKind(SyntaxKind.Identifier)) {
|
|
175
|
+
const declaration = sourceFile
|
|
176
|
+
.getVariableDeclarations()
|
|
177
|
+
.find((candidate) => candidate.getName() === resolved.getText());
|
|
178
|
+
const initializer = declaration?.getInitializer();
|
|
179
|
+
return initializer ? resolveObjectLiteral(initializer, sourceFile) : null;
|
|
180
|
+
}
|
|
181
|
+
return resolved.isKind(SyntaxKind.ObjectLiteralExpression) ? resolved : null;
|
|
182
|
+
};
|
|
163
183
|
function parseInteractionColorMap(obj) {
|
|
164
184
|
const map = {};
|
|
165
185
|
for (const prop of obj.getProperties()) {
|
|
@@ -173,7 +193,7 @@ function parseInteractionColorMap(obj) {
|
|
|
173
193
|
}
|
|
174
194
|
return map;
|
|
175
195
|
}
|
|
176
|
-
function parseInteractionColor(obj, themeName) {
|
|
196
|
+
function parseInteractionColor(obj, themeName, sourceFile) {
|
|
177
197
|
const swatch = {};
|
|
178
198
|
for (const prop of obj.getProperties()) {
|
|
179
199
|
if (!prop.isKind(SyntaxKind.PropertyAssignment))
|
|
@@ -185,22 +205,30 @@ function parseInteractionColor(obj, themeName) {
|
|
|
185
205
|
`{ color: { default, hover, focus, active, disabled }, onColor?, inkColor? }. ` +
|
|
186
206
|
`Run \`nx g @ethlete/core:migrate-surface-interaction-swatch\` to convert your themes.`);
|
|
187
207
|
}
|
|
188
|
-
if (!initializer
|
|
208
|
+
if (!initializer || !isSwatchKey(name))
|
|
189
209
|
continue;
|
|
190
|
-
const
|
|
210
|
+
const mapObject = resolveObjectLiteral(initializer, sourceFile);
|
|
211
|
+
if (!mapObject) {
|
|
212
|
+
throw new Error(`Surface theme "${themeName}" has an interactionColor.${name} value that cannot be resolved.`);
|
|
213
|
+
}
|
|
214
|
+
const map = parseInteractionColorMap(mapObject);
|
|
191
215
|
if (name === 'color') {
|
|
192
|
-
if (map.default
|
|
193
|
-
|
|
216
|
+
if (!map.default || !map.hover || !map.focus || !map.active || !map.disabled) {
|
|
217
|
+
throw new Error(`Surface theme "${themeName}" must define default, hover, focus, active and disabled interaction colors.`);
|
|
194
218
|
}
|
|
219
|
+
swatch.color = map;
|
|
195
220
|
continue;
|
|
196
221
|
}
|
|
197
222
|
if (map.default) {
|
|
198
223
|
swatch[name] = map;
|
|
199
224
|
}
|
|
200
225
|
}
|
|
201
|
-
|
|
226
|
+
if (!swatch.color) {
|
|
227
|
+
throw new Error(`Surface theme "${themeName}" interactionColor must define a color swatch.`);
|
|
228
|
+
}
|
|
229
|
+
return swatch;
|
|
202
230
|
}
|
|
203
|
-
function parseSurfaceThemeObject(obj) {
|
|
231
|
+
function parseSurfaceThemeObject(obj, sourceFile) {
|
|
204
232
|
const properties = obj.getProperties();
|
|
205
233
|
const theme = {};
|
|
206
234
|
let interactionColorObj = null;
|
|
@@ -219,8 +247,11 @@ function parseSurfaceThemeObject(obj) {
|
|
|
219
247
|
}
|
|
220
248
|
continue;
|
|
221
249
|
}
|
|
222
|
-
if (propName === 'interactionColor'
|
|
223
|
-
interactionColorObj = initializer;
|
|
250
|
+
if (propName === 'interactionColor') {
|
|
251
|
+
interactionColorObj = resolveObjectLiteral(initializer, sourceFile);
|
|
252
|
+
if (!interactionColorObj) {
|
|
253
|
+
throw new Error('Surface theme interactionColor must be an inline or same-file object literal.');
|
|
254
|
+
}
|
|
224
255
|
continue;
|
|
225
256
|
}
|
|
226
257
|
if (propName === 'elevation') {
|
|
@@ -254,7 +285,7 @@ function parseSurfaceThemeObject(obj) {
|
|
|
254
285
|
throw new Error('SurfaceTheme must have name, type, elevation, background, color, colorMuted, colorSubtle, and border properties');
|
|
255
286
|
}
|
|
256
287
|
if (interactionColorObj) {
|
|
257
|
-
theme.interactionColor = parseInteractionColor(interactionColorObj, theme.name);
|
|
288
|
+
theme.interactionColor = parseInteractionColor(interactionColorObj, theme.name, sourceFile);
|
|
258
289
|
}
|
|
259
290
|
return theme;
|
|
260
291
|
}
|
|
@@ -566,4 +597,3 @@ function findMainStylesFile(tree) {
|
|
|
566
597
|
return potentialFiles;
|
|
567
598
|
}
|
|
568
599
|
//#endregion
|
|
569
|
-
//# sourceMappingURL=generator.js.map
|
package/package.json
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ethlete/core",
|
|
3
|
-
"version": "5.0.0-next.
|
|
3
|
+
"version": "5.0.0-next.54",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"peerDependencies": {
|
|
7
|
-
"@analogjs/vite-plugin-angular": "2.6.3",
|
|
8
7
|
"@angular/common": "22.0.7",
|
|
9
8
|
"@angular/core": "22.0.7",
|
|
10
9
|
"@angular/forms": "22.0.7",
|
|
@@ -15,13 +14,9 @@
|
|
|
15
14
|
"@nx/devkit": "23.1.0",
|
|
16
15
|
"rxjs": "7.8.2",
|
|
17
16
|
"ts-morph": "21.0.1",
|
|
18
|
-
"typescript": "6.0.3"
|
|
19
|
-
"vite": "8.1.5"
|
|
17
|
+
"typescript": "6.0.3"
|
|
20
18
|
},
|
|
21
19
|
"peerDependenciesMeta": {
|
|
22
|
-
"@analogjs/vite-plugin-angular": {
|
|
23
|
-
"optional": true
|
|
24
|
-
},
|
|
25
20
|
"@nx/devkit": {
|
|
26
21
|
"optional": true
|
|
27
22
|
},
|
|
@@ -30,9 +25,6 @@
|
|
|
30
25
|
},
|
|
31
26
|
"typescript": {
|
|
32
27
|
"optional": true
|
|
33
|
-
},
|
|
34
|
-
"vite": {
|
|
35
|
-
"optional": true
|
|
36
28
|
}
|
|
37
29
|
},
|
|
38
30
|
"generators": "./generators/generators.json",
|