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