1use std::borrow::Cow;
2
3use anyhow::{Result, bail};
4use async_trait::async_trait;
5use bincode::{Decode, Encode};
6use serde::{Deserialize, Serialize};
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::{JoinIterExt, NonLocalValue, ResolvedVc, ValueDefault, Vc, util::WrapFuture};
21use turbo_tasks_fs::FileSystemPath;
22use turbopack_core::{
23 file_source::FileSource,
24 ident::AssetIdent,
25 issue::{Issue, IssueExt, IssueSeverity, IssueSource, IssueStage, StyledString},
26 source::Source,
27};
28use turbopack_ecmascript::{
29 EcmascriptInputTransforms, EcmascriptModuleAssetType,
30 analyzer::{
31 Bump, ConstantNumber, ConstantValue, JsValue, ObjectPart, ThreadLocal, graph::EvalContext,
32 },
33 parse::{ParseResult, parse},
34};
35
36use crate::{
37 app_structure::AppPageLoaderTree,
38 next_config::RouteHas,
39 next_manifests::ProxyMatcher,
40 util::{MiddlewareMatcherKind, NextRuntime},
41};
42
43#[derive(
44 Default, PartialEq, Eq, Clone, Copy, Debug, Deserialize, NonLocalValue, Encode, Decode,
45)]
46#[serde(rename_all = "kebab-case")]
47pub enum NextSegmentDynamic {
48 #[default]
49 Auto,
50 ForceDynamic,
51 Error,
52 ForceStatic,
53}
54
55#[derive(
56 Default, PartialEq, Eq, Clone, Copy, Debug, Deserialize, NonLocalValue, Encode, Decode,
57)]
58#[serde(rename_all = "kebab-case")]
59pub enum NextSegmentFetchCache {
60 #[default]
61 Auto,
62 DefaultCache,
63 OnlyCache,
64 ForceCache,
65 DefaultNoStore,
66 OnlyNoStore,
67 ForceNoStore,
68}
69
70#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, NonLocalValue, Encode, Decode)]
71pub enum NextRevalidate {
72 #[default]
73 Never,
74 ForceCache,
75 Frequency {
76 seconds: u32,
77 },
78}
79
80#[derive(PartialEq, Eq, Clone, Debug, NonLocalValue, Encode, Decode)]
81pub enum NextSegmentRegion {
82 Single(RcStr),
83 Multiple(Vec<RcStr>),
84}
85
86#[derive(PartialEq, Eq, Clone, Copy, Debug, NonLocalValue, Encode, Decode, Serialize)]
87#[serde(rename_all = "lowercase")]
88pub enum NextSegmentRsc {
89 Server,
90 Client,
91}
92
93impl NextSegmentRegion {
94 pub fn to_vec(&self) -> Vec<RcStr> {
95 match self {
96 Self::Single(region) => vec![region.clone()],
97 Self::Multiple(regions) => regions.clone(),
98 }
99 }
100}
101
102#[turbo_tasks::value(shared)]
103#[derive(Debug, Default, Clone)]
104pub struct NextSegmentConfig {
105 pub rsc: Option<NextSegmentRsc>,
106 pub dynamic: Option<NextSegmentDynamic>,
107 pub dynamic_params: Option<bool>,
108 pub revalidate: Option<NextRevalidate>,
109 pub max_duration: Option<u32>,
110 pub fetch_cache: Option<NextSegmentFetchCache>,
111 pub runtime: Option<NextRuntime>,
112 pub preferred_region: Option<NextSegmentRegion>,
113 pub middleware_matcher: Option<Vec<MiddlewareMatcherKind>>,
114 pub unstable_allow_dynamic: Option<Vec<RcStr>>,
115
116 pub generate_image_metadata: bool,
118 pub generate_sitemaps: bool,
119 #[turbo_tasks(unsafe_ignore)]
120 #[bincode(with_serde)]
121 pub generate_static_params: Option<Span>,
122 #[turbo_tasks(unsafe_ignore)]
123 #[bincode(with_serde)]
124 pub instant: Option<Span>,
125 #[turbo_tasks(unsafe_ignore)]
126 #[bincode(with_serde)]
127 pub prefetch: Option<Span>,
128 #[turbo_tasks(unsafe_ignore)]
129 #[bincode(with_serde)]
130 pub ensure_static: Option<Span>,
131}
132
133#[derive(Default)]
135struct LegacySegmentConfig {
136 runtime: Option<NextRuntime>,
137 max_duration: Option<u32>,
138}
139
140#[turbo_tasks::value_impl]
141impl ValueDefault for NextSegmentConfig {
142 #[turbo_tasks::function]
143 pub fn value_default() -> Vc<Self> {
144 NextSegmentConfig::default().cell()
145 }
146}
147
148impl NextSegmentConfig {
149 pub fn apply_parent_config(&mut self, parent: &Self) {
152 let NextSegmentConfig {
153 rsc: _,
155 dynamic,
156 dynamic_params,
157 revalidate,
158 max_duration,
159 fetch_cache,
160 runtime,
161 preferred_region,
162 instant: _,
164 prefetch: _,
165 ensure_static: _,
166 middleware_matcher: _,
168 unstable_allow_dynamic: _,
169 generate_image_metadata: _,
170 generate_sitemaps: _,
171 generate_static_params: _,
172 } = self;
173 *dynamic = dynamic.or(parent.dynamic);
174 *dynamic_params = dynamic_params.or(parent.dynamic_params);
175 *revalidate = revalidate.or(parent.revalidate);
176 *max_duration = max_duration.or(parent.max_duration);
177 *fetch_cache = fetch_cache.or(parent.fetch_cache);
178 *runtime = runtime.or(parent.runtime);
179 *preferred_region = preferred_region.take().or(parent.preferred_region.clone());
180 }
181
182 pub fn apply_parallel_config(&mut self, parallel_config: &Self) -> Result<()> {
185 fn merge_parallel<T: PartialEq + Clone>(
186 a: &mut Option<T>,
187 b: &Option<T>,
188 name: &str,
189 ) -> Result<()> {
190 match (a.as_ref(), b) {
191 (Some(a), Some(b)) if *a != *b => {
192 bail!(
193 "Sibling segment configs have conflicting values for {}",
194 name
195 )
196 }
197 (None, Some(b)) => {
198 *a = Some(b.clone());
199 }
200 _ => {}
201 }
202 Ok(())
203 }
204 let Self {
205 rsc,
206 dynamic,
207 dynamic_params,
208 revalidate,
209 max_duration,
210 fetch_cache,
211 runtime,
212 preferred_region,
213 instant: _,
215 prefetch: _,
216 ensure_static: _,
217 middleware_matcher: _,
219 unstable_allow_dynamic: _,
220 generate_image_metadata: _,
221 generate_sitemaps: _,
222 generate_static_params: _,
223 } = self;
224 *rsc = (*rsc).or(parallel_config.rsc);
227 merge_parallel(dynamic, ¶llel_config.dynamic, "dynamic")?;
228 merge_parallel(
229 dynamic_params,
230 ¶llel_config.dynamic_params,
231 "dynamicParams",
232 )?;
233 merge_parallel(revalidate, ¶llel_config.revalidate, "revalidate")?;
234 merge_parallel(max_duration, ¶llel_config.max_duration, "maxDuration")?;
235 merge_parallel(fetch_cache, ¶llel_config.fetch_cache, "fetchCache")?;
236 merge_parallel(runtime, ¶llel_config.runtime, "runtime")?;
237 match (preferred_region.as_ref(), ¶llel_config.preferred_region) {
238 (Some(a), Some(b)) if a.to_vec() != b.to_vec() => {
239 bail!("Sibling segment configs have conflicting values for preferredRegion")
240 }
241 (None, Some(b)) => {
242 *preferred_region = Some(b.clone());
243 }
244 _ => {}
245 }
246 Ok(())
247 }
248
249 pub fn get_proxy_matchers(
250 &self,
251 has_i18n: bool,
252 has_i18n_locales: bool,
253 base_path: Option<&str>,
254 ) -> Option<Vec<ProxyMatcher>> {
255 self.middleware_matcher.as_ref().map(|matchers| {
256 matchers
257 .iter()
258 .map(|matcher| {
259 let mut matcher = match matcher {
260 MiddlewareMatcherKind::Str(matcher) => ProxyMatcher {
261 original_source: matcher.as_str().into(),
262 ..Default::default()
263 },
264 MiddlewareMatcherKind::Matcher(matcher) => matcher.clone(),
265 };
266
267 let mut source = matcher.original_source.to_string();
269 let is_root = source == "/";
270 let has_locale = matcher.locale;
271
272 if has_i18n_locales && has_locale {
273 if is_root {
274 source.clear();
275 }
276 source.insert_str(0, "/:nextInternalLocale((?!_next/)[^/.]{1,})");
277 }
278
279 if is_root {
284 source.push('(');
285 if has_i18n {
286 source.push_str("|\\.json|");
287 }
288 source.push_str("/?index|/?index\\.json|");
289 source.push_str("/?index(?:\\.rsc|\\.segments/.+\\.segment\\.rsc)");
290 source.push_str(")?");
291 } else {
292 source.push_str("{(\\.json|\\.rsc|\\.segments/.+\\.segment\\.rsc)}?");
293 };
294
295 source.insert_str(0, "/:nextData(_next/data/[^/]{1,})?");
296
297 if let Some(base_path) = base_path.as_ref() {
298 source.insert_str(0, base_path);
299 }
300
301 matcher.regexp = Some(RcStr::from(source));
306
307 matcher
308 })
309 .collect()
310 })
311 }
312}
313
314#[turbo_tasks::value(shared)]
316pub struct NextSegmentConfigParsingIssue {
317 ident: ResolvedVc<AssetIdent>,
318 key: RcStr,
319 error: RcStr,
320 detail: Option<ResolvedVc<StyledString>>,
321 source: IssueSource,
322 severity: IssueSeverity,
323}
324
325#[turbo_tasks::value_impl]
326impl NextSegmentConfigParsingIssue {
327 #[turbo_tasks::function]
328 pub fn new(
329 ident: ResolvedVc<AssetIdent>,
330 key: RcStr,
331 error: RcStr,
332 detail: Option<ResolvedVc<StyledString>>,
333 source: IssueSource,
334 severity: IssueSeverity,
335 ) -> Vc<Self> {
336 Self {
337 ident,
338 key,
339 error,
340 detail,
341 source,
342 severity,
343 }
344 .cell()
345 }
346}
347
348#[async_trait]
349#[turbo_tasks::value_impl]
350impl Issue for NextSegmentConfigParsingIssue {
351 fn severity(&self) -> IssueSeverity {
352 self.severity
353 }
354
355 async fn title(&self) -> Result<StyledString> {
356 Ok(StyledString::Line(vec![
357 StyledString::Text(
358 format!(
359 "Next.js can't recognize the exported `{}` field in route. ",
360 self.key,
361 )
362 .into(),
363 ),
364 StyledString::Text(self.error.clone()),
365 ]))
366 }
367
368 fn stage(&self) -> IssueStage {
369 IssueStage::Parse
370 }
371
372 async fn file_path(&self) -> Result<FileSystemPath> {
373 Ok(self.ident.await?.path.clone())
374 }
375
376 async fn description(&self) -> Result<Option<StyledString>> {
377 Ok(Some(StyledString::Text(rcstr!(
378 "The exported configuration object in a source file needs to have a very specific \
379 format from which some properties can be statically parsed at compiled-time."
380 ))))
381 }
382
383 async fn detail(&self) -> Result<Option<StyledString>> {
384 match self.detail {
385 Some(d) => Ok(Some((*d.await?).clone())),
386 None => Ok(None),
387 }
388 }
389
390 fn documentation_link(&self) -> RcStr {
391 rcstr!("https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config")
392 }
393
394 fn source(&self) -> Option<IssueSource> {
395 Some(self.source)
396 }
397}
398
399#[turbo_tasks::task_input]
400#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Encode, Decode)]
401pub enum ParseSegmentMode {
402 Base,
403 App,
405 Proxy,
407}
408
409#[turbo_tasks::function]
471pub async fn parse_segment_config_from_source(
472 source: ResolvedVc<Box<dyn Source>>,
473 mode: ParseSegmentMode,
474) -> Result<Vc<NextSegmentConfig>> {
475 let ident = source.ident().await?;
476 let path = &ident.path;
477
478 if path.path.ends_with(".d.ts")
481 || !(path.path.ends_with(".js")
482 || path.path.ends_with(".jsx")
483 || path.path.ends_with(".ts")
484 || path.path.ends_with(".tsx"))
485 {
486 return Ok(Default::default());
487 }
488
489 let result = &*parse(
490 *source,
491 if path.path.ends_with(".ts") {
492 EcmascriptModuleAssetType::Typescript {
493 tsx: false,
494 analyze_types: false,
495 }
496 } else if path.path.ends_with(".tsx") {
497 EcmascriptModuleAssetType::Typescript {
498 tsx: true,
499 analyze_types: false,
500 }
501 } else {
502 EcmascriptModuleAssetType::Ecmascript
503 },
504 EcmascriptInputTransforms::empty(),
505 rcstr!("development"),
508 false,
509 false,
510 )
511 .await?;
512
513 let ParseResult::Ok {
514 program: Program::Module(module_ast),
515 eval_context,
516 globals,
517 ..
518 } = result
519 else {
520 return Ok(Default::default());
522 };
523
524 let arena = ThreadLocal::new();
527 let mut config = WrapFuture::new(
528 async {
529 let mut config = NextSegmentConfig::default();
530 let mut legacy_config = LegacySegmentConfig::default();
531
532 let mut parse = async |ident, init, span| {
533 parse_config_value(
534 source,
535 mode,
536 &mut config,
537 &mut legacy_config,
538 eval_context,
539 &arena,
540 ident,
541 init,
542 span,
543 )
544 .await
545 };
546
547 for item in &module_ast.body {
548 match item {
549 ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(decl)) => match &decl.decl {
550 Decl::Class(decl) => {
551 parse(
552 Cow::Borrowed(decl.ident.sym.as_str()),
553 Some(Cow::Owned(Expr::Class(ClassExpr {
554 ident: None,
555 class: decl.class.clone(),
556 }))),
557 decl.span(),
558 )
559 .await?
560 }
561 Decl::Fn(decl) => {
562 parse(
563 Cow::Borrowed(decl.ident.sym.as_str()),
564 Some(Cow::Owned(Expr::Fn(FnExpr {
565 ident: None,
566 function: decl.function.clone(),
567 }))),
568 decl.span(),
569 )
570 .await?
571 }
572 Decl::Var(decl) => {
573 for decl in &decl.decls {
574 let Some(ident) = decl.name.as_ident() else {
575 continue;
576 };
577
578 let key = &ident.id.sym;
579
580 parse(
581 Cow::Borrowed(key.as_str()),
582 Some(
583 decl.init.as_deref().map(Cow::Borrowed).unwrap_or_else(
584 || Cow::Owned(*Expr::undefined(DUMMY_SP)),
585 ),
586 ),
587 if key == "config" {
590 ident.id.span
591 } else {
592 decl.span()
593 },
594 )
595 .await?;
596 }
597 }
598 _ => continue,
599 },
600 ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(named)) => {
601 for specifier in &named.specifiers {
602 if let ExportSpecifier::Named(named) = specifier {
603 parse(
604 match named.exported.as_ref().unwrap_or(&named.orig) {
605 ModuleExportName::Ident(ident) => {
606 Cow::Borrowed(ident.sym.as_str())
607 }
608 ModuleExportName::Str(s) => s.value.to_string_lossy(),
609 },
610 None,
611 specifier.span(),
612 )
613 .await?;
614 }
615 }
616 }
617 _ => {
618 continue;
619 }
620 }
621 }
622 config.runtime = config.runtime.or(legacy_config.runtime);
625 config.max_duration = config.max_duration.or(legacy_config.max_duration);
626 anyhow::Ok(config)
627 },
628 |f, ctx| GLOBALS.set(globals, || f.poll(ctx)),
629 )
630 .await?;
631
632 let is_client_entry = module_ast
633 .body
634 .iter()
635 .take_while(|i| match i {
636 ModuleItem::Stmt(stmt) => stmt.directive_continue(),
637 ModuleItem::ModuleDecl(_) => false,
638 })
639 .filter_map(|i| i.as_stmt())
640 .any(|f| match f {
641 Stmt::Expr(ExprStmt { expr, .. }) => match &**expr {
642 Expr::Lit(Lit::Str(Str { value, .. })) => value == "use client",
643 _ => false,
644 },
645 _ => false,
646 });
647
648 config.rsc = Some(if is_client_entry {
649 NextSegmentRsc::Client
650 } else {
651 NextSegmentRsc::Server
652 });
653
654 if mode == ParseSegmentMode::App && is_client_entry {
655 if let Some(span) = config.generate_static_params {
656 invalid_config(
657 source,
658 "generateStaticParams",
659 span,
660 rcstr!(
661 "App pages cannot use both \"use client\" and export function \
662 \"generateStaticParams()\"."
663 ),
664 None,
665 IssueSeverity::Error,
666 )
667 .await?;
668 }
669
670 let server_only_route_configs = [
671 ("instant", config.instant),
672 ("prefetch", config.prefetch),
673 ("ensureStatic", config.ensure_static),
674 ];
675 for (config_name, usage) in server_only_route_configs {
676 if let Some(span) = usage {
677 invalid_config(
678 source,
679 config_name,
680 span,
681 format!(
682 "\"{}\" is a route segment config and can only be used when the segment \
683 is a Server Component module. Remove the \"use client\" directive to use \
684 this API.",
685 config_name
686 )
687 .into(),
688 None,
689 IssueSeverity::Error,
690 )
691 .await?;
692 }
693 }
694 }
695
696 Ok(config.cell())
697}
698
699async fn invalid_config(
700 source: ResolvedVc<Box<dyn Source>>,
701 key: &str,
702 span: Span,
703 error: RcStr,
704 value: Option<&JsValue<'_>>,
705 severity: IssueSeverity,
706) -> Result<()> {
707 let detail = if let Some(value) = value {
708 let (explainer, hints) = value.explain(2, 0);
709 Some(*StyledString::Text(format!("Got {explainer}.{hints}").into()).resolved_cell())
710 } else {
711 None
712 };
713
714 NextSegmentConfigParsingIssue::new(
715 source.ident(),
716 key.into(),
717 error,
718 detail,
719 IssueSource::from_swc_offsets(source, span.lo.to_u32(), span.hi.to_u32()),
720 severity,
721 )
722 .to_resolved()
723 .await?
724 .emit();
725 Ok(())
726}
727
728async fn parse_config_value(
729 source: ResolvedVc<Box<dyn Source>>,
730 mode: ParseSegmentMode,
731 config: &mut NextSegmentConfig,
732 legacy_config: &mut LegacySegmentConfig,
733 eval_context: &EvalContext,
734 arena: &ThreadLocal<Bump>,
735 key: Cow<'_, str>,
736 init: Option<Cow<'_, Expr>>,
737 span: Span,
738) -> Result<()> {
739 let get_value = || {
740 let init = init.as_deref();
741 let init = match init {
744 Some(Expr::TsAs(TsAsExpr { expr, .. }))
745 | Some(Expr::TsTypeAssertion(TsTypeAssertion { expr, .. }))
746 | Some(Expr::TsConstAssertion(TsConstAssertion { expr, .. }))
747 | Some(Expr::TsSatisfies(TsSatisfiesExpr { expr, .. })) => Some(&**expr),
748 _ => init,
749 };
750 init.map(|init| eval_context.eval(arena.get_or_default(), init))
751 .map(|v| {
752 if let JsValue::FreeVar(name) = &v
755 && name == "undefined"
756 {
757 JsValue::Constant(ConstantValue::Undefined)
758 } else {
759 v
760 }
761 })
762 };
763
764 match &*key {
765 "config" => {
766 let Some(value) = get_value() else {
767 return invalid_config(
768 source,
769 "config",
770 span,
771 rcstr!("It mustn't be reexported."),
772 None,
773 IssueSeverity::Error,
774 )
775 .await;
776 };
777
778 if mode == ParseSegmentMode::App {
779 return invalid_config(
780 source,
781 "config",
782 span,
783 rcstr!(
784 "Page config in `config` is deprecated and ignored, use individual \
785 exports instead."
786 ),
787 Some(&value),
788 IssueSeverity::Warning,
789 )
790 .await;
791 }
792
793 let JsValue::Object { parts, .. } = &value else {
794 return invalid_config(
795 source,
796 "config",
797 span,
798 rcstr!("It needs to be a static object."),
799 Some(&value),
800 IssueSeverity::Error,
801 )
802 .await;
803 };
804
805 for part in parts {
806 let ObjectPart::KeyValue(key, value) = part else {
807 return invalid_config(
808 source,
809 "config",
810 span,
811 rcstr!("It contains unsupported spread."),
812 Some(&value),
813 IssueSeverity::Error,
814 )
815 .await;
816 };
817
818 let Some(key) = key.as_str() else {
819 return invalid_config(
820 source,
821 "config",
822 span,
823 rcstr!("It must only contain string keys."),
824 Some(value),
825 IssueSeverity::Error,
826 )
827 .await;
828 };
829
830 if matches!(value, JsValue::Constant(ConstantValue::Undefined)) {
831 continue;
832 }
833 match key {
834 "runtime" => {
835 let Some(val) = value.as_str() else {
836 return invalid_config(
837 source,
838 "config",
839 span,
840 rcstr!("`runtime` needs to be a static string."),
841 Some(value),
842 IssueSeverity::Error,
843 )
844 .await;
845 };
846
847 let runtime = match serde_json::from_value(Value::String(val.to_string())) {
848 Ok(runtime) => Some(runtime),
849 Err(err) => {
850 return invalid_config(
851 source,
852 "config",
853 span,
854 format!("`runtime` has an invalid value: {err}.").into(),
855 Some(value),
856 IssueSeverity::Error,
857 )
858 .await;
859 }
860 };
861
862 if mode == ParseSegmentMode::Proxy && runtime == Some(NextRuntime::Edge) {
863 invalid_config(
864 source,
865 "config",
866 span,
867 rcstr!("Proxy does not support Edge runtime."),
868 Some(value),
869 IssueSeverity::Error,
870 )
871 .await?;
872 continue;
873 }
874
875 legacy_config.runtime = runtime
876 }
877 "matcher" => {
878 config.middleware_matcher =
879 parse_route_matcher_from_js_value(source, span, value).await?;
880 }
881 "unstable_allowDynamic" => {
882 config.unstable_allow_dynamic = parse_static_string_or_array_from_js_value(
883 source,
884 span,
885 "config",
886 "unstable_allowDynamic",
887 value,
888 )
889 .await?;
890 }
891 "regions" => {
892 config.preferred_region = parse_preferred_region_from_js_value(
893 source, span, "config", "regions", value,
894 )
895 .await?;
896 }
897 "maxDuration" => {
898 legacy_config.max_duration = match parse_max_duration(value) {
899 Ok(duration) => duration,
900 Err(error) => {
901 return invalid_config(
902 source,
903 "config",
904 span,
905 format!("`maxDuration` {error}").into(),
906 Some(value),
907 IssueSeverity::Error,
908 )
909 .await;
910 }
911 };
912 }
913 _ => {
914 }
916 }
917 }
918 }
919 "dynamic" => {
920 let Some(value) = get_value() else {
921 return invalid_config(
922 source,
923 "dynamic",
924 span,
925 rcstr!("It mustn't be reexported."),
926 None,
927 IssueSeverity::Error,
928 )
929 .await;
930 };
931 if matches!(value, JsValue::Constant(ConstantValue::Undefined)) {
932 return Ok(());
933 }
934 let Some(val) = value.as_str() else {
935 return invalid_config(
936 source,
937 "dynamic",
938 span,
939 rcstr!("It needs to be a static string."),
940 Some(&value),
941 IssueSeverity::Error,
942 )
943 .await;
944 };
945
946 config.dynamic = match serde_json::from_value(Value::String(val.to_string())) {
947 Ok(dynamic) => Some(dynamic),
948 Err(err) => {
949 return invalid_config(
950 source,
951 "dynamic",
952 span,
953 format!("It has an invalid value: {err}.").into(),
954 Some(&value),
955 IssueSeverity::Error,
956 )
957 .await;
958 }
959 };
960 }
961 "dynamicParams" => {
962 let Some(value) = get_value() else {
963 return invalid_config(
964 source,
965 "dynamicParams",
966 span,
967 rcstr!("It mustn't be reexported."),
968 None,
969 IssueSeverity::Error,
970 )
971 .await;
972 };
973 if matches!(value, JsValue::Constant(ConstantValue::Undefined)) {
974 return Ok(());
975 }
976 let Some(val) = value.as_bool() else {
977 return invalid_config(
978 source,
979 "dynamicParams",
980 span,
981 rcstr!("It needs to be a static boolean."),
982 Some(&value),
983 IssueSeverity::Error,
984 )
985 .await;
986 };
987
988 config.dynamic_params = Some(val);
989 }
990 "revalidate" => {
991 let Some(value) = get_value() else {
992 return invalid_config(
993 source,
994 "revalidate",
995 span,
996 rcstr!("It mustn't be reexported."),
997 None,
998 IssueSeverity::Error,
999 )
1000 .await;
1001 };
1002
1003 match value {
1004 JsValue::Constant(ConstantValue::Num(ConstantNumber(val))) if val >= 0.0 => {
1005 config.revalidate = Some(NextRevalidate::Frequency {
1006 seconds: val as u32,
1007 });
1008 }
1009 JsValue::Constant(ConstantValue::False) => {
1010 config.revalidate = Some(NextRevalidate::Never);
1011 }
1012 JsValue::Constant(ConstantValue::Str(str)) if str.as_str() == "force-cache" => {
1013 config.revalidate = Some(NextRevalidate::ForceCache);
1014 }
1015 _ => {
1016 }
1019 }
1020 }
1021 "maxDuration" => {
1022 let Some(value) = get_value() else {
1023 return invalid_config(
1024 source,
1025 "maxDuration",
1026 span,
1027 rcstr!("It mustn't be reexported."),
1028 None,
1029 IssueSeverity::Error,
1030 )
1031 .await;
1032 };
1033
1034 config.max_duration = match parse_max_duration(&value) {
1035 Ok(duration) => duration,
1036 Err(error) => {
1037 return invalid_config(
1038 source,
1039 "maxDuration",
1040 span,
1041 format!("It {error}").into(),
1042 Some(&value),
1043 IssueSeverity::Error,
1044 )
1045 .await;
1046 }
1047 };
1048 }
1049 "fetchCache" => {
1050 let Some(value) = get_value() else {
1051 return invalid_config(
1052 source,
1053 "fetchCache",
1054 span,
1055 rcstr!("It mustn't be reexported."),
1056 None,
1057 IssueSeverity::Error,
1058 )
1059 .await;
1060 };
1061 if matches!(value, JsValue::Constant(ConstantValue::Undefined)) {
1062 return Ok(());
1063 }
1064 let Some(val) = value.as_str() else {
1065 return invalid_config(
1066 source,
1067 "fetchCache",
1068 span,
1069 rcstr!("It needs to be a static string."),
1070 Some(&value),
1071 IssueSeverity::Error,
1072 )
1073 .await;
1074 };
1075
1076 config.fetch_cache = match serde_json::from_value(Value::String(val.to_string())) {
1077 Ok(fetch_cache) => Some(fetch_cache),
1078 Err(err) => {
1079 return invalid_config(
1080 source,
1081 "fetchCache",
1082 span,
1083 format!("It has an invalid value: {err}.").into(),
1084 Some(&value),
1085 IssueSeverity::Error,
1086 )
1087 .await;
1088 }
1089 };
1090 }
1091 "runtime" => {
1092 let Some(value) = get_value() else {
1093 return invalid_config(
1094 source,
1095 "runtime",
1096 span,
1097 rcstr!("It mustn't be reexported."),
1098 None,
1099 IssueSeverity::Error,
1100 )
1101 .await;
1102 };
1103 if matches!(value, JsValue::Constant(ConstantValue::Undefined)) {
1104 return Ok(());
1105 }
1106 let Some(val) = value.as_str() else {
1107 return invalid_config(
1108 source,
1109 "runtime",
1110 span,
1111 rcstr!("It needs to be a static string."),
1112 Some(&value),
1113 IssueSeverity::Error,
1114 )
1115 .await;
1116 };
1117
1118 config.runtime = match serde_json::from_value(Value::String(val.to_string())) {
1119 Ok(runtime) => Some(runtime),
1120 Err(err) => {
1121 return invalid_config(
1122 source,
1123 "runtime",
1124 span,
1125 format!("It has an invalid value: {err}.").into(),
1126 Some(&value),
1127 IssueSeverity::Error,
1128 )
1129 .await;
1130 }
1131 };
1132 }
1133 "preferredRegion" => {
1134 let Some(value) = get_value() else {
1135 return invalid_config(
1136 source,
1137 "preferredRegion",
1138 span,
1139 rcstr!("It mustn't be reexported."),
1140 None,
1141 IssueSeverity::Error,
1142 )
1143 .await;
1144 };
1145 if matches!(value, JsValue::Constant(ConstantValue::Undefined)) {
1146 return Ok(());
1147 }
1148
1149 config.preferred_region = parse_preferred_region_from_js_value(
1150 source,
1151 span,
1152 "preferredRegion",
1153 "preferredRegion",
1154 &value,
1155 )
1156 .await?;
1157 }
1158 "generateImageMetadata" => {
1159 config.generate_image_metadata = true;
1160 }
1161 "generateSitemaps" => {
1162 config.generate_sitemaps = true;
1163 }
1164 "generateStaticParams" => {
1165 config.generate_static_params = Some(span);
1166 }
1167 "instant" => {
1168 config.instant = Some(span);
1169 }
1170 "prefetch" => {
1171 config.prefetch = Some(span);
1172 }
1173 "ensureStatic" => {
1174 config.ensure_static = Some(span);
1175 }
1176 _ => {}
1177 }
1178
1179 Ok(())
1180}
1181
1182fn parse_max_duration(value: &JsValue<'_>) -> Result<Option<u32>, &'static str> {
1183 if matches!(value, JsValue::Constant(ConstantValue::Undefined)) {
1184 return Ok(None);
1185 }
1186 let JsValue::Constant(ConstantValue::Num(ConstantNumber(val))) = value else {
1187 return Err("needs to be a static number.");
1188 };
1189 if !val.is_finite() || *val < 0.0 || val.fract() != 0.0 || *val > u32::MAX as f64 {
1190 return Err("needs to be a non-negative integer no greater than 4294967295.");
1191 }
1192 Ok(Some(*val as u32))
1193}
1194
1195async fn parse_preferred_region_from_js_value(
1196 source: ResolvedVc<Box<dyn Source>>,
1197 span: Span,
1198 key: &str,
1199 sub_key: &str,
1200 value: &JsValue<'_>,
1201) -> Result<Option<NextSegmentRegion>> {
1202 let is_array = matches!(value, JsValue::Array { .. });
1203 Ok(
1204 parse_static_string_or_array_from_js_value(source, span, key, sub_key, value)
1205 .await?
1206 .and_then(|regions| {
1207 if is_array {
1208 Some(NextSegmentRegion::Multiple(regions))
1209 } else {
1210 regions.into_iter().next().map(NextSegmentRegion::Single)
1211 }
1212 }),
1213 )
1214}
1215
1216async fn parse_static_string_or_array_from_js_value(
1217 source: ResolvedVc<Box<dyn Source>>,
1218 span: Span,
1219 key: &str,
1220 sub_key: &str,
1221 value: &JsValue<'_>,
1222) -> Result<Option<Vec<RcStr>>> {
1223 Ok(match value {
1224 JsValue::Constant(ConstantValue::Str(str)) => Some(vec![str.to_string().into()]),
1226 JsValue::Array { items, .. } => {
1229 let mut result = Vec::new();
1230 for (i, item) in items.iter().enumerate() {
1231 if let Some(str) = item.as_str() {
1232 result.push(str.to_string().into());
1233 } else {
1234 invalid_config(
1235 source,
1236 key,
1237 span,
1238 format!(
1239 "Entry `{sub_key}[{i}]` needs to be a static string or array of \
1240 static strings."
1241 )
1242 .into(),
1243 Some(item),
1244 IssueSeverity::Error,
1245 )
1246 .await?;
1247 }
1248 }
1249 Some(result)
1250 }
1251 _ => {
1252 invalid_config(
1253 source,
1254 key,
1255 span,
1256 if sub_key != key {
1257 format!("`{sub_key}` needs to be a static string or array of static strings.")
1258 .into()
1259 } else {
1260 rcstr!("It needs to be a static string or array of static strings.")
1261 },
1262 Some(value),
1263 IssueSeverity::Error,
1264 )
1265 .await?;
1266 return Ok(None);
1267 }
1268 })
1269}
1270
1271async fn parse_route_matcher_from_js_value(
1272 source: ResolvedVc<Box<dyn Source>>,
1273 span: Span,
1274 value: &JsValue<'_>,
1275) -> Result<Option<Vec<MiddlewareMatcherKind>>> {
1276 let parse_matcher_kind_matcher =
1277 async |value: &JsValue<'_>, sub_key: &str, matcher_idx: usize| {
1278 let mut route_has = vec![];
1279 if let JsValue::Array { items, .. } = value {
1280 for (i, item) in items.iter().enumerate() {
1281 if let JsValue::Object { parts, .. } = item {
1282 let mut route_type = None;
1283 let mut route_key = None;
1284 let mut route_value = None;
1285
1286 for matcher_part in parts {
1287 if let ObjectPart::KeyValue(part_key, part_value) = matcher_part {
1288 match part_key.as_str() {
1289 Some("type") => {
1290 if let Some(part_value) = part_value.as_str().filter(|v| {
1291 *v == "header"
1292 || *v == "cookie"
1293 || *v == "query"
1294 || *v == "host"
1295 }) {
1296 route_type = Some(part_value);
1297 } else {
1298 invalid_config(
1299 source,
1300 "config",
1301 span,
1302 format!(
1303 "`matcher[{matcher_idx}].{sub_key}[{i}].type` \
1304 must be one of the strings: 'header', \
1305 'cookie', 'query', 'host'"
1306 )
1307 .into(),
1308 Some(part_value),
1309 IssueSeverity::Error,
1310 )
1311 .await?;
1312 }
1313 }
1314 Some("key") => {
1315 if let Some(part_value) = part_value.as_str() {
1316 route_key = Some(part_value);
1317 } else {
1318 invalid_config(
1319 source,
1320 "config",
1321 span,
1322 format!(
1323 "`matcher[{matcher_idx}].{sub_key}[{i}].key` \
1324 must be a string"
1325 )
1326 .into(),
1327 Some(part_value),
1328 IssueSeverity::Error,
1329 )
1330 .await?;
1331 }
1332 }
1333 Some("value") => {
1334 if let Some(part_value) = part_value.as_str() {
1335 route_value = Some(part_value);
1336 } else {
1337 invalid_config(
1338 source,
1339 "config",
1340 span,
1341 format!(
1342 "`matcher[{matcher_idx}].{sub_key}[{i}].\
1343 value` must be a string"
1344 )
1345 .into(),
1346 Some(part_value),
1347 IssueSeverity::Error,
1348 )
1349 .await?;
1350 }
1351 }
1352 _ => {
1353 invalid_config(
1354 source,
1355 "config",
1356 span,
1357 format!(
1358 "Unexpected property in \
1359 `matcher[{matcher_idx}].{sub_key}[{i}]` object"
1360 )
1361 .into(),
1362 Some(part_key),
1363 IssueSeverity::Error,
1364 )
1365 .await?;
1366 }
1367 }
1368 }
1369 }
1370 let r = match route_type {
1371 Some("header") => route_key.map(|route_key| RouteHas::Header {
1372 key: route_key.into(),
1373 value: route_value.map(From::from),
1374 }),
1375 Some("cookie") => route_key.map(|route_key| RouteHas::Cookie {
1376 key: route_key.into(),
1377 value: route_value.map(From::from),
1378 }),
1379 Some("query") => route_key.map(|route_key| RouteHas::Query {
1380 key: route_key.into(),
1381 value: route_value.map(From::from),
1382 }),
1383 Some("host") => route_value.map(|route_value| RouteHas::Host {
1384 value: route_value.into(),
1385 }),
1386 _ => None,
1387 };
1388
1389 if let Some(r) = r {
1390 route_has.push(r);
1391 }
1392 }
1393 }
1394 }
1395
1396 anyhow::Ok(route_has)
1397 };
1398
1399 let mut matchers = vec![];
1400
1401 match value {
1402 JsValue::Constant(ConstantValue::Str(matcher)) => {
1403 matchers.push(MiddlewareMatcherKind::Str(matcher.to_string()));
1404 }
1405 JsValue::Array { items, .. } => {
1406 for (i, item) in items.iter().enumerate() {
1407 if let Some(matcher) = item.as_str() {
1408 matchers.push(MiddlewareMatcherKind::Str(matcher.to_string()));
1409 } else if let JsValue::Object { parts, .. } = item {
1410 let mut matcher = ProxyMatcher::default();
1411 let mut had_source = false;
1412 for matcher_part in parts {
1413 if let ObjectPart::KeyValue(key, value) = matcher_part {
1414 match key.as_str() {
1415 Some("source") => {
1416 if let Some(value) = value.as_str() {
1417 had_source = true;
1422 matcher.original_source = value.into();
1423 } else {
1424 invalid_config(
1425 source,
1426 "config",
1427 span,
1428 format!(
1429 "`source` in `matcher[{i}]` object must be a \
1430 string"
1431 )
1432 .into(),
1433 Some(value),
1434 IssueSeverity::Error,
1435 )
1436 .await?;
1437 }
1438 }
1439 Some("locale") => {
1440 if let Some(value) = value.as_bool()
1441 && !value
1442 {
1443 matcher.locale = false;
1444 } else if matches!(
1445 value,
1446 JsValue::Constant(ConstantValue::Undefined)
1447 ) {
1448 } else {
1450 invalid_config(
1451 source,
1452 "config",
1453 span,
1454 format!(
1455 "`locale` in `matcher[{i}]` object must be false \
1456 or undefined"
1457 )
1458 .into(),
1459 Some(value),
1460 IssueSeverity::Error,
1461 )
1462 .await?;
1463 }
1464 }
1465 Some("missing") => {
1466 matcher.missing =
1467 Some(parse_matcher_kind_matcher(value, "missing", i).await?)
1468 }
1469 Some("has") => {
1470 matcher.has =
1471 Some(parse_matcher_kind_matcher(value, "has", i).await?)
1472 }
1473 Some("regexp") => {
1474 }
1476 _ => {
1477 invalid_config(
1478 source,
1479 "config",
1480 span,
1481 format!("Unexpected property in `matcher[{i}]` object")
1482 .into(),
1483 Some(key),
1484 IssueSeverity::Error,
1485 )
1486 .await?;
1487 }
1488 }
1489 }
1490 }
1491 if !had_source {
1492 invalid_config(
1493 source,
1494 "config",
1495 span,
1496 format!("Missing `source` in `matcher[{i}]` object").into(),
1497 Some(value),
1498 IssueSeverity::Error,
1499 )
1500 .await?;
1501 }
1502
1503 matchers.push(MiddlewareMatcherKind::Matcher(matcher));
1504 } else {
1505 invalid_config(
1506 source,
1507 "config",
1508 span,
1509 format!(
1510 "Entry `matcher[{i}]` need to be static strings or static objects."
1511 )
1512 .into(),
1513 Some(value),
1514 IssueSeverity::Error,
1515 )
1516 .await?;
1517 }
1518 }
1519 }
1520 _ => {
1521 invalid_config(
1522 source,
1523 "config",
1524 span,
1525 rcstr!(
1526 "`matcher` needs to be a static string or array of static strings or array of \
1527 static objects."
1528 ),
1529 Some(value),
1530 IssueSeverity::Error,
1531 )
1532 .await?
1533 }
1534 }
1535
1536 Ok(if matchers.is_empty() {
1537 None
1538 } else {
1539 Some(matchers)
1540 })
1541}
1542
1543#[turbo_tasks::function]
1546pub async fn parse_segment_config_from_loader_tree(
1547 loader_tree: Vc<AppPageLoaderTree>,
1548) -> Result<Vc<NextSegmentConfig>> {
1549 let loader_tree = &*loader_tree.await?;
1550
1551 Ok(parse_segment_config_from_loader_tree_internal(loader_tree)
1552 .await?
1553 .cell())
1554}
1555
1556async fn parse_segment_config_from_loader_tree_internal(
1557 loader_tree: &AppPageLoaderTree,
1558) -> Result<NextSegmentConfig> {
1559 let mut config = NextSegmentConfig::default();
1560
1561 let parallel_configs = loader_tree
1562 .parallel_routes
1563 .values()
1564 .map(|loader_tree| Box::pin(parse_segment_config_from_loader_tree_internal(loader_tree)))
1565 .join()
1566 .await;
1567
1568 for tree in parallel_configs {
1569 config.apply_parallel_config(&tree?)?;
1570 }
1571
1572 let modules = &loader_tree.modules;
1573 for path in [
1574 modules.page.clone(),
1575 modules.default.clone(),
1576 modules.layout.clone(),
1577 ]
1578 .into_iter()
1579 .flatten()
1580 {
1581 let source = Vc::upcast(FileSource::new(path.clone()));
1582 config.apply_parent_config(
1583 &*parse_segment_config_from_source(source, ParseSegmentMode::App).await?,
1584 );
1585 }
1586
1587 Ok(config)
1588}