frameshot-mcp 0.8.0 → 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.
Files changed (73) hide show
  1. package/README.md +68 -122
  2. package/action.yml +14 -5
  3. package/dist/{chunk-MA3FOIQY.js → chunk-6OTGZDUU.js} +1 -1
  4. package/dist/{chunk-PYWXJZTZ.js → chunk-WACVDBUF.js} +809 -120
  5. package/dist/cli.js +123 -125
  6. package/dist/index.js +390 -532
  7. package/dist/renderer.d.ts +21 -3
  8. package/dist/renderer.js +1 -1
  9. package/dist/stubs/gatsby.js +20 -0
  10. package/dist/stubs/next-font.js +9 -0
  11. package/dist/stubs/next-headers.js +20 -0
  12. package/dist/stubs/next-image.js +34 -0
  13. package/dist/stubs/next-link.js +25 -0
  14. package/dist/stubs/next-navigation.js +17 -0
  15. package/dist/stubs/next-router.js +19 -0
  16. package/dist/stubs/nuxt-app.js +37 -0
  17. package/dist/stubs/nuxt-imports.js +13 -0
  18. package/dist/stubs/qwik-city.js +33 -0
  19. package/dist/stubs/react-router.js +67 -0
  20. package/dist/stubs/server-only.js +2 -0
  21. package/dist/stubs/solid-router.js +27 -0
  22. package/dist/stubs/solid-start.js +18 -0
  23. package/dist/stubs/stubs/gatsby.js +20 -0
  24. package/dist/stubs/stubs/next-font.js +9 -0
  25. package/dist/stubs/stubs/next-headers.js +20 -0
  26. package/dist/stubs/stubs/next-image.js +34 -0
  27. package/dist/stubs/stubs/next-link.js +25 -0
  28. package/dist/stubs/stubs/next-navigation.js +17 -0
  29. package/dist/stubs/stubs/next-router.js +19 -0
  30. package/dist/stubs/stubs/nuxt-app.js +37 -0
  31. package/dist/stubs/stubs/nuxt-imports.js +13 -0
  32. package/dist/stubs/stubs/qwik-city.js +33 -0
  33. package/dist/stubs/stubs/react-router.js +67 -0
  34. package/dist/stubs/stubs/server-only.js +2 -0
  35. package/dist/stubs/stubs/solid-router.js +27 -0
  36. package/dist/stubs/stubs/solid-start.js +18 -0
  37. package/dist/stubs/stubs/sveltekit-environment.js +5 -0
  38. package/dist/stubs/stubs/sveltekit-navigation.js +11 -0
  39. package/dist/stubs/stubs/sveltekit-stores.js +15 -0
  40. package/dist/stubs/stubs/vike.js +11 -0
  41. package/dist/stubs/sveltekit-environment.js +5 -0
  42. package/dist/stubs/sveltekit-navigation.js +11 -0
  43. package/dist/stubs/sveltekit-stores.js +15 -0
  44. package/dist/stubs/vike.js +11 -0
  45. package/package.json +14 -3
  46. package/scripts/render-changed.mjs +75 -21
  47. package/dist/chunk-3CDSNOX5.js +0 -869
  48. package/dist/chunk-3LVWVDET.js +0 -849
  49. package/dist/chunk-47YJG5HR.js +0 -690
  50. package/dist/chunk-67JZQ6OI.js +0 -819
  51. package/dist/chunk-AUACBLHM.js +0 -191
  52. package/dist/chunk-AZCGKIMU.js +0 -850
  53. package/dist/chunk-B3CLIGWU.js +0 -786
  54. package/dist/chunk-C6QSY4WR.js +0 -811
  55. package/dist/chunk-DX54PJKO.js +0 -603
  56. package/dist/chunk-EMCJGIMY.js +0 -984
  57. package/dist/chunk-FQNWGR62.js +0 -849
  58. package/dist/chunk-FTYTZW6D.js +0 -203
  59. package/dist/chunk-GVOEFYEX.js +0 -139
  60. package/dist/chunk-JGVKYXY2.js +0 -857
  61. package/dist/chunk-JYPEA4P2.js +0 -846
  62. package/dist/chunk-KHK35HDD.js +0 -855
  63. package/dist/chunk-L2CADTS7.js +0 -191
  64. package/dist/chunk-O7NWWFIU.js +0 -871
  65. package/dist/chunk-Q7A3DLED.js +0 -848
  66. package/dist/chunk-Q7NQA4ZM.js +0 -1095
  67. package/dist/chunk-SIA6XEHM.js +0 -811
  68. package/dist/chunk-ST35YDI6.js +0 -834
  69. package/dist/chunk-T5OBJK35.js +0 -855
  70. package/dist/chunk-U3GHS7KO.js +0 -837
  71. package/dist/chunk-WS2ASCD6.js +0 -683
  72. package/dist/chunk-WZMHVSUA.js +0 -847
  73. package/dist/chunk-ZZST6K7Y.js +0 -987
@@ -4,6 +4,31 @@ var __export = (target, all) => {
4
4
  __defProp(target, name, { get: all[name], enumerable: true });
5
5
  };
6
6
 
7
+ // src/domain/types.ts
8
+ var DEFAULT_RENDER_OPTIONS = {
9
+ viewport: { width: 1280, height: 800 },
10
+ engines: ["chromium"],
11
+ fullPage: true,
12
+ darkMode: false,
13
+ css: "",
14
+ tailwindVersion: "3",
15
+ waitFor: 0,
16
+ autoFit: false
17
+ };
18
+ var DEVICE_PRESETS = {
19
+ mobile: { width: 375, height: 667 },
20
+ tablet: { width: 768, height: 1024 },
21
+ desktop: { width: 1280, height: 800 }
22
+ };
23
+ var EXT_TO_FRAMEWORK = {
24
+ ".jsx": "react",
25
+ ".tsx": "react",
26
+ ".vue": "vue",
27
+ ".svelte": "svelte",
28
+ ".html": "html",
29
+ ".htm": "html"
30
+ };
31
+
7
32
  // src/infrastructure/browser-pool.ts
