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