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 +28 -2
- package/dist/{chunk-FQ3BVCX7.js → chunk-J5JI2IC5.js} +113 -35
- package/dist/{chunk-AF64XFKX.js → chunk-SPOPEVPM.js} +1 -1
- package/dist/cli.js +2 -2
- package/dist/index.js +32 -16
- package/dist/renderer.d.ts +16 -0
- package/dist/renderer.js +1 -1
- package/dist/stubs/next-headers.js +21 -4
- package/dist/stubs/orm-proxy.js +66 -0
- package/dist/stubs/stubs/next-headers.js +21 -4
- package/dist/stubs/stubs/orm-proxy.js +66 -0
- package/package.json +1 -1
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.
|
|
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.
|
|
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(
|
|
435
|
+
plugins.push(
|
|
436
|
+
...await loadPluginFromProject(projectRoot, "@vitejs/plugin-vue")
|
|
437
|
+
);
|
|
421
438
|
if (hasInstalled(projectRoot, "svelte"))
|
|
422
439
|
plugins.push(
|
|
423
|
-
...await
|
|
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(
|
|
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(
|
|
508
|
+
plugins.push(
|
|
509
|
+
...await loadPluginFromProject(projectRoot, "@vitejs/plugin-vue")
|
|
510
|
+
);
|
|
486
511
|
if (hasInstalled(projectRoot, "svelte"))
|
|
487
512
|
plugins.push(
|
|
488
|
-
...await
|
|
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(
|
|
520
|
+
plugins.push(
|
|
521
|
+
...await loadPluginFromProject(projectRoot, "vite-plugin-solid")
|
|
522
|
+
);
|
|
492
523
|
if (hasInstalled(projectRoot, "preact"))
|
|
493
|
-
plugins.push(
|
|
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(
|
|
592
|
-
return
|
|
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(
|
|
747
|
-
return
|
|
779
|
+
async getPlugins(root) {
|
|
780
|
+
return loadPluginFromProject(
|
|
781
|
+
root,
|
|
782
|
+
"@sveltejs/vite-plugin-svelte",
|
|
783
|
+
"svelte"
|
|
784
|
+
);
|
|
748
785
|
}
|
|
749
|
-
useFrameshotVite() {
|
|
750
|
-
return
|
|
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(
|
|
775
|
-
return
|
|
811
|
+
async getPlugins(root) {
|
|
812
|
+
return loadPluginFromProject(
|
|
813
|
+
root,
|
|
814
|
+
"@sveltejs/vite-plugin-svelte",
|
|
815
|
+
"svelte"
|
|
816
|
+
);
|
|
776
817
|
}
|
|
777
|
-
useFrameshotVite() {
|
|
778
|
-
return
|
|
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(
|
|
865
|
-
return
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
1546
|
+
const raw2 = await Promise.all(
|
|
1488
1547
|
opts.engines.map((engine) => this.renderUrl(engine, url, opts))
|
|
1489
1548
|
);
|
|
1490
|
-
return { results:
|
|
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
|
|
1503
|
-
return {
|
|
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
|
};
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
createContainer
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-SPOPEVPM.js";
|
|
5
5
|
import {
|
|
6
6
|
EXT_TO_FRAMEWORK
|
|
7
|
-
} from "./chunk-
|
|
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-
|
|
4
|
+
} from "./chunk-SPOPEVPM.js";
|
|
5
5
|
import {
|
|
6
6
|
DEVICE_PRESETS,
|
|
7
7
|
EXT_TO_FRAMEWORK,
|
|
8
8
|
__export
|
|
9
|
-
} from "./chunk-
|
|
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
|
-
|
|
14932
|
-
|
|
14933
|
-
|
|
14934
|
-
|
|
14935
|
-
|
|
14936
|
-
|
|
14937
|
-
|
|
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
|
)
|
package/dist/renderer.d.ts
CHANGED
|
@@ -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
|
@@ -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
|
-
|
|
15
|
-
|
|
16
|
-
export const
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
export const
|
|
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.
|
|
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",
|