rerender-lens 0.2.0 → 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.
Files changed (63) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +146 -5
  3. package/dist/budget-COBu7jBU.d.cts +64 -0
  4. package/dist/budget-LkNjGtRc.d.ts +64 -0
  5. package/dist/cli.cjs +458 -0
  6. package/dist/cli.cjs.map +1 -0
  7. package/dist/cli.d.cts +4 -0
  8. package/dist/cli.d.ts +4 -0
  9. package/dist/cli.js +452 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/devtools-BHGUUo-p.d.ts +244 -0
  12. package/dist/devtools-DmhWiWcN.d.cts +244 -0
  13. package/dist/index.cjs +652 -23
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +40 -336
  16. package/dist/index.d.ts +40 -336
  17. package/dist/index.js +636 -24
  18. package/dist/index.js.map +1 -1
  19. package/dist/notifiers-BGQtWKfX.d.cts +90 -0
  20. package/dist/notifiers-BjjGSHpp.d.ts +90 -0
  21. package/dist/playwright.cjs +196 -0
  22. package/dist/playwright.cjs.map +1 -0
  23. package/dist/playwright.d.cts +35 -0
  24. package/dist/playwright.d.ts +35 -0
  25. package/dist/playwright.js +184 -0
  26. package/dist/playwright.js.map +1 -0
  27. package/dist/relay.cjs +166 -0
  28. package/dist/relay.cjs.map +1 -0
  29. package/dist/relay.d.cts +41 -0
  30. package/dist/relay.d.ts +41 -0
  31. package/dist/relay.js +161 -0
  32. package/dist/relay.js.map +1 -0
  33. package/dist/rerender-lens.iife.js +1717 -0
  34. package/dist/setup.cjs +1510 -0
  35. package/dist/setup.cjs.map +1 -0
  36. package/dist/setup.d.cts +2 -0
  37. package/dist/setup.d.ts +2 -0
  38. package/dist/setup.js +1508 -0
  39. package/dist/setup.js.map +1 -0
  40. package/dist/types-BzUEVkxJ.d.cts +177 -0
  41. package/dist/types-BzUEVkxJ.d.ts +177 -0
  42. package/dist/vite.cjs +113 -0
  43. package/dist/vite.cjs.map +1 -0
  44. package/dist/vite.d.cts +84 -0
  45. package/dist/vite.d.ts +84 -0
  46. package/dist/vite.js +103 -0
  47. package/dist/vite.js.map +1 -0
  48. package/dist/vitest-setup.cjs +1299 -0
  49. package/dist/vitest-setup.cjs.map +1 -0
  50. package/dist/vitest-setup.d.cts +8 -0
  51. package/dist/vitest-setup.d.ts +8 -0
  52. package/dist/vitest-setup.js +1297 -0
  53. package/dist/vitest-setup.js.map +1 -0
  54. package/dist/vitest.cjs +1360 -0
  55. package/dist/vitest.cjs.map +1 -0
  56. package/dist/vitest.d.cts +52 -0
  57. package/dist/vitest.d.ts +52 -0
  58. package/dist/vitest.js +1351 -0
  59. package/dist/vitest.js.map +1 -0
  60. package/package.json +84 -4
  61. package/panel/panel.css +418 -0
  62. package/panel/panel.html +12 -0
  63. package/panel/panel.js +3325 -0
