@fragments-sdk/cli 0.10.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/dist/bin.js +26 -8
  2. package/dist/bin.js.map +1 -1
  3. package/dist/{chunk-ZDA3PLQ6.js → chunk-5G3VZH43.js} +2 -2
  4. package/dist/{chunk-566BNPQZ.js → chunk-HRFUSSZI.js} +25 -6
  5. package/dist/chunk-HRFUSSZI.js.map +1 -0
  6. package/dist/{chunk-CAMXG5HJ.js → chunk-ZM4ZQZWZ.js} +2 -2
  7. package/dist/{generate-BGKTKO6E.js → generate-FBHSXR3D.js} +2 -2
  8. package/dist/index.js +2 -2
  9. package/dist/{init-Q53R5Q2T.js → init-UFGK5TCN.js} +77 -6
  10. package/dist/init-UFGK5TCN.js.map +1 -0
  11. package/dist/{scan-OQU7M4GH.js → scan-CJF2DOQW.js} +3 -3
  12. package/dist/{scan-generate-T5QNUG7N.js → scan-generate-SJAN5MVI.js} +2 -2
  13. package/dist/snapshot-SV2JOFZH.js +139 -0
  14. package/dist/snapshot-SV2JOFZH.js.map +1 -0
  15. package/dist/{test-2CSOSS3B.js → test-Z5LVO724.js} +2 -2
  16. package/dist/{tokens-DXEGYTOJ.js → tokens-CE46OTMD.js} +2 -2
  17. package/dist/{viewer-DBEPYM3G.js → viewer-DLLJIMCK.js} +69 -47
  18. package/dist/viewer-DLLJIMCK.js.map +1 -0
  19. package/package.json +6 -14
  20. package/src/bin.ts +30 -0
  21. package/src/commands/init.ts +76 -1
  22. package/src/commands/snapshot.ts +197 -0
  23. package/src/core/loader.ts +38 -8
  24. package/src/viewer/__tests__/viewer-integration.test.ts +85 -74
  25. package/src/viewer/server.ts +37 -22
  26. package/src/viewer/vite-plugin.ts +25 -9
  27. package/dist/chunk-566BNPQZ.js.map +0 -1
  28. package/dist/init-Q53R5Q2T.js.map +0 -1
  29. package/dist/viewer-DBEPYM3G.js.map +0 -1
  30. package/src/viewer/__tests__/a11y-fixes.test.ts +0 -358
  31. package/src/viewer/__tests__/jsx-parser.test.ts +0 -502
  32. package/src/viewer/__tests__/render-utils.test.ts +0 -232
  33. package/src/viewer/__tests__/style-utils.test.ts +0 -404
  34. package/src/viewer/assets/fragments-logo.ts +0 -4
  35. package/src/viewer/components/AccessibilityPanel.tsx +0 -1457
  36. package/src/viewer/components/ActionCapture.tsx +0 -172
  37. package/src/viewer/components/ActionsPanel.tsx +0 -332
  38. package/src/viewer/components/AllVariantsPreview.tsx +0 -78
  39. package/src/viewer/components/App.tsx +0 -582
  40. package/src/viewer/components/BottomPanel.tsx +0 -288
  41. package/src/viewer/components/CodePanel.naming.test.tsx +0 -59
  42. package/src/viewer/components/CodePanel.tsx +0 -118
  43. package/src/viewer/components/CommandPalette.tsx +0 -392
  44. package/src/viewer/components/ComponentDocView.tsx +0 -164
  45. package/src/viewer/components/ComponentGraph.tsx +0 -380
  46. package/src/viewer/components/ComponentHeader.tsx +0 -88
  47. package/src/viewer/components/ContractPanel.tsx +0 -241
  48. package/src/viewer/components/EmptyVariantMessage.tsx +0 -54
  49. package/src/viewer/components/ErrorBoundary.tsx +0 -97
  50. package/src/viewer/components/FigmaEmbed.tsx +0 -238
  51. package/src/viewer/components/FragmentEditor.tsx +0 -525
  52. package/src/viewer/components/FragmentRenderer.tsx +0 -61
  53. package/src/viewer/components/HeaderSearch.tsx +0 -24
  54. package/src/viewer/components/HealthDashboard.tsx +0 -441
  55. package/src/viewer/components/HmrStatusIndicator.tsx +0 -61
  56. package/src/viewer/components/Icons.tsx +0 -479
  57. package/src/viewer/components/InteractionsPanel.tsx +0 -757
  58. package/src/viewer/components/IsolatedPreviewFrame.tsx +0 -346
  59. package/src/viewer/components/IsolatedRender.tsx +0 -113
  60. package/src/viewer/components/KeyboardShortcutsHelp.tsx +0 -53
  61. package/src/viewer/components/LandingPage.tsx +0 -421
  62. package/src/viewer/components/Layout.tsx +0 -27
  63. package/src/viewer/components/LeftSidebar.tsx +0 -472
  64. package/src/viewer/components/LoadErrorMessage.tsx +0 -102
  65. package/src/viewer/components/MultiViewportPreview.tsx +0 -522
  66. package/src/viewer/components/NoVariantsMessage.tsx +0 -59
  67. package/src/viewer/components/PanelShell.tsx +0 -161
  68. package/src/viewer/components/PerformancePanel.tsx +0 -304
  69. package/src/viewer/components/PreviewArea.tsx +0 -472
  70. package/src/viewer/components/PreviewAside.tsx +0 -168
  71. package/src/viewer/components/PreviewFrameHost.tsx +0 -303
  72. package/src/viewer/components/PreviewPane.tsx +0 -149
  73. package/src/viewer/components/PreviewToolbar.tsx +0 -80
  74. package/src/viewer/components/PropsEditor.tsx +0 -506
  75. package/src/viewer/components/PropsTable.tsx +0 -111
  76. package/src/viewer/components/RelationsSection.tsx +0 -88
  77. package/src/viewer/components/ResizablePanel.tsx +0 -271
  78. package/src/viewer/components/RightSidebar.tsx +0 -102
  79. package/src/viewer/components/RuntimeToolsRegistrar.tsx +0 -17
  80. package/src/viewer/components/ScreenshotButton.tsx +0 -90
  81. package/src/viewer/components/Sidebar.tsx +0 -169
  82. package/src/viewer/components/SkeletonLoader.tsx +0 -161
  83. package/src/viewer/components/ThemeProvider.tsx +0 -42
  84. package/src/viewer/components/Toast.tsx +0 -3
  85. package/src/viewer/components/TokenStylePanel.tsx +0 -699
  86. package/src/viewer/components/TopToolbar.tsx +0 -159
  87. package/src/viewer/components/UsageSection.tsx +0 -95
  88. package/src/viewer/components/VariantMatrix.tsx +0 -388
  89. package/src/viewer/components/VariantRenderer.tsx +0 -131
  90. package/src/viewer/components/VariantTabs.tsx +0 -40
  91. package/src/viewer/components/ViewerHeader.tsx +0 -69
  92. package/src/viewer/components/ViewerStateSync.tsx +0 -52
  93. package/src/viewer/components/ViewportSelector.tsx +0 -172
  94. package/src/viewer/components/WebMCPDevTools.tsx +0 -503
  95. package/src/viewer/components/WebMCPIntegration.tsx +0 -47
  96. package/src/viewer/components/WebMCPStatusIndicator.tsx +0 -60
  97. package/src/viewer/components/_future/CreatePage.tsx +0 -836
  98. package/src/viewer/components/viewer-utils.ts +0 -16
  99. package/src/viewer/composition-renderer.ts +0 -381
  100. package/src/viewer/constants/index.ts +0 -1
  101. package/src/viewer/constants/ui.ts +0 -166
  102. package/src/viewer/entry.tsx +0 -335
  103. package/src/viewer/hooks/index.ts +0 -2
  104. package/src/viewer/hooks/useA11yCache.ts +0 -383
  105. package/src/viewer/hooks/useA11yService.ts +0 -364
  106. package/src/viewer/hooks/useActions.ts +0 -138
  107. package/src/viewer/hooks/useAppState.ts +0 -147
  108. package/src/viewer/hooks/useCompiledFragments.ts +0 -42
  109. package/src/viewer/hooks/useFigmaIntegration.ts +0 -132
  110. package/src/viewer/hooks/useHmrStatus.ts +0 -109
  111. package/src/viewer/hooks/useKeyboardShortcuts.ts +0 -270
  112. package/src/viewer/hooks/usePreviewBridge.ts +0 -347
  113. package/src/viewer/hooks/useScrollSpy.ts +0 -78
  114. package/src/viewer/hooks/useUrlState.ts +0 -318
  115. package/src/viewer/hooks/useViewSettings.ts +0 -111
  116. package/src/viewer/index.html +0 -28
  117. package/src/viewer/intelligence/healthReport.ts +0 -505
  118. package/src/viewer/intelligence/styleDrift.ts +0 -340
  119. package/src/viewer/intelligence/usageScanner.ts +0 -309
  120. package/src/viewer/jsx-parser.ts +0 -486
  121. package/src/viewer/preview-frame-entry.tsx +0 -25
  122. package/src/viewer/preview-frame.html +0 -125
  123. package/src/viewer/public/favicon.ico +0 -0
  124. package/src/viewer/render-template.html +0 -68
  125. package/src/viewer/styles/globals.css +0 -278
  126. package/src/viewer/types/a11y.ts +0 -197
  127. package/src/viewer/utils/a11y-fixes.ts +0 -509
  128. package/src/viewer/utils/actionExport.ts +0 -372
  129. package/src/viewer/utils/colorSchemes.ts +0 -201
  130. package/src/viewer/utils/detectRelationships.ts +0 -256
  131. package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss +0 -10
  132. package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss.d.ts +0 -2
  133. package/src/viewer/vendor/shared/src/ComponentDocContent.tsx +0 -274
  134. package/src/viewer/vendor/shared/src/DocsHeaderBar.tsx +0 -129
  135. package/src/viewer/vendor/shared/src/DocsPageAsideHost.tsx +0 -89
  136. package/src/viewer/vendor/shared/src/DocsPageShell.tsx +0 -124
  137. package/src/viewer/vendor/shared/src/DocsSearchCommand.tsx +0 -99
  138. package/src/viewer/vendor/shared/src/DocsSidebarNav.tsx +0 -66
  139. package/src/viewer/vendor/shared/src/PropsTable.module.scss +0 -68
  140. package/src/viewer/vendor/shared/src/PropsTable.module.scss.d.ts +0 -2
  141. package/src/viewer/vendor/shared/src/PropsTable.tsx +0 -76
  142. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss +0 -114
  143. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss.d.ts +0 -2
  144. package/src/viewer/vendor/shared/src/VariantPreviewCard.tsx +0 -134
  145. package/src/viewer/vendor/shared/src/docs-data/index.ts +0 -32
  146. package/src/viewer/vendor/shared/src/docs-data/mcp-configs.ts +0 -72
  147. package/src/viewer/vendor/shared/src/docs-data/palettes.ts +0 -75
  148. package/src/viewer/vendor/shared/src/docs-data/setup-examples.ts +0 -55
  149. package/src/viewer/vendor/shared/src/docs-layout.scss +0 -28
  150. package/src/viewer/vendor/shared/src/docs-layout.scss.d.ts +0 -2
  151. package/src/viewer/vendor/shared/src/index.ts +0 -34
  152. package/src/viewer/vendor/shared/src/types.ts +0 -53
  153. package/src/viewer/webmcp/__tests__/analytics.test.ts +0 -108
  154. package/src/viewer/webmcp/analytics.ts +0 -165
  155. package/src/viewer/webmcp/index.ts +0 -3
  156. package/src/viewer/webmcp/posthog-bridge.ts +0 -39
  157. package/src/viewer/webmcp/runtime-tools.ts +0 -152
  158. package/src/viewer/webmcp/scan-utils.ts +0 -135
  159. package/src/viewer/webmcp/use-tool-analytics.ts +0 -69
  160. package/src/viewer/webmcp/viewer-state.ts +0 -45
  161. /package/dist/{chunk-ZDA3PLQ6.js.map → chunk-5G3VZH43.js.map} +0 -0
  162. /package/dist/{chunk-CAMXG5HJ.js.map → chunk-ZM4ZQZWZ.js.map} +0 -0
  163. /package/dist/{generate-BGKTKO6E.js.map → generate-FBHSXR3D.js.map} +0 -0
  164. /package/dist/{scan-OQU7M4GH.js.map → scan-CJF2DOQW.js.map} +0 -0
  165. /package/dist/{scan-generate-T5QNUG7N.js.map → scan-generate-SJAN5MVI.js.map} +0 -0
  166. /package/dist/{test-2CSOSS3B.js.map → test-Z5LVO724.js.map} +0 -0
  167. /package/dist/{tokens-DXEGYTOJ.js.map → tokens-CE46OTMD.js.map} +0 -0
