Skip to content

Commit 84e103b

Browse files
authored
chore: Backport SWC-based RC optimization (#78260)
### What? Backport #75605 ### Why? x-ref: https://vercel.slack.com/archives/C06DGJ420TZ/p1744644426250019
1 parent a9e7a24 commit 84e103b

File tree

10 files changed

+183
-17
lines changed

10 files changed

+183
-17
lines changed

crates/napi/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ pub mod minify;
5050
#[cfg(not(target_arch = "wasm32"))]
5151
pub mod next_api;
5252
pub mod parse;
53+
pub mod react_compiler;
5354
pub mod transform;
5455
#[cfg(not(target_arch = "wasm32"))]
5556
pub mod turbo_trace_server;

crates/napi/src/react_compiler.rs

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
use std::{path::PathBuf, sync::Arc};
2+
3+
use napi::bindgen_prelude::*;
4+
use next_custom_transforms::react_compiler;
5+
use swc_core::{
6+
common::{SourceMap, GLOBALS},
7+
ecma::{
8+
ast::EsVersion,
9+
parser::{parse_file_as_program, Syntax, TsSyntax},
10+
},
11+
};
12+
13+
pub struct CheckTask {
14+
pub filename: PathBuf,
15+
}
16+
17+
#[napi]
18+
impl Task for CheckTask {
19+
type Output = bool;
20+
type JsValue = bool;
21+
22+
fn compute(&mut self) -> napi::Result<Self::Output> {
23+
GLOBALS.set(&Default::default(), || {
24+
//
25+
let cm = Arc::new(SourceMap::default());
26+
let Ok(fm) = cm.load_file(&self.filename.clone()) else {
27+
return Ok(false);
28+
};
29+
let mut errors = vec![];
30+
let Ok(program) = parse_file_as_program(
31+
&fm,
32+
Syntax::Typescript(TsSyntax {
33+
tsx: true,
34+
..Default::default()
35+
}),
36+
EsVersion::EsNext,
37+
None,
38+
&mut errors,
39+
) else {
40+
return Ok(false);
41+
};
42+
if !errors.is_empty() {
43+
return Ok(false);
44+
}
45+
46+
Ok(react_compiler::is_required(&program))
47+
})
48+
}
49+
50+
fn resolve(&mut self, _env: Env, result: Self::Output) -> napi::Result<Self::JsValue> {
51+
Ok(result)
52+
}
53+
}
54+
55+
#[napi]
56+
pub fn is_react_compiler_required(
57+
filename: String,
58+
signal: Option<AbortSignal>,
59+
) -> AsyncTask<CheckTask> {
60+
let filename = PathBuf::from(filename);
61+
AsyncTask::with_optional_signal(CheckTask { filename }, signal)
62+
}

crates/next-custom-transforms/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ use rustc_hash::FxHasher;
3939

4040
pub mod chain_transforms;
4141
mod linter;
42+
pub mod react_compiler;
4243
pub mod transforms;
4344

4445
type FxIndexMap<K, V> = IndexMap<K, V, BuildHasherDefault<FxHasher>>;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
use swc_core::ecma::{
2+
ast::{Callee, Expr, FnDecl, FnExpr, Program, ReturnStmt},
3+
visit::{Visit, VisitWith},
4+
};
5+
6+
pub fn is_required(program: &Program) -> bool {
7+
let mut finder = Finder::default();
8+
finder.visit_program(program);
9+
finder.found
10+
}
11+
12+
#[derive(Default)]
13+
struct Finder {
14+
found: bool,
15+
16+
/// We are in a function that starts with a capital letter or it's a function that starts with
17+
/// `use`
18+
is_interested: bool,
19+
}
20+
21+
impl Visit for Finder {
22+
fn visit_callee(&mut self, node: &Callee) {
23+
if self.is_interested {
24+
if let Callee::Expr(e) = node {
25+
if let Expr::Ident(c) = &**e {
26+
if c.sym.starts_with("use") {
27+
self.found = true;
28+
return;
29+
}
30+
}
31+
}
32+
}
33+
34+
node.visit_children_with(self);
35+
}
36+
37+
fn visit_fn_decl(&mut self, node: &FnDecl) {
38+
let old = self.is_interested;
39+
self.is_interested = node.ident.sym.starts_with("use")
40+
|| node.ident.sym.starts_with(|c: char| c.is_ascii_uppercase());
41+
42+
node.visit_children_with(self);
43+
44+
self.is_interested = old;
45+
}
46+
47+
fn visit_fn_expr(&mut self, node: &FnExpr) {
48+
let old = self.is_interested;
49+
50+
self.is_interested = node.ident.as_ref().is_some_and(|ident| {
51+
ident.sym.starts_with("use") || ident.sym.starts_with(|c: char| c.is_ascii_uppercase())
52+
});
53+
54+
node.visit_children_with(self);
55+
56+
self.is_interested = old;
57+
}
58+
59+
fn visit_return_stmt(&mut self, node: &ReturnStmt) {
60+
if self.is_interested {
61+
if let Some(Expr::JSXElement(..) | Expr::JSXEmpty(..) | Expr::JSXFragment(..)) =
62+
node.arg.as_deref()
63+
{
64+
self.found = true;
65+
return;
66+
}
67+
}
68+
69+
node.visit_children_with(self);
70+
}
71+
}

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

+10-5
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { NextBabelLoaderOptions, NextJsLoaderContext } from './types'
88
import { consumeIterator } from './util'
99
import * as Log from '../../output/log'
1010
import jsx from 'next/dist/compiled/babel/plugin-syntax-jsx'
11+
import { isReactCompilerRequired } from '../../swc'
1112

1213
const nextDistPath =
1314
/(next[\\/]dist[\\/]shared[\\/]lib)|(next[\\/]dist[\\/]client)|(next[\\/]dist[\\/]pages)/
@@ -257,15 +258,15 @@ function checkCustomBabelConfigDeprecation(
257258
* Generate a new, flat Babel config, ready to be handed to Babel-traverse.
258259
* This config should have no unresolved overrides, presets, etc.
259260
*/
260-
function getFreshConfig(
261+
async function getFreshConfig(
261262
this: NextJsLoaderContext,
262263
cacheCharacteristics: CharacteristicsGermaneToCaching,
263264
loaderOptions: NextBabelLoaderOptions,
264265
target: string,
265266
filename: string,
266267
inputSourceMap?: object | null
267268
) {
268-
const hasReactCompiler = (() => {
269+
const hasReactCompiler = await (async () => {
269270
if (
270271
loaderOptions.reactCompilerPlugins &&
271272
loaderOptions.reactCompilerPlugins.length === 0
@@ -284,6 +285,10 @@ function getFreshConfig(
284285
return false
285286
}
286287

288+
if (!(await isReactCompilerRequired(filename))) {
289+
return false
290+
}
291+
287292
return true
288293
})()
289294

@@ -436,7 +441,7 @@ type BabelConfig = any
436441
const configCache: Map<any, BabelConfig> = new Map()
437442
const configFiles: Set<string> = new Set()
438443

439-
export default function getConfig(
444+
export default async function getConfig(
440445
this: NextJsLoaderContext,
441446
{
442447
source,
@@ -451,7 +456,7 @@ export default function getConfig(
451456
filename: string
452457
inputSourceMap?: object | null
453458
}
454-
): BabelConfig {
459+
): Promise<BabelConfig> {
455460
const cacheCharacteristics = getCacheCharacteristics(
456461
loaderOptions,
457462
source,
@@ -493,7 +498,7 @@ export default function getConfig(
493498
)
494499
}
495500

496-
const freshConfig = getFreshConfig.call(
501+
const freshConfig = await getFreshConfig.call(
497502
this,
498503
cacheCharacteristics,
499504
loaderOptions,

packages/next/src/build/babel/loader/index.ts

+11-10
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,17 @@ async function nextBabelLoader(
2727

2828
const loaderSpanInner = parentTrace.traceChild('next-babel-turbo-transform')
2929
const { code: transformedSource, map: outputSourceMap } =
30-
loaderSpanInner.traceFn(() =>
31-
transform.call(
32-
this,
33-
inputSource,
34-
inputSourceMap,
35-
loaderOptions,
36-
filename,
37-
target,
38-
loaderSpanInner
39-
)
30+
await loaderSpanInner.traceAsyncFn(
31+
async () =>
32+
await transform.call(
33+
this,
34+
inputSource,
35+
inputSourceMap,
36+
loaderOptions,
37+
filename,
38+
target,
39+
loaderSpanInner
40+
)
4041
)
4142

4243
return [transformedSource, outputSourceMap]

packages/next/src/build/babel/loader/transform.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ function transformAst(file: any, babelConfig: any, parentSpan: Span) {
6969
}
7070
}
7171

72-
export default function transform(
72+
export default async function transform(
7373
this: NextJsLoaderContext,
7474
source: string,
7575
inputSourceMap: object | null | undefined,
@@ -79,7 +79,7 @@ export default function transform(
7979
parentSpan: Span
8080
) {
8181
const getConfigSpan = parentSpan.traceChild('babel-turbo-get-config')
82-
const babelConfig = getConfig.call(this, {
82+
const babelConfig = await getConfig.call(this, {
8383
source,
8484
loaderOptions,
8585
inputSourceMap,

packages/next/src/build/swc/generated-native.d.ts

+4
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,10 @@ export declare function parse(
367367
filename?: string | undefined | null,
368368
signal?: AbortSignal | undefined | null
369369
): Promise<string>
370+
export declare function isReactCompilerRequired(
371+
filename: string,
372+
signal?: AbortSignal | undefined | null
373+
): Promise<boolean>
370374
export declare function transform(
371375
src: string | Buffer | undefined,
372376
isModule: boolean,

packages/next/src/build/swc/index.ts

+17
Original file line numberDiff line numberDiff line change
@@ -1104,6 +1104,11 @@ async function loadWasm(importPath = '') {
11041104
return bindings.mdxCompileSync(src, getMdxOptions(options))
11051105
},
11061106
},
1107+
reactCompiler: {
1108+
isReactCompilerRequired(_filename: string) {
1109+
return Promise.resolve(true)
1110+
},
1111+
},
11071112
}
11081113
return wasmBindings
11091114
} catch (e: any) {
@@ -1275,6 +1280,11 @@ function loadNative(importPath?: string) {
12751280
},
12761281
},
12771282
},
1283+
reactCompiler: {
1284+
isReactCompilerRequired: (filename: string) => {
1285+
return bindings.isReactCompilerRequired(filename)
1286+
},
1287+
},
12781288
}
12791289
return nativeBindings
12801290
}
@@ -1320,6 +1330,13 @@ export async function minify(
13201330
return bindings.minify(src, options)
13211331
}
13221332

1333+
export async function isReactCompilerRequired(
1334+
filename: string
1335+
): Promise<boolean> {
1336+
let bindings = await loadBindings()
1337+
return bindings.reactCompiler.isReactCompilerRequired(filename)
1338+
}
1339+
13231340
export async function parse(src: string, options: any): Promise<any> {
13241341
let bindings = await loadBindings()
13251342
let parserOptions = getParserOptions(options)

packages/next/src/build/swc/types.ts

+4
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ export interface Binding {
3939
transformStyleAttr(transformAttrOptions: any): Promise<any>
4040
}
4141
}
42+
43+
reactCompiler: {
44+
isReactCompilerRequired(filename: string): Promise<boolean>
45+
}
4246
}
4347

4448
export type StyledString =

0 commit comments

Comments
 (0)