next_core/
app_segment_config.rs

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    /// Whether these metadata exports are defined in the source file.
83    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    /// Applies the parent config to this config, setting any unset values to
97    /// the parent's values.
98    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    /// Applies a config from a parallel route to this config, returning an
119    /// error if there are conflicting values.
120    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, &parallel_config.dynamic, "dynamic")?;
153        merge_parallel(
154            dynamic_params,
155            &parallel_config.dynamic_params,
156            "dynamicParams",
157        )?;
158        merge_parallel(revalidate, &parallel_config.revalidate, "revalidate")?;
159        merge_parallel(fetch_cache, &parallel_config.fetch_cache, "fetchCache")?;
160        merge_parallel(runtime, &parallel_config.runtime, "runtime")?;
161        merge_parallel(
162            preferred_region,
163            &parallel_config.preferred_region,
164            "referredRegion",
165        )?;
166        merge_parallel(
167            experimental_ppr,
168            &parallel_config.experimental_ppr,
169            "experimental_ppr",
170        )?;
171        Ok(())
172    }
173}
174
175/// An issue that occurred while parsing the app segment config.
176#[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    // Don't try parsing if it's not a javascript file, otherwise it will emit an
262    // issue causing the build to "fail".
263    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                        // create an empty expression of {}, we don't need init for function
330                        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                    //noop; revalidate validation occurs in runtime at
436                    //https://github.com/vercel/next.js/blob/cd46c221d2b7f796f963d2b81eea1e405023db23/packages/next/src/server/lib/patch-fetch.ts#L20
437                }
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                // Single value is turned into a single-element Vec.
495                JsValue::Constant(ConstantValue::Str(str)) => vec![str.to_string().into()],
496                // Array of strings is turned into a Vec. If one of the values in not a String it
497                // will error.
498                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        // Match exported generateImageMetadata function and generateSitemaps function, and pass
529        // them to config.
530        "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}