diff --git a/demo/vite-project/src/components/tanstack-form-test.tsx b/demo/vite-project/src/components/tanstack-form-test.tsx new file mode 100644 index 000000000..247060b8b --- /dev/null +++ b/demo/vite-project/src/components/tanstack-form-test.tsx @@ -0,0 +1,123 @@ +import React from 'react'; + +// Simulating Tanstack Forms pattern +function useForm() { + return { + AppForm: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + CancelButton: ({ children, onClick }: { children: React.ReactNode; onClick?: () => void }) => ( + + ), + SubmitButton: ({ children }: { children: React.ReactNode }) => ( + + ), + }; +} + +// Simulating uppercase workaround +function useFormUppercase() { + return { + AppForm: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + CancelButton: ({ children, onClick }: { children: React.ReactNode; onClick?: () => void }) => ( + + ), + SubmitButton: ({ children }: { children: React.ReactNode }) => ( + + ), + }; +} + +export default function TanstackFormTest() { + const form = useForm(); + const FormContent = useFormUppercase(); + + const handleCancel = () => { + alert('Cancel clicked!'); + }; + + return ( +
+

Tanstack Forms + Lingo.dev Compiler Issue #1165

+ +
+

✅ Previously broken: Lowercase variable name (form) - now fixed

+

Using: const form = useForm()

+

Expected: Blue border, styled buttons with click functionality

+ +
+ Cancel + Submit +
+
+
+ +
+

Working: Uppercase variable name (FormContent)

+

Using: const FormContent = useFormUppercase()

+

Expected: Green border, styled buttons with click functionality

+ +
+ Cancel + Submit +
+
+
+
+ ); +} + diff --git a/packages/compiler/src/jsx-scope-inject.ts b/packages/compiler/src/jsx-scope-inject.ts index d1003a68a..a69c8c689 100644 --- a/packages/compiler/src/jsx-scope-inject.ts +++ b/packages/compiler/src/jsx-scope-inject.ts @@ -14,6 +14,25 @@ import { getJsxExpressions } from "./utils/jsx-expressions"; import { collectJsxScopes, getJsxScopeAttribute } from "./utils/jsx-scope"; import { setJsxAttributeValue } from "./utils/jsx-attribute"; +/** + * Creates a proper AST node from a string that may contain dots (member expressions). + * For example, "form.Button" becomes a member expression AST, while "Button" becomes an identifier. + * @param str - The string to convert (e.g., "form.Button" or "Button") + * @returns A Babel AST Expression node + */ +function createMemberExpressionFromString(str: string): t.Expression { + const parts = str.split('.'); + if (parts.length === 1) { + return t.identifier(parts[0]); + } + + let expr: t.Expression = t.identifier(parts[0]); + for (let i = 1; i < parts.length; i++) { + expr = t.memberExpression(expr, t.identifier(parts[i])); + } + return expr; +} + export const lingoJsxScopeInjectMutation = createCodeMutation((payload) => { const mode = getModuleExecutionMode(payload.ast, payload.params.rsc); const jsxScopes = collectJsxScopes(payload.ast); @@ -55,8 +74,13 @@ export const lingoJsxScopeInjectMutation = createCodeMutation((payload) => { } as any; // Add $as prop - const as = /^[A-Z]/.test(originalJsxElementName) - ? t.identifier(originalJsxElementName) + // Check if it's a member expression (contains dot) or starts with uppercase. + // Member expressions (e.g., form.Button) and uppercase names (e.g., Button) + // should be treated as component references, not HTML element strings. + const isMemberExpression = originalJsxElementName.includes("."); + const isComponent = /^[A-Z]/.test(originalJsxElementName); + const as = isMemberExpression || isComponent + ? createMemberExpressionFromString(originalJsxElementName) : originalJsxElementName; setJsxAttributeValue(newNodePath, "$as", as);