Skip to content

Commit 272ef6e

Browse files
committed
Turbopack: Allow arrays to be used as values in turbopack.rules config
1 parent 3fc1a68 commit 272ef6e

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};
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(
@@ -1392,11 +1413,12 @@ impl NextConfig {
13921413
return Ok(Vc::cell(Vec::new()));
13931414
}
13941415
let mut rules = Vec::new();
1395-
for (glob, rule) in turbo_rules.iter() {
1396-
fn transform_loaders(loaders: &[LoaderItem]) -> ResolvedVc<WebpackLoaderItems> {
1416+
for (glob, rule_collection) in turbo_rules.iter() {
1417+
fn transform_loaders(
1418+
loaders: &mut dyn Iterator<Item = &LoaderItem>,
1419+
) -> ResolvedVc<WebpackLoaderItems> {
13971420
ResolvedVc::cell(
13981421
loaders
1399-
.iter()
14001422
.map(|item| match item {
14011423
LoaderItem::LoaderName(name) => WebpackLoaderItem {
14021424
loader: name.clone(),
@@ -1445,65 +1467,67 @@ impl NextConfig {
14451467
}
14461468
}
14471469
let config_file_path = || project_path.join(&self.config_file_name);
1448-
match rule {
1449-
RuleConfigItemOrShortcut::Loaders(loaders) => {
1450-
rules.push((
1451-
glob.clone(),
1452-
LoaderRuleItem {
1453-
loaders: transform_loaders(loaders),
1454-
rename_as: None,
1455-
condition: None,
1456-
},
1457-
));
1458-
}
1459-
RuleConfigItemOrShortcut::Advanced(rule) => {
1460-
if let FindRuleResult::Found(RuleConfigItemOptions {
1461-
loaders,
1462-
rename_as,
1463-
condition,
1464-
}) = find_rule(rule, &active_conditions)
1465-
{
1466-
// If the extension contains a wildcard, and the rename_as does not,
1467-
// emit an issue to prevent users from encountering duplicate module names.
1468-
if glob.contains("*")
1469-
&& let Some(rename_as) = rename_as.as_ref()
1470-
&& !rename_as.contains("*")
1470+
for item in &rule_collection.0 {
1471+
match item {
1472+
RuleConfigCollectionItem::Shorthand(loaders) => {
1473+
rules.push((
1474+
glob.clone(),
1475+
LoaderRuleItem {
1476+
loaders: transform_loaders(&mut [loaders].into_iter()),
1477+
rename_as: None,
1478+
condition: None,
1479+
},
1480+
));
1481+
}
1482+
RuleConfigCollectionItem::Full(rule_config_item) => {
1483+
if let FindRuleResult::Found(RuleConfigItemOptions {
1484+
loaders,
1485+
rename_as,
1486+
condition,
1487+
}) = find_rule(rule_config_item, &active_conditions)
14711488
{
1472-
InvalidLoaderRuleRenameAsIssue {
1473-
glob: glob.clone(),
1474-
config_file_path: config_file_path()?,
1475-
rename_as: rename_as.clone(),
1476-
}
1477-
.resolved_cell()
1478-
.emit();
1479-
}
1480-
1481-
// convert from Next.js-specific condition type to internal Turbopack
1482-
// condition type
1483-
let condition = if let Some(condition) = condition {
1484-
if let Ok(cond) = ConditionItem::try_from(condition.clone()) {
1485-
Some(cond)
1486-
} else {
1487-
InvalidLoaderRuleConditionIssue {
1488-
condition: condition.clone(),
1489+
// If the extension contains a wildcard, and the rename_as does not,
1490+
// emit an issue to prevent users from encountering duplicate module
1491+
// names.
1492+
if glob.contains("*")
1493+
&& let Some(rename_as) = rename_as.as_ref()
1494+
&& !rename_as.contains("*")
1495+
{
1496+
InvalidLoaderRuleRenameAsIssue {
1497+
glob: glob.clone(),
14891498
config_file_path: config_file_path()?,
1499+
rename_as: rename_as.clone(),
14901500
}
14911501
.resolved_cell()
14921502
.emit();
1493-
None
14941503
}
1495-
} else {
1496-
None
1497-
};
14981504

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

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)