@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,172 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { BRAND } from "../core/index.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Base error class for all service errors.
|
|
6
|
+
* Includes error code and optional suggestion for resolution.
|
|
7
|
+
*/
|
|
8
|
+
export class ServiceError extends Error {
|
|
9
|
+
constructor(
|
|
10
|
+
message: string,
|
|
11
|
+
public readonly code: string,
|
|
12
|
+
public readonly suggestion?: string
|
|
13
|
+
) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = `${BRAND.name}Error`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Format error for CLI output
|
|
20
|
+
*/
|
|
21
|
+
toCliString(): string {
|
|
22
|
+
let str = `Error: ${this.message}`;
|
|
23
|
+
if (this.suggestion) {
|
|
24
|
+
str += `\n Suggestion: ${this.suggestion}`;
|
|
25
|
+
}
|
|
26
|
+
return str;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Compute SHA-256 hash of a buffer
|
|
32
|
+
*/
|
|
33
|
+
export function computeHash(data: Buffer): string {
|
|
34
|
+
return `sha256:${createHash("sha256").update(data).digest("hex")}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Simple timer utility for measuring operations
|
|
39
|
+
*/
|
|
40
|
+
export class Timer {
|
|
41
|
+
private startTime: number;
|
|
42
|
+
|
|
43
|
+
constructor() {
|
|
44
|
+
this.startTime = performance.now();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get elapsed time in milliseconds
|
|
49
|
+
*/
|
|
50
|
+
elapsed(): number {
|
|
51
|
+
return Math.round(performance.now() - this.startTime);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Reset timer and return elapsed time
|
|
56
|
+
*/
|
|
57
|
+
reset(): number {
|
|
58
|
+
const elapsed = this.elapsed();
|
|
59
|
+
this.startTime = performance.now();
|
|
60
|
+
return elapsed;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Create a deferred promise with external resolve/reject
|
|
66
|
+
*/
|
|
67
|
+
export function createDeferred<T>(): {
|
|
68
|
+
promise: Promise<T>;
|
|
69
|
+
resolve: (value: T) => void;
|
|
70
|
+
reject: (error: Error) => void;
|
|
71
|
+
} {
|
|
72
|
+
let resolve!: (value: T) => void;
|
|
73
|
+
let reject!: (error: Error) => void;
|
|
74
|
+
|
|
75
|
+
const promise = new Promise<T>((res, rej) => {
|
|
76
|
+
resolve = res;
|
|
77
|
+
reject = rej;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return { promise, resolve, reject };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Sleep for a given number of milliseconds
|
|
85
|
+
*/
|
|
86
|
+
export function sleep(ms: number): Promise<void> {
|
|
87
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Convert buffer to base64 data URL
|
|
92
|
+
*/
|
|
93
|
+
export function bufferToBase64Url(
|
|
94
|
+
buffer: Buffer,
|
|
95
|
+
mimeType = "image/png"
|
|
96
|
+
): string {
|
|
97
|
+
return `data:${mimeType};base64,${buffer.toString("base64")}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Convert base64 data URL to buffer
|
|
102
|
+
*/
|
|
103
|
+
export function base64UrlToBuffer(dataUrl: string): Buffer {
|
|
104
|
+
const base64 = dataUrl.replace(/^data:[^;]+;base64,/, "");
|
|
105
|
+
return Buffer.from(base64, "base64");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Sanitize a string for use as a filename
|
|
110
|
+
*/
|
|
111
|
+
export function sanitizeFilename(name: string): string {
|
|
112
|
+
return name
|
|
113
|
+
.replace(/[/\\?%*:|"<>]/g, "-")
|
|
114
|
+
.replace(/\s+/g, "-")
|
|
115
|
+
.replace(/-+/g, "-")
|
|
116
|
+
.replace(/^-|-$/g, "");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Build path for screenshot file
|
|
121
|
+
*/
|
|
122
|
+
export function buildScreenshotPath(
|
|
123
|
+
component: string,
|
|
124
|
+
variant: string,
|
|
125
|
+
theme: string
|
|
126
|
+
): string {
|
|
127
|
+
const safeComponent = sanitizeFilename(component);
|
|
128
|
+
const safeVariant = sanitizeFilename(variant);
|
|
129
|
+
return `${theme}/${safeComponent}/${safeVariant}.png`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Format milliseconds for display
|
|
134
|
+
*/
|
|
135
|
+
export function formatMs(ms: number): string {
|
|
136
|
+
if (ms < 1000) {
|
|
137
|
+
return `${ms}ms`;
|
|
138
|
+
}
|
|
139
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Retry an async operation with exponential backoff
|
|
144
|
+
*/
|
|
145
|
+
export async function retry<T>(
|
|
146
|
+
fn: () => Promise<T>,
|
|
147
|
+
options: {
|
|
148
|
+
maxAttempts?: number;
|
|
149
|
+
initialDelayMs?: number;
|
|
150
|
+
maxDelayMs?: number;
|
|
151
|
+
} = {}
|
|
152
|
+
): Promise<T> {
|
|
153
|
+
const { maxAttempts = 3, initialDelayMs = 100, maxDelayMs = 1000 } = options;
|
|
154
|
+
|
|
155
|
+
let lastError: Error | undefined;
|
|
156
|
+
let delay = initialDelayMs;
|
|
157
|
+
|
|
158
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
159
|
+
try {
|
|
160
|
+
return await fn();
|
|
161
|
+
} catch (error) {
|
|
162
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
163
|
+
|
|
164
|
+
if (attempt < maxAttempts) {
|
|
165
|
+
await sleep(delay);
|
|
166
|
+
delay = Math.min(delay * 2, maxDelayMs);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
throw lastError;
|
|
172
|
+
}
|
package/src/setup.ts
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { BRAND } from './core/index.js';
|
|
3
|
+
import { loadConfig, discoverSegmentFiles } from './core/node.js';
|
|
4
|
+
import { buildSegments } from './build.js';
|
|
5
|
+
import {
|
|
6
|
+
detectStorybookConfig,
|
|
7
|
+
discoverStoryFiles as discoverStorybookFiles,
|
|
8
|
+
parseStoryFile,
|
|
9
|
+
convertToSegment,
|
|
10
|
+
} from './migrate/index.js';
|
|
11
|
+
|
|
12
|
+
export interface SetupOptions {
|
|
13
|
+
skipStorybook?: boolean;
|
|
14
|
+
skipFigma?: boolean;
|
|
15
|
+
skipBuild?: boolean;
|
|
16
|
+
silent?: boolean;
|
|
17
|
+
configPath?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SetupResult {
|
|
21
|
+
segmentFilesCreated: number;
|
|
22
|
+
segmentsBuilt: number;
|
|
23
|
+
figmaLinked: number;
|
|
24
|
+
errors: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface SegmentInfo {
|
|
28
|
+
name: string;
|
|
29
|
+
filePath: string;
|
|
30
|
+
hasFigma: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check if segments.json exists and is newer than all segment files.
|
|
35
|
+
*/
|
|
36
|
+
async function isSegmentsJsonStale(configDir: string, outFile: string): Promise<{ stale: boolean; missing: boolean }> {
|
|
37
|
+
const fs = await import('node:fs/promises');
|
|
38
|
+
const path = await import('node:path');
|
|
39
|
+
const fg = await import('fast-glob');
|
|
40
|
+
|
|
41
|
+
const segmentsJsonPath = path.join(configDir, outFile);
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const segmentsJsonStat = await fs.stat(segmentsJsonPath);
|
|
45
|
+
|
|
46
|
+
// Find all segment files
|
|
47
|
+
const segmentFiles = await fg.default(`**/*${BRAND.fileExtension}`, {
|
|
48
|
+
cwd: configDir,
|
|
49
|
+
ignore: ['**/node_modules/**'],
|
|
50
|
+
absolute: true,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Check if any segment file is newer than segments.json
|
|
54
|
+
for (const file of segmentFiles) {
|
|
55
|
+
const stat = await fs.stat(file);
|
|
56
|
+
if (stat.mtimeMs > segmentsJsonStat.mtimeMs) {
|
|
57
|
+
return { stale: true, missing: false };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { stale: false, missing: false };
|
|
62
|
+
} catch {
|
|
63
|
+
// segments.json doesn't exist
|
|
64
|
+
return { stale: false, missing: true };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Load segment files and check which ones have Figma links.
|
|
70
|
+
*/
|
|
71
|
+
async function loadSegmentInfo(segmentFiles: Array<{ absolutePath: string; relativePath: string }>): Promise<SegmentInfo[]> {
|
|
72
|
+
const fs = await import('node:fs/promises');
|
|
73
|
+
const segments: SegmentInfo[] = [];
|
|
74
|
+
|
|
75
|
+
for (const file of segmentFiles) {
|
|
76
|
+
try {
|
|
77
|
+
const content = await fs.readFile(file.absolutePath, 'utf-8');
|
|
78
|
+
const nameMatch = content.match(/name:\s*['"]([^'"]+)['"]/);
|
|
79
|
+
const hasFigma = /meta:\s*\{[^}]*figma:\s*['"]https?:/.test(content);
|
|
80
|
+
|
|
81
|
+
if (nameMatch) {
|
|
82
|
+
segments.push({
|
|
83
|
+
name: nameMatch[1],
|
|
84
|
+
filePath: file.absolutePath,
|
|
85
|
+
hasFigma,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
// Skip files that can't be read
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return segments;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Run auto-setup for segments dev server.
|
|
98
|
+
*
|
|
99
|
+
* This function:
|
|
100
|
+
* 1. Checks for segment files, imports from Storybook if none found
|
|
101
|
+
* 2. Builds segments.json if missing or stale
|
|
102
|
+
* 3. Links Figma if configured but no segments are linked
|
|
103
|
+
*/
|
|
104
|
+
export async function runSetup(options: SetupOptions = {}): Promise<SetupResult> {
|
|
105
|
+
const fs = await import('node:fs/promises');
|
|
106
|
+
const path = await import('node:path');
|
|
107
|
+
|
|
108
|
+
const result: SetupResult = {
|
|
109
|
+
segmentFilesCreated: 0,
|
|
110
|
+
segmentsBuilt: 0,
|
|
111
|
+
figmaLinked: 0,
|
|
112
|
+
errors: [],
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const log = (msg: string) => {
|
|
116
|
+
if (!options.silent) console.log(msg);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
// Load config
|
|
121
|
+
const { config, configDir } = await loadConfig(options.configPath);
|
|
122
|
+
|
|
123
|
+
// Step 1: Check for segment files
|
|
124
|
+
log(pc.dim('Checking for fragment files...'));
|
|
125
|
+
let segmentFiles = await discoverSegmentFiles(config, configDir);
|
|
126
|
+
|
|
127
|
+
if (segmentFiles.length === 0 && !options.skipStorybook) {
|
|
128
|
+
// No fragment files - check for Storybook
|
|
129
|
+
log(pc.yellow('\n No fragment files found'));
|
|
130
|
+
|
|
131
|
+
const sbConfig = await detectStorybookConfig(configDir);
|
|
132
|
+
if (sbConfig) {
|
|
133
|
+
log(pc.dim(` Found Storybook at ${sbConfig.configPath}`));
|
|
134
|
+
log(pc.dim(' Converting stories to fragments...\n'));
|
|
135
|
+
|
|
136
|
+
// Discover and convert stories
|
|
137
|
+
const storyFiles = await discoverStorybookFiles(configDir, sbConfig.storyPatterns);
|
|
138
|
+
|
|
139
|
+
if (storyFiles.length > 0) {
|
|
140
|
+
let converted = 0;
|
|
141
|
+
for (const storyFile of storyFiles) {
|
|
142
|
+
try {
|
|
143
|
+
const parsed = await parseStoryFile(storyFile);
|
|
144
|
+
const segmentResult = convertToSegment(parsed);
|
|
145
|
+
|
|
146
|
+
// Create directory and write file
|
|
147
|
+
await fs.mkdir(path.dirname(segmentResult.outputFile), { recursive: true });
|
|
148
|
+
await fs.writeFile(segmentResult.outputFile, segmentResult.code);
|
|
149
|
+
converted++;
|
|
150
|
+
} catch {
|
|
151
|
+
// Skip files that can't be converted
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
result.segmentFilesCreated = converted;
|
|
156
|
+
log(pc.green(` Generated ${converted} fragment file(s)`));
|
|
157
|
+
|
|
158
|
+
// Refresh segment files list
|
|
159
|
+
segmentFiles = await discoverSegmentFiles(config, configDir);
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
log(pc.dim(' No Storybook config found'));
|
|
163
|
+
log(pc.dim(` Run ${pc.cyan(`${BRAND.cliCommand} add <ComponentName>`)} to create your first fragment`));
|
|
164
|
+
}
|
|
165
|
+
} else if (segmentFiles.length > 0) {
|
|
166
|
+
log(pc.green(` Found ${segmentFiles.length} fragment file(s)`));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Step 2: Build fragments.json if needed
|
|
170
|
+
if (segmentFiles.length > 0 && !options.skipBuild) {
|
|
171
|
+
const outFile = config.outFile || BRAND.outFile;
|
|
172
|
+
const { stale, missing } = await isSegmentsJsonStale(configDir, outFile);
|
|
173
|
+
|
|
174
|
+
if (missing || stale) {
|
|
175
|
+
const reason = missing ? 'Building' : 'Rebuilding';
|
|
176
|
+
log(pc.dim(`\n${reason} ${BRAND.outFile}...`));
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const buildResult = await buildSegments(config, configDir);
|
|
180
|
+
result.segmentsBuilt = buildResult.segmentCount;
|
|
181
|
+
|
|
182
|
+
if (buildResult.errors.length > 0) {
|
|
183
|
+
for (const err of buildResult.errors) {
|
|
184
|
+
result.errors.push(`${err.file}: ${err.error}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
log(pc.green(` Built ${buildResult.segmentCount} fragment(s)`));
|
|
189
|
+
} catch (error) {
|
|
190
|
+
result.errors.push(`Build failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
log(pc.dim(`\n ${BRAND.outFile} is up to date`));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Step 3: Link Figma if configured
|
|
198
|
+
if (!options.skipFigma && config.figmaFile && process.env.FIGMA_ACCESS_TOKEN) {
|
|
199
|
+
const segments = await loadSegmentInfo(segmentFiles);
|
|
200
|
+
const linkedCount = segments.filter((s) => s.hasFigma).length;
|
|
201
|
+
|
|
202
|
+
if (linkedCount === 0 && segments.length > 0) {
|
|
203
|
+
log(pc.dim('\n Figma configured but no fragments linked'));
|
|
204
|
+
log(pc.dim(` Run ${pc.cyan(`${BRAND.cliCommand} link figma --auto`)} to auto-link components`));
|
|
205
|
+
// Note: We don't auto-link here because it requires API calls and user may want control
|
|
206
|
+
// But we inform them about the option
|
|
207
|
+
} else if (linkedCount > 0) {
|
|
208
|
+
log(pc.dim(`\n ${linkedCount}/${segments.length} fragment(s) linked to Figma`));
|
|
209
|
+
}
|
|
210
|
+
} else if (!options.skipFigma && config.figmaFile && !process.env.FIGMA_ACCESS_TOKEN) {
|
|
211
|
+
log(pc.dim('\n Figma file configured but FIGMA_ACCESS_TOKEN not set'));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
} catch (error) {
|
|
215
|
+
result.errors.push(error instanceof Error ? error.message : 'Unknown error');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Print setup summary with user-friendly output.
|
|
223
|
+
*/
|
|
224
|
+
export function printSetupSummary(result: SetupResult): void {
|
|
225
|
+
console.log();
|
|
226
|
+
|
|
227
|
+
if (result.segmentFilesCreated > 0) {
|
|
228
|
+
console.log(pc.green(` Imported ${result.segmentFilesCreated} segments from Storybook`));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (result.segmentsBuilt > 0) {
|
|
232
|
+
console.log(pc.green(` Built ${result.segmentsBuilt} segments`));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (result.errors.length > 0) {
|
|
236
|
+
console.log(pc.yellow('\n Warnings:'));
|
|
237
|
+
for (const error of result.errors) {
|
|
238
|
+
console.log(pc.dim(` ${error}`));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Wrapper
|
|
3
|
+
*
|
|
4
|
+
* Provides error handling and common patterns for CLI commands.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import pc from 'picocolors';
|
|
8
|
+
import { BRAND } from '../core/index.js';
|
|
9
|
+
import { DevServerConnectionError, DevServerError } from './dev-server-client.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Options for command execution
|
|
13
|
+
*/
|
|
14
|
+
export interface CommandOptions {
|
|
15
|
+
/** CI mode - output JSON and exit non-zero on failure */
|
|
16
|
+
ci?: boolean;
|
|
17
|
+
/** Port for dev server */
|
|
18
|
+
port?: number | string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Format and print a dev server connection error with helpful suggestions
|
|
23
|
+
*/
|
|
24
|
+
export function formatConnectionError(error: DevServerConnectionError, port: number | string): string {
|
|
25
|
+
return (
|
|
26
|
+
`Cannot connect to dev server at ${error.serverUrl}\n\n` +
|
|
27
|
+
`The command requires the dev server to be running.\n` +
|
|
28
|
+
`Start it with: ${pc.cyan(`${BRAND.cliCommand} dev`)}\n\n` +
|
|
29
|
+
`Alternatively, run with a different port:\n` +
|
|
30
|
+
` ${pc.cyan(`${BRAND.cliCommand} <command> --port ${port}`)}`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Handle errors in a command, formatting appropriately for CI vs interactive mode
|
|
36
|
+
*/
|
|
37
|
+
export function handleCommandError(
|
|
38
|
+
error: unknown,
|
|
39
|
+
options: CommandOptions = {}
|
|
40
|
+
): never {
|
|
41
|
+
const { ci = false, port = 6006 } = options;
|
|
42
|
+
|
|
43
|
+
if (error instanceof DevServerConnectionError) {
|
|
44
|
+
if (ci) {
|
|
45
|
+
console.log(JSON.stringify({ error: `Cannot connect to dev server at ${error.serverUrl}` }));
|
|
46
|
+
} else {
|
|
47
|
+
console.error(pc.red('Error:'), formatConnectionError(error, port));
|
|
48
|
+
}
|
|
49
|
+
} else if (error instanceof DevServerError) {
|
|
50
|
+
if (ci) {
|
|
51
|
+
console.log(JSON.stringify({ error: error.message }));
|
|
52
|
+
} else {
|
|
53
|
+
console.error(pc.red('Error:'), error.message);
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
57
|
+
if (ci) {
|
|
58
|
+
console.log(JSON.stringify({ error: message }));
|
|
59
|
+
} else {
|
|
60
|
+
console.error(pc.red('Error:'), message);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Wrap a command function with standard error handling
|
|
69
|
+
*/
|
|
70
|
+
export function withErrorHandling<T extends unknown[], R>(
|
|
71
|
+
fn: (...args: T) => Promise<R>,
|
|
72
|
+
options: CommandOptions = {}
|
|
73
|
+
): (...args: T) => Promise<R> {
|
|
74
|
+
return async (...args: T): Promise<R> => {
|
|
75
|
+
try {
|
|
76
|
+
return await fn(...args);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
handleCommandError(error, options);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev Server Client
|
|
3
|
+
*
|
|
4
|
+
* HTTP client for communicating with the fragments dev server.
|
|
5
|
+
* Provides typed methods for all server endpoints.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
ComplianceResult,
|
|
10
|
+
SegmentInfo,
|
|
11
|
+
ContextData,
|
|
12
|
+
ViolationItem,
|
|
13
|
+
A11yResult,
|
|
14
|
+
} from './types.js';
|
|
15
|
+
|
|
16
|
+
export interface DevServerClientOptions {
|
|
17
|
+
/** Base URL of the dev server */
|
|
18
|
+
baseUrl: string;
|
|
19
|
+
/** Request timeout in milliseconds */
|
|
20
|
+
timeout?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Request body for compliance endpoint
|
|
25
|
+
*/
|
|
26
|
+
export interface ComplianceRequest {
|
|
27
|
+
/** Component name */
|
|
28
|
+
component: string;
|
|
29
|
+
/** Variant name (optional) */
|
|
30
|
+
variant?: string;
|
|
31
|
+
/** Theme to use (default: "default") */
|
|
32
|
+
theme?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Client for interacting with the fragments dev server
|
|
37
|
+
*/
|
|
38
|
+
export class DevServerClient {
|
|
39
|
+
private baseUrl: string;
|
|
40
|
+
private timeout: number;
|
|
41
|
+
|
|
42
|
+
constructor(options: DevServerClientOptions) {
|
|
43
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, ''); // Remove trailing slash
|
|
44
|
+
this.timeout = options.timeout ?? 30000;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if the dev server is reachable
|
|
49
|
+
*/
|
|
50
|
+
async ping(): Promise<boolean> {
|
|
51
|
+
try {
|
|
52
|
+
const controller = new AbortController();
|
|
53
|
+
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
54
|
+
|
|
55
|
+
const response = await fetch(`${this.baseUrl}/segments/context?format=json`, {
|
|
56
|
+
signal: controller.signal,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
clearTimeout(timeoutId);
|
|
60
|
+
return response.ok;
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get all segments from the context endpoint
|
|
68
|
+
*/
|
|
69
|
+
async getSegments(): Promise<SegmentInfo[]> {
|
|
70
|
+
const response = await this.fetch('/segments/context?format=json');
|
|
71
|
+
const data = await response.json() as {
|
|
72
|
+
components: Record<string, { category?: string; description?: string; status?: string; figma?: string }>;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Transform object to array of SegmentInfo
|
|
76
|
+
const components = data.components || {};
|
|
77
|
+
return Object.entries(components).map(([name, info]) => ({
|
|
78
|
+
name,
|
|
79
|
+
category: info.category || 'components',
|
|
80
|
+
description: info.description,
|
|
81
|
+
status: info.status,
|
|
82
|
+
figma: info.figma,
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get compliance data for a component
|
|
88
|
+
*/
|
|
89
|
+
async getCompliance(request: ComplianceRequest): Promise<ComplianceResult> {
|
|
90
|
+
const response = await this.fetch('/segments/compliance', {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: { 'Content-Type': 'application/json' },
|
|
93
|
+
body: JSON.stringify(request),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const data = await response.json() as ComplianceResult;
|
|
97
|
+
return data;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get accessibility results for a component
|
|
102
|
+
*/
|
|
103
|
+
async getA11y(component: string, variant?: string): Promise<A11yResult> {
|
|
104
|
+
const response = await this.fetch('/fragments/a11y', {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
headers: { 'Content-Type': 'application/json' },
|
|
107
|
+
body: JSON.stringify({ component, variant }),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const data = await response.json() as A11yResult;
|
|
111
|
+
return data;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Internal fetch wrapper with error handling
|
|
116
|
+
*/
|
|
117
|
+
private async fetch(path: string, options?: RequestInit): Promise<Response> {
|
|
118
|
+
const url = `${this.baseUrl}${path}`;
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const controller = new AbortController();
|
|
122
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
123
|
+
|
|
124
|
+
const response = await fetch(url, {
|
|
125
|
+
...options,
|
|
126
|
+
signal: controller.signal,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
clearTimeout(timeoutId);
|
|
130
|
+
|
|
131
|
+
if (!response.ok) {
|
|
132
|
+
const errorBody = await response.text().catch(() => '');
|
|
133
|
+
throw new DevServerError(
|
|
134
|
+
`Server returned ${response.status}: ${response.statusText}`,
|
|
135
|
+
response.status,
|
|
136
|
+
errorBody
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return response;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (error instanceof DevServerError) {
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Handle connection errors
|
|
147
|
+
const errMsg = error instanceof Error && error.cause
|
|
148
|
+
? String((error.cause as { code?: string }).code || error.message)
|
|
149
|
+
: error instanceof Error
|
|
150
|
+
? error.message
|
|
151
|
+
: 'Unknown error';
|
|
152
|
+
|
|
153
|
+
if (errMsg.includes('ECONNREFUSED') || errMsg.includes('fetch failed')) {
|
|
154
|
+
throw new DevServerConnectionError(
|
|
155
|
+
`Cannot connect to dev server at ${this.baseUrl}`,
|
|
156
|
+
this.baseUrl
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Error thrown when the dev server returns an error response
|
|
167
|
+
*/
|
|
168
|
+
export class DevServerError extends Error {
|
|
169
|
+
constructor(
|
|
170
|
+
message: string,
|
|
171
|
+
public readonly statusCode: number,
|
|
172
|
+
public readonly body?: string
|
|
173
|
+
) {
|
|
174
|
+
super(message);
|
|
175
|
+
this.name = 'DevServerError';
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Error thrown when we can't connect to the dev server
|
|
181
|
+
*/
|
|
182
|
+
export class DevServerConnectionError extends Error {
|
|
183
|
+
constructor(
|
|
184
|
+
message: string,
|
|
185
|
+
public readonly serverUrl: string
|
|
186
|
+
) {
|
|
187
|
+
super(message);
|
|
188
|
+
this.name = 'DevServerConnectionError';
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Create a dev server client with default options
|
|
194
|
+
*/
|
|
195
|
+
export function createDevServerClient(port: number | string = 6006): DevServerClient {
|
|
196
|
+
return new DevServerClient({
|
|
197
|
+
baseUrl: `http://localhost:${port}`,
|
|
198
|
+
});
|
|
199
|
+
}
|