@vercel/next-browser 0.5.1 → 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.
package/README.md CHANGED
@@ -38,7 +38,7 @@ Requires Node >= 20.
38
38
 
39
39
  | Command | Description |
40
40
  | ------------------------------------ | -------------------------------------------------- |
41
- | `open <url> [--cookies-json <file>]` | Launch browser and navigate (with optional cookies) |
41
+ | `open <url> [--cookies <file>]` | Launch browser and navigate (with optional cookies) |
42
42
  | `close` | Close browser and kill daemon |
43
43
 
44
44
  ### Navigation
@@ -62,9 +62,9 @@ Requires Node >= 20.
62
62
  | `snapshot` | Accessibility tree with `[ref=eN]` markers on interactive elements |
63
63
  | `errors` | Build and runtime errors for the current page |
64
64
  | `logs` | Recent dev server log output |
65
+ | `browser-logs` | Browser console output (log, warn, error, info) |
65
66
  | `network [idx]` | List network requests, or inspect one (headers, body) |
66
- | `preview [caption]` | Screenshot + open in viewer window (accumulates across calls) |
67
- | `screenshot` | Viewport PNG to a temp file (`--full-page` for entire page) |
67
+ | `screenshot [caption] [--full-page]` | Viewport PNG to a temp file (caption shown in Screenshot Log) |
68
68
 
69
69
  ### Interaction
70
70
 
@@ -77,11 +77,20 @@ Requires Node >= 20.
77
77
 
78
78
  ### Performance & PPR
79
79
 
80
- | Command | Description |
81
- | -------------- | ------------------------------------------------------------ |
82
- | `perf [url]` | Core Web Vitals + React hydration timing in one pass |
83
- | `ppr lock` | Freeze dynamic content to inspect the static shell |
84
- | `ppr unlock` | Resume dynamic content and print shell analysis |
80
+ | Command | Description |
81
+ | ------------------------------ | ---------------------------------------------------- |
82
+ | `perf [url]` | Core Web Vitals + React hydration timing in one pass |
83
+ | `renders start` | Start recording React re-renders |
84
+ | `renders stop [--json]` | Stop and print per-component render profile |
85
+ | `ppr lock` | Freeze dynamic content to inspect the static shell |
86
+ | `ppr unlock` | Resume dynamic content and print shell analysis |
87
+
88
+ ### Instrumentation
89
+
90
+ | Command | Description |
91
+ | ------------------------------ | ---------------------------------------------------- |
92
+ | `instrumentation set <path>` | Inject script before page scripts on every navigation |
93
+ | `instrumentation clear` | Remove instrumentation script |
85
94
 
86
95
  ### Next.js MCP
87
96
 
package/dist/browser.js CHANGED
@@ -30,6 +30,7 @@ let page = null;
30
30
  let profileDirPath = null;
31
31
  let initialOrigin = null;
32
32
  let ssrLocked = false;
33
+ let instrumentationVersion = 0;
33
34
  let screenshotBrowser = null;
34
35
  let screenshotPage = null;
35
36
  let screenshotEntries = [];
