@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.
Files changed (124) hide show
  1. package/dist/bin.js +996 -79
  2. package/dist/bin.js.map +1 -1
  3. package/dist/{chunk-ICAIQ57V.js → chunk-6JBGU74P.js} +5 -3
  4. package/dist/chunk-6JBGU74P.js.map +1 -0
  5. package/dist/chunk-7OPWMLOE.js +1625 -0
  6. package/dist/chunk-7OPWMLOE.js.map +1 -0
  7. package/dist/{chunk-2H2JAA3U.js → chunk-CVXKXVOY.js} +3 -3
  8. package/dist/{chunk-2H2JAA3U.js.map → chunk-CVXKXVOY.js.map} +1 -1
  9. package/dist/{chunk-IOJE35DZ.js → chunk-NWQ4CJOQ.js} +3 -3
  10. package/dist/{chunk-2DJH4F4P.js → chunk-RVRTRESS.js} +3 -3
  11. package/dist/{chunk-V7YLRR4C.js → chunk-TJ34N7C7.js} +41 -4
  12. package/dist/{chunk-V7YLRR4C.js.map → chunk-TJ34N7C7.js.map} +1 -1
  13. package/dist/{chunk-XNWDI6UT.js → chunk-XHUDJNN3.js} +5 -5
  14. package/dist/{core-DKHB7FYV.js → core-W2HYIQW6.js} +4 -4
  15. package/dist/{generate-KL24VZVD.js → generate-LMTISDIJ.js} +5 -5
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +15 -7
  18. package/dist/index.js.map +1 -1
  19. package/dist/{init-NION5S3M.js → init-7CHRKQ7P.js} +5 -5
  20. package/dist/mcp-bin.js +8 -220
  21. package/dist/mcp-bin.js.map +1 -1
  22. package/dist/scan-WY23TJCP.js +12 -0
  23. package/dist/{service-RWUMZ3EW.js → service-T2L7VLTE.js} +5 -5
  24. package/dist/static-viewer-GBR7YNF3.js +12 -0
  25. package/dist/{test-ECPEXFDN.js → test-OJRXNDO2.js} +4 -4
  26. package/dist/{tokens-ITADYVPF.js → tokens-3BWDESVM.js} +6 -6
  27. package/dist/viewer-SUFOISZM.js +1822 -0
  28. package/dist/viewer-SUFOISZM.js.map +1 -0
  29. package/package.json +6 -5
  30. package/src/bin.ts +31 -0
  31. package/src/build.ts +147 -13
  32. package/src/cli-commands.ts +18 -0
  33. package/src/commands/__tests__/a11y-scoring.test.ts +278 -0
  34. package/src/commands/a11y-report.ts +625 -0
  35. package/src/commands/a11y.ts +168 -14
  36. package/src/commands/build.ts +16 -0
  37. package/src/commands/graph.ts +274 -0
  38. package/src/core/auto-props.ts +464 -0
  39. package/src/core/composition.ts +64 -1
  40. package/src/core/graph-extractor.test.ts +542 -0
  41. package/src/core/graph-extractor.ts +601 -0
  42. package/src/core/importAnalyzer.ts +5 -0
  43. package/src/core/schema.ts +2 -0
  44. package/src/core/types.ts +3 -1
  45. package/src/index.ts +4 -0
  46. package/src/mcp/server.ts +13 -220
  47. package/src/theme/__tests__/component-contrast.test.ts +338 -0
  48. package/src/theme/__tests__/contrast-validation.test.ts +326 -0
  49. package/src/theme/contrast.test.ts +331 -0
  50. package/src/theme/contrast.ts +246 -0
  51. package/src/theme/generator.ts +213 -1
  52. package/src/theme/index.ts +16 -0
  53. package/src/theme/types.ts +51 -0
  54. package/src/viewer/__tests__/a11y-fixes.test.ts +358 -0
  55. package/src/viewer/__tests__/viewer-integration.test.ts +2 -7
  56. package/src/viewer/components/AccessibilityPanel.tsx +493 -433
  57. package/src/viewer/components/ActionCapture.tsx +1 -1
  58. package/src/viewer/components/ActionsPanel.tsx +142 -183
  59. package/src/viewer/components/App.tsx +276 -183
  60. package/src/viewer/components/BottomPanel.tsx +40 -80
  61. package/src/viewer/components/CodePanel.tsx +9 -87
  62. package/src/viewer/components/CommandPalette.tsx +117 -74
  63. package/src/viewer/components/ComponentGraph.tsx +143 -126
  64. package/src/viewer/components/ComponentHeader.tsx +46 -43
  65. package/src/viewer/components/ContractPanel.tsx +124 -117
  66. package/src/viewer/components/ErrorBoundary.tsx +47 -35
  67. package/src/viewer/components/FigmaEmbed.tsx +18 -13
  68. package/src/viewer/components/FragmentEditor.tsx +126 -63
  69. package/src/viewer/components/HealthDashboard.tsx +146 -171
  70. package/src/viewer/components/HmrStatusIndicator.tsx +31 -41
  71. package/src/viewer/components/Icons.tsx +151 -98
  72. package/src/viewer/components/InteractionsPanel.tsx +317 -264
  73. package/src/viewer/components/IsolatedPreviewFrame.tsx +52 -27
  74. package/src/viewer/components/IsolatedRender.tsx +12 -6
  75. package/src/viewer/components/KeyboardShortcutsHelp.tsx +34 -70
  76. package/src/viewer/components/LandingPage.tsx +285 -305
  77. package/src/viewer/components/Layout.tsx +12 -10
  78. package/src/viewer/components/LeftSidebar.tsx +103 -155
  79. package/src/viewer/components/MultiViewportPreview.tsx +254 -63
  80. package/src/viewer/components/PreviewArea.tsx +113 -44
  81. package/src/viewer/components/PreviewFrameHost.tsx +36 -6
  82. package/src/viewer/components/PreviewPane.tsx +2 -3
  83. package/src/viewer/components/PreviewToolbar.tsx +109 -105
  84. package/src/viewer/components/PropsEditor.tsx +154 -74
  85. package/src/viewer/components/PropsTable.tsx +95 -82
  86. package/src/viewer/components/RelationsSection.tsx +71 -40
  87. package/src/viewer/components/ResizablePanel.tsx +158 -55
  88. package/src/viewer/components/RightSidebar.tsx +46 -56
  89. package/src/viewer/components/ScreenshotButton.tsx +12 -12
  90. package/src/viewer/components/SkeletonLoader.tsx +99 -83
  91. package/src/viewer/components/StoryRenderer.tsx +4 -11
  92. package/src/viewer/components/Toast.tsx +3 -67
  93. package/src/viewer/components/TokenStylePanel.tsx +136 -118
  94. package/src/viewer/components/UsageSection.tsx +26 -26
  95. package/src/viewer/components/VariantMatrix.tsx +140 -47
  96. package/src/viewer/components/VariantTabs.tsx +24 -68
  97. package/src/viewer/components/ViewportSelector.tsx +121 -114
  98. package/src/viewer/constants/ui.ts +23 -22
  99. package/src/viewer/entry.tsx +8 -3
  100. package/src/viewer/index.ts +3 -6
  101. package/src/viewer/preview-frame.html +43 -18
  102. package/src/viewer/server.ts +7 -16
  103. package/src/viewer/styles/globals.css +46 -85
  104. package/src/viewer/utils/a11y-fixes.ts +53 -30
  105. package/dist/chunk-ICAIQ57V.js.map +0 -1
  106. package/dist/chunk-U4GQ2JTD.js +0 -832
  107. package/dist/chunk-U4GQ2JTD.js.map +0 -1
  108. package/dist/scan-ESEXV7LF.js +0 -12
  109. package/dist/static-viewer-O37MJ5B6.js +0 -12
  110. package/dist/viewer-YDGFDTK5.js +0 -11104
  111. package/dist/viewer-YDGFDTK5.js.map +0 -1
  112. package/src/viewer/postcss.config.js +0 -6
  113. package/src/viewer/tailwind.config.js +0 -37
  114. /package/dist/{chunk-IOJE35DZ.js.map → chunk-NWQ4CJOQ.js.map} +0 -0
  115. /package/dist/{chunk-2DJH4F4P.js.map → chunk-RVRTRESS.js.map} +0 -0
  116. /package/dist/{chunk-XNWDI6UT.js.map → chunk-XHUDJNN3.js.map} +0 -0
  117. /package/dist/{core-DKHB7FYV.js.map → core-W2HYIQW6.js.map} +0 -0
  118. /package/dist/{generate-KL24VZVD.js.map → generate-LMTISDIJ.js.map} +0 -0
  119. /package/dist/{init-NION5S3M.js.map → init-7CHRKQ7P.js.map} +0 -0
  120. /package/dist/{scan-ESEXV7LF.js.map → scan-WY23TJCP.js.map} +0 -0
  121. /package/dist/{service-RWUMZ3EW.js.map → service-T2L7VLTE.js.map} +0 -0
  122. /package/dist/{static-viewer-O37MJ5B6.js.map → static-viewer-GBR7YNF3.js.map} +0 -0
  123. /package/dist/{test-ECPEXFDN.js.map → test-OJRXNDO2.js.map} +0 -0
  124. /package/dist/{tokens-ITADYVPF.js.map → tokens-3BWDESVM.js.map} +0 -0
