@vercel/next-browser 0.1.0 → 0.1.2
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 +16 -26
- package/dist/browser.js +377 -0
- package/dist/cli.js +219 -0
- package/dist/client.js +72 -0
- package/dist/daemon.js +119 -0
- package/dist/mcp.js +31 -0
- package/dist/network.js +101 -0
- package/{src/paths.ts → dist/paths.js} +0 -2
- package/dist/sourcemap.js +84 -0
- package/dist/suspense.js +332 -0
- package/dist/tree.js +231 -0
- package/package.json +7 -5
- package/src/browser.ts +0 -408
- package/src/cli.ts +0 -233
- package/src/client.ts +0 -80
- package/src/daemon.ts +0 -140
- package/src/mcp.ts +0 -37
- package/src/network.ts +0 -124
- package/src/sourcemap.ts +0 -84
- package/src/suspense.ts +0 -361
- package/src/tree.ts +0 -240
package/dist/suspense.js
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import * as sourcemap from "./sourcemap.js";
|
|
2
|
+
export async function snapshot(page) {
|
|
3
|
+
return page.evaluate(inPageSuspense, true);
|
|
4
|
+
}
|
|
5
|
+
export async function countBoundaries(page) {
|
|
6
|
+
const boundaries = await page.evaluate(inPageSuspense, false).catch(() => []);
|
|
7
|
+
const nonRoot = boundaries.filter((b) => b.parentID !== 0);
|
|
8
|
+
return { total: nonRoot.length, suspended: nonRoot.filter((b) => b.isSuspended).length };
|
|
9
|
+
}
|
|
10
|
+
export async function snapshotFromDom(page) {
|
|
11
|
+
const count = await page.evaluate(() => document.querySelectorAll('template[id^="B:"]').length).catch(() => 0);
|
|
12
|
+
const boundaries = [];
|
|
13
|
+
for (let i = 0; i < count; i++) {
|
|
14
|
+
boundaries.push({
|
|
15
|
+
id: -(i + 1),
|
|
16
|
+
parentID: 1,
|
|
17
|
+
name: `shell-hole-${i}`,
|
|
18
|
+
isSuspended: true,
|
|
19
|
+
environments: [],
|
|
20
|
+
suspendedBy: [],
|
|
21
|
+
unknownSuspenders: null,
|
|
22
|
+
owners: [],
|
|
23
|
+
jsxSource: null,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return boundaries;
|
|
27
|
+
}
|
|
28
|
+
export async function formatAnalysis(unlocked, locked, origin) {
|
|
29
|
+
await resolveSources(unlocked, origin);
|
|
30
|
+
await resolveSources(locked, origin);
|
|
31
|
+
const fromDom = locked.length > 0 && locked[0].id < 0;
|
|
32
|
+
const holes = [];
|
|
33
|
+
const statics = [];
|
|
34
|
+
if (fromDom) {
|
|
35
|
+
const dynamicUnlocked = unlocked.filter((b) => b.parentID !== 0 && b.suspendedBy.length > 0);
|
|
36
|
+
for (const lb of locked) {
|
|
37
|
+
const match = dynamicUnlocked.shift();
|
|
38
|
+
holes.push({ shell: lb, full: match });
|
|
39
|
+
}
|
|
40
|
+
for (const ub of unlocked) {
|
|
41
|
+
if (ub.parentID !== 0 && ub.suspendedBy.length === 0)
|
|
42
|
+
statics.push(ub);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const unlockedByKey = new Map();
|
|
47
|
+
for (const b of unlocked)
|
|
48
|
+
unlockedByKey.set(boundaryKey(b), b);
|
|
49
|
+
for (const lb of locked) {
|
|
50
|
+
if (lb.isSuspended) {
|
|
51
|
+
holes.push({ shell: lb, full: unlockedByKey.get(boundaryKey(lb)) });
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
statics.push(lb);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const totalBoundaries = fromDom ? holes.length + statics.length : locked.length;
|
|
59
|
+
const lines = [
|
|
60
|
+
"# PPR Shell Analysis",
|
|
61
|
+
`# ${totalBoundaries} boundaries: ${holes.length} dynamic holes, ${statics.length} static`,
|
|
62
|
+
"",
|
|
63
|
+
];
|
|
64
|
+
if (holes.length > 0) {
|
|
65
|
+
lines.push("## Dynamic holes (suspended in shell)");
|
|
66
|
+
for (const { shell, full } of holes) {
|
|
67
|
+
const b = full ?? shell;
|
|
68
|
+
const name = b.name?.startsWith("shell-hole") ? "(hole)" : (b.name ?? "(unnamed)");
|
|
69
|
+
const src = b.jsxSource ? `${b.jsxSource[0]}:${b.jsxSource[1]}:${b.jsxSource[2]}` : null;
|
|
70
|
+
lines.push(` ${name}${src ? ` at ${src}` : ""}`);
|
|
71
|
+
if (b.owners.length > 0)
|
|
72
|
+
lines.push(` rendered by: ${b.owners.join(" > ")}`);
|
|
73
|
+
if (shell.environments.length > 0)
|
|
74
|
+
lines.push(` environments: ${shell.environments.join(", ")}`);
|
|
75
|
+
if (full && full.suspendedBy.length > 0) {
|
|
76
|
+
lines.push(" blocked by:");
|
|
77
|
+
for (const s of full.suspendedBy) {
|
|
78
|
+
const dur = s.duration > 0 ? ` (${s.duration}ms)` : "";
|
|
79
|
+
const env = s.env ? ` [${s.env}]` : "";
|
|
80
|
+
const owner = s.ownerName ? ` initiated by <${s.ownerName}>` : "";
|
|
81
|
+
const awaiter = s.awaiterName ? ` awaited in <${s.awaiterName}>` : "";
|
|
82
|
+
lines.push(` - ${s.name}: ${s.description || "(no description)"}${dur}${env}${owner}${awaiter}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else if (full?.unknownSuspenders) {
|
|
86
|
+
lines.push(` suspenders unknown: ${full.unknownSuspenders}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
lines.push("");
|
|
90
|
+
}
|
|
91
|
+
if (statics.length > 0) {
|
|
92
|
+
lines.push("## Static (pre-rendered in shell)");
|
|
93
|
+
for (const b of statics) {
|
|
94
|
+
const name = b.name ?? "(unnamed)";
|
|
95
|
+
const src = b.jsxSource ? ` at ${b.jsxSource[0]}:${b.jsxSource[1]}:${b.jsxSource[2]}` : "";
|
|
96
|
+
lines.push(` ${name}${src}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return lines.join("\n");
|
|
100
|
+
}
|
|
101
|
+
function boundaryKey(b) {
|
|
102
|
+
if (b.jsxSource)
|
|
103
|
+
return `${b.jsxSource[0]}:${b.jsxSource[1]}:${b.jsxSource[2]}`;
|
|
104
|
+
return b.name ?? `id-${b.id}`;
|
|
105
|
+
}
|
|
106
|
+
async function resolveSources(boundaries, origin) {
|
|
107
|
+
for (const b of boundaries) {
|
|
108
|
+
if (b.jsxSource) {
|
|
109
|
+
const [file, line, col] = b.jsxSource;
|
|
110
|
+
const resolved = (await sourcemap.resolve(origin, file, line, col)) ??
|
|
111
|
+
(await sourcemap.resolveViaMap(origin, file, line, col));
|
|
112
|
+
if (resolved)
|
|
113
|
+
b.jsxSource = [resolved.file, resolved.line, resolved.column];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async function inPageSuspense(inspect) {
|
|
118
|
+
const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
119
|
+
if (!hook)
|
|
120
|
+
throw new Error("React DevTools hook not installed");
|
|
121
|
+
const ri = hook.rendererInterfaces?.get?.(1);
|
|
122
|
+
if (!ri)
|
|
123
|
+
throw new Error("no React renderer attached");
|
|
124
|
+
const batches = await collect(ri);
|
|
125
|
+
const boundaryMap = new Map();
|
|
126
|
+
for (const ops of batches)
|
|
127
|
+
decodeSuspenseOps(ops, boundaryMap);
|
|
128
|
+
const results = [];
|
|
129
|
+
for (const b of boundaryMap.values()) {
|
|
130
|
+
if (b.parentID === 0)
|
|
131
|
+
continue;
|
|
132
|
+
const boundary = {
|
|
133
|
+
id: b.id,
|
|
134
|
+
parentID: b.parentID,
|
|
135
|
+
name: b.name,
|
|
136
|
+
isSuspended: b.isSuspended,
|
|
137
|
+
environments: b.environments,
|
|
138
|
+
suspendedBy: [],
|
|
139
|
+
unknownSuspenders: null,
|
|
140
|
+
owners: [],
|
|
141
|
+
jsxSource: null,
|
|
142
|
+
};
|
|
143
|
+
if (inspect && ri.hasElementWithId(b.id)) {
|
|
144
|
+
const displayName = ri.getDisplayNameForElementID(b.id);
|
|
145
|
+
if (displayName)
|
|
146
|
+
boundary.name = displayName;
|
|
147
|
+
const result = ri.inspectElement(1, b.id, null, true);
|
|
148
|
+
if (result?.type === "full-data") {
|
|
149
|
+
parseInspection(boundary, result.value);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
results.push(boundary);
|
|
153
|
+
}
|
|
154
|
+
return results;
|
|
155
|
+
function collect(renderer) {
|
|
156
|
+
return new Promise((resolve) => {
|
|
157
|
+
const out = [];
|
|
158
|
+
const listener = (e) => {
|
|
159
|
+
const p = e.data?.payload;
|
|
160
|
+
if (e.data?.source === "react-devtools-bridge" && p?.event === "operations") {
|
|
161
|
+
out.push(p.payload);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
window.addEventListener("message", listener);
|
|
165
|
+
renderer.flushInitialOperations();
|
|
166
|
+
setTimeout(() => {
|
|
167
|
+
window.removeEventListener("message", listener);
|
|
168
|
+
resolve(out);
|
|
169
|
+
}, 50);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
function decodeSuspenseOps(ops, map) {
|
|
173
|
+
let i = 2;
|
|
174
|
+
const strings = [null];
|
|
175
|
+
const tableEnd = ++i + ops[i - 1];
|
|
176
|
+
while (i < tableEnd) {
|
|
177
|
+
const len = ops[i++];
|
|
178
|
+
strings.push(String.fromCodePoint(...ops.slice(i, i + len)));
|
|
179
|
+
i += len;
|
|
180
|
+
}
|
|
181
|
+
while (i < ops.length) {
|
|
182
|
+
const op = ops[i];
|
|
183
|
+
if (op === 1) {
|
|
184
|
+
const type = ops[i + 2];
|
|
185
|
+
i += 3 + (type === 11 ? 4 : 5);
|
|
186
|
+
}
|
|
187
|
+
else if (op === 2) {
|
|
188
|
+
i += 2 + ops[i + 1];
|
|
189
|
+
}
|
|
190
|
+
else if (op === 3) {
|
|
191
|
+
i += 3 + ops[i + 2];
|
|
192
|
+
}
|
|
193
|
+
else if (op === 4) {
|
|
194
|
+
i += 3;
|
|
195
|
+
}
|
|
196
|
+
else if (op === 5) {
|
|
197
|
+
i += 4;
|
|
198
|
+
}
|
|
199
|
+
else if (op === 6) {
|
|
200
|
+
i++;
|
|
201
|
+
}
|
|
202
|
+
else if (op === 7) {
|
|
203
|
+
i += 3;
|
|
204
|
+
}
|
|
205
|
+
else if (op === 8) {
|
|
206
|
+
const id = ops[i + 1];
|
|
207
|
+
const parentID = ops[i + 2];
|
|
208
|
+
const nameStrID = ops[i + 3];
|
|
209
|
+
const isSuspended = ops[i + 4] === 1;
|
|
210
|
+
const numRects = ops[i + 5];
|
|
211
|
+
i += 6;
|
|
212
|
+
if (numRects !== -1)
|
|
213
|
+
i += numRects * 4;
|
|
214
|
+
map.set(id, {
|
|
215
|
+
id,
|
|
216
|
+
parentID,
|
|
217
|
+
name: strings[nameStrID] ?? null,
|
|
218
|
+
isSuspended,
|
|
219
|
+
environments: [],
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
else if (op === 9) {
|
|
223
|
+
i += 2 + ops[i + 1];
|
|
224
|
+
}
|
|
225
|
+
else if (op === 10) {
|
|
226
|
+
i += 3 + ops[i + 2];
|
|
227
|
+
}
|
|
228
|
+
else if (op === 11) {
|
|
229
|
+
const numRects = ops[i + 2];
|
|
230
|
+
i += 3;
|
|
231
|
+
if (numRects !== -1)
|
|
232
|
+
i += numRects * 4;
|
|
233
|
+
}
|
|
234
|
+
else if (op === 12) {
|
|
235
|
+
i++;
|
|
236
|
+
const changeLen = ops[i++];
|
|
237
|
+
for (let c = 0; c < changeLen; c++) {
|
|
238
|
+
const id = ops[i++];
|
|
239
|
+
i++; // hasUniqueSuspenders
|
|
240
|
+
i++; // endTime
|
|
241
|
+
const isSuspended = ops[i++] === 1;
|
|
242
|
+
const envLen = ops[i++];
|
|
243
|
+
const envs = [];
|
|
244
|
+
for (let e = 0; e < envLen; e++) {
|
|
245
|
+
const name = strings[ops[i++]];
|
|
246
|
+
if (name != null)
|
|
247
|
+
envs.push(name);
|
|
248
|
+
}
|
|
249
|
+
const node = map.get(id);
|
|
250
|
+
if (node) {
|
|
251
|
+
node.isSuspended = isSuspended;
|
|
252
|
+
for (const env of envs) {
|
|
253
|
+
if (!node.environments.includes(env))
|
|
254
|
+
node.environments.push(env);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
else if (op === 13) {
|
|
260
|
+
i += 2;
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
i++;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function parseInspection(boundary, data) {
|
|
268
|
+
const rawSuspendedBy = data.suspendedBy;
|
|
269
|
+
const rawSuspenders = Array.isArray(rawSuspendedBy)
|
|
270
|
+
? rawSuspendedBy
|
|
271
|
+
: Array.isArray(rawSuspendedBy?.data)
|
|
272
|
+
? rawSuspendedBy.data
|
|
273
|
+
: null;
|
|
274
|
+
if (rawSuspenders) {
|
|
275
|
+
for (const entry of rawSuspenders) {
|
|
276
|
+
const awaited = entry?.awaited;
|
|
277
|
+
if (!awaited)
|
|
278
|
+
continue;
|
|
279
|
+
const desc = preview(awaited.description) || preview(awaited.value);
|
|
280
|
+
boundary.suspendedBy.push({
|
|
281
|
+
name: awaited.name ?? "unknown",
|
|
282
|
+
description: desc,
|
|
283
|
+
duration: awaited.end && awaited.start ? Math.round(awaited.end - awaited.start) : 0,
|
|
284
|
+
env: awaited.env ?? entry?.env ?? null,
|
|
285
|
+
ownerName: awaited.owner?.displayName ?? null,
|
|
286
|
+
awaiterName: entry?.owner?.displayName ?? null,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (data.unknownSuspenders && data.unknownSuspenders !== 0) {
|
|
291
|
+
const reasons = {
|
|
292
|
+
1: "production build (no debug info)",
|
|
293
|
+
2: "old React version (missing tracking)",
|
|
294
|
+
3: "thrown Promise (library using throw instead of use())",
|
|
295
|
+
};
|
|
296
|
+
boundary.unknownSuspenders = reasons[data.unknownSuspenders] ?? "unknown reason";
|
|
297
|
+
}
|
|
298
|
+
if (Array.isArray(data.owners)) {
|
|
299
|
+
for (const o of data.owners) {
|
|
300
|
+
if (o?.displayName)
|
|
301
|
+
boundary.owners.push(o.displayName);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (Array.isArray(data.stack) && data.stack.length > 0) {
|
|
305
|
+
const frame = data.stack[0];
|
|
306
|
+
if (Array.isArray(frame) && frame.length >= 4) {
|
|
307
|
+
boundary.jsxSource = [frame[1] || "(unknown)", frame[2], frame[3]];
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function preview(v) {
|
|
312
|
+
if (v == null)
|
|
313
|
+
return "";
|
|
314
|
+
if (typeof v === "string")
|
|
315
|
+
return v;
|
|
316
|
+
if (typeof v !== "object")
|
|
317
|
+
return String(v);
|
|
318
|
+
if (typeof v.preview_long === "string")
|
|
319
|
+
return v.preview_long;
|
|
320
|
+
if (typeof v.preview_short === "string")
|
|
321
|
+
return v.preview_short;
|
|
322
|
+
if (typeof v.value === "string")
|
|
323
|
+
return v.value;
|
|
324
|
+
try {
|
|
325
|
+
const s = JSON.stringify(v);
|
|
326
|
+
return s.length > 80 ? s.slice(0, 77) + "..." : s;
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
return "";
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
package/dist/tree.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
export async function snapshot(page) {
|
|
2
|
+
return page.evaluate(inPageSnapshot);
|
|
3
|
+
}
|
|
4
|
+
export async function inspect(page, id) {
|
|
5
|
+
return page.evaluate(inPageInspect, id);
|
|
6
|
+
}
|
|
7
|
+
export function path(nodes, id) {
|
|
8
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
9
|
+
const names = [];
|
|
10
|
+
for (let n = byId.get(id); n; n = byId.get(n.parent)) {
|
|
11
|
+
names.push(n.name ?? typeName(n.type));
|
|
12
|
+
}
|
|
13
|
+
return names.reverse().join(" > ");
|
|
14
|
+
}
|
|
15
|
+
const HEADER = "# React component tree\n" +
|
|
16
|
+
"# Columns: depth id parent name [key=...]\n" +
|
|
17
|
+
"# Use `tree <id>` for props/hooks/state. IDs valid until next navigation.\n";
|
|
18
|
+
export function format(nodes) {
|
|
19
|
+
const children = new Map();
|
|
20
|
+
for (const n of nodes) {
|
|
21
|
+
const list = children.get(n.parent) ?? [];
|
|
22
|
+
list.push(n);
|
|
23
|
+
children.set(n.parent, list);
|
|
24
|
+
}
|
|
25
|
+
const lines = [HEADER];
|
|
26
|
+
for (const root of children.get(0) ?? [])
|
|
27
|
+
walk(root, 0);
|
|
28
|
+
return lines.join("\n");
|
|
29
|
+
function walk(node, depth) {
|
|
30
|
+
const name = node.name ?? typeName(node.type);
|
|
31
|
+
const key = node.key ? ` key=${JSON.stringify(node.key)}` : "";
|
|
32
|
+
const parent = node.parent || "-";
|
|
33
|
+
lines.push(`${depth} ${node.id} ${parent} ${name}${key}`);
|
|
34
|
+
for (const c of children.get(node.id) ?? [])
|
|
35
|
+
walk(c, depth + 1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function typeName(type) {
|
|
39
|
+
const names = {
|
|
40
|
+
11: "Root",
|
|
41
|
+
12: "Suspense",
|
|
42
|
+
13: "SuspenseList",
|
|
43
|
+
};
|
|
44
|
+
return names[type] ?? `(${type})`;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Runs inside the page. Asks the DevTools backend to flush the full
|
|
48
|
+
* component tree as an operations batch, then decodes TREE_OPERATION_ADD
|
|
49
|
+
* entries into a flat node list.
|
|
50
|
+
*
|
|
51
|
+
* Wire format (React DevTools v6/v7):
|
|
52
|
+
* Header: [rendererID, rootID, stringTableSize, ...stringTable]
|
|
53
|
+
* ADD: [1, id, type, ...4 ints] if type == 11 (Root)
|
|
54
|
+
* [1, id, type, parentID, ownerID,
|
|
55
|
+
* displayNameStrID, keyStrID, _] otherwise
|
|
56
|
+
*/
|
|
57
|
+
async function inPageSnapshot() {
|
|
58
|
+
const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
59
|
+
if (!hook)
|
|
60
|
+
throw new Error("React DevTools hook not installed");
|
|
61
|
+
const ri = hook.rendererInterfaces?.get?.(1);
|
|
62
|
+
if (!ri)
|
|
63
|
+
throw new Error("no React renderer attached");
|
|
64
|
+
const batches = await collect(ri);
|
|
65
|
+
return batches.flatMap(decode);
|
|
66
|
+
function collect(ri) {
|
|
67
|
+
return new Promise((resolve) => {
|
|
68
|
+
const out = [];
|
|
69
|
+
const listener = (e) => {
|
|
70
|
+
const p = e.data?.payload;
|
|
71
|
+
if (e.data?.source === "react-devtools-bridge" && p?.event === "operations") {
|
|
72
|
+
out.push(p.payload);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
window.addEventListener("message", listener);
|
|
76
|
+
ri.flushInitialOperations();
|
|
77
|
+
setTimeout(() => {
|
|
78
|
+
window.removeEventListener("message", listener);
|
|
79
|
+
resolve(out);
|
|
80
|
+
}, 50);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function decode(ops) {
|
|
84
|
+
let i = 2;
|
|
85
|
+
const strings = [null];
|
|
86
|
+
const tableEnd = ++i + ops[i - 1];
|
|
87
|
+
while (i < tableEnd) {
|
|
88
|
+
const len = ops[i++];
|
|
89
|
+
strings.push(String.fromCodePoint(...ops.slice(i, i + len)));
|
|
90
|
+
i += len;
|
|
91
|
+
}
|
|
92
|
+
const nodes = [];
|
|
93
|
+
while (i < ops.length) {
|
|
94
|
+
const op = ops[i];
|
|
95
|
+
if (op === 1) {
|
|
96
|
+
const id = ops[i + 1];
|
|
97
|
+
const type = ops[i + 2];
|
|
98
|
+
i += 3;
|
|
99
|
+
if (type === 11) {
|
|
100
|
+
nodes.push({ id, type, name: null, key: null, parent: 0 });
|
|
101
|
+
i += 4;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
nodes.push({
|
|
105
|
+
id,
|
|
106
|
+
type,
|
|
107
|
+
name: strings[ops[i + 2]] ?? null,
|
|
108
|
+
key: strings[ops[i + 3]] ?? null,
|
|
109
|
+
parent: ops[i],
|
|
110
|
+
});
|
|
111
|
+
i += 5;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
i += skip(op, ops, i);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return nodes;
|
|
119
|
+
}
|
|
120
|
+
function skip(op, ops, i) {
|
|
121
|
+
if (op === 2)
|
|
122
|
+
return 2 + ops[i + 1];
|
|
123
|
+
if (op === 3)
|
|
124
|
+
return 3 + ops[i + 2];
|
|
125
|
+
if (op === 4)
|
|
126
|
+
return 3;
|
|
127
|
+
if (op === 5)
|
|
128
|
+
return 4;
|
|
129
|
+
if (op === 6)
|
|
130
|
+
return 1;
|
|
131
|
+
if (op === 7)
|
|
132
|
+
return 3;
|
|
133
|
+
if (op === 8)
|
|
134
|
+
return 6 + rects(ops[i + 5]);
|
|
135
|
+
if (op === 9)
|
|
136
|
+
return 2 + ops[i + 1];
|
|
137
|
+
if (op === 10)
|
|
138
|
+
return 3 + ops[i + 2];
|
|
139
|
+
if (op === 11)
|
|
140
|
+
return 3 + rects(ops[i + 2]);
|
|
141
|
+
if (op === 12)
|
|
142
|
+
return suspenders(ops, i);
|
|
143
|
+
if (op === 13)
|
|
144
|
+
return 2;
|
|
145
|
+
return 1;
|
|
146
|
+
}
|
|
147
|
+
function rects(n) {
|
|
148
|
+
return n === -1 ? 0 : n * 4;
|
|
149
|
+
}
|
|
150
|
+
function suspenders(ops, i) {
|
|
151
|
+
let j = i + 2;
|
|
152
|
+
for (let c = 0; c < ops[i + 1]; c++)
|
|
153
|
+
j += 5 + ops[j + 4];
|
|
154
|
+
return j - i;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Runs inside the page. Calls rendererInterface.inspectElement(id) —
|
|
159
|
+
* the same call the DevTools sidebar uses — and formats the dehydrated
|
|
160
|
+
* props/hooks/state into a plain-text summary.
|
|
161
|
+
*/
|
|
162
|
+
function inPageInspect(id) {
|
|
163
|
+
const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
164
|
+
const ri = hook?.rendererInterfaces?.get?.(1);
|
|
165
|
+
if (!ri)
|
|
166
|
+
throw new Error("no React renderer attached");
|
|
167
|
+
if (!ri.hasElementWithId(id))
|
|
168
|
+
throw new Error(`element ${id} not found (page reloaded?)`);
|
|
169
|
+
const result = ri.inspectElement(1, id, null, true);
|
|
170
|
+
if (result?.type !== "full-data")
|
|
171
|
+
throw new Error(`inspect failed: ${result?.type}`);
|
|
172
|
+
const v = result.value;
|
|
173
|
+
const name = ri.getDisplayNameForElementID(id);
|
|
174
|
+
const lines = [`${name} #${id}`];
|
|
175
|
+
if (v.key != null)
|
|
176
|
+
lines.push(`key: ${JSON.stringify(v.key)}`);
|
|
177
|
+
section("props", v.props);
|
|
178
|
+
section("hooks", v.hooks);
|
|
179
|
+
section("state", v.state);
|
|
180
|
+
section("context", v.context);
|
|
181
|
+
if (v.owners?.length) {
|
|
182
|
+
const chain = v.owners.map((o) => o.displayName).join(" > ");
|
|
183
|
+
lines.push(`rendered by: ${chain}`);
|
|
184
|
+
}
|
|
185
|
+
const source = Array.isArray(v.source)
|
|
186
|
+
? [v.source[1], v.source[2], v.source[3]]
|
|
187
|
+
: null;
|
|
188
|
+
return { text: lines.join("\n"), source };
|
|
189
|
+
function section(label, payload) {
|
|
190
|
+
const data = payload?.data ?? payload;
|
|
191
|
+
if (data == null)
|
|
192
|
+
return;
|
|
193
|
+
if (Array.isArray(data)) {
|
|
194
|
+
if (data.length === 0)
|
|
195
|
+
return;
|
|
196
|
+
lines.push(`${label}:`);
|
|
197
|
+
for (const h of data)
|
|
198
|
+
lines.push(` ${hookLine(h)}`);
|
|
199
|
+
}
|
|
200
|
+
else if (typeof data === "object") {
|
|
201
|
+
const entries = Object.entries(data);
|
|
202
|
+
if (entries.length === 0)
|
|
203
|
+
return;
|
|
204
|
+
lines.push(`${label}:`);
|
|
205
|
+
for (const [k, val] of entries)
|
|
206
|
+
lines.push(` ${k}: ${preview(val)}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function hookLine(h) {
|
|
210
|
+
const idx = h.id != null ? `[${h.id}] ` : "";
|
|
211
|
+
const sub = h.subHooks?.length ? ` (${h.subHooks.length} sub)` : "";
|
|
212
|
+
return `${idx}${h.name}: ${preview(h.value)}${sub}`;
|
|
213
|
+
}
|
|
214
|
+
function preview(v) {
|
|
215
|
+
if (v == null)
|
|
216
|
+
return String(v);
|
|
217
|
+
if (typeof v !== "object")
|
|
218
|
+
return JSON.stringify(v);
|
|
219
|
+
const d = v;
|
|
220
|
+
if (d.type === "undefined")
|
|
221
|
+
return "undefined";
|
|
222
|
+
if (d.preview_long)
|
|
223
|
+
return d.preview_long;
|
|
224
|
+
if (d.preview_short)
|
|
225
|
+
return d.preview_short;
|
|
226
|
+
if (Array.isArray(v))
|
|
227
|
+
return `[${v.map(preview).join(", ")}]`;
|
|
228
|
+
const entries = Object.entries(v).map(([k, val]) => `${k}: ${preview(val)}`);
|
|
229
|
+
return `{${entries.join(", ")}}`;
|
|
230
|
+
}
|
|
231
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vercel/next-browser",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Headed Playwright browser with React DevTools pre-loaded",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -9,21 +9,23 @@
|
|
|
9
9
|
},
|
|
10
10
|
"type": "module",
|
|
11
11
|
"engines": {
|
|
12
|
-
"node": ">=
|
|
12
|
+
"node": ">=20"
|
|
13
13
|
},
|
|
14
14
|
"publishConfig": {
|
|
15
15
|
"access": "public"
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
|
-
"
|
|
18
|
+
"dist",
|
|
19
19
|
"extensions"
|
|
20
20
|
],
|
|
21
21
|
"bin": {
|
|
22
|
-
"next-browser": "./
|
|
22
|
+
"next-browser": "./dist/cli.js"
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
25
|
"start": "node src/cli.ts",
|
|
26
|
-
"typecheck": "tsc"
|
|
26
|
+
"typecheck": "tsc",
|
|
27
|
+
"build": "tsc -p tsconfig.build.json",
|
|
28
|
+
"prepack": "pnpm build"
|
|
27
29
|
},
|
|
28
30
|
"dependencies": {
|
|
29
31
|
"@next/playwright": "16.2.0-canary.80",
|