@usefragments/core 1.0.0

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 (228) hide show
  1. package/LICENSE +21 -0
  2. package/dist/chunk-243QYRUF.js +2145 -0
  3. package/dist/chunk-243QYRUF.js.map +1 -0
  4. package/dist/chunk-3LLRNCPX.js +111 -0
  5. package/dist/chunk-3LLRNCPX.js.map +1 -0
  6. package/dist/chunk-EIYNNS77.js +48 -0
  7. package/dist/chunk-EIYNNS77.js.map +1 -0
  8. package/dist/chunk-JNBFJ34I.js +326 -0
  9. package/dist/chunk-JNBFJ34I.js.map +1 -0
  10. package/dist/chunk-R3X7UBHU.js +228 -0
  11. package/dist/chunk-R3X7UBHU.js.map +1 -0
  12. package/dist/chunk-SH4KPIYH.js +393 -0
  13. package/dist/chunk-SH4KPIYH.js.map +1 -0
  14. package/dist/chunk-V4VQB57N.js +56 -0
  15. package/dist/chunk-V4VQB57N.js.map +1 -0
  16. package/dist/chunk-X34IA4LR.js +319 -0
  17. package/dist/chunk-X34IA4LR.js.map +1 -0
  18. package/dist/chunk-YF65VYRY.js +493 -0
  19. package/dist/chunk-YF65VYRY.js.map +1 -0
  20. package/dist/codes/index.d.ts +40 -0
  21. package/dist/codes/index.js +23 -0
  22. package/dist/codes/index.js.map +1 -0
  23. package/dist/compiled-types/index.d.ts +5 -0
  24. package/dist/compiled-types/index.js +1 -0
  25. package/dist/compiled-types/index.js.map +1 -0
  26. package/dist/generate/index.d.ts +47 -0
  27. package/dist/generate/index.js +11 -0
  28. package/dist/generate/index.js.map +1 -0
  29. package/dist/governance-BOa3KyiJ.d.ts +3279 -0
  30. package/dist/graph/index.d.ts +66 -0
  31. package/dist/graph/index.js +17 -0
  32. package/dist/graph/index.js.map +1 -0
  33. package/dist/index.d.ts +11388 -0
  34. package/dist/index.js +8711 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/package-identity.d.ts +42 -0
  37. package/dist/package-identity.js +23 -0
  38. package/dist/package-identity.js.map +1 -0
  39. package/dist/preview/index.d.ts +484 -0
  40. package/dist/preview/index.js +1129 -0
  41. package/dist/preview/index.js.map +1 -0
  42. package/dist/react-types.d.ts +22 -0
  43. package/dist/react-types.js +1 -0
  44. package/dist/react-types.js.map +1 -0
  45. package/dist/registry.d.ts +2367 -0
  46. package/dist/registry.js +62 -0
  47. package/dist/registry.js.map +1 -0
  48. package/dist/schemas/index.d.ts +2584 -0
  49. package/dist/schemas/index.js +55 -0
  50. package/dist/schemas/index.js.map +1 -0
  51. package/dist/severity.d.ts +18 -0
  52. package/dist/severity.js +27 -0
  53. package/dist/severity.js.map +1 -0
  54. package/dist/test-utils.d.ts +14 -0
  55. package/dist/test-utils.js +63 -0
  56. package/dist/test-utils.js.map +1 -0
  57. package/dist/topology/index.d.ts +48 -0
  58. package/dist/topology/index.js +7 -0
  59. package/dist/topology/index.js.map +1 -0
  60. package/dist/types-xJ2xyp_G.d.ts +119 -0
  61. package/package.json +120 -0
  62. package/src/__tests__/contract-parser.test.ts +614 -0
  63. package/src/__tests__/dtcg-generators.test.ts +439 -0
  64. package/src/__tests__/dtcg-parser.test.ts +884 -0
  65. package/src/__tests__/preview-runtime.test.tsx +111 -0
  66. package/src/__tests__/schema.test.ts +31 -0
  67. package/src/__tests__/severity.test.ts +34 -0
  68. package/src/__tests__/style-comparison.test.ts +662 -0
  69. package/src/__tests__/tokens-compat.test.ts +51 -0
  70. package/src/agent-format.test.ts +105 -0
  71. package/src/agent-format.ts +141 -0
  72. package/src/bundle.ts +149 -0
  73. package/src/codes/__tests__/codes.test.ts +73 -0
  74. package/src/codes/codes.ts +463 -0
  75. package/src/codes/index.ts +7 -0
  76. package/src/codes/source-finding-bridge.ts +113 -0
  77. package/src/compiled-types/index.ts +293 -0
  78. package/src/component-contract.ts +231 -0
  79. package/src/component-discovery.ts +80 -0
  80. package/src/composition.test.ts +262 -0
  81. package/src/composition.ts +318 -0
  82. package/src/config-paths.test.ts +78 -0
  83. package/src/config-paths.ts +47 -0
  84. package/src/config.ts +18 -0
  85. package/src/conform-prove.test.ts +274 -0
  86. package/src/conform-prove.ts +326 -0
  87. package/src/conform.ts +160 -0
  88. package/src/constants.ts +114 -0
  89. package/src/context.ts +2 -0
  90. package/src/contract/fixture.ts +110 -0
  91. package/src/contract/hash.test.ts +86 -0
  92. package/src/contract/hash.ts +195 -0
  93. package/src/contract/index.ts +43 -0
  94. package/src/contract/preimage.test.ts +254 -0
  95. package/src/contract/preimage.ts +365 -0
  96. package/src/contract/stamp.test.ts +100 -0
  97. package/src/contract/stamp.ts +105 -0
  98. package/src/contract-parser.ts +141 -0
  99. package/src/defineFragment.test.ts +257 -0
  100. package/src/defineFragment.ts +293 -0
  101. package/src/dtcg-generators.ts +324 -0
  102. package/src/dtcg-parser.ts +1 -0
  103. package/src/dtcg.ts +237 -0
  104. package/src/facts/__snapshots__/facts.test.ts.snap +124 -0
  105. package/src/facts/builders.ts +835 -0
  106. package/src/facts/compile.ts +422 -0
  107. package/src/facts/fact-index.ts +394 -0
  108. package/src/facts/facts.test.ts +494 -0
  109. package/src/facts/ids.ts +125 -0
  110. package/src/facts/index.ts +119 -0
  111. package/src/facts/types.ts +614 -0
  112. package/src/figma.ts +263 -0
  113. package/src/fragment-types.ts +214 -0
  114. package/src/generate/generate.test.ts +594 -0
  115. package/src/generate/index.ts +438 -0
  116. package/src/governance-integrity.test.ts +186 -0
  117. package/src/governance-integrity.ts +282 -0
  118. package/src/governance.test.ts +300 -0
  119. package/src/governance.ts +605 -0
  120. package/src/governance.type-test.ts +75 -0
  121. package/src/graph/engine.ts +446 -0
  122. package/src/graph/graph.test.ts +746 -0
  123. package/src/graph/index.ts +32 -0
  124. package/src/graph/serialization.ts +55 -0
  125. package/src/graph/types.ts +178 -0
  126. package/src/index.ts +891 -0
  127. package/src/local-canonical.ts +102 -0
  128. package/src/mcp-snapshot.ts +222 -0
  129. package/src/package-identity-match.ts +33 -0
  130. package/src/package-identity.generated.ts +233 -0
  131. package/src/package-identity.test.ts +99 -0
  132. package/src/package-identity.ts +157 -0
  133. package/src/performance-presets.ts +142 -0
  134. package/src/preview/comparison-artifact.test.ts +228 -0
  135. package/src/preview/comparison-artifact.ts +181 -0
  136. package/src/preview/comparison-context.ts +93 -0
  137. package/src/preview/comparison-evidence.ts +112 -0
  138. package/src/preview/comparison-format.ts +21 -0
  139. package/src/preview/comparison-presentation.ts +187 -0
  140. package/src/preview/comparison-references.test.ts +430 -0
  141. package/src/preview/comparison-references.ts +72 -0
  142. package/src/preview/comparison-score.ts +59 -0
  143. package/src/preview/comparison-styles.ts +197 -0
  144. package/src/preview/comparison.test.ts +559 -0
  145. package/src/preview/comparison.ts +488 -0
  146. package/src/preview/index.ts +10 -0
  147. package/src/preview/types.ts +309 -0
  148. package/src/preview/validation.test.ts +290 -0
  149. package/src/preview/validation.ts +356 -0
  150. package/src/preview-runtime.tsx +144 -0
  151. package/src/raw-html-canonical.test.ts +41 -0
  152. package/src/raw-html-canonical.ts +179 -0
  153. package/src/react-types.ts +21 -0
  154. package/src/registry-install-plan.ts +344 -0
  155. package/src/registry.test.ts +245 -0
  156. package/src/registry.ts +369 -0
  157. package/src/rules/a11y-required-accessible-name.ts +93 -0
  158. package/src/rules/color-math.test.ts +72 -0
  159. package/src/rules/color-math.ts +192 -0
  160. package/src/rules/components-forbidden-prop-value.ts +108 -0
  161. package/src/rules/components-prefer-library.ts +943 -0
  162. package/src/rules/components-unknown-prop.ts +94 -0
  163. package/src/rules/composition-pattern-schema.ts +125 -0
  164. package/src/rules/composition-pattern.test.ts +320 -0
  165. package/src/rules/composition-pattern.ts +271 -0
  166. package/src/rules/emit-gate.test.ts +42 -0
  167. package/src/rules/emit-gate.ts +60 -0
  168. package/src/rules/finding.ts +54 -0
  169. package/src/rules/index.ts +235 -0
  170. package/src/rules/jsx-preferred-component.ts +68 -0
  171. package/src/rules/jsx-preferred-import-path.ts +67 -0
  172. package/src/rules/presets.ts +57 -0
  173. package/src/rules/props-invalid-value.ts +115 -0
  174. package/src/rules/rules.test.ts +3636 -0
  175. package/src/rules/spacing-resolution.ts +184 -0
  176. package/src/rules/styles-no-raw-color.ts +329 -0
  177. package/src/rules/styles-no-raw-dimensions.ts +357 -0
  178. package/src/rules/styles-no-raw-spacing.ts +236 -0
  179. package/src/rules/styles-no-raw-typography.ts +237 -0
  180. package/src/rules/tailwind-arbitrary-color.test.ts +128 -0
  181. package/src/rules/tailwind-arbitrary-color.ts +93 -0
  182. package/src/rules/tailwind-arbitrary-spacing.test.ts +157 -0
  183. package/src/rules/tailwind-arbitrary-spacing.ts +120 -0
  184. package/src/rules/tailwind-forbidden-palette.test.ts +116 -0
  185. package/src/rules/tailwind-forbidden-palette.ts +107 -0
  186. package/src/rules/tailwind-off-scale-spacing-token.test.ts +100 -0
  187. package/src/rules/tailwind-off-scale-spacing-token.ts +108 -0
  188. package/src/rules/tailwind-raw-color-via-token.test.ts +108 -0
  189. package/src/rules/tailwind-raw-color-via-token.ts +82 -0
  190. package/src/rules/tailwind-rule-utils.ts +106 -0
  191. package/src/rules/tailwind-unknown-class.test.ts +157 -0
  192. package/src/rules/tailwind-unknown-class.ts +121 -0
  193. package/src/rules/taxonomy.test.ts +238 -0
  194. package/src/rules/theme-no-theme-coupled-literal.ts +132 -0
  195. package/src/rules/tiers.ts +111 -0
  196. package/src/rules/tokens-css-vars-must-be-defined.test.ts +244 -0
  197. package/src/rules/tokens-css-vars-must-be-defined.ts +156 -0
  198. package/src/rules/tokens-require-dual-fallback.ts +78 -0
  199. package/src/rules/types.ts +27 -0
  200. package/src/rules/utils.test.ts +110 -0
  201. package/src/rules/utils.ts +599 -0
  202. package/src/schema.ts +427 -0
  203. package/src/schemas/index.ts +275 -0
  204. package/src/schemas/schemas.test.ts +124 -0
  205. package/src/severity.ts +54 -0
  206. package/src/storyAdapter.test.ts +571 -0
  207. package/src/storyAdapter.ts +761 -0
  208. package/src/storyFilters.test.ts +350 -0
  209. package/src/storyFilters.ts +253 -0
  210. package/src/storybook-csf.ts +11 -0
  211. package/src/style-comparison.ts +423 -0
  212. package/src/test-utils.ts +68 -0
  213. package/src/token-parser.ts +2 -0
  214. package/src/token-types.ts +287 -0
  215. package/src/tokens/categories.ts +131 -0
  216. package/src/tokens/color.ts +193 -0
  217. package/src/tokens/design-token-parser.test.ts +159 -0
  218. package/src/tokens/design-token-parser.ts +402 -0
  219. package/src/tokens/dtcg.ts +428 -0
  220. package/src/tokens/index.ts +130 -0
  221. package/src/tokens/lookup.ts +19 -0
  222. package/src/tokens/scss.ts +171 -0
  223. package/src/tokens/tailwind-v4.ts +187 -0
  224. package/src/tokens/types.ts +43 -0
  225. package/src/topology/index.ts +9 -0
  226. package/src/topology/resolve-area.test.ts +174 -0
  227. package/src/topology/resolve-area.ts +215 -0
  228. package/src/types.ts +1057 -0
