react-render-detective 0.4.0 → 0.6.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +299 -0
  2. package/README.md +2 -2
  3. package/dist/babel.cjs +2 -2
  4. package/dist/babel.d.cts +11 -0
  5. package/dist/babel.d.ts +11 -0
  6. package/dist/babel.js +1 -1
  7. package/dist/{chunk-QOGTEVBP.js → chunk-3TZNM7JO.js} +103 -4
  8. package/dist/chunk-3TZNM7JO.js.map +1 -0
  9. package/dist/{chunk-HOTY7J3X.js → chunk-D7M4UC3Y.js} +26 -2
  10. package/dist/chunk-D7M4UC3Y.js.map +1 -0
  11. package/dist/{chunk-PHYYA67T.cjs → chunk-EXDRRUUQ.cjs} +69 -2
  12. package/dist/chunk-EXDRRUUQ.cjs.map +1 -0
  13. package/dist/{chunk-DZ3BZ654.cjs → chunk-LCGXVRJR.cjs} +103 -4
  14. package/dist/chunk-LCGXVRJR.cjs.map +1 -0
  15. package/dist/{chunk-MCIQMYNR.js → chunk-QVHAC7AD.js} +69 -2
  16. package/dist/chunk-QVHAC7AD.js.map +1 -0
  17. package/dist/{chunk-JD4IJ4MN.cjs → chunk-RGT44QQI.cjs} +26 -2
  18. package/dist/chunk-RGT44QQI.cjs.map +1 -0
  19. package/dist/core.cjs +18 -18
  20. package/dist/core.d.cts +27 -3
  21. package/dist/core.d.ts +27 -3
  22. package/dist/core.js +1 -1
  23. package/dist/index.cjs +197 -295
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +35 -115
  26. package/dist/index.d.ts +35 -115
  27. package/dist/index.js +166 -258
  28. package/dist/index.js.map +1 -1
  29. package/dist/interactions.cjs +252 -0
  30. package/dist/interactions.cjs.map +1 -0
  31. package/dist/interactions.d.cts +110 -0
  32. package/dist/interactions.d.ts +110 -0
  33. package/dist/interactions.js +241 -0
  34. package/dist/interactions.js.map +1 -0
  35. package/dist/overlay.cjs +5 -5
  36. package/dist/overlay.js +2 -2
  37. package/dist/testing.d.cts +1 -1
  38. package/dist/testing.d.ts +1 -1
  39. package/dist/{types-gl13xwmN.d.cts → types-CRET8EhB.d.cts} +20 -2
  40. package/dist/{types-gl13xwmN.d.ts → types-CRET8EhB.d.ts} +20 -2
  41. package/dist/vite.cjs +2 -2
  42. package/dist/vite.js +1 -1
  43. package/package.json +14 -4
  44. package/dist/chunk-DZ3BZ654.cjs.map +0 -1
  45. package/dist/chunk-HOTY7J3X.js.map +0 -1
  46. package/dist/chunk-JD4IJ4MN.cjs.map +0 -1
  47. package/dist/chunk-MCIQMYNR.js.map +0 -1
  48. package/dist/chunk-PHYYA67T.cjs.map +0 -1
  49. package/dist/chunk-QOGTEVBP.js.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
- var chunkJD4IJ4MN_cjs = require('./chunk-JD4IJ4MN.cjs');
4
- var chunkDZ3BZ654_cjs = require('./chunk-DZ3BZ654.cjs');
3
+ var chunkRGT44QQI_cjs = require('./chunk-RGT44QQI.cjs');
5
4
  var chunkH5RG2EPP_cjs = require('./chunk-H5RG2EPP.cjs');
5
+ var chunkLCGXVRJR_cjs = require('./chunk-LCGXVRJR.cjs');
6
6
  var React = require('react');
7
7
  var jsxRuntime = require('react/jsx-runtime');
8
8
 