8
33
  import { chromium, firefox, webkit } from "playwright";
9
34
  var BrowserPool = class {
@@ -78,14 +103,18 @@ var BrowserPool = class {
78
103
  const launcher = { chromium, firefox, webkit }[engine];
79
104
  let browser;
80
105
  try {
81
- browser = await launcher.launch({
82
- headless: true,
83
- ...engine === "chromium" ? { channel: "chrome" } : {}
84
- });
85
- } catch (_e) {
86
- throw new Error(
87
- `${engine} is not installed. Run: npx playwright install ${engine}`
88
- );
106
+ browser = await launcher.launch({ headless: true });
107
+ } catch {
108
+ try {
109
+ browser = await launcher.launch({
110
+ headless: true,
111
+ ...engine === "chromium" ? { channel: "chrome" } : {}
112
+ });
113
+ } catch (_e) {
114
+ throw new Error(
115
+ `${engine} is not installed. Run: npx playwright install ${engine}`
116
+ );
117
+ }
89
118
  }
90
119
  const slot = { browser, idlePages: [] };
91
120
  this.pool.set(engine, slot);
@@ -235,6 +264,7 @@ var SnapshotStore = class {
235
264
  // src/infrastructure/project-detector.ts
236
265
  import { existsSync, readFileSync } from "fs";
237
266
  import { dirname, join, resolve } from "path";
267
+ import { parseTsconfig } from "get-tsconfig";
238
268
  var VITE_CONFIG_NAMES = [
239
269
  "vite.config.ts",
240
270
  "vite.config.js",
@@ -245,9 +275,12 @@ var ProjectDetector = class {
245
275
  detect(filePath) {
246
276
  const root = this.findProjectRoot(filePath);
247
277
  const viteConfigPath = this.findViteConfig(root);
248
- const framework = this.detectFramework(root);
278
+ const pkg = this.readPackageJson(root);
279
+ const framework = this.detectFramework(pkg);
249
280
  const hasVite = this.checkViteAvailable(root);
250
- return { root, viteConfigPath, framework, hasVite };
281
+ const isNextJs = this.checkIsNextJs(pkg);
282
+ const pathAliases = this.readTsconfigAliases(root);
283
+ return { root, viteConfigPath, framework, hasVite, isNextJs, pathAliases };
251
284
  }
252
285
  findProjectRoot(startPath) {
253
286
  let dir = dirname(resolve(startPath));
@@ -268,39 +301,616 @@ var ProjectDetector = class {
268
301
  }
269
302
  return void 0;
270
303
  }
271
- detectFramework(root) {
304
+ readPackageJson(root) {
272
305
  const pkgPath = join(root, "package.json");
273
- if (!existsSync(pkgPath)) return "html";
306
+ if (!existsSync(pkgPath)) return null;
274
307
  try {
275
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
276
- const allDeps = {
277
- ...pkg.dependencies,
278
- ...pkg.devDependencies
279
- };
280
- if (allDeps.react || allDeps["react-dom"]) return "react";
281
- if (allDeps.vue) return "vue";
282
- if (allDeps.svelte) return "svelte";
308
+ return JSON.parse(readFileSync(pkgPath, "utf-8"));
283
309
  } catch {
310
+ return null;
284
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";
285
321
  return "html";
286
322
  }
287
323
  checkViteAvailable(root) {
288
324
  const vitePkgPath = join(root, "node_modules", "vite", "package.json");
289
325
  return existsSync(vitePkgPath);
290
326
  }
327
+ checkIsNextJs(pkg) {
328
+ if (!pkg) return false;
329
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
330
+ return !!allDeps.next;
331
+ }
332
+ readTsconfigAliases(root) {
333
+ const tscPath = join(root, "tsconfig.json");
334
+ if (!existsSync(tscPath)) return {};
335
+ try {
336
+ const tsconfig = parseTsconfig(tscPath);
337
+ const paths = tsconfig.compilerOptions?.paths ?? {};
338
+ const baseUrl = tsconfig.compilerOptions?.baseUrl ?? ".";
339
+ const result = {};
340
+ for (const [alias, targets] of Object.entries(paths)) {
341
+ const key = alias.replace(/\/\*$/, "");
342
+ const target = (targets[0] ?? "").replace(/\/\*$/, "");
343
+ result[key] = join(root, baseUrl, target);
344
+ }
345
+ return result;
346
+ } catch {
347
+ return {};
348
+ }
349
+ }
291
350
  };
292
351
 
293
352
  // src/infrastructure/vite-bundler.ts
294
- import { existsSync as existsSync2 } from "fs";
353
+ import { existsSync as existsSync4 } from "fs";
295
354
  import { createRequire } from "module";
296
- import { join as join2, resolve as resolve2 } from "path";
355
+ import { dirname as dirname2, join as join18, resolve as resolve2 } from "path";
356
+ import { fileURLToPath } from "url";
357
+
358
+ // src/infrastructure/adapters/astro.ts
359
+ import { join as join3 } from "path";
360
+
361
+ // src/infrastructure/adapters/helpers.ts
362
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
363
+ import { join as join2 } from "path";
364
+ function hasDep(pkgPath, names) {
365
+ if (!existsSync2(pkgPath)) return false;
366
+ try {
367
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
368
+ const all = { ...pkg.dependencies, ...pkg.devDependencies };
369
+ return names.some((n) => !!all[n]);
370
+ } catch {
371
+ return false;
372
+ }
373
+ }
374
+ function hasInstalled(projectRoot, name) {
375
+ return existsSync2(join2(projectRoot, "node_modules", ...name.split("/")));
376
+ }
377
+ async function loadPlugin(pkg, fnName) {
378
+ try {
379
+ const mod = await import(pkg);
380
+ const candidate = fnName ? mod[fnName] : mod.default ?? mod;
381
+ if (typeof candidate === "function") {
382
+ const result = candidate();
383
+ return Array.isArray(result) ? result : [result];
384
+ }
385
+ } catch {
386
+ process.stderr.write(`[frameshot] ${pkg} not available
387
+ `);
388
+ }
389
+ return [];
390
+ }
391
+ function loadReactPlugin() {
392
+ return loadPlugin("@vitejs/plugin-react");
393
+ }
394
+ function rscDirectivePlugin() {
395
+ return {
396
+ name: "frameshot-rsc-directives",
397
+ transform(code) {
398
+ return code.replace(/^['"]use (client|server)['"];?\s*/m, "");
399
+ }
400
+ };
401
+ }
402
+
403
+ // src/infrastructure/adapters/astro.ts
404
+ var AstroAdapter = class {
405
+ id = "astro";
406
+ detect(projectRoot) {
407
+ return hasDep(join3(projectRoot, "package.json"), ["astro"]);
408
+ }
409
+ getAliases() {
410
+ return {};
411
+ }
412
+ getOptimizeDeps() {
413
+ return [];
414
+ }
415
+ async getPlugins(projectRoot) {
416
+ const plugins = [];
417
+ if (hasInstalled(projectRoot, "react"))
418
+ plugins.push(...await loadReactPlugin());
419
+ if (hasInstalled(projectRoot, "vue"))
420
+ plugins.push(...await loadPlugin("@vitejs/plugin-vue"));
421
+ if (hasInstalled(projectRoot, "svelte"))
422
+ plugins.push(
423
+ ...await loadPlugin("@sveltejs/vite-plugin-svelte", "svelte")
424
+ );
425
+ if (hasInstalled(projectRoot, "solid-js"))
426
+ plugins.push(...await loadPlugin("vite-plugin-solid"));
427
+ return plugins;
428
+ }
429
+ useFrameshotVite() {
430
+ return true;
431
+ }
432
+ skipProjectConfig() {
433
+ return true;
434
+ }
435
+ };
436
+
437
+ // src/infrastructure/adapters/gatsby.ts
438
+ import { join as join4 } from "path";
439
+ var GatsbyAdapter = class {
440
+ id = "gatsby";
441
+ detect(projectRoot) {
442
+ return hasDep(join4(projectRoot, "package.json"), ["gatsby"]);
443
+ }
444
+ getAliases(_root, stubsDir) {
445
+ return { gatsby: join4(stubsDir, "gatsby.js") };
446
+ }
447
+ getOptimizeDeps(_root) {
448
+ return ["react", "react-dom/client"];
449
+ }
450
+ async getPlugins(_root) {
451
+ return loadReactPlugin();
452
+ }
453
+ useFrameshotVite() {
454
+ return true;
455
+ }
456
+ skipProjectConfig() {
457
+ return true;
458
+ }
459
+ };
460
+
461
+ // src/infrastructure/adapters/generic.ts
462
+ var GenericAdapter = class {
463
+ id = "generic";
464
+ detect() {
465
+ return true;
466
+ }
467
+ getAliases() {
468
+ return {};
469
+ }
470
+ getOptimizeDeps(projectRoot) {
471
+ const deps = [];
472
+ if (hasInstalled(projectRoot, "react"))
473
+ deps.push("react", "react-dom/client");
474
+ if (hasInstalled(projectRoot, "vue")) deps.push("vue");
475
+ if (hasInstalled(projectRoot, "solid-js"))
476
+ deps.push("solid-js", "solid-js/web");
477
+ if (hasInstalled(projectRoot, "preact")) deps.push("preact");
478
+ return deps;
479
+ }
480
+ async getPlugins(projectRoot) {
481
+ const plugins = [];
482
+ if (hasInstalled(projectRoot, "react"))
483
+ plugins.push(...await loadReactPlugin());
484
+ if (hasInstalled(projectRoot, "vue"))
485
+ plugins.push(...await loadPlugin("@vitejs/plugin-vue"));
486
+ if (hasInstalled(projectRoot, "svelte"))
487
+ plugins.push(
488
+ ...await loadPlugin("@sveltejs/vite-plugin-svelte", "svelte")
489
+ );
490
+ if (hasInstalled(projectRoot, "solid-js"))
491
+ plugins.push(...await loadPlugin("vite-plugin-solid"));
492
+ if (hasInstalled(projectRoot, "preact"))
493
+ plugins.push(...await loadPlugin("@preact/preset-vite"));
494
+ return plugins;
495
+ }
496
+ useFrameshotVite() {
497
+ return true;
498
+ }
499
+ skipProjectConfig() {
500
+ return true;
501
+ }
502
+ };
503
+
504
+ // src/infrastructure/adapters/lit.ts
505
+ import { join as join5 } from "path";
506
+ var LitAdapter = class {
507
+ id = "lit";
508
+ detect(projectRoot) {
509
+ return hasDep(join5(projectRoot, "package.json"), ["lit", "lit-element"]);
510
+ }
511
+ getAliases() {
512
+ return {};
513
+ }
514
+ getOptimizeDeps() {
515
+ return ["lit"];
516
+ }
517
+ async getPlugins(_root) {
518
+ return [];
519
+ }
520
+ useFrameshotVite() {
521
+ return true;
522
+ }
523
+ skipProjectConfig() {
524
+ return true;
525
+ }
526
+ };
527
+
528
+ // src/infrastructure/adapters/next.ts
529
+ import { join as join6 } from "path";
530
+ var NextJsAdapter = class {
531
+ id = "next";
532
+ detect(projectRoot) {
533
+ return hasDep(join6(projectRoot, "package.json"), ["next"]);
534
+ }
535
+ getAliases(_root, stubsDir) {
536
+ return {
537
+ "next/navigation": join6(stubsDir, "next-navigation.js"),
538
+ "next/router": join6(stubsDir, "next-router.js"),
539
+ "next/image": join6(stubsDir, "next-image.js"),
540
+ "next/link": join6(stubsDir, "next-link.js"),
541
+ "next/font/google": join6(stubsDir, "next-font.js"),
542
+ "next/font/local": join6(stubsDir, "next-font.js"),
543
+ "next/headers": join6(stubsDir, "next-headers.js"),
544
+ "server-only": join6(stubsDir, "server-only.js"),
545
+ "client-only": join6(stubsDir, "server-only.js")
546
+ };
547
+ }
548
+ getOptimizeDeps(projectRoot) {
549
+ const base = ["react", "react-dom/client"];
550
+ const optional = [
551
+ "@mui/material",
552
+ "@mui/icons-material",
553
+ "@emotion/react",
554
+ "@emotion/styled",
555
+ "@chakra-ui/react",
556
+ "styled-components"
557
+ ];
558
+ return [
559
+ ...base,
560
+ ...optional.filter((p) => hasInstalled(projectRoot, p.split("/")[0]))
561
+ ];
562
+ }
563
+ async getPlugins(_root) {
564
+ return [...await loadReactPlugin(), rscDirectivePlugin()];
565
+ }
566
+ useFrameshotVite() {
567
+ return true;
568
+ }
569
+ skipProjectConfig() {
570
+ return true;
571
+ }
572
+ };
573
+
574
+ // src/infrastructure/adapters/nuxt.ts
575
+ import { join as join7 } from "path";
576
+ var NuxtAdapter = class {
577
+ id = "nuxt";
578
+ detect(projectRoot) {
579
+ return hasDep(join7(projectRoot, "package.json"), ["nuxt"]);
580
+ }
581
+ getAliases(_root, stubsDir) {
582
+ return {
583
+ "#app": join7(stubsDir, "nuxt-app.js"),
584
+ "#imports": join7(stubsDir, "nuxt-imports.js"),
585
+ "#head": join7(stubsDir, "nuxt-app.js")
586
+ };
587
+ }
588
+ getOptimizeDeps(_root) {
589
+ return ["vue"];
590
+ }
591
+ async getPlugins(_root) {
592
+ return loadPlugin("@vitejs/plugin-vue");
593
+ }
594
+ useFrameshotVite() {
595
+ return true;
596
+ }
597
+ skipProjectConfig() {
598
+ return true;
599
+ }
600
+ };
601
+
602
+ // src/infrastructure/adapters/preact.ts
603
+ import { join as join8 } from "path";
604
+ var PreactAdapter = class {
605
+ id = "preact";
606
+ detect(projectRoot) {
607
+ return hasDep(join8(projectRoot, "package.json"), ["preact"]);
608
+ }
609
+ getAliases() {
610
+ return {};
611
+ }
612
+ getOptimizeDeps() {
613
+ return ["preact", "preact/hooks"];
614
+ }
615
+ async getPlugins(_root) {
616
+ return loadPlugin("@preact/preset-vite");
617
+ }
618
+ useFrameshotVite() {
619
+ return true;
620
+ }
621
+ skipProjectConfig() {
622
+ return true;
623
+ }
624
+ };
625
+
626
+ // src/infrastructure/adapters/qwik.ts
627
+ import { join as join9 } from "path";
628
+ var QwikAdapter = class {
629
+ id = "qwik";
630
+ detect(projectRoot) {
631
+ return hasDep(join9(projectRoot, "package.json"), ["@builder.io/qwik"]);
632
+ }
633
+ getAliases(_root, stubsDir) {
634
+ return { "@builder.io/qwik-city": join9(stubsDir, "qwik-city.js") };
635
+ }
636
+ getOptimizeDeps(_root) {
637
+ return ["@builder.io/qwik"];
638
+ }
639
+ async getPlugins(_root) {
640
+ return loadPlugin("@builder.io/qwik/optimizer", "qwikVite");
641
+ }
642
+ useFrameshotVite() {
643
+ return false;
644
+ }
645
+ skipProjectConfig() {
646
+ return true;
647
+ }
648
+ };
649
+
650
+ // src/infrastructure/adapters/react-router.ts
651
+ import { join as join10 } from "path";
652
+ var ReactRouterAdapter = class {
653
+ id = "react-router";
654
+ detect(projectRoot) {
655
+ return hasDep(join10(projectRoot, "package.json"), [
656
+ "@react-router/dev",
657
+ "@remix-run/react"
658
+ ]);
659
+ }
660
+ getAliases(_root, stubsDir) {
661
+ return {
662
+ "react-router": join10(stubsDir, "react-router.js"),
663
+ "react-router-dom": join10(stubsDir, "react-router.js"),
664
+ "@remix-run/react": join10(stubsDir, "react-router.js")
665
+ };
666
+ }
667
+ getOptimizeDeps(_root) {
668
+ return ["react", "react-dom/client"];
669
+ }
670
+ async getPlugins(_root) {
671
+ return [...await loadReactPlugin(), rscDirectivePlugin()];
672
+ }
673
+ useFrameshotVite() {
674
+ return true;
675
+ }
676
+ skipProjectConfig() {
677
+ return true;
678
+ }
679
+ };
680
+
681
+ // src/infrastructure/adapters/solid.ts
682
+ import { join as join11 } from "path";
683
+ var SolidAdapter = class {
684
+ id = "solid";
685
+ detect(projectRoot) {
686
+ return hasDep(join11(projectRoot, "package.json"), ["solid-js"]);
687
+ }
688
+ getAliases(_root, stubsDir) {
689
+ return { "@solidjs/router": join11(stubsDir, "solid-router.js") };
690
+ }
691
+ getOptimizeDeps() {
692
+ return ["solid-js", "solid-js/web"];
693
+ }
694
+ async getPlugins(_root) {
695
+ return loadPlugin("vite-plugin-solid");
696
+ }
697
+ useFrameshotVite() {
698
+ return true;
699
+ }
700
+ skipProjectConfig() {
701
+ return true;
702
+ }
703
+ };
704
+
705
+ // src/infrastructure/adapters/solid-start.ts
706
+ import { join as join12 } from "path";
707
+ var SolidStartAdapter = class {
708
+ id = "solid-start";
709
+ detect(projectRoot) {
710
+ return hasDep(join12(projectRoot, "package.json"), ["@solidjs/start"]);
711
+ }
712
+ getAliases(_root, stubsDir) {
713
+ return {
714
+ "@solidjs/start": join12(stubsDir, "solid-start.js"),
715
+ "@solidjs/start/router": join12(stubsDir, "solid-router.js"),
716
+ "@solidjs/router": join12(stubsDir, "solid-router.js")
717
+ };
718
+ }
719
+ getOptimizeDeps(_root) {
720
+ return ["solid-js", "solid-js/web"];
721
+ }
722
+ async getPlugins(_root) {
723
+ return loadPlugin("vite-plugin-solid");
724
+ }
725
+ useFrameshotVite() {
726
+ return true;
727
+ }
728
+ skipProjectConfig() {
729
+ return true;
730
+ }
731
+ };
732
+
733
+ // src/infrastructure/adapters/svelte.ts
734
+ import { join as join13 } from "path";
735
+ var SvelteAdapter = class {
736
+ id = "svelte";
737
+ detect(projectRoot) {
738
+ return hasDep(join13(projectRoot, "package.json"), ["svelte"]);
739
+ }
740
+ getAliases() {
741
+ return {};
742
+ }
743
+ getOptimizeDeps() {
744
+ return [];
745
+ }
746
+ async getPlugins(_root) {
747
+ return loadPlugin("@sveltejs/vite-plugin-svelte", "svelte");
748
+ }
749
+ useFrameshotVite() {
750
+ return true;
751
+ }
752
+ skipProjectConfig() {
753
+ return true;
754
+ }
755
+ };
756
+
757
+ // src/infrastructure/adapters/sveltekit.ts
758
+ import { join as join14 } from "path";
759
+ var SvelteKitAdapter = class {
760
+ id = "sveltekit";
761
+ detect(projectRoot) {
762
+ return hasDep(join14(projectRoot, "package.json"), ["@sveltejs/kit"]);
763
+ }
764
+ getAliases(_root, stubsDir) {
765
+ return {
766
+ "$app/navigation": join14(stubsDir, "sveltekit-navigation.js"),
767
+ "$app/stores": join14(stubsDir, "sveltekit-stores.js"),
768
+ "$app/environment": join14(stubsDir, "sveltekit-environment.js")
769
+ };
770
+ }
771
+ getOptimizeDeps(_root) {
772
+ return [];
773
+ }
774
+ async getPlugins(_root) {
775
+ return loadPlugin("@sveltejs/vite-plugin-svelte", "svelte");
776
+ }
777
+ useFrameshotVite() {
778
+ return true;
779
+ }
780
+ skipProjectConfig() {
781
+ return true;
782
+ }
783
+ };
784
+
785
+ // src/infrastructure/adapters/vike.ts
786
+ import { join as join15 } from "path";
787
+ var VikeAdapter = class {
788
+ id = "vike";
789
+ detect(projectRoot) {
790
+ return hasDep(join15(projectRoot, "package.json"), ["vike"]);
791
+ }
792
+ getAliases(_root, stubsDir) {
793
+ return {
794
+ vike: join15(stubsDir, "vike.js"),
795
+ "vike/client/router": join15(stubsDir, "vike.js")
796
+ };
797
+ }
798
+ getOptimizeDeps(projectRoot) {
799
+ const deps = [];
800
+ if (hasInstalled(projectRoot, "react"))
801
+ deps.push("react", "react-dom/client");
802
+ if (hasInstalled(projectRoot, "vue")) deps.push("vue");
803
+ return deps;
804
+ }
805
+ async getPlugins(projectRoot) {
806
+ const plugins = [];
807
+ if (hasInstalled(projectRoot, "react"))
808
+ plugins.push(...await loadReactPlugin());
809
+ if (hasInstalled(projectRoot, "vue"))
810
+ plugins.push(...await loadPlugin("@vitejs/plugin-vue"));
811
+ return plugins;
812
+ }
813
+ useFrameshotVite() {
814
+ return true;
815
+ }
816
+ skipProjectConfig() {
817
+ return true;
818
+ }
819
+ };
820
+
821
+ // src/infrastructure/adapters/vite-react.ts
822
+ import { existsSync as existsSync3 } from "fs";
823
+ import { join as join16 } from "path";
824
+ var ViteReactAdapter = class {
825
+ id = "vite-react";
826
+ detect(projectRoot) {
827
+ return hasDep(join16(projectRoot, "package.json"), ["react"]) && this.hasViteConfig(projectRoot);
828
+ }
829
+ hasViteConfig(root) {
830
+ return ["vite.config.ts", "vite.config.js", "vite.config.mjs"].some(
831
+ (n) => existsSync3(join16(root, n))
832
+ );
833
+ }
834
+ getAliases() {
835
+ return {};
836
+ }
837
+ getOptimizeDeps() {
838
+ return ["react", "react-dom/client"];
839
+ }
840
+ async getPlugins() {
841
+ return [];
842
+ }
843
+ useFrameshotVite() {
844
+ return false;
845
+ }
846
+ skipProjectConfig() {
847
+ return false;
848
+ }
849
+ };
850
+
851
+ // src/infrastructure/adapters/vue.ts
852
+ import { join as join17 } from "path";
853
+ var VueAdapter = class {
854
+ id = "vue";
855
+ detect(projectRoot) {
856
+ return hasDep(join17(projectRoot, "package.json"), ["vue"]);
857
+ }
858
+ getAliases() {
859
+ return {};
860
+ }
861
+ getOptimizeDeps() {
862
+ return ["vue"];
863
+ }
864
+ async getPlugins(_root) {
865
+ return loadPlugin("@vitejs/plugin-vue");
866
+ }
867
+ useFrameshotVite() {
868
+ return true;
869
+ }
870
+ skipProjectConfig() {
871
+ return true;
872
+ }
873
+ };
874
+
875
+ // src/infrastructure/adapters/registry.ts
876
+ var ADAPTERS = [
877
+ // Meta-frameworks first — most specific match wins.
878
+ new NextJsAdapter(),
879
+ new NuxtAdapter(),
880
+ new SvelteKitAdapter(),
881
+ new SolidStartAdapter(),
882
+ new ReactRouterAdapter(),
883
+ new GatsbyAdapter(),
884
+ new QwikAdapter(),
885
+ new AstroAdapter(),
886
+ new VikeAdapter(),
887
+ // Core UI libraries.
888
+ new VueAdapter(),
889
+ new SvelteAdapter(),
890
+ new SolidAdapter(),
891
+ new PreactAdapter(),
892
+ new LitAdapter(),
893
+ new ViteReactAdapter(),
894
+ // Catch-all.
895
+ new GenericAdapter()
896
+ ];
897
+ function selectAdapter(projectRoot) {
898
+ for (const adapter of ADAPTERS) {
899
+ if (adapter.detect(projectRoot)) return adapter;
900
+ }
901
+ return new GenericAdapter();
902
+ }
903
+
904
+ // src/infrastructure/vite-bundler.ts
905
+ var STUBS_DIR = join18(dirname2(fileURLToPath(import.meta.url)), "stubs");
297
906
  var ViteBundler = class {
298
907
  servers = /* @__PURE__ */ new Map();
299
908
  detector = new ProjectDetector();
300
909
  async getUrl(filePath, options = {}) {
301
910
  const absPath = resolve2(filePath);
302
- const project = options.projectRoot ? this.detector.detect(join2(options.projectRoot, "package.json")) : this.detector.detect(absPath);
303
- if (!project.hasVite) {
911
+ const project = options.projectRoot ? this.detector.detect(join18(options.projectRoot, "package.json")) : this.detector.detect(absPath);
912
+ const adapter = selectAdapter(project.root);
913
+ if (!project.hasVite && !adapter.useFrameshotVite()) {
304
914
  throw new Error(
305
915
  `Vite not found in ${project.root}. Install vite: npm install -D vite`
306
916
  );
@@ -312,7 +922,7 @@ var ViteBundler = class {
312
922
  options.props,
313
923
  project.root
314
924
  );
315
- const cached = await this.ensureServer(project);
925
+ const cached = await this.ensureServer(project, adapter);
316
926
  cached.currentEntry = entryCode;
317
927
  const mod = cached.server.moduleGraph.getModuleById(
318
928
  "\0virtual:frameshot-entry"
@@ -332,17 +942,94 @@ var ViteBundler = class {
332
942
  }
333
943
  this.servers.clear();
334
944
  }
335
- async ensureServer(project) {
945
+ // Public for testing
946
+ generateEntry(componentPath, framework, props, projectRoot) {
947
+ const propsJson = props ? JSON.stringify(props) : "{}";
948
+ const cssImport = projectRoot ? this.findGlobalCss(projectRoot) : "";
949
+ switch (framework) {
950
+ case "react": {
951
+ const hasRouter = projectRoot && this.hasPackage(projectRoot, "react-router-dom");
952
+ const routerImport = hasRouter ? `import { MemoryRouter } from "react-router-dom";` : "";
953
+ const wrap = hasRouter ? (inner) => `createElement(MemoryRouter, null, ${inner})` : (inner) => inner;
954
+ return `${cssImport}
955
+ import { createElement, Suspense } from "react";
956
+ import { createRoot } from "react-dom/client";
957
+ ${routerImport}
958
+ import Component from "${componentPath}";
959
+ const props = ${propsJson};
960
+ const root = createRoot(document.getElementById("app"));
961
+ root.render(${wrap(`createElement(Suspense, { fallback: createElement("div", null, "Loading...") }, createElement(Component, props))`)});
962
+ `;
963
+ }
964
+ case "vue":
965
+ return `${cssImport}
966
+ import { createApp } from "vue";
967
+ import Component from "${componentPath}";
968
+ const props = ${propsJson};
969
+ createApp(Component, props).mount("#app");
970
+ `;
971
+ case "svelte":
972
+ return `${cssImport}
973
+ import Component from "${componentPath}";
974
+ const props = ${propsJson};
975
+ const target = document.getElementById("app");
976
+ // Svelte 5 uses mount(); Svelte 4 uses new Component(). Try v5 first.
977
+ try {
978
+ const svelte = await import("svelte");
979
+ if (typeof svelte.mount === "function") {
980
+ svelte.mount(Component, { target, props });
981
+ } else {
982
+ new Component({ target, props });
983
+ }
984
+ } catch {
985
+ new Component({ target, props });
986
+ }
987
+ `;
988
+ case "solid":
989
+ return `${cssImport}
990
+ import { render } from "solid-js/web";
991
+ import Component from "${componentPath}";
992
+ const props = ${propsJson};
993
+ render(() => Component(props), document.getElementById("app"));
994
+ `;
995
+ case "preact":
996
+ return `${cssImport}
997
+ import { h, render } from "preact";
998
+ import Component from "${componentPath}";
999
+ const props = ${propsJson};
1000
+ render(h(Component, props), document.getElementById("app"));
1001
+ `;
1002
+ default:
1003
+ return `${cssImport}
1004
+ const res = await fetch("${componentPath}");
1005
+ const html = await res.text();
1006
+ document.getElementById("app").innerHTML = html;
1007
+ `;
1008
+ }
1009
+ }
1010
+ async ensureServer(project, adapter) {
336
1011
  const existing = this.servers.get(project.root);
337
1012
  if (existing) return existing;
338
- const vite = await this.importVite(project.root);
1013
+ const vite = adapter.useFrameshotVite() ? await this.importFrameshotVite() : await this.importVite(project.root);
339
1014
  if (!vite) {
340
- throw new Error(`Failed to import vite from ${project.root}`);
1015
+ throw new Error(`Failed to import vite for ${project.root}`);
1016
+ }
1017
+ const adapterPlugins = await adapter.getPlugins(project.root);
1018
+ const adapterAliases = adapter.getAliases(project.root, STUBS_DIR);
1019
+ const aliasEntries = [];
1020
+ for (const [key, value] of Object.entries(project.pathAliases)) {
1021
+ aliasEntries.push({
1022
+ find: new RegExp(`^${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/`),
1023
+ replacement: `${value}/`
1024
+ });
1025
+ }
1026
+ for (const [key, value] of Object.entries(adapterAliases)) {
1027
+ aliasEntries.push({ find: key, replacement: value });
341
1028
  }
342
1029
  const self = this;
343
1030
  const server = await vite.createServer({
344
1031
  root: project.root,
345
- configFile: project.viteConfigPath ?? false,
1032
+ configFile: adapter.skipProjectConfig() ? false : project.viteConfigPath ?? false,
346
1033
  server: {
347
1034
  port: 0,
348
1035
  strictPort: false,
@@ -350,6 +1037,10 @@ var ViteBundler = class {
350
1037
  hmr: false
351
1038
  },
352
1039
  logLevel: "error",
1040
+ resolve: aliasEntries.length > 0 ? {
1041
+ alias: aliasEntries,
1042
+ dedupe: ["react", "react-dom", "vue", "svelte"]
1043
+ } : void 0,
353
1044
  plugins: [
354
1045
  {
355
1046
  name: "frameshot-virtual",
@@ -396,10 +1087,11 @@ var ViteBundler = class {
396
1087
  }
397
1088
  });
398
1089
  }
399
- }
1090
+ },
1091
+ ...adapterPlugins
400
1092
  ],
401
1093
  optimizeDeps: {
402
- include: this.getOptimizeDepsInclude(project.framework)
1094
+ include: adapter.getOptimizeDeps(project.root)
403
1095
  }
404
1096
  });
405
1097
  await server.listen();
@@ -408,15 +1100,16 @@ var ViteBundler = class {
408
1100
  const cached = {
409
1101
  server,
410
1102
  port,
411
- currentEntry: ""
1103
+ currentEntry: "",
1104
+ adapter
412
1105
  };
413
1106
  this.servers.set(project.root, cached);
414
1107
  return cached;
415
1108
  }
416
- async importVite(projectRoot) {
1109
+ async importFrameshotVite() {
417
1110
  try {
418
- const require2 = createRequire(join2(projectRoot, "package.json"));
419
- const vitePath = require2.resolve("vite");
1111
+ const requireFromHere = createRequire(import.meta.url);
1112
+ const vitePath = requireFromHere.resolve("vite");
420
1113
  const mod = await import(vitePath);
421
1114
  if (mod && typeof mod === "object" && "createServer" in mod) {
422
1115
  return mod;
@@ -432,54 +1125,27 @@ var ViteBundler = class {
432
1125
  }
433
1126
  return null;
434
1127
  }
1128
+ async importVite(projectRoot) {
1129
+ try {
1130
+ const require2 = createRequire(join18(projectRoot, "package.json"));
1131
+ const vitePath = require2.resolve("vite");
1132
+ const mod = await import(vitePath);
1133
+ if (mod && typeof mod === "object" && "createServer" in mod) {
1134
+ return mod;
1135
+ }
1136
+ } catch {
1137
+ }
1138
+ return this.importFrameshotVite();
1139
+ }
435
1140
  hasPackage(projectRoot, name) {
436
1141
  try {
437
- const require2 = createRequire(join2(projectRoot, "package.json"));
1142
+ const require2 = createRequire(join18(projectRoot, "package.json"));
438
1143
  require2.resolve(name);
439
1144
  return true;
440
1145
  } catch {
441
1146
  return false;
442
1147
  }
443
1148
  }
444
- generateEntry(componentPath, framework, props, projectRoot) {
445
- const propsJson = props ? JSON.stringify(props) : "{}";
446
- const cssImport = projectRoot ? this.findGlobalCss(projectRoot) : "";
447
- switch (framework) {
448
- case "react": {
449
- const hasRouter = projectRoot && this.hasPackage(projectRoot, "react-router-dom");
450
- const routerImport = hasRouter ? `import { MemoryRouter } from "react-router-dom";` : "";
451
- const wrap = hasRouter ? (inner) => `createElement(MemoryRouter, null, ${inner})` : (inner) => inner;
452
- return `${cssImport}
453
- import { createElement, Suspense } from "react";
454
- import { createRoot } from "react-dom/client";
455
- ${routerImport}
456
- import Component from "${componentPath}";
457
- const props = ${propsJson};
458
- const root = createRoot(document.getElementById("app"));
459
- root.render(${wrap(`createElement(Suspense, { fallback: createElement("div", null, "Loading...") }, createElement(Component, props))`)});
460
- `;
461
- }
462
- case "vue":
463
- return `${cssImport}
464
- import { createApp } from "vue";
465
- import Component from "${componentPath}";
466
- const props = ${propsJson};
467
- createApp(Component, props).mount("#app");
468
- `;
469
- case "svelte":
470
- return `${cssImport}
471
- import Component from "${componentPath}";
472
- const props = ${propsJson};
473
- new Component({ target: document.getElementById("app"), props });
474
- `;
475
- default:
476
- return `${cssImport}
477
- const res = await fetch("${componentPath}");
478
- const html = await res.text();
479
- document.getElementById("app").innerHTML = html;
480
- `;
481
- }
482
- }
483
1149
  findGlobalCss(projectRoot) {
484
1150
  const candidates = [
485
1151
  "src/index.css",
@@ -491,25 +1157,13 @@ document.getElementById("app").innerHTML = html;
491
1157
  "app/globals.css"
492
1158
  ];
493
1159
  for (const candidate of candidates) {
494
- const fullPath = join2(projectRoot, candidate);
495
- if (existsSync2(fullPath)) {
1160
+ const fullPath = join18(projectRoot, candidate);
1161
+ if (existsSync4(fullPath)) {
496
1162
  return `import "${fullPath}";`;
497
1163
  }
498
1164
  }
499
1165
  return "";
500
1166
  }
501
- getOptimizeDepsInclude(framework) {
502
- switch (framework) {
503
- case "react":
504
- return ["react", "react-dom/client"];
505
- case "vue":
506
- return ["vue"];
507
- case "svelte":
508
- return [];
509
- default:
510
- return [];
511
- }
512
- }
513
1167
  };
514
1168
 
515
1169
  // src/use-cases/audit.ts
@@ -591,34 +1245,9 @@ var AuditUseCase = class {
591
1245
  }
592
1246
  };
593
1247
 
594
- // src/domain/types.ts
595
- var DEFAULT_RENDER_OPTIONS = {
596
- viewport: { width: 1280, height: 800 },
597
- engines: ["chromium"],
598
- fullPage: true,
599
- darkMode: false,
600
- css: "",
601
- tailwindVersion: "3",
602
- waitFor: 0,
603
- autoFit: false
604
- };
605
- var DEVICE_PRESETS = {
606
- mobile: { width: 375, height: 667 },
607
- tablet: { width: 768, height: 1024 },
608
- desktop: { width: 1280, height: 800 }
609
- };
610
- var EXT_TO_FRAMEWORK = {
611
- ".jsx": "react",
612
- ".tsx": "react",
613
- ".vue": "vue",
614
- ".svelte": "svelte",
615
- ".html": "html",
616
- ".htm": "html"
617
- };
618
-
619
1248
  // src/use-cases/catalog.ts
620
1249
  import { readdirSync, statSync } from "fs";
621
- import { extname, join as join3, relative } from "path";
1250
+ import { extname, join as join19, relative } from "path";
622
1251
  var CatalogUseCase = class {
623
1252
  constructor(renderUseCase) {
624
1253
  this.renderUseCase = renderUseCase;
@@ -676,7 +1305,7 @@ var CatalogUseCase = class {
676
1305
  const entries = readdirSync(dir);
677
1306
  for (const entry of entries) {
678
1307
  if (entry.startsWith(".") || entry === "node_modules") continue;
679
- const fullPath = join3(dir, entry);
1308
+ const fullPath = join19(dir, entry);
680
1309
  const stat = statSync(fullPath);
681
1310
  if (stat.isDirectory() && recursive) {
682
1311
  files.push(...this.scanDirectory(fullPath, recursive));
@@ -740,10 +1369,11 @@ var DiffUseCase = class {
740
1369
  };
741
1370
 
742
1371
  // src/use-cases/render.ts
743
- import { readFileSync as readFileSync2 } from "fs";
1372
+ import { readFileSync as readFileSync3 } from "fs";
744
1373
  import { extname as extname2 } from "path";
745
1374
 
746
1375
  // src/infrastructure/page-utils.ts
1376
+ import picomatch from "picomatch";
747
1377
  function attachConsoleCapture(page) {
748
1378
  const errors = [];
749
1379
  const onMessage = (msg) => {
@@ -760,6 +1390,46 @@ function attachConsoleCapture(page) {
760
1390
  }
761
1391
  };
762
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
+ }
763
1433
  async function takeScreenshot(page, engine, fullPage, errors, cleanup) {
764
1434
  const screenshot = await page.screenshot({ type: "png", fullPage });
765
1435
  const metrics = await page.evaluate(() => ({
@@ -825,7 +1495,7 @@ var RenderUseCase = class {
825
1495
  );
826
1496
  }
827
1497
  }
828
- const code = readFileSync2(filePath, "utf-8");
1498
+ const code = readFileSync3(filePath, "utf-8");
829
1499
  const results = await this.render(code, framework, options);
830
1500
  return { results, mode: "cdn" };
831
1501
  }
@@ -944,6 +1614,7 @@ var RenderUseCase = class {
944
1614
  const page = await this.pool.getPage(engine);
945
1615
  await page.setViewportSize(options.viewport);
946
1616
  const { errors, cleanup } = attachConsoleCapture(page);
1617
+ if (options.mock) await attachMocks(page, options.mock);
947
1618
  await page.setContent(html, { waitUntil: "load", timeout: 1e4 });
948
1619
  if (options.waitFor > 0) await page.waitForTimeout(options.waitFor);
949
1620
  const result = await takeScreenshot(
@@ -959,9 +1630,27 @@ var RenderUseCase = class {
959
1630
  async renderUrl(engine, url, options) {
960
1631
  const page = await this.pool.getPage(engine);
961
1632
  const { errors, cleanup } = attachConsoleCapture(page);
1633
+ if (options.mock) await attachMocks(page, options.mock);
962
1634
  const viewport = options.autoFit ? await this.resolveAutoFitViewport(page, url, options) : options.viewport;
963
1635
  await page.setViewportSize(viewport);
964
1636
  await this.navigateAndWait(page, url, options.waitFor);
1637
+ const overlayState = await page.evaluate(() => {
1638
+ const overlay = document.querySelector("vite-error-overlay");
1639
+ const app = document.getElementById("app");
1640
+ const hasContent = !!app?.firstElementChild;
1641
+ if (!overlay) return { overlay: null, hasContent };
1642
+ const shadow = overlay.shadowRoot;
1643
+ const messageEl = shadow?.querySelector(".message-body");
1644
+ return {
1645
+ overlay: messageEl?.textContent?.trim() ?? "Vite error overlay",
1646
+ hasContent
1647
+ };
1648
+ }).catch(() => ({ overlay: null, hasContent: false }));
1649
+ if (overlayState.overlay && !overlayState.hasContent) {
1650
+ cleanup();
1651
+ this.pool.releasePage(engine, page);
1652
+ throw new Error(`Vite compilation error: ${overlayState.overlay}`);
1653
+ }
965
1654
  const result = await takeScreenshot(
966
1655
  page,
967
1656
  engine,
@@ -1106,6 +1795,8 @@ var SnapshotUseCase = class {
1106
1795
 
1107
1796
  export {
1108
1797
  __export,
1798
+ DEVICE_PRESETS,
1799
+ EXT_TO_FRAMEWORK,
1109
1800
  BrowserPool,
1110
1801
  HtmlBuilder,
1111
1802
  ImageComparator,
@@ -1113,8 +1804,6 @@ export {
1113
1804
  ProjectDetector,
1114
1805
  ViteBundler,
1115
1806
  AuditUseCase,
1116
- DEVICE_PRESETS,
1117
- EXT_TO_FRAMEWORK,
1118
1807
  CatalogUseCase,
1119
1808
  DiffUseCase,
1120
1809
  RenderUseCase,