diff --git a/Cargo.lock b/Cargo.lock index 5935c16399c9..511d1ca72343 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2328,6 +2328,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -3102,6 +3103,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "home" version = "0.5.9" @@ -4967,6 +4977,180 @@ dependencies = [ "turbo-unix-path", ] +[[package]] +name = "nextjs_react_compiler" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b22b5a98bd2b715d54384ca378d8451e7961c5c16e970bdaff0b5358ab7551" +dependencies = [ + "indexmap 2.13.0", + "nextjs_react_compiler_ast", + "nextjs_react_compiler_diagnostics", + "nextjs_react_compiler_hir", + "nextjs_react_compiler_inference", + "nextjs_react_compiler_lowering", + "nextjs_react_compiler_optimization", + "nextjs_react_compiler_reactive_scopes", + "nextjs_react_compiler_ssa", + "nextjs_react_compiler_typeinference", + "nextjs_react_compiler_validation", + "serde", + "serde_json", +] + +[[package]] +name = "nextjs_react_compiler_ast" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a801871be2129e520a2f12139f88db11c67000c24fe3ef33a39a951e3a7ee8f4" +dependencies = [ + "indexmap 2.13.0", + "serde", + "serde-transcode", + "serde_json", +] + +[[package]] +name = "nextjs_react_compiler_diagnostics" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad98f822369142a319e223fa7828b55908f2ff5e66d6055386c9c6220fcec8d4" +dependencies = [ + "serde", +] + +[[package]] +name = "nextjs_react_compiler_hir" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9781a9729984e1fbe1e7910a4b8b1a1085105c27e9977b9a8fd7de987f6f45c1" +dependencies = [ + "indexmap 2.13.0", + "nextjs_react_compiler_diagnostics", + "serde", + "serde_json", +] + +[[package]] +name = "nextjs_react_compiler_inference" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32ad078392098a3eef51a40eb2ff01fc6ba30e83f8ddb81b1a9c6aa1aefb0888" +dependencies = [ + "indexmap 2.13.0", + "nextjs_react_compiler_diagnostics", + "nextjs_react_compiler_hir", + "nextjs_react_compiler_lowering", + "nextjs_react_compiler_optimization", + "nextjs_react_compiler_ssa", + "nextjs_react_compiler_utils", +] + +[[package]] +name = "nextjs_react_compiler_lowering" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4f7aa0de73b7371b17b8a4bc8293dfc014a815443cf4f189bdc04ff292728e" +dependencies = [ + "indexmap 2.13.0", + "nextjs_react_compiler_ast", + "nextjs_react_compiler_diagnostics", + "nextjs_react_compiler_hir", + "serde_json", +] + +[[package]] +name = "nextjs_react_compiler_optimization" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed5edc18e4b472c022f7ffa8ccee53ddfb47d176cf1e883b4ed7949721ac585" +dependencies = [ + "indexmap 2.13.0", + "nextjs_react_compiler_diagnostics", + "nextjs_react_compiler_hir", + "nextjs_react_compiler_lowering", + "nextjs_react_compiler_ssa", +] + +[[package]] +name = "nextjs_react_compiler_reactive_scopes" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bd347140ecfc25f5aae2aacf777bbd73df510bc4d78d974e56e8817265f4e22" +dependencies = [ + "hmac", + "indexmap 2.13.0", + "nextjs_react_compiler_ast", + "nextjs_react_compiler_diagnostics", + "nextjs_react_compiler_hir", + "serde_json", + "sha2", +] + +[[package]] +name = "nextjs_react_compiler_ssa" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a837a1efcea17cfd695e00d72fb5e3c31ad60dcbc9362aa9a5ae26c086dba373" +dependencies = [ + "indexmap 2.13.0", + "nextjs_react_compiler_diagnostics", + "nextjs_react_compiler_hir", + "nextjs_react_compiler_lowering", +] + +[[package]] +name = "nextjs_react_compiler_swc" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe86fd086bd939c6ff101af9b0afbdd2c094f63116ea364c3a8e53ccf40afc5" +dependencies = [ + "indexmap 2.13.0", + "nextjs_react_compiler", + "nextjs_react_compiler_ast", + "nextjs_react_compiler_diagnostics", + "nextjs_react_compiler_hir", + "serde", + "serde_json", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_codegen", + "swc_ecma_parser", + "swc_ecma_visit", +] + +[[package]] +name = "nextjs_react_compiler_typeinference" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4513a9d17b98adb088a070764a7e438b28333e244f0d93fac1c5994282dd6f4b" +dependencies = [ + "nextjs_react_compiler_diagnostics", + "nextjs_react_compiler_hir", + "nextjs_react_compiler_ssa", +] + +[[package]] +name = "nextjs_react_compiler_utils" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae4bbd3dea223b62e1a53176c17520b5c6564a0949a39e1abd9fe9a3acc2237" +dependencies = [ + "indexmap 2.13.0", +] + +[[package]] +name = "nextjs_react_compiler_validation" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5110ff50d5e8898d0d1515694e8a388e048eb5bd0bfdedd1d6f9cefad4eb32f3" +dependencies = [ + "indexmap 2.13.0", + "nextjs_react_compiler_diagnostics", + "nextjs_react_compiler_hir", +] + [[package]] name = "nix" version = "0.30.1" @@ -6994,6 +7178,15 @@ dependencies = [ "wyz 0.2.0", ] +[[package]] +name = "serde-transcode" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "590c0e25c2a5bb6e85bf5c1bce768ceb86b316e7a01bdf07d2cb4ec2271990e2" +dependencies = [ + "serde", +] + [[package]] name = "serde-wasm-bindgen" version = "0.4.5" @@ -10417,6 +10610,9 @@ dependencies = [ "indexmap 2.13.0", "indoc", "itertools 0.10.5", + "next-custom-transforms", + "nextjs_react_compiler", + "nextjs_react_compiler_swc", "num-bigint", "num-traits", "parking_lot", diff --git a/Cargo.toml b/Cargo.toml index 15c56f68bfcc..6511503f51ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -209,6 +209,9 @@ turbopack-trace-server = { path = "turbopack/crates/turbopack-trace-server" } turbopack-trace-utils = { path = "turbopack/crates/turbopack-trace-utils" } turbopack-wasm = { path = "turbopack/crates/turbopack-wasm" } +react_compiler_swc = { version = "0.1.5", package = "nextjs_react_compiler_swc" } +react_compiler = { version = "0.1.5", package = "nextjs_react_compiler" } + # SWC crates swc_core = { version = "65.0.3", default-features = false, features = [ "ecma_loader_lru", diff --git a/crates/next-core/src/next_client/context.rs b/crates/next-core/src/next_client/context.rs index c80d22eb46b4..a7018fccf878 100644 --- a/crates/next-core/src/next_client/context.rs +++ b/crates/next-core/src/next_client/context.rs @@ -343,6 +343,8 @@ pub async fn get_client_module_options_context( .resolved_cell() }); + let enable_rust_react_compiler = *next_config.rust_react_compiler().await?; + let module_options_context = ModuleOptionsContext { ecmascript: EcmascriptOptionsContext { esm_url_rewrite_behavior: Some(UrlRewriteBehavior::Relative), @@ -423,6 +425,7 @@ pub async fn get_client_module_options_context( enable_jsx: Some(jsx_runtime_options), enable_typescript_transform: Some(tsconfig), enable_decorators: Some(decorators_options.to_resolved().await?), + enable_rust_react_compiler, ..module_options_context.ecmascript.clone() }, enable_webpack_loaders, diff --git a/crates/next-core/src/next_config.rs b/crates/next-core/src/next_config.rs index 4985fcea69ae..3d268e4dc733 100644 --- a/crates/next-core/src/next_config.rs +++ b/crates/next-core/src/next_config.rs @@ -29,7 +29,10 @@ use turbopack_core::{ module_graph::style_groups::StyleGroupsAlgorithm, resolve::ResolveAliasMap, }; -use turbopack_ecmascript::{OptionTreeShaking, TreeShakingMode}; +use turbopack_ecmascript::{ + OptionTreeShaking, TreeShakingMode, + transform::{OptionReactCompilerCompilationMode, ReactCompilerCompilationMode}, +}; use turbopack_ecmascript_plugins::transform::{ emotion::EmotionTransformConfig, relay::RelayConfig, styled_components::StyledComponentsTransformConfig, @@ -927,16 +930,6 @@ pub enum MdxRsOptions { Option(MdxTransformOptions), } -#[turbo_tasks::value(shared, operation)] -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum ReactCompilerCompilationMode { - #[default] - Infer, - Annotation, - All, -} - #[turbo_tasks::value(shared, operation)] #[derive(Clone, Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] @@ -1327,6 +1320,8 @@ pub struct ExperimentalConfig { turbopack_local_postcss_config: Option, // Whether to enable the global-not-found convention global_not_found: Option, + /// Experimental Rust React compiler (Turbopack only); requires `reactCompiler`. + turbopack_rust_react_compiler: Option, /// Defaults to false in development mode, true in production mode. turbopack_remove_unused_imports: Option, /// Defaults to false in development mode, true in production mode. @@ -2128,6 +2123,26 @@ impl NextConfig { options.cell() } + /// Returns compilation mode when both `reactCompiler` and `turbopackRustReactCompiler` are set; + /// `None` otherwise. + #[turbo_tasks::function] + pub fn rust_react_compiler(&self) -> Vc { + let use_rust = self + .experimental + .turbopack_rust_react_compiler + .unwrap_or(false); + let mode = match (use_rust, &self.react_compiler) { + (true, Some(ReactCompilerOptionsOrBoolean::Boolean(true))) => { + Some(ReactCompilerCompilationMode::Infer) + } + (true, Some(ReactCompilerOptionsOrBoolean::Option(opts))) => { + Some(opts.compilation_mode) + } + _ => None, + }; + Vc::cell(mode) + } + #[turbo_tasks::function] pub fn sass_config(&self) -> Vc { Vc::cell(self.sass_options.clone().unwrap_or_default()) diff --git a/crates/next-core/src/next_server/context.rs b/crates/next-core/src/next_server/context.rs index 30aa3f38ca5a..e4a3d9a10830 100644 --- a/crates/next-core/src/next_server/context.rs +++ b/crates/next-core/src/next_server/context.rs @@ -555,6 +555,8 @@ pub async fn get_server_module_options_context( .flatten() .collect(); + let enable_rust_react_compiler = *next_config.rust_react_compiler().await?; + let source_maps = *next_config.server_source_maps().await?; let module_options_context = ModuleOptionsContext { ecmascript: EcmascriptOptionsContext { @@ -659,6 +661,7 @@ pub async fn get_server_module_options_context( enable_jsx: Some(jsx_runtime_options), enable_typescript_transform: Some(tsconfig), enable_decorators: Some(decorators_options.to_resolved().await?), + enable_rust_react_compiler: None, ..module_options_context.ecmascript }, enable_webpack_loaders, @@ -721,6 +724,7 @@ pub async fn get_server_module_options_context( enable_jsx: Some(jsx_runtime_options), enable_typescript_transform: Some(tsconfig), enable_decorators: Some(decorators_options.to_resolved().await?), + enable_rust_react_compiler, ..module_options_context.ecmascript }, enable_webpack_loaders, @@ -801,6 +805,7 @@ pub async fn get_server_module_options_context( enable_jsx: Some(rsc_jsx_runtime_options), enable_typescript_transform: Some(tsconfig), enable_decorators: Some(decorators_options.to_resolved().await?), + enable_rust_react_compiler: None, ..module_options_context.ecmascript }, enable_webpack_loaders, @@ -877,6 +882,7 @@ pub async fn get_server_module_options_context( enable_jsx: Some(rsc_jsx_runtime_options), enable_typescript_transform: Some(tsconfig), enable_decorators: Some(decorators_options.to_resolved().await?), + enable_rust_react_compiler: None, ..module_options_context.ecmascript }, enable_webpack_loaders, @@ -964,6 +970,7 @@ pub async fn get_server_module_options_context( enable_jsx: Some(jsx_runtime_options), enable_typescript_transform: Some(tsconfig), enable_decorators: Some(decorators_options.to_resolved().await?), + enable_rust_react_compiler: None, ..module_options_context.ecmascript }, enable_webpack_loaders, diff --git a/crates/next-core/src/next_shared/webpack_rules/babel.rs b/crates/next-core/src/next_shared/webpack_rules/babel.rs index d208cd00a278..cb25a7d1a1ff 100644 --- a/crates/next-core/src/next_shared/webpack_rules/babel.rs +++ b/crates/next-core/src/next_shared/webpack_rules/babel.rs @@ -15,12 +15,11 @@ use turbopack_core::{ resolve::{node::node_cjs_resolve_options, parse::Request, pattern::Pattern, resolve}, source::Source, }; +use turbopack_ecmascript::transform::ReactCompilerCompilationMode; use turbopack_node::transforms::webpack::WebpackLoaderItem; use crate::{ - next_config::{ - NextConfig, ReactCompilerCompilationMode, ReactCompilerOptions, ReactCompilerTarget, - }, + next_config::{NextConfig, ReactCompilerOptions, ReactCompilerTarget}, next_import_map::try_get_next_package, next_shared::webpack_rules::{ ManuallyConfiguredBuiltinLoaderIssue, WebpackLoaderBuiltinCondition, @@ -119,10 +118,13 @@ pub async fn get_babel_loader_rules( } let react_compiler_options = next_config.react_compiler_options().await?; + let use_rust_react_compiler = next_config.rust_react_compiler().await?.is_some(); - // if there's no babel config and react-compiler shouldn't be enabled, bail out early + // bail early: no babel config, no react-compiler, or Rust React Compiler is active (babel has + // nothing to do). if babel_config_path.is_none() && (react_compiler_options.is_none() + || use_rust_react_compiler || !builtin_conditions.contains(&WebpackLoaderBuiltinCondition::Browser)) { return Ok(Vec::new()); @@ -151,6 +153,7 @@ pub async fn get_babel_loader_rules( let mut loader_conditions = Vec::new(); if let Some(react_compiler_options) = react_compiler_options.as_ref() + && !use_rust_react_compiler && let Some(babel_plugin_path) = resolve_babel_plugin_react_compiler(next_config, project_path).await? { diff --git a/packages/next/errors.json b/packages/next/errors.json index 8f8511df230c..a278ff5ae3b3 100644 --- a/packages/next/errors.json +++ b/packages/next/errors.json @@ -1360,5 +1360,7 @@ "1359": "Route \"%s\": Next.js encountered uncached or runtime data during prerendering.\\n\\n\\`fetch(...)\\`, \\`cookies()\\`, \\`headers()\\`, \\`params\\`, \\`searchParams\\`, or \\`connection()\\` accessed outside of \\`\\` prevents the route from being prerendered, blocking the page load and leading to a slower user experience.\\n\\nWays to fix this:\\n - [cache] Cache the data access with \\`\"use cache\"\\`\\n https://nextjs.org/docs/messages/blocking-prerender-dynamic#cache-the-component-or-data\\n - [stream] Provide a placeholder with \\`\\` around the data access\\n https://nextjs.org/docs/messages/blocking-prerender-dynamic#wrap-in-or-move-into-suspense\\n - [cache] If the runtime data is \\`params\\` and they're known, prerender them with \\`generateStaticParams\\`\\n https://nextjs.org/docs/messages/blocking-prerender-runtime#for-known-params-prerender\\n - [block] Set \\`export const instant = false\\` to silence this warning and allow a blocking route\\n https://nextjs.org/docs/messages/blocking-prerender-dynamic#allow-blocking-route", "1360": "`retry()` can only be used in the App Router. Use `reset()` in the Pages Router.", "1361": "`catchError` can only be used in Client Components.", - "1362": "A navigated to \"%s\", but Partial Prefetching is not enabled for that route, so its dynamic data was included in the prefetch. Enable Partial Prefetching app-wide by setting \\`partialPrefetching: true\\` in next.config, or per-route by exporting \\`const prefetch = 'partial'\\` from the page or layout." + "1362": "A navigated to \"%s\", but Partial Prefetching is not enabled for that route, so its dynamic data was included in the prefetch. Enable Partial Prefetching app-wide by setting \\`partialPrefetching: true\\` in next.config, or per-route by exporting \\`const prefetch = 'partial'\\` from the page or layout.", + "1363": "\\`experimental.turbopackRustReactCompiler\\` is only supported with Turbopack. Please remove the option or run Next.js with Turbopack in %s.", + "1364": "\\`experimental.turbopackRustReactCompiler\\` requires \\`reactCompiler\\` to be enabled. Please add \\`reactCompiler: true\\` in %s." } diff --git a/packages/next/src/server/config-schema.ts b/packages/next/src/server/config-schema.ts index 95ec10c9c8da..ad3ad21d2675 100644 --- a/packages/next/src/server/config-schema.ts +++ b/packages/next/src/server/config-schema.ts @@ -435,6 +435,7 @@ export const experimentalSchema = { }) .optional(), globalNotFound: z.boolean().optional(), + turbopackRustReactCompiler: z.boolean().optional(), browserDebugInfoInTerminal: z .union([ z.boolean(), diff --git a/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts index 4b39e954e435..efd986192ce1 100644 --- a/packages/next/src/server/config-shared.ts +++ b/packages/next/src/server/config-shared.ts @@ -1164,6 +1164,12 @@ export interface ExperimentalConfig { */ globalNotFound?: boolean + /** + * @experimental Use the Rust port of the React compiler (Turbopack only). + * Requires `reactCompiler` to be enabled. + */ + turbopackRustReactCompiler?: boolean + /** * Enable debug information to be forwarded from browser to dev server stdout/stderr. * diff --git a/packages/next/src/server/config.ts b/packages/next/src/server/config.ts index ba22af82472e..928837ad28a3 100644 --- a/packages/next/src/server/config.ts +++ b/packages/next/src/server/config.ts @@ -512,6 +512,26 @@ function assignDefaultsAndValidate( `Please remove the option or run Next.js with webpack in ${configFileName}.` ) } + + if ( + result.experimental.turbopackRustReactCompiler && + !process.env.TURBOPACK + ) { + throw new Error( + `\`experimental.turbopackRustReactCompiler\` is only supported with Turbopack. ` + + `Please remove the option or run Next.js with Turbopack in ${configFileName}.` + ) + } + + if ( + result.experimental.turbopackRustReactCompiler && + !result.reactCompiler + ) { + throw new Error( + `\`experimental.turbopackRustReactCompiler\` requires \`reactCompiler\` to be enabled. ` + + `Please add \`reactCompiler: true\` in ${configFileName}.` + ) + } } if (result.experimental.cachedNavigations && !result.cacheComponents) { diff --git a/test/e2e/react-compiler/react-compiler.test.ts b/test/e2e/react-compiler/react-compiler.test.ts index 35ef9429bc81..253466f71064 100644 --- a/test/e2e/react-compiler/react-compiler.test.ts +++ b/test/e2e/react-compiler/react-compiler.test.ts @@ -1,5 +1,5 @@ import { isReact18, nextTestSetup, FileRef } from 'e2e-utils' -import { waitForRedbox } from 'next-test-utils' +import { waitForRedbox, shouldUseTurbopack } from 'next-test-utils' import { join } from 'path' import stripAnsi from 'strip-ansi' @@ -16,9 +16,14 @@ function normalizeCodeLocInfo(str) { ) } -describe.each(['default', 'babelrc'] as const)( +describe.each(['default', 'babelrc', 'rust'] as const)( 'react-compiler %s', (variant) => { + if (variant === 'rust' && !shouldUseTurbopack()) { + it.skip('rust react-compiler requires Turbopack', () => {}) + return + } + const dependencies = (global as any).isNextDeploy ? // `link` is incompatible with the npm version used when this test is deployed { @@ -34,7 +39,17 @@ describe.each(['default', 'babelrc'] as const)( : { app: new FileRef(join(__dirname, 'app')), pages: new FileRef(join(__dirname, 'pages')), - 'next.config.js': new FileRef(join(__dirname, 'next.config.js')), + 'next.config.js': + variant === 'rust' + ? ` + /** @type {import('next').NextConfig} */ + module.exports = { + reactCompiler: true, + experimental: { turbopackRustReactCompiler: true }, + reactProductionProfiling: true, + } + ` + : new FileRef(join(__dirname, 'next.config.js')), 'reference-library': new FileRef( join(__dirname, 'reference-library') ), @@ -102,7 +117,14 @@ describe.each(['default', 'babelrc'] as const)( expect(cliOutput).not.toMatch(/error/) }) - it('should name functions in dev', async () => { + // TODO: The Rust port of the React Compiler does not yet implement the + // `Page[useEffect()]` naming heuristic that the Babel plugin applies, so the + // memoized temporaries surface as `t0`, `t1`, … in stack frames. + // + // Re-enable once `nextjs_react_compiler` learns to set debug names on generated + // expressions. + const it_ = variant === 'rust' ? it.skip : it + it_('should name functions in dev', async () => { const browser = await next.browser('/function-naming') await browser.waitForElementByCss( '[data-testid="call-frame"][aria-busy="false"]', diff --git a/turbopack/crates/turbopack-ecmascript/Cargo.toml b/turbopack/crates/turbopack-ecmascript/Cargo.toml index 13f1e427c6bd..f46cf72e69ac 100644 --- a/turbopack/crates/turbopack-ecmascript/Cargo.toml +++ b/turbopack/crates/turbopack-ecmascript/Cargo.toml @@ -43,6 +43,9 @@ regex = { workspace = true } rustc-hash = { workspace = true } serde = { workspace = true } serde_json = { workspace = true, features = ["raw_value"] } +react_compiler_swc = { workspace = true } +react_compiler = { workspace = true } +next-custom-transforms = { workspace = true, default-features = false } swc_sourcemap = { workspace = true } smallvec = { workspace = true } strsim = { workspace = true } @@ -89,6 +92,9 @@ swc_core = { workspace = true, features = [ "base", ] } +[features] +default = [] + [dev-dependencies] criterion = { workspace = true, features = ["async_tokio"] } rstest = { workspace = true } diff --git a/turbopack/crates/turbopack-ecmascript/src/parse.rs b/turbopack/crates/turbopack-ecmascript/src/parse.rs index 9157890f8acf..b5f509d2d2d5 100644 --- a/turbopack/crates/turbopack-ecmascript/src/parse.rs +++ b/turbopack/crates/turbopack-ecmascript/src/parse.rs @@ -564,6 +564,7 @@ async fn parse_file_content( query_str: query, file_path: fs_path.clone(), source, + source_text: &fm.src, node_env, }; let span = tracing::trace_span!("transforms"); diff --git a/turbopack/crates/turbopack-ecmascript/src/transform/mod.rs b/turbopack/crates/turbopack-ecmascript/src/transform/mod.rs index 6c4ca8a238a5..3aad327e95be 100644 --- a/turbopack/crates/turbopack-ecmascript/src/transform/mod.rs +++ b/turbopack/crates/turbopack-ecmascript/src/transform/mod.rs @@ -2,6 +2,7 @@ use std::{fmt::Debug, hash::Hash, sync::Arc}; use anyhow::{Result, bail}; use async_trait::async_trait; +use serde::{Deserialize, Serialize}; use swc_core::{ atoms::{Atom, atom}, base::SwcComments, @@ -21,10 +22,14 @@ use swc_core::{ }, quote, }; -use turbo_rcstr::RcStr; +use turbo_rcstr::{RcStr, rcstr}; use turbo_tasks::{ResolvedVc, Vc}; use turbo_tasks_fs::FileSystemPath; -use turbopack_core::{environment::Environment, source::Source}; +use turbopack_core::{ + environment::Environment, + issue::{Issue, IssueExt, IssueSeverity, IssueSource, IssueStage, StyledString}, + source::Source, +}; use crate::runtime_functions::{TURBOPACK_MODULE, TURBOPACK_REFRESH}; @@ -82,8 +87,34 @@ pub enum EcmascriptInputTransform { emit_decorators_metadata: bool, use_define_for_class_fields: bool, }, + ReactCompilerRust { + compilation_mode: ReactCompilerCompilationMode, + }, +} + +#[turbo_tasks::value(shared, operation)] +#[derive(Default, Debug, Clone, Copy, Hash, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum ReactCompilerCompilationMode { + #[default] + Infer, + Annotation, + All, } +impl ReactCompilerCompilationMode { + pub fn as_str(self) -> &'static str { + match self { + ReactCompilerCompilationMode::Infer => "infer", + ReactCompilerCompilationMode::Annotation => "annotation", + ReactCompilerCompilationMode::All => "all", + } + } +} + +#[turbo_tasks::value(transparent)] +pub struct OptionReactCompilerCompilationMode(Option); + /// The CustomTransformer trait allows you to implement your own custom SWC /// transformer to run over all ECMAScript files imported in the graph. #[async_trait] @@ -134,6 +165,9 @@ pub struct TransformContext<'a> { pub query_str: RcStr, pub file_path: FileSystemPath, pub source: ResolvedVc>, + /// Original source text; used by transforms that need the raw text (e.g. + /// `react_compiler_swc`). + pub source_text: &'a str, /// The value of `process.env.NODE_ENV` for this compilation /// (e.g. `"development"` or `"production"`). pub node_env: RcStr, @@ -343,6 +377,9 @@ impl EcmascriptInputTransform { apply_transform(program, helpers, decorators(config)) } + EcmascriptInputTransform::ReactCompilerRust { compilation_mode } => { + apply_rust_react_compiler(program, ctx, helpers, *compilation_mode).await? + } EcmascriptInputTransform::Plugin(transform) => { // We cannot pass helpers to plugins, so we return them as is transform.await?.transform(program, ctx).await?; @@ -352,6 +389,118 @@ impl EcmascriptInputTransform { } } +#[turbo_tasks::value] +struct ReactCompilerIssue { + source: IssueSource, + message: RcStr, + severity: IssueSeverity, +} + +#[async_trait] +#[turbo_tasks::value_impl] +impl Issue for ReactCompilerIssue { + fn severity(&self) -> IssueSeverity { + self.severity + } + + async fn file_path(&self) -> anyhow::Result { + self.source.file_path().await + } + + fn source(&self) -> Option { + Some(self.source) + } + + fn stage(&self) -> IssueStage { + IssueStage::Transform + } + + async fn title(&self) -> anyhow::Result { + Ok(StyledString::Text(rcstr!("React Compiler"))) + } + + async fn description(&self) -> anyhow::Result> { + Ok(Some(StyledString::Text(self.message.clone()))) + } +} + +async fn apply_rust_react_compiler( + program: &mut Program, + ctx: &TransformContext<'_>, + helpers: HelperData, + compilation_mode: ReactCompilerCompilationMode, +) -> Result { + let Program::Module(module) = program else { + return Ok(helpers); + }; + + let options = react_compiler_swc_options(ctx, compilation_mode); + let result = react_compiler_swc::transform(module, ctx.source_text, options); + + for diag in &result.diagnostics { + let issue_source = match diag.span { + Some((start, end)) => IssueSource::from_swc_offsets(ctx.source, start, end), + None => IssueSource::from_source_only(ctx.source), + }; + + // React Compiler errors are non-fatal; downgrade to Warning. + let severity = IssueSeverity::Warning; + ReactCompilerIssue { + source: issue_source, + message: RcStr::from(diag.message.as_str()), + severity, + } + .resolved_cell() + .emit(); + } + + if let Some(compiled_module) = result.module { + *program = Program::Module(compiled_module); + + // TODO(react-compiler-swc): The Rust React Compiler emits every identifier with + // `SyntaxContext::empty()` in `convert_ast_reverse.rs`. + // + // Remove this once `nextjs_react_compiler_swc` + // preserves/assigns contexts on the converted AST. + program.mutate(swc_core::ecma::transforms::base::resolver( + ctx.unresolved_mark, + ctx.top_level_mark, + true, + )); + } + + Ok(helpers) +} + +fn react_compiler_swc_options( + ctx: &TransformContext<'_>, + compilation_mode: ReactCompilerCompilationMode, +) -> react_compiler::entrypoint::plugin_options::PluginOptions { + use react_compiler::entrypoint::plugin_options::{CompilerTarget, PluginOptions}; + + PluginOptions { + should_compile: true, + enable_reanimated: false, + is_dev: ctx.node_env != "production", + filename: Some(ctx.file_name_str.to_string()), + compilation_mode: compilation_mode.as_str().to_string(), + panic_threshold: "none".to_string(), + target: CompilerTarget::Version("19".to_string()), + gating: None, + dynamic_gating: None, + no_emit: false, + output_mode: None, + eslint_suppression_rules: None, + flow_suppressions: false, + ignore_use_no_forget: false, + custom_opt_out_directives: None, + environment: Default::default(), + source_code: None, + profiling: false, + debug: false, + } +} + fn apply_transform(program: &mut Program, helpers: HelperData, op: impl Pass) -> HelperData { let helpers = Helpers::from_data(helpers); HELPERS.set(&helpers, || { diff --git a/turbopack/crates/turbopack-tests/tests/react_compiler.rs b/turbopack/crates/turbopack-tests/tests/react_compiler.rs new file mode 100644 index 000000000000..f3c09d08447c --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/react_compiler.rs @@ -0,0 +1,62 @@ +//! Asserts that the Rust React compiler transform fires on the +//! `source_maps/react-compiler` snapshot fixture: output must +//! contain memoized cache slots, not the original un-compiled form. + +#![cfg(test)] + +use std::{fs, path::Path}; + +fn fixture_output_dir() -> std::path::PathBuf { + Path::new(env!("TURBO_PNPM_WORKSPACE_DIR")) + .join("turbopack/crates/turbopack-tests/tests/snapshot/source_maps/react-compiler/output") +} + +/// Find the JS chunk that contains the compiled Component.jsx content. +fn find_component_chunk() -> String { + let dir = fixture_output_dir(); + assert!( + dir.exists(), + "output directory not found — run `UPDATE=1 cargo test -p turbopack-tests --test snapshot \ + -- source_maps__react_compiler` to generate it" + ); + for entry in fs::read_dir(&dir).expect("failed to read output dir") { + let path = entry.unwrap().path(); + if path.extension().and_then(|e| e.to_str()) != Some("js") { + continue; + } + let content = fs::read_to_string(&path).expect("failed to read chunk"); + if content.contains("Component.jsx") { + return content; + } + } + panic!("no chunk containing Component.jsx found in {dir:?}"); +} + +#[test] +fn react_compiler_inserts_memo_cache() { + let chunk = find_component_chunk(); + assert!( + chunk.contains("compiler-runtime") && chunk.contains("[\"c\"]"), + "expected the React compiler runtime cache helper (`c` from `react/compiler-runtime`) in \ + compiled output — React compiler may not have run" + ); +} + +#[test] +fn react_compiler_inserts_cache_slots() { + let chunk = find_component_chunk(); + assert!( + chunk.contains("$[0]"), + "expected cache slot reads (`$[0]`) — React compiler may not have run" + ); +} + +#[test] +fn react_compiler_gates_jsx_on_cache_slots() { + let chunk = find_component_chunk(); + assert!( + chunk.contains("$[0] !==") || chunk.contains("$[1] !=="), + "expected cache-invalidation gate (`$[N] !== dep`) — memoization may not have been \ + applied to JSX output" + ); +} diff --git a/turbopack/crates/turbopack-tests/tests/snapshot.rs b/turbopack/crates/turbopack-tests/tests/snapshot.rs index c0fd39e98413..68c15004b8bd 100644 --- a/turbopack/crates/turbopack-tests/tests/snapshot.rs +++ b/turbopack/crates/turbopack-tests/tests/snapshot.rs @@ -57,7 +57,7 @@ use turbopack_core::{ }; use turbopack_ecmascript::{ AnalyzeMode, CustomTransformer, EcmascriptInputTransform, TransformPlugin, TreeShakingMode, - chunk::EcmascriptChunkType, + chunk::EcmascriptChunkType, transform::ReactCompilerCompilationMode, }; use turbopack_ecmascript_plugins::transform::{ emotion::{EmotionTransformConfig, EmotionTransformer}, @@ -102,6 +102,8 @@ struct SnapshotOptions { source_map_source_type: SourceMapSourceType, #[serde(default = "default_chunk_loading_global")] chunk_loading_global: String, + #[serde(default)] + enable_rust_react_compiler: bool, } #[derive(Debug, Deserialize, Default)] @@ -135,6 +137,7 @@ impl Default for SnapshotOptions { enable_debug_ids: false, source_map_source_type: SourceMapSourceType::default(), chunk_loading_global: default_chunk_loading_global(), + enable_rust_react_compiler: false, } } } @@ -402,6 +405,9 @@ async fn run_test_operation(resource: RcStr) -> Result> { ignore_dynamic_requests: true, infer_module_side_effects: true, enable_exports_info_inlining: true, + enable_rust_react_compiler: options + .enable_rust_react_compiler + .then_some(ReactCompilerCompilationMode::Infer), ..Default::default() }, environment: Some(env), diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/node_modules/react/compiler-runtime.js b/turbopack/crates/turbopack-tests/tests/snapshot/node_modules/react/compiler-runtime.js new file mode 100644 index 000000000000..4dcce4f43af2 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/snapshot/node_modules/react/compiler-runtime.js @@ -0,0 +1,3 @@ +export function c() { + return 'purposefully empty stub for react/compiler-runtime.js' +} diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/node_modules/react/index.js b/turbopack/crates/turbopack-tests/tests/snapshot/node_modules/react/index.js index 98e53699f540..1ec8e261b1c8 100644 --- a/turbopack/crates/turbopack-tests/tests/snapshot/node_modules/react/index.js +++ b/turbopack/crates/turbopack-tests/tests/snapshot/node_modules/react/index.js @@ -1,3 +1,7 @@ export function jsx() { return 'purposefully empty stub for react/index.js' } + +export function useState() { + return 'purposefully empty stub for react/index.js' +} diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/react-compiler/input/Component.jsx b/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/react-compiler/input/Component.jsx new file mode 100644 index 000000000000..bc7f6a9f42e3 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/react-compiler/input/Component.jsx @@ -0,0 +1,13 @@ +import { useState } from 'react' + +export function Counter({ initialCount }) { + const [count, setCount] = useState(initialCount) + const doubled = count * 2 + return ( +
+

{count}

+

{doubled}

+ +
+ ) +} diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/react-compiler/input/index.js b/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/react-compiler/input/index.js new file mode 100644 index 000000000000..c28d3a1fd027 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/react-compiler/input/index.js @@ -0,0 +1,3 @@ +import { Counter } from './Component.jsx' + +console.log(Counter) diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/react-compiler/options.json b/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/react-compiler/options.json new file mode 100644 index 000000000000..1282b1b30fae --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/react-compiler/options.json @@ -0,0 +1,3 @@ +{ + "enableRustReactCompiler": true +} diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/react-compiler/output/0_9x_turbopack-tests_tests_snapshot_source_maps_react-compiler_input_index_1qbvt27.js.map b/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/react-compiler/output/0_9x_turbopack-tests_tests_snapshot_source_maps_react-compiler_input_index_1qbvt27.js.map new file mode 100644 index 000000000000..c15d7ec00382 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/react-compiler/output/0_9x_turbopack-tests_tests_snapshot_source_maps_react-compiler_input_index_1qbvt27.js.map @@ -0,0 +1,5 @@ +{ + "version": 3, + "sources": [], + "sections": [] +} \ No newline at end of file diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/react-compiler/output/0rv8_turbopack-tests_tests_snapshot_source_maps_react-compiler_input_index_1qbvt27.js b/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/react-compiler/output/0rv8_turbopack-tests_tests_snapshot_source_maps_react-compiler_input_index_1qbvt27.js new file mode 100644 index 000000000000..94dfd718d6eb --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/react-compiler/output/0rv8_turbopack-tests_tests_snapshot_source_maps_react-compiler_input_index_1qbvt27.js @@ -0,0 +1,5 @@ +(globalThis["TURBOPACK"] || (globalThis["TURBOPACK"] = [])).push([ + "output/0rv8_turbopack-tests_tests_snapshot_source_maps_react-compiler_input_index_1qbvt27.js", + {"otherChunks":["output/turbopack_crates_turbopack-tests_tests_snapshot_16i0hbs._.js"],"runtimeModuleIds":["[project]/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/react-compiler/input/index.js [test] (ecmascript)"]} +]); +// Dummy runtime \ No newline at end of file diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/react-compiler/output/turbopack_crates_turbopack-tests_tests_snapshot_16i0hbs._.js b/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/react-compiler/output/turbopack_crates_turbopack-tests_tests_snapshot_16i0hbs._.js new file mode 100644 index 000000000000..0e8beedd0efe --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/react-compiler/output/turbopack_crates_turbopack-tests_tests_snapshot_16i0hbs._.js @@ -0,0 +1,118 @@ +(globalThis["TURBOPACK"] || (globalThis["TURBOPACK"] = [])).push(["output/turbopack_crates_turbopack-tests_tests_snapshot_16i0hbs._.js", +"[project]/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/react-compiler/input/Component.jsx [test] (ecmascript)", ((__turbopack_context__) => { +"use strict"; + +__turbopack_context__.s([ + "Counter", + ()=>Counter +]); +var __TURBOPACK__imported__module__$5b$project$5d2f$turbopack$2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$node_modules$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$test$5d$__$28$ecmascript$29$__ = __turbopack_context__.i("[project]/turbopack/crates/turbopack-tests/tests/snapshot/node_modules/react/jsx-dev-runtime.js [test] (ecmascript)"); +var __TURBOPACK__imported__module__$5b$project$5d2f$turbopack$2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$node_modules$2f$react$2f$compiler$2d$runtime$2e$js__$5b$test$5d$__$28$ecmascript$29$__ = __turbopack_context__.i("[project]/turbopack/crates/turbopack-tests/tests/snapshot/node_modules/react/compiler-runtime.js [test] (ecmascript)"); +var __TURBOPACK__imported__module__$5b$project$5d2f$turbopack$2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$node_modules$2f$react$2f$index$2e$js__$5b$test$5d$__$28$ecmascript$29$__ = __turbopack_context__.i("[project]/turbopack/crates/turbopack-tests/tests/snapshot/node_modules/react/index.js [test] (ecmascript)"); +; +; +; +function Counter(t0) { + const $ = (0, __TURBOPACK__imported__module__$5b$project$5d2f$turbopack$2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$node_modules$2f$react$2f$compiler$2d$runtime$2e$js__$5b$test$5d$__$28$ecmascript$29$__["c"])(10); + const { initialCount } = t0; + const [count, setCount] = (0, __TURBOPACK__imported__module__$5b$project$5d2f$turbopack$2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$node_modules$2f$react$2f$index$2e$js__$5b$test$5d$__$28$ecmascript$29$__["useState"])(initialCount); + const doubled = count * 2; + let t1; + if ($[0] !== count) { + t1 = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$turbopack$2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$node_modules$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$test$5d$__$28$ecmascript$29$__["jsxDEV"])("p", { + children: count + }, void 0, false, void 0, this); + $[0] = count; + $[1] = t1; + } else { + t1 = $[1]; + } + let t2; + if ($[2] !== doubled) { + t2 = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$turbopack$2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$node_modules$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$test$5d$__$28$ecmascript$29$__["jsxDEV"])("p", { + children: doubled + }, void 0, false, void 0, this); + $[2] = doubled; + $[3] = t2; + } else { + t2 = $[3]; + } + let t3; + if ($[4] !== count) { + t3 = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$turbopack$2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$node_modules$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$test$5d$__$28$ecmascript$29$__["jsxDEV"])("button", { + onClick: ()=>setCount(count + 1), + children: "increment" + }, void 0, false, void 0, this); + $[4] = count; + $[5] = t3; + } else { + t3 = $[5]; + } + let t4; + if ($[6] !== t1 || $[7] !== t2 || $[8] !== t3) { + t4 = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$turbopack$2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$node_modules$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$test$5d$__$28$ecmascript$29$__["jsxDEV"])("div", { + children: [ + t1, + t2, + t3 + ] + }, void 0, true, void 0, this); + $[6] = t1; + $[7] = t2; + $[8] = t3; + $[9] = t4; + } else { + t4 = $[9]; + } + return t4; +} +}), +"[project]/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/react-compiler/input/index.js [test] (ecmascript)", ((__turbopack_context__) => { +"use strict"; + +__turbopack_context__.s([]); +var __TURBOPACK__imported__module__$5b$project$5d2f$turbopack$2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$source_maps$2f$react$2d$compiler$2f$input$2f$Component$2e$jsx__$5b$test$5d$__$28$ecmascript$29$__ = __turbopack_context__.i("[project]/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/react-compiler/input/Component.jsx [test] (ecmascript)"); +; +console.log(__TURBOPACK__imported__module__$5b$project$5d2f$turbopack$2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$source_maps$2f$react$2d$compiler$2f$input$2f$Component$2e$jsx__$5b$test$5d$__$28$ecmascript$29$__["Counter"]); +}), +"[project]/turbopack/crates/turbopack-tests/tests/snapshot/node_modules/react/jsx-dev-runtime.js [test] (ecmascript)", ((__turbopack_context__) => { +"use strict"; + +__turbopack_context__.s([ + "jsxDEV", + ()=>jsxDEV +]); +function jsxDEV() { + return 'purposefully empty stub for react/jsx-dev-runtime.js'; +} +}), +"[project]/turbopack/crates/turbopack-tests/tests/snapshot/node_modules/react/compiler-runtime.js [test] (ecmascript)", ((__turbopack_context__) => { +"use strict"; + +__turbopack_context__.s([ + "c", + ()=>c +]); +function c() { + return 'purposefully empty stub for react/compiler-runtime.js'; +} +}), +"[project]/turbopack/crates/turbopack-tests/tests/snapshot/node_modules/react/index.js [test] (ecmascript)", ((__turbopack_context__) => { +"use strict"; + +__turbopack_context__.s([ + "jsx", + ()=>jsx, + "useState", + ()=>useState +]); +function jsx() { + return 'purposefully empty stub for react/index.js'; +} +function useState() { + return 'purposefully empty stub for react/index.js'; +} +}), +]); + +//# sourceMappingURL=turbopack_crates_turbopack-tests_tests_snapshot_16i0hbs._.js.map \ No newline at end of file diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/react-compiler/output/turbopack_crates_turbopack-tests_tests_snapshot_16i0hbs._.js.map b/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/react-compiler/output/turbopack_crates_turbopack-tests_tests_snapshot_16i0hbs._.js.map new file mode 100644 index 000000000000..6455e97fa19c --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/react-compiler/output/turbopack_crates_turbopack-tests_tests_snapshot_16i0hbs._.js.map @@ -0,0 +1,10 @@ +{ + "version": 3, + "sources": [], + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["turbopack:///[project]/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/react-compiler/input/Component.jsx"],"sourcesContent":["import { useState } from 'react'\n\nexport function Counter({ initialCount }) {\n const [count, setCount] = useState(initialCount)\n const doubled = count * 2\n return (\n
\n

