Skip to main content

next_core/
segment_config.rs

1use std::borrow::Cow;
2
3use anyhow::{Result, bail};
4use async_trait::async_trait;
5use bincode::{Decode, Encode};
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use swc_core::{
9    common::{DUMMY_SP, GLOBALS, Span, Spanned, source_map::SmallPos},
10    ecma::{
11        ast::{
12            ClassExpr, Decl, ExportSpecifier, Expr, ExprStmt, FnExpr, Lit, ModuleDecl,
13            ModuleExportName, ModuleItem, Program, Stmt, Str, TsAsExpr, TsConstAssertion,
14            TsSatisfiesExpr, TsTypeAssertion,
15        },
16        utils::IsDirective,
17    },
18};
19use turbo_rcstr::{RcStr, rcstr};
20use turbo_tasks::{JoinIterExt, NonLocalValue, ResolvedVc, ValueDefault, Vc, util::WrapFuture};
21use turbo_tasks_fs::FileSystemPath;
22use turbopack_core::{
23    file_source::FileSource,
24    ident::AssetIdent,
25    issue::{Issue, IssueExt, IssueSeverity, IssueSource, IssueStage, StyledString},
26    source::Source,
27};
28use turbopack_ecmascript::{
29    EcmascriptInputTransforms, EcmascriptModuleAssetType,
30    analyzer::{
31        Bump, ConstantNumber, ConstantValue, JsValue, ObjectPart, ThreadLocal, graph::EvalContext,
32    },
33    parse::{ParseResult, parse},
34};
35
36use crate::{
37    app_structure::AppPageLoaderTree,
38    next_config::RouteHas,
39    next_manifests::ProxyMatcher,
40    util::{MiddlewareMatcherKind, NextRuntime},
41};
42
43#[derive(
44    Default, PartialEq, Eq, Clone, Copy, Debug, Deserialize, NonLocalValue, Encode, Decode,
45)]
46#[serde(rename_all = "kebab-case")]
47pub enum NextSegmentDynamic {
48    #[default]
49    Auto,
50    ForceDynamic,
51    Error,
52    ForceStatic,
53}
54
55#[derive(
56    Default, PartialEq, Eq, Clone, Copy, Debug, Deserialize, NonLocalValue, Encode, Decode,
57)]
58#[serde(rename_all = "kebab-case")]
59pub enum NextSegmentFetchCache {
60    #[default]
61    Auto,
62    DefaultCache,
63    OnlyCache,
64    ForceCache,
65    DefaultNoStore,
66    OnlyNoStore,
67    ForceNoStore,
68}
69
70#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, NonLocalValue, Encode, Decode)]
71pub enum NextRevalidate {
72    #[default]
73    Never,
74    ForceCache,
75    Frequency {
76        seconds: u32,
77    },
78}
79
80#[derive(PartialEq, Eq, Clone, Debug, NonLocalValue, Encode, Decode)]
81pub enum NextSegmentRegion {
82    Single(RcStr),
83    Multiple(Vec<RcStr>),
84}
85
86#[derive(PartialEq, Eq, Clone, Copy, Debug, NonLocalValue, Encode, Decode, Serialize)]
87#[serde(rename_all = "lowercase")]
88pub enum NextSegmentRsc {
89    Server,
90    Client,
91}
92
93impl NextSegmentRegion {
94    pub fn to_vec(&self) -> Vec<RcStr> {
95        match self {
96            Self::Single(region) => vec![region.clone()],
97            Self::Multiple(regions) => regions.clone(),
98        }
99    }
100}
101
102#[turbo_tasks::value(shared)]
103#[derive(Debug, Default, Clone)]
104pub struct NextSegmentConfig {
105    pub rsc: Option<NextSegmentRsc>,
106    pub dynamic: Option<NextSegmentDynamic>,
107    pub dynamic_params: Option<bool>,
108    pub revalidate: Option<NextRevalidate>,
109    pub max_duration: Option<u32>,
110    pub fetch_cache: Option<NextSegmentFetchCache>,
111    pub runtime: Option<NextRuntime>,
112    pub preferred_region: Option<NextSegmentRegion>,
113    pub middleware_matcher: Option<Vec<MiddlewareMatcherKind>>,
114    pub unstable_allow_dynamic: Option<Vec<RcStr>>,
115
116    /// Whether these exports are defined in the source file.
117    pub generate_image_metadata: bool,
118    pub generate_sitemaps: bool,
119    #[turbo_tasks(unsafe_ignore)]
120    #[bincode(with_serde)]
121    pub generate_static_params: Option<Span>,
122    #[turbo_tasks(unsafe_ignore)]
123    #[bincode(with_serde)]
124    pub instant: Option<Span>,
125    #[turbo_tasks(unsafe_ignore)]
126    #[bincode(with_serde)]
127    pub prefetch: Option<Span>,
128    #[turbo_tasks(unsafe_ignore)]
129    #[bincode(with_serde)]
130    pub ensure_static: Option<Span>,
131}
132
133/// Legacy config fields that are fallbacks for individual exports outside the App Router.
134#[derive(Default)]
135struct LegacySegmentConfig {
136    runtime: Option<NextRuntime>,
137    max_duration: Option<u32>,
138}
139
140#[turbo_tasks::value_impl]
141impl ValueDefault for NextSegmentConfig {
142    #[turbo_tasks::function]
143    pub fn value_default() -> Vc<Self> {
144        NextSegmentConfig::default().cell()
145    }
146}
147
148impl NextSegmentConfig {
149    /// Applies the parent config to this config, setting any unset values to
150    /// the parent's values.
151    pub fn apply_parent_config(&mut self, parent: &Self) {
152        let NextSegmentConfig {
153            // A layout's RSC classification does not apply to its child page.
154            rsc: _,
155            dynamic,
156            dynamic_params,
157            revalidate,
158            max_duration,
159            fetch_cache,
160            runtime,
161            preferred_region,
162            // TODO what about these?
163            instant: _,
164            prefetch: _,
165            ensure_static: _,
166            // Don't need merging
167            middleware_matcher: _,
168            unstable_allow_dynamic: _,
169            generate_image_metadata: _,
170            generate_sitemaps: _,
171            generate_static_params: _,
172        } = self;
173        *dynamic = dynamic.or(parent.dynamic);
174        *dynamic_params = dynamic_params.or(parent.dynamic_params);
175        *revalidate = revalidate.or(parent.revalidate);
176        *max_duration = max_duration.or(parent.max_duration);
177        *fetch_cache = fetch_cache.or(parent.fetch_cache);
178        *runtime = runtime.or(parent.runtime);
179        *preferred_region = preferred_region.take().or(parent.preferred_region.clone());
180    }
181
182    /// Applies a config from a parallel route to this config, returning an
183    /// error if there are conflicting values.
184    pub fn apply_parallel_config(&mut self, parallel_config: &Self) -> Result<()> {
185        fn merge_parallel<T: PartialEq + Clone>(
186            a: &mut Option<T>,
187            b: &Option<T>,
188            name: &str,
189        ) -> Result<()> {
190            match (a.as_ref(), b) {
191                (Some(a), Some(b)) if *a != *b => {
192                    bail!(
193                        "Sibling segment configs have conflicting values for {}",
194                        name
195                    )
196                }
197                (None, Some(b)) => {
198                    *a = Some(b.clone());
199                }
200                _ => {}
201            }
202            Ok(())
203        }
204        let Self {
205            rsc,
206            dynamic,
207            dynamic_params,
208            revalidate,
209            max_duration,
210            fetch_cache,
211            runtime,
212            preferred_region,
213            // TODO what about these?
214            instant: _,
215            prefetch: _,
216            ensure_static: _,
217            // Don't need merging
218            middleware_matcher: _,
219            unstable_allow_dynamic: _,
220            generate_image_metadata: _,
221            generate_sitemaps: _,
222            generate_static_params: _,
223        } = self;
224        // Carry the leaf page's RSC classification up through its loader-tree branch. Unlike route
225        // segment config, classifications in different parallel branches are not conflicts.
226        *rsc = (*rsc).or(parallel_config.rsc);
227        merge_parallel(dynamic, &parallel_config.dynamic, "dynamic")?;
228        merge_parallel(
229            dynamic_params,
230            &parallel_config.dynamic_params,
231            "dynamicParams",
232        )?;
233        merge_parallel(revalidate, &parallel_config.revalidate, "revalidate")?;
234        merge_parallel(max_duration, &parallel_config.max_duration, "maxDuration")?;
235        merge_parallel(fetch_cache, &parallel_config.fetch_cache, "fetchCache")?;
236        merge_parallel(runtime, &parallel_config.runtime, "runtime")?;
237        match (preferred_region.as_ref(), &parallel_config.preferred_region) {
238            (Some(a), Some(b)) if a.to_vec() != b.to_vec() => {
239                bail!("Sibling segment configs have conflicting values for preferredRegion")
240            }
241            (None, Some(b)) => {
242                *preferred_region = Some(b.clone());
243            }
244            _ => {}
245        }
246        Ok(())
247    }
248
249    pub fn get_proxy_matchers(
250        &self,
251        has_i18n: bool,
252        has_i18n_locales: bool,
253        base_path: Option<&str>,
254    ) -> Option<Vec<ProxyMatcher>> {
255        self.middleware_matcher.as_ref().map(|matchers| {
256            matchers
257                .iter()
258                .map(|matcher| {
259                    let mut matcher = match matcher {
260                        MiddlewareMatcherKind::Str(matcher) => ProxyMatcher {
261                            original_source: matcher.as_str().into(),
262                            ..Default::default()
263                        },
264                        MiddlewareMatcherKind::Matcher(matcher) => matcher.clone(),
265                    };
266
267                    // Mirrors implementation in get-page-static-info.ts getMiddlewareMatchers
268                    let mut source = matcher.original_source.to_string();
269                    let is_root = source == "/";
270                    let has_locale = matcher.locale;
271
272                    if has_i18n_locales && has_locale {
273                        if is_root {
274                            source.clear();
275                        }
276                        source.insert_str(0, "/:nextInternalLocale((?!_next/)[^/.]{1,})");
277                    }
278
279                    // Match transport-specific route forms that resolve to the
280                    // same page:
281                    // - Pages Router data routes: /_next/data/<build-id>/...
282                    // - App Router transport routes: .rsc, ...segments/...segment.rsc
283                    if is_root {
284                        source.push('(');
285                        if has_i18n {
286                            source.push_str("|\\.json|");
287                        }
288                        source.push_str("/?index|/?index\\.json|");
289                        source.push_str("/?index(?:\\.rsc|\\.segments/.+\\.segment\\.rsc)");
290                        source.push_str(")?");
291                    } else {
292                        source.push_str("{(\\.json|\\.rsc|\\.segments/.+\\.segment\\.rsc)}?");
293                    };
294
295                    source.insert_str(0, "/:nextData(_next/data/[^/]{1,})?");
296
297                    if let Some(base_path) = base_path.as_ref() {
298                        source.insert_str(0, base_path);
299                    }
300
301                    // TODO: The implementation of getMiddlewareMatchers outputs a regex here
302                    // using path-to-regexp. Currently there is no
303                    // equivalent of that so it post-processes
304                    // this value to the relevant regex in manifest-loader.ts
305                    matcher.regexp = Some(RcStr::from(source));
306
307                    matcher
308                })
309                .collect()
310        })
311    }
312}
313
314/// An issue that occurred while parsing the app segment config.
315#[turbo_tasks::value(shared)]
316pub struct NextSegmentConfigParsingIssue {
317    ident: ResolvedVc<AssetIdent>,
318    key: RcStr,
319    error: RcStr,
320    detail: Option<ResolvedVc<StyledString>>,
321    source: IssueSource,
322    severity: IssueSeverity,
323}
324
325#[turbo_tasks::value_impl]
326impl NextSegmentConfigParsingIssue {
327    #[turbo_tasks::function]
328    pub fn new(
329        ident: ResolvedVc<AssetIdent>,
330        key: RcStr,
331        error: RcStr,
332        detail: Option<ResolvedVc<StyledString>>,
333        source: IssueSource,
334        severity: IssueSeverity,
335    ) -> Vc<Self> {
336        Self {
337            ident,
338            key,
339            error,
340            detail,
341            source,
342            severity,
343        }
344        .cell()
345    }
346}
347
348#[async_trait]
349#[turbo_tasks::value_impl]
350impl Issue for NextSegmentConfigParsingIssue {
351    fn severity(&self) -> IssueSeverity {
352        self.severity
353    }
354
355    async fn title(&self) -> Result<StyledString> {
356        Ok(StyledString::Line(vec![
357            StyledString::Text(
358                format!(
359                    "Next.js can't recognize the exported `{}` field in route. ",
360                    self.key,
361                )
362                .into(),
363            ),
364            StyledString::Text(self.error.clone()),
365        ]))
366    }
367
368    fn stage(&self) -> IssueStage {
369        IssueStage::Parse
370    }
371
372    async fn file_path(&self) -> Result<FileSystemPath> {
373        Ok(self.ident.await?.path.clone())
374    }
375
376    async fn description(&self) -> Result<Option<StyledString>> {
377        Ok(Some(StyledString::Text(rcstr!(
378            "The exported configuration object in a source file needs to have a very specific \
379             format from which some properties can be statically parsed at compiled-time."
380        ))))
381    }
382
383    async fn detail(&self) -> Result<Option<StyledString>> {
384        match self.detail {
385            Some(d) => Ok(Some((*d.await?).clone())),
386            None => Ok(None),
387        }
388    }
389
390    fn documentation_link(&self) -> RcStr {
391        rcstr!("https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config")
392    }
393
394    fn source(&self) -> Option<IssueSource> {
395        Some(self.source)
396    }
397}
398
399#[turbo_tasks::task_input]
400#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Encode, Decode)]
401pub enum ParseSegmentMode {
402    Base,
403    // Disallows "use client + generateStatic" and ignores/warns about `export const config`
404    App,
405    // Disallows config = { runtime: "edge" }
406    Proxy,
407}
408
409/// Parse the raw source code of a file to get the segment config local to that file.
410///
411/// See [the Next.js documentation for Route Segment
412/// Configs](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config).
413///
414/// Pages router and middleware use this directly. App router uses
415/// `parse_segment_config_from_loader_tree` instead, which aggregates configuration information
416/// across multiple files.
417///
418/// ## A Note on Parsing the Raw Source Code
419///
420/// A better API would use `ModuleAssetContext::process` to convert the `Source` to a `Module`,
421/// instead of parsing the raw source code. That would ensure that things like webpack loaders can
422/// run before SWC tries to parse the file, e.g. to strip unsupported syntax using Babel. However,
423/// because the config includes `runtime`, we can't know which context to use until after parsing
424/// the file.
425///
426/// This could be solved with speculative parsing:
427/// 1. Speculatively process files and extract route segment configs using the Node.js
428///    `ModuleAssetContext` first. This is the common/happy codepath.
429/// 2. If we get a config specifying `runtime = "edge"`, we should use the Edge runtime's
430///    `ModuleAssetContext` and re-process the file(s), extracting the segment config again.
431/// 3. If we failed to get a configuration (e.g. a parse error), we need speculatively process with
432///    the Edge runtime and look for a `runtime = "edge"` configuration key. If that also fails,
433///    then we should report any issues/errors from the first attempt using the Node.js context.
434///
435/// While a speculative parsing algorithm is straightforward, there are a few factors that make it
436/// impractical to implement:
437///
438/// - The app router config is loaded across many different files (page, layout, or route handler,
439///   including an arbitrary number of those files in parallel routes), and once we discover that
440///   something specified edge runtime, we must restart that entire loop, so try/reparse logic can't
441///   be cleanly encapsulated to an operation over a single file.
442///
443/// - There's a lot of tracking that needs to happen to later suppress `Issue` collectibles on
444///   speculatively-executed `OperationVc`s.
445///
446/// - Most things default to the node.js runtime and can be overridden to edge runtime, but
447///   middleware is an exception, so different codepaths have different defaults.
448///
449/// The `runtime` option is going to be deprecated, and we may eventually remove edge runtime
450/// completely (in Next 18?), so it doesn't make sense to spend a ton of time improving logic around
451/// that. In the future, doing this the right way with the `ModuleAssetContext` will be easy (there
452/// will only be one, no speculative parsing is needed), and I think it's okay to use a hacky
453/// solution for a couple years until that day comes.
454///
455/// ## What does webpack do?
456///
457/// The logic is in `packages/next/src/build/analysis/get-page-static-info.ts`, but it's very
458/// similar to what we do here.
459///
460/// There are a couple of notable differences:
461///
462/// - The webpack implementation uses a regexp (`PARSE_PATTERN`) to skip parsing some files, but
463///   this regexp is imperfect and may also suppress some lints that we have. The performance
464///   benefit is small, so we're not currently doing this (but we could revisit that decision in the
465///   future).
466///
467/// - The `parseModule` helper function swallows errors (!) returning a `null` ast value when
468///   parsing fails. This seems bad, as it may lead to silently-ignored segment configs, so we don't
469///   want to do this.
470#[turbo_tasks::function]
471pub async fn parse_segment_config_from_source(
472    source: ResolvedVc<Box<dyn Source>>,
473    mode: ParseSegmentMode,
474) -> Result<Vc<NextSegmentConfig>> {
475    let ident = source.ident().await?;
476    let path = &ident.path;
477
478    // Don't try parsing if it's not a javascript file, otherwise it will emit an
479    // issue causing the build to "fail".
480    if path.path.ends_with(".d.ts")
481        || !(path.path.ends_with(".js")
482            || path.path.ends_with(".jsx")
483            || path.path.ends_with(".ts")
484            || path.path.ends_with(".tsx"))
485    {
486        return Ok(Default::default());
487    }
488
489    let result = &*parse(
490        *source,
491        if path.path.ends_with(".ts") {
492            EcmascriptModuleAssetType::Typescript {
493                tsx: false,
494                analyze_types: false,
495            }
496        } else if path.path.ends_with(".tsx") {
497            EcmascriptModuleAssetType::Typescript {
498                tsx: true,
499                analyze_types: false,
500            }
501        } else {
502            EcmascriptModuleAssetType::Ecmascript
503        },
504        EcmascriptInputTransforms::empty(),
505        // node_env is not used here: EcmascriptInputTransforms::empty() means no
506        // transforms are applied, so TransformContext::node_env is never accessed.
507        rcstr!("development"),
508        false,
509        false,
510    )
511    .await?;
512
513    let ParseResult::Ok {
514        program: Program::Module(module_ast),
515        eval_context,
516        globals,
517        ..
518    } = result
519    else {
520        // The `parse` call has already emitted parse issues in case of `ParseResult::Unparsable`
521        return Ok(Default::default());
522    };
523
524    // Arena for the `JsValue`s produced while evaluating config expressions;
525    // freed when this function returns.
526    let arena = ThreadLocal::new();
527    let mut config = WrapFuture::new(
528        async {
529            let mut config = NextSegmentConfig::default();
530            let mut legacy_config = LegacySegmentConfig::default();
531
532            let mut parse = async |ident, init, span| {
533                parse_config_value(
534                    source,
535                    mode,
536                    &mut config,
537                    &mut legacy_config,
538                    eval_context,
539                    &arena,
540                    ident,
541                    init,
542                    span,
543                )
544                .await
545            };
546
547            for item in &module_ast.body {
548                match item {
549                    ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(decl)) => match &decl.decl {
550                        Decl::Class(decl) => {
551                            parse(
552                                Cow::Borrowed(decl.ident.sym.as_str()),
553                                Some(Cow::Owned(Expr::Class(ClassExpr {
554                                    ident: None,
555                                    class: decl.class.clone(),
556                                }))),
557                                decl.span(),
558                            )
559                            .await?
560                        }
561                        Decl::Fn(decl) => {
562                            parse(
563                                Cow::Borrowed(decl.ident.sym.as_str()),
564                                Some(Cow::Owned(Expr::Fn(FnExpr {
565                                    ident: None,
566                                    function: decl.function.clone(),
567                                }))),
568                                decl.span(),
569                            )
570                            .await?
571                        }
572                        Decl::Var(decl) => {
573                            for decl in &decl.decls {
574                                let Some(ident) = decl.name.as_ident() else {
575                                    continue;
576                                };
577
578                                let key = &ident.id.sym;
579
580                                parse(
581                                    Cow::Borrowed(key.as_str()),
582                                    Some(
583                                        decl.init.as_deref().map(Cow::Borrowed).unwrap_or_else(
584                                            || Cow::Owned(*Expr::undefined(DUMMY_SP)),
585                                        ),
586                                    ),
587                                    // The config object can span hundreds of lines. Don't
588                                    // highlight the whole thing
589                                    if key == "config" {
590                                        ident.id.span
591                                    } else {
592                                        decl.span()
593                                    },
594                                )
595                                .await?;
596                            }
597                        }
598                        _ => continue,
599                    },
600                    ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(named)) => {
601                        for specifier in &named.specifiers {
602                            if let ExportSpecifier::Named(named) = specifier {
603                                parse(
604                                    match named.exported.as_ref().unwrap_or(&named.orig) {
605                                        ModuleExportName::Ident(ident) => {
606                                            Cow::Borrowed(ident.sym.as_str())
607                                        }
608                                        ModuleExportName::Str(s) => s.value.to_string_lossy(),
609                                    },
610                                    None,
611                                    specifier.span(),
612                                )
613                                .await?;
614                            }
615                        }
616                    }
617                    _ => {
618                        continue;
619                    }
620                }
621            }
622            // Individual exports take precedence over legacy config fields, regardless of
623            // export order. App Router ignores legacy config, so its fallback stays empty.
624            config.runtime = config.runtime.or(legacy_config.runtime);
625            config.max_duration = config.max_duration.or(legacy_config.max_duration);
626            anyhow::Ok(config)
627        },
628        |f, ctx| GLOBALS.set(globals, || f.poll(ctx)),
629    )
630    .await?;
631
632    let is_client_entry = module_ast
633        .body
634        .iter()
635        .take_while(|i| match i {
636            ModuleItem::Stmt(stmt) => stmt.directive_continue(),
637            ModuleItem::ModuleDecl(_) => false,
638        })
639        .filter_map(|i| i.as_stmt())
640        .any(|f| match f {
641            Stmt::Expr(ExprStmt { expr, .. }) => match &**expr {
642                Expr::Lit(Lit::Str(Str { value, .. })) => value == "use client",
643                _ => false,
644            },
645            _ => false,
646        });
647
648    config.rsc = Some(if is_client_entry {
649        NextSegmentRsc::Client
650    } else {
651        NextSegmentRsc::Server
652    });
653
654    if mode == ParseSegmentMode::App && is_client_entry {
655        if let Some(span) = config.generate_static_params {
656            invalid_config(
657                source,
658                "generateStaticParams",
659                span,
660                rcstr!(
661                    "App pages cannot use both \"use client\" and export function \
662                     \"generateStaticParams()\"."
663                ),
664                None,
665                IssueSeverity::Error,
666            )
667            .await?;
668        }
669
670        let server_only_route_configs = [
671            ("instant", config.instant),
672            ("prefetch", config.prefetch),
673            ("ensureStatic", config.ensure_static),
674        ];
675        for (config_name, usage) in server_only_route_configs {
676            if let Some(span) = usage {
677                invalid_config(
678                    source,
679                    config_name,
680                    span,
681                    format!(
682                        "\"{}\" is a route segment config and can only be used when the segment \
683                         is a Server Component module. Remove the \"use client\" directive to use \
684                         this API.",
685                        config_name
686                    )
687                    .into(),
688                    None,
689                    IssueSeverity::Error,
690                )
691                .await?;
692            }
693        }
694    }
695
696    Ok(config.cell())
697}
698
699async fn invalid_config(
700    source: ResolvedVc<Box<dyn Source>>,
701    key: &str,
702    span: Span,
703    error: RcStr,
704    value: Option<&JsValue<'_>>,
705    severity: IssueSeverity,
706) -> Result<()> {
707    let detail = if let Some(value) = value {
708        let (explainer, hints) = value.explain(2, 0);
709        Some(*StyledString::Text(format!("Got {explainer}.{hints}").into()).resolved_cell())
710    } else {
711        None
712    };
713
714    NextSegmentConfigParsingIssue::new(
715        source.ident(),
716        key.into(),
717        error,
718        detail,
719        IssueSource::from_swc_offsets(source, span.lo.to_u32(), span.hi.to_u32()),
720        severity,
721    )
722    .to_resolved()
723    .await?
724    .emit();
725    Ok(())
726}
727
728async fn parse_config_value(
729    source: ResolvedVc<Box<dyn Source>>,
730    mode: ParseSegmentMode,
731    config: &mut NextSegmentConfig,
732    legacy_config: &mut LegacySegmentConfig,
733    eval_context: &EvalContext,
734    arena: &ThreadLocal<Bump>,
735    key: Cow<'_, str>,
736    init: Option<Cow<'_, Expr>>,
737    span: Span,
738) -> Result<()> {
739    let get_value = || {
740        let init = init.as_deref();
741        // Unwrap typecasts such as `export const config = { .. } satisfies ProxyConfig`, usually
742        // this is already transpiled away, but we are looking at the original source here.
743        let init = match init {
744            Some(Expr::TsAs(TsAsExpr { expr, .. }))
745            | Some(Expr::TsTypeAssertion(TsTypeAssertion { expr, .. }))
746            | Some(Expr::TsConstAssertion(TsConstAssertion { expr, .. }))
747            | Some(Expr::TsSatisfies(TsSatisfiesExpr { expr, .. })) => Some(&**expr),
748            _ => init,
749        };
750        init.map(|init| eval_context.eval(arena.get_or_default(), init))
751            .map(|v| {
752                // Special case, as we don't call `link` here: assume that `undefined` is a free
753                // variable.
754                if let JsValue::FreeVar(name) = &v
755                    && name == "undefined"
756                {
757                    JsValue::Constant(ConstantValue::Undefined)
758                } else {
759                    v
760                }
761            })
762    };
763
764    match &*key {
765        "config" => {
766            let Some(value) = get_value() else {
767                return invalid_config(
768                    source,
769                    "config",
770                    span,
771                    rcstr!("It mustn't be reexported."),
772                    None,
773                    IssueSeverity::Error,
774                )
775                .await;
776            };
777
778            if mode == ParseSegmentMode::App {
779                return invalid_config(
780                    source,
781                    "config",
782                    span,
783                    rcstr!(
784                        "Page config in `config` is deprecated and ignored, use individual \
785                         exports instead."
786                    ),
787                    Some(&value),
788                    IssueSeverity::Warning,
789                )
790                .await;
791            }
792
793            let JsValue::Object { parts, .. } = &value else {
794                return invalid_config(
795                    source,
796                    "config",
797                    span,
798                    rcstr!("It needs to be a static object."),
799                    Some(&value),
800                    IssueSeverity::Error,
801                )
802                .await;
803            };
804
805            for part in parts {
806                let ObjectPart::KeyValue(key, value) = part else {
807                    return invalid_config(
808                        source,
809                        "config",
810                        span,
811                        rcstr!("It contains unsupported spread."),
812                        Some(&value),
813                        IssueSeverity::Error,
814                    )
815                    .await;
816                };
817
818                let Some(key) = key.as_str() else {
819                    return invalid_config(
820                        source,
821                        "config",
822                        span,
823                        rcstr!("It must only contain string keys."),
824                        Some(value),
825                        IssueSeverity::Error,
826                    )
827                    .await;
828                };
829
830                if matches!(value, JsValue::Constant(ConstantValue::Undefined)) {
831                    continue;
832                }
833                match key {
834                    "runtime" => {
835                        let Some(val) = value.as_str() else {
836                            return invalid_config(
837                                source,
838                                "config",
839                                span,
840                                rcstr!("`runtime` needs to be a static string."),
841                                Some(value),
842                                IssueSeverity::Error,
843                            )
844                            .await;
845                        };
846
847                        let runtime = match serde_json::from_value(Value::String(val.to_string())) {
848                            Ok(runtime) => Some(runtime),
849                            Err(err) => {
850                                return invalid_config(
851                                    source,
852                                    "config",
853                                    span,
854                                    format!("`runtime` has an invalid value: {err}.").into(),
855                                    Some(value),
856                                    IssueSeverity::Error,
857                                )
858                                .await;
859                            }
860                        };
861
862                        if mode == ParseSegmentMode::Proxy && runtime == Some(NextRuntime::Edge) {
863                            invalid_config(
864                                source,
865                                "config",
866                                span,
867                                rcstr!("Proxy does not support Edge runtime."),
868                                Some(value),
869                                IssueSeverity::Error,
870                            )
871                            .await?;
872                            continue;
873                        }
874
875                        legacy_config.runtime = runtime
876                    }
877                    "matcher" => {
878                        config.middleware_matcher =
879                            parse_route_matcher_from_js_value(source, span, value).await?;
880                    }
881                    "unstable_allowDynamic" => {
882                        config.unstable_allow_dynamic = parse_static_string_or_array_from_js_value(
883                            source,
884                            span,
885                            "config",
886                            "unstable_allowDynamic",
887                            value,
888                        )
889                        .await?;
890                    }
891                    "regions" => {
892                        config.preferred_region = parse_preferred_region_from_js_value(
893                            source, span, "config", "regions", value,
894                        )
895                        .await?;
896                    }
897                    "maxDuration" => {
898                        legacy_config.max_duration = match parse_max_duration(value) {
899                            Ok(duration) => duration,
900                            Err(error) => {
901                                return invalid_config(
902                                    source,
903                                    "config",
904                                    span,
905                                    format!("`maxDuration` {error}").into(),
906                                    Some(value),
907                                    IssueSeverity::Error,
908                                )
909                                .await;
910                            }
911                        };
912                    }
913                    _ => {
914                        // Ignore,
915                    }
916                }
917            }
918        }
919        "dynamic" => {
920            let Some(value) = get_value() else {
921                return invalid_config(
922                    source,
923                    "dynamic",
924                    span,
925                    rcstr!("It mustn't be reexported."),
926                    None,
927                    IssueSeverity::Error,
928                )
929                .await;
930            };
931            if matches!(value, JsValue::Constant(ConstantValue::Undefined)) {
932                return Ok(());
933            }
934            let Some(val) = value.as_str() else {
935                return invalid_config(
936                    source,
937                    "dynamic",
938                    span,
939                    rcstr!("It needs to be a static string."),
940                    Some(&value),
941                    IssueSeverity::Error,
942                )
943                .await;
944            };
945
946            config.dynamic = match serde_json::from_value(Value::String(val.to_string())) {
947                Ok(dynamic) => Some(dynamic),
948                Err(err) => {
949                    return invalid_config(
950                        source,
951                        "dynamic",
952                        span,
953                        format!("It has an invalid value: {err}.").into(),
954                        Some(&value),
955                        IssueSeverity::Error,
956                    )
957                    .await;
958                }
959            };
960        }
961        "dynamicParams" => {
962            let Some(value) = get_value() else {
963                return invalid_config(
964                    source,
965                    "dynamicParams",
966                    span,
967                    rcstr!("It mustn't be reexported."),
968                    None,
969                    IssueSeverity::Error,
970                )
971                .await;
972            };
973            if matches!(value, JsValue::Constant(ConstantValue::Undefined)) {
974                return Ok(());
975            }
976            let Some(val) = value.as_bool() else {
977                return invalid_config(
978                    source,
979                    "dynamicParams",
980                    span,
981                    rcstr!("It needs to be a static boolean."),
982                    Some(&value),
983                    IssueSeverity::Error,
984                )
985                .await;
986            };
987
988            config.dynamic_params = Some(val);
989        }
990        "revalidate" => {
991            let Some(value) = get_value() else {
992                return invalid_config(
993                    source,
994                    "revalidate",
995                    span,
996                    rcstr!("It mustn't be reexported."),
997                    None,
998                    IssueSeverity::Error,
999                )
1000                .await;
1001            };
1002
1003            match value {
1004                JsValue::Constant(ConstantValue::Num(ConstantNumber(val))) if val >= 0.0 => {
1005                    config.revalidate = Some(NextRevalidate::Frequency {
1006                        seconds: val as u32,
1007                    });
1008                }
1009                JsValue::Constant(ConstantValue::False) => {
1010                    config.revalidate = Some(NextRevalidate::Never);
1011                }
1012                JsValue::Constant(ConstantValue::Str(str)) if str.as_str() == "force-cache" => {
1013                    config.revalidate = Some(NextRevalidate::ForceCache);
1014                }
1015                _ => {
1016                    //noop; revalidate validation occurs in runtime at
1017                    //https://github.com/vercel/next.js/blob/cd46c221d2b7f796f963d2b81eea1e405023db23/packages/next/src/server/lib/patch-fetch.ts#L20
1018                }
1019            }
1020        }
1021        "maxDuration" => {
1022            let Some(value) = get_value() else {
1023                return invalid_config(
1024                    source,
1025                    "maxDuration",
1026                    span,
1027                    rcstr!("It mustn't be reexported."),
1028                    None,
1029                    IssueSeverity::Error,
1030                )
1031                .await;
1032            };
1033
1034            config.max_duration = match parse_max_duration(&value) {
1035                Ok(duration) => duration,
1036                Err(error) => {
1037                    return invalid_config(
1038                        source,
1039                        "maxDuration",
1040                        span,
1041                        format!("It {error}").into(),
1042                        Some(&value),
1043                        IssueSeverity::Error,
1044                    )
1045                    .await;
1046                }
1047            };
1048        }
1049        "fetchCache" => {
1050            let Some(value) = get_value() else {
1051                return invalid_config(
1052                    source,
1053                    "fetchCache",
1054                    span,
1055                    rcstr!("It mustn't be reexported."),
1056                    None,
1057                    IssueSeverity::Error,
1058                )
1059                .await;
1060            };
1061            if matches!(value, JsValue::Constant(ConstantValue::Undefined)) {
1062                return Ok(());
1063            }
1064            let Some(val) = value.as_str() else {
1065                return invalid_config(
1066                    source,
1067                    "fetchCache",
1068                    span,
1069                    rcstr!("It needs to be a static string."),
1070                    Some(&value),
1071                    IssueSeverity::Error,
1072                )
1073                .await;
1074            };
1075
1076            config.fetch_cache = match serde_json::from_value(Value::String(val.to_string())) {
1077                Ok(fetch_cache) => Some(fetch_cache),
1078                Err(err) => {
1079                    return invalid_config(
1080                        source,
1081                        "fetchCache",
1082                        span,
1083                        format!("It has an invalid value: {err}.").into(),
1084                        Some(&value),
1085                        IssueSeverity::Error,
1086                    )
1087                    .await;
1088                }
1089            };
1090        }
1091        "runtime" => {
1092            let Some(value) = get_value() else {
1093                return invalid_config(
1094                    source,
1095                    "runtime",
1096                    span,
1097                    rcstr!("It mustn't be reexported."),
1098                    None,
1099                    IssueSeverity::Error,
1100                )
1101                .await;
1102            };
1103            if matches!(value, JsValue::Constant(ConstantValue::Undefined)) {
1104                return Ok(());
1105            }
1106            let Some(val) = value.as_str() else {
1107                return invalid_config(
1108                    source,
1109                    "runtime",
1110                    span,
1111                    rcstr!("It needs to be a static string."),
1112                    Some(&value),
1113                    IssueSeverity::Error,
1114                )
1115                .await;
1116            };
1117
1118            config.runtime = match serde_json::from_value(Value::String(val.to_string())) {
1119                Ok(runtime) => Some(runtime),
1120                Err(err) => {
1121                    return invalid_config(
1122                        source,
1123                        "runtime",
1124                        span,
1125                        format!("It has an invalid value: {err}.").into(),
1126                        Some(&value),
1127                        IssueSeverity::Error,
1128                    )
1129                    .await;
1130                }
1131            };
1132        }
1133        "preferredRegion" => {
1134            let Some(value) = get_value() else {
1135                return invalid_config(
1136                    source,
1137                    "preferredRegion",
1138                    span,
1139                    rcstr!("It mustn't be reexported."),
1140                    None,
1141                    IssueSeverity::Error,
1142                )
1143                .await;
1144            };
1145            if matches!(value, JsValue::Constant(ConstantValue::Undefined)) {
1146                return Ok(());
1147            }
1148
1149            config.preferred_region = parse_preferred_region_from_js_value(
1150                source,
1151                span,
1152                "preferredRegion",
1153                "preferredRegion",
1154                &value,
1155            )
1156            .await?;
1157        }
1158        "generateImageMetadata" => {
1159            config.generate_image_metadata = true;
1160        }
1161        "generateSitemaps" => {
1162            config.generate_sitemaps = true;
1163        }
1164        "generateStaticParams" => {
1165            config.generate_static_params = Some(span);
1166        }
1167        "instant" => {
1168            config.instant = Some(span);
1169        }
1170        "prefetch" => {
1171            config.prefetch = Some(span);
1172        }
1173        "ensureStatic" => {
1174            config.ensure_static = Some(span);
1175        }
1176        _ => {}
1177    }
1178
1179    Ok(())
1180}
1181
1182fn parse_max_duration(value: &JsValue<'_>) -> Result<Option<u32>, &'static str> {
1183    if matches!(value, JsValue::Constant(ConstantValue::Undefined)) {
1184        return Ok(None);
1185    }
1186    let JsValue::Constant(ConstantValue::Num(ConstantNumber(val))) = value else {
1187        return Err("needs to be a static number.");
1188    };
1189    if !val.is_finite() || *val < 0.0 || val.fract() != 0.0 || *val > u32::MAX as f64 {
1190        return Err("needs to be a non-negative integer no greater than 4294967295.");
1191    }
1192    Ok(Some(*val as u32))
1193}
1194
1195async fn parse_preferred_region_from_js_value(
1196    source: ResolvedVc<Box<dyn Source>>,
1197    span: Span,
1198    key: &str,
1199    sub_key: &str,
1200    value: &JsValue<'_>,
1201) -> Result<Option<NextSegmentRegion>> {
1202    let is_array = matches!(value, JsValue::Array { .. });
1203    Ok(
1204        parse_static_string_or_array_from_js_value(source, span, key, sub_key, value)
1205            .await?
1206            .and_then(|regions| {
1207                if is_array {
1208                    Some(NextSegmentRegion::Multiple(regions))
1209                } else {
1210                    regions.into_iter().next().map(NextSegmentRegion::Single)
1211                }
1212            }),
1213    )
1214}
1215
1216async fn parse_static_string_or_array_from_js_value(
1217    source: ResolvedVc<Box<dyn Source>>,
1218    span: Span,
1219    key: &str,
1220    sub_key: &str,
1221    value: &JsValue<'_>,
1222) -> Result<Option<Vec<RcStr>>> {
1223    Ok(match value {
1224        // Single value is turned into a single-element Vec.
1225        JsValue::Constant(ConstantValue::Str(str)) => Some(vec![str.to_string().into()]),
1226        // Array of strings is turned into a Vec. If one of the values in not a String it
1227        // will error.
1228        JsValue::Array { items, .. } => {
1229            let mut result = Vec::new();
1230            for (i, item) in items.iter().enumerate() {
1231                if let Some(str) = item.as_str() {
1232                    result.push(str.to_string().into());
1233                } else {
1234                    invalid_config(
1235                        source,
1236                        key,
1237                        span,
1238                        format!(
1239                            "Entry `{sub_key}[{i}]` needs to be a static string or array of \
1240                             static strings."
1241                        )
1242                        .into(),
1243                        Some(item),
1244                        IssueSeverity::Error,
1245                    )
1246                    .await?;
1247                }
1248            }
1249            Some(result)
1250        }
1251        _ => {
1252            invalid_config(
1253                source,
1254                key,
1255                span,
1256                if sub_key != key {
1257                    format!("`{sub_key}` needs to be a static string or array of static strings.")
1258                        .into()
1259                } else {
1260                    rcstr!("It needs to be a static string or array of static strings.")
1261                },
1262                Some(value),
1263                IssueSeverity::Error,
1264            )
1265            .await?;
1266            return Ok(None);
1267        }
1268    })
1269}
1270
1271async fn parse_route_matcher_from_js_value(
1272    source: ResolvedVc<Box<dyn Source>>,
1273    span: Span,
1274    value: &JsValue<'_>,
1275) -> Result<Option<Vec<MiddlewareMatcherKind>>> {
1276    let parse_matcher_kind_matcher =
1277        async |value: &JsValue<'_>, sub_key: &str, matcher_idx: usize| {
1278            let mut route_has = vec![];
1279            if let JsValue::Array { items, .. } = value {
1280                for (i, item) in items.iter().enumerate() {
1281                    if let JsValue::Object { parts, .. } = item {
1282                        let mut route_type = None;
1283                        let mut route_key = None;
1284                        let mut route_value = None;
1285
1286                        for matcher_part in parts {
1287                            if let ObjectPart::KeyValue(part_key, part_value) = matcher_part {
1288                                match part_key.as_str() {
1289                                    Some("type") => {
1290                                        if let Some(part_value) = part_value.as_str().filter(|v| {
1291                                            *v == "header"
1292                                                || *v == "cookie"
1293                                                || *v == "query"
1294                                                || *v == "host"
1295                                        }) {
1296                                            route_type = Some(part_value);
1297                                        } else {
1298                                            invalid_config(
1299                                                source,
1300                                                "config",
1301                                                span,
1302                                                format!(
1303                                                    "`matcher[{matcher_idx}].{sub_key}[{i}].type` \
1304                                                     must be one of the strings: 'header', \
1305                                                     'cookie', 'query', 'host'"
1306                                                )
1307                                                .into(),
1308                                                Some(part_value),
1309                                                IssueSeverity::Error,
1310                                            )
1311                                            .await?;
1312                                        }
1313                                    }
1314                                    Some("key") => {
1315                                        if let Some(part_value) = part_value.as_str() {
1316                                            route_key = Some(part_value);
1317                                        } else {
1318                                            invalid_config(
1319                                                source,
1320                                                "config",
1321                                                span,
1322                                                format!(
1323                                                    "`matcher[{matcher_idx}].{sub_key}[{i}].key` \
1324                                                     must be a string"
1325                                                )
1326                                                .into(),
1327                                                Some(part_value),
1328                                                IssueSeverity::Error,
1329                                            )
1330                                            .await?;
1331                                        }
1332                                    }
1333                                    Some("value") => {
1334                                        if let Some(part_value) = part_value.as_str() {
1335                                            route_value = Some(part_value);
1336                                        } else {
1337                                            invalid_config(
1338                                                source,
1339                                                "config",
1340                                                span,
1341                                                format!(
1342                                                    "`matcher[{matcher_idx}].{sub_key}[{i}].\
1343                                                     value` must be a string"
1344                                                )
1345                                                .into(),
1346                                                Some(part_value),
1347                                                IssueSeverity::Error,
1348                                            )
1349                                            .await?;
1350                                        }
1351                                    }
1352                                    _ => {
1353                                        invalid_config(
1354                                            source,
1355                                            "config",
1356                                            span,
1357                                            format!(
1358                                                "Unexpected property in \
1359                                                 `matcher[{matcher_idx}].{sub_key}[{i}]` object"
1360                                            )
1361                                            .into(),
1362                                            Some(part_key),
1363                                            IssueSeverity::Error,
1364                                        )
1365                                        .await?;
1366                                    }
1367                                }
1368                            }
1369                        }
1370                        let r = match route_type {
1371                            Some("header") => route_key.map(|route_key| RouteHas::Header {
1372                                key: route_key.into(),
1373                                value: route_value.map(From::from),
1374                            }),
1375                            Some("cookie") => route_key.map(|route_key| RouteHas::Cookie {
1376                                key: route_key.into(),
1377                                value: route_value.map(From::from),
1378                            }),
1379                            Some("query") => route_key.map(|route_key| RouteHas::Query {
1380                                key: route_key.into(),
1381                                value: route_value.map(From::from),
1382                            }),
1383                            Some("host") => route_value.map(|route_value| RouteHas::Host {
1384                                value: route_value.into(),
1385                            }),
1386                            _ => None,
1387                        };
1388
1389                        if let Some(r) = r {
1390                            route_has.push(r);
1391                        }
1392                    }
1393                }
1394            }
1395
1396            anyhow::Ok(route_has)
1397        };
1398
1399    let mut matchers = vec![];
1400
1401    match value {
1402        JsValue::Constant(ConstantValue::Str(matcher)) => {
1403            matchers.push(MiddlewareMatcherKind::Str(matcher.to_string()));
1404        }
1405        JsValue::Array { items, .. } => {
1406            for (i, item) in items.iter().enumerate() {
1407                if let Some(matcher) = item.as_str() {
1408                    matchers.push(MiddlewareMatcherKind::Str(matcher.to_string()));
1409                } else if let JsValue::Object { parts, .. } = item {
1410                    let mut matcher = ProxyMatcher::default();
1411                    let mut had_source = false;
1412                    for matcher_part in parts {
1413                        if let ObjectPart::KeyValue(key, value) = matcher_part {
1414                            match key.as_str() {
1415                                Some("source") => {
1416                                    if let Some(value) = value.as_str() {
1417                                        // TODO the actual validation would be:
1418                                        // - starts with /
1419                                        // - at most 4096 chars
1420                                        // - can be parsed with `path-to-regexp`
1421                                        had_source = true;
1422                                        matcher.original_source = value.into();
1423                                    } else {
1424                                        invalid_config(
1425                                            source,
1426                                            "config",
1427                                            span,
1428                                            format!(
1429                                                "`source` in `matcher[{i}]` object must be a \
1430                                                 string"
1431                                            )
1432                                            .into(),
1433                                            Some(value),
1434                                            IssueSeverity::Error,
1435                                        )
1436                                        .await?;
1437                                    }
1438                                }
1439                                Some("locale") => {
1440                                    if let Some(value) = value.as_bool()
1441                                        && !value
1442                                    {
1443                                        matcher.locale = false;
1444                                    } else if matches!(
1445                                        value,
1446                                        JsValue::Constant(ConstantValue::Undefined)
1447                                    ) {
1448                                        // ignore
1449                                    } else {
1450                                        invalid_config(
1451                                            source,
1452                                            "config",
1453                                            span,
1454                                            format!(
1455                                                "`locale` in `matcher[{i}]` object must be false \
1456                                                 or undefined"
1457                                            )
1458                                            .into(),
1459                                            Some(value),
1460                                            IssueSeverity::Error,
1461                                        )
1462                                        .await?;
1463                                    }
1464                                }
1465                                Some("missing") => {
1466                                    matcher.missing =
1467                                        Some(parse_matcher_kind_matcher(value, "missing", i).await?)
1468                                }
1469                                Some("has") => {
1470                                    matcher.has =
1471                                        Some(parse_matcher_kind_matcher(value, "has", i).await?)
1472                                }
1473                                Some("regexp") => {
1474                                    // ignored for now
1475                                }
1476                                _ => {
1477                                    invalid_config(
1478                                        source,
1479                                        "config",
1480                                        span,
1481                                        format!("Unexpected property in `matcher[{i}]` object")
1482                                            .into(),
1483                                        Some(key),
1484                                        IssueSeverity::Error,
1485                                    )
1486                                    .await?;
1487                                }
1488                            }
1489                        }
1490                    }
1491                    if !had_source {
1492                        invalid_config(
1493                            source,
1494                            "config",
1495                            span,
1496                            format!("Missing `source` in `matcher[{i}]` object").into(),
1497                            Some(value),
1498                            IssueSeverity::Error,
1499                        )
1500                        .await?;
1501                    }
1502
1503                    matchers.push(MiddlewareMatcherKind::Matcher(matcher));
1504                } else {
1505                    invalid_config(
1506                        source,
1507                        "config",
1508                        span,
1509                        format!(
1510                            "Entry `matcher[{i}]` need to be static strings or static objects."
1511                        )
1512                        .into(),
1513                        Some(value),
1514                        IssueSeverity::Error,
1515                    )
1516                    .await?;
1517                }
1518            }
1519        }
1520        _ => {
1521            invalid_config(
1522                source,
1523                "config",
1524                span,
1525                rcstr!(
1526                    "`matcher` needs to be a static string or array of static strings or array of \
1527                     static objects."
1528                ),
1529                Some(value),
1530                IssueSeverity::Error,
1531            )
1532            .await?
1533        }
1534    }
1535
1536    Ok(if matchers.is_empty() {
1537        None
1538    } else {
1539        Some(matchers)
1540    })
1541}
1542
1543/// A wrapper around [`parse_segment_config_from_source`] that merges route segment configuration
1544/// information from all relevant files (page, layout, parallel routes, etc).
1545#[turbo_tasks::function]
1546pub async fn parse_segment_config_from_loader_tree(
1547    loader_tree: Vc<AppPageLoaderTree>,
1548) -> Result<Vc<NextSegmentConfig>> {
1549    let loader_tree = &*loader_tree.await?;
1550
1551    Ok(parse_segment_config_from_loader_tree_internal(loader_tree)
1552        .await?
1553        .cell())
1554}
1555
1556async fn parse_segment_config_from_loader_tree_internal(
1557    loader_tree: &AppPageLoaderTree,
1558) -> Result<NextSegmentConfig> {
1559    let mut config = NextSegmentConfig::default();
1560
1561    let parallel_configs = loader_tree
1562        .parallel_routes
1563        .values()
1564        .map(|loader_tree| Box::pin(parse_segment_config_from_loader_tree_internal(loader_tree)))
1565        .join()
1566        .await;
1567
1568    for tree in parallel_configs {
1569        config.apply_parallel_config(&tree?)?;
1570    }
1571
1572    let modules = &loader_tree.modules;
1573    for path in [
1574        modules.page.clone(),
1575        modules.default.clone(),
1576        modules.layout.clone(),
1577    ]
1578    .into_iter()
1579    .flatten()
1580    {
1581        let source = Vc::upcast(FileSource::new(path.clone()));
1582        config.apply_parent_config(
1583            &*parse_segment_config_from_source(source, ParseSegmentMode::App).await?,
1584        );
1585    }
1586
1587    Ok(config)
1588}