diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts index 486773d5eb91..94c2032c2587 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts @@ -1712,9 +1712,10 @@ function codegenInstructionValue( break; } case 'JsxExpression': { + const isBuiltinTag = instrValue.tag.kind === 'BuiltinTag'; const attributes: Array = []; for (const attribute of instrValue.props) { - attributes.push(codegenJsxAttribute(cx, attribute)); + attributes.push(codegenJsxAttribute(cx, attribute, isBuiltinTag)); } let tagValue = instrValue.tag.kind === 'Identifier' @@ -2123,9 +2124,28 @@ function codegenInstructionValue( */ const STRING_REQUIRES_EXPR_CONTAINER_PATTERN = /[\u{0000}-\u{001F}\u{007F}\u{0080}-\u{FFFF}\u{010000}-\u{10FFFF}]|"|\\/u; + +/** + * The Babel JSX transform (@babel/plugin-transform-react-jsx) normalizes + * newlines followed by whitespace in plain JSX string attributes: + * + * value.value = value.value.replace(/\n\s+/g, " "); + * + * However, this normalization is skipped for JSXExpressionContainer values. + * When the compiler wraps strings in expression containers (because they + * match STRING_REQUIRES_EXPR_CONTAINER_PATTERN), the JSX transform bypasses + * normalization. The server code (uncompiled or differently compiled) goes + * through the standard normalization, creating a hydration mismatch. + * + * We apply the same normalization here before the expression container check, + * ensuring compiled output matches the behavior of non-compiled code. + */ +const JSX_STRING_NEWLINE_PATTERN = /\n\s+/g; + function codegenJsxAttribute( cx: Context, attribute: JsxAttribute, + isBuiltinTag: boolean, ): t.JSXAttribute | t.JSXSpreadAttribute { switch (attribute.kind) { case 'JsxAttribute': { @@ -2145,6 +2165,18 @@ function codegenJsxAttribute( switch (innerValue.type) { case 'StringLiteral': { value = innerValue; + if ( + isBuiltinTag && + !cx.fbtOperands.has(attribute.place.identifier.id) + ) { + const normalized = value.value.replace( + JSX_STRING_NEWLINE_PATTERN, + ' ', + ); + if (normalized !== value.value) { + value = createStringLiteral(value.loc, normalized); + } + } if ( STRING_REQUIRES_EXPR_CONTAINER_PATTERN.test(value.value) && !cx.fbtOperands.has(attribute.place.identifier.id) diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-multiline-classname-expression.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-multiline-classname-expression.expect.md new file mode 100644 index 000000000000..0dd4c5b6eeac --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-multiline-classname-expression.expect.md @@ -0,0 +1,36 @@ + +## Input + +```javascript +function Component() { + return ( +
+ Hello +
+ ); +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function Component() { + const $ = _c(1); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 =
Hello
; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-multiline-classname-expression.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-multiline-classname-expression.js new file mode 100644 index 000000000000..80b337cec273 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-multiline-classname-expression.js @@ -0,0 +1,9 @@ +function Component() { + return ( +
+ Hello +
+ ); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-multiline-classname-hydration.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-multiline-classname-hydration.expect.md new file mode 100644 index 000000000000..1481c4b9a8ec --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-multiline-classname-hydration.expect.md @@ -0,0 +1,43 @@ + +## Input + +```javascript +function Component() { + return ( +
+ Hello +
+ ); +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function Component() { + const $ = _c(1); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = ( +
+ Hello +
+ ); + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-multiline-classname-hydration.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-multiline-classname-hydration.js new file mode 100644 index 000000000000..57a8948561eb --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-multiline-classname-hydration.js @@ -0,0 +1,12 @@ +function Component() { + return ( +
+ Hello +
+ ); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-multiline-classname-newline-indent.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-multiline-classname-newline-indent.expect.md new file mode 100644 index 000000000000..83970c76b8f8 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-multiline-classname-newline-indent.expect.md @@ -0,0 +1,36 @@ + +## Input + +```javascript +function Component() { + return ( +
+ Hello +
+ ); +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function Component() { + const $ = _c(1); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 =
Hello
; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-multiline-classname-newline-indent.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-multiline-classname-newline-indent.js new file mode 100644 index 000000000000..fe81bd1cf61f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-multiline-classname-newline-indent.js @@ -0,0 +1,9 @@ +function Component() { + return ( +
+ Hello +
+ ); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-multiline-component-prop-preserved.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-multiline-component-prop-preserved.expect.md new file mode 100644 index 000000000000..6e979b28bea5 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-multiline-component-prop-preserved.expect.md @@ -0,0 +1,34 @@ + +## Input + +```javascript +function Component() { + return ( + + ); +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function Component() { + const $ = _c(1); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = ; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-multiline-component-prop-preserved.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-multiline-component-prop-preserved.js new file mode 100644 index 000000000000..931d1bf2223b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-multiline-component-prop-preserved.js @@ -0,0 +1,7 @@ +function Component() { + return ( + + ); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-multiline-title-attribute.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-multiline-title-attribute.expect.md new file mode 100644 index 000000000000..c18f93fb808b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-multiline-title-attribute.expect.md @@ -0,0 +1,37 @@ + +## Input + +```javascript +function Component() { + return ( +
+ Hello +
+ ); +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function Component() { + const $ = _c(1); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 =
Hello
; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-multiline-title-attribute.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-multiline-title-attribute.js new file mode 100644 index 000000000000..08c874456747 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-multiline-title-attribute.js @@ -0,0 +1,10 @@ +function Component() { + return ( +
+ Hello +
+ ); +}