rerender-lens 0.3.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.
- package/CHANGELOG.md +118 -0
- package/README.md +192 -353
- package/dist/{budget-COBu7jBU.d.cts → budget-BnJlYwrV.d.ts} +20 -10
- package/dist/{budget-LkNjGtRc.d.ts → budget-CRswn-nC.d.cts} +20 -10
- package/dist/cli.cjs +248 -31
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +248 -31
- package/dist/cli.js.map +1 -1
- package/dist/{devtools-BHGUUo-p.d.ts → devtools-BYWat7nQ.d.ts} +17 -3
- package/dist/{devtools-DmhWiWcN.d.cts → devtools-DJi_6AfJ.d.cts} +17 -3
- package/dist/index.cjs +635 -109
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +131 -8
- package/dist/index.d.ts +131 -8
- package/dist/index.js +620 -110
- package/dist/index.js.map +1 -1
- package/dist/jest-setup.cjs +1673 -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 +1671 -0
- package/dist/jest-setup.js.map +1 -0
- package/dist/jest.cjs +1760 -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 +1751 -0
- package/dist/jest.js.map +1 -0
- package/dist/{notifiers-BjjGSHpp.d.ts → notifiers-DwKlvv4H.d.cts} +8 -4
- package/dist/{notifiers-BGQtWKfX.d.cts → notifiers-FNzKHs0a.d.ts} +8 -4
- package/dist/playwright.cjs +235 -30
- package/dist/playwright.cjs.map +1 -1
- package/dist/playwright.d.cts +8 -6
- package/dist/playwright.d.ts +8 -6
- package/dist/playwright.js +235 -31
- package/dist/playwright.js.map +1 -1
- package/dist/relay.cjs +10 -2
- package/dist/relay.cjs.map +1 -1
- package/dist/relay.js +10 -2
- package/dist/relay.js.map +1 -1
- package/dist/rerender-lens.iife.js +1100 -337
- package/dist/runner-report-B8Sc3FZO.d.cts +46 -0
- package/dist/runner-report-CW8X5RwK.d.ts +46 -0
- package/dist/setup.cjs +308 -95
- package/dist/setup.cjs.map +1 -1
- package/dist/setup.js +308 -95
- package/dist/setup.js.map +1 -1
- package/dist/{types-BzUEVkxJ.d.cts → types-DW5-N2XH.d.cts} +55 -2
- package/dist/{types-BzUEVkxJ.d.ts → types-DW5-N2XH.d.ts} +55 -2
- package/dist/vite.d.cts +1 -1
- package/dist/vite.d.ts +1 -1
- package/dist/vitest-setup.cjs +452 -87
- 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 +452 -87
- package/dist/vitest-setup.js.map +1 -1
- package/dist/vitest.cjs +515 -123
- 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 +516 -124
- package/dist/vitest.js.map +1 -1
- package/package.json +41 -5
- package/panel/panel.css +1 -34
- package/panel/panel.js +865 -713
package/dist/jest.js
ADDED
|
@@ -0,0 +1,1751 @@
|
|
|
1
|
+
import { existsSync, readFileSync, appendFileSync, mkdirSync, writeFileSync, rmSync } from 'fs';
|
|
2
|
+
import { tmpdir } from 'os';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
|
|
5
|
+
// src/runner-report.ts
|
|
6
|
+
|
|
7
|
+
// src/diff.ts
|
|
8
|
+
var REACT_ELEMENT = /* @__PURE__ */ Symbol.for("react.element");
|
|
9
|
+
var REACT_TRANSITIONAL_ELEMENT = /* @__PURE__ */ Symbol.for("react.transitional.element");
|
|
10
|
+
function isReactElement(v) {
|
|
11
|
+
if (typeof v !== "object" || v === null) return false;
|
|
12
|
+
const t = v.$$typeof;
|
|
13
|
+
return t === REACT_ELEMENT || t === REACT_TRANSITIONAL_ELEMENT;
|
|
14
|
+
}
|
|
15
|
+
function isPlainObject(v) {
|
|
16
|
+
if (typeof v !== "object" || v === null) return false;
|
|
17
|
+
const proto = Object.getPrototypeOf(v);
|
|
18
|
+
return proto === Object.prototype || proto === null;
|
|
19
|
+
}
|
|
20
|
+
function remember(seen, a, b) {
|
|
21
|
+
let set = seen.get(a);
|
|
22
|
+
if (!set) {
|
|
23
|
+
set = /* @__PURE__ */ new Set();
|
|
24
|
+
seen.set(a, set);
|
|
25
|
+
}
|
|
26
|
+
if (set.has(b)) return true;
|
|
27
|
+
set.add(b);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
var scope = null;
|
|
31
|
+
var DEFAULT_DIFF_BUDGET = 5e4;
|
|
32
|
+
function beginDiffScope(budget = 2e5) {
|
|
33
|
+
const previous = scope;
|
|
34
|
+
scope = { cache: /* @__PURE__ */ new WeakMap(), remaining: budget, exhausted: false };
|
|
35
|
+
return () => {
|
|
36
|
+
scope = previous;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function diffBudgetExhausted() {
|
|
40
|
+
return scope?.exhausted === true;
|
|
41
|
+
}
|
|
42
|
+
function deepEqual(a, b, seen = /* @__PURE__ */ new Map()) {
|
|
43
|
+
if (Object.is(a, b)) return true;
|
|
44
|
+
if (typeof a !== typeof b) return false;
|
|
45
|
+
if (typeof a !== "object" || a === null || b === null) return false;
|
|
46
|
+
if (!scope) {
|
|
47
|
+
const end = beginDiffScope(DEFAULT_DIFF_BUDGET);
|
|
48
|
+
try {
|
|
49
|
+
return deepEqual(a, b, seen);
|
|
50
|
+
} finally {
|
|
51
|
+
end();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const sc = scope;
|
|
55
|
+
const objA = a;
|
|
56
|
+
const objB = b;
|
|
57
|
+
const hit = sc.cache.get(objA)?.get(objB);
|
|
58
|
+
if (hit !== void 0) return hit;
|
|
59
|
+
if (--sc.remaining < 0) {
|
|
60
|
+
sc.exhausted = true;
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
const result = deepEqualObjects(objA, objB, seen);
|
|
64
|
+
let m = sc.cache.get(objA);
|
|
65
|
+
if (!m) {
|
|
66
|
+
m = /* @__PURE__ */ new WeakMap();
|
|
67
|
+
sc.cache.set(objA, m);
|
|
68
|
+
}
|
|
69
|
+
m.set(objB, result);
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
function deepEqualObjects(a, b, seen) {
|
|
73
|
+
const objA = a;
|
|
74
|
+
const objB = b;
|
|
75
|
+
if (remember(seen, objA, objB)) return true;
|
|
76
|
+
if (a instanceof Date) return b instanceof Date && a.getTime() === b.getTime();
|
|
77
|
+
if (a instanceof RegExp) return b instanceof RegExp && a.source === b.source && a.flags === b.flags;
|
|
78
|
+
if (Array.isArray(a)) {
|
|
79
|
+
if (!Array.isArray(b) || a.length !== b.length) return false;
|
|
80
|
+
for (let i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i], seen)) return false;
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
if (Array.isArray(b)) return false;
|
|
84
|
+
if (ArrayBuffer.isView(a)) {
|
|
85
|
+
if (!ArrayBuffer.isView(b) || a.constructor !== b.constructor) return false;
|
|
86
|
+
const ta = a;
|
|
87
|
+
const tb = b;
|
|
88
|
+
if (ta.length !== tb.length) return false;
|
|
89
|
+
for (let i = 0; i < ta.length; i++) if (ta[i] !== tb[i]) return false;
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
if (a instanceof Map) {
|
|
93
|
+
if (!(b instanceof Map) || a.size !== b.size) return false;
|
|
94
|
+
for (const [k, v] of a) {
|
|
95
|
+
if (!b.has(k) || !deepEqual(v, b.get(k), seen)) return false;
|
|
96
|
+
}
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
if (a instanceof Set) {
|
|
100
|
+
if (!(b instanceof Set) || a.size !== b.size) return false;
|
|
101
|
+
if (a.size > 50) {
|
|
102
|
+
for (const v of a) if (!b.has(v)) return false;
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
outer: for (const v of a) {
|
|
106
|
+
if (b.has(v)) continue;
|
|
107
|
+
for (const w of b) if (deepEqual(v, w, seen)) continue outer;
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
if (isReactElement(a)) {
|
|
113
|
+
if (!isReactElement(b)) return false;
|
|
114
|
+
return a.type === b.type && a.key === b.key && deepEqual(a.props, b.props, seen);
|
|
115
|
+
}
|
|
116
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
117
|
+
const ka = Object.keys(a);
|
|
118
|
+
const kb = Object.keys(b);
|
|
119
|
+
if (ka.length !== kb.length) return false;
|
|
120
|
+
const rb = b;
|
|
121
|
+
for (const k of ka) {
|
|
122
|
+
if (!Object.prototype.hasOwnProperty.call(b, k)) return false;
|
|
123
|
+
if (!deepEqual(a[k], rb[k], seen)) return false;
|
|
124
|
+
}
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
var AVOIDABLE_KINDS = /* @__PURE__ */ new Set(["deep-equal", "function", "element"]);
|
|
130
|
+
function classify(prev, next) {
|
|
131
|
+
if (typeof prev === "function" && typeof next === "function") {
|
|
132
|
+
return prev.name === next.name && prev.toString() === next.toString() ? "function" : "different";
|
|
133
|
+
}
|
|
134
|
+
if (isReactElement(prev) && isReactElement(next)) {
|
|
135
|
+
return deepEqual(prev, next) ? "element" : "different";
|
|
136
|
+
}
|
|
137
|
+
return deepEqual(prev, next) ? "deep-equal" : "different";
|
|
138
|
+
}
|
|
139
|
+
function joinPath(base, key) {
|
|
140
|
+
if (typeof key === "number") return `${base}[${key}]`;
|
|
141
|
+
if (base === "") return key;
|
|
142
|
+
return /^[A-Za-z_$][\w$]*$/.test(key) ? `${base}.${key}` : `${base}[${JSON.stringify(key)}]`;
|
|
143
|
+
}
|
|
144
|
+
function diffRecords(prev, next, basePath = "") {
|
|
145
|
+
const p = prev ?? {};
|
|
146
|
+
const n = next ?? {};
|
|
147
|
+
const changes = [];
|
|
148
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(p), ...Object.keys(n)]);
|
|
149
|
+
for (const key of keys) {
|
|
150
|
+
const inPrev = Object.prototype.hasOwnProperty.call(p, key);
|
|
151
|
+
const inNext = Object.prototype.hasOwnProperty.call(n, key);
|
|
152
|
+
const path = joinPath(basePath, key);
|
|
153
|
+
if (inPrev && !inNext) {
|
|
154
|
+
changes.push({ path, kind: "removed", prev: p[key], next: void 0 });
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (!inPrev && inNext) {
|
|
158
|
+
changes.push({ path, kind: "added", prev: void 0, next: n[key] });
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const a = p[key];
|
|
162
|
+
const b = n[key];
|
|
163
|
+
if (Object.is(a, b)) continue;
|
|
164
|
+
const kind = classify(a, b);
|
|
165
|
+
if (kind === "different") {
|
|
166
|
+
changes.push({ path: diffBudgetExhausted() ? path : firstDifferentPath(a, b, path) ?? path, kind, prev: a, next: b });
|
|
167
|
+
} else {
|
|
168
|
+
changes.push({ path, kind, prev: a, next: b });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return changes;
|
|
172
|
+
}
|
|
173
|
+
function firstDifferentPath(a, b, path = "", depth = 0) {
|
|
174
|
+
if (depth > 8) return path || "(value)";
|
|
175
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
176
|
+
if (a.length !== b.length) return path ? `${path}.length` : "length";
|
|
177
|
+
for (let i = 0; i < a.length; i++) {
|
|
178
|
+
if (!deepEqual(a[i], b[i])) return firstDifferentPath(a[i], b[i], joinPath(path, i), depth + 1);
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
183
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
184
|
+
for (const k of keys) {
|
|
185
|
+
if (!deepEqual(a[k], b[k])) return firstDifferentPath(a[k], b[k], joinPath(path, k), depth + 1);
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
return deepEqual(a, b) ? null : path || "(value)";
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/report.ts
|
|
193
|
+
var now = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
194
|
+
var hookLabel = (c) => c.custom && c.custom.length ? `${c.custom.join(" \u203A ")} \u203A ${c.hook}#${c.index}` : `${c.hook} #${c.index}`;
|
|
195
|
+
function storeAdvice(custom) {
|
|
196
|
+
const chain = custom ?? [];
|
|
197
|
+
if (chain.some((n) => /^useSelector$/.test(n)))
|
|
198
|
+
return "the Redux selector returns a new object on every call: return a stored slice, pass shallowEqual as the equality function, or memoize it with createSelector";
|
|
199
|
+
if (chain.some((n) => /^useAppSelector$/.test(n)))
|
|
200
|
+
return "the selector returns a new object on every call: return a stored slice, pass shallowEqual, or memoize it with createSelector";
|
|
201
|
+
if (chain.some((n) => /^use[A-Z]\w*Store$/.test(n) || n === "useStore" || n === "useBoundStore"))
|
|
202
|
+
return "the store selector returns a new object on every call: select a primitive, or wrap the selector with useShallow (Zustand) / an equality function";
|
|
203
|
+
return "getSnapshot returns a new reference with the same contents: cache the snapshot in the store and return the same object while the data is unchanged";
|
|
204
|
+
}
|
|
205
|
+
var isGenuine = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
|
|
206
|
+
var isChildren = (change) => change.path === "children";
|
|
207
|
+
var isRefObject = (change) => change.path === "ref" && change.kind === "deep-equal";
|
|
208
|
+
function fixFor(change) {
|
|
209
|
+
if (isChildren(change) && (change.kind === "element" || change.kind === "deep-equal")) {
|
|
210
|
+
return `lift the children out of the parent's render: memoize them with useMemo, hoist static elements to module scope, or render them from a component that does not re-render`;
|
|
211
|
+
}
|
|
212
|
+
if (isRefObject(change)) return `create the ref once with useRef (or createRef outside the render) and pass the same object every time`;
|
|
213
|
+
switch (change.kind) {
|
|
214
|
+
case "function":
|
|
215
|
+
return `wrap it in useCallback (or hoist it out of the parent's render)`;
|
|
216
|
+
case "element":
|
|
217
|
+
return `memoize the element with useMemo, or pass it as children from a stable parent`;
|
|
218
|
+
case "deep-equal":
|
|
219
|
+
return Array.isArray(change.next) ? `memoize the array with useMemo (same items, new reference)` : `memoize the object with useMemo, or hoist it to module scope if it is constant`;
|
|
220
|
+
default:
|
|
221
|
+
return "";
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function describe(change) {
|
|
225
|
+
if (isChildren(change) && (change.kind === "element" || change.kind === "deep-equal")) {
|
|
226
|
+
return `children are new React elements with the same types and props on every render of the parent`;
|
|
227
|
+
}
|
|
228
|
+
if (isRefObject(change)) return `prop "ref" is a new ref object on every render (React re-attaches it each time)`;
|
|
229
|
+
switch (change.kind) {
|
|
230
|
+
case "deep-equal":
|
|
231
|
+
return `prop "${change.path}" is a new reference but deep-equal to the previous value`;
|
|
232
|
+
case "function":
|
|
233
|
+
return `prop "${change.path}" is a new function instance on every render`;
|
|
234
|
+
case "element":
|
|
235
|
+
return `prop "${change.path}" is a new React element with the same type and props`;
|
|
236
|
+
case "different":
|
|
237
|
+
return `prop "${change.path}" changed`;
|
|
238
|
+
case "added":
|
|
239
|
+
return `prop "${change.path}" was added`;
|
|
240
|
+
case "removed":
|
|
241
|
+
return `prop "${change.path}" was removed`;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function buildReport(input) {
|
|
245
|
+
const stateChanges = input.stateChanges ?? [];
|
|
246
|
+
const hookChanges = input.hookChanges ?? [];
|
|
247
|
+
const isStateHook = (h) => h.hook === "useState" || h.hook === "useReducer";
|
|
248
|
+
const genuineProps = input.propChanges.some(isGenuine);
|
|
249
|
+
const genuineState = stateChanges.some(isGenuine) || hookChanges.some((h) => isStateHook(h) && isGenuine(h));
|
|
250
|
+
const genuineHooks = hookChanges.some((h) => !isStateHook(h) && isGenuine(h));
|
|
251
|
+
const causes = [genuineProps && "props", genuineState && "state", genuineHooks && "hooks"].filter(Boolean);
|
|
252
|
+
let trigger;
|
|
253
|
+
if (causes.length === 0) trigger = "parent";
|
|
254
|
+
else if (causes.length === 1) trigger = causes[0];
|
|
255
|
+
else trigger = "mixed";
|
|
256
|
+
const nothingChanged = input.propChanges.length === 0 && stateChanges.length === 0 && hookChanges.length === 0;
|
|
257
|
+
const cacheHit = input.compiled === true && trigger === "parent" && input.propChanges.length === 0;
|
|
258
|
+
const revealed = input.revealed === true && trigger === "parent";
|
|
259
|
+
const avoidable = trigger === "parent" && !cacheHit && !revealed;
|
|
260
|
+
const reasons = [];
|
|
261
|
+
const memoFix = input.classComponent ? `Make "${input.component}" extend PureComponent (or implement shouldComponentUpdate)` : `Wrap "${input.component}" in React.memo (or extend PureComponent)`;
|
|
262
|
+
if (cacheHit) {
|
|
263
|
+
reasons.push(`compiled by React Compiler: its output is memoized and the inputs did not change, so this render is cheap; nothing to fix.`);
|
|
264
|
+
} else if (revealed) {
|
|
265
|
+
reasons.push(
|
|
266
|
+
`re-rendered because the Suspense boundary above it switched from its fallback to content: React re-renders the content it kept hidden while the fallback was shown. A one-off; nothing to fix.`
|
|
267
|
+
);
|
|
268
|
+
} else if (avoidable && nothingChanged) {
|
|
269
|
+
const who = input.parent ? `<${input.parent.name}> re-rendered (${describeTrigger(input.parent.trigger)})` : "its parent re-rendered";
|
|
270
|
+
reasons.push(`re-rendered with identical props because ${who}. ${memoFix}.`);
|
|
271
|
+
} else if (avoidable && input.parent) {
|
|
272
|
+
reasons.push(`caused by <${input.parent.name}> re-rendering (${describeTrigger(input.parent.trigger)}).`);
|
|
273
|
+
}
|
|
274
|
+
for (const c of input.propChanges) {
|
|
275
|
+
const fix = fixFor(c);
|
|
276
|
+
reasons.push(fix ? `${describe(c)}: ${fix}.` : `${describe(c)}.`);
|
|
277
|
+
}
|
|
278
|
+
const memoized = input.memoized !== false;
|
|
279
|
+
if (avoidable && !memoized && !input.compiled && input.propChanges.length > 0) {
|
|
280
|
+
reasons.push(
|
|
281
|
+
`"${input.component}" is not memoized, so fixing the props alone will not stop this re-render: also ${input.classComponent ? "extend PureComponent (or implement shouldComponentUpdate)" : "wrap it in React.memo (or extend PureComponent)"}.`
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
for (const c of stateChanges) {
|
|
285
|
+
if (isGenuine(c)) reasons.push(`state "${c.path}" changed.`);
|
|
286
|
+
else reasons.push(`setState was called with a value deep-equal to the current "${c.path}" (new reference, same contents).`);
|
|
287
|
+
}
|
|
288
|
+
for (const c of hookChanges) {
|
|
289
|
+
if (c.hook === "useContext" && isGenuine(c)) {
|
|
290
|
+
const where = c.provider && c.provider.component ? ` (provided by <${c.provider.component}>)` : "";
|
|
291
|
+
const keys = c.changedKeys && typeof c.totalKeys === "number" && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys ? `: only ${c.changedKeys.map((k) => `"${k}"`).join(", ")} of ${c.totalKeys} keys changed, yet every consumer re-renders. Split the context or memoize the slices consumers read` : "";
|
|
292
|
+
reasons.push(`${c.path} changed${where}${keys}.`);
|
|
293
|
+
} else if (c.hook === "useDeferredValue" && isGenuine(c)) {
|
|
294
|
+
reasons.push(`${hookLabel(c)} caught up with its latest value: this is the deferred second render React schedules after the urgent one; nothing to fix.`);
|
|
295
|
+
} else if (isGenuine(c)) reasons.push(`${hookLabel(c)} changed.`);
|
|
296
|
+
else if (isStateHook(c))
|
|
297
|
+
reasons.push(`${hookLabel(c)} was set to a value deep-equal to the current one (new reference, same contents): reuse the existing object or bail out before calling the setter.`);
|
|
298
|
+
else if (c.hook === "useSyncExternalStore") reasons.push(`${hookLabel(c)} returned a new reference that is deep-equal to the previous value: ${storeAdvice(c.custom)}.`);
|
|
299
|
+
else reasons.push(`${hookLabel(c)} returned a new reference that is deep-equal to the previous value: memoize the context/store value where it is produced.`);
|
|
300
|
+
}
|
|
301
|
+
if (input.commitCause === "effect-after-commit") {
|
|
302
|
+
const who = input.updaters && input.updaters.length ? input.updaters.map((u) => `<${u}>`).join(", ") : "a component that rendered in it";
|
|
303
|
+
reasons.push(`this commit was scheduled right after commit #${input.afterCommit ?? "?"} by ${who}: an effect there set state (effect \u2192 setState loop). Derive the value during render or compute it before setting state.`);
|
|
304
|
+
} else if (input.commitCause === "suspense-resolved") {
|
|
305
|
+
reasons.push("this commit shows content that was suspended (a Suspense boundary resolved).");
|
|
306
|
+
}
|
|
307
|
+
const report = {
|
|
308
|
+
component: input.component,
|
|
309
|
+
instanceId: input.instanceId ?? 0,
|
|
310
|
+
commitId: input.commitId ?? 0,
|
|
311
|
+
renderCount: input.renderCount,
|
|
312
|
+
trigger,
|
|
313
|
+
avoidable,
|
|
314
|
+
memoized,
|
|
315
|
+
props: { prev: input.prevProps, next: input.nextProps },
|
|
316
|
+
propChanges: input.propChanges,
|
|
317
|
+
stateChanges,
|
|
318
|
+
hookChanges,
|
|
319
|
+
parent: input.parent ?? null,
|
|
320
|
+
owner: input.owner ?? null,
|
|
321
|
+
path: input.path ?? [],
|
|
322
|
+
reasons,
|
|
323
|
+
time: now()
|
|
324
|
+
};
|
|
325
|
+
if (input.compiled) report.compiled = true;
|
|
326
|
+
if (input.selfDuration !== void 0) report.selfDuration = input.selfDuration;
|
|
327
|
+
if (input.treeDuration !== void 0) report.treeDuration = input.treeDuration;
|
|
328
|
+
if (input.commitPriority) report.commitPriority = input.commitPriority;
|
|
329
|
+
if (input.source) report.source = input.source;
|
|
330
|
+
if (input.hookState) report.hookState = input.hookState;
|
|
331
|
+
if (input.contexts) report.contexts = input.contexts;
|
|
332
|
+
if (input.state) report.state = input.state;
|
|
333
|
+
if (input.updaters && input.updaters.length) report.updaters = input.updaters;
|
|
334
|
+
if (input.commitCause) report.commitCause = input.commitCause;
|
|
335
|
+
if (input.afterCommit !== void 0) report.afterCommit = input.afterCommit;
|
|
336
|
+
if (input.key !== void 0) report.key = input.key;
|
|
337
|
+
return report;
|
|
338
|
+
}
|
|
339
|
+
function describeTrigger(t) {
|
|
340
|
+
switch (t) {
|
|
341
|
+
case "props":
|
|
342
|
+
return "its props changed";
|
|
343
|
+
case "state":
|
|
344
|
+
return "its state changed";
|
|
345
|
+
case "hooks":
|
|
346
|
+
return "a context or store it reads changed";
|
|
347
|
+
case "mixed":
|
|
348
|
+
return "its props and state changed";
|
|
349
|
+
default:
|
|
350
|
+
return "its own parent re-rendered";
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
var KIND_LABEL = {
|
|
354
|
+
"deep-equal": "equal by value",
|
|
355
|
+
function: "new function",
|
|
356
|
+
element: "equal element",
|
|
357
|
+
different: "changed",
|
|
358
|
+
added: "added",
|
|
359
|
+
removed: "removed"
|
|
360
|
+
};
|
|
361
|
+
function summarize(report) {
|
|
362
|
+
const counts2 = /* @__PURE__ */ new Map();
|
|
363
|
+
for (const c of [...report.propChanges || [], ...report.stateChanges || [], ...report.hookChanges || []]) {
|
|
364
|
+
counts2.set(c.kind, (counts2.get(c.kind) ?? 0) + 1);
|
|
365
|
+
}
|
|
366
|
+
const parts = [...counts2].map(([kind, n]) => `${n} ${KIND_LABEL[kind] || kind}`);
|
|
367
|
+
if (parts.length === 0) parts.push("no changes");
|
|
368
|
+
return parts.join(", ");
|
|
369
|
+
}
|
|
370
|
+
function printReport(report, options) {
|
|
371
|
+
const c = options.console ?? console;
|
|
372
|
+
const open = options.collapse === false ? c.group : c.groupCollapsed;
|
|
373
|
+
const verdict = report.avoidable ? "avoidable re-render" : `re-render (${report.trigger})`;
|
|
374
|
+
open.call(c, `[rerender-lens] <${report.component}> ${verdict}: ${summarize(report)}`);
|
|
375
|
+
for (const r of report.reasons) c.log(`- ${r}`);
|
|
376
|
+
if (report.path.length) c.log(`at ${[...report.path, report.component].join(" > ")}`);
|
|
377
|
+
for (const ch of [...report.propChanges, ...report.stateChanges, ...report.hookChanges]) {
|
|
378
|
+
c.log(`${ch.path} (${KIND_LABEL[ch.kind]})`, { prev: ch.prev, next: ch.next });
|
|
379
|
+
}
|
|
380
|
+
c.log("props", report.props);
|
|
381
|
+
if (report.hookState && report.hookState.length) c.log("hooks", Object.fromEntries(report.hookState.map((h) => [h.path, h.value])));
|
|
382
|
+
if (report.contexts && report.contexts.length) c.log("contexts", Object.fromEntries(report.contexts.map((x) => [x.name, x.value])));
|
|
383
|
+
if (report.state) c.log("state", report.state);
|
|
384
|
+
c.groupEnd();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// src/state.ts
|
|
388
|
+
var KEY = /* @__PURE__ */ Symbol.for("rerender-lens.state");
|
|
389
|
+
function getState() {
|
|
390
|
+
const g = globalThis;
|
|
391
|
+
let s = g[KEY];
|
|
392
|
+
if (!s) {
|
|
393
|
+
s = { options: {}, enabled: false, printed: /* @__PURE__ */ new Map(), detach: null, nextInstanceId: 1, nextCommitId: 1, warnedOnce: /* @__PURE__ */ new Set(), scheduled: 0, commits: 0, overheadMs: 0, maxCommitMs: 0, truncated: 0 };
|
|
394
|
+
g[KEY] = s;
|
|
395
|
+
}
|
|
396
|
+
return s;
|
|
397
|
+
}
|
|
398
|
+
function warnOnce(key, message) {
|
|
399
|
+
const s = getState();
|
|
400
|
+
if (s.warnedOnce.has(key)) return;
|
|
401
|
+
s.warnedOnce.add(key);
|
|
402
|
+
(s.options.console ?? console).warn(`[rerender-lens] ${message}`);
|
|
403
|
+
}
|
|
404
|
+
function dispatch(report, override) {
|
|
405
|
+
const s = getState();
|
|
406
|
+
const options = s.options;
|
|
407
|
+
if (!options.silent && (report.avoidable || options.logAll)) {
|
|
408
|
+
const max = options.maxReportsPerComponent ?? 0;
|
|
409
|
+
const n = (s.printed.get(report.component) ?? 0) + 1;
|
|
410
|
+
s.printed.set(report.component, n);
|
|
411
|
+
if (max <= 0 || n <= max) {
|
|
412
|
+
printReport(report, options);
|
|
413
|
+
} else if (n === max + 1) {
|
|
414
|
+
(options.console ?? console).log(
|
|
415
|
+
`[rerender-lens] <${report.component}> reached maxReportsPerComponent (${max}); further reports are not printed.`
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (options.notifier) {
|
|
420
|
+
try {
|
|
421
|
+
options.notifier(report);
|
|
422
|
+
} catch (err) {
|
|
423
|
+
warnOnce("notifier", `notifier threw: ${String(err)}`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// src/types.ts
|
|
429
|
+
var MARKER = "rerenderLens";
|
|
430
|
+
|
|
431
|
+
// src/tracker.ts
|
|
432
|
+
var MEMO = /* @__PURE__ */ Symbol.for("react.memo");
|
|
433
|
+
var FORWARD_REF = /* @__PURE__ */ Symbol.for("react.forward_ref");
|
|
434
|
+
var isMemo = (t) => typeof t === "object" && t !== null && t.$$typeof === MEMO;
|
|
435
|
+
var isForwardRef = (t) => typeof t === "object" && t !== null && t.$$typeof === FORWARD_REF;
|
|
436
|
+
var isClass = (t) => typeof t === "function" && !!t.prototype?.isReactComponent;
|
|
437
|
+
var isPureClass = (t) => isClass(t) && !!t.prototype.isPureReactComponent;
|
|
438
|
+
var isComponentLike = (t) => typeof t === "function" || isMemo(t) || isForwardRef(t);
|
|
439
|
+
function getDisplayName(type) {
|
|
440
|
+
if (typeof type === "string") return type;
|
|
441
|
+
if (isMemo(type)) return type.displayName ?? getDisplayName(type.type);
|
|
442
|
+
if (isForwardRef(type)) return type.displayName ?? getDisplayName(type.render);
|
|
443
|
+
if (typeof type === "function") {
|
|
444
|
+
const t = type;
|
|
445
|
+
return t.displayName ?? (t.name || "Anonymous");
|
|
446
|
+
}
|
|
447
|
+
return "Anonymous";
|
|
448
|
+
}
|
|
449
|
+
function hasMarker(type) {
|
|
450
|
+
if (type === null || typeof type !== "function" && typeof type !== "object") return false;
|
|
451
|
+
if (type[MARKER] === true) return true;
|
|
452
|
+
if (isMemo(type)) return hasMarker(type.type);
|
|
453
|
+
if (isForwardRef(type)) return hasMarker(type.render);
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
function matches(m, name) {
|
|
457
|
+
if (typeof m === "string") return m === name;
|
|
458
|
+
if (m instanceof RegExp) return m.test(name);
|
|
459
|
+
return m(name);
|
|
460
|
+
}
|
|
461
|
+
function shouldTrack(type, o) {
|
|
462
|
+
if (!isComponentLike(type)) return false;
|
|
463
|
+
const name = getDisplayName(type);
|
|
464
|
+
if (o.exclude?.some((m) => matches(m, name))) return false;
|
|
465
|
+
if (hasMarker(type)) return true;
|
|
466
|
+
if (o.include?.some((m) => matches(m, name))) return true;
|
|
467
|
+
if (o.trackAllComponents) return true;
|
|
468
|
+
if (o.trackAllMemoized) return isMemo(type) || isPureClass(type);
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
function init(options = {}) {
|
|
472
|
+
const s = getState();
|
|
473
|
+
s.options = { ...options };
|
|
474
|
+
s.printed.clear();
|
|
475
|
+
if (!s.enabled) {
|
|
476
|
+
s.detach = attach();
|
|
477
|
+
s.enabled = true;
|
|
478
|
+
}
|
|
479
|
+
return disable;
|
|
480
|
+
}
|
|
481
|
+
function disable() {
|
|
482
|
+
const s = getState();
|
|
483
|
+
if (!s.enabled) return;
|
|
484
|
+
s.detach?.();
|
|
485
|
+
s.detach = null;
|
|
486
|
+
s.enabled = false;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// src/hookNames.ts
|
|
490
|
+
var NODE_COUNT = {
|
|
491
|
+
useState: 1,
|
|
492
|
+
useReducer: 1,
|
|
493
|
+
useRef: 1,
|
|
494
|
+
useMemo: 1,
|
|
495
|
+
useCallback: 1,
|
|
496
|
+
useEffect: 1,
|
|
497
|
+
useLayoutEffect: 1,
|
|
498
|
+
useInsertionEffect: 1,
|
|
499
|
+
useImperativeHandle: 1,
|
|
500
|
+
useSyncExternalStore: 2,
|
|
501
|
+
useTransition: 2,
|
|
502
|
+
useDeferredValue: 1,
|
|
503
|
+
useId: 1,
|
|
504
|
+
useOptimistic: 1,
|
|
505
|
+
useActionState: 3,
|
|
506
|
+
useFormState: 3,
|
|
507
|
+
useEffectEvent: 1,
|
|
508
|
+
useCacheRefresh: 1
|
|
509
|
+
};
|
|
510
|
+
var BUILTIN_HOOKS = /* @__PURE__ */ new Set([...Object.keys(NODE_COUNT), "useContext", "useDebugValue", "use"]);
|
|
511
|
+
var noop = () => {
|
|
512
|
+
};
|
|
513
|
+
function customHooksFromStack(stack, componentName, marker) {
|
|
514
|
+
const names = [];
|
|
515
|
+
let past = false;
|
|
516
|
+
for (const line of stack.split("\n")) {
|
|
517
|
+
const m = /^\s*at\s+(?:async\s+)?([^\s(]+)|^([^@\s]+)@/.exec(line);
|
|
518
|
+
const name = m ? (m[1] || m[2] || "").replace(/^Object\./, "").replace(/^Proxy\./, "") : "";
|
|
519
|
+
if (!name) continue;
|
|
520
|
+
if (!past) {
|
|
521
|
+
if (name.includes(marker)) past = true;
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
if (name === componentName || name.endsWith("." + componentName)) break;
|
|
525
|
+
if (/^use[A-Z0-9_]/.test(name) && !BUILTIN_HOOKS.has(name)) names.push(name);
|
|
526
|
+
if (names.length > 8) break;
|
|
527
|
+
}
|
|
528
|
+
return names;
|
|
529
|
+
}
|
|
530
|
+
var cache = /* @__PURE__ */ new WeakMap();
|
|
531
|
+
function makeDispatcher(fiber, componentName, out, marker) {
|
|
532
|
+
let node = fiber.memoizedState;
|
|
533
|
+
let index = 0;
|
|
534
|
+
const take = (count) => {
|
|
535
|
+
const first = node;
|
|
536
|
+
for (let i = 0; i < count && node; i++) node = node.next;
|
|
537
|
+
index += count;
|
|
538
|
+
return first;
|
|
539
|
+
};
|
|
540
|
+
const record = (primitive, count) => {
|
|
541
|
+
const stack = new Error().stack || "";
|
|
542
|
+
const chain = customHooksFromStack(stack, componentName, marker);
|
|
543
|
+
const start = index;
|
|
544
|
+
const first = take(count);
|
|
545
|
+
if (chain.length) for (let i = 0; i < count; i++) out.set(start + i, chain);
|
|
546
|
+
return first;
|
|
547
|
+
};
|
|
548
|
+
const memoized = (n) => n ? n.memoizedState : void 0;
|
|
549
|
+
const dispatcher = {};
|
|
550
|
+
const define = (name, impl) => {
|
|
551
|
+
const fn = { [marker + name]: (...a) => impl(...a) }[marker + name];
|
|
552
|
+
dispatcher[name] = fn;
|
|
553
|
+
};
|
|
554
|
+
define("useState", () => {
|
|
555
|
+
const n = record("useState", 1);
|
|
556
|
+
return [memoized(n), noop];
|
|
557
|
+
});
|
|
558
|
+
define("useReducer", () => {
|
|
559
|
+
const n = record("useReducer", 1);
|
|
560
|
+
return [memoized(n), noop];
|
|
561
|
+
});
|
|
562
|
+
define("useRef", (init2) => {
|
|
563
|
+
const n = record("useRef", 1);
|
|
564
|
+
return n ? n.memoizedState : { current: init2 };
|
|
565
|
+
});
|
|
566
|
+
define("useMemo", (fn) => {
|
|
567
|
+
const n = record("useMemo", 1);
|
|
568
|
+
const s = memoized(n);
|
|
569
|
+
return Array.isArray(s) ? s[0] : typeof fn === "function" ? fn() : void 0;
|
|
570
|
+
});
|
|
571
|
+
define("useCallback", (fn) => {
|
|
572
|
+
const n = record("useCallback", 1);
|
|
573
|
+
const s = memoized(n);
|
|
574
|
+
return Array.isArray(s) ? s[0] : fn;
|
|
575
|
+
});
|
|
576
|
+
for (const effect of ["useEffect", "useLayoutEffect", "useInsertionEffect", "useImperativeHandle"]) define(effect, () => void record(effect, 1));
|
|
577
|
+
define("useContext", (ctx) => {
|
|
578
|
+
record("useContext", 0);
|
|
579
|
+
const c = ctx;
|
|
580
|
+
return c ? c._currentValue : void 0;
|
|
581
|
+
});
|
|
582
|
+
define("useDebugValue", () => void record("useDebugValue", 0));
|
|
583
|
+
define("useSyncExternalStore", (_subscribe, getSnapshot) => {
|
|
584
|
+
const n = record("useSyncExternalStore", 2);
|
|
585
|
+
return n ? n.memoizedState : typeof getSnapshot === "function" ? getSnapshot() : void 0;
|
|
586
|
+
});
|
|
587
|
+
define("useTransition", () => {
|
|
588
|
+
record("useTransition", 2);
|
|
589
|
+
return [false, noop];
|
|
590
|
+
});
|
|
591
|
+
define("useDeferredValue", (v) => {
|
|
592
|
+
const n = record("useDeferredValue", 1);
|
|
593
|
+
return n ? n.memoizedState : v;
|
|
594
|
+
});
|
|
595
|
+
define("useId", () => {
|
|
596
|
+
const n = record("useId", 1);
|
|
597
|
+
return n ? n.memoizedState : "id";
|
|
598
|
+
});
|
|
599
|
+
define("useOptimistic", (v) => {
|
|
600
|
+
const n = record("useOptimistic", 1);
|
|
601
|
+
return [n ? n.memoizedState : v, noop];
|
|
602
|
+
});
|
|
603
|
+
for (const action of ["useActionState", "useFormState"]) {
|
|
604
|
+
define(action, (_fn, init2) => {
|
|
605
|
+
const n = record(action, 3);
|
|
606
|
+
return [n ? n.memoizedState : init2, noop, false];
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
define("useEffectEvent", (fn) => {
|
|
610
|
+
record("useEffectEvent", 1);
|
|
611
|
+
return fn;
|
|
612
|
+
});
|
|
613
|
+
define("useCacheRefresh", () => {
|
|
614
|
+
record("useCacheRefresh", 1);
|
|
615
|
+
return noop;
|
|
616
|
+
});
|
|
617
|
+
define("useHostTransitionStatus", () => ({ pending: false, data: null, method: null, action: null }));
|
|
618
|
+
define("useMemoCache", (size) => new Array(Number(size) || 0).fill(/* @__PURE__ */ Symbol.for("react.memo_cache_sentinel")));
|
|
619
|
+
define("use", (usable) => {
|
|
620
|
+
record("use", 0);
|
|
621
|
+
const u = usable;
|
|
622
|
+
if (u && typeof u.then === "function") {
|
|
623
|
+
if (u.status === "fulfilled") return u.value;
|
|
624
|
+
throw new Error("rerender-lens: cannot replay use() on a pending promise");
|
|
625
|
+
}
|
|
626
|
+
return u ? u._currentValue : void 0;
|
|
627
|
+
});
|
|
628
|
+
define("readContext", (ctx) => ctx?._currentValue);
|
|
629
|
+
return { dispatcher, nodesConsumed: () => index };
|
|
630
|
+
}
|
|
631
|
+
function renderFunctionOf(type) {
|
|
632
|
+
let t = type;
|
|
633
|
+
while (t && typeof t === "object" && t.type) t = t.type;
|
|
634
|
+
if (t && typeof t === "object" && typeof t.render === "function") return { fn: t.render, name: t.render.name || t.displayName || "Component" };
|
|
635
|
+
if (typeof t === "function") return { fn: t, name: t.name || "Component" };
|
|
636
|
+
return { fn: null, name: "Component" };
|
|
637
|
+
}
|
|
638
|
+
function resolveHookNames(fiber, ref) {
|
|
639
|
+
const type = fiber.type;
|
|
640
|
+
if (!type || !ref) return /* @__PURE__ */ new Map();
|
|
641
|
+
const cached = cache.get(type);
|
|
642
|
+
if (cached) return cached.byNode;
|
|
643
|
+
const result = { byNode: /* @__PURE__ */ new Map(), ok: false };
|
|
644
|
+
cache.set(type, result);
|
|
645
|
+
const { fn, name } = renderFunctionOf(type);
|
|
646
|
+
if (!fn) return result.byNode;
|
|
647
|
+
const useH = "H" in ref;
|
|
648
|
+
const marker = "__rl_";
|
|
649
|
+
const previous = useH ? ref.H : ref.current;
|
|
650
|
+
const { dispatcher, nodesConsumed } = makeDispatcher(fiber, name, result.byNode, marker);
|
|
651
|
+
const proxy = new Proxy(dispatcher, {
|
|
652
|
+
get(target, key) {
|
|
653
|
+
if (key in target) return target[key];
|
|
654
|
+
return () => void 0;
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
let total = 0;
|
|
658
|
+
for (let n = fiber.memoizedState; n && typeof n === "object" && "next" in n; n = n.next) total++;
|
|
659
|
+
try {
|
|
660
|
+
if (useH) ref.H = proxy;
|
|
661
|
+
else ref.current = proxy;
|
|
662
|
+
const secondArg = fiber.tag === 11 ? fiber.stateNode?.ref ?? null : void 0;
|
|
663
|
+
fn(fiber.memoizedProps ?? {}, secondArg);
|
|
664
|
+
result.ok = nodesConsumed() === total;
|
|
665
|
+
if (!result.ok) result.byNode.clear();
|
|
666
|
+
} catch {
|
|
667
|
+
result.byNode.clear();
|
|
668
|
+
} finally {
|
|
669
|
+
if (useH) ref.H = previous;
|
|
670
|
+
else ref.current = previous;
|
|
671
|
+
}
|
|
672
|
+
return result.byNode;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// src/fiber.ts
|
|
676
|
+
var FunctionComponent = 0;
|
|
677
|
+
var ClassComponent = 1;
|
|
678
|
+
var HostRoot = 3;
|
|
679
|
+
var ContextProvider = 10;
|
|
680
|
+
var ForwardRef = 11;
|
|
681
|
+
var SuspenseComponent = 13;
|
|
682
|
+
var MemoComponent = 14;
|
|
683
|
+
var SimpleMemoComponent = 15;
|
|
684
|
+
var PerformedWork = 1;
|
|
685
|
+
var HOOK = "__REACT_DEVTOOLS_GLOBAL_HOOK__";
|
|
686
|
+
var nowMs = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
687
|
+
function ensureDevtoolsHook() {
|
|
688
|
+
const g = globalThis;
|
|
689
|
+
let hook = g[HOOK];
|
|
690
|
+
if (!hook) {
|
|
691
|
+
let nextId = 0;
|
|
692
|
+
const renderers = /* @__PURE__ */ new Map();
|
|
693
|
+
hook = {
|
|
694
|
+
renderers,
|
|
695
|
+
supportsFiber: true,
|
|
696
|
+
inject(renderer) {
|
|
697
|
+
const id = ++nextId;
|
|
698
|
+
renderers.set(id, renderer);
|
|
699
|
+
return id;
|
|
700
|
+
},
|
|
701
|
+
onCommitFiberRoot() {
|
|
702
|
+
},
|
|
703
|
+
onCommitFiberUnmount() {
|
|
704
|
+
},
|
|
705
|
+
onScheduleFiberRoot() {
|
|
706
|
+
},
|
|
707
|
+
onPostCommitFiberRoot() {
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
g[HOOK] = hook;
|
|
711
|
+
}
|
|
712
|
+
return hook;
|
|
713
|
+
}
|
|
714
|
+
var capturedRenderers = /* @__PURE__ */ new Map();
|
|
715
|
+
var dispatcherRefs = /* @__PURE__ */ new Map();
|
|
716
|
+
var INJECT_WRAPPED = /* @__PURE__ */ Symbol.for("rerender-lens.injectWrapped");
|
|
717
|
+
var ATTACHED = /* @__PURE__ */ Symbol.for("rerender-lens.attached");
|
|
718
|
+
function pickRenderer(r) {
|
|
719
|
+
const info = r ?? {};
|
|
720
|
+
return { version: info.version, bundleType: info.bundleType, rendererPackageName: info.rendererPackageName };
|
|
721
|
+
}
|
|
722
|
+
function wrapInject(hook) {
|
|
723
|
+
const h = hook;
|
|
724
|
+
if (h[INJECT_WRAPPED] || typeof hook.inject !== "function") return;
|
|
725
|
+
const original = hook.inject;
|
|
726
|
+
hook.inject = function(renderer) {
|
|
727
|
+
const id = original.call(this, renderer);
|
|
728
|
+
const key = typeof id === "number" ? id : capturedRenderers.size + 1;
|
|
729
|
+
capturedRenderers.set(key, pickRenderer(renderer));
|
|
730
|
+
const ref = renderer?.currentDispatcherRef;
|
|
731
|
+
if (ref && typeof ref === "object") dispatcherRefs.set(key, ref);
|
|
732
|
+
return id;
|
|
733
|
+
};
|
|
734
|
+
h[INJECT_WRAPPED] = true;
|
|
735
|
+
}
|
|
736
|
+
function getDispatcherRef() {
|
|
737
|
+
for (const ref of dispatcherRefs.values()) return ref;
|
|
738
|
+
const g = globalThis;
|
|
739
|
+
const hook = g[HOOK];
|
|
740
|
+
let found = null;
|
|
741
|
+
if (hook && hook.renderers && typeof hook.renderers.forEach === "function") {
|
|
742
|
+
hook.renderers.forEach((r) => {
|
|
743
|
+
const ref = r?.currentDispatcherRef;
|
|
744
|
+
if (!found && ref && typeof ref === "object") found = ref;
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
return found;
|
|
748
|
+
}
|
|
749
|
+
function attach() {
|
|
750
|
+
const hook = ensureDevtoolsHook();
|
|
751
|
+
wrapInject(hook);
|
|
752
|
+
if (hook[ATTACHED]) return () => {
|
|
753
|
+
};
|
|
754
|
+
hook[ATTACHED] = true;
|
|
755
|
+
const previous = hook.onCommitFiberRoot;
|
|
756
|
+
const patched = function(id, root, priority, didError) {
|
|
757
|
+
const started = nowMs();
|
|
758
|
+
try {
|
|
759
|
+
onCommit(root, priorityLabel(priority));
|
|
760
|
+
} catch (err) {
|
|
761
|
+
warnOnce("commit", `failed to inspect a commit: ${String(err)}`);
|
|
762
|
+
} finally {
|
|
763
|
+
const spent = nowMs() - started;
|
|
764
|
+
const s = getState();
|
|
765
|
+
s.overheadMs += spent;
|
|
766
|
+
if (spent > s.maxCommitMs) s.maxCommitMs = spent;
|
|
767
|
+
}
|
|
768
|
+
if (typeof previous === "function") return previous.call(this, id, root, priority, didError);
|
|
769
|
+
};
|
|
770
|
+
hook.onCommitFiberRoot = patched;
|
|
771
|
+
const previousSchedule = hook.onScheduleFiberRoot;
|
|
772
|
+
const patchedSchedule = function(id, root, children) {
|
|
773
|
+
getState().scheduled++;
|
|
774
|
+
if (typeof previousSchedule === "function") return previousSchedule.call(this, id, root, children);
|
|
775
|
+
};
|
|
776
|
+
hook.onScheduleFiberRoot = patchedSchedule;
|
|
777
|
+
return () => {
|
|
778
|
+
hook[ATTACHED] = false;
|
|
779
|
+
if (hook.onCommitFiberRoot === patched) hook.onCommitFiberRoot = previous;
|
|
780
|
+
if (hook.onScheduleFiberRoot === patchedSchedule) hook.onScheduleFiberRoot = previousSchedule;
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
function priorityLabel(priority) {
|
|
784
|
+
switch (priority) {
|
|
785
|
+
case 1:
|
|
786
|
+
return "immediate";
|
|
787
|
+
case 2:
|
|
788
|
+
return "user-blocking";
|
|
789
|
+
case 3:
|
|
790
|
+
return "normal";
|
|
791
|
+
case 4:
|
|
792
|
+
return "low";
|
|
793
|
+
case 5:
|
|
794
|
+
return "idle";
|
|
795
|
+
default:
|
|
796
|
+
return void 0;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
var flagsOf = (f) => f.flags ?? f.effectTag ?? 0;
|
|
800
|
+
var isComponentTag = (tag) => tag === FunctionComponent || tag === ClassComponent || tag === ForwardRef || tag === MemoComponent || tag === SimpleMemoComponent;
|
|
801
|
+
function isMemoizedFiber(fiber) {
|
|
802
|
+
if (fiber.tag === MemoComponent || fiber.tag === SimpleMemoComponent) return true;
|
|
803
|
+
if (fiber.tag === ClassComponent) {
|
|
804
|
+
const inst = fiber.stateNode;
|
|
805
|
+
return !!(inst && inst.isPureReactComponent);
|
|
806
|
+
}
|
|
807
|
+
return false;
|
|
808
|
+
}
|
|
809
|
+
function isCompiledFiber(fiber) {
|
|
810
|
+
if (fiber.tag === ClassComponent) return false;
|
|
811
|
+
const cache2 = fiber.updateQueue?.memoCache;
|
|
812
|
+
return !!cache2 && Array.isArray(cache2.data);
|
|
813
|
+
}
|
|
814
|
+
function durationsOf(fiber) {
|
|
815
|
+
if (typeof fiber.actualDuration !== "number") return null;
|
|
816
|
+
const tree = fiber.actualDuration;
|
|
817
|
+
let children = 0;
|
|
818
|
+
for (let c = fiber.child; c; c = c.sibling) if (typeof c.actualDuration === "number") children += c.actualDuration;
|
|
819
|
+
return { self: Math.max(0, tree - children), tree };
|
|
820
|
+
}
|
|
821
|
+
function fiberType(fiber) {
|
|
822
|
+
return fiber.elementType ?? fiber.type;
|
|
823
|
+
}
|
|
824
|
+
function fiberName(fiber) {
|
|
825
|
+
return getDisplayName(fiberType(fiber));
|
|
826
|
+
}
|
|
827
|
+
function didRender(fiber) {
|
|
828
|
+
return (flagsOf(fiber) & PerformedWork) !== 0;
|
|
829
|
+
}
|
|
830
|
+
function isHotSwapped(fiber, alt) {
|
|
831
|
+
return fiber.type !== alt.type;
|
|
832
|
+
}
|
|
833
|
+
var counts = /* @__PURE__ */ new WeakMap();
|
|
834
|
+
var ids = /* @__PURE__ */ new WeakMap();
|
|
835
|
+
var fibersById = /* @__PURE__ */ new Map();
|
|
836
|
+
var hasWeakRef = typeof WeakRef === "function";
|
|
837
|
+
var pruneAt = 5e3;
|
|
838
|
+
function remember2(id, fiber) {
|
|
839
|
+
if (!hasWeakRef) return;
|
|
840
|
+
if (fibersById.get(id)?.deref() === fiber) return;
|
|
841
|
+
fibersById.set(id, new WeakRef(fiber));
|
|
842
|
+
if (fibersById.size > pruneAt) {
|
|
843
|
+
for (const [k, ref] of fibersById) if (!ref.deref()) fibersById.delete(k);
|
|
844
|
+
pruneAt = Math.max(5e3, fibersById.size * 2);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
function instanceIdOf(fiber) {
|
|
848
|
+
return ids.get(fiber) ?? (fiber.alternate ? ids.get(fiber.alternate) : void 0);
|
|
849
|
+
}
|
|
850
|
+
function instanceId(fiber) {
|
|
851
|
+
const existing = instanceIdOf(fiber);
|
|
852
|
+
if (existing !== void 0) {
|
|
853
|
+
ids.set(fiber, existing);
|
|
854
|
+
remember2(existing, fiber);
|
|
855
|
+
return existing;
|
|
856
|
+
}
|
|
857
|
+
const s = getState();
|
|
858
|
+
const id = s.nextInstanceId++;
|
|
859
|
+
ids.set(fiber, id);
|
|
860
|
+
remember2(id, fiber);
|
|
861
|
+
return id;
|
|
862
|
+
}
|
|
863
|
+
function nearestComponent(fiber) {
|
|
864
|
+
let f = fiber;
|
|
865
|
+
while (f && f.tag !== HostRoot) {
|
|
866
|
+
if (isComponentTag(f.tag)) return f;
|
|
867
|
+
f = f.return;
|
|
868
|
+
}
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
871
|
+
var INTERNAL_FRAME = /react-dom|react_jsx|jsx-(dev-)?runtime|\/react\/|node_modules\/react|react-stack-bottom-frame|react_stack_bottom_frame|scheduler/;
|
|
872
|
+
function parseStackLocation(stack) {
|
|
873
|
+
for (const line of stack.split("\n", 24)) {
|
|
874
|
+
const m = /(?:at\s+(?:.*?\s+)?\(?|@)?((?:https?|file|webpack|vite|blob):[^\s()]+?):(\d+):(\d+)\)?\s*$/.exec(line.trim());
|
|
875
|
+
if (!m || !m[1] || INTERNAL_FRAME.test(m[1])) continue;
|
|
876
|
+
return { fileName: m[1], lineNumber: Number(m[2]), columnNumber: Number(m[3]) };
|
|
877
|
+
}
|
|
878
|
+
return void 0;
|
|
879
|
+
}
|
|
880
|
+
function sourceOf(fiber) {
|
|
881
|
+
const src = fiber._debugSource;
|
|
882
|
+
if (src && typeof src.fileName === "string") {
|
|
883
|
+
const out = { fileName: src.fileName };
|
|
884
|
+
if (typeof src.lineNumber === "number") out.lineNumber = src.lineNumber;
|
|
885
|
+
if (typeof src.columnNumber === "number") out.columnNumber = src.columnNumber;
|
|
886
|
+
return out;
|
|
887
|
+
}
|
|
888
|
+
const st = fiber._debugStack;
|
|
889
|
+
if (st && typeof st === "object") {
|
|
890
|
+
if (sourceCache.has(st)) return sourceCache.get(st);
|
|
891
|
+
const text = typeof st.stack === "string" ? st.stack : null;
|
|
892
|
+
const out = text ? parseStackLocation(text) : void 0;
|
|
893
|
+
sourceCache.set(st, out);
|
|
894
|
+
return out;
|
|
895
|
+
}
|
|
896
|
+
return typeof st === "string" ? parseStackLocation(st) : void 0;
|
|
897
|
+
}
|
|
898
|
+
var sourceCache = /* @__PURE__ */ new WeakMap();
|
|
899
|
+
function bumpCount(fiber) {
|
|
900
|
+
const prev = counts.get(fiber) ?? (fiber.alternate ? counts.get(fiber.alternate) : void 0) ?? 0;
|
|
901
|
+
const n = prev + 1;
|
|
902
|
+
counts.set(fiber, n);
|
|
903
|
+
return n;
|
|
904
|
+
}
|
|
905
|
+
function componentPath(fiber) {
|
|
906
|
+
const path = [];
|
|
907
|
+
let f = fiber.return;
|
|
908
|
+
while (f && f.tag !== HostRoot) {
|
|
909
|
+
if (isComponentTag(f.tag)) path.unshift(fiberName(f));
|
|
910
|
+
f = f.return;
|
|
911
|
+
}
|
|
912
|
+
return path;
|
|
913
|
+
}
|
|
914
|
+
function ownerName(fiber) {
|
|
915
|
+
const owner = fiber._debugOwner;
|
|
916
|
+
if (!owner) return null;
|
|
917
|
+
if (owner.type !== void 0) return getDisplayName(owner.type);
|
|
918
|
+
return typeof owner.name === "string" ? owner.name : null;
|
|
919
|
+
}
|
|
920
|
+
var isStateNode = (n) => !!n.queue && typeof n.queue.lastRenderedReducer === "function";
|
|
921
|
+
var isStoreNode = (n) => !!n.queue && typeof n.queue.getSnapshot === "function";
|
|
922
|
+
function hookLabel2(node) {
|
|
923
|
+
if (isStoreNode(node)) return "useSyncExternalStore";
|
|
924
|
+
if (isStateNode(node)) {
|
|
925
|
+
const r = node.queue.lastRenderedReducer;
|
|
926
|
+
return r.name === "basicStateReducer" ? "useState" : "useReducer";
|
|
927
|
+
}
|
|
928
|
+
return "state";
|
|
929
|
+
}
|
|
930
|
+
var NODELESS_HOOKS = /* @__PURE__ */ new Set(["useContext", "useDebugValue", "use"]);
|
|
931
|
+
function hookLabelsFor(fiber, first) {
|
|
932
|
+
const types = fiber._debugHookTypes?.filter((t) => !NODELESS_HOOKS.has(t));
|
|
933
|
+
let nodeCount = 0;
|
|
934
|
+
for (let n = first; n; n = n.next) nodeCount++;
|
|
935
|
+
return types && types.length === nodeCount ? types : null;
|
|
936
|
+
}
|
|
937
|
+
var isHookList = (v) => !!v && typeof v === "object" && "next" in v;
|
|
938
|
+
function snapshotHooks(fiber) {
|
|
939
|
+
const out = [];
|
|
940
|
+
if (fiber.tag === ClassComponent) return out;
|
|
941
|
+
const first = fiber.memoizedState;
|
|
942
|
+
if (!isHookList(first)) return out;
|
|
943
|
+
const labels = hookLabelsFor(fiber, first);
|
|
944
|
+
let i = 0;
|
|
945
|
+
for (let n = first; n; n = n.next, i++) {
|
|
946
|
+
if (!isStateNode(n) && !isStoreNode(n)) continue;
|
|
947
|
+
const hook = labels?.[i] ?? hookLabel2(n);
|
|
948
|
+
out.push({ path: `${hook}#${i}`, hook, index: i, value: n.memoizedState });
|
|
949
|
+
}
|
|
950
|
+
return out;
|
|
951
|
+
}
|
|
952
|
+
function snapshotContexts(fiber) {
|
|
953
|
+
const out = [];
|
|
954
|
+
for (let d = fiber.dependencies?.firstContext ?? null; d; d = d.next) out.push({ name: d.context.displayName ?? "Context", value: d.memoizedValue });
|
|
955
|
+
return out;
|
|
956
|
+
}
|
|
957
|
+
function diffHooks(fiber, alt) {
|
|
958
|
+
const out = [];
|
|
959
|
+
if (fiber.tag === ClassComponent) return out;
|
|
960
|
+
let a = alt.memoizedState;
|
|
961
|
+
let b = fiber.memoizedState;
|
|
962
|
+
if (!a || !b || typeof b !== "object" || !("next" in b)) return out;
|
|
963
|
+
const labels = hookLabelsFor(fiber, b);
|
|
964
|
+
let i = 0;
|
|
965
|
+
while (a && b) {
|
|
966
|
+
const deferred = labels?.[i] === "useDeferredValue";
|
|
967
|
+
if ((deferred || isStateNode(b) || isStoreNode(b)) && !Object.is(a.memoizedState, b.memoizedState)) {
|
|
968
|
+
const hook = labels?.[i] ?? hookLabel2(b);
|
|
969
|
+
out.push({
|
|
970
|
+
path: `${hook}#${i}`,
|
|
971
|
+
hook,
|
|
972
|
+
index: i,
|
|
973
|
+
kind: classify(a.memoizedState, b.memoizedState),
|
|
974
|
+
prev: a.memoizedState,
|
|
975
|
+
next: b.memoizedState
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
a = a.next;
|
|
979
|
+
b = b.next;
|
|
980
|
+
i++;
|
|
981
|
+
}
|
|
982
|
+
return out;
|
|
983
|
+
}
|
|
984
|
+
function findProvider(fiber, context) {
|
|
985
|
+
let f = fiber.return;
|
|
986
|
+
while (f && f.tag !== HostRoot) {
|
|
987
|
+
if (f.tag === ContextProvider) {
|
|
988
|
+
const t = f.type;
|
|
989
|
+
if (t === context || t && typeof t === "object" && t._context === context) return f;
|
|
990
|
+
}
|
|
991
|
+
f = f.return;
|
|
992
|
+
}
|
|
993
|
+
return null;
|
|
994
|
+
}
|
|
995
|
+
var isPlainObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v) && Object.getPrototypeOf(v) === Object.prototype;
|
|
996
|
+
function diffContexts(fiber, alt) {
|
|
997
|
+
const out = [];
|
|
998
|
+
let a = alt.dependencies?.firstContext ?? null;
|
|
999
|
+
let b = fiber.dependencies?.firstContext ?? null;
|
|
1000
|
+
let i = 0;
|
|
1001
|
+
if (b && !("memoizedValue" in b)) {
|
|
1002
|
+
warnOnce("contextValues", "this React does not record context values on the fiber (React < 18): context changes are reported as plain parent re-renders.");
|
|
1003
|
+
return out;
|
|
1004
|
+
}
|
|
1005
|
+
while (a && b) {
|
|
1006
|
+
if (a.context === b.context && !Object.is(a.memoizedValue, b.memoizedValue)) {
|
|
1007
|
+
const name = b.context.displayName ?? "Context";
|
|
1008
|
+
const change = {
|
|
1009
|
+
path: `useContext(${name})`,
|
|
1010
|
+
hook: "useContext",
|
|
1011
|
+
index: i,
|
|
1012
|
+
kind: classify(a.memoizedValue, b.memoizedValue),
|
|
1013
|
+
prev: a.memoizedValue,
|
|
1014
|
+
next: b.memoizedValue
|
|
1015
|
+
};
|
|
1016
|
+
const provider = findProvider(fiber, b.context);
|
|
1017
|
+
if (provider) {
|
|
1018
|
+
const path = componentPath(provider);
|
|
1019
|
+
change.provider = { component: path[path.length - 1] ?? null, path };
|
|
1020
|
+
}
|
|
1021
|
+
if (isPlainObject2(a.memoizedValue) && isPlainObject2(b.memoizedValue)) {
|
|
1022
|
+
const prev = a.memoizedValue;
|
|
1023
|
+
const next = b.memoizedValue;
|
|
1024
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(prev), ...Object.keys(next)]);
|
|
1025
|
+
change.changedKeys = [...keys].filter((k) => !Object.is(prev[k], next[k]));
|
|
1026
|
+
change.totalKeys = keys.size;
|
|
1027
|
+
}
|
|
1028
|
+
out.push(change);
|
|
1029
|
+
}
|
|
1030
|
+
a = a.next;
|
|
1031
|
+
b = b.next;
|
|
1032
|
+
i++;
|
|
1033
|
+
}
|
|
1034
|
+
return out;
|
|
1035
|
+
}
|
|
1036
|
+
var isGenuine2 = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
|
|
1037
|
+
var isRefObject2 = (v) => {
|
|
1038
|
+
if (typeof v !== "object" || v === null) return false;
|
|
1039
|
+
const keys = Object.keys(v);
|
|
1040
|
+
return keys.length === 1 && keys[0] === "current";
|
|
1041
|
+
};
|
|
1042
|
+
function diffProps(prev, next) {
|
|
1043
|
+
if (prev === next) return [];
|
|
1044
|
+
const pr = prev.ref;
|
|
1045
|
+
const nr = next.ref;
|
|
1046
|
+
if (pr !== nr && isRefObject2(pr) && isRefObject2(nr)) {
|
|
1047
|
+
const { ref: _p, ...restPrev } = prev;
|
|
1048
|
+
const { ref: _n, ...restNext } = next;
|
|
1049
|
+
return [{ path: "ref", kind: "deep-equal", prev: pr, next: nr }, ...diffRecords(restPrev, restNext)];
|
|
1050
|
+
}
|
|
1051
|
+
return diffRecords(prev, next);
|
|
1052
|
+
}
|
|
1053
|
+
function analyze(fiber, alt, trackHooks) {
|
|
1054
|
+
const prevProps = alt.memoizedProps ?? {};
|
|
1055
|
+
const nextProps = fiber.memoizedProps ?? {};
|
|
1056
|
+
const propChanges = diffProps(prevProps, nextProps);
|
|
1057
|
+
let stateChanges = [];
|
|
1058
|
+
let hookChanges = [];
|
|
1059
|
+
if (fiber.tag === ClassComponent) {
|
|
1060
|
+
if (alt.memoizedState !== fiber.memoizedState) {
|
|
1061
|
+
stateChanges = diffRecords(
|
|
1062
|
+
alt.memoizedState ?? {},
|
|
1063
|
+
fiber.memoizedState ?? {}
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
} else if (trackHooks) {
|
|
1067
|
+
hookChanges = [...diffHooks(fiber, alt), ...diffContexts(fiber, alt)];
|
|
1068
|
+
}
|
|
1069
|
+
const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore" && h.hook !== "useDeferredValue";
|
|
1070
|
+
const causes = [];
|
|
1071
|
+
if (propChanges.some(isGenuine2)) causes.push("props");
|
|
1072
|
+
if (stateChanges.some(isGenuine2) || hookChanges.some((h) => isStateHook(h) && isGenuine2(h))) causes.push("state");
|
|
1073
|
+
if (hookChanges.some((h) => !isStateHook(h) && isGenuine2(h))) causes.push("hooks");
|
|
1074
|
+
const trigger = causes.length === 0 ? "parent" : causes.length === 1 ? causes[0] : "mixed";
|
|
1075
|
+
return { propChanges, stateChanges, hookChanges, trigger };
|
|
1076
|
+
}
|
|
1077
|
+
function nearestRenderedAncestor(fiber, cache2, trackHooks) {
|
|
1078
|
+
let f = fiber.return;
|
|
1079
|
+
while (f && f.tag !== HostRoot) {
|
|
1080
|
+
if (isComponentTag(f.tag) && f.alternate && didRender(f)) {
|
|
1081
|
+
let info = cache2.get(f);
|
|
1082
|
+
if (info === void 0) {
|
|
1083
|
+
info = { name: fiberName(f), trigger: analyze(f, f.alternate, trackHooks).trigger };
|
|
1084
|
+
cache2.set(f, info);
|
|
1085
|
+
}
|
|
1086
|
+
return info;
|
|
1087
|
+
}
|
|
1088
|
+
f = f.return;
|
|
1089
|
+
}
|
|
1090
|
+
return null;
|
|
1091
|
+
}
|
|
1092
|
+
function updatersOf(root) {
|
|
1093
|
+
const set = root.memoizedUpdaters;
|
|
1094
|
+
if (!set || typeof set.forEach !== "function") return [];
|
|
1095
|
+
const names = [];
|
|
1096
|
+
set.forEach((f) => {
|
|
1097
|
+
const comp = nearestComponent(f);
|
|
1098
|
+
if (comp) {
|
|
1099
|
+
const name = fiberName(comp);
|
|
1100
|
+
if (!names.includes(name)) names.push(name);
|
|
1101
|
+
}
|
|
1102
|
+
});
|
|
1103
|
+
return names;
|
|
1104
|
+
}
|
|
1105
|
+
var lastCommit = null;
|
|
1106
|
+
var EFFECT_LOOP_WINDOW_MS = 50;
|
|
1107
|
+
var MAX_REPORTS_PER_COMMIT = 200;
|
|
1108
|
+
var COMMIT_TIME_BUDGET_MS = 25;
|
|
1109
|
+
function isDeferredCatchUp(fiber) {
|
|
1110
|
+
const types = fiber._debugHookTypes;
|
|
1111
|
+
if (!types || !types.includes("useDeferredValue") || !fiber.alternate) return false;
|
|
1112
|
+
const first = fiber.memoizedState;
|
|
1113
|
+
if (!isHookList(first)) return false;
|
|
1114
|
+
const labels = hookLabelsFor(fiber, first);
|
|
1115
|
+
if (!labels) return false;
|
|
1116
|
+
let a = fiber.alternate.memoizedState;
|
|
1117
|
+
let b = first;
|
|
1118
|
+
for (let i = 0; a && b; a = a.next, b = b.next, i++) {
|
|
1119
|
+
if (labels[i] === "useDeferredValue" && !Object.is(a.memoizedState, b.memoizedState)) return true;
|
|
1120
|
+
}
|
|
1121
|
+
return false;
|
|
1122
|
+
}
|
|
1123
|
+
function isUnder(fiber, boundaries) {
|
|
1124
|
+
for (let f = fiber.return; f && f.tag !== HostRoot; f = f.return) if (f.tag === SuspenseComponent && boundaries.includes(f)) return true;
|
|
1125
|
+
return false;
|
|
1126
|
+
}
|
|
1127
|
+
function onCommit(root, commitPriority) {
|
|
1128
|
+
const s = getState();
|
|
1129
|
+
if (!s.enabled) return;
|
|
1130
|
+
s.commits++;
|
|
1131
|
+
const o = s.options;
|
|
1132
|
+
const trackHooks = o.trackHooks !== false;
|
|
1133
|
+
const rendered = [];
|
|
1134
|
+
const renderedNames = /* @__PURE__ */ new Set();
|
|
1135
|
+
let hot = false;
|
|
1136
|
+
let resolvedBoundaries = null;
|
|
1137
|
+
const stack = [root.current];
|
|
1138
|
+
while (stack.length) {
|
|
1139
|
+
const fiber = stack.pop();
|
|
1140
|
+
const alt = fiber.alternate;
|
|
1141
|
+
if (alt && isComponentTag(fiber.tag) && didRender(fiber)) {
|
|
1142
|
+
if (isHotSwapped(fiber, alt)) hot = true;
|
|
1143
|
+
renderedNames.add(fiberName(fiber));
|
|
1144
|
+
if (shouldTrack(fiberType(fiber), o)) rendered.push(fiber);
|
|
1145
|
+
}
|
|
1146
|
+
if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null) (resolvedBoundaries ?? (resolvedBoundaries = [])).push(fiber);
|
|
1147
|
+
if (!alt || fiber.child !== alt.child) {
|
|
1148
|
+
for (let c = fiber.child; c; c = c.sibling) stack.push(c);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
if (hot && o.ignoreHotReload !== false) return;
|
|
1152
|
+
rendered.reverse();
|
|
1153
|
+
const at = nowMs();
|
|
1154
|
+
const updaters = updatersOf(root);
|
|
1155
|
+
let commitCause;
|
|
1156
|
+
let afterCommit;
|
|
1157
|
+
const inputDriven = commitPriority === "immediate" || commitPriority === "user-blocking";
|
|
1158
|
+
if (!inputDriven && lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u)) && !rendered.some(isDeferredCatchUp)) {
|
|
1159
|
+
commitCause = "effect-after-commit";
|
|
1160
|
+
afterCommit = lastCommit.id;
|
|
1161
|
+
} else if (resolvedBoundaries) commitCause = "suspense-resolved";
|
|
1162
|
+
if (rendered.length === 0) {
|
|
1163
|
+
lastCommit = { id: s.nextCommitId, at, rendered: renderedNames };
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
const commitId = s.nextCommitId++;
|
|
1167
|
+
lastCommit = { id: commitId, at, rendered: renderedNames };
|
|
1168
|
+
const dispatcherRef = o.resolveHookNames ? getDispatcherRef() : null;
|
|
1169
|
+
const parentCache = /* @__PURE__ */ new Map();
|
|
1170
|
+
const endDiffScope = beginDiffScope();
|
|
1171
|
+
const deadline = at + COMMIT_TIME_BUDGET_MS;
|
|
1172
|
+
try {
|
|
1173
|
+
for (let i = 0; i < rendered.length; i++) {
|
|
1174
|
+
if (i >= MAX_REPORTS_PER_COMMIT || (i & 15) === 15 && nowMs() > deadline) {
|
|
1175
|
+
s.truncated += rendered.length - i;
|
|
1176
|
+
warnOnce("truncated", `a commit rendered ${rendered.length} tracked components; only ${i} were reported (see info().truncated). Narrow \`include\` or turn off includeState.`);
|
|
1177
|
+
break;
|
|
1178
|
+
}
|
|
1179
|
+
const fiber = rendered[i];
|
|
1180
|
+
const alt = fiber.alternate;
|
|
1181
|
+
const a = analyze(fiber, alt, trackHooks);
|
|
1182
|
+
const durations = durationsOf(fiber);
|
|
1183
|
+
let hookState = o.includeState !== false && trackHooks ? snapshotHooks(fiber) : void 0;
|
|
1184
|
+
if (dispatcherRef && fiber.tag !== ClassComponent && (a.hookChanges.length || hookState && hookState.length)) {
|
|
1185
|
+
const names = resolveHookNames(fiber, dispatcherRef);
|
|
1186
|
+
if (names.size) {
|
|
1187
|
+
for (const c of a.hookChanges) if (c.hook !== "useContext" && names.has(c.index)) c.custom = names.get(c.index);
|
|
1188
|
+
if (hookState) hookState = hookState.map((h) => names.has(h.index) ? { ...h, custom: names.get(h.index) } : h);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
const report = buildReport({
|
|
1192
|
+
component: fiberName(fiber),
|
|
1193
|
+
instanceId: instanceId(fiber),
|
|
1194
|
+
renderCount: bumpCount(fiber),
|
|
1195
|
+
prevProps: alt.memoizedProps ?? {},
|
|
1196
|
+
nextProps: fiber.memoizedProps ?? {},
|
|
1197
|
+
propChanges: a.propChanges,
|
|
1198
|
+
stateChanges: a.stateChanges,
|
|
1199
|
+
hookChanges: a.hookChanges,
|
|
1200
|
+
...o.includeState !== false ? {
|
|
1201
|
+
hookState,
|
|
1202
|
+
contexts: trackHooks ? snapshotContexts(fiber) : void 0,
|
|
1203
|
+
state: fiber.tag === ClassComponent && fiber.memoizedState && typeof fiber.memoizedState === "object" ? fiber.memoizedState : void 0
|
|
1204
|
+
} : {},
|
|
1205
|
+
updaters,
|
|
1206
|
+
commitCause,
|
|
1207
|
+
afterCommit,
|
|
1208
|
+
key: fiber.key === null || fiber.key === void 0 ? null : String(fiber.key),
|
|
1209
|
+
parent: a.trigger === "parent" ? nearestRenderedAncestor(fiber, parentCache, trackHooks) : null,
|
|
1210
|
+
owner: ownerName(fiber),
|
|
1211
|
+
path: componentPath(fiber),
|
|
1212
|
+
memoized: isMemoizedFiber(fiber),
|
|
1213
|
+
compiled: isCompiledFiber(fiber),
|
|
1214
|
+
classComponent: fiber.tag === ClassComponent,
|
|
1215
|
+
revealed: resolvedBoundaries !== null && a.trigger === "parent" && isUnder(fiber, resolvedBoundaries),
|
|
1216
|
+
selfDuration: durations ? durations.self : void 0,
|
|
1217
|
+
treeDuration: durations ? durations.tree : void 0,
|
|
1218
|
+
commitId,
|
|
1219
|
+
commitPriority,
|
|
1220
|
+
source: sourceOf(fiber)
|
|
1221
|
+
});
|
|
1222
|
+
dispatch(report);
|
|
1223
|
+
}
|
|
1224
|
+
} finally {
|
|
1225
|
+
endDiffScope();
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// src/causes.ts
|
|
1230
|
+
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);
|
|
1231
|
+
var bump = (m, key, n = 1) => void m.set(key, (m.get(key) || 0) + n);
|
|
1232
|
+
function indexByComponent(reports) {
|
|
1233
|
+
const index = /* @__PURE__ */ new Map();
|
|
1234
|
+
for (const r of reports) {
|
|
1235
|
+
const list = index.get(r.component);
|
|
1236
|
+
if (list) list.push(r);
|
|
1237
|
+
else index.set(r.component, [r]);
|
|
1238
|
+
}
|
|
1239
|
+
return index;
|
|
1240
|
+
}
|
|
1241
|
+
function rootCauseOf(r, commitReports, index = indexByComponent(commitReports)) {
|
|
1242
|
+
let cur = r;
|
|
1243
|
+
const seen = /* @__PURE__ */ new Set([r]);
|
|
1244
|
+
while (cur.trigger === "parent" && cur.parent) {
|
|
1245
|
+
const parent = cur.parent;
|
|
1246
|
+
const candidates = index.get(parent.name);
|
|
1247
|
+
const p = candidates && candidates.find((x) => isAncestorReport(x, cur));
|
|
1248
|
+
if (!p || seen.has(p)) return { name: parent.name, trigger: parent.trigger, report: null };
|
|
1249
|
+
seen.add(p);
|
|
1250
|
+
cur = p;
|
|
1251
|
+
}
|
|
1252
|
+
return cur === r ? null : { name: cur.component, trigger: cur.trigger, report: cur };
|
|
1253
|
+
}
|
|
1254
|
+
function rootCausesOf(reports) {
|
|
1255
|
+
const roots = /* @__PURE__ */ new Map();
|
|
1256
|
+
const rootByReport = /* @__PURE__ */ new Map();
|
|
1257
|
+
const index = indexByComponent(reports);
|
|
1258
|
+
let avoidable = 0;
|
|
1259
|
+
let wasted = 0;
|
|
1260
|
+
for (const r of reports) {
|
|
1261
|
+
if (!r.avoidable) continue;
|
|
1262
|
+
avoidable++;
|
|
1263
|
+
if (typeof r.selfDuration === "number") wasted += r.selfDuration;
|
|
1264
|
+
const root = rootCauseOf(r, reports, index);
|
|
1265
|
+
const name = root ? root.name : r.parent && r.parent.name || "(unknown)";
|
|
1266
|
+
const trigger = root ? root.trigger : r.parent && r.parent.trigger || "parent";
|
|
1267
|
+
rootByReport.set(r, name);
|
|
1268
|
+
let agg = roots.get(name);
|
|
1269
|
+
if (!agg) {
|
|
1270
|
+
agg = { name, trigger, count: 0, components: /* @__PURE__ */ new Map() };
|
|
1271
|
+
roots.set(name, agg);
|
|
1272
|
+
}
|
|
1273
|
+
agg.count++;
|
|
1274
|
+
bump(agg.components, r.component);
|
|
1275
|
+
}
|
|
1276
|
+
return { roots: [...roots.values()].sort((a, b) => b.count - a.count), rootByReport, avoidable, wasted };
|
|
1277
|
+
}
|
|
1278
|
+
function groupByCommit(reports) {
|
|
1279
|
+
const out = /* @__PURE__ */ new Map();
|
|
1280
|
+
for (const r of reports) {
|
|
1281
|
+
if (typeof r.commitId !== "number" || r.commitId <= 0) continue;
|
|
1282
|
+
const list = out.get(r.commitId);
|
|
1283
|
+
if (list) list.push(r);
|
|
1284
|
+
else out.set(r.commitId, [r]);
|
|
1285
|
+
}
|
|
1286
|
+
return out;
|
|
1287
|
+
}
|
|
1288
|
+
function rankRootCauses(reports) {
|
|
1289
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1290
|
+
for (const commit of groupByCommit(reports).values()) {
|
|
1291
|
+
for (const root of rootCausesOf(commit).roots) {
|
|
1292
|
+
let agg = byName.get(root.name);
|
|
1293
|
+
if (!agg) {
|
|
1294
|
+
agg = { name: root.name, trigger: root.trigger, commits: 0, count: 0, components: /* @__PURE__ */ new Map() };
|
|
1295
|
+
byName.set(root.name, agg);
|
|
1296
|
+
}
|
|
1297
|
+
agg.commits++;
|
|
1298
|
+
agg.count += root.count;
|
|
1299
|
+
for (const [c, n] of root.components) bump(agg.components, c, n);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
return [...byName.values()].sort((a, b) => b.count - a.count || b.commits - a.commits || a.name.localeCompare(b.name));
|
|
1303
|
+
}
|
|
1304
|
+
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
1305
|
+
function formatRootCauses(reports, limit = 5) {
|
|
1306
|
+
const ranked = rankRootCauses(reports);
|
|
1307
|
+
const lines = ranked.slice(0, limit).map((root, i) => `${String(i + 1).padStart(2)}. <${root.name}> (${root.trigger}) started ${plural(root.commits, "commit")}, ${plural(root.count, "avoidable re-render")} (${[...root.components].map(([c, n]) => `<${c}>${n > 1 ? " x" + n : ""}`).join(", ")})`);
|
|
1308
|
+
if (ranked.length > limit) lines.push(` \u2026 ${ranked.length - limit} more`);
|
|
1309
|
+
return lines.join("\n");
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// src/fixes.ts
|
|
1313
|
+
var MAX_FIX_REPORTS = 50;
|
|
1314
|
+
var rootOf = (path) => path.split(/[.[]/)[0] || path;
|
|
1315
|
+
var identifier = (name) => /^[A-Za-z_$][\w$]*$/.test(name) ? name : "value";
|
|
1316
|
+
function shortValue(v, max = 60) {
|
|
1317
|
+
let s;
|
|
1318
|
+
try {
|
|
1319
|
+
s = JSON.stringify(v);
|
|
1320
|
+
} catch {
|
|
1321
|
+
s = String(v);
|
|
1322
|
+
}
|
|
1323
|
+
if (s === void 0) s = String(v);
|
|
1324
|
+
return s.length > max ? s.slice(0, max - 3) + "..." : s;
|
|
1325
|
+
}
|
|
1326
|
+
function fixesFor(r) {
|
|
1327
|
+
const out = [];
|
|
1328
|
+
const ownerName2 = r.owner || r.parent && r.parent.name || null;
|
|
1329
|
+
const propChanges = r.propChanges || [];
|
|
1330
|
+
const changes = [...propChanges, ...r.stateChanges || [], ...r.hookChanges || []];
|
|
1331
|
+
if (r.avoidable && (changes.length === 0 || r.memoized === false)) {
|
|
1332
|
+
const identical = changes.length === 0;
|
|
1333
|
+
out.push({
|
|
1334
|
+
kind: "memo",
|
|
1335
|
+
owner: r.component,
|
|
1336
|
+
target: r.component,
|
|
1337
|
+
prop: null,
|
|
1338
|
+
label: `Wrap <${r.component}> in React.memo`,
|
|
1339
|
+
detail: identical ? `<${r.component}> re-rendered with identical props because <${r.parent && r.parent.name || "its parent"}> re-rendered.` : `<${r.component}> is not memoized: fixing its props alone will not stop the re-render.`,
|
|
1340
|
+
snippet: `// ${r.component}
|
|
1341
|
+
import { memo } from 'react';
|
|
1342
|
+
|
|
1343
|
+
export const ${r.component} = memo(function ${r.component}(props) {
|
|
1344
|
+
// ...
|
|
1345
|
+
});
|
|
1346
|
+
// class components: extend PureComponent instead`
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
for (const c of propChanges) {
|
|
1350
|
+
if (!AVOIDABLE_KINDS.has(c.kind)) continue;
|
|
1351
|
+
const owner = ownerName2 || "?";
|
|
1352
|
+
const root = rootOf(c.path);
|
|
1353
|
+
const id = identifier(root);
|
|
1354
|
+
if (root === "children" && (c.kind === "element" || c.kind === "deep-equal")) {
|
|
1355
|
+
out.push({
|
|
1356
|
+
kind: "children",
|
|
1357
|
+
owner,
|
|
1358
|
+
target: r.component,
|
|
1359
|
+
prop: "children",
|
|
1360
|
+
label: `memoize children of <${r.component}> in <${owner}>`,
|
|
1361
|
+
detail: `<${owner}> re-creates the children of <${r.component}> on every render; they have the same types and props each time.`,
|
|
1362
|
+
snippet: `// ${owner}
|
|
1363
|
+
import { useMemo } from 'react';
|
|
1364
|
+
|
|
1365
|
+
const children = useMemo(() => (
|
|
1366
|
+
<>{/* the same elements */}</>
|
|
1367
|
+
), [/* deps */]);
|
|
1368
|
+
|
|
1369
|
+
<${r.component}>{children}</${r.component}>
|
|
1370
|
+
|
|
1371
|
+
// static children: hoist them to module scope
|
|
1372
|
+
const STATIC = <em>hi</em>;`
|
|
1373
|
+
});
|
|
1374
|
+
} else if (c.kind === "function") {
|
|
1375
|
+
out.push({
|
|
1376
|
+
kind: "useCallback",
|
|
1377
|
+
owner,
|
|
1378
|
+
target: r.component,
|
|
1379
|
+
prop: root,
|
|
1380
|
+
label: `useCallback(${root}) in <${owner}>`,
|
|
1381
|
+
detail: `prop "${c.path}" of <${r.component}> is a new function on every render of <${owner}>.`,
|
|
1382
|
+
snippet: `// ${owner}
|
|
1383
|
+
import { useCallback } from 'react';
|
|
1384
|
+
|
|
1385
|
+
const ${id} = useCallback((/* args */) => {
|
|
1386
|
+
// ...
|
|
1387
|
+
}, [/* deps */]);
|
|
1388
|
+
|
|
1389
|
+
<${r.component} ${root}={${id}} />`
|
|
1390
|
+
});
|
|
1391
|
+
} else if (c.kind === "element") {
|
|
1392
|
+
out.push({
|
|
1393
|
+
kind: "useMemoElement",
|
|
1394
|
+
owner,
|
|
1395
|
+
target: r.component,
|
|
1396
|
+
prop: root,
|
|
1397
|
+
label: `memoize element prop ${root} in <${owner}>`,
|
|
1398
|
+
detail: `prop "${c.path}" of <${r.component}> is a new element with the same type and props on every render of <${owner}>.`,
|
|
1399
|
+
snippet: `// ${owner}
|
|
1400
|
+
import { useMemo } from 'react';
|
|
1401
|
+
|
|
1402
|
+
const ${id} = useMemo(() => ${shortValue(c.next, 40)}, [/* deps */]);
|
|
1403
|
+
// or pass it as children from a component that does not re-render`
|
|
1404
|
+
});
|
|
1405
|
+
} else {
|
|
1406
|
+
const isArray = Array.isArray(c.next);
|
|
1407
|
+
out.push({
|
|
1408
|
+
kind: "useMemo",
|
|
1409
|
+
owner,
|
|
1410
|
+
target: r.component,
|
|
1411
|
+
prop: root,
|
|
1412
|
+
label: `useMemo(${root}) in <${owner}>`,
|
|
1413
|
+
detail: `prop "${c.path}" of <${r.component}> is a new ${isArray ? "array" : "object"} with the same contents on every render of <${owner}>.`,
|
|
1414
|
+
snippet: `// ${owner}
|
|
1415
|
+
import { useMemo } from 'react';
|
|
1416
|
+
|
|
1417
|
+
const ${id} = useMemo(() => (${shortValue(c.next, 80)}), [/* deps */]);
|
|
1418
|
+
|
|
1419
|
+
// or, when it never changes, hoist it to module scope:
|
|
1420
|
+
const ${id.toUpperCase()} = ${shortValue(c.next, 80)};`
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
for (const c of [...r.stateChanges || [], ...r.hookChanges || []]) {
|
|
1425
|
+
const isContext = c.hook === "useContext" || /^useContext/.test(c.path);
|
|
1426
|
+
const ctxName = isContext ? (/useContext\((.*)\)/.exec(c.path) || [])[1] || "Context" : "";
|
|
1427
|
+
const providerOwner = isContext && c.provider && c.provider.component ? c.provider.component : null;
|
|
1428
|
+
if (isContext && c.kind === "different" && c.changedKeys && typeof c.totalKeys === "number" && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys) {
|
|
1429
|
+
out.push({
|
|
1430
|
+
kind: "splitContext",
|
|
1431
|
+
owner: providerOwner || `${ctxName}.Provider`,
|
|
1432
|
+
target: r.component,
|
|
1433
|
+
prop: ctxName,
|
|
1434
|
+
label: `split ${ctxName}${providerOwner ? ` in <${providerOwner}>` : ""}: only ${c.changedKeys.join(", ")} changed`,
|
|
1435
|
+
detail: `${c.changedKeys.map((k) => `"${k}"`).join(", ")} of ${c.totalKeys} keys changed in ${ctxName}, yet every consumer (like <${r.component}>) re-rendered. Consumers that read the other keys re-render for nothing.`,
|
|
1436
|
+
snippet: `// ${providerOwner || "Provider"}
|
|
1437
|
+
// one context per independently-changing slice
|
|
1438
|
+
const ${identifier(ctxName)}Static = createContext(...);
|
|
1439
|
+
const ${identifier(ctxName)}${c.changedKeys.map((k) => k[0].toUpperCase() + k.slice(1)).join("")} = createContext(...);
|
|
1440
|
+
|
|
1441
|
+
// or keep one context and let consumers select a slice:
|
|
1442
|
+
const ${c.changedKeys[0]} = useContextSelector(${ctxName}, (v) => v.${c.changedKeys[0]});`
|
|
1443
|
+
});
|
|
1444
|
+
continue;
|
|
1445
|
+
}
|
|
1446
|
+
if (!AVOIDABLE_KINDS.has(c.kind)) continue;
|
|
1447
|
+
if (isContext) {
|
|
1448
|
+
out.push({
|
|
1449
|
+
kind: "contextValue",
|
|
1450
|
+
owner: providerOwner || `${ctxName}.Provider`,
|
|
1451
|
+
target: r.component,
|
|
1452
|
+
prop: ctxName,
|
|
1453
|
+
label: `memoize the ${ctxName} provider value${providerOwner ? ` in <${providerOwner}>` : ""}`,
|
|
1454
|
+
detail: `<${r.component}> re-rendered because ${ctxName} produced a new value that is deep-equal to the previous one${providerOwner ? ` (Provider rendered by <${providerOwner}>)` : ""}.`,
|
|
1455
|
+
snippet: `// ${providerOwner || `where <${ctxName}.Provider> is rendered`}
|
|
1456
|
+
const value = useMemo(() => ({ /* ... */ }), [/* deps */]);
|
|
1457
|
+
<${ctxName}.Provider value={value}>`
|
|
1458
|
+
});
|
|
1459
|
+
} else if (c.hook === "useSyncExternalStore") {
|
|
1460
|
+
const chain = c.custom || [];
|
|
1461
|
+
const redux = chain.some((n) => /^use(App)?Selector$/.test(n));
|
|
1462
|
+
const zustand = !redux && chain.some((n) => /^use[A-Z]\w*Store$/.test(n) || n === "useStore" || n === "useBoundStore");
|
|
1463
|
+
const via = chain.length ? ` via ${chain.join(" \u203A ")}` : "";
|
|
1464
|
+
out.push({
|
|
1465
|
+
kind: "storeSnapshot",
|
|
1466
|
+
owner: r.component,
|
|
1467
|
+
target: r.component,
|
|
1468
|
+
prop: c.path,
|
|
1469
|
+
label: redux ? `memoize the selector in <${r.component}>` : zustand ? `useShallow in <${r.component}>` : `stable getSnapshot in <${r.component}>`,
|
|
1470
|
+
detail: redux ? `the selector${via} returns a new object on every call, so the component re-renders on every store change.` : zustand ? `the store selector${via} returns a new object on every call, so the component re-renders on every store change.` : `${c.path}${via} returned a new reference with the same contents; getSnapshot must return a cached value.`,
|
|
1471
|
+
snippet: redux ? `// ${r.component}
|
|
1472
|
+
import { shallowEqual } from 'react-redux';
|
|
1473
|
+
const slice = useSelector(selectSlice, shallowEqual);
|
|
1474
|
+
// or memoize: const selectSlice = createSelector([selectA, selectB], (a, b) => ({ a, b }));` : zustand ? `// ${r.component}
|
|
1475
|
+
import { useShallow } from 'zustand/react/shallow';
|
|
1476
|
+
const { a, b } = useStore(useShallow((s) => ({ a: s.a, b: s.b })));
|
|
1477
|
+
// or select a primitive: const a = useStore((s) => s.a);` : `// ${r.component}
|
|
1478
|
+
// getSnapshot must return the same reference while the data is unchanged
|
|
1479
|
+
const snapshot = useSyncExternalStore(subscribe, store.getSnapshot /* cached */);`
|
|
1480
|
+
});
|
|
1481
|
+
} else {
|
|
1482
|
+
out.push({
|
|
1483
|
+
kind: "bailout",
|
|
1484
|
+
owner: r.component,
|
|
1485
|
+
target: r.component,
|
|
1486
|
+
prop: c.path,
|
|
1487
|
+
label: `bail out before setting ${c.path} in <${r.component}>`,
|
|
1488
|
+
detail: `${c.path} was set to a value deep-equal to the current one (new reference).`,
|
|
1489
|
+
snippet: `// ${r.component}
|
|
1490
|
+
setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
return out;
|
|
1495
|
+
}
|
|
1496
|
+
var fixKey = (f) => `${f.kind}|${f.owner}|${f.prop || f.target}`;
|
|
1497
|
+
function rankFixes(reports) {
|
|
1498
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1499
|
+
for (const r of reports) {
|
|
1500
|
+
if (!r.avoidable) continue;
|
|
1501
|
+
for (const f of fixesFor(r)) {
|
|
1502
|
+
const key = fixKey(f);
|
|
1503
|
+
let agg = byKey.get(key);
|
|
1504
|
+
if (!agg) {
|
|
1505
|
+
agg = { ...f, key, count: 0, components: {}, reports: [] };
|
|
1506
|
+
byKey.set(key, agg);
|
|
1507
|
+
}
|
|
1508
|
+
agg.count++;
|
|
1509
|
+
agg.components[r.component] = (agg.components[r.component] || 0) + 1;
|
|
1510
|
+
if (agg.reports.length < MAX_FIX_REPORTS) agg.reports.push(r);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
return [...byKey.values()].sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
|
|
1514
|
+
}
|
|
1515
|
+
function formatFixes(reports, limit = 10) {
|
|
1516
|
+
const ranked = rankFixes(reports);
|
|
1517
|
+
if (!ranked.length) return "No avoidable re-renders.";
|
|
1518
|
+
const lines = ranked.slice(0, limit).map((f, i) => `${String(i + 1).padStart(2)}. ${f.label} (removes ${f.count}: ${Object.entries(f.components).map(([c, n]) => `<${c}>${n > 1 ? " x" + n : ""}`).join(", ")})`);
|
|
1519
|
+
if (ranked.length > limit) lines.push(` \u2026 ${ranked.length - limit} more`);
|
|
1520
|
+
const roots = formatRootCauses(reports, 5);
|
|
1521
|
+
if (roots) lines.push("", "Root causes:", roots);
|
|
1522
|
+
return lines.join("\n");
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// src/sessions.ts
|
|
1526
|
+
function summarizeReports(reports, meta = {}) {
|
|
1527
|
+
var _a;
|
|
1528
|
+
const byComponent = {};
|
|
1529
|
+
let avoidable = 0;
|
|
1530
|
+
let wasted = 0;
|
|
1531
|
+
for (const r of reports) {
|
|
1532
|
+
const c = byComponent[_a = r.component] || (byComponent[_a] = { total: 0, avoidable: 0, wasted: 0 });
|
|
1533
|
+
c.total++;
|
|
1534
|
+
if (r.avoidable) {
|
|
1535
|
+
c.avoidable++;
|
|
1536
|
+
avoidable++;
|
|
1537
|
+
if (typeof r.selfDuration === "number") {
|
|
1538
|
+
c.wasted += r.selfDuration;
|
|
1539
|
+
wasted += r.selfDuration;
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
return {
|
|
1544
|
+
id: meta.id ?? `s${Date.now().toString(36)}`,
|
|
1545
|
+
name: meta.name ?? "session",
|
|
1546
|
+
startedAt: meta.startedAt ?? (reports[0]?.time ?? 0),
|
|
1547
|
+
endedAt: meta.endedAt ?? (reports[reports.length - 1]?.time ?? null),
|
|
1548
|
+
total: reports.length,
|
|
1549
|
+
avoidable,
|
|
1550
|
+
wasted,
|
|
1551
|
+
byComponent,
|
|
1552
|
+
fixes: rankFixes(reports).map((f) => ({ key: f.key, label: f.label, count: f.count }))
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
// src/budget.ts
|
|
1557
|
+
function avoidableCounts(reports) {
|
|
1558
|
+
const counts2 = {};
|
|
1559
|
+
for (const r of reports) if (r.avoidable) counts2[r.component] = (counts2[r.component] || 0) + 1;
|
|
1560
|
+
return counts2;
|
|
1561
|
+
}
|
|
1562
|
+
function checkBudget(reports, budget) {
|
|
1563
|
+
const b = typeof budget === "number" ? { "*": budget } : budget;
|
|
1564
|
+
const fallback = typeof b["*"] === "number" ? b["*"] : 0;
|
|
1565
|
+
const counts2 = avoidableCounts(reports);
|
|
1566
|
+
const violations = [];
|
|
1567
|
+
const slack = [];
|
|
1568
|
+
const names = /* @__PURE__ */ new Set([...Object.keys(counts2), ...Object.keys(b).filter((k) => k !== "*")]);
|
|
1569
|
+
for (const component of names) {
|
|
1570
|
+
const allowed = typeof b[component] === "number" ? b[component] : fallback;
|
|
1571
|
+
const avoidable = counts2[component] || 0;
|
|
1572
|
+
if (avoidable > allowed) violations.push({ component, avoidable, allowed });
|
|
1573
|
+
else if (avoidable < allowed) slack.push({ component, avoidable, allowed });
|
|
1574
|
+
}
|
|
1575
|
+
violations.sort((x, y) => y.avoidable - y.allowed - (x.avoidable - x.allowed));
|
|
1576
|
+
return { ok: violations.length === 0, violations, slack, counts: counts2 };
|
|
1577
|
+
}
|
|
1578
|
+
function assertWithinBudget(reports, budget) {
|
|
1579
|
+
const result = checkBudget(reports, budget);
|
|
1580
|
+
if (result.ok) return result;
|
|
1581
|
+
const lines = result.violations.map((v) => ` <${v.component}>: ${v.avoidable} avoidable re-render${v.avoidable === 1 ? "" : "s"} (budget ${v.allowed})`);
|
|
1582
|
+
throw new Error(`Re-render budget exceeded:
|
|
1583
|
+
${lines.join("\n")}
|
|
1584
|
+
|
|
1585
|
+
Fixes, most impact first:
|
|
1586
|
+
${formatFixes(reports)}`);
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
// src/notifiers.ts
|
|
1590
|
+
function createCollector() {
|
|
1591
|
+
const reports = [];
|
|
1592
|
+
return {
|
|
1593
|
+
reports,
|
|
1594
|
+
get avoidable() {
|
|
1595
|
+
return reports.filter((r) => r.avoidable);
|
|
1596
|
+
},
|
|
1597
|
+
notifier: (r) => {
|
|
1598
|
+
reports.push(r);
|
|
1599
|
+
},
|
|
1600
|
+
clear: () => {
|
|
1601
|
+
reports.length = 0;
|
|
1602
|
+
},
|
|
1603
|
+
assertNoAvoidable() {
|
|
1604
|
+
const bad = reports.filter((r) => r.avoidable);
|
|
1605
|
+
if (bad.length === 0) return;
|
|
1606
|
+
const lines = bad.map((r) => ` <${r.component}> render #${r.renderCount}:
|
|
1607
|
+
${r.reasons.map((x) => ` - ${x}`).join("\n")}`);
|
|
1608
|
+
throw new Error(`${bad.length} avoidable re-render${bad.length === 1 ? "" : "s"} detected:
|
|
1609
|
+
${lines.join("\n")}
|
|
1610
|
+
|
|
1611
|
+
Fixes, most impact first:
|
|
1612
|
+
${formatFixes(reports)}`);
|
|
1613
|
+
},
|
|
1614
|
+
fixes: () => rankFixes(reports),
|
|
1615
|
+
summary: (name) => summarizeReports(reports, name ? { name } : {}),
|
|
1616
|
+
assertWithinBudget: (budget) => assertWithinBudget(reports, budget)
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// src/runner-report.ts
|
|
1621
|
+
var OUT_ENV = "RERENDER_LENS_OUT";
|
|
1622
|
+
function setupRerenderLens(options = {}, hooks = {}) {
|
|
1623
|
+
ensureDevtoolsHook();
|
|
1624
|
+
const { failFast, ...rest } = options;
|
|
1625
|
+
const collector = createCollector();
|
|
1626
|
+
init({ trackAllMemoized: true, silent: true, ...rest, notifier: collector.notifier });
|
|
1627
|
+
const flush = () => {
|
|
1628
|
+
const reports = collector.reports.slice();
|
|
1629
|
+
if (!reports.length) return;
|
|
1630
|
+
const file = process.env[OUT_ENV];
|
|
1631
|
+
if (file) appendFileSync(file, JSON.stringify({ file: hooks.testPath?.() ?? "", reports: reports.map(slim) }) + "\n");
|
|
1632
|
+
collector.clear();
|
|
1633
|
+
if (failFast) {
|
|
1634
|
+
const avoidable = reports.filter((r) => r.avoidable);
|
|
1635
|
+
if (avoidable.length) throw new Error(`rerender-lens: ${avoidable.length} avoidable re-render${avoidable.length === 1 ? "" : "s"} in this file
|
|
1636
|
+
${formatFixes(avoidable, 5)}`);
|
|
1637
|
+
}
|
|
1638
|
+
};
|
|
1639
|
+
hooks.afterAll?.(flush);
|
|
1640
|
+
return { collector, flush };
|
|
1641
|
+
}
|
|
1642
|
+
function slim(r) {
|
|
1643
|
+
const value = (v) => Array.isArray(v) ? [] : typeof v === "function" ? `\u0192 ${v.name || ""}` : v && typeof v === "object" ? {} : v;
|
|
1644
|
+
return {
|
|
1645
|
+
...r,
|
|
1646
|
+
propChanges: r.propChanges.map((c) => ({ ...c, prev: value(c.prev), next: value(c.next) })),
|
|
1647
|
+
stateChanges: r.stateChanges.map((c) => ({ ...c, prev: value(c.prev), next: value(c.next) })),
|
|
1648
|
+
hookChanges: r.hookChanges.map((c) => ({ ...c, prev: value(c.prev), next: value(c.next) })),
|
|
1649
|
+
props: { prev: {}, next: {} },
|
|
1650
|
+
hookState: void 0,
|
|
1651
|
+
contexts: void 0,
|
|
1652
|
+
state: void 0
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
function createRunFile() {
|
|
1656
|
+
const dir = join(tmpdir(), "rerender-lens");
|
|
1657
|
+
mkdirSync(dir, { recursive: true });
|
|
1658
|
+
const file = join(dir, `run-${process.pid}-${Date.now()}.jsonl`);
|
|
1659
|
+
writeFileSync(file, "");
|
|
1660
|
+
process.env[OUT_ENV] = file;
|
|
1661
|
+
return file;
|
|
1662
|
+
}
|
|
1663
|
+
function finishRun(file, options = {}) {
|
|
1664
|
+
const reports = readRun(file);
|
|
1665
|
+
const log = options.write ?? ((line) => void process.stdout.write(line + "\n"));
|
|
1666
|
+
const avoidable = reports.filter((r) => r.avoidable).length;
|
|
1667
|
+
let error = null;
|
|
1668
|
+
log("");
|
|
1669
|
+
log(`rerender-lens: ${reports.length} report${reports.length === 1 ? "" : "s"}, ${avoidable} avoidable across the run`);
|
|
1670
|
+
if (avoidable) log(formatFixes(reports, options.limit ?? 10));
|
|
1671
|
+
if (options.exportTo) writeFileSync(options.exportTo, JSON.stringify({ rerenderLens: true, version: 2, exportedAt: (/* @__PURE__ */ new Date()).toISOString(), reports }, null, 2));
|
|
1672
|
+
if (options.budget) {
|
|
1673
|
+
const budget = JSON.parse(readFileSync(options.budget, "utf8"));
|
|
1674
|
+
const result = checkBudget(reports, budget);
|
|
1675
|
+
if (!result.ok) {
|
|
1676
|
+
const lines = result.violations.map((v) => ` budget exceeded: <${v.component}> ${v.avoidable} avoidable (allowed ${v.allowed})`);
|
|
1677
|
+
for (const line of lines) log(line);
|
|
1678
|
+
error = new Error(`rerender-lens: re-render budget exceeded (${options.budget})
|
|
1679
|
+
${lines.join("\n")}`);
|
|
1680
|
+
} else log(` within budget (${options.budget})`);
|
|
1681
|
+
}
|
|
1682
|
+
try {
|
|
1683
|
+
rmSync(file, { force: true });
|
|
1684
|
+
} catch {
|
|
1685
|
+
}
|
|
1686
|
+
if (process.env[OUT_ENV] === file) delete process.env[OUT_ENV];
|
|
1687
|
+
return { reports, error };
|
|
1688
|
+
}
|
|
1689
|
+
var RunReporterCore = class {
|
|
1690
|
+
constructor(options = {}) {
|
|
1691
|
+
this.done = false;
|
|
1692
|
+
this.options = options;
|
|
1693
|
+
this.file = createRunFile();
|
|
1694
|
+
}
|
|
1695
|
+
/** Publish the path again (the runner may have replaced `process.env` since the constructor ran). */
|
|
1696
|
+
arm() {
|
|
1697
|
+
process.env[OUT_ENV] = this.file;
|
|
1698
|
+
}
|
|
1699
|
+
/** The run's result, or null when the run was already finished (runners fire two "done" events). */
|
|
1700
|
+
finish() {
|
|
1701
|
+
if (this.done) return null;
|
|
1702
|
+
this.done = true;
|
|
1703
|
+
return finishRun(this.file, this.options);
|
|
1704
|
+
}
|
|
1705
|
+
};
|
|
1706
|
+
function readRun(file) {
|
|
1707
|
+
if (!existsSync(file)) return [];
|
|
1708
|
+
const out = [];
|
|
1709
|
+
for (const line of readFileSync(file, "utf8").split("\n")) {
|
|
1710
|
+
if (!line.trim()) continue;
|
|
1711
|
+
try {
|
|
1712
|
+
const parsed = JSON.parse(line);
|
|
1713
|
+
if (Array.isArray(parsed.reports)) out.push(...parsed.reports);
|
|
1714
|
+
} catch {
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
return out;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
// src/jest.ts
|
|
1721
|
+
function reporterOptionsOf(a, b) {
|
|
1722
|
+
if (b && typeof b === "object") return b;
|
|
1723
|
+
const first = a;
|
|
1724
|
+
if (first && typeof first === "object" && !("rootDir" in first) && ("budget" in first || "limit" in first || "exportTo" in first || "write" in first)) return first;
|
|
1725
|
+
return {};
|
|
1726
|
+
}
|
|
1727
|
+
var RerenderLensJestReporter = class {
|
|
1728
|
+
/** Jest: `new Reporter(globalConfig, reporterOptions, reporterContext)`. */
|
|
1729
|
+
constructor(globalConfigOrOptions, options) {
|
|
1730
|
+
this.error = null;
|
|
1731
|
+
this.core = new RunReporterCore(reporterOptionsOf(globalConfigOrOptions, options));
|
|
1732
|
+
}
|
|
1733
|
+
onRunStart() {
|
|
1734
|
+
this.core.arm();
|
|
1735
|
+
}
|
|
1736
|
+
onRunComplete() {
|
|
1737
|
+
const result = this.core.finish();
|
|
1738
|
+
if (result?.error) {
|
|
1739
|
+
this.error = result.error;
|
|
1740
|
+
process.exitCode = 1;
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
/** Jest fails the run when a reporter reports an error. */
|
|
1744
|
+
getLastError() {
|
|
1745
|
+
return this.error ?? void 0;
|
|
1746
|
+
}
|
|
1747
|
+
};
|
|
1748
|
+
|
|
1749
|
+
export { OUT_ENV, checkBudget, RerenderLensJestReporter as default, formatFixes, readRun, setupRerenderLens };
|
|
1750
|
+
//# sourceMappingURL=jest.js.map
|
|
1751
|
+
//# sourceMappingURL=jest.js.map
|