next_core/
pages_structure.rs

1use anyhow::Result;
2use tracing::Instrument;
3use turbo_rcstr::RcStr;
4use turbo_tasks::{OptionVcExt, ResolvedVc, TryJoinIterExt, Vc};
5use turbo_tasks_fs::{
6    DirectoryContent, DirectoryEntry, FileSystemEntryType, FileSystemPath, FileSystemPathOption,
7};
8
9use crate::next_import_map::get_next_package;
10
11/// A final route in the pages directory.
12#[turbo_tasks::value]
13pub struct PagesStructureItem {
14    pub base_path: FileSystemPath,
15    pub extensions: ResolvedVc<Vec<RcStr>>,
16    pub fallback_path: Option<FileSystemPath>,
17
18    /// Pathname of this item in the Next.js router.
19    pub next_router_path: FileSystemPath,
20    /// Unique path corresponding to this item. This differs from
21    /// `next_router_path` in that it will include the trailing /index for index
22    /// routes, which allows for differentiating with potential /index
23    /// directories.
24    pub original_path: FileSystemPath,
25}
26
27#[turbo_tasks::value_impl]
28impl PagesStructureItem {
29    #[turbo_tasks::function]
30    fn new(
31        base_path: FileSystemPath,
32        extensions: ResolvedVc<Vec<RcStr>>,
33        fallback_path: Option<FileSystemPath>,
34        next_router_path: FileSystemPath,
35        original_path: FileSystemPath,
36    ) -> Vc<Self> {
37        PagesStructureItem {
38            base_path,
39            extensions,
40            fallback_path,
41            next_router_path,
42            original_path,
43        }
44        .cell()
45    }
46
47    #[turbo_tasks::function]
48    pub async fn file_path(&self) -> Result<Vc<FileSystemPath>> {
49        // Check if the file path + extension exists in the filesystem, if so use that. If not fall
50        // back to the base path.
51        for ext in self.extensions.await?.into_iter() {
52            let file_path = self.base_path.append(&format!(".{ext}"))?;
53            let ty = *file_path.get_type().await?;
54            if matches!(ty, FileSystemEntryType::File | FileSystemEntryType::Symlink) {
55                return Ok(file_path.cell());
56            }
57        }
58        if let Some(fallback_path) = &self.fallback_path {
59            Ok(fallback_path.clone().cell())
60        } else {
61            // If the file path that was passed in already has an extension, for example
62            // `pages/index.js` it won't match the extensions list above because it already had an
63            // extension and for example `.js.js` obviously won't match
64            Ok(self.base_path.clone().cell())
65        }
66    }
67}
68
69/// A (sub)directory in the pages directory with all analyzed routes and
70/// folders.
71#[turbo_tasks::value]
72pub struct PagesStructure {
73    pub app: ResolvedVc<PagesStructureItem>,
74    pub document: ResolvedVc<PagesStructureItem>,
75    pub error: ResolvedVc<PagesStructureItem>,
76    pub error_500: Option<ResolvedVc<PagesStructureItem>>,
77    pub api: Option<ResolvedVc<PagesDirectoryStructure>>,
78    pub pages: Option<ResolvedVc<PagesDirectoryStructure>>,
79    pub has_user_pages: bool,
80    pub should_create_pages_entries: bool,
81}
82
83#[turbo_tasks::value]
84pub struct PagesDirectoryStructure {
85    pub project_path: FileSystemPath,
86    pub next_router_path: FileSystemPath,
87    pub items: Vec<ResolvedVc<PagesStructureItem>>,
88    pub children: Vec<ResolvedVc<PagesDirectoryStructure>>,
89}
90
91#[turbo_tasks::value_impl]
92impl PagesDirectoryStructure {
93    /// Returns the path to the directory of this structure in the project file
94    /// system.
95    #[turbo_tasks::function]
96    pub fn project_path(&self) -> Vc<FileSystemPath> {
97        self.project_path.clone().cell()
98    }
99}
100
101/// Finds and returns the [PagesStructure] of the pages directory if existing.
102#[turbo_tasks::function]
103pub async fn find_pages_structure(
104    project_root: FileSystemPath,
105    next_router_root: FileSystemPath,
106    page_extensions: Vc<Vec<RcStr>>,
107    next_mode: Vc<crate::mode::NextMode>,
108) -> Result<Vc<PagesStructure>> {
109    let pages_root = project_root.join("pages")?.realpath().owned().await?;
110    let pages_root = if *pages_root.get_type().await? == FileSystemEntryType::Directory {
111        Some(pages_root)
112    } else {
113        let src_pages_root = project_root.join("src/pages")?.realpath().owned().await?;
114        if *src_pages_root.get_type().await? == FileSystemEntryType::Directory {
115            Some(src_pages_root)
116        } else {
117            // If neither pages nor src/pages exists, we still want to generate
118            // the pages structure, but with no pages and default values for
119            // _app, _document and _error.
120            None
121        }
122    };
123
124    Ok(get_pages_structure_for_root_directory(
125        project_root,
126        Vc::cell(pages_root),
127        next_router_root,
128        page_extensions,
129        next_mode,
130    ))
131}
132
133/// Handles the root pages directory.
134#[turbo_tasks::function]
135async fn get_pages_structure_for_root_directory(
136    project_root: FileSystemPath,
137    project_path: Vc<FileSystemPathOption>,
138    next_router_path: FileSystemPath,
139    page_extensions: Vc<Vec<RcStr>>,
140    next_mode: Vc<crate::mode::NextMode>,
141) -> Result<Vc<PagesStructure>> {
142    let page_extensions_raw = &*page_extensions.await?;
143
144    let mut api_directory = None;
145    let mut error_500_item = None;
146
147    let project_path = project_path.await?;
148    let pages_directory = if let Some(project_path) = &*project_path {
149        let mut children = vec![];
150        let mut items = vec![];
151
152        let dir_content = project_path.read_dir().await?;
153        if let DirectoryContent::Entries(entries) = &*dir_content {
154            for (name, entry) in entries.iter() {
155                let entry = entry.clone().resolve_symlink().await?;
156                match entry {
157                    DirectoryEntry::File(_) => {
158                        // Do not process .d.ts files as routes
159                        if name.ends_with(".d.ts") {
160                            continue;
161                        }
162                        let Some(basename) = page_basename(name, page_extensions_raw) else {
163                            continue;
164                        };
165                        let base_path = project_path.join(basename)?;
166                        match basename {
167                            "_app" | "_document" | "_error" => {}
168                            "500" => {
169                                let item_next_router_path = next_router_path_for_basename(
170                                    next_router_path.clone(),
171                                    basename,
172                                )?;
173                                let item_original_path = next_router_path.join(basename)?;
174                                let item = PagesStructureItem::new(
175                                    base_path,
176                                    page_extensions,
177                                    None,
178                                    item_next_router_path,
179                                    item_original_path,
180                                );
181
182                                error_500_item = Some(item);
183
184                                items.push((basename, item));
185                            }
186
187                            basename => {
188                                let item_next_router_path = next_router_path_for_basename(
189                                    next_router_path.clone(),
190                                    basename,
191                                )?;
192                                let item_original_path = next_router_path.join(basename)?;
193                                items.push((
194                                    basename,
195                                    PagesStructureItem::new(
196                                        base_path,
197                                        page_extensions,
198                                        None,
199                                        item_next_router_path,
200                                        item_original_path,
201                                    ),
202                                ));
203                            }
204                        }
205                    }
206                    DirectoryEntry::Directory(dir_project_path) => match name.as_str() {
207                        "api" => {
208                            api_directory = Some(
209                                get_pages_structure_for_directory(
210                                    dir_project_path.clone(),
211                                    next_router_path.join(name)?,
212                                    1,
213                                    page_extensions,
214                                )
215                                .to_resolved()
216                                .await?,
217                            );
218                        }
219                        _ => {
220                            children.push((
221                                name,
222                                get_pages_structure_for_directory(
223                                    dir_project_path.clone(),
224                                    next_router_path.join(name)?,
225                                    1,
226                                    page_extensions,
227                                ),
228                            ));
229                        }
230                    },
231                    _ => {}
232                }
233            }
234        }
235
236        // Ensure deterministic order since read_dir is not deterministic
237        items.sort_by_key(|(k, _)| *k);
238        children.sort_by_key(|(k, _)| *k);
239
240        Some(
241            PagesDirectoryStructure {
242                project_path: project_path.clone(),
243                next_router_path: next_router_path.clone(),
244                items: items
245                    .into_iter()
246                    .map(|(_, v)| async move { v.to_resolved().await })
247                    .try_join()
248                    .await?,
249                children: children
250                    .into_iter()
251                    .map(|(_, v)| async move { v.to_resolved().await })
252                    .try_join()
253                    .await?,
254            }
255            .resolved_cell(),
256        )
257    } else {
258        None
259    };
260
261    let pages_path = if let Some(project_path) = &*project_path {
262        project_path.clone()
263    } else {
264        project_root.join("pages")?
265    };
266
267    // Check if there are any actual user pages (not just _app, _document, _error)
268    // error_500_item can be auto-generated for app router, so only count it if there are other user
269    // pages
270    let has_user_pages = pages_directory.is_some() || api_directory.is_some();
271
272    // Only skip user pages routes during build mode when there are no user pages
273    let should_create_pages_entries = has_user_pages || next_mode.await?.is_development();
274
275    let app_item = {
276        let app_router_path = next_router_path.join("_app")?;
277        PagesStructureItem::new(
278            pages_path.join("_app")?,
279            page_extensions,
280            Some(
281                get_next_package(project_root.clone())
282                    .await?
283                    .join("app.js")?,
284            ),
285            app_router_path.clone(),
286            app_router_path,
287        )
288    };
289
290    let document_item = {
291        let document_router_path = next_router_path.join("_document")?;
292        PagesStructureItem::new(
293            pages_path.join("_document")?,
294            page_extensions,
295            Some(
296                get_next_package(project_root.clone())
297                    .await?
298                    .join("document.js")?,
299            ),
300            document_router_path.clone(),
301            document_router_path,
302        )
303    };
304
305    let error_item = {
306        let error_router_path = next_router_path.join("_error")?;
307        PagesStructureItem::new(
308            pages_path.join("_error")?,
309            page_extensions,
310            Some(
311                get_next_package(project_root.clone())
312                    .await?
313                    .join("error.js")?,
314            ),
315            error_router_path.clone(),
316            error_router_path,
317        )
318    };
319
320    Ok(PagesStructure {
321        app: app_item.to_resolved().await?,
322        document: document_item.to_resolved().await?,
323        error: error_item.to_resolved().await?,
324        error_500: error_500_item.to_resolved().await?,
325        api: api_directory,
326        pages: pages_directory,
327        has_user_pages,
328        should_create_pages_entries,
329    }
330    .cell())
331}
332
333/// Handles a directory in the pages directory (or the pages directory itself).
334/// Calls itself recursively for sub directories.
335#[turbo_tasks::function]
336async fn get_pages_structure_for_directory(
337    project_path: FileSystemPath,
338    next_router_path: FileSystemPath,
339    position: u32,
340    page_extensions: Vc<Vec<RcStr>>,
341) -> Result<Vc<PagesDirectoryStructure>> {
342    let span = {
343        let path = project_path.value_to_string().await?.to_string();
344        tracing::info_span!("analyse pages structure", name = path)
345    };
346    async move {
347        let page_extensions_raw = &*page_extensions.await?;
348
349        let mut children = vec![];
350        let mut items = vec![];
351        let dir_content = project_path.read_dir().await?;
352        if let DirectoryContent::Entries(entries) = &*dir_content {
353            for (name, entry) in entries.iter() {
354                match entry {
355                    DirectoryEntry::File(_) => {
356                        let Some(basename) = page_basename(name, page_extensions_raw) else {
357                            continue;
358                        };
359                        let item_next_router_path = match basename {
360                            "index" => next_router_path.clone(),
361                            _ => next_router_path.join(basename)?,
362                        };
363                        let base_path = project_path.join(name)?;
364                        let item_original_name = next_router_path.join(basename)?;
365                        items.push((
366                            basename,
367                            PagesStructureItem::new(
368                                base_path,
369                                page_extensions,
370                                None,
371                                item_next_router_path,
372                                item_original_name,
373                            ),
374                        ));
375                    }
376                    DirectoryEntry::Directory(dir_project_path) => {
377                        children.push((
378                            name,
379                            get_pages_structure_for_directory(
380                                dir_project_path.clone(),
381                                next_router_path.join(name)?,
382                                position + 1,
383                                page_extensions,
384                            ),
385                        ));
386                    }
387                    _ => {}
388                }
389            }
390        }
391
392        // Ensure deterministic order since read_dir is not deterministic
393        items.sort_by_key(|(k, _)| *k);
394
395        // Ensure deterministic order since read_dir is not deterministic
396        children.sort_by_key(|(k, _)| *k);
397
398        Ok(PagesDirectoryStructure {
399            project_path: project_path.clone(),
400            next_router_path: next_router_path.clone(),
401            items: items
402                .into_iter()
403                .map(|(_, v)| v)
404                .map(|v| async move { v.to_resolved().await })
405                .try_join()
406                .await?,
407            children: children
408                .into_iter()
409                .map(|(_, v)| v)
410                .map(|v| async move { v.to_resolved().await })
411                .try_join()
412                .await?,
413        }
414        .cell())
415    }
416    .instrument(span)
417    .await
418}
419
420fn page_basename<'a>(name: &'a str, page_extensions: &'a [RcStr]) -> Option<&'a str> {
421    page_extensions
422        .iter()
423        .find_map(|allowed| name.strip_suffix(&**allowed)?.strip_suffix('.'))
424}
425
426fn next_router_path_for_basename(
427    next_router_path: FileSystemPath,
428    basename: &str,
429) -> Result<FileSystemPath> {
430    Ok(if basename == "index" {
431        next_router_path.clone()
432    } else {
433        next_router_path.join(basename)?
434    })
435}