Skip to content

Commit 2ea4100

Browse files
committed
Turbopack: Allow arrays to be used as values in turbopack.rules config
1 parent 81b915f commit 2ea4100

File tree

6 files changed

+178
-136
lines changed

6 files changed

+178
-136
lines changed

crates/next-core/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ workspace = true
1515
anyhow = { workspace = true }
1616
async-trait = { workspace = true }
1717
base64 = "0.21.0"
18-
either = { workspace = true }
18+
either = { workspace = true, features = ["serde"] }
1919
once_cell = { workspace = true }
2020
qstring = { workspace = true }
2121
regex = { workspace = true }

crates/next-core/src/next_config.rs

Lines changed: 92 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::{collections::BTreeSet, str::FromStr, sync::LazyLock};
22

33
use anyhow::{Context, Result, bail};
4+
use either::Either;
45
use rustc_hash::FxHashSet;
56
use serde::{Deserialize, Deserializer, Serialize};
67
use serde_json::Value as JsonValue;
@@ -558,7 +559,7 @@ pub enum RemotePatternProtocol {
558559
pub struct TurbopackConfig {
559560
/// This option has been replaced by `rules`.
560561
pub loaders: Option<JsonValue>,
561-
pub rules: Option<FxIndexMap<RcStr, RuleConfigItemOrShortcut>>,
562+
pub rules: Option<FxIndexMap<RcStr, RuleConfigCollection>>,
562563
#[turbo_tasks(trace_ignore)]
563564
pub conditions: Option<FxIndexMap<RcStr, ConfigConditionItem>>,
564565
pub resolve_alias: Option<FxIndexMap<RcStr, JsonValue>>,
@@ -666,6 +667,16 @@ impl TryFrom<ConfigConditionItem> for ConditionItem {
666667
}
667668
}
668669

670+
#[derive(
671+
Clone, Debug, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs, NonLocalValue, OperationValue,
672+
)]
673+
#[serde(rename_all = "camelCase", untagged)]
674+
pub enum RuleConfigItem {
675+
Options(RuleConfigItemOptions),
676+
LegacyConditional(FxIndexMap<RcStr, RuleConfigItem>),
677+
LegacyBoolean(bool),
678+
}
679+
669680
#[derive(
670681
Clone, Debug, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs, NonLocalValue, OperationValue,
671682
)]
@@ -678,23 +689,33 @@ pub struct RuleConfigItemOptions {
678689
pub condition: Option<ConfigConditionItem>,
679690
}
680691

