@fragments-sdk/cli 0.7.14 → 0.7.16

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 (67) hide show
  1. package/dist/bin.js +7 -7
  2. package/dist/{chunk-CRTN6BIW.js → chunk-QLTLLQBI.js} +2 -2
  3. package/dist/{chunk-TQOGBAOZ.js → chunk-WLXFE6XW.js} +91 -2
  4. package/dist/chunk-WLXFE6XW.js.map +1 -0
  5. package/dist/core/index.d.ts +44 -3
  6. package/dist/core/index.js +11 -3
  7. package/dist/{defineFragment-C6PFzZyo.d.ts → defineFragment-BI9KoPrs.d.ts} +1 -1
  8. package/dist/{generate-ZPERYZLF.js → generate-ICIPKCKV.js} +2 -2
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.js +2 -2
  11. package/dist/init-DIZ6UNBL.js +806 -0
  12. package/dist/init-DIZ6UNBL.js.map +1 -0
  13. package/dist/mcp-bin.js +2 -2
  14. package/dist/{scan-BSMLGBX4.js → scan-X3DI2X5G.js} +2 -2
  15. package/dist/{service-QACVPR37.js → service-JEWWTSKI.js} +2 -2
  16. package/dist/{static-viewer-2RQD5QLR.js → static-viewer-JIWCYKVK.js} +2 -2
  17. package/dist/{tokens-A3BZIQPB.js → tokens-K2AGUUOJ.js} +2 -2
  18. package/dist/{viewer-CNLZQUFO.js → viewer-QKIAPTPG.js} +126 -15
  19. package/dist/viewer-QKIAPTPG.js.map +1 -0
  20. package/package.json +3 -2
  21. package/src/commands/init-framework.ts +414 -0
  22. package/src/commands/init.ts +41 -1
  23. package/src/core/__tests__/preview-runtime.test.tsx +111 -0
  24. package/src/core/index.ts +13 -0
  25. package/src/core/preview-runtime.tsx +144 -0
  26. package/src/viewer/components/App.tsx +8 -3
  27. package/src/viewer/components/FragmentRenderer.tsx +61 -0
  28. package/src/viewer/components/HealthDashboard.tsx +1 -1
  29. package/src/viewer/components/IsolatedPreviewFrame.tsx +10 -8
  30. package/src/viewer/components/PreviewFrameHost.tsx +27 -60
  31. package/src/viewer/components/PropsTable.tsx +2 -2
  32. package/src/viewer/components/RuntimeToolsRegistrar.tsx +17 -0
  33. package/src/viewer/components/SkeletonLoader.tsx +114 -125
  34. package/src/viewer/components/VariantMatrix.tsx +3 -3
  35. package/src/viewer/components/ViewerStateSync.tsx +52 -0
  36. package/src/viewer/components/WebMCPDevTools.tsx +509 -0
  37. package/src/viewer/components/WebMCPIntegration.tsx +47 -0
  38. package/src/viewer/components/WebMCPStatusIndicator.tsx +60 -0
  39. package/src/viewer/entry.tsx +32 -5
  40. package/src/viewer/hooks/useA11yService.ts +1 -135
  41. package/src/viewer/hooks/useCompiledFragments.ts +42 -0
  42. package/src/viewer/index.html +1 -1
  43. package/src/viewer/public/favicon.ico +0 -0
  44. package/src/viewer/server.ts +59 -3
  45. package/src/viewer/vendor/shared/src/DocsHeaderBar.tsx +19 -0
  46. package/src/viewer/vendor/shared/src/DocsPageAsideHost.tsx +1 -1
  47. package/src/viewer/vendor/shared/src/DocsSearchCommand.tsx +69 -104
  48. package/src/viewer/vite-plugin.ts +76 -1
  49. package/src/viewer/webmcp/__tests__/analytics.test.ts +108 -0
  50. package/src/viewer/webmcp/analytics.ts +165 -0
  51. package/src/viewer/webmcp/index.ts +3 -0
  52. package/src/viewer/webmcp/posthog-bridge.ts +39 -0
  53. package/src/viewer/webmcp/runtime-tools.ts +152 -0
  54. package/src/viewer/webmcp/scan-utils.ts +135 -0
  55. package/src/viewer/webmcp/use-tool-analytics.ts +69 -0
  56. package/src/viewer/webmcp/viewer-state.ts +45 -0
  57. package/dist/chunk-TQOGBAOZ.js.map +0 -1
  58. package/dist/init-GID2DXB3.js +0 -498
  59. package/dist/init-GID2DXB3.js.map +0 -1
  60. package/dist/viewer-CNLZQUFO.js.map +0 -1
  61. package/src/viewer/components/StoryRenderer.tsx +0 -121
  62. /package/dist/{chunk-CRTN6BIW.js.map → chunk-QLTLLQBI.js.map} +0 -0
  63. /package/dist/{generate-ZPERYZLF.js.map → generate-ICIPKCKV.js.map} +0 -0
  64. /package/dist/{scan-BSMLGBX4.js.map → scan-X3DI2X5G.js.map} +0 -0
  65. /package/dist/{service-QACVPR37.js.map → service-JEWWTSKI.js.map} +0 -0
  66. /package/dist/{static-viewer-2RQD5QLR.js.map → static-viewer-JIWCYKVK.js.map} +0 -0
  67. /package/dist/{tokens-A3BZIQPB.js.map → tokens-K2AGUUOJ.js.map} +0 -0
