@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.
Files changed (124) hide show
  1. package/dist/bin.js +996 -79
  2. package/dist/bin.js.map +1 -1
  3. package/dist/{chunk-ICAIQ57V.js → chunk-6JBGU74P.js} +5 -3
  4. package/dist/chunk-6JBGU74P.js.map +1 -0
  5. package/dist/chunk-7OPWMLOE.js +1625 -0
  6. package/dist/chunk-7OPWMLOE.js.map +1 -0
  7. package/dist/{chunk-2H2JAA3U.js → chunk-CVXKXVOY.js} +3 -3
  8. package/dist/{chunk-2H2JAA3U.js.map → chunk-CVXKXVOY.js.map} +1 -1
  9. package/dist/{chunk-IOJE35DZ.js → chunk-NWQ4CJOQ.js} +3 -3
  10. package/dist/{chunk-2DJH4F4P.js → chunk-RVRTRESS.js} +3 -3
  11. package/dist/{chunk-V7YLRR4C.js → chunk-TJ34N7C7.js} +41 -4
  12. package/dist/{chunk-V7YLRR4C.js.map → chunk-TJ34N7C7.js.map} +1 -1
  13. package/dist/{chunk-XNWDI6UT.js → chunk-XHUDJNN3.js} +5 -5
  14. package/dist/{core-DKHB7FYV.js → core-W2HYIQW6.js} +4 -4
  15. package/dist/{generate-KL24VZVD.js → generate-LMTISDIJ.js} +5 -5
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +15 -7
  18. package/dist/index.js.map +1 -1
  19. package/dist/{init-NION5S3M.js → init-7CHRKQ7P.js} +5 -5
  20. package/dist/mcp-bin.js +8 -220
  21. package/dist/mcp-bin.js.map +1 -1
  22. package/dist/scan-WY23TJCP.js +12 -0
  23. package/dist/{service-RWUMZ3EW.js → service-T2L7VLTE.js} +5 -5
  24. package/dist/static-viewer-GBR7YNF3.js +12 -0
  25. package/dist/{test-ECPEXFDN.js → test-OJRXNDO2.js} +4 -4
  26. package/dist/{tokens-ITADYVPF.js → tokens-3BWDESVM.js} +6 -6
  27. package/dist/viewer-SUFOISZM.js +1822 -0
  28. package/dist/viewer-SUFOISZM.js.map +1 -0
  29. package/package.json +6 -5
  30. package/src/bin.ts +31 -0
  31. package/src/build.ts +147 -13
  32. package/src/cli-commands.ts +18 -0
  33. package/src/commands/__tests__/a11y-scoring.test.ts +278 -0
  34. package/src/commands/a11y-report.ts +625 -0
  35. package/src/commands/a11y.ts +168 -14
  36. package/src/commands/build.ts +16 -0
  37. package/src/commands/graph.ts +274 -0
  38. package/src/core/auto-props.ts +464 -0
  39. package/src/core/composition.ts +64 -1
  40. package/src/core/graph-extractor.test.ts +542 -0
  41. package/src/core/graph-extractor.ts +601 -0
  42. package/src/core/importAnalyzer.ts +5 -0
  43. package/src/core/schema.ts +2 -0
  44. package/src/core/types.ts +3 -1
  45. package/src/index.ts +4 -0
  46. package/src/mcp/server.ts +13 -220
  47. package/src/theme/__tests__/component-contrast.test.ts +338 -0
  48. package/src/theme/__tests__/contrast-validation.test.ts +326 -0
  49. package/src/theme/contrast.test.ts +331 -0
  50. package/src/theme/contrast.ts +246 -0
  51. package/src/theme/generator.ts +213 -1
  52. package/src/theme/index.ts +16 -0
  53. package/src/theme/types.ts +51 -0
  54. package/src/viewer/__tests__/a11y-fixes.test.ts +358 -0
  55. package/src/viewer/__tests__/viewer-integration.test.ts +2 -7
  56. package/src/viewer/components/AccessibilityPanel.tsx +493 -433
  57. package/src/viewer/components/ActionCapture.tsx +1 -1
  58. package/src/viewer/components/ActionsPanel.tsx +142 -183
  59. package/src/viewer/components/App.tsx +276 -183
  60. package/src/viewer/components/BottomPanel.tsx +40 -80
  61. package/src/viewer/components/CodePanel.tsx +9 -87
  62. package/src/viewer/components/CommandPalette.tsx +117 -74
  63. package/src/viewer/components/ComponentGraph.tsx +143 -126
  64. package/src/viewer/components/ComponentHeader.tsx +46 -43
  65. package/src/viewer/components/ContractPanel.tsx +124 -117
  66. package/src/viewer/components/ErrorBoundary.tsx +47 -35
  67. package/src/viewer/components/FigmaEmbed.tsx +18 -13
  68. package/src/viewer/components/FragmentEditor.tsx +126 -63
  69. package/src/viewer/components/HealthDashboard.tsx +146 -171
  70. package/src/viewer/components/HmrStatusIndicator.tsx +31 -41
  71. package/src/viewer/components/Icons.tsx +151 -98
  72. package/src/viewer/components/InteractionsPanel.tsx +317 -264
  73. package/src/viewer/components/IsolatedPreviewFrame.tsx +52 -27
  74. package/src/viewer/components/IsolatedRender.tsx +12 -6
  75. package/src/viewer/components/KeyboardShortcutsHelp.tsx +34 -70
  76. package/src/viewer/components/LandingPage.tsx +285 -305
  77. package/src/viewer/components/Layout.tsx +12 -10
  78. package/src/viewer/components/LeftSidebar.tsx +103 -155
  79. package/src/viewer/components/MultiViewportPreview.tsx +254 -63
  80. package/src/viewer/components/PreviewArea.tsx +113 -44
  81. package/src/viewer/components/PreviewFrameHost.tsx +36 -6
  82. package/src/viewer/components/PreviewPane.tsx +2 -3
  83. package/src/viewer/components/PreviewToolbar.tsx +109 -105
  84. package/src/viewer/components/PropsEditor.tsx +154 -74
  85. package/src/viewer/components/PropsTable.tsx +95 -82
  86. package/src/viewer/components/RelationsSection.tsx +71 -40
  87. package/src/viewer/components/ResizablePanel.tsx +158 -55
  88. package/src/viewer/components/RightSidebar.tsx +46 -56
  89. package/src/viewer/components/ScreenshotButton.tsx +12 -12
  90. package/src/viewer/components/SkeletonLoader.tsx +99 -83
  91. package/src/viewer/components/StoryRenderer.tsx +4 -11
  92. package/src/viewer/components/Toast.tsx +3 -67
  93. package/src/viewer/components/TokenStylePanel.tsx +136 -118
  94. package/src/viewer/components/UsageSection.tsx +26 -26
  95. package/src/viewer/components/VariantMatrix.tsx +140 -47
  96. package/src/viewer/components/VariantTabs.tsx +24 -68
  97. package/src/viewer/components/ViewportSelector.tsx +121 -114
  98. package/src/viewer/constants/ui.ts +23 -22
  99. package/src/viewer/entry.tsx +8 -3
  100. package/src/viewer/index.ts +3 -6
  101. package/src/viewer/preview-frame.html +43 -18
  102. package/src/viewer/server.ts +7 -16
  103. package/src/viewer/styles/globals.css +46 -85
  104. package/src/viewer/utils/a11y-fixes.ts +53 -30
  105. package/dist/chunk-ICAIQ57V.js.map +0 -1
  106. package/dist/chunk-U4GQ2JTD.js +0 -832
  107. package/dist/chunk-U4GQ2JTD.js.map +0 -1
  108. package/dist/scan-ESEXV7LF.js +0 -12
  109. package/dist/static-viewer-O37MJ5B6.js +0 -12
  110. package/dist/viewer-YDGFDTK5.js +0 -11104
  111. package/dist/viewer-YDGFDTK5.js.map +0 -1
  112. package/src/viewer/postcss.config.js +0 -6
  113. package/src/viewer/tailwind.config.js +0 -37
  114. /package/dist/{chunk-IOJE35DZ.js.map → chunk-NWQ4CJOQ.js.map} +0 -0
  115. /package/dist/{chunk-2DJH4F4P.js.map → chunk-RVRTRESS.js.map} +0 -0
  116. /package/dist/{chunk-XNWDI6UT.js.map → chunk-XHUDJNN3.js.map} +0 -0
  117. /package/dist/{core-DKHB7FYV.js.map → core-W2HYIQW6.js.map} +0 -0
  118. /package/dist/{generate-KL24VZVD.js.map → generate-LMTISDIJ.js.map} +0 -0
  119. /package/dist/{init-NION5S3M.js.map → init-7CHRKQ7P.js.map} +0 -0
  120. /package/dist/{scan-ESEXV7LF.js.map → scan-WY23TJCP.js.map} +0 -0
  121. /package/dist/{service-RWUMZ3EW.js.map → service-T2L7VLTE.js.map} +0 -0
  122. /package/dist/{static-viewer-O37MJ5B6.js.map → static-viewer-GBR7YNF3.js.map} +0 -0
  123. /package/dist/{test-ECPEXFDN.js.map → test-OJRXNDO2.js.map} +0 -0
  124. /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