Repository: rollup/rollup (2025-08-01T17:43:16Z)

Issue:
Wrong code generated for JSX syntax
### Rollup Version

4.34.8

### Operating System (or Browser)

NA

### Node Version (if applicable)

NA

### Link To Reproduction

https://rollupjs.org/repl/?version=4.34.8&shareable=JTdCJTIyZXhhbXBsZSUyMiUzQSUyMiUyMiUyQyUyMm1vZHVsZXMlMjIlM0ElNUIlN0IlMjJjb2RlJTIyJTNBJTIyZnVuY3Rpb24lMjBmKCklMjAlN0IlNUNuJTIwJTIwJTIwJTIwcmV0dXJuJTIwKCU1Q24lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlM0MlM0UlNUNuJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTNDZGl2JTNFJTVDbiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUzQ2ltZyUyMCUyRiUzRSU1Q24lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlM0MlMkZkaXYlM0UlNUNuJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTNDJTJGJTNFJTVDbiUyMCUyMCUyMCUyMCklM0IlNUNuJTdEJTVDbiU1Q25leHBvcnQlMjBkZWZhdWx0JTIwZiUzQiU1Q24lMjIlMkMlMjJpc0VudHJ5JTIyJTNBdHJ1ZSUyQyUyMm5hbWUlMjIlM0ElMjJtYWluLmpzJTIyJTdEJTVEJTJDJTIyb3B0aW9ucyUyMiUzQSU3QiUyMmpzeCUyMiUzQXRydWUlMkMlMjJvdXRwdXQlMjIlM0ElN0IlMjJmb3JtYXQlMjIlM0ElMjJlcyUyMiU3RCU3RCU3RA==

### Expected Behaviour

Having in rollup config:
```
const rollupOptions = {
    ...
    jsx: {
        mode: 'classic',
        factory: 'createElement',
        fragment: 'createFragment',
        importSource: null,
    },
};

```
And input file:
```
    return (
        <>
            <div>
                <img />
            </div>
        </>
    );

```

JSX syntax assumes that all whitespaces at the beginning and end of each line (and empty lines, and newline characters after tag) should be removed.

Expected output (split into lines for readability):
```
    return (
        /*#__PURE__*/createElement(createFragment, null,
            /*#__PURE__*/createElement("div", null,
                /*#__PURE__*/createElement("img", null)
            )
        )
    );

```

### Actual Behaviour

Rollup generates (split into lines for readability):
```
    return (
        /*#__PURE__*/createElement(createFragment, null,
            "\n            ",
            /*#__PURE__*/createElement("div", null,
                "\n                ",
                /*#__PURE__*/createElement("img", null),
                "\n            "
            ),
            "\n        "
        )
    );

```


Discussion:
> JSX syntax assumes that all whitespaces at the beginning and end of each line

Sounds reasonable. Do you know if there is an exact spec somewhere? To be honest, I just assumed that SWC would handle this for us, but apparently not at the parsing level.
I haven't found details in spec (https://facebook.github.io/jsx/), but found such description (or comments) in several places. Removing whitespaces would mimic React's behavior. And it seems more natural too.

Some discussion on similar topic:
https://github.com/babel/babel/issues/10356
This issue has been resolved via #6051 as part of rollup@4.48.1. You can test it via `npm install rollup`.



Patch:
diff --git a/src/ast/nodes/JSXAttribute.ts b/src/ast/nodes/JSXAttribute.ts
index b43818c6e35..a2ad6325ed4 100644
--- a/src/ast/nodes/JSXAttribute.ts
+++ b/src/ast/nodes/JSXAttribute.ts
@@ -7,7 +7,7 @@ import type JSXExpressionContainer from './JSXExpressionContainer';
 import type JSXFragment from './JSXFragment';
 import JSXIdentifier from './JSXIdentifier';
 import type JSXNamespacedName from './JSXNamespacedName';
-import type Literal from './Literal';
+import Literal from './Literal';
 import type * as NodeType from './NodeType';
 import { NodeBase, onlyIncludeSelf } from './shared/Node';
 
