Skip to main content

next_core/
app_structure.rs

1use std::collections::BTreeMap;
2
3use anyhow::{Context, Result, bail};
4use async_trait::async_trait;
5use bincode::{Decode, Encode};
6use indexmap::map::{Entry, OccupiedEntry};
7use rustc_hash::FxHashMap;
8use tracing::Instrument;
9use turbo_rcstr::{RcStr, rcstr};
10use turbo_tasks::{
11    FxIndexMap, FxIndexSet, NonLocalValue, ResolvedVc, TryJoinIterExt, ValueDefault,
12    ValueToStringRef, Vc, debug::ValueDebugFormat, fxindexmap, trace::TraceRawVcs, turbobail,
13};
14use turbo_tasks_fs::{DirectoryContent, DirectoryEntry, FileSystemEntryType, FileSystemPath};
15use turbopack_core::issue::{Issue, IssueExt, IssueSeverity, IssueStage, StyledString};
16
17use crate::{
18    mode::NextMode,
19    next_app::{
20        AppPage, AppPath, PageSegment, PageType,
21        metadata::{
22            GlobalMetadataFileMatch, MetadataFileMatch, match_global_metadata_file,
23            match_local_metadata_file, normalize_metadata_route,
24        },
25    },
26    next_import_map::get_next_package,
27};
28
29// Next.js ignores underscores for routes but you can use %5f to still serve an underscored
30// route.
31fn normalize_underscore(string: &str) -> String {
32    string.replace("%5F", "_")
33}
34
35/// A final route in the app directory.
36#[turbo_tasks::value]
37#[derive(Default, Debug, Clone)]
38pub struct AppDirModules {
39    pub page: Option<FileSystemPath>,
40    pub layout: Option<FileSystemPath>,
41    pub error: Option<FileSystemPath>,
42    pub global_error: Option<FileSystemPath>,
43    pub global_not_found: Option<FileSystemPath>,
44    pub loading: Option<FileSystemPath>,
45    pub template: Option<FileSystemPath>,
46    pub forbidden: Option<FileSystemPath>,
47    pub unauthorized: Option<FileSystemPath>,
48    pub not_found: Option<FileSystemPath>,
49    pub default: Option<FileSystemPath>,
50    pub route: Option<FileSystemPath>,
51    pub metadata: Metadata,
52}
53
54impl AppDirModules {
55    fn without_leaves(&self) -> Self {
56        Self {
57            page: None,
58            layout: self.layout.clone(),
59            error: self.error.clone(),
60            global_error: self.global_error.clone(),
61            global_not_found: self.global_not_found.clone(),
62            loading: self.loading.clone(),
63            template: self.template.clone(),
64            not_found: self.not_found.clone(),
65            forbidden: self.forbidden.clone(),
66            unauthorized: self.unauthorized.clone(),
67            default: None,
68            route: None,
69            metadata: self.metadata.clone(),
70        }
71    }
72}
73
74/// A single metadata file plus an optional "alt" text file.
75#[derive(Clone, Debug, PartialEq, Eq, TraceRawVcs, NonLocalValue, Encode, Decode)]
76pub enum MetadataWithAltItem {
77    Static {
78        path: FileSystemPath,
79        alt_path: Option<FileSystemPath>,
80    },
81    Dynamic {
82        path: FileSystemPath,
83    },
84}
85
86/// A single metadata file.
87#[turbo_tasks::task_input]
88#[derive(Clone, Debug, Hash, PartialEq, Eq, TraceRawVcs, Encode, Decode)]
89pub enum MetadataItem {
90    Static { path: FileSystemPath },
91    Dynamic { path: FileSystemPath },
92}
93
94#[turbo_tasks::function]
95pub async fn get_metadata_route_name(meta: MetadataItem) -> Result<Vc<RcStr>> {
96    Ok(match meta {
97        MetadataItem::Static { path } => Vc::cell(path.file_name().into()),
98        MetadataItem::Dynamic { path } => {
99            let Some(stem) = path.file_stem() else {
100                turbobail!("unable to resolve file stem for metadata item at {path}");
101            };
102
103            match stem {
104                "manifest" => Vc::cell(rcstr!("manifest.webmanifest")),
105                _ => Vc::cell(RcStr::from(stem)),
106            }
107        }
108    })
109}
110
111impl MetadataItem {
112    pub fn into_path(self) -> FileSystemPath {
113        match self {
114            MetadataItem::Static { path } => path,
115            MetadataItem::Dynamic { path } => path,
116        }
117    }
118}
119
120impl From<MetadataWithAltItem> for MetadataItem {
121    fn from(value: MetadataWithAltItem) -> Self {
122        match value {
123            MetadataWithAltItem::Static { path, .. } => MetadataItem::Static { path },
124            MetadataWithAltItem::Dynamic { path } => MetadataItem::Dynamic { path },
125        }
126    }
127}
128
129/// Metadata file that can be placed in any segment of the app directory.
130#[derive(Default, Clone, Debug, PartialEq, Eq, TraceRawVcs, NonLocalValue, Encode, Decode)]
131pub struct Metadata {
132    pub icon: Vec<MetadataWithAltItem>,
133    pub apple: Vec<MetadataWithAltItem>,
134    pub twitter: Vec<MetadataWithAltItem>,
135    pub open_graph: Vec<MetadataWithAltItem>,
136    pub sitemap: Option<MetadataItem>,
137    // The page indicates where the metadata is defined and captured.
138    // The steps for capturing metadata (get_directory_tree) and constructing
139    // LoaderTree (directory_tree_to_entrypoints) is separated,
140    // and child loader tree can trickle down metadata when clone / merge components calculates
141    // the actual path incorrectly with fillMetadataSegment.
142    //
143    // This is only being used for the static metadata files.
144    pub base_page: Option<AppPage>,
145}
146
147impl Metadata {
148    pub fn is_empty(&self) -> bool {
149        let Metadata {
150            icon,
151            apple,
152            twitter,
153            open_graph,
154            sitemap,
155            base_page: _,
156        } = self;
157        icon.is_empty()
158            && apple.is_empty()
159            && twitter.is_empty()
160            && open_graph.is_empty()
161            && sitemap.is_none()
162    }
163}
164
165/// Metadata files that can be placed in the root of the app directory.
166#[turbo_tasks::value]
167#[derive(Default, Clone, Debug)]
168pub struct GlobalMetadata {
169    pub favicon: Option<MetadataItem>,
170    pub robots: Option<MetadataItem>,
171    pub manifest: Option<MetadataItem>,
172}
173
174impl GlobalMetadata {
175    pub fn is_empty(&self) -> bool {
176        let GlobalMetadata {
177            favicon,
178            robots,
179            manifest,
180        } = self;
181        favicon.is_none() && robots.is_none() && manifest.is_none()
182    }
183}
184
185#[turbo_tasks::value]
186#[derive(Debug)]
187pub struct DirectoryTree {
188    /// key is e.g. "dashboard", "(dashboard)", "@slot"
189    pub subdirectories: BTreeMap<RcStr, ResolvedVc<DirectoryTree>>,
190    pub modules: AppDirModules,
191}
192
193#[turbo_tasks::value]
194#[derive(Clone, Debug)]
195struct PlainDirectoryTree {
196    /// key is e.g. "dashboard", "(dashboard)", "@slot"
197    pub subdirectories: BTreeMap<RcStr, PlainDirectoryTree>,
198    pub modules: AppDirModules,
199    /// Flattened URL tree with route groups and parallel routes transparent.
200    pub url_tree: UrlSegmentTree,
201}
202
203/// A tree representing the URL segment structure, with route groups and parallel
204/// routes flattened out. This provides a unified view of all segments at each URL
205/// level, regardless of which route group they're defined in.
206///
207/// For example, given this directory structure:
208///
209/// ```text
210/// app/
211/// ├── (group1)/
212/// │   └── products/
213/// │       └── sale/
214/// └── (group2)/
215///     └── products/
216///         └── [id]/
217/// ```
218///
219/// The UrlSegmentTree would be:
220///
221/// ```text
222/// (root)
223/// └── products/
224///     ├── sale/
225///     └── [id]/
226/// ```
227///
228/// This makes it easy to find all siblings at a given URL level.
229#[derive(Clone, Debug, Default, PartialEq, Eq, TraceRawVcs, NonLocalValue, Encode, Decode)]
230struct UrlSegmentTree {
231    pub children: BTreeMap<RcStr, UrlSegmentTree>,
232}
233
234impl UrlSegmentTree {
235    fn static_children(&self) -> Vec<RcStr> {
236        self.children
237            .keys()
238            .filter(|name| !is_dynamic_segment(name))
239            .cloned()
240            .collect()
241    }
242
243    fn get_child(&self, segment: &str) -> Option<&UrlSegmentTree> {
244        self.children.get(segment)
245    }
246}
247
248fn build_url_segment_tree_from_subdirs(
249    subdirs: &BTreeMap<RcStr, PlainDirectoryTree>,
250) -> UrlSegmentTree {
251    let mut result = UrlSegmentTree::default();
252    build_url_segment_tree_recursive(subdirs, &mut result);
253    result
254}
255
256/// Recursively builds the URL segment tree by accumulating children at each
257/// URL level. Segments from different route groups that share the same URL path
258/// are merged together.
259///
260/// Example: `(group1)/products/sale/` and `(group2)/products/[id]/` both
261/// contribute to a single `products/` node containing both `sale/` and `[id]/`.
262fn build_url_segment_tree_recursive(
263    subdirs: &BTreeMap<RcStr, PlainDirectoryTree>,
264    result: &mut UrlSegmentTree,
265) {
266    for (name, subtree) in subdirs {
267        if is_url_transparent_segment(name) {
268            // Transparent segments (route groups, parallel routes) don't create
269            // a new URL level. Recurse with the same `result` so their children
270            // are accumulated at the current level.
271            build_url_segment_tree_recursive(&subtree.subdirectories, result);
272        } else {
273            // Non-transparent segments create a new URL level. Get or create a
274            // child node for this segment, then recurse to accumulate its children.
275            // Using `or_default()` ensures that if this segment was already added
276            // from a different route group, we merge into it rather than replace.
277            let child = result.children.entry(name.clone()).or_default();
278            build_url_segment_tree_recursive(&subtree.subdirectories, child);
279        }
280    }
281}
282
283#[turbo_tasks::value_impl]
284impl DirectoryTree {
285    #[turbo_tasks::function]
286    pub async fn into_plain(&self) -> Result<Vc<PlainDirectoryTree>> {
287        let mut subdirectories = BTreeMap::new();
288
289        for (name, subdirectory) in &self.subdirectories {
290            subdirectories.insert(name.clone(), subdirectory.into_plain().owned().await?);
291        }
292
293        let url_tree = build_url_segment_tree_from_subdirs(&subdirectories);
294
295        Ok(PlainDirectoryTree {
296            subdirectories,
297            modules: self.modules.clone(),
298            url_tree,
299        }
300        .cell())
301    }
302}
303
304#[turbo_tasks::value(transparent)]
305pub struct OptionAppDir(Option<FileSystemPath>);
306
307/// Finds and returns the [DirectoryTree] of the app directory if existing.
308#[turbo_tasks::function]
309pub async fn find_app_dir(project_path: FileSystemPath) -> Result<Vc<OptionAppDir>> {
310    let app = project_path.join("app")?;
311    let src_app = project_path.join("src/app")?;
312    let app_dir = if *app.get_type().await? == FileSystemEntryType::Directory {
313        app
314    } else if *src_app.get_type().await? == FileSystemEntryType::Directory {
315        src_app
316    } else {
317        return Ok(Vc::cell(None));
318    };
319
320    Ok(Vc::cell(Some(app_dir)))
321}
322
323#[turbo_tasks::function]
324async fn get_directory_tree(
325    dir: FileSystemPath,
326    page_extensions: Vc<Vec<RcStr>>,
327) -> Result<Vc<DirectoryTree>> {
328    let span = tracing::info_span!(
329        "read app directory tree",
330        name = display(dir.to_string_ref().await?)
331    );
332    get_directory_tree_internal(dir, page_extensions)
333        .instrument(span)
334        .await
335}
336
337async fn get_directory_tree_internal(
338    dir: FileSystemPath,
339    page_extensions: Vc<Vec<RcStr>>,
340) -> Result<Vc<DirectoryTree>> {
341    let DirectoryContent::Entries(entries) = &*dir.read_dir().await? else {
342        // the file watcher might invalidate things in the wrong order,
343        // and we have to account for the eventual consistency of turbo-tasks
344        // so we just return an empty tree here.
345        return Ok(DirectoryTree {
346            subdirectories: Default::default(),
347            modules: AppDirModules::default(),
348        }
349        .cell());
350    };
351    let page_extensions_value = page_extensions.await?;
352
353    let mut subdirectories = BTreeMap::new();
354    let mut modules = AppDirModules::default();
355
356    let mut metadata_icon = Vec::new();
357    let mut metadata_apple = Vec::new();
358    let mut metadata_open_graph = Vec::new();
359    let mut metadata_twitter = Vec::new();
360
361    for (basename, entry) in entries {
362        let entry = entry.clone().resolve_symlink().await?;
363        match entry {
364            DirectoryEntry::File(file) => {
365                // Do not process .d.ts files as routes
366                if basename.ends_with(".d.ts") {
367                    continue;
368                }
369                if let Some((stem, ext)) = basename.split_once('.')
370                    && page_extensions_value.iter().any(|e| e == ext)
371                {
372                    match stem {
373                        "page" => modules.page = Some(file.clone()),
374                        "layout" => modules.layout = Some(file.clone()),
375                        "error" => modules.error = Some(file.clone()),
376                        "global-error" => modules.global_error = Some(file.clone()),
377                        "global-not-found" => modules.global_not_found = Some(file.clone()),
378                        "loading" => modules.loading = Some(file.clone()),
379                        "template" => modules.template = Some(file.clone()),
380                        "forbidden" => modules.forbidden = Some(file.clone()),
381                        "unauthorized" => modules.unauthorized = Some(file.clone()),
382                        "not-found" => modules.not_found = Some(file.clone()),
383                        "default" => modules.default = Some(file.clone()),
384                        "route" => modules.route = Some(file.clone()),
385                        _ => {}
386                    }
387                }
388
389                let Some(MetadataFileMatch {
390                    metadata_type,
391                    number,
392                    dynamic,
393                }) = match_local_metadata_file(basename.as_str(), &page_extensions_value)
394                else {
395                    continue;
396                };
397
398                let entry = match metadata_type {
399                    "icon" => &mut metadata_icon,
400                    "apple-icon" => &mut metadata_apple,
401                    "twitter-image" => &mut metadata_twitter,
402                    "opengraph-image" => &mut metadata_open_graph,
403                    "sitemap" => {
404                        if dynamic {
405                            modules.metadata.sitemap = Some(MetadataItem::Dynamic { path: file });
406                        } else {
407                            modules.metadata.sitemap = Some(MetadataItem::Static { path: file });
408                        }
409                        continue;
410                    }
411                    _ => continue,
412                };
413
414                if dynamic {
415                    entry.push((number, MetadataWithAltItem::Dynamic { path: file }));
416                    continue;
417                }
418
419                let file_name = file.file_name();
420                let basename = file_name
421                    .rsplit_once('.')
422                    .map_or(file_name, |(basename, _)| basename);
423                let alt_path = file.parent().join(&format!("{basename}.alt.txt"))?;
424                let alt_path = matches!(&*alt_path.get_type().await?, FileSystemEntryType::File)
425                    .then_some(alt_path);
426
427                entry.push((
428                    number,
429                    MetadataWithAltItem::Static {
430                        path: file,
431                        alt_path,
432                    },
433                ));
434            }
435            DirectoryEntry::Directory(dir)
436                // appDir ignores paths starting with an underscore
437                if !basename.starts_with('_') => {
438                    let result = get_directory_tree(dir.clone(), page_extensions)
439                        .to_resolved()
440                        .await?;
441                    subdirectories.insert(basename.clone(), result);
442                }
443            // TODO(WEB-952) handle symlinks in app dir
444            _ => {}
445        }
446    }
447
448    fn sort<T>(mut list: Vec<(Option<u32>, T)>) -> Vec<T> {
449        list.sort_by_key(|(num, _)| *num);
450        list.into_iter().map(|(_, item)| item).collect()
451    }
452
453    modules.metadata.icon = sort(metadata_icon);
454    modules.metadata.apple = sort(metadata_apple);
455    modules.metadata.twitter = sort(metadata_twitter);
456    modules.metadata.open_graph = sort(metadata_open_graph);
457
458    Ok(DirectoryTree {
459        subdirectories,
460        modules,
461    }
462    .cell())
463}
464
465#[turbo_tasks::value]
466#[derive(Debug, Clone)]
467pub struct AppPageLoaderTree {
468    pub page: AppPage,
469    pub segment: RcStr,
470    #[bincode(with = "turbo_bincode::indexmap")]
471    pub parallel_routes: FxIndexMap<RcStr, AppPageLoaderTree>,
472    pub modules: AppDirModules,
473    pub global_metadata: ResolvedVc<GlobalMetadata>,
474    /// For dynamic segments, contains the list of static sibling segments that
475    /// exist at the same URL path level. Used by the client router to determine
476    /// if a prefetch can be reused.
477    pub static_siblings: Vec<RcStr>,
478}
479
480impl AppPageLoaderTree {
481    /// Returns true if there's a page match in this loader tree.
482    pub fn has_page(&self) -> bool {
483        if &*self.segment == "__PAGE__" {
484            return true;
485        }
486
487        for (_, tree) in &self.parallel_routes {
488            if tree.has_page() {
489                return true;
490            }
491        }
492
493        false
494    }
495
496    /// Returns whether the only match in this tree is for a catch-all
497    /// route.
498    pub fn has_only_catchall(&self) -> bool {
499        if &*self.segment == "__PAGE__" && !self.page.is_catchall() {
500            return false;
501        }
502
503        for (_, tree) in &self.parallel_routes {
504            if !tree.has_only_catchall() {
505                return false;
506            }
507        }
508
509        true
510    }
511
512    /// Returns true if this loader tree contains an intercepting route match.
513    pub fn is_intercepting(&self) -> bool {
514        if self.page.is_intercepting() && self.has_page() {
515            return true;
516        }
517
518        for (_, tree) in &self.parallel_routes {
519            if tree.is_intercepting() {
520                return true;
521            }
522        }
523
524        false
525    }
526
527    /// Returns the specificity of the page (i.e. the number of segments
528    /// affecting the path)
529    pub fn get_specificity(&self) -> usize {
530        if &*self.segment == "__PAGE__" {
531            return AppPath::from(self.page.clone()).len();
532        }
533
534        let mut specificity = 0;
535
536        for (_, tree) in &self.parallel_routes {
537            specificity = specificity.max(tree.get_specificity());
538        }
539
540        specificity
541    }
542}
543
544#[turbo_tasks::value(transparent)]
545#[derive(Default)]
546pub struct RootParamVecOption(Option<Vec<RcStr>>);
547
548#[turbo_tasks::value_impl]
549impl ValueDefault for RootParamVecOption {
550    #[turbo_tasks::function]
551    fn value_default() -> Vc<Self> {
552        Vc::cell(Default::default())
553    }
554}
555
556#[turbo_tasks::value(transparent)]
557pub struct FileSystemPathVec(Vec<FileSystemPath>);
558
559#[turbo_tasks::value_impl]
560impl ValueDefault for FileSystemPathVec {
561    #[turbo_tasks::function]
562    fn value_default() -> Vc<Self> {
563        Vc::cell(Vec::new())
564    }
565}
566
567#[turbo_tasks::task_input]
568#[derive(Clone, PartialEq, Eq, Hash, TraceRawVcs, ValueDebugFormat, Debug, Encode, Decode)]
569pub enum Entrypoint {
570    AppPage {
571        pages: Vec<AppPage>,
572        loader_tree: ResolvedVc<AppPageLoaderTree>,
573        root_params: ResolvedVc<RootParamVecOption>,
574    },
575    AppRoute {
576        page: AppPage,
577        path: FileSystemPath,
578        root_layouts: ResolvedVc<FileSystemPathVec>,
579        root_params: ResolvedVc<RootParamVecOption>,
580    },
581    AppMetadata {
582        page: AppPage,
583        metadata: MetadataItem,
584        root_params: ResolvedVc<RootParamVecOption>,
585    },
586}
587
588impl Entrypoint {
589    pub fn page(&self) -> &AppPage {
590        match self {
591            Entrypoint::AppPage { pages, .. } => pages.first().unwrap(),
592            Entrypoint::AppRoute { page, .. } => page,
593            Entrypoint::AppMetadata { page, .. } => page,
594        }
595    }
596    pub fn root_params(&self) -> ResolvedVc<RootParamVecOption> {
597        match self {
598            Entrypoint::AppPage { root_params, .. } => *root_params,
599            Entrypoint::AppRoute { root_params, .. } => *root_params,
600            Entrypoint::AppMetadata { root_params, .. } => *root_params,
601        }
602    }
603}
604
605#[turbo_tasks::value(transparent)]
606pub struct Entrypoints(
607    #[bincode(with = "turbo_bincode::indexmap")] FxIndexMap<AppPath, Entrypoint>,
608);
609
610fn is_parallel_route(name: &str) -> bool {
611    name.starts_with('@')
612}
613
614fn is_group_route(name: &str) -> bool {
615    name.starts_with('(') && name.ends_with(')')
616}
617
618/// Returns true if this segment is "transparent" from a URL perspective.
619/// Route groups like `(marketing)` and parallel routes like `@modal` exist in
620/// the file system but don't contribute to the URL path.
621fn is_url_transparent_segment(name: &str) -> bool {
622    is_group_route(name) || is_parallel_route(name)
623}
624
625fn is_dynamic_segment(name: &str) -> bool {
626    name.starts_with('[') && name.ends_with(']')
627}
628
629fn match_parallel_route(name: &str) -> Option<&str> {
630    name.strip_prefix('@')
631}
632
633fn conflict_issue(
634    app_dir: FileSystemPath,
635    e: &'_ OccupiedEntry<'_, AppPath, Entrypoint>,
636    a: &str,
637    b: &str,
638    value_a: &AppPage,
639    value_b: &AppPage,
640) {
641    let item_names = if a == b {
642        format!("{a}s")
643    } else {
644        format!("{a} and {b}")
645    };
646
647    DirectoryTreeIssue {
648        app_dir,
649        message: StyledString::Text(
650            format!(
651                "Conflicting {} at {}: {a} at {value_a} and {b} at {value_b}",
652                item_names,
653                e.key(),
654            )
655            .into(),
656        )
657        .resolved_cell(),
658        severity: IssueSeverity::Error,
659    }
660    .resolved_cell()
661    .emit();
662}
663
664fn add_app_page(
665    app_dir: FileSystemPath,
666    result: &mut FxIndexMap<AppPath, Entrypoint>,
667    page: AppPage,
668    loader_tree: ResolvedVc<AppPageLoaderTree>,
669    root_params: ResolvedVc<RootParamVecOption>,
670) {
671    let mut e = match result.entry(page.clone().into()) {
672        Entry::Occupied(e) => e,
673        Entry::Vacant(e) => {
674            e.insert(Entrypoint::AppPage {
675                pages: vec![page],
676                loader_tree,
677                root_params,
678            });
679            return;
680        }
681    };
682
683    let conflict = |existing_name: &str, existing_page: &AppPage| {
684        conflict_issue(app_dir, &e, "page", existing_name, &page, existing_page);
685    };
686
687    let value = e.get();
688    match value {
689        Entrypoint::AppPage {
690            pages: existing_pages,
691            loader_tree: existing_loader_tree,
692            ..
693        } => {
694            // loader trees should always match for the same path as they are generated by a
695            // turbo tasks function
696            if *existing_loader_tree != loader_tree {
697                conflict("page", existing_pages.first().unwrap());
698            }
699
700            let Entrypoint::AppPage {
701                pages: stored_pages,
702                ..
703            } = e.get_mut()
704            else {
705                unreachable!("Entrypoint::AppPage was already matched");
706            };
707
708            stored_pages.push(page);
709            stored_pages.sort();
710        }
711        Entrypoint::AppRoute {
712            page: existing_page,
713            ..
714        } => {
715            conflict("route", existing_page);
716        }
717        Entrypoint::AppMetadata {
718            page: existing_page,
719            ..
720        } => {
721            conflict("metadata", existing_page);
722        }
723    }
724}
725
726fn add_app_route(
727    app_dir: FileSystemPath,
728    result: &mut FxIndexMap<AppPath, Entrypoint>,
729    page: AppPage,
730    path: FileSystemPath,
731    root_layouts: ResolvedVc<FileSystemPathVec>,
732    root_params: ResolvedVc<RootParamVecOption>,
733) {
734    let e = match result.entry(page.clone().into()) {
735        Entry::Occupied(e) => e,
736        Entry::Vacant(e) => {
737            e.insert(Entrypoint::AppRoute {
738                page,
739                path,
740                root_layouts,
741                root_params,
742            });
743            return;
744        }
745    };
746
747    let conflict = |existing_name: &str, existing_page: &AppPage| {
748        conflict_issue(app_dir, &e, "route", existing_name, &page, existing_page);
749    };
750
751    let value = e.get();
752    match value {
753        Entrypoint::AppPage { pages, .. } => {
754            conflict("page", pages.first().unwrap());
755        }
756        Entrypoint::AppRoute {
757            page: existing_page,
758            ..
759        } => {
760            conflict("route", existing_page);
761        }
762        Entrypoint::AppMetadata {
763            page: existing_page,
764            ..
765        } => {
766            conflict("metadata", existing_page);
767        }
768    }
769}
770
771fn add_app_metadata_route(
772    app_dir: FileSystemPath,
773    result: &mut FxIndexMap<AppPath, Entrypoint>,
774    page: AppPage,
775    metadata: MetadataItem,
776    root_params: ResolvedVc<RootParamVecOption>,
777) {
778    let e = match result.entry(page.clone().into()) {
779        Entry::Occupied(e) => e,
780        Entry::Vacant(e) => {
781            e.insert(Entrypoint::AppMetadata {
782                page,
783                metadata,
784                root_params,
785            });
786            return;
787        }
788    };
789
790    let conflict = |existing_name: &str, existing_page: &AppPage| {
791        conflict_issue(app_dir, &e, "metadata", existing_name, &page, existing_page);
792    };
793
794    let value = e.get();
795    match value {
796        Entrypoint::AppPage { pages, .. } => {
797            conflict("page", pages.first().unwrap());
798        }
799        Entrypoint::AppRoute {
800            page: existing_page,
801            ..
802        } => {
803            conflict("route", existing_page);
804        }
805        Entrypoint::AppMetadata {
806            page: existing_page,
807            ..
808        } => {
809            conflict("metadata", existing_page);
810        }
811    }
812}
813
814#[turbo_tasks::function]
815pub fn get_entrypoints(
816    app_dir: FileSystemPath,
817    page_extensions: Vc<Vec<RcStr>>,
818    is_global_not_found_enabled: Vc<bool>,
819    next_mode: Vc<NextMode>,
820) -> Vc<Entrypoints> {
821    directory_tree_to_entrypoints(
822        app_dir.clone(),
823        get_directory_tree(app_dir.clone(), page_extensions),
824        get_global_metadata(app_dir, page_extensions),
825        is_global_not_found_enabled,
826        next_mode,
827        Default::default(),
828        Default::default(),
829    )
830}
831
832#[turbo_tasks::value(transparent)]
833pub struct CollectedRootParams(#[bincode(with = "turbo_bincode::indexset")] FxIndexSet<RcStr>);
834
835#[turbo_tasks::function]
836pub async fn collect_root_params(
837    entrypoints: ResolvedVc<Entrypoints>,
838) -> Result<Vc<CollectedRootParams>> {
839    let mut collected_root_params = FxIndexSet::<RcStr>::default();
840    for (_, entrypoint) in entrypoints.await?.iter() {
841        if let Some(ref root_params) = *entrypoint.root_params().await? {
842            collected_root_params.extend(root_params.iter().cloned());
843        }
844    }
845    Ok(Vc::cell(collected_root_params))
846}
847
848#[turbo_tasks::function]
849fn directory_tree_to_entrypoints(
850    app_dir: FileSystemPath,
851    directory_tree: Vc<DirectoryTree>,
852    global_metadata: Vc<GlobalMetadata>,
853    is_global_not_found_enabled: Vc<bool>,
854    next_mode: Vc<NextMode>,
855    root_layouts: Vc<FileSystemPathVec>,
856    root_params: Vc<RootParamVecOption>,
857) -> Vc<Entrypoints> {
858    directory_tree_to_entrypoints_internal(
859        app_dir,
860        global_metadata,
861        is_global_not_found_enabled,
862        next_mode,
863        rcstr!(""),
864        directory_tree,
865        AppPage::new(),
866        root_layouts,
867        root_params,
868    )
869}
870
871#[turbo_tasks::value]
872struct DuplicateParallelRouteIssue {
873    app_dir: FileSystemPath,
874    previously_inserted_page: AppPage,
875    page: AppPage,
876}
877
878#[async_trait]
879#[turbo_tasks::value_impl]
880impl Issue for DuplicateParallelRouteIssue {
881    async fn file_path(&self) -> Result<FileSystemPath> {
882        self.app_dir.join(&self.page.to_string())
883    }
884
885    fn stage(&self) -> IssueStage {
886        IssueStage::ProcessModule
887    }
888
889    async fn title(&self) -> Result<StyledString> {
890        Ok(StyledString::Text(
891            format!(
892                "You cannot have two parallel pages that resolve to the same path. Please check \
893                 {} and {}.",
894                self.previously_inserted_page, self.page
895            )
896            .into(),
897        ))
898    }
899}
900
901#[turbo_tasks::value]
902struct MissingRootLayoutIssue {
903    app_dir: FileSystemPath,
904    page_path: FileSystemPath,
905}
906
907#[async_trait]
908#[turbo_tasks::value_impl]
909impl Issue for MissingRootLayoutIssue {
910    async fn file_path(&self) -> Result<FileSystemPath> {
911        Ok(self.page_path.clone())
912    }
913
914    fn stage(&self) -> IssueStage {
915        IssueStage::AppStructure
916    }
917
918    fn severity(&self) -> IssueSeverity {
919        IssueSeverity::Error
920    }
921
922    async fn title(&self) -> Result<StyledString> {
923        let page_path = self
924            .app_dir
925            .get_path_to(&self.page_path)
926            .context("page should be within the app directory")?;
927
928        Ok(StyledString::Text(
929            format!(
930                "{page_path} doesn't have a root layout. To fix this error, make sure every page \
931                 has a root layout."
932            )
933            .into(),
934        ))
935    }
936}
937
938#[turbo_tasks::value]
939struct MissingDefaultParallelRouteIssue {
940    app_dir: FileSystemPath,
941    app_page: AppPage,
942    slot_name: RcStr,
943}
944
945#[turbo_tasks::function]
946fn missing_default_parallel_route_issue(
947    app_dir: FileSystemPath,
948    app_page: AppPage,
949    slot_name: RcStr,
950) -> Vc<MissingDefaultParallelRouteIssue> {
951    MissingDefaultParallelRouteIssue {
952        app_dir,
953        app_page,
954        slot_name,
955    }
956    .cell()
957}
958
959#[async_trait]
960#[turbo_tasks::value_impl]
961impl Issue for MissingDefaultParallelRouteIssue {
962    async fn file_path(&self) -> Result<FileSystemPath> {
963        self.app_dir
964            .join(&self.app_page.to_string())?
965            .join(&format!("@{}", self.slot_name))
966    }
967
968    fn stage(&self) -> IssueStage {
969        IssueStage::AppStructure
970    }
971
972    fn severity(&self) -> IssueSeverity {
973        IssueSeverity::Error
974    }
975
976    async fn title(&self) -> Result<StyledString> {
977        Ok(StyledString::Text(
978            format!(
979                "Missing required default.js file for parallel route at {}/@{}",
980                self.app_page, self.slot_name
981            )
982            .into(),
983        ))
984    }
985
986    async fn description(&self) -> Result<Option<StyledString>> {
987        Ok(Some(StyledString::Stack(vec![
988            StyledString::Text(
989                format!(
990                    "The parallel route slot \"@{}\" is missing a default.js file. When using \
991                     parallel routes, each slot must have a default.js file to serve as a \
992                     fallback.",
993                    self.slot_name
994                )
995                .into(),
996            ),
997            StyledString::Text(
998                format!(
999                    "Create a default.js file at: {}/@{}/default.js",
1000                    self.app_page, self.slot_name
1001                )
1002                .into(),
1003            ),
1004        ])))
1005    }
1006
1007    fn documentation_link(&self) -> RcStr {
1008        rcstr!("https://nextjs.org/docs/messages/slot-missing-default")
1009    }
1010}
1011
1012fn page_path_except_parallel(loader_tree: &AppPageLoaderTree) -> Option<AppPage> {
1013    if loader_tree.page.iter().any(|v| {
1014        matches!(
1015            v,
1016            PageSegment::CatchAll(..)
1017                | PageSegment::OptionalCatchAll(..)
1018                | PageSegment::Parallel(..)
1019        )
1020    }) {
1021        return None;
1022    }
1023
1024    if loader_tree.modules.page.is_some() {
1025        return Some(loader_tree.page.clone());
1026    }
1027
1028    if let Some(children) = loader_tree.parallel_routes.get("children") {
1029        return page_path_except_parallel(children);
1030    }
1031
1032    None
1033}
1034
1035/// Checks if a directory tree has child routes (non-parallel, non-group routes).
1036/// Leaf segments don't need default.js because there are no child routes
1037/// that could cause the parallel slot to unmatch.
1038fn has_child_routes(directory_tree: &PlainDirectoryTree) -> bool {
1039    for (name, subdirectory) in &directory_tree.subdirectories {
1040        // Skip parallel routes (start with '@')
1041        if is_parallel_route(name) {
1042            continue;
1043        }
1044
1045        // Skip route groups, but check if they have pages inside
1046        if is_group_route(name) {
1047            // Recursively check if the group has child routes
1048            if has_child_routes(subdirectory) {
1049                return true;
1050            }
1051            continue;
1052        }
1053
1054        // If we get here, it's a regular route segment (child route)
1055        return true;
1056    }
1057
1058    false
1059}
1060
1061async fn check_duplicate(
1062    duplicate: &mut FxHashMap<AppPath, AppPage>,
1063    loader_tree: &AppPageLoaderTree,
1064    app_dir: FileSystemPath,
1065) -> Result<()> {
1066    let page_path = page_path_except_parallel(loader_tree);
1067
1068    if let Some(page_path) = page_path
1069        && let Some(prev) = duplicate.insert(AppPath::from(page_path.clone()), page_path.clone())
1070        && prev != page_path
1071    {
1072        DuplicateParallelRouteIssue {
1073            app_dir: app_dir.clone(),
1074            previously_inserted_page: prev.clone(),
1075            page: loader_tree.page.clone(),
1076        }
1077        .resolved_cell()
1078        .emit();
1079    }
1080
1081    Ok(())
1082}
1083
1084#[turbo_tasks::value(transparent)]
1085struct AppPageLoaderTreeOption(Option<ResolvedVc<AppPageLoaderTree>>);
1086
1087/// creates the loader tree for a specific route (pathname / [AppPath])
1088#[turbo_tasks::function]
1089async fn directory_tree_to_loader_tree(
1090    app_dir: FileSystemPath,
1091    global_metadata: Vc<GlobalMetadata>,
1092    directory_name: RcStr,
1093    directory_tree: Vc<DirectoryTree>,
1094    app_page: AppPage,
1095    // the page this loader tree is constructed for
1096    for_app_path: AppPath,
1097) -> Result<Vc<AppPageLoaderTreeOption>> {
1098    let plain_tree_vc = directory_tree.into_plain();
1099    let plain_tree = &*plain_tree_vc.await?;
1100
1101    let tree = directory_tree_to_loader_tree_internal(
1102        app_dir,
1103        global_metadata,
1104        directory_name,
1105        plain_tree,
1106        app_page,
1107        for_app_path,
1108        AppDirModules::default(),
1109        Some(&plain_tree.url_tree),
1110    )
1111    .await?;
1112
1113    Ok(Vc::cell(tree.map(AppPageLoaderTree::resolved_cell)))
1114}
1115
1116/// Checks the current module if it needs to be updated with the default page.
1117/// If the module is already set, update the parent module to the same value.
1118/// If the parent module is set and module is not set, set the module to the parent module.
1119/// If the module and the parent module are not set, set them to the default value.
1120///
1121/// # Arguments
1122/// * `app_dir` - The application directory.
1123/// * `module` - The current module to check and update if it is not set.
1124/// * `parent_module` - The parent module to update if the current module is set or both are not
1125///   set.
1126/// * `file_path` - The file path to the default page if neither the current module nor the parent
1127///   module is set.
1128/// * `is_first_layer_group_route` - If true, the module will be overridden with the parent module
1129///   if it is not set.
1130async fn check_and_update_module_references(
1131    app_dir: FileSystemPath,
1132    module: &mut Option<FileSystemPath>,
1133    parent_module: &mut Option<FileSystemPath>,
1134    file_path: &str,
1135    is_first_layer_group_route: bool,
1136) -> Result<()> {
1137    match (module.as_mut(), parent_module.as_mut()) {
1138        // If the module is set, update the parent module to the same value
1139        (Some(module), _) => *parent_module = Some(module.clone()),
1140        // If we are in a first layer group route and we have a parent module, we want to override
1141        // a nonexistent module with the parent module
1142        (None, Some(parent_module)) if is_first_layer_group_route => {
1143            *module = Some(parent_module.clone())
1144        }
1145        // If we are not in a first layer group route, and the module is not set, and the parent
1146        // module is set, we do nothing
1147        (None, Some(_)) => {}
1148        // If the module is not set, and the parent module is not set, we override with the default
1149        // page. This can only happen in the root directory because after this the parent module
1150        // will always be set.
1151        (None, None) => {
1152            let default_page = get_next_package(app_dir).await?.join(file_path)?;
1153            *module = Some(default_page.clone());
1154            *parent_module = Some(default_page);
1155        }
1156    }
1157
1158    Ok(())
1159}
1160
1161/// Checks if the current directory is the root directory and if the module is not set.
1162/// If the module is not set, it will be set to the default page.
1163///
1164/// # Arguments
1165/// * `app_dir` - The application directory.
1166/// * `module` - The module to check and update if it is not set.
1167/// * `file_path` - The file path to the default page if the module is not set.
1168async fn check_and_update_global_module_references(
1169    app_dir: FileSystemPath,
1170    module: &mut Option<FileSystemPath>,
1171    file_path: &str,
1172) -> Result<()> {
1173    if module.is_none() {
1174        *module = Some(get_next_package(app_dir).await?.join(file_path)?);
1175    }
1176
1177    Ok(())
1178}
1179
1180async fn directory_tree_to_loader_tree_internal(
1181    app_dir: FileSystemPath,
1182    global_metadata: Vc<GlobalMetadata>,
1183    directory_name: RcStr,
1184    directory_tree: &PlainDirectoryTree,
1185    app_page: AppPage,
1186    // the page this loader tree is constructed for
1187    for_app_path: AppPath,
1188    mut parent_modules: AppDirModules,
1189    url_tree: Option<&UrlSegmentTree>,
1190) -> Result<Option<AppPageLoaderTree>> {
1191    let app_path = AppPath::from(app_page.clone());
1192
1193    if !for_app_path.contains(&app_path) {
1194        return Ok(None);
1195    }
1196
1197    let mut modules = directory_tree.modules.clone();
1198
1199    // Capture the current page for the metadata to calculate segment relative to
1200    // the corresponding page for the static metadata files.
1201    modules.metadata.base_page = Some(app_page.clone());
1202
1203    // the root directory in the app dir.
1204    let is_root_directory = app_page.is_root();
1205
1206    // If the first layer is a group route, we treat it as root layer
1207    let is_first_layer_group_route = app_page.is_first_layer_group_route();
1208
1209    // Handle the non-global modules that should always be overridden for top level groups or set to
1210    // the default page if they are not set.
1211    if is_root_directory || is_first_layer_group_route {
1212        check_and_update_module_references(
1213            app_dir.clone(),
1214            &mut modules.not_found,
1215            &mut parent_modules.not_found,
1216            "dist/client/components/builtin/not-found.js",
1217            is_first_layer_group_route,
1218        )
1219        .await?;
1220
1221        check_and_update_module_references(
1222            app_dir.clone(),
1223            &mut modules.forbidden,
1224            &mut parent_modules.forbidden,
1225            "dist/client/components/builtin/forbidden.js",
1226            is_first_layer_group_route,
1227        )
1228        .await?;
1229
1230        check_and_update_module_references(
1231            app_dir.clone(),
1232            &mut modules.unauthorized,
1233            &mut parent_modules.unauthorized,
1234            "dist/client/components/builtin/unauthorized.js",
1235            is_first_layer_group_route,
1236        )
1237        .await?;
1238    }
1239
1240    if is_root_directory {
1241        check_and_update_global_module_references(
1242            app_dir.clone(),
1243            &mut modules.global_error,
1244            "dist/client/components/builtin/global-error.js",
1245        )
1246        .await?;
1247    }
1248
1249    // For dynamic segments like [id], find all static siblings at the same URL level.
1250    // This is used by the client to determine if a prefetch can be reused when
1251    // navigating between routes that share the same parent layout.
1252    let static_siblings: Vec<RcStr> = if is_dynamic_segment(&directory_name) {
1253        url_tree
1254            .map(|t| {
1255                t.static_children()
1256                    .into_iter()
1257                    .filter(|s| s != &directory_name)
1258                    .collect()
1259            })
1260            .unwrap_or_default()
1261    } else {
1262        // Static segments don't need sibling info - only dynamic segments use it
1263        Vec::new()
1264    };
1265
1266    let mut tree = AppPageLoaderTree {
1267        page: app_page.clone(),
1268        segment: directory_name.clone(),
1269        parallel_routes: FxIndexMap::default(),
1270        modules: modules.without_leaves(),
1271        global_metadata: global_metadata.to_resolved().await?,
1272        static_siblings,
1273    };
1274
1275    let current_level_is_parallel_route = is_parallel_route(&directory_name);
1276
1277    if current_level_is_parallel_route {
1278        tree.segment = rcstr!("(__SLOT__)");
1279    }
1280
1281    if let Some(page) = (app_path == for_app_path || app_path.is_catchall())
1282        .then_some(modules.page)
1283        .flatten()
1284    {
1285        tree.parallel_routes.insert(
1286            rcstr!("children"),
1287            AppPageLoaderTree {
1288                page: app_page.clone(),
1289                segment: rcstr!("__PAGE__"),
1290                parallel_routes: FxIndexMap::default(),
1291                modules: AppDirModules {
1292                    page: Some(page),
1293                    metadata: modules.metadata,
1294                    ..Default::default()
1295                },
1296                global_metadata: global_metadata.to_resolved().await?,
1297                static_siblings: Vec::new(),
1298            },
1299        );
1300    }
1301
1302    let mut duplicate = FxHashMap::default();
1303
1304    for (subdir_name, subdirectory) in &directory_tree.subdirectories {
1305        let parallel_route_key = match_parallel_route(subdir_name);
1306
1307        let mut child_app_page = app_page.clone();
1308        let mut illegal_path_error = None;
1309
1310        // When constructing the app_page fails (e. g. due to limitations of the order),
1311        // we only want to emit the error when there are actual pages below that
1312        // directory.
1313        if let Err(e) = child_app_page.push_str(&normalize_underscore(subdir_name)) {
1314            illegal_path_error = Some(e);
1315        }
1316
1317        // Root/transparent segments don't consume a URL level; others descend.
1318        let child_url_tree: Option<&UrlSegmentTree> =
1319            if directory_name.is_empty() || is_url_transparent_segment(&directory_name) {
1320                url_tree
1321            } else {
1322                url_tree.and_then(|t| t.get_child(&directory_name))
1323            };
1324
1325        let subtree = Box::pin(directory_tree_to_loader_tree_internal(
1326            app_dir.clone(),
1327            global_metadata,
1328            subdir_name.clone(),
1329            subdirectory,
1330            child_app_page.clone(),
1331            for_app_path.clone(),
1332            parent_modules.clone(),
1333            child_url_tree,
1334        ))
1335        .await?;
1336
1337        if let Some(illegal_path) = subtree.as_ref().and(illegal_path_error) {
1338            return Err(illegal_path);
1339        }
1340
1341        if let Some(subtree) = subtree {
1342            if let Some(key) = parallel_route_key {
1343                // Validate that parallel routes (except "children") have a default.js file.
1344                // This validation matches the webpack loader's logic but is implemented
1345                // differently due to Turbopack's single-pass recursive processing.
1346
1347                // Check if we're inside a catch-all route (i.e., the parallel route is a child
1348                // of a catch-all segment). Only skip validation if the slot is UNDER a catch-all.
1349                // For example:
1350                //   /[...catchAll]/@slot - is_inside_catchall = true (skip validation) ✓
1351                //   /@slot/[...catchAll] - is_inside_catchall = false (require default) ✓
1352                // The catch-all provides fallback behavior, so default.js is not required.
1353                let is_inside_catchall = app_page.is_catchall();
1354
1355                // Check if this is a leaf segment (no child routes).
1356                // Leaf segments don't need default.js because there are no child routes
1357                // that could cause the parallel slot to unmatch. For example:
1358                //   /repo-overview/@slot/page with no child routes - is_leaf_segment = true (skip
1359                // validation) ✓   /repo-overview/@slot/page with
1360                // /repo-overview/child/page - is_leaf_segment = false (require default) ✓
1361                // This also handles route groups correctly by filtering them out.
1362                let is_leaf_segment = !has_child_routes(directory_tree);
1363
1364                // Turbopack-specific: Check if the parallel slot has matching child routes.
1365                // In webpack, this is checked implicitly via the two-phase processing:
1366                // slots with content are processed first and skip validation in the second phase.
1367                // In Turbopack's single-pass approach, we check directly if the slot has child
1368                // routes. If the slot has child routes that match the parent's
1369                // child routes, it can render content for those routes and doesn't
1370                // need a default. For example:
1371                //   /parent/@slot/page + /parent/@slot/child + /parent/child - slot_has_children =
1372                // true (skip validation) ✓   /parent/@slot/page + /parent/child (no
1373                // @slot/child) - slot_has_children = false (require default) ✓
1374                let slot_has_children = has_child_routes(subdirectory);
1375
1376                if key != "children"
1377                    && subdirectory.modules.default.is_none()
1378                    && !is_inside_catchall
1379                    && !is_leaf_segment
1380                    && !slot_has_children
1381                {
1382                    missing_default_parallel_route_issue(
1383                        app_dir.clone(),
1384                        app_page.clone(),
1385                        key.into(),
1386                    )
1387                    .to_resolved()
1388                    .await?
1389                    .emit();
1390                }
1391
1392                tree.parallel_routes.insert(key.into(), subtree);
1393                continue;
1394            }
1395
1396            // skip groups which don't have a page match.
1397            if is_group_route(subdir_name) && !subtree.has_page() {
1398                continue;
1399            }
1400
1401            if subtree.has_page() {
1402                check_duplicate(&mut duplicate, &subtree, app_dir.clone()).await?;
1403            }
1404
1405            if let Some(current_tree) = tree.parallel_routes.get("children") {
1406                if current_tree.has_only_catchall()
1407                    && (!subtree.has_only_catchall()
1408                        || current_tree.get_specificity() < subtree.get_specificity())
1409                {
1410                    tree.parallel_routes
1411                        .insert(rcstr!("children"), subtree.clone());
1412                }
1413            } else {
1414                tree.parallel_routes.insert(rcstr!("children"), subtree);
1415            }
1416        } else if let Some(key) = parallel_route_key {
1417            bail!(
1418                "missing page or default for parallel route `{}` (page: {})",
1419                key,
1420                app_page
1421            );
1422        }
1423    }
1424
1425    // make sure we don't have a match for other slots if there's an intercepting route match
1426    // we only check subtrees as the current level could trigger `is_intercepting`
1427    if tree
1428        .parallel_routes
1429        .iter()
1430        .any(|(_, parallel_tree)| parallel_tree.is_intercepting())
1431    {
1432        let mut keys_to_replace = Vec::new();
1433
1434        for (key, parallel_tree) in &tree.parallel_routes {
1435            if !parallel_tree.is_intercepting() {
1436                keys_to_replace.push(key.clone());
1437            }
1438        }
1439
1440        for key in keys_to_replace {
1441            let subdir_name: RcStr = format!("@{key}").into();
1442
1443            let default = if key == "children" {
1444                modules.default.clone()
1445            } else if let Some(subdirectory) = directory_tree.subdirectories.get(&subdir_name) {
1446                subdirectory.modules.default.clone()
1447            } else {
1448                None
1449            };
1450
1451            let is_inside_catchall = app_page.is_catchall();
1452
1453            // Check if this is a leaf segment (no child routes).
1454            let is_leaf_segment = !has_child_routes(directory_tree);
1455
1456            // Only emit the issue if this is not the children slot and there's no default
1457            // component. The children slot is implicit and doesn't require a default.js
1458            // file. Also skip validation if the slot is UNDER a catch-all route or if
1459            // this is a leaf segment (no child routes).
1460            if default.is_none() && key != "children" && !is_inside_catchall && !is_leaf_segment {
1461                missing_default_parallel_route_issue(
1462                    app_dir.clone(),
1463                    app_page.clone(),
1464                    key.clone(),
1465                )
1466                .to_resolved()
1467                .await?
1468                .emit();
1469            }
1470
1471            tree.parallel_routes.insert(
1472                key.clone(),
1473                default_route_tree(
1474                    app_dir.clone(),
1475                    global_metadata,
1476                    app_page.clone(),
1477                    default,
1478                    key.clone(),
1479                    for_app_path.clone(),
1480                )
1481                .await?,
1482            );
1483        }
1484    }
1485
1486    if tree.parallel_routes.is_empty() {
1487        if modules.default.is_some() || current_level_is_parallel_route {
1488            tree = default_route_tree(
1489                app_dir.clone(),
1490                global_metadata,
1491                app_page.clone(),
1492                modules.default.clone(),
1493                rcstr!("children"),
1494                for_app_path.clone(),
1495            )
1496            .await?;
1497        } else {
1498            return Ok(None);
1499        }
1500    } else if tree.parallel_routes.get("children").is_none() {
1501        tree.parallel_routes.insert(
1502            rcstr!("children"),
1503            default_route_tree(
1504                app_dir.clone(),
1505                global_metadata,
1506                app_page.clone(),
1507                modules.default.clone(),
1508                rcstr!("children"),
1509                for_app_path.clone(),
1510            )
1511            .await?,
1512        );
1513    }
1514
1515    Ok(Some(tree))
1516}
1517
1518async fn default_route_tree(
1519    app_dir: FileSystemPath,
1520    global_metadata: Vc<GlobalMetadata>,
1521    app_page: AppPage,
1522    default_component: Option<FileSystemPath>,
1523    slot_name: RcStr,
1524    for_app_path: AppPath,
1525) -> Result<AppPageLoaderTree> {
1526    Ok(AppPageLoaderTree {
1527        page: app_page.clone(),
1528        segment: rcstr!("__DEFAULT__"),
1529        parallel_routes: FxIndexMap::default(),
1530        modules: if let Some(default) = default_component {
1531            AppDirModules {
1532                default: Some(default),
1533                ..Default::default()
1534            }
1535        } else {
1536            let contains_interception = for_app_path.contains_interception();
1537
1538            let default_file = if contains_interception && slot_name == "children" {
1539                "dist/client/components/builtin/default-null.js"
1540            } else {
1541                "dist/client/components/builtin/default.js"
1542            };
1543
1544            AppDirModules {
1545                default: Some(get_next_package(app_dir).await?.join(default_file)?),
1546                ..Default::default()
1547            }
1548        },
1549        global_metadata: global_metadata.to_resolved().await?,
1550        static_siblings: Vec::new(),
1551    })
1552}
1553
1554#[turbo_tasks::function]
1555async fn directory_tree_to_entrypoints_internal(
1556    app_dir: FileSystemPath,
1557    global_metadata: ResolvedVc<GlobalMetadata>,
1558    is_global_not_found_enabled: Vc<bool>,
1559    next_mode: Vc<NextMode>,
1560    directory_name: RcStr,
1561    directory_tree: Vc<DirectoryTree>,
1562    app_page: AppPage,
1563    root_layouts: ResolvedVc<FileSystemPathVec>,
1564    root_params: ResolvedVc<RootParamVecOption>,
1565) -> Result<Vc<Entrypoints>> {
1566    let span = tracing::info_span!("build layout trees", name = display(&app_page));
1567    directory_tree_to_entrypoints_internal_untraced(
1568        app_dir,
1569        global_metadata,
1570        is_global_not_found_enabled,
1571        next_mode,
1572        directory_name,
1573        directory_tree,
1574        app_page,
1575        root_layouts,
1576        root_params,
1577    )
1578    .instrument(span)
1579    .await
1580}
1581
1582async fn directory_tree_to_entrypoints_internal_untraced(
1583    app_dir: FileSystemPath,
1584    global_metadata: ResolvedVc<GlobalMetadata>,
1585    is_global_not_found_enabled: Vc<bool>,
1586    next_mode: Vc<NextMode>,
1587    directory_name: RcStr,
1588    directory_tree: Vc<DirectoryTree>,
1589    app_page: AppPage,
1590    root_layouts: ResolvedVc<FileSystemPathVec>,
1591    root_params: ResolvedVc<RootParamVecOption>,
1592) -> Result<Vc<Entrypoints>> {
1593    let mut result = FxIndexMap::default();
1594
1595    let directory_tree_vc = directory_tree;
1596    let directory_tree = &*directory_tree.await?;
1597
1598    let subdirectories = &directory_tree.subdirectories;
1599    let modules = &directory_tree.modules;
1600    // Route can have its own segment config, also can inherit from the layout root
1601    // segment config. https://nextjs.org/docs/app/building-your-application/rendering/edge-and-nodejs-runtimes#segment-runtime-option
1602    // Pass down layouts from each tree to apply segment config when adding route.
1603    let root_layouts = if let Some(layout) = &modules.layout {
1604        let mut layouts = root_layouts.owned().await?;
1605        layouts.push(layout.clone());
1606        ResolvedVc::cell(layouts)
1607    } else {
1608        root_layouts
1609    };
1610
1611    // TODO: `root_layouts` is a misnomer, they're just parent layouts
1612    let root_params = if root_params.await?.is_none() && (*root_layouts.await?).len() == 1 {
1613        // found a root layout. the params up-to-and-including this point are the root params
1614        // for all child segments
1615        ResolvedVc::cell(Some(
1616            app_page
1617                .0
1618                .iter()
1619                .filter_map(|segment| match segment {
1620                    PageSegment::Dynamic(param)
1621                    | PageSegment::CatchAll(param)
1622                    | PageSegment::OptionalCatchAll(param) => Some(param.clone()),
1623                    _ => None,
1624                })
1625                .collect::<Vec<RcStr>>(),
1626        ))
1627    } else {
1628        root_params
1629    };
1630
1631    if let Some(page_path) = &modules.page {
1632        if root_layouts.await?.is_empty() {
1633            MissingRootLayoutIssue {
1634                app_dir: app_dir.clone(),
1635                page_path: page_path.clone(),
1636            }
1637            .resolved_cell()
1638            .emit();
1639        }
1640
1641        let app_path = AppPath::from(app_page.clone());
1642
1643        let loader_tree = *directory_tree_to_loader_tree(
1644            app_dir.clone(),
1645            *global_metadata,
1646            directory_name.clone(),
1647            directory_tree_vc,
1648            app_page.clone(),
1649            app_path,
1650        )
1651        .await?;
1652
1653        add_app_page(
1654            app_dir.clone(),
1655            &mut result,
1656            app_page.complete(PageType::Page)?,
1657            loader_tree.context("loader tree should be created for a page/default")?,
1658            root_params,
1659        );
1660    }
1661
1662    if let Some(route) = &modules.route {
1663        add_app_route(
1664            app_dir.clone(),
1665            &mut result,
1666            app_page.complete(PageType::Route)?,
1667            route.clone(),
1668            root_layouts,
1669            root_params,
1670        );
1671    }
1672
1673    let Metadata {
1674        icon,
1675        apple,
1676        twitter,
1677        open_graph,
1678        sitemap,
1679        base_page: _,
1680    } = &modules.metadata;
1681
1682    for meta in sitemap
1683        .iter()
1684        .cloned()
1685        .chain(icon.iter().cloned().map(MetadataItem::from))
1686        .chain(apple.iter().cloned().map(MetadataItem::from))
1687        .chain(twitter.iter().cloned().map(MetadataItem::from))
1688        .chain(open_graph.iter().cloned().map(MetadataItem::from))
1689    {
1690        let app_page = app_page.clone_push_str(&get_metadata_route_name(meta.clone()).await?)?;
1691
1692        add_app_metadata_route(
1693            app_dir.clone(),
1694            &mut result,
1695            normalize_metadata_route(app_page)?,
1696            meta,
1697            root_params,
1698        );
1699    }
1700
1701    // root path: /
1702    if app_page.is_root() {
1703        let GlobalMetadata {
1704            favicon,
1705            robots,
1706            manifest,
1707        } = &*global_metadata.await?;
1708
1709        for meta in favicon.iter().chain(robots.iter()).chain(manifest.iter()) {
1710            let app_page =
1711                app_page.clone_push_str(&get_metadata_route_name(meta.clone()).await?)?;
1712
1713            add_app_metadata_route(
1714                app_dir.clone(),
1715                &mut result,
1716                normalize_metadata_route(app_page)?,
1717                meta.clone(),
1718                root_params,
1719            );
1720        }
1721
1722        let mut modules = directory_tree.modules.clone();
1723
1724        // fill in the default modules for the not-found entrypoint
1725        if modules.layout.is_none() {
1726            modules.layout = Some(
1727                get_next_package(app_dir.clone())
1728                    .await?
1729                    .join("dist/client/components/builtin/layout.js")?,
1730            );
1731        }
1732
1733        if modules.not_found.is_none() {
1734            modules.not_found = Some(
1735                get_next_package(app_dir.clone())
1736                    .await?
1737                    .join("dist/client/components/builtin/not-found.js")?,
1738            );
1739        }
1740        if modules.forbidden.is_none() {
1741            modules.forbidden = Some(
1742                get_next_package(app_dir.clone())
1743                    .await?
1744                    .join("dist/client/components/builtin/forbidden.js")?,
1745            );
1746        }
1747        if modules.unauthorized.is_none() {
1748            modules.unauthorized = Some(
1749                get_next_package(app_dir.clone())
1750                    .await?
1751                    .join("dist/client/components/builtin/unauthorized.js")?,
1752            );
1753        }
1754        if modules.global_error.is_none() {
1755            modules.global_error = Some(
1756                get_next_package(app_dir.clone())
1757                    .await?
1758                    .join("dist/client/components/builtin/global-error.js")?,
1759            );
1760        }
1761
1762        // Next.js has this logic in "collect-app-paths", where the root not-found page
1763        // is considered as its own entry point.
1764
1765        // Determine if we enable the global not-found feature.
1766        let is_global_not_found_enabled = *is_global_not_found_enabled.await?;
1767        let use_global_not_found =
1768            is_global_not_found_enabled || modules.global_not_found.is_some();
1769
1770        let not_found_root_modules = modules.without_leaves();
1771        let not_found_tree = AppPageLoaderTree {
1772            page: app_page.clone(),
1773            segment: directory_name.clone(),
1774            parallel_routes: fxindexmap! {
1775                rcstr!("children") => AppPageLoaderTree {
1776                    page: app_page.clone(),
1777                    segment: rcstr!("/_not-found"),
1778                    parallel_routes: fxindexmap! {
1779                        rcstr!("children") => AppPageLoaderTree {
1780                            page: app_page.clone(),
1781                            segment: rcstr!("__PAGE__"),
1782                            parallel_routes: FxIndexMap::default(),
1783                            modules: if use_global_not_found {
1784                                // if global-not-found.js is present:
1785                                // leaf module only keeps page pointing to empty-stub
1786                                AppDirModules {
1787                                    // page is built-in/empty-stub
1788                                    page: Some(get_next_package(app_dir.clone())
1789                                        .await?
1790                                        .join("dist/client/components/builtin/empty-stub.js")?,
1791                                    ),
1792                                    ..Default::default()
1793                                }
1794                            } else {
1795                                // if global-not-found.js is not present:
1796                                // we search if we can compose root layout with the root not-found.js;
1797                                AppDirModules {
1798                                    page: match modules.not_found {
1799                                        Some(v) => Some(v),
1800                                        None => Some(get_next_package(app_dir.clone())
1801                                            .await?
1802                                            .join("dist/client/components/builtin/not-found.js")?,
1803                                        ),
1804                                    },
1805                                    ..Default::default()
1806                                }
1807                            },
1808                            global_metadata,
1809                            static_siblings: Vec::new(),
1810                        }
1811                    },
1812                    modules: AppDirModules {
1813                        ..Default::default()
1814                    },
1815                    global_metadata,
1816                    static_siblings: Vec::new(),
1817                },
1818            },
1819            modules: AppDirModules {
1820                // `global-not-found.js` does not need a layout since it's included.
1821                // Skip it if it's present.
1822                // Otherwise, we need to compose it with the root layout to compose with
1823                // not-found.js boundary.
1824                layout: if use_global_not_found {
1825                    match modules.global_not_found {
1826                        Some(v) => Some(v),
1827                        None => Some(
1828                            get_next_package(app_dir.clone())
1829                                .await?
1830                                .join("dist/client/components/builtin/global-not-found.js")?,
1831                        ),
1832                    }
1833                } else {
1834                    modules.layout
1835                },
1836                ..not_found_root_modules
1837            },
1838            global_metadata,
1839            static_siblings: Vec::new(),
1840        }
1841        .resolved_cell();
1842
1843        {
1844            let app_page = app_page
1845                .clone_push_str("_not-found")?
1846                .complete(PageType::Page)?;
1847
1848            add_app_page(
1849                app_dir.clone(),
1850                &mut result,
1851                app_page,
1852                not_found_tree,
1853                root_params,
1854            );
1855        }
1856
1857        // Create production global error page only in build mode
1858        // This aligns with webpack: default Pages entries (including /_error) are only added when
1859        // the build isn't app-only. If the build is app-only (no user pages/api), we should still
1860        // expose the app global error so runtime errors render, but we shouldn't emit it otherwise.
1861        if matches!(*next_mode.await?, NextMode::Build) {
1862            // Create a `_global-error/page` route using user's global-error.js or built-in
1863            // fallback.
1864            let next_package = get_next_package(app_dir.clone()).await?;
1865            let global_error_tree = AppPageLoaderTree {
1866                page: app_page.clone(),
1867                segment: directory_name.clone(),
1868                parallel_routes: fxindexmap! {
1869                    rcstr!("children") => AppPageLoaderTree {
1870                        page: app_page.clone(),
1871                        segment: rcstr!("__PAGE__"),
1872                        parallel_routes: FxIndexMap::default(),
1873                        modules: AppDirModules {
1874                            page: Some(next_package
1875                                .join("dist/client/components/builtin/app-error.js")?),
1876                            ..Default::default()
1877                        },
1878                        global_metadata,
1879                        static_siblings: Vec::new(),
1880                    }
1881                },
1882                // global-error is needed for getGlobalErrorStyles to work during rendering.
1883                // Use user's custom global-error if defined, otherwise builtin fallback.
1884                modules: AppDirModules {
1885                    global_error: modules.global_error.clone(),
1886                    ..Default::default()
1887                },
1888                global_metadata,
1889                static_siblings: Vec::new(),
1890            }
1891            .resolved_cell();
1892
1893            let app_global_error_page = app_page
1894                .clone_push_str("_global-error")?
1895                .complete(PageType::Page)?;
1896            add_app_page(
1897                app_dir.clone(),
1898                &mut result,
1899                app_global_error_page,
1900                global_error_tree,
1901                root_params,
1902            );
1903        }
1904    }
1905
1906    let app_page = &app_page;
1907    let directory_name = &directory_name;
1908    let subdirectories = subdirectories
1909        .iter()
1910        .map(|(subdir_name, &subdirectory)| {
1911            let app_dir = app_dir.clone();
1912
1913            async move {
1914                let mut child_app_page = app_page.clone();
1915                let mut illegal_path = None;
1916
1917                // When constructing the app_page fails (e. g. due to limitations of the order),
1918                // we only want to emit the error when there are actual pages below that
1919                // directory.
1920                if let Err(e) = child_app_page.push_str(&normalize_underscore(subdir_name)) {
1921                    illegal_path = Some(e);
1922                }
1923
1924                let map = directory_tree_to_entrypoints_internal(
1925                    app_dir.clone(),
1926                    *global_metadata,
1927                    is_global_not_found_enabled,
1928                    next_mode,
1929                    subdir_name.clone(),
1930                    *subdirectory,
1931                    child_app_page.clone(),
1932                    *root_layouts,
1933                    *root_params,
1934                )
1935                .await?;
1936
1937                if let Some(illegal_path) = illegal_path
1938                    && !map.is_empty()
1939                {
1940                    return Err(illegal_path);
1941                }
1942
1943                let mut loader_trees = Vec::new();
1944
1945                for (_, entrypoint) in map.iter() {
1946                    if let Entrypoint::AppPage { ref pages, .. } = *entrypoint {
1947                        for page in pages {
1948                            let app_path = AppPath::from(page.clone());
1949
1950                            let loader_tree = directory_tree_to_loader_tree(
1951                                app_dir.clone(),
1952                                *global_metadata,
1953                                directory_name.clone(),
1954                                directory_tree_vc,
1955                                app_page.clone(),
1956                                app_path,
1957                            );
1958                            loader_trees.push(loader_tree);
1959                        }
1960                    }
1961                }
1962                Ok((map, loader_trees))
1963            }
1964        })
1965        .try_join()
1966        .await?;
1967
1968    for (map, loader_trees) in subdirectories.iter() {
1969        let mut i = 0;
1970        for (_, entrypoint) in map.iter() {
1971            match entrypoint {
1972                Entrypoint::AppPage {
1973                    pages,
1974                    loader_tree: _,
1975                    root_params,
1976                } => {
1977                    for page in pages {
1978                        let loader_tree = *loader_trees[i].await?;
1979                        i += 1;
1980
1981                        add_app_page(
1982                            app_dir.clone(),
1983                            &mut result,
1984                            page.clone(),
1985                            loader_tree
1986                                .context("loader tree should be created for a page/default")?,
1987                            *root_params,
1988                        );
1989                    }
1990                }
1991                Entrypoint::AppRoute {
1992                    page,
1993                    path,
1994                    root_layouts,
1995                    root_params,
1996                } => {
1997                    add_app_route(
1998                        app_dir.clone(),
1999                        &mut result,
2000                        page.clone(),
2001                        path.clone(),
2002                        *root_layouts,
2003                        *root_params,
2004                    );
2005                }
2006                Entrypoint::AppMetadata {
2007                    page,
2008                    metadata,
2009                    root_params,
2010                } => {
2011                    add_app_metadata_route(
2012                        app_dir.clone(),
2013                        &mut result,
2014                        page.clone(),
2015                        metadata.clone(),
2016                        *root_params,
2017                    );
2018                }
2019            }
2020        }
2021    }
2022    Ok(Vc::cell(result))
2023}
2024
2025/// Returns the global metadata for an app directory.
2026#[turbo_tasks::function]
2027pub async fn get_global_metadata(
2028    app_dir: FileSystemPath,
2029    page_extensions: Vc<Vec<RcStr>>,
2030) -> Result<Vc<GlobalMetadata>> {
2031    let DirectoryContent::Entries(entries) = &*app_dir.read_dir().await? else {
2032        bail!("app_dir must be a directory")
2033    };
2034    let mut metadata = GlobalMetadata::default();
2035
2036    for (basename, entry) in entries {
2037        let DirectoryEntry::File(file) = entry else {
2038            continue;
2039        };
2040
2041        let Some(GlobalMetadataFileMatch {
2042            metadata_type,
2043            dynamic,
2044        }) = match_global_metadata_file(basename, &page_extensions.await?)
2045        else {
2046            continue;
2047        };
2048
2049        let entry = match metadata_type {
2050            "favicon" => &mut metadata.favicon,
2051            "manifest" => &mut metadata.manifest,
2052            "robots" => &mut metadata.robots,
2053            _ => continue,
2054        };
2055
2056        if dynamic {
2057            *entry = Some(MetadataItem::Dynamic { path: file.clone() });
2058        } else {
2059            *entry = Some(MetadataItem::Static { path: file.clone() });
2060        }
2061        // TODO(WEB-952) handle symlinks in app dir
2062    }
2063
2064    Ok(metadata.cell())
2065}
2066
2067#[turbo_tasks::value(shared)]
2068struct DirectoryTreeIssue {
2069    pub severity: IssueSeverity,
2070    pub app_dir: FileSystemPath,
2071    pub message: ResolvedVc<StyledString>,
2072}
2073
2074#[async_trait]
2075#[turbo_tasks::value_impl]
2076impl Issue for DirectoryTreeIssue {
2077    fn severity(&self) -> IssueSeverity {
2078        self.severity
2079    }
2080
2081    async fn title(&self) -> Result<StyledString> {
2082        Ok(StyledString::Text(rcstr!(
2083            "An issue occurred while preparing your Next.js app"
2084        )))
2085    }
2086
2087    fn stage(&self) -> IssueStage {
2088        IssueStage::AppStructure
2089    }
2090
2091    async fn file_path(&self) -> Result<FileSystemPath> {
2092        Ok(self.app_dir.clone())
2093    }
2094
2095    async fn description(&self) -> Result<Option<StyledString>> {
2096        Ok(Some((*self.message.await?).clone()))
2097    }
2098}