frameshot-mcp 0.9.7 → 0.10.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.
@@ -10,7 +10,7 @@ import {
10
10
  SnapshotStore,
11
11
  SnapshotUseCase,
12
12
  ViteBundler
13
- } from "./chunk-MEBQ7ZWA.js";
13
+ } from "./chunk-WACVDBUF.js";
14
14
 
15
15
  // src/use-cases/watch.ts
16
16
  import { watch } from "chokidar";
@@ -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 framework = this.detectFramework(root);
278
+ const pkg = this.readPackageJson(root);
279
+ const framework = this.detectFramework(pkg);
278
280
  const hasVite = this.checkViteAvailable(root);
279
- const isNextJs = this.checkIsNextJs(root);
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
- detectFramework(root) {
304
+ readPackageJson(root) {
303
305
  const pkgPath = join(root, "package.json");
304
- if (!existsSync(pkgPath)) return "html";
306
+ if (!existsSync(pkgPath)) return null;
305
307
  try {
306
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
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(root) {
325
- const pkgPath = join(root, "package.json");
326
- if (!existsSync(pkgPath)) return false;
327
- try {
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 content = readFileSync(tscPath, "utf-8");
340
- const stripped = content.replace(/^\s*\/\/[^\n]*/gm, "");
341
- const tsc = JSON.parse(stripped);
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(() => ({
@@ -1578,6 +1614,7 @@ var RenderUseCase = class {
1578
1614
  const page = await this.pool.getPage(engine);
1579
1615
  await page.setViewportSize(options.viewport);
1580
1616
  const { errors, cleanup } = attachConsoleCapture(page);
1617
+ if (options.mock) await attachMocks(page, options.mock);
1581
1618
  await page.setContent(html, { waitUntil: "load", timeout: 1e4 });
1582
1619
  if (options.waitFor > 0) await page.waitForTimeout(options.waitFor);
1583
1620
  const result = await takeScreenshot(
@@ -1593,6 +1630,7 @@ var RenderUseCase = class {
1593
1630
  async renderUrl(engine, url, options) {
1594
1631
  const page = await this.pool.getPage(engine);
1595
1632
  const { errors, cleanup } = attachConsoleCapture(page);
1633
+ if (options.mock) await attachMocks(page, options.mock);
1596
1634
  const viewport = options.autoFit ? await this.resolveAutoFitViewport(page, url, options) : options.viewport;
1597
1635
  await page.setViewportSize(viewport);
1598
1636
  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-VUYZHZBH.js";
4
+ } from "./chunk-6OTGZDUU.js";
5
5
  import {
6
6
  EXT_TO_FRAMEWORK
7
- } from "./chunk-MEBQ7ZWA.js";
7
+ } from "./chunk-WACVDBUF.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
- let i = 0;
54
- const interval = setInterval(() => {
55
- process.stderr.write(
56
- `\r ${c.purple}${FRAMES[i % FRAMES.length]}${c.reset} ${c.dim}${text}${c.reset}`
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
- clearInterval(interval);
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 command = args[0] ?? "help";
304
- const positional = args[1] ?? "";
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: ".frameshot",
307
- recursive: false
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
- for (let i = 2; i < args.length; i++) {
310
- const arg = args[i];
311
- switch (arg) {
312
- case "--props":
313
- try {
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" || command === "--help" || command === "-h") {
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-VUYZHZBH.js";
4
+ } from "./chunk-6OTGZDUU.js";
5
5
  import {
6
6
  DEVICE_PRESETS,
7
7
  EXT_TO_FRAMEWORK,
8
8
  __export
9
- } from "./chunk-MEBQ7ZWA.js";
9
+ } from "./chunk-WACVDBUF.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,7 +14903,8 @@ 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
14910
  const { results, mode } = await useCase.renderFile(path, {
@@ -14909,7 +14914,8 @@ function registerRenderFileTools(server2, useCase) {
14909
14914
  engines: ["chromium"],
14910
14915
  darkMode,
14911
14916
  tailwindVersion,
14912
- projectRoot
14917
+ projectRoot,
14918
+ mock
14913
14919
  });
14914
14920
  const elapsed = Math.round(performance.now() - start);
14915
14921
  const ext = extname(path).toLowerCase();
@@ -14948,7 +14954,8 @@ ${r.consoleErrors.join("\n")}` : ""}`
14948
14954
  'Render both: ["light","dark"] returns 2 screenshots for comparison'
14949
14955
  ),
14950
14956
  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)")
14957
+ tailwindVersion: external_exports.enum(["3", "4"]).optional().default("3").describe("Tailwind CSS version (3 or 4)"),
14958
+ mock: mockSchema
14952
14959
  },
14953
14960
  wrapHandler(
14954
14961
  async ({
@@ -14961,7 +14968,8 @@ ${r.consoleErrors.join("\n")}` : ""}`
14961
14968
  darkMode,
14962
14969
  colorSchemes,
14963
14970
  css,
14964
- tailwindVersion
14971
+ tailwindVersion,
14972
+ mock
14965
14973
  }) => {
14966
14974
  const start = performance.now();
14967
14975
  const schemes = colorSchemes ?? (darkMode ? ["dark"] : ["light"]);
@@ -14973,7 +14981,8 @@ ${r.consoleErrors.join("\n")}` : ""}`
14973
14981
  engines,
14974
14982
  darkMode: scheme === "dark",
14975
14983
  css,
14976
- tailwindVersion
14984
+ tailwindVersion,
14985
+ mock
14977
14986
  })
14978
14987
  )
14979
14988
  );
@@ -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;
package/dist/renderer.js CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  SnapshotStore,
14
14
  SnapshotUseCase,
15
15
  ViteBundler
16
- } from "./chunk-MEBQ7ZWA.js";
16
+ } from "./chunk-WACVDBUF.js";
17
17
  export {
18
18
  AuditUseCase,
19
19
  BrowserPool,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frameshot-mcp",
3
- "version": "0.9.7",
3
+ "version": "0.10.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
- const EXCLUDE_PATTERNS = INPUT_EXCLUDE.split(",")
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
- .map((p) => {
33
- // Convert glob pattern to regex: *.test.* /[^/]*\.test\.[^/]*/
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
- const base = filePath.split("/").pop() ?? "";
84
- return EXCLUDE_PATTERNS.some((re) => re.test(base));
83
+ return isExcludedGlob(basename(filePath));
85
84
  }
86
85
 
87
86
  function findFiles(workspace, patterns) {