Skip to content

Commit d425de8

Browse files
committed
Turbopack: Add new webpack loader rule/condition syntax in config
1 parent 94f422e commit d425de8

File tree

21 files changed

+909
-179
lines changed

21 files changed

+909
-179
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: 159 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -566,53 +566,104 @@ 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+
)]
572+
#[serde(deny_unknown_fields)]
570573
pub struct RegexComponents {
571574
source: RcStr,
572575
flags: RcStr,
573576
}
574577

575-
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
576-
#[serde(tag = "type", content = "value", rename_all = "camelCase")]
578+
/// This type should not be hand-written, but instead `packages/next/src/build/swc/index.ts` will
579+
/// transform a JS `RegExp` to a `RegexComponents` or a string to a `Glob` before passing it to us.
580+
///
581+
/// This is needed because `RegExp` objects are not otherwise serializable.
582+
#[derive(
583+
Clone, PartialEq, Eq, Debug, Serialize, Deserialize, TraceRawVcs, NonLocalValue, OperationValue,
584+
)]
585+
#[serde(
586+
tag = "type",
587+
content = "value",
588+
rename_all = "camelCase",
589+
deny_unknown_fields
590+
)]
577591
pub enum ConfigConditionPath {
578592
Glob(RcStr),
579593
Regex(RegexComponents),
580594
}
581595

