@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,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fragments baseline - Manage visual regression baselines
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readdir, rm, mkdir } from 'node:fs/promises';
|
|
6
|
+
import { join, relative } from 'node:path';
|
|
7
|
+
import pc from 'picocolors';
|
|
8
|
+
import { BRAND, type Theme } from '../core/index.js';
|
|
9
|
+
import { loadConfig } from '../core/node.js';
|
|
10
|
+
import { runScreenshotCommand } from '../screenshot.js';
|
|
11
|
+
import { StorageManager } from '../service/index.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Options for baseline command
|
|
15
|
+
*/
|
|
16
|
+
export interface BaselineOptions {
|
|
17
|
+
/** Path to config file */
|
|
18
|
+
config?: string;
|
|
19
|
+
/** Specific variant to update */
|
|
20
|
+
variant?: string;
|
|
21
|
+
/** Update/delete all baselines */
|
|
22
|
+
all?: boolean;
|
|
23
|
+
/** Theme for baseline (light/dark) */
|
|
24
|
+
theme?: Theme;
|
|
25
|
+
/** Dev server port */
|
|
26
|
+
port?: number | string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Result of baseline command
|
|
31
|
+
*/
|
|
32
|
+
export interface BaselineResult {
|
|
33
|
+
success: boolean;
|
|
34
|
+
action: 'update' | 'list' | 'delete';
|
|
35
|
+
count?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Run the baseline command
|
|
40
|
+
*/
|
|
41
|
+
export async function baseline(
|
|
42
|
+
action: string,
|
|
43
|
+
component: string | undefined,
|
|
44
|
+
options: BaselineOptions = {}
|
|
45
|
+
): Promise<BaselineResult> {
|
|
46
|
+
const { config: configPath, variant, all = false, theme = 'light', port = 6006 } = options;
|
|
47
|
+
|
|
48
|
+
const { config, configDir } = await loadConfig(configPath);
|
|
49
|
+
|
|
50
|
+
// Initialize storage manager
|
|
51
|
+
const storage = new StorageManager({
|
|
52
|
+
projectRoot: configDir,
|
|
53
|
+
viewport: config.screenshots?.viewport,
|
|
54
|
+
});
|
|
55
|
+
await storage.initialize();
|
|
56
|
+
|
|
57
|
+
console.log(pc.cyan(`\n${BRAND.name} Baseline Manager\n`));
|
|
58
|
+
|
|
59
|
+
const baseUrl = `http://localhost:${port}`;
|
|
60
|
+
|
|
61
|
+
switch (action) {
|
|
62
|
+
case 'update':
|
|
63
|
+
return updateBaseline(component, options, config, configDir, baseUrl);
|
|
64
|
+
|
|
65
|
+
case 'list':
|
|
66
|
+
return listBaselines(configDir);
|
|
67
|
+
|
|
68
|
+
case 'delete':
|
|
69
|
+
return deleteBaseline(component, options, configDir);
|
|
70
|
+
|
|
71
|
+
default:
|
|
72
|
+
console.log(pc.red(`Unknown action: ${action}`));
|
|
73
|
+
console.log(pc.dim('Available actions: update, list, delete\n'));
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Update baselines
|
|
80
|
+
*/
|
|
81
|
+
async function updateBaseline(
|
|
82
|
+
component: string | undefined,
|
|
83
|
+
options: BaselineOptions,
|
|
84
|
+
config: any,
|
|
85
|
+
configDir: string,
|
|
86
|
+
baseUrl: string
|
|
87
|
+
): Promise<BaselineResult> {
|
|
88
|
+
const { variant, all = false, theme = 'light' as const } = options;
|
|
89
|
+
|
|
90
|
+
if (!component && !all) {
|
|
91
|
+
// Interactive mode - prompt for component selection
|
|
92
|
+
const { select } = await import('@inquirer/prompts');
|
|
93
|
+
|
|
94
|
+
// Fetch available components
|
|
95
|
+
const contextResp = await fetch(`${baseUrl}/fragments/context?format=json`);
|
|
96
|
+
if (!contextResp.ok) {
|
|
97
|
+
throw new Error('Failed to fetch fragments. Make sure dev server is running.');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const contextData = JSON.parse(await contextResp.text());
|
|
101
|
+
const segments = contextData.components || [];
|
|
102
|
+
|
|
103
|
+
if (segments.length === 0) {
|
|
104
|
+
console.log(pc.yellow('No components found.\n'));
|
|
105
|
+
return { success: true, action: 'update', count: 0 };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
component = await select({
|
|
109
|
+
message: 'Select component to update:',
|
|
110
|
+
choices: segments.map((s: { name: string }) => ({
|
|
111
|
+
name: s.name,
|
|
112
|
+
value: s.name,
|
|
113
|
+
})),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (all) {
|
|
118
|
+
// Update all baselines
|
|
119
|
+
console.log(pc.dim('Updating all baselines...\n'));
|
|
120
|
+
|
|
121
|
+
await runScreenshotCommand(config, configDir, {
|
|
122
|
+
theme,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
console.log(pc.green('\n✓ All baselines updated\n'));
|
|
126
|
+
} else if (component) {
|
|
127
|
+
// Update specific component
|
|
128
|
+
console.log(pc.dim(`Updating baselines for ${component}...\n`));
|
|
129
|
+
|
|
130
|
+
await runScreenshotCommand(config, configDir, {
|
|
131
|
+
component,
|
|
132
|
+
variant,
|
|
133
|
+
theme,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
console.log(pc.green(`\n✓ Baselines updated for ${component}\n`));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Show file paths
|
|
140
|
+
const baselinesDir = join(configDir, BRAND.dataDir, 'baselines');
|
|
141
|
+
console.log(pc.dim(`Baselines directory: ${relative(process.cwd(), baselinesDir)}\n`));
|
|
142
|
+
|
|
143
|
+
return { success: true, action: 'update' };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* List all baselines
|
|
148
|
+
*/
|
|
149
|
+
async function listBaselines(configDir: string): Promise<BaselineResult> {
|
|
150
|
+
const baselinesDir = join(configDir, BRAND.dataDir, 'baselines');
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const files = await readdir(baselinesDir, { recursive: true });
|
|
154
|
+
const pngFiles = (files as string[]).filter((f) => f.endsWith('.png'));
|
|
155
|
+
|
|
156
|
+
if (pngFiles.length === 0) {
|
|
157
|
+
console.log(pc.yellow('No baselines found.\n'));
|
|
158
|
+
console.log(pc.dim(`Run ${pc.cyan(`${BRAND.cliCommand} screenshot`)} to capture baselines.\n`));
|
|
159
|
+
return { success: true, action: 'list', count: 0 };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.log(pc.bold('Baselines:\n'));
|
|
163
|
+
for (const file of pngFiles) {
|
|
164
|
+
console.log(` ${file}`);
|
|
165
|
+
}
|
|
166
|
+
console.log();
|
|
167
|
+
console.log(pc.dim(`Total: ${pngFiles.length} baseline(s)\n`));
|
|
168
|
+
|
|
169
|
+
return { success: true, action: 'list', count: pngFiles.length };
|
|
170
|
+
} catch {
|
|
171
|
+
console.log(pc.yellow('No baselines directory found.\n'));
|
|
172
|
+
console.log(pc.dim(`Run ${pc.cyan(`${BRAND.cliCommand} screenshot`)} to capture baselines.\n`));
|
|
173
|
+
return { success: true, action: 'list', count: 0 };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Delete baselines
|
|
179
|
+
*/
|
|
180
|
+
async function deleteBaseline(
|
|
181
|
+
component: string | undefined,
|
|
182
|
+
options: BaselineOptions,
|
|
183
|
+
configDir: string
|
|
184
|
+
): Promise<BaselineResult> {
|
|
185
|
+
const { all = false } = options;
|
|
186
|
+
const baselinesDir = join(configDir, BRAND.dataDir, 'baselines');
|
|
187
|
+
|
|
188
|
+
if (all) {
|
|
189
|
+
// Delete all baselines
|
|
190
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
191
|
+
|
|
192
|
+
const confirmed = await confirm({
|
|
193
|
+
message: 'Delete ALL baselines? This cannot be undone.',
|
|
194
|
+
default: false,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
if (!confirmed) {
|
|
198
|
+
console.log(pc.dim('\nNo changes made.\n'));
|
|
199
|
+
return { success: true, action: 'delete', count: 0 };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
await rm(baselinesDir, { recursive: true, force: true });
|
|
203
|
+
console.log(pc.green('\n✓ All baselines deleted\n'));
|
|
204
|
+
return { success: true, action: 'delete' };
|
|
205
|
+
} else if (component) {
|
|
206
|
+
// Delete specific component baselines
|
|
207
|
+
const componentDir = join(baselinesDir, component);
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
await rm(componentDir, { recursive: true, force: true });
|
|
211
|
+
console.log(pc.green(`✓ Baselines deleted for ${component}\n`));
|
|
212
|
+
return { success: true, action: 'delete' };
|
|
213
|
+
} catch {
|
|
214
|
+
console.log(pc.yellow(`No baselines found for ${component}.\n`));
|
|
215
|
+
return { success: true, action: 'delete', count: 0 };
|
|
216
|
+
}
|
|
217
|
+
} else {
|
|
218
|
+
console.log(pc.yellow('Specify a component name or use --all flag.\n'));
|
|
219
|
+
return { success: false, action: 'delete' };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fragments build - Build compiled fragments.json and .fragments/ directory
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import pc from 'picocolors';
|
|
6
|
+
import { BRAND } from '../core/index.js';
|
|
7
|
+
import { loadConfig } from '../core/node.js';
|
|
8
|
+
import { buildSegments, buildFragmentsDir } from '../build.js';
|
|
9
|
+
import { scan } from './scan.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Options for build command
|
|
13
|
+
*/
|
|
14
|
+
export interface BuildOptions {
|
|
15
|
+
/** Path to config file */
|
|
16
|
+
config?: string;
|
|
17
|
+
/** Output file path */
|
|
18
|
+
output?: string;
|
|
19
|
+
/** Also generate .fragments/registry.json and context.md */
|
|
20
|
+
registry?: boolean;
|
|
21
|
+
/** Only generate .fragments/ directory (skip fragments.json) */
|
|
22
|
+
registryOnly?: boolean;
|
|
23
|
+
/** Build from source code (zero-config, no fragment files needed) */
|
|
24
|
+
fromSource?: boolean;
|
|
25
|
+
/** Skip usage analysis when building from source */
|
|
26
|
+
skipUsage?: boolean;
|
|
27
|
+
/** Skip Storybook parsing when building from source */
|
|
28
|
+
skipStorybook?: boolean;
|
|
29
|
+
/** Verbose output */
|
|
30
|
+
verbose?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Result of build command
|
|
35
|
+
*/
|
|
36
|
+
export interface BuildResult {
|
|
37
|
+
success: boolean;
|
|
38
|
+
segmentCount?: number;
|
|
39
|
+
outputPath?: string;
|
|
40
|
+
componentCount?: number;
|
|
41
|
+
registryPath?: string;
|
|
42
|
+
contextPath?: string;
|
|
43
|
+
errors: Array<{
|
|
44
|
+
file: string;
|
|
45
|
+
error: string;
|
|
46
|
+
}>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Run the build command
|
|
51
|
+
*/
|
|
52
|
+
export async function build(options: BuildOptions = {}): Promise<BuildResult> {
|
|
53
|
+
// If building from source, use the scan pipeline
|
|
54
|
+
if (options.fromSource) {
|
|
55
|
+
console.log(pc.cyan(`\n${BRAND.name} Build (from source)\n`));
|
|
56
|
+
console.log(pc.dim('Using zero-config source extraction pipeline\n'));
|
|
57
|
+
|
|
58
|
+
const scanResult = await scan({
|
|
59
|
+
config: options.config,
|
|
60
|
+
output: options.output,
|
|
61
|
+
skipUsage: options.skipUsage,
|
|
62
|
+
skipStorybook: options.skipStorybook,
|
|
63
|
+
verbose: options.verbose,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
success: scanResult.success,
|
|
68
|
+
segmentCount: scanResult.componentCount,
|
|
69
|
+
outputPath: scanResult.outputPath,
|
|
70
|
+
errors: scanResult.errors.map((e) => ({ file: e.component, error: e.error })),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const { config, configDir } = await loadConfig(options.config);
|
|
75
|
+
|
|
76
|
+
if (options.output) {
|
|
77
|
+
config.outFile = options.output;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log(pc.cyan(`\n${BRAND.name} Build\n`));
|
|
81
|
+
|
|
82
|
+
const errors: Array<{ file: string; error: string }> = [];
|
|
83
|
+
let segmentCount: number | undefined;
|
|
84
|
+
let outputPath: string | undefined;
|
|
85
|
+
let componentCount: number | undefined;
|
|
86
|
+
let registryPath: string | undefined;
|
|
87
|
+
let contextPath: string | undefined;
|
|
88
|
+
|
|
89
|
+
// Build fragments.json unless --registry-only
|
|
90
|
+
if (!options.registryOnly) {
|
|
91
|
+
console.log(pc.dim('Compiling fragments...\n'));
|
|
92
|
+
|
|
93
|
+
const result = await buildSegments(config, configDir);
|
|
94
|
+
|
|
95
|
+
if (result.errors.length > 0) {
|
|
96
|
+
console.log(pc.yellow('Build completed with errors:\n'));
|
|
97
|
+
for (const error of result.errors) {
|
|
98
|
+
console.log(` ${pc.red('✗')} ${error.file}: ${error.error}`);
|
|
99
|
+
errors.push(error);
|
|
100
|
+
}
|
|
101
|
+
console.log();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
segmentCount = result.segmentCount;
|
|
105
|
+
outputPath = result.outputPath;
|
|
106
|
+
|
|
107
|
+
console.log(pc.green(`✓ Built ${result.segmentCount} fragment(s)`));
|
|
108
|
+
console.log(pc.dim(` Output: ${result.outputPath}\n`));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Build .fragments/ directory if --registry or --registry-only
|
|
112
|
+
if (options.registry || options.registryOnly) {
|
|
113
|
+
console.log(pc.dim('Generating registry and context...\n'));
|
|
114
|
+
|
|
115
|
+
const fragmentsResult = await buildFragmentsDir(config, configDir);
|
|
116
|
+
|
|
117
|
+
if (fragmentsResult.errors.length > 0) {
|
|
118
|
+
console.log(pc.yellow('Registry build completed with errors:\n'));
|
|
119
|
+
for (const error of fragmentsResult.errors) {
|
|
120
|
+
console.log(` ${pc.red('✗')} ${error.file}: ${error.error}`);
|
|
121
|
+
errors.push(error);
|
|
122
|
+
}
|
|
123
|
+
console.log();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
componentCount = fragmentsResult.componentCount;
|
|
127
|
+
registryPath = fragmentsResult.registryPath;
|
|
128
|
+
contextPath = fragmentsResult.contextPath;
|
|
129
|
+
|
|
130
|
+
console.log(pc.green(`✓ Generated registry with ${fragmentsResult.componentCount} component(s)`));
|
|
131
|
+
console.log(pc.dim(` Registry: ${fragmentsResult.registryPath}`));
|
|
132
|
+
console.log(pc.dim(` Context: ${fragmentsResult.contextPath}\n`));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
success: errors.length === 0,
|
|
137
|
+
segmentCount,
|
|
138
|
+
outputPath,
|
|
139
|
+
componentCount,
|
|
140
|
+
registryPath,
|
|
141
|
+
contextPath,
|
|
142
|
+
errors,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fragments compare - Compare component renders against Figma designs
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import pc from 'picocolors';
|
|
6
|
+
import { writeFile, mkdir } from 'node:fs/promises';
|
|
7
|
+
import { resolve, join } from 'node:path';
|
|
8
|
+
import { BRAND } from '../core/index.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Options for compare command
|
|
12
|
+
*/
|
|
13
|
+
export interface CompareOptions {
|
|
14
|
+
/** Path to config file */
|
|
15
|
+
config?: string;
|
|
16
|
+
/** Compare specific variant */
|
|
17
|
+
variant?: string;
|
|
18
|
+
/** Figma frame URL (uses segment figma link if not provided) */
|
|
19
|
+
figma?: string;
|
|
20
|
+
/** Diff threshold percentage */
|
|
21
|
+
threshold?: number;
|
|
22
|
+
/** Compare all components with Figma links */
|
|
23
|
+
all?: boolean;
|
|
24
|
+
/** Save diff images to directory */
|
|
25
|
+
output?: string;
|
|
26
|
+
/** Dev server port */
|
|
27
|
+
port?: number | string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Compare result from server
|
|
32
|
+
*/
|
|
33
|
+
interface CompareResult {
|
|
34
|
+
match?: boolean;
|
|
35
|
+
diffPercentage?: number;
|
|
36
|
+
threshold?: number;
|
|
37
|
+
figmaUrl?: string;
|
|
38
|
+
rendered?: string;
|
|
39
|
+
figma?: string;
|
|
40
|
+
diff?: string;
|
|
41
|
+
changedRegions?: Array<{ x: number; y: number; width: number; height: number }>;
|
|
42
|
+
error?: string;
|
|
43
|
+
suggestion?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Result of compare command
|
|
48
|
+
*/
|
|
49
|
+
export interface CompareCommandResult {
|
|
50
|
+
success: boolean;
|
|
51
|
+
passed: number;
|
|
52
|
+
failed: number;
|
|
53
|
+
skipped: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Run the compare command
|
|
58
|
+
*/
|
|
59
|
+
export async function compare(
|
|
60
|
+
component: string | undefined,
|
|
61
|
+
options: CompareOptions = {}
|
|
62
|
+
): Promise<CompareCommandResult> {
|
|
63
|
+
const {
|
|
64
|
+
variant,
|
|
65
|
+
figma: figmaUrl,
|
|
66
|
+
threshold = 1.0,
|
|
67
|
+
all = false,
|
|
68
|
+
output,
|
|
69
|
+
port = 6006,
|
|
70
|
+
} = options;
|
|
71
|
+
|
|
72
|
+
// Check for Figma access token
|
|
73
|
+
if (!process.env.FIGMA_ACCESS_TOKEN) {
|
|
74
|
+
console.error(pc.red('\nFIGMA_ACCESS_TOKEN environment variable required.'));
|
|
75
|
+
console.log(pc.dim('Generate at: https://www.figma.com/developers/api#access-tokens'));
|
|
76
|
+
console.log(pc.dim(' export FIGMA_ACCESS_TOKEN=figd_xxx'));
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const baseUrl = `http://localhost:${port}`;
|
|
81
|
+
|
|
82
|
+
console.log(pc.cyan(`\n${BRAND.name} Design Verification\n`));
|
|
83
|
+
|
|
84
|
+
if (all) {
|
|
85
|
+
// Compare all components with Figma links
|
|
86
|
+
return compareAll(baseUrl, threshold, output);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Compare single or selected components
|
|
90
|
+
let componentsToCompare: string[] = [];
|
|
91
|
+
|
|
92
|
+
if (component) {
|
|
93
|
+
componentsToCompare = [component];
|
|
94
|
+
} else {
|
|
95
|
+
// No component specified - show interactive selection
|
|
96
|
+
componentsToCompare = await selectComponents(baseUrl);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (componentsToCompare.length === 0) {
|
|
100
|
+
console.log(pc.dim('\nNo components selected.'));
|
|
101
|
+
return { success: true, passed: 0, failed: 0, skipped: 0 };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Compare selected components
|
|
105
|
+
if (componentsToCompare.length === 1) {
|
|
106
|
+
console.log(pc.dim(`Comparing ${componentsToCompare[0]} to Figma design...\n`));
|
|
107
|
+
} else {
|
|
108
|
+
console.log(pc.dim(`Comparing ${componentsToCompare.length} components to Figma designs...\n`));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let passed = 0;
|
|
112
|
+
let failed = 0;
|
|
113
|
+
|
|
114
|
+
for (const comp of componentsToCompare) {
|
|
115
|
+
const response = await fetch(`${baseUrl}/segments/compare`, {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
headers: { 'Content-Type': 'application/json' },
|
|
118
|
+
body: JSON.stringify({
|
|
119
|
+
component: comp,
|
|
120
|
+
variant,
|
|
121
|
+
figmaUrl,
|
|
122
|
+
threshold,
|
|
123
|
+
figmaToken: process.env.FIGMA_ACCESS_TOKEN,
|
|
124
|
+
}),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const result = await response.json() as CompareResult;
|
|
128
|
+
|
|
129
|
+
if (result.error) {
|
|
130
|
+
failed++;
|
|
131
|
+
console.log(`${pc.red('✗')} ${pc.bold(comp)} - ${result.error}`);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (result.match) {
|
|
136
|
+
passed++;
|
|
137
|
+
console.log(`${pc.green('✓')} ${pc.bold(comp)} ${pc.dim(`${result.diffPercentage}%`)}`);
|
|
138
|
+
} else {
|
|
139
|
+
failed++;
|
|
140
|
+
console.log(`${pc.red('✗')} ${pc.bold(comp)} ${pc.yellow(`${result.diffPercentage}%`)} ${pc.dim(`(threshold: ${threshold}%)`)}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Save images if output dir specified
|
|
144
|
+
if (output && result.rendered && result.figma && result.diff) {
|
|
145
|
+
await saveImages(output, comp, result);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Summary for multiple components
|
|
150
|
+
if (componentsToCompare.length > 1) {
|
|
151
|
+
console.log();
|
|
152
|
+
console.log(pc.dim('───────────────────────────────────────'));
|
|
153
|
+
console.log(`\n${pc.green(`${passed} passed`)}, ${pc.red(`${failed} failed`)}\n`);
|
|
154
|
+
} else {
|
|
155
|
+
console.log();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (output && componentsToCompare.length > 0) {
|
|
159
|
+
console.log(pc.dim(`Images saved to: ${output}/\n`));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
success: failed === 0,
|
|
164
|
+
passed,
|
|
165
|
+
failed,
|
|
166
|
+
skipped: 0,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Compare all components with Figma links
|
|
172
|
+
*/
|
|
173
|
+
async function compareAll(
|
|
174
|
+
baseUrl: string,
|
|
175
|
+
threshold: number,
|
|
176
|
+
output?: string
|
|
177
|
+
): Promise<CompareCommandResult> {
|
|
178
|
+
console.log(pc.dim('Comparing all components with Figma links...\n'));
|
|
179
|
+
|
|
180
|
+
// Fetch the context to get all segments
|
|
181
|
+
const contextResp = await fetch(`${baseUrl}/segments/context?format=json`);
|
|
182
|
+
if (!contextResp.ok) {
|
|
183
|
+
throw new Error('Failed to fetch segments. Make sure dev server is running.');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const contextText = await contextResp.text();
|
|
187
|
+
let segments: Array<{ name: string; figma?: string }> = [];
|
|
188
|
+
try {
|
|
189
|
+
const contextData = JSON.parse(contextText);
|
|
190
|
+
segments = contextData.components || [];
|
|
191
|
+
} catch {
|
|
192
|
+
segments = [];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (segments.length === 0) {
|
|
196
|
+
console.log(pc.yellow('No components found with Figma links.'));
|
|
197
|
+
console.log(pc.dim('Add figma field to your segment definitions:'));
|
|
198
|
+
console.log(pc.dim(' meta: { figma: "https://figma.com/file/..." }'));
|
|
199
|
+
return { success: true, passed: 0, failed: 0, skipped: 0 };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let passed = 0;
|
|
203
|
+
let failed = 0;
|
|
204
|
+
let skipped = 0;
|
|
205
|
+
|
|
206
|
+
for (const seg of segments) {
|
|
207
|
+
// Skip components without figma links
|
|
208
|
+
if (!seg.figma) {
|
|
209
|
+
skipped++;
|
|
210
|
+
console.log(`${pc.dim('⏭️')} ${pc.dim(seg.name)} ${pc.dim('(no figma link)')}`);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const response = await fetch(`${baseUrl}/segments/compare`, {
|
|
216
|
+
method: 'POST',
|
|
217
|
+
headers: { 'Content-Type': 'application/json' },
|
|
218
|
+
body: JSON.stringify({
|
|
219
|
+
component: seg.name,
|
|
220
|
+
threshold,
|
|
221
|
+
figmaToken: process.env.FIGMA_ACCESS_TOKEN,
|
|
222
|
+
}),
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const result = await response.json() as CompareResult;
|
|
226
|
+
|
|
227
|
+
if (result.error) {
|
|
228
|
+
failed++;
|
|
229
|
+
console.log(`${pc.red('✗')} ${pc.bold(seg.name)} - ${result.error}`);
|
|
230
|
+
} else if (result.match) {
|
|
231
|
+
passed++;
|
|
232
|
+
console.log(`${pc.green('✓')} ${pc.bold(seg.name)} ${pc.dim(`${result.diffPercentage}%`)}`);
|
|
233
|
+
} else {
|
|
234
|
+
failed++;
|
|
235
|
+
console.log(`${pc.red('✗')} ${pc.bold(seg.name)} ${pc.yellow(`${result.diffPercentage}%`)} ${pc.dim(`(threshold: ${threshold}%)`)}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Save images if output dir specified
|
|
239
|
+
if (output && result.rendered && result.figma && result.diff) {
|
|
240
|
+
await saveImages(output, seg.name, result);
|
|
241
|
+
}
|
|
242
|
+
} catch (error) {
|
|
243
|
+
failed++;
|
|
244
|
+
console.log(`${pc.red('✗')} ${pc.bold(seg.name)} - ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
console.log();
|
|
249
|
+
console.log(pc.dim('───────────────────────────────────────'));
|
|
250
|
+
console.log(`\n${pc.green(`${passed} passed`)}, ${pc.red(`${failed} failed`)}, ${pc.dim(`${skipped} skipped`)}\n`);
|
|
251
|
+
|
|
252
|
+
if (output) {
|
|
253
|
+
console.log(pc.dim(`Images saved to: ${output}/\n`));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
success: failed === 0,
|
|
258
|
+
passed,
|
|
259
|
+
failed,
|
|
260
|
+
skipped,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Show interactive selection for components with Figma links
|
|
266
|
+
*/
|
|
267
|
+
async function selectComponents(baseUrl: string): Promise<string[]> {
|
|
268
|
+
console.log(pc.dim('Fetching components with Figma links...\n'));
|
|
269
|
+
|
|
270
|
+
const contextResp = await fetch(`${baseUrl}/segments/context?format=json`);
|
|
271
|
+
if (!contextResp.ok) {
|
|
272
|
+
throw new Error('Failed to fetch segments. Make sure dev server is running.');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const contextText = await contextResp.text();
|
|
276
|
+
let segments: Array<{ name: string; figma?: string }> = [];
|
|
277
|
+
try {
|
|
278
|
+
const contextData = JSON.parse(contextText);
|
|
279
|
+
segments = (contextData.components || []).filter((s: { figma?: string }) => s.figma);
|
|
280
|
+
} catch {
|
|
281
|
+
segments = [];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (segments.length === 0) {
|
|
285
|
+
console.log(pc.yellow('No components found with Figma links.'));
|
|
286
|
+
console.log(pc.dim('Add figma field to your segment definitions:'));
|
|
287
|
+
console.log(pc.dim(' meta: { figma: "https://figma.com/file/..." }'));
|
|
288
|
+
return [];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const { checkbox } = await import('@inquirer/prompts');
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
const choices = segments.map((seg) => ({
|
|
295
|
+
name: seg.name,
|
|
296
|
+
value: seg.name,
|
|
297
|
+
checked: true,
|
|
298
|
+
}));
|
|
299
|
+
|
|
300
|
+
return await checkbox({
|
|
301
|
+
message: 'Select components to compare:',
|
|
302
|
+
choices,
|
|
303
|
+
pageSize: 15,
|
|
304
|
+
});
|
|
305
|
+
} catch {
|
|
306
|
+
// User cancelled (Ctrl+C)
|
|
307
|
+
console.log(pc.dim('\nNo changes made.'));
|
|
308
|
+
return [];
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Save comparison images to output directory
|
|
314
|
+
*/
|
|
315
|
+
async function saveImages(
|
|
316
|
+
outputDir: string,
|
|
317
|
+
component: string,
|
|
318
|
+
result: CompareResult
|
|
319
|
+
): Promise<void> {
|
|
320
|
+
const dir = resolve(process.cwd(), outputDir);
|
|
321
|
+
await mkdir(dir, { recursive: true });
|
|
322
|
+
|
|
323
|
+
const saveImage = async (data: string, filename: string) => {
|
|
324
|
+
const base64 = data.replace('data:image/png;base64,', '');
|
|
325
|
+
await writeFile(join(dir, filename), Buffer.from(base64, 'base64'));
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
if (result.rendered) {
|
|
329
|
+
await saveImage(result.rendered, `${component}-rendered.png`);
|
|
330
|
+
}
|
|
331
|
+
if (result.figma) {
|
|
332
|
+
await saveImage(result.figma, `${component}-figma.png`);
|
|
333
|
+
}
|
|
334
|
+
if (result.diff) {
|
|
335
|
+
await saveImage(result.diff, `${component}-diff.png`);
|
|
336
|
+
}
|
|
337
|
+
}
|