rerender-lens 0.4.0 → 0.5.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 (66) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +192 -362
  3. package/dist/{budget-COBu7jBU.d.cts → budget-BnJlYwrV.d.ts} +20 -10
  4. package/dist/{budget-LkNjGtRc.d.ts → budget-CRswn-nC.d.cts} +20 -10
  5. package/dist/cli.cjs +248 -31
  6. package/dist/cli.cjs.map +1 -1
  7. package/dist/cli.js +248 -31
  8. package/dist/cli.js.map +1 -1
  9. package/dist/{devtools-CZebzpF6.d.ts → devtools-BYWat7nQ.d.ts} +7 -1
  10. package/dist/{devtools-BkCct3cJ.d.cts → devtools-DJi_6AfJ.d.cts} +7 -1
  11. package/dist/index.cjs +439 -41
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.d.cts +131 -8
  14. package/dist/index.d.ts +131 -8
  15. package/dist/index.js +424 -42
  16. package/dist/index.js.map +1 -1
  17. package/dist/jest-setup.cjs +1673 -0
  18. package/dist/jest-setup.cjs.map +1 -0
  19. package/dist/jest-setup.d.cts +8 -0
  20. package/dist/jest-setup.d.ts +8 -0
  21. package/dist/jest-setup.js +1671 -0
  22. package/dist/jest-setup.js.map +1 -0
  23. package/dist/jest.cjs +1760 -0
  24. package/dist/jest.cjs.map +1 -0
  25. package/dist/jest.d.cts +23 -0
  26. package/dist/jest.d.ts +23 -0
  27. package/dist/jest.js +1751 -0
  28. package/dist/jest.js.map +1 -0
  29. package/dist/{notifiers-BjjGSHpp.d.ts → notifiers-DwKlvv4H.d.cts} +8 -4
  30. package/dist/{notifiers-BGQtWKfX.d.cts → notifiers-FNzKHs0a.d.ts} +8 -4
  31. package/dist/playwright.cjs +230 -28
  32. package/dist/playwright.cjs.map +1 -1
  33. package/dist/playwright.d.cts +4 -4
  34. package/dist/playwright.d.ts +4 -4
  35. package/dist/playwright.js +230 -28
  36. package/dist/playwright.js.map +1 -1
  37. package/dist/relay.cjs +10 -2
  38. package/dist/relay.cjs.map +1 -1
  39. package/dist/relay.js +10 -2
  40. package/dist/relay.js.map +1 -1
  41. package/dist/rerender-lens.iife.js +356 -54
  42. package/dist/runner-report-B8Sc3FZO.d.cts +46 -0
  43. package/dist/runner-report-CW8X5RwK.d.ts +46 -0
  44. package/dist/setup.cjs +112 -27
  45. package/dist/setup.cjs.map +1 -1
  46. package/dist/setup.js +112 -27
  47. package/dist/setup.js.map +1 -1
  48. package/dist/{types-BzUEVkxJ.d.cts → types-DW5-N2XH.d.cts} +55 -2
  49. package/dist/{types-BzUEVkxJ.d.ts → types-DW5-N2XH.d.ts} +55 -2
  50. package/dist/vite.d.cts +1 -1
  51. package/dist/vite.d.ts +1 -1
  52. package/dist/vitest-setup.cjs +337 -41
  53. package/dist/vitest-setup.cjs.map +1 -1
  54. package/dist/vitest-setup.d.cts +4 -4
  55. package/dist/vitest-setup.d.ts +4 -4
  56. package/dist/vitest-setup.js +337 -41
  57. package/dist/vitest-setup.js.map +1 -1
  58. package/dist/vitest.cjs +400 -77
  59. package/dist/vitest.cjs.map +1 -1
  60. package/dist/vitest.d.cts +21 -39
  61. package/dist/vitest.d.ts +21 -39
  62. package/dist/vitest.js +401 -78
  63. package/dist/vitest.js.map +1 -1
  64. package/package.json +40 -4
  65. package/panel/panel.css +1 -0
  66. package/panel/panel.js +499 -309
package/panel/panel.js CHANGED
@@ -1,190 +1,288 @@
1
1
  /* Built from extension/src/panel.ts by `npm run build`; do not edit by hand. */
2
2
  "use strict";
