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