@treelocator/runtime 0.4.7 → 0.6.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.
Files changed (166) hide show
  1. package/.eslintignore +1 -0
  2. package/dist/_generated_styles.d.ts +1 -1
  3. package/dist/_generated_styles.js +20 -0
  4. package/dist/_generated_tree_icon.d.ts +1 -1
  5. package/dist/adapters/HtmlElementTreeNode.d.ts +2 -2
  6. package/dist/adapters/HtmlElementTreeNode.js +4 -6
  7. package/dist/adapters/createTreeNode.js +17 -44
  8. package/dist/adapters/detectFramework.d.ts +8 -0
  9. package/dist/adapters/detectFramework.js +25 -0
  10. package/dist/adapters/detectFramework.test.d.ts +1 -0
  11. package/dist/adapters/detectFramework.test.js +60 -0
  12. package/dist/adapters/jsx/jsxAdapter.js +54 -89
  13. package/dist/adapters/jsx/jsxAdapter.test.d.ts +1 -0
  14. package/dist/adapters/jsx/jsxAdapter.test.js +273 -0
  15. package/dist/adapters/nextjs/parseNextjsDataAttributes.js +1 -1
  16. package/dist/adapters/nextjs/parseNextjsDataAttributes.test.d.ts +1 -0
  17. package/dist/adapters/nextjs/parseNextjsDataAttributes.test.js +158 -0
  18. package/dist/adapters/react/findFiberByHtmlElement.d.ts +1 -1
  19. package/dist/adapters/react/findFiberByHtmlElement.js +1 -1
  20. package/dist/adapters/react/getAllParentsElementsAndRootComponent.js +4 -0
  21. package/dist/adapters/resolveAdapter.d.ts +1 -1
  22. package/dist/adapters/resolveAdapter.js +4 -8
  23. package/dist/adapters/svelte/svelteAdapter.test.d.ts +1 -0
  24. package/dist/adapters/svelte/svelteAdapter.test.js +280 -0
  25. package/dist/adapters/vue/vueAdapter.test.d.ts +1 -0
  26. package/dist/adapters/vue/vueAdapter.test.js +222 -0
  27. package/dist/browserApi.d.ts +148 -0
  28. package/dist/browserApi.js +146 -5
  29. package/dist/browserApi.test.d.ts +1 -0
  30. package/dist/browserApi.test.js +287 -0
  31. package/dist/components/RecordingPillButton.d.ts +11 -0
  32. package/dist/components/RecordingPillButton.js +202 -0
  33. package/dist/components/RecordingResults.d.ts +2 -0
  34. package/dist/components/RecordingResults.js +213 -78
  35. package/dist/components/Runtime.js +161 -554
  36. package/dist/components/SettingsPanel.d.ts +5 -0
  37. package/dist/components/SettingsPanel.js +312 -0
  38. package/dist/consoleCapture.d.ts +9 -0
  39. package/dist/consoleCapture.js +95 -0
  40. package/dist/dejitter/recorder.d.ts +7 -1
  41. package/dist/dejitter/recorder.js +64 -1
  42. package/dist/functions/cssRuleInspector.d.ts +83 -0
  43. package/dist/functions/cssRuleInspector.js +608 -0
  44. package/dist/functions/cssRuleInspector.test.d.ts +1 -0
  45. package/dist/functions/cssRuleInspector.test.js +439 -0
  46. package/dist/functions/deduplicateLabels.test.d.ts +1 -0
  47. package/dist/functions/deduplicateLabels.test.js +178 -0
  48. package/dist/functions/enrichAncestrySourceMaps.js +0 -1
  49. package/dist/functions/extractComputedStyles.d.ts +51 -0
  50. package/dist/functions/extractComputedStyles.js +447 -0
  51. package/dist/functions/extractComputedStyles.test.d.ts +1 -0
  52. package/dist/functions/extractComputedStyles.test.js +549 -0
  53. package/dist/functions/formatAncestryChain.d.ts +8 -0
  54. package/dist/functions/formatAncestryChain.js +21 -1
  55. package/dist/functions/formatAncestryChain.test.js +18 -0
  56. package/dist/functions/getUsableName.test.d.ts +1 -0
  57. package/dist/functions/getUsableName.test.js +219 -0
  58. package/dist/functions/isCombinationModifiersPressed.test.d.ts +1 -0
  59. package/dist/functions/isCombinationModifiersPressed.test.js +192 -0
  60. package/dist/functions/mergeRects.test.js +210 -1
  61. package/dist/functions/namedSnapshots.d.ts +52 -0
  62. package/dist/functions/namedSnapshots.js +161 -0
  63. package/dist/functions/namedSnapshots.test.d.ts +1 -0
  64. package/dist/functions/namedSnapshots.test.js +85 -0
  65. package/dist/functions/normalizeFilePath.test.d.ts +1 -0
  66. package/dist/functions/normalizeFilePath.test.js +66 -0
  67. package/dist/functions/parseDataId.test.d.ts +1 -0
  68. package/dist/functions/parseDataId.test.js +101 -0
  69. package/dist/hooks/getStorage.d.ts +3 -0
  70. package/dist/hooks/getStorage.js +17 -0
  71. package/dist/hooks/useEventListeners.d.ts +15 -0
  72. package/dist/hooks/useEventListeners.js +56 -0
  73. package/dist/hooks/useLocatorStorage.d.ts +18 -0
  74. package/dist/hooks/useLocatorStorage.js +41 -0
  75. package/dist/hooks/useLocatorStorage.test.d.ts +1 -0
  76. package/dist/hooks/useLocatorStorage.test.js +124 -0
  77. package/dist/hooks/useRecordingState.d.ts +43 -0
  78. package/dist/hooks/useRecordingState.js +387 -0
  79. package/dist/hooks/useSettings.d.ts +13 -0
  80. package/dist/hooks/useSettings.js +66 -0
  81. package/dist/index.d.ts +5 -2
  82. package/dist/index.js +4 -2
  83. package/dist/initRuntime.d.ts +3 -1
  84. package/dist/initRuntime.js +4 -1
  85. package/dist/mcpBridge.d.ts +61 -0
  86. package/dist/mcpBridge.js +534 -0
  87. package/dist/mcpBridge.test.d.ts +1 -0
  88. package/dist/mcpBridge.test.js +248 -0
  89. package/dist/output.css +20 -0
  90. package/dist/visualDiff/diff.d.ts +9 -0
  91. package/dist/visualDiff/diff.js +209 -0
  92. package/dist/visualDiff/diff.test.d.ts +1 -0
  93. package/dist/visualDiff/diff.test.js +253 -0
  94. package/dist/visualDiff/settle.d.ts +3 -0
  95. package/dist/visualDiff/settle.js +50 -0
  96. package/dist/visualDiff/settle.test.d.ts +1 -0
  97. package/dist/visualDiff/settle.test.js +65 -0
  98. package/dist/visualDiff/snapshot.d.ts +4 -0
  99. package/dist/visualDiff/snapshot.js +84 -0
  100. package/dist/visualDiff/snapshot.test.d.ts +1 -0
  101. package/dist/visualDiff/snapshot.test.js +245 -0
  102. package/dist/visualDiff/types.d.ts +37 -0
  103. package/dist/visualDiff/types.js +1 -0
  104. package/package.json +2 -2
  105. package/scripts/wrapCSS.js +1 -1
  106. package/scripts/wrapImage.js +1 -1
  107. package/src/_generated_styles.ts +21 -1
  108. package/src/_generated_tree_icon.ts +1 -1
  109. package/src/adapters/HtmlElementTreeNode.ts +10 -7
  110. package/src/adapters/createTreeNode.ts +12 -51
  111. package/src/adapters/detectFramework.test.ts +73 -0
  112. package/src/adapters/detectFramework.ts +28 -0
  113. package/src/adapters/jsx/jsxAdapter.test.ts +240 -0
  114. package/src/adapters/jsx/jsxAdapter.ts +53 -106
  115. package/src/adapters/nextjs/parseNextjsDataAttributes.test.ts +212 -0
  116. package/src/adapters/nextjs/parseNextjsDataAttributes.ts +1 -1
  117. package/src/adapters/react/findDebugSource.ts +5 -6
  118. package/src/adapters/react/findFiberByHtmlElement.ts +3 -3
  119. package/src/adapters/react/getAllParentsElementsAndRootComponent.ts +3 -0
  120. package/src/adapters/react/reactAdapter.ts +1 -2
  121. package/src/adapters/resolveAdapter.ts +4 -14
  122. package/src/adapters/svelte/svelteAdapter.test.ts +334 -0
  123. package/src/adapters/vue/vueAdapter.test.ts +259 -0
  124. package/src/browserApi.test.ts +329 -0
  125. package/src/browserApi.ts +351 -4
  126. package/src/components/RecordingPillButton.tsx +301 -0
  127. package/src/components/RecordingResults.tsx +114 -13
  128. package/src/components/Runtime.tsx +176 -621
  129. package/src/components/SettingsPanel.tsx +339 -0
  130. package/src/consoleCapture.ts +113 -0
  131. package/src/dejitter/recorder.ts +67 -3
  132. package/src/functions/cssRuleInspector.test.ts +517 -0
  133. package/src/functions/cssRuleInspector.ts +708 -0
  134. package/src/functions/deduplicateLabels.test.ts +115 -0
  135. package/src/functions/enrichAncestrySourceMaps.ts +6 -3
  136. package/src/functions/extractComputedStyles.test.ts +681 -0
  137. package/src/functions/extractComputedStyles.ts +768 -0
  138. package/src/functions/formatAncestryChain.test.ts +23 -1
  139. package/src/functions/formatAncestryChain.ts +22 -1
  140. package/src/functions/getUsableName.test.ts +242 -0
  141. package/src/functions/isCombinationModifiersPressed.test.ts +156 -0
  142. package/src/functions/mergeRects.test.ts +111 -1
  143. package/src/functions/namedSnapshots.test.ts +106 -0
  144. package/src/functions/namedSnapshots.ts +232 -0
  145. package/src/functions/normalizeFilePath.test.ts +80 -0
  146. package/src/functions/parseDataId.test.ts +125 -0
  147. package/src/hooks/getStorage.ts +26 -0
  148. package/src/hooks/useEventListeners.ts +97 -0
  149. package/src/hooks/useLocatorStorage.test.ts +127 -0
  150. package/src/hooks/useLocatorStorage.ts +60 -0
  151. package/src/hooks/useRecordingState.ts +516 -0
  152. package/src/hooks/useSettings.ts +83 -0
  153. package/src/index.ts +10 -5
  154. package/src/initRuntime.ts +5 -0
  155. package/src/mcpBridge.test.ts +260 -0
  156. package/src/mcpBridge.ts +677 -0
  157. package/src/visualDiff/diff.test.ts +167 -0
  158. package/src/visualDiff/diff.ts +242 -0
  159. package/src/visualDiff/settle.test.ts +77 -0
  160. package/src/visualDiff/settle.ts +62 -0
  161. package/src/visualDiff/snapshot.test.ts +200 -0
  162. package/src/visualDiff/snapshot.ts +119 -0
  163. package/src/visualDiff/types.ts +40 -0
  164. package/tsconfig.json +3 -1
  165. package/vitest.config.ts +18 -0
  166. package/jest.config.ts +0 -195
