frameshot-mcp 0.9.7 → 0.11.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/dist/{chunk-VUYZHZBH.js → chunk-AF64XFKX.js} +1 -1
- package/dist/{chunk-MEBQ7ZWA.js → chunk-FQ3BVCX7.js} +73 -32
- package/dist/cli.js +27 -42
- package/dist/index.js +45 -30
- package/dist/renderer.d.ts +16 -0
- package/dist/renderer.js +1 -1
- package/package.json +7 -1
- package/scripts/render-changed.mjs +17 -13
|
@@ -264,6 +264,7 @@ var SnapshotStore = class {
|
|
|
264
264
|
// src/infrastructure/project-detector.ts
|
|
265
265
|
import { existsSync, readFileSync } from "fs";
|
|
266
266
|
import { dirname, join, resolve } from "path";
|
|
267
|
+
import { parseTsconfig } from "get-tsconfig";
|
|
267
268
|
var VITE_CONFIG_NAMES = [
|
|
268
269
|
"vite.config.ts",
|
|
269
270
|
"vite.config.js",
|
|
@@ -274,9 +275,10 @@ var ProjectDetector = class {
|
|
|
274
275
|
detect(filePath) {
|
|
275
276
|
const root = this.findProjectRoot(filePath);
|
|
276
277
|
const viteConfigPath = this.findViteConfig(root);
|
|
277
|
-
const
|
|
278
|
+
const pkg = this.readPackageJson(root);
|
|
279
|
+
const framework = this.detectFramework(pkg);
|
|
278
280
|
const hasVite = this.checkViteAvailable(root);
|
|
279
|
-
const isNextJs = this.checkIsNextJs(
|
|
281
|
+
const isNextJs = this.checkIsNextJs(pkg);
|
|
280
282
|
const pathAliases = this.readTsconfigAliases(root);
|
|
281
283
|
return { root, viteConfigPath, framework, hasVite, isNextJs, pathAliases };
|
|
282
284
|
}
|
|
@@ -299,48 +301,41 @@ var ProjectDetector = class {
|
|
|
299
301
|
}
|
|
300
302
|
return void 0;
|
|
301
303
|
}
|
|
302
|
-
|
|
304
|
+
readPackageJson(root) {
|
|
303
305
|
const pkgPath = join(root, "package.json");
|
|
304
|
-
if (!existsSync(pkgPath)) return
|
|
306
|
+
if (!existsSync(pkgPath)) return null;
|
|
305
307
|
try {
|
|
306
|
-
|
|
307
|
-
const allDeps = {
|
|
308
|
-
...pkg.dependencies,
|
|
309
|
-
...pkg.devDependencies
|
|
310
|
-
};
|
|
311
|
-
if (allDeps["solid-js"]) return "solid";
|
|
312
|
-
if (allDeps.preact) return "preact";
|
|
313
|
-
if (allDeps.react || allDeps["react-dom"]) return "react";
|
|
314
|
-
if (allDeps.vue) return "vue";
|
|
315
|
-
if (allDeps.svelte) return "svelte";
|
|
308
|
+
return JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
316
309
|
} catch {
|
|
310
|
+
return null;
|
|
317
311
|
}
|
|
312
|
+
}
|
|
313
|
+
detectFramework(pkg) {
|
|
314
|
+
if (!pkg) return "html";
|
|
315
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
316
|
+
if (allDeps["solid-js"]) return "solid";
|
|
317
|
+
if (allDeps.preact) return "preact";
|
|
318
|
+
if (allDeps.react || allDeps["react-dom"]) return "react";
|
|
319
|
+
if (allDeps.vue) return "vue";
|
|
320
|
+
if (allDeps.svelte) return "svelte";
|
|
318
321
|
return "html";
|
|
319
322
|
}
|
|
320
323
|
checkViteAvailable(root) {
|
|
321
324
|
const vitePkgPath = join(root, "node_modules", "vite", "package.json");
|
|
322
325
|
return existsSync(vitePkgPath);
|
|
323
326
|
}
|
|
324
|
-
checkIsNextJs(
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
329
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
330
|
-
return !!allDeps.next;
|
|
331
|
-
} catch {
|
|
332
|
-
return false;
|
|
333
|
-
}
|
|
327
|
+
checkIsNextJs(pkg) {
|
|
328
|
+
if (!pkg) return false;
|
|
329
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
330
|
+
return !!allDeps.next;
|
|
334
331
|
}
|
|
335
332
|
readTsconfigAliases(root) {
|
|
336
333
|
const tscPath = join(root, "tsconfig.json");
|
|
337
334
|
if (!existsSync(tscPath)) return {};
|
|
338
335
|
try {
|
|
339
|
-
const
|
|
340
|
-
const
|
|
341
|
-
const
|
|
342
|
-
const paths = tsc.compilerOptions?.paths ?? {};
|
|
343
|
-
const baseUrl = tsc.compilerOptions?.baseUrl ?? ".";
|
|
336
|
+
const tsconfig = parseTsconfig(tscPath);
|
|
337
|
+
const paths = tsconfig.compilerOptions?.paths ?? {};
|
|
338
|
+
const baseUrl = tsconfig.compilerOptions?.baseUrl ?? ".";
|
|
344
339
|
const result = {};
|
|
345
340
|
for (const [alias, targets] of Object.entries(paths)) {
|
|
346
341
|
const key = alias.replace(/\/\*$/, "");
|
|
@@ -1378,6 +1373,7 @@ import { readFileSync as readFileSync3 } from "fs";
|
|
|
1378
1373
|
import { extname as extname2 } from "path";
|
|
1379
1374
|
|
|
1380
1375
|
// src/infrastructure/page-utils.ts
|
|
1376
|
+
import picomatch from "picomatch";
|
|
1381
1377
|
function attachConsoleCapture(page) {
|
|
1382
1378
|
const errors = [];
|
|
1383
1379
|
const onMessage = (msg) => {
|
|
@@ -1394,6 +1390,46 @@ function attachConsoleCapture(page) {
|
|
|
1394
1390
|
}
|
|
1395
1391
|
};
|
|
1396
1392
|
}
|
|
1393
|
+
function normalizeResponse(value) {
|
|
1394
|
+
if (value && typeof value === "object" && "body" in value) {
|
|
1395
|
+
return value;
|
|
1396
|
+
}
|
|
1397
|
+
return { body: value };
|
|
1398
|
+
}
|
|
1399
|
+
async function attachMocks(page, mocks) {
|
|
1400
|
+
const matchers = Object.entries(mocks).map(([pattern, value]) => ({
|
|
1401
|
+
pattern,
|
|
1402
|
+
isMatch: pattern.startsWith("http") ? (url) => url === pattern : picomatch(pattern, { dot: true }),
|
|
1403
|
+
response: normalizeResponse(value)
|
|
1404
|
+
}));
|
|
1405
|
+
if (matchers.length === 0) return;
|
|
1406
|
+
await page.route("**/*", async (route) => {
|
|
1407
|
+
const url = route.request().url();
|
|
1408
|
+
const pathname = (() => {
|
|
1409
|
+
try {
|
|
1410
|
+
return new URL(url).pathname;
|
|
1411
|
+
} catch {
|
|
1412
|
+
return url;
|
|
1413
|
+
}
|
|
1414
|
+
})();
|
|
1415
|
+
const hit = matchers.find((m) => m.isMatch(url)) ?? matchers.find((m) => m.isMatch(pathname));
|
|
1416
|
+
if (!hit) {
|
|
1417
|
+
await route.continue();
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
const spec = hit.response;
|
|
1421
|
+
const isString = typeof spec.body === "string";
|
|
1422
|
+
const isBinary = spec.body instanceof Uint8Array || spec.body instanceof ArrayBuffer;
|
|
1423
|
+
const body = isString ? spec.body : isBinary ? Buffer.from(spec.body) : JSON.stringify(spec.body ?? null);
|
|
1424
|
+
const contentType = spec.contentType ?? (isString ? "text/plain" : "application/json");
|
|
1425
|
+
await route.fulfill({
|
|
1426
|
+
status: spec.status ?? 200,
|
|
1427
|
+
contentType,
|
|
1428
|
+
body,
|
|
1429
|
+
headers: spec.headers
|
|
1430
|
+
});
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1397
1433
|
async function takeScreenshot(page, engine, fullPage, errors, cleanup) {
|
|
1398
1434
|
const screenshot = await page.screenshot({ type: "png", fullPage });
|
|
1399
1435
|
const metrics = await page.evaluate(() => ({
|
|
@@ -1440,6 +1476,7 @@ var RenderUseCase = class {
|
|
|
1440
1476
|
const opts = this.resolveOptions(options);
|
|
1441
1477
|
const ext = extname2(filePath).toLowerCase();
|
|
1442
1478
|
const framework = EXT_TO_FRAMEWORK[ext] ?? "react";
|
|
1479
|
+
let fallbackReason;
|
|
1443
1480
|
if (this.viteBundler) {
|
|
1444
1481
|
try {
|
|
1445
1482
|
const { url } = await this.viteBundler.getUrl(filePath, {
|
|
@@ -1452,16 +1489,18 @@ var RenderUseCase = class {
|
|
|
1452
1489
|
);
|
|
1453
1490
|
return { results: results2, mode: "vite" };
|
|
1454
1491
|
} catch (err) {
|
|
1455
|
-
|
|
1492
|
+
fallbackReason = err instanceof Error ? err.message : String(err);
|
|
1456
1493
|
process.stderr.write(
|
|
1457
|
-
`[frameshot] Vite pipeline failed (${
|
|
1494
|
+
`[frameshot] Vite pipeline failed (${fallbackReason}), falling back to CDN
|
|
1458
1495
|
`
|
|
1459
1496
|
);
|
|
1460
1497
|
}
|
|
1498
|
+
} else {
|
|
1499
|
+
fallbackReason = "Vite bundler not available";
|
|
1461
1500
|
}
|
|
1462
1501
|
const code = readFileSync3(filePath, "utf-8");
|
|
1463
1502
|
const results = await this.render(code, framework, options);
|
|
1464
|
-
return { results, mode: "cdn" };
|
|
1503
|
+
return { results, mode: "cdn", fallbackReason };
|
|
1465
1504
|
}
|
|
1466
1505
|
async renderInteraction(code, framework, interactions, options = {}) {
|
|
1467
1506
|
const opts = this.resolveOptions(options);
|
|
@@ -1578,6 +1617,7 @@ var RenderUseCase = class {
|
|
|
1578
1617
|
const page = await this.pool.getPage(engine);
|
|
1579
1618
|
await page.setViewportSize(options.viewport);
|
|
1580
1619
|
const { errors, cleanup } = attachConsoleCapture(page);
|
|
1620
|
+
if (options.mock) await attachMocks(page, options.mock);
|
|
1581
1621
|
await page.setContent(html, { waitUntil: "load", timeout: 1e4 });
|
|
1582
1622
|
if (options.waitFor > 0) await page.waitForTimeout(options.waitFor);
|
|
1583
1623
|
const result = await takeScreenshot(
|
|
@@ -1593,6 +1633,7 @@ var RenderUseCase = class {
|
|
|
1593
1633
|
async renderUrl(engine, url, options) {
|
|
1594
1634
|
const page = await this.pool.getPage(engine);
|
|
1595
1635
|
const { errors, cleanup } = attachConsoleCapture(page);
|
|
1636
|
+
if (options.mock) await attachMocks(page, options.mock);
|
|
1596
1637
|
const viewport = options.autoFit ? await this.resolveAutoFitViewport(page, url, options) : options.viewport;
|
|
1597
1638
|
await page.setViewportSize(viewport);
|
|
1598
1639
|
await this.navigateAndWait(page, url, options.waitFor);
|
package/dist/cli.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
createContainer
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-AF64XFKX.js";
|
|
5
5
|
import {
|
|
6
6
|
EXT_TO_FRAMEWORK
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-FQ3BVCX7.js";
|
|
8
8
|
|
|
9
9
|
// src/cli.ts
|
|
10
10
|
import { mkdirSync } from "fs";
|
|
11
11
|
import { resolve as resolve2 } from "path";
|
|
12
|
+
import mri from "mri";
|
|
12
13
|
|
|
13
14
|
// src/cli/commands.ts
|
|
14
15
|
import { execSync } from "child_process";
|
|
@@ -16,6 +17,7 @@ import { readFileSync, writeFileSync } from "fs";
|
|
|
16
17
|
import { basename, extname, relative, resolve } from "path";
|
|
17
18
|
|
|
18
19
|
// src/cli/display.ts
|
|
20
|
+
import { createSpinner } from "nanospinner";
|
|
19
21
|
var c = {
|
|
20
22
|
reset: "\x1B[0m",
|
|
21
23
|
bold: "\x1B[1m",
|
|
@@ -50,18 +52,13 @@ function info(label, value) {
|
|
|
50
52
|
log(` ${c.gray}${label.padEnd(6)}${c.reset} ${value}`);
|
|
51
53
|
}
|
|
52
54
|
function spinner(text) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
);
|
|
58
|
-
i++;
|
|
59
|
-
}, 80);
|
|
55
|
+
const s = createSpinner(`${c.dim}${text}${c.reset}`, {
|
|
56
|
+
color: "magenta",
|
|
57
|
+
frames: FRAMES
|
|
58
|
+
}).start();
|
|
60
59
|
return {
|
|
61
60
|
stop(finalText) {
|
|
62
|
-
|
|
63
|
-
process.stderr.write(`\r ${c.green}\u2713${c.reset} ${finalText}\x1B[K
|
|
64
|
-
`);
|
|
61
|
+
s.success({ text: finalText });
|
|
65
62
|
}
|
|
66
63
|
};
|
|
67
64
|
}
|
|
@@ -300,39 +297,27 @@ async function cmdDiff(useCase, filePath, flags, outDir) {
|
|
|
300
297
|
|
|
301
298
|
// src/cli.ts
|
|
302
299
|
function parseArgs(args) {
|
|
303
|
-
const
|
|
304
|
-
|
|
300
|
+
const parsed = mri(args, {
|
|
301
|
+
alias: { o: "out", r: "recursive", h: "help" },
|
|
302
|
+
boolean: ["recursive", "help"],
|
|
303
|
+
string: ["out", "props"],
|
|
304
|
+
default: { out: ".frameshot", recursive: false }
|
|
305
|
+
});
|
|
306
|
+
const [command = "help", positional = ""] = parsed._;
|
|
305
307
|
const flags = {
|
|
306
|
-
out:
|
|
307
|
-
recursive:
|
|
308
|
+
out: parsed.out,
|
|
309
|
+
recursive: parsed.recursive,
|
|
310
|
+
width: parsed.width ? Number(parsed.width) : void 0,
|
|
311
|
+
height: parsed.height ? Number(parsed.height) : void 0
|
|
308
312
|
};
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
flags.props = JSON.parse(args[++i]);
|
|
315
|
-
} catch {
|
|
316
|
-
error("Invalid --props JSON");
|
|
317
|
-
}
|
|
318
|
-
break;
|
|
319
|
-
case "--out":
|
|
320
|
-
case "-o":
|
|
321
|
-
flags.out = args[++i];
|
|
322
|
-
break;
|
|
323
|
-
case "--recursive":
|
|
324
|
-
case "-r":
|
|
325
|
-
flags.recursive = true;
|
|
326
|
-
break;
|
|
327
|
-
case "--width":
|
|
328
|
-
flags.width = Number.parseInt(args[++i], 10);
|
|
329
|
-
break;
|
|
330
|
-
case "--height":
|
|
331
|
-
flags.height = Number.parseInt(args[++i], 10);
|
|
332
|
-
break;
|
|
313
|
+
if (parsed.props) {
|
|
314
|
+
try {
|
|
315
|
+
flags.props = JSON.parse(parsed.props);
|
|
316
|
+
} catch {
|
|
317
|
+
error("Invalid --props JSON");
|
|
333
318
|
}
|
|
334
319
|
}
|
|
335
|
-
return { command, positional, flags };
|
|
320
|
+
return { command: String(command), positional: String(positional), flags };
|
|
336
321
|
}
|
|
337
322
|
var HELP = `
|
|
338
323
|
${brand()} ${c.dim}v0.7.0${c.reset}
|
|
@@ -362,7 +347,7 @@ var HELP = `
|
|
|
362
347
|
`;
|
|
363
348
|
async function main() {
|
|
364
349
|
const { command, positional, flags } = parseArgs(process.argv.slice(2));
|
|
365
|
-
if (command === "help"
|
|
350
|
+
if (command === "help") {
|
|
366
351
|
process.stdout.write(HELP);
|
|
367
352
|
return;
|
|
368
353
|
}
|
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-AF64XFKX.js";
|
|
5
5
|
import {
|
|
6
6
|
DEVICE_PRESETS,
|
|
7
7
|
EXT_TO_FRAMEWORK,
|
|
8
8
|
__export
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-FQ3BVCX7.js";
|
|
10
10
|
|
|
11
11
|
// src/index.ts
|
|
12
12
|
import { execSync } from "child_process";
|
|
@@ -14528,6 +14528,9 @@ function date4(params) {
|
|
|
14528
14528
|
config(en_default());
|
|
14529
14529
|
|
|
14530
14530
|
// src/tools/tool-utils.ts
|
|
14531
|
+
var mockSchema = external_exports.record(external_exports.string(), external_exports.unknown()).optional().describe(
|
|
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
|
+
);
|
|
14531
14534
|
function wrapHandler(handler) {
|
|
14532
14535
|
return async (args) => {
|
|
14533
14536
|
try {
|
|
@@ -14889,7 +14892,8 @@ function registerRenderFileTools(server2, useCase) {
|
|
|
14889
14892
|
tailwindVersion: external_exports.enum(["3", "4"]).optional().default("3").describe("Tailwind CSS version (3 or 4)"),
|
|
14890
14893
|
projectRoot: external_exports.string().optional().describe(
|
|
14891
14894
|
"Project root directory override (defaults to auto-detect from file path)"
|
|
14892
|
-
)
|
|
14895
|
+
),
|
|
14896
|
+
mock: mockSchema
|
|
14893
14897
|
},
|
|
14894
14898
|
wrapHandler(
|
|
14895
14899
|
async ({
|
|
@@ -14899,21 +14903,29 @@ function registerRenderFileTools(server2, useCase) {
|
|
|
14899
14903
|
height,
|
|
14900
14904
|
darkMode,
|
|
14901
14905
|
tailwindVersion,
|
|
14902
|
-
projectRoot
|
|
14906
|
+
projectRoot,
|
|
14907
|
+
mock
|
|
14903
14908
|
}) => {
|
|
14904
14909
|
const start = performance.now();
|
|
14905
|
-
const { results, mode } = await useCase.renderFile(
|
|
14906
|
-
|
|
14907
|
-
|
|
14908
|
-
|
|
14909
|
-
|
|
14910
|
-
|
|
14911
|
-
|
|
14912
|
-
|
|
14913
|
-
|
|
14910
|
+
const { results, mode, fallbackReason } = await useCase.renderFile(
|
|
14911
|
+
path,
|
|
14912
|
+
{
|
|
14913
|
+
props,
|
|
14914
|
+
viewport: { width, height },
|
|
14915
|
+
fullPage: true,
|
|
14916
|
+
engines: ["chromium"],
|
|
14917
|
+
darkMode,
|
|
14918
|
+
tailwindVersion,
|
|
14919
|
+
projectRoot,
|
|
14920
|
+
mock
|
|
14921
|
+
}
|
|
14922
|
+
);
|
|
14914
14923
|
const elapsed = Math.round(performance.now() - start);
|
|
14915
14924
|
const ext = extname(path).toLowerCase();
|
|
14916
14925
|
const framework = EXT_TO_FRAMEWORK[ext] ?? "react";
|
|
14926
|
+
const fallbackNote = mode === "cdn" && fallbackReason ? `
|
|
14927
|
+
\u2139\uFE0F Fell back to CDN pipeline: ${fallbackReason}
|
|
14928
|
+
Imports from node_modules may not resolve. To fix: install vite as a devDep and add a minimal vite.config.` : "";
|
|
14917
14929
|
const content = results.flatMap((r) => [
|
|
14918
14930
|
{
|
|
14919
14931
|
type: "image",
|
|
@@ -14924,7 +14936,7 @@ function registerRenderFileTools(server2, useCase) {
|
|
|
14924
14936
|
type: "text",
|
|
14925
14937
|
text: `[${mode}/${framework}] ${r.width}x${r.height} (${elapsed}ms)${r.consoleErrors.length ? `
|
|
14926
14938
|
\u26A0\uFE0F Console errors:
|
|
14927
|
-
${r.consoleErrors.join("\n")}` : ""}`
|
|
14939
|
+
${r.consoleErrors.join("\n")}` : ""}${fallbackNote}`
|
|
14928
14940
|
}
|
|
14929
14941
|
]);
|
|
14930
14942
|
return { content };
|
|
@@ -14948,7 +14960,8 @@ ${r.consoleErrors.join("\n")}` : ""}`
|
|
|
14948
14960
|
'Render both: ["light","dark"] returns 2 screenshots for comparison'
|
|
14949
14961
|
),
|
|
14950
14962
|
css: external_exports.string().optional().describe("Custom CSS to inject (design tokens, variables, etc)"),
|
|
14951
|
-
tailwindVersion: external_exports.enum(["3", "4"]).optional().default("3").describe("Tailwind CSS version (3 or 4)")
|
|
14963
|
+
tailwindVersion: external_exports.enum(["3", "4"]).optional().default("3").describe("Tailwind CSS version (3 or 4)"),
|
|
14964
|
+
mock: mockSchema
|
|
14952
14965
|
},
|
|
14953
14966
|
wrapHandler(
|
|
14954
14967
|
async ({
|
|
@@ -14961,7 +14974,8 @@ ${r.consoleErrors.join("\n")}` : ""}`
|
|
|
14961
14974
|
darkMode,
|
|
14962
14975
|
colorSchemes,
|
|
14963
14976
|
css,
|
|
14964
|
-
tailwindVersion
|
|
14977
|
+
tailwindVersion,
|
|
14978
|
+
mock
|
|
14965
14979
|
}) => {
|
|
14966
14980
|
const start = performance.now();
|
|
14967
14981
|
const schemes = colorSchemes ?? (darkMode ? ["dark"] : ["light"]);
|
|
@@ -14973,7 +14987,8 @@ ${r.consoleErrors.join("\n")}` : ""}`
|
|
|
14973
14987
|
engines,
|
|
14974
14988
|
darkMode: scheme === "dark",
|
|
14975
14989
|
css,
|
|
14976
|
-
tailwindVersion
|
|
14990
|
+
tailwindVersion,
|
|
14991
|
+
mock
|
|
14977
14992
|
})
|
|
14978
14993
|
)
|
|
14979
14994
|
);
|
|
@@ -15560,21 +15575,9 @@ function registerAllTools(server2, useCases) {
|
|
|
15560
15575
|
|
|
15561
15576
|
// src/index.ts
|
|
15562
15577
|
var container = createContainer();
|
|
15563
|
-
async function ensureBrowser() {
|
|
15564
|
-
try {
|
|
15565
|
-
await container.pool.warmup(["chromium"]);
|
|
15566
|
-
} catch {
|
|
15567
|
-
try {
|
|
15568
|
-
execSync("npx playwright install chromium", { stdio: "pipe" });
|
|
15569
|
-
await container.pool.warmup(["chromium"]);
|
|
15570
|
-
} catch {
|
|
15571
|
-
}
|
|
15572
|
-
}
|
|
15573
|
-
}
|
|
15574
|
-
await ensureBrowser();
|
|
15575
15578
|
var server = new McpServer({
|
|
15576
15579
|
name: "frameshot",
|
|
15577
|
-
version: "0.
|
|
15580
|
+
version: "0.10.0"
|
|
15578
15581
|
});
|
|
15579
15582
|
registerAllTools(server, {
|
|
15580
15583
|
render: container.renderUseCase,
|
|
@@ -15587,6 +15590,18 @@ registerAllTools(server, {
|
|
|
15587
15590
|
});
|
|
15588
15591
|
var transport = new StdioServerTransport();
|
|
15589
15592
|
await server.connect(transport);
|
|
15593
|
+
void (async () => {
|
|
15594
|
+
try {
|
|
15595
|
+
await container.pool.warmup(["chromium"]);
|
|
15596
|
+
} catch {
|
|
15597
|
+
try {
|
|
15598
|
+
execSync("npx playwright install chromium", { stdio: "pipe" });
|
|
15599
|
+
await container.pool.warmup(["chromium"]).catch(() => {
|
|
15600
|
+
});
|
|
15601
|
+
} catch {
|
|
15602
|
+
}
|
|
15603
|
+
}
|
|
15604
|
+
})();
|
|
15590
15605
|
process.on("SIGINT", async () => {
|
|
15591
15606
|
await container.shutdown();
|
|
15592
15607
|
process.exit(0);
|
package/dist/renderer.d.ts
CHANGED
|
@@ -17,6 +17,19 @@ interface RenderOptions {
|
|
|
17
17
|
tailwindVersion: "3" | "4";
|
|
18
18
|
waitFor: number;
|
|
19
19
|
autoFit: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Mock network requests. Keys are URL patterns (path like `/api/users`,
|
|
22
|
+
* glob like `**\/api/*`, or full URLs); values are the response body
|
|
23
|
+
* (auto-serialized as JSON) or a {@link MockResponse} for full control.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* mock: {
|
|
27
|
+
* '/api/users': [{ id: 1, name: 'Alice' }],
|
|
28
|
+
* '/api/posts/*': { status: 200, body: { data: [] } },
|
|
29
|
+
* 'https://api.example.com/x': 'plain text body',
|
|
30
|
+
* }
|
|
31
|
+
*/
|
|
32
|
+
mock?: Record<string, unknown>;
|
|
20
33
|
}
|
|
21
34
|
interface ScreenshotResult {
|
|
22
35
|
engine: Engine;
|
|
@@ -150,6 +163,7 @@ declare class ProjectDetector {
|
|
|
150
163
|
detect(filePath: string): ProjectConfig;
|
|
151
164
|
private findProjectRoot;
|
|
152
165
|
private findViteConfig;
|
|
166
|
+
private readPackageJson;
|
|
153
167
|
private detectFramework;
|
|
154
168
|
private checkViteAvailable;
|
|
155
169
|
private checkIsNextJs;
|
|
@@ -204,6 +218,8 @@ declare class RenderUseCase {
|
|
|
204
218
|
renderFile(filePath: string, options?: Partial<FileRenderOptions>): Promise<{
|
|
205
219
|
results: ScreenshotResult[];
|
|
206
220
|
mode: "vite" | "cdn";
|
|
221
|
+
/** If we fell back to CDN, the reason the Vite pipeline was skipped. */
|
|
222
|
+
fallbackReason?: string;
|
|
207
223
|
}>;
|
|
208
224
|
renderInteraction(code: string, framework: Framework, interactions: Interaction[], options?: Partial<RenderOptions>): Promise<{
|
|
209
225
|
image: string;
|
package/dist/renderer.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frameshot-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.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",
|
|
@@ -72,6 +72,11 @@
|
|
|
72
72
|
"@vitejs/plugin-vue": "^6.0.7",
|
|
73
73
|
"axe-core": "^4.12.1",
|
|
74
74
|
"chokidar": "^5.0.0",
|
|
75
|
+
"get-tsconfig": "^4.14.0",
|
|
76
|
+
"mri": "^1.2.0",
|
|
77
|
+
"nanospinner": "^1.2.2",
|
|
78
|
+
"picocolors": "^1.1.1",
|
|
79
|
+
"picomatch": "^4.0.4",
|
|
75
80
|
"pixelmatch": "^7.2.0",
|
|
76
81
|
"playwright": "^1.52.0",
|
|
77
82
|
"pngjs": "^7.0.0",
|
|
@@ -88,6 +93,7 @@
|
|
|
88
93
|
"devDependencies": {
|
|
89
94
|
"@biomejs/biome": "^2.5.0",
|
|
90
95
|
"@types/node": "^26.0.0",
|
|
96
|
+
"@types/picomatch": "^4.0.3",
|
|
91
97
|
"@types/pngjs": "^6.0.5",
|
|
92
98
|
"tsup": "^8.0.0",
|
|
93
99
|
"typescript": "^5.7.0",
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { execSync } from "node:child_process";
|
|
4
4
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { basename, extname, join, relative } from "node:path";
|
|
6
|
+
import picomatch from "picomatch";
|
|
6
7
|
|
|
7
8
|
const {
|
|
8
9
|
INPUT_PATHS = "",
|
|
@@ -26,14 +27,13 @@ const COMPONENT_EXTS = new Set(
|
|
|
26
27
|
INPUT_EXTENSIONS.split(",").map((e) => e.trim().toLowerCase()).filter(Boolean),
|
|
27
28
|
);
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
// Compile exclude globs once via picomatch — handles `*`, `**`, `?`, `[abc]`,
|
|
31
|
+
// brace expansion, and negation properly. Match against the file basename.
|
|
32
|
+
const excludeGlobs = INPUT_EXCLUDE.split(",")
|
|
30
33
|
.map((p) => p.trim())
|
|
31
|
-
.filter(Boolean)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const escaped = p.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*");
|
|
35
|
-
return new RegExp(escaped);
|
|
36
|
-
});
|
|
34
|
+
.filter(Boolean);
|
|
35
|
+
const isExcludedGlob =
|
|
36
|
+
excludeGlobs.length > 0 ? picomatch(excludeGlobs) : () => false;
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* Derive a human-friendly display label from a file path.
|
|
@@ -80,8 +80,7 @@ function safeFileId(label) {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
function isExcluded(filePath) {
|
|
83
|
-
|
|
84
|
-
return EXCLUDE_PATTERNS.some((re) => re.test(base));
|
|
83
|
+
return isExcludedGlob(basename(filePath));
|
|
85
84
|
}
|
|
86
85
|
|
|
87
86
|
function findFiles(workspace, patterns) {
|
|
@@ -260,12 +259,17 @@ async function main() {
|
|
|
260
259
|
try {
|
|
261
260
|
// Render current (after) version
|
|
262
261
|
console.log(` Rendering (after): ${filePath}`);
|
|
263
|
-
const {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
262
|
+
const {
|
|
263
|
+
results: afterResults,
|
|
264
|
+
mode,
|
|
265
|
+
fallbackReason,
|
|
266
|
+
} = await renderUseCase.renderFile(filePath, renderOpts);
|
|
267
267
|
const afterResult = afterResults[0];
|
|
268
268
|
|
|
269
|
+
if (mode === "cdn" && fallbackReason) {
|
|
270
|
+
console.log(` ℹ️ CDN fallback: ${fallbackReason}`);
|
|
271
|
+
}
|
|
272
|
+
|
|
269
273
|
const afterPath = join(outputDir, `${safeName}_after.png`);
|
|
270
274
|
writeFileSync(afterPath, Buffer.from(afterResult.image, "base64"));
|
|
271
275
|
|