@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.
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +4783 -0
- package/dist/bin.js.map +1 -0
- package/dist/chunk-4FDQSGKX.js +786 -0
- package/dist/chunk-4FDQSGKX.js.map +1 -0
- package/dist/chunk-7H2MMGYG.js +369 -0
- package/dist/chunk-7H2MMGYG.js.map +1 -0
- package/dist/chunk-BSCG3IP7.js +619 -0
- package/dist/chunk-BSCG3IP7.js.map +1 -0
- package/dist/chunk-LY2CFFPY.js +898 -0
- package/dist/chunk-LY2CFFPY.js.map +1 -0
- package/dist/chunk-MUZ6CM66.js +6636 -0
- package/dist/chunk-MUZ6CM66.js.map +1 -0
- package/dist/chunk-OAENNG3G.js +1489 -0
- package/dist/chunk-OAENNG3G.js.map +1 -0
- package/dist/chunk-XHNKNI6J.js +235 -0
- package/dist/chunk-XHNKNI6J.js.map +1 -0
- package/dist/core-DWKLGY4N.js +68 -0
- package/dist/core-DWKLGY4N.js.map +1 -0
- package/dist/generate-4LQNJ7SX.js +249 -0
- package/dist/generate-4LQNJ7SX.js.map +1 -0
- package/dist/index.d.ts +775 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/init-EMVI47QG.js +416 -0
- package/dist/init-EMVI47QG.js.map +1 -0
- package/dist/mcp-bin.d.ts +1 -0
- package/dist/mcp-bin.js +1117 -0
- package/dist/mcp-bin.js.map +1 -0
- package/dist/scan-4YPRF7FV.js +12 -0
- package/dist/scan-4YPRF7FV.js.map +1 -0
- package/dist/service-QSZMZJBJ.js +208 -0
- package/dist/service-QSZMZJBJ.js.map +1 -0
- package/dist/static-viewer-MIPGZ4Z7.js +12 -0
- package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
- package/dist/test-SQ5ZHXWU.js +1067 -0
- package/dist/test-SQ5ZHXWU.js.map +1 -0
- package/dist/tokens-HSGMYK64.js +173 -0
- package/dist/tokens-HSGMYK64.js.map +1 -0
- package/dist/viewer-YRF4SQE4.js +11101 -0
- package/dist/viewer-YRF4SQE4.js.map +1 -0
- package/package.json +107 -0
- package/src/ai.ts +266 -0
- package/src/analyze.ts +265 -0
- package/src/bin.ts +916 -0
- package/src/build.ts +248 -0
- package/src/commands/a11y.ts +302 -0
- package/src/commands/add.ts +313 -0
- package/src/commands/audit.ts +195 -0
- package/src/commands/baseline.ts +221 -0
- package/src/commands/build.ts +144 -0
- package/src/commands/compare.ts +337 -0
- package/src/commands/context.ts +107 -0
- package/src/commands/dev.ts +107 -0
- package/src/commands/enhance.ts +858 -0
- package/src/commands/generate.ts +391 -0
- package/src/commands/init.ts +531 -0
- package/src/commands/link/figma.ts +645 -0
- package/src/commands/link/index.ts +10 -0
- package/src/commands/link/storybook.ts +267 -0
- package/src/commands/list.ts +49 -0
- package/src/commands/metrics.ts +114 -0
- package/src/commands/reset.ts +242 -0
- package/src/commands/scan.ts +537 -0
- package/src/commands/storygen.ts +207 -0
- package/src/commands/tokens.ts +251 -0
- package/src/commands/validate.ts +93 -0
- package/src/commands/verify.ts +215 -0
- package/src/core/composition.test.ts +262 -0
- package/src/core/composition.ts +255 -0
- package/src/core/config.ts +84 -0
- package/src/core/constants.ts +111 -0
- package/src/core/context.ts +380 -0
- package/src/core/defineSegment.ts +137 -0
- package/src/core/discovery.ts +337 -0
- package/src/core/figma.ts +263 -0
- package/src/core/fragment-types.ts +214 -0
- package/src/core/generators/context.ts +389 -0
- package/src/core/generators/index.ts +23 -0
- package/src/core/generators/registry.ts +364 -0
- package/src/core/generators/typescript-extractor.ts +374 -0
- package/src/core/importAnalyzer.ts +217 -0
- package/src/core/index.ts +149 -0
- package/src/core/loader.ts +155 -0
- package/src/core/node.ts +63 -0
- package/src/core/parser.ts +551 -0
- package/src/core/previewLoader.ts +172 -0
- package/src/core/schema/fragment.schema.json +189 -0
- package/src/core/schema/registry.schema.json +137 -0
- package/src/core/schema.ts +182 -0
- package/src/core/storyAdapter.test.ts +571 -0
- package/src/core/storyAdapter.ts +761 -0
- package/src/core/token-types.ts +287 -0
- package/src/core/types.ts +754 -0
- package/src/diff.ts +323 -0
- package/src/index.ts +43 -0
- package/src/mcp/__tests__/projectFields.test.ts +130 -0
- package/src/mcp/bin.ts +36 -0
- package/src/mcp/index.ts +8 -0
- package/src/mcp/server.ts +1310 -0
- package/src/mcp/utils.ts +54 -0
- package/src/mcp-bin.ts +36 -0
- package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
- package/src/migrate/__tests__/args/args.test.ts +452 -0
- package/src/migrate/__tests__/meta/meta.test.ts +198 -0
- package/src/migrate/__tests__/stories/stories.test.ts +278 -0
- package/src/migrate/__tests__/utils/utils.test.ts +371 -0
- package/src/migrate/__tests__/values/values.test.ts +303 -0
- package/src/migrate/bin.ts +108 -0
- package/src/migrate/converter.ts +658 -0
- package/src/migrate/detect.ts +196 -0
- package/src/migrate/index.ts +45 -0
- package/src/migrate/migrate.ts +163 -0
- package/src/migrate/parser.ts +1136 -0
- package/src/migrate/report.ts +624 -0
- package/src/migrate/types.ts +169 -0
- package/src/screenshot.ts +249 -0
- package/src/service/__tests__/ast-utils.test.ts +426 -0
- package/src/service/__tests__/enhance-scanner.test.ts +200 -0
- package/src/service/__tests__/figma/figma.test.ts +652 -0
- package/src/service/__tests__/metrics-store.test.ts +409 -0
- package/src/service/__tests__/patch-generator.test.ts +186 -0
- package/src/service/__tests__/props-extractor.test.ts +365 -0
- package/src/service/__tests__/token-registry.test.ts +267 -0
- package/src/service/analytics.ts +659 -0
- package/src/service/ast-utils.ts +444 -0
- package/src/service/browser-pool.ts +339 -0
- package/src/service/capture.ts +267 -0
- package/src/service/diff.ts +279 -0
- package/src/service/enhance/aggregator.ts +489 -0
- package/src/service/enhance/cache.ts +275 -0
- package/src/service/enhance/codebase-scanner.ts +357 -0
- package/src/service/enhance/context-generator.ts +529 -0
- package/src/service/enhance/doc-extractor.ts +523 -0
- package/src/service/enhance/index.ts +131 -0
- package/src/service/enhance/props-extractor.ts +665 -0
- package/src/service/enhance/scanner.ts +445 -0
- package/src/service/enhance/storybook-parser.ts +552 -0
- package/src/service/enhance/types.ts +346 -0
- package/src/service/enhance/variant-renderer.ts +479 -0
- package/src/service/figma.ts +1008 -0
- package/src/service/index.ts +249 -0
- package/src/service/metrics-store.ts +333 -0
- package/src/service/patch-generator.ts +349 -0
- package/src/service/report.ts +854 -0
- package/src/service/storage.ts +401 -0
- package/src/service/token-fixes.ts +281 -0
- package/src/service/token-parser.ts +504 -0
- package/src/service/token-registry.ts +721 -0
- package/src/service/utils.ts +172 -0
- package/src/setup.ts +241 -0
- package/src/shared/command-wrapper.ts +81 -0
- package/src/shared/dev-server-client.ts +199 -0
- package/src/shared/index.ts +8 -0
- package/src/shared/segment-loader.ts +59 -0
- package/src/shared/types.ts +147 -0
- package/src/static-viewer.ts +715 -0
- package/src/test/discovery.ts +172 -0
- package/src/test/index.ts +281 -0
- package/src/test/reporters/console.ts +194 -0
- package/src/test/reporters/json.ts +190 -0
- package/src/test/reporters/junit.ts +186 -0
- package/src/test/runner.ts +598 -0
- package/src/test/types.ts +245 -0
- package/src/test/watch.ts +200 -0
- package/src/validators.ts +152 -0
- package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
- package/src/viewer/__tests__/render-utils.test.ts +232 -0
- package/src/viewer/__tests__/style-utils.test.ts +404 -0
- package/src/viewer/bin.ts +86 -0
- package/src/viewer/cli/health.ts +256 -0
- package/src/viewer/cli/index.ts +33 -0
- package/src/viewer/cli/scan.ts +124 -0
- package/src/viewer/cli/utils.ts +174 -0
- package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
- package/src/viewer/components/ActionCapture.tsx +172 -0
- package/src/viewer/components/ActionsPanel.tsx +371 -0
- package/src/viewer/components/App.tsx +638 -0
- package/src/viewer/components/BottomPanel.tsx +224 -0
- package/src/viewer/components/CodePanel.tsx +589 -0
- package/src/viewer/components/CommandPalette.tsx +336 -0
- package/src/viewer/components/ComponentGraph.tsx +394 -0
- package/src/viewer/components/ComponentHeader.tsx +85 -0
- package/src/viewer/components/ContractPanel.tsx +234 -0
- package/src/viewer/components/ErrorBoundary.tsx +85 -0
- package/src/viewer/components/FigmaEmbed.tsx +231 -0
- package/src/viewer/components/FragmentEditor.tsx +485 -0
- package/src/viewer/components/HealthDashboard.tsx +452 -0
- package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
- package/src/viewer/components/Icons.tsx +417 -0
- package/src/viewer/components/InteractionsPanel.tsx +720 -0
- package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
- package/src/viewer/components/IsolatedRender.tsx +111 -0
- package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
- package/src/viewer/components/LandingPage.tsx +441 -0
- package/src/viewer/components/Layout.tsx +22 -0
- package/src/viewer/components/LeftSidebar.tsx +391 -0
- package/src/viewer/components/MultiViewportPreview.tsx +429 -0
- package/src/viewer/components/PreviewArea.tsx +404 -0
- package/src/viewer/components/PreviewFrameHost.tsx +310 -0
- package/src/viewer/components/PreviewPane.tsx +150 -0
- package/src/viewer/components/PreviewToolbar.tsx +176 -0
- package/src/viewer/components/PropsEditor.tsx +512 -0
- package/src/viewer/components/PropsTable.tsx +98 -0
- package/src/viewer/components/RelationsSection.tsx +57 -0
- package/src/viewer/components/ResizablePanel.tsx +328 -0
- package/src/viewer/components/RightSidebar.tsx +118 -0
- package/src/viewer/components/ScreenshotButton.tsx +90 -0
- package/src/viewer/components/Sidebar.tsx +169 -0
- package/src/viewer/components/SkeletonLoader.tsx +156 -0
- package/src/viewer/components/StoryRenderer.tsx +128 -0
- package/src/viewer/components/ThemeProvider.tsx +96 -0
- package/src/viewer/components/Toast.tsx +67 -0
- package/src/viewer/components/TokenStylePanel.tsx +708 -0
- package/src/viewer/components/UsageSection.tsx +95 -0
- package/src/viewer/components/VariantMatrix.tsx +350 -0
- package/src/viewer/components/VariantRenderer.tsx +131 -0
- package/src/viewer/components/VariantTabs.tsx +84 -0
- package/src/viewer/components/ViewportSelector.tsx +165 -0
- package/src/viewer/components/_future/CreatePage.tsx +836 -0
- package/src/viewer/composition-renderer.ts +381 -0
- package/src/viewer/constants/index.ts +1 -0
- package/src/viewer/constants/ui.ts +185 -0
- package/src/viewer/entry.tsx +299 -0
- package/src/viewer/hooks/index.ts +2 -0
- package/src/viewer/hooks/useA11yCache.ts +383 -0
- package/src/viewer/hooks/useA11yService.ts +498 -0
- package/src/viewer/hooks/useActions.ts +138 -0
- package/src/viewer/hooks/useAppState.ts +124 -0
- package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
- package/src/viewer/hooks/useHmrStatus.ts +109 -0
- package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
- package/src/viewer/hooks/usePreviewBridge.ts +347 -0
- package/src/viewer/hooks/useScrollSpy.ts +78 -0
- package/src/viewer/hooks/useUrlState.ts +330 -0
- package/src/viewer/hooks/useViewSettings.ts +125 -0
- package/src/viewer/index.html +28 -0
- package/src/viewer/index.ts +14 -0
- package/src/viewer/intelligence/healthReport.ts +505 -0
- package/src/viewer/intelligence/styleDrift.ts +340 -0
- package/src/viewer/intelligence/usageScanner.ts +309 -0
- package/src/viewer/jsx-parser.ts +485 -0
- package/src/viewer/postcss.config.js +6 -0
- package/src/viewer/preview-frame-entry.tsx +25 -0
- package/src/viewer/preview-frame.html +109 -0
- package/src/viewer/render-template.html +68 -0
- package/src/viewer/render-utils.ts +170 -0
- package/src/viewer/server.ts +276 -0
- package/src/viewer/style-utils.ts +414 -0
- package/src/viewer/styles/globals.css +355 -0
- package/src/viewer/tailwind.config.js +37 -0
- package/src/viewer/types/a11y.ts +197 -0
- package/src/viewer/utils/a11y-fixes.ts +471 -0
- package/src/viewer/utils/actionExport.ts +372 -0
- package/src/viewer/utils/colorSchemes.ts +201 -0
- package/src/viewer/utils/detectRelationships.ts +256 -0
- 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
|
+
}
|