Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions demo/vite-project/src/components/tanstack-form-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import React from 'react';

// Simulating Tanstack Forms pattern
function useForm() {
return {
AppForm: ({ children }: { children: React.ReactNode }) => (
<form style={{ padding: '20px', border: '2px solid blue' }}>{children}</form>
),
CancelButton: ({ children, onClick }: { children: React.ReactNode; onClick?: () => void }) => (
<button
type="button"
onClick={onClick}
style={{
padding: '10px 20px',
backgroundColor: 'red',
color: 'white',
marginRight: '10px',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
{children}
</button>
),
SubmitButton: ({ children }: { children: React.ReactNode }) => (
<button
type="submit"
style={{
padding: '10px 20px',
backgroundColor: 'green',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
{children}
</button>
),
};
}

// Simulating uppercase workaround
function useFormUppercase() {
return {
AppForm: ({ children }: { children: React.ReactNode }) => (
<form style={{ padding: '20px', border: '2px solid green' }}>{children}</form>
),
CancelButton: ({ children, onClick }: { children: React.ReactNode; onClick?: () => void }) => (
<button
type="button"
onClick={onClick}
style={{
padding: '10px 20px',
backgroundColor: 'red',
color: 'white',
marginRight: '10px',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
{children}
</button>
),
SubmitButton: ({ children }: { children: React.ReactNode }) => (
<button
type="submit"
style={{
padding: '10px 20px',
backgroundColor: 'green',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
{children}
</button>
),
};
}

export default function TanstackFormTest() {
const form = useForm();
const FormContent = useFormUppercase();

const handleCancel = () => {
alert('Cancel clicked!');
};

return (
<div style={{ padding: '20px' }}>
<h2>Tanstack Forms + Lingo.dev Compiler Issue #1165</h2>

<div style={{ marginBottom: '40px' }}>
<h3>Broken: Lowercase variable name (form)</h3>
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The heading says "Broken" but this test file is included in the PR that fixes the issue. After the fix, this case should work correctly. Consider updating the heading to something like "Previously broken: Lowercase variable name (form) - now fixed" to accurately reflect the current state.

Suggested change
<h3>Broken: Lowercase variable name (form)</h3>
<h3>Previously broken: Lowercase variable name (form) - now fixed</h3>

Copilot uses AI. Check for mistakes.
<p>Using: <code>const form = useForm()</code></p>
<p>Expected: Blue border, styled buttons with click functionality</p>
<form.AppForm>
<div>
<form.CancelButton onClick={handleCancel}>Cancel</form.CancelButton>
<form.SubmitButton>Submit</form.SubmitButton>
</div>
</form.AppForm>
</div>

<div>
<h3>Working: Uppercase variable name (FormContent)</h3>
<p>Using: <code>const FormContent = useFormUppercase()</code></p>
<p>Expected: Green border, styled buttons with click functionality</p>
<FormContent.AppForm>
<div>
<FormContent.CancelButton onClick={handleCancel}>Cancel</FormContent.CancelButton>
<FormContent.SubmitButton>Submit</FormContent.SubmitButton>
</div>
</FormContent.AppForm>
</div>
</div>
);
}

5 changes: 4 additions & 1 deletion packages/compiler/src/jsx-scope-inject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ export const lingoJsxScopeInjectMutation = createCodeMutation((payload) => {
} as any;

// Add $as prop
const as = /^[A-Z]/.test(originalJsxElementName)
// Check if it's a member expression (contains dot) or starts with uppercase
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The comment could be more specific about the rationale. Consider expanding it to explain why member expressions need special handling:

// 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

This makes it clearer for future maintainers why both conditions are checked.

Suggested change
// Check if it's a member expression (contains dot) or starts with uppercase
// 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.

Copilot uses AI. Check for mistakes.
const isMemberExpression = originalJsxElementName.includes(".");
const isComponent = /^[A-Z]/.test(originalJsxElementName);
const as = isMemberExpression || isComponent
? t.identifier(originalJsxElementName)
: originalJsxElementName;
Comment on lines 82 to 84
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using t.identifier() with a dotted string like "form.Button" creates an invalid AST node because JavaScript identifiers cannot contain dots. For member expressions, you need to build a proper member expression AST structure.

Consider creating a helper function that parses the dotted string and builds the correct AST:

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;
}

Then use it like:

const as = isMemberExpression || isComponent
  ? createMemberExpressionFromString(originalJsxElementName)
  : originalJsxElementName;

This will correctly generate form.Button as a member expression AST node instead of an invalid identifier.

Copilot uses AI. Check for mistakes.
setJsxAttributeValue(newNodePath, "$as", as);
Expand Down
Loading