@treelocator/runtime 0.5.2 → 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 (163) 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/functions/cssRuleInspector.d.ts +83 -0
  41. package/dist/functions/cssRuleInspector.js +608 -0
  42. package/dist/functions/cssRuleInspector.test.d.ts +1 -0
  43. package/dist/functions/cssRuleInspector.test.js +439 -0
  44. package/dist/functions/deduplicateLabels.test.d.ts +1 -0
  45. package/dist/functions/deduplicateLabels.test.js +178 -0
  46. package/dist/functions/enrichAncestrySourceMaps.js +0 -1
  47. package/dist/functions/extractComputedStyles.d.ts +51 -0
  48. package/dist/functions/extractComputedStyles.js +447 -0
  49. package/dist/functions/extractComputedStyles.test.d.ts +1 -0
  50. package/dist/functions/extractComputedStyles.test.js +549 -0
  51. package/dist/functions/formatAncestryChain.d.ts +8 -0
  52. package/dist/functions/formatAncestryChain.js +21 -1
  53. package/dist/functions/formatAncestryChain.test.js +18 -0
  54. package/dist/functions/getUsableName.test.d.ts +1 -0
  55. package/dist/functions/getUsableName.test.js +219 -0
  56. package/dist/functions/isCombinationModifiersPressed.test.d.ts +1 -0
  57. package/dist/functions/isCombinationModifiersPressed.test.js +192 -0
  58. package/dist/functions/mergeRects.test.js +210 -1
  59. package/dist/functions/namedSnapshots.d.ts +52 -0
  60. package/dist/functions/namedSnapshots.js +161 -0
  61. package/dist/functions/namedSnapshots.test.d.ts +1 -0
  62. package/dist/functions/namedSnapshots.test.js +85 -0
  63. package/dist/functions/normalizeFilePath.test.d.ts +1 -0
  64. package/dist/functions/normalizeFilePath.test.js +66 -0
  65. package/dist/functions/parseDataId.test.d.ts +1 -0
  66. package/dist/functions/parseDataId.test.js +101 -0
  67. package/dist/hooks/getStorage.d.ts +3 -0
  68. package/dist/hooks/getStorage.js +17 -0
  69. package/dist/hooks/useEventListeners.d.ts +15 -0
  70. package/dist/hooks/useEventListeners.js +56 -0
  71. package/dist/hooks/useLocatorStorage.d.ts +18 -0
  72. package/dist/hooks/useLocatorStorage.js +41 -0
  73. package/dist/hooks/useLocatorStorage.test.d.ts +1 -0
  74. package/dist/hooks/useLocatorStorage.test.js +124 -0
  75. package/dist/hooks/useRecordingState.d.ts +43 -0
  76. package/dist/hooks/useRecordingState.js +387 -0
  77. package/dist/hooks/useSettings.d.ts +13 -0
  78. package/dist/hooks/useSettings.js +66 -0
  79. package/dist/index.d.ts +5 -2
  80. package/dist/index.js +4 -2
  81. package/dist/initRuntime.d.ts +3 -1
  82. package/dist/initRuntime.js +4 -1
  83. package/dist/mcpBridge.d.ts +61 -0
  84. package/dist/mcpBridge.js +534 -0
  85. package/dist/mcpBridge.test.d.ts +1 -0
  86. package/dist/mcpBridge.test.js +248 -0
  87. package/dist/output.css +20 -0
  88. package/dist/visualDiff/diff.d.ts +9 -0
  89. package/dist/visualDiff/diff.js +209 -0
  90. package/dist/visualDiff/diff.test.d.ts +1 -0
  91. package/dist/visualDiff/diff.test.js +253 -0
  92. package/dist/visualDiff/settle.d.ts +3 -0
  93. package/dist/visualDiff/settle.js +50 -0
  94. package/dist/visualDiff/settle.test.d.ts +1 -0
  95. package/dist/visualDiff/settle.test.js +65 -0
  96. package/dist/visualDiff/snapshot.d.ts +4 -0
  97. package/dist/visualDiff/snapshot.js +84 -0
  98. package/dist/visualDiff/snapshot.test.d.ts +1 -0
  99. package/dist/visualDiff/snapshot.test.js +245 -0
  100. package/dist/visualDiff/types.d.ts +37 -0
  101. package/dist/visualDiff/types.js +1 -0
  102. package/package.json +2 -2
  103. package/scripts/wrapCSS.js +1 -1
  104. package/scripts/wrapImage.js +1 -1
  105. package/src/_generated_styles.ts +21 -1
  106. package/src/_generated_tree_icon.ts +1 -1
  107. package/src/adapters/HtmlElementTreeNode.ts +10 -7
  108. package/src/adapters/createTreeNode.ts +12 -51
  109. package/src/adapters/detectFramework.test.ts +73 -0
  110. package/src/adapters/detectFramework.ts +28 -0
  111. package/src/adapters/jsx/jsxAdapter.test.ts +240 -0
  112. package/src/adapters/jsx/jsxAdapter.ts +53 -106
  113. package/src/adapters/nextjs/parseNextjsDataAttributes.test.ts +212 -0
  114. package/src/adapters/nextjs/parseNextjsDataAttributes.ts +1 -1
  115. package/src/adapters/react/findDebugSource.ts +5 -6
  116. package/src/adapters/react/findFiberByHtmlElement.ts +3 -3
  117. package/src/adapters/react/getAllParentsElementsAndRootComponent.ts +3 -0
  118. package/src/adapters/react/reactAdapter.ts +1 -2
  119. package/src/adapters/resolveAdapter.ts +4 -14
  120. package/src/adapters/svelte/svelteAdapter.test.ts +334 -0
  121. package/src/adapters/vue/vueAdapter.test.ts +259 -0
  122. package/src/browserApi.test.ts +329 -0
  123. package/src/browserApi.ts +351 -4
  124. package/src/components/RecordingPillButton.tsx +301 -0
  125. package/src/components/RecordingResults.tsx +114 -13
  126. package/src/components/Runtime.tsx +176 -621
  127. package/src/components/SettingsPanel.tsx +339 -0
  128. package/src/consoleCapture.ts +113 -0
  129. package/src/functions/cssRuleInspector.test.ts +517 -0
  130. package/src/functions/cssRuleInspector.ts +708 -0
  131. package/src/functions/deduplicateLabels.test.ts +115 -0
  132. package/src/functions/enrichAncestrySourceMaps.ts +6 -3
  133. package/src/functions/extractComputedStyles.test.ts +681 -0
  134. package/src/functions/extractComputedStyles.ts +768 -0
  135. package/src/functions/formatAncestryChain.test.ts +23 -1
  136. package/src/functions/formatAncestryChain.ts +22 -1
  137. package/src/functions/getUsableName.test.ts +242 -0
  138. package/src/functions/isCombinationModifiersPressed.test.ts +156 -0
  139. package/src/functions/mergeRects.test.ts +111 -1
  140. package/src/functions/namedSnapshots.test.ts +106 -0
  141. package/src/functions/namedSnapshots.ts +232 -0
  142. package/src/functions/normalizeFilePath.test.ts +80 -0
  143. package/src/functions/parseDataId.test.ts +125 -0
  144. package/src/hooks/getStorage.ts +26 -0
  145. package/src/hooks/useEventListeners.ts +97 -0
  146. package/src/hooks/useLocatorStorage.test.ts +127 -0
  147. package/src/hooks/useLocatorStorage.ts +60 -0
  148. package/src/hooks/useRecordingState.ts +516 -0
  149. package/src/hooks/useSettings.ts +83 -0
  150. package/src/index.ts +10 -5
  151. package/src/initRuntime.ts +5 -0
  152. package/src/mcpBridge.test.ts +260 -0
  153. package/src/mcpBridge.ts +677 -0
  154. package/src/visualDiff/diff.test.ts +167 -0
  155. package/src/visualDiff/diff.ts +242 -0
  156. package/src/visualDiff/settle.test.ts +77 -0
  157. package/src/visualDiff/settle.ts +62 -0
  158. package/src/visualDiff/snapshot.test.ts +200 -0
  159. package/src/visualDiff/snapshot.ts +119 -0
  160. package/src/visualDiff/types.ts +40 -0
  161. package/tsconfig.json +3 -1
  162. package/vitest.config.ts +18 -0
  163. package/jest.config.ts +0 -195
