Skip to content

Commit ff46de3

Browse files
committed
Turbopack: Add new webpack loader rule/condition syntax in config
1 parent 11c9953 commit ff46de3

File tree

14 files changed

+730
-150
lines changed

14 files changed

+730
-150
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: 145 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -566,53 +566,92 @@ 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
}
597-
598-
type Error = anyhow::Error;
599605
}
600606

601-
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
602-
pub struct ConfigConditionItem {
603-
pub path: Option<ConfigConditionPath>,
604-
pub content: Option<RegexComponents>,
607+
#[derive(
608+
Serialize, Deserialize, Clone, PartialEq, Eq, Debug, TraceRawVcs, NonLocalValue, OperationValue,
609+
)]
610+
// We can end up with confusing behaviors if we silently ignore extra properties, since `Base` will
611+
// match nearly every object, since it has no required field.
612+
#[serde(deny_unknown_fields)]
613+
pub enum ConfigConditionItem {
614+
#[serde(rename = "all")]
615+
All(Box<[ConfigConditionItem]>),
616+
#[serde(rename = "any")]
617+
Any(Box<[ConfigConditionItem]>),
618+
#[serde(rename = "not")]
619+
Not(Box<ConfigConditionItem>),
620+
#[serde(untagged)]
621+
Builtin(WebpackLoaderBuiltinCondition),
622+
#[serde(untagged)]
623+
Base {
624+
path: Option<ConfigConditionPath>,
625+
content: Option<RegexComponents>,
626+
},
605627
}
606628

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()?,
629+
impl TryFrom<ConfigConditionItem> for ConditionItem {
630+
type Error = anyhow::Error;
631+
632+
fn try_from(config: ConfigConditionItem) -> Result<Self> {
633+
let try_from_vec = |conds: Box<[_]>| {
634+
conds
635+
.into_iter()
636+
.map(ConditionItem::try_from)
637+
.collect::<Result<_>>()
638+
};
639+
Ok(match config {
640+
ConfigConditionItem::All(conds) => ConditionItem::All(try_from_vec(conds)?),
641+
ConfigConditionItem::Any(conds) => ConditionItem::Any(try_from_vec(conds)?),
642+
ConfigConditionItem::Not(cond) => ConditionItem::Not(Box::new((*cond).try_into()?)),
643+
ConfigConditionItem::Builtin(cond) => {
644+
ConditionItem::Builtin(RcStr::from(cond.as_str()))
645+
}
646+
ConfigConditionItem::Base { path, content } => ConditionItem::Base {
647+
path: path.map(ConditionPath::try_from).transpose()?,
648+
content: content
649+
.map(EsRegex::try_from)
650+
.transpose()?
651+
.map(EsRegex::resolved_cell),
652+
},
612653
})
613654
}
614-
615-
type Error = anyhow::Error;
616655
}
617656

618657
#[derive(
@@ -623,6 +662,7 @@ pub struct RuleConfigItemOptions {
623662
pub loaders: Vec<LoaderItem>,
624663
#[serde(default, alias = "as")]
625664
pub rename_as: Option<RcStr>,
665+
pub condition: Option<ConfigConditionItem>,
626666
}
627667

