Skip to main content

next_core/
app_structure.rs

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