next_core/
next_config.rs

1use anyhow::{Context, Result, bail};
2use either::Either;
3use rustc_hash::FxHashSet;
4use serde::{Deserialize, Deserializer, Serialize};
5use serde_json::Value as JsonValue;
6use turbo_esregex::EsRegex;
7use turbo_rcstr::{RcStr, rcstr};
8use turbo_tasks::{
9    FxIndexMap, NonLocalValue, OperationValue, ResolvedVc, TaskInput, Vc, debug::ValueDebugFormat,
10    trace::TraceRawVcs,
11};
12use turbo_tasks_env::{EnvMap, ProcessEnv};
13use turbo_tasks_fetch::FetchClient;
14use turbo_tasks_fs::FileSystemPath;
15use turbopack::module_options::{
16    ConditionItem, ConditionPath, LoaderRuleItem, WebpackRules,
17    module_options_context::{MdxTransformOptions, OptionWebpackConditions},
18};
19use turbopack_core::{
20    issue::{Issue, IssueExt, IssueStage, OptionStyledString, StyledString},
21    resolve::ResolveAliasMap,
22};
23use turbopack_ecmascript::{OptionTreeShaking, TreeShakingMode};
24use turbopack_ecmascript_plugins::transform::{
25    emotion::EmotionTransformConfig, relay::RelayConfig,
26    styled_components::StyledComponentsTransformConfig,
27};
28use turbopack_node::transforms::webpack::{WebpackLoaderItem, WebpackLoaderItems};
29
30use crate::{
31    app_structure::FileSystemPathVec,
32    mode::NextMode,
33    next_import_map::mdx_import_source_file,
34    next_shared::{
35        transforms::ModularizeImportPackageConfig, webpack_rules::WebpackLoaderBuiltinCondition,
36    },
37};
38
39#[turbo_tasks::value]
40struct NextConfigAndCustomRoutes {
41    config: ResolvedVc<NextConfig>,
42    custom_routes: ResolvedVc<CustomRoutes>,
43}
44
45#[turbo_tasks::value]
46struct CustomRoutes {
47    rewrites: ResolvedVc<Rewrites>,
48}
49
50#[turbo_tasks::value(transparent)]
51pub struct ModularizeImports(FxIndexMap<String, ModularizeImportPackageConfig>);
52
53#[turbo_tasks::value(transparent)]
54#[derive(Clone, Debug)]
55pub struct CacheKinds(FxHashSet<RcStr>);
56
57impl CacheKinds {
58    pub fn extend<I: IntoIterator<Item = RcStr>>(&mut self, iter: I) {
59        self.0.extend(iter);
60    }
61}
62
63impl Default for CacheKinds {
64    fn default() -> Self {
65        CacheKinds(
66            ["default", "remote", "private"]
67                .iter()
68                .map(|&s| s.into())
69                .collect(),
70        )
71    }
72}
73
74#[turbo_tasks::value(eq = "manual")]
75#[derive(Clone, Debug, Default, PartialEq)]
76#[serde(default, rename_all = "camelCase")]
77pub struct NextConfig {
78    // TODO all fields should be private and access should be wrapped within a turbo-tasks function
79    // Otherwise changing NextConfig will lead to invalidating all tasks accessing it.
80    pub config_file: Option<RcStr>,
81    pub config_file_name: RcStr,
82
83    /// In-memory cache size in bytes.
84    ///
85    /// If `cache_max_memory_size: 0` disables in-memory caching.
86    pub cache_max_memory_size: Option<f64>,
87    /// custom path to a cache handler to use
88    pub cache_handler: Option<RcStr>,
89
90    pub env: FxIndexMap<String, JsonValue>,
91    pub experimental: ExperimentalConfig,
92    pub images: ImageConfig,
93    pub page_extensions: Vec<RcStr>,
94    pub react_production_profiling: Option<bool>,
95    pub react_strict_mode: Option<bool>,
96    pub transpile_packages: Option<Vec<RcStr>>,
97    pub modularize_imports: Option<FxIndexMap<String, ModularizeImportPackageConfig>>,
98    pub dist_dir: Option<RcStr>,
99    pub deployment_id: Option<RcStr>,
100    sass_options: Option<serde_json::Value>,
101    pub trailing_slash: Option<bool>,
102    pub asset_prefix: Option<RcStr>,
103    pub base_path: Option<RcStr>,
104    pub skip_middleware_url_normalize: Option<bool>,
105    pub skip_trailing_slash_redirect: Option<bool>,
106    pub i18n: Option<I18NConfig>,
107    pub cross_origin: Option<CrossOriginConfig>,
108    pub dev_indicators: Option<DevIndicatorsConfig>,
109    pub output: Option<OutputType>,
110    pub turbopack: Option<TurbopackConfig>,
111    production_browser_source_maps: bool,
112    output_file_tracing_includes: Option<serde_json::Value>,
113    output_file_tracing_excludes: Option<serde_json::Value>,
114    // TODO: This option is not respected, it uses Turbopack's root instead.
115    output_file_tracing_root: Option<RcStr>,
116
117    /// Enables the bundling of node_modules packages (externals) for pages
118    /// server-side bundles.
119    ///
120    /// [API Reference](https://nextjs.org/docs/pages/api-reference/next-config-js/bundlePagesRouterDependencies)
121    pub bundle_pages_router_dependencies: Option<bool>,
122
123    /// A list of packages that should be treated as external on the server
124    /// build.
125    ///
126    /// [API Reference](https://nextjs.org/docs/app/api-reference/next-config-js/serverExternalPackages)
127    pub server_external_packages: Option<Vec<RcStr>>,
128
129    #[serde(rename = "_originalRedirects")]
130    pub original_redirects: Option<Vec<Redirect>>,
131
132    // Partially supported
133    pub compiler: Option<CompilerConfig>,
134
135    pub optimize_fonts: Option<bool>,
136
137    // unsupported
138    amp: AmpConfig,
139    clean_dist_dir: bool,
140    compress: bool,
141    eslint: EslintConfig,
142    exclude_default_moment_locales: bool,
143    // this can be a function in js land
144    export_path_map: Option<serde_json::Value>,
145    // this is a function in js land
146    generate_build_id: Option<serde_json::Value>,
147    generate_etags: bool,
148    http_agent_options: HttpAgentConfig,
149    on_demand_entries: OnDemandEntriesConfig,
150    powered_by_header: bool,
151    public_runtime_config: FxIndexMap<String, serde_json::Value>,
152    server_runtime_config: FxIndexMap<String, serde_json::Value>,
153    static_page_generation_timeout: f64,
154    target: Option<String>,
155    typescript: TypeScriptConfig,
156    use_file_system_public_routes: bool,
157    webpack: Option<serde_json::Value>,
158}
159
160#[derive(
161    Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs, NonLocalValue, OperationValue,
162)]
163#[serde(rename_all = "kebab-case")]
164pub enum CrossOriginConfig {
165    Anonymous,
166    UseCredentials,
167}
168
169#[derive(
170    Clone,
171    Debug,
172    Default,
173    PartialEq,
174    Serialize,
175    Deserialize,
176    TraceRawVcs,
177    NonLocalValue,
178    OperationValue,
179)]
180#[serde(rename_all = "camelCase")]
181struct AmpConfig {
182    canonical_base: Option<String>,
183}
184
185#[derive(
186    Clone,
187    Debug,
188    Default,
189    PartialEq,
190    Serialize,
191    Deserialize,
192    TraceRawVcs,
193    NonLocalValue,
194    OperationValue,
195)]
196#[serde(rename_all = "camelCase")]
197struct EslintConfig {
198    dirs: Option<Vec<String>>,
199    ignore_during_builds: Option<bool>,
200}
201
202#[derive(
203    Clone,
204    Debug,
205    Default,
206    PartialEq,
207    Serialize,
208    Deserialize,
209    TraceRawVcs,
210    NonLocalValue,
211    OperationValue,
212)]
213#[serde(rename_all = "kebab-case")]
214pub enum BuildActivityPositions {
215    #[default]
216    BottomRight,
217    BottomLeft,
218    TopRight,
219    TopLeft,
220}
221
222#[derive(
223    Clone,
224    Debug,
225    Default,
226    PartialEq,
227    Serialize,
228    Deserialize,
229    TraceRawVcs,
230    NonLocalValue,
231    OperationValue,
232)]
233#[serde(rename_all = "camelCase")]
234pub struct DevIndicatorsOptions {
235    pub build_activity_position: Option<BuildActivityPositions>,
236    pub position: Option<BuildActivityPositions>,
237}
238
239#[derive(
240    Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs, NonLocalValue, OperationValue,
241)]
242#[serde(untagged)]
243pub enum DevIndicatorsConfig {
244    WithOptions(DevIndicatorsOptions),
245    Boolean(bool),
246}
247
248#[derive(
249    Clone,
250    Debug,
251    Default,
252    PartialEq,
253    Serialize,
254    Deserialize,
255    TraceRawVcs,
256    NonLocalValue,
257    OperationValue,
258)]
259#[serde(rename_all = "camelCase")]
260struct OnDemandEntriesConfig {
261    max_inactive_age: f64,
262    pages_buffer_length: f64,
263}
264
265#[derive(
266    Clone,
267    Debug,
268    Default,
269    PartialEq,
270    Serialize,
271    Deserialize,
272    TraceRawVcs,
273    NonLocalValue,
274    OperationValue,
275)]
276#[serde(rename_all = "camelCase")]
277struct HttpAgentConfig {
278    keep_alive: bool,
279}
280
281#[derive(
282    Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs, NonLocalValue, OperationValue,
283)]
284#[serde(rename_all = "camelCase")]
285pub struct DomainLocale {
286    pub default_locale: String,
287    pub domain: String,
288    pub http: Option<bool>,
289    pub locales: Option<Vec<String>>,
290}
291
292#[derive(
293    Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs, NonLocalValue, OperationValue,
294)]
295#[serde(rename_all = "camelCase")]
296pub struct I18NConfig {
297    pub default_locale: String,
298    pub domains: Option<Vec<DomainLocale>>,
299    pub locale_detection: Option<bool>,
300    pub locales: Vec<String>,
301}
302
303#[derive(
304    Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs, NonLocalValue, OperationValue,
305)]
306#[serde(rename_all = "kebab-case")]
307pub enum OutputType {
308    Standalone,
309    Export,
310}
311
312#[derive(
313    Debug,
314    Clone,
315    Hash,
316    Eq,
317    PartialEq,
318    Ord,
319    PartialOrd,
320    TaskInput,
321    TraceRawVcs,
322    Serialize,
323    Deserialize,
324    NonLocalValue,
325    OperationValue,
326)]
327#[serde(tag = "type", rename_all = "kebab-case")]
328pub enum RouteHas {
329    Header {
330        key: RcStr,
331        #[serde(skip_serializing_if = "Option::is_none")]
332        value: Option<RcStr>,
333    },
334    Cookie {
335        key: RcStr,
336        #[serde(skip_serializing_if = "Option::is_none")]
337        value: Option<RcStr>,
338    },
339    Query {
340        key: RcStr,
341        #[serde(skip_serializing_if = "Option::is_none")]
342        value: Option<RcStr>,
343    },
344    Host {
345        value: RcStr,
346    },
347}
348
349#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, TraceRawVcs, NonLocalValue)]
350#[serde(rename_all = "camelCase")]
351pub struct HeaderValue {
352    pub key: RcStr,
353    pub value: RcStr,
354}
355
356#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs, NonLocalValue)]
357#[serde(rename_all = "camelCase")]
358pub struct Header {
359    pub source: String,
360    #[serde(skip_serializing_if = "Option::is_none")]
361    pub base_path: Option<bool>,
362    #[serde(skip_serializing_if = "Option::is_none")]
363    pub locale: Option<bool>,
364    pub headers: Vec<HeaderValue>,
365    #[serde(skip_serializing_if = "Option::is_none")]
366    pub has: Option<Vec<RouteHas>>,
367    #[serde(skip_serializing_if = "Option::is_none")]
368    pub missing: Option<Vec<RouteHas>>,
369}
370
371#[derive(
372    Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs, NonLocalValue, OperationValue,
373)]
374#[serde(rename_all = "camelCase")]
375pub enum RedirectStatus {
376    StatusCode(f64),
377    Permanent(bool),
378}
379
380#[derive(
381    Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs, NonLocalValue, OperationValue,
382)]
383#[serde(rename_all = "camelCase")]
384pub struct Redirect {
385    pub source: String,
386    pub destination: String,
387    #[serde(skip_serializing_if = "Option::is_none")]
388    pub base_path: Option<bool>,
389    #[serde(skip_serializing_if = "Option::is_none")]
390    pub locale: Option<bool>,
391    #[serde(skip_serializing_if = "Option::is_none")]
392    pub has: Option<Vec<RouteHas>>,
393    #[serde(skip_serializing_if = "Option::is_none")]
394    pub missing: Option<Vec<RouteHas>>,
395
396    #[serde(flatten)]
397    pub status: RedirectStatus,
398}
399
400#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs, NonLocalValue)]
401#[serde(rename_all = "camelCase")]
402pub struct Rewrite {
403    pub source: String,
404    pub destination: String,
405    #[serde(skip_serializing_if = "Option::is_none")]
406    pub base_path: Option<bool>,
407    #[serde(skip_serializing_if = "Option::is_none")]
408    pub locale: Option<bool>,
409    #[serde(skip_serializing_if = "Option::is_none")]
410    pub has: Option<Vec<RouteHas>>,
411    #[serde(skip_serializing_if = "Option::is_none")]
412    pub missing: Option<Vec<RouteHas>>,
413}
414
415#[turbo_tasks::value(eq = "manual")]
416#[derive(Clone, Debug, Default, PartialEq)]
417#[serde(rename_all = "camelCase")]
418pub struct Rewrites {
419    pub before_files: Vec<Rewrite>,
420    pub after_files: Vec<Rewrite>,
421    pub fallback: Vec<Rewrite>,
422}
423
424#[derive(
425    Clone,
426    Debug,
427    Default,
428    PartialEq,
429    Serialize,
430    Deserialize,
431    TraceRawVcs,
432    NonLocalValue,
433    OperationValue,
434)]
435#[serde(rename_all = "camelCase")]
436pub struct TypeScriptConfig {
437    pub ignore_build_errors: Option<bool>,
438    pub tsconfig_path: Option<String>,
439}
440
441#[turbo_tasks::value(eq = "manual", operation)]
442#[derive(Clone, Debug, PartialEq)]
443#[serde(rename_all = "camelCase")]
444pub struct ImageConfig {
445    pub device_sizes: Vec<u16>,
446    pub image_sizes: Vec<u16>,
447    pub path: String,
448    pub loader: ImageLoader,
449    #[serde(deserialize_with = "empty_string_is_none")]
450    pub loader_file: Option<String>,
451    pub domains: Vec<String>,
452    pub disable_static_images: bool,
453    #[serde(rename = "minimumCacheTTL")]
454    pub minimum_cache_ttl: u64,
455    pub formats: Vec<ImageFormat>,
456    #[serde(rename = "dangerouslyAllowSVG")]
457    pub dangerously_allow_svg: bool,
458    pub content_security_policy: String,
459    pub remote_patterns: Vec<RemotePattern>,
460    pub unoptimized: bool,
461}
462
463fn empty_string_is_none<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
464where
465    D: Deserializer<'de>,
466{
467    let o = Option::<String>::deserialize(deserializer)?;
468    Ok(o.filter(|s| !s.is_empty()))
469}
470
471impl Default for ImageConfig {
472    fn default() -> Self {
473        // https://github.com/vercel/next.js/blob/327634eb/packages/next/shared/lib/image-config.ts#L100-L114
474        Self {
475            device_sizes: vec![640, 750, 828, 1080, 1200, 1920, 2048, 3840],
476            image_sizes: vec![16, 32, 48, 64, 96, 128, 256, 384],
477            path: "/_next/image".to_string(),
478            loader: ImageLoader::Default,
479            loader_file: None,
480            domains: vec![],
481            disable_static_images: false,
482            minimum_cache_ttl: 60,
483            formats: vec![ImageFormat::Webp],
484            dangerously_allow_svg: false,
485            content_security_policy: "".to_string(),
486            remote_patterns: vec![],
487            unoptimized: false,
488        }
489    }
490}
491
492#[derive(
493    Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs, NonLocalValue, OperationValue,
494)]
495#[serde(rename_all = "kebab-case")]
496pub enum ImageLoader {
497    Default,
498    Imgix,
499    Cloudinary,
500    Akamai,
501    Custom,
502}
503
504#[derive(
505    Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs, NonLocalValue, OperationValue,
506)]
507pub enum ImageFormat {
508    #[serde(rename = "image/webp")]
509    Webp,
510    #[serde(rename = "image/avif")]
511    Avif,
512}
513
514#[derive(
515    Clone,
516    Debug,
517    Default,
518    PartialEq,
519    Serialize,
520    Deserialize,
521    TraceRawVcs,
522    NonLocalValue,
523    OperationValue,
524)]
525#[serde(rename_all = "camelCase")]
526pub struct RemotePattern {
527    pub hostname: String,
528    #[serde(skip_serializing_if = "Option::is_none")]
529    pub protocol: Option<RemotePatternProtocol>,
530    #[serde(skip_serializing_if = "Option::is_none")]
531    pub port: Option<String>,
532    #[serde(skip_serializing_if = "Option::is_none")]
533    pub pathname: Option<String>,
534}
535
536#[derive(
537    Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs, NonLocalValue, OperationValue,
538)]
539#[serde(rename_all = "kebab-case")]
540pub enum RemotePatternProtocol {
541    Http,
542    Https,
543}
544
545#[derive(
546    Clone,
547    Debug,
548    Default,
549    PartialEq,
550    Serialize,
551    Deserialize,
552    TraceRawVcs,
553    NonLocalValue,
554    OperationValue,
555)]
556#[serde(rename_all = "camelCase")]
557pub struct TurbopackConfig {
558    /// This option has been replaced by `rules`.
559    pub loaders: Option<JsonValue>,
560    pub rules: Option<FxIndexMap<RcStr, RuleConfigCollection>>,
561    #[turbo_tasks(trace_ignore)]
562    pub conditions: Option<FxIndexMap<RcStr, ConfigConditionItem>>,
563    pub resolve_alias: Option<FxIndexMap<RcStr, JsonValue>>,
564    pub resolve_extensions: Option<Vec<RcStr>>,
565    pub module_ids: Option<ModuleIds>,
566}
567
568#[derive(
569    Serialize, Deserialize, Clone, PartialEq, Eq, Debug, TraceRawVcs, NonLocalValue, OperationValue,
570)]
571#[serde(deny_unknown_fields)]
572pub struct RegexComponents {
573    source: RcStr,
574    flags: RcStr,
575}
576
577/// This type should not be hand-written, but instead `packages/next/src/build/swc/index.ts` will
578/// transform a JS `RegExp` to a `RegexComponents` or a string to a `Glob` before passing it to us.
579///
580/// This is needed because `RegExp` objects are not otherwise serializable.
581#[derive(
582    Clone, PartialEq, Eq, Debug, Serialize, Deserialize, TraceRawVcs, NonLocalValue, OperationValue,
583)]
584#[serde(
585    tag = "type",
586    content = "value",
587    rename_all = "camelCase",
588    deny_unknown_fields
589)]
590pub enum ConfigConditionPath {
591    Glob(RcStr),
592    Regex(RegexComponents),
593}
594
595impl TryFrom<ConfigConditionPath> for ConditionPath {
596    type Error = anyhow::Error;
597
598    fn try_from(config: ConfigConditionPath) -> Result<ConditionPath> {
599        Ok(match config {
600            ConfigConditionPath::Glob(path) => ConditionPath::Glob(path),
601            ConfigConditionPath::Regex(path) => {
602                ConditionPath::Regex(EsRegex::try_from(path)?.resolved_cell())
603            }
604        })
605    }
606}
607
608impl TryFrom<RegexComponents> for EsRegex {
609    type Error = anyhow::Error;
610
611    fn try_from(components: RegexComponents) -> Result<EsRegex> {
612        EsRegex::new(&components.source, &components.flags)
613    }
614}
615
616#[derive(
617    Serialize, Deserialize, Clone, PartialEq, Eq, Debug, TraceRawVcs, NonLocalValue, OperationValue,
618)]
619// We can end up with confusing behaviors if we silently ignore extra properties, since `Base` will
620// match nearly every object, since it has no required field.
621#[serde(deny_unknown_fields)]
622pub enum ConfigConditionItem {
623    #[serde(rename = "all")]
624    All(Box<[ConfigConditionItem]>),
625    #[serde(rename = "any")]
626    Any(Box<[ConfigConditionItem]>),
627    #[serde(rename = "not")]
628    Not(Box<ConfigConditionItem>),
629    #[serde(untagged)]
630    Builtin(WebpackLoaderBuiltinCondition),
631    #[serde(untagged)]
632    Base {
633        #[serde(default)]
634        path: Option<ConfigConditionPath>,
635        #[serde(default)]
636        content: Option<RegexComponents>,
637    },
638}
639
640impl TryFrom<ConfigConditionItem> for ConditionItem {
641    type Error = anyhow::Error;
642
643    fn try_from(config: ConfigConditionItem) -> Result<Self> {
644        let try_from_vec = |conds: Box<[_]>| {
645            conds
646                .into_iter()
647                .map(ConditionItem::try_from)
648                .collect::<Result<_>>()
649        };
650        Ok(match config {
651            ConfigConditionItem::All(conds) => ConditionItem::All(try_from_vec(conds)?),
652            ConfigConditionItem::Any(conds) => ConditionItem::Any(try_from_vec(conds)?),
653            ConfigConditionItem::Not(cond) => ConditionItem::Not(Box::new((*cond).try_into()?)),
654            ConfigConditionItem::Builtin(cond) => {
655                ConditionItem::Builtin(RcStr::from(cond.as_str()))
656            }
657            ConfigConditionItem::Base { path, content } => ConditionItem::Base {
658                path: path.map(ConditionPath::try_from).transpose()?,
659                content: content
660                    .map(EsRegex::try_from)
661                    .transpose()?
662                    .map(EsRegex::resolved_cell),
663            },
664        })
665    }
666}
667
668#[derive(
669    Clone, Debug, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs, NonLocalValue, OperationValue,
670)]
671#[serde(rename_all = "camelCase")]
672pub struct RuleConfigItem {
673    pub loaders: Vec<LoaderItem>,
674    #[serde(default, alias = "as")]
675    pub rename_as: Option<RcStr>,
676    #[serde(default)]
677    pub condition: Option<ConfigConditionItem>,
678}
679
680#[derive(Clone, Debug, PartialEq, Eq, Serialize, TraceRawVcs, NonLocalValue, OperationValue)]
681#[serde(transparent)]
682pub struct RuleConfigCollection(Vec<RuleConfigCollectionItem>);
683
684impl<'de> Deserialize<'de> for RuleConfigCollection {
685    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
686    where
687        D: Deserializer<'de>,
688    {
689        match either::serde_untagged::deserialize::<Vec<RuleConfigCollectionItem>, RuleConfigItem, D>(
690            deserializer,
691        )? {
692            Either::Left(collection) => Ok(RuleConfigCollection(collection)),
693            Either::Right(item) => Ok(RuleConfigCollection(vec![RuleConfigCollectionItem::Full(
694                item,
695            )])),
696        }
697    }
698}
699
700#[derive(
701    Clone, Debug, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs, NonLocalValue, OperationValue,
702)]
703#[serde(untagged)]
704pub enum RuleConfigCollectionItem {
705    Shorthand(LoaderItem),
706    Full(RuleConfigItem),
707}
708
709#[derive(
710    Clone, Debug, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs, NonLocalValue, OperationValue,
711)]
712#[serde(untagged)]
713pub enum LoaderItem {
714    LoaderName(RcStr),
715    LoaderOptions(WebpackLoaderItem),
716}
717
718#[turbo_tasks::value(operation)]
719#[derive(Copy, Clone, Debug)]
720#[serde(rename_all = "camelCase")]
721pub enum ModuleIds {
722    Named,
723    Deterministic,
724}
725
726#[turbo_tasks::value(transparent)]
727pub struct OptionModuleIds(pub Option<ModuleIds>);
728
729#[derive(
730    Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs, NonLocalValue, OperationValue,
731)]
732#[serde(untagged)]
733pub enum MdxRsOptions {
734    Boolean(bool),
735    Option(MdxTransformOptions),
736}
737
738#[turbo_tasks::value(shared, operation)]
739#[derive(Clone, Debug)]
740#[serde(rename_all = "camelCase")]
741pub enum ReactCompilerMode {
742    Infer,
743    Annotation,
744    All,
745}
746
747/// Subset of react compiler options
748#[turbo_tasks::value(shared, operation)]
749#[derive(Clone, Debug)]
750#[serde(rename_all = "camelCase")]
751pub struct ReactCompilerOptions {
752    #[serde(skip_serializing_if = "Option::is_none")]
753    pub compilation_mode: Option<ReactCompilerMode>,
754    #[serde(skip_serializing_if = "Option::is_none")]
755    pub panic_threshold: Option<RcStr>,
756}
757
758#[derive(
759    Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs, NonLocalValue, OperationValue,
760)]
761#[serde(untagged)]
762pub enum ReactCompilerOptionsOrBoolean {
763    Boolean(bool),
764    Option(ReactCompilerOptions),
765}
766
767#[turbo_tasks::value(transparent)]
768pub struct OptionalReactCompilerOptions(Option<ResolvedVc<ReactCompilerOptions>>);
769
770#[derive(
771    Clone,
772    Debug,
773    Default,
774    PartialEq,
775    Serialize,
776    Deserialize,
777    TraceRawVcs,
778    ValueDebugFormat,
779    NonLocalValue,
780    OperationValue,
781)]
782#[serde(rename_all = "camelCase")]
783pub struct ExperimentalConfig {
784    // all fields should be private and access should be wrapped within a turbo-tasks function
785    // Otherwise changing ExperimentalConfig will lead to invalidating all tasks accessing it.
786    allowed_revalidate_header_keys: Option<Vec<RcStr>>,
787    client_router_filter: Option<bool>,
788    /// decimal for percent for possible false positives e.g. 0.01 for 10%
789    /// potential false matches lower percent increases size of the filter
790    client_router_filter_allowed_rate: Option<f64>,
791    client_router_filter_redirects: Option<bool>,
792    fetch_cache_key_prefix: Option<RcStr>,
793    isr_flush_to_disk: Option<bool>,
794    /// For use with `@next/mdx`. Compile MDX files using the new Rust compiler.
795    /// @see [api reference](https://nextjs.org/docs/app/api-reference/next-config-js/mdxRs)
796    mdx_rs: Option<MdxRsOptions>,
797    strict_next_head: Option<bool>,
798    swc_plugins: Option<Vec<(RcStr, serde_json::Value)>>,
799    external_middleware_rewrites_resolve: Option<bool>,
800    scroll_restoration: Option<bool>,
801    manual_client_base_path: Option<bool>,
802    optimistic_client_cache: Option<bool>,
803    middleware_prefetch: Option<MiddlewarePrefetchType>,
804    /// optimizeCss can be boolean or critters' option object
805    /// Use Record<string, unknown> as critters doesn't export its Option type ([link](https://github.com/GoogleChromeLabs/critters/blob/a590c05f9197b656d2aeaae9369df2483c26b072/packages/critters/src/index.d.ts))
806    optimize_css: Option<serde_json::Value>,
807    next_script_workers: Option<bool>,
808    web_vitals_attribution: Option<Vec<RcStr>>,
809    server_actions: Option<ServerActionsOrLegacyBool>,
810    sri: Option<SubResourceIntegrity>,
811    react_compiler: Option<ReactCompilerOptionsOrBoolean>,
812    cache_components: Option<bool>,
813    use_cache: Option<bool>,
814    root_params: Option<bool>,
815    // ---
816    // UNSUPPORTED
817    // ---
818    adjust_font_fallbacks: Option<bool>,
819    adjust_font_fallbacks_with_size_adjust: Option<bool>,
820    after: Option<bool>,
821    amp: Option<serde_json::Value>,
822    app_document_preloading: Option<bool>,
823    cache_handlers: Option<FxIndexMap<RcStr, RcStr>>,
824    cache_life: Option<FxIndexMap<String, CacheLifeProfile>>,
825    case_sensitive_routes: Option<bool>,
826    cpus: Option<f64>,
827    cra_compat: Option<bool>,
828    disable_optimized_loading: Option<bool>,
829    disable_postcss_preset_env: Option<bool>,
830    esm_externals: Option<EsmExternals>,
831    extension_alias: Option<serde_json::Value>,
832    external_dir: Option<bool>,
833    /// If set to `false`, webpack won't fall back to polyfill Node.js modules
834    /// in the browser Full list of old polyfills is accessible here:
835    /// [webpack/webpack#Module_notound_error.js#L13-L42](https://github.com/webpack/webpack/blob/2a0536cf510768111a3a6dceeb14cb79b9f59273/lib/Module_not_found_error.js#L13-L42)
836    fallback_node_polyfills: Option<bool>, // false
837    force_swc_transforms: Option<bool>,
838    fully_specified: Option<bool>,
839    gzip_size: Option<bool>,
840
841    pub inline_css: Option<bool>,
842    instrumentation_hook: Option<bool>,
843    client_trace_metadata: Option<Vec<String>>,
844    large_page_data_bytes: Option<f64>,
845    logging: Option<serde_json::Value>,
846    memory_based_workers_count: Option<bool>,
847    /// Optimize React APIs for server builds.
848    optimize_server_react: Option<bool>,
849    /// Automatically apply the "modularize_imports" optimization to imports of
850    /// the specified packages.
851    optimize_package_imports: Option<Vec<RcStr>>,
852    /// Using this feature will enable the `react@experimental` for the `app`
853    /// directory.
854    ppr: Option<ExperimentalPartialPrerendering>,
855    taint: Option<bool>,
856    #[serde(rename = "routerBFCache")]
857    router_bfcache: Option<bool>,
858    proxy_timeout: Option<f64>,
859    /// enables the minification of server code.
860    server_minification: Option<bool>,
861    /// Enables source maps generation for the server production bundle.
862    server_source_maps: Option<bool>,
863    swc_trace_profiling: Option<bool>,
864    /// @internal Used by the Next.js internals only.
865    trust_host_header: Option<bool>,
866
867    url_imports: Option<serde_json::Value>,
868    view_transition: Option<bool>,
869    /// This option is to enable running the Webpack build in a worker thread
870    /// (doesn't apply to Turbopack).
871    webpack_build_worker: Option<bool>,
872    worker_threads: Option<bool>,
873
874    turbopack_minify: Option<bool>,
875    turbopack_persistent_caching: Option<bool>,
876    turbopack_source_maps: Option<bool>,
877    turbopack_tree_shaking: Option<bool>,
878    turbopack_scope_hoisting: Option<bool>,
879    turbopack_use_system_tls_certs: Option<bool>,
880    /// Disable automatic configuration of the sass loader.
881    #[serde(default)]
882    turbopack_use_builtin_sass: Option<bool>,
883    /// Disable automatic configuration of the babel loader when a babel configuration file is
884    /// present.
885    #[serde(default)]
886    turbopack_use_builtin_babel: Option<bool>,
887    // Whether to enable the global-not-found convention
888    global_not_found: Option<bool>,
889    /// Defaults to false in development mode, true in production mode.
890    turbopack_remove_unused_exports: Option<bool>,
891    /// Devtool option for the segment explorer.
892    devtool_segment_explorer: Option<bool>,
893}
894
895#[derive(
896    Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs, NonLocalValue, OperationValue,
897)]
898#[serde(rename_all = "camelCase")]
899pub struct CacheLifeProfile {
900    #[serde(skip_serializing_if = "Option::is_none")]
901    pub stale: Option<u32>,
902    #[serde(skip_serializing_if = "Option::is_none")]
903    pub revalidate: Option<u32>,
904    #[serde(skip_serializing_if = "Option::is_none")]
905    pub expire: Option<u32>,
906}
907
908#[test]
909fn test_cache_life_profiles() {
910    let json = serde_json::json!({
911        "cacheLife": {
912            "frequent": {
913                "stale": 19,
914                "revalidate": 100,
915            },
916        }
917    });
918
919    let config: ExperimentalConfig = serde_json::from_value(json).unwrap();
920    let mut expected_cache_life = FxIndexMap::default();
921
922    expected_cache_life.insert(
923        "frequent".to_string(),
924        CacheLifeProfile {
925            stale: Some(19),
926            revalidate: Some(100),
927            expire: None,
928        },
929    );
930
931    assert_eq!(config.cache_life, Some(expected_cache_life));
932}
933
934#[test]
935fn test_cache_life_profiles_invalid() {
936    let json = serde_json::json!({
937        "cacheLife": {
938            "invalid": {
939                "stale": "invalid_value",
940            },
941        }
942    });
943
944    let result: Result<ExperimentalConfig, _> = serde_json::from_value(json);
945
946    assert!(
947        result.is_err(),
948        "Deserialization should fail due to invalid 'stale' value type"
949    );
950}
951
952#[derive(
953    Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs, NonLocalValue, OperationValue,
954)]
955#[serde(rename_all = "lowercase")]
956pub enum ExperimentalPartialPrerenderingIncrementalValue {
957    Incremental,
958}
959
960#[derive(
961    Clone, Debug, PartialEq, Deserialize, Serialize, TraceRawVcs, NonLocalValue, OperationValue,
962)]
963#[serde(untagged)]
964pub enum ExperimentalPartialPrerendering {
965    Boolean(bool),
966    Incremental(ExperimentalPartialPrerenderingIncrementalValue),
967}
968
969#[test]
970fn test_parse_experimental_partial_prerendering() {
971    let json = serde_json::json!({
972        "ppr": "incremental"
973    });
974    let config: ExperimentalConfig = serde_json::from_value(json).unwrap();
975    assert_eq!(
976        config.ppr,
977        Some(ExperimentalPartialPrerendering::Incremental(
978            ExperimentalPartialPrerenderingIncrementalValue::Incremental
979        ))
980    );
981
982    let json = serde_json::json!({
983        "ppr": true
984    });
985    let config: ExperimentalConfig = serde_json::from_value(json).unwrap();
986    assert_eq!(
987        config.ppr,
988        Some(ExperimentalPartialPrerendering::Boolean(true))
989    );
990
991    // Expect if we provide a random string, it will fail.
992    let json = serde_json::json!({
993        "ppr": "random"
994    });
995    let config = serde_json::from_value::<ExperimentalConfig>(json);
996    assert!(config.is_err());
997}
998
999#[derive(
1000    Clone, Debug, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs, NonLocalValue, OperationValue,
1001)]
1002#[serde(rename_all = "camelCase")]
1003pub struct SubResourceIntegrity {
1004    pub algorithm: Option<RcStr>,
1005}
1006
1007#[derive(
1008    Clone, Debug, PartialEq, Deserialize, Serialize, TraceRawVcs, NonLocalValue, OperationValue,
1009)]
1010#[serde(untagged)]
1011pub enum ServerActionsOrLegacyBool {
1012    /// The current way to configure server actions sub behaviors.
1013    ServerActionsConfig(ServerActions),
1014
1015    /// The legacy way to disable server actions. This is no longer used, server
1016    /// actions is always enabled.
1017    LegacyBool(bool),
1018}
1019
1020#[derive(
1021    Clone, Debug, PartialEq, Deserialize, Serialize, TraceRawVcs, NonLocalValue, OperationValue,
1022)]
1023#[serde(rename_all = "kebab-case")]
1024pub enum EsmExternalsValue {
1025    Loose,
1026}
1027
1028#[derive(
1029    Clone, Debug, PartialEq, Deserialize, Serialize, TraceRawVcs, NonLocalValue, OperationValue,
1030)]
1031#[serde(untagged)]
1032pub enum EsmExternals {
1033    Loose(EsmExternalsValue),
1034    Bool(bool),
1035}
1036
1037// Test for esm externals deserialization.
1038#[test]
1039fn test_esm_externals_deserialization() {
1040    let json = serde_json::json!({
1041        "esmExternals": true
1042    });
1043    let config: ExperimentalConfig = serde_json::from_value(json).unwrap();
1044    assert_eq!(config.esm_externals, Some(EsmExternals::Bool(true)));
1045
1046    let json = serde_json::json!({
1047        "esmExternals": "loose"
1048    });
1049    let config: ExperimentalConfig = serde_json::from_value(json).unwrap();
1050    assert_eq!(
1051        config.esm_externals,
1052        Some(EsmExternals::Loose(EsmExternalsValue::Loose))
1053    );
1054}
1055
1056#[derive(
1057    Clone,
1058    Debug,
1059    Default,
1060    PartialEq,
1061    Eq,
1062    Deserialize,
1063    Serialize,
1064    TraceRawVcs,
1065    NonLocalValue,
1066    OperationValue,
1067)]
1068#[serde(rename_all = "camelCase")]
1069pub struct ServerActions {
1070    /// Allows adjusting body parser size limit for server actions.
1071    pub body_size_limit: Option<SizeLimit>,
1072}
1073
1074#[derive(Clone, Debug, Serialize, Deserialize, TraceRawVcs, NonLocalValue, OperationValue)]
1075#[serde(untagged)]
1076pub enum SizeLimit {
1077    Number(f64),
1078    WithUnit(String),
1079}
1080
1081// Manual implementation of PartialEq and Eq for SizeLimit because f64 doesn't
1082// implement Eq.
1083impl PartialEq for SizeLimit {
1084    fn eq(&self, other: &Self) -> bool {
1085        match (self, other) {
1086            (SizeLimit::Number(a), SizeLimit::Number(b)) => a.to_bits() == b.to_bits(),
1087            (SizeLimit::WithUnit(a), SizeLimit::WithUnit(b)) => a == b,
1088            _ => false,
1089        }
1090    }
1091}
1092
1093impl Eq for SizeLimit {}
1094
1095#[derive(
1096    Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs, NonLocalValue, OperationValue,
1097)]
1098#[serde(rename_all = "kebab-case")]
1099pub enum MiddlewarePrefetchType {
1100    Strict,
1101    Flexible,
1102}
1103
1104#[derive(
1105    Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs, NonLocalValue, OperationValue,
1106)]
1107#[serde(untagged)]
1108pub enum EmotionTransformOptionsOrBoolean {
1109    Boolean(bool),
1110    Options(EmotionTransformConfig),
1111}
1112
1113impl EmotionTransformOptionsOrBoolean {
1114    pub fn is_enabled(&self) -> bool {
1115        match self {
1116            Self::Boolean(enabled) => *enabled,
1117            _ => true,
1118        }
1119    }
1120}
1121
1122#[derive(
1123    Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs, NonLocalValue, OperationValue,
1124)]
1125#[serde(untagged)]
1126pub enum StyledComponentsTransformOptionsOrBoolean {
1127    Boolean(bool),
1128    Options(StyledComponentsTransformConfig),
1129}
1130
1131impl StyledComponentsTransformOptionsOrBoolean {
1132    pub fn is_enabled(&self) -> bool {
1133        match self {
1134            Self::Boolean(enabled) => *enabled,
1135            _ => true,
1136        }
1137    }
1138}
1139
1140#[turbo_tasks::value(eq = "manual")]
1141#[derive(Clone, Debug, PartialEq, Default, OperationValue)]
1142#[serde(rename_all = "camelCase")]
1143pub struct CompilerConfig {
1144    pub react_remove_properties: Option<ReactRemoveProperties>,
1145    pub relay: Option<RelayConfig>,
1146    pub emotion: Option<EmotionTransformOptionsOrBoolean>,
1147    pub remove_console: Option<RemoveConsoleConfig>,
1148    pub styled_components: Option<StyledComponentsTransformOptionsOrBoolean>,
1149}
1150
1151#[derive(
1152    Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs, NonLocalValue, OperationValue,
1153)]
1154#[serde(untagged, rename_all = "camelCase")]
1155pub enum ReactRemoveProperties {
1156    Boolean(bool),
1157    Config { properties: Option<Vec<String>> },
1158}
1159
1160impl ReactRemoveProperties {
1161    pub fn is_enabled(&self) -> bool {
1162        match self {
1163            Self::Boolean(enabled) => *enabled,
1164            _ => true,
1165        }
1166    }
1167}
1168
1169#[derive(
1170    Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs, NonLocalValue, OperationValue,
1171)]
1172#[serde(untagged)]
1173pub enum RemoveConsoleConfig {
1174    Boolean(bool),
1175    Config { exclude: Option<Vec<String>> },
1176}
1177
1178impl RemoveConsoleConfig {
1179    pub fn is_enabled(&self) -> bool {
1180        match self {
1181            Self::Boolean(enabled) => *enabled,
1182            _ => true,
1183        }
1184    }
1185}
1186
1187#[turbo_tasks::value(transparent)]
1188pub struct ResolveExtensions(Option<Vec<RcStr>>);
1189
1190#[turbo_tasks::value(transparent)]
1191pub struct SwcPlugins(Vec<(RcStr, serde_json::Value)>);
1192
1193#[turbo_tasks::value(transparent)]
1194pub struct OptionalMdxTransformOptions(Option<ResolvedVc<MdxTransformOptions>>);
1195
1196#[turbo_tasks::value(transparent)]
1197
1198pub struct OptionSubResourceIntegrity(Option<SubResourceIntegrity>);
1199
1200#[turbo_tasks::value(transparent)]
1201pub struct OptionFileSystemPath(Option<FileSystemPath>);
1202
1203#[turbo_tasks::value(transparent)]
1204pub struct OptionServerActions(Option<ServerActions>);
1205
1206#[turbo_tasks::value(transparent)]
1207pub struct OptionJsonValue(pub Option<serde_json::Value>);
1208
1209fn turbopack_config_documentation_link() -> RcStr {
1210    rcstr!("https://nextjs.org/docs/app/api-reference/config/next-config-js/turbopack#configuring-webpack-loaders")
1211}
1212
1213#[turbo_tasks::value(shared)]
1214struct InvalidLoaderRuleRenameAsIssue {
1215    glob: RcStr,
1216    rename_as: RcStr,
1217    config_file_path: FileSystemPath,
1218}
1219
1220#[turbo_tasks::value_impl]
1221impl Issue for InvalidLoaderRuleRenameAsIssue {
1222    #[turbo_tasks::function]
1223    async fn file_path(&self) -> Result<Vc<FileSystemPath>> {
1224        Ok(self.config_file_path.clone().cell())
1225    }
1226
1227    #[turbo_tasks::function]
1228    fn stage(&self) -> Vc<IssueStage> {
1229        IssueStage::Config.cell()
1230    }
1231
1232    #[turbo_tasks::function]
1233    async fn title(&self) -> Result<Vc<StyledString>> {
1234        Ok(
1235            StyledString::Text(format!("Invalid loader rule for extension: {}", self.glob).into())
1236                .cell(),
1237        )
1238    }
1239
1240    #[turbo_tasks::function]
1241    async fn description(&self) -> Result<Vc<OptionStyledString>> {
1242        Ok(Vc::cell(Some(
1243            StyledString::Text(RcStr::from(format!(
1244                "The extension {} contains a wildcard, but the `as` option does not: {}",
1245                self.glob, self.rename_as,
1246            )))
1247            .resolved_cell(),
1248        )))
1249    }
1250
1251    #[turbo_tasks::function]
1252    fn documentation_link(&self) -> Vc<RcStr> {
1253        Vc::cell(turbopack_config_documentation_link())
1254    }
1255}
1256
1257#[turbo_tasks::value(shared)]
1258struct InvalidLoaderRuleConditionIssue {
1259    condition: ConfigConditionItem,
1260    config_file_path: FileSystemPath,
1261}
1262
1263#[turbo_tasks::value_impl]
1264impl Issue for InvalidLoaderRuleConditionIssue {
1265    #[turbo_tasks::function]
1266    async fn file_path(self: Vc<Self>) -> Result<Vc<FileSystemPath>> {
1267        Ok(self.await?.config_file_path.clone().cell())
1268    }
1269
1270    #[turbo_tasks::function]
1271    fn stage(self: Vc<Self>) -> Vc<IssueStage> {
1272        IssueStage::Config.cell()
1273    }
1274
1275    #[turbo_tasks::function]
1276    async fn title(&self) -> Result<Vc<StyledString>> {
1277        Ok(StyledString::Text(rcstr!("Invalid condition for Turbopack loader rule")).cell())
1278    }
1279
1280    #[turbo_tasks::function]
1281    async fn description(&self) -> Result<Vc<OptionStyledString>> {
1282        Ok(Vc::cell(Some(
1283            StyledString::Text(RcStr::from(
1284                serde_json::to_string_pretty(&self.condition)
1285                    .expect("condition must be serializable"),
1286            ))
1287            .resolved_cell(),
1288        )))
1289    }
1290
1291    #[turbo_tasks::function]
1292    fn documentation_link(&self) -> Vc<RcStr> {
1293        Vc::cell(turbopack_config_documentation_link())
1294    }
1295}
1296
1297#[turbo_tasks::value_impl]
1298impl NextConfig {
1299    #[turbo_tasks::function]
1300    pub async fn from_string(string: Vc<RcStr>) -> Result<Vc<Self>> {
1301        let string = string.await?;
1302        let mut jdeserializer = serde_json::Deserializer::from_str(&string);
1303        let config: NextConfig = serde_path_to_error::deserialize(&mut jdeserializer)
1304            .with_context(|| format!("failed to parse next.config.js: {string}"))?;
1305        Ok(config.cell())
1306    }
1307
1308    #[turbo_tasks::function]
1309    pub fn bundle_pages_router_dependencies(&self) -> Vc<bool> {
1310        Vc::cell(self.bundle_pages_router_dependencies.unwrap_or_default())
1311    }
1312
1313    #[turbo_tasks::function]
1314    pub fn enable_react_production_profiling(&self) -> Vc<bool> {
1315        Vc::cell(self.react_production_profiling.unwrap_or_default())
1316    }
1317
1318    #[turbo_tasks::function]
1319    pub fn server_external_packages(&self) -> Vc<Vec<RcStr>> {
1320        Vc::cell(
1321            self.server_external_packages
1322                .as_ref()
1323                .cloned()
1324                .unwrap_or_default(),
1325        )
1326    }
1327
1328    #[turbo_tasks::function]
1329    pub fn is_standalone(&self) -> Vc<bool> {
1330        Vc::cell(self.output == Some(OutputType::Standalone))
1331    }
1332
1333    #[turbo_tasks::function]
1334    pub fn cache_handler(&self, project_path: FileSystemPath) -> Result<Vc<OptionFileSystemPath>> {
1335        if let Some(handler) = &self.cache_handler {
1336            Ok(Vc::cell(Some(project_path.join(handler)?)))
1337        } else {
1338            Ok(Vc::cell(None))
1339        }
1340    }
1341
1342    #[turbo_tasks::function]
1343    pub fn compiler(&self) -> Vc<CompilerConfig> {
1344        self.compiler.clone().unwrap_or_default().cell()
1345    }
1346
1347    #[turbo_tasks::function]
1348    pub fn env(&self) -> Vc<EnvMap> {
1349        // The value expected for env is Record<String, String>, but config itself
1350        // allows arbitrary object (https://github.com/vercel/next.js/blob/25ba8a74b7544dfb6b30d1b67c47b9cb5360cb4e/packages/next/src/server/config-schema.ts#L203)
1351        // then stringifies it. We do the interop here as well.
1352        let env = self
1353            .env
1354            .iter()
1355            .map(|(k, v)| {
1356                (
1357                    k.as_str().into(),
1358                    if let JsonValue::String(s) = v {
1359                        // A string value is kept, calling `to_string` would wrap in to quotes.
1360                        s.as_str().into()
1361                    } else {
1362                        v.to_string().into()
1363                    },
1364                )
1365            })
1366            .collect();
1367
1368        Vc::cell(env)
1369    }
1370
1371    #[turbo_tasks::function]
1372    pub fn image_config(&self) -> Vc<ImageConfig> {
1373        self.images.clone().cell()
1374    }
1375
1376    #[turbo_tasks::function]
1377    pub fn page_extensions(&self) -> Vc<Vec<RcStr>> {
1378        Vc::cell(self.page_extensions.clone())
1379    }
1380
1381    #[turbo_tasks::function]
1382    pub fn is_global_not_found_enabled(&self) -> Vc<bool> {
1383        Vc::cell(self.experimental.global_not_found.unwrap_or_default())
1384    }
1385
1386    #[turbo_tasks::function]
1387    pub fn transpile_packages(&self) -> Vc<Vec<RcStr>> {
1388        Vc::cell(self.transpile_packages.clone().unwrap_or_default())
1389    }
1390
1391    #[turbo_tasks::function]
1392    pub async fn webpack_rules(&self, project_path: FileSystemPath) -> Result<Vc<WebpackRules>> {
1393        let Some(turbo_rules) = self.turbopack.as_ref().and_then(|t| t.rules.as_ref()) else {
1394            return Ok(Vc::cell(Vec::new()));
1395        };
1396        if turbo_rules.is_empty() {
1397            return Ok(Vc::cell(Vec::new()));
1398        }
1399        let mut rules = Vec::new();
1400        for (glob, rule_collection) in turbo_rules.iter() {
1401            fn transform_loaders(
1402                loaders: &mut dyn Iterator<Item = &LoaderItem>,
1403            ) -> ResolvedVc<WebpackLoaderItems> {
1404                ResolvedVc::cell(
1405                    loaders
1406                        .map(|item| match item {
1407                            LoaderItem::LoaderName(name) => WebpackLoaderItem {
1408                                loader: name.clone(),
1409                                options: Default::default(),
1410                            },
1411                            LoaderItem::LoaderOptions(options) => options.clone(),
1412                        })
1413                        .collect(),
1414                )
1415            }
1416            let config_file_path = || project_path.join(&self.config_file_name);
1417            for item in &rule_collection.0 {
1418                match item {
1419                    RuleConfigCollectionItem::Shorthand(loaders) => {
1420                        rules.push((
1421                            glob.clone(),
1422                            LoaderRuleItem {
1423                                loaders: transform_loaders(&mut [loaders].into_iter()),
1424                                rename_as: None,
1425                                condition: None,
1426                            },
1427                        ));
1428                    }
1429                    RuleConfigCollectionItem::Full(RuleConfigItem {
1430                        loaders,
1431                        rename_as,
1432                        condition,
1433                    }) => {
1434                        // If the extension contains a wildcard, and the rename_as does not,
1435                        // emit an issue to prevent users from encountering duplicate module
1436                        // names.
1437                        if glob.contains("*")
1438                            && let Some(rename_as) = rename_as.as_ref()
1439                            && !rename_as.contains("*")
1440                        {
1441                            InvalidLoaderRuleRenameAsIssue {
1442                                glob: glob.clone(),
1443                                config_file_path: config_file_path()?,
1444                                rename_as: rename_as.clone(),
1445                            }
1446                            .resolved_cell()
1447                            .emit();
1448                        }
1449
1450                        // convert from Next.js-specific condition type to internal Turbopack
1451                        // condition type
1452                        let condition = if let Some(condition) = condition {
1453                            if let Ok(cond) = ConditionItem::try_from(condition.clone()) {
1454                                Some(cond)
1455                            } else {
1456                                InvalidLoaderRuleConditionIssue {
1457                                    condition: condition.clone(),
1458                                    config_file_path: config_file_path()?,
1459                                }
1460                                .resolved_cell()
1461                                .emit();
1462                                None
1463                            }
1464                        } else {
1465                            None
1466                        };
1467                        rules.push((
1468                            glob.clone(),
1469                            LoaderRuleItem {
1470                                loaders: transform_loaders(&mut loaders.iter()),
1471                                rename_as: rename_as.clone(),
1472                                condition,
1473                            },
1474                        ));
1475                    }
1476                }
1477            }
1478        }
1479        Ok(Vc::cell(rules))
1480    }
1481
1482    #[turbo_tasks::function]
1483    pub fn webpack_conditions(&self) -> Result<Vc<OptionWebpackConditions>> {
1484        let Some(config_conditions) = self.turbopack.as_ref().and_then(|t| t.conditions.as_ref())
1485        else {
1486            return Ok(Vc::cell(None));
1487        };
1488
1489        let conditions = config_conditions
1490            .iter()
1491            .map(|(k, v)| {
1492                let item: Result<ConditionItem> = TryInto::<ConditionItem>::try_into((*v).clone());
1493                item.map(|item| (k.clone(), item))
1494            })
1495            .collect::<Result<FxIndexMap<RcStr, ConditionItem>>>()?;
1496
1497        Ok(Vc::cell(Some(ResolvedVc::cell(conditions))))
1498    }
1499
1500    #[turbo_tasks::function]
1501    pub fn persistent_caching_enabled(&self) -> Result<Vc<bool>> {
1502        Ok(Vc::cell(
1503            self.experimental
1504                .turbopack_persistent_caching
1505                .unwrap_or_default(),
1506        ))
1507    }
1508
1509    #[turbo_tasks::function]
1510    pub fn resolve_alias_options(&self) -> Result<Vc<ResolveAliasMap>> {
1511        let Some(resolve_alias) = self
1512            .turbopack
1513            .as_ref()
1514            .and_then(|t| t.resolve_alias.as_ref())
1515        else {
1516            return Ok(ResolveAliasMap::cell(ResolveAliasMap::default()));
1517        };
1518        let alias_map: ResolveAliasMap = resolve_alias.try_into()?;
1519        Ok(alias_map.cell())
1520    }
1521
1522    #[turbo_tasks::function]
1523    pub fn resolve_extension(&self) -> Vc<ResolveExtensions> {
1524        let Some(resolve_extensions) = self
1525            .turbopack
1526            .as_ref()
1527            .and_then(|t| t.resolve_extensions.as_ref())
1528        else {
1529            return Vc::cell(None);
1530        };
1531        Vc::cell(Some(resolve_extensions.clone()))
1532    }
1533
1534    #[turbo_tasks::function]
1535    pub fn import_externals(&self) -> Result<Vc<bool>> {
1536        Ok(Vc::cell(match self.experimental.esm_externals {
1537            Some(EsmExternals::Bool(b)) => b,
1538            Some(EsmExternals::Loose(_)) => bail!("esmExternals = \"loose\" is not supported"),
1539            None => true,
1540        }))
1541    }
1542
1543    #[turbo_tasks::function]
1544    pub fn mdx_rs(&self) -> Vc<OptionalMdxTransformOptions> {
1545        let options = &self.experimental.mdx_rs;
1546
1547        let options = match options {
1548            Some(MdxRsOptions::Boolean(true)) => OptionalMdxTransformOptions(Some(
1549                MdxTransformOptions {
1550                    provider_import_source: Some(mdx_import_source_file()),
1551                    ..Default::default()
1552                }
1553                .resolved_cell(),
1554            )),
1555            Some(MdxRsOptions::Option(options)) => OptionalMdxTransformOptions(Some(
1556                MdxTransformOptions {
1557                    provider_import_source: Some(
1558                        options
1559                            .provider_import_source
1560                            .clone()
1561                            .unwrap_or(mdx_import_source_file()),
1562                    ),
1563                    ..options.clone()
1564                }
1565                .resolved_cell(),
1566            )),
1567            _ => OptionalMdxTransformOptions(None),
1568        };
1569
1570        options.cell()
1571    }
1572
1573    #[turbo_tasks::function]
1574    pub fn modularize_imports(&self) -> Vc<ModularizeImports> {
1575        Vc::cell(self.modularize_imports.clone().unwrap_or_default())
1576    }
1577
1578    #[turbo_tasks::function]
1579    pub fn experimental_cache_handlers(
1580        &self,
1581        project_path: FileSystemPath,
1582    ) -> Result<Vc<FileSystemPathVec>> {
1583        if let Some(handlers) = &self.experimental.cache_handlers {
1584            Ok(Vc::cell(
1585                handlers
1586                    .values()
1587                    .map(|h| project_path.join(h))
1588                    .collect::<Result<Vec<_>>>()?,
1589            ))
1590        } else {
1591            Ok(Vc::cell(vec![]))
1592        }
1593    }
1594
1595    #[turbo_tasks::function]
1596    pub fn experimental_swc_plugins(&self) -> Vc<SwcPlugins> {
1597        Vc::cell(self.experimental.swc_plugins.clone().unwrap_or_default())
1598    }
1599
1600    #[turbo_tasks::function]
1601    pub fn experimental_sri(&self) -> Vc<OptionSubResourceIntegrity> {
1602        Vc::cell(self.experimental.sri.clone())
1603    }
1604
1605    #[turbo_tasks::function]
1606    pub fn experimental_server_actions(&self) -> Vc<OptionServerActions> {
1607        Vc::cell(match self.experimental.server_actions.as_ref() {
1608            Some(ServerActionsOrLegacyBool::ServerActionsConfig(server_actions)) => {
1609                Some(server_actions.clone())
1610            }
1611            Some(ServerActionsOrLegacyBool::LegacyBool(true)) => Some(ServerActions::default()),
1612            _ => None,
1613        })
1614    }
1615
1616    #[turbo_tasks::function]
1617    pub fn experimental_turbopack_use_builtin_babel(&self) -> Vc<Option<bool>> {
1618        Vc::cell(self.experimental.turbopack_use_builtin_babel)
1619    }
1620
1621    #[turbo_tasks::function]
1622    pub fn experimental_turbopack_use_builtin_sass(&self) -> Vc<Option<bool>> {
1623        Vc::cell(self.experimental.turbopack_use_builtin_sass)
1624    }
1625
1626    #[turbo_tasks::function]
1627    pub fn react_compiler(&self) -> Vc<OptionalReactCompilerOptions> {
1628        let options = &self.experimental.react_compiler;
1629
1630        let options = match options {
1631            Some(ReactCompilerOptionsOrBoolean::Boolean(true)) => {
1632                OptionalReactCompilerOptions(Some(
1633                    ReactCompilerOptions {
1634                        compilation_mode: None,
1635                        panic_threshold: None,
1636                    }
1637                    .resolved_cell(),
1638                ))
1639            }
1640            Some(ReactCompilerOptionsOrBoolean::Option(options)) => OptionalReactCompilerOptions(
1641                Some(ReactCompilerOptions { ..options.clone() }.resolved_cell()),
1642            ),
1643            _ => OptionalReactCompilerOptions(None),
1644        };
1645
1646        options.cell()
1647    }
1648
1649    #[turbo_tasks::function]
1650    pub fn sass_config(&self) -> Vc<JsonValue> {
1651        Vc::cell(self.sass_options.clone().unwrap_or_default())
1652    }
1653
1654    #[turbo_tasks::function]
1655    pub fn skip_middleware_url_normalize(&self) -> Vc<bool> {
1656        Vc::cell(self.skip_middleware_url_normalize.unwrap_or(false))
1657    }
1658
1659    #[turbo_tasks::function]
1660    pub fn skip_trailing_slash_redirect(&self) -> Vc<bool> {
1661        Vc::cell(self.skip_trailing_slash_redirect.unwrap_or(false))
1662    }
1663
1664    /// Returns the final asset prefix. If an assetPrefix is set, it's used.
1665    /// Otherwise, the basePath is used.
1666    #[turbo_tasks::function]
1667    pub async fn computed_asset_prefix(self: Vc<Self>) -> Result<Vc<Option<RcStr>>> {
1668        let this = self.await?;
1669
1670        Ok(Vc::cell(Some(
1671            format!(
1672                "{}/_next/",
1673                if let Some(asset_prefix) = &this.asset_prefix {
1674                    asset_prefix
1675                } else {
1676                    this.base_path.as_ref().map_or("", |b| b.as_str())
1677                }
1678                .trim_end_matches('/')
1679            )
1680            .into(),
1681        )))
1682    }
1683
1684    /// Returns the suffix to use for chunk loading.
1685    #[turbo_tasks::function]
1686    pub async fn chunk_suffix_path(self: Vc<Self>) -> Result<Vc<Option<RcStr>>> {
1687        let this = self.await?;
1688
1689        match &this.deployment_id {
1690            Some(deployment_id) => Ok(Vc::cell(Some(format!("?dpl={deployment_id}").into()))),
1691            None => Ok(Vc::cell(None)),
1692        }
1693    }
1694
1695    #[turbo_tasks::function]
1696    pub fn enable_ppr(&self) -> Vc<bool> {
1697        Vc::cell(
1698            self.experimental
1699                .ppr
1700                .as_ref()
1701                .map(|ppr| match ppr {
1702                    ExperimentalPartialPrerendering::Incremental(
1703                        ExperimentalPartialPrerenderingIncrementalValue::Incremental,
1704                    ) => true,
1705                    ExperimentalPartialPrerendering::Boolean(b) => *b,
1706                })
1707                .unwrap_or(false),
1708        )
1709    }
1710
1711    #[turbo_tasks::function]
1712    pub fn enable_taint(&self) -> Vc<bool> {
1713        Vc::cell(self.experimental.taint.unwrap_or(false))
1714    }
1715
1716    #[turbo_tasks::function]
1717    pub fn enable_router_bfcache(&self) -> Vc<bool> {
1718        Vc::cell(self.experimental.router_bfcache.unwrap_or(false))
1719    }
1720
1721    #[turbo_tasks::function]
1722    pub fn enable_view_transition(&self) -> Vc<bool> {
1723        Vc::cell(self.experimental.view_transition.unwrap_or(false))
1724    }
1725
1726    #[turbo_tasks::function]
1727    pub fn enable_cache_components(&self) -> Vc<bool> {
1728        Vc::cell(self.experimental.cache_components.unwrap_or(false))
1729    }
1730
1731    #[turbo_tasks::function]
1732    pub fn enable_use_cache(&self) -> Vc<bool> {
1733        Vc::cell(
1734            self.experimental
1735                .use_cache
1736                // "use cache" was originally implicitly enabled with the
1737                // cacheComponents flag, so we transfer the value for cacheComponents to the
1738                // explicit useCache flag to ensure backwards compatibility.
1739                .unwrap_or(self.experimental.cache_components.unwrap_or(false)),
1740        )
1741    }
1742
1743    #[turbo_tasks::function]
1744    pub fn enable_root_params(&self) -> Vc<bool> {
1745        Vc::cell(
1746            self.experimental
1747                .root_params
1748                // rootParams should be enabled implicitly in cacheComponents.
1749                .unwrap_or(self.experimental.cache_components.unwrap_or(false)),
1750        )
1751    }
1752
1753    #[turbo_tasks::function]
1754    pub fn cache_kinds(&self) -> Vc<CacheKinds> {
1755        let mut cache_kinds = CacheKinds::default();
1756
1757        if let Some(handlers) = self.experimental.cache_handlers.as_ref() {
1758            cache_kinds.extend(handlers.keys().cloned());
1759        }
1760
1761        cache_kinds.cell()
1762    }
1763
1764    #[turbo_tasks::function]
1765    pub fn optimize_package_imports(&self) -> Vc<Vec<RcStr>> {
1766        Vc::cell(
1767            self.experimental
1768                .optimize_package_imports
1769                .clone()
1770                .unwrap_or_default(),
1771        )
1772    }
1773
1774    #[turbo_tasks::function]
1775    pub fn tree_shaking_mode_for_foreign_code(
1776        &self,
1777        _is_development: bool,
1778    ) -> Vc<OptionTreeShaking> {
1779        OptionTreeShaking(match self.experimental.turbopack_tree_shaking {
1780            Some(false) => Some(TreeShakingMode::ReexportsOnly),
1781            Some(true) => Some(TreeShakingMode::ModuleFragments),
1782            None => Some(TreeShakingMode::ReexportsOnly),
1783        })
1784        .cell()
1785    }
1786
1787    #[turbo_tasks::function]
1788    pub fn tree_shaking_mode_for_user_code(&self, _is_development: bool) -> Vc<OptionTreeShaking> {
1789        OptionTreeShaking(match self.experimental.turbopack_tree_shaking {
1790            Some(false) => Some(TreeShakingMode::ReexportsOnly),
1791            Some(true) => Some(TreeShakingMode::ModuleFragments),
1792            None => Some(TreeShakingMode::ReexportsOnly),
1793        })
1794        .cell()
1795    }
1796
1797    #[turbo_tasks::function]
1798    pub async fn turbopack_remove_unused_exports(&self, mode: Vc<NextMode>) -> Result<Vc<bool>> {
1799        Ok(Vc::cell(
1800            self.experimental
1801                .turbopack_remove_unused_exports
1802                .unwrap_or(matches!(*mode.await?, NextMode::Build)),
1803        ))
1804    }
1805
1806    #[turbo_tasks::function]
1807    pub async fn module_ids(&self, mode: Vc<NextMode>) -> Result<Vc<ModuleIds>> {
1808        Ok(match *mode.await? {
1809            // Ignore configuration in development mode, HMR only works with `named`
1810            NextMode::Development => ModuleIds::Named.cell(),
1811            NextMode::Build => self
1812                .turbopack
1813                .as_ref()
1814                .and_then(|t| t.module_ids)
1815                .unwrap_or(ModuleIds::Deterministic)
1816                .cell(),
1817        })
1818    }
1819
1820    #[turbo_tasks::function]
1821    pub async fn turbo_minify(&self, mode: Vc<NextMode>) -> Result<Vc<bool>> {
1822        let minify = self.experimental.turbopack_minify;
1823        Ok(Vc::cell(
1824            minify.unwrap_or(matches!(*mode.await?, NextMode::Build)),
1825        ))
1826    }
1827
1828    #[turbo_tasks::function]
1829    pub async fn turbo_scope_hoisting(&self, mode: Vc<NextMode>) -> Result<Vc<bool>> {
1830        Ok(Vc::cell(match *mode.await? {
1831            // Ignore configuration in development mode to not break HMR
1832            NextMode::Development => false,
1833            NextMode::Build => self.experimental.turbopack_scope_hoisting.unwrap_or(true),
1834        }))
1835    }
1836
1837    #[turbo_tasks::function]
1838    pub async fn client_source_maps(&self, mode: Vc<NextMode>) -> Result<Vc<bool>> {
1839        let source_maps = self.experimental.turbopack_source_maps;
1840        Ok(Vc::cell(source_maps.unwrap_or(match &*mode.await? {
1841            NextMode::Development => true,
1842            NextMode::Build => self.production_browser_source_maps,
1843        })))
1844    }
1845
1846    #[turbo_tasks::function]
1847    pub fn server_source_maps(&self) -> Result<Vc<bool>> {
1848        let source_maps = self.experimental.turbopack_source_maps;
1849        Ok(Vc::cell(source_maps.unwrap_or(true)))
1850    }
1851
1852    #[turbo_tasks::function]
1853    pub fn typescript_tsconfig_path(&self) -> Result<Vc<Option<RcStr>>> {
1854        Ok(Vc::cell(
1855            self.typescript
1856                .tsconfig_path
1857                .as_ref()
1858                .map(|path| path.to_owned().into()),
1859        ))
1860    }
1861
1862    #[turbo_tasks::function]
1863    pub fn output_file_tracing_includes(&self) -> Vc<OptionJsonValue> {
1864        Vc::cell(self.output_file_tracing_includes.clone())
1865    }
1866
1867    #[turbo_tasks::function]
1868    pub fn output_file_tracing_excludes(&self) -> Vc<OptionJsonValue> {
1869        Vc::cell(self.output_file_tracing_excludes.clone())
1870    }
1871
1872    #[turbo_tasks::function]
1873    pub async fn fetch_client(&self, env: Vc<Box<dyn ProcessEnv>>) -> Result<Vc<FetchClient>> {
1874        // Support both an env var and the experimental flag to provide more flexibility to
1875        // developers on locked down systems, depending on if they want to configure this on a
1876        // per-system or per-project basis.
1877        let use_system_tls_certs = env
1878            .read(rcstr!("NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS"))
1879            .await?
1880            .as_ref()
1881            .and_then(|env_value| {
1882                // treat empty value same as an unset value
1883                (!env_value.is_empty()).then(|| env_value == "1" || env_value == "true")
1884            })
1885            .or(self.experimental.turbopack_use_system_tls_certs)
1886            .unwrap_or(false);
1887        Ok(FetchClient {
1888            tls_built_in_webpki_certs: !use_system_tls_certs,
1889            tls_built_in_native_certs: use_system_tls_certs,
1890        }
1891        .cell())
1892    }
1893}
1894
1895/// A subset of ts/jsconfig that next.js implicitly
1896/// interops with.
1897#[turbo_tasks::value(serialization = "custom", eq = "manual")]
1898#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1899#[serde(rename_all = "camelCase")]
1900pub struct JsConfig {
1901    compiler_options: Option<serde_json::Value>,
1902}
1903
1904#[turbo_tasks::value_impl]
1905impl JsConfig {
1906    #[turbo_tasks::function]
1907    pub async fn from_string(string: Vc<RcStr>) -> Result<Vc<Self>> {
1908        let string = string.await?;
1909        let config: JsConfig = serde_json::from_str(&string)
1910            .with_context(|| format!("failed to parse next.config.js: {string}"))?;
1911
1912        Ok(config.cell())
1913    }
1914
1915    #[turbo_tasks::function]
1916    pub fn compiler_options(&self) -> Vc<serde_json::Value> {
1917        Vc::cell(self.compiler_options.clone().unwrap_or_default())
1918    }
1919}
1920
1921#[cfg(test)]
1922mod tests {
1923    use super::*;
1924
1925    #[test]
1926    fn test_serde_rule_config_item_options() {
1927        let json_value = serde_json::json!({
1928            "loaders": [],
1929            "as": "*.js",
1930            "condition": {
1931                "all": [
1932                    "production",
1933                    {"not": "foreign"},
1934                    {"any": [
1935                        "browser",
1936                        {
1937                            "path": { "type": "glob", "value": "*.svg"},
1938                            "content": {
1939                                "source": "@someTag",
1940                                "flags": ""
1941                            }
1942                        }
1943                    ]},
1944                ],
1945            }
1946        });
1947
1948        let rule_config: RuleConfigItem = serde_json::from_value(json_value).unwrap();
1949
1950        assert_eq!(
1951            rule_config,
1952            RuleConfigItem {
1953                loaders: vec![],
1954                rename_as: Some(rcstr!("*.js")),
1955                condition: Some(ConfigConditionItem::All(
1956                    [
1957                        ConfigConditionItem::Builtin(WebpackLoaderBuiltinCondition::Production),
1958                        ConfigConditionItem::Not(Box::new(ConfigConditionItem::Builtin(
1959                            WebpackLoaderBuiltinCondition::Foreign
1960                        ))),
1961                        ConfigConditionItem::Any(
1962                            vec![
1963                                ConfigConditionItem::Builtin(
1964                                    WebpackLoaderBuiltinCondition::Browser
1965                                ),
1966                                ConfigConditionItem::Base {
1967                                    path: Some(ConfigConditionPath::Glob(rcstr!("*.svg"))),
1968                                    content: Some(RegexComponents {
1969                                        source: rcstr!("@someTag"),
1970                                        flags: rcstr!(""),
1971                                    }),
1972                                },
1973                            ]
1974                            .into(),
1975                        ),
1976                    ]
1977                    .into(),
1978                )),
1979            }
1980        );
1981    }
1982}