next/swc drops the leading whitespace of a JSX text node that contains an HTML entity
### Link to the code that reproduces this issue
https://github.com/chrisgory/nextswc-jsx-entity-leading-space-repro
### To Reproduce
`app/page.tsx` is the whole reproduction — four variants, one of which loses a space.
1. `npm install`
2. `npm run build` (Turbopack, the Next 16 default)
3. Inspect the emitted chunk:
```bash
grep -rhoE '"(AAA|BBB|DDD)"\}\),("[^"]{0,22}|" ","[^"]{0,18})' .next/server/chunks/ssr/*.js
```
Also reproduces with `npm run build:webpack` (`next build --webpack`) and in `npm run dev`.
### Current vs. Expected behavior
A JSX text node that (a) begins with a space and (b) contains an HTML entity loses its
leading space. Without the entity — same shape, same position — the space is kept.
```jsx
<p><b>AAA</b> alpha’s tail.</p> {/* entity */}
<p><b>BBB</b> bravo plain tail.</p> {/* no entity */}
```
Emitted by `next build` (identical under Turbopack and webpack):
```js
"AAA"}),"alpha’s tail." // ← leading space GONE
"BBB"})," bravo plain tail." // ← leading space kept
```
Rendered: **`AAAalpha’s tail.`** vs `BBB bravo plain tail.`
Expected: both keep the space. Only whitespace containing a newline is stripped by JSX;
this space has no newline in it, and its handling should not depend on whether the text
node happens to contain an entity.
`’`, `'`, ` ` and numeric (`’`) entities all trigger it.
### Scope
Verified against build output:
| case | result |
| --- | --- |
| `<b>AAA</b> alpha’s tail.` — leading space + entity | **space dropped** |
| `<b>BBB</b> bravo plain tail.` — leading space, no entity | kept |
| `charlie’s tail <b>CCC</b>` — **trailing** space + entity | kept |
| `<b>DDD</b>{' '}delta’s tail.` — explicit expression | kept |
So it is specific to **leading** whitespace on an **entity-bearing** text node.
### Not bundler-specific, and not build-specific
Both bundlers and both modes are affected, which points at the shared `next/swc`
JSX transform rather than Turbopack:
| | leading space |
| --- | --- |
| `next build` (Turbopack) | **dropped** |
| `next build --webpack` | **dropped** |
| `next dev` | **dropped** |
### Divergence from the reference implementations
The same source through Babel and through standalone `@swc/core` keeps the space, so
this looks specific to Next's copy/configuration of the transform:
```js
// @babel/plugin-transform-react-jsx (runtime: automatic)
}), " alpha’s tail."] // ← space kept
// @swc/core (standalone, runtime: automatic, minified or not)
_jsx(L,{children:"link"})," alpha’s tail." // ← space kept
// next/swc (next dev + next build)
"AAA"}),"alpha’s tail." // ← space dropped
```
Babel is the reference JSX implementation, and upstream `swc` agrees with it, which
suggests `next/swc` is the outlier here rather than this being intended JSX semantics.
### Why it is easy to miss
The source looks correct, and `react/no-unescaped-entities` actively pushes authors
toward writing `’`/`'` — which is exactly what triggers this. The result is a
silently missing space in shipped copy. We hit it in production on a real sentence:
```jsx
<Link href="/pipeline">open the pipeline</Link> to see what’s next.
```
rendering as **"open the pipelineto see what's next"**. A sweep of our build output
found 28 occurrences across 20 files.
### Workaround
Write the space as its own expression, so it is no longer part of the entity-bearing
text node:
```jsx
<Link href="/pipeline">open the pipeline</Link>{' '}
to see what’s next.
```
Using the literal character (`’`) instead of the entity also avoids it.
### Provide environment information
```
Operating System:
Platform: darwin
Arch: arm64
Binaries:
Node: 22.16.0
Relevant Packages:
next: 16.2.7
react: 19.2.0
react-dom: 19.2.0
```
### Which area(s) are affected?
SWC transforms, Turbopack, Webpack
### Which stage(s) are affected?
`next dev`, `next build`
2 条评论