@@ -0,0 +1,414 @@
1
+ /**
2
+ * Framework detection and auto-configuration for fragments init.
3
+ *
4
+ * Detects the consumer's framework (Next.js, Vite, Remix, Astro)
5
+ * and generates appropriate configuration files.
6
+ */
7
+
8
+ import { readFile, writeFile, access } from "node:fs/promises";
9
+ import { join } from "node:path";
10
+ import pc from "picocolors";
11
+
12
+ // ============================================
13
+ // Types
14
+ // ============================================
15
+
16
+ export type Framework = "nextjs" | "vite" | "remix" | "astro" | "unknown";
17
+
18
+ export interface FrameworkDetection {
19
+ framework: Framework;
20
+ /** Package that triggered the detection */
21
+ detectedBy: string | null;
22
+ }
23
+
24
+ export interface FrameworkSetupOptions {
25
+ /** Project root directory */
26
+ projectRoot: string;
27
+ /** Override auto-detected framework */
28
+ framework?: Framework;
29
+ /** Seed overrides for globals.scss generation */
30
+ seeds?: {
31
+ brand?: string;
32
+ neutral?: string;
33
+ density?: string;
34
+ radiusStyle?: string;
35
+ };
36
+ }
37
+
38
+ export interface FrameworkSetupResult {
39
+ framework: Framework;
40
+ filesCreated: string[];
41
+ packagesToInstall: string[];
42
+ configModified: string[];
43
+ warnings: string[];
44
+ }
45
+
46
+ // ============================================
47
+ // Framework Detection
48
+ // ============================================
49
+
50
+ /**
51
+ * Detect framework from package.json dependencies
52
+ */
53
+ export async function detectFramework(
54
+ projectRoot: string
55
+ ): Promise<FrameworkDetection> {
56
+ try {
57
+ const pkgPath = join(projectRoot, "package.json");
58
+ const pkgContent = await readFile(pkgPath, "utf-8");
59
+ const pkg = JSON.parse(pkgContent);
60
+
61
+ const allDeps = {
62
+ ...pkg.dependencies,
63
+ ...pkg.devDependencies,
64
+ };
65
+
66
+ // Check in order of specificity
67
+ if (allDeps["next"]) {
68
+ return { framework: "nextjs", detectedBy: "next" };
69
+ }
70
+ if (allDeps["@remix-run/react"]) {
71
+ return { framework: "remix", detectedBy: "@remix-run/react" };
72
+ }
73
+ if (allDeps["astro"]) {
74
+ return { framework: "astro", detectedBy: "astro" };
75
+ }
76
+ if (allDeps["vite"]) {
77
+ return { framework: "vite", detectedBy: "vite" };
78
+ }
79
+
80
+ return { framework: "unknown", detectedBy: null };
81
+ } catch {
82
+ return { framework: "unknown", detectedBy: null };
83
+ }
84
+ }
85
+
86
+ // ============================================
87
+ // Globals SCSS Generation
88
+ // ============================================
89
+
90
+ function generateGlobalsSCSS(seeds?: FrameworkSetupOptions["seeds"]): string {
91
+ const withClauses: string[] = [];
92
+
93
+ if (seeds?.brand) {
94
+ withClauses.push(` $fui-brand: ${seeds.brand}`);
95
+ }
96
+ if (seeds?.neutral) {
97
+ withClauses.push(` $fui-neutral: "${seeds.neutral}"`);
98
+ }
99
+ if (seeds?.density) {
100
+ withClauses.push(` $fui-density: "${seeds.density}"`);
101
+ }
102
+ if (seeds?.radiusStyle) {
103
+ withClauses.push(` $fui-radius-style: "${seeds.radiusStyle}"`);
104
+ }
105
+
106
+ const useStatement =
107
+ withClauses.length > 0
108
+ ? `@use '@fragments-sdk/ui/styles' with (\n${withClauses.join(",\n")}\n);`
109
+ : `@use '@fragments-sdk/ui/styles';`;
110
+
111
+ return `// Fragments SDK Global Styles
112
+ // Customize seed values to theme the entire design system.
113
+ // See: https://usefragments.com/docs/theming
114
+ ${useStatement}
115
+ `;
116
+ }
117
+
118
+ // ============================================
119
+ // Providers Component Generation
120
+ // ============================================
121
+
122
+ function generateProviders(): string {
123
+ return `'use client';
124
+
125
+ import { ThemeProvider } from '@fragments-sdk/ui';
126
+
127
+ export function Providers({ children }: { children: React.ReactNode }) {
128
+ return (
129
+ <ThemeProvider defaultTheme="system" attribute="data-theme">
130
+ {children}
131
+ </ThemeProvider>
132
+ );
133
+ }
134
+ `;
135
+ }
136
+
137
+ // ============================================
138
+ // Per-Framework Configuration
139
+ // ============================================
140
+
141
+ async function setupNextJS(
142
+ projectRoot: string,
143
+ options: FrameworkSetupOptions
144
+ ): Promise<FrameworkSetupResult> {
145
+ const result: FrameworkSetupResult = {
146
+ framework: "nextjs",
147
+ filesCreated: [],
148
+ packagesToInstall: [],
149
+ configModified: [],
150
+ warnings: [],
151
+ };
152
+
153
+ // Check if sass is installed
154
+ try {
155
+ const pkgContent = await readFile(
156
+ join(projectRoot, "package.json"),
157
+ "utf-8"
158
+ );
159
+ const pkg = JSON.parse(pkgContent);
160
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
161
+ if (!allDeps["sass"]) {
162
+ result.packagesToInstall.push("sass");
163
+ }
164
+ } catch {
165
+ // Proceed without checking
166
+ }
167
+
168
+ // Update next.config if transpilePackages is needed
169
+ const nextConfigPaths = [
170
+ "next.config.ts",
171
+ "next.config.mjs",
172
+ "next.config.js",
173
+ ];
174
+
175
+ for (const configName of nextConfigPaths) {
176
+ const configPath = join(projectRoot, configName);
177
+ try {
178
+ await access(configPath);
179
+ const content = await readFile(configPath, "utf-8");
180
+
181
+ if (!content.includes("transpilePackages")) {
182
+ // Add transpilePackages to the config
183
+ if (content.includes("const nextConfig")) {
184
+ const updated = content.replace(
185
+ /const nextConfig\s*=\s*\{/,
186
+ `const nextConfig = {\n transpilePackages: ['@fragments-sdk/ui'],`
187
+ );
188
+ await writeFile(configPath, updated, "utf-8");
189
+ result.configModified.push(configName);
190
+ } else {
191
+ result.warnings.push(
192
+ `Could not auto-modify ${configName}. Add transpilePackages: ['@fragments-sdk/ui'] manually.`
193
+ );
194
+ }
195
+ }
196
+ break;
197
+ } catch {
198
+ continue;
199
+ }
200
+ }
201
+
202
+ // Generate globals.scss
203
+ const globalsPath = join(projectRoot, "src", "styles", "globals.scss");
204
+ try {
205
+ await access(globalsPath);
206
+ result.warnings.push(
207
+ "src/styles/globals.scss already exists. Skipped generation."
208
+ );
209
+ } catch {
210
+ await writeFile(globalsPath, generateGlobalsSCSS(options.seeds), "utf-8");
211
+ result.filesCreated.push("src/styles/globals.scss");
212
+ }
213
+
214
+ // Generate providers.tsx
215
+ const providersPath = join(projectRoot, "src", "providers.tsx");
216
+ try {
217
+ await access(providersPath);
218
+ result.warnings.push("src/providers.tsx already exists. Skipped.");
219
+ } catch {
220
+ await writeFile(providersPath, generateProviders(), "utf-8");
221
+ result.filesCreated.push("src/providers.tsx");
222
+ }
223
+
224
+ return result;
225
+ }
226
+
227
+ async function setupVite(
228
+ projectRoot: string,
229
+ options: FrameworkSetupOptions
230
+ ): Promise<FrameworkSetupResult> {
231
+ const result: FrameworkSetupResult = {
232
+ framework: "vite",
233
+ filesCreated: [],
234
+ packagesToInstall: [],
235
+ configModified: [],
236
+ warnings: [],
237
+ };
238
+
239
+ // Check if sass is installed
240
+ try {
241
+ const pkgContent = await readFile(
242
+ join(projectRoot, "package.json"),
243
+ "utf-8"
244
+ );
245
+ const pkg = JSON.parse(pkgContent);
246
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
247
+ if (!allDeps["sass"]) {
248
+ result.packagesToInstall.push("sass");
249
+ }
250
+ } catch {
251
+ // Proceed
252
+ }
253
+
254
+ // Generate globals.scss
255
+ const globalsPath = join(projectRoot, "src", "styles", "globals.scss");
256
+ try {
257
+ await access(globalsPath);
258
+ result.warnings.push(
259
+ "src/styles/globals.scss already exists. Skipped generation."
260
+ );
261
+ } catch {
262
+ await writeFile(globalsPath, generateGlobalsSCSS(options.seeds), "utf-8");
263
+ result.filesCreated.push("src/styles/globals.scss");
264
+ }
265
+
266
+ // Generate providers.tsx
267
+ const providersPath = join(projectRoot, "src", "providers.tsx");
268
+ try {
269
+ await access(providersPath);
270
+ result.warnings.push("src/providers.tsx already exists. Skipped.");
271
+ } catch {
272
+ await writeFile(providersPath, generateProviders(), "utf-8");
273
+ result.filesCreated.push("src/providers.tsx");
274
+ }
275
+
276
+ return result;
277
+ }
278
+
279
+ async function setupRemix(
280
+ projectRoot: string,
281
+ options: FrameworkSetupOptions
282
+ ): Promise<FrameworkSetupResult> {
283
+ const result: FrameworkSetupResult = {
284
+ framework: "remix",
285
+ filesCreated: [],
286
+ packagesToInstall: [],
287
+ configModified: [],
288
+ warnings: [],
289
+ };
290
+
291
+ result.packagesToInstall.push("sass");
292
+
293
+ // Generate globals.scss in app/styles
294
+ const globalsPath = join(projectRoot, "app", "styles", "globals.scss");
295
+ try {
296
+ await access(globalsPath);
297
+ result.warnings.push(
298
+ "app/styles/globals.scss already exists. Skipped generation."
299
+ );
300
+ } catch {
301
+ await writeFile(globalsPath, generateGlobalsSCSS(options.seeds), "utf-8");
302
+ result.filesCreated.push("app/styles/globals.scss");
303
+ }
304
+
305
+ // Generate providers.tsx
306
+ const providersPath = join(projectRoot, "app", "providers.tsx");
307
+ try {
308
+ await access(providersPath);
309
+ result.warnings.push("app/providers.tsx already exists. Skipped.");
310
+ } catch {
311
+ await writeFile(providersPath, generateProviders(), "utf-8");
312
+ result.filesCreated.push("app/providers.tsx");
313
+ }
314
+
315
+ result.warnings.push(
316
+ 'Add @fragments-sdk/ui to serverDependenciesToBundle in remix.config if using source imports.'
317
+ );
318
+
319
+ return result;
320
+ }
321
+
322
+ async function setupAstro(
323
+ projectRoot: string,
324
+ options: FrameworkSetupOptions
325
+ ): Promise<FrameworkSetupResult> {
326
+ const result: FrameworkSetupResult = {
327
+ framework: "astro",
328
+ filesCreated: [],
329
+ packagesToInstall: [],
330
+ configModified: [],
331
+ warnings: [],
332
+ };
333
+
334
+ result.packagesToInstall.push("sass");
335
+
336
+ // Generate globals.scss
337
+ const globalsPath = join(projectRoot, "src", "styles", "globals.scss");
338
+ try {
339
+ await access(globalsPath);
340
+ result.warnings.push(
341
+ "src/styles/globals.scss already exists. Skipped generation."
342
+ );
343
+ } catch {
344
+ await writeFile(globalsPath, generateGlobalsSCSS(options.seeds), "utf-8");
345
+ result.filesCreated.push("src/styles/globals.scss");
346
+ }
347
+
348
+ return result;
349
+ }
350
+
351
+ // ============================================
352
+ // Main Setup Function
353
+ // ============================================
354
+
355
+ /**
356
+ * Set up framework-specific configuration for @fragments-sdk/ui
357
+ */
358
+ export async function setupFramework(
359
+ options: FrameworkSetupOptions
360
+ ): Promise<FrameworkSetupResult> {
361
+ const { projectRoot } = options;
362
+
363
+ // Detect or use provided framework
364
+ let framework = options.framework;
365
+ if (!framework || framework === "unknown") {
366
+ const detection = await detectFramework(projectRoot);
367
+ framework = detection.framework;
368
+
369
+ if (detection.detectedBy) {
370
+ console.log(
371
+ pc.green(` Detected ${frameworkLabel(framework)}`) +
372
+ pc.dim(` (via ${detection.detectedBy})`)
373
+ );
374
+ }
375
+ } else {
376
+ console.log(pc.green(` Framework: ${frameworkLabel(framework)}`));
377
+ }
378
+
379
+ switch (framework) {
380
+ case "nextjs":
381
+ return setupNextJS(projectRoot, options);
382
+ case "vite":
383
+ return setupVite(projectRoot, options);
384
+ case "remix":
385
+ return setupRemix(projectRoot, options);
386
+ case "astro":
387
+ return setupAstro(projectRoot, options);
388
+ default:
389
+ return {
390
+ framework: "unknown",
391
+ filesCreated: [],
392
+ packagesToInstall: ["sass"],
393
+ configModified: [],
394
+ warnings: [
395
+ "Could not detect framework. Install sass and import @fragments-sdk/ui/styles manually.",
396
+ ],
397
+ };
398
+ }
399
+ }
400
+
401
+ function frameworkLabel(framework: Framework): string {
402
+ switch (framework) {
403
+ case "nextjs":
404
+ return "Next.js";
405
+ case "vite":
406
+ return "Vite";
407
+ case "remix":
408
+ return "Remix";
409
+ case "astro":
410
+ return "Astro";
411
+ default:
412
+ return "Unknown";
413
+ }
414
+ }
@@ -14,6 +14,11 @@ import pc from "picocolors";
14
14
  import { BRAND } from "../core/index.js";
15
15
  import fg from "fast-glob";
16
16
  import { input, confirm, select } from "@inquirer/prompts";
17
+ import {
18
+ setupFramework,
19
+ detectFramework,
20
+ type Framework,
21
+ } from "./init-framework.js";
17
22
 
18
23
  export interface InitOptions {
19
24
  /** Project root directory */
@@ -22,6 +27,8 @@ export interface InitOptions {
22
27
  force?: boolean;
23
28
  /** Non-interactive mode - auto-detect and use defaults */
24
29
  yes?: boolean;
30
+ /** Explicit framework override */
31
+ framework?: string;
25
32
  }
26
33
 
27
34
  export interface InitResult {
@@ -596,7 +603,40 @@ export async function init(options: InitOptions = {}): Promise<InitResult> {
596
603
  }
597
604
  }
598
605
 
599
- // Step 6: Show next steps or start server
606
+ // Step 6: Framework-specific configuration
607
+ console.log(pc.dim("\nConfiguring framework integration...\n"));
608
+
609
+ const frameworkOverride = options.framework as Framework | undefined;
610
+ const frameworkResult = await setupFramework({
611
+ projectRoot,
612
+ framework: frameworkOverride,
613
+ });
614
+
615
+ if (frameworkResult.filesCreated.length > 0) {
616
+ for (const file of frameworkResult.filesCreated) {
617
+ console.log(pc.green(`✓ Created ${file}`));
618
+ }
619
+ }
620
+
621
+ if (frameworkResult.configModified.length > 0) {
622
+ for (const file of frameworkResult.configModified) {
623
+ console.log(pc.green(`✓ Updated ${file}`));
624
+ }
625
+ }
626
+
627
+ if (frameworkResult.packagesToInstall.length > 0) {
628
+ const pkgs = frameworkResult.packagesToInstall.join(" ");
629
+ console.log(
630
+ pc.yellow(`\n⚠ Install required dependencies: `) +
631
+ pc.bold(`pnpm add -D ${pkgs}`)
632
+ );
633
+ }
634
+
635
+ for (const warning of frameworkResult.warnings) {
636
+ console.log(pc.yellow(` Note: ${warning}`));
637
+ }
638
+
639
+ // Step 7: Show next steps or start server
600
640
  if (errors.length === 0) {
601
641
  console.log(pc.green("\n✓ Setup complete!\n"));
602
642
 
@@ -0,0 +1,111 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import {
3
+ executeVariantLoaders,
4
+ resolvePreviewRuntimeState,
5
+ type PreviewVariantLike,
6
+ } from "../preview-runtime.js";
7
+
8
+ describe("executeVariantLoaders", () => {
9
+ it("returns host loadedData when no loaders are defined", async () => {
10
+ const loadedData = { source: "host", count: 1 };
11
+ const result = await executeVariantLoaders(undefined, loadedData);
12
+ expect(result).toEqual(loadedData);
13
+ });
14
+
15
+ it("merges loader results and applies host loadedData last", async () => {
16
+ const result = await executeVariantLoaders(
17
+ [
18
+ async () => ({ count: 1, fromFirst: true }),
19
+ async () => ({ count: 2, fromSecond: true }),
20
+ ],
21
+ { count: 3, fromHost: true },
22
+ );
23
+
24
+ expect(result).toEqual({
25
+ count: 3,
26
+ fromFirst: true,
27
+ fromSecond: true,
28
+ fromHost: true,
29
+ });
30
+ });
31
+ });
32
+
33
+ describe("resolvePreviewRuntimeState", () => {
34
+ it("renders variants with no loaders", async () => {
35
+ const variant: PreviewVariantLike = {
36
+ render: () => "Preview content",
37
+ };
38
+
39
+ const state = await resolvePreviewRuntimeState({ variant });
40
+
41
+ expect(state.isLoading).toBe(false);
42
+ expect(state.error).toBeNull();
43
+ expect(state.content).toBe("Preview content");
44
+ });
45
+
46
+ it("renders with successful loaders", async () => {
47
+ const variant: PreviewVariantLike = {
48
+ loaders: [async () => ({ label: "from-loader" })],
49
+ render: (options) => `Loaded: ${String(options?.loadedData?.label)}`,
50
+ };
51
+
52
+ const state = await resolvePreviewRuntimeState({ variant });
53
+
54
+ expect(state.error).toBeNull();
55
+ expect(state.loadedData).toEqual({ label: "from-loader" });
56
+ expect(state.content).toBe("Loaded: from-loader");
57
+ });
58
+
59
+ it("returns an error when a loader rejects", async () => {
60
+ const variant: PreviewVariantLike = {
61
+ loaders: [async () => { throw new Error("loader failed"); }],
62
+ render: () => "should not render",
63
+ };
64
+
65
+ const state = await resolvePreviewRuntimeState({ variant });
66
+
67
+ expect(state.content).toBeNull();
68
+ expect(state.error?.message).toContain("loader failed");
69
+ });
70
+
71
+ it("returns an error when render throws", async () => {
72
+ const variant: PreviewVariantLike = {
73
+ render: () => {
74
+ throw new Error("render failed");
75
+ },
76
+ };
77
+
78
+ const state = await resolvePreviewRuntimeState({ variant });
79
+
80
+ expect(state.content).toBeNull();
81
+ expect(state.error?.message).toContain("render failed");
82
+ });
83
+
84
+ it("normalizes non-Error throwables", async () => {
85
+ const variant: PreviewVariantLike = {
86
+ loaders: [async () => { throw "string-failure"; }],
87
+ render: () => "never",
88
+ };
89
+
90
+ const state = await resolvePreviewRuntimeState({ variant });
91
+ expect(state.error).not.toBeNull();
92
+ expect(state.error?.message).toContain("string-failure");
93
+ });
94
+
95
+ it("uses host data as override input in render options", async () => {
96
+ const renderSpy = vi.fn((options?: { loadedData?: Record<string, unknown> }) => options?.loadedData);
97
+ const variant: PreviewVariantLike = {
98
+ loaders: [async () => ({ count: 1 })],
99
+ render: renderSpy,
100
+ };
101
+
102
+ const state = await resolvePreviewRuntimeState({
103
+ variant,
104
+ loadedData: { count: 2, source: "host" },
105
+ });
106
+
107
+ expect(renderSpy).toHaveBeenCalledTimes(1);
108
+ expect(state.loadedData).toEqual({ count: 2, source: "host" });
109
+ expect(state.content).toEqual({ count: 2, source: "host" });
110
+ });
111
+ });
package/src/core/index.ts CHANGED
@@ -161,3 +161,16 @@ export type {
161
161
  CompositionSuggestion,
162
162
  CompositionGuideline,
163
163
  } from "./composition.js";
164
+
165
+ // Shared preview runtime
166
+ export {
167
+ executeVariantLoaders,
168
+ resolvePreviewRuntimeState,
169
+ usePreviewVariantRuntime,
170
+ PreviewVariantRuntime,
171
+ } from "./preview-runtime.js";
172
+ export type {
173
+ PreviewVariantLike,
174
+ PreviewRuntimeState,
175
+ PreviewRuntimeOptions,
176
+ } from "./preview-runtime.js";