Skip to content

Commit 0ba1b90

Browse files
committed
Enable anonymous function naming in React Compiler
1 parent e337cb5 commit 0ba1b90

File tree

8 files changed

+226
-120
lines changed

8 files changed

+226
-120
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@
155155
"abort-controller": "3.0.0",
156156
"alex": "9.1.0",
157157
"async-sema": "3.0.1",
158-
"babel-plugin-react-compiler": "19.1.0-rc.2",
158+
"babel-plugin-react-compiler": "0.0.0-experimental-3fde738-20250918",
159159
"browserslist": "4.25.1",
160160
"buffer": "5.6.0",
161161
"cheerio": "0.22.0",

packages/next/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@
233233
"async-sema": "3.0.0",
234234
"axe-playwright": "2.0.3",
235235
"babel-loader": "10.0.0",
236-
"babel-plugin-react-compiler": "19.1.0-rc.2",
236+
"babel-plugin-react-compiler": "0.0.0-experimental-3fde738-20250918",
237237
"babel-plugin-transform-define": "2.0.0",
238238
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
239239
"browserify-zlib": "0.2.0",

packages/next/src/build/get-babel-loader-config.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { EnvironmentConfig } from 'babel-plugin-react-compiler'
12
import path from 'path'
23
import type { JSONValue, ReactCompilerOptions } from '../server/config-shared'
34
import type { NextBabelLoaderOptions } from './babel/loader/types'
@@ -24,9 +25,15 @@ const getReactCompilerPlugins = (
2425
}
2526