@@ -143,6 +143,8 @@ function generateConfig(options: {
143
143
  includePaths: string[];
144
144
  componentPaths: string[];
145
145
  framework: string;
146
+ themeBlock?: string;
147
+ snapshotsBlock?: string;
146
148
  }): string {
147
149
  const includeStr = options.includePaths.map((p) => ` '${p}'`).join(",\n");
148
150
  const componentStr = options.componentPaths.map((p) => ` '${p}'`).join(",\n");
@@ -165,7 +167,7 @@ ${componentStr}
165
167
 
166
168
  // Framework (react, vue, svelte)
167
169
  framework: '${options.framework}',
168
- };
170
+ ${options.themeBlock || ""}${options.snapshotsBlock || ""}};
169
171
 
170
172
  export default config;
171
173
  `;
@@ -532,6 +534,8 @@ export async function init(options: InitOptions = {}): Promise<InitResult> {
532
534
  let runScan = scenario === "components" || scenario === "stories";
533
535
  let createExample = scenario === "fresh";
534
536
  let startServer = false;
537
+ let themeBlock = "";
538
+ let snapshotsBlock = "";
535
539
 
536
540
  if (!options.yes) {
537
541
  // Ask about component location
@@ -548,6 +552,75 @@ export async function init(options: InitOptions = {}): Promise<InitResult> {
548
552
  });
549
553
  }
550
554
 
555
+ // Theme seed configuration
556
+ const configureTheme = await confirm({
557
+ message: "Configure theme seeds? (brand color, density, radius)",
558
+ default: false,
559
+ });
560
+
561
+ if (configureTheme) {
562
+ const brand = await input({
563
+ message: "Brand color (hex)",
564
+ default: "#18181b",
565
+ validate: (v) => /^#[0-9a-fA-F]{6}$/.test(v) || "Enter a valid hex color (e.g., #6366f1)",
566
+ });
567
+
568
+ const neutral = await select({
569
+ message: "Neutral palette",
570
+ choices: [
571
+ { value: "stone", name: "Stone (warm gray)" },
572
+ { value: "ice", name: "Ice (cool blue-gray)" },
573
+ { value: "earth", name: "Earth (olive/khaki)" },
574
+ { value: "sand", name: "Sand (warm beige)" },
575
+ { value: "fire", name: "Fire (warm red-gray)" },
576
+ ],
577
+ default: "stone",
578
+ });
579
+
580
+ const density = await select({
581
+ message: "Spacing density",
582
+ choices: [
583
+ { value: "compact", name: "Compact (tighter spacing)" },
584
+ { value: "default", name: "Default" },
585
+ { value: "relaxed", name: "Relaxed (more breathing room)" },
586
+ ],
587
+ default: "default",
588
+ });
589
+
590
+ const radiusStyle = await select({
591
+ message: "Border radius style",
592
+ choices: [
593
+ { value: "sharp", name: "Sharp (0px)" },
594
+ { value: "subtle", name: "Subtle (2px)" },
595
+ { value: "default", name: "Default (6px)" },
596
+ { value: "rounded", name: "Rounded (10px)" },
597
+ { value: "pill", name: "Pill (999px)" },
598
+ ],
599
+ default: "default",
600
+ });
601
+
602
+ // Build theme config block — only include non-default values
603
+ const themeEntries: string[] = [];
604
+ if (brand !== "#18181b") themeEntries.push(` brand: '${brand}'`);
605
+ if (neutral !== "stone") themeEntries.push(` neutral: '${neutral}'`);
606
+ if (density !== "default") themeEntries.push(` density: '${density}'`);
607
+ if (radiusStyle !== "default") themeEntries.push(` radiusStyle: '${radiusStyle}'`);
608
+
609
+ if (themeEntries.length > 0) {
610
+ themeBlock = `\n // Theme seed values (derives 120+ CSS custom properties)\n theme: {\n${themeEntries.join(",\n")},\n },\n`;
611
+ }
612
+ }
613
+
614
+ // Snapshot toggle
615
+ const enableSnapshots = await confirm({
616
+ message: "Enable visual snapshot tests per component variant?",
617
+ default: false,
618
+ });
619
+
620
+ if (enableSnapshots) {
621
+ snapshotsBlock = `\n // Visual snapshot testing\n snapshots: {\n enabled: true,\n },\n`;
622
+ }
623
+
551
624
  // Ask about starting the server