package/panel/panel.js ADDED
@@ -0,0 +1,3325 @@
1
+ /* Built from extension/src/panel.ts by `npm run build`; do not edit by hand. */
2
+ "use strict";
3
+ (() => {
4
+ // extension/src/panel.ts
5
+ var OPTIONAL_COLUMNS = [
6
+ { key: "places", label: "Places" },
7
+ { key: "lastSeen", label: "Last seen" }
8
+ ];
9
+ var VIRTUAL_THRESHOLD = 200;
10
+ var GRID_ROW_H = 26;
11
+ var PROTOCOL = 2;
12
+ var KIND_LABEL = {
13
+ "deep-equal": "equal by value",
14
+ function: "new function",
15
+ element: "equal element",
16
+ different: "changed",
17
+ added: "added",
18
+ removed: "removed"
19
+ };
20
+ var AVOIDABLE_KINDS = /* @__PURE__ */ new Set(["deep-equal", "function", "element"]);
21
+ var FN_PREFIX = "\u0192 ";
22
+ var MAX_REPORTS = 2e3;
23
+ var MAX_PER_NODE = 200;
24
+ var MAX_COMMITS = 500;
25
+ var ROW_H = 22;
26
+ var ITEM_H = 20;
27
+ var OVERSCAN = 8;
28
+ var FALLBACK_VIEWPORT = 800;
29
+ function el(tag, attrs, children) {
30
+ const node = document.createElement(tag);
31
+ if (attrs) {
32
+ for (const k of Object.keys(attrs)) {
33
+ const v = attrs[k];
34
+ if (k === "class") node.className = String(v);
35
+ else if (k === "text") node.textContent = String(v);
36
+ else if (k.startsWith("on")) {
37
+ if (typeof v === "function") node.addEventListener(k.slice(2), v);
38
+ } else if (v === true) node.setAttribute(k, "");
39
+ else if (v !== void 0 && v !== null && v !== false) node.setAttribute(k, String(v));
40
+ }
41
+ }
42
+ if (children) {
43
+ for (const c of [].concat(children)) if (c != null) node.append(c);
44
+ }
45
+ return node;
46
+ }
47
+ function fmtTime(ms) {
48
+ const d = new Date(ms);
49
+ const p = (n, w) => String(n).padStart(w, "0");
50
+ return `${p(d.getHours(), 2)}:${p(d.getMinutes(), 2)}:${p(d.getSeconds(), 2)}.${p(d.getMilliseconds(), 3)}`;
51
+ }
52
+ var fmtMs = (n) => typeof n === "number" && Number.isFinite(n) ? `${n.toFixed(1)} ms` : "";
53
+ var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
54
+ var componentList = (m) => [...m].map(([c, n]) => `<${c}>${n > 1 ? " \xD7" + n : ""}`).join(", ");
55
+ function changesOf(report) {
56
+ return [].concat(report.propChanges || [], report.stateChanges || [], report.hookChanges || []);
57
+ }
58
+ function summarize(report) {
59
+ const counts = /* @__PURE__ */ new Map();
60
+ for (const c of changesOf(report)) counts.set(c.kind, (counts.get(c.kind) || 0) + 1);
61
+ if (counts.size === 0) return "no changes";
62
+ return [...counts].map(([k, n]) => `${n} ${KIND_LABEL[k] || k}`).join(", ");
63
+ }
64
+ var isRecord = (v) => typeof v === "object" && v !== null;
65
+ function normalizeReport(p) {
66
+ if (!isRecord(p) || typeof p.component !== "string") return null;
67
+ const arr = (x) => Array.isArray(x) ? x.filter((c) => isRecord(c) && typeof c.path === "string") : [];
68
+ const props = isRecord(p.props) ? p.props : {};
69
+ const parent = isRecord(p.parent) && typeof p.parent.name === "string" ? { name: p.parent.name, trigger: typeof p.parent.trigger === "string" ? p.parent.trigger : "parent" } : null;
70
+ const r = {
71
+ component: p.component,
72
+ instanceId: typeof p.instanceId === "number" ? p.instanceId : 0,
73
+ commitId: typeof p.commitId === "number" ? p.commitId : 0,
74
+ renderCount: typeof p.renderCount === "number" ? p.renderCount : 0,
75
+ trigger: typeof p.trigger === "string" ? p.trigger : "parent",
76
+ avoidable: !!p.avoidable,
77
+ props: { prev: isRecord(props.prev) ? props.prev : {}, next: isRecord(props.next) ? props.next : {} },
78
+ propChanges: arr(p.propChanges),
79
+ stateChanges: arr(p.stateChanges),
80
+ hookChanges: arr(p.hookChanges),
81
+ parent,
82
+ owner: typeof p.owner === "string" ? p.owner : null,
83
+ path: Array.isArray(p.path) ? p.path.filter((x) => typeof x === "string") : [],
84
+ reasons: Array.isArray(p.reasons) ? p.reasons.filter((x) => typeof x === "string") : [],
85
+ time: typeof p.time === "number" ? p.time : 0,
86
+ receivedAt: typeof p.receivedAt === "number" ? p.receivedAt : 0
87
+ };
88
+ if (typeof p.selfDuration === "number") r.selfDuration = p.selfDuration;
89
+ if (typeof p.treeDuration === "number") r.treeDuration = p.treeDuration;
90
+ if (typeof p.memoized === "boolean") r.memoized = p.memoized;
91
+ if (typeof p.commitPriority === "string") r.commitPriority = p.commitPriority;
92
+ if (Array.isArray(p.hookState)) r.hookState = p.hookState.filter((h) => isRecord(h) && typeof h.path === "string");
93
+ if (Array.isArray(p.contexts)) r.contexts = p.contexts.filter((c) => isRecord(c) && typeof c.name === "string");
94
+ if (isRecord(p.state)) r.state = p.state;
95
+ if (Array.isArray(p.updaters)) r.updaters = p.updaters.filter((u) => typeof u === "string");
96
+ if (p.commitCause === "effect-after-commit" || p.commitCause === "suspense-resolved") r.commitCause = p.commitCause;
97
+ if (typeof p.afterCommit === "number") r.afterCommit = p.afterCommit;
98
+ if (typeof p.key === "string") r.key = p.key;
99
+ if (isRecord(p.source) && typeof p.source.fileName === "string") r.source = p.source;
100
+ return r;
101
+ }
102
+ function valueNode(v, depth = 0) {
103
+ if (v === null || v === void 0) return el("span", { class: "v nil", text: String(v) });
104
+ const t = typeof v;
105
+ if (typeof v === "string") {
106
+ if (v.startsWith(FN_PREFIX)) return el("span", { class: "v fn", text: v });
107
+ if (/^<[^>]+>$/.test(v)) return el("span", { class: "v", text: v });
108
+ return el("span", { class: "v str", text: JSON.stringify(v) });
109
+ }
110
+ if (t === "number" || t === "bigint") return el("span", { class: "v num", text: String(v) });
111
+ if (t === "boolean") return el("span", { class: "v bool", text: String(v) });
112
+ if (Array.isArray(v)) {
113
+ const short = v.length <= 4 && v.every((x) => typeof x !== "object" || x === null);
114
+ if (short) {
115
+ const s = el("span", { class: "v" }, "[");
116
+ v.forEach((x, i) => {
117
+ if (i) s.append(", ");
118
+ s.append(valueNode(x, depth + 1));
119
+ });
120
+ s.append("]");
121
+ return s;
122
+ }
123
+ return objectDetails(`Array(${v.length})`, v);
124
+ }
125
+ const o = v;
126
+ if (o.$type === "element") {
127
+ const props = isRecord(o.props) ? o.props : {};
128
+ const label2 = `<${String(o.name)}${o.key !== void 0 ? ` key=${JSON.stringify(o.key)}` : ""}>`;
129
+ return Object.keys(props).length ? objectDetails(label2, props) : el("span", { class: "v", text: label2 });
130
+ }
131
+ if (o.$type === "Date") return el("span", { class: "v", text: `Date(${String(o.value)})` });
132
+ if (o.$type === "RegExp") return el("span", { class: "v", text: String(o.value) });
133
+ if (o.$type === "Map" && Array.isArray(o.entries)) return objectDetails(`Map(${o.entries.length})`, o.entries);
134
+ if (o.$type === "Set" && Array.isArray(o.values)) return objectDetails(`Set(${o.values.length})`, o.values);
135
+ const keys = Object.keys(o).filter((k) => k !== "$type");
136
+ const label = `${o.$type ? String(o.$type) + " " : ""}{${keys.slice(0, 3).join(", ")}${keys.length > 3 ? ", ..." : ""}}`;
137
+ return objectDetails(label, o);
138
+ }
139
+ function objectDetails(label, obj) {
140
+ const d = el("details", { class: "obj" }, [el("summary", { text: label })]);
141
+ d.addEventListener(
142
+ "toggle",
143
+ () => {
144
+ if (d.open && !d.querySelector("pre")) d.append(el("pre", { text: JSON.stringify(obj, null, 2) }));
145
+ },
146
+ { once: true }
147
+ );
148
+ return d;
149
+ }
150
+ function firstDifferentPath(a, b, base = "") {
151
+ if (a === b) return null;
152
+ if (!isRecord(a) || !isRecord(b)) return base || "(value)";
153
+ if (Array.isArray(a) !== Array.isArray(b)) return base || "(value)";
154
+ if (Array.isArray(a) && Array.isArray(b)) {
155
+ if (a.length !== b.length) return base ? `${base}.length` : "length";
156
+ for (let i = 0; i < a.length; i++) {
157
+ const p = firstDifferentPath(a[i], b[i], `${base}[${i}]`);
158
+ if (p) return p;
159
+ }
160
+ return null;
161
+ }
162
+ const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
163
+ for (const k of keys) {
164
+ const p = firstDifferentPath(a[k], b[k], base ? `${base}.${k}` : k);
165
+ if (p) return p;
166
+ }
167
+ return null;
168
+ }
169
+ function diffLeaves(a, b, limit = 20, base = "", out = []) {
170
+ if (out.length >= limit || a === b) return out;
171
+ if (!isRecord(a) || !isRecord(b) || Array.isArray(a) !== Array.isArray(b)) {
172
+ out.push({ path: base || "(value)", prev: a, next: b });
173
+ return out;
174
+ }
175
+ if (Array.isArray(a) && Array.isArray(b)) {
176
+ const n = Math.max(a.length, b.length);
177
+ for (let i = 0; i < n && out.length < limit; i++) diffLeaves(a[i], b[i], limit, `${base}[${i}]`, out);
178
+ return out;
179
+ }
180
+ for (const k of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])) {
181
+ if (out.length >= limit) break;
182
+ diffLeaves(a[k], b[k], limit, base ? `${base}.${k}` : k, out);
183
+ }
184
+ return out;
185
+ }
186
+ function shortValue(v, max = 60) {
187
+ let s;
188
+ try {
189
+ s = JSON.stringify(v);
190
+ } catch {
191
+ s = String(v);
192
+ }
193
+ if (s === void 0) s = String(v);
194
+ return s.length > max ? s.slice(0, max - 3) + "..." : s;
195
+ }
196
+ var identifier = (name) => /^[A-Za-z_$][\w$]*$/.test(name) ? name : "value";
197
+ function fixesFor(r) {
198
+ const out = [];
199
+ const ownerName = r.owner || r.parent && r.parent.name || null;
200
+ const changes = changesOf(r);
201
+ const avoidableProps = (r.propChanges || []).filter((c) => AVOIDABLE_KINDS.has(c.kind));
202
+ if (r.avoidable && (changes.length === 0 || r.memoized === false)) {
203
+ const identical = changes.length === 0;
204
+ out.push({
205
+ kind: "memo",
206
+ owner: r.component,
207
+ target: r.component,
208
+ prop: null,
209
+ label: `Wrap <${r.component}> in React.memo`,
210
+ detail: identical ? `<${r.component}> re-rendered with identical props because <${r.parent && r.parent.name || "its parent"}> re-rendered.` : `<${r.component}> is not memoized: fixing its props alone will not stop the re-render.`,
211
+ snippet: `// ${r.component}
212
+ import { memo } from 'react';
213
+
214
+ export const ${r.component} = memo(function ${r.component}(props) {
215
+ // ...
216
+ });
217
+ // class components: extend PureComponent instead`
218
+ });
219
+ }
220
+ for (const c of avoidableProps) {
221
+ const owner = ownerName || "?";
222
+ const root = c.path.split(/[.[]/)[0] || c.path;
223
+ const id = identifier(root);
224
+ if (root === "children" && (c.kind === "element" || c.kind === "deep-equal")) {
225
+ out.push({
226
+ kind: "children",
227
+ owner,
228
+ target: r.component,
229
+ prop: "children",
230
+ label: `memoize children of <${r.component}> in <${owner}>`,
231
+ detail: `<${owner}> re-creates the children of <${r.component}> on every render; they have the same types and props each time.`,
232
+ snippet: `// ${owner}
233
+ import { useMemo } from 'react';
234
+
235
+ const children = useMemo(() => (
236
+ <>{/* the same elements */}</>
237
+ ), [/* deps */]);
238
+
239
+ <${r.component}>{children}</${r.component}>
240
+
241
+ // static children: hoist them to module scope
242
+ const STATIC = <em>hi</em>;`
243
+ });
244
+ continue;
245
+ }
246
+ if (c.kind === "function") {
247
+ out.push({
248
+ kind: "useCallback",
249
+ owner,
250
+ target: r.component,
251
+ prop: root,
252
+ label: `useCallback(${root}) in <${owner}>`,
253
+ detail: `prop "${c.path}" of <${r.component}> is a new function on every render of <${owner}>.`,
254
+ snippet: `// ${owner}
255
+ import { useCallback } from 'react';
256
+
257
+ const ${id} = useCallback((/* args */) => {
258
+ // ...
259
+ }, [/* deps */]);
260
+
261
+ <${r.component} ${root}={${id}} />`
262
+ });
263
+ } else if (c.kind === "element") {
264
+ out.push({
265
+ kind: "useMemoElement",
266
+ owner,
267
+ target: r.component,
268
+ prop: root,
269
+ label: `memoize element prop ${root} in <${owner}>`,
270
+ detail: `prop "${c.path}" of <${r.component}> is a new element with the same type and props on every render of <${owner}>.`,
271
+ snippet: `// ${owner}
272
+ import { useMemo } from 'react';
273
+
274
+ const ${id} = useMemo(() => ${shortValue(c.next, 40)}, [/* deps */]);
275
+ // or pass it as children from a component that does not re-render`
276
+ });
277
+ } else {
278
+ const isArray = Array.isArray(c.next);
279
+ out.push({
280
+ kind: "useMemo",
281
+ owner,
282
+ target: r.component,
283
+ prop: root,
284
+ label: `useMemo(${root}) in <${owner}>`,
285
+ detail: `prop "${c.path}" of <${r.component}> is a new ${isArray ? "array" : "object"} with the same contents on every render of <${owner}>.`,
286
+ snippet: `// ${owner}
287
+ import { useMemo } from 'react';
288
+
289
+ const ${id} = useMemo(() => (${shortValue(c.next, 80)}), [/* deps */]);
290
+
291
+ // or, when it never changes, hoist it to module scope:
292
+ const ${id.toUpperCase()} = ${shortValue(c.next, 80)};`
293
+ });
294
+ }
295
+ }
296
+ for (const c of [].concat(r.stateChanges || [], r.hookChanges || [])) {
297
+ const isContext = c.hook === "useContext" || /^useContext/.test(c.path);
298
+ const ctxName = isContext ? (/useContext\((.*)\)/.exec(c.path) || [])[1] || "Context" : "";
299
+ const providerOwner = isContext && c.provider && c.provider.component ? c.provider.component : null;
300
+ if (isContext && c.kind === "different" && c.changedKeys && typeof c.totalKeys === "number" && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys) {
301
+ out.push({
302
+ kind: "splitContext",
303
+ owner: providerOwner || `${ctxName}.Provider`,
304
+ target: r.component,
305
+ prop: ctxName,
306
+ label: `split ${ctxName}${providerOwner ? ` in <${providerOwner}>` : ""}: only ${c.changedKeys.join(", ")} changed`,
307
+ detail: `${c.changedKeys.map((k) => `"${k}"`).join(", ")} of ${c.totalKeys} keys changed in ${ctxName}, yet every consumer (like <${r.component}>) re-rendered. Consumers that read the other keys re-render for nothing.`,
308
+ snippet: `// ${providerOwner || "Provider"}
309
+ // one context per independently-changing slice
310
+ const ${identifier(ctxName)}Static = createContext(...);
311
+ const ${identifier(ctxName)}${c.changedKeys.map((k) => k[0].toUpperCase() + k.slice(1)).join("")} = createContext(...);
312
+
313
+ // or keep one context and let consumers select a slice:
314
+ const ${c.changedKeys[0]} = useContextSelector(${ctxName}, (v) => v.${c.changedKeys[0]});`
315
+ });
316
+ continue;
317
+ }
318
+ if (!AVOIDABLE_KINDS.has(c.kind)) continue;
319
+ if (isContext) {
320
+ const name = ctxName;
321
+ out.push({
322
+ kind: "contextValue",
323
+ owner: providerOwner || `${name}.Provider`,
324
+ target: r.component,
325
+ prop: name,
326
+ label: `memoize the ${name} provider value${providerOwner ? ` in <${providerOwner}>` : ""}`,
327
+ detail: `<${r.component}> re-rendered because ${name} produced a new value that is deep-equal to the previous one${providerOwner ? ` (Provider rendered by <${providerOwner}>)` : ""}.`,
328
+ snippet: `// ${providerOwner || `where <${name}.Provider> is rendered`}
329
+ const value = useMemo(() => ({ /* ... */ }), [/* deps */]);
330
+ <${name}.Provider value={value}>`
331
+ });
332
+ } else if (c.hook === "useSyncExternalStore") {
333
+ const chain = c.custom || [];
334
+ const redux = chain.some((n) => /^use(App)?Selector$/.test(n));
335
+ const zustand = !redux && chain.some((n) => /^use[A-Z]\w*Store$/.test(n) || n === "useStore" || n === "useBoundStore");
336
+ const via = chain.length ? ` via ${chain.join(" \u203A ")}` : "";
337
+ out.push({
338
+ kind: "storeSnapshot",
339
+ owner: r.component,
340
+ target: r.component,
341
+ prop: c.path,
342
+ label: redux ? `memoize the selector in <${r.component}>` : zustand ? `useShallow in <${r.component}>` : `stable getSnapshot in <${r.component}>`,
343
+ detail: redux ? `the selector${via} returns a new object on every call, so the component re-renders on every store change.` : zustand ? `the store selector${via} returns a new object on every call, so the component re-renders on every store change.` : `${c.path}${via} returned a new reference with the same contents; getSnapshot must return a cached value.`,
344
+ snippet: redux ? `// ${r.component}
345
+ import { shallowEqual } from 'react-redux';
346
+ const slice = useSelector(selectSlice, shallowEqual);
347
+ // or memoize: const selectSlice = createSelector([selectA, selectB], (a, b) => ({ a, b }));` : zustand ? `// ${r.component}
348
+ import { useShallow } from 'zustand/react/shallow';
349
+ const { a, b } = useStore(useShallow((s) => ({ a: s.a, b: s.b })));
350
+ // or select a primitive: const a = useStore((s) => s.a);` : `// ${r.component}
351
+ // getSnapshot must return the same reference while the data is unchanged
352
+ const snapshot = useSyncExternalStore(subscribe, store.getSnapshot /* cached */);`
353
+ });
354
+ } else {
355
+ out.push({
356
+ kind: "bailout",
357
+ owner: r.component,
358
+ target: r.component,
359
+ prop: c.path,
360
+ label: `bail out before setting ${c.path} in <${r.component}>`,
361
+ detail: `${c.path} was set to a value deep-equal to the current one (new reference).`,
362
+ snippet: `// ${r.component}
363
+ setState((prev) => (deepEqual(prev, next) ? prev : next));`
364
+ });
365
+ }
366
+ }
367
+ return out;
368
+ }
369
+ var fixKey = (f) => `${f.kind}|${f.owner}|${f.prop || f.target}`;
370
+ function rankFixes(reports) {
371
+ const byKey = /* @__PURE__ */ new Map();
372
+ for (const r of reports) {
373
+ if (!r.avoidable) continue;
374
+ for (const f of fixesFor(r)) {
375
+ const k = fixKey(f);
376
+ let agg = byKey.get(k);
377
+ if (!agg) {
378
+ agg = { ...f, key: k, count: 0, components: /* @__PURE__ */ new Map(), reports: [] };
379
+ byKey.set(k, agg);
380
+ }
381
+ agg.count++;
382
+ agg.components.set(r.component, (agg.components.get(r.component) || 0) + 1);
383
+ if (agg.reports.length < 50) agg.reports.push(r);
384
+ }
385
+ }
386
+ return [...byKey.values()].sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
387
+ }
388
+ var isAncestorReport = (anc, r) => anc.path.length < r.path.length && r.path[anc.path.length] === anc.component && anc.path.every((p, i) => r.path[i] === p);
389
+ function rootCauseOf(r, commitReports) {
390
+ let cur = r;
391
+ const seen = /* @__PURE__ */ new Set([r]);
392
+ while (cur.trigger === "parent" && cur.parent) {
393
+ const parent = cur.parent;
394
+ const p = commitReports.find((x) => x.component === parent.name && isAncestorReport(x, cur));
395
+ if (!p || seen.has(p)) return { name: parent.name, trigger: parent.trigger, report: null };
396
+ seen.add(p);
397
+ cur = p;
398
+ }
399
+ return cur === r ? null : { name: cur.component, trigger: cur.trigger, report: cur };
400
+ }
401
+ function analyzeCommit(reports) {
402
+ const roots = /* @__PURE__ */ new Map();
403
+ let avoidable = 0;
404
+ let wasted = 0;
405
+ for (const r of reports) {
406
+ if (!r.avoidable) continue;
407
+ avoidable++;
408
+ if (typeof r.selfDuration === "number") wasted += r.selfDuration;
409
+ const root = rootCauseOf(r, reports);
410
+ const name = root ? root.name : r.parent && r.parent.name || "(unknown)";
411
+ const trigger = root ? root.trigger : r.parent && r.parent.trigger || "parent";
412
+ let agg = roots.get(name);
413
+ if (!agg) {
414
+ agg = { name, trigger, count: 0, components: /* @__PURE__ */ new Map() };
415
+ roots.set(name, agg);
416
+ }
417
+ agg.count++;
418
+ agg.components.set(r.component, (agg.components.get(r.component) || 0) + 1);
419
+ }
420
+ const first = reports[0];
421
+ return {
422
+ id: first ? first.commitId : 0,
423
+ receivedAt: first ? first.receivedAt : 0,
424
+ total: reports.length,
425
+ avoidable,
426
+ wasted,
427
+ roots: [...roots.values()].sort((a, b) => b.count - a.count),
428
+ contexts: contextAttribution(reports),
429
+ fixes: rankFixes(reports),
430
+ reports
431
+ };
432
+ }
433
+ function contextAttribution(reports) {
434
+ const byCtx = /* @__PURE__ */ new Map();
435
+ for (const r of reports) {
436
+ for (const c of r.hookChanges || []) {
437
+ if (c.hook !== "useContext" && !/^useContext/.test(c.path)) continue;
438
+ const m = /useContext\((.*)\)/.exec(c.path);
439
+ const name = m && m[1] ? m[1] : c.path;
440
+ let agg = byCtx.get(name);
441
+ if (!agg) {
442
+ agg = { name, consumers: 0, avoidable: 0, components: /* @__PURE__ */ new Map(), commits: /* @__PURE__ */ new Set(), providers: /* @__PURE__ */ new Map(), changedKeys: /* @__PURE__ */ new Set(), totalKeys: 0 };
443
+ byCtx.set(name, agg);
444
+ }
445
+ agg.consumers++;
446
+ if (AVOIDABLE_KINDS.has(c.kind)) agg.avoidable++;
447
+ agg.components.set(r.component, (agg.components.get(r.component) || 0) + 1);
448
+ agg.commits.add(r.commitId);
449
+ if (c.provider && c.provider.component) agg.providers.set(c.provider.component, (agg.providers.get(c.provider.component) || 0) + 1);
450
+ if (c.changedKeys) for (const k of c.changedKeys) agg.changedKeys.add(k);
451
+ if (typeof c.totalKeys === "number") agg.totalKeys = Math.max(agg.totalKeys, c.totalKeys);
452
+ }
453
+ }
454
+ return [...byCtx.values()].sort((a, b) => b.consumers - a.consumers);
455
+ }
456
+ function cascadeTree(reports) {
457
+ const root = { name: "", children: /* @__PURE__ */ new Map(), report: null };
458
+ for (const r of reports) {
459
+ let node = root;
460
+ for (const seg of r.path.concat([r.component])) {
461
+ let next = node.children.get(seg);
462
+ if (!next) {
463
+ next = { name: seg, children: /* @__PURE__ */ new Map(), report: null };
464
+ node.children.set(seg, next);
465
+ }
466
+ node = next;
467
+ }
468
+ if (!node.report || r.avoidable) node.report = r;
469
+ node.count = (node.count || 0) + 1;
470
+ if (r.avoidable) node.avoidable = (node.avoidable || 0) + 1;
471
+ }
472
+ return root;
473
+ }
474
+ function rootCauseSummary(name, commits) {
475
+ const out = { name, trigger: "parent", commits: [], total: 0, components: /* @__PURE__ */ new Map(), fixes: [] };
476
+ const affected = [];
477
+ for (const [key, reports] of commits) {
478
+ const analysis = analyzeCommit(reports);
479
+ const root = analysis.roots.find((x) => x.name === name);
480
+ if (!root) continue;
481
+ out.trigger = root.trigger;
482
+ out.commits.push({ key, analysis, count: root.count, components: root.components });
483
+ out.total += root.count;
484
+ for (const [c, n] of root.components) out.components.set(c, (out.components.get(c) || 0) + n);
485
+ for (const r of reports) {
486
+ if (!r.avoidable) continue;
487
+ const rc = rootCauseOf(r, reports);
488
+ if ((rc ? rc.name : r.parent && r.parent.name) === name) affected.push(r);
489
+ }
490
+ }
491
+ out.commits.reverse();
492
+ out.fixes = rankFixes(affected);
493
+ return out;
494
+ }
495
+ function summarizeSession(session, reports) {
496
+ var _a;
497
+ const byComponent = {};
498
+ let avoidable = 0;
499
+ let wasted = 0;
500
+ for (const r of reports) {
501
+ const c = byComponent[_a = r.component] || (byComponent[_a] = { total: 0, avoidable: 0, wasted: 0 });
502
+ c.total++;
503
+ if (r.avoidable) {
504
+ c.avoidable++;
505
+ avoidable++;
506
+ if (typeof r.selfDuration === "number") {
507
+ c.wasted += r.selfDuration;
508
+ wasted += r.selfDuration;
509
+ }
510
+ }
511
+ }
512
+ return {
513
+ id: session.id,
514
+ name: session.name,
515
+ startedAt: session.startedAt,
516
+ endedAt: session.endedAt,
517
+ total: reports.length,
518
+ avoidable,
519
+ wasted,
520
+ byComponent,
521
+ fixes: rankFixes(reports).map((f) => ({ key: f.key, label: f.label, count: f.count }))
522
+ };
523
+ }
524
+ function compareSessions(before, after) {
525
+ const names = /* @__PURE__ */ new Set([...Object.keys(before.byComponent), ...Object.keys(after.byComponent)]);
526
+ const rows = [];
527
+ for (const component of names) {
528
+ const b = before.byComponent[component]?.avoidable || 0;
529
+ const a = after.byComponent[component]?.avoidable || 0;
530
+ if (b || a) rows.push({ component, before: b, after: a, delta: a - b });
531
+ }
532
+ rows.sort((x, y) => x.delta - y.delta || y.before - x.before || x.component.localeCompare(y.component));
533
+ const afterKeys = new Set(after.fixes.map((f) => f.key));
534
+ const beforeKeys = new Set(before.fixes.map((f) => f.key));
535
+ return {
536
+ before,
537
+ after,
538
+ rows,
539
+ total: { before: before.total, after: after.total, delta: after.total - before.total },
540
+ avoidable: { before: before.avoidable, after: after.avoidable, delta: after.avoidable - before.avoidable },
541
+ wasted: { before: before.wasted, after: after.wasted, delta: after.wasted - before.wasted },
542
+ resolvedFixes: before.fixes.filter((f) => !afterKeys.has(f.key)),
543
+ newFixes: after.fixes.filter((f) => !beforeKeys.has(f.key))
544
+ };
545
+ }
546
+ var PRIORITY_LABEL = { immediate: "discrete input", "user-blocking": "continuous input", normal: "transition / async", low: "low", idle: "idle" };
547
+ var b64url = (bytes) => {
548
+ let s = "";
549
+ for (const b of bytes) s += String.fromCharCode(b);
550
+ return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
551
+ };
552
+ var unb64url = (s) => Uint8Array.from(atob(s.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
553
+ async function transformBytes(bytes, transform) {
554
+ const source = new ReadableStream({
555
+ start(controller) {
556
+ controller.enqueue(bytes);
557
+ controller.close();
558
+ }
559
+ });
560
+ const reader = source.pipeThrough(transform).getReader();
561
+ const chunks = [];
562
+ let total = 0;
563
+ for (; ; ) {
564
+ const { value, done } = await reader.read();
565
+ if (done) break;
566
+ chunks.push(value);
567
+ total += value.length;
568
+ }
569
+ const out = new Uint8Array(total);
570
+ let offset = 0;
571
+ for (const c of chunks) {
572
+ out.set(c, offset);
573
+ offset += c.length;
574
+ }
575
+ return out;
576
+ }
577
+ async function encodeShare(report) {
578
+ const json = JSON.stringify(report);
579
+ const bytes = new TextEncoder().encode(json);
580
+ if (typeof CompressionStream === "function") {
581
+ try {
582
+ return "d." + b64url(await transformBytes(bytes, new CompressionStream("deflate-raw")));
583
+ } catch {
584
+ }
585
+ }
586
+ return "j." + b64url(bytes);
587
+ }
588
+ async function decodeShare(value) {
589
+ const [kind, data] = value.split(".", 2);
590
+ if (!data) return null;
591
+ let bytes = unb64url(data);
592
+ if (kind === "d") {
593
+ if (typeof DecompressionStream !== "function") return null;
594
+ try {
595
+ bytes = await transformBytes(bytes, new DecompressionStream("deflate-raw"));
596
+ } catch {
597
+ return null;
598
+ }
599
+ }
600
+ try {
601
+ return normalizeReport(JSON.parse(new TextDecoder().decode(bytes)));
602
+ } catch {
603
+ return null;
604
+ }
605
+ }
606
+ function sourceContext(text, line, around = 3) {
607
+ const lines = text.split("\n");
608
+ const from = Math.max(1, line - around);
609
+ const to = Math.min(lines.length, line + around);
610
+ const out = [];
611
+ for (let n = from; n <= to; n++) out.push({ n, text: lines[n - 1] ?? "", hit: n === line });
612
+ return out;
613
+ }
614
+ function reportToMarkdown(r) {
615
+ const lines = [];
616
+ lines.push(`### <${r.component}> ${r.avoidable ? "avoidable re-render" : `re-render (${r.trigger})`} #${r.renderCount}`);
617
+ lines.push("");
618
+ for (const x of r.reasons || []) lines.push(`- ${x}`);
619
+ if (r.path && r.path.length) lines.push("", `**Path:** ${r.path.concat([r.component]).join(" > ")}`);
620
+ if (r.parent) lines.push(`**Triggered by:** <${r.parent.name}> (${r.parent.trigger})`);
621
+ if (r.owner) lines.push(`**Created by:** <${r.owner}>`);
622
+ if (r.source) lines.push(`**Source:** ${r.source.fileName}${r.source.lineNumber ? ":" + r.source.lineNumber : ""}`);
623
+ const changes = changesOf(r);
624
+ if (changes.length) {
625
+ lines.push("", "| path | kind | prev | next |", "| --- | --- | --- | --- |");
626
+ for (const c of changes) lines.push(`| ${c.path} | ${KIND_LABEL[c.kind] || c.kind} | \`${shortValue(c.prev, 40)}\` | \`${shortValue(c.next, 40)}\` |`);
627
+ }
628
+ if (r.hookState && r.hookState.length) {
629
+ lines.push("", "**Hooks**", "");
630
+ for (const h of r.hookState) lines.push(`- ${h.path}: \`${shortValue(h.value, 60)}\``);
631
+ }
632
+ if (r.state && Object.keys(r.state).length) lines.push("", `**State:** \`${shortValue(r.state, 120)}\``);
633
+ if (r.contexts && r.contexts.length) {
634
+ lines.push("", "**Contexts**", "");
635
+ for (const c of r.contexts) lines.push(`- ${c.name}: \`${shortValue(c.value, 60)}\``);
636
+ }
637
+ const fixes = fixesFor(r);
638
+ if (fixes.length) {
639
+ lines.push("", "**Fix**", "");
640
+ for (const f of fixes) lines.push(`- ${f.label}`);
641
+ lines.push("", "```jsx", fixes[0].snippet, "```");
642
+ }
643
+ return lines.join("\n");
644
+ }
645
+ function changeRow(label, c, suffix = "") {
646
+ const tr = el("tr", { class: "changed" + (AVOIDABLE_KINDS.has(c.kind) ? "" : " real") });
647
+ tr.append(el("td", { class: "k", text: label }));
648
+ const td = el("td");
649
+ td.append(valueNode(c.prev), el("span", { class: "arrow", text: "\u2192" }));
650
+ td.append(c.kind === "removed" ? el("span", { class: "v nil", text: "(removed)" }) : valueNode(c.next));
651
+ let kind = (KIND_LABEL[c.kind] || c.kind) + suffix;
652
+ const objectDiff = c.kind === "different" && isRecord(c.prev) && isRecord(c.next);
653
+ if (objectDiff) {
654
+ const p = firstDifferentPath(c.prev, c.next, c.path);
655
+ if (p) kind += ` at ${p}`;
656
+ }
657
+ td.append(el("span", { class: "kind", text: kind }));
658
+ if (objectDiff) {
659
+ const leaves = diffLeaves(c.prev, c.next, 20, c.path);
660
+ if (leaves.length) {
661
+ const d = el("details", { class: "diff" }, [el("summary", { text: `${leaves.length}${leaves.length >= 20 ? "+" : ""} differing ${leaves.length === 1 ? "leaf" : "leaves"}` })]);
662
+ const table = el("table", { class: "kv leaves" });
663
+ for (const leaf of leaves) {
664
+ const row = el("tr");
665
+ row.append(el("td", { class: "k", text: leaf.path }));
666
+ const cell = el("td");
667
+ cell.append(valueNode(leaf.prev), el("span", { class: "arrow", text: "\u2192" }), valueNode(leaf.next));
668
+ row.append(cell);
669
+ table.append(row);
670
+ }
671
+ d.append(table);
672
+ td.append(d);
673
+ }
674
+ }
675
+ tr.append(td);
676
+ return tr;
677
+ }
678
+ function kvSection(title, next, changes) {
679
+ const byKey = new Map(changes.map((c) => [c.path.split(/[.[]/)[0], c]));
680
+ const table = el("table", { class: "kv" });
681
+ for (const k of Object.keys(next || {})) {
682
+ const c = byKey.get(k);
683
+ if (c) {
684
+ table.append(changeRow(k, c, c.path !== k ? ` at ${c.path}` : ""));
685
+ } else {
686
+ const tr = el("tr");
687
+ tr.append(el("td", { class: "k", text: k }));
688
+ const td = el("td");
689
+ td.append(valueNode(next[k]));
690
+ tr.append(td);
691
+ table.append(tr);
692
+ }
693
+ }
694
+ for (const c of changes) if (c.kind === "removed") table.append(changeRow(c.path, c));
695
+ if (!table.children.length) table.append(el("tr", null, [el("td", { class: "v nil", text: "no props" })]));
696
+ return el("div", { class: "section" }, [el("h3", { text: title }), table]);
697
+ }
698
+ function sourceLabel(src) {
699
+ const file = src.fileName.replace(/^https?:\/\/[^/]+/, "").replace(/\?.*$/, "");
700
+ return `${file}${src.lineNumber ? ":" + src.lineNumber : ""}`;
701
+ }
702
+ function reportView(r, actions = {}) {
703
+ const frag = document.createDocumentFragment();
704
+ const duration = typeof r.selfDuration === "number" ? ` \xB7 ${fmtMs(r.selfDuration)} self${typeof r.treeDuration === "number" && r.treeDuration > r.selfDuration ? `, ${fmtMs(r.treeDuration)} with children` : ""}` : "";
705
+ const head = el("div", { class: "section" }, [
706
+ el("h3", { text: "Why did this render?" }),
707
+ el("div", null, [
708
+ el("span", { class: "verdict " + (r.avoidable ? "avoid" : "ok"), text: r.avoidable ? "Avoidable re-render" : `Re-render (${r.trigger})` }),
709
+ el("span", { class: "meta", text: ` #${r.renderCount} \xB7 ${summarize(r)}${duration}` })
710
+ ]),
711
+ el("ul", { class: "reasons" }, (r.reasons || []).map((x) => el("li", { text: x })))
712
+ ]);
713
+ if (!actions.compact) {
714
+ const bar = el("div", { class: "actions" });
715
+ const src = r.source;
716
+ if (src && actions.openSource) {
717
+ const open = actions.openSource;
718
+ bar.append(el("button", { title: src.fileName, onclick: () => open(src) }, `\u2197 ${sourceLabel(src)}`));
719
+ } else if (src) bar.append(el("span", { class: "meta", title: src.fileName, text: sourceLabel(src) }));
720
+ if (actions.highlight && r.instanceId) {
721
+ const highlight = actions.highlight;
722
+ bar.append(el("button", { onclick: () => highlight(r.instanceId) }, "\u25A3 Highlight"));
723
+ }
724
+ if (actions.copy) {
725
+ const copy = actions.copy;
726
+ bar.append(el("button", { onclick: () => copy(reportToMarkdown(r)) }, "\u2398 Copy as Markdown"));
727
+ }
728
+ if (actions.share) {
729
+ const share = actions.share;
730
+ bar.append(el("button", { title: "Copy a link that opens this report in the panel", onclick: () => share(r) }, "\u{1F517} Copy link"));
731
+ }
732
+ if (bar.children.length) head.append(bar);
733
+ const loc = r.source;
734
+ if (loc && loc.lineNumber && actions.readSource) {
735
+ const box = el("pre", { class: "source-context", text: "loading source\u2026" });
736
+ head.append(box);
737
+ const line = loc.lineNumber;
738
+ void actions.readSource(loc.fileName).then((text) => {
739
+ box.textContent = "";
740
+ if (!text) {
741
+ box.textContent = "source not available";
742
+ return;
743
+ }
744
+ for (const l of sourceContext(text, line)) box.append(el("span", { class: "line" + (l.hit ? " hit" : "") }, [el("span", { class: "ln", text: String(l.n).padStart(4) }), " ", l.text, "\n"]));
745
+ }).catch(() => {
746
+ box.textContent = "source not available";
747
+ });
748
+ }
749
+ }
750
+ frag.append(head);
751
+ const by = el("div", { class: "section" }, [el("h3", { text: "Rendered by" })]);
752
+ const crumbs = el("div", { class: "crumbs" });
753
+ const parts = [].concat(r.path || []);
754
+ parts.forEach((p, i) => {
755
+ if (i) crumbs.append(" \u203A ");
756
+ crumbs.append(p);
757
+ });
758
+ if (parts.length) crumbs.append(" \u203A ");
759
+ crumbs.append(el("b", { text: r.component }));
760
+ by.append(crumbs);
761
+ if (r.parent) by.append(el("div", { text: `Triggered by <${r.parent.name}> (${r.parent.trigger})` }));
762
+ else by.append(el("div", { text: "Update started in this component" }));
763
+ if (r.owner) by.append(el("div", { class: "meta", text: `Created by <${r.owner}>` }));
764
+ if (r.memoized === false) by.append(el("div", { class: "meta", text: "Not memoized (re-renders whenever its parent does)" }));
765
+ else if (r.memoized === true) by.append(el("div", { class: "meta", text: "Memoized (React.memo / PureComponent)" }));
766
+ if (r.updaters && r.updaters.length) by.append(el("div", { text: `Update scheduled by ${r.updaters.map((u) => `<${u}>`).join(", ")}` }));
767
+ if (r.commitCause === "effect-after-commit") by.append(el("div", { class: "cause effect", text: `Effect loop: state set right after commit #${r.afterCommit ?? "?"}` }));
768
+ else if (r.commitCause === "suspense-resolved") by.append(el("div", { class: "cause suspense", text: "Suspense boundary resolved in this commit" }));
769
+ if (r.commitId) by.append(el("div", { class: "meta", text: `Commit #${r.commitId}${r.commitPriority ? ` \xB7 ${PRIORITY_LABEL[r.commitPriority] || r.commitPriority} priority` : ""}` }));
770
+ frag.append(by);
771
+ frag.append(kvSection("Props", r.props ? r.props.next : {}, r.propChanges || []));
772
+ const hookChanges = new Map((r.hookChanges || []).map((c) => [c.path, c]));
773
+ const stateChanges = r.stateChanges || [];
774
+ if (r.hookState || r.contexts || r.state) {
775
+ if (r.hookState && r.hookState.length) {
776
+ const table = el("table", { class: "kv" });
777
+ for (const h of r.hookState) {
778
+ const c = hookChanges.get(h.path);
779
+ const label = h.custom && h.custom.length ? `${h.custom.join(" \u203A ")} \u203A ${h.path}` : h.path;
780
+ if (c) table.append(changeRow(label, c));
781
+ else {
782
+ const tr = el("tr");
783
+ tr.append(el("td", { class: "k", text: label }));
784
+ const td = el("td");
785
+ td.append(valueNode(h.value));
786
+ tr.append(td);
787
+ table.append(tr);
788
+ }
789
+ }
790
+ frag.append(el("div", { class: "section" }, [el("h3", { text: "Hooks" }), table]));
791
+ }
792
+ if (r.state) frag.append(kvSection("State", r.state, stateChanges));
793
+ if (r.contexts && r.contexts.length) {
794
+ const table = el("table", { class: "kv" });
795
+ for (const ctx of r.contexts) {
796
+ const c = hookChanges.get(`useContext(${ctx.name})`);
797
+ if (c) table.append(changeRow(ctx.name, c));
798
+ else {
799
+ const tr = el("tr");
800
+ tr.append(el("td", { class: "k", text: ctx.name }));
801
+ const td = el("td");
802
+ td.append(valueNode(ctx.value));
803
+ tr.append(td);
804
+ table.append(tr);
805
+ }
806
+ }
807
+ frag.append(el("div", { class: "section" }, [el("h3", { text: "Contexts" }), table]));
808
+ }
809
+ const leftover = [...hookChanges.values()].filter((c) => !(r.hookState || []).some((h) => h.path === c.path) && !(r.contexts || []).some((x) => `useContext(${x.name})` === c.path));
810
+ if (leftover.length) {
811
+ const table = el("table", { class: "kv" });
812
+ for (const c of leftover) table.append(changeRow(c.path, c));
813
+ frag.append(el("div", { class: "section" }, [el("h3", { text: "Other hooks that changed" }), table]));
814
+ }
815
+ return frag;
816
+ }
817
+ const hooks = [].concat(r.hookChanges || [], stateChanges);
818
+ if (hooks.length) {
819
+ const table = el("table", { class: "kv" });
820
+ for (const c of hooks) table.append(changeRow(c.custom && c.custom.length ? `${c.custom.join(" \u203A ")} \u203A ${c.path}` : c.path, c));
821
+ frag.append(el("div", { class: "section" }, [el("h3", { text: "State & hooks that changed" }), table]));
822
+ }
823
+ return frag;
824
+ }
825
+ function fixView(fixes, actions = {}) {
826
+ const frag = document.createDocumentFragment();
827
+ if (!fixes.length) {
828
+ frag.append(el("div", { class: "section" }, [el("h3", { text: "Fix" }), el("div", { class: "meta", text: "Nothing to fix: this render was caused by a genuine change." })]));
829
+ return frag;
830
+ }
831
+ for (const f of fixes) {
832
+ const ranked = "count" in f ? f : null;
833
+ const sec = el("div", { class: "section fix" }, [
834
+ el("h3", { text: f.label }),
835
+ el("div", { text: f.detail }),
836
+ ranked ? el("div", { class: "meta", text: `removes ${plural(ranked.count, "avoidable re-render")}: ${componentList(ranked.components)}` }) : null,
837
+ el("pre", { class: "snippet", text: f.snippet })
838
+ ]);
839
+ if (actions.copy) {
840
+ const copy = actions.copy;
841
+ sec.append(el("button", { onclick: () => copy(f.snippet) }, "\u2398 Copy snippet"));
842
+ }
843
+ frag.append(sec);
844
+ }
845
+ return frag;
846
+ }
847
+ function virtualList(container, rowHeight, rowFor, opts = {}) {
848
+ const inner = el("div", { class: "virtual-inner", role: "list" });
849
+ if (opts.attach !== false) container.append(inner);
850
+ let items = [];
851
+ const mounted = /* @__PURE__ */ new Map();
852
+ let raf = 0;
853
+ const render = () => {
854
+ raf = 0;
855
+ if (!inner.isConnected) return;
856
+ const height = container.clientHeight || FALLBACK_VIEWPORT;
857
+ const top = container.scrollTop - (opts.headerHeight ? opts.headerHeight() : 0);
858
+ const start = Math.max(0, Math.floor(top / rowHeight) - OVERSCAN);
859
+ const end = Math.min(items.length, Math.ceil((top + height) / rowHeight) + OVERSCAN);
860
+ inner.style.height = `${items.length * rowHeight}px`;
861
+ const keep = /* @__PURE__ */ new Set();
862
+ for (let i = start; i < end; i++) {
863
+ const row = rowFor(items[i], i);
864
+ row.style.top = `${i * rowHeight}px`;
865
+ if (row.parentNode !== inner) inner.append(row);
866
+ mounted.set(row, i);
867
+ keep.add(row);
868
+ }
869
+ for (const row of [...mounted.keys()]) {
870
+ if (!keep.has(row)) {
871
+ row.remove();
872
+ mounted.delete(row);
873
+ }
874
+ }
875
+ };
876
+ container.addEventListener("scroll", () => {
877
+ if (!raf) raf = typeof requestAnimationFrame === "function" ? requestAnimationFrame(render) : setTimeout(render, 0);
878
+ });
879
+ return {
880
+ container,
881
+ inner,
882
+ get items() {
883
+ return items;
884
+ },
885
+ setItems(next) {
886
+ items = next;
887
+ render();
888
+ },
889
+ render,
890
+ scrollTo(index) {
891
+ const height = container.clientHeight || FALLBACK_VIEWPORT;
892
+ const top = index * rowHeight;
893
+ if (top < container.scrollTop) container.scrollTop = top;
894
+ else if (top + rowHeight > container.scrollTop + height) container.scrollTop = top + rowHeight - height;
895
+ render();
896
+ }
897
+ };
898
+ }
899
+ function createPanel(root, transport, options = {}) {
900
+ const state = {
901
+ tree: { name: "", children: /* @__PURE__ */ new Map(), reports: [], total: 0, avoidable: 0, wasted: 0, expanded: true, path: [], key: "" },
902
+ nodesByKey: /* @__PURE__ */ new Map(),
903
+ reports: [],
904
+ commits: /* @__PURE__ */ new Map(),
905
+ commitOrder: [],
906
+ selectedKey: null,
907
+ selectedReport: null,
908
+ selectedCommit: null,
909
+ selectedFix: null,
910
+ selectedRoot: null,
911
+ view: "tree",
912
+ tab: "latest",
913
+ paused: false,
914
+ avoidableOnly: false,
915
+ filter: "",
916
+ relay: false,
917
+ library: null,
918
+ polling: false,
919
+ streamCollapsed: false,
920
+ collapsed: /* @__PURE__ */ new Set(),
921
+ sort: { key: "avoidable", dir: -1 },
922
+ flashOn: false,
923
+ settingsOpen: false,
924
+ origin: null,
925
+ legacyCommit: 0,
926
+ tabLabel: transport.tabLabel ?? null,
927
+ compact: false,
928
+ sessions: [],
929
+ recording: null,
930
+ selectedSession: null,
931
+ compareWith: null,
932
+ byInstance: false,
933
+ columns: [],
934
+ notes: {}
935
+ };
936
+ let persistTimer = null;
937
+ let queue = [];
938
+ let flushScheduled = false;
939
+ const schedule = typeof requestAnimationFrame === "function" ? (fn) => requestAnimationFrame(fn) : (fn) => setTimeout(fn, 0);
940
+ const copyText = (text) => {
941
+ if (transport.copy) transport.copy(text);
942
+ else if (typeof navigator !== "undefined" && navigator.clipboard) navigator.clipboard.writeText(text).catch(() => {
943
+ });
944
+ toast("Copied");
945
+ };
946
+ root.textContent = "";
947
+ const search = el("input", {
948
+ type: "search",
949
+ placeholder: "Search components (text or /regex/)",
950
+ oninput: () => {
951
+ state.filter = search.value;
952
+ renderLeft();
953
+ renderStream();
954
+ persist();
955
+ }
956
+ });
957
+ const iconButton = (glyph, label, title, onclick, extra = {}) => el("button", { class: "ib", title, onclick, "aria-label": label, ...extra }, [el("span", { class: "glyph", text: glyph }), el("span", { class: "label", text: label })]);
958
+ const pauseBtn = iconButton("\u23F8", "Pause", "Pause / resume (reports keep buffering in the page)", () => {
959
+ state.paused = !state.paused;
960
+ pauseBtn.classList.toggle("active", state.paused);
961
+ pauseBtn.querySelector(".glyph").textContent = state.paused ? "\u25B6" : "\u23F8";
962
+ pauseBtn.querySelector(".label").textContent = state.paused ? "Resume" : "Pause";
963
+ });
964
+ const clearBtn = iconButton("\u2298", "Clear", "Clear the panel and the page buffer", () => {
965
+ clearAll();
966
+ transport.clear?.();
967
+ transport.badge?.(0);
968
+ });
969
+ const replayBtn = iconButton("\u21BB", "Replay", "Replay buffered reports from the page", () => transport.replay?.());
970
+ const recordBtn = iconButton("\u23FA", "Record", "Record a session to compare before and after a fix", () => {
971
+ if (state.recording) stopRecording();
972
+ else startRecording();
973
+ });
974
+ const exportBtn = iconButton("\u2913", "Export", "Export reports as JSON", exportJson);
975
+ const importInput = el("input", { type: "file", accept: "application/json,.json", class: "hidden-file" });
976
+ importInput.addEventListener("change", () => {
977
+ const f = importInput.files && importInput.files[0];
978
+ if (f) importFile(f);
979
+ importInput.value = "";
980
+ });
981
+ const importBtn = iconButton("\u2912", "Import", "Import a JSON export", () => importInput.click());
982
+ const avoidCheck = el("input", {
983
+ type: "checkbox",
984
+ onchange: () => {
985
+ state.avoidableOnly = avoidCheck.checked;
986
+ renderLeft();
987
+ renderStream();
988
+ persist();
989
+ }
990
+ });
991
+ const settingsBtn = iconButton("\u2699", "Settings", "Settings", () => toggleSettings());
992
+ const status = el("span", { class: "status", title: "" }, [el("span", { class: "dot" }), el("span", { class: "status-text", text: "no page" })]);
993
+ const tabChip = el("span", { class: "tab-chip", hidden: true, title: "The tab this panel follows" });
994
+ const undock = transport.undock ? [
995
+ el("span", { class: "sep" }),
996
+ iconButton("\u2AFF", "Side panel", "Show this panel next to the page (Chrome side panel)", () => void transport.undock("sidepanel").catch((e) => toast(String(e.message || e)))),
997
+ iconButton("\u29C9", "Window", "Show this panel in its own window", () => void transport.undock("window").catch((e) => toast(String(e.message || e))))
998
+ ] : [];
999
+ const toolbar = el("div", { class: "toolbar" }, [
1000
+ search,
1001
+ el("span", { class: "sep" }),
1002
+ pauseBtn,
1003
+ clearBtn,
1004
+ replayBtn,
1005
+ recordBtn,
1006
+ el("span", { class: "sep" }),
1007
+ exportBtn,
1008
+ importBtn,
1009
+ importInput,
1010
+ el("span", { class: "sep" }),
1011
+ el("label", { class: "check" }, [avoidCheck, el("span", { class: "label", text: "Avoidable only" })]),
1012
+ ...undock,
1013
+ el("span", { class: "spacer" }),
1014
+ tabChip,
1015
+ status,
1016
+ settingsBtn
1017
+ ]);
1018
+ const summary = el("div", { class: "summary" });
1019
+ const banner = el("div", { class: "banner", hidden: true });
1020
+ const viewsBar = el("div", { class: "views" });
1021
+ const VIEWS = [
1022
+ ["tree", "Tree"],
1023
+ ["offenders", "Offenders"],
1024
+ ["commits", "Commits"],
1025
+ ["fixes", "Fixes"],
1026
+ ["sessions", "Sessions"]
1027
+ ];
1028
+ const viewButtons = /* @__PURE__ */ new Map();
1029
+ for (const [id, label] of VIEWS) {
1030
+ const b = el("button", { "data-view": id, onclick: () => setView(id) }, label);
1031
+ viewButtons.set(id, b);
1032
+ viewsBar.append(b);
1033
+ }
1034
+ const instancesBtn = el(
1035
+ "button",
1036
+ {
1037
+ class: "instances",
1038
+ title: "Group the tree by instance (key or id) instead of by component name",
1039
+ onclick: () => {
1040
+ state.byInstance = !state.byInstance;
1041
+ instancesBtn.classList.toggle("active", state.byInstance);
1042
+ rebuildTree();
1043
+ persist();
1044
+ }
1045
+ },
1046
+ "\u205D Instances"
1047
+ );
1048
+ viewsBar.append(el("span", { class: "spacer" }), instancesBtn);
1049
+ const tree = el("div", { class: "tree", tabindex: "0", onkeydown: onTreeKey, role: "tree" });
1050
+ const table = el("div", { class: "table-wrap", hidden: true });
1051
+ const left = el("div", { class: "left" }, [viewsBar, tree, table]);
1052
+ const resizer = el("div", { class: "resizer", title: "Drag to resize" });
1053
+ const details = el("div", { class: "details" });
1054
+ const settings = el("div", { class: "drawer", hidden: true });
1055
+ const main = el("div", { class: "main" }, [left, resizer, details, settings]);
1056
+ const streamList = el("div", { class: "stream-list" });
1057
+ const streamCount = el("span", { class: "count", text: "0 reports" });
1058
+ const stream = el("div", { class: "stream" }, [
1059
+ el(
1060
+ "div",
1061
+ {
1062
+ class: "stream-header",
1063
+ onclick: () => {
1064
+ state.streamCollapsed = !state.streamCollapsed;
1065
+ stream.classList.toggle("collapsed", state.streamCollapsed);
1066
+ persist();
1067
+ }
1068
+ },
1069
+ [el("span", { text: "\u25BE Live stream" }), streamCount]
1070
+ ),
1071
+ streamList
1072
+ ]);
1073
+ const toastEl = el("div", { class: "toast", hidden: true, role: "status", "aria-live": "polite" });
1074
+ root.classList.add("rl");
1075
+ search.setAttribute("aria-label", "Search components; prefix with ~ to search values");
1076
+ search.placeholder = "Search components (text, /regex/, ~value)";
1077
+ streamList.setAttribute("aria-label", "Live stream of reports");
1078
+ details.setAttribute("role", "region");
1079
+ details.setAttribute("aria-label", "Details");
1080
+ settings.setAttribute("role", "dialog");
1081
+ settings.setAttribute("aria-label", "Settings");
1082
+ root.append(toolbar, summary, banner, main, stream, toastEl);
1083
+ root.addEventListener("keydown", onGlobalKey);
1084
+ function setCompact(on) {
1085
+ if (state.compact === on) return;
1086
+ state.compact = on;
1087
+ root.classList.toggle("compact", on);
1088
+ treeList.render();
1089
+ streamItems.render();
1090
+ }
1091
+ const measure = () => setCompact(root.clientWidth > 0 && root.clientWidth < 720);
1092
+ if (typeof ResizeObserver === "function") new ResizeObserver(measure).observe(root);
1093
+ else window.addEventListener("resize", measure);
1094
+ function renderSummary() {
1095
+ summary.textContent = "";
1096
+ const total = state.reports.length;
1097
+ if (!total) {
1098
+ summary.hidden = true;
1099
+ return;
1100
+ }
1101
+ summary.hidden = false;
1102
+ let avoidable = 0;
1103
+ let wasted = 0;
1104
+ const perComponent = /* @__PURE__ */ new Map();
1105
+ for (const r of state.reports) {
1106
+ if (!r.avoidable) continue;
1107
+ avoidable++;
1108
+ if (typeof r.selfDuration === "number") wasted += r.selfDuration;
1109
+ if (!isMuted(r.component)) perComponent.set(r.component, (perComponent.get(r.component) || 0) + 1);
1110
+ }
1111
+ const top = [...perComponent].sort((a, b) => b[1] - a[1])[0];
1112
+ const fix = avoidable ? rankFixes(state.reports.filter((r) => !isMuted(r.component)))[0] : void 0;
1113
+ const stat = (value, label, cls = "") => el("span", { class: "stat " + cls }, [el("b", { text: value }), el("span", { class: "label", text: label })]);
1114
+ summary.append(stat(String(total), plural(total, "render").replace(/^\d+ /, "")), stat(String(avoidable), "avoidable", avoidable ? "bad" : "good"));
1115
+ if (wasted) summary.append(stat(fmtMs(wasted), "wasted", "bad"));
1116
+ if (top) {
1117
+ summary.append(
1118
+ el("button", { class: "stat link", title: "Select the component with the most avoidable re-renders", onclick: () => panelApi.select(top[0]) }, [
1119
+ el("span", { class: "label", text: "top" }),
1120
+ el("b", { class: "mono", text: `<${top[0]}>` }),
1121
+ el("span", { class: "label", text: `\xD7${top[1]}` })
1122
+ ])
1123
+ );
1124
+ }
1125
+ if (fix) {
1126
+ summary.append(
1127
+ el(
1128
+ "button",
1129
+ {
1130
+ class: "stat link",
1131
+ title: "Open the Fixes view",
1132
+ onclick: () => {
1133
+ state.selectedFix = fix.key;
1134
+ state.tab = "fixlist";
1135
+ setView("fixes");
1136
+ }
1137
+ },
1138
+ [el("span", { class: "label", text: "best fix" }), el("b", { class: "mono", text: fix.label }), el("span", { class: "label", text: `\u2212${fix.count}` })]
1139
+ )
1140
+ );
1141
+ }
1142
+ }
1143
+ let toastTimer = null;
1144
+ function toast(text) {
1145
+ toastEl.textContent = text;
1146
+ toastEl.hidden = false;
1147
+ if (toastTimer) clearTimeout(toastTimer);
1148
+ toastTimer = setTimeout(() => {
1149
+ toastEl.hidden = true;
1150
+ }, 1200);
1151
+ }
1152
+ let drag = null;
1153
+ resizer.addEventListener("mousedown", (e) => {
1154
+ drag = { x: e.clientX, w: left.getBoundingClientRect().width };
1155
+ e.preventDefault();
1156
+ });
1157
+ window.addEventListener("mousemove", (e) => {
1158
+ if (!drag) return;
1159
+ const w = Math.max(180, Math.min(drag.w + e.clientX - drag.x, root.clientWidth - 240));
1160
+ left.style.width = w + "px";
1161
+ state.treeWidth = w;
1162
+ });
1163
+ window.addEventListener("mouseup", () => {
1164
+ if (drag) persist();
1165
+ drag = null;
1166
+ });
1167
+ function persist() {
1168
+ if (!transport.storage) return;
1169
+ if (persistTimer) clearTimeout(persistTimer);
1170
+ persistTimer = setTimeout(() => {
1171
+ const saved = {
1172
+ filter: state.filter,
1173
+ avoidableOnly: state.avoidableOnly,
1174
+ view: state.view,
1175
+ tab: state.tab === "history" || state.tab === "fix" ? state.tab : "latest",
1176
+ streamCollapsed: state.streamCollapsed,
1177
+ collapsed: [...state.collapsed],
1178
+ treeWidth: state.treeWidth,
1179
+ flashOn: state.flashOn,
1180
+ byInstance: state.byInstance,
1181
+ columns: state.columns
1182
+ };
1183
+ transport.storage.set("panel", saved);
1184
+ }, 150);
1185
+ }
1186
+ function restoreSessions(raw) {
1187
+ if (!Array.isArray(raw)) return;
1188
+ for (const s of raw) {
1189
+ if (!isRecord(s) || typeof s.id !== "string" || typeof s.name !== "string" || !isRecord(s.byComponent)) continue;
1190
+ if (state.sessions.some((x) => x.id === s.id)) continue;
1191
+ state.sessions.push({
1192
+ id: s.id,
1193
+ name: s.name,
1194
+ startedAt: typeof s.startedAt === "number" ? s.startedAt : 0,
1195
+ endedAt: typeof s.endedAt === "number" ? s.endedAt : 0,
1196
+ total: typeof s.total === "number" ? s.total : 0,
1197
+ avoidable: typeof s.avoidable === "number" ? s.avoidable : 0,
1198
+ wasted: typeof s.wasted === "number" ? s.wasted : 0,
1199
+ byComponent: s.byComponent,
1200
+ fixes: Array.isArray(s.fixes) ? s.fixes : [],
1201
+ reports: []
1202
+ });
1203
+ }
1204
+ state.sessions.sort((a, b) => a.startedAt - b.startedAt);
1205
+ if (state.view === "sessions") renderLeft();
1206
+ }
1207
+ function restore(raw) {
1208
+ if (!isRecord(raw)) return;
1209
+ const saved = raw;
1210
+ if (typeof saved.filter === "string") {
1211
+ state.filter = saved.filter;
1212
+ search.value = saved.filter;
1213
+ }
1214
+ if (typeof saved.avoidableOnly === "boolean") {
1215
+ state.avoidableOnly = saved.avoidableOnly;
1216
+ avoidCheck.checked = saved.avoidableOnly;
1217
+ }
1218
+ if (Array.isArray(saved.collapsed)) state.collapsed = new Set(saved.collapsed.filter((x) => typeof x === "string"));
1219
+ if (typeof saved.streamCollapsed === "boolean") {
1220
+ state.streamCollapsed = saved.streamCollapsed;
1221
+ stream.classList.toggle("collapsed", state.streamCollapsed);
1222
+ }
1223
+ if (typeof saved.treeWidth === "number" && saved.treeWidth > 100) {
1224
+ state.treeWidth = saved.treeWidth;
1225
+ left.style.width = saved.treeWidth + "px";
1226
+ }
1227
+ if (typeof saved.flashOn === "boolean") state.flashOn = saved.flashOn;
1228
+ if (typeof saved.byInstance === "boolean" && saved.byInstance !== state.byInstance) {
1229
+ state.byInstance = saved.byInstance;
1230
+ instancesBtn.classList.toggle("active", state.byInstance);
1231
+ rebuildTree();
1232
+ }
1233
+ if (Array.isArray(saved.columns)) state.columns = saved.columns.filter((c) => OPTIONAL_COLUMNS.some((o) => o.key === c));
1234
+ if (saved.tab === "history" || saved.tab === "fix") state.tab = saved.tab;
1235
+ if (saved.view && viewButtons.has(saved.view)) state.view = saved.view;
1236
+ for (const n of state.nodesByKey.values()) n.expanded = !state.collapsed.has(n.key);
1237
+ setView(state.view);
1238
+ renderStream();
1239
+ }
1240
+ const keyOf = (path) => path.join(" ");
1241
+ function nodeFor(path) {
1242
+ const key = keyOf(path);
1243
+ const found = state.nodesByKey.get(key);
1244
+ if (found) return found;
1245
+ let parent = state.tree;
1246
+ for (let i = 0; i < path.length; i++) {
1247
+ const name = path[i];
1248
+ const k = keyOf(path.slice(0, i + 1));
1249
+ let n = state.nodesByKey.get(k);
1250
+ if (!n) {
1251
+ n = { name, children: /* @__PURE__ */ new Map(), reports: [], total: 0, avoidable: 0, wasted: 0, expanded: !state.collapsed.has(k), path: path.slice(0, i + 1), key: k };
1252
+ state.nodesByKey.set(k, n);
1253
+ parent.children.set(name, n);
1254
+ }
1255
+ parent = n;
1256
+ }
1257
+ return parent;
1258
+ }
1259
+ const instanceLabel = (r) => r.key ? `${r.component} key=${JSON.stringify(r.key)}` : `${r.component} #${r.instanceId}`;
1260
+ const nodeOfReport = (r) => nodeFor(r.path.concat([state.byInstance ? instanceLabel(r) : r.component]));
1261
+ function rebuildTree() {
1262
+ state.tree.children.clear();
1263
+ state.nodesByKey.clear();
1264
+ rowEls.clear();
1265
+ state.selectedKey = null;
1266
+ for (const r of state.reports) {
1267
+ const node = nodeOfReport(r);
1268
+ node.reports.push(r);
1269
+ if (node.reports.length > MAX_PER_NODE) node.reports.shift();
1270
+ node.total++;
1271
+ if (r.avoidable) {
1272
+ node.avoidable++;
1273
+ if (typeof r.selfDuration === "number") node.wasted += r.selfDuration;
1274
+ }
1275
+ node.lastReport = r;
1276
+ }
1277
+ renderLeft();
1278
+ renderDetails();
1279
+ }
1280
+ function commitKeyFor(report) {
1281
+ if (report.commitId > 0) return report.commitId;
1282
+ return -state.legacyCommit;
1283
+ }
1284
+ function ingest(report) {
1285
+ if (!report.receivedAt) report.receivedAt = Date.now();
1286
+ state.reports.push(report);
1287
+ if (state.reports.length > MAX_REPORTS) state.reports.shift();
1288
+ const node = nodeOfReport(report);
1289
+ node.reports.push(report);
1290
+ if (node.reports.length > MAX_PER_NODE) node.reports.shift();
1291
+ node.total++;
1292
+ if (report.avoidable) {
1293
+ node.avoidable++;
1294
+ if (typeof report.selfDuration === "number") node.wasted += report.selfDuration;
1295
+ }
1296
+ node.lastReport = report;
1297
+ node.flash = true;
1298
+ node.flashAt = Date.now();
1299
+ const ck = commitKeyFor(report);
1300
+ let list = state.commits.get(ck);
1301
+ if (!list) {
1302
+ list = [];
1303
+ state.commits.set(ck, list);
1304
+ state.commitOrder.push(ck);
1305
+ if (state.commitOrder.length > MAX_COMMITS) state.commits.delete(state.commitOrder.shift());
1306
+ }
1307
+ list.push(report);
1308
+ return node;
1309
+ }
1310
+ function flush() {
1311
+ flushScheduled = false;
1312
+ if (!queue.length) return;
1313
+ const batch = queue;
1314
+ queue = [];
1315
+ state.legacyCommit++;
1316
+ let touchedSelected = false;
1317
+ let avoidableCount = 0;
1318
+ for (const r of batch) {
1319
+ const node = ingest(r);
1320
+ if (state.recording && state.recording.reports.length < MAX_REPORTS) state.recording.reports.push(r);
1321
+ if (r.avoidable) avoidableCount++;
1322
+ if (state.selectedKey === node.key) {
1323
+ touchedSelected = true;
1324
+ if (state.tab === "latest") state.selectedReport = r;
1325
+ }
1326
+ }
1327
+ renderLeft();
1328
+ renderStream(batch);
1329
+ renderSummary();
1330
+ if (touchedSelected || state.view === "commits" || state.view === "fixes" || state.tab === "root" || state.recording && state.tab === "session") renderDetails();
1331
+ if (state.polling && avoidableCount) transport.badge?.(state.reports.filter((r) => r.avoidable).length);
1332
+ }
1333
+ function enqueue(report) {
1334
+ queue.push(report);
1335
+ if (!flushScheduled) {
1336
+ flushScheduled = true;
1337
+ schedule(flush);
1338
+ }
1339
+ }
1340
+ function clearAll() {
1341
+ state.tree.children.clear();
1342
+ state.nodesByKey.clear();
1343
+ state.reports = [];
1344
+ state.commits.clear();
1345
+ state.commitOrder = [];
1346
+ state.selectedKey = null;
1347
+ state.selectedReport = null;
1348
+ state.selectedCommit = null;
1349
+ state.selectedFix = null;
1350
+ state.selectedRoot = null;
1351
+ if (state.tab === "commit" || state.tab === "fixlist" || state.tab === "root") state.tab = "latest";
1352
+ queue = [];
1353
+ renderLeft();
1354
+ renderDetails();
1355
+ renderStream();
1356
+ renderSummary();
1357
+ }
1358
+ const valueQuery = () => state.filter.trim().startsWith("~") ? state.filter.trim().slice(1).toLowerCase() : null;
1359
+ function matchesFilter(name) {
1360
+ if (!state.filter || valueQuery() !== null) return true;
1361
+ const f = state.filter.trim();
1362
+ const m = /^\/(.+)\/([a-z]*)$/.exec(f);
1363
+ if (m && m[1] !== void 0) {
1364
+ try {
1365
+ return new RegExp(m[1], m[2]).test(name);
1366
+ } catch {
1367
+ }
1368
+ }
1369
+ return name.toLowerCase().includes(f.toLowerCase());
1370
+ }
1371
+ const valueCache = /* @__PURE__ */ new WeakMap();
1372
+ function matchesValues(r) {
1373
+ const q = valueQuery();
1374
+ if (q === null) return true;
1375
+ if (!q) return true;
1376
+ let text = valueCache.get(r);
1377
+ if (text === void 0) {
1378
+ try {
1379
+ text = JSON.stringify({ p: r.props.next, h: (r.hookState || []).map((x) => x.value), c: (r.contexts || []).map((x) => x.value), s: r.state ?? null }).toLowerCase();
1380
+ } catch {
1381
+ text = "";
1382
+ }
1383
+ valueCache.set(r, text);
1384
+ }
1385
+ return text.includes(q);
1386
+ }
1387
+ function visible(node) {
1388
+ const own = (!state.avoidableOnly || node.avoidable > 0) && matchesFilter(node.name) && node.total > 0 && (valueQuery() === null || node.reports.some(matchesValues));
1389
+ if (own) return true;
1390
+ for (const c of node.children.values()) if (visible(c)) return true;
1391
+ return false;
1392
+ }
1393
+ const isMuted = (component) => !!state.notes[component]?.muted;
1394
+ const passes = (r) => (!state.avoidableOnly || r.avoidable) && matchesFilter(r.component) && matchesValues(r);
1395
+ const ranked = () => state.reports.filter((r) => passes(r) && !isMuted(r.component));
1396
+ const filteredReports = () => state.reports.filter(passes);
1397
+ function setView(view) {
1398
+ state.view = view;
1399
+ for (const [id, b] of viewButtons) b.classList.toggle("active", id === view);
1400
+ tree.hidden = view !== "tree";
1401
+ table.hidden = view === "tree";
1402
+ renderLeft();
1403
+ renderDetails();
1404
+ persist();
1405
+ }
1406
+ function renderLeft() {
1407
+ if (state.view === "tree") renderTree();
1408
+ else if (state.view === "offenders") renderOffenders();
1409
+ else if (state.view === "commits") renderCommits();
1410
+ else if (state.view === "sessions") renderSessions();
1411
+ else renderFixes();
1412
+ }
1413
+ const rowEls = /* @__PURE__ */ new Map();
1414
+ const treeList = virtualList(tree, ROW_H, ({ node, depth }) => rowFor(node, depth));
1415
+ const emptyEl = el("div", { class: "empty" }, [
1416
+ el("div", { text: "No re-renders reported yet." }),
1417
+ el("div", null, ["Call ", el("code", { text: "init({ notifier: createDevtoolsNotifier() })" }), " in the page, or enable injection in Settings, then interact with it."])
1418
+ ]);
1419
+ function renderTree() {
1420
+ const flat = [];
1421
+ const walk = (node, depth) => {
1422
+ for (const child of node.children.values()) {
1423
+ if (!visible(child)) continue;
1424
+ flat.push({ node: child, depth });
1425
+ if (child.expanded) walk(child, depth + 1);
1426
+ }
1427
+ };
1428
+ walk(state.tree, 0);
1429
+ if (flat.length === 0) {
1430
+ if (!emptyEl.parentNode) tree.append(emptyEl);
1431
+ } else emptyEl.remove();
1432
+ if (rowEls.size > flat.length * 2 + 64) {
1433
+ const live = new Set(flat.map((f) => f.node.key));
1434
+ for (const key of [...rowEls.keys()]) if (!live.has(key)) rowEls.delete(key);
1435
+ }
1436
+ treeList.setItems(flat);
1437
+ }
1438
+ function hoverHighlight(node, on) {
1439
+ const r = node.lastReport;
1440
+ if (!r || !r.instanceId) return;
1441
+ transport.highlight?.(on ? r.instanceId : null);
1442
+ }
1443
+ function rowFor(node, depth) {
1444
+ let row = rowEls.get(node.key);
1445
+ if (!row) {
1446
+ const created = el("div", {
1447
+ class: "row",
1448
+ "data-key": node.key,
1449
+ role: "treeitem",
1450
+ onclick: () => select(node),
1451
+ onmouseenter: () => hoverHighlight(node, true),
1452
+ onmouseleave: () => hoverHighlight(node, false),
1453
+ onanimationend: () => created.classList.remove("flash")
1454
+ });
1455
+ created.append(el("span", { class: "indent" }));
1456
+ created.append(
1457
+ el("span", {
1458
+ class: "chevron",
1459
+ onclick: (e) => {
1460
+ e.stopPropagation();
1461
+ toggleExpanded(node);
1462
+ }
1463
+ })
1464
+ );
1465
+ created.append(
1466
+ el("span", { class: "tag" }, [el("span", { class: "bracket", text: "<" }), el("span", { class: "name", text: node.name }), el("span", { class: "bracket", text: ">" })])
1467
+ );
1468
+ created.append(el("span", { class: "badges" }));
1469
+ rowEls.set(node.key, created);
1470
+ row = created;
1471
+ }
1472
+ row.classList.toggle("selected", state.selectedKey === node.key);
1473
+ row.setAttribute("aria-selected", state.selectedKey === node.key ? "true" : "false");
1474
+ row.setAttribute("aria-level", String(depth + 1));
1475
+ row.classList.toggle("muted", isMuted(node.lastReport ? node.lastReport.component : node.name));
1476
+ const indent = row.querySelector(".indent");
1477
+ if (indent.childElementCount !== depth) {
1478
+ indent.textContent = "";
1479
+ for (let i = 0; i < depth; i++) indent.append(el("span", { class: "guide" }));
1480
+ }
1481
+ const hasChildren = [...node.children.values()].some(visible);
1482
+ const chevron = row.querySelector(".chevron");
1483
+ chevron.classList.toggle("leaf", !hasChildren);
1484
+ chevron.textContent = node.expanded ? "\u25BE" : "\u25B8";
1485
+ const badges = row.querySelector(".badges");
1486
+ badges.textContent = "";
1487
+ if (node.avoidable) badges.append(el("span", { class: "badge avoid", title: "avoidable re-renders", text: String(node.avoidable) }));
1488
+ if (node.total) badges.append(el("span", { class: "badge", title: "re-renders", text: String(node.total) }));
1489
+ if (node.flash) {
1490
+ node.flash = false;
1491
+ if (Date.now() - (node.flashAt || 0) < 1e3) {
1492
+ row.classList.remove("flash");
1493
+ void row.offsetWidth;
1494
+ row.classList.add("flash");
1495
+ }
1496
+ }
1497
+ return row;
1498
+ }
1499
+ function toggleExpanded(node, value) {
1500
+ node.expanded = value === void 0 ? !node.expanded : value;
1501
+ if (node.expanded) state.collapsed.delete(node.key);
1502
+ else state.collapsed.add(node.key);
1503
+ renderTree();
1504
+ persist();
1505
+ }
1506
+ function select(node, report) {
1507
+ state.selectedKey = node.key;
1508
+ state.selectedReport = report || node.lastReport || null;
1509
+ if (report && state.tab !== "fix") state.tab = "latest";
1510
+ if (state.tab === "commit" || state.tab === "fixlist" || state.tab === "root") state.tab = "latest";
1511
+ renderLeft();
1512
+ renderDetails();
1513
+ if (state.view === "tree") {
1514
+ const idx = treeList.items.findIndex((f) => f.node.key === node.key);
1515
+ if (idx >= 0) treeList.scrollTo(idx);
1516
+ }
1517
+ }
1518
+ function onTreeKey(e) {
1519
+ const rows = treeList.items;
1520
+ if (!rows.length) return;
1521
+ const idx = rows.findIndex((f) => f.node.key === state.selectedKey);
1522
+ if (e.key === "ArrowDown") {
1523
+ e.preventDefault();
1524
+ select(rows[Math.min(rows.length - 1, idx + 1)].node);
1525
+ } else if (e.key === "ArrowUp") {
1526
+ e.preventDefault();
1527
+ select(rows[Math.max(0, idx - 1)].node);
1528
+ } else if (e.key === "ArrowRight" && idx >= 0) {
1529
+ toggleExpanded(rows[idx].node, true);
1530
+ } else if (e.key === "ArrowLeft" && idx >= 0) {
1531
+ toggleExpanded(rows[idx].node, false);
1532
+ }
1533
+ }
1534
+ function onGlobalKey(e) {
1535
+ const target = e.target;
1536
+ const inField = !!target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT");
1537
+ if (e.key === "Escape") {
1538
+ transport.highlight?.(null);
1539
+ if (state.settingsOpen && settings.contains(target)) {
1540
+ toggleSettings(false);
1541
+ return;
1542
+ }
1543
+ if (inField) target.blur();
1544
+ return;
1545
+ }
1546
+ if (inField || e.ctrlKey || e.metaKey || e.altKey) return;
1547
+ if (e.key === "/") {
1548
+ e.preventDefault();
1549
+ search.focus();
1550
+ search.select();
1551
+ } else if (e.key === "f" && state.selectedKey) {
1552
+ e.preventDefault();
1553
+ state.tab = "fix";
1554
+ renderDetails();
1555
+ persist();
1556
+ }
1557
+ }
1558
+ function sortableHeader(label, key, numeric = false) {
1559
+ const active = state.sort.key === key;
1560
+ return el(
1561
+ "th",
1562
+ {
1563
+ class: (active ? "sorted " : "") + (numeric ? "num" : ""),
1564
+ onclick: () => {
1565
+ state.sort = { key, dir: active ? -state.sort.dir : numeric ? -1 : 1 };
1566
+ renderLeft();
1567
+ }
1568
+ },
1569
+ label + (active ? state.sort.dir < 0 ? " \u25BE" : " \u25B4" : "")
1570
+ );
1571
+ }
1572
+ function offenderRows() {
1573
+ const byName = /* @__PURE__ */ new Map();
1574
+ for (const r of filteredReports()) {
1575
+ let o = byName.get(r.component);
1576
+ if (!o) {
1577
+ o = { component: r.component, total: 0, avoidable: 0, wasted: 0, paths: /* @__PURE__ */ new Set(), places: 0, lastSeen: 0, reports: [], fix: "", muted: isMuted(r.component) };
1578
+ byName.set(r.component, o);
1579
+ }
1580
+ o.total++;
1581
+ if (r.avoidable) {
1582
+ o.avoidable++;
1583
+ if (typeof r.selfDuration === "number") o.wasted += r.selfDuration;
1584
+ }
1585
+ o.paths.add(keyOf(r.path));
1586
+ o.lastSeen = Math.max(o.lastSeen, r.receivedAt);
1587
+ o.reports.push(r);
1588
+ }
1589
+ const rows = [...byName.values()];
1590
+ for (const o of rows) {
1591
+ o.places = o.paths.size;
1592
+ const fixes = o.avoidable ? rankFixes(o.reports) : [];
1593
+ o.fix = fixes.length ? fixes[0].label : "";
1594
+ }
1595
+ const { key, dir } = state.sort;
1596
+ rows.sort((a, b) => {
1597
+ const va = a[key];
1598
+ const vb = b[key];
1599
+ const c = typeof va === "number" && typeof vb === "number" ? va - vb : String(va).localeCompare(String(vb));
1600
+ return c * dir || b.avoidable - a.avoidable;
1601
+ });
1602
+ return rows;
1603
+ }
1604
+ function columnsMenu() {
1605
+ const menu = el("details", { class: "columns-menu" }, [el("summary", { title: "Choose columns", text: "\u2699 Columns" })]);
1606
+ const box = el("div", { class: "menu" });
1607
+ for (const col of OPTIONAL_COLUMNS) {
1608
+ const input = el("input", { type: "checkbox" });
1609
+ input.checked = state.columns.includes(col.key);
1610
+ input.addEventListener("change", () => {
1611
+ state.columns = OPTIONAL_COLUMNS.map((c) => c.key).filter((k) => k === col.key ? input.checked : state.columns.includes(k));
1612
+ renderOffenders();
1613
+ persist();
1614
+ });
1615
+ box.append(el("label", { class: "opt" }, [input, col.label]));
1616
+ }
1617
+ menu.append(box);
1618
+ return menu;
1619
+ }
1620
+ const offenderCell = (o, key) => {
1621
+ switch (key) {
1622
+ case "places":
1623
+ return el("td", { class: "num", text: String(o.places) });
1624
+ case "lastSeen":
1625
+ return el("td", { class: "num mono", text: o.lastSeen ? fmtTime(o.lastSeen) : "" });
1626
+ default:
1627
+ return el("td");
1628
+ }
1629
+ };
1630
+ function offenderRow(o, tag) {
1631
+ const cells = [
1632
+ el("td", { class: "c" }, [el("span", { class: "name", text: o.component }), o.muted ? el("span", { class: "badge", text: "muted" }) : null, o.paths.size > 1 && !state.columns.includes("places") ? el("span", { class: "meta", text: ` \xD7${o.paths.size} places` }) : null]),
1633
+ el("td", { class: "num" }, o.avoidable ? el("span", { class: "badge avoid", text: String(o.avoidable) }) : "0"),
1634
+ el("td", { class: "num", text: String(o.total) }),
1635
+ el("td", { class: "num", text: o.wasted ? fmtMs(o.wasted) : "" }),
1636
+ ...state.columns.map((k) => offenderCell(o, k)),
1637
+ el("td", { class: "fix", text: o.fix })
1638
+ ];
1639
+ const attrs = {
1640
+ class: (o.avoidable ? "has-avoid" : "") + (o.muted ? " muted" : ""),
1641
+ role: tag === "div" ? "listitem" : null,
1642
+ onclick: () => {
1643
+ const last = o.reports[o.reports.length - 1];
1644
+ state.tab = "fix";
1645
+ select(nodeOfReport(last), last);
1646
+ }
1647
+ };
1648
+ if (tag === "tr") return el("tr", attrs, cells);
1649
+ const row = el("div", { ...attrs, class: "vrow " + attrs.class });
1650
+ for (const c of cells) {
1651
+ const span = el("span", { class: c.className });
1652
+ span.append(...c.childNodes);
1653
+ row.append(span);
1654
+ }
1655
+ return row;
1656
+ }
1657
+ const offendersList = virtualList(table, GRID_ROW_H, (o) => offenderRow(o, "div"), { attach: false, headerHeight: () => table.querySelector(".vhead")?.offsetHeight || 0 });
1658
+ function offendersHeader() {
1659
+ return [sortableHeader("Component", "component"), sortableHeader("Avoidable", "avoidable", true), sortableHeader("Total", "total", true), sortableHeader("Wasted", "wasted", true), ...state.columns.map((k) => sortableHeader(OPTIONAL_COLUMNS.find((c) => c.key === k).label, k, true)), el("th", { text: "Top fix" })];
1660
+ }
1661
+ function renderOffenders() {
1662
+ table.textContent = "";
1663
+ const rows = offenderRows();
1664
+ if (!rows.length) {
1665
+ table.append(el("div", { class: "empty", text: "No re-renders reported yet." }));
1666
+ return;
1667
+ }
1668
+ const gridCols = `minmax(140px, 2fr) 70px 60px 80px ${state.columns.map(() => "90px").join(" ")} minmax(120px, 2fr)`;
1669
+ if (rows.length > VIRTUAL_THRESHOLD) {
1670
+ const head = el("div", { class: "vhead", style: `grid-template-columns:${gridCols}` });
1671
+ for (const th of offendersHeader()) {
1672
+ const cell = el("span", { class: th.className, onclick: null });
1673
+ cell.append(...th.childNodes);
1674
+ cell.addEventListener("click", () => th.click());
1675
+ head.append(cell);
1676
+ }
1677
+ head.append(columnsMenu());
1678
+ table.append(head, offendersList.inner);
1679
+ offendersList.inner.style.setProperty("--grid-cols", gridCols);
1680
+ offendersList.setItems(rows);
1681
+ return;
1682
+ }
1683
+ const t = el("table", { class: "grid" });
1684
+ const headRow = el("tr", null, offendersHeader());
1685
+ t.append(el("thead", null, headRow));
1686
+ const body = el("tbody");
1687
+ for (const o of rows) body.append(offenderRow(o, "tr"));
1688
+ t.append(body);
1689
+ table.append(el("div", { class: "table-tools" }, columnsMenu()), t);
1690
+ }
1691
+ function commitSummaries() {
1692
+ const out = [];
1693
+ for (let i = state.commitOrder.length - 1; i >= 0; i--) {
1694
+ const key = state.commitOrder[i];
1695
+ const reports = state.commits.get(key);
1696
+ if (!reports || !reports.some(passes)) continue;
1697
+ out.push({ key, analysis: analyzeCommit(reports) });
1698
+ }
1699
+ return out;
1700
+ }
1701
+ function showCommit(key) {
1702
+ state.selectedCommit = key;
1703
+ state.tab = "commit";
1704
+ renderLeft();
1705
+ renderDetails();
1706
+ }
1707
+ function showRoot(name) {
1708
+ state.selectedRoot = name;
1709
+ state.tab = "root";
1710
+ renderDetails();
1711
+ }
1712
+ function renderCommits() {
1713
+ table.textContent = "";
1714
+ const items = commitSummaries();
1715
+ if (!items.length) {
1716
+ table.append(el("div", { class: "empty", text: "No commits yet." }));
1717
+ return;
1718
+ }
1719
+ const list = el("ul", { class: "commits" });
1720
+ for (const { key, analysis } of items) {
1721
+ const root2 = analysis.roots[0];
1722
+ list.append(
1723
+ el("li", { class: (state.selectedCommit === key && state.tab === "commit" ? "selected " : "") + (analysis.avoidable ? "has-avoid" : ""), onclick: () => showCommit(key) }, [
1724
+ el("span", { class: "id", text: key > 0 ? `#${key}` : "\u2014" }),
1725
+ el("span", { class: "t", text: fmtTime(analysis.receivedAt) }),
1726
+ el("span", { class: "n", text: plural(analysis.total, "render") }),
1727
+ analysis.avoidable ? el("span", { class: "badge avoid", text: `${analysis.avoidable} avoidable` }) : el("span", { class: "badge", text: "ok" }),
1728
+ analysis.reports[0]?.commitPriority ? el("span", { class: "prio " + analysis.reports[0].commitPriority, title: "commit priority", text: PRIORITY_LABEL[analysis.reports[0].commitPriority] || analysis.reports[0].commitPriority }) : null,
1729
+ analysis.reports[0]?.commitCause === "effect-after-commit" ? el("span", { class: "cause effect", title: "state set right after the previous commit (effect \u2192 setState)", text: `effect loop \u2190 #${analysis.reports[0].afterCommit ?? "?"}` }) : analysis.reports[0]?.commitCause === "suspense-resolved" ? el("span", { class: "cause suspense", text: "suspense resolved" }) : null,
1730
+ el("span", {
1731
+ class: "root",
1732
+ text: root2 ? `\u2190 <${root2.name}> (${root2.trigger})` : analysis.reports[0]?.updaters?.length ? `\u2190 set by ${analysis.reports[0].updaters.map((u) => `<${u}>`).join(", ")}` : ""
1733
+ })
1734
+ ])
1735
+ );
1736
+ }
1737
+ table.append(list);
1738
+ }
1739
+ function fixItem(f, tag) {
1740
+ return el(
1741
+ tag,
1742
+ {
1743
+ class: (tag === "div" ? "vrow fix-row " : "") + (state.selectedFix === f.key && state.tab === "fixlist" ? "selected" : ""),
1744
+ role: tag === "div" ? "listitem" : null,
1745
+ "aria-selected": state.selectedFix === f.key && state.tab === "fixlist" ? "true" : null,
1746
+ onclick: () => {
1747
+ state.selectedFix = f.key;
1748
+ state.tab = "fixlist";
1749
+ renderLeft();
1750
+ renderDetails();
1751
+ }
1752
+ },
1753
+ [
1754
+ el("span", { class: "badge avoid", title: "avoidable re-renders removed", text: String(f.count) }),
1755
+ el("span", { class: "label", text: f.label }),
1756
+ el("span", { class: "meta", text: componentList(f.components) })
1757
+ ]
1758
+ );
1759
+ }
1760
+ const fixesList = virtualList(table, GRID_ROW_H, (f) => fixItem(f, "div"), { attach: false, headerHeight: () => table.querySelector(".section-title")?.offsetHeight || 0 });
1761
+ function renderFixes() {
1762
+ table.textContent = "";
1763
+ const reports = ranked();
1764
+ const fixes = rankFixes(reports);
1765
+ const contexts = contextAttribution(reports);
1766
+ const mutedCount = Object.values(state.notes).filter((n) => n.muted).length;
1767
+ if (!fixes.length && !contexts.length) {
1768
+ table.append(el("div", { class: "empty", text: mutedCount ? `No avoidable re-renders outside the ${plural(mutedCount, "muted component")}.` : "No avoidable re-renders, nothing to fix." }));
1769
+ return;
1770
+ }
1771
+ if (fixes.length) {
1772
+ const title = el("div", { class: "section-title", text: `Ranked by avoidable re-renders removed${mutedCount ? ` (${plural(mutedCount, "muted component")} hidden)` : ""}` });
1773
+ if (fixes.length > VIRTUAL_THRESHOLD) {
1774
+ table.append(title, fixesList.inner);
1775
+ fixesList.setItems(fixes);
1776
+ } else {
1777
+ const list = el("ol", { class: "fixes", role: "list" });
1778
+ for (const f of fixes) list.append(fixItem(f, "li"));
1779
+ table.append(title, list);
1780
+ }
1781
+ }
1782
+ if (contexts.length) {
1783
+ const list = el("ul", { class: "contexts" });
1784
+ for (const c of contexts) {
1785
+ const partial = c.changedKeys.size > 0 && c.totalKeys > c.changedKeys.size;
1786
+ list.append(
1787
+ el("li", null, [
1788
+ el("span", { class: "name", text: c.name }),
1789
+ c.providers.size ? el("span", { class: "meta", text: ` (provided by ${componentList(c.providers)})` }) : null,
1790
+ el("span", {
1791
+ class: "meta",
1792
+ text: ` changed in ${plural(c.commits.size, "commit")}, ${plural(c.consumers, "consumer re-render")}${c.avoidable ? `, ${c.avoidable} with an equal value` : ""}: ${componentList(c.components)}`
1793
+ }),
1794
+ partial ? el("div", { class: "meta hint", text: `only ${[...c.changedKeys].join(", ")} of ${c.totalKeys} keys changed: consumers of the other keys re-render for nothing. Split the context or select slices.` }) : null
1795
+ ])
1796
+ );
1797
+ }
1798
+ table.append(el("div", { class: "section-title", text: "Contexts" }), list);
1799
+ }
1800
+ }
1801
+ function renderDetails() {
1802
+ const openLabels = new Set([...details.querySelectorAll("details.obj[open] > summary")].map((x) => x.textContent));
1803
+ details.textContent = "";
1804
+ if (state.tab === "commit" && state.selectedCommit !== null) renderCommitDetails();
1805
+ else if (state.tab === "fixlist" && state.selectedFix) renderFixDetails();
1806
+ else if (state.tab === "root" && state.selectedRoot) renderRootDetails();
1807
+ else if (state.tab === "session" && state.selectedSession) renderSessionDetails();
1808
+ else renderNodeDetails(state.selectedKey ? state.nodesByKey.get(state.selectedKey) ?? null : null);
1809
+ if (openLabels.size) {
1810
+ for (const d of details.querySelectorAll("details.obj")) {
1811
+ if (openLabels.has(d.querySelector("summary").textContent)) d.open = true;
1812
+ }
1813
+ }
1814
+ }
1815
+ const reportActions = () => ({
1816
+ openSource: transport.openResource ? (src) => transport.openResource(src.fileName, src.lineNumber, src.columnNumber) : null,
1817
+ highlight: transport.highlight ? (id) => transport.highlight(id) : null,
1818
+ copy: copyText,
1819
+ share: transport.panelUrl ? (r) => {
1820
+ void encodeShare(r).then((code) => copyText(`${transport.panelUrl}?report=${code}`));
1821
+ } : null,
1822
+ readSource: transport.readSource ? (url) => transport.readSource(url) : null
1823
+ });
1824
+ const tabButton = (id, label, onclick) => el("button", { class: state.tab === id ? "active" : "", onclick }, label);
1825
+ function renderNodeDetails(node) {
1826
+ if (!node) {
1827
+ details.append(el("div", { class: "empty", text: "Select a component to see why it re-rendered." }));
1828
+ return;
1829
+ }
1830
+ const componentName = node.lastReport ? node.lastReport.component : node.name;
1831
+ const note = state.notes[componentName] || {};
1832
+ const noteBtn = el("button", { class: "ib small" + (note.note ? " active" : ""), title: note.note ? note.note : "Add a note for this component", "aria-label": "Note", onclick: () => toggleNoteEditor() }, [el("span", { class: "glyph", text: "\u270E" })]);
1833
+ const muteBtn = el("button", { class: "ib small" + (note.muted ? " active" : ""), title: note.muted ? "Muted: hidden from Fixes and the summary. Click to unmute." : "Mute: hide this component from Fixes and the summary", "aria-label": note.muted ? "Unmute" : "Mute", onclick: () => setNote(componentName, { muted: !note.muted }) }, [el("span", { class: "glyph", text: note.muted ? "\u{1F515}" : "\u{1F514}" })]);
1834
+ const header = el("div", { class: "details-header" }, [
1835
+ el("span", { class: "title" }, [el("span", { class: "bracket", text: "<" }), el("span", { class: "name", text: node.name }), el("span", { class: "bracket", text: ">" })]),
1836
+ el("span", { class: "meta", text: `${plural(node.total, "re-render")}, ${node.avoidable} avoidable${node.wasted ? ", " + fmtMs(node.wasted) + " wasted" : ""}` }),
1837
+ noteBtn,
1838
+ muteBtn,
1839
+ el("span", { class: "tabs" }, [
1840
+ tabButton("latest", "Report", () => {
1841
+ state.tab = "latest";
1842
+ state.selectedReport = node.lastReport ?? null;
1843
+ renderDetails();
1844
+ persist();
1845
+ }),
1846
+ tabButton("history", `History (${node.reports.length})`, () => {
1847
+ state.tab = "history";
1848
+ renderDetails();
1849
+ persist();
1850
+ }),
1851
+ tabButton("fix", "Fix", () => {
1852
+ state.tab = "fix";
1853
+ renderDetails();
1854
+ persist();
1855
+ })
1856
+ ])
1857
+ ]);
1858
+ details.append(header);
1859
+ const body = el("div", { class: "details-body" });
1860
+ details.append(body);
1861
+ if (note.note || noteEditorOpen === componentName) {
1862
+ const input = el("input", { type: "text", class: "note-input", placeholder: 'Note for this component (e.g. "known, ticket #123")', value: note.note || "" });
1863
+ input.addEventListener("change", () => {
1864
+ setNote(componentName, { note: input.value.trim() || void 0 });
1865
+ noteEditorOpen = null;
1866
+ });
1867
+ input.addEventListener("keydown", (e) => {
1868
+ if (e.key === "Escape") {
1869
+ noteEditorOpen = null;
1870
+ renderDetails();
1871
+ }
1872
+ });
1873
+ body.append(el("div", { class: "note-box" + (note.muted ? " muted" : "") }, [el("span", { class: "glyph", text: "\u270E" }), input, note.muted ? el("span", { class: "badge", text: "muted" }) : null]));
1874
+ if (noteEditorOpen === componentName) setTimeout(() => input.focus(), 0);
1875
+ }
1876
+ if (state.tab === "history") {
1877
+ const list = el("ul", { class: "history" });
1878
+ for (const r2 of [...node.reports].reverse()) {
1879
+ list.append(
1880
+ el(
1881
+ "li",
1882
+ {
1883
+ class: state.selectedReport === r2 ? "selected" : "",
1884
+ onclick: () => {
1885
+ state.selectedReport = r2;
1886
+ state.tab = "latest";
1887
+ renderDetails();
1888
+ }
1889
+ },
1890
+ [
1891
+ el("span", { class: "t", text: fmtTime(r2.receivedAt) }),
1892
+ el("span", { class: "n", text: "#" + r2.renderCount }),
1893
+ el("span", { class: "verdict " + (r2.avoidable ? "avoid" : "ok"), text: r2.avoidable ? "avoidable" : r2.trigger }),
1894
+ el("span", { class: "sum", text: summarize(r2) })
1895
+ ]
1896
+ )
1897
+ );
1898
+ }
1899
+ body.append(list);
1900
+ return;
1901
+ }
1902
+ if (state.tab === "fix") {
1903
+ body.append(fixView(rankFixes(node.reports), { copy: copyText }));
1904
+ return;
1905
+ }
1906
+ const r = state.selectedReport || node.lastReport;
1907
+ if (!r) return;
1908
+ body.append(reportView(r, reportActions()));
1909
+ }
1910
+ function rootsList(roots) {
1911
+ return el(
1912
+ "ul",
1913
+ { class: "roots" },
1914
+ roots.map(
1915
+ (root2) => el("li", null, [
1916
+ el("a", { class: "root-link", href: "#", title: "Every commit this component started", onclick: (e) => (e.preventDefault(), showRoot(root2.name)) }, `<${root2.name}>`),
1917
+ ` (${root2.trigger}) \u2192 ${plural(root2.count, "avoidable re-render")}: `,
1918
+ el("span", { class: "meta", text: componentList(root2.components) })
1919
+ ])
1920
+ )
1921
+ );
1922
+ }
1923
+ function renderCommitDetails() {
1924
+ const key = state.selectedCommit;
1925
+ const reports = state.commits.get(key);
1926
+ if (!reports) {
1927
+ details.append(el("div", { class: "empty", text: "This commit is no longer buffered." }));
1928
+ return;
1929
+ }
1930
+ const a = analyzeCommit(reports);
1931
+ details.append(
1932
+ el("div", { class: "details-header" }, [
1933
+ el("span", { class: "title", text: key > 0 ? `Commit #${key}` : "Commit" }),
1934
+ el("span", { class: "meta", text: `${plural(a.total, "render")}, ${a.avoidable} avoidable${a.wasted ? ", " + fmtMs(a.wasted) + " wasted" : ""} \xB7 ${fmtTime(a.receivedAt)}` })
1935
+ ])
1936
+ );
1937
+ const body = el("div", { class: "details-body" });
1938
+ details.append(body);
1939
+ if (a.roots.length) body.append(el("div", { class: "section" }, [el("h3", { text: "Root causes" }), rootsList(a.roots)]));
1940
+ if (a.contexts.length) {
1941
+ body.append(
1942
+ el("div", { class: "section" }, [
1943
+ el("h3", { text: "Contexts that changed" }),
1944
+ el("ul", { class: "roots" }, a.contexts.map((c) => el("li", null, [el("b", { text: c.name }), ` \u2192 ${plural(c.consumers, "consumer")} re-rendered${c.avoidable ? ` (${c.avoidable} with an equal value)` : ""}`])))
1945
+ ])
1946
+ );
1947
+ }
1948
+ const cascade = el("div", { class: "cascade" });
1949
+ const walk = (node, depth) => {
1950
+ for (const child of node.children.values()) {
1951
+ const r = child.report;
1952
+ const line = el(
1953
+ "div",
1954
+ {
1955
+ class: "cascade-row" + (r ? r.avoidable ? " avoid" : " ok" : " untracked"),
1956
+ style: `padding-left:${depth * 14}px`,
1957
+ onclick: r ? () => select(nodeOfReport(r), r) : null
1958
+ },
1959
+ [
1960
+ el("span", { class: "tag" }, [el("span", { class: "bracket", text: "<" }), el("span", { class: "name", text: child.name }), el("span", { class: "bracket", text: ">" })]),
1961
+ r ? el("span", { class: "verdict " + (r.avoidable ? "avoid" : "ok"), text: r.avoidable ? "avoidable" : r.trigger }) : el("span", { class: "meta", text: "did not render or untracked" }),
1962
+ child.count && child.count > 1 ? el("span", { class: "meta", text: ` \xD7${child.count}` }) : null,
1963
+ r && r.avoidable ? el("span", { class: "meta", text: " " + summarize(r) }) : null
1964
+ ]
1965
+ );
1966
+ cascade.append(line);
1967
+ walk(child, depth + 1);
1968
+ }
1969
+ };
1970
+ walk(cascadeTree(reports), 0);
1971
+ body.append(el("div", { class: "section" }, [el("h3", { text: "Render cascade" }), cascade]));
1972
+ if (a.fixes.length) {
1973
+ body.append(el("div", { class: "section-title", text: "Fixes for this commit" }));
1974
+ body.append(fixView(a.fixes, { copy: copyText }));
1975
+ }
1976
+ }
1977
+ function affectedList(reports) {
1978
+ const list = el("ul", { class: "history" });
1979
+ for (const r of reports.slice().reverse()) {
1980
+ list.append(
1981
+ el("li", { onclick: () => select(nodeOfReport(r), r) }, [
1982
+ el("span", { class: "t", text: fmtTime(r.receivedAt) }),
1983
+ el("span", { class: "comp", text: `<${r.component}>` }),
1984
+ el("span", { class: "sum", text: summarize(r) })
1985
+ ])
1986
+ );
1987
+ }
1988
+ return list;
1989
+ }
1990
+ function renderFixDetails() {
1991
+ const fix = rankFixes(filteredReports()).find((f) => f.key === state.selectedFix);
1992
+ if (!fix) {
1993
+ details.append(el("div", { class: "empty", text: "Select a fix." }));
1994
+ return;
1995
+ }
1996
+ details.append(el("div", { class: "details-header" }, [el("span", { class: "title", text: fix.label }), el("span", { class: "meta", text: `removes ${plural(fix.count, "avoidable re-render")}` })]));
1997
+ const body = el("div", { class: "details-body" });
1998
+ details.append(body);
1999
+ body.append(fixView([fix], { copy: copyText }));
2000
+ body.append(el("div", { class: "section" }, [el("h3", { text: "Affected re-renders" }), affectedList(fix.reports)]));
2001
+ }
2002
+ function renderRootDetails() {
2003
+ const name = state.selectedRoot;
2004
+ const s = rootCauseSummary(name, state.commits);
2005
+ details.append(
2006
+ el("div", { class: "details-header" }, [
2007
+ el("span", { class: "title" }, ["Root cause ", el("span", { class: "name", text: `<${name}>` })]),
2008
+ el("span", { class: "meta", text: s.commits.length ? `started ${plural(s.commits.length, "commit")} with ${plural(s.total, "avoidable re-render")} (${s.trigger})` : "no commits in the buffer" })
2009
+ ])
2010
+ );
2011
+ const body = el("div", { class: "details-body" });
2012
+ details.append(body);
2013
+ if (!s.commits.length) return;
2014
+ body.append(el("div", { class: "section" }, [el("h3", { text: "Components that re-rendered avoidably because of it" }), el("div", { class: "meta", text: componentList(s.components) })]));
2015
+ const list = el("ul", { class: "commits root-commits" });
2016
+ for (const c of s.commits) {
2017
+ list.append(
2018
+ el("li", { onclick: () => showCommit(c.key) }, [
2019
+ el("span", { class: "id", text: c.key > 0 ? `#${c.key}` : "\u2014" }),
2020
+ el("span", { class: "t", text: fmtTime(c.analysis.receivedAt) }),
2021
+ el("span", { class: "badge avoid", text: `${c.count} avoidable` }),
2022
+ el("span", { class: "root", text: componentList(c.components) })
2023
+ ])
2024
+ );
2025
+ }
2026
+ body.append(el("div", { class: "section" }, [el("h3", { text: "Commits" }), list]));
2027
+ if (s.fixes.length) {
2028
+ body.append(el("div", { class: "section-title", text: "Fixes" }));
2029
+ body.append(fixView(s.fixes, { copy: copyText }));
2030
+ }
2031
+ }
2032
+ let noteEditorOpen = null;
2033
+ function toggleNoteEditor() {
2034
+ const node = state.selectedKey ? state.nodesByKey.get(state.selectedKey) : null;
2035
+ const name = node ? node.lastReport ? node.lastReport.component : node.name : null;
2036
+ noteEditorOpen = noteEditorOpen === name ? null : name;
2037
+ renderDetails();
2038
+ }
2039
+ function setNote(component, patch) {
2040
+ const next = { ...state.notes[component] || {}, ...patch };
2041
+ if (!next.note && !next.muted) delete state.notes[component];
2042
+ else state.notes[component] = next;
2043
+ if (transport.storage) transport.storage.set("notes", state.notes);
2044
+ renderSummary();
2045
+ renderLeft();
2046
+ renderDetails();
2047
+ }
2048
+ function restoreNotes(raw) {
2049
+ if (!isRecord(raw)) return;
2050
+ const notes = {};
2051
+ for (const [k, v] of Object.entries(raw)) {
2052
+ if (!isRecord(v)) continue;
2053
+ const n = {};
2054
+ if (typeof v.note === "string" && v.note) n.note = v.note;
2055
+ if (v.muted === true) n.muted = true;
2056
+ if (n.note || n.muted) notes[k] = n;
2057
+ }
2058
+ state.notes = notes;
2059
+ renderSummary();
2060
+ renderLeft();
2061
+ renderDetails();
2062
+ }
2063
+ const sessionSummary = (s) => summarizeSession(s, s.reports);
2064
+ function persistSessions() {
2065
+ if (!transport.storage) return;
2066
+ const summaries = state.sessions.filter((s) => s.endedAt).slice(-20).map(sessionSummary);
2067
+ transport.storage.set("sessions", summaries);
2068
+ }
2069
+ function startRecording(name) {
2070
+ if (state.recording) stopRecording();
2071
+ const startedAt = Date.now();
2072
+ const session = {
2073
+ id: `s${startedAt.toString(36)}${Math.random().toString(36).slice(2, 6)}`,
2074
+ name: name || `Session ${state.sessions.length + 1}`,
2075
+ startedAt,
2076
+ endedAt: null,
2077
+ total: 0,
2078
+ avoidable: 0,
2079
+ wasted: 0,
2080
+ byComponent: {},
2081
+ fixes: [],
2082
+ reports: []
2083
+ };
2084
+ state.sessions.push(session);
2085
+ state.recording = session;
2086
+ recordBtn.classList.add("active", "rec");
2087
+ recordBtn.querySelector(".glyph").textContent = "\u23F9";
2088
+ recordBtn.querySelector(".label").textContent = "Stop";
2089
+ state.selectedSession = session.id;
2090
+ state.tab = "session";
2091
+ if (state.view === "sessions") renderLeft();
2092
+ renderDetails();
2093
+ toast(`Recording ${session.name}`);
2094
+ return session;
2095
+ }
2096
+ function stopRecording() {
2097
+ const session = state.recording;
2098
+ if (!session) return null;
2099
+ session.endedAt = Date.now();
2100
+ Object.assign(session, sessionSummary(session), { reports: session.reports });
2101
+ state.recording = null;
2102
+ recordBtn.classList.remove("active", "rec");
2103
+ recordBtn.querySelector(".glyph").textContent = "\u23FA";
2104
+ recordBtn.querySelector(".label").textContent = "Record";
2105
+ persistSessions();
2106
+ if (state.view === "sessions") renderLeft();
2107
+ if (state.tab === "session") renderDetails();
2108
+ toast(`${session.name}: ${plural(session.avoidable, "avoidable re-render")}`);
2109
+ return session;
2110
+ }
2111
+ const fmtDuration = (s) => {
2112
+ const ms = (s.endedAt || Date.now()) - s.startedAt;
2113
+ return ms < 6e4 ? `${(ms / 1e3).toFixed(ms < 1e4 ? 1 : 0)} s` : `${Math.round(ms / 6e4)} min`;
2114
+ };
2115
+ function renderSessions() {
2116
+ table.textContent = "";
2117
+ if (!state.sessions.length) {
2118
+ table.append(
2119
+ el("div", { class: "empty" }, [
2120
+ el("div", { text: "No sessions yet." }),
2121
+ el("div", null, ["Press ", el("code", { text: "Record" }), ", use the app, press ", el("code", { text: "Stop" }), ". Apply a fix, record again, and compare the two."])
2122
+ ])
2123
+ );
2124
+ return;
2125
+ }
2126
+ const list = el("ul", { class: "sessions" });
2127
+ for (const s of [...state.sessions].reverse()) {
2128
+ const live = s === state.recording;
2129
+ const summary2 = live ? sessionSummary(s) : s;
2130
+ list.append(
2131
+ el(
2132
+ "li",
2133
+ {
2134
+ class: (state.selectedSession === s.id && state.tab === "session" ? "selected " : "") + (live ? "live" : ""),
2135
+ onclick: () => {
2136
+ state.selectedSession = s.id;
2137
+ state.tab = "session";
2138
+ renderLeft();
2139
+ renderDetails();
2140
+ }
2141
+ },
2142
+ [
2143
+ el("span", { class: "name", text: s.name }),
2144
+ live ? el("span", { class: "badge rec", text: "recording" }) : el("span", { class: "t", text: fmtDuration(summary2) }),
2145
+ el("span", { class: "n", text: plural(summary2.total, "render") }),
2146
+ summary2.avoidable ? el("span", { class: "badge avoid", text: `${summary2.avoidable} avoidable` }) : el("span", { class: "badge", text: "clean" }),
2147
+ summary2.wasted ? el("span", { class: "meta", text: fmtMs(summary2.wasted) }) : null
2148
+ ]
2149
+ )
2150
+ );
2151
+ }
2152
+ table.append(list);
2153
+ }
2154
+ function deltaCell(n, suffix = "", decimals) {
2155
+ const cls = n < 0 ? "good" : n > 0 ? "bad" : "";
2156
+ const value = decimals !== void 0 ? n.toFixed(decimals) : Number.isInteger(n) ? String(n) : n.toFixed(1);
2157
+ return el("td", { class: "num delta " + cls, text: n === 0 ? "\xB10" : `${n > 0 ? "+" : ""}${value}${suffix}` });
2158
+ }
2159
+ function renderSessionDetails() {
2160
+ const session = state.sessions.find((s) => s.id === state.selectedSession);
2161
+ if (!session) {
2162
+ details.append(el("div", { class: "empty", text: "Select a session." }));
2163
+ return;
2164
+ }
2165
+ const live = session === state.recording;
2166
+ const summary2 = live ? sessionSummary(session) : session;
2167
+ const nameInput = el("input", { type: "text", class: "session-name", value: session.name, title: "Rename" });
2168
+ nameInput.addEventListener("change", () => {
2169
+ session.name = nameInput.value.trim() || session.name;
2170
+ nameInput.value = session.name;
2171
+ persistSessions();
2172
+ if (state.view === "sessions") renderLeft();
2173
+ });
2174
+ details.append(
2175
+ el("div", { class: "details-header" }, [
2176
+ nameInput,
2177
+ el("span", { class: "meta", text: `${live ? "recording \xB7 " : ""}${fmtDuration(summary2)} \xB7 ${plural(summary2.total, "render")}, ${summary2.avoidable} avoidable${summary2.wasted ? ", " + fmtMs(summary2.wasted) + " wasted" : ""}` }),
2178
+ live ? el("button", { class: "ib", onclick: () => stopRecording() }, "\u23F9 Stop") : null
2179
+ ])
2180
+ );
2181
+ const body = el("div", { class: "details-body" });
2182
+ details.append(body);
2183
+ const others = state.sessions.filter((s) => s !== session && s.endedAt);
2184
+ const compareSec = el("div", { class: "section compare" }, [el("h3", { text: "Compare" })]);
2185
+ if (!others.length) {
2186
+ compareSec.append(el("div", { class: "meta", text: "Record a second session (after a fix) to compare against this one." }));
2187
+ } else {
2188
+ const select2 = el("select", { class: "compare-select" });
2189
+ select2.append(el("option", { value: "", text: "Compare with\u2026" }));
2190
+ for (const o of others) select2.append(el("option", { value: o.id, text: o.name }));
2191
+ const baseline = state.compareWith && others.some((o) => o.id === state.compareWith) ? state.compareWith : others[others.length - 1].id;
2192
+ select2.value = baseline;
2193
+ select2.addEventListener("change", () => {
2194
+ state.compareWith = select2.value || null;
2195
+ renderDetails();
2196
+ });
2197
+ compareSec.append(el("div", { class: "meta" }, ["Baseline: ", select2, " \u2192 this session"]));
2198
+ const before = others.find((o) => o.id === baseline);
2199
+ const cmp = compareSessions(before, summary2);
2200
+ const t = el("table", { class: "grid compare-grid" });
2201
+ t.append(el("thead", null, el("tr", null, [el("th", { text: "Avoidable re-renders" }), el("th", { class: "num", text: before.name }), el("th", { class: "num", text: summary2.name }), el("th", { class: "num", text: "\u0394" })])));
2202
+ const tb = el("tbody");
2203
+ const totalRow = el("tr", { class: "total" }, [el("td", { text: "All components" }), el("td", { class: "num", text: String(cmp.avoidable.before) }), el("td", { class: "num", text: String(cmp.avoidable.after) })]);
2204
+ totalRow.append(deltaCell(cmp.avoidable.delta));
2205
+ tb.append(totalRow);
2206
+ if (cmp.wasted.before || cmp.wasted.after) {
2207
+ const w = el("tr", { class: "total" }, [el("td", { text: "Wasted time" }), el("td", { class: "num", text: fmtMs(cmp.wasted.before) }), el("td", { class: "num", text: fmtMs(cmp.wasted.after) })]);
2208
+ w.append(deltaCell(cmp.wasted.delta, " ms", 1));
2209
+ tb.append(w);
2210
+ }
2211
+ for (const row of cmp.rows) {
2212
+ const tr = el("tr", null, [el("td", { class: "c" }, el("span", { class: "name", text: row.component })), el("td", { class: "num", text: String(row.before) }), el("td", { class: "num", text: String(row.after) })]);
2213
+ tr.append(deltaCell(row.delta));
2214
+ tb.append(tr);
2215
+ }
2216
+ t.append(tb);
2217
+ compareSec.append(t);
2218
+ if (cmp.resolvedFixes.length) compareSec.append(el("div", { class: "meta" }, [el("b", { text: "No longer needed: " }), cmp.resolvedFixes.map((f) => f.label).join(" \xB7 ")]));
2219
+ if (cmp.newFixes.length) compareSec.append(el("div", { class: "meta" }, [el("b", { text: "New: " }), cmp.newFixes.map((f) => f.label).join(" \xB7 ")]));
2220
+ }
2221
+ body.append(compareSec);
2222
+ const comps = Object.entries(summary2.byComponent).sort((a, b) => b[1].avoidable - a[1].avoidable || b[1].total - a[1].total);
2223
+ if (comps.length) {
2224
+ const t = el("table", { class: "grid" });
2225
+ t.append(el("thead", null, el("tr", null, [el("th", { text: "Component" }), el("th", { class: "num", text: "Avoidable" }), el("th", { class: "num", text: "Total" }), el("th", { class: "num", text: "Wasted" })])));
2226
+ const tb = el("tbody");
2227
+ for (const [name, c] of comps.slice(0, 50)) {
2228
+ tb.append(
2229
+ el("tr", { onclick: () => panelApi.select(name) }, [
2230
+ el("td", { class: "c" }, el("span", { class: "name", text: name })),
2231
+ el("td", { class: "num" }, c.avoidable ? el("span", { class: "badge avoid", text: String(c.avoidable) }) : "0"),
2232
+ el("td", { class: "num", text: String(c.total) }),
2233
+ el("td", { class: "num", text: c.wasted ? fmtMs(c.wasted) : "" })
2234
+ ])
2235
+ );
2236
+ }
2237
+ t.append(tb);
2238
+ body.append(el("div", { class: "section" }, [el("h3", { text: "Components in this session" }), t]));
2239
+ }
2240
+ if (summary2.fixes.length) {
2241
+ body.append(el("div", { class: "section" }, [el("h3", { text: "Fixes suggested" }), el("ol", { class: "fix-list" }, summary2.fixes.slice(0, 10).map((f) => el("li", null, [el("span", { class: "badge avoid", text: String(f.count) }), " ", el("span", { class: "mono", text: f.label })])))]));
2242
+ }
2243
+ }
2244
+ const itemEls = /* @__PURE__ */ new WeakMap();
2245
+ const streamItems = virtualList(streamList, ITEM_H, (r) => {
2246
+ let li = itemEls.get(r);
2247
+ if (!li) {
2248
+ li = el("div", { class: "stream-item", onclick: () => select(nodeOfReport(r), r) }, [
2249
+ el("span", { class: "t", text: fmtTime(r.receivedAt) }),
2250
+ el("span", { class: "c", text: r.component }),
2251
+ el("span", { class: "v " + (r.avoidable ? "avoid" : "ok"), text: r.avoidable ? "avoidable" : r.trigger }),
2252
+ el("span", { class: "s", text: summarize(r) })
2253
+ ]);
2254
+ itemEls.set(r, li);
2255
+ }
2256
+ return li;
2257
+ });
2258
+ let streamShown = [];
2259
+ function renderStream(batch) {
2260
+ if (batch) {
2261
+ const fresh = batch.filter(passes).reverse();
2262
+ if (fresh.length) streamShown = fresh.concat(streamShown);
2263
+ if (streamShown.length > MAX_REPORTS) streamShown.length = MAX_REPORTS;
2264
+ } else {
2265
+ streamShown = filteredReports().reverse();
2266
+ }
2267
+ streamCount.textContent = plural(streamShown.length, "report");
2268
+ streamItems.setItems(streamShown);
2269
+ }
2270
+ function exportJson() {
2271
+ const data = {
2272
+ rerenderLens: true,
2273
+ version: PROTOCOL,
2274
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
2275
+ origin: state.origin,
2276
+ reports: state.reports,
2277
+ sessions: state.sessions.filter((s) => s.endedAt).map(sessionSummary)
2278
+ };
2279
+ const text = JSON.stringify(data, null, 2);
2280
+ const name = `rerender-lens-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.json`;
2281
+ if (transport.download) {
2282
+ transport.download(name, text);
2283
+ return;
2284
+ }
2285
+ try {
2286
+ const blob = new Blob([text], { type: "application/json" });
2287
+ const url = URL.createObjectURL(blob);
2288
+ const a = el("a", { href: url, download: name });
2289
+ document.body.append(a);
2290
+ a.click();
2291
+ a.remove();
2292
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
2293
+ } catch {
2294
+ copyText(text);
2295
+ }
2296
+ }
2297
+ function importData(data) {
2298
+ const reports = Array.isArray(data) ? data : isRecord(data) && Array.isArray(data.reports) ? data.reports : null;
2299
+ if (!reports) throw new Error("not a rerender-lens export");
2300
+ clearAll();
2301
+ if (isRecord(data) && Array.isArray(data.sessions)) restoreSessions(data.sessions);
2302
+ let n = 0;
2303
+ for (const p of reports) {
2304
+ const r = normalizeReport(p);
2305
+ if (r) {
2306
+ enqueue(r);
2307
+ n++;
2308
+ }
2309
+ }
2310
+ flush();
2311
+ toast(`Imported ${plural(n, "report")}`);
2312
+ return n;
2313
+ }
2314
+ function importFile(file) {
2315
+ const reader = new FileReader();
2316
+ reader.onload = () => {
2317
+ try {
2318
+ importData(JSON.parse(String(reader.result)));
2319
+ } catch (e) {
2320
+ toast(`Import failed: ${e.message}`);
2321
+ }
2322
+ };
2323
+ reader.readAsText(file);
2324
+ }
2325
+ function renderStatus() {
2326
+ const lib = state.library;
2327
+ let text;
2328
+ let cls = "none";
2329
+ let title = "";
2330
+ if (lib) {
2331
+ const react = lib.react && lib.react[0];
2332
+ text = `connected \xB7 lib ${lib.library || "?"}${react && react.version ? ` \xB7 React ${react.version}` : ""}${lib.production ? " (prod)" : ""}`;
2333
+ cls = "connected";
2334
+ title = state.relay ? "live via content script" : "polling the page";
2335
+ if (lib.overhead) title += ` \xB7 library overhead ${lib.overhead.totalMs.toFixed(1)} ms over ${plural(lib.commits ?? 0, "commit")}, worst ${lib.overhead.maxCommitMs.toFixed(1)} ms`;
2336
+ } else if (state.relay || state.polling) {
2337
+ text = "no library in page";
2338
+ cls = "partial";
2339
+ title = "rerender-lens is not running in this page";
2340
+ } else {
2341
+ text = "no page";
2342
+ }
2343
+ status.className = "status " + cls;
2344
+ status.title = title;
2345
+ status.querySelector(".status-text").textContent = text;
2346
+ tabChip.hidden = !state.tabLabel;
2347
+ tabChip.textContent = state.tabLabel || "";
2348
+ banner.textContent = "";
2349
+ const warnings = [];
2350
+ if (lib && typeof lib.protocol === "number" && lib.protocol !== PROTOCOL) {
2351
+ warnings.push(
2352
+ lib.protocol < PROTOCOL ? `The page runs rerender-lens ${lib.library || ""} (protocol ${lib.protocol}); this panel expects protocol ${PROTOCOL}. Update the rerender-lens package for commit grouping, source links and settings.` : `The page runs a newer rerender-lens (protocol ${lib.protocol}) than this panel (${PROTOCOL}). Update the extension.`
2353
+ );
2354
+ }
2355
+ if (lib && lib.production) warnings.push("Production React build detected: component names may be minified and hooks are unlabeled. Use a development build.");
2356
+ if (lib && lib.injected && lib.source === "page")
2357
+ warnings.push(`The page runs its own rerender-lens ${lib.library || ""}; the copy injected by the extension stepped aside. Turn injection off for this origin in Settings to avoid loading the library twice.`);
2358
+ if (lib && lib.enabled === false) warnings.push("rerender-lens is present but disabled in this page.");
2359
+ banner.hidden = warnings.length === 0;
2360
+ for (const w of warnings) banner.append(el("div", { text: w }));
2361
+ }
2362
+ function setRelay(on) {
2363
+ state.relay = on;
2364
+ if (!on) state.library = null;
2365
+ renderStatus();
2366
+ }
2367
+ function setLibrary(info) {
2368
+ state.library = info;
2369
+ renderStatus();
2370
+ if (state.settingsOpen) void renderSettings();
2371
+ if (state.flashOn) transport.flashAvoidable?.(true);
2372
+ }
2373
+ function toggleSettings(open) {
2374
+ state.settingsOpen = open === void 0 ? !state.settingsOpen : open;
2375
+ settings.hidden = !state.settingsOpen;
2376
+ settingsBtn.classList.toggle("active", state.settingsOpen);
2377
+ settingsBtn.setAttribute("aria-expanded", String(state.settingsOpen));
2378
+ if (state.settingsOpen) void renderSettings().then(() => settings.querySelector("input, button")?.focus());
2379
+ else settingsBtn.focus();
2380
+ }
2381
+ function optionRow(label, key, current, onchange) {
2382
+ const input = el("input", { type: "checkbox" });
2383
+ input.checked = !!current[key];
2384
+ input.addEventListener("change", () => onchange({ [key]: input.checked }));
2385
+ return el("label", { class: "opt" }, [input, label]);
2386
+ }
2387
+ async function renderSettings() {
2388
+ settings.textContent = "";
2389
+ settings.append(el("div", { class: "drawer-header" }, [el("b", { text: "Settings" }), el("button", { onclick: () => toggleSettings(false) }, "\u2715")]));
2390
+ const body = el("div", { class: "drawer-body" });
2391
+ settings.append(body);
2392
+ if (transport.originStatus) {
2393
+ const site = el("div", { class: "section" }, [el("h3", { text: "This site" }), el("div", { class: "meta", text: state.origin || "" })]);
2394
+ body.append(site);
2395
+ try {
2396
+ const st = await transport.originStatus();
2397
+ if (st) {
2398
+ const enabled = el("input", { type: "checkbox" });
2399
+ enabled.checked = st.enabled;
2400
+ enabled.disabled = st.builtIn;
2401
+ const inject = el("input", { type: "checkbox" });
2402
+ inject.checked = st.inject;
2403
+ inject.disabled = !st.enabled;
2404
+ const defer = el("input", { type: "checkbox" });
2405
+ defer.checked = !!st.deferHook;
2406
+ defer.disabled = !st.inject;
2407
+ const msg = el("div", { class: "meta" });
2408
+ const apply = async () => {
2409
+ try {
2410
+ if (enabled.checked && !st.permitted && transport.requestPermission) {
2411
+ const ok = await transport.requestPermission();
2412
+ if (!ok) {
2413
+ msg.textContent = "Permission not granted. You can also enable the site from the toolbar icon.";
2414
+ enabled.checked = false;
2415
+ return;
2416
+ }
2417
+ }
2418
+ await transport.setOrigin?.({ enabled: enabled.checked, inject: enabled.checked && inject.checked, deferHook: inject.checked && defer.checked });
2419
+ void renderSettings();
2420
+ } catch (e) {
2421
+ msg.textContent = String(e.message || e);
2422
+ }
2423
+ };
2424
+ enabled.addEventListener("change", () => {
2425
+ if (!enabled.checked) inject.checked = false;
2426
+ void apply();
2427
+ });
2428
+ inject.addEventListener("change", () => void apply());
2429
+ defer.addEventListener("change", () => void apply());
2430
+ site.append(
2431
+ el("label", { class: "opt" }, [enabled, st.builtIn ? "Enabled (local development host)" : "Enable on this site"]),
2432
+ el("label", { class: "opt" }, [inject, "Inject the library into the page (no app code needed)"]),
2433
+ el("label", { class: "opt", title: "Only needed when React DevTools is installed and its Components tab comes up empty" }, [defer, "Let React DevTools create the hook (if both are installed)"]),
2434
+ el("div", { class: "meta", text: st.inject ? "Injection is on. Reload the page after changing it." : "Without injection the page must call init({ notifier: createDevtoolsNotifier() })." }),
2435
+ msg
2436
+ );
2437
+ }
2438
+ } catch (e) {
2439
+ site.append(el("div", { class: "meta", text: String(e.message || e) }));
2440
+ }
2441
+ }
2442
+ const flash = el("input", { type: "checkbox" });
2443
+ flash.checked = state.flashOn;
2444
+ flash.addEventListener("change", () => {
2445
+ state.flashOn = flash.checked;
2446
+ transport.flashAvoidable?.(state.flashOn);
2447
+ persist();
2448
+ });
2449
+ body.append(
2450
+ el("div", { class: "section" }, [
2451
+ el("h3", { text: "Panel" }),
2452
+ el("label", { class: "opt" }, [flash, "Flash avoidable re-renders in the page"]),
2453
+ el("div", { class: "meta", text: "Shortcuts: / search, f fix tab, Esc clear highlight, arrows in the tree." })
2454
+ ])
2455
+ );
2456
+ const lib = state.library;
2457
+ const sec = el("div", { class: "section" }, [el("h3", { text: "Library options" })]);
2458
+ body.append(sec);
2459
+ if (!lib || !transport.configure) {
2460
+ sec.append(el("div", { class: "meta", text: "Connect to a page running rerender-lens to change its options." }));
2461
+ return;
2462
+ }
2463
+ if (lib.protocol < PROTOCOL) {
2464
+ sec.append(el("div", { class: "meta", text: "The page library is too old to be configured from here." }));
2465
+ return;
2466
+ }
2467
+ const current = Object.assign({}, lib.options || {});
2468
+ const applyOptions = async (patch) => {
2469
+ Object.assign(current, patch);
2470
+ try {
2471
+ const applied = await transport.configure(patch);
2472
+ if (applied && state.library) state.library.options = applied;
2473
+ if (transport.storage && state.library) transport.storage.set("settings", Object.assign({}, state.library.options));
2474
+ toast("Applied");
2475
+ } catch (e) {
2476
+ toast(`Failed: ${e.message}`);
2477
+ }
2478
+ };
2479
+ sec.append(
2480
+ optionRow("Track every React.memo / PureComponent", "trackAllMemoized", current, applyOptions),
2481
+ optionRow("Track every component (noisy)", "trackAllComponents", current, applyOptions),
2482
+ optionRow("Diff hook state and contexts", "trackHooks", { trackHooks: current.trackHooks !== false }, applyOptions),
2483
+ optionRow("Include current hooks, state and contexts in every report", "includeState", { includeState: current.includeState !== false }, applyOptions),
2484
+ optionRow("Resolve custom hook names (re-runs each component type once)", "resolveHookNames", current, applyOptions),
2485
+ optionRow("Ignore Fast Refresh commits", "ignoreHotReload", { ignoreHotReload: current.ignoreHotReload !== false }, applyOptions),
2486
+ optionRow("Print to the page console", "silent", { silent: !current.silent }, (p) => applyOptions({ silent: !p.silent })),
2487
+ optionRow("Print genuine re-renders too (logAll)", "logAll", current, applyOptions)
2488
+ );
2489
+ const listInput = (label, key) => {
2490
+ const input = el("input", { type: "text", placeholder: "Name, /regex/, ...", value: (current[key] || []).join(", ") });
2491
+ input.addEventListener("change", () => void applyOptions({ [key]: input.value.split(",").map((s) => s.trim()).filter(Boolean) }));
2492
+ return el("label", { class: "opt col" }, [label, input]);
2493
+ };
2494
+ sec.append(listInput("Include (display names)", "include"), listInput("Exclude", "exclude"));
2495
+ const max = el("input", { type: "number", min: "0", value: String(current.maxReportsPerComponent || 0) });
2496
+ max.addEventListener("change", () => void applyOptions({ maxReportsPerComponent: Math.max(0, Number(max.value) || 0) }));
2497
+ sec.append(el("label", { class: "opt col" }, ["Stop printing a component after N reports (0 = never)", max]));
2498
+ }
2499
+ function handle(message) {
2500
+ if (!isRecord(message)) return;
2501
+ if (transport.origin && transport.origin !== state.origin) state.origin = transport.origin;
2502
+ switch (message.type) {
2503
+ case "connected":
2504
+ setRelay(true);
2505
+ break;
2506
+ case "disconnected":
2507
+ setRelay(false);
2508
+ break;
2509
+ case "polling":
2510
+ state.polling = !!message.on;
2511
+ renderStatus();
2512
+ break;
2513
+ case "tab-label":
2514
+ state.tabLabel = typeof message.payload === "string" ? message.payload : null;
2515
+ renderStatus();
2516
+ break;
2517
+ case "tab":
2518
+ state.tabLabel = typeof message.payload === "string" ? message.payload : null;
2519
+ state.origin = transport.origin || null;
2520
+ clearAll();
2521
+ state.library = null;
2522
+ renderStatus();
2523
+ break;
2524
+ case "hello":
2525
+ setLibrary(isRecord(message.payload) ? Object.assign({ protocol: message.version || 1 }, message.payload) : { protocol: message.version || 1 });
2526
+ break;
2527
+ case "clear":
2528
+ case "navigated":
2529
+ clearAll();
2530
+ if (message.type === "navigated") {
2531
+ state.library = null;
2532
+ renderStatus();
2533
+ }
2534
+ break;
2535
+ case "report": {
2536
+ if (state.paused) break;
2537
+ const r = normalizeReport(message.payload);
2538
+ if (r) enqueue(r);
2539
+ break;
2540
+ }
2541
+ }
2542
+ }
2543
+ const panelApi = {
2544
+ state,
2545
+ handle,
2546
+ flush,
2547
+ clearAll,
2548
+ importData,
2549
+ select: (name) => {
2550
+ for (const n of state.nodesByKey.values()) if (n.name === name) return select(n);
2551
+ },
2552
+ setView,
2553
+ openSettings: () => toggleSettings(true),
2554
+ startRecording,
2555
+ stopRecording,
2556
+ setNote
2557
+ };
2558
+ if (options.theme === "dark") document.documentElement.classList.add("theme-dark");
2559
+ state.origin = transport.origin || null;
2560
+ setView(state.view);
2561
+ renderDetails();
2562
+ renderStream();
2563
+ renderSummary();
2564
+ renderStatus();
2565
+ measure();
2566
+ if (transport.storage) {
2567
+ Promise.resolve(transport.storage.get("panel")).then(restore, () => {
2568
+ });
2569
+ Promise.resolve(transport.storage.get("sessions")).then(restoreSessions, () => {
2570
+ });
2571
+ Promise.resolve(transport.storage.get("notes")).then(restoreNotes, () => {
2572
+ });
2573
+ }
2574
+ transport.subscribe(handle);
2575
+ return panelApi;
2576
+ }
2577
+ function createRelayTransport(io) {
2578
+ let listener = null;
2579
+ let relayConnected = false;
2580
+ let pollTimer = null;
2581
+ let since = 0;
2582
+ let origin = null;
2583
+ let panelPort = null;
2584
+ let currentTab = io.tabId();
2585
+ const emit = (m) => {
2586
+ if (listener) listener(m);
2587
+ };
2588
+ const send = (message) => new Promise(
2589
+ (resolve, reject) => chrome.runtime.sendMessage(message, (res) => {
2590
+ if (chrome.runtime.lastError) reject(new Error(chrome.runtime.lastError.message));
2591
+ else if (!res || !res.ok) reject(new Error(res && res.error || "no response"));
2592
+ else resolve(res.result);
2593
+ })
2594
+ );
2595
+ async function resolveOrigin() {
2596
+ origin = await io.origin().catch(() => null);
2597
+ transport.origin = origin;
2598
+ }
2599
+ async function syncWithPage() {
2600
+ try {
2601
+ const info = await io.bridge("info");
2602
+ if (info) {
2603
+ emit({ type: "hello", version: info.protocol || 1, payload: info });
2604
+ return true;
2605
+ }
2606
+ } catch {
2607
+ }
2608
+ return false;
2609
+ }
2610
+ async function pollOnce() {
2611
+ try {
2612
+ const res = await io.bridge("pull", since);
2613
+ if (!res) return;
2614
+ if (res.dropped) emit({ type: "clear" });
2615
+ for (const p of res.reports) emit({ type: "report", payload: p });
2616
+ since = res.seq;
2617
+ } catch {
2618
+ }
2619
+ }
2620
+ function setPolling(on) {
2621
+ if (on && !pollTimer) {
2622
+ pollTimer = setInterval(() => void pollOnce(), 500);
2623
+ emit({ type: "polling", on: true });
2624
+ } else if (!on && pollTimer) {
2625
+ clearInterval(pollTimer);
2626
+ pollTimer = null;
2627
+ emit({ type: "polling", on: false });
2628
+ }
2629
+ }
2630
+ async function attachToPage() {
2631
+ if (!await syncWithPage()) return;
2632
+ if (relayConnected) io.bridge("replay").catch(() => {
2633
+ });
2634
+ else {
2635
+ const res = await io.bridge("pull", 0).catch(() => null);
2636
+ if (res) {
2637
+ for (const p of res.reports) emit({ type: "report", payload: p });
2638
+ since = res.seq;
2639
+ } else io.bridge("replay").catch(() => {
2640
+ });
2641
+ setPolling(true);
2642
+ }
2643
+ }
2644
+ function connect() {
2645
+ if (panelPort) {
2646
+ try {
2647
+ panelPort.disconnect();
2648
+ } catch {
2649
+ }
2650
+ panelPort = null;
2651
+ }
2652
+ if (currentTab === null) return;
2653
+ const port = chrome.runtime.connect({ name: "rerender-lens-panel" });
2654
+ panelPort = port;
2655
+ port.postMessage({ type: "init", tabId: currentTab });
2656
+ port.onMessage.addListener((m) => {
2657
+ if (!m || panelPort !== port) return;
2658
+ if (m.type === "connected") {
2659
+ relayConnected = true;
2660
+ setPolling(false);
2661
+ } else if (m.type === "disconnected") {
2662
+ relayConnected = false;
2663
+ void syncWithPage().then((ok) => ok && setPolling(true));
2664
+ }
2665
+ emit(m);
2666
+ });
2667
+ port.onDisconnect.addListener(() => {
2668
+ if (panelPort !== port) return;
2669
+ panelPort = null;
2670
+ setTimeout(connect, 1e3);
2671
+ });
2672
+ }
2673
+ const transport = {
2674
+ origin,
2675
+ tabLabel: io.tabLabel ?? null,
2676
+ subscribe(fn) {
2677
+ listener = fn;
2678
+ connect();
2679
+ io.onNavigated(() => {
2680
+ since = 0;
2681
+ fn({ type: "navigated" });
2682
+ void resolveOrigin().then(() => {
2683
+ setTimeout(() => void attachToPage(), 1200);
2684
+ });
2685
+ });
2686
+ io.onTabChange?.((label) => {
2687
+ transport.tabLabel = label;
2688
+ const next = io.tabId();
2689
+ if (next === currentTab) {
2690
+ fn({ type: "tab-label", payload: label });
2691
+ return;
2692
+ }
2693
+ since = 0;
2694
+ relayConnected = false;
2695
+ setPolling(false);
2696
+ currentTab = next;
2697
+ void resolveOrigin().then(() => {
2698
+ fn({ type: "tab", payload: label });
2699
+ connect();
2700
+ void attachToPage();
2701
+ });
2702
+ });
2703
+ void resolveOrigin().then(() => attachToPage());
2704
+ },
2705
+ replay() {
2706
+ if (relayConnected) io.bridge("replay").catch(() => {
2707
+ });
2708
+ else {
2709
+ since = 0;
2710
+ emit({ type: "clear" });
2711
+ void syncWithPage().then(() => pollOnce());
2712
+ }
2713
+ },
2714
+ clear() {
2715
+ io.bridge("clear").catch(() => {
2716
+ });
2717
+ since = 0;
2718
+ },
2719
+ configure: (options) => io.bridge("configure", options).then((r) => r ?? void 0),
2720
+ highlight: (id) => io.bridge("highlight", id).catch(() => {
2721
+ }),
2722
+ flashAvoidable: (on) => io.bridge("flash", !!on).catch(() => {
2723
+ }),
2724
+ originStatus: () => origin ? send({ type: "origin:status", origin }) : Promise.resolve(null),
2725
+ setOrigin: (cfg) => send({ type: "origin:set", origin, enabled: cfg.enabled, inject: cfg.inject, deferHook: cfg.deferHook }),
2726
+ requestPermission: () => chrome.permissions.request({ origins: [origin + "/*"] }),
2727
+ storage: {
2728
+ get: (key) => new Promise((resolve) => chrome.storage.local.get(`${key}:${origin}`, (got) => resolve(got ? got[`${key}:${origin}`] : void 0))),
2729
+ set: (key, value) => new Promise((resolve) => chrome.storage.local.set({ [`${key}:${origin}`]: value }, resolve))
2730
+ },
2731
+ badge(count) {
2732
+ if (panelPort) panelPort.postMessage({ type: "badge", count });
2733
+ },
2734
+ copy: (text) => navigator.clipboard.writeText(text).catch(() => {
2735
+ })
2736
+ };
2737
+ if (io.openResource) transport.openResource = io.openResource;
2738
+ if (io.undock) transport.undock = io.undock;
2739
+ if (io.readSource) transport.readSource = io.readSource;
2740
+ transport.panelUrl = chrome.runtime.getURL("panel.html");
2741
+ return transport;
2742
+ }
2743
+ function devtoolsIO() {
2744
+ const tabId = chrome.devtools.inspectedWindow.tabId;
2745
+ const evalIn = (code) => new Promise(
2746
+ (resolve, reject) => chrome.devtools.inspectedWindow.eval(code, (result, err) => {
2747
+ if (err && (err.isException || err.isError)) reject(new Error(err.value || err.description || "eval failed"));
2748
+ else resolve(result);
2749
+ })
2750
+ );
2751
+ const expressions = {
2752
+ info: () => "b.info?b.info():{count:b.size,protocol:b.version}",
2753
+ pull: (since) => `b.pull?b.pull(${Number(since) || 0}):null`,
2754
+ replay: () => "b.replay()",
2755
+ clear: () => "b.clear()",
2756
+ configure: (o) => `b.configure(${JSON.stringify(o ?? {})})`,
2757
+ highlight: (id) => `b.highlight(${id === null || id === void 0 ? "null" : Number(id)})`,
2758
+ flash: (on) => `b.flashAvoidable(${!!on})`
2759
+ };
2760
+ return {
2761
+ tabId: () => tabId,
2762
+ origin: () => evalIn("location.origin"),
2763
+ bridge: (cmd, arg) => evalIn(`(function(){var b=window.__RERENDER_LENS_DEVTOOLS__;if(!b)return null;try{return (${expressions[cmd](arg)});}catch(e){return {__error:String(e)}}})()`).then((r) => {
2764
+ if (isRecord(r) && typeof r.__error === "string") throw new Error(r.__error);
2765
+ return r;
2766
+ }),
2767
+ onNavigated: (cb) => chrome.devtools.network.onNavigated.addListener(cb),
2768
+ openResource(url, line, col) {
2769
+ if (chrome.devtools.panels.openResource) chrome.devtools.panels.openResource(url, Math.max(0, (line || 1) - 1), Math.max(0, (col || 1) - 1), () => {
2770
+ });
2771
+ },
2772
+ undock: (mode) => openOutside(mode, tabId),
2773
+ readSource: (url) => new Promise((resolve) => {
2774
+ const bare = url.replace(/\?.*$/, "");
2775
+ chrome.devtools.inspectedWindow.getResources((resources) => {
2776
+ const res = resources.find((x) => x.url === url) || resources.find((x) => x.url.replace(/\?.*$/, "") === bare);
2777
+ if (!res) return resolve(null);
2778
+ res.getContent((content) => resolve(typeof content === "string" ? content : null));
2779
+ });
2780
+ })
2781
+ };
2782
+ }
2783
+ function pageBridgeCommand(cmd, arg) {
2784
+ const b = window.__RERENDER_LENS_DEVTOOLS__;
2785
+ if (!b) return null;
2786
+ try {
2787
+ switch (cmd) {
2788
+ case "info":
2789
+ return b.info ? b.info() : { count: b.size, protocol: b.version };
2790
+ case "pull":
2791
+ return b.pull ? b.pull(arg) : null;
2792
+ case "replay":
2793
+ b.replay?.();
2794
+ return true;
2795
+ case "clear":
2796
+ b.clear?.();
2797
+ return true;
2798
+ case "configure":
2799
+ return b.configure ? b.configure(arg) : null;
2800
+ case "highlight":
2801
+ return b.highlight ? b.highlight(arg) : false;
2802
+ case "flash":
2803
+ b.flashAvoidable?.(arg);
2804
+ return true;
2805
+ }
2806
+ } catch (e) {
2807
+ return { __error: String(e) };
2808
+ }
2809
+ return null;
2810
+ }
2811
+ async function openOutside(mode, tabId) {
2812
+ if (mode === "sidepanel") {
2813
+ const sp = chrome.sidePanel;
2814
+ if (!sp) throw new Error('This browser has no side panel API; use "Window" instead.');
2815
+ await sp.setOptions({ tabId, path: "sidepanel.html?tabId=" + encodeURIComponent(String(tabId)), enabled: true });
2816
+ await sp.open({ tabId });
2817
+ return true;
2818
+ }
2819
+ return new Promise(
2820
+ (resolve, reject) => chrome.runtime.sendMessage({ type: "window:open", tabId }, (res) => {
2821
+ if (chrome.runtime.lastError) reject(new Error(chrome.runtime.lastError.message));
2822
+ else if (!res || !res.ok) reject(new Error(res && res.error || "could not open a window"));
2823
+ else resolve(true);
2824
+ })
2825
+ );
2826
+ }
2827
+ function standaloneIO(opts = {}) {
2828
+ let tabId = typeof opts.tabId === "number" ? opts.tabId : null;
2829
+ const pinned = tabId !== null;
2830
+ let label = null;
2831
+ const labelOf = (tab) => {
2832
+ if (!tab) return null;
2833
+ const o = tab.url ? tab.url.replace(/^https?:\/\//, "").replace(/\/.*$/, "") : "";
2834
+ return tab.title ? `${tab.title}${o ? " \xB7 " + o : ""}` : o || null;
2835
+ };
2836
+ const exec = (target, world, func, args = []) => chrome.scripting.executeScript({ target: { tabId: target }, world, func, args }).then((results) => results && results[0] ? results[0].result : null);
2837
+ const io = {
2838
+ tabId: () => tabId,
2839
+ tabLabel: label,
2840
+ async origin() {
2841
+ if (tabId === null) return null;
2842
+ try {
2843
+ return await exec(tabId, "ISOLATED", () => location.origin);
2844
+ } catch {
2845
+ try {
2846
+ const tab = await chrome.tabs.get(tabId);
2847
+ return tab && tab.url ? new URL(tab.url).origin : null;
2848
+ } catch {
2849
+ return null;
2850
+ }
2851
+ }
2852
+ },
2853
+ bridge: (cmd, arg) => {
2854
+ if (tabId === null) return Promise.resolve(null);
2855
+ return exec(tabId, "MAIN", pageBridgeCommand, [cmd, arg ?? null]).then((r) => {
2856
+ if (isRecord(r) && typeof r.__error === "string") throw new Error(r.__error);
2857
+ return r;
2858
+ });
2859
+ },
2860
+ onNavigated(cb) {
2861
+ chrome.tabs.onUpdated.addListener((id, info) => {
2862
+ if (id === tabId && info.status === "loading") cb();
2863
+ });
2864
+ },
2865
+ onTabChange(cb) {
2866
+ const announce = async () => {
2867
+ try {
2868
+ const tab = tabId === null ? void 0 : await chrome.tabs.get(tabId);
2869
+ label = labelOf(tab);
2870
+ } catch {
2871
+ label = null;
2872
+ }
2873
+ cb(label);
2874
+ };
2875
+ if (pinned) {
2876
+ chrome.tabs.onUpdated.addListener((id, info) => {
2877
+ if (id === tabId && (info.title || info.url)) void announce();
2878
+ });
2879
+ void announce();
2880
+ return;
2881
+ }
2882
+ chrome.tabs.onActivated.addListener(({ tabId: active }) => {
2883
+ tabId = active;
2884
+ void announce();
2885
+ });
2886
+ void chrome.tabs.query({ active: true, currentWindow: true }).then((tabs) => {
2887
+ const t = tabs && tabs[0];
2888
+ if (t && typeof t.id === "number") {
2889
+ tabId = t.id;
2890
+ void announce();
2891
+ }
2892
+ });
2893
+ },
2894
+ openResource: (url) => void chrome.tabs.create({ url }),
2895
+ undock: (mode) => tabId === null ? Promise.reject(new Error("no tab")) : openOutside(mode, tabId),
2896
+ // Dev servers serve the module source; host permission for the origin is required (and present when the panel works at all).
2897
+ readSource: (url) => fetch(url).then((res) => res.ok ? res.text() : null).catch(() => null)
2898
+ };
2899
+ return io;
2900
+ }
2901
+ function bootExtension() {
2902
+ const prefersDark = typeof matchMedia === "function" && matchMedia("(prefers-color-scheme: dark)").matches;
2903
+ const theme = chrome.devtools.panels.themeName === "dark" || prefersDark ? "dark" : "light";
2904
+ createPanel(document.getElementById("root"), createRelayTransport(devtoolsIO()), { theme });
2905
+ }
2906
+ function bootStandalone(opts = {}) {
2907
+ const prefersDark = typeof matchMedia === "function" && matchMedia("(prefers-color-scheme: dark)").matches;
2908
+ return createPanel(document.getElementById("root"), createRelayTransport(standaloneIO(opts)), { theme: prefersDark ? "dark" : "light" });
2909
+ }
2910
+ function sampleReports() {
2911
+ const fn = (name) => FN_PREFIX + name;
2912
+ return [
2913
+ {
2914
+ component: "ProductRow",
2915
+ path: ["App", "ProductPage", "ProductList"],
2916
+ trigger: "parent",
2917
+ avoidable: true,
2918
+ renderCount: 1,
2919
+ instanceId: 4,
2920
+ commitId: 1,
2921
+ memoized: true,
2922
+ commitPriority: "immediate",
2923
+ owner: "ProductList",
2924
+ parent: { name: "ProductPage", trigger: "state" },
2925
+ selfDuration: 0.8,
2926
+ treeDuration: 1.1,
2927
+ source: { fileName: "http://localhost:5199/src/ProductList.tsx", lineNumber: 14, columnNumber: 7 },
2928
+ props: {
2929
+ prev: { product: { id: 1, name: "Keyboard", price: 49 }, style: { color: "red" }, onSelect: fn("onSelect"), selected: false },
2930
+ next: { product: { id: 1, name: "Keyboard", price: 49 }, style: { color: "red" }, onSelect: fn("onSelect"), selected: false }
2931
+ },
2932
+ propChanges: [
2933
+ { path: "style", kind: "deep-equal", prev: { color: "red" }, next: { color: "red" } },
2934
+ { path: "onSelect", kind: "function", prev: fn("onSelect"), next: fn("onSelect") }
2935
+ ],
2936
+ stateChanges: [],
2937
+ hookChanges: [],
2938
+ reasons: [
2939
+ "caused by <ProductPage> re-rendering (its state changed).",
2940
+ 'prop "style" is a new reference but deep-equal to the previous value: memoize the object with useMemo, or hoist it to module scope if it is constant.',
2941
+ `prop "onSelect" is a new function instance on every render: wrap it in useCallback (or hoist it out of the parent's render).`
2942
+ ]
2943
+ },
2944
+ {
2945
+ component: "Toolbar",
2946
+ path: ["App", "ProductPage"],
2947
+ trigger: "parent",
2948
+ avoidable: true,
2949
+ renderCount: 1,
2950
+ instanceId: 2,
2951
+ commitId: 1,
2952
+ memoized: false,
2953
+ commitPriority: "immediate",
2954
+ owner: "ProductPage",
2955
+ parent: { name: "ProductPage", trigger: "state" },
2956
+ selfDuration: 0.3,
2957
+ props: { prev: { title: "Products", count: 3 }, next: { title: "Products", count: 3 } },
2958
+ propChanges: [],
2959
+ stateChanges: [],
2960
+ hookChanges: [],
2961
+ reasons: ['re-rendered with identical props because <ProductPage> re-rendered (its state changed). Wrap "Toolbar" in React.memo (or extend PureComponent).']
2962
+ },
2963
+ {
2964
+ component: "ProductPage",
2965
+ path: ["App"],
2966
+ trigger: "state",
2967
+ avoidable: false,
2968
+ renderCount: 1,
2969
+ instanceId: 3,
2970
+ commitId: 1,
2971
+ memoized: false,
2972
+ owner: "App",
2973
+ parent: null,
2974
+ props: { prev: { placeholder: "Search", filters: { sort: "asc", page: 1 } }, next: { placeholder: "Search", filters: { sort: "asc", page: 2 } } },
2975
+ propChanges: [{ path: "filters", kind: "different", prev: { sort: "asc", page: 1 }, next: { sort: "asc", page: 2 } }],
2976
+ stateChanges: [],
2977
+ hookChanges: [{ path: "useState#0", hook: "useState", index: 0, kind: "different", prev: "ab", next: "abc" }],
2978
+ reasons: ["useState #0 changed."],
2979
+ hookState: [
2980
+ { path: "useState#0", hook: "useState", index: 0, value: "abc" },
2981
+ { path: "useState#1", hook: "useState", index: 1, value: 3 },
2982
+ { path: "useReducer#3", hook: "useReducer", index: 3, value: { cart: [1, 2], open: false } }
2983
+ ],
2984
+ contexts: [{ name: "Theme", value: { mode: "light", user: "ann" } }]
2985
+ },
2986
+ {
2987
+ component: "Sidebar",
2988
+ path: ["App"],
2989
+ trigger: "hooks",
2990
+ avoidable: false,
2991
+ renderCount: 1,
2992
+ instanceId: 5,
2993
+ owner: "App",
2994
+ parent: null,
2995
+ commitId: 2,
2996
+ memoized: true,
2997
+ commitPriority: "normal",
2998
+ props: { prev: {}, next: {} },
2999
+ propChanges: [],
3000
+ stateChanges: [],
3001
+ hookChanges: [{ path: "useContext(Theme)", hook: "useContext", index: 0, kind: "different", prev: { mode: "light", user: "ann" }, next: { mode: "dark", user: "ann" }, provider: { component: "App", path: ["App"] }, changedKeys: ["mode"], totalKeys: 2 }],
3002
+ reasons: ['useContext(Theme) changed (provided by <App>): only "mode" of 2 keys changed, yet every consumer re-renders. Split the context or memoize the slices consumers read.']
3003
+ }
3004
+ ];
3005
+ }
3006
+ function floodReports(n) {
3007
+ const out = [];
3008
+ const components = Math.max(10, Math.floor(n / 10));
3009
+ for (let i = 0; i < n; i++) {
3010
+ const id = i % components;
3011
+ const depth = 1 + id % 6;
3012
+ const path = ["App"];
3013
+ for (let d = 1; d < depth; d++) path.push(`Section${(id * 7 + d) % 40}`);
3014
+ const avoidable = id % 3 !== 0;
3015
+ out.push({
3016
+ component: `Item${id}`,
3017
+ path,
3018
+ trigger: avoidable ? "parent" : "props",
3019
+ avoidable,
3020
+ renderCount: Math.floor(i / components) + 1,
3021
+ instanceId: id + 1,
3022
+ commitId: Math.floor(i / 50) + 1,
3023
+ memoized: id % 2 === 0,
3024
+ owner: path[path.length - 1],
3025
+ parent: { name: path[path.length - 1], trigger: "state" },
3026
+ selfDuration: id % 7 / 10,
3027
+ props: { prev: { style: { w: id }, n: i }, next: { style: { w: id }, n: i + (avoidable ? 0 : 1) } },
3028
+ propChanges: avoidable ? [{ path: "style", kind: "deep-equal", prev: { w: id }, next: { w: id } }] : [{ path: "n", kind: "different", prev: i, next: i + 1 }],
3029
+ stateChanges: [],
3030
+ hookChanges: [],
3031
+ reasons: [avoidable ? 'prop "style" is a new reference but deep-equal to the previous value.' : 'prop "n" changed.']
3032
+ });
3033
+ }
3034
+ return out;
3035
+ }
3036
+ function createBroadcastTransport(name) {
3037
+ let listener = null;
3038
+ let channel = null;
3039
+ const pending = /* @__PURE__ */ new Map();
3040
+ const emit = (m) => {
3041
+ if (listener) listener(m);
3042
+ };
3043
+ const open = () => {
3044
+ if (channel) return channel;
3045
+ channel = new BroadcastChannel(name);
3046
+ channel.onmessage = (event) => {
3047
+ const data = event.data;
3048
+ if (!data) return;
3049
+ if (data.__rerenderLensReply === true && typeof data.id === "string") {
3050
+ const p = pending.get(data.id);
3051
+ if (!p) return;
3052
+ pending.delete(data.id);
3053
+ clearTimeout(p.timer);
3054
+ p.resolve(typeof data.error === "string" ? new Error(data.error) : data.result);
3055
+ return;
3056
+ }
3057
+ if (data.__rerenderLens === true && typeof data.type === "string") emit({ type: data.type, version: typeof data.version === "number" ? data.version : void 0, payload: data.payload });
3058
+ };
3059
+ return channel;
3060
+ };
3061
+ const bridge = (cmd, arg) => new Promise((resolve, reject) => {
3062
+ const id = Math.random().toString(36).slice(2);
3063
+ const timer = setTimeout(() => {
3064
+ pending.delete(id);
3065
+ resolve(null);
3066
+ }, 1500);
3067
+ pending.set(id, {
3068
+ resolve: (v) => v instanceof Error ? reject(v) : resolve(v),
3069
+ timer
3070
+ });
3071
+ open().postMessage({ __rerenderLensCmd: true, id, cmd, arg });
3072
+ });
3073
+ const mem = (key) => `rerender-lens:${name}:${key}`;
3074
+ const transport = {
3075
+ origin: location.origin,
3076
+ tabLabel: `channel "${name}"`,
3077
+ panelUrl: location.origin + location.pathname,
3078
+ subscribe(fn) {
3079
+ listener = fn;
3080
+ open();
3081
+ fn({ type: "connected" });
3082
+ void bridge("info").then(async (info) => {
3083
+ if (!info) {
3084
+ fn({ type: "disconnected" });
3085
+ return;
3086
+ }
3087
+ fn({ type: "hello", version: info.protocol || 1, payload: info });
3088
+ const res = await bridge("pull", 0);
3089
+ if (res) for (const p of res.reports) fn({ type: "report", payload: p });
3090
+ });
3091
+ },
3092
+ replay: () => void bridge("replay"),
3093
+ clear: () => void bridge("clear"),
3094
+ configure: (options) => bridge("configure", options).then((r) => r ?? void 0),
3095
+ highlight: (id) => bridge("highlight", id).catch(() => {
3096
+ }),
3097
+ flashAvoidable: (on) => bridge("flash", !!on).catch(() => {
3098
+ }),
3099
+ storage: {
3100
+ get: (key) => {
3101
+ try {
3102
+ const raw = localStorage.getItem(mem(key));
3103
+ return raw ? JSON.parse(raw) : void 0;
3104
+ } catch {
3105
+ return void 0;
3106
+ }
3107
+ },
3108
+ set: (key, value) => {
3109
+ try {
3110
+ localStorage.setItem(mem(key), JSON.stringify(value));
3111
+ } catch {
3112
+ }
3113
+ }
3114
+ },
3115
+ readSource: (url) => fetch(url).then((res) => res.ok ? res.text() : null).catch(() => null),
3116
+ copy: (text) => navigator.clipboard.writeText(text).catch(() => {
3117
+ })
3118
+ };
3119
+ return transport;
3120
+ }
3121
+ function createRelayClientTransport(relayUrl, ES = EventSource) {
3122
+ const base = relayUrl.replace(/\/$/, "");
3123
+ let listener = null;
3124
+ let stream = null;
3125
+ let appsOnline = null;
3126
+ const pending = /* @__PURE__ */ new Map();
3127
+ const emit = (m) => {
3128
+ if (listener) listener(m);
3129
+ };
3130
+ const post = (message) => fetch(`${base}/message`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(message) }).then(() => void 0);
3131
+ const bridge = (cmd, arg) => new Promise((resolve, reject) => {
3132
+ const id = Math.random().toString(36).slice(2);
3133
+ const timer = setTimeout(() => {
3134
+ pending.delete(id);
3135
+ resolve(null);
3136
+ }, 2e3);
3137
+ pending.set(id, { resolve: (v) => v instanceof Error ? reject(v) : resolve(v), timer });
3138
+ post({ __rerenderLensCmd: true, id, cmd, arg }).catch(() => {
3139
+ clearTimeout(timer);
3140
+ pending.delete(id);
3141
+ resolve(null);
3142
+ });
3143
+ });
3144
+ const handleData = (data) => {
3145
+ let parsed;
3146
+ try {
3147
+ parsed = JSON.parse(data);
3148
+ } catch {
3149
+ return;
3150
+ }
3151
+ for (const m of Array.isArray(parsed) ? parsed : [parsed]) {
3152
+ if (!isRecord(m)) continue;
3153
+ if (m.__rerenderLensReply === true && typeof m.id === "string") {
3154
+ const p = pending.get(m.id);
3155
+ if (!p) continue;
3156
+ pending.delete(m.id);
3157
+ clearTimeout(p.timer);
3158
+ p.resolve(typeof m.error === "string" ? new Error(m.error) : m.result);
3159
+ } else if (m.__rerenderLens === true && m.type === "relay") {
3160
+ const apps = isRecord(m.payload) && typeof m.payload.apps === "number" ? m.payload.apps : 0;
3161
+ const wasOnline = appsOnline;
3162
+ appsOnline = apps;
3163
+ if (apps > 0 && wasOnline !== null && wasOnline === 0) void attach();
3164
+ else if (apps === 0) emit({ type: "disconnected" });
3165
+ } else if (m.__rerenderLens === true && typeof m.type === "string") emit({ type: m.type, version: typeof m.version === "number" ? m.version : void 0, payload: m.payload });
3166
+ }
3167
+ };
3168
+ async function attach() {
3169
+ const info = await bridge("info");
3170
+ if (!info) {
3171
+ emit({ type: "disconnected" });
3172
+ return;
3173
+ }
3174
+ emit({ type: "connected" });
3175
+ emit({ type: "hello", version: info.protocol || 1, payload: info });
3176
+ const res = await bridge("pull", 0);
3177
+ if (res) for (const p of res.reports) emit({ type: "report", payload: p });
3178
+ }
3179
+ const mem = (key) => `rerender-lens:relay:${base}:${key}`;
3180
+ const transport = {
3181
+ origin: base,
3182
+ tabLabel: `relay ${base.replace(/^https?:\/\//, "")}`,
3183
+ panelUrl: location.origin + location.pathname,
3184
+ subscribe(fn) {
3185
+ listener = fn;
3186
+ const open = () => {
3187
+ stream = new ES(`${base}/events?role=panel`);
3188
+ stream.onmessage = (e) => handleData(e.data);
3189
+ stream.onerror = () => {
3190
+ emit({ type: "disconnected" });
3191
+ };
3192
+ };
3193
+ open();
3194
+ void attach();
3195
+ },
3196
+ replay: () => void bridge("replay"),
3197
+ clear: () => void bridge("clear"),
3198
+ configure: (options) => bridge("configure", options).then((r) => r ?? void 0),
3199
+ highlight: (id) => bridge("highlight", id).catch(() => {
3200
+ }),
3201
+ flashAvoidable: (on) => bridge("flash", !!on).catch(() => {
3202
+ }),
3203
+ storage: {
3204
+ get: (key) => {
3205
+ try {
3206
+ const raw = localStorage.getItem(mem(key));
3207
+ return raw ? JSON.parse(raw) : void 0;
3208
+ } catch {
3209
+ return void 0;
3210
+ }
3211
+ },
3212
+ set: (key, value) => {
3213
+ try {
3214
+ localStorage.setItem(mem(key), JSON.stringify(value));
3215
+ } catch {
3216
+ }
3217
+ }
3218
+ },
3219
+ readSource: (url) => fetch(url).then((res) => res.ok ? res.text() : null).catch(() => null),
3220
+ copy: (text) => navigator.clipboard.writeText(text).catch(() => {
3221
+ })
3222
+ };
3223
+ return transport;
3224
+ }
3225
+ function bootRelay(relayUrl) {
3226
+ const prefersDark = typeof matchMedia === "function" && matchMedia("(prefers-color-scheme: dark)").matches;
3227
+ return createPanel(document.getElementById("root"), createRelayClientTransport(relayUrl), { theme: prefersDark ? "dark" : "light" });
3228
+ }
3229
+ function bootBroadcast(name) {
3230
+ const prefersDark = typeof matchMedia === "function" && matchMedia("(prefers-color-scheme: dark)").matches;
3231
+ return createPanel(document.getElementById("root"), createBroadcastTransport(name), { theme: prefersDark ? "dark" : "light" });
3232
+ }
3233
+ async function bootShared(code) {
3234
+ const report = await decodeShare(code);
3235
+ const transport = { subscribe() {
3236
+ }, panelUrl: location.origin + location.pathname };
3237
+ const panel = createPanel(document.getElementById("root"), transport, { theme: typeof matchMedia === "function" && matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" });
3238
+ if (!report) {
3239
+ panel.handle({ type: "hello", version: PROTOCOL, payload: { protocol: PROTOCOL, library: "shared link", react: [], enabled: false } });
3240
+ return;
3241
+ }
3242
+ panel.importData([report]);
3243
+ panel.select(report.component);
3244
+ }
3245
+ function bootDemo() {
3246
+ const params2 = new URLSearchParams(location.search);
3247
+ const flood = Number(params2.get("flood") || 0);
3248
+ const sample = sampleReports();
3249
+ let i = 0;
3250
+ let commit = 0;
3251
+ const mem = {};
3252
+ const transport = {
3253
+ origin: "http://localhost:5199",
3254
+ subscribe(fn) {
3255
+ fn({ type: "connected" });
3256
+ fn({ type: "hello", version: PROTOCOL, payload: { count: 0, library: "demo", protocol: PROTOCOL, react: [{ version: "19.2.0", bundleType: 1 }], production: false, enabled: true, options: { trackAllMemoized: true, silent: true }, source: "page", injected: false } });
3257
+ if (flood > 0) {
3258
+ const t0 = performance.now();
3259
+ for (const r of floodReports(flood)) fn({ type: "report", payload: r });
3260
+ requestAnimationFrame(() => console.log(`[rerender-lens demo] ${flood} reports ingested and rendered in ${(performance.now() - t0).toFixed(0)} ms`));
3261
+ return;
3262
+ }
3263
+ const tick = () => {
3264
+ const r = JSON.parse(JSON.stringify(sample[i % sample.length]));
3265
+ r.renderCount = Math.floor(i / sample.length) + 1;
3266
+ if (i % sample.length === 0) commit++;
3267
+ r.commitId = commit + (r.commitId === 2 ? 100 : 0);
3268
+ fn({ type: "report", payload: r });
3269
+ i++;
3270
+ if (i < 14) setTimeout(tick, i < 4 ? 50 : 900);
3271
+ };
3272
+ tick();
3273
+ },
3274
+ replay() {
3275
+ },
3276
+ clear() {
3277
+ },
3278
+ configure: (o) => Promise.resolve(o),
3279
+ highlight() {
3280
+ },
3281
+ flashAvoidable() {
3282
+ },
3283
+ originStatus: () => Promise.resolve({ origin: "http://localhost:5199", builtIn: true, permitted: true, enabled: true, inject: false, deferHook: false }),
3284
+ setOrigin: () => Promise.resolve(),
3285
+ storage: { get: (k) => Promise.resolve(mem[k]), set: (k, v) => Promise.resolve(mem[k] = v) },
3286
+ panelUrl: location.origin + location.pathname,
3287
+ readSource: () => Promise.resolve("import { memo } from 'react';\n\nexport const ProductList = memo(function ProductList(props) {\n const [selected, setSelected] = useState(null);\n return (\n <ul>\n {props.products.map((p) => (\n <ProductRow key={p.id} product={p} style={{ color: 'red' }} onSelect={(id) => props.onSelect(id)} />\n ))}\n </ul>\n );\n});\n")
3288
+ };
3289
+ const dark = /theme=dark/.test(location.search) || typeof matchMedia === "function" && matchMedia("(prefers-color-scheme: dark)").matches;
3290
+ createPanel(document.getElementById("root"), transport, { theme: dark ? "dark" : "light" });
3291
+ }
3292
+ var api = {
3293
+ PROTOCOL,
3294
+ createPanel,
3295
+ summarize,
3296
+ valueNode,
3297
+ reportView,
3298
+ fixView,
3299
+ normalizeReport,
3300
+ reportToMarkdown,
3301
+ sampleReports,
3302
+ floodReports,
3303
+ bootStandalone,
3304
+ bootBroadcast,
3305
+ bootRelay,
3306
+ createRelayTransport,
3307
+ createBroadcastTransport,
3308
+ createRelayClientTransport,
3309
+ encodeShare,
3310
+ decodeShare,
3311
+ sourceContext,
3312
+ analysis: { firstDifferentPath, diffLeaves, fixesFor, rankFixes, rootCauseOf, analyzeCommit, contextAttribution, cascadeTree, rootCauseSummary, summarizeSession, compareSessions }
3313
+ };
3314
+ window.RerenderLensPanel = api;
3315
+ var hasChrome = typeof chrome !== "undefined" && !!chrome && !!chrome.runtime && !!chrome.runtime.id;
3316
+ var hasDevtools = hasChrome && !!chrome.devtools && !!chrome.devtools.inspectedWindow;
3317
+ var params = typeof location !== "undefined" ? new URLSearchParams(location.search) : new URLSearchParams();
3318
+ var pathname = typeof location !== "undefined" ? String(location.pathname) : "";
3319
+ if (params.has("report")) void bootShared(params.get("report") || "");
3320
+ else if (params.has("relay") && typeof EventSource === "function") bootRelay(params.get("relay") || location.origin);
3321
+ else if (params.has("channel") && typeof BroadcastChannel === "function") bootBroadcast(params.get("channel") || "rerender-lens");
3322
+ else if (hasDevtools && /panel\.html/.test(pathname) && !params.has("tabId")) bootExtension();
3323
+ else if (hasChrome && (/sidepanel\.html/.test(pathname) || params.has("tabId"))) bootStandalone({ tabId: params.has("tabId") ? Number(params.get("tabId")) : null });
3324
+ else if (params.has("demo")) bootDemo();
3325
+ })();