Skip to content

Commit db4f7ce

Browse files
committed
Turbopack: Add new webpack loader rule/condition syntax in config
1 parent 116770b commit db4f7ce

File tree

11 files changed

+443
-104
lines changed

11 files changed

+443
-104
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/next-core/src/next_client/context.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -248,11 +248,11 @@ pub async fn get_client_module_options_context(
248248
let mut foreign_conditions = loader_conditions.clone();
249249
foreign_conditions.insert(WebpackLoaderBuiltinCondition::Foreign);
250250
let foreign_enable_webpack_loaders =
251-
webpack_loader_options(project_path.clone(), next_config, true, foreign_conditions).await?;
251+
webpack_loader_options(project_path.clone(), next_config, foreign_conditions).await?;
252252

253253
// Now creates a webpack rules that applies to all code.
254254
let enable_webpack_loaders =
255-
webpack_loader_options(project_path.clone(), next_config, false, loader_conditions).await?;
255+
webpack_loader_options(project_path.clone(), next_config, loader_conditions).await?;
256256

257257
let tree_shaking_mode_for_user_code = *next_config
258258
.tree_shaking_mode_for_user_code(next_mode.is_development())

crates/next-core/src/next_config.rs

Lines changed: 92 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::{collections::BTreeSet, str::FromStr};
22

33
use anyhow::{Context, Result, bail};
44
use rustc_hash::FxHashSet;
5-
use serde::{Deserialize, Deserializer, Serialize};
5+
use serde::{Deserialize, Deserializer, Serialize, Serializer};
66
use serde_json::Value as JsonValue;
77
use turbo_esregex::EsRegex;
88
use turbo_rcstr::{RcStr, rcstr};
@@ -566,53 +566,102 @@ pub struct TurbopackConfig {
566566
pub module_ids: Option<ModuleIds>,
567567
}
568568

569-
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
569+
#[derive(
570+
Serialize, Deserialize, Clone, PartialEq, Eq, Debug, TraceRawVcs, NonLocalValue, OperationValue,
571+
)]
570572
pub struct RegexComponents {
571573
source: RcStr,
572574
flags: RcStr,
573575
}
574576

575-
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
577+
#[derive(
578+
Clone, PartialEq, Eq, Debug, Serialize, Deserialize, TraceRawVcs, NonLocalValue, OperationValue,
579+
)]
576580
#[serde(tag = "type", content = "value", rename_all = "camelCase")]
577581
pub enum ConfigConditionPath {
578582
Glob(RcStr),
579583
Regex(RegexComponents),
580584
}
581585

