@griffel/transform 3.0.4 → 3.0.5

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.
Files changed (188) hide show
  1. package/CHANGELOG.json +404 -0
  2. package/CHANGELOG.md +130 -0
  3. package/__fixtures__/assets/blank.jpg +0 -0
  4. package/__fixtures__/assets/code.ts +12 -0
  5. package/__fixtures__/assets/empty.jpg +0 -0
  6. package/__fixtures__/assets/output.meta.json +12 -0
  7. package/__fixtures__/assets/output.ts +12 -0
  8. package/__fixtures__/assets-multiple-declarations/blank.jpg +0 -0
  9. package/__fixtures__/assets-multiple-declarations/code.ts +8 -0
  10. package/__fixtures__/assets-multiple-declarations/empty.jpg +0 -0
  11. package/__fixtures__/assets-multiple-declarations/output.meta.json +9 -0
  12. package/__fixtures__/assets-multiple-declarations/output.ts +6 -0
  13. package/__fixtures__/assets-reset-styles/blank.jpg +0 -0
  14. package/__fixtures__/assets-reset-styles/code.ts +11 -0
  15. package/__fixtures__/assets-reset-styles/empty.jpg +0 -0
  16. package/__fixtures__/assets-reset-styles/output.meta.json +11 -0
  17. package/__fixtures__/assets-reset-styles/output.ts +7 -0
  18. package/__fixtures__/assets-urls/code.ts +13 -0
  19. package/__fixtures__/assets-urls/output.meta.json +14 -0
  20. package/__fixtures__/assets-urls/output.ts +10 -0
  21. package/__fixtures__/at-rules/code.ts +14 -0
  22. package/__fixtures__/at-rules/output.meta.json +15 -0
  23. package/__fixtures__/at-rules/output.ts +10 -0
  24. package/__fixtures__/config-classname-hash-salt/code.ts +5 -0
  25. package/__fixtures__/config-classname-hash-salt/output.meta.json +7 -0
  26. package/__fixtures__/config-classname-hash-salt/output.ts +3 -0
  27. package/__fixtures__/config-evaluation-rules/code.ts +8 -0
  28. package/__fixtures__/config-evaluation-rules/consts.ts +1 -0
  29. package/__fixtures__/config-evaluation-rules/output.meta.json +7 -0
  30. package/__fixtures__/config-evaluation-rules/output.ts +4 -0
  31. package/__fixtures__/config-evaluation-rules/sampleEvaluator.cjs +12 -0
  32. package/__fixtures__/error-argument-count/fixture.js +4 -0
  33. package/__fixtures__/error-cjs/fixture.js +4 -0
  34. package/__fixtures__/error-on-undefined/fixture.ts +7 -0
  35. package/__fixtures__/error-on-undefined/tokens.ts +1 -0
  36. package/__fixtures__/export-default/code.ts +6 -0
  37. package/__fixtures__/export-default/output.meta.json +14 -0
  38. package/__fixtures__/export-default/output.ts +6 -0
  39. package/__fixtures__/function-mixin/code.ts +6 -0
  40. package/__fixtures__/function-mixin/mixins.ts +7 -0
  41. package/__fixtures__/function-mixin/output.meta.json +7 -0
  42. package/__fixtures__/function-mixin/output.ts +4 -0
  43. package/__fixtures__/function-mixin/tokens.ts +3 -0
  44. package/__fixtures__/import-alias/code.ts +5 -0
  45. package/__fixtures__/import-alias/output.meta.json +7 -0
  46. package/__fixtures__/import-alias/output.ts +3 -0
  47. package/__fixtures__/import-custom-module/code.ts +6 -0
  48. package/__fixtures__/import-custom-module/output.meta.json +7 -0
  49. package/__fixtures__/import-custom-module/output.ts +4 -0
  50. package/__fixtures__/keyframes/code.ts +22 -0
  51. package/__fixtures__/keyframes/output.meta.json +17 -0
  52. package/__fixtures__/keyframes/output.ts +6 -0
  53. package/__fixtures__/multiple-declarations/code.ts +8 -0
  54. package/__fixtures__/multiple-declarations/output.meta.json +7 -0
  55. package/__fixtures__/multiple-declarations/output.ts +4 -0
  56. package/__fixtures__/non-existing-module-call/code.ts +10 -0
  57. package/__fixtures__/non-existing-module-call/module.ts +13 -0
  58. package/__fixtures__/non-existing-module-call/output.meta.json +7 -0
  59. package/__fixtures__/non-existing-module-call/output.ts +6 -0
  60. package/__fixtures__/object/code.ts +6 -0
  61. package/__fixtures__/object/output.meta.json +14 -0
  62. package/__fixtures__/object/output.ts +6 -0
  63. package/__fixtures__/object-computed-keys/code.ts +8 -0
  64. package/__fixtures__/object-computed-keys/output.meta.json +14 -0
  65. package/__fixtures__/object-computed-keys/output.ts +8 -0
  66. package/__fixtures__/object-imported-keys/code.ts +9 -0
  67. package/__fixtures__/object-imported-keys/consts.ts +2 -0
  68. package/__fixtures__/object-imported-keys/output.meta.json +7 -0
  69. package/__fixtures__/object-imported-keys/output.ts +4 -0
  70. package/__fixtures__/object-mixins/code.ts +11 -0
  71. package/__fixtures__/object-mixins/mixins.ts +17 -0
  72. package/__fixtures__/object-mixins/output.meta.json +16 -0
  73. package/__fixtures__/object-mixins/output.ts +10 -0
  74. package/__fixtures__/object-mixins/tokens.ts +3 -0
  75. package/__fixtures__/object-nesting/code.ts +13 -0
  76. package/__fixtures__/object-nesting/consts.ts +1 -0
  77. package/__fixtures__/object-nesting/output.meta.json +9 -0
  78. package/__fixtures__/object-nesting/output.ts +6 -0
  79. package/__fixtures__/object-reset/code.ts +8 -0
  80. package/__fixtures__/object-reset/output.meta.json +7 -0
  81. package/__fixtures__/object-reset/output.ts +5 -0
  82. package/__fixtures__/object-sequence-expr/code.ts +16 -0
  83. package/__fixtures__/object-sequence-expr/output.meta.json +7 -0
  84. package/__fixtures__/object-sequence-expr/output.ts +7 -0
  85. package/__fixtures__/object-shorthands/code.ts +8 -0
  86. package/__fixtures__/object-shorthands/output.meta.json +20 -0
  87. package/__fixtures__/object-shorthands/output.ts +5 -0
  88. package/__fixtures__/object-variables/code.ts +9 -0
  89. package/__fixtures__/object-variables/output.meta.json +15 -0
  90. package/__fixtures__/object-variables/output.ts +9 -0
  91. package/__fixtures__/object-variables/vars.ts +5 -0
  92. package/__fixtures__/reset-styles/code.ts +6 -0
  93. package/__fixtures__/reset-styles/output.meta.json +7 -0
  94. package/__fixtures__/reset-styles/output.ts +3 -0
  95. package/__fixtures__/reset-styles-at-rules/code.ts +7 -0
  96. package/__fixtures__/reset-styles-at-rules/output.meta.json +8 -0
  97. package/__fixtures__/reset-styles-at-rules/output.ts +3 -0
  98. package/__fixtures__/rules-with-metadata/code.ts +9 -0
  99. package/__fixtures__/rules-with-metadata/output.meta.json +14 -0
  100. package/__fixtures__/rules-with-metadata/output.ts +3 -0
  101. package/__fixtures__/shared-mixins/code.ts +7 -0
  102. package/__fixtures__/shared-mixins/mixins.ts +6 -0
  103. package/__fixtures__/shared-mixins/output.meta.json +7 -0
  104. package/__fixtures__/shared-mixins/output.ts +8 -0
  105. package/__fixtures__/static-styles/code.ts +7 -0
  106. package/__fixtures__/static-styles/output.meta.json +7 -0
  107. package/__fixtures__/static-styles/output.ts +3 -0
  108. package/__fixtures__/static-styles-array/code.ts +10 -0
  109. package/__fixtures__/static-styles-array/output.meta.json +7 -0
  110. package/__fixtures__/static-styles-array/output.ts +3 -0
  111. package/__fixtures__/static-styles-string/code.ts +3 -0
  112. package/__fixtures__/static-styles-string/output.meta.json +7 -0
  113. package/__fixtures__/static-styles-string/output.ts +3 -0
  114. package/__fixtures__/tokens/code.ts +11 -0
  115. package/__fixtures__/tokens/output.meta.json +12 -0
  116. package/__fixtures__/tokens/output.ts +7 -0
  117. package/__fixtures__/tokens/tokens.ts +4 -0
  118. package/__fixtures__/unsupported-css-properties/fixture.ts +16 -0
  119. package/__fixtures__/unsupported-css-properties/output.meta.json +5 -0
  120. package/__fixtures__/unsupported-css-properties/output.ts +3 -0
  121. package/eslint.config.mjs +31 -0
  122. package/package.json +7 -8
  123. package/project.json +51 -0
  124. package/src/{constants.mjs → constants.mts} +0 -1
  125. package/src/evaluation/astEvaluator.mts +109 -0
  126. package/src/evaluation/astEvaluator.test.mts +126 -0
  127. package/src/evaluation/batchEvaluator.mts +79 -0
  128. package/src/evaluation/evalCache.mts +84 -0
  129. package/src/evaluation/fluentTokensPlugin.mts +82 -0
  130. package/src/evaluation/fluentTokensPlugin.test.mts +130 -0
  131. package/src/evaluation/module.mts +271 -0
  132. package/src/evaluation/module.test.mts +133 -0
  133. package/src/evaluation/{process.mjs → process.mts} +12 -7
  134. package/src/evaluation/types.mts +49 -0
  135. package/src/evaluation/vmEvaluator.mts +45 -0
  136. package/src/evaluation/vmEvaluator.test.mts +30 -0
  137. package/src/{index.d.mts → index.mts} +4 -0
  138. package/src/transformSync.mts +425 -0
  139. package/src/transformSync.test.mts +429 -0
  140. package/src/types.mts +13 -0
  141. package/src/utils/convertESMtoCJS.mts +226 -0
  142. package/src/utils/convertESMtoCJS.test.mts +159 -0
  143. package/src/utils/dedupeCSSRules.mts +25 -0
  144. package/src/utils/dedupeCSSRules.test.mts +39 -0
  145. package/tsconfig.json +19 -0
  146. package/tsconfig.lib.json +30 -0
  147. package/tsconfig.spec.json +24 -0
  148. package/vitest.config.mts +18 -0
  149. package/LICENSE.md +0 -21
  150. package/src/constants.d.mts +0 -2
  151. package/src/constants.mjs.map +0 -1
  152. package/src/evaluation/astEvaluator.d.mts +0 -20
  153. package/src/evaluation/astEvaluator.mjs +0 -90
  154. package/src/evaluation/astEvaluator.mjs.map +0 -1
  155. package/src/evaluation/batchEvaluator.d.mts +0 -13
  156. package/src/evaluation/batchEvaluator.mjs +0 -54
  157. package/src/evaluation/batchEvaluator.mjs.map +0 -1
  158. package/src/evaluation/evalCache.d.mts +0 -9
  159. package/src/evaluation/evalCache.mjs +0 -65
  160. package/src/evaluation/evalCache.mjs.map +0 -1
  161. package/src/evaluation/fluentTokensPlugin.d.mts +0 -2
  162. package/src/evaluation/fluentTokensPlugin.mjs +0 -70
  163. package/src/evaluation/fluentTokensPlugin.mjs.map +0 -1
  164. package/src/evaluation/module.d.mts +0 -44
  165. package/src/evaluation/module.mjs +0 -207
  166. package/src/evaluation/module.mjs.map +0 -1
  167. package/src/evaluation/process.d.mts +0 -24
  168. package/src/evaluation/process.mjs.map +0 -1
  169. package/src/evaluation/types.d.mts +0 -34
  170. package/src/evaluation/types.mjs +0 -2
  171. package/src/evaluation/types.mjs.map +0 -1
  172. package/src/evaluation/vmEvaluator.d.mts +0 -3
  173. package/src/evaluation/vmEvaluator.mjs +0 -33
  174. package/src/evaluation/vmEvaluator.mjs.map +0 -1
  175. package/src/index.mjs +0 -9
  176. package/src/index.mjs.map +0 -1
  177. package/src/transformSync.d.mts +0 -41
  178. package/src/transformSync.mjs +0 -252
  179. package/src/transformSync.mjs.map +0 -1
  180. package/src/types.d.mts +0 -12
  181. package/src/types.mjs +0 -2
  182. package/src/types.mjs.map +0 -1
  183. package/src/utils/convertESMtoCJS.d.mts +0 -6
  184. package/src/utils/convertESMtoCJS.mjs +0 -203
  185. package/src/utils/convertESMtoCJS.mjs.map +0 -1
  186. package/src/utils/dedupeCSSRules.d.mts +0 -6
  187. package/src/utils/dedupeCSSRules.mjs +0 -19
  188. package/src/utils/dedupeCSSRules.mjs.map +0 -1
