next_core/
segment_config.rs

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