@treelocator/runtime
Advanced tools
@@ -23,4 +23,5 @@ import { TreeNode } from "../types/TreeNode"; | ||
| * Truncate ancestry to keep only the local context. | ||
| * - If the clicked element has no filePath: keep up to the first ancestor with a file. | ||
| * - If the clicked element has a filePath: keep up to the first ancestor with a DIFFERENT file. | ||
| * - If the clicked element has no file: keep up to the first ancestor with a file. | ||
| * - If the clicked element has a file: keep up to the first ancestor with a DIFFERENT file. | ||
| * Checks both client filePath and serverComponents for file info. | ||
| * The ancestry array is bottom-up: index 0 = clicked element, last = root. | ||
@@ -27,0 +28,0 @@ */ |
@@ -126,5 +126,20 @@ import { parsePhoenixServerComponents } from "../adapters/phoenix/parsePhoenixComments"; | ||
| /** | ||
| * Get the effective file path from an AncestryItem, checking both | ||
| * client filePath and serverComponents (Next.js RSC, Phoenix, etc.). | ||
| */ | ||
| function getItemFilePath(item) { | ||
| if (item.filePath) return item.filePath; | ||
| if (item.serverComponents && item.serverComponents.length > 0) { | ||
| const comp = item.serverComponents.find(sc => sc.type === "component"); | ||
| if (comp) return comp.filePath; | ||
| return item.serverComponents[0].filePath; | ||
| } | ||
| return undefined; | ||
| } | ||
| /** | ||
| * Truncate ancestry to keep only the local context. | ||
| * - If the clicked element has no filePath: keep up to the first ancestor with a file. | ||
| * - If the clicked element has a filePath: keep up to the first ancestor with a DIFFERENT file. | ||
| * - If the clicked element has no file: keep up to the first ancestor with a file. | ||
| * - If the clicked element has a file: keep up to the first ancestor with a DIFFERENT file. | ||
| * Checks both client filePath and serverComponents for file info. | ||
| * The ancestry array is bottom-up: index 0 = clicked element, last = root. | ||
@@ -134,6 +149,6 @@ */ | ||
| if (items.length === 0) return items; | ||
| const clickedFile = items[0]?.filePath; | ||
| const clickedFile = getItemFilePath(items[0]); | ||
| if (!clickedFile) { | ||
| // Clicked element has no file: find first ancestor with any file | ||
| const firstWithFile = items.findIndex(item => item.filePath); | ||
| const firstWithFile = items.findIndex(item => getItemFilePath(item)); | ||
| if (firstWithFile === -1) return items; | ||
@@ -145,3 +160,4 @@ return items.slice(0, firstWithFile + 1); | ||
| for (let i = 1; i < items.length; i++) { | ||
| if (items[i].filePath && items[i].filePath !== clickedFile) { | ||
| const ancestorFile = getItemFilePath(items[i]); | ||
| if (ancestorFile && ancestorFile !== clickedFile) { | ||
| return items.slice(0, i + 1); | ||
@@ -148,0 +164,0 @@ } |
@@ -370,2 +370,60 @@ import { describe, it, expect } from "vitest"; | ||
| }); | ||
| it("uses serverComponents file path when filePath is missing", () => { | ||
| const items = [{ | ||
| elementName: "div", | ||
| componentName: "TurnActivityBox", | ||
| serverComponents: [{ | ||
| name: "TurnActivityBox", | ||
| filePath: "components/MessageRow.tsx", | ||
| line: 921, | ||
| type: "component" | ||
| }] | ||
| }, { | ||
| elementName: "div", | ||
| componentName: "MessageRow", | ||
| serverComponents: [{ | ||
| name: "MessageRow", | ||
| filePath: "components/chat/ChatViewport.tsx", | ||
| line: 917, | ||
| type: "component" | ||
| }] | ||
| }, { | ||
| elementName: "main", | ||
| componentName: "Home", | ||
| serverComponents: [{ | ||
| name: "Home", | ||
| filePath: "app/page.tsx", | ||
| line: 817, | ||
| type: "component" | ||
| }] | ||
| }]; | ||
| const result = truncateAtFirstFile(items); | ||
| expect(result).toEqual([items[0], items[1]]); | ||
| }); | ||
| it("uses serverComponents when clicked element has no filePath but has serverComponents", () => { | ||
| const items = [{ | ||
| elementName: "span", | ||
| componentName: "Button" | ||
| }, { | ||
| elementName: "div", | ||
| componentName: "Card", | ||
| serverComponents: [{ | ||
| name: "Card", | ||
| filePath: "src/Card.tsx", | ||
| line: 10, | ||
| type: "component" | ||
| }] | ||
| }, { | ||
| elementName: "div", | ||
| componentName: "App", | ||
| serverComponents: [{ | ||
| name: "App", | ||
| filePath: "src/App.tsx", | ||
| line: 1, | ||
| type: "component" | ||
| }] | ||
| }]; | ||
| const result = truncateAtFirstFile(items); | ||
| expect(result).toEqual([items[0], items[1]]); | ||
| }); | ||
| it("returns empty array for empty input", () => { | ||
@@ -372,0 +430,0 @@ expect(truncateAtFirstFile([])).toEqual([]); |
+1
-1
| { | ||
| "name": "@treelocator/runtime", | ||
| "version": "0.4.6", | ||
| "version": "0.4.7", | ||
| "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,0 +5,0 @@ "keywords": [ |
@@ -339,2 +339,30 @@ import { describe, it, expect } from "vitest"; | ||
| it("uses serverComponents file path when filePath is missing", () => { | ||
| const items: AncestryItem[] = [ | ||
| { elementName: "div", componentName: "TurnActivityBox", serverComponents: [{ name: "TurnActivityBox", filePath: "components/MessageRow.tsx", line: 921, type: "component" }] }, | ||
| { elementName: "div", componentName: "MessageRow", serverComponents: [{ name: "MessageRow", filePath: "components/chat/ChatViewport.tsx", line: 917, type: "component" }] }, | ||
| { elementName: "main", componentName: "Home", serverComponents: [{ name: "Home", filePath: "app/page.tsx", line: 817, type: "component" }] }, | ||
| ]; | ||
| const result = truncateAtFirstFile(items); | ||
| expect(result).toEqual([ | ||
| items[0], | ||
| items[1], | ||
| ]); | ||
| }); | ||
| it("uses serverComponents when clicked element has no filePath but has serverComponents", () => { | ||
| const items: AncestryItem[] = [ | ||
| { elementName: "span", componentName: "Button" }, | ||
| { elementName: "div", componentName: "Card", serverComponents: [{ name: "Card", filePath: "src/Card.tsx", line: 10, type: "component" }] }, | ||
| { elementName: "div", componentName: "App", serverComponents: [{ name: "App", filePath: "src/App.tsx", line: 1, type: "component" }] }, | ||
| ]; | ||
| const result = truncateAtFirstFile(items); | ||
| expect(result).toEqual([ | ||
| items[0], | ||
| items[1], | ||
| ]); | ||
| }); | ||
| it("returns empty array for empty input", () => { | ||
@@ -341,0 +369,0 @@ expect(truncateAtFirstFile([])).toEqual([]); |
@@ -168,5 +168,20 @@ import { TreeNode, TreeNodeComponent, TreeNodeElement } from "../types/TreeNode"; | ||
| /** | ||
| * Get the effective file path from an AncestryItem, checking both | ||
| * client filePath and serverComponents (Next.js RSC, Phoenix, etc.). | ||
| */ | ||
| function getItemFilePath(item: AncestryItem): string | undefined { | ||
| if (item.filePath) return item.filePath; | ||
| if (item.serverComponents && item.serverComponents.length > 0) { | ||
| const comp = item.serverComponents.find((sc) => sc.type === "component"); | ||
| if (comp) return comp.filePath; | ||
| return item.serverComponents[0]!.filePath; | ||
| } | ||
| return undefined; | ||
| } | ||
| /** | ||
| * Truncate ancestry to keep only the local context. | ||
| * - If the clicked element has no filePath: keep up to the first ancestor with a file. | ||
| * - If the clicked element has a filePath: keep up to the first ancestor with a DIFFERENT file. | ||
| * - If the clicked element has no file: keep up to the first ancestor with a file. | ||
| * - If the clicked element has a file: keep up to the first ancestor with a DIFFERENT file. | ||
| * Checks both client filePath and serverComponents for file info. | ||
| * The ancestry array is bottom-up: index 0 = clicked element, last = root. | ||
@@ -177,7 +192,7 @@ */ | ||
| const clickedFile = items[0]?.filePath; | ||
| const clickedFile = getItemFilePath(items[0]!); | ||
| if (!clickedFile) { | ||
| // Clicked element has no file: find first ancestor with any file | ||
| const firstWithFile = items.findIndex((item) => item.filePath); | ||
| const firstWithFile = items.findIndex((item) => getItemFilePath(item)); | ||
| if (firstWithFile === -1) return items; | ||
@@ -189,3 +204,4 @@ return items.slice(0, firstWithFile + 1); | ||
| for (let i = 1; i < items.length; i++) { | ||
| if (items[i]!.filePath && items[i]!.filePath !== clickedFile) { | ||
| const ancestorFile = getItemFilePath(items[i]!); | ||
| if (ancestorFile && ancestorFile !== clickedFile) { | ||
| return items.slice(0, i + 1); | ||
@@ -192,0 +208,0 @@ } |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
798305
0.55%17556
0.65%