@@ -0,0 +1,126 @@
1
+ import { parseSync, type ObjectExpression, type Program } from 'oxc-parser';
2
+ import { walk } from 'oxc-walker';
3
+ import { describe, it, expect } from 'vitest';
4
+
5
+ import { astEvaluator } from './astEvaluator.mjs';
6
+ import { fluentTokensPlugin } from './fluentTokensPlugin.mjs';
7
+
8
+ function getFirstObjectExpression(code: string): { node: ObjectExpression; program: Program } {
9
+ const result = parseSync('test.js', code);
10
+
11
+ if (result.errors.length > 0) {
12
+ throw new Error(`Parsing errors: ${result.errors.map(e => e.message).join(', ')}`);
13
+ }
14
+
15
+ let objectExpression: ObjectExpression | null = null;
16
+
17
+ walk(result.program, {
18
+ enter(node) {
19
+ if (node.type === 'ObjectExpression' && !objectExpression) {
20
+ objectExpression = node;
21
+ }
22
+ },
23
+ });
24
+
25
+ if (!objectExpression) {
26
+ throw new Error('No "ObjectExpression" found in the code');
27
+ }
28
+
29
+ return { node: objectExpression, program: result.program };
30
+ }
31
+
32
+ describe('staticEvaluator', () => {
33
+ it('evaluates simple object expressions', () => {
34
+ const code = 'const obj = { color: "red", size: 14 };';
35
+
36
+ const { node, program } = getFirstObjectExpression(code);
37
+ const evaluation = astEvaluator(node, program);
38
+
39
+ expect(evaluation.confident).toBe(true);
40
+ expect(evaluation.value).toEqual({ color: 'red', size: 14 });
41
+ });
42
+
43
+ it('evaluates nested object expressions', () => {
44
+ const code = 'const obj = { root: { color: "blue", padding: 0 }, secondary: { bg: "white" } };';
45
+
46
+ const { node, program } = getFirstObjectExpression(code);
47
+ const evaluation = astEvaluator(node, program);
48
+
49
+ expect(evaluation.confident).toBe(true);
50
+ expect(evaluation.value).toEqual({
51
+ root: { color: 'blue', padding: 0 },
52
+ secondary: { bg: 'white' },
53
+ });
54
+ });
55
+
56
+ it('handles mixed literal types', () => {
57
+ const code = 'const obj = { str: "hello", num: 42, bool: true, nil: null };';
58
+
59
+ const { node, program } = getFirstObjectExpression(code);
60
+ const evaluation = astEvaluator(node, program);
61
+
62
+ expect(evaluation.confident).toBe(true);
63
+ expect(evaluation.value).toEqual({
64
+ str: 'hello',
65
+ num: 42,
66
+ bool: true,
67
+ nil: null,
68
+ });
69
+ });
70
+
71
+ it('returns confident: false for unsupported expressions', () => {
72
+ const code = 'const obj = { computed: variable };';
73
+
74
+ const { node, program } = getFirstObjectExpression(code);
75
+ const evaluation = astEvaluator(node, program);
76
+
77
+ expect(evaluation.confident).toBe(false);
78
+ expect(evaluation.value).toBeUndefined();
79
+ });
80
+
81
+ it('returns confident: false for TemplateLiteral without plugins', () => {
82
+ const code = 'const obj = { margin: `${tokens.spacingVerticalS} 0` };';
83
+
84
+ const { node, program } = getFirstObjectExpression(code);
85
+ const evaluation = astEvaluator(node, program);
86
+
87
+ expect(evaluation.confident).toBe(false);
88
+ expect(evaluation.value).toBeUndefined();
89
+ });
90
+
91
+ it('returns confident: false for MemberExpression without plugins', () => {
92
+ const code = 'const obj = { color: tokens.colorNeutralForeground1Selected };';
93
+
94
+ const { node, program } = getFirstObjectExpression(code);
95
+ const evaluation = astEvaluator(node, program);
96
+
97
+ expect(evaluation.confident).toBe(false);
98
+ expect(evaluation.value).toBeUndefined();
99
+ });
100
+
101
+ describe('@fluentui/react-components integration', () => {
102
+ it('evaluates style object with tokens when fluentTokensPlugin is provided', () => {
103
+ const code = `
104
+ export const useButtonStyles = makeStyles({
105
+ staticWrapper: {
106
+ margin: \`\${tokens.spacingVerticalS} 0\`,
107
+ display: 'inline-block',
108
+ color: tokens.colorNeutralForeground1Selected,
109
+ }
110
+ });
111
+ `;
112
+
113
+ const { node, program } = getFirstObjectExpression(code);
114
+ const evaluation = astEvaluator(node, program, [fluentTokensPlugin]);
115
+
116
+ expect(evaluation.confident).toBe(true);
117
+ expect(evaluation.value).toEqual({
118
+ staticWrapper: {
119
+ margin: 'var(--spacingVerticalS) 0',
120
+ display: 'inline-block',
121
+ color: 'var(--colorNeutralForeground1Selected)',
122
+ },
123
+ });
124
+ });
125
+ });
126
+ });
@@ -0,0 +1,79 @@
1
+ import type { Program } from 'oxc-parser';
2
+
3
+ import type { StyleCall } from '../types.mjs';
4
+ import type { TransformResolver } from './module.mjs';
5
+ import type { AstEvaluatorPlugin, EvalRule } from './types.mjs';
6
+ import { astEvaluator } from './astEvaluator.mjs';
7
+ import { vmEvaluator } from './vmEvaluator.mjs';
8
+
9
+ /**
10
+ * Batch evaluates all style calls in a file for better performance.
11
+ * Uses static evaluation first, then falls back to VM evaluation for complex expressions.
12
+ * Optimizes VM evaluation by sharing module loading and parsing overhead.
13
+ */
14
+ export function batchEvaluator(
15
+ sourceCode: string,
16
+ filename: string,
17
+ styleCalls: StyleCall[],
18
+ evaluationRules: EvalRule[],
19
+ resolveFilename: TransformResolver,
20
+ programAst: Program,
21
+ astEvaluationPlugins: AstEvaluatorPlugin[] = [],
22
+ ): {
23
+ usedVMForEvaluation: boolean;
24
+ evaluationResults: unknown[];
25
+ } {
26
+ const evaluationResults: unknown[] = new Array(styleCalls.length);
27
+
28
+ // Track which indices need VM evaluation and build the expression code string
29
+ const vmIndices: number[] = [];
30
+ let expressionCode = '';
31
+
32
+ // First pass: try static evaluation for all calls
33
+ for (let i = 0; i < styleCalls.length; i++) {
34
+ const styleCall = styleCalls[i];
35
+ const staticResult = astEvaluator(styleCall.argumentNode, programAst, astEvaluationPlugins);
36
+
37
+ if (staticResult.confident) {
38
+ evaluationResults[i] = staticResult.value;
39
+ continue;
40
+ }
41
+
42
+ // Mark for VM evaluation
43
+ if (expressionCode.length > 0) {
44
+ expressionCode += ',';
45
+ }
46
+ expressionCode += styleCall.argumentCode;
47
+ vmIndices.push(i);
48
+ }
49
+
50
+ if (vmIndices.length === 0) {
51
+ return {
52
+ usedVMForEvaluation: false,
53
+ evaluationResults,
54
+ };
55
+ }
56
+
57
+ // Second pass: batch VM evaluation for complex expressions
58
+ const vmResult = vmEvaluator(sourceCode, filename, expressionCode, evaluationRules, resolveFilename);
59
+
60
+ if (!vmResult.confident) {
61
+ if (vmResult.error) {
62
+ throw vmResult.error;
63
+ } else {
64
+ throw new Error('Evaluation of a code fragment failed, this is a bug, please report it');
65
+ }
66
+ }
67
+
68
+ // Map VM results back to correct indices
69
+ const vmValues = vmResult.value as unknown[];
70
+
71
+ for (let i = 0; i < vmIndices.length; i++) {
72
+ evaluationResults[vmIndices[i]] = vmValues[i];
73
+ }
74
+
75
+ return {
76
+ usedVMForEvaluation: true,
77
+ evaluationResults,
78
+ };
79
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Evaluation cache for module results.
3
+ * Copied from @linaria/babel-preset v3.0.0-beta.19, adapted to use `debug` package.
4
+ */
5
+
6
+ import { createHash } from 'node:crypto';
7
+ import createDebug from 'debug';
8
+
9
+ const debug = createDebug('griffel:eval-cache');
10
+
11
+ const fileHashes = new Map<string, string>();
12
+ const evalCache = new Map<string, unknown>();
13
+ const fileKeys = new Map<string, string[]>();
14
+
15
+ const hash = (text: string): string => createHash('sha1').update(text).digest('base64');
16
+
17
+ let lastText = '';
18
+ let lastHash = hash(lastText);
19
+
20
+ const memoizedHash = (text: string): string => {
21
+ if (lastText !== text) {
22
+ lastHash = hash(text);
23
+ lastText = text;
24
+ }
25
+
26
+ return lastHash;
27
+ };
28
+
29
+ const toKey = (filename: string, exports: string[]): string =>
30
+ exports.length > 0 ? `${filename}:${exports.join(',')}` : filename;
31
+
32
+ export const clear = (): void => {
33
+ fileHashes.clear();
34
+ evalCache.clear();
35
+ fileKeys.clear();
36
+ };
37
+
38
+ export const clearForFile = (filename: string): void => {
39
+ const keys = fileKeys.get(filename) ?? [];
40
+
41
+ if (keys.length === 0) {
42
+ return;
43
+ }
44
+
45
+ debug('clear-for-file', filename);
46
+ keys.forEach(key => {
47
+ fileHashes.delete(key);
48
+ evalCache.delete(key);
49
+ });
50
+ fileKeys.set(filename, []);
51
+ };
52
+
53
+ export const has = ([filename, ...exports]: string[], text: string): boolean => {
54
+ const key = toKey(filename, exports);
55
+ const textHash = memoizedHash(text);
56
+ debug('has', `${key} ${textHash}`);
57
+ return fileHashes.get(key) === textHash;
58
+ };
59
+
60
+ export const get = ([filename, ...exports]: string[], text: string): unknown => {
61
+ const key = toKey(filename, exports);
62
+ const textHash = memoizedHash(text);
63
+ debug('get', `${key} ${textHash}`);
64
+
65
+ if (fileHashes.get(key) !== textHash) {
66
+ return undefined;
67
+ }
68
+
69
+ return evalCache.get(key);
70
+ };
71
+
72
+ export const set = ([filename, ...exports]: string[], text: string, value: unknown): void => {
73
+ const key = toKey(filename, exports);
74
+ const textHash = memoizedHash(text);
75
+ debug('set', `${key} ${textHash}`);
76
+ fileHashes.set(key, textHash);
77
+ evalCache.set(key, value);
78
+
79
+ if (!fileKeys.has(filename)) {
80
+ fileKeys.set(filename, []);
81
+ }
82
+
83
+ fileKeys.get(filename)!.push(key);
84
+ };
@@ -0,0 +1,82 @@
1
+ import type { MemberExpression, TemplateLiteral } from 'oxc-parser';
2
+
3
+ import { DEOPT, type Deopt } from './astEvaluator.mjs';
4
+ import type { AstEvaluatorPlugin } from './types.mjs';
5
+
6
+ // zIndex tokens are not part of the theme and include default fallback values.
7
+ // See: https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/zIndexes.ts
8
+ const Z_INDEX_DEFAULTS: Record<string, number> = {
9
+ zIndexBackground: 0,
10
+ zIndexContent: 1,
11
+ zIndexOverlay: 1000,
12
+ zIndexPopup: 2000,
13
+ zIndexMessages: 3000,
14
+ zIndexFloating: 4000,
15
+ zIndexPriority: 5000,
16
+ zIndexDebug: 6000,
17
+ };
18
+
19
+ /**
20
+ * Evaluates template literals that contain Fluent UI design tokens.
21
+ * Transforms `${tokens.propertyName}` expressions into CSS custom properties: `var(--propertyName)`
22
+ */
23
+ function evaluateTemplateLiteralWithTokens(node: TemplateLiteral): string | Deopt {
24
+ let result = '';
25
+
26
+ for (let i = 0; i < node.quasis.length; i++) {
27
+ result += node.quasis[i].value.cooked;
28
+
29
+ if (i < node.expressions.length) {
30
+ const expression = node.expressions[i];
31
+
32
+ if (
33
+ expression.type === 'MemberExpression' &&
34
+ expression.object.type === 'Identifier' &&
35
+ expression.object.name === 'tokens' &&
36
+ expression.property.type === 'Identifier' &&
37
+ !expression.computed
38
+ ) {
39
+ const name = expression.property.name;
40
+ const fallback = Z_INDEX_DEFAULTS[name];
41
+ result += fallback !== undefined ? `var(--${name}, ${fallback})` : `var(--${name})`;
42
+ } else {
43
+ return DEOPT;
44
+ }
45
+ }
46
+ }
47
+
48
+ return result;
49
+ }
50
+
51
+ /**
52
+ * Evaluates member expressions that reference Fluent UI design tokens.
53
+ * Transforms `tokens.propertyName` expressions into CSS custom properties: `var(--propertyName)`
54
+ */
55
+ function evaluateTokensMemberExpression(node: MemberExpression): string | Deopt {
56
+ if (
57
+ node.object.type === 'Identifier' &&
58
+ node.object.name === 'tokens' &&
59
+ node.property.type === 'Identifier' &&
60
+ !node.computed
61
+ ) {
62
+ const name = node.property.name;
63
+ const fallback = Z_INDEX_DEFAULTS[name];
64
+ return fallback !== undefined ? `var(--${name}, ${fallback})` : `var(--${name})`;
65
+ } else {
66
+ return DEOPT;
67
+ }
68
+ }
69
+
70
+ export const fluentTokensPlugin: AstEvaluatorPlugin = {
71
+ name: 'fluentTokensPlugin',
72
+ evaluateNode(node) {
73
+ switch (node.type) {
74
+ case 'TemplateLiteral':
75
+ return evaluateTemplateLiteralWithTokens(node);
76
+ case 'MemberExpression':
77
+ return evaluateTokensMemberExpression(node);
78
+ default:
79
+ return DEOPT;
80
+ }
81
+ },
82
+ };
@@ -0,0 +1,130 @@
1
+ import { parseSync, type Node, type Program } from 'oxc-parser';
2
+ import { walk } from 'oxc-walker';
3
+ import { describe, it, expect } from 'vitest';
4
+
5
+ import { DEOPT } from './astEvaluator.mjs';
6
+ import { fluentTokensPlugin } from './fluentTokensPlugin.mjs';
7
+ import type { AstEvaluatorContext } from './types.mjs';
8
+
9
+ function getFirstNodeOfType(code: string, type: string): { node: Node; program: Program } {
10
+ const result = parseSync('test.js', code);
11
+
12
+ if (result.errors.length > 0) {
13
+ throw new Error(`Parsing errors: ${result.errors.map(e => e.message).join(', ')}`);
14
+ }
15
+
16
+ let found: Node | null = null;
17
+
18
+ walk(result.program, {
19
+ enter(node) {
20
+ if (node.type === type && !found) {
21
+ found = node;
22
+ }
23
+ },
24
+ });
25
+
26
+ if (!found) {
27
+ throw new Error(`No "${type}" found in the code`);
28
+ }
29
+
30
+ return { node: found, program: result.program };
31
+ }
32
+
33
+ function makeContext(program: Program): AstEvaluatorContext {
34
+ return {
35
+ programAst: program,
36
+ evaluateNode: () => DEOPT,
37
+ };
38
+ }
39
+
40
+ describe('fluentTokensPlugin', () => {
41
+ describe('MemberExpression', () => {
42
+ it('evaluates tokens.propertyName to var(--propertyName)', () => {
43
+ const code = 'const x = tokens.colorBrandBackground;';
44
+ const { node, program } = getFirstNodeOfType(code, 'MemberExpression');
45
+
46
+ const result = fluentTokensPlugin.evaluateNode(node, makeContext(program));
47
+ expect(result).toBe('var(--colorBrandBackground)');
48
+ });
49
+
50
+ it('returns DEOPT for non-tokens member expression', () => {
51
+ const code = 'const x = foo.bar;';
52
+ const { node, program } = getFirstNodeOfType(code, 'MemberExpression');
53
+
54
+ expect(fluentTokensPlugin.evaluateNode(node, makeContext(program))).toBe(DEOPT);
55
+ });
56
+
57
+ it('returns DEOPT for computed member expression on tokens', () => {
58
+ const code = 'const x = tokens["colorBrandBackground"];';
59
+ const { node, program } = getFirstNodeOfType(code, 'MemberExpression');
60
+
61
+ expect(fluentTokensPlugin.evaluateNode(node, makeContext(program))).toBe(DEOPT);
62
+ });
63
+
64
+ it('evaluates zIndex tokens with fallback values', () => {
65
+ const code = 'const x = tokens.zIndexPopup;';
66
+ const { node, program } = getFirstNodeOfType(code, 'MemberExpression');
67
+
68
+ expect(fluentTokensPlugin.evaluateNode(node, makeContext(program))).toBe('var(--zIndexPopup, 2000)');
69
+ });
70
+
71
+ it('evaluates all zIndex tokens with correct fallbacks', () => {
72
+ const cases: [string, string][] = [
73
+ ['tokens.zIndexBackground', 'var(--zIndexBackground, 0)'],
74
+ ['tokens.zIndexContent', 'var(--zIndexContent, 1)'],
75
+ ['tokens.zIndexOverlay', 'var(--zIndexOverlay, 1000)'],
76
+ ['tokens.zIndexPopup', 'var(--zIndexPopup, 2000)'],
77
+ ['tokens.zIndexMessages', 'var(--zIndexMessages, 3000)'],
78
+ ['tokens.zIndexFloating', 'var(--zIndexFloating, 4000)'],
79
+ ['tokens.zIndexPriority', 'var(--zIndexPriority, 5000)'],
80
+ ['tokens.zIndexDebug', 'var(--zIndexDebug, 6000)'],
81
+ ];
82
+
83
+ for (const [expr, expected] of cases) {
84
+ const { node, program } = getFirstNodeOfType(`const x = ${expr};`, 'MemberExpression');
85
+ expect(fluentTokensPlugin.evaluateNode(node, makeContext(program))).toBe(expected);
86
+ }
87
+ });
88
+ });
89
+
90
+ describe('TemplateLiteral', () => {
91
+ it('evaluates template literal with tokens expression', () => {
92
+ const code = 'const x = `${tokens.spacingVerticalS} 0`;';
93
+ const { node, program } = getFirstNodeOfType(code, 'TemplateLiteral');
94
+
95
+ const result = fluentTokensPlugin.evaluateNode(node, makeContext(program));
96
+ expect(result).toBe('var(--spacingVerticalS) 0');
97
+ });
98
+
99
+ it('evaluates simple template literal without expressions', () => {
100
+ const code = 'const x = `hello world`;';
101
+ const { node, program } = getFirstNodeOfType(code, 'TemplateLiteral');
102
+
103
+ const result = fluentTokensPlugin.evaluateNode(node, makeContext(program));
104
+ expect(result).toBe('hello world');
105
+ });
106
+
107
+ it('returns DEOPT for non-tokens expression in template literal', () => {
108
+ const code = 'const x = `${foo.bar} 0`;';
109
+ const { node, program } = getFirstNodeOfType(code, 'TemplateLiteral');
110
+
111
+ expect(fluentTokensPlugin.evaluateNode(node, makeContext(program))).toBe(DEOPT);
112
+ });
113
+
114
+ it('evaluates template literal with zIndex token including fallback', () => {
115
+ const code = 'const x = `${tokens.zIndexOverlay}`;';
116
+ const { node, program } = getFirstNodeOfType(code, 'TemplateLiteral');
117
+
118
+ expect(fluentTokensPlugin.evaluateNode(node, makeContext(program))).toBe('var(--zIndexOverlay, 1000)');
119
+ });
120
+ });
121
+
122
+ describe('unsupported node types', () => {
123
+ it('returns DEOPT for Identifier', () => {
124
+ const code = 'const x = foo;';
125
+ const { node, program } = getFirstNodeOfType(code, 'Identifier');
126
+
127
+ expect(fluentTokensPlugin.evaluateNode(node, makeContext(program))).toBe(DEOPT);
128
+ });
129
+ });
130
+ });