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}
80
81#[turbo_tasks::value]
82pub struct PagesDirectoryStructure {
83    pub project_path: FileSystemPath,
84    pub next_router_path: FileSystemPath,
85    pub items: Vec<ResolvedVc<PagesStructureItem>>,
86    pub children: Vec<ResolvedVc<PagesDirectoryStructure>>,
87}
88
89#[turbo_tasks::value_impl]
90impl PagesDirectoryStructure {
91    /// Returns the path to the directory of this structure in the project file
92    /// system.
93    #[turbo_tasks::function]
94    pub fn project_path(&self) -> Vc<FileSystemPath> {
95        self.project_path.clone().cell()
96    }
97}
98
99/// Finds and returns the [PagesStructure] of the pages directory if existing.
100#[turbo_tasks::function]
101pub async fn find_pages_structure(
102    project_root: FileSystemPath,
103    next_router_root: FileSystemPath,
104    page_extensions: Vc<Vec<RcStr>>,
105) -> Result<Vc<PagesStructure>> {
106    let pages_root = project_root.join("pages")?.realpath().owned().await?;
107    let pages_root = if *pages_root.get_type().await? == FileSystemEntryType::Directory {
108        Some(pages_root)
109    } else {
110        let src_pages_root = project_root.join("src/pages")?.realpath().owned().await?;
111        if *src_pages_root.get_type().await? == FileSystemEntryType::Directory {
112            Some(src_pages_root)
113        } else {
114            // If neither pages nor src/pages exists, we still want to generate
115            // the pages structure, but with no pages and default values for
116            // _app, _document and _error.
117            None
118        }
119    };
120
121    Ok(get_pages_structure_for_root_directory(
122        project_root,
123        Vc::cell(pages_root),
124        next_router_root,
125        page_extensions,
126    ))
127}
128
129/// Handles the root pages directory.
130#[turbo_tasks::function]
131async fn get_pages_structure_for_root_directory(
132    project_root: FileSystemPath,
133    project_path: Vc<FileSystemPathOption>,
134    next_router_path: FileSystemPath,
135    page_extensions: Vc<Vec<RcStr>>,
136) -> Result<Vc<PagesStructure>> {
137    let page_extensions_raw = &*page_extensions.await?;
138
139    let mut api_directory = None;
140    let mut error_500_item = None;
141
142    let project_path = project_path.await?;
143    let pages_directory = if let Some(project_path) = &*project_path {
144        let mut children = vec![];
145        let mut items = vec![];
146
147        let dir_content = project_path.read_dir().await?;
148        if let DirectoryContent::Entries(entries) = &*dir_content {
149            for (name, entry) in entries.iter() {
150                let entry = entry.clone().resolve_symlink().await?;
151                match entry {
152                    DirectoryEntry::File(_) => {
153                        // Do not process .d.ts files as routes
154                        if name.ends_with(".d.ts") {
155                            continue;
156                        }
157                        let Some(basename) = page_basename(name, page_extensions_raw) else {
158                            continue;
159                        };
160                        let base_path = project_path.join(basename)?;
161                        match basename {
162                            "_app" | "_document" | "_error" => {}
163                            "500" => {
164                                let item_next_router_path = next_router_path_for_basename(
165                                    next_router_path.clone(),
166                                    basename,
167                                )?;
168                                let item_original_path = next_router_path.join(basename)?;
169                                let item = PagesStructureItem::new(
170                                    base_path,
171                                    page_extensions,
172                                    None,
173                                    item_next_router_path,
174                                    item_original_path,
175                                );
176
177                                error_500_item = Some(item);
178
179                                items.push((basename, item));
180                            }
181
182                            basename => {
183                                let item_next_router_path = next_router_path_for_basename(
184                                    next_router_path.clone(),
185                                    basename,
186                                )?;
187                                let item_original_path = next_router_path.join(basename)?;
188                                items.push((
189                                    basename,
190                                    PagesStructureItem::new(
191                                        base_path,
192                                        page_extensions,
193                                        None,
194                                        item_next_router_path,
195                                        item_original_path,
196                                    ),
197                                ));
198                            }
199                        }
200                    }
201                    DirectoryEntry::Directory(dir_project_path) => match name.as_str() {
202                        "api" => {
203                            api_directory = Some(
204                                get_pages_structure_for_directory(
205                                    dir_project_path.clone(),
206                                    next_router_path.join(name)?,
207                                    1,
208                                    page_extensions,
209                                )
210                                .to_resolved()
211                                .await?,
212                            );
213                        }
214                        _ => {
215                            children.push((
216                                name,
217                                get_pages_structure_for_directory(
218                                    dir_project_path.clone(),
219                                    next_router_path.join(name)?,
220                                    1,
221                                    page_extensions,
222                                ),
223                            ));
224                        }
225                    },
226                    _ => {}
227                }
228            }
229        }
230
231        // Ensure deterministic order since read_dir is not deterministic
232        items.sort_by_key(|(k, _)| *k);
233        children.sort_by_key(|(k, _)| *k);
234
235        Some(
236            PagesDirectoryStructure {
237                project_path: project_path.clone(),
238                next_router_path: next_router_path.clone(),
239                items: items
240                    .into_iter()
241                    .map(|(_, v)| async move { v.to_resolved().await })
242                    .try_join()
243                    .await?,
244                children: children
245                    .into_iter()
246                    .map(|(_, v)| async move { v.to_resolved().await })
247                    .try_join()
248                    .await?,
249            }
250            .resolved_cell(),
251        )
252    } else {
253        None
254    };
255
256    let pages_path = if let Some(project_path) = &*project_path {
257        project_path.clone()
258    } else {
259        project_root.join("pages")?
260    };
261
262    let app_item = {
263        let app_router_path = next_router_path.join("_app")?;
264        PagesStructureItem::new(
265            pages_path.join("_app")?,
266            page_extensions,
267            Some(
268                get_next_package(project_root.clone())
269                    .await?
270                    .join("app.js")?,
271            ),
272            app_router_path.clone(),
273            app_router_path,
274        )
275    };
276
277    let document_item = {
278        let document_router_path = next_router_path.join("_document")?;
279        PagesStructureItem::new(
280            pages_path.join("_document")?,
281            page_extensions,
282            Some(
283                get_next_package(project_root.clone())
284                    .await?
285                    .join("document.js")?,
286            ),
287            document_router_path.clone(),
288            document_router_path,
289        )
290    };
291
292    let error_item = {
293        let error_router_path = next_router_path.join("_error")?;
294        PagesStructureItem::new(
295            pages_path.join("_error")?,
296            page_extensions,
297            Some(
298                get_next_package(project_root.clone())
299                    .await?
300                    .join("error.js")?,
301            ),
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    }
315    .cell())
316}
317
318/// Handles a directory in the pages directory (or the pages directory itself).
319/// Calls itself recursively for sub directories.
320#[turbo_tasks::function]
321async fn get_pages_structure_for_directory(
322    project_path: FileSystemPath,
323    next_router_path: FileSystemPath,
324    position: u32,
325    page_extensions: Vc<Vec<RcStr>>,
326) -> Result<Vc<PagesDirectoryStructure>> {
327    let span = {
328        let path = project_path.value_to_string().await?.to_string();
329        tracing::info_span!("analyse pages structure", name = path)
330    };
331    async move {
332        let page_extensions_raw = &*page_extensions.await?;
333
334        let mut children = vec![];
335        let mut items = vec![];
336        let dir_content = project_path.read_dir().await?;
337        if let DirectoryContent::Entries(entries) = &*dir_content {
338            for (name, entry) in entries.iter() {
339                match entry {
340                    DirectoryEntry::File(_) => {
341                        let Some(basename) = page_basename(name, page_extensions_raw) else {
342                            continue;
343                        };
344                        let item_next_router_path = match basename {
345                            "index" => next_router_path.clone(),
346                            _ => next_router_path.join(basename)?,
347                        };
348                        let base_path = project_path.join(name)?;
349                        let item_original_name = next_router_path.join(basename)?;
350                        items.push((
351                            basename,
352                            PagesStructureItem::new(
353                                base_path,
354                                page_extensions,
355                                None,
356                                item_next_router_path,
357                                item_original_name,
358                            ),
359                        ));
360                    }
361                    DirectoryEntry::Directory(dir_project_path) => {
362                        children.push((
363                            name,
364                            get_pages_structure_for_directory(
365                                dir_project_path.clone(),
366                                next_router_path.join(name)?,
367                                position + 1,
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}