@treelocator/runtime 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,30 @@
1
+
2
+ > @treelocator/runtime@0.1.4 build /Users/wende/projects/locatorjs/packages/runtime
3
+ > concurrently pnpm:build:*
4
+
5
+ [wrapImage]
6
+ [wrapImage] > @treelocator/runtime@0.1.4 build:wrapImage /Users/wende/projects/locatorjs/packages/runtime
7
+ [wrapImage] > node ./scripts/wrapImage.js
8
+ [wrapImage]
9
+ [tailwind]
10
+ [tailwind] > @treelocator/runtime@0.1.4 build:tailwind /Users/wende/projects/locatorjs/packages/runtime
11
+ [tailwind] > tailwindcss -i ./src/main.css -o ./dist/output.css
12
+ [tailwind]
13
+ [ts]
14
+ [ts] > @treelocator/runtime@0.1.4 build:ts /Users/wende/projects/locatorjs/packages/runtime
15
+ [ts] > tsc --declaration --emitDeclarationOnly --noEmit false --outDir dist
16
+ [ts]
17
+ [babel]
18
+ [babel] > @treelocator/runtime@0.1.4 build:babel /Users/wende/projects/locatorjs/packages/runtime
19
+ [babel] > babel src --out-dir dist --extensions .js,.jsx,.ts,.tsx
20
+ [babel]
21
+ [wrapImage] Tree icon file generated
22
+ [wrapImage] pnpm run build:wrapImage exited with code 0
23
+ [tailwind]
24
+ [tailwind] Rebuilding...
25
+ [tailwind]
26
+ [tailwind] Done in 160ms.
27
+ [tailwind] pnpm run build:tailwind exited with code 0
28
+ [babel] Successfully compiled 72 files with Babel (416ms).
29
+ [babel] pnpm run build:babel exited with code 0
30
+ [ts] pnpm run build:ts exited with code 0
@@ -0,0 +1,18 @@
1
+
2
+ > @treelocator/runtime@0.1.2 test /Users/wende/projects/locatorjs/packages/runtime
3
+ > vitest run
4
+
5
+
6
+ RUN v2.1.9 /Users/wende/projects/locatorjs/packages/runtime
7
+
8
+ ✓ src/functions/cropPath.test.ts (2 tests) 1ms
9
+ ✓ src/functions/evalTemplate.test.ts (1 test) 1ms
10
+ ✓ src/functions/transformPath.test.ts (3 tests) 2ms
11
+ ✓ src/functions/mergeRects.test.ts (1 test) 1ms
12
+ ✓ src/functions/getUsableFileName.test.tsx (4 tests) 1ms
13
+
14
+ Test Files 5 passed (5)
15
+ Tests 11 passed (11)
16
+ Start at 18:29:05
17
+ Duration 687ms (transform 240ms, setup 0ms, collect 278ms, tests 7ms, environment 1ms, prepare 366ms)
18
+
@@ -0,0 +1,4 @@
1
+
2
+ > @locator/runtime@0.5.1 ts /Users/wende/projects/locatorjs/packages/runtime
3
+ > tsc --noEmit
4
+
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Krzysztof Wende
4
+ Copyright (c) 2023 Michael Musil (original LocatorJS)
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
@@ -4,6 +4,8 @@ export interface AncestryItem {
4
4
  componentName?: string;
5
5
  filePath?: string;
6
6
  line?: number;
7
+ id?: string;
8
+ nthChild?: number;
7
9
  }
8
10
  export declare function collectAncestry(node: TreeNode): AncestryItem[];
9
11
  export declare function formatAncestryChain(items: AncestryItem[]): string;
@@ -1,5 +1,24 @@
1
1
  // Elements to exclude from ancestry (not useful for debugging)
2
2
  const EXCLUDED_ELEMENTS = new Set(["html", "body", "head"]);
