@treelocator/runtime 0.1.3 → 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.
@@ -1,30 +1,30 @@
1
1
 
2
- > @treelocator/runtime@0.1.3 build /Users/wende/projects/locatorjs/packages/runtime
2
+ > @treelocator/runtime@0.1.4 build /Users/wende/projects/locatorjs/packages/runtime
3
3
  > concurrently pnpm:build:*
4
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]
5
9
  [tailwind]
6
- [tailwind] > @treelocator/runtime@0.1.3 build:tailwind /Users/wende/projects/locatorjs/packages/runtime
10
+ [tailwind] > @treelocator/runtime@0.1.4 build:tailwind /Users/wende/projects/locatorjs/packages/runtime
7
11
  [tailwind] > tailwindcss -i ./src/main.css -o ./dist/output.css
8
12
  [tailwind]
9
- [babel]
10
- [babel] > @treelocator/runtime@0.1.3 build:babel /Users/wende/projects/locatorjs/packages/runtime
11
- [babel] > babel src --out-dir dist --extensions .js,.jsx,.ts,.tsx
12
- [babel]
13
13
  [ts]
14
- [ts] > @treelocator/runtime@0.1.3 build:ts /Users/wende/projects/locatorjs/packages/runtime
14
+ [ts] > @treelocator/runtime@0.1.4 build:ts /Users/wende/projects/locatorjs/packages/runtime
15
15
  [ts] > tsc --declaration --emitDeclarationOnly --noEmit false --outDir dist
16
16
  [ts]
17
- [wrapImage]
18
- [wrapImage] > @treelocator/runtime@0.1.3 build:wrapImage /Users/wende/projects/locatorjs/packages/runtime
19
- [wrapImage] > node ./scripts/wrapImage.js
20
- [wrapImage]
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
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 171ms.
26
+ [tailwind] Done in 160ms.
27
27
  [tailwind] pnpm run build:tailwind exited with code 0
28
- [babel] Successfully compiled 71 files with Babel (446ms).
28
+ [babel] Successfully compiled 72 files with Babel (416ms).
29
29
  [babel] pnpm run build:babel exited with code 0
30
30
  [ts] pnpm run build:ts exited with code 0
@@ -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.3",
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",
@@ -72,5 +72,5 @@
72
72
  "directory": "packages/runtime"
73
73
  },
74
74
  "license": "MIT",
75
- "gitHead": "17ecb45a2605c14823ac3436b10ac1a3ab9bc62f"
75
+ "gitHead": "a36be153e103505d6814e0d8b5c3a5cb17c1df0c"
76
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}` : "";