@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,485 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSX Parser
|
|
3
|
+
*
|
|
4
|
+
* Parses JSX strings into a structured component tree that can be
|
|
5
|
+
* used for rendering multi-component compositions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { parse } from "@babel/parser";
|
|
9
|
+
import type {
|
|
10
|
+
JSXElement,
|
|
11
|
+
JSXFragment,
|
|
12
|
+
JSXText,
|
|
13
|
+
JSXExpressionContainer,
|
|
14
|
+
JSXAttribute,
|
|
15
|
+
JSXSpreadAttribute,
|
|
16
|
+
Expression,
|
|
17
|
+
Node,
|
|
18
|
+
} from "@babel/types";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Represents a parsed JSX component node
|
|
22
|
+
*/
|
|
23
|
+
export interface ComponentNode {
|
|
24
|
+
/** Component name (e.g., "Button", "Card") or HTML element name (e.g., "div") */
|
|
25
|
+
name: string;
|
|
26
|
+
/** Whether this is an HTML element (lowercase) vs custom component (PascalCase) */
|
|
27
|
+
isHtmlElement: boolean;
|
|
28
|
+
/** Props/attributes passed to the component */
|
|
29
|
+
props: Record<string, unknown>;
|
|
30
|
+
/** Child nodes (can be ComponentNode, string, or expression) */
|
|
31
|
+
children: Array<ComponentNode | string>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Result of parsing a JSX string
|
|
36
|
+
*/
|
|
37
|
+
export interface ParseResult {
|
|
38
|
+
/** The root component tree */
|
|
39
|
+
tree: ComponentNode | ComponentNode[];
|
|
40
|
+
/** List of all unique component names found (excluding HTML elements) */
|
|
41
|
+
components: string[];
|
|
42
|
+
/** Any parsing errors */
|
|
43
|
+
errors: string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Parse a JSX string into a component tree.
|
|
48
|
+
*
|
|
49
|
+
* @param jsx - The JSX string to parse
|
|
50
|
+
* @returns ParseResult with the component tree and metadata
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* const result = parseJSX('<Card><Button variant="primary">Click</Button></Card>');
|
|
55
|
+
* // result.tree = { name: 'Card', props: {}, children: [{ name: 'Button', props: { variant: 'primary' }, children: ['Click'] }] }
|
|
56
|
+
* // result.components = ['Card', 'Button']
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export function parseJSX(jsx: string): ParseResult {
|
|
60
|
+
const errors: string[] = [];
|
|
61
|
+
const components = new Set<string>();
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
// Wrap in a fragment to handle multiple root elements
|
|
65
|
+
const wrappedJsx = `<>${jsx}</>`;
|
|
66
|
+
|
|
67
|
+
const ast = parse(wrappedJsx, {
|
|
68
|
+
sourceType: "module",
|
|
69
|
+
plugins: ["jsx"],
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Find the expression statement containing our JSX
|
|
73
|
+
const program = ast.program;
|
|
74
|
+
const firstStatement = program.body[0];
|
|
75
|
+
|
|
76
|
+
if (firstStatement?.type !== "ExpressionStatement") {
|
|
77
|
+
return {
|
|
78
|
+
tree: [],
|
|
79
|
+
components: [],
|
|
80
|
+
errors: ["Invalid JSX: expected expression statement"],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const expression = firstStatement.expression;
|
|
85
|
+
|
|
86
|
+
if (expression.type !== "JSXFragment" && expression.type !== "JSXElement") {
|
|
87
|
+
return {
|
|
88
|
+
tree: [],
|
|
89
|
+
components: [],
|
|
90
|
+
errors: ["Invalid JSX: expected JSX element or fragment"],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Parse the JSX tree
|
|
95
|
+
const parseNode = (
|
|
96
|
+
node: JSXElement | JSXFragment | JSXText | JSXExpressionContainer
|
|
97
|
+
): ComponentNode | string | null => {
|
|
98
|
+
if (node.type === "JSXText") {
|
|
99
|
+
const text = node.value.trim();
|
|
100
|
+
return text || null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (node.type === "JSXExpressionContainer") {
|
|
104
|
+
// Handle expressions like {variable} or {condition && <Component />}
|
|
105
|
+
const exprValue = evaluateExpression(node.expression);
|
|
106
|
+
// Convert to string or return null if not renderable
|
|
107
|
+
if (exprValue === null || exprValue === undefined) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
if (typeof exprValue === "string") {
|
|
111
|
+
return exprValue;
|
|
112
|
+
}
|
|
113
|
+
// For non-string primitives, convert to string
|
|
114
|
+
return String(exprValue);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (node.type === "JSXFragment") {
|
|
118
|
+
// For fragments, return children directly
|
|
119
|
+
const children = node.children
|
|
120
|
+
.map((child) => parseNode(child as JSXElement | JSXFragment | JSXText | JSXExpressionContainer))
|
|
121
|
+
.filter((c): c is ComponentNode | string => c !== null);
|
|
122
|
+
|
|
123
|
+
// If only one child, return it directly
|
|
124
|
+
if (children.length === 1 && typeof children[0] !== "string") {
|
|
125
|
+
return children[0];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Return a special fragment node
|
|
129
|
+
return {
|
|
130
|
+
name: "Fragment",
|
|
131
|
+
isHtmlElement: false,
|
|
132
|
+
props: {},
|
|
133
|
+
children,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (node.type === "JSXElement") {
|
|
138
|
+
const openingElement = node.openingElement;
|
|
139
|
+
let name: string;
|
|
140
|
+
|
|
141
|
+
if (openingElement.name.type === "JSXIdentifier") {
|
|
142
|
+
name = openingElement.name.name;
|
|
143
|
+
} else if (openingElement.name.type === "JSXMemberExpression") {
|
|
144
|
+
// Handle things like MyLib.Button
|
|
145
|
+
name = getMemberExpressionName(openingElement.name);
|
|
146
|
+
} else {
|
|
147
|
+
name = "Unknown";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Check if it's an HTML element (lowercase first letter)
|
|
151
|
+
const isHtmlElement = /^[a-z]/.test(name);
|
|
152
|
+
|
|
153
|
+
// Track custom components
|
|
154
|
+
if (!isHtmlElement && name !== "Fragment") {
|
|
155
|
+
components.add(name);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Parse props
|
|
159
|
+
const props: Record<string, unknown> = {};
|
|
160
|
+
for (const attr of openingElement.attributes) {
|
|
161
|
+
if (attr.type === "JSXAttribute") {
|
|
162
|
+
const propName = attr.name.type === "JSXIdentifier" ? attr.name.name : String(attr.name.name);
|
|
163
|
+
const propValue = parseAttributeValue(attr);
|
|
164
|
+
props[propName] = propValue;
|
|
165
|
+
} else if (attr.type === "JSXSpreadAttribute") {
|
|
166
|
+
// Handle spread attributes {...props}
|
|
167
|
+
const spreadValue = evaluateExpression(attr.argument);
|
|
168
|
+
if (typeof spreadValue === "object" && spreadValue !== null) {
|
|
169
|
+
Object.assign(props, spreadValue);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Parse children
|
|
175
|
+
const children = node.children
|
|
176
|
+
.map((child) => parseNode(child as JSXElement | JSXFragment | JSXText | JSXExpressionContainer))
|
|
177
|
+
.filter((c): c is ComponentNode | string => c !== null);
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
name,
|
|
181
|
+
isHtmlElement,
|
|
182
|
+
props,
|
|
183
|
+
children,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return null;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// Parse the root
|
|
191
|
+
const rootNode = parseNode(expression);
|
|
192
|
+
|
|
193
|
+
if (rootNode === null) {
|
|
194
|
+
return {
|
|
195
|
+
tree: [],
|
|
196
|
+
components: [],
|
|
197
|
+
errors: ["Failed to parse JSX"],
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// If we got a Fragment with multiple children from our wrapper, unwrap it
|
|
202
|
+
if (
|
|
203
|
+
typeof rootNode !== "string" &&
|
|
204
|
+
rootNode.name === "Fragment" &&
|
|
205
|
+
rootNode.children.length > 0
|
|
206
|
+
) {
|
|
207
|
+
// Check if all children are component nodes
|
|
208
|
+
const componentChildren = rootNode.children.filter(
|
|
209
|
+
(c): c is ComponentNode => typeof c !== "string"
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
if (componentChildren.length === 1) {
|
|
213
|
+
return {
|
|
214
|
+
tree: componentChildren[0],
|
|
215
|
+
components: Array.from(components).sort(),
|
|
216
|
+
errors,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (componentChildren.length > 1) {
|
|
221
|
+
return {
|
|
222
|
+
tree: componentChildren,
|
|
223
|
+
components: Array.from(components).sort(),
|
|
224
|
+
errors,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
tree: typeof rootNode === "string" ? [] : rootNode,
|
|
231
|
+
components: Array.from(components).sort(),
|
|
232
|
+
errors,
|
|
233
|
+
};
|
|
234
|
+
} catch (error) {
|
|
235
|
+
const message = error instanceof Error ? error.message : "Unknown parse error";
|
|
236
|
+
return {
|
|
237
|
+
tree: [],
|
|
238
|
+
components: [],
|
|
239
|
+
errors: [message],
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Get the full name from a JSX member expression (e.g., MyLib.Button)
|
|
246
|
+
*/
|
|
247
|
+
function getMemberExpressionName(node: any): string {
|
|
248
|
+
if (node.type === "JSXIdentifier") {
|
|
249
|
+
return node.name;
|
|
250
|
+
}
|
|
251
|
+
if (node.type === "JSXMemberExpression") {
|
|
252
|
+
return `${getMemberExpressionName(node.object)}.${node.property.name}`;
|
|
253
|
+
}
|
|
254
|
+
return "Unknown";
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Parse a JSX attribute value
|
|
259
|
+
*/
|
|
260
|
+
function parseAttributeValue(attr: JSXAttribute): unknown {
|
|
261
|
+
const value = attr.value;
|
|
262
|
+
|
|
263
|
+
// Boolean attribute (no value means true)
|
|
264
|
+
if (value === null || value === undefined) {
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// String literal
|
|
269
|
+
if (value.type === "StringLiteral") {
|
|
270
|
+
return (value as { value: string }).value;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Expression container {value}
|
|
274
|
+
if (value.type === "JSXExpressionContainer") {
|
|
275
|
+
return evaluateExpression((value as JSXExpressionContainer).expression);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// JSX element as attribute value
|
|
279
|
+
if (value.type === "JSXElement" || value.type === "JSXFragment") {
|
|
280
|
+
// This would be something like <Component prop={<Other />} />
|
|
281
|
+
// We'll return a string representation for now
|
|
282
|
+
return "[JSX Element]";
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Evaluate a JavaScript expression from JSX.
|
|
290
|
+
* This handles common cases but is intentionally limited for safety.
|
|
291
|
+
*/
|
|
292
|
+
function evaluateExpression(expr: Expression | Node): unknown {
|
|
293
|
+
if (!expr || expr.type === "JSXEmptyExpression") {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
switch (expr.type) {
|
|
298
|
+
case "StringLiteral":
|
|
299
|
+
return expr.value;
|
|
300
|
+
|
|
301
|
+
case "NumericLiteral":
|
|
302
|
+
return expr.value;
|
|
303
|
+
|
|
304
|
+
case "BooleanLiteral":
|
|
305
|
+
return expr.value;
|
|
306
|
+
|
|
307
|
+
case "NullLiteral":
|
|
308
|
+
return null;
|
|
309
|
+
|
|
310
|
+
case "Identifier":
|
|
311
|
+
// Handle special identifiers
|
|
312
|
+
if (expr.name === "undefined") return undefined;
|
|
313
|
+
if (expr.name === "true") return true;
|
|
314
|
+
if (expr.name === "false") return false;
|
|
315
|
+
// Return the identifier name as a placeholder
|
|
316
|
+
return `{${expr.name}}`;
|
|
317
|
+
|
|
318
|
+
case "TemplateLiteral":
|
|
319
|
+
// Handle template literals with no expressions
|
|
320
|
+
if (expr.expressions.length === 0 && expr.quasis.length === 1) {
|
|
321
|
+
return expr.quasis[0].value.cooked || expr.quasis[0].value.raw;
|
|
322
|
+
}
|
|
323
|
+
// For template literals with expressions, return a placeholder
|
|
324
|
+
return "[Template Literal]";
|
|
325
|
+
|
|
326
|
+
case "ObjectExpression":
|
|
327
|
+
// Parse object literals like {{ key: 'value' }}
|
|
328
|
+
const obj: Record<string, unknown> = {};
|
|
329
|
+
for (const prop of expr.properties) {
|
|
330
|
+
if (prop.type === "ObjectProperty") {
|
|
331
|
+
const key =
|
|
332
|
+
prop.key.type === "Identifier"
|
|
333
|
+
? prop.key.name
|
|
334
|
+
: prop.key.type === "StringLiteral"
|
|
335
|
+
? prop.key.value
|
|
336
|
+
: String(prop.key);
|
|
337
|
+
obj[key] = evaluateExpression(prop.value as Expression);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return obj;
|
|
341
|
+
|
|
342
|
+
case "ArrayExpression":
|
|
343
|
+
// Parse array literals like {[1, 2, 3]}
|
|
344
|
+
return expr.elements.map((el) =>
|
|
345
|
+
el ? evaluateExpression(el as Expression) : null
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
case "ArrowFunctionExpression":
|
|
349
|
+
case "FunctionExpression":
|
|
350
|
+
// Return a placeholder for functions
|
|
351
|
+
return "[Function]";
|
|
352
|
+
|
|
353
|
+
case "UnaryExpression":
|
|
354
|
+
// Handle negation like {-5}
|
|
355
|
+
if (expr.operator === "-" && expr.argument.type === "NumericLiteral") {
|
|
356
|
+
return -expr.argument.value;
|
|
357
|
+
}
|
|
358
|
+
if (expr.operator === "!" && expr.argument.type === "BooleanLiteral") {
|
|
359
|
+
return !expr.argument.value;
|
|
360
|
+
}
|
|
361
|
+
return null;
|
|
362
|
+
|
|
363
|
+
case "ConditionalExpression":
|
|
364
|
+
// Ternary operator - return a placeholder
|
|
365
|
+
return "[Conditional]";
|
|
366
|
+
|
|
367
|
+
case "LogicalExpression":
|
|
368
|
+
// && or || - return a placeholder
|
|
369
|
+
return "[Logical Expression]";
|
|
370
|
+
|
|
371
|
+
case "MemberExpression":
|
|
372
|
+
// Something like obj.prop - return placeholder
|
|
373
|
+
return "[Member Expression]";
|
|
374
|
+
|
|
375
|
+
case "CallExpression":
|
|
376
|
+
// Function call - return placeholder
|
|
377
|
+
return "[Function Call]";
|
|
378
|
+
|
|
379
|
+
default:
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Extract all component names from a parsed tree
|
|
386
|
+
*/
|
|
387
|
+
export function extractComponents(tree: ComponentNode | ComponentNode[]): string[] {
|
|
388
|
+
const components = new Set<string>();
|
|
389
|
+
|
|
390
|
+
const visit = (node: ComponentNode) => {
|
|
391
|
+
if (!node.isHtmlElement && node.name !== "Fragment") {
|
|
392
|
+
components.add(node.name);
|
|
393
|
+
}
|
|
394
|
+
for (const child of node.children) {
|
|
395
|
+
if (typeof child !== "string") {
|
|
396
|
+
visit(child);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
if (Array.isArray(tree)) {
|
|
402
|
+
for (const node of tree) {
|
|
403
|
+
visit(node);
|
|
404
|
+
}
|
|
405
|
+
} else {
|
|
406
|
+
visit(tree);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return Array.from(components).sort();
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Validate that all components in the tree exist in the available components
|
|
414
|
+
*/
|
|
415
|
+
export function validateComponents(
|
|
416
|
+
tree: ComponentNode | ComponentNode[],
|
|
417
|
+
availableComponents: string[]
|
|
418
|
+
): { valid: boolean; missing: string[]; suggestions: Map<string, string[]> } {
|
|
419
|
+
const used = extractComponents(tree);
|
|
420
|
+
const available = new Set(availableComponents.map((c) => c.toLowerCase()));
|
|
421
|
+
const availableOriginal = new Map(
|
|
422
|
+
availableComponents.map((c) => [c.toLowerCase(), c])
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
const missing: string[] = [];
|
|
426
|
+
const suggestions = new Map<string, string[]>();
|
|
427
|
+
|
|
428
|
+
for (const component of used) {
|
|
429
|
+
const lower = component.toLowerCase();
|
|
430
|
+
if (!available.has(lower)) {
|
|
431
|
+
missing.push(component);
|
|
432
|
+
|
|
433
|
+
// Find similar components for suggestions
|
|
434
|
+
const similar = availableComponents.filter((c) => {
|
|
435
|
+
const clower = c.toLowerCase();
|
|
436
|
+
// Check for partial matches
|
|
437
|
+
return (
|
|
438
|
+
clower.includes(lower) ||
|
|
439
|
+
lower.includes(clower) ||
|
|
440
|
+
levenshteinDistance(lower, clower) <= 2
|
|
441
|
+
);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
if (similar.length > 0) {
|
|
445
|
+
suggestions.set(component, similar.slice(0, 3));
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
valid: missing.length === 0,
|
|
452
|
+
missing,
|
|
453
|
+
suggestions,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Simple Levenshtein distance for fuzzy matching
|
|
459
|
+
*/
|
|
460
|
+
function levenshteinDistance(a: string, b: string): number {
|
|
461
|
+
const matrix: number[][] = [];
|
|
462
|
+
|
|
463
|
+
for (let i = 0; i <= b.length; i++) {
|
|
464
|
+
matrix[i] = [i];
|
|
465
|
+
}
|
|
466
|
+
for (let j = 0; j <= a.length; j++) {
|
|
467
|
+
matrix[0][j] = j;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
for (let i = 1; i <= b.length; i++) {
|
|
471
|
+
for (let j = 1; j <= a.length; j++) {
|
|
472
|
+
if (b.charAt(i - 1) === a.charAt(j - 1)) {
|
|
473
|
+
matrix[i][j] = matrix[i - 1][j - 1];
|
|
474
|
+
} else {
|
|
475
|
+
matrix[i][j] = Math.min(
|
|
476
|
+
matrix[i - 1][j - 1] + 1,
|
|
477
|
+
matrix[i][j - 1] + 1,
|
|
478
|
+
matrix[i - 1][j] + 1
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return matrix[b.length][a.length];
|
|
485
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preview Frame Entry Point
|
|
3
|
+
*
|
|
4
|
+
* This is the entry point for the isolated iframe that renders component previews.
|
|
5
|
+
* It runs inside the iframe and receives render requests from the parent window
|
|
6
|
+
* via postMessage.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createRoot } from 'react-dom/client';
|
|
10
|
+
import { PreviewFrameHost } from './components/PreviewFrameHost.js';
|
|
11
|
+
|
|
12
|
+
// Mount the preview host
|
|
13
|
+
const rootElement = document.getElementById('preview-root');
|
|
14
|
+
|
|
15
|
+
if (rootElement) {
|
|
16
|
+
const root = createRoot(rootElement);
|
|
17
|
+
root.render(<PreviewFrameHost />);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// HMR support
|
|
21
|
+
// @ts-expect-error Vite HMR types
|
|
22
|
+
if (import.meta.hot) {
|
|
23
|
+
// @ts-expect-error Vite HMR types
|
|
24
|
+
import.meta.hot.accept();
|
|
25
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Fragment Preview</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
|
+
<link
|
|
10
|
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
|
11
|
+
rel="stylesheet"
|
|
12
|
+
/>
|
|
13
|
+
<style>
|
|
14
|
+
/* Reset and base styles for isolated preview */
|
|
15
|
+
*, *::before, *::after {
|
|
16
|
+
box-sizing: border-box;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
html, body {
|
|
20
|
+
margin: 0;
|
|
21
|
+
padding: 0;
|
|
22
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
23
|
+
-webkit-font-smoothing: antialiased;
|
|
24
|
+
-moz-osx-font-smoothing: grayscale;
|
|
25
|
+
width: 100%;
|
|
26
|
+
height: 100%;
|
|
27
|
+
overflow: auto;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
body {
|
|
31
|
+
background: transparent;
|
|
32
|
+
min-height: 100%;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#preview-root {
|
|
36
|
+
width: 100%;
|
|
37
|
+
min-height: 100%;
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
justify-content: center;
|
|
41
|
+
padding: 24px;
|
|
42
|
+
box-sizing: border-box;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* Hide scrollbars but allow scrolling */
|
|
46
|
+
html::-webkit-scrollbar {
|
|
47
|
+
width: 0;
|
|
48
|
+
height: 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
html {
|
|
52
|
+
scrollbar-width: none;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* Error display styles */
|
|
56
|
+
.preview-error {
|
|
57
|
+
padding: 16px;
|
|
58
|
+
background: #fef2f2;
|
|
59
|
+
border: 1px solid #fecaca;
|
|
60
|
+
border-radius: 8px;
|
|
61
|
+
color: #dc2626;
|
|
62
|
+
font-size: 14px;
|
|
63
|
+
max-width: 400px;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.preview-error pre {
|
|
67
|
+
margin: 8px 0 0;
|
|
68
|
+
padding: 8px;
|
|
69
|
+
background: #fff;
|
|
70
|
+
border-radius: 4px;
|
|
71
|
+
overflow-x: auto;
|
|
72
|
+
font-size: 12px;
|
|
73
|
+
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* Loading indicator */
|
|
77
|
+
.preview-loading {
|
|
78
|
+
display: flex;
|
|
79
|
+
align-items: center;
|
|
80
|
+
justify-content: center;
|
|
81
|
+
gap: 8px;
|
|
82
|
+
color: #6b7280;
|
|
83
|
+
font-size: 14px;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.preview-loading .spinner {
|
|
87
|
+
width: 20px;
|
|
88
|
+
height: 20px;
|
|
89
|
+
border: 2px solid #e5e7eb;
|
|
90
|
+
border-top-color: #3b82f6;
|
|
91
|
+
border-radius: 50%;
|
|
92
|
+
animation: spin 0.8s linear infinite;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@keyframes spin {
|
|
96
|
+
to { transform: rotate(360deg); }
|
|
97
|
+
}
|
|
98
|
+
</style>
|
|
99
|
+
</head>
|
|
100
|
+
<body>
|
|
101
|
+
<div id="preview-root">
|
|
102
|
+
<div class="preview-loading">
|
|
103
|
+
<div class="spinner"></div>
|
|
104
|
+
<span>Loading preview...</span>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
<script type="module" src="/src/preview-frame-entry.tsx"></script>
|
|
108
|
+
</body>
|
|
109
|
+
</html>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Segments Render</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
|
+
<link
|
|
10
|
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
|
11
|
+
rel="stylesheet"
|
|
12
|
+
/>
|
|
13
|
+
<style>
|
|
14
|
+
/* Reset and base styles for isolated rendering */
|
|
15
|
+
*, *::before, *::after {
|
|
16
|
+
box-sizing: border-box;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
html, body {
|
|
20
|
+
margin: 0;
|
|
21
|
+
padding: 0;
|
|
22
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
23
|
+
-webkit-font-smoothing: antialiased;
|
|
24
|
+
-moz-osx-font-smoothing: grayscale;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
body {
|
|
28
|
+
background: #ffffff;
|
|
29
|
+
min-height: 100vh;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#render-root {
|
|
33
|
+
padding: 16px;
|
|
34
|
+
display: inline-block;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* Signal that rendering is complete */
|
|
38
|
+
#render-root.ready {
|
|
39
|
+
/* Used by Playwright to know when to capture */
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* Error display */
|
|
43
|
+
.render-error {
|
|
44
|
+
padding: 16px;
|
|
45
|
+
background: #fef2f2;
|
|
46
|
+
border: 1px solid #fecaca;
|
|
47
|
+
border-radius: 8px;
|
|
48
|
+
color: #dc2626;
|
|
49
|
+
font-size: 14px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.render-error pre {
|
|
53
|
+
margin: 8px 0 0;
|
|
54
|
+
padding: 8px;
|
|
55
|
+
background: #fff;
|
|
56
|
+
border-radius: 4px;
|
|
57
|
+
overflow-x: auto;
|
|
58
|
+
font-size: 12px;
|
|
59
|
+
font-family: 'JetBrains Mono', monospace;
|
|
60
|
+
}
|
|
61
|
+
</style>
|
|
62
|
+
<!-- PROJECT_STYLES_PLACEHOLDER -->
|
|
63
|
+
</head>
|
|
64
|
+
<body>
|
|
65
|
+
<div id="render-root"></div>
|
|
66
|
+
<!-- RENDER_SCRIPT_PLACEHOLDER -->
|
|
67
|
+
</body>
|
|
68
|
+
</html>
|