rerender-lens 0.4.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +127 -0
- package/README.md +208 -359
- package/dist/{budget-LkNjGtRc.d.ts → budget-CA1MS7lB.d.cts} +20 -10
- package/dist/{budget-COBu7jBU.d.cts → budget-ok1VDJ4t.d.ts} +20 -10
- package/dist/cli.cjs +322 -53
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +322 -53
- package/dist/cli.js.map +1 -1
- package/dist/{devtools-BkCct3cJ.d.cts → devtools-Cl5n_52v.d.ts} +17 -2
- package/dist/{devtools-CZebzpF6.d.ts → devtools-UwQx2VYS.d.cts} +17 -2
- package/dist/index.cjs +537 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +137 -8
- package/dist/index.d.ts +137 -8
- package/dist/index.js +521 -50
- package/dist/index.js.map +1 -1
- package/dist/jest-setup.cjs +1690 -0
- package/dist/jest-setup.cjs.map +1 -0
- package/dist/jest-setup.d.cts +8 -0
- package/dist/jest-setup.d.ts +8 -0
- package/dist/jest-setup.js +1688 -0
- package/dist/jest-setup.js.map +1 -0
- package/dist/jest.cjs +1777 -0
- package/dist/jest.cjs.map +1 -0
- package/dist/jest.d.cts +23 -0
- package/dist/jest.d.ts +23 -0
- package/dist/jest.js +1768 -0
- package/dist/jest.js.map +1 -0
- package/dist/{notifiers-BjjGSHpp.d.ts → notifiers-B5poAEvE.d.cts} +8 -4
- package/dist/{notifiers-BGQtWKfX.d.cts → notifiers-CqQW0Eum.d.ts} +8 -4
- package/dist/playwright.cjs +230 -28
- package/dist/playwright.cjs.map +1 -1
- package/dist/playwright.d.cts +4 -4
- package/dist/playwright.d.ts +4 -4
- package/dist/playwright.js +230 -28
- package/dist/playwright.js.map +1 -1
- package/dist/relay.cjs +71 -19
- package/dist/relay.cjs.map +1 -1
- package/dist/relay.d.cts +25 -3
- package/dist/relay.d.ts +25 -3
- package/dist/relay.js +71 -19
- package/dist/relay.js.map +1 -1
- package/dist/rerender-lens.iife.js +457 -62
- package/dist/runner-report-Bvqoe89M.d.ts +46 -0
- package/dist/runner-report-CvpDg2zt.d.cts +46 -0
- package/dist/setup.cjs +209 -35
- package/dist/setup.cjs.map +1 -1
- package/dist/setup.js +209 -35
- package/dist/setup.js.map +1 -1
- package/dist/{types-BzUEVkxJ.d.cts → types-DwNN5cWe.d.cts} +82 -2
- package/dist/{types-BzUEVkxJ.d.ts → types-DwNN5cWe.d.ts} +82 -2
- package/dist/vite.d.cts +1 -1
- package/dist/vite.d.ts +1 -1
- package/dist/vitest-setup.cjs +357 -44
- package/dist/vitest-setup.cjs.map +1 -1
- package/dist/vitest-setup.d.cts +4 -4
- package/dist/vitest-setup.d.ts +4 -4
- package/dist/vitest-setup.js +357 -44
- package/dist/vitest-setup.js.map +1 -1
- package/dist/vitest.cjs +420 -80
- package/dist/vitest.cjs.map +1 -1
- package/dist/vitest.d.cts +21 -39
- package/dist/vitest.d.ts +21 -39
- package/dist/vitest.js +421 -81
- package/dist/vitest.js.map +1 -1
- package/package.json +41 -4
- package/panel/panel.css +49 -0
- package/panel/panel.js +871 -353
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { R as RenderReport, O as Options } from './types-DwNN5cWe.js';
|
|
2
|
+
import { C as Collector } from './notifiers-CqQW0Eum.js';
|
|
3
|
+
|
|
4
|
+
/** Environment variable carrying the path of the run's JSONL file from the reporter to the workers. */
|
|
5
|
+
declare const OUT_ENV = "RERENDER_LENS_OUT";
|
|
6
|
+
interface SetupOptions extends Omit<Options, 'notifier'> {
|
|
7
|
+
/** Also fail each test file that recorded avoidable re-renders (default false: the reporter decides). */
|
|
8
|
+
failFast?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** How the setup ties into the test runner (`{ afterAll }` from the runner's globals). */
|
|
11
|
+
interface SetupHooks {
|
|
12
|
+
afterAll?: (fn: () => void) => void;
|
|
13
|
+
/** Path of the current test file, for the reporter's output. */
|
|
14
|
+
testPath?: () => string | undefined;
|
|
15
|
+
}
|
|
16
|
+
interface LensSetup {
|
|
17
|
+
collector: Collector;
|
|
18
|
+
/** Append this file's reports to the shared output and clear the collector; registered with `afterAll` when given. */
|
|
19
|
+
flush(): void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Call from a setup file. Creates the DevTools hook before react-dom loads, starts the library with
|
|
23
|
+
* a collector, and appends this file's reports to the shared output when it ends. The collector is
|
|
24
|
+
* returned for per-test assertions (`assertNoAvoidable`, `assertWithinBudget`).
|
|
25
|
+
*/
|
|
26
|
+
declare function setupRerenderLens(options?: SetupOptions, hooks?: SetupHooks): LensSetup;
|
|
27
|
+
interface ReporterOptions {
|
|
28
|
+
/** Path to a budget JSON (`{ "*": 0, "Row": 2 }`). When set, the run fails on violations. */
|
|
29
|
+
budget?: string;
|
|
30
|
+
/** Print at most this many fixes. Default 10. */
|
|
31
|
+
limit?: number;
|
|
32
|
+
/** Also write the collected reports here as a panel-compatible export (`Sessions > Import`). */
|
|
33
|
+
exportTo?: string;
|
|
34
|
+
/** Where the lines go. Default `process.stdout`. */
|
|
35
|
+
write?: (line: string) => void;
|
|
36
|
+
}
|
|
37
|
+
interface RunResult {
|
|
38
|
+
/** Every report of the run, from every worker. */
|
|
39
|
+
reports: RenderReport[];
|
|
40
|
+
/** Set when a budget file was given and exceeded; the run must fail. */
|
|
41
|
+
error: Error | null;
|
|
42
|
+
}
|
|
43
|
+
/** Reports from every worker of a run. */
|
|
44
|
+
declare function readRun(file: string): RenderReport[];
|
|
45
|
+
|
|
46
|
+
export { type LensSetup as L, OUT_ENV as O, type ReporterOptions as R, type SetupHooks as S, type RunResult as a, type SetupOptions as b, readRun as r, setupRerenderLens as s };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { R as RenderReport, O as Options } from './types-DwNN5cWe.cjs';
|
|
2
|
+
import { C as Collector } from './notifiers-B5poAEvE.cjs';
|
|
3
|
+
|
|
4
|
+
/** Environment variable carrying the path of the run's JSONL file from the reporter to the workers. */
|
|
5
|
+
declare const OUT_ENV = "RERENDER_LENS_OUT";
|
|
6
|
+
interface SetupOptions extends Omit<Options, 'notifier'> {
|
|
7
|
+
/** Also fail each test file that recorded avoidable re-renders (default false: the reporter decides). */
|
|
8
|
+
failFast?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** How the setup ties into the test runner (`{ afterAll }` from the runner's globals). */
|
|
11
|
+
interface SetupHooks {
|
|
12
|
+
afterAll?: (fn: () => void) => void;
|
|
13
|
+
/** Path of the current test file, for the reporter's output. */
|
|
14
|
+
testPath?: () => string | undefined;
|
|
15
|
+
}
|
|
16
|
+
interface LensSetup {
|
|
17
|
+
collector: Collector;
|
|
18
|
+
/** Append this file's reports to the shared output and clear the collector; registered with `afterAll` when given. */
|
|
19
|
+
flush(): void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Call from a setup file. Creates the DevTools hook before react-dom loads, starts the library with
|
|
23
|
+
* a collector, and appends this file's reports to the shared output when it ends. The collector is
|
|
24
|
+
* returned for per-test assertions (`assertNoAvoidable`, `assertWithinBudget`).
|
|
25
|
+
*/
|
|
26
|
+
declare function setupRerenderLens(options?: SetupOptions, hooks?: SetupHooks): LensSetup;
|
|
27
|
+
interface ReporterOptions {
|
|
28
|
+
/** Path to a budget JSON (`{ "*": 0, "Row": 2 }`). When set, the run fails on violations. */
|
|
29
|
+
budget?: string;
|
|
30
|
+
/** Print at most this many fixes. Default 10. */
|
|
31
|
+
limit?: number;
|
|
32
|
+
/** Also write the collected reports here as a panel-compatible export (`Sessions > Import`). */
|
|
33
|
+
exportTo?: string;
|
|
34
|
+
/** Where the lines go. Default `process.stdout`. */
|
|
35
|
+
write?: (line: string) => void;
|
|
36
|
+
}
|
|
37
|
+
interface RunResult {
|
|
38
|
+
/** Every report of the run, from every worker. */
|
|
39
|
+
reports: RenderReport[];
|
|
40
|
+
/** Set when a budget file was given and exceeded; the run must fail. */
|
|
41
|
+
error: Error | null;
|
|
42
|
+
}
|
|
43
|
+
/** Reports from every worker of a run. */
|
|
44
|
+
declare function readRun(file: string): RenderReport[];
|
|
45
|
+
|
|
46
|
+
export { type LensSetup as L, OUT_ENV as O, type ReporterOptions as R, type SetupHooks as S, type RunResult as a, type SetupOptions as b, readRun as r, setupRerenderLens as s };
|
package/dist/setup.cjs
CHANGED
|
@@ -161,30 +161,30 @@ function diffRecords(prev, next, basePath = "") {
|
|
|
161
161
|
if (Object.is(a, b)) continue;
|
|
162
162
|
const kind = classify(a, b);
|
|
163
163
|
if (kind === "different") {
|
|
164
|
-
changes.push({ path: diffBudgetExhausted() ? path : firstDifferentPath(a, b, path), kind, prev: a, next: b });
|
|
164
|
+
changes.push({ path: diffBudgetExhausted() ? path : firstDifferentPath(a, b, path) ?? path, kind, prev: a, next: b });
|
|
165
165
|
} else {
|
|
166
166
|
changes.push({ path, kind, prev: a, next: b });
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
return changes;
|
|
170
170
|
}
|
|
171
|
-
function firstDifferentPath(a, b, path, depth = 0) {
|
|
172
|
-
if (depth > 8) return path;
|
|
171
|
+
function firstDifferentPath(a, b, path = "", depth = 0) {
|
|
172
|
+
if (depth > 8) return path || "(value)";
|
|
173
173
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
174
|
-
if (a.length !== b.length) return `${path}.length
|
|
174
|
+
if (a.length !== b.length) return path ? `${path}.length` : "length";
|
|
175
175
|
for (let i = 0; i < a.length; i++) {
|
|
176
176
|
if (!deepEqual(a[i], b[i])) return firstDifferentPath(a[i], b[i], joinPath(path, i), depth + 1);
|
|
177
177
|
}
|
|
178
|
-
return
|
|
178
|
+
return null;
|
|
179
179
|
}
|
|
180
180
|
if (isPlainObject(a) && isPlainObject(b)) {
|
|
181
181
|
const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
182
182
|
for (const k of keys) {
|
|
183
183
|
if (!deepEqual(a[k], b[k])) return firstDifferentPath(a[k], b[k], joinPath(path, k), depth + 1);
|
|
184
184
|
}
|
|
185
|
-
return
|
|
185
|
+
return null;
|
|
186
186
|
}
|
|
187
|
-
return path;
|
|
187
|
+
return deepEqual(a, b) ? null : path || "(value)";
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
// src/report.ts
|
|
@@ -202,10 +202,12 @@ function storeAdvice(custom) {
|
|
|
202
202
|
}
|
|
203
203
|
var isGenuine = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
|
|
204
204
|
var isChildren = (change) => change.path === "children";
|
|
205
|
+
var isRefObject = (change) => change.path === "ref" && change.kind === "deep-equal";
|
|
205
206
|
function fixFor(change) {
|
|
206
207
|
if (isChildren(change) && (change.kind === "element" || change.kind === "deep-equal")) {
|
|
207
208
|
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`;
|
|
208
209
|
}
|
|
210
|
+
if (isRefObject(change)) return `create the ref once with useRef (or createRef outside the render) and pass the same object every time`;
|
|
209
211
|
switch (change.kind) {
|
|
210
212
|
case "function":
|
|
211
213
|
return `wrap it in useCallback (or hoist it out of the parent's render)`;
|
|
@@ -221,6 +223,7 @@ function describe(change) {
|
|
|
221
223
|
if (isChildren(change) && (change.kind === "element" || change.kind === "deep-equal")) {
|
|
222
224
|
return `children are new React elements with the same types and props on every render of the parent`;
|
|
223
225
|
}
|
|
226
|
+
if (isRefObject(change)) return `prop "ref" is a new ref object on every render (React re-attaches it each time)`;
|
|
224
227
|
switch (change.kind) {
|
|
225
228
|
case "deep-equal":
|
|
226
229
|
return `prop "${change.path}" is a new reference but deep-equal to the previous value`;
|
|
@@ -248,11 +251,21 @@ function buildReport(input) {
|
|
|
248
251
|
if (causes.length === 0) trigger = "parent";
|
|
249
252
|
else if (causes.length === 1) trigger = causes[0];
|
|
250
253
|
else trigger = "mixed";
|
|
251
|
-
const
|
|
254
|
+
const nothingChanged = input.propChanges.length === 0 && stateChanges.length === 0 && hookChanges.length === 0;
|
|
255
|
+
const cacheHit = input.compiled === true && trigger === "parent" && input.propChanges.length === 0;
|
|
256
|
+
const revealed = input.revealed === true && trigger === "parent";
|
|
257
|
+
const avoidable = trigger === "parent" && !cacheHit && !revealed;
|
|
252
258
|
const reasons = [];
|
|
253
|
-
|
|
259
|
+
const memoFix = input.classComponent ? `Make "${input.component}" extend PureComponent (or implement shouldComponentUpdate)` : `Wrap "${input.component}" in React.memo (or extend PureComponent)`;
|
|
260
|
+
if (cacheHit) {
|
|
261
|
+
reasons.push(`compiled by React Compiler: its output is memoized and the inputs did not change, so this render is cheap; nothing to fix.`);
|
|
262
|
+
} else if (revealed) {
|
|
263
|
+
reasons.push(
|
|
264
|
+
`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.`
|
|
265
|
+
);
|
|
266
|
+
} else if (avoidable && nothingChanged) {
|
|
254
267
|
const who = input.parent ? `<${input.parent.name}> re-rendered (${describeTrigger(input.parent.trigger)})` : "its parent re-rendered";
|
|
255
|
-
reasons.push(`re-rendered with identical props because ${who}.
|
|
268
|
+
reasons.push(`re-rendered with identical props because ${who}. ${memoFix}.`);
|
|
256
269
|
} else if (avoidable && input.parent) {
|
|
257
270
|
reasons.push(`caused by <${input.parent.name}> re-rendering (${describeTrigger(input.parent.trigger)}).`);
|
|
258
271
|
}
|
|
@@ -261,9 +274,9 @@ function buildReport(input) {
|
|
|
261
274
|
reasons.push(fix ? `${describe(c)}: ${fix}.` : `${describe(c)}.`);
|
|
262
275
|
}
|
|
263
276
|
const memoized = input.memoized !== false;
|
|
264
|
-
if (avoidable && !memoized && input.propChanges.length > 0) {
|
|
277
|
+
if (avoidable && !memoized && !input.compiled && input.propChanges.length > 0) {
|
|
265
278
|
reasons.push(
|
|
266
|
-
`"${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).`
|
|
279
|
+
`"${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)"}.`
|
|
267
280
|
);
|
|
268
281
|
}
|
|
269
282
|
for (const c of stateChanges) {
|
|
@@ -275,6 +288,8 @@ function buildReport(input) {
|
|
|
275
288
|
const where = c.provider && c.provider.component ? ` (provided by <${c.provider.component}>)` : "";
|
|
276
289
|
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` : "";
|
|
277
290
|
reasons.push(`${c.path} changed${where}${keys}.`);
|
|
291
|
+
} else if (c.hook === "useDeferredValue" && isGenuine(c)) {
|
|
292
|
+
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.`);
|
|
278
293
|
} else if (isGenuine(c)) reasons.push(`${hookLabel(c)} changed.`);
|
|
279
294
|
else if (isStateHook(c))
|
|
280
295
|
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.`);
|
|
@@ -305,6 +320,7 @@ function buildReport(input) {
|
|
|
305
320
|
reasons,
|
|
306
321
|
time: now()
|
|
307
322
|
};
|
|
323
|
+
if (input.compiled) report.compiled = true;
|
|
308
324
|
if (input.selfDuration !== void 0) report.selfDuration = input.selfDuration;
|
|
309
325
|
if (input.treeDuration !== void 0) report.treeDuration = input.treeDuration;
|
|
310
326
|
if (input.commitPriority) report.commitPriority = input.commitPriority;
|
|
@@ -342,10 +358,10 @@ var KIND_LABEL = {
|
|
|
342
358
|
};
|
|
343
359
|
function summarize(report) {
|
|
344
360
|
const counts2 = /* @__PURE__ */ new Map();
|
|
345
|
-
for (const c of [...report.propChanges, ...report.stateChanges, ...report.hookChanges]) {
|
|
361
|
+
for (const c of [...report.propChanges || [], ...report.stateChanges || [], ...report.hookChanges || []]) {
|
|
346
362
|
counts2.set(c.kind, (counts2.get(c.kind) ?? 0) + 1);
|
|
347
363
|
}
|
|
348
|
-
const parts = [...counts2].map(([kind, n]) => `${n} ${KIND_LABEL[kind]}`);
|
|
364
|
+
const parts = [...counts2].map(([kind, n]) => `${n} ${KIND_LABEL[kind] || kind}`);
|
|
349
365
|
if (parts.length === 0) parts.push("no changes");
|
|
350
366
|
return parts.join(", ");
|
|
351
367
|
}
|
|
@@ -367,16 +383,27 @@ function printReport(report, options) {
|
|
|
367
383
|
}
|
|
368
384
|
|
|
369
385
|
// src/state.ts
|
|
386
|
+
var MAX_SEEN_COMPONENTS = 500;
|
|
370
387
|
var KEY = /* @__PURE__ */ Symbol.for("rerender-lens.state");
|
|
371
388
|
function getState() {
|
|
372
389
|
const g = globalThis;
|
|
373
390
|
let s = g[KEY];
|
|
374
391
|
if (!s) {
|
|
375
|
-
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 };
|
|
392
|
+
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, seen: /* @__PURE__ */ new Set(), seenTracked: /* @__PURE__ */ new Set(), seenOverflow: false };
|
|
376
393
|
g[KEY] = s;
|
|
377
394
|
}
|
|
378
395
|
return s;
|
|
379
396
|
}
|
|
397
|
+
function noteRendered(s, name, tracked) {
|
|
398
|
+
if (!s.seen.has(name)) {
|
|
399
|
+
if (s.seen.size >= MAX_SEEN_COMPONENTS) {
|
|
400
|
+
s.seenOverflow = true;
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
s.seen.add(name);
|
|
404
|
+
}
|
|
405
|
+
if (tracked) s.seenTracked.add(name);
|
|
406
|
+
}
|
|
380
407
|
function warnOnce(key, message) {
|
|
381
408
|
const s = getState();
|
|
382
409
|
if (s.warnedOnce.has(key)) return;
|
|
@@ -408,6 +435,27 @@ function dispatch(report, override) {
|
|
|
408
435
|
}
|
|
409
436
|
|
|
410
437
|
// src/hookNames.ts
|
|
438
|
+
var NODE_COUNT = {
|
|
439
|
+
useState: 1,
|
|
440
|
+
useReducer: 1,
|
|
441
|
+
useRef: 1,
|
|
442
|
+
useMemo: 1,
|
|
443
|
+
useCallback: 1,
|
|
444
|
+
useEffect: 1,
|
|
445
|
+
useLayoutEffect: 1,
|
|
446
|
+
useInsertionEffect: 1,
|
|
447
|
+
useImperativeHandle: 1,
|
|
448
|
+
useSyncExternalStore: 2,
|
|
449
|
+
useTransition: 2,
|
|
450
|
+
useDeferredValue: 1,
|
|
451
|
+
useId: 1,
|
|
452
|
+
useOptimistic: 1,
|
|
453
|
+
useActionState: 3,
|
|
454
|
+
useFormState: 3,
|
|
455
|
+
useEffectEvent: 1,
|
|
456
|
+
useCacheRefresh: 1
|
|
457
|
+
};
|
|
458
|
+
var BUILTIN_HOOKS = /* @__PURE__ */ new Set([...Object.keys(NODE_COUNT), "useContext", "useDebugValue", "use"]);
|
|
411
459
|
var noop = () => {
|
|
412
460
|
};
|
|
413
461
|
function customHooksFromStack(stack, componentName, marker) {
|
|
@@ -422,7 +470,7 @@ function customHooksFromStack(stack, componentName, marker) {
|
|
|
422
470
|
continue;
|
|
423
471
|
}
|
|
424
472
|
if (name === componentName || name.endsWith("." + componentName)) break;
|
|
425
|
-
if (/^use[A-Z0-9_]/.test(name)) names.push(name);
|
|
473
|
+
if (/^use[A-Z0-9_]/.test(name) && !BUILTIN_HOOKS.has(name)) names.push(name);
|
|
426
474
|
if (names.length > 8) break;
|
|
427
475
|
}
|
|
428
476
|
return names;
|
|
@@ -722,6 +770,11 @@ function isMemoizedFiber(fiber) {
|
|
|
722
770
|
}
|
|
723
771
|
return false;
|
|
724
772
|
}
|
|
773
|
+
function isCompiledFiber(fiber) {
|
|
774
|
+
if (fiber.tag === ClassComponent) return false;
|
|
775
|
+
const cache2 = fiber.updateQueue?.memoCache;
|
|
776
|
+
return !!cache2 && Array.isArray(cache2.data);
|
|
777
|
+
}
|
|
725
778
|
function durationsOf(fiber) {
|
|
726
779
|
if (typeof fiber.actualDuration !== "number") return null;
|
|
727
780
|
const tree = fiber.actualDuration;
|
|
@@ -900,7 +953,8 @@ function diffHooks(fiber, alt) {
|
|
|
900
953
|
const labels = hookLabelsFor(fiber, b);
|
|
901
954
|
let i = 0;
|
|
902
955
|
while (a && b) {
|
|
903
|
-
|
|
956
|
+
const deferred = labels?.[i] === "useDeferredValue";
|
|
957
|
+
if ((deferred || isStateNode(b) || isStoreNode(b)) && !Object.is(a.memoizedState, b.memoizedState)) {
|
|
904
958
|
const hook = labels?.[i] ?? hookLabel2(b);
|
|
905
959
|
out.push({
|
|
906
960
|
path: `${hook}#${i}`,
|
|
@@ -934,6 +988,10 @@ function diffContexts(fiber, alt) {
|
|
|
934
988
|
let a = alt.dependencies?.firstContext ?? null;
|
|
935
989
|
let b = fiber.dependencies?.firstContext ?? null;
|
|
936
990
|
let i = 0;
|
|
991
|
+
if (b && !("memoizedValue" in b)) {
|
|
992
|
+
warnOnce("contextValues", "this React does not record context values on the fiber (React < 18): context changes are reported as plain parent re-renders.");
|
|
993
|
+
return out;
|
|
994
|
+
}
|
|
937
995
|
while (a && b) {
|
|
938
996
|
if (a.context === b.context && !Object.is(a.memoizedValue, b.memoizedValue)) {
|
|
939
997
|
const name = b.context.displayName ?? "Context";
|
|
@@ -966,10 +1024,26 @@ function diffContexts(fiber, alt) {
|
|
|
966
1024
|
return out;
|
|
967
1025
|
}
|
|
968
1026
|
var isGenuine2 = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
|
|
1027
|
+
var isRefObject2 = (v) => {
|
|
1028
|
+
if (typeof v !== "object" || v === null) return false;
|
|
1029
|
+
const keys = Object.keys(v);
|
|
1030
|
+
return keys.length === 1 && keys[0] === "current";
|
|
1031
|
+
};
|
|
1032
|
+
function diffProps(prev, next) {
|
|
1033
|
+
if (prev === next) return [];
|
|
1034
|
+
const pr = prev.ref;
|
|
1035
|
+
const nr = next.ref;
|
|
1036
|
+
if (pr !== nr && isRefObject2(pr) && isRefObject2(nr)) {
|
|
1037
|
+
const { ref: _p, ...restPrev } = prev;
|
|
1038
|
+
const { ref: _n, ...restNext } = next;
|
|
1039
|
+
return [{ path: "ref", kind: "deep-equal", prev: pr, next: nr }, ...diffRecords(restPrev, restNext)];
|
|
1040
|
+
}
|
|
1041
|
+
return diffRecords(prev, next);
|
|
1042
|
+
}
|
|
969
1043
|
function analyze(fiber, alt, trackHooks) {
|
|
970
1044
|
const prevProps = alt.memoizedProps ?? {};
|
|
971
1045
|
const nextProps = fiber.memoizedProps ?? {};
|
|
972
|
-
const propChanges =
|
|
1046
|
+
const propChanges = diffProps(prevProps, nextProps);
|
|
973
1047
|
let stateChanges = [];
|
|
974
1048
|
let hookChanges = [];
|
|
975
1049
|
if (fiber.tag === ClassComponent) {
|
|
@@ -982,7 +1056,7 @@ function analyze(fiber, alt, trackHooks) {
|
|
|
982
1056
|
} else if (trackHooks) {
|
|
983
1057
|
hookChanges = [...diffHooks(fiber, alt), ...diffContexts(fiber, alt)];
|
|
984
1058
|
}
|
|
985
|
-
const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore";
|
|
1059
|
+
const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore" && h.hook !== "useDeferredValue";
|
|
986
1060
|
const causes = [];
|
|
987
1061
|
if (propChanges.some(isGenuine2)) causes.push("props");
|
|
988
1062
|
if (stateChanges.some(isGenuine2) || hookChanges.some((h) => isStateHook(h) && isGenuine2(h))) causes.push("state");
|
|
@@ -1022,6 +1096,24 @@ var lastCommit = null;
|
|
|
1022
1096
|
var EFFECT_LOOP_WINDOW_MS = 50;
|
|
1023
1097
|
var MAX_REPORTS_PER_COMMIT = 200;
|
|
1024
1098
|
var COMMIT_TIME_BUDGET_MS = 25;
|
|
1099
|
+
function isDeferredCatchUp(fiber) {
|
|
1100
|
+
const types = fiber._debugHookTypes;
|
|
1101
|
+
if (!types || !types.includes("useDeferredValue") || !fiber.alternate) return false;
|
|
1102
|
+
const first = fiber.memoizedState;
|
|
1103
|
+
if (!isHookList(first)) return false;
|
|
1104
|
+
const labels = hookLabelsFor(fiber, first);
|
|
1105
|
+
if (!labels) return false;
|
|
1106
|
+
let a = fiber.alternate.memoizedState;
|
|
1107
|
+
let b = first;
|
|
1108
|
+
for (let i = 0; a && b; a = a.next, b = b.next, i++) {
|
|
1109
|
+
if (labels[i] === "useDeferredValue" && !Object.is(a.memoizedState, b.memoizedState)) return true;
|
|
1110
|
+
}
|
|
1111
|
+
return false;
|
|
1112
|
+
}
|
|
1113
|
+
function isUnder(fiber, boundaries) {
|
|
1114
|
+
for (let f = fiber.return; f && f.tag !== HostRoot; f = f.return) if (f.tag === SuspenseComponent && boundaries.includes(f)) return true;
|
|
1115
|
+
return false;
|
|
1116
|
+
}
|
|
1025
1117
|
function onCommit(root, commitPriority) {
|
|
1026
1118
|
const s = getState();
|
|
1027
1119
|
if (!s.enabled) return;
|
|
@@ -1031,17 +1123,20 @@ function onCommit(root, commitPriority) {
|
|
|
1031
1123
|
const rendered = [];
|
|
1032
1124
|
const renderedNames = /* @__PURE__ */ new Set();
|
|
1033
1125
|
let hot = false;
|
|
1034
|
-
let
|
|
1126
|
+
let resolvedBoundaries = null;
|
|
1035
1127
|
const stack = [root.current];
|
|
1036
1128
|
while (stack.length) {
|
|
1037
1129
|
const fiber = stack.pop();
|
|
1038
1130
|
const alt = fiber.alternate;
|
|
1039
1131
|
if (alt && isComponentTag(fiber.tag) && didRender(fiber)) {
|
|
1040
1132
|
if (isHotSwapped(fiber, alt)) hot = true;
|
|
1041
|
-
|
|
1042
|
-
|
|
1133
|
+
const name = fiberName(fiber);
|
|
1134
|
+
renderedNames.add(name);
|
|
1135
|
+
const tracked = shouldTrack(fiberType(fiber), o);
|
|
1136
|
+
noteRendered(s, name, tracked);
|
|
1137
|
+
if (tracked) rendered.push(fiber);
|
|
1043
1138
|
}
|
|
1044
|
-
if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null)
|
|
1139
|
+
if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null) (resolvedBoundaries ?? (resolvedBoundaries = [])).push(fiber);
|
|
1045
1140
|
if (!alt || fiber.child !== alt.child) {
|
|
1046
1141
|
for (let c = fiber.child; c; c = c.sibling) stack.push(c);
|
|
1047
1142
|
}
|
|
@@ -1053,10 +1148,10 @@ function onCommit(root, commitPriority) {
|
|
|
1053
1148
|
let commitCause;
|
|
1054
1149
|
let afterCommit;
|
|
1055
1150
|
const inputDriven = commitPriority === "immediate" || commitPriority === "user-blocking";
|
|
1056
|
-
if (!inputDriven && lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u))) {
|
|
1151
|
+
if (!inputDriven && lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u)) && !rendered.some(isDeferredCatchUp)) {
|
|
1057
1152
|
commitCause = "effect-after-commit";
|
|
1058
1153
|
afterCommit = lastCommit.id;
|
|
1059
|
-
} else if (
|
|
1154
|
+
} else if (resolvedBoundaries) commitCause = "suspense-resolved";
|
|
1060
1155
|
if (rendered.length === 0) {
|
|
1061
1156
|
lastCommit = { id: s.nextCommitId, at, rendered: renderedNames };
|
|
1062
1157
|
return;
|
|
@@ -1108,6 +1203,9 @@ function onCommit(root, commitPriority) {
|
|
|
1108
1203
|
owner: ownerName(fiber),
|
|
1109
1204
|
path: componentPath(fiber),
|
|
1110
1205
|
memoized: isMemoizedFiber(fiber),
|
|
1206
|
+
compiled: isCompiledFiber(fiber),
|
|
1207
|
+
classComponent: fiber.tag === ClassComponent,
|
|
1208
|
+
revealed: resolvedBoundaries !== null && a.trigger === "parent" && isUnder(fiber, resolvedBoundaries),
|
|
1111
1209
|
selfDuration: durations ? durations.self : void 0,
|
|
1112
1210
|
treeDuration: durations ? durations.tree : void 0,
|
|
1113
1211
|
commitId,
|
|
@@ -1161,10 +1259,53 @@ function shouldTrack(type, o) {
|
|
|
1161
1259
|
if (o.trackAllMemoized) return isMemo(type) || isPureClass(type);
|
|
1162
1260
|
return false;
|
|
1163
1261
|
}
|
|
1262
|
+
var ANONYMOUS = "Anonymous";
|
|
1263
|
+
function explainTracking(type, o) {
|
|
1264
|
+
if (typeof type === "string")
|
|
1265
|
+
return { tracked: false, name: type, memoized: false, reason: `<${type}> is a DOM element, not a React component; only components are tracked.` };
|
|
1266
|
+
if (!isComponentLike(type))
|
|
1267
|
+
return { tracked: false, name: ANONYMOUS, memoized: false, reason: "This is not a React component type, so it is never tracked." };
|
|
1268
|
+
const name = getDisplayName(type);
|
|
1269
|
+
const memo = isMemo(type);
|
|
1270
|
+
const memoized = memo || isPureClass(type);
|
|
1271
|
+
if (o.exclude?.some((m) => matches(m, name)))
|
|
1272
|
+
return { tracked: false, name, memoized, reason: `<${name}> matches "exclude", which wins over every other rule.`, fix: `Remove "${name}" from the exclude list.` };
|
|
1273
|
+
if (hasMarker(type)) return { tracked: true, name, memoized, reason: `<${name}> is marked with track() (the ${MARKER} static).` };
|
|
1274
|
+
if (o.include?.some((m) => matches(m, name))) return { tracked: true, name, memoized, reason: `<${name}> matches "include".` };
|
|
1275
|
+
if (o.trackAllComponents) return { tracked: true, name, memoized, reason: '"Track every component" is on, so every component is tracked.' };
|
|
1276
|
+
if (o.trackAllMemoized && memoized)
|
|
1277
|
+
return { tracked: true, name, memoized, reason: `<${name}> is ${memo ? "a React.memo component" : "a PureComponent"}, and "Track every React.memo and PureComponent" is on.` };
|
|
1278
|
+
if (name === ANONYMOUS)
|
|
1279
|
+
return {
|
|
1280
|
+
tracked: false,
|
|
1281
|
+
name,
|
|
1282
|
+
memoized,
|
|
1283
|
+
reason: 'This component has no display name, so "include" and "exclude" cannot address it.',
|
|
1284
|
+
fix: "Give the function a name, set displayName, or mark it with track(fn, 'Name')."
|
|
1285
|
+
};
|
|
1286
|
+
if (o.trackAllMemoized)
|
|
1287
|
+
return {
|
|
1288
|
+
tracked: false,
|
|
1289
|
+
name,
|
|
1290
|
+
memoized,
|
|
1291
|
+
reason: `"Track every React.memo and PureComponent" only covers React.memo components and PureComponent classes, and <${name}> is neither.`,
|
|
1292
|
+
fix: `Add "${name}" to include, wrap it in React.memo, or turn on "Track every component".`
|
|
1293
|
+
};
|
|
1294
|
+
return {
|
|
1295
|
+
tracked: false,
|
|
1296
|
+
name,
|
|
1297
|
+
memoized,
|
|
1298
|
+
reason: `Nothing selects <${name}>: it is not marked with track(), no "include" pattern matches it, and neither "track every" option is on.`,
|
|
1299
|
+
fix: `Add "${name}" to include, or turn on "Track every React.memo and PureComponent" / "Track every component".`
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1164
1302
|
function init(options = {}) {
|
|
1165
1303
|
const s = getState();
|
|
1166
1304
|
s.options = { ...options };
|
|
1167
1305
|
s.printed.clear();
|
|
1306
|
+
s.seen.clear();
|
|
1307
|
+
s.seenTracked.clear();
|
|
1308
|
+
s.seenOverflow = false;
|
|
1168
1309
|
if (!s.enabled) {
|
|
1169
1310
|
s.detach = attach();
|
|
1170
1311
|
s.enabled = true;
|
|
@@ -1290,7 +1431,7 @@ function flash(target, duration = 500) {
|
|
|
1290
1431
|
}
|
|
1291
1432
|
|
|
1292
1433
|
// src/version.ts
|
|
1293
|
-
var VERSION = "0.
|
|
1434
|
+
var VERSION = "0.7.0" ;
|
|
1294
1435
|
|
|
1295
1436
|
// src/devtools.ts
|
|
1296
1437
|
var DEVTOOLS_MARKER = "__rerenderLens";
|
|
@@ -1416,6 +1557,18 @@ function createDevtoolsNotifier(options = {}) {
|
|
|
1416
1557
|
target.postMessage(msg, "*");
|
|
1417
1558
|
};
|
|
1418
1559
|
const source = options.source ?? "page";
|
|
1560
|
+
const trackingSummary = () => {
|
|
1561
|
+
const s = getState();
|
|
1562
|
+
const o = s.options;
|
|
1563
|
+
return {
|
|
1564
|
+
mode: o.trackAllComponents ? "all" : o.trackAllMemoized ? "memoized" : "marked",
|
|
1565
|
+
include: (o.include ?? []).map(matcherToString).filter((x) => x !== null),
|
|
1566
|
+
exclude: (o.exclude ?? []).map(matcherToString).filter((x) => x !== null),
|
|
1567
|
+
renderedCount: s.seen.size,
|
|
1568
|
+
trackedCount: s.seenTracked.size,
|
|
1569
|
+
overflow: s.seenOverflow
|
|
1570
|
+
};
|
|
1571
|
+
};
|
|
1419
1572
|
const info = () => ({
|
|
1420
1573
|
count: buffer.length,
|
|
1421
1574
|
library: VERSION,
|
|
@@ -1429,7 +1582,8 @@ function createDevtoolsNotifier(options = {}) {
|
|
|
1429
1582
|
scheduled: getState().scheduled,
|
|
1430
1583
|
commits: getState().commits,
|
|
1431
1584
|
overhead: { totalMs: getState().overheadMs, maxCommitMs: getState().maxCommitMs },
|
|
1432
|
-
truncated: getState().truncated
|
|
1585
|
+
truncated: getState().truncated,
|
|
1586
|
+
tracking: trackingSummary()
|
|
1433
1587
|
});
|
|
1434
1588
|
const bridge = {
|
|
1435
1589
|
replay: () => {
|
|
@@ -1475,6 +1629,10 @@ function createDevtoolsNotifier(options = {}) {
|
|
|
1475
1629
|
flashAvoidable: (on) => {
|
|
1476
1630
|
flashOn = !!on;
|
|
1477
1631
|
},
|
|
1632
|
+
explain: (target2) => {
|
|
1633
|
+
const comp = typeof target2 === "number" ? fiberById(target2) : nearestComponent(fiberForNode(target2));
|
|
1634
|
+
return comp ? explainTracking(fiberType(comp), getState().options) : null;
|
|
1635
|
+
},
|
|
1478
1636
|
inspect: (node) => {
|
|
1479
1637
|
const comp = nearestComponent(fiberForNode(node));
|
|
1480
1638
|
if (!comp) return null;
|
|
@@ -1489,6 +1647,7 @@ function createDevtoolsNotifier(options = {}) {
|
|
|
1489
1647
|
component: fiberName(comp),
|
|
1490
1648
|
instanceId: id,
|
|
1491
1649
|
tracked: shouldTrack(fiberType(comp), getState().options),
|
|
1650
|
+
tracking: explainTracking(fiberType(comp), getState().options),
|
|
1492
1651
|
path,
|
|
1493
1652
|
reports: id === null ? [] : buffer.filter((e) => e.instanceId === id).slice(-20).map((e) => e.payload)
|
|
1494
1653
|
};
|
|
@@ -1523,6 +1682,9 @@ function createDevtoolsNotifier(options = {}) {
|
|
|
1523
1682
|
bridge.flashAvoidable(!!data.arg);
|
|
1524
1683
|
reply.result = true;
|
|
1525
1684
|
break;
|
|
1685
|
+
case "explain":
|
|
1686
|
+
reply.result = bridge.explain(data.arg);
|
|
1687
|
+
break;
|
|
1526
1688
|
default:
|
|
1527
1689
|
reply.error = `unknown command ${String(data.cmd)}`;
|
|
1528
1690
|
}
|
|
@@ -1531,6 +1693,7 @@ function createDevtoolsNotifier(options = {}) {
|
|
|
1531
1693
|
}
|
|
1532
1694
|
return reply;
|
|
1533
1695
|
};
|
|
1696
|
+
const isRelayGreeting = (data) => !!data && typeof data === "object" && data.__rerenderLensRelay === true && typeof data.app === "string";
|
|
1534
1697
|
const isCommand = (data) => !!data && typeof data === "object" && data.__rerenderLensCmd === true && typeof data.id === "string";
|
|
1535
1698
|
const envelope = (type, payload) => ({ [DEVTOOLS_MARKER]: true, version: PROTOCOL_VERSION, type, payload });
|
|
1536
1699
|
const sinks = [];
|
|
@@ -1553,14 +1716,20 @@ function createDevtoolsNotifier(options = {}) {
|
|
|
1553
1716
|
const relayUrl = options.relay ?? (typeof window !== "undefined" ? window.__RERENDER_LENS_RELAY__ : void 0);
|
|
1554
1717
|
const ES = options.eventSource ?? (typeof EventSource === "function" ? EventSource : null);
|
|
1555
1718
|
if (relayUrl && ES && typeof fetch === "function") {
|
|
1556
|
-
const
|
|
1719
|
+
const [rawBase, relayQuery = ""] = relayUrl.split("?");
|
|
1720
|
+
const base = (rawBase ?? "").replace(/\/$/, "");
|
|
1721
|
+
const relayToken = new URLSearchParams(relayQuery).get("token");
|
|
1722
|
+
const auth = relayToken ? `token=${encodeURIComponent(relayToken)}` : "";
|
|
1557
1723
|
let queue = [];
|
|
1558
1724
|
let scheduled = false;
|
|
1725
|
+
let appId = null;
|
|
1559
1726
|
const flush = () => {
|
|
1560
1727
|
scheduled = false;
|
|
1561
1728
|
const batch = queue;
|
|
1562
1729
|
queue = [];
|
|
1563
|
-
|
|
1730
|
+
const params = [appId ? `app=${encodeURIComponent(appId)}` : "", auth].filter(Boolean).join("&");
|
|
1731
|
+
const to = `${base}/message${params ? `?${params}` : ""}`;
|
|
1732
|
+
fetch(to, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(batch), keepalive: true }).catch(() => {
|
|
1564
1733
|
});
|
|
1565
1734
|
};
|
|
1566
1735
|
const relaySend = (message) => {
|
|
@@ -1571,7 +1740,8 @@ function createDevtoolsNotifier(options = {}) {
|
|
|
1571
1740
|
}
|
|
1572
1741
|
};
|
|
1573
1742
|
try {
|
|
1574
|
-
const
|
|
1743
|
+
const label = typeof location !== "undefined" ? `${location.host}${location.pathname}`.slice(0, 120) : "";
|
|
1744
|
+
const stream = new ES(`${base}/events?role=app${label ? `&label=${encodeURIComponent(label)}` : ""}${auth ? `&${auth}` : ""}`);
|
|
1575
1745
|
stream.onmessage = (event) => {
|
|
1576
1746
|
let parsed;
|
|
1577
1747
|
try {
|
|
@@ -1579,7 +1749,10 @@ function createDevtoolsNotifier(options = {}) {
|
|
|
1579
1749
|
} catch {
|
|
1580
1750
|
return;
|
|
1581
1751
|
}
|
|
1582
|
-
for (const m of Array.isArray(parsed) ? parsed : [parsed])
|
|
1752
|
+
for (const m of Array.isArray(parsed) ? parsed : [parsed]) {
|
|
1753
|
+
if (isRelayGreeting(m)) appId = m.app;
|
|
1754
|
+
else if (isCommand(m)) relaySend(runCommand(m));
|
|
1755
|
+
}
|
|
1583
1756
|
};
|
|
1584
1757
|
stream.onerror = () => {
|
|
1585
1758
|
};
|
|
@@ -1623,15 +1796,16 @@ function createDevtoolsNotifier(options = {}) {
|
|
|
1623
1796
|
}
|
|
1624
1797
|
|
|
1625
1798
|
// src/setup.ts
|
|
1626
|
-
var
|
|
1799
|
+
var read = (get) => {
|
|
1627
1800
|
try {
|
|
1628
|
-
return
|
|
1801
|
+
return get();
|
|
1629
1802
|
} catch {
|
|
1630
1803
|
return void 0;
|
|
1631
1804
|
}
|
|
1632
1805
|
};
|
|
1633
|
-
|
|
1634
|
-
|
|
1806
|
+
var production = read(() => process.env.NODE_ENV) === "production";
|
|
1807
|
+
if (!production && typeof window !== "undefined" && !window.__RERENDER_LENS_DEVTOOLS__) {
|
|
1808
|
+
const relay = read(() => process.env.RERENDER_LENS_RELAY) || read(() => process.env.NEXT_PUBLIC_RERENDER_LENS_RELAY);
|
|
1635
1809
|
init({ trackAllMemoized: true, notifier: createDevtoolsNotifier(relay ? { relay } : {}) });
|
|
1636
1810
|
}
|
|
1637
1811
|
//# sourceMappingURL=setup.cjs.map
|