@vercel/next-browser 0.1.8 → 0.2.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/dist/cli.js CHANGED
@@ -51,14 +51,49 @@ if (cmd === "reload") {
51
51
  const res = await send("reload");
52
52
  exit(res, res.ok ? `reloaded → ${res.data}` : "");
53
53
  }
54
- if (cmd === "capture-goto") {
55
- const res = await send("capture-goto", arg ? { url: arg } : {});
54
+ if (cmd === "perf") {
55
+ const res = await send("perf", arg ? { url: arg } : {});
56
56
  if (!res.ok)
57
57
  exit(res, "");
58
- const data = res.data;
59
- exit(res, `${data.frames} frames ${data.dir}\n` +
60
- "\n" +
61
- "frame-0000.png is the PPR shell. Remaining frames capture hydration → data.");
58
+ const d = res.data;
59
+ const lines = [`# Page Load Profile — ${d.url}`, ""];
60
+ // Core Web Vitals
61
+ lines.push("## Core Web Vitals");
62
+ const ttfbStr = d.ttfb != null ? `${d.ttfb}ms` : "—";
63
+ lines.push(` TTFB ${ttfbStr.padStart(10)}`);
64
+ if (d.lcp) {
65
+ const lcpLabel = d.lcp.element ? ` (${d.lcp.element}${d.lcp.url ? `: ${d.lcp.url.slice(0, 60)}` : ""})` : "";
66
+ lines.push(` LCP ${String(d.lcp.startTime + "ms").padStart(10)}${lcpLabel}`);
67
+ }
68
+ else {
69
+ lines.push(` LCP —`);
70
+ }
71
+ lines.push(` CLS ${String(d.cls.score).padStart(10)}`);
72
+ lines.push("");
73
+ // React Hydration
74
+ if (d.hydration) {
75
+ lines.push(`## React Hydration — ${d.hydration.duration}ms (${d.hydration.startTime}ms → ${d.hydration.endTime}ms)`);
76
+ }
77
+ else {
78
+ lines.push("## React Hydration — no data (requires profiling build)");
79
+ }
80
+ if (d.phases.length > 0) {
81
+ for (const p of d.phases) {
82
+ lines.push(` ${p.label.padEnd(28)} ${String(p.duration + "ms").padStart(10)} (${p.startTime} → ${p.endTime})`);
83
+ }
84
+ lines.push("");
85
+ }
86
+ if (d.hydratedComponents.length > 0) {
87
+ lines.push(`## Hydrated components (${d.hydratedComponents.length} total, sorted by duration)`);
88
+ const top = d.hydratedComponents.slice(0, 30);
89
+ for (const c of top) {
90
+ lines.push(` ${c.name.padEnd(40)} ${String(c.duration + "ms").padStart(10)}`);
91
+ }
92
+ if (d.hydratedComponents.length > 30) {
93
+ lines.push(` ... and ${d.hydratedComponents.length - 30} more`);
94
+ }
95
+ }
96
+ exit(res, lines.join("\n"));
62
97
  }
63
98
  if (cmd === "restart-server") {
64
99
  const res = await send("restart");
@@ -97,12 +132,61 @@ if (cmd === "screenshot") {
97
132
  const res = await send("screenshot");
98
133
  exit(res, res.ok ? String(res.data) : "");
99
134
  }
100
- if (cmd === "eval") {
135
+ if (cmd === "snapshot") {
136
+ const res = await send("snapshot");
137
+ exit(res, res.ok ? json(res.data) : "");
138
+ }
139
+ if (cmd === "click") {
101
140
  if (!arg) {
102
- console.error("usage: next-browser eval <script>");
141
+ console.error("usage: next-browser click <ref|text|selector>");
142
+ process.exit(1);
143
+ }
144
+ const res = await send("click", { selector: arg });
145
+ exit(res, "clicked");
146
+ }
147
+ if (cmd === "fill") {
148
+ const value = args[2];
149
+ if (!arg || value === undefined) {
150
+ console.error("usage: next-browser fill <ref|selector> <value>");
151
+ process.exit(1);
152
+ }
153
+ const res = await send("fill", { selector: arg, value });
154
+ exit(res, "filled");
155
+ }
156
+ if (cmd === "eval") {
157
+ // Check if first arg is a ref (e.g. "e3") — if so, second arg is the script
158
+ let ref;
159
+ let scriptArg = arg;
160
+ let fileArgIdx = 2;
161
+ if (arg && /^e\d+$/.test(arg)) {
162
+ ref = arg;
163
+ scriptArg = args[2];
164
+ fileArgIdx = 3;
165
+ }
166
+ let script;
167
+ if (scriptArg === "--file" || scriptArg === "-f") {
168
+ const filePath = args[fileArgIdx];
169
+ if (!filePath) {
170
+ console.error("usage: next-browser eval [ref] --file <path>");
171
+ process.exit(1);
172
+ }
173
+ script = readFileSync(filePath, "utf-8");
174
+ }
175
+ else if (scriptArg === "-") {
176
+ // Read from stdin
177
+ const chunks = [];
178
+ for await (const chunk of process.stdin)
179
+ chunks.push(chunk);
180
+ script = Buffer.concat(chunks).toString("utf-8");
181
+ }
182
+ else {
183
+ script = scriptArg;
184
+ }
185
+ if (!script) {
186
+ console.error("usage: next-browser eval [ref] <script>\n next-browser eval [ref] --file <path>\n echo 'script' | next-browser eval -");
103
187
  process.exit(1);
104
188
  }
105
- const res = await send("eval", { script: arg });
189
+ const res = await send("eval", { script, ...(ref ? { selector: ref } : {}) });
106
190
  exit(res, res.ok ? json(res.data) : "");
107
191
  }
108
192
  if (cmd === "tree") {
@@ -237,7 +321,7 @@ function printUsage() {
237
321
  " push [path] client-side navigation (interactive picker if no path)\n" +
238
322
  " back go back in history\n" +
239
323
  " reload reload current page\n" +
240
- " capture-goto [url] capture loading sequence (PPR shell hydration → data)\n" +
324
+ " perf [url] profile page load (CWVs + React hydration timing)\n" +
241
325
  " restart-server restart the Next.js dev server (clears fs cache)\n" +
242
326
  "\n" +
243
327
  " ppr lock enter PPR instant-navigation mode\n" +
@@ -248,7 +332,12 @@ function printUsage() {
248
332
  "\n" +
249
333
  " viewport [WxH] show or set viewport size (e.g. 1280x720)\n" +
250
334
  " screenshot save full-page screenshot to tmp file\n" +
251
- " eval <script> evaluate JS in page context\n" +
335
+ " snapshot accessibility tree with interactive refs\n" +
336
+ " click <ref|sel> click an element (real pointer events)\n" +
337
+ " fill <ref|sel> <v> fill a text input\n" +
338
+ " eval [ref] <script> evaluate JS in page context\n" +
339
+ " eval --file <path> evaluate JS from a file\n" +
340
+ " eval - evaluate JS from stdin\n" +
252
341
  "\n" +
253
342
  " errors show build/runtime errors\n" +
254
343
  " logs show recent dev server log output\n" +
package/dist/daemon.js CHANGED
@@ -57,8 +57,8 @@ async function run(cmd) {
57
57
  const data = await browser.reload();
58
58
  return { ok: true, data };
59
59
  }
60
- if (cmd.action === "capture-goto") {
61
- const data = await browser.captureGoto(cmd.url);
60
+ if (cmd.action === "perf") {
61
+ const data = await browser.perf(cmd.url);
62
62
  return { ok: true, data };
63
63
  }
64
64
  if (cmd.action === "restart") {
@@ -97,8 +97,20 @@ async function run(cmd) {
97
97
  const data = await browser.node(cmd.nodeId);
98
98
  return { ok: true, data };
99
99
  }
100
+ if (cmd.action === "snapshot") {
101
+ const data = await browser.snapshot();
102
+ return { ok: true, data };
103
+ }
104
+ if (cmd.action === "click") {
105
+ await browser.click(cmd.selector);
106
+ return { ok: true };
107
+ }
108
+ if (cmd.action === "fill") {
109
+ await browser.fill(cmd.selector, cmd.value);
110
+ return { ok: true };
111
+ }
100
112
  if (cmd.action === "eval") {
101
- const data = await browser.evaluate(cmd.script);
113
+ const data = await browser.evaluate(cmd.script, cmd.selector);
102
114
  return { ok: true, data };
103
115
  }
104
116
  if (cmd.action === "mcp") {
package/dist/paths.js CHANGED
@@ -2,5 +2,7 @@ import { homedir } from "node:os";
2
2
  import { join } from "node:path";
3
3
  const dir = join(homedir(), ".next-browser");
4
4
  export const socketDir = dir;
5
- export const socketPath = join(dir, "default.sock");
5
+ export const socketPath = process.platform === "win32"
6
+ ? "//./pipe/next-browser-default"
7
+ : join(dir, "default.sock");
6
8
  export const pidFile = join(dir, "default.pid");
package/dist/suspense.js CHANGED
@@ -124,61 +124,46 @@ export function formatReport(report) {
124
124
  }
125
125
  lines.push("");
126
126
  }
127
- lines.push("## Dynamic holes (suspended in shell)");
128
- for (const hole of report.holes) {
129
- const name = hole.name ?? "(unnamed)";
130
- const src = hole.source ? `${hole.source[0]}:${hole.source[1]}:${hole.source[2]}` : null;
131
- lines.push(` ${name}${src ? ` at ${src}` : ""}`);
132
- if (hole.renderedBy.length > 0) {
133
- lines.push(` rendered by: ${hole.renderedBy.map((o) => {
134
- const env = o.env ? ` [${o.env}]` : "";
135
- const src = o.source ? ` at ${o.source[0]}:${o.source[1]}` : "";
136
- return `${o.name}${env}${src}`;
137
- }).join(" > ")}`);
138
- }
139
- if (hole.environments.length > 0)
140
- lines.push(` environments: ${hole.environments.join(", ")}`);
141
- if (hole.primaryBlocker) {
142
- lines.push(` primary blocker: ${hole.primaryBlocker.name} ` +
143
- `(${hole.primaryBlocker.kind}, actionability ${labelActionability(hole.primaryBlocker.actionability)})`);
144
- if (hole.fallbackSource.path) {
145
- lines.push(` fallback source: ${hole.fallbackSource.path} ` +
146
- `(${hole.fallbackSource.confidence} confidence)`);
147
- }
148
- if (hole.primaryBlocker.sourceFrame) {
149
- lines.push(` source: ${hole.primaryBlocker.sourceFrame[0] || "(anonymous)"} ` +
150
- `${hole.primaryBlocker.sourceFrame[1]}:${hole.primaryBlocker.sourceFrame[2]}`);
127
+ // Detail section only shows info NOT already in the Quick Reference table:
128
+ // owner chains, environment tags, secondary blockers, and stack frames.
129
+ const holesWithDetail = report.holes.filter((h) => h.renderedBy.length > 0 || h.environments.length > 0 || h.blockers.length > 1 || h.unknownSuspenders);
130
+ if (holesWithDetail.length > 0) {
131
+ lines.push("## Detail");
132
+ for (const hole of holesWithDetail) {
133
+ const name = hole.name ?? "(unnamed)";
134
+ lines.push(` ${name}`);
135
+ if (hole.renderedBy.length > 0) {
136
+ lines.push(` rendered by: ${hole.renderedBy.map((o) => {
137
+ const env = o.env ? ` [${o.env}]` : "";
138
+ const src = o.source ? ` at ${o.source[0]}:${o.source[1]}` : "";
139
+ return `${o.name}${env}${src}`;
140
+ }).join(" > ")}`);
151
141
  }
152
- lines.push(` next step: ${hole.recommendation}`);
153
- }
154
- if (hole.blockers.length > 0) {
155
- lines.push(" blocked by:");
156
- for (const blocker of hole.blockers) {
157
- const dur = hole.primaryBlocker?.name === blocker.name ? " [primary]" : "";
158
- const env = blocker.env ? ` [${blocker.env}]` : "";
159
- const owner = blocker.ownerName ? ` initiated by <${blocker.ownerName}>` : "";
160
- const awaiter = blocker.awaiterName ? ` awaited in <${blocker.awaiterName}>` : "";
161
- lines.push(` - ${blocker.name}: ${blocker.description || "(no description)"}${env}${dur}${owner}${awaiter}`);
162
- if (blocker.ownerFrame) {
163
- const [fn, file, line] = blocker.ownerFrame;
164
- lines.push(` owner: ${fn || "(anonymous)"} ${file}:${line}`);
165
- }
166
- if (blocker.awaiterFrame && !blocker.ownerFrame) {
167
- const [fn, file, line] = blocker.awaiterFrame;
168
- lines.push(` awaiter: ${fn || "(anonymous)"} ${file}:${line}`);
169
- }
170
- if (blocker.ownerFrame && hole.primaryBlocker?.name === blocker.name) {
171
- for (const [fn, file, line] of [blocker.ownerFrame].slice(0, 3)) {
172
- lines.push(` at ${fn || "(anonymous)"} ${file}:${line}`);
142
+ if (hole.environments.length > 0)
143
+ lines.push(` environments: ${hole.environments.join(", ")}`);
144
+ if (hole.blockers.length > 1) {
145
+ lines.push(" secondary blockers:");
146
+ for (const blocker of hole.blockers.slice(1)) {
147
+ const env = blocker.env ? ` [${blocker.env}]` : "";
148
+ const owner = blocker.ownerName ? ` initiated by <${blocker.ownerName}>` : "";
149
+ const awaiter = blocker.awaiterName ? ` awaited in <${blocker.awaiterName}>` : "";
150
+ lines.push(` - ${blocker.name}: ${blocker.description || "(no description)"}${env}${owner}${awaiter}`);
151
+ if (blocker.ownerFrame) {
152
+ const [fn, file, line] = blocker.ownerFrame;
153
+ lines.push(` owner: ${fn || "(anonymous)"} ${file}:${line}`);
154
+ }
155
+ else if (blocker.awaiterFrame) {
156
+ const [fn, file, line] = blocker.awaiterFrame;
157
+ lines.push(` awaiter: ${fn || "(anonymous)"} ${file}:${line}`);
173
158
  }
174
159
  }
175
160
  }
161
+ if (hole.unknownSuspenders) {
162
+ lines.push(` suspenders unknown: ${hole.unknownSuspenders}`);
163
+ }
176
164
  }
177
- else if (hole.unknownSuspenders) {
178
- lines.push(` suspenders unknown: ${hole.unknownSuspenders}`);
179
- }
165
+ lines.push("");
180
166
  }
181
- lines.push("");
182
167
  }
183
168
  if (report.statics.length > 0) {
184
169
  lines.push("## Static (pre-rendered in shell)");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/next-browser",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
4
4
  "description": "Headed Playwright browser with React DevTools pre-loaded",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -21,20 +21,23 @@
21
21
  "bin": {
22
22
  "next-browser": "./dist/cli.js"
23
23
  },
24
- "scripts": {
25
- "start": "node src/cli.ts",
26
- "typecheck": "tsc",
27
- "build": "tsc -p tsconfig.build.json",
28
- "prepack": "pnpm build"
29
- },
30
24
  "dependencies": {
31
25
  "@next/playwright": "16.2.0-canary.80",
32
26
  "playwright": "^1.50.0",
33
27
  "source-map-js": "^1.2.1"
34
28
  },
35
- "packageManager": "pnpm@10.29.2",
36
29
  "devDependencies": {
30
+ "@changesets/changelog-github": "^0.6.0",
31
+ "@changesets/cli": "^2.30.0",
37
32
  "@types/node": "^22.0.0",
38
33
  "typescript": "^5.7.0"
34
+ },
35
+ "scripts": {
36
+ "start": "node src/cli.ts",
37
+ "typecheck": "tsc",
38
+ "build": "tsc -p tsconfig.build.json",
39
+ "changeset": "changeset",
40
+ "version": "changeset version",
41
+ "release": "pnpm build && changeset publish"
39
42
  }
40
- }
43
+ }
@@ -1,72 +0,0 @@
1
- import { connect as netConnect } from "node:net";
2
- import { readFileSync, existsSync, rmSync } from "node:fs";
3
- import { spawn } from "node:child_process";
4
- import { setTimeout as sleep } from "node:timers/promises";
5
- import { fileURLToPath } from "node:url";
6
- import { cloudSocketPath, cloudPidFile } from "./cloud-paths.js";
7
- export async function cloudSend(action, payload = {}) {
8
- await ensureCloudDaemon();
9
- const socket = await connect();
10
- const id = String(Date.now());
11
- socket.write(JSON.stringify({ id, action, ...payload }) + "\n");
12
- const line = await readLine(socket);
13
- socket.end();
14
- return JSON.parse(line);
15
- }
16
- async function ensureCloudDaemon() {
17
- if (daemonAlive() && (await connect().then(ok, no)))
18
- return;
19
- const ext = import.meta.url.endsWith(".ts") ? ".ts" : ".js";
20
- const daemon = fileURLToPath(new URL(`./cloud-daemon${ext}`, import.meta.url));
21
- const child = spawn(process.execPath, [daemon], {
22
- detached: true,
23
- stdio: "ignore",
24
- });
25
- child.unref();
26
- for (let i = 0; i < 50; i++) {
27
- if (await connect().then(ok, no))
28
- return;
29
- await sleep(100);
30
- }
31
- throw new Error(`cloud daemon failed to start (${cloudSocketPath})`);
32
- }
33
- function daemonAlive() {
34
- if (!existsSync(cloudPidFile))
35
- return false;
36
- const pid = Number(readFileSync(cloudPidFile, "utf-8"));
37
- try {
38
- process.kill(pid, 0);
39
- return true;
40
- }
41
- catch {
42
- rmSync(cloudPidFile, { force: true });
43
- rmSync(cloudSocketPath, { force: true });
44
- return false;
45
- }
46
- }
47
- function connect() {
48
- return new Promise((resolve, reject) => {
49
- const socket = netConnect(cloudSocketPath);
50
- socket.once("connect", () => resolve(socket));
51
- socket.once("error", reject);
52
- });
53
- }
54
- function readLine(socket) {
55
- return new Promise((resolve, reject) => {
56
- let buffer = "";
57
- socket.on("data", (chunk) => {
58
- buffer += chunk;
59
- const newline = buffer.indexOf("\n");
60
- if (newline >= 0)
61
- resolve(buffer.slice(0, newline));
62
- });
63
- socket.on("error", reject);
64
- });
65
- }
66
- function ok(s) {
67
- s.destroy();
68
- return true;
69
- }
70
- function no() {
71
- return false;
72
- }
@@ -1,87 +0,0 @@
1
- import { createServer } from "node:net";
2
- import { mkdirSync, writeFileSync, rmSync } from "node:fs";
3
- import * as cloud from "./cloud.js";
4
- import { cloudSocketDir, cloudSocketPath, cloudPidFile } from "./cloud-paths.js";
5
- mkdirSync(cloudSocketDir, { recursive: true, mode: 0o700 });
6
- rmSync(cloudSocketPath, { force: true });
7
- rmSync(cloudPidFile, { force: true });
8
- writeFileSync(cloudPidFile, String(process.pid));
9
- const server = createServer((socket) => {
10
- let buffer = "";
11
- socket.on("data", (chunk) => {
12
- buffer += chunk;
13
- let newline;
14
- while ((newline = buffer.indexOf("\n")) >= 0) {
15
- const line = buffer.slice(0, newline);
16
- buffer = buffer.slice(newline + 1);
17
- if (line)
18
- dispatch(line, socket);
19
- }
20
- });
21
- socket.on("error", () => { });
22
- });
23
- server.listen(cloudSocketPath);
24
- process.on("SIGINT", shutdown);
25
- process.on("SIGTERM", shutdown);
26
- process.on("exit", cleanup);
27
- async function dispatch(line, socket) {
28
- const cmd = JSON.parse(line);
29
- const result = await run(cmd).catch((err) => ({
30
- ok: false,
31
- error: err.message,
32
- }));
33
- socket.write(JSON.stringify({ id: cmd.id, ...result }) + "\n");
34
- if (cmd.action === "destroy")
35
- setImmediate(shutdown);
36
- }
37
- async function run(cmd) {
38
- if (cmd.action === "create") {
39
- const data = await cloud.create();
40
- return { ok: true, data };
41
- }
42
- if (cmd.action === "exec") {
43
- if (!cmd.command)
44
- return { ok: false, error: "missing command" };
45
- const result = await cloud.exec(cmd.command);
46
- const output = [
47
- result.stdout,
48
- result.stderr ? `stderr:\n${result.stderr}` : "",
49
- result.exitCode !== 0 ? `exit code: ${result.exitCode}` : "",
50
- ]
51
- .filter(Boolean)
52
- .join("\n");
53
- if (result.exitCode === 0)
54
- return { ok: true, data: output };
55
- return { ok: false, error: output || `exit code: ${result.exitCode}` };
56
- }
57
- if (cmd.action === "status") {
58
- const data = await cloud.status();
59
- return { ok: true, data };
60
- }
61
- if (cmd.action === "upload") {
62
- if (!cmd.localPath || !cmd.remotePath)
63
- return { ok: false, error: "missing localPath or remotePath" };
64
- const data = await cloud.upload(cmd.localPath, cmd.remotePath);
65
- return { ok: true, data };
66
- }
67
- if (cmd.action === "destroy") {
68
- const data = await cloud.destroy();
69
- return { ok: true, data };
70
- }
71
- return { ok: false, error: `unknown action: ${cmd.action}` };
72
- }
73
- async function shutdown() {
74
- try {
75
- await cloud.destroy();
76
- }
77
- catch {
78
- // best effort
79
- }
80
- server.close();
81
- cleanup();
82
- process.exit(0);
83
- }
84
- function cleanup() {
85
- rmSync(cloudSocketPath, { force: true });
86
- rmSync(cloudPidFile, { force: true });
87
- }
@@ -1,7 +0,0 @@
1
- import { homedir } from "node:os";
2
- import { join } from "node:path";
3
- const dir = join(homedir(), ".next-browser");
4
- export const cloudSocketDir = dir;
5
- export const cloudSocketPath = join(dir, "cloud.sock");
6
- export const cloudPidFile = join(dir, "cloud.pid");
7
- export const cloudStateFile = join(dir, "cloud.json");