@fragments-sdk/cli 0.2.2

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 (259) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/dist/bin.d.ts +1 -0
  4. package/dist/bin.js +4783 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/chunk-4FDQSGKX.js +786 -0
  7. package/dist/chunk-4FDQSGKX.js.map +1 -0
  8. package/dist/chunk-7H2MMGYG.js +369 -0
  9. package/dist/chunk-7H2MMGYG.js.map +1 -0
  10. package/dist/chunk-BSCG3IP7.js +619 -0
  11. package/dist/chunk-BSCG3IP7.js.map +1 -0
  12. package/dist/chunk-LY2CFFPY.js +898 -0
  13. package/dist/chunk-LY2CFFPY.js.map +1 -0
  14. package/dist/chunk-MUZ6CM66.js +6636 -0
  15. package/dist/chunk-MUZ6CM66.js.map +1 -0
  16. package/dist/chunk-OAENNG3G.js +1489 -0
  17. package/dist/chunk-OAENNG3G.js.map +1 -0
  18. package/dist/chunk-XHNKNI6J.js +235 -0
  19. package/dist/chunk-XHNKNI6J.js.map +1 -0
  20. package/dist/core-DWKLGY4N.js +68 -0
  21. package/dist/core-DWKLGY4N.js.map +1 -0
  22. package/dist/generate-4LQNJ7SX.js +249 -0
  23. package/dist/generate-4LQNJ7SX.js.map +1 -0
  24. package/dist/index.d.ts +775 -0
  25. package/dist/index.js +41 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/init-EMVI47QG.js +416 -0
  28. package/dist/init-EMVI47QG.js.map +1 -0
  29. package/dist/mcp-bin.d.ts +1 -0
  30. package/dist/mcp-bin.js +1117 -0
  31. package/dist/mcp-bin.js.map +1 -0
  32. package/dist/scan-4YPRF7FV.js +12 -0
  33. package/dist/scan-4YPRF7FV.js.map +1 -0
  34. package/dist/service-QSZMZJBJ.js +208 -0
  35. package/dist/service-QSZMZJBJ.js.map +1 -0
  36. package/dist/static-viewer-MIPGZ4Z7.js +12 -0
  37. package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
  38. package/dist/test-SQ5ZHXWU.js +1067 -0
  39. package/dist/test-SQ5ZHXWU.js.map +1 -0
  40. package/dist/tokens-HSGMYK64.js +173 -0
  41. package/dist/tokens-HSGMYK64.js.map +1 -0
  42. package/dist/viewer-YRF4SQE4.js +11101 -0
  43. package/dist/viewer-YRF4SQE4.js.map +1 -0
  44. package/package.json +107 -0
  45. package/src/ai.ts +266 -0
  46. package/src/analyze.ts +265 -0
  47. package/src/bin.ts +916 -0
  48. package/src/build.ts +248 -0
  49. package/src/commands/a11y.ts +302 -0
  50. package/src/commands/add.ts +313 -0
  51. package/src/commands/audit.ts +195 -0
  52. package/src/commands/baseline.ts +221 -0
  53. package/src/commands/build.ts +144 -0
  54. package/src/commands/compare.ts +337 -0
  55. package/src/commands/context.ts +107 -0
  56. package/src/commands/dev.ts +107 -0
  57. package/src/commands/enhance.ts +858 -0
  58. package/src/commands/generate.ts +391 -0
  59. package/src/commands/init.ts +531 -0
  60. package/src/commands/link/figma.ts +645 -0
  61. package/src/commands/link/index.ts +10 -0
  62. package/src/commands/link/storybook.ts +267 -0
  63. package/src/commands/list.ts +49 -0
  64. package/src/commands/metrics.ts +114 -0
  65. package/src/commands/reset.ts +242 -0
  66. package/src/commands/scan.ts +537 -0
  67. package/src/commands/storygen.ts +207 -0
  68. package/src/commands/tokens.ts +251 -0
  69. package/src/commands/validate.ts +93 -0
  70. package/src/commands/verify.ts +215 -0
  71. package/src/core/composition.test.ts +262 -0
  72. package/src/core/composition.ts +255 -0
  73. package/src/core/config.ts +84 -0
  74. package/src/core/constants.ts +111 -0
  75. package/src/core/context.ts +380 -0
  76. package/src/core/defineSegment.ts +137 -0
  77. package/src/core/discovery.ts +337 -0
  78. package/src/core/figma.ts +263 -0
  79. package/src/core/fragment-types.ts +214 -0
  80. package/src/core/generators/context.ts +389 -0
  81. package/src/core/generators/index.ts +23 -0
  82. package/src/core/generators/registry.ts +364 -0
  83. package/src/core/generators/typescript-extractor.ts +374 -0
  84. package/src/core/importAnalyzer.ts +217 -0
  85. package/src/core/index.ts +149 -0
  86. package/src/core/loader.ts +155 -0
  87. package/src/core/node.ts +63 -0
  88. package/src/core/parser.ts +551 -0
  89. package/src/core/previewLoader.ts +172 -0
  90. package/src/core/schema/fragment.schema.json +189 -0
  91. package/src/core/schema/registry.schema.json +137 -0
  92. package/src/core/schema.ts +182 -0
  93. package/src/core/storyAdapter.test.ts +571 -0
  94. package/src/core/storyAdapter.ts +761 -0
  95. package/src/core/token-types.ts +287 -0
  96. package/src/core/types.ts +754 -0
  97. package/src/diff.ts +323 -0
  98. package/src/index.ts +43 -0
  99. package/src/mcp/__tests__/projectFields.test.ts +130 -0
  100. package/src/mcp/bin.ts +36 -0
  101. package/src/mcp/index.ts +8 -0
  102. package/src/mcp/server.ts +1310 -0
  103. package/src/mcp/utils.ts +54 -0
  104. package/src/mcp-bin.ts +36 -0
  105. package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
  106. package/src/migrate/__tests__/args/args.test.ts +452 -0
  107. package/src/migrate/__tests__/meta/meta.test.ts +198 -0
  108. package/src/migrate/__tests__/stories/stories.test.ts +278 -0
  109. package/src/migrate/__tests__/utils/utils.test.ts +371 -0
  110. package/src/migrate/__tests__/values/values.test.ts +303 -0
  111. package/src/migrate/bin.ts +108 -0
  112. package/src/migrate/converter.ts +658 -0
  113. package/src/migrate/detect.ts +196 -0
  114. package/src/migrate/index.ts +45 -0
  115. package/src/migrate/migrate.ts +163 -0
  116. package/src/migrate/parser.ts +1136 -0
  117. package/src/migrate/report.ts +624 -0
  118. package/src/migrate/types.ts +169 -0
  119. package/src/screenshot.ts +249 -0
  120. package/src/service/__tests__/ast-utils.test.ts +426 -0
  121. package/src/service/__tests__/enhance-scanner.test.ts +200 -0
  122. package/src/service/__tests__/figma/figma.test.ts +652 -0
  123. package/src/service/__tests__/metrics-store.test.ts +409 -0
  124. package/src/service/__tests__/patch-generator.test.ts +186 -0
  125. package/src/service/__tests__/props-extractor.test.ts +365 -0
  126. package/src/service/__tests__/token-registry.test.ts +267 -0
  127. package/src/service/analytics.ts +659 -0
  128. package/src/service/ast-utils.ts +444 -0
  129. package/src/service/browser-pool.ts +339 -0
  130. package/src/service/capture.ts +267 -0
  131. package/src/service/diff.ts +279 -0
  132. package/src/service/enhance/aggregator.ts +489 -0
  133. package/src/service/enhance/cache.ts +275 -0
  134. package/src/service/enhance/codebase-scanner.ts +357 -0
  135. package/src/service/enhance/context-generator.ts +529 -0
  136. package/src/service/enhance/doc-extractor.ts +523 -0
  137. package/src/service/enhance/index.ts +131 -0
  138. package/src/service/enhance/props-extractor.ts +665 -0
  139. package/src/service/enhance/scanner.ts +445 -0
  140. package/src/service/enhance/storybook-parser.ts +552 -0
  141. package/src/service/enhance/types.ts +346 -0
  142. package/src/service/enhance/variant-renderer.ts +479 -0
  143. package/src/service/figma.ts +1008 -0
  144. package/src/service/index.ts +249 -0
  145. package/src/service/metrics-store.ts +333 -0
  146. package/src/service/patch-generator.ts +349 -0
  147. package/src/service/report.ts +854 -0
  148. package/src/service/storage.ts +401 -0
  149. package/src/service/token-fixes.ts +281 -0
  150. package/src/service/token-parser.ts +504 -0
  151. package/src/service/token-registry.ts +721 -0
  152. package/src/service/utils.ts +172 -0
  153. package/src/setup.ts +241 -0
  154. package/src/shared/command-wrapper.ts +81 -0
  155. package/src/shared/dev-server-client.ts +199 -0
  156. package/src/shared/index.ts +8 -0
  157. package/src/shared/segment-loader.ts +59 -0
  158. package/src/shared/types.ts +147 -0
  159. package/src/static-viewer.ts +715 -0
  160. package/src/test/discovery.ts +172 -0
  161. package/src/test/index.ts +281 -0
  162. package/src/test/reporters/console.ts +194 -0
  163. package/src/test/reporters/json.ts +190 -0
  164. package/src/test/reporters/junit.ts +186 -0
  165. package/src/test/runner.ts +598 -0
  166. package/src/test/types.ts +245 -0
  167. package/src/test/watch.ts +200 -0
  168. package/src/validators.ts +152 -0
  169. package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
  170. package/src/viewer/__tests__/render-utils.test.ts +232 -0
  171. package/src/viewer/__tests__/style-utils.test.ts +404 -0
  172. package/src/viewer/bin.ts +86 -0
  173. package/src/viewer/cli/health.ts +256 -0
  174. package/src/viewer/cli/index.ts +33 -0
  175. package/src/viewer/cli/scan.ts +124 -0
  176. package/src/viewer/cli/utils.ts +174 -0
  177. package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
  178. package/src/viewer/components/ActionCapture.tsx +172 -0
  179. package/src/viewer/components/ActionsPanel.tsx +371 -0
  180. package/src/viewer/components/App.tsx +638 -0
  181. package/src/viewer/components/BottomPanel.tsx +224 -0
  182. package/src/viewer/components/CodePanel.tsx +589 -0
  183. package/src/viewer/components/CommandPalette.tsx +336 -0
  184. package/src/viewer/components/ComponentGraph.tsx +394 -0
  185. package/src/viewer/components/ComponentHeader.tsx +85 -0
  186. package/src/viewer/components/ContractPanel.tsx +234 -0
  187. package/src/viewer/components/ErrorBoundary.tsx +85 -0
  188. package/src/viewer/components/FigmaEmbed.tsx +231 -0
  189. package/src/viewer/components/FragmentEditor.tsx +485 -0
  190. package/src/viewer/components/HealthDashboard.tsx +452 -0
  191. package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
  192. package/src/viewer/components/Icons.tsx +417 -0
  193. package/src/viewer/components/InteractionsPanel.tsx +720 -0
  194. package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
  195. package/src/viewer/components/IsolatedRender.tsx +111 -0
  196. package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
  197. package/src/viewer/components/LandingPage.tsx +441 -0
  198. package/src/viewer/components/Layout.tsx +22 -0
  199. package/src/viewer/components/LeftSidebar.tsx +391 -0
  200. package/src/viewer/components/MultiViewportPreview.tsx +429 -0
  201. package/src/viewer/components/PreviewArea.tsx +404 -0
  202. package/src/viewer/components/PreviewFrameHost.tsx +310 -0
  203. package/src/viewer/components/PreviewPane.tsx +150 -0
  204. package/src/viewer/components/PreviewToolbar.tsx +176 -0
  205. package/src/viewer/components/PropsEditor.tsx +512 -0
  206. package/src/viewer/components/PropsTable.tsx +98 -0
  207. package/src/viewer/components/RelationsSection.tsx +57 -0
  208. package/src/viewer/components/ResizablePanel.tsx +328 -0
  209. package/src/viewer/components/RightSidebar.tsx +118 -0
  210. package/src/viewer/components/ScreenshotButton.tsx +90 -0
  211. package/src/viewer/components/Sidebar.tsx +169 -0
  212. package/src/viewer/components/SkeletonLoader.tsx +156 -0
  213. package/src/viewer/components/StoryRenderer.tsx +128 -0
  214. package/src/viewer/components/ThemeProvider.tsx +96 -0
  215. package/src/viewer/components/Toast.tsx +67 -0
  216. package/src/viewer/components/TokenStylePanel.tsx +708 -0
  217. package/src/viewer/components/UsageSection.tsx +95 -0
  218. package/src/viewer/components/VariantMatrix.tsx +350 -0
  219. package/src/viewer/components/VariantRenderer.tsx +131 -0
  220. package/src/viewer/components/VariantTabs.tsx +84 -0
  221. package/src/viewer/components/ViewportSelector.tsx +165 -0
  222. package/src/viewer/components/_future/CreatePage.tsx +836 -0
  223. package/src/viewer/composition-renderer.ts +381 -0
  224. package/src/viewer/constants/index.ts +1 -0
  225. package/src/viewer/constants/ui.ts +185 -0
  226. package/src/viewer/entry.tsx +299 -0
  227. package/src/viewer/hooks/index.ts +2 -0
  228. package/src/viewer/hooks/useA11yCache.ts +383 -0
  229. package/src/viewer/hooks/useA11yService.ts +498 -0
  230. package/src/viewer/hooks/useActions.ts +138 -0
  231. package/src/viewer/hooks/useAppState.ts +124 -0
  232. package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
  233. package/src/viewer/hooks/useHmrStatus.ts +109 -0
  234. package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
  235. package/src/viewer/hooks/usePreviewBridge.ts +347 -0
  236. package/src/viewer/hooks/useScrollSpy.ts +78 -0
  237. package/src/viewer/hooks/useUrlState.ts +330 -0
  238. package/src/viewer/hooks/useViewSettings.ts +125 -0
  239. package/src/viewer/index.html +28 -0
  240. package/src/viewer/index.ts +14 -0
  241. package/src/viewer/intelligence/healthReport.ts +505 -0
  242. package/src/viewer/intelligence/styleDrift.ts +340 -0
  243. package/src/viewer/intelligence/usageScanner.ts +309 -0
  244. package/src/viewer/jsx-parser.ts +485 -0
  245. package/src/viewer/postcss.config.js +6 -0
  246. package/src/viewer/preview-frame-entry.tsx +25 -0
  247. package/src/viewer/preview-frame.html +109 -0
  248. package/src/viewer/render-template.html +68 -0
  249. package/src/viewer/render-utils.ts +170 -0
  250. package/src/viewer/server.ts +276 -0
  251. package/src/viewer/style-utils.ts +414 -0
  252. package/src/viewer/styles/globals.css +355 -0
  253. package/src/viewer/tailwind.config.js +37 -0
  254. package/src/viewer/types/a11y.ts +197 -0
  255. package/src/viewer/utils/a11y-fixes.ts +471 -0
  256. package/src/viewer/utils/actionExport.ts +372 -0
  257. package/src/viewer/utils/colorSchemes.ts +201 -0
  258. package/src/viewer/utils/detectRelationships.ts +256 -0
  259. package/src/viewer/vite-plugin.ts +2143 -0
