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