Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: Check file using SWC before applying React Compiler #75605

Draft
wants to merge 13 commits into
base: canary
Choose a base branch
from
1 change: 1 addition & 0 deletions crates/napi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
121 changes: 121 additions & 0 deletions crates/napi/src/react_compiler.rs
Original file line number Diff line number Diff line change
@@ -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<Self::Output> {
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<Self::JsValue> {
Ok(result)
}
}

#[napi]
pub fn is_react_compiler_required(
filename: String,
signal: Option<AbortSignal>,
) -> AsyncTask<CheckTask> {
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);
}
}
29 changes: 24 additions & 5 deletions packages/next/src/build/babel/loader/get-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)/
Expand Down Expand Up @@ -257,29 +258,40 @@ 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,
target: string,
filename: string,
inputSourceMap?: object | null
) {
const hasReactCompiler = (() => {
const hasReactCompiler = await (async () => {
if (
loaderOptions.reactCompilerPlugins &&
loaderOptions.reactCompilerPlugins.length === 0
) {
return false
}

if (
filename.includes('/node_modules/') ||
filename.includes('\\node_modules\\')
) {
return false
}

if (
loaderOptions.reactCompilerExclude &&
loaderOptions.reactCompilerExclude(filename)
) {
return false
}

if (!(await isReactCompilerRequired(filename))) {
return false
}

return true
})()

Expand Down Expand Up @@ -325,6 +337,10 @@ function getFreshConfig(
}

if (loaderOptions.transformMode === 'standalone') {
if (!reactCompilerPluginsIfEnabled.length) {
return null
}

options.plugins = [jsx, ...reactCompilerPluginsIfEnabled]
options.presets = [
[
Expand Down Expand Up @@ -428,7 +444,7 @@ type BabelConfig = any
const configCache: Map<any, BabelConfig> = new Map()
const configFiles: Set<string> = new Set()

export default function getConfig(
export default async function getConfig(
this: NextJsLoaderContext,
{
source,
Expand All @@ -443,7 +459,7 @@ export default function getConfig(
filename: string
inputSourceMap?: object | null
}
): BabelConfig {
): Promise<BabelConfig> {
const cacheCharacteristics = getCacheCharacteristics(
loaderOptions,
source,
Expand All @@ -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,
Expand All @@ -482,7 +501,7 @@ export default function getConfig(
)
}

const freshConfig = getFreshConfig.call(
const freshConfig = await getFreshConfig.call(
this,
cacheCharacteristics,
loaderOptions,
Expand Down
21 changes: 11 additions & 10 deletions packages/next/src/build/babel/loader/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
7 changes: 5 additions & 2 deletions packages/next/src/build/babel/loader/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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')
Expand Down
4 changes: 4 additions & 0 deletions packages/next/src/build/swc/generated-native.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,10 @@ export declare function parse(
filename?: string | undefined | null,
signal?: AbortSignal | undefined | null
): Promise<string>
export declare function isReactCompilerRequired(
filename: string,
signal?: AbortSignal | undefined | null
): Promise<boolean>
export declare function transform(
src: string | Buffer | undefined,
isModule: boolean,
Expand Down
17 changes: 17 additions & 0 deletions packages/next/src/build/swc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -1253,6 +1258,11 @@ function loadNative(importPath?: string) {
},
},
},
reactCompiler: {
isReactCompilerRequired: (filename: string) => {
return bindings.isReactCompilerRequired(filename)
},
},
}
return nativeBindings
}
Expand Down Expand Up @@ -1295,6 +1305,13 @@ export async function minify(src: string, options: any): Promise<string> {
return bindings.minify(src, options)
}

export async function isReactCompilerRequired(
filename: string
): Promise<boolean> {
let bindings = await loadBindings()
return bindings.reactCompiler.isReactCompilerRequired(filename)
}

export async function parse(src: string, options: any): Promise<any> {
let bindings = await loadBindings()
let parserOptions = getParserOptions(options)
Expand Down
4 changes: 4 additions & 0 deletions packages/next/src/build/swc/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ export interface Binding {
transformStyleAttr(transformAttrOptions: any): Promise<any>
}
}

reactCompiler: {
isReactCompilerRequired(filename: string): Promise<boolean>
}
}

export type StyledString =
Expand Down
Loading