rerender-lens 0.2.0 → 0.3.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 +69 -0
- package/README.md +146 -5
- package/dist/budget-COBu7jBU.d.cts +64 -0
- package/dist/budget-LkNjGtRc.d.ts +64 -0
- package/dist/cli.cjs +458 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +4 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +452 -0
- package/dist/cli.js.map +1 -0
- package/dist/devtools-BHGUUo-p.d.ts +244 -0
- package/dist/devtools-DmhWiWcN.d.cts +244 -0
- package/dist/index.cjs +652 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -336
- package/dist/index.d.ts +40 -336
- package/dist/index.js +636 -24
- package/dist/index.js.map +1 -1
- package/dist/notifiers-BGQtWKfX.d.cts +90 -0
- package/dist/notifiers-BjjGSHpp.d.ts +90 -0
- package/dist/playwright.cjs +196 -0
- package/dist/playwright.cjs.map +1 -0
- package/dist/playwright.d.cts +35 -0
- package/dist/playwright.d.ts +35 -0
- package/dist/playwright.js +184 -0
- package/dist/playwright.js.map +1 -0
- package/dist/relay.cjs +166 -0
- package/dist/relay.cjs.map +1 -0
- package/dist/relay.d.cts +41 -0
- package/dist/relay.d.ts +41 -0
- package/dist/relay.js +161 -0
- package/dist/relay.js.map +1 -0
- package/dist/rerender-lens.iife.js +1717 -0
- package/dist/setup.cjs +1510 -0
- package/dist/setup.cjs.map +1 -0
- package/dist/setup.d.cts +2 -0
- package/dist/setup.d.ts +2 -0
- package/dist/setup.js +1508 -0
- package/dist/setup.js.map +1 -0
- package/dist/types-BzUEVkxJ.d.cts +177 -0
- package/dist/types-BzUEVkxJ.d.ts +177 -0
- package/dist/vite.cjs +113 -0
- package/dist/vite.cjs.map +1 -0
- package/dist/vite.d.cts +84 -0
- package/dist/vite.d.ts +84 -0
- package/dist/vite.js +103 -0
- package/dist/vite.js.map +1 -0
- package/dist/vitest-setup.cjs +1299 -0
- package/dist/vitest-setup.cjs.map +1 -0
- package/dist/vitest-setup.d.cts +8 -0
- package/dist/vitest-setup.d.ts +8 -0
- package/dist/vitest-setup.js +1297 -0
- package/dist/vitest-setup.js.map +1 -0
- package/dist/vitest.cjs +1360 -0
- package/dist/vitest.cjs.map +1 -0
- package/dist/vitest.d.cts +52 -0
- package/dist/vitest.d.ts +52 -0
- package/dist/vitest.js +1351 -0
- package/dist/vitest.js.map +1 -0
- package/package.json +84 -4
- package/panel/panel.css +418 -0
- package/panel/panel.html +12 -0
- package/panel/panel.js +3325 -0
package/dist/index.js
CHANGED
|
@@ -145,6 +145,17 @@ function firstDifferentPath(a, b, path, depth = 0) {
|
|
|
145
145
|
|
|
146
146
|
// src/report.ts
|
|
147
147
|
var now = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
148
|
+
var hookLabel = (c) => c.custom && c.custom.length ? `${c.custom.join(" \u203A ")} \u203A ${c.hook}#${c.index}` : `${c.hook} #${c.index}`;
|
|
149
|
+
function storeAdvice(custom) {
|
|
150
|
+
const chain = custom ?? [];
|
|
151
|
+
if (chain.some((n) => /^useSelector$/.test(n)))
|
|
152
|
+
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";
|
|
153
|
+
if (chain.some((n) => /^useAppSelector$/.test(n)))
|
|
154
|
+
return "the selector returns a new object on every call: return a stored slice, pass shallowEqual, or memoize it with createSelector";
|
|
155
|
+
if (chain.some((n) => /^use[A-Z]\w*Store$/.test(n) || n === "useStore" || n === "useBoundStore"))
|
|
156
|
+
return "the store selector returns a new object on every call: select a primitive, or wrap the selector with useShallow (Zustand) / an equality function";
|
|
157
|
+
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";
|
|
158
|
+
}
|
|
148
159
|
var isGenuine = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
|
|
149
160
|
var isChildren = (change) => change.path === "children";
|
|
150
161
|
function fixFor(change) {
|
|
@@ -220,10 +231,17 @@ function buildReport(input) {
|
|
|
220
231
|
const where = c.provider && c.provider.component ? ` (provided by <${c.provider.component}>)` : "";
|
|
221
232
|
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` : "";
|
|
222
233
|
reasons.push(`${c.path} changed${where}${keys}.`);
|
|
223
|
-
} else if (isGenuine(c)) reasons.push(`${c
|
|
234
|
+
} else if (isGenuine(c)) reasons.push(`${hookLabel(c)} changed.`);
|
|
224
235
|
else if (isStateHook(c))
|
|
225
|
-
reasons.push(`${c
|
|
226
|
-
else reasons.push(`${c
|
|
236
|
+
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.`);
|
|
237
|
+
else if (c.hook === "useSyncExternalStore") reasons.push(`${hookLabel(c)} returned a new reference that is deep-equal to the previous value: ${storeAdvice(c.custom)}.`);
|
|
238
|
+
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.`);
|
|
239
|
+
}
|
|
240
|
+
if (input.commitCause === "effect-after-commit") {
|
|
241
|
+
const who = input.updaters && input.updaters.length ? input.updaters.map((u) => `<${u}>`).join(", ") : "a component that rendered in it";
|
|
242
|
+
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.`);
|
|
243
|
+
} else if (input.commitCause === "suspense-resolved") {
|
|
244
|
+
reasons.push("this commit shows content that was suspended (a Suspense boundary resolved).");
|
|
227
245
|
}
|
|
228
246
|
const report = {
|
|
229
247
|
component: input.component,
|
|
@@ -247,6 +265,13 @@ function buildReport(input) {
|
|
|
247
265
|
if (input.treeDuration !== void 0) report.treeDuration = input.treeDuration;
|
|
248
266
|
if (input.commitPriority) report.commitPriority = input.commitPriority;
|
|
249
267
|
if (input.source) report.source = input.source;
|
|
268
|
+
if (input.hookState) report.hookState = input.hookState;
|
|
269
|
+
if (input.contexts) report.contexts = input.contexts;
|
|
270
|
+
if (input.state) report.state = input.state;
|
|
271
|
+
if (input.updaters && input.updaters.length) report.updaters = input.updaters;
|
|
272
|
+
if (input.commitCause) report.commitCause = input.commitCause;
|
|
273
|
+
if (input.afterCommit !== void 0) report.afterCommit = input.afterCommit;
|
|
274
|
+
if (input.key !== void 0) report.key = input.key;
|
|
250
275
|
return report;
|
|
251
276
|
}
|
|
252
277
|
function describeTrigger(t) {
|
|
@@ -291,6 +316,9 @@ function printReport(report, options) {
|
|
|
291
316
|
c.log(`${ch.path} (${KIND_LABEL[ch.kind]})`, { prev: ch.prev, next: ch.next });
|
|
292
317
|
}
|
|
293
318
|
c.log("props", report.props);
|
|
319
|
+
if (report.hookState && report.hookState.length) c.log("hooks", Object.fromEntries(report.hookState.map((h) => [h.path, h.value])));
|
|
320
|
+
if (report.contexts && report.contexts.length) c.log("contexts", Object.fromEntries(report.contexts.map((x) => [x.name, x.value])));
|
|
321
|
+
if (report.state) c.log("state", report.state);
|
|
294
322
|
c.groupEnd();
|
|
295
323
|
}
|
|
296
324
|
|
|
@@ -300,7 +328,7 @@ function getState() {
|
|
|
300
328
|
const g = globalThis;
|
|
301
329
|
let s = g[KEY];
|
|
302
330
|
if (!s) {
|
|
303
|
-
s = { options: {}, enabled: false, printed: /* @__PURE__ */ new Map(), detach: null, nextInstanceId: 1, nextCommitId: 1, warnedOnce: /* @__PURE__ */ new Set(), scheduled: 0, commits: 0 };
|
|
331
|
+
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 };
|
|
304
332
|
g[KEY] = s;
|
|
305
333
|
}
|
|
306
334
|
return s;
|
|
@@ -335,6 +363,171 @@ function dispatch(report, override) {
|
|
|
335
363
|
}
|
|
336
364
|
}
|
|
337
365
|
|
|
366
|
+
// src/hookNames.ts
|
|
367
|
+
var noop = () => {
|
|
368
|
+
};
|
|
369
|
+
function customHooksFromStack(stack, componentName, marker) {
|
|
370
|
+
const names = [];
|
|
371
|
+
let past = false;
|
|
372
|
+
for (const line of stack.split("\n")) {
|
|
373
|
+
const m = /^\s*at\s+(?:async\s+)?([^\s(]+)|^([^@\s]+)@/.exec(line);
|
|
374
|
+
const name = m ? (m[1] || m[2] || "").replace(/^Object\./, "").replace(/^Proxy\./, "") : "";
|
|
375
|
+
if (!name) continue;
|
|
376
|
+
if (!past) {
|
|
377
|
+
if (name.includes(marker)) past = true;
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (name === componentName || name.endsWith("." + componentName)) break;
|
|
381
|
+
if (/^use[A-Z0-9_]/.test(name)) names.push(name);
|
|
382
|
+
if (names.length > 8) break;
|
|
383
|
+
}
|
|
384
|
+
return names;
|
|
385
|
+
}
|
|
386
|
+
var cache = /* @__PURE__ */ new WeakMap();
|
|
387
|
+
function makeDispatcher(fiber, componentName, out, marker) {
|
|
388
|
+
let node = fiber.memoizedState;
|
|
389
|
+
let index = 0;
|
|
390
|
+
const take = (count) => {
|
|
391
|
+
const first = node;
|
|
392
|
+
for (let i = 0; i < count && node; i++) node = node.next;
|
|
393
|
+
index += count;
|
|
394
|
+
return first;
|
|
395
|
+
};
|
|
396
|
+
const record = (primitive, count) => {
|
|
397
|
+
const stack = new Error().stack || "";
|
|
398
|
+
const chain = customHooksFromStack(stack, componentName, marker);
|
|
399
|
+
const start = index;
|
|
400
|
+
const first = take(count);
|
|
401
|
+
if (chain.length) for (let i = 0; i < count; i++) out.set(start + i, chain);
|
|
402
|
+
return first;
|
|
403
|
+
};
|
|
404
|
+
const memoized = (n) => n ? n.memoizedState : void 0;
|
|
405
|
+
const dispatcher = {};
|
|
406
|
+
const define = (name, impl) => {
|
|
407
|
+
const fn = { [marker + name]: (...a) => impl(...a) }[marker + name];
|
|
408
|
+
dispatcher[name] = fn;
|
|
409
|
+
};
|
|
410
|
+
define("useState", () => {
|
|
411
|
+
const n = record("useState", 1);
|
|
412
|
+
return [memoized(n), noop];
|
|
413
|
+
});
|
|
414
|
+
define("useReducer", () => {
|
|
415
|
+
const n = record("useReducer", 1);
|
|
416
|
+
return [memoized(n), noop];
|
|
417
|
+
});
|
|
418
|
+
define("useRef", (init2) => {
|
|
419
|
+
const n = record("useRef", 1);
|
|
420
|
+
return n ? n.memoizedState : { current: init2 };
|
|
421
|
+
});
|
|
422
|
+
define("useMemo", (fn) => {
|
|
423
|
+
const n = record("useMemo", 1);
|
|
424
|
+
const s = memoized(n);
|
|
425
|
+
return Array.isArray(s) ? s[0] : typeof fn === "function" ? fn() : void 0;
|
|
426
|
+
});
|
|
427
|
+
define("useCallback", (fn) => {
|
|
428
|
+
const n = record("useCallback", 1);
|
|
429
|
+
const s = memoized(n);
|
|
430
|
+
return Array.isArray(s) ? s[0] : fn;
|
|
431
|
+
});
|
|
432
|
+
for (const effect of ["useEffect", "useLayoutEffect", "useInsertionEffect", "useImperativeHandle"]) define(effect, () => void record(effect, 1));
|
|
433
|
+
define("useContext", (ctx) => {
|
|
434
|
+
record("useContext", 0);
|
|
435
|
+
const c = ctx;
|
|
436
|
+
return c ? c._currentValue : void 0;
|
|
437
|
+
});
|
|
438
|
+
define("useDebugValue", () => void record("useDebugValue", 0));
|
|
439
|
+
define("useSyncExternalStore", (_subscribe, getSnapshot) => {
|
|
440
|
+
const n = record("useSyncExternalStore", 2);
|
|
441
|
+
return n ? n.memoizedState : typeof getSnapshot === "function" ? getSnapshot() : void 0;
|
|
442
|
+
});
|
|
443
|
+
define("useTransition", () => {
|
|
444
|
+
record("useTransition", 2);
|
|
445
|
+
return [false, noop];
|
|
446
|
+
});
|
|
447
|
+
define("useDeferredValue", (v) => {
|
|
448
|
+
const n = record("useDeferredValue", 1);
|
|
449
|
+
return n ? n.memoizedState : v;
|
|
450
|
+
});
|
|
451
|
+
define("useId", () => {
|
|
452
|
+
const n = record("useId", 1);
|
|
453
|
+
return n ? n.memoizedState : "id";
|
|
454
|
+
});
|
|
455
|
+
define("useOptimistic", (v) => {
|
|
456
|
+
const n = record("useOptimistic", 1);
|
|
457
|
+
return [n ? n.memoizedState : v, noop];
|
|
458
|
+
});
|
|
459
|
+
for (const action of ["useActionState", "useFormState"]) {
|
|
460
|
+
define(action, (_fn, init2) => {
|
|
461
|
+
const n = record(action, 3);
|
|
462
|
+
return [n ? n.memoizedState : init2, noop, false];
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
define("useEffectEvent", (fn) => {
|
|
466
|
+
record("useEffectEvent", 1);
|
|
467
|
+
return fn;
|
|
468
|
+
});
|
|
469
|
+
define("useCacheRefresh", () => {
|
|
470
|
+
record("useCacheRefresh", 1);
|
|
471
|
+
return noop;
|
|
472
|
+
});
|
|
473
|
+
define("useHostTransitionStatus", () => ({ pending: false, data: null, method: null, action: null }));
|
|
474
|
+
define("useMemoCache", (size) => new Array(Number(size) || 0).fill(/* @__PURE__ */ Symbol.for("react.memo_cache_sentinel")));
|
|
475
|
+
define("use", (usable) => {
|
|
476
|
+
record("use", 0);
|
|
477
|
+
const u = usable;
|
|
478
|
+
if (u && typeof u.then === "function") {
|
|
479
|
+
if (u.status === "fulfilled") return u.value;
|
|
480
|
+
throw new Error("rerender-lens: cannot replay use() on a pending promise");
|
|
481
|
+
}
|
|
482
|
+
return u ? u._currentValue : void 0;
|
|
483
|
+
});
|
|
484
|
+
define("readContext", (ctx) => ctx?._currentValue);
|
|
485
|
+
return { dispatcher, nodesConsumed: () => index };
|
|
486
|
+
}
|
|
487
|
+
function renderFunctionOf(type) {
|
|
488
|
+
let t = type;
|
|
489
|
+
while (t && typeof t === "object" && t.type) t = t.type;
|
|
490
|
+
if (t && typeof t === "object" && typeof t.render === "function") return { fn: t.render, name: t.render.name || t.displayName || "Component" };
|
|
491
|
+
if (typeof t === "function") return { fn: t, name: t.name || "Component" };
|
|
492
|
+
return { fn: null, name: "Component" };
|
|
493
|
+
}
|
|
494
|
+
function resolveHookNames(fiber, ref) {
|
|
495
|
+
const type = fiber.type;
|
|
496
|
+
if (!type || !ref) return /* @__PURE__ */ new Map();
|
|
497
|
+
const cached = cache.get(type);
|
|
498
|
+
if (cached) return cached.byNode;
|
|
499
|
+
const result = { byNode: /* @__PURE__ */ new Map(), ok: false };
|
|
500
|
+
cache.set(type, result);
|
|
501
|
+
const { fn, name } = renderFunctionOf(type);
|
|
502
|
+
if (!fn) return result.byNode;
|
|
503
|
+
const useH = "H" in ref;
|
|
504
|
+
const marker = "__rl_";
|
|
505
|
+
const previous = useH ? ref.H : ref.current;
|
|
506
|
+
const { dispatcher, nodesConsumed } = makeDispatcher(fiber, name, result.byNode, marker);
|
|
507
|
+
const proxy = new Proxy(dispatcher, {
|
|
508
|
+
get(target, key) {
|
|
509
|
+
if (key in target) return target[key];
|
|
510
|
+
return () => void 0;
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
let total = 0;
|
|
514
|
+
for (let n = fiber.memoizedState; n && typeof n === "object" && "next" in n; n = n.next) total++;
|
|
515
|
+
try {
|
|
516
|
+
if (useH) ref.H = proxy;
|
|
517
|
+
else ref.current = proxy;
|
|
518
|
+
const secondArg = fiber.tag === 11 ? fiber.stateNode?.ref ?? null : void 0;
|
|
519
|
+
fn(fiber.memoizedProps ?? {}, secondArg);
|
|
520
|
+
result.ok = nodesConsumed() === total;
|
|
521
|
+
if (!result.ok) result.byNode.clear();
|
|
522
|
+
} catch {
|
|
523
|
+
result.byNode.clear();
|
|
524
|
+
} finally {
|
|
525
|
+
if (useH) ref.H = previous;
|
|
526
|
+
else ref.current = previous;
|
|
527
|
+
}
|
|
528
|
+
return result.byNode;
|
|
529
|
+
}
|
|
530
|
+
|
|
338
531
|
// src/fiber.ts
|
|
339
532
|
var FunctionComponent = 0;
|
|
340
533
|
var ClassComponent = 1;
|
|
@@ -342,10 +535,12 @@ var HostRoot = 3;
|
|
|
342
535
|
var HostComponent = 5;
|
|
343
536
|
var ContextProvider = 10;
|
|
344
537
|
var ForwardRef = 11;
|
|
538
|
+
var SuspenseComponent = 13;
|
|
345
539
|
var MemoComponent = 14;
|
|
346
540
|
var SimpleMemoComponent = 15;
|
|
347
541
|
var PerformedWork = 1;
|
|
348
542
|
var HOOK = "__REACT_DEVTOOLS_GLOBAL_HOOK__";
|
|
543
|
+
var nowMs = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
349
544
|
function ensureDevtoolsHook() {
|
|
350
545
|
const g = globalThis;
|
|
351
546
|
let hook = g[HOOK];
|
|
@@ -374,7 +569,9 @@ function ensureDevtoolsHook() {
|
|
|
374
569
|
return hook;
|
|
375
570
|
}
|
|
376
571
|
var capturedRenderers = /* @__PURE__ */ new Map();
|
|
572
|
+
var dispatcherRefs = /* @__PURE__ */ new Map();
|
|
377
573
|
var INJECT_WRAPPED = /* @__PURE__ */ Symbol.for("rerender-lens.injectWrapped");
|
|
574
|
+
var ATTACHED = /* @__PURE__ */ Symbol.for("rerender-lens.attached");
|
|
378
575
|
function pickRenderer(r) {
|
|
379
576
|
const info = r ?? {};
|
|
380
577
|
return { version: info.version, bundleType: info.bundleType, rendererPackageName: info.rendererPackageName };
|
|
@@ -385,20 +582,45 @@ function wrapInject(hook) {
|
|
|
385
582
|
const original = hook.inject;
|
|
386
583
|
hook.inject = function(renderer) {
|
|
387
584
|
const id = original.call(this, renderer);
|
|
388
|
-
|
|
585
|
+
const key = typeof id === "number" ? id : capturedRenderers.size + 1;
|
|
586
|
+
capturedRenderers.set(key, pickRenderer(renderer));
|
|
587
|
+
const ref = renderer?.currentDispatcherRef;
|
|
588
|
+
if (ref && typeof ref === "object") dispatcherRefs.set(key, ref);
|
|
389
589
|
return id;
|
|
390
590
|
};
|
|
391
591
|
h[INJECT_WRAPPED] = true;
|
|
392
592
|
}
|
|
593
|
+
function getDispatcherRef() {
|
|
594
|
+
for (const ref of dispatcherRefs.values()) return ref;
|
|
595
|
+
const g = globalThis;
|
|
596
|
+
const hook = g[HOOK];
|
|
597
|
+
let found = null;
|
|
598
|
+
if (hook && hook.renderers && typeof hook.renderers.forEach === "function") {
|
|
599
|
+
hook.renderers.forEach((r) => {
|
|
600
|
+
const ref = r?.currentDispatcherRef;
|
|
601
|
+
if (!found && ref && typeof ref === "object") found = ref;
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
return found;
|
|
605
|
+
}
|
|
393
606
|
function attach() {
|
|
394
607
|
const hook = ensureDevtoolsHook();
|
|
395
608
|
wrapInject(hook);
|
|
609
|
+
if (hook[ATTACHED]) return () => {
|
|
610
|
+
};
|
|
611
|
+
hook[ATTACHED] = true;
|
|
396
612
|
const previous = hook.onCommitFiberRoot;
|
|
397
613
|
const patched = function(id, root, priority, didError) {
|
|
614
|
+
const started = nowMs();
|
|
398
615
|
try {
|
|
399
616
|
onCommit(root, priorityLabel(priority));
|
|
400
617
|
} catch (err) {
|
|
401
618
|
warnOnce("commit", `failed to inspect a commit: ${String(err)}`);
|
|
619
|
+
} finally {
|
|
620
|
+
const spent = nowMs() - started;
|
|
621
|
+
const s = getState();
|
|
622
|
+
s.overheadMs += spent;
|
|
623
|
+
if (spent > s.maxCommitMs) s.maxCommitMs = spent;
|
|
402
624
|
}
|
|
403
625
|
if (typeof previous === "function") return previous.call(this, id, root, priority, didError);
|
|
404
626
|
};
|
|
@@ -410,6 +632,7 @@ function attach() {
|
|
|
410
632
|
};
|
|
411
633
|
hook.onScheduleFiberRoot = patchedSchedule;
|
|
412
634
|
return () => {
|
|
635
|
+
hook[ATTACHED] = false;
|
|
413
636
|
if (hook.onCommitFiberRoot === patched) hook.onCommitFiberRoot = previous;
|
|
414
637
|
if (hook.onScheduleFiberRoot === patchedSchedule) hook.onScheduleFiberRoot = previousSchedule;
|
|
415
638
|
};
|
|
@@ -579,7 +802,7 @@ function ownerName(fiber) {
|
|
|
579
802
|
}
|
|
580
803
|
var isStateNode = (n) => !!n.queue && typeof n.queue.lastRenderedReducer === "function";
|
|
581
804
|
var isStoreNode = (n) => !!n.queue && typeof n.queue.getSnapshot === "function";
|
|
582
|
-
function
|
|
805
|
+
function hookLabel2(node) {
|
|
583
806
|
if (isStoreNode(node)) return "useSyncExternalStore";
|
|
584
807
|
if (isStateNode(node)) {
|
|
585
808
|
const r = node.queue.lastRenderedReducer;
|
|
@@ -588,20 +811,43 @@ function hookLabel(node) {
|
|
|
588
811
|
return "state";
|
|
589
812
|
}
|
|
590
813
|
var NODELESS_HOOKS = /* @__PURE__ */ new Set(["useContext", "useDebugValue", "use"]);
|
|
814
|
+
function hookLabelsFor(fiber, first) {
|
|
815
|
+
const types = fiber._debugHookTypes?.filter((t) => !NODELESS_HOOKS.has(t));
|
|
816
|
+
let nodeCount = 0;
|
|
817
|
+
for (let n = first; n; n = n.next) nodeCount++;
|
|
818
|
+
return types && types.length === nodeCount ? types : null;
|
|
819
|
+
}
|
|
820
|
+
var isHookList = (v) => !!v && typeof v === "object" && "next" in v;
|
|
821
|
+
function snapshotHooks(fiber) {
|
|
822
|
+
const out = [];
|
|
823
|
+
if (fiber.tag === ClassComponent) return out;
|
|
824
|
+
const first = fiber.memoizedState;
|
|
825
|
+
if (!isHookList(first)) return out;
|
|
826
|
+
const labels = hookLabelsFor(fiber, first);
|
|
827
|
+
let i = 0;
|
|
828
|
+
for (let n = first; n; n = n.next, i++) {
|
|
829
|
+
if (!isStateNode(n) && !isStoreNode(n)) continue;
|
|
830
|
+
const hook = labels?.[i] ?? hookLabel2(n);
|
|
831
|
+
out.push({ path: `${hook}#${i}`, hook, index: i, value: n.memoizedState });
|
|
832
|
+
}
|
|
833
|
+
return out;
|
|
834
|
+
}
|
|
835
|
+
function snapshotContexts(fiber) {
|
|
836
|
+
const out = [];
|
|
837
|
+
for (let d = fiber.dependencies?.firstContext ?? null; d; d = d.next) out.push({ name: d.context.displayName ?? "Context", value: d.memoizedValue });
|
|
838
|
+
return out;
|
|
839
|
+
}
|
|
591
840
|
function diffHooks(fiber, alt) {
|
|
592
841
|
const out = [];
|
|
593
842
|
if (fiber.tag === ClassComponent) return out;
|
|
594
843
|
let a = alt.memoizedState;
|
|
595
844
|
let b = fiber.memoizedState;
|
|
596
845
|
if (!a || !b || typeof b !== "object" || !("next" in b)) return out;
|
|
597
|
-
const
|
|
598
|
-
let nodeCount = 0;
|
|
599
|
-
for (let n = b; n; n = n.next) nodeCount++;
|
|
600
|
-
const labels = types && types.length === nodeCount ? types : null;
|
|
846
|
+
const labels = hookLabelsFor(fiber, b);
|
|
601
847
|
let i = 0;
|
|
602
848
|
while (a && b) {
|
|
603
849
|
if ((isStateNode(b) || isStoreNode(b)) && !Object.is(a.memoizedState, b.memoizedState)) {
|
|
604
|
-
const hook = labels?.[i] ??
|
|
850
|
+
const hook = labels?.[i] ?? hookLabel2(b);
|
|
605
851
|
out.push({
|
|
606
852
|
path: `${hook}#${i}`,
|
|
607
853
|
hook,
|
|
@@ -690,14 +936,14 @@ function analyze(fiber, alt, trackHooks) {
|
|
|
690
936
|
const trigger = causes.length === 0 ? "parent" : causes.length === 1 ? causes[0] : "mixed";
|
|
691
937
|
return { propChanges, stateChanges, hookChanges, trigger };
|
|
692
938
|
}
|
|
693
|
-
function nearestRenderedAncestor(fiber,
|
|
939
|
+
function nearestRenderedAncestor(fiber, cache2, trackHooks) {
|
|
694
940
|
let f = fiber.return;
|
|
695
941
|
while (f && f.tag !== HostRoot) {
|
|
696
942
|
if (isComponentTag(f.tag) && f.alternate && didRender(f)) {
|
|
697
|
-
let info =
|
|
943
|
+
let info = cache2.get(f);
|
|
698
944
|
if (info === void 0) {
|
|
699
945
|
info = { name: fiberName(f), trigger: analyze(f, f.alternate, trackHooks).trigger };
|
|
700
|
-
|
|
946
|
+
cache2.set(f, info);
|
|
701
947
|
}
|
|
702
948
|
return info;
|
|
703
949
|
}
|
|
@@ -705,6 +951,21 @@ function nearestRenderedAncestor(fiber, cache, trackHooks) {
|
|
|
705
951
|
}
|
|
706
952
|
return null;
|
|
707
953
|
}
|
|
954
|
+
function updatersOf(root) {
|
|
955
|
+
const set = root.memoizedUpdaters;
|
|
956
|
+
if (!set || typeof set.forEach !== "function") return [];
|
|
957
|
+
const names = [];
|
|
958
|
+
set.forEach((f) => {
|
|
959
|
+
const comp = nearestComponent(f);
|
|
960
|
+
if (comp) {
|
|
961
|
+
const name = fiberName(comp);
|
|
962
|
+
if (!names.includes(name)) names.push(name);
|
|
963
|
+
}
|
|
964
|
+
});
|
|
965
|
+
return names;
|
|
966
|
+
}
|
|
967
|
+
var lastCommit = null;
|
|
968
|
+
var EFFECT_LOOP_WINDOW_MS = 50;
|
|
708
969
|
function onCommit(root, commitPriority) {
|
|
709
970
|
const s = getState();
|
|
710
971
|
if (!s.enabled) return;
|
|
@@ -712,28 +973,53 @@ function onCommit(root, commitPriority) {
|
|
|
712
973
|
const o = s.options;
|
|
713
974
|
const trackHooks = o.trackHooks !== false;
|
|
714
975
|
const rendered = [];
|
|
976
|
+
const renderedNames = /* @__PURE__ */ new Set();
|
|
715
977
|
let hot = false;
|
|
978
|
+
let suspenseResolved = false;
|
|
716
979
|
const stack = [root.current];
|
|
717
980
|
while (stack.length) {
|
|
718
981
|
const fiber = stack.pop();
|
|
719
982
|
const alt = fiber.alternate;
|
|
720
983
|
if (alt && isComponentTag(fiber.tag) && didRender(fiber)) {
|
|
721
984
|
if (isHotSwapped(fiber, alt)) hot = true;
|
|
985
|
+
renderedNames.add(fiberName(fiber));
|
|
722
986
|
if (shouldTrack(fiberType(fiber), o)) rendered.push(fiber);
|
|
723
987
|
}
|
|
988
|
+
if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null) suspenseResolved = true;
|
|
724
989
|
if (!alt || fiber.child !== alt.child) {
|
|
725
990
|
for (let c = fiber.child; c; c = c.sibling) stack.push(c);
|
|
726
991
|
}
|
|
727
992
|
}
|
|
728
993
|
if (hot && o.ignoreHotReload !== false) return;
|
|
729
994
|
rendered.reverse();
|
|
730
|
-
|
|
995
|
+
const at = nowMs();
|
|
996
|
+
const updaters = updatersOf(root);
|
|
997
|
+
let commitCause;
|
|
998
|
+
let afterCommit;
|
|
999
|
+
if (lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u))) {
|
|
1000
|
+
commitCause = "effect-after-commit";
|
|
1001
|
+
afterCommit = lastCommit.id;
|
|
1002
|
+
} else if (suspenseResolved) commitCause = "suspense-resolved";
|
|
1003
|
+
if (rendered.length === 0) {
|
|
1004
|
+
lastCommit = { id: s.nextCommitId, at, rendered: renderedNames };
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
731
1007
|
const commitId = s.nextCommitId++;
|
|
1008
|
+
lastCommit = { id: commitId, at, rendered: renderedNames };
|
|
1009
|
+
const dispatcherRef = o.resolveHookNames ? getDispatcherRef() : null;
|
|
732
1010
|
const parentCache = /* @__PURE__ */ new Map();
|
|
733
1011
|
for (const fiber of rendered) {
|
|
734
1012
|
const alt = fiber.alternate;
|
|
735
1013
|
const a = analyze(fiber, alt, trackHooks);
|
|
736
1014
|
const durations = durationsOf(fiber);
|
|
1015
|
+
let hookState = o.includeState !== false && trackHooks ? snapshotHooks(fiber) : void 0;
|
|
1016
|
+
if (dispatcherRef && fiber.tag !== ClassComponent && (a.hookChanges.length || hookState && hookState.length)) {
|
|
1017
|
+
const names = resolveHookNames(fiber, dispatcherRef);
|
|
1018
|
+
if (names.size) {
|
|
1019
|
+
for (const c of a.hookChanges) if (c.hook !== "useContext" && names.has(c.index)) c.custom = names.get(c.index);
|
|
1020
|
+
if (hookState) hookState = hookState.map((h) => names.has(h.index) ? { ...h, custom: names.get(h.index) } : h);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
737
1023
|
const report = buildReport({
|
|
738
1024
|
component: fiberName(fiber),
|
|
739
1025
|
instanceId: instanceId(fiber),
|
|
@@ -743,6 +1029,15 @@ function onCommit(root, commitPriority) {
|
|
|
743
1029
|
propChanges: a.propChanges,
|
|
744
1030
|
stateChanges: a.stateChanges,
|
|
745
1031
|
hookChanges: a.hookChanges,
|
|
1032
|
+
...o.includeState !== false ? {
|
|
1033
|
+
hookState,
|
|
1034
|
+
contexts: trackHooks ? snapshotContexts(fiber) : void 0,
|
|
1035
|
+
state: fiber.tag === ClassComponent && fiber.memoizedState && typeof fiber.memoizedState === "object" ? fiber.memoizedState : void 0
|
|
1036
|
+
} : {},
|
|
1037
|
+
updaters,
|
|
1038
|
+
commitCause,
|
|
1039
|
+
afterCommit,
|
|
1040
|
+
key: fiber.key === null || fiber.key === void 0 ? null : String(fiber.key),
|
|
746
1041
|
parent: a.trigger === "parent" ? nearestRenderedAncestor(fiber, parentCache, trackHooks) : null,
|
|
747
1042
|
owner: ownerName(fiber),
|
|
748
1043
|
path: componentPath(fiber),
|
|
@@ -855,6 +1150,201 @@ function useWhyRerender(name, values, options) {
|
|
|
855
1150
|
});
|
|
856
1151
|
}
|
|
857
1152
|
|
|
1153
|
+
// src/fixes.ts
|
|
1154
|
+
var AVOIDABLE = /* @__PURE__ */ new Set(["deep-equal", "function", "element"]);
|
|
1155
|
+
var rootOf = (path) => path.split(/[.[]/)[0] || path;
|
|
1156
|
+
function fixesFor(r) {
|
|
1157
|
+
const out = [];
|
|
1158
|
+
const owner = r.owner || r.parent && r.parent.name || "?";
|
|
1159
|
+
const changes = [...r.propChanges, ...r.stateChanges, ...r.hookChanges];
|
|
1160
|
+
if (r.avoidable && (changes.length === 0 || r.memoized === false)) {
|
|
1161
|
+
out.push({
|
|
1162
|
+
kind: "memo",
|
|
1163
|
+
owner: r.component,
|
|
1164
|
+
target: r.component,
|
|
1165
|
+
prop: null,
|
|
1166
|
+
label: `Wrap <${r.component}> in React.memo`,
|
|
1167
|
+
detail: changes.length === 0 ? `<${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.`
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
for (const c of r.propChanges) {
|
|
1171
|
+
if (!AVOIDABLE.has(c.kind)) continue;
|
|
1172
|
+
const root = rootOf(c.path);
|
|
1173
|
+
if (root === "children" && (c.kind === "element" || c.kind === "deep-equal")) {
|
|
1174
|
+
out.push({ kind: "children", owner, target: r.component, prop: "children", label: `memoize children of <${r.component}> in <${owner}>`, detail: `<${owner}> re-creates the children of <${r.component}> on every render.` });
|
|
1175
|
+
} else if (c.kind === "function") {
|
|
1176
|
+
out.push({ kind: "useCallback", owner, target: r.component, prop: root, label: `useCallback(${root}) in <${owner}>`, detail: `prop "${c.path}" of <${r.component}> is a new function on every render of <${owner}>.` });
|
|
1177
|
+
} else if (c.kind === "element") {
|
|
1178
|
+
out.push({ kind: "useMemoElement", owner, target: r.component, prop: root, label: `memoize element prop ${root} in <${owner}>`, detail: `prop "${c.path}" of <${r.component}> is a new element with the same type and props on every render of <${owner}>.` });
|
|
1179
|
+
} else {
|
|
1180
|
+
out.push({ kind: "useMemo", owner, target: r.component, prop: root, label: `useMemo(${root}) in <${owner}>`, detail: `prop "${c.path}" of <${r.component}> is a new ${Array.isArray(c.next) ? "array" : "object"} with the same contents on every render of <${owner}>.` });
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
for (const c of [...r.stateChanges, ...r.hookChanges]) {
|
|
1184
|
+
const isContext = c.hook === "useContext";
|
|
1185
|
+
const ctxName = isContext ? (/useContext\((.*)\)/.exec(c.path) || [])[1] || "Context" : "";
|
|
1186
|
+
const providerOwner = isContext && c.provider && c.provider.component ? c.provider.component : null;
|
|
1187
|
+
if (isContext && c.kind === "different" && c.changedKeys && typeof c.totalKeys === "number" && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys) {
|
|
1188
|
+
out.push({
|
|
1189
|
+
kind: "splitContext",
|
|
1190
|
+
owner: providerOwner || `${ctxName}.Provider`,
|
|
1191
|
+
target: r.component,
|
|
1192
|
+
prop: ctxName,
|
|
1193
|
+
label: `split ${ctxName}${providerOwner ? ` in <${providerOwner}>` : ""}: only ${c.changedKeys.join(", ")} changed`,
|
|
1194
|
+
detail: `${c.changedKeys.length} of ${c.totalKeys} keys changed in ${ctxName}, yet every consumer re-rendered.`
|
|
1195
|
+
});
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1198
|
+
if (!AVOIDABLE.has(c.kind)) continue;
|
|
1199
|
+
if (isContext) {
|
|
1200
|
+
out.push({ kind: "contextValue", owner: providerOwner || `${ctxName}.Provider`, target: r.component, prop: ctxName, label: `memoize the ${ctxName} provider value${providerOwner ? ` in <${providerOwner}>` : ""}`, detail: `${ctxName} produced a new value that is deep-equal to the previous one.` });
|
|
1201
|
+
} else if (c.hook === "useSyncExternalStore") {
|
|
1202
|
+
const chain = c.custom || [];
|
|
1203
|
+
const redux = chain.some((n) => /^use(App)?Selector$/.test(n));
|
|
1204
|
+
const zustand = !redux && chain.some((n) => /^use[A-Z]\w*Store$/.test(n) || n === "useStore" || n === "useBoundStore");
|
|
1205
|
+
out.push({ kind: "storeSnapshot", owner: r.component, target: r.component, prop: c.path, label: redux ? `memoize the selector in <${r.component}>` : zustand ? `useShallow in <${r.component}>` : `stable getSnapshot in <${r.component}>`, detail: storeAdvice(c.custom) });
|
|
1206
|
+
} else {
|
|
1207
|
+
out.push({ kind: "bailout", owner: r.component, target: r.component, prop: c.path, label: `bail out before setting ${c.path} in <${r.component}>`, detail: `${c.path} was set to a value deep-equal to the current one (new reference).` });
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
return out;
|
|
1211
|
+
}
|
|
1212
|
+
var fixKey = (f) => `${f.kind}|${f.owner}|${f.prop || f.target}`;
|
|
1213
|
+
function rankFixes(reports) {
|
|
1214
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1215
|
+
for (const r of reports) {
|
|
1216
|
+
if (!r.avoidable) continue;
|
|
1217
|
+
for (const f of fixesFor(r)) {
|
|
1218
|
+
const key = fixKey(f);
|
|
1219
|
+
let agg = byKey.get(key);
|
|
1220
|
+
if (!agg) {
|
|
1221
|
+
agg = { ...f, key, count: 0, components: {} };
|
|
1222
|
+
byKey.set(key, agg);
|
|
1223
|
+
}
|
|
1224
|
+
agg.count++;
|
|
1225
|
+
agg.components[r.component] = (agg.components[r.component] || 0) + 1;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
return [...byKey.values()].sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
|
|
1229
|
+
}
|
|
1230
|
+
function formatFixes(reports, limit = 10) {
|
|
1231
|
+
const ranked = rankFixes(reports);
|
|
1232
|
+
if (!ranked.length) return "No avoidable re-renders.";
|
|
1233
|
+
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(", ")})`);
|
|
1234
|
+
if (ranked.length > limit) lines.push(` \u2026 ${ranked.length - limit} more`);
|
|
1235
|
+
return lines.join("\n");
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// src/sessions.ts
|
|
1239
|
+
function summarizeReports(reports, meta = {}) {
|
|
1240
|
+
var _a;
|
|
1241
|
+
const byComponent = {};
|
|
1242
|
+
let avoidable = 0;
|
|
1243
|
+
let wasted = 0;
|
|
1244
|
+
for (const r of reports) {
|
|
1245
|
+
const c = byComponent[_a = r.component] || (byComponent[_a] = { total: 0, avoidable: 0, wasted: 0 });
|
|
1246
|
+
c.total++;
|
|
1247
|
+
if (r.avoidable) {
|
|
1248
|
+
c.avoidable++;
|
|
1249
|
+
avoidable++;
|
|
1250
|
+
if (typeof r.selfDuration === "number") {
|
|
1251
|
+
c.wasted += r.selfDuration;
|
|
1252
|
+
wasted += r.selfDuration;
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
return {
|
|
1257
|
+
id: meta.id ?? `s${Date.now().toString(36)}`,
|
|
1258
|
+
name: meta.name ?? "session",
|
|
1259
|
+
startedAt: meta.startedAt ?? (reports[0]?.time ?? 0),
|
|
1260
|
+
endedAt: meta.endedAt ?? (reports[reports.length - 1]?.time ?? null),
|
|
1261
|
+
total: reports.length,
|
|
1262
|
+
avoidable,
|
|
1263
|
+
wasted,
|
|
1264
|
+
byComponent,
|
|
1265
|
+
fixes: rankFixes(reports).map((f) => ({ key: f.key, label: f.label, count: f.count }))
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
function compareSummaries(before, after) {
|
|
1269
|
+
const names = /* @__PURE__ */ new Set([...Object.keys(before.byComponent), ...Object.keys(after.byComponent)]);
|
|
1270
|
+
const rows = [];
|
|
1271
|
+
for (const component of names) {
|
|
1272
|
+
const b = before.byComponent[component]?.avoidable || 0;
|
|
1273
|
+
const a = after.byComponent[component]?.avoidable || 0;
|
|
1274
|
+
if (b || a) rows.push({ component, before: b, after: a, delta: a - b });
|
|
1275
|
+
}
|
|
1276
|
+
rows.sort((x, y) => x.delta - y.delta || y.before - x.before || x.component.localeCompare(y.component));
|
|
1277
|
+
const afterKeys = new Set(after.fixes.map((f) => f.key));
|
|
1278
|
+
const beforeKeys = new Set(before.fixes.map((f) => f.key));
|
|
1279
|
+
return {
|
|
1280
|
+
before,
|
|
1281
|
+
after,
|
|
1282
|
+
rows,
|
|
1283
|
+
total: { before: before.total, after: after.total, delta: after.total - before.total },
|
|
1284
|
+
avoidable: { before: before.avoidable, after: after.avoidable, delta: after.avoidable - before.avoidable },
|
|
1285
|
+
wasted: { before: before.wasted, after: after.wasted, delta: after.wasted - before.wasted },
|
|
1286
|
+
resolvedFixes: before.fixes.filter((f) => !afterKeys.has(f.key)),
|
|
1287
|
+
newFixes: after.fixes.filter((f) => !beforeKeys.has(f.key)),
|
|
1288
|
+
regressions: rows.filter((r) => r.delta > 0)
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
function formatComparison(c) {
|
|
1292
|
+
const w = Math.max(14, ...c.rows.map((r) => r.component.length + 2));
|
|
1293
|
+
const line = (label, b, a, d) => `${label.padEnd(w)} ${String(b).padStart(8)} ${String(a).padStart(8)} ${d.padStart(8)}`;
|
|
1294
|
+
const delta = (n, suffix = "") => n === 0 ? "\xB10" : `${n > 0 ? "+" : ""}${Number.isInteger(n) ? n : n.toFixed(1)}${suffix}`;
|
|
1295
|
+
const out = [line("avoidable", c.before.name.slice(0, 8), c.after.name.slice(0, 8), "\u0394"), line("all components", c.avoidable.before, c.avoidable.after, delta(c.avoidable.delta))];
|
|
1296
|
+
if (c.wasted.before || c.wasted.after) out.push(line("wasted ms", c.wasted.before.toFixed(1), c.wasted.after.toFixed(1), delta(Number(c.wasted.delta.toFixed(1)))));
|
|
1297
|
+
for (const r of c.rows) out.push(line(`<${r.component}>`, r.before, r.after, delta(r.delta)));
|
|
1298
|
+
if (c.resolvedFixes.length) out.push("", "no longer needed:", ...c.resolvedFixes.map((f) => ` - ${f.label}`));
|
|
1299
|
+
if (c.newFixes.length) out.push("", "new:", ...c.newFixes.map((f) => ` - ${f.label}`));
|
|
1300
|
+
return out.join("\n");
|
|
1301
|
+
}
|
|
1302
|
+
function parseExport(data) {
|
|
1303
|
+
if (Array.isArray(data)) return { reports: data, sessions: [], summary: null };
|
|
1304
|
+
if (data && typeof data === "object") {
|
|
1305
|
+
const d = data;
|
|
1306
|
+
if (Array.isArray(d.reports)) return { reports: d.reports, sessions: Array.isArray(d.sessions) ? d.sessions : [], summary: null };
|
|
1307
|
+
if (d.byComponent && typeof d.byComponent === "object") return { reports: [], sessions: [], summary: data };
|
|
1308
|
+
}
|
|
1309
|
+
throw new Error("not a rerender-lens export or session summary");
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// src/budget.ts
|
|
1313
|
+
function avoidableCounts(reports) {
|
|
1314
|
+
const counts2 = {};
|
|
1315
|
+
for (const r of reports) if (r.avoidable) counts2[r.component] = (counts2[r.component] || 0) + 1;
|
|
1316
|
+
return counts2;
|
|
1317
|
+
}
|
|
1318
|
+
function checkBudget(reports, budget) {
|
|
1319
|
+
const b = typeof budget === "number" ? { "*": budget } : budget;
|
|
1320
|
+
const fallback = typeof b["*"] === "number" ? b["*"] : 0;
|
|
1321
|
+
const counts2 = avoidableCounts(reports);
|
|
1322
|
+
const violations = [];
|
|
1323
|
+
const slack = [];
|
|
1324
|
+
const names = /* @__PURE__ */ new Set([...Object.keys(counts2), ...Object.keys(b).filter((k) => k !== "*")]);
|
|
1325
|
+
for (const component of names) {
|
|
1326
|
+
const allowed = typeof b[component] === "number" ? b[component] : fallback;
|
|
1327
|
+
const avoidable = counts2[component] || 0;
|
|
1328
|
+
if (avoidable > allowed) violations.push({ component, avoidable, allowed });
|
|
1329
|
+
else if (avoidable < allowed) slack.push({ component, avoidable, allowed });
|
|
1330
|
+
}
|
|
1331
|
+
violations.sort((x, y) => y.avoidable - y.allowed - (x.avoidable - x.allowed));
|
|
1332
|
+
return { ok: violations.length === 0, violations, slack, counts: counts2 };
|
|
1333
|
+
}
|
|
1334
|
+
function toBudget(reports) {
|
|
1335
|
+
return { "*": 0, ...avoidableCounts(reports) };
|
|
1336
|
+
}
|
|
1337
|
+
function assertWithinBudget(reports, budget) {
|
|
1338
|
+
const result = checkBudget(reports, budget);
|
|
1339
|
+
if (result.ok) return result;
|
|
1340
|
+
const lines = result.violations.map((v) => ` <${v.component}>: ${v.avoidable} avoidable re-render${v.avoidable === 1 ? "" : "s"} (budget ${v.allowed})`);
|
|
1341
|
+
throw new Error(`Re-render budget exceeded:
|
|
1342
|
+
${lines.join("\n")}
|
|
1343
|
+
|
|
1344
|
+
Fixes, most impact first:
|
|
1345
|
+
${formatFixes(reports)}`);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
858
1348
|
// src/notifiers.ts
|
|
859
1349
|
function createCollector() {
|
|
860
1350
|
const reports = [];
|
|
@@ -875,8 +1365,14 @@ function createCollector() {
|
|
|
875
1365
|
const lines = bad.map((r) => ` <${r.component}> render #${r.renderCount}:
|
|
876
1366
|
${r.reasons.map((x) => ` - ${x}`).join("\n")}`);
|
|
877
1367
|
throw new Error(`${bad.length} avoidable re-render${bad.length === 1 ? "" : "s"} detected:
|
|
878
|
-
${lines.join("\n")}
|
|
879
|
-
|
|
1368
|
+
${lines.join("\n")}
|
|
1369
|
+
|
|
1370
|
+
Fixes, most impact first:
|
|
1371
|
+
${formatFixes(reports)}`);
|
|
1372
|
+
},
|
|
1373
|
+
fixes: () => rankFixes(reports),
|
|
1374
|
+
summary: (name) => summarizeReports(reports, name ? { name } : {}),
|
|
1375
|
+
assertWithinBudget: (budget) => assertWithinBudget(reports, budget)
|
|
880
1376
|
};
|
|
881
1377
|
}
|
|
882
1378
|
function combineNotifiers(...notifiers) {
|
|
@@ -974,11 +1470,12 @@ function flash(target, duration = 500) {
|
|
|
974
1470
|
}
|
|
975
1471
|
|
|
976
1472
|
// src/version.ts
|
|
977
|
-
var VERSION = "0.
|
|
1473
|
+
var VERSION = "0.3.0" ;
|
|
978
1474
|
|
|
979
1475
|
// src/devtools.ts
|
|
980
1476
|
var DEVTOOLS_MARKER = "__rerenderLens";
|
|
981
1477
|
var PROTOCOL_VERSION = 2;
|
|
1478
|
+
var DEFAULT_CHANNEL = "rerender-lens";
|
|
982
1479
|
function serialize(value, maxDepth = 6, seen = /* @__PURE__ */ new WeakSet(), depth = 0) {
|
|
983
1480
|
if (value === null || value === void 0) return value;
|
|
984
1481
|
const t = typeof value;
|
|
@@ -1029,7 +1526,7 @@ var stringToMatcher = (s) => {
|
|
|
1029
1526
|
};
|
|
1030
1527
|
function serializeOptions(o) {
|
|
1031
1528
|
const out = {};
|
|
1032
|
-
const bools = ["trackAllMemoized", "trackAllComponents", "trackHooks", "logAll", "silent", "collapse", "ignoreHotReload"];
|
|
1529
|
+
const bools = ["trackAllMemoized", "trackAllComponents", "trackHooks", "includeState", "resolveHookNames", "logAll", "silent", "collapse", "ignoreHotReload"];
|
|
1033
1530
|
for (const k of bools) if (typeof o[k] === "boolean") out[k] = o[k];
|
|
1034
1531
|
if (typeof o.maxReportsPerComponent === "number") out.maxReportsPerComponent = o.maxReportsPerComponent;
|
|
1035
1532
|
if (o.include) out.include = o.include.map(matcherToString).filter((x) => x !== null);
|
|
@@ -1038,7 +1535,7 @@ function serializeOptions(o) {
|
|
|
1038
1535
|
}
|
|
1039
1536
|
function deserializeOptions(o) {
|
|
1040
1537
|
const out = {};
|
|
1041
|
-
const bools = ["trackAllMemoized", "trackAllComponents", "trackHooks", "logAll", "silent", "collapse", "ignoreHotReload"];
|
|
1538
|
+
const bools = ["trackAllMemoized", "trackAllComponents", "trackHooks", "includeState", "resolveHookNames", "logAll", "silent", "collapse", "ignoreHotReload"];
|
|
1042
1539
|
for (const k of bools) if (typeof o[k] === "boolean") out[k] = o[k];
|
|
1043
1540
|
if (typeof o.maxReportsPerComponent === "number") out.maxReportsPerComponent = o.maxReportsPerComponent;
|
|
1044
1541
|
if (Array.isArray(o.include)) out.include = o.include.filter((s) => typeof s === "string" && s).map(stringToMatcher);
|
|
@@ -1069,7 +1566,8 @@ function createDevtoolsNotifier(options = {}) {
|
|
|
1069
1566
|
source,
|
|
1070
1567
|
injected: typeof window !== "undefined" && typeof window.__RERENDER_LENS_INJECTED__ === "string",
|
|
1071
1568
|
scheduled: getState().scheduled,
|
|
1072
|
-
commits: getState().commits
|
|
1569
|
+
commits: getState().commits,
|
|
1570
|
+
overhead: { totalMs: getState().overheadMs, maxCommitMs: getState().maxCommitMs }
|
|
1073
1571
|
});
|
|
1074
1572
|
const bridge = {
|
|
1075
1573
|
replay: () => {
|
|
@@ -1134,12 +1632,126 @@ function createDevtoolsNotifier(options = {}) {
|
|
|
1134
1632
|
}
|
|
1135
1633
|
};
|
|
1136
1634
|
if (typeof window !== "undefined") window.__RERENDER_LENS_DEVTOOLS__ = bridge;
|
|
1137
|
-
|
|
1635
|
+
const runCommand = (data) => {
|
|
1636
|
+
const reply = { __rerenderLensReply: true, id: data.id };
|
|
1637
|
+
try {
|
|
1638
|
+
switch (data.cmd) {
|
|
1639
|
+
case "info":
|
|
1640
|
+
reply.result = info();
|
|
1641
|
+
break;
|
|
1642
|
+
case "pull":
|
|
1643
|
+
reply.result = bridge.pull(typeof data.arg === "number" ? data.arg : 0);
|
|
1644
|
+
break;
|
|
1645
|
+
case "replay":
|
|
1646
|
+
bridge.replay();
|
|
1647
|
+
reply.result = true;
|
|
1648
|
+
break;
|
|
1649
|
+
case "clear":
|
|
1650
|
+
bridge.clear();
|
|
1651
|
+
reply.result = true;
|
|
1652
|
+
break;
|
|
1653
|
+
case "configure":
|
|
1654
|
+
reply.result = bridge.configure(data.arg ?? {});
|
|
1655
|
+
break;
|
|
1656
|
+
case "highlight":
|
|
1657
|
+
reply.result = bridge.highlight(typeof data.arg === "number" ? data.arg : null);
|
|
1658
|
+
break;
|
|
1659
|
+
case "flash":
|
|
1660
|
+
bridge.flashAvoidable(!!data.arg);
|
|
1661
|
+
reply.result = true;
|
|
1662
|
+
break;
|
|
1663
|
+
default:
|
|
1664
|
+
reply.error = `unknown command ${String(data.cmd)}`;
|
|
1665
|
+
}
|
|
1666
|
+
} catch (e) {
|
|
1667
|
+
reply.error = String(e.message || e);
|
|
1668
|
+
}
|
|
1669
|
+
return reply;
|
|
1670
|
+
};
|
|
1671
|
+
const isCommand = (data) => !!data && typeof data === "object" && data.__rerenderLensCmd === true && typeof data.id === "string";
|
|
1672
|
+
const envelope = (type, payload) => ({ [DEVTOOLS_MARKER]: true, version: PROTOCOL_VERSION, type, payload });
|
|
1673
|
+
const sinks = [];
|
|
1674
|
+
if (options.channel && typeof BroadcastChannel === "function") {
|
|
1675
|
+
const name = typeof options.channel === "string" ? options.channel : DEFAULT_CHANNEL;
|
|
1676
|
+
try {
|
|
1677
|
+
const channel = new BroadcastChannel(name);
|
|
1678
|
+
channel.onmessage = (event) => {
|
|
1679
|
+
if (isCommand(event.data)) channel.postMessage(runCommand(event.data));
|
|
1680
|
+
};
|
|
1681
|
+
sinks.push((message) => {
|
|
1682
|
+
try {
|
|
1683
|
+
channel.postMessage(message);
|
|
1684
|
+
} catch {
|
|
1685
|
+
}
|
|
1686
|
+
});
|
|
1687
|
+
} catch {
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
const relayUrl = options.relay ?? (typeof window !== "undefined" ? window.__RERENDER_LENS_RELAY__ : void 0);
|
|
1691
|
+
const ES = options.eventSource ?? (typeof EventSource === "function" ? EventSource : null);
|
|
1692
|
+
if (relayUrl && ES && typeof fetch === "function") {
|
|
1693
|
+
const base = relayUrl.replace(/\/$/, "");
|
|
1694
|
+
let queue = [];
|
|
1695
|
+
let scheduled = false;
|
|
1696
|
+
const flush = () => {
|
|
1697
|
+
scheduled = false;
|
|
1698
|
+
const batch = queue;
|
|
1699
|
+
queue = [];
|
|
1700
|
+
fetch(`${base}/message`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(batch), keepalive: true }).catch(() => {
|
|
1701
|
+
});
|
|
1702
|
+
};
|
|
1703
|
+
const relaySend = (message) => {
|
|
1704
|
+
queue.push(message);
|
|
1705
|
+
if (!scheduled) {
|
|
1706
|
+
scheduled = true;
|
|
1707
|
+
setTimeout(flush, 0);
|
|
1708
|
+
}
|
|
1709
|
+
};
|
|
1710
|
+
try {
|
|
1711
|
+
const stream = new ES(`${base}/events?role=app`);
|
|
1712
|
+
stream.onmessage = (event) => {
|
|
1713
|
+
let parsed;
|
|
1714
|
+
try {
|
|
1715
|
+
parsed = JSON.parse(event.data);
|
|
1716
|
+
} catch {
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
for (const m of Array.isArray(parsed) ? parsed : [parsed]) if (isCommand(m)) relaySend(runCommand(m));
|
|
1720
|
+
};
|
|
1721
|
+
stream.onerror = () => {
|
|
1722
|
+
};
|
|
1723
|
+
sinks.push(relaySend);
|
|
1724
|
+
} catch {
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
const postAll = (type, payload) => {
|
|
1728
|
+
post(type, payload);
|
|
1729
|
+
if (sinks.length) {
|
|
1730
|
+
const message = envelope(type, payload);
|
|
1731
|
+
for (const sink of sinks) sink(message);
|
|
1732
|
+
}
|
|
1733
|
+
};
|
|
1734
|
+
if (sinks.length) {
|
|
1735
|
+
const replay = bridge.replay;
|
|
1736
|
+
const clear = bridge.clear;
|
|
1737
|
+
bridge.replay = () => {
|
|
1738
|
+
for (const sink of sinks) {
|
|
1739
|
+
sink(envelope("hello", info()));
|
|
1740
|
+
for (const e of buffer) sink(envelope("report", e.payload));
|
|
1741
|
+
}
|
|
1742
|
+
replay();
|
|
1743
|
+
};
|
|
1744
|
+
bridge.clear = () => {
|
|
1745
|
+
clear();
|
|
1746
|
+
for (const sink of sinks) sink(envelope("clear"));
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
postAll("hello", info());
|
|
1138
1750
|
return (report) => {
|
|
1139
1751
|
const payload = serialize(report, maxDepth);
|
|
1140
1752
|
buffer.push({ seq: ++seq, instanceId: report.instanceId, payload });
|
|
1141
1753
|
if (buffer.length > bufferSize) buffer.splice(0, buffer.length - bufferSize);
|
|
1142
|
-
|
|
1754
|
+
postAll("report", payload);
|
|
1143
1755
|
if (flashOn && report.avoidable && report.instanceId) {
|
|
1144
1756
|
const fiber = fiberById(report.instanceId);
|
|
1145
1757
|
if (fiber) flash({ nodes: hostNodesOf(fiber), label: report.component });
|
|
@@ -1147,6 +1759,6 @@ function createDevtoolsNotifier(options = {}) {
|
|
|
1147
1759
|
};
|
|
1148
1760
|
}
|
|
1149
1761
|
|
|
1150
|
-
export { DEVTOOLS_MARKER, MARKER, PROTOCOL_VERSION, VERSION, buildReport, classify, combineNotifiers, configure, createCollector, createDevtoolsNotifier, deepEqual, deserializeOptions, diffRecords, disable, ensureDevtoolsHook, getDisplayName, getRenderers, init, isEnabled, isProductionReact, printReport, serialize, serializeOptions, shouldTrack, summarize, track, useWhyRerender };
|
|
1762
|
+
export { DEFAULT_CHANNEL, DEVTOOLS_MARKER, MARKER, PROTOCOL_VERSION, VERSION, assertWithinBudget, avoidableCounts, buildReport, checkBudget, classify, combineNotifiers, compareSummaries, configure, createCollector, createDevtoolsNotifier, customHooksFromStack, deepEqual, deserializeOptions, diffRecords, disable, ensureDevtoolsHook, fixKey, fixesFor, formatComparison, formatFixes, getDisplayName, getRenderers, hookLabel, init, isEnabled, isProductionReact, parseExport, printReport, rankFixes, resolveHookNames, serialize, serializeOptions, shouldTrack, storeAdvice, summarize, summarizeReports, toBudget, track, useWhyRerender };
|
|
1151
1763
|
//# sourceMappingURL=index.js.map
|
|
1152
1764
|
//# sourceMappingURL=index.js.map
|