582-
impl TryInto<ConditionPath> for ConfigConditionPath {
583-
fn try_into(self) -> Result<ConditionPath> {
584-
Ok(match self {
586+
impl TryFrom<ConfigConditionPath> for ConditionPath {
587+
type Error = anyhow::Error;
588+
589+
fn try_from(config: ConfigConditionPath) -> Result<ConditionPath> {
590+
Ok(match config {
585591
ConfigConditionPath::Glob(path) => ConditionPath::Glob(path),
586-
ConfigConditionPath::Regex(path) => ConditionPath::Regex(path.try_into()?),
592+
ConfigConditionPath::Regex(path) => {
593+
ConditionPath::Regex(EsRegex::try_from(path)?.resolved_cell())
594+
}
587595
})
588596
}
597+
}
589598

599+
impl TryFrom<RegexComponents> for EsRegex {
590600
type Error = anyhow::Error;
591-
}
592601

593-
impl TryInto<ResolvedVc<EsRegex>> for RegexComponents {
594-
fn try_into(self) -> Result<ResolvedVc<EsRegex>> {
595-
Ok(EsRegex::new(&self.source, &self.flags)?.resolved_cell())
602+
fn try_from(components: RegexComponents) -> Result<EsRegex> {
603+
EsRegex::new(&components.source, &components.flags)
596604
}
605+
}
597606

598-
type Error = anyhow::Error;
607+
#[derive(
608+
Serialize, Deserialize, Clone, PartialEq, Eq, Debug, TraceRawVcs, NonLocalValue, OperationValue,
609+
)]
610+
pub enum ConfigConditionItem {
611+
#[serde(rename = "all", alias = "and")]
612+
All(Box<[ConfigConditionItem]>),
613+
#[serde(rename = "any", alias = "or")]
614+
Any(Box<[ConfigConditionItem]>),
615+
#[serde(rename = "not")]
616+
Not(Box<ConfigConditionItem>),
617+
#[serde(
618+
untagged,
619+
serialize_with = "serialize_rcstr",
620+
deserialize_with = "deserialize_rcstr"
621+
)]
622+
Builtin(RcStr),
623+
#[serde(untagged)]
624+
Base {
625+
path: Option<ConfigConditionPath>,
626+
content: Option<RegexComponents>,
627+
},
599628
}
600629

601-
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
602-
pub struct ConfigConditionItem {
603-
pub path: Option<ConfigConditionPath>,
604-
pub content: Option<RegexComponents>,
630+
fn serialize_rcstr<S>(field: &RcStr, serializer: S) -> Result<S::Ok, S::Error>
631+
where
632+
S: Serializer,
633+
{
634+
field.serialize(serializer)
605635
}
606636

607-
impl TryInto<ConditionItem> for ConfigConditionItem {
608-
fn try_into(self) -> Result<ConditionItem> {
609-
Ok(ConditionItem {
610-
path: self.path.map(|p| p.try_into()).transpose()?,
611-
content: self.content.map(|r| r.try_into()).transpose()?,
612-
})
613-
}
637+
fn deserialize_rcstr<'de, D>(deserializer: D) -> Result<RcStr, D::Error>
638+
where
639+
D: Deserializer<'de>,
640+
{
641+
RcStr::deserialize(deserializer)
642+
}
614643

644+
impl TryFrom<ConfigConditionItem> for ConditionItem {
615645
type Error = anyhow::Error;
646+
647+
fn try_from(config: ConfigConditionItem) -> Result<Self> {
648+
Ok(match config {
649+
ConfigConditionItem::All(conds) => ConditionItem::All(
650+
conds
651+
.into_iter()
652+
.map(ConditionItem::try_from)
653+
.collect::<Result<_>>()?,
654+
),
655+
ConfigConditionItem::Base { path, content } => ConditionItem::Base {
656+
path: path.map(ConditionPath::try_from).transpose()?,
657+
content: content
658+
.map(EsRegex::try_from)
659+
.transpose()?
660+
.map(EsRegex::resolved_cell),
661+
},
662+
_ => todo!(),
663+
})
664+
}
616665
}
617666

618667
#[derive(
@@ -623,6 +672,7 @@ pub struct RuleConfigItemOptions {
623672
pub loaders: Vec<LoaderItem>,
624673
#[serde(default, alias = "as")]
625674
pub rename_as: Option<RcStr>,
675+
pub condition: Option<ConfigConditionItem>,
626676
}
627677

628678
#[derive(
@@ -640,8 +690,8 @@ pub enum RuleConfigItemOrShortcut {
640690
#[serde(rename_all = "camelCase", untagged)]
641691
pub enum RuleConfigItem {
642692
Options(RuleConfigItemOptions),
643-
Conditional(FxIndexMap<RcStr, RuleConfigItem>),
644-
Boolean(bool),
693+
LegacyConditional(FxIndexMap<RcStr, RuleConfigItem>),
694+
LegacyBoolean(bool),
645695
}
646696

647697
#[derive(
@@ -1320,13 +1370,16 @@ impl NextConfig {
13201370
NotFound,
13211371
Break,
13221372
}
1373+
// This logic is needed for the `LegacyConditional`/`LegacyBoolean` configuration
1374+
// syntax. This is technically public syntax, but was never documented and it is
1375+
// unlikely that anyone is depending on it (outside of some Next.js internals).
13231376
fn find_rule<'a>(
13241377
rule: &'a RuleConfigItem,
13251378
active_conditions: &BTreeSet<WebpackLoaderBuiltinCondition>,
13261379
) -> FindRuleResult<'a> {
13271380
match rule {
13281381
RuleConfigItem::Options(rule) => FindRuleResult::Found(rule),
1329-
RuleConfigItem::Conditional(map) => {
1382+
RuleConfigItem::LegacyConditional(map) => {
13301383
for (condition, rule) in map.iter() {
13311384
let condition = WebpackLoaderBuiltinCondition::from_str(condition);
13321385
if let Ok(condition) = condition
@@ -1346,7 +1399,7 @@ impl NextConfig {
13461399
}
13471400
FindRuleResult::NotFound
13481401
}
1349-
RuleConfigItem::Boolean(_) => FindRuleResult::Break,
1402+
RuleConfigItem::LegacyBoolean(_) => FindRuleResult::Break,
13501403
}
13511404
}
13521405
match rule {
@@ -1356,12 +1409,16 @@ impl NextConfig {
13561409
LoaderRuleItem {
13571410
loaders: transform_loaders(loaders),
13581411
rename_as: None,
1412+
condition: None,
13591413
},
13601414
);
13611415
}
13621416
RuleConfigItemOrShortcut::Advanced(rule) => {
1363-
if let FindRuleResult::Found(RuleConfigItemOptions { loaders, rename_as }) =
1364-
find_rule(rule, &active_conditions)
1417+
if let FindRuleResult::Found(RuleConfigItemOptions {
1418+
loaders,
1419+
rename_as,
1420+
condition,
1421+
}) = find_rule(rule, &active_conditions)
13651422
{
13661423
// If the extension contains a wildcard, and the rename_as does not,
13671424
// emit an issue to prevent users from encountering duplicate module names.
@@ -1383,6 +1440,12 @@ impl NextConfig {
13831440
LoaderRuleItem {
13841441
loaders: transform_loaders(loaders),
13851442
rename_as: rename_as.clone(),
1443+
// TODO(bgw): Emit `InvalidLoaderRuleError` if this fails instead of
1444+
// using try (?)
1445+
condition: condition
1446+
.clone()
1447+
.map(ConditionItem::try_from)
1448+
.transpose()?,
13861449
},
13871450
);
13881451
}

crates/next-core/src/next_server/context.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -493,11 +493,11 @@ pub async fn get_server_module_options_context(
493493
let mut foreign_conditions = loader_conditions.clone();
494494
foreign_conditions.insert(WebpackLoaderBuiltinCondition::Foreign);
495495
let foreign_enable_webpack_loaders =
496-
webpack_loader_options(project_path.clone(), next_config, true, foreign_conditions).await?;
496+
webpack_loader_options(project_path.clone(), next_config, foreign_conditions).await?;
497497

498498
// Now creates a webpack rules that applies to all code.
499499
let enable_webpack_loaders =
500-
webpack_loader_options(project_path.clone(), next_config, false, loader_conditions).await?;
500+
webpack_loader_options(project_path.clone(), next_config, loader_conditions).await?;
501501

502502
let tree_shaking_mode_for_user_code = *next_config
503503
.tree_shaking_mode_for_user_code(next_mode.is_development())

crates/next-core/src/next_shared/webpack_rules/babel.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ pub async fn maybe_add_babel_loader(
9797
LoaderRuleItem {
9898
loaders: ResolvedVc::cell(vec![loader]),
9999
rename_as: Some(rcstr!("*")),
100+
condition: None,
100101
},
101102
);
102103
}

crates/next-core/src/next_shared/webpack_rules/mod.rs

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ use serde::{Deserialize, Serialize};
55
use turbo_rcstr::rcstr;
66
use turbo_tasks::{NonLocalValue, ResolvedVc, TaskInput, Vc, trace::TraceRawVcs};
77
use turbo_tasks_fs::FileSystemPath;
8-
use turbopack::module_options::WebpackLoadersOptions;
8+
use turbopack::module_options::{
9+
WebpackLoaderBuiltinConditionSet, WebpackLoaderBuiltinConditionSetMatch, WebpackLoadersOptions,
10+
};
911
use turbopack_core::resolve::{ExternalTraced, ExternalType, options::ImportMapping};
1012

1113
use self::{babel::maybe_add_babel_loader, sass::maybe_add_sass_loader};
@@ -96,21 +98,49 @@ impl PartialEq<WebpackLoaderBuiltinCondition> for &str {
9698
}
9799
}
98100

101+
#[turbo_tasks::value]
102+
struct NextWebpackLoaderBuiltinConditionSet(BTreeSet<WebpackLoaderBuiltinCondition>);
103+
104+
#[turbo_tasks::value_impl]
105+
impl NextWebpackLoaderBuiltinConditionSet {
106+
#[turbo_tasks::function]
107+
fn new(
108+
conditions: BTreeSet<WebpackLoaderBuiltinCondition>,
109+
) -> Vc<Box<dyn WebpackLoaderBuiltinConditionSet>> {
110+
Vc::upcast::<Box<dyn WebpackLoaderBuiltinConditionSet>>(
111+
NextWebpackLoaderBuiltinConditionSet(conditions).cell(),
112+
)
113+
}
114+
}
115+
116+
#[turbo_tasks::value_impl]
117+
impl WebpackLoaderBuiltinConditionSet for NextWebpackLoaderBuiltinConditionSet {
118+
fn match_condition(&self, condition: &str) -> WebpackLoaderBuiltinConditionSetMatch {
119+
match WebpackLoaderBuiltinCondition::from_str(condition) {
120+
Ok(cond) => {
121+
if self.0.contains(&cond) {
122+
WebpackLoaderBuiltinConditionSetMatch::Matched
123+
} else {
124+
WebpackLoaderBuiltinConditionSetMatch::Unmatched
125+
}
126+
}
127+
Err(_) => WebpackLoaderBuiltinConditionSetMatch::Invalid,
128+
}
129+
}
130+
}
131+
99132
pub async fn webpack_loader_options(
100133
project_path: FileSystemPath,
101134
next_config: Vc<NextConfig>,
102-
foreign: bool,
103-
loader_conditions: BTreeSet<WebpackLoaderBuiltinCondition>,
135+
builtin_conditions: BTreeSet<WebpackLoaderBuiltinCondition>,
104136
) -> Result<Option<ResolvedVc<WebpackLoadersOptions>>> {
105-
let rules = *next_config
106-
.webpack_rules(loader_conditions, project_path.clone())
137+
let mut rules = *next_config
138+
.webpack_rules(builtin_conditions.clone(), project_path.clone())
107139
.await?;
108-
let rules = *maybe_add_sass_loader(next_config.sass_config(), rules.map(|v| *v)).await?;
109-
let rules = if foreign {
110-
rules
111-
} else {
112-
*maybe_add_babel_loader(project_path.clone(), rules.map(|v| *v)).await?
113-
};
140+
rules = *maybe_add_sass_loader(next_config.sass_config(), rules.map(|v| *v)).await?;
141+
if !builtin_conditions.contains(&WebpackLoaderBuiltinCondition::Foreign) {
142+
rules = *maybe_add_babel_loader(project_path.clone(), rules.map(|v| *v)).await?;
143+
}
114144

115145
let conditions = next_config.webpack_conditions().to_resolved().await?;
116146
Ok(if let Some(rules) = rules {
@@ -119,6 +149,9 @@ pub async fn webpack_loader_options(
119149
rules,
120150
conditions,
121151
loader_runner_package: Some(loader_runner_package_mapping().to_resolved().await?),
152+
builtin_conditions: NextWebpackLoaderBuiltinConditionSet::new(builtin_conditions)
153+
.to_resolved()
154+
.await?,
122155
}
123156
.resolved_cell(),
124157
)

crates/next-core/src/next_shared/webpack_rules/sass.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ pub async fn maybe_add_sass_loader(
8787
LoaderRuleItem {
8888
loaders: ResolvedVc::cell(vec![resolve_url_loader, sass_loader]),
8989
rename_as: Some(format!("*{rename}").into()),
90+
condition: None,
9091
},
9192
);
9293
}

turbopack/crates/turbopack/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ workspace = true
1818

1919
[dependencies]
2020
anyhow = { workspace = true }
21+
either = { workspace = true }
2122
regex = { workspace = true }
2223
rustc-hash = { workspace = true }
2324
smallvec = { workspace = true }

0 commit comments

Comments
 (0)