1use std::{future::Future, ops::Deref};
2
3use anyhow::{Result, bail};
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use swc_core::{
7 common::{GLOBALS, Span, Spanned, source_map::SmallPos},
8 ecma::ast::{Decl, Expr, FnExpr, Ident, Program},
9};
10use turbo_rcstr::RcStr;
11use turbo_tasks::{
12 NonLocalValue, ResolvedVc, TryJoinIterExt, ValueDefault, Vc, trace::TraceRawVcs,
13 util::WrapFuture,
14};
15use turbo_tasks_fs::FileSystemPath;
16use turbopack_core::{
17 file_source::FileSource,
18 ident::AssetIdent,
19 issue::{
20 Issue, IssueExt, IssueSeverity, IssueSource, IssueStage, OptionIssueSource,
21 OptionStyledString, StyledString,
22 },
23 source::Source,
24};
25use turbopack_ecmascript::{
26 EcmascriptInputTransforms, EcmascriptModuleAssetType,
27 analyzer::{ConstantNumber, ConstantValue, JsValue, graph::EvalContext},
28 parse::{ParseResult, parse},
29};
30
31use crate::{app_structure::AppPageLoaderTree, util::NextRuntime};
32
33#[derive(
34 Default, PartialEq, Eq, Clone, Copy, Debug, TraceRawVcs, Serialize, Deserialize, NonLocalValue,
35)]
36#[serde(rename_all = "kebab-case")]
37pub enum NextSegmentDynamic {
38 #[default]
39 Auto,
40 ForceDynamic,
41 Error,
42 ForceStatic,
43}
44
45#[derive(
46 Default, PartialEq, Eq, Clone, Copy, Debug, TraceRawVcs, Serialize, Deserialize, NonLocalValue,
47)]
48#[serde(rename_all = "kebab-case")]
49pub enum NextSegmentFetchCache {
50 #[default]
51 Auto,
52 DefaultCache,
53 OnlyCache,
54 ForceCache,
55 DefaultNoStore,
56 OnlyNoStore,
57 ForceNoStore,
58}
59
60#[derive(
61 Default, PartialEq, Eq, Clone, Copy, Debug, TraceRawVcs, Serialize, Deserialize, NonLocalValue,
62)]
63pub enum NextRevalidate {
64 #[default]
65 Never,
66 ForceCache,
67 Frequency {
68 seconds: u32,
69 },
70}
71
72#[turbo_tasks::value(into = "shared")]
73#[derive(Debug, Default, Clone)]
74pub struct NextSegmentConfig {
75 pub dynamic: Option<NextSegmentDynamic>,
76 pub dynamic_params: Option<bool>,
77 pub revalidate: Option<NextRevalidate>,
78 pub fetch_cache: Option<NextSegmentFetchCache>,
79 pub runtime: Option<NextRuntime>,
80 pub preferred_region: Option<Vec<RcStr>>,
81 pub experimental_ppr: Option<bool>,
82 pub generate_image_metadata: bool,
84 pub generate_sitemaps: bool,
85}
86
87#[turbo_tasks::value_impl]
88impl ValueDefault for NextSegmentConfig {
89 #[turbo_tasks::function]
90 pub fn value_default() -> Vc<Self> {
91 NextSegmentConfig::default().cell()
92 }
93}
94
95impl NextSegmentConfig {
96 pub fn apply_parent_config(&mut self, parent: &Self) {
99 let NextSegmentConfig {
100 dynamic,
101 dynamic_params,
102 revalidate,
103 fetch_cache,
104 runtime,
105 preferred_region,
106 experimental_ppr,
107 ..
108 } = self;
109 *dynamic = dynamic.or(parent.dynamic);
110 *dynamic_params = dynamic_params.or(parent.dynamic_params);
111 *revalidate = revalidate.or(parent.revalidate);
112 *fetch_cache = fetch_cache.or(parent.fetch_cache);
113 *runtime = runtime.or(parent.runtime);
114 *preferred_region = preferred_region.take().or(parent.preferred_region.clone());
115 *experimental_ppr = experimental_ppr.or(parent.experimental_ppr);
116 }
117
118 pub fn apply_parallel_config(&mut self, parallel_config: &Self) -> Result<()> {
121 fn merge_parallel<T: PartialEq + Clone>(
122 a: &mut Option<T>,
123 b: &Option<T>,
124 name: &str,
125 ) -> Result<()> {
126 match (a.as_ref(), b) {
127 (Some(a), Some(b)) => {
128 if *a != *b {
129 bail!(
130 "Sibling segment configs have conflicting values for {}",
131 name
132 )
133 }
134 }
135 (None, Some(b)) => {
136 *a = Some(b.clone());
137 }
138 _ => {}
139 }
140 Ok(())
141 }
142 let Self {
143 dynamic,
144 dynamic_params,
145 revalidate,
146 fetch_cache,
147 runtime,
148 preferred_region,
149 experimental_ppr,
150 ..
151 } = self;
152 merge_parallel(dynamic, ¶llel_config.dynamic, "dynamic")?;
153 merge_parallel(
154 dynamic_params,
155 ¶llel_config.dynamic_params,
156 "dynamicParams",
157 )?;
158 merge_parallel(revalidate, ¶llel_config.revalidate, "revalidate")?;
159 merge_parallel(fetch_cache, ¶llel_config.fetch_cache, "fetchCache")?;
160 merge_parallel(runtime, ¶llel_config.runtime, "runtime")?;
161 merge_parallel(
162 preferred_region,
163 ¶llel_config.preferred_region,
164 "referredRegion",
165 )?;
166 merge_parallel(
167 experimental_ppr,
168 ¶llel_config.experimental_ppr,
169 "experimental_ppr",
170 )?;
171 Ok(())
172 }
173}
174
175#[turbo_tasks::value(shared)]
177pub struct NextSegmentConfigParsingIssue {
178 ident: ResolvedVc<AssetIdent>,
179 detail: ResolvedVc<StyledString>,
180 source: IssueSource,
181}
182
183#[turbo_tasks::value_impl]
184impl NextSegmentConfigParsingIssue {
185 #[turbo_tasks::function]
186 pub fn new(
187 ident: ResolvedVc<AssetIdent>,
188 detail: ResolvedVc<StyledString>,
189 source: IssueSource,
190 ) -> Vc<Self> {
191 Self {
192 ident,
193 detail,
194 source,
195 }
196 .cell()
197 }
198}
199
200#[turbo_tasks::value_impl]
201impl Issue for NextSegmentConfigParsingIssue {
202 #[turbo_tasks::function]
203 fn severity(&self) -> Vc<IssueSeverity> {
204 IssueSeverity::Warning.into()
205 }
206
207 #[turbo_tasks::function]
208 fn title(&self) -> Vc<StyledString> {
209 StyledString::Text("Unable to parse config export in source file".into()).cell()
210 }
211
212 #[turbo_tasks::function]
213 fn stage(&self) -> Vc<IssueStage> {
214 IssueStage::Parse.into()
215 }
216
217 #[turbo_tasks::function]
218 fn file_path(&self) -> Vc<FileSystemPath> {
219 self.ident.path()
220 }
221
222 #[turbo_tasks::function]
223 fn description(&self) -> Vc<OptionStyledString> {
224 Vc::cell(Some(
225 StyledString::Text(
226 "The exported configuration object in a source file needs to have a very specific \
227 format from which some properties can be statically parsed at compiled-time."
228 .into(),
229 )
230 .resolved_cell(),
231 ))
232 }
233
234 #[turbo_tasks::function]
235 fn detail(&self) -> Vc<OptionStyledString> {
236 Vc::cell(Some(self.detail))
237 }
238
239 #[turbo_tasks::function]
240 fn documentation_link(&self) -> Vc<RcStr> {
241 Vc::cell(
242 "https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config"
243 .into(),
244 )
245 }
246
247 #[turbo_tasks::function]
248 async fn source(&self) -> Result<Vc<OptionIssueSource>> {
249 Ok(Vc::cell(Some(
250 self.source.resolve_source_map().await?.into_owned(),
251 )))
252 }
253}
254
255#[turbo_tasks::function]
256pub async fn parse_segment_config_from_source(
257 source: ResolvedVc<Box<dyn Source>>,
258) -> Result<Vc<NextSegmentConfig>> {
259 let path = source.ident().path().await?;
260
261 if path.path.ends_with(".d.ts")
264 || !(path.path.ends_with(".js")
265 || path.path.ends_with(".jsx")
266 || path.path.ends_with(".ts")
267 || path.path.ends_with(".tsx"))
268 {
269 return Ok(Default::default());
270 }
271
272 let result = &*parse(
273 *source,
274 turbo_tasks::Value::new(if path.path.ends_with(".ts") {
275 EcmascriptModuleAssetType::Typescript {
276 tsx: false,
277 analyze_types: false,
278 }
279 } else if path.path.ends_with(".tsx") {
280 EcmascriptModuleAssetType::Typescript {
281 tsx: true,
282 analyze_types: false,
283 }
284 } else {
285 EcmascriptModuleAssetType::Ecmascript
286 }),
287 EcmascriptInputTransforms::empty(),
288 )
289 .await?;
290
291 let ParseResult::Ok {
292 program: Program::Module(module_ast),
293 eval_context,
294 globals,
295 ..
296 } = result
297 else {
298 return Ok(Default::default());
299 };
300
301 let config = WrapFuture::new(
302 async {
303 let mut config = NextSegmentConfig::default();
304
305 for item in &module_ast.body {
306 let Some(export_decl) = item
307 .as_module_decl()
308 .and_then(|mod_decl| mod_decl.as_export_decl())
309 else {
310 continue;
311 };
312
313 match &export_decl.decl {
314 Decl::Var(var_decl) => {
315 for decl in &var_decl.decls {
316 let Some(ident) = decl.name.as_ident().map(|ident| ident.deref())
317 else {
318 continue;
319 };
320
321 if let Some(init) = decl.init.as_ref() {
322 parse_config_value(source, &mut config, ident, init, eval_context)
323 .await?;
324 }
325 }
326 }
327 Decl::Fn(fn_decl) => {
328 let ident = &fn_decl.ident;
329 let init = Expr::Fn(FnExpr {
331 ident: None,
332 function: fn_decl.function.clone(),
333 });
334 parse_config_value(source, &mut config, ident, &init, eval_context).await?;
335 }
336 _ => {}
337 }
338 }
339 anyhow::Ok(config)
340 },
341 |f, ctx| GLOBALS.set(globals, || f.poll(ctx)),
342 )
343 .await?;
344
345 Ok(config.cell())
346}
347
348async fn parse_config_value(
349 source: ResolvedVc<Box<dyn Source>>,
350 config: &mut NextSegmentConfig,
351 ident: &Ident,
352 init: &Expr,
353 eval_context: &EvalContext,
354) -> Result<()> {
355 let span = init.span();
356 async fn invalid_config(
357 source: ResolvedVc<Box<dyn Source>>,
358 span: Span,
359 detail: &str,
360 value: &JsValue,
361 ) -> Result<()> {
362 let (explainer, hints) = value.explain(2, 0);
363 let detail =
364 StyledString::Text(format!("{detail} Got {explainer}.{hints}").into()).resolved_cell();
365
366 NextSegmentConfigParsingIssue::new(
367 source.ident(),
368 *detail,
369 IssueSource::from_swc_offsets(source, span.lo.to_u32(), span.hi.to_u32()),
370 )
371 .to_resolved()
372 .await?
373 .emit();
374 Ok(())
375 }
376
377 match &*ident.sym {
378 "dynamic" => {
379 let value = eval_context.eval(init);
380 let Some(val) = value.as_str() else {
381 invalid_config(
382 source,
383 span,
384 "`dynamic` needs to be a static string",
385 &value,
386 )
387 .await?;
388 return Ok(());
389 };
390
391 config.dynamic = match serde_json::from_value(Value::String(val.to_string())) {
392 Ok(dynamic) => Some(dynamic),
393 Err(err) => {
394 invalid_config(
395 source,
396 span,
397 &format!("`dynamic` has an invalid value: {err}"),
398 &value,
399 )
400 .await?;
401 return Ok(());
402 }
403 };
404 }
405 "dynamicParams" => {
406 let value = eval_context.eval(init);
407 let Some(val) = value.as_bool() else {
408 invalid_config(
409 source,
410 span,
411 "`dynamicParams` needs to be a static boolean",
412 &value,
413 )
414 .await?;
415 return Ok(());
416 };
417
418 config.dynamic_params = Some(val);
419 }
420 "revalidate" => {
421 let value = eval_context.eval(init);
422 match value {
423 JsValue::Constant(ConstantValue::Num(ConstantNumber(val))) if val >= 0.0 => {
424 config.revalidate = Some(NextRevalidate::Frequency {
425 seconds: val as u32,
426 });
427 }
428 JsValue::Constant(ConstantValue::False) => {
429 config.revalidate = Some(NextRevalidate::Never);
430 }
431 JsValue::Constant(ConstantValue::Str(str)) if str.as_str() == "force-cache" => {
432 config.revalidate = Some(NextRevalidate::ForceCache);
433 }
434 _ => {
435 }
438 }
439 }
440 "fetchCache" => {
441 let value = eval_context.eval(init);
442 let Some(val) = value.as_str() else {
443 return invalid_config(
444 source,
445 span,
446 "`fetchCache` needs to be a static string",
447 &value,
448 )
449 .await;
450 };
451
452 config.fetch_cache = match serde_json::from_value(Value::String(val.to_string())) {
453 Ok(fetch_cache) => Some(fetch_cache),
454 Err(err) => {
455 return invalid_config(
456 source,
457 span,
458 &format!("`fetchCache` has an invalid value: {err}"),
459 &value,
460 )
461 .await;
462 }
463 };
464 }
465 "runtime" => {
466 let value = eval_context.eval(init);
467 let Some(val) = value.as_str() else {
468 return invalid_config(
469 source,
470 span,
471 "`runtime` needs to be a static string",
472 &value,
473 )
474 .await;
475 };
476
477 config.runtime = match serde_json::from_value(Value::String(val.to_string())) {
478 Ok(runtime) => Some(runtime),
479 Err(err) => {
480 return invalid_config(
481 source,
482 span,
483 &format!("`runtime` has an invalid value: {err}"),
484 &value,
485 )
486 .await;
487 }
488 };
489 }
490 "preferredRegion" => {
491 let value = eval_context.eval(init);
492
493 let preferred_region = match value {
494 JsValue::Constant(ConstantValue::Str(str)) => vec![str.to_string().into()],
496 JsValue::Array { items, .. } => {
499 let mut regions = Vec::new();
500 for item in items {
501 if let JsValue::Constant(ConstantValue::Str(str)) = item {
502 regions.push(str.to_string().into());
503 } else {
504 return invalid_config(
505 source,
506 span,
507 "Values of the `preferredRegion` array need to static strings",
508 &item,
509 )
510 .await;
511 }
512 }
513 regions
514 }
515 _ => {
516 return invalid_config(
517 source,
518 span,
519 "`preferredRegion` needs to be a static string or array of static strings",
520 &value,
521 )
522 .await;
523 }
524 };
525
526 config.preferred_region = Some(preferred_region);
527 }
528 "generateImageMetadata" => {
531 config.generate_image_metadata = true;
532 }
533 "generateSitemaps" => {
534 config.generate_sitemaps = true;
535 }
536 "experimental_ppr" => {
537 let value = eval_context.eval(init);
538 let Some(val) = value.as_bool() else {
539 return invalid_config(
540 source,
541 span,
542 "`experimental_ppr` needs to be a static boolean",
543 &value,
544 )
545 .await;
546 };
547
548 config.experimental_ppr = Some(val);
549 }
550 _ => {}
551 }
552
553 Ok(())
554}
555
556#[turbo_tasks::function]
557pub async fn parse_segment_config_from_loader_tree(
558 loader_tree: Vc<AppPageLoaderTree>,
559) -> Result<Vc<NextSegmentConfig>> {
560 let loader_tree = &*loader_tree.await?;
561
562 Ok(parse_segment_config_from_loader_tree_internal(loader_tree)
563 .await?
564 .cell())
565}
566
567pub async fn parse_segment_config_from_loader_tree_internal(
568 loader_tree: &AppPageLoaderTree,
569) -> Result<NextSegmentConfig> {
570 let mut config = NextSegmentConfig::default();
571
572 let parallel_configs = loader_tree
573 .parallel_routes
574 .values()
575 .map(|loader_tree| async move {
576 Box::pin(parse_segment_config_from_loader_tree_internal(loader_tree)).await
577 })
578 .try_join()
579 .await?;
580
581 for tree in parallel_configs {
582 config.apply_parallel_config(&tree)?;
583 }
584
585 let modules = &loader_tree.modules;
586 for path in [modules.page, modules.default, modules.layout]
587 .into_iter()
588 .flatten()
589 {
590 let source = Vc::upcast(FileSource::new(*path));
591 config.apply_parent_config(&*parse_segment_config_from_source(source).await?);
592 }
593
594 Ok(config)
595}