@vercel/next-browser 0.5.0 → 0.5.1

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 (2) hide show
  1. package/dist/browser.js +86 -8
  2. package/package.json +1 -1
package/dist/browser.js CHANGED
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * Module-level state: one browser context, one page, one PPR lock.
12
12
  */
13
- import { readFileSync, mkdirSync, writeFileSync } from "node:fs";
13
+ import { readFileSync, mkdirSync, writeFileSync, unlinkSync } from "node:fs";
14
14
  import { join, resolve } from "node:path";
15
15
  import { tmpdir } from "node:os";
16
16
  import { chromium } from "playwright";
@@ -568,21 +568,99 @@ async function refreshScreenshotLog() {
568
568
  function escapeHtml(s) {
569
569
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
570
570
  }
571
+ /** Max screenshot dimension in pixels — keeps images under the 2000px limit
572
+ * that multi-image LLM requests impose. */
573
+ const SCREENSHOT_MAX_DIM = 1280;
571
574
  /** Screenshot saved to a temp file. Opens the Screenshot Log window in headed mode.
572
- * Returns the file path. */
575
+ * Returns the file path for a single image, or a directory path when the
576
+ * image is sliced into multiple chunks (full-page on long pages). */
573
577
  export async function screenshot(opts) {
574
578
  if (!page)
575
579
  throw new Error("browser not open");
576
580
  await hideDevOverlay();
577
- const { join } = await import("node:path");
578
- const { tmpdir } = await import("node:os");
579
581
  const path = join(tmpdir(), `next-browser-${Date.now()}.png`);
580
- await page.screenshot({ path, fullPage: opts?.fullPage });
581
- const imgData = readFileSync(path).toString("base64");
582
+ // scale: 'css' prevents Retina 2x doubling (1440x900 stays 1440x900).
583
+ await page.screenshot({ path, fullPage: opts?.fullPage, scale: "css" });
584
+ const result = await sliceIfNeeded(path, SCREENSHOT_MAX_DIM);
582
585
  const timestamp = new Date().toLocaleTimeString();
583
- screenshotEntries.unshift({ caption: opts?.caption, imgData, timestamp });
586
+ if (typeof result === "string") {
587
+ // Single file — fits within limits.
588
+ const imgData = readFileSync(result).toString("base64");
589
+ screenshotEntries.unshift({ caption: opts?.caption, imgData, timestamp });
590
+ }
591
+ else {
592
+ // Multiple chunks — add each to the screenshot log.
593
+ for (const chunk of result.files) {
594
+ const imgData = readFileSync(chunk).toString("base64");
595
+ const label = opts?.caption
596
+ ? `${opts.caption} (${result.files.indexOf(chunk) + 1}/${result.files.length})`
597
+ : `chunk ${result.files.indexOf(chunk) + 1}/${result.files.length}`;
598
+ screenshotEntries.unshift({ caption: label, imgData, timestamp });
599
+ }
600
+ }
584
601
  await refreshScreenshotLog();
585
- return path;
602
+ return typeof result === "string" ? result : result.dir;
603
+ }
604
+ /** If the screenshot fits within `maxDim`, scale it in-place and return the
605
+ * path. Otherwise slice it into ≤maxDim chunks inside a temp directory and
606
+ * return `{ dir, files }`. */
607
+ async function sliceIfNeeded(filePath, maxDim) {
608
+ const buf = readFileSync(filePath);
609
+ if (buf.length < 24)
610
+ return filePath;
611
+ const w = buf.readUInt32BE(16);
612
+ const h = buf.readUInt32BE(20);
613
+ if (w <= maxDim && h <= maxDim)
614
+ return filePath;
615
+ if (!page)
616
+ return filePath;
617
+ const b64 = buf.toString("base64");
618
+ const wScale = Math.min(maxDim / w, 1);
619
+ const scaledW = Math.round(w * wScale);
620
+ const scaledH = Math.round(h * wScale);
621
+ if (scaledH <= maxDim) {
622
+ // Fits after width scaling — single resized file.
623
+ const resized = await page.evaluate(async ({ src, nw, nh }) => {
624
+ const img = new Image();
625
+ await new Promise((r, e) => { img.onload = () => r(); img.onerror = () => e(); img.src = `data:image/png;base64,${src}`; });
626
+ const c = document.createElement("canvas");
627
+ c.width = nw;
628
+ c.height = nh;
629
+ c.getContext("2d").drawImage(img, 0, 0, nw, nh);
630
+ return c.toDataURL("image/png").split(",")[1];
631
+ }, { src: b64, nw: scaledW, nh: scaledH });
632
+ writeFileSync(filePath, Buffer.from(resized, "base64"));
633
+ return filePath;
634
+ }
635
+ // Slice into chunks of maxDim height (in scaled pixels).
636
+ const chunkCount = Math.ceil(scaledH / maxDim);
637
+ const chunks = await page.evaluate(async ({ src, nw, totalH, max, count }) => {
638
+ const img = new Image();
639
+ await new Promise((r, e) => { img.onload = () => r(); img.onerror = () => e(); img.src = `data:image/png;base64,${src}`; });
640
+ const results = [];
641
+ for (let i = 0; i < count; i++) {
642
+ const sy = (i * max) / (totalH / img.height);
643
+ const sh = Math.min(max, totalH - i * max) / (totalH / img.height);
644
+ const dh = Math.min(max, totalH - i * max);
645
+ const c = document.createElement("canvas");
646
+ c.width = nw;
647
+ c.height = dh;
648
+ c.getContext("2d").drawImage(img, 0, sy, img.width, sh, 0, 0, nw, dh);
649
+ results.push(c.toDataURL("image/png").split(",")[1]);
650
+ }
651
+ return results;
652
+ }, { src: b64, nw: scaledW, totalH: scaledH, max: maxDim, count: chunkCount });
653
+ const dir = join(tmpdir(), `next-browser-screenshots-${Date.now()}`);
654
+ mkdirSync(dir, { recursive: true });
655
+ const files = [];
656
+ for (let i = 0; i < chunks.length; i++) {
657
+ const p = join(dir, `${String(i + 1).padStart(3, "0")}.png`);
658
+ writeFileSync(p, Buffer.from(chunks[i], "base64"));
659
+ files.push(p);
660
+ }
661
+ // Clean up the original full file.
662
+ unlinkSync(filePath);
663
+ return { dir, files };
586
664
  }
587
665
  /** Remove Next.js devtools overlay from the page before screenshots. */
588
666
  async function hideDevOverlay() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/next-browser",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Headed Playwright browser with React DevTools pre-loaded",
5
5
  "license": "MIT",
6
6
  "repository": {