@treelocator/runtime 0.4.1 → 0.4.5

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 (67) hide show
  1. package/dist/_generated_styles.js +115 -15
  2. package/dist/adapters/getElementInfo.js +3 -17
  3. package/dist/adapters/getParentsPath.js +3 -33
  4. package/dist/adapters/getTree.js +3 -33
  5. package/dist/adapters/jsx/getJSXComponentBoundingBox.js +0 -1
  6. package/dist/adapters/jsx/jsxAdapter.js +0 -3
  7. package/dist/adapters/jsx/runtimeStore.js +0 -12
  8. package/dist/adapters/react/reactAdapter.js +0 -8
  9. package/dist/adapters/resolveAdapter.d.ts +8 -0
  10. package/dist/adapters/resolveAdapter.js +28 -0
  11. package/dist/adapters/vue/vueAdapter.js +0 -14
  12. package/dist/browserApi.d.ts +1 -1
  13. package/dist/browserApi.js +8 -10
  14. package/dist/components/MaybeOutline.d.ts +1 -0
  15. package/dist/components/MaybeOutline.js +38 -29
  16. package/dist/components/Outline.d.ts +1 -0
  17. package/dist/components/Outline.js +20 -16
  18. package/dist/components/Runtime.js +30 -18
  19. package/dist/dejitter/recorder.js +0 -1
  20. package/dist/functions/formatAncestryChain.d.ts +6 -0
  21. package/dist/functions/formatAncestryChain.js +11 -0
  22. package/dist/functions/formatAncestryChain.test.js +75 -1
  23. package/dist/functions/getUsableName.js +0 -21
  24. package/dist/output.css +10 -40
  25. package/dist/types/types.d.ts +1 -32
  26. package/package.json +1 -1
  27. package/src/_generated_styles.ts +115 -15
  28. package/src/adapters/getElementInfo.tsx +3 -23
  29. package/src/adapters/getParentsPath.tsx +3 -42
  30. package/src/adapters/getTree.tsx +3 -42
  31. package/src/adapters/jsx/getJSXComponentBoundingBox.ts +0 -1
  32. package/src/adapters/jsx/jsxAdapter.ts +0 -2
  33. package/src/adapters/jsx/runtimeStore.ts +0 -11
  34. package/src/adapters/react/reactAdapter.ts +0 -7
  35. package/src/adapters/resolveAdapter.ts +38 -0
  36. package/src/adapters/vue/vueAdapter.ts +0 -14
  37. package/src/browserApi.ts +9 -12
  38. package/src/components/MaybeOutline.tsx +4 -2
  39. package/src/components/Outline.tsx +2 -1
  40. package/src/components/Runtime.tsx +27 -18
  41. package/src/dejitter/recorder.ts +43 -44
  42. package/src/functions/formatAncestryChain.test.ts +47 -0
  43. package/src/functions/formatAncestryChain.ts +11 -0
  44. package/src/functions/getUsableName.ts +0 -21
  45. package/src/types/types.ts +1 -32
  46. package/src/adapters/react/fiberToSimple.tsx +0 -72
  47. package/src/adapters/react/gatherFiberRoots.tsx +0 -36
  48. package/src/adapters/react/makeFiberId.tsx +0 -19
  49. package/src/adapters/react/searchDevtoolsRenderersForClosestTarget.ts +0 -15
  50. package/src/components/Button.tsx +0 -14
  51. package/src/components/ComponentOutline.tsx +0 -98
  52. package/src/components/SimpleNodeOutline.tsx +0 -27
  53. package/src/components/Tooltip.tsx +0 -28
  54. package/src/functions/cropPath.test.ts +0 -18
  55. package/src/functions/cropPath.ts +0 -12
  56. package/src/functions/evalTemplate.test.ts +0 -12
  57. package/src/functions/evalTemplate.ts +0 -8
  58. package/src/functions/findNames.ts +0 -20
  59. package/src/functions/getBoundingRect.tsx +0 -11
  60. package/src/functions/getComposedBoundingBox.tsx +0 -25
  61. package/src/functions/getIdsOnPathToRoot.tsx +0 -21
  62. package/src/functions/getMultipleElementsBoundingBox.tsx +0 -25
  63. package/src/functions/getPathToParent.tsx +0 -17
  64. package/src/functions/getUsableFileName.test.tsx +0 -24
  65. package/src/functions/getUsableFileName.tsx +0 -19
  66. package/src/functions/transformPath.test.ts +0 -28
  67. package/src/functions/transformPath.ts +0 -7
@@ -7,7 +7,7 @@ import { Targets as SetupTargets } from "../types/types";
7
7
  import { MaybeOutline } from "./MaybeOutline";
8
8
  import { isLocatorsOwnElement } from "../functions/isLocatorsOwnElement";
