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, 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 fn severity(&self) -> IssueSeverity {
203 IssueSeverity::Warning
204 }
205
206 #[turbo_tasks::function]
207 fn title(&self) -> Vc<StyledString> {
208 StyledString::Text(rcstr!(
209 "Next.js can't recognize the exported `config` field in route"
210 ))
211 .cell()
212 }
213
214 #[turbo_tasks::function]
215 fn stage(&self) -> Vc<IssueStage> {
216 IssueStage::Parse.into()
217 }
218
219 #[turbo_tasks::function]
220 fn file_path(&self) -> Vc<FileSystemPath> {
221 self.ident.path()
222 }
223
224 #[turbo_tasks::function]
225 fn description(&self) -> Vc<OptionStyledString> {
226 Vc::cell(Some(
227 StyledString::Text(rcstr!(
228 "The exported configuration object in a source file needs to have a very specific \
229 format from which some properties can be statically parsed at compiled-time."
230 ))
231 .resolved_cell(),
232 ))
233 }
234
235 #[turbo_tasks::function]
236 fn detail(&self) -> Vc<OptionStyledString> {
237 Vc::cell(Some(self.detail))
238 }
239
240 #[turbo_tasks::function]
241 fn documentation_link(&self) -> Vc<RcStr> {
242 Vc::cell(rcstr!(
243 "https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config"
244 ))
245 }
246
247 #[turbo_tasks::function]
248 fn source(&self) -> Vc<OptionIssueSource> {
249 Vc::cell(Some(self.source))
250 }
251}
252
253#[turbo_tasks::function]
254pub async fn parse_segment_config_from_source(
255 source: ResolvedVc<Box<dyn Source>>,
256) -> Result<Vc<NextSegmentConfig>> {
257 let path = source.ident().path().await?;
258
259 if path.path.ends_with(".d.ts")
262 || !(path.path.ends_with(".js")
263 || path.path.ends_with(".jsx")
264 || path.path.ends_with(".ts")
265 || path.path.ends_with(".tsx"))
266 {
267 return Ok(Default::default());
268 }
269
270 let result = &*parse(
271 *source,
272 if path.path.ends_with(".ts") {
273 EcmascriptModuleAssetType::Typescript {
274 tsx: false,
275 analyze_types: false,
276 }
277 } else if path.path.ends_with(".tsx") {
278 EcmascriptModuleAssetType::Typescript {
279 tsx: true,
280 analyze_types: false,
281 }
282 } else {
283 EcmascriptModuleAssetType::Ecmascript
284 },
285 EcmascriptInputTransforms::empty(),
286 )
287 .await?;
288
289 let ParseResult::Ok {
290 program: Program::Module(module_ast),
291 eval_context,
292 globals,
293 ..
294 } = result
295 else {
296 return Ok(Default::default());
297 };
298
299 let config = WrapFuture::new(
300 async {
301 let mut config = NextSegmentConfig::default();
302
303 for item in &module_ast.body {
304 let Some(export_decl) = item
305 .as_module_decl()
306 .and_then(|mod_decl| mod_decl.as_export_decl())
307 else {
308 continue;
309 };
310
311 match &export_decl.decl {
312 Decl::Var(var_decl) => {
313 for decl in &var_decl.decls {
314 let Some(ident) = decl.name.as_ident().map(|ident| ident.deref())
315 else {
316 continue;
317 };
318
319 if let Some(init) = decl.init.as_ref() {
320 parse_config_value(source, &mut config, ident, init, eval_context)
321 .await?;
322 }
323 }
324 }
325 Decl::Fn(fn_decl) => {
326 let ident = &fn_decl.ident;
327 let init = Expr::Fn(FnExpr {
329 ident: None,
330 function: fn_decl.function.clone(),
331 });
332 parse_config_value(source, &mut config, ident, &init, eval_context).await?;
333 }
334 _ => {}
335 }
336 }
337 anyhow::Ok(config)
338 },
339 |f, ctx| GLOBALS.set(globals, || f.poll(ctx)),
340 )
341 .await?;
342
343 Ok(config.cell())
344}
345
346async fn parse_config_value(
347 source: ResolvedVc<Box<dyn Source>>,
348 config: &mut NextSegmentConfig,
349 ident: &Ident,
350 init: &Expr,
351 eval_context: &EvalContext,
352) -> Result<()> {
353 let span = init.span();
354 async fn invalid_config(
355 source: ResolvedVc<Box<dyn Source>>,
356 span: Span,
357 detail: &str,
358 value: &JsValue,
359 ) -> Result<()> {
360 let (explainer, hints) = value.explain(2, 0);
361 let detail =
362 StyledString::Text(format!("{detail} Got {explainer}.{hints}").into()).resolved_cell();
363
364 NextSegmentConfigParsingIssue::new(
365 source.ident(),
366 *detail,
367 IssueSource::from_swc_offsets(source, span.lo.to_u32(), span.hi.to_u32()),
368 )
369 .to_resolved()
370 .await?
371 .emit();
372 Ok(())
373 }
374
375 match &*ident.sym {
376 "dynamic" => {
377 let value = eval_context.eval(init);
378 let Some(val) = value.as_str() else {
379 invalid_config(
380 source,
381 span,
382 "`dynamic` needs to be a static string",
383 &value,
384 )
385 .await?;
386 return Ok(());
387 };
388
389 config.dynamic = match serde_json::from_value(Value::String(val.to_string())) {
390 Ok(dynamic) => Some(dynamic),
391 Err(err) => {
392 invalid_config(
393 source,
394 span,
395 &format!("`dynamic` has an invalid value: {err}"),
396 &value,
397 )
398 .await?;
399 return Ok(());
400 }
401 };
402 }
403 "dynamicParams" => {
404 let value = eval_context.eval(init);
405 let Some(val) = value.as_bool() else {
406 invalid_config(
407 source,
408 span,
409 "`dynamicParams` needs to be a static boolean",
410 &value,
411 )
412 .await?;
413 return Ok(());
414 };
415
416 config.dynamic_params = Some(val);
417 }
418 "revalidate" => {
419 let value = eval_context.eval(init);
420 match value {
421 JsValue::Constant(ConstantValue::Num(ConstantNumber(val))) if val >= 0.0 => {
422 config.revalidate = Some(NextRevalidate::Frequency {
423 seconds: val as u32,
424 });
425 }
426 JsValue::Constant(ConstantValue::False) => {
427 config.revalidate = Some(NextRevalidate::Never);
428 }
429 JsValue::Constant(ConstantValue::Str(str)) if str.as_str() == "force-cache" => {
430 config.revalidate = Some(NextRevalidate::ForceCache);
431 }
432 _ => {
433 }
436 }
437 }
438 "fetchCache" => {
439 let value = eval_context.eval(init);
440 let Some(val) = value.as_str() else {
441 return invalid_config(
442 source,
443 span,
444 "`fetchCache` needs to be a static string",
445 &value,
446 )
447 .await;
448 };
449
450 config.fetch_cache = match serde_json::from_value(Value::String(val.to_string())) {
451 Ok(fetch_cache) => Some(fetch_cache),
452 Err(err) => {
453 return invalid_config(
454 source,
455 span,
456 &format!("`fetchCache` has an invalid value: {err}"),
457 &value,
458 )
459 .await;
460 }
461 };
462 }
463 "runtime" => {
464 let value = eval_context.eval(init);
465 let Some(val) = value.as_str() else {
466 return invalid_config(
467 source,
468 span,
469 "`runtime` needs to be a static string",
470 &value,
471 )
472 .await;
473 };
474
475 config.runtime = match serde_json::from_value(Value::String(val.to_string())) {
476 Ok(runtime) => Some(runtime),
477 Err(err) => {
478 return invalid_config(
479 source,
480 span,
481 &format!("`runtime` has an invalid value: {err}"),
482 &value,
483 )
484 .await;
485 }
486 };
487 }
488 "preferredRegion" => {
489 let value = eval_context.eval(init);
490
491 let preferred_region = match value {
492 JsValue::Constant(ConstantValue::Str(str)) => vec![str.to_string().into()],
494 JsValue::Array { items, .. } => {
497 let mut regions = Vec::new();
498 for item in items {
499 if let JsValue::Constant(ConstantValue::Str(str)) = item {
500 regions.push(str.to_string().into());
501 } else {
502 return invalid_config(
503 source,
504 span,
505 "Values of the `preferredRegion` array need to static strings",
506 &item,
507 )
508 .await;
509 }
510 }
511 regions
512 }
513 _ => {
514 return invalid_config(
515 source,
516 span,
517 "`preferredRegion` needs to be a static string or array of static strings",
518 &value,
519 )
520 .await;
521 }
522 };
523
524 config.preferred_region = Some(preferred_region);
525 }
526 "generateImageMetadata" => {
529 config.generate_image_metadata = true;
530 }
531 "generateSitemaps" => {
532 config.generate_sitemaps = true;
533 }
534 "experimental_ppr" => {
535 let value = eval_context.eval(init);
536 let Some(val) = value.as_bool() else {
537 return invalid_config(
538 source,
539 span,
540 "`experimental_ppr` needs to be a static boolean",
541 &value,
542 )
543 .await;
544 };
545
546 config.experimental_ppr = Some(val);
547 }
548 _ => {}
549 }
550
551 Ok(())
552}
553
554#[turbo_tasks::function]
555pub async fn parse_segment_config_from_loader_tree(
556 loader_tree: Vc<AppPageLoaderTree>,
557) -> Result<Vc<NextSegmentConfig>> {
558 let loader_tree = &*loader_tree.await?;
559
560 Ok(parse_segment_config_from_loader_tree_internal(loader_tree)
561 .await?
562 .cell())
563}
564
565pub async fn parse_segment_config_from_loader_tree_internal(
566 loader_tree: &AppPageLoaderTree,
567) -> Result<NextSegmentConfig> {
568 let mut config = NextSegmentConfig::default();
569
570 let parallel_configs = loader_tree
571 .parallel_routes
572 .values()
573 .map(|loader_tree| async move {
574 Box::pin(parse_segment_config_from_loader_tree_internal(loader_tree)).await
575 })
576 .try_join()
577 .await?;
578
579 for tree in parallel_configs {
580 config.apply_parallel_config(&tree)?;
581 }
582
583 let modules = &loader_tree.modules;
584 for path in [
585 modules.page.clone(),
586 modules.default.clone(),
587 modules.layout.clone(),
588 ]
589 .into_iter()
590 .flatten()
591 {
592 let source = Vc::upcast(FileSource::new(path.clone()));
593 config.apply_parent_config(&*parse_segment_config_from_source(source).await?);
594 }
595
596 Ok(config)
597}