frameshot-mcp 0.3.0 → 0.7.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.
@@ -0,0 +1,811 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ // src/domain/types.ts
8
+ var DEVICE_PRESETS = {
9
+ mobile: { width: 375, height: 667 },
10
+ tablet: { width: 768, height: 1024 },
11
+ desktop: { width: 1280, height: 800 }
12
+ };
13
+ var EXT_TO_FRAMEWORK = {
14
+ ".jsx": "react",
15
+ ".tsx": "react",
16
+ ".vue": "vue",
17
+ ".svelte": "svelte",
18
+ ".html": "html",
19
+ ".htm": "html"
20
+ };
21
+
22
+ // src/infrastructure/browser-pool.ts
23
+ import { chromium, firefox, webkit } from "playwright";
24
+ var BrowserPool = class {
25
+ pool = /* @__PURE__ */ new Map();
26
+ async warmup(engines) {
27
+ await Promise.all(engines.map((e) => this.getSlot(e)));
28
+ }
29
+ async getPage(engine) {
30
+ const slot = await this.getSlot(engine);
31
+ return slot.page;
32
+ }
33
+ async setViewport(engine, viewport) {
34
+ const slot = await this.getSlot(engine);
35
+ const current = slot.page.viewportSize();
36
+ if (current?.width !== viewport.width || current?.height !== viewport.height) {
37
+ await slot.page.setViewportSize(viewport);
38
+ }
39
+ }
40
+ async shutdown() {
41
+ for (const slot of this.pool.values()) {
42
+ await slot.browser.close().catch(() => {
43
+ });
44
+ }
45
+ this.pool.clear();
46
+ }
47
+ async getSlot(engine) {
48
+ const existing = this.pool.get(engine);
49
+ if (existing?.browser.isConnected() && existing.ready) {
50
+ return existing;
51
+ }
52
+ const launcher = { chromium, firefox, webkit }[engine];
53
+ let browser;
54
+ try {
55
+ browser = await launcher.launch({
56
+ headless: true,
57
+ ...engine === "chromium" ? { channel: "chrome" } : {}
58
+ });
59
+ } catch (_e) {
60
+ throw new Error(
61
+ `${engine} is not installed. Run: npx playwright install ${engine}`
62
+ );
63
+ }
64
+ const context = await browser.newContext({
65
+ viewport: { width: 1280, height: 800 },
66
+ deviceScaleFactor: 2
67
+ });
68
+ const page = await context.newPage();
69
+ await page.setContent(
70
+ '<html><head><script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio"></script></head><body></body></html>',
71
+ { waitUntil: "networkidle" }
72
+ );
73
+ const slot = { browser, page, ready: true };
74
+ this.pool.set(engine, slot);
75
+ return slot;
76
+ }
77
+ };
78
+
79
+ // src/infrastructure/html-builder.ts
80
+ var HtmlBuilder = class {
81
+ build(code, framework, options) {
82
+ const { darkMode, css, tailwindVersion } = options;
83
+ const tailwind = tailwindVersion === "4" ? '<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>' : '<script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio"></script>';
84
+ const tailwindConfig = tailwindVersion === "4" ? "" : "<script>tailwind.config={darkMode:'class'}</script>";
85
+ const customCss = css ? `<style>${css}</style>` : "";
86
+ const baseStyle = `<style>*{margin:0;box-sizing:border-box}body{padding:16px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}</style>`;
87
+ const htmlClass = darkMode ? ' class="dark"' : "";
88
+ if (framework === "html") {
89
+ if (code.includes("<html")) return code;
90
+ return `<!DOCTYPE html><html${htmlClass}><head><meta charset="utf-8">${tailwind}${tailwindConfig}${baseStyle}${customCss}</head><body>${code}</body></html>`;
91
+ }
92
+ if (framework === "react") {
93
+ const cleanedCode = code.replace(/['"]use client['"];?\n?/g, "").replace(/['"]use server['"];?\n?/g, "").replace(/import\s+.*?\s+from\s+['"]next\/image['"];?\n?/g, "").replace(/import\s+.*?\s+from\s+['"]next\/link['"];?\n?/g, "");
94
+ return `<!DOCTYPE html><html${htmlClass}><head><meta charset="utf-8">${tailwind}${tailwindConfig}
95
+ <script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
96
+ <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
97
+ <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
98
+ ${baseStyle}${customCss}</head><body><div id="root"></div>
99
+ <script type="text/babel">
100
+ const Image = (props) => React.createElement('img', {...props, src: props.src?.src || props.src});
101
+ const Link = ({href, children, ...props}) => React.createElement('a', {href, ...props}, children);
102
+ ${cleanedCode}
103
+ const _C = typeof App!=='undefined'?App:typeof Component!=='undefined'?Component:typeof Default!=='undefined'?Default:null;
104
+ if(_C)ReactDOM.createRoot(document.getElementById('root')).render(React.createElement(_C));
105
+ </script></body></html>`;
106
+ }
107
+ if (framework === "vue") {
108
+ return `<!DOCTYPE html><html${htmlClass}><head><meta charset="utf-8">${tailwind}${tailwindConfig}
109
+ <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
110
+ ${baseStyle}${customCss}</head><body><div id="app"></div>
111
+ <script>
112
+ const{createApp,ref,reactive,computed,onMounted,watch,watchEffect}=Vue;
113
+ ${code}
114
+ const _C=typeof App!=='undefined'?App:typeof Component!=='undefined'?Component:null;
115
+ if(_C)createApp(_C).mount('#app');
116
+ </script></body></html>`;
117
+ }
118
+ if (framework === "svelte") {
119
+ return `<!DOCTYPE html><html${htmlClass}><head><meta charset="utf-8">${tailwind}${tailwindConfig}
120
+ <script src="https://unpkg.com/svelte@4/compiler.cjs"></script>
121
+ ${baseStyle}${customCss}</head><body><div id="app"></div>
122
+ <script type="module">
123
+ import "https://unpkg.com/svelte@4/internal/index.mjs";
124
+ ${code}
125
+ const _C=typeof App!=='undefined'?App:typeof Component!=='undefined'?Component:null;
126
+ if(_C)new _C({target:document.getElementById('app')});
127
+ </script></body></html>`;
128
+ }
129
+ return code;
130
+ }
131
+ };
132
+
133
+ // src/infrastructure/image-comparator.ts
134
+ import pixelmatch from "pixelmatch";
135
+ import { PNG } from "pngjs";
136
+ var ImageComparator = class {
137
+ diff(imageA, imageB, threshold = 0.1) {
138
+ const bufA = Buffer.from(imageA, "base64");
139
+ const bufB = Buffer.from(imageB, "base64");
140
+ const pngA = PNG.sync.read(bufA);
141
+ const pngB = PNG.sync.read(bufB);
142
+ const width = Math.max(pngA.width, pngB.width);
143
+ const height = Math.max(pngA.height, pngB.height);
144
+ const normalizedA = new PNG({ width, height });
145
+ const normalizedB = new PNG({ width, height });
146
+ PNG.bitblt(pngA, normalizedA, 0, 0, pngA.width, pngA.height, 0, 0);
147
+ PNG.bitblt(pngB, normalizedB, 0, 0, pngB.width, pngB.height, 0, 0);
148
+ const diffPng = new PNG({ width, height });
149
+ const diffPixels = pixelmatch(
150
+ normalizedA.data,
151
+ normalizedB.data,
152
+ diffPng.data,
153
+ width,
154
+ height,
155
+ { threshold }
156
+ );
157
+ const totalPixels = width * height;
158
+ return {
159
+ diff: PNG.sync.write(diffPng).toString("base64"),
160
+ diffPixels,
161
+ totalPixels,
162
+ diffPercentage: Math.round(diffPixels / totalPixels * 1e4) / 100
163
+ };
164
+ }
165
+ composite(images, columns, labelHeight = 30) {
166
+ const pngs = images.map((buf) => PNG.sync.read(buf));
167
+ const cellWidth = Math.max(...pngs.map((p) => p.width));
168
+ const cellHeight = Math.max(...pngs.map((p) => p.height));
169
+ const rows = Math.ceil(pngs.length / columns);
170
+ const gridWidth = cellWidth * columns;
171
+ const gridHeight = (cellHeight + labelHeight) * rows;
172
+ const grid = new PNG({ width: gridWidth, height: gridHeight });
173
+ grid.data.fill(255);
174
+ for (let i = 0; i < pngs.length; i++) {
175
+ const col = i % columns;
176
+ const row = Math.floor(i / columns);
177
+ const x = col * cellWidth;
178
+ const y = row * (cellHeight + labelHeight) + labelHeight;
179
+ PNG.bitblt(pngs[i], grid, 0, 0, pngs[i].width, pngs[i].height, x, y);
180
+ }
181
+ return {
182
+ image: PNG.sync.write(grid).toString("base64"),
183
+ width: gridWidth,
184
+ height: gridHeight
185
+ };
186
+ }
187
+ };
188
+
189
+ // src/infrastructure/project-detector.ts
190
+ import { existsSync, readFileSync } from "fs";
191
+ import { dirname, join, resolve } from "path";
192
+ var VITE_CONFIG_NAMES = [
193
+ "vite.config.ts",
194
+ "vite.config.js",
195
+ "vite.config.mjs",
196
+ "vite.config.mts"
197
+ ];
198
+ var ProjectDetector = class {
199
+ detect(filePath) {
200
+ const root = this.findProjectRoot(filePath);
201
+ const viteConfigPath = this.findViteConfig(root);
202
+ const framework = this.detectFramework(root);
203
+ const hasVite = this.checkViteAvailable(root);
204
+ return { root, viteConfigPath, framework, hasVite };
205
+ }
206
+ findProjectRoot(startPath) {
207
+ let dir = dirname(resolve(startPath));
208
+ while (dir !== dirname(dir)) {
209
+ if (existsSync(join(dir, "package.json"))) {
210
+ return dir;
211
+ }
212
+ dir = dirname(dir);
213
+ }
214
+ return dirname(resolve(startPath));
215
+ }
216
+ findViteConfig(root) {
217
+ for (const name of VITE_CONFIG_NAMES) {
218
+ const configPath = join(root, name);
219
+ if (existsSync(configPath)) {
220
+ return configPath;
221
+ }
222
+ }
223
+ return void 0;
224
+ }
225
+ detectFramework(root) {
226
+ const pkgPath = join(root, "package.json");
227
+ if (!existsSync(pkgPath)) return "html";
228
+ try {
229
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
230
+ const allDeps = {
231
+ ...pkg.dependencies,
232
+ ...pkg.devDependencies
233
+ };
234
+ if (allDeps.react || allDeps["react-dom"]) return "react";
235
+ if (allDeps.vue) return "vue";
236
+ if (allDeps.svelte) return "svelte";
237
+ } catch {
238
+ }
239
+ return "html";
240
+ }
241
+ checkViteAvailable(root) {
242
+ const vitePkgPath = join(root, "node_modules", "vite", "package.json");
243
+ return existsSync(vitePkgPath);
244
+ }
245
+ };
246
+
247
+ // src/infrastructure/vite-bundler.ts
248
+ import { existsSync as existsSync2 } from "fs";
249
+ import { createRequire } from "module";
250
+ import { join as join2, resolve as resolve2 } from "path";
251
+ var ViteBundler = class {
252
+ servers = /* @__PURE__ */ new Map();
253
+ detector = new ProjectDetector();
254
+ async getUrl(filePath, options = {}) {
255
+ const absPath = resolve2(filePath);
256
+ const project = options.projectRoot ? this.detector.detect(join2(options.projectRoot, "package.json")) : this.detector.detect(absPath);
257
+ if (!project.hasVite) {
258
+ throw new Error(
259
+ `Vite not found in ${project.root}. Install vite: npm install -D vite`
260
+ );
261
+ }
262
+ const framework = options.framework ?? project.framework;
263
+ const entryCode = this.generateEntry(
264
+ absPath,
265
+ framework,
266
+ options.props,
267
+ project.root
268
+ );
269
+ const cached = await this.ensureServer(project);
270
+ cached.currentEntry = entryCode;
271
+ cached.lastUsed = Date.now();
272
+ const mod = cached.server.moduleGraph.getModuleById(
273
+ "\0virtual:frameshot-entry"
274
+ );
275
+ if (mod) {
276
+ cached.server.moduleGraph.invalidateModule(mod);
277
+ }
278
+ return {
279
+ url: `http://127.0.0.1:${cached.port}/@frameshot/`,
280
+ project
281
+ };
282
+ }
283
+ async shutdown() {
284
+ for (const cached of this.servers.values()) {
285
+ await cached.server.close().catch(() => {
286
+ });
287
+ }
288
+ this.servers.clear();
289
+ }
290
+ async ensureServer(project) {
291
+ const existing = this.servers.get(project.root);
292
+ if (existing) {
293
+ return existing;
294
+ }
295
+ const vite = await this.importVite(project.root);
296
+ if (!vite) {
297
+ throw new Error(`Failed to import vite from ${project.root}`);
298
+ }
299
+ const self = this;
300
+ const server = await vite.createServer({
301
+ root: project.root,
302
+ configFile: project.viteConfigPath ?? false,
303
+ server: {
304
+ port: 0,
305
+ strictPort: false,
306
+ host: "127.0.0.1",
307
+ hmr: false
308
+ },
309
+ logLevel: "error",
310
+ plugins: [
311
+ {
312
+ name: "frameshot-virtual",
313
+ resolveId(id) {
314
+ if (id === "virtual:frameshot-entry")
315
+ return "\0virtual:frameshot-entry";
316
+ return null;
317
+ },
318
+ load(id) {
319
+ if (id === "\0virtual:frameshot-entry") {
320
+ const cached2 = self.servers.get(project.root);
321
+ return cached2?.currentEntry ?? "";
322
+ }
323
+ return null;
324
+ }
325
+ },
326
+ {
327
+ name: "frameshot-html",
328
+ // biome-ignore lint/suspicious/noExplicitAny: Vite plugin API types unavailable
329
+ configureServer(server2) {
330
+ server2.middlewares.use((req, res, next) => {
331
+ if (req.url === "/@frameshot/" || req.url === "/@frameshot") {
332
+ const html = `<!DOCTYPE html>
333
+ <html>
334
+ <head><meta charset="utf-8"></head>
335
+ <body>
336
+ <div id="app"></div>
337
+ <script type="module">import "virtual:frameshot-entry";</script>
338
+ </body>
339
+ </html>`;
340
+ server2.transformIndexHtml(req.url, html).then((transformed) => {
341
+ res.statusCode = 200;
342
+ res.setHeader("Content-Type", "text/html");
343
+ res.end(transformed);
344
+ }).catch(next);
345
+ } else {
346
+ next();
347
+ }
348
+ });
349
+ }
350
+ }
351
+ ],
352
+ optimizeDeps: {
353
+ include: this.getOptimizeDepsInclude(project.framework)
354
+ }
355
+ });
356
+ await server.listen();
357
+ const address = server.httpServer?.address();
358
+ const port = typeof address === "object" && address ? address.port : 5173;
359
+ const cached = {
360
+ server,
361
+ port,
362
+ lastUsed: Date.now(),
363
+ currentEntry: ""
364
+ };
365
+ this.servers.set(project.root, cached);
366
+ return cached;
367
+ }
368
+ // biome-ignore lint/suspicious/noExplicitAny: dynamic import of optional peer dep
369
+ async importVite(projectRoot) {
370
+ try {
371
+ const require2 = createRequire(join2(projectRoot, "package.json"));
372
+ const vitePath = require2.resolve("vite");
373
+ return await import(vitePath);
374
+ } catch {
375
+ try {
376
+ return await import("vite");
377
+ } catch {
378
+ return null;
379
+ }
380
+ }
381
+ }
382
+ generateEntry(componentPath, framework, props, projectRoot) {
383
+ const propsJson = props ? JSON.stringify(props) : "{}";
384
+ const cssImport = projectRoot ? this.findGlobalCss(projectRoot) : "";
385
+ switch (framework) {
386
+ case "react":
387
+ return `${cssImport}
388
+ import { createElement } from "react";
389
+ import { createRoot } from "react-dom/client";
390
+ import Component from "${componentPath}";
391
+ const props = ${propsJson};
392
+ const root = createRoot(document.getElementById("app"));
393
+ root.render(createElement(Component, props));
394
+ `;
395
+ case "vue":
396
+ return `${cssImport}
397
+ import { createApp } from "vue";
398
+ import Component from "${componentPath}";
399
+ const props = ${propsJson};
400
+ createApp(Component, props).mount("#app");
401
+ `;
402
+ case "svelte":
403
+ return `${cssImport}
404
+ import Component from "${componentPath}";
405
+ const props = ${propsJson};
406
+ new Component({ target: document.getElementById("app"), props });
407
+ `;
408
+ default:
409
+ return `${cssImport}
410
+ const res = await fetch("${componentPath}");
411
+ const html = await res.text();
412
+ document.getElementById("app").innerHTML = html;
413
+ `;
414
+ }
415
+ }
416
+ findGlobalCss(projectRoot) {
417
+ const candidates = [
418
+ "src/index.css",
419
+ "src/main.css",
420
+ "src/globals.css",
421
+ "src/app.css",
422
+ "src/style.css",
423
+ "src/styles.css",
424
+ "app/globals.css"
425
+ ];
426
+ for (const candidate of candidates) {
427
+ const fullPath = join2(projectRoot, candidate);
428
+ if (existsSync2(fullPath)) {
429
+ return `import "${fullPath}";`;
430
+ }
431
+ }
432
+ return "";
433
+ }
434
+ getOptimizeDepsInclude(framework) {
435
+ switch (framework) {
436
+ case "react":
437
+ return ["react", "react-dom/client"];
438
+ case "vue":
439
+ return ["vue"];
440
+ case "svelte":
441
+ return [];
442
+ default:
443
+ return [];
444
+ }
445
+ }
446
+ };
447
+
448
+ // src/use-cases/catalog.ts
449
+ import { readdirSync, statSync } from "fs";
450
+ import { extname, join as join3, relative } from "path";
451
+ var CatalogUseCase = class {
452
+ constructor(renderUseCase) {
453
+ this.renderUseCase = renderUseCase;
454
+ }
455
+ renderUseCase;
456
+ async renderCatalog(directory, options = {}) {
457
+ const { recursive = false, ...renderOptions } = options;
458
+ const files = this.scanDirectory(directory, recursive);
459
+ const results = [];
460
+ for (const filePath of files) {
461
+ const relativePath = relative(directory, filePath);
462
+ const framework = this.detectFramework(filePath);
463
+ try {
464
+ const { results: renderResults } = await this.renderUseCase.renderFile(
465
+ filePath,
466
+ {
467
+ ...renderOptions,
468
+ engines: ["chromium"]
469
+ }
470
+ );
471
+ const result = renderResults[0];
472
+ results.push({
473
+ path: relativePath,
474
+ framework,
475
+ image: result.image,
476
+ width: result.width,
477
+ height: result.height,
478
+ consoleErrors: result.consoleErrors
479
+ });
480
+ } catch {
481
+ results.push({
482
+ path: relativePath,
483
+ framework,
484
+ image: "",
485
+ width: 0,
486
+ height: 0,
487
+ consoleErrors: [`Failed to render ${relativePath}`]
488
+ });
489
+ }
490
+ }
491
+ return results;
492
+ }
493
+ scanDirectory(dir, recursive) {
494
+ const componentExtensions = /* @__PURE__ */ new Set([
495
+ ".jsx",
496
+ ".tsx",
497
+ ".vue",
498
+ ".svelte",
499
+ ".html",
500
+ ".htm"
501
+ ]);
502
+ const files = [];
503
+ const entries = readdirSync(dir);
504
+ for (const entry of entries) {
505
+ if (entry.startsWith(".") || entry === "node_modules") continue;
506
+ const fullPath = join3(dir, entry);
507
+ const stat = statSync(fullPath);
508
+ if (stat.isDirectory() && recursive) {
509
+ files.push(...this.scanDirectory(fullPath, recursive));
510
+ } else if (stat.isFile() && componentExtensions.has(extname(entry).toLowerCase())) {
511
+ files.push(fullPath);
512
+ }
513
+ }
514
+ return files;
515
+ }
516
+ detectFramework(filePath) {
517
+ return EXT_TO_FRAMEWORK[extname(filePath).toLowerCase()] ?? "react";
518
+ }
519
+ };
520
+
521
+ // src/use-cases/diff.ts
522
+ var DiffUseCase = class {
523
+ constructor(renderUseCase, imageComparator) {
524
+ this.renderUseCase = renderUseCase;
525
+ this.imageComparator = imageComparator;
526
+ }
527
+ renderUseCase;
528
+ imageComparator;
529
+ async diffComponent(before, after, framework, options = {}) {
530
+ const [beforeResults, afterResults] = await Promise.all([
531
+ this.renderUseCase.render(before, framework, {
532
+ ...options,
533
+ engines: ["chromium"]
534
+ }),
535
+ this.renderUseCase.render(after, framework, {
536
+ ...options,
537
+ engines: ["chromium"]
538
+ })
539
+ ]);
540
+ const comparison = this.imageComparator.diff(
541
+ beforeResults[0].image,
542
+ afterResults[0].image
543
+ );
544
+ return {
545
+ before: beforeResults[0].image,
546
+ after: afterResults[0].image,
547
+ ...comparison
548
+ };
549
+ }
550
+ async diffFromReference(code, framework, referenceImage, options = {}) {
551
+ const { threshold = 0.1, ...renderOptions } = options;
552
+ const [renderResult] = await this.renderUseCase.render(code, framework, {
553
+ ...renderOptions,
554
+ engines: ["chromium"]
555
+ });
556
+ const comparison = this.imageComparator.diff(
557
+ referenceImage,
558
+ renderResult.image,
559
+ threshold
560
+ );
561
+ return {
562
+ rendered: renderResult.image,
563
+ ...comparison,
564
+ passed: comparison.diffPercentage === 0
565
+ };
566
+ }
567
+ };
568
+
569
+ // src/use-cases/render.ts
570
+ import { readFileSync as readFileSync2 } from "fs";
571
+ import { extname as extname2 } from "path";
572
+ var RenderUseCase = class {
573
+ constructor(pool, htmlBuilder, imageComparator, viteBundler) {
574
+ this.pool = pool;
575
+ this.htmlBuilder = htmlBuilder;
576
+ this.imageComparator = imageComparator;
577
+ this.viteBundler = viteBundler;
578
+ }
579
+ pool;
580
+ htmlBuilder;
581
+ imageComparator;
582
+ viteBundler;
583
+ async render(code, framework, options = {}) {
584
+ const opts = this.resolveOptions(options);
585
+ const html = this.htmlBuilder.build(code, framework, {
586
+ darkMode: opts.darkMode,
587
+ css: opts.css,
588
+ tailwindVersion: opts.tailwindVersion
589
+ });
590
+ return Promise.all(
591
+ opts.engines.map((engine) => this.renderHtml(engine, html, opts))
592
+ );
593
+ }
594
+ async renderFile(filePath, options = {}) {
595
+ const opts = this.resolveOptions(options);
596
+ const ext = extname2(filePath).toLowerCase();
597
+ const framework = EXT_TO_FRAMEWORK[ext] ?? "react";
598
+ if (this.viteBundler) {
599
+ try {
600
+ const { url } = await this.viteBundler.getUrl(filePath, {
601
+ props: options.props,
602
+ framework,
603
+ projectRoot: options.projectRoot
604
+ });
605
+ const results2 = await Promise.all(
606
+ opts.engines.map((engine) => this.renderUrl(engine, url, opts))
607
+ );
608
+ return { results: results2, mode: "vite" };
609
+ } catch {
610
+ }
611
+ }
612
+ const code = readFileSync2(filePath, "utf-8");
613
+ const results = await this.render(code, framework, options);
614
+ return { results, mode: "cdn" };
615
+ }
616
+ async renderInteraction(code, framework, interactions, options = {}) {
617
+ const opts = this.resolveOptions(options);
618
+ const html = this.htmlBuilder.build(code, framework, {
619
+ darkMode: opts.darkMode,
620
+ css: opts.css,
621
+ tailwindVersion: opts.tailwindVersion
622
+ });
623
+ const page = await this.pool.getPage("chromium");
624
+ await this.pool.setViewport("chromium", opts.viewport);
625
+ await page.setContent(html, { waitUntil: "load", timeout: 1e4 });
626
+ for (const interaction of interactions) {
627
+ switch (interaction.action) {
628
+ case "click":
629
+ if (interaction.selector) await page.click(interaction.selector);
630
+ break;
631
+ case "hover":
632
+ if (interaction.selector) await page.hover(interaction.selector);
633
+ break;
634
+ case "focus":
635
+ if (interaction.selector) await page.focus(interaction.selector);
636
+ break;
637
+ case "type":
638
+ if (interaction.selector && interaction.value)
639
+ await page.fill(interaction.selector, interaction.value);
640
+ break;
641
+ case "wait":
642
+ await page.waitForTimeout(interaction.ms ?? 300);
643
+ break;
644
+ }
645
+ }
646
+ const screenshot = await page.screenshot({ type: "png", fullPage: true });
647
+ const metrics = await page.evaluate(() => ({
648
+ w: document.documentElement.scrollWidth,
649
+ h: document.documentElement.scrollHeight
650
+ }));
651
+ return {
652
+ image: screenshot.toString("base64"),
653
+ width: metrics.w,
654
+ height: metrics.h
655
+ };
656
+ }
657
+ async captureAnimation(code, framework, options = {}) {
658
+ const { frames = 5, duration = 1e3 } = options;
659
+ const opts = this.resolveOptions(options);
660
+ const html = this.htmlBuilder.build(code, framework, {
661
+ darkMode: opts.darkMode,
662
+ css: opts.css,
663
+ tailwindVersion: opts.tailwindVersion
664
+ });
665
+ const page = await this.pool.getPage("chromium");
666
+ await this.pool.setViewport("chromium", opts.viewport);
667
+ await page.setContent(html, { waitUntil: "load", timeout: 1e4 });
668
+ const interval = duration / (frames - 1);
669
+ const results = [];
670
+ for (let i = 0; i < frames; i++) {
671
+ if (i > 0) await page.waitForTimeout(interval);
672
+ const screenshot = await page.screenshot({
673
+ type: "png",
674
+ fullPage: false
675
+ });
676
+ results.push({
677
+ timestamp: Math.round(i * interval),
678
+ image: screenshot.toString("base64")
679
+ });
680
+ }
681
+ return results;
682
+ }
683
+ async renderGrid(cells, framework, options = {}) {
684
+ const { columns = Math.min(cells.length, 3) } = options;
685
+ const screenshots = await Promise.all(
686
+ cells.map(async (cell) => {
687
+ const [result] = await this.render(cell.code, framework, {
688
+ ...options,
689
+ engines: ["chromium"]
690
+ });
691
+ return Buffer.from(result.image, "base64");
692
+ })
693
+ );
694
+ const composite = this.imageComparator.composite(screenshots, columns);
695
+ return { ...composite, cells: cells.length };
696
+ }
697
+ async renderMatrix(code, framework, viewports, themes, options = {}) {
698
+ const combinations = [];
699
+ for (const viewport of viewports) {
700
+ for (const theme of themes) {
701
+ combinations.push({ viewport, theme });
702
+ }
703
+ }
704
+ return Promise.all(
705
+ combinations.map(async ({ viewport, theme }) => {
706
+ const [result] = await this.render(code, framework, {
707
+ ...options,
708
+ viewport: { width: viewport.width, height: viewport.height },
709
+ darkMode: theme === "dark",
710
+ engines: ["chromium"],
711
+ fullPage: true
712
+ });
713
+ return {
714
+ viewport: viewport.label,
715
+ theme,
716
+ image: result.image,
717
+ width: result.width,
718
+ height: result.height,
719
+ consoleErrors: result.consoleErrors
720
+ };
721
+ })
722
+ );
723
+ }
724
+ async renderHtml(engine, html, options) {
725
+ const page = await this.pool.getPage(engine);
726
+ await this.pool.setViewport(engine, options.viewport);
727
+ const consoleErrors = [];
728
+ const onError = (msg) => {
729
+ if (msg.type() === "error") consoleErrors.push(msg.text());
730
+ };
731
+ page.on("console", onError);
732
+ page.on("pageerror", (err) => consoleErrors.push(err.message));
733
+ await page.setContent(html, { waitUntil: "load", timeout: 1e4 });
734
+ if (options.waitFor > 0) {
735
+ await page.waitForTimeout(options.waitFor);
736
+ }
737
+ const screenshot = await page.screenshot({
738
+ type: "png",
739
+ fullPage: options.fullPage
740
+ });
741
+ const metrics = await page.evaluate(() => ({
742
+ w: document.documentElement.scrollWidth,
743
+ h: document.documentElement.scrollHeight
744
+ }));
745
+ page.removeListener("console", onError);
746
+ return {
747
+ engine,
748
+ image: screenshot.toString("base64"),
749
+ width: metrics.w,
750
+ height: metrics.h,
751
+ consoleErrors
752
+ };
753
+ }
754
+ async renderUrl(engine, url, options) {
755
+ const page = await this.pool.getPage(engine);
756
+ await this.pool.setViewport(engine, options.viewport);
757
+ const consoleErrors = [];
758
+ const onError = (msg) => {
759
+ if (msg.type() === "error") consoleErrors.push(msg.text());
760
+ };
761
+ page.on("console", onError);
762
+ page.on("pageerror", (err) => consoleErrors.push(err.message));
763
+ await page.goto(url, { waitUntil: "load", timeout: 15e3 });
764
+ await page.waitForLoadState("networkidle", { timeout: 5e3 }).catch(() => {
765
+ });
766
+ if (options.waitFor > 0) {
767
+ await page.waitForTimeout(options.waitFor);
768
+ }
769
+ const screenshot = await page.screenshot({
770
+ type: "png",
771
+ fullPage: options.fullPage
772
+ });
773
+ const metrics = await page.evaluate(() => ({
774
+ w: document.documentElement.scrollWidth,
775
+ h: document.documentElement.scrollHeight
776
+ }));
777
+ page.removeListener("console", onError);
778
+ return {
779
+ engine,
780
+ image: screenshot.toString("base64"),
781
+ width: metrics.w,
782
+ height: metrics.h,
783
+ consoleErrors
784
+ };
785
+ }
786
+ resolveOptions(partial) {
787
+ return {
788
+ viewport: partial.viewport ?? { width: 1280, height: 800 },
789
+ engines: partial.engines ?? ["chromium"],
790
+ fullPage: partial.fullPage ?? true,
791
+ darkMode: partial.darkMode ?? false,
792
+ css: partial.css ?? "",
793
+ tailwindVersion: partial.tailwindVersion ?? "3",
794
+ waitFor: partial.waitFor ?? 0
795
+ };
796
+ }
797
+ };
798
+
799
+ export {
800
+ __export,
801
+ DEVICE_PRESETS,
802
+ EXT_TO_FRAMEWORK,
803
+ BrowserPool,
804
+ HtmlBuilder,
805
+ ImageComparator,
806
+ ProjectDetector,
807
+ ViteBundler,
808
+ CatalogUseCase,
809
+ DiffUseCase,
810
+ RenderUseCase
811
+ };