@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.
- package/dist/_generated_styles.js +115 -15
- package/dist/adapters/getElementInfo.js +3 -17
- package/dist/adapters/getParentsPath.js +3 -33
- package/dist/adapters/getTree.js +3 -33
- package/dist/adapters/jsx/getJSXComponentBoundingBox.js +0 -1
- package/dist/adapters/jsx/jsxAdapter.js +0 -3
- package/dist/adapters/jsx/runtimeStore.js +0 -12
- package/dist/adapters/react/reactAdapter.js +0 -8
- package/dist/adapters/resolveAdapter.d.ts +8 -0
- package/dist/adapters/resolveAdapter.js +28 -0
- package/dist/adapters/vue/vueAdapter.js +0 -14
- package/dist/browserApi.d.ts +1 -1
- package/dist/browserApi.js +8 -10
- package/dist/components/MaybeOutline.d.ts +1 -0
- package/dist/components/MaybeOutline.js +38 -29
- package/dist/components/Outline.d.ts +1 -0
- package/dist/components/Outline.js +20 -16
- package/dist/components/Runtime.js +30 -18
- package/dist/dejitter/recorder.js +0 -1
- package/dist/functions/formatAncestryChain.d.ts +6 -0
- package/dist/functions/formatAncestryChain.js +11 -0
- package/dist/functions/formatAncestryChain.test.js +75 -1
- package/dist/functions/getUsableName.js +0 -21
- package/dist/output.css +10 -40
- package/dist/types/types.d.ts +1 -32
- package/package.json +1 -1
- package/src/_generated_styles.ts +115 -15
- package/src/adapters/getElementInfo.tsx +3 -23
- package/src/adapters/getParentsPath.tsx +3 -42
- package/src/adapters/getTree.tsx +3 -42
- package/src/adapters/jsx/getJSXComponentBoundingBox.ts +0 -1
- package/src/adapters/jsx/jsxAdapter.ts +0 -2
- package/src/adapters/jsx/runtimeStore.ts +0 -11
- package/src/adapters/react/reactAdapter.ts +0 -7
- package/src/adapters/resolveAdapter.ts +38 -0
- package/src/adapters/vue/vueAdapter.ts +0 -14
- package/src/browserApi.ts +9 -12
- package/src/components/MaybeOutline.tsx +4 -2
- package/src/components/Outline.tsx +2 -1
- package/src/components/Runtime.tsx +27 -18
- package/src/dejitter/recorder.ts +43 -44
- package/src/functions/formatAncestryChain.test.ts +47 -0
- package/src/functions/formatAncestryChain.ts +11 -0
- package/src/functions/getUsableName.ts +0 -21
- package/src/types/types.ts +1 -32
- package/src/adapters/react/fiberToSimple.tsx +0 -72
- package/src/adapters/react/gatherFiberRoots.tsx +0 -36
- package/src/adapters/react/makeFiberId.tsx +0 -19
- package/src/adapters/react/searchDevtoolsRenderersForClosestTarget.ts +0 -15
- package/src/components/Button.tsx +0 -14
- package/src/components/ComponentOutline.tsx +0 -98
- package/src/components/SimpleNodeOutline.tsx +0 -27
- package/src/components/Tooltip.tsx +0 -28
- package/src/functions/cropPath.test.ts +0 -18
- package/src/functions/cropPath.ts +0 -12
- package/src/functions/evalTemplate.test.ts +0 -12
- package/src/functions/evalTemplate.ts +0 -8
- package/src/functions/findNames.ts +0 -20
- package/src/functions/getBoundingRect.tsx +0 -11
- package/src/functions/getComposedBoundingBox.tsx +0 -25
- package/src/functions/getIdsOnPathToRoot.tsx +0 -21
- package/src/functions/getMultipleElementsBoundingBox.tsx +0 -25
- package/src/functions/getPathToParent.tsx +0 -17
- package/src/functions/getUsableFileName.test.tsx +0 -24
- package/src/functions/getUsableFileName.tsx +0 -19
- package/src/functions/transformPath.test.ts +0 -28
- 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
|
-
|
|
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() ? (
|
package/src/dejitter/recorder.ts
CHANGED
|
@@ -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]
|
|
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]
|
|
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]
|
|
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]
|
|
360
|
-
const last = timeline[timeline.length - 1]
|
|
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]
|
|
388
|
-
endT: timeline[timeline.length - 1]
|
|
389
|
-
duration: timeline[timeline.length - 1]
|
|
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]
|
|
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]
|
|
431
|
-
const d2 = numeric[i]
|
|
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]
|
|
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]
|
|
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]
|
|
556
|
-
const curr = extractNumeric(timeline[i]
|
|
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]
|
|
558
|
+
t: timeline[i]!.t,
|
|
560
559
|
delta: Math.abs(curr - prev),
|
|
561
|
-
from: timeline[i - 1]
|
|
562
|
-
to: timeline[i]
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
756
|
-
const tEnd = numeric[runEnd + 1]
|
|
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]
|
|
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]
|
|
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]
|
|
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";
|
package/src/types/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
-
}
|