rerender-lens 0.1.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.
@@ -0,0 +1,562 @@
1
+ 'use strict';
2
+
3
+ // src/types.ts
4
+ var MARKER = "rerenderLens";
5
+
6
+ // src/diff.ts
7
+ var REACT_ELEMENT = /* @__PURE__ */ Symbol.for("react.element");
8
+ var REACT_TRANSITIONAL_ELEMENT = /* @__PURE__ */ Symbol.for("react.transitional.element");
9
+ function isReactElement(v) {
10
+ if (typeof v !== "object" || v === null) return false;
11
+ const t = v.$$typeof;
12
+ return t === REACT_ELEMENT || t === REACT_TRANSITIONAL_ELEMENT;
13
+ }
14
+ function isPlainObject(v) {
15
+ if (typeof v !== "object" || v === null) return false;
16
+ const proto = Object.getPrototypeOf(v);
17
+ return proto === Object.prototype || proto === null;
18
+ }
19
+ function remember(seen, a, b) {
20
+ let set = seen.get(a);
21
+ if (!set) {
22
+ set = /* @__PURE__ */ new Set();
23
+ seen.set(a, set);
24
+ }
25
+ if (set.has(b)) return true;
26
+ set.add(b);
27
+ return false;
28
+ }
29
+ function deepEqual(a, b, seen = /* @__PURE__ */ new Map()) {
30
+ if (Object.is(a, b)) return true;
31
+ if (typeof a !== typeof b) return false;
32
+ if (typeof a !== "object" || a === null || b === null) return false;
33
+ const objA = a;
34
+ const objB = b;
35
+ if (remember(seen, objA, objB)) return true;
36
+ if (a instanceof Date) return b instanceof Date && a.getTime() === b.getTime();
37
+ if (a instanceof RegExp) return b instanceof RegExp && a.source === b.source && a.flags === b.flags;
38
+ if (Array.isArray(a)) {
39
+ if (!Array.isArray(b) || a.length !== b.length) return false;
40
+ for (let i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i], seen)) return false;
41
+ return true;
42
+ }
43
+ if (Array.isArray(b)) return false;
44
+ if (ArrayBuffer.isView(a)) {
45
+ if (!ArrayBuffer.isView(b) || a.constructor !== b.constructor) return false;
46
+ const ta = a;
47
+ const tb = b;
48
+ if (ta.length !== tb.length) return false;
49
+ for (let i = 0; i < ta.length; i++) if (ta[i] !== tb[i]) return false;
50
+ return true;
51
+ }
52
+ if (a instanceof Map) {
53
+ if (!(b instanceof Map) || a.size !== b.size) return false;
54
+ for (const [k, v] of a) {
55
+ if (!b.has(k) || !deepEqual(v, b.get(k), seen)) return false;
56
+ }
57
+ return true;
58
+ }
59
+ if (a instanceof Set) {
60
+ if (!(b instanceof Set) || a.size !== b.size) return false;
61
+ outer: for (const v of a) {
62
+ if (b.has(v)) continue;
63
+ for (const w of b) if (deepEqual(v, w, seen)) continue outer;
64
+ return false;
65
+ }
66
+ return true;
67
+ }
68
+ if (isReactElement(a)) {
69
+ if (!isReactElement(b)) return false;
70
+ return a.type === b.type && a.key === b.key && deepEqual(a.props, b.props, seen);
71
+ }
72
+ if (isPlainObject(a) && isPlainObject(b)) {
73
+ const ka = Object.keys(a);
74
+ const kb = Object.keys(b);
75
+ if (ka.length !== kb.length) return false;
76
+ for (const k of ka) {
77
+ if (!Object.prototype.hasOwnProperty.call(b, k)) return false;
78
+ if (!deepEqual(a[k], b[k], seen)) return false;
79
+ }
80
+ return true;
81
+ }
82
+ return false;
83
+ }
84
+ function classify(prev, next) {
85
+ if (typeof prev === "function" && typeof next === "function") {
86
+ return prev.name === next.name && prev.toString() === next.toString() ? "function" : "different";
87
+ }
88
+ if (isReactElement(prev) && isReactElement(next)) {
89
+ return deepEqual(prev, next) ? "element" : "different";
90
+ }
91
+ return deepEqual(prev, next) ? "deep-equal" : "different";
92
+ }
93
+ function joinPath(base, key) {
94
+ if (typeof key === "number") return `${base}[${key}]`;
95
+ if (base === "") return key;
96
+ return /^[A-Za-z_$][\w$]*$/.test(key) ? `${base}.${key}` : `${base}[${JSON.stringify(key)}]`;
97
+ }
98
+ function diffRecords(prev, next, basePath = "") {
99
+ const p = prev ?? {};
100
+ const n = next ?? {};
101
+ const changes = [];
102
+ const keys = /* @__PURE__ */ new Set([...Object.keys(p), ...Object.keys(n)]);
103
+ for (const key of keys) {
104
+ const inPrev = Object.prototype.hasOwnProperty.call(p, key);
105
+ const inNext = Object.prototype.hasOwnProperty.call(n, key);
106
+ const path = joinPath(basePath, key);
107
+ if (inPrev && !inNext) {
108
+ changes.push({ path, kind: "removed", prev: p[key], next: void 0 });
109
+ continue;
110
+ }
111
+ if (!inPrev && inNext) {
112
+ changes.push({ path, kind: "added", prev: void 0, next: n[key] });
113
+ continue;
114
+ }
115
+ const a = p[key];
116
+ const b = n[key];
117
+ if (Object.is(a, b)) continue;
118
+ const kind = classify(a, b);
119
+ if (kind === "different") {
120
+ changes.push({ path: firstDifferentPath(a, b, path), kind, prev: a, next: b });
121
+ } else {
122
+ changes.push({ path, kind, prev: a, next: b });
123
+ }
124
+ }
125
+ return changes;
126
+ }
127
+ function firstDifferentPath(a, b, path, depth = 0) {
128
+ if (depth > 8) return path;
129
+ if (Array.isArray(a) && Array.isArray(b)) {
130
+ if (a.length !== b.length) return `${path}.length`;
131
+ for (let i = 0; i < a.length; i++) {
132
+ if (!deepEqual(a[i], b[i])) return firstDifferentPath(a[i], b[i], joinPath(path, i), depth + 1);
133
+ }
134
+ return path;
135
+ }
136
+ if (isPlainObject(a) && isPlainObject(b)) {
137
+ const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
138
+ for (const k of keys) {
139
+ if (!deepEqual(a[k], b[k])) return firstDifferentPath(a[k], b[k], joinPath(path, k), depth + 1);
140
+ }
141
+ return path;
142
+ }
143
+ return path;
144
+ }
145
+
146
+ // src/report.ts
147
+ var now = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
148
+ var isGenuine = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
149
+ function fixFor(change) {
150
+ switch (change.kind) {
151
+ case "function":
152
+ return `wrap it in useCallback (or hoist it out of the parent's render)`;
153
+ case "element":
154
+ return `memoize the element with useMemo, or pass it as children from a stable parent`;
155
+ case "deep-equal":
156
+ return Array.isArray(change.next) ? `memoize the array with useMemo (same items, new reference)` : `memoize the object with useMemo, or hoist it to module scope if it is constant`;
157
+ default:
158
+ return "";
159
+ }
160
+ }
161
+ function describe(change) {
162
+ switch (change.kind) {
163
+ case "deep-equal":
164
+ return `prop "${change.path}" is a new reference but deep-equal to the previous value`;
165
+ case "function":
166
+ return `prop "${change.path}" is a new function instance on every render`;
167
+ case "element":
168
+ return `prop "${change.path}" is a new React element with the same type and props`;
169
+ case "different":
170
+ return `prop "${change.path}" changed`;
171
+ case "added":
172
+ return `prop "${change.path}" was added`;
173
+ case "removed":
174
+ return `prop "${change.path}" was removed`;
175
+ }
176
+ }
177
+ function buildReport(input) {
178
+ const stateChanges = input.stateChanges ?? [];
179
+ const hookChanges = input.hookChanges ?? [];
180
+ const isStateHook = (h) => h.hook === "useState" || h.hook === "useReducer";
181
+ const genuineProps = input.propChanges.some(isGenuine);
182
+ const genuineState = stateChanges.some(isGenuine) || hookChanges.some((h) => isStateHook(h) && isGenuine(h));
183
+ const genuineHooks = hookChanges.some((h) => !isStateHook(h) && isGenuine(h));
184
+ const causes = [genuineProps && "props", genuineState && "state", genuineHooks && "hooks"].filter(Boolean);
185
+ let trigger;
186
+ if (causes.length === 0) trigger = "parent";
187
+ else if (causes.length === 1) trigger = causes[0];
188
+ else trigger = "mixed";
189
+ const avoidable = trigger === "parent";
190
+ const reasons = [];
191
+ if (avoidable && input.propChanges.length === 0 && stateChanges.length === 0 && hookChanges.length === 0) {
192
+ reasons.push(
193
+ `re-rendered with identical props because its parent re-rendered. Wrap "${input.component}" in React.memo (or extend PureComponent).`
194
+ );
195
+ }
196
+ for (const c of input.propChanges) {
197
+ const fix = fixFor(c);
198
+ reasons.push(fix ? `${describe(c)}: ${fix}.` : `${describe(c)}.`);
199
+ }
200
+ for (const c of stateChanges) {
201
+ if (isGenuine(c)) reasons.push(`state "${c.path}" changed.`);
202
+ else reasons.push(`setState was called with a value deep-equal to the current "${c.path}" (new reference, same contents).`);
203
+ }
204
+ for (const c of hookChanges) {
205
+ if (isGenuine(c)) reasons.push(`${c.hook} #${c.index} changed.`);
206
+ else if (isStateHook(c))
207
+ reasons.push(`${c.hook} #${c.index} 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.`);
208
+ else reasons.push(`${c.hook} #${c.index} returned a new reference that is deep-equal to the previous value: memoize the context/store value where it is produced.`);
209
+ }
210
+ return {
211
+ component: input.component,
212
+ renderCount: input.renderCount,
213
+ trigger,
214
+ avoidable,
215
+ props: { prev: input.prevProps, next: input.nextProps },
216
+ propChanges: input.propChanges,
217
+ stateChanges,
218
+ hookChanges,
219
+ reasons,
220
+ time: now()
221
+ };
222
+ }
223
+ var KIND_LABEL = {
224
+ "deep-equal": "equal by value",
225
+ function: "new function",
226
+ element: "equal element",
227
+ different: "changed",
228
+ added: "added",
229
+ removed: "removed"
230
+ };
231
+ function summarize(report) {
232
+ const counts = /* @__PURE__ */ new Map();
233
+ for (const c of [...report.propChanges, ...report.stateChanges, ...report.hookChanges]) {
234
+ counts.set(c.kind, (counts.get(c.kind) ?? 0) + 1);
235
+ }
236
+ const parts = [...counts].map(([kind, n]) => `${n} ${KIND_LABEL[kind]}`);
237
+ if (parts.length === 0) parts.push("no changes");
238
+ return parts.join(", ");
239
+ }
240
+ function printReport(report, options) {
241
+ const c = options.console ?? console;
242
+ const open = options.collapse === false ? c.group : c.groupCollapsed;
243
+ const verdict = report.avoidable ? "avoidable re-render" : `re-render (${report.trigger})`;
244
+ open.call(c, `[rerender-lens] <${report.component}> ${verdict}: ${summarize(report)}`);
245
+ for (const r of report.reasons) c.log(`- ${r}`);
246
+ for (const ch of [...report.propChanges, ...report.stateChanges, ...report.hookChanges]) {
247
+ c.log(`${ch.path} (${KIND_LABEL[ch.kind]})`, { prev: ch.prev, next: ch.next });
248
+ }
249
+ c.log("props", report.props);
250
+ c.groupEnd();
251
+ }
252
+
253
+ // src/state.ts
254
+ var KEY = /* @__PURE__ */ Symbol.for("rerender-lens.state");
255
+ function getState() {
256
+ const g = globalThis;
257
+ let s = g[KEY];
258
+ if (!s) {
259
+ s = {
260
+ React: null,
261
+ options: {},
262
+ generation: 0,
263
+ decisions: /* @__PURE__ */ new WeakMap(),
264
+ wrappers: /* @__PURE__ */ new WeakMap(),
265
+ wrapperSet: /* @__PURE__ */ new WeakSet(),
266
+ originals: {},
267
+ capture: null,
268
+ patched: false
269
+ };
270
+ g[KEY] = s;
271
+ }
272
+ return s;
273
+ }
274
+ function dispatch(report, override) {
275
+ const options = override ? { ...getState().options, ...override } : getState().options;
276
+ if (!options.silent && (report.avoidable || options.logAll)) {
277
+ printReport(report, options);
278
+ }
279
+ if (options.notifier) {
280
+ try {
281
+ options.notifier(report);
282
+ } catch (err) {
283
+ (options.console ?? console).warn("[rerender-lens] notifier threw", err);
284
+ }
285
+ }
286
+ }
287
+
288
+ // src/tracker.ts
289
+ var MEMO = /* @__PURE__ */ Symbol.for("react.memo");
290
+ var FORWARD_REF = /* @__PURE__ */ Symbol.for("react.forward_ref");
291
+ var isMemo = (t) => typeof t === "object" && t !== null && t.$$typeof === MEMO;
292
+ var isForwardRef = (t) => typeof t === "object" && t !== null && t.$$typeof === FORWARD_REF;
293
+ var isClass = (t) => typeof t === "function" && !!t.prototype?.isReactComponent;
294
+ var isPureClass = (t) => isClass(t) && !!t.prototype.isPureReactComponent;
295
+ var isComponentLike = (t) => typeof t === "function" || isMemo(t) || isForwardRef(t);
296
+ function getDisplayName(type) {
297
+ if (typeof type === "string") return type;
298
+ if (isMemo(type)) return type.displayName ?? getDisplayName(type.type);
299
+ if (isForwardRef(type)) return type.displayName ?? getDisplayName(type.render);
300
+ if (typeof type === "function") {
301
+ const t = type;
302
+ return t.displayName ?? (t.name || "Anonymous");
303
+ }
304
+ return "Anonymous";
305
+ }
306
+ function hasMarker(type) {
307
+ if (type === null || typeof type !== "function" && typeof type !== "object") return false;
308
+ if (type[MARKER] === true) return true;
309
+ if (isMemo(type)) return hasMarker(type.type);
310
+ if (isForwardRef(type)) return hasMarker(type.render);
311
+ return false;
312
+ }
313
+ function matches(m, name) {
314
+ if (typeof m === "string") return m === name;
315
+ if (m instanceof RegExp) return m.test(name);
316
+ return m(name);
317
+ }
318
+ function shouldTrack(type, o) {
319
+ if (!isComponentLike(type)) return false;
320
+ const name = getDisplayName(type);
321
+ if (o.exclude?.some((m) => matches(m, name))) return false;
322
+ if (hasMarker(type)) return true;
323
+ if (o.include?.some((m) => matches(m, name))) return true;
324
+ if (o.trackAllComponents) return true;
325
+ if (o.trackAllMemoized) return isMemo(type) || isPureClass(type);
326
+ return false;
327
+ }
328
+ var SKIP_STATICS = /* @__PURE__ */ new Set(["length", "name", "prototype", "arguments", "caller", "$$typeof", "render", "type", "compare"]);
329
+ function hoistStatics(from, to) {
330
+ for (const key of Object.getOwnPropertyNames(from)) {
331
+ if (SKIP_STATICS.has(key)) continue;
332
+ const desc = Object.getOwnPropertyDescriptor(from, key);
333
+ if (desc) {
334
+ try {
335
+ Object.defineProperty(to, key, desc);
336
+ } catch {
337
+ }
338
+ }
339
+ }
340
+ }
341
+ function sameHookRefs(prev, next) {
342
+ if (!prev || !next) return true;
343
+ if (prev.length !== next.length) return false;
344
+ for (let i = 0; i < prev.length; i++) if (!Object.is(prev[i].value, next[i].value)) return false;
345
+ return true;
346
+ }
347
+ function diffHooks(prev, next) {
348
+ if (!prev || !next) return [];
349
+ const out = [];
350
+ const len = Math.max(prev.length, next.length);
351
+ for (let i = 0; i < len; i++) {
352
+ const a = prev[i];
353
+ const b = next[i];
354
+ if (!a || !b) continue;
355
+ if (Object.is(a.value, b.value)) continue;
356
+ out.push({ path: `${b.hook}#${i}`, hook: b.hook, index: i, kind: classify(a.value, b.value), prev: a.value, next: b.value });
357
+ }
358
+ return out;
359
+ }
360
+ function wrapRender(R, render, name) {
361
+ const Wrapped = function(props, second) {
362
+ const s = getState();
363
+ const store = R.useRef(null);
364
+ const capture = s.options.trackHooks === false ? null : [];
365
+ const outer = s.capture;
366
+ s.capture = capture;
367
+ let result;
368
+ try {
369
+ result = render(props, second);
370
+ } finally {
371
+ s.capture = outer;
372
+ }
373
+ const prev = store.current;
374
+ if (!prev || !s.patched) {
375
+ store.current = { props, hooks: capture, count: prev?.count ?? 0 };
376
+ return result;
377
+ }
378
+ const sameProps = prev.props === props;
379
+ if (sameProps && sameHookRefs(prev.hooks, capture)) {
380
+ store.current = { props, hooks: capture, count: prev.count };
381
+ return result;
382
+ }
383
+ const count = prev.count + 1;
384
+ store.current = { props, hooks: capture, count };
385
+ dispatch(
386
+ buildReport({
387
+ component: name,
388
+ renderCount: count,
389
+ prevProps: prev.props,
390
+ nextProps: props,
391
+ propChanges: sameProps ? [] : diffRecords(prev.props, props),
392
+ hookChanges: diffHooks(prev.hooks, capture)
393
+ })
394
+ );
395
+ return result;
396
+ };
397
+ hoistStatics(render, Wrapped);
398
+ Object.defineProperty(Wrapped, "name", { value: name, configurable: true });
399
+ Wrapped.displayName = name;
400
+ return Wrapped;
401
+ }
402
+ var COUNT = /* @__PURE__ */ Symbol.for("rerender-lens.count");
403
+ function wrapClass(Original, name) {
404
+ class Tracked extends Original {
405
+ componentDidUpdate(prevProps, prevState, snapshot) {
406
+ const s = getState();
407
+ if (s.patched) {
408
+ const self = this;
409
+ const count = self[COUNT] = (self[COUNT] ?? 0) + 1;
410
+ const propChanges = prevProps === this.props ? [] : diffRecords(prevProps, this.props);
411
+ const stateChanges = prevState === this.state ? [] : diffRecords(prevState ?? {}, this.state ?? {});
412
+ dispatch(
413
+ buildReport({
414
+ component: name,
415
+ renderCount: count,
416
+ prevProps,
417
+ nextProps: this.props,
418
+ propChanges,
419
+ stateChanges
420
+ })
421
+ );
422
+ }
423
+ const superDidUpdate = Original.prototype.componentDidUpdate;
424
+ if (superDidUpdate) superDidUpdate.call(this, prevProps, prevState, snapshot);
425
+ }
426
+ }
427
+ Tracked.displayName = name;
428
+ return Tracked;
429
+ }
430
+ function createWrapper(R, type) {
431
+ const name = getDisplayName(type);
432
+ if (isMemo(type)) {
433
+ const inner = createWrapper(R, type.type);
434
+ if (!inner) return null;
435
+ const wrapped = R.memo(inner, type.compare ?? void 0);
436
+ hoistStatics(type, wrapped);
437
+ wrapped.displayName = name;
438
+ return wrapped;
439
+ }
440
+ if (isForwardRef(type)) {
441
+ const wrapped = R.forwardRef(wrapRender(R, type.render, name));
442
+ hoistStatics(type, wrapped);
443
+ wrapped.displayName = name;
444
+ return wrapped;
445
+ }
446
+ if (isClass(type)) return wrapClass(type, name);
447
+ if (typeof type === "function") return wrapRender(R, type, name);
448
+ return null;
449
+ }
450
+ function resolveType(type) {
451
+ const s = getState();
452
+ if (!s.patched || !s.React) return type;
453
+ if (type === null || typeof type !== "function" && typeof type !== "object") return type;
454
+ if (s.wrapperSet.has(type)) return type;
455
+ let tracked;
456
+ const d = s.decisions.get(type);
457
+ if (d && d.generation === s.generation) {
458
+ tracked = d.tracked;
459
+ } else {
460
+ tracked = shouldTrack(type, s.options);
461
+ s.decisions.set(type, { generation: s.generation, tracked });
462
+ }
463
+ if (!tracked) return type;
464
+ let w = s.wrappers.get(type);
465
+ if (!w) {
466
+ const created = createWrapper(s.React, type);
467
+ if (!created) return type;
468
+ w = created;
469
+ s.wrappers.set(type, w);
470
+ s.wrapperSet.add(w);
471
+ }
472
+ return w;
473
+ }
474
+ var HOOK_KEYS = ["useState", "useReducer", "useContext", "useSyncExternalStore"];
475
+ function assign(target, key, value) {
476
+ try {
477
+ target[key] = value;
478
+ } catch {
479
+ }
480
+ if (target[key] !== value) {
481
+ throw new Error(
482
+ `[rerender-lens] Cannot patch React.${key}. Pass the default import (\`import React from 'react'\`), not a namespace import.`
483
+ );
484
+ }
485
+ }
486
+ function init(R, options = {}) {
487
+ const s = getState();
488
+ if (s.patched) {
489
+ configure(options);
490
+ return disable;
491
+ }
492
+ s.React = R;
493
+ s.options = { ...options };
494
+ s.generation++;
495
+ const origCreateElement = R.createElement;
496
+ s.originals.createElement = origCreateElement;
497
+ assign(R, "createElement", function createElement(...args) {
498
+ args[0] = resolveType(args[0]);
499
+ return origCreateElement.apply(R, args);
500
+ });
501
+ for (const key of HOOK_KEYS) {
502
+ const orig = R[key];
503
+ if (typeof orig !== "function") continue;
504
+ s.originals[key] = orig;
505
+ const patched = function(...args) {
506
+ const r = orig.apply(this, args);
507
+ const cap = getState().capture;
508
+ if (cap) cap.push({ hook: key, value: key === "useState" || key === "useReducer" ? r[0] : r });
509
+ return r;
510
+ };
511
+ Object.defineProperty(patched, "name", { value: key, configurable: true });
512
+ assign(R, key, patched);
513
+ }
514
+ s.patched = true;
515
+ return disable;
516
+ }
517
+ function configure(options) {
518
+ const s = getState();
519
+ s.options = { ...s.options, ...options };
520
+ s.generation++;
521
+ }
522
+ function disable() {
523
+ const s = getState();
524
+ if (!s.patched || !s.React) return;
525
+ const R = s.React;
526
+ for (const [key, orig] of Object.entries(s.originals)) {
527
+ if (orig) R[key] = orig;
528
+ }
529
+ s.originals = {};
530
+ s.patched = false;
531
+ s.React = null;
532
+ s.capture = null;
533
+ }
534
+ function isEnabled() {
535
+ return getState().patched;
536
+ }
537
+ function track(component, name) {
538
+ const t = component;
539
+ t[MARKER] = true;
540
+ if (name) t.displayName = name;
541
+ return component;
542
+ }
543
+
544
+ exports.MARKER = MARKER;
545
+ exports.buildReport = buildReport;
546
+ exports.classify = classify;
547
+ exports.configure = configure;
548
+ exports.deepEqual = deepEqual;
549
+ exports.diffRecords = diffRecords;
550
+ exports.disable = disable;
551
+ exports.dispatch = dispatch;
552
+ exports.getDisplayName = getDisplayName;
553
+ exports.getState = getState;
554
+ exports.init = init;
555
+ exports.isEnabled = isEnabled;
556
+ exports.isReactElement = isReactElement;
557
+ exports.printReport = printReport;
558
+ exports.resolveType = resolveType;
559
+ exports.summarize = summarize;
560
+ exports.track = track;
561
+ //# sourceMappingURL=chunk-DOYB4MKH.cjs.map
562
+ //# sourceMappingURL=chunk-DOYB4MKH.cjs.map