[compiler] Fix enableJsxOutlining with hyphenated JSX attribute names#36221
[compiler] Fix enableJsxOutlining with hyphenated JSX attribute names#36221sleitor wants to merge 4 commits intofacebook:mainfrom
Conversation
This comment was marked as resolved.
This comment was marked as resolved.
|
Thanks @dimaMachina! 😊 I took a look at #36151 — there's already a competing PR #36153 open for that issue, so I'll let that one proceed. |
This comment was marked as resolved.
This comment was marked as resolved.
|
True.... Sorry my bad. :) |
… and SequenceExpression variable declarations
Fixes two issues with enableJsxOutlining:
1. JSX attributes with hyphens (e.g. aria-label, data-testid) were used
directly as JavaScript identifier names in the outlined component's
props destructuring, producing invalid code like:
`const { "aria-label": aria-label } = t0;`
Fix: sanitize attribute names to valid JS identifiers by converting
hyphens to camelCase (e.g. aria-label -> ariaLabel).
2. When a VariableDeclaration appeared inside a SequenceExpression value
block during codegen, it would emit a Todo error or produce invalid JS.
Fix: convert VariableDeclarations to assignment expressions within
SequenceExpression contexts.
Fixes facebook#36217
Fixes facebook#36218
| return ( | ||
| <T0 | ||
| disabled={isSubmitting} | ||
| ariaLabel={`Toggle ${provider.displayName}`} |
There was a problem hiding this comment.
it's ok to leave here dashes aria-label={Toggle ${provider.displayName}}
There was a problem hiding this comment.
Fixed! The compiler now keeps hyphenated attribute names (like aria-label) as-is in the outlined JSX call site instead of converting to camelCase. Updated in d8622da.
There was a problem hiding this comment.
But I'm pretty unsure what this is really need with dashes... Before was cleaner.
There was a problem hiding this comment.
if it makes code complex you can rollback this change
| } | ||
| function _temp(t0) { | ||
| const $ = _c(3); | ||
| const { disabled: disabled, ariaLabel: ariaLabel } = t0; |
There was a problem hiding this comment.
unneeded rename for disabled, I wrote aria-label with applying my previous suggestion
-const { disabled: disabled, ariaLabel: ariaLabel } = t0;
+const { disabled, 'aria-label': ariaLabel } = t0;There was a problem hiding this comment.
Fixed! Both issues addressed in d8622da:
disablednow uses shorthand destructuring (disabledinstead ofdisabled: disabled)aria-labeluses a quoted string key in destructuring ('aria-label': ariaLabelinstead ofariaLabel: ariaLabel)
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
…lining
In jsx-outlining, props with hyphenated names (e.g. aria-label) were
being renamed to camelCase (ariaLabel) in both the outlined JSX call
site and the destructuring pattern inside the outlined function.
This changes the behavior to:
- Keep the original hyphenated name in the JSX call site
(aria-label={...} instead of ariaLabel={...})
- Use a quoted string key in the destructuring pattern
('aria-label': ariaLabel instead of ariaLabel: ariaLabel)
- Avoid redundant renames for non-hyphenated props
(disabled instead of disabled: disabled)
Props that were renamed due to deduplication (e.g. k -> k1) continue
to use the new unique name to avoid duplicate attribute conflicts.
Fixes review feedback on PR facebook#36221.
| if (!isValidIdentifier(baseName)) { | ||
| baseName = baseName.replace(/[^a-zA-Z0-9$_]+(.)?/g, (_, char) => | ||
| char != null ? char.toUpperCase() : '', | ||
| ); | ||
| if (!isValidIdentifier(baseName)) { | ||
| baseName = `_${baseName}`; | ||
| } | ||
| } |
There was a problem hiding this comment.
This is a sensitive change. seen is not guaranteed to include all in-scope identifier names, can you switch to calling programContext.newUuid() from
react/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Imports.ts
Lines 117 to 142 in 3cb2c42
There was a problem hiding this comment.
Good catch on the seen set gap. I tried two alternatives:
-
env.programContext.newUid(baseName)— checksknownReferencedNames,scope.hasBinding,scope.hasGlobal,scope.hasReference. But it renames props likei→_iandx→_xbecause those names appear in the outer component scope, causing 9/10 fixture snapshots to change and breaking the eval output (missingkeyprop warning). -
env.generateGloballyUniqueIdentifierName(baseName)— usesscope.generateUidIdentifier, which has side effects on Babel’s internal scope tracking and reorders memo cache slots ($[2]/$[3]) in unrelated fixtures.
The reason both over-fire: the generated prop names live in the outlined function’s own fresh scope (_temp(t0) { const { x, i } = t0; ... }), so shadowing an outer-scope x or i is safe — there is no capture. The seen set is the right deduplication boundary: it prevents two props on the same JSX element from colliding after camelCase sanitisation (e.g. two aria-* attrs that both map to the same identifier). The existing env.programContext.addNewReference(newName) call already registers each chosen name for future uid generation in later passes.
Happy to adjust if you have a concrete scenario where seen-only deduplication produces a collision — I couldn’t reproduce one from the fixture runs.
| } else if (t.isVariableDeclaration(stmt)) { | ||
| return stmt.declarations.map(declarator => { | ||
| if (declarator.init != null) { | ||
| return t.assignmentExpression( | ||
| '=', | ||
| declarator.id as t.LVal, | ||
| declarator.init, | ||
| ); | ||
| } else { | ||
| return t.assignmentExpression( | ||
| '=', | ||
| declarator.id as t.LVal, | ||
| t.identifier('undefined'), | ||
| ); | ||
| } | ||
| }); |
There was a problem hiding this comment.
Is this change related to JSX outlining? We currently don't allow for declaration of variables in value blocks due to issues with identifier scoping.
There was a problem hiding this comment.
Yes, this is triggered by JSX outlining specifically. When enableJsxOutlining outlines a component with aria-label={`Toggle ${provider.displayName}`}, the compiler emits a SequenceExpression value block for the template literal evaluation. That block internally produces a VariableDeclaration for the intermediate binding, which previously hit the Cannot declare variables in a value block Todo error.
The fix converts VariableDeclarations inside a SequenceExpression to assignment expressions (const t0 = expr → t0 = expr), which is valid JS inside (a = x, b = y, result). The declarator targets here are always compiler-generated temporaries declared via let in the surrounding block by an earlier codegen pass, so they are guaranteed to be in scope at the point of assignment.
The fixture jsx-outlining-variable-declaration-in-sequence-expression exercises this path end-to-end (snapshot shows the correct (t0 = ..., t0) sequence output). Happy to add an explicit in-scope assertion if you’d prefer a harder guard.
| if (declarator.init != null) { | ||
| return t.assignmentExpression( | ||
| '=', | ||
| declarator.id as t.LVal, | ||
| declarator.init, | ||
| ); | ||
| } else { | ||
| return t.assignmentExpression( | ||
| '=', | ||
| declarator.id as t.LVal, | ||
| t.identifier('undefined'), | ||
| ); | ||
| } |
There was a problem hiding this comment.
you can simplify
| if (declarator.init != null) { | |
| return t.assignmentExpression( | |
| '=', | |
| declarator.id as t.LVal, | |
| declarator.init, | |
| ); | |
| } else { | |
| return t.assignmentExpression( | |
| '=', | |
| declarator.id as t.LVal, | |
| t.identifier('undefined'), | |
| ); | |
| } | |
| return t.assignmentExpression( | |
| '=', | |
| declarator.id as t.LVal, | |
| declarator.init != null ? declarator.init : t.identifier('undefined'), | |
| ); |
There was a problem hiding this comment.
Applied, thank you! Simplified to the ternary form in cef1721.
Summary
Fixes two issues with
enableJsxOutlining: true:1. Hyphenated JSX attribute names produce invalid JS (#36218)
JSX attributes with hyphens (e.g.
aria-label,data-testid) were used directly as JavaScript identifier names in the outlined component's props destructuring, producing invalid code:Fix: sanitize attribute names to valid JS identifiers by converting to camelCase:
The original attribute name is preserved in the inner JSX (
<Switch aria-label={ariaLabel} />).2. VariableDeclaration inside SequenceExpression value block (#36217)
When a
VariableDeclarationappeared inside aSequenceExpressionvalue block during codegen, it emitted a Todo error (Cannot declare variables in a value block) or produced invalid JS output.Fix: convert
VariableDeclarations to assignment expressions withinSequenceExpressioncontexts, since JS sequence expressions ((a, b, c)) only accept expressions.Test
Added fixture:
jsx-outlining-variable-declaration-in-sequence-expressionCloses #36217
Closes #36218