frameshot-mcp 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -53,7 +53,7 @@ Two lines. Auto-detects changed components. Posts before/after/diff screenshots
53
53
  - uses: actions/checkout@v7
54
54
  with:
55
55
  fetch-depth: 0
56
- - uses: kamegoro/frameshot@v0.8.0
56
+ - uses: kamegoro/frameshot@v0.11.0
57
57
  ```
58
58
 
59
59
  <p align="center">
@@ -70,7 +70,7 @@ Changed `.tsx`, `.jsx`, `.vue`, `.svelte`, `.astro`, `.mdx` files detected autom
70
70
  <summary>Options</summary>
71
71
 
72
72
  ```yaml
73
- - uses: kamegoro/frameshot@v0.8.0
73
+ - uses: kamegoro/frameshot@v0.11.0
74
74
  with:
75
75
  paths: "./src/components/*.tsx" # default: auto-detect
76
76
  extensions: ".jsx,.tsx,.vue" # default: .jsx,.tsx,.vue,.svelte,.astro,.mdx
@@ -95,6 +95,32 @@ render_file("src/components/Dashboard.tsx")
95
95
 
96
96
  Resolves your project's real imports, Tailwind config, CSS Modules, and path aliases — not a CDN polyfill.
97
97
 
98
+ ### Works out of the box with 16 frameworks
99
+
100
+ | Meta-frameworks | Core libraries |
101
+ |---|---|
102
+ | Next.js, Nuxt, SvelteKit, SolidStart | React, Vue, Svelte |
103
+ | Remix / React Router 7, Gatsby | Solid, Preact |
104
+ | Qwik, Astro, Vike | Lit / Web Components |
105
+
106
+ Auto-detected from `package.json`. No config needed for the common case. Framework-specific imports (`next/navigation`, `$app/stores`, `useLoaderData`, …) are stubbed automatically so components render in isolation.
107
+
108
+ ### Mock API calls
109
+
110
+ Components that fetch data from `/api/...` render in their loaded state instead of showing a spinner:
111
+
112
+ ```
113
+ render_file({
114
+ path: "src/components/UserList.tsx",
115
+ mock: {
116
+ "/api/users": [{ id: 1, name: "Alice" }],
117
+ "/api/posts/*": { status: 200, body: { data: [] } },
118
+ },
119
+ })
120
+ ```
121
+
122
+ Pattern syntax: path (`/api/users`), glob (`**/api/*`), or full URL.
123
+
98
124
  ---
99
125
 
100
126
  ## Using with Claude
