diff --git a/crates/napi/src/lib.rs b/crates/napi/src/lib.rs index e4b4795d8030d..5ef12a332597d 100644 --- a/crates/napi/src/lib.rs +++ b/crates/napi/src/lib.rs @@ -54,6 +54,7 @@ pub mod minify; #[cfg(not(target_arch = "wasm32"))] pub mod next_api; pub mod parse; +pub mod react_compiler; pub mod transform; #[cfg(not(target_arch = "wasm32"))] pub mod turbo_trace_server; diff --git a/crates/napi/src/react_compiler.rs b/crates/napi/src/react_compiler.rs new file mode 100644 index 0000000000000..80677ef4d6118 --- /dev/null +++ b/crates/napi/src/react_compiler.rs @@ -0,0 +1,121 @@ +use std::{path::PathBuf, sync::Arc}; + +use anyhow::Context as _; +use napi::bindgen_prelude::*; +use swc_core::{ + common::{SourceMap, GLOBALS}, + ecma::{ + ast::{Callee, EsVersion, Expr, FnDecl, Program, ReturnStmt}, + parser::{parse_file_as_program, Syntax, TsSyntax}, + visit::{Visit, VisitWith}, + }, +}; + +use crate::util::MapErr; + +pub struct CheckTask { + pub filename: PathBuf, +} + +#[napi] +impl Task for CheckTask { + type Output = bool; + type JsValue = bool; + + fn compute(&mut self) -> napi::Result { + GLOBALS.set(&Default::default(), || { + // + let cm = Arc::new(SourceMap::default()); + let fm = cm + .load_file(&self.filename.clone()) + .context("failed to load file") + .convert_err()?; + let mut errors = vec![]; + let Ok(program) = parse_file_as_program( + &fm, + Syntax::Typescript(TsSyntax { + tsx: true, + ..Default::default() + }), + EsVersion::EsNext, + None, + &mut errors, + ) else { + return Ok(false); + }; + if !errors.is_empty() { + return Ok(false); + } + + Ok(is_required(&program)) + }) + } + + fn resolve(&mut self, _env: Env, result: Self::Output) -> napi::Result { + Ok(result) + } +} + +#[napi] +pub fn is_react_compiler_required( + filename: String, + signal: Option, +) -> AsyncTask { + let filename = PathBuf::from(filename); + AsyncTask::with_optional_signal(CheckTask { filename }, signal) +} + +fn is_required(program: &Program) -> bool { + let mut finder = Finder::default(); + finder.visit_program(program); + finder.found +} + +#[derive(Default)] +struct Finder { + found: bool, + + /// We are in a function that starts with a capital letter or it's a function that starts with + /// `use` + is_interested: bool, +} + +impl Visit for Finder { + fn visit_callee(&mut self, node: &Callee) { + if self.is_interested { + if let Callee::Expr(e) = node { + if let Expr::Ident(c) = &**e { + if c.sym.starts_with("use") { + self.found = true; + return; + } + } + } + } + + node.visit_children_with(self); + } + + fn visit_fn_decl(&mut self, node: &FnDecl) { + let old = self.is_interested; + self.is_interested = node.ident.sym.starts_with("use") + || node.ident.sym.starts_with(|c: char| c.is_ascii_uppercase()); + + node.visit_children_with(self); + + self.is_interested = old; + } + + fn visit_return_stmt(&mut self, node: &ReturnStmt) { + if self.is_interested { + if let Some(Expr::JSXElement(..) | Expr::JSXEmpty(..) | Expr::JSXFragment(..)) = + node.arg.as_deref() + { + self.found = true; + return; + } + } + + node.visit_children_with(self); + } +} diff --git a/packages/next/src/build/babel/loader/get-config.ts b/packages/next/src/build/babel/loader/get-config.ts index 186ce06f42d17..8e5309d30a244 100644 --- a/packages/next/src/build/babel/loader/get-config.ts +++ b/packages/next/src/build/babel/loader/get-config.ts @@ -8,6 +8,7 @@ import type { NextBabelLoaderOptions, NextJsLoaderContext } from './types' import { consumeIterator } from './util' import * as Log from '../../output/log' import jsx from 'next/dist/compiled/babel/plugin-syntax-jsx' +import { isReactCompilerRequired } from '../../swc' const nextDistPath = /(next[\\/]dist[\\/]shared[\\/]lib)|(next[\\/]dist[\\/]client)|(next[\\/]dist[\\/]pages)/ @@ -257,7 +258,7 @@ function checkCustomBabelConfigDeprecation( * Generate a new, flat Babel config, ready to be handed to Babel-traverse. * This config should have no unresolved overrides, presets, etc. */ -function getFreshConfig( +async function getFreshConfig( this: NextJsLoaderContext, cacheCharacteristics: CharacteristicsGermaneToCaching, loaderOptions: NextBabelLoaderOptions, @@ -265,7 +266,7 @@ function getFreshConfig( filename: string, inputSourceMap?: object | null ) { - const hasReactCompiler = (() => { + const hasReactCompiler = await (async () => { if ( loaderOptions.reactCompilerPlugins && loaderOptions.reactCompilerPlugins.length === 0 @@ -273,6 +274,13 @@ function getFreshConfig( return false } + if ( + filename.includes('/node_modules/') || + filename.includes('\\node_modules\\') + ) { + return false + } + if ( loaderOptions.reactCompilerExclude && loaderOptions.reactCompilerExclude(filename) @@ -280,6 +288,10 @@ function getFreshConfig( return false } + if (!(await isReactCompilerRequired(filename))) { + return false + } + return true })() @@ -325,6 +337,10 @@ function getFreshConfig( } if (loaderOptions.transformMode === 'standalone') { + if (!reactCompilerPluginsIfEnabled.length) { + return null + } + options.plugins = [jsx, ...reactCompilerPluginsIfEnabled] options.presets = [ [ @@ -428,7 +444,7 @@ type BabelConfig = any const configCache: Map = new Map() const configFiles: Set = new Set() -export default function getConfig( +export default async function getConfig( this: NextJsLoaderContext, { source, @@ -443,7 +459,7 @@ export default function getConfig( filename: string inputSourceMap?: object | null } -): BabelConfig { +): Promise { const cacheCharacteristics = getCacheCharacteristics( loaderOptions, source, @@ -458,6 +474,9 @@ export default function getConfig( const cacheKey = getCacheKey(cacheCharacteristics) if (configCache.has(cacheKey)) { const cachedConfig = configCache.get(cacheKey) + if (!cachedConfig) { + return null + } return { ...cachedConfig, @@ -482,7 +501,7 @@ export default function getConfig( ) } - const freshConfig = getFreshConfig.call( + const freshConfig = await getFreshConfig.call( this, cacheCharacteristics, loaderOptions, diff --git a/packages/next/src/build/babel/loader/index.ts b/packages/next/src/build/babel/loader/index.ts index 119ca6b8dc513..99c42e991006a 100644 --- a/packages/next/src/build/babel/loader/index.ts +++ b/packages/next/src/build/babel/loader/index.ts @@ -27,16 +27,17 @@ async function nextBabelLoader( const loaderSpanInner = parentTrace.traceChild('next-babel-turbo-transform') const { code: transformedSource, map: outputSourceMap } = - loaderSpanInner.traceFn(() => - transform.call( - this, - inputSource, - inputSourceMap, - loaderOptions, - filename, - target, - loaderSpanInner - ) + await loaderSpanInner.traceAsyncFn( + async () => + await transform.call( + this, + inputSource, + inputSourceMap, + loaderOptions, + filename, + target, + loaderSpanInner + ) ) return [transformedSource, outputSourceMap] diff --git a/packages/next/src/build/babel/loader/transform.ts b/packages/next/src/build/babel/loader/transform.ts index 8357313bb943c..71cf68bd06872 100644 --- a/packages/next/src/build/babel/loader/transform.ts +++ b/packages/next/src/build/babel/loader/transform.ts @@ -69,7 +69,7 @@ function transformAst(file: any, babelConfig: any, parentSpan: Span) { } } -export default function transform( +export default async function transform( this: NextJsLoaderContext, source: string, inputSourceMap: object | null | undefined, @@ -79,13 +79,16 @@ export default function transform( parentSpan: Span ) { const getConfigSpan = parentSpan.traceChild('babel-turbo-get-config') - const babelConfig = getConfig.call(this, { + const babelConfig = await getConfig.call(this, { source, loaderOptions, inputSourceMap, target, filename, }) + if (!babelConfig) { + return { code: source, map: inputSourceMap } + } getConfigSpan.stop() const normalizeSpan = parentSpan.traceChild('babel-turbo-normalize-file') diff --git a/packages/next/src/build/swc/generated-native.d.ts b/packages/next/src/build/swc/generated-native.d.ts index e9e15ae7f4b7f..87a5b62ab0312 100644 --- a/packages/next/src/build/swc/generated-native.d.ts +++ b/packages/next/src/build/swc/generated-native.d.ts @@ -350,6 +350,10 @@ export declare function parse( filename?: string | undefined | null, signal?: AbortSignal | undefined | null ): Promise +export declare function isReactCompilerRequired( + filename: string, + signal?: AbortSignal | undefined | null +): Promise export declare function transform( src: string | Buffer | undefined, isModule: boolean, diff --git a/packages/next/src/build/swc/index.ts b/packages/next/src/build/swc/index.ts index 58e17490b974c..890f15e0cd454 100644 --- a/packages/next/src/build/swc/index.ts +++ b/packages/next/src/build/swc/index.ts @@ -1082,6 +1082,11 @@ async function loadWasm(importPath = '') { return bindings.mdxCompileSync(src, getMdxOptions(options)) }, }, + reactCompiler: { + isReactCompilerRequired(_filename: string) { + return Promise.resolve(true) + }, + }, } return wasmBindings } catch (e: any) { @@ -1253,6 +1258,11 @@ function loadNative(importPath?: string) { }, }, }, + reactCompiler: { + isReactCompilerRequired: (filename: string) => { + return bindings.isReactCompilerRequired(filename) + }, + }, } return nativeBindings } @@ -1295,6 +1305,13 @@ export async function minify(src: string, options: any): Promise { return bindings.minify(src, options) } +export async function isReactCompilerRequired( + filename: string +): Promise { + let bindings = await loadBindings() + return bindings.reactCompiler.isReactCompilerRequired(filename) +} + export async function parse(src: string, options: any): Promise { let bindings = await loadBindings() let parserOptions = getParserOptions(options) diff --git a/packages/next/src/build/swc/types.ts b/packages/next/src/build/swc/types.ts index e4d2fb08562b9..bf45ce6bf7aba 100644 --- a/packages/next/src/build/swc/types.ts +++ b/packages/next/src/build/swc/types.ts @@ -39,6 +39,10 @@ export interface Binding { transformStyleAttr(transformAttrOptions: any): Promise } } + + reactCompiler: { + isReactCompilerRequired(filename: string): Promise + } } export type StyledString =