681-
#[derive(
682-
Clone, Debug, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs, NonLocalValue, OperationValue,
683-
)]
684-
#[serde(rename_all = "camelCase", untagged)]
685-
pub enum RuleConfigItemOrShortcut {
686-
Loaders(Vec<LoaderItem>),
687-
Advanced(RuleConfigItem),
692+
#[derive(Clone, Debug, PartialEq, Eq, Serialize, TraceRawVcs, NonLocalValue, OperationValue)]
693+
#[serde(transparent)]
694+
pub struct RuleConfigCollection(Vec<RuleConfigCollectionItem>);
695+
696+
impl<'de> Deserialize<'de> for RuleConfigCollection {
697+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
698+
where
699+
D: Deserializer<'de>,
700+
{
701+
match either::serde_untagged::deserialize::<Vec<RuleConfigCollectionItem>, RuleConfigItem, D>(
702+
deserializer,
703+
)? {
704+
Either::Left(collection) => Ok(RuleConfigCollection(collection)),
705+
Either::Right(item) => Ok(RuleConfigCollection(vec![RuleConfigCollectionItem::Full(
706+
item,
707+
)])),
708+
}
709+
}
688710
}
689711

690712
#[derive(
691713
Clone, Debug, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs, NonLocalValue, OperationValue,
692714
)]
693-
#[serde(rename_all = "camelCase", untagged)]
694-
pub enum RuleConfigItem {
695-
Options(RuleConfigItemOptions),
696-
LegacyConditional(FxIndexMap<RcStr, RuleConfigItem>),
697-
LegacyBoolean(bool),
715+
#[serde(untagged)]
716+
pub enum RuleConfigCollectionItem {
717+
Shorthand(LoaderItem),
718+
Full(RuleConfigItem),
698719
}
699720

700721
#[derive(
@@ -1395,11 +1416,12 @@ impl NextConfig {
13951416
return Ok(Vc::cell(Vec::new()));
13961417
}
13971418
let mut rules = Vec::new();
1398-
for (glob, rule) in turbo_rules.iter() {
1399-
fn transform_loaders(loaders: &[LoaderItem]) -> ResolvedVc<WebpackLoaderItems> {
1419+
for (glob, rule_collection) in turbo_rules.iter() {
1420+
fn transform_loaders(
1421+
loaders: Box<dyn Iterator<Item = &LoaderItem> + '_>,
1422+
) -> ResolvedVc<WebpackLoaderItems> {
14001423
ResolvedVc::cell(
14011424
loaders
1402-
.iter()
14031425
.map(|item| match item {
14041426
LoaderItem::LoaderName(name) => WebpackLoaderItem {
14051427
loader: name.clone(),
@@ -1448,65 +1470,67 @@ impl NextConfig {
14481470
}
14491471
}
14501472
let config_file_path = || project_path.join(&self.config_file_name);
1451-
match rule {
1452-
RuleConfigItemOrShortcut::Loaders(loaders) => {
1453-
rules.push((
1454-
glob.clone(),
1455-
LoaderRuleItem {
1456-
loaders: transform_loaders(loaders),
1457-
rename_as: None,
1458-
condition: None,
1459-
},
1460-
));
1461-
}
1462-
RuleConfigItemOrShortcut::Advanced(rule) => {
1463-
if let FindRuleResult::Found(RuleConfigItemOptions {
1464-
loaders,
1465-
rename_as,
1466-
condition,
1467-
}) = find_rule(rule, &active_conditions)
1468-
{
1469-
// If the extension contains a wildcard, and the rename_as does not,
1470-
// emit an issue to prevent users from encountering duplicate module names.
1471-
if glob.contains("*")
1472-
&& let Some(rename_as) = rename_as.as_ref()
1473-
&& !rename_as.contains("*")
1473+
for item in &rule_collection.0 {
1474+
match item {
1475+
RuleConfigCollectionItem::Shorthand(loaders) => {
1476+
rules.push((
1477+
glob.clone(),
1478+
LoaderRuleItem {
1479+
loaders: transform_loaders(Box::new([loaders].into_iter())),
1480+
rename_as: None,
1481+
condition: None,
1482+
},
1483+
));
1484+
}
1485+
RuleConfigCollectionItem::Full(rule_config_item) => {
1486+
if let FindRuleResult::Found(RuleConfigItemOptions {
1487+
loaders,
1488+
rename_as,
1489+
condition,
1490+
}) = find_rule(rule_config_item, &active_conditions)
14741491
{
1475-
InvalidLoaderRuleRenameAsIssue {
1476-
glob: glob.clone(),
1477-
config_file_path: config_file_path()?,
1478-
rename_as: rename_as.clone(),
1479-
}
1480-
.resolved_cell()
1481-
.emit();
1482-
}
1483-
1484-
// convert from Next.js-specific condition type to internal Turbopack
1485-
// condition type
1486-
let condition = if let Some(condition) = condition {
1487-
if let Ok(cond) = ConditionItem::try_from(condition.clone()) {
1488-
Some(cond)
1489-
} else {
1490-
InvalidLoaderRuleConditionIssue {
1491-
condition: condition.clone(),
1492+
// If the extension contains a wildcard, and the rename_as does not,
1493+
// emit an issue to prevent users from encountering duplicate module
1494+
// names.
1495+
if glob.contains("*")
1496+
&& let Some(rename_as) = rename_as.as_ref()
1497+
&& !rename_as.contains("*")
1498+
{
1499+
InvalidLoaderRuleRenameAsIssue {
1500+
glob: glob.clone(),
14921501
config_file_path: config_file_path()?,
1502+
rename_as: rename_as.clone(),
14931503
}
14941504
.resolved_cell()
14951505
.emit();
1496-
None
14971506
}
1498-
} else {
1499-
None
1500-
};
15011507

1502-
rules.push((
1503-
glob.clone(),
1504-
LoaderRuleItem {
1505-
loaders: transform_loaders(loaders),
1506-
rename_as: rename_as.clone(),
1507-
condition,
1508-
},
1509-
));
1508+
// convert from Next.js-specific condition type to internal Turbopack
1509+
// condition type
1510+
let condition = if let Some(condition) = condition {
1511+
if let Ok(cond) = ConditionItem::try_from(condition.clone()) {
1512+
Some(cond)
1513+
} else {
1514+
InvalidLoaderRuleConditionIssue {
1515+
condition: condition.clone(),
1516+
config_file_path: config_file_path()?,
1517+
}
1518+
.resolved_cell()
1519+
.emit();
1520+
None
1521+
}
1522+
} else {
1523+
None
1524+
};
1525+
rules.push((
1526+
glob.clone(),
1527+
LoaderRuleItem {
1528+
loaders: transform_loaders(Box::new(loaders.iter())),
1529+
rename_as: rename_as.clone(),
1530+
condition,
1531+
},
1532+
));
1533+
}
15101534
}
15111535
}
15121536
}

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

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ import type {
1414
TurbopackLoaderBuiltinCondition,
1515
TurbopackLoaderItem,
1616
TurbopackRuleCondition,
17+
TurbopackRuleConfigCollection,
1718
TurbopackRuleConfigItem,
1819
TurbopackRuleConfigItemOptions,
19-
TurbopackRuleConfigItemOrShortcut,
2020
} from '../../server/config-shared'
2121
import { isDeepStrictEqual } from 'util'
2222
import { type DefineEnvOptions, getDefineEnv } from '../define-env'
@@ -1014,13 +1014,19 @@ function bindingToApi(
10141014

10151015
// Note: Returns an updated `turbopackRules` with serialized conditions. Does not mutate in-place.
10161016
function serializeTurbopackRules(
1017-
turbopackRules: Record<string, TurbopackRuleConfigItemOrShortcut>
1017+
turbopackRules: Record<string, TurbopackRuleConfigCollection>
10181018
): Record<string, any> {
10191019
const serializedRules: Record<string, any> = {}
10201020
for (const [glob, rule] of Object.entries(turbopackRules)) {
10211021
if (Array.isArray(rule)) {
1022-
checkLoaderItems(rule, glob)
1023-
serializedRules[glob] = rule
1022+
serializedRules[glob] = rule.map((item) => {
1023+
if (typeof item !== 'string' && 'loaders' in item) {
1024+
return serializeConfigItem(item, glob)
1025+
} else {
1026+
checkLoaderItem(item, glob)
1027+
return item
1028+
}
1029+
})
10241030
} else {
10251031
serializedRules[glob] = serializeConfigItem(rule, glob)
10261032
}
@@ -1036,7 +1042,9 @@ function bindingToApi(
10361042
let serializedRule: any = rule
10371043
if ('loaders' in rule) {
10381044
const narrowedRule = rule as TurbopackRuleConfigItemOptions
1039-
checkLoaderItems(narrowedRule.loaders, glob)
1045+
for (const item of narrowedRule.loaders) {
1046+
checkLoaderItem(item, glob)
1047+
}
10401048
if (narrowedRule.condition != null) {
10411049
serializedRule = {
10421050
...rule,
@@ -1056,19 +1064,15 @@ function bindingToApi(
10561064
return serializedRule
10571065
}
10581066

1059-
function checkLoaderItems(
1060-
loaderItems: TurbopackLoaderItem[],
1061-
glob: string
1062-
) {
1063-
for (const loaderItem of loaderItems) {
1064-
if (
1065-
typeof loaderItem !== 'string' &&
1066-
!isDeepStrictEqual(loaderItem, JSON.parse(JSON.stringify(loaderItem)))
1067-
) {
1068-
throw new Error(
1069-
`loader ${loaderItem.loader} for match "${glob}" does not have serializable options. Ensure that options passed are plain JavaScript objects and values.`
1070-
)
1071-
}
1067+
function checkLoaderItem(loaderItem: TurbopackLoaderItem, glob: string) {
1068+
if (
1069+
typeof loaderItem !== 'string' &&
1070+
!isDeepStrictEqual(loaderItem, JSON.parse(JSON.stringify(loaderItem)))
1071+
) {
1072+
throw new Error(
1073+
`loader ${loaderItem.loader} for match "${glob}" does not have serializable options. ` +
1074+
'Ensure that options passed are plain JavaScript objects and values.'
1075+
)
10721076
}
10731077
}
10741078
}

packages/next/src/server/config-schema.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type {
1212
TurbopackOptions,
1313
TurbopackRuleConfigItem,
1414
TurbopackRuleConfigItemOptions,
15-
TurbopackRuleConfigItemOrShortcut,
15+
TurbopackRuleConfigCollection,
1616
TurbopackRuleCondition,
1717
TurbopackLoaderBuiltinCondition,
1818
} from './config-shared'
@@ -151,11 +151,14 @@ const zTurbopackRuleConfigItem: zod.ZodType<TurbopackRuleConfigItem> = z.union([
151151
zTurbopackRuleConfigItemOptions,
152152
])
153153

154-
const zTurbopackRuleConfigItemOrShortcut: zod.ZodType<TurbopackRuleConfigItemOrShortcut> =
155-
z.union([z.array(zTurbopackLoaderItem), zTurbopackRuleConfigItem])
154+
const zTurbopackRuleConfigCollection: zod.ZodType<TurbopackRuleConfigCollection> =
155+
z.union([
156+
zTurbopackRuleConfigItem,
157+
z.array(z.union([zTurbopackLoaderItem, zTurbopackRuleConfigItemOptions])),
158+
])
156159

157160
const zTurbopackConfig: zod.ZodType<TurbopackOptions> = z.strictObject({
158-
rules: z.record(z.string(), zTurbopackRuleConfigItemOrShortcut).optional(),
161+
rules: z.record(z.string(), zTurbopackRuleConfigCollection).optional(),
159162
conditions: z.record(z.string(), zTurbopackCondition).optional(),
160163
resolveAlias: z
161164
.record(
@@ -177,7 +180,7 @@ const zTurbopackConfig: zod.ZodType<TurbopackOptions> = z.strictObject({
177180
const zDeprecatedExperimentalTurboConfig: zod.ZodType<DeprecatedExperimentalTurboOptions> =
178181
z.strictObject({
179182
loaders: z.record(z.string(), z.array(zTurbopackLoaderItem)).optional(),
180-
rules: z.record(z.string(), zTurbopackRuleConfigItemOrShortcut).optional(),
183+
rules: z.record(z.string(), zTurbopackRuleConfigCollection).optional(),
181184
resolveAlias: z
182185
.record(
183186
z.string(),

packages/next/src/server/config-shared.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -289,9 +289,18 @@ export type TurbopackRuleConfigItem =
289289
| { [condition in TurbopackLoaderBuiltinCondition]?: TurbopackRuleConfigItem }
290290
| false
291291

292-
export type TurbopackRuleConfigItemOrShortcut =
293-
| TurbopackLoaderItem[]
292+
/**
293+
* This can be an object representing a single configuration, or a list of
294+
* loaders and/or rule configuration objects.
295+
*
296+
* - A list of loader path strings or objects is the "shorthand" syntax.
297+
* - A list of rule configuration objects can be useful when each configuration
298+
* object has different `condition` fields, but still match the same top-level
299+
* path glob.
300+
*/
301+
export type TurbopackRuleConfigCollection =
294302
| TurbopackRuleConfigItem
303+
| (TurbopackLoaderItem | TurbopackRuleConfigItemOptions)[]
295304

296305
export interface TurbopackOptions {
297306
/**
@@ -316,7 +325,7 @@ export interface TurbopackOptions {
316325
*
317326
* @see [Turbopack Loaders](https://nextjs.org/docs/app/api-reference/next-config-js/turbo#webpack-loaders)
318327
*/
319-
rules?: Record<string, TurbopackRuleConfigItemOrShortcut>
328+
rules?: Record<string, TurbopackRuleConfigCollection>
320329

321330
/**
322331
* (`next --turbopack` only) A list of conditions to apply when running webpack loaders with Turbopack.

0 commit comments

Comments
 (0)