@@ -26,6 +26,40 @@ function _interopNamespace(e) {
26
26
 
27
27
  var React__namespace = /*#__PURE__*/_interopNamespace(React);
28
28
 
29
+ // src/console/style.ts
30
+ var PALETTE = {
31
+ plain: "",
32
+ dim: "color:#8a94a6",
33
+ strong: "font-weight:600",
34
+ good: "color:#1f9d5b",
35
+ warn: "color:#c2820a",
36
+ bad: "color:#d1493f;font-weight:600"
37
+ };
38
+ var supportsStyling = () => {
39
+ try {
40
+ return typeof window !== "undefined" && typeof document !== "undefined";
41
+ } catch {
42
+ return false;
43
+ }
44
+ };
45
+ function styled(segments) {
46
+ if (!supportsStyling()) {
47
+ return [segments.map((s) => typeof s === "string" ? s : s[0]).join("")];
48
+ }
49
+ let format = "";
50
+ const styles = [];
51
+ for (const segment of segments) {
52
+ if (typeof segment === "string") {
53
+ format += segment.replace(/%/g, "%%");
54
+ continue;
55
+ }
56
+ const [text, tone] = segment;
57
+ format += `%c${text.replace(/%/g, "%%")}%c`;
58
+ styles.push(PALETTE[tone], "");
59
+ }
60
+ return [format, ...styles];
61
+ }
62
+
29
63
  // src/console/reporter.ts
30
64
  var ICON = {
31
65
  normal: "\xB7",
@@ -34,30 +68,102 @@ var ICON = {
34
68
  "very-slow": "\u25B2\u25B2",
35
69
  critical: "\u25A0"
36
70
  };
71
+ var BATCH_WINDOW_MS = 400;
37
72
  function attachConsoleReporter(detective) {
38
- return detective.subscribe((event) => {
39
- const mode = detective.config.mode;
40
- if (mode === "silent") return;
73
+ let pending = [];
74
+ let timer;
75
+ const flush = () => {
76
+ timer = void 0;
77
+ const batch = pending;
78
+ pending = [];
79
+ if (batch.length === 0) return;
41
80
  try {
42
- if (mode === "verbose") printVerbose(event);
43
- else printConcise(event, detective.config.slowRenderThreshold);
81
+ printBatch(batch, detective.config.slowRenderThreshold);
44
82
  } catch {
45
83
  }
84
+ };
85
+ const unsubscribe = detective.subscribe((event) => {
86
+ const mode = detective.config.mode;
87
+ if (mode === "silent") return;
88
+ if (mode === "verbose") {
89
+ try {
90
+ printVerbose(event);
91
+ } catch {
92
+ }
93
+ return;
94
+ }
95
+ pending.push(event);
96
+ if (timer === void 0) {
97
+ timer = setTimeout(flush, BATCH_WINDOW_MS);
98
+ timer.unref?.();
99
+ }
46
100
  });
101
+ return () => {
102
+ unsubscribe();
103
+ if (timer !== void 0) clearTimeout(timer);
104
+ };
47
105
  }
48
- function printConcise(event, slowThreshold) {
49
- const { diagnosis, timings, component } = event;
50
- const changed = event.changedProps.map((c) => c.key).join(", ");
51
- const parts = [
52
- `[RRD] ${component.name} #${event.renderNumber}`,
53
- `Reason: ${label(event)}`,
54
- changed ? `Changed: ${changed}` : void 0,
55
- timings.subtreeDuration > 0 ? `Duration: ${timings.selfDuration.toFixed(1)}ms` : void 0
56
- ].filter(Boolean);
57
- const line = `${ICON[diagnosis.severity] ?? "\xB7"} ${parts.join(" ")}`;
58
- const slow = timings.selfDuration >= slowThreshold;
59
- if (slow) console.warn(line);
60
- else console.log(line);
106
+ function printBatch(batch, slowThreshold) {
107
+ const byComponent = /* @__PURE__ */ new Map();
108
+ let totalMs = 0;
109
+ let avoidable = 0;
110
+ for (const event of batch) {
111
+ const key = event.component.name;
112
+ const entry = byComponent.get(key) ?? {
113
+ name: key,
114
+ source: event.component.source,
115
+ count: 0,
116
+ totalMs: 0,
117
+ reasons: /* @__PURE__ */ new Map(),
118
+ notableReasons: /* @__PURE__ */ new Map(),
119
+ avoidable: 0,
120
+ remount: false,
121
+ slow: false
122
+ };
123
+ entry.count++;
124
+ entry.totalMs += event.timings.selfDuration;
125
+ entry.reasons.set(event.diagnosis.reason, (entry.reasons.get(event.diagnosis.reason) ?? 0) + 1);
126
+ if (event.diagnosis.potentiallyAvoidable || event.timings.selfDuration >= slowThreshold) {
127
+ entry.notableReasons.set(event.diagnosis.reason, (entry.notableReasons.get(event.diagnosis.reason) ?? 0) + 1);
128
+ }
129
+ if (event.diagnosis.potentiallyAvoidable) entry.avoidable++;
130
+ if (event.diagnosis.summary.includes("rebuilt")) entry.remount = true;
131
+ if (event.timings.selfDuration >= slowThreshold) entry.slow = true;
132
+ if (!entry.worst || event.timings.selfDuration > entry.worst.timings.selfDuration) entry.worst = event;
133
+ byComponent.set(key, entry);
134
+ totalMs += event.timings.selfDuration;
135
+ if (event.diagnosis.potentiallyAvoidable) avoidable++;
136
+ }
137
+ const notable = [...byComponent.values()].filter((a) => a.avoidable > 0 || a.slow || a.remount);
138
+ if (notable.length === 0) return;
139
+ const ranked = [...notable].sort((a, b) => b.totalMs - a.totalMs);
140
+ const anySlow = ranked.some((a) => a.slow);
141
+ const segments = [
142
+ ["[RRD] ", "dim"],
143
+ [`${batch.length} render${batch.length === 1 ? "" : "s"}`, "strong"],
144
+ [` \xB7 ${totalMs.toFixed(1)}ms`, "dim"]
145
+ ];
146
+ if (avoidable > 0) segments.push([` \xB7 ${avoidable} potentially avoidable`, "warn"]);
147
+ for (const a of ranked.slice(0, 8)) {
148
+ const reasonSource = a.notableReasons.size > 0 ? a.notableReasons : a.reasons;
149
+ const reason = [...reasonSource.entries()].sort((x, y) => y[1] - x[1])[0]?.[0] ?? "unknown";
150
+ const tone = a.remount ? "bad" : a.slow ? "warn" : a.avoidable > 0 ? "warn" : "good";
151
+ const label2 = ` ${ICON[a.slow ? "slow" : "normal"]} ${a.name}${a.count > 1 ? ` \xD7${a.count}` : ""}`;
152
+ segments.push("\n", [label2.padEnd(30), tone], [`${a.totalMs.toFixed(1)}ms`, "plain"], [` ${reason}`, "dim"]);
153
+ if (a.remount) segments.push([" rebuilt", "bad"]);
154
+ if (a.avoidable > 0) segments.push([` ${a.avoidable} avoidable`, "warn"]);
155
+ if (a.source) segments.push(["\n " + a.source, "dim"]);
156
+ }
157
+ const worst = ranked[0]?.worst;
158
+ segments.push(
159
+ "\n",
160
+ [
161
+ worst?.diagnosis.suggestion ? ` \u2192 ${worst.diagnosis.suggestion}` : " \u2192 rrd.printOpportunities() to rank everything by recoverable time",
162
+ "dim"
163
+ ]
164
+ );
165
+ const log = anySlow ? console.warn : console.log;
166
+ log(...styled(segments));
61
167
  }
62
168
  function printVerbose(event) {
63
169
  const { diagnosis, timings } = event;
@@ -80,12 +186,10 @@ function printVerbose(event) {
80
186
  for (const c of event.changedProps) console.log(propLine(c));
81
187
  console.groupEnd();
82
188
  }
83
- if (event.unchangedProps.length > 0) {
84
- console.log(`Props same: ${event.unchangedProps.join(", ")}`);
85
- }
189
+ if (event.unchangedProps.length > 0) console.log(`Props same: ${event.unchangedProps.join(", ")}`);
86
190
  if (event.trackedState.length > 0) {
87
191
  console.log(
88
- `State: ${event.trackedState.map((s) => `${s.name}: ${chunkDZ3BZ654_cjs.formatInspected(s.previous)} \u2192 ${chunkDZ3BZ654_cjs.formatInspected(s.current)}`).join(", ")}`
192
+ `State: ${event.trackedState.map((s) => `${s.name}: ${chunkLCGXVRJR_cjs.formatInspected(s.previous)} \u2192 ${chunkLCGXVRJR_cjs.formatInspected(s.current)}`).join(", ")}`
89
193
  );
90
194
  }
91
195
  if (event.contextChanges.length > 0) {
@@ -102,11 +206,11 @@ function printVerbose(event) {
102
206
  }
103
207
  function propLine(c) {
104
208
  const head = `${c.key} (${c.valueType}) \u2014 ${describeKind(c)}`;
105
- if (c.kind === "added") return `${head}: ${chunkDZ3BZ654_cjs.formatInspected(c.current)}`;
106
- if (c.kind === "removed") return `${head}: was ${chunkDZ3BZ654_cjs.formatInspected(c.previous)}`;
209
+ if (c.kind === "added") return `${head}: ${chunkLCGXVRJR_cjs.formatInspected(c.current)}`;
210
+ if (c.kind === "removed") return `${head}: was ${chunkLCGXVRJR_cjs.formatInspected(c.previous)}`;
107
211
  return `${head}
108
- previous: ${chunkDZ3BZ654_cjs.formatInspected(c.previous)}
109
- current: ${chunkDZ3BZ654_cjs.formatInspected(c.current)}`;
212
+ previous: ${chunkLCGXVRJR_cjs.formatInspected(c.previous)}
213
+ current: ${chunkLCGXVRJR_cjs.formatInspected(c.current)}`;
110
214
  }
111
215
  function describeKind(c) {
112
216
  switch (c.kind) {
@@ -133,6 +237,8 @@ function label(event) {
133
237
  return "context update";
134
238
  case "state":
135
239
  return "state changed";
240
+ case "store":
241
+ return "store update";
136
242
  case "state-or-external":
137
243
  return "state or external store";
138
244
  case "unknown":
@@ -140,194 +246,6 @@ function label(event) {
140
246
  }
141
247
  }
142
248
 
143
- // src/core/interactions.ts
144
- var COMMIT_SLACK_MS = 100;
145
- var FALLBACK_CLOSE_MS = 50;
146
- var InteractionTracker = class {
147
- constructor(capacity = 50) {
148
- this.capacity = capacity;
149
- this.records = [];
150
- this.nextId = 0;
151
- }
152
- /** Returns false when the browser cannot report event timing. */
153
- start() {
154
- if (this.observer) return true;
155
- const PO = globalThis.PerformanceObserver;
156
- const supported = PO?.supportedEntryTypes?.includes("event");
157
- if (!PO || !supported) return false;
158
- try {
159
- const observer = new PO((list) => {
160
- for (const entry of list.getEntries()) {
161
- const timing = entry;
162
- this.record({
163
- name: timing.name,
164
- startTime: timing.startTime,
165
- duration: timing.duration,
166
- target: describeTarget(timing.target)
167
- });
168
- }
169
- });
170
- observer.observe({ type: "event", buffered: true, durationThreshold: 16 });
171
- this.observer = observer;
172
- return true;
173
- } catch {
174
- return false;
175
- }
176
- }
177
- stop() {
178
- this.observer?.disconnect();
179
- this.observer = void 0;
180
- }
181
- /** Is the automatic path available in this browser? */
182
- get automatic() {
183
- return this.observer !== void 0;
184
- }
185
- /**
186
- * Time an interaction by hand.
187
- *
188
- * The automatic path depends on the Event Timing API, which Safari only
189
- * gained in 16.4 and which does not fire for synthetic input at all — so
190
- * anything driven by a test harness records nothing. This measures a specific
191
- * action instead, up to the paint that follows it, and needs no browser
192
- * support beyond `performance.now`.
193
- */
194
- measure(label2, action) {
195
- const startTime = now();
196
- let handlerMs = 0;
197
- let finished = false;
198
- const finish = () => {
199
- if (finished) return;
200
- finished = true;
201
- this.record({ name: label2, startTime, duration: now() - startTime, handlerMs });
202
- };
203
- let result;
204
- try {
205
- result = action();
206
- handlerMs = now() - startTime;
207
- } catch (error) {
208
- handlerMs = now() - startTime;
209
- finish();
210
- throw error;
211
- }
212
- const raf = globalThis.requestAnimationFrame;
213
- if (raf) raf(() => raf(finish));
214
- setTimeout(finish, FALLBACK_CLOSE_MS);
215
- return result;
216
- }
217
- /** Exposed for tests and for `measure`. */
218
- record(timing) {
219
- const record = {
220
- id: `interaction_${++this.nextId}`,
221
- type: timing.name,
222
- target: timing.target,
223
- startTime: timing.startTime,
224
- durationMs: timing.duration,
225
- handlerMs: timing.handlerMs,
226
- renders: [],
227
- renderTimeMs: 0,
228
- avoidableRenderTimeMs: 0
229
- };
230
- this.records.push(record);
231
- if (this.records.length > this.capacity) this.records.shift();
232
- return record;
233
- }
234
- clear() {
235
- this.records.length = 0;
236
- }
237
- /**
238
- * Joins render events to interactions by commit time. A render belongs to an
239
- * interaction when it committed between the event starting and shortly after
240
- * it finished — React commits just after the event handler returns.
241
- */
242
- attribute(events) {
243
- for (const record of this.records) {
244
- const from = record.startTime;
245
- const to = record.startTime + record.durationMs + COMMIT_SLACK_MS;
246
- record.renders = events.filter((e) => e.timings.commitTime >= from && e.timings.commitTime <= to);
247
- record.renderTimeMs = record.renders.reduce((a, e) => a + e.timings.selfDuration, 0);
248
- record.avoidableRenderTimeMs = record.renders.filter((e) => e.diagnosis.potentiallyAvoidable).reduce((a, e) => a + e.timings.selfDuration, 0);
249
- }
250
- return [...this.records].sort((a, b) => b.durationMs - a.durationMs);
251
- }
252
- };
253
- function summarise(record) {
254
- const byComponent = /* @__PURE__ */ new Map();
255
- for (const event of record.renders) {
256
- const entry = byComponent.get(event.component.name) ?? {
257
- renders: 0,
258
- totalMs: 0,
259
- source: event.component.source,
260
- cause: event.diagnosis.reason
261
- };
262
- entry.renders++;
263
- entry.totalMs += event.timings.selfDuration;
264
- byComponent.set(event.component.name, entry);
265
- }
266
- const contributors = [...byComponent.entries()].map(([component, v]) => ({ component, source: v.source, renders: v.renders, totalMs: v.totalMs, cause: v.cause })).sort((a, b) => b.totalMs - a.totalMs);
267
- const top = contributors[0];
268
- const accounted = (record.handlerMs ?? record.durationMs) + record.renderTimeMs;
269
- const idleWindow = record.handlerMs !== void 0 && record.durationMs > accounted * 3 && record.durationMs - accounted > 100;
270
- const effectiveMs = idleWindow ? accounted : record.durationMs;
271
- const share = effectiveMs > 0 ? record.renderTimeMs / effectiveMs : 0;
272
- let headline;
273
- let nextStep;
274
- let confidence = "medium";
275
- if (idleWindow) {
276
- headline = `${record.type}: ${fmt(record.handlerMs ?? 0)} in the handler and ${fmt(record.renderTimeMs)} rendering. The measured window was ${fmt(record.durationMs)}, but most of that was the page waiting for a frame \u2014 ignore it.`;
277
- nextStep = record.renderTimeMs > (record.handlerMs ?? 0) ? `Rendering dominates the real work${top ? `; start with ${top.component}` : ""}.` : "The handler itself costs more than rendering. Profile the handler, not React.";
278
- return { interaction: record, contributors, headline, nextStep, confidence: "medium" };
279
- }
280
- if (record.renders.length === 0) {
281
- headline = `${record.type} took ${fmt(record.durationMs)}, and no instrumented component rendered inside it.`;
282
- nextStep = "The cost is somewhere other than React rendering \u2014 an event handler, a layout, or an uninstrumented component. Instrument more of the tree to narrow it down.";
283
- confidence = "low";
284
- } else if (share >= 0.4 && record.avoidableRenderTimeMs > 0) {
285
- headline = `${record.type} took ${fmt(record.durationMs)}; ${fmt(record.renderTimeMs)} of it was rendering, and ${fmt(record.avoidableRenderTimeMs)} of that had no input change to explain it.`;
286
- nextStep = top ? `Start with ${top.component}${top.source ? ` (${top.source})` : ""} \u2014 ${fmt(top.totalMs)} across ${top.renders} render${top.renders === 1 ? "" : "s"}.` : "Look at the top contributor below.";
287
- confidence = "high";
288
- } else if (share >= 0.4) {
289
- headline = `${record.type} took ${fmt(record.durationMs)}; ${fmt(record.renderTimeMs)} of it was rendering, all of it explained by real input changes.`;
290
- nextStep = "This is genuine work. Make the renders cheaper rather than fewer \u2014 or do less of it per interaction.";
291
- confidence = "high";
292
- } else {
293
- headline = `${record.type} took ${fmt(record.durationMs)}, but only ${fmt(record.renderTimeMs)} was React rendering.`;
294
- nextStep = "Most of the cost is outside rendering \u2014 event handlers, layout or paint. A browser profile will show more than this tool can.";
295
- confidence = "medium";
296
- }
297
- return { interaction: record, contributors, headline, nextStep, confidence };
298
- }
299
- function formatInteraction(summary) {
300
- const { interaction: i } = summary;
301
- const lines = [
302
- `${i.type}${i.target ? ` on ${i.target}` : ""} ${fmt(i.durationMs)}`,
303
- "",
304
- summary.headline
305
- ];
306
- if (summary.contributors.length > 0) {
307
- lines.push("", "Rendering inside this interaction");
308
- for (const c of summary.contributors.slice(0, 8)) {
309
- lines.push(
310
- ` ${c.component.padEnd(22)} ${String(c.renders).padStart(4)} render(s) ${fmt(c.totalMs).padStart(8)} ${c.cause}`
311
- );
312
- }
313
- }
314
- lines.push("", `Next step`, ` ${summary.nextStep}`, "", `Confidence: ${summary.confidence}`);
315
- return lines.join("\n");
316
- }
317
- function describeTarget(target) {
318
- if (!target || typeof target !== "object") return void 0;
319
- const el = target;
320
- if (!el.tagName) return void 0;
321
- const tag = el.tagName.toLowerCase();
322
- if (el.id) return `${tag}#${el.id}`;
323
- const className = typeof el.className === "string" ? el.className.trim().split(/\s+/)[0] : void 0;
324
- if (className) return `${tag}.${className}`;
325
- const text = el.textContent?.trim().slice(0, 20);
326
- return text ? `${tag} "${text}"` : tag;
327
- }
328
- var fmt = (ms) => `${ms.toFixed(1)}ms`;
329
- var now = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
330
-
331
249
  // src/core/opportunities.ts
332
250
  var DEFAULT_MIN_SAVING_MS = 1;
333
251
  function rankOpportunities({ events, lifecycles, minSavingMs = DEFAULT_MIN_SAVING_MS }) {
@@ -335,7 +253,7 @@ function rankOpportunities({ events, lifecycles, minSavingMs = DEFAULT_MIN_SAVIN
335
253
  const out = [];
336
254
  for (const name of names) {
337
255
  const lifecycle = lifecycles.get(name);
338
- const explanation = chunkJD4IJ4MN_cjs.explainEvents(name, events, lifecycle);
256
+ const explanation = chunkRGT44QQI_cjs.explainEvents(name, events, lifecycle);
339
257
  if (!explanation) continue;
340
258
  const mounts = events.filter((e) => e.component.name === name && e.phase === "mount");
341
259
  const averageMountCost = mounts.length ? mounts.reduce((a, e) => a + e.timings.selfDuration, 0) / mounts.length : 0;
@@ -381,7 +299,7 @@ function formatOpportunities(opportunities) {
381
299
  var AncestryContext = React.createContext(void 0);
382
300
  AncestryContext.displayName = "RenderDetectiveAncestry";
383
301
  function useInstrumentedNode(name, props, source) {
384
- const detective = chunkDZ3BZ654_cjs.getDetective();
302
+ const detective = chunkLCGXVRJR_cjs.getDetective();
385
303
  const parent = React.useContext(AncestryContext);
386
304
  const [node] = React.useState(
387
305
  () => detective.enabled ? detective.createNode(name, parent, source) : void 0
@@ -431,7 +349,7 @@ var IS_WRAPPER = /* @__PURE__ */ Symbol.for("react-render-detective.wrapper");
431
349
  function withRenderDetective(Component, options = {}) {
432
350
  if (Component[IS_WRAPPER]) return Component;
433
351
  const name = options.name ?? componentName(Component);
434
- chunkDZ3BZ654_cjs.getDetective().noteDefinition(name, options.source, options.declaredInRender === true);
352
+ chunkLCGXVRJR_cjs.getDetective().noteDefinition(name, options.source, options.declaredInRender === true);
435
353
  function RenderDetected(props) {
436
354
  const { node, onRender } = useInstrumentedNode(name, props, options.source);
437
355
  return renderInstrumented(node, onRender, /* @__PURE__ */ jsxRuntime.jsx(Component, { ...props }));
@@ -446,8 +364,40 @@ function RenderDetective({ name, children }) {
446
364
  const { node, onRender } = useInstrumentedNode(name, { children });
447
365
  return renderInstrumented(node, onRender, children);
448
366
  }
367
+ var useOwnerId = typeof React__namespace.useId === "function" ? React__namespace.useId : () => "";
368
+ function trackSelector(value, options = {}) {
369
+ const detective = chunkLCGXVRJR_cjs.getDetective();
370
+ const node = React.useContext(AncestryContext);
371
+ const ownerId = useOwnerId();
372
+ const previous = React.useRef(void 0);
373
+ if (!detective.enabled) return value;
374
+ if (node && node.stateOwner === void 0) node.stateOwner = ownerId;
375
+ const owns = !node || node.stateOwner === ownerId;
376
+ const prior = previous.current;
377
+ previous.current = { value };
378
+ if (!prior || !node || !owns) return value;
379
+ if (Object.is(prior.value, value)) return value;
380
+ try {
381
+ const equal = chunkLCGXVRJR_cjs.shallowEqual(prior.value, value, detective.config);
382
+ detective.recordSelectorChange(node, {
383
+ name: options.name ?? options.source ?? "selector",
384
+ source: options.source,
385
+ referenceOnly: equal === true,
386
+ previous: chunkLCGXVRJR_cjs.inspect(prior.value, detective.config.inspection),
387
+ current: chunkLCGXVRJR_cjs.inspect(value, detective.config.inspection)
388
+ });
389
+ } catch {
390
+ }
391
+ return value;
392
+ }
393
+ function createTrackedSelectorHook(hook, options = {}) {
394
+ return function useTrackedSelector(...args) {
395
+ const value = hook(...args);
396
+ return trackSelector(value, options);
397
+ };
398
+ }
449
399
  function useRenderDiagnostics(name, props) {
450
- const detective = chunkDZ3BZ654_cjs.getDetective();
400
+ const detective = chunkLCGXVRJR_cjs.getDetective();
451
401
  const parent = React.useContext(AncestryContext);
452
402
  const [node] = React.useState(() => {
453
403
  if (!detective.enabled) return void 0;
@@ -480,11 +430,11 @@ function useRenderDiagnostics(name, props) {
480
430
  }
481
431
  return last.current;
482
432
  }
483
- var useOwnerId = typeof React__namespace.useId === "function" ? React__namespace.useId : () => "";
433
+ var useOwnerId2 = typeof React__namespace.useId === "function" ? React__namespace.useId : () => "";
484
434
  function useTrackedState(name, initial) {
485
- const detective = chunkDZ3BZ654_cjs.getDetective();
435
+ const detective = chunkLCGXVRJR_cjs.getDetective();
486
436
  const node = React.useContext(AncestryContext);
487
- const ownerId = useOwnerId();
437
+ const ownerId = useOwnerId2();
488
438
  const [state, setState] = React.useState(initial);
489
439
  const previous = React.useRef(state);
490
440
  if (node && node.stateOwner === void 0) node.stateOwner = ownerId;
@@ -492,8 +442,8 @@ function useTrackedState(name, initial) {
492
442
  if (node && owns && !Object.is(previous.current, state)) {
493
443
  detective.recordStateChange(node, {
494
444
  name,
495
- previous: chunkDZ3BZ654_cjs.inspect(previous.current, detective.config.inspection),
496
- current: chunkDZ3BZ654_cjs.inspect(state, detective.config.inspection)
445
+ previous: chunkLCGXVRJR_cjs.inspect(previous.current, detective.config.inspection),
446
+ current: chunkLCGXVRJR_cjs.inspect(state, detective.config.inspection)
497
447
  });
498
448
  previous.current = state;
499
449
  } else if (!owns) {
@@ -502,7 +452,7 @@ function useTrackedState(name, initial) {
502
452
  return [state, setState];
503
453
  }
504
454
  function useTrackedEffect(name, effect, deps) {
505
- const detective = chunkDZ3BZ654_cjs.getDetective();
455
+ const detective = chunkLCGXVRJR_cjs.getDetective();
506
456
  const previous = React.useRef(void 0);
507
457
  const changed = React.useRef([]);
508
458
  if (detective.enabled) {
@@ -518,7 +468,7 @@ function useTrackedEffect(name, effect, deps) {
518
468
  }, deps);
519
469
  }
520
470
  function useTrackedContextValue(contextName, value) {
521
- const detective = chunkDZ3BZ654_cjs.getDetective();
471
+ const detective = chunkLCGXVRJR_cjs.getDetective();
522
472
  const previous = React.useRef(void 0);
523
473
  const first = React.useRef(true);
524
474
  if (detective.enabled) {
@@ -526,8 +476,8 @@ function useTrackedContextValue(contextName, value) {
526
476
  first.current = false;
527
477
  } else if (!Object.is(previous.current, value)) {
528
478
  const prev = previous.current;
529
- const equal = chunkDZ3BZ654_cjs.shallowEqual(prev, value, detective.config);
530
- const changedKeys = isRecord(prev) && isRecord(value) ? chunkDZ3BZ654_cjs.diffProps(prev, value, detective.config).changed.map((c) => c.key) : [];
479
+ const equal = chunkLCGXVRJR_cjs.shallowEqual(prev, value, detective.config);
480
+ const changedKeys = isRecord(prev) && isRecord(value) ? chunkLCGXVRJR_cjs.diffProps(prev, value, detective.config).changed.map((c) => c.key) : [];
531
481
  detective.recordContextChange({
532
482
  contextName,
533
483
  changedKeys,
@@ -545,21 +495,12 @@ function isRecord(v) {
545
495
 
546
496
  // src/index.ts
547
497
  var REPORTER = /* @__PURE__ */ Symbol.for("react-render-detective.reporter");
548
- var INTERACTIONS = /* @__PURE__ */ Symbol.for("react-render-detective.interactions");
549
- function tracker() {
550
- const g = globalThis;
551
- if (!g[INTERACTIONS]) g[INTERACTIONS] = new InteractionTracker();
552
- return g[INTERACTIONS];
553
- }
554
498
  function init(options = {}) {
555
- const detective = chunkDZ3BZ654_cjs.getDetective();
499
+ const detective = chunkLCGXVRJR_cjs.getDetective();
556
500
  detective.init(options);
557
501
  const g = globalThis;
558
502
  g[REPORTER]?.();
559
503
  g[REPORTER] = void 0;
560
- if (detective.enabled) {
561
- tracker().start();
562
- }
563
504
  if (detective.enabled && detective.config.mode !== "silent") {
564
505
  g[REPORTER] = attachConsoleReporter(detective);
565
506
  } else if (!detective.enabled) {
@@ -569,77 +510,49 @@ function init(options = {}) {
569
510
  }
570
511
  }
571
512
  function configure(options) {
572
- chunkDZ3BZ654_cjs.getDetective().configure(options);
513
+ chunkLCGXVRJR_cjs.getDetective().configure(options);
573
514
  }
574
515
  function getConfig() {
575
- return chunkDZ3BZ654_cjs.getDetective().config;
516
+ return chunkLCGXVRJR_cjs.getDetective().config;
576
517
  }
577
518
  function isEnabled() {
578
- return chunkDZ3BZ654_cjs.getDetective().enabled;
519
+ return chunkLCGXVRJR_cjs.getDetective().enabled;
579
520
  }
580
521
  function getEvents() {
581
- return chunkDZ3BZ654_cjs.getDetective().getEvents();
522
+ return chunkLCGXVRJR_cjs.getDetective().getEvents();
582
523
  }
583
524
  function getStats() {
584
- return chunkDZ3BZ654_cjs.getDetective().getStats();
525
+ return chunkLCGXVRJR_cjs.getDetective().getStats();
585
526
  }
586
527
  function getComponentStats(name) {
587
- return chunkDZ3BZ654_cjs.getDetective().getComponentStats(name);
528
+ return chunkLCGXVRJR_cjs.getDetective().getComponentStats(name);
588
529
  }
589
530
  function subscribe(listener) {
590
- return chunkDZ3BZ654_cjs.getDetective().subscribe(listener);
531
+ return chunkLCGXVRJR_cjs.getDetective().subscribe(listener);
591
532
  }
592
533
  function clear() {
593
- chunkDZ3BZ654_cjs.getDetective().clear();
594
- tracker().clear();
534
+ chunkLCGXVRJR_cjs.getDetective().clear();
595
535
  }
596
536
  function reset() {
597
537
  const g = globalThis;
598
538
  g[REPORTER]?.();
599
539
  g[REPORTER] = void 0;
600
- g[INTERACTIONS]?.stop();
601
- g[INTERACTIONS] = void 0;
602
- chunkDZ3BZ654_cjs.getDetective().reset();
540
+ chunkLCGXVRJR_cjs.getDetective().reset();
603
541
  }
604
542
  function explain(componentName2) {
605
543
  const explanation = explainStructured(componentName2);
606
- return explanation ? chunkJD4IJ4MN_cjs.formatExplanation(explanation) : void 0;
544
+ return explanation ? chunkRGT44QQI_cjs.formatExplanation(explanation) : void 0;
607
545
  }
608
546
  function explainStructured(componentName2) {
609
- return chunkJD4IJ4MN_cjs.explainEvents(componentName2, getEvents(), chunkDZ3BZ654_cjs.getDetective().lifecycleOf(componentName2));
547
+ return chunkRGT44QQI_cjs.explainEvents(componentName2, getEvents(), chunkLCGXVRJR_cjs.getDetective().lifecycleOf(componentName2));
610
548
  }
611
549
  function getRenderProfile(scenario) {
612
550
  const remounts = {};
613
- for (const stats of chunkDZ3BZ654_cjs.getDetective().getComponentStats()) remounts[stats.name] = stats.remountCount;
551
+ for (const stats of chunkLCGXVRJR_cjs.getDetective().getComponentStats()) remounts[stats.name] = stats.remountCount;
614
552
  return chunkH5RG2EPP_cjs.profileFromEvents(scenario, getEvents(), remounts);
615
553
  }
616
- function getInteractions() {
617
- return tracker().attribute(getEvents());
618
- }
619
- function explainInteractionStructured(id) {
620
- const records = getInteractions();
621
- const record = id ? records.find((r) => r.id === id) : records[0];
622
- return record ? summarise(record) : void 0;
623
- }
624
- function explainInteraction(id) {
625
- const summary = explainInteractionStructured(id);
626
- return summary ? formatInteraction(summary) : void 0;
627
- }
628
- function printInteractions(limit = 5) {
629
- const records = getInteractions().slice(0, limit);
630
- if (records.length === 0) {
631
- console.log(
632
- tracker().automatic ? "No interactions recorded yet. Event timing is working \u2014 nothing has taken longer than 16ms.\nSynthetic clicks from a test harness never produce these entries; use measureInteraction() there." : "This browser does not report event timing (Safari before 16.4, jsdom).\nUse measureInteraction(label, fn) to time interactions by hand."
633
- );
634
- return;
635
- }
636
- console.log(records.map((r) => formatInteraction(summarise(r))).join("\n\n"));
637
- }
638
- function measureInteraction(label2, action) {
639
- return tracker().measure(label2, action);
640
- }
641
554
  function getOpportunities(limit = 10) {
642
- const detective = chunkDZ3BZ654_cjs.getDetective();
555
+ const detective = chunkLCGXVRJR_cjs.getDetective();
643
556
  const lifecycles = /* @__PURE__ */ new Map();
644
557
  for (const stats of detective.getComponentStats()) {
645
558
  lifecycles.set(stats.name, { remounts: stats.remountCount });
@@ -696,51 +609,40 @@ var ReactRenderDetective = {
696
609
  explainStructured,
697
610
  getOpportunities,
698
611
  printOpportunities,
699
- getInteractions,
700
- explainInteraction,
701
- explainInteractionStructured,
702
- printInteractions,
703
- measureInteraction,
704
612
  getRenderProfile,
705
613
  printStats
706
614
  };
707
615
 
708
616
  Object.defineProperty(exports, "explainEvents", {
709
617
  enumerable: true,
710
- get: function () { return chunkJD4IJ4MN_cjs.explainEvents; }
618
+ get: function () { return chunkRGT44QQI_cjs.explainEvents; }
711
619
  });
712
620
  Object.defineProperty(exports, "formatExplanation", {
713
621
  enumerable: true,
714
- get: function () { return chunkJD4IJ4MN_cjs.formatExplanation; }
622
+ get: function () { return chunkRGT44QQI_cjs.formatExplanation; }
715
623
  });
716
- exports.InteractionTracker = InteractionTracker;
717
624
  exports.ReactRenderDetective = ReactRenderDetective;
718
625
  exports.RenderDetective = RenderDetective;
719
626
  exports.clear = clear;
720
627
  exports.configure = configure;
628
+ exports.createTrackedSelectorHook = createTrackedSelectorHook;
721
629
  exports.explain = explain;
722
- exports.explainInteraction = explainInteraction;
723
- exports.explainInteractionStructured = explainInteractionStructured;
724
630
  exports.explainStructured = explainStructured;
725
- exports.formatInteraction = formatInteraction;
726
631
  exports.formatOpportunities = formatOpportunities;
727
632
  exports.getComponentStats = getComponentStats;
728
633
  exports.getConfig = getConfig;
729
634
  exports.getEvents = getEvents;
730
- exports.getInteractions = getInteractions;
731
635
  exports.getOpportunities = getOpportunities;
732
636
  exports.getRenderProfile = getRenderProfile;
733
637
  exports.getStats = getStats;
734
638
  exports.init = init;
735
639
  exports.isEnabled = isEnabled;
736
- exports.measureInteraction = measureInteraction;
737
- exports.printInteractions = printInteractions;
738
640
  exports.printOpportunities = printOpportunities;
739
641
  exports.printStats = printStats;
740
642
  exports.rankOpportunities = rankOpportunities;
741
643
  exports.reset = reset;
742
644
  exports.subscribe = subscribe;
743
- exports.summariseInteraction = summarise;
645
+ exports.trackSelector = trackSelector;
744
646
  exports.useRenderDiagnostics = useRenderDiagnostics;
745
647
  exports.useTrackedContextValue = useTrackedContextValue;
746
648
  exports.useTrackedEffect = useTrackedEffect;