@ethlete/core 5.0.0-next.52 → 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.
@@ -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
- // Add to existing import
754
- const existingImports = existingImport.importClause.namedBindings.elements.map((el) => el.name.text);
755
- const newImports = importsList.filter((imp) => !existingImports.includes(imp));
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
- if (routerStateServiceVars.includes(memberName)) {
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
- if (routerStateServiceVars.includes(paramName)) {
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') && !path.includes('.spec.ts')) {
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') && !path.includes('.spec.ts')) {
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
- // For each signal property, replace usages with ()
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
- // Replace in the file content
204
- updatedContent =
205
- updatedContent.slice(0, prop.initializer.getStart(updatedSourceFileForInline)) +
206
- newInitializer +
207
- updatedContent.slice(prop.initializer.getEnd());
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
- let updatedContent = content;
283
- let hasChanges = false;
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: isEthleteCore ? 'injectIsBase' : 'injectIsLg',
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}`, `#${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
- if (cssVariablesUsed.hasViewportVariables) {
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 existingImportNames = existingImport.importClause.namedBindings.elements.map((el) => el.name.text);
1281
- const newImports = importsToAdd.filter((imp) => !existingImportNames.includes(imp));
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
- // Resolve the identifier to its declaration
137
+ let themeObj;
138
+ let name = '<inline>';
135
139
  if (element.isKind(SyntaxKind.Identifier)) {
136
- const name = element.getText();
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
- let themeObj = themeDecl.getInitializer();
146
+ themeObj = themeDecl.getInitializer();
143
147
  if (!themeObj) {
144
148
  logger.warn(`⚠️ Theme ${name} has no initializer`);
145
149
  continue;
146
150
  }
147
- // Handle 'as const' and 'satisfies X' wrappers on individual theme objects, in
148
- // either order (e.g. `{...} satisfies Theme`, `{...} as const`, or both combined).
149
- while (themeObj.isKind(SyntaxKind.AsExpression) || themeObj.isKind(SyntaxKind.SatisfiesExpression)) {
150
- themeObj = themeObj.getExpression();
151
- }
152
- if (!themeObj.isKind(SyntaxKind.ObjectLiteralExpression)) {
153
- logger.warn(`⚠️ Theme ${name} is not an object literal`);
154
- continue;
155
- }
156
- try {
157
- const theme = parseThemeObject(themeObj, sourceFile);
158
- themes.push(theme);
159
- }
160
- catch (error) {
161
- logger.warn(`⚠️ Failed to parse theme ${name}: ${error instanceof Error ? error.message : String(error)}`);
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
- const name = element.getText();
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
- let themeObj = themeDecl.getInitializer();
134
+ themeObj = themeDecl.getInitializer();
130
135
  if (!themeObj) {
131
136
  logger.warn(`⚠️ Surface theme ${name} has no initializer`);
132
137
  continue;
133
138
  }
134
- // Handle 'as const' and 'satisfies X' wrappers on individual theme objects, in
135
- // either order (e.g. `{...} satisfies SurfaceTheme`, `{...} as const`, or both).
136
- while (themeObj.isKind(SyntaxKind.AsExpression) || themeObj.isKind(SyntaxKind.SatisfiesExpression)) {
137
- themeObj = themeObj.getExpression();
138
- }
139
- if (!themeObj.isKind(SyntaxKind.ObjectLiteralExpression)) {
140
- logger.warn(`⚠️ Surface theme ${name} is not an object literal`);
141
- continue;
142
- }
143
- try {
144
- const theme = parseSurfaceThemeObject(themeObj);
145
- themes.push(theme);
146
- }
147
- catch (error) {
148
- logger.warn(`⚠️ Failed to parse surface theme ${name}: ${error instanceof Error ? error.message : String(error)}`);
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?.isKind(SyntaxKind.ObjectLiteralExpression) || !isSwatchKey(name))
208
+ if (!initializer || !isSwatchKey(name))
189
209
  continue;
190
- const map = parseInteractionColorMap(initializer);
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 && map.hover && map.focus && map.active && map.disabled) {
193
- swatch.color = map;
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
- return swatch.color ? swatch : undefined;
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' && initializer.isKind(SyntaxKind.ObjectLiteralExpression)) {
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.52",
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",