@@ -87,6 +88,7 @@ export async function close() {
87
88
  release = null;
88
89
  settled = null;
89
90
  ssrLocked = false;
91
+ instrumentationVersion = 0;
90
92
  // Clean up temp profile directory.
91
93
  if (profileDirPath) {
92
94
  const { rmSync } = await import("node:fs");
@@ -95,6 +97,30 @@ export async function close() {
95
97
  initialOrigin = null;
96
98
  }
97
99
  }
100
+ // ── Instrumentation ─────────────────────────────────────────────────────────
101
+ //
102
+ // addInitScript can't be removed, so we version-gate: each `set` bumps the
103
+ // version and registers a new script that only runs when its version matches.
104
+ // `clear` bumps the version with no matching script, disabling the old one.
105
+ export async function instrumentationSet(script) {
106
+ if (!page)
107
+ throw new Error("browser not open");
108
+ const ctx = page.context();
109
+ const v = ++instrumentationVersion;
110
+ // The version gate runs first, then the guarded script.
111
+ await ctx.addInitScript(`window.__NB_IV__=${v}`);
112
+ await ctx.addInitScript(`if(window.__NB_IV__===${v}){${script}}`);
113
+ // Apply immediately on the current page.
114
+ await page.evaluate(script);
115
+ }
116
+ export async function instrumentationClear() {
117
+ if (!page)
118
+ throw new Error("browser not open");
119
+ const ctx = page.context();
120
+ // Bump version — no matching script, so nothing runs.
121
+ const v = ++instrumentationVersion;
122
+ await ctx.addInitScript(`window.__NB_IV__=${v}`);
123
+ }
98
124
  // ── PPR lock/unlock ──────────────────────────────────────────────────────────
99
125
  //
100
126
  // The lock uses @next/playwright's `instant()` which sets the
@@ -451,6 +477,9 @@ export async function push(path) {
451
477
  if (!page)
452
478
  throw new Error("browser not open");
453
479
  const before = page.url();
480
+ const resolved = new URL(path, before).href;
481
+ if (resolved === before)
482
+ throw new Error("already on this URL");
454
483
  await page.evaluate((p) => window.next.router.push(p), path);
455
484
  await page.waitForURL((u) => u.href !== before, { timeout: 10_000 }).catch(() => { });
456
485
  return page.url();
package/dist/cli.js CHANGED
@@ -150,7 +150,15 @@ if (cmd === "screenshot") {
150
150
  const fullPage = args.includes("--full-page");
151
151
  const caption = args.slice(1).filter((a) => a !== "--full-page").join(" ") || undefined;
152
152
  const res = await send("screenshot", { fullPage, caption });
153
- exit(res, res.ok ? String(res.data) : "");
153
+ if (res.ok) {
154
+ const { path, errors } = res.data;
155
+ const parts = [path];
156
+ if (errors)
157
+ parts.push("\nerrors:\n" + json(errors));
158
+ console.log(parts.join(""));
159
+ process.exit(0);
160
+ }
161
+ exit(res, "");
154
162
  }
155
163
  if (cmd === "snapshot") {
156
164
  const res = await send("snapshot");
@@ -369,6 +377,20 @@ if (cmd === "viewport") {
369
377
  const data = res.data;
370
378
  exit(res, `${data.width}x${data.height}`);
371
379
  }
380
+ if (cmd === "instrumentation" && arg === "set") {
381
+ const filePath = args[2];
382
+ if (!filePath) {
383
+ console.error("usage: next-browser instrumentation set <path>");
384
+ process.exit(1);
385
+ }
386
+ const script = readFileSync(filePath, "utf-8");
387
+ const res = await send("instrumentation-set", { instrumentationScript: script });
388
+ exit(res, "instrumentation set");
389
+ }
390
+ if (cmd === "instrumentation" && arg === "clear") {
391
+ const res = await send("instrumentation-clear");
392
+ exit(res, "instrumentation cleared");
393
+ }
372
394
  if (cmd === "close") {
373
395
  const res = await send("close");
374
396
  exit(res, "closed");
@@ -477,5 +499,8 @@ function printUsage() {
477
499
  " page show current page segments and router info\n" +
478
500
  " project show project path and dev server url\n" +
479
501
  " routes list app routes\n" +
480
- " action <id> inspect a server action by id");
502
+ " action <id> inspect a server action by id\n" +
503
+ "\n" +
504
+ " instrumentation set <path> inject script before page scripts\n" +
505
+ " instrumentation clear remove instrumentation script");
481
506
  }
package/dist/cookies.js CHANGED
@@ -4,7 +4,8 @@
4
4
  *
5
5
  * 1. JSON array — Playwright-style `[{"name": "x", "value": "y"}, ...]`.
6
6
  * 2. cURL command — as produced by DevTools → Network → Copy as cURL.
7
- * The Cookie header is extracted from the `-H 'cookie: …'` argument.
7
+ * Cookies are extracted from `-H 'cookie: …'` or `-b '…'`/`--cookie '…'`
8
+ * (macOS Chrome uses `-b` instead of `-H`).
8
9
  * 3. Bare cookie header — `name=v; name=v; ...` (e.g. the value of the
9
10
  * Cookie row in DevTools → Network → Request Headers).
10
11
  *
@@ -43,8 +44,12 @@ function extractCookieHeaderFromCurl(curl) {
43
44
  const joined = curl.replace(/\\\r?\n\s*/g, " ").replace(/\^\r?\n\s*/g, " ");
44
45
  // -H 'cookie: …' (bash) or -H "cookie: …" (cmd). Chrome/Firefox use one
45
46
  // or the other depending on which Copy-as-cURL variant the user picked.
46
- const m = joined.match(/-H\s+(['"])\s*cookie\s*:\s*([\s\S]*?)\1/i);
47
- return m ? m[2] : null;
47
+ const h = joined.match(/-H\s+(['"])\s*cookie\s*:\s*([\s\S]*?)\1/i);
48
+ if (h)
49
+ return h[2];
50
+ // macOS Chrome uses -b (or --cookie) instead of -H for cookies.
51
+ const b = joined.match(/(?:-b|--cookie)\s+(['"])([\s\S]*?)\1/);
52
+ return b ? b[2] : null;
48
53
  }
49
54
  function parseCookieHeader(header) {
50
55
  const pairs = [];
package/dist/daemon.js CHANGED
@@ -78,8 +78,11 @@ async function run(cmd) {
78
78
  return { ok: true, data };
79
79
  }
80
80
  if (cmd.action === "screenshot") {
81
- const data = await browser.screenshot({ fullPage: cmd.fullPage, caption: cmd.caption });
82
- return { ok: true, data };
81
+ const [path, errors] = await Promise.all([
82
+ browser.screenshot({ fullPage: cmd.fullPage, caption: cmd.caption }),
83
+ browser.mcp("get_errors").catch(() => null),
84
+ ]);
85
+ return { ok: true, data: { path, errors } };
83
86
  }
84
87
  if (cmd.action === "links") {
85
88
  const data = await browser.links();
@@ -145,6 +148,14 @@ async function run(cmd) {
145
148
  const data = await browser.viewportSize();
146
149
  return { ok: true, data };
147
150
  }
151
+ if (cmd.action === "instrumentation-set") {
152
+ await browser.instrumentationSet(cmd.instrumentationScript);
153
+ return { ok: true };
154
+ }
155
+ if (cmd.action === "instrumentation-clear") {
156
+ await browser.instrumentationClear();
157
+ return { ok: true };
158
+ }
148
159
  if (cmd.action === "close") {
149
160
  await browser.close();
150
161
  return { ok: true };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/next-browser",
3
- "version": "0.5.1",
3
+ "version": "0.7.0",
4
4
  "description": "Headed Playwright browser with React DevTools pre-loaded",
5
5
  "license": "MIT",
6
6
  "repository": {