@treelocator/runtime 0.2.0 → 0.3.1
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 +14 -14
- package/.turbo/turbo-test.log +6 -55
- package/dist/adapters/createTreeNode.js +10 -1
- 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/getAllParentsElementsAndRootComponent.d.ts +1 -1
- package/dist/adapters/react/getAllParentsElementsAndRootComponent.js +6 -4
- package/dist/adapters/react/getAllWrappingParents.js +1 -1
- package/dist/adapters/react/reactAdapter.js +5 -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 +2 -2
- package/dist/components/Runtime.js +13 -0
- package/dist/functions/enrichAncestrySourceMaps.d.ts +7 -0
- package/dist/functions/enrichAncestrySourceMaps.js +80 -0
- package/package.json +3 -3
- package/src/adapters/createTreeNode.ts +10 -1
- package/src/adapters/react/findDebugSource.ts +40 -0
- package/src/adapters/react/findFiberByHtmlElement.ts +26 -1
- package/src/adapters/react/getAllParentsElementsAndRootComponent.ts +7 -7
- package/src/adapters/react/getAllWrappingParents.ts +1 -1
- package/src/adapters/react/reactAdapter.ts +7 -3
- package/src/adapters/react/resolveSourceMap.ts +316 -0
- package/src/browserApi.ts +27 -25
- package/src/components/MaybeOutline.tsx +1 -1
- package/src/components/Runtime.tsx +15 -0
- package/src/functions/enrichAncestrySourceMaps.ts +103 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
1
|
|
|
2
|
-
> @treelocator/runtime@0.1
|
|
2
|
+
> @treelocator/runtime@0.3.1 build /Users/wende/projects/treelocatorjs/packages/runtime
|
|
3
3
|
> concurrently pnpm:build:*
|
|
4
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
5
|
[wrapImage]
|
|
14
|
-
[wrapImage] > @treelocator/runtime@0.1
|
|
6
|
+
[wrapImage] > @treelocator/runtime@0.3.1 build:wrapImage /Users/wende/projects/treelocatorjs/packages/runtime
|
|
15
7
|
[wrapImage] > node ./scripts/wrapImage.js
|
|
16
8
|
[wrapImage]
|
|
17
9
|
[ts]
|
|
18
|
-
[ts] > @treelocator/runtime@0.1
|
|
10
|
+
[ts] > @treelocator/runtime@0.3.1 build:ts /Users/wende/projects/treelocatorjs/packages/runtime
|
|
19
11
|
[ts] > tsc --declaration --emitDeclarationOnly --noEmit false --outDir dist
|
|
20
12
|
[ts]
|
|
13
|
+
[babel]
|
|
14
|
+
[babel] > @treelocator/runtime@0.3.1 build:babel /Users/wende/projects/treelocatorjs/packages/runtime
|
|
15
|
+
[babel] > babel src --out-dir dist --extensions .js,.jsx,.ts,.tsx
|
|
16
|
+
[babel]
|
|
17
|
+
[tailwind]
|
|
18
|
+
[tailwind] > @treelocator/runtime@0.3.1 build:tailwind /Users/wende/projects/treelocatorjs/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
|
+
[babel] [baseline-browser-mapping] The data in this module is over two months old. To ensure accurate Baseline data, please update: `npm i baseline-browser-mapping@latest -D`
|
|
23
24
|
[tailwind] [baseline-browser-mapping] The data in this module is over two months old. To ensure accurate Baseline data, please update: `npm i baseline-browser-mapping@latest -D`
|
|
24
25
|
[tailwind]
|
|
25
26
|
[tailwind] Rebuilding...
|
|
26
27
|
[tailwind]
|
|
27
|
-
[tailwind] Done in
|
|
28
|
+
[tailwind] Done in 175ms.
|
|
28
29
|
[tailwind] pnpm run build:tailwind exited with code 0
|
|
29
|
-
[babel]
|
|
30
|
-
[babel] Successfully compiled 80 files with Babel (499ms).
|
|
30
|
+
[babel] Successfully compiled 82 files with Babel (602ms).
|
|
31
31
|
[babel] pnpm run build:babel exited with code 0
|
|
32
32
|
[ts] pnpm run build:ts exited with code 0
|
package/.turbo/turbo-test.log
CHANGED
|
@@ -1,63 +1,14 @@
|
|
|
1
1
|
|
|
2
|
-
> @treelocator/runtime@0.
|
|
2
|
+
> @treelocator/runtime@0.3.0 test /Users/wende/projects/treelocatorjs/packages/runtime
|
|
3
3
|
> vitest run
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
RUN v2.1.9 /Users/wende/projects/
|
|
6
|
+
RUN v2.1.9 /Users/wende/projects/treelocatorjs/packages/runtime
|
|
7
7
|
|
|
8
|
+
✓ src/functions/formatAncestryChain.test.ts (9 tests) 2ms
|
|
8
9
|
✓ src/functions/evalTemplate.test.ts (1 test) 1ms
|
|
9
|
-
✓ src/functions/transformPath.test.ts (3 tests) 1ms
|
|
10
|
-
✓ src/functions/mergeRects.test.ts (1 test) 2ms
|
|
11
10
|
✓ src/functions/cropPath.test.ts (2 tests) 1ms
|
|
12
|
-
✓ src/functions/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
→ expected 'div in App at src/App.jsx:104\n └─…' to be 'div in App at src/App.jsx:104\n └─…' // Object.is equality
|
|
16
|
-
× formatAncestryChain > shows single owner component without arrow when only one in chain 1ms
|
|
17
|
-
→ expected 'Button at src/Button.tsx:10' to be 'button in Button at src/Button.tsx:10' // Object.is equality
|
|
18
|
-
✓ src/adapters/phoenix/__tests__/parsePhoenixComments.test.ts (14 tests) 16ms
|
|
19
|
-
|
|
20
|
-
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯
|
|
21
|
-
|
|
22
|
-
FAIL src/functions/formatAncestryChain.test.ts > formatAncestryChain > shows all owner components when ownerComponents is provided
|
|
23
|
-
AssertionError: expected 'div in App at src/App.jsx:104\n └─…' to be 'div in App at src/App.jsx:104\n └─…' // Object.is equality
|
|
24
|
-
|
|
25
|
-
- Expected
|
|
26
|
-
+ Received
|
|
27
|
-
|
|
28
|
-
div in App at src/App.jsx:104
|
|
29
|
-
- └─ div#sidebar-panel in Sidebar > GlassPanel at src/components/game/Sidebar.jsx:78
|
|
30
|
-
+ └─ GlassPanel#sidebar-panel in Sidebar at src/components/game/Sidebar.jsx:78
|
|
31
|
-
|
|
32
|
-
❯ src/functions/formatAncestryChain.test.ts:120:20
|
|
33
|
-
118|
|
|
34
|
-
119| const result = formatAncestryChain(items);
|
|
35
|
-
120| expect(result).toBe(
|
|
36
|
-
| ^
|
|
37
|
-
121| `div in App at src/App.jsx:104
|
|
38
|
-
122| └─ div#sidebar-panel in Sidebar > GlassPanel at src/components/gam…
|
|
39
|
-
|
|
40
|
-
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯
|
|
41
|
-
|
|
42
|
-
FAIL src/functions/formatAncestryChain.test.ts > formatAncestryChain > shows single owner component without arrow when only one in chain
|
|
43
|
-
AssertionError: expected 'Button at src/Button.tsx:10' to be 'button in Button at src/Button.tsx:10' // Object.is equality
|
|
44
|
-
|
|
45
|
-
Expected: "button in Button at src/Button.tsx:10"
|
|
46
|
-
Received: "Button at src/Button.tsx:10"
|
|
47
|
-
|
|
48
|
-
❯ src/functions/formatAncestryChain.test.ts:138:20
|
|
49
|
-
136|
|
|
50
|
-
137| const result = formatAncestryChain(items);
|
|
51
|
-
138| expect(result).toBe("button in Button at src/Button.tsx:10");
|
|
52
|
-
| ^
|
|
53
|
-
139| });
|
|
54
|
-
140| });
|
|
55
|
-
|
|
56
|
-
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/2]⎯
|
|
57
|
-
|
|
58
|
-
Test Files 1 failed | 6 passed (7)
|
|
59
|
-
Tests 2 failed | 32 passed (34)
|
|
60
|
-
Start at 11:30:22
|
|
61
|
-
Duration 904ms (transform 370ms, setup 0ms, collect 434ms, tests 32ms, environment 497ms, prepare 595ms)
|
|
62
|
-
|
|
11
|
+
✓ src/functions/mergeRects.test.ts (1 test) 1ms
|
|
12
|
+
✓ src/functions/getUsableFileName.test.tsx (4 tests) 2ms
|
|
13
|
+
✓ src/functions/transformPath.test.ts (3 tests) 6ms
|
|
63
14
|
ELIFECYCLE Test failed. See above for more details.
|
|
@@ -4,6 +4,15 @@ import { SvelteTreeNodeElement } from "./svelte/svelteAdapter";
|
|
|
4
4
|
import { VueTreeNodeElement } from "./vue/vueAdapter";
|
|
5
5
|
import { detectJSX, detectReact, detectSvelte, detectVue } from "@locator/shared";
|
|
6
6
|
import { detectPhoenix } from "./phoenix/detectPhoenix";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Fallback React detection: check if any DOM element has __reactFiber$ keys.
|
|
10
|
+
* Works without React DevTools extension (where detectReact() fails because
|
|
11
|
+
* the renderers Map is empty).
|
|
12
|
+
*/
|
|
13
|
+
function hasReactFiberKeys(element) {
|
|
14
|
+
return Object.keys(element).some(k => k.startsWith("__reactFiber$"));
|
|
15
|
+
}
|
|
7
16
|
export function createTreeNode(element, adapterId) {
|
|
8
17
|
// Check for explicit adapter ID first
|
|
9
18
|
if (adapterId === "react") {
|
|
@@ -26,7 +35,7 @@ export function createTreeNode(element, adapterId) {
|
|
|
26
35
|
if (detectVue()) {
|
|
27
36
|
return new VueTreeNodeElement(element);
|
|
28
37
|
}
|
|
29
|
-
if (detectReact()) {
|
|
38
|
+
if (detectReact() || hasReactFiberKeys(element)) {
|
|
30
39
|
return new ReactTreeNodeElement(element);
|
|
31
40
|
}
|
|
32
41
|
|
|
@@ -3,3 +3,16 @@ export declare function findDebugSource(fiber: Fiber): {
|
|
|
3
3
|
fiber: Fiber;
|
|
4
4
|
source: Source;
|
|
5
5
|
} | null;
|
|
6
|
+
/**
|
|
7
|
+
* Async version of findDebugSource that supports React 19's _debugStack.
|
|
8
|
+
* Falls back to synchronous _debugSource check first (React 18).
|
|
9
|
+
* If that fails, parses _debugStack and resolves via source maps.
|
|
10
|
+
*/
|
|
11
|
+
export declare function findDebugSourceAsync(fiber: Fiber): Promise<{
|
|
12
|
+
fiber: Fiber;
|
|
13
|
+
source: Source;
|
|
14
|
+
} | null>;
|
|
15
|
+
/**
|
|
16
|
+
* Check if this is a React 19+ environment (has _debugStack but not _debugSource).
|
|
17
|
+
*/
|
|
18
|
+
export declare function isReact19Fiber(fiber: Fiber): boolean;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { resolveSourceFromDebugStack } from "./resolveSourceMap";
|
|
1
2
|
export function findDebugSource(fiber) {
|
|
2
3
|
let current = fiber;
|
|
3
4
|
while (current) {
|
|
5
|
+
// React 18 and earlier: _debugSource is a structured object
|
|
4
6
|
if (current._debugSource) {
|
|
5
7
|
return {
|
|
6
8
|
fiber: current,
|
|
@@ -10,4 +12,39 @@ export function findDebugSource(fiber) {
|
|
|
10
12
|
current = current._debugOwner || null;
|
|
11
13
|
}
|
|
12
14
|
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Async version of findDebugSource that supports React 19's _debugStack.
|
|
19
|
+
* Falls back to synchronous _debugSource check first (React 18).
|
|
20
|
+
* If that fails, parses _debugStack and resolves via source maps.
|
|
21
|
+
*/
|
|
22
|
+
export async function findDebugSourceAsync(fiber) {
|
|
23
|
+
// Try synchronous path first (React 18)
|
|
24
|
+
const syncResult = findDebugSource(fiber);
|
|
25
|
+
if (syncResult) return syncResult;
|
|
26
|
+
|
|
27
|
+
// React 19: try resolving via _debugStack + source maps
|
|
28
|
+
let current = fiber;
|
|
29
|
+
while (current) {
|
|
30
|
+
const debugStack = current._debugStack;
|
|
31
|
+
if (debugStack?.stack) {
|
|
32
|
+
const source = await resolveSourceFromDebugStack(debugStack);
|
|
33
|
+
if (source) {
|
|
34
|
+
return {
|
|
35
|
+
fiber: current,
|
|
36
|
+
source
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
current = current._debugOwner || null;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check if this is a React 19+ environment (has _debugStack but not _debugSource).
|
|
47
|
+
*/
|
|
48
|
+
export function isReact19Fiber(fiber) {
|
|
49
|
+
return !fiber._debugSource && !!fiber._debugStack;
|
|
13
50
|
}
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import { findDebugSource } from "./findDebugSource";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Find the React fiber key on a DOM element (e.g., "__reactFiber$abc123").
|
|
5
|
+
* Works across all React versions that attach fibers to DOM nodes.
|
|
6
|
+
*/
|
|
7
|
+
function findFiberFromDOMElement(element) {
|
|
8
|
+
const fiberKey = Object.keys(element).find(k => k.startsWith("__reactFiber$"));
|
|
9
|
+
if (fiberKey) {
|
|
10
|
+
return element[fiberKey];
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
2
14
|
export function findFiberByHtmlElement(target, shouldHaveDebugSource) {
|
|
15
|
+
// Try via DevTools renderers first (available when React DevTools extension is installed)
|
|
3
16
|
const renderers = window.__REACT_DEVTOOLS_GLOBAL_HOOK__?.renderers;
|
|
4
|
-
// console.log("RENDERERS: ", renderers);
|
|
5
17
|
const renderersValues = renderers?.values();
|
|
6
18
|
if (renderersValues) {
|
|
7
19
|
for (const renderer of Array.from(renderersValues)) {
|
|
@@ -18,5 +30,15 @@ export function findFiberByHtmlElement(target, shouldHaveDebugSource) {
|
|
|
18
30
|
}
|
|
19
31
|
}
|
|
20
32
|
}
|
|
33
|
+
|
|
34
|
+
// Fallback: read fiber directly from DOM element's __reactFiber$ property.
|
|
35
|
+
// This works without the React DevTools extension and across React 16-19.
|
|
36
|
+
const fiber = findFiberFromDOMElement(target);
|
|
37
|
+
if (fiber) {
|
|
38
|
+
if (shouldHaveDebugSource) {
|
|
39
|
+
return findDebugSource(fiber)?.fiber || null;
|
|
40
|
+
}
|
|
41
|
+
return fiber;
|
|
42
|
+
}
|
|
21
43
|
return null;
|
|
22
44
|
}
|
|
@@ -5,8 +5,9 @@ import { isStyledElement } from "./isStyled";
|
|
|
5
5
|
export function getAllParentsElementsAndRootComponent(fiber) {
|
|
6
6
|
const parentElements = [];
|
|
7
7
|
const deepestElement = fiber.stateNode;
|
|
8
|
-
if (!deepestElement || !(deepestElement instanceof
|
|
9
|
-
|
|
8
|
+
if (!deepestElement || !(deepestElement instanceof Element)) {
|
|
9
|
+
console.warn("[TreeLocator] Skipping fiber with non-Element stateNode:", fiber.type, fiber.stateNode);
|
|
10
|
+
return null;
|
|
10
11
|
}
|
|
11
12
|
let componentBox = deepestElement.getBoundingClientRect();
|
|
12
13
|
|
|
@@ -15,7 +16,7 @@ export function getAllParentsElementsAndRootComponent(fiber) {
|
|
|
15
16
|
while (currentFiber._debugOwner || currentFiber.return) {
|
|
16
17
|
currentFiber = currentFiber._debugOwner || currentFiber.return;
|
|
17
18
|
const currentElement = currentFiber.stateNode;
|
|
18
|
-
if (!currentElement || !(currentElement instanceof
|
|
19
|
+
if (!currentElement || !(currentElement instanceof Element)) {
|
|
19
20
|
return {
|
|
20
21
|
component: currentFiber,
|
|
21
22
|
parentElements,
|
|
@@ -30,5 +31,6 @@ export function getAllParentsElementsAndRootComponent(fiber) {
|
|
|
30
31
|
link: null
|
|
31
32
|
});
|
|
32
33
|
}
|
|
33
|
-
|
|
34
|
+
console.warn("[TreeLocator] Could not find root component for fiber:", fiber.type);
|
|
35
|
+
return null;
|
|
34
36
|
}
|
|
@@ -4,7 +4,7 @@ export function getAllWrappingParents(fiber) {
|
|
|
4
4
|
let currentFiber = fiber;
|
|
5
5
|
while (currentFiber.return) {
|
|
6
6
|
currentFiber = currentFiber.return;
|
|
7
|
-
if (currentFiber.stateNode && currentFiber.stateNode instanceof
|
|
7
|
+
if (currentFiber.stateNode && currentFiber.stateNode instanceof Element) {
|
|
8
8
|
return parents;
|
|
9
9
|
}
|
|
10
10
|
|
|
@@ -13,11 +13,15 @@ export function getElementInfo(found) {
|
|
|
13
13
|
const labels = [];
|
|
14
14
|
const fiber = findFiberByHtmlElement(found, false);
|
|
15
15
|
if (fiber) {
|
|
16
|
+
const result = getAllParentsElementsAndRootComponent(fiber);
|
|
17
|
+
if (!result) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
16
20
|
const {
|
|
17
21
|
component,
|
|
18
22
|
componentBox,
|
|
19
23
|
parentElements
|
|
20
|
-
} =
|
|
24
|
+
} = result;
|
|
21
25
|
const allPotentialComponentFibers = getAllWrappingParents(component);
|
|
22
26
|
|
|
23
27
|
// This handles a common case when the component root is basically the comopnent itself, so I want to go to usage of the component
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Source } from "@locator/shared";
|
|
2
|
+
/**
|
|
3
|
+
* Parse the React 19 _debugStack Error.stack to extract the caller's location.
|
|
4
|
+
*
|
|
5
|
+
* Stack format:
|
|
6
|
+
* Error: react-stack-top-frame
|
|
7
|
+
* at exports.jsxDEV (http://localhost:3000/_next/.../chunk.js:410:33)
|
|
8
|
+
* at Home (http://localhost:3000/_next/.../chunk.js:8789:416)
|
|
9
|
+
* at Object.react_stack_bottom_frame (...)
|
|
10
|
+
*
|
|
11
|
+
* The second frame (after jsxDEV/jsx) is the component that created the element.
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseDebugStack(stack: string): {
|
|
14
|
+
url: string;
|
|
15
|
+
line: number;
|
|
16
|
+
column: number;
|
|
17
|
+
} | null;
|
|
18
|
+
/**
|
|
19
|
+
* Resolve a bundled location to its original source using source maps.
|
|
20
|
+
* Returns null if resolution fails.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveSourceLocation(url: string, line: number, column: number): Promise<Source | null>;
|
|
23
|
+
/**
|
|
24
|
+
* Given a React 19 fiber's _debugStack, resolve the source location
|
|
25
|
+
* by parsing the stack trace and looking up source maps.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveSourceFromDebugStack(debugStack: {
|
|
28
|
+
stack?: string;
|
|
29
|
+
}): Promise<Source | null>;
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// Cache: bundled script URL -> source map
|
|
2
|
+
const sourceMapCache = new Map();
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parse the React 19 _debugStack Error.stack to extract the caller's location.
|
|
6
|
+
*
|
|
7
|
+
* Stack format:
|
|
8
|
+
* Error: react-stack-top-frame
|
|
9
|
+
* at exports.jsxDEV (http://localhost:3000/_next/.../chunk.js:410:33)
|
|
10
|
+
* at Home (http://localhost:3000/_next/.../chunk.js:8789:416)
|
|
11
|
+
* at Object.react_stack_bottom_frame (...)
|
|
12
|
+
*
|
|
13
|
+
* The second frame (after jsxDEV/jsx) is the component that created the element.
|
|
14
|
+
*/
|
|
15
|
+
export function parseDebugStack(stack) {
|
|
16
|
+
const lines = stack.split("\n");
|
|
17
|
+
|
|
18
|
+
// Find the component caller frame (skip react-stack-top-frame and jsx factory)
|
|
19
|
+
for (let i = 1; i < lines.length; i++) {
|
|
20
|
+
const line = lines[i]?.trim();
|
|
21
|
+
if (!line) continue;
|
|
22
|
+
|
|
23
|
+
// Skip react internal frames
|
|
24
|
+
if (line.includes("react-stack-top-frame") || line.includes("react_stack_bottom_frame")) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Skip JSX factory frames (jsxDEV, jsx, jsxs)
|
|
29
|
+
if (line.includes("jsxDEV") || line.includes("exports.jsx ") || line.includes("exports.jsxs ")) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Parse "at Name (url:line:col)" or "at url:line:col"
|
|
34
|
+
const match = line.match(/at\s+(?:\S+\s+)?\(?(https?:\/\/.+?):(\d+):(\d+)\)?/) || line.match(/at\s+(https?:\/\/.+?):(\d+):(\d+)/);
|
|
35
|
+
if (match && match[1] && match[2] && match[3]) {
|
|
36
|
+
return {
|
|
37
|
+
url: match[1],
|
|
38
|
+
line: parseInt(match[2], 10),
|
|
39
|
+
column: parseInt(match[3], 10)
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
async function fetchSourceMap(scriptUrl) {
|
|
46
|
+
if (sourceMapCache.has(scriptUrl)) {
|
|
47
|
+
return sourceMapCache.get(scriptUrl);
|
|
48
|
+
}
|
|
49
|
+
const promise = (async () => {
|
|
50
|
+
try {
|
|
51
|
+
// Fetch the script to find sourceMappingURL
|
|
52
|
+
const scriptResp = await fetch(scriptUrl);
|
|
53
|
+
if (!scriptResp.ok) return null;
|
|
54
|
+
const scriptText = await scriptResp.text();
|
|
55
|
+
const match = scriptText.match(/\/\/[#@]\s*sourceMappingURL=(.+?)(?:\s|$)/);
|
|
56
|
+
if (!match || !match[1]) return null;
|
|
57
|
+
|
|
58
|
+
// Resolve the source map URL relative to the script URL
|
|
59
|
+
let mapUrl = match[1];
|
|
60
|
+
if (!mapUrl.startsWith("http")) {
|
|
61
|
+
const base = scriptUrl.substring(0, scriptUrl.lastIndexOf("/") + 1);
|
|
62
|
+
mapUrl = base + mapUrl;
|
|
63
|
+
}
|
|
64
|
+
const mapResp = await fetch(mapUrl);
|
|
65
|
+
if (!mapResp.ok) return null;
|
|
66
|
+
return await mapResp.json();
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
})();
|
|
71
|
+
sourceMapCache.set(scriptUrl, promise);
|
|
72
|
+
return promise;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Decode a single VLQ value from a mappings string, returns [value, charsConsumed]
|
|
76
|
+
const VLQ_BASE_SHIFT = 5;
|
|
77
|
+
const VLQ_BASE = 1 << VLQ_BASE_SHIFT; // 32
|
|
78
|
+
const VLQ_BASE_MASK = VLQ_BASE - 1; // 31
|
|
79
|
+
const VLQ_CONTINUATION_BIT = VLQ_BASE; // 32
|
|
80
|
+
const BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
81
|
+
const base64Map = new Map();
|
|
82
|
+
for (let i = 0; i < BASE64_CHARS.length; i++) {
|
|
83
|
+
base64Map.set(BASE64_CHARS[i], i);
|
|
84
|
+
}
|
|
85
|
+
function decodeVLQ(str, index) {
|
|
86
|
+
let result = 0;
|
|
87
|
+
let shift = 0;
|
|
88
|
+
let continuation;
|
|
89
|
+
let i = index;
|
|
90
|
+
do {
|
|
91
|
+
const char = str[i];
|
|
92
|
+
if (!char) return [0, i];
|
|
93
|
+
const digit = base64Map.get(char);
|
|
94
|
+
if (digit === undefined) return [0, i];
|
|
95
|
+
i++;
|
|
96
|
+
continuation = (digit & VLQ_CONTINUATION_BIT) !== 0;
|
|
97
|
+
result += (digit & VLQ_BASE_MASK) << shift;
|
|
98
|
+
shift += VLQ_BASE_SHIFT;
|
|
99
|
+
} while (continuation);
|
|
100
|
+
|
|
101
|
+
// Convert from VLQ signed
|
|
102
|
+
const isNegative = (result & 1) === 1;
|
|
103
|
+
result >>= 1;
|
|
104
|
+
return [isNegative ? -result : result, i];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Find the original source location for a generated line/column in a basic source map.
|
|
109
|
+
*/
|
|
110
|
+
function resolveInBasicMap(map, targetLine, targetColumn) {
|
|
111
|
+
const mappings = map.mappings;
|
|
112
|
+
if (!mappings) return null;
|
|
113
|
+
let generatedLine = 1;
|
|
114
|
+
let generatedColumn = 0;
|
|
115
|
+
let sourceIndex = 0;
|
|
116
|
+
let sourceLine = 0;
|
|
117
|
+
let sourceColumn = 0;
|
|
118
|
+
let bestSource = null;
|
|
119
|
+
let i = 0;
|
|
120
|
+
while (i < mappings.length) {
|
|
121
|
+
const char = mappings[i];
|
|
122
|
+
if (char === ";") {
|
|
123
|
+
generatedLine++;
|
|
124
|
+
generatedColumn = 0;
|
|
125
|
+
i++;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (char === ",") {
|
|
129
|
+
i++;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Decode segment
|
|
134
|
+
let colDelta;
|
|
135
|
+
[colDelta, i] = decodeVLQ(mappings, i);
|
|
136
|
+
generatedColumn += colDelta;
|
|
137
|
+
|
|
138
|
+
// Check if there's source info (segments can be 1, 4, or 5 fields)
|
|
139
|
+
if (i < mappings.length && mappings[i] !== "," && mappings[i] !== ";") {
|
|
140
|
+
let srcDelta, lineDelta, srcColDelta;
|
|
141
|
+
[srcDelta, i] = decodeVLQ(mappings, i);
|
|
142
|
+
sourceIndex += srcDelta;
|
|
143
|
+
[lineDelta, i] = decodeVLQ(mappings, i);
|
|
144
|
+
sourceLine += lineDelta;
|
|
145
|
+
[srcColDelta, i] = decodeVLQ(mappings, i);
|
|
146
|
+
sourceColumn += srcColDelta;
|
|
147
|
+
|
|
148
|
+
// Skip optional name index
|
|
149
|
+
if (i < mappings.length && mappings[i] !== "," && mappings[i] !== ";") {
|
|
150
|
+
[, i] = decodeVLQ(mappings, i);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Check if this mapping is at or before our target
|
|
154
|
+
if (generatedLine === targetLine && generatedColumn <= targetColumn) {
|
|
155
|
+
const fileName = map.sources[sourceIndex];
|
|
156
|
+
if (fileName) {
|
|
157
|
+
bestSource = {
|
|
158
|
+
fileName: cleanSourcePath(fileName),
|
|
159
|
+
lineNumber: sourceLine + 1,
|
|
160
|
+
// source maps are 0-indexed
|
|
161
|
+
columnNumber: sourceColumn
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// If we've passed the target line, we can stop
|
|
167
|
+
if (generatedLine > targetLine) {
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return bestSource;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Clean up source file paths from source maps.
|
|
177
|
+
* Strips file:// protocol and common project root prefixes.
|
|
178
|
+
*/
|
|
179
|
+
function cleanSourcePath(filePath) {
|
|
180
|
+
// Strip file:// protocol
|
|
181
|
+
let cleaned = filePath.replace(/^file:\/\//, "");
|
|
182
|
+
|
|
183
|
+
// Strip webpack/turbopack internal prefixes
|
|
184
|
+
cleaned = cleaned.replace(/^\[project\]\//, "");
|
|
185
|
+
cleaned = cleaned.replace(/^webpack:\/\/[^/]*\//, "");
|
|
186
|
+
return cleaned;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Resolve a bundled location to its original source using source maps.
|
|
191
|
+
* Returns null if resolution fails.
|
|
192
|
+
*/
|
|
193
|
+
export async function resolveSourceLocation(url, line, column) {
|
|
194
|
+
const sourceMap = await fetchSourceMap(url);
|
|
195
|
+
if (!sourceMap) return null;
|
|
196
|
+
|
|
197
|
+
// Handle indexed/sectioned source maps (used by Turbopack)
|
|
198
|
+
if ("sections" in sourceMap && sourceMap.sections) {
|
|
199
|
+
const sections = sourceMap.sections;
|
|
200
|
+
|
|
201
|
+
// Find the section that contains our target line
|
|
202
|
+
let targetSection = null;
|
|
203
|
+
for (let i = sections.length - 1; i >= 0; i--) {
|
|
204
|
+
const section = sections[i];
|
|
205
|
+
if (!section) continue;
|
|
206
|
+
if (section.offset.line < line) {
|
|
207
|
+
targetSection = section;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
if (section.offset.line === line && section.offset.column <= column) {
|
|
211
|
+
targetSection = section;
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (!targetSection) return null;
|
|
216
|
+
|
|
217
|
+
// Resolve within the section's map (adjust line/column relative to section offset)
|
|
218
|
+
const relLine = line - targetSection.offset.line;
|
|
219
|
+
const relCol = line === targetSection.offset.line ? column - targetSection.offset.column : column;
|
|
220
|
+
return resolveInBasicMap(targetSection.map, relLine, relCol);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Handle basic source maps
|
|
224
|
+
return resolveInBasicMap(sourceMap, line, column);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Given a React 19 fiber's _debugStack, resolve the source location
|
|
229
|
+
* by parsing the stack trace and looking up source maps.
|
|
230
|
+
*/
|
|
231
|
+
export async function resolveSourceFromDebugStack(debugStack) {
|
|
232
|
+
if (!debugStack?.stack) return null;
|
|
233
|
+
const parsed = parseDebugStack(debugStack.stack);
|
|
234
|
+
if (!parsed) return null;
|
|
235
|
+
return resolveSourceLocation(parsed.url, parsed.line, parsed.column);
|
|
236
|
+
}
|
package/dist/browserApi.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ export interface LocatorJSAPI {
|
|
|
27
27
|
* });
|
|
28
28
|
* console.log(path);
|
|
29
29
|
*/
|
|
30
|
-
getPath(elementOrSelector: HTMLElement | string): string | null
|
|
30
|
+
getPath(elementOrSelector: HTMLElement | string): Promise<string | null>;
|
|
31
31
|
/**
|
|
32
32
|
* Get raw ancestry data for an element.
|
|
33
33
|
* Returns an array of objects containing component names, file paths, and line numbers.
|
|
@@ -54,7 +54,7 @@ export interface LocatorJSAPI {
|
|
|
54
54
|
* return ancestry?.map(item => item.componentName).filter(Boolean);
|
|
55
55
|
* });
|
|
56
56
|
*/
|
|
57
|
-
getAncestry(elementOrSelector: HTMLElement | string): AncestryItem[] | null
|
|
57
|
+
getAncestry(elementOrSelector: HTMLElement | string): Promise<AncestryItem[] | null>;
|
|
58
58
|
/**
|
|
59
59
|
* Get both formatted path and raw ancestry data in a single call.
|
|
60
60
|
* Convenience method that combines getPath() and getAncestry().
|
|
@@ -78,10 +78,10 @@ export interface LocatorJSAPI {
|
|
|
78
78
|
* console.log('Source files:', data.ancestry.map(a => a.filePath));
|
|
79
79
|
* }
|
|
80
80
|
*/
|
|
81
|
-
getPathData(elementOrSelector: HTMLElement | string): {
|
|
81
|
+
getPathData(elementOrSelector: HTMLElement | string): Promise<{
|
|
82
82
|
path: string;
|
|
83
83
|
ancestry: AncestryItem[];
|
|
84
|
-
} | null
|
|
84
|
+
} | null>;
|
|
85
85
|
/**
|
|
86
86
|
* Display help information about the LocatorJS API.
|
|
87
87
|
* Shows usage examples and method descriptions for browser automation tools.
|
package/dist/browserApi.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createTreeNode } from "./adapters/createTreeNode";
|
|
2
2
|
import { collectAncestry, formatAncestryChain } from "./functions/formatAncestryChain";
|
|
3
|
+
import { enrichAncestryWithSourceMaps } from "./functions/enrichAncestrySourceMaps";
|
|
3
4
|
let adapterId;
|
|
4
5
|
function resolveElement(elementOrSelector) {
|
|
5
6
|
if (typeof elementOrSelector === "string") {
|
|
@@ -15,6 +16,11 @@ function getAncestryForElement(element) {
|
|
|
15
16
|
}
|
|
16
17
|
return collectAncestry(treeNode);
|
|
17
18
|
}
|
|
19
|
+
async function getEnrichedAncestryForElement(element) {
|
|
20
|
+
const ancestry = getAncestryForElement(element);
|
|
21
|
+
if (!ancestry) return null;
|
|
22
|
+
return enrichAncestryWithSourceMaps(ancestry, element);
|
|
23
|
+
}
|
|
18
24
|
const HELP_TEXT = `
|
|
19
25
|
╔═══════════════════════════════════════════════════════════════════════════╗
|
|
20
26
|
║ TreeLocatorJS Browser API ║
|
|
@@ -119,34 +125,26 @@ export function createBrowserAPI(adapterIdParam) {
|
|
|
119
125
|
getPath(elementOrSelector) {
|
|
120
126
|
const element = resolveElement(elementOrSelector);
|
|
121
127
|
if (!element) {
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
const ancestry = getAncestryForElement(element);
|
|
125
|
-
if (!ancestry) {
|
|
126
|
-
return null;
|
|
128
|
+
return Promise.resolve(null);
|
|
127
129
|
}
|
|
128
|
-
return formatAncestryChain(ancestry);
|
|
130
|
+
return getEnrichedAncestryForElement(element).then(ancestry => ancestry ? formatAncestryChain(ancestry) : null);
|
|
129
131
|
},
|
|
130
132
|
getAncestry(elementOrSelector) {
|
|
131
133
|
const element = resolveElement(elementOrSelector);
|
|
132
134
|
if (!element) {
|
|
133
|
-
return null;
|
|
135
|
+
return Promise.resolve(null);
|
|
134
136
|
}
|
|
135
|
-
return
|
|
137
|
+
return getEnrichedAncestryForElement(element);
|
|
136
138
|
},
|
|
137
139
|
getPathData(elementOrSelector) {
|
|
138
140
|
const element = resolveElement(elementOrSelector);
|
|
139
141
|
if (!element) {
|
|
140
|
-
return null;
|
|
141
|
-
}
|
|
142
|
-
const ancestry = getAncestryForElement(element);
|
|
143
|
-
if (!ancestry) {
|
|
144
|
-
return null;
|
|
142
|
+
return Promise.resolve(null);
|
|
145
143
|
}
|
|
146
|
-
return {
|
|
144
|
+
return getEnrichedAncestryForElement(element).then(ancestry => ancestry ? {
|
|
147
145
|
path: formatAncestryChain(ancestry),
|
|
148
146
|
ancestry
|
|
149
|
-
};
|
|
147
|
+
} : null);
|
|
150
148
|
},
|
|
151
149
|
help() {
|
|
152
150
|
return HELP_TEXT;
|