3
3
  (() => {
4
- // extension/src/panel.ts
5
- var PROTOCOL = 2;
6
- var KIND_LABEL = {
7
- "deep-equal": "equal by value",
8
- function: "new function",
9
- element: "equal element",
10
- different: "changed",
11
- added: "added",
12
- removed: "removed"
13
- };
14
- var AVOIDABLE_KINDS = /* @__PURE__ */ new Set(["deep-equal", "function", "element"]);
15
- var FN_PREFIX = "\u0192 ";
16
- var MAX_REPORTS = 2e3;
17
- var MAX_PER_NODE = 200;
18
- var MAX_COMMITS = 500;
19
- var MAX_PER_COMMIT = 500;
20
- var THROTTLE_MIN_REPORTS = 200;
21
- var BEST_FIX_INTERVAL = 500;
22
- var LEFT_RENDER_INTERVAL = 250;
23
- var SYNC_RETRY_MIN = 500;
24
- var SYNC_RETRY_MAX = 5e3;
25
- var SYNC_RETRIES = 20;
26
- var NAVIGATION_SETTLE = 1200;
27
- var ROW_H = 22;
28
- var ITEM_H = 20;
29
- var OVERSCAN = 8;
30
- var FALLBACK_VIEWPORT = 800;
31
- function el(tag, attrs, children) {
32
- const node = document.createElement(tag);
33
- if (attrs) {
34
- for (const k of Object.keys(attrs)) {
35
- const v = attrs[k];
36
- if (k === "class") node.className = String(v);
37
- else if (k === "text") node.textContent = String(v);
38
- else if (k.startsWith("on")) {
39
- if (typeof v === "function") node.addEventListener(k.slice(2), v);
40
- } else if (v === true) node.setAttribute(k, "");
41
- else if (v !== void 0 && v !== null && v !== false) node.setAttribute(k, String(v));
42
- }
43
- }
44
- if (children) {
45
- for (const c of [].concat(children)) if (c != null) node.append(c);
46
- }
47
- return node;
4
+ // src/diff.ts
5
+ var REACT_ELEMENT = /* @__PURE__ */ Symbol.for("react.element");
6
+ var REACT_TRANSITIONAL_ELEMENT = /* @__PURE__ */ Symbol.for("react.transitional.element");
7
+ function isReactElement(v) {
8
+ if (typeof v !== "object" || v === null) return false;
9
+ const t = v.$$typeof;
10
+ return t === REACT_ELEMENT || t === REACT_TRANSITIONAL_ELEMENT;
48
11
  }
49
- function fmtTime(ms) {
50
- const d = new Date(ms);
51
- const p = (n, w) => String(n).padStart(w, "0");
52
- return `${p(d.getHours(), 2)}:${p(d.getMinutes(), 2)}:${p(d.getSeconds(), 2)}.${p(d.getMilliseconds(), 3)}`;
12
+ function isPlainObject(v) {
13
+ if (typeof v !== "object" || v === null) return false;
14
+ const proto = Object.getPrototypeOf(v);
15
+ return proto === Object.prototype || proto === null;
53
16
  }
54
- var fmtMs = (n) => typeof n === "number" && Number.isFinite(n) ? `${n.toFixed(1)} ms` : "";
55
- var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
56
- var componentList = (m) => [...m].map(([c, n]) => `<${c}>${n > 1 ? " \xD7" + n : ""}`).join(", ");
57
- function changesOf(report) {
58
- return [].concat(report.propChanges || [], report.stateChanges || [], report.hookChanges || []);
59
- }
60
- function summarize(report) {
61
- const counts = /* @__PURE__ */ new Map();
62
- for (const c of changesOf(report)) counts.set(c.kind, (counts.get(c.kind) || 0) + 1);
63
- if (counts.size === 0) return "no changes";
64
- return [...counts].map(([k, n]) => `${n} ${KIND_LABEL[k] || k}`).join(", ");
17
+ function remember(seen, a, b) {
18
+ let set = seen.get(a);
19
+ if (!set) {
20
+ set = /* @__PURE__ */ new Set();
21
+ seen.set(a, set);
22
+ }
23
+ if (set.has(b)) return true;
24
+ set.add(b);
25
+ return false;
65
26
  }
66
- var isRecord = (v) => typeof v === "object" && v !== null;
67
- function normalizeReport(p) {
68
- if (!isRecord(p) || typeof p.component !== "string") return null;
69
- const arr = (x) => Array.isArray(x) ? x.filter((c) => isRecord(c) && typeof c.path === "string") : [];
70
- const props = isRecord(p.props) ? p.props : {};
71
- 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;
72
- const r = {
73
- component: p.component,
74
- instanceId: typeof p.instanceId === "number" ? p.instanceId : 0,
75
- commitId: typeof p.commitId === "number" ? p.commitId : 0,
76
- renderCount: typeof p.renderCount === "number" ? p.renderCount : 0,
77
- trigger: typeof p.trigger === "string" ? p.trigger : "parent",
78
- avoidable: !!p.avoidable,
79
- props: { prev: isRecord(props.prev) ? props.prev : {}, next: isRecord(props.next) ? props.next : {} },
80
- propChanges: arr(p.propChanges),
81
- stateChanges: arr(p.stateChanges),
82
- hookChanges: arr(p.hookChanges),
83
- parent,
84
- owner: typeof p.owner === "string" ? p.owner : null,
85
- path: Array.isArray(p.path) ? p.path.filter((x) => typeof x === "string") : [],
86
- reasons: Array.isArray(p.reasons) ? p.reasons.filter((x) => typeof x === "string") : [],
87
- time: typeof p.time === "number" ? p.time : 0,
88
- receivedAt: typeof p.receivedAt === "number" ? p.receivedAt : 0
27
+ var scope = null;
28
+ var DEFAULT_DIFF_BUDGET = 5e4;
29
+ function beginDiffScope(budget = 2e5) {
30
+ const previous = scope;
31
+ scope = { cache: /* @__PURE__ */ new WeakMap(), remaining: budget, exhausted: false };
32
+ return () => {
33
+ scope = previous;
89
34
  };
90
- if (typeof p.selfDuration === "number") r.selfDuration = p.selfDuration;
91
- if (typeof p.treeDuration === "number") r.treeDuration = p.treeDuration;
92
- if (typeof p.memoized === "boolean") r.memoized = p.memoized;
93
- if (typeof p.commitPriority === "string") r.commitPriority = p.commitPriority;
94
- if (Array.isArray(p.hookState)) r.hookState = p.hookState.filter((h) => isRecord(h) && typeof h.path === "string");
95
- if (Array.isArray(p.contexts)) r.contexts = p.contexts.filter((c) => isRecord(c) && typeof c.name === "string");
96
- if (isRecord(p.state)) r.state = p.state;
97
- if (Array.isArray(p.updaters)) r.updaters = p.updaters.filter((u) => typeof u === "string");
98
- if (p.commitCause === "effect-after-commit" || p.commitCause === "suspense-resolved") r.commitCause = p.commitCause;
99
- if (typeof p.afterCommit === "number") r.afterCommit = p.afterCommit;
100
- if (typeof p.key === "string") r.key = p.key;
101
- if (isRecord(p.source) && typeof p.source.fileName === "string") r.source = p.source;
102
- return r;
103
35
  }
104
- function valueNode(v, depth = 0) {
105
- if (v === null || v === void 0) return el("span", { class: "v nil", text: String(v) });
106
- const t = typeof v;
107
- if (typeof v === "string") {
108
- if (v.startsWith(FN_PREFIX)) return el("span", { class: "v fn", text: v });
109
- if (/^<[^>]+>$/.test(v)) return el("span", { class: "v", text: v });
110
- return el("span", { class: "v str", text: JSON.stringify(v) });
36
+ function deepEqual(a, b, seen = /* @__PURE__ */ new Map()) {
37
+ if (Object.is(a, b)) return true;
38
+ if (typeof a !== typeof b) return false;
39
+ if (typeof a !== "object" || a === null || b === null) return false;
40
+ if (!scope) {
41
+ const end = beginDiffScope(DEFAULT_DIFF_BUDGET);
42
+ try {
43
+ return deepEqual(a, b, seen);
44
+ } finally {
45
+ end();
46
+ }
47
+ }
48
+ const sc = scope;
49
+ const objA = a;
50
+ const objB = b;
51
+ const hit = sc.cache.get(objA)?.get(objB);
52
+ if (hit !== void 0) return hit;
53
+ if (--sc.remaining < 0) {
54
+ sc.exhausted = true;
55
+ return false;
111
56
  }
112
- if (t === "number" || t === "bigint") return el("span", { class: "v num", text: String(v) });
113
- if (t === "boolean") return el("span", { class: "v bool", text: String(v) });
114
- if (Array.isArray(v)) {
115
- const short = v.length <= 4 && v.every((x) => typeof x !== "object" || x === null);
116
- if (short) {
117
- const s = el("span", { class: "v" }, "[");
118
- v.forEach((x, i) => {
119
- if (i) s.append(", ");
120
- s.append(valueNode(x, depth + 1));
121
- });
122
- s.append("]");
123
- return s;
57
+ const result = deepEqualObjects(objA, objB, seen);
58
+ let m = sc.cache.get(objA);
59
+ if (!m) {
60
+ m = /* @__PURE__ */ new WeakMap();
61
+ sc.cache.set(objA, m);
62
+ }
63
+ m.set(objB, result);
64
+ return result;
65
+ }
66
+ function deepEqualObjects(a, b, seen) {
67
+ const objA = a;
68
+ const objB = b;
69
+ if (remember(seen, objA, objB)) return true;
70
+ if (a instanceof Date) return b instanceof Date && a.getTime() === b.getTime();
71
+ if (a instanceof RegExp) return b instanceof RegExp && a.source === b.source && a.flags === b.flags;
72
+ if (Array.isArray(a)) {
73
+ if (!Array.isArray(b) || a.length !== b.length) return false;
74
+ for (let i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i], seen)) return false;
75
+ return true;
76
+ }
77
+ if (Array.isArray(b)) return false;
78
+ if (ArrayBuffer.isView(a)) {
79
+ if (!ArrayBuffer.isView(b) || a.constructor !== b.constructor) return false;
80
+ const ta = a;
81
+ const tb = b;
82
+ if (ta.length !== tb.length) return false;
83
+ for (let i = 0; i < ta.length; i++) if (ta[i] !== tb[i]) return false;
84
+ return true;
85
+ }
86
+ if (a instanceof Map) {
87
+ if (!(b instanceof Map) || a.size !== b.size) return false;
88
+ for (const [k, v] of a) {
89
+ if (!b.has(k) || !deepEqual(v, b.get(k), seen)) return false;
124
90
  }
125
- return objectDetails(`Array(${v.length})`, v);
91
+ return true;
126
92
  }
127
- const o = v;
128
- if (o.$type === "element") {
129
- const props = isRecord(o.props) ? o.props : {};
130
- const label2 = `<${String(o.name)}${o.key !== void 0 ? ` key=${JSON.stringify(o.key)}` : ""}>`;
131
- return Object.keys(props).length ? objectDetails(label2, props) : el("span", { class: "v", text: label2 });
93
+ if (a instanceof Set) {
94
+ if (!(b instanceof Set) || a.size !== b.size) return false;
95
+ if (a.size > 50) {
96
+ for (const v of a) if (!b.has(v)) return false;
97
+ return true;
98
+ }
99
+ outer: for (const v of a) {
100
+ if (b.has(v)) continue;
101
+ for (const w of b) if (deepEqual(v, w, seen)) continue outer;
102
+ return false;
103
+ }
104
+ return true;
132
105
  }
133
- if (o.$type === "Date") return el("span", { class: "v", text: `Date(${String(o.value)})` });
134
- if (o.$type === "RegExp") return el("span", { class: "v", text: String(o.value) });
135
- if (o.$type === "Map" && Array.isArray(o.entries)) return objectDetails(`Map(${o.entries.length})`, o.entries);
136
- if (o.$type === "Set" && Array.isArray(o.values)) return objectDetails(`Set(${o.values.length})`, o.values);
137
- const keys = Object.keys(o).filter((k) => k !== "$type");
138
- const label = `${o.$type ? String(o.$type) + " " : ""}{${keys.slice(0, 3).join(", ")}${keys.length > 3 ? ", ..." : ""}}`;
139
- return objectDetails(label, o);
106
+ if (isReactElement(a)) {
107
+ if (!isReactElement(b)) return false;
108
+ return a.type === b.type && a.key === b.key && deepEqual(a.props, b.props, seen);
109
+ }
110
+ if (isPlainObject(a) && isPlainObject(b)) {
111
+ const ka = Object.keys(a);
112
+ const kb = Object.keys(b);
113
+ if (ka.length !== kb.length) return false;
114
+ const rb = b;
115
+ for (const k of ka) {
116
+ if (!Object.prototype.hasOwnProperty.call(b, k)) return false;
117
+ if (!deepEqual(a[k], rb[k], seen)) return false;
118
+ }
119
+ return true;
120
+ }
121
+ return false;
140
122
  }
141
- function objectDetails(label, obj) {
142
- const d = el("details", { class: "obj" }, [el("summary", { text: label })]);
143
- d.addEventListener(
144
- "toggle",
145
- () => {
146
- if (d.open && !d.querySelector("pre")) d.append(el("pre", { text: JSON.stringify(obj, null, 2) }));
147
- },
148
- { once: true }
149
- );
150
- return d;
123
+ var AVOIDABLE_KINDS = /* @__PURE__ */ new Set(["deep-equal", "function", "element"]);
124
+ function joinPath(base, key) {
125
+ if (typeof key === "number") return `${base}[${key}]`;
126
+ if (base === "") return key;
127
+ return /^[A-Za-z_$][\w$]*$/.test(key) ? `${base}.${key}` : `${base}[${JSON.stringify(key)}]`;
151
128
  }
152
- function firstDifferentPath(a, b, base = "") {
153
- if (a === b) return null;
154
- if (!isRecord(a) || !isRecord(b)) return base || "(value)";
155
- if (Array.isArray(a) !== Array.isArray(b)) return base || "(value)";
129
+ function firstDifferentPath(a, b, path = "", depth = 0) {
130
+ if (depth > 8) return path || "(value)";
156
131
  if (Array.isArray(a) && Array.isArray(b)) {
157
- if (a.length !== b.length) return base ? `${base}.length` : "length";
132
+ if (a.length !== b.length) return path ? `${path}.length` : "length";
158
133
  for (let i = 0; i < a.length; i++) {
159
- const p = firstDifferentPath(a[i], b[i], `${base}[${i}]`);
160
- if (p) return p;
134
+ if (!deepEqual(a[i], b[i])) return firstDifferentPath(a[i], b[i], joinPath(path, i), depth + 1);
161
135
  }
162
136
  return null;
163
137
  }
164
- const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
165
- for (const k of keys) {
166
- const p = firstDifferentPath(a[k], b[k], base ? `${base}.${k}` : k);
167
- if (p) return p;
138
+ if (isPlainObject(a) && isPlainObject(b)) {
139
+ const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
140
+ for (const k of keys) {
141
+ if (!deepEqual(a[k], b[k])) return firstDifferentPath(a[k], b[k], joinPath(path, k), depth + 1);
142
+ }
143
+ return null;
168
144
  }
169
- return null;
145
+ return deepEqual(a, b) ? null : path || "(value)";
170
146
  }
171
- function diffLeaves(a, b, limit = 20, base = "", out = []) {
172
- if (out.length >= limit || a === b) return out;
173
- if (!isRecord(a) || !isRecord(b) || Array.isArray(a) !== Array.isArray(b)) {
174
- out.push({ path: base || "(value)", prev: a, next: b });
175
- return out;
176
- }
147
+ function diffLeaves(a, b, limit = 20, path = "", out = []) {
148
+ if (out.length >= limit || Object.is(a, b)) return out;
177
149
  if (Array.isArray(a) && Array.isArray(b)) {
178
150
  const n = Math.max(a.length, b.length);
179
- for (let i = 0; i < n && out.length < limit; i++) diffLeaves(a[i], b[i], limit, `${base}[${i}]`, out);
151
+ for (let i = 0; i < n && out.length < limit; i++) diffLeaves(a[i], b[i], limit, joinPath(path, i), out);
180
152
  return out;
181
153
  }
182
- for (const k of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])) {
183
- if (out.length >= limit) break;
184
- diffLeaves(a[k], b[k], limit, base ? `${base}.${k}` : k, out);
154
+ if (isPlainObject(a) && isPlainObject(b)) {
155
+ for (const k of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])) {
156
+ if (out.length >= limit) break;
157
+ diffLeaves(a[k], b[k], limit, joinPath(path, k), out);
158
+ }
159
+ return out;
185
160
  }
161
+ if (!deepEqual(a, b)) out.push({ path: path || "(value)", prev: a, next: b });
186
162
  return out;
187
163
  }
164
+
165
+ // src/causes.ts
166
+ 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);
167
+ var bump = (m, key, n = 1) => void m.set(key, (m.get(key) || 0) + n);
168
+ function indexByComponent(reports) {
169
+ const index = /* @__PURE__ */ new Map();
170
+ for (const r of reports) {
171
+ const list = index.get(r.component);
172
+ if (list) list.push(r);
173
+ else index.set(r.component, [r]);
174
+ }
175
+ return index;
176
+ }
177
+ function rootCauseOf(r, commitReports, index = indexByComponent(commitReports)) {
178
+ let cur = r;
179
+ const seen = /* @__PURE__ */ new Set([r]);
180
+ while (cur.trigger === "parent" && cur.parent) {
181
+ const parent = cur.parent;
182
+ const candidates = index.get(parent.name);
183
+ const p = candidates && candidates.find((x) => isAncestorReport(x, cur));
184
+ if (!p || seen.has(p)) return { name: parent.name, trigger: parent.trigger, report: null };
185
+ seen.add(p);
186
+ cur = p;
187
+ }
188
+ return cur === r ? null : { name: cur.component, trigger: cur.trigger, report: cur };
189
+ }
190
+ function rootCausesOf(reports) {
191
+ const roots = /* @__PURE__ */ new Map();
192
+ const rootByReport = /* @__PURE__ */ new Map();
193
+ const index = indexByComponent(reports);
194
+ let avoidable = 0;
195
+ let wasted = 0;
196
+ for (const r of reports) {
197
+ if (!r.avoidable) continue;
198
+ avoidable++;
199
+ if (typeof r.selfDuration === "number") wasted += r.selfDuration;
200
+ const root = rootCauseOf(r, reports, index);
201
+ const name = root ? root.name : r.parent && r.parent.name || "(unknown)";
202
+ const trigger = root ? root.trigger : r.parent && r.parent.trigger || "parent";
203
+ rootByReport.set(r, name);
204
+ let agg = roots.get(name);
205
+ if (!agg) {
206
+ agg = { name, trigger, count: 0, components: /* @__PURE__ */ new Map() };
207
+ roots.set(name, agg);
208
+ }
209
+ agg.count++;
210
+ bump(agg.components, r.component);
211
+ }
212
+ return { roots: [...roots.values()].sort((a, b) => b.count - a.count), rootByReport, avoidable, wasted };
213
+ }
214
+ function analyzeCommit(reports) {
215
+ const first = reports[0];
216
+ return {
217
+ id: first && typeof first.commitId === "number" ? first.commitId : 0,
218
+ total: reports.length,
219
+ ...rootCausesOf(reports),
220
+ contexts: contextAttribution(reports),
221
+ reports
222
+ };
223
+ }
224
+ function contextAttribution(reports) {
225
+ const byCtx = /* @__PURE__ */ new Map();
226
+ for (const r of reports) {
227
+ for (const c of r.hookChanges || []) {
228
+ if (c.hook !== "useContext" && !/^useContext/.test(c.path)) continue;
229
+ const m = /useContext\((.*)\)/.exec(c.path);
230
+ const name = m && m[1] ? m[1] : c.path;
231
+ let agg = byCtx.get(name);
232
+ if (!agg) {
233
+ agg = { name, consumers: 0, avoidable: 0, components: /* @__PURE__ */ new Map(), commits: /* @__PURE__ */ new Set(), providers: /* @__PURE__ */ new Map(), changedKeys: /* @__PURE__ */ new Set(), totalKeys: 0 };
234
+ byCtx.set(name, agg);
235
+ }
236
+ agg.consumers++;
237
+ if (AVOIDABLE_KINDS.has(c.kind)) agg.avoidable++;
238
+ bump(agg.components, r.component);
239
+ agg.commits.add(typeof r.commitId === "number" ? r.commitId : 0);
240
+ if (c.provider && c.provider.component) bump(agg.providers, c.provider.component);
241
+ if (c.changedKeys) for (const k of c.changedKeys) agg.changedKeys.add(k);
242
+ if (typeof c.totalKeys === "number") agg.totalKeys = Math.max(agg.totalKeys, c.totalKeys);
243
+ }
244
+ }
245
+ return [...byCtx.values()].sort((a, b) => b.consumers - a.consumers);
246
+ }
247
+ function cascadeTree(reports) {
248
+ const root = { name: "", children: /* @__PURE__ */ new Map(), report: null };
249
+ for (const r of reports) {
250
+ let node = root;
251
+ for (const seg of r.path.concat([r.component])) {
252
+ let next = node.children.get(seg);
253
+ if (!next) {
254
+ next = { name: seg, children: /* @__PURE__ */ new Map(), report: null };
255
+ node.children.set(seg, next);
256
+ }
257
+ node = next;
258
+ }
259
+ if (!node.report || r.avoidable) node.report = r;
260
+ node.count = (node.count || 0) + 1;
261
+ if (r.avoidable) node.avoidable = (node.avoidable || 0) + 1;
262
+ }
263
+ return root;
264
+ }
265
+ function rootCauseSummary(name, commits, analyze) {
266
+ const analyzeFn = analyze || ((_, reports) => analyzeCommit(reports));
267
+ const out = { name, trigger: "parent", commits: [], total: 0, components: /* @__PURE__ */ new Map(), affected: [] };
268
+ for (const [key, reports] of commits) {
269
+ const analysis = analyzeFn(key, reports);
270
+ const root = analysis.roots.find((x) => x.name === name);
271
+ if (!root) continue;
272
+ out.trigger = root.trigger;
273
+ out.commits.push({ key, analysis, count: root.count, components: root.components });
274
+ out.total += root.count;
275
+ for (const [c, n] of root.components) bump(out.components, c, n);
276
+ for (const r of reports) if (r.avoidable && analysis.rootByReport.get(r) === name) out.affected.push(r);
277
+ }
278
+ out.commits.reverse();
279
+ return out;
280
+ }
281
+
282
+ // src/fixes.ts
283
+ var MAX_FIX_REPORTS = 50;
284
+ var rootOf = (path) => path.split(/[.[]/)[0] || path;
285
+ var identifier = (name) => /^[A-Za-z_$][\w$]*$/.test(name) ? name : "value";
188
286
  function shortValue(v, max = 60) {
189
287
  let s;
190
288
  try {
@@ -195,12 +293,11 @@
195
293
  if (s === void 0) s = String(v);
196
294
  return s.length > max ? s.slice(0, max - 3) + "..." : s;
197
295
  }
198
- var identifier = (name) => /^[A-Za-z_$][\w$]*$/.test(name) ? name : "value";
199
296
  function fixesFor(r) {
200
297
  const out = [];
201
298
  const ownerName = r.owner || r.parent && r.parent.name || null;
202
- const changes = changesOf(r);
203
- const avoidableProps = (r.propChanges || []).filter((c) => AVOIDABLE_KINDS.has(c.kind));
299
+ const propChanges = r.propChanges || [];
300
+ const changes = [...propChanges, ...r.stateChanges || [], ...r.hookChanges || []];
204
301
  if (r.avoidable && (changes.length === 0 || r.memoized === false)) {
205
302
  const identical = changes.length === 0;
206
303
  out.push({
@@ -219,9 +316,10 @@ export const ${r.component} = memo(function ${r.component}(props) {
219
316
  // class components: extend PureComponent instead`
220
317
  });
221
318
  }
222
- for (const c of avoidableProps) {
319
+ for (const c of propChanges) {
320
+ if (!AVOIDABLE_KINDS.has(c.kind)) continue;
223
321
  const owner = ownerName || "?";
224
- const root = c.path.split(/[.[]/)[0] || c.path;
322
+ const root = rootOf(c.path);
225
323
  const id = identifier(root);
226
324
  if (root === "children" && (c.kind === "element" || c.kind === "deep-equal")) {
227
325
  out.push({
@@ -243,9 +341,7 @@ const children = useMemo(() => (
243
341
  // static children: hoist them to module scope
244
342
  const STATIC = <em>hi</em>;`
245
343
  });
246
- continue;
247
- }
248
- if (c.kind === "function") {
344
+ } else if (c.kind === "function") {
249
345
  out.push({
250
346
  kind: "useCallback",
251
347
  owner,
@@ -295,7 +391,7 @@ const ${id.toUpperCase()} = ${shortValue(c.next, 80)};`
295
391
  });
296
392
  }
297
393
  }
298
- for (const c of [].concat(r.stateChanges || [], r.hookChanges || [])) {
394
+ for (const c of [...r.stateChanges || [], ...r.hookChanges || []]) {
299
395
  const isContext = c.hook === "useContext" || /^useContext/.test(c.path);
300
396
  const ctxName = isContext ? (/useContext\((.*)\)/.exec(c.path) || [])[1] || "Context" : "";
301
397
  const providerOwner = isContext && c.provider && c.provider.component ? c.provider.component : null;
@@ -319,17 +415,16 @@ const ${c.changedKeys[0]} = useContextSelector(${ctxName}, (v) => v.${c.changedK
319
415
  }
320
416
  if (!AVOIDABLE_KINDS.has(c.kind)) continue;
321
417
  if (isContext) {
322
- const name = ctxName;
323
418
  out.push({
324
419
  kind: "contextValue",
325
- owner: providerOwner || `${name}.Provider`,
420
+ owner: providerOwner || `${ctxName}.Provider`,
326
421
  target: r.component,
327
- prop: name,
328
- label: `memoize the ${name} provider value${providerOwner ? ` in <${providerOwner}>` : ""}`,
329
- detail: `<${r.component}> re-rendered because ${name} produced a new value that is deep-equal to the previous one${providerOwner ? ` (Provider rendered by <${providerOwner}>)` : ""}.`,
330
- snippet: `// ${providerOwner || `where <${name}.Provider> is rendered`}
422
+ prop: ctxName,
423
+ label: `memoize the ${ctxName} provider value${providerOwner ? ` in <${providerOwner}>` : ""}`,
424
+ detail: `<${r.component}> re-rendered because ${ctxName} produced a new value that is deep-equal to the previous one${providerOwner ? ` (Provider rendered by <${providerOwner}>)` : ""}.`,
425
+ snippet: `// ${providerOwner || `where <${ctxName}.Provider> is rendered`}
331
426
  const value = useMemo(() => ({ /* ... */ }), [/* deps */]);
332
- <${name}.Provider value={value}>`
427
+ <${ctxName}.Provider value={value}>`
333
428
  });
334
429
  } else if (c.hook === "useSyncExternalStore") {
335
430
  const chain = c.custom || [];
@@ -374,137 +469,41 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
374
469
  for (const r of reports) {
375
470
  if (!r.avoidable) continue;
376
471
  for (const f of fixesFor(r)) {
377
- const k = fixKey(f);
378
- let agg = byKey.get(k);
472
+ const key = fixKey(f);
473
+ let agg = byKey.get(key);
379
474
  if (!agg) {
380
- agg = { ...f, key: k, count: 0, components: /* @__PURE__ */ new Map(), reports: [] };
381
- byKey.set(k, agg);
475
+ agg = { ...f, key, count: 0, components: {}, reports: [] };
476
+ byKey.set(key, agg);
382
477
  }
383
478
  agg.count++;
384
- agg.components.set(r.component, (agg.components.get(r.component) || 0) + 1);
385
- if (agg.reports.length < 50) agg.reports.push(r);
479
+ agg.components[r.component] = (agg.components[r.component] || 0) + 1;
480
+ if (agg.reports.length < MAX_FIX_REPORTS) agg.reports.push(r);
386
481
  }
387
482
  }
388
483
  return [...byKey.values()].sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
389
484
  }
390
- 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);
391
- function indexByComponent(reports) {
392
- const index = /* @__PURE__ */ new Map();
393
- for (const r of reports) {
394
- const list = index.get(r.component);
395
- if (list) list.push(r);
396
- else index.set(r.component, [r]);
397
- }
398
- return index;
399
- }
400
- function rootCauseOf(r, commitReports, index = indexByComponent(commitReports)) {
401
- let cur = r;
402
- const seen = /* @__PURE__ */ new Set([r]);
403
- while (cur.trigger === "parent" && cur.parent) {
404
- const parent = cur.parent;
405
- const candidates = index.get(parent.name);
406
- const p = candidates && candidates.find((x) => isAncestorReport(x, cur));
407
- if (!p || seen.has(p)) return { name: parent.name, trigger: parent.trigger, report: null };
408
- seen.add(p);
409
- cur = p;
410
- }
411
- return cur === r ? null : { name: cur.component, trigger: cur.trigger, report: cur };
412
- }
413
- function analyzeCommit(reports) {
414
- const roots = /* @__PURE__ */ new Map();
415
- const rootByReport = /* @__PURE__ */ new Map();
416
- const index = indexByComponent(reports);
417
- let avoidable = 0;
418
- let wasted = 0;
419
- for (const r of reports) {
420
- if (!r.avoidable) continue;
421
- avoidable++;
422
- if (typeof r.selfDuration === "number") wasted += r.selfDuration;
423
- const root = rootCauseOf(r, reports, index);
424
- const name = root ? root.name : r.parent && r.parent.name || "(unknown)";
425
- const trigger = root ? root.trigger : r.parent && r.parent.trigger || "parent";
426
- rootByReport.set(r, name);
427
- let agg = roots.get(name);
428
- if (!agg) {
429
- agg = { name, trigger, count: 0, components: /* @__PURE__ */ new Map() };
430
- roots.set(name, agg);
431
- }
432
- agg.count++;
433
- agg.components.set(r.component, (agg.components.get(r.component) || 0) + 1);
434
- }
435
- const first = reports[0];
436
- return {
437
- id: first ? first.commitId : 0,
438
- receivedAt: first ? first.receivedAt : 0,
439
- total: reports.length,
440
- avoidable,
441
- wasted,
442
- roots: [...roots.values()].sort((a, b) => b.count - a.count),
443
- rootByReport,
444
- contexts: contextAttribution(reports),
445
- fixes: rankFixes(reports),
446
- reports
447
- };
448
- }
449
- function contextAttribution(reports) {
450
- const byCtx = /* @__PURE__ */ new Map();
451
- for (const r of reports) {
452
- for (const c of r.hookChanges || []) {
453
- if (c.hook !== "useContext" && !/^useContext/.test(c.path)) continue;
454
- const m = /useContext\((.*)\)/.exec(c.path);
455
- const name = m && m[1] ? m[1] : c.path;
456
- let agg = byCtx.get(name);
457
- if (!agg) {
458
- agg = { name, consumers: 0, avoidable: 0, components: /* @__PURE__ */ new Map(), commits: /* @__PURE__ */ new Set(), providers: /* @__PURE__ */ new Map(), changedKeys: /* @__PURE__ */ new Set(), totalKeys: 0 };
459
- byCtx.set(name, agg);
460
- }
461
- agg.consumers++;
462
- if (AVOIDABLE_KINDS.has(c.kind)) agg.avoidable++;
463
- agg.components.set(r.component, (agg.components.get(r.component) || 0) + 1);
464
- agg.commits.add(r.commitId);
465
- if (c.provider && c.provider.component) agg.providers.set(c.provider.component, (agg.providers.get(c.provider.component) || 0) + 1);
466
- if (c.changedKeys) for (const k of c.changedKeys) agg.changedKeys.add(k);
467
- if (typeof c.totalKeys === "number") agg.totalKeys = Math.max(agg.totalKeys, c.totalKeys);
468
- }
469
- }
470
- return [...byCtx.values()].sort((a, b) => b.consumers - a.consumers);
471
- }
472
- function cascadeTree(reports) {
473
- const root = { name: "", children: /* @__PURE__ */ new Map(), report: null };
474
- for (const r of reports) {
475
- let node = root;
476
- for (const seg of r.path.concat([r.component])) {
477
- let next = node.children.get(seg);
478
- if (!next) {
479
- next = { name: seg, children: /* @__PURE__ */ new Map(), report: null };
480
- node.children.set(seg, next);
481
- }
482
- node = next;
483
- }
484
- if (!node.report || r.avoidable) node.report = r;
485
- node.count = (node.count || 0) + 1;
486
- if (r.avoidable) node.avoidable = (node.avoidable || 0) + 1;
487
- }
488
- return root;
489
- }
490
- function rootCauseSummary(name, commits, analyze = (_, reports) => analyzeCommit(reports)) {
491
- const out = { name, trigger: "parent", commits: [], total: 0, components: /* @__PURE__ */ new Map(), fixes: [] };
492
- const affected = [];
493
- for (const [key, reports] of commits) {
494
- const analysis = analyze(key, reports);
495
- const root = analysis.roots.find((x) => x.name === name);
496
- if (!root) continue;
497
- out.trigger = root.trigger;
498
- out.commits.push({ key, analysis, count: root.count, components: root.components });
499
- out.total += root.count;
500
- for (const [c, n] of root.components) out.components.set(c, (out.components.get(c) || 0) + n);
501
- for (const r of reports) if (r.avoidable && analysis.rootByReport.get(r) === name) affected.push(r);
485
+
486
+ // src/report.ts
487
+ var KIND_LABEL = {
488
+ "deep-equal": "equal by value",
489
+ function: "new function",
490
+ element: "equal element",
491
+ different: "changed",
492
+ added: "added",
493
+ removed: "removed"
494
+ };
495
+ function summarize(report) {
496
+ const counts = /* @__PURE__ */ new Map();
497
+ for (const c of [...report.propChanges || [], ...report.stateChanges || [], ...report.hookChanges || []]) {
498
+ counts.set(c.kind, (counts.get(c.kind) ?? 0) + 1);
502
499
  }
503
- out.commits.reverse();
504
- out.fixes = rankFixes(affected);
505
- return out;
500
+ const parts = [...counts].map(([kind, n]) => `${n} ${KIND_LABEL[kind] || kind}`);
501
+ if (parts.length === 0) parts.push("no changes");
502
+ return parts.join(", ");
506
503
  }
507
- function summarizeSession(session, reports) {
504
+
505
+ // src/sessions.ts
506
+ function summarizeReports(reports, meta = {}) {
508
507
  var _a;
509
508
  const byComponent = {};
510
509
  let avoidable = 0;
@@ -522,10 +521,10 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
522
521
  }
523
522
  }
524
523
  return {
525
- id: session.id,
526
- name: session.name,
527
- startedAt: session.startedAt,
528
- endedAt: session.endedAt,
524
+ id: meta.id ?? `s${Date.now().toString(36)}`,
525
+ name: meta.name ?? "session",
526
+ startedAt: meta.startedAt ?? (reports[0]?.time ?? 0),
527
+ endedAt: meta.endedAt ?? (reports[reports.length - 1]?.time ?? null),
529
528
  total: reports.length,
530
529
  avoidable,
531
530
  wasted,
@@ -533,7 +532,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
533
532
  fixes: rankFixes(reports).map((f) => ({ key: f.key, label: f.label, count: f.count }))
534
533
  };
535
534
  }
536
- function compareSessions(before, after) {
535
+ function compareSummaries(before, after) {
537
536
  const names = /* @__PURE__ */ new Set([...Object.keys(before.byComponent), ...Object.keys(after.byComponent)]);
538
537
  const rows = [];
539
538
  for (const component of names) {
@@ -552,8 +551,155 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
552
551
  avoidable: { before: before.avoidable, after: after.avoidable, delta: after.avoidable - before.avoidable },
553
552
  wasted: { before: before.wasted, after: after.wasted, delta: after.wasted - before.wasted },
554
553
  resolvedFixes: before.fixes.filter((f) => !afterKeys.has(f.key)),
555
- newFixes: after.fixes.filter((f) => !beforeKeys.has(f.key))
554
+ newFixes: after.fixes.filter((f) => !beforeKeys.has(f.key)),
555
+ regressions: rows.filter((r) => r.delta > 0)
556
+ };
557
+ }
558
+ var summarizeSession = (session, reports) => summarizeReports(reports, session);
559
+ var compareSessions = compareSummaries;
560
+
561
+ // extension/src/panel.ts
562
+ var PROTOCOL = 2;
563
+ var FN_PREFIX = "\u0192 ";
564
+ var MAX_REPORTS = 2e3;
565
+ var MAX_PER_NODE = 200;
566
+ var MAX_COMMITS = 500;
567
+ var MAX_PER_COMMIT = 500;
568
+ var THROTTLE_MIN_REPORTS = 200;
569
+ var BEST_FIX_INTERVAL = 500;
570
+ var LEFT_RENDER_INTERVAL = 250;
571
+ var SYNC_RETRY_MIN = 500;
572
+ var SYNC_RETRY_MAX = 5e3;
573
+ var SYNC_RETRIES = 20;
574
+ var NAVIGATION_SETTLE = 1200;
575
+ var INFO_REFRESH_MS = 3e3;
576
+ var ROW_H = 22;
577
+ var ITEM_H = 20;
578
+ var OVERSCAN = 8;
579
+ var FALLBACK_VIEWPORT = 800;
580
+ function el(tag, attrs, children) {
581
+ const node = document.createElement(tag);
582
+ if (attrs) {
583
+ for (const k of Object.keys(attrs)) {
584
+ const v = attrs[k];
585
+ if (k === "class") node.className = String(v);
586
+ else if (k === "text") node.textContent = String(v);
587
+ else if (k.startsWith("on")) {
588
+ if (typeof v === "function") node.addEventListener(k.slice(2), v);
589
+ } else if (v === true) node.setAttribute(k, "");
590
+ else if (v !== void 0 && v !== null && v !== false) node.setAttribute(k, String(v));
591
+ }
592
+ }
593
+ if (children) {
594
+ for (const c of [].concat(children)) if (c != null) node.append(c);
595
+ }
596
+ return node;
597
+ }
598
+ function fmtTime(ms) {
599
+ const d = new Date(ms);
600
+ const p = (n, w) => String(n).padStart(w, "0");
601
+ return `${p(d.getHours(), 2)}:${p(d.getMinutes(), 2)}:${p(d.getSeconds(), 2)}.${p(d.getMilliseconds(), 3)}`;
602
+ }
603
+ var fmtMs = (n) => typeof n === "number" && Number.isFinite(n) ? `${n.toFixed(1)} ms` : "";
604
+ var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
605
+ var componentList = (m) => (m instanceof Map ? [...m] : Object.entries(m)).map(([c, n]) => `<${c}>${n > 1 ? " \xD7" + n : ""}`).join(", ");
606
+ function changesOf(report) {
607
+ return [].concat(report.propChanges || [], report.stateChanges || [], report.hookChanges || []);
608
+ }
609
+ var isRecord = (v) => typeof v === "object" && v !== null;
610
+ function normalizeReport(p) {
611
+ if (!isRecord(p) || typeof p.component !== "string") return null;
612
+ const arr = (x) => Array.isArray(x) ? x.filter((c) => isRecord(c) && typeof c.path === "string") : [];
613
+ const props = isRecord(p.props) ? p.props : {};
614
+ 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;
615
+ const r = {
616
+ component: p.component,
617
+ instanceId: typeof p.instanceId === "number" ? p.instanceId : 0,
618
+ commitId: typeof p.commitId === "number" ? p.commitId : 0,
619
+ renderCount: typeof p.renderCount === "number" ? p.renderCount : 0,
620
+ trigger: typeof p.trigger === "string" ? p.trigger : "parent",
621
+ avoidable: !!p.avoidable,
622
+ props: { prev: isRecord(props.prev) ? props.prev : {}, next: isRecord(props.next) ? props.next : {} },
623
+ propChanges: arr(p.propChanges),
624
+ stateChanges: arr(p.stateChanges),
625
+ hookChanges: arr(p.hookChanges),
626
+ parent,
627
+ owner: typeof p.owner === "string" ? p.owner : null,
628
+ path: Array.isArray(p.path) ? p.path.filter((x) => typeof x === "string") : [],
629
+ reasons: Array.isArray(p.reasons) ? p.reasons.filter((x) => typeof x === "string") : [],
630
+ time: typeof p.time === "number" ? p.time : 0,
631
+ receivedAt: typeof p.receivedAt === "number" ? p.receivedAt : 0
556
632
  };
633
+ if (typeof p.selfDuration === "number") r.selfDuration = p.selfDuration;
634
+ if (typeof p.treeDuration === "number") r.treeDuration = p.treeDuration;
635
+ if (typeof p.memoized === "boolean") r.memoized = p.memoized;
636
+ if (p.compiled === true) r.compiled = true;
637
+ if (typeof p.commitPriority === "string") r.commitPriority = p.commitPriority;
638
+ if (Array.isArray(p.hookState)) r.hookState = p.hookState.filter((h) => isRecord(h) && typeof h.path === "string");
639
+ if (Array.isArray(p.contexts)) r.contexts = p.contexts.filter((c) => isRecord(c) && typeof c.name === "string");
640
+ if (isRecord(p.state)) r.state = p.state;
641
+ if (Array.isArray(p.updaters)) r.updaters = p.updaters.filter((u) => typeof u === "string");
642
+ if (p.commitCause === "effect-after-commit" || p.commitCause === "suspense-resolved") r.commitCause = p.commitCause;
643
+ if (typeof p.afterCommit === "number") r.afterCommit = p.afterCommit;
644
+ if (typeof p.key === "string") r.key = p.key;
645
+ if (isRecord(p.source) && typeof p.source.fileName === "string") r.source = p.source;
646
+ return r;
647
+ }
648
+ function valueNode(v, depth = 0) {
649
+ if (v === null || v === void 0) return el("span", { class: "v nil", text: String(v) });
650
+ const t = typeof v;
651
+ if (typeof v === "string") {
652
+ if (v.startsWith(FN_PREFIX)) return el("span", { class: "v fn", text: v });
653
+ if (/^<[^>]+>$/.test(v)) return el("span", { class: "v", text: v });
654
+ return el("span", { class: "v str", text: JSON.stringify(v) });
655
+ }
656
+ if (t === "number" || t === "bigint") return el("span", { class: "v num", text: String(v) });
657
+ if (t === "boolean") return el("span", { class: "v bool", text: String(v) });
658
+ if (Array.isArray(v)) {
659
+ const short = v.length <= 4 && v.every((x) => typeof x !== "object" || x === null);
660
+ if (short) {
661
+ const s = el("span", { class: "v" }, "[");
662
+ v.forEach((x, i) => {
663
+ if (i) s.append(", ");
664
+ s.append(valueNode(x, depth + 1));
665
+ });
666
+ s.append("]");
667
+ return s;
668
+ }
669
+ return objectDetails(`Array(${v.length})`, v);
670
+ }
671
+ const o = v;
672
+ if (o.$type === "element") {
673
+ const props = isRecord(o.props) ? o.props : {};
674
+ const label2 = `<${String(o.name)}${o.key !== void 0 ? ` key=${JSON.stringify(o.key)}` : ""}>`;
675
+ return Object.keys(props).length ? objectDetails(label2, props) : el("span", { class: "v", text: label2 });
676
+ }
677
+ if (o.$type === "Date") return el("span", { class: "v", text: `Date(${String(o.value)})` });
678
+ if (o.$type === "RegExp") return el("span", { class: "v", text: String(o.value) });
679
+ if (o.$type === "Map" && Array.isArray(o.entries)) return objectDetails(`Map(${o.entries.length})`, o.entries);
680
+ if (o.$type === "Set" && Array.isArray(o.values)) return objectDetails(`Set(${o.values.length})`, o.values);
681
+ const keys = Object.keys(o).filter((k) => k !== "$type");
682
+ const label = `${o.$type ? String(o.$type) + " " : ""}{${keys.slice(0, 3).join(", ")}${keys.length > 3 ? ", ..." : ""}}`;
683
+ return objectDetails(label, o);
684
+ }
685
+ function objectDetails(label, obj) {
686
+ const d = el("details", { class: "obj" }, [el("summary", { text: label })]);
687
+ d.addEventListener(
688
+ "toggle",
689
+ () => {
690
+ if (d.open && !d.querySelector("pre")) d.append(el("pre", { text: JSON.stringify(obj, null, 2) }));
691
+ },
692
+ { once: true }
693
+ );
694
+ return d;
695
+ }
696
+ function analyzeCommit2(reports) {
697
+ const first = reports[0];
698
+ return { ...analyzeCommit(reports), receivedAt: first ? first.receivedAt : 0, fixes: rankFixes(reports) };
699
+ }
700
+ function rootCauseSummary2(name, commits, analyze = (_, reports) => analyzeCommit2(reports)) {
701
+ const s = rootCauseSummary(name, commits, analyze);
702
+ return { ...s, fixes: rankFixes(s.affected) };
557
703
  }
558
704
  var PRIORITY_LABEL = { immediate: "discrete input", "user-blocking": "continuous input", normal: "transition / async", low: "low", idle: "idle" };
559
705
  function sourceContext(text, line, around = 3) {
@@ -710,7 +856,8 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
710
856
  if (r.parent) by.append(el("div", { text: `Triggered by <${r.parent.name}> (${r.parent.trigger})` }));
711
857
  else by.append(el("div", { text: "Update started in this component" }));
712
858
  if (r.owner) by.append(el("div", { class: "meta", text: `Created by <${r.owner}>` }));
713
- if (r.memoized === false) by.append(el("div", { class: "meta", text: "Not memoized (re-renders whenever its parent does)" }));
859
+ if (r.compiled) by.append(el("div", { class: "meta", text: "Compiled by React Compiler (output memoized per input; identical inputs make the render cheap)" }));
860
+ else if (r.memoized === false) by.append(el("div", { class: "meta", text: "Not memoized (re-renders whenever its parent does)" }));
714
861
  else if (r.memoized === true) by.append(el("div", { class: "meta", text: "Memoized (React.memo / PureComponent)" }));
715
862
  if (r.updaters && r.updaters.length) by.append(el("div", { text: `Update scheduled by ${r.updaters.map((u) => `<${u}>`).join(", ")}` }));
716
863
  if (r.commitCause === "effect-after-commit") by.append(el("div", { class: "cause effect", text: `Effect loop: state set right after commit #${r.afterCommit ?? "?"}` }));
@@ -1250,7 +1397,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1250
1397
  function analysisFor(key, reports) {
1251
1398
  const cached = commitAnalyses.get(key);
1252
1399
  if (cached && cached.len === reports.length) return cached.analysis;
1253
- const analysis = analyzeCommit(reports);
1400
+ const analysis = analyzeCommit2(reports);
1254
1401
  commitAnalyses.set(key, { len: reports.length, analysis });
1255
1402
  return analysis;
1256
1403
  }
@@ -1981,7 +2128,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1981
2128
  }
1982
2129
  function renderRootDetails() {
1983
2130
  const name = state.selectedRoot;
1984
- const s = rootCauseSummary(name, state.commits, analysisFor);
2131
+ const s = rootCauseSummary2(name, state.commits, analysisFor);
1985
2132
  details.append(
1986
2133
  el("div", { class: "details-header" }, [
1987
2134
  el("span", { class: "title" }, ["Root cause ", el("span", { class: "name", text: `<${name}>` })]),
@@ -2434,6 +2581,33 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2434
2581
  const max = el("input", { type: "number", min: "0", value: String(current.maxReportsPerComponent || 0) });
2435
2582
  max.addEventListener("change", () => void applyOptions({ maxReportsPerComponent: Math.max(0, Number(max.value) || 0) }));
2436
2583
  sec.append(el("label", { class: "opt col" }, ["Stop printing a component after N reports (0 = never)", max]));
2584
+ const help = el("div", { class: "section" }, [el("h3", { text: "Help" })]);
2585
+ help.append(
2586
+ el("div", { class: "meta", text: "Opens a GitHub issue prefilled with versions, options and counts. No report contents are included; attach an Export if it helps." }),
2587
+ el("a", { class: "report-link", href: issueUrl(), target: "_blank", rel: "noreferrer", text: "Report a problem \u2197" })
2588
+ );
2589
+ body.append(help);
2590
+ }
2591
+ function issueUrl() {
2592
+ const lib = state.library;
2593
+ const ext = typeof chrome !== "undefined" && chrome.runtime && typeof chrome.runtime.getManifest === "function" ? chrome.runtime.getManifest().version : "n/a";
2594
+ const react = lib?.react?.map((r) => `${r.version || "?"}${r.bundleType === 0 ? " (prod)" : ""}`).join(", ") || "not detected";
2595
+ const transportLabel = state.tabLabel || (state.relay ? "content script" : state.polling ? "polling" : "none");
2596
+ const lines = [
2597
+ "### What happened",
2598
+ "",
2599
+ "(what you did, what you expected, what the panel showed)",
2600
+ "",
2601
+ "### Environment",
2602
+ `- extension ${ext}, panel protocol ${PROTOCOL}, transport: ${transportLabel}`,
2603
+ `- library ${lib?.library || "not detected"} (protocol ${lib?.protocol ?? "?"}), React ${react}${lib?.production ? ", production build" : ""}`,
2604
+ `- options: ${JSON.stringify(lib?.options || {})}`,
2605
+ `- overhead: ${lib?.overhead ? `${lib.overhead.totalMs.toFixed(1)} ms over ${lib.commits ?? 0} commits, worst ${lib.overhead.maxCommitMs.toFixed(1)} ms` : "n/a"}; reports skipped by the cap: ${lib?.truncated ?? 0}`,
2606
+ `- buffered: ${state.reports.length} reports, ${totals.avoidable} avoidable; top fix: ${bestFix ? bestFix.label : "none"}`,
2607
+ `- browser: ${typeof navigator !== "undefined" ? navigator.userAgent : "n/a"}`
2608
+ ];
2609
+ const params2 = new URLSearchParams({ title: "Panel: ", body: lines.join("\n"), labels: "bug" });
2610
+ return `https://github.com/NexaLeaf/rerender-lens/issues/new?${params2.toString()}`;
2437
2611
  }
2438
2612
  function handle(message) {
2439
2613
  if (!isRecord(message)) return;
@@ -2590,12 +2764,28 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2590
2764
  const info = await io.bridge("info");
2591
2765
  if (info) {
2592
2766
  emit({ type: "hello", version: info.protocol, payload: info });
2767
+ startInfoRefresh();
2593
2768
  return true;
2594
2769
  }
2595
2770
  } catch {
2596
2771
  }
2597
2772
  return false;
2598
2773
  }
2774
+ let infoTimer = null;
2775
+ function startInfoRefresh() {
2776
+ if (infoTimer) return;
2777
+ infoTimer = setInterval(() => {
2778
+ if (disposed) return stopInfoRefresh();
2779
+ io.bridge("info").then((info) => {
2780
+ if (info) emit({ type: "hello", version: info.protocol, payload: info });
2781
+ else stopInfoRefresh();
2782
+ }).catch(() => stopInfoRefresh());
2783
+ }, INFO_REFRESH_MS);
2784
+ }
2785
+ function stopInfoRefresh() {
2786
+ if (infoTimer) clearInterval(infoTimer);
2787
+ infoTimer = null;
2788
+ }
2599
2789
  let syncGen = 0;
2600
2790
  let syncTimer = null;
2601
2791
  let disposed = false;
@@ -2603,6 +2793,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2603
2793
  syncGen++;
2604
2794
  if (syncTimer) clearTimeout(syncTimer);
2605
2795
  syncTimer = null;
2796
+ stopInfoRefresh();
2606
2797
  }
2607
2798
  function syncWithRetry(onReady, initialDelay = 0) {
2608
2799
  cancelSync();
@@ -3158,7 +3349,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
3158
3349
  const apps = isRecord(m.payload) && typeof m.payload.apps === "number" ? m.payload.apps : 0;
3159
3350
  const wasOnline = appsOnline;
3160
3351
  appsOnline = apps;
3161
- if (apps > 0 && wasOnline !== null && wasOnline === 0) void attach();
3352
+ if (apps > 0 && (wasOnline === null || wasOnline === 0)) void attach();
3162
3353
  else if (apps === 0) emit({ type: "disconnected" });
3163
3354
  } 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 });
3164
3355
  }
@@ -3187,7 +3378,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
3187
3378
  };
3188
3379
  };
3189
3380
  open();
3190
- void attach();
3191
3381
  },
3192
3382
  replay: () => void bridge("replay"),
3193
3383
  clear: () => void bridge("clear"),
@@ -3272,7 +3462,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
3272
3462
  createBroadcastTransport,
3273
3463
  createRelayClientTransport,
3274
3464
  sourceContext,
3275
- analysis: { firstDifferentPath, diffLeaves, fixesFor, rankFixes, rootCauseOf, analyzeCommit, contextAttribution, cascadeTree, rootCauseSummary, summarizeSession, compareSessions }
3465
+ analysis: { firstDifferentPath, diffLeaves, fixesFor, rankFixes, rootCauseOf, analyzeCommit: analyzeCommit2, contextAttribution, cascadeTree, rootCauseSummary: rootCauseSummary2, summarizeSession, compareSessions }
3276
3466
  };
3277
3467
  window.RerenderLensPanel = api;
3278
3468
  var hasChrome = typeof chrome !== "undefined" && !!chrome && !!chrome.runtime && !!chrome.runtime.id;