@fragments-sdk/cli 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +77 -14
- package/dist/bin.js +247 -247
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-CVXKXVOY.js → chunk-3T6QL7IY.js} +47 -29
- package/dist/chunk-3T6QL7IY.js.map +1 -0
- package/dist/{chunk-7OPWMLOE.js → chunk-7KUSBMI4.js} +114 -112
- package/dist/chunk-7KUSBMI4.js.map +1 -0
- package/dist/{chunk-XHUDJNN3.js → chunk-DH4ETVSM.js} +18 -18
- package/dist/chunk-DH4ETVSM.js.map +1 -0
- package/dist/{chunk-RVRTRESS.js → chunk-DQHWLAUV.js} +29 -29
- package/dist/chunk-DQHWLAUV.js.map +1 -0
- package/dist/{chunk-6JBGU74P.js → chunk-GHYYFAQN.js} +23 -23
- package/dist/chunk-GHYYFAQN.js.map +1 -0
- package/dist/{chunk-NWQ4CJOQ.js → chunk-GKX2HPZ6.js} +40 -40
- package/dist/chunk-GKX2HPZ6.js.map +1 -0
- package/dist/{chunk-TJ34N7C7.js → chunk-OOGTG5FM.js} +34 -33
- package/dist/chunk-OOGTG5FM.js.map +1 -0
- package/dist/{core-W2HYIQW6.js → core-UQXZTBFZ.js} +24 -26
- package/dist/{generate-LMTISDIJ.js → generate-GP6ZLAQB.js} +5 -5
- package/dist/generate-GP6ZLAQB.js.map +1 -0
- package/dist/index.d.ts +23 -27
- package/dist/index.js +10 -10
- package/dist/{init-7CHRKQ7P.js → init-W72WBSU2.js} +5 -5
- package/dist/{init-7CHRKQ7P.js.map → init-W72WBSU2.js.map} +1 -1
- package/dist/mcp-bin.js +73 -73
- package/dist/mcp-bin.js.map +1 -1
- package/dist/scan-V54HWRDY.js +12 -0
- package/dist/{service-T2L7VLTE.js → service-PVGTYUKX.js} +6 -6
- package/dist/{static-viewer-GBR7YNF3.js → static-viewer-KILKIVN7.js} +4 -4
- package/dist/{test-OJRXNDO2.js → test-3YRYQRGV.js} +19 -19
- package/dist/test-3YRYQRGV.js.map +1 -0
- package/dist/{tokens-3BWDESVM.js → tokens-IXSQHPQK.js} +5 -5
- package/dist/{viewer-SUFOISZM.js → viewer-K42REJU2.js} +199 -199
- package/dist/viewer-K42REJU2.js.map +1 -0
- package/package.json +13 -2
- package/src/ai.ts +5 -5
- package/src/analyze.ts +11 -11
- package/src/bin.ts +1 -1
- package/src/build.ts +37 -35
- package/src/commands/a11y.ts +6 -6
- package/src/commands/add.ts +11 -11
- package/src/commands/audit.ts +4 -4
- package/src/commands/baseline.ts +3 -3
- package/src/commands/build.ts +8 -8
- package/src/commands/compare.ts +20 -20
- package/src/commands/context.ts +16 -16
- package/src/commands/enhance.ts +36 -36
- package/src/commands/generate.ts +1 -1
- package/src/commands/graph.ts +5 -5
- package/src/commands/init.ts +1 -1
- package/src/commands/link/figma.ts +82 -82
- package/src/commands/link/index.ts +3 -3
- package/src/commands/link/storybook.ts +9 -9
- package/src/commands/list.ts +2 -2
- package/src/commands/reset.ts +15 -15
- package/src/commands/scan.ts +27 -27
- package/src/commands/storygen.ts +24 -24
- package/src/commands/validate.ts +2 -2
- package/src/commands/verify.ts +8 -8
- package/src/core/auto-props.ts +4 -4
- package/src/core/composition.test.ts +36 -36
- package/src/core/composition.ts +19 -19
- package/src/core/config.ts +6 -6
- package/src/core/{defineSegment.ts → defineFragment.ts} +16 -22
- package/src/core/discovery.ts +6 -6
- package/src/core/figma.ts +2 -2
- package/src/core/graph-extractor.test.ts +77 -77
- package/src/core/graph-extractor.ts +32 -32
- package/src/core/importAnalyzer.ts +1 -1
- package/src/core/index.ts +22 -23
- package/src/core/loader.ts +21 -24
- package/src/core/node.ts +5 -5
- package/src/core/parser.ts +71 -31
- package/src/core/previewLoader.ts +1 -1
- package/src/core/schema.ts +16 -16
- package/src/core/storyAdapter.test.ts +87 -87
- package/src/core/storyAdapter.ts +16 -16
- package/src/core/token-parser.ts +9 -1
- package/src/core/types.ts +21 -26
- package/src/diff.ts +22 -22
- package/src/index.ts +2 -2
- package/src/mcp/server.ts +80 -80
- package/src/migrate/__tests__/utils/utils.test.ts +3 -3
- package/src/migrate/bin.ts +4 -4
- package/src/migrate/converter.ts +16 -16
- package/src/migrate/index.ts +3 -3
- package/src/migrate/migrate.ts +3 -3
- package/src/migrate/parser.ts +8 -8
- package/src/migrate/report.ts +2 -2
- package/src/migrate/types.ts +4 -4
- package/src/screenshot.ts +22 -22
- package/src/service/__tests__/props-extractor.test.ts +15 -15
- package/src/service/analytics.ts +39 -39
- package/src/service/enhance/codebase-scanner.ts +1 -1
- package/src/service/enhance/index.ts +1 -1
- package/src/service/enhance/props-extractor.ts +2 -2
- package/src/service/enhance/types.ts +2 -2
- package/src/service/index.ts +2 -2
- package/src/service/metrics-store.ts +1 -1
- package/src/service/patch-generator.ts +1 -1
- package/src/setup.ts +52 -52
- package/src/shared/dev-server-client.ts +7 -7
- package/src/shared/fragment-loader.ts +59 -0
- package/src/shared/index.ts +1 -1
- package/src/shared/types.ts +4 -4
- package/src/static-viewer.ts +35 -35
- package/src/test/discovery.ts +6 -6
- package/src/test/index.ts +5 -5
- package/src/test/reporters/console.ts +1 -1
- package/src/test/reporters/junit.ts +1 -1
- package/src/test/runner.ts +7 -7
- package/src/test/types.ts +3 -3
- package/src/test/watch.ts +9 -9
- package/src/validators.ts +26 -26
- package/src/viewer/__tests__/render-utils.test.ts +28 -28
- package/src/viewer/__tests__/viewer-integration.test.ts +4 -4
- package/src/viewer/cli/health.ts +26 -26
- package/src/viewer/components/App.tsx +79 -79
- package/src/viewer/components/BottomPanel.tsx +17 -17
- package/src/viewer/components/CodePanel.tsx +3 -3
- package/src/viewer/components/CommandPalette.tsx +11 -11
- package/src/viewer/components/ComponentGraph.tsx +28 -28
- package/src/viewer/components/ComponentHeader.tsx +2 -2
- package/src/viewer/components/ContractPanel.tsx +6 -6
- package/src/viewer/components/FigmaEmbed.tsx +9 -9
- package/src/viewer/components/HealthDashboard.tsx +17 -17
- package/src/viewer/components/InteractionsPanel.tsx +2 -2
- package/src/viewer/components/IsolatedPreviewFrame.tsx +6 -6
- package/src/viewer/components/IsolatedRender.tsx +10 -10
- package/src/viewer/components/LeftSidebar.tsx +28 -28
- package/src/viewer/components/MultiViewportPreview.tsx +14 -14
- package/src/viewer/components/PreviewArea.tsx +11 -11
- package/src/viewer/components/PreviewFrameHost.tsx +51 -51
- package/src/viewer/components/RightSidebar.tsx +9 -9
- package/src/viewer/components/Sidebar.tsx +17 -17
- package/src/viewer/components/StoryRenderer.tsx +2 -2
- package/src/viewer/components/TokenStylePanel.tsx +1 -1
- package/src/viewer/components/UsageSection.tsx +2 -2
- package/src/viewer/components/VariantMatrix.tsx +11 -11
- package/src/viewer/components/VariantRenderer.tsx +3 -3
- package/src/viewer/components/VariantTabs.tsx +2 -2
- package/src/viewer/components/_future/CreatePage.tsx +6 -6
- package/src/viewer/composition-renderer.ts +11 -11
- package/src/viewer/entry.tsx +40 -40
- package/src/viewer/hooks/useFigmaIntegration.ts +1 -1
- package/src/viewer/hooks/usePreviewBridge.ts +5 -5
- package/src/viewer/hooks/useUrlState.ts +6 -6
- package/src/viewer/index.ts +2 -2
- package/src/viewer/intelligence/healthReport.ts +17 -17
- package/src/viewer/intelligence/styleDrift.ts +1 -1
- package/src/viewer/intelligence/usageScanner.ts +1 -1
- package/src/viewer/render-template.html +1 -1
- package/src/viewer/render-utils.ts +21 -21
- package/src/viewer/server.ts +18 -18
- package/src/viewer/utils/detectRelationships.ts +22 -22
- package/src/viewer/vite-plugin.ts +213 -213
- package/dist/chunk-6JBGU74P.js.map +0 -1
- package/dist/chunk-7OPWMLOE.js.map +0 -1
- package/dist/chunk-CVXKXVOY.js.map +0 -1
- package/dist/chunk-NWQ4CJOQ.js.map +0 -1
- package/dist/chunk-RVRTRESS.js.map +0 -1
- package/dist/chunk-TJ34N7C7.js.map +0 -1
- package/dist/chunk-XHUDJNN3.js.map +0 -1
- package/dist/generate-LMTISDIJ.js.map +0 -1
- package/dist/scan-WY23TJCP.js +0 -12
- package/dist/test-OJRXNDO2.js.map +0 -1
- package/dist/viewer-SUFOISZM.js.map +0 -1
- package/src/shared/segment-loader.ts +0 -59
- /package/dist/{core-W2HYIQW6.js.map → core-UQXZTBFZ.js.map} +0 -0
- /package/dist/{scan-WY23TJCP.js.map → scan-V54HWRDY.js.map} +0 -0
- /package/dist/{service-T2L7VLTE.js.map → service-PVGTYUKX.js.map} +0 -0
- /package/dist/{static-viewer-GBR7YNF3.js.map → static-viewer-KILKIVN7.js.map} +0 -0
- /package/dist/{tokens-3BWDESVM.js.map → tokens-IXSQHPQK.js.map} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Fragments Vite Plugin
|
|
3
3
|
*
|
|
4
4
|
* This plugin runs WITHIN the project's Vite context, giving it access to:
|
|
5
5
|
* - All project dependencies (React, UI libraries, etc.)
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
* - Project's tsconfig paths
|
|
9
9
|
*
|
|
10
10
|
* It provides:
|
|
11
|
-
* - Virtual module for
|
|
11
|
+
* - Virtual module for fragment imports
|
|
12
12
|
* - Viewer UI served at /fragments/
|
|
13
|
-
* - HMR support for
|
|
13
|
+
* - HMR support for fragment file changes
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import type { Plugin, ViteDevServer, ResolvedConfig } from "vite";
|
|
@@ -18,7 +18,7 @@ import { resolve, dirname } from "node:path";
|
|
|
18
18
|
import { fileURLToPath } from "node:url";
|
|
19
19
|
import { readFile } from "node:fs/promises";
|
|
20
20
|
import { transform } from "esbuild";
|
|
21
|
-
import type {
|
|
21
|
+
import type { FragmentsConfig, CompiledFragment } from "../core/index.js";
|
|
22
22
|
import { generateContext, BRAND } from "../core/index.js";
|
|
23
23
|
import {
|
|
24
24
|
findStorybookDir,
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
import svgr from "vite-plugin-svgr";
|
|
29
29
|
import {
|
|
30
30
|
generateRenderScript,
|
|
31
|
-
|
|
31
|
+
findFragmentByName,
|
|
32
32
|
getAvailableComponents,
|
|
33
33
|
type RenderRequest,
|
|
34
34
|
} from "./render-utils.js";
|
|
@@ -47,7 +47,7 @@ interface CompareRequest {
|
|
|
47
47
|
variant?: string;
|
|
48
48
|
/** Props to render with */
|
|
49
49
|
props?: Record<string, unknown>;
|
|
50
|
-
/** Figma URL (optional if
|
|
50
|
+
/** Figma URL (optional if fragment has figma link) */
|
|
51
51
|
figmaUrl?: string;
|
|
52
52
|
/** Viewport dimensions */
|
|
53
53
|
viewport?: { width: number; height: number };
|
|
@@ -131,30 +131,30 @@ async function getSharedRenderPool() {
|
|
|
131
131
|
return { pool: sharedRenderPool, bufferToBase64Url: browserPoolModule.bufferToBase64Url };
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
export interface
|
|
135
|
-
/** Discovered
|
|
136
|
-
|
|
134
|
+
export interface FragmentsPluginOptions {
|
|
135
|
+
/** Discovered fragment files */
|
|
136
|
+
fragmentFiles: Array<{
|
|
137
137
|
absolutePath: string;
|
|
138
138
|
relativePath: string;
|
|
139
139
|
}>;
|
|
140
140
|
|
|
141
|
-
/**
|
|
142
|
-
config:
|
|
141
|
+
/** Fragments configuration */
|
|
142
|
+
config: FragmentsConfig;
|
|
143
143
|
|
|
144
144
|
/** Project root directory */
|
|
145
145
|
projectRoot: string;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
/**
|
|
149
|
-
* Create the
|
|
149
|
+
* Create the Fragments Vite plugin.
|
|
150
150
|
* Returns an array of plugins to support SVGR and other transforms.
|
|
151
151
|
*/
|
|
152
|
-
export function
|
|
153
|
-
const {
|
|
152
|
+
export function fragmentsPlugin(options: FragmentsPluginOptions): Plugin[] {
|
|
153
|
+
const { fragmentFiles, config, projectRoot } = options;
|
|
154
154
|
|
|
155
155
|
// Virtual module IDs
|
|
156
|
-
const
|
|
157
|
-
const
|
|
156
|
+
const VIRTUAL_FRAGMENTS = `virtual:${BRAND.nameLower}`;
|
|
157
|
+
const VIRTUAL_FRAGMENTS_RESOLVED = `\0virtual:${BRAND.nameLower}`;
|
|
158
158
|
|
|
159
159
|
const VIRTUAL_VIEWER_ENTRY = `virtual:${BRAND.nameLower}-viewer-entry`;
|
|
160
160
|
const VIRTUAL_VIEWER_ENTRY_RESOLVED = `\0virtual:${BRAND.nameLower}-viewer-entry`;
|
|
@@ -171,11 +171,11 @@ export function segmentsPlugin(options: SegmentsPluginOptions): Plugin[] {
|
|
|
171
171
|
? findPreviewConfigPath(storybookDir)
|
|
172
172
|
: null;
|
|
173
173
|
|
|
174
|
-
// Track
|
|
175
|
-
const
|
|
174
|
+
// Track fragment files for HMR
|
|
175
|
+
const fragmentFileSet = new Set(fragmentFiles.map((f) => f.absolutePath));
|
|
176
176
|
|
|
177
177
|
const mainPlugin: Plugin = {
|
|
178
|
-
name: "
|
|
178
|
+
name: "fragments",
|
|
179
179
|
|
|
180
180
|
// Add process.env shim and esbuild config for Storybook compatibility
|
|
181
181
|
config() {
|
|
@@ -226,15 +226,15 @@ export function segmentsPlugin(options: SegmentsPluginOptions): Plugin[] {
|
|
|
226
226
|
return;
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
// Load
|
|
230
|
-
const
|
|
231
|
-
|
|
229
|
+
// Load fragments to find the component
|
|
230
|
+
const loadedFragments = await loadFragmentsForRender(
|
|
231
|
+
fragmentFiles,
|
|
232
232
|
projectRoot
|
|
233
233
|
);
|
|
234
|
-
const
|
|
234
|
+
const fragmentInfo = findFragmentByName(component, loadedFragments);
|
|
235
235
|
|
|
236
|
-
if (!
|
|
237
|
-
const available = getAvailableComponents(
|
|
236
|
+
if (!fragmentInfo) {
|
|
237
|
+
const available = getAvailableComponents(loadedFragments);
|
|
238
238
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
239
239
|
res.end(
|
|
240
240
|
JSON.stringify({
|
|
@@ -246,22 +246,22 @@ export function segmentsPlugin(options: SegmentsPluginOptions): Plugin[] {
|
|
|
246
246
|
return;
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
-
// Find the absolute path for the
|
|
250
|
-
const
|
|
251
|
-
(f) => f.relativePath ===
|
|
249
|
+
// Find the absolute path for the fragment
|
|
250
|
+
const fragmentFile = fragmentFiles.find(
|
|
251
|
+
(f) => f.relativePath === fragmentInfo.path
|
|
252
252
|
);
|
|
253
|
-
if (!
|
|
253
|
+
if (!fragmentFile) {
|
|
254
254
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
255
255
|
res.end(
|
|
256
|
-
JSON.stringify({ error: "Could not resolve
|
|
256
|
+
JSON.stringify({ error: "Could not resolve fragment file path" })
|
|
257
257
|
);
|
|
258
258
|
return;
|
|
259
259
|
}
|
|
260
260
|
|
|
261
261
|
// Generate render script
|
|
262
262
|
const renderScript = generateRenderScript(
|
|
263
|
-
|
|
264
|
-
|
|
263
|
+
fragmentFile.absolutePath,
|
|
264
|
+
fragmentInfo.name,
|
|
265
265
|
props
|
|
266
266
|
);
|
|
267
267
|
|
|
@@ -354,23 +354,23 @@ export function segmentsPlugin(options: SegmentsPluginOptions): Plugin[] {
|
|
|
354
354
|
return;
|
|
355
355
|
}
|
|
356
356
|
|
|
357
|
-
// Debug: Log
|
|
357
|
+
// Debug: Log fragment files
|
|
358
358
|
console.log("[Fragments] Compare request for:", component);
|
|
359
|
-
console.log("[Fragments]
|
|
360
|
-
console.log("[Fragments] First 3
|
|
359
|
+
console.log("[Fragments] fragmentFiles count:", fragmentFiles.length);
|
|
360
|
+
console.log("[Fragments] First 3 fragment files:", fragmentFiles.slice(0, 3).map(f => f.relativePath));
|
|
361
361
|
console.log("[Fragments] projectRoot:", projectRoot);
|
|
362
362
|
|
|
363
|
-
// Load
|
|
364
|
-
const
|
|
365
|
-
|
|
363
|
+
// Load fragments to find the component and its figma URL
|
|
364
|
+
const loadedFragments = await loadFragmentsForRender(
|
|
365
|
+
fragmentFiles,
|
|
366
366
|
projectRoot
|
|
367
367
|
);
|
|
368
|
-
console.log("[Fragments]
|
|
369
|
-
console.log("[Fragments] First 3 loaded:",
|
|
370
|
-
const
|
|
368
|
+
console.log("[Fragments] loadedFragments count:", loadedFragments.length);
|
|
369
|
+
console.log("[Fragments] First 3 loaded:", loadedFragments.slice(0, 3).map(s => s.fragment.meta.name));
|
|
370
|
+
const fragmentInfo = findFragmentByName(component, loadedFragments);
|
|
371
371
|
|
|
372
|
-
if (!
|
|
373
|
-
const available = getAvailableComponents(
|
|
372
|
+
if (!fragmentInfo) {
|
|
373
|
+
const available = getAvailableComponents(loadedFragments);
|
|
374
374
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
375
375
|
res.end(
|
|
376
376
|
JSON.stringify({
|
|
@@ -382,17 +382,17 @@ export function segmentsPlugin(options: SegmentsPluginOptions): Plugin[] {
|
|
|
382
382
|
return;
|
|
383
383
|
}
|
|
384
384
|
|
|
385
|
-
// Find full
|
|
386
|
-
const
|
|
385
|
+
// Find full fragment data to get figma URL
|
|
386
|
+
const fullFragmentData = await loadFullFragmentForCompare(
|
|
387
387
|
_server,
|
|
388
|
-
|
|
388
|
+
fragmentFiles,
|
|
389
389
|
component,
|
|
390
390
|
variant,
|
|
391
391
|
projectRoot
|
|
392
392
|
);
|
|
393
393
|
|
|
394
394
|
// Determine which Figma URL to use (request > variant > meta)
|
|
395
|
-
const effectiveFigmaUrl = figmaUrl ||
|
|
395
|
+
const effectiveFigmaUrl = figmaUrl || fullFragmentData?.figmaUrl;
|
|
396
396
|
|
|
397
397
|
if (!effectiveFigmaUrl) {
|
|
398
398
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
@@ -400,7 +400,7 @@ export function segmentsPlugin(options: SegmentsPluginOptions): Plugin[] {
|
|
|
400
400
|
JSON.stringify({
|
|
401
401
|
error: `No Figma URL for component '${component}'`,
|
|
402
402
|
suggestion:
|
|
403
|
-
"Add 'figma' field to
|
|
403
|
+
"Add 'figma' field to fragment definition or provide figmaUrl in request",
|
|
404
404
|
})
|
|
405
405
|
);
|
|
406
406
|
return;
|
|
@@ -418,14 +418,14 @@ export function segmentsPlugin(options: SegmentsPluginOptions): Plugin[] {
|
|
|
418
418
|
return;
|
|
419
419
|
}
|
|
420
420
|
|
|
421
|
-
// Find
|
|
422
|
-
const
|
|
423
|
-
(f) => f.relativePath ===
|
|
421
|
+
// Find fragment file for rendering
|
|
422
|
+
const fragmentFile = fragmentFiles.find(
|
|
423
|
+
(f) => f.relativePath === fragmentInfo.path
|
|
424
424
|
);
|
|
425
|
-
if (!
|
|
425
|
+
if (!fragmentFile) {
|
|
426
426
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
427
427
|
res.end(
|
|
428
|
-
JSON.stringify({ error: "Could not resolve
|
|
428
|
+
JSON.stringify({ error: "Could not resolve fragment file path" })
|
|
429
429
|
);
|
|
430
430
|
return;
|
|
431
431
|
}
|
|
@@ -449,8 +449,8 @@ export function segmentsPlugin(options: SegmentsPluginOptions): Plugin[] {
|
|
|
449
449
|
|
|
450
450
|
// Generate render script and request ID for component capture
|
|
451
451
|
const renderScript = generateRenderScript(
|
|
452
|
-
|
|
453
|
-
|
|
452
|
+
fragmentFile.absolutePath,
|
|
453
|
+
fragmentInfo.name,
|
|
454
454
|
props
|
|
455
455
|
);
|
|
456
456
|
const requestId =
|
|
@@ -765,12 +765,12 @@ export function segmentsPlugin(options: SegmentsPluginOptions): Plugin[] {
|
|
|
765
765
|
return;
|
|
766
766
|
}
|
|
767
767
|
|
|
768
|
-
// Load
|
|
769
|
-
const
|
|
770
|
-
const
|
|
768
|
+
// Load fragment data
|
|
769
|
+
const loadedFragments = await loadFragmentsForRender(fragmentFiles, projectRoot);
|
|
770
|
+
const fragmentInfo = findFragmentByName(component, loadedFragments);
|
|
771
771
|
|
|
772
|
-
if (!
|
|
773
|
-
const available = getAvailableComponents(
|
|
772
|
+
if (!fragmentInfo) {
|
|
773
|
+
const available = getAvailableComponents(loadedFragments);
|
|
774
774
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
775
775
|
res.end(JSON.stringify({
|
|
776
776
|
error: `Component '${component}' not found. Available: ${available.join(", ")}`,
|
|
@@ -778,13 +778,13 @@ export function segmentsPlugin(options: SegmentsPluginOptions): Plugin[] {
|
|
|
778
778
|
return;
|
|
779
779
|
}
|
|
780
780
|
|
|
781
|
-
// Find
|
|
782
|
-
const
|
|
783
|
-
(f) => f.relativePath ===
|
|
781
|
+
// Find fragment file for rendering
|
|
782
|
+
const fragmentFile = fragmentFiles.find(
|
|
783
|
+
(f) => f.relativePath === fragmentInfo.path
|
|
784
784
|
);
|
|
785
|
-
if (!
|
|
785
|
+
if (!fragmentFile) {
|
|
786
786
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
787
|
-
res.end(JSON.stringify({ error: "Could not resolve
|
|
787
|
+
res.end(JSON.stringify({ error: "Could not resolve fragment file path" }));
|
|
788
788
|
return;
|
|
789
789
|
}
|
|
790
790
|
|
|
@@ -804,8 +804,8 @@ export function segmentsPlugin(options: SegmentsPluginOptions): Plugin[] {
|
|
|
804
804
|
|
|
805
805
|
// Generate render script and capture with styles
|
|
806
806
|
const renderScript = generateRenderScript(
|
|
807
|
-
|
|
808
|
-
|
|
807
|
+
fragmentFile.absolutePath,
|
|
808
|
+
fragmentInfo.name,
|
|
809
809
|
{}
|
|
810
810
|
);
|
|
811
811
|
const requestId = Date.now().toString(36) + Math.random().toString(36).slice(2);
|
|
@@ -913,16 +913,16 @@ export function segmentsPlugin(options: SegmentsPluginOptions): Plugin[] {
|
|
|
913
913
|
| "json";
|
|
914
914
|
const compact = url.searchParams.get("compact") === "true";
|
|
915
915
|
|
|
916
|
-
// Load all
|
|
917
|
-
const
|
|
916
|
+
// Load all fragments from BRAND.outFile
|
|
917
|
+
const compiledFragments = await loadFragmentsForContext(
|
|
918
918
|
_server,
|
|
919
|
-
|
|
919
|
+
fragmentFiles,
|
|
920
920
|
config,
|
|
921
921
|
projectRoot
|
|
922
922
|
);
|
|
923
923
|
|
|
924
924
|
const { content, tokenEstimate } = generateContext(
|
|
925
|
-
|
|
925
|
+
compiledFragments,
|
|
926
926
|
{
|
|
927
927
|
format,
|
|
928
928
|
compact,
|
|
@@ -1032,12 +1032,12 @@ export function segmentsPlugin(options: SegmentsPluginOptions): Plugin[] {
|
|
|
1032
1032
|
return;
|
|
1033
1033
|
}
|
|
1034
1034
|
|
|
1035
|
-
// Load
|
|
1036
|
-
const
|
|
1037
|
-
const
|
|
1035
|
+
// Load fragment data
|
|
1036
|
+
const loadedFragments = await loadFragmentsForRender(fragmentFiles, projectRoot);
|
|
1037
|
+
const fragmentInfo = findFragmentByName(component, loadedFragments);
|
|
1038
1038
|
|
|
1039
|
-
if (!
|
|
1040
|
-
const available = getAvailableComponents(
|
|
1039
|
+
if (!fragmentInfo) {
|
|
1040
|
+
const available = getAvailableComponents(loadedFragments);
|
|
1041
1041
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1042
1042
|
res.end(JSON.stringify({
|
|
1043
1043
|
error: `Component '${component}' not found. Available: ${available.join(", ")}`,
|
|
@@ -1063,11 +1063,11 @@ export function segmentsPlugin(options: SegmentsPluginOptions): Plugin[] {
|
|
|
1063
1063
|
// 2. Compare with Figma styles to find hardcoded values
|
|
1064
1064
|
// 3. Generate patches for each hardcoded value
|
|
1065
1065
|
|
|
1066
|
-
// Get source file path from
|
|
1067
|
-
const
|
|
1068
|
-
(f) => f.relativePath ===
|
|
1066
|
+
// Get source file path from fragment
|
|
1067
|
+
const fragmentFile = fragmentFiles.find(
|
|
1068
|
+
(f) => f.relativePath === fragmentInfo.path
|
|
1069
1069
|
);
|
|
1070
|
-
const sourceFile =
|
|
1070
|
+
const sourceFile = fragmentFile?.relativePath || `${component}.tsx`;
|
|
1071
1071
|
|
|
1072
1072
|
// For demonstration, we'll create a placeholder response
|
|
1073
1073
|
// In production, this would use style comparison + AST patching
|
|
@@ -1109,7 +1109,7 @@ export function segmentsPlugin(options: SegmentsPluginOptions): Plugin[] {
|
|
|
1109
1109
|
return;
|
|
1110
1110
|
}
|
|
1111
1111
|
|
|
1112
|
-
if (req.url === "/
|
|
1112
|
+
if (req.url === "/fragments" || req.url === "/fragments/") {
|
|
1113
1113
|
// Redirect to /fragments/
|
|
1114
1114
|
if (!req.url.endsWith("/")) {
|
|
1115
1115
|
res.writeHead(302, { Location: "/fragments/" });
|
|
@@ -1138,8 +1138,8 @@ export function segmentsPlugin(options: SegmentsPluginOptions): Plugin[] {
|
|
|
1138
1138
|
|
|
1139
1139
|
// Resolve virtual modules
|
|
1140
1140
|
resolveId(id) {
|
|
1141
|
-
if (id ===
|
|
1142
|
-
return
|
|
1141
|
+
if (id === VIRTUAL_FRAGMENTS) {
|
|
1142
|
+
return VIRTUAL_FRAGMENTS_RESOLVED;
|
|
1143
1143
|
}
|
|
1144
1144
|
if (id === VIRTUAL_VIEWER_ENTRY) {
|
|
1145
1145
|
return VIRTUAL_VIEWER_ENTRY_RESOLVED;
|
|
@@ -1152,8 +1152,8 @@ export function segmentsPlugin(options: SegmentsPluginOptions): Plugin[] {
|
|
|
1152
1152
|
|
|
1153
1153
|
// Load virtual modules
|
|
1154
1154
|
load(id) {
|
|
1155
|
-
if (id ===
|
|
1156
|
-
return
|
|
1155
|
+
if (id === VIRTUAL_FRAGMENTS_RESOLVED) {
|
|
1156
|
+
return generateFragmentsModule(fragmentFiles, config, previewConfigPath);
|
|
1157
1157
|
}
|
|
1158
1158
|
if (id === VIRTUAL_VIEWER_ENTRY_RESOLVED) {
|
|
1159
1159
|
return generateViewerEntry();
|
|
@@ -1164,11 +1164,11 @@ export function segmentsPlugin(options: SegmentsPluginOptions): Plugin[] {
|
|
|
1164
1164
|
return null;
|
|
1165
1165
|
},
|
|
1166
1166
|
|
|
1167
|
-
// Handle HMR for
|
|
1167
|
+
// Handle HMR for fragment files
|
|
1168
1168
|
handleHotUpdate({ file, server }) {
|
|
1169
|
-
if (
|
|
1170
|
-
// Invalidate the virtual
|
|
1171
|
-
const mod = server.moduleGraph.getModuleById(
|
|
1169
|
+
if (fragmentFileSet.has(file)) {
|
|
1170
|
+
// Invalidate the virtual fragments module
|
|
1171
|
+
const mod = server.moduleGraph.getModuleById(VIRTUAL_FRAGMENTS_RESOLVED);
|
|
1172
1172
|
if (mod) {
|
|
1173
1173
|
server.moduleGraph.invalidateModule(mod);
|
|
1174
1174
|
}
|
|
@@ -1176,7 +1176,7 @@ export function segmentsPlugin(options: SegmentsPluginOptions): Plugin[] {
|
|
|
1176
1176
|
// Send HMR update
|
|
1177
1177
|
server.ws.send({
|
|
1178
1178
|
type: "custom",
|
|
1179
|
-
event: "
|
|
1179
|
+
event: "fragments:update",
|
|
1180
1180
|
data: { file },
|
|
1181
1181
|
});
|
|
1182
1182
|
|
|
@@ -1191,7 +1191,7 @@ export function segmentsPlugin(options: SegmentsPluginOptions): Plugin[] {
|
|
|
1191
1191
|
// Uses the `load` hook instead of `transform` because we need to intercept
|
|
1192
1192
|
// the file BEFORE Vite's import-analysis tries to parse it
|
|
1193
1193
|
const jsxTransformPlugin: Plugin = {
|
|
1194
|
-
name: "
|
|
1194
|
+
name: "fragments-jsx-transform",
|
|
1195
1195
|
enforce: "pre",
|
|
1196
1196
|
async load(id) {
|
|
1197
1197
|
// Only handle .js files that might contain JSX (like preview.js)
|
|
@@ -1247,7 +1247,7 @@ export function segmentsPlugin(options: SegmentsPluginOptions): Plugin[] {
|
|
|
1247
1247
|
},
|
|
1248
1248
|
include: "**/*.svg",
|
|
1249
1249
|
}),
|
|
1250
|
-
// Main
|
|
1250
|
+
// Main fragments plugin
|
|
1251
1251
|
mainPlugin,
|
|
1252
1252
|
];
|
|
1253
1253
|
}
|
|
@@ -1260,37 +1260,37 @@ function isStoryFile(filePath: string): boolean {
|
|
|
1260
1260
|
}
|
|
1261
1261
|
|
|
1262
1262
|
/**
|
|
1263
|
-
* Get the base component path from a
|
|
1264
|
-
* e.g., "src/components/Button/Button.
|
|
1263
|
+
* Get the base component path from a fragment or story file path.
|
|
1264
|
+
* e.g., "src/components/Button/Button.fragment.tsx" -> "src/components/Button/Button"
|
|
1265
1265
|
* e.g., "src/components/Button/Button.stories.tsx" -> "src/components/Button/Button"
|
|
1266
1266
|
*/
|
|
1267
1267
|
function getBaseComponentPath(filePath: string): string {
|
|
1268
|
-
return filePath.replace(/\.(
|
|
1268
|
+
return filePath.replace(/\.(fragment|stories)\.(tsx?|jsx?)$/, "");
|
|
1269
1269
|
}
|
|
1270
1270
|
|
|
1271
1271
|
/**
|
|
1272
|
-
* Generate the virtual
|
|
1273
|
-
* Uses dynamic imports for lazy loading -
|
|
1274
|
-
* Supports both native .
|
|
1272
|
+
* Generate the virtual fragments module.
|
|
1273
|
+
* Uses dynamic imports for lazy loading - fragments are loaded on demand.
|
|
1274
|
+
* Supports both native .fragment.tsx files and Storybook .stories.tsx files.
|
|
1275
1275
|
* Integrates Storybook preview config for global decorators, parameters, etc.
|
|
1276
1276
|
*
|
|
1277
|
-
* MERGE STRATEGY: When both .
|
|
1277
|
+
* MERGE STRATEGY: When both .fragment.tsx and .stories.tsx exist for the same component:
|
|
1278
1278
|
* - Use .stories.tsx for RENDERING (variants, props, etc.) - it's the source of truth
|
|
1279
|
-
* - Merge METADATA from .
|
|
1279
|
+
* - Merge METADATA from .fragment.tsx (Figma URLs, AI descriptions, usage guidelines)
|
|
1280
1280
|
* - This gives us the best of both worlds: working renders + rich metadata
|
|
1281
1281
|
*/
|
|
1282
|
-
function
|
|
1283
|
-
|
|
1284
|
-
config:
|
|
1282
|
+
function generateFragmentsModule(
|
|
1283
|
+
fragmentFiles: Array<{ absolutePath: string; relativePath: string }>,
|
|
1284
|
+
config: FragmentsConfig,
|
|
1285
1285
|
previewConfigPath: string | null
|
|
1286
1286
|
): string {
|
|
1287
1287
|
// Group files by base component path to identify pairs
|
|
1288
1288
|
const filesByBasePath = new Map<string, {
|
|
1289
1289
|
storyFile?: { absolutePath: string; relativePath: string };
|
|
1290
|
-
|
|
1290
|
+
fragmentFile?: { absolutePath: string; relativePath: string };
|
|
1291
1291
|
}>();
|
|
1292
1292
|
|
|
1293
|
-
for (const file of
|
|
1293
|
+
for (const file of fragmentFiles) {
|
|
1294
1294
|
const basePath = getBaseComponentPath(file.relativePath);
|
|
1295
1295
|
const isStory = isStoryFile(file.relativePath);
|
|
1296
1296
|
|
|
@@ -1299,25 +1299,25 @@ function generateSegmentsModule(
|
|
|
1299
1299
|
if (isStory) {
|
|
1300
1300
|
existing.storyFile = file;
|
|
1301
1301
|
} else {
|
|
1302
|
-
existing.
|
|
1302
|
+
existing.fragmentFile = file;
|
|
1303
1303
|
}
|
|
1304
1304
|
|
|
1305
1305
|
filesByBasePath.set(basePath, existing);
|
|
1306
1306
|
}
|
|
1307
1307
|
|
|
1308
1308
|
// Generate loaders with metadata merge support
|
|
1309
|
-
// Priority: stories for rendering,
|
|
1309
|
+
// Priority: stories for rendering, fragment for metadata (Figma URLs, etc.)
|
|
1310
1310
|
const loaders = Array.from(filesByBasePath.values())
|
|
1311
1311
|
.map((files) => {
|
|
1312
1312
|
// Determine which file to use for rendering
|
|
1313
|
-
const primaryFile = files.storyFile || files.
|
|
1313
|
+
const primaryFile = files.storyFile || files.fragmentFile;
|
|
1314
1314
|
if (!primaryFile) return null;
|
|
1315
1315
|
|
|
1316
1316
|
const isStory = !!files.storyFile;
|
|
1317
1317
|
|
|
1318
|
-
// If we have both, include the
|
|
1319
|
-
const metadataPath = (files.storyFile && files.
|
|
1320
|
-
? files.
|
|
1318
|
+
// If we have both, include the fragment file path for metadata merge
|
|
1319
|
+
const metadataPath = (files.storyFile && files.fragmentFile)
|
|
1320
|
+
? files.fragmentFile.absolutePath
|
|
1321
1321
|
: null;
|
|
1322
1322
|
|
|
1323
1323
|
return ` {
|
|
@@ -1332,11 +1332,11 @@ function generateSegmentsModule(
|
|
|
1332
1332
|
|
|
1333
1333
|
// Generate preview config import if available
|
|
1334
1334
|
const previewImport = previewConfigPath
|
|
1335
|
-
? `import * as previewConfig from "virtual:
|
|
1335
|
+
? `import * as previewConfig from "virtual:fragments-preview";`
|
|
1336
1336
|
: "";
|
|
1337
1337
|
const previewSetup = previewConfigPath
|
|
1338
1338
|
? `
|
|
1339
|
-
// Set global preview config before loading
|
|
1339
|
+
// Set global preview config before loading fragments
|
|
1340
1340
|
setPreviewConfig({
|
|
1341
1341
|
decorators: previewConfig.decorators,
|
|
1342
1342
|
parameters: previewConfig.parameters,
|
|
@@ -1349,88 +1349,88 @@ setPreviewConfig({
|
|
|
1349
1349
|
: "";
|
|
1350
1350
|
|
|
1351
1351
|
return `
|
|
1352
|
-
import {
|
|
1352
|
+
import { storyModuleToFragment, setPreviewConfig } from "@fragments/core";
|
|
1353
1353
|
${previewImport}
|
|
1354
1354
|
${previewSetup}
|
|
1355
|
-
// Lazy
|
|
1356
|
-
const
|
|
1355
|
+
// Lazy fragment loaders (supports both .fragment.tsx and .stories.tsx)
|
|
1356
|
+
const fragmentLoaders = [
|
|
1357
1357
|
${loaders}
|
|
1358
1358
|
];
|
|
1359
1359
|
|
|
1360
|
-
// Cache for loaded
|
|
1361
|
-
const
|
|
1360
|
+
// Cache for loaded fragments
|
|
1361
|
+
const loadedFragments = new Map();
|
|
1362
1362
|
|
|
1363
1363
|
/**
|
|
1364
|
-
* Merge metadata from a
|
|
1364
|
+
* Merge metadata from a fragment file into a story-based fragment.
|
|
1365
1365
|
* This preserves Figma URLs and other AI-agent focused data.
|
|
1366
1366
|
*/
|
|
1367
|
-
function mergeMetadata(
|
|
1368
|
-
if (!metadataModule?.default) return
|
|
1367
|
+
function mergeMetadata(fragment, metadataModule) {
|
|
1368
|
+
if (!metadataModule?.default) return fragment;
|
|
1369
1369
|
|
|
1370
1370
|
const metadata = metadataModule.default;
|
|
1371
1371
|
|
|
1372
1372
|
// Merge meta-level Figma URL
|
|
1373
|
-
if (metadata.meta?.figma && !
|
|
1374
|
-
|
|
1373
|
+
if (metadata.meta?.figma && !fragment.meta.figma) {
|
|
1374
|
+
fragment.meta.figma = metadata.meta.figma;
|
|
1375
1375
|
}
|
|
1376
1376
|
|
|
1377
1377
|
// Merge description if not present
|
|
1378
|
-
if (metadata.meta?.description && !
|
|
1379
|
-
|
|
1378
|
+
if (metadata.meta?.description && !fragment.meta.description) {
|
|
1379
|
+
fragment.meta.description = metadata.meta.description;
|
|
1380
1380
|
}
|
|
1381
1381
|
|
|
1382
1382
|
// Merge variant-level Figma URLs
|
|
1383
|
-
if (metadata.variants &&
|
|
1383
|
+
if (metadata.variants && fragment.variants) {
|
|
1384
1384
|
for (const metaVariant of metadata.variants) {
|
|
1385
|
-
const
|
|
1386
|
-
if (
|
|
1387
|
-
|
|
1385
|
+
const fragmentVariant = fragment.variants.find(v => v.name === metaVariant.name);
|
|
1386
|
+
if (fragmentVariant && metaVariant.figma && !fragmentVariant.figma) {
|
|
1387
|
+
fragmentVariant.figma = metaVariant.figma;
|
|
1388
1388
|
}
|
|
1389
1389
|
}
|
|
1390
1390
|
}
|
|
1391
1391
|
|
|
1392
|
-
return
|
|
1392
|
+
return fragment;
|
|
1393
1393
|
}
|
|
1394
1394
|
|
|
1395
|
-
// Load all
|
|
1396
|
-
// Gracefully handles individual failures - one bad story won't break all
|
|
1397
|
-
export async function
|
|
1395
|
+
// Load all fragments (for initial render)
|
|
1396
|
+
// Gracefully handles individual failures - one bad story won't break all fragments
|
|
1397
|
+
export async function loadAllFragments() {
|
|
1398
1398
|
const results = await Promise.all(
|
|
1399
|
-
|
|
1399
|
+
fragmentLoaders.map(async (loader) => {
|
|
1400
1400
|
try {
|
|
1401
|
-
if (
|
|
1402
|
-
const cached =
|
|
1403
|
-
return cached ? { path: loader.path,
|
|
1401
|
+
if (loadedFragments.has(loader.path)) {
|
|
1402
|
+
const cached = loadedFragments.get(loader.path);
|
|
1403
|
+
return cached ? { path: loader.path, fragment: cached } : null;
|
|
1404
1404
|
}
|
|
1405
1405
|
|
|
1406
1406
|
const module = await loader.loader();
|
|
1407
1407
|
|
|
1408
|
-
// Convert story modules to
|
|
1409
|
-
let
|
|
1408
|
+
// Convert story modules to fragments at runtime
|
|
1409
|
+
let fragment;
|
|
1410
1410
|
if (loader.isStory) {
|
|
1411
|
-
|
|
1412
|
-
//
|
|
1413
|
-
if (!
|
|
1414
|
-
|
|
1411
|
+
fragment = storyModuleToFragment(module, loader.path);
|
|
1412
|
+
// storyModuleToFragment returns null for stories without a component
|
|
1413
|
+
if (!fragment) {
|
|
1414
|
+
loadedFragments.set(loader.path, null);
|
|
1415
1415
|
return null;
|
|
1416
1416
|
}
|
|
1417
1417
|
} else {
|
|
1418
|
-
|
|
1418
|
+
fragment = module.default;
|
|
1419
1419
|
}
|
|
1420
1420
|
|
|
1421
|
-
// Merge metadata from corresponding
|
|
1421
|
+
// Merge metadata from corresponding fragment file if available
|
|
1422
1422
|
if (loader.metadataLoader) {
|
|
1423
1423
|
try {
|
|
1424
1424
|
const metadataModule = await loader.metadataLoader();
|
|
1425
|
-
|
|
1425
|
+
fragment = mergeMetadata(fragment, metadataModule);
|
|
1426
1426
|
} catch (metaError) {
|
|
1427
1427
|
// Metadata loading is optional - don't fail if it errors
|
|
1428
1428
|
console.warn("[Fragments] Could not load metadata for " + loader.path + ":", metaError.message);
|
|
1429
1429
|
}
|
|
1430
1430
|
}
|
|
1431
1431
|
|
|
1432
|
-
|
|
1433
|
-
return { path: loader.path,
|
|
1432
|
+
loadedFragments.set(loader.path, fragment);
|
|
1433
|
+
return { path: loader.path, fragment };
|
|
1434
1434
|
} catch (error) {
|
|
1435
1435
|
console.warn("[Fragments] Failed to load " + loader.path + ":", error.message);
|
|
1436
1436
|
return null;
|
|
@@ -1441,62 +1441,62 @@ export async function loadAllSegments() {
|
|
|
1441
1441
|
return results.filter(r => r !== null);
|
|
1442
1442
|
}
|
|
1443
1443
|
|
|
1444
|
-
// Load a single
|
|
1445
|
-
export async function
|
|
1446
|
-
const loader =
|
|
1444
|
+
// Load a single fragment by path
|
|
1445
|
+
export async function loadFragment(path) {
|
|
1446
|
+
const loader = fragmentLoaders.find(l => l.path === path);
|
|
1447
1447
|
if (!loader) return null;
|
|
1448
1448
|
|
|
1449
|
-
if (
|
|
1450
|
-
return
|
|
1449
|
+
if (loadedFragments.has(path)) {
|
|
1450
|
+
return loadedFragments.get(path);
|
|
1451
1451
|
}
|
|
1452
1452
|
|
|
1453
1453
|
const module = await loader.loader();
|
|
1454
1454
|
|
|
1455
|
-
// Convert story modules to
|
|
1456
|
-
let
|
|
1455
|
+
// Convert story modules to fragments at runtime
|
|
1456
|
+
let fragment;
|
|
1457
1457
|
if (loader.isStory) {
|
|
1458
|
-
|
|
1458
|
+
fragment = storyModuleToFragment(module, path);
|
|
1459
1459
|
} else {
|
|
1460
|
-
|
|
1460
|
+
fragment = module.default;
|
|
1461
1461
|
}
|
|
1462
1462
|
|
|
1463
|
-
// Merge metadata from corresponding
|
|
1464
|
-
if (loader.metadataLoader &&
|
|
1463
|
+
// Merge metadata from corresponding fragment file if available
|
|
1464
|
+
if (loader.metadataLoader && fragment) {
|
|
1465
1465
|
try {
|
|
1466
1466
|
const metadataModule = await loader.metadataLoader();
|
|
1467
|
-
|
|
1467
|
+
fragment = mergeMetadata(fragment, metadataModule);
|
|
1468
1468
|
} catch (metaError) {
|
|
1469
1469
|
console.warn("[Fragments] Could not load metadata for " + path + ":", metaError.message);
|
|
1470
1470
|
}
|
|
1471
1471
|
}
|
|
1472
1472
|
|
|
1473
|
-
|
|
1474
|
-
return
|
|
1473
|
+
loadedFragments.set(path, fragment);
|
|
1474
|
+
return fragment;
|
|
1475
1475
|
}
|
|
1476
1476
|
|
|
1477
|
-
// For backwards compatibility, load all
|
|
1477
|
+
// For backwards compatibility, load all fragments synchronously on import
|
|
1478
1478
|
// This is still lazy per-file but awaited at module load
|
|
1479
|
-
let
|
|
1480
|
-
const
|
|
1479
|
+
let fragments = [];
|
|
1480
|
+
const fragmentsPromise = loadAllFragments().then(s => { fragments = s; return s; });
|
|
1481
1481
|
|
|
1482
|
-
export {
|
|
1482
|
+
export { fragments, fragmentsPromise };
|
|
1483
1483
|
export const config = ${JSON.stringify(config)};
|
|
1484
1484
|
|
|
1485
1485
|
// HMR support
|
|
1486
1486
|
if (import.meta.hot) {
|
|
1487
1487
|
import.meta.hot.accept();
|
|
1488
1488
|
|
|
1489
|
-
import.meta.hot.on("
|
|
1489
|
+
import.meta.hot.on("fragments:update", (data) => {
|
|
1490
1490
|
console.log("[Fragments] File updated:", data.file);
|
|
1491
|
-
// Clear cache for the updated file (handles both .
|
|
1492
|
-
for (const [path, _] of
|
|
1493
|
-
const basePath = path.replace(/\\.(
|
|
1491
|
+
// Clear cache for the updated file (handles both .fragment and .stories)
|
|
1492
|
+
for (const [path, _] of loadedFragments) {
|
|
1493
|
+
const basePath = path.replace(/\\.(fragment|stories)\\.tsx?$/, '');
|
|
1494
1494
|
if (data.file.includes(basePath)) {
|
|
1495
|
-
|
|
1495
|
+
loadedFragments.delete(path);
|
|
1496
1496
|
}
|
|
1497
1497
|
}
|
|
1498
1498
|
// Trigger re-render in viewer
|
|
1499
|
-
window.dispatchEvent(new CustomEvent("
|
|
1499
|
+
window.dispatchEvent(new CustomEvent("fragments:update"));
|
|
1500
1500
|
});
|
|
1501
1501
|
}
|
|
1502
1502
|
`;
|
|
@@ -1507,38 +1507,38 @@ if (import.meta.hot) {
|
|
|
1507
1507
|
*/
|
|
1508
1508
|
function generateViewerEntry(): string {
|
|
1509
1509
|
return `
|
|
1510
|
-
import {
|
|
1510
|
+
import { fragments, config } from "virtual:fragments";
|
|
1511
1511
|
|
|
1512
1512
|
// Re-export for viewer
|
|
1513
|
-
export {
|
|
1513
|
+
export { fragments, config };
|
|
1514
1514
|
|
|
1515
1515
|
// Initialize viewer
|
|
1516
|
-
console.log("[Fragments] Loaded",
|
|
1516
|
+
console.log("[Fragments] Loaded", fragments.length, "fragment(s)");
|
|
1517
1517
|
`;
|
|
1518
1518
|
}
|
|
1519
1519
|
|
|
1520
1520
|
/**
|
|
1521
|
-
* Load
|
|
1521
|
+
* Load fragments for context generation.
|
|
1522
1522
|
* Uses BRAND.outFile to avoid SSR module loading issues with React CJS modules.
|
|
1523
1523
|
*/
|
|
1524
|
-
async function
|
|
1524
|
+
async function loadFragmentsForContext(
|
|
1525
1525
|
_server: ViteDevServer,
|
|
1526
|
-
|
|
1527
|
-
_config:
|
|
1526
|
+
_fragmentFiles: Array<{ absolutePath: string; relativePath: string }>,
|
|
1527
|
+
_config: FragmentsConfig,
|
|
1528
1528
|
configDir?: string
|
|
1529
|
-
): Promise<
|
|
1529
|
+
): Promise<CompiledFragment[]> {
|
|
1530
1530
|
const { join } = await import("node:path");
|
|
1531
1531
|
|
|
1532
1532
|
// Read from outFile (avoids SSR issues with React CJS)
|
|
1533
|
-
const
|
|
1533
|
+
const fragmentsJsonPath = join(configDir || process.cwd(), BRAND.outFile);
|
|
1534
1534
|
|
|
1535
1535
|
try {
|
|
1536
|
-
const content = await readFile(
|
|
1536
|
+
const content = await readFile(fragmentsJsonPath, "utf-8");
|
|
1537
1537
|
const data = JSON.parse(content) as {
|
|
1538
|
-
|
|
1538
|
+
fragments: Record<string, CompiledFragment>;
|
|
1539
1539
|
};
|
|
1540
1540
|
|
|
1541
|
-
return Object.values(data.
|
|
1541
|
+
return Object.values(data.fragments || {});
|
|
1542
1542
|
} catch (error) {
|
|
1543
1543
|
console.warn(
|
|
1544
1544
|
`[${BRAND.name}] Failed to load ${BRAND.outFile} for context:`,
|
|
@@ -1571,7 +1571,7 @@ async function serveViewerHTML(res: any, server: ViteDevServer): Promise<void> {
|
|
|
1571
1571
|
} catch (error) {
|
|
1572
1572
|
console.error("[Fragments] Error serving viewer:", error);
|
|
1573
1573
|
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
1574
|
-
res.end("Error loading
|
|
1574
|
+
res.end("Error loading Fragments viewer");
|
|
1575
1575
|
}
|
|
1576
1576
|
}
|
|
1577
1577
|
|
|
@@ -1623,30 +1623,30 @@ async function parseJsonBody(req: any): Promise<unknown> {
|
|
|
1623
1623
|
}
|
|
1624
1624
|
|
|
1625
1625
|
/**
|
|
1626
|
-
* Load
|
|
1626
|
+
* Load fragments for render from BRAND.outFile or by building on-the-fly.
|
|
1627
1627
|
* This avoids SSR issues with React components.
|
|
1628
1628
|
*/
|
|
1629
|
-
async function
|
|
1630
|
-
|
|
1629
|
+
async function loadFragmentsForRender(
|
|
1630
|
+
fragmentFiles: Array<{ absolutePath: string; relativePath: string }>,
|
|
1631
1631
|
configDir: string
|
|
1632
|
-
): Promise<Array<{ path: string;
|
|
1632
|
+
): Promise<Array<{ path: string; fragment: { meta: { name: string } } }>> {
|
|
1633
1633
|
const { join } = await import("node:path");
|
|
1634
1634
|
|
|
1635
1635
|
// Try to read from the project directory
|
|
1636
|
-
const
|
|
1636
|
+
const fragmentsJsonPath = join(configDir, BRAND.outFile);
|
|
1637
1637
|
|
|
1638
1638
|
try {
|
|
1639
|
-
const content = await readFile(
|
|
1639
|
+
const content = await readFile(fragmentsJsonPath, "utf-8");
|
|
1640
1640
|
const data = JSON.parse(content) as {
|
|
1641
|
-
|
|
1641
|
+
fragments: Record<string, { filePath: string; meta: { name: string } }>;
|
|
1642
1642
|
};
|
|
1643
1643
|
|
|
1644
1644
|
// Convert to the expected format if we have entries
|
|
1645
|
-
const
|
|
1646
|
-
if (
|
|
1647
|
-
return
|
|
1648
|
-
path:
|
|
1649
|
-
|
|
1645
|
+
const fragmentEntries = Object.values(data.fragments || {});
|
|
1646
|
+
if (fragmentEntries.length > 0) {
|
|
1647
|
+
return fragmentEntries.map((fragment) => ({
|
|
1648
|
+
path: fragment.filePath,
|
|
1649
|
+
fragment: { meta: { name: fragment.meta.name } },
|
|
1650
1650
|
}));
|
|
1651
1651
|
}
|
|
1652
1652
|
// Fall through to file-based extraction if outFile is empty
|
|
@@ -1655,20 +1655,20 @@ async function loadSegmentsForRender(
|
|
|
1655
1655
|
}
|
|
1656
1656
|
|
|
1657
1657
|
// Extract component names from file paths (fallback)
|
|
1658
|
-
return
|
|
1658
|
+
return fragmentFiles.map((f) => {
|
|
1659
1659
|
let name: string;
|
|
1660
1660
|
if (isStoryFile(f.relativePath)) {
|
|
1661
1661
|
// Extract name from path like "src/components/Button/Button.stories.tsx"
|
|
1662
1662
|
const match = f.relativePath.match(/\/([^/]+)\.stories\./);
|
|
1663
1663
|
name = match ? match[1] : f.relativePath;
|
|
1664
1664
|
} else {
|
|
1665
|
-
// Extract name from path like "src/components/Button/Button.
|
|
1666
|
-
const match = f.relativePath.match(/\/([^/]+)\.
|
|
1665
|
+
// Extract name from path like "src/components/Button/Button.fragment.tsx"
|
|
1666
|
+
const match = f.relativePath.match(/\/([^/]+)\.fragment\./);
|
|
1667
1667
|
name = match ? match[1] : f.relativePath;
|
|
1668
1668
|
}
|
|
1669
1669
|
return {
|
|
1670
1670
|
path: f.relativePath,
|
|
1671
|
-
|
|
1671
|
+
fragment: { meta: { name } },
|
|
1672
1672
|
};
|
|
1673
1673
|
});
|
|
1674
1674
|
}
|
|
@@ -1942,12 +1942,12 @@ async function captureRenderWithStyles(
|
|
|
1942
1942
|
}
|
|
1943
1943
|
|
|
1944
1944
|
/**
|
|
1945
|
-
* Load full
|
|
1945
|
+
* Load full fragment data to get figma URL from fragment or variant.
|
|
1946
1946
|
* Uses BRAND.outFile to avoid SSR module loading issues with React CJS modules.
|
|
1947
1947
|
*/
|
|
1948
|
-
async function
|
|
1948
|
+
async function loadFullFragmentForCompare(
|
|
1949
1949
|
_server: ViteDevServer,
|
|
1950
|
-
|
|
1950
|
+
_fragmentFiles: Array<{ absolutePath: string; relativePath: string }>,
|
|
1951
1951
|
componentName: string,
|
|
1952
1952
|
variantName?: string,
|
|
1953
1953
|
configDir?: string
|
|
@@ -1955,12 +1955,12 @@ async function loadFullSegmentForCompare(
|
|
|
1955
1955
|
const { join } = await import("node:path");
|
|
1956
1956
|
|
|
1957
1957
|
// Try to read from outFile (avoids SSR issues with React CJS)
|
|
1958
|
-
const
|
|
1958
|
+
const fragmentsJsonPath = join(configDir || process.cwd(), BRAND.outFile);
|
|
1959
1959
|
|
|
1960
1960
|
try {
|
|
1961
|
-
const content = await readFile(
|
|
1961
|
+
const content = await readFile(fragmentsJsonPath, "utf-8");
|
|
1962
1962
|
const data = JSON.parse(content) as {
|
|
1963
|
-
|
|
1963
|
+
fragments: Record<
|
|
1964
1964
|
string,
|
|
1965
1965
|
{
|
|
1966
1966
|
meta: { name: string; figma?: string };
|
|
@@ -1969,22 +1969,22 @@ async function loadFullSegmentForCompare(
|
|
|
1969
1969
|
>;
|
|
1970
1970
|
};
|
|
1971
1971
|
|
|
1972
|
-
const
|
|
1973
|
-
if (!
|
|
1972
|
+
const fragment = data.fragments[componentName];
|
|
1973
|
+
if (!fragment) {
|
|
1974
1974
|
return null;
|
|
1975
1975
|
}
|
|
1976
1976
|
|
|
1977
1977
|
// Priority: variant.figma > meta.figma
|
|
1978
|
-
if (variantName &&
|
|
1979
|
-
const variant =
|
|
1978
|
+
if (variantName && fragment.variants) {
|
|
1979
|
+
const variant = fragment.variants.find((v) => v.name === variantName);
|
|
1980
1980
|
if (variant?.figma) {
|
|
1981
1981
|
return { figmaUrl: variant.figma };
|
|
1982
1982
|
}
|
|
1983
1983
|
}
|
|
1984
1984
|
|
|
1985
1985
|
// Fall back to meta.figma
|
|
1986
|
-
if (
|
|
1987
|
-
return { figmaUrl:
|
|
1986
|
+
if (fragment.meta.figma) {
|
|
1987
|
+
return { figmaUrl: fragment.meta.figma };
|
|
1988
1988
|
}
|
|
1989
1989
|
|
|
1990
1990
|
return null;
|