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