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