@fragments-sdk/cli 0.14.3 → 0.15.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/README.md +0 -3
- package/dist/bin.js +4290 -3754
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-TXFCEDOC.js → chunk-2WXKALIG.js} +2 -2
- package/dist/{chunk-I34BC3CU.js → chunk-32LIWN2P.js} +1006 -3
- package/dist/chunk-32LIWN2P.js.map +1 -0
- package/dist/{chunk-55KERLWL.js → chunk-65WSVDV5.js} +314 -89
- package/dist/chunk-65WSVDV5.js.map +1 -0
- package/dist/chunk-7DZC4YEV.js +294 -0
- package/dist/chunk-7DZC4YEV.js.map +1 -0
- package/dist/{chunk-LOYS64QS.js → chunk-7WHVW72L.js} +230 -19
- package/dist/chunk-7WHVW72L.js.map +1 -0
- package/dist/{chunk-PJT5IZ37.js → chunk-BJE3425I.js} +19 -52
- package/dist/{chunk-PJT5IZ37.js.map → chunk-BJE3425I.js.map} +1 -1
- package/dist/{chunk-5A6X2Y73.js → chunk-CZD3AD4Q.js} +12 -11
- package/dist/chunk-CZD3AD4Q.js.map +1 -0
- package/dist/{chunk-EYXVAMEX.js → chunk-MN3TJ3D5.js} +72 -3
- package/dist/chunk-MN3TJ3D5.js.map +1 -0
- package/dist/chunk-QCN35LJU.js +630 -0
- package/dist/chunk-QCN35LJU.js.map +1 -0
- package/dist/chunk-T47OLCSF.js +36 -0
- package/dist/chunk-T47OLCSF.js.map +1 -0
- package/dist/{chunk-APTQIBS5.js → chunk-XJQ5BIWI.js} +144 -1049
- package/dist/chunk-XJQ5BIWI.js.map +1 -0
- package/dist/codebase-scanner-VOTPXRYW.js +22 -0
- package/dist/converter-JLINP7CJ.js +34 -0
- package/dist/converter-JLINP7CJ.js.map +1 -0
- package/dist/core/index.js +43 -1
- package/dist/{generate-RYWIPDN2.js → generate-A4FP5426.js} +3 -4
- package/dist/{generate-RYWIPDN2.js.map → generate-A4FP5426.js.map} +1 -1
- package/dist/govern-scan-UCBZR6D6.js +280 -0
- package/dist/govern-scan-UCBZR6D6.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +11 -11
- package/dist/{init-WRUSW7R5.js → init-HGSM35XA.js} +131 -128
- package/dist/init-HGSM35XA.js.map +1 -0
- package/dist/{init-cloud-REQ3XLHO.js → init-cloud-MQ6GRJAZ.js} +2 -2
- package/dist/mcp-bin.js +5 -36
- package/dist/mcp-bin.js.map +1 -1
- package/dist/scan-VNNKACG2.js +15 -0
- package/dist/{scan-generate-TFZVL3BT.js → scan-generate-TWRHNU5M.js} +335 -46
- package/dist/scan-generate-TWRHNU5M.js.map +1 -0
- package/dist/scanner-7LAZYPWZ.js +13 -0
- package/dist/{service-HKJ6B7P7.js → service-FHQU7YS7.js} +27 -23
- package/dist/{snapshot-C5DYIGIV.js → snapshot-KQEQ6XHL.js} +2 -2
- package/dist/{static-viewer-DUVC4UIM.js → static-viewer-63PG6FWY.js} +3 -3
- package/dist/static-viewer-63PG6FWY.js.map +1 -0
- package/dist/{test-JW7JIDFG.js → test-UQYUCZIS.js} +4 -6
- package/dist/{test-JW7JIDFG.js.map → test-UQYUCZIS.js.map} +1 -1
- package/dist/{tokens-KE73G5JC.js → tokens-6GYKDV6U.js} +6 -5
- package/dist/{tokens-KE73G5JC.js.map → tokens-6GYKDV6U.js.map} +1 -1
- package/dist/tokens-generate-VTZV5EEW.js +86 -0
- package/dist/tokens-generate-VTZV5EEW.js.map +1 -0
- package/package.json +6 -6
- package/src/bin.ts +210 -48
- package/src/build.ts +130 -6
- package/src/commands/__fixtures__/shadcn-label-wrapper/package.json +7 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.contract.json +42 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.tsx +11 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.contract.json +20 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.tsx +14 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/tsconfig.app.json +23 -0
- package/src/commands/__tests__/init.test.ts +113 -0
- package/src/commands/__tests__/scan-generate.test.ts +188 -69
- package/src/commands/__tests__/verify.test.ts +91 -0
- package/src/commands/discover.ts +151 -0
- package/src/commands/enhance.ts +3 -1
- package/src/commands/govern-scan.ts +386 -0
- package/src/commands/govern.ts +2 -2
- package/src/commands/init.ts +152 -28
- package/src/commands/inspect.ts +290 -0
- package/src/commands/migrate-contract.ts +85 -0
- package/src/commands/scan-generate.ts +438 -50
- package/src/commands/scan.ts +1 -0
- package/src/commands/setup.ts +27 -50
- package/src/commands/tokens-generate.ts +113 -0
- package/src/commands/verify.ts +195 -1
- package/src/core/__fixtures__/shadcn-input/input.tsx +7 -0
- package/src/core/__fixtures__/shadcn-input/tsconfig.json +14 -0
- package/src/core/__fixtures__/shadcn-label/label.tsx +11 -0
- package/src/core/__fixtures__/shadcn-label/primitive.tsx +14 -0
- package/src/core/__fixtures__/shadcn-label/tsconfig.json +14 -0
- package/src/core/__fixtures__/shadcn-radix-label/label.tsx +11 -0
- package/src/core/__fixtures__/shadcn-radix-label/node_modules/radix-ui/index.d.ts +12 -0
- package/src/core/__fixtures__/shadcn-radix-label/tsconfig.json +14 -0
- package/src/core/__tests__/contract-parity.test.ts +316 -0
- package/src/core/component-extractor.test.ts +39 -0
- package/src/core/component-extractor.ts +92 -1
- package/src/core/config.ts +2 -1
- package/src/core/discovery.ts +13 -2
- package/src/core/drift-verifier.ts +123 -0
- package/src/core/extractor-adapter.ts +80 -0
- package/src/mcp/__tests__/projectFields.test.ts +1 -1
- package/src/mcp/utils.ts +1 -50
- package/src/migrate/converter.ts +3 -3
- package/src/migrate/fragment-to-contract.ts +253 -0
- package/src/migrate/report.ts +1 -1
- package/src/scripts/token-benchmark.ts +121 -0
- package/src/service/__tests__/props-extractor.test.ts +94 -0
- package/src/service/__tests__/token-normalizer.test.ts +690 -0
- package/src/service/ast-utils.ts +4 -23
- package/src/service/babel-config.ts +23 -0
- package/src/service/enhance/converter.ts +61 -0
- package/src/service/enhance/props-extractor.ts +25 -8
- package/src/service/enhance/scanner.ts +5 -24
- package/src/service/snippet-validation.ts +9 -3
- package/src/service/token-normalizer.ts +510 -0
- package/src/shared/index.ts +1 -0
- package/src/shared/project-fields.ts +46 -0
- package/src/viewer/__tests__/viewer-integration.test.ts +8 -8
- package/src/viewer/preview-adapter.ts +116 -0
- package/src/viewer/style-utils.ts +27 -412
- package/src/viewer/vite-plugin.ts +2 -2
- package/dist/chunk-55KERLWL.js.map +0 -1
- package/dist/chunk-5A6X2Y73.js.map +0 -1
- package/dist/chunk-APTQIBS5.js.map +0 -1
- package/dist/chunk-EYXVAMEX.js.map +0 -1
- package/dist/chunk-I34BC3CU.js.map +0 -1
- package/dist/chunk-LOYS64QS.js.map +0 -1
- package/dist/chunk-ZKTFKHWN.js +0 -324
- package/dist/chunk-ZKTFKHWN.js.map +0 -1
- package/dist/discovery-VDANZAJ2.js +0 -28
- package/dist/init-WRUSW7R5.js.map +0 -1
- package/dist/scan-YJHQIRKG.js +0 -14
- package/dist/scan-generate-TFZVL3BT.js.map +0 -1
- package/dist/viewer-2TZS3NDL.js +0 -2730
- package/dist/viewer-2TZS3NDL.js.map +0 -1
- package/src/commands/dev.ts +0 -107
- /package/dist/{chunk-TXFCEDOC.js.map → chunk-2WXKALIG.js.map} +0 -0
- /package/dist/{discovery-VDANZAJ2.js.map → codebase-scanner-VOTPXRYW.js.map} +0 -0
- /package/dist/{init-cloud-REQ3XLHO.js.map → init-cloud-MQ6GRJAZ.js.map} +0 -0
- /package/dist/{scan-YJHQIRKG.js.map → scan-VNNKACG2.js.map} +0 -0
- /package/dist/{service-HKJ6B7P7.js.map → scanner-7LAZYPWZ.js.map} +0 -0
- /package/dist/{static-viewer-DUVC4UIM.js.map → service-FHQU7YS7.js.map} +0 -0
- /package/dist/{snapshot-C5DYIGIV.js.map → snapshot-KQEQ6XHL.js.map} +0 -0
package/dist/viewer-2TZS3NDL.js
DELETED
|
@@ -1,2730 +0,0 @@
|
|
|
1
|
-
import { createRequire as __banner_createRequire } from 'module'; const require = __banner_createRequire(import.meta.url);
|
|
2
|
-
import {
|
|
3
|
-
findPreviewConfigPath,
|
|
4
|
-
findStorybookDir,
|
|
5
|
-
generatePreviewModule,
|
|
6
|
-
loadConfig,
|
|
7
|
-
parseFragmentFile
|
|
8
|
-
} from "./chunk-55KERLWL.js";
|
|
9
|
-
import {
|
|
10
|
-
discoverFragmentFiles,
|
|
11
|
-
discoverInstalledFragments
|
|
12
|
-
} from "./chunk-ZKTFKHWN.js";
|
|
13
|
-
import "./chunk-D2CDBRNU.js";
|
|
14
|
-
import {
|
|
15
|
-
BRAND,
|
|
16
|
-
generateContext
|
|
17
|
-
} from "./chunk-I34BC3CU.js";
|
|
18
|
-
import "./chunk-Z7EY4VHE.js";
|
|
19
|
-
|
|
20
|
-
// src/viewer/server.ts
|
|
21
|
-
import {
|
|
22
|
-
createServer,
|
|
23
|
-
mergeConfig,
|
|
24
|
-
loadConfigFromFile
|
|
25
|
-
} from "vite";
|
|
26
|
-
import react from "@vitejs/plugin-react";
|
|
27
|
-
import { resolve as resolve2, dirname as dirname2, join } from "path";
|
|
28
|
-
import { existsSync as existsSync2, realpathSync } from "fs";
|
|
29
|
-
import { readFile as readFile2 } from "fs/promises";
|
|
30
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
31
|
-
|
|
32
|
-
// src/viewer/vite-plugin.ts
|
|
33
|
-
import { resolve, dirname } from "path";
|
|
34
|
-
import { fileURLToPath } from "url";
|
|
35
|
-
import { existsSync } from "fs";
|
|
36
|
-
import { readFile } from "fs/promises";
|
|
37
|
-
import { transform } from "esbuild";
|
|
38
|
-
import svgr from "vite-plugin-svgr";
|
|
39
|
-
|
|
40
|
-
// src/viewer/render-utils.ts
|
|
41
|
-
function serializeValue(value) {
|
|
42
|
-
if (value === null) return "null";
|
|
43
|
-
if (value === void 0) return "undefined";
|
|
44
|
-
if (typeof value === "string") return JSON.stringify(value);
|
|
45
|
-
if (typeof value === "number") return String(value);
|
|
46
|
-
if (typeof value === "boolean") return String(value);
|
|
47
|
-
if (Array.isArray(value)) {
|
|
48
|
-
return `[${value.map(serializeValue).join(", ")}]`;
|
|
49
|
-
}
|
|
50
|
-
if (typeof value === "object") {
|
|
51
|
-
const entries = Object.entries(value).map(([k, v]) => `${JSON.stringify(k)}: ${serializeValue(v)}`).join(", ");
|
|
52
|
-
return `{${entries}}`;
|
|
53
|
-
}
|
|
54
|
-
return "undefined";
|
|
55
|
-
}
|
|
56
|
-
function serializePropsToJsx(props) {
|
|
57
|
-
return Object.entries(props).filter(([_, v]) => v !== void 0).map(([key, value]) => {
|
|
58
|
-
if (typeof value === "string") {
|
|
59
|
-
return `${key}=${JSON.stringify(value)}`;
|
|
60
|
-
}
|
|
61
|
-
return `${key}={${serializeValue(value)}}`;
|
|
62
|
-
}).join(" ");
|
|
63
|
-
}
|
|
64
|
-
function findFragmentByName(componentName, fragments) {
|
|
65
|
-
const match = fragments.find(
|
|
66
|
-
(s) => s.fragment.meta.name.toLowerCase() === componentName.toLowerCase()
|
|
67
|
-
);
|
|
68
|
-
if (!match) return null;
|
|
69
|
-
return {
|
|
70
|
-
name: match.fragment.meta.name,
|
|
71
|
-
path: match.path
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
function getAvailableComponents(fragments) {
|
|
75
|
-
return fragments.map((s) => s.fragment.meta.name).sort();
|
|
76
|
-
}
|
|
77
|
-
function generateRenderScript(fragmentPath, componentName, props = {}) {
|
|
78
|
-
const propsJsx = serializePropsToJsx(props);
|
|
79
|
-
const propsString = propsJsx ? ` ${propsJsx}` : "";
|
|
80
|
-
const hasChildren = "children" in props && props.children !== void 0;
|
|
81
|
-
const childrenContent = hasChildren ? String(props.children) : "";
|
|
82
|
-
const propsWithoutChildren = { ...props };
|
|
83
|
-
delete propsWithoutChildren.children;
|
|
84
|
-
const propsJsxNoChildren = serializePropsToJsx(propsWithoutChildren);
|
|
85
|
-
const propsStringNoChildren = propsJsxNoChildren ? ` ${propsJsxNoChildren}` : "";
|
|
86
|
-
return `
|
|
87
|
-
import React from "react";
|
|
88
|
-
import { createRoot } from "react-dom/client";
|
|
89
|
-
|
|
90
|
-
// Import the fragment to get the component
|
|
91
|
-
async function render() {
|
|
92
|
-
const root = document.getElementById("render-root");
|
|
93
|
-
|
|
94
|
-
try {
|
|
95
|
-
// Dynamic import of the fragment file
|
|
96
|
-
const fragmentModule = await import("${fragmentPath}");
|
|
97
|
-
const fragment = fragmentModule.default;
|
|
98
|
-
|
|
99
|
-
if (!fragment || !fragment.component) {
|
|
100
|
-
throw new Error("Fragment does not export a component");
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const Component = fragment.component;
|
|
104
|
-
|
|
105
|
-
// Create React root and render
|
|
106
|
-
const reactRoot = createRoot(root);
|
|
107
|
-
${hasChildren ? `reactRoot.render(React.createElement(Component, ${JSON.stringify(propsWithoutChildren)}, ${JSON.stringify(childrenContent)}));` : `reactRoot.render(React.createElement(Component, ${JSON.stringify(props)}));`}
|
|
108
|
-
|
|
109
|
-
// Signal that rendering is complete
|
|
110
|
-
// Wait a frame for React to flush
|
|
111
|
-
requestAnimationFrame(() => {
|
|
112
|
-
requestAnimationFrame(() => {
|
|
113
|
-
root.classList.add("ready");
|
|
114
|
-
window.__RENDER_READY__ = true;
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
} catch (error) {
|
|
118
|
-
console.error("Render error:", error);
|
|
119
|
-
root.innerHTML = \`
|
|
120
|
-
<div class="render-error">
|
|
121
|
-
<strong>Render Error</strong>
|
|
122
|
-
<pre>\${error.message}</pre>
|
|
123
|
-
</div>
|
|
124
|
-
\`;
|
|
125
|
-
root.classList.add("ready");
|
|
126
|
-
window.__RENDER_READY__ = true;
|
|
127
|
-
window.__RENDER_ERROR__ = error.message;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
render();
|
|
132
|
-
`;
|
|
133
|
-
}
|
|
134
|
-
function generateVariantRenderScript(fragmentPath, componentName, variantName) {
|
|
135
|
-
const variantNameLower = JSON.stringify(variantName.toLowerCase());
|
|
136
|
-
return `
|
|
137
|
-
import React from "react";
|
|
138
|
-
import { createRoot } from "react-dom/client";
|
|
139
|
-
|
|
140
|
-
async function render() {
|
|
141
|
-
const root = document.getElementById("render-root");
|
|
142
|
-
|
|
143
|
-
try {
|
|
144
|
-
const fragmentModule = await import("${fragmentPath}");
|
|
145
|
-
const fragment = fragmentModule.default;
|
|
146
|
-
|
|
147
|
-
if (!fragment || !fragment.variants || fragment.variants.length === 0) {
|
|
148
|
-
throw new Error("Fragment has no variants");
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const variant = fragment.variants.find(
|
|
152
|
-
v => v.name.toLowerCase() === ${variantNameLower}
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
if (!variant) {
|
|
156
|
-
const available = fragment.variants.map(v => v.name).join(", ");
|
|
157
|
-
throw new Error("Variant '" + ${JSON.stringify(variantName)} + "' not found. Available: " + available);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const element = variant.render();
|
|
161
|
-
|
|
162
|
-
const reactRoot = createRoot(root);
|
|
163
|
-
reactRoot.render(element);
|
|
164
|
-
|
|
165
|
-
requestAnimationFrame(() => {
|
|
166
|
-
requestAnimationFrame(() => {
|
|
167
|
-
root.classList.add("ready");
|
|
168
|
-
window.__RENDER_READY__ = true;
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
} catch (error) {
|
|
172
|
-
console.error("Render error:", error);
|
|
173
|
-
root.innerHTML = \`
|
|
174
|
-
<div class="render-error">
|
|
175
|
-
<strong>Render Error</strong>
|
|
176
|
-
<pre>\${error.message}</pre>
|
|
177
|
-
</div>
|
|
178
|
-
\`;
|
|
179
|
-
root.classList.add("ready");
|
|
180
|
-
window.__RENDER_READY__ = true;
|
|
181
|
-
window.__RENDER_ERROR__ = error.message;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
render();
|
|
186
|
-
`;
|
|
187
|
-
}
|
|
188
|
-
function generateA11yRenderScript(fragmentPath, componentName, variantName) {
|
|
189
|
-
const variantLookup = variantName ? `
|
|
190
|
-
const variant = fragment.variants?.find(
|
|
191
|
-
v => v.name.toLowerCase() === ${JSON.stringify(variantName.toLowerCase())}
|
|
192
|
-
);
|
|
193
|
-
if (!variant) {
|
|
194
|
-
throw new Error("Variant '${variantName}' not found");
|
|
195
|
-
}
|
|
196
|
-
element = variant.render();` : `
|
|
197
|
-
element = React.createElement(fragment.component, {});`;
|
|
198
|
-
return `
|
|
199
|
-
import React from "react";
|
|
200
|
-
import { createRoot } from "react-dom/client";
|
|
201
|
-
import axe from "axe-core";
|
|
202
|
-
|
|
203
|
-
async function render() {
|
|
204
|
-
const root = document.getElementById("render-root");
|
|
205
|
-
|
|
206
|
-
try {
|
|
207
|
-
const fragmentModule = await import("${fragmentPath}");
|
|
208
|
-
const fragment = fragmentModule.default;
|
|
209
|
-
|
|
210
|
-
if (!fragment || !fragment.component) {
|
|
211
|
-
throw new Error("Fragment does not export a component");
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
let element;
|
|
215
|
-
${variantLookup}
|
|
216
|
-
|
|
217
|
-
const reactRoot = createRoot(root);
|
|
218
|
-
reactRoot.render(element);
|
|
219
|
-
|
|
220
|
-
// Wait for React to flush rendering
|
|
221
|
-
await new Promise(resolve => {
|
|
222
|
-
requestAnimationFrame(() => {
|
|
223
|
-
requestAnimationFrame(resolve);
|
|
224
|
-
});
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
// Additional settle time for CSS/animations
|
|
228
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
229
|
-
|
|
230
|
-
// Run axe-core accessibility audit
|
|
231
|
-
const results = await axe.run('#render-root', {
|
|
232
|
-
runOnly: {
|
|
233
|
-
type: 'tag',
|
|
234
|
-
values: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'best-practice'],
|
|
235
|
-
},
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
window.__AXE_RESULTS__ = results;
|
|
239
|
-
window.__RENDER_READY__ = true;
|
|
240
|
-
} catch (error) {
|
|
241
|
-
console.error("A11y audit error:", error);
|
|
242
|
-
window.__AXE_ERROR__ = error.message;
|
|
243
|
-
window.__RENDER_READY__ = true;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
render();
|
|
248
|
-
`;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// src/viewer/style-utils.ts
|
|
252
|
-
function compareStyles(figmaStyles, renderedStyles) {
|
|
253
|
-
const properties = [];
|
|
254
|
-
const cleanFigmaStyles = {};
|
|
255
|
-
const propsToCompare = [
|
|
256
|
-
"backgroundColor",
|
|
257
|
-
"borderColor",
|
|
258
|
-
"borderWidth",
|
|
259
|
-
"borderRadius",
|
|
260
|
-
"fontFamily",
|
|
261
|
-
"fontSize",
|
|
262
|
-
"fontWeight",
|
|
263
|
-
"lineHeight",
|
|
264
|
-
"letterSpacing",
|
|
265
|
-
"textAlign",
|
|
266
|
-
"boxShadow",
|
|
267
|
-
"padding",
|
|
268
|
-
"gap",
|
|
269
|
-
"opacity"
|
|
270
|
-
];
|
|
271
|
-
for (const prop of propsToCompare) {
|
|
272
|
-
const figmaValue = figmaStyles[prop];
|
|
273
|
-
const renderedValue = renderedStyles[prop];
|
|
274
|
-
if (figmaValue !== void 0) {
|
|
275
|
-
cleanFigmaStyles[prop] = figmaValue;
|
|
276
|
-
const match = compareStyleValue(prop, figmaValue, renderedValue || "");
|
|
277
|
-
properties.push({
|
|
278
|
-
property: prop,
|
|
279
|
-
figma: figmaValue,
|
|
280
|
-
rendered: renderedValue || "(not set)",
|
|
281
|
-
match
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
const allMatch = properties.every((p) => p.match);
|
|
286
|
-
return {
|
|
287
|
-
match: allMatch,
|
|
288
|
-
properties,
|
|
289
|
-
figmaStyles: cleanFigmaStyles,
|
|
290
|
-
renderedStyles
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
function compareStyleValue(prop, figma, rendered) {
|
|
294
|
-
const normalizedFigma = normalizeStyleValue(prop, figma);
|
|
295
|
-
const normalizedRendered = normalizeStyleValue(prop, rendered);
|
|
296
|
-
if (normalizedFigma === normalizedRendered) {
|
|
297
|
-
return true;
|
|
298
|
-
}
|
|
299
|
-
if (prop === "backgroundColor" || prop === "borderColor") {
|
|
300
|
-
return compareColors(normalizedFigma, normalizedRendered, 5);
|
|
301
|
-
}
|
|
302
|
-
if (["borderWidth", "borderRadius", "fontSize", "padding", "gap"].includes(prop)) {
|
|
303
|
-
return compareNumericValues(normalizedFigma, normalizedRendered, 1);
|
|
304
|
-
}
|
|
305
|
-
return false;
|
|
306
|
-
}
|
|
307
|
-
function normalizeStyleValue(prop, value) {
|
|
308
|
-
let normalized = value.trim().replace(/\s+/g, " ");
|
|
309
|
-
if (prop === "boxShadow" && normalized === "none") {
|
|
310
|
-
normalized = "";
|
|
311
|
-
}
|
|
312
|
-
if (normalized.match(/rgba\(\s*0\s*,\s*0\s*,\s*0\s*,\s*0\s*\)/)) {
|
|
313
|
-
normalized = "transparent";
|
|
314
|
-
}
|
|
315
|
-
return normalized;
|
|
316
|
-
}
|
|
317
|
-
function compareColors(color1, color2, tolerance) {
|
|
318
|
-
const rgb1 = parseColor(color1);
|
|
319
|
-
const rgb2 = parseColor(color2);
|
|
320
|
-
if (!rgb1 || !rgb2) {
|
|
321
|
-
return color1 === color2;
|
|
322
|
-
}
|
|
323
|
-
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;
|
|
324
|
-
}
|
|
325
|
-
function parseColor(color) {
|
|
326
|
-
const hexMatch = color.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
|
|
327
|
-
if (hexMatch) {
|
|
328
|
-
return {
|
|
329
|
-
r: parseInt(hexMatch[1], 16),
|
|
330
|
-
g: parseInt(hexMatch[2], 16),
|
|
331
|
-
b: parseInt(hexMatch[3], 16)
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
const rgbaMatch = color.match(
|
|
335
|
-
/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)/
|
|
336
|
-
);
|
|
337
|
-
if (rgbaMatch) {
|
|
338
|
-
return {
|
|
339
|
-
r: parseInt(rgbaMatch[1], 10),
|
|
340
|
-
g: parseInt(rgbaMatch[2], 10),
|
|
341
|
-
b: parseInt(rgbaMatch[3], 10),
|
|
342
|
-
a: rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
return null;
|
|
346
|
-
}
|
|
347
|
-
function compareNumericValues(value1, value2, tolerance) {
|
|
348
|
-
const num1 = parseFloat(value1);
|
|
349
|
-
const num2 = parseFloat(value2);
|
|
350
|
-
if (isNaN(num1) || isNaN(num2)) {
|
|
351
|
-
return value1 === value2;
|
|
352
|
-
}
|
|
353
|
-
return Math.abs(num1 - num2) <= tolerance;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// src/viewer/vite-plugin.ts
|
|
357
|
-
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
358
|
-
var monorepoViewerSrc = resolve(__dirname, "..", "..", "viewer", "src");
|
|
359
|
-
var npmViewerSrc = (() => {
|
|
360
|
-
const cliNm = resolve(__dirname, "..", "node_modules", "@fragments-sdk", "viewer", "src");
|
|
361
|
-
if (existsSync(cliNm)) return cliNm;
|
|
362
|
-
let dir = resolve(__dirname, "..");
|
|
363
|
-
for (let i = 0; i < 5; i++) {
|
|
364
|
-
const candidate = resolve(dir, "node_modules", "@fragments-sdk", "viewer", "src");
|
|
365
|
-
if (existsSync(candidate)) return candidate;
|
|
366
|
-
dir = resolve(dir, "..");
|
|
367
|
-
}
|
|
368
|
-
return null;
|
|
369
|
-
})();
|
|
370
|
-
var viewerAssetsRoot = existsSync(monorepoViewerSrc) ? monorepoViewerSrc : npmViewerSrc ?? monorepoViewerSrc;
|
|
371
|
-
var pendingRenders = /* @__PURE__ */ new Map();
|
|
372
|
-
var sharedRenderPool = null;
|
|
373
|
-
var browserPoolModule = null;
|
|
374
|
-
async function getSharedRenderPool() {
|
|
375
|
-
if (!browserPoolModule) {
|
|
376
|
-
browserPoolModule = await import("./service-HKJ6B7P7.js");
|
|
377
|
-
}
|
|
378
|
-
if (!sharedRenderPool) {
|
|
379
|
-
sharedRenderPool = new browserPoolModule.BrowserPool({
|
|
380
|
-
viewport: { width: 800, height: 600 },
|
|
381
|
-
// Default viewport, will be overridden per page
|
|
382
|
-
poolSize: 2,
|
|
383
|
-
// Keep 2 contexts warm for parallel requests
|
|
384
|
-
idleTimeoutMs: 6e4
|
|
385
|
-
// Keep warm for 60 seconds
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
|
-
return { pool: sharedRenderPool, bufferToBase64Url: browserPoolModule.bufferToBase64Url };
|
|
389
|
-
}
|
|
390
|
-
function fragmentsPlugin(options) {
|
|
391
|
-
const { fragmentFiles, config, projectRoot } = options;
|
|
392
|
-
const VIRTUAL_FRAGMENTS = `virtual:${BRAND.nameLower}`;
|
|
393
|
-
const VIRTUAL_FRAGMENTS_RESOLVED = `\0virtual:${BRAND.nameLower}`;
|
|
394
|
-
const VIRTUAL_VIEWER_ENTRY = `virtual:${BRAND.nameLower}-viewer-entry`;
|
|
395
|
-
const VIRTUAL_VIEWER_ENTRY_RESOLVED = `\0virtual:${BRAND.nameLower}-viewer-entry`;
|
|
396
|
-
const VIRTUAL_PREVIEW = `virtual:${BRAND.nameLower}-preview`;
|
|
397
|
-
const VIRTUAL_PREVIEW_RESOLVED = `\0virtual:${BRAND.nameLower}-preview`;
|
|
398
|
-
let server = null;
|
|
399
|
-
let resolvedConfig = null;
|
|
400
|
-
const storybookDir = findStorybookDir(projectRoot);
|
|
401
|
-
const previewConfigPath = storybookDir ? findPreviewConfigPath(storybookDir) : null;
|
|
402
|
-
const fragmentFileSet = new Set(fragmentFiles.map((f) => f.absolutePath));
|
|
403
|
-
const mainPlugin = {
|
|
404
|
-
name: "fragments",
|
|
405
|
-
// Add process.env shim and esbuild config for Storybook compatibility
|
|
406
|
-
config() {
|
|
407
|
-
return {
|
|
408
|
-
build: {
|
|
409
|
-
rollupOptions: {
|
|
410
|
-
onwarn(warning, defaultHandler) {
|
|
411
|
-
if (warning.code === "MODULE_LEVEL_DIRECTIVE" || warning.message && warning.message.includes("has been externalized")) {
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
|
-
defaultHandler(warning);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
},
|
|
418
|
-
define: {
|
|
419
|
-
// Shim process.env for story files that use it (e.g., process.env.STORYBOOK_*)
|
|
420
|
-
"process.env": "{}"
|
|
421
|
-
},
|
|
422
|
-
esbuild: {
|
|
423
|
-
// Handle JSX in .js files (common in Storybook preview.js files)
|
|
424
|
-
loader: "tsx",
|
|
425
|
-
include: /\.(tsx?|jsx?)$/
|
|
426
|
-
},
|
|
427
|
-
optimizeDeps: {
|
|
428
|
-
// Force esbuild to handle .js files with JSX
|
|
429
|
-
esbuildOptions: {
|
|
430
|
-
loader: {
|
|
431
|
-
".js": "jsx"
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
};
|
|
436
|
-
},
|
|
437
|
-
// Store resolved config
|
|
438
|
-
configResolved(config2) {
|
|
439
|
-
resolvedConfig = config2;
|
|
440
|
-
},
|
|
441
|
-
// Store server reference for HMR
|
|
442
|
-
configureServer(_server) {
|
|
443
|
-
server = _server;
|
|
444
|
-
_server.middlewares.use(async (req, res, next) => {
|
|
445
|
-
if (req.url === "/fragments/render" && req.method === "POST") {
|
|
446
|
-
try {
|
|
447
|
-
const body = await parseJsonBody(req);
|
|
448
|
-
const { component, props = {}, viewport, variant } = body;
|
|
449
|
-
if (!component) {
|
|
450
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
451
|
-
res.end(
|
|
452
|
-
JSON.stringify({ error: "Missing required field: component" })
|
|
453
|
-
);
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
456
|
-
const loadedFragments = await loadFragmentsForRender(
|
|
457
|
-
fragmentFiles,
|
|
458
|
-
projectRoot
|
|
459
|
-
);
|
|
460
|
-
const fragmentInfo = findFragmentByName(component, loadedFragments);
|
|
461
|
-
if (!fragmentInfo) {
|
|
462
|
-
const available = getAvailableComponents(loadedFragments);
|
|
463
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
464
|
-
res.end(
|
|
465
|
-
JSON.stringify({
|
|
466
|
-
error: `Component '${component}' not found. Available: ${available.join(
|
|
467
|
-
", "
|
|
468
|
-
)}`
|
|
469
|
-
})
|
|
470
|
-
);
|
|
471
|
-
return;
|
|
472
|
-
}
|
|
473
|
-
const fragmentFile = fragmentFiles.find(
|
|
474
|
-
(f) => f.relativePath === fragmentInfo.path
|
|
475
|
-
);
|
|
476
|
-
if (!fragmentFile) {
|
|
477
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
478
|
-
res.end(
|
|
479
|
-
JSON.stringify({ error: "Could not resolve fragment file path" })
|
|
480
|
-
);
|
|
481
|
-
return;
|
|
482
|
-
}
|
|
483
|
-
const renderScript = variant ? generateVariantRenderScript(
|
|
484
|
-
fragmentFile.absolutePath,
|
|
485
|
-
fragmentInfo.name,
|
|
486
|
-
variant
|
|
487
|
-
) : generateRenderScript(
|
|
488
|
-
fragmentFile.absolutePath,
|
|
489
|
-
fragmentInfo.name,
|
|
490
|
-
props
|
|
491
|
-
);
|
|
492
|
-
const requestId = Date.now().toString(36) + Math.random().toString(36).slice(2);
|
|
493
|
-
pendingRenders.set(requestId, { script: renderScript, viewport });
|
|
494
|
-
const address = _server.httpServer?.address();
|
|
495
|
-
const port = typeof address === "object" && address ? address.port : 6006;
|
|
496
|
-
const screenshot = await captureRender(
|
|
497
|
-
`http://localhost:${port}/fragments/__render__/${requestId}`,
|
|
498
|
-
viewport || { width: 800, height: 600 }
|
|
499
|
-
);
|
|
500
|
-
pendingRenders.delete(requestId);
|
|
501
|
-
res.setHeader("Content-Type", "application/json");
|
|
502
|
-
res.end(JSON.stringify({ screenshot }));
|
|
503
|
-
} catch (error) {
|
|
504
|
-
console.error("[Fragments] Error rendering:", error);
|
|
505
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
506
|
-
res.end(
|
|
507
|
-
JSON.stringify({
|
|
508
|
-
error: error instanceof Error ? error.message : "Render failed"
|
|
509
|
-
})
|
|
510
|
-
);
|
|
511
|
-
}
|
|
512
|
-
return;
|
|
513
|
-
}
|
|
514
|
-
if (req.url?.startsWith("/fragments/__render__/")) {
|
|
515
|
-
const requestId = req.url.split("/fragments/__render__/")[1]?.split("?")[0];
|
|
516
|
-
const renderData = pendingRenders.get(requestId || "");
|
|
517
|
-
if (!renderData) {
|
|
518
|
-
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
519
|
-
res.end("Render request not found or expired");
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
522
|
-
await serveRenderHTML(res, _server, renderData.script);
|
|
523
|
-
return;
|
|
524
|
-
}
|
|
525
|
-
if (req.url === "/fragments/compare" && req.method === "POST") {
|
|
526
|
-
try {
|
|
527
|
-
const body = await parseJsonBody(req);
|
|
528
|
-
const {
|
|
529
|
-
component,
|
|
530
|
-
variant,
|
|
531
|
-
props = {},
|
|
532
|
-
figmaUrl,
|
|
533
|
-
viewport,
|
|
534
|
-
threshold = 1,
|
|
535
|
-
includeStyleDiff = false
|
|
536
|
-
} = body;
|
|
537
|
-
if (!component) {
|
|
538
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
539
|
-
res.end(
|
|
540
|
-
JSON.stringify({ error: "Missing required field: component" })
|
|
541
|
-
);
|
|
542
|
-
return;
|
|
543
|
-
}
|
|
544
|
-
const figmaToken = body.figmaToken || process.env.FIGMA_ACCESS_TOKEN || config.figmaToken;
|
|
545
|
-
if (!figmaToken && !figmaUrl) {
|
|
546
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
547
|
-
res.end(
|
|
548
|
-
JSON.stringify({
|
|
549
|
-
error: `No Figma access token configured. Figma token: ${figmaToken}`,
|
|
550
|
-
suggestion: "Set FIGMA_ACCESS_TOKEN env var, add figmaToken to fragments.config.ts, or provide in request"
|
|
551
|
-
})
|
|
552
|
-
);
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
console.log("[Fragments] Compare request for:", component);
|
|
556
|
-
console.log("[Fragments] fragmentFiles count:", fragmentFiles.length);
|
|
557
|
-
console.log("[Fragments] First 3 fragment files:", fragmentFiles.slice(0, 3).map((f) => f.relativePath));
|
|
558
|
-
console.log("[Fragments] projectRoot:", projectRoot);
|
|
559
|
-
const loadedFragments = await loadFragmentsForRender(
|
|
560
|
-
fragmentFiles,
|
|
561
|
-
projectRoot
|
|
562
|
-
);
|
|
563
|
-
console.log("[Fragments] loadedFragments count:", loadedFragments.length);
|
|
564
|
-
console.log("[Fragments] First 3 loaded:", loadedFragments.slice(0, 3).map((s) => s.fragment.meta.name));
|
|
565
|
-
const fragmentInfo = findFragmentByName(component, loadedFragments);
|
|
566
|
-
if (!fragmentInfo) {
|
|
567
|
-
const available = getAvailableComponents(loadedFragments);
|
|
568
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
569
|
-
res.end(
|
|
570
|
-
JSON.stringify({
|
|
571
|
-
error: `Component '${component}' not found. Available: ${available.join(
|
|
572
|
-
", "
|
|
573
|
-
)}`
|
|
574
|
-
})
|
|
575
|
-
);
|
|
576
|
-
return;
|
|
577
|
-
}
|
|
578
|
-
const fullFragmentData = await loadFullFragmentForCompare(
|
|
579
|
-
_server,
|
|
580
|
-
fragmentFiles,
|
|
581
|
-
component,
|
|
582
|
-
variant,
|
|
583
|
-
projectRoot
|
|
584
|
-
);
|
|
585
|
-
const effectiveFigmaUrl = figmaUrl || fullFragmentData?.figmaUrl;
|
|
586
|
-
if (!effectiveFigmaUrl) {
|
|
587
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
588
|
-
res.end(
|
|
589
|
-
JSON.stringify({
|
|
590
|
-
error: `No Figma URL for component '${component}'`,
|
|
591
|
-
suggestion: "Add 'figma' field to fragment definition or provide figmaUrl in request"
|
|
592
|
-
})
|
|
593
|
-
);
|
|
594
|
-
return;
|
|
595
|
-
}
|
|
596
|
-
if (!figmaToken) {
|
|
597
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
598
|
-
res.end(
|
|
599
|
-
JSON.stringify({
|
|
600
|
-
error: "Figma access token required for comparison",
|
|
601
|
-
suggestion: "Set FIGMA_ACCESS_TOKEN env var or add figmaToken to fragments.config.ts"
|
|
602
|
-
})
|
|
603
|
-
);
|
|
604
|
-
return;
|
|
605
|
-
}
|
|
606
|
-
const fragmentFile = fragmentFiles.find(
|
|
607
|
-
(f) => f.relativePath === fragmentInfo.path
|
|
608
|
-
);
|
|
609
|
-
if (!fragmentFile) {
|
|
610
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
611
|
-
res.end(
|
|
612
|
-
JSON.stringify({ error: "Could not resolve fragment file path" })
|
|
613
|
-
);
|
|
614
|
-
return;
|
|
615
|
-
}
|
|
616
|
-
const address = _server.httpServer?.address();
|
|
617
|
-
const port = typeof address === "object" && address ? address.port : 6006;
|
|
618
|
-
const renderViewport = viewport || { width: 800, height: 600 };
|
|
619
|
-
const { FigmaClient, bufferToBase64Url } = await import("./service-HKJ6B7P7.js");
|
|
620
|
-
const figmaClient = new FigmaClient({
|
|
621
|
-
accessToken: figmaToken
|
|
622
|
-
});
|
|
623
|
-
const { fileKey, nodeId } = figmaClient.parseUrl(effectiveFigmaUrl);
|
|
624
|
-
const renderScript = generateRenderScript(
|
|
625
|
-
fragmentFile.absolutePath,
|
|
626
|
-
fragmentInfo.name,
|
|
627
|
-
props
|
|
628
|
-
);
|
|
629
|
-
const requestId = Date.now().toString(36) + Math.random().toString(36).slice(2);
|
|
630
|
-
pendingRenders.set(requestId, {
|
|
631
|
-
script: renderScript,
|
|
632
|
-
viewport: renderViewport
|
|
633
|
-
});
|
|
634
|
-
try {
|
|
635
|
-
const [captureResult, figmaImageResult, figmaDesignProps] = await Promise.all([
|
|
636
|
-
// Render and capture the component (with optional computed styles)
|
|
637
|
-
captureRenderWithStyles(
|
|
638
|
-
`http://localhost:${port}/fragments/__render__/${requestId}`,
|
|
639
|
-
renderViewport,
|
|
640
|
-
includeStyleDiff
|
|
641
|
-
),
|
|
642
|
-
// Fetch Figma image
|
|
643
|
-
figmaClient.getImageFromUrl(effectiveFigmaUrl),
|
|
644
|
-
// Fetch Figma design properties (only if includeStyleDiff is true)
|
|
645
|
-
includeStyleDiff ? figmaClient.getNodeProperties(fileKey, nodeId) : Promise.resolve(null)
|
|
646
|
-
]);
|
|
647
|
-
const renderedImage = captureResult.screenshot;
|
|
648
|
-
const renderedStyles = captureResult.computedStyles;
|
|
649
|
-
const figmaImage = bufferToBase64Url(figmaImageResult.data);
|
|
650
|
-
const compareResult = await compareImages(
|
|
651
|
-
renderedImage,
|
|
652
|
-
figmaImage,
|
|
653
|
-
threshold
|
|
654
|
-
);
|
|
655
|
-
const response = {
|
|
656
|
-
match: compareResult.matches,
|
|
657
|
-
diffPercentage: compareResult.diffPercentage,
|
|
658
|
-
threshold,
|
|
659
|
-
rendered: renderedImage,
|
|
660
|
-
figma: figmaImage,
|
|
661
|
-
diff: compareResult.diffImage || renderedImage,
|
|
662
|
-
figmaUrl: effectiveFigmaUrl,
|
|
663
|
-
changedRegions: compareResult.changedRegions
|
|
664
|
-
};
|
|
665
|
-
if (includeStyleDiff && figmaDesignProps && renderedStyles) {
|
|
666
|
-
const figmaStyles = figmaClient.convertToCSS(figmaDesignProps);
|
|
667
|
-
const figmaStylesRecord = { ...figmaStyles };
|
|
668
|
-
const styleDiffResult = compareStyles(figmaStylesRecord, renderedStyles);
|
|
669
|
-
response.styleDiff = styleDiffResult;
|
|
670
|
-
if (!styleDiffResult.match) {
|
|
671
|
-
response.match = false;
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
res.setHeader("Content-Type", "application/json");
|
|
675
|
-
res.end(JSON.stringify(response));
|
|
676
|
-
} finally {
|
|
677
|
-
pendingRenders.delete(requestId);
|
|
678
|
-
}
|
|
679
|
-
} catch (error) {
|
|
680
|
-
console.error("[Fragments] Error comparing:", error);
|
|
681
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
682
|
-
res.end(
|
|
683
|
-
JSON.stringify({
|
|
684
|
-
error: error instanceof Error ? error.message : "Compare failed"
|
|
685
|
-
})
|
|
686
|
-
);
|
|
687
|
-
}
|
|
688
|
-
return;
|
|
689
|
-
}
|
|
690
|
-
if (req.url === "/fragments/figma-styles" && req.method === "POST") {
|
|
691
|
-
try {
|
|
692
|
-
const body = await parseJsonBody(req);
|
|
693
|
-
const { figmaUrl } = body;
|
|
694
|
-
if (!figmaUrl) {
|
|
695
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
696
|
-
res.end(JSON.stringify({ error: "Missing figmaUrl" }));
|
|
697
|
-
return;
|
|
698
|
-
}
|
|
699
|
-
const figmaToken = process.env.FIGMA_ACCESS_TOKEN || config.figmaToken;
|
|
700
|
-
if (!figmaToken) {
|
|
701
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
702
|
-
res.end(
|
|
703
|
-
JSON.stringify({
|
|
704
|
-
error: "No Figma access token configured",
|
|
705
|
-
suggestion: "Set FIGMA_ACCESS_TOKEN env var or add figmaToken to fragments.config.ts"
|
|
706
|
-
})
|
|
707
|
-
);
|
|
708
|
-
return;
|
|
709
|
-
}
|
|
710
|
-
const { FigmaClient } = await import("./service-HKJ6B7P7.js");
|
|
711
|
-
const figmaClient = new FigmaClient({ accessToken: figmaToken });
|
|
712
|
-
const { fileKey, nodeId } = figmaClient.parseUrl(figmaUrl);
|
|
713
|
-
const figmaDesignProps = await figmaClient.getNodeProperties(
|
|
714
|
-
fileKey,
|
|
715
|
-
nodeId
|
|
716
|
-
);
|
|
717
|
-
const figmaStyles = figmaClient.convertToCSS(figmaDesignProps);
|
|
718
|
-
res.setHeader("Content-Type", "application/json");
|
|
719
|
-
res.end(JSON.stringify({ styles: figmaStyles }));
|
|
720
|
-
} catch (error) {
|
|
721
|
-
console.error("[Fragments] Error fetching Figma styles:", error);
|
|
722
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
723
|
-
res.end(
|
|
724
|
-
JSON.stringify({
|
|
725
|
-
error: error instanceof Error ? error.message : "Failed to fetch Figma styles"
|
|
726
|
-
})
|
|
727
|
-
);
|
|
728
|
-
}
|
|
729
|
-
return;
|
|
730
|
-
}
|
|
731
|
-
if (req.url?.startsWith("/fragments/tokens")) {
|
|
732
|
-
try {
|
|
733
|
-
const url = new URL(req.url, "http://localhost");
|
|
734
|
-
const format = url.searchParams.get("format") || "json";
|
|
735
|
-
const category = url.searchParams.get("category");
|
|
736
|
-
const theme = url.searchParams.get("theme");
|
|
737
|
-
if (!config.tokens || !config.tokens.include || config.tokens.include.length === 0) {
|
|
738
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
739
|
-
res.end(JSON.stringify({
|
|
740
|
-
error: "No token configuration found",
|
|
741
|
-
suggestion: "Add 'tokens' config to fragments.config.ts with 'include' patterns for CSS/SCSS files",
|
|
742
|
-
example: {
|
|
743
|
-
tokens: {
|
|
744
|
-
include: ["src/styles/theme.scss", "src/styles/variables.css"],
|
|
745
|
-
themeSelectors: { ":root": "default", "[data-theme='dark']": "dark" }
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
}));
|
|
749
|
-
return;
|
|
750
|
-
}
|
|
751
|
-
const { getSharedTokenRegistry } = await import("./service-HKJ6B7P7.js");
|
|
752
|
-
const registry = getSharedTokenRegistry();
|
|
753
|
-
if (!registry.isInitialized()) {
|
|
754
|
-
await registry.initialize(config.tokens, projectRoot);
|
|
755
|
-
}
|
|
756
|
-
let tokens = registry.getAllTokens();
|
|
757
|
-
if (category) {
|
|
758
|
-
tokens = tokens.filter((t) => t.category === category);
|
|
759
|
-
}
|
|
760
|
-
if (theme) {
|
|
761
|
-
tokens = tokens.filter((t) => t.theme === theme || t.theme === "default");
|
|
762
|
-
}
|
|
763
|
-
const meta = registry.getMeta();
|
|
764
|
-
if (format === "summary") {
|
|
765
|
-
const summary = {
|
|
766
|
-
totalTokens: meta?.totalTokens || 0,
|
|
767
|
-
byCategory: {},
|
|
768
|
-
byTheme: {},
|
|
769
|
-
parseTimeMs: meta?.parseTimeMs || 0,
|
|
770
|
-
sourceFiles: meta?.sourceFiles || []
|
|
771
|
-
};
|
|
772
|
-
for (const token of registry.getAllTokens()) {
|
|
773
|
-
summary.byCategory[token.category] = (summary.byCategory[token.category] || 0) + 1;
|
|
774
|
-
summary.byTheme[token.theme] = (summary.byTheme[token.theme] || 0) + 1;
|
|
775
|
-
}
|
|
776
|
-
res.setHeader("Content-Type", "application/json");
|
|
777
|
-
res.end(JSON.stringify(summary, null, 2));
|
|
778
|
-
} else {
|
|
779
|
-
res.setHeader("Content-Type", "application/json");
|
|
780
|
-
res.end(JSON.stringify({
|
|
781
|
-
tokens,
|
|
782
|
-
meta
|
|
783
|
-
}, null, 2));
|
|
784
|
-
}
|
|
785
|
-
} catch (error) {
|
|
786
|
-
console.error("[Fragments] Error fetching tokens:", error);
|
|
787
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
788
|
-
res.end(JSON.stringify({
|
|
789
|
-
error: error instanceof Error ? error.message : "Failed to fetch tokens"
|
|
790
|
-
}));
|
|
791
|
-
}
|
|
792
|
-
return;
|
|
793
|
-
}
|
|
794
|
-
if (req.url === "/fragments/token-match" && req.method === "POST") {
|
|
795
|
-
try {
|
|
796
|
-
const body = await parseJsonBody(req);
|
|
797
|
-
const { value, propertyType, theme } = body;
|
|
798
|
-
if (!value) {
|
|
799
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
800
|
-
res.end(JSON.stringify({ error: "Missing required field: value" }));
|
|
801
|
-
return;
|
|
802
|
-
}
|
|
803
|
-
if (!config.tokens || !config.tokens.include || config.tokens.include.length === 0) {
|
|
804
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
805
|
-
res.end(JSON.stringify({
|
|
806
|
-
error: "No token configuration found",
|
|
807
|
-
suggestion: "Add 'tokens' config to fragments.config.ts"
|
|
808
|
-
}));
|
|
809
|
-
return;
|
|
810
|
-
}
|
|
811
|
-
const { getSharedTokenRegistry } = await import("./service-HKJ6B7P7.js");
|
|
812
|
-
const registry = getSharedTokenRegistry();
|
|
813
|
-
if (!registry.isInitialized()) {
|
|
814
|
-
await registry.initialize(config.tokens, projectRoot);
|
|
815
|
-
}
|
|
816
|
-
const result = registry.matchValue({
|
|
817
|
-
value,
|
|
818
|
-
propertyType,
|
|
819
|
-
theme
|
|
820
|
-
});
|
|
821
|
-
res.setHeader("Content-Type", "application/json");
|
|
822
|
-
res.end(JSON.stringify(result));
|
|
823
|
-
} catch (error) {
|
|
824
|
-
console.error("[Fragments] Error matching token:", error);
|
|
825
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
826
|
-
res.end(JSON.stringify({
|
|
827
|
-
error: error instanceof Error ? error.message : "Failed to match token"
|
|
828
|
-
}));
|
|
829
|
-
}
|
|
830
|
-
return;
|
|
831
|
-
}
|
|
832
|
-
if (req.url === "/fragments/compliance" && req.method === "POST") {
|
|
833
|
-
try {
|
|
834
|
-
const body = await parseJsonBody(req);
|
|
835
|
-
const { component, variant, theme = "default" } = body;
|
|
836
|
-
if (!component) {
|
|
837
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
838
|
-
res.end(JSON.stringify({ error: "Missing required field: component" }));
|
|
839
|
-
return;
|
|
840
|
-
}
|
|
841
|
-
if (!config.tokens || !config.tokens.include || config.tokens.include.length === 0) {
|
|
842
|
-
res.setHeader("Content-Type", "application/json");
|
|
843
|
-
res.end(JSON.stringify({
|
|
844
|
-
component,
|
|
845
|
-
variant,
|
|
846
|
-
compliance: 100,
|
|
847
|
-
totalProperties: 0,
|
|
848
|
-
hardcoded: 0,
|
|
849
|
-
usingTokens: 0,
|
|
850
|
-
violations: [],
|
|
851
|
-
note: "No token configuration found - token compliance checking disabled"
|
|
852
|
-
}));
|
|
853
|
-
return;
|
|
854
|
-
}
|
|
855
|
-
const loadedFragments = await loadFragmentsForRender(fragmentFiles, projectRoot);
|
|
856
|
-
const fragmentInfo = findFragmentByName(component, loadedFragments);
|
|
857
|
-
if (!fragmentInfo) {
|
|
858
|
-
const available = getAvailableComponents(loadedFragments);
|
|
859
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
860
|
-
res.end(JSON.stringify({
|
|
861
|
-
error: `Component '${component}' not found. Available: ${available.join(", ")}`
|
|
862
|
-
}));
|
|
863
|
-
return;
|
|
864
|
-
}
|
|
865
|
-
const fragmentFile = fragmentFiles.find(
|
|
866
|
-
(f) => f.relativePath === fragmentInfo.path
|
|
867
|
-
);
|
|
868
|
-
if (!fragmentFile) {
|
|
869
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
870
|
-
res.end(JSON.stringify({ error: "Could not resolve fragment file path" }));
|
|
871
|
-
return;
|
|
872
|
-
}
|
|
873
|
-
const { getSharedTokenRegistry } = await import("./service-HKJ6B7P7.js");
|
|
874
|
-
const registry = getSharedTokenRegistry();
|
|
875
|
-
if (!registry.isInitialized()) {
|
|
876
|
-
await registry.initialize(config.tokens, projectRoot);
|
|
877
|
-
}
|
|
878
|
-
const address = _server.httpServer?.address();
|
|
879
|
-
const port = typeof address === "object" && address ? address.port : 6006;
|
|
880
|
-
const renderViewport = { width: 800, height: 600 };
|
|
881
|
-
const renderScript = generateRenderScript(
|
|
882
|
-
fragmentFile.absolutePath,
|
|
883
|
-
fragmentInfo.name,
|
|
884
|
-
{}
|
|
885
|
-
);
|
|
886
|
-
const requestId = Date.now().toString(36) + Math.random().toString(36).slice(2);
|
|
887
|
-
pendingRenders.set(requestId, { script: renderScript, viewport: renderViewport });
|
|
888
|
-
try {
|
|
889
|
-
const captureResult = await captureRenderWithStyles(
|
|
890
|
-
`http://localhost:${port}/fragments/__render__/${requestId}`,
|
|
891
|
-
renderViewport,
|
|
892
|
-
true
|
|
893
|
-
// extractStyles = true
|
|
894
|
-
);
|
|
895
|
-
const computedStyles = captureResult.computedStyles || {};
|
|
896
|
-
const styleDiffs = [];
|
|
897
|
-
for (const [property, value] of Object.entries(computedStyles)) {
|
|
898
|
-
if (!value) continue;
|
|
899
|
-
const matchResult = registry.matchValue({
|
|
900
|
-
value,
|
|
901
|
-
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,
|
|
902
|
-
theme
|
|
903
|
-
});
|
|
904
|
-
const isUsingToken = matchResult.exactMatches.length > 0;
|
|
905
|
-
styleDiffs.push({
|
|
906
|
-
property,
|
|
907
|
-
figma: value,
|
|
908
|
-
// Use the value as both figma and rendered for self-comparison
|
|
909
|
-
rendered: value,
|
|
910
|
-
match: isUsingToken
|
|
911
|
-
});
|
|
912
|
-
}
|
|
913
|
-
const usageSummary = registry.calculateUsageSummary(styleDiffs, theme);
|
|
914
|
-
const violations = usageSummary.hardcodedProperties.map((hp) => {
|
|
915
|
-
const suggestion = hp.suggestedFix ? `Use ${hp.suggestedFix.tokenName} (${hp.suggestedFix.tokenValue})` : void 0;
|
|
916
|
-
return {
|
|
917
|
-
property: hp.property,
|
|
918
|
-
issue: `Hardcoded value "${hp.rendered}" should use a design token`,
|
|
919
|
-
severity: "warning",
|
|
920
|
-
suggestion,
|
|
921
|
-
expected: hp.figmaToken,
|
|
922
|
-
actual: hp.rendered
|
|
923
|
-
};
|
|
924
|
-
});
|
|
925
|
-
res.setHeader("Content-Type", "application/json");
|
|
926
|
-
res.end(JSON.stringify({
|
|
927
|
-
component,
|
|
928
|
-
variant,
|
|
929
|
-
compliance: usageSummary.compliancePercent,
|
|
930
|
-
totalProperties: usageSummary.totalProperties,
|
|
931
|
-
hardcoded: usageSummary.hardcoded,
|
|
932
|
-
usingTokens: usageSummary.usingTokens,
|
|
933
|
-
violations
|
|
934
|
-
}));
|
|
935
|
-
} finally {
|
|
936
|
-
pendingRenders.delete(requestId);
|
|
937
|
-
}
|
|
938
|
-
} catch (error) {
|
|
939
|
-
console.error("[Fragments] Error checking compliance:", error);
|
|
940
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
941
|
-
res.end(JSON.stringify({
|
|
942
|
-
error: error instanceof Error ? error.message : "Compliance check failed"
|
|
943
|
-
}));
|
|
944
|
-
}
|
|
945
|
-
return;
|
|
946
|
-
}
|
|
947
|
-
if (req.url === "/fragments/compiled.json") {
|
|
948
|
-
try {
|
|
949
|
-
const { join: join2 } = await import("path");
|
|
950
|
-
const fragmentsJsonPath = join2(projectRoot, BRAND.outFile);
|
|
951
|
-
const content = await readFile(fragmentsJsonPath, "utf-8");
|
|
952
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
953
|
-
res.end(content);
|
|
954
|
-
} catch (error) {
|
|
955
|
-
console.warn(`[${BRAND.name}] Failed to serve compiled.json:`, error);
|
|
956
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
957
|
-
res.end(JSON.stringify({
|
|
958
|
-
error: `${BRAND.outFile} not found. Run '${BRAND.cliCommand} build' first.`
|
|
959
|
-
}));
|
|
960
|
-
}
|
|
961
|
-
return;
|
|
962
|
-
}
|
|
963
|
-
if (req.url?.startsWith("/fragments/context")) {
|
|
964
|
-
try {
|
|
965
|
-
const url = new URL(req.url, "http://localhost");
|
|
966
|
-
const format = url.searchParams.get("format") || "markdown";
|
|
967
|
-
const compact = url.searchParams.get("compact") === "true";
|
|
968
|
-
const compiledFragments = await loadFragmentsForContext(
|
|
969
|
-
_server,
|
|
970
|
-
fragmentFiles,
|
|
971
|
-
config,
|
|
972
|
-
projectRoot
|
|
973
|
-
);
|
|
974
|
-
const { content, tokenEstimate } = generateContext(
|
|
975
|
-
compiledFragments,
|
|
976
|
-
{
|
|
977
|
-
format,
|
|
978
|
-
compact,
|
|
979
|
-
include: {
|
|
980
|
-
code: url.searchParams.get("code") === "true",
|
|
981
|
-
relations: url.searchParams.get("relations") === "true"
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
);
|
|
985
|
-
res.setHeader("X-Token-Estimate", String(tokenEstimate));
|
|
986
|
-
res.setHeader(
|
|
987
|
-
"Content-Type",
|
|
988
|
-
format === "json" ? "application/json" : "text/markdown; charset=utf-8"
|
|
989
|
-
);
|
|
990
|
-
res.end(content);
|
|
991
|
-
} catch (error) {
|
|
992
|
-
console.error("[Fragments] Error generating context:", error);
|
|
993
|
-
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
994
|
-
res.end(
|
|
995
|
-
"Error generating context: " + (error instanceof Error ? error.message : error)
|
|
996
|
-
);
|
|
997
|
-
}
|
|
998
|
-
return;
|
|
999
|
-
}
|
|
1000
|
-
if (req.url === "/fragments/save" && req.method === "POST") {
|
|
1001
|
-
try {
|
|
1002
|
-
const body = await parseJsonBody(req);
|
|
1003
|
-
const { componentName, fragment } = body;
|
|
1004
|
-
if (!componentName || !fragment) {
|
|
1005
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1006
|
-
res.end(
|
|
1007
|
-
JSON.stringify({
|
|
1008
|
-
error: "Missing required fields: componentName, fragment"
|
|
1009
|
-
})
|
|
1010
|
-
);
|
|
1011
|
-
return;
|
|
1012
|
-
}
|
|
1013
|
-
const { writeFile, mkdir } = await import("fs/promises");
|
|
1014
|
-
const { join: join2 } = await import("path");
|
|
1015
|
-
const { BRAND: BRAND2 } = await import("./core/index.js");
|
|
1016
|
-
const fragmentsDir = join2(projectRoot, BRAND2.dataDir, BRAND2.componentsDir);
|
|
1017
|
-
await mkdir(fragmentsDir, { recursive: true });
|
|
1018
|
-
const fragmentPath = join2(
|
|
1019
|
-
fragmentsDir,
|
|
1020
|
-
`${componentName}${BRAND2.fileExtension}`
|
|
1021
|
-
);
|
|
1022
|
-
await writeFile(
|
|
1023
|
-
fragmentPath,
|
|
1024
|
-
JSON.stringify(fragment, null, 2),
|
|
1025
|
-
"utf-8"
|
|
1026
|
-
);
|
|
1027
|
-
res.setHeader("Content-Type", "application/json");
|
|
1028
|
-
res.end(JSON.stringify({ success: true, path: fragmentPath }));
|
|
1029
|
-
} catch (error) {
|
|
1030
|
-
console.error("[Fragments] Error saving fragment:", error);
|
|
1031
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1032
|
-
res.end(
|
|
1033
|
-
JSON.stringify({
|
|
1034
|
-
error: error instanceof Error ? error.message : "Save failed"
|
|
1035
|
-
})
|
|
1036
|
-
);
|
|
1037
|
-
}
|
|
1038
|
-
return;
|
|
1039
|
-
}
|
|
1040
|
-
if (req.url === "/fragments/fix" && req.method === "POST") {
|
|
1041
|
-
try {
|
|
1042
|
-
const body = await parseJsonBody(req);
|
|
1043
|
-
const { component, variant, fixType = "all" } = body;
|
|
1044
|
-
if (!component) {
|
|
1045
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1046
|
-
res.end(JSON.stringify({ error: "Missing required field: component" }));
|
|
1047
|
-
return;
|
|
1048
|
-
}
|
|
1049
|
-
if (!config.tokens || !config.tokens.include || config.tokens.include.length === 0) {
|
|
1050
|
-
try {
|
|
1051
|
-
const { discoverTokenFiles } = await import("./discovery-VDANZAJ2.js");
|
|
1052
|
-
const discovered = await discoverTokenFiles(projectRoot);
|
|
1053
|
-
if (discovered.length > 0) {
|
|
1054
|
-
config.tokens = {
|
|
1055
|
-
...config.tokens,
|
|
1056
|
-
include: discovered.map((f) => f.relativePath)
|
|
1057
|
-
};
|
|
1058
|
-
} else {
|
|
1059
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1060
|
-
res.end(JSON.stringify({
|
|
1061
|
-
error: "No token files found",
|
|
1062
|
-
suggestion: "Add 'tokens' config to fragments.config.ts or add token files matching default patterns (_variables.scss, tokens.scss, etc.)"
|
|
1063
|
-
}));
|
|
1064
|
-
return;
|
|
1065
|
-
}
|
|
1066
|
-
} catch {
|
|
1067
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1068
|
-
res.end(JSON.stringify({
|
|
1069
|
-
error: "No token configuration found and auto-discovery failed",
|
|
1070
|
-
suggestion: "Add 'tokens' config to fragments.config.ts to enable fix generation"
|
|
1071
|
-
}));
|
|
1072
|
-
return;
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
const loadedFragments = await loadFragmentsForRender(fragmentFiles, projectRoot);
|
|
1076
|
-
const fragmentInfo = findFragmentByName(component, loadedFragments);
|
|
1077
|
-
if (!fragmentInfo) {
|
|
1078
|
-
const available = getAvailableComponents(loadedFragments);
|
|
1079
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1080
|
-
res.end(JSON.stringify({
|
|
1081
|
-
error: `Component '${component}' not found. Available: ${available.join(", ")}`
|
|
1082
|
-
}));
|
|
1083
|
-
return;
|
|
1084
|
-
}
|
|
1085
|
-
const {
|
|
1086
|
-
getSharedTokenRegistry,
|
|
1087
|
-
generateTokenPatches
|
|
1088
|
-
} = await import("./service-HKJ6B7P7.js");
|
|
1089
|
-
const registry = getSharedTokenRegistry();
|
|
1090
|
-
if (!registry.isInitialized()) {
|
|
1091
|
-
await registry.initialize(config.tokens, projectRoot);
|
|
1092
|
-
}
|
|
1093
|
-
const fragmentFile = fragmentFiles.find(
|
|
1094
|
-
(f) => f.relativePath === fragmentInfo.path
|
|
1095
|
-
);
|
|
1096
|
-
const sourceFile = fragmentFile?.relativePath || `${component}.tsx`;
|
|
1097
|
-
const styleDiffs = [];
|
|
1098
|
-
if (fragmentFile) {
|
|
1099
|
-
try {
|
|
1100
|
-
const renderScript = generateRenderScript(
|
|
1101
|
-
fragmentFile.absolutePath,
|
|
1102
|
-
fragmentInfo.name,
|
|
1103
|
-
{}
|
|
1104
|
-
);
|
|
1105
|
-
const requestId = Date.now().toString(36) + Math.random().toString(36).slice(2);
|
|
1106
|
-
pendingRenders.set(requestId, {
|
|
1107
|
-
script: renderScript,
|
|
1108
|
-
viewport: { width: 800, height: 600 }
|
|
1109
|
-
});
|
|
1110
|
-
const address = _server.httpServer?.address();
|
|
1111
|
-
const port = typeof address === "object" && address ? address.port : 6006;
|
|
1112
|
-
const { computedStyles } = await captureRenderWithStyles(
|
|
1113
|
-
`http://localhost:${port}/fragments/__render__/${requestId}`,
|
|
1114
|
-
{ width: 800, height: 600 },
|
|
1115
|
-
true
|
|
1116
|
-
);
|
|
1117
|
-
pendingRenders.delete(requestId);
|
|
1118
|
-
if (computedStyles) {
|
|
1119
|
-
const tokenValues = /* @__PURE__ */ new Map();
|
|
1120
|
-
const allTokens = registry.getAllTokens();
|
|
1121
|
-
for (const t of allTokens) {
|
|
1122
|
-
if (t.resolvedValue) {
|
|
1123
|
-
tokenValues.set(t.resolvedValue, t.name);
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
for (const [prop, value] of Object.entries(computedStyles)) {
|
|
1127
|
-
if (!value || value === "transparent" || value === "rgba(0, 0, 0, 0)") continue;
|
|
1128
|
-
const matchesToken = tokenValues.has(value);
|
|
1129
|
-
if (!matchesToken) {
|
|
1130
|
-
styleDiffs.push({
|
|
1131
|
-
property: prop,
|
|
1132
|
-
figma: value,
|
|
1133
|
-
// Using rendered as "expected" since we have no Figma
|
|
1134
|
-
rendered: value,
|
|
1135
|
-
match: false
|
|
1136
|
-
});
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
} catch (renderErr) {
|
|
1141
|
-
console.warn("[Fragments] Could not render for style extraction:", renderErr);
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
const result = generateTokenPatches(
|
|
1145
|
-
component,
|
|
1146
|
-
styleDiffs,
|
|
1147
|
-
registry,
|
|
1148
|
-
{ sourceFile }
|
|
1149
|
-
);
|
|
1150
|
-
res.setHeader("Content-Type", "application/json");
|
|
1151
|
-
res.end(JSON.stringify({
|
|
1152
|
-
patches: result.patches,
|
|
1153
|
-
summary: result.summary,
|
|
1154
|
-
fixableCount: result.fixableCount,
|
|
1155
|
-
unfixableCount: result.unfixableCount
|
|
1156
|
-
}));
|
|
1157
|
-
} catch (error) {
|
|
1158
|
-
console.error("[Fragments] Error generating fixes:", error);
|
|
1159
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1160
|
-
res.end(JSON.stringify({
|
|
1161
|
-
error: error instanceof Error ? error.message : "Fix generation failed"
|
|
1162
|
-
}));
|
|
1163
|
-
}
|
|
1164
|
-
return;
|
|
1165
|
-
}
|
|
1166
|
-
if (req.url === "/fragments/a11y" && req.method === "POST") {
|
|
1167
|
-
try {
|
|
1168
|
-
const body = await parseJsonBody(req);
|
|
1169
|
-
const { component, variant: variantName, standard = "AA" } = body;
|
|
1170
|
-
if (!component) {
|
|
1171
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1172
|
-
res.end(
|
|
1173
|
-
JSON.stringify({ error: "Missing required field: component" })
|
|
1174
|
-
);
|
|
1175
|
-
return;
|
|
1176
|
-
}
|
|
1177
|
-
const loadedFragments = await loadFragmentsForRender(
|
|
1178
|
-
fragmentFiles,
|
|
1179
|
-
projectRoot
|
|
1180
|
-
);
|
|
1181
|
-
const fragmentInfo = findFragmentByName(component, loadedFragments);
|
|
1182
|
-
if (!fragmentInfo) {
|
|
1183
|
-
const available = getAvailableComponents(loadedFragments);
|
|
1184
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1185
|
-
res.end(
|
|
1186
|
-
JSON.stringify({
|
|
1187
|
-
error: `Component '${component}' not found. Available: ${available.join(", ")}`
|
|
1188
|
-
})
|
|
1189
|
-
);
|
|
1190
|
-
return;
|
|
1191
|
-
}
|
|
1192
|
-
const fragmentFile = fragmentFiles.find(
|
|
1193
|
-
(f) => f.relativePath === fragmentInfo.path
|
|
1194
|
-
);
|
|
1195
|
-
if (!fragmentFile) {
|
|
1196
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1197
|
-
res.end(
|
|
1198
|
-
JSON.stringify({ error: "Could not resolve fragment file path" })
|
|
1199
|
-
);
|
|
1200
|
-
return;
|
|
1201
|
-
}
|
|
1202
|
-
const variantNames = [];
|
|
1203
|
-
if (variantName) {
|
|
1204
|
-
variantNames.push(variantName);
|
|
1205
|
-
} else {
|
|
1206
|
-
const fullData = await loadFullFragmentData(projectRoot);
|
|
1207
|
-
const fragmentData = fullData ? Object.values(fullData.fragments).find(
|
|
1208
|
-
(f) => f.meta.name.toLowerCase() === component.toLowerCase()
|
|
1209
|
-
) : null;
|
|
1210
|
-
if (fragmentData && fragmentData.variants?.length > 0) {
|
|
1211
|
-
for (const v of fragmentData.variants) {
|
|
1212
|
-
variantNames.push(v.name);
|
|
1213
|
-
}
|
|
1214
|
-
} else {
|
|
1215
|
-
variantNames.push("Default");
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
const address = _server.httpServer?.address();
|
|
1219
|
-
const port = typeof address === "object" && address ? address.port : 6006;
|
|
1220
|
-
const results = [];
|
|
1221
|
-
for (const vName of variantNames) {
|
|
1222
|
-
const a11yScript = generateA11yRenderScript(
|
|
1223
|
-
fragmentFile.absolutePath,
|
|
1224
|
-
fragmentInfo.name,
|
|
1225
|
-
vName === "Default" && !variantName ? void 0 : vName
|
|
1226
|
-
);
|
|
1227
|
-
const requestId = Date.now().toString(36) + Math.random().toString(36).slice(2);
|
|
1228
|
-
pendingRenders.set(requestId, {
|
|
1229
|
-
script: a11yScript,
|
|
1230
|
-
viewport: { width: 800, height: 600 }
|
|
1231
|
-
});
|
|
1232
|
-
try {
|
|
1233
|
-
const auditResult = await captureA11yAudit(
|
|
1234
|
-
`http://localhost:${port}/fragments/__render__/${requestId}`,
|
|
1235
|
-
{ width: 800, height: 600 }
|
|
1236
|
-
);
|
|
1237
|
-
let critical = 0;
|
|
1238
|
-
let serious = 0;
|
|
1239
|
-
let moderate = 0;
|
|
1240
|
-
let minor = 0;
|
|
1241
|
-
for (const violation of auditResult.violations ?? []) {
|
|
1242
|
-
switch (violation.impact) {
|
|
1243
|
-
case "critical":
|
|
1244
|
-
critical++;
|
|
1245
|
-
break;
|
|
1246
|
-
case "serious":
|
|
1247
|
-
serious++;
|
|
1248
|
-
break;
|
|
1249
|
-
case "moderate":
|
|
1250
|
-
moderate++;
|
|
1251
|
-
break;
|
|
1252
|
-
case "minor":
|
|
1253
|
-
minor++;
|
|
1254
|
-
break;
|
|
1255
|
-
}
|
|
1256
|
-
}
|
|
1257
|
-
results.push({
|
|
1258
|
-
variant: vName,
|
|
1259
|
-
violations: auditResult.violations?.length ?? 0,
|
|
1260
|
-
passes: auditResult.passes?.length ?? 0,
|
|
1261
|
-
incomplete: auditResult.incomplete?.length ?? 0,
|
|
1262
|
-
summary: {
|
|
1263
|
-
total: critical + serious + moderate + minor,
|
|
1264
|
-
critical,
|
|
1265
|
-
serious,
|
|
1266
|
-
moderate,
|
|
1267
|
-
minor
|
|
1268
|
-
},
|
|
1269
|
-
violationDetails: (auditResult.violations ?? []).map((v) => ({
|
|
1270
|
-
id: v.id,
|
|
1271
|
-
impact: v.impact,
|
|
1272
|
-
description: v.description,
|
|
1273
|
-
helpUrl: v.helpUrl,
|
|
1274
|
-
nodes: v.nodes.length
|
|
1275
|
-
}))
|
|
1276
|
-
});
|
|
1277
|
-
} catch (err) {
|
|
1278
|
-
results.push({
|
|
1279
|
-
variant: vName,
|
|
1280
|
-
violations: 0,
|
|
1281
|
-
passes: 0,
|
|
1282
|
-
incomplete: 0,
|
|
1283
|
-
summary: {
|
|
1284
|
-
total: 0,
|
|
1285
|
-
critical: 0,
|
|
1286
|
-
serious: 0,
|
|
1287
|
-
moderate: 0,
|
|
1288
|
-
minor: 0
|
|
1289
|
-
}
|
|
1290
|
-
});
|
|
1291
|
-
} finally {
|
|
1292
|
-
pendingRenders.delete(requestId);
|
|
1293
|
-
}
|
|
1294
|
-
}
|
|
1295
|
-
res.setHeader("Content-Type", "application/json");
|
|
1296
|
-
res.end(JSON.stringify({ results }));
|
|
1297
|
-
} catch (error) {
|
|
1298
|
-
console.error("[Fragments] Error running a11y audit:", error);
|
|
1299
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1300
|
-
res.end(
|
|
1301
|
-
JSON.stringify({
|
|
1302
|
-
error: error instanceof Error ? error.message : "A11y audit failed"
|
|
1303
|
-
})
|
|
1304
|
-
);
|
|
1305
|
-
}
|
|
1306
|
-
return;
|
|
1307
|
-
}
|
|
1308
|
-
if (req.url === "/fragments/perf-data" && req.method === "GET") {
|
|
1309
|
-
try {
|
|
1310
|
-
const { readFile: readFileAsync } = await import("fs/promises");
|
|
1311
|
-
const { resolve: resolvePath, join: joinPath } = await import("path");
|
|
1312
|
-
const outFilePath = resolvePath(
|
|
1313
|
-
projectRoot,
|
|
1314
|
-
config.outFile ?? BRAND.outFile
|
|
1315
|
-
);
|
|
1316
|
-
const raw = await readFileAsync(outFilePath, "utf-8");
|
|
1317
|
-
const parsed = JSON.parse(raw);
|
|
1318
|
-
const components = {};
|
|
1319
|
-
for (const [name, frag] of Object.entries(
|
|
1320
|
-
parsed.fragments
|
|
1321
|
-
)) {
|
|
1322
|
-
if (frag.performance) {
|
|
1323
|
-
components[name] = frag.performance;
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
1326
|
-
res.setHeader("Content-Type", "application/json");
|
|
1327
|
-
res.end(
|
|
1328
|
-
JSON.stringify({
|
|
1329
|
-
summary: parsed.performanceSummary ?? null,
|
|
1330
|
-
components
|
|
1331
|
-
})
|
|
1332
|
-
);
|
|
1333
|
-
} catch {
|
|
1334
|
-
res.setHeader("Content-Type", "application/json");
|
|
1335
|
-
res.end(JSON.stringify({ summary: null, components: {} }));
|
|
1336
|
-
}
|
|
1337
|
-
return;
|
|
1338
|
-
}
|
|
1339
|
-
if (req.url?.startsWith("/fragments/preview")) {
|
|
1340
|
-
if (req.url === "/fragments/preview") {
|
|
1341
|
-
res.writeHead(302, { Location: "/fragments/preview/" });
|
|
1342
|
-
res.end();
|
|
1343
|
-
return;
|
|
1344
|
-
}
|
|
1345
|
-
await servePreviewFrameHTML(res, _server);
|
|
1346
|
-
return;
|
|
1347
|
-
}
|
|
1348
|
-
if (req.url === "/fragments" || req.url === "/fragments/") {
|
|
1349
|
-
if (!req.url.endsWith("/")) {
|
|
1350
|
-
res.writeHead(302, { Location: "/fragments/" });
|
|
1351
|
-
res.end();
|
|
1352
|
-
return;
|
|
1353
|
-
}
|
|
1354
|
-
serveViewerHTML(res, _server);
|
|
1355
|
-
return;
|
|
1356
|
-
}
|
|
1357
|
-
next();
|
|
1358
|
-
});
|
|
1359
|
-
_server.httpServer?.once("listening", () => {
|
|
1360
|
-
const address = _server.httpServer?.address();
|
|
1361
|
-
const port = typeof address === "object" && address ? address.port : 6006;
|
|
1362
|
-
console.log(
|
|
1363
|
-
`
|
|
1364
|
-
\u{1F4E6} Fragments Viewer: http://localhost:${port}/fragments/
|
|
1365
|
-
`
|
|
1366
|
-
);
|
|
1367
|
-
});
|
|
1368
|
-
},
|
|
1369
|
-
// Resolve virtual modules
|
|
1370
|
-
resolveId(id) {
|
|
1371
|
-
if (id === VIRTUAL_FRAGMENTS) {
|
|
1372
|
-
return VIRTUAL_FRAGMENTS_RESOLVED;
|
|
1373
|
-
}
|
|
1374
|
-
if (id === VIRTUAL_VIEWER_ENTRY) {
|
|
1375
|
-
return VIRTUAL_VIEWER_ENTRY_RESOLVED;
|
|
1376
|
-
}
|
|
1377
|
-
if (id === VIRTUAL_PREVIEW) {
|
|
1378
|
-
return VIRTUAL_PREVIEW_RESOLVED;
|
|
1379
|
-
}
|
|
1380
|
-
return null;
|
|
1381
|
-
},
|
|
1382
|
-
// Load virtual modules
|
|
1383
|
-
load(id) {
|
|
1384
|
-
if (id === VIRTUAL_FRAGMENTS_RESOLVED) {
|
|
1385
|
-
return generateFragmentsModule(fragmentFiles, config, previewConfigPath, projectRoot);
|
|
1386
|
-
}
|
|
1387
|
-
if (id === VIRTUAL_VIEWER_ENTRY_RESOLVED) {
|
|
1388
|
-
return generateViewerEntry();
|
|
1389
|
-
}
|
|
1390
|
-
if (id === VIRTUAL_PREVIEW_RESOLVED) {
|
|
1391
|
-
return generatePreviewModule(previewConfigPath);
|
|
1392
|
-
}
|
|
1393
|
-
return null;
|
|
1394
|
-
},
|
|
1395
|
-
// Handle HMR for fragment files
|
|
1396
|
-
handleHotUpdate({ file, server: server2 }) {
|
|
1397
|
-
if (fragmentFileSet.has(file)) {
|
|
1398
|
-
const mod = server2.moduleGraph.getModuleById(VIRTUAL_FRAGMENTS_RESOLVED);
|
|
1399
|
-
if (mod) {
|
|
1400
|
-
server2.moduleGraph.invalidateModule(mod);
|
|
1401
|
-
}
|
|
1402
|
-
server2.ws.send({
|
|
1403
|
-
type: "custom",
|
|
1404
|
-
event: "fragments:update",
|
|
1405
|
-
data: { file }
|
|
1406
|
-
});
|
|
1407
|
-
return [];
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
};
|
|
1411
|
-
const jsxTransformPlugin = {
|
|
1412
|
-
name: "fragments-jsx-transform",
|
|
1413
|
-
enforce: "pre",
|
|
1414
|
-
async load(id) {
|
|
1415
|
-
if (!id.endsWith(".js")) return null;
|
|
1416
|
-
if (!id.includes(".storybook")) return null;
|
|
1417
|
-
const fs = await import("fs/promises");
|
|
1418
|
-
let code;
|
|
1419
|
-
try {
|
|
1420
|
-
code = await fs.readFile(id, "utf-8");
|
|
1421
|
-
} catch {
|
|
1422
|
-
return null;
|
|
1423
|
-
}
|
|
1424
|
-
const hasOpeningTag = code.includes("<");
|
|
1425
|
-
const hasSelfClosingTag = code.includes("/>");
|
|
1426
|
-
const hasClosingTag = code.includes("</");
|
|
1427
|
-
if (!hasOpeningTag || !hasSelfClosingTag && !hasClosingTag) return null;
|
|
1428
|
-
try {
|
|
1429
|
-
const result = await transform(code, {
|
|
1430
|
-
loader: "jsx",
|
|
1431
|
-
jsx: "automatic",
|
|
1432
|
-
sourcefile: id,
|
|
1433
|
-
sourcemap: true
|
|
1434
|
-
});
|
|
1435
|
-
return {
|
|
1436
|
-
code: result.code,
|
|
1437
|
-
map: result.map
|
|
1438
|
-
};
|
|
1439
|
-
} catch (error) {
|
|
1440
|
-
console.warn(`[Fragments] JSX transform failed for ${id}:`, error instanceof Error ? error.message : error);
|
|
1441
|
-
return null;
|
|
1442
|
-
}
|
|
1443
|
-
}
|
|
1444
|
-
};
|
|
1445
|
-
return [
|
|
1446
|
-
// JSX transform for .js files (must run first)
|
|
1447
|
-
jsxTransformPlugin,
|
|
1448
|
-
// SVGR plugin to handle `import { ReactComponent } from "*.svg"` pattern
|
|
1449
|
-
svgr({
|
|
1450
|
-
svgrOptions: {
|
|
1451
|
-
exportType: "named"
|
|
1452
|
-
// Export as { ReactComponent }
|
|
1453
|
-
},
|
|
1454
|
-
include: "**/*.svg"
|
|
1455
|
-
}),
|
|
1456
|
-
// Main fragments plugin
|
|
1457
|
-
mainPlugin
|
|
1458
|
-
];
|
|
1459
|
-
}
|
|
1460
|
-
function isStoryFile(filePath) {
|
|
1461
|
-
return /\.stories\.(tsx?|jsx?)$/.test(filePath);
|
|
1462
|
-
}
|
|
1463
|
-
function getBaseComponentPath(filePath) {
|
|
1464
|
-
return filePath.replace(/\.(fragment|stories)\.(tsx?|jsx?)$/, "");
|
|
1465
|
-
}
|
|
1466
|
-
function extractComponentName(filePath) {
|
|
1467
|
-
const match = filePath.match(/([^/\\]+)\.(fragment|stories)\.(tsx?|jsx?)$/);
|
|
1468
|
-
return match ? match[1] : filePath.split("/").pop() || filePath;
|
|
1469
|
-
}
|
|
1470
|
-
async function readProjectPackageName(root) {
|
|
1471
|
-
try {
|
|
1472
|
-
const pkgJson = JSON.parse(await readFile(resolve(root, "package.json"), "utf-8"));
|
|
1473
|
-
return pkgJson.name || null;
|
|
1474
|
-
} catch {
|
|
1475
|
-
return null;
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1478
|
-
async function extractDependenciesFromSource(absolutePath) {
|
|
1479
|
-
try {
|
|
1480
|
-
const source = await readFile(absolutePath, "utf-8");
|
|
1481
|
-
const depsMatch = source.match(/dependencies:\s*\[([\s\S]*?)\]/);
|
|
1482
|
-
if (!depsMatch) return [];
|
|
1483
|
-
const names = [];
|
|
1484
|
-
const nameRegex = /name:\s*['"]([^'"]+)['"]/g;
|
|
1485
|
-
let match;
|
|
1486
|
-
while ((match = nameRegex.exec(depsMatch[1])) !== null) {
|
|
1487
|
-
names.push(match[1]);
|
|
1488
|
-
}
|
|
1489
|
-
return names;
|
|
1490
|
-
} catch {
|
|
1491
|
-
return [];
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
async function generateFragmentsModule(fragmentFiles, config, previewConfigPath, projectRoot) {
|
|
1495
|
-
const authoredVariantCodeCache = /* @__PURE__ */ new Map();
|
|
1496
|
-
async function loadAuthoredVariantCode(fragmentFilePath) {
|
|
1497
|
-
if (authoredVariantCodeCache.has(fragmentFilePath)) {
|
|
1498
|
-
return authoredVariantCodeCache.get(fragmentFilePath);
|
|
1499
|
-
}
|
|
1500
|
-
try {
|
|
1501
|
-
const source = await readFile(fragmentFilePath, "utf-8");
|
|
1502
|
-
const parsed = parseFragmentFile(source, fragmentFilePath);
|
|
1503
|
-
const variantCodeByName = {};
|
|
1504
|
-
for (const variant of parsed.variants) {
|
|
1505
|
-
if (typeof variant.code !== "string") continue;
|
|
1506
|
-
const normalized = variant.code.trim();
|
|
1507
|
-
if (normalized.length === 0) continue;
|
|
1508
|
-
variantCodeByName[variant.name] = normalized;
|
|
1509
|
-
}
|
|
1510
|
-
authoredVariantCodeCache.set(fragmentFilePath, variantCodeByName);
|
|
1511
|
-
return variantCodeByName;
|
|
1512
|
-
} catch {
|
|
1513
|
-
authoredVariantCodeCache.set(fragmentFilePath, {});
|
|
1514
|
-
return {};
|
|
1515
|
-
}
|
|
1516
|
-
}
|
|
1517
|
-
const storyOnlyFiles = fragmentFiles.filter((f) => isStoryFile(f.relativePath));
|
|
1518
|
-
const { detectSubComponentPaths: _detectSubs } = await import("./core/index.js");
|
|
1519
|
-
const subComponentMap = _detectSubs(storyOnlyFiles);
|
|
1520
|
-
const filesByBasePath = /* @__PURE__ */ new Map();
|
|
1521
|
-
for (const file of fragmentFiles) {
|
|
1522
|
-
const basePath = getBaseComponentPath(file.relativePath);
|
|
1523
|
-
const isStory = isStoryFile(file.relativePath);
|
|
1524
|
-
const existing = filesByBasePath.get(basePath) || {};
|
|
1525
|
-
if (isStory) {
|
|
1526
|
-
existing.storyFile = file;
|
|
1527
|
-
} else {
|
|
1528
|
-
existing.fragmentFile = file;
|
|
1529
|
-
}
|
|
1530
|
-
filesByBasePath.set(basePath, existing);
|
|
1531
|
-
}
|
|
1532
|
-
const loaderEntries = await Promise.all(
|
|
1533
|
-
Array.from(filesByBasePath.values()).map(async (files) => {
|
|
1534
|
-
const primaryFile = files.storyFile || files.fragmentFile;
|
|
1535
|
-
if (!primaryFile) return null;
|
|
1536
|
-
const isStory = !!files.storyFile;
|
|
1537
|
-
const metadataPath = files.storyFile && files.fragmentFile ? files.fragmentFile.absolutePath : null;
|
|
1538
|
-
const codeSourceFile = files.fragmentFile && files.fragmentFile.absolutePath ? files.fragmentFile.absolutePath : !isStory ? primaryFile.absolutePath : null;
|
|
1539
|
-
const authoredVariantCode = codeSourceFile ? await loadAuthoredVariantCode(codeSourceFile) : {};
|
|
1540
|
-
const componentName = extractComponentName(primaryFile.relativePath);
|
|
1541
|
-
const fragmentSource = files.fragmentFile || primaryFile;
|
|
1542
|
-
const dependencies = await extractDependenciesFromSource(fragmentSource.absolutePath);
|
|
1543
|
-
const parentComponent = isStory ? subComponentMap.get(primaryFile.relativePath) : void 0;
|
|
1544
|
-
return ` {
|
|
1545
|
-
path: "${primaryFile.relativePath}",
|
|
1546
|
-
isStory: ${isStory},
|
|
1547
|
-
componentName: ${JSON.stringify(componentName)},
|
|
1548
|
-
dependencies: ${JSON.stringify(dependencies)},
|
|
1549
|
-
isSubComponent: ${!!parentComponent},
|
|
1550
|
-
parentComponent: ${parentComponent ? JSON.stringify(parentComponent) : "null"},
|
|
1551
|
-
loader: () => import("${primaryFile.absolutePath}"),
|
|
1552
|
-
metadataLoader: ${metadataPath ? `() => import("${metadataPath}")` : "null"},
|
|
1553
|
-
authoredVariantCode: ${JSON.stringify(authoredVariantCode)}
|
|
1554
|
-
}`;
|
|
1555
|
-
})
|
|
1556
|
-
);
|
|
1557
|
-
const loaders = loaderEntries.filter(Boolean).join(",\n");
|
|
1558
|
-
const previewImport = previewConfigPath ? `import * as previewConfig from "virtual:fragments-preview";` : "";
|
|
1559
|
-
const previewSetup = previewConfigPath ? `
|
|
1560
|
-
// Set global preview config before loading fragments
|
|
1561
|
-
setPreviewConfig({
|
|
1562
|
-
decorators: previewConfig.decorators,
|
|
1563
|
-
parameters: previewConfig.parameters,
|
|
1564
|
-
globalTypes: previewConfig.globalTypes,
|
|
1565
|
-
args: previewConfig.args,
|
|
1566
|
-
argTypes: previewConfig.argTypes,
|
|
1567
|
-
loaders: previewConfig.loaders,
|
|
1568
|
-
});
|
|
1569
|
-
` : "";
|
|
1570
|
-
const storybookFilterConfig = JSON.stringify(config.storybook ?? {});
|
|
1571
|
-
return `
|
|
1572
|
-
import { storyModuleToFragment, setPreviewConfig, checkStoryExclusion, isForceIncluded, isConfigExcluded } from "@fragments-sdk/cli/core";
|
|
1573
|
-
${previewImport}
|
|
1574
|
-
${previewSetup}
|
|
1575
|
-
// Storybook filter config (deep-merged with defaults at build time)
|
|
1576
|
-
const storybookFilterConfig = ${storybookFilterConfig};
|
|
1577
|
-
|
|
1578
|
-
// Lazy fragment loaders (supports both .fragment.tsx and .stories.tsx)
|
|
1579
|
-
const fragmentLoaders = [
|
|
1580
|
-
${loaders}
|
|
1581
|
-
];
|
|
1582
|
-
|
|
1583
|
-
// Cache for loaded fragments
|
|
1584
|
-
const loadedFragments = new Map();
|
|
1585
|
-
|
|
1586
|
-
/**
|
|
1587
|
-
* Merge metadata from a fragment file into a story-based fragment.
|
|
1588
|
-
* This preserves Figma URLs and other AI-agent focused data.
|
|
1589
|
-
*/
|
|
1590
|
-
function mergeMetadata(fragment, metadataModule) {
|
|
1591
|
-
if (!metadataModule?.default) return fragment;
|
|
1592
|
-
|
|
1593
|
-
const metadata = metadataModule.default;
|
|
1594
|
-
|
|
1595
|
-
// Merge meta-level Figma URL
|
|
1596
|
-
if (metadata.meta?.figma && !fragment.meta.figma) {
|
|
1597
|
-
fragment.meta.figma = metadata.meta.figma;
|
|
1598
|
-
}
|
|
1599
|
-
|
|
1600
|
-
// Merge description if not present
|
|
1601
|
-
if (metadata.meta?.description && !fragment.meta.description) {
|
|
1602
|
-
fragment.meta.description = metadata.meta.description;
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
|
-
// Merge variant-level Figma URLs
|
|
1606
|
-
if (metadata.variants && fragment.variants) {
|
|
1607
|
-
for (const metaVariant of metadata.variants) {
|
|
1608
|
-
const fragmentVariant = fragment.variants.find(v => v.name === metaVariant.name);
|
|
1609
|
-
if (!fragmentVariant) continue;
|
|
1610
|
-
|
|
1611
|
-
// Use authored code snippets from fragment metadata when story variants
|
|
1612
|
-
// don't define their own code.
|
|
1613
|
-
if (metaVariant.code && !fragmentVariant.code) {
|
|
1614
|
-
fragmentVariant.code = metaVariant.code;
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
|
-
if (metaVariant.figma && !fragmentVariant.figma) {
|
|
1618
|
-
fragmentVariant.figma = metaVariant.figma;
|
|
1619
|
-
}
|
|
1620
|
-
}
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
return fragment;
|
|
1624
|
-
}
|
|
1625
|
-
|
|
1626
|
-
function mergeAuthoredVariantCode(fragment, authoredVariantCode) {
|
|
1627
|
-
if (!fragment?.variants || !authoredVariantCode) return fragment;
|
|
1628
|
-
|
|
1629
|
-
for (const variant of fragment.variants) {
|
|
1630
|
-
if (!variant || variant.code) continue;
|
|
1631
|
-
const code = authoredVariantCode[variant.name];
|
|
1632
|
-
if (typeof code === "string" && code.trim().length > 0) {
|
|
1633
|
-
variant.code = code;
|
|
1634
|
-
}
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
|
-
return fragment;
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
|
-
// Diagnostics: track exclusions for FRAGMENTS_DEBUG
|
|
1641
|
-
const exclusionLog = [];
|
|
1642
|
-
|
|
1643
|
-
/**
|
|
1644
|
-
* Try to load a paired .fragment.tsx file as fallback when a story is excluded.
|
|
1645
|
-
* Returns the fragment if available, null otherwise.
|
|
1646
|
-
*/
|
|
1647
|
-
async function tryFallbackFragment(loader) {
|
|
1648
|
-
if (!loader.metadataLoader) return null;
|
|
1649
|
-
try {
|
|
1650
|
-
const mod = await loader.metadataLoader();
|
|
1651
|
-
return mod?.default ? { path: loader.path, fragment: mod.default } : null;
|
|
1652
|
-
} catch { return null; }
|
|
1653
|
-
}
|
|
1654
|
-
|
|
1655
|
-
// Load all fragments (for initial render)
|
|
1656
|
-
// Gracefully handles individual failures - one bad story won't break all fragments
|
|
1657
|
-
// Applies smart filtering for Storybook stories (SVG icons, deprecated, test stories, sub-components)
|
|
1658
|
-
export async function loadAllFragments() {
|
|
1659
|
-
const results = await Promise.all(
|
|
1660
|
-
fragmentLoaders.map(async (loader) => {
|
|
1661
|
-
try {
|
|
1662
|
-
if (loadedFragments.has(loader.path)) {
|
|
1663
|
-
const cached = loadedFragments.get(loader.path);
|
|
1664
|
-
return cached ? { path: loader.path, fragment: cached } : null;
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
// --- Pre-load filtering (config-level and sub-component checks) ---
|
|
1668
|
-
if (loader.isStory) {
|
|
1669
|
-
// Force-include bypasses all filters
|
|
1670
|
-
if (!isForceIncluded(loader.componentName, storybookFilterConfig)) {
|
|
1671
|
-
// Config explicit exclude
|
|
1672
|
-
if (isConfigExcluded(loader.componentName, storybookFilterConfig)) {
|
|
1673
|
-
exclusionLog.push({ component: loader.componentName, reason: 'config-excluded', detail: 'Matches storybook.exclude pattern', path: loader.path });
|
|
1674
|
-
const fallback = await tryFallbackFragment(loader);
|
|
1675
|
-
if (fallback) return fallback;
|
|
1676
|
-
loadedFragments.set(loader.path, null);
|
|
1677
|
-
return null;
|
|
1678
|
-
}
|
|
1679
|
-
|
|
1680
|
-
// Sub-component check (directory-based, computed at build time)
|
|
1681
|
-
if (loader.isSubComponent && storybookFilterConfig.excludeSubComponents !== false) {
|
|
1682
|
-
exclusionLog.push({ component: loader.componentName, reason: 'sub-component', detail: 'Sub-component of ' + loader.parentComponent, path: loader.path });
|
|
1683
|
-
const fallback = await tryFallbackFragment(loader);
|
|
1684
|
-
if (fallback) return fallback;
|
|
1685
|
-
loadedFragments.set(loader.path, null);
|
|
1686
|
-
return null;
|
|
1687
|
-
}
|
|
1688
|
-
}
|
|
1689
|
-
}
|
|
1690
|
-
|
|
1691
|
-
const module = await loader.loader();
|
|
1692
|
-
|
|
1693
|
-
// Convert story modules to fragments at runtime
|
|
1694
|
-
let fragment;
|
|
1695
|
-
if (loader.isStory) {
|
|
1696
|
-
fragment = storyModuleToFragment(module, loader.path);
|
|
1697
|
-
// storyModuleToFragment returns null for stories without a component
|
|
1698
|
-
if (!fragment) {
|
|
1699
|
-
loadedFragments.set(loader.path, null);
|
|
1700
|
-
return null;
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
// --- Post-load filtering (needs loaded module data) ---
|
|
1704
|
-
if (!isForceIncluded(loader.componentName, storybookFilterConfig)) {
|
|
1705
|
-
const meta = module.default || {};
|
|
1706
|
-
const exclusion = checkStoryExclusion({
|
|
1707
|
-
storybookTitle: meta.title,
|
|
1708
|
-
componentName: fragment.meta?.name || loader.componentName,
|
|
1709
|
-
componentDisplayName: meta.component?.displayName,
|
|
1710
|
-
componentFunctionName: meta.component?.name,
|
|
1711
|
-
tags: meta.tags,
|
|
1712
|
-
variantCount: fragment.variants?.length || 0,
|
|
1713
|
-
filePath: loader.path,
|
|
1714
|
-
config: storybookFilterConfig,
|
|
1715
|
-
});
|
|
1716
|
-
|
|
1717
|
-
if (exclusion.excluded) {
|
|
1718
|
-
exclusionLog.push({ component: fragment.meta?.name || loader.componentName, reason: exclusion.reason, detail: exclusion.detail, path: loader.path });
|
|
1719
|
-
const fallback = await tryFallbackFragment(loader);
|
|
1720
|
-
if (fallback) return fallback;
|
|
1721
|
-
loadedFragments.set(loader.path, null);
|
|
1722
|
-
return null;
|
|
1723
|
-
}
|
|
1724
|
-
}
|
|
1725
|
-
} else {
|
|
1726
|
-
fragment = module.default;
|
|
1727
|
-
}
|
|
1728
|
-
|
|
1729
|
-
// Merge metadata from corresponding fragment file if available
|
|
1730
|
-
if (loader.metadataLoader) {
|
|
1731
|
-
try {
|
|
1732
|
-
const metadataModule = await loader.metadataLoader();
|
|
1733
|
-
fragment = mergeMetadata(fragment, metadataModule);
|
|
1734
|
-
} catch (metaError) {
|
|
1735
|
-
// Metadata loading is optional - don't fail if it errors
|
|
1736
|
-
console.warn("[Fragments] Could not load metadata for " + loader.path + ":", metaError.message);
|
|
1737
|
-
}
|
|
1738
|
-
}
|
|
1739
|
-
|
|
1740
|
-
fragment = mergeAuthoredVariantCode(fragment, loader.authoredVariantCode);
|
|
1741
|
-
|
|
1742
|
-
loadedFragments.set(loader.path, fragment);
|
|
1743
|
-
return { path: loader.path, fragment };
|
|
1744
|
-
} catch (error) {
|
|
1745
|
-
console.warn("[Fragments] Failed to load " + loader.path + ":", error.message);
|
|
1746
|
-
// Create an error stub so the component still appears in the sidebar
|
|
1747
|
-
// with a helpful message about what went wrong
|
|
1748
|
-
return {
|
|
1749
|
-
path: loader.path,
|
|
1750
|
-
fragment: {
|
|
1751
|
-
meta: {
|
|
1752
|
-
name: loader.componentName || loader.path,
|
|
1753
|
-
description: 'Failed to load',
|
|
1754
|
-
category: '',
|
|
1755
|
-
},
|
|
1756
|
-
variants: [],
|
|
1757
|
-
_loadError: {
|
|
1758
|
-
message: error.message,
|
|
1759
|
-
dependencies: loader.dependencies || [],
|
|
1760
|
-
},
|
|
1761
|
-
},
|
|
1762
|
-
};
|
|
1763
|
-
}
|
|
1764
|
-
})
|
|
1765
|
-
);
|
|
1766
|
-
|
|
1767
|
-
// Log exclusions in debug mode
|
|
1768
|
-
if (exclusionLog.length > 0) {
|
|
1769
|
-
console.log("[Fragments] Filtered " + exclusionLog.length + " component(s)");
|
|
1770
|
-
if (typeof process !== 'undefined' && process.env?.FRAGMENTS_DEBUG) {
|
|
1771
|
-
console.table(exclusionLog);
|
|
1772
|
-
}
|
|
1773
|
-
}
|
|
1774
|
-
|
|
1775
|
-
// Filter out nulls (fragments that had no component)
|
|
1776
|
-
return results.filter(r => r !== null);
|
|
1777
|
-
}
|
|
1778
|
-
|
|
1779
|
-
// Load a single fragment by path
|
|
1780
|
-
export async function loadFragment(path) {
|
|
1781
|
-
const loader = fragmentLoaders.find(l => l.path === path);
|
|
1782
|
-
if (!loader) return null;
|
|
1783
|
-
|
|
1784
|
-
if (loadedFragments.has(path)) {
|
|
1785
|
-
return loadedFragments.get(path);
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
const module = await loader.loader();
|
|
1789
|
-
|
|
1790
|
-
// Convert story modules to fragments at runtime
|
|
1791
|
-
let fragment;
|
|
1792
|
-
if (loader.isStory) {
|
|
1793
|
-
fragment = storyModuleToFragment(module, path);
|
|
1794
|
-
} else {
|
|
1795
|
-
fragment = module.default;
|
|
1796
|
-
}
|
|
1797
|
-
|
|
1798
|
-
// Merge metadata from corresponding fragment file if available
|
|
1799
|
-
if (loader.metadataLoader && fragment) {
|
|
1800
|
-
try {
|
|
1801
|
-
const metadataModule = await loader.metadataLoader();
|
|
1802
|
-
fragment = mergeMetadata(fragment, metadataModule);
|
|
1803
|
-
} catch (metaError) {
|
|
1804
|
-
console.warn("[Fragments] Could not load metadata for " + path + ":", metaError.message);
|
|
1805
|
-
}
|
|
1806
|
-
}
|
|
1807
|
-
|
|
1808
|
-
if (fragment) {
|
|
1809
|
-
fragment = mergeAuthoredVariantCode(fragment, loader.authoredVariantCode);
|
|
1810
|
-
}
|
|
1811
|
-
|
|
1812
|
-
if (fragment) {
|
|
1813
|
-
loadedFragments.set(path, fragment);
|
|
1814
|
-
}
|
|
1815
|
-
return fragment;
|
|
1816
|
-
}
|
|
1817
|
-
|
|
1818
|
-
// For backwards compatibility, load all fragments synchronously on import
|
|
1819
|
-
// This is still lazy per-file but awaited at module load
|
|
1820
|
-
let fragments = [];
|
|
1821
|
-
const fragmentsPromise = loadAllFragments().then(s => { fragments = s; return s; });
|
|
1822
|
-
|
|
1823
|
-
export { fragments, fragmentsPromise, exclusionLog };
|
|
1824
|
-
export const config = ${JSON.stringify(config)};
|
|
1825
|
-
|
|
1826
|
-
// Auto-detect consumer package name from package.json
|
|
1827
|
-
export const projectPackageName = ${JSON.stringify(await readProjectPackageName(projectRoot))};
|
|
1828
|
-
|
|
1829
|
-
// HMR support
|
|
1830
|
-
if (import.meta.hot) {
|
|
1831
|
-
import.meta.hot.accept();
|
|
1832
|
-
|
|
1833
|
-
import.meta.hot.on("fragments:update", (data) => {
|
|
1834
|
-
console.log("[Fragments] File updated:", data.file);
|
|
1835
|
-
// Clear cache for the updated file (handles both .fragment and .stories)
|
|
1836
|
-
for (const [path, _] of loadedFragments) {
|
|
1837
|
-
const basePath = path.replace(/\\.(fragment|stories)\\.tsx?$/, '');
|
|
1838
|
-
if (data.file.includes(basePath)) {
|
|
1839
|
-
loadedFragments.delete(path);
|
|
1840
|
-
}
|
|
1841
|
-
}
|
|
1842
|
-
// Trigger re-render in viewer
|
|
1843
|
-
window.dispatchEvent(new CustomEvent("fragments:update"));
|
|
1844
|
-
});
|
|
1845
|
-
}
|
|
1846
|
-
`;
|
|
1847
|
-
}
|
|
1848
|
-
function generateViewerEntry() {
|
|
1849
|
-
return `
|
|
1850
|
-
import { fragments, config } from "virtual:fragments";
|
|
1851
|
-
|
|
1852
|
-
// Re-export for viewer
|
|
1853
|
-
export { fragments, config };
|
|
1854
|
-
|
|
1855
|
-
// Initialize viewer
|
|
1856
|
-
console.log("[Fragments] Loaded", fragments.length, "fragment(s)");
|
|
1857
|
-
`;
|
|
1858
|
-
}
|
|
1859
|
-
async function loadFragmentsForContext(_server, _fragmentFiles, _config, configDir) {
|
|
1860
|
-
const { join: join2 } = await import("path");
|
|
1861
|
-
const fragmentsJsonPath = join2(configDir || process.cwd(), BRAND.outFile);
|
|
1862
|
-
try {
|
|
1863
|
-
const content = await readFile(fragmentsJsonPath, "utf-8");
|
|
1864
|
-
const data = JSON.parse(content);
|
|
1865
|
-
return Object.values(data.fragments || {});
|
|
1866
|
-
} catch (error) {
|
|
1867
|
-
console.warn(
|
|
1868
|
-
`[${BRAND.name}] Failed to load ${BRAND.outFile} for context:`,
|
|
1869
|
-
error
|
|
1870
|
-
);
|
|
1871
|
-
console.warn(`[${BRAND.name}] Run '${BRAND.cliCommand} build' to generate ${BRAND.outFile}`);
|
|
1872
|
-
return [];
|
|
1873
|
-
}
|
|
1874
|
-
}
|
|
1875
|
-
async function serveViewerHTML(res, server) {
|
|
1876
|
-
const viewerSrc = viewerAssetsRoot;
|
|
1877
|
-
const viewerPkgRoot = resolve(viewerSrc, "..");
|
|
1878
|
-
const entryPath = resolve(viewerSrc, "entry.tsx");
|
|
1879
|
-
try {
|
|
1880
|
-
let html = await readFile(resolve(viewerPkgRoot, "index.html"), "utf-8");
|
|
1881
|
-
html = html.replace("/src/entry.tsx", entryPath);
|
|
1882
|
-
html = await server.transformIndexHtml("/fragments/", html);
|
|
1883
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1884
|
-
res.end(html);
|
|
1885
|
-
} catch (error) {
|
|
1886
|
-
console.error("[Fragments] Error serving viewer:", error);
|
|
1887
|
-
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
1888
|
-
res.end("Error loading Fragments viewer");
|
|
1889
|
-
}
|
|
1890
|
-
}
|
|
1891
|
-
async function servePreviewFrameHTML(res, server) {
|
|
1892
|
-
const viewerSrc = viewerAssetsRoot;
|
|
1893
|
-
const entryPath = resolve(viewerSrc, "preview-frame-entry.tsx");
|
|
1894
|
-
try {
|
|
1895
|
-
let html = await readFile(resolve(viewerSrc, "preview-frame.html"), "utf-8");
|
|
1896
|
-
html = html.replace("/src/preview-frame-entry.tsx", entryPath);
|
|
1897
|
-
html = await server.transformIndexHtml("/fragments/preview/", html);
|
|
1898
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1899
|
-
res.end(html);
|
|
1900
|
-
} catch (error) {
|
|
1901
|
-
console.error("[Fragments] Error serving preview frame:", error);
|
|
1902
|
-
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
1903
|
-
res.end("Error loading preview frame");
|
|
1904
|
-
}
|
|
1905
|
-
}
|
|
1906
|
-
async function parseJsonBody(req) {
|
|
1907
|
-
return new Promise((resolve3, reject) => {
|
|
1908
|
-
let body = "";
|
|
1909
|
-
req.on("data", (chunk) => {
|
|
1910
|
-
body += chunk.toString();
|
|
1911
|
-
});
|
|
1912
|
-
req.on("end", () => {
|
|
1913
|
-
try {
|
|
1914
|
-
resolve3(JSON.parse(body));
|
|
1915
|
-
} catch (error) {
|
|
1916
|
-
reject(new Error("Invalid JSON body"));
|
|
1917
|
-
}
|
|
1918
|
-
});
|
|
1919
|
-
req.on("error", reject);
|
|
1920
|
-
});
|
|
1921
|
-
}
|
|
1922
|
-
async function loadFragmentsForRender(fragmentFiles, configDir) {
|
|
1923
|
-
const { join: join2 } = await import("path");
|
|
1924
|
-
const fragmentsJsonPath = join2(configDir, BRAND.outFile);
|
|
1925
|
-
try {
|
|
1926
|
-
const content = await readFile(fragmentsJsonPath, "utf-8");
|
|
1927
|
-
const data = JSON.parse(content);
|
|
1928
|
-
const fragmentEntries = Object.values(data.fragments || {});
|
|
1929
|
-
if (fragmentEntries.length > 0) {
|
|
1930
|
-
return fragmentEntries.map((fragment) => ({
|
|
1931
|
-
path: fragment.filePath,
|
|
1932
|
-
fragment: { meta: { name: fragment.meta.name } }
|
|
1933
|
-
}));
|
|
1934
|
-
}
|
|
1935
|
-
} catch {
|
|
1936
|
-
}
|
|
1937
|
-
return fragmentFiles.map((f) => {
|
|
1938
|
-
let name;
|
|
1939
|
-
if (isStoryFile(f.relativePath)) {
|
|
1940
|
-
const match = f.relativePath.match(/\/([^/]+)\.stories\./);
|
|
1941
|
-
name = match ? match[1] : f.relativePath;
|
|
1942
|
-
} else {
|
|
1943
|
-
const match = f.relativePath.match(/\/([^/]+)\.fragment\./);
|
|
1944
|
-
name = match ? match[1] : f.relativePath;
|
|
1945
|
-
}
|
|
1946
|
-
return {
|
|
1947
|
-
path: f.relativePath,
|
|
1948
|
-
fragment: { meta: { name } }
|
|
1949
|
-
};
|
|
1950
|
-
});
|
|
1951
|
-
}
|
|
1952
|
-
async function loadFullFragmentData(configDir) {
|
|
1953
|
-
const { join: join2 } = await import("path");
|
|
1954
|
-
const fragmentsJsonPath = join2(configDir, BRAND.outFile);
|
|
1955
|
-
try {
|
|
1956
|
-
const content = await readFile(fragmentsJsonPath, "utf-8");
|
|
1957
|
-
return JSON.parse(content);
|
|
1958
|
-
} catch {
|
|
1959
|
-
return null;
|
|
1960
|
-
}
|
|
1961
|
-
}
|
|
1962
|
-
async function captureA11yAudit(url, viewport) {
|
|
1963
|
-
const { pool } = await getSharedRenderPool();
|
|
1964
|
-
const ctx = await pool.acquire();
|
|
1965
|
-
const page = await ctx.newPage();
|
|
1966
|
-
try {
|
|
1967
|
-
await page.setViewportSize(viewport);
|
|
1968
|
-
await page.goto(url, { waitUntil: "networkidle" });
|
|
1969
|
-
await page.waitForFunction(
|
|
1970
|
-
() => window.__RENDER_READY__ === true,
|
|
1971
|
-
{ timeout: 15e3 }
|
|
1972
|
-
);
|
|
1973
|
-
const error = await page.evaluate(() => window.__AXE_ERROR__);
|
|
1974
|
-
if (error) {
|
|
1975
|
-
throw new Error(`A11y audit error: ${error}`);
|
|
1976
|
-
}
|
|
1977
|
-
const results = await page.evaluate(() => window.__AXE_RESULTS__);
|
|
1978
|
-
if (!results) {
|
|
1979
|
-
throw new Error("Axe results not available \u2014 axe-core may not be installed");
|
|
1980
|
-
}
|
|
1981
|
-
return results;
|
|
1982
|
-
} finally {
|
|
1983
|
-
await page.close();
|
|
1984
|
-
pool.release(ctx);
|
|
1985
|
-
}
|
|
1986
|
-
}
|
|
1987
|
-
async function serveRenderHTML(res, server, renderScript) {
|
|
1988
|
-
const viewerRoot = viewerAssetsRoot;
|
|
1989
|
-
try {
|
|
1990
|
-
let html = await readFile(
|
|
1991
|
-
resolve(viewerRoot, "render-template.html"),
|
|
1992
|
-
"utf-8"
|
|
1993
|
-
);
|
|
1994
|
-
html = html.replace(
|
|
1995
|
-
"<!-- RENDER_SCRIPT_PLACEHOLDER -->",
|
|
1996
|
-
`<script type="module">${renderScript}</script>`
|
|
1997
|
-
);
|
|
1998
|
-
const uniqueUrl = `/fragments/__render__/${Date.now()}`;
|
|
1999
|
-
html = await server.transformIndexHtml(uniqueUrl, html);
|
|
2000
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
2001
|
-
res.end(html);
|
|
2002
|
-
} catch (error) {
|
|
2003
|
-
console.error("[Fragments] Error serving render page:", error);
|
|
2004
|
-
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
2005
|
-
res.end("Error loading render page");
|
|
2006
|
-
}
|
|
2007
|
-
}
|
|
2008
|
-
async function captureRender(url, viewport) {
|
|
2009
|
-
const { pool, bufferToBase64Url } = await getSharedRenderPool();
|
|
2010
|
-
const ctx = await pool.acquire();
|
|
2011
|
-
const page = await ctx.newPage();
|
|
2012
|
-
try {
|
|
2013
|
-
await page.setViewportSize(viewport);
|
|
2014
|
-
await page.goto(url, { waitUntil: "networkidle" });
|
|
2015
|
-
await page.waitForFunction(
|
|
2016
|
-
() => window.__RENDER_READY__ === true,
|
|
2017
|
-
{ timeout: 1e4 }
|
|
2018
|
-
);
|
|
2019
|
-
const error = await page.evaluate(() => window.__RENDER_ERROR__);
|
|
2020
|
-
if (error) {
|
|
2021
|
-
throw new Error(`Render error: ${error}`);
|
|
2022
|
-
}
|
|
2023
|
-
const element = await page.$("#render-root");
|
|
2024
|
-
if (!element) {
|
|
2025
|
-
throw new Error("Render root element not found");
|
|
2026
|
-
}
|
|
2027
|
-
const screenshot = await element.screenshot({ type: "png" });
|
|
2028
|
-
return bufferToBase64Url(screenshot);
|
|
2029
|
-
} finally {
|
|
2030
|
-
await page.close();
|
|
2031
|
-
pool.release(ctx);
|
|
2032
|
-
}
|
|
2033
|
-
}
|
|
2034
|
-
async function captureRenderWithStyles(url, viewport, extractStyles) {
|
|
2035
|
-
const { pool, bufferToBase64Url } = await getSharedRenderPool();
|
|
2036
|
-
const ctx = await pool.acquire();
|
|
2037
|
-
const page = await ctx.newPage();
|
|
2038
|
-
try {
|
|
2039
|
-
await page.setViewportSize(viewport);
|
|
2040
|
-
await page.goto(url, { waitUntil: "networkidle" });
|
|
2041
|
-
await page.waitForFunction(
|
|
2042
|
-
() => window.__RENDER_READY__ === true,
|
|
2043
|
-
{ timeout: 1e4 }
|
|
2044
|
-
);
|
|
2045
|
-
const error = await page.evaluate(() => window.__RENDER_ERROR__);
|
|
2046
|
-
if (error) {
|
|
2047
|
-
throw new Error(`Render error: ${error}`);
|
|
2048
|
-
}
|
|
2049
|
-
const element = await page.$("#render-root");
|
|
2050
|
-
if (!element) {
|
|
2051
|
-
throw new Error("Render root element not found");
|
|
2052
|
-
}
|
|
2053
|
-
let computedStyles = null;
|
|
2054
|
-
if (extractStyles) {
|
|
2055
|
-
computedStyles = await page.evaluate(() => {
|
|
2056
|
-
const root = document.getElementById("render-root");
|
|
2057
|
-
if (!root) return null;
|
|
2058
|
-
const isVisibleColor = (color) => {
|
|
2059
|
-
if (!color) return false;
|
|
2060
|
-
if (color === "transparent") return false;
|
|
2061
|
-
if (color === "rgba(0, 0, 0, 0)") return false;
|
|
2062
|
-
if (color.includes("rgba") && color.includes(", 0)")) return false;
|
|
2063
|
-
return true;
|
|
2064
|
-
};
|
|
2065
|
-
const extractStylesFromElement = (el) => {
|
|
2066
|
-
const styles = window.getComputedStyle(el);
|
|
2067
|
-
const relevantProps = [
|
|
2068
|
-
"backgroundColor",
|
|
2069
|
-
"borderColor",
|
|
2070
|
-
"borderWidth",
|
|
2071
|
-
"borderRadius",
|
|
2072
|
-
"fontFamily",
|
|
2073
|
-
"fontSize",
|
|
2074
|
-
"fontWeight",
|
|
2075
|
-
"lineHeight",
|
|
2076
|
-
"letterSpacing",
|
|
2077
|
-
"textAlign",
|
|
2078
|
-
"boxShadow",
|
|
2079
|
-
"padding",
|
|
2080
|
-
"paddingTop",
|
|
2081
|
-
"paddingRight",
|
|
2082
|
-
"paddingBottom",
|
|
2083
|
-
"paddingLeft",
|
|
2084
|
-
"gap",
|
|
2085
|
-
"opacity",
|
|
2086
|
-
"width",
|
|
2087
|
-
"height"
|
|
2088
|
-
];
|
|
2089
|
-
const result2 = {};
|
|
2090
|
-
for (const prop of relevantProps) {
|
|
2091
|
-
const value = styles.getPropertyValue(
|
|
2092
|
-
prop.replace(/([A-Z])/g, "-$1").toLowerCase()
|
|
2093
|
-
);
|
|
2094
|
-
if (value) {
|
|
2095
|
-
result2[prop] = value;
|
|
2096
|
-
}
|
|
2097
|
-
}
|
|
2098
|
-
return result2;
|
|
2099
|
-
};
|
|
2100
|
-
const candidates = root.querySelectorAll("*");
|
|
2101
|
-
let bestElement = null;
|
|
2102
|
-
let bestScore = -1;
|
|
2103
|
-
for (const el of candidates) {
|
|
2104
|
-
const htmlEl = el;
|
|
2105
|
-
const styles = window.getComputedStyle(htmlEl);
|
|
2106
|
-
let score = 0;
|
|
2107
|
-
const bg = styles.backgroundColor;
|
|
2108
|
-
if (isVisibleColor(bg)) {
|
|
2109
|
-
score += 10;
|
|
2110
|
-
}
|
|
2111
|
-
const border = styles.borderWidth;
|
|
2112
|
-
if (border && border !== "0px") {
|
|
2113
|
-
score += 3;
|
|
2114
|
-
}
|
|
2115
|
-
const boxShadow = styles.boxShadow;
|
|
2116
|
-
if (boxShadow && boxShadow !== "none") {
|
|
2117
|
-
score += 3;
|
|
2118
|
-
}
|
|
2119
|
-
const tagName = htmlEl.tagName.toLowerCase();
|
|
2120
|
-
if (["button", "a", "input", "select", "textarea"].includes(tagName)) {
|
|
2121
|
-
score += 5;
|
|
2122
|
-
}
|
|
2123
|
-
if (htmlEl.getAttribute("role") === "button") {
|
|
2124
|
-
score += 5;
|
|
2125
|
-
}
|
|
2126
|
-
const rect = htmlEl.getBoundingClientRect();
|
|
2127
|
-
if (rect.width < 10 || rect.height < 10) {
|
|
2128
|
-
score -= 10;
|
|
2129
|
-
}
|
|
2130
|
-
if (rect.width > 500 || rect.height > 500) {
|
|
2131
|
-
score -= 3;
|
|
2132
|
-
}
|
|
2133
|
-
if (score > bestScore) {
|
|
2134
|
-
bestScore = score;
|
|
2135
|
-
bestElement = htmlEl;
|
|
2136
|
-
}
|
|
2137
|
-
}
|
|
2138
|
-
if (!bestElement) {
|
|
2139
|
-
bestElement = root.firstElementChild;
|
|
2140
|
-
}
|
|
2141
|
-
if (!bestElement) return null;
|
|
2142
|
-
const result = extractStylesFromElement(bestElement);
|
|
2143
|
-
if (result.paddingTop && result.paddingRight && result.paddingBottom && result.paddingLeft) {
|
|
2144
|
-
const t = result.paddingTop;
|
|
2145
|
-
const r = result.paddingRight;
|
|
2146
|
-
const b = result.paddingBottom;
|
|
2147
|
-
const l = result.paddingLeft;
|
|
2148
|
-
if (t === r && r === b && b === l) {
|
|
2149
|
-
result.padding = t;
|
|
2150
|
-
} else if (t === b && r === l) {
|
|
2151
|
-
result.padding = `${t} ${r}`;
|
|
2152
|
-
} else {
|
|
2153
|
-
result.padding = `${t} ${r} ${b} ${l}`;
|
|
2154
|
-
}
|
|
2155
|
-
}
|
|
2156
|
-
return result;
|
|
2157
|
-
});
|
|
2158
|
-
}
|
|
2159
|
-
const screenshot = await element.screenshot({ type: "png" });
|
|
2160
|
-
return {
|
|
2161
|
-
screenshot: bufferToBase64Url(screenshot),
|
|
2162
|
-
computedStyles
|
|
2163
|
-
};
|
|
2164
|
-
} finally {
|
|
2165
|
-
await page.close();
|
|
2166
|
-
pool.release(ctx);
|
|
2167
|
-
}
|
|
2168
|
-
}
|
|
2169
|
-
async function loadFullFragmentForCompare(_server, _fragmentFiles, componentName, variantName, configDir) {
|
|
2170
|
-
const { join: join2 } = await import("path");
|
|
2171
|
-
const fragmentsJsonPath = join2(configDir || process.cwd(), BRAND.outFile);
|
|
2172
|
-
try {
|
|
2173
|
-
const content = await readFile(fragmentsJsonPath, "utf-8");
|
|
2174
|
-
const data = JSON.parse(content);
|
|
2175
|
-
const fragment = data.fragments[componentName];
|
|
2176
|
-
if (!fragment) {
|
|
2177
|
-
return null;
|
|
2178
|
-
}
|
|
2179
|
-
if (variantName && fragment.variants) {
|
|
2180
|
-
const variant = fragment.variants.find((v) => v.name === variantName);
|
|
2181
|
-
if (variant?.figma) {
|
|
2182
|
-
return { figmaUrl: variant.figma };
|
|
2183
|
-
}
|
|
2184
|
-
}
|
|
2185
|
-
if (fragment.meta.figma) {
|
|
2186
|
-
return { figmaUrl: fragment.meta.figma };
|
|
2187
|
-
}
|
|
2188
|
-
return null;
|
|
2189
|
-
} catch {
|
|
2190
|
-
console.warn(
|
|
2191
|
-
`[${BRAND.name}] ${BRAND.outFile} not found, run '${BRAND.cliCommand} build' first`
|
|
2192
|
-
);
|
|
2193
|
-
return null;
|
|
2194
|
-
}
|
|
2195
|
-
}
|
|
2196
|
-
async function compareImages(image1Base64, image2Base64, threshold) {
|
|
2197
|
-
const { DiffEngine, base64UrlToBuffer, bufferToBase64Url } = await import("./service-HKJ6B7P7.js");
|
|
2198
|
-
const { PNG } = await import("pngjs");
|
|
2199
|
-
const buffer1 = base64UrlToBuffer(image1Base64);
|
|
2200
|
-
const buffer2 = base64UrlToBuffer(image2Base64);
|
|
2201
|
-
const png1 = PNG.sync.read(buffer1);
|
|
2202
|
-
const png2 = PNG.sync.read(buffer2);
|
|
2203
|
-
let finalBuffer1 = buffer1;
|
|
2204
|
-
let finalBuffer2 = buffer2;
|
|
2205
|
-
if (png1.width !== png2.width || png1.height !== png2.height) {
|
|
2206
|
-
const targetWidth = Math.max(png1.width, png2.width);
|
|
2207
|
-
const targetHeight = Math.max(png1.height, png2.height);
|
|
2208
|
-
if (png1.width !== targetWidth || png1.height !== targetHeight) {
|
|
2209
|
-
finalBuffer1 = await resizePng(
|
|
2210
|
-
buffer1,
|
|
2211
|
-
png1.width,
|
|
2212
|
-
png1.height,
|
|
2213
|
-
targetWidth,
|
|
2214
|
-
targetHeight
|
|
2215
|
-
);
|
|
2216
|
-
}
|
|
2217
|
-
if (png2.width !== targetWidth || png2.height !== targetHeight) {
|
|
2218
|
-
finalBuffer2 = await resizePng(
|
|
2219
|
-
buffer2,
|
|
2220
|
-
png2.width,
|
|
2221
|
-
png2.height,
|
|
2222
|
-
targetWidth,
|
|
2223
|
-
targetHeight
|
|
2224
|
-
);
|
|
2225
|
-
}
|
|
2226
|
-
}
|
|
2227
|
-
const screenshot1 = {
|
|
2228
|
-
data: finalBuffer1,
|
|
2229
|
-
hash: "",
|
|
2230
|
-
viewport: { width: png1.width, height: png1.height },
|
|
2231
|
-
capturedAt: /* @__PURE__ */ new Date(),
|
|
2232
|
-
metadata: {
|
|
2233
|
-
component: "",
|
|
2234
|
-
variant: "",
|
|
2235
|
-
theme: "light",
|
|
2236
|
-
renderTimeMs: 0,
|
|
2237
|
-
captureTimeMs: 0
|
|
2238
|
-
}
|
|
2239
|
-
};
|
|
2240
|
-
const screenshot2 = {
|
|
2241
|
-
data: finalBuffer2,
|
|
2242
|
-
hash: "",
|
|
2243
|
-
viewport: { width: png2.width, height: png2.height },
|
|
2244
|
-
capturedAt: /* @__PURE__ */ new Date(),
|
|
2245
|
-
metadata: {
|
|
2246
|
-
component: "",
|
|
2247
|
-
variant: "",
|
|
2248
|
-
theme: "light",
|
|
2249
|
-
renderTimeMs: 0,
|
|
2250
|
-
captureTimeMs: 0
|
|
2251
|
-
}
|
|
2252
|
-
};
|
|
2253
|
-
const diffEngine = new DiffEngine(threshold);
|
|
2254
|
-
const result = diffEngine.compare(screenshot1, screenshot2, { threshold });
|
|
2255
|
-
return {
|
|
2256
|
-
matches: result.matches,
|
|
2257
|
-
diffPercentage: result.diffPercentage,
|
|
2258
|
-
diffImage: result.diffImage ? bufferToBase64Url(result.diffImage) : void 0,
|
|
2259
|
-
changedRegions: result.changedRegions
|
|
2260
|
-
};
|
|
2261
|
-
}
|
|
2262
|
-
async function resizePng(buffer, srcWidth, srcHeight, targetWidth, targetHeight) {
|
|
2263
|
-
const { PNG } = await import("pngjs");
|
|
2264
|
-
const srcPng = PNG.sync.read(buffer);
|
|
2265
|
-
const dstPng = new PNG({
|
|
2266
|
-
width: targetWidth,
|
|
2267
|
-
height: targetHeight,
|
|
2268
|
-
fill: true
|
|
2269
|
-
});
|
|
2270
|
-
for (let y = 0; y < targetHeight; y++) {
|
|
2271
|
-
for (let x = 0; x < targetWidth; x++) {
|
|
2272
|
-
const idx = (y * targetWidth + x) * 4;
|
|
2273
|
-
dstPng.data[idx] = 255;
|
|
2274
|
-
dstPng.data[idx + 1] = 255;
|
|
2275
|
-
dstPng.data[idx + 2] = 255;
|
|
2276
|
-
dstPng.data[idx + 3] = 255;
|
|
2277
|
-
}
|
|
2278
|
-
}
|
|
2279
|
-
for (let y = 0; y < srcHeight; y++) {
|
|
2280
|
-
for (let x = 0; x < srcWidth; x++) {
|
|
2281
|
-
const srcIdx = (y * srcWidth + x) * 4;
|
|
2282
|
-
const dstIdx = (y * targetWidth + x) * 4;
|
|
2283
|
-
dstPng.data[dstIdx] = srcPng.data[srcIdx];
|
|
2284
|
-
dstPng.data[dstIdx + 1] = srcPng.data[srcIdx + 1];
|
|
2285
|
-
dstPng.data[dstIdx + 2] = srcPng.data[srcIdx + 2];
|
|
2286
|
-
dstPng.data[dstIdx + 3] = srcPng.data[srcIdx + 3];
|
|
2287
|
-
}
|
|
2288
|
-
}
|
|
2289
|
-
return PNG.sync.write(dstPng);
|
|
2290
|
-
}
|
|
2291
|
-
|
|
2292
|
-
// src/viewer/server.ts
|
|
2293
|
-
var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
2294
|
-
var cliPackageRoot = resolve2(__dirname2, "..");
|
|
2295
|
-
var packagesRoot = resolve2(cliPackageRoot, "..");
|
|
2296
|
-
var localUiLibRoot = resolve2(packagesRoot, "../libs/ui/src");
|
|
2297
|
-
var localWebMCPRoot = resolve2(packagesRoot, "webmcp/src");
|
|
2298
|
-
var localContextRoot = resolve2(packagesRoot, "context/src");
|
|
2299
|
-
function resolveViewerRoot(nodeModulesDir) {
|
|
2300
|
-
const monorepoPath = resolve2(packagesRoot, "viewer");
|
|
2301
|
-
if (existsSync2(join(monorepoPath, "src/shared/index.ts"))) {
|
|
2302
|
-
return monorepoPath;
|
|
2303
|
-
}
|
|
2304
|
-
const npmPath = join(nodeModulesDir, "@fragments-sdk/viewer");
|
|
2305
|
-
if (existsSync2(npmPath)) {
|
|
2306
|
-
return npmPath;
|
|
2307
|
-
}
|
|
2308
|
-
const cliNpmPath = join(cliPackageRoot, "node_modules/@fragments-sdk/viewer");
|
|
2309
|
-
if (existsSync2(cliNpmPath)) {
|
|
2310
|
-
return cliNpmPath;
|
|
2311
|
-
}
|
|
2312
|
-
return monorepoPath;
|
|
2313
|
-
}
|
|
2314
|
-
function resolveUiLib(nodeModulesDir) {
|
|
2315
|
-
const localIndex = join(localUiLibRoot, "index.ts");
|
|
2316
|
-
if (existsSync2(localIndex)) {
|
|
2317
|
-
return localUiLibRoot;
|
|
2318
|
-
}
|
|
2319
|
-
const installedUi = join(nodeModulesDir, "@fragments-sdk/ui/src/index.ts");
|
|
2320
|
-
if (existsSync2(installedUi)) {
|
|
2321
|
-
return resolve2(dirname2(installedUi));
|
|
2322
|
-
}
|
|
2323
|
-
const installedUiDist = join(nodeModulesDir, "@fragments-sdk/ui");
|
|
2324
|
-
if (existsSync2(installedUiDist)) {
|
|
2325
|
-
return installedUiDist;
|
|
2326
|
-
}
|
|
2327
|
-
return localUiLibRoot;
|
|
2328
|
-
}
|
|
2329
|
-
function resolveWebMCPLib(nodeModulesDir) {
|
|
2330
|
-
const localIndex = join(localWebMCPRoot, "index.ts");
|
|
2331
|
-
if (existsSync2(localIndex)) {
|
|
2332
|
-
return localWebMCPRoot;
|
|
2333
|
-
}
|
|
2334
|
-
const installedSrc = join(nodeModulesDir, "@fragments-sdk/webmcp/src/index.ts");
|
|
2335
|
-
if (existsSync2(installedSrc)) {
|
|
2336
|
-
return resolve2(dirname2(installedSrc));
|
|
2337
|
-
}
|
|
2338
|
-
const installedDist = join(nodeModulesDir, "@fragments-sdk/webmcp");
|
|
2339
|
-
if (existsSync2(installedDist)) {
|
|
2340
|
-
return installedDist;
|
|
2341
|
-
}
|
|
2342
|
-
return localWebMCPRoot;
|
|
2343
|
-
}
|
|
2344
|
-
function resolveContextLib(nodeModulesDir) {
|
|
2345
|
-
const localIndex = join(localContextRoot, "index.ts");
|
|
2346
|
-
if (existsSync2(localIndex)) {
|
|
2347
|
-
return localContextRoot;
|
|
2348
|
-
}
|
|
2349
|
-
const installedSrc = join(nodeModulesDir, "@fragments-sdk/context/src/index.ts");
|
|
2350
|
-
if (existsSync2(installedSrc)) {
|
|
2351
|
-
return resolve2(dirname2(installedSrc));
|
|
2352
|
-
}
|
|
2353
|
-
const installedDist = join(nodeModulesDir, "@fragments-sdk/context");
|
|
2354
|
-
if (existsSync2(installedDist)) {
|
|
2355
|
-
return installedDist;
|
|
2356
|
-
}
|
|
2357
|
-
return localContextRoot;
|
|
2358
|
-
}
|
|
2359
|
-
function resolveViewerLib(nodeModulesDir) {
|
|
2360
|
-
const viewerRoot = resolveViewerRoot(nodeModulesDir);
|
|
2361
|
-
return resolve2(viewerRoot, "src");
|
|
2362
|
-
}
|
|
2363
|
-
function cjsInteropPlugin(nodeModulesPath) {
|
|
2364
|
-
const virtualModules = {
|
|
2365
|
-
// use-sync-external-store/shim is CJS-only, used by @base-ui/react.
|
|
2366
|
-
// Since React 18+, useSyncExternalStore is built-in — redirect to React's native export.
|
|
2367
|
-
"use-sync-external-store/shim": `export { useSyncExternalStore } from 'react';`,
|
|
2368
|
-
// use-sync-external-store/shim/with-selector is CJS-only, used by @base-ui/utils.
|
|
2369
|
-
// Provides useSyncExternalStoreWithSelector — a selector wrapper around useSyncExternalStore.
|
|
2370
|
-
"use-sync-external-store/shim/with-selector": `
|
|
2371
|
-
import { useSyncExternalStore, useRef, useEffect, useMemo, useDebugValue } from 'react';
|
|
2372
|
-
export function useSyncExternalStoreWithSelector(subscribe, getSnapshot, getServerSnapshot, selector, isEqual) {
|
|
2373
|
-
var instRef = useRef(null);
|
|
2374
|
-
if (instRef.current === null) instRef.current = { hasValue: false, value: null };
|
|
2375
|
-
var inst = instRef.current;
|
|
2376
|
-
var _useMemo = useMemo(function () {
|
|
2377
|
-
var hasMemo = false, memoizedSnapshot, memoizedSelection;
|
|
2378
|
-
function memoizedSelector(nextSnapshot) {
|
|
2379
|
-
if (!hasMemo) {
|
|
2380
|
-
hasMemo = true;
|
|
2381
|
-
memoizedSnapshot = nextSnapshot;
|
|
2382
|
-
var nextSelection = selector(nextSnapshot);
|
|
2383
|
-
if (isEqual !== undefined && inst.hasValue && isEqual(inst.value, nextSelection))
|
|
2384
|
-
return memoizedSelection = inst.value;
|
|
2385
|
-
return memoizedSelection = nextSelection;
|
|
2386
|
-
}
|
|
2387
|
-
if (Object.is(memoizedSnapshot, nextSnapshot)) return memoizedSelection;
|
|
2388
|
-
var nextSelection = selector(nextSnapshot);
|
|
2389
|
-
if (isEqual !== undefined && isEqual(memoizedSelection, nextSelection)) {
|
|
2390
|
-
memoizedSnapshot = nextSnapshot;
|
|
2391
|
-
return memoizedSelection;
|
|
2392
|
-
}
|
|
2393
|
-
memoizedSnapshot = nextSnapshot;
|
|
2394
|
-
return memoizedSelection = nextSelection;
|
|
2395
|
-
}
|
|
2396
|
-
var maybeGetServerSnapshot = getServerSnapshot === undefined ? null : getServerSnapshot;
|
|
2397
|
-
return [function () { return memoizedSelector(getSnapshot()); },
|
|
2398
|
-
maybeGetServerSnapshot === null ? undefined : function () { return memoizedSelector(maybeGetServerSnapshot()); }];
|
|
2399
|
-
}, [getSnapshot, getServerSnapshot, selector, isEqual]);
|
|
2400
|
-
var value = useSyncExternalStore(subscribe, _useMemo[0], _useMemo[1]);
|
|
2401
|
-
useEffect(function () { inst.hasValue = true; inst.value = value; }, [value]);
|
|
2402
|
-
useDebugValue(value);
|
|
2403
|
-
return value;
|
|
2404
|
-
}`
|
|
2405
|
-
};
|
|
2406
|
-
return {
|
|
2407
|
-
name: "fragments-cjs-interop",
|
|
2408
|
-
enforce: "pre",
|
|
2409
|
-
resolveId(source) {
|
|
2410
|
-
if (source in virtualModules) {
|
|
2411
|
-
return `\0cjs-interop:${source}`;
|
|
2412
|
-
}
|
|
2413
|
-
return void 0;
|
|
2414
|
-
},
|
|
2415
|
-
load(id) {
|
|
2416
|
-
if (!id.startsWith("\0cjs-interop:")) return void 0;
|
|
2417
|
-
const pkg = id.slice("\0cjs-interop:".length);
|
|
2418
|
-
return virtualModules[pkg];
|
|
2419
|
-
}
|
|
2420
|
-
};
|
|
2421
|
-
}
|
|
2422
|
-
function optionalPeerDepsPlugin(uiLibRoot) {
|
|
2423
|
-
const optionalDeps = [
|
|
2424
|
-
"@tanstack/react-table",
|
|
2425
|
-
"shiki",
|
|
2426
|
-
"recharts",
|
|
2427
|
-
"react-day-picker",
|
|
2428
|
-
"date-fns",
|
|
2429
|
-
"react-colorful",
|
|
2430
|
-
"react-markdown",
|
|
2431
|
-
"remark-gfm",
|
|
2432
|
-
"@tiptap/react",
|
|
2433
|
-
"@tiptap/starter-kit",
|
|
2434
|
-
"@tiptap/extension-link"
|
|
2435
|
-
];
|
|
2436
|
-
let resolvedUiRoot;
|
|
2437
|
-
try {
|
|
2438
|
-
resolvedUiRoot = realpathSync(uiLibRoot);
|
|
2439
|
-
} catch {
|
|
2440
|
-
resolvedUiRoot = uiLibRoot;
|
|
2441
|
-
}
|
|
2442
|
-
const availableDeps = /* @__PURE__ */ new Set();
|
|
2443
|
-
for (const dep of optionalDeps) {
|
|
2444
|
-
const depPath = join(uiLibRoot, "..", "node_modules", ...dep.split("/"));
|
|
2445
|
-
if (existsSync2(depPath)) availableDeps.add(dep);
|
|
2446
|
-
}
|
|
2447
|
-
return {
|
|
2448
|
-
name: "fragments:optional-peer-deps",
|
|
2449
|
-
enforce: "pre",
|
|
2450
|
-
transform(code, id) {
|
|
2451
|
-
let resolvedId;
|
|
2452
|
-
try {
|
|
2453
|
-
resolvedId = realpathSync(id);
|
|
2454
|
-
} catch {
|
|
2455
|
-
resolvedId = id;
|
|
2456
|
-
}
|
|
2457
|
-
if (!resolvedId.startsWith(resolvedUiRoot)) return;
|
|
2458
|
-
if (!id.endsWith(".tsx") && !id.endsWith(".ts")) return;
|
|
2459
|
-
if (!code.includes("require(")) return;
|
|
2460
|
-
let transformed = code;
|
|
2461
|
-
const imports = [];
|
|
2462
|
-
let counter = 0;
|
|
2463
|
-
for (const dep of availableDeps) {
|
|
2464
|
-
const escapedDep = dep.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2465
|
-
const regex = new RegExp(`require\\(['"]${escapedDep}['"]\\)`, "g");
|
|
2466
|
-
if (regex.test(code)) {
|
|
2467
|
-
const varName = `__fui_dep_${counter++}`;
|
|
2468
|
-
imports.push(`import * as ${varName} from '${dep}';`);
|
|
2469
|
-
regex.lastIndex = 0;
|
|
2470
|
-
transformed = transformed.replace(regex, varName);
|
|
2471
|
-
}
|
|
2472
|
-
}
|
|
2473
|
-
if (imports.length > 0) {
|
|
2474
|
-
transformed = imports.join("\n") + "\n" + transformed;
|
|
2475
|
-
return { code: transformed, map: null };
|
|
2476
|
-
}
|
|
2477
|
-
}
|
|
2478
|
-
};
|
|
2479
|
-
}
|
|
2480
|
-
async function detectTsconfigPaths(projectRoot) {
|
|
2481
|
-
const aliases = {};
|
|
2482
|
-
const tsconfigPath = join(projectRoot, "tsconfig.json");
|
|
2483
|
-
if (!existsSync2(tsconfigPath)) return aliases;
|
|
2484
|
-
try {
|
|
2485
|
-
const content = await readFile2(tsconfigPath, "utf-8");
|
|
2486
|
-
const cleaned = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
2487
|
-
const tsconfig = JSON.parse(cleaned);
|
|
2488
|
-
const paths = tsconfig?.compilerOptions?.paths;
|
|
2489
|
-
const baseUrl = tsconfig?.compilerOptions?.baseUrl || ".";
|
|
2490
|
-
if (paths) {
|
|
2491
|
-
for (const [alias, targets] of Object.entries(paths)) {
|
|
2492
|
-
const target = targets[0];
|
|
2493
|
-
if (target) {
|
|
2494
|
-
const cleanAlias = alias.replace("/*", "");
|
|
2495
|
-
const cleanTarget = target.replace("/*", "");
|
|
2496
|
-
aliases[cleanAlias] = resolve2(projectRoot, baseUrl, cleanTarget);
|
|
2497
|
-
}
|
|
2498
|
-
}
|
|
2499
|
-
}
|
|
2500
|
-
} catch {
|
|
2501
|
-
}
|
|
2502
|
-
return aliases;
|
|
2503
|
-
}
|
|
2504
|
-
async function detectStorybookAliases(projectRoot) {
|
|
2505
|
-
const aliases = {};
|
|
2506
|
-
const assetsDir = resolve2(projectRoot, "assets");
|
|
2507
|
-
const fontsDir = resolve2(projectRoot, "assets/fonts");
|
|
2508
|
-
if (existsSync2(fontsDir)) {
|
|
2509
|
-
aliases["/fonts"] = fontsDir;
|
|
2510
|
-
}
|
|
2511
|
-
if (existsSync2(assetsDir)) {
|
|
2512
|
-
aliases["/assets"] = assetsDir;
|
|
2513
|
-
}
|
|
2514
|
-
return aliases;
|
|
2515
|
-
}
|
|
2516
|
-
async function createDevServer(options = {}) {
|
|
2517
|
-
const startTime = performance.now();
|
|
2518
|
-
const {
|
|
2519
|
-
port = 6006,
|
|
2520
|
-
configPath,
|
|
2521
|
-
open = true,
|
|
2522
|
-
projectRoot = process.cwd()
|
|
2523
|
-
} = options;
|
|
2524
|
-
console.log("\n\u{1F527} Loading configuration...");
|
|
2525
|
-
const { config, configDir } = await loadConfig(configPath);
|
|
2526
|
-
const fragmentFiles = await discoverFragmentFiles(config, configDir);
|
|
2527
|
-
const installedFiles = await discoverInstalledFragments(projectRoot);
|
|
2528
|
-
const allFragmentFiles = [...fragmentFiles, ...installedFiles];
|
|
2529
|
-
console.log(`\u{1F4E6} Found ${fragmentFiles.length} local + ${installedFiles.length} installed fragment file(s)`);
|
|
2530
|
-
let projectViteConfig = {};
|
|
2531
|
-
const viteConfigPath = findViteConfig(projectRoot);
|
|
2532
|
-
if (viteConfigPath) {
|
|
2533
|
-
console.log(`\u{1F4C4} Using project Vite config: ${viteConfigPath}`);
|
|
2534
|
-
try {
|
|
2535
|
-
const loaded = await loadConfigFromFile(
|
|
2536
|
-
{ command: "serve", mode: "development" },
|
|
2537
|
-
viteConfigPath,
|
|
2538
|
-
projectRoot
|
|
2539
|
-
);
|
|
2540
|
-
if (loaded) {
|
|
2541
|
-
projectViteConfig = loaded.config;
|
|
2542
|
-
}
|
|
2543
|
-
} catch (error) {
|
|
2544
|
-
console.warn("\u26A0\uFE0F Could not load project Vite config:", error);
|
|
2545
|
-
}
|
|
2546
|
-
} else {
|
|
2547
|
-
console.log("\u2139\uFE0F No project Vite config found, using defaults");
|
|
2548
|
-
}
|
|
2549
|
-
const nodeModulesPath = findNodeModules(projectRoot);
|
|
2550
|
-
const uiLibRoot = resolveUiLib(nodeModulesPath);
|
|
2551
|
-
const viewerRoot = resolveViewerRoot(nodeModulesPath);
|
|
2552
|
-
const viewerLibRoot = resolveViewerLib(nodeModulesPath);
|
|
2553
|
-
const webmcpLibRoot = resolveWebMCPLib(nodeModulesPath);
|
|
2554
|
-
const contextLibRoot = resolveContextLib(nodeModulesPath);
|
|
2555
|
-
const isWebMCPSource = existsSync2(join(webmcpLibRoot, "react/index.ts"));
|
|
2556
|
-
const isContextSource = existsSync2(join(contextLibRoot, "types/index.ts"));
|
|
2557
|
-
console.log(`\u{1F4C1} Using node_modules: ${nodeModulesPath}`);
|
|
2558
|
-
const tsconfigAliases = await detectTsconfigPaths(projectRoot);
|
|
2559
|
-
const storybookAliases = await detectStorybookAliases(projectRoot);
|
|
2560
|
-
const storybookDir = resolve2(projectRoot, ".storybook");
|
|
2561
|
-
const hasStorybookDir = existsSync2(storybookDir);
|
|
2562
|
-
if (Object.keys(tsconfigAliases).length > 0) {
|
|
2563
|
-
console.log(`\u{1F4CE} Detected ${Object.keys(tsconfigAliases).length} tsconfig path alias(es)`);
|
|
2564
|
-
}
|
|
2565
|
-
if (hasStorybookDir) {
|
|
2566
|
-
console.log(`\u{1F4D8} Detected .storybook directory`);
|
|
2567
|
-
}
|
|
2568
|
-
const installedPkgRoots = [...new Set(
|
|
2569
|
-
installedFiles.map((f) => {
|
|
2570
|
-
const idx = f.absolutePath.indexOf("/node_modules/");
|
|
2571
|
-
if (idx === -1) return dirname2(f.absolutePath);
|
|
2572
|
-
const afterNm = f.absolutePath.slice(idx + "/node_modules/".length);
|
|
2573
|
-
const pkgName = afterNm.startsWith("@") ? afterNm.split("/").slice(0, 2).join("/") : afterNm.split("/")[0];
|
|
2574
|
-
return resolve2(projectRoot, "node_modules", pkgName);
|
|
2575
|
-
})
|
|
2576
|
-
)];
|
|
2577
|
-
const fragmentsConfig = {
|
|
2578
|
-
configFile: false,
|
|
2579
|
-
// Don't load config again
|
|
2580
|
-
root: projectRoot,
|
|
2581
|
-
// Run from PROJECT root
|
|
2582
|
-
publicDir: resolve2(viewerRoot, "public"),
|
|
2583
|
-
// Serve static assets (favicon) from viewer
|
|
2584
|
-
base: "/",
|
|
2585
|
-
server: {
|
|
2586
|
-
port,
|
|
2587
|
-
open: open ? "/fragments/" : false,
|
|
2588
|
-
fs: {
|
|
2589
|
-
// Allow serving files from viewer package, project, shared libs, node_modules root, and .storybook
|
|
2590
|
-
allow: [
|
|
2591
|
-
viewerRoot,
|
|
2592
|
-
uiLibRoot,
|
|
2593
|
-
viewerLibRoot,
|
|
2594
|
-
webmcpLibRoot,
|
|
2595
|
-
contextLibRoot,
|
|
2596
|
-
projectRoot,
|
|
2597
|
-
configDir,
|
|
2598
|
-
dirname2(nodeModulesPath),
|
|
2599
|
-
...hasStorybookDir ? [storybookDir] : [],
|
|
2600
|
-
...installedPkgRoots
|
|
2601
|
-
]
|
|
2602
|
-
}
|
|
2603
|
-
},
|
|
2604
|
-
plugins: [
|
|
2605
|
-
// React support (if not already in project config)
|
|
2606
|
-
...hasReactPlugin(projectViteConfig) ? [] : [react()],
|
|
2607
|
-
// CJS interop for packages imported from within node_modules
|
|
2608
|
-
cjsInteropPlugin(nodeModulesPath),
|
|
2609
|
-
// Rewrite require() → ESM import for optional peer deps (e.g., @tanstack/react-table)
|
|
2610
|
-
optionalPeerDepsPlugin(uiLibRoot),
|
|
2611
|
-
// Fragments plugins (array including SVGR)
|
|
2612
|
-
...fragmentsPlugin({
|
|
2613
|
-
fragmentFiles: allFragmentFiles,
|
|
2614
|
-
config,
|
|
2615
|
-
projectRoot
|
|
2616
|
-
})
|
|
2617
|
-
],
|
|
2618
|
-
// CSS configuration — preserve original hyphenated class names in CSS modules
|
|
2619
|
-
// Vite 6 defaults to camelCaseOnly, but our components use styles['gap-sm'] etc.
|
|
2620
|
-
css: {
|
|
2621
|
-
modules: {
|
|
2622
|
-
localsConvention: "camelCase"
|
|
2623
|
-
},
|
|
2624
|
-
preprocessorOptions: {
|
|
2625
|
-
scss: {
|
|
2626
|
-
api: "modern-compiler",
|
|
2627
|
-
loadPaths: [
|
|
2628
|
-
resolve2(projectRoot, "src"),
|
|
2629
|
-
resolve2(projectRoot, "src/styles")
|
|
2630
|
-
]
|
|
2631
|
-
}
|
|
2632
|
-
}
|
|
2633
|
-
},
|
|
2634
|
-
optimizeDeps: {
|
|
2635
|
-
// Include common dependencies for faster startup
|
|
2636
|
-
include: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime"]
|
|
2637
|
-
},
|
|
2638
|
-
// Ensure we can resolve viewer's dependencies
|
|
2639
|
-
resolve: {
|
|
2640
|
-
// Dedupe ensures all imports of these packages resolve to the same copy
|
|
2641
|
-
dedupe: ["react", "react-dom"],
|
|
2642
|
-
alias: {
|
|
2643
|
-
// Project-specific aliases (tsconfig paths, Storybook static dirs)
|
|
2644
|
-
// Listed first — Fragments-specific aliases below take precedence
|
|
2645
|
-
...tsconfigAliases,
|
|
2646
|
-
...storybookAliases,
|
|
2647
|
-
// Resolve @fragments-sdk/ui to local source or installed package
|
|
2648
|
-
"@fragments-sdk/ui": uiLibRoot,
|
|
2649
|
-
// Resolve @fragments-sdk/viewer subpaths to viewer package source
|
|
2650
|
-
"@fragments-sdk/viewer/shared": join(viewerLibRoot, "shared/index.ts"),
|
|
2651
|
-
"@fragments-sdk/viewer/app": join(viewerLibRoot, "app/index.ts"),
|
|
2652
|
-
"@fragments-sdk/viewer/docs-data": join(viewerLibRoot, "shared/docs-data/index.ts"),
|
|
2653
|
-
// Resolve @fragments-sdk/webmcp subpaths to monorepo source or installed package
|
|
2654
|
-
"@fragments-sdk/webmcp/react": isWebMCPSource ? join(webmcpLibRoot, "react/index.ts") : join(webmcpLibRoot, "dist/react/index.js"),
|
|
2655
|
-
"@fragments-sdk/webmcp/fragments": isWebMCPSource ? join(webmcpLibRoot, "fragments/index.ts") : join(webmcpLibRoot, "dist/fragments/index.js"),
|
|
2656
|
-
"@fragments-sdk/webmcp": webmcpLibRoot,
|
|
2657
|
-
// Resolve @fragments-sdk/context subpaths to monorepo source or installed package
|
|
2658
|
-
"@fragments-sdk/context/types": isContextSource ? join(contextLibRoot, "types/index.ts") : join(contextLibRoot, "dist/types/index.js"),
|
|
2659
|
-
"@fragments-sdk/context/mcp-tools": isContextSource ? join(contextLibRoot, "mcp-tools/index.ts") : join(contextLibRoot, "dist/mcp-tools/index.js"),
|
|
2660
|
-
"@fragments-sdk/context": contextLibRoot,
|
|
2661
|
-
// Resolve @fragments-sdk/cli/core to the CLI's own core source
|
|
2662
|
-
"@fragments-sdk/cli/core": resolve2(cliPackageRoot, "src/core/index.ts"),
|
|
2663
|
-
// Ensure ALL react imports resolve to project's node_modules
|
|
2664
|
-
// This is critical for viewer files loaded from outside project root
|
|
2665
|
-
"react": safeRealpath(join(nodeModulesPath, "react")),
|
|
2666
|
-
"react-dom": safeRealpath(join(nodeModulesPath, "react-dom")),
|
|
2667
|
-
"react/jsx-runtime": safeRealpath(join(nodeModulesPath, "react/jsx-runtime")),
|
|
2668
|
-
"react/jsx-dev-runtime": safeRealpath(join(nodeModulesPath, "react/jsx-dev-runtime"))
|
|
2669
|
-
}
|
|
2670
|
-
}
|
|
2671
|
-
};
|
|
2672
|
-
const mergedConfig = mergeConfig(projectViteConfig, fragmentsConfig);
|
|
2673
|
-
console.log("\u{1F680} Starting dev server...\n");
|
|
2674
|
-
const server = await createServer(mergedConfig);
|
|
2675
|
-
await server.listen();
|
|
2676
|
-
const startupTime = ((performance.now() - startTime) / 1e3).toFixed(2);
|
|
2677
|
-
console.log(`\u26A1 Server ready in ${startupTime}s`);
|
|
2678
|
-
return server;
|
|
2679
|
-
}
|
|
2680
|
-
function safeRealpath(p) {
|
|
2681
|
-
try {
|
|
2682
|
-
return realpathSync(p);
|
|
2683
|
-
} catch {
|
|
2684
|
-
return p;
|
|
2685
|
-
}
|
|
2686
|
-
}
|
|
2687
|
-
function findNodeModules(startDir) {
|
|
2688
|
-
let current = startDir;
|
|
2689
|
-
while (current !== dirname2(current)) {
|
|
2690
|
-
const nodeModulesPath = join(current, "node_modules");
|
|
2691
|
-
if (existsSync2(join(nodeModulesPath, "react"))) {
|
|
2692
|
-
return nodeModulesPath;
|
|
2693
|
-
}
|
|
2694
|
-
current = dirname2(current);
|
|
2695
|
-
}
|
|
2696
|
-
return join(startDir, "node_modules");
|
|
2697
|
-
}
|
|
2698
|
-
function findViteConfig(projectRoot) {
|
|
2699
|
-
const configFiles = [
|
|
2700
|
-
"vite.config.ts",
|
|
2701
|
-
"vite.config.js",
|
|
2702
|
-
"vite.config.mts",
|
|
2703
|
-
"vite.config.mjs"
|
|
2704
|
-
];
|
|
2705
|
-
for (const file of configFiles) {
|
|
2706
|
-
const path = join(projectRoot, file);
|
|
2707
|
-
if (existsSync2(path)) {
|
|
2708
|
-
return path;
|
|
2709
|
-
}
|
|
2710
|
-
}
|
|
2711
|
-
return null;
|
|
2712
|
-
}
|
|
2713
|
-
function hasReactPlugin(config) {
|
|
2714
|
-
if (!config.plugins) return false;
|
|
2715
|
-
const plugins = Array.isArray(config.plugins) ? config.plugins : [config.plugins];
|
|
2716
|
-
return plugins.some((plugin) => {
|
|
2717
|
-
if (!plugin) return false;
|
|
2718
|
-
if (Array.isArray(plugin)) {
|
|
2719
|
-
return plugin.some(
|
|
2720
|
-
(p) => p && typeof p === "object" && "name" in p && p.name?.includes("react")
|
|
2721
|
-
);
|
|
2722
|
-
}
|
|
2723
|
-
return typeof plugin === "object" && "name" in plugin && plugin.name?.includes("react");
|
|
2724
|
-
});
|
|
2725
|
-
}
|
|
2726
|
-
export {
|
|
2727
|
-
createDevServer,
|
|
2728
|
-
fragmentsPlugin
|
|
2729
|
-
};
|
|
2730
|
-
//# sourceMappingURL=viewer-2TZS3NDL.js.map
|