3
+ function isTreeNodeElement(node) {
4
+ return "getElement" in node && typeof node.getElement === "function";
5
+ }
6
+
7
+ /**
8
+ * Calculate nth-child position if there are multiple siblings of the same type.
9
+ * Returns undefined if the element is unique among its siblings, or 1-indexed position otherwise.
10
+ */
11
+ function getNthChildIfAmbiguous(element) {
12
+ const parent = element.parentElement;
13
+ if (!parent) return undefined;
14
+ const tagName = element.tagName;
15
+ const siblings = Array.from(parent.children).filter(child => child.tagName === tagName);
16
+
17
+ // Only return position if there are multiple siblings of the same type
18
+ if (siblings.length <= 1) return undefined;
19
+ const index = siblings.indexOf(element);
20
+ return index + 1; // 1-indexed for CSS nth-child compatibility
21
+ }
3
22
  export function collectAncestry(node) {
4
23
  const items = [];
5
24
  let current = node;
@@ -14,6 +33,20 @@ export function collectAncestry(node) {
14
33
  const item = {
15
34
  elementName: current.name
16
35
  };
36
+
37
+ // Extract ID and nth-child from the DOM element if available
38
+ if (isTreeNodeElement(current)) {
39
+ const element = current.getElement();
40
+ if (element instanceof Element) {
41
+ if (element.id) {
42
+ item.id = element.id;
43
+ }
44
+ const nthChild = getNthChildIfAmbiguous(element);
45
+ if (nthChild !== undefined) {
46
+ item.nthChild = nthChild;
47
+ }
48
+ }
49
+ }
17
50
  if (component) {
18
51
  item.componentName = component.label;
19
52
  if (component.callLink) {
@@ -45,9 +78,18 @@ export function formatAncestryChain(items) {
45
78
  reversed.forEach((item, index) => {
46
79
  const indent = " ".repeat(index);
47
80
  const prefix = index === 0 ? "" : "└─ ";
48
- let description = item.elementName;
81
+
82
+ // Build element selector: elementName:nth-child(n)#id
83
+ let selector = item.elementName;
84
+ if (item.nthChild !== undefined) {
85
+ selector += `:nth-child(${item.nthChild})`;
86
+ }
87
+ if (item.id) {
88
+ selector += `#${item.id}`;
89
+ }
90
+ let description = selector;
49
91
  if (item.componentName) {
50
- description = `${item.elementName} in ${item.componentName}`;
92
+ description = `${selector} in ${item.componentName}`;
51
93
  }
52
94
  const location = item.filePath ? ` at ${item.filePath}:${item.line}` : "";
53
95
  lines.push(`${indent}${prefix}${description}${location}`);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,81 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { formatAncestryChain } from "./formatAncestryChain";
3
+ describe("formatAncestryChain", () => {
4
+ it("formats basic ancestry without ID or nth-child", () => {
5
+ const items = [{
6
+ elementName: "button",
7
+ componentName: "Button"
8
+ }, {
9
+ elementName: "div",
10
+ componentName: "App"
11
+ }];
12
+ const result = formatAncestryChain(items);
13
+ expect(result).toBe(`div in App
14
+ └─ button in Button`);
15
+ });
16
+ it("includes ID when present", () => {
17
+ const items = [{
18
+ elementName: "button",
19
+ componentName: "Button",
20
+ id: "submit-btn"
21
+ }, {
22
+ elementName: "div",
23
+ componentName: "App"
24
+ }];
25
+ const result = formatAncestryChain(items);
26
+ expect(result).toBe(`div in App
27
+ └─ button#submit-btn in Button`);
28
+ });
29
+ it("includes nth-child when present", () => {
30
+ const items = [{
31
+ elementName: "li",
32
+ componentName: "ListItem",
33
+ nthChild: 3
34
+ }, {
35
+ elementName: "ul",
36
+ componentName: "List"
37
+ }];
38
+ const result = formatAncestryChain(items);
39
+ expect(result).toBe(`ul in List
40
+ └─ li:nth-child(3) in ListItem`);
41
+ });
42
+ it("includes both nth-child and ID when present", () => {
43
+ const items = [{
44
+ elementName: "li",
45
+ componentName: "ListItem",
46
+ nthChild: 2,
47
+ id: "special-item"
48
+ }, {
49
+ elementName: "ul",
50
+ componentName: "List"
51
+ }];
52
+ const result = formatAncestryChain(items);
53
+ expect(result).toBe(`ul in List
54
+ └─ li:nth-child(2)#special-item in ListItem`);
55
+ });
56
+ it("includes file location when present", () => {
57
+ const items = [{
58
+ elementName: "button",
59
+ componentName: "Button",
60
+ id: "save",
61
+ filePath: "src/Button.tsx",
62
+ line: 42
63
+ }];
64
+ const result = formatAncestryChain(items);
65
+ expect(result).toBe("button#save in Button at src/Button.tsx:42");
66
+ });
67
+ it("formats element without component name", () => {
68
+ const items = [{
69
+ elementName: "div",
70
+ id: "main",
71
+ filePath: "src/App.tsx",
72
+ line: 10
73
+ }];
74
+ const result = formatAncestryChain(items);
75
+ expect(result).toBe("div#main at src/App.tsx:10");
76
+ });
77
+ it("returns empty string for empty items", () => {
78
+ const result = formatAncestryChain([]);
79
+ expect(result).toBe("");
80
+ });
81
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treelocator/runtime",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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",
@@ -21,6 +21,7 @@
21
21
  "e2e"
22
22
  ],
23
23
  "main": "dist/index.js",
24
+ "sideEffects": false,
24
25
  "publishConfig": {
25
26
  "access": "public"
26
27
  },
@@ -71,5 +72,5 @@
71
72
  "directory": "packages/runtime"
72
73
  },
73
74
  "license": "MIT",
74
- "gitHead": "afc8a534284e818665bb4d03b7fa940ec5ad5880"
75
+ "gitHead": "a36be153e103505d6814e0d8b5c3a5cb17c1df0c"
75
76
  }
@@ -0,0 +1,94 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ AncestryItem,
4
+ formatAncestryChain,
5
+ collectAncestry,
6
+ } from "./formatAncestryChain";
7
+
8
+ describe("formatAncestryChain", () => {
9
+ it("formats basic ancestry without ID or nth-child", () => {
10
+ const items: AncestryItem[] = [
11
+ { elementName: "button", componentName: "Button" },
12
+ { elementName: "div", componentName: "App" },
13
+ ];
14
+
15
+ const result = formatAncestryChain(items);
16
+ expect(result).toBe(
17
+ `div in App
18
+ └─ button in Button`
19
+ );
20
+ });
21
+
22
+ it("includes ID when present", () => {
23
+ const items: AncestryItem[] = [
24
+ { elementName: "button", componentName: "Button", id: "submit-btn" },
25
+ { elementName: "div", componentName: "App" },
26
+ ];
27
+
28
+ const result = formatAncestryChain(items);
29
+ expect(result).toBe(
30
+ `div in App
31
+ └─ button#submit-btn in Button`
32
+ );
33
+ });
34
+
35
+ it("includes nth-child when present", () => {
36
+ const items: AncestryItem[] = [
37
+ { elementName: "li", componentName: "ListItem", nthChild: 3 },
38
+ { elementName: "ul", componentName: "List" },
39
+ ];
40
+
41
+ const result = formatAncestryChain(items);
42
+ expect(result).toBe(
43
+ `ul in List
44
+ └─ li:nth-child(3) in ListItem`
45
+ );
46
+ });
47
+
48
+ it("includes both nth-child and ID when present", () => {
49
+ const items: AncestryItem[] = [
50
+ {
51
+ elementName: "li",
52
+ componentName: "ListItem",
53
+ nthChild: 2,
54
+ id: "special-item",
55
+ },
56
+ { elementName: "ul", componentName: "List" },
57
+ ];
58
+
59
+ const result = formatAncestryChain(items);
60
+ expect(result).toBe(
61
+ `ul in List
62
+ └─ li:nth-child(2)#special-item in ListItem`
63
+ );
64
+ });
65
+
66
+ it("includes file location when present", () => {
67
+ const items: AncestryItem[] = [
68
+ {
69
+ elementName: "button",
70
+ componentName: "Button",
71
+ id: "save",
72
+ filePath: "src/Button.tsx",
73
+ line: 42,
74
+ },
75
+ ];
76
+
77
+ const result = formatAncestryChain(items);
78
+ expect(result).toBe("button#save in Button at src/Button.tsx:42");
79
+ });
80
+
81
+ it("formats element without component name", () => {
82
+ const items: AncestryItem[] = [
83
+ { elementName: "div", id: "main", filePath: "src/App.tsx", line: 10 },
84
+ ];
85
+
86
+ const result = formatAncestryChain(items);
87
+ expect(result).toBe("div#main at src/App.tsx:10");
88
+ });
89
+
90
+ it("returns empty string for empty items", () => {
91
+ const result = formatAncestryChain([]);
92
+ expect(result).toBe("");
93
+ });
94
+ });
@@ -1,15 +1,41 @@
1
- import { TreeNode } from "../types/TreeNode";
1
+ import { TreeNode, TreeNodeElement } from "../types/TreeNode";
2
2
 
3
3
  export interface AncestryItem {
4
4
  elementName: string;
5
5
  componentName?: string;
6
6
  filePath?: string;
7
7
  line?: number;
8
+ id?: string;
9
+ nthChild?: number; // 1-indexed, only set when there are ambiguous siblings
8
10
  }
9
11
 
10
12
  // Elements to exclude from ancestry (not useful for debugging)
11
13
  const EXCLUDED_ELEMENTS = new Set(["html", "body", "head"]);
12
14
 
15
+ function isTreeNodeElement(node: TreeNode): node is TreeNodeElement {
16
+ return "getElement" in node && typeof node.getElement === "function";
17
+ }
18
+
19
+ /**
20
+ * Calculate nth-child position if there are multiple siblings of the same type.
21
+ * Returns undefined if the element is unique among its siblings, or 1-indexed position otherwise.
22
+ */
23
+ function getNthChildIfAmbiguous(element: Element): number | undefined {
24
+ const parent = element.parentElement;
25
+ if (!parent) return undefined;
26
+
27
+ const tagName = element.tagName;
28
+ const siblings = Array.from(parent.children).filter(
29
+ (child) => child.tagName === tagName
30
+ );
31
+
32
+ // Only return position if there are multiple siblings of the same type
33
+ if (siblings.length <= 1) return undefined;
34
+
35
+ const index = siblings.indexOf(element);
36
+ return index + 1; // 1-indexed for CSS nth-child compatibility
37
+ }
38
+
13
39
  export function collectAncestry(node: TreeNode): AncestryItem[] {
14
40
  const items: AncestryItem[] = [];
15
41
  let current: TreeNode | null = node;
@@ -28,6 +54,20 @@ export function collectAncestry(node: TreeNode): AncestryItem[] {
28
54
  elementName: current.name,
29
55
  };
30
56
 
57
+ // Extract ID and nth-child from the DOM element if available
58
+ if (isTreeNodeElement(current)) {
59
+ const element = current.getElement();
60
+ if (element instanceof Element) {
61
+ if (element.id) {
62
+ item.id = element.id;
63
+ }
64
+ const nthChild = getNthChildIfAmbiguous(element);
65
+ if (nthChild !== undefined) {
66
+ item.nthChild = nthChild;
67
+ }
68
+ }
69
+ }
70
+
31
71
  if (component) {
32
72
  item.componentName = component.label;
33
73
  if (component.callLink) {
@@ -66,9 +106,18 @@ export function formatAncestryChain(items: AncestryItem[]): string {
66
106
  const indent = " ".repeat(index);
67
107
  const prefix = index === 0 ? "" : "└─ ";
68
108
 
69
- let description = item.elementName;
109
+ // Build element selector: elementName:nth-child(n)#id
110
+ let selector = item.elementName;
111
+ if (item.nthChild !== undefined) {
112
+ selector += `:nth-child(${item.nthChild})`;
113
+ }
114
+ if (item.id) {
115
+ selector += `#${item.id}`;
116
+ }
117
+
118
+ let description = selector;
70
119
  if (item.componentName) {
71
- description = `${item.elementName} in ${item.componentName}`;
120
+ description = `${selector} in ${item.componentName}`;
72
121
  }
73
122
 
74
123
  const location = item.filePath ? ` at ${item.filePath}:${item.line}` : "";