Skip to main content

next_core/
segment_config.rs

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