Skip to content

Commit c126483

Browse files
committed
Improve source tracking for JSX/TSX with Babel AST
Adds Babel AST-based transformation for React, Solid, and Preact to accurately inject tracking attributes only into JSX elements, avoiding TypeScript type contexts. Falls back to regex-based transformation if Babel is unavailable or fails. Also introduces TypeScript type context detection in regex-based parsing to prevent incorrect attribute injection into generics and type annotations.
1 parent 2570206 commit c126483

4 files changed

Lines changed: 446 additions & 8 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,8 @@
103103
"@xterm/xterm": "^5.6.0-beta.136",
104104
"electron-context-menu": "^4.1.1",
105105
"esbuild": "^0.27.2",
106-
"esbuild-plugin-vue3": "^0.5.1",
107106
"esbuild-plugin-solid": "^0.6.0",
107+
"esbuild-plugin-vue3": "^0.5.1",
108108
"esbuild-svelte": "^0.9.4",
109109
"http-proxy-agent": "^7.0.0",
110110
"https-proxy-agent": "^7.0.2",

src/vs/workbench/contrib/roopik/electron-main/build/injectors/sourceTrackingCore.ts

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,70 @@ export const CLOSING_TAG_REGEX = /<\/([a-zA-Z][a-zA-Z0-9.-]*)>/g;
104104
// String Safety Checks
105105
// ============================================
106106

107+
/**
108+
* Check if a position in code is inside a TypeScript type context.
109+
* This prevents injecting attributes into TypeScript generics like:
110+
* - React.MouseEvent<HTMLDivElement> (type annotation)
111+
* - Array<string> (generic parameter)
112+
* - T extends HTMLElement (extends clause)
113+
* - Promise<Result<T>> (nested generics)
114+
*
115+
* @param code - The full source code
116+
* @param position - Character position to check (position of '<')
117+
* @returns True if inside a TypeScript type context
118+
*/
119+
export function isInsideTypeScriptTypeContext(code: string, position: number): boolean {
120+
// Look backwards from position to find context
121+
const beforeMatch = code.substring(Math.max(0, position - 100), position);
122+
123+
// Pattern 1: Type annotation - "event: React.MouseEvent<" or "value: Array<"
124+
// Look for ": TypeName<" pattern (colon followed by identifier then our position)
125+
if (/:\s*[A-Za-z_$][A-Za-z0-9_$.<>]*$/.test(beforeMatch)) {
126+
return true;
127+
}
128+
129+
// Pattern 2: Generic parameter - "Promise<Result<" (nested angle brackets)
130+
// Count unclosed < before this position
131+
let angleBracketDepth = 0;
132+
for (let i = 0; i < beforeMatch.length; i++) {
133+
const char = beforeMatch[i];
134+
if (char === '<') {
135+
angleBracketDepth++;
136+
} else if (char === '>') {
137+
angleBracketDepth = Math.max(0, angleBracketDepth - 1);
138+
}
139+
}
140+
if (angleBracketDepth > 0) {
141+
return true;
142+
}
143+
144+
// Pattern 3: Extends clause - "T extends HTMLElement" or "interface Foo extends Bar<"
145+
if (/\bextends\s+[A-Za-z_$][A-Za-z0-9_$.<>]*$/.test(beforeMatch)) {
146+
return true;
147+
}
148+
149+
// Pattern 4: Type parameter declaration - "function foo<T extends "
150+
if (/[<,]\s*[A-Za-z_$][A-Za-z0-9_$]*\s+extends\s+[A-Za-z_$][A-Za-z0-9_$.<>]*$/.test(beforeMatch)) {
151+
return true;
152+
}
153+
154+
// Pattern 5: Return type annotation - "): Promise<" or "=> Array<"
155+
if (/[):]\s*[A-Za-z_$][A-Za-z0-9_$.<>]*$/.test(beforeMatch)) {
156+
// Extra check: make sure it's not JSX return like "return <div>"
157+
// JSX returns usually have whitespace/newline after return keyword
158+
if (!/\breturn\s+$/.test(beforeMatch) && !/=>\s*$/.test(beforeMatch.replace(/[A-Za-z_$][A-Za-z0-9_$.<>]*$/, ''))) {
159+
return true;
160+
}
161+
}
162+
163+
// Pattern 6: Type assertion - "as HTMLDivElement" or "<HTMLDivElement>"
164+
if (/\bas\s+[A-Za-z_$][A-Za-z0-9_$.<>]*$/.test(beforeMatch)) {
165+
return true;
166+
}
167+
168+
return false;
169+
}
170+
107171
/**
108172
* Check if a position in code is inside a string literal or template literal.
109173
* This prevents modifying HTML code that's displayed as text content
@@ -278,7 +342,9 @@ export function findMatchingClosingTag(allMatches: TagMatch[], openingPos: numbe
278342
foundOpening = true;
279343
continue;
280344
}
281-
if (!foundOpening) continue;
345+
if (!foundOpening) {
346+
continue;
347+
}
282348

283349
if (match.tagName === tagName) {
284350
if (match.type === 'opening') {
@@ -356,6 +422,12 @@ export function parseElements(code: string, options: ParseOptions): ElementRepla
356422
continue;
357423
}
358424

425+
// TYPESCRIPT: Skip if inside TypeScript type context (generics, type annotations)
426+
// This prevents injecting into: React.MouseEvent<HTMLDivElement>, Array<string>, etc.
427+
if (isInsideTypeScriptTypeContext(code, matchStart)) {
428+
continue;
429+
}
430+
359431
// Calculate start position
360432
const startPos = calculatePosition(code, matchStart, baseLineOffset);
361433

0 commit comments

Comments
 (0)