diff --git a/src/vite.ts b/src/vite.ts
index fe85aa9..8b2cacf 100644
--- a/src/vite.ts
+++ b/src/vite.ts
@@ -32,25 +32,61 @@ import type { SolidGrabPluginOptions } from "./types.js";
// We inject `data-solid-source="file:line:col"` right after the tag name.
/**
- * Matches the opening of a JSX element:
- *
| | <= | <<
- * Accessor
| createContext (TypeScript generics)
- *
- * The negative lookbehind (?)/g;
+/** Matches component-style names: PascalCase or contains a dot (Foo.Bar). */
+const COMPONENT_NAME_RE = /^[A-Z]|[.]/;
+
+/** Keywords that can directly precede a JSX expression. */
+const JSX_PRECEDING_KEYWORDS = new Set([
+ "return", "yield", "case", "default", "else",
+]);
+
/**
- * Matches component-style names: PascalCase or contains a dot (Foo.Bar).
+ * Walks backwards from `<` (skipping whitespace) to determine whether
+ * it's a JSX opening tag or a less-than comparison.
+ *
+ * - After `)`, `]`, quotes → comparison (end of expression)
+ * - After `(`, `{`, `=`, etc. → JSX (start of expression)
+ * - After a keyword like `return` → JSX
+ * - After any other identifier → comparison
*/
-const COMPONENT_NAME_RE = /^[A-Z]|[.]/;
+function isLikelyJsx(code: string, ltIndex: number): boolean {
+ let i = ltIndex - 1;
+ while (i >= 0 && (code[i] === " " || code[i] === "\t" || code[i] === "\n" || code[i] === "\r")) {
+ i--;
+ }
+
+ if (i < 0) return true; // Start of file
+
+ const ch = code[i]!;
+
+ // Expression-ending tokens → comparison
+ if (ch === ")" || ch === "]" || ch === '"' || ch === "'" || ch === "`") {
+ return false;
+ }
+
+ // Operators/punctuation that introduce an expression → JSX
+ if ("({[,;:?=!>&|+-*/%^~".includes(ch)) {
+ return true;
+ }
+
+ // Word character → JSX only if it's a keyword like `return`
+ if (/\w/.test(ch)) {
+ const end = i + 1;
+ while (i >= 0 && /\w/.test(code[i]!)) i--;
+ const word = code.slice(i + 1, end);
+ return JSX_PRECEDING_KEYWORDS.has(word);
+ }
+
+ return true; // Conservative default
+}
function transformJsx(
code: string,
@@ -92,6 +128,12 @@ function transformJsx(
const suffix = match[3]!; // space, `/>`, or `>`
const offset = match.index;
+
+ // Skip comparisons like `x < y` — only inject into actual JSX
+ if (!isLikelyJsx(code, offset)) {
+ continue;
+ }
+
const [line, col] = getLineCol(offset);
const isComponent = COMPONENT_NAME_RE.test(tagName);
diff --git a/tests/vite-plugin.test.ts b/tests/vite-plugin.test.ts
index 5ff1fd0..74006a7 100644
--- a/tests/vite-plugin.test.ts
+++ b/tests/vite-plugin.test.ts
@@ -112,6 +112,82 @@ describe("transform", () => {
expect(result.code).not.toContain("Accessor {
+ const plugin = createPlugin();
+ const code = [
+ `function foo() {`,
+ ` const x = count() < totalItems`,
+ ` if (a < b) {}`,
+ ` return userTier < minimumTier`,
+ `}`,
+ ].join("\n");
+ const result = (plugin as any).transform(code, "/project/src/foo.tsx");
+ expect(result).toBeNull();
+ });
+
+ test("does not inject into comparison after function call", () => {
+ const plugin = createPlugin();
+ const code = [
+ `function foo() {`,
+ ` return visiblePageCount() < totalPages`,
+ `}`,
+ ].join("\n");
+ const result = (plugin as any).transform(code, "/project/src/foo.tsx");
+ expect(result).toBeNull();
+ });
+
+ test("does not inject into comparison in conditional expression", () => {
+ const plugin = createPlugin();
+ const code = [
+ `if (`,
+ ` newIndex !== currentIndex() &&`,
+ ` newIndex >= 0 &&`,
+ ` newIndex < props.images.length`,
+ `) {}`,
+ ].join("\n");
+ const result = (plugin as any).transform(code, "/project/src/foo.tsx");
+ expect(result).toBeNull();
+ });
+
+ test("injects into JSX after return keyword", () => {
+ const plugin = createPlugin();
+ const code = `function App() {\n return hello
;\n}`;
+ const result = (plugin as any).transform(code, "/project/src/App.tsx");
+ expect(result).not.toBeNull();
+ expect(result.code).toContain("data-solid-source=");
+ });
+
+ test("injects into JSX after logical operators", () => {
+ const plugin = createPlugin();
+ const code = `const el = show() && visible
;`;
+ const result = (plugin as any).transform(code, "/project/src/App.tsx");
+ expect(result).not.toBeNull();
+ expect(result.code).toContain("data-solid-source=");
+ });
+
+ test("injects into JSX in ternary expression", () => {
+ const plugin = createPlugin();
+ const code = `const el = cond ? a
: b;`;
+ const result = (plugin as any).transform(code, "/project/src/App.tsx");
+ expect(result).not.toBeNull();
+ expect(result.code).toContain("data-solid-source=");
+ });
+
+ test("handles mixed comparisons and JSX in the same file", () => {
+ const plugin = createPlugin();
+ const code = [
+ `function App() {`,
+ ` const isSmall = count() < maxItems;`,
+ ` return {isSmall ? small : big}
;`,
+ `}`,
+ ].join("\n");
+ const result = (plugin as any).transform(code, "/project/src/App.tsx");
+ expect(result).not.toBeNull();
+ expect(result.code).toContain("data-solid-source=");
+ // Comparison should be untouched
+ expect(result.code).toContain("count() < maxItems");
+ });
+
test("respects jsxLocation: false", () => {
const plugin = createPlugin({ jsxLocation: false });
const code = `function App() {\n return hello
;\n}`;