@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.
- package/.eslintignore +1 -0
- package/dist/_generated_styles.d.ts +1 -1
- package/dist/_generated_styles.js +20 -0
- package/dist/_generated_tree_icon.d.ts +1 -1
- package/dist/adapters/HtmlElementTreeNode.d.ts +2 -2
- package/dist/adapters/HtmlElementTreeNode.js +4 -6
- package/dist/adapters/createTreeNode.js +17 -44
- package/dist/adapters/detectFramework.d.ts +8 -0
- package/dist/adapters/detectFramework.js +25 -0
- package/dist/adapters/detectFramework.test.d.ts +1 -0
- package/dist/adapters/detectFramework.test.js +60 -0
- package/dist/adapters/jsx/jsxAdapter.js +54 -89
- package/dist/adapters/jsx/jsxAdapter.test.d.ts +1 -0
- package/dist/adapters/jsx/jsxAdapter.test.js +273 -0
- package/dist/adapters/nextjs/parseNextjsDataAttributes.js +1 -1
- package/dist/adapters/nextjs/parseNextjsDataAttributes.test.d.ts +1 -0
- package/dist/adapters/nextjs/parseNextjsDataAttributes.test.js +158 -0
- package/dist/adapters/react/findFiberByHtmlElement.d.ts +1 -1
- package/dist/adapters/react/findFiberByHtmlElement.js +1 -1
- package/dist/adapters/react/getAllParentsElementsAndRootComponent.js +4 -0
- package/dist/adapters/resolveAdapter.d.ts +1 -1
- package/dist/adapters/resolveAdapter.js +4 -8
- package/dist/adapters/svelte/svelteAdapter.test.d.ts +1 -0
- package/dist/adapters/svelte/svelteAdapter.test.js +280 -0
- package/dist/adapters/vue/vueAdapter.test.d.ts +1 -0
- package/dist/adapters/vue/vueAdapter.test.js +222 -0
- package/dist/browserApi.d.ts +148 -0
- package/dist/browserApi.js +146 -5
- package/dist/browserApi.test.d.ts +1 -0
- package/dist/browserApi.test.js +287 -0
- package/dist/components/RecordingPillButton.d.ts +11 -0
- package/dist/components/RecordingPillButton.js +202 -0
- package/dist/components/RecordingResults.d.ts +2 -0
- package/dist/components/RecordingResults.js +213 -78
- package/dist/components/Runtime.js +161 -554
- package/dist/components/SettingsPanel.d.ts +5 -0
- package/dist/components/SettingsPanel.js +312 -0
- package/dist/consoleCapture.d.ts +9 -0
- package/dist/consoleCapture.js +95 -0
- package/dist/dejitter/recorder.d.ts +7 -1
- package/dist/dejitter/recorder.js +64 -1
- package/dist/functions/cssRuleInspector.d.ts +83 -0
- package/dist/functions/cssRuleInspector.js +608 -0
- package/dist/functions/cssRuleInspector.test.d.ts +1 -0
- package/dist/functions/cssRuleInspector.test.js +439 -0
- package/dist/functions/deduplicateLabels.test.d.ts +1 -0
- package/dist/functions/deduplicateLabels.test.js +178 -0
- package/dist/functions/enrichAncestrySourceMaps.js +0 -1
- package/dist/functions/extractComputedStyles.d.ts +51 -0
- package/dist/functions/extractComputedStyles.js +447 -0
- package/dist/functions/extractComputedStyles.test.d.ts +1 -0
- package/dist/functions/extractComputedStyles.test.js +549 -0
- package/dist/functions/formatAncestryChain.d.ts +8 -0
- package/dist/functions/formatAncestryChain.js +21 -1
- package/dist/functions/formatAncestryChain.test.js +18 -0
- package/dist/functions/getUsableName.test.d.ts +1 -0
- package/dist/functions/getUsableName.test.js +219 -0
- package/dist/functions/isCombinationModifiersPressed.test.d.ts +1 -0
- package/dist/functions/isCombinationModifiersPressed.test.js +192 -0
- package/dist/functions/mergeRects.test.js +210 -1
- package/dist/functions/namedSnapshots.d.ts +52 -0
- package/dist/functions/namedSnapshots.js +161 -0
- package/dist/functions/namedSnapshots.test.d.ts +1 -0
- package/dist/functions/namedSnapshots.test.js +85 -0
- package/dist/functions/normalizeFilePath.test.d.ts +1 -0
- package/dist/functions/normalizeFilePath.test.js +66 -0
- package/dist/functions/parseDataId.test.d.ts +1 -0
- package/dist/functions/parseDataId.test.js +101 -0
- package/dist/hooks/getStorage.d.ts +3 -0
- package/dist/hooks/getStorage.js +17 -0
- package/dist/hooks/useEventListeners.d.ts +15 -0
- package/dist/hooks/useEventListeners.js +56 -0
- package/dist/hooks/useLocatorStorage.d.ts +18 -0
- package/dist/hooks/useLocatorStorage.js +41 -0
- package/dist/hooks/useLocatorStorage.test.d.ts +1 -0
- package/dist/hooks/useLocatorStorage.test.js +124 -0
- package/dist/hooks/useRecordingState.d.ts +43 -0
- package/dist/hooks/useRecordingState.js +387 -0
- package/dist/hooks/useSettings.d.ts +13 -0
- package/dist/hooks/useSettings.js +66 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +4 -2
- package/dist/initRuntime.d.ts +3 -1
- package/dist/initRuntime.js +4 -1
- package/dist/mcpBridge.d.ts +61 -0
- package/dist/mcpBridge.js +534 -0
- package/dist/mcpBridge.test.d.ts +1 -0
- package/dist/mcpBridge.test.js +248 -0
- package/dist/output.css +20 -0
- package/dist/visualDiff/diff.d.ts +9 -0
- package/dist/visualDiff/diff.js +209 -0
- package/dist/visualDiff/diff.test.d.ts +1 -0
- package/dist/visualDiff/diff.test.js +253 -0
- package/dist/visualDiff/settle.d.ts +3 -0
- package/dist/visualDiff/settle.js +50 -0
- package/dist/visualDiff/settle.test.d.ts +1 -0
- package/dist/visualDiff/settle.test.js +65 -0
- package/dist/visualDiff/snapshot.d.ts +4 -0
- package/dist/visualDiff/snapshot.js +84 -0
- package/dist/visualDiff/snapshot.test.d.ts +1 -0
- package/dist/visualDiff/snapshot.test.js +245 -0
- package/dist/visualDiff/types.d.ts +37 -0
- package/dist/visualDiff/types.js +1 -0
- package/package.json +2 -2
- package/scripts/wrapCSS.js +1 -1
- package/scripts/wrapImage.js +1 -1
- package/src/_generated_styles.ts +21 -1
- package/src/_generated_tree_icon.ts +1 -1
- package/src/adapters/HtmlElementTreeNode.ts +10 -7
- package/src/adapters/createTreeNode.ts +12 -51
- package/src/adapters/detectFramework.test.ts +73 -0
- package/src/adapters/detectFramework.ts +28 -0
- package/src/adapters/jsx/jsxAdapter.test.ts +240 -0
- package/src/adapters/jsx/jsxAdapter.ts +53 -106
- package/src/adapters/nextjs/parseNextjsDataAttributes.test.ts +212 -0
- package/src/adapters/nextjs/parseNextjsDataAttributes.ts +1 -1
- package/src/adapters/react/findDebugSource.ts +5 -6
- package/src/adapters/react/findFiberByHtmlElement.ts +3 -3
- package/src/adapters/react/getAllParentsElementsAndRootComponent.ts +3 -0
- package/src/adapters/react/reactAdapter.ts +1 -2
- package/src/adapters/resolveAdapter.ts +4 -14
- package/src/adapters/svelte/svelteAdapter.test.ts +334 -0
- package/src/adapters/vue/vueAdapter.test.ts +259 -0
- package/src/browserApi.test.ts +329 -0
- package/src/browserApi.ts +351 -4
- package/src/components/RecordingPillButton.tsx +301 -0
- package/src/components/RecordingResults.tsx +114 -13
- package/src/components/Runtime.tsx +176 -621
- package/src/components/SettingsPanel.tsx +339 -0
- package/src/consoleCapture.ts +113 -0
- package/src/dejitter/recorder.ts +67 -3
- package/src/functions/cssRuleInspector.test.ts +517 -0
- package/src/functions/cssRuleInspector.ts +708 -0
- package/src/functions/deduplicateLabels.test.ts +115 -0
- package/src/functions/enrichAncestrySourceMaps.ts +6 -3
- package/src/functions/extractComputedStyles.test.ts +681 -0
- package/src/functions/extractComputedStyles.ts +768 -0
- package/src/functions/formatAncestryChain.test.ts +23 -1
- package/src/functions/formatAncestryChain.ts +22 -1
- package/src/functions/getUsableName.test.ts +242 -0
- package/src/functions/isCombinationModifiersPressed.test.ts +156 -0
- package/src/functions/mergeRects.test.ts +111 -1
- package/src/functions/namedSnapshots.test.ts +106 -0
- package/src/functions/namedSnapshots.ts +232 -0
- package/src/functions/normalizeFilePath.test.ts +80 -0
- package/src/functions/parseDataId.test.ts +125 -0
- package/src/hooks/getStorage.ts +26 -0
- package/src/hooks/useEventListeners.ts +97 -0
- package/src/hooks/useLocatorStorage.test.ts +127 -0
- package/src/hooks/useLocatorStorage.ts +60 -0
- package/src/hooks/useRecordingState.ts +516 -0
- package/src/hooks/useSettings.ts +83 -0
- package/src/index.ts +10 -5
- package/src/initRuntime.ts +5 -0
- package/src/mcpBridge.test.ts +260 -0
- package/src/mcpBridge.ts +677 -0
- package/src/visualDiff/diff.test.ts +167 -0
- package/src/visualDiff/diff.ts +242 -0
- package/src/visualDiff/settle.test.ts +77 -0
- package/src/visualDiff/settle.ts +62 -0
- package/src/visualDiff/snapshot.test.ts +200 -0
- package/src/visualDiff/snapshot.ts +119 -0
- package/src/visualDiff/types.ts +40 -0
- package/tsconfig.json +3 -1
- package/vitest.config.ts +18 -0
- 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)
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
|
6
|
+
* Otherwise, auto-detect the framework.
|
|
7
7
|
*/
|
|
8
8
|
export declare function resolveAdapter(adapterId?: AdapterId): AdapterObject | null;
|