{count}

\n

{doubled}

\n \n
\n )\n}\n"],"names":[],"mappings":";;;;;AAAA;AAAA;;;;AAEO"}}, + {"offset": {"line": 72, "column": 0}, "map": {"version":3,"sources":["turbopack:///[project]/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/react-compiler/input/index.js"],"sourcesContent":["import { Counter } from './Component.jsx'\n\nconsole.log(Counter)\n"],"names":["console","log"],"mappings":";AAAA;;AAEAA,QAAQC,GAAG,CAAC,8NAAO"}}, + {"offset": {"line": 80, "column": 0}, "map": {"version":3,"sources":["turbopack:///[project]/turbopack/crates/turbopack-tests/tests/snapshot/node_modules/react/jsx-dev-runtime.js"],"sourcesContent":["export function jsxDEV() {\n return 'purposefully empty stub for react/jsx-dev-runtime.js'\n}\n"],"names":["jsxDEV"],"mappings":";;;;AAAO,SAASA;IACd,OAAO;AACT","ignoreList":[0]}}, + {"offset": {"line": 91, "column": 0}, "map": {"version":3,"sources":["turbopack:///[project]/turbopack/crates/turbopack-tests/tests/snapshot/node_modules/react/compiler-runtime.js"],"sourcesContent":["export function c() {\n return 'purposefully empty stub for react/compiler-runtime.js'\n}\n"],"names":["c"],"mappings":";;;;AAAO,SAASA;IACd,OAAO;AACT","ignoreList":[0]}}, + {"offset": {"line": 102, "column": 0}, "map": {"version":3,"sources":["turbopack:///[project]/turbopack/crates/turbopack-tests/tests/snapshot/node_modules/react/index.js"],"sourcesContent":["export function jsx() {\n return 'purposefully empty stub for react/index.js'\n}\n\nexport function useState() {\n return 'purposefully empty stub for react/index.js'\n}\n"],"names":["jsx","useState"],"mappings":";;;;;;AAAO,SAASA;IACd,OAAO;AACT;AAEO,SAASC;IACd,OAAO;AACT","ignoreList":[0]}}] +} \ No newline at end of file diff --git a/turbopack/crates/turbopack/src/module_options/mod.rs b/turbopack/crates/turbopack/src/module_options/mod.rs index 203919ae5a8f..aff18abc72a5 100644 --- a/turbopack/crates/turbopack/src/module_options/mod.rs +++ b/turbopack/crates/turbopack/src/module_options/mod.rs @@ -232,6 +232,7 @@ impl ModuleOptions { ecmascript: EcmascriptOptionsContext { enable_jsx, + enable_rust_react_compiler, enable_types, ref enable_typescript_transform, ref enable_decorators, @@ -301,6 +302,12 @@ impl ModuleOptions { let mut ecma_preprocess = vec![]; let mut postprocess = vec![]; + // Runs first so it sees the original source text (text-bridge re-parses after + // react_compiler_swc). + if let Some(compilation_mode) = enable_rust_react_compiler { + ecma_preprocess.push(EcmascriptInputTransform::ReactCompilerRust { compilation_mode }); + } + // Order of transforms is important. e.g. if the React transform occurs before // Styled JSX, there won't be JSX nodes for Styled JSX to transform. // If a custom plugin requires specific order _before_ core transform kicks in, @@ -355,6 +362,9 @@ impl ModuleOptions { None }; + // Snapshot before decorators so the TypeScript chain also includes e.g. ReactCompilerRust. + let extra_preprocess = ecma_preprocess.clone(); + if let Some(decorators_transform) = &decorators_transform { // Apply decorators transform for the ModuleType::Ecmascript as well after // constructing ts_app_transforms. Ecmascript can have decorators for @@ -364,7 +374,9 @@ impl ModuleOptions { // Since typescript transform (`ts_app_transforms`) needs to apply decorators // _before_ stripping types, we create ts_app_transforms first in a // specific order with typescript, then apply decorators to app_transforms. - ecma_preprocess.splice(0..0, [decorators_transform.clone()]); + // + // Append so ReactCompilerRust (needs original source text) runs before decorators. + ecma_preprocess.push(decorators_transform.clone()); } let ecma_preprocess = ResolvedVc::cell(ecma_preprocess); @@ -723,10 +735,12 @@ impl ModuleOptions { if let Some(options) = enable_typescript_transform { let options = options.await?; + // Prepend extra_preprocess (e.g. ReactCompilerRust) so it runs before decorators and + // TypeScript. let ts_preprocess = ResolvedVc::cell( - decorators_transform - .clone() + extra_preprocess .into_iter() + .chain(decorators_transform.clone()) .chain(std::iter::once(EcmascriptInputTransform::TypeScript { use_define_for_class_fields: options.use_define_for_class_fields, verbatim_module_syntax: options.verbatim_module_syntax, diff --git a/turbopack/crates/turbopack/src/module_options/module_options_context.rs b/turbopack/crates/turbopack/src/module_options/module_options_context.rs index 4b6a34b57148..61ceea902d41 100644 --- a/turbopack/crates/turbopack/src/module_options/module_options_context.rs +++ b/turbopack/crates/turbopack/src/module_options/module_options_context.rs @@ -14,8 +14,9 @@ use turbopack_core::{ environment::Environment, resolve::options::ImportMapping, }; use turbopack_ecmascript::{ - AnalyzeMode, TreeShakingMode, TypeofWindow, references::esm::UrlRewriteBehavior, - transform::PresetEnvConfig, + AnalyzeMode, TreeShakingMode, TypeofWindow, + references::esm::UrlRewriteBehavior, + transform::{PresetEnvConfig, ReactCompilerCompilationMode}, }; pub use turbopack_mdx::MdxTransformOptions; use turbopack_node::{ @@ -250,6 +251,7 @@ pub struct EcmascriptOptionsContext { // node_modules. pub enable_typeof_window_inlining: Option, pub enable_jsx: Option>, + pub enable_rust_react_compiler: Option, /// Follow type references and resolve declaration files in additional to /// normal resolution. pub enable_types: bool,