@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.
Files changed (30) hide show
  1. package/.turbo/turbo-build.log +14 -14
  2. package/.turbo/turbo-test.log +6 -55
  3. package/dist/adapters/createTreeNode.js +10 -1
  4. package/dist/adapters/react/findDebugSource.d.ts +13 -0
  5. package/dist/adapters/react/findDebugSource.js +37 -0
  6. package/dist/adapters/react/findFiberByHtmlElement.js +23 -1
  7. package/dist/adapters/react/getAllParentsElementsAndRootComponent.d.ts +1 -1
  8. package/dist/adapters/react/getAllParentsElementsAndRootComponent.js +6 -4
  9. package/dist/adapters/react/getAllWrappingParents.js +1 -1
  10. package/dist/adapters/react/reactAdapter.js +5 -1
  11. package/dist/adapters/react/resolveSourceMap.d.ts +29 -0
  12. package/dist/adapters/react/resolveSourceMap.js +236 -0
  13. package/dist/browserApi.d.ts +4 -4
  14. package/dist/browserApi.js +13 -15
  15. package/dist/components/MaybeOutline.js +2 -2
  16. package/dist/components/Runtime.js +13 -0
  17. package/dist/functions/enrichAncestrySourceMaps.d.ts +7 -0
  18. package/dist/functions/enrichAncestrySourceMaps.js +80 -0
  19. package/package.json +3 -3
  20. package/src/adapters/createTreeNode.ts +10 -1
  21. package/src/adapters/react/findDebugSource.ts +40 -0
  22. package/src/adapters/react/findFiberByHtmlElement.ts +26 -1
  23. package/src/adapters/react/getAllParentsElementsAndRootComponent.ts +7 -7
  24. package/src/adapters/react/getAllWrappingParents.ts +1 -1
  25. package/src/adapters/react/reactAdapter.ts +7 -3
  26. package/src/adapters/react/resolveSourceMap.ts +316 -0
  27. package/src/browserApi.ts +27 -25
  28. package/src/components/MaybeOutline.tsx +1 -1
  29. package/src/components/Runtime.tsx +15 -0
  30. package/src/functions/enrichAncestrySourceMaps.ts +103 -0
@@ -1,32 +1,32 @@
1
1
 
2
- > @treelocator/runtime@0.1.8 build /Users/wende/projects/locatorjs/packages/runtime
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.8 build:wrapImage /Users/wende/projects/locatorjs/packages/runtime
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.8 build:ts /Users/wende/projects/locatorjs/packages/runtime
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 173ms.
28
+ [tailwind] Done in 175ms.
28
29
  [tailwind] pnpm run build:tailwind exited with code 0
29
- [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`
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
@@ -1,63 +1,14 @@
1
1
 
2
- > @treelocator/runtime@0.1.8 test /Users/wende/projects/locatorjs/packages/runtime
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/locatorjs/packages/runtime
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/getUsableFileName.test.tsx (4 tests) 4ms
13
- src/functions/formatAncestryChain.test.ts (9 tests | 2 failed) 7ms
14
- × formatAncestryChain > shows all owner components when ownerComponents is provided 4ms
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,4 +5,4 @@ export declare function getAllParentsElementsAndRootComponent(fiber: Fiber): {
5
5
  component: Fiber;
6
6
  componentBox: SimpleDOMRect;
7
7
  parentElements: ElementInfo[];
8
- };
8
+ } | null;
@@ -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 HTMLElement)) {
9
- throw new Error("This functions works only for Fibres with HTMLElement stateNode");
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 HTMLElement)) {
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
- throw new Error("Could not find root component");
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 HTMLElement) {
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
- } = getAllParentsElementsAndRootComponent(fiber);
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
+ }
@@ -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.
@@ -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 getAncestryForElement(element);
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;