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 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 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 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, ¶llel_config.dynamic, "dynamic")?;
189 merge_parallel(
190 dynamic_params,
191 ¶llel_config.dynamic_params,
192 "dynamicParams",
193 )?;
194 merge_parallel(revalidate, ¶llel_config.revalidate, "revalidate")?;
195 merge_parallel(fetch_cache, ¶llel_config.fetch_cache, "fetchCache")?;
196 merge_parallel(runtime, ¶llel_config.runtime, "runtime")?;
197 merge_parallel(
198 preferred_region,
199 ¶llel_config.preferred_region,
200 "preferredRegion",
201 )?;
202 Ok(())
203 }
204}
205
206#[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 App,
297 Proxy,
299}
300
301#[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 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 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 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 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 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 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 }
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 }
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 JsValue::Constant(ConstantValue::Str(str)) => Some(vec![str.to_string().into()]),
1009 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 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 } 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 }
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#[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}