@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,401 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile, stat, readdir, rm } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import {
|
|
5
|
+
BRAND,
|
|
6
|
+
DEFAULTS,
|
|
7
|
+
type Screenshot,
|
|
8
|
+
type BaselineInfo,
|
|
9
|
+
type Manifest,
|
|
10
|
+
type Viewport,
|
|
11
|
+
type Theme,
|
|
12
|
+
} from '../core/index.js';
|
|
13
|
+
import { ServiceError, buildScreenshotPath, computeHash } from './utils.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Storage manager configuration
|
|
17
|
+
*/
|
|
18
|
+
export interface StorageConfig {
|
|
19
|
+
/** Root directory for project (default: process.cwd()) */
|
|
20
|
+
projectRoot?: string;
|
|
21
|
+
|
|
22
|
+
/** Default viewport */
|
|
23
|
+
viewport?: Viewport;
|
|
24
|
+
|
|
25
|
+
/** Default diff threshold */
|
|
26
|
+
threshold?: number;
|
|
27
|
+
|
|
28
|
+
/** Capture delay */
|
|
29
|
+
captureDelay?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Manages baseline screenshot storage and manifest.
|
|
34
|
+
*/
|
|
35
|
+
export class StorageManager {
|
|
36
|
+
private readonly projectRoot: string;
|
|
37
|
+
private readonly dataDir: string;
|
|
38
|
+
private readonly screenshotsDir: string;
|
|
39
|
+
private readonly manifestPath: string;
|
|
40
|
+
private readonly config: Required<Omit<StorageConfig, 'projectRoot'>>;
|
|
41
|
+
|
|
42
|
+
private manifest: Manifest | null = null;
|
|
43
|
+
|
|
44
|
+
constructor(config: StorageConfig = {}) {
|
|
45
|
+
this.projectRoot = config.projectRoot ?? process.cwd();
|
|
46
|
+
this.dataDir = join(this.projectRoot, BRAND.dataDir);
|
|
47
|
+
this.screenshotsDir = join(this.dataDir, BRAND.screenshotsDir);
|
|
48
|
+
this.manifestPath = join(this.dataDir, BRAND.manifestFile);
|
|
49
|
+
|
|
50
|
+
this.config = {
|
|
51
|
+
viewport: config.viewport ?? DEFAULTS.viewport,
|
|
52
|
+
threshold: config.threshold ?? DEFAULTS.diffThreshold,
|
|
53
|
+
captureDelay: config.captureDelay ?? DEFAULTS.captureDelayMs,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get the data directory path
|
|
59
|
+
*/
|
|
60
|
+
get dataDirPath(): string {
|
|
61
|
+
return this.dataDir;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get the screenshots directory path
|
|
66
|
+
*/
|
|
67
|
+
get screenshotsDirPath(): string {
|
|
68
|
+
return this.screenshotsDir;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Initialize the storage directory structure
|
|
73
|
+
*/
|
|
74
|
+
async initialize(): Promise<void> {
|
|
75
|
+
// Create directories
|
|
76
|
+
await mkdir(this.screenshotsDir, { recursive: true });
|
|
77
|
+
await mkdir(join(this.dataDir, BRAND.cacheDir), { recursive: true });
|
|
78
|
+
await mkdir(join(this.dataDir, BRAND.diffDir), { recursive: true });
|
|
79
|
+
|
|
80
|
+
// Load or create manifest
|
|
81
|
+
if (existsSync(this.manifestPath)) {
|
|
82
|
+
await this.loadManifest();
|
|
83
|
+
} else {
|
|
84
|
+
this.manifest = this.createEmptyManifest();
|
|
85
|
+
await this.saveManifest();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Save a screenshot as a baseline
|
|
91
|
+
*/
|
|
92
|
+
async saveBaseline(screenshot: Screenshot): Promise<BaselineInfo> {
|
|
93
|
+
await this.ensureInitialized();
|
|
94
|
+
|
|
95
|
+
const { component, variant, theme } = screenshot.metadata;
|
|
96
|
+
const relativePath = buildScreenshotPath(component, variant, theme);
|
|
97
|
+
const absolutePath = join(this.screenshotsDir, relativePath);
|
|
98
|
+
|
|
99
|
+
// Ensure directory exists
|
|
100
|
+
await mkdir(dirname(absolutePath), { recursive: true });
|
|
101
|
+
|
|
102
|
+
// Write the image
|
|
103
|
+
await writeFile(absolutePath, screenshot.data);
|
|
104
|
+
|
|
105
|
+
// Get file size
|
|
106
|
+
const stats = await stat(absolutePath);
|
|
107
|
+
|
|
108
|
+
// Create baseline info
|
|
109
|
+
const baselineInfo: BaselineInfo = {
|
|
110
|
+
component,
|
|
111
|
+
variant,
|
|
112
|
+
theme,
|
|
113
|
+
path: relativePath,
|
|
114
|
+
hash: screenshot.hash,
|
|
115
|
+
viewport: screenshot.viewport,
|
|
116
|
+
capturedAt: screenshot.capturedAt.toISOString(),
|
|
117
|
+
fileSize: stats.size,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// Update manifest
|
|
121
|
+
this.updateManifestBaseline(baselineInfo);
|
|
122
|
+
await this.saveManifest();
|
|
123
|
+
|
|
124
|
+
return baselineInfo;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Load a baseline screenshot
|
|
129
|
+
*/
|
|
130
|
+
async loadBaseline(
|
|
131
|
+
component: string,
|
|
132
|
+
variant: string,
|
|
133
|
+
theme: Theme = DEFAULTS.theme
|
|
134
|
+
): Promise<Screenshot | null> {
|
|
135
|
+
await this.ensureInitialized();
|
|
136
|
+
|
|
137
|
+
const info = this.getBaselineInfo(component, variant, theme);
|
|
138
|
+
if (!info) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const absolutePath = join(this.screenshotsDir, info.path);
|
|
143
|
+
|
|
144
|
+
if (!existsSync(absolutePath)) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const data = await readFile(absolutePath);
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
data,
|
|
152
|
+
hash: info.hash,
|
|
153
|
+
viewport: info.viewport,
|
|
154
|
+
capturedAt: new Date(info.capturedAt),
|
|
155
|
+
metadata: {
|
|
156
|
+
component: info.component,
|
|
157
|
+
variant: info.variant,
|
|
158
|
+
theme: info.theme,
|
|
159
|
+
renderTimeMs: 0, // Not available for loaded baselines
|
|
160
|
+
captureTimeMs: 0,
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get baseline info from manifest
|
|
167
|
+
*/
|
|
168
|
+
getBaselineInfo(
|
|
169
|
+
component: string,
|
|
170
|
+
variant: string,
|
|
171
|
+
theme: Theme = DEFAULTS.theme
|
|
172
|
+
): BaselineInfo | null {
|
|
173
|
+
if (!this.manifest) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const key = this.buildBaselineKey(component, variant, theme);
|
|
178
|
+
const componentBaselines = this.manifest.baselines[component];
|
|
179
|
+
|
|
180
|
+
if (!componentBaselines) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return componentBaselines[key] ?? null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Check if a baseline exists
|
|
189
|
+
*/
|
|
190
|
+
hasBaseline(
|
|
191
|
+
component: string,
|
|
192
|
+
variant: string,
|
|
193
|
+
theme: Theme = DEFAULTS.theme
|
|
194
|
+
): boolean {
|
|
195
|
+
return this.getBaselineInfo(component, variant, theme) !== null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* List all baselines
|
|
200
|
+
*/
|
|
201
|
+
listBaselines(): BaselineInfo[] {
|
|
202
|
+
if (!this.manifest) {
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const baselines: BaselineInfo[] = [];
|
|
207
|
+
|
|
208
|
+
for (const componentBaselines of Object.values(this.manifest.baselines)) {
|
|
209
|
+
for (const baseline of Object.values(componentBaselines)) {
|
|
210
|
+
baselines.push(baseline);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return baselines;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* List baselines for a specific component
|
|
219
|
+
*/
|
|
220
|
+
listComponentBaselines(component: string): BaselineInfo[] {
|
|
221
|
+
if (!this.manifest) {
|
|
222
|
+
return [];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const componentBaselines = this.manifest.baselines[component];
|
|
226
|
+
if (!componentBaselines) {
|
|
227
|
+
return [];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return Object.values(componentBaselines);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Delete a baseline
|
|
235
|
+
*/
|
|
236
|
+
async deleteBaseline(
|
|
237
|
+
component: string,
|
|
238
|
+
variant: string,
|
|
239
|
+
theme: Theme = DEFAULTS.theme
|
|
240
|
+
): Promise<boolean> {
|
|
241
|
+
await this.ensureInitialized();
|
|
242
|
+
|
|
243
|
+
const info = this.getBaselineInfo(component, variant, theme);
|
|
244
|
+
if (!info) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Delete the file
|
|
249
|
+
const absolutePath = join(this.screenshotsDir, info.path);
|
|
250
|
+
if (existsSync(absolutePath)) {
|
|
251
|
+
await rm(absolutePath);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Remove from manifest
|
|
255
|
+
const key = this.buildBaselineKey(component, variant, theme);
|
|
256
|
+
if (this.manifest?.baselines[component]) {
|
|
257
|
+
delete this.manifest.baselines[component][key];
|
|
258
|
+
|
|
259
|
+
// Clean up empty component entry
|
|
260
|
+
if (Object.keys(this.manifest.baselines[component]).length === 0) {
|
|
261
|
+
delete this.manifest.baselines[component];
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
await this.saveManifest();
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Save a diff image
|
|
271
|
+
*/
|
|
272
|
+
async saveDiff(
|
|
273
|
+
component: string,
|
|
274
|
+
variant: string,
|
|
275
|
+
theme: Theme,
|
|
276
|
+
diffImage: Buffer
|
|
277
|
+
): Promise<string> {
|
|
278
|
+
const relativePath = buildScreenshotPath(component, variant, theme).replace(
|
|
279
|
+
'.png',
|
|
280
|
+
'-diff.png'
|
|
281
|
+
);
|
|
282
|
+
const absolutePath = join(this.dataDir, BRAND.diffDir, relativePath);
|
|
283
|
+
|
|
284
|
+
await mkdir(dirname(absolutePath), { recursive: true });
|
|
285
|
+
await writeFile(absolutePath, diffImage);
|
|
286
|
+
|
|
287
|
+
return absolutePath;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Get the manifest
|
|
292
|
+
*/
|
|
293
|
+
getManifest(): Manifest | null {
|
|
294
|
+
return this.manifest;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Reload manifest from disk
|
|
299
|
+
*/
|
|
300
|
+
async reloadManifest(): Promise<void> {
|
|
301
|
+
await this.loadManifest();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Build a unique key for baseline lookup
|
|
306
|
+
*/
|
|
307
|
+
private buildBaselineKey(
|
|
308
|
+
component: string,
|
|
309
|
+
variant: string,
|
|
310
|
+
theme: Theme
|
|
311
|
+
): string {
|
|
312
|
+
return `${variant}:${theme}`;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Update manifest with a baseline
|
|
317
|
+
*/
|
|
318
|
+
private updateManifestBaseline(info: BaselineInfo): void {
|
|
319
|
+
if (!this.manifest) {
|
|
320
|
+
this.manifest = this.createEmptyManifest();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (!this.manifest.baselines[info.component]) {
|
|
324
|
+
this.manifest.baselines[info.component] = {};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const key = this.buildBaselineKey(info.component, info.variant, info.theme);
|
|
328
|
+
this.manifest.baselines[info.component][key] = info;
|
|
329
|
+
this.manifest.generatedAt = new Date().toISOString();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Create an empty manifest
|
|
334
|
+
*/
|
|
335
|
+
private createEmptyManifest(): Manifest {
|
|
336
|
+
return {
|
|
337
|
+
version: '1.0.0',
|
|
338
|
+
generatedAt: new Date().toISOString(),
|
|
339
|
+
config: {
|
|
340
|
+
defaultViewport: this.config.viewport,
|
|
341
|
+
defaultThreshold: this.config.threshold,
|
|
342
|
+
captureDelay: this.config.captureDelay,
|
|
343
|
+
},
|
|
344
|
+
baselines: {},
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Load manifest from disk
|
|
350
|
+
*/
|
|
351
|
+
private async loadManifest(): Promise<void> {
|
|
352
|
+
try {
|
|
353
|
+
const content = await readFile(this.manifestPath, 'utf-8');
|
|
354
|
+
this.manifest = JSON.parse(content) as Manifest;
|
|
355
|
+
} catch {
|
|
356
|
+
this.manifest = this.createEmptyManifest();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Save manifest to disk
|
|
362
|
+
*/
|
|
363
|
+
private async saveManifest(): Promise<void> {
|
|
364
|
+
if (!this.manifest) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
await mkdir(dirname(this.manifestPath), { recursive: true });
|
|
369
|
+
await writeFile(
|
|
370
|
+
this.manifestPath,
|
|
371
|
+
JSON.stringify(this.manifest, null, 2),
|
|
372
|
+
'utf-8'
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Ensure storage is initialized
|
|
378
|
+
*/
|
|
379
|
+
private async ensureInitialized(): Promise<void> {
|
|
380
|
+
if (!this.manifest) {
|
|
381
|
+
await this.initialize();
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Error class for storage errors
|
|
388
|
+
*/
|
|
389
|
+
export class StorageError extends ServiceError {
|
|
390
|
+
constructor(message: string, code: string, suggestion?: string) {
|
|
391
|
+
super(message, code, suggestion);
|
|
392
|
+
this.name = `${BRAND.name}StorageError`;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Create a storage manager
|
|
398
|
+
*/
|
|
399
|
+
export function createStorageManager(config?: StorageConfig): StorageManager {
|
|
400
|
+
return new StorageManager(config);
|
|
401
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token Fix Suggestions
|
|
3
|
+
*
|
|
4
|
+
* Generates AI-friendly fix suggestions when hardcoded values
|
|
5
|
+
* should be replaced with design tokens.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
DesignToken,
|
|
10
|
+
EnhancedStyleDiffItem,
|
|
11
|
+
TokenFix,
|
|
12
|
+
TokenUsageSummary,
|
|
13
|
+
} from "../core/index.js";
|
|
14
|
+
import { TokenRegistryManager } from "./token-registry.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Context for AI agents to understand and fix token issues
|
|
18
|
+
*/
|
|
19
|
+
export interface TokenFixContext {
|
|
20
|
+
/** Component name */
|
|
21
|
+
componentName: string;
|
|
22
|
+
|
|
23
|
+
/** Summary of token usage */
|
|
24
|
+
summary: TokenUsageSummary;
|
|
25
|
+
|
|
26
|
+
/** Detailed fix suggestions for each hardcoded property */
|
|
27
|
+
fixes: TokenFixDetail[];
|
|
28
|
+
|
|
29
|
+
/** Markdown-formatted context for AI agents */
|
|
30
|
+
markdown: string;
|
|
31
|
+
|
|
32
|
+
/** JSON-formatted context for programmatic use */
|
|
33
|
+
json: {
|
|
34
|
+
component: string;
|
|
35
|
+
compliancePercent: number;
|
|
36
|
+
totalProperties: number;
|
|
37
|
+
hardcodedCount: number;
|
|
38
|
+
fixes: Array<{
|
|
39
|
+
property: string;
|
|
40
|
+
currentValue: string;
|
|
41
|
+
suggestedToken: string;
|
|
42
|
+
tokenValue: string;
|
|
43
|
+
codeFix: string;
|
|
44
|
+
reason: string;
|
|
45
|
+
}>;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Detailed fix for a single property
|
|
51
|
+
*/
|
|
52
|
+
export interface TokenFixDetail {
|
|
53
|
+
/** CSS property name (e.g., "backgroundColor") */
|
|
54
|
+
property: string;
|
|
55
|
+
|
|
56
|
+
/** CSS property name in kebab-case (e.g., "background-color") */
|
|
57
|
+
cssProperty: string;
|
|
58
|
+
|
|
59
|
+
/** Current hardcoded value */
|
|
60
|
+
currentValue: string;
|
|
61
|
+
|
|
62
|
+
/** Token that should be used */
|
|
63
|
+
suggestedToken: DesignToken;
|
|
64
|
+
|
|
65
|
+
/** The code fix to apply */
|
|
66
|
+
codeFix: string;
|
|
67
|
+
|
|
68
|
+
/** Confidence score 0-1 */
|
|
69
|
+
confidence: number;
|
|
70
|
+
|
|
71
|
+
/** Human-readable explanation */
|
|
72
|
+
reason: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Generate token fixes for a set of style diffs
|
|
77
|
+
*/
|
|
78
|
+
export function generateTokenFixes(
|
|
79
|
+
componentName: string,
|
|
80
|
+
styleDiffs: Array<{
|
|
81
|
+
property: string;
|
|
82
|
+
figma: string;
|
|
83
|
+
rendered: string;
|
|
84
|
+
match: boolean;
|
|
85
|
+
}>,
|
|
86
|
+
registry: TokenRegistryManager,
|
|
87
|
+
theme = "default"
|
|
88
|
+
): TokenFixContext {
|
|
89
|
+
// Calculate usage summary
|
|
90
|
+
const summary = registry.calculateUsageSummary(styleDiffs, theme);
|
|
91
|
+
|
|
92
|
+
// Generate detailed fixes
|
|
93
|
+
const fixes: TokenFixDetail[] = [];
|
|
94
|
+
|
|
95
|
+
for (const hardcoded of summary.hardcodedProperties) {
|
|
96
|
+
if (!hardcoded.suggestedFix) continue;
|
|
97
|
+
|
|
98
|
+
const token = registry.getToken(hardcoded.suggestedFix.tokenName);
|
|
99
|
+
if (!token) continue;
|
|
100
|
+
|
|
101
|
+
const cssProperty = toCssProperty(hardcoded.property);
|
|
102
|
+
|
|
103
|
+
fixes.push({
|
|
104
|
+
property: hardcoded.property,
|
|
105
|
+
cssProperty,
|
|
106
|
+
currentValue: hardcoded.rendered,
|
|
107
|
+
suggestedToken: token,
|
|
108
|
+
codeFix: hardcoded.suggestedFix.codeFix,
|
|
109
|
+
confidence: hardcoded.suggestedFix.confidence,
|
|
110
|
+
reason: hardcoded.suggestedFix.reason,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Generate markdown context for AI
|
|
115
|
+
const markdown = generateMarkdownContext(componentName, summary, fixes);
|
|
116
|
+
|
|
117
|
+
// Generate JSON context
|
|
118
|
+
const json = {
|
|
119
|
+
component: componentName,
|
|
120
|
+
compliancePercent: summary.compliancePercent,
|
|
121
|
+
totalProperties: summary.totalProperties,
|
|
122
|
+
hardcodedCount: summary.hardcoded,
|
|
123
|
+
fixes: fixes.map((f) => ({
|
|
124
|
+
property: f.property,
|
|
125
|
+
currentValue: f.currentValue,
|
|
126
|
+
suggestedToken: f.suggestedToken.name,
|
|
127
|
+
tokenValue: f.suggestedToken.resolvedValue,
|
|
128
|
+
codeFix: f.codeFix,
|
|
129
|
+
reason: f.reason,
|
|
130
|
+
})),
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
componentName,
|
|
135
|
+
summary,
|
|
136
|
+
fixes,
|
|
137
|
+
markdown,
|
|
138
|
+
json,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Generate markdown context for AI agents
|
|
144
|
+
*/
|
|
145
|
+
function generateMarkdownContext(
|
|
146
|
+
componentName: string,
|
|
147
|
+
summary: TokenUsageSummary,
|
|
148
|
+
fixes: TokenFixDetail[]
|
|
149
|
+
): string {
|
|
150
|
+
const lines: string[] = [];
|
|
151
|
+
|
|
152
|
+
// Header
|
|
153
|
+
lines.push(`## Design Token Analysis: ${componentName}`);
|
|
154
|
+
lines.push("");
|
|
155
|
+
|
|
156
|
+
// Summary
|
|
157
|
+
lines.push(`### Compliance: ${summary.compliancePercent}%`);
|
|
158
|
+
lines.push("");
|
|
159
|
+
lines.push(`| Metric | Count |`);
|
|
160
|
+
lines.push(`|--------|-------|`);
|
|
161
|
+
lines.push(`| Total Properties | ${summary.totalProperties} |`);
|
|
162
|
+
lines.push(`| Using Tokens | ${summary.usingTokens} |`);
|
|
163
|
+
lines.push(`| Hardcoded | ${summary.hardcoded} |`);
|
|
164
|
+
lines.push(`| Implicit Matches | ${summary.implicitMatches} |`);
|
|
165
|
+
lines.push("");
|
|
166
|
+
|
|
167
|
+
if (fixes.length === 0) {
|
|
168
|
+
lines.push("✅ **All styles are using design tokens correctly.**");
|
|
169
|
+
return lines.join("\n");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Issues
|
|
173
|
+
lines.push(`### Issues Found: ${fixes.length} hardcoded value(s)`);
|
|
174
|
+
lines.push("");
|
|
175
|
+
|
|
176
|
+
for (let i = 0; i < fixes.length; i++) {
|
|
177
|
+
const fix = fixes[i];
|
|
178
|
+
lines.push(`#### ${i + 1}. \`${fix.cssProperty}\``);
|
|
179
|
+
lines.push("");
|
|
180
|
+
lines.push(`- **Current Value**: \`${fix.currentValue}\``);
|
|
181
|
+
lines.push(`- **Suggested Token**: \`${fix.suggestedToken.name}\` (${fix.suggestedToken.resolvedValue})`);
|
|
182
|
+
lines.push(`- **Confidence**: ${Math.round(fix.confidence * 100)}%`);
|
|
183
|
+
lines.push("");
|
|
184
|
+
lines.push("**Fix:**");
|
|
185
|
+
lines.push("```css");
|
|
186
|
+
lines.push(`/* Before */ ${fix.cssProperty}: ${fix.currentValue};`);
|
|
187
|
+
lines.push(`/* After */ ${fix.codeFix}`);
|
|
188
|
+
lines.push("```");
|
|
189
|
+
lines.push("");
|
|
190
|
+
lines.push(`> ${fix.reason}`);
|
|
191
|
+
lines.push("");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Why use tokens
|
|
195
|
+
lines.push("### Why Use Design Tokens?");
|
|
196
|
+
lines.push("");
|
|
197
|
+
lines.push("1. **Consistency**: Ensures visual consistency across the application");
|
|
198
|
+
lines.push("2. **Maintainability**: Change a token value once, update everywhere");
|
|
199
|
+
lines.push("3. **Theming**: Enables dark mode and other theme variations");
|
|
200
|
+
lines.push("4. **Design System Compliance**: Keeps code aligned with design specifications");
|
|
201
|
+
lines.push("");
|
|
202
|
+
|
|
203
|
+
return lines.join("\n");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Generate AI context for a single component's token issues
|
|
208
|
+
*
|
|
209
|
+
* This format is optimized for AI agents to understand and fix
|
|
210
|
+
*/
|
|
211
|
+
export function generateAIFixContext(
|
|
212
|
+
componentName: string,
|
|
213
|
+
hardcodedItems: EnhancedStyleDiffItem[],
|
|
214
|
+
registry: TokenRegistryManager
|
|
215
|
+
): string {
|
|
216
|
+
if (hardcodedItems.length === 0) {
|
|
217
|
+
return `Component "${componentName}" is fully compliant with design tokens.`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const lines: string[] = [];
|
|
221
|
+
|
|
222
|
+
lines.push(`# Design Token Fix Required: ${componentName}`);
|
|
223
|
+
lines.push("");
|
|
224
|
+
lines.push(`Found ${hardcodedItems.length} hardcoded CSS value(s) that should use design tokens.`);
|
|
225
|
+
lines.push("");
|
|
226
|
+
|
|
227
|
+
for (const item of hardcodedItems) {
|
|
228
|
+
if (!item.suggestedFix) continue;
|
|
229
|
+
|
|
230
|
+
const token = registry.getToken(item.suggestedFix.tokenName);
|
|
231
|
+
const cssProperty = toCssProperty(item.property);
|
|
232
|
+
|
|
233
|
+
lines.push(`## Issue: Hardcoded \`${cssProperty}\``);
|
|
234
|
+
lines.push("");
|
|
235
|
+
lines.push("| | |");
|
|
236
|
+
lines.push("|---|---|");
|
|
237
|
+
lines.push(`| **Property** | \`${cssProperty}\` |`);
|
|
238
|
+
lines.push(`| **Current Value** | \`${item.rendered}\` |`);
|
|
239
|
+
lines.push(`| **Expected Token** | \`${item.suggestedFix.tokenName}\` |`);
|
|
240
|
+
lines.push(`| **Token Value** | \`${token?.resolvedValue || item.suggestedFix.tokenValue}\` |`);
|
|
241
|
+
lines.push("");
|
|
242
|
+
lines.push("**Suggested Fix:**");
|
|
243
|
+
lines.push("```css");
|
|
244
|
+
lines.push(`/* Replace hardcoded value */`);
|
|
245
|
+
lines.push(`${cssProperty}: var(${item.suggestedFix.tokenName});`);
|
|
246
|
+
lines.push("```");
|
|
247
|
+
lines.push("");
|
|
248
|
+
|
|
249
|
+
if (token) {
|
|
250
|
+
lines.push("**Token Details:**");
|
|
251
|
+
lines.push(`- Category: ${token.category}`);
|
|
252
|
+
lines.push(`- Level: ${token.level === 1 ? "Base" : token.level === 2 ? "Semantic" : "Component"}`);
|
|
253
|
+
lines.push(`- Theme: ${token.theme}`);
|
|
254
|
+
if (token.description) {
|
|
255
|
+
lines.push(`- Description: ${token.description}`);
|
|
256
|
+
}
|
|
257
|
+
lines.push("");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
lines.push("---");
|
|
261
|
+
lines.push("");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
lines.push("## Why This Matters");
|
|
265
|
+
lines.push("");
|
|
266
|
+
lines.push("Using design tokens instead of hardcoded values:");
|
|
267
|
+
lines.push("- Ensures the component matches the Figma design");
|
|
268
|
+
lines.push("- Enables automatic theme switching (light/dark mode)");
|
|
269
|
+
lines.push("- Maintains consistency across the design system");
|
|
270
|
+
lines.push("- Makes future design updates easier");
|
|
271
|
+
lines.push("");
|
|
272
|
+
|
|
273
|
+
return lines.join("\n");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Convert camelCase to kebab-case
|
|
278
|
+
*/
|
|
279
|
+
function toCssProperty(prop: string): string {
|
|
280
|
+
return prop.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
281
|
+
}
|