Skip to main content

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                                    page_extensions,
213                                )
214                                .to_resolved()
215                                .await?,
216                            );
217                        }
218                        _ => {
219                            children.push((
220                                name,
221                                get_pages_structure_for_directory(
222                                    dir_project_path.clone(),
223                                    next_router_path.join(name)?,
224                                    page_extensions,
225                                ),
226                            ));
227                        }
228                    },
229                    _ => {}
230                }
231            }
232        }
233
234        // Ensure deterministic order since read_dir is not deterministic
235        items.sort_by_key(|(k, _)| *k);
236        children.sort_by_key(|(k, _)| *k);
237
238        Some(
239            PagesDirectoryStructure {
240                project_path: project_path.clone(),
241                next_router_path: next_router_path.clone(),
242                items: items
243                    .into_iter()
244                    .map(|(_, v)| async move { v.to_resolved().await })
245                    .try_join()
246                    .await?,
247                children: children
248                    .into_iter()
249                    .map(|(_, v)| async move { v.to_resolved().await })
250                    .try_join()
251                    .await?,
252            }
253            .resolved_cell(),
254        )
255    } else {
256        None
257    };
258
259    let pages_path = if let Some(project_path) = &*project_path {
260        project_path.clone()
261    } else {
262        project_root.join("pages")?
263    };
264
265    // Check if there are any actual user pages (not just _app, _document, _error)
266    // error_500_item can be auto-generated for app router, so only count it if there are other user
267    // pages
268    let has_user_pages = pages_directory.is_some() || api_directory.is_some();
269
270    // Only skip user pages routes during build mode when there are no user pages
271    let should_create_pages_entries = has_user_pages || next_mode.await?.is_development();
272    let next_package = get_next_package(project_root.clone()).await?;
273
274    let app_item = {
275        let app_router_path = next_router_path.join("_app")?;
276        PagesStructureItem::new(
277            pages_path.join("_app")?,
278            page_extensions,
279            Some(next_package.join("app.js")?),
280            app_router_path.clone(),
281            app_router_path,
282        )
283    };
284
285    let document_item = {
286        let document_router_path = next_router_path.join("_document")?;
287        PagesStructureItem::new(
288            pages_path.join("_document")?,
289            page_extensions,
290            Some(next_package.join("document.js")?),
291            document_router_path.clone(),
292            document_router_path,
293        )
294    };
295
296    let error_item = {
297        let error_router_path = next_router_path.join("_error")?;
298        PagesStructureItem::new(
299            pages_path.join("_error")?,
300            page_extensions,
301            Some(next_package.join("error.js")?),
302            error_router_path.clone(),
303            error_router_path,
304        )
305    };
306
307    Ok(PagesStructure {
308        app: app_item.to_resolved().await?,
309        document: document_item.to_resolved().await?,
310        error: error_item.to_resolved().await?,
311        error_500: error_500_item.to_resolved().await?,
312        api: api_directory,
313        pages: pages_directory,
314        has_user_pages,
315        should_create_pages_entries,
316    }
317    .cell())
318}
319
320/// Handles a directory in the pages directory (or the pages directory itself).
321/// Calls itself recursively for sub directories.
322#[turbo_tasks::function]
323async fn get_pages_structure_for_directory(
324    project_path: FileSystemPath,
325    next_router_path: FileSystemPath,
326    page_extensions: Vc<Vec<RcStr>>,
327) -> Result<Vc<PagesDirectoryStructure>> {
328    let span = tracing::info_span!(
329        "analyze pages structure",
330        name = display(project_path.value_to_string().await?)
331    );
332    async move {
333        let page_extensions_raw = &*page_extensions.await?;
334
335        let mut children = vec![];
336        let mut items = vec![];
337        let dir_content = project_path.read_dir().await?;
338        if let DirectoryContent::Entries(entries) = &*dir_content {
339            for (name, entry) in entries.iter() {
340                match entry {
341                    DirectoryEntry::File(_) => {
342                        let Some(basename) = page_basename(name, page_extensions_raw) else {
343                            continue;
344                        };
345                        let item_next_router_path = match basename {
346                            "index" => next_router_path.clone(),
347                            _ => next_router_path.join(basename)?,
348                        };
349                        let base_path = project_path.join(name)?;
350                        let item_original_name = next_router_path.join(basename)?;
351                        items.push((
352                            basename,
353                            PagesStructureItem::new(
354                                base_path,
355                                page_extensions,
356                                None,
357                                item_next_router_path,
358                                item_original_name,
359                            ),
360                        ));
361                    }
362                    DirectoryEntry::Directory(dir_project_path) => {
363                        children.push((
364                            name,
365                            get_pages_structure_for_directory(
366                                dir_project_path.clone(),
367                                next_router_path.join(name)?,
368                                page_extensions,
369                            ),
370                        ));
371                    }
372                    _ => {}
373                }
374            }
375        }
376
377        // Ensure deterministic order since read_dir is not deterministic
378        items.sort_by_key(|(k, _)| *k);
379
380        // Ensure deterministic order since read_dir is not deterministic
381        children.sort_by_key(|(k, _)| *k);
382
383        Ok(PagesDirectoryStructure {
384            project_path: project_path.clone(),
385            next_router_path: next_router_path.clone(),
386            items: items
387                .into_iter()
388                .map(|(_, v)| v)
389                .map(|v| async move { v.to_resolved().await })
390                .try_join()
391                .await?,
392            children: children
393                .into_iter()
394                .map(|(_, v)| v)
395                .map(|v| async move { v.to_resolved().await })
396                .try_join()
397                .await?,
398        }
399        .cell())
400    }
401    .instrument(span)
402    .await
403}
404
405fn page_basename<'a>(name: &'a str, page_extensions: &'a [RcStr]) -> Option<&'a str> {
406    page_extensions
407        .iter()
408        .find_map(|allowed| name.strip_suffix(&**allowed)?.strip_suffix('.'))
409}
410
411fn next_router_path_for_basename(
412    next_router_path: FileSystemPath,
413    basename: &str,
414) -> Result<FileSystemPath> {
415    Ok(if basename == "index" {
416        next_router_path.clone()
417    } else {
418        next_router_path.join(basename)?
419    })
420}