@@ -1,832 +0,0 @@
1
- import { createRequire } from 'module'; const require = createRequire(import.meta.url);
2
- import {
3
- BrowserPool,
4
- CaptureEngine,
5
- DiffEngine,
6
- StorageManager,
7
- analyzeDesignSystem,
8
- formatMs,
9
- generateHtmlReport,
10
- getGrade
11
- } from "./chunk-IOJE35DZ.js";
12
- import {
13
- discoverBlockFiles,
14
- discoverComponentFiles,
15
- discoverSegmentFiles,
16
- discoverTokenFiles,
17
- extractComponentName,
18
- generateContextMd,
19
- generateRegistry,
20
- loadSegmentFile,
21
- parseSegmentFile
22
- } from "./chunk-2H2JAA3U.js";
23
- import {
24
- compileBlock,
25
- parseTokenFile
26
- } from "./chunk-V7YLRR4C.js";
27
- import {
28
- BRAND,
29
- DEFAULTS,
30
- segmentDefinitionSchema
31
- } from "./chunk-ICAIQ57V.js";
32
-
33
- // src/validators.ts
34
- async function validateSchema(config, configDir) {
35
- const files = await discoverSegmentFiles(config, configDir);
36
- const errors = [];
37
- const warnings = [];
38
- for (const file of files) {
39
- try {
40
- const segment = await loadSegmentFile(file.absolutePath);
41
- if (!segment) {
42
- errors.push({
43
- file: file.relativePath,
44
- message: "No default export found",
45
- details: `Segment files must have a default export from defineSegment()`
46
- });
47
- continue;
48
- }
49
- const result = segmentDefinitionSchema.safeParse(segment);
50
- if (!result.success) {
51
- const details = result.error.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join("; ");
52
- errors.push({
53
- file: file.relativePath,
54
- message: "Invalid segment schema",
55
- details
56
- });
57
- }
58
- } catch (error) {
59
- errors.push({
60
- file: file.relativePath,
61
- message: "Failed to load segment file",
62
- details: error instanceof Error ? error.message : String(error)
63
- });
64
- }
65
- }
66
- return {
67
- valid: errors.length === 0,
68
- errors,
69
- warnings
70
- };
71
- }
72
- async function validateCoverage(config, configDir) {
73
- const segmentFiles = await discoverSegmentFiles(config, configDir);
74
- const componentFiles = await discoverComponentFiles(config, configDir);
75
- const errors = [];
76
- const warnings = [];
77
- const documentedComponents = /* @__PURE__ */ new Set();
78
- for (const file of segmentFiles) {
79
- try {
80
- const segment = await loadSegmentFile(file.absolutePath);
81
- if (segment?.meta?.name) {
82
- documentedComponents.add(segment.meta.name);
83
- }
84
- } catch {
85
- }
86
- }
87
- for (const file of componentFiles) {
88
- const componentName = extractComponentName(file.relativePath);
89
- const segmentPath = file.relativePath.replace(
90
- /\.(tsx?|jsx?)$/,
91
- BRAND.fileExtension
92
- );
93
- const hasSegmentFile = segmentFiles.some(
94
- (s) => s.relativePath === segmentPath
95
- );
96
- if (!hasSegmentFile && !documentedComponents.has(componentName)) {
97
- warnings.push({
98
- file: file.relativePath,
99
- message: `Component "${componentName}" has no segment documentation`
100
- });
101
- }
102
- }
103
- return {
104
- valid: errors.length === 0,
105
- errors,
106
- warnings
107
- };
108
- }
109
- async function validateAll(config, configDir) {
110
- const [schemaResult, coverageResult] = await Promise.all([
111
- validateSchema(config, configDir),
112
- validateCoverage(config, configDir)
113
- ]);
114
- return {
115
- valid: schemaResult.valid && coverageResult.valid,
116
- errors: [...schemaResult.errors, ...coverageResult.errors],
117
- warnings: [...schemaResult.warnings, ...coverageResult.warnings]
118
- };
119
- }
120
-
121
- // src/build.ts
122
- import { readFile, writeFile, mkdir } from "fs/promises";
123
- import { resolve, join } from "path";
124
- import { existsSync } from "fs";
125
- async function buildSegments(config, configDir) {
126
- const files = await discoverSegmentFiles(config, configDir);
127
- const errors = [];
128
- const warnings = [];
129
- const segments = {};
130
- for (const file of files) {
131
- try {
132
- const content = await readFile(file.absolutePath, "utf-8");
133
- const parsed = parseSegmentFile(content, file.relativePath);
134
- for (const warning of parsed.warnings) {
135
- warnings.push({ file: file.relativePath, warning });
136
- }
137
- if (!parsed.meta.name) {
138
- errors.push({
139
- file: file.relativePath,
140
- error: "Missing meta.name in fragment definition"
141
- });
142
- continue;
143
- }
144
- const compiled = {
145
- filePath: file.relativePath,
146
- meta: {
147
- name: parsed.meta.name,
148
- description: parsed.meta.description ?? "",
149
- category: parsed.meta.category ?? "Uncategorized",
150
- status: parsed.meta.status,
151
- tags: parsed.meta.tags,
152
- since: parsed.meta.since,
153
- figma: parsed.meta.figma
154
- },
155
- usage: {
156
- when: parsed.usage.when ?? [],
157
- whenNot: parsed.usage.whenNot ?? [],
158
- guidelines: parsed.usage.guidelines,
159
- accessibility: parsed.usage.accessibility
160
- },
161
- props: Object.fromEntries(
162
- Object.entries(parsed.props).map(([name, prop]) => [
163
- name,
164
- {
165
- type: prop.type ?? "custom",
166
- description: prop.description ?? "",
167
- default: prop.default,
168
- required: prop.required,
169
- values: prop.values,
170
- constraints: prop.constraints
171
- }
172
- ])
173
- ),
174
- relations: parsed.relations.map((rel) => ({
175
- component: rel.component,
176
- relationship: rel.relationship,
177
- note: rel.note
178
- })),
179
- variants: parsed.variants.map((v) => ({
180
- name: v.name,
181
- description: v.description,
182
- ...v.code && { code: v.code },
183
- ...v.figma && { figma: v.figma },
184
- ...v.args && { args: v.args }
185
- })),
186
- // Include AI metadata if present
187
- ...parsed.ai && { ai: parsed.ai }
188
- };
189
- segments[parsed.meta.name] = compiled;
190
- } catch (error) {
191
- errors.push({
192
- file: file.relativePath,
193
- error: error instanceof Error ? error.message : String(error)
194
- });
195
- }
196
- }
197
- const blocks = {};
198
- try {
199
- const blockFiles = await discoverBlockFiles(configDir, config.exclude);
200
- for (const file of blockFiles) {
201
- try {
202
- let raw = await loadSegmentFile(file.absolutePath);
203
- if (raw && "default" in raw && typeof raw.default === "object") {
204
- raw = raw.default;
205
- }
206
- const def = raw;
207
- if (def && typeof def === "object" && "name" in def && "code" in def && "components" in def) {
208
- const compiled = compileBlock(def, file.relativePath);
209
- blocks[compiled.name] = compiled;
210
- }
211
- } catch (error) {
212
- warnings.push({
213
- file: file.relativePath,
214
- warning: `Failed to load block: ${error instanceof Error ? error.message : String(error)}`
215
- });
216
- }
217
- }
218
- } catch {
219
- }
220
- let tokens;
221
- try {
222
- const tokenPatterns = config.tokens?.include;
223
- const tokenFiles = await discoverTokenFiles(configDir, tokenPatterns, config.exclude);
224
- if (tokenFiles.length > 0) {
225
- const mergedCategories = {};
226
- let prefix = "--";
227
- let total = 0;
228
- for (const file of tokenFiles) {
229
- const content = await readFile(file.absolutePath, "utf-8");
230
- const parsed = parseTokenFile(content, file.relativePath);
231
- prefix = parsed.prefix;
232
- total += parsed.total;
233
- for (const [cat, catTokens] of Object.entries(parsed.categories)) {
234
- if (!mergedCategories[cat]) {
235
- mergedCategories[cat] = [];
236
- }
237
- for (const t of catTokens) {
238
- if (!mergedCategories[cat].some((e) => e.name === t.name)) {
239
- mergedCategories[cat].push({ name: t.name, description: t.description });
240
- }
241
- }
242
- }
243
- }
244
- if (total > 0) {
245
- tokens = { prefix, total, categories: mergedCategories };
246
- }
247
- }
248
- } catch {
249
- }
250
- let packageName;
251
- const pkgJsonPath = resolve(configDir, "package.json");
252
- if (existsSync(pkgJsonPath)) {
253
- try {
254
- const pkg = JSON.parse(await readFile(pkgJsonPath, "utf-8"));
255
- if (pkg.name) packageName = pkg.name;
256
- } catch {
257
- }
258
- }
259
- const output = {
260
- version: "1.0.0",
261
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
262
- ...packageName && { packageName },
263
- segments,
264
- ...Object.keys(blocks).length > 0 && { blocks },
265
- ...tokens && { tokens }
266
- };
267
- const outputPath = resolve(configDir, config.outFile ?? BRAND.outFile);
268
- await writeFile(outputPath, JSON.stringify(output));
269
- return {
270
- success: errors.length === 0,
271
- outputPath,
272
- segmentCount: Object.keys(segments).length,
273
- errors,
274
- warnings
275
- };
276
- }
277
- async function buildFragmentsDir(config, configDir) {
278
- const fragmentsDir = join(configDir, BRAND.dataDir);
279
- const componentsDir = join(fragmentsDir, BRAND.componentsDir);
280
- await mkdir(fragmentsDir, { recursive: true });
281
- await mkdir(componentsDir, { recursive: true });
282
- const registryResult = await generateRegistry({
283
- projectRoot: configDir,
284
- componentPatterns: config.components || ["src/**/*.tsx", "src/**/*.ts"],
285
- storyPatterns: config.include || ["src/**/*.stories.tsx"],
286
- fragmentsDir,
287
- registryOptions: config.registry || {}
288
- });
289
- const errors = [...registryResult.errors];
290
- const warnings = [...registryResult.warnings];
291
- const indexPath = join(fragmentsDir, "index.json");
292
- await writeFile(indexPath, JSON.stringify(registryResult.index, null, 2));
293
- const registryPath = join(fragmentsDir, BRAND.registryFile);
294
- await writeFile(registryPath, JSON.stringify(registryResult.registry, null, 2));
295
- const contextResult = generateContextMd(registryResult.registry, {
296
- format: "markdown",
297
- compact: false,
298
- include: {
299
- props: false,
300
- // AI can read TypeScript directly
301
- relations: true,
302
- code: false
303
- }
304
- });
305
- const contextPath = join(fragmentsDir, BRAND.contextFile);
306
- await writeFile(contextPath, contextResult.content);
307
- return {
308
- success: errors.length === 0,
309
- indexPath,
310
- registryPath,
311
- contextPath,
312
- componentCount: registryResult.registry.componentCount,
313
- errors,
314
- warnings
315
- };
316
- }
317
-
318
- // src/screenshot.ts
319
- import pc from "picocolors";
320
- async function runScreenshotCommand(config, configDir, options = {}) {
321
- const startTime = Date.now();
322
- const errors = [];
323
- const storage = new StorageManager({
324
- projectRoot: configDir,
325
- viewport: options.width && options.height ? { width: options.width, height: options.height } : config.screenshots?.viewport
326
- });
327
- await storage.initialize();
328
- const segmentFiles = await discoverSegmentFiles(config, configDir);
329
- if (segmentFiles.length === 0) {
330
- console.log(pc.yellow("No segment files found."));
331
- return {
332
- success: true,
333
- captured: 0,
334
- skipped: 0,
335
- errors: [],
336
- totalTimeMs: Date.now() - startTime
337
- };
338
- }
339
- const segments = [];
340
- for (const file of segmentFiles) {
341
- try {
342
- const segment = await loadSegmentFile(file.absolutePath);
343
- if (segment) {
344
- segments.push({ path: file.relativePath, segment });
345
- }
346
- } catch (error) {
347
- errors.push({
348
- component: file.relativePath,
349
- variant: "",
350
- error: error instanceof Error ? error.message : String(error)
351
- });
352
- }
353
- }
354
- const filteredSegments = options.component ? segments.filter((s) => s.segment.meta.name === options.component) : segments;
355
- if (options.component && filteredSegments.length === 0) {
356
- console.log(pc.yellow(`Component "${options.component}" not found.`));
357
- return {
358
- success: false,
359
- captured: 0,
360
- skipped: 0,
361
- errors: [],
362
- totalTimeMs: Date.now() - startTime
363
- };
364
- }
365
- const variantsToCapture = [];
366
- for (const { segment } of filteredSegments) {
367
- const variants = options.variant ? segment.variants.filter((v) => v.name === options.variant) : segment.variants;
368
- for (const variant of variants) {
369
- variantsToCapture.push({
370
- component: segment.meta.name,
371
- variant: variant.name,
372
- render: variant.render
373
- });
374
- }
375
- }
376
- if (variantsToCapture.length === 0) {
377
- console.log(pc.yellow("No variants to capture."));
378
- return {
379
- success: true,
380
- captured: 0,
381
- skipped: 0,
382
- errors: [],
383
- totalTimeMs: Date.now() - startTime
384
- };
385
- }
386
- const theme = options.theme ?? DEFAULTS.theme;
387
- const viewport = {
388
- width: options.width ?? config.screenshots?.viewport?.width ?? DEFAULTS.viewport.width,
389
- height: options.height ?? config.screenshots?.viewport?.height ?? DEFAULTS.viewport.height
390
- };
391
- console.log(pc.cyan(`
392
- ${BRAND.name} Screenshot
393
- `));
394
- console.log(pc.dim(`Capturing variants (theme: ${theme}, viewport: ${viewport.width}x${viewport.height}):
395
- `));
396
- const pool = new BrowserPool({
397
- viewport
398
- });
399
- const viewerPort = DEFAULTS.port;
400
- const baseUrl = `http://localhost:${viewerPort}`;
401
- const captureEngine = new CaptureEngine(pool, baseUrl);
402
- let captured = 0;
403
- let skipped = 0;
404
- const captureOptions = {
405
- theme,
406
- viewport,
407
- delay: config.screenshots?.delay ?? DEFAULTS.captureDelayMs
408
- };
409
- try {
410
- console.log(pc.dim("Starting browser..."));
411
- await pool.warmup();
412
- console.log(pc.dim("Browser ready.\n"));
413
- for (const { component, variant } of variantsToCapture) {
414
- const hasExisting = storage.hasBaseline(component, variant, theme);
415
- if (hasExisting && !options.update) {
416
- console.log(` ${pc.dim("\u25CB")} ${component}/${variant} ${pc.dim("(skipped)")}`);
417
- skipped++;
418
- continue;
419
- }
420
- try {
421
- const screenshot = await captureEngine.captureVariant(
422
- component,
423
- variant,
424
- captureOptions
425
- );
426
- await storage.saveBaseline(screenshot);
427
- const totalTime = screenshot.metadata.renderTimeMs + screenshot.metadata.captureTimeMs;
428
- console.log(
429
- ` ${pc.green("\u2713")} ${component}/${variant} ${pc.dim(formatMs(totalTime))}`
430
- );
431
- captured++;
432
- } catch (error) {
433
- const errorMsg = error instanceof Error ? error.message : String(error);
434
- console.log(` ${pc.red("\u2717")} ${component}/${variant} ${pc.dim(errorMsg)}`);
435
- errors.push({ component, variant, error: errorMsg });
436
- }
437
- }
438
- } finally {
439
- await pool.shutdown();
440
- }
441
- const totalTimeMs = Date.now() - startTime;
442
- console.log();
443
- if (errors.length === 0) {
444
- console.log(pc.green(`\u2713 Captured ${captured} screenshot(s) in ${formatMs(totalTimeMs)}`));
445
- } else {
446
- console.log(pc.yellow(`\u26A0 Captured ${captured} screenshot(s) with ${errors.length} error(s)`));
447
- }
448
- if (skipped > 0) {
449
- console.log(pc.dim(` ${skipped} skipped (use --update to recapture)`));
450
- }
451
- console.log(pc.dim(` Stored in ${storage.screenshotsDirPath}
452
- `));
453
- return {
454
- success: errors.length === 0,
455
- captured,
456
- skipped,
457
- errors,
458
- totalTimeMs
459
- };
460
- }
461
-
462
- // src/diff.ts
463
- import pc2 from "picocolors";
464
- async function runDiffCommand(config, configDir, options = {}) {
465
- const startTime = Date.now();
466
- const results = [];
467
- const storage = new StorageManager({
468
- projectRoot: configDir,
469
- viewport: config.screenshots?.viewport
470
- });
471
- await storage.initialize();
472
- const threshold = options.threshold ?? config.screenshots?.threshold ?? DEFAULTS.diffThreshold;
473
- const diffEngine = new DiffEngine(threshold);
474
- const segmentFiles = await discoverSegmentFiles(config, configDir);
475
- if (segmentFiles.length === 0) {
476
- console.log(pc2.yellow("No segment files found."));
477
- return {
478
- success: true,
479
- total: 0,
480
- passed: 0,
481
- failed: 0,
482
- missing: 0,
483
- results: [],
484
- totalTimeMs: Date.now() - startTime
485
- };
486
- }
487
- const segments = [];
488
- for (const file of segmentFiles) {
489
- try {
490
- const segment = await loadSegmentFile(file.absolutePath);
491
- if (segment) {
492
- segments.push({ path: file.relativePath, segment });
493
- }
494
- } catch {
495
- }
496
- }
497
- const filteredSegments = options.component ? segments.filter((s) => s.segment.meta.name === options.component) : segments;
498
- if (options.component && filteredSegments.length === 0) {
499
- console.log(pc2.yellow(`Component "${options.component}" not found.`));
500
- return {
501
- success: false,
502
- total: 0,
503
- passed: 0,
504
- failed: 0,
505
- missing: 0,
506
- results: [],
507
- totalTimeMs: Date.now() - startTime
508
- };
509
- }
510
- const variantsToDiff = [];
511
- for (const { segment } of filteredSegments) {
512
- const variants = options.variant ? segment.variants.filter((v) => v.name === options.variant) : segment.variants;
513
- for (const variant of variants) {
514
- variantsToDiff.push({
515
- component: segment.meta.name,
516
- variant: variant.name
517
- });
518
- }
519
- }
520
- if (variantsToDiff.length === 0) {
521
- console.log(pc2.yellow("No variants to compare."));
522
- return {
523
- success: true,
524
- total: 0,
525
- passed: 0,
526
- failed: 0,
527
- missing: 0,
528
- results: [],
529
- totalTimeMs: Date.now() - startTime
530
- };
531
- }
532
- const theme = options.theme ?? DEFAULTS.theme;
533
- const viewport = config.screenshots?.viewport ?? DEFAULTS.viewport;
534
- console.log(pc2.cyan(`
535
- ${BRAND.name} Diff
536
- `));
537
- console.log(pc2.dim(`Comparing against baselines (theme: ${theme}, threshold: ${threshold}%):
538
- `));
539
- const pool = new BrowserPool({
540
- viewport
541
- });
542
- const viewerPort = DEFAULTS.port;
543
- const baseUrl = `http://localhost:${viewerPort}`;
544
- const captureEngine = new CaptureEngine(pool, baseUrl);
545
- let passed = 0;
546
- let failed = 0;
547
- let missing = 0;
548
- const captureOptions = {
549
- theme,
550
- viewport,
551
- delay: config.screenshots?.delay ?? DEFAULTS.captureDelayMs
552
- };
553
- try {
554
- await pool.warmup();
555
- for (const { component, variant } of variantsToDiff) {
556
- const baseline = await storage.loadBaseline(component, variant, theme);
557
- if (!baseline) {
558
- console.log(
559
- ` ${pc2.yellow("?")} ${component}/${variant} ${pc2.dim("(no baseline)")}`
560
- );
561
- missing++;
562
- continue;
563
- }
564
- try {
565
- const current = await captureEngine.captureVariant(
566
- component,
567
- variant,
568
- captureOptions
569
- );
570
- if (diffEngine.areIdentical(current, baseline)) {
571
- console.log(` ${pc2.green("\u2713")} ${component}/${variant} ${pc2.dim("0.0%")}`);
572
- results.push({
573
- component,
574
- variant,
575
- theme,
576
- result: {
577
- matches: true,
578
- diffPercentage: 0,
579
- diffPixelCount: 0,
580
- totalPixels: current.viewport.width * current.viewport.height,
581
- changedRegions: [],
582
- diffTimeMs: 0
583
- }
584
- });
585
- passed++;
586
- continue;
587
- }
588
- const diffResult = diffEngine.compare(current, baseline, { threshold });
589
- if (diffResult.matches) {
590
- console.log(
591
- ` ${pc2.green("\u2713")} ${component}/${variant} ${pc2.dim(`${diffResult.diffPercentage}%`)}`
592
- );
593
- passed++;
594
- } else {
595
- let diffImagePath;
596
- if (diffResult.diffImage) {
597
- diffImagePath = await storage.saveDiff(
598
- component,
599
- variant,
600
- theme,
601
- diffResult.diffImage
602
- );
603
- }
604
- console.log(
605
- ` ${pc2.red("\u2717")} ${component}/${variant} ${pc2.yellow(`${diffResult.diffPercentage}%`)}` + (diffImagePath ? pc2.dim(` \u2192 ${diffImagePath}`) : "")
606
- );
607
- failed++;
608
- results.push({
609
- component,
610
- variant,
611
- theme,
612
- result: diffResult,
613
- diffImagePath
614
- });
615
- continue;
616
- }
617
- results.push({
618
- component,
619
- variant,
620
- theme,
621
- result: diffResult
622
- });
623
- } catch (error) {
624
- const errorMsg = error instanceof Error ? error.message : String(error);
625
- console.log(` ${pc2.red("!")} ${component}/${variant} ${pc2.dim(errorMsg)}`);
626
- failed++;
627
- }
628
- }
629
- } finally {
630
- await pool.shutdown();
631
- }
632
- const totalTimeMs = Date.now() - startTime;
633
- const total = passed + failed + missing;
634
- console.log();
635
- if (failed === 0 && missing === 0) {
636
- console.log(pc2.green(`\u2713 All ${passed} variant(s) match baselines`));
637
- } else if (failed > 0) {
638
- console.log(pc2.red(`\u2717 ${failed} variant(s) differ from baselines`));
639
- }
640
- if (missing > 0) {
641
- console.log(pc2.yellow(` ${missing} variant(s) have no baseline (run \`${BRAND.cliCommand} screenshot\`)`));
642
- }
643
- console.log(pc2.dim(` Completed in ${formatMs(totalTimeMs)}
644
- `));
645
- const success = failed === 0;
646
- return {
647
- success,
648
- total,
649
- passed,
650
- failed,
651
- missing,
652
- results,
653
- totalTimeMs
654
- };
655
- }
656
-
657
- // src/analyze.ts
658
- import { existsSync as existsSync2 } from "fs";
659
- import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
660
- import { join as join2, dirname } from "path";
661
- import pc3 from "picocolors";
662
- async function runAnalyzeCommand(config, configDir, options = {}) {
663
- const format = options.format ?? "html";
664
- const minScore = options.minScore ?? 0;
665
- console.log(pc3.cyan(`
666
- ${BRAND.name} Analyzer
667
- `));
668
- const segmentsPath = join2(configDir, config.outFile ?? "segments.json");
669
- if (!existsSync2(segmentsPath)) {
670
- console.log(pc3.red(`\u2717 No segments.json found. Run \`${BRAND.cliCommand} build\` first.
671
- `));
672
- return {
673
- success: false,
674
- analytics: createEmptyAnalytics()
675
- };
676
- }
677
- console.log(pc3.dim("Analyzing design system...\n"));
678
- const content = await readFile2(segmentsPath, "utf-8");
679
- const data = JSON.parse(content);
680
- const analytics = analyzeDesignSystem(data);
681
- printConsoleSummary(analytics);
682
- let outputPath;
683
- if (format === "html" || format === "json") {
684
- outputPath = options.output ?? getDefaultOutputPath(format, configDir);
685
- await mkdir2(dirname(outputPath), { recursive: true });
686
- if (format === "html") {
687
- const html = generateHtmlReport(analytics);
688
- await writeFile2(outputPath, html);
689
- console.log(pc3.green(`\u2713 Report generated: ${outputPath}
690
- `));
691
- } else {
692
- await writeFile2(outputPath, JSON.stringify(analytics, null, 2));
693
- console.log(pc3.green(`\u2713 JSON report generated: ${outputPath}
694
- `));
695
- }
696
- if (options.open && format === "html") {
697
- await openInBrowser(outputPath);
698
- }
699
- }
700
- const passedCi = analytics.summary.overallScore >= minScore;
701
- if (options.ci) {
702
- if (passedCi) {
703
- console.log(
704
- pc3.green(`\u2713 Score ${analytics.summary.overallScore} meets minimum threshold ${minScore}
705
- `)
706
- );
707
- } else {
708
- console.log(
709
- pc3.red(
710
- `\u2717 Score ${analytics.summary.overallScore} below minimum threshold ${minScore}
711
- `
712
- )
713
- );
714
- }
715
- }
716
- return {
717
- success: !options.ci || passedCi,
718
- analytics,
719
- outputPath
720
- };
721
- }
722
- function printConsoleSummary(analytics) {
723
- const { summary, coverage, recommendations } = analytics;
724
- const grade = getGrade(summary.overallScore);
725
- console.log(
726
- pc3.bold(
727
- `Overall Score: ${colorizeScore(summary.overallScore)} (${grade})
728
- `
729
- )
730
- );
731
- console.log(pc3.dim("Summary"));
732
- console.log(` Components: ${pc3.white(summary.totalComponents.toString())}`);
733
- console.log(` Variants: ${pc3.white(summary.totalVariants.toString())}`);
734
- console.log(` Props: ${pc3.white(summary.totalProps.toString())}`);
735
- console.log(` Categories: ${pc3.white(summary.categories.join(", "))}`);
736
- console.log();
737
- console.log(pc3.dim("Coverage"));
738
- console.log(` Description: ${formatCoverage(coverage.fields.description)}`);
739
- console.log(` Usage when: ${formatCoverage(coverage.fields.usageWhen)}`);
740
- console.log(` Usage whenNot:${formatCoverage(coverage.fields.usageWhenNot)}`);
741
- console.log(` Guidelines: ${formatCoverage(coverage.fields.guidelines)}`);
742
- console.log(` Relations: ${formatCoverage(coverage.fields.relations)}`);
743
- console.log();
744
- if (recommendations.length > 0) {
745
- console.log(pc3.dim("Top Recommendations"));
746
- for (const rec of recommendations.slice(0, 3)) {
747
- const priority = rec.priority === "high" ? pc3.red(`[${rec.priority}]`) : rec.priority === "medium" ? pc3.yellow(`[${rec.priority}]`) : pc3.dim(`[${rec.priority}]`);
748
- console.log(` ${priority} ${rec.title}`);
749
- }
750
- console.log();
751
- }
752
- }
753
- function formatCoverage(field) {
754
- const pct = colorizeScore(field.percentage);
755
- return `${pct} (${field.covered}/${field.total})`;
756
- }
757
- function colorizeScore(score) {
758
- if (score >= 80) return pc3.green(`${score}%`);
759
- if (score >= 60) return pc3.yellow(`${score}%`);
760
- return pc3.red(`${score}%`);
761
- }
762
- function getDefaultOutputPath(format, configDir) {
763
- const filename = format === "html" ? "segments-report.html" : "segments-report.json";
764
- return join2(configDir, filename);
765
- }
766
- async function openInBrowser(path) {
767
- const { platform } = await import("os");
768
- const { exec } = await import("child_process");
769
- const os = platform();
770
- const cmd = os === "darwin" ? `open "${path}"` : os === "win32" ? `start "" "${path}"` : `xdg-open "${path}"`;
771
- exec(cmd);
772
- }
773
- function createEmptyAnalytics() {
774
- return {
775
- analyzedAt: /* @__PURE__ */ new Date(),
776
- summary: {
777
- totalComponents: 0,
778
- totalVariants: 0,
779
- totalProps: 0,
780
- categories: [],
781
- overallScore: 0
782
- },
783
- inventory: {
784
- byCategory: {},
785
- byStatus: {},
786
- byVariantCount: [],
787
- byPropCount: []
788
- },
789
- coverage: {
790
- overall: 0,
791
- fields: {
792
- description: { covered: 0, total: 0, percentage: 0 },
793
- usageWhen: { covered: 0, total: 0, percentage: 0 },
794
- usageWhenNot: { covered: 0, total: 0, percentage: 0 },
795
- guidelines: { covered: 0, total: 0, percentage: 0 },
796
- accessibility: { covered: 0, total: 0, percentage: 0 },
797
- relations: { covered: 0, total: 0, percentage: 0 },
798
- propDescriptions: { covered: 0, total: 0, percentage: 0 },
799
- propConstraints: { covered: 0, total: 0, percentage: 0 }
800
- },
801
- incomplete: []
802
- },
803
- quality: {
804
- missingWhenNot: [],
805
- isolated: [],
806
- deprecated: [],
807
- fewVariants: [],
808
- undocumentedProps: [],
809
- unconstrainedProps: []
810
- },
811
- distribution: {
812
- variantsPerComponent: [],
813
- propsPerComponent: [],
814
- componentsPerCategory: [],
815
- statusDistribution: [],
816
- tagFrequency: []
817
- },
818
- recommendations: []
819
- };
820
- }
821
-
822
- export {
823
- validateSchema,
824
- validateCoverage,
825
- validateAll,
826
- buildSegments,
827
- buildFragmentsDir,
828
- runScreenshotCommand,
829
- runDiffCommand,
830
- runAnalyzeCommand
831
- };
832
- //# sourceMappingURL=chunk-U4GQ2JTD.js.map