@@ -351,7 +351,7 @@ var ProjectDetector = class {
351
351
 
352
352
  // src/infrastructure/vite-bundler.ts
353
353
  import { existsSync as existsSync4 } from "fs";
354
- import { createRequire } from "module";
354
+ import { createRequire as createRequire2 } from "module";
355
355
  import { dirname as dirname2, join as join18, resolve as resolve2 } from "path";
356
356
  import { fileURLToPath } from "url";
357
357
 
@@ -360,6 +360,7 @@ import { join as join3 } from "path";
360
360
 
361
361
  // src/infrastructure/adapters/helpers.ts
362
362
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
363
+ import { createRequire } from "module";
363
364
  import { join as join2 } from "path";
364
365
  function hasDep(pkgPath, names) {
365
366
  if (!existsSync2(pkgPath)) return false;
@@ -388,6 +389,20 @@ async function loadPlugin(pkg, fnName) {
388
389
  }
389
390
  return [];
390
391
  }
392
+ async function loadPluginFromProject(projectRoot, pkg, fnName) {
393
+ try {
394
+ const require2 = createRequire(join2(projectRoot, "package.json"));
395
+ const resolvedPath = require2.resolve(pkg);
396
+ const mod = await import(`file://${resolvedPath}`);
397
+ const candidate = fnName ? mod[fnName] : mod.default ?? mod;
398
+ if (typeof candidate === "function") {
399
+ const result = candidate();
400
+ return Array.isArray(result) ? result : [result];
401
+ }
402
+ } catch {
403
+ }
404
+ return loadPlugin(pkg, fnName);
405
+ }
391
406
  function loadReactPlugin() {
392
407
  return loadPlugin("@vitejs/plugin-react");
393
408
  }
@@ -417,13 +432,21 @@ var AstroAdapter = class {
417
432
  if (hasInstalled(projectRoot, "react"))
418
433
  plugins.push(...await loadReactPlugin());
419
434
  if (hasInstalled(projectRoot, "vue"))
420
- plugins.push(...await loadPlugin("@vitejs/plugin-vue"));
435
+ plugins.push(
436
+ ...await loadPluginFromProject(projectRoot, "@vitejs/plugin-vue")
437
+ );
421
438
  if (hasInstalled(projectRoot, "svelte"))
422
439
  plugins.push(
423
- ...await loadPlugin("@sveltejs/vite-plugin-svelte", "svelte")
440
+ ...await loadPluginFromProject(
441
+ projectRoot,
442
+ "@sveltejs/vite-plugin-svelte",
443
+ "svelte"
444
+ )
424
445
  );
425
446
  if (hasInstalled(projectRoot, "solid-js"))
426
- plugins.push(...await loadPlugin("vite-plugin-solid"));
447
+ plugins.push(
448
+ ...await loadPluginFromProject(projectRoot, "vite-plugin-solid")
449
+ );
427
450
  return plugins;
428
451
  }
429
452
  useFrameshotVite() {
@@ -482,15 +505,25 @@ var GenericAdapter = class {
482
505
  if (hasInstalled(projectRoot, "react"))
483
506
  plugins.push(...await loadReactPlugin());
484
507
  if (hasInstalled(projectRoot, "vue"))
485
- plugins.push(...await loadPlugin("@vitejs/plugin-vue"));
508
+ plugins.push(
509
+ ...await loadPluginFromProject(projectRoot, "@vitejs/plugin-vue")
510
+ );
486
511
  if (hasInstalled(projectRoot, "svelte"))
487
512
  plugins.push(
488
- ...await loadPlugin("@sveltejs/vite-plugin-svelte", "svelte")
513
+ ...await loadPluginFromProject(
514
+ projectRoot,
515
+ "@sveltejs/vite-plugin-svelte",
516
+ "svelte"
517
+ )
489
518
  );
490
519
  if (hasInstalled(projectRoot, "solid-js"))
491
- plugins.push(...await loadPlugin("vite-plugin-solid"));
520
+ plugins.push(
521
+ ...await loadPluginFromProject(projectRoot, "vite-plugin-solid")
522
+ );
492
523
  if (hasInstalled(projectRoot, "preact"))
493
- plugins.push(...await loadPlugin("@preact/preset-vite"));
524
+ plugins.push(
525
+ ...await loadPluginFromProject(projectRoot, "@preact/preset-vite")
526
+ );
494
527
  return plugins;
495
528
  }
496
529
  useFrameshotVite() {
@@ -588,8 +621,8 @@ var NuxtAdapter = class {
588
621
  getOptimizeDeps(_root) {
589
622
  return ["vue"];
590
623
  }
591
- async getPlugins(_root) {
592
- return loadPlugin("@vitejs/plugin-vue");
624
+ async getPlugins(root) {
625
+ return loadPluginFromProject(root, "@vitejs/plugin-vue");
593
626
  }
594
627
  useFrameshotVite() {
595
628
  return true;
@@ -743,11 +776,15 @@ var SvelteAdapter = class {
743
776
  getOptimizeDeps() {
744
777
  return [];
745
778
  }
746
- async getPlugins(_root) {
747
- return loadPlugin("@sveltejs/vite-plugin-svelte", "svelte");
779
+ async getPlugins(root) {
780
+ return loadPluginFromProject(
781
+ root,
782
+ "@sveltejs/vite-plugin-svelte",
783
+ "svelte"
784
+ );
748
785
  }
749
- useFrameshotVite() {
750
- return true;
786
+ useFrameshotVite(projectRoot) {
787
+ return projectRoot ? !hasInstalled(projectRoot, "vite") : false;
751
788
  }
752
789
  skipProjectConfig() {
753
790
  return true;
@@ -771,11 +808,15 @@ var SvelteKitAdapter = class {
771
808
  getOptimizeDeps(_root) {
772
809
  return [];
773
810
  }
774
- async getPlugins(_root) {
775
- return loadPlugin("@sveltejs/vite-plugin-svelte", "svelte");
811
+ async getPlugins(root) {
812
+ return loadPluginFromProject(
813
+ root,
814
+ "@sveltejs/vite-plugin-svelte",
815
+ "svelte"
816
+ );
776
817
  }
777
- useFrameshotVite() {
778
- return true;
818
+ useFrameshotVite(projectRoot) {
819
+ return projectRoot ? !hasInstalled(projectRoot, "vite") : false;
779
820
  }
780
821
  skipProjectConfig() {
781
822
  return true;
@@ -861,8 +902,8 @@ var VueAdapter = class {
861
902
  getOptimizeDeps() {
862
903
  return ["vue"];
863
904
  }
864
- async getPlugins(_root) {
865
- return loadPlugin("@vitejs/plugin-vue");
905
+ async getPlugins(root) {
906
+ return loadPluginFromProject(root, "@vitejs/plugin-vue");
866
907
  }
867
908
  useFrameshotVite() {
868
909
  return true;
@@ -910,7 +951,7 @@ var ViteBundler = class {
910
951
  const absPath = resolve2(filePath);
911
952
  const project = options.projectRoot ? this.detector.detect(join18(options.projectRoot, "package.json")) : this.detector.detect(absPath);
912
953
  const adapter = selectAdapter(project.root);
913
- if (!project.hasVite && !adapter.useFrameshotVite()) {
954
+ if (!project.hasVite && !adapter.useFrameshotVite(project.root)) {
914
955
  throw new Error(
915
956
  `Vite not found in ${project.root}. Install vite: npm install -D vite`
916
957
  );
@@ -988,9 +1029,10 @@ try {
988
1029
  case "solid":
989
1030
  return `${cssImport}
990
1031
  import { render } from "solid-js/web";
1032
+ import { createComponent } from "solid-js";
991
1033
  import Component from "${componentPath}";
992
1034
  const props = ${propsJson};
993
- render(() => Component(props), document.getElementById("app"));
1035
+ render(() => createComponent(Component, props), document.getElementById("app"));
994
1036
  `;
995
1037
  case "preact":
996
1038
  return `${cssImport}
@@ -1010,12 +1052,29 @@ document.getElementById("app").innerHTML = html;
1010
1052
  async ensureServer(project, adapter) {
1011
1053
  const existing = this.servers.get(project.root);
1012
1054
  if (existing) return existing;
1013
- const vite = adapter.useFrameshotVite() ? await this.importFrameshotVite() : await this.importVite(project.root);
1055
+ const vite = adapter.useFrameshotVite(project.root) ? await this.importFrameshotVite() : await this.importVite(project.root);
1014
1056
  if (!vite) {
1015
1057
  throw new Error(`Failed to import vite for ${project.root}`);
1016
1058
  }
1017
1059
  const adapterPlugins = await adapter.getPlugins(project.root);
1018
1060
  const adapterAliases = adapter.getAliases(project.root, STUBS_DIR);
1061
+ const ORM_PACKAGES = [
1062
+ "@prisma/client",
1063
+ "drizzle-orm",
1064
+ "mongoose",
1065
+ "typeorm",
1066
+ "@mikro-orm/core",
1067
+ "sequelize",
1068
+ "knex",
1069
+ "objection",
1070
+ "bookshelf"
1071
+ ];
1072
+ const ormStub = join18(STUBS_DIR, "orm-proxy.js");
1073
+ for (const pkg of ORM_PACKAGES) {
1074
+ if (existsSync4(join18(project.root, "node_modules", ...pkg.split("/")))) {
1075
+ adapterAliases[pkg] = ormStub;
1076
+ }
1077
+ }
1019
1078
  const aliasEntries = [];
1020
1079
  for (const [key, value] of Object.entries(project.pathAliases)) {
1021
1080
  aliasEntries.push({
@@ -1108,7 +1167,7 @@ document.getElementById("app").innerHTML = html;
1108
1167
  }
1109
1168
  async importFrameshotVite() {
1110
1169
  try {
1111
- const requireFromHere = createRequire(import.meta.url);
1170
+ const requireFromHere = createRequire2(import.meta.url);
1112
1171
  const vitePath = requireFromHere.resolve("vite");
1113
1172
  const mod = await import(vitePath);
1114
1173
  if (mod && typeof mod === "object" && "createServer" in mod) {
@@ -1127,7 +1186,7 @@ document.getElementById("app").innerHTML = html;
1127
1186
  }
1128
1187
  async importVite(projectRoot) {
1129
1188
  try {
1130
- const require2 = createRequire(join18(projectRoot, "package.json"));
1189
+ const require2 = createRequire2(join18(projectRoot, "package.json"));
1131
1190
  const vitePath = require2.resolve("vite");
1132
1191
  const mod = await import(vitePath);
1133
1192
  if (mod && typeof mod === "object" && "createServer" in mod) {
@@ -1139,7 +1198,7 @@ document.getElementById("app").innerHTML = html;
1139
1198
  }
1140
1199
  hasPackage(projectRoot, name) {
1141
1200
  try {
1142
- const require2 = createRequire(join18(projectRoot, "package.json"));
1201
+ const require2 = createRequire2(join18(projectRoot, "package.json"));
1143
1202
  require2.resolve(name);
1144
1203
  return true;
1145
1204
  } catch {
@@ -1369,7 +1428,7 @@ var DiffUseCase = class {
1369
1428
  };
1370
1429
 
1371
1430
  // src/use-cases/render.ts
1372
- import { readFileSync as readFileSync3 } from "fs";
1431
+ import { readFileSync as readFileSync3, writeFileSync } from "fs";
1373
1432
  import { extname as extname2 } from "path";
1374
1433
 
1375
1434
  // src/infrastructure/page-utils.ts
@@ -1468,26 +1527,26 @@ var RenderUseCase = class {
1468
1527
  css: opts.css,
1469
1528
  tailwindVersion: opts.tailwindVersion
1470
1529
  });
1471
- return Promise.all(
1530
+ const results = await Promise.all(
1472
1531
  opts.engines.map((engine) => this.renderHtml(engine, html, opts))
1473
1532
  );
1533
+ return this.maybeSave(results, opts.outputPath);
1474
1534
  }
1475
1535
  async renderFile(filePath, options = {}) {
1476
1536
  const opts = this.resolveOptions(options);
1477
1537
  const ext = extname2(filePath).toLowerCase();
1478
- const framework = EXT_TO_FRAMEWORK[ext] ?? "react";
1538
+ const extFramework = EXT_TO_FRAMEWORK[ext] ?? "react";
1479
1539
  let fallbackReason;
1480
1540
  if (this.viteBundler) {
1481
1541
  try {
1482
1542
  const { url } = await this.viteBundler.getUrl(filePath, {
1483
1543
  props: options.props,
1484
- framework,
1485
1544
  projectRoot: options.projectRoot
1486
1545
  });
1487
- const results2 = await Promise.all(
1546
+ const raw2 = await Promise.all(
1488
1547
  opts.engines.map((engine) => this.renderUrl(engine, url, opts))
1489
1548
  );
1490
- return { results: results2, mode: "vite" };
1549
+ return { results: this.maybeSave(raw2, opts.outputPath), mode: "vite" };
1491
1550
  } catch (err) {
1492
1551
  fallbackReason = err instanceof Error ? err.message : String(err);
1493
1552
  process.stderr.write(
@@ -1499,8 +1558,12 @@ var RenderUseCase = class {
1499
1558
  fallbackReason = "Vite bundler not available";
1500
1559
  }
1501
1560
  const code = readFileSync3(filePath, "utf-8");
1502
- const results = await this.render(code, framework, options);
1503
- return { results, mode: "cdn", fallbackReason };
1561
+ const raw = await this.render(code, extFramework, options);
1562
+ return {
1563
+ results: this.maybeSave(raw, opts.outputPath),
1564
+ mode: "cdn",
1565
+ fallbackReason
1566
+ };
1504
1567
  }
1505
1568
  async renderInteraction(code, framework, interactions, options = {}) {
1506
1569
  const opts = this.resolveOptions(options);
@@ -1688,6 +1751,19 @@ var RenderUseCase = class {
1688
1751
  height: Math.max(measured.height + AUTO_FIT_PAD * 2, AUTO_FIT_MIN.height)
1689
1752
  };
1690
1753
  }
1754
+ /**
1755
+ * When outputPath is set, write the PNG to disk and replace the inline
1756
+ * base64 with an empty string to keep the MCP response small.
1757
+ */
1758
+ maybeSave(results, outputPath) {
1759
+ if (!outputPath) return results;
1760
+ return results.map((r) => {
1761
+ const filePath = results.length === 1 ? outputPath : outputPath.replace(/\.png$/i, `-${r.engine}.png`);
1762
+ const buf = Buffer.from(r.image, "base64");
1763
+ writeFileSync(filePath, buf);
1764
+ return { ...r, image: "", savedPath: filePath };
1765
+ });
1766
+ }
1691
1767
  resolveOptions(partial) {
1692
1768
  return {
1693
1769
  viewport: partial.viewport ?? DEFAULT_RENDER_OPTIONS.viewport,
@@ -1697,7 +1773,9 @@ var RenderUseCase = class {
1697
1773
  css: partial.css ?? DEFAULT_RENDER_OPTIONS.css,
1698
1774
  tailwindVersion: partial.tailwindVersion ?? DEFAULT_RENDER_OPTIONS.tailwindVersion,
1699
1775
  waitFor: partial.waitFor ?? DEFAULT_RENDER_OPTIONS.waitFor,
1700
- autoFit: partial.autoFit ?? DEFAULT_RENDER_OPTIONS.autoFit
1776
+ autoFit: partial.autoFit ?? DEFAULT_RENDER_OPTIONS.autoFit,
1777
+ mock: partial.mock,
1778
+ outputPath: partial.outputPath
1701
1779
  };
1702
1780
  }
1703
1781
  };
@@ -10,7 +10,7 @@ import {
10
10
  SnapshotStore,
11
11
  SnapshotUseCase,
12
12
  ViteBundler
13
- } from "./chunk-FQ3BVCX7.js";
13
+ } from "./chunk-J5JI2IC5.js";
14
14
 
15
15
  // src/use-cases/watch.ts
16
16
  import { watch } from "chokidar";
package/dist/cli.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createContainer
4
- } from "./chunk-AF64XFKX.js";
4
+ } from "./chunk-SPOPEVPM.js";
5
5
  import {
6
6
  EXT_TO_FRAMEWORK
7
- } from "./chunk-FQ3BVCX7.js";
7
+ } from "./chunk-J5JI2IC5.js";
8
8
 
9
9
  // src/cli.ts
10
10
  import { mkdirSync } from "fs";
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createContainer
4
- } from "./chunk-AF64XFKX.js";
4
+ } from "./chunk-SPOPEVPM.js";
5
5
  import {
6
6
  DEVICE_PRESETS,
7
7
  EXT_TO_FRAMEWORK,
8
8
  __export
9
- } from "./chunk-FQ3BVCX7.js";
9
+ } from "./chunk-J5JI2IC5.js";
10
10
 
11
11
  // src/index.ts
12
12
  import { execSync } from "child_process";
@@ -14531,6 +14531,9 @@ config(en_default());
14531
14531
  var mockSchema = external_exports.record(external_exports.string(), external_exports.unknown()).optional().describe(
14532
14532
  "Mock network responses. Keys: URL pattern (path '/api/users', glob '**/api/*', or full URL). Values: response body (auto-serialized as JSON) or { status, contentType, body, headers }."
14533
14533
  );
14534
+ var outputPathSchema = external_exports.string().optional().describe(
14535
+ "Absolute path to save the PNG (e.g. '/tmp/button.png'). When set, returns a text summary instead of base64 image data \u2014 use this in long agent sessions to avoid context bloat."
14536
+ );
14534
14537
  function wrapHandler(handler) {
14535
14538
  return async (args) => {
14536
14539
  try {
@@ -14893,7 +14896,8 @@ function registerRenderFileTools(server2, useCase) {
14893
14896
  projectRoot: external_exports.string().optional().describe(
14894
14897
  "Project root directory override (defaults to auto-detect from file path)"
14895
14898
  ),
14896
- mock: mockSchema
14899
+ mock: mockSchema,
14900
+ outputPath: outputPathSchema
14897
14901
  },
14898
14902
  wrapHandler(
14899
14903
  async ({
@@ -14904,7 +14908,8 @@ function registerRenderFileTools(server2, useCase) {
14904
14908
  darkMode,
14905
14909
  tailwindVersion,
14906
14910
  projectRoot,
14907
- mock
14911
+ mock,
14912
+ outputPath
14908
14913
  }) => {
14909
14914
  const start = performance.now();
14910
14915
  const { results, mode, fallbackReason } = await useCase.renderFile(
@@ -14917,7 +14922,8 @@ function registerRenderFileTools(server2, useCase) {
14917
14922
  darkMode,
14918
14923
  tailwindVersion,
14919
14924
  projectRoot,
14920
- mock
14925
+ mock,
14926
+ outputPath
14921
14927
  }
14922
14928
  );
14923
14929
  const elapsed = Math.round(performance.now() - start);
@@ -14926,19 +14932,29 @@ function registerRenderFileTools(server2, useCase) {
14926
14932
  const fallbackNote = mode === "cdn" && fallbackReason ? `
14927
14933
  \u2139\uFE0F Fell back to CDN pipeline: ${fallbackReason}
14928
14934
  Imports from node_modules may not resolve. To fix: install vite as a devDep and add a minimal vite.config.` : "";
14929
- const content = results.flatMap((r) => [
14930
- {
14931
- type: "image",
14932
- data: r.image,
14933
- mimeType: "image/png"
14934
- },
14935
- {
14936
- type: "text",
14937
- text: `[${mode}/${framework}] ${r.width}x${r.height} (${elapsed}ms)${r.consoleErrors.length ? `
14935
+ const content = results.flatMap((r) => {
14936
+ if (r.savedPath) {
14937
+ return [
14938
+ {
14939
+ type: "text",
14940
+ text: `\u2713 Saved to ${r.savedPath} (${r.width}\xD7${r.height}, ${elapsed}ms)${fallbackNote}`
14941
+ }
14942
+ ];
14943
+ }
14944
+ return [
14945
+ {
14946
+ type: "image",
14947
+ data: r.image,
14948
+ mimeType: "image/png"
14949
+ },
14950
+ {
14951
+ type: "text",
14952
+ text: `[${mode}/${framework}] ${r.width}x${r.height} (${elapsed}ms)${r.consoleErrors.length ? `
14938
14953
  \u26A0\uFE0F Console errors:
14939
14954
  ${r.consoleErrors.join("\n")}` : ""}${fallbackNote}`
14940
- }
14941
- ]);
14955
+ }
14956
+ ];
14957
+ });
14942
14958
  return { content };
14943
14959
  }
14944
14960
  )
@@ -30,6 +30,15 @@ interface RenderOptions {
30
30
  * }
31
31
  */
32
32
  mock?: Record<string, unknown>;
33
+ /**
34
+ * Write the screenshot to disk instead of returning it inline as base64.
35
+ * When set, tools return a short text summary ("Saved to /path 400×300 12KB")
36
+ * instead of the full image data. This prevents large base64 blobs from
37
+ * filling the LLM's context window in multi-step agent workflows.
38
+ *
39
+ * @example outputPath: "/tmp/my-component.png"
40
+ */
41
+ outputPath?: string;
33
42
  }
34
43
  interface ScreenshotResult {
35
44
  engine: Engine;
@@ -37,6 +46,8 @@ interface ScreenshotResult {
37
46
  width: number;
38
47
  height: number;
39
48
  consoleErrors: string[];
49
+ /** Set when outputPath was provided — the image was saved here instead of inline. */
50
+ savedPath?: string;
40
51
  }
41
52
  interface DiffResult {
42
53
  before: string;
@@ -254,6 +265,11 @@ declare class RenderUseCase {
254
265
  private renderUrl;
255
266
  private navigateAndWait;
256
267
  private resolveAutoFitViewport;
268
+ /**
269
+ * When outputPath is set, write the PNG to disk and replace the inline
270
+ * base64 with an empty string to keep the MCP response small.
271
+ */
272
+ private maybeSave;
257
273
  private resolveOptions;
258
274
  }
259
275
 
package/dist/renderer.js CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  SnapshotStore,
14
14
  SnapshotUseCase,
15
15
  ViteBundler
16
- } from "./chunk-FQ3BVCX7.js";
16
+ } from "./chunk-J5JI2IC5.js";
17
17
  export {
18
18
  AuditUseCase,
19
19
  BrowserPool,
@@ -1,4 +1,8 @@
1
- // Stub for next/headers — server-only APIs that must not crash during component preview
1
+ // Stub for next/headers — server-only APIs that must not crash during component preview.
2
+ //
3
+ // Next.js 15 made headers()/cookies() async. We export both sync and async
4
+ // variants so components compiled for either version work.
5
+
2
6
  const emptyMap = {
3
7
  get: () => undefined,
4
8
  getAll: () => [],
@@ -10,10 +14,23 @@ const emptyMap = {
10
14
  set: () => {},
11
15
  delete: () => {},
12
16
  toString: () => "",
17
+ toJSON: () => ({}),
18
+ append: () => {},
19
+ [Symbol.iterator]: () => [][Symbol.iterator](),
13
20
  };
14
- export const headers = () => emptyMap;
15
- export const cookies = () => emptyMap;
16
- export const draftMode = () => ({
21
+
22
+ // Next.js 15+: headers() and cookies() are async
23
+ export const headers = async () => emptyMap;
24
+ export const cookies = async () => emptyMap;
25
+
26
+ // Next.js 15 additions
27
+ export const connection = async () => {};
28
+ export const after = (fn) => {
29
+ if (typeof fn === "function") fn();
30
+ };
31
+
32
+ // Draft mode
33
+ export const draftMode = async () => ({
17
34
  isEnabled: false,
18
35
  enable: () => {},
19
36
  disable: () => {},
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Universal stub for server-only ORM/database packages.
3
+ *
4
+ * Prisma, Drizzle, Mongoose, TypeORM etc. require a running database and
5
+ * Node.js-only modules (net, tls, dns) that break in a browser/Vite context.
6
+ *
7
+ * This stub returns a Proxy that:
8
+ * - Returns empty arrays for .findMany(), .find(), .all() style calls
9
+ * - Returns null for .findFirst(), .findOne() style calls
10
+ * - Returns the input for .create(), .update(), .save() style calls
11
+ * - Returns { count: 0 } for .count(), .deleteMany()
12
+ * - Chains infinitely (every property returns another proxy)
13
+ * - Never throws — components render in their "empty data" state
14
+ */
15
+
16
+ function makeProxy(depth = 0) {
17
+ if (depth > 8) return undefined; // prevent infinite chain
18
+ return new Proxy(() => Promise.resolve([]), {
19
+ get(_, prop) {
20
+ if (prop === "then") return undefined; // not a thenable
21
+ if (prop === Symbol.toPrimitive) return () => "[ORM stub]";
22
+ if (prop === Symbol.iterator) return function* () {};
23
+ // Common result shapes
24
+ if (
25
+ typeof prop === "string" &&
26
+ /^(findMany|findAll|all|list|getAll|fetchAll)$/.test(prop)
27
+ )
28
+ return () => Promise.resolve([]);
29
+ if (
30
+ typeof prop === "string" &&
31
+ /^(findFirst|findOne|findUnique|get|fetch|findById)$/.test(prop)
32
+ )
33
+ return () => Promise.resolve(null);
34
+ if (
35
+ typeof prop === "string" &&
36
+ /^(count|countDocuments|estimatedDocumentCount)$/.test(prop)
37
+ )
38
+ return () => Promise.resolve(0);
39
+ if (
40
+ typeof prop === "string" &&
41
+ /^(create|insert|save|update|upsert|set)$/.test(prop)
42
+ )
43
+ return (data) => Promise.resolve(data ?? {});
44
+ if (
45
+ typeof prop === "string" &&
46
+ /^(delete|deleteMany|remove|destroy)$/.test(prop)
47
+ )
48
+ return () => Promise.resolve({ count: 0 });
49
+ return makeProxy(depth + 1);
50
+ },
51
+ apply() {
52
+ return Promise.resolve([]);
53
+ },
54
+ construct() {
55
+ return makeProxy(depth + 1);
56
+ },
57
+ });
58
+ }
59
+
60
+ // Default export and named exports both return a proxy
61
+ export default makeProxy();
62
+ export const PrismaClient = () => makeProxy();
63
+ export const drizzle = () => makeProxy();
64
+ export const createClient = () => makeProxy();
65
+ export const getConnection = () => makeProxy();
66
+ export const DataSource = () => makeProxy();
@@ -1,4 +1,8 @@
1
- // Stub for next/headers — server-only APIs that must not crash during component preview
1
+ // Stub for next/headers — server-only APIs that must not crash during component preview.
2
+ //
3
+ // Next.js 15 made headers()/cookies() async. We export both sync and async
4
+ // variants so components compiled for either version work.
5
+
2
6
  const emptyMap = {
3
7
  get: () => undefined,
4
8
  getAll: () => [],
@@ -10,10 +14,23 @@ const emptyMap = {
10
14
  set: () => {},
11
15
  delete: () => {},
12
16
  toString: () => "",
17
+ toJSON: () => ({}),
18
+ append: () => {},
19
+ [Symbol.iterator]: () => [][Symbol.iterator](),
13
20
  };
14
- export const headers = () => emptyMap;
15
- export const cookies = () => emptyMap;
16
- export const draftMode = () => ({
21
+
22
+ // Next.js 15+: headers() and cookies() are async
23
+ export const headers = async () => emptyMap;
24
+ export const cookies = async () => emptyMap;
25
+
26
+ // Next.js 15 additions
27
+ export const connection = async () => {};
28
+ export const after = (fn) => {
29
+ if (typeof fn === "function") fn();
30
+ };
31
+
32
+ // Draft mode
33
+ export const draftMode = async () => ({
17
34
  isEnabled: false,
18
35
  enable: () => {},
19
36
  disable: () => {},
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Universal stub for server-only ORM/database packages.
3
+ *
4
+ * Prisma, Drizzle, Mongoose, TypeORM etc. require a running database and
5
+ * Node.js-only modules (net, tls, dns) that break in a browser/Vite context.
6
+ *
7
+ * This stub returns a Proxy that:
8
+ * - Returns empty arrays for .findMany(), .find(), .all() style calls
9
+ * - Returns null for .findFirst(), .findOne() style calls
10
+ * - Returns the input for .create(), .update(), .save() style calls
11
+ * - Returns { count: 0 } for .count(), .deleteMany()
12
+ * - Chains infinitely (every property returns another proxy)
13
+ * - Never throws — components render in their "empty data" state
14
+ */
15
+
16
+ function makeProxy(depth = 0) {
17
+ if (depth > 8) return undefined; // prevent infinite chain
18
+ return new Proxy(() => Promise.resolve([]), {
19
+ get(_, prop) {
20
+ if (prop === "then") return undefined; // not a thenable
21
+ if (prop === Symbol.toPrimitive) return () => "[ORM stub]";
22
+ if (prop === Symbol.iterator) return function* () {};
23
+ // Common result shapes
24
+ if (
25
+ typeof prop === "string" &&
26
+ /^(findMany|findAll|all|list|getAll|fetchAll)$/.test(prop)
27
+ )
28
+ return () => Promise.resolve([]);
29
+ if (
30
+ typeof prop === "string" &&
31
+ /^(findFirst|findOne|findUnique|get|fetch|findById)$/.test(prop)
32
+ )
33
+ return () => Promise.resolve(null);
34
+ if (
35
+ typeof prop === "string" &&
36
+ /^(count|countDocuments|estimatedDocumentCount)$/.test(prop)
37
+ )
38
+ return () => Promise.resolve(0);
39
+ if (
40
+ typeof prop === "string" &&
41
+ /^(create|insert|save|update|upsert|set)$/.test(prop)
42
+ )
43
+ return (data) => Promise.resolve(data ?? {});
44
+ if (
45
+ typeof prop === "string" &&
46
+ /^(delete|deleteMany|remove|destroy)$/.test(prop)
47
+ )
48
+ return () => Promise.resolve({ count: 0 });
49
+ return makeProxy(depth + 1);
50
+ },
51
+ apply() {
52
+ return Promise.resolve([]);
53
+ },
54
+ construct() {
55
+ return makeProxy(depth + 1);
56
+ },
57
+ });
58
+ }
59
+
60
+ // Default export and named exports both return a proxy
61
+ export default makeProxy();
62
+ export const PrismaClient = () => makeProxy();
63
+ export const drizzle = () => makeProxy();
64
+ export const createClient = () => makeProxy();
65
+ export const getConnection = () => makeProxy();
66
+ export const DataSource = () => makeProxy();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frameshot-mcp",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Zero-config visual testing for AI agents. Render project components with full Vite dependency resolution — no stories, no config.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",