Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions crates/next-core/src/next_shared/webpack_rules/babel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::{collections::BTreeSet, sync::LazyLock};

use anyhow::{Context, Result};
use regex::Regex;
use serde::Serialize;
use turbo_esregex::EsRegex;
use turbo_rcstr::{RcStr, rcstr};
use turbo_tasks::{ResolvedVc, Vc};
Expand All @@ -16,7 +17,7 @@ use turbopack_core::{
use turbopack_node::transforms::webpack::WebpackLoaderItem;

use crate::{
next_config::{NextConfig, ReactCompilerCompilationMode},
next_config::{NextConfig, ReactCompilerCompilationMode, ReactCompilerOptions},
next_import_map::try_get_next_package,
next_shared::webpack_rules::{
ManuallyConfiguredBuiltinLoaderIssue, WebpackLoaderBuiltinCondition,
Expand Down Expand Up @@ -145,15 +146,38 @@ pub async fn get_babel_loader_rules(
}

let mut loader_conditions = Vec::new();
if let Some(react_compiler_options) = &*react_compiler_options
if let Some(react_compiler_options) = react_compiler_options.as_ref()
&& let Some(babel_plugin_path) =
resolve_babel_plugin_react_compiler(next_config, project_path).await?
{
let react_compiler_options = react_compiler_options.await?;

// we don't want to accept user-supplied `environment` options, but we do want to pass
// `enableNameAnonymousFunctions` down to the babel plugin based on dev/prod.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct EnvironmentOptions {
enable_name_anonymous_functions: bool,
}

#[derive(Serialize)]
struct ResolvedOptions<'a> {
#[serde(flatten)]
base: &'a ReactCompilerOptions,
environment: EnvironmentOptions,
}

let resolved_options = ResolvedOptions {
base: &react_compiler_options,
environment: EnvironmentOptions {
enable_name_anonymous_functions: builtin_conditions
.contains(&WebpackLoaderBuiltinCondition::Development),
},
};
let react_compiler_plugins =
serde_json::Value::Array(vec![serde_json::Value::Array(vec![
serde_json::Value::String(babel_plugin_path.into_owned()),
serde_json::to_value(&*react_compiler_options)
serde_json::to_value(resolved_options)
.expect("react compiler options JSON serialization should never fail"),
])]);

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@
"abort-controller": "3.0.0",
"alex": "9.1.0",
"async-sema": "3.0.1",
"babel-plugin-react-compiler": "19.1.0-rc.2",
"babel-plugin-react-compiler": "0.0.0-experimental-3fde738-20250918",
"browserslist": "4.25.1",
"buffer": "5.6.0",
"cheerio": "0.22.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@
"async-sema": "3.0.0",
"axe-playwright": "2.0.3",
"babel-loader": "10.0.0",
"babel-plugin-react-compiler": "19.1.0-rc.2",
"babel-plugin-react-compiler": "0.0.0-experimental-3fde738-20250918",
"babel-plugin-transform-define": "2.0.0",
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
"browserify-zlib": "0.2.0",
Expand Down
11 changes: 5 additions & 6 deletions packages/next/src/build/get-babel-loader-config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { EnvironmentConfig } from 'babel-plugin-react-compiler'
import path from 'path'
import type { JSONValue, ReactCompilerOptions } from '../server/config-shared'
import type { NextBabelLoaderOptions } from './babel/loader/types'
Expand All @@ -23,16 +24,14 @@ const getReactCompilerPlugins = (
return undefined
}

const defaultOptions: ReactCompilerOptions = isDev
? {
// TODO: enable `environment.enableNameAnonymousFunctions`Ï
}
: {}
const environment: Pick<EnvironmentConfig, 'enableNameAnonymousFunctions'> = {
enableNameAnonymousFunctions: isDev,
}
const options: ReactCompilerOptions =
typeof maybeOptions === 'boolean' ? {} : maybeOptions
const compilerOptions: JSONValue = {
...defaultOptions,
...options,
environment,
}
return [[getReactCompiler(), compilerOptions]]
}
Expand Down
43 changes: 21 additions & 22 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions test/e2e/react-compiler/app/function-naming/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use client'

import { useEffect, useState } from 'react'

export default function Page() {
const [callFrame, setCallFrame] = useState(null)
useEffect(() => {
const error = new Error('test-top-frame')
console.error(error)

const callStack = new Error('test-top-frame').stack.split(
'test-top-frame\n'
)[1]
// indices might change due to different compiler optimizations
const callFrame = callStack.split('\n')[0]
Comment on lines +11 to +15
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const callStack = new Error('test-top-frame').stack.split(
'test-top-frame\n'
)[1]
// indices might change due to different compiler optimizations
const callFrame = callStack.split('\n')[0]
const stackParts = new Error('test-top-frame').stack?.split(
'test-top-frame\n'
)
const callStack = stackParts?.[1]
// indices might change due to different compiler optimizations
const callFrame = callStack?.split('\n')[0] || 'unknown'

The stack trace parsing code can throw a TypeError if the stack format doesn't match expectations, causing the test to fail unexpectedly.

View Details

Analysis

TypeError in stack trace parsing can crash function-naming test

What fails: page.tsx useEffect in test/e2e/react-compiler/app/function-naming/ throws TypeError when Error.stack format varies across JavaScript engines

How to reproduce:

// Simulate non-standard stack format (can occur in different browsers/engines)
const mockError = { stack: 'Error: some-other-error\n    at somewhere' };
const callStack = mockError.stack.split('test-top-frame\n')[1]; // undefined
const callFrame = callStack.split('\n')[0]; // TypeError: Cannot read properties of undefined (reading 'split')

Result: TypeError: "Cannot read properties of undefined (reading 'split')" when stack doesn't contain expected pattern

Expected: Should handle missing/malformed stack traces gracefully per MDN Error.stack docs - stack format varies between engines and isn't standardized

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fine. Then the test fails.

setCallFrame(callFrame)
}, [])
return (
<pre data-testid="call-frame" aria-busy={callFrame === null}>
{String(callFrame)}
</pre>
)
}
38 changes: 21 additions & 17 deletions test/e2e/react-compiler/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,31 @@
'use client'

import { useEffect } from 'react'
import { Profiler, useReducer } from 'react'

export default function Page() {
let $_: any
if (typeof window !== 'undefined') {
// eslint-disable-next-line no-eval
$_ = eval('$')
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Compiler now bails out when it detects eval. The new test will assert on more specific Compiler behavior by checking that we actually don't re-render children due to memoization.

}
if (typeof window !== 'undefined') {
;(window as any).staticChildRenders = 0
}

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

export default function Page() {
const [count, increment] = useReducer((n) => n + 1, 1)
return (
<>
<div>
<h1 id="react-compiler-enabled-message" />
<p>hello world</p>
</div>
<div data-testid="parent-commits">Parent commits: {count}</div>
<button onClick={increment}>Increment</button>
<StaticChild />
</>
)
}
1 change: 1 addition & 0 deletions test/e2e/react-compiler/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const nextConfig = {
experimental: {
reactCompiler: true,
},
reactProductionProfiling: true,
}

module.exports = nextConfig
Loading
Loading