Skip to content

Commit 96f2617

Browse files
committed
final adjustments
1 parent 417f075 commit 96f2617

File tree

18 files changed

+312
-136
lines changed

18 files changed

+312
-136
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export const tokens = {
1+
export const customTokens = {
22
colorPaletteBlueBorder2: 'var(--colorPaletteBlueBorder2)',
33
colorBrandBackground: 'var(--colorBrandBackground)',
44
};

packages/transform/__fixtures__/object-shorthands/output.meta.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"usedProcessing": true,
3-
"usedVMForEvaluation": true,
3+
"usedVMForEvaluation": false,
44
"cssRulesByBucket": {
55
"d": [
66
[
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { makeStyles } from '@griffel/react';
2-
import { tokens } from './tokens';
2+
import { customTokens } from './tokens';
33

44
export const useStyles = makeStyles({
55
root: {
66
backgroundColor: 'black',
7-
color: tokens.colorPaletteBlueBorder2,
7+
color: customTokens.colorPaletteBlueBorder2,
88
display: 'flex',
99
},
10-
rootPrimary: { color: tokens.colorBrandBackground },
10+
rootPrimary: { color: customTokens.colorBrandBackground },
1111
});

packages/transform/__fixtures__/tokens/output.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { __css } from '@griffel/react';
2-
import { tokens } from './tokens';
2+
import { customTokens } from './tokens';
33

44
export const useStyles = __css({
55
root: { De3pzq: 'f1734hy', sj55zd: 'ff34w31', mc9l5x: 'f22iagw' },
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export const tokens = {
1+
export const customTokens = {
22
colorPaletteBlueBorder2: 'var(--colorPaletteBlueBorder2)',
33
colorBrandBackground: 'var(--colorBrandBackground)',
44
};

packages/transform/src/evaluation/astEvaluator.mts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Node, ObjectExpression } from 'oxc-parser';
1+
import type { Node, ObjectExpression, TemplateLiteral, MemberExpression } from 'oxc-parser';
22
import type { EvaluationResult } from './types.mjs';
33

44
class DeoptError extends Error {}
@@ -11,6 +11,12 @@ function evaluateNode(node: Node): unknown {
1111
case 'ObjectExpression':
1212
return evaluateObjectExpression(node);
1313

14+
case 'TemplateLiteral':
15+
return evaluateTemplateLiteral(node);
16+
17+
case 'MemberExpression':
18+
return evaluateMemberExpression(node);
19+
1420
default:
1521
// Deopt for any unsupported node type
1622
throw new DeoptError(`Unsupported node type: ${node.type}`);
@@ -52,6 +58,67 @@ function evaluateObjectExpression(node: ObjectExpression): unknown {
5258
return obj;
5359
}
5460

61+
/**
62+
* Evaluates template literals that contain Fluent UI design tokens.
63+
* Transforms `${tokens.propertyName}` expressions into CSS custom properties: `var(--propertyName)`
64+
*
65+
* @param node - The TemplateLiteral AST node to evaluate
66+
* @returns The evaluated string with token expressions converted to CSS custom properties
67+
*/
68+
function evaluateTemplateLiteral(node: TemplateLiteral): string {
69+
let result = '';
70+
71+
for (let i = 0; i < node.quasis.length; i++) {
72+
// Add the literal part
73+
result += node.quasis[i].value.cooked;
74+
75+
// Add the expression part if it exists
76+
if (i < node.expressions.length) {
77+
const expression = node.expressions[i];
78+
79+
// Handle tokens.propertyName expressions specifically
80+
if (
81+
expression.type === 'MemberExpression' &&
82+
expression.object.type === 'Identifier' &&
83+
expression.object.name === 'tokens' &&
84+
expression.property.type === 'Identifier' &&
85+
!expression.computed
86+
) {
87+
// Transform tokens.propertyName to var(--propertyName)
88+
result += `var(--${expression.property.name})`;
89+
} else {
90+
// For any other expression type, we can't evaluate it
91+
throw new DeoptError('Only tokens.propertyName expressions are supported in template literals');
92+
}
93+
}
94+
}
95+
96+
return result;
97+
}
98+
99+
/**
100+
* Evaluates member expressions that reference Fluent UI design tokens.
101+
* Transforms `tokens.propertyName` expressions into CSS custom properties: `var(--propertyName)`
102+
*
103+
* @param node - The MemberExpression AST node to evaluate
104+
* @returns The CSS custom property string
105+
*/
106+
function evaluateMemberExpression(node: MemberExpression): string {
107+
// Handle tokens.propertyName expressions specifically
108+
if (
109+
node.object.type === 'Identifier' &&
110+
node.object.name === 'tokens' &&
111+
node.property.type === 'Identifier' &&
112+
!node.computed
113+
) {
114+
// Transform tokens.propertyName to var(--propertyName)
115+
return `var(--${node.property.name})`;
116+
} else {
117+
// For any other member expression, we can't evaluate it
118+
throw new DeoptError('Only tokens.propertyName member expressions are supported');
119+
}
120+
}
121+
55122
/**
56123
* Simple static evaluator for object expressions with nested objects.
57124
* Based on Babel's evaluation approach but simplified for our specific use case.

packages/transform/src/evaluation/astEvaluator.test.mts

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,31 @@
1-
import { parseSync } from 'oxc-parser';
1+
import { parseSync, type ObjectExpression } from 'oxc-parser';
2+
import { walk } from 'oxc-walker';
23
import { describe, it, expect } from 'vitest';
34

45
import { astEvaluator } from './astEvaluator.mjs';
56

67
function getFirstObjectExpression(code: string) {
78
const result = parseSync('test.js', code);
89

9-
const programBody = result.program.body;
10-
const firstStatement = programBody[0];
11-
12-
if (firstStatement.type !== 'VariableDeclaration') {
13-
throw new Error('First statement is not a variable declaration');
10+
if (result.errors.length > 0) {
11+
throw new Error(`Parsing errors: ${result.errors.map(e => e.message).join(', ')}`);
1412
}
1513

16-
if (firstStatement.declarations.length === 0) {
17-
throw new Error('No variable declarations found');
18-
}
14+
let objectExpression: ObjectExpression | null = null;
15+
16+
walk(result.program, {
17+
enter(node) {
18+
if (node.type === 'ObjectExpression' && !objectExpression) {
19+
objectExpression = node;
20+
}
21+
},
22+
});
1923

20-
if (!firstStatement.declarations[0].init || firstStatement.declarations[0].init.type !== 'ObjectExpression') {
21-
throw new Error('The variable is not initialized with an object expression');
24+
if (!objectExpression) {
25+
throw new Error('No "ObjectExpression" found in the code');
2226
}
2327

24-
return firstStatement.declarations[0].init;
28+
return objectExpression;
2529
}
2630

2731
describe('staticEvaluator', () => {
@@ -72,4 +76,30 @@ describe('staticEvaluator', () => {
7276
expect(evaluation.confident).toBe(false);
7377
expect(evaluation.value).toBeUndefined();
7478
});
79+
80+
describe('@fluentui/react-components integration', () => {
81+
it('evaluates style object with tokens', () => {
82+
const code = `
83+
export const useButtonStyles = makeStyles({
84+
staticWrapper: {
85+
margin: \`\${tokens.spacingVerticalS} 0\`,
86+
display: 'inline-block',
87+
color: tokens.colorNeutralForeground1Selected,
88+
}
89+
});
90+
`;
91+
92+
const objectExpression = getFirstObjectExpression(code);
93+
const evaluation = astEvaluator(objectExpression);
94+
95+
expect(evaluation.confident).toBe(true);
96+
expect(evaluation.value).toEqual({
97+
staticWrapper: {
98+
margin: 'var(--spacingVerticalS) 0',
99+
display: 'inline-block',
100+
color: 'var(--colorNeutralForeground1Selected)',
101+
},
102+
});
103+
});
104+
});
75105
});

packages/transform/src/evaluation/batchEvaluator.mts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import type { StyleCall } from '../types.mjs';
44
import { astEvaluator } from './astEvaluator.mjs';
55
import { vmEvaluator } from './vmEvaluator.mjs';
66

7-
const EVALUATION_SKIPPED = '__EVALUATION_SKIPPED__';
8-
97
/**
108
* Batch evaluates all style calls in a file for better performance.
119
* Uses static evaluation first, then falls back to VM evaluation for complex expressions.
@@ -23,7 +21,7 @@ export function batchEvaluator(
2321
} {
2422
const evaluationResults: unknown[] = new Array(styleCalls.length);
2523

26-
const argumentsCode = new Array(styleCalls.length).fill(EVALUATION_SKIPPED);
24+
const argumentsCode = new Array(styleCalls.length).fill(null);
2725
let vmEvaluationNeeded = false;
2826

2927
// First pass: try static evaluation for all calls
@@ -62,7 +60,7 @@ export function batchEvaluator(
6260
const vmValues = vmResult.value as unknown[];
6361

6462
for (let i = 0; i < vmValues.length; i++) {
65-
if (vmValues[i] === EVALUATION_SKIPPED) {
63+
if (vmValues[i] === null) {
6664
// This was already evaluated statically
6765
continue;
6866
}

packages/transform/src/evaluation/vmEvaluator.mts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ if (typeof module !== 'undefined' && module.exports) {
4848
const result = (mod.exports as { __mkPreval: unknown }).__mkPreval;
4949

5050
if (isError(result)) {
51-
return { confident: false };
51+
return { confident: false, error: result };
5252
}
5353

5454
return { confident: true, value: result };

packages/transform/src/transformSync.mts

Lines changed: 30 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { parseSync, type Node } from 'oxc-parser';
22
import { walk } from 'oxc-walker';
33
import MagicString from 'magic-string';
44
import { type Evaluator, type StrictOptions } from '@linaria/babel-preset';
5-
import shakerEvaluator from '@linaria/shaker';
5+
import _shaker from '@linaria/shaker';
66
import {
77
resolveStyleRulesForSlots,
88
resolveResetStyleRules,
@@ -17,6 +17,8 @@ import { batchEvaluator } from './evaluation/batchEvaluator.mjs';
1717
import { dedupeCSSRules } from './utils/dedupeCSSRules.mjs';
1818
import type { StyleCall } from './types.mjs';
1919

20+
const shakerEvaluator = (_shaker.default || _shaker) as unknown as Evaluator;
21+
2022
export type TransformOptions = {
2123
filename: string;
2224

@@ -143,31 +145,30 @@ function concatCSSRulesByBucket(bucketA: CSSRulesByBucket = {}, bucketB: CSSRule
143145
* Transforms passed source code with oxc-parser and oxc-walker instead of Babel.
144146
*/
145147
export function transformSync(sourceCode: string, options: TransformOptions): TransformResult {
146-
if (!options.filename) {
148+
const {
149+
babelOptions = {},
150+
filename,
151+
classNameHashSalt = '',
152+
generateMetadata = false,
153+
modules = ['@griffel/core', '@griffel/react', '@fluentui/react-components'],
154+
evaluationRules = [
155+
{ action: shakerEvaluator },
156+
{
157+
test: /[/\\]node_modules[/\\]/,
158+
action: 'ignore',
159+
},
160+
],
161+
} = options;
162+
163+
if (!filename) {
147164
throw new Error('Transform error: "filename" option is required');
148165
}
149166

150-
const evaluationRules: NonNullable<StrictOptions['rules']> = [
151-
// TODO: why TS error there?!
152-
{ action: shakerEvaluator as unknown as Evaluator },
153-
{
154-
test: /[/\\]node_modules[/\\]/,
155-
action: 'ignore',
156-
},
157-
];
158-
const transformOptions: Required<TransformOptions> = {
159-
babelOptions: {},
160-
classNameHashSalt: '',
161-
generateMetadata: false,
162-
modules: ['@griffel/core', '@griffel/react', '@fluentui/react-components'],
163-
evaluationRules,
164-
...options,
165-
};
166-
167-
const parseResult = parseSync(options.filename, sourceCode);
167+
const parseResult = parseSync(filename, sourceCode);
168+
parseResult.module.staticImports; // TODO use it over imports collected manually
168169

169170
if (parseResult.errors.length > 0) {
170-
throw new Error(`Failed to parse "${options.filename}": ${parseResult.errors.map(e => e.message).join(', ')}`);
171+
throw new Error(`Failed to parse "${filename}": ${parseResult.errors.map(e => e.message).join(', ')}`);
171172
}
172173

173174
const magicString = new MagicString(sourceCode);
@@ -189,7 +190,7 @@ export function transformSync(sourceCode: string, options: TransformOptions): Tr
189190
if (node.type === 'ImportDeclaration') {
190191
const moduleSource = node.source.value;
191192

192-
if (transformOptions.modules.includes(moduleSource)) {
193+
if (modules.includes(moduleSource)) {
193194
const specifiers = node.specifiers.reduce<
194195
{
195196
imported: string;
@@ -309,10 +310,10 @@ export function transformSync(sourceCode: string, options: TransformOptions): Tr
309310
// Process style calls - evaluate and transform
310311
const { evaluationResults, usedVMForEvaluation } = batchEvaluator(
311312
sourceCode,
312-
options.filename,
313+
filename,
313314
styleCalls,
314-
transformOptions.babelOptions,
315-
transformOptions.evaluationRules,
315+
babelOptions,
316+
evaluationRules,
316317
);
317318

318319
for (let i = 0; i < styleCalls.length; i++) {
@@ -324,13 +325,10 @@ export function transformSync(sourceCode: string, options: TransformOptions): Tr
324325
{
325326
const stylesBySlots = evaluationResult as Record<string, GriffelStyle>;
326327
// TODO fix naming
327-
const [classnamesMapping, cssRulesByBucketA] = resolveStyleRulesForSlots(
328-
stylesBySlots,
329-
transformOptions.classNameHashSalt,
330-
);
328+
const [classnamesMapping, cssRulesByBucketA] = resolveStyleRulesForSlots(stylesBySlots, classNameHashSalt);
331329
const uniqueCSSRules = dedupeCSSRules(cssRulesByBucket);
332330

333-
if (transformOptions.generateMetadata) {
331+
if (generateMetadata) {
334332
buildCSSEntriesMetadata(cssEntries, classnamesMapping, uniqueCSSRules, styleCall.declaratorId);
335333
}
336334

@@ -344,12 +342,9 @@ export function transformSync(sourceCode: string, options: TransformOptions): Tr
344342
case 'makeResetStyles':
345343
{
346344
const styles = evaluationResult as GriffelResetStyle;
347-
const [ltrClassName, rtlClassName, cssRules] = resolveResetStyleRules(
348-
styles,
349-
transformOptions.classNameHashSalt,
350-
);
345+
const [ltrClassName, rtlClassName, cssRules] = resolveResetStyleRules(styles, classNameHashSalt);
351346

352-
if (transformOptions.generateMetadata) {
347+
if (generateMetadata) {
353348
buildCSSResetEntriesMetadata(cssResetEntries, cssRules, styleCall.declaratorId);
354349
}
355350

0 commit comments

Comments
 (0)