628668
#[derive(
@@ -640,8 +680,8 @@ pub enum RuleConfigItemOrShortcut {
640680
#[serde(rename_all = "camelCase", untagged)]
641681
pub enum RuleConfigItem {
642682
Options(RuleConfigItemOptions),
643-
Conditional(FxIndexMap<RcStr, RuleConfigItem>),
644-
Boolean(bool),
683+
LegacyConditional(FxIndexMap<RcStr, RuleConfigItem>),
684+
LegacyBoolean(bool),
645685
}
646686

647687
#[derive(
@@ -1320,13 +1360,16 @@ impl NextConfig {
13201360
NotFound,
13211361
Break,
13221362
}
1363+
// This logic is needed for the `LegacyConditional`/`LegacyBoolean` configuration
1364+
// syntax. This is technically public syntax, but was never documented and it is
1365+
// unlikely that anyone is depending on it (outside of some Next.js internals).
13231366
fn find_rule<'a>(
13241367
rule: &'a RuleConfigItem,
13251368
active_conditions: &BTreeSet<WebpackLoaderBuiltinCondition>,
13261369
) -> FindRuleResult<'a> {
13271370
match rule {
13281371
RuleConfigItem::Options(rule) => FindRuleResult::Found(rule),
1329-
RuleConfigItem::Conditional(map) => {
1372+
RuleConfigItem::LegacyConditional(map) => {
13301373
for (condition, rule) in map.iter() {
13311374
let condition = WebpackLoaderBuiltinCondition::from_str(condition);
13321375
if let Ok(condition) = condition
@@ -1346,7 +1389,7 @@ impl NextConfig {
13461389
}
13471390
FindRuleResult::NotFound
13481391
}
1349-
RuleConfigItem::Boolean(_) => FindRuleResult::Break,
1392+
RuleConfigItem::LegacyBoolean(_) => FindRuleResult::Break,
13501393
}
13511394
}
13521395
match rule {
@@ -1356,12 +1399,16 @@ impl NextConfig {
13561399
LoaderRuleItem {
13571400
loaders: transform_loaders(loaders),
13581401
rename_as: None,
1402+
condition: None,
13591403
},
13601404
);
13611405
}
13621406
RuleConfigItemOrShortcut::Advanced(rule) => {
1363-
if let FindRuleResult::Found(RuleConfigItemOptions { loaders, rename_as }) =
1364-
find_rule(rule, &active_conditions)
1407+
if let FindRuleResult::Found(RuleConfigItemOptions {
1408+
loaders,
1409+
rename_as,
1410+
condition,
1411+
}) = find_rule(rule, &active_conditions)
13651412
{
13661413
// If the extension contains a wildcard, and the rename_as does not,
13671414
// emit an issue to prevent users from encountering duplicate module names.
@@ -1383,6 +1430,12 @@ impl NextConfig {
13831430
LoaderRuleItem {
13841431
loaders: transform_loaders(loaders),
13851432
rename_as: rename_as.clone(),
1433+
// TODO(bgw): Emit `InvalidLoaderRuleError` if this fails instead of
1434+
// using try (?)
1435+
condition: condition
1436+
.clone()
1437+
.map(ConditionItem::try_from)
1438+
.transpose()?,
13861439
},
13871440
);
13881441
}
@@ -1820,3 +1873,66 @@ impl JsConfig {
18201873
Vc::cell(self.compiler_options.clone().unwrap_or_default())
18211874
}
18221875
}
1876+
1877+
#[cfg(test)]
1878+
mod tests {
1879+
use super::*;
1880+
1881+
#[test]
1882+
fn test_serde_rule_config_item_options() {
1883+
let json_value = serde_json::json!({
1884+
"loaders": [],
1885+
"as": "*.js",
1886+
"condition": {
1887+
"all": [
1888+
"production",
1889+
{"not": "foreign"},
1890+
{"any": [
1891+
"browser",
1892+
{
1893+
"path": { "type": "glob", "value": "*.svg"},
1894+
"content": {
1895+
"source": "@someTag",
1896+
"flags": ""
1897+
}
1898+
}
1899+
]},
1900+
],
1901+
}
1902+
});
1903+
1904+
let rule_config: RuleConfigItemOptions = serde_json::from_value(json_value).unwrap();
1905+
1906+
assert_eq!(
1907+
rule_config,
1908+
RuleConfigItemOptions {
1909+
loaders: vec![],
1910+
rename_as: Some(rcstr!("*.js")),
1911+
condition: Some(ConfigConditionItem::All(
1912+
[
1913+
ConfigConditionItem::Builtin(WebpackLoaderBuiltinCondition::Production),
1914+
ConfigConditionItem::Not(Box::new(ConfigConditionItem::Builtin(
1915+
WebpackLoaderBuiltinCondition::Foreign
1916+
))),
1917+
ConfigConditionItem::Any(
1918+
vec![
1919+
ConfigConditionItem::Builtin(
1920+
WebpackLoaderBuiltinCondition::Browser
1921+
),
1922+
ConfigConditionItem::Base {
1923+
path: Some(ConfigConditionPath::Glob(rcstr!("*.svg"))),
1924+
content: Some(RegexComponents {
1925+
source: rcstr!("@someTag"),
1926+
flags: rcstr!(""),
1927+
}),
1928+
},
1929+
]
1930+
.into(),
1931+
),
1932+
]
1933+
.into(),
1934+
)),
1935+
}
1936+
);
1937+
}
1938+
}

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)