@fragments-sdk/cli 0.10.1 → 0.11.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 (149) hide show
  1. package/dist/bin.js +20 -2
  2. package/dist/bin.js.map +1 -1
  3. package/dist/{init-NDQXUWDU.js → init-UFGK5TCN.js} +75 -4
  4. package/dist/init-UFGK5TCN.js.map +1 -0
  5. package/dist/snapshot-SV2JOFZH.js +139 -0
  6. package/dist/snapshot-SV2JOFZH.js.map +1 -0
  7. package/dist/{viewer-DNMNC5VS.js → viewer-DLLJIMCK.js} +68 -46
  8. package/dist/viewer-DLLJIMCK.js.map +1 -0
  9. package/package.json +6 -14
  10. package/src/bin.ts +30 -0
  11. package/src/commands/init.ts +76 -1
  12. package/src/commands/snapshot.ts +197 -0
  13. package/src/viewer/__tests__/viewer-integration.test.ts +85 -74
  14. package/src/viewer/server.ts +37 -22
  15. package/src/viewer/vite-plugin.ts +25 -9
  16. package/dist/init-NDQXUWDU.js.map +0 -1
  17. package/dist/viewer-DNMNC5VS.js.map +0 -1
  18. package/src/viewer/__tests__/a11y-fixes.test.ts +0 -358
  19. package/src/viewer/__tests__/jsx-parser.test.ts +0 -502
  20. package/src/viewer/__tests__/render-utils.test.ts +0 -232
  21. package/src/viewer/__tests__/style-utils.test.ts +0 -404
  22. package/src/viewer/assets/fragments-logo.ts +0 -4
  23. package/src/viewer/assets/fragments_logo.png +0 -0
  24. package/src/viewer/components/AccessibilityPanel.tsx +0 -1457
  25. package/src/viewer/components/ActionCapture.tsx +0 -172
  26. package/src/viewer/components/ActionsPanel.tsx +0 -332
  27. package/src/viewer/components/AllVariantsPreview.tsx +0 -78
  28. package/src/viewer/components/App.tsx +0 -582
  29. package/src/viewer/components/BottomPanel.tsx +0 -288
  30. package/src/viewer/components/CodePanel.naming.test.tsx +0 -59
  31. package/src/viewer/components/CodePanel.tsx +0 -118
  32. package/src/viewer/components/CommandPalette.tsx +0 -392
  33. package/src/viewer/components/ComponentDocView.tsx +0 -164
  34. package/src/viewer/components/ComponentGraph.tsx +0 -380
  35. package/src/viewer/components/ComponentHeader.tsx +0 -88
  36. package/src/viewer/components/ContractPanel.tsx +0 -241
  37. package/src/viewer/components/EmptyVariantMessage.tsx +0 -54
  38. package/src/viewer/components/ErrorBoundary.tsx +0 -97
  39. package/src/viewer/components/FigmaEmbed.tsx +0 -238
  40. package/src/viewer/components/FragmentEditor.tsx +0 -525
  41. package/src/viewer/components/FragmentRenderer.tsx +0 -61
  42. package/src/viewer/components/HeaderSearch.tsx +0 -24
  43. package/src/viewer/components/HealthDashboard.tsx +0 -441
  44. package/src/viewer/components/HmrStatusIndicator.tsx +0 -61
  45. package/src/viewer/components/Icons.tsx +0 -479
  46. package/src/viewer/components/InteractionsPanel.tsx +0 -757
  47. package/src/viewer/components/IsolatedPreviewFrame.tsx +0 -346
  48. package/src/viewer/components/IsolatedRender.tsx +0 -113
  49. package/src/viewer/components/KeyboardShortcutsHelp.tsx +0 -53
  50. package/src/viewer/components/LandingPage.tsx +0 -421
  51. package/src/viewer/components/Layout.tsx +0 -27
  52. package/src/viewer/components/LeftSidebar.tsx +0 -472
  53. package/src/viewer/components/LoadErrorMessage.tsx +0 -102
  54. package/src/viewer/components/MultiViewportPreview.tsx +0 -522
  55. package/src/viewer/components/NoVariantsMessage.tsx +0 -59
  56. package/src/viewer/components/PanelShell.tsx +0 -161
  57. package/src/viewer/components/PerformancePanel.tsx +0 -304
  58. package/src/viewer/components/PreviewArea.tsx +0 -472
  59. package/src/viewer/components/PreviewAside.tsx +0 -168
  60. package/src/viewer/components/PreviewFrameHost.tsx +0 -303
  61. package/src/viewer/components/PreviewPane.tsx +0 -149
  62. package/src/viewer/components/PreviewToolbar.tsx +0 -80
  63. package/src/viewer/components/PropsEditor.tsx +0 -506
  64. package/src/viewer/components/PropsTable.tsx +0 -111
  65. package/src/viewer/components/RelationsSection.tsx +0 -88
  66. package/src/viewer/components/ResizablePanel.tsx +0 -271
  67. package/src/viewer/components/RightSidebar.tsx +0 -102
  68. package/src/viewer/components/RuntimeToolsRegistrar.tsx +0 -17
  69. package/src/viewer/components/ScreenshotButton.tsx +0 -90
  70. package/src/viewer/components/Sidebar.tsx +0 -169
  71. package/src/viewer/components/SkeletonLoader.tsx +0 -161
  72. package/src/viewer/components/ThemeProvider.tsx +0 -42
  73. package/src/viewer/components/Toast.tsx +0 -3
  74. package/src/viewer/components/TokenStylePanel.tsx +0 -699
  75. package/src/viewer/components/TopToolbar.tsx +0 -159
  76. package/src/viewer/components/UsageSection.tsx +0 -95
  77. package/src/viewer/components/VariantMatrix.tsx +0 -388
  78. package/src/viewer/components/VariantRenderer.tsx +0 -131
  79. package/src/viewer/components/VariantTabs.tsx +0 -40
  80. package/src/viewer/components/ViewerHeader.tsx +0 -69
  81. package/src/viewer/components/ViewerStateSync.tsx +0 -52
  82. package/src/viewer/components/ViewportSelector.tsx +0 -172
  83. package/src/viewer/components/WebMCPDevTools.tsx +0 -503
  84. package/src/viewer/components/WebMCPIntegration.tsx +0 -47
  85. package/src/viewer/components/WebMCPStatusIndicator.tsx +0 -60
  86. package/src/viewer/components/_future/CreatePage.tsx +0 -836
  87. package/src/viewer/components/viewer-utils.ts +0 -16
  88. package/src/viewer/composition-renderer.ts +0 -381
  89. package/src/viewer/constants/index.ts +0 -1
  90. package/src/viewer/constants/ui.ts +0 -166
  91. package/src/viewer/entry.tsx +0 -335
  92. package/src/viewer/hooks/index.ts +0 -2
  93. package/src/viewer/hooks/useA11yCache.ts +0 -383
  94. package/src/viewer/hooks/useA11yService.ts +0 -364
  95. package/src/viewer/hooks/useActions.ts +0 -138
  96. package/src/viewer/hooks/useAppState.ts +0 -147
  97. package/src/viewer/hooks/useCompiledFragments.ts +0 -42
  98. package/src/viewer/hooks/useFigmaIntegration.ts +0 -132
  99. package/src/viewer/hooks/useHmrStatus.ts +0 -109
  100. package/src/viewer/hooks/useKeyboardShortcuts.ts +0 -270
  101. package/src/viewer/hooks/usePreviewBridge.ts +0 -347
  102. package/src/viewer/hooks/useScrollSpy.ts +0 -78
  103. package/src/viewer/hooks/useUrlState.ts +0 -318
  104. package/src/viewer/hooks/useViewSettings.ts +0 -111
  105. package/src/viewer/index.html +0 -28
  106. package/src/viewer/intelligence/healthReport.ts +0 -505
  107. package/src/viewer/intelligence/styleDrift.ts +0 -340
  108. package/src/viewer/intelligence/usageScanner.ts +0 -309
  109. package/src/viewer/jsx-parser.ts +0 -486
  110. package/src/viewer/preview-frame-entry.tsx +0 -25
  111. package/src/viewer/preview-frame.html +0 -125
  112. package/src/viewer/public/favicon.ico +0 -0
  113. package/src/viewer/render-template.html +0 -68
  114. package/src/viewer/styles/globals.css +0 -278
  115. package/src/viewer/types/a11y.ts +0 -197
  116. package/src/viewer/utils/a11y-fixes.ts +0 -509
  117. package/src/viewer/utils/actionExport.ts +0 -372
  118. package/src/viewer/utils/colorSchemes.ts +0 -201
  119. package/src/viewer/utils/detectRelationships.ts +0 -256
  120. package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss +0 -10
  121. package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss.d.ts +0 -2
  122. package/src/viewer/vendor/shared/src/ComponentDocContent.tsx +0 -274
  123. package/src/viewer/vendor/shared/src/DocsHeaderBar.tsx +0 -129
  124. package/src/viewer/vendor/shared/src/DocsPageAsideHost.tsx +0 -89
  125. package/src/viewer/vendor/shared/src/DocsPageShell.tsx +0 -124
  126. package/src/viewer/vendor/shared/src/DocsSearchCommand.tsx +0 -99
  127. package/src/viewer/vendor/shared/src/DocsSidebarNav.tsx +0 -66
  128. package/src/viewer/vendor/shared/src/PropsTable.module.scss +0 -68
  129. package/src/viewer/vendor/shared/src/PropsTable.module.scss.d.ts +0 -2
  130. package/src/viewer/vendor/shared/src/PropsTable.tsx +0 -76
  131. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss +0 -114
  132. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss.d.ts +0 -2
  133. package/src/viewer/vendor/shared/src/VariantPreviewCard.tsx +0 -137
  134. package/src/viewer/vendor/shared/src/docs-data/index.ts +0 -32
  135. package/src/viewer/vendor/shared/src/docs-data/mcp-configs.ts +0 -72
  136. package/src/viewer/vendor/shared/src/docs-data/palettes.ts +0 -75
  137. package/src/viewer/vendor/shared/src/docs-data/setup-examples.ts +0 -55
  138. package/src/viewer/vendor/shared/src/docs-layout.scss +0 -28
  139. package/src/viewer/vendor/shared/src/docs-layout.scss.d.ts +0 -2
  140. package/src/viewer/vendor/shared/src/index.ts +0 -34
  141. package/src/viewer/vendor/shared/src/types.ts +0 -53
  142. package/src/viewer/webmcp/__tests__/analytics.test.ts +0 -108
  143. package/src/viewer/webmcp/analytics.ts +0 -165
  144. package/src/viewer/webmcp/index.ts +0 -3
  145. package/src/viewer/webmcp/posthog-bridge.ts +0 -39
  146. package/src/viewer/webmcp/runtime-tools.ts +0 -152
  147. package/src/viewer/webmcp/scan-utils.ts +0 -135
  148. package/src/viewer/webmcp/use-tool-analytics.ts +0 -69
  149. package/src/viewer/webmcp/viewer-state.ts +0 -45
@@ -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
- }