2627
if (maybeOptions) {
27-
const defaultOptions: ReactCompilerOptions = isDev
28+
const defaultOptions: ReactCompilerOptions & {
29+
environment?: {
30+
enableNameAnonymousFunctions?: EnvironmentConfig['enableNameAnonymousFunctions']
31+
}
32+
} = isDev
2833
? {
29-
// TODO: enable `environment.enableNameAnonymousFunctions`Ï
34+
environment: {
35+
enableNameAnonymousFunctions: true,
36+
},
3037
}
3138
: {}
3239
const options: ReactCompilerOptions =

pnpm-lock.yaml

Lines changed: 21 additions & 21 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use client'
2+
3+
import { useEffect, useState } from 'react'
4+
5+
export default function Page() {
6+
const [callFrame, setCallFrame] = useState(null)
7+
useEffect(() => {
8+
const error = new Error('test-top-frame')
9+
console.error(error)
10+
11+
const callStack = new Error('test-top-frame').stack.split(
12+
'test-top-frame\n'
13+
)[1]
14+
// indices might change due to different compiler optimizations
15+
const callFrame = callStack.split('\n')[0]
16+
setCallFrame(callFrame)
17+
}, [])
18+
return (
19+
<pre data-testid="call-frame" aria-busy={callFrame === null}>
20+
{String(callFrame)}
21+
</pre>
22+
)
23+
}
Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,31 @@
11
'use client'
22

3-
import { useEffect } from 'react'
3+
import { Profiler, useReducer } from 'react'
44

5-
export default function Page() {
6-
let $_: any
7-
if (typeof window !== 'undefined') {
8-
// eslint-disable-next-line no-eval
9-
$_ = eval('$')
10-
}
5+
if (typeof window !== 'undefined') {
6+
;(window as any).staticChildRenders = 0
7+
}
118

12-
useEffect(() => {
13-
if (Array.isArray($_)) {
14-
document.getElementById('react-compiler-enabled-message')!.textContent =
15-
`React compiler is enabled with ${$_!.length} memo slots`
16-
}
17-
})
9+
function StaticChild() {
10+
return (
11+
<Profiler
12+
onRender={(id, phase) => {
13+
;(window as any).staticChildRenders += 1
14+
}}
15+
id="test"
16+
>
17+
<div>static child</div>
18+
</Profiler>
19+
)
20+
}
1821

22+
export default function Page() {
23+
const [count, increment] = useReducer((n) => n + 1, 1)
1924
return (
2025
<>
21-
<div>
22-
<h1 id="react-compiler-enabled-message" />
23-
<p>hello world</p>
24-
</div>
26+
<div data-testid="parent-commits">Parent commits: {count}</div>
27+
<button onClick={increment}>Increment</button>
28+
<StaticChild />
2529
</>
2630
)
2731
}

test/e2e/react-compiler/next.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const nextConfig = {
55
experimental: {
66
reactCompiler: true,
77
},
8+
reactProductionProfiling: true,
89
}
910

1011
module.exports = nextConfig

test/e2e/react-compiler/react-compiler.test.ts

Lines changed: 149 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -16,88 +16,159 @@ function normalizeCodeLocInfo(str) {
1616
)
1717
}
1818

19-
describe.each(['default', 'babelrc'])('react-compiler %s', (variant) => {
20-
const dependencies = (global as any).isNextDeploy
21-
? // `link` is incompatible with the npm version used when this test is deployed
22-
{
23-
'reference-library': 'file:./reference-library',
24-
}
25-
: {
26-
'reference-library': 'link:./reference-library',
27-
}
28-
const { next, isNextDev } = nextTestSetup({
29-
files:
30-
variant === 'babelrc'
31-
? __dirname
32-
: {
33-
app: new FileRef(join(__dirname, 'app')),
34-
'next.config.js': new FileRef(join(__dirname, 'next.config.js')),
35-
'reference-library': new FileRef(
36-
join(__dirname, 'reference-library')
37-
),
38-
},
39-
40-
dependencies: {
41-
'babel-plugin-react-compiler': '19.1.0-rc.2',
42-
...dependencies,
43-
},
44-
})
45-
46-
it('should show an experimental warning', async () => {
47-
await retry(() => {
48-
expect(next.cliOutput).toContain('Experiments (use with caution)')
49-
expect(stripAnsi(next.cliOutput)).toContain('✓ reactCompiler')
19+
describe.each(['default', 'babelrc'] as const)(
20+
'react-compiler %s',
21+
(variant) => {
22+
const dependencies = (global as any).isNextDeploy
23+
? // `link` is incompatible with the npm version used when this test is deployed
24+
{
25+
'reference-library': 'file:./reference-library',
26+
}
27+
: {
28+
'reference-library': 'link:./reference-library',
29+
}
30+
const { next, isNextDev, isTurbopack } = nextTestSetup({
31+
files:
32+
variant === 'babelrc'
33+
? __dirname
34+
: {
35+
app: new FileRef(join(__dirname, 'app')),
36+
'next.config.js': new FileRef(join(__dirname, 'next.config.js')),
37+
'reference-library': new FileRef(
38+
join(__dirname, 'reference-library')
39+
),
40+
},
41+
// TODO: set only config instead once bundlers are consistent
42+
buildArgs: ['--profile'],
43+
dependencies: {
44+
'babel-plugin-react-compiler': '0.0.0-experimental-3fde738-20250918',
45+
...dependencies,
46+
},
47+
})
48+
49+
it('should show an experimental warning', async () => {
50+
await retry(() => {
51+
expect(next.cliOutput).toContain('Experiments (use with caution)')
52+
expect(stripAnsi(next.cliOutput)).toContain('✓ reactCompiler')
53+
})
5054
})
51-
})
5255

53-
it('should render', async () => {
54-
const browser = await next.browser('/')
56+
it('should memoize Components', async () => {
57+
const browser = await next.browser('/')
58+
59+
expect(await browser.eval('window.staticChildRenders')).toEqual(1)
60+
expect(
61+
await browser.elementByCss('[data-testid="parent-commits"]').text()
62+
).toEqual('Parent commits: 1')
63+
64+
await browser.elementByCss('button').click()
65+
await browser.elementByCss('button').click()
66+
await browser.elementByCss('button').click()
67+
68+
expect(await browser.eval('window.staticChildRenders')).toEqual(1)
69+
expect(
70+
await browser.elementByCss('[data-testid="parent-commits"]').text()
71+
).toEqual('Parent commits: 4')
72+
})
73+
74+
it('should work with a library that uses the react-server condition', async () => {
75+
const outputIndex = next.cliOutput.length
76+
await next.render('/library-react-server')
77+
78+
const cliOutput = stripAnsi(next.cliOutput.slice(outputIndex))
79+
expect(cliOutput).not.toMatch(/error/)
80+
})
5581

56-
await retry(async () => {
57-
const text = await browser
58-
.elementByCss('#react-compiler-enabled-message')
82+
it('should work with a library using use client', async () => {
83+
const outputIndex = next.cliOutput.length
84+
await next.render('/library-client')
85+
86+
const cliOutput = stripAnsi(next.cliOutput.slice(outputIndex))
87+
expect(cliOutput).not.toMatch(/error/)
88+
})
89+
90+
it('should name functions in dev', async () => {
91+
const browser = await next.browser('/function-naming')
92+
await browser.waitForElementByCss(
93+
'[data-testid="call-frame"][aria-busy="false"]',
94+
5000
95+
)
96+
97+
const callFrame = await browser
98+
.elementByCss('[data-testid="call-frame"]')
5999
.text()
60-
expect(text).toMatch(/React compiler is enabled with \d+ memo slots/)
100+
const functionName =
101+
variant === 'babelrc'
102+
? // next/babel transpiles away arrow functions defeating the React Compiler naming
103+
'PageUseEffect'
104+
: // expected naming heuristic from React Compiler. This may change in future.
105+
// Just make sure this is the heuristic from the React Compiler not something else.
106+
'Page[useEffect()]'
107+
if (isNextDev) {
108+
if (isTurbopack) {
109+
// FIXME: https://linear.app/vercel/issue/NAR-351
110+
await expect(browser).toDisplayCollapsedRedbox(`
111+
{
112+
"description": "test-top-frame",
113+
"environmentLabel": null,
114+
"label": "Console Error",
115+
"source": null,
116+
"stack": [
117+
"<FIXME-file-protocol>",
118+
],
119+
}
120+
`)
121+
} else {
122+
await expect(browser).toDisplayCollapsedRedbox(`
123+
{
124+
"description": "test-top-frame",
125+
"environmentLabel": null,
126+
"label": "Console Error",
127+
"source": "app/function-naming/page.tsx (8:19) @ ${functionName}
128+
> 8 | const error = new Error('test-top-frame')
129+
| ^",
130+
"stack": [
131+
"${functionName} app/function-naming/page.tsx (8:19)",
132+
],
133+
}
134+
`)
135+
}
136+
// We care more about the sourcemapped frame in the Redbox.
137+
// This assertion is only here to show that the negative assertion below is valid.
138+
if (variant === 'babelrc') {
139+
}
140+
expect(normalizeCodeLocInfo(callFrame)).toEqual(
141+
` at ${functionName} (**)`
142+
)
143+
} else {
144+
expect(normalizeCodeLocInfo(callFrame)).not.toEqual(
145+
` at ${functionName} (**)`
146+
)
147+
}
61148
})
62-
})
63-
64-
it('should work with a library that uses the react-server condition', async () => {
65-
const outputIndex = next.cliOutput.length
66-
await next.render('/library-react-server')
67-
68-
const cliOutput = stripAnsi(next.cliOutput.slice(outputIndex))
69-
expect(cliOutput).not.toMatch(/error/)
70-
})
71-
72-
it('should work with a library using use client', async () => {
73-
const outputIndex = next.cliOutput.length
74-
await next.render('/library-client')
75-
76-
const cliOutput = stripAnsi(next.cliOutput.slice(outputIndex))
77-
expect(cliOutput).not.toMatch(/error/)
78-
})
79-
80-
it('throws if the React Compiler is used in a React Server environment', async () => {
81-
const outputIndex = next.cliOutput.length
82-
const browser = await next.browser('/library-missing-react-server')
83-
84-
const cliOutput = normalizeCodeLocInfo(
85-
stripAnsi(next.cliOutput.slice(outputIndex))
86-
)
87-
if (isNextDev) {
88-
// TODO(NDX-663): Unhelpful error message.
89-
// Should say that the library should have a react-server entrypoint that doesn't use the React Compiler.
90-
expect(cliOutput).toContain(
91-
'' +
92-
"\n ⨯ TypeError: Cannot read properties of undefined (reading 'H')" +
93-
// location not important. Just that this is the only frame.
94-
// TODO: Stack should start at product code. Possible React limitation.
95-
'\n at Container (**)' +
96-
// Will just point to original file location
97-
'\n 2 |'
149+
150+
it('throws if the React Compiler is used in a React Server environment', async () => {
151+
const outputIndex = next.cliOutput.length
152+
const browser = await next.browser('/library-missing-react-server')
153+
154+
const cliOutput = normalizeCodeLocInfo(
155+
stripAnsi(next.cliOutput.slice(outputIndex))
98156
)
157+
if (isNextDev) {
158+
// TODO(NDX-663): Unhelpful error message.
159+
// Should say that the library should have a react-server entrypoint that doesn't use the React Compiler.
160+
expect(cliOutput).toContain(
161+
'' +
162+
"\n ⨯ TypeError: Cannot read properties of undefined (reading 'H')" +
163+
// location not important. Just that this is the only frame.
164+
// TODO: Stack should start at product code. Possible React limitation.
165+
'\n at Container (**)' +
166+
// Will just point to original file location
167+
'\n 2 |'
168+
)
99169

100-
await assertHasRedbox(browser)
101-
}
102-
})
103-
})
170+
await assertHasRedbox(browser)
171+
}
172+
})
173+
}
174+
)

0 commit comments

Comments
 (0)