@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,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fragments verify - Verify component compliance for CI pipelines
|
|
3
|
+
*
|
|
4
|
+
* Uses the /segments/compliance endpoint to get real token compliance data
|
|
5
|
+
* based on computed styles compared against the design token registry.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import pc from 'picocolors';
|
|
9
|
+
import { BRAND } from '../core/index.js';
|
|
10
|
+
import { loadConfig } from '../core/node.js';
|
|
11
|
+
import {
|
|
12
|
+
createDevServerClient,
|
|
13
|
+
DevServerConnectionError,
|
|
14
|
+
type ComplianceResult,
|
|
15
|
+
type ViolationItem,
|
|
16
|
+
type SegmentInfo,
|
|
17
|
+
} from '../shared/index.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Options for verify command
|
|
21
|
+
*/
|
|
22
|
+
export interface VerifyOptions {
|
|
23
|
+
/** Path to config file */
|
|
24
|
+
config?: string;
|
|
25
|
+
/** CI mode - output JSON and exit non-zero on failure */
|
|
26
|
+
ci?: boolean;
|
|
27
|
+
/** Minimum compliance percentage (default: 80) */
|
|
28
|
+
minCompliance?: number;
|
|
29
|
+
/** Dev server port */
|
|
30
|
+
port?: number | string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Result item from verification
|
|
35
|
+
*/
|
|
36
|
+
export interface VerifyResultItem {
|
|
37
|
+
component: string;
|
|
38
|
+
compliance: number;
|
|
39
|
+
passed: boolean;
|
|
40
|
+
violations: ViolationItem[];
|
|
41
|
+
totalProperties: number;
|
|
42
|
+
hardcoded: number;
|
|
43
|
+
usingTokens: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Summary of verification results
|
|
48
|
+
*/
|
|
49
|
+
export interface VerifySummary {
|
|
50
|
+
passed: boolean;
|
|
51
|
+
compliance: number;
|
|
52
|
+
threshold: number;
|
|
53
|
+
totalComponents: number;
|
|
54
|
+
passedComponents: number;
|
|
55
|
+
failedComponents: number;
|
|
56
|
+
results: VerifyResultItem[];
|
|
57
|
+
violations: Array<{
|
|
58
|
+
component: string;
|
|
59
|
+
property: string;
|
|
60
|
+
issue: string;
|
|
61
|
+
severity: 'error' | 'warning';
|
|
62
|
+
suggestion?: string;
|
|
63
|
+
}>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Run the verify command
|
|
68
|
+
*/
|
|
69
|
+
export async function verify(
|
|
70
|
+
component: string | undefined,
|
|
71
|
+
options: VerifyOptions = {}
|
|
72
|
+
): Promise<VerifySummary> {
|
|
73
|
+
const { config: configPath, ci = false, port = 6006 } = options;
|
|
74
|
+
|
|
75
|
+
// Load config to get threshold
|
|
76
|
+
const { config } = await loadConfig(configPath);
|
|
77
|
+
const minCompliance = options.minCompliance ?? config.ci?.minCompliance ?? 80;
|
|
78
|
+
|
|
79
|
+
const client = createDevServerClient(port);
|
|
80
|
+
const results: VerifyResultItem[] = [];
|
|
81
|
+
let totalCompliance = 0;
|
|
82
|
+
let componentCount = 0;
|
|
83
|
+
|
|
84
|
+
if (!ci) {
|
|
85
|
+
console.log(pc.cyan(`\n${BRAND.name} Compliance Verification\n`));
|
|
86
|
+
console.log(pc.dim(`Minimum compliance: ${minCompliance}%\n`));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Check if dev server is reachable
|
|
90
|
+
const isReachable = await client.ping();
|
|
91
|
+
if (!isReachable) {
|
|
92
|
+
throw new DevServerConnectionError(
|
|
93
|
+
`Cannot connect to dev server at http://localhost:${port}`,
|
|
94
|
+
`http://localhost:${port}`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Fetch all segments
|
|
99
|
+
let segments = await client.getSegments();
|
|
100
|
+
|
|
101
|
+
// Filter by component if specified
|
|
102
|
+
if (component) {
|
|
103
|
+
segments = segments.filter(
|
|
104
|
+
s => s.name.toLowerCase() === component.toLowerCase()
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
if (segments.length === 0) {
|
|
108
|
+
const error = { error: `Component "${component}" not found` };
|
|
109
|
+
if (ci) {
|
|
110
|
+
console.log(JSON.stringify(error));
|
|
111
|
+
} else {
|
|
112
|
+
console.log(pc.red(error.error));
|
|
113
|
+
}
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Check compliance for each segment
|
|
119
|
+
for (const seg of segments) {
|
|
120
|
+
try {
|
|
121
|
+
// Get real compliance from the server
|
|
122
|
+
const complianceResult = await client.getCompliance({
|
|
123
|
+
component: seg.name,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const passed = complianceResult.compliance >= minCompliance;
|
|
127
|
+
|
|
128
|
+
results.push({
|
|
129
|
+
component: seg.name,
|
|
130
|
+
compliance: complianceResult.compliance,
|
|
131
|
+
passed,
|
|
132
|
+
violations: complianceResult.violations,
|
|
133
|
+
totalProperties: complianceResult.totalProperties,
|
|
134
|
+
hardcoded: complianceResult.hardcoded,
|
|
135
|
+
usingTokens: complianceResult.usingTokens,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
totalCompliance += complianceResult.compliance;
|
|
139
|
+
componentCount++;
|
|
140
|
+
|
|
141
|
+
if (!ci) {
|
|
142
|
+
const icon = passed ? pc.green('✓') : pc.red('✗');
|
|
143
|
+
const complianceStr = passed
|
|
144
|
+
? pc.green(`${complianceResult.compliance}%`)
|
|
145
|
+
: pc.red(`${complianceResult.compliance}%`);
|
|
146
|
+
console.log(` ${icon} ${seg.name} ${complianceStr}`);
|
|
147
|
+
|
|
148
|
+
// Show violations in interactive mode
|
|
149
|
+
if (!passed && complianceResult.violations.length > 0) {
|
|
150
|
+
const violationsToShow = complianceResult.violations.slice(0, 3);
|
|
151
|
+
for (const v of violationsToShow) {
|
|
152
|
+
console.log(pc.dim(` - ${v.property}: ${v.issue}`));
|
|
153
|
+
if (v.suggestion) {
|
|
154
|
+
console.log(pc.dim(` ${pc.cyan('→')} ${v.suggestion}`));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (complianceResult.violations.length > 3) {
|
|
158
|
+
console.log(pc.dim(` ... and ${complianceResult.violations.length - 3} more`));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} catch (error) {
|
|
163
|
+
// Handle individual component errors
|
|
164
|
+
results.push({
|
|
165
|
+
component: seg.name,
|
|
166
|
+
compliance: 0,
|
|
167
|
+
passed: false,
|
|
168
|
+
violations: [{
|
|
169
|
+
property: 'unknown',
|
|
170
|
+
issue: error instanceof Error ? error.message : 'Unknown error',
|
|
171
|
+
severity: 'error',
|
|
172
|
+
}],
|
|
173
|
+
totalProperties: 0,
|
|
174
|
+
hardcoded: 0,
|
|
175
|
+
usingTokens: 0,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
if (!ci) {
|
|
179
|
+
console.log(` ${pc.red('✗')} ${seg.name} ${pc.red('error')}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const averageCompliance = componentCount > 0 ? totalCompliance / componentCount : 100;
|
|
185
|
+
const allPassed = results.every(r => r.passed);
|
|
186
|
+
|
|
187
|
+
const summary: VerifySummary = {
|
|
188
|
+
passed: allPassed,
|
|
189
|
+
compliance: Math.round(averageCompliance * 100) / 100,
|
|
190
|
+
threshold: minCompliance,
|
|
191
|
+
totalComponents: componentCount,
|
|
192
|
+
passedComponents: results.filter(r => r.passed).length,
|
|
193
|
+
failedComponents: results.filter(r => !r.passed).length,
|
|
194
|
+
results,
|
|
195
|
+
violations: results.flatMap(r => r.violations.map(v => ({
|
|
196
|
+
component: r.component,
|
|
197
|
+
...v,
|
|
198
|
+
}))),
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
if (ci) {
|
|
202
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
203
|
+
} else {
|
|
204
|
+
console.log();
|
|
205
|
+
if (allPassed) {
|
|
206
|
+
console.log(pc.green(`✓ All ${componentCount} component(s) meet ${minCompliance}% compliance threshold`));
|
|
207
|
+
} else {
|
|
208
|
+
const failedCount = results.filter(r => !r.passed).length;
|
|
209
|
+
console.log(pc.red(`✗ ${failedCount} component(s) below ${minCompliance}% compliance threshold`));
|
|
210
|
+
}
|
|
211
|
+
console.log(pc.dim(` Average compliance: ${summary.compliance}%\n`));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return summary;
|
|
215
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { analyzeComposition } from "./composition.js";
|
|
3
|
+
import type { CompiledSegment } from "./types.js";
|
|
4
|
+
|
|
5
|
+
function makeSegment(overrides: Partial<CompiledSegment> & { meta: CompiledSegment["meta"] }): CompiledSegment {
|
|
6
|
+
return {
|
|
7
|
+
filePath: "src/components/Test.tsx",
|
|
8
|
+
usage: { when: [], whenNot: [] },
|
|
9
|
+
props: {},
|
|
10
|
+
variants: [],
|
|
11
|
+
...overrides,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const baseSegments: Record<string, CompiledSegment> = {
|
|
16
|
+
Button: makeSegment({
|
|
17
|
+
meta: { name: "Button", description: "A button", category: "actions", status: "stable" },
|
|
18
|
+
relations: [
|
|
19
|
+
{ component: "IconButton", relationship: "alternative", note: "Use for icon-only actions" },
|
|
20
|
+
{ component: "ButtonGroup", relationship: "composition", note: "Wrap multiple buttons" },
|
|
21
|
+
],
|
|
22
|
+
usage: {
|
|
23
|
+
when: ["User needs to trigger an action"],
|
|
24
|
+
whenNot: ["Use Link for navigation"],
|
|
25
|
+
},
|
|
26
|
+
}),
|
|
27
|
+
IconButton: makeSegment({
|
|
28
|
+
meta: { name: "IconButton", description: "Icon-only button", category: "actions", status: "stable" },
|
|
29
|
+
relations: [
|
|
30
|
+
{ component: "Button", relationship: "alternative", note: "Use for labeled actions" },
|
|
31
|
+
],
|
|
32
|
+
}),
|
|
33
|
+
ButtonGroup: makeSegment({
|
|
34
|
+
meta: { name: "ButtonGroup", description: "Groups buttons", category: "actions", status: "stable" },
|
|
35
|
+
relations: [
|
|
36
|
+
{ component: "Button", relationship: "child", note: "Contains buttons" },
|
|
37
|
+
],
|
|
38
|
+
}),
|
|
39
|
+
TextField: makeSegment({
|
|
40
|
+
meta: { name: "TextField", description: "Text input", category: "forms", status: "stable" },
|
|
41
|
+
relations: [
|
|
42
|
+
{ component: "Form", relationship: "parent", note: "Should be inside a Form" },
|
|
43
|
+
{ component: "Label", relationship: "sibling", note: "Pair with a Label" },
|
|
44
|
+
],
|
|
45
|
+
usage: {
|
|
46
|
+
when: ["User needs to enter text"],
|
|
47
|
+
whenNot: ["Use TextArea for multiline input"],
|
|
48
|
+
},
|
|
49
|
+
}),
|
|
50
|
+
Form: makeSegment({
|
|
51
|
+
meta: { name: "Form", description: "Form container", category: "forms", status: "stable" },
|
|
52
|
+
}),
|
|
53
|
+
Label: makeSegment({
|
|
54
|
+
meta: { name: "Label", description: "Form label", category: "forms", status: "stable" },
|
|
55
|
+
}),
|
|
56
|
+
Alert: makeSegment({
|
|
57
|
+
meta: { name: "Alert", description: "Feedback alert", category: "feedback", status: "stable" },
|
|
58
|
+
}),
|
|
59
|
+
Toast: makeSegment({
|
|
60
|
+
meta: { name: "Toast", description: "Toast notification", category: "feedback", status: "experimental" },
|
|
61
|
+
}),
|
|
62
|
+
OldButton: makeSegment({
|
|
63
|
+
meta: { name: "OldButton", description: "Use Button instead", category: "actions", status: "deprecated" },
|
|
64
|
+
}),
|
|
65
|
+
Link: makeSegment({
|
|
66
|
+
meta: { name: "Link", description: "Navigation link", category: "navigation", status: "stable" },
|
|
67
|
+
usage: {
|
|
68
|
+
when: ["User needs to navigate"],
|
|
69
|
+
whenNot: ["Use Button for actions"],
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
describe("analyzeComposition", () => {
|
|
75
|
+
describe("name validation", () => {
|
|
76
|
+
it("should split components into found and unknown", () => {
|
|
77
|
+
const result = analyzeComposition(baseSegments, ["Button", "NonExistent", "TextField", "AlsoFake"]);
|
|
78
|
+
expect(result.components).toEqual(["Button", "TextField"]);
|
|
79
|
+
expect(result.unknown).toEqual(["NonExistent", "AlsoFake"]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should handle all valid names", () => {
|
|
83
|
+
const result = analyzeComposition(baseSegments, ["Button", "TextField"]);
|
|
84
|
+
expect(result.components).toEqual(["Button", "TextField"]);
|
|
85
|
+
expect(result.unknown).toEqual([]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should handle all unknown names", () => {
|
|
89
|
+
const result = analyzeComposition(baseSegments, ["Foo", "Bar"]);
|
|
90
|
+
expect(result.components).toEqual([]);
|
|
91
|
+
expect(result.unknown).toEqual(["Foo", "Bar"]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should handle empty input", () => {
|
|
95
|
+
const result = analyzeComposition(baseSegments, []);
|
|
96
|
+
expect(result.components).toEqual([]);
|
|
97
|
+
expect(result.unknown).toEqual([]);
|
|
98
|
+
expect(result.warnings).toEqual([]);
|
|
99
|
+
expect(result.suggestions).toEqual([]);
|
|
100
|
+
expect(result.guidelines).toEqual([]);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("relation checks", () => {
|
|
105
|
+
it("should warn about missing parent", () => {
|
|
106
|
+
const result = analyzeComposition(baseSegments, ["TextField"]);
|
|
107
|
+
const parentWarning = result.warnings.find((w) => w.type === "missing_parent");
|
|
108
|
+
expect(parentWarning).toBeDefined();
|
|
109
|
+
expect(parentWarning!.component).toBe("TextField");
|
|
110
|
+
expect(parentWarning!.relatedComponent).toBe("Form");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should not warn about parent when parent is selected", () => {
|
|
114
|
+
const result = analyzeComposition(baseSegments, ["TextField", "Form"]);
|
|
115
|
+
const parentWarning = result.warnings.find((w) => w.type === "missing_parent");
|
|
116
|
+
expect(parentWarning).toBeUndefined();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("should suggest missing children", () => {
|
|
120
|
+
const result = analyzeComposition(baseSegments, ["ButtonGroup"]);
|
|
121
|
+
const childSuggestion = result.suggestions.find(
|
|
122
|
+
(s) => s.component === "Button" && s.relationship === "child"
|
|
123
|
+
);
|
|
124
|
+
expect(childSuggestion).toBeDefined();
|
|
125
|
+
expect(childSuggestion!.sourceComponent).toBe("ButtonGroup");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("should warn about missing composition peers", () => {
|
|
129
|
+
const result = analyzeComposition(baseSegments, ["Button"]);
|
|
130
|
+
const compWarning = result.warnings.find((w) => w.type === "missing_composition");
|
|
131
|
+
expect(compWarning).toBeDefined();
|
|
132
|
+
expect(compWarning!.component).toBe("Button");
|
|
133
|
+
expect(compWarning!.relatedComponent).toBe("ButtonGroup");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("should not warn when composition peer is selected", () => {
|
|
137
|
+
const result = analyzeComposition(baseSegments, ["Button", "ButtonGroup"]);
|
|
138
|
+
const compWarning = result.warnings.find((w) => w.type === "missing_composition");
|
|
139
|
+
expect(compWarning).toBeUndefined();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("should suggest missing siblings", () => {
|
|
143
|
+
const result = analyzeComposition(baseSegments, ["TextField", "Form"]);
|
|
144
|
+
const siblingSuggestion = result.suggestions.find(
|
|
145
|
+
(s) => s.component === "Label" && s.relationship === "sibling"
|
|
146
|
+
);
|
|
147
|
+
expect(siblingSuggestion).toBeDefined();
|
|
148
|
+
expect(siblingSuggestion!.sourceComponent).toBe("TextField");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("should warn about redundant alternatives", () => {
|
|
152
|
+
const result = analyzeComposition(baseSegments, ["Button", "IconButton"]);
|
|
153
|
+
const altWarning = result.warnings.find((w) => w.type === "redundant_alternative");
|
|
154
|
+
expect(altWarning).toBeDefined();
|
|
155
|
+
expect(altWarning!.component).toBe("Button");
|
|
156
|
+
expect(altWarning!.relatedComponent).toBe("IconButton");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("should not warn when only one alternative is selected", () => {
|
|
160
|
+
const result = analyzeComposition(baseSegments, ["Button"]);
|
|
161
|
+
const altWarning = result.warnings.find((w) => w.type === "redundant_alternative");
|
|
162
|
+
expect(altWarning).toBeUndefined();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe("usage conflict checks", () => {
|
|
167
|
+
it("should emit guideline when whenNot mentions another selected component", () => {
|
|
168
|
+
const result = analyzeComposition(baseSegments, ["Button", "Link"]);
|
|
169
|
+
const conflict = result.guidelines.find(
|
|
170
|
+
(g) => g.component === "Link" && g.guideline.includes("Button")
|
|
171
|
+
);
|
|
172
|
+
expect(conflict).toBeDefined();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("should not emit guideline when no conflict exists", () => {
|
|
176
|
+
const result = analyzeComposition(baseSegments, ["Button", "Alert"]);
|
|
177
|
+
expect(result.guidelines).toEqual([]);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe("status warnings", () => {
|
|
182
|
+
it("should warn about deprecated components", () => {
|
|
183
|
+
const result = analyzeComposition(baseSegments, ["OldButton"]);
|
|
184
|
+
const depWarning = result.warnings.find((w) => w.type === "deprecated");
|
|
185
|
+
expect(depWarning).toBeDefined();
|
|
186
|
+
expect(depWarning!.component).toBe("OldButton");
|
|
187
|
+
expect(depWarning!.message).toContain("deprecated");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("should warn about experimental components", () => {
|
|
191
|
+
const result = analyzeComposition(baseSegments, ["Toast"]);
|
|
192
|
+
const expWarning = result.warnings.find((w) => w.type === "experimental");
|
|
193
|
+
expect(expWarning).toBeDefined();
|
|
194
|
+
expect(expWarning!.component).toBe("Toast");
|
|
195
|
+
expect(expWarning!.message).toContain("experimental");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("should not warn about stable components", () => {
|
|
199
|
+
const result = analyzeComposition(baseSegments, ["Button"]);
|
|
200
|
+
const statusWarnings = result.warnings.filter(
|
|
201
|
+
(w) => w.type === "deprecated" || w.type === "experimental"
|
|
202
|
+
);
|
|
203
|
+
expect(statusWarnings).toEqual([]);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe("category gap analysis", () => {
|
|
208
|
+
it("should suggest feedback component when forms are selected without feedback", () => {
|
|
209
|
+
const result = analyzeComposition(baseSegments, ["TextField", "Form"]);
|
|
210
|
+
const gapSuggestion = result.suggestions.find(
|
|
211
|
+
(s) => s.relationship === "category_gap"
|
|
212
|
+
);
|
|
213
|
+
expect(gapSuggestion).toBeDefined();
|
|
214
|
+
expect(gapSuggestion!.component).toBe("Alert");
|
|
215
|
+
expect(gapSuggestion!.reason).toContain("feedback");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("should suggest feedback component when actions are selected without feedback", () => {
|
|
219
|
+
const result = analyzeComposition(baseSegments, ["Button", "ButtonGroup"]);
|
|
220
|
+
const gapSuggestion = result.suggestions.find(
|
|
221
|
+
(s) => s.relationship === "category_gap"
|
|
222
|
+
);
|
|
223
|
+
expect(gapSuggestion).toBeDefined();
|
|
224
|
+
expect(gapSuggestion!.component).toBe("Alert");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("should not suggest category gap when feedback is already present", () => {
|
|
228
|
+
const result = analyzeComposition(baseSegments, ["Button", "ButtonGroup", "Alert"]);
|
|
229
|
+
const gapSuggestion = result.suggestions.find(
|
|
230
|
+
(s) => s.relationship === "category_gap"
|
|
231
|
+
);
|
|
232
|
+
expect(gapSuggestion).toBeUndefined();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("should prefer stable components over experimental in category gap suggestions", () => {
|
|
236
|
+
// Alert is stable, Toast is experimental — should pick Alert
|
|
237
|
+
const result = analyzeComposition(baseSegments, ["TextField", "Form"]);
|
|
238
|
+
const gapSuggestion = result.suggestions.find(
|
|
239
|
+
(s) => s.relationship === "category_gap"
|
|
240
|
+
);
|
|
241
|
+
expect(gapSuggestion?.component).toBe("Alert");
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe("deduplication", () => {
|
|
246
|
+
it("should not suggest the same component twice", () => {
|
|
247
|
+
const result = analyzeComposition(baseSegments, ["ButtonGroup", "Button"]);
|
|
248
|
+
const buttonSuggestions = result.suggestions.filter(
|
|
249
|
+
(s) => s.component === "Button"
|
|
250
|
+
);
|
|
251
|
+
// Button is already selected so it shouldn't be suggested
|
|
252
|
+
expect(buttonSuggestions).toEqual([]);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe("context parameter", () => {
|
|
257
|
+
it("should accept optional context without error", () => {
|
|
258
|
+
const result = analyzeComposition(baseSegments, ["Button"], "building a form page");
|
|
259
|
+
expect(result.components).toEqual(["Button"]);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
});
|