@fragments-sdk/cli 0.10.1 → 0.12.1

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 (223) hide show
  1. package/dist/ai-client-I6MDWNYA.js +21 -0
  2. package/dist/bin.js +292 -367
  3. package/dist/bin.js.map +1 -1
  4. package/dist/{chunk-PW7QTQA6.js → chunk-4OC7FTJB.js} +2 -2
  5. package/dist/{chunk-HRFUSSZI.js → chunk-AM4MRTMN.js} +2 -2
  6. package/dist/{chunk-5G3VZH43.js → chunk-GVDSFQ4E.js} +281 -351
  7. package/dist/chunk-GVDSFQ4E.js.map +1 -0
  8. package/dist/chunk-JJ2VRTBU.js +626 -0
  9. package/dist/chunk-JJ2VRTBU.js.map +1 -0
  10. package/dist/{chunk-D5PYOXEI.js → chunk-LVWFOLUZ.js} +148 -13
  11. package/dist/{chunk-D5PYOXEI.js.map → chunk-LVWFOLUZ.js.map} +1 -1
  12. package/dist/{chunk-WXSR2II7.js → chunk-OQKMEFOS.js} +58 -6
  13. package/dist/chunk-OQKMEFOS.js.map +1 -0
  14. package/dist/chunk-SXTKFDCR.js +104 -0
  15. package/dist/chunk-SXTKFDCR.js.map +1 -0
  16. package/dist/chunk-T5OMVL7E.js +443 -0
  17. package/dist/chunk-T5OMVL7E.js.map +1 -0
  18. package/dist/{chunk-ZM4ZQZWZ.js → chunk-TPWGL2XS.js} +39 -37
  19. package/dist/chunk-TPWGL2XS.js.map +1 -0
  20. package/dist/{chunk-OQO55NKV.js → chunk-WFS63PCW.js} +85 -11
  21. package/dist/chunk-WFS63PCW.js.map +1 -0
  22. package/dist/core/index.js +9 -1
  23. package/dist/{discovery-NEOY4MPN.js → discovery-ZJQSXF56.js} +3 -3
  24. package/dist/{generate-FBHSXR3D.js → generate-RJFS2JWA.js} +4 -4
  25. package/dist/index.js +7 -6
  26. package/dist/index.js.map +1 -1
  27. package/dist/init-ZSX3NRCZ.js +636 -0
  28. package/dist/init-ZSX3NRCZ.js.map +1 -0
  29. package/dist/mcp-bin.js +2 -2
  30. package/dist/{scan-CJF2DOQW.js → scan-3PMCJ4RB.js} +6 -6
  31. package/dist/scan-generate-SYU4PYZD.js +1115 -0
  32. package/dist/scan-generate-SYU4PYZD.js.map +1 -0
  33. package/dist/{service-TQYWY65E.js → service-VMGNJZ42.js} +3 -3
  34. package/dist/snapshot-XOISO2IS.js +139 -0
  35. package/dist/snapshot-XOISO2IS.js.map +1 -0
  36. package/dist/{static-viewer-NUBFPKWH.js → static-viewer-5GXH2MGE.js} +3 -3
  37. package/dist/static-viewer-5GXH2MGE.js.map +1 -0
  38. package/dist/{test-Z5LVO724.js → test-SI4NSHQX.js} +4 -4
  39. package/dist/{tokens-CE46OTMD.js → tokens-T6SIVUT5.js} +5 -5
  40. package/dist/{viewer-DNMNC5VS.js → viewer-7ZEAFBVN.js} +80 -58
  41. package/dist/viewer-7ZEAFBVN.js.map +1 -0
  42. package/package.json +6 -14
  43. package/src/ai-client.ts +156 -0
  44. package/src/bin.ts +74 -2
  45. package/src/build.ts +95 -33
  46. package/src/commands/__tests__/drift-sync.test.ts +252 -0
  47. package/src/commands/__tests__/scan-generate.test.ts +497 -45
  48. package/src/commands/enhance.ts +11 -35
  49. package/src/commands/init.ts +296 -193
  50. package/src/commands/scan-generate.ts +740 -139
  51. package/src/commands/scan.ts +37 -32
  52. package/src/commands/setup.ts +143 -52
  53. package/src/commands/snapshot.ts +197 -0
  54. package/src/commands/sync.ts +357 -0
  55. package/src/commands/validate.ts +43 -1
  56. package/src/core/component-extractor.test.ts +282 -0
  57. package/src/core/component-extractor.ts +1030 -0
  58. package/src/core/discovery.ts +93 -7
  59. package/src/service/enhance/props-extractor.ts +235 -13
  60. package/src/validators.ts +236 -0
  61. package/src/viewer/__tests__/viewer-integration.test.ts +85 -74
  62. package/src/viewer/server.ts +37 -22
  63. package/src/viewer/vite-plugin.ts +25 -9
  64. package/dist/chunk-5G3VZH43.js.map +0 -1
  65. package/dist/chunk-OQO55NKV.js.map +0 -1
  66. package/dist/chunk-WXSR2II7.js.map +0 -1
  67. package/dist/chunk-ZM4ZQZWZ.js.map +0 -1
  68. package/dist/init-NDQXUWDU.js +0 -796
  69. package/dist/init-NDQXUWDU.js.map +0 -1
  70. package/dist/scan-generate-SJAN5MVI.js +0 -691
  71. package/dist/scan-generate-SJAN5MVI.js.map +0 -1
  72. package/dist/viewer-DNMNC5VS.js.map +0 -1
  73. package/src/ai.ts +0 -266
  74. package/src/commands/init-framework.ts +0 -414
  75. package/src/mcp/bin.ts +0 -36
  76. package/src/migrate/bin.ts +0 -114
  77. package/src/theme/index.ts +0 -77
  78. package/src/viewer/__tests__/a11y-fixes.test.ts +0 -358
  79. package/src/viewer/__tests__/jsx-parser.test.ts +0 -502
  80. package/src/viewer/__tests__/render-utils.test.ts +0 -232
  81. package/src/viewer/__tests__/style-utils.test.ts +0 -404
  82. package/src/viewer/assets/fragments-logo.ts +0 -4
  83. package/src/viewer/assets/fragments_logo.png +0 -0
  84. package/src/viewer/bin.ts +0 -86
  85. package/src/viewer/cli/health.ts +0 -256
  86. package/src/viewer/cli/index.ts +0 -33
  87. package/src/viewer/cli/scan.ts +0 -124
  88. package/src/viewer/cli/utils.ts +0 -174
  89. package/src/viewer/components/AccessibilityPanel.tsx +0 -1457
  90. package/src/viewer/components/ActionCapture.tsx +0 -172
  91. package/src/viewer/components/ActionsPanel.tsx +0 -332
  92. package/src/viewer/components/AllVariantsPreview.tsx +0 -78
  93. package/src/viewer/components/App.tsx +0 -582
  94. package/src/viewer/components/BottomPanel.tsx +0 -288
  95. package/src/viewer/components/CodePanel.naming.test.tsx +0 -59
  96. package/src/viewer/components/CodePanel.tsx +0 -118
  97. package/src/viewer/components/CommandPalette.tsx +0 -392
  98. package/src/viewer/components/ComponentDocView.tsx +0 -164
  99. package/src/viewer/components/ComponentGraph.tsx +0 -380
  100. package/src/viewer/components/ComponentHeader.tsx +0 -88
  101. package/src/viewer/components/ContractPanel.tsx +0 -241
  102. package/src/viewer/components/EmptyVariantMessage.tsx +0 -54
  103. package/src/viewer/components/ErrorBoundary.tsx +0 -97
  104. package/src/viewer/components/FigmaEmbed.tsx +0 -238
  105. package/src/viewer/components/FragmentEditor.tsx +0 -525
  106. package/src/viewer/components/FragmentRenderer.tsx +0 -61
  107. package/src/viewer/components/HeaderSearch.tsx +0 -24
  108. package/src/viewer/components/HealthDashboard.tsx +0 -441
  109. package/src/viewer/components/HmrStatusIndicator.tsx +0 -61
  110. package/src/viewer/components/Icons.tsx +0 -479
  111. package/src/viewer/components/InteractionsPanel.tsx +0 -757
  112. package/src/viewer/components/IsolatedPreviewFrame.tsx +0 -346
  113. package/src/viewer/components/IsolatedRender.tsx +0 -113
  114. package/src/viewer/components/KeyboardShortcutsHelp.tsx +0 -53
  115. package/src/viewer/components/LandingPage.tsx +0 -421
  116. package/src/viewer/components/Layout.tsx +0 -27
  117. package/src/viewer/components/LeftSidebar.tsx +0 -472
  118. package/src/viewer/components/LoadErrorMessage.tsx +0 -102
  119. package/src/viewer/components/MultiViewportPreview.tsx +0 -522
  120. package/src/viewer/components/NoVariantsMessage.tsx +0 -59
  121. package/src/viewer/components/PanelShell.tsx +0 -161
  122. package/src/viewer/components/PerformancePanel.tsx +0 -304
  123. package/src/viewer/components/PreviewArea.tsx +0 -472
  124. package/src/viewer/components/PreviewAside.tsx +0 -168
  125. package/src/viewer/components/PreviewFrameHost.tsx +0 -303
  126. package/src/viewer/components/PreviewPane.tsx +0 -149
  127. package/src/viewer/components/PreviewToolbar.tsx +0 -80
  128. package/src/viewer/components/PropsEditor.tsx +0 -506
  129. package/src/viewer/components/PropsTable.tsx +0 -111
  130. package/src/viewer/components/RelationsSection.tsx +0 -88
  131. package/src/viewer/components/ResizablePanel.tsx +0 -271
  132. package/src/viewer/components/RightSidebar.tsx +0 -102
  133. package/src/viewer/components/RuntimeToolsRegistrar.tsx +0 -17
  134. package/src/viewer/components/ScreenshotButton.tsx +0 -90
  135. package/src/viewer/components/Sidebar.tsx +0 -169
  136. package/src/viewer/components/SkeletonLoader.tsx +0 -161
  137. package/src/viewer/components/ThemeProvider.tsx +0 -42
  138. package/src/viewer/components/Toast.tsx +0 -3
  139. package/src/viewer/components/TokenStylePanel.tsx +0 -699
  140. package/src/viewer/components/TopToolbar.tsx +0 -159
  141. package/src/viewer/components/UsageSection.tsx +0 -95
  142. package/src/viewer/components/VariantMatrix.tsx +0 -388
  143. package/src/viewer/components/VariantRenderer.tsx +0 -131
  144. package/src/viewer/components/VariantTabs.tsx +0 -40
  145. package/src/viewer/components/ViewerHeader.tsx +0 -69
  146. package/src/viewer/components/ViewerStateSync.tsx +0 -52
  147. package/src/viewer/components/ViewportSelector.tsx +0 -172
  148. package/src/viewer/components/WebMCPDevTools.tsx +0 -503
  149. package/src/viewer/components/WebMCPIntegration.tsx +0 -47
  150. package/src/viewer/components/WebMCPStatusIndicator.tsx +0 -60
  151. package/src/viewer/components/_future/CreatePage.tsx +0 -836
  152. package/src/viewer/components/viewer-utils.ts +0 -16
  153. package/src/viewer/composition-renderer.ts +0 -381
  154. package/src/viewer/constants/index.ts +0 -1
  155. package/src/viewer/constants/ui.ts +0 -166
  156. package/src/viewer/entry.tsx +0 -335
  157. package/src/viewer/hooks/index.ts +0 -2
  158. package/src/viewer/hooks/useA11yCache.ts +0 -383
  159. package/src/viewer/hooks/useA11yService.ts +0 -364
  160. package/src/viewer/hooks/useActions.ts +0 -138
  161. package/src/viewer/hooks/useAppState.ts +0 -147
  162. package/src/viewer/hooks/useCompiledFragments.ts +0 -42
  163. package/src/viewer/hooks/useFigmaIntegration.ts +0 -132
  164. package/src/viewer/hooks/useHmrStatus.ts +0 -109
  165. package/src/viewer/hooks/useKeyboardShortcuts.ts +0 -270
  166. package/src/viewer/hooks/usePreviewBridge.ts +0 -347
  167. package/src/viewer/hooks/useScrollSpy.ts +0 -78
  168. package/src/viewer/hooks/useUrlState.ts +0 -318
  169. package/src/viewer/hooks/useViewSettings.ts +0 -111
  170. package/src/viewer/index.html +0 -28
  171. package/src/viewer/intelligence/healthReport.ts +0 -505
  172. package/src/viewer/intelligence/styleDrift.ts +0 -340
  173. package/src/viewer/intelligence/usageScanner.ts +0 -309
  174. package/src/viewer/jsx-parser.ts +0 -486
  175. package/src/viewer/preview-frame-entry.tsx +0 -25
  176. package/src/viewer/preview-frame.html +0 -125
  177. package/src/viewer/public/favicon.ico +0 -0
  178. package/src/viewer/render-template.html +0 -68
  179. package/src/viewer/styles/globals.css +0 -278
  180. package/src/viewer/types/a11y.ts +0 -197
  181. package/src/viewer/utils/a11y-fixes.ts +0 -509
  182. package/src/viewer/utils/actionExport.ts +0 -372
  183. package/src/viewer/utils/colorSchemes.ts +0 -201
  184. package/src/viewer/utils/detectRelationships.ts +0 -256
  185. package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss +0 -10
  186. package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss.d.ts +0 -2
  187. package/src/viewer/vendor/shared/src/ComponentDocContent.tsx +0 -274
  188. package/src/viewer/vendor/shared/src/DocsHeaderBar.tsx +0 -129
  189. package/src/viewer/vendor/shared/src/DocsPageAsideHost.tsx +0 -89
  190. package/src/viewer/vendor/shared/src/DocsPageShell.tsx +0 -124
  191. package/src/viewer/vendor/shared/src/DocsSearchCommand.tsx +0 -99
  192. package/src/viewer/vendor/shared/src/DocsSidebarNav.tsx +0 -66
  193. package/src/viewer/vendor/shared/src/PropsTable.module.scss +0 -68
  194. package/src/viewer/vendor/shared/src/PropsTable.module.scss.d.ts +0 -2
  195. package/src/viewer/vendor/shared/src/PropsTable.tsx +0 -76
  196. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss +0 -114
  197. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss.d.ts +0 -2
  198. package/src/viewer/vendor/shared/src/VariantPreviewCard.tsx +0 -137
  199. package/src/viewer/vendor/shared/src/docs-data/index.ts +0 -32
  200. package/src/viewer/vendor/shared/src/docs-data/mcp-configs.ts +0 -72
  201. package/src/viewer/vendor/shared/src/docs-data/palettes.ts +0 -75
  202. package/src/viewer/vendor/shared/src/docs-data/setup-examples.ts +0 -55
  203. package/src/viewer/vendor/shared/src/docs-layout.scss +0 -28
  204. package/src/viewer/vendor/shared/src/docs-layout.scss.d.ts +0 -2
  205. package/src/viewer/vendor/shared/src/index.ts +0 -34
  206. package/src/viewer/vendor/shared/src/types.ts +0 -53
  207. package/src/viewer/webmcp/__tests__/analytics.test.ts +0 -108
  208. package/src/viewer/webmcp/analytics.ts +0 -165
  209. package/src/viewer/webmcp/index.ts +0 -3
  210. package/src/viewer/webmcp/posthog-bridge.ts +0 -39
  211. package/src/viewer/webmcp/runtime-tools.ts +0 -152
  212. package/src/viewer/webmcp/scan-utils.ts +0 -135
  213. package/src/viewer/webmcp/use-tool-analytics.ts +0 -69
  214. package/src/viewer/webmcp/viewer-state.ts +0 -45
  215. /package/dist/{discovery-NEOY4MPN.js.map → ai-client-I6MDWNYA.js.map} +0 -0
  216. /package/dist/{chunk-PW7QTQA6.js.map → chunk-4OC7FTJB.js.map} +0 -0
  217. /package/dist/{chunk-HRFUSSZI.js.map → chunk-AM4MRTMN.js.map} +0 -0
  218. /package/dist/{scan-CJF2DOQW.js.map → discovery-ZJQSXF56.js.map} +0 -0
  219. /package/dist/{generate-FBHSXR3D.js.map → generate-RJFS2JWA.js.map} +0 -0
  220. /package/dist/{service-TQYWY65E.js.map → scan-3PMCJ4RB.js.map} +0 -0
  221. /package/dist/{static-viewer-NUBFPKWH.js.map → service-VMGNJZ42.js.map} +0 -0
  222. /package/dist/{test-Z5LVO724.js.map → test-SI4NSHQX.js.map} +0 -0
  223. /package/dist/{tokens-CE46OTMD.js.map → tokens-T6SIVUT5.js.map} +0 -0
@@ -1,340 +0,0 @@
1
- /**
2
- * Style Drift Detection
3
- * Analyzes style drift between Figma designs and rendered components
4
- */
5
-
6
- import type { FragmentDefinition, 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
- }
@@ -1,309 +0,0 @@
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('.fragment.')) {
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
- }