@@ -0,0 +1,340 @@
1
+ /**
2
+ * Style Drift Detection
3
+ * Analyzes style drift between Figma designs and rendered components
4
+ */
5
+
6
+ import type { SegmentDefinition, DesignToken } from '../../core/index.js';
7
+
8
+ export type DriftSeverity = 'high' | 'medium' | 'low';
9
+
10
+ export interface StyleDrift {
11
+ property: string;
12
+ expected: string;
13
+ actual: string;
14
+ expectedToken?: string;
15
+ severity: DriftSeverity;
16
+ suggestion?: string;
17
+ }
18
+
19
+ export interface DriftReport {
20
+ component: string;
21
+ variant: string;
22
+ figmaUrl?: string;
23
+ drifts: StyleDrift[];
24
+ complianceScore: number;
25
+ totalProperties: number;
26
+ matchingProperties: number;
27
+ }
28
+
29
+ export interface DriftScanOptions {
30
+ /** Filter to specific components */
31
+ components?: string[];
32
+ /** Filter to specific CSS properties */
33
+ properties?: string[];
34
+ /** Minimum compliance score to report (0-100) */
35
+ threshold?: number;
36
+ }
37
+
38
+ export interface DriftSummary {
39
+ totalComponents: number;
40
+ componentsWithDrift: number;
41
+ totalDrifts: number;
42
+ averageCompliance: number;
43
+ byProperty: Record<string, number>;
44
+ bySeverity: Record<DriftSeverity, number>;
45
+ }
46
+
47
+ export interface FullDriftResult {
48
+ reports: DriftReport[];
49
+ summary: DriftSummary;
50
+ }
51
+
52
+ /**
53
+ * Severity rules for different CSS properties
54
+ * High: Brand-critical properties
55
+ * Medium: Layout properties
56
+ * Low: Decorative properties
57
+ */
58
+ const PROPERTY_SEVERITY: Record<string, DriftSeverity> = {
59
+ // High severity - brand critical
60
+ color: 'high',
61
+ backgroundColor: 'high',
62
+ borderColor: 'high',
63
+ fontFamily: 'high',
64
+ fontSize: 'high',
65
+ fontWeight: 'high',
66
+
67
+ // Medium severity - layout
68
+ padding: 'medium',
69
+ margin: 'medium',
70
+ gap: 'medium',
71
+ borderRadius: 'medium',
72
+ borderWidth: 'medium',
73
+ lineHeight: 'medium',
74
+ letterSpacing: 'medium',
75
+
76
+ // Low severity - decorative
77
+ boxShadow: 'low',
78
+ opacity: 'low',
79
+ transition: 'low',
80
+ textAlign: 'low',
81
+ };
82
+
83
+ /**
84
+ * Get severity for a CSS property
85
+ */
86
+ export function getPropertySeverity(property: string): DriftSeverity {
87
+ return PROPERTY_SEVERITY[property] || 'low';
88
+ }
89
+
90
+ /**
91
+ * Normalize a CSS value for comparison
92
+ */
93
+ function normalizeValue(value: string): string {
94
+ if (!value) return '';
95
+
96
+ const trimmed = value.toLowerCase().trim();
97
+
98
+ // Normalize hex colors
99
+ if (trimmed.match(/^#[0-9a-f]{3}$/i)) {
100
+ const [r, g, b] = trimmed.slice(1).split('');
101
+ return `#${r}${r}${g}${g}${b}${b}`;
102
+ }
103
+
104
+ // Normalize rgb to hex
105
+ const rgbMatch = value.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
106
+ if (rgbMatch) {
107
+ const r = parseInt(rgbMatch[1], 10).toString(16).padStart(2, '0');
108
+ const g = parseInt(rgbMatch[2], 10).toString(16).padStart(2, '0');
109
+ const b = parseInt(rgbMatch[3], 10).toString(16).padStart(2, '0');
110
+ return `#${r}${g}${b}`;
111
+ }
112
+
113
+ return trimmed;
114
+ }
115
+
116
+ /**
117
+ * Compare two style values with tolerance for numeric values
118
+ */
119
+ function valuesMatch(property: string, expected: string, actual: string): boolean {
120
+ if (expected === actual) return true;
121
+ if (!expected || !actual) return false;
122
+
123
+ const normalizedExpected = normalizeValue(expected);
124
+ const normalizedActual = normalizeValue(actual);
125
+
126
+ if (normalizedExpected === normalizedActual) return true;
127
+
128
+ // Numeric comparison with tolerance
129
+ const expectedNum = parseFloat(expected);
130
+ const actualNum = parseFloat(actual);
131
+
132
+ if (!isNaN(expectedNum) && !isNaN(actualNum)) {
133
+ return Math.abs(expectedNum - actualNum) <= 1;
134
+ }
135
+
136
+ return false;
137
+ }
138
+
139
+ /**
140
+ * Find a token that matches a value
141
+ */
142
+ function findMatchingToken(
143
+ value: string,
144
+ tokens: DesignToken[],
145
+ property: string
146
+ ): DesignToken | null {
147
+ const normalized = normalizeValue(value);
148
+
149
+ // Property-to-category mapping
150
+ const categoryMap: Record<string, string[]> = {
151
+ color: ['color'],
152
+ backgroundColor: ['color'],
153
+ borderColor: ['color'],
154
+ fontSize: ['typography'],
155
+ fontWeight: ['typography'],
156
+ fontFamily: ['typography'],
157
+ padding: ['spacing'],
158
+ margin: ['spacing'],
159
+ gap: ['spacing'],
160
+ borderRadius: ['radius'],
161
+ borderWidth: ['border'],
162
+ };
163
+
164
+ const expectedCategories = categoryMap[property];
165
+
166
+ for (const token of tokens) {
167
+ const tokenNormalized = normalizeValue(token.resolvedValue);
168
+ if (tokenNormalized === normalized) {
169
+ // If we have expected categories, check for match
170
+ if (expectedCategories && expectedCategories.includes(token.category)) {
171
+ return token;
172
+ }
173
+ // If no expected categories, accept any match
174
+ if (!expectedCategories) {
175
+ return token;
176
+ }
177
+ }
178
+ }
179
+
180
+ return null;
181
+ }
182
+
183
+ /**
184
+ * Generate a fix suggestion for a drift
185
+ */
186
+ function generateSuggestion(property: string, token: DesignToken | null): string | undefined {
187
+ if (!token) return undefined;
188
+
189
+ const cssProperty = property.replace(/([A-Z])/g, '-$1').toLowerCase();
190
+ return `${cssProperty}: var(${token.name});`;
191
+ }
192
+
193
+ /**
194
+ * Analyze drift for a single component/variant
195
+ */
196
+ export function analyzeVariantDrift(
197
+ figmaStyles: Record<string, string>,
198
+ renderedStyles: Record<string, string>,
199
+ tokens: DesignToken[] = [],
200
+ options: DriftScanOptions = {}
201
+ ): { drifts: StyleDrift[]; complianceScore: number; totalProperties: number; matchingProperties: number } {
202
+ const allProps = new Set([...Object.keys(figmaStyles), ...Object.keys(renderedStyles)]);
203
+ const propertyFilter = options.properties ? new Set(options.properties) : null;
204
+
205
+ const drifts: StyleDrift[] = [];
206
+ let matchingCount = 0;
207
+ let totalCount = 0;
208
+
209
+ for (const property of allProps) {
210
+ // Apply property filter if specified
211
+ if (propertyFilter && !propertyFilter.has(property)) continue;
212
+
213
+ const expected = figmaStyles[property] || '';
214
+ const actual = renderedStyles[property] || '';
215
+
216
+ // Skip if both are empty
217
+ if (!expected && !actual) continue;
218
+
219
+ totalCount++;
220
+
221
+ if (valuesMatch(property, expected, actual)) {
222
+ matchingCount++;
223
+ continue;
224
+ }
225
+
226
+ // Find token for expected value
227
+ const expectedToken = findMatchingToken(expected, tokens, property);
228
+ const severity = getPropertySeverity(property);
229
+
230
+ drifts.push({
231
+ property,
232
+ expected: expected || '(not set)',
233
+ actual: actual || '(not set)',
234
+ expectedToken: expectedToken?.name,
235
+ severity,
236
+ suggestion: generateSuggestion(property, expectedToken),
237
+ });
238
+ }
239
+
240
+ const complianceScore = totalCount > 0 ? Math.round((matchingCount / totalCount) * 100) : 100;
241
+
242
+ return {
243
+ drifts,
244
+ complianceScore,
245
+ totalProperties: totalCount,
246
+ matchingProperties: matchingCount,
247
+ };
248
+ }
249
+
250
+ /**
251
+ * Create a drift report for a component/variant
252
+ */
253
+ export function createDriftReport(
254
+ component: string,
255
+ variant: string,
256
+ figmaStyles: Record<string, string>,
257
+ renderedStyles: Record<string, string>,
258
+ tokens: DesignToken[] = [],
259
+ figmaUrl?: string,
260
+ options: DriftScanOptions = {}
261
+ ): DriftReport {
262
+ const analysis = analyzeVariantDrift(figmaStyles, renderedStyles, tokens, options);
263
+
264
+ return {
265
+ component,
266
+ variant,
267
+ figmaUrl,
268
+ drifts: analysis.drifts,
269
+ complianceScore: analysis.complianceScore,
270
+ totalProperties: analysis.totalProperties,
271
+ matchingProperties: analysis.matchingProperties,
272
+ };
273
+ }
274
+
275
+ /**
276
+ * Aggregate drift reports into a summary
277
+ */
278
+ export function aggregateDriftReports(reports: DriftReport[]): DriftSummary {
279
+ const byProperty: Record<string, number> = {};
280
+ const bySeverity: Record<DriftSeverity, number> = { high: 0, medium: 0, low: 0 };
281
+
282
+ let totalDrifts = 0;
283
+ let componentsWithDrift = 0;
284
+ let totalCompliance = 0;
285
+
286
+ for (const report of reports) {
287
+ if (report.drifts.length > 0) {
288
+ componentsWithDrift++;
289
+ }
290
+
291
+ totalCompliance += report.complianceScore;
292
+
293
+ for (const drift of report.drifts) {
294
+ totalDrifts++;
295
+ byProperty[drift.property] = (byProperty[drift.property] || 0) + 1;
296
+ bySeverity[drift.severity]++;
297
+ }
298
+ }
299
+
300
+ return {
301
+ totalComponents: reports.length,
302
+ componentsWithDrift,
303
+ totalDrifts,
304
+ averageCompliance: reports.length > 0 ? Math.round(totalCompliance / reports.length) : 100,
305
+ byProperty,
306
+ bySeverity,
307
+ };
308
+ }
309
+
310
+ /**
311
+ * Get the worst offenders (components with most/worst drift)
312
+ */
313
+ export function getWorstOffenders(reports: DriftReport[], limit = 5): DriftReport[] {
314
+ // Sort by: 1) High severity count, 2) Total drifts, 3) Compliance (ascending)
315
+ return [...reports]
316
+ .filter((r) => r.drifts.length > 0)
317
+ .sort((a, b) => {
318
+ const aHigh = a.drifts.filter((d) => d.severity === 'high').length;
319
+ const bHigh = b.drifts.filter((d) => d.severity === 'high').length;
320
+
321
+ if (aHigh !== bHigh) return bHigh - aHigh;
322
+ if (a.drifts.length !== b.drifts.length) return b.drifts.length - a.drifts.length;
323
+ return a.complianceScore - b.complianceScore;
324
+ })
325
+ .slice(0, limit);
326
+ }
327
+
328
+ /**
329
+ * Generate fix suggestions for all drifts in a report
330
+ */
331
+ export function generateAllFixes(report: DriftReport): string {
332
+ const fixes = report.drifts
333
+ .filter((d) => d.suggestion)
334
+ .map((d) => `/* ${d.property}: ${d.expected} -> ${d.actual} */\n${d.suggestion}`)
335
+ .join('\n\n');
336
+
337
+ if (!fixes) return '';
338
+
339
+ return `/* Fixes for ${report.component} - ${report.variant} */\n\n${fixes}`;
340
+ }
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Component Usage Scanner
3
+ * Scans a codebase for design system component imports and usage
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+
9
+ export interface UsageLocation {
10
+ file: string;
11
+ line: number;
12
+ importType: 'named' | 'default' | 'namespace';
13
+ usageCount: number;
14
+ }
15
+
16
+ export interface UsageScanResult {
17
+ component: string;
18
+ usages: UsageLocation[];
19
+ totalUsages: number;
20
+ }
21
+
22
+ export interface ScanOptions {
23
+ /** Directory to scan */
24
+ directory: string;
25
+ /** Glob patterns to include */
26
+ include?: string[];
27
+ /** Glob patterns to exclude */
28
+ exclude?: string[];
29
+ /** Filter to specific components */
30
+ components?: string[];
31
+ /** Package names to look for */
32
+ packagePatterns?: string[];
33
+ }
34
+
35
+ export interface ScanSummary {
36
+ totalFiles: number;
37
+ filesWithUsage: number;
38
+ totalComponents: number;
39
+ totalUsages: number;
40
+ scanTimeMs: number;
41
+ }
42
+
43
+ export interface FullScanResult {
44
+ results: UsageScanResult[];
45
+ summary: ScanSummary;
46
+ }
47
+
48
+ const DEFAULT_EXCLUDE = [
49
+ 'node_modules',
50
+ 'dist',
51
+ '.git',
52
+ 'build',
53
+ 'coverage',
54
+ '.next',
55
+ '.cache',
56
+ '__tests__',
57
+ ];
58
+
59
+ const DEFAULT_EXTENSIONS = ['.tsx', '.ts', '.jsx', '.js'];
60
+
61
+ /**
62
+ * Recursively find all files matching criteria
63
+ */
64
+ function findFiles(
65
+ dir: string,
66
+ exclude: string[],
67
+ extensions: string[],
68
+ files: string[] = []
69
+ ): string[] {
70
+ if (!fs.existsSync(dir)) {
71
+ return files;
72
+ }
73
+
74
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
75
+
76
+ for (const entry of entries) {
77
+ const fullPath = path.join(dir, entry.name);
78
+
79
+ // Check exclusions
80
+ const shouldExclude = exclude.some((pattern) => {
81
+ if (pattern.includes('*')) {
82
+ const regex = new RegExp(pattern.replace(/\./g, '\\.').replace(/\*/g, '.*'));
83
+ return regex.test(entry.name);
84
+ }
85
+ return entry.name === pattern;
86
+ });
87
+
88
+ if (shouldExclude) {
89
+ continue;
90
+ }
91
+
92
+ if (entry.isDirectory()) {
93
+ findFiles(fullPath, exclude, extensions, files);
94
+ } else if (entry.isFile() && extensions.some((ext) => entry.name.endsWith(ext))) {
95
+ // Skip test and story files
96
+ if (entry.name.includes('.test.') || entry.name.includes('.spec.') ||
97
+ entry.name.includes('.stories.') || entry.name.includes('.segment.')) {
98
+ continue;
99
+ }
100
+ files.push(fullPath);
101
+ }
102
+ }
103
+
104
+ return files;
105
+ }
106
+
107
+ /**
108
+ * Parse a file for component imports from design system packages
109
+ */
110
+ function parseFileForUsage(
111
+ filePath: string,
112
+ packagePatterns: string[]
113
+ ): Map<string, UsageLocation> {
114
+ const results = new Map<string, UsageLocation>();
115
+
116
+ try {
117
+ const content = fs.readFileSync(filePath, 'utf-8');
118
+ const lines = content.split('\n');
119
+
120
+ // Build regex patterns for package matching
121
+ const packageRegexStr = packagePatterns
122
+ .map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
123
+ .join('|');
124
+
125
+ // Named import pattern: import { Button, Card } from '@fragments/react'
126
+ const namedImportRegex = new RegExp(
127
+ `import\\s*\\{([^}]+)\\}\\s*from\\s*['"](?:${packageRegexStr})[^'"]*['"]`,
128
+ 'g'
129
+ );
130
+
131
+ // Default import pattern: import Button from '@fragments/react/Button'
132
+ const defaultImportRegex = new RegExp(
133
+ `import\\s+(\\w+)\\s+from\\s*['"](?:${packageRegexStr})[^'"]*['"]`,
134
+ 'g'
135
+ );
136
+
137
+ // Namespace import pattern: import * as UI from '@fragments/react'
138
+ const namespaceImportRegex = new RegExp(
139
+ `import\\s*\\*\\s*as\\s+(\\w+)\\s+from\\s*['"](?:${packageRegexStr})[^'"]*['"]`,
140
+ 'g'
141
+ );
142
+
143
+ lines.forEach((line, index) => {
144
+ // Named imports
145
+ let match: RegExpExecArray | null;
146
+ namedImportRegex.lastIndex = 0;
147
+
148
+ while ((match = namedImportRegex.exec(line)) !== null) {
149
+ const componentNames = match[1].split(',').map((c) => {
150
+ // Handle "Component as Alias" syntax
151
+ const parts = c.trim().split(/\s+as\s+/);
152
+ return parts[0].trim();
153
+ }).filter((c) => c && /^[A-Z]/.test(c));
154
+
155
+ for (const component of componentNames) {
156
+ if (!results.has(component)) {
157
+ results.set(component, {
158
+ file: filePath,
159
+ line: index + 1,
160
+ importType: 'named',
161
+ usageCount: 0,
162
+ });
163
+ }
164
+ }
165
+ }
166
+
167
+ // Default imports (only if PascalCase - likely a component)
168
+ defaultImportRegex.lastIndex = 0;
169
+ while ((match = defaultImportRegex.exec(line)) !== null) {
170
+ const component = match[1].trim();
171
+ if (/^[A-Z]/.test(component)) {
172
+ if (!results.has(component)) {
173
+ results.set(component, {
174
+ file: filePath,
175
+ line: index + 1,
176
+ importType: 'default',
177
+ usageCount: 0,
178
+ });
179
+ }
180
+ }
181
+ }
182
+
183
+ // Namespace imports
184
+ namespaceImportRegex.lastIndex = 0;
185
+ while ((match = namespaceImportRegex.exec(line)) !== null) {
186
+ const namespace = match[1].trim();
187
+ if (!results.has(namespace)) {
188
+ results.set(namespace, {
189
+ file: filePath,
190
+ line: index + 1,
191
+ importType: 'namespace',
192
+ usageCount: 0,
193
+ });
194
+ }
195
+ }
196
+ });
197
+
198
+ // Count component usages in JSX
199
+ for (const [component, location] of results) {
200
+ if (location.importType === 'namespace') {
201
+ const nsUsageRegex = new RegExp(`<${component}\\.\\w+`, 'g');
202
+ const matches = content.match(nsUsageRegex);
203
+ location.usageCount = matches ? matches.length : 0;
204
+ } else {
205
+ const jsxOpenRegex = new RegExp(`<${component}(?:\\s|>|\\/)`, 'g');
206
+ const matches = content.match(jsxOpenRegex);
207
+ location.usageCount = matches ? matches.length : 0;
208
+ }
209
+ }
210
+
211
+ return results;
212
+ } catch {
213
+ return results;
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Scan a directory for component usage
219
+ */
220
+ export async function scanForUsages(options: ScanOptions): Promise<FullScanResult> {
221
+ const startTime = Date.now();
222
+
223
+ const {
224
+ directory,
225
+ exclude = DEFAULT_EXCLUDE,
226
+ components,
227
+ packagePatterns = ['@fragments'],
228
+ } = options;
229
+
230
+ const resolvedDir = path.resolve(directory);
231
+ const files = findFiles(resolvedDir, exclude, DEFAULT_EXTENSIONS);
232
+
233
+ const componentMap = new Map<string, UsageLocation[]>();
234
+ let filesWithUsage = 0;
235
+
236
+ for (const file of files) {
237
+ const fileResults = parseFileForUsage(file, packagePatterns);
238
+
239
+ if (fileResults.size > 0) {
240
+ filesWithUsage++;
241
+ }
242
+
243
+ for (const [component, location] of fileResults) {
244
+ if (components && !components.includes(component)) {
245
+ continue;
246
+ }
247
+
248
+ if (!componentMap.has(component)) {
249
+ componentMap.set(component, []);
250
+ }
251
+ componentMap.get(component)!.push(location);
252
+ }
253
+ }
254
+
255
+ const results: UsageScanResult[] = [];
256
+
257
+ for (const [component, usages] of componentMap) {
258
+ const totalUsages = usages.reduce((sum, u) => sum + u.usageCount, 0);
259
+ results.push({
260
+ component,
261
+ usages,
262
+ totalUsages,
263
+ });
264
+ }
265
+
266
+ results.sort((a, b) => b.totalUsages - a.totalUsages);
267
+
268
+ const scanTimeMs = Date.now() - startTime;
269
+
270
+ return {
271
+ results,
272
+ summary: {
273
+ totalFiles: files.length,
274
+ filesWithUsage,
275
+ totalComponents: results.length,
276
+ totalUsages: results.reduce((sum, r) => sum + r.totalUsages, 0),
277
+ scanTimeMs,
278
+ },
279
+ };
280
+ }
281
+
282
+ /**
283
+ * Get a simple usage count map for a quick summary
284
+ */
285
+ export function getUsageCounts(results: UsageScanResult[]): Map<string, number> {
286
+ return new Map(results.map((r) => [r.component, r.totalUsages]));
287
+ }
288
+
289
+ /**
290
+ * Find components that are imported but never used in JSX
291
+ */
292
+ export function findUnusedImports(results: UsageScanResult[]): string[] {
293
+ return results
294
+ .filter((r) => r.totalUsages === 0)
295
+ .map((r) => r.component);
296
+ }
297
+
298
+ /**
299
+ * Get files using a specific component
300
+ */
301
+ export function getFilesUsingComponent(
302
+ results: UsageScanResult[],
303
+ componentName: string
304
+ ): string[] {
305
+ const result = results.find((r) => r.component === componentName);
306
+ if (!result) return [];
307
+
308
+ return [...new Set(result.usages.map((u) => u.file))];
309
+ }