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,786 @@
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 { createRequire } from "module";
249
+ import { join as join2, resolve as resolve2 } from "path";
250
+ var ViteBundler = class {
251
+ servers = /* @__PURE__ */ new Map();
252
+ detector = new ProjectDetector();
253
+ async getUrl(filePath, options = {}) {
254
+ const absPath = resolve2(filePath);
255
+ const project = options.projectRoot ? this.detector.detect(join2(options.projectRoot, "package.json")) : this.detector.detect(absPath);
256
+ if (!project.hasVite) {
257
+ throw new Error(
258
+ `Vite not found in ${project.root}. Install vite: npm install -D vite`
259
+ );
260
+ }
261
+ const framework = options.framework ?? project.framework;
262
+ const entryCode = this.generateEntry(absPath, framework, options.props);
263
+ const cached = await this.ensureServer(project);
264
+ cached.currentEntry = entryCode;
265
+ cached.lastUsed = Date.now();
266
+ const mod = cached.server.moduleGraph.getModuleById(
267
+ "\0virtual:frameshot-entry"
268
+ );
269
+ if (mod) {
270
+ cached.server.moduleGraph.invalidateModule(mod);
271
+ }
272
+ return {
273
+ url: `http://127.0.0.1:${cached.port}/@frameshot/`,
274
+ project
275
+ };
276
+ }
277
+ async shutdown() {
278
+ for (const cached of this.servers.values()) {
279
+ await cached.server.close().catch(() => {
280
+ });
281
+ }
282
+ this.servers.clear();
283
+ }
284
+ async ensureServer(project) {
285
+ const existing = this.servers.get(project.root);
286
+ if (existing) {
287
+ return existing;
288
+ }
289
+ const vite = await this.importVite(project.root);
290
+ if (!vite) {
291
+ throw new Error(`Failed to import vite from ${project.root}`);
292
+ }
293
+ const self = this;
294
+ const server = await vite.createServer({
295
+ root: project.root,
296
+ configFile: project.viteConfigPath ?? false,
297
+ server: {
298
+ port: 0,
299
+ strictPort: false,
300
+ host: "127.0.0.1",
301
+ hmr: false
302
+ },
303
+ logLevel: "error",
304
+ plugins: [
305
+ {
306
+ name: "frameshot-virtual",
307
+ resolveId(id) {
308
+ if (id === "virtual:frameshot-entry")
309
+ return "\0virtual:frameshot-entry";
310
+ return null;
311
+ },
312
+ load(id) {
313
+ if (id === "\0virtual:frameshot-entry") {
314
+ const cached2 = self.servers.get(project.root);
315
+ return cached2?.currentEntry ?? "";
316
+ }
317
+ return null;
318
+ }
319
+ },
320
+ {
321
+ name: "frameshot-html",
322
+ // biome-ignore lint/suspicious/noExplicitAny: Vite plugin API types unavailable
323
+ configureServer(server2) {
324
+ server2.middlewares.use((req, res, next) => {
325
+ if (req.url === "/@frameshot/" || req.url === "/@frameshot") {
326
+ const html = `<!DOCTYPE html>
327
+ <html>
328
+ <head><meta charset="utf-8"></head>
329
+ <body>
330
+ <div id="app"></div>
331
+ <script type="module">import "virtual:frameshot-entry";</script>
332
+ </body>
333
+ </html>`;
334
+ server2.transformIndexHtml(req.url, html).then((transformed) => {
335
+ res.statusCode = 200;
336
+ res.setHeader("Content-Type", "text/html");
337
+ res.end(transformed);
338
+ }).catch(next);
339
+ } else {
340
+ next();
341
+ }
342
+ });
343
+ }
344
+ }
345
+ ],
346
+ optimizeDeps: {
347
+ include: this.getOptimizeDepsInclude(project.framework)
348
+ }
349
+ });
350
+ await server.listen();
351
+ const address = server.httpServer?.address();
352
+ const port = typeof address === "object" && address ? address.port : 5173;
353
+ const cached = {
354
+ server,
355
+ port,
356
+ lastUsed: Date.now(),
357
+ currentEntry: ""
358
+ };
359
+ this.servers.set(project.root, cached);
360
+ return cached;
361
+ }
362
+ // biome-ignore lint/suspicious/noExplicitAny: dynamic import of optional peer dep
363
+ async importVite(projectRoot) {
364
+ try {
365
+ const require2 = createRequire(join2(projectRoot, "package.json"));
366
+ const vitePath = require2.resolve("vite");
367
+ return await import(vitePath);
368
+ } catch {
369
+ try {
370
+ return await import("vite");
371
+ } catch {
372
+ return null;
373
+ }
374
+ }
375
+ }
376
+ generateEntry(componentPath, framework, props) {
377
+ const propsJson = props ? JSON.stringify(props) : "{}";
378
+ switch (framework) {
379
+ case "react":
380
+ return `
381
+ import { createElement } from "react";
382
+ import { createRoot } from "react-dom/client";
383
+ import Component from "${componentPath}";
384
+ const props = ${propsJson};
385
+ const root = createRoot(document.getElementById("app"));
386
+ root.render(createElement(Component, props));
387
+ `;
388
+ case "vue":
389
+ return `
390
+ import { createApp } from "vue";
391
+ import Component from "${componentPath}";
392
+ const props = ${propsJson};
393
+ createApp(Component, props).mount("#app");
394
+ `;
395
+ case "svelte":
396
+ return `
397
+ import Component from "${componentPath}";
398
+ const props = ${propsJson};
399
+ new Component({ target: document.getElementById("app"), props });
400
+ `;
401
+ default:
402
+ return `
403
+ const res = await fetch("${componentPath}");
404
+ const html = await res.text();
405
+ document.getElementById("app").innerHTML = html;
406
+ `;
407
+ }
408
+ }
409
+ getOptimizeDepsInclude(framework) {
410
+ switch (framework) {
411
+ case "react":
412
+ return ["react", "react-dom/client"];
413
+ case "vue":
414
+ return ["vue"];
415
+ case "svelte":
416
+ return [];
417
+ default:
418
+ return [];
419
+ }
420
+ }
421
+ };
422
+
423
+ // src/use-cases/catalog.ts
424
+ import { readdirSync, statSync } from "fs";
425
+ import { extname, join as join3, relative } from "path";
426
+ var CatalogUseCase = class {
427
+ constructor(renderUseCase) {
428
+ this.renderUseCase = renderUseCase;
429
+ }
430
+ renderUseCase;
431
+ async renderCatalog(directory, options = {}) {
432
+ const { recursive = false, ...renderOptions } = options;
433
+ const files = this.scanDirectory(directory, recursive);
434
+ const results = [];
435
+ for (const filePath of files) {
436
+ const relativePath = relative(directory, filePath);
437
+ const framework = this.detectFramework(filePath);
438
+ try {
439
+ const { results: renderResults } = await this.renderUseCase.renderFile(
440
+ filePath,
441
+ {
442
+ ...renderOptions,
443
+ engines: ["chromium"]
444
+ }
445
+ );
446
+ const result = renderResults[0];
447
+ results.push({
448
+ path: relativePath,
449
+ framework,
450
+ image: result.image,
451
+ width: result.width,
452
+ height: result.height,
453
+ consoleErrors: result.consoleErrors
454
+ });
455
+ } catch {
456
+ results.push({
457
+ path: relativePath,
458
+ framework,
459
+ image: "",
460
+ width: 0,
461
+ height: 0,
462
+ consoleErrors: [`Failed to render ${relativePath}`]
463
+ });
464
+ }
465
+ }
466
+ return results;
467
+ }
468
+ scanDirectory(dir, recursive) {
469
+ const componentExtensions = /* @__PURE__ */ new Set([
470
+ ".jsx",
471
+ ".tsx",
472
+ ".vue",
473
+ ".svelte",
474
+ ".html",
475
+ ".htm"
476
+ ]);
477
+ const files = [];
478
+ const entries = readdirSync(dir);
479
+ for (const entry of entries) {
480
+ if (entry.startsWith(".") || entry === "node_modules") continue;
481
+ const fullPath = join3(dir, entry);
482
+ const stat = statSync(fullPath);
483
+ if (stat.isDirectory() && recursive) {
484
+ files.push(...this.scanDirectory(fullPath, recursive));
485
+ } else if (stat.isFile() && componentExtensions.has(extname(entry).toLowerCase())) {
486
+ files.push(fullPath);
487
+ }
488
+ }
489
+ return files;
490
+ }
491
+ detectFramework(filePath) {
492
+ return EXT_TO_FRAMEWORK[extname(filePath).toLowerCase()] ?? "react";
493
+ }
494
+ };
495
+
496
+ // src/use-cases/diff.ts
497
+ var DiffUseCase = class {
498
+ constructor(renderUseCase, imageComparator) {
499
+ this.renderUseCase = renderUseCase;
500
+ this.imageComparator = imageComparator;
501
+ }
502
+ renderUseCase;
503
+ imageComparator;
504
+ async diffComponent(before, after, framework, options = {}) {
505
+ const [beforeResults, afterResults] = await Promise.all([
506
+ this.renderUseCase.render(before, framework, {
507
+ ...options,
508
+ engines: ["chromium"]
509
+ }),
510
+ this.renderUseCase.render(after, framework, {
511
+ ...options,
512
+ engines: ["chromium"]
513
+ })
514
+ ]);
515
+ const comparison = this.imageComparator.diff(
516
+ beforeResults[0].image,
517
+ afterResults[0].image
518
+ );
519
+ return {
520
+ before: beforeResults[0].image,
521
+ after: afterResults[0].image,
522
+ ...comparison
523
+ };
524
+ }
525
+ async diffFromReference(code, framework, referenceImage, options = {}) {
526
+ const { threshold = 0.1, ...renderOptions } = options;
527
+ const [renderResult] = await this.renderUseCase.render(code, framework, {
528
+ ...renderOptions,
529
+ engines: ["chromium"]
530
+ });
531
+ const comparison = this.imageComparator.diff(
532
+ referenceImage,
533
+ renderResult.image,
534
+ threshold
535
+ );
536
+ return {
537
+ rendered: renderResult.image,
538
+ ...comparison,
539
+ passed: comparison.diffPercentage === 0
540
+ };
541
+ }
542
+ };
543
+
544
+ // src/use-cases/render.ts
545
+ import { readFileSync as readFileSync2 } from "fs";
546
+ import { extname as extname2 } from "path";
547
+ var RenderUseCase = class {
548
+ constructor(pool, htmlBuilder, imageComparator, viteBundler) {
549
+ this.pool = pool;
550
+ this.htmlBuilder = htmlBuilder;
551
+ this.imageComparator = imageComparator;
552
+ this.viteBundler = viteBundler;
553
+ }
554
+ pool;
555
+ htmlBuilder;
556
+ imageComparator;
557
+ viteBundler;
558
+ async render(code, framework, options = {}) {
559
+ const opts = this.resolveOptions(options);
560
+ const html = this.htmlBuilder.build(code, framework, {
561
+ darkMode: opts.darkMode,
562
+ css: opts.css,
563
+ tailwindVersion: opts.tailwindVersion
564
+ });
565
+ return Promise.all(
566
+ opts.engines.map((engine) => this.renderHtml(engine, html, opts))
567
+ );
568
+ }
569
+ async renderFile(filePath, options = {}) {
570
+ const opts = this.resolveOptions(options);
571
+ const ext = extname2(filePath).toLowerCase();
572
+ const framework = EXT_TO_FRAMEWORK[ext] ?? "react";
573
+ if (this.viteBundler) {
574
+ try {
575
+ const { url } = await this.viteBundler.getUrl(filePath, {
576
+ props: options.props,
577
+ framework,
578
+ projectRoot: options.projectRoot
579
+ });
580
+ const results2 = await Promise.all(
581
+ opts.engines.map((engine) => this.renderUrl(engine, url, opts))
582
+ );
583
+ return { results: results2, mode: "vite" };
584
+ } catch {
585
+ }
586
+ }
587
+ const code = readFileSync2(filePath, "utf-8");
588
+ const results = await this.render(code, framework, options);
589
+ return { results, mode: "cdn" };
590
+ }
591
+ async renderInteraction(code, framework, interactions, options = {}) {
592
+ const opts = this.resolveOptions(options);
593
+ const html = this.htmlBuilder.build(code, framework, {
594
+ darkMode: opts.darkMode,
595
+ css: opts.css,
596
+ tailwindVersion: opts.tailwindVersion
597
+ });
598
+ const page = await this.pool.getPage("chromium");
599
+ await this.pool.setViewport("chromium", opts.viewport);
600
+ await page.setContent(html, { waitUntil: "load", timeout: 1e4 });
601
+ for (const interaction of interactions) {
602
+ switch (interaction.action) {
603
+ case "click":
604
+ if (interaction.selector) await page.click(interaction.selector);
605
+ break;
606
+ case "hover":
607
+ if (interaction.selector) await page.hover(interaction.selector);
608
+ break;
609
+ case "focus":
610
+ if (interaction.selector) await page.focus(interaction.selector);
611
+ break;
612
+ case "type":
613
+ if (interaction.selector && interaction.value)
614
+ await page.fill(interaction.selector, interaction.value);
615
+ break;
616
+ case "wait":
617
+ await page.waitForTimeout(interaction.ms ?? 300);
618
+ break;
619
+ }
620
+ }
621
+ const screenshot = await page.screenshot({ type: "png", fullPage: true });
622
+ const metrics = await page.evaluate(() => ({
623
+ w: document.documentElement.scrollWidth,
624
+ h: document.documentElement.scrollHeight
625
+ }));
626
+ return {
627
+ image: screenshot.toString("base64"),
628
+ width: metrics.w,
629
+ height: metrics.h
630
+ };
631
+ }
632
+ async captureAnimation(code, framework, options = {}) {
633
+ const { frames = 5, duration = 1e3 } = options;
634
+ const opts = this.resolveOptions(options);
635
+ const html = this.htmlBuilder.build(code, framework, {
636
+ darkMode: opts.darkMode,
637
+ css: opts.css,
638
+ tailwindVersion: opts.tailwindVersion
639
+ });
640
+ const page = await this.pool.getPage("chromium");
641
+ await this.pool.setViewport("chromium", opts.viewport);
642
+ await page.setContent(html, { waitUntil: "load", timeout: 1e4 });
643
+ const interval = duration / (frames - 1);
644
+ const results = [];
645
+ for (let i = 0; i < frames; i++) {
646
+ if (i > 0) await page.waitForTimeout(interval);
647
+ const screenshot = await page.screenshot({
648
+ type: "png",
649
+ fullPage: false
650
+ });
651
+ results.push({
652
+ timestamp: Math.round(i * interval),
653
+ image: screenshot.toString("base64")
654
+ });
655
+ }
656
+ return results;
657
+ }
658
+ async renderGrid(cells, framework, options = {}) {
659
+ const { columns = Math.min(cells.length, 3) } = options;
660
+ const screenshots = await Promise.all(
661
+ cells.map(async (cell) => {
662
+ const [result] = await this.render(cell.code, framework, {
663
+ ...options,
664
+ engines: ["chromium"]
665
+ });
666
+ return Buffer.from(result.image, "base64");
667
+ })
668
+ );
669
+ const composite = this.imageComparator.composite(screenshots, columns);
670
+ return { ...composite, cells: cells.length };
671
+ }
672
+ async renderMatrix(code, framework, viewports, themes, options = {}) {
673
+ const combinations = [];
674
+ for (const viewport of viewports) {
675
+ for (const theme of themes) {
676
+ combinations.push({ viewport, theme });
677
+ }
678
+ }
679
+ return Promise.all(
680
+ combinations.map(async ({ viewport, theme }) => {
681
+ const [result] = await this.render(code, framework, {
682
+ ...options,
683
+ viewport: { width: viewport.width, height: viewport.height },
684
+ darkMode: theme === "dark",
685
+ engines: ["chromium"],
686
+ fullPage: true
687
+ });
688
+ return {
689
+ viewport: viewport.label,
690
+ theme,
691
+ image: result.image,
692
+ width: result.width,
693
+ height: result.height,
694
+ consoleErrors: result.consoleErrors
695
+ };
696
+ })
697
+ );
698
+ }
699
+ async renderHtml(engine, html, options) {
700
+ const page = await this.pool.getPage(engine);
701
+ await this.pool.setViewport(engine, options.viewport);
702
+ const consoleErrors = [];
703
+ const onError = (msg) => {
704
+ if (msg.type() === "error") consoleErrors.push(msg.text());
705
+ };
706
+ page.on("console", onError);
707
+ page.on("pageerror", (err) => consoleErrors.push(err.message));
708
+ await page.setContent(html, { waitUntil: "load", timeout: 1e4 });
709
+ if (options.waitFor > 0) {
710
+ await page.waitForTimeout(options.waitFor);
711
+ }
712
+ const screenshot = await page.screenshot({
713
+ type: "png",
714
+ fullPage: options.fullPage
715
+ });
716
+ const metrics = await page.evaluate(() => ({
717
+ w: document.documentElement.scrollWidth,
718
+ h: document.documentElement.scrollHeight
719
+ }));
720
+ page.removeListener("console", onError);
721
+ return {
722
+ engine,
723
+ image: screenshot.toString("base64"),
724
+ width: metrics.w,
725
+ height: metrics.h,
726
+ consoleErrors
727
+ };
728
+ }
729
+ async renderUrl(engine, url, options) {
730
+ const page = await this.pool.getPage(engine);
731
+ await this.pool.setViewport(engine, options.viewport);
732
+ const consoleErrors = [];
733
+ const onError = (msg) => {
734
+ if (msg.type() === "error") consoleErrors.push(msg.text());
735
+ };
736
+ page.on("console", onError);
737
+ page.on("pageerror", (err) => consoleErrors.push(err.message));
738
+ await page.goto(url, { waitUntil: "load", timeout: 15e3 });
739
+ await page.waitForLoadState("networkidle", { timeout: 5e3 }).catch(() => {
740
+ });
741
+ if (options.waitFor > 0) {
742
+ await page.waitForTimeout(options.waitFor);
743
+ }
744
+ const screenshot = await page.screenshot({
745
+ type: "png",
746
+ fullPage: options.fullPage
747
+ });
748
+ const metrics = await page.evaluate(() => ({
749
+ w: document.documentElement.scrollWidth,
750
+ h: document.documentElement.scrollHeight
751
+ }));
752
+ page.removeListener("console", onError);
753
+ return {
754
+ engine,
755
+ image: screenshot.toString("base64"),
756
+ width: metrics.w,
757
+ height: metrics.h,
758
+ consoleErrors
759
+ };
760
+ }
761
+ resolveOptions(partial) {
762
+ return {
763
+ viewport: partial.viewport ?? { width: 1280, height: 800 },
764
+ engines: partial.engines ?? ["chromium"],
765
+ fullPage: partial.fullPage ?? true,
766
+ darkMode: partial.darkMode ?? false,
767
+ css: partial.css ?? "",
768
+ tailwindVersion: partial.tailwindVersion ?? "3",
769
+ waitFor: partial.waitFor ?? 0
770
+ };
771
+ }
772
+ };
773
+
774
+ export {
775
+ __export,
776
+ DEVICE_PRESETS,
777
+ EXT_TO_FRAMEWORK,
778
+ BrowserPool,
779
+ HtmlBuilder,
780
+ ImageComparator,
781
+ ProjectDetector,
782
+ ViteBundler,
783
+ CatalogUseCase,
784
+ DiffUseCase,
785
+ RenderUseCase
786
+ };