@fragments-sdk/cli 0.14.3 → 0.15.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 (135) hide show
  1. package/README.md +0 -3
  2. package/dist/bin.js +4290 -3754
  3. package/dist/bin.js.map +1 -1
  4. package/dist/{chunk-TXFCEDOC.js → chunk-2WXKALIG.js} +2 -2
  5. package/dist/{chunk-I34BC3CU.js → chunk-32LIWN2P.js} +1006 -3
  6. package/dist/chunk-32LIWN2P.js.map +1 -0
  7. package/dist/{chunk-55KERLWL.js → chunk-65WSVDV5.js} +314 -89
  8. package/dist/chunk-65WSVDV5.js.map +1 -0
  9. package/dist/chunk-7DZC4YEV.js +294 -0
  10. package/dist/chunk-7DZC4YEV.js.map +1 -0
  11. package/dist/{chunk-LOYS64QS.js → chunk-7WHVW72L.js} +230 -19
  12. package/dist/chunk-7WHVW72L.js.map +1 -0
  13. package/dist/{chunk-PJT5IZ37.js → chunk-BJE3425I.js} +19 -52
  14. package/dist/{chunk-PJT5IZ37.js.map → chunk-BJE3425I.js.map} +1 -1
  15. package/dist/{chunk-5A6X2Y73.js → chunk-CZD3AD4Q.js} +12 -11
  16. package/dist/chunk-CZD3AD4Q.js.map +1 -0
  17. package/dist/{chunk-EYXVAMEX.js → chunk-MN3TJ3D5.js} +72 -3
  18. package/dist/chunk-MN3TJ3D5.js.map +1 -0
  19. package/dist/chunk-QCN35LJU.js +630 -0
  20. package/dist/chunk-QCN35LJU.js.map +1 -0
  21. package/dist/chunk-T47OLCSF.js +36 -0
  22. package/dist/chunk-T47OLCSF.js.map +1 -0
  23. package/dist/{chunk-APTQIBS5.js → chunk-XJQ5BIWI.js} +144 -1049
  24. package/dist/chunk-XJQ5BIWI.js.map +1 -0
  25. package/dist/codebase-scanner-VOTPXRYW.js +22 -0
  26. package/dist/converter-JLINP7CJ.js +34 -0
  27. package/dist/converter-JLINP7CJ.js.map +1 -0
  28. package/dist/core/index.js +43 -1
  29. package/dist/{generate-RYWIPDN2.js → generate-A4FP5426.js} +3 -4
  30. package/dist/{generate-RYWIPDN2.js.map → generate-A4FP5426.js.map} +1 -1
  31. package/dist/govern-scan-UCBZR6D6.js +280 -0
  32. package/dist/govern-scan-UCBZR6D6.js.map +1 -0
  33. package/dist/index.d.ts +2 -1
  34. package/dist/index.js +11 -11
  35. package/dist/{init-WRUSW7R5.js → init-HGSM35XA.js} +131 -128
  36. package/dist/init-HGSM35XA.js.map +1 -0
  37. package/dist/{init-cloud-REQ3XLHO.js → init-cloud-MQ6GRJAZ.js} +2 -2
  38. package/dist/mcp-bin.js +5 -36
  39. package/dist/mcp-bin.js.map +1 -1
  40. package/dist/scan-VNNKACG2.js +15 -0
  41. package/dist/{scan-generate-TFZVL3BT.js → scan-generate-TWRHNU5M.js} +335 -46
  42. package/dist/scan-generate-TWRHNU5M.js.map +1 -0
  43. package/dist/scanner-7LAZYPWZ.js +13 -0
  44. package/dist/{service-HKJ6B7P7.js → service-FHQU7YS7.js} +27 -23
  45. package/dist/{snapshot-C5DYIGIV.js → snapshot-KQEQ6XHL.js} +2 -2
  46. package/dist/{static-viewer-DUVC4UIM.js → static-viewer-63PG6FWY.js} +3 -3
  47. package/dist/static-viewer-63PG6FWY.js.map +1 -0
  48. package/dist/{test-JW7JIDFG.js → test-UQYUCZIS.js} +4 -6
  49. package/dist/{test-JW7JIDFG.js.map → test-UQYUCZIS.js.map} +1 -1
  50. package/dist/{tokens-KE73G5JC.js → tokens-6GYKDV6U.js} +6 -5
  51. package/dist/{tokens-KE73G5JC.js.map → tokens-6GYKDV6U.js.map} +1 -1
  52. package/dist/tokens-generate-VTZV5EEW.js +86 -0
  53. package/dist/tokens-generate-VTZV5EEW.js.map +1 -0
  54. package/package.json +6 -6
  55. package/src/bin.ts +210 -48
  56. package/src/build.ts +130 -6
  57. package/src/commands/__fixtures__/shadcn-label-wrapper/package.json +7 -0
  58. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.contract.json +42 -0
  59. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.tsx +11 -0
  60. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.contract.json +20 -0
  61. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.tsx +14 -0
  62. package/src/commands/__fixtures__/shadcn-label-wrapper/tsconfig.app.json +23 -0
  63. package/src/commands/__tests__/init.test.ts +113 -0
  64. package/src/commands/__tests__/scan-generate.test.ts +188 -69
  65. package/src/commands/__tests__/verify.test.ts +91 -0
  66. package/src/commands/discover.ts +151 -0
  67. package/src/commands/enhance.ts +3 -1
  68. package/src/commands/govern-scan.ts +386 -0
  69. package/src/commands/govern.ts +2 -2
  70. package/src/commands/init.ts +152 -28
  71. package/src/commands/inspect.ts +290 -0
  72. package/src/commands/migrate-contract.ts +85 -0
  73. package/src/commands/scan-generate.ts +438 -50
  74. package/src/commands/scan.ts +1 -0
  75. package/src/commands/setup.ts +27 -50
  76. package/src/commands/tokens-generate.ts +113 -0
  77. package/src/commands/verify.ts +195 -1
  78. package/src/core/__fixtures__/shadcn-input/input.tsx +7 -0
  79. package/src/core/__fixtures__/shadcn-input/tsconfig.json +14 -0
  80. package/src/core/__fixtures__/shadcn-label/label.tsx +11 -0
  81. package/src/core/__fixtures__/shadcn-label/primitive.tsx +14 -0
  82. package/src/core/__fixtures__/shadcn-label/tsconfig.json +14 -0
  83. package/src/core/__fixtures__/shadcn-radix-label/label.tsx +11 -0
  84. package/src/core/__fixtures__/shadcn-radix-label/node_modules/radix-ui/index.d.ts +12 -0
  85. package/src/core/__fixtures__/shadcn-radix-label/tsconfig.json +14 -0
  86. package/src/core/__tests__/contract-parity.test.ts +316 -0
  87. package/src/core/component-extractor.test.ts +39 -0
  88. package/src/core/component-extractor.ts +92 -1
  89. package/src/core/config.ts +2 -1
  90. package/src/core/discovery.ts +13 -2
  91. package/src/core/drift-verifier.ts +123 -0
  92. package/src/core/extractor-adapter.ts +80 -0
  93. package/src/mcp/__tests__/projectFields.test.ts +1 -1
  94. package/src/mcp/utils.ts +1 -50
  95. package/src/migrate/converter.ts +3 -3
  96. package/src/migrate/fragment-to-contract.ts +253 -0
  97. package/src/migrate/report.ts +1 -1
  98. package/src/scripts/token-benchmark.ts +121 -0
  99. package/src/service/__tests__/props-extractor.test.ts +94 -0
  100. package/src/service/__tests__/token-normalizer.test.ts +690 -0
  101. package/src/service/ast-utils.ts +4 -23
  102. package/src/service/babel-config.ts +23 -0
  103. package/src/service/enhance/converter.ts +61 -0
  104. package/src/service/enhance/props-extractor.ts +25 -8
  105. package/src/service/enhance/scanner.ts +5 -24
  106. package/src/service/snippet-validation.ts +9 -3
  107. package/src/service/token-normalizer.ts +510 -0
  108. package/src/shared/index.ts +1 -0
  109. package/src/shared/project-fields.ts +46 -0
  110. package/src/viewer/__tests__/viewer-integration.test.ts +8 -8
  111. package/src/viewer/preview-adapter.ts +116 -0
  112. package/src/viewer/style-utils.ts +27 -412
  113. package/src/viewer/vite-plugin.ts +2 -2
  114. package/dist/chunk-55KERLWL.js.map +0 -1
  115. package/dist/chunk-5A6X2Y73.js.map +0 -1
  116. package/dist/chunk-APTQIBS5.js.map +0 -1
  117. package/dist/chunk-EYXVAMEX.js.map +0 -1
  118. package/dist/chunk-I34BC3CU.js.map +0 -1
  119. package/dist/chunk-LOYS64QS.js.map +0 -1
  120. package/dist/chunk-ZKTFKHWN.js +0 -324
  121. package/dist/chunk-ZKTFKHWN.js.map +0 -1
  122. package/dist/discovery-VDANZAJ2.js +0 -28
  123. package/dist/init-WRUSW7R5.js.map +0 -1
  124. package/dist/scan-YJHQIRKG.js +0 -14
  125. package/dist/scan-generate-TFZVL3BT.js.map +0 -1
  126. package/dist/viewer-2TZS3NDL.js +0 -2730
  127. package/dist/viewer-2TZS3NDL.js.map +0 -1
  128. package/src/commands/dev.ts +0 -107
  129. /package/dist/{chunk-TXFCEDOC.js.map → chunk-2WXKALIG.js.map} +0 -0
  130. /package/dist/{discovery-VDANZAJ2.js.map → codebase-scanner-VOTPXRYW.js.map} +0 -0
  131. /package/dist/{init-cloud-REQ3XLHO.js.map → init-cloud-MQ6GRJAZ.js.map} +0 -0
  132. /package/dist/{scan-YJHQIRKG.js.map → scan-VNNKACG2.js.map} +0 -0
  133. /package/dist/{service-HKJ6B7P7.js.map → scanner-7LAZYPWZ.js.map} +0 -0
  134. /package/dist/{static-viewer-DUVC4UIM.js.map → service-FHQU7YS7.js.map} +0 -0
  135. /package/dist/{snapshot-C5DYIGIV.js.map → snapshot-KQEQ6XHL.js.map} +0 -0
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Preview Adapter — generates render modules from contract.json metadata.
3
+ *
4
+ * Each adapter takes a CompiledFragment sourced from a .contract.json
5
+ * and produces a virtual ES module string that a bundler (Vite) can serve.
6
+ */
7
+
8
+ import type { CompiledFragment } from '@fragments-sdk/core';
9
+
10
+ export interface PreviewConfig {
11
+ setupModule?: string;
12
+ wrapperModule?: string;
13
+ wrapperExport?: string;
14
+ css?: string[];
15
+ theme?: 'light' | 'dark';
16
+ }
17
+
18
+ export interface PreviewAdapter {
19
+ framework: string;
20
+ canHandle(fragment: CompiledFragment): boolean;
21
+ generateRenderModule(
22
+ fragment: CompiledFragment,
23
+ variant: string,
24
+ previewConfig: PreviewConfig,
25
+ ): string;
26
+ }
27
+
28
+ /**
29
+ * React preview adapter — generates a virtual module that:
30
+ * 1. Imports the component from sourcePath/exportName
31
+ * 2. Wraps with providers from project config
32
+ * 3. Renders with args from the selected example
33
+ * 4. Injects CSS from config
34
+ */
35
+ class ReactPreviewAdapter implements PreviewAdapter {
36
+ framework = 'react';
37
+
38
+ canHandle(fragment: CompiledFragment): boolean {
39
+ return (fragment.framework ?? 'react') === 'react';
40
+ }
41
+
42
+ generateRenderModule(
43
+ fragment: CompiledFragment,
44
+ variant: string,
45
+ config: PreviewConfig,
46
+ ): string {
47
+ if (!fragment.sourcePath || !fragment.exportName) {
48
+ throw new Error(
49
+ `Cannot preview ${fragment.meta.name}: missing sourcePath or exportName`,
50
+ );
51
+ }
52
+
53
+ const example = fragment.variants.find((v) => v.name === variant) ??
54
+ fragment.variants[0];
55
+
56
+ const args = example?.args ?? {};
57
+ const argsStr = JSON.stringify(args);
58
+
59
+ const lines: string[] = [];
60
+
61
+ // Setup module (global CSS, MSW, etc.)
62
+ if (config.setupModule) {
63
+ lines.push(`import '${config.setupModule}';`);
64
+ }
65
+
66
+ // CSS imports
67
+ if (config.css?.length) {
68
+ for (const css of config.css) {
69
+ lines.push(`import '${css}';`);
70
+ }
71
+ }
72
+
73
+ // Import React
74
+ lines.push(`import React from 'react';`);
75
+
76
+ // Import the component
77
+ lines.push(`import { ${fragment.exportName} } from '${fragment.sourcePath}';`);
78
+
79
+ // Import wrapper if configured
80
+ if (config.wrapperModule && config.wrapperExport) {
81
+ lines.push(`import { ${config.wrapperExport} } from '${config.wrapperModule}';`);
82
+ }
83
+
84
+ // Generate the render function
85
+ lines.push('');
86
+ lines.push(`const args = ${argsStr};`);
87
+ lines.push('');
88
+ lines.push('export default function PreviewRender() {');
89
+
90
+ if (config.wrapperModule && config.wrapperExport) {
91
+ lines.push(` return React.createElement(${config.wrapperExport}, null,`);
92
+ lines.push(` React.createElement(${fragment.exportName}, args)`);
93
+ lines.push(' );');
94
+ } else {
95
+ lines.push(` return React.createElement(${fragment.exportName}, args);`);
96
+ }
97
+
98
+ lines.push('}');
99
+
100
+ return lines.join('\n');
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Create a preview adapter for the given framework.
106
+ */
107
+ export function createPreviewAdapter(
108
+ framework: string,
109
+ _config: PreviewConfig,
110
+ ): PreviewAdapter {
111
+ if (framework === 'react') {
112
+ return new ReactPreviewAdapter();
113
+ }
114
+ // Future: Vue, Svelte adapters
115
+ throw new Error(`No preview adapter available for framework: ${framework}`);
116
+ }
@@ -1,414 +1,29 @@
1
1
  /**
2
- * Style comparison utilities for comparing Figma design properties
3
- * with rendered component computed styles.
4
- */
5
-
6
- /**
7
- * Style diff result for a single CSS property
8
- */
9
- export interface StyleDiffItem {
10
- /** CSS property name */
11
- property: string;
12
- /** Expected value from Figma */
13
- figma: string;
14
- /** Actual value from rendered component */
15
- rendered: string;
16
- /** Whether values match (within tolerance) */
17
- match: boolean;
18
- }
19
-
20
- /**
21
- * Result of comparing styles
22
- */
23
- export interface StyleComparisonResult {
24
- /** Whether all styles match */
25
- match: boolean;
26
- /** Individual property comparisons */
27
- properties: StyleDiffItem[];
28
- /** CSS properties from Figma design */
29
- figmaStyles: Record<string, string>;
30
- /** Computed CSS properties from rendered component */
31
- renderedStyles: Record<string, string>;
32
- }
33
-
34
- /**
35
- * Compare Figma CSS properties with rendered computed styles.
36
- */
37
- export function compareStyles(
38
- figmaStyles: Record<string, string | undefined>,
39
- renderedStyles: Record<string, string>
40
- ): StyleComparisonResult {
41
- const properties: StyleDiffItem[] = [];
42
- const cleanFigmaStyles: Record<string, string> = {};
43
-
44
- // Properties to compare
45
- const propsToCompare = [
46
- "backgroundColor",
47
- "borderColor",
48
- "borderWidth",
49
- "borderRadius",
50
- "fontFamily",
51
- "fontSize",
52
- "fontWeight",
53
- "lineHeight",
54
- "letterSpacing",
55
- "textAlign",
56
- "boxShadow",
57
- "padding",
58
- "gap",
59
- "opacity",
60
- ];
61
-
62
- for (const prop of propsToCompare) {
63
- const figmaValue = figmaStyles[prop];
64
- const renderedValue = renderedStyles[prop];
65
-
66
- if (figmaValue !== undefined) {
67
- cleanFigmaStyles[prop] = figmaValue;
68
-
69
- const match = compareStyleValue(prop, figmaValue, renderedValue || "");
70
- properties.push({
71
- property: prop,
72
- figma: figmaValue,
73
- rendered: renderedValue || "(not set)",
74
- match,
75
- });
76
- }
77
- }
78
-
79
- const allMatch = properties.every((p) => p.match);
80
-
81
- return {
82
- match: allMatch,
83
- properties,
84
- figmaStyles: cleanFigmaStyles,
85
- renderedStyles,
86
- };
87
- }
88
-
89
- /**
90
- * Compare a single style value with tolerance for color and numeric differences.
91
- */
92
- export function compareStyleValue(
93
- prop: string,
94
- figma: string,
95
- rendered: string
96
- ): boolean {
97
- // Normalize values for comparison
98
- const normalizedFigma = normalizeStyleValue(prop, figma);
99
- const normalizedRendered = normalizeStyleValue(prop, rendered);
100
-
101
- // Direct match
102
- if (normalizedFigma === normalizedRendered) {
103
- return true;
104
- }
105
-
106
- // Color comparison with tolerance
107
- if (prop === "backgroundColor" || prop === "borderColor") {
108
- return compareColors(normalizedFigma, normalizedRendered, 5);
109
- }
110
-
111
- // Numeric comparison with tolerance (for pixels)
112
- if (
113
- ["borderWidth", "borderRadius", "fontSize", "padding", "gap"].includes(prop)
114
- ) {
115
- return compareNumericValues(normalizedFigma, normalizedRendered, 1);
116
- }
117
-
118
- return false;
119
- }
120
-
121
- /**
122
- * Normalize a style value for comparison.
123
- */
124
- export function normalizeStyleValue(prop: string, value: string): string {
125
- // Remove extra whitespace
126
- let normalized = value.trim().replace(/\s+/g, " ");
127
-
128
- // Normalize "none" shadow to empty
129
- if (prop === "boxShadow" && normalized === "none") {
130
- normalized = "";
131
- }
132
-
133
- // Normalize rgba(0, 0, 0, 0) to "transparent"
134
- if (normalized.match(/rgba\(\s*0\s*,\s*0\s*,\s*0\s*,\s*0\s*\)/)) {
135
- normalized = "transparent";
136
- }
137
-
138
- return normalized;
139
- }
140
-
141
- /**
142
- * Compare two color values with tolerance.
143
- */
144
- export function compareColors(
145
- color1: string,
146
- color2: string,
147
- tolerance: number
148
- ): boolean {
149
- const rgb1 = parseColor(color1);
150
- const rgb2 = parseColor(color2);
151
-
152
- if (!rgb1 || !rgb2) {
153
- return color1 === color2;
154
- }
155
-
156
- return (
157
- Math.abs(rgb1.r - rgb2.r) <= tolerance &&
158
- Math.abs(rgb1.g - rgb2.g) <= tolerance &&
159
- Math.abs(rgb1.b - rgb2.b) <= tolerance &&
160
- Math.abs((rgb1.a ?? 1) - (rgb2.a ?? 1)) <= 0.05
161
- );
162
- }
163
-
164
- /**
165
- * Parse a color string to RGB values.
166
- */
167
- export function parseColor(
168
- color: string
169
- ): { r: number; g: number; b: number; a?: number } | null {
170
- // Handle hex colors
171
- const hexMatch = color.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
172
- if (hexMatch) {
173
- return {
174
- r: parseInt(hexMatch[1], 16),
175
- g: parseInt(hexMatch[2], 16),
176
- b: parseInt(hexMatch[3], 16),
177
- };
178
- }
179
-
180
- // Handle rgb/rgba
181
- const rgbaMatch = color.match(
182
- /rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)/
183
- );
184
- if (rgbaMatch) {
185
- return {
186
- r: parseInt(rgbaMatch[1], 10),
187
- g: parseInt(rgbaMatch[2], 10),
188
- b: parseInt(rgbaMatch[3], 10),
189
- a: rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1,
190
- };
191
- }
192
-
193
- return null;
194
- }
195
-
196
- /**
197
- * Compare numeric values (e.g., "10px" vs "11px") with tolerance.
198
- */
199
- export function compareNumericValues(
200
- value1: string,
201
- value2: string,
202
- tolerance: number
203
- ): boolean {
204
- const num1 = parseFloat(value1);
205
- const num2 = parseFloat(value2);
206
-
207
- if (isNaN(num1) || isNaN(num2)) {
208
- return value1 === value2;
209
- }
210
-
211
- return Math.abs(num1 - num2) <= tolerance;
212
- }
213
-
214
- // ----- Enhanced Token-Aware Style Comparison -----
215
-
216
- import type {
217
- EnhancedStyleDiffItem,
218
- TokenFix,
219
- TokenUsageSummary,
220
- DesignToken,
221
- } from "../core/index.js";
222
-
223
- /**
224
- * Enhanced style diff result with token information
225
- */
226
- export interface EnhancedStyleComparisonResult extends StyleComparisonResult {
227
- /** Individual property comparisons with token info */
228
- properties: EnhancedStyleDiffItem[];
229
- /** Token usage summary */
230
- tokenSummary?: TokenUsageSummary;
231
- }
232
-
233
- /**
234
- * Token registry interface for style comparison
235
- * (subset of TokenRegistryManager methods needed here)
236
- */
237
- export interface TokenLookup {
238
- findByValue(value: string, theme?: string): string[];
239
- getToken(name: string): DesignToken | undefined;
240
- calculateUsageSummary(
241
- styleDiffs: Array<{
242
- property: string;
243
- figma: string;
244
- rendered: string;
245
- match: boolean;
246
- }>,
247
- theme?: string
248
- ): TokenUsageSummary;
249
- }
250
-
251
- /**
252
- * Compare styles with token awareness.
2
+ * Style comparison utilities thin re-export from @fragments-sdk/core.
253
3
  *
254
- * This enhanced version:
255
- * 1. Performs normal style comparison
256
- * 2. Identifies which values match design tokens
257
- * 3. Flags hardcoded values that should use tokens
258
- * 4. Generates fix suggestions
259
- */
260
- export function compareStylesWithTokens(
261
- figmaStyles: Record<string, string | undefined>,
262
- renderedStyles: Record<string, string>,
263
- tokenLookup?: TokenLookup,
264
- theme = "default"
265
- ): EnhancedStyleComparisonResult {
266
- const properties: EnhancedStyleDiffItem[] = [];
267
- const cleanFigmaStyles: Record<string, string> = {};
268
-
269
- // Properties to compare
270
- const propsToCompare = [
271
- "backgroundColor",
272
- "borderColor",
273
- "borderWidth",
274
- "borderRadius",
275
- "fontFamily",
276
- "fontSize",
277
- "fontWeight",
278
- "lineHeight",
279
- "letterSpacing",
280
- "textAlign",
281
- "boxShadow",
282
- "padding",
283
- "gap",
284
- "opacity",
285
- "color",
286
- ];
287
-
288
- for (const prop of propsToCompare) {
289
- const figmaValue = figmaStyles[prop];
290
- const renderedValue = renderedStyles[prop];
291
-
292
- if (figmaValue !== undefined) {
293
- cleanFigmaStyles[prop] = figmaValue;
294
-
295
- const match = compareStyleValue(prop, figmaValue, renderedValue || "");
296
-
297
- // Build enhanced diff item
298
- const item: EnhancedStyleDiffItem = {
299
- property: prop,
300
- figma: figmaValue,
301
- rendered: renderedValue || "(not set)",
302
- match,
303
- isHardcoded: false,
304
- };
305
-
306
- // Add token information if registry is available
307
- if (tokenLookup) {
308
- const figmaTokens = tokenLookup.findByValue(figmaValue, theme);
309
- const renderedTokens = renderedValue
310
- ? tokenLookup.findByValue(renderedValue, theme)
311
- : [];
312
-
313
- if (figmaTokens.length > 0) {
314
- item.figmaToken = figmaTokens[0];
315
- }
316
-
317
- if (renderedTokens.length > 0) {
318
- item.renderedToken = renderedTokens[0];
319
- }
320
-
321
- // Determine if this is a hardcoded value
322
- // Hardcoded = Figma matches a token, but rendered doesn't use a token
323
- item.isHardcoded = !!item.figmaToken && !item.renderedToken;
324
-
325
- // Generate fix suggestion if hardcoded
326
- if (item.isHardcoded && item.figmaToken) {
327
- const token = tokenLookup.getToken(item.figmaToken);
328
- if (token) {
329
- const cssProperty = toCssProperty(prop);
330
- item.suggestedFix = {
331
- tokenName: item.figmaToken,
332
- tokenValue: token.resolvedValue,
333
- codeFix: `${cssProperty}: var(${item.figmaToken});`,
334
- confidence: 0.9,
335
- reason: `Figma uses token ${item.figmaToken} (${token.resolvedValue}). Replace hardcoded value with token for consistency.`,
336
- };
337
- }
338
- }
339
- }
340
-
341
- properties.push(item);
342
- }
343
- }
344
-
345
- const allMatch = properties.every((p) => p.match);
346
-
347
- // Calculate token summary if registry available
348
- let tokenSummary: TokenUsageSummary | undefined;
349
- if (tokenLookup) {
350
- tokenSummary = tokenLookup.calculateUsageSummary(
351
- properties.map((p) => ({
352
- property: p.property,
353
- figma: p.figma,
354
- rendered: p.rendered,
355
- match: p.match,
356
- })),
357
- theme
358
- );
359
- }
360
-
361
- return {
362
- match: allMatch,
363
- properties,
364
- figmaStyles: cleanFigmaStyles,
365
- renderedStyles,
366
- tokenSummary,
367
- };
368
- }
369
-
370
- /**
371
- * Convert camelCase to kebab-case CSS property
372
- */
373
- function toCssProperty(prop: string): string {
374
- return prop.replace(/([A-Z])/g, "-$1").toLowerCase();
375
- }
376
-
377
- /**
378
- * Format token summary for display
379
- */
380
- export function formatTokenSummary(summary: TokenUsageSummary): string {
381
- const lines: string[] = [];
382
-
383
- lines.push(`Token Compliance: ${summary.compliancePercent}%`);
384
- lines.push(
385
- `${summary.usingTokens}/${summary.totalProperties} properties using tokens`
386
- );
387
-
388
- if (summary.hardcoded > 0) {
389
- lines.push(`${summary.hardcoded} hardcoded value(s) detected`);
390
- }
391
-
392
- if (summary.implicitMatches > 0) {
393
- lines.push(`${summary.implicitMatches} implicit match(es)`);
394
- }
395
-
396
- return lines.join("\n");
397
- }
398
-
399
- /**
400
- * Get status badge for token compliance
401
- */
402
- export function getComplianceBadge(
403
- compliancePercent: number
404
- ): { label: string; color: string } {
405
- if (compliancePercent >= 100) {
406
- return { label: "Excellent", color: "green" };
407
- } else if (compliancePercent >= 80) {
408
- return { label: "Good", color: "blue" };
409
- } else if (compliancePercent >= 50) {
410
- return { label: "Fair", color: "yellow" };
411
- } else {
412
- return { label: "Poor", color: "red" };
413
- }
414
- }
4
+ * The canonical comparison engine lives in @fragments-sdk/core/style-comparison.
5
+ * This file exists for backwards compatibility with existing imports.
6
+ */
7
+ export {
8
+ // Types
9
+ type StyleDiffItem,
10
+ type StyleComparisonResult,
11
+ type EnhancedStyleComparisonResult,
12
+ type TokenLookup,
13
+ type NormalizedToken,
14
+ type NormalizedStyleMap,
15
+ type StyleComparisonOptions,
16
+ // Functions
17
+ compareStyles,
18
+ compareStyleValue,
19
+ compareStylesWithTokens,
20
+ normalizeStyleValue,
21
+ compareColors,
22
+ parseColor,
23
+ compareNumericValues,
24
+ formatTokenSummary,
25
+ getComplianceBadge,
26
+ // Constants
27
+ DEFAULT_STYLE_PROPERTIES,
28
+ DEFAULT_ENHANCED_STYLE_PROPERTIES,
29
+ } from "@fragments-sdk/core";
@@ -1532,9 +1532,9 @@ export function fragmentsPlugin(options: FragmentsPluginOptions): Plugin[] {
1532
1532
  return null;
1533
1533
  },
1534
1534
 
1535
- // Handle HMR for fragment files
1535
+ // Handle HMR for fragment and contract files
1536
1536
  handleHotUpdate({ file, server }) {
1537
- if (fragmentFileSet.has(file)) {
1537
+ if (fragmentFileSet.has(file) || file.endsWith('.contract.json')) {
1538
1538
  // Invalidate the virtual fragments module
1539
1539
  const mod = server.moduleGraph.getModuleById(VIRTUAL_FRAGMENTS_RESOLVED);
1540
1540
  if (mod) {