@@ -3,62 +3,64 @@ import { goUpByTheTree } from "../goUpByTheTree";
3
3
  import { HtmlElementTreeNode } from "../HtmlElementTreeNode";
4
4
  import { getExpressionData } from "./getExpressionData";
5
5
  import { getJSXComponentBoundingBox } from "./getJSXComponentBoundingBox";
6
+ function resolveJSXLocatorData(element) {
7
+ const dataId = element.getAttribute("data-locatorjs-id");
8
+ const dataPath = element.getAttribute("data-locatorjs");
9
+ if (!dataId && !dataPath) return null;
10
+ let fileFullPath;
11
+ if (dataPath) {
12
+ const parsed = parseDataPath(dataPath);
13
+ if (!parsed) return null;
14
+ [fileFullPath] = parsed;
15
+ } else if (dataId) {
16
+ [fileFullPath] = parseDataId(dataId);
17
+ } else {
18
+ return null;
19
+ }
20
+ const locatorData = window.__LOCATOR_DATA__;
21
+ const fileData = locatorData?.[fileFullPath];
22
+ let filePath;
23
+ let projectPath;
24
+ if (fileData) {
25
+ filePath = fileData.filePath;
26
+ projectPath = fileData.projectPath;
27
+ } else {
28
+ [projectPath, filePath] = splitFullPath(fileFullPath);
29
+ }
30
+ return {
31
+ fileFullPath,
32
+ fileData,
33
+ filePath,
34
+ projectPath
35
+ };
36
+ }
6
37
  export function getElementInfo(target) {
7
38
  const found = target.closest("[data-locatorjs-id], [data-locatorjs]");
8
39
 
9
40
  // Support both HTMLElement and SVGElement
10
41
  // SVG elements don't have dataset, so use getAttribute instead
11
- const dataId = found?.getAttribute("data-locatorjs-id");
12
- const dataPath = found?.getAttribute("data-locatorjs");
13
42
  const styledDataId = found?.getAttribute("data-locatorjs-styled");
14
- if (found && (found instanceof HTMLElement || found instanceof SVGElement) && (dataId || dataPath || styledDataId)) {
15
- if (!dataId && !dataPath) {
43
+ if (found && (found instanceof HTMLElement || found instanceof SVGElement)) {
44
+ const resolved = resolveJSXLocatorData(found);
45
+ if (!resolved && !styledDataId) {
16
46
  return null;
17
47
  }
18
- let fileFullPath;
19
- if (dataPath) {
20
- const parsed = parseDataPath(dataPath);
21
- if (!parsed) {
22
- return null;
23
- }
24
- [fileFullPath] = parsed;
25
- } else if (dataId) {
26
- [fileFullPath] = parseDataId(dataId);
27
- } else {
48
+ if (!resolved) {
28
49
  return null;
29
50
  }
51
+ const {
52
+ fileFullPath,
53
+ fileData,
54
+ filePath,
55
+ projectPath
56
+ } = resolved;
30
57
  const locatorData = window.__LOCATOR_DATA__;
31
- const fileData = locatorData?.[fileFullPath];
32
-
33
- // Handle styled components (only when locatorData is available)
34
- const [styledFileFullPath, styledId] = styledDataId ? parseDataId(styledDataId) : [null, null];
35
- const styledFileData = styledFileFullPath && locatorData?.[styledFileFullPath];
36
- const styledExpData = styledFileData && styledFileData.styledDefinitions[Number(styledId)];
37
-
38
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
39
- const styledLink = styledExpData && {
40
- filePath: styledFileData.filePath,
41
- projectPath: styledFileData.projectPath,
42
- column: (styledExpData.loc?.start.column || 0) + 1,
43
- line: styledExpData.loc?.start.line || 0
44
- };
45
58
 
46
59
  // Get expression data (works with or without locatorData)
47
60
  const expData = getExpressionData(found, fileData || null);
48
61
  if (!expData) {
49
62
  return null;
50
63
  }
51
-
52
- // Extract file path components
53
- let filePath;
54
- let projectPath;
55
- if (fileData) {
56
- filePath = fileData.filePath;
57
- projectPath = fileData.projectPath;
58
- } else {
59
- // If no fileData, split the full path
60
- [projectPath, filePath] = splitFullPath(fileFullPath);
61
- }
62
64
  const wrappingComponent = expData.wrappingComponentId !== null && fileData ? fileData.components[Number(expData.wrappingComponentId)] : null;
63
65
  return {
64
66
  thisElement: {
@@ -89,41 +91,19 @@ export function getElementInfo(target) {
89
91
  }
90
92
  export class JSXTreeNodeElement extends HtmlElementTreeNode {
91
93
  getSource() {
92
- // Use getAttribute instead of dataset to support both HTML and SVG elements
93
- const dataId = this.element.getAttribute("data-locatorjs-id");
94
- const dataPath = this.element.getAttribute("data-locatorjs");
95
- if (!dataId && !dataPath) {
96
- return null;
97
- }
98
- let fileFullPath;
99
- if (dataPath) {
100
- const parsed = parseDataPath(dataPath);
101
- if (!parsed) {
102
- return null;
103
- }
104
- [fileFullPath] = parsed;
105
- } else if (dataId) {
106
- [fileFullPath] = parseDataId(dataId);
107
- } else {
108
- return null;
109
- }
110
- const locatorData = window.__LOCATOR_DATA__;
111
- const fileData = locatorData?.[fileFullPath];
94
+ const resolved = resolveJSXLocatorData(this.element);
95
+ if (!resolved) return null;
96
+ const {
97
+ fileData,
98
+ filePath,
99
+ projectPath
100
+ } = resolved;
112
101
 
113
102
  // Get expression data (works with or without locatorData)
114
103
  const expData = getExpressionData(this.element, fileData || null);
115
104
  if (expData) {
116
- let fileName;
117
- let projectPath;
118
- if (fileData) {
119
- fileName = fileData.filePath;
120
- projectPath = fileData.projectPath;
121
- } else {
122
- // If no fileData, split the full path
123
- [projectPath, fileName] = splitFullPath(fileFullPath);
124
- }
125
105
  return {
126
- fileName,
106
+ fileName: filePath,
127
107
  projectPath,
128
108
  columnNumber: (expData.loc.start.column || 0) + 1,
129
109
  lineNumber: expData.loc.start.line || 0
@@ -132,26 +112,11 @@ export class JSXTreeNodeElement extends HtmlElementTreeNode {
132
112
  return null;
133
113
  }
134
114
  getComponent() {
135
- // Use getAttribute instead of dataset to support both HTML and SVG elements
136
- const dataId = this.element.getAttribute("data-locatorjs-id");
137
- const dataPath = this.element.getAttribute("data-locatorjs");
138
- if (!dataId && !dataPath) {
139
- return null;
140
- }
141
- let fileFullPath;
142
- if (dataPath) {
143
- const parsed = parseDataPath(dataPath);
144
- if (!parsed) {
145
- return null;
146
- }
147
- [fileFullPath] = parsed;
148
- } else if (dataId) {
149
- [fileFullPath] = parseDataId(dataId);
150
- } else {
151
- return null;
152
- }
153
- const locatorData = window.__LOCATOR_DATA__;
154
- const fileData = locatorData?.[fileFullPath];
115
+ const resolved = resolveJSXLocatorData(this.element);
116
+ if (!resolved) return null;
117
+ const {
118
+ fileData
119
+ } = resolved;
155
120
 
156
121
  // Component information is only available when we have fileData
157
122
  if (fileData) {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,273 @@
1
+ import { afterEach, describe, expect, test } from "vitest";
2
+ import { getElementInfo, JSXTreeNodeElement } from "./jsxAdapter";
3
+ function makeElement(attrs = {}) {
4
+ const el = document.createElement("div");
5
+ for (const [k, v] of Object.entries(attrs)) {
6
+ el.setAttribute(k, v);
7
+ }
8
+ return el;
9
+ }
10
+ afterEach(() => {
11
+ delete window.__LOCATOR_DATA__;
12
+ });
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // getElementInfo
16
+ // ---------------------------------------------------------------------------
17
+
18
+ describe("getElementInfo", () => {
19
+ test("returns null when element has no data-locatorjs attributes", () => {
20
+ const el = makeElement({
21
+ class: "foo"
22
+ });
23
+ expect(getElementInfo(el)).toBeNull();
24
+ });
25
+ test("returns FullElementInfo from data-locatorjs path attribute (no __LOCATOR_DATA__)", () => {
26
+ // Format: /project/src/Button.tsx:10:5
27
+ const el = makeElement({
28
+ "data-locatorjs": "/project/src/Button.tsx:10:5"
29
+ });
30
+ document.body.appendChild(el);
31
+ const info = getElementInfo(el);
32
+ expect(info).not.toBeNull();
33
+
34
+ // splitFullPath splits on /src/ → projectPath="/project", filePath="/src/Button.tsx"
35
+ expect(info.thisElement.link).toMatchObject({
36
+ filePath: "/src/Button.tsx",
37
+ projectPath: "/project",
38
+ line: 10,
39
+ column: 6 // column + 1
40
+ });
41
+ expect(info.htmlElement).toBe(el);
42
+ document.body.removeChild(el);
43
+ });
44
+ test("returns FullElementInfo using fileData from __LOCATOR_DATA__ when data-locatorjs-id is set", () => {
45
+ const fileFullPath = "/project/src/Button.tsx";
46
+ const fileData = {
47
+ filePath: "/src/Button.tsx",
48
+ projectPath: "/project",
49
+ expressions: [{
50
+ name: "Button",
51
+ loc: {
52
+ start: {
53
+ line: 5,
54
+ column: 2
55
+ },
56
+ end: {
57
+ line: 5,
58
+ column: 10
59
+ }
60
+ },
61
+ wrappingComponentId: null
62
+ }],
63
+ components: [],
64
+ styledDefinitions: []
65
+ };
66
+ window.__LOCATOR_DATA__ = {
67
+ [fileFullPath]: fileData
68
+ };
69
+ const el = makeElement({
70
+ "data-locatorjs-id": `${fileFullPath}::0`
71
+ });
72
+ document.body.appendChild(el);
73
+ const info = getElementInfo(el);
74
+ expect(info).not.toBeNull();
75
+ expect(info.thisElement.link).toMatchObject({
76
+ filePath: "/src/Button.tsx",
77
+ projectPath: "/project",
78
+ line: 5,
79
+ column: 3
80
+ });
81
+ expect(info.thisElement.label).toBe("Button");
82
+ document.body.removeChild(el);
83
+ });
84
+ test("finds attributes on nearest ancestor via closest()", () => {
85
+ const parent = makeElement({
86
+ "data-locatorjs": "/project/src/Layout.tsx:20:0"
87
+ });
88
+ const child = makeElement();
89
+ parent.appendChild(child);
90
+ document.body.appendChild(parent);
91
+ const info = getElementInfo(child);
92
+ expect(info).not.toBeNull();
93
+ expect(info.thisElement.link).toMatchObject({
94
+ line: 20,
95
+ column: 1
96
+ });
97
+ expect(info.htmlElement).toBe(parent);
98
+ document.body.removeChild(parent);
99
+ });
100
+ test("returns null when data-locatorjs attribute has malformed path (only one colon)", () => {
101
+ // parseDataPath returns null for a path with only one colon
102
+ const el = makeElement({
103
+ "data-locatorjs": "/project/src/Button.tsx:10"
104
+ });
105
+ document.body.appendChild(el);
106
+ const info = getElementInfo(el);
107
+ expect(info).toBeNull();
108
+ document.body.removeChild(el);
109
+ });
110
+ });
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // JSXTreeNodeElement.getSource
114
+ // ---------------------------------------------------------------------------
115
+
116
+ describe("JSXTreeNodeElement.getSource", () => {
117
+ test("returns null when element has no data-locatorjs attributes", () => {
118
+ const el = makeElement();
119
+ const node = new JSXTreeNodeElement(el);
120
+ expect(node.getSource()).toBeNull();
121
+ });
122
+ test("returns Source from data-locatorjs attribute without __LOCATOR_DATA__", () => {
123
+ const el = makeElement({
124
+ "data-locatorjs": "/project/src/App.tsx:15:3"
125
+ });
126
+ const node = new JSXTreeNodeElement(el);
127
+ const source = node.getSource();
128
+ expect(source).not.toBeNull();
129
+ expect(source).toMatchObject({
130
+ fileName: "/src/App.tsx",
131
+ projectPath: "/project",
132
+ lineNumber: 15,
133
+ columnNumber: 4 // column + 1
134
+ });
135
+ });
136
+ test("returns Source using fileData from __LOCATOR_DATA__ when data-locatorjs-id is set", () => {
137
+ const fileFullPath = "/project/src/App.tsx";
138
+ const fileData = {
139
+ filePath: "/src/App.tsx",
140
+ projectPath: "/project",
141
+ expressions: [{
142
+ name: "App",
143
+ loc: {
144
+ start: {
145
+ line: 8,
146
+ column: 4
147
+ },
148
+ end: {
149
+ line: 8,
150
+ column: 12
151
+ }
152
+ },
153
+ wrappingComponentId: null
154
+ }],
155
+ components: [],
156
+ styledDefinitions: []
157
+ };
158
+ window.__LOCATOR_DATA__ = {
159
+ [fileFullPath]: fileData
160
+ };
161
+ const el = makeElement({
162
+ "data-locatorjs-id": `${fileFullPath}::0`
163
+ });
164
+ const node = new JSXTreeNodeElement(el);
165
+ const source = node.getSource();
166
+ expect(source).not.toBeNull();
167
+ expect(source).toMatchObject({
168
+ fileName: "/src/App.tsx",
169
+ projectPath: "/project",
170
+ lineNumber: 8,
171
+ columnNumber: 5
172
+ });
173
+ });
174
+ });
175
+
176
+ // ---------------------------------------------------------------------------
177
+ // JSXTreeNodeElement.getComponent
178
+ // ---------------------------------------------------------------------------
179
+
180
+ describe("JSXTreeNodeElement.getComponent", () => {
181
+ test("returns null when element has no data-locatorjs attributes", () => {
182
+ const el = makeElement();
183
+ const node = new JSXTreeNodeElement(el);
184
+ expect(node.getComponent()).toBeNull();
185
+ });
186
+ test("returns null when __LOCATOR_DATA__ is not set (no fileData)", () => {
187
+ const el = makeElement({
188
+ "data-locatorjs": "/project/src/Card.tsx:5:0"
189
+ });
190
+ const node = new JSXTreeNodeElement(el);
191
+ expect(node.getComponent()).toBeNull();
192
+ });
193
+ test("returns null when expression has no wrappingComponentId", () => {
194
+ const fileFullPath = "/project/src/Card.tsx";
195
+ window.__LOCATOR_DATA__ = {
196
+ [fileFullPath]: {
197
+ filePath: "/src/Card.tsx",
198
+ projectPath: "/project",
199
+ expressions: [{
200
+ name: "div",
201
+ loc: {
202
+ start: {
203
+ line: 3,
204
+ column: 0
205
+ },
206
+ end: {
207
+ line: 3,
208
+ column: 5
209
+ }
210
+ },
211
+ wrappingComponentId: null
212
+ }],
213
+ components: [],
214
+ styledDefinitions: []
215
+ }
216
+ };
217
+ const el = makeElement({
218
+ "data-locatorjs-id": `${fileFullPath}::0`
219
+ });
220
+ const node = new JSXTreeNodeElement(el);
221
+ expect(node.getComponent()).toBeNull();
222
+ });
223
+ test("returns TreeNodeComponent when fileData and wrappingComponentId are present", () => {
224
+ const fileFullPath = "/project/src/Form.tsx";
225
+ window.__LOCATOR_DATA__ = {
226
+ [fileFullPath]: {
227
+ filePath: "/src/Form.tsx",
228
+ projectPath: "/project",
229
+ expressions: [{
230
+ name: "input",
231
+ loc: {
232
+ start: {
233
+ line: 12,
234
+ column: 4
235
+ },
236
+ end: {
237
+ line: 12,
238
+ column: 12
239
+ }
240
+ },
241
+ wrappingComponentId: 0
242
+ }],
243
+ components: [{
244
+ name: "Form",
245
+ loc: {
246
+ start: {
247
+ line: 1,
248
+ column: 0
249
+ },
250
+ end: {
251
+ line: 20,
252
+ column: 1
253
+ }
254
+ }
255
+ }],
256
+ styledDefinitions: []
257
+ }
258
+ };
259
+ const el = makeElement({
260
+ "data-locatorjs-id": `${fileFullPath}::0`
261
+ });
262
+ const node = new JSXTreeNodeElement(el);
263
+ const component = node.getComponent();
264
+ expect(component).not.toBeNull();
265
+ expect(component.label).toBe("Form");
266
+ expect(component.definitionLink).toMatchObject({
267
+ fileName: "/src/Form.tsx",
268
+ projectPath: "/project",
269
+ lineNumber: 1,
270
+ columnNumber: 1
271
+ });
272
+ });
273
+ });
@@ -48,7 +48,7 @@ function parseDataLocatorjsValue(value) {
48
48
  if (parts.length < 2) return null;
49
49
 
50
50
  // Last two parts are column and line (in reverse order)
51
- const column = parts.pop();
51
+ parts.pop(); // discard column
52
52
  const line = parts.pop();
53
53
 
54
54
  // Everything else is the file path (which may contain colons on Windows)
@@ -0,0 +1,158 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { collectNextjsServerComponents, parseNextjsServerComponents } from "./parseNextjsDataAttributes";
3
+ describe("collectNextjsServerComponents", () => {
4
+ test("extracts component name from layout.tsx", () => {
5
+ const element = document.createElement("div");
6
+ element.setAttribute("data-locatorjs", "/apps/next-16/app/layout.tsx:27:4");
7
+ const result = collectNextjsServerComponents(element);
8
+ expect(result).toHaveLength(1);
9
+ expect(result[0]).toEqual({
10
+ name: "RootLayout",
11
+ filePath: "app/layout.tsx",
12
+ line: 27,
13
+ type: "component"
14
+ });
15
+ });
16
+ test("extracts component name from page.tsx", () => {
17
+ const element = document.createElement("div");
18
+ element.setAttribute("data-locatorjs", "/apps/next-16/app/page.tsx:5:4");
19
+ const result = collectNextjsServerComponents(element);
20
+ expect(result).toHaveLength(1);
21
+ expect(result[0]).toEqual({
22
+ name: "Page",
23
+ filePath: "app/page.tsx",
24
+ line: 5,
25
+ type: "component"
26
+ });
27
+ });
28
+ test("extracts custom component name from Header.tsx", () => {
29
+ const element = document.createElement("div");
30
+ element.setAttribute("data-locatorjs", "/apps/next-16/app/components/Header.tsx:10:2");
31
+ const result = collectNextjsServerComponents(element);
32
+ expect(result).toHaveLength(1);
33
+ expect(result[0]).toEqual({
34
+ name: "Header",
35
+ filePath: "app/components/Header.tsx",
36
+ line: 10,
37
+ type: "component"
38
+ });
39
+ });
40
+ test("extracts custom component name from Button.tsx", () => {
41
+ const element = document.createElement("div");
42
+ element.setAttribute("data-locatorjs", "/home/project/src/components/Button.tsx:15:5");
43
+ const result = collectNextjsServerComponents(element);
44
+ expect(result).toHaveLength(1);
45
+ expect(result[0]).toEqual({
46
+ name: "Button",
47
+ filePath: "src/components/Button.tsx",
48
+ line: 15,
49
+ type: "component"
50
+ });
51
+ });
52
+ test("returns empty array when element has no data-locatorjs attribute", () => {
53
+ const element = document.createElement("div");
54
+ const result = collectNextjsServerComponents(element);
55
+ expect(result).toEqual([]);
56
+ });
57
+ test("handles nested layout files", () => {
58
+ const element = document.createElement("div");
59
+ element.setAttribute("data-locatorjs", "/workspace/app/dashboard/layout.tsx:3:2");
60
+ const result = collectNextjsServerComponents(element);
61
+ expect(result).toHaveLength(1);
62
+ expect(result[0].name).toBe("RootLayout");
63
+ });
64
+ test("handles TypeScript file extension", () => {
65
+ const element = document.createElement("div");
66
+ element.setAttribute("data-locatorjs", "/home/project/app/layout.ts:10:1");
67
+ const result = collectNextjsServerComponents(element);
68
+ expect(result).toHaveLength(1);
69
+ expect(result[0]).toEqual({
70
+ name: "RootLayout",
71
+ filePath: "app/layout.ts",
72
+ line: 10,
73
+ type: "component"
74
+ });
75
+ });
76
+ test("handles JSX file extension", () => {
77
+ const element = document.createElement("div");
78
+ element.setAttribute("data-locatorjs", "/home/project/app/page.jsx:5:1");
79
+ const result = collectNextjsServerComponents(element);
80
+ expect(result).toHaveLength(1);
81
+ expect(result[0].name).toBe("Page");
82
+ });
83
+ test("handles JavaScript file extension", () => {
84
+ const element = document.createElement("div");
85
+ element.setAttribute("data-locatorjs", "/home/project/app/layout.js:5:1");
86
+ const result = collectNextjsServerComponents(element);
87
+ expect(result).toHaveLength(1);
88
+ expect(result[0].name).toBe("RootLayout");
89
+ });
90
+ test("parses line and column numbers correctly", () => {
91
+ const element = document.createElement("div");
92
+ element.setAttribute("data-locatorjs", "/path/app/page.tsx:123:45");
93
+ const result = collectNextjsServerComponents(element);
94
+ expect(result[0].line).toBe(123);
95
+ });
96
+ test("handles empty data-locatorjs attribute", () => {
97
+ const element = document.createElement("div");
98
+ element.setAttribute("data-locatorjs", "");
99
+ const result = collectNextjsServerComponents(element);
100
+ expect(result).toEqual([]);
101
+ });
102
+ test("handles malformed data-locatorjs attribute with missing colons", () => {
103
+ const element = document.createElement("div");
104
+ element.setAttribute("data-locatorjs", "/path/to/file.tsx");
105
+ const result = collectNextjsServerComponents(element);
106
+ expect(result).toEqual([]);
107
+ });
108
+ test("handles malformed data-locatorjs attribute with non-numeric line", () => {
109
+ const element = document.createElement("div");
110
+ element.setAttribute("data-locatorjs", "/path/to/file.tsx:abc:5");
111
+ const result = collectNextjsServerComponents(element);
112
+
113
+ // parseInt("abc", 10) returns NaN, but the function still returns a component
114
+ // This test validates current behavior - the parser doesn't validate numeric values
115
+ expect(result).toHaveLength(1);
116
+ expect(Number.isNaN(result[0].line)).toBe(true);
117
+ });
118
+ });
119
+ describe("parseNextjsServerComponents", () => {
120
+ test("returns server component info when data-locatorjs present", () => {
121
+ const element = document.createElement("div");
122
+ element.setAttribute("data-locatorjs", "/apps/next-16/app/page.tsx:5:4");
123
+ const result = parseNextjsServerComponents(element);
124
+ expect(result).not.toBeNull();
125
+ expect(result).toHaveLength(1);
126
+ expect(result[0].name).toBe("Page");
127
+ });
128
+ test("returns null when element has no data-locatorjs attribute", () => {
129
+ const element = document.createElement("div");
130
+ const result = parseNextjsServerComponents(element);
131
+ expect(result).toBeNull();
132
+ });
133
+ test("returns null when data-locatorjs is empty", () => {
134
+ const element = document.createElement("div");
135
+ element.setAttribute("data-locatorjs", "");
136
+ const result = parseNextjsServerComponents(element);
137
+ expect(result).toBeNull();
138
+ });
139
+ test("returns null when data-locatorjs is malformed", () => {
140
+ const element = document.createElement("div");
141
+ element.setAttribute("data-locatorjs", "/path/to/file.tsx");
142
+ const result = parseNextjsServerComponents(element);
143
+
144
+ // Parser returns null for malformed attributes (less than 2 parts after split)
145
+ expect(result).toBeNull();
146
+ });
147
+ test("returns correct server component info from parseNextjsServerComponents", () => {
148
+ const element = document.createElement("div");
149
+ element.setAttribute("data-locatorjs", "/workspace/app/dashboard/layout.tsx:42:10");
150
+ const result = parseNextjsServerComponents(element);
151
+ expect(result).toEqual([{
152
+ name: "RootLayout",
153
+ filePath: "app/dashboard/layout.tsx",
154
+ line: 42,
155
+ type: "component"
156
+ }]);
157
+ });
158
+ });
@@ -1,2 +1,2 @@
1
1
  import { Fiber } from "@locator/shared";
2
- export declare function findFiberByHtmlElement(target: HTMLElement, shouldHaveDebugSource: boolean): Fiber | null;
2
+ export declare function findFiberByHtmlElement(target: HTMLElement | SVGElement, shouldHaveDebugSource: boolean): Fiber | null;
@@ -7,7 +7,7 @@ import { findDebugSource } from "./findDebugSource";
7
7
  function findFiberFromDOMElement(element) {
8
8
  const fiberKey = Object.keys(element).find(k => k.startsWith("__reactFiber$"));
9
9
  if (fiberKey) {
10
- return element[fiberKey];
10
+ return element[fiberKey] ?? null;
11
11
  }
12
12
  return null;
13
13
  }
@@ -1,3 +1,5 @@
1
+ /* eslint-disable no-console */
2
+
1
3
  import { getUsableName } from "../../functions/getUsableName";
2
4
  import { mergeRects } from "../../functions/mergeRects";
3
5
  import { getFiberComponentBoundingBox } from "./getFiberComponentBoundingBox";
@@ -6,6 +8,7 @@ export function getAllParentsElementsAndRootComponent(fiber) {
6
8
  const parentElements = [];
7
9
  const deepestElement = fiber.stateNode;
8
10
  if (!deepestElement || !(deepestElement instanceof Element)) {
11
+ // eslint-disable-next-line no-console
9
12
  console.warn("[TreeLocator] Skipping fiber with non-Element stateNode:", fiber.type, fiber.stateNode);
10
13
  return null;
11
14
  }
@@ -31,6 +34,7 @@ export function getAllParentsElementsAndRootComponent(fiber) {
31
34
  link: null
32
35
  });
33
36
  }
37
+ // eslint-disable-next-line no-console
34
38
  console.warn("[TreeLocator] Could not find root component for fiber:", fiber.type);
35
39
  return null;
36
40
  }
@@ -3,6 +3,6 @@ import type { AdapterId } from "../consts";
3
3
  /**
4
4
  * Resolve the framework adapter to use.
5
5
  * If an explicit adapterId is given, return that adapter.
6
- * Otherwise, auto-detect the framework in priority order.
6
+ * Otherwise, auto-detect the framework.
7
7
  */
8
8
  export declare function resolveAdapter(adapterId?: AdapterId): AdapterObject | null;