@treelocator/runtime 0.1.8 → 0.3.0
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/adapters/createTreeNode.js +41 -4
- package/dist/adapters/nextjs/parseNextjsDataAttributes.d.ts +31 -0
- package/dist/adapters/nextjs/parseNextjsDataAttributes.js +106 -0
- package/dist/adapters/phoenix/__tests__/parsePhoenixComments.test.d.ts +4 -0
- package/dist/adapters/phoenix/__tests__/parsePhoenixComments.test.js +218 -0
- package/dist/adapters/phoenix/detectPhoenix.d.ts +11 -0
- package/dist/adapters/phoenix/detectPhoenix.js +38 -0
- package/dist/adapters/phoenix/index.d.ts +10 -0
- package/dist/adapters/phoenix/index.js +9 -0
- package/dist/adapters/phoenix/parsePhoenixComments.d.ts +35 -0
- package/dist/adapters/phoenix/parsePhoenixComments.js +131 -0
- package/dist/adapters/phoenix/types.d.ts +16 -0
- package/dist/adapters/phoenix/types.js +1 -0
- package/dist/adapters/react/findDebugSource.d.ts +13 -0
- package/dist/adapters/react/findDebugSource.js +37 -0
- package/dist/adapters/react/findFiberByHtmlElement.js +23 -1
- package/dist/adapters/react/getFiberLabel.js +2 -1
- package/dist/adapters/react/resolveSourceMap.d.ts +29 -0
- package/dist/adapters/react/resolveSourceMap.js +236 -0
- package/dist/browserApi.d.ts +4 -4
- package/dist/browserApi.js +13 -15
- package/dist/components/MaybeOutline.js +65 -3
- package/dist/components/Runtime.js +13 -0
- package/dist/functions/enrichAncestrySourceMaps.d.ts +7 -0
- package/dist/functions/enrichAncestrySourceMaps.js +80 -0
- package/dist/functions/formatAncestryChain.d.ts +3 -0
- package/dist/functions/formatAncestryChain.js +104 -15
- package/dist/functions/formatAncestryChain.test.js +26 -20
- package/dist/functions/normalizeFilePath.d.ts +14 -0
- package/dist/functions/normalizeFilePath.js +40 -0
- package/dist/output.css +87 -15
- package/dist/types/ServerComponentInfo.d.ts +14 -0
- package/dist/types/ServerComponentInfo.js +1 -0
- package/package.json +4 -3
- package/src/adapters/createTreeNode.ts +44 -3
- package/src/adapters/nextjs/parseNextjsDataAttributes.ts +112 -0
- package/src/adapters/phoenix/__tests__/parsePhoenixComments.test.ts +264 -0
- package/src/adapters/phoenix/detectPhoenix.ts +44 -0
- package/src/adapters/phoenix/index.ts +11 -0
- package/src/adapters/phoenix/parsePhoenixComments.ts +140 -0
- package/src/adapters/phoenix/types.ts +16 -0
- package/src/adapters/react/findDebugSource.ts +40 -0
- package/src/adapters/react/findFiberByHtmlElement.ts +26 -1
- package/src/adapters/react/getFiberLabel.ts +2 -1
- package/src/adapters/react/reactAdapter.ts +2 -1
- package/src/adapters/react/resolveSourceMap.ts +316 -0
- package/src/browserApi.ts +27 -25
- package/src/components/MaybeOutline.tsx +63 -4
- package/src/components/Runtime.tsx +15 -0
- package/src/functions/enrichAncestrySourceMaps.ts +103 -0
- package/src/functions/formatAncestryChain.test.ts +26 -20
- package/src/functions/formatAncestryChain.ts +121 -15
- package/src/functions/normalizeFilePath.ts +41 -0
- package/src/types/ServerComponentInfo.ts +14 -0
- package/.turbo/turbo-build.log +0 -30
- package/.turbo/turbo-test.log +0 -19
- package/.turbo/turbo-ts.log +0 -4
- package/LICENSE +0 -22
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { AncestryItem } from "./formatAncestryChain";
|
|
2
|
+
import { resolveSourceLocation, parseDebugStack } from "../adapters/react/resolveSourceMap";
|
|
3
|
+
import { normalizeFilePath } from "./normalizeFilePath";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Check if any DOM element has React 19 fibers (with _debugStack instead of _debugSource).
|
|
7
|
+
*/
|
|
8
|
+
function isReact19Environment(): boolean {
|
|
9
|
+
const el = document.querySelector("[class]") || document.body;
|
|
10
|
+
if (!el) return false;
|
|
11
|
+
|
|
12
|
+
const fiberKey = Object.keys(el).find((k) => k.startsWith("__reactFiber$"));
|
|
13
|
+
if (!fiberKey) return false;
|
|
14
|
+
|
|
15
|
+
const fiber = (el as any)[fiberKey];
|
|
16
|
+
return !fiber?._debugSource && !!(fiber as any)?._debugStack;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Walk a fiber's _debugOwner chain and collect _debugStack stack traces for each component.
|
|
21
|
+
* Returns a map from component name to its parsed stack location.
|
|
22
|
+
*/
|
|
23
|
+
function collectFiberStacks(
|
|
24
|
+
element: HTMLElement
|
|
25
|
+
): Map<string, { url: string; line: number; column: number }> {
|
|
26
|
+
const stacks = new Map<string, { url: string; line: number; column: number }>();
|
|
27
|
+
|
|
28
|
+
const fiberKey = Object.keys(element).find((k) =>
|
|
29
|
+
k.startsWith("__reactFiber$")
|
|
30
|
+
);
|
|
31
|
+
if (!fiberKey) return stacks;
|
|
32
|
+
|
|
33
|
+
let fiber = (element as any)[fiberKey];
|
|
34
|
+
|
|
35
|
+
// Collect stacks from the fiber itself and its _debugOwner chain
|
|
36
|
+
while (fiber) {
|
|
37
|
+
const debugStack = fiber._debugStack;
|
|
38
|
+
if (debugStack?.stack) {
|
|
39
|
+
const parsed = parseDebugStack(debugStack.stack);
|
|
40
|
+
if (parsed) {
|
|
41
|
+
const name =
|
|
42
|
+
fiber.type?.name || fiber.type?.displayName || fiber.type;
|
|
43
|
+
if (typeof name === "string") {
|
|
44
|
+
stacks.set(name, parsed);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
fiber = fiber._debugOwner || null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return stacks;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Enrich ancestry items that are missing filePath by resolving via source maps.
|
|
56
|
+
* This is an async operation that fetches source maps for React 19 environments.
|
|
57
|
+
* For React 18 (where _debugSource exists), this is a no-op.
|
|
58
|
+
*/
|
|
59
|
+
export async function enrichAncestryWithSourceMaps(
|
|
60
|
+
items: AncestryItem[],
|
|
61
|
+
element?: HTMLElement
|
|
62
|
+
): Promise<AncestryItem[]> {
|
|
63
|
+
// Skip if all items already have file paths, or not React 19
|
|
64
|
+
const needsEnrichment = items.some((item) => item.componentName && !item.filePath);
|
|
65
|
+
if (!needsEnrichment || !isReact19Environment()) {
|
|
66
|
+
return items;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Collect _debugStack info from the DOM element's fiber chain
|
|
70
|
+
const stacks = element ? collectFiberStacks(element) : new Map();
|
|
71
|
+
|
|
72
|
+
// Resolve source maps in parallel for items missing filePath
|
|
73
|
+
const enriched = await Promise.all(
|
|
74
|
+
items.map(async (item) => {
|
|
75
|
+
if (item.filePath || !item.componentName) return item;
|
|
76
|
+
|
|
77
|
+
// Find the stack trace for this component
|
|
78
|
+
const stack = stacks.get(item.componentName);
|
|
79
|
+
if (!stack) return item;
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const source = await resolveSourceLocation(
|
|
83
|
+
stack.url,
|
|
84
|
+
stack.line,
|
|
85
|
+
stack.column
|
|
86
|
+
);
|
|
87
|
+
if (source) {
|
|
88
|
+
return {
|
|
89
|
+
...item,
|
|
90
|
+
filePath: normalizeFilePath(source.fileName),
|
|
91
|
+
line: source.lineNumber,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
// Source map resolution failed — keep item as-is
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return item;
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
return enriched;
|
|
103
|
+
}
|
|
@@ -6,46 +6,49 @@ import {
|
|
|
6
6
|
} from "./formatAncestryChain";
|
|
7
7
|
|
|
8
8
|
describe("formatAncestryChain", () => {
|
|
9
|
-
it("
|
|
9
|
+
it("uses component name only at component boundaries", () => {
|
|
10
10
|
const items: AncestryItem[] = [
|
|
11
11
|
{ elementName: "button", componentName: "Button" },
|
|
12
12
|
{ elementName: "div", componentName: "App" },
|
|
13
13
|
];
|
|
14
14
|
|
|
15
15
|
const result = formatAncestryChain(items);
|
|
16
|
+
// App is root (component boundary), Button is different component (boundary)
|
|
16
17
|
expect(result).toBe(
|
|
17
|
-
`
|
|
18
|
-
└─
|
|
18
|
+
`App
|
|
19
|
+
└─ Button`
|
|
19
20
|
);
|
|
20
21
|
});
|
|
21
22
|
|
|
22
|
-
it("
|
|
23
|
+
it("shows element name when same component as parent", () => {
|
|
23
24
|
const items: AncestryItem[] = [
|
|
24
|
-
{ elementName: "button", componentName: "
|
|
25
|
+
{ elementName: "button", componentName: "App", id: "submit-btn" },
|
|
25
26
|
{ elementName: "div", componentName: "App" },
|
|
26
27
|
];
|
|
27
28
|
|
|
28
29
|
const result = formatAncestryChain(items);
|
|
30
|
+
// Both are in App, so second item shows element name not component name
|
|
29
31
|
expect(result).toBe(
|
|
30
|
-
`
|
|
31
|
-
└─ button#submit-btn
|
|
32
|
+
`App
|
|
33
|
+
└─ button#submit-btn`
|
|
32
34
|
);
|
|
33
35
|
});
|
|
34
36
|
|
|
35
|
-
it("includes nth-child
|
|
37
|
+
it("includes nth-child with component name at boundary", () => {
|
|
36
38
|
const items: AncestryItem[] = [
|
|
37
39
|
{ elementName: "li", componentName: "ListItem", nthChild: 3 },
|
|
38
40
|
{ elementName: "ul", componentName: "List" },
|
|
39
41
|
];
|
|
40
42
|
|
|
41
43
|
const result = formatAncestryChain(items);
|
|
44
|
+
// Different components = boundaries
|
|
42
45
|
expect(result).toBe(
|
|
43
|
-
`
|
|
44
|
-
└─
|
|
46
|
+
`List
|
|
47
|
+
└─ ListItem:nth-child(3)`
|
|
45
48
|
);
|
|
46
49
|
});
|
|
47
50
|
|
|
48
|
-
it("includes both nth-child and ID
|
|
51
|
+
it("includes both nth-child and ID at component boundary", () => {
|
|
49
52
|
const items: AncestryItem[] = [
|
|
50
53
|
{
|
|
51
54
|
elementName: "li",
|
|
@@ -58,12 +61,12 @@ describe("formatAncestryChain", () => {
|
|
|
58
61
|
|
|
59
62
|
const result = formatAncestryChain(items);
|
|
60
63
|
expect(result).toBe(
|
|
61
|
-
`
|
|
62
|
-
└─
|
|
64
|
+
`List
|
|
65
|
+
└─ ListItem:nth-child(2)#special-item`
|
|
63
66
|
);
|
|
64
67
|
});
|
|
65
68
|
|
|
66
|
-
it("includes file location
|
|
69
|
+
it("includes file location at component boundary", () => {
|
|
67
70
|
const items: AncestryItem[] = [
|
|
68
71
|
{
|
|
69
72
|
elementName: "button",
|
|
@@ -75,7 +78,8 @@ describe("formatAncestryChain", () => {
|
|
|
75
78
|
];
|
|
76
79
|
|
|
77
80
|
const result = formatAncestryChain(items);
|
|
78
|
-
|
|
81
|
+
// First item is always a boundary (no previous item)
|
|
82
|
+
expect(result).toBe("Button#save at src/Button.tsx:42");
|
|
79
83
|
});
|
|
80
84
|
|
|
81
85
|
it("formats element without component name", () => {
|
|
@@ -92,7 +96,7 @@ describe("formatAncestryChain", () => {
|
|
|
92
96
|
expect(result).toBe("");
|
|
93
97
|
});
|
|
94
98
|
|
|
95
|
-
it("
|
|
99
|
+
it("uses innermost component as display name with outer components in chain", () => {
|
|
96
100
|
const items: AncestryItem[] = [
|
|
97
101
|
{
|
|
98
102
|
elementName: "div",
|
|
@@ -117,13 +121,14 @@ describe("formatAncestryChain", () => {
|
|
|
117
121
|
];
|
|
118
122
|
|
|
119
123
|
const result = formatAncestryChain(items);
|
|
124
|
+
// GlassPanel (innermost) is the display name, Sidebar (outer) shown in "in"
|
|
120
125
|
expect(result).toBe(
|
|
121
|
-
`
|
|
122
|
-
└─
|
|
126
|
+
`App at src/App.jsx:104
|
|
127
|
+
└─ GlassPanel#sidebar-panel in Sidebar at src/components/game/Sidebar.jsx:78`
|
|
123
128
|
);
|
|
124
129
|
});
|
|
125
130
|
|
|
126
|
-
it("
|
|
131
|
+
it("uses component name as display name when only one in chain", () => {
|
|
127
132
|
const items: AncestryItem[] = [
|
|
128
133
|
{
|
|
129
134
|
elementName: "button",
|
|
@@ -135,6 +140,7 @@ describe("formatAncestryChain", () => {
|
|
|
135
140
|
];
|
|
136
141
|
|
|
137
142
|
const result = formatAncestryChain(items);
|
|
138
|
-
|
|
143
|
+
// Single component becomes the display name, no "in X" needed
|
|
144
|
+
expect(result).toBe("Button at src/Button.tsx:10");
|
|
139
145
|
});
|
|
140
146
|
});
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { TreeNode, TreeNodeComponent, TreeNodeElement } from "../types/TreeNode";
|
|
2
|
+
import { ServerComponentInfo } from "../types/ServerComponentInfo";
|
|
3
|
+
import { parsePhoenixServerComponents } from "../adapters/phoenix/parsePhoenixComments";
|
|
4
|
+
import { parseNextjsServerComponents } from "../adapters/nextjs/parseNextjsDataAttributes";
|
|
5
|
+
import { normalizeFilePath } from "./normalizeFilePath";
|
|
2
6
|
|
|
3
7
|
export interface OwnerComponentInfo {
|
|
4
8
|
name: string;
|
|
@@ -15,6 +19,8 @@ export interface AncestryItem {
|
|
|
15
19
|
nthChild?: number; // 1-indexed, only set when there are ambiguous siblings
|
|
16
20
|
/** All owner components from outermost (Sidebar) to innermost (GlassPanel) */
|
|
17
21
|
ownerComponents?: OwnerComponentInfo[];
|
|
22
|
+
/** Server-side components (Phoenix LiveView, Rails, Next.js RSC, etc.) */
|
|
23
|
+
serverComponents?: ServerComponentInfo[];
|
|
18
24
|
}
|
|
19
25
|
|
|
20
26
|
// Elements to exclude from ancestry (not useful for debugging)
|
|
@@ -49,7 +55,7 @@ function treeNodeComponentToOwnerInfo(
|
|
|
49
55
|
): OwnerComponentInfo {
|
|
50
56
|
return {
|
|
51
57
|
name: comp.label,
|
|
52
|
-
filePath: comp.callLink?.fileName,
|
|
58
|
+
filePath: comp.callLink?.fileName ? normalizeFilePath(comp.callLink.fileName) : undefined,
|
|
53
59
|
line: comp.callLink?.lineNumber,
|
|
54
60
|
};
|
|
55
61
|
}
|
|
@@ -82,6 +88,23 @@ export function collectAncestry(node: TreeNode): AncestryItem[] {
|
|
|
82
88
|
if (nthChild !== undefined) {
|
|
83
89
|
item.nthChild = nthChild;
|
|
84
90
|
}
|
|
91
|
+
|
|
92
|
+
// Parse server components from various sources
|
|
93
|
+
// 1. Phoenix LiveView (HTML comments)
|
|
94
|
+
const phoenixComponents = parsePhoenixServerComponents(element);
|
|
95
|
+
|
|
96
|
+
// 2. Next.js Server Components (data-locatorjs attributes)
|
|
97
|
+
const nextjsComponents = parseNextjsServerComponents(element);
|
|
98
|
+
|
|
99
|
+
// Combine all server components
|
|
100
|
+
const allServerComponents = [
|
|
101
|
+
...(phoenixComponents || []),
|
|
102
|
+
...(nextjsComponents || []),
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
if (allServerComponents.length > 0) {
|
|
106
|
+
item.serverComponents = allServerComponents;
|
|
107
|
+
}
|
|
85
108
|
}
|
|
86
109
|
}
|
|
87
110
|
|
|
@@ -93,7 +116,7 @@ export function collectAncestry(node: TreeNode): AncestryItem[] {
|
|
|
93
116
|
// Use outermost component as the primary component name
|
|
94
117
|
item.componentName = outermost.label;
|
|
95
118
|
if (outermost.callLink) {
|
|
96
|
-
item.filePath = outermost.callLink.fileName;
|
|
119
|
+
item.filePath = normalizeFilePath(outermost.callLink.fileName);
|
|
97
120
|
item.line = outermost.callLink.lineNumber;
|
|
98
121
|
}
|
|
99
122
|
} else {
|
|
@@ -102,19 +125,19 @@ export function collectAncestry(node: TreeNode): AncestryItem[] {
|
|
|
102
125
|
if (component) {
|
|
103
126
|
item.componentName = component.label;
|
|
104
127
|
if (component.callLink) {
|
|
105
|
-
item.filePath = component.callLink.fileName;
|
|
128
|
+
item.filePath = normalizeFilePath(component.callLink.fileName);
|
|
106
129
|
item.line = component.callLink.lineNumber;
|
|
107
130
|
}
|
|
108
131
|
}
|
|
109
132
|
}
|
|
110
133
|
|
|
111
134
|
if (!item.filePath && source) {
|
|
112
|
-
item.filePath = source.fileName;
|
|
135
|
+
item.filePath = normalizeFilePath(source.fileName);
|
|
113
136
|
item.line = source.lineNumber;
|
|
114
137
|
}
|
|
115
138
|
|
|
116
|
-
// Only include items that have useful info (component name or
|
|
117
|
-
if (item.componentName || item.filePath) {
|
|
139
|
+
// Only include items that have useful info (component name, file path, or server components)
|
|
140
|
+
if (item.componentName || item.filePath || item.serverComponents) {
|
|
118
141
|
items.push(item);
|
|
119
142
|
}
|
|
120
143
|
|
|
@@ -138,8 +161,36 @@ export function formatAncestryChain(items: AncestryItem[]): string {
|
|
|
138
161
|
const indent = " ".repeat(index);
|
|
139
162
|
const prefix = index === 0 ? "" : "└─ ";
|
|
140
163
|
|
|
141
|
-
//
|
|
142
|
-
|
|
164
|
+
// Get the previous item's component to detect component boundaries
|
|
165
|
+
const prevItem = index > 0 ? reversed[index - 1] : null;
|
|
166
|
+
const prevComponentName = prevItem?.componentName || prevItem?.ownerComponents?.[prevItem.ownerComponents.length - 1]?.name;
|
|
167
|
+
|
|
168
|
+
// Get current item's innermost component
|
|
169
|
+
const currentComponentName = item.ownerComponents?.[item.ownerComponents.length - 1]?.name || item.componentName;
|
|
170
|
+
|
|
171
|
+
// Determine the display name for the element
|
|
172
|
+
// Use component name ONLY when crossing a component boundary (root element of a component)
|
|
173
|
+
// This prevents "App -> App:nth-child(5)" when both are just elements inside App
|
|
174
|
+
let displayName = item.elementName;
|
|
175
|
+
let outerComponents: string[] = [];
|
|
176
|
+
const isComponentBoundary = currentComponentName && currentComponentName !== prevComponentName;
|
|
177
|
+
|
|
178
|
+
if (isComponentBoundary) {
|
|
179
|
+
if (item.ownerComponents && item.ownerComponents.length > 0) {
|
|
180
|
+
// Use innermost component as display name, show outer ones in "in X > Y"
|
|
181
|
+
const innermost = item.ownerComponents[item.ownerComponents.length - 1];
|
|
182
|
+
if (innermost) {
|
|
183
|
+
displayName = innermost.name;
|
|
184
|
+
// Outer components (excluding innermost)
|
|
185
|
+
outerComponents = item.ownerComponents.slice(0, -1).map((c) => c.name);
|
|
186
|
+
}
|
|
187
|
+
} else if (item.componentName) {
|
|
188
|
+
displayName = item.componentName;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Build element selector: displayName:nth-child(n)#id
|
|
193
|
+
let selector = displayName;
|
|
143
194
|
if (item.nthChild !== undefined) {
|
|
144
195
|
selector += `:nth-child(${item.nthChild})`;
|
|
145
196
|
}
|
|
@@ -149,15 +200,70 @@ export function formatAncestryChain(items: AncestryItem[]): string {
|
|
|
149
200
|
|
|
150
201
|
let description = selector;
|
|
151
202
|
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
203
|
+
// Build component description parts
|
|
204
|
+
const parts: string[] = [];
|
|
205
|
+
|
|
206
|
+
// Server components (Phoenix/Next.js/Rails/etc.)
|
|
207
|
+
if (item.serverComponents && item.serverComponents.length > 0) {
|
|
208
|
+
// Group server components by framework (detected by file extension)
|
|
209
|
+
const phoenixComponents = item.serverComponents.filter((sc) =>
|
|
210
|
+
sc.filePath.match(/\.(ex|exs|heex)$/)
|
|
211
|
+
);
|
|
212
|
+
const nextjsComponents = item.serverComponents.filter((sc) =>
|
|
213
|
+
sc.filePath.match(/\.(tsx?|jsx?)$/)
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
// Format Phoenix components
|
|
217
|
+
if (phoenixComponents.length > 0) {
|
|
218
|
+
const names = phoenixComponents
|
|
219
|
+
.filter((sc) => sc.type === "component")
|
|
220
|
+
.map((sc) => sc.name);
|
|
221
|
+
if (names.length > 0) {
|
|
222
|
+
parts.push(`[Phoenix: ${names.join(" > ")}]`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Format Next.js components
|
|
227
|
+
if (nextjsComponents.length > 0) {
|
|
228
|
+
const names = nextjsComponents
|
|
229
|
+
.filter((sc) => sc.type === "component")
|
|
230
|
+
.map((sc) => sc.name);
|
|
231
|
+
if (names.length > 0) {
|
|
232
|
+
parts.push(`[Next.js: ${names.join(" > ")}]`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Client components - show outer components for context (if any)
|
|
238
|
+
if (outerComponents.length > 0) {
|
|
239
|
+
parts.push(`in ${outerComponents.join(" > ")}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (parts.length > 0) {
|
|
243
|
+
description = `${selector} ${parts.join(" ")}`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Build location string
|
|
247
|
+
const locationParts: string[] = [];
|
|
248
|
+
|
|
249
|
+
// Server component locations
|
|
250
|
+
if (item.serverComponents && item.serverComponents.length > 0) {
|
|
251
|
+
item.serverComponents.forEach((sc) => {
|
|
252
|
+
const prefix = sc.type === "caller" ? " (called from)" : "";
|
|
253
|
+
locationParts.push(`${sc.filePath}:${sc.line}${prefix}`);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Client component location (only add if different from server components)
|
|
258
|
+
if (item.filePath) {
|
|
259
|
+
const clientLocation = `${item.filePath}:${item.line}`;
|
|
260
|
+
// Only add if this location isn't already in the list
|
|
261
|
+
if (!locationParts.includes(clientLocation)) {
|
|
262
|
+
locationParts.push(clientLocation);
|
|
263
|
+
}
|
|
158
264
|
}
|
|
159
265
|
|
|
160
|
-
const location =
|
|
266
|
+
const location = locationParts.length > 0 ? ` at ${locationParts.join(", ")}` : "";
|
|
161
267
|
|
|
162
268
|
lines.push(`${indent}${prefix}${description}${location}`);
|
|
163
269
|
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert absolute file paths to relative paths for display.
|
|
3
|
+
*
|
|
4
|
+
* This ensures consistent path display across different sources:
|
|
5
|
+
* - React _debugSource may provide absolute paths
|
|
6
|
+
* - Next.js data-locatorjs attributes may have absolute paths
|
|
7
|
+
* - Phoenix comments use relative paths
|
|
8
|
+
*
|
|
9
|
+
* Examples:
|
|
10
|
+
* - "/Users/name/project/src/App.tsx" → "src/App.tsx"
|
|
11
|
+
* - "/workspace/apps/next-16/app/page.tsx" → "app/page.tsx"
|
|
12
|
+
* - "src/components/Button.tsx" → "src/components/Button.tsx" (unchanged)
|
|
13
|
+
*/
|
|
14
|
+
export function normalizeFilePath(filePath: string): string {
|
|
15
|
+
// If it's already relative (doesn't start with /), return as-is
|
|
16
|
+
if (!filePath.startsWith("/")) {
|
|
17
|
+
return filePath;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Find common project indicators to trim the path
|
|
21
|
+
const indicators = ["/app/", "/src/", "/pages/", "/components/", "/lib/"];
|
|
22
|
+
|
|
23
|
+
for (const indicator of indicators) {
|
|
24
|
+
const index = filePath.indexOf(indicator);
|
|
25
|
+
if (index !== -1) {
|
|
26
|
+
// Return from the indicator onwards (remove leading slash)
|
|
27
|
+
return filePath.substring(index + 1);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// If no indicator found, try to get just the last few path segments
|
|
32
|
+
const parts = filePath.split("/");
|
|
33
|
+
|
|
34
|
+
// Return last 3-4 segments if available (e.g., "apps/next-16/app/page.tsx")
|
|
35
|
+
if (parts.length > 3) {
|
|
36
|
+
return parts.slice(-4).join("/");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Last resort: return as-is
|
|
40
|
+
return filePath;
|
|
41
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a server-side component in the ancestry chain.
|
|
3
|
+
* Used for Phoenix LiveView, Rails ViewComponents, Next.js RSC, etc.
|
|
4
|
+
*/
|
|
5
|
+
export interface ServerComponentInfo {
|
|
6
|
+
/** Component name (e.g., "AppWeb.CoreComponents.button") or "@caller" for call site */
|
|
7
|
+
name: string;
|
|
8
|
+
/** File path (e.g., "lib/app_web/core_components.ex") */
|
|
9
|
+
filePath: string;
|
|
10
|
+
/** Line number in the source file */
|
|
11
|
+
line: number;
|
|
12
|
+
/** Type of server component annotation */
|
|
13
|
+
type: "component" | "caller";
|
|
14
|
+
}
|
package/.turbo/turbo-build.log
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
> @treelocator/runtime@0.1.8 build /Users/wende/projects/locatorjs/packages/runtime
|
|
3
|
-
> concurrently pnpm:build:*
|
|
4
|
-
|
|
5
|
-
[tailwind]
|
|
6
|
-
[tailwind] > @treelocator/runtime@0.1.8 build:tailwind /Users/wende/projects/locatorjs/packages/runtime
|
|
7
|
-
[tailwind] > tailwindcss -i ./src/main.css -o ./dist/output.css
|
|
8
|
-
[tailwind]
|
|
9
|
-
[babel]
|
|
10
|
-
[babel] > @treelocator/runtime@0.1.8 build:babel /Users/wende/projects/locatorjs/packages/runtime
|
|
11
|
-
[babel] > babel src --out-dir dist --extensions .js,.jsx,.ts,.tsx
|
|
12
|
-
[babel]
|
|
13
|
-
[ts]
|
|
14
|
-
[ts] > @treelocator/runtime@0.1.8 build:ts /Users/wende/projects/locatorjs/packages/runtime
|
|
15
|
-
[ts] > tsc --declaration --emitDeclarationOnly --noEmit false --outDir dist
|
|
16
|
-
[ts]
|
|
17
|
-
[wrapImage]
|
|
18
|
-
[wrapImage] > @treelocator/runtime@0.1.8 build:wrapImage /Users/wende/projects/locatorjs/packages/runtime
|
|
19
|
-
[wrapImage] > node ./scripts/wrapImage.js
|
|
20
|
-
[wrapImage]
|
|
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 164ms.
|
|
27
|
-
[tailwind] pnpm run build:tailwind exited with code 0
|
|
28
|
-
[babel] Successfully compiled 72 files with Babel (409ms).
|
|
29
|
-
[babel] pnpm run build:babel exited with code 0
|
|
30
|
-
[ts] pnpm run build:ts exited with code 0
|
package/.turbo/turbo-test.log
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
> @treelocator/runtime@0.1.5 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/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
|
|
14
|
-
|
|
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)
|
|
19
|
-
|
package/.turbo/turbo-ts.log
DELETED
package/LICENSE
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
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.
|