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