@@ -0,0 +1,73 @@
1
+ import { describe, expect, test, vi, beforeEach } from "vitest";
2
+ import { detectFramework } from "./detectFramework";
3
+
4
+ vi.mock("@locator/shared", () => ({
5
+ detectSvelte: vi.fn(() => false),
6
+ detectVue: vi.fn(() => false),
7
+ detectReact: vi.fn(() => false),
8
+ detectJSX: vi.fn(() => false),
9
+ }));
10
+
11
+ vi.mock("./phoenix/detectPhoenix", () => ({
12
+ detectPhoenix: vi.fn(() => false),
13
+ }));
14
+
15
+ import { detectSvelte, detectVue, detectReact, detectJSX } from "@locator/shared";
16
+ import { detectPhoenix } from "./phoenix/detectPhoenix";
17
+
18
+ beforeEach(() => {
19
+ vi.mocked(detectSvelte).mockReturnValue(false);
20
+ vi.mocked(detectVue).mockReturnValue(false);
21
+ vi.mocked(detectReact).mockReturnValue(false);
22
+ vi.mocked(detectJSX).mockReturnValue(false);
23
+ vi.mocked(detectPhoenix).mockReturnValue(false);
24
+ });
25
+
26
+ describe("detectFramework", () => {
27
+ test("returns null when all detect functions return false and no element", () => {
28
+ expect(detectFramework()).toBe(null);
29
+ });
30
+
31
+ test("returns 'svelte' when detectSvelte returns true", () => {
32
+ vi.mocked(detectSvelte).mockReturnValue(true);
33
+ expect(detectFramework()).toBe("svelte");
34
+ });
35
+
36
+ test("returns 'vue' when detectVue returns true", () => {
37
+ vi.mocked(detectVue).mockReturnValue(true);
38
+ expect(detectFramework()).toBe("vue");
39
+ });
40
+
41
+ test("returns 'react' when detectReact returns true", () => {
42
+ vi.mocked(detectReact).mockReturnValue(true);
43
+ expect(detectFramework()).toBe("react");
44
+ });
45
+
46
+ test("returns 'jsx' when detectJSX returns true", () => {
47
+ vi.mocked(detectJSX).mockReturnValue(true);
48
+ expect(detectFramework()).toBe("jsx");
49
+ });
50
+
51
+ test("returns 'jsx' when detectPhoenix returns true", () => {
52
+ vi.mocked(detectPhoenix).mockReturnValue(true);
53
+ expect(detectFramework()).toBe("jsx");
54
+ });
55
+
56
+ test("returns 'react' when element has __reactFiber$ key even when detectReact returns false", () => {
57
+ const element = document.createElement("div");
58
+ (element as any)["__reactFiber$abc123"] = {};
59
+ expect(detectFramework(element)).toBe("react");
60
+ });
61
+
62
+ test("returns 'jsx' when element has dataset.locatorjsId", () => {
63
+ const element = document.createElement("div");
64
+ element.dataset.locatorjsId = "some-id";
65
+ expect(detectFramework(element)).toBe("jsx");
66
+ });
67
+
68
+ test("returns 'svelte' over 'react' when both detectSvelte and detectReact return true", () => {
69
+ vi.mocked(detectSvelte).mockReturnValue(true);
70
+ vi.mocked(detectReact).mockReturnValue(true);
71
+ expect(detectFramework()).toBe("svelte");
72
+ });
73
+ });
@@ -0,0 +1,28 @@
1
+ import { detectJSX, detectReact, detectSvelte, detectVue } from "@locator/shared";
2
+ import { detectPhoenix } from "./phoenix/detectPhoenix";
3
+
4
+ export type FrameworkId = "svelte" | "vue" | "react" | "jsx" | null;
5
+
6
+ /**
7
+ * Check if a DOM element has __reactFiber$ keys.
8
+ * Works without React DevTools extension.
9
+ */
10
+ function hasReactFiberKeys(element?: HTMLElement): boolean {
11
+ if (!element) return false;
12
+ return Object.keys(element).some((k) => k.startsWith("__reactFiber$"));
13
+ }
14
+
15
+ /**
16
+ * Detect the active framework, optionally considering element-level hints.
17
+ *
18
+ * Priority order: Svelte > Vue > React > JSX > Phoenix (uses JSX adapter)
19
+ * JSX must be last because global data can leak from the LocatorJS extension.
20
+ */
21
+ export function detectFramework(element?: HTMLElement): FrameworkId {
22
+ if (detectSvelte()) return "svelte";
23
+ if (detectVue()) return "vue";
24
+ if (detectReact() || hasReactFiberKeys(element)) return "react";
25
+ if (detectJSX() || (element && element.dataset.locatorjsId)) return "jsx";
26
+ if (detectPhoenix()) return "jsx";
27
+ return null;
28
+ }
@@ -0,0 +1,240 @@
1
+ import { afterEach, describe, expect, test } from "vitest";
2
+ import { getElementInfo, JSXTreeNodeElement } from "./jsxAdapter";
3
+
4
+ function makeElement(attrs: Record<string, string> = {}): HTMLElement {
5
+ const el = document.createElement("div");
6
+ for (const [k, v] of Object.entries(attrs)) {
7
+ el.setAttribute(k, v);
8
+ }
9
+ return el;
10
+ }
11
+
12
+ afterEach(() => {
13
+ delete (window as any).__LOCATOR_DATA__;
14
+ });
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // getElementInfo
18
+ // ---------------------------------------------------------------------------
19
+
20
+ describe("getElementInfo", () => {
21
+ test("returns null when element has no data-locatorjs attributes", () => {
22
+ const el = makeElement({ class: "foo" });
23
+ expect(getElementInfo(el)).toBeNull();
24
+ });
25
+
26
+ test("returns FullElementInfo from data-locatorjs path attribute (no __LOCATOR_DATA__)", () => {
27
+ // Format: /project/src/Button.tsx:10:5
28
+ const el = makeElement({ "data-locatorjs": "/project/src/Button.tsx:10:5" });
29
+ document.body.appendChild(el);
30
+
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
+
43
+ document.body.removeChild(el);
44
+ });
45
+
46
+ test("returns FullElementInfo using fileData from __LOCATOR_DATA__ when data-locatorjs-id is set", () => {
47
+ const fileFullPath = "/project/src/Button.tsx";
48
+ const fileData = {
49
+ filePath: "/src/Button.tsx",
50
+ projectPath: "/project",
51
+ expressions: [
52
+ {
53
+ name: "Button",
54
+ loc: { start: { line: 5, column: 2 }, end: { line: 5, column: 10 } },
55
+ wrappingComponentId: null,
56
+ },
57
+ ],
58
+ components: [],
59
+ styledDefinitions: [],
60
+ };
61
+ (window as any).__LOCATOR_DATA__ = { [fileFullPath]: fileData };
62
+
63
+ const el = makeElement({ "data-locatorjs-id": `${fileFullPath}::0` });
64
+ document.body.appendChild(el);
65
+
66
+ const info = getElementInfo(el);
67
+ expect(info).not.toBeNull();
68
+ expect(info!.thisElement.link).toMatchObject({
69
+ filePath: "/src/Button.tsx",
70
+ projectPath: "/project",
71
+ line: 5,
72
+ column: 3,
73
+ });
74
+ expect(info!.thisElement.label).toBe("Button");
75
+
76
+ document.body.removeChild(el);
77
+ });
78
+
79
+ test("finds attributes on nearest ancestor via closest()", () => {
80
+ const parent = makeElement({
81
+ "data-locatorjs": "/project/src/Layout.tsx:20:0",
82
+ });
83
+ const child = makeElement();
84
+ parent.appendChild(child);
85
+ document.body.appendChild(parent);
86
+
87
+ const info = getElementInfo(child);
88
+ expect(info).not.toBeNull();
89
+ expect(info!.thisElement.link).toMatchObject({
90
+ line: 20,
91
+ column: 1,
92
+ });
93
+ expect(info!.htmlElement).toBe(parent);
94
+
95
+ document.body.removeChild(parent);
96
+ });
97
+
98
+ test("returns null when data-locatorjs attribute has malformed path (only one colon)", () => {
99
+ // parseDataPath returns null for a path with only one colon
100
+ const el = makeElement({ "data-locatorjs": "/project/src/Button.tsx:10" });
101
+ document.body.appendChild(el);
102
+ const info = getElementInfo(el);
103
+ expect(info).toBeNull();
104
+ document.body.removeChild(el);
105
+ });
106
+ });
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // JSXTreeNodeElement.getSource
110
+ // ---------------------------------------------------------------------------
111
+
112
+ describe("JSXTreeNodeElement.getSource", () => {
113
+ test("returns null when element has no data-locatorjs attributes", () => {
114
+ const el = makeElement();
115
+ const node = new JSXTreeNodeElement(el);
116
+ expect(node.getSource()).toBeNull();
117
+ });
118
+
119
+ test("returns Source from data-locatorjs attribute without __LOCATOR_DATA__", () => {
120
+ const el = makeElement({ "data-locatorjs": "/project/src/App.tsx:15:3" });
121
+ const node = new JSXTreeNodeElement(el);
122
+
123
+ const source = node.getSource();
124
+ expect(source).not.toBeNull();
125
+ expect(source).toMatchObject({
126
+ fileName: "/src/App.tsx",
127
+ projectPath: "/project",
128
+ lineNumber: 15,
129
+ columnNumber: 4, // column + 1
130
+ });
131
+ });
132
+
133
+ test("returns Source using fileData from __LOCATOR_DATA__ when data-locatorjs-id is set", () => {
134
+ const fileFullPath = "/project/src/App.tsx";
135
+ const fileData = {
136
+ filePath: "/src/App.tsx",
137
+ projectPath: "/project",
138
+ expressions: [
139
+ {
140
+ name: "App",
141
+ loc: { start: { line: 8, column: 4 }, end: { line: 8, column: 12 } },
142
+ wrappingComponentId: null,
143
+ },
144
+ ],
145
+ components: [],
146
+ styledDefinitions: [],
147
+ };
148
+ (window as any).__LOCATOR_DATA__ = { [fileFullPath]: fileData };
149
+
150
+ const el = makeElement({ "data-locatorjs-id": `${fileFullPath}::0` });
151
+ const node = new JSXTreeNodeElement(el);
152
+
153
+ const source = node.getSource();
154
+ expect(source).not.toBeNull();
155
+ expect(source).toMatchObject({
156
+ fileName: "/src/App.tsx",
157
+ projectPath: "/project",
158
+ lineNumber: 8,
159
+ columnNumber: 5,
160
+ });
161
+ });
162
+ });
163
+
164
+ // ---------------------------------------------------------------------------
165
+ // JSXTreeNodeElement.getComponent
166
+ // ---------------------------------------------------------------------------
167
+
168
+ describe("JSXTreeNodeElement.getComponent", () => {
169
+ test("returns null when element has no data-locatorjs attributes", () => {
170
+ const el = makeElement();
171
+ const node = new JSXTreeNodeElement(el);
172
+ expect(node.getComponent()).toBeNull();
173
+ });
174
+
175
+ test("returns null when __LOCATOR_DATA__ is not set (no fileData)", () => {
176
+ const el = makeElement({ "data-locatorjs": "/project/src/Card.tsx:5:0" });
177
+ const node = new JSXTreeNodeElement(el);
178
+ expect(node.getComponent()).toBeNull();
179
+ });
180
+
181
+ test("returns null when expression has no wrappingComponentId", () => {
182
+ const fileFullPath = "/project/src/Card.tsx";
183
+ (window as any).__LOCATOR_DATA__ = {
184
+ [fileFullPath]: {
185
+ filePath: "/src/Card.tsx",
186
+ projectPath: "/project",
187
+ expressions: [
188
+ {
189
+ name: "div",
190
+ loc: { start: { line: 3, column: 0 }, end: { line: 3, column: 5 } },
191
+ wrappingComponentId: null,
192
+ },
193
+ ],
194
+ components: [],
195
+ styledDefinitions: [],
196
+ },
197
+ };
198
+
199
+ const el = makeElement({ "data-locatorjs-id": `${fileFullPath}::0` });
200
+ const node = new JSXTreeNodeElement(el);
201
+ expect(node.getComponent()).toBeNull();
202
+ });
203
+
204
+ test("returns TreeNodeComponent when fileData and wrappingComponentId are present", () => {
205
+ const fileFullPath = "/project/src/Form.tsx";
206
+ (window as any).__LOCATOR_DATA__ = {
207
+ [fileFullPath]: {
208
+ filePath: "/src/Form.tsx",
209
+ projectPath: "/project",
210
+ expressions: [
211
+ {
212
+ name: "input",
213
+ loc: { start: { line: 12, column: 4 }, end: { line: 12, column: 12 } },
214
+ wrappingComponentId: 0,
215
+ },
216
+ ],
217
+ components: [
218
+ {
219
+ name: "Form",
220
+ loc: { start: { line: 1, column: 0 }, end: { line: 20, column: 1 } },
221
+ },
222
+ ],
223
+ styledDefinitions: [],
224
+ },
225
+ };
226
+
227
+ const el = makeElement({ "data-locatorjs-id": `${fileFullPath}::0` });
228
+ const node = new JSXTreeNodeElement(el);
229
+
230
+ const component = node.getComponent();
231
+ expect(component).not.toBeNull();
232
+ expect(component!.label).toBe("Form");
233
+ expect(component!.definitionLink).toMatchObject({
234
+ fileName: "/src/Form.tsx",
235
+ projectPath: "/project",
236
+ lineNumber: 1,
237
+ columnNumber: 1,
238
+ });
239
+ });
240
+ });
@@ -17,58 +17,68 @@ import { HtmlElementTreeNode } from "../HtmlElementTreeNode";
17
17
  import { getExpressionData } from "./getExpressionData";
