Skip to content

Commit 94237fb

Browse files
wbinnssmitheps1lon
authored andcommitted
Turbopack: Implement regex support for matching webpack loaders (#78733)
Inspired by https://x.com/wSokra/status/1907794635914612777, this allows you to declare a ‘marker’ in place of a glob, allowing you to express more conditions than simply just a glob pattern. To start, this allows us to implement regexes. For example, to match either `.svg` or `.svgr` files, use this `next.config.ts`: ``` const nextConfig: NextConfig = { turbopack: { rules: { '#svg': { as: '*.js', loaders: ['@svgr/webpack'], } }, conditions: { '#svg': { path: /\.svgr?$/, } } } }; ```
1 parent 8ea7984 commit 94237fb

File tree

11 files changed

+179
-9
lines changed

11 files changed

+179
-9
lines changed

Cargo.lock

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

crates/next-core/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ remove_console = "0.37.0"
3737
itertools = { workspace = true }
3838
auto-hash-map = { workspace = true }
3939
percent-encoding = "2.3.1"
40+
serde_path_to_error = { workspace = true }
4041

4142
swc_core = { workspace = true, features = [
4243
"base",
@@ -60,6 +61,7 @@ modularize_imports = { workspace = true }
6061
swc_relay = { workspace = true }
6162

6263
turbo-rcstr = { workspace = true }
64+
turbo-esregex = { workspace = true }
6365
turbo-tasks = { workspace = true }
6466
turbo-tasks-bytes = { workspace = true }
6567
turbo-tasks-env = { workspace = true }

crates/next-core/src/next_config.rs

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ use turbo_tasks::{
1010
use turbo_tasks_env::EnvMap;
1111
use turbo_tasks_fs::FileSystemPath;
1212
use turbopack::module_options::{
13-
module_options_context::MdxTransformOptions, LoaderRuleItem, OptionWebpackRules,
13+
module_options_context::{
14+
ConditionItem, ConditionPath, MdxTransformOptions, OptionWebpackConditions,
15+
},
16+
LoaderRuleItem, OptionWebpackRules,
1417
};
1518
use turbopack_core::{
1619
issue::{Issue, IssueSeverity, IssueStage, OptionStyledString, StyledString},
@@ -541,11 +544,57 @@ pub struct TurbopackConfig {
541544
/// This option has been replaced by `rules`.
542545
pub loaders: Option<JsonValue>,
543546
pub rules: Option<FxIndexMap<RcStr, RuleConfigItemOrShortcut>>,
547+
#[turbo_tasks(trace_ignore)]
548+
pub conditions: Option<FxIndexMap<RcStr, ConfigConditionItem>>,
544549
pub resolve_alias: Option<FxIndexMap<RcStr, JsonValue>>,
545550
pub resolve_extensions: Option<Vec<RcStr>>,
546551
pub module_ids: Option<ModuleIds>,
547552
}
548553

554+
#[derive(Clone, Debug, PartialEq, Serialize, TraceRawVcs, NonLocalValue)]
555+
pub struct ConfigConditionItem(ConditionItem);
556+
557+
impl<'de> Deserialize<'de> for ConfigConditionItem {
558+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
559+
where
560+
D: Deserializer<'de>,
561+
{
562+
#[derive(Deserialize)]
563+
struct RegexComponents {
564+
source: RcStr,
565+
flags: RcStr,
566+
}
567+
568+
#[derive(Deserialize)]
569+
struct ConfigPath {
570+
path: RegexOrGlob,
571+
}
572+
573+
#[derive(Deserialize)]
574+
#[serde(tag = "type", rename_all = "lowercase")]
575+
enum RegexOrGlob {
576+
Regexp { value: RegexComponents },
577+
Glob { value: String },
578+
}
579+
580+
let config_path = ConfigPath::deserialize(deserializer)?;
581+
let condition_item = match config_path.path {
582+
RegexOrGlob::Regexp { value } => {
583+
let regex = turbo_esregex::EsRegex::new(&value.source, &value.flags)
584+
.map_err(serde::de::Error::custom)?;
585+
ConditionItem {
586+
path: ConditionPath::Regex(regex.resolved_cell()),
587+
}
588+
}
589+
RegexOrGlob::Glob { value } => ConditionItem {
590+
path: ConditionPath::Glob(value.into()),
591+
},
592+
};
593+
594+
Ok(ConfigConditionItem(condition_item))
595+
}
596+
}
597+
549598
#[derive(
550599
Clone, Debug, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs, NonLocalValue, OperationValue,
551600
)]
@@ -1065,7 +1114,8 @@ impl NextConfig {
10651114
#[turbo_tasks::function]
10661115
pub async fn from_string(string: Vc<RcStr>) -> Result<Vc<Self>> {
10671116
let string = string.await?;
1068-
let config: NextConfig = serde_json::from_str(&string)
1117+
let mut jdeserializer = serde_json::Deserializer::from_str(&string);
1118+
let config: NextConfig = serde_path_to_error::deserialize(&mut jdeserializer)
10691119
.with_context(|| format!("failed to parse next.config.js: {}", string))?;
10701120
Ok(config.cell())
10711121
}
@@ -1227,6 +1277,22 @@ impl NextConfig {
12271277
Vc::cell(Some(ResolvedVc::cell(rules)))
12281278
}
12291279

1280+
#[turbo_tasks::function]
1281+
pub fn webpack_conditions(&self) -> Vc<OptionWebpackConditions> {
1282+
let Some(config_conditions) = self.turbopack.as_ref().and_then(|t| t.conditions.as_ref())
1283+
else {
1284+
return Vc::cell(None);
1285+
};
1286+
1287+
let conditions = FxIndexMap::from_iter(
1288+
config_conditions
1289+
.iter()
1290+
.map(|(k, v)| (k.clone(), v.0.clone())),
1291+
);
1292+
1293+
Vc::cell(Some(ResolvedVc::cell(conditions)))
1294+
}
1295+
12301296
#[turbo_tasks::function]
12311297
pub fn persistent_caching_enabled(&self) -> Result<Vc<bool>> {
12321298
Ok(Vc::cell(

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,22 @@ pub async fn webpack_loader_options(
1515
project_path: ResolvedVc<FileSystemPath>,
1616
next_config: Vc<NextConfig>,
1717
foreign: bool,
18-
conditions: Vec<RcStr>,
18+
condition_strs: Vec<RcStr>,
1919
) -> Result<Option<ResolvedVc<WebpackLoadersOptions>>> {
20-
let rules = *next_config.webpack_rules(conditions).await?;
20+
let rules = *next_config.webpack_rules(condition_strs).await?;
2121
let rules = *maybe_add_sass_loader(next_config.sass_config(), rules.map(|v| *v)).await?;
2222
let rules = if foreign {
2323
rules
2424
} else {
2525
*maybe_add_babel_loader(*project_path, rules.map(|v| *v)).await?
2626
};
27+
28+
let conditions = next_config.webpack_conditions().to_resolved().await?;
2729
Ok(if let Some(rules) = rules {
2830
Some(
2931
WebpackLoadersOptions {
3032
rules,
33+
conditions,
3134
loader_runner_package: Some(loader_runner_package_mapping().to_resolved().await?),
3235
}
3336
.resolved_cell(),

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -874,6 +874,33 @@ function bindingToApi(
874874
}
875875
}
876876

877+
const conditions: (typeof nextConfig)['turbopack']['conditions'] =
878+
nextConfigSerializable.turbopack?.conditions
879+
if (conditions) {
880+
type SerializedConditions = {
881+
[key: string]: {
882+
path:
883+
| { type: 'regexp'; value: { source: string; flags: string } }
884+
| { type: 'glob'; value: string }
885+
}
886+
}
887+
888+
const serializedConditions: SerializedConditions = {}
889+
for (const [key, value] of Object.entries(conditions)) {
890+
serializedConditions[key] = {
891+
...value,
892+
path:
893+
value.path instanceof RegExp
894+
? {
895+
type: 'regexp',
896+
value: { source: value.path.source, flags: value.path.flags },
897+
}
898+
: { type: 'glob', value: value.path },
899+
}
900+
}
901+
nextConfigSerializable.turbopack.conditions = serializedConditions
902+
}
903+
877904
return JSON.stringify(nextConfigSerializable, null, 2)
878905
}
879906

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
TurbopackRuleConfigItem,
1414
TurbopackRuleConfigItemOptions,
1515
TurbopackRuleConfigItemOrShortcut,
16+
TurbopackRuleCondition,
1617
} from './config-shared'
1718
import type {
1819
Header,
@@ -128,8 +129,13 @@ const zTurboRuleConfigItem: zod.ZodType<TurbopackRuleConfigItem> = z.union([
128129
const zTurboRuleConfigItemOrShortcut: zod.ZodType<TurbopackRuleConfigItemOrShortcut> =
129130
z.union([z.array(zTurboLoaderItem), zTurboRuleConfigItem])
130131

132+
const zTurboCondition: zod.ZodType<TurbopackRuleCondition> = z.object({
133+
path: z.union([z.string(), z.instanceof(RegExp)]),
134+
})
135+
131136
const zTurbopackConfig: zod.ZodType<TurbopackOptions> = z.strictObject({
132137
rules: z.record(z.string(), zTurboRuleConfigItemOrShortcut).optional(),
138+
conditions: z.record(z.string(), zTurboCondition).optional(),
133139
resolveAlias: z
134140
.record(
135141
z.string(),

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,10 @@ export type TurbopackLoaderItem =
105105
options: Record<string, JSONValue>
106106
}
107107

108+
export type TurbopackRuleCondition = {
109+
path: string | RegExp
110+
}
111+
108112
export type TurbopackRuleConfigItemOrShortcut =
109113
| TurbopackLoaderItem[]
110114
| TurbopackRuleConfigItem
@@ -144,6 +148,13 @@ export interface TurbopackOptions {
144148
*/
145149
rules?: Record<string, TurbopackRuleConfigItemOrShortcut>
146150

151+
/**
152+
* (`next --turbopack` only) A list of conditions to apply when running webpack loaders with Turbopack.
153+
*
154+
* @see [Turbopack Loaders](https://nextjs.org/docs/app/api-reference/next-config-js/turbo#webpack-loaders)
155+
*/
156+
conditions?: Record<string, TurbopackRuleCondition>
157+
147158
/**
148159
* The module ID strategy to use for Turbopack.
149160
* If not set, the default is `'named'` for development and `'deterministic'`

turbopack/crates/turbopack/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ tokio = { workspace = true }
2929
tracing = { workspace = true }
3030

3131
turbo-rcstr = { workspace = true }
32+
turbo-esregex = { workspace = true }
3233
turbo-tasks = { workspace = true }
3334
turbo-tasks-env = { workspace = true }
3435
turbo-tasks-fs = { workspace = true }

turbopack/crates/turbopack/src/module_options/mod.rs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -627,16 +627,40 @@ impl ModuleOptions {
627627
path.context("need_path in ModuleOptions::new is incorrect")?,
628628
)
629629
};
630-
for (glob, rule) in webpack_loaders_options.rules.await?.iter() {
630+
for (key, rule) in webpack_loaders_options.rules.await?.iter() {
631631
rules.push(ModuleRule::new(
632632
RuleCondition::All(vec![
633-
if !glob.contains('/') {
634-
RuleCondition::ResourceBasePathGlob(Glob::new(glob.clone()).await?)
635-
} else {
633+
if key.starts_with("#") {
634+
// This is a custom marker requiring a corresponding condition entry
635+
let conditions = (*webpack_loaders_options.conditions.await?)
636+
.context(
637+
"Expected a condition entry for the webpack loader rule \
638+
matching {key}. Create a `conditions` mapping in your \
639+
next.config.js",
640+
)?
641+
.await?;
642+
643+
let condition = conditions.get(key).context(
644+
"Expected a condition entry for the webpack loader rule matching \
645+
{key}.",
646+
)?;
647+
648+
match &condition.path {
649+
ConditionPath::Glob(glob) => RuleCondition::ResourcePathGlob {
650+
base: execution_context.project_path().await?,
651+
glob: Glob::new(glob.clone()).await?,
652+
},
653+
ConditionPath::Regex(regex) => {
654+
RuleCondition::ResourcePathEsRegex(regex.await?)
655+
}
656+
}
657+
} else if key.contains('/') {
636658
RuleCondition::ResourcePathGlob {
637659
base: execution_context.project_path().await?,
638-
glob: Glob::new(glob.clone()).await?,
660+
glob: Glob::new(key.clone()).await?,
639661
}
662+
} else {
663+
RuleCondition::ResourceBasePathGlob(Glob::new(key.clone()).await?)
640664
},
641665
RuleCondition::not(RuleCondition::ResourceIsVirtualSource),
642666
]),

turbopack/crates/turbopack/src/module_options/module_options_context.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
use std::fmt::Debug;
2+
13
use serde::{Deserialize, Serialize};
4+
use turbo_esregex::EsRegex;
25
use turbo_rcstr::RcStr;
36
use turbo_tasks::{trace::TraceRawVcs, FxIndexMap, NonLocalValue, ResolvedVc, ValueDefault, Vc};
47
use turbo_tasks_fs::FileSystemPath;
@@ -31,10 +34,31 @@ pub struct WebpackRules(FxIndexMap<RcStr, LoaderRuleItem>);
3134
#[turbo_tasks::value(transparent)]
3235
pub struct OptionWebpackRules(Option<ResolvedVc<WebpackRules>>);
3336

37+
#[derive(Default)]
38+
#[turbo_tasks::value(transparent)]
39+
pub struct WebpackConditions(pub FxIndexMap<RcStr, ConditionItem>);
40+
41+
#[derive(Default)]
42+
#[turbo_tasks::value(transparent)]
43+
pub struct OptionWebpackConditions(Option<ResolvedVc<WebpackConditions>>);
44+
45+
#[derive(Clone, PartialEq, Eq, Debug, TraceRawVcs, Serialize, Deserialize, NonLocalValue)]
46+
pub enum ConditionPath {
47+
Glob(RcStr),
48+
Regex(ResolvedVc<EsRegex>),
49+
}
50+
51+
#[turbo_tasks::value(shared)]
52+
#[derive(Clone, Debug)]
53+
pub struct ConditionItem {
54+
pub path: ConditionPath,
55+
}
56+
3457
#[turbo_tasks::value(shared)]
3558
#[derive(Clone, Debug)]
3659
pub struct WebpackLoadersOptions {
3760
pub rules: ResolvedVc<WebpackRules>,
61+
pub conditions: ResolvedVc<OptionWebpackConditions>,
3862
pub loader_runner_package: Option<ResolvedVc<ImportMapping>>,
3963
}
4064

0 commit comments

Comments
 (0)