@@ -0,0 +1,884 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { parseDTCGFile, isDTCGFile } from '../dtcg-parser.js';
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Helpers
6
+ // ---------------------------------------------------------------------------
7
+
8
+ function makeTokenFile(obj: Record<string, unknown>): string {
9
+ return JSON.stringify(obj);
10
+ }
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // isDTCGFile
14
+ // ---------------------------------------------------------------------------
15
+
16
+ describe('isDTCGFile', () => {
17
+ it('detects .tokens.json extension', () => {
18
+ expect(isDTCGFile('design.tokens.json')).toBe(true);
19
+ expect(isDTCGFile('src/tokens/brand.tokens.json')).toBe(true);
20
+ });
21
+
22
+ it('detects .tokens extension', () => {
23
+ expect(isDTCGFile('brand.tokens')).toBe(true);
24
+ });
25
+
26
+ it('rejects non-DTCG files', () => {
27
+ expect(isDTCGFile('variables.scss')).toBe(false);
28
+ expect(isDTCGFile('tokens.css')).toBe(false);
29
+ expect(isDTCGFile('package.json')).toBe(false);
30
+ });
31
+
32
+ it('accepts common unqualified DTCG token filenames', () => {
33
+ expect(isDTCGFile('tokens.json')).toBe(true);
34
+ expect(isDTCGFile('design-tokens.json')).toBe(true);
35
+ });
36
+ });
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Basic parsing
40
+ // ---------------------------------------------------------------------------
41
+
42
+ describe('parseDTCGFile', () => {
43
+ describe('basic token parsing', () => {
44
+ it('parses a simple color token', () => {
45
+ const content = makeTokenFile({
46
+ color: {
47
+ $type: 'color',
48
+ primary: {
49
+ $value: '#ff5500',
50
+ $description: 'Primary brand color',
51
+ },
52
+ },
53
+ });
54
+
55
+ const result = parseDTCGFile(content, 'test.tokens.json');
56
+
57
+ expect(result.total).toBe(1);
58
+ expect(result.categories.colors).toBeDefined();
59
+ expect(result.categories.colors).toHaveLength(1);
60
+ expect(result.categories.colors[0].value).toBe('#ff5500');
61
+ expect(result.categories.colors[0].description).toBe('Primary brand color');
62
+ });
63
+
64
+ it('parses multiple tokens across groups', () => {
65
+ const content = makeTokenFile({
66
+ color: {
67
+ $type: 'color',
68
+ primary: { $value: '#ff5500' },
69
+ secondary: { $value: '#0055ff' },
70
+ },
71
+ spacing: {
72
+ $type: 'dimension',
73
+ sm: { $value: { value: 4, unit: 'px' } },
74
+ md: { $value: { value: 8, unit: 'px' } },
75
+ lg: { $value: { value: 16, unit: 'px' } },
76
+ },
77
+ });
78
+
79
+ const result = parseDTCGFile(content, 'test.tokens.json');
80
+
81
+ expect(result.total).toBe(5);
82
+ expect(result.categories.colors).toHaveLength(2);
83
+ expect(result.categories.spacing).toHaveLength(3);
84
+ expect(result.categories.colors[0].name).toBe('--color-primary');
85
+ expect(result.categories.spacing[0].name).toBe('--spacing-sm');
86
+ });
87
+
88
+ it('returns zero tokens for an empty file', () => {
89
+ const result = parseDTCGFile('{}', 'test.tokens.json');
90
+
91
+ expect(result.total).toBe(0);
92
+ expect(Object.keys(result.categories)).toHaveLength(0);
93
+ });
94
+
95
+ it('skips tokens without a resolvable type', () => {
96
+ const content = makeTokenFile({
97
+ mystery: {
98
+ unknown: { $value: 'something' },
99
+ },
100
+ });
101
+
102
+ const result = parseDTCGFile(content, 'test.tokens.json');
103
+ expect(result.total).toBe(0);
104
+ });
105
+ });
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // All 13 DTCG token types
109
+ // ---------------------------------------------------------------------------
110
+
111
+ describe('token type handling', () => {
112
+ it('parses color tokens with hex string value', () => {
113
+ const content = makeTokenFile({
114
+ color: {
115
+ $type: 'color',
116
+ brand: { $value: '#e63946' },
117
+ },
118
+ });
119
+ const result = parseDTCGFile(content, 'test.tokens.json');
120
+ expect(result.categories.colors[0].value).toBe('#e63946');
121
+ });
122
+
123
+ it('parses color tokens with object value (hex field)', () => {
124
+ const content = makeTokenFile({
125
+ color: {
126
+ $type: 'color',
127
+ brand: {
128
+ $value: { colorSpace: 'srgb', components: [0.9, 0.22, 0.27], hex: '#e63845' },
129
+ },
130
+ },
131
+ });
132
+ const result = parseDTCGFile(content, 'test.tokens.json');
133
+ expect(result.categories.colors[0].value).toBe('#e63845');
134
+ });
135
+
136
+ it('parses color tokens with object value (components only)', () => {
137
+ const content = makeTokenFile({
138
+ color: {
139
+ $type: 'color',
140
+ brand: {
141
+ $value: { colorSpace: 'srgb', components: [1, 0, 0] },
142
+ },
143
+ },
144
+ });
145
+ const result = parseDTCGFile(content, 'test.tokens.json');
146
+ expect(result.categories.colors[0].value).toBe('rgb(255, 0, 0)');
147
+ });
148
+
149
+ it('parses color with alpha from components', () => {
150
+ const content = makeTokenFile({
151
+ color: {
152
+ $type: 'color',
153
+ overlay: {
154
+ $value: { colorSpace: 'srgb', components: [0, 0, 0], hex: '#000000', alpha: 0.5 },
155
+ },
156
+ },
157
+ });
158
+ const result = parseDTCGFile(content, 'test.tokens.json');
159
+ expect(result.categories.colors[0].value).toBe('rgba(0, 0, 0, 0.5)');
160
+ });
161
+
162
+ it('parses dimension tokens with object value', () => {
163
+ const content = makeTokenFile({
164
+ spacing: {
165
+ $type: 'dimension',
166
+ sm: { $value: { value: 4, unit: 'px' } },
167
+ md: { $value: { value: 1, unit: 'rem' } },
168
+ },
169
+ });
170
+ const result = parseDTCGFile(content, 'test.tokens.json');
171
+ expect(result.categories.spacing).toHaveLength(2);
172
+ expect(result.categories.spacing[0].value).toBe('4px');
173
+ expect(result.categories.spacing[1].value).toBe('1rem');
174
+ });
175
+
176
+ it('parses dimension tokens with string value', () => {
177
+ const content = makeTokenFile({
178
+ spacing: {
179
+ $type: 'dimension',
180
+ gap: { $value: '16px' },
181
+ },
182
+ });
183
+ const result = parseDTCGFile(content, 'test.tokens.json');
184
+ expect(result.categories.spacing[0].value).toBe('16px');
185
+ });
186
+
187
+ it('maps dimension tokens with "radius" in name to radius category', () => {
188
+ const content = makeTokenFile({
189
+ radius: {
190
+ $type: 'dimension',
191
+ sm: { $value: { value: 4, unit: 'px' } },
192
+ },
193
+ });
194
+ const result = parseDTCGFile(content, 'test.tokens.json');
195
+ expect(result.categories.radius).toBeDefined();
196
+ expect(result.categories.radius).toHaveLength(1);
197
+ });
198
+
199
+ it('parses fontFamily tokens', () => {
200
+ const content = makeTokenFile({
201
+ font: {
202
+ $type: 'fontFamily',
203
+ sans: { $value: ['Inter', 'Helvetica', 'sans-serif'] },
204
+ mono: { $value: 'JetBrains Mono' },
205
+ },
206
+ });
207
+ const result = parseDTCGFile(content, 'test.tokens.json');
208
+ expect(result.categories.typography).toHaveLength(2);
209
+ expect(result.categories.typography[0].value).toBe('Inter, Helvetica, sans-serif');
210
+ expect(result.categories.typography[1].value).toBe('JetBrains Mono');
211
+ });
212
+
213
+ it('parses fontWeight tokens', () => {
214
+ const content = makeTokenFile({
215
+ weight: {
216
+ $type: 'fontWeight',
217
+ regular: { $value: 400 },
218
+ bold: { $value: 700 },
219
+ },
220
+ });
221
+ const result = parseDTCGFile(content, 'test.tokens.json');
222
+ expect(result.categories.typography).toHaveLength(2);
223
+ expect(result.categories.typography[0].value).toBe('400');
224
+ expect(result.categories.typography[1].value).toBe('700');
225
+ });
226
+
227
+ it('parses duration tokens', () => {
228
+ const content = makeTokenFile({
229
+ motion: {
230
+ $type: 'duration',
231
+ fast: { $value: '100ms' },
232
+ slow: { $value: '500ms' },
233
+ },
234
+ });
235
+ const result = parseDTCGFile(content, 'test.tokens.json');
236
+ expect(result.categories.transitions).toHaveLength(2);
237
+ expect(result.categories.transitions[0].value).toBe('100ms');
238
+ });
239
+
240
+ it('parses cubicBezier tokens', () => {
241
+ const content = makeTokenFile({
242
+ easing: {
243
+ $type: 'cubicBezier',
244
+ standard: { $value: [0.4, 0, 0.2, 1] },
245
+ },
246
+ });
247
+ const result = parseDTCGFile(content, 'test.tokens.json');
248
+ expect(result.categories.transitions).toHaveLength(1);
249
+ expect(result.categories.transitions[0].value).toBe('cubic-bezier(0.4, 0, 0.2, 1)');
250
+ });
251
+
252
+ it('parses number tokens', () => {
253
+ const content = makeTokenFile({
254
+ scale: {
255
+ $type: 'number',
256
+ ratio: { $value: 1.5 },
257
+ },
258
+ });
259
+ const result = parseDTCGFile(content, 'test.tokens.json');
260
+ expect(result.categories.other).toHaveLength(1);
261
+ expect(result.categories.other[0].value).toBe('1.5');
262
+ });
263
+
264
+ it('parses shadow tokens (single)', () => {
265
+ const content = makeTokenFile({
266
+ shadow: {
267
+ $type: 'shadow',
268
+ sm: {
269
+ $value: {
270
+ offsetX: '0px',
271
+ offsetY: '1px',
272
+ blur: '3px',
273
+ spread: '0px',
274
+ color: 'rgba(0, 0, 0, 0.1)',
275
+ },
276
+ },
277
+ },
278
+ });
279
+ const result = parseDTCGFile(content, 'test.tokens.json');
280
+ expect(result.categories.shadows).toHaveLength(1);
281
+ expect(result.categories.shadows[0].value).toContain('0px 1px 3px');
282
+ });
283
+
284
+ it('parses shadow tokens (multiple layers)', () => {
285
+ const content = makeTokenFile({
286
+ shadow: {
287
+ $type: 'shadow',
288
+ lg: {
289
+ $value: [
290
+ { offsetX: '0px', offsetY: '4px', blur: '8px', color: 'rgba(0, 0, 0, 0.1)' },
291
+ { offsetX: '0px', offsetY: '2px', blur: '4px', color: 'rgba(0, 0, 0, 0.06)' },
292
+ ],
293
+ },
294
+ },
295
+ });
296
+ const result = parseDTCGFile(content, 'test.tokens.json');
297
+ expect(result.categories.shadows[0].value).toContain(',');
298
+ });
299
+
300
+ it('parses shadow tokens with inset', () => {
301
+ const content = makeTokenFile({
302
+ shadow: {
303
+ $type: 'shadow',
304
+ inner: {
305
+ $value: {
306
+ offsetX: '0px',
307
+ offsetY: '2px',
308
+ blur: '4px',
309
+ color: 'rgba(0, 0, 0, 0.05)',
310
+ inset: true,
311
+ },
312
+ },
313
+ },
314
+ });
315
+ const result = parseDTCGFile(content, 'test.tokens.json');
316
+ expect(result.categories.shadows[0].value).toMatch(/^inset /);
317
+ });
318
+
319
+ it('parses border tokens', () => {
320
+ const content = makeTokenFile({
321
+ border: {
322
+ $type: 'border',
323
+ default: {
324
+ $value: {
325
+ color: '#e0e0e0',
326
+ width: '1px',
327
+ style: 'solid',
328
+ },
329
+ },
330
+ },
331
+ });
332
+ const result = parseDTCGFile(content, 'test.tokens.json');
333
+ expect(result.categories.borders).toHaveLength(1);
334
+ expect(result.categories.borders[0].value).toBe('1px solid #e0e0e0');
335
+ });
336
+
337
+ it('parses strokeStyle tokens', () => {
338
+ const content = makeTokenFile({
339
+ stroke: {
340
+ $type: 'strokeStyle',
341
+ dashed: { $value: 'dashed' },
342
+ },
343
+ });
344
+ const result = parseDTCGFile(content, 'test.tokens.json');
345
+ expect(result.categories.borders).toHaveLength(1);
346
+ expect(result.categories.borders[0].value).toBe('dashed');
347
+ });
348
+
349
+ it('parses transition tokens', () => {
350
+ const content = makeTokenFile({
351
+ motion: {
352
+ $type: 'transition',
353
+ fade: {
354
+ $value: {
355
+ duration: '200ms',
356
+ timingFunction: [0.4, 0, 0.2, 1],
357
+ delay: '0ms',
358
+ },
359
+ },
360
+ },
361
+ });
362
+ const result = parseDTCGFile(content, 'test.tokens.json');
363
+ expect(result.categories.transitions).toHaveLength(1);
364
+ expect(result.categories.transitions[0].value).toContain('200ms');
365
+ expect(result.categories.transitions[0].value).toContain('cubic-bezier');
366
+ });
367
+
368
+ it('parses gradient tokens', () => {
369
+ const content = makeTokenFile({
370
+ gradient: {
371
+ $type: 'gradient',
372
+ brand: {
373
+ $value: [
374
+ { color: '#ff5500', position: 0 },
375
+ { color: '#0055ff', position: 1 },
376
+ ],
377
+ },
378
+ },
379
+ });
380
+ const result = parseDTCGFile(content, 'test.tokens.json');
381
+ expect(result.categories.colors).toHaveLength(1);
382
+ expect(result.categories.colors[0].value).toContain('linear-gradient');
383
+ expect(result.categories.colors[0].value).toContain('#ff5500');
384
+ });
385
+
386
+ it('parses typography composite tokens', () => {
387
+ const content = makeTokenFile({
388
+ type: {
389
+ $type: 'typography',
390
+ heading: {
391
+ $value: {
392
+ fontFamily: 'Inter',
393
+ fontSize: '24px',
394
+ fontWeight: 700,
395
+ lineHeight: 1.2,
396
+ },
397
+ },
398
+ },
399
+ });
400
+ const result = parseDTCGFile(content, 'test.tokens.json');
401
+ expect(result.categories.typography).toHaveLength(1);
402
+ expect(result.categories.typography[0].value).toContain('700');
403
+ expect(result.categories.typography[0].value).toContain('24px');
404
+ expect(result.categories.typography[0].value).toContain('Inter');
405
+ });
406
+ });
407
+
408
+ // ---------------------------------------------------------------------------
409
+ // $type inheritance
410
+ // ---------------------------------------------------------------------------
411
+
412
+ describe('$type inheritance', () => {
413
+ it('inherits $type from parent group', () => {
414
+ const content = makeTokenFile({
415
+ color: {
416
+ $type: 'color',
417
+ brand: {
418
+ primary: { $value: '#ff5500' },
419
+ secondary: { $value: '#0055ff' },
420
+ },
421
+ surface: {
422
+ default: { $value: '#ffffff' },
423
+ },
424
+ },
425
+ });
426
+
427
+ const result = parseDTCGFile(content, 'test.tokens.json');
428
+
429
+ expect(result.total).toBe(3);
430
+ expect(result.categories.colors).toHaveLength(3);
431
+ });
432
+
433
+ it('child $type overrides inherited $type', () => {
434
+ const content = makeTokenFile({
435
+ tokens: {
436
+ $type: 'color',
437
+ accent: { $value: '#ff5500' },
438
+ spacing: {
439
+ $type: 'dimension',
440
+ sm: { $value: '4px' },
441
+ },
442
+ },
443
+ });
444
+
445
+ const result = parseDTCGFile(content, 'test.tokens.json');
446
+
447
+ expect(result.categories.colors).toHaveLength(1);
448
+ expect(result.categories.spacing).toHaveLength(1);
449
+ });
450
+
451
+ it('inherits $type through multiple nesting levels', () => {
452
+ const content = makeTokenFile({
453
+ ds: {
454
+ $type: 'color',
455
+ brand: {
456
+ primary: {
457
+ light: { $value: '#ffaa80' },
458
+ dark: { $value: '#cc4400' },
459
+ },
460
+ },
461
+ },
462
+ });
463
+
464
+ const result = parseDTCGFile(content, 'test.tokens.json');
465
+
466
+ expect(result.total).toBe(2);
467
+ expect(result.categories.colors).toHaveLength(2);
468
+ });
469
+ });
470
+
471
+ // ---------------------------------------------------------------------------
472
+ // Alias resolution
473
+ // ---------------------------------------------------------------------------
474
+
475
+ describe('alias resolution', () => {
476
+ it('resolves simple aliases', () => {
477
+ const content = makeTokenFile({
478
+ color: {
479
+ $type: 'color',
480
+ brand: { $value: '#ff5500' },
481
+ accent: { $value: '{color.brand}' },
482
+ },
483
+ });
484
+
485
+ const result = parseDTCGFile(content, 'test.tokens.json');
486
+
487
+ expect(result.total).toBe(2);
488
+ const accent = result.categories.colors.find((t) => t.name.includes('accent'));
489
+ expect(accent).toBeDefined();
490
+ expect(accent!.value).toBe('#ff5500');
491
+ });
492
+
493
+ it('resolves chained aliases', () => {
494
+ const content = makeTokenFile({
495
+ color: {
496
+ $type: 'color',
497
+ base: { $value: '#e63946' },
498
+ brand: { $value: '{color.base}' },
499
+ primary: { $value: '{color.brand}' },
500
+ },
501
+ });
502
+
503
+ const result = parseDTCGFile(content, 'test.tokens.json');
504
+
505
+ const primary = result.categories.colors.find((t) => t.name.includes('primary'));
506
+ expect(primary).toBeDefined();
507
+ expect(primary!.value).toBe('#e63946');
508
+ });
509
+
510
+ it('resolves aliases across groups', () => {
511
+ const content = makeTokenFile({
512
+ primitive: {
513
+ $type: 'color',
514
+ blue500: { $value: '#3b82f6' },
515
+ },
516
+ semantic: {
517
+ $type: 'color',
518
+ primary: { $value: '{primitive.blue500}' },
519
+ },
520
+ });
521
+
522
+ const result = parseDTCGFile(content, 'test.tokens.json');
523
+
524
+ const primary = result.categories.colors.find((t) => t.name.includes('primary'));
525
+ expect(primary!.value).toBe('#3b82f6');
526
+ });
527
+
528
+ it('detects circular aliases and throws', () => {
529
+ const content = makeTokenFile({
530
+ color: {
531
+ $type: 'color',
532
+ a: { $value: '{color.b}' },
533
+ b: { $value: '{color.a}' },
534
+ },
535
+ });
536
+
537
+ // Should not throw — circular aliases are caught and the raw value is kept
538
+ const result = parseDTCGFile(content, 'test.tokens.json');
539
+ expect(result.total).toBe(2);
540
+ });
541
+
542
+ it('detects self-referencing aliases', () => {
543
+ const content = makeTokenFile({
544
+ color: {
545
+ $type: 'color',
546
+ recursive: { $value: '{color.recursive}' },
547
+ },
548
+ });
549
+
550
+ const result = parseDTCGFile(content, 'test.tokens.json');
551
+ expect(result.total).toBe(1);
552
+ });
553
+
554
+ it('detects deeply nested circular aliases (exceeding max depth)', () => {
555
+ // Create a chain of 12 aliases that will exceed MAX_ALIAS_DEPTH of 10
556
+ const tokens: Record<string, unknown> = { $type: 'color' };
557
+ for (let i = 0; i < 12; i++) {
558
+ tokens[`t${i}`] = { $value: `{chain.t${(i + 1) % 12}}` };
559
+ }
560
+
561
+ const content = makeTokenFile({ chain: tokens });
562
+ const result = parseDTCGFile(content, 'test.tokens.json');
563
+ expect(result.total).toBe(12);
564
+ });
565
+
566
+ it('handles alias to non-existent path gracefully', () => {
567
+ const content = makeTokenFile({
568
+ color: {
569
+ $type: 'color',
570
+ broken: { $value: '{does.not.exist}' },
571
+ },
572
+ });
573
+
574
+ const result = parseDTCGFile(content, 'test.tokens.json');
575
+ expect(result.total).toBe(1);
576
+ // Value should be the unresolved alias string
577
+ expect(result.categories.colors[0].value).toBe('{does.not.exist}');
578
+ });
579
+ });
580
+
581
+ // ---------------------------------------------------------------------------
582
+ // $extends group inheritance
583
+ // ---------------------------------------------------------------------------
584
+
585
+ describe('$extends group inheritance', () => {
586
+ it('resolves $extends from another group', () => {
587
+ const content = makeTokenFile({
588
+ base: {
589
+ $type: 'color',
590
+ primary: { $value: '#ff5500' },
591
+ secondary: { $value: '#0055ff' },
592
+ },
593
+ theme: {
594
+ $extends: 'base',
595
+ primary: { $value: '#ee4400' }, // Override
596
+ },
597
+ });
598
+
599
+ const result = parseDTCGFile(content, 'test.tokens.json');
600
+
601
+ // base has 2, theme inherits secondary and overrides primary = 2 more
602
+ const themeTokens = result.categories.colors.filter((t) => t.name.includes('theme'));
603
+ expect(themeTokens.length).toBeGreaterThanOrEqual(2);
604
+
605
+ // The overridden primary should have the new value
606
+ const themePrimary = themeTokens.find((t) => t.name.includes('primary'));
607
+ expect(themePrimary).toBeDefined();
608
+ expect(themePrimary!.value).toBe('#ee4400');
609
+ });
610
+ });
611
+
612
+ // ---------------------------------------------------------------------------
613
+ // $deprecated propagation
614
+ // ---------------------------------------------------------------------------
615
+
616
+ describe('$deprecated handling', () => {
617
+ it('preserves $deprecated boolean on tokens', () => {
618
+ const content = makeTokenFile({
619
+ color: {
620
+ $type: 'color',
621
+ old: { $value: '#ff0000', $deprecated: true },
622
+ },
623
+ });
624
+
625
+ const result = parseDTCGFile(content, 'test.tokens.json');
626
+ expect(result.total).toBe(1);
627
+ // Deprecated tokens are still parsed — consumers can filter
628
+ });
629
+
630
+ it('preserves $deprecated string message on tokens', () => {
631
+ const content = makeTokenFile({
632
+ color: {
633
+ $type: 'color',
634
+ legacy: {
635
+ $value: '#ff0000',
636
+ $deprecated: 'Use color.brand instead',
637
+ },
638
+ },
639
+ });
640
+
641
+ const result = parseDTCGFile(content, 'test.tokens.json');
642
+ expect(result.total).toBe(1);
643
+ });
644
+ });
645
+
646
+ // ---------------------------------------------------------------------------
647
+ // $extensions preservation
648
+ // ---------------------------------------------------------------------------
649
+
650
+ describe('$extensions handling', () => {
651
+ it('reads prefix from $extensions["com.usefragments"].prefix', () => {
652
+ const content = makeTokenFile({
653
+ $extensions: {
654
+ 'com.usefragments': { prefix: 'mui' },
655
+ },
656
+ color: {
657
+ $type: 'color',
658
+ primary: { $value: '#1976d2' },
659
+ },
660
+ });
661
+
662
+ const result = parseDTCGFile(content, 'test.tokens.json');
663
+ expect(result.prefix).toBe('--mui-');
664
+ });
665
+ });
666
+
667
+ // ---------------------------------------------------------------------------
668
+ // Prefix detection
669
+ // ---------------------------------------------------------------------------
670
+
671
+ describe('prefix detection', () => {
672
+ it('detects prefix from single top-level group', () => {
673
+ const content = makeTokenFile({
674
+ ds: {
675
+ $type: 'color',
676
+ primary: { $value: '#ff5500' },
677
+ },
678
+ });
679
+
680
+ const result = parseDTCGFile(content, 'test.tokens.json');
681
+ expect(result.prefix).toBe('--ds-');
682
+ });
683
+
684
+ it('detects prefix from $extensions over top-level group', () => {
685
+ const content = makeTokenFile({
686
+ $extensions: {
687
+ 'com.usefragments': { prefix: 'brand' },
688
+ },
689
+ ds: {
690
+ $type: 'color',
691
+ primary: { $value: '#ff5500' },
692
+ },
693
+ });
694
+
695
+ const result = parseDTCGFile(content, 'test.tokens.json');
696
+ expect(result.prefix).toBe('--brand-');
697
+ });
698
+
699
+ it('falls back to -- for empty files', () => {
700
+ const result = parseDTCGFile('{}', 'test.tokens.json');
701
+ expect(result.prefix).toBe('--');
702
+ });
703
+ });
704
+
705
+ // ---------------------------------------------------------------------------
706
+ // CSS variable naming
707
+ // ---------------------------------------------------------------------------
708
+
709
+ describe('CSS variable naming', () => {
710
+ it('converts dot paths to hyphenated CSS names', () => {
711
+ const content = makeTokenFile({
712
+ color: {
713
+ $type: 'color',
714
+ brand: {
715
+ primary: { $value: '#ff5500' },
716
+ },
717
+ },
718
+ });
719
+
720
+ const result = parseDTCGFile(content, 'test.tokens.json');
721
+ const token = result.categories.colors[0];
722
+ expect(token.name).toContain('brand-primary');
723
+ expect(token.name).toMatch(/^--/);
724
+ });
725
+ });
726
+
727
+ // ---------------------------------------------------------------------------
728
+ // Edge cases
729
+ // ---------------------------------------------------------------------------
730
+
731
+ describe('edge cases', () => {
732
+ it('handles deeply nested token trees', () => {
733
+ const content = makeTokenFile({
734
+ level1: {
735
+ $type: 'color',
736
+ level2: {
737
+ level3: {
738
+ level4: {
739
+ deep: { $value: '#001122' },
740
+ },
741
+ },
742
+ },
743
+ },
744
+ });
745
+
746
+ const result = parseDTCGFile(content, 'test.tokens.json');
747
+ expect(result.total).toBe(1);
748
+ expect(result.categories.colors[0].name).toContain('level2');
749
+ });
750
+
751
+ it('ignores non-object children', () => {
752
+ const content = makeTokenFile({
753
+ color: {
754
+ $type: 'color',
755
+ primary: { $value: '#ff5500' },
756
+ },
757
+ });
758
+
759
+ const result = parseDTCGFile(content, 'test.tokens.json');
760
+ expect(result.total).toBe(1);
761
+ });
762
+
763
+ it('handles large token files efficiently', () => {
764
+ const tokens: Record<string, unknown> = { $type: 'color' };
765
+ for (let i = 0; i < 500; i++) {
766
+ tokens[`color-${i}`] = { $value: `#${String(i).padStart(6, '0')}` };
767
+ }
768
+
769
+ const content = makeTokenFile({ palette: tokens });
770
+ const start = performance.now();
771
+ const result = parseDTCGFile(content, 'test.tokens.json');
772
+ const elapsed = performance.now() - start;
773
+
774
+ expect(result.total).toBe(500);
775
+ expect(elapsed).toBeLessThan(1000); // Should parse 500 tokens in under 1 second
776
+ });
777
+
778
+ it('throws on invalid JSON', () => {
779
+ expect(() => parseDTCGFile('not json', 'test.tokens.json')).toThrow();
780
+ });
781
+
782
+ it('handles tokens with special characters in keys', () => {
783
+ const content = makeTokenFile({
784
+ 'my-brand': {
785
+ $type: 'color',
786
+ 'dark-mode': {
787
+ 'bg-primary': { $value: '#1a1a2e' },
788
+ },
789
+ },
790
+ });
791
+
792
+ const result = parseDTCGFile(content, 'test.tokens.json');
793
+ expect(result.total).toBe(1);
794
+ });
795
+
796
+ it('handles mixed groups with both tokens and sub-groups', () => {
797
+ const content = makeTokenFile({
798
+ color: {
799
+ $type: 'color',
800
+ primary: { $value: '#ff5500' },
801
+ shades: {
802
+ light: { $value: '#ffaa80' },
803
+ dark: { $value: '#cc4400' },
804
+ },
805
+ },
806
+ });
807
+
808
+ const result = parseDTCGFile(content, 'test.tokens.json');
809
+ expect(result.total).toBe(3);
810
+ });
811
+
812
+ it('handles dimension tokens with zero value', () => {
813
+ const content = makeTokenFile({
814
+ spacing: {
815
+ $type: 'dimension',
816
+ none: { $value: { value: 0, unit: 'px' } },
817
+ },
818
+ });
819
+
820
+ const result = parseDTCGFile(content, 'test.tokens.json');
821
+ expect(result.categories.spacing[0].value).toBe('0px');
822
+ });
823
+
824
+ it('handles color with all-zero components', () => {
825
+ const content = makeTokenFile({
826
+ color: {
827
+ $type: 'color',
828
+ black: {
829
+ $value: { colorSpace: 'srgb', components: [0, 0, 0] },
830
+ },
831
+ },
832
+ });
833
+
834
+ const result = parseDTCGFile(content, 'test.tokens.json');
835
+ expect(result.categories.colors[0].value).toBe('rgb(0, 0, 0)');
836
+ });
837
+
838
+ it('handles color with non-normalized components (0-255 range)', () => {
839
+ const content = makeTokenFile({
840
+ color: {
841
+ $type: 'color',
842
+ red: {
843
+ $value: { colorSpace: 'srgb', components: [255, 0, 0] },
844
+ },
845
+ },
846
+ });
847
+
848
+ const result = parseDTCGFile(content, 'test.tokens.json');
849
+ expect(result.categories.colors[0].value).toBe('rgb(255, 0, 0)');
850
+ });
851
+ });
852
+
853
+ // ---------------------------------------------------------------------------
854
+ // Output shape compatibility
855
+ // ---------------------------------------------------------------------------
856
+
857
+ describe('TokenParseOutput compatibility', () => {
858
+ it('returns the same shape as the SCSS parser', () => {
859
+ const content = makeTokenFile({
860
+ ds: {
861
+ $type: 'color',
862
+ primary: { $value: '#ff5500' },
863
+ },
864
+ });
865
+
866
+ const result = parseDTCGFile(content, 'test.tokens.json');
867
+
868
+ // Verify shape matches TokenParseOutput
869
+ expect(result).toHaveProperty('prefix');
870
+ expect(result).toHaveProperty('categories');
871
+ expect(result).toHaveProperty('total');
872
+ expect(typeof result.prefix).toBe('string');
873
+ expect(typeof result.total).toBe('number');
874
+ expect(typeof result.categories).toBe('object');
875
+
876
+ // Verify individual token shape matches ParsedToken
877
+ const token = result.categories.colors[0];
878
+ expect(token).toHaveProperty('name');
879
+ expect(token).toHaveProperty('category');
880
+ expect(typeof token.name).toBe('string');
881
+ expect(typeof token.category).toBe('string');
882
+ });
883
+ });
884
+ });