552
625
  startServer = await confirm({
553
626
  message: "Start the viewer now?",
@@ -575,6 +648,8 @@ export async function init(options: InitOptions = {}): Promise<InitResult> {
575
648
  includePaths,
576
649
  componentPaths: [`${componentPath}/**/*.tsx`],
577
650
  framework: "react",
651
+ themeBlock,
652
+ snapshotsBlock,
578
653
  });
579
654
 
580
655
  try {
@@ -0,0 +1,197 @@
1
+ /**
2
+ * fragments snapshot - Run visual snapshot tests per component variant
3
+ *
4
+ * Starts the dev server (if not already running), then runs Playwright
5
+ * snapshot tests against all component variants discovered in fragments.json.
6
+ */
7
+
8
+ import { resolve } from "node:path";
9
+ import { execSync, spawn } from "node:child_process";
10
+ import { existsSync } from "node:fs";
11
+ import pc from "picocolors";
12
+ import { BRAND } from "../core/index.js";
13
+
14
+ export interface SnapshotOptions {
15
+ /** Port of a running dev server (skips starting one) */
16
+ port?: number | string;
17
+ /** Update existing snapshots instead of comparing */
18
+ update?: boolean;
19
+ /** Filter to a specific component name */
20
+ component?: string;
21
+ /** Path to Playwright config (auto-detected if omitted) */
22
+ config?: string;
23
+ /** Path to the snapshot spec file */
24
+ spec?: string;
25
+ /** CI mode — non-interactive, exit 1 on mismatch */
26
+ ci?: boolean;
27
+ }
28
+
29
+ export interface SnapshotResult {
30
+ success: boolean;
31
+ totalTests: number;
32
+ passed: number;
33
+ failed: number;
34
+ }
35
+
36
+ /**
37
+ * Find the snapshot spec file.
38
+ * Checks project root e2e/ first, then falls back to the CLI's bundled spec.
39
+ */
40
+ function findSnapshotSpec(projectRoot: string, explicitPath?: string): string | null {
41
+ if (explicitPath) {
42
+ const resolved = resolve(projectRoot, explicitPath);
43
+ return existsSync(resolved) ? resolved : null;
44
+ }
45
+
46
+ // Check common locations
47
+ const candidates = [
48
+ resolve(projectRoot, "e2e/component-visual-snapshots.spec.ts"),
49
+ resolve(projectRoot, "tests/visual-snapshots.spec.ts"),
50
+ resolve(projectRoot, "test/visual-snapshots.spec.ts"),
51
+ ];
52
+
53
+ for (const candidate of candidates) {
54
+ if (existsSync(candidate)) {
55
+ return candidate;
56
+ }
57
+ }
58
+
59
+ return null;
60
+ }
61
+
62
+ /**
63
+ * Run visual snapshot tests.
64
+ */
65
+ export async function snapshot(options: SnapshotOptions = {}): Promise<SnapshotResult> {
66
+ const projectRoot = process.cwd();
67
+ const {
68
+ port,
69
+ update = false,
70
+ component,
71
+ ci = false,
72
+ } = options;
73
+
74
+ console.log(pc.cyan(`\n${BRAND.name} Visual Snapshots\n`));
75
+
76
+ // Check that fragments.json exists
77
+ const fragmentsJson = resolve(projectRoot, BRAND.outFile);
78
+ if (!existsSync(fragmentsJson)) {
79
+ console.error(
80
+ pc.red(`${BRAND.outFile} not found. Run ${pc.bold(`${BRAND.cliCommand} build`)} first.`)
81
+ );
82
+ return { success: false, totalTests: 0, passed: 0, failed: 0 };
83
+ }
84
+
85
+ // Find the snapshot spec
86
+ const specPath = findSnapshotSpec(projectRoot, options.spec);
87
+ if (!specPath) {
88
+ console.error(
89
+ pc.red("No snapshot spec found.") + "\n" +
90
+ pc.dim("Expected: e2e/component-visual-snapshots.spec.ts\n") +
91
+ pc.dim(`Create one or specify with --spec <path>`)
92
+ );
93
+ return { success: false, totalTests: 0, passed: 0, failed: 0 };
94
+ }
95
+
96
+ console.log(pc.dim(`Spec: ${specPath}`));
97
+
98
+ // Check for Playwright
99
+ try {
100
+ execSync("npx playwright --version", { stdio: "pipe" });
101
+ } catch {
102
+ console.error(
103
+ pc.red("Playwright not found.") + "\n" +
104
+ pc.dim("Install it: pnpm add -D @playwright/test && npx playwright install chromium")
105
+ );
106
+ return { success: false, totalTests: 0, passed: 0, failed: 0 };
107
+ }
108
+
109
+ // Build Playwright args
110
+ const args = ["playwright", "test", specPath];
111
+
112
+ if (update) {
113
+ args.push("--update-snapshots");
114
+ console.log(pc.yellow("Updating snapshots (baselines will be overwritten)"));
115
+ }
116
+
117
+ if (component) {
118
+ args.push("--grep", component);
119
+ console.log(pc.dim(`Filtering: ${component}`));
120
+ }
121
+
122
+ // If a port is specified, set the BASE_URL env var so the spec
123
+ // can connect to an already-running dev server
124
+ const env: Record<string, string> = { ...process.env as Record<string, string> };
125
+ if (port) {
126
+ env.FRAGMENTS_DEV_PORT = String(port);
127
+ console.log(pc.dim(`Using running dev server on port ${port}`));
128
+ }
129
+
130
+ console.log(pc.dim("\nRunning snapshot tests...\n"));
131
+
132
+ // Run Playwright
133
+ return new Promise<SnapshotResult>((resolveResult) => {
134
+ const child = spawn("npx", args, {
135
+ cwd: projectRoot,
136
+ stdio: ci ? "pipe" : "inherit",
137
+ env,
138
+ });
139
+
140
+ let stdout = "";
141
+
142
+ if (ci && child.stdout) {
143
+ child.stdout.on("data", (data) => {
144
+ stdout += data.toString();
145
+ });
146
+ }
147
+ if (ci && child.stderr) {
148
+ child.stderr.on("data", (data) => {
149
+ stdout += data.toString();
150
+ });
151
+ }
152
+
153
+ child.on("close", (code) => {
154
+ const success = code === 0;
155
+
156
+ if (ci) {
157
+ // Parse Playwright output for test counts
158
+ const passedMatch = stdout.match(/(\d+) passed/);
159
+ const failedMatch = stdout.match(/(\d+) failed/);
160
+ const passed = passedMatch ? parseInt(passedMatch[1], 10) : 0;
161
+ const failed = failedMatch ? parseInt(failedMatch[1], 10) : 0;
162
+
163
+ if (!success) {
164
+ process.stdout.write(stdout);
165
+ }
166
+
167
+ resolveResult({
168
+ success,
169
+ totalTests: passed + failed,
170
+ passed,
171
+ failed,
172
+ });
173
+ } else {
174
+ if (success) {
175
+ console.log(pc.green("\n✓ All snapshots match\n"));
176
+ } else if (update) {
177
+ console.log(pc.green("\n✓ Snapshots updated\n"));
178
+ } else {
179
+ console.log(pc.red("\n✗ Snapshot mismatches detected"));
180
+ console.log(pc.dim(`Run ${pc.bold(`${BRAND.cliCommand} snapshot --update`)} to accept changes\n`));
181
+ }
182
+
183
+ resolveResult({
184
+ success: success || update,
185
+ totalTests: 0,
186
+ passed: 0,
187
+ failed: 0,
188
+ });
189
+ }
190
+ });
191
+
192
+ child.on("error", (err) => {
193
+ console.error(pc.red(`Failed to run Playwright: ${err.message}`));
194
+ resolveResult({ success: false, totalTests: 0, passed: 0, failed: 0 });
195
+ });
196
+ });
197
+ }
@@ -53,6 +53,37 @@ function createFragmentsCoreShimPlugin(): Plugin {
53
53
  export async function loadFragmentFile(
54
54
  absolutePath: string
55
55
  ): Promise<FragmentDefinition | null> {
56
+ const unwrapFragmentExport = (
57
+ value: unknown
58
+ ): FragmentDefinition | null => {
59
+ if (!value) return null;
60
+
61
+ // Some CJS/ESM interop paths produce { default: fragment } wrappers.
62
+ // Prefer a shape that actually looks like a fragment definition.
63
+ const asObject = (v: unknown): Record<string, unknown> | null =>
64
+ v && typeof v === 'object' ? (v as Record<string, unknown>) : null;
65
+ const isFragmentLike = (v: unknown): boolean => {
66
+ const obj = asObject(v);
67
+ return !!obj && ('component' in obj || 'meta' in obj || 'variants' in obj);
68
+ };
69
+
70
+ if (isFragmentLike(value)) {
71
+ return value as FragmentDefinition;
72
+ }
73
+
74
+ const first = asObject(value)?.default;
75
+ if (isFragmentLike(first)) {
76
+ return first as FragmentDefinition;
77
+ }
78
+
79
+ const second = asObject(first)?.default;
80
+ if (isFragmentLike(second)) {
81
+ return second as FragmentDefinition;
82
+ }
83
+
84
+ return (value as FragmentDefinition) ?? null;
85
+ };
86
+
56
87
  const ext = absolutePath.split('.').pop()?.toLowerCase();
57
88
  const needsTransform = ext === 'tsx' || ext === 'ts' || ext === 'jsx';
58
89
 
@@ -60,24 +91,23 @@ export async function loadFragmentFile(
60
91
  // Plain JS file, import directly
61
92
  const fileUrl = pathToFileURL(absolutePath).href;
62
93
  const module = await import(fileUrl);
63
- return module.default ?? null;
94
+ return unwrapFragmentExport(module.default ?? null);
64
95
  }
65
96
 
66
97
  // Bundle the file with all its local dependencies
67
98
  const sourceDir = dirname(absolutePath);
68
99
  const baseName = basename(absolutePath, `.${ext}`);
69
- // Use .cjs extension to ensure CommonJS evaluation
70
- const tempFile = join(sourceDir, `.${baseName}.fragments-temp-${Date.now()}.cjs`);
100
+ // Use .mjs and ESM output for compatibility with ESM-only dependencies.
101
+ const tempFile = join(sourceDir, `.${baseName}.fragments-temp-${Date.now()}.mjs`);
71
102
 
72
103
  try {
73
- // Use esbuild to bundle the fragment file
74
- // We inject a shim for @fragments-sdk/cli/core so it doesn't need to be installed
75
- // Using CommonJS format to avoid ESM/CJS interop issues with node_modules
104
+ // Use esbuild to bundle the fragment file.
105
+ // We inject a shim for @fragments-sdk/cli/core so it doesn't need to be installed.
76
106
  await build({
77
107
  entryPoints: [absolutePath],
78
108
  outfile: tempFile,
79
109
  bundle: true,
80
- format: 'cjs', // Use CommonJS to avoid ESM issues with lodash, etc.
110
+ format: 'esm',
81
111
  target: 'es2022',
82
112
  jsx: 'automatic',
83
113
  platform: 'node',
@@ -114,7 +144,7 @@ export async function loadFragmentFile(
114
144
 
115
145
  const fileUrl = pathToFileURL(tempFile).href;
116
146
  const module = await import(fileUrl);
117
- return module.default ?? null;
147
+ return unwrapFragmentExport(module.default ?? null);
118
148
  } finally {
119
149
  // Clean up temp file
120
150
  try {
@@ -7,63 +7,101 @@ import { tmpdir } from "node:os";
7
7
  import { discoverInstalledFragments } from "../../core/discovery.js";
8
8
 
9
9
  /**
10
- * Integration tests for the consolidated viewer.
10
+ * Integration tests for the viewer architecture.
11
11
  *
12
- * After packages were merged into @fragments-sdk/cli, the viewer's
13
- * path resolution changed. These tests verify that:
14
- * - Viewer assets (HTML, TSX entry points) are found at the correct paths
15
- * - The @fragments-sdk/cli/core alias resolves to the consolidated core source
12
+ * After the viewer extraction (Delivery A), viewer UI code lives in
13
+ * packages/viewer/ while CLI keeps the Vite server and plugin.
14
+ * These tests verify:
15
+ * - Viewer assets (HTML, TSX entry points) are found in packages/viewer/
16
+ * - The CLI server/plugin reference the viewer package correctly
17
+ * - The @fragments-sdk/cli/core alias resolves to consolidated core source
16
18
  * - The virtual module generates valid import statements
17
- * - The Vite config references correct file system locations
18
19
  */
19
20
 
20
- // Simulate the same path resolution used in server.ts and vite-plugin.ts
21
- // At runtime, __dirname is dist/. In tests (vitest), it's the source dir.
22
21
  const testDir = dirname(fileURLToPath(import.meta.url));
23
22
  const viewerDir = resolve(testDir, "..");
24
23
  const cliPackageRoot = resolve(viewerDir, "../..");
24
+ const packagesRoot = resolve(cliPackageRoot, "..");
25
+ const viewerPackageRoot = resolve(packagesRoot, "viewer");
26
+ const viewerSrc = resolve(viewerPackageRoot, "src");
25
27
 
26
- describe("viewer path resolution", () => {
27
- it("viewerRoot resolves to a directory containing index.html", () => {
28
- // server.ts: viewerRoot = resolve(cliPackageRoot, "src/viewer")
29
- const viewerRoot = resolve(cliPackageRoot, "src/viewer");
30
- expect(existsSync(resolve(viewerRoot, "index.html"))).toBe(true);
28
+ describe("viewer package path resolution", () => {
29
+ it("viewer package root exists", () => {
30
+ expect(existsSync(viewerPackageRoot)).toBe(true);
31
31
  });
32
32
 
33
- it("viewerRoot contains entry.tsx", () => {
34
- const viewerRoot = resolve(cliPackageRoot, "src/viewer");
35
- expect(existsSync(resolve(viewerRoot, "entry.tsx"))).toBe(true);
33
+ it("viewer package contains index.html at root", () => {
34
+ expect(existsSync(resolve(viewerPackageRoot, "index.html"))).toBe(true);
36
35
  });
37
36
 
38
- it("viewerRoot contains preview-frame.html", () => {
39
- const viewerRoot = resolve(cliPackageRoot, "src/viewer");
40
- expect(existsSync(resolve(viewerRoot, "preview-frame.html"))).toBe(true);
37
+ it("viewer package contains entry.tsx", () => {
38
+ expect(existsSync(resolve(viewerSrc, "entry.tsx"))).toBe(true);
41
39
  });
42
40
 
43
- it("viewerRoot contains preview-frame-entry.tsx", () => {
44
- const viewerRoot = resolve(cliPackageRoot, "src/viewer");
45
- expect(existsSync(resolve(viewerRoot, "preview-frame-entry.tsx"))).toBe(true);
41
+ it("viewer package contains preview-frame.html in src/", () => {
42
+ expect(existsSync(resolve(viewerSrc, "preview-frame.html"))).toBe(true);
46
43
  });
47
44
 
48
- it("viewerRoot contains render-template.html", () => {
49
- const viewerRoot = resolve(cliPackageRoot, "src/viewer");
50
- expect(existsSync(resolve(viewerRoot, "render-template.html"))).toBe(true);
45
+ it("viewer package contains preview-frame-entry.tsx", () => {
46
+ expect(existsSync(resolve(viewerSrc, "preview-frame-entry.tsx"))).toBe(
47
+ true
48
+ );
49
+ });
50
+
51
+ it("viewer package contains render-template.html in src/", () => {
52
+ expect(existsSync(resolve(viewerSrc, "render-template.html"))).toBe(true);
51
53
  });
52
54
 
53
- it("viewerRoot does not contain tailwind.config.js (removed in Tailwind→Fragments migration)", () => {
54
- const viewerRoot = resolve(cliPackageRoot, "src/viewer");
55
- expect(existsSync(resolve(viewerRoot, "tailwind.config.js"))).toBe(false);
55
+ it("viewer package contains shared/index.ts barrel", () => {
56
+ expect(existsSync(resolve(viewerSrc, "shared/index.ts"))).toBe(true);
56
57
  });
57
58
 
58
- it("viewerRoot contains styles/globals.css", () => {
59
- const viewerRoot = resolve(cliPackageRoot, "src/viewer");
60
- expect(existsSync(resolve(viewerRoot, "styles/globals.css"))).toBe(true);
59
+ it("viewer package contains app/index.ts barrel", () => {
60
+ expect(existsSync(resolve(viewerSrc, "app/index.ts"))).toBe(true);
61
+ });
62
+ });
63
+
64
+ describe("CLI viewer server references viewer package", () => {
65
+ it("server.ts has resolveViewerRoot function for monorepo and npm resolution", async () => {
66
+ const serverPath = resolve(viewerDir, "server.ts");
67
+ const content = await readFile(serverPath, "utf-8");
68
+
69
+ expect(content).toContain("function resolveViewerRoot(nodeModulesDir: string)");
70
+ // Checks monorepo path first
71
+ expect(content).toContain('resolve(packagesRoot, "viewer")');
72
+ // Falls back to npm-installed path
73
+ expect(content).toContain("@fragments-sdk/viewer");
74
+ });
75
+
76
+ it("server.ts sets up viewer subpath aliases", async () => {
77
+ const serverPath = resolve(viewerDir, "server.ts");
78
+ const content = await readFile(serverPath, "utf-8");
79
+
80
+ expect(content).toContain("@fragments-sdk/viewer/shared");
81
+ expect(content).toContain("@fragments-sdk/viewer/app");
82
+ });
83
+
84
+ it("server.ts does not reference old resolveFragmentsPackage for core", async () => {
85
+ const serverPath = resolve(viewerDir, "server.ts");
86
+ const content = await readFile(serverPath, "utf-8");
87
+
88
+ expect(content).not.toContain('resolveFragmentsPackage("core"');
89
+ });
90
+
91
+ it("no source files import from @fragments-sdk/cli/core as a runtime dependency", async () => {
92
+ const serverPath = resolve(viewerDir, "server.ts");
93
+ const content = await readFile(serverPath, "utf-8");
94
+
95
+ const lines = content.split("\n");
96
+ const importLines = lines.filter(
97
+ (l) => l.startsWith("import") && l.includes("@fragments-sdk/cli/core")
98
+ );
99
+ expect(importLines).toHaveLength(0);
61
100
  });
62
101
  });
63
102
 
64
103
  describe("@fragments-sdk/cli/core alias resolution", () => {
65
104
  it("core/index.ts exists at the expected path", () => {
66
- // server.ts: "@fragments-sdk/cli/core": resolve(cliPackageRoot, "src/core/index.ts")
67
105
  const corePath = resolve(cliPackageRoot, "src/core/index.ts");
68
106
  expect(existsSync(corePath)).toBe(true);
69
107
  });
@@ -71,19 +109,15 @@ describe("@fragments-sdk/cli/core alias resolution", () => {
71
109
  it("core/index.ts re-exports from @fragments-sdk/core", async () => {
72
110
  const corePath = resolve(cliPackageRoot, "src/core/index.ts");
73
111
  const content = await readFile(corePath, "utf-8");
74
- // After core extraction, cli/core is a thin re-export shim
75
112
  expect(content).toContain("@fragments-sdk/core");
76
113
  });
77
114
  });
78
115
 
79
116
  describe("virtual module @fragments-sdk/cli/core import", () => {
80
117
  it("vite-plugin generates import from @fragments-sdk/cli/core (resolved via alias)", async () => {
81
- // The virtual module template in vite-plugin.ts must import from @fragments-sdk/cli/core
82
- // which is resolved by the Vite alias to the consolidated core source
83
118
  const pluginPath = resolve(viewerDir, "vite-plugin.ts");
84
119
  const content = await readFile(pluginPath, "utf-8");
85
120
 
86
- // The generated virtual module string should reference @fragments-sdk/cli/core
87
121
  expect(content).toContain(
88
122
  'import { storyModuleToFragment, setPreviewConfig, checkStoryExclusion, isForceIncluded, isConfigExcluded } from "@fragments-sdk/cli/core"'
89
123
  );
@@ -93,43 +127,47 @@ describe("virtual module @fragments-sdk/cli/core import", () => {
93
127
  const serverPath = resolve(viewerDir, "server.ts");
94
128
  const content = await readFile(serverPath, "utf-8");
95
129
 
96
- // The alias must resolve @fragments-sdk/cli/core to the CLI's core source
97
- expect(content).toContain('"@fragments-sdk/cli/core": resolve(cliPackageRoot, "src/core/index.ts")');
130
+ expect(content).toContain(
131
+ '"@fragments-sdk/cli/core": resolve(cliPackageRoot, "src/core/index.ts")'
132
+ );
98
133
  });
99
134
 
100
135
  it("vite-plugin merges authored variant code from metadata fragments", async () => {
101
136
  const pluginPath = resolve(viewerDir, "vite-plugin.ts");
102
137
  const content = await readFile(pluginPath, "utf-8");
103
138
 
104
- expect(content).toContain("if (metaVariant.code && !fragmentVariant.code)");
139
+ expect(content).toContain(
140
+ "if (metaVariant.code && !fragmentVariant.code)"
141
+ );
105
142
  expect(content).toContain("fragmentVariant.code = metaVariant.code;");
106
143
  });
107
144
  });
108
145
 
109
146
  describe("viewer HTML templates", () => {
110
147
  it("index.html contains entry.tsx script reference", async () => {
111
- const htmlPath = resolve(viewerDir, "index.html");
148
+ const htmlPath = resolve(viewerPackageRoot, "index.html");
112
149
  const content = await readFile(htmlPath, "utf-8");
113
150
 
114
- // The HTML references /src/entry.tsx which gets rewritten to an absolute path
115
151
  expect(content).toContain('src="/src/entry.tsx"');
116
152
  });
117
153
 
118
154
  it("preview-frame.html contains preview-frame-entry.tsx reference", async () => {
119
- const htmlPath = resolve(viewerDir, "preview-frame.html");
155
+ const htmlPath = resolve(viewerSrc, "preview-frame.html");
120
156
  const content = await readFile(htmlPath, "utf-8");
121
157
 
122
158
  expect(content).toContain('src="/src/preview-frame-entry.tsx"');
123
159
  });
124
160
 
125
- it("vite-plugin rewrites entry paths using viewerAssetsRoot", async () => {
161
+ it("vite-plugin resolves viewerAssetsRoot with npm fallback", async () => {
126
162
  const pluginPath = resolve(viewerDir, "vite-plugin.ts");
127
163
  const content = await readFile(pluginPath, "utf-8");
128
164
 
129
- // The plugin must use viewerAssetsRoot (not __dirname or resolve(__dirname, ".."))
130
- // to find viewer HTML and entry files
131
- expect(content).toContain("const viewerAssetsRoot = resolve(__dirname,");
132
- expect(content).toContain('const viewerRoot = viewerAssetsRoot');
165
+ // Tries monorepo path first
166
+ expect(content).toContain("monorepoViewerSrc");
167
+ // Falls back to npm-installed
168
+ expect(content).toContain("npmViewerSrc");
169
+ // Resolves to a final viewerAssetsRoot
170
+ expect(content).toContain("const viewerAssetsRoot =");
133
171
  });
134
172
  });
135
173
 
@@ -139,7 +177,6 @@ describe("discoverInstalledFragments", () => {
139
177
  beforeAll(async () => {
140
178
  tmpDir = await mkdtemp(resolve(tmpdir(), "fragments-test-"));
141
179
 
142
- // Create a fake project with a dependency that has fragments
143
180
  await writeFile(
144
181
  resolve(tmpDir, "package.json"),
145
182
  JSON.stringify({
@@ -147,7 +184,6 @@ describe("discoverInstalledFragments", () => {
147
184
  })
148
185
  );
149
186
 
150
- // @acme/ui declares fragments
151
187
  const acmeDir = resolve(tmpDir, "node_modules/@acme/ui");
152
188
  await mkdir(resolve(acmeDir, "src/components"), { recursive: true });
153
189
  await writeFile(
@@ -163,7 +199,6 @@ describe("discoverInstalledFragments", () => {
163
199
  "export default {};"
164
200
  );
165
201
 
166
- // some-lib does NOT declare fragments
167
202
  const someLibDir = resolve(tmpDir, "node_modules/some-lib");
168
203
  await mkdir(resolve(someLibDir, "src"), { recursive: true });
169
204
  await writeFile(
@@ -209,27 +244,3 @@ describe("discoverInstalledFragments", () => {
209
244
  await rm(emptyDir, { recursive: true, force: true });
210
245
  });
211
246
  });
212
-
213
- describe("no stale @fragments/* package references", () => {
214
- it("server.ts does not reference old resolveFragmentsPackage for core", async () => {
215
- const serverPath = resolve(viewerDir, "server.ts");
216
- const content = await readFile(serverPath, "utf-8");
217
-
218
- // Should NOT try to resolve @fragments-sdk/cli/core from node_modules
219
- expect(content).not.toContain('resolveFragmentsPackage("core"');
220
- });
221
-
222
- it("no source files import from @fragments-sdk/cli/core as a runtime dependency", async () => {
223
- // All source-level imports should use relative paths (../core/).
224
- // Only the generated virtual module string uses @fragments-sdk/cli/core (resolved via alias).
225
- const serverPath = resolve(viewerDir, "server.ts");
226
- const content = await readFile(serverPath, "utf-8");
227
-
228
- // server.ts should import from relative paths, not @fragments-sdk/cli/core
229
- const lines = content.split("\n");
230
- const importLines = lines.filter(
231
- (l) => l.startsWith("import") && l.includes("@fragments-sdk/cli/core")
232
- );
233
- expect(importLines).toHaveLength(0);
234
- });
235
- });