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