@vercel/next-browser 0.1.8 → 0.3.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 +154 -53
- package/dist/browser.js +423 -88
- package/dist/cli.js +119 -17
- package/dist/daemon.js +27 -7
- package/dist/paths.js +3 -1
- package/dist/suspense.js +34 -49
- package/package.json +12 -9
- package/dist/cloud-client.js +0 -72
- package/dist/cloud-daemon.js +0 -87
- package/dist/cloud-paths.js +0 -7
- package/dist/cloud.js +0 -230
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 === "
|
|
55
|
-
const res = await send("
|
|
54
|
+
if (cmd === "perf") {
|
|
55
|
+
const res = await send("perf", arg ? { url: arg } : {});
|
|
56
56
|
if (!res.ok)
|
|
57
57
|
exit(res, "");
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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");
|
|
@@ -85,24 +120,84 @@ if (cmd === "goto") {
|
|
|
85
120
|
const res = await send("goto", { url: arg });
|
|
86
121
|
exit(res, res.ok ? `→ ${res.data}` : "");
|
|
87
122
|
}
|
|
88
|
-
if (cmd === "ssr
|
|
89
|
-
const res = await send("ssr-
|
|
90
|
-
exit(res,
|
|
123
|
+
if (cmd === "ssr" && arg === "lock") {
|
|
124
|
+
const res = await send("ssr-lock");
|
|
125
|
+
exit(res, "ssr locked — external scripts blocked on all navigations");
|
|
126
|
+
}
|
|
127
|
+
if (cmd === "ssr" && arg === "unlock") {
|
|
128
|
+
const res = await send("ssr-unlock");
|
|
129
|
+
exit(res, "ssr unlocked — external scripts re-enabled");
|
|
91
130
|
}
|
|
92
131
|
if (cmd === "back") {
|
|
93
132
|
const res = await send("back");
|
|
94
133
|
exit(res, "back");
|
|
95
134
|
}
|
|
135
|
+
if (cmd === "preview") {
|
|
136
|
+
const clear = args.includes("--clear");
|
|
137
|
+
const caption = args.filter((a) => a !== "--clear").slice(1).join(" ") || undefined;
|
|
138
|
+
const res = await send("preview", { caption, clear });
|
|
139
|
+
exit(res, res.ok ? `preview → ${res.data}` : "");
|
|
140
|
+
}
|
|
96
141
|
if (cmd === "screenshot") {
|
|
97
|
-
const
|
|
142
|
+
const fullPage = args.includes("--full-page");
|
|
143
|
+
const res = await send("screenshot", { fullPage });
|
|
98
144
|
exit(res, res.ok ? String(res.data) : "");
|
|
99
145
|
}
|
|
100
|
-
if (cmd === "
|
|
146
|
+
if (cmd === "snapshot") {
|
|
147
|
+
const res = await send("snapshot");
|
|
148
|
+
exit(res, res.ok ? json(res.data) : "");
|
|
149
|
+
}
|
|
150
|
+
if (cmd === "click") {
|
|
101
151
|
if (!arg) {
|
|
102
|
-
console.error("usage: next-browser
|
|
152
|
+
console.error("usage: next-browser click <ref|text|selector>");
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
const res = await send("click", { selector: arg });
|
|
156
|
+
exit(res, "clicked");
|
|
157
|
+
}
|
|
158
|
+
if (cmd === "fill") {
|
|
159
|
+
const value = args[2];
|
|
160
|
+
if (!arg || value === undefined) {
|
|
161
|
+
console.error("usage: next-browser fill <ref|selector> <value>");
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
const res = await send("fill", { selector: arg, value });
|
|
165
|
+
exit(res, "filled");
|
|
166
|
+
}
|
|
167
|
+
if (cmd === "eval") {
|
|
168
|
+
// Check if first arg is a ref (e.g. "e3") — if so, second arg is the script
|
|
169
|
+
let ref;
|
|
170
|
+
let scriptArg = arg;
|
|
171
|
+
let fileArgIdx = 2;
|
|
172
|
+
if (arg && /^e\d+$/.test(arg)) {
|
|
173
|
+
ref = arg;
|
|
174
|
+
scriptArg = args[2];
|
|
175
|
+
fileArgIdx = 3;
|
|
176
|
+
}
|
|
177
|
+
let script;
|
|
178
|
+
if (scriptArg === "--file" || scriptArg === "-f") {
|
|
179
|
+
const filePath = args[fileArgIdx];
|
|
180
|
+
if (!filePath) {
|
|
181
|
+
console.error("usage: next-browser eval [ref] --file <path>");
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
script = readFileSync(filePath, "utf-8");
|
|
185
|
+
}
|
|
186
|
+
else if (scriptArg === "-") {
|
|
187
|
+
// Read from stdin
|
|
188
|
+
const chunks = [];
|
|
189
|
+
for await (const chunk of process.stdin)
|
|
190
|
+
chunks.push(chunk);
|
|
191
|
+
script = Buffer.concat(chunks).toString("utf-8");
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
script = scriptArg;
|
|
195
|
+
}
|
|
196
|
+
if (!script) {
|
|
197
|
+
console.error("usage: next-browser eval [ref] <script>\n next-browser eval [ref] --file <path>\n echo 'script' | next-browser eval -");
|
|
103
198
|
process.exit(1);
|
|
104
199
|
}
|
|
105
|
-
const res = await send("eval", { script:
|
|
200
|
+
const res = await send("eval", { script, ...(ref ? { selector: ref } : {}) });
|
|
106
201
|
exit(res, res.ok ? json(res.data) : "");
|
|
107
202
|
}
|
|
108
203
|
if (cmd === "tree") {
|
|
@@ -233,11 +328,12 @@ function printUsage() {
|
|
|
233
328
|
" close close browser and daemon\n" +
|
|
234
329
|
"\n" +
|
|
235
330
|
" goto <url> full-page navigation (new document load)\n" +
|
|
236
|
-
" ssr
|
|
331
|
+
" ssr lock block external scripts on all navigations\n" +
|
|
332
|
+
" ssr unlock re-enable external scripts\n" +
|
|
237
333
|
" push [path] client-side navigation (interactive picker if no path)\n" +
|
|
238
334
|
" back go back in history\n" +
|
|
239
335
|
" reload reload current page\n" +
|
|
240
|
-
"
|
|
336
|
+
" perf [url] profile page load (CWVs + React hydration timing)\n" +
|
|
241
337
|
" restart-server restart the Next.js dev server (clears fs cache)\n" +
|
|
242
338
|
"\n" +
|
|
243
339
|
" ppr lock enter PPR instant-navigation mode\n" +
|
|
@@ -247,8 +343,14 @@ function printUsage() {
|
|
|
247
343
|
" tree <id> inspect component (props, hooks, state, source)\n" +
|
|
248
344
|
"\n" +
|
|
249
345
|
" viewport [WxH] show or set viewport size (e.g. 1280x720)\n" +
|
|
250
|
-
"
|
|
251
|
-
"
|
|
346
|
+
" preview [caption] [--clear] screenshot + open in viewer (accumulates)\n" +
|
|
347
|
+
" screenshot [--full-page] save screenshot to tmp file\n" +
|
|
348
|
+
" snapshot accessibility tree with interactive refs\n" +
|
|
349
|
+
" click <ref|sel> click an element (real pointer events)\n" +
|
|
350
|
+
" fill <ref|sel> <v> fill a text input\n" +
|
|
351
|
+
" eval [ref] <script> evaluate JS in page context\n" +
|
|
352
|
+
" eval --file <path> evaluate JS from a file\n" +
|
|
353
|
+
" eval - evaluate JS from stdin\n" +
|
|
252
354
|
"\n" +
|
|
253
355
|
" errors show build/runtime errors\n" +
|
|
254
356
|
" logs show recent dev server log output\n" +
|
package/dist/daemon.js
CHANGED
|
@@ -57,16 +57,20 @@ async function run(cmd) {
|
|
|
57
57
|
const data = await browser.reload();
|
|
58
58
|
return { ok: true, data };
|
|
59
59
|
}
|
|
60
|
-
if (cmd.action === "
|
|
61
|
-
const data = await browser.
|
|
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") {
|
|
65
65
|
const data = await browser.restart();
|
|
66
66
|
return { ok: true, data };
|
|
67
67
|
}
|
|
68
|
+
if (cmd.action === "preview") {
|
|
69
|
+
const data = await browser.preview(cmd.caption, cmd.clear);
|
|
70
|
+
return { ok: true, data };
|
|
71
|
+
}
|
|
68
72
|
if (cmd.action === "screenshot") {
|
|
69
|
-
const data = await browser.screenshot();
|
|
73
|
+
const data = await browser.screenshot({ fullPage: cmd.fullPage });
|
|
70
74
|
return { ok: true, data };
|
|
71
75
|
}
|
|
72
76
|
if (cmd.action === "links") {
|
|
@@ -81,9 +85,13 @@ async function run(cmd) {
|
|
|
81
85
|
const data = await browser.goto(cmd.url);
|
|
82
86
|
return { ok: true, data };
|
|
83
87
|
}
|
|
84
|
-
if (cmd.action === "ssr-
|
|
85
|
-
|
|
86
|
-
return { ok: true
|
|
88
|
+
if (cmd.action === "ssr-lock") {
|
|
89
|
+
await browser.ssrLock();
|
|
90
|
+
return { ok: true };
|
|
91
|
+
}
|
|
92
|
+
if (cmd.action === "ssr-unlock") {
|
|
93
|
+
await browser.ssrUnlock();
|
|
94
|
+
return { ok: true };
|
|
87
95
|
}
|
|
88
96
|
if (cmd.action === "back") {
|
|
89
97
|
await browser.back();
|
|
@@ -97,8 +105,20 @@ async function run(cmd) {
|
|
|
97
105
|
const data = await browser.node(cmd.nodeId);
|
|
98
106
|
return { ok: true, data };
|
|
99
107
|
}
|
|
108
|
+
if (cmd.action === "snapshot") {
|
|
109
|
+
const data = await browser.snapshot();
|
|
110
|
+
return { ok: true, data };
|
|
111
|
+
}
|
|
112
|
+
if (cmd.action === "click") {
|
|
113
|
+
await browser.click(cmd.selector);
|
|
114
|
+
return { ok: true };
|
|
115
|
+
}
|
|
116
|
+
if (cmd.action === "fill") {
|
|
117
|
+
await browser.fill(cmd.selector, cmd.value);
|
|
118
|
+
return { ok: true };
|
|
119
|
+
}
|
|
100
120
|
if (cmd.action === "eval") {
|
|
101
|
-
const data = await browser.evaluate(cmd.script);
|
|
121
|
+
const data = await browser.evaluate(cmd.script, cmd.selector);
|
|
102
122
|
return { ok: true, data };
|
|
103
123
|
}
|
|
104
124
|
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 =
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
lines.push(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.3.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
|
+
}
|
package/dist/cloud-client.js
DELETED
|
@@ -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
|
-
}
|
package/dist/cloud-daemon.js
DELETED
|
@@ -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
|
-
}
|
package/dist/cloud-paths.js
DELETED
|
@@ -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");
|