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().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().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    let next_package = get_next_package(project_root.clone()).await?;
275
276    let app_item = {
277        let app_router_path = next_router_path.join("_app")?;
278        PagesStructureItem::new(
279            pages_path.join("_app")?,
280            page_extensions,
281            Some(next_package.join("app.js")?),
282            app_router_path.clone(),
283            app_router_path,
284        )
285    };
286
287    let document_item = {
288        let document_router_path = next_router_path.join("_document")?;
289        PagesStructureItem::new(
290            pages_path.join("_document")?,
291            page_extensions,
292            Some(next_package.join("document.js")?),
293            document_router_path.clone(),
294            document_router_path,
295        )
296    };
297
298    let error_item = {
299        let error_router_path = next_router_path.join("_error")?;
300        PagesStructureItem::new(
301            pages_path.join("_error")?,
302            page_extensions,
303            Some(next_package.join("error.js")?),
304            error_router_path.clone(),
305            error_router_path,
306        )
307    };
308
309    Ok(PagesStructure {
310        app: app_item.to_resolved().await?,
311        document: document_item.to_resolved().await?,
312        error: error_item.to_resolved().await?,
313        error_500: error_500_item.to_resolved().await?,
314        api: api_directory,
315        pages: pages_directory,
316        has_user_pages,
317        should_create_pages_entries,
318    }
319    .cell())
320}
321
322/// Handles a directory in the pages directory (or the pages directory itself).
323/// Calls itself recursively for sub directories.
324#[turbo_tasks::function]
325async fn get_pages_structure_for_directory(
326    project_path: FileSystemPath,
327    next_router_path: FileSystemPath,
328    position: u32,
329    page_extensions: Vc<Vec<RcStr>>,
330) -> Result<Vc<PagesDirectoryStructure>> {
331    let span = tracing::info_span!(
332        "analyze pages structure",
333        name = display(project_path.value_to_string().await?)
334    );
335    async move {
336        let page_extensions_raw = &*page_extensions.await?;
337
338        let mut children = vec![];
339        let mut items = vec![];
340        let dir_content = project_path.read_dir().await?;
341        if let DirectoryContent::Entries(entries) = &*dir_content {
342            for (name, entry) in entries.iter() {
343                match entry {
344                    DirectoryEntry::File(_) => {
345                        let Some(basename) = page_basename(name, page_extensions_raw) else {
346                            continue;
347                        };
348                        let item_next_router_path = match basename {
349                            "index" => next_router_path.clone(),
350                            _ => next_router_path.join(basename)?,
351                        };
352                        let base_path = project_path.join(name)?;
353                        let item_original_name = next_router_path.join(basename)?;
354                        items.push((
355                            basename,
356                            PagesStructureItem::new(
357                                base_path,
358                                page_extensions,
359                                None,
360                                item_next_router_path,
361                                item_original_name,
362                            ),
363                        ));
364                    }
365                    DirectoryEntry::Directory(dir_project_path) => {
366                        children.push((
367                            name,
368                            get_pages_structure_for_directory(
369                                dir_project_path.clone(),
370                                next_router_path.join(name)?,
371                                position + 1,
372                                page_extensions,
373                            ),
374                        ));
375                    }
376                    _ => {}
377                }
378            }
379        }
380
381        // Ensure deterministic order since read_dir is not deterministic
382        items.sort_by_key(|(k, _)| *k);
383
384        // Ensure deterministic order since read_dir is not deterministic
385        children.sort_by_key(|(k, _)| *k);
386
387        Ok(PagesDirectoryStructure {
388            project_path: project_path.clone(),
389            next_router_path: next_router_path.clone(),
390            items: items
391                .into_iter()
392                .map(|(_, v)| v)
393                .map(|v| async move { v.to_resolved().await })
394                .try_join()
395                .await?,
396            children: children
397                .into_iter()
398                .map(|(_, v)| v)
399                .map(|v| async move { v.to_resolved().await })
400                .try_join()
401                .await?,
402        }
403        .cell())
404    }
405    .instrument(span)
406    .await
407}
408
409fn page_basename<'a>(name: &'a str, page_extensions: &'a [RcStr]) -> Option<&'a str> {
410    page_extensions
411        .iter()
412        .find_map(|allowed| name.strip_suffix(&**allowed)?.strip_suffix('.'))
413}
414
415fn next_router_path_for_basename(
416    next_router_path: FileSystemPath,
417    basename: &str,
418) -> Result<FileSystemPath> {
419    Ok(if basename == "index" {
420        next_router_path.clone()
421    } else {
422        next_router_path.join(basename)?
423    })
424}