18
18
  import { getJSXComponentBoundingBox } from "./getJSXComponentBoundingBox";
19
19
 
20
+ type JSXLocatorData = {
21
+ fileFullPath: string;
22
+ fileData: FileStorage | undefined;
23
+ filePath: string;
24
+ projectPath: string;
25
+ };
26
+
27
+ function resolveJSXLocatorData(element: Element): JSXLocatorData | null {
28
+ const dataId = element.getAttribute("data-locatorjs-id");
29
+ const dataPath = element.getAttribute("data-locatorjs");
30
+
31
+ if (!dataId && !dataPath) return null;
32
+
33
+ let fileFullPath: string;
34
+
35
+ if (dataPath) {
36
+ const parsed = parseDataPath(dataPath);
37
+ if (!parsed) return null;
38
+ [fileFullPath] = parsed;
39
+ } else if (dataId) {
40
+ [fileFullPath] = parseDataId(dataId);
41
+ } else {
42
+ return null;
43
+ }
44
+
45
+ const locatorData = window.__LOCATOR_DATA__;
46
+ const fileData: FileStorage | undefined = locatorData?.[fileFullPath];
47
+
48
+ let filePath: string;
49
+ let projectPath: string;
50
+
51
+ if (fileData) {
52
+ filePath = fileData.filePath;
53
+ projectPath = fileData.projectPath;
54
+ } else {
55
+ [projectPath, filePath] = splitFullPath(fileFullPath);
56
+ }
57
+
58
+ return { fileFullPath, fileData, filePath, projectPath };
59
+ }
60
+
20
61
  export function getElementInfo(target: HTMLElement): FullElementInfo | null {
21
62
  const found = target.closest("[data-locatorjs-id], [data-locatorjs]");
22
63
 
23
64
  // Support both HTMLElement and SVGElement
24
65
  // SVG elements don't have dataset, so use getAttribute instead
25
- const dataId = found?.getAttribute("data-locatorjs-id");
26
- const dataPath = found?.getAttribute("data-locatorjs");
27
66
  const styledDataId = found?.getAttribute("data-locatorjs-styled");
28
67
 
29
68
  if (
30
69
  found &&
31
- (found instanceof HTMLElement || found instanceof SVGElement) &&
32
- (dataId || dataPath || styledDataId)
70
+ (found instanceof HTMLElement || found instanceof SVGElement)
33
71
  ) {
34
-
35
- if (!dataId && !dataPath) {
72
+ const resolved = resolveJSXLocatorData(found);
73
+ if (!resolved && !styledDataId) {
36
74
  return null;
37
75
  }
38
-
39
- let fileFullPath: string;
40
-
41
- if (dataPath) {
42
- const parsed = parseDataPath(dataPath);
43
- if (!parsed) {
44
- return null;
45
- }
46
- [fileFullPath] = parsed;
47
- } else if (dataId) {
48
- [fileFullPath] = parseDataId(dataId);
49
- } else {
76
+ if (!resolved) {
50
77
  return null;
51
78
  }
52
79
 
80
+ const { fileFullPath, fileData, filePath, projectPath } = resolved;
53
81
  const locatorData = window.__LOCATOR_DATA__;
54
- const fileData: FileStorage | undefined = locatorData?.[fileFullPath];
55
-
56
- // Handle styled components (only when locatorData is available)
57
- const [styledFileFullPath, styledId] = styledDataId
58
- ? parseDataId(styledDataId)
59
- : [null, null];
60
- const styledFileData: FileStorage | undefined =
61
- styledFileFullPath && locatorData?.[styledFileFullPath];
62
- const styledExpData =
63
- styledFileData && styledFileData.styledDefinitions[Number(styledId)];
64
-
65
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
66
- const styledLink = styledExpData && {
67
- filePath: styledFileData.filePath,
68
- projectPath: styledFileData.projectPath,
69
- column: (styledExpData.loc?.start.column || 0) + 1,
70
- line: styledExpData.loc?.start.line || 0,
71
- };
72
82
 
73
83
  // Get expression data (works with or without locatorData)
74
84
  const expData = getExpressionData(found, fileData || null);
@@ -76,18 +86,6 @@ export function getElementInfo(target: HTMLElement): FullElementInfo | null {
76
86
  return null;
77
87
  }
78
88
 
79
- // Extract file path components
80
- let filePath: string;
81
- let projectPath: string;
82
-
83
- if (fileData) {
84
- filePath = fileData.filePath;
85
- projectPath = fileData.projectPath;
86
- } else {
87
- // If no fileData, split the full path
88
- [projectPath, filePath] = splitFullPath(fileFullPath);
89
- }
90
-
91
89
  const wrappingComponent =
92
90
  expData.wrappingComponentId !== null && fileData
93
91
  ? fileData.components[Number(expData.wrappingComponentId)]
@@ -133,47 +131,16 @@ export function getElementInfo(target: HTMLElement): FullElementInfo | null {
133
131
 
134
132
  export class JSXTreeNodeElement extends HtmlElementTreeNode {
135
133
  getSource(): Source | null {
136
- // Use getAttribute instead of dataset to support both HTML and SVG elements
137
- const dataId = this.element.getAttribute("data-locatorjs-id");
138
- const dataPath = this.element.getAttribute("data-locatorjs");
139
-
140
- if (!dataId && !dataPath) {
141
- return null;
142
- }
134
+ const resolved = resolveJSXLocatorData(this.element);
135
+ if (!resolved) return null;
143
136
 
144
- let fileFullPath: string;
145
-
146
- if (dataPath) {
147
- const parsed = parseDataPath(dataPath);
148
- if (!parsed) {
149
- return null;
150
- }
151
- [fileFullPath] = parsed;
152
- } else if (dataId) {
153
- [fileFullPath] = parseDataId(dataId);
154
- } else {
155
- return null;
156
- }
157
-
158
- const locatorData = window.__LOCATOR_DATA__;
159
- const fileData: FileStorage | undefined = locatorData?.[fileFullPath];
137
+ const { fileData, filePath, projectPath } = resolved;
160
138
 
161
139
  // Get expression data (works with or without locatorData)
162
140
  const expData = getExpressionData(this.element, fileData || null);
163
141
  if (expData) {
164
- let fileName: string;
165
- let projectPath: string;
166
-
167
- if (fileData) {
168
- fileName = fileData.filePath;
169
- projectPath = fileData.projectPath;
170
- } else {
171
- // If no fileData, split the full path
172
- [projectPath, fileName] = splitFullPath(fileFullPath);
173
- }
174
-
175
142
  return {
176
- fileName,
143
+ fileName: filePath,
177
144
  projectPath,
178
145
  columnNumber: (expData.loc.start.column || 0) + 1,
179
146
  lineNumber: expData.loc.start.line || 0,
@@ -183,30 +150,10 @@ export class JSXTreeNodeElement extends HtmlElementTreeNode {
183
150
  return null;
184
151
  }
185
152
  getComponent(): TreeNodeComponent | null {
186
- // Use getAttribute instead of dataset to support both HTML and SVG elements
187
- const dataId = this.element.getAttribute("data-locatorjs-id");
188
- const dataPath = this.element.getAttribute("data-locatorjs");
189
-
190
- if (!dataId && !dataPath) {
191
- return null;
192
- }
193
-
194
- let fileFullPath: string;
153
+ const resolved = resolveJSXLocatorData(this.element);
154
+ if (!resolved) return null;
195
155
 
196
- if (dataPath) {
197
- const parsed = parseDataPath(dataPath);
198
- if (!parsed) {
199
- return null;
200
- }
201
- [fileFullPath] = parsed;
202
- } else if (dataId) {
203
- [fileFullPath] = parseDataId(dataId);
204
- } else {
205
- return null;
206
- }
207
-
208
- const locatorData = window.__LOCATOR_DATA__;
209
- const fileData: FileStorage | undefined = locatorData?.[fileFullPath];
156
+ const { fileData } = resolved;
210
157
 
211
158
  // Component information is only available when we have fileData
212
159
  if (fileData) {