582-
impl TryInto<ConditionPath> for ConfigConditionPath {
583-
fn try_into(self) -> Result<ConditionPath> {
584-
Ok(match self {
596+
impl TryFrom<ConfigConditionPath> for ConditionPath {
597+
type Error = anyhow::Error;
598+
599+
fn try_from(config: ConfigConditionPath) -> Result<ConditionPath> {
600+
Ok(match config {
585601
ConfigConditionPath::Glob(path) => ConditionPath::Glob(path),
586-
ConfigConditionPath::Regex(path) => ConditionPath::Regex(path.try_into()?),
602+
ConfigConditionPath::Regex(path) => {
603+
ConditionPath::Regex(EsRegex::try_from(path)?.resolved_cell())
604+
}
587605
})
588606
}
607+
}
589608

609+
impl TryFrom<RegexComponents> for EsRegex {
590610
type Error = anyhow::Error;
591-
}
592611

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())
612+
fn try_from(components: RegexComponents) -> Result<EsRegex> {
613+
EsRegex::new(&components.source, &components.flags)
596614
}
597-
598-
type Error = anyhow::Error;
599615
}
600616

601-
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
602-
pub struct ConfigConditionItem {
603-
pub path: Option<ConfigConditionPath>,
604-
pub content: Option<RegexComponents>,
617+
#[derive(
618+
Serialize, Deserialize, Clone, PartialEq, Eq, Debug, TraceRawVcs, NonLocalValue, OperationValue,
619+
)]
620+
// We can end up with confusing behaviors if we silently ignore extra properties, since `Base` will
621+
// match nearly every object, since it has no required field.
622+
#[serde(deny_unknown_fields)]
623+
pub enum ConfigConditionItem {
624+
#[serde(rename = "all")]
625+
All(Box<[ConfigConditionItem]>),
626+
#[serde(rename = "any")]
627+
Any(Box<[ConfigConditionItem]>),
628+
#[serde(rename = "not")]
629+
Not(Box<ConfigConditionItem>),
630+
#[serde(untagged)]
631+
Builtin(WebpackLoaderBuiltinCondition),
632+
#[serde(untagged)]
633+
Base {
634+
#[serde(default)]
635+
path: Option<ConfigConditionPath>,
636+
#[serde(default)]
637+
content: Option<RegexComponents>,
638+
},
605639
}
606640

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()?,
641+
impl TryFrom<ConfigConditionItem> for ConditionItem {
642+
type Error = anyhow::Error;
643+
644+
fn try_from(config: ConfigConditionItem) -> Result<Self> {
645+
let try_from_vec = |conds: Box<[_]>| {
646+
conds
647+
.into_iter()
648+
.map(ConditionItem::try_from)
649+
.collect::<Result<_>>()
650+
};
651+
Ok(match config {
652+
ConfigConditionItem::All(conds) => ConditionItem::All(try_from_vec(conds)?),
653+
ConfigConditionItem::Any(conds) => ConditionItem::Any(try_from_vec(conds)?),
654+
ConfigConditionItem::Not(cond) => ConditionItem::Not(Box::new((*cond).try_into()?)),
655+
ConfigConditionItem::Builtin(cond) => {
656+
ConditionItem::Builtin(RcStr::from(cond.as_str()))
657+
}
658+
ConfigConditionItem::Base { path, content } => ConditionItem::Base {
659+
path: path.map(ConditionPath::try_from).transpose()?,
660+
content: content
661+
.map(EsRegex::try_from)
662+
.transpose()?
663+
.map(EsRegex::resolved_cell),
664+
},
612665
})
613666
}
614-
615-
type Error = anyhow::Error;
616667
}
617668

618669
#[derive(
@@ -623,6 +674,8 @@ pub struct RuleConfigItemOptions {
623674
pub loaders: Vec<LoaderItem>,
624675
#[serde(default, alias = "as")]
625676
pub rename_as: Option<RcStr>,
677+
#[serde(default)]
678+
pub condition: Option<ConfigConditionItem>,
626679
}
627680

628681
#[derive(
@@ -640,8 +693,8 @@ pub enum RuleConfigItemOrShortcut {
640693
#[serde(rename_all = "camelCase", untagged)]
641694
pub enum RuleConfigItem {
642695
Options(RuleConfigItemOptions),
643-
Conditional(FxIndexMap<RcStr, RuleConfigItem>),
644-
Boolean(bool),
696+
LegacyConditional(FxIndexMap<RcStr, RuleConfigItem>),
697+
LegacyBoolean(bool),
645698
}
646699

647700
#[derive(
@@ -1320,13 +1373,16 @@ impl NextConfig {
13201373
NotFound,
13211374
Break,
13221375
}
1376+
// This logic is needed for the `LegacyConditional`/`LegacyBoolean` configuration
1377+
// syntax. This is technically public syntax, but was never documented and it is
1378+
// unlikely that anyone is depending on it (outside of some Next.js internals).
13231379
fn find_rule<'a>(
13241380
rule: &'a RuleConfigItem,
13251381
active_conditions: &BTreeSet<WebpackLoaderBuiltinCondition>,
13261382
) -> FindRuleResult<'a> {
13271383
match rule {
13281384
RuleConfigItem::Options(rule) => FindRuleResult::Found(rule),
1329-
RuleConfigItem::Conditional(map) => {
1385+
RuleConfigItem::LegacyConditional(map) => {
13301386
for (condition, rule) in map.iter() {
13311387
let condition = WebpackLoaderBuiltinCondition::from_str(condition);
13321388
if let Ok(condition) = condition
@@ -1346,7 +1402,7 @@ impl NextConfig {
13461402
}
13471403
FindRuleResult::NotFound
13481404
}
1349-
RuleConfigItem::Boolean(_) => FindRuleResult::Break,
1405+
RuleConfigItem::LegacyBoolean(_) => FindRuleResult::Break,
13501406
}
13511407
}
13521408
match rule {
@@ -1356,12 +1412,16 @@ impl NextConfig {
13561412
LoaderRuleItem {
13571413
loaders: transform_loaders(loaders),
13581414
rename_as: None,
1415+
condition: None,
13591416
},
13601417
);
13611418
}
13621419
RuleConfigItemOrShortcut::Advanced(rule) => {
1363-
if let FindRuleResult::Found(RuleConfigItemOptions { loaders, rename_as }) =
1364-
find_rule(rule, &active_conditions)
1420+
if let FindRuleResult::Found(RuleConfigItemOptions {
1421+
loaders,
1422+
rename_as,
1423+
condition,
1424+
}) = find_rule(rule, &active_conditions)
13651425
{
13661426
// If the extension contains a wildcard, and the rename_as does not,
13671427
// emit an issue to prevent users from encountering duplicate module names.
@@ -1383,6 +1443,12 @@ impl NextConfig {
13831443
LoaderRuleItem {
13841444
loaders: transform_loaders(loaders),
13851445
rename_as: rename_as.clone(),
1446+
// TODO(bgw): Emit `InvalidLoaderRuleError` if this fails instead of
1447+
// using try (?)
1448+
condition: condition
1449+
.clone()
1450+
.map(ConditionItem::try_from)
1451+
.transpose()?,
13861452
},
13871453
);
13881454
}
@@ -1820,3 +1886,66 @@ impl JsConfig {
18201886
Vc::cell(self.compiler_options.clone().unwrap_or_default())
18211887
}
18221888
}
1889+
1890+
#[cfg(test)]
1891+
mod tests {
1892+
use super::*;
1893+
1894+
#[test]
1895+
fn test_serde_rule_config_item_options() {
1896+
let json_value = serde_json::json!({
1897+
"loaders": [],
1898+
"as": "*.js",
1899+
"condition": {
1900+
"all": [
1901+
"production",
1902+
{"not": "foreign"},
1903+
{"any": [
1904+
"browser",
1905+
{
1906+
"path": { "type": "glob", "value": "*.svg"},
1907+
"content": {
1908+
"source": "@someTag",
1909+
"flags": ""
1910+
}
1911+
}
1912+
]},
1913+
],
1914+
}
1915+
});
1916+
1917+
let rule_config: RuleConfigItemOptions = serde_json::from_value(json_value).unwrap();
1918+
1919+
assert_eq!(
1920+
rule_config,
1921+
RuleConfigItemOptions {
1922+
loaders: vec![],
1923+
rename_as: Some(rcstr!("*.js")),
1924+
condition: Some(ConfigConditionItem::All(
1925+
[
1926+
ConfigConditionItem::Builtin(WebpackLoaderBuiltinCondition::Production),
1927+
ConfigConditionItem::Not(Box::new(ConfigConditionItem::Builtin(
1928+
WebpackLoaderBuiltinCondition::Foreign
1929+
))),
1930+
ConfigConditionItem::Any(
1931+
vec![
1932+
ConfigConditionItem::Builtin(
1933+
WebpackLoaderBuiltinCondition::Browser
1934+
),
1935+
ConfigConditionItem::Base {
1936+
path: Some(ConfigConditionPath::Glob(rcstr!("*.svg"))),
1937+
content: Some(RegexComponents {
1938+
source: rcstr!("@someTag"),
1939+
flags: rcstr!(""),
1940+
}),
1941+
},
1942+
]
1943+
.into(),
1944+
),
1945+
]
1946+
.into(),
1947+
)),
1948+
}
1949+
);
1950+
}
1951+
}

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
}

0 commit comments

Comments
 (0)