@fragments-sdk/cli 0.5.2 → 0.7.0
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/dist/bin.js +996 -79
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-ICAIQ57V.js → chunk-6JBGU74P.js} +5 -3
- package/dist/chunk-6JBGU74P.js.map +1 -0
- package/dist/chunk-7OPWMLOE.js +1625 -0
- package/dist/chunk-7OPWMLOE.js.map +1 -0
- package/dist/{chunk-2H2JAA3U.js → chunk-CVXKXVOY.js} +3 -3
- package/dist/{chunk-2H2JAA3U.js.map → chunk-CVXKXVOY.js.map} +1 -1
- package/dist/{chunk-IOJE35DZ.js → chunk-NWQ4CJOQ.js} +3 -3
- package/dist/{chunk-2DJH4F4P.js → chunk-RVRTRESS.js} +3 -3
- package/dist/{chunk-V7YLRR4C.js → chunk-TJ34N7C7.js} +41 -4
- package/dist/{chunk-V7YLRR4C.js.map → chunk-TJ34N7C7.js.map} +1 -1
- package/dist/{chunk-XNWDI6UT.js → chunk-XHUDJNN3.js} +5 -5
- package/dist/{core-DKHB7FYV.js → core-W2HYIQW6.js} +4 -4
- package/dist/{generate-KL24VZVD.js → generate-LMTISDIJ.js} +5 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +15 -7
- package/dist/index.js.map +1 -1
- package/dist/{init-NION5S3M.js → init-7CHRKQ7P.js} +5 -5
- package/dist/mcp-bin.js +8 -220
- package/dist/mcp-bin.js.map +1 -1
- package/dist/scan-WY23TJCP.js +12 -0
- package/dist/{service-RWUMZ3EW.js → service-T2L7VLTE.js} +5 -5
- package/dist/static-viewer-GBR7YNF3.js +12 -0
- package/dist/{test-ECPEXFDN.js → test-OJRXNDO2.js} +4 -4
- package/dist/{tokens-ITADYVPF.js → tokens-3BWDESVM.js} +6 -6
- package/dist/viewer-SUFOISZM.js +1822 -0
- package/dist/viewer-SUFOISZM.js.map +1 -0
- package/package.json +6 -5
- package/src/bin.ts +31 -0
- package/src/build.ts +147 -13
- package/src/cli-commands.ts +18 -0
- package/src/commands/__tests__/a11y-scoring.test.ts +278 -0
- package/src/commands/a11y-report.ts +625 -0
- package/src/commands/a11y.ts +168 -14
- package/src/commands/build.ts +16 -0
- package/src/commands/graph.ts +274 -0
- package/src/core/auto-props.ts +464 -0
- package/src/core/composition.ts +64 -1
- package/src/core/graph-extractor.test.ts +542 -0
- package/src/core/graph-extractor.ts +601 -0
- package/src/core/importAnalyzer.ts +5 -0
- package/src/core/schema.ts +2 -0
- package/src/core/types.ts +3 -1
- package/src/index.ts +4 -0
- package/src/mcp/server.ts +13 -220
- package/src/theme/__tests__/component-contrast.test.ts +338 -0
- package/src/theme/__tests__/contrast-validation.test.ts +326 -0
- package/src/theme/contrast.test.ts +331 -0
- package/src/theme/contrast.ts +246 -0
- package/src/theme/generator.ts +213 -1
- package/src/theme/index.ts +16 -0
- package/src/theme/types.ts +51 -0
- package/src/viewer/__tests__/a11y-fixes.test.ts +358 -0
- package/src/viewer/__tests__/viewer-integration.test.ts +2 -7
- package/src/viewer/components/AccessibilityPanel.tsx +493 -433
- package/src/viewer/components/ActionCapture.tsx +1 -1
- package/src/viewer/components/ActionsPanel.tsx +142 -183
- package/src/viewer/components/App.tsx +276 -183
- package/src/viewer/components/BottomPanel.tsx +40 -80
- package/src/viewer/components/CodePanel.tsx +9 -87
- package/src/viewer/components/CommandPalette.tsx +117 -74
- package/src/viewer/components/ComponentGraph.tsx +143 -126
- package/src/viewer/components/ComponentHeader.tsx +46 -43
- package/src/viewer/components/ContractPanel.tsx +124 -117
- package/src/viewer/components/ErrorBoundary.tsx +47 -35
- package/src/viewer/components/FigmaEmbed.tsx +18 -13
- package/src/viewer/components/FragmentEditor.tsx +126 -63
- package/src/viewer/components/HealthDashboard.tsx +146 -171
- package/src/viewer/components/HmrStatusIndicator.tsx +31 -41
- package/src/viewer/components/Icons.tsx +151 -98
- package/src/viewer/components/InteractionsPanel.tsx +317 -264
- package/src/viewer/components/IsolatedPreviewFrame.tsx +52 -27
- package/src/viewer/components/IsolatedRender.tsx +12 -6
- package/src/viewer/components/KeyboardShortcutsHelp.tsx +34 -70
- package/src/viewer/components/LandingPage.tsx +285 -305
- package/src/viewer/components/Layout.tsx +12 -10
- package/src/viewer/components/LeftSidebar.tsx +103 -155
- package/src/viewer/components/MultiViewportPreview.tsx +254 -63
- package/src/viewer/components/PreviewArea.tsx +113 -44
- package/src/viewer/components/PreviewFrameHost.tsx +36 -6
- package/src/viewer/components/PreviewPane.tsx +2 -3
- package/src/viewer/components/PreviewToolbar.tsx +109 -105
- package/src/viewer/components/PropsEditor.tsx +154 -74
- package/src/viewer/components/PropsTable.tsx +95 -82
- package/src/viewer/components/RelationsSection.tsx +71 -40
- package/src/viewer/components/ResizablePanel.tsx +158 -55
- package/src/viewer/components/RightSidebar.tsx +46 -56
- package/src/viewer/components/ScreenshotButton.tsx +12 -12
- package/src/viewer/components/SkeletonLoader.tsx +99 -83
- package/src/viewer/components/StoryRenderer.tsx +4 -11
- package/src/viewer/components/Toast.tsx +3 -67
- package/src/viewer/components/TokenStylePanel.tsx +136 -118
- package/src/viewer/components/UsageSection.tsx +26 -26
- package/src/viewer/components/VariantMatrix.tsx +140 -47
- package/src/viewer/components/VariantTabs.tsx +24 -68
- package/src/viewer/components/ViewportSelector.tsx +121 -114
- package/src/viewer/constants/ui.ts +23 -22
- package/src/viewer/entry.tsx +8 -3
- package/src/viewer/index.ts +3 -6
- package/src/viewer/preview-frame.html +43 -18
- package/src/viewer/server.ts +7 -16
- package/src/viewer/styles/globals.css +46 -85
- package/src/viewer/utils/a11y-fixes.ts +53 -30
- package/dist/chunk-ICAIQ57V.js.map +0 -1
- package/dist/chunk-U4GQ2JTD.js +0 -832
- package/dist/chunk-U4GQ2JTD.js.map +0 -1
- package/dist/scan-ESEXV7LF.js +0 -12
- package/dist/static-viewer-O37MJ5B6.js +0 -12
- package/dist/viewer-YDGFDTK5.js +0 -11104
- package/dist/viewer-YDGFDTK5.js.map +0 -1
- package/src/viewer/postcss.config.js +0 -6
- package/src/viewer/tailwind.config.js +0 -37
- /package/dist/{chunk-IOJE35DZ.js.map → chunk-NWQ4CJOQ.js.map} +0 -0
- /package/dist/{chunk-2DJH4F4P.js.map → chunk-RVRTRESS.js.map} +0 -0
- /package/dist/{chunk-XNWDI6UT.js.map → chunk-XHUDJNN3.js.map} +0 -0
- /package/dist/{core-DKHB7FYV.js.map → core-W2HYIQW6.js.map} +0 -0
- /package/dist/{generate-KL24VZVD.js.map → generate-LMTISDIJ.js.map} +0 -0
- /package/dist/{init-NION5S3M.js.map → init-7CHRKQ7P.js.map} +0 -0
- /package/dist/{scan-ESEXV7LF.js.map → scan-WY23TJCP.js.map} +0 -0
- /package/dist/{service-RWUMZ3EW.js.map → service-T2L7VLTE.js.map} +0 -0
- /package/dist/{static-viewer-O37MJ5B6.js.map → static-viewer-GBR7YNF3.js.map} +0 -0
- /package/dist/{test-ECPEXFDN.js.map → test-OJRXNDO2.js.map} +0 -0
- /package/dist/{tokens-ITADYVPF.js.map → tokens-3BWDESVM.js.map} +0 -0
|
@@ -0,0 +1,1822 @@
|
|
|
1
|
+
import { createRequire as __banner_createRequire } from 'module'; const require = __banner_createRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
discoverInstalledFragments,
|
|
4
|
+
discoverSegmentFiles,
|
|
5
|
+
findPreviewConfigPath,
|
|
6
|
+
findStorybookDir,
|
|
7
|
+
generatePreviewModule,
|
|
8
|
+
loadConfig
|
|
9
|
+
} from "./chunk-CVXKXVOY.js";
|
|
10
|
+
import {
|
|
11
|
+
generateContext
|
|
12
|
+
} from "./chunk-TJ34N7C7.js";
|
|
13
|
+
import {
|
|
14
|
+
BRAND
|
|
15
|
+
} from "./chunk-6JBGU74P.js";
|
|
16
|
+
|
|
17
|
+
// src/viewer/server.ts
|
|
18
|
+
import {
|
|
19
|
+
createServer,
|
|
20
|
+
mergeConfig,
|
|
21
|
+
loadConfigFromFile
|
|
22
|
+
} from "vite";
|
|
23
|
+
import react from "@vitejs/plugin-react";
|
|
24
|
+
import { resolve as resolve2, dirname as dirname2, join } from "path";
|
|
25
|
+
import { existsSync, realpathSync } from "fs";
|
|
26
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
27
|
+
|
|
28
|
+
// src/viewer/vite-plugin.ts
|
|
29
|
+
import { resolve, dirname } from "path";
|
|
30
|
+
import { fileURLToPath } from "url";
|
|
31
|
+
import { readFile } from "fs/promises";
|
|
32
|
+
import { transform } from "esbuild";
|
|
33
|
+
import svgr from "vite-plugin-svgr";
|
|
34
|
+
|
|
35
|
+
// src/viewer/render-utils.ts
|
|
36
|
+
function serializeValue(value) {
|
|
37
|
+
if (value === null) return "null";
|
|
38
|
+
if (value === void 0) return "undefined";
|
|
39
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
40
|
+
if (typeof value === "number") return String(value);
|
|
41
|
+
if (typeof value === "boolean") return String(value);
|
|
42
|
+
if (Array.isArray(value)) {
|
|
43
|
+
return `[${value.map(serializeValue).join(", ")}]`;
|
|
44
|
+
}
|
|
45
|
+
if (typeof value === "object") {
|
|
46
|
+
const entries = Object.entries(value).map(([k, v]) => `${JSON.stringify(k)}: ${serializeValue(v)}`).join(", ");
|
|
47
|
+
return `{${entries}}`;
|
|
48
|
+
}
|
|
49
|
+
return "undefined";
|
|
50
|
+
}
|
|
51
|
+
function serializePropsToJsx(props) {
|
|
52
|
+
return Object.entries(props).filter(([_, v]) => v !== void 0).map(([key, value]) => {
|
|
53
|
+
if (typeof value === "string") {
|
|
54
|
+
return `${key}=${JSON.stringify(value)}`;
|
|
55
|
+
}
|
|
56
|
+
return `${key}={${serializeValue(value)}}`;
|
|
57
|
+
}).join(" ");
|
|
58
|
+
}
|
|
59
|
+
function findSegmentByName(componentName, segments) {
|
|
60
|
+
const match = segments.find(
|
|
61
|
+
(s) => s.segment.meta.name.toLowerCase() === componentName.toLowerCase()
|
|
62
|
+
);
|
|
63
|
+
if (!match) return null;
|
|
64
|
+
return {
|
|
65
|
+
name: match.segment.meta.name,
|
|
66
|
+
path: match.path
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function getAvailableComponents(segments) {
|
|
70
|
+
return segments.map((s) => s.segment.meta.name).sort();
|
|
71
|
+
}
|
|
72
|
+
function generateRenderScript(segmentPath, componentName, props = {}) {
|
|
73
|
+
const propsJsx = serializePropsToJsx(props);
|
|
74
|
+
const propsString = propsJsx ? ` ${propsJsx}` : "";
|
|
75
|
+
const hasChildren = "children" in props && props.children !== void 0;
|
|
76
|
+
const childrenContent = hasChildren ? String(props.children) : "";
|
|
77
|
+
const propsWithoutChildren = { ...props };
|
|
78
|
+
delete propsWithoutChildren.children;
|
|
79
|
+
const propsJsxNoChildren = serializePropsToJsx(propsWithoutChildren);
|
|
80
|
+
const propsStringNoChildren = propsJsxNoChildren ? ` ${propsJsxNoChildren}` : "";
|
|
81
|
+
return `
|
|
82
|
+
import React from "react";
|
|
83
|
+
import { createRoot } from "react-dom/client";
|
|
84
|
+
|
|
85
|
+
// Import the segment to get the component
|
|
86
|
+
async function render() {
|
|
87
|
+
const root = document.getElementById("render-root");
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
// Dynamic import of the segment file
|
|
91
|
+
const segmentModule = await import("${segmentPath}");
|
|
92
|
+
const segment = segmentModule.default;
|
|
93
|
+
|
|
94
|
+
if (!segment || !segment.component) {
|
|
95
|
+
throw new Error("Segment does not export a component");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const Component = segment.component;
|
|
99
|
+
|
|
100
|
+
// Create React root and render
|
|
101
|
+
const reactRoot = createRoot(root);
|
|
102
|
+
${hasChildren ? `reactRoot.render(React.createElement(Component, ${JSON.stringify(propsWithoutChildren)}, ${JSON.stringify(childrenContent)}));` : `reactRoot.render(React.createElement(Component, ${JSON.stringify(props)}));`}
|
|
103
|
+
|
|
104
|
+
// Signal that rendering is complete
|
|
105
|
+
// Wait a frame for React to flush
|
|
106
|
+
requestAnimationFrame(() => {
|
|
107
|
+
requestAnimationFrame(() => {
|
|
108
|
+
root.classList.add("ready");
|
|
109
|
+
window.__RENDER_READY__ = true;
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error("Render error:", error);
|
|
114
|
+
root.innerHTML = \`
|
|
115
|
+
<div class="render-error">
|
|
116
|
+
<strong>Render Error</strong>
|
|
117
|
+
<pre>\${error.message}</pre>
|
|
118
|
+
</div>
|
|
119
|
+
\`;
|
|
120
|
+
root.classList.add("ready");
|
|
121
|
+
window.__RENDER_READY__ = true;
|
|
122
|
+
window.__RENDER_ERROR__ = error.message;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
render();
|
|
127
|
+
`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/viewer/style-utils.ts
|
|
131
|
+
function compareStyles(figmaStyles, renderedStyles) {
|
|
132
|
+
const properties = [];
|
|
133
|
+
const cleanFigmaStyles = {};
|
|
134
|
+
const propsToCompare = [
|
|
135
|
+
"backgroundColor",
|
|
136
|
+
"borderColor",
|
|
137
|
+
"borderWidth",
|
|
138
|
+
"borderRadius",
|
|
139
|
+
"fontFamily",
|
|
140
|
+
"fontSize",
|
|
141
|
+
"fontWeight",
|
|
142
|
+
"lineHeight",
|
|
143
|
+
"letterSpacing",
|
|
144
|
+
"textAlign",
|
|
145
|
+
"boxShadow",
|
|
146
|
+
"padding",
|
|
147
|
+
"gap",
|
|
148
|
+
"opacity"
|
|
149
|
+
];
|
|
150
|
+
for (const prop of propsToCompare) {
|
|
151
|
+
const figmaValue = figmaStyles[prop];
|
|
152
|
+
const renderedValue = renderedStyles[prop];
|
|
153
|
+
if (figmaValue !== void 0) {
|
|
154
|
+
cleanFigmaStyles[prop] = figmaValue;
|
|
155
|
+
const match = compareStyleValue(prop, figmaValue, renderedValue || "");
|
|
156
|
+
properties.push({
|
|
157
|
+
property: prop,
|
|
158
|
+
figma: figmaValue,
|
|
159
|
+
rendered: renderedValue || "(not set)",
|
|
160
|
+
match
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const allMatch = properties.every((p) => p.match);
|
|
165
|
+
return {
|
|
166
|
+
match: allMatch,
|
|
167
|
+
properties,
|
|
168
|
+
figmaStyles: cleanFigmaStyles,
|
|
169
|
+
renderedStyles
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function compareStyleValue(prop, figma, rendered) {
|
|
173
|
+
const normalizedFigma = normalizeStyleValue(prop, figma);
|
|
174
|
+
const normalizedRendered = normalizeStyleValue(prop, rendered);
|
|
175
|
+
if (normalizedFigma === normalizedRendered) {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
if (prop === "backgroundColor" || prop === "borderColor") {
|
|
179
|
+
return compareColors(normalizedFigma, normalizedRendered, 5);
|
|
180
|
+
}
|
|
181
|
+
if (["borderWidth", "borderRadius", "fontSize", "padding", "gap"].includes(prop)) {
|
|
182
|
+
return compareNumericValues(normalizedFigma, normalizedRendered, 1);
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
function normalizeStyleValue(prop, value) {
|
|
187
|
+
let normalized = value.trim().replace(/\s+/g, " ");
|
|
188
|
+
if (prop === "boxShadow" && normalized === "none") {
|
|
189
|
+
normalized = "";
|
|
190
|
+
}
|
|
191
|
+
if (normalized.match(/rgba\(\s*0\s*,\s*0\s*,\s*0\s*,\s*0\s*\)/)) {
|
|
192
|
+
normalized = "transparent";
|
|
193
|
+
}
|
|
194
|
+
return normalized;
|
|
195
|
+
}
|
|
196
|
+
function compareColors(color1, color2, tolerance) {
|
|
197
|
+
const rgb1 = parseColor(color1);
|
|
198
|
+
const rgb2 = parseColor(color2);
|
|
199
|
+
if (!rgb1 || !rgb2) {
|
|
200
|
+
return color1 === color2;
|
|
201
|
+
}
|
|
202
|
+
return Math.abs(rgb1.r - rgb2.r) <= tolerance && Math.abs(rgb1.g - rgb2.g) <= tolerance && Math.abs(rgb1.b - rgb2.b) <= tolerance && Math.abs((rgb1.a ?? 1) - (rgb2.a ?? 1)) <= 0.05;
|
|
203
|
+
}
|
|
204
|
+
function parseColor(color) {
|
|
205
|
+
const hexMatch = color.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
|
|
206
|
+
if (hexMatch) {
|
|
207
|
+
return {
|
|
208
|
+
r: parseInt(hexMatch[1], 16),
|
|
209
|
+
g: parseInt(hexMatch[2], 16),
|
|
210
|
+
b: parseInt(hexMatch[3], 16)
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
const rgbaMatch = color.match(
|
|
214
|
+
/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)/
|
|
215
|
+
);
|
|
216
|
+
if (rgbaMatch) {
|
|
217
|
+
return {
|
|
218
|
+
r: parseInt(rgbaMatch[1], 10),
|
|
219
|
+
g: parseInt(rgbaMatch[2], 10),
|
|
220
|
+
b: parseInt(rgbaMatch[3], 10),
|
|
221
|
+
a: rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
function compareNumericValues(value1, value2, tolerance) {
|
|
227
|
+
const num1 = parseFloat(value1);
|
|
228
|
+
const num2 = parseFloat(value2);
|
|
229
|
+
if (isNaN(num1) || isNaN(num2)) {
|
|
230
|
+
return value1 === value2;
|
|
231
|
+
}
|
|
232
|
+
return Math.abs(num1 - num2) <= tolerance;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// src/viewer/vite-plugin.ts
|
|
236
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
237
|
+
var viewerAssetsRoot = resolve(__dirname, "..", "src/viewer");
|
|
238
|
+
var pendingRenders = /* @__PURE__ */ new Map();
|
|
239
|
+
var sharedRenderPool = null;
|
|
240
|
+
var browserPoolModule = null;
|
|
241
|
+
async function getSharedRenderPool() {
|
|
242
|
+
if (!browserPoolModule) {
|
|
243
|
+
browserPoolModule = await import("./service-T2L7VLTE.js");
|
|
244
|
+
}
|
|
245
|
+
if (!sharedRenderPool) {
|
|
246
|
+
sharedRenderPool = new browserPoolModule.BrowserPool({
|
|
247
|
+
viewport: { width: 800, height: 600 },
|
|
248
|
+
// Default viewport, will be overridden per page
|
|
249
|
+
poolSize: 2,
|
|
250
|
+
// Keep 2 contexts warm for parallel requests
|
|
251
|
+
idleTimeoutMs: 6e4
|
|
252
|
+
// Keep warm for 60 seconds
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
return { pool: sharedRenderPool, bufferToBase64Url: browserPoolModule.bufferToBase64Url };
|
|
256
|
+
}
|
|
257
|
+
function segmentsPlugin(options) {
|
|
258
|
+
const { segmentFiles, config, projectRoot } = options;
|
|
259
|
+
const VIRTUAL_SEGMENTS = `virtual:${BRAND.nameLower}`;
|
|
260
|
+
const VIRTUAL_SEGMENTS_RESOLVED = `\0virtual:${BRAND.nameLower}`;
|
|
261
|
+
const VIRTUAL_VIEWER_ENTRY = `virtual:${BRAND.nameLower}-viewer-entry`;
|
|
262
|
+
const VIRTUAL_VIEWER_ENTRY_RESOLVED = `\0virtual:${BRAND.nameLower}-viewer-entry`;
|
|
263
|
+
const VIRTUAL_PREVIEW = `virtual:${BRAND.nameLower}-preview`;
|
|
264
|
+
const VIRTUAL_PREVIEW_RESOLVED = `\0virtual:${BRAND.nameLower}-preview`;
|
|
265
|
+
let server = null;
|
|
266
|
+
let resolvedConfig = null;
|
|
267
|
+
const storybookDir = findStorybookDir(projectRoot);
|
|
268
|
+
const previewConfigPath = storybookDir ? findPreviewConfigPath(storybookDir) : null;
|
|
269
|
+
const segmentFileSet = new Set(segmentFiles.map((f) => f.absolutePath));
|
|
270
|
+
const mainPlugin = {
|
|
271
|
+
name: "segments",
|
|
272
|
+
// Add process.env shim and esbuild config for Storybook compatibility
|
|
273
|
+
config() {
|
|
274
|
+
return {
|
|
275
|
+
define: {
|
|
276
|
+
// Shim process.env for story files that use it (e.g., process.env.STORYBOOK_*)
|
|
277
|
+
"process.env": "{}"
|
|
278
|
+
},
|
|
279
|
+
esbuild: {
|
|
280
|
+
// Handle JSX in .js files (common in Storybook preview.js files)
|
|
281
|
+
loader: "tsx",
|
|
282
|
+
include: /\.(tsx?|jsx?)$/
|
|
283
|
+
},
|
|
284
|
+
optimizeDeps: {
|
|
285
|
+
// Force esbuild to handle .js files with JSX
|
|
286
|
+
esbuildOptions: {
|
|
287
|
+
loader: {
|
|
288
|
+
".js": "jsx"
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
},
|
|
294
|
+
// Store resolved config
|
|
295
|
+
configResolved(config2) {
|
|
296
|
+
resolvedConfig = config2;
|
|
297
|
+
},
|
|
298
|
+
// Store server reference for HMR
|
|
299
|
+
configureServer(_server) {
|
|
300
|
+
server = _server;
|
|
301
|
+
_server.middlewares.use(async (req, res, next) => {
|
|
302
|
+
if (req.url === "/fragments/render" && req.method === "POST") {
|
|
303
|
+
try {
|
|
304
|
+
const body = await parseJsonBody(req);
|
|
305
|
+
const { component, props = {}, viewport } = body;
|
|
306
|
+
if (!component) {
|
|
307
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
308
|
+
res.end(
|
|
309
|
+
JSON.stringify({ error: "Missing required field: component" })
|
|
310
|
+
);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const loadedSegments = await loadSegmentsForRender(
|
|
314
|
+
segmentFiles,
|
|
315
|
+
projectRoot
|
|
316
|
+
);
|
|
317
|
+
const segmentInfo = findSegmentByName(component, loadedSegments);
|
|
318
|
+
if (!segmentInfo) {
|
|
319
|
+
const available = getAvailableComponents(loadedSegments);
|
|
320
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
321
|
+
res.end(
|
|
322
|
+
JSON.stringify({
|
|
323
|
+
error: `Component '${component}' not found. Available: ${available.join(
|
|
324
|
+
", "
|
|
325
|
+
)}`
|
|
326
|
+
})
|
|
327
|
+
);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const segmentFile = segmentFiles.find(
|
|
331
|
+
(f) => f.relativePath === segmentInfo.path
|
|
332
|
+
);
|
|
333
|
+
if (!segmentFile) {
|
|
334
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
335
|
+
res.end(
|
|
336
|
+
JSON.stringify({ error: "Could not resolve segment file path" })
|
|
337
|
+
);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const renderScript = generateRenderScript(
|
|
341
|
+
segmentFile.absolutePath,
|
|
342
|
+
segmentInfo.name,
|
|
343
|
+
props
|
|
344
|
+
);
|
|
345
|
+
const requestId = Date.now().toString(36) + Math.random().toString(36).slice(2);
|
|
346
|
+
pendingRenders.set(requestId, { script: renderScript, viewport });
|
|
347
|
+
const address = _server.httpServer?.address();
|
|
348
|
+
const port = typeof address === "object" && address ? address.port : 6006;
|
|
349
|
+
const screenshot = await captureRender(
|
|
350
|
+
`http://localhost:${port}/fragments/__render__/${requestId}`,
|
|
351
|
+
viewport || { width: 800, height: 600 }
|
|
352
|
+
);
|
|
353
|
+
pendingRenders.delete(requestId);
|
|
354
|
+
res.setHeader("Content-Type", "application/json");
|
|
355
|
+
res.end(JSON.stringify({ screenshot }));
|
|
356
|
+
} catch (error) {
|
|
357
|
+
console.error("[Fragments] Error rendering:", error);
|
|
358
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
359
|
+
res.end(
|
|
360
|
+
JSON.stringify({
|
|
361
|
+
error: error instanceof Error ? error.message : "Render failed"
|
|
362
|
+
})
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (req.url?.startsWith("/fragments/__render__/")) {
|
|
368
|
+
const requestId = req.url.split("/fragments/__render__/")[1]?.split("?")[0];
|
|
369
|
+
const renderData = pendingRenders.get(requestId || "");
|
|
370
|
+
if (!renderData) {
|
|
371
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
372
|
+
res.end("Render request not found or expired");
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
await serveRenderHTML(res, _server, renderData.script);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (req.url === "/fragments/compare" && req.method === "POST") {
|
|
379
|
+
try {
|
|
380
|
+
const body = await parseJsonBody(req);
|
|
381
|
+
const {
|
|
382
|
+
component,
|
|
383
|
+
variant,
|
|
384
|
+
props = {},
|
|
385
|
+
figmaUrl,
|
|
386
|
+
viewport,
|
|
387
|
+
threshold = 1,
|
|
388
|
+
includeStyleDiff = false
|
|
389
|
+
} = body;
|
|
390
|
+
if (!component) {
|
|
391
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
392
|
+
res.end(
|
|
393
|
+
JSON.stringify({ error: "Missing required field: component" })
|
|
394
|
+
);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const figmaToken = body.figmaToken || process.env.FIGMA_ACCESS_TOKEN || config.figmaToken;
|
|
398
|
+
if (!figmaToken && !figmaUrl) {
|
|
399
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
400
|
+
res.end(
|
|
401
|
+
JSON.stringify({
|
|
402
|
+
error: `No Figma access token configured. Figma token: ${figmaToken}`,
|
|
403
|
+
suggestion: "Set FIGMA_ACCESS_TOKEN env var, add figmaToken to fragments.config.ts, or provide in request"
|
|
404
|
+
})
|
|
405
|
+
);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
console.log("[Fragments] Compare request for:", component);
|
|
409
|
+
console.log("[Fragments] segmentFiles count:", segmentFiles.length);
|
|
410
|
+
console.log("[Fragments] First 3 segment files:", segmentFiles.slice(0, 3).map((f) => f.relativePath));
|
|
411
|
+
console.log("[Fragments] projectRoot:", projectRoot);
|
|
412
|
+
const loadedSegments = await loadSegmentsForRender(
|
|
413
|
+
segmentFiles,
|
|
414
|
+
projectRoot
|
|
415
|
+
);
|
|
416
|
+
console.log("[Fragments] loadedSegments count:", loadedSegments.length);
|
|
417
|
+
console.log("[Fragments] First 3 loaded:", loadedSegments.slice(0, 3).map((s) => s.segment.meta.name));
|
|
418
|
+
const segmentInfo = findSegmentByName(component, loadedSegments);
|
|
419
|
+
if (!segmentInfo) {
|
|
420
|
+
const available = getAvailableComponents(loadedSegments);
|
|
421
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
422
|
+
res.end(
|
|
423
|
+
JSON.stringify({
|
|
424
|
+
error: `Component '${component}' not found. Available: ${available.join(
|
|
425
|
+
", "
|
|
426
|
+
)}`
|
|
427
|
+
})
|
|
428
|
+
);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const fullSegmentData = await loadFullSegmentForCompare(
|
|
432
|
+
_server,
|
|
433
|
+
segmentFiles,
|
|
434
|
+
component,
|
|
435
|
+
variant,
|
|
436
|
+
projectRoot
|
|
437
|
+
);
|
|
438
|
+
const effectiveFigmaUrl = figmaUrl || fullSegmentData?.figmaUrl;
|
|
439
|
+
if (!effectiveFigmaUrl) {
|
|
440
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
441
|
+
res.end(
|
|
442
|
+
JSON.stringify({
|
|
443
|
+
error: `No Figma URL for component '${component}'`,
|
|
444
|
+
suggestion: "Add 'figma' field to segment definition or provide figmaUrl in request"
|
|
445
|
+
})
|
|
446
|
+
);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (!figmaToken) {
|
|
450
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
451
|
+
res.end(
|
|
452
|
+
JSON.stringify({
|
|
453
|
+
error: "Figma access token required for comparison",
|
|
454
|
+
suggestion: "Set FIGMA_ACCESS_TOKEN env var or add figmaToken to fragments.config.ts"
|
|
455
|
+
})
|
|
456
|
+
);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const segmentFile = segmentFiles.find(
|
|
460
|
+
(f) => f.relativePath === segmentInfo.path
|
|
461
|
+
);
|
|
462
|
+
if (!segmentFile) {
|
|
463
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
464
|
+
res.end(
|
|
465
|
+
JSON.stringify({ error: "Could not resolve segment file path" })
|
|
466
|
+
);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const address = _server.httpServer?.address();
|
|
470
|
+
const port = typeof address === "object" && address ? address.port : 6006;
|
|
471
|
+
const renderViewport = viewport || { width: 800, height: 600 };
|
|
472
|
+
const { FigmaClient, bufferToBase64Url } = await import("./service-T2L7VLTE.js");
|
|
473
|
+
const figmaClient = new FigmaClient({
|
|
474
|
+
accessToken: figmaToken
|
|
475
|
+
});
|
|
476
|
+
const { fileKey, nodeId } = figmaClient.parseUrl(effectiveFigmaUrl);
|
|
477
|
+
const renderScript = generateRenderScript(
|
|
478
|
+
segmentFile.absolutePath,
|
|
479
|
+
segmentInfo.name,
|
|
480
|
+
props
|
|
481
|
+
);
|
|
482
|
+
const requestId = Date.now().toString(36) + Math.random().toString(36).slice(2);
|
|
483
|
+
pendingRenders.set(requestId, {
|
|
484
|
+
script: renderScript,
|
|
485
|
+
viewport: renderViewport
|
|
486
|
+
});
|
|
487
|
+
try {
|
|
488
|
+
const [captureResult, figmaImageResult, figmaDesignProps] = await Promise.all([
|
|
489
|
+
// Render and capture the component (with optional computed styles)
|
|
490
|
+
captureRenderWithStyles(
|
|
491
|
+
`http://localhost:${port}/fragments/__render__/${requestId}`,
|
|
492
|
+
renderViewport,
|
|
493
|
+
includeStyleDiff
|
|
494
|
+
),
|
|
495
|
+
// Fetch Figma image
|
|
496
|
+
figmaClient.getImageFromUrl(effectiveFigmaUrl),
|
|
497
|
+
// Fetch Figma design properties (only if includeStyleDiff is true)
|
|
498
|
+
includeStyleDiff ? figmaClient.getNodeProperties(fileKey, nodeId) : Promise.resolve(null)
|
|
499
|
+
]);
|
|
500
|
+
const renderedImage = captureResult.screenshot;
|
|
501
|
+
const renderedStyles = captureResult.computedStyles;
|
|
502
|
+
const figmaImage = bufferToBase64Url(figmaImageResult.data);
|
|
503
|
+
const compareResult = await compareImages(
|
|
504
|
+
renderedImage,
|
|
505
|
+
figmaImage,
|
|
506
|
+
threshold
|
|
507
|
+
);
|
|
508
|
+
const response = {
|
|
509
|
+
match: compareResult.matches,
|
|
510
|
+
diffPercentage: compareResult.diffPercentage,
|
|
511
|
+
threshold,
|
|
512
|
+
rendered: renderedImage,
|
|
513
|
+
figma: figmaImage,
|
|
514
|
+
diff: compareResult.diffImage || renderedImage,
|
|
515
|
+
figmaUrl: effectiveFigmaUrl,
|
|
516
|
+
changedRegions: compareResult.changedRegions
|
|
517
|
+
};
|
|
518
|
+
if (includeStyleDiff && figmaDesignProps && renderedStyles) {
|
|
519
|
+
const figmaStyles = figmaClient.convertToCSS(figmaDesignProps);
|
|
520
|
+
const figmaStylesRecord = { ...figmaStyles };
|
|
521
|
+
const styleDiffResult = compareStyles(figmaStylesRecord, renderedStyles);
|
|
522
|
+
response.styleDiff = styleDiffResult;
|
|
523
|
+
if (!styleDiffResult.match) {
|
|
524
|
+
response.match = false;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
res.setHeader("Content-Type", "application/json");
|
|
528
|
+
res.end(JSON.stringify(response));
|
|
529
|
+
} finally {
|
|
530
|
+
pendingRenders.delete(requestId);
|
|
531
|
+
}
|
|
532
|
+
} catch (error) {
|
|
533
|
+
console.error("[Fragments] Error comparing:", error);
|
|
534
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
535
|
+
res.end(
|
|
536
|
+
JSON.stringify({
|
|
537
|
+
error: error instanceof Error ? error.message : "Compare failed"
|
|
538
|
+
})
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
if (req.url === "/fragments/figma-styles" && req.method === "POST") {
|
|
544
|
+
try {
|
|
545
|
+
const body = await parseJsonBody(req);
|
|
546
|
+
const { figmaUrl } = body;
|
|
547
|
+
if (!figmaUrl) {
|
|
548
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
549
|
+
res.end(JSON.stringify({ error: "Missing figmaUrl" }));
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
const figmaToken = process.env.FIGMA_ACCESS_TOKEN || config.figmaToken;
|
|
553
|
+
if (!figmaToken) {
|
|
554
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
555
|
+
res.end(
|
|
556
|
+
JSON.stringify({
|
|
557
|
+
error: "No Figma access token configured",
|
|
558
|
+
suggestion: "Set FIGMA_ACCESS_TOKEN env var or add figmaToken to fragments.config.ts"
|
|
559
|
+
})
|
|
560
|
+
);
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const { FigmaClient } = await import("./service-T2L7VLTE.js");
|
|
564
|
+
const figmaClient = new FigmaClient({ accessToken: figmaToken });
|
|
565
|
+
const { fileKey, nodeId } = figmaClient.parseUrl(figmaUrl);
|
|
566
|
+
const figmaDesignProps = await figmaClient.getNodeProperties(
|
|
567
|
+
fileKey,
|
|
568
|
+
nodeId
|
|
569
|
+
);
|
|
570
|
+
const figmaStyles = figmaClient.convertToCSS(figmaDesignProps);
|
|
571
|
+
res.setHeader("Content-Type", "application/json");
|
|
572
|
+
res.end(JSON.stringify({ styles: figmaStyles }));
|
|
573
|
+
} catch (error) {
|
|
574
|
+
console.error("[Fragments] Error fetching Figma styles:", error);
|
|
575
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
576
|
+
res.end(
|
|
577
|
+
JSON.stringify({
|
|
578
|
+
error: error instanceof Error ? error.message : "Failed to fetch Figma styles"
|
|
579
|
+
})
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (req.url?.startsWith("/fragments/tokens")) {
|
|
585
|
+
try {
|
|
586
|
+
const url = new URL(req.url, "http://localhost");
|
|
587
|
+
const format = url.searchParams.get("format") || "json";
|
|
588
|
+
const category = url.searchParams.get("category");
|
|
589
|
+
const theme = url.searchParams.get("theme");
|
|
590
|
+
if (!config.tokens || !config.tokens.include || config.tokens.include.length === 0) {
|
|
591
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
592
|
+
res.end(JSON.stringify({
|
|
593
|
+
error: "No token configuration found",
|
|
594
|
+
suggestion: "Add 'tokens' config to fragments.config.ts with 'include' patterns for CSS/SCSS files",
|
|
595
|
+
example: {
|
|
596
|
+
tokens: {
|
|
597
|
+
include: ["src/styles/theme.scss", "src/styles/variables.css"],
|
|
598
|
+
themeSelectors: { ":root": "default", "[data-theme='dark']": "dark" }
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}));
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
const { getSharedTokenRegistry } = await import("./service-T2L7VLTE.js");
|
|
605
|
+
const registry = getSharedTokenRegistry();
|
|
606
|
+
if (!registry.isInitialized()) {
|
|
607
|
+
await registry.initialize(config.tokens, projectRoot);
|
|
608
|
+
}
|
|
609
|
+
let tokens = registry.getAllTokens();
|
|
610
|
+
if (category) {
|
|
611
|
+
tokens = tokens.filter((t) => t.category === category);
|
|
612
|
+
}
|
|
613
|
+
if (theme) {
|
|
614
|
+
tokens = tokens.filter((t) => t.theme === theme || t.theme === "default");
|
|
615
|
+
}
|
|
616
|
+
const meta = registry.getMeta();
|
|
617
|
+
if (format === "summary") {
|
|
618
|
+
const summary = {
|
|
619
|
+
totalTokens: meta?.totalTokens || 0,
|
|
620
|
+
byCategory: {},
|
|
621
|
+
byTheme: {},
|
|
622
|
+
parseTimeMs: meta?.parseTimeMs || 0,
|
|
623
|
+
sourceFiles: meta?.sourceFiles || []
|
|
624
|
+
};
|
|
625
|
+
for (const token of registry.getAllTokens()) {
|
|
626
|
+
summary.byCategory[token.category] = (summary.byCategory[token.category] || 0) + 1;
|
|
627
|
+
summary.byTheme[token.theme] = (summary.byTheme[token.theme] || 0) + 1;
|
|
628
|
+
}
|
|
629
|
+
res.setHeader("Content-Type", "application/json");
|
|
630
|
+
res.end(JSON.stringify(summary, null, 2));
|
|
631
|
+
} else {
|
|
632
|
+
res.setHeader("Content-Type", "application/json");
|
|
633
|
+
res.end(JSON.stringify({
|
|
634
|
+
tokens,
|
|
635
|
+
meta
|
|
636
|
+
}, null, 2));
|
|
637
|
+
}
|
|
638
|
+
} catch (error) {
|
|
639
|
+
console.error("[Fragments] Error fetching tokens:", error);
|
|
640
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
641
|
+
res.end(JSON.stringify({
|
|
642
|
+
error: error instanceof Error ? error.message : "Failed to fetch tokens"
|
|
643
|
+
}));
|
|
644
|
+
}
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
if (req.url === "/fragments/token-match" && req.method === "POST") {
|
|
648
|
+
try {
|
|
649
|
+
const body = await parseJsonBody(req);
|
|
650
|
+
const { value, propertyType, theme } = body;
|
|
651
|
+
if (!value) {
|
|
652
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
653
|
+
res.end(JSON.stringify({ error: "Missing required field: value" }));
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
if (!config.tokens || !config.tokens.include || config.tokens.include.length === 0) {
|
|
657
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
658
|
+
res.end(JSON.stringify({
|
|
659
|
+
error: "No token configuration found",
|
|
660
|
+
suggestion: "Add 'tokens' config to fragments.config.ts"
|
|
661
|
+
}));
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
const { getSharedTokenRegistry } = await import("./service-T2L7VLTE.js");
|
|
665
|
+
const registry = getSharedTokenRegistry();
|
|
666
|
+
if (!registry.isInitialized()) {
|
|
667
|
+
await registry.initialize(config.tokens, projectRoot);
|
|
668
|
+
}
|
|
669
|
+
const result = registry.matchValue({
|
|
670
|
+
value,
|
|
671
|
+
propertyType,
|
|
672
|
+
theme
|
|
673
|
+
});
|
|
674
|
+
res.setHeader("Content-Type", "application/json");
|
|
675
|
+
res.end(JSON.stringify(result));
|
|
676
|
+
} catch (error) {
|
|
677
|
+
console.error("[Fragments] Error matching token:", error);
|
|
678
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
679
|
+
res.end(JSON.stringify({
|
|
680
|
+
error: error instanceof Error ? error.message : "Failed to match token"
|
|
681
|
+
}));
|
|
682
|
+
}
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (req.url === "/fragments/compliance" && req.method === "POST") {
|
|
686
|
+
try {
|
|
687
|
+
const body = await parseJsonBody(req);
|
|
688
|
+
const { component, variant, theme = "default" } = body;
|
|
689
|
+
if (!component) {
|
|
690
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
691
|
+
res.end(JSON.stringify({ error: "Missing required field: component" }));
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
if (!config.tokens || !config.tokens.include || config.tokens.include.length === 0) {
|
|
695
|
+
res.setHeader("Content-Type", "application/json");
|
|
696
|
+
res.end(JSON.stringify({
|
|
697
|
+
component,
|
|
698
|
+
variant,
|
|
699
|
+
compliance: 100,
|
|
700
|
+
totalProperties: 0,
|
|
701
|
+
hardcoded: 0,
|
|
702
|
+
usingTokens: 0,
|
|
703
|
+
violations: [],
|
|
704
|
+
note: "No token configuration found - token compliance checking disabled"
|
|
705
|
+
}));
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
const loadedSegments = await loadSegmentsForRender(segmentFiles, projectRoot);
|
|
709
|
+
const segmentInfo = findSegmentByName(component, loadedSegments);
|
|
710
|
+
if (!segmentInfo) {
|
|
711
|
+
const available = getAvailableComponents(loadedSegments);
|
|
712
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
713
|
+
res.end(JSON.stringify({
|
|
714
|
+
error: `Component '${component}' not found. Available: ${available.join(", ")}`
|
|
715
|
+
}));
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
const segmentFile = segmentFiles.find(
|
|
719
|
+
(f) => f.relativePath === segmentInfo.path
|
|
720
|
+
);
|
|
721
|
+
if (!segmentFile) {
|
|
722
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
723
|
+
res.end(JSON.stringify({ error: "Could not resolve segment file path" }));
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
const { getSharedTokenRegistry } = await import("./service-T2L7VLTE.js");
|
|
727
|
+
const registry = getSharedTokenRegistry();
|
|
728
|
+
if (!registry.isInitialized()) {
|
|
729
|
+
await registry.initialize(config.tokens, projectRoot);
|
|
730
|
+
}
|
|
731
|
+
const address = _server.httpServer?.address();
|
|
732
|
+
const port = typeof address === "object" && address ? address.port : 6006;
|
|
733
|
+
const renderViewport = { width: 800, height: 600 };
|
|
734
|
+
const renderScript = generateRenderScript(
|
|
735
|
+
segmentFile.absolutePath,
|
|
736
|
+
segmentInfo.name,
|
|
737
|
+
{}
|
|
738
|
+
);
|
|
739
|
+
const requestId = Date.now().toString(36) + Math.random().toString(36).slice(2);
|
|
740
|
+
pendingRenders.set(requestId, { script: renderScript, viewport: renderViewport });
|
|
741
|
+
try {
|
|
742
|
+
const captureResult = await captureRenderWithStyles(
|
|
743
|
+
`http://localhost:${port}/fragments/__render__/${requestId}`,
|
|
744
|
+
renderViewport,
|
|
745
|
+
true
|
|
746
|
+
// extractStyles = true
|
|
747
|
+
);
|
|
748
|
+
const computedStyles = captureResult.computedStyles || {};
|
|
749
|
+
const styleDiffs = [];
|
|
750
|
+
for (const [property, value] of Object.entries(computedStyles)) {
|
|
751
|
+
if (!value) continue;
|
|
752
|
+
const matchResult = registry.matchValue({
|
|
753
|
+
value,
|
|
754
|
+
propertyType: property.toLowerCase().includes("color") ? "color" : property.toLowerCase().includes("font") ? "typography" : property.toLowerCase().includes("spacing") || property.toLowerCase().includes("padding") || property.toLowerCase().includes("margin") ? "spacing" : void 0,
|
|
755
|
+
theme
|
|
756
|
+
});
|
|
757
|
+
const isUsingToken = matchResult.exactMatches.length > 0;
|
|
758
|
+
styleDiffs.push({
|
|
759
|
+
property,
|
|
760
|
+
figma: value,
|
|
761
|
+
// Use the value as both figma and rendered for self-comparison
|
|
762
|
+
rendered: value,
|
|
763
|
+
match: isUsingToken
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
const usageSummary = registry.calculateUsageSummary(styleDiffs, theme);
|
|
767
|
+
const violations = usageSummary.hardcodedProperties.map((hp) => {
|
|
768
|
+
const suggestion = hp.suggestedFix ? `Use ${hp.suggestedFix.tokenName} (${hp.suggestedFix.tokenValue})` : void 0;
|
|
769
|
+
return {
|
|
770
|
+
property: hp.property,
|
|
771
|
+
issue: `Hardcoded value "${hp.rendered}" should use a design token`,
|
|
772
|
+
severity: "warning",
|
|
773
|
+
suggestion,
|
|
774
|
+
expected: hp.figmaToken,
|
|
775
|
+
actual: hp.rendered
|
|
776
|
+
};
|
|
777
|
+
});
|
|
778
|
+
res.setHeader("Content-Type", "application/json");
|
|
779
|
+
res.end(JSON.stringify({
|
|
780
|
+
component,
|
|
781
|
+
variant,
|
|
782
|
+
compliance: usageSummary.compliancePercent,
|
|
783
|
+
totalProperties: usageSummary.totalProperties,
|
|
784
|
+
hardcoded: usageSummary.hardcoded,
|
|
785
|
+
usingTokens: usageSummary.usingTokens,
|
|
786
|
+
violations
|
|
787
|
+
}));
|
|
788
|
+
} finally {
|
|
789
|
+
pendingRenders.delete(requestId);
|
|
790
|
+
}
|
|
791
|
+
} catch (error) {
|
|
792
|
+
console.error("[Fragments] Error checking compliance:", error);
|
|
793
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
794
|
+
res.end(JSON.stringify({
|
|
795
|
+
error: error instanceof Error ? error.message : "Compliance check failed"
|
|
796
|
+
}));
|
|
797
|
+
}
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
if (req.url?.startsWith("/fragments/context")) {
|
|
801
|
+
try {
|
|
802
|
+
const url = new URL(req.url, "http://localhost");
|
|
803
|
+
const format = url.searchParams.get("format") || "markdown";
|
|
804
|
+
const compact = url.searchParams.get("compact") === "true";
|
|
805
|
+
const compiledSegments = await loadSegmentsForContext(
|
|
806
|
+
_server,
|
|
807
|
+
segmentFiles,
|
|
808
|
+
config,
|
|
809
|
+
projectRoot
|
|
810
|
+
);
|
|
811
|
+
const { content, tokenEstimate } = generateContext(
|
|
812
|
+
compiledSegments,
|
|
813
|
+
{
|
|
814
|
+
format,
|
|
815
|
+
compact,
|
|
816
|
+
include: {
|
|
817
|
+
code: url.searchParams.get("code") === "true",
|
|
818
|
+
relations: url.searchParams.get("relations") === "true"
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
);
|
|
822
|
+
res.setHeader("X-Token-Estimate", String(tokenEstimate));
|
|
823
|
+
res.setHeader(
|
|
824
|
+
"Content-Type",
|
|
825
|
+
format === "json" ? "application/json" : "text/markdown; charset=utf-8"
|
|
826
|
+
);
|
|
827
|
+
res.end(content);
|
|
828
|
+
} catch (error) {
|
|
829
|
+
console.error("[Fragments] Error generating context:", error);
|
|
830
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
831
|
+
res.end(
|
|
832
|
+
"Error generating context: " + (error instanceof Error ? error.message : error)
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
if (req.url === "/fragments/save" && req.method === "POST") {
|
|
838
|
+
try {
|
|
839
|
+
const body = await parseJsonBody(req);
|
|
840
|
+
const { componentName, fragment } = body;
|
|
841
|
+
if (!componentName || !fragment) {
|
|
842
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
843
|
+
res.end(
|
|
844
|
+
JSON.stringify({
|
|
845
|
+
error: "Missing required fields: componentName, fragment"
|
|
846
|
+
})
|
|
847
|
+
);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
const { writeFile, mkdir } = await import("fs/promises");
|
|
851
|
+
const { join: join2 } = await import("path");
|
|
852
|
+
const { BRAND: BRAND2 } = await import("./core-W2HYIQW6.js");
|
|
853
|
+
const fragmentsDir = join2(projectRoot, BRAND2.dataDir, BRAND2.componentsDir);
|
|
854
|
+
await mkdir(fragmentsDir, { recursive: true });
|
|
855
|
+
const fragmentPath = join2(
|
|
856
|
+
fragmentsDir,
|
|
857
|
+
`${componentName}${BRAND2.fileExtension}`
|
|
858
|
+
);
|
|
859
|
+
await writeFile(
|
|
860
|
+
fragmentPath,
|
|
861
|
+
JSON.stringify(fragment, null, 2),
|
|
862
|
+
"utf-8"
|
|
863
|
+
);
|
|
864
|
+
res.setHeader("Content-Type", "application/json");
|
|
865
|
+
res.end(JSON.stringify({ success: true, path: fragmentPath }));
|
|
866
|
+
} catch (error) {
|
|
867
|
+
console.error("[Fragments] Error saving fragment:", error);
|
|
868
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
869
|
+
res.end(
|
|
870
|
+
JSON.stringify({
|
|
871
|
+
error: error instanceof Error ? error.message : "Save failed"
|
|
872
|
+
})
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
if (req.url === "/fragments/fix" && req.method === "POST") {
|
|
878
|
+
try {
|
|
879
|
+
const body = await parseJsonBody(req);
|
|
880
|
+
const { component, variant, fixType = "all" } = body;
|
|
881
|
+
if (!component) {
|
|
882
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
883
|
+
res.end(JSON.stringify({ error: "Missing required field: component" }));
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
if (!config.tokens || !config.tokens.include || config.tokens.include.length === 0) {
|
|
887
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
888
|
+
res.end(JSON.stringify({
|
|
889
|
+
error: "No token configuration found",
|
|
890
|
+
suggestion: "Add 'tokens' config to fragments.config.ts to enable fix generation"
|
|
891
|
+
}));
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
const loadedSegments = await loadSegmentsForRender(segmentFiles, projectRoot);
|
|
895
|
+
const segmentInfo = findSegmentByName(component, loadedSegments);
|
|
896
|
+
if (!segmentInfo) {
|
|
897
|
+
const available = getAvailableComponents(loadedSegments);
|
|
898
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
899
|
+
res.end(JSON.stringify({
|
|
900
|
+
error: `Component '${component}' not found. Available: ${available.join(", ")}`
|
|
901
|
+
}));
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
const {
|
|
905
|
+
getSharedTokenRegistry,
|
|
906
|
+
generateTokenPatches
|
|
907
|
+
} = await import("./service-T2L7VLTE.js");
|
|
908
|
+
const registry = getSharedTokenRegistry();
|
|
909
|
+
if (!registry.isInitialized()) {
|
|
910
|
+
await registry.initialize(config.tokens, projectRoot);
|
|
911
|
+
}
|
|
912
|
+
const segmentFile = segmentFiles.find(
|
|
913
|
+
(f) => f.relativePath === segmentInfo.path
|
|
914
|
+
);
|
|
915
|
+
const sourceFile = segmentFile?.relativePath || `${component}.tsx`;
|
|
916
|
+
const result = generateTokenPatches(
|
|
917
|
+
component,
|
|
918
|
+
[],
|
|
919
|
+
// Would be populated by actual style diffs
|
|
920
|
+
registry,
|
|
921
|
+
{ sourceFile }
|
|
922
|
+
);
|
|
923
|
+
res.setHeader("Content-Type", "application/json");
|
|
924
|
+
res.end(JSON.stringify({
|
|
925
|
+
patches: result.patches,
|
|
926
|
+
summary: result.summary,
|
|
927
|
+
fixableCount: result.fixableCount,
|
|
928
|
+
unfixableCount: result.unfixableCount
|
|
929
|
+
}));
|
|
930
|
+
} catch (error) {
|
|
931
|
+
console.error("[Fragments] Error generating fixes:", error);
|
|
932
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
933
|
+
res.end(JSON.stringify({
|
|
934
|
+
error: error instanceof Error ? error.message : "Fix generation failed"
|
|
935
|
+
}));
|
|
936
|
+
}
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
if (req.url?.startsWith("/fragments/preview")) {
|
|
940
|
+
if (req.url === "/fragments/preview") {
|
|
941
|
+
res.writeHead(302, { Location: "/fragments/preview/" });
|
|
942
|
+
res.end();
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
await servePreviewFrameHTML(res, _server);
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
if (req.url === "/segments" || req.url === "/fragments/") {
|
|
949
|
+
if (!req.url.endsWith("/")) {
|
|
950
|
+
res.writeHead(302, { Location: "/fragments/" });
|
|
951
|
+
res.end();
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
serveViewerHTML(res, _server);
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
next();
|
|
958
|
+
});
|
|
959
|
+
_server.httpServer?.once("listening", () => {
|
|
960
|
+
const address = _server.httpServer?.address();
|
|
961
|
+
const port = typeof address === "object" && address ? address.port : 6006;
|
|
962
|
+
console.log(
|
|
963
|
+
`
|
|
964
|
+
\u{1F4E6} Fragments Viewer: http://localhost:${port}/fragments/
|
|
965
|
+
`
|
|
966
|
+
);
|
|
967
|
+
});
|
|
968
|
+
},
|
|
969
|
+
// Resolve virtual modules
|
|
970
|
+
resolveId(id) {
|
|
971
|
+
if (id === VIRTUAL_SEGMENTS) {
|
|
972
|
+
return VIRTUAL_SEGMENTS_RESOLVED;
|
|
973
|
+
}
|
|
974
|
+
if (id === VIRTUAL_VIEWER_ENTRY) {
|
|
975
|
+
return VIRTUAL_VIEWER_ENTRY_RESOLVED;
|
|
976
|
+
}
|
|
977
|
+
if (id === VIRTUAL_PREVIEW) {
|
|
978
|
+
return VIRTUAL_PREVIEW_RESOLVED;
|
|
979
|
+
}
|
|
980
|
+
return null;
|
|
981
|
+
},
|
|
982
|
+
// Load virtual modules
|
|
983
|
+
load(id) {
|
|
984
|
+
if (id === VIRTUAL_SEGMENTS_RESOLVED) {
|
|
985
|
+
return generateSegmentsModule(segmentFiles, config, previewConfigPath);
|
|
986
|
+
}
|
|
987
|
+
if (id === VIRTUAL_VIEWER_ENTRY_RESOLVED) {
|
|
988
|
+
return generateViewerEntry();
|
|
989
|
+
}
|
|
990
|
+
if (id === VIRTUAL_PREVIEW_RESOLVED) {
|
|
991
|
+
return generatePreviewModule(previewConfigPath);
|
|
992
|
+
}
|
|
993
|
+
return null;
|
|
994
|
+
},
|
|
995
|
+
// Handle HMR for segment files
|
|
996
|
+
handleHotUpdate({ file, server: server2 }) {
|
|
997
|
+
if (segmentFileSet.has(file)) {
|
|
998
|
+
const mod = server2.moduleGraph.getModuleById(VIRTUAL_SEGMENTS_RESOLVED);
|
|
999
|
+
if (mod) {
|
|
1000
|
+
server2.moduleGraph.invalidateModule(mod);
|
|
1001
|
+
}
|
|
1002
|
+
server2.ws.send({
|
|
1003
|
+
type: "custom",
|
|
1004
|
+
event: "segments:update",
|
|
1005
|
+
data: { file }
|
|
1006
|
+
});
|
|
1007
|
+
return [];
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
const jsxTransformPlugin = {
|
|
1012
|
+
name: "segments-jsx-transform",
|
|
1013
|
+
enforce: "pre",
|
|
1014
|
+
async load(id) {
|
|
1015
|
+
if (!id.endsWith(".js")) return null;
|
|
1016
|
+
if (!id.includes(".storybook")) return null;
|
|
1017
|
+
const fs = await import("fs/promises");
|
|
1018
|
+
let code;
|
|
1019
|
+
try {
|
|
1020
|
+
code = await fs.readFile(id, "utf-8");
|
|
1021
|
+
} catch {
|
|
1022
|
+
return null;
|
|
1023
|
+
}
|
|
1024
|
+
const hasOpeningTag = code.includes("<");
|
|
1025
|
+
const hasSelfClosingTag = code.includes("/>");
|
|
1026
|
+
const hasClosingTag = code.includes("</");
|
|
1027
|
+
if (!hasOpeningTag || !hasSelfClosingTag && !hasClosingTag) return null;
|
|
1028
|
+
try {
|
|
1029
|
+
const result = await transform(code, {
|
|
1030
|
+
loader: "jsx",
|
|
1031
|
+
jsx: "automatic",
|
|
1032
|
+
sourcefile: id,
|
|
1033
|
+
sourcemap: true
|
|
1034
|
+
});
|
|
1035
|
+
return {
|
|
1036
|
+
code: result.code,
|
|
1037
|
+
map: result.map
|
|
1038
|
+
};
|
|
1039
|
+
} catch (error) {
|
|
1040
|
+
console.warn(`[Fragments] JSX transform failed for ${id}:`, error instanceof Error ? error.message : error);
|
|
1041
|
+
return null;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
return [
|
|
1046
|
+
// JSX transform for .js files (must run first)
|
|
1047
|
+
jsxTransformPlugin,
|
|
1048
|
+
// SVGR plugin to handle `import { ReactComponent } from "*.svg"` pattern
|
|
1049
|
+
svgr({
|
|
1050
|
+
svgrOptions: {
|
|
1051
|
+
exportType: "named"
|
|
1052
|
+
// Export as { ReactComponent }
|
|
1053
|
+
},
|
|
1054
|
+
include: "**/*.svg"
|
|
1055
|
+
}),
|
|
1056
|
+
// Main segments plugin
|
|
1057
|
+
mainPlugin
|
|
1058
|
+
];
|
|
1059
|
+
}
|
|
1060
|
+
function isStoryFile(filePath) {
|
|
1061
|
+
return /\.stories\.(tsx?|jsx?)$/.test(filePath);
|
|
1062
|
+
}
|
|
1063
|
+
function getBaseComponentPath(filePath) {
|
|
1064
|
+
return filePath.replace(/\.(segment|stories)\.(tsx?|jsx?)$/, "");
|
|
1065
|
+
}
|
|
1066
|
+
function generateSegmentsModule(segmentFiles, config, previewConfigPath) {
|
|
1067
|
+
const filesByBasePath = /* @__PURE__ */ new Map();
|
|
1068
|
+
for (const file of segmentFiles) {
|
|
1069
|
+
const basePath = getBaseComponentPath(file.relativePath);
|
|
1070
|
+
const isStory = isStoryFile(file.relativePath);
|
|
1071
|
+
const existing = filesByBasePath.get(basePath) || {};
|
|
1072
|
+
if (isStory) {
|
|
1073
|
+
existing.storyFile = file;
|
|
1074
|
+
} else {
|
|
1075
|
+
existing.segmentFile = file;
|
|
1076
|
+
}
|
|
1077
|
+
filesByBasePath.set(basePath, existing);
|
|
1078
|
+
}
|
|
1079
|
+
const loaders = Array.from(filesByBasePath.values()).map((files) => {
|
|
1080
|
+
const primaryFile = files.storyFile || files.segmentFile;
|
|
1081
|
+
if (!primaryFile) return null;
|
|
1082
|
+
const isStory = !!files.storyFile;
|
|
1083
|
+
const metadataPath = files.storyFile && files.segmentFile ? files.segmentFile.absolutePath : null;
|
|
1084
|
+
return ` {
|
|
1085
|
+
path: "${primaryFile.relativePath}",
|
|
1086
|
+
isStory: ${isStory},
|
|
1087
|
+
loader: () => import("${primaryFile.absolutePath}"),
|
|
1088
|
+
metadataLoader: ${metadataPath ? `() => import("${metadataPath}")` : "null"}
|
|
1089
|
+
}`;
|
|
1090
|
+
}).filter(Boolean).join(",\n");
|
|
1091
|
+
const previewImport = previewConfigPath ? `import * as previewConfig from "virtual:segments-preview";` : "";
|
|
1092
|
+
const previewSetup = previewConfigPath ? `
|
|
1093
|
+
// Set global preview config before loading segments
|
|
1094
|
+
setPreviewConfig({
|
|
1095
|
+
decorators: previewConfig.decorators,
|
|
1096
|
+
parameters: previewConfig.parameters,
|
|
1097
|
+
globalTypes: previewConfig.globalTypes,
|
|
1098
|
+
args: previewConfig.args,
|
|
1099
|
+
argTypes: previewConfig.argTypes,
|
|
1100
|
+
loaders: previewConfig.loaders,
|
|
1101
|
+
});
|
|
1102
|
+
` : "";
|
|
1103
|
+
return `
|
|
1104
|
+
import { storyModuleToSegment, setPreviewConfig } from "@fragments/core";
|
|
1105
|
+
${previewImport}
|
|
1106
|
+
${previewSetup}
|
|
1107
|
+
// Lazy segment loaders (supports both .segment.tsx and .stories.tsx)
|
|
1108
|
+
const segmentLoaders = [
|
|
1109
|
+
${loaders}
|
|
1110
|
+
];
|
|
1111
|
+
|
|
1112
|
+
// Cache for loaded segments
|
|
1113
|
+
const loadedSegments = new Map();
|
|
1114
|
+
|
|
1115
|
+
/**
|
|
1116
|
+
* Merge metadata from a segment file into a story-based segment.
|
|
1117
|
+
* This preserves Figma URLs and other AI-agent focused data.
|
|
1118
|
+
*/
|
|
1119
|
+
function mergeMetadata(segment, metadataModule) {
|
|
1120
|
+
if (!metadataModule?.default) return segment;
|
|
1121
|
+
|
|
1122
|
+
const metadata = metadataModule.default;
|
|
1123
|
+
|
|
1124
|
+
// Merge meta-level Figma URL
|
|
1125
|
+
if (metadata.meta?.figma && !segment.meta.figma) {
|
|
1126
|
+
segment.meta.figma = metadata.meta.figma;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// Merge description if not present
|
|
1130
|
+
if (metadata.meta?.description && !segment.meta.description) {
|
|
1131
|
+
segment.meta.description = metadata.meta.description;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// Merge variant-level Figma URLs
|
|
1135
|
+
if (metadata.variants && segment.variants) {
|
|
1136
|
+
for (const metaVariant of metadata.variants) {
|
|
1137
|
+
const segmentVariant = segment.variants.find(v => v.name === metaVariant.name);
|
|
1138
|
+
if (segmentVariant && metaVariant.figma && !segmentVariant.figma) {
|
|
1139
|
+
segmentVariant.figma = metaVariant.figma;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
return segment;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// Load all segments (for initial render)
|
|
1148
|
+
// Gracefully handles individual failures - one bad story won't break all segments
|
|
1149
|
+
export async function loadAllSegments() {
|
|
1150
|
+
const results = await Promise.all(
|
|
1151
|
+
segmentLoaders.map(async (loader) => {
|
|
1152
|
+
try {
|
|
1153
|
+
if (loadedSegments.has(loader.path)) {
|
|
1154
|
+
const cached = loadedSegments.get(loader.path);
|
|
1155
|
+
return cached ? { path: loader.path, segment: cached } : null;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
const module = await loader.loader();
|
|
1159
|
+
|
|
1160
|
+
// Convert story modules to segments at runtime
|
|
1161
|
+
let segment;
|
|
1162
|
+
if (loader.isStory) {
|
|
1163
|
+
segment = storyModuleToSegment(module, loader.path);
|
|
1164
|
+
// storyModuleToSegment returns null for stories without a component
|
|
1165
|
+
if (!segment) {
|
|
1166
|
+
loadedSegments.set(loader.path, null);
|
|
1167
|
+
return null;
|
|
1168
|
+
}
|
|
1169
|
+
} else {
|
|
1170
|
+
segment = module.default;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// Merge metadata from corresponding segment file if available
|
|
1174
|
+
if (loader.metadataLoader) {
|
|
1175
|
+
try {
|
|
1176
|
+
const metadataModule = await loader.metadataLoader();
|
|
1177
|
+
segment = mergeMetadata(segment, metadataModule);
|
|
1178
|
+
} catch (metaError) {
|
|
1179
|
+
// Metadata loading is optional - don't fail if it errors
|
|
1180
|
+
console.warn("[Fragments] Could not load metadata for " + loader.path + ":", metaError.message);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
loadedSegments.set(loader.path, segment);
|
|
1185
|
+
return { path: loader.path, segment };
|
|
1186
|
+
} catch (error) {
|
|
1187
|
+
console.warn("[Fragments] Failed to load " + loader.path + ":", error.message);
|
|
1188
|
+
return null;
|
|
1189
|
+
}
|
|
1190
|
+
})
|
|
1191
|
+
);
|
|
1192
|
+
// Filter out failed loads
|
|
1193
|
+
return results.filter(r => r !== null);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// Load a single segment by path
|
|
1197
|
+
export async function loadSegment(path) {
|
|
1198
|
+
const loader = segmentLoaders.find(l => l.path === path);
|
|
1199
|
+
if (!loader) return null;
|
|
1200
|
+
|
|
1201
|
+
if (loadedSegments.has(path)) {
|
|
1202
|
+
return loadedSegments.get(path);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
const module = await loader.loader();
|
|
1206
|
+
|
|
1207
|
+
// Convert story modules to segments at runtime
|
|
1208
|
+
let segment;
|
|
1209
|
+
if (loader.isStory) {
|
|
1210
|
+
segment = storyModuleToSegment(module, path);
|
|
1211
|
+
} else {
|
|
1212
|
+
segment = module.default;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// Merge metadata from corresponding segment file if available
|
|
1216
|
+
if (loader.metadataLoader && segment) {
|
|
1217
|
+
try {
|
|
1218
|
+
const metadataModule = await loader.metadataLoader();
|
|
1219
|
+
segment = mergeMetadata(segment, metadataModule);
|
|
1220
|
+
} catch (metaError) {
|
|
1221
|
+
console.warn("[Fragments] Could not load metadata for " + path + ":", metaError.message);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
loadedSegments.set(path, segment);
|
|
1226
|
+
return segment;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// For backwards compatibility, load all segments synchronously on import
|
|
1230
|
+
// This is still lazy per-file but awaited at module load
|
|
1231
|
+
let segments = [];
|
|
1232
|
+
const segmentsPromise = loadAllSegments().then(s => { segments = s; return s; });
|
|
1233
|
+
|
|
1234
|
+
export { segments, segmentsPromise };
|
|
1235
|
+
export const config = ${JSON.stringify(config)};
|
|
1236
|
+
|
|
1237
|
+
// HMR support
|
|
1238
|
+
if (import.meta.hot) {
|
|
1239
|
+
import.meta.hot.accept();
|
|
1240
|
+
|
|
1241
|
+
import.meta.hot.on("segments:update", (data) => {
|
|
1242
|
+
console.log("[Fragments] File updated:", data.file);
|
|
1243
|
+
// Clear cache for the updated file (handles both .segment and .stories)
|
|
1244
|
+
for (const [path, _] of loadedSegments) {
|
|
1245
|
+
const basePath = path.replace(/\\.(segment|stories)\\.tsx?$/, '');
|
|
1246
|
+
if (data.file.includes(basePath)) {
|
|
1247
|
+
loadedSegments.delete(path);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
// Trigger re-render in viewer
|
|
1251
|
+
window.dispatchEvent(new CustomEvent("segments:update"));
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
`;
|
|
1255
|
+
}
|
|
1256
|
+
function generateViewerEntry() {
|
|
1257
|
+
return `
|
|
1258
|
+
import { segments, config } from "virtual:segments";
|
|
1259
|
+
|
|
1260
|
+
// Re-export for viewer
|
|
1261
|
+
export { segments, config };
|
|
1262
|
+
|
|
1263
|
+
// Initialize viewer
|
|
1264
|
+
console.log("[Fragments] Loaded", segments.length, "segment(s)");
|
|
1265
|
+
`;
|
|
1266
|
+
}
|
|
1267
|
+
async function loadSegmentsForContext(_server, _segmentFiles, _config, configDir) {
|
|
1268
|
+
const { join: join2 } = await import("path");
|
|
1269
|
+
const segmentsJsonPath = join2(configDir || process.cwd(), BRAND.outFile);
|
|
1270
|
+
try {
|
|
1271
|
+
const content = await readFile(segmentsJsonPath, "utf-8");
|
|
1272
|
+
const data = JSON.parse(content);
|
|
1273
|
+
return Object.values(data.segments || {});
|
|
1274
|
+
} catch (error) {
|
|
1275
|
+
console.warn(
|
|
1276
|
+
`[${BRAND.name}] Failed to load ${BRAND.outFile} for context:`,
|
|
1277
|
+
error
|
|
1278
|
+
);
|
|
1279
|
+
console.warn(`[${BRAND.name}] Run '${BRAND.cliCommand} build' to generate ${BRAND.outFile}`);
|
|
1280
|
+
return [];
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
async function serveViewerHTML(res, server) {
|
|
1284
|
+
const viewerRoot2 = viewerAssetsRoot;
|
|
1285
|
+
const entryPath = resolve(viewerRoot2, "entry.tsx");
|
|
1286
|
+
try {
|
|
1287
|
+
let html = await readFile(resolve(viewerRoot2, "index.html"), "utf-8");
|
|
1288
|
+
html = html.replace("/src/entry.tsx", entryPath);
|
|
1289
|
+
html = await server.transformIndexHtml("/fragments/", html);
|
|
1290
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1291
|
+
res.end(html);
|
|
1292
|
+
} catch (error) {
|
|
1293
|
+
console.error("[Fragments] Error serving viewer:", error);
|
|
1294
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
1295
|
+
res.end("Error loading Segments viewer");
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
async function servePreviewFrameHTML(res, server) {
|
|
1299
|
+
const viewerRoot2 = viewerAssetsRoot;
|
|
1300
|
+
const entryPath = resolve(viewerRoot2, "preview-frame-entry.tsx");
|
|
1301
|
+
try {
|
|
1302
|
+
let html = await readFile(resolve(viewerRoot2, "preview-frame.html"), "utf-8");
|
|
1303
|
+
html = html.replace("/src/preview-frame-entry.tsx", entryPath);
|
|
1304
|
+
html = await server.transformIndexHtml("/fragments/preview/", html);
|
|
1305
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1306
|
+
res.end(html);
|
|
1307
|
+
} catch (error) {
|
|
1308
|
+
console.error("[Fragments] Error serving preview frame:", error);
|
|
1309
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
1310
|
+
res.end("Error loading preview frame");
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
async function parseJsonBody(req) {
|
|
1314
|
+
return new Promise((resolve3, reject) => {
|
|
1315
|
+
let body = "";
|
|
1316
|
+
req.on("data", (chunk) => {
|
|
1317
|
+
body += chunk.toString();
|
|
1318
|
+
});
|
|
1319
|
+
req.on("end", () => {
|
|
1320
|
+
try {
|
|
1321
|
+
resolve3(JSON.parse(body));
|
|
1322
|
+
} catch (error) {
|
|
1323
|
+
reject(new Error("Invalid JSON body"));
|
|
1324
|
+
}
|
|
1325
|
+
});
|
|
1326
|
+
req.on("error", reject);
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
async function loadSegmentsForRender(segmentFiles, configDir) {
|
|
1330
|
+
const { join: join2 } = await import("path");
|
|
1331
|
+
const segmentsJsonPath = join2(configDir, BRAND.outFile);
|
|
1332
|
+
try {
|
|
1333
|
+
const content = await readFile(segmentsJsonPath, "utf-8");
|
|
1334
|
+
const data = JSON.parse(content);
|
|
1335
|
+
const segmentEntries = Object.values(data.segments || {});
|
|
1336
|
+
if (segmentEntries.length > 0) {
|
|
1337
|
+
return segmentEntries.map((segment) => ({
|
|
1338
|
+
path: segment.filePath,
|
|
1339
|
+
segment: { meta: { name: segment.meta.name } }
|
|
1340
|
+
}));
|
|
1341
|
+
}
|
|
1342
|
+
} catch {
|
|
1343
|
+
}
|
|
1344
|
+
return segmentFiles.map((f) => {
|
|
1345
|
+
let name;
|
|
1346
|
+
if (isStoryFile(f.relativePath)) {
|
|
1347
|
+
const match = f.relativePath.match(/\/([^/]+)\.stories\./);
|
|
1348
|
+
name = match ? match[1] : f.relativePath;
|
|
1349
|
+
} else {
|
|
1350
|
+
const match = f.relativePath.match(/\/([^/]+)\.segment\./);
|
|
1351
|
+
name = match ? match[1] : f.relativePath;
|
|
1352
|
+
}
|
|
1353
|
+
return {
|
|
1354
|
+
path: f.relativePath,
|
|
1355
|
+
segment: { meta: { name } }
|
|
1356
|
+
};
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
async function serveRenderHTML(res, server, renderScript) {
|
|
1360
|
+
const viewerRoot2 = viewerAssetsRoot;
|
|
1361
|
+
try {
|
|
1362
|
+
let html = await readFile(
|
|
1363
|
+
resolve(viewerRoot2, "render-template.html"),
|
|
1364
|
+
"utf-8"
|
|
1365
|
+
);
|
|
1366
|
+
html = html.replace(
|
|
1367
|
+
"<!-- RENDER_SCRIPT_PLACEHOLDER -->",
|
|
1368
|
+
`<script type="module">${renderScript}</script>`
|
|
1369
|
+
);
|
|
1370
|
+
const uniqueUrl = `/fragments/__render__/${Date.now()}`;
|
|
1371
|
+
html = await server.transformIndexHtml(uniqueUrl, html);
|
|
1372
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1373
|
+
res.end(html);
|
|
1374
|
+
} catch (error) {
|
|
1375
|
+
console.error("[Fragments] Error serving render page:", error);
|
|
1376
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
1377
|
+
res.end("Error loading render page");
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
async function captureRender(url, viewport) {
|
|
1381
|
+
const { pool, bufferToBase64Url } = await getSharedRenderPool();
|
|
1382
|
+
const ctx = await pool.acquire();
|
|
1383
|
+
const page = await ctx.newPage();
|
|
1384
|
+
try {
|
|
1385
|
+
await page.setViewportSize(viewport);
|
|
1386
|
+
await page.goto(url, { waitUntil: "networkidle" });
|
|
1387
|
+
await page.waitForFunction(
|
|
1388
|
+
() => window.__RENDER_READY__ === true,
|
|
1389
|
+
{ timeout: 1e4 }
|
|
1390
|
+
);
|
|
1391
|
+
const error = await page.evaluate(() => window.__RENDER_ERROR__);
|
|
1392
|
+
if (error) {
|
|
1393
|
+
throw new Error(`Render error: ${error}`);
|
|
1394
|
+
}
|
|
1395
|
+
const element = await page.$("#render-root");
|
|
1396
|
+
if (!element) {
|
|
1397
|
+
throw new Error("Render root element not found");
|
|
1398
|
+
}
|
|
1399
|
+
const screenshot = await element.screenshot({ type: "png" });
|
|
1400
|
+
return bufferToBase64Url(screenshot);
|
|
1401
|
+
} finally {
|
|
1402
|
+
await page.close();
|
|
1403
|
+
pool.release(ctx);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
async function captureRenderWithStyles(url, viewport, extractStyles) {
|
|
1407
|
+
const { pool, bufferToBase64Url } = await getSharedRenderPool();
|
|
1408
|
+
const ctx = await pool.acquire();
|
|
1409
|
+
const page = await ctx.newPage();
|
|
1410
|
+
try {
|
|
1411
|
+
await page.setViewportSize(viewport);
|
|
1412
|
+
await page.goto(url, { waitUntil: "networkidle" });
|
|
1413
|
+
await page.waitForFunction(
|
|
1414
|
+
() => window.__RENDER_READY__ === true,
|
|
1415
|
+
{ timeout: 1e4 }
|
|
1416
|
+
);
|
|
1417
|
+
const error = await page.evaluate(() => window.__RENDER_ERROR__);
|
|
1418
|
+
if (error) {
|
|
1419
|
+
throw new Error(`Render error: ${error}`);
|
|
1420
|
+
}
|
|
1421
|
+
const element = await page.$("#render-root");
|
|
1422
|
+
if (!element) {
|
|
1423
|
+
throw new Error("Render root element not found");
|
|
1424
|
+
}
|
|
1425
|
+
let computedStyles = null;
|
|
1426
|
+
if (extractStyles) {
|
|
1427
|
+
computedStyles = await page.evaluate(() => {
|
|
1428
|
+
const root = document.getElementById("render-root");
|
|
1429
|
+
if (!root) return null;
|
|
1430
|
+
const isVisibleColor = (color) => {
|
|
1431
|
+
if (!color) return false;
|
|
1432
|
+
if (color === "transparent") return false;
|
|
1433
|
+
if (color === "rgba(0, 0, 0, 0)") return false;
|
|
1434
|
+
if (color.includes("rgba") && color.includes(", 0)")) return false;
|
|
1435
|
+
return true;
|
|
1436
|
+
};
|
|
1437
|
+
const extractStylesFromElement = (el) => {
|
|
1438
|
+
const styles = window.getComputedStyle(el);
|
|
1439
|
+
const relevantProps = [
|
|
1440
|
+
"backgroundColor",
|
|
1441
|
+
"borderColor",
|
|
1442
|
+
"borderWidth",
|
|
1443
|
+
"borderRadius",
|
|
1444
|
+
"fontFamily",
|
|
1445
|
+
"fontSize",
|
|
1446
|
+
"fontWeight",
|
|
1447
|
+
"lineHeight",
|
|
1448
|
+
"letterSpacing",
|
|
1449
|
+
"textAlign",
|
|
1450
|
+
"boxShadow",
|
|
1451
|
+
"padding",
|
|
1452
|
+
"paddingTop",
|
|
1453
|
+
"paddingRight",
|
|
1454
|
+
"paddingBottom",
|
|
1455
|
+
"paddingLeft",
|
|
1456
|
+
"gap",
|
|
1457
|
+
"opacity",
|
|
1458
|
+
"width",
|
|
1459
|
+
"height"
|
|
1460
|
+
];
|
|
1461
|
+
const result2 = {};
|
|
1462
|
+
for (const prop of relevantProps) {
|
|
1463
|
+
const value = styles.getPropertyValue(
|
|
1464
|
+
prop.replace(/([A-Z])/g, "-$1").toLowerCase()
|
|
1465
|
+
);
|
|
1466
|
+
if (value) {
|
|
1467
|
+
result2[prop] = value;
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
return result2;
|
|
1471
|
+
};
|
|
1472
|
+
const candidates = root.querySelectorAll("*");
|
|
1473
|
+
let bestElement = null;
|
|
1474
|
+
let bestScore = -1;
|
|
1475
|
+
for (const el of candidates) {
|
|
1476
|
+
const htmlEl = el;
|
|
1477
|
+
const styles = window.getComputedStyle(htmlEl);
|
|
1478
|
+
let score = 0;
|
|
1479
|
+
const bg = styles.backgroundColor;
|
|
1480
|
+
if (isVisibleColor(bg)) {
|
|
1481
|
+
score += 10;
|
|
1482
|
+
}
|
|
1483
|
+
const border = styles.borderWidth;
|
|
1484
|
+
if (border && border !== "0px") {
|
|
1485
|
+
score += 3;
|
|
1486
|
+
}
|
|
1487
|
+
const boxShadow = styles.boxShadow;
|
|
1488
|
+
if (boxShadow && boxShadow !== "none") {
|
|
1489
|
+
score += 3;
|
|
1490
|
+
}
|
|
1491
|
+
const tagName = htmlEl.tagName.toLowerCase();
|
|
1492
|
+
if (["button", "a", "input", "select", "textarea"].includes(tagName)) {
|
|
1493
|
+
score += 5;
|
|
1494
|
+
}
|
|
1495
|
+
if (htmlEl.getAttribute("role") === "button") {
|
|
1496
|
+
score += 5;
|
|
1497
|
+
}
|
|
1498
|
+
const rect = htmlEl.getBoundingClientRect();
|
|
1499
|
+
if (rect.width < 10 || rect.height < 10) {
|
|
1500
|
+
score -= 10;
|
|
1501
|
+
}
|
|
1502
|
+
if (rect.width > 500 || rect.height > 500) {
|
|
1503
|
+
score -= 3;
|
|
1504
|
+
}
|
|
1505
|
+
if (score > bestScore) {
|
|
1506
|
+
bestScore = score;
|
|
1507
|
+
bestElement = htmlEl;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
if (!bestElement) {
|
|
1511
|
+
bestElement = root.firstElementChild;
|
|
1512
|
+
}
|
|
1513
|
+
if (!bestElement) return null;
|
|
1514
|
+
const result = extractStylesFromElement(bestElement);
|
|
1515
|
+
if (result.paddingTop && result.paddingRight && result.paddingBottom && result.paddingLeft) {
|
|
1516
|
+
const t = result.paddingTop;
|
|
1517
|
+
const r = result.paddingRight;
|
|
1518
|
+
const b = result.paddingBottom;
|
|
1519
|
+
const l = result.paddingLeft;
|
|
1520
|
+
if (t === r && r === b && b === l) {
|
|
1521
|
+
result.padding = t;
|
|
1522
|
+
} else if (t === b && r === l) {
|
|
1523
|
+
result.padding = `${t} ${r}`;
|
|
1524
|
+
} else {
|
|
1525
|
+
result.padding = `${t} ${r} ${b} ${l}`;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
return result;
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
const screenshot = await element.screenshot({ type: "png" });
|
|
1532
|
+
return {
|
|
1533
|
+
screenshot: bufferToBase64Url(screenshot),
|
|
1534
|
+
computedStyles
|
|
1535
|
+
};
|
|
1536
|
+
} finally {
|
|
1537
|
+
await page.close();
|
|
1538
|
+
pool.release(ctx);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
async function loadFullSegmentForCompare(_server, _segmentFiles, componentName, variantName, configDir) {
|
|
1542
|
+
const { join: join2 } = await import("path");
|
|
1543
|
+
const segmentsJsonPath = join2(configDir || process.cwd(), BRAND.outFile);
|
|
1544
|
+
try {
|
|
1545
|
+
const content = await readFile(segmentsJsonPath, "utf-8");
|
|
1546
|
+
const data = JSON.parse(content);
|
|
1547
|
+
const segment = data.segments[componentName];
|
|
1548
|
+
if (!segment) {
|
|
1549
|
+
return null;
|
|
1550
|
+
}
|
|
1551
|
+
if (variantName && segment.variants) {
|
|
1552
|
+
const variant = segment.variants.find((v) => v.name === variantName);
|
|
1553
|
+
if (variant?.figma) {
|
|
1554
|
+
return { figmaUrl: variant.figma };
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
if (segment.meta.figma) {
|
|
1558
|
+
return { figmaUrl: segment.meta.figma };
|
|
1559
|
+
}
|
|
1560
|
+
return null;
|
|
1561
|
+
} catch {
|
|
1562
|
+
console.warn(
|
|
1563
|
+
`[${BRAND.name}] ${BRAND.outFile} not found, run '${BRAND.cliCommand} build' first`
|
|
1564
|
+
);
|
|
1565
|
+
return null;
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
async function compareImages(image1Base64, image2Base64, threshold) {
|
|
1569
|
+
const { DiffEngine, base64UrlToBuffer, bufferToBase64Url } = await import("./service-T2L7VLTE.js");
|
|
1570
|
+
const { PNG } = await import("pngjs");
|
|
1571
|
+
const buffer1 = base64UrlToBuffer(image1Base64);
|
|
1572
|
+
const buffer2 = base64UrlToBuffer(image2Base64);
|
|
1573
|
+
const png1 = PNG.sync.read(buffer1);
|
|
1574
|
+
const png2 = PNG.sync.read(buffer2);
|
|
1575
|
+
let finalBuffer1 = buffer1;
|
|
1576
|
+
let finalBuffer2 = buffer2;
|
|
1577
|
+
if (png1.width !== png2.width || png1.height !== png2.height) {
|
|
1578
|
+
const targetWidth = Math.max(png1.width, png2.width);
|
|
1579
|
+
const targetHeight = Math.max(png1.height, png2.height);
|
|
1580
|
+
if (png1.width !== targetWidth || png1.height !== targetHeight) {
|
|
1581
|
+
finalBuffer1 = await resizePng(
|
|
1582
|
+
buffer1,
|
|
1583
|
+
png1.width,
|
|
1584
|
+
png1.height,
|
|
1585
|
+
targetWidth,
|
|
1586
|
+
targetHeight
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
if (png2.width !== targetWidth || png2.height !== targetHeight) {
|
|
1590
|
+
finalBuffer2 = await resizePng(
|
|
1591
|
+
buffer2,
|
|
1592
|
+
png2.width,
|
|
1593
|
+
png2.height,
|
|
1594
|
+
targetWidth,
|
|
1595
|
+
targetHeight
|
|
1596
|
+
);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
const screenshot1 = {
|
|
1600
|
+
data: finalBuffer1,
|
|
1601
|
+
hash: "",
|
|
1602
|
+
viewport: { width: png1.width, height: png1.height },
|
|
1603
|
+
capturedAt: /* @__PURE__ */ new Date(),
|
|
1604
|
+
metadata: {
|
|
1605
|
+
component: "",
|
|
1606
|
+
variant: "",
|
|
1607
|
+
theme: "light",
|
|
1608
|
+
renderTimeMs: 0,
|
|
1609
|
+
captureTimeMs: 0
|
|
1610
|
+
}
|
|
1611
|
+
};
|
|
1612
|
+
const screenshot2 = {
|
|
1613
|
+
data: finalBuffer2,
|
|
1614
|
+
hash: "",
|
|
1615
|
+
viewport: { width: png2.width, height: png2.height },
|
|
1616
|
+
capturedAt: /* @__PURE__ */ new Date(),
|
|
1617
|
+
metadata: {
|
|
1618
|
+
component: "",
|
|
1619
|
+
variant: "",
|
|
1620
|
+
theme: "light",
|
|
1621
|
+
renderTimeMs: 0,
|
|
1622
|
+
captureTimeMs: 0
|
|
1623
|
+
}
|
|
1624
|
+
};
|
|
1625
|
+
const diffEngine = new DiffEngine(threshold);
|
|
1626
|
+
const result = diffEngine.compare(screenshot1, screenshot2, { threshold });
|
|
1627
|
+
return {
|
|
1628
|
+
matches: result.matches,
|
|
1629
|
+
diffPercentage: result.diffPercentage,
|
|
1630
|
+
diffImage: result.diffImage ? bufferToBase64Url(result.diffImage) : void 0,
|
|
1631
|
+
changedRegions: result.changedRegions
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
async function resizePng(buffer, srcWidth, srcHeight, targetWidth, targetHeight) {
|
|
1635
|
+
const { PNG } = await import("pngjs");
|
|
1636
|
+
const srcPng = PNG.sync.read(buffer);
|
|
1637
|
+
const dstPng = new PNG({
|
|
1638
|
+
width: targetWidth,
|
|
1639
|
+
height: targetHeight,
|
|
1640
|
+
fill: true
|
|
1641
|
+
});
|
|
1642
|
+
for (let y = 0; y < targetHeight; y++) {
|
|
1643
|
+
for (let x = 0; x < targetWidth; x++) {
|
|
1644
|
+
const idx = (y * targetWidth + x) * 4;
|
|
1645
|
+
dstPng.data[idx] = 255;
|
|
1646
|
+
dstPng.data[idx + 1] = 255;
|
|
1647
|
+
dstPng.data[idx + 2] = 255;
|
|
1648
|
+
dstPng.data[idx + 3] = 255;
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
for (let y = 0; y < srcHeight; y++) {
|
|
1652
|
+
for (let x = 0; x < srcWidth; x++) {
|
|
1653
|
+
const srcIdx = (y * srcWidth + x) * 4;
|
|
1654
|
+
const dstIdx = (y * targetWidth + x) * 4;
|
|
1655
|
+
dstPng.data[dstIdx] = srcPng.data[srcIdx];
|
|
1656
|
+
dstPng.data[dstIdx + 1] = srcPng.data[srcIdx + 1];
|
|
1657
|
+
dstPng.data[dstIdx + 2] = srcPng.data[srcIdx + 2];
|
|
1658
|
+
dstPng.data[dstIdx + 3] = srcPng.data[srcIdx + 3];
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
return PNG.sync.write(dstPng);
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
// src/viewer/server.ts
|
|
1665
|
+
var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
1666
|
+
var cliPackageRoot = resolve2(__dirname2, "..");
|
|
1667
|
+
var viewerRoot = resolve2(cliPackageRoot, "src/viewer");
|
|
1668
|
+
var packagesRoot = resolve2(cliPackageRoot, "..");
|
|
1669
|
+
var uiLibRoot = resolve2(packagesRoot, "../libs/ui/src");
|
|
1670
|
+
async function createDevServer(options = {}) {
|
|
1671
|
+
const startTime = performance.now();
|
|
1672
|
+
const {
|
|
1673
|
+
port = 6006,
|
|
1674
|
+
configPath,
|
|
1675
|
+
open = true,
|
|
1676
|
+
projectRoot = process.cwd()
|
|
1677
|
+
} = options;
|
|
1678
|
+
console.log("\n\u{1F527} Loading configuration...");
|
|
1679
|
+
const { config, configDir } = await loadConfig(configPath);
|
|
1680
|
+
const segmentFiles = await discoverSegmentFiles(config, configDir);
|
|
1681
|
+
const installedFiles = await discoverInstalledFragments(projectRoot);
|
|
1682
|
+
const allSegmentFiles = [...segmentFiles, ...installedFiles];
|
|
1683
|
+
console.log(`\u{1F4E6} Found ${segmentFiles.length} local + ${installedFiles.length} installed fragment file(s)`);
|
|
1684
|
+
let projectViteConfig = {};
|
|
1685
|
+
const viteConfigPath = findViteConfig(projectRoot);
|
|
1686
|
+
if (viteConfigPath) {
|
|
1687
|
+
console.log(`\u{1F4C4} Using project Vite config: ${viteConfigPath}`);
|
|
1688
|
+
try {
|
|
1689
|
+
const loaded = await loadConfigFromFile(
|
|
1690
|
+
{ command: "serve", mode: "development" },
|
|
1691
|
+
viteConfigPath,
|
|
1692
|
+
projectRoot
|
|
1693
|
+
);
|
|
1694
|
+
if (loaded) {
|
|
1695
|
+
projectViteConfig = loaded.config;
|
|
1696
|
+
}
|
|
1697
|
+
} catch (error) {
|
|
1698
|
+
console.warn("\u26A0\uFE0F Could not load project Vite config:", error);
|
|
1699
|
+
}
|
|
1700
|
+
} else {
|
|
1701
|
+
console.log("\u2139\uFE0F No project Vite config found, using defaults");
|
|
1702
|
+
}
|
|
1703
|
+
const nodeModulesPath = findNodeModules(projectRoot);
|
|
1704
|
+
console.log(`\u{1F4C1} Using node_modules: ${nodeModulesPath}`);
|
|
1705
|
+
const installedPkgRoots = [...new Set(
|
|
1706
|
+
installedFiles.map((f) => {
|
|
1707
|
+
const idx = f.absolutePath.indexOf("/node_modules/");
|
|
1708
|
+
if (idx === -1) return dirname2(f.absolutePath);
|
|
1709
|
+
const afterNm = f.absolutePath.slice(idx + "/node_modules/".length);
|
|
1710
|
+
const pkgName = afterNm.startsWith("@") ? afterNm.split("/").slice(0, 2).join("/") : afterNm.split("/")[0];
|
|
1711
|
+
return resolve2(projectRoot, "node_modules", pkgName);
|
|
1712
|
+
})
|
|
1713
|
+
)];
|
|
1714
|
+
const segmentsConfig = {
|
|
1715
|
+
configFile: false,
|
|
1716
|
+
// Don't load config again
|
|
1717
|
+
root: projectRoot,
|
|
1718
|
+
// Run from PROJECT root
|
|
1719
|
+
base: "/",
|
|
1720
|
+
server: {
|
|
1721
|
+
port,
|
|
1722
|
+
open: open ? "/fragments/" : false,
|
|
1723
|
+
fs: {
|
|
1724
|
+
// Allow serving files from viewer package, project, UI library, and node_modules root
|
|
1725
|
+
allow: [viewerRoot, uiLibRoot, projectRoot, configDir, dirname2(nodeModulesPath), ...installedPkgRoots]
|
|
1726
|
+
}
|
|
1727
|
+
},
|
|
1728
|
+
plugins: [
|
|
1729
|
+
// React support (if not already in project config)
|
|
1730
|
+
...hasReactPlugin(projectViteConfig) ? [] : [react()],
|
|
1731
|
+
// Segments plugins (array including SVGR)
|
|
1732
|
+
...segmentsPlugin({
|
|
1733
|
+
segmentFiles: allSegmentFiles,
|
|
1734
|
+
config,
|
|
1735
|
+
projectRoot
|
|
1736
|
+
})
|
|
1737
|
+
],
|
|
1738
|
+
// CSS configuration
|
|
1739
|
+
css: {},
|
|
1740
|
+
optimizeDeps: {
|
|
1741
|
+
// Include common dependencies for faster startup
|
|
1742
|
+
include: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime"]
|
|
1743
|
+
},
|
|
1744
|
+
// Ensure we can resolve viewer's dependencies
|
|
1745
|
+
resolve: {
|
|
1746
|
+
// Dedupe ensures all imports of these packages resolve to the same copy
|
|
1747
|
+
dedupe: ["react", "react-dom"],
|
|
1748
|
+
alias: {
|
|
1749
|
+
// Allow importing from viewer package
|
|
1750
|
+
"@fragments/viewer": viewerRoot,
|
|
1751
|
+
// Resolve @fragments/ui to the UI library source for dogfooding
|
|
1752
|
+
"@fragments/ui": resolve2(uiLibRoot, "index.ts"),
|
|
1753
|
+
// Resolve @fragments/core to the consolidated core source
|
|
1754
|
+
"@fragments/core": resolve2(cliPackageRoot, "src/core/index.ts"),
|
|
1755
|
+
// Ensure ALL react imports resolve to project's node_modules
|
|
1756
|
+
// This is critical for viewer files loaded from outside project root
|
|
1757
|
+
"react": safeRealpath(join(nodeModulesPath, "react")),
|
|
1758
|
+
"react-dom": safeRealpath(join(nodeModulesPath, "react-dom")),
|
|
1759
|
+
"react/jsx-runtime": safeRealpath(join(nodeModulesPath, "react/jsx-runtime")),
|
|
1760
|
+
"react/jsx-dev-runtime": safeRealpath(join(nodeModulesPath, "react/jsx-dev-runtime"))
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
};
|
|
1764
|
+
const mergedConfig = mergeConfig(projectViteConfig, segmentsConfig);
|
|
1765
|
+
console.log("\u{1F680} Starting dev server...\n");
|
|
1766
|
+
const server = await createServer(mergedConfig);
|
|
1767
|
+
await server.listen();
|
|
1768
|
+
const startupTime = ((performance.now() - startTime) / 1e3).toFixed(2);
|
|
1769
|
+
console.log(`\u26A1 Server ready in ${startupTime}s`);
|
|
1770
|
+
return server;
|
|
1771
|
+
}
|
|
1772
|
+
function safeRealpath(p) {
|
|
1773
|
+
try {
|
|
1774
|
+
return realpathSync(p);
|
|
1775
|
+
} catch {
|
|
1776
|
+
return p;
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
function findNodeModules(startDir) {
|
|
1780
|
+
let current = startDir;
|
|
1781
|
+
while (current !== dirname2(current)) {
|
|
1782
|
+
const nodeModulesPath = join(current, "node_modules");
|
|
1783
|
+
if (existsSync(join(nodeModulesPath, "react"))) {
|
|
1784
|
+
return nodeModulesPath;
|
|
1785
|
+
}
|
|
1786
|
+
current = dirname2(current);
|
|
1787
|
+
}
|
|
1788
|
+
return join(startDir, "node_modules");
|
|
1789
|
+
}
|
|
1790
|
+
function findViteConfig(projectRoot) {
|
|
1791
|
+
const configFiles = [
|
|
1792
|
+
"vite.config.ts",
|
|
1793
|
+
"vite.config.js",
|
|
1794
|
+
"vite.config.mts",
|
|
1795
|
+
"vite.config.mjs"
|
|
1796
|
+
];
|
|
1797
|
+
for (const file of configFiles) {
|
|
1798
|
+
const path = join(projectRoot, file);
|
|
1799
|
+
if (existsSync(path)) {
|
|
1800
|
+
return path;
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
return null;
|
|
1804
|
+
}
|
|
1805
|
+
function hasReactPlugin(config) {
|
|
1806
|
+
if (!config.plugins) return false;
|
|
1807
|
+
const plugins = Array.isArray(config.plugins) ? config.plugins : [config.plugins];
|
|
1808
|
+
return plugins.some((plugin) => {
|
|
1809
|
+
if (!plugin) return false;
|
|
1810
|
+
if (Array.isArray(plugin)) {
|
|
1811
|
+
return plugin.some(
|
|
1812
|
+
(p) => p && typeof p === "object" && "name" in p && p.name?.includes("react")
|
|
1813
|
+
);
|
|
1814
|
+
}
|
|
1815
|
+
return typeof plugin === "object" && "name" in plugin && plugin.name?.includes("react");
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
export {
|
|
1819
|
+
createDevServer,
|
|
1820
|
+
segmentsPlugin
|
|
1821
|
+
};
|
|
1822
|
+
//# sourceMappingURL=viewer-SUFOISZM.js.map
|