@@ -29,6 +29,16 @@ export default class JSXAttribute extends NodeBase {
 				}
 				if (value) {
 					code.overwrite(name.end, value.start, ': ', { contentOnly: true });
+					// foo="aa \n aa"
+					if (
+						value instanceof Literal &&
+						typeof value.value === 'string' &&
+						value.value.includes('\n')
+					) {
+						code.overwrite(value.start, value.end, JSON.stringify(value.value), {
+							contentOnly: true
+						});
+					}
 				} else {
 					code.appendLeft(name.end, ': true');
 				}
diff --git a/src/ast/nodes/JSXText.ts b/src/ast/nodes/JSXText.ts
index e06d0383dee..29436377ff0 100644
--- a/src/ast/nodes/JSXText.ts
+++ b/src/ast/nodes/JSXText.ts
@@ -3,19 +3,36 @@ import type { NormalizedJsxOptions } from '../../rollup/types';
 import type * as NodeType from './NodeType';
 import { NodeBase, onlyIncludeSelf } from './shared/Node';
 
+const RE_WHITESPACE_TRIM = /^[ \t]*\r?\n[ \t\r\n]*|[ \t]*\r?\n[ \t\r\n]*$/g;
+const RE_WHITESPACE_MERGE = /[ \t]*\r?\n[ \t\r\n]*/g;
+
 export default class JSXText extends NodeBase {
 	type!: NodeType.tJSXText;
 	value!: string;
 	raw!: string;
 
+	private renderedText: string | undefined;
+
+	shouldRender() {
+		return !!this.getRenderedText();
+	}
+
 	render(code: MagicString) {
 		const { mode } = this.scope.context.options.jsx as NormalizedJsxOptions;
 		if (mode !== 'preserve') {
-			code.overwrite(this.start, this.end, JSON.stringify(this.value), {
+			code.overwrite(this.start, this.end, JSON.stringify(this.getRenderedText()), {
 				contentOnly: true
 			});
 		}
 	}
+
+	private getRenderedText() {
+		if (this.renderedText === undefined)
+			this.renderedText = this.value
+				.replace(RE_WHITESPACE_TRIM, '')
+				.replace(RE_WHITESPACE_MERGE, ' ');
+		return this.renderedText;
+	}
 }
 
 JSXText.prototype.includeNode = onlyIncludeSelf;
diff --git a/src/ast/nodes/shared/JSXElementBase.ts b/src/ast/nodes/shared/JSXElementBase.ts
index e5bba910d07..40c53646fda 100644
--- a/src/ast/nodes/shared/JSXElementBase.ts
+++ b/src/ast/nodes/shared/JSXElementBase.ts
@@ -6,6 +6,7 @@ import type { InclusionContext } from '../../ExecutionContext';
 import type Variable from '../../variables/Variable';
 import JSXEmptyExpression from '../JSXEmptyExpression';
 import JSXExpressionContainer from '../JSXExpressionContainer';
+import JSXText from '../JSXText';
 import type { JSXChild, JsxMode } from './jsxHelpers';
 import { getAndIncludeFactoryVariable } from './jsxHelpers';
 import type { IncludeChildren } from './Node';
@@ -68,8 +69,9 @@ export default class JSXElementBase extends NodeBase {
 		let firstChild: JSXChild | null = null;
 		for (const child of children) {
 			if (
-				child instanceof JSXExpressionContainer &&
-				child.expression instanceof JSXEmptyExpression
+				(child instanceof JSXExpressionContainer &&
+					child.expression instanceof JSXEmptyExpression) ||
+				(child instanceof JSXText && !child.shouldRender())
 			) {
 				code.remove(childrenEnd, child.end);
 			} else {
diff --git a/src/utils/jsx.ts b/src/utils/jsx.ts
index 978cc0ba2a4..e2e2d502fe0 100644
--- a/src/utils/jsx.ts
+++ b/src/utils/jsx.ts
@@ -3,7 +3,7 @@ import JSXEmptyExpression from '../ast/nodes/JSXEmptyExpression';
 import JSXExpressionContainer from '../ast/nodes/JSXExpressionContainer';
 import type JSXFragment from '../ast/nodes/JSXFragment';
 import type JSXSpreadChild from '../ast/nodes/JSXSpreadChild';
-import type JSXText from '../ast/nodes/JSXText';
+import JSXText from '../ast/nodes/JSXText';
 
 export function getRenderedJsxChildren(
 	children: (JSXText | JSXExpressionContainer | JSXElement | JSXFragment | JSXSpreadChild)[]
@@ -11,7 +11,10 @@ export function getRenderedJsxChildren(
 	let renderedChildren = 0;
 	for (const child of children) {
 		if (
-			!(child instanceof JSXExpressionContainer && child.expression instanceof JSXEmptyExpression)
+			!(
+				child instanceof JSXExpressionContainer && child.expression instanceof JSXEmptyExpression
+			) &&
+			(!(child instanceof JSXText) || child.shouldRender())
 		) {
 			renderedChildren++;
 		}


Test patch:
diff --git a/test/form/samples/jsx/transpiles-react-jsx/_expected.js b/test/form/samples/jsx/transpiles-react-jsx/_expected.js
index 53b7d3dba63..5a95432bff1 100644
--- a/test/form/samples/jsx/transpiles-react-jsx/_expected.js
+++ b/test/form/samples/jsx/transpiles-react-jsx/_expected.js
@@ -71,6 +71,11 @@ console.log(/*#__PURE__*/react.createElement(Foo$1, Object.assign({}, obj$1, obj
 console.log(/*#__PURE__*/react.createElement(Foo$1, Object.assign({}, obj$1, { key: "1" })));
 console.log(/*#__PURE__*/react.createElement(Foo$1, Object.assign({}, obj$1, { key: "1" }), /*#__PURE__*/jsx$2(Foo$1, {})));
 
+// whitespace nightmares
+console.log(
+  /*#__PURE__*/jsxs$2("div", { children: ["Hello World", /*#__PURE__*/jsxs$2("p", { children: [" JSX ", /*#__PURE__*/jsx$2("b", { children: "whitespaces" }), " are so  A N N O Y I N G  to deal with!! "] }), /*#__PURE__*/jsx$2("span", { foo: "bar\n      baz" }), /*#__PURE__*/jsx$2("p", { children: "x y" }), /*#__PURE__*/jsx$2(Fragment, { children: /*#__PURE__*/jsx$2("div", {}) }), /*#__PURE__*/jsx$2(Fragment, { children: /*#__PURE__*/jsx$2("div", {}) })] })
+);
+
 const Foo = 'wrong Foo 2';
 const obj = 'wrong obj 2';
 const jsx = 'wrong jsx 2';
diff --git a/test/form/samples/jsx/transpiles-react-jsx/jsx.js b/test/form/samples/jsx/transpiles-react-jsx/jsx.js
index e3346bf6393..b0b4880d195 100644
--- a/test/form/samples/jsx/transpiles-react-jsx/jsx.js
+++ b/test/form/samples/jsx/transpiles-react-jsx/jsx.js
@@ -61,3 +61,36 @@ console.log(<Foo {...obj} {...obj} x="1" key="1" y="1" />);
 console.log(<Foo {...obj} {...obj} x="1" key y="1" />);
 console.log(<Foo {...obj} key="1"></Foo>);
 console.log(<Foo {...obj} key="1"><Foo/></Foo>);
+
+// whitespace nightmares
+console.log(
+  <div>
+    Hello
+        World
+    <p> JSX <b>whitespaces</b> are
+          so  A N N O Y I N G  to
+deal with!! </p>
+    
+	<span foo="bar
+      baz"/>
+    <p>
+
+
+
+
+      x
+
+
+      y
+
+
+
+
+      
+    </p>
+    <><div/></>
+    <>
+      <div/>
+    </>
+  </div>
+)