9
9
  import { Toast } from "./Toast";
10
- import { collectAncestry, formatAncestryChain } from "../functions/formatAncestryChain";
10
+ import { collectAncestry, formatAncestryChain, truncateAtFirstFile } from "../functions/formatAncestryChain";
11
11
  import { enrichAncestryWithSourceMaps } from "../functions/enrichAncestrySourceMaps";
12
12
  import { createTreeNode } from "../adapters/createTreeNode";
13
13
  import treeIconUrl from "../_generated_tree_icon";
@@ -15,6 +15,15 @@ import { createDejitterRecorder, DejitterAPI, DejitterFinding, DejitterSummary }
15
15
  import { RecordingOutline } from "./RecordingOutline";
16
16
  import { RecordingResults, InteractionEvent } from "./RecordingResults";
17
17
 
18
+ const DEJITTER_CONFIG: Partial<import("../dejitter/recorder").DejitterConfig> = {
19
+ selector: '[data-treelocator-recording]',
20
+ props: ['opacity', 'transform', 'boundingRect', 'width', 'height'],
21
+ sampleRate: 15,
22
+ maxDuration: 30000,
23
+ idleTimeout: 0,
24
+ mutations: true,
25
+ };
26
+
18
27
  type RuntimeProps = {
19
28
  adapterId?: AdapterId;
20
29
  targets: Targets;
@@ -22,6 +31,7 @@ type RuntimeProps = {
22
31
 
23
32
  function Runtime(props: RuntimeProps) {
24
33
  const [holdingModKey, setHoldingModKey] = createSignal<boolean>(false);
34
+ const [holdingShift, setHoldingShift] = createSignal<boolean>(false);
25
35
  const [currentElement, setCurrentElement] = createSignal<HTMLElement | null>(
26
36
  null
27
37
  );
@@ -98,15 +108,18 @@ function Runtime(props: RuntimeProps) {
98
108
 
99
109
  function keyUpListener(e: KeyboardEvent) {
100
110
  setHoldingModKey(isCombinationModifiersPressed(e));
111
+ setHoldingShift(e.shiftKey);
101
112
  }
102
113
 
103
114
  function keyDownListener(e: KeyboardEvent) {
104
115
  setHoldingModKey(isCombinationModifiersPressed(e, true));
116
+ setHoldingShift(e.shiftKey);
105
117
  }
106
118
 
107
119
  function mouseMoveListener(e: MouseEvent) {
108
120
  // Update modifier state from mouse events - more reliable than keydown/keyup
109
121
  setHoldingModKey(e.altKey);
122
+ setHoldingShift(e.shiftKey);
110
123
  }
111
124
 
112
125
  function findElementAtPoint(e: MouseEvent): HTMLElement | null {
@@ -155,14 +168,7 @@ function Runtime(props: RuntimeProps) {
155
168
  setRecordedElement(element);
156
169
 
157
170
  dejitterInstance = createDejitterRecorder();
158
- dejitterInstance.configure({
159
- selector: '[data-treelocator-recording]',
160
- props: ['opacity', 'transform', 'boundingRect', 'width', 'height'],
161
- sampleRate: 15,
162
- maxDuration: 30000,
163
- idleTimeout: 0,
164
- mutations: true,
165
- });
171
+ dejitterInstance.configure(DEJITTER_CONFIG);
166
172
  dejitterInstance.start();
167
173
  startInteractionTracker();
168
174
  setRecordingState('recording');
@@ -288,14 +294,7 @@ function Runtime(props: RuntimeProps) {
288
294
  setRecordedElement(element);
289
295
 
290
296
  dejitterInstance = createDejitterRecorder();
291
- dejitterInstance.configure({
292
- selector: '[data-treelocator-recording]',
293
- props: ['opacity', 'transform', 'boundingRect', 'width', 'height'],
294
- sampleRate: 15,
295
- maxDuration: 30000,
296
- idleTimeout: 0,
297
- mutations: true,
298
- });
297
+ dejitterInstance.configure(DEJITTER_CONFIG);
299
298
  dejitterInstance.start();
300
299
  setRecordingState('recording');
301
300
  setReplaying(true);
@@ -382,6 +381,7 @@ function Runtime(props: RuntimeProps) {
382
381
  setRecordedElement(null);
383
382
  setViewingPrevious(false);
384
383
  setRecordingState('idle');
384
+ try { localStorage.removeItem(STORAGE_KEY); } catch {}
385
385
  }
386
386
 
387
387
  function hasPreviousRecording(): boolean {
@@ -443,6 +443,7 @@ function Runtime(props: RuntimeProps) {
443
443
 
444
444
  function mouseOverListener(e: MouseEvent) {
445
445
  setHoldingModKey(e.altKey);
446
+ setHoldingShift(e.shiftKey);
446
447
 
447
448
  // Don't update hovered element while recording -- highlight is sticky
448
449
  if (recordingState() === 'recording') return;
@@ -501,7 +502,12 @@ function Runtime(props: RuntimeProps) {
501
502
  // Copy ancestry to clipboard on alt+click
502
503
  const treeNode = createTreeNode(element as HTMLElement, props.adapterId);
503
504
  if (treeNode) {
504
- const ancestry = collectAncestry(treeNode);
505
+ let ancestry = collectAncestry(treeNode);
506
+
507
+ // Alt+Shift: keep from bottom up to the first element with a file, discard above
508
+ if (e.shiftKey) {
509
+ ancestry = truncateAtFirstFile(ancestry);
510
+ }
505
511
 
506
512
  // Write immediately with component names (preserves user gesture for clipboard API)
507
513
  const formatted = formatAncestryChain(ancestry);
@@ -564,6 +570,8 @@ function Runtime(props: RuntimeProps) {
564
570
  }
565
571
 
566
572
  onCleanup(() => {
573
+ stopReplay();
574
+ stopInteractionTracker();
567
575
  for (const root of roots) {
568
576
  root.removeEventListener("keyup", keyUpListener as EventListener);
569
577
  root.removeEventListener("keydown", keyDownListener as EventListener);
@@ -596,6 +604,7 @@ function Runtime(props: RuntimeProps) {
596
604
  currentElement={currentElement()!}
597
605
  adapterId={props.adapterId}
598
606
  targets={props.targets}
607
+ dashed={holdingShift()}
599
608
  />
600
609
  ) : null}
601
610
  {recordingState() === 'recording' && recordedElement() ? (
@@ -1,4 +1,3 @@
1
- // @ts-nocheck
2
1
  /**
3
2
  * Dejitter — Animation frame recorder & jank detector (vendored)
4
3
  *
@@ -218,13 +217,13 @@ export function createDejitterRecorder(): DejitterAPI {
218
217
  function downsample(): any[] {
219
218
  if (rawFrames.length === 0) return [];
220
219
 
221
- const duration = rawFrames[rawFrames.length - 1].t;
220
+ const duration = rawFrames[rawFrames.length - 1]!.t;
222
221
  const targetFrames = Math.max(1, Math.round((duration / 1000) * config.sampleRate));
223
222
 
224
223
  const changeIndex = new Map<string, Array<{ frameIdx: number; t: number; value: any }>>();
225
224
 
226
225
  for (let fi = 0; fi < rawFrames.length; fi++) {
227
- const frame = rawFrames[fi];
226
+ const frame = rawFrames[fi]!;
228
227
  for (const change of frame.changes) {
229
228
  const { id, ...props } = change;
230
229
  for (const [prop, value] of Object.entries(props)) {
@@ -251,7 +250,7 @@ export function createDejitterRecorder(): DejitterAPI {
251
250
  } else {
252
251
  for (let i = 0; i < targetFrames; i++) {
253
252
  const srcIdx = Math.round((i / (targetFrames - 1)) * (changes.length - 1));
254
- const c = changes[srcIdx];
253
+ const c = changes[srcIdx]!;
255
254
  outputEvents.push({ t: c.t, id, prop, value: c.value });
256
255
  }
257
256
  }
@@ -296,7 +295,7 @@ export function createDejitterRecorder(): DejitterAPI {
296
295
 
297
296
  function buildPropStats(): { targetFrames: number; props: any[] } {
298
297
  const stats: Record<string, any> = {};
299
- const duration = rawFrames.length ? rawFrames[rawFrames.length - 1].t : 0;
298
+ const duration = rawFrames.length ? rawFrames[rawFrames.length - 1]!.t : 0;
300
299
  const targetFrames = Math.max(1, Math.round((duration / 1000) * config.sampleRate));
301
300
 
302
301
  for (const f of rawFrames) {
@@ -344,7 +343,7 @@ export function createDejitterRecorder(): DejitterAPI {
344
343
  if (value === 'none' || value === '' || value === 'auto') return 0;
345
344
  const matrixMatch = String(value).match(/^matrix\(([^)]+)\)$/);
346
345
  if (matrixMatch) {
347
- const parts = matrixMatch[1].split(',').map(Number);
346
+ const parts = matrixMatch[1]!.split(',').map(Number);
348
347
  const tx = parts[4] || 0;
349
348
  const ty = parts[5] || 0;
350
349
  return Math.abs(tx) > Math.abs(ty) ? tx : ty;
@@ -356,8 +355,8 @@ export function createDejitterRecorder(): DejitterAPI {
356
355
  function detectBounce(timeline: Array<{ t: number; value: any }>): any | null {
357
356
  if (timeline.length < 3) return null;
358
357
 
359
- const first = timeline[0].value;
360
- const last = timeline[timeline.length - 1].value;
358
+ const first = timeline[0]!.value;
359
+ const last = timeline[timeline.length - 1]!.value;
361
360
  if (first !== last) return null;
362
361
 
363
362
  const firstNum = extractNumeric(first);
@@ -384,9 +383,9 @@ export function createDejitterRecorder(): DejitterAPI {
384
383
  peak: peakValue,
385
384
  peakDeviation: Math.round(peakDeviation * 10) / 10,
386
385
  peakT,
387
- startT: timeline[0].t,
388
- endT: timeline[timeline.length - 1].t,
389
- duration: timeline[timeline.length - 1].t - timeline[0].t,
386
+ startT: timeline[0]!.t,
387
+ endT: timeline[timeline.length - 1]!.t,
388
+ duration: timeline[timeline.length - 1]!.t - timeline[0]!.t,
390
389
  };
391
390
  }
392
391
 
@@ -394,7 +393,7 @@ export function createDejitterRecorder(): DejitterAPI {
394
393
  const byElem: Record<string, any[]> = {};
395
394
  for (const p of propStats.props) {
396
395
  if (!byElem[p.elem]) byElem[p.elem] = [];
397
- byElem[p.elem].push(p);
396
+ byElem[p.elem]!.push(p);
398
397
  }
399
398
 
400
399
  const outliers: any[] = [];
@@ -427,8 +426,8 @@ export function createDejitterRecorder(): DejitterAPI {
427
426
  function countReversals(numeric: Array<{ t: number; val: number }>): number {
428
427
  let reversals = 0;
429
428
  for (let i = 2; i < numeric.length; i++) {
430
- const d1 = numeric[i - 1].val - numeric[i - 2].val;
431
- const d2 = numeric[i].val - numeric[i - 1].val;
429
+ const d1 = numeric[i - 1]!.val - numeric[i - 2]!.val;
430
+ const d2 = numeric[i]!.val - numeric[i - 1]!.val;
432
431
  if (Math.abs(d1) > config.thresholds.shiver.minDelta && Math.abs(d2) > config.thresholds.shiver.minDelta && d1 * d2 < 0) {
433
432
  reversals++;
434
433
  }
@@ -511,7 +510,7 @@ export function createDejitterRecorder(): DejitterAPI {
511
510
 
512
511
  const vals = numeric.map((n) => n.val);
513
512
  const amplitude = Math.round((Math.max(...vals) - Math.min(...vals)) * 10) / 10;
514
- const hz = Math.round((reversals / ((numeric[numeric.length - 1].t - numeric[0].t) / 1000)) * 10) / 10;
513
+ const hz = Math.round((reversals / ((numeric[numeric.length - 1]!.t - numeric[0]!.t) / 1000)) * 10) / 10;
515
514
 
516
515
  findings.push(makeFinding(
517
516
  'shiver',
@@ -531,7 +530,7 @@ export function createDejitterRecorder(): DejitterAPI {
531
530
  range: [Math.min(...vals), Math.max(...vals)],
532
531
  uniqueValues: uniqueVals.length,
533
532
  isTwoValueFight,
534
- durationMs: Math.round(numeric[numeric.length - 1].t - numeric[0].t),
533
+ durationMs: Math.round(numeric[numeric.length - 1]!.t - numeric[0]!.t),
535
534
  },
536
535
  }
537
536
  ));
@@ -552,21 +551,21 @@ export function createDejitterRecorder(): DejitterAPI {
552
551
 
553
552
  const deltas: Array<{ t: number; delta: number; from: any; to: any }> = [];
554
553
  for (let i = 1; i < timeline.length; i++) {
555
- const prev = extractNumeric(timeline[i - 1].value);
556
- const curr = extractNumeric(timeline[i].value);
554
+ const prev = extractNumeric(timeline[i - 1]!.value);
555
+ const curr = extractNumeric(timeline[i]!.value);
557
556
  if (prev === null || curr === null) continue;
558
557
  deltas.push({
559
- t: timeline[i].t,
558
+ t: timeline[i]!.t,
560
559
  delta: Math.abs(curr - prev),
561
- from: timeline[i - 1].value,
562
- to: timeline[i].value,
560
+ from: timeline[i - 1]!.value,
561
+ to: timeline[i]!.value,
563
562
  });
564
563
  }
565
564
 
566
565
  if (deltas.length < 3) continue;
567
566
 
568
567
  const sortedDeltas = deltas.map((d) => d.delta).sort((a, b) => a - b);
569
- const medianDelta = sortedDeltas[Math.floor(sortedDeltas.length / 2)];
568
+ const medianDelta = sortedDeltas[Math.floor(sortedDeltas.length / 2)]!;
570
569
  if (medianDelta === 0) continue;
571
570
 
572
571
  const jmpT = config.thresholds.jump;
@@ -615,7 +614,7 @@ export function createDejitterRecorder(): DejitterAPI {
615
614
 
616
615
  const deltas: number[] = [];
617
616
  for (let i = 1; i < numeric.length; i++) {
618
- deltas.push(numeric[i].val - numeric[i - 1].val);
617
+ deltas.push(numeric[i]!.val - numeric[i - 1]!.val);
619
618
  }
620
619
 
621
620
  const windowSize = 5;
@@ -625,31 +624,31 @@ export function createDejitterRecorder(): DejitterAPI {
625
624
  if (i - winStart < 2) { i++; continue; }
626
625
 
627
626
  let sum = 0;
628
- for (let w = winStart; w < i; w++) sum += deltas[w];
627
+ for (let w = winStart; w < i; w++) sum += deltas[w]!;
629
628
  const dominantDir = Math.sign(sum);
630
629
  if (dominantDir === 0) { i++; continue; }
631
630
 
632
- if (deltas[i] !== 0 && Math.sign(deltas[i]) !== dominantDir) {
631
+ if (deltas[i]! !== 0 && Math.sign(deltas[i]!) !== dominantDir) {
633
632
  const reversalStart = i;
634
633
  let reversalEnd = i;
635
634
  while (
636
635
  reversalEnd + 1 < deltas.length &&
637
636
  reversalEnd - reversalStart + 1 < st.maxFrames &&
638
- deltas[reversalEnd + 1] !== 0 &&
639
- Math.sign(deltas[reversalEnd + 1]) !== dominantDir
637
+ deltas[reversalEnd + 1]! !== 0 &&
638
+ Math.sign(deltas[reversalEnd + 1]!) !== dominantDir
640
639
  ) {
641
640
  reversalEnd++;
642
641
  }
643
642
 
644
643
  const afterIdx = reversalEnd + 1;
645
- if (afterIdx >= deltas.length || Math.sign(deltas[afterIdx]) !== dominantDir) {
644
+ if (afterIdx >= deltas.length || Math.sign(deltas[afterIdx]!) !== dominantDir) {
646
645
  i = reversalEnd + 1;
647
646
  continue;
648
647
  }
649
648
 
650
649
  let reversalMag = 0;
651
650
  for (let r = reversalStart; r <= reversalEnd; r++) {
652
- reversalMag += Math.abs(deltas[r]);
651
+ reversalMag += Math.abs(deltas[r]!);
653
652
  }
654
653
 
655
654
  const localStart = Math.max(0, reversalStart - windowSize);
@@ -658,7 +657,7 @@ export function createDejitterRecorder(): DejitterAPI {
658
657
  let localCount = 0;
659
658
  for (let l = localStart; l <= localEnd; l++) {
660
659
  if (l >= reversalStart && l <= reversalEnd) continue;
661
- localSum += Math.abs(deltas[l]);
660
+ localSum += Math.abs(deltas[l]!);
662
661
  localCount++;
663
662
  }
664
663
  const localVelocity = localCount > 0 ? localSum / localCount : 0;
@@ -668,7 +667,7 @@ export function createDejitterRecorder(): DejitterAPI {
668
667
  if (ratio >= st.velocityRatio) {
669
668
  const reversalFrameCount = reversalEnd - reversalStart + 1;
670
669
  const severity = ratio >= 1.0 ? 'high' : ratio >= 0.5 ? 'medium' : 'low';
671
- const t = numeric[reversalStart + 1].t;
670
+ const t = numeric[reversalStart + 1]!.t;
672
671
  findings.push(makeFinding(
673
672
  'stutter', severity,
674
673
  p.elem, elements[p.elem], p.prop,
@@ -717,15 +716,15 @@ export function createDejitterRecorder(): DejitterAPI {
717
716
 
718
717
  const deltas: number[] = [];
719
718
  for (let i = 1; i < numeric.length; i++) {
720
- deltas.push(Math.abs(numeric[i].val - numeric[i - 1].val));
719
+ deltas.push(Math.abs(numeric[i]!.val - numeric[i - 1]!.val));
721
720
  }
722
721
 
723
722
  let i = 0;
724
723
  while (i < deltas.length) {
725
- if (deltas[i] > sk.maxDelta) { i++; continue; }
724
+ if (deltas[i]! > sk.maxDelta) { i++; continue; }
726
725
 
727
726
  const runStart = i;
728
- while (i < deltas.length && deltas[i] <= sk.maxDelta) i++;
727
+ while (i < deltas.length && deltas[i]! <= sk.maxDelta) i++;
729
728
  const runEnd = i - 1;
730
729
  const stillCount = runEnd - runStart + 1;
731
730
 
@@ -737,13 +736,13 @@ export function createDejitterRecorder(): DejitterAPI {
737
736
 
738
737
  const beforeStart = Math.max(0, runStart - windowSize);
739
738
  for (let b = beforeStart; b < runStart; b++) {
740
- surroundingSum += deltas[b];
739
+ surroundingSum += deltas[b]!;
741
740
  surroundingCount++;
742
741
  }
743
742
 
744
743
  const afterEnd = Math.min(deltas.length - 1, runEnd + windowSize);
745
744
  for (let a = runEnd + 1; a <= afterEnd; a++) {
746
- surroundingSum += deltas[a];
745
+ surroundingSum += deltas[a]!;
747
746
  surroundingCount++;
748
747
  }
749
748
 
@@ -752,8 +751,8 @@ export function createDejitterRecorder(): DejitterAPI {
752
751
 
753
752
  if (meanSurroundingVelocity < sk.minSurroundingVelocity) continue;
754
753
 
755
- const tStart = numeric[runStart].t;
756
- const tEnd = numeric[runEnd + 1].t;
754
+ const tStart = numeric[runStart]!.t;
755
+ const tEnd = numeric[runEnd + 1]!.t;
757
756
  const duration = Math.round(tEnd - tStart);
758
757
 
759
758
  const severity = duration >= sk.highDuration ? 'high' : duration >= sk.medDuration ? 'medium' : 'low';
@@ -769,7 +768,7 @@ export function createDejitterRecorder(): DejitterAPI {
769
768
  duration,
770
769
  stillFrames: stillCount,
771
770
  meanSurroundingVelocity: Math.round(meanSurroundingVelocity * 10) / 10,
772
- stuckValue: numeric[runStart].val,
771
+ stuckValue: numeric[runStart]!.val,
773
772
  },
774
773
  }
775
774
  ));
@@ -793,10 +792,10 @@ export function createDejitterRecorder(): DejitterAPI {
793
792
  const deduped: DejitterFinding[] = [];
794
793
  for (const group of shiverGroups.values()) {
795
794
  if (group.length === 1) {
796
- deduped.push(group[0]);
795
+ deduped.push(group[0]!);
797
796
  } else {
798
797
  group.sort((a, b) => b.shiver.amplitude - a.shiver.amplitude);
799
- const rep = { ...group[0] };
798
+ const rep = { ...group[0]! };
800
799
  (rep as any).affectedElements = group.length;
801
800
  rep.description += ` (affects ${group.length} elements)`;
802
801
  deduped.push(rep);
@@ -818,7 +817,7 @@ export function createDejitterRecorder(): DejitterAPI {
818
817
  findings = deduplicateShivers(findings);
819
818
 
820
819
  const sevOrder: Record<string, number> = { high: 0, medium: 1, low: 2, info: 3 };
821
- findings.sort((a, b) => sevOrder[a.severity] - sevOrder[b.severity]);
820
+ findings.sort((a, b) => sevOrder[a.severity]! - sevOrder[b.severity]!);
822
821
 
823
822
  return findings;
824
823
  }
@@ -887,7 +886,7 @@ export function createDejitterRecorder(): DejitterAPI {
887
886
 
888
887
  return {
889
888
  config: { ...config },
890
- duration: rawFrames.length ? rawFrames[rawFrames.length - 1].t : 0,
889
+ duration: rawFrames.length ? rawFrames[rawFrames.length - 1]!.t : 0,
891
890
  rawFrameCount: rawFrames.length,
892
891
  outputFrameCount: samples.length,
893
892
  mutationEvents: mutations.length,
@@ -906,7 +905,7 @@ export function createDejitterRecorder(): DejitterAPI {
906
905
  byMode[p.mode] = (byMode[p.mode] || 0) + 1;
907
906
  }
908
907
  const data: DejitterSummary = {
909
- duration: rawFrames.length ? rawFrames[rawFrames.length - 1].t : 0,
908
+ duration: rawFrames.length ? rawFrames[rawFrames.length - 1]!.t : 0,
910
909
  rawFrameCount: rawFrames.length,
911
910
  targetOutputFrames: propStats.targetFrames,
912
911
  mutationEvents: mutations.length,
@@ -3,6 +3,7 @@ import {
3
3
  AncestryItem,
4
4
  formatAncestryChain,
5
5
  collectAncestry,
6
+ truncateAtFirstFile,
6
7
  } from "./formatAncestryChain";
7
8
 
8
9
  describe("formatAncestryChain", () => {
@@ -266,4 +267,50 @@ describe("formatAncestryChain", () => {
266
267
  );
267
268
  });
268
269
  });
270
+
271
+ describe("truncateAtFirstFile", () => {
272
+ it("keeps from clicked element up to first ancestor with filePath", () => {
273
+ // Bottom-up: clicked element first, root last
274
+ const items: AncestryItem[] = [
275
+ { elementName: "span", componentName: "Button" },
276
+ { elementName: "div", componentName: "Card" },
277
+ { elementName: "div", componentName: "Layout", filePath: "src/Layout.tsx", line: 10 },
278
+ { elementName: "div", componentName: "App", filePath: "src/App.tsx", line: 1 },
279
+ ];
280
+
281
+ const result = truncateAtFirstFile(items);
282
+ expect(result).toEqual([
283
+ { elementName: "span", componentName: "Button" },
284
+ { elementName: "div", componentName: "Card" },
285
+ { elementName: "div", componentName: "Layout", filePath: "src/Layout.tsx", line: 10 },
286
+ ]);
287
+ });
288
+
289
+ it("returns just the clicked element when it already has a filePath", () => {
290
+ const items: AncestryItem[] = [
291
+ { elementName: "button", componentName: "Button", filePath: "src/Button.tsx", line: 5 },
292
+ { elementName: "div", componentName: "Layout", filePath: "src/Layout.tsx", line: 10 },
293
+ { elementName: "div", componentName: "App", filePath: "src/App.tsx", line: 1 },
294
+ ];
295
+
296
+ const result = truncateAtFirstFile(items);
297
+ expect(result).toEqual([
298
+ { elementName: "button", componentName: "Button", filePath: "src/Button.tsx", line: 5 },
299
+ ]);
300
+ });
301
+
302
+ it("returns all items when none have a filePath", () => {
303
+ const items: AncestryItem[] = [
304
+ { elementName: "span", componentName: "A" },
305
+ { elementName: "div", componentName: "B" },
306
+ ];
307
+
308
+ const result = truncateAtFirstFile(items);
309
+ expect(result).toEqual(items);
310
+ });
311
+
312
+ it("returns empty array for empty input", () => {
313
+ expect(truncateAtFirstFile([])).toEqual([]);
314
+ });
315
+ });
269
316
  });
@@ -164,6 +164,17 @@ function getInnermostNamedComponent(item: AncestryItem | null | undefined): stri
164
164
  return undefined;
165
165
  }
166
166
 
167
+ /**
168
+ * Truncate ancestry from the bottom (clicked element) up to and including
169
+ * the first item that has a filePath. Everything above that is discarded.
170
+ * The ancestry array is bottom-up: index 0 = clicked element, last = root.
171
+ */
172
+ export function truncateAtFirstFile(items: AncestryItem[]): AncestryItem[] {
173
+ const firstWithFile = items.findIndex((item) => item.filePath);
174
+ if (firstWithFile === -1) return items;
175
+ return items.slice(0, firstWithFile + 1);
176
+ }
177
+
167
178
  export function formatAncestryChain(items: AncestryItem[]): string {
168
179
  if (items.length === 0) {
169
180
  return "";
@@ -1,26 +1,5 @@
1
1
  import { Fiber } from "@locator/shared";
2
2
 
3
- // function printDebugOwnerTree(fiber: Fiber): string | null {
4
- // let current: Fiber | null = fiber || null;
5
- // let results = [];
6
- // while (current) {
7
- // results.push(getUsableName(current));
8
- // current = current._debugOwner || null;
9
- // }
10
- // console.log('DEBUG OWNER: ', results);
11
- // return null;
12
- // }
13
- // function printReturnTree(fiber: Fiber): string | null {
14
- // let current: Fiber | null = fiber || null;
15
- // let results = [];
16
- // while (current) {
17
- // results.push(getUsableName(current));
18
- // current = current.return || null;
19
- // }
20
- // console.log('RETURN: ', results);
21
- // return null;
22
- // }
23
-
24
3
  export function getUsableName(fiber: Fiber | null | undefined): string {
25
4
  if (!fiber) {
26
5
  return "Not found";
@@ -1,4 +1,4 @@
1
- import { Fiber, Target } from "@locator/shared";
1
+ import { Target } from "@locator/shared";
2
2
 
3
3
  export type Source = {
4
4
  fileName: string;
@@ -7,35 +7,6 @@ export type Source = {
7
7
  projectPath?: string;
8
8
  };
9
9
 
10
- type SimpleElement = {
11
- type: "element";
12
- name: string;
13
- uniqueId: string;
14
- fiber: Fiber;
15
- box: SimpleDOMRect | null;
16
- element: Element | Text;
17
- children: (SimpleElement | SimpleComponent)[];
18
- source: Source | null;
19
- };
20
-
21
- type SimpleComponent = {
22
- type: "component";
23
- uniqueId: string;
24
- name: string;
25
- fiber: Fiber;
26
- box: SimpleDOMRect | null;
27
- children: (SimpleElement | SimpleComponent)[];
28
- source: Source | null;
29
- definitionSourceFile: string | null;
30
- };
31
-
32
- export type SimpleNode = SimpleElement | SimpleComponent;
33
-
34
- export type HighlightedNode = {
35
- getNode: () => SimpleNode | null;
36
- setNode: (node: SimpleNode | null) => void;
37
- };
38
-
39
10
  export type SimpleDOMRect = {
40
11
  height: number;
41
12
  width: number;
@@ -51,5 +22,3 @@ export type LinkProps = {
51
22
  line: number;
52
23
  column: number;
53
24
  };
54
-
55
- export type ContextMenuState = { target: HTMLElement; x: number; y: number };
@@ -1,72 +0,0 @@
1
- import { Fiber } from "@locator/shared";
2
- import { getBoundingRect } from "../../functions/getBoundingRect";
3
- import { getComposedBoundingBox } from "../../functions/getComposedBoundingBox";
4
- import { getUsableName } from "../../functions/getUsableName";
5
-
6
- import { getAllFiberChildren } from "./getAllFiberChildren";
7
- import { SimpleNode } from "../../types/types";
8
- import { makeFiberId } from "./makeFiberId";
9
-
10
- export function fiberToSimple(
11
- fiber: Fiber,
12
- manualChildren?: SimpleNode[]
13
- ): SimpleNode {
14
- let simpleChildren;
15
- if (fiber.elementType?.styledComponentId) {
16
- const children = getAllFiberChildren(fiber);
17
- if (children.length === 1) {
18
- const simple = fiberToSimple(children[0]!, manualChildren);
19
- simple.name = `${simple.name} (styled)`;
20
- return simple;
21
- }
22
- }
23
-
24
- if (manualChildren) {
25
- simpleChildren = manualChildren;
26
- } else {
27
- const children = getAllFiberChildren(fiber);
28
-
29
- simpleChildren = children.map((child) => {
30
- return fiberToSimple(child);
31
- });
32
- }
33
-
34
- const element =
35
- fiber.stateNode instanceof Element || fiber.stateNode instanceof Text
36
- ? fiber.stateNode
37
- : fiber.stateNode?.containerInfo;
38
-
39
- if (element) {
40
- const box = getBoundingRect(element);
41
- return {
42
- type: "element",
43
- element: element,
44
- fiber: fiber,
45
- uniqueId: makeFiberId(fiber),
46
- name: getUsableName(fiber),
47
- box: box || getComposedBoundingBox(simpleChildren),
48
- children: simpleChildren,
49
- source: fiber._debugSource || null,
50
- };
51
- } else {
52
- return {
53
- type: "component",
54
- fiber: fiber,
55
- uniqueId: makeFiberId(fiber),
56
- name: getUsableName(fiber),
57
- box: getComposedBoundingBox(simpleChildren),
58
- children: simpleChildren,
59
- source: fiber._debugSource || null,
60
- definitionSourceFile: simpleChildren.reduce<string | null>(
61
- (acc, curr) => {
62
- if (curr.source?.fileName) {
63
- return curr.source?.fileName;
64
- } else {
65
- return acc;
66
- }
67
- },
68
- null
69
- ),
70
- };
71
- }
72
- }
@@ -1,36 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { Fiber } from "@locator/shared";
3
- import { findFiberByHtmlElement } from "./findFiberByHtmlElement";
4
-
5
- export function gatherFiberRoots(
6
- parentNode: HTMLElement,
7
- mutable_foundFibers: Fiber[]
8
- ) {
9
- const nodes = parentNode.childNodes;
10
- for (let i = 0; i < nodes.length; i++) {
11
- const node = nodes[i];
12
- if (node instanceof HTMLElement) {
13
- const fiber =
14
- (node as any)._reactRootContainer?._internalRoot?.current ||
15
- (node as any)._reactRootContainer?.current;
16
- if (fiber) {
17
- mutable_foundFibers.push(getRoot(fiber));
18
- } else {
19
- const rootFiber = findFiberByHtmlElement(node!, false);
20
- if (rootFiber) {
21
- mutable_foundFibers.push(getRoot(rootFiber));
22
- } else {
23
- gatherFiberRoots(node, mutable_foundFibers);
24
- }
25
- }
26
- }
27
- }
28
- }
29
-
30
- function getRoot(fiber: Fiber): Fiber {
31
- let thisFiber: Fiber = fiber;
32
- while (thisFiber.return) {
33
- thisFiber = thisFiber.return;
34
- }
35
- return thisFiber;
36
- }