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