Skip to main content

next_core/
segment_config.rs

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