rerender-lens 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +78 -0
- package/README.md +192 -362
- 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-CZebzpF6.d.ts → devtools-BYWat7nQ.d.ts} +7 -1
- package/dist/{devtools-BkCct3cJ.d.cts → devtools-DJi_6AfJ.d.cts} +7 -1
- package/dist/index.cjs +439 -41
- 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 +424 -42
- 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 +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 +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 +356 -54
- package/dist/runner-report-B8Sc3FZO.d.cts +46 -0
- package/dist/runner-report-CW8X5RwK.d.ts +46 -0
- package/dist/setup.cjs +112 -27
- package/dist/setup.cjs.map +1 -1
- package/dist/setup.js +112 -27
- 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 +337 -41
- 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 +337 -41
- package/dist/vitest-setup.js.map +1 -1
- package/dist/vitest.cjs +400 -77
- 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 +401 -78
- package/dist/vitest.js.map +1 -1
- package/package.json +40 -4
- package/panel/panel.css +1 -0
- package/panel/panel.js +499 -309
package/dist/vitest.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, readFileSync, appendFileSync, mkdirSync, writeFileSync, rmSync } from 'fs';
|
|
2
2
|
import { tmpdir } from 'os';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
|
|
5
|
-
// src/
|
|
5
|
+
// src/runner-report.ts
|
|
6
6
|
|
|
7
7
|
// src/diff.ts
|
|
8
8
|
var REACT_ELEMENT = /* @__PURE__ */ Symbol.for("react.element");
|
|
@@ -126,6 +126,7 @@ function deepEqualObjects(a, b, seen) {
|
|
|
126
126
|
}
|
|
127
127
|
return false;
|
|
128
128
|
}
|
|
129
|
+
var AVOIDABLE_KINDS = /* @__PURE__ */ new Set(["deep-equal", "function", "element"]);
|
|
129
130
|
function classify(prev, next) {
|
|
130
131
|
if (typeof prev === "function" && typeof next === "function") {
|
|
131
132
|
return prev.name === next.name && prev.toString() === next.toString() ? "function" : "different";
|
|
@@ -162,30 +163,30 @@ function diffRecords(prev, next, basePath = "") {
|
|
|
162
163
|
if (Object.is(a, b)) continue;
|
|
163
164
|
const kind = classify(a, b);
|
|
164
165
|
if (kind === "different") {
|
|
165
|
-
changes.push({ path: diffBudgetExhausted() ? path : firstDifferentPath(a, b, path), kind, prev: a, next: b });
|
|
166
|
+
changes.push({ path: diffBudgetExhausted() ? path : firstDifferentPath(a, b, path) ?? path, kind, prev: a, next: b });
|
|
166
167
|
} else {
|
|
167
168
|
changes.push({ path, kind, prev: a, next: b });
|
|
168
169
|
}
|
|
169
170
|
}
|
|
170
171
|
return changes;
|
|
171
172
|
}
|
|
172
|
-
function firstDifferentPath(a, b, path, depth = 0) {
|
|
173
|
-
if (depth > 8) return path;
|
|
173
|
+
function firstDifferentPath(a, b, path = "", depth = 0) {
|
|
174
|
+
if (depth > 8) return path || "(value)";
|
|
174
175
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
175
|
-
if (a.length !== b.length) return `${path}.length
|
|
176
|
+
if (a.length !== b.length) return path ? `${path}.length` : "length";
|
|
176
177
|
for (let i = 0; i < a.length; i++) {
|
|
177
178
|
if (!deepEqual(a[i], b[i])) return firstDifferentPath(a[i], b[i], joinPath(path, i), depth + 1);
|
|
178
179
|
}
|
|
179
|
-
return
|
|
180
|
+
return null;
|
|
180
181
|
}
|
|
181
182
|
if (isPlainObject(a) && isPlainObject(b)) {
|
|
182
183
|
const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
183
184
|
for (const k of keys) {
|
|
184
185
|
if (!deepEqual(a[k], b[k])) return firstDifferentPath(a[k], b[k], joinPath(path, k), depth + 1);
|
|
185
186
|
}
|
|
186
|
-
return
|
|
187
|
+
return null;
|
|
187
188
|
}
|
|
188
|
-
return path;
|
|
189
|
+
return deepEqual(a, b) ? null : path || "(value)";
|
|
189
190
|
}
|
|
190
191
|
|
|
191
192
|
// src/report.ts
|
|
@@ -203,10 +204,12 @@ function storeAdvice(custom) {
|
|
|
203
204
|
}
|
|
204
205
|
var isGenuine = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
|
|
205
206
|
var isChildren = (change) => change.path === "children";
|
|
207
|
+
var isRefObject = (change) => change.path === "ref" && change.kind === "deep-equal";
|
|
206
208
|
function fixFor(change) {
|
|
207
209
|
if (isChildren(change) && (change.kind === "element" || change.kind === "deep-equal")) {
|
|
208
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`;
|
|
209
211
|
}
|
|
212
|
+
if (isRefObject(change)) return `create the ref once with useRef (or createRef outside the render) and pass the same object every time`;
|
|
210
213
|
switch (change.kind) {
|
|
211
214
|
case "function":
|
|
212
215
|
return `wrap it in useCallback (or hoist it out of the parent's render)`;
|
|
@@ -222,6 +225,7 @@ function describe(change) {
|
|
|
222
225
|
if (isChildren(change) && (change.kind === "element" || change.kind === "deep-equal")) {
|
|
223
226
|
return `children are new React elements with the same types and props on every render of the parent`;
|
|
224
227
|
}
|
|
228
|
+
if (isRefObject(change)) return `prop "ref" is a new ref object on every render (React re-attaches it each time)`;
|
|
225
229
|
switch (change.kind) {
|
|
226
230
|
case "deep-equal":
|
|
227
231
|
return `prop "${change.path}" is a new reference but deep-equal to the previous value`;
|
|
@@ -249,11 +253,21 @@ function buildReport(input) {
|
|
|
249
253
|
if (causes.length === 0) trigger = "parent";
|
|
250
254
|
else if (causes.length === 1) trigger = causes[0];
|
|
251
255
|
else trigger = "mixed";
|
|
252
|
-
const
|
|
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;
|
|
253
260
|
const reasons = [];
|
|
254
|
-
|
|
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) {
|
|
255
269
|
const who = input.parent ? `<${input.parent.name}> re-rendered (${describeTrigger(input.parent.trigger)})` : "its parent re-rendered";
|
|
256
|
-
reasons.push(`re-rendered with identical props because ${who}.
|
|
270
|
+
reasons.push(`re-rendered with identical props because ${who}. ${memoFix}.`);
|
|
257
271
|
} else if (avoidable && input.parent) {
|
|
258
272
|
reasons.push(`caused by <${input.parent.name}> re-rendering (${describeTrigger(input.parent.trigger)}).`);
|
|
259
273
|
}
|
|
@@ -262,9 +276,9 @@ function buildReport(input) {
|
|
|
262
276
|
reasons.push(fix ? `${describe(c)}: ${fix}.` : `${describe(c)}.`);
|
|
263
277
|
}
|
|
264
278
|
const memoized = input.memoized !== false;
|
|
265
|
-
if (avoidable && !memoized && input.propChanges.length > 0) {
|
|
279
|
+
if (avoidable && !memoized && !input.compiled && input.propChanges.length > 0) {
|
|
266
280
|
reasons.push(
|
|
267
|
-
`"${input.component}" is not memoized, so fixing the props alone will not stop this re-render: also wrap it in React.memo (or extend PureComponent).`
|
|
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)"}.`
|
|
268
282
|
);
|
|
269
283
|
}
|
|
270
284
|
for (const c of stateChanges) {
|
|
@@ -276,6 +290,8 @@ function buildReport(input) {
|
|
|
276
290
|
const where = c.provider && c.provider.component ? ` (provided by <${c.provider.component}>)` : "";
|
|
277
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` : "";
|
|
278
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.`);
|
|
279
295
|
} else if (isGenuine(c)) reasons.push(`${hookLabel(c)} changed.`);
|
|
280
296
|
else if (isStateHook(c))
|
|
281
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.`);
|
|
@@ -306,6 +322,7 @@ function buildReport(input) {
|
|
|
306
322
|
reasons,
|
|
307
323
|
time: now()
|
|
308
324
|
};
|
|
325
|
+
if (input.compiled) report.compiled = true;
|
|
309
326
|
if (input.selfDuration !== void 0) report.selfDuration = input.selfDuration;
|
|
310
327
|
if (input.treeDuration !== void 0) report.treeDuration = input.treeDuration;
|
|
311
328
|
if (input.commitPriority) report.commitPriority = input.commitPriority;
|
|
@@ -343,10 +360,10 @@ var KIND_LABEL = {
|
|
|
343
360
|
};
|
|
344
361
|
function summarize(report) {
|
|
345
362
|
const counts2 = /* @__PURE__ */ new Map();
|
|
346
|
-
for (const c of [...report.propChanges, ...report.stateChanges, ...report.hookChanges]) {
|
|
363
|
+
for (const c of [...report.propChanges || [], ...report.stateChanges || [], ...report.hookChanges || []]) {
|
|
347
364
|
counts2.set(c.kind, (counts2.get(c.kind) ?? 0) + 1);
|
|
348
365
|
}
|
|
349
|
-
const parts = [...counts2].map(([kind, n]) => `${n} ${KIND_LABEL[kind]}`);
|
|
366
|
+
const parts = [...counts2].map(([kind, n]) => `${n} ${KIND_LABEL[kind] || kind}`);
|
|
350
367
|
if (parts.length === 0) parts.push("no changes");
|
|
351
368
|
return parts.join(", ");
|
|
352
369
|
}
|
|
@@ -470,6 +487,27 @@ function disable() {
|
|
|
470
487
|
}
|
|
471
488
|
|
|
472
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"]);
|
|
473
511
|
var noop = () => {
|
|
474
512
|
};
|
|
475
513
|
function customHooksFromStack(stack, componentName, marker) {
|
|
@@ -484,7 +522,7 @@ function customHooksFromStack(stack, componentName, marker) {
|
|
|
484
522
|
continue;
|
|
485
523
|
}
|
|
486
524
|
if (name === componentName || name.endsWith("." + componentName)) break;
|
|
487
|
-
if (/^use[A-Z0-9_]/.test(name)) names.push(name);
|
|
525
|
+
if (/^use[A-Z0-9_]/.test(name) && !BUILTIN_HOOKS.has(name)) names.push(name);
|
|
488
526
|
if (names.length > 8) break;
|
|
489
527
|
}
|
|
490
528
|
return names;
|
|
@@ -768,6 +806,11 @@ function isMemoizedFiber(fiber) {
|
|
|
768
806
|
}
|
|
769
807
|
return false;
|
|
770
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
|
+
}
|
|
771
814
|
function durationsOf(fiber) {
|
|
772
815
|
if (typeof fiber.actualDuration !== "number") return null;
|
|
773
816
|
const tree = fiber.actualDuration;
|
|
@@ -920,7 +963,8 @@ function diffHooks(fiber, alt) {
|
|
|
920
963
|
const labels = hookLabelsFor(fiber, b);
|
|
921
964
|
let i = 0;
|
|
922
965
|
while (a && b) {
|
|
923
|
-
|
|
966
|
+
const deferred = labels?.[i] === "useDeferredValue";
|
|
967
|
+
if ((deferred || isStateNode(b) || isStoreNode(b)) && !Object.is(a.memoizedState, b.memoizedState)) {
|
|
924
968
|
const hook = labels?.[i] ?? hookLabel2(b);
|
|
925
969
|
out.push({
|
|
926
970
|
path: `${hook}#${i}`,
|
|
@@ -954,6 +998,10 @@ function diffContexts(fiber, alt) {
|
|
|
954
998
|
let a = alt.dependencies?.firstContext ?? null;
|
|
955
999
|
let b = fiber.dependencies?.firstContext ?? null;
|
|
956
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
|
+
}
|
|
957
1005
|
while (a && b) {
|
|
958
1006
|
if (a.context === b.context && !Object.is(a.memoizedValue, b.memoizedValue)) {
|
|
959
1007
|
const name = b.context.displayName ?? "Context";
|
|
@@ -986,10 +1034,26 @@ function diffContexts(fiber, alt) {
|
|
|
986
1034
|
return out;
|
|
987
1035
|
}
|
|
988
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
|
+
}
|
|
989
1053
|
function analyze(fiber, alt, trackHooks) {
|
|
990
1054
|
const prevProps = alt.memoizedProps ?? {};
|
|
991
1055
|
const nextProps = fiber.memoizedProps ?? {};
|
|
992
|
-
const propChanges =
|
|
1056
|
+
const propChanges = diffProps(prevProps, nextProps);
|
|
993
1057
|
let stateChanges = [];
|
|
994
1058
|
let hookChanges = [];
|
|
995
1059
|
if (fiber.tag === ClassComponent) {
|
|
@@ -1002,7 +1066,7 @@ function analyze(fiber, alt, trackHooks) {
|
|
|
1002
1066
|
} else if (trackHooks) {
|
|
1003
1067
|
hookChanges = [...diffHooks(fiber, alt), ...diffContexts(fiber, alt)];
|
|
1004
1068
|
}
|
|
1005
|
-
const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore";
|
|
1069
|
+
const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore" && h.hook !== "useDeferredValue";
|
|
1006
1070
|
const causes = [];
|
|
1007
1071
|
if (propChanges.some(isGenuine2)) causes.push("props");
|
|
1008
1072
|
if (stateChanges.some(isGenuine2) || hookChanges.some((h) => isStateHook(h) && isGenuine2(h))) causes.push("state");
|
|
@@ -1042,6 +1106,24 @@ var lastCommit = null;
|
|
|
1042
1106
|
var EFFECT_LOOP_WINDOW_MS = 50;
|
|
1043
1107
|
var MAX_REPORTS_PER_COMMIT = 200;
|
|
1044
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
|
+
}
|
|
1045
1127
|
function onCommit(root, commitPriority) {
|
|
1046
1128
|
const s = getState();
|
|
1047
1129
|
if (!s.enabled) return;
|
|
@@ -1051,7 +1133,7 @@ function onCommit(root, commitPriority) {
|
|
|
1051
1133
|
const rendered = [];
|
|
1052
1134
|
const renderedNames = /* @__PURE__ */ new Set();
|
|
1053
1135
|
let hot = false;
|
|
1054
|
-
let
|
|
1136
|
+
let resolvedBoundaries = null;
|
|
1055
1137
|
const stack = [root.current];
|
|
1056
1138
|
while (stack.length) {
|
|
1057
1139
|
const fiber = stack.pop();
|
|
@@ -1061,7 +1143,7 @@ function onCommit(root, commitPriority) {
|
|
|
1061
1143
|
renderedNames.add(fiberName(fiber));
|
|
1062
1144
|
if (shouldTrack(fiberType(fiber), o)) rendered.push(fiber);
|
|
1063
1145
|
}
|
|
1064
|
-
if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null)
|
|
1146
|
+
if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null) (resolvedBoundaries ?? (resolvedBoundaries = [])).push(fiber);
|
|
1065
1147
|
if (!alt || fiber.child !== alt.child) {
|
|
1066
1148
|
for (let c = fiber.child; c; c = c.sibling) stack.push(c);
|
|
1067
1149
|
}
|
|
@@ -1073,10 +1155,10 @@ function onCommit(root, commitPriority) {
|
|
|
1073
1155
|
let commitCause;
|
|
1074
1156
|
let afterCommit;
|
|
1075
1157
|
const inputDriven = commitPriority === "immediate" || commitPriority === "user-blocking";
|
|
1076
|
-
if (!inputDriven && lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u))) {
|
|
1158
|
+
if (!inputDriven && lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u)) && !rendered.some(isDeferredCatchUp)) {
|
|
1077
1159
|
commitCause = "effect-after-commit";
|
|
1078
1160
|
afterCommit = lastCommit.id;
|
|
1079
|
-
} else if (
|
|
1161
|
+
} else if (resolvedBoundaries) commitCause = "suspense-resolved";
|
|
1080
1162
|
if (rendered.length === 0) {
|
|
1081
1163
|
lastCommit = { id: s.nextCommitId, at, rendered: renderedNames };
|
|
1082
1164
|
return;
|
|
@@ -1128,6 +1210,9 @@ function onCommit(root, commitPriority) {
|
|
|
1128
1210
|
owner: ownerName(fiber),
|
|
1129
1211
|
path: componentPath(fiber),
|
|
1130
1212
|
memoized: isMemoizedFiber(fiber),
|
|
1213
|
+
compiled: isCompiledFiber(fiber),
|
|
1214
|
+
classComponent: fiber.tag === ClassComponent,
|
|
1215
|
+
revealed: resolvedBoundaries !== null && a.trigger === "parent" && isUnder(fiber, resolvedBoundaries),
|
|
1131
1216
|
selfDuration: durations ? durations.self : void 0,
|
|
1132
1217
|
treeDuration: durations ? durations.tree : void 0,
|
|
1133
1218
|
commitId,
|
|
@@ -1141,38 +1226,203 @@ function onCommit(root, commitPriority) {
|
|
|
1141
1226
|
}
|
|
1142
1227
|
}
|
|
1143
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
|
+
|
|
1144
1312
|
// src/fixes.ts
|
|
1145
|
-
var
|
|
1313
|
+
var MAX_FIX_REPORTS = 50;
|
|
1146
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
|
+
}
|
|
1147
1326
|
function fixesFor(r) {
|
|
1148
1327
|
const out = [];
|
|
1149
|
-
const
|
|
1150
|
-
const
|
|
1328
|
+
const ownerName2 = r.owner || r.parent && r.parent.name || null;
|
|
1329
|
+
const propChanges = r.propChanges || [];
|
|
1330
|
+
const changes = [...propChanges, ...r.stateChanges || [], ...r.hookChanges || []];
|
|
1151
1331
|
if (r.avoidable && (changes.length === 0 || r.memoized === false)) {
|
|
1332
|
+
const identical = changes.length === 0;
|
|
1152
1333
|
out.push({
|
|
1153
1334
|
kind: "memo",
|
|
1154
1335
|
owner: r.component,
|
|
1155
1336
|
target: r.component,
|
|
1156
1337
|
prop: null,
|
|
1157
1338
|
label: `Wrap <${r.component}> in React.memo`,
|
|
1158
|
-
detail:
|
|
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`
|
|
1159
1347
|
});
|
|
1160
1348
|
}
|
|
1161
|
-
for (const c of
|
|
1162
|
-
if (!
|
|
1349
|
+
for (const c of propChanges) {
|
|
1350
|
+
if (!AVOIDABLE_KINDS.has(c.kind)) continue;
|
|
1351
|
+
const owner = ownerName2 || "?";
|
|
1163
1352
|
const root = rootOf(c.path);
|
|
1353
|
+
const id = identifier(root);
|
|
1164
1354
|
if (root === "children" && (c.kind === "element" || c.kind === "deep-equal")) {
|
|
1165
|
-
out.push({
|
|
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
|
+
});
|
|
1166
1374
|
} else if (c.kind === "function") {
|
|
1167
|
-
out.push({
|
|
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
|
+
});
|
|
1168
1391
|
} else if (c.kind === "element") {
|
|
1169
|
-
out.push({
|
|
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
|
+
});
|
|
1170
1405
|
} else {
|
|
1171
|
-
|
|
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
|
+
});
|
|
1172
1422
|
}
|
|
1173
1423
|
}
|
|
1174
|
-
for (const c of [...r.stateChanges, ...r.hookChanges]) {
|
|
1175
|
-
const isContext = c.hook === "useContext";
|
|
1424
|
+
for (const c of [...r.stateChanges || [], ...r.hookChanges || []]) {
|
|
1425
|
+
const isContext = c.hook === "useContext" || /^useContext/.test(c.path);
|
|
1176
1426
|
const ctxName = isContext ? (/useContext\((.*)\)/.exec(c.path) || [])[1] || "Context" : "";
|
|
1177
1427
|
const providerOwner = isContext && c.provider && c.provider.component ? c.provider.component : null;
|
|
1178
1428
|
if (isContext && c.kind === "different" && c.changedKeys && typeof c.totalKeys === "number" && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys) {
|
|
@@ -1182,20 +1432,63 @@ function fixesFor(r) {
|
|
|
1182
1432
|
target: r.component,
|
|
1183
1433
|
prop: ctxName,
|
|
1184
1434
|
label: `split ${ctxName}${providerOwner ? ` in <${providerOwner}>` : ""}: only ${c.changedKeys.join(", ")} changed`,
|
|
1185
|
-
detail: `${c.changedKeys.
|
|
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]});`
|
|
1186
1443
|
});
|
|
1187
1444
|
continue;
|
|
1188
1445
|
}
|
|
1189
|
-
if (!
|
|
1446
|
+
if (!AVOIDABLE_KINDS.has(c.kind)) continue;
|
|
1190
1447
|
if (isContext) {
|
|
1191
|
-
out.push({
|
|
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
|
+
});
|
|
1192
1459
|
} else if (c.hook === "useSyncExternalStore") {
|
|
1193
1460
|
const chain = c.custom || [];
|
|
1194
1461
|
const redux = chain.some((n) => /^use(App)?Selector$/.test(n));
|
|
1195
1462
|
const zustand = !redux && chain.some((n) => /^use[A-Z]\w*Store$/.test(n) || n === "useStore" || n === "useBoundStore");
|
|
1196
|
-
|
|
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
|
+
});
|
|
1197
1481
|
} else {
|
|
1198
|
-
out.push({
|
|
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
|
+
});
|
|
1199
1492
|
}
|
|
1200
1493
|
}
|
|
1201
1494
|
return out;
|
|
@@ -1209,11 +1502,12 @@ function rankFixes(reports) {
|
|
|
1209
1502
|
const key = fixKey(f);
|
|
1210
1503
|
let agg = byKey.get(key);
|
|
1211
1504
|
if (!agg) {
|
|
1212
|
-
agg = { ...f, key, count: 0, components: {} };
|
|
1505
|
+
agg = { ...f, key, count: 0, components: {}, reports: [] };
|
|
1213
1506
|
byKey.set(key, agg);
|
|
1214
1507
|
}
|
|
1215
1508
|
agg.count++;
|
|
1216
1509
|
agg.components[r.component] = (agg.components[r.component] || 0) + 1;
|
|
1510
|
+
if (agg.reports.length < MAX_FIX_REPORTS) agg.reports.push(r);
|
|
1217
1511
|
}
|
|
1218
1512
|
}
|
|
1219
1513
|
return [...byKey.values()].sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
|
|
@@ -1223,6 +1517,8 @@ function formatFixes(reports, limit = 10) {
|
|
|
1223
1517
|
if (!ranked.length) return "No avoidable re-renders.";
|
|
1224
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(", ")})`);
|
|
1225
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);
|
|
1226
1522
|
return lines.join("\n");
|
|
1227
1523
|
}
|
|
1228
1524
|
|
|
@@ -1321,7 +1617,7 @@ ${formatFixes(reports)}`);
|
|
|
1321
1617
|
};
|
|
1322
1618
|
}
|
|
1323
1619
|
|
|
1324
|
-
// src/
|
|
1620
|
+
// src/runner-report.ts
|
|
1325
1621
|
var OUT_ENV = "RERENDER_LENS_OUT";
|
|
1326
1622
|
function setupRerenderLens(options = {}, hooks = {}) {
|
|
1327
1623
|
ensureDevtoolsHook();
|
|
@@ -1356,49 +1652,55 @@ function slim(r) {
|
|
|
1356
1652
|
state: void 0
|
|
1357
1653
|
};
|
|
1358
1654
|
}
|
|
1359
|
-
|
|
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 {
|
|
1360
1690
|
constructor(options = {}) {
|
|
1361
1691
|
this.done = false;
|
|
1362
1692
|
this.options = options;
|
|
1363
|
-
|
|
1364
|
-
mkdirSync(dir, { recursive: true });
|
|
1365
|
-
this.file = join(dir, `run-${process.pid}-${Date.now()}.jsonl`);
|
|
1366
|
-
writeFileSync(this.file, "");
|
|
1367
|
-
process.env[OUT_ENV] = this.file;
|
|
1693
|
+
this.file = createRunFile();
|
|
1368
1694
|
}
|
|
1369
|
-
|
|
1695
|
+
/** Publish the path again (the runner may have replaced `process.env` since the constructor ran). */
|
|
1696
|
+
arm() {
|
|
1370
1697
|
process.env[OUT_ENV] = this.file;
|
|
1371
1698
|
}
|
|
1372
|
-
|
|
1373
|
-
this.finish();
|
|
1374
|
-
}
|
|
1375
|
-
/** Vitest < 3 name for the same event. */
|
|
1376
|
-
onFinished() {
|
|
1377
|
-
this.finish();
|
|
1378
|
-
}
|
|
1699
|
+
/** The run's result, or null when the run was already finished (runners fire two "done" events). */
|
|
1379
1700
|
finish() {
|
|
1380
|
-
if (this.done) return;
|
|
1701
|
+
if (this.done) return null;
|
|
1381
1702
|
this.done = true;
|
|
1382
|
-
|
|
1383
|
-
const log = this.options.write ?? ((line) => void process.stdout.write(line + "\n"));
|
|
1384
|
-
const avoidable = reports.filter((r) => r.avoidable).length;
|
|
1385
|
-
log("");
|
|
1386
|
-
log(`rerender-lens: ${reports.length} report${reports.length === 1 ? "" : "s"}, ${avoidable} avoidable across the run`);
|
|
1387
|
-
if (avoidable) log(formatFixes(reports, this.options.limit ?? 10));
|
|
1388
|
-
if (this.options.exportTo) writeFileSync(this.options.exportTo, JSON.stringify({ rerenderLens: true, version: 2, exportedAt: (/* @__PURE__ */ new Date()).toISOString(), reports }, null, 2));
|
|
1389
|
-
if (this.options.budget) {
|
|
1390
|
-
const budget = JSON.parse(readFileSync(this.options.budget, "utf8"));
|
|
1391
|
-
const result = checkBudget(reports, budget);
|
|
1392
|
-
if (!result.ok) {
|
|
1393
|
-
for (const v of result.violations) log(` budget exceeded: <${v.component}> ${v.avoidable} avoidable (allowed ${v.allowed})`);
|
|
1394
|
-
process.exitCode = 1;
|
|
1395
|
-
} else log(` within budget (${this.options.budget})`);
|
|
1396
|
-
}
|
|
1397
|
-
try {
|
|
1398
|
-
rmSync(this.file, { force: true });
|
|
1399
|
-
} catch {
|
|
1400
|
-
}
|
|
1401
|
-
if (process.env[OUT_ENV] === this.file) delete process.env[OUT_ENV];
|
|
1703
|
+
return finishRun(this.file, this.options);
|
|
1402
1704
|
}
|
|
1403
1705
|
};
|
|
1404
1706
|
function readRun(file) {
|
|
@@ -1415,6 +1717,27 @@ function readRun(file) {
|
|
|
1415
1717
|
return out;
|
|
1416
1718
|
}
|
|
1417
1719
|
|
|
1720
|
+
// src/vitest.ts
|
|
1721
|
+
var RerenderLensReporter = class {
|
|
1722
|
+
constructor(options = {}) {
|
|
1723
|
+
this.core = new RunReporterCore(options);
|
|
1724
|
+
}
|
|
1725
|
+
onInit() {
|
|
1726
|
+
this.core.arm();
|
|
1727
|
+
}
|
|
1728
|
+
onTestRunEnd() {
|
|
1729
|
+
this.finish();
|
|
1730
|
+
}
|
|
1731
|
+
/** Vitest < 3 name for the same event. */
|
|
1732
|
+
onFinished() {
|
|
1733
|
+
this.finish();
|
|
1734
|
+
}
|
|
1735
|
+
finish() {
|
|
1736
|
+
const result = this.core.finish();
|
|
1737
|
+
if (result?.error) process.exitCode = 1;
|
|
1738
|
+
}
|
|
1739
|
+
};
|
|
1740
|
+
|
|
1418
1741
|
export { OUT_ENV, checkBudget, RerenderLensReporter as default, formatFixes, readRun, setupRerenderLens };
|
|
1419
1742
|
//# sourceMappingURL=vitest.js.map
|
|
1420
1743
|
//# sourceMappingURL=vitest.js.map
|