@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.
- package/dist/bin.js +26 -8
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-ZDA3PLQ6.js → chunk-5G3VZH43.js} +2 -2
- package/dist/{chunk-566BNPQZ.js → chunk-HRFUSSZI.js} +25 -6
- package/dist/chunk-HRFUSSZI.js.map +1 -0
- package/dist/{chunk-CAMXG5HJ.js → chunk-ZM4ZQZWZ.js} +2 -2
- package/dist/{generate-BGKTKO6E.js → generate-FBHSXR3D.js} +2 -2
- package/dist/index.js +2 -2
- package/dist/{init-Q53R5Q2T.js → init-UFGK5TCN.js} +77 -6
- package/dist/init-UFGK5TCN.js.map +1 -0
- package/dist/{scan-OQU7M4GH.js → scan-CJF2DOQW.js} +3 -3
- package/dist/{scan-generate-T5QNUG7N.js → scan-generate-SJAN5MVI.js} +2 -2
- package/dist/snapshot-SV2JOFZH.js +139 -0
- package/dist/snapshot-SV2JOFZH.js.map +1 -0
- package/dist/{test-2CSOSS3B.js → test-Z5LVO724.js} +2 -2
- package/dist/{tokens-DXEGYTOJ.js → tokens-CE46OTMD.js} +2 -2
- package/dist/{viewer-DBEPYM3G.js → viewer-DLLJIMCK.js} +69 -47
- package/dist/viewer-DLLJIMCK.js.map +1 -0
- package/package.json +6 -14
- package/src/bin.ts +30 -0
- package/src/commands/init.ts +76 -1
- package/src/commands/snapshot.ts +197 -0
- package/src/core/loader.ts +38 -8
- package/src/viewer/__tests__/viewer-integration.test.ts +85 -74
- package/src/viewer/server.ts +37 -22
- package/src/viewer/vite-plugin.ts +25 -9
- package/dist/chunk-566BNPQZ.js.map +0 -1
- package/dist/init-Q53R5Q2T.js.map +0 -1
- package/dist/viewer-DBEPYM3G.js.map +0 -1
- package/src/viewer/__tests__/a11y-fixes.test.ts +0 -358
- package/src/viewer/__tests__/jsx-parser.test.ts +0 -502
- package/src/viewer/__tests__/render-utils.test.ts +0 -232
- package/src/viewer/__tests__/style-utils.test.ts +0 -404
- package/src/viewer/assets/fragments-logo.ts +0 -4
- package/src/viewer/components/AccessibilityPanel.tsx +0 -1457
- package/src/viewer/components/ActionCapture.tsx +0 -172
- package/src/viewer/components/ActionsPanel.tsx +0 -332
- package/src/viewer/components/AllVariantsPreview.tsx +0 -78
- package/src/viewer/components/App.tsx +0 -582
- package/src/viewer/components/BottomPanel.tsx +0 -288
- package/src/viewer/components/CodePanel.naming.test.tsx +0 -59
- package/src/viewer/components/CodePanel.tsx +0 -118
- package/src/viewer/components/CommandPalette.tsx +0 -392
- package/src/viewer/components/ComponentDocView.tsx +0 -164
- package/src/viewer/components/ComponentGraph.tsx +0 -380
- package/src/viewer/components/ComponentHeader.tsx +0 -88
- package/src/viewer/components/ContractPanel.tsx +0 -241
- package/src/viewer/components/EmptyVariantMessage.tsx +0 -54
- package/src/viewer/components/ErrorBoundary.tsx +0 -97
- package/src/viewer/components/FigmaEmbed.tsx +0 -238
- package/src/viewer/components/FragmentEditor.tsx +0 -525
- package/src/viewer/components/FragmentRenderer.tsx +0 -61
- package/src/viewer/components/HeaderSearch.tsx +0 -24
- package/src/viewer/components/HealthDashboard.tsx +0 -441
- package/src/viewer/components/HmrStatusIndicator.tsx +0 -61
- package/src/viewer/components/Icons.tsx +0 -479
- package/src/viewer/components/InteractionsPanel.tsx +0 -757
- package/src/viewer/components/IsolatedPreviewFrame.tsx +0 -346
- package/src/viewer/components/IsolatedRender.tsx +0 -113
- package/src/viewer/components/KeyboardShortcutsHelp.tsx +0 -53
- package/src/viewer/components/LandingPage.tsx +0 -421
- package/src/viewer/components/Layout.tsx +0 -27
- package/src/viewer/components/LeftSidebar.tsx +0 -472
- package/src/viewer/components/LoadErrorMessage.tsx +0 -102
- package/src/viewer/components/MultiViewportPreview.tsx +0 -522
- package/src/viewer/components/NoVariantsMessage.tsx +0 -59
- package/src/viewer/components/PanelShell.tsx +0 -161
- package/src/viewer/components/PerformancePanel.tsx +0 -304
- package/src/viewer/components/PreviewArea.tsx +0 -472
- package/src/viewer/components/PreviewAside.tsx +0 -168
- package/src/viewer/components/PreviewFrameHost.tsx +0 -303
- package/src/viewer/components/PreviewPane.tsx +0 -149
- package/src/viewer/components/PreviewToolbar.tsx +0 -80
- package/src/viewer/components/PropsEditor.tsx +0 -506
- package/src/viewer/components/PropsTable.tsx +0 -111
- package/src/viewer/components/RelationsSection.tsx +0 -88
- package/src/viewer/components/ResizablePanel.tsx +0 -271
- package/src/viewer/components/RightSidebar.tsx +0 -102
- package/src/viewer/components/RuntimeToolsRegistrar.tsx +0 -17
- package/src/viewer/components/ScreenshotButton.tsx +0 -90
- package/src/viewer/components/Sidebar.tsx +0 -169
- package/src/viewer/components/SkeletonLoader.tsx +0 -161
- package/src/viewer/components/ThemeProvider.tsx +0 -42
- package/src/viewer/components/Toast.tsx +0 -3
- package/src/viewer/components/TokenStylePanel.tsx +0 -699
- package/src/viewer/components/TopToolbar.tsx +0 -159
- package/src/viewer/components/UsageSection.tsx +0 -95
- package/src/viewer/components/VariantMatrix.tsx +0 -388
- package/src/viewer/components/VariantRenderer.tsx +0 -131
- package/src/viewer/components/VariantTabs.tsx +0 -40
- package/src/viewer/components/ViewerHeader.tsx +0 -69
- package/src/viewer/components/ViewerStateSync.tsx +0 -52
- package/src/viewer/components/ViewportSelector.tsx +0 -172
- package/src/viewer/components/WebMCPDevTools.tsx +0 -503
- package/src/viewer/components/WebMCPIntegration.tsx +0 -47
- package/src/viewer/components/WebMCPStatusIndicator.tsx +0 -60
- package/src/viewer/components/_future/CreatePage.tsx +0 -836
- package/src/viewer/components/viewer-utils.ts +0 -16
- package/src/viewer/composition-renderer.ts +0 -381
- package/src/viewer/constants/index.ts +0 -1
- package/src/viewer/constants/ui.ts +0 -166
- package/src/viewer/entry.tsx +0 -335
- package/src/viewer/hooks/index.ts +0 -2
- package/src/viewer/hooks/useA11yCache.ts +0 -383
- package/src/viewer/hooks/useA11yService.ts +0 -364
- package/src/viewer/hooks/useActions.ts +0 -138
- package/src/viewer/hooks/useAppState.ts +0 -147
- package/src/viewer/hooks/useCompiledFragments.ts +0 -42
- package/src/viewer/hooks/useFigmaIntegration.ts +0 -132
- package/src/viewer/hooks/useHmrStatus.ts +0 -109
- package/src/viewer/hooks/useKeyboardShortcuts.ts +0 -270
- package/src/viewer/hooks/usePreviewBridge.ts +0 -347
- package/src/viewer/hooks/useScrollSpy.ts +0 -78
- package/src/viewer/hooks/useUrlState.ts +0 -318
- package/src/viewer/hooks/useViewSettings.ts +0 -111
- package/src/viewer/index.html +0 -28
- package/src/viewer/intelligence/healthReport.ts +0 -505
- package/src/viewer/intelligence/styleDrift.ts +0 -340
- package/src/viewer/intelligence/usageScanner.ts +0 -309
- package/src/viewer/jsx-parser.ts +0 -486
- package/src/viewer/preview-frame-entry.tsx +0 -25
- package/src/viewer/preview-frame.html +0 -125
- package/src/viewer/public/favicon.ico +0 -0
- package/src/viewer/render-template.html +0 -68
- package/src/viewer/styles/globals.css +0 -278
- package/src/viewer/types/a11y.ts +0 -197
- package/src/viewer/utils/a11y-fixes.ts +0 -509
- package/src/viewer/utils/actionExport.ts +0 -372
- package/src/viewer/utils/colorSchemes.ts +0 -201
- package/src/viewer/utils/detectRelationships.ts +0 -256
- package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss +0 -10
- package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss.d.ts +0 -2
- package/src/viewer/vendor/shared/src/ComponentDocContent.tsx +0 -274
- package/src/viewer/vendor/shared/src/DocsHeaderBar.tsx +0 -129
- package/src/viewer/vendor/shared/src/DocsPageAsideHost.tsx +0 -89
- package/src/viewer/vendor/shared/src/DocsPageShell.tsx +0 -124
- package/src/viewer/vendor/shared/src/DocsSearchCommand.tsx +0 -99
- package/src/viewer/vendor/shared/src/DocsSidebarNav.tsx +0 -66
- package/src/viewer/vendor/shared/src/PropsTable.module.scss +0 -68
- package/src/viewer/vendor/shared/src/PropsTable.module.scss.d.ts +0 -2
- package/src/viewer/vendor/shared/src/PropsTable.tsx +0 -76
- package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss +0 -114
- package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss.d.ts +0 -2
- package/src/viewer/vendor/shared/src/VariantPreviewCard.tsx +0 -134
- package/src/viewer/vendor/shared/src/docs-data/index.ts +0 -32
- package/src/viewer/vendor/shared/src/docs-data/mcp-configs.ts +0 -72
- package/src/viewer/vendor/shared/src/docs-data/palettes.ts +0 -75
- package/src/viewer/vendor/shared/src/docs-data/setup-examples.ts +0 -55
- package/src/viewer/vendor/shared/src/docs-layout.scss +0 -28
- package/src/viewer/vendor/shared/src/docs-layout.scss.d.ts +0 -2
- package/src/viewer/vendor/shared/src/index.ts +0 -34
- package/src/viewer/vendor/shared/src/types.ts +0 -53
- package/src/viewer/webmcp/__tests__/analytics.test.ts +0 -108
- package/src/viewer/webmcp/analytics.ts +0 -165
- package/src/viewer/webmcp/index.ts +0 -3
- package/src/viewer/webmcp/posthog-bridge.ts +0 -39
- package/src/viewer/webmcp/runtime-tools.ts +0 -152
- package/src/viewer/webmcp/scan-utils.ts +0 -135
- package/src/viewer/webmcp/use-tool-analytics.ts +0 -69
- package/src/viewer/webmcp/viewer-state.ts +0 -45
- /package/dist/{chunk-ZDA3PLQ6.js.map → chunk-5G3VZH43.js.map} +0 -0
- /package/dist/{chunk-CAMXG5HJ.js.map → chunk-ZM4ZQZWZ.js.map} +0 -0
- /package/dist/{generate-BGKTKO6E.js.map → generate-FBHSXR3D.js.map} +0 -0
- /package/dist/{scan-OQU7M4GH.js.map → scan-CJF2DOQW.js.map} +0 -0
- /package/dist/{scan-generate-T5QNUG7N.js.map → scan-generate-SJAN5MVI.js.map} +0 -0
- /package/dist/{test-2CSOSS3B.js.map → test-Z5LVO724.js.map} +0 -0
- /package/dist/{tokens-DXEGYTOJ.js.map → tokens-CE46OTMD.js.map} +0 -0
package/src/commands/init.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/core/loader.ts
CHANGED
|
@@ -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 .
|
|
70
|
-
const tempFile = join(sourceDir, `.${baseName}.fragments-temp-${Date.now()}.
|
|
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: '
|
|
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
|
|
10
|
+
* Integration tests for the viewer architecture.
|
|
11
11
|
*
|
|
12
|
-
* After
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* -
|
|
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("
|
|
28
|
-
|
|
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("
|
|
34
|
-
|
|
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("
|
|
39
|
-
|
|
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("
|
|
44
|
-
|
|
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("
|
|
49
|
-
|
|
50
|
-
|
|
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("
|
|
54
|
-
|
|
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("
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
expect(content).toContain(
|
|
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
|
-
});
|