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 MissingDefaultParallelRouteIssue {
903    app_dir: FileSystemPath,
904    app_page: AppPage,
905    slot_name: RcStr,
906}
907
908#[turbo_tasks::function]
909fn missing_default_parallel_route_issue(
910    app_dir: FileSystemPath,
911    app_page: AppPage,
912    slot_name: RcStr,
913) -> Vc<MissingDefaultParallelRouteIssue> {
914    MissingDefaultParallelRouteIssue {
915        app_dir,
916        app_page,
917        slot_name,
918    }
919    .cell()
920}
921
922#[async_trait]
923#[turbo_tasks::value_impl]
924impl Issue for MissingDefaultParallelRouteIssue {
925    async fn file_path(&self) -> Result<FileSystemPath> {
926        self.app_dir
927            .join(&self.app_page.to_string())?
928            .join(&format!("@{}", self.slot_name))
929    }
930
931    fn stage(&self) -> IssueStage {
932        IssueStage::AppStructure
933    }
934
935    fn severity(&self) -> IssueSeverity {
936        IssueSeverity::Error
937    }
938
939    async fn title(&self) -> Result<StyledString> {
940        Ok(StyledString::Text(
941            format!(
942                "Missing required default.js file for parallel route at {}/@{}",
943                self.app_page, self.slot_name
944            )
945            .into(),
946        ))
947    }
948
949    async fn description(&self) -> Result<Option<StyledString>> {
950        Ok(Some(StyledString::Stack(vec![
951            StyledString::Text(
952                format!(
953                    "The parallel route slot \"@{}\" is missing a default.js file. When using \
954                     parallel routes, each slot must have a default.js file to serve as a \
955                     fallback.",
956                    self.slot_name
957                )
958                .into(),
959            ),
960            StyledString::Text(
961                format!(
962                    "Create a default.js file at: {}/@{}/default.js",
963                    self.app_page, self.slot_name
964                )
965                .into(),
966            ),
967        ])))
968    }
969
970    fn documentation_link(&self) -> RcStr {
971        rcstr!("https://nextjs.org/docs/messages/slot-missing-default")
972    }
973}
974
975fn page_path_except_parallel(loader_tree: &AppPageLoaderTree) -> Option<AppPage> {
976    if loader_tree.page.iter().any(|v| {
977        matches!(
978            v,
979            PageSegment::CatchAll(..)
980                | PageSegment::OptionalCatchAll(..)
981                | PageSegment::Parallel(..)
982        )
983    }) {
984        return None;
985    }
986
987    if loader_tree.modules.page.is_some() {
988        return Some(loader_tree.page.clone());
989    }
990
991    if let Some(children) = loader_tree.parallel_routes.get("children") {
992        return page_path_except_parallel(children);
993    }
994
995    None
996}
997
998/// Checks if a directory tree has child routes (non-parallel, non-group routes).
999/// Leaf segments don't need default.js because there are no child routes
1000/// that could cause the parallel slot to unmatch.
1001fn has_child_routes(directory_tree: &PlainDirectoryTree) -> bool {
1002    for (name, subdirectory) in &directory_tree.subdirectories {
1003        // Skip parallel routes (start with '@')
1004        if is_parallel_route(name) {
1005            continue;
1006        }
1007
1008        // Skip route groups, but check if they have pages inside
1009        if is_group_route(name) {
1010            // Recursively check if the group has child routes
1011            if has_child_routes(subdirectory) {
1012                return true;
1013            }
1014            continue;
1015        }
1016
1017        // If we get here, it's a regular route segment (child route)
1018        return true;
1019    }
1020
1021    false
1022}
1023
1024async fn check_duplicate(
1025    duplicate: &mut FxHashMap<AppPath, AppPage>,
1026    loader_tree: &AppPageLoaderTree,
1027    app_dir: FileSystemPath,
1028) -> Result<()> {
1029    let page_path = page_path_except_parallel(loader_tree);
1030
1031    if let Some(page_path) = page_path
1032        && let Some(prev) = duplicate.insert(AppPath::from(page_path.clone()), page_path.clone())
1033        && prev != page_path
1034    {
1035        DuplicateParallelRouteIssue {
1036            app_dir: app_dir.clone(),
1037            previously_inserted_page: prev.clone(),
1038            page: loader_tree.page.clone(),
1039        }
1040        .resolved_cell()
1041        .emit();
1042    }
1043
1044    Ok(())
1045}
1046
1047#[turbo_tasks::value(transparent)]
1048struct AppPageLoaderTreeOption(Option<ResolvedVc<AppPageLoaderTree>>);
1049
1050/// creates the loader tree for a specific route (pathname / [AppPath])
1051#[turbo_tasks::function]
1052async fn directory_tree_to_loader_tree(
1053    app_dir: FileSystemPath,
1054    global_metadata: Vc<GlobalMetadata>,
1055    directory_name: RcStr,
1056    directory_tree: Vc<DirectoryTree>,
1057    app_page: AppPage,
1058    // the page this loader tree is constructed for
1059    for_app_path: AppPath,
1060) -> Result<Vc<AppPageLoaderTreeOption>> {
1061    let plain_tree_vc = directory_tree.into_plain();
1062    let plain_tree = &*plain_tree_vc.await?;
1063
1064    let tree = directory_tree_to_loader_tree_internal(
1065        app_dir,
1066        global_metadata,
1067        directory_name,
1068        plain_tree,
1069        app_page,
1070        for_app_path,
1071        AppDirModules::default(),
1072        Some(&plain_tree.url_tree),
1073    )
1074    .await?;
1075
1076    Ok(Vc::cell(tree.map(AppPageLoaderTree::resolved_cell)))
1077}
1078
1079/// Checks the current module if it needs to be updated with the default page.
1080/// If the module is already set, update the parent module to the same value.
1081/// If the parent module is set and module is not set, set the module to the parent module.
1082/// If the module and the parent module are not set, set them to the default value.
1083///
1084/// # Arguments
1085/// * `app_dir` - The application directory.
1086/// * `module` - The current module to check and update if it is not set.
1087/// * `parent_module` - The parent module to update if the current module is set or both are not
1088///   set.
1089/// * `file_path` - The file path to the default page if neither the current module nor the parent
1090///   module is set.
1091/// * `is_first_layer_group_route` - If true, the module will be overridden with the parent module
1092///   if it is not set.
1093async fn check_and_update_module_references(
1094    app_dir: FileSystemPath,
1095    module: &mut Option<FileSystemPath>,
1096    parent_module: &mut Option<FileSystemPath>,
1097    file_path: &str,
1098    is_first_layer_group_route: bool,
1099) -> Result<()> {
1100    match (module.as_mut(), parent_module.as_mut()) {
1101        // If the module is set, update the parent module to the same value
1102        (Some(module), _) => *parent_module = Some(module.clone()),
1103        // If we are in a first layer group route and we have a parent module, we want to override
1104        // a nonexistent module with the parent module
1105        (None, Some(parent_module)) if is_first_layer_group_route => {
1106            *module = Some(parent_module.clone())
1107        }
1108        // If we are not in a first layer group route, and the module is not set, and the parent
1109        // module is set, we do nothing
1110        (None, Some(_)) => {}
1111        // If the module is not set, and the parent module is not set, we override with the default
1112        // page. This can only happen in the root directory because after this the parent module
1113        // will always be set.
1114        (None, None) => {
1115            let default_page = get_next_package(app_dir).await?.join(file_path)?;
1116            *module = Some(default_page.clone());
1117            *parent_module = Some(default_page);
1118        }
1119    }
1120
1121    Ok(())
1122}
1123
1124/// Checks if the current directory is the root directory and if the module is not set.
1125/// If the module is not set, it will be set to the default page.
1126///
1127/// # Arguments
1128/// * `app_dir` - The application directory.
1129/// * `module` - The module to check and update if it is not set.
1130/// * `file_path` - The file path to the default page if the module is not set.
1131async fn check_and_update_global_module_references(
1132    app_dir: FileSystemPath,
1133    module: &mut Option<FileSystemPath>,
1134    file_path: &str,
1135) -> Result<()> {
1136    if module.is_none() {
1137        *module = Some(get_next_package(app_dir).await?.join(file_path)?);
1138    }
1139
1140    Ok(())
1141}
1142
1143async fn directory_tree_to_loader_tree_internal(
1144    app_dir: FileSystemPath,
1145    global_metadata: Vc<GlobalMetadata>,
1146    directory_name: RcStr,
1147    directory_tree: &PlainDirectoryTree,
1148    app_page: AppPage,
1149    // the page this loader tree is constructed for
1150    for_app_path: AppPath,
1151    mut parent_modules: AppDirModules,
1152    url_tree: Option<&UrlSegmentTree>,
1153) -> Result<Option<AppPageLoaderTree>> {
1154    let app_path = AppPath::from(app_page.clone());
1155
1156    if !for_app_path.contains(&app_path) {
1157        return Ok(None);
1158    }
1159
1160    let mut modules = directory_tree.modules.clone();
1161
1162    // Capture the current page for the metadata to calculate segment relative to
1163    // the corresponding page for the static metadata files.
1164    modules.metadata.base_page = Some(app_page.clone());
1165
1166    // the root directory in the app dir.
1167    let is_root_directory = app_page.is_root();
1168
1169    // If the first layer is a group route, we treat it as root layer
1170    let is_first_layer_group_route = app_page.is_first_layer_group_route();
1171
1172    // Handle the non-global modules that should always be overridden for top level groups or set to
1173    // the default page if they are not set.
1174    if is_root_directory || is_first_layer_group_route {
1175        check_and_update_module_references(
1176            app_dir.clone(),
1177            &mut modules.not_found,
1178            &mut parent_modules.not_found,
1179            "dist/client/components/builtin/not-found.js",
1180            is_first_layer_group_route,
1181        )
1182        .await?;
1183
1184        check_and_update_module_references(
1185            app_dir.clone(),
1186            &mut modules.forbidden,
1187            &mut parent_modules.forbidden,
1188            "dist/client/components/builtin/forbidden.js",
1189            is_first_layer_group_route,
1190        )
1191        .await?;
1192
1193        check_and_update_module_references(
1194            app_dir.clone(),
1195            &mut modules.unauthorized,
1196            &mut parent_modules.unauthorized,
1197            "dist/client/components/builtin/unauthorized.js",
1198            is_first_layer_group_route,
1199        )
1200        .await?;
1201    }
1202
1203    if is_root_directory {
1204        check_and_update_global_module_references(
1205            app_dir.clone(),
1206            &mut modules.global_error,
1207            "dist/client/components/builtin/global-error.js",
1208        )
1209        .await?;
1210    }
1211
1212    // For dynamic segments like [id], find all static siblings at the same URL level.
1213    // This is used by the client to determine if a prefetch can be reused when
1214    // navigating between routes that share the same parent layout.
1215    let static_siblings: Vec<RcStr> = if is_dynamic_segment(&directory_name) {
1216        url_tree
1217            .map(|t| {
1218                t.static_children()
1219                    .into_iter()
1220                    .filter(|s| s != &directory_name)
1221                    .collect()
1222            })
1223            .unwrap_or_default()
1224    } else {
1225        // Static segments don't need sibling info - only dynamic segments use it
1226        Vec::new()
1227    };
1228
1229    let mut tree = AppPageLoaderTree {
1230        page: app_page.clone(),
1231        segment: directory_name.clone(),
1232        parallel_routes: FxIndexMap::default(),
1233        modules: modules.without_leaves(),
1234        global_metadata: global_metadata.to_resolved().await?,
1235        static_siblings,
1236    };
1237
1238    let current_level_is_parallel_route = is_parallel_route(&directory_name);
1239
1240    if current_level_is_parallel_route {
1241        tree.segment = rcstr!("(__SLOT__)");
1242    }
1243
1244    if let Some(page) = (app_path == for_app_path || app_path.is_catchall())
1245        .then_some(modules.page)
1246        .flatten()
1247    {
1248        tree.parallel_routes.insert(
1249            rcstr!("children"),
1250            AppPageLoaderTree {
1251                page: app_page.clone(),
1252                segment: rcstr!("__PAGE__"),
1253                parallel_routes: FxIndexMap::default(),
1254                modules: AppDirModules {
1255                    page: Some(page),
1256                    metadata: modules.metadata,
1257                    ..Default::default()
1258                },
1259                global_metadata: global_metadata.to_resolved().await?,
1260                static_siblings: Vec::new(),
1261            },
1262        );
1263    }
1264
1265    let mut duplicate = FxHashMap::default();
1266
1267    for (subdir_name, subdirectory) in &directory_tree.subdirectories {
1268        let parallel_route_key = match_parallel_route(subdir_name);
1269
1270        let mut child_app_page = app_page.clone();
1271        let mut illegal_path_error = None;
1272
1273        // When constructing the app_page fails (e. g. due to limitations of the order),
1274        // we only want to emit the error when there are actual pages below that
1275        // directory.
1276        if let Err(e) = child_app_page.push_str(&normalize_underscore(subdir_name)) {
1277            illegal_path_error = Some(e);
1278        }
1279
1280        // Root/transparent segments don't consume a URL level; others descend.
1281        let child_url_tree: Option<&UrlSegmentTree> =
1282            if directory_name.is_empty() || is_url_transparent_segment(&directory_name) {
1283                url_tree
1284            } else {
1285                url_tree.and_then(|t| t.get_child(&directory_name))
1286            };
1287
1288        let subtree = Box::pin(directory_tree_to_loader_tree_internal(
1289            app_dir.clone(),
1290            global_metadata,
1291            subdir_name.clone(),
1292            subdirectory,
1293            child_app_page.clone(),
1294            for_app_path.clone(),
1295            parent_modules.clone(),
1296            child_url_tree,
1297        ))
1298        .await?;
1299
1300        if let Some(illegal_path) = subtree.as_ref().and(illegal_path_error) {
1301            return Err(illegal_path);
1302        }
1303
1304        if let Some(subtree) = subtree {
1305            if let Some(key) = parallel_route_key {
1306                // Validate that parallel routes (except "children") have a default.js file.
1307                // This validation matches the webpack loader's logic but is implemented
1308                // differently due to Turbopack's single-pass recursive processing.
1309
1310                // Check if we're inside a catch-all route (i.e., the parallel route is a child
1311                // of a catch-all segment). Only skip validation if the slot is UNDER a catch-all.
1312                // For example:
1313                //   /[...catchAll]/@slot - is_inside_catchall = true (skip validation) ✓
1314                //   /@slot/[...catchAll] - is_inside_catchall = false (require default) ✓
1315                // The catch-all provides fallback behavior, so default.js is not required.
1316                let is_inside_catchall = app_page.is_catchall();
1317
1318                // Check if this is a leaf segment (no child routes).
1319                // Leaf segments don't need default.js because there are no child routes
1320                // that could cause the parallel slot to unmatch. For example:
1321                //   /repo-overview/@slot/page with no child routes - is_leaf_segment = true (skip
1322                // validation) ✓   /repo-overview/@slot/page with
1323                // /repo-overview/child/page - is_leaf_segment = false (require default) ✓
1324                // This also handles route groups correctly by filtering them out.
1325                let is_leaf_segment = !has_child_routes(directory_tree);
1326
1327                // Turbopack-specific: Check if the parallel slot has matching child routes.
1328                // In webpack, this is checked implicitly via the two-phase processing:
1329                // slots with content are processed first and skip validation in the second phase.
1330                // In Turbopack's single-pass approach, we check directly if the slot has child
1331                // routes. If the slot has child routes that match the parent's
1332                // child routes, it can render content for those routes and doesn't
1333                // need a default. For example:
1334                //   /parent/@slot/page + /parent/@slot/child + /parent/child - slot_has_children =
1335                // true (skip validation) ✓   /parent/@slot/page + /parent/child (no
1336                // @slot/child) - slot_has_children = false (require default) ✓
1337                let slot_has_children = has_child_routes(subdirectory);
1338
1339                if key != "children"
1340                    && subdirectory.modules.default.is_none()
1341                    && !is_inside_catchall
1342                    && !is_leaf_segment
1343                    && !slot_has_children
1344                {
1345                    missing_default_parallel_route_issue(
1346                        app_dir.clone(),
1347                        app_page.clone(),
1348                        key.into(),
1349                    )
1350                    .to_resolved()
1351                    .await?
1352                    .emit();
1353                }
1354
1355                tree.parallel_routes.insert(key.into(), subtree);
1356                continue;
1357            }
1358
1359            // skip groups which don't have a page match.
1360            if is_group_route(subdir_name) && !subtree.has_page() {
1361                continue;
1362            }
1363
1364            if subtree.has_page() {
1365                check_duplicate(&mut duplicate, &subtree, app_dir.clone()).await?;
1366            }
1367
1368            if let Some(current_tree) = tree.parallel_routes.get("children") {
1369                if current_tree.has_only_catchall()
1370                    && (!subtree.has_only_catchall()
1371                        || current_tree.get_specificity() < subtree.get_specificity())
1372                {
1373                    tree.parallel_routes
1374                        .insert(rcstr!("children"), subtree.clone());
1375                }
1376            } else {
1377                tree.parallel_routes.insert(rcstr!("children"), subtree);
1378            }
1379        } else if let Some(key) = parallel_route_key {
1380            bail!(
1381                "missing page or default for parallel route `{}` (page: {})",
1382                key,
1383                app_page
1384            );
1385        }
1386    }
1387
1388    // make sure we don't have a match for other slots if there's an intercepting route match
1389    // we only check subtrees as the current level could trigger `is_intercepting`
1390    if tree
1391        .parallel_routes
1392        .iter()
1393        .any(|(_, parallel_tree)| parallel_tree.is_intercepting())
1394    {
1395        let mut keys_to_replace = Vec::new();
1396
1397        for (key, parallel_tree) in &tree.parallel_routes {
1398            if !parallel_tree.is_intercepting() {
1399                keys_to_replace.push(key.clone());
1400            }
1401        }
1402
1403        for key in keys_to_replace {
1404            let subdir_name: RcStr = format!("@{key}").into();
1405
1406            let default = if key == "children" {
1407                modules.default.clone()
1408            } else if let Some(subdirectory) = directory_tree.subdirectories.get(&subdir_name) {
1409                subdirectory.modules.default.clone()
1410            } else {
1411                None
1412            };
1413
1414            let is_inside_catchall = app_page.is_catchall();
1415
1416            // Check if this is a leaf segment (no child routes).
1417            let is_leaf_segment = !has_child_routes(directory_tree);
1418
1419            // Only emit the issue if this is not the children slot and there's no default
1420            // component. The children slot is implicit and doesn't require a default.js
1421            // file. Also skip validation if the slot is UNDER a catch-all route or if
1422            // this is a leaf segment (no child routes).
1423            if default.is_none() && key != "children" && !is_inside_catchall && !is_leaf_segment {
1424                missing_default_parallel_route_issue(
1425                    app_dir.clone(),
1426                    app_page.clone(),
1427                    key.clone(),
1428                )
1429                .to_resolved()
1430                .await?
1431                .emit();
1432            }
1433
1434            tree.parallel_routes.insert(
1435                key.clone(),
1436                default_route_tree(
1437                    app_dir.clone(),
1438                    global_metadata,
1439                    app_page.clone(),
1440                    default,
1441                    key.clone(),
1442                    for_app_path.clone(),
1443                )
1444                .await?,
1445            );
1446        }
1447    }
1448
1449    if tree.parallel_routes.is_empty() {
1450        if modules.default.is_some() || current_level_is_parallel_route {
1451            tree = default_route_tree(
1452                app_dir.clone(),
1453                global_metadata,
1454                app_page.clone(),
1455                modules.default.clone(),
1456                rcstr!("children"),
1457                for_app_path.clone(),
1458            )
1459            .await?;
1460        } else {
1461            return Ok(None);
1462        }
1463    } else if tree.parallel_routes.get("children").is_none() {
1464        tree.parallel_routes.insert(
1465            rcstr!("children"),
1466            default_route_tree(
1467                app_dir.clone(),
1468                global_metadata,
1469                app_page.clone(),
1470                modules.default.clone(),
1471                rcstr!("children"),
1472                for_app_path.clone(),
1473            )
1474            .await?,
1475        );
1476    }
1477
1478    if tree.parallel_routes.len() > 1
1479        && tree.parallel_routes.keys().next().map(|s| s.as_str()) != Some("children")
1480    {
1481        // children must go first for next.js to work correctly
1482        tree.parallel_routes
1483            .move_index(tree.parallel_routes.len() - 1, 0);
1484    }
1485
1486    Ok(Some(tree))
1487}
1488
1489async fn default_route_tree(
1490    app_dir: FileSystemPath,
1491    global_metadata: Vc<GlobalMetadata>,
1492    app_page: AppPage,
1493    default_component: Option<FileSystemPath>,
1494    slot_name: RcStr,
1495    for_app_path: AppPath,
1496) -> Result<AppPageLoaderTree> {
1497    Ok(AppPageLoaderTree {
1498        page: app_page.clone(),
1499        segment: rcstr!("__DEFAULT__"),
1500        parallel_routes: FxIndexMap::default(),
1501        modules: if let Some(default) = default_component {
1502            AppDirModules {
1503                default: Some(default),
1504                ..Default::default()
1505            }
1506        } else {
1507            let contains_interception = for_app_path.contains_interception();
1508
1509            let default_file = if contains_interception && slot_name == "children" {
1510                "dist/client/components/builtin/default-null.js"
1511            } else {
1512                "dist/client/components/builtin/default.js"
1513            };
1514
1515            AppDirModules {
1516                default: Some(get_next_package(app_dir).await?.join(default_file)?),
1517                ..Default::default()
1518            }
1519        },
1520        global_metadata: global_metadata.to_resolved().await?,
1521        static_siblings: Vec::new(),
1522    })
1523}
1524
1525#[turbo_tasks::function]
1526async fn directory_tree_to_entrypoints_internal(
1527    app_dir: FileSystemPath,
1528    global_metadata: ResolvedVc<GlobalMetadata>,
1529    is_global_not_found_enabled: Vc<bool>,
1530    next_mode: Vc<NextMode>,
1531    directory_name: RcStr,
1532    directory_tree: Vc<DirectoryTree>,
1533    app_page: AppPage,
1534    root_layouts: ResolvedVc<FileSystemPathVec>,
1535    root_params: ResolvedVc<RootParamVecOption>,
1536) -> Result<Vc<Entrypoints>> {
1537    let span = tracing::info_span!("build layout trees", name = display(&app_page));
1538    directory_tree_to_entrypoints_internal_untraced(
1539        app_dir,
1540        global_metadata,
1541        is_global_not_found_enabled,
1542        next_mode,
1543        directory_name,
1544        directory_tree,
1545        app_page,
1546        root_layouts,
1547        root_params,
1548    )
1549    .instrument(span)
1550    .await
1551}
1552
1553async fn directory_tree_to_entrypoints_internal_untraced(
1554    app_dir: FileSystemPath,
1555    global_metadata: ResolvedVc<GlobalMetadata>,
1556    is_global_not_found_enabled: Vc<bool>,
1557    next_mode: Vc<NextMode>,
1558    directory_name: RcStr,
1559    directory_tree: Vc<DirectoryTree>,
1560    app_page: AppPage,
1561    root_layouts: ResolvedVc<FileSystemPathVec>,
1562    root_params: ResolvedVc<RootParamVecOption>,
1563) -> Result<Vc<Entrypoints>> {
1564    let mut result = FxIndexMap::default();
1565
1566    let directory_tree_vc = directory_tree;
1567    let directory_tree = &*directory_tree.await?;
1568
1569    let subdirectories = &directory_tree.subdirectories;
1570    let modules = &directory_tree.modules;
1571    // Route can have its own segment config, also can inherit from the layout root
1572    // segment config. https://nextjs.org/docs/app/building-your-application/rendering/edge-and-nodejs-runtimes#segment-runtime-option
1573    // Pass down layouts from each tree to apply segment config when adding route.
1574    let root_layouts = if let Some(layout) = &modules.layout {
1575        let mut layouts = root_layouts.owned().await?;
1576        layouts.push(layout.clone());
1577        ResolvedVc::cell(layouts)
1578    } else {
1579        root_layouts
1580    };
1581
1582    // TODO: `root_layouts` is a misnomer, they're just parent layouts
1583    let root_params = if root_params.await?.is_none() && (*root_layouts.await?).len() == 1 {
1584        // found a root layout. the params up-to-and-including this point are the root params
1585        // for all child segments
1586        ResolvedVc::cell(Some(
1587            app_page
1588                .0
1589                .iter()
1590                .filter_map(|segment| match segment {
1591                    PageSegment::Dynamic(param)
1592                    | PageSegment::CatchAll(param)
1593                    | PageSegment::OptionalCatchAll(param) => Some(param.clone()),
1594                    _ => None,
1595                })
1596                .collect::<Vec<RcStr>>(),
1597        ))
1598    } else {
1599        root_params
1600    };
1601
1602    if modules.page.is_some() {
1603        let app_path = AppPath::from(app_page.clone());
1604
1605        let loader_tree = *directory_tree_to_loader_tree(
1606            app_dir.clone(),
1607            *global_metadata,
1608            directory_name.clone(),
1609            directory_tree_vc,
1610            app_page.clone(),
1611            app_path,
1612        )
1613        .await?;
1614
1615        add_app_page(
1616            app_dir.clone(),
1617            &mut result,
1618            app_page.complete(PageType::Page)?,
1619            loader_tree.context("loader tree should be created for a page/default")?,
1620            root_params,
1621        );
1622    }
1623
1624    if let Some(route) = &modules.route {
1625        add_app_route(
1626            app_dir.clone(),
1627            &mut result,
1628            app_page.complete(PageType::Route)?,
1629            route.clone(),
1630            root_layouts,
1631            root_params,
1632        );
1633    }
1634
1635    let Metadata {
1636        icon,
1637        apple,
1638        twitter,
1639        open_graph,
1640        sitemap,
1641        base_page: _,
1642    } = &modules.metadata;
1643
1644    for meta in sitemap
1645        .iter()
1646        .cloned()
1647        .chain(icon.iter().cloned().map(MetadataItem::from))
1648        .chain(apple.iter().cloned().map(MetadataItem::from))
1649        .chain(twitter.iter().cloned().map(MetadataItem::from))
1650        .chain(open_graph.iter().cloned().map(MetadataItem::from))
1651    {
1652        let app_page = app_page.clone_push_str(&get_metadata_route_name(meta.clone()).await?)?;
1653
1654        add_app_metadata_route(
1655            app_dir.clone(),
1656            &mut result,
1657            normalize_metadata_route(app_page)?,
1658            meta,
1659            root_params,
1660        );
1661    }
1662
1663    // root path: /
1664    if app_page.is_root() {
1665        let GlobalMetadata {
1666            favicon,
1667            robots,
1668            manifest,
1669        } = &*global_metadata.await?;
1670
1671        for meta in favicon.iter().chain(robots.iter()).chain(manifest.iter()) {
1672            let app_page =
1673                app_page.clone_push_str(&get_metadata_route_name(meta.clone()).await?)?;
1674
1675            add_app_metadata_route(
1676                app_dir.clone(),
1677                &mut result,
1678                normalize_metadata_route(app_page)?,
1679                meta.clone(),
1680                root_params,
1681            );
1682        }
1683
1684        let mut modules = directory_tree.modules.clone();
1685
1686        // fill in the default modules for the not-found entrypoint
1687        if modules.layout.is_none() {
1688            modules.layout = Some(
1689                get_next_package(app_dir.clone())
1690                    .await?
1691                    .join("dist/client/components/builtin/layout.js")?,
1692            );
1693        }
1694
1695        if modules.not_found.is_none() {
1696            modules.not_found = Some(
1697                get_next_package(app_dir.clone())
1698                    .await?
1699                    .join("dist/client/components/builtin/not-found.js")?,
1700            );
1701        }
1702        if modules.forbidden.is_none() {
1703            modules.forbidden = Some(
1704                get_next_package(app_dir.clone())
1705                    .await?
1706                    .join("dist/client/components/builtin/forbidden.js")?,
1707            );
1708        }
1709        if modules.unauthorized.is_none() {
1710            modules.unauthorized = Some(
1711                get_next_package(app_dir.clone())
1712                    .await?
1713                    .join("dist/client/components/builtin/unauthorized.js")?,
1714            );
1715        }
1716        if modules.global_error.is_none() {
1717            modules.global_error = Some(
1718                get_next_package(app_dir.clone())
1719                    .await?
1720                    .join("dist/client/components/builtin/global-error.js")?,
1721            );
1722        }
1723
1724        // Next.js has this logic in "collect-app-paths", where the root not-found page
1725        // is considered as its own entry point.
1726
1727        // Determine if we enable the global not-found feature.
1728        let is_global_not_found_enabled = *is_global_not_found_enabled.await?;
1729        let use_global_not_found =
1730            is_global_not_found_enabled || modules.global_not_found.is_some();
1731
1732        let not_found_root_modules = modules.without_leaves();
1733        let not_found_tree = AppPageLoaderTree {
1734            page: app_page.clone(),
1735            segment: directory_name.clone(),
1736            parallel_routes: fxindexmap! {
1737                rcstr!("children") => AppPageLoaderTree {
1738                    page: app_page.clone(),
1739                    segment: rcstr!("/_not-found"),
1740                    parallel_routes: fxindexmap! {
1741                        rcstr!("children") => AppPageLoaderTree {
1742                            page: app_page.clone(),
1743                            segment: rcstr!("__PAGE__"),
1744                            parallel_routes: FxIndexMap::default(),
1745                            modules: if use_global_not_found {
1746                                // if global-not-found.js is present:
1747                                // leaf module only keeps page pointing to empty-stub
1748                                AppDirModules {
1749                                    // page is built-in/empty-stub
1750                                    page: Some(get_next_package(app_dir.clone())
1751                                        .await?
1752                                        .join("dist/client/components/builtin/empty-stub.js")?,
1753                                    ),
1754                                    ..Default::default()
1755                                }
1756                            } else {
1757                                // if global-not-found.js is not present:
1758                                // we search if we can compose root layout with the root not-found.js;
1759                                AppDirModules {
1760                                    page: match modules.not_found {
1761                                        Some(v) => Some(v),
1762                                        None => Some(get_next_package(app_dir.clone())
1763                                            .await?
1764                                            .join("dist/client/components/builtin/not-found.js")?,
1765                                        ),
1766                                    },
1767                                    ..Default::default()
1768                                }
1769                            },
1770                            global_metadata,
1771                            static_siblings: Vec::new(),
1772                        }
1773                    },
1774                    modules: AppDirModules {
1775                        ..Default::default()
1776                    },
1777                    global_metadata,
1778                    static_siblings: Vec::new(),
1779                },
1780            },
1781            modules: AppDirModules {
1782                // `global-not-found.js` does not need a layout since it's included.
1783                // Skip it if it's present.
1784                // Otherwise, we need to compose it with the root layout to compose with
1785                // not-found.js boundary.
1786                layout: if use_global_not_found {
1787                    match modules.global_not_found {
1788                        Some(v) => Some(v),
1789                        None => Some(
1790                            get_next_package(app_dir.clone())
1791                                .await?
1792                                .join("dist/client/components/builtin/global-not-found.js")?,
1793                        ),
1794                    }
1795                } else {
1796                    modules.layout
1797                },
1798                ..not_found_root_modules
1799            },
1800            global_metadata,
1801            static_siblings: Vec::new(),
1802        }
1803        .resolved_cell();
1804
1805        {
1806            let app_page = app_page
1807                .clone_push_str("_not-found")?
1808                .complete(PageType::Page)?;
1809
1810            add_app_page(
1811                app_dir.clone(),
1812                &mut result,
1813                app_page,
1814                not_found_tree,
1815                root_params,
1816            );
1817        }
1818
1819        // Create production global error page only in build mode
1820        // This aligns with webpack: default Pages entries (including /_error) are only added when
1821        // the build isn't app-only. If the build is app-only (no user pages/api), we should still
1822        // expose the app global error so runtime errors render, but we shouldn't emit it otherwise.
1823        if matches!(*next_mode.await?, NextMode::Build) {
1824            // Create a `_global-error/page` route using user's global-error.js or built-in
1825            // fallback.
1826            let next_package = get_next_package(app_dir.clone()).await?;
1827            let global_error_tree = AppPageLoaderTree {
1828                page: app_page.clone(),
1829                segment: directory_name.clone(),
1830                parallel_routes: fxindexmap! {
1831                    rcstr!("children") => AppPageLoaderTree {
1832                        page: app_page.clone(),
1833                        segment: rcstr!("__PAGE__"),
1834                        parallel_routes: FxIndexMap::default(),
1835                        modules: AppDirModules {
1836                            page: Some(next_package
1837                                .join("dist/client/components/builtin/app-error.js")?),
1838                            ..Default::default()
1839                        },
1840                        global_metadata,
1841                        static_siblings: Vec::new(),
1842                    }
1843                },
1844                // global-error is needed for getGlobalErrorStyles to work during rendering.
1845                // Use user's custom global-error if defined, otherwise builtin fallback.
1846                modules: AppDirModules {
1847                    global_error: modules.global_error.clone(),
1848                    ..Default::default()
1849                },
1850                global_metadata,
1851                static_siblings: Vec::new(),
1852            }
1853            .resolved_cell();
1854
1855            let app_global_error_page = app_page
1856                .clone_push_str("_global-error")?
1857                .complete(PageType::Page)?;
1858            add_app_page(
1859                app_dir.clone(),
1860                &mut result,
1861                app_global_error_page,
1862                global_error_tree,
1863                root_params,
1864            );
1865        }
1866    }
1867
1868    let app_page = &app_page;
1869    let directory_name = &directory_name;
1870    let subdirectories = subdirectories
1871        .iter()
1872        .map(|(subdir_name, &subdirectory)| {
1873            let app_dir = app_dir.clone();
1874
1875            async move {
1876                let mut child_app_page = app_page.clone();
1877                let mut illegal_path = None;
1878
1879                // When constructing the app_page fails (e. g. due to limitations of the order),
1880                // we only want to emit the error when there are actual pages below that
1881                // directory.
1882                if let Err(e) = child_app_page.push_str(&normalize_underscore(subdir_name)) {
1883                    illegal_path = Some(e);
1884                }
1885
1886                let map = directory_tree_to_entrypoints_internal(
1887                    app_dir.clone(),
1888                    *global_metadata,
1889                    is_global_not_found_enabled,
1890                    next_mode,
1891                    subdir_name.clone(),
1892                    *subdirectory,
1893                    child_app_page.clone(),
1894                    *root_layouts,
1895                    *root_params,
1896                )
1897                .await?;
1898
1899                if let Some(illegal_path) = illegal_path
1900                    && !map.is_empty()
1901                {
1902                    return Err(illegal_path);
1903                }
1904
1905                let mut loader_trees = Vec::new();
1906
1907                for (_, entrypoint) in map.iter() {
1908                    if let Entrypoint::AppPage { ref pages, .. } = *entrypoint {
1909                        for page in pages {
1910                            let app_path = AppPath::from(page.clone());
1911
1912                            let loader_tree = directory_tree_to_loader_tree(
1913                                app_dir.clone(),
1914                                *global_metadata,
1915                                directory_name.clone(),
1916                                directory_tree_vc,
1917                                app_page.clone(),
1918                                app_path,
1919                            );
1920                            loader_trees.push(loader_tree);
1921                        }
1922                    }
1923                }
1924                Ok((map, loader_trees))
1925            }
1926        })
1927        .try_join()
1928        .await?;
1929
1930    for (map, loader_trees) in subdirectories.iter() {
1931        let mut i = 0;
1932        for (_, entrypoint) in map.iter() {
1933            match entrypoint {
1934                Entrypoint::AppPage {
1935                    pages,
1936                    loader_tree: _,
1937                    root_params,
1938                } => {
1939                    for page in pages {
1940                        let loader_tree = *loader_trees[i].await?;
1941                        i += 1;
1942
1943                        add_app_page(
1944                            app_dir.clone(),
1945                            &mut result,
1946                            page.clone(),
1947                            loader_tree
1948                                .context("loader tree should be created for a page/default")?,
1949                            *root_params,
1950                        );
1951                    }
1952                }
1953                Entrypoint::AppRoute {
1954                    page,
1955                    path,
1956                    root_layouts,
1957                    root_params,
1958                } => {
1959                    add_app_route(
1960                        app_dir.clone(),
1961                        &mut result,
1962                        page.clone(),
1963                        path.clone(),
1964                        *root_layouts,
1965                        *root_params,
1966                    );
1967                }
1968                Entrypoint::AppMetadata {
1969                    page,
1970                    metadata,
1971                    root_params,
1972                } => {
1973                    add_app_metadata_route(
1974                        app_dir.clone(),
1975                        &mut result,
1976                        page.clone(),
1977                        metadata.clone(),
1978                        *root_params,
1979                    );
1980                }
1981            }
1982        }
1983    }
1984    Ok(Vc::cell(result))
1985}
1986
1987/// Returns the global metadata for an app directory.
1988#[turbo_tasks::function]
1989pub async fn get_global_metadata(
1990    app_dir: FileSystemPath,
1991    page_extensions: Vc<Vec<RcStr>>,
1992) -> Result<Vc<GlobalMetadata>> {
1993    let DirectoryContent::Entries(entries) = &*app_dir.read_dir().await? else {
1994        bail!("app_dir must be a directory")
1995    };
1996    let mut metadata = GlobalMetadata::default();
1997
1998    for (basename, entry) in entries {
1999        let DirectoryEntry::File(file) = entry else {
2000            continue;
2001        };
2002
2003        let Some(GlobalMetadataFileMatch {
2004            metadata_type,
2005            dynamic,
2006        }) = match_global_metadata_file(basename, &page_extensions.await?)
2007        else {
2008            continue;
2009        };
2010
2011        let entry = match metadata_type {
2012            "favicon" => &mut metadata.favicon,
2013            "manifest" => &mut metadata.manifest,
2014            "robots" => &mut metadata.robots,
2015            _ => continue,
2016        };
2017
2018        if dynamic {
2019            *entry = Some(MetadataItem::Dynamic { path: file.clone() });
2020        } else {
2021            *entry = Some(MetadataItem::Static { path: file.clone() });
2022        }
2023        // TODO(WEB-952) handle symlinks in app dir
2024    }
2025
2026    Ok(metadata.cell())
2027}
2028
2029#[turbo_tasks::value(shared)]
2030struct DirectoryTreeIssue {
2031    pub severity: IssueSeverity,
2032    pub app_dir: FileSystemPath,
2033    pub message: ResolvedVc<StyledString>,
2034}
2035
2036#[async_trait]
2037#[turbo_tasks::value_impl]
2038impl Issue for DirectoryTreeIssue {
2039    fn severity(&self) -> IssueSeverity {
2040        self.severity
2041    }
2042
2043    async fn title(&self) -> Result<StyledString> {
2044        Ok(StyledString::Text(rcstr!(
2045            "An issue occurred while preparing your Next.js app"
2046        )))
2047    }
2048
2049    fn stage(&self) -> IssueStage {
2050        IssueStage::AppStructure
2051    }
2052
2053    async fn file_path(&self) -> Result<FileSystemPath> {
2054        Ok(self.app_dir.clone())
2055    }
2056
2057    async fn description(&self) -> Result<Option<StyledString>> {
2058        Ok(Some((*self.message.await?).clone()))
2059    }
2060}