rerender-lens 0.4.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +127 -0
- package/README.md +208 -359
- package/dist/{budget-LkNjGtRc.d.ts → budget-CA1MS7lB.d.cts} +20 -10
- package/dist/{budget-COBu7jBU.d.cts → budget-ok1VDJ4t.d.ts} +20 -10
- package/dist/cli.cjs +322 -53
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +322 -53
- package/dist/cli.js.map +1 -1
- package/dist/{devtools-BkCct3cJ.d.cts → devtools-Cl5n_52v.d.ts} +17 -2
- package/dist/{devtools-CZebzpF6.d.ts → devtools-UwQx2VYS.d.cts} +17 -2
- package/dist/index.cjs +537 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +137 -8
- package/dist/index.d.ts +137 -8
- package/dist/index.js +521 -50
- package/dist/index.js.map +1 -1
- package/dist/jest-setup.cjs +1690 -0
- package/dist/jest-setup.cjs.map +1 -0
- package/dist/jest-setup.d.cts +8 -0
- package/dist/jest-setup.d.ts +8 -0
- package/dist/jest-setup.js +1688 -0
- package/dist/jest-setup.js.map +1 -0
- package/dist/jest.cjs +1777 -0
- package/dist/jest.cjs.map +1 -0
- package/dist/jest.d.cts +23 -0
- package/dist/jest.d.ts +23 -0
- package/dist/jest.js +1768 -0
- package/dist/jest.js.map +1 -0
- package/dist/{notifiers-BjjGSHpp.d.ts → notifiers-B5poAEvE.d.cts} +8 -4
- package/dist/{notifiers-BGQtWKfX.d.cts → notifiers-CqQW0Eum.d.ts} +8 -4
- package/dist/playwright.cjs +230 -28
- package/dist/playwright.cjs.map +1 -1
- package/dist/playwright.d.cts +4 -4
- package/dist/playwright.d.ts +4 -4
- package/dist/playwright.js +230 -28
- package/dist/playwright.js.map +1 -1
- package/dist/relay.cjs +71 -19
- package/dist/relay.cjs.map +1 -1
- package/dist/relay.d.cts +25 -3
- package/dist/relay.d.ts +25 -3
- package/dist/relay.js +71 -19
- package/dist/relay.js.map +1 -1
- package/dist/rerender-lens.iife.js +457 -62
- package/dist/runner-report-Bvqoe89M.d.ts +46 -0
- package/dist/runner-report-CvpDg2zt.d.cts +46 -0
- package/dist/setup.cjs +209 -35
- package/dist/setup.cjs.map +1 -1
- package/dist/setup.js +209 -35
- package/dist/setup.js.map +1 -1
- package/dist/{types-BzUEVkxJ.d.cts → types-DwNN5cWe.d.cts} +82 -2
- package/dist/{types-BzUEVkxJ.d.ts → types-DwNN5cWe.d.ts} +82 -2
- package/dist/vite.d.cts +1 -1
- package/dist/vite.d.ts +1 -1
- package/dist/vitest-setup.cjs +357 -44
- package/dist/vitest-setup.cjs.map +1 -1
- package/dist/vitest-setup.d.cts +4 -4
- package/dist/vitest-setup.d.ts +4 -4
- package/dist/vitest-setup.js +357 -44
- package/dist/vitest-setup.js.map +1 -1
- package/dist/vitest.cjs +420 -80
- package/dist/vitest.cjs.map +1 -1
- package/dist/vitest.d.cts +21 -39
- package/dist/vitest.d.ts +21 -39
- package/dist/vitest.js +421 -81
- package/dist/vitest.js.map +1 -1
- package/package.json +41 -4
- package/panel/panel.css +49 -0
- package/panel/panel.js +871 -353
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
|
-
//
|
|
5
|
-
var
|
|
6
|
-
var
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
return
|
|
53
|
-
}
|
|
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 || []);
|
|
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;
|
|
59
16
|
}
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
if (typeof
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
91
|
+
return true;
|
|
126
92
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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 (
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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,
|
|
153
|
-
if (
|
|
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
|
|
132
|
+
if (a.length !== b.length) return path ? `${path}.length` : "length";
|
|
158
133
|
for (let i = 0; i < a.length; i++) {
|
|
159
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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,
|
|
172
|
-
if (out.length >= limit || a
|
|
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,
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
203
|
-
const
|
|
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
|
|
319
|
+
for (const c of propChanges) {
|
|
320
|
+
if (!AVOIDABLE_KINDS.has(c.kind)) continue;
|
|
223
321
|
const owner = ownerName || "?";
|
|
224
|
-
const root = 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
|
-
|
|
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 [
|
|
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 || `${
|
|
420
|
+
owner: providerOwner || `${ctxName}.Provider`,
|
|
326
421
|
target: r.component,
|
|
327
|
-
prop:
|
|
328
|
-
label: `memoize the ${
|
|
329
|
-
detail: `<${r.component}> re-rendered because ${
|
|
330
|
-
snippet: `// ${providerOwner || `where <${
|
|
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
|
-
<${
|
|
427
|
+
<${ctxName}.Provider value={value}>`
|
|
333
428
|
});
|
|
334
429
|
} else if (c.hook === "useSyncExternalStore") {
|
|
335
430
|
const chain = c.custom || [];
|
|
@@ -348,163 +443,67 @@ import { shallowEqual } from 'react-redux';
|
|
|
348
443
|
const slice = useSelector(selectSlice, shallowEqual);
|
|
349
444
|
// or memoize: const selectSlice = createSelector([selectA, selectB], (a, b) => ({ a, b }));` : zustand ? `// ${r.component}
|
|
350
445
|
import { useShallow } from 'zustand/react/shallow';
|
|
351
|
-
const { a, b } = useStore(useShallow((s) => ({ a: s.a, b: s.b })));
|
|
352
|
-
// or select a primitive: const a = useStore((s) => s.a);` : `// ${r.component}
|
|
353
|
-
// getSnapshot must return the same reference while the data is unchanged
|
|
354
|
-
const snapshot = useSyncExternalStore(subscribe, store.getSnapshot /* cached */);`
|
|
355
|
-
});
|
|
356
|
-
} else {
|
|
357
|
-
out.push({
|
|
358
|
-
kind: "bailout",
|
|
359
|
-
owner: r.component,
|
|
360
|
-
target: r.component,
|
|
361
|
-
prop: c.path,
|
|
362
|
-
label: `bail out before setting ${c.path} in <${r.component}>`,
|
|
363
|
-
detail: `${c.path} was set to a value deep-equal to the current one (new reference).`,
|
|
364
|
-
snippet: `// ${r.component}
|
|
365
|
-
setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
return out;
|
|
370
|
-
}
|
|
371
|
-
var fixKey = (f) => `${f.kind}|${f.owner}|${f.prop || f.target}`;
|
|
372
|
-
function rankFixes(reports) {
|
|
373
|
-
const byKey = /* @__PURE__ */ new Map();
|
|
374
|
-
for (const r of reports) {
|
|
375
|
-
if (!r.avoidable) continue;
|
|
376
|
-
for (const f of fixesFor(r)) {
|
|
377
|
-
const k = fixKey(f);
|
|
378
|
-
let agg = byKey.get(k);
|
|
379
|
-
if (!agg) {
|
|
380
|
-
agg = { ...f, key: k, count: 0, components: /* @__PURE__ */ new Map(), reports: [] };
|
|
381
|
-
byKey.set(k, agg);
|
|
382
|
-
}
|
|
383
|
-
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);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
return [...byKey.values()].sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
|
|
389
|
-
}
|
|
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);
|
|
446
|
+
const { a, b } = useStore(useShallow((s) => ({ a: s.a, b: s.b })));
|
|
447
|
+
// or select a primitive: const a = useStore((s) => s.a);` : `// ${r.component}
|
|
448
|
+
// getSnapshot must return the same reference while the data is unchanged
|
|
449
|
+
const snapshot = useSyncExternalStore(subscribe, store.getSnapshot /* cached */);`
|
|
450
|
+
});
|
|
451
|
+
} else {
|
|
452
|
+
out.push({
|
|
453
|
+
kind: "bailout",
|
|
454
|
+
owner: r.component,
|
|
455
|
+
target: r.component,
|
|
456
|
+
prop: c.path,
|
|
457
|
+
label: `bail out before setting ${c.path} in <${r.component}>`,
|
|
458
|
+
detail: `${c.path} was set to a value deep-equal to the current one (new reference).`,
|
|
459
|
+
snippet: `// ${r.component}
|
|
460
|
+
setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
461
|
+
});
|
|
468
462
|
}
|
|
469
463
|
}
|
|
470
|
-
return
|
|
464
|
+
return out;
|
|
471
465
|
}
|
|
472
|
-
|
|
473
|
-
|
|
466
|
+
var fixKey = (f) => `${f.kind}|${f.owner}|${f.prop || f.target}`;
|
|
467
|
+
function rankFixes(reports) {
|
|
468
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
474
469
|
for (const r of reports) {
|
|
475
|
-
|
|
476
|
-
for (const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
470
|
+
if (!r.avoidable) continue;
|
|
471
|
+
for (const f of fixesFor(r)) {
|
|
472
|
+
const key = fixKey(f);
|
|
473
|
+
let agg = byKey.get(key);
|
|
474
|
+
if (!agg) {
|
|
475
|
+
agg = { ...f, key, count: 0, components: {}, reports: [] };
|
|
476
|
+
byKey.set(key, agg);
|
|
481
477
|
}
|
|
482
|
-
|
|
478
|
+
agg.count++;
|
|
479
|
+
agg.components[r.component] = (agg.components[r.component] || 0) + 1;
|
|
480
|
+
if (agg.reports.length < MAX_FIX_REPORTS) agg.reports.push(r);
|
|
483
481
|
}
|
|
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
482
|
}
|
|
488
|
-
return
|
|
483
|
+
return [...byKey.values()].sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
|
|
489
484
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
|
|
504
|
-
|
|
505
|
-
return
|
|
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
|
-
|
|
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:
|
|
526
|
-
name:
|
|
527
|
-
startedAt:
|
|
528
|
-
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
|
|
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,159 @@ 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
|
+
var MAX_STRIP_BARS = 200;
|
|
581
|
+
var MIN_BAR_PCT = 8;
|
|
582
|
+
function el(tag, attrs, children) {
|
|
583
|
+
const node = document.createElement(tag);
|
|
584
|
+
if (attrs) {
|
|
585
|
+
for (const k of Object.keys(attrs)) {
|
|
586
|
+
const v = attrs[k];
|
|
587
|
+
if (k === "class") node.className = String(v);
|
|
588
|
+
else if (k === "text") node.textContent = String(v);
|
|
589
|
+
else if (k.startsWith("on")) {
|
|
590
|
+
if (typeof v === "function") node.addEventListener(k.slice(2), v);
|
|
591
|
+
} else if (v === true) node.setAttribute(k, "");
|
|
592
|
+
else if (v !== void 0 && v !== null && v !== false) node.setAttribute(k, String(v));
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
if (children) {
|
|
596
|
+
for (const c of [].concat(children)) if (c != null) node.append(c);
|
|
597
|
+
}
|
|
598
|
+
return node;
|
|
599
|
+
}
|
|
600
|
+
function fmtTime(ms) {
|
|
601
|
+
const d = new Date(ms);
|
|
602
|
+
const p = (n, w) => String(n).padStart(w, "0");
|
|
603
|
+
return `${p(d.getHours(), 2)}:${p(d.getMinutes(), 2)}:${p(d.getSeconds(), 2)}.${p(d.getMilliseconds(), 3)}`;
|
|
604
|
+
}
|
|
605
|
+
var fmtMs = (n) => typeof n === "number" && Number.isFinite(n) ? `${n.toFixed(1)} ms` : "";
|
|
606
|
+
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
607
|
+
var fmtSpan = (ms) => ms < 1e3 ? `${Math.round(ms)}ms` : ms < 6e4 ? `${(ms / 1e3).toFixed(1)}s` : `${(ms / 6e4).toFixed(1)}m`;
|
|
608
|
+
var spokenAgo = (ms) => ms < 1e3 ? "just now" : ms < 6e4 ? `${Math.round(ms / 1e3)} seconds ago` : `${Math.round(ms / 6e4)} minutes ago`;
|
|
609
|
+
var componentList = (m) => (m instanceof Map ? [...m] : Object.entries(m)).map(([c, n]) => `<${c}>${n > 1 ? " \xD7" + n : ""}`).join(", ");
|
|
610
|
+
function changesOf(report) {
|
|
611
|
+
return [].concat(report.propChanges || [], report.stateChanges || [], report.hookChanges || []);
|
|
612
|
+
}
|
|
613
|
+
var isRecord = (v) => typeof v === "object" && v !== null;
|
|
614
|
+
function normalizeReport(p) {
|
|
615
|
+
if (!isRecord(p) || typeof p.component !== "string") return null;
|
|
616
|
+
const arr = (x) => Array.isArray(x) ? x.filter((c) => isRecord(c) && typeof c.path === "string") : [];
|
|
617
|
+
const props = isRecord(p.props) ? p.props : {};
|
|
618
|
+
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;
|
|
619
|
+
const r = {
|
|
620
|
+
component: p.component,
|
|
621
|
+
instanceId: typeof p.instanceId === "number" ? p.instanceId : 0,
|
|
622
|
+
commitId: typeof p.commitId === "number" ? p.commitId : 0,
|
|
623
|
+
renderCount: typeof p.renderCount === "number" ? p.renderCount : 0,
|
|
624
|
+
trigger: typeof p.trigger === "string" ? p.trigger : "parent",
|
|
625
|
+
avoidable: !!p.avoidable,
|
|
626
|
+
props: { prev: isRecord(props.prev) ? props.prev : {}, next: isRecord(props.next) ? props.next : {} },
|
|
627
|
+
propChanges: arr(p.propChanges),
|
|
628
|
+
stateChanges: arr(p.stateChanges),
|
|
629
|
+
hookChanges: arr(p.hookChanges),
|
|
630
|
+
parent,
|
|
631
|
+
owner: typeof p.owner === "string" ? p.owner : null,
|
|
632
|
+
path: Array.isArray(p.path) ? p.path.filter((x) => typeof x === "string") : [],
|
|
633
|
+
reasons: Array.isArray(p.reasons) ? p.reasons.filter((x) => typeof x === "string") : [],
|
|
634
|
+
time: typeof p.time === "number" ? p.time : 0,
|
|
635
|
+
receivedAt: typeof p.receivedAt === "number" ? p.receivedAt : 0
|
|
556
636
|
};
|
|
637
|
+
if (typeof p.selfDuration === "number") r.selfDuration = p.selfDuration;
|
|
638
|
+
if (typeof p.treeDuration === "number") r.treeDuration = p.treeDuration;
|
|
639
|
+
if (typeof p.memoized === "boolean") r.memoized = p.memoized;
|
|
640
|
+
if (p.compiled === true) r.compiled = true;
|
|
641
|
+
if (typeof p.commitPriority === "string") r.commitPriority = p.commitPriority;
|
|
642
|
+
if (Array.isArray(p.hookState)) r.hookState = p.hookState.filter((h) => isRecord(h) && typeof h.path === "string");
|
|
643
|
+
if (Array.isArray(p.contexts)) r.contexts = p.contexts.filter((c) => isRecord(c) && typeof c.name === "string");
|
|
644
|
+
if (isRecord(p.state)) r.state = p.state;
|
|
645
|
+
if (Array.isArray(p.updaters)) r.updaters = p.updaters.filter((u) => typeof u === "string");
|
|
646
|
+
if (p.commitCause === "effect-after-commit" || p.commitCause === "suspense-resolved") r.commitCause = p.commitCause;
|
|
647
|
+
if (typeof p.afterCommit === "number") r.afterCommit = p.afterCommit;
|
|
648
|
+
if (typeof p.key === "string") r.key = p.key;
|
|
649
|
+
if (isRecord(p.source) && typeof p.source.fileName === "string") r.source = p.source;
|
|
650
|
+
return r;
|
|
651
|
+
}
|
|
652
|
+
function valueNode(v, depth = 0) {
|
|
653
|
+
if (v === null || v === void 0) return el("span", { class: "v nil", text: String(v) });
|
|
654
|
+
const t = typeof v;
|
|
655
|
+
if (typeof v === "string") {
|
|
656
|
+
if (v.startsWith(FN_PREFIX)) return el("span", { class: "v fn", text: v });
|
|
657
|
+
if (/^<[^>]+>$/.test(v)) return el("span", { class: "v", text: v });
|
|
658
|
+
return el("span", { class: "v str", text: JSON.stringify(v) });
|
|
659
|
+
}
|
|
660
|
+
if (t === "number" || t === "bigint") return el("span", { class: "v num", text: String(v) });
|
|
661
|
+
if (t === "boolean") return el("span", { class: "v bool", text: String(v) });
|
|
662
|
+
if (Array.isArray(v)) {
|
|
663
|
+
const short = v.length <= 4 && v.every((x) => typeof x !== "object" || x === null);
|
|
664
|
+
if (short) {
|
|
665
|
+
const s = el("span", { class: "v" }, "[");
|
|
666
|
+
v.forEach((x, i) => {
|
|
667
|
+
if (i) s.append(", ");
|
|
668
|
+
s.append(valueNode(x, depth + 1));
|
|
669
|
+
});
|
|
670
|
+
s.append("]");
|
|
671
|
+
return s;
|
|
672
|
+
}
|
|
673
|
+
return objectDetails(`Array(${v.length})`, v);
|
|
674
|
+
}
|
|
675
|
+
const o = v;
|
|
676
|
+
if (o.$type === "element") {
|
|
677
|
+
const props = isRecord(o.props) ? o.props : {};
|
|
678
|
+
const label2 = `<${String(o.name)}${o.key !== void 0 ? ` key=${JSON.stringify(o.key)}` : ""}>`;
|
|
679
|
+
return Object.keys(props).length ? objectDetails(label2, props) : el("span", { class: "v", text: label2 });
|
|
680
|
+
}
|
|
681
|
+
if (o.$type === "Date") return el("span", { class: "v", text: `Date(${String(o.value)})` });
|
|
682
|
+
if (o.$type === "RegExp") return el("span", { class: "v", text: String(o.value) });
|
|
683
|
+
if (o.$type === "Map" && Array.isArray(o.entries)) return objectDetails(`Map(${o.entries.length})`, o.entries);
|
|
684
|
+
if (o.$type === "Set" && Array.isArray(o.values)) return objectDetails(`Set(${o.values.length})`, o.values);
|
|
685
|
+
const keys = Object.keys(o).filter((k) => k !== "$type");
|
|
686
|
+
const label = `${o.$type ? String(o.$type) + " " : ""}{${keys.slice(0, 3).join(", ")}${keys.length > 3 ? ", ..." : ""}}`;
|
|
687
|
+
return objectDetails(label, o);
|
|
688
|
+
}
|
|
689
|
+
function objectDetails(label, obj) {
|
|
690
|
+
const d = el("details", { class: "obj" }, [el("summary", { text: label })]);
|
|
691
|
+
d.addEventListener(
|
|
692
|
+
"toggle",
|
|
693
|
+
() => {
|
|
694
|
+
if (d.open && !d.querySelector("pre")) d.append(el("pre", { text: JSON.stringify(obj, null, 2) }));
|
|
695
|
+
},
|
|
696
|
+
{ once: true }
|
|
697
|
+
);
|
|
698
|
+
return d;
|
|
699
|
+
}
|
|
700
|
+
function analyzeCommit2(reports) {
|
|
701
|
+
const first = reports[0];
|
|
702
|
+
return { ...analyzeCommit(reports), receivedAt: first ? first.receivedAt : 0, fixes: rankFixes(reports) };
|
|
703
|
+
}
|
|
704
|
+
function rootCauseSummary2(name, commits, analyze = (_, reports) => analyzeCommit2(reports)) {
|
|
705
|
+
const s = rootCauseSummary(name, commits, analyze);
|
|
706
|
+
return { ...s, fixes: rankFixes(s.affected) };
|
|
557
707
|
}
|
|
558
708
|
var PRIORITY_LABEL = { immediate: "discrete input", "user-blocking": "continuous input", normal: "transition / async", low: "low", idle: "idle" };
|
|
559
709
|
function sourceContext(text, line, around = 3) {
|
|
@@ -710,7 +860,8 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
710
860
|
if (r.parent) by.append(el("div", { text: `Triggered by <${r.parent.name}> (${r.parent.trigger})` }));
|
|
711
861
|
else by.append(el("div", { text: "Update started in this component" }));
|
|
712
862
|
if (r.owner) by.append(el("div", { class: "meta", text: `Created by <${r.owner}>` }));
|
|
713
|
-
if (r.
|
|
863
|
+
if (r.compiled) by.append(el("div", { class: "meta", text: "Compiled by React Compiler (output memoized per input; identical inputs make the render cheap)" }));
|
|
864
|
+
else if (r.memoized === false) by.append(el("div", { class: "meta", text: "Not memoized (re-renders whenever its parent does)" }));
|
|
714
865
|
else if (r.memoized === true) by.append(el("div", { class: "meta", text: "Memoized (React.memo / PureComponent)" }));
|
|
715
866
|
if (r.updaters && r.updaters.length) by.append(el("div", { text: `Update scheduled by ${r.updaters.map((u) => `<${u}>`).join(", ")}` }));
|
|
716
867
|
if (r.commitCause === "effect-after-commit") by.append(el("div", { class: "cause effect", text: `Effect loop: state set right after commit #${r.afterCommit ?? "?"}` }));
|
|
@@ -852,6 +1003,8 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
852
1003
|
reports: [],
|
|
853
1004
|
commits: /* @__PURE__ */ new Map(),
|
|
854
1005
|
commitOrder: [],
|
|
1006
|
+
apps: [],
|
|
1007
|
+
appId: null,
|
|
855
1008
|
selectedKey: null,
|
|
856
1009
|
selectedReport: null,
|
|
857
1010
|
selectedCommit: null,
|
|
@@ -877,7 +1030,9 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
877
1030
|
recording: null,
|
|
878
1031
|
selectedSession: null,
|
|
879
1032
|
compareWith: null,
|
|
880
|
-
byInstance: false
|
|
1033
|
+
byInstance: false,
|
|
1034
|
+
timeWindow: null,
|
|
1035
|
+
stripCollapsed: false
|
|
881
1036
|
};
|
|
882
1037
|
let persistTimer = null;
|
|
883
1038
|
let queue = [];
|
|
@@ -937,6 +1092,8 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
937
1092
|
const settingsBtn = iconButton("\u2699", "Settings", "Settings", () => toggleSettings());
|
|
938
1093
|
const status = el("span", { class: "status", title: "" }, [el("span", { class: "dot" }), el("span", { class: "status-text", text: "no page" })]);
|
|
939
1094
|
const tabChip = el("span", { class: "tab-chip", hidden: true, title: "The tab this panel follows" });
|
|
1095
|
+
const appPicker = el("select", { class: "app-picker", hidden: true, title: "Which connected app this panel watches", "aria-label": "App" });
|
|
1096
|
+
appPicker.addEventListener("change", () => transport.selectApp?.(appPicker.value));
|
|
940
1097
|
const undock = transport.undock ? [
|
|
941
1098
|
el("span", { class: "sep" }),
|
|
942
1099
|
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)))),
|
|
@@ -958,11 +1115,31 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
958
1115
|
...undock,
|
|
959
1116
|
el("span", { class: "spacer" }),
|
|
960
1117
|
tabChip,
|
|
1118
|
+
appPicker,
|
|
961
1119
|
status,
|
|
962
1120
|
settingsBtn
|
|
963
1121
|
]);
|
|
964
1122
|
const summary = el("div", { class: "summary" });
|
|
965
1123
|
const banner = el("div", { class: "banner", hidden: true });
|
|
1124
|
+
const stripChevron = el("span", { class: "chevron", text: "\u25BE" });
|
|
1125
|
+
const stripCount = el("span", { class: "count", text: "0 commits" });
|
|
1126
|
+
const stripChip = el("button", {
|
|
1127
|
+
class: "chip",
|
|
1128
|
+
hidden: true,
|
|
1129
|
+
title: "Clear the time window (Esc)",
|
|
1130
|
+
onclick: (e) => {
|
|
1131
|
+
e.stopPropagation();
|
|
1132
|
+
clearWindow();
|
|
1133
|
+
}
|
|
1134
|
+
});
|
|
1135
|
+
const stripBars = el("div", { class: "bars" });
|
|
1136
|
+
const stripFrom = el("span", { class: "from", text: "" });
|
|
1137
|
+
const stripTo = el("span", { class: "to", text: "now" });
|
|
1138
|
+
const stripHeader = el("div", { class: "timeline-header", onclick: () => toggleStrip() }, [stripChevron, el("span", { class: "title", text: "Timeline" }), stripChip, stripCount]);
|
|
1139
|
+
const timeline = el("div", { class: "timeline", hidden: true, role: "group", "aria-label": "Commit timeline" }, [
|
|
1140
|
+
stripHeader,
|
|
1141
|
+
el("div", { class: "timeline-body" }, [stripBars, el("div", { class: "timeline-axis" }, [stripFrom, stripTo])])
|
|
1142
|
+
]);
|
|
966
1143
|
const viewsBar = el("div", { class: "views" });
|
|
967
1144
|
const VIEWS = [
|
|
968
1145
|
["tree", "Tree"],
|
|
@@ -1025,7 +1202,9 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1025
1202
|
details.setAttribute("aria-label", "Details");
|
|
1026
1203
|
settings.setAttribute("role", "dialog");
|
|
1027
1204
|
settings.setAttribute("aria-label", "Settings");
|
|
1028
|
-
|
|
1205
|
+
stripBars.setAttribute("role", "group");
|
|
1206
|
+
stripBars.setAttribute("aria-label", "Commits over time; arrow keys move between commits, Enter selects, drag to filter every view to a time window");
|
|
1207
|
+
root.append(toolbar, summary, banner, timeline, main, stream, toastEl);
|
|
1029
1208
|
root.addEventListener("keydown", onGlobalKey);
|
|
1030
1209
|
function setCompact(on) {
|
|
1031
1210
|
if (state.compact === on) return;
|
|
@@ -1144,7 +1323,8 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1144
1323
|
collapsed: [...state.collapsed],
|
|
1145
1324
|
treeWidth: state.treeWidth,
|
|
1146
1325
|
flashOn: state.flashOn,
|
|
1147
|
-
byInstance: state.byInstance
|
|
1326
|
+
byInstance: state.byInstance,
|
|
1327
|
+
stripCollapsed: state.stripCollapsed
|
|
1148
1328
|
};
|
|
1149
1329
|
transport.storage.set("panel", saved);
|
|
1150
1330
|
}, 150);
|
|
@@ -1196,6 +1376,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1196
1376
|
instancesBtn.classList.toggle("active", state.byInstance);
|
|
1197
1377
|
rebuildTree();
|
|
1198
1378
|
}
|
|
1379
|
+
if (typeof saved.stripCollapsed === "boolean" && saved.stripCollapsed !== state.stripCollapsed) toggleStrip(saved.stripCollapsed);
|
|
1199
1380
|
if (saved.tab === "history" || saved.tab === "fix") state.tab = saved.tab;
|
|
1200
1381
|
if (saved.view && viewButtons.has(saved.view)) state.view = saved.view;
|
|
1201
1382
|
for (const n of state.nodesByKey.values()) n.expanded = !state.collapsed.has(n.key);
|
|
@@ -1250,7 +1431,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1250
1431
|
function analysisFor(key, reports) {
|
|
1251
1432
|
const cached = commitAnalyses.get(key);
|
|
1252
1433
|
if (cached && cached.len === reports.length) return cached.analysis;
|
|
1253
|
-
const analysis =
|
|
1434
|
+
const analysis = analyzeCommit2(reports);
|
|
1254
1435
|
commitAnalyses.set(key, { len: reports.length, analysis });
|
|
1255
1436
|
return analysis;
|
|
1256
1437
|
}
|
|
@@ -1351,6 +1532,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1351
1532
|
function renderHeavy(details2) {
|
|
1352
1533
|
heavyAt = Date.now();
|
|
1353
1534
|
if (state.view !== "tree") renderLeft();
|
|
1535
|
+
renderStrip();
|
|
1354
1536
|
if (details2) renderDetails();
|
|
1355
1537
|
}
|
|
1356
1538
|
function scheduleHeavy(details2) {
|
|
@@ -1397,12 +1579,14 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1397
1579
|
state.selectedCommit = null;
|
|
1398
1580
|
state.selectedFix = null;
|
|
1399
1581
|
state.selectedRoot = null;
|
|
1582
|
+
state.timeWindow = null;
|
|
1400
1583
|
if (state.tab === "commit" || state.tab === "fixlist" || state.tab === "root") state.tab = "latest";
|
|
1401
1584
|
queue = [];
|
|
1402
1585
|
renderLeft();
|
|
1403
1586
|
renderDetails();
|
|
1404
1587
|
renderStream();
|
|
1405
1588
|
renderSummary();
|
|
1589
|
+
renderStrip();
|
|
1406
1590
|
}
|
|
1407
1591
|
let filterCache = { filter: "", value: null, regex: null, text: "" };
|
|
1408
1592
|
function parsedFilter() {
|
|
@@ -1443,17 +1627,45 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1443
1627
|
}
|
|
1444
1628
|
return text.includes(q);
|
|
1445
1629
|
}
|
|
1630
|
+
const inWindow = (r) => {
|
|
1631
|
+
const w = state.timeWindow;
|
|
1632
|
+
return !w || r.receivedAt >= w.from && r.receivedAt <= w.to;
|
|
1633
|
+
};
|
|
1634
|
+
const windowKey = () => {
|
|
1635
|
+
const w = state.timeWindow;
|
|
1636
|
+
return w ? `${w.from}-${w.to}` : "";
|
|
1637
|
+
};
|
|
1638
|
+
const NO_COUNTS = { total: 0, avoidable: 0 };
|
|
1639
|
+
let windowNodes = null;
|
|
1640
|
+
function nodeCounts(node) {
|
|
1641
|
+
if (!state.timeWindow) return node;
|
|
1642
|
+
const key = `${dataGen}|${windowKey()}|${state.byInstance}`;
|
|
1643
|
+
if (!windowNodes || windowNodes.key !== key) {
|
|
1644
|
+
const map = /* @__PURE__ */ new Map();
|
|
1645
|
+
for (const r of state.reports) {
|
|
1646
|
+
if (!inWindow(r)) continue;
|
|
1647
|
+
const k = nodeOfReport(r).key;
|
|
1648
|
+
let c = map.get(k);
|
|
1649
|
+
if (!c) map.set(k, c = { total: 0, avoidable: 0 });
|
|
1650
|
+
c.total++;
|
|
1651
|
+
if (r.avoidable) c.avoidable++;
|
|
1652
|
+
}
|
|
1653
|
+
windowNodes = { key, map };
|
|
1654
|
+
}
|
|
1655
|
+
return windowNodes.map.get(node.key) || NO_COUNTS;
|
|
1656
|
+
}
|
|
1446
1657
|
function visible(node) {
|
|
1447
|
-
const
|
|
1658
|
+
const c = nodeCounts(node);
|
|
1659
|
+
const own = (!state.avoidableOnly || c.avoidable > 0) && matchesFilter(node.name) && c.total > 0 && (valueQuery() === null || node.reports.some(matchesValues));
|
|
1448
1660
|
if (own) return true;
|
|
1449
|
-
for (const
|
|
1661
|
+
for (const child of node.children.values()) if (visible(child)) return true;
|
|
1450
1662
|
return false;
|
|
1451
1663
|
}
|
|
1452
|
-
const passes = (r) => (!state.avoidableOnly || r.avoidable) && matchesFilter(r.component) && matchesValues(r);
|
|
1664
|
+
const passes = (r) => inWindow(r) && (!state.avoidableOnly || r.avoidable) && matchesFilter(r.component) && matchesValues(r);
|
|
1453
1665
|
let dataGen = 0;
|
|
1454
1666
|
let filtered = { key: "", reports: [], fixes: null };
|
|
1455
1667
|
function filteredReports() {
|
|
1456
|
-
const key = `${dataGen}|${state.avoidableOnly}|${state.filter}`;
|
|
1668
|
+
const key = `${dataGen}|${state.avoidableOnly}|${state.filter}|${windowKey()}`;
|
|
1457
1669
|
if (filtered.key !== key) filtered = { key, reports: state.reports.filter(passes), fixes: null };
|
|
1458
1670
|
return filtered.reports;
|
|
1459
1671
|
}
|
|
@@ -1471,6 +1683,154 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1471
1683
|
renderDetails();
|
|
1472
1684
|
persist();
|
|
1473
1685
|
}
|
|
1686
|
+
let bars = [];
|
|
1687
|
+
const barEls = /* @__PURE__ */ new Map();
|
|
1688
|
+
const barNodes = () => [...stripBars.children];
|
|
1689
|
+
function renderStrip() {
|
|
1690
|
+
const order = state.commitOrder;
|
|
1691
|
+
const next = [];
|
|
1692
|
+
let max = 1;
|
|
1693
|
+
for (let i = Math.max(0, order.length - MAX_STRIP_BARS); i < order.length; i++) {
|
|
1694
|
+
const key = order[i];
|
|
1695
|
+
const reports = state.commits.get(key);
|
|
1696
|
+
if (!reports || !reports.length) continue;
|
|
1697
|
+
let avoidable = 0;
|
|
1698
|
+
let from = Infinity;
|
|
1699
|
+
let to = 0;
|
|
1700
|
+
for (const r of reports) {
|
|
1701
|
+
if (r.avoidable) avoidable++;
|
|
1702
|
+
if (r.receivedAt < from) from = r.receivedAt;
|
|
1703
|
+
if (r.receivedAt > to) to = r.receivedAt;
|
|
1704
|
+
}
|
|
1705
|
+
next.push({ key, total: reports.length, avoidable, from, to });
|
|
1706
|
+
if (reports.length > max) max = reports.length;
|
|
1707
|
+
}
|
|
1708
|
+
bars = next;
|
|
1709
|
+
if (!bars.length) {
|
|
1710
|
+
timeline.hidden = true;
|
|
1711
|
+
stripBars.textContent = "";
|
|
1712
|
+
barEls.clear();
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
timeline.hidden = false;
|
|
1716
|
+
const now = bars[bars.length - 1].to;
|
|
1717
|
+
const win = state.timeWindow;
|
|
1718
|
+
const live = /* @__PURE__ */ new Set();
|
|
1719
|
+
const frag = document.createDocumentFragment();
|
|
1720
|
+
for (let i = 0; i < bars.length; i++) {
|
|
1721
|
+
const b = bars[i];
|
|
1722
|
+
live.add(b.key);
|
|
1723
|
+
let node = barEls.get(b.key);
|
|
1724
|
+
if (!node) {
|
|
1725
|
+
node = el("button", { class: "bar", type: "button", "data-commit": String(b.key) }, [el("span", { class: "avoid" })]);
|
|
1726
|
+
barEls.set(b.key, node);
|
|
1727
|
+
}
|
|
1728
|
+
node.setAttribute("data-index", String(i));
|
|
1729
|
+
node.style.height = `${Math.max(MIN_BAR_PCT, Math.round(100 * Math.sqrt(b.total / max)))}%`;
|
|
1730
|
+
node.firstElementChild.style.height = `${Math.round(100 * b.avoidable / b.total)}%`;
|
|
1731
|
+
node.classList.toggle("has-avoid", b.avoidable > 0);
|
|
1732
|
+
node.classList.toggle("selected", state.selectedCommit === b.key && state.tab === "commit");
|
|
1733
|
+
node.classList.toggle("in-window", !!win && b.to >= win.from && b.from <= win.to);
|
|
1734
|
+
node.setAttribute("aria-label", `commit ${b.key}, ${plural(b.total, "render")}, ${b.avoidable} avoidable, ${spokenAgo(Math.max(0, now - b.to))}`);
|
|
1735
|
+
node.title = `#${b.key} \xB7 ${plural(b.total, "render")} \xB7 ${b.avoidable} avoidable`;
|
|
1736
|
+
frag.append(node);
|
|
1737
|
+
}
|
|
1738
|
+
stripBars.textContent = "";
|
|
1739
|
+
stripBars.append(frag);
|
|
1740
|
+
for (const key of [...barEls.keys()]) if (!live.has(key)) barEls.delete(key);
|
|
1741
|
+
const span = now - bars[0].from;
|
|
1742
|
+
stripFrom.textContent = span > 0 ? `-${fmtSpan(span)}` : "0ms";
|
|
1743
|
+
stripTo.textContent = "now";
|
|
1744
|
+
stripCount.textContent = plural(bars.length, "commit") + (order.length > bars.length ? ` of ${order.length}` : "");
|
|
1745
|
+
if (win) {
|
|
1746
|
+
let n = 0;
|
|
1747
|
+
for (const r of state.reports) if (inWindow(r)) n++;
|
|
1748
|
+
stripChip.textContent = `${fmtSpan(win.to - win.from)} window, ${plural(n, "report")} \xB7 Clear`;
|
|
1749
|
+
stripChip.hidden = false;
|
|
1750
|
+
} else stripChip.hidden = true;
|
|
1751
|
+
}
|
|
1752
|
+
function toggleStrip(value) {
|
|
1753
|
+
state.stripCollapsed = value === void 0 ? !state.stripCollapsed : value;
|
|
1754
|
+
timeline.classList.toggle("collapsed", state.stripCollapsed);
|
|
1755
|
+
stripChevron.textContent = state.stripCollapsed ? "\u25B8" : "\u25BE";
|
|
1756
|
+
if (value === void 0) persist();
|
|
1757
|
+
}
|
|
1758
|
+
function setWindow(from, to) {
|
|
1759
|
+
state.timeWindow = from <= to ? { from, to } : { from: to, to: from };
|
|
1760
|
+
afterWindowChange();
|
|
1761
|
+
}
|
|
1762
|
+
function clearWindow() {
|
|
1763
|
+
if (!state.timeWindow) return;
|
|
1764
|
+
state.timeWindow = null;
|
|
1765
|
+
afterWindowChange();
|
|
1766
|
+
}
|
|
1767
|
+
function afterWindowChange() {
|
|
1768
|
+
renderLeft();
|
|
1769
|
+
renderStream();
|
|
1770
|
+
renderDetails();
|
|
1771
|
+
renderStrip();
|
|
1772
|
+
}
|
|
1773
|
+
const barAt = (target) => {
|
|
1774
|
+
const node = target && typeof target.closest === "function" ? target.closest(".bar") : null;
|
|
1775
|
+
const i = node ? Number(node.getAttribute("data-index")) : -1;
|
|
1776
|
+
return Number.isInteger(i) && i >= 0 && i < bars.length ? i : -1;
|
|
1777
|
+
};
|
|
1778
|
+
let brush = null;
|
|
1779
|
+
let swallowClick = false;
|
|
1780
|
+
const paintBrush = () => {
|
|
1781
|
+
const lo = brush ? Math.min(brush.a, brush.b) : -1;
|
|
1782
|
+
const hi = brush ? Math.max(brush.a, brush.b) : -2;
|
|
1783
|
+
const nodes = barNodes();
|
|
1784
|
+
for (let i = 0; i < nodes.length; i++) nodes[i].classList.toggle("brushing", i >= lo && i <= hi);
|
|
1785
|
+
};
|
|
1786
|
+
stripBars.addEventListener("mousedown", (e) => {
|
|
1787
|
+
swallowClick = false;
|
|
1788
|
+
const i = barAt(e.target);
|
|
1789
|
+
if (i < 0) return;
|
|
1790
|
+
brush = { a: i, b: i };
|
|
1791
|
+
});
|
|
1792
|
+
stripBars.addEventListener("mousemove", (e) => {
|
|
1793
|
+
if (!brush) return;
|
|
1794
|
+
const i = barAt(e.target);
|
|
1795
|
+
if (i < 0 || i === brush.b) return;
|
|
1796
|
+
brush.b = i;
|
|
1797
|
+
paintBrush();
|
|
1798
|
+
});
|
|
1799
|
+
window.addEventListener("mouseup", (e) => {
|
|
1800
|
+
if (!brush) return;
|
|
1801
|
+
const i = barAt(e.target);
|
|
1802
|
+
if (i >= 0) brush.b = i;
|
|
1803
|
+
const { a, b } = brush;
|
|
1804
|
+
brush = null;
|
|
1805
|
+
paintBrush();
|
|
1806
|
+
if (a === b) return;
|
|
1807
|
+
swallowClick = true;
|
|
1808
|
+
setWindow(bars[Math.min(a, b)].from, bars[Math.max(a, b)].to);
|
|
1809
|
+
});
|
|
1810
|
+
stripBars.addEventListener("click", (e) => {
|
|
1811
|
+
if (swallowClick) {
|
|
1812
|
+
swallowClick = false;
|
|
1813
|
+
return;
|
|
1814
|
+
}
|
|
1815
|
+
const i = barAt(e.target);
|
|
1816
|
+
if (i >= 0) selectCommitBar(bars[i].key);
|
|
1817
|
+
});
|
|
1818
|
+
stripBars.addEventListener("keydown", (e) => {
|
|
1819
|
+
swallowClick = false;
|
|
1820
|
+
const key = e.key;
|
|
1821
|
+
if (key !== "ArrowLeft" && key !== "ArrowRight" && key !== "Home" && key !== "End") return;
|
|
1822
|
+
const nodes = barNodes();
|
|
1823
|
+
const i = nodes.indexOf(document.activeElement);
|
|
1824
|
+
if (i < 0) return;
|
|
1825
|
+
e.preventDefault();
|
|
1826
|
+
const to = key === "ArrowLeft" ? Math.max(0, i - 1) : key === "ArrowRight" ? Math.min(nodes.length - 1, i + 1) : key === "Home" ? 0 : nodes.length - 1;
|
|
1827
|
+
nodes[to].focus();
|
|
1828
|
+
});
|
|
1829
|
+
function selectCommitBar(key) {
|
|
1830
|
+
showCommit(key);
|
|
1831
|
+
if (state.view !== "commits") setView("commits");
|
|
1832
|
+
renderStrip();
|
|
1833
|
+
}
|
|
1474
1834
|
function renderLeft() {
|
|
1475
1835
|
if (state.view === "tree") renderTree();
|
|
1476
1836
|
else if (state.view === "offenders") renderOffenders();
|
|
@@ -1480,10 +1840,73 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1480
1840
|
}
|
|
1481
1841
|
const rowEls = /* @__PURE__ */ new Map();
|
|
1482
1842
|
const treeList = virtualList(tree, ROW_H, ({ node, depth }) => rowFor(node, depth));
|
|
1483
|
-
const emptyEl = el("div", { class: "empty" }
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1843
|
+
const emptyEl = el("div", { class: "empty" });
|
|
1844
|
+
function trackingSentence(t) {
|
|
1845
|
+
const what = t.mode === "all" ? "Tracking every component" : t.mode === "memoized" ? "Tracking every React.memo and PureComponent" : "Tracking only components marked with track()";
|
|
1846
|
+
const parts = [what];
|
|
1847
|
+
if (t.include.length) parts.push(`${t.mode === "marked" ? "and" : "plus"} names matching ${t.include.join(", ")}`);
|
|
1848
|
+
if (t.exclude.length) parts.push(`except ${t.exclude.join(", ")}`);
|
|
1849
|
+
const rendered = `${t.renderedCount}${t.overflow ? "+" : ""} component${t.renderedCount === 1 ? "" : "s"} rendered`;
|
|
1850
|
+
return `${parts.join(", ")}; ${rendered}, ${t.trackedCount} of them tracked.`;
|
|
1851
|
+
}
|
|
1852
|
+
function renderEmpty() {
|
|
1853
|
+
emptyEl.textContent = "";
|
|
1854
|
+
emptyEl.append(el("div", { class: "empty-head", text: "No re-renders reported yet." }));
|
|
1855
|
+
const t = state.library?.tracking;
|
|
1856
|
+
if (!t) {
|
|
1857
|
+
emptyEl.append(
|
|
1858
|
+
el("div", null, ["Call ", el("code", { text: "init({ notifier: createDevtoolsNotifier() })" }), " in the page, or enable injection in Settings, then interact with it."])
|
|
1859
|
+
);
|
|
1860
|
+
return;
|
|
1861
|
+
}
|
|
1862
|
+
emptyEl.append(el("div", { class: "empty-tracking", text: trackingSentence(t) }));
|
|
1863
|
+
if (t.mode === "all") {
|
|
1864
|
+
emptyEl.append(el("div", { text: "Every component is tracked, so this is waiting for a re-render: interact with the page. The first render of a component is never reported." }));
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1867
|
+
const untracked = t.renderedCount - t.trackedCount;
|
|
1868
|
+
emptyEl.append(
|
|
1869
|
+
el("div", {
|
|
1870
|
+
text: untracked > 0 ? `${untracked} component${untracked === 1 ? "" : "s"} rendered without being tracked. Widen what is tracked, or interact with the page if the tracked ones simply have not re-rendered yet.` : "Interact with the page: the first render of a component is never reported, only re-renders."
|
|
1871
|
+
})
|
|
1872
|
+
);
|
|
1873
|
+
if (!transport.configure) return;
|
|
1874
|
+
const trackAll = el("button", { class: "primary", onclick: () => void applyTracking({ trackAllComponents: true }) }, "Track every component");
|
|
1875
|
+
const match = el("input", { class: "empty-match", type: "text", placeholder: "Name or /regex/", "aria-label": "Track components matching" });
|
|
1876
|
+
const addMatch = () => {
|
|
1877
|
+
const value = match.value.trim();
|
|
1878
|
+
if (!value) return;
|
|
1879
|
+
const include = (state.library?.tracking?.include || []).slice();
|
|
1880
|
+
if (!include.includes(value)) include.push(value);
|
|
1881
|
+
match.value = "";
|
|
1882
|
+
void applyTracking({ include });
|
|
1883
|
+
};
|
|
1884
|
+
match.addEventListener("keydown", (e) => {
|
|
1885
|
+
if (e.key === "Enter") addMatch();
|
|
1886
|
+
});
|
|
1887
|
+
emptyEl.append(
|
|
1888
|
+
el("div", { class: "empty-actions" }, [trackAll, match, el("button", { onclick: addMatch }, "Track components matching\u2026")])
|
|
1889
|
+
);
|
|
1890
|
+
}
|
|
1891
|
+
async function applyTracking(patch) {
|
|
1892
|
+
if (!transport.configure) return;
|
|
1893
|
+
try {
|
|
1894
|
+
const applied = await transport.configure(patch);
|
|
1895
|
+
if (state.library) {
|
|
1896
|
+
if (applied) state.library.options = applied;
|
|
1897
|
+
const t = state.library.tracking;
|
|
1898
|
+
if (t) {
|
|
1899
|
+
if (patch.trackAllComponents) t.mode = "all";
|
|
1900
|
+
if (patch.include) t.include = patch.include;
|
|
1901
|
+
}
|
|
1902
|
+
transport.storage?.set("settings", Object.assign({}, state.library.options));
|
|
1903
|
+
}
|
|
1904
|
+
toast("Applied");
|
|
1905
|
+
renderTree();
|
|
1906
|
+
} catch (e) {
|
|
1907
|
+
toast(`Failed: ${e.message}`);
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1487
1910
|
function renderTree() {
|
|
1488
1911
|
const flat = [];
|
|
1489
1912
|
const walk = (node, depth) => {
|
|
@@ -1495,6 +1918,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1495
1918
|
};
|
|
1496
1919
|
walk(state.tree, 0);
|
|
1497
1920
|
if (flat.length === 0) {
|
|
1921
|
+
renderEmpty();
|
|
1498
1922
|
if (!emptyEl.parentNode) tree.append(emptyEl);
|
|
1499
1923
|
} else emptyEl.remove();
|
|
1500
1924
|
if (rowEls.size > flat.length * 2 + 64) {
|
|
@@ -1551,8 +1975,9 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1551
1975
|
chevron.textContent = node.expanded ? "\u25BE" : "\u25B8";
|
|
1552
1976
|
const badges = row.querySelector(".badges");
|
|
1553
1977
|
badges.textContent = "";
|
|
1554
|
-
|
|
1555
|
-
if (
|
|
1978
|
+
const counts = nodeCounts(node);
|
|
1979
|
+
if (counts.avoidable) badges.append(el("span", { class: "badge avoid", title: "avoidable re-renders", text: String(counts.avoidable) }));
|
|
1980
|
+
if (counts.total) badges.append(el("span", { class: "badge", title: "re-renders", text: String(counts.total) }));
|
|
1556
1981
|
if (node.flash) {
|
|
1557
1982
|
node.flash = false;
|
|
1558
1983
|
if (Date.now() - (node.flashAt || 0) < 1e3) {
|
|
@@ -1607,7 +2032,11 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1607
2032
|
toggleSettings(false);
|
|
1608
2033
|
return;
|
|
1609
2034
|
}
|
|
1610
|
-
if (inField)
|
|
2035
|
+
if (inField) {
|
|
2036
|
+
target.blur();
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
2039
|
+
clearWindow();
|
|
1611
2040
|
return;
|
|
1612
2041
|
}
|
|
1613
2042
|
if (inField || e.ctrlKey || e.metaKey || e.altKey) return;
|
|
@@ -1715,6 +2144,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1715
2144
|
state.tab = "commit";
|
|
1716
2145
|
renderLeft();
|
|
1717
2146
|
renderDetails();
|
|
2147
|
+
renderStrip();
|
|
1718
2148
|
}
|
|
1719
2149
|
function showRoot(name) {
|
|
1720
2150
|
state.selectedRoot = name;
|
|
@@ -1981,7 +2411,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1981
2411
|
}
|
|
1982
2412
|
function renderRootDetails() {
|
|
1983
2413
|
const name = state.selectedRoot;
|
|
1984
|
-
const s =
|
|
2414
|
+
const s = rootCauseSummary2(name, state.commits, analysisFor);
|
|
1985
2415
|
details.append(
|
|
1986
2416
|
el("div", { class: "details-header" }, [
|
|
1987
2417
|
el("span", { class: "title" }, ["Root cause ", el("span", { class: "name", text: `<${name}>` })]),
|
|
@@ -2291,6 +2721,16 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
2291
2721
|
status.querySelector(".status-text").textContent = text;
|
|
2292
2722
|
tabChip.hidden = !state.tabLabel;
|
|
2293
2723
|
tabChip.textContent = state.tabLabel || "";
|
|
2724
|
+
appPicker.hidden = state.apps.length < 2;
|
|
2725
|
+
if (!appPicker.hidden) {
|
|
2726
|
+
const want = state.apps.map((a) => `${a.id}:${a.label}`).join("|");
|
|
2727
|
+
if (appPicker.dataset.apps !== want) {
|
|
2728
|
+
appPicker.dataset.apps = want;
|
|
2729
|
+
appPicker.textContent = "";
|
|
2730
|
+
for (const a of state.apps) appPicker.append(el("option", { value: a.id, text: a.label || a.id }));
|
|
2731
|
+
}
|
|
2732
|
+
if (state.appId) appPicker.value = state.appId;
|
|
2733
|
+
}
|
|
2294
2734
|
banner.textContent = "";
|
|
2295
2735
|
const warnings = [];
|
|
2296
2736
|
if (lib && typeof lib.protocol === "number" && lib.protocol > PROTOCOL) warnings.push(`The page runs a newer rerender-lens (protocol ${lib.protocol}) than this panel (${PROTOCOL}). Update the extension.`);
|
|
@@ -2310,6 +2750,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
2310
2750
|
function setLibrary(info) {
|
|
2311
2751
|
state.library = info;
|
|
2312
2752
|
renderStatus();
|
|
2753
|
+
if (emptyEl.parentNode) renderEmpty();
|
|
2313
2754
|
if (state.settingsOpen) void renderSettings();
|
|
2314
2755
|
if (state.flashOn) transport.flashAvoidable?.(true);
|
|
2315
2756
|
}
|
|
@@ -2434,6 +2875,33 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
2434
2875
|
const max = el("input", { type: "number", min: "0", value: String(current.maxReportsPerComponent || 0) });
|
|
2435
2876
|
max.addEventListener("change", () => void applyOptions({ maxReportsPerComponent: Math.max(0, Number(max.value) || 0) }));
|
|
2436
2877
|
sec.append(el("label", { class: "opt col" }, ["Stop printing a component after N reports (0 = never)", max]));
|
|
2878
|
+
const help = el("div", { class: "section" }, [el("h3", { text: "Help" })]);
|
|
2879
|
+
help.append(
|
|
2880
|
+
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." }),
|
|
2881
|
+
el("a", { class: "report-link", href: issueUrl(), target: "_blank", rel: "noreferrer", text: "Report a problem \u2197" })
|
|
2882
|
+
);
|
|
2883
|
+
body.append(help);
|
|
2884
|
+
}
|
|
2885
|
+
function issueUrl() {
|
|
2886
|
+
const lib = state.library;
|
|
2887
|
+
const ext = typeof chrome !== "undefined" && chrome.runtime && typeof chrome.runtime.getManifest === "function" ? chrome.runtime.getManifest().version : "n/a";
|
|
2888
|
+
const react = lib?.react?.map((r) => `${r.version || "?"}${r.bundleType === 0 ? " (prod)" : ""}`).join(", ") || "not detected";
|
|
2889
|
+
const transportLabel = state.tabLabel || (state.relay ? "content script" : state.polling ? "polling" : "none");
|
|
2890
|
+
const lines = [
|
|
2891
|
+
"### What happened",
|
|
2892
|
+
"",
|
|
2893
|
+
"(what you did, what you expected, what the panel showed)",
|
|
2894
|
+
"",
|
|
2895
|
+
"### Environment",
|
|
2896
|
+
`- extension ${ext}, panel protocol ${PROTOCOL}, transport: ${transportLabel}`,
|
|
2897
|
+
`- library ${lib?.library || "not detected"} (protocol ${lib?.protocol ?? "?"}), React ${react}${lib?.production ? ", production build" : ""}`,
|
|
2898
|
+
`- options: ${JSON.stringify(lib?.options || {})}`,
|
|
2899
|
+
`- 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}`,
|
|
2900
|
+
`- buffered: ${state.reports.length} reports, ${totals.avoidable} avoidable; top fix: ${bestFix ? bestFix.label : "none"}`,
|
|
2901
|
+
`- browser: ${typeof navigator !== "undefined" ? navigator.userAgent : "n/a"}`
|
|
2902
|
+
];
|
|
2903
|
+
const params2 = new URLSearchParams({ title: "Panel: ", body: lines.join("\n"), labels: "bug" });
|
|
2904
|
+
return `https://github.com/NexaLeaf/rerender-lens/issues/new?${params2.toString()}`;
|
|
2437
2905
|
}
|
|
2438
2906
|
function handle(message) {
|
|
2439
2907
|
if (!isRecord(message)) return;
|
|
@@ -2460,6 +2928,13 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
2460
2928
|
state.library = null;
|
|
2461
2929
|
renderStatus();
|
|
2462
2930
|
break;
|
|
2931
|
+
case "apps": {
|
|
2932
|
+
const p = isRecord(message.payload) ? message.payload : {};
|
|
2933
|
+
state.apps = Array.isArray(p.list) ? p.list : [];
|
|
2934
|
+
state.appId = typeof p.selected === "string" ? p.selected : null;
|
|
2935
|
+
renderStatus();
|
|
2936
|
+
break;
|
|
2937
|
+
}
|
|
2463
2938
|
case "hello":
|
|
2464
2939
|
if (isRecord(message.payload)) setLibrary(message.payload);
|
|
2465
2940
|
break;
|
|
@@ -2590,12 +3065,28 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
2590
3065
|
const info = await io.bridge("info");
|
|
2591
3066
|
if (info) {
|
|
2592
3067
|
emit({ type: "hello", version: info.protocol, payload: info });
|
|
3068
|
+
startInfoRefresh();
|
|
2593
3069
|
return true;
|
|
2594
3070
|
}
|
|
2595
3071
|
} catch {
|
|
2596
3072
|
}
|
|
2597
3073
|
return false;
|
|
2598
3074
|
}
|
|
3075
|
+
let infoTimer = null;
|
|
3076
|
+
function startInfoRefresh() {
|
|
3077
|
+
if (infoTimer) return;
|
|
3078
|
+
infoTimer = setInterval(() => {
|
|
3079
|
+
if (disposed) return stopInfoRefresh();
|
|
3080
|
+
io.bridge("info").then((info) => {
|
|
3081
|
+
if (info) emit({ type: "hello", version: info.protocol, payload: info });
|
|
3082
|
+
else stopInfoRefresh();
|
|
3083
|
+
}).catch(() => stopInfoRefresh());
|
|
3084
|
+
}, INFO_REFRESH_MS);
|
|
3085
|
+
}
|
|
3086
|
+
function stopInfoRefresh() {
|
|
3087
|
+
if (infoTimer) clearInterval(infoTimer);
|
|
3088
|
+
infoTimer = null;
|
|
3089
|
+
}
|
|
2599
3090
|
let syncGen = 0;
|
|
2600
3091
|
let syncTimer = null;
|
|
2601
3092
|
let disposed = false;
|
|
@@ -2603,6 +3094,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
2603
3094
|
syncGen++;
|
|
2604
3095
|
if (syncTimer) clearTimeout(syncTimer);
|
|
2605
3096
|
syncTimer = null;
|
|
3097
|
+
stopInfoRefresh();
|
|
2606
3098
|
}
|
|
2607
3099
|
function syncWithRetry(onReady, initialDelay = 0) {
|
|
2608
3100
|
cancelSync();
|
|
@@ -3060,6 +3552,8 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
3060
3552
|
function floodReports(n) {
|
|
3061
3553
|
const out = [];
|
|
3062
3554
|
const components = Math.max(10, Math.floor(n / 10));
|
|
3555
|
+
const spread = 9e4;
|
|
3556
|
+
const t0 = Date.now() - spread;
|
|
3063
3557
|
for (let i = 0; i < n; i++) {
|
|
3064
3558
|
const id = i % components;
|
|
3065
3559
|
const depth = 1 + id % 6;
|
|
@@ -3067,6 +3561,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
3067
3561
|
for (let d = 1; d < depth; d++) path.push(`Section${(id * 7 + d) % 40}`);
|
|
3068
3562
|
const avoidable = id % 3 !== 0;
|
|
3069
3563
|
out.push({
|
|
3564
|
+
receivedAt: t0 + Math.round(i / Math.max(1, n - 1) * spread),
|
|
3070
3565
|
component: `Item${id}`,
|
|
3071
3566
|
path,
|
|
3072
3567
|
trigger: avoidable ? "parent" : "props",
|
|
@@ -3136,14 +3631,22 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
3136
3631
|
return transport;
|
|
3137
3632
|
}
|
|
3138
3633
|
function createRelayClientTransport(relayUrl, ES = EventSource) {
|
|
3139
|
-
const
|
|
3634
|
+
const [rawBase, relayQuery = ""] = relayUrl.split("?");
|
|
3635
|
+
const base = (rawBase ?? "").replace(/\/$/, "");
|
|
3636
|
+
const relayToken = new URLSearchParams(relayQuery).get("token");
|
|
3637
|
+
const auth = relayToken ? `token=${encodeURIComponent(relayToken)}` : "";
|
|
3140
3638
|
let listener = null;
|
|
3141
3639
|
let stream = null;
|
|
3142
3640
|
let appsOnline = null;
|
|
3641
|
+
let roster = [];
|
|
3642
|
+
let selected = null;
|
|
3143
3643
|
const emit = (m) => {
|
|
3144
3644
|
if (listener) listener(m);
|
|
3145
3645
|
};
|
|
3146
|
-
const post = (message) =>
|
|
3646
|
+
const post = (message) => {
|
|
3647
|
+
const body = selected && isRecord(message) && message.__rerenderLensCmd === true ? { ...message, app: selected } : message;
|
|
3648
|
+
return fetch(`${base}/message${auth ? `?${auth}` : ""}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }).then(() => void 0);
|
|
3649
|
+
};
|
|
3147
3650
|
const { bridge, reply } = commandBridge(post, 2e3);
|
|
3148
3651
|
const handleData = (data) => {
|
|
3149
3652
|
let parsed;
|
|
@@ -3155,12 +3658,21 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
3155
3658
|
for (const m of Array.isArray(parsed) ? parsed : [parsed]) {
|
|
3156
3659
|
if (!isRecord(m) || reply(m)) continue;
|
|
3157
3660
|
if (m.__rerenderLens === true && m.type === "relay") {
|
|
3158
|
-
const
|
|
3661
|
+
const p = isRecord(m.payload) ? m.payload : {};
|
|
3662
|
+
const apps = typeof p.apps === "number" ? p.apps : 0;
|
|
3663
|
+
roster = Array.isArray(p.list) ? p.list : [];
|
|
3159
3664
|
const wasOnline = appsOnline;
|
|
3665
|
+
const wasSelected = selected;
|
|
3160
3666
|
appsOnline = apps;
|
|
3161
|
-
if (
|
|
3667
|
+
if (!roster.length) selected = null;
|
|
3668
|
+
else if (!selected || !roster.some((a) => a.id === selected)) selected = roster[0].id;
|
|
3669
|
+
emit({ type: "apps", payload: { list: roster, selected } });
|
|
3670
|
+
if (apps > 0 && (wasOnline === null || wasOnline === 0 || selected !== wasSelected)) void attach();
|
|
3162
3671
|
else if (apps === 0) emit({ type: "disconnected" });
|
|
3163
|
-
} else if (m.__rerenderLens === true && typeof m.type === "string")
|
|
3672
|
+
} else if (m.__rerenderLens === true && typeof m.type === "string") {
|
|
3673
|
+
if (typeof m.app === "string" && selected && m.app !== selected) continue;
|
|
3674
|
+
emit({ type: m.type, version: typeof m.version === "number" ? m.version : void 0, payload: m.payload });
|
|
3675
|
+
}
|
|
3164
3676
|
}
|
|
3165
3677
|
};
|
|
3166
3678
|
async function attach() {
|
|
@@ -3177,17 +3689,23 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
3177
3689
|
const transport = {
|
|
3178
3690
|
origin: base,
|
|
3179
3691
|
tabLabel: `relay ${base.replace(/^https?:\/\//, "")}`,
|
|
3692
|
+
selectApp(id) {
|
|
3693
|
+
if (id === selected || !roster.some((a) => a.id === id)) return;
|
|
3694
|
+
selected = id;
|
|
3695
|
+
emit({ type: "apps", payload: { list: roster, selected } });
|
|
3696
|
+
emit({ type: "navigated" });
|
|
3697
|
+
void attach();
|
|
3698
|
+
},
|
|
3180
3699
|
subscribe(fn) {
|
|
3181
3700
|
listener = fn;
|
|
3182
3701
|
const open = () => {
|
|
3183
|
-
stream = new ES(`${base}/events?role=panel`);
|
|
3702
|
+
stream = new ES(`${base}/events?role=panel${auth ? `&${auth}` : ""}`);
|
|
3184
3703
|
stream.onmessage = (e) => handleData(e.data);
|
|
3185
3704
|
stream.onerror = () => {
|
|
3186
3705
|
emit({ type: "disconnected" });
|
|
3187
3706
|
};
|
|
3188
3707
|
};
|
|
3189
3708
|
open();
|
|
3190
|
-
void attach();
|
|
3191
3709
|
},
|
|
3192
3710
|
replay: () => void bridge("replay"),
|
|
3193
3711
|
clear: () => void bridge("clear"),
|
|
@@ -3272,7 +3790,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
3272
3790
|
createBroadcastTransport,
|
|
3273
3791
|
createRelayClientTransport,
|
|
3274
3792
|
sourceContext,
|
|
3275
|
-
analysis: { firstDifferentPath, diffLeaves, fixesFor, rankFixes, rootCauseOf, analyzeCommit, contextAttribution, cascadeTree, rootCauseSummary, summarizeSession, compareSessions }
|
|
3793
|
+
analysis: { firstDifferentPath, diffLeaves, fixesFor, rankFixes, rootCauseOf, analyzeCommit: analyzeCommit2, contextAttribution, cascadeTree, rootCauseSummary: rootCauseSummary2, summarizeSession, compareSessions }
|
|
3276
3794
|
};
|
|
3277
3795
|
window.RerenderLensPanel = api;
|
|
3278
3796
|
var hasChrome = typeof chrome !== "undefined" && !!chrome && !!chrome.runtime && !!chrome.runtime.id;
|