@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,658 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storybook to Segments Converter
|
|
3
|
+
*
|
|
4
|
+
* Transforms parsed Storybook data into Segment definitions.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ParsedStoryFile, ParsedArgType, ConversionResult } from "./types.js";
|
|
8
|
+
import { storyNameToTitle, extractCategory } from "./parser.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Sanitize a component name to be a valid JavaScript identifier.
|
|
12
|
+
* "No recent searches" -> "NoRecentSearches"
|
|
13
|
+
* "My-Component" -> "MyComponent"
|
|
14
|
+
*/
|
|
15
|
+
function sanitizeComponentName(name: string): string {
|
|
16
|
+
// Convert to PascalCase: remove spaces/hyphens, capitalize each word
|
|
17
|
+
return name
|
|
18
|
+
.split(/[\s-_]+/)
|
|
19
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
20
|
+
.join("")
|
|
21
|
+
.replace(/[^a-zA-Z0-9]/g, ""); // Remove any remaining invalid chars
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Convert a parsed story file into a segment definition.
|
|
26
|
+
*/
|
|
27
|
+
export function convertToSegment(parsed: ParsedStoryFile): ConversionResult {
|
|
28
|
+
const warnings: string[] = [...parsed.warnings];
|
|
29
|
+
const todos: string[] = [];
|
|
30
|
+
|
|
31
|
+
// Extract category and name from title
|
|
32
|
+
const category = extractCategory(parsed.meta.title);
|
|
33
|
+
const componentName = sanitizeComponentName(parsed.meta.componentName);
|
|
34
|
+
|
|
35
|
+
// Check if we have a valid component import path
|
|
36
|
+
// If not, the story likely defines the component locally or uses a pattern we can't parse
|
|
37
|
+
if (!parsed.meta.componentImport) {
|
|
38
|
+
warnings.push(`No importable component found - story may define component locally`);
|
|
39
|
+
|
|
40
|
+
// Determine output file path for the error result
|
|
41
|
+
const outputFile = parsed.filePath
|
|
42
|
+
.replace(/\.stories\.(tsx?|jsx?|mdx)$/, ".segment.tsx");
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
sourceFile: parsed.filePath,
|
|
46
|
+
outputFile,
|
|
47
|
+
code: "",
|
|
48
|
+
componentName,
|
|
49
|
+
category,
|
|
50
|
+
variantCount: 0,
|
|
51
|
+
propCount: 0,
|
|
52
|
+
confidence: 0,
|
|
53
|
+
todos: [],
|
|
54
|
+
warnings,
|
|
55
|
+
success: false,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Convert argTypes to props
|
|
60
|
+
const props = convertArgTypesToProps(parsed.argTypes);
|
|
61
|
+
|
|
62
|
+
// Convert stories to variants
|
|
63
|
+
const variants = convertStoriesToVariants(parsed, componentName);
|
|
64
|
+
|
|
65
|
+
// Track what's missing (needs human input)
|
|
66
|
+
todos.push("Add usage.when - scenarios where this component is appropriate");
|
|
67
|
+
todos.push("Add usage.whenNot - scenarios where alternatives should be used");
|
|
68
|
+
todos.push("Add usage.guidelines - best practices");
|
|
69
|
+
todos.push("Add relations - related components");
|
|
70
|
+
|
|
71
|
+
if (Object.keys(props).length > 0) {
|
|
72
|
+
const propsWithoutConstraints = Object.entries(props)
|
|
73
|
+
.filter(([, p]) => !p.constraints?.length)
|
|
74
|
+
.map(([name]) => name);
|
|
75
|
+
|
|
76
|
+
if (propsWithoutConstraints.length > 0) {
|
|
77
|
+
todos.push(`Add constraints for props: ${propsWithoutConstraints.slice(0, 3).join(", ")}${propsWithoutConstraints.length > 3 ? "..." : ""}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Extract skipped variants info
|
|
82
|
+
const skippedVariants = variants
|
|
83
|
+
.filter(v => v.needsManualReview && v.skipReason)
|
|
84
|
+
.map(v => ({ name: v.name, reason: v.skipReason! }));
|
|
85
|
+
|
|
86
|
+
// Generate the segment code with _generated metadata
|
|
87
|
+
const code = generateSegmentCode({
|
|
88
|
+
componentName,
|
|
89
|
+
componentImport: parsed.meta.componentImport,
|
|
90
|
+
description: parsed.meta.description,
|
|
91
|
+
category,
|
|
92
|
+
tags: parsed.meta.tags,
|
|
93
|
+
props,
|
|
94
|
+
variants,
|
|
95
|
+
todos,
|
|
96
|
+
generated: {
|
|
97
|
+
source: "storybook",
|
|
98
|
+
sourceFile: parsed.filePath,
|
|
99
|
+
confidence: parsed.confidence,
|
|
100
|
+
timestamp: new Date().toISOString(),
|
|
101
|
+
skippedVariants: skippedVariants.length > 0 ? skippedVariants : undefined,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Determine output file path
|
|
106
|
+
const outputFile = parsed.filePath
|
|
107
|
+
.replace(/\.stories\.(tsx?|jsx?|mdx)$/, ".segment.tsx");
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
sourceFile: parsed.filePath,
|
|
111
|
+
outputFile,
|
|
112
|
+
code,
|
|
113
|
+
componentName,
|
|
114
|
+
category,
|
|
115
|
+
variantCount: variants.length,
|
|
116
|
+
propCount: Object.keys(props).length,
|
|
117
|
+
confidence: parsed.confidence,
|
|
118
|
+
todos,
|
|
119
|
+
warnings,
|
|
120
|
+
success: true,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Convert Storybook argTypes to Segments props.
|
|
126
|
+
*/
|
|
127
|
+
function convertArgTypesToProps(
|
|
128
|
+
argTypes: Record<string, ParsedArgType>
|
|
129
|
+
): Record<string, PropDef> {
|
|
130
|
+
const props: Record<string, PropDef> = {};
|
|
131
|
+
|
|
132
|
+
for (const [name, argType] of Object.entries(argTypes)) {
|
|
133
|
+
props[name] = convertArgType(name, argType);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return props;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
interface PropDef {
|
|
140
|
+
type: string;
|
|
141
|
+
description?: string;
|
|
142
|
+
default?: unknown;
|
|
143
|
+
required?: boolean;
|
|
144
|
+
values?: string[];
|
|
145
|
+
constraints?: string[];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Convert a single argType to a prop definition.
|
|
150
|
+
*/
|
|
151
|
+
function convertArgType(name: string, argType: ParsedArgType): PropDef {
|
|
152
|
+
const prop: PropDef = {
|
|
153
|
+
type: inferPropType(argType),
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
if (argType.description) {
|
|
157
|
+
prop.description = argType.description;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (argType.defaultValue !== undefined) {
|
|
161
|
+
prop.default = argType.defaultValue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (argType.required) {
|
|
165
|
+
prop.required = true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Add enum values if present
|
|
169
|
+
if (argType.options && argType.options.length > 0) {
|
|
170
|
+
prop.type = "enum";
|
|
171
|
+
prop.values = argType.options;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return prop;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Infer prop type from Storybook control type.
|
|
179
|
+
*/
|
|
180
|
+
function inferPropType(argType: ParsedArgType): string {
|
|
181
|
+
// If explicit type is provided
|
|
182
|
+
if (argType.type) {
|
|
183
|
+
const typeMap: Record<string, string> = {
|
|
184
|
+
string: "string",
|
|
185
|
+
number: "number",
|
|
186
|
+
boolean: "boolean",
|
|
187
|
+
object: "object",
|
|
188
|
+
array: "array",
|
|
189
|
+
function: "function",
|
|
190
|
+
};
|
|
191
|
+
return typeMap[argType.type.toLowerCase()] ?? "custom";
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Infer from control type
|
|
195
|
+
if (argType.control) {
|
|
196
|
+
const controlMap: Record<string, string> = {
|
|
197
|
+
text: "string",
|
|
198
|
+
number: "number",
|
|
199
|
+
boolean: "boolean",
|
|
200
|
+
select: "enum",
|
|
201
|
+
radio: "enum",
|
|
202
|
+
"inline-radio": "enum",
|
|
203
|
+
check: "boolean",
|
|
204
|
+
"inline-check": "boolean",
|
|
205
|
+
range: "number",
|
|
206
|
+
object: "object",
|
|
207
|
+
array: "array",
|
|
208
|
+
date: "string",
|
|
209
|
+
color: "string",
|
|
210
|
+
};
|
|
211
|
+
return controlMap[argType.control] ?? "custom";
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// If has options, it's an enum
|
|
215
|
+
if (argType.options && argType.options.length > 0) {
|
|
216
|
+
return "enum";
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return "custom";
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
interface VariantDef {
|
|
223
|
+
name: string;
|
|
224
|
+
description: string;
|
|
225
|
+
renderCode: string;
|
|
226
|
+
needsManualReview: boolean;
|
|
227
|
+
skipReason?: string;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Convert stories to variant definitions.
|
|
232
|
+
*
|
|
233
|
+
* Stories with custom render functions or unrenderable args are marked
|
|
234
|
+
* as needing manual review since we can't reliably extract complex JSX.
|
|
235
|
+
* For these, use the viewer's native story support instead of migration.
|
|
236
|
+
*/
|
|
237
|
+
function convertStoriesToVariants(
|
|
238
|
+
parsed: ParsedStoryFile,
|
|
239
|
+
componentName: string
|
|
240
|
+
): VariantDef[] {
|
|
241
|
+
return parsed.stories.map((story) => {
|
|
242
|
+
// Check if story has custom render or args we can't statically render
|
|
243
|
+
const hasCustomRender = story.hasCustomRender === true;
|
|
244
|
+
const unrenderableReason = getUnrenderableReason(story.args);
|
|
245
|
+
const needsManualReview = hasCustomRender || unrenderableReason !== null;
|
|
246
|
+
|
|
247
|
+
// Determine skip reason
|
|
248
|
+
let skipReason: string | undefined;
|
|
249
|
+
if (hasCustomRender) {
|
|
250
|
+
skipReason = "uses custom render function";
|
|
251
|
+
} else if (unrenderableReason) {
|
|
252
|
+
skipReason = unrenderableReason;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Generate render code - will include comments for unrenderable values
|
|
256
|
+
const renderCode = hasCustomRender
|
|
257
|
+
? `<${componentName} />` // Placeholder for custom render
|
|
258
|
+
: generateRenderCode(componentName, story.args);
|
|
259
|
+
|
|
260
|
+
const description = story.description ??
|
|
261
|
+
`${storyNameToTitle(story.name)} variant`;
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
name: storyNameToTitle(story.name),
|
|
265
|
+
description,
|
|
266
|
+
renderCode,
|
|
267
|
+
needsManualReview,
|
|
268
|
+
skipReason,
|
|
269
|
+
};
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Check if args contain values that can't be statically rendered.
|
|
275
|
+
* Returns the reason string if unrenderable, null if renderable.
|
|
276
|
+
*/
|
|
277
|
+
function getUnrenderableReason(args: Record<string, unknown>, path: string = ""): string | null {
|
|
278
|
+
for (const [key, value] of Object.entries(args)) {
|
|
279
|
+
const currentPath = path ? `${path}.${key}` : key;
|
|
280
|
+
if (typeof value === "string") {
|
|
281
|
+
// JSX, expressions, and variable references can't be rendered statically
|
|
282
|
+
if (value === "__JSX__") {
|
|
283
|
+
return `JSX element in prop "${currentPath}"`;
|
|
284
|
+
}
|
|
285
|
+
if (value === "__EXPR__") {
|
|
286
|
+
return `expression in prop "${currentPath}"`;
|
|
287
|
+
}
|
|
288
|
+
if (value.startsWith("__REF__")) {
|
|
289
|
+
const ref = value.slice(7);
|
|
290
|
+
return `variable reference "${ref}" in prop "${currentPath}"`;
|
|
291
|
+
}
|
|
292
|
+
if (value === "__SPREAD__" || key === "__SPREAD__") {
|
|
293
|
+
return `spread syntax in args`;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (typeof value === "object" && value !== null) {
|
|
297
|
+
const nestedReason = getUnrenderableReason(value as Record<string, unknown>, currentPath);
|
|
298
|
+
if (nestedReason) {
|
|
299
|
+
return nestedReason;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Backward compatibility wrapper
|
|
307
|
+
function hasUnrenderableArgs(args: Record<string, unknown>): boolean {
|
|
308
|
+
return getUnrenderableReason(args) !== null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Generate render code from component name and args.
|
|
313
|
+
*/
|
|
314
|
+
function generateRenderCode(
|
|
315
|
+
componentName: string,
|
|
316
|
+
args: Record<string, unknown>
|
|
317
|
+
): string {
|
|
318
|
+
const entries = Object.entries(args);
|
|
319
|
+
|
|
320
|
+
if (entries.length === 0) {
|
|
321
|
+
return `<${componentName} />`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Check for children
|
|
325
|
+
const children = args.children;
|
|
326
|
+
const otherArgs = Object.entries(args).filter(([k]) => k !== "children");
|
|
327
|
+
|
|
328
|
+
// Build props string
|
|
329
|
+
const propsString = otherArgs
|
|
330
|
+
.map(([key, value]) => formatPropValue(key, value))
|
|
331
|
+
.filter(Boolean)
|
|
332
|
+
.join(" ");
|
|
333
|
+
|
|
334
|
+
if (children !== undefined) {
|
|
335
|
+
const childrenStr = formatChildrenValue(children);
|
|
336
|
+
return propsString
|
|
337
|
+
? `<${componentName} ${propsString}>${childrenStr}</${componentName}>`
|
|
338
|
+
: `<${componentName}>${childrenStr}</${componentName}>`;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return propsString
|
|
342
|
+
? `<${componentName} ${propsString} />`
|
|
343
|
+
: `<${componentName} />`;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Format children value for JSX.
|
|
348
|
+
*/
|
|
349
|
+
function formatChildrenValue(value: unknown): string {
|
|
350
|
+
if (typeof value === "string") {
|
|
351
|
+
// Handle special markers
|
|
352
|
+
if (value === "__JSX__") return "{null /* JSX children */}";
|
|
353
|
+
if (value === "__EXPR__") return "{null /* expression */}";
|
|
354
|
+
if (value.startsWith("__REF__")) return `{${value.slice(7)}}`;
|
|
355
|
+
return value;
|
|
356
|
+
}
|
|
357
|
+
return String(value);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Format a prop value for JSX.
|
|
362
|
+
*/
|
|
363
|
+
function formatPropValue(key: string, value: unknown): string {
|
|
364
|
+
if (value === undefined || value === null) {
|
|
365
|
+
return "";
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (typeof value === "string") {
|
|
369
|
+
// Handle special markers from parser
|
|
370
|
+
if (value === "__JSX__") {
|
|
371
|
+
// Use undefined as placeholder - JSX comments can't be prop values
|
|
372
|
+
return `${key}={undefined /* JSX */}`;
|
|
373
|
+
}
|
|
374
|
+
if (value === "__EXPR__") {
|
|
375
|
+
return `${key}={undefined /* expression */}`;
|
|
376
|
+
}
|
|
377
|
+
if (value.startsWith("__REF__")) {
|
|
378
|
+
// Variable reference - output as expression
|
|
379
|
+
return `${key}={${value.slice(7)}}`;
|
|
380
|
+
}
|
|
381
|
+
// For strings with special characters (quotes, backslashes, newlines),
|
|
382
|
+
// use JSX expression syntax {""} instead of attribute syntax ""
|
|
383
|
+
// This ensures proper JavaScript string escaping
|
|
384
|
+
if (value.includes('"') || value.includes("\\") || value.includes("\n")) {
|
|
385
|
+
return `${key}={"${escapeString(value)}"}`;
|
|
386
|
+
}
|
|
387
|
+
return `${key}="${value}"`;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (typeof value === "boolean") {
|
|
391
|
+
return value ? key : `${key}={false}`;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (typeof value === "number") {
|
|
395
|
+
return `${key}={${value}}`;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (Array.isArray(value)) {
|
|
399
|
+
// Format array value
|
|
400
|
+
const formatted = formatArrayValue(value);
|
|
401
|
+
return `${key}={${formatted}}`;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (typeof value === "object" && value !== null) {
|
|
405
|
+
// Format nested object
|
|
406
|
+
const formatted = formatObjectValue(value as Record<string, unknown>);
|
|
407
|
+
return `${key}={${formatted}}`;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// For other complex values, use JSON
|
|
411
|
+
return `${key}={${JSON.stringify(value)}}`;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Format a nested object value for JSX.
|
|
416
|
+
*/
|
|
417
|
+
function formatObjectValue(obj: Record<string, unknown>): string {
|
|
418
|
+
const entries = Object.entries(obj);
|
|
419
|
+
if (entries.length === 0) return "{}";
|
|
420
|
+
|
|
421
|
+
const props = entries
|
|
422
|
+
.map(([k, v]) => {
|
|
423
|
+
const formatted = formatValueForObject(v);
|
|
424
|
+
return `${k}: ${formatted}`;
|
|
425
|
+
})
|
|
426
|
+
.join(", ");
|
|
427
|
+
|
|
428
|
+
return `{ ${props} }`;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Format an array value for JSX.
|
|
433
|
+
*/
|
|
434
|
+
function formatArrayValue(arr: unknown[]): string {
|
|
435
|
+
if (arr.length === 0) return "[]";
|
|
436
|
+
|
|
437
|
+
const items = arr.map((item) => formatValueForObject(item));
|
|
438
|
+
return `[${items.join(", ")}]`;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Format a value for use inside an object/array literal.
|
|
443
|
+
*/
|
|
444
|
+
function formatValueForObject(value: unknown): string {
|
|
445
|
+
if (value === undefined) return "undefined";
|
|
446
|
+
if (value === null) return "null";
|
|
447
|
+
|
|
448
|
+
if (typeof value === "string") {
|
|
449
|
+
// Handle special markers - use valid JS values with comments
|
|
450
|
+
if (value === "__JSX__") return "undefined /* JSX */";
|
|
451
|
+
if (value === "__EXPR__") return "undefined /* expression */";
|
|
452
|
+
if (value.startsWith("__REF__")) return value.slice(7);
|
|
453
|
+
return `"${escapeString(value)}"`;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (typeof value === "boolean" || typeof value === "number") {
|
|
457
|
+
return String(value);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (Array.isArray(value)) {
|
|
461
|
+
return formatArrayValue(value);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (typeof value === "object") {
|
|
465
|
+
return formatObjectValue(value as Record<string, unknown>);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return JSON.stringify(value);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
interface SkippedVariant {
|
|
472
|
+
name: string;
|
|
473
|
+
reason: string;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
interface GeneratedMetadata {
|
|
477
|
+
source: "storybook" | "manual" | "ai";
|
|
478
|
+
sourceFile: string;
|
|
479
|
+
confidence: number;
|
|
480
|
+
timestamp: string;
|
|
481
|
+
skippedVariants?: SkippedVariant[];
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
interface GenerateOptions {
|
|
485
|
+
componentName: string;
|
|
486
|
+
componentImport: string;
|
|
487
|
+
description?: string;
|
|
488
|
+
category: string;
|
|
489
|
+
tags?: string[];
|
|
490
|
+
props: Record<string, PropDef>;
|
|
491
|
+
variants: VariantDef[];
|
|
492
|
+
todos: string[];
|
|
493
|
+
generated?: GeneratedMetadata;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Generate the full segment file code.
|
|
498
|
+
*
|
|
499
|
+
* Note: We use a placeholder component instead of importing the real component.
|
|
500
|
+
* This allows the segment build to work without needing all component dependencies.
|
|
501
|
+
* The viewer will load the actual component at runtime.
|
|
502
|
+
*/
|
|
503
|
+
function generateSegmentCode(options: GenerateOptions): string {
|
|
504
|
+
const {
|
|
505
|
+
componentName,
|
|
506
|
+
componentImport,
|
|
507
|
+
description,
|
|
508
|
+
category,
|
|
509
|
+
tags,
|
|
510
|
+
props,
|
|
511
|
+
variants,
|
|
512
|
+
todos,
|
|
513
|
+
generated,
|
|
514
|
+
} = options;
|
|
515
|
+
|
|
516
|
+
// Format props
|
|
517
|
+
const propsCode = formatPropsCode(props);
|
|
518
|
+
|
|
519
|
+
// Format variants - stories with custom renders get TODO comments
|
|
520
|
+
const variantsCode = formatVariantsCode(componentName, variants);
|
|
521
|
+
|
|
522
|
+
// Format tags
|
|
523
|
+
const tagsCode = tags && tags.length > 0
|
|
524
|
+
? `tags: [${tags.map((t) => `"${t}"`).join(", ")}],`
|
|
525
|
+
: "";
|
|
526
|
+
|
|
527
|
+
// Format TODOs as comments
|
|
528
|
+
const todosComments = todos.length > 0
|
|
529
|
+
? todos.map((t) => ` // TODO: ${t}`).join("\n") + "\n"
|
|
530
|
+
: "";
|
|
531
|
+
|
|
532
|
+
// Format _generated metadata
|
|
533
|
+
let generatedCode = "";
|
|
534
|
+
if (generated) {
|
|
535
|
+
const skippedCode = generated.skippedVariants && generated.skippedVariants.length > 0
|
|
536
|
+
? `
|
|
537
|
+
skippedVariants: [
|
|
538
|
+
${generated.skippedVariants.map(sv => ` { name: "${escapeString(sv.name)}", reason: "${escapeString(sv.reason)}" },`).join("\n")}
|
|
539
|
+
],`
|
|
540
|
+
: "";
|
|
541
|
+
|
|
542
|
+
generatedCode = `
|
|
543
|
+
_generated: {
|
|
544
|
+
source: "${generated.source}",
|
|
545
|
+
sourceFile: "${escapeString(generated.sourceFile)}",
|
|
546
|
+
confidence: ${generated.confidence.toFixed(2)},
|
|
547
|
+
timestamp: "${generated.timestamp}",${skippedCode}
|
|
548
|
+
},
|
|
549
|
+
`;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Import the actual component - this makes the segment immediately usable
|
|
553
|
+
return `import { defineSegment } from "@fragments/core";
|
|
554
|
+
import { ${componentName} } from "${componentImport}";
|
|
555
|
+
|
|
556
|
+
export default defineSegment({
|
|
557
|
+
component: ${componentName},
|
|
558
|
+
|
|
559
|
+
meta: {
|
|
560
|
+
name: "${componentName}",
|
|
561
|
+
description: "${escapeString(description ?? `${componentName} component`)}",
|
|
562
|
+
category: "${category}",
|
|
563
|
+
${tagsCode}
|
|
564
|
+
// status: undefined, // TODO: Set to stable/beta/deprecated/experimental
|
|
565
|
+
},
|
|
566
|
+
|
|
567
|
+
usage: {
|
|
568
|
+
// TODO: Add specific use cases - when should developers use this component?
|
|
569
|
+
when: [
|
|
570
|
+
${todosComments} ],
|
|
571
|
+
// TODO: Add anti-patterns - when should developers NOT use this component?
|
|
572
|
+
whenNot: [],
|
|
573
|
+
},
|
|
574
|
+
|
|
575
|
+
${propsCode}
|
|
576
|
+
|
|
577
|
+
relations: [
|
|
578
|
+
// TODO: Add related components
|
|
579
|
+
],
|
|
580
|
+
|
|
581
|
+
${variantsCode}
|
|
582
|
+
${generatedCode}});
|
|
583
|
+
`;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Format props object for code generation.
|
|
588
|
+
*/
|
|
589
|
+
function formatPropsCode(props: Record<string, PropDef>): string {
|
|
590
|
+
if (Object.keys(props).length === 0) {
|
|
591
|
+
return " props: {},";
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const entries = Object.entries(props).map(([name, prop]) => {
|
|
595
|
+
const lines: string[] = [];
|
|
596
|
+
lines.push(` ${name}: {`);
|
|
597
|
+
lines.push(` type: "${prop.type}",`);
|
|
598
|
+
|
|
599
|
+
if (prop.values && prop.values.length > 0) {
|
|
600
|
+
lines.push(` values: [${prop.values.map((v) => `"${v}"`).join(", ")}],`);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (prop.default !== undefined) {
|
|
604
|
+
const defaultVal = typeof prop.default === "string"
|
|
605
|
+
? `"${prop.default}"`
|
|
606
|
+
: String(prop.default);
|
|
607
|
+
lines.push(` default: ${defaultVal},`);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (prop.required) {
|
|
611
|
+
lines.push(` required: true,`);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (prop.description) {
|
|
615
|
+
lines.push(` description: "${escapeString(prop.description)}",`);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
lines.push(` },`);
|
|
619
|
+
return lines.join("\n");
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
return ` props: {\n${entries.join("\n")}\n },`;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Format variants array for code generation.
|
|
627
|
+
*
|
|
628
|
+
* Variants needing manual review get a placeholder render that won't crash.
|
|
629
|
+
* This makes the segment file a starting point that humans can enhance.
|
|
630
|
+
*/
|
|
631
|
+
function formatVariantsCode(componentName: string, variants: VariantDef[]): string {
|
|
632
|
+
// Filter out variants that can't be rendered - they would cause runtime errors
|
|
633
|
+
const renderableVariants = variants.filter((v) => !v.needsManualReview);
|
|
634
|
+
|
|
635
|
+
if (renderableVariants.length === 0) {
|
|
636
|
+
return " variants: [],";
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const entries = renderableVariants.map((variant) => {
|
|
640
|
+
return ` {
|
|
641
|
+
name: "${variant.name}",
|
|
642
|
+
description: "${escapeString(variant.description)}",
|
|
643
|
+
render: () => ${variant.renderCode},
|
|
644
|
+
},`;
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
return ` variants: [\n${entries.join("\n")}\n ],`;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Escape special characters in strings for code generation.
|
|
652
|
+
*/
|
|
653
|
+
function escapeString(str: string): string {
|
|
654
|
+
return str
|
|
655
|
+
.replace(/\\/g, "\\\\")
|
|
656
|
+
.replace(/"/g, '\\"')
|
|
657
|
+
.replace(/\n/g, "\\n");
|
|
658
|
+
}
|