@fragments-sdk/cli 0.5.2 → 0.7.0
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/dist/bin.js +996 -79
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-ICAIQ57V.js → chunk-6JBGU74P.js} +5 -3
- package/dist/chunk-6JBGU74P.js.map +1 -0
- package/dist/chunk-7OPWMLOE.js +1625 -0
- package/dist/chunk-7OPWMLOE.js.map +1 -0
- package/dist/{chunk-2H2JAA3U.js → chunk-CVXKXVOY.js} +3 -3
- package/dist/{chunk-2H2JAA3U.js.map → chunk-CVXKXVOY.js.map} +1 -1
- package/dist/{chunk-IOJE35DZ.js → chunk-NWQ4CJOQ.js} +3 -3
- package/dist/{chunk-2DJH4F4P.js → chunk-RVRTRESS.js} +3 -3
- package/dist/{chunk-V7YLRR4C.js → chunk-TJ34N7C7.js} +41 -4
- package/dist/{chunk-V7YLRR4C.js.map → chunk-TJ34N7C7.js.map} +1 -1
- package/dist/{chunk-XNWDI6UT.js → chunk-XHUDJNN3.js} +5 -5
- package/dist/{core-DKHB7FYV.js → core-W2HYIQW6.js} +4 -4
- package/dist/{generate-KL24VZVD.js → generate-LMTISDIJ.js} +5 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +15 -7
- package/dist/index.js.map +1 -1
- package/dist/{init-NION5S3M.js → init-7CHRKQ7P.js} +5 -5
- package/dist/mcp-bin.js +8 -220
- package/dist/mcp-bin.js.map +1 -1
- package/dist/scan-WY23TJCP.js +12 -0
- package/dist/{service-RWUMZ3EW.js → service-T2L7VLTE.js} +5 -5
- package/dist/static-viewer-GBR7YNF3.js +12 -0
- package/dist/{test-ECPEXFDN.js → test-OJRXNDO2.js} +4 -4
- package/dist/{tokens-ITADYVPF.js → tokens-3BWDESVM.js} +6 -6
- package/dist/viewer-SUFOISZM.js +1822 -0
- package/dist/viewer-SUFOISZM.js.map +1 -0
- package/package.json +6 -5
- package/src/bin.ts +31 -0
- package/src/build.ts +147 -13
- package/src/cli-commands.ts +18 -0
- package/src/commands/__tests__/a11y-scoring.test.ts +278 -0
- package/src/commands/a11y-report.ts +625 -0
- package/src/commands/a11y.ts +168 -14
- package/src/commands/build.ts +16 -0
- package/src/commands/graph.ts +274 -0
- package/src/core/auto-props.ts +464 -0
- package/src/core/composition.ts +64 -1
- package/src/core/graph-extractor.test.ts +542 -0
- package/src/core/graph-extractor.ts +601 -0
- package/src/core/importAnalyzer.ts +5 -0
- package/src/core/schema.ts +2 -0
- package/src/core/types.ts +3 -1
- package/src/index.ts +4 -0
- package/src/mcp/server.ts +13 -220
- package/src/theme/__tests__/component-contrast.test.ts +338 -0
- package/src/theme/__tests__/contrast-validation.test.ts +326 -0
- package/src/theme/contrast.test.ts +331 -0
- package/src/theme/contrast.ts +246 -0
- package/src/theme/generator.ts +213 -1
- package/src/theme/index.ts +16 -0
- package/src/theme/types.ts +51 -0
- package/src/viewer/__tests__/a11y-fixes.test.ts +358 -0
- package/src/viewer/__tests__/viewer-integration.test.ts +2 -7
- package/src/viewer/components/AccessibilityPanel.tsx +493 -433
- package/src/viewer/components/ActionCapture.tsx +1 -1
- package/src/viewer/components/ActionsPanel.tsx +142 -183
- package/src/viewer/components/App.tsx +276 -183
- package/src/viewer/components/BottomPanel.tsx +40 -80
- package/src/viewer/components/CodePanel.tsx +9 -87
- package/src/viewer/components/CommandPalette.tsx +117 -74
- package/src/viewer/components/ComponentGraph.tsx +143 -126
- package/src/viewer/components/ComponentHeader.tsx +46 -43
- package/src/viewer/components/ContractPanel.tsx +124 -117
- package/src/viewer/components/ErrorBoundary.tsx +47 -35
- package/src/viewer/components/FigmaEmbed.tsx +18 -13
- package/src/viewer/components/FragmentEditor.tsx +126 -63
- package/src/viewer/components/HealthDashboard.tsx +146 -171
- package/src/viewer/components/HmrStatusIndicator.tsx +31 -41
- package/src/viewer/components/Icons.tsx +151 -98
- package/src/viewer/components/InteractionsPanel.tsx +317 -264
- package/src/viewer/components/IsolatedPreviewFrame.tsx +52 -27
- package/src/viewer/components/IsolatedRender.tsx +12 -6
- package/src/viewer/components/KeyboardShortcutsHelp.tsx +34 -70
- package/src/viewer/components/LandingPage.tsx +285 -305
- package/src/viewer/components/Layout.tsx +12 -10
- package/src/viewer/components/LeftSidebar.tsx +103 -155
- package/src/viewer/components/MultiViewportPreview.tsx +254 -63
- package/src/viewer/components/PreviewArea.tsx +113 -44
- package/src/viewer/components/PreviewFrameHost.tsx +36 -6
- package/src/viewer/components/PreviewPane.tsx +2 -3
- package/src/viewer/components/PreviewToolbar.tsx +109 -105
- package/src/viewer/components/PropsEditor.tsx +154 -74
- package/src/viewer/components/PropsTable.tsx +95 -82
- package/src/viewer/components/RelationsSection.tsx +71 -40
- package/src/viewer/components/ResizablePanel.tsx +158 -55
- package/src/viewer/components/RightSidebar.tsx +46 -56
- package/src/viewer/components/ScreenshotButton.tsx +12 -12
- package/src/viewer/components/SkeletonLoader.tsx +99 -83
- package/src/viewer/components/StoryRenderer.tsx +4 -11
- package/src/viewer/components/Toast.tsx +3 -67
- package/src/viewer/components/TokenStylePanel.tsx +136 -118
- package/src/viewer/components/UsageSection.tsx +26 -26
- package/src/viewer/components/VariantMatrix.tsx +140 -47
- package/src/viewer/components/VariantTabs.tsx +24 -68
- package/src/viewer/components/ViewportSelector.tsx +121 -114
- package/src/viewer/constants/ui.ts +23 -22
- package/src/viewer/entry.tsx +8 -3
- package/src/viewer/index.ts +3 -6
- package/src/viewer/preview-frame.html +43 -18
- package/src/viewer/server.ts +7 -16
- package/src/viewer/styles/globals.css +46 -85
- package/src/viewer/utils/a11y-fixes.ts +53 -30
- package/dist/chunk-ICAIQ57V.js.map +0 -1
- package/dist/chunk-U4GQ2JTD.js +0 -832
- package/dist/chunk-U4GQ2JTD.js.map +0 -1
- package/dist/scan-ESEXV7LF.js +0 -12
- package/dist/static-viewer-O37MJ5B6.js +0 -12
- package/dist/viewer-YDGFDTK5.js +0 -11104
- package/dist/viewer-YDGFDTK5.js.map +0 -1
- package/src/viewer/postcss.config.js +0 -6
- package/src/viewer/tailwind.config.js +0 -37
- /package/dist/{chunk-IOJE35DZ.js.map → chunk-NWQ4CJOQ.js.map} +0 -0
- /package/dist/{chunk-2DJH4F4P.js.map → chunk-RVRTRESS.js.map} +0 -0
- /package/dist/{chunk-XNWDI6UT.js.map → chunk-XHUDJNN3.js.map} +0 -0
- /package/dist/{core-DKHB7FYV.js.map → core-W2HYIQW6.js.map} +0 -0
- /package/dist/{generate-KL24VZVD.js.map → generate-LMTISDIJ.js.map} +0 -0
- /package/dist/{init-NION5S3M.js.map → init-7CHRKQ7P.js.map} +0 -0
- /package/dist/{scan-ESEXV7LF.js.map → scan-WY23TJCP.js.map} +0 -0
- /package/dist/{service-RWUMZ3EW.js.map → service-T2L7VLTE.js.map} +0 -0
- /package/dist/{static-viewer-O37MJ5B6.js.map → static-viewer-GBR7YNF3.js.map} +0 -0
- /package/dist/{test-ECPEXFDN.js.map → test-OJRXNDO2.js.map} +0 -0
- /package/dist/{tokens-ITADYVPF.js.map → tokens-3BWDESVM.js.map} +0 -0
package/src/commands/a11y.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* for all components in the design system.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import fs from 'node:fs';
|
|
8
9
|
import pc from 'picocolors';
|
|
9
10
|
import { BRAND } from '../core/index.js';
|
|
10
11
|
import { loadConfig } from '../core/node.js';
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
createDevServerClient,
|
|
13
14
|
DevServerConnectionError,
|
|
14
15
|
} from '../shared/index.js';
|
|
16
|
+
import { generateA11yReport } from './a11y-report.js';
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* Options for a11y command
|
|
@@ -27,6 +29,14 @@ export interface A11yOptions {
|
|
|
27
29
|
component?: string;
|
|
28
30
|
/** Dev server port */
|
|
29
31
|
port?: number | string;
|
|
32
|
+
/** Output format: table (default), json, or github (markdown) */
|
|
33
|
+
format?: 'table' | 'json' | 'github';
|
|
34
|
+
/** WCAG standard level to check against */
|
|
35
|
+
standard?: 'AA' | 'AAA';
|
|
36
|
+
/** Generate standalone HTML compliance report */
|
|
37
|
+
report?: boolean;
|
|
38
|
+
/** Output path for the HTML report */
|
|
39
|
+
output?: string;
|
|
30
40
|
}
|
|
31
41
|
|
|
32
42
|
/**
|
|
@@ -69,6 +79,18 @@ export interface A11yComponentResult {
|
|
|
69
79
|
totalSerious: number;
|
|
70
80
|
}
|
|
71
81
|
|
|
82
|
+
/**
|
|
83
|
+
* A11y score breakdown
|
|
84
|
+
*/
|
|
85
|
+
export interface A11yScore {
|
|
86
|
+
/** Numeric score 0–100 */
|
|
87
|
+
score: number;
|
|
88
|
+
/** Percentage of components meeting AA */
|
|
89
|
+
aaPercent: number;
|
|
90
|
+
/** Percentage of components meeting AAA */
|
|
91
|
+
aaaPercent: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
72
94
|
/**
|
|
73
95
|
* Summary of a11y results
|
|
74
96
|
*/
|
|
@@ -93,13 +115,98 @@ export interface A11ySummary {
|
|
|
93
115
|
totalMinor: number;
|
|
94
116
|
/** Whether CI check passed (no critical/serious) */
|
|
95
117
|
passed: boolean;
|
|
118
|
+
/** Computed a11y score */
|
|
119
|
+
score?: A11yScore;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Calculate an accessibility score from the summary.
|
|
124
|
+
*
|
|
125
|
+
* Starts at 100 and subtracts per violation:
|
|
126
|
+
* critical: -10, serious: -5, moderate: -2, minor: -1
|
|
127
|
+
*/
|
|
128
|
+
export function calculateA11yScore(summary: A11ySummary): A11yScore {
|
|
129
|
+
const deductions =
|
|
130
|
+
summary.totalCritical * 10 +
|
|
131
|
+
summary.totalSerious * 5 +
|
|
132
|
+
summary.totalModerate * 2 +
|
|
133
|
+
summary.totalMinor * 1;
|
|
134
|
+
|
|
135
|
+
const score = Math.max(0, 100 - deductions);
|
|
136
|
+
|
|
137
|
+
// AA = no critical/serious violations
|
|
138
|
+
const aaComponents = summary.components.filter(
|
|
139
|
+
c => c.totalCritical === 0 && c.totalSerious === 0
|
|
140
|
+
).length;
|
|
141
|
+
const aaPercent = summary.totalComponents > 0
|
|
142
|
+
? Math.round((aaComponents / summary.totalComponents) * 100)
|
|
143
|
+
: 100;
|
|
144
|
+
|
|
145
|
+
// AAA = no violations at all
|
|
146
|
+
const aaaComponents = summary.components.filter(
|
|
147
|
+
c => c.totalViolations === 0
|
|
148
|
+
).length;
|
|
149
|
+
const aaaPercent = summary.totalComponents > 0
|
|
150
|
+
? Math.round((aaaComponents / summary.totalComponents) * 100)
|
|
151
|
+
: 100;
|
|
152
|
+
|
|
153
|
+
return { score, aaPercent, aaaPercent };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Format summary as GitHub-flavored Markdown.
|
|
158
|
+
*/
|
|
159
|
+
export function formatGitHub(summary: A11ySummary): string {
|
|
160
|
+
const score = summary.score ?? calculateA11yScore(summary);
|
|
161
|
+
const badge = summary.passed ? 'passing' : 'failing';
|
|
162
|
+
const badgeColor = summary.passed ? 'brightgreen' : 'red';
|
|
163
|
+
const lines: string[] = [];
|
|
164
|
+
|
|
165
|
+
lines.push(`## ${BRAND.name} Accessibility Report`);
|
|
166
|
+
lines.push('');
|
|
167
|
+
lines.push(``);
|
|
168
|
+
lines.push(`**Score:** ${score.score}/100 | **AA:** ${score.aaPercent}% | **AAA:** ${score.aaaPercent}%`);
|
|
169
|
+
lines.push('');
|
|
170
|
+
|
|
171
|
+
// Component table
|
|
172
|
+
lines.push('| Component | Variants | Violations | Critical | Serious | Status |');
|
|
173
|
+
lines.push('|-----------|----------|------------|----------|---------|--------|');
|
|
174
|
+
|
|
175
|
+
for (const result of summary.components) {
|
|
176
|
+
const statusIcon = result.status === 'PASS' ? 'PASS' : result.status === 'WARN' ? 'WARN' : 'FAIL';
|
|
177
|
+
const variantCount = result.results.length || 1;
|
|
178
|
+
lines.push(
|
|
179
|
+
`| ${result.component} | ${variantCount} | ${result.totalViolations} | ${result.totalCritical} | ${result.totalSerious} | ${statusIcon} |`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
lines.push('');
|
|
184
|
+
lines.push(`**Summary:** ${summary.accessibleComponents}/${summary.totalComponents} components accessible (${summary.accessiblePercent}%)`);
|
|
185
|
+
lines.push(`**Violations:** ${summary.totalViolations} total (${summary.totalCritical} critical, ${summary.totalSerious} serious, ${summary.totalModerate} moderate, ${summary.totalMinor} minor)`);
|
|
186
|
+
|
|
187
|
+
if (summary.totalCritical + summary.totalSerious > 0) {
|
|
188
|
+
lines.push('');
|
|
189
|
+
lines.push(`**Blocking:** ${summary.totalCritical + summary.totalSerious} critical/serious issues must be fixed`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
lines.push('');
|
|
193
|
+
|
|
194
|
+
return lines.join('\n');
|
|
96
195
|
}
|
|
97
196
|
|
|
98
197
|
/**
|
|
99
198
|
* Run the a11y command
|
|
100
199
|
*/
|
|
101
200
|
export async function a11y(options: A11yOptions = {}): Promise<A11ySummary> {
|
|
102
|
-
const {
|
|
201
|
+
const {
|
|
202
|
+
config: configPath,
|
|
203
|
+
json = false,
|
|
204
|
+
ci = false,
|
|
205
|
+
component,
|
|
206
|
+
port = 6006,
|
|
207
|
+
format = json ? 'json' : 'table',
|
|
208
|
+
standard = 'AA',
|
|
209
|
+
} = options;
|
|
103
210
|
|
|
104
211
|
// Load config
|
|
105
212
|
await loadConfig(configPath);
|
|
@@ -107,7 +214,9 @@ export async function a11y(options: A11yOptions = {}): Promise<A11ySummary> {
|
|
|
107
214
|
const client = createDevServerClient(port);
|
|
108
215
|
const componentResults: A11yComponentResult[] = [];
|
|
109
216
|
|
|
110
|
-
|
|
217
|
+
const isJsonOutput = format === 'json' || json;
|
|
218
|
+
|
|
219
|
+
if (!isJsonOutput && format !== 'github') {
|
|
111
220
|
console.log(pc.cyan(`\n${BRAND.name} Accessibility Report\n`));
|
|
112
221
|
}
|
|
113
222
|
|
|
@@ -124,7 +233,7 @@ export async function a11y(options: A11yOptions = {}): Promise<A11ySummary> {
|
|
|
124
233
|
const segments = await client.getSegments();
|
|
125
234
|
|
|
126
235
|
if (segments.length === 0) {
|
|
127
|
-
if (
|
|
236
|
+
if (isJsonOutput) {
|
|
128
237
|
console.log(JSON.stringify({ error: 'No fragments found', components: [] }));
|
|
129
238
|
} else {
|
|
130
239
|
console.log(pc.yellow('No fragments found.\n'));
|
|
@@ -150,7 +259,7 @@ export async function a11y(options: A11yOptions = {}): Promise<A11ySummary> {
|
|
|
150
259
|
|
|
151
260
|
if (component && componentsToCheck.length === 0) {
|
|
152
261
|
const error = `Component '${component}' not found. Available: ${segments.map(s => s.name).join(', ')}`;
|
|
153
|
-
if (
|
|
262
|
+
if (isJsonOutput) {
|
|
154
263
|
console.log(JSON.stringify({ error }));
|
|
155
264
|
} else {
|
|
156
265
|
console.log(pc.red(error));
|
|
@@ -158,7 +267,7 @@ export async function a11y(options: A11yOptions = {}): Promise<A11ySummary> {
|
|
|
158
267
|
throw new Error(error);
|
|
159
268
|
}
|
|
160
269
|
|
|
161
|
-
if (!
|
|
270
|
+
if (!isJsonOutput && format !== 'github') {
|
|
162
271
|
console.log(pc.dim(`Checking ${componentsToCheck.length} component(s) for accessibility issues...\n`));
|
|
163
272
|
}
|
|
164
273
|
|
|
@@ -186,6 +295,11 @@ export async function a11y(options: A11yOptions = {}): Promise<A11ySummary> {
|
|
|
186
295
|
status = 'WARN';
|
|
187
296
|
}
|
|
188
297
|
|
|
298
|
+
// In AAA mode, any violation is a failure
|
|
299
|
+
if (standard === 'AAA' && totalViolations > 0) {
|
|
300
|
+
status = 'FAIL';
|
|
301
|
+
}
|
|
302
|
+
|
|
189
303
|
componentResults.push({
|
|
190
304
|
component: seg.name,
|
|
191
305
|
results: a11yResult.results,
|
|
@@ -194,7 +308,7 @@ export async function a11y(options: A11yOptions = {}): Promise<A11ySummary> {
|
|
|
194
308
|
totalCritical,
|
|
195
309
|
totalSerious,
|
|
196
310
|
});
|
|
197
|
-
} catch
|
|
311
|
+
} catch {
|
|
198
312
|
// Handle individual component errors
|
|
199
313
|
componentResults.push({
|
|
200
314
|
component: seg.name,
|
|
@@ -223,6 +337,10 @@ export async function a11y(options: A11yOptions = {}): Promise<A11ySummary> {
|
|
|
223
337
|
}
|
|
224
338
|
}
|
|
225
339
|
|
|
340
|
+
const passed = standard === 'AAA'
|
|
341
|
+
? totalViolations === 0
|
|
342
|
+
: totalCritical === 0 && totalSerious === 0;
|
|
343
|
+
|
|
226
344
|
const summary: A11ySummary = {
|
|
227
345
|
totalComponents: componentResults.length,
|
|
228
346
|
accessibleComponents,
|
|
@@ -235,14 +353,18 @@ export async function a11y(options: A11yOptions = {}): Promise<A11ySummary> {
|
|
|
235
353
|
totalSerious,
|
|
236
354
|
totalModerate,
|
|
237
355
|
totalMinor,
|
|
238
|
-
passed
|
|
356
|
+
passed,
|
|
239
357
|
};
|
|
240
358
|
|
|
241
|
-
|
|
242
|
-
|
|
359
|
+
summary.score = calculateA11yScore(summary);
|
|
360
|
+
|
|
361
|
+
// --- Output ---
|
|
362
|
+
if (format === 'github') {
|
|
363
|
+
console.log(formatGitHub(summary));
|
|
364
|
+
} else if (isJsonOutput) {
|
|
243
365
|
console.log(JSON.stringify(summary, null, 2));
|
|
244
366
|
} else {
|
|
245
|
-
//
|
|
367
|
+
// Rich table output
|
|
246
368
|
console.log(pc.bold(
|
|
247
369
|
'Component'.padEnd(20) +
|
|
248
370
|
'Variants'.padEnd(10) +
|
|
@@ -273,6 +395,30 @@ export async function a11y(options: A11yOptions = {}): Promise<A11ySummary> {
|
|
|
273
395
|
}
|
|
274
396
|
|
|
275
397
|
console.log(pc.dim('─'.repeat(72)));
|
|
398
|
+
console.log();
|
|
399
|
+
|
|
400
|
+
// Violation breakdown with dotted leaders
|
|
401
|
+
const categories = [
|
|
402
|
+
{ label: 'Critical', count: totalCritical, color: pc.red },
|
|
403
|
+
{ label: 'Serious', count: totalSerious, color: pc.red },
|
|
404
|
+
{ label: 'Moderate', count: totalModerate, color: pc.yellow },
|
|
405
|
+
{ label: 'Minor', count: totalMinor, color: pc.dim },
|
|
406
|
+
];
|
|
407
|
+
|
|
408
|
+
for (const cat of categories) {
|
|
409
|
+
if (cat.count > 0) {
|
|
410
|
+
const dots = '.'.repeat(Math.max(1, 30 - cat.label.length));
|
|
411
|
+
console.log(` ${cat.label} ${pc.dim(dots)} ${cat.color(String(cat.count))}`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Score box
|
|
416
|
+
const { score, aaPercent, aaaPercent } = summary.score;
|
|
417
|
+
console.log();
|
|
418
|
+
console.log(pc.bold(` Score: ${score}/100`));
|
|
419
|
+
console.log(` AA compliance .... ${aaPercent}%`);
|
|
420
|
+
console.log(` AAA compliance ... ${aaaPercent}%`);
|
|
421
|
+
console.log(` Standard ......... WCAG ${standard}`);
|
|
276
422
|
|
|
277
423
|
console.log();
|
|
278
424
|
console.log(pc.bold('Summary:'));
|
|
@@ -281,19 +427,27 @@ export async function a11y(options: A11yOptions = {}): Promise<A11ySummary> {
|
|
|
281
427
|
|
|
282
428
|
if (!summary.passed) {
|
|
283
429
|
console.log();
|
|
284
|
-
console.log(pc.red('
|
|
430
|
+
console.log(pc.red('x Accessibility check failed - critical/serious violations found'));
|
|
285
431
|
} else if (totalViolations > 0) {
|
|
286
432
|
console.log();
|
|
287
|
-
console.log(pc.yellow('
|
|
433
|
+
console.log(pc.yellow('! Minor/moderate violations found - consider fixing for better accessibility'));
|
|
288
434
|
} else {
|
|
289
435
|
console.log();
|
|
290
|
-
console.log(pc.green('
|
|
436
|
+
console.log(pc.green('v All components pass accessibility checks'));
|
|
291
437
|
}
|
|
292
438
|
|
|
293
439
|
console.log();
|
|
294
440
|
}
|
|
295
441
|
|
|
296
|
-
//
|
|
442
|
+
// Generate HTML report if requested
|
|
443
|
+
if (options.report) {
|
|
444
|
+
const outputPath = options.output ?? 'a11y-report.html';
|
|
445
|
+
const html = generateA11yReport(summary);
|
|
446
|
+
fs.writeFileSync(outputPath, html, 'utf-8');
|
|
447
|
+
console.log(pc.green('v Report generated: ' + outputPath));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// In CI mode, throw if check did not pass
|
|
297
451
|
if (ci && !summary.passed) {
|
|
298
452
|
throw new Error(`Accessibility check failed: ${totalCritical} critical, ${totalSerious} serious violations found`);
|
|
299
453
|
}
|
package/src/commands/build.ts
CHANGED
|
@@ -101,6 +101,14 @@ export async function build(options: BuildOptions = {}): Promise<BuildResult> {
|
|
|
101
101
|
console.log();
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
if (result.warnings.length > 0) {
|
|
105
|
+
console.log(pc.yellow('Build warnings:\n'));
|
|
106
|
+
for (const warning of result.warnings) {
|
|
107
|
+
console.log(` ${pc.yellow('⚠')} ${warning.file}: ${warning.warning}`);
|
|
108
|
+
}
|
|
109
|
+
console.log();
|
|
110
|
+
}
|
|
111
|
+
|
|
104
112
|
segmentCount = result.segmentCount;
|
|
105
113
|
outputPath = result.outputPath;
|
|
106
114
|
|
|
@@ -123,6 +131,14 @@ export async function build(options: BuildOptions = {}): Promise<BuildResult> {
|
|
|
123
131
|
console.log();
|
|
124
132
|
}
|
|
125
133
|
|
|
134
|
+
if (fragmentsResult.warnings.length > 0) {
|
|
135
|
+
console.log(pc.yellow('Registry warnings:\n'));
|
|
136
|
+
for (const warning of fragmentsResult.warnings) {
|
|
137
|
+
console.log(` ${pc.yellow('⚠')} ${warning.file}: ${warning.warning}`);
|
|
138
|
+
}
|
|
139
|
+
console.log();
|
|
140
|
+
}
|
|
141
|
+
|
|
126
142
|
componentCount = fragmentsResult.componentCount;
|
|
127
143
|
registryPath = fragmentsResult.registryPath;
|
|
128
144
|
contextPath = fragmentsResult.contextPath;
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fragments graph` — query the component relationship graph from the CLI.
|
|
3
|
+
*
|
|
4
|
+
* Loads fragments.json, instantiates the ComponentGraphEngine, and runs
|
|
5
|
+
* the requested query mode. Output formats: table (colored terminal), json, dot.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import pc from 'picocolors';
|
|
9
|
+
import { readFile } from 'node:fs/promises';
|
|
10
|
+
import { resolve } from 'node:path';
|
|
11
|
+
import type { CompiledSegmentsFile } from '../core/index.js';
|
|
12
|
+
import { BRAND } from '../core/index.js';
|
|
13
|
+
import { loadConfig } from '../core/node.js';
|
|
14
|
+
import {
|
|
15
|
+
ComponentGraphEngine,
|
|
16
|
+
deserializeGraph,
|
|
17
|
+
} from '@fragments-sdk/context/graph';
|
|
18
|
+
import type { GraphEdgeType } from '@fragments-sdk/context/graph';
|
|
19
|
+
|
|
20
|
+
export interface GraphCommandOptions {
|
|
21
|
+
config?: string;
|
|
22
|
+
mode?: string;
|
|
23
|
+
target?: string;
|
|
24
|
+
edgeTypes?: string;
|
|
25
|
+
depth?: number;
|
|
26
|
+
format?: 'table' | 'json' | 'dot';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function graph(
|
|
30
|
+
component: string | undefined,
|
|
31
|
+
options: GraphCommandOptions,
|
|
32
|
+
): Promise<void> {
|
|
33
|
+
const { config, configDir } = await loadConfig(options.config);
|
|
34
|
+
const outputPath = resolve(configDir, config.outFile ?? BRAND.outFile);
|
|
35
|
+
|
|
36
|
+
let data: CompiledSegmentsFile;
|
|
37
|
+
try {
|
|
38
|
+
const content = await readFile(outputPath, 'utf-8');
|
|
39
|
+
data = JSON.parse(content) as CompiledSegmentsFile;
|
|
40
|
+
} catch {
|
|
41
|
+
console.error(
|
|
42
|
+
pc.red(`Error: Could not load ${BRAND.outFile}. Run \`${BRAND.cliCommand} build\` first.`),
|
|
43
|
+
);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!data.graph) {
|
|
48
|
+
console.error(
|
|
49
|
+
pc.red(`Error: No graph data in ${BRAND.outFile}. Rebuild with the latest CLI.`),
|
|
50
|
+
);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const graph = deserializeGraph(data.graph);
|
|
55
|
+
const blocks = data.blocks
|
|
56
|
+
? Object.fromEntries(
|
|
57
|
+
Object.entries(data.blocks).map(([k, v]) => [k, { components: v.components }]),
|
|
58
|
+
)
|
|
59
|
+
: undefined;
|
|
60
|
+
const engine = new ComponentGraphEngine(graph, blocks);
|
|
61
|
+
|
|
62
|
+
const mode = options.mode ?? (component ? 'dependencies' : 'health');
|
|
63
|
+
const format = options.format ?? 'table';
|
|
64
|
+
const edgeTypes = options.edgeTypes
|
|
65
|
+
? (options.edgeTypes.split(',') as GraphEdgeType[])
|
|
66
|
+
: undefined;
|
|
67
|
+
|
|
68
|
+
// Validate component exists for modes that require it
|
|
69
|
+
const needsComponent = ['dependencies', 'dependents', 'impact', 'path', 'composition', 'alternatives'];
|
|
70
|
+
if (needsComponent.includes(mode) && !component) {
|
|
71
|
+
console.error(pc.red(`Error: "${mode}" mode requires a component name.`));
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
if (component && !engine.hasNode(component)) {
|
|
75
|
+
console.error(pc.red(`Error: Component "${component}" not found in graph.`));
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
switch (mode) {
|
|
80
|
+
case 'health': {
|
|
81
|
+
const health = engine.getHealth();
|
|
82
|
+
if (format === 'json') {
|
|
83
|
+
console.log(JSON.stringify(health, null, 2));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log(pc.bold('\nComponent Graph Health\n'));
|
|
88
|
+
console.log(` ${pc.cyan('Nodes:')} ${health.nodeCount}`);
|
|
89
|
+
console.log(` ${pc.cyan('Edges:')} ${health.edgeCount}`);
|
|
90
|
+
console.log(` ${pc.cyan('Avg degree:')} ${health.averageDegree}`);
|
|
91
|
+
console.log(` ${pc.cyan('Islands:')} ${health.connectedComponents.length}`);
|
|
92
|
+
console.log(` ${pc.cyan('Coverage:')} ${health.compositionCoverage}% in blocks`);
|
|
93
|
+
console.log(` ${pc.cyan('Orphans:')} ${health.orphans.length > 0 ? health.orphans.join(', ') : pc.green('none')}`);
|
|
94
|
+
|
|
95
|
+
if (health.hubs.length > 0) {
|
|
96
|
+
console.log(`\n ${pc.bold('Top hubs:')}`);
|
|
97
|
+
for (const hub of health.hubs.slice(0, 5)) {
|
|
98
|
+
console.log(` ${pc.yellow(hub.name)} — ${hub.degree} connections`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
console.log();
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
case 'dependencies': {
|
|
106
|
+
const deps = engine.dependencies(component!, edgeTypes);
|
|
107
|
+
if (format === 'json') {
|
|
108
|
+
console.log(JSON.stringify({ component, dependencies: deps }, null, 2));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
console.log(pc.bold(`\nDependencies of ${component}\n`));
|
|
113
|
+
if (deps.length === 0) {
|
|
114
|
+
console.log(' No outgoing dependencies.');
|
|
115
|
+
} else {
|
|
116
|
+
for (const dep of deps) {
|
|
117
|
+
console.log(` ${pc.yellow(dep.target)} ${pc.dim(`(${dep.type})`)}${dep.note ? ` — ${dep.note}` : ''}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
console.log();
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
case 'dependents': {
|
|
125
|
+
const deps = engine.dependents(component!, edgeTypes);
|
|
126
|
+
if (format === 'json') {
|
|
127
|
+
console.log(JSON.stringify({ component, dependents: deps }, null, 2));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log(pc.bold(`\nDependents of ${component}\n`));
|
|
132
|
+
if (deps.length === 0) {
|
|
133
|
+
console.log(' No incoming dependents.');
|
|
134
|
+
} else {
|
|
135
|
+
for (const dep of deps) {
|
|
136
|
+
console.log(` ${pc.yellow(dep.source)} ${pc.dim(`(${dep.type})`)}${dep.note ? ` — ${dep.note}` : ''}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
console.log();
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
case 'impact': {
|
|
144
|
+
const result = engine.impact(component!, options.depth ?? 3);
|
|
145
|
+
if (format === 'json') {
|
|
146
|
+
console.log(JSON.stringify(result, null, 2));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.log(pc.bold(`\nImpact analysis: ${component}\n`));
|
|
151
|
+
console.log(` ${pc.red(`${result.totalAffected}`)} affected components, ${pc.red(`${result.affectedBlocks.length}`)} affected blocks\n`);
|
|
152
|
+
|
|
153
|
+
if (result.affected.length > 0) {
|
|
154
|
+
console.log(` ${pc.bold('Affected components:')}`);
|
|
155
|
+
for (const entry of result.affected) {
|
|
156
|
+
const indent = ' '.repeat(entry.depth + 1);
|
|
157
|
+
console.log(`${indent}${pc.yellow(entry.component)} ${pc.dim(`(depth ${entry.depth}, via ${entry.edgeType})`)}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (result.affectedBlocks.length > 0) {
|
|
162
|
+
console.log(`\n ${pc.bold('Affected blocks:')} ${result.affectedBlocks.join(', ')}`);
|
|
163
|
+
}
|
|
164
|
+
console.log();
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
case 'path': {
|
|
169
|
+
if (!options.target) {
|
|
170
|
+
console.error(pc.red('Error: --target is required for path mode.'));
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const result = engine.path(component!, options.target);
|
|
175
|
+
if (format === 'json') {
|
|
176
|
+
console.log(JSON.stringify(result, null, 2));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
console.log(pc.bold(`\nPath: ${component} → ${options.target}\n`));
|
|
181
|
+
if (!result.found) {
|
|
182
|
+
console.log(` ${pc.red('No path found.')}`);
|
|
183
|
+
} else {
|
|
184
|
+
console.log(` ${result.path.map(n => pc.yellow(n)).join(pc.dim(' → '))}`);
|
|
185
|
+
if (result.edges.length > 0) {
|
|
186
|
+
console.log(` ${pc.dim(`(${result.edges.map(e => e.type).join(' → ')})`)}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
console.log();
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
case 'composition': {
|
|
194
|
+
const tree = engine.composition(component!);
|
|
195
|
+
if (format === 'json') {
|
|
196
|
+
console.log(JSON.stringify(tree, null, 2));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
console.log(pc.bold(`\nComposition: ${component}\n`));
|
|
201
|
+
console.log(` ${pc.cyan('Pattern:')} ${tree.compositionPattern ?? 'unknown'}`);
|
|
202
|
+
if (tree.parent) {
|
|
203
|
+
console.log(` ${pc.cyan('Parent:')} ${tree.parent}`);
|
|
204
|
+
}
|
|
205
|
+
if (tree.subComponents.length > 0) {
|
|
206
|
+
console.log(` ${pc.cyan('Sub-components:')}`);
|
|
207
|
+
for (const sub of tree.subComponents) {
|
|
208
|
+
const isRequired = tree.requiredChildren.includes(sub);
|
|
209
|
+
console.log(` ${pc.yellow(sub)}${isRequired ? pc.red(' (required)') : ''}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (tree.siblings.length > 0) {
|
|
213
|
+
console.log(` ${pc.cyan('Siblings:')} ${tree.siblings.join(', ')}`);
|
|
214
|
+
}
|
|
215
|
+
if (tree.blocks.length > 0) {
|
|
216
|
+
console.log(` ${pc.cyan('In blocks:')} ${tree.blocks.join(', ')}`);
|
|
217
|
+
}
|
|
218
|
+
console.log();
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
case 'alternatives': {
|
|
223
|
+
const alts = engine.alternatives(component!);
|
|
224
|
+
if (format === 'json') {
|
|
225
|
+
console.log(JSON.stringify({ component, alternatives: alts }, null, 2));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
console.log(pc.bold(`\nAlternatives for ${component}\n`));
|
|
230
|
+
if (alts.length === 0) {
|
|
231
|
+
console.log(' No known alternatives.');
|
|
232
|
+
} else {
|
|
233
|
+
for (const alt of alts) {
|
|
234
|
+
console.log(` ${pc.yellow(alt.component)}${alt.note ? ` — ${alt.note}` : ''}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
console.log();
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
case 'islands': {
|
|
242
|
+
const islands = engine.islands();
|
|
243
|
+
if (format === 'json') {
|
|
244
|
+
console.log(JSON.stringify({ islands }, null, 2));
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
console.log(pc.bold(`\nConnected Islands (${islands.length})\n`));
|
|
249
|
+
for (let i = 0; i < islands.length; i++) {
|
|
250
|
+
console.log(` ${pc.cyan(`Island ${i + 1}`)} (${islands[i].length} components): ${islands[i].join(', ')}`);
|
|
251
|
+
}
|
|
252
|
+
console.log();
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
default:
|
|
257
|
+
console.error(pc.red(`Unknown mode: "${mode}". Valid: health, dependencies, dependents, impact, path, composition, alternatives, islands`));
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Graphviz dot output
|
|
262
|
+
if (format === 'dot') {
|
|
263
|
+
const lines = ['digraph ComponentGraph {', ' rankdir=LR;', ' node [shape=box, style=rounded];'];
|
|
264
|
+
for (const node of graph.nodes) {
|
|
265
|
+
lines.push(` "${node.name}" [label="${node.name}\\n(${node.category})"];`);
|
|
266
|
+
}
|
|
267
|
+
for (const edge of graph.edges) {
|
|
268
|
+
const style = edge.type === 'alternative-to' ? 'dashed' : 'solid';
|
|
269
|
+
lines.push(` "${edge.source}" -> "${edge.target}" [label="${edge.type}", style=${style}];`);
|
|
270
|
+
}
|
|
271
|
+
lines.push('}');
|
|
272
|
+
console.log(lines.join('\n'));
|
|
273
|
+
}
|
|
274
|
+
}
|