@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
package/src/browserApi.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  formatAncestryChain,
6
6
  AncestryItem,
7
7
  } from "./functions/formatAncestryChain";
8
+ import { enrichAncestryWithSourceMaps } from "./functions/enrichAncestrySourceMaps";
8
9
 
9
10
  export interface LocatorJSAPI {
10
11
  /**
@@ -33,7 +34,7 @@ export interface LocatorJSAPI {
33
34
  * });
34
35
  * console.log(path);
35
36
  */
36
- getPath(elementOrSelector: HTMLElement | string): string | null;
37
+ getPath(elementOrSelector: HTMLElement | string): Promise<string | null>;
37
38
 
38
39
  /**
39
40
  * Get raw ancestry data for an element.
@@ -61,7 +62,7 @@ export interface LocatorJSAPI {
61
62
  * return ancestry?.map(item => item.componentName).filter(Boolean);
62
63
  * });
63
64
  */
64
- getAncestry(elementOrSelector: HTMLElement | string): AncestryItem[] | null;
65
+ getAncestry(elementOrSelector: HTMLElement | string): Promise<AncestryItem[] | null>;
65
66
 
66
67
  /**
67
68
  * Get both formatted path and raw ancestry data in a single call.
@@ -88,7 +89,7 @@ export interface LocatorJSAPI {
88
89
  */
89
90
  getPathData(
90
91
  elementOrSelector: HTMLElement | string
91
- ): { path: string; ancestry: AncestryItem[] } | null;
92
+ ): Promise<{ path: string; ancestry: AncestryItem[] } | null>;
92
93
 
93
94
  /**
94
95
  * Display help information about the LocatorJS API.
@@ -128,6 +129,14 @@ function getAncestryForElement(element: HTMLElement): AncestryItem[] | null {
128
129
  return collectAncestry(treeNode);
129
130
  }
130
131
 
132
+ async function getEnrichedAncestryForElement(
133
+ element: HTMLElement
134
+ ): Promise<AncestryItem[] | null> {
135
+ const ancestry = getAncestryForElement(element);
136
+ if (!ancestry) return null;
137
+ return enrichAncestryWithSourceMaps(ancestry, element);
138
+ }
139
+
131
140
  const HELP_TEXT = `
132
141
  ╔═══════════════════════════════════════════════════════════════════════════╗
133
142
  ║ TreeLocatorJS Browser API ║
@@ -233,46 +242,39 @@ export function createBrowserAPI(
233
242
  adapterId = adapterIdParam;
234
243
 
235
244
  return {
236
- getPath(elementOrSelector: HTMLElement | string): string | null {
245
+ getPath(elementOrSelector: HTMLElement | string): Promise<string | null> {
237
246
  const element = resolveElement(elementOrSelector);
238
247
  if (!element) {
239
- return null;
240
- }
241
-
242
- const ancestry = getAncestryForElement(element);
243
- if (!ancestry) {
244
- return null;
248
+ return Promise.resolve(null);
245
249
  }
246
250
 
247
- return formatAncestryChain(ancestry);
251
+ return getEnrichedAncestryForElement(element).then((ancestry) =>
252
+ ancestry ? formatAncestryChain(ancestry) : null
253
+ );
248
254
  },
249
255
 
250
- getAncestry(elementOrSelector: HTMLElement | string): AncestryItem[] | null {
256
+ getAncestry(
257
+ elementOrSelector: HTMLElement | string
258
+ ): Promise<AncestryItem[] | null> {
251
259
  const element = resolveElement(elementOrSelector);
252
260
  if (!element) {
253
- return null;
261
+ return Promise.resolve(null);
254
262
  }
255
263
 
256
- return getAncestryForElement(element);
264
+ return getEnrichedAncestryForElement(element);
257
265
  },
258
266
 
259
267
  getPathData(
260
268
  elementOrSelector: HTMLElement | string
261
- ): { path: string; ancestry: AncestryItem[] } | null {
269
+ ): Promise<{ path: string; ancestry: AncestryItem[] } | null> {
262
270
  const element = resolveElement(elementOrSelector);
263
271
  if (!element) {
264
- return null;
265
- }
266
-
267
- const ancestry = getAncestryForElement(element);
268
- if (!ancestry) {
269
- return null;
272
+ return Promise.resolve(null);
270
273
  }
271
274
 
272
- return {
273
- path: formatAncestryChain(ancestry),
274
- ancestry,
275
- };
275
+ return getEnrichedAncestryForElement(element).then((ancestry) =>
276
+ ancestry ? { path: formatAncestryChain(ancestry), ancestry } : null
277
+ );
276
278
  },
277
279
 
278
280
  help(): string {
@@ -108,7 +108,7 @@ export function MaybeOutline(props: {
108
108
  >
109
109
  {props.currentElement.tagName.toLowerCase()}
110
110
  {props.currentElement.id ? `#${props.currentElement.id}` : ""}
111
- {props.currentElement.className ? `.${props.currentElement.className.split(" ")[0]}` : ""}
111
+ {props.currentElement.getAttribute('class') ? `.${props.currentElement.getAttribute('class')!.split(" ")[0]}` : ""}
112
112
  </div>
113
113
  </div>
114
114
  )}
@@ -8,6 +8,7 @@ import { MaybeOutline } from "./MaybeOutline";
8
8
  import { isLocatorsOwnElement } from "../functions/isLocatorsOwnElement";
9
9
  import { Toast } from "./Toast";
10
10
  import { collectAncestry, formatAncestryChain } from "../functions/formatAncestryChain";
11
+ import { enrichAncestryWithSourceMaps } from "../functions/enrichAncestrySourceMaps";
11
12
  import { createTreeNode } from "../adapters/createTreeNode";
12
13
  import treeIconUrl from "../_generated_tree_icon";
13
14
 
@@ -153,10 +154,24 @@ function Runtime(props: RuntimeProps) {
153
154
  const treeNode = createTreeNode(element as HTMLElement, props.adapterId);
154
155
  if (treeNode) {
155
156
  const ancestry = collectAncestry(treeNode);
157
+
158
+ // Write immediately with component names (preserves user gesture for clipboard API)
156
159
  const formatted = formatAncestryChain(ancestry);
157
160
  navigator.clipboard.writeText(formatted).then(() => {
158
161
  setToastMessage("Copied to clipboard");
159
162
  });
163
+
164
+ // For React 19+: try to enrich with source map file paths and re-copy
165
+ enrichAncestryWithSourceMaps(ancestry, element as HTMLElement).then(
166
+ (enriched) => {
167
+ const enrichedFormatted = formatAncestryChain(enriched);
168
+ if (enrichedFormatted !== formatted) {
169
+ navigator.clipboard.writeText(enrichedFormatted).then(() => {
170
+ setToastMessage("Copied to clipboard");
171
+ });
172
+ }
173
+ }
174
+ );
160
175
  }
161
176
 
162
177
  // Deactivate toggle after click
@@ -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
+ }