@vercel/next-browser 0.1.5 → 0.1.7

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/browser.js CHANGED
@@ -28,6 +28,7 @@ const installHook = readFileSync(join(extensionPath, "build", "installHook.js"),
28
28
  let context = null;
29
29
  let page = null;
30
30
  let profileDirPath = null;
31
+ let initialOrigin = null;
31
32
  // ── Browser lifecycle ────────────────────────────────────────────────────────
32
33
  /**
33
34
  * Launch the browser (if not already open) and optionally navigate to a URL.
@@ -41,6 +42,7 @@ export async function open(url) {
41
42
  net.attach(page);
42
43
  }
43
44
  if (url) {
45
+ initialOrigin = new URL(url).origin;
44
46
  await page.goto(url, { waitUntil: "domcontentloaded" });
45
47
  }
46
48
  }
@@ -67,6 +69,7 @@ export async function close() {
67
69
  const { rmSync } = await import("node:fs");
68
70
  rmSync(profileDirPath, { recursive: true, force: true });
69
71
  profileDirPath = null;
72
+ initialOrigin = null;
70
73
  }
71
74
  }
72
75
  // ── PPR lock/unlock ──────────────────────────────────────────────────────────
@@ -336,6 +339,7 @@ export async function goto(url) {
336
339
  if (!page)
337
340
  throw new Error("browser not open");
338
341
  const target = new URL(url, page.url()).href;
342
+ initialOrigin = new URL(target).origin;
339
343
  await page.goto(target, { waitUntil: "domcontentloaded" });
340
344
  return target;
341
345
  }
@@ -408,11 +412,17 @@ export async function evaluate(script) {
408
412
  throw new Error("browser not open");
409
413
  return page.evaluate(script);
410
414
  }
411
- /** Call a Next.js dev server MCP tool (JSON-RPC over SSE at /_next/mcp). */
415
+ /**
416
+ * Call a Next.js dev server MCP tool (JSON-RPC over SSE at /_next/mcp).
417
+ *
418
+ * Uses the initial navigation origin (before any proxy redirects) rather than
419
+ * the current page origin. This handles microfrontends proxies that redirect
420
+ * e.g. localhost:3332 -> localhost:3024 but don't forward /_next/mcp.
421
+ */
412
422
  export async function mcp(tool, args) {
413
423
  if (!page)
414
424
  throw new Error("browser not open");
415
- const origin = new URL(page.url()).origin;
425
+ const origin = initialOrigin ?? new URL(page.url()).origin;
416
426
  return nextMcp.call(origin, tool, args);
417
427
  }
418
428
  /** Get network request log, or detail for a specific request index. */
package/dist/cli.js CHANGED
@@ -18,6 +18,7 @@ if (cmd === "open") {
18
18
  console.error("usage: next-browser open <url> [--cookies-json <file>]");
19
19
  process.exit(1);
20
20
  }
21
+ const url = /^https?:\/\//.test(arg) ? arg : `http://${arg}`;
21
22
  const cookieIdx = args.indexOf("--cookies-json");
22
23
  const cookieFile = cookieIdx >= 0 ? args[cookieIdx + 1] : undefined;
23
24
  if (cookieFile) {
@@ -26,15 +27,15 @@ if (cmd === "open") {
26
27
  exit(res, "");
27
28
  const raw = readFileSync(cookieFile, "utf-8");
28
29
  const cookies = JSON.parse(raw);
29
- const domain = new URL(arg).hostname;
30
+ const domain = new URL(url).hostname;
30
31
  const cRes = await send("cookies", { cookies, domain });
31
32
  if (!cRes.ok)
32
33
  exit(cRes, "");
33
- await send("goto", { url: arg });
34
- exit(res, `opened → ${arg} (${cookies.length} cookies for ${domain})`);
34
+ await send("goto", { url });
35
+ exit(res, `opened → ${url} (${cookies.length} cookies for ${domain})`);
35
36
  }
36
- const res = await send("open", { url: arg });
37
- exit(res, `opened → ${arg}`);
37
+ const res = await send("open", { url });
38
+ exit(res, `opened → ${url}`);
38
39
  }
39
40
  if (cmd === "ppr" && arg === "lock") {
40
41
  const res = await send("lock");
@@ -43,19 +43,27 @@ async function run(cmd) {
43
43
  if (!cmd.command)
44
44
  return { ok: false, error: "missing command" };
45
45
  const result = await cloud.exec(cmd.command);
46
- const data = [
46
+ const output = [
47
47
  result.stdout,
48
48
  result.stderr ? `stderr:\n${result.stderr}` : "",
49
49
  result.exitCode !== 0 ? `exit code: ${result.exitCode}` : "",
50
50
  ]
51
51
  .filter(Boolean)
52
52
  .join("\n");
53
- return { ok: result.exitCode === 0, data };
53
+ if (result.exitCode === 0)
54
+ return { ok: true, data: output };
55
+ return { ok: false, error: output || `exit code: ${result.exitCode}` };
54
56
  }
55
57
  if (cmd.action === "status") {
56
58
  const data = await cloud.status();
57
59
  return { ok: true, data };
58
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
+ }
59
67
  if (cmd.action === "destroy") {
60
68
  const data = await cloud.destroy();
61
69
  return { ok: true, data };
package/dist/cloud.js CHANGED
@@ -7,7 +7,6 @@ import { dirname, join, resolve } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { cloudStateFile } from "./cloud-paths.js";
9
9
  const __dirname = dirname(fileURLToPath(import.meta.url));
10
- // Dynamic import so the main CLI doesn't fail if @vercel/sandbox isn't installed
11
10
  async function getSandboxSDK() {
12
11
  try {
13
12
  return await import("@vercel/sandbox");
@@ -32,14 +31,37 @@ function loadState() {
32
31
  function clearState() {
33
32
  rmSync(cloudStateFile, { force: true });
34
33
  }
35
- // Keep the live Sandbox instance in memory (daemon process)
36
34
  let sandbox = null;
35
+ /** System dependencies required for headless Chromium on Amazon Linux 2023. */
36
+ const CHROME_SYSTEM_DEPS = [
37
+ "nspr", "nss", "atk", "at-spi2-atk", "cups-libs", "libdrm",
38
+ "libxkbcommon", "libXcomposite", "libXdamage", "libXfixes",
39
+ "libXrandr", "mesa-libgbm", "alsa-lib", "cairo", "pango",
40
+ "glib2", "gtk3", "libX11", "libXext", "libXcursor", "libXi", "libXtst",
41
+ ];
42
+ async function runInSandbox(cmd) {
43
+ if (!sandbox)
44
+ throw new Error("no sandbox");
45
+ const result = await sandbox.runCommand({
46
+ cmd: "bash",
47
+ args: ["-lc", cmd],
48
+ });
49
+ let stdout = "";
50
+ let stderr = "";
51
+ for await (const log of result.logs()) {
52
+ if (log.stream === "stdout")
53
+ stdout += log.data;
54
+ else
55
+ stderr += log.data;
56
+ }
57
+ await result.wait();
58
+ return { exitCode: result.exitCode, stdout, stderr };
59
+ }
37
60
  export async function create() {
38
61
  if (sandbox) {
39
62
  return `sandbox already running: ${sandbox.sandboxId}`;
40
63
  }
41
64
  loadEnv();
42
- // Check for existing state (previous daemon)
43
65
  const existing = loadState();
44
66
  if (existing) {
45
67
  try {
@@ -50,14 +72,14 @@ export async function create() {
50
72
  }
51
73
  }
52
74
  catch {
53
- // stale state, clean up
75
+ // stale state
54
76
  }
55
77
  clearState();
56
78
  }
57
79
  const { Sandbox } = await getSandboxSDK();
58
80
  sandbox = await Sandbox.create({
59
- resources: { vcpus: 4 },
60
- timeout: 300_000,
81
+ resources: { vcpus: 8 },
82
+ timeout: 18_000_000, // 5 hours (max for Pro/Enterprise)
61
83
  ports: [3000],
62
84
  runtime: "node22",
63
85
  });
@@ -73,6 +95,36 @@ export async function create() {
73
95
  // no public URL yet
74
96
  }
75
97
  saveState(state);
98
+ // ── Full environment setup ───────────────────────────────────
99
+ // 1. Chrome system deps (headless Chromium on Amazon Linux 2023)
100
+ await runInSandbox(`sudo dnf install -y ${CHROME_SYSTEM_DEPS.join(" ")} > /dev/null 2>&1`);
101
+ // 2. next-browser + Playwright Chromium
102
+ await runInSandbox(`npm install -g @vercel/next-browser 2>&1 | tail -1`);
103
+ await runInSandbox(`npm install -g playwright @playwright/browser-chromium 2>&1 | tail -1`);
104
+ await runInSandbox(`npx playwright install chromium 2>&1 | tail -1`);
105
+ // 3. SSH key (for private git repos)
106
+ const { homedir } = await import("node:os");
107
+ const sshKeyPath = join(homedir(), ".ssh", "id_ed25519");
108
+ if (existsSync(sshKeyPath)) {
109
+ const key = readFileSync(sshKeyPath, "utf-8");
110
+ await runInSandbox(`mkdir -p ~/.ssh && chmod 700 ~/.ssh`);
111
+ await sandbox.writeFiles([
112
+ { path: "/home/vercel-sandbox/.ssh/id_ed25519", content: Buffer.from(key) },
113
+ ]);
114
+ await runInSandbox(`chmod 600 ~/.ssh/id_ed25519`);
115
+ await runInSandbox(`ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null`);
116
+ }
117
+ // 4. npmrc (for private packages)
118
+ const npmrcPath = join(homedir(), ".npmrc");
119
+ if (existsSync(npmrcPath)) {
120
+ const npmrc = readFileSync(npmrcPath, "utf-8");
121
+ await sandbox.writeFiles([
122
+ { path: "/home/vercel-sandbox/.npmrc", content: Buffer.from(npmrc) },
123
+ ]);
124
+ }
125
+ // 5. Node 24 via nvm (many projects require it)
126
+ await runInSandbox(`curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash > /dev/null 2>&1`);
127
+ await runInSandbox(`source ~/.nvm/nvm.sh && nvm install 24 > /dev/null 2>&1`);
76
128
  return [
77
129
  `sandbox created: ${sandbox.sandboxId}`,
78
130
  state.publicUrl ? `url: ${state.publicUrl}` : null,
@@ -83,20 +135,15 @@ export async function create() {
83
135
  export async function exec(command) {
84
136
  if (!sandbox)
85
137
  throw new Error("no sandbox running — run `cloud create` first");
86
- const result = await sandbox.runCommand({
87
- cmd: "bash",
88
- args: ["-lc", command],
89
- });
90
- let stdout = "";
91
- let stderr = "";
92
- for await (const log of result.logs()) {
93
- if (log.stream === "stdout")
94
- stdout += log.data;
95
- else
96
- stderr += log.data;
97
- }
98
- await result.wait();
99
- return { exitCode: result.exitCode, stdout, stderr };
138
+ return runInSandbox(command);
139
+ }
140
+ /** Upload a local file to the sandbox. */
141
+ export async function upload(localPath, remotePath) {
142
+ if (!sandbox)
143
+ throw new Error("no sandbox running — run `cloud create` first");
144
+ const content = readFileSync(localPath);
145
+ await sandbox.writeFiles([{ path: remotePath, content }]);
146
+ return `uploaded ${localPath} → ${remotePath} (${content.length} bytes)`;
100
147
  }
101
148
  export async function destroy() {
102
149
  if (!sandbox) {
@@ -147,7 +194,6 @@ export async function status() {
147
194
  }
148
195
  /**
149
196
  * Load .env.local for Vercel credentials.
150
- * Searches from package directory and common locations.
151
197
  */
152
198
  function loadEnv() {
153
199
  const candidates = [
package/dist/mcp.js CHANGED
@@ -27,5 +27,12 @@ export async function call(origin, tool, args = {}) {
27
27
  if (parsed.error)
28
28
  throw new Error(parsed.error.message);
29
29
  const text = parsed.result?.content?.[0]?.text;
30
- return text ? JSON.parse(text) : parsed.result;
30
+ if (!text)
31
+ return parsed.result;
32
+ try {
33
+ return JSON.parse(text);
34
+ }
35
+ catch {
36
+ return text;
37
+ }
31
38
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/next-browser",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Headed Playwright browser with React DevTools pre-loaded",
5
5
  "license": "MIT",
6
6
  "repository": {