@treelocator/runtime 0.1.5 → 0.1.7
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/.turbo/turbo-build.log +13 -13
- package/.turbo/turbo-test.log +11 -10
- package/dist/adapters/HtmlElementTreeNode.d.ts +1 -0
- package/dist/adapters/HtmlElementTreeNode.js +4 -1
- package/dist/adapters/react/reactAdapter.d.ts +9 -0
- package/dist/adapters/react/reactAdapter.js +48 -10
- package/dist/components/Runtime.js +77 -27
- package/dist/functions/formatAncestryChain.d.ts +7 -0
- package/dist/functions/formatAncestryChain.js +34 -7
- package/dist/functions/formatAncestryChain.test.js +41 -0
- package/dist/types/TreeNode.d.ts +6 -0
- package/package.json +2 -2
- package/src/adapters/HtmlElementTreeNode.ts +5 -2
- package/src/adapters/react/reactAdapter.ts +54 -16
- package/src/components/Runtime.tsx +85 -28
- package/src/functions/formatAncestryChain.test.ts +46 -0
- package/src/functions/formatAncestryChain.ts +45 -8
- package/src/types/TreeNode.ts +6 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
|
|
2
|
-
> @treelocator/runtime@0.1.
|
|
2
|
+
> @treelocator/runtime@0.1.7 build /Users/wende/projects/locatorjs/packages/runtime
|
|
3
3
|
> concurrently pnpm:build:*
|
|
4
4
|
|
|
5
|
+
[babel]
|
|
6
|
+
[babel] > @treelocator/runtime@0.1.7 build:babel /Users/wende/projects/locatorjs/packages/runtime
|
|
7
|
+
[babel] > babel src --out-dir dist --extensions .js,.jsx,.ts,.tsx
|
|
8
|
+
[babel]
|
|
5
9
|
[ts]
|
|
6
|
-
[ts] > @treelocator/runtime@0.1.
|
|
10
|
+
[ts] > @treelocator/runtime@0.1.7 build:ts /Users/wende/projects/locatorjs/packages/runtime
|
|
7
11
|
[ts] > tsc --declaration --emitDeclarationOnly --noEmit false --outDir dist
|
|
8
12
|
[ts]
|
|
9
|
-
[tailwind]
|
|
10
|
-
[tailwind] > @treelocator/runtime@0.1.5 build:tailwind /Users/wende/projects/locatorjs/packages/runtime
|
|
11
|
-
[tailwind] > tailwindcss -i ./src/main.css -o ./dist/output.css
|
|
12
|
-
[tailwind]
|
|
13
13
|
[wrapImage]
|
|
14
|
-
[wrapImage] > @treelocator/runtime@0.1.
|
|
14
|
+
[wrapImage] > @treelocator/runtime@0.1.7 build:wrapImage /Users/wende/projects/locatorjs/packages/runtime
|
|
15
15
|
[wrapImage] > node ./scripts/wrapImage.js
|
|
16
16
|
[wrapImage]
|
|
17
|
-
[
|
|
18
|
-
[
|
|
19
|
-
[
|
|
20
|
-
[
|
|
17
|
+
[tailwind]
|
|
18
|
+
[tailwind] > @treelocator/runtime@0.1.7 build:tailwind /Users/wende/projects/locatorjs/packages/runtime
|
|
19
|
+
[tailwind] > tailwindcss -i ./src/main.css -o ./dist/output.css
|
|
20
|
+
[tailwind]
|
|
21
21
|
[wrapImage] Tree icon file generated
|
|
22
22
|
[wrapImage] pnpm run build:wrapImage exited with code 0
|
|
23
23
|
[tailwind]
|
|
24
24
|
[tailwind] Rebuilding...
|
|
25
25
|
[tailwind]
|
|
26
|
-
[tailwind] Done in
|
|
26
|
+
[tailwind] Done in 180ms.
|
|
27
27
|
[tailwind] pnpm run build:tailwind exited with code 0
|
|
28
|
-
[babel] Successfully compiled 72 files with Babel (
|
|
28
|
+
[babel] Successfully compiled 72 files with Babel (691ms).
|
|
29
29
|
[babel] pnpm run build:babel exited with code 0
|
|
30
30
|
[ts] pnpm run build:ts exited with code 0
|
package/.turbo/turbo-test.log
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
|
|
2
|
-
> @treelocator/runtime@0.1.
|
|
2
|
+
> @treelocator/runtime@0.1.5 test /Users/wende/projects/locatorjs/packages/runtime
|
|
3
3
|
> vitest run
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
RUN v2.1.9 /Users/wende/projects/locatorjs/packages/runtime
|
|
7
7
|
|
|
8
|
-
✓ src/functions/
|
|
9
|
-
✓ src/functions/evalTemplate.test.ts (1 test)
|
|
10
|
-
✓ src/functions/transformPath.test.ts (3 tests)
|
|
11
|
-
✓ src/functions/
|
|
12
|
-
✓ src/functions/
|
|
8
|
+
✓ src/functions/mergeRects.test.ts (1 test) 2ms
|
|
9
|
+
✓ src/functions/evalTemplate.test.ts (1 test) 2ms
|
|
10
|
+
✓ src/functions/transformPath.test.ts (3 tests) 3ms
|
|
11
|
+
✓ src/functions/getUsableFileName.test.tsx (4 tests) 3ms
|
|
12
|
+
✓ src/functions/formatAncestryChain.test.ts (7 tests) 2ms
|
|
13
|
+
✓ src/functions/cropPath.test.ts (2 tests) 2ms
|
|
13
14
|
|
|
14
|
-
Test Files
|
|
15
|
-
Tests
|
|
16
|
-
Start at
|
|
17
|
-
Duration
|
|
15
|
+
Test Files 6 passed (6)
|
|
16
|
+
Tests 18 passed (18)
|
|
17
|
+
Start at 17:17:22
|
|
18
|
+
Duration 718ms (transform 282ms, setup 0ms, collect 336ms, tests 14ms, environment 1ms, prepare 463ms)
|
|
18
19
|
|
|
@@ -18,7 +18,7 @@ export class HtmlElementTreeNode {
|
|
|
18
18
|
getChildren() {
|
|
19
19
|
const children = Array.from(this.element.children);
|
|
20
20
|
return children.map(child => {
|
|
21
|
-
if (child instanceof HTMLElement) {
|
|
21
|
+
if (child instanceof HTMLElement || child instanceof SVGElement) {
|
|
22
22
|
// @ts-ignore
|
|
23
23
|
return new this.constructor(child);
|
|
24
24
|
} else {
|
|
@@ -40,4 +40,7 @@ export class HtmlElementTreeNode {
|
|
|
40
40
|
getComponent() {
|
|
41
41
|
throw new Error("Method not implemented.");
|
|
42
42
|
}
|
|
43
|
+
getOwnerComponents() {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
43
46
|
}
|
|
@@ -5,7 +5,16 @@ import { HtmlElementTreeNode } from "../HtmlElementTreeNode";
|
|
|
5
5
|
export declare function getElementInfo(found: HTMLElement): FullElementInfo | null;
|
|
6
6
|
export declare class ReactTreeNodeElement extends HtmlElementTreeNode {
|
|
7
7
|
getSource(): Source | null;
|
|
8
|
+
private fiberToTreeNodeComponent;
|
|
8
9
|
getComponent(): TreeNodeComponent | null;
|
|
10
|
+
/**
|
|
11
|
+
* Traverse the _debugOwner chain to collect all owner components.
|
|
12
|
+
* This finds wrapper components (like Sidebar) that don't render their own DOM elements
|
|
13
|
+
* but wrap other components (like GlassPanel) that do.
|
|
14
|
+
*
|
|
15
|
+
* Returns array from outermost owner (Sidebar) to innermost (GlassPanel).
|
|
16
|
+
*/
|
|
17
|
+
getOwnerComponents(): TreeNodeComponent[];
|
|
9
18
|
}
|
|
10
19
|
declare const reactAdapter: AdapterObject;
|
|
11
20
|
export default reactAdapter;
|
|
@@ -63,23 +63,61 @@ export class ReactTreeNodeElement extends HtmlElementTreeNode {
|
|
|
63
63
|
}
|
|
64
64
|
return null;
|
|
65
65
|
}
|
|
66
|
+
fiberToTreeNodeComponent(fiber) {
|
|
67
|
+
const fiberLabel = getFiberLabel(fiber, findDebugSource(fiber)?.source);
|
|
68
|
+
return {
|
|
69
|
+
label: fiberLabel.label,
|
|
70
|
+
callLink: fiberLabel.link && {
|
|
71
|
+
fileName: fiberLabel.link.filePath,
|
|
72
|
+
lineNumber: fiberLabel.link.line,
|
|
73
|
+
columnNumber: fiberLabel.link.column,
|
|
74
|
+
projectPath: fiberLabel.link.projectPath
|
|
75
|
+
} || undefined
|
|
76
|
+
};
|
|
77
|
+
}
|
|
66
78
|
getComponent() {
|
|
67
79
|
const fiber = findFiberByHtmlElement(this.element, false);
|
|
68
80
|
const componentFiber = fiber?._debugOwner;
|
|
69
81
|
if (componentFiber) {
|
|
70
|
-
|
|
71
|
-
return {
|
|
72
|
-
label: fiberLabel.label,
|
|
73
|
-
callLink: fiberLabel.link && {
|
|
74
|
-
fileName: fiberLabel.link.filePath,
|
|
75
|
-
lineNumber: fiberLabel.link.line,
|
|
76
|
-
columnNumber: fiberLabel.link.column,
|
|
77
|
-
projectPath: fiberLabel.link.projectPath
|
|
78
|
-
} || undefined
|
|
79
|
-
};
|
|
82
|
+
return this.fiberToTreeNodeComponent(componentFiber);
|
|
80
83
|
}
|
|
81
84
|
return null;
|
|
82
85
|
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Traverse the _debugOwner chain to collect all owner components.
|
|
89
|
+
* This finds wrapper components (like Sidebar) that don't render their own DOM elements
|
|
90
|
+
* but wrap other components (like GlassPanel) that do.
|
|
91
|
+
*
|
|
92
|
+
* Returns array from outermost owner (Sidebar) to innermost (GlassPanel).
|
|
93
|
+
*/
|
|
94
|
+
getOwnerComponents() {
|
|
95
|
+
const fiber = findFiberByHtmlElement(this.element, false);
|
|
96
|
+
if (!fiber) return [];
|
|
97
|
+
|
|
98
|
+
// Get the parent DOM element's owner to know when to stop
|
|
99
|
+
const parentElement = this.element.parentElement;
|
|
100
|
+
let parentOwnerFiber = null;
|
|
101
|
+
if (parentElement) {
|
|
102
|
+
const parentFiber = findFiberByHtmlElement(parentElement, false);
|
|
103
|
+
parentOwnerFiber = parentFiber?._debugOwner || null;
|
|
104
|
+
}
|
|
105
|
+
const components = [];
|
|
106
|
+
let currentFiber = fiber._debugOwner;
|
|
107
|
+
|
|
108
|
+
// Traverse up the _debugOwner chain until we hit the parent's owner or run out
|
|
109
|
+
while (currentFiber) {
|
|
110
|
+
// Stop if we've reached the parent DOM element's owner component
|
|
111
|
+
if (parentOwnerFiber && currentFiber === parentOwnerFiber) {
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
components.push(this.fiberToTreeNodeComponent(currentFiber));
|
|
115
|
+
currentFiber = currentFiber._debugOwner || null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Reverse so outermost (Sidebar) comes first, innermost (GlassPanel) last
|
|
119
|
+
return components.reverse();
|
|
120
|
+
}
|
|
83
121
|
}
|
|
84
122
|
function getTree(element) {
|
|
85
123
|
const originalRoot = new ReactTreeNodeElement(element);
|
|
@@ -39,14 +39,36 @@ function Runtime(props) {
|
|
|
39
39
|
setHoldingModKey(e.altKey);
|
|
40
40
|
}
|
|
41
41
|
function mouseOverListener(e) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
// Also update modifier state
|
|
43
|
+
setHoldingModKey(e.altKey);
|
|
44
|
+
|
|
45
|
+
// Use elementsFromPoint to find elements including ones with pointer-events-none
|
|
46
|
+
const elementsAtPoint = document.elementsFromPoint(e.clientX, e.clientY);
|
|
47
|
+
|
|
48
|
+
// Find the topmost element with locator data for highlighting
|
|
49
|
+
let element = null;
|
|
50
|
+
for (const el of elementsAtPoint) {
|
|
51
|
+
if (isLocatorsOwnElement(el)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (el instanceof HTMLElement || el instanceof SVGElement) {
|
|
55
|
+
const withLocator = el.closest('[data-locatorjs-id], [data-locatorjs]');
|
|
56
|
+
if (withLocator) {
|
|
57
|
+
element = withLocator;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Fallback to e.target
|
|
64
|
+
if (!element) {
|
|
65
|
+
const target = e.target;
|
|
66
|
+
if (target && (target instanceof HTMLElement || target instanceof SVGElement)) {
|
|
67
|
+
element = target instanceof SVGElement ? target.closest('svg') ?? target : target;
|
|
46
68
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
69
|
+
}
|
|
70
|
+
if (element && !isLocatorsOwnElement(element)) {
|
|
71
|
+
setCurrentElement(element);
|
|
50
72
|
}
|
|
51
73
|
}
|
|
52
74
|
function mouseDownUpListener(e) {
|
|
@@ -62,31 +84,59 @@ function Runtime(props) {
|
|
|
62
84
|
if (!e.altKey && !isCombinationModifiersPressed(e) && !locatorActive()) {
|
|
63
85
|
return;
|
|
64
86
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
87
|
+
|
|
88
|
+
// Use elementsFromPoint to find all elements at click position,
|
|
89
|
+
// including ones with pointer-events-none (like canvas overlays)
|
|
90
|
+
const elementsAtPoint = document.elementsFromPoint(e.clientX, e.clientY);
|
|
91
|
+
|
|
92
|
+
// Find the topmost element with locator data
|
|
93
|
+
let element = null;
|
|
94
|
+
for (const el of elementsAtPoint) {
|
|
95
|
+
if (isLocatorsOwnElement(el)) {
|
|
96
|
+
continue;
|
|
69
97
|
}
|
|
70
|
-
if (
|
|
71
|
-
|
|
98
|
+
if (el instanceof HTMLElement || el instanceof SVGElement) {
|
|
99
|
+
// Check if this element or its closest ancestor has locator data
|
|
100
|
+
const withLocator = el.closest('[data-locatorjs-id], [data-locatorjs]');
|
|
101
|
+
if (withLocator) {
|
|
102
|
+
element = withLocator;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
72
105
|
}
|
|
73
|
-
|
|
74
|
-
e.stopPropagation();
|
|
106
|
+
}
|
|
75
107
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
navigator.clipboard.writeText(formatted).then(() => {
|
|
82
|
-
setToastMessage("Copied to clipboard");
|
|
83
|
-
});
|
|
108
|
+
// Fallback to e.target if elementsFromPoint didn't find anything
|
|
109
|
+
if (!element) {
|
|
110
|
+
const target = e.target;
|
|
111
|
+
if (target && (target instanceof HTMLElement || target instanceof SVGElement)) {
|
|
112
|
+
element = target instanceof SVGElement ? target.closest('[data-locatorjs-id], [data-locatorjs]') ?? target.closest('svg') ?? target : target;
|
|
84
113
|
}
|
|
114
|
+
}
|
|
115
|
+
if (!element) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (element instanceof HTMLElement && element.shadowRoot) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (isLocatorsOwnElement(element)) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
e.preventDefault();
|
|
125
|
+
e.stopPropagation();
|
|
85
126
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
127
|
+
// Copy ancestry to clipboard on alt+click
|
|
128
|
+
const treeNode = createTreeNode(element, props.adapterId);
|
|
129
|
+
if (treeNode) {
|
|
130
|
+
const ancestry = collectAncestry(treeNode);
|
|
131
|
+
const formatted = formatAncestryChain(ancestry);
|
|
132
|
+
navigator.clipboard.writeText(formatted).then(() => {
|
|
133
|
+
setToastMessage("Copied to clipboard");
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Deactivate toggle after click
|
|
138
|
+
if (locatorActive()) {
|
|
139
|
+
setLocatorActive(false);
|
|
90
140
|
}
|
|
91
141
|
}
|
|
92
142
|
function scrollListener() {
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { TreeNode } from "../types/TreeNode";
|
|
2
|
+
export interface OwnerComponentInfo {
|
|
3
|
+
name: string;
|
|
4
|
+
filePath?: string;
|
|
5
|
+
line?: number;
|
|
6
|
+
}
|
|
2
7
|
export interface AncestryItem {
|
|
3
8
|
elementName: string;
|
|
4
9
|
componentName?: string;
|
|
@@ -6,6 +11,8 @@ export interface AncestryItem {
|
|
|
6
11
|
line?: number;
|
|
7
12
|
id?: string;
|
|
8
13
|
nthChild?: number;
|
|
14
|
+
/** All owner components from outermost (Sidebar) to innermost (GlassPanel) */
|
|
15
|
+
ownerComponents?: OwnerComponentInfo[];
|
|
9
16
|
}
|
|
10
17
|
export declare function collectAncestry(node: TreeNode): AncestryItem[];
|
|
11
18
|
export declare function formatAncestryChain(items: AncestryItem[]): string;
|
|
@@ -19,6 +19,13 @@ function getNthChildIfAmbiguous(element) {
|
|
|
19
19
|
const index = siblings.indexOf(element);
|
|
20
20
|
return index + 1; // 1-indexed for CSS nth-child compatibility
|
|
21
21
|
}
|
|
22
|
+
function treeNodeComponentToOwnerInfo(comp) {
|
|
23
|
+
return {
|
|
24
|
+
name: comp.label,
|
|
25
|
+
filePath: comp.callLink?.fileName,
|
|
26
|
+
line: comp.callLink?.lineNumber
|
|
27
|
+
};
|
|
28
|
+
}
|
|
22
29
|
export function collectAncestry(node) {
|
|
23
30
|
const items = [];
|
|
24
31
|
let current = node;
|
|
@@ -28,7 +35,6 @@ export function collectAncestry(node) {
|
|
|
28
35
|
current = current.getParent();
|
|
29
36
|
continue;
|
|
30
37
|
}
|
|
31
|
-
const component = current.getComponent();
|
|
32
38
|
const source = current.getSource();
|
|
33
39
|
const item = {
|
|
34
40
|
elementName: current.name
|
|
@@ -47,11 +53,27 @@ export function collectAncestry(node) {
|
|
|
47
53
|
}
|
|
48
54
|
}
|
|
49
55
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
|
|
57
|
+
// Get all owner components (from outermost like Sidebar to innermost like GlassPanel)
|
|
58
|
+
const ownerComponents = current.getOwnerComponents();
|
|
59
|
+
const outermost = ownerComponents[0];
|
|
60
|
+
if (outermost) {
|
|
61
|
+
item.ownerComponents = ownerComponents.map(treeNodeComponentToOwnerInfo);
|
|
62
|
+
// Use outermost component as the primary component name
|
|
63
|
+
item.componentName = outermost.label;
|
|
64
|
+
if (outermost.callLink) {
|
|
65
|
+
item.filePath = outermost.callLink.fileName;
|
|
66
|
+
item.line = outermost.callLink.lineNumber;
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
// Fallback to single component if getOwnerComponents not available
|
|
70
|
+
const component = current.getComponent();
|
|
71
|
+
if (component) {
|
|
72
|
+
item.componentName = component.label;
|
|
73
|
+
if (component.callLink) {
|
|
74
|
+
item.filePath = component.callLink.fileName;
|
|
75
|
+
item.line = component.callLink.lineNumber;
|
|
76
|
+
}
|
|
55
77
|
}
|
|
56
78
|
}
|
|
57
79
|
if (!item.filePath && source) {
|
|
@@ -88,7 +110,12 @@ export function formatAncestryChain(items) {
|
|
|
88
110
|
selector += `#${item.id}`;
|
|
89
111
|
}
|
|
90
112
|
let description = selector;
|
|
91
|
-
|
|
113
|
+
|
|
114
|
+
// Show all owner components if available (Sidebar > GlassPanel)
|
|
115
|
+
if (item.ownerComponents && item.ownerComponents.length > 0) {
|
|
116
|
+
const componentChain = item.ownerComponents.map(c => c.name).join(" > ");
|
|
117
|
+
description = `${selector} in ${componentChain}`;
|
|
118
|
+
} else if (item.componentName) {
|
|
92
119
|
description = `${selector} in ${item.componentName}`;
|
|
93
120
|
}
|
|
94
121
|
const location = item.filePath ? ` at ${item.filePath}:${item.line}` : "";
|
|
@@ -78,4 +78,45 @@ describe("formatAncestryChain", () => {
|
|
|
78
78
|
const result = formatAncestryChain([]);
|
|
79
79
|
expect(result).toBe("");
|
|
80
80
|
});
|
|
81
|
+
it("shows all owner components when ownerComponents is provided", () => {
|
|
82
|
+
const items = [{
|
|
83
|
+
elementName: "div",
|
|
84
|
+
id: "sidebar-panel",
|
|
85
|
+
componentName: "Sidebar",
|
|
86
|
+
filePath: "src/components/game/Sidebar.jsx",
|
|
87
|
+
line: 78,
|
|
88
|
+
ownerComponents: [{
|
|
89
|
+
name: "Sidebar",
|
|
90
|
+
filePath: "src/components/game/Sidebar.jsx",
|
|
91
|
+
line: 78
|
|
92
|
+
}, {
|
|
93
|
+
name: "GlassPanel",
|
|
94
|
+
filePath: "src/components/common/GlassPanel.jsx",
|
|
95
|
+
line: 39
|
|
96
|
+
}]
|
|
97
|
+
}, {
|
|
98
|
+
elementName: "div",
|
|
99
|
+
componentName: "App",
|
|
100
|
+
filePath: "src/App.jsx",
|
|
101
|
+
line: 104
|
|
102
|
+
}];
|
|
103
|
+
const result = formatAncestryChain(items);
|
|
104
|
+
expect(result).toBe(`div in App at src/App.jsx:104
|
|
105
|
+
└─ div#sidebar-panel in Sidebar > GlassPanel at src/components/game/Sidebar.jsx:78`);
|
|
106
|
+
});
|
|
107
|
+
it("shows single owner component without arrow when only one in chain", () => {
|
|
108
|
+
const items = [{
|
|
109
|
+
elementName: "button",
|
|
110
|
+
componentName: "Button",
|
|
111
|
+
ownerComponents: [{
|
|
112
|
+
name: "Button",
|
|
113
|
+
filePath: "src/Button.tsx",
|
|
114
|
+
line: 10
|
|
115
|
+
}],
|
|
116
|
+
filePath: "src/Button.tsx",
|
|
117
|
+
line: 10
|
|
118
|
+
}];
|
|
119
|
+
const result = formatAncestryChain(items);
|
|
120
|
+
expect(result).toBe("button in Button at src/Button.tsx:10");
|
|
121
|
+
});
|
|
81
122
|
});
|
package/dist/types/TreeNode.d.ts
CHANGED
|
@@ -8,6 +8,12 @@ export interface TreeNode {
|
|
|
8
8
|
getChildren(): TreeNode[];
|
|
9
9
|
getSource(): Source | null;
|
|
10
10
|
getComponent(): TreeNodeComponent | null;
|
|
11
|
+
/**
|
|
12
|
+
* Get all owner components in the ownership chain.
|
|
13
|
+
* For React: traverses _debugOwner chain until hitting parent DOM element's owner.
|
|
14
|
+
* Returns array from outermost (user's component like Sidebar) to innermost (wrapper like GlassPanel).
|
|
15
|
+
*/
|
|
16
|
+
getOwnerComponents(): TreeNodeComponent[];
|
|
11
17
|
}
|
|
12
18
|
export type TreeNodeComponent = {
|
|
13
19
|
label: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@treelocator/runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "TreeLocatorJS runtime for component ancestry tracking. Alt+click any element to copy its component tree to clipboard. Exposes window.__treelocator__ API for browser automation (Playwright, Puppeteer, Selenium, Cypress).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"locator",
|
|
@@ -72,5 +72,5 @@
|
|
|
72
72
|
"directory": "packages/runtime"
|
|
73
73
|
},
|
|
74
74
|
"license": "MIT",
|
|
75
|
-
"gitHead": "
|
|
75
|
+
"gitHead": "233b05abf1f13da9e1d49b73db62bb8561636618"
|
|
76
76
|
}
|
|
@@ -25,9 +25,9 @@ export class HtmlElementTreeNode implements TreeNode {
|
|
|
25
25
|
const children = Array.from(this.element.children);
|
|
26
26
|
return children
|
|
27
27
|
.map((child) => {
|
|
28
|
-
if (child instanceof HTMLElement) {
|
|
28
|
+
if (child instanceof HTMLElement || child instanceof SVGElement) {
|
|
29
29
|
// @ts-ignore
|
|
30
|
-
return new this.constructor(child);
|
|
30
|
+
return new this.constructor(child as HTMLElement);
|
|
31
31
|
} else {
|
|
32
32
|
return null;
|
|
33
33
|
}
|
|
@@ -48,4 +48,7 @@ export class HtmlElementTreeNode implements TreeNode {
|
|
|
48
48
|
getComponent(): TreeNodeComponent | null {
|
|
49
49
|
throw new Error("Method not implemented.");
|
|
50
50
|
}
|
|
51
|
+
getOwnerComponents(): TreeNodeComponent[] {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
51
54
|
}
|
|
@@ -81,30 +81,68 @@ export class ReactTreeNodeElement extends HtmlElementTreeNode {
|
|
|
81
81
|
}
|
|
82
82
|
return null;
|
|
83
83
|
}
|
|
84
|
+
|
|
85
|
+
private fiberToTreeNodeComponent(fiber: Fiber): TreeNodeComponent {
|
|
86
|
+
const fiberLabel = getFiberLabel(fiber, findDebugSource(fiber)?.source);
|
|
87
|
+
return {
|
|
88
|
+
label: fiberLabel.label,
|
|
89
|
+
callLink:
|
|
90
|
+
(fiberLabel.link && {
|
|
91
|
+
fileName: fiberLabel.link.filePath,
|
|
92
|
+
lineNumber: fiberLabel.link.line,
|
|
93
|
+
columnNumber: fiberLabel.link.column,
|
|
94
|
+
projectPath: fiberLabel.link.projectPath,
|
|
95
|
+
}) ||
|
|
96
|
+
undefined,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
84
100
|
getComponent(): TreeNodeComponent | null {
|
|
85
101
|
const fiber = findFiberByHtmlElement(this.element, false);
|
|
86
102
|
const componentFiber = fiber?._debugOwner;
|
|
87
103
|
|
|
88
104
|
if (componentFiber) {
|
|
89
|
-
|
|
90
|
-
componentFiber,
|
|
91
|
-
findDebugSource(componentFiber)?.source
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
return {
|
|
95
|
-
label: fiberLabel.label,
|
|
96
|
-
callLink:
|
|
97
|
-
(fiberLabel.link && {
|
|
98
|
-
fileName: fiberLabel.link.filePath,
|
|
99
|
-
lineNumber: fiberLabel.link.line,
|
|
100
|
-
columnNumber: fiberLabel.link.column,
|
|
101
|
-
projectPath: fiberLabel.link.projectPath,
|
|
102
|
-
}) ||
|
|
103
|
-
undefined,
|
|
104
|
-
};
|
|
105
|
+
return this.fiberToTreeNodeComponent(componentFiber);
|
|
105
106
|
}
|
|
106
107
|
return null;
|
|
107
108
|
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Traverse the _debugOwner chain to collect all owner components.
|
|
112
|
+
* This finds wrapper components (like Sidebar) that don't render their own DOM elements
|
|
113
|
+
* but wrap other components (like GlassPanel) that do.
|
|
114
|
+
*
|
|
115
|
+
* Returns array from outermost owner (Sidebar) to innermost (GlassPanel).
|
|
116
|
+
*/
|
|
117
|
+
getOwnerComponents(): TreeNodeComponent[] {
|
|
118
|
+
const fiber = findFiberByHtmlElement(this.element, false);
|
|
119
|
+
if (!fiber) return [];
|
|
120
|
+
|
|
121
|
+
// Get the parent DOM element's owner to know when to stop
|
|
122
|
+
const parentElement = this.element.parentElement;
|
|
123
|
+
let parentOwnerFiber: Fiber | null = null;
|
|
124
|
+
if (parentElement) {
|
|
125
|
+
const parentFiber = findFiberByHtmlElement(parentElement, false);
|
|
126
|
+
parentOwnerFiber = parentFiber?._debugOwner || null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const components: TreeNodeComponent[] = [];
|
|
130
|
+
let currentFiber = fiber._debugOwner;
|
|
131
|
+
|
|
132
|
+
// Traverse up the _debugOwner chain until we hit the parent's owner or run out
|
|
133
|
+
while (currentFiber) {
|
|
134
|
+
// Stop if we've reached the parent DOM element's owner component
|
|
135
|
+
if (parentOwnerFiber && currentFiber === parentOwnerFiber) {
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
components.push(this.fiberToTreeNodeComponent(currentFiber));
|
|
140
|
+
currentFiber = currentFiber._debugOwner || null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Reverse so outermost (Sidebar) comes first, innermost (GlassPanel) last
|
|
144
|
+
return components.reverse();
|
|
145
|
+
}
|
|
108
146
|
}
|
|
109
147
|
|
|
110
148
|
function getTree(element: HTMLElement): TreeState | null {
|
|
@@ -48,15 +48,39 @@ function Runtime(props: RuntimeProps) {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
function mouseOverListener(e: MouseEvent) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
// Also update modifier state
|
|
52
|
+
setHoldingModKey(e.altKey);
|
|
53
|
+
|
|
54
|
+
// Use elementsFromPoint to find elements including ones with pointer-events-none
|
|
55
|
+
const elementsAtPoint = document.elementsFromPoint(e.clientX, e.clientY);
|
|
56
|
+
|
|
57
|
+
// Find the topmost element with locator data for highlighting
|
|
58
|
+
let element: HTMLElement | null = null;
|
|
59
|
+
for (const el of elementsAtPoint) {
|
|
60
|
+
if (isLocatorsOwnElement(el as HTMLElement)) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (el instanceof HTMLElement || el instanceof SVGElement) {
|
|
64
|
+
const withLocator = el.closest('[data-locatorjs-id], [data-locatorjs]');
|
|
65
|
+
if (withLocator) {
|
|
66
|
+
element = withLocator as HTMLElement;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Fallback to e.target
|
|
73
|
+
if (!element) {
|
|
74
|
+
const target = e.target;
|
|
75
|
+
if (target && (target instanceof HTMLElement || target instanceof SVGElement)) {
|
|
76
|
+
element = target instanceof SVGElement
|
|
77
|
+
? (target.closest('svg') as HTMLElement | null) ?? (target as unknown as HTMLElement)
|
|
78
|
+
: target;
|
|
55
79
|
}
|
|
80
|
+
}
|
|
56
81
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
setHoldingModKey(e.altKey);
|
|
82
|
+
if (element && !isLocatorsOwnElement(element)) {
|
|
83
|
+
setCurrentElement(element);
|
|
60
84
|
}
|
|
61
85
|
}
|
|
62
86
|
|
|
@@ -76,33 +100,66 @@ function Runtime(props: RuntimeProps) {
|
|
|
76
100
|
return;
|
|
77
101
|
}
|
|
78
102
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
103
|
+
// Use elementsFromPoint to find all elements at click position,
|
|
104
|
+
// including ones with pointer-events-none (like canvas overlays)
|
|
105
|
+
const elementsAtPoint = document.elementsFromPoint(e.clientX, e.clientY);
|
|
106
|
+
|
|
107
|
+
// Find the topmost element with locator data
|
|
108
|
+
let element: Element | null = null;
|
|
109
|
+
for (const el of elementsAtPoint) {
|
|
110
|
+
if (isLocatorsOwnElement(el as HTMLElement)) {
|
|
111
|
+
continue;
|
|
83
112
|
}
|
|
113
|
+
if (el instanceof HTMLElement || el instanceof SVGElement) {
|
|
114
|
+
// Check if this element or its closest ancestor has locator data
|
|
115
|
+
const withLocator = el.closest('[data-locatorjs-id], [data-locatorjs]');
|
|
116
|
+
if (withLocator) {
|
|
117
|
+
element = withLocator;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
84
122
|
|
|
85
|
-
|
|
86
|
-
|
|
123
|
+
// Fallback to e.target if elementsFromPoint didn't find anything
|
|
124
|
+
if (!element) {
|
|
125
|
+
const target = e.target;
|
|
126
|
+
if (target && (target instanceof HTMLElement || target instanceof SVGElement)) {
|
|
127
|
+
element = target instanceof SVGElement
|
|
128
|
+
? (target.closest('[data-locatorjs-id], [data-locatorjs]') as Element | null) ??
|
|
129
|
+
(target.closest('svg') as Element | null) ??
|
|
130
|
+
target
|
|
131
|
+
: target;
|
|
87
132
|
}
|
|
133
|
+
}
|
|
88
134
|
|
|
89
|
-
|
|
90
|
-
|
|
135
|
+
if (!element) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
91
138
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const ancestry = collectAncestry(treeNode);
|
|
96
|
-
const formatted = formatAncestryChain(ancestry);
|
|
97
|
-
navigator.clipboard.writeText(formatted).then(() => {
|
|
98
|
-
setToastMessage("Copied to clipboard");
|
|
99
|
-
});
|
|
100
|
-
}
|
|
139
|
+
if (element instanceof HTMLElement && element.shadowRoot) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
101
142
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
143
|
+
if (isLocatorsOwnElement(element as HTMLElement)) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
e.preventDefault();
|
|
148
|
+
e.stopPropagation();
|
|
149
|
+
|
|
150
|
+
// Copy ancestry to clipboard on alt+click
|
|
151
|
+
const treeNode = createTreeNode(element as HTMLElement, props.adapterId);
|
|
152
|
+
if (treeNode) {
|
|
153
|
+
const ancestry = collectAncestry(treeNode);
|
|
154
|
+
const formatted = formatAncestryChain(ancestry);
|
|
155
|
+
navigator.clipboard.writeText(formatted).then(() => {
|
|
156
|
+
setToastMessage("Copied to clipboard");
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Deactivate toggle after click
|
|
161
|
+
if (locatorActive()) {
|
|
162
|
+
setLocatorActive(false);
|
|
106
163
|
}
|
|
107
164
|
}
|
|
108
165
|
|
|
@@ -91,4 +91,50 @@ describe("formatAncestryChain", () => {
|
|
|
91
91
|
const result = formatAncestryChain([]);
|
|
92
92
|
expect(result).toBe("");
|
|
93
93
|
});
|
|
94
|
+
|
|
95
|
+
it("shows all owner components when ownerComponents is provided", () => {
|
|
96
|
+
const items: AncestryItem[] = [
|
|
97
|
+
{
|
|
98
|
+
elementName: "div",
|
|
99
|
+
id: "sidebar-panel",
|
|
100
|
+
componentName: "Sidebar",
|
|
101
|
+
filePath: "src/components/game/Sidebar.jsx",
|
|
102
|
+
line: 78,
|
|
103
|
+
ownerComponents: [
|
|
104
|
+
{
|
|
105
|
+
name: "Sidebar",
|
|
106
|
+
filePath: "src/components/game/Sidebar.jsx",
|
|
107
|
+
line: 78,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "GlassPanel",
|
|
111
|
+
filePath: "src/components/common/GlassPanel.jsx",
|
|
112
|
+
line: 39,
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
{ elementName: "div", componentName: "App", filePath: "src/App.jsx", line: 104 },
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
const result = formatAncestryChain(items);
|
|
120
|
+
expect(result).toBe(
|
|
121
|
+
`div in App at src/App.jsx:104
|
|
122
|
+
└─ div#sidebar-panel in Sidebar > GlassPanel at src/components/game/Sidebar.jsx:78`
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("shows single owner component without arrow when only one in chain", () => {
|
|
127
|
+
const items: AncestryItem[] = [
|
|
128
|
+
{
|
|
129
|
+
elementName: "button",
|
|
130
|
+
componentName: "Button",
|
|
131
|
+
ownerComponents: [{ name: "Button", filePath: "src/Button.tsx", line: 10 }],
|
|
132
|
+
filePath: "src/Button.tsx",
|
|
133
|
+
line: 10,
|
|
134
|
+
},
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
const result = formatAncestryChain(items);
|
|
138
|
+
expect(result).toBe("button in Button at src/Button.tsx:10");
|
|
139
|
+
});
|
|
94
140
|
});
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import { TreeNode, TreeNodeElement } from "../types/TreeNode";
|
|
1
|
+
import { TreeNode, TreeNodeComponent, TreeNodeElement } from "../types/TreeNode";
|
|
2
|
+
|
|
3
|
+
export interface OwnerComponentInfo {
|
|
4
|
+
name: string;
|
|
5
|
+
filePath?: string;
|
|
6
|
+
line?: number;
|
|
7
|
+
}
|
|
2
8
|
|
|
3
9
|
export interface AncestryItem {
|
|
4
10
|
elementName: string;
|
|
@@ -7,6 +13,8 @@ export interface AncestryItem {
|
|
|
7
13
|
line?: number;
|
|
8
14
|
id?: string;
|
|
9
15
|
nthChild?: number; // 1-indexed, only set when there are ambiguous siblings
|
|
16
|
+
/** All owner components from outermost (Sidebar) to innermost (GlassPanel) */
|
|
17
|
+
ownerComponents?: OwnerComponentInfo[];
|
|
10
18
|
}
|
|
11
19
|
|
|
12
20
|
// Elements to exclude from ancestry (not useful for debugging)
|
|
@@ -36,6 +44,16 @@ function getNthChildIfAmbiguous(element: Element): number | undefined {
|
|
|
36
44
|
return index + 1; // 1-indexed for CSS nth-child compatibility
|
|
37
45
|
}
|
|
38
46
|
|
|
47
|
+
function treeNodeComponentToOwnerInfo(
|
|
48
|
+
comp: TreeNodeComponent
|
|
49
|
+
): OwnerComponentInfo {
|
|
50
|
+
return {
|
|
51
|
+
name: comp.label,
|
|
52
|
+
filePath: comp.callLink?.fileName,
|
|
53
|
+
line: comp.callLink?.lineNumber,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
39
57
|
export function collectAncestry(node: TreeNode): AncestryItem[] {
|
|
40
58
|
const items: AncestryItem[] = [];
|
|
41
59
|
let current: TreeNode | null = node;
|
|
@@ -47,7 +65,6 @@ export function collectAncestry(node: TreeNode): AncestryItem[] {
|
|
|
47
65
|
continue;
|
|
48
66
|
}
|
|
49
67
|
|
|
50
|
-
const component = current.getComponent();
|
|
51
68
|
const source = current.getSource();
|
|
52
69
|
|
|
53
70
|
const item: AncestryItem = {
|
|
@@ -68,11 +85,26 @@ export function collectAncestry(node: TreeNode): AncestryItem[] {
|
|
|
68
85
|
}
|
|
69
86
|
}
|
|
70
87
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
88
|
+
// Get all owner components (from outermost like Sidebar to innermost like GlassPanel)
|
|
89
|
+
const ownerComponents = current.getOwnerComponents();
|
|
90
|
+
const outermost = ownerComponents[0];
|
|
91
|
+
if (outermost) {
|
|
92
|
+
item.ownerComponents = ownerComponents.map(treeNodeComponentToOwnerInfo);
|
|
93
|
+
// Use outermost component as the primary component name
|
|
94
|
+
item.componentName = outermost.label;
|
|
95
|
+
if (outermost.callLink) {
|
|
96
|
+
item.filePath = outermost.callLink.fileName;
|
|
97
|
+
item.line = outermost.callLink.lineNumber;
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
// Fallback to single component if getOwnerComponents not available
|
|
101
|
+
const component = current.getComponent();
|
|
102
|
+
if (component) {
|
|
103
|
+
item.componentName = component.label;
|
|
104
|
+
if (component.callLink) {
|
|
105
|
+
item.filePath = component.callLink.fileName;
|
|
106
|
+
item.line = component.callLink.lineNumber;
|
|
107
|
+
}
|
|
76
108
|
}
|
|
77
109
|
}
|
|
78
110
|
|
|
@@ -116,7 +148,12 @@ export function formatAncestryChain(items: AncestryItem[]): string {
|
|
|
116
148
|
}
|
|
117
149
|
|
|
118
150
|
let description = selector;
|
|
119
|
-
|
|
151
|
+
|
|
152
|
+
// Show all owner components if available (Sidebar > GlassPanel)
|
|
153
|
+
if (item.ownerComponents && item.ownerComponents.length > 0) {
|
|
154
|
+
const componentChain = item.ownerComponents.map((c) => c.name).join(" > ");
|
|
155
|
+
description = `${selector} in ${componentChain}`;
|
|
156
|
+
} else if (item.componentName) {
|
|
120
157
|
description = `${selector} in ${item.componentName}`;
|
|
121
158
|
}
|
|
122
159
|
|
package/src/types/TreeNode.ts
CHANGED
|
@@ -9,6 +9,12 @@ export interface TreeNode {
|
|
|
9
9
|
getChildren(): TreeNode[];
|
|
10
10
|
getSource(): Source | null;
|
|
11
11
|
getComponent(): TreeNodeComponent | null;
|
|
12
|
+
/**
|
|
13
|
+
* Get all owner components in the ownership chain.
|
|
14
|
+
* For React: traverses _debugOwner chain until hitting parent DOM element's owner.
|
|
15
|
+
* Returns array from outermost (user's component like Sidebar) to innermost (wrapper like GlassPanel).
|
|
16
|
+
*/
|
|
17
|
+
getOwnerComponents(): TreeNodeComponent[];
|
|
12
18
|
}
|
|
13
19
|
|
|
14
20
|
export type TreeNodeComponent = {
|