Skip to content

Commit 9d596b1

Browse files
authored
Turbopack: Fix babel-loader (allowing built-in or manual configuration) (#82676)
## Context: What's broken? For using `styled-jsx` with `react-compiler`, we need to use `babel-loader`, but it turns out that outside of our current narrow use of `react-compiler`, our usage of `babel-loader` in Turbopack is totally broken: * Babel configs cause us to exit with an error claiming it's not supported. * We have code in Turbopack for enabling `babel-loader`, but we try to use the version from NPM instead of the one bundled internally with Next.js. We should use the one bundled internally with Next.js. * We shouldn't run all the babel transforms in the `next/babel` preset because they're redundant with SWC. `react-compiler` added a "standalone" mode to the babel-loader that's close to what we want, but I need to extend it for Turbopack's use-case. ## This PR - Remove the warnings/errors that say babel isn't supported with Turbopack. - Automatically enable babel when a config file is present. - Modify the `next/babel` preset in Turbopack (I'm re-using/extending `'standalone'` mode) to enable syntax plugins, but disable any transformations or down-leveling (we expect SWC to do this). This is a bit hacky because babel's presets aren't designed to support this configuration. - Pre-bundle `plugin-syntax-typescript`. This is a stub package that's also used by the typescript preset, so this really just exposes an extra entrypoint into the babel bundle. - Migrate one of the legacy babel `test/integration` tests (that uses flow syntax) to `test/e2e`. The turbopack `foreign` condition doesn't work correctly without test isolation. - Enable Turbopack for all the babel-related integration and e2e tests I could find. ## Follow-ups - [ ] #83502 React compiler can cause babel to run *twice*. Merge the logic for automatic configuration of `react-compiler` (currently in JS) and `babel` (in Rust), so that this can't happen. - [ ] #84002 Update the docs to show that Babel is now supported.
1 parent 0761a92 commit 9d596b1

File tree

37 files changed

+501
-448
lines changed

37 files changed

+501
-448
lines changed
Lines changed: 37 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
11
use std::sync::LazyLock;
22

3-
use anyhow::Result;
3+
use anyhow::{Context, Result};
44
use regex::Regex;
55
use turbo_rcstr::{RcStr, rcstr};
6-
use turbo_tasks::{ResolvedVc, Vc};
7-
use turbo_tasks_fs::{self, FileSystemEntryType, FileSystemPath};
6+
use turbo_tasks::ResolvedVc;
7+
use turbo_tasks_fs::{self, FileSystemEntryType, FileSystemPath, to_sys_path};
88
use turbopack::module_options::{ConditionItem, LoaderRuleItem};
9-
use turbopack_core::{
10-
issue::{Issue, IssueExt, IssueSeverity, IssueStage, OptionStyledString, StyledString},
11-
reference_type::{CommonJsReferenceSubType, ReferenceType},
12-
resolve::{node::node_cjs_resolve_options, parse::Request, pattern::Pattern, resolve},
13-
};
149
use turbopack_node::transforms::webpack::WebpackLoaderItem;
1510

1611
use crate::next_shared::webpack_rules::WebpackLoaderBuiltinCondition;
@@ -32,6 +27,10 @@ const BABEL_CONFIG_FILES: &[&str] = &[
3227
static BABEL_LOADER_RE: LazyLock<Regex> =
3328
LazyLock::new(|| Regex::new(r"(^|/)@?babel[-/]loader($|/|\.)").unwrap());
3429

30+
/// The forked version of babel-loader that we should use for automatic configuration. This version
31+
/// is always available, as it's installed as part of next.js.
32+
const NEXT_JS_BABEL_LOADER: &str = "next/dist/build/babel/loader";
33+
3534
pub async fn detect_likely_babel_loader(
3635
webpack_rules: &[(RcStr, LoaderRuleItem)],
3736
) -> Result<Option<RcStr>> {
@@ -54,41 +53,39 @@ pub async fn detect_likely_babel_loader(
5453
pub async fn get_babel_loader_rules(
5554
project_root: FileSystemPath,
5655
) -> Result<Vec<(RcStr, LoaderRuleItem)>> {
57-
let mut has_babel_config = false;
56+
let mut babel_config_path = None;
5857
for &filename in BABEL_CONFIG_FILES {
59-
let filetype = *project_root.join(filename)?.get_type().await?;
58+
let path = project_root.join(filename)?;
59+
let filetype = *path.get_type().await?;
6060
if matches!(filetype, FileSystemEntryType::File) {
61-
has_babel_config = true;
61+
babel_config_path = Some(path);
6262
break;
6363
}
6464
}
65-
if !has_babel_config {
65+
let Some(babel_config_path) = babel_config_path else {
6666
return Ok(Vec::new());
67-
}
67+
};
6868

69-
if !*is_babel_loader_available(project_root.clone()).await? {
70-
BabelIssue {
71-
path: project_root.clone(),
72-
title: StyledString::Text(rcstr!(
73-
"Unable to resolve babel-loader, but a babel config is present"
74-
))
75-
.resolved_cell(),
76-
description: StyledString::Text(rcstr!(
77-
"Make sure babel-loader is installed via your package manager."
78-
))
79-
.resolved_cell(),
80-
severity: IssueSeverity::Fatal,
81-
}
82-
.resolved_cell()
83-
.emit();
84-
}
69+
// - See `packages/next/src/build/babel/loader/types.d.ts` for all the configuration options.
70+
// - See `packages/next/src/build/get-babel-loader-config.ts` for how we use this in webpack.
71+
let serde_json::Value::Object(loader_options) = serde_json::json!({
72+
// `transformMode: default` (what the webpack implementation does) would run all of the
73+
// Next.js-specific transforms as babel transforms. Because we always have to pay the cost
74+
// of parsing with SWC after the webpack loader runs, we want to keep running those
75+
// transforms using SWC, so use `standalone` instead.
76+
"transformMode": "standalone",
77+
"cwd": to_sys_path_str(project_root).await?,
78+
"configFile": to_sys_path_str(babel_config_path).await?,
79+
}) else {
80+
unreachable!("is an object")
81+
};
8582

8683
Ok(vec![(
8784
rcstr!("*.{js,jsx,ts,tsx,cjs,mjs,mts,cts}"),
8885
LoaderRuleItem {
8986
loaders: ResolvedVc::cell(vec![WebpackLoaderItem {
90-
loader: rcstr!("babel-loader"),
91-
options: Default::default(),
87+
loader: rcstr!(NEXT_JS_BABEL_LOADER),
88+
options: loader_options,
9289
}]),
9390
rename_as: Some(rcstr!("*")),
9491
condition: Some(ConditionItem::Not(Box::new(ConditionItem::Builtin(
@@ -98,49 +95,13 @@ pub async fn get_babel_loader_rules(
9895
)])
9996
}
10097

101-
#[turbo_tasks::function]
102-
pub async fn is_babel_loader_available(project_path: FileSystemPath) -> Result<Vc<bool>> {
103-
let result = resolve(
104-
project_path.clone(),
105-
ReferenceType::CommonJs(CommonJsReferenceSubType::Undefined),
106-
Request::parse(Pattern::Constant(rcstr!("babel-loader/package.json"))),
107-
node_cjs_resolve_options(project_path),
108-
);
109-
let assets = result.primary_sources().await?;
110-
Ok(Vc::cell(!assets.is_empty()))
111-
}
112-
113-
#[turbo_tasks::value]
114-
struct BabelIssue {
115-
path: FileSystemPath,
116-
title: ResolvedVc<StyledString>,
117-
description: ResolvedVc<StyledString>,
118-
severity: IssueSeverity,
119-
}
120-
121-
#[turbo_tasks::value_impl]
122-
impl Issue for BabelIssue {
123-
#[turbo_tasks::function]
124-
fn stage(&self) -> Vc<IssueStage> {
125-
IssueStage::Transform.into()
126-
}
127-
128-
fn severity(&self) -> IssueSeverity {
129-
self.severity
130-
}
131-
132-
#[turbo_tasks::function]
133-
fn file_path(&self) -> Vc<FileSystemPath> {
134-
self.path.clone().cell()
135-
}
136-
137-
#[turbo_tasks::function]
138-
fn title(&self) -> Vc<StyledString> {
139-
*self.title
140-
}
141-
142-
#[turbo_tasks::function]
143-
fn description(&self) -> Vc<OptionStyledString> {
144-
Vc::cell(Some(self.description))
145-
}
98+
/// A system path that can be passed to the webpack loader
99+
async fn to_sys_path_str(path: FileSystemPath) -> Result<String> {
100+
let sys_path = to_sys_path(path)
101+
.await?
102+
.context("path should use a DiskFileSystem")?;
103+
Ok(sys_path
104+
.to_str()
105+
.with_context(|| format!("{sys_path:?} is not valid utf-8"))?
106+
.to_owned())
146107
}

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@
100100
"@babel/parser": "7.27.0",
101101
"@babel/plugin-syntax-explicit-resource-management": "7.25.7",
102102
"@babel/plugin-transform-object-rest-spread": "7.25.9",
103-
"@babel/preset-flow": "7.25.9",
104103
"@babel/preset-react": "7.26.3",
105104
"@changesets/changelog-github": "0.5.1",
106105
"@changesets/cli": "2.29.3",

packages/next/errors.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -808,5 +808,6 @@
808808
"807": "Expected a %s response header.",
809809
"808": "Invalid binary HMR message: insufficient data (expected %s bytes, got %s)",
810810
"809": "Invalid binary HMR message of type %s",
811-
"810": "React debug channel stream error"
811+
"810": "React debug channel stream error",
812+
"811": "unsupported transformMode in loader options: %s"
812813
}

0 commit comments

Comments
 (0)