next_core/
pages_structure.rs

1use anyhow::Result;
2use tracing::Instrument;
3use turbo_rcstr::RcStr;
4use turbo_tasks::{OptionVcExt, ResolvedVc, TryJoinIterExt, ValueToString, 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: ResolvedVc<FileSystemPath>,
15    pub extensions: ResolvedVc<Vec<RcStr>>,
16    pub fallback_path: Option<ResolvedVc<FileSystemPath>>,
17
18    /// Pathname of this item in the Next.js router.
19    pub next_router_path: ResolvedVc<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: ResolvedVc<FileSystemPath>,
25}
26
27#[turbo_tasks::value_impl]
28impl PagesStructureItem {
29    #[turbo_tasks::function]
30    fn new(
31        base_path: ResolvedVc<FileSystemPath>,
32        extensions: ResolvedVc<Vec<RcStr>>,
33        fallback_path: Option<ResolvedVc<FileSystemPath>>,
34        next_router_path: ResolvedVc<FileSystemPath>,
35        original_path: ResolvedVc<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: Vc<FileSystemPath> = self.base_path.append(format!(".{ext}").into());
53            let ty = *file_path.get_type().await?;
54            if matches!(ty, FileSystemEntryType::File | FileSystemEntryType::Symlink) {
55                return Ok(file_path);
56            }
57        }
58        if let Some(fallback_path) = self.fallback_path {
59            Ok(*fallback_path)
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)
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: ResolvedVc<FileSystemPath>,
84    pub next_router_path: ResolvedVc<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
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: Vc<FileSystemPath>,
103    next_router_root: Vc<FileSystemPath>,
104    page_extensions: Vc<Vec<RcStr>>,
105) -> Result<Vc<PagesStructure>> {
106    let pages_root = project_root
107        .join("pages".into())
108        .realpath()
109        .to_resolved()
110        .await?;
111    let pages_root = if *pages_root.get_type().await? == FileSystemEntryType::Directory {
112        Some(pages_root)
113    } else {
114        let src_pages_root = project_root
115            .join("src/pages".into())
116            .realpath()
117            .to_resolved()
118            .await?;
119        if *src_pages_root.get_type().await? == FileSystemEntryType::Directory {
120            Some(src_pages_root)
121        } else {
122            // If neither pages nor src/pages exists, we still want to generate
123            // the pages structure, but with no pages and default values for
124            // _app, _document and _error.
125            None
126        }
127    };
128
129    Ok(get_pages_structure_for_root_directory(
130        project_root,
131        Vc::cell(pages_root),
132        next_router_root,
133        page_extensions,
134    ))
135}
136
137/// Handles the root pages directory.
138#[turbo_tasks::function]
139async fn get_pages_structure_for_root_directory(
140    project_root: Vc<FileSystemPath>,
141    project_path: Vc<FileSystemPathOption>,
142    next_router_path: Vc<FileSystemPath>,
143    page_extensions: Vc<Vec<RcStr>>,
144) -> Result<Vc<PagesStructure>> {
145    let page_extensions_raw = &*page_extensions.await?;
146
147    let mut api_directory = None;
148    let mut error_500_item = None;
149
150    let project_path = project_path.await?;
151    let pages_directory = if let Some(project_path) = &*project_path {
152        let mut children = vec![];
153        let mut items = vec![];
154
155        let dir_content = project_path.read_dir().await?;
156        if let DirectoryContent::Entries(entries) = &*dir_content {
157            for (name, entry) in entries.iter() {
158                let entry = entry.resolve_symlink().await?;
159                match entry {
160                    DirectoryEntry::File(_) => {
161                        // Do not process .d.ts files as routes
162                        if name.ends_with(".d.ts") {
163                            continue;
164                        }
165                        let Some(basename) = page_basename(name, page_extensions_raw) else {
166                            continue;
167                        };
168                        let base_path = project_path.join(basename.into());
169                        match basename {
170                            "_app" | "_document" | "_error" => {}
171                            "500" => {
172                                let item_next_router_path =
173                                    next_router_path_for_basename(next_router_path, basename);
174                                let item_original_path = next_router_path.join(basename.into());
175                                let item = PagesStructureItem::new(
176                                    base_path,
177                                    page_extensions,
178                                    None,
179                                    item_next_router_path,
180                                    item_original_path,
181                                );
182
183                                error_500_item = Some(item);
184
185                                items.push((basename, item));
186                            }
187
188                            basename => {
189                                let item_next_router_path =
190                                    next_router_path_for_basename(next_router_path, basename);
191                                let item_original_path = next_router_path.join(basename.into());
192                                items.push((
193                                    basename,
194                                    PagesStructureItem::new(
195                                        base_path,
196                                        page_extensions,
197                                        None,
198                                        item_next_router_path,
199                                        item_original_path,
200                                    ),
201                                ));
202                            }
203                        }
204                    }
205                    DirectoryEntry::Directory(dir_project_path) => match name.as_str() {
206                        "api" => {
207                            api_directory = Some(
208                                get_pages_structure_for_directory(
209                                    *dir_project_path,
210                                    next_router_path.join(name.clone()),
211                                    1,
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,
223                                    next_router_path.join(name.clone()),
224                                    1,
225                                    page_extensions,
226                                ),
227                            ));
228                        }
229                    },
230                    _ => {}
231                }
232            }
233        }
234
235        // Ensure deterministic order since read_dir is not deterministic
236        items.sort_by_key(|(k, _)| *k);
237        children.sort_by_key(|(k, _)| *k);
238
239        Some(
240            PagesDirectoryStructure {
241                project_path: *project_path,
242                next_router_path: next_router_path.to_resolved().await?,
243                items: items
244                    .into_iter()
245                    .map(|(_, v)| async move { v.to_resolved().await })
246                    .try_join()
247                    .await?,
248                children: children
249                    .into_iter()
250                    .map(|(_, v)| async move { v.to_resolved().await })
251                    .try_join()
252                    .await?,
253            }
254            .resolved_cell(),
255        )
256    } else {
257        None
258    };
259
260    let pages_path = if let Some(project_path) = *project_path {
261        *project_path
262    } else {
263        project_root.join("pages".into())
264    };
265
266    let app_item = {
267        let app_router_path = next_router_path.join("_app".into());
268        PagesStructureItem::new(
269            pages_path.join("_app".into()),
270            page_extensions,
271            Some(get_next_package(project_root).join("app.js".into())),
272            app_router_path,
273            app_router_path,
274        )
275    };
276
277    let document_item = {
278        let document_router_path = next_router_path.join("_document".into());
279        PagesStructureItem::new(
280            pages_path.join("_document".into()),
281            page_extensions,
282            Some(get_next_package(project_root).join("document.js".into())),
283            document_router_path,
284            document_router_path,
285        )
286    };
287
288    let error_item = {
289        let error_router_path = next_router_path.join("_error".into());
290        PagesStructureItem::new(
291            pages_path.join("_error".into()),
292            page_extensions,
293            Some(get_next_package(project_root).join("error.js".into())),
294            error_router_path,
295            error_router_path,
296        )
297    };
298
299    Ok(PagesStructure {
300        app: app_item.to_resolved().await?,
301        document: document_item.to_resolved().await?,
302        error: error_item.to_resolved().await?,
303        error_500: error_500_item.to_resolved().await?,
304        api: api_directory,
305        pages: pages_directory,
306    }
307    .cell())
308}
309
310/// Handles a directory in the pages directory (or the pages directory itself).
311/// Calls itself recursively for sub directories.
312#[turbo_tasks::function]
313async fn get_pages_structure_for_directory(
314    project_path: Vc<FileSystemPath>,
315    next_router_path: Vc<FileSystemPath>,
316    position: u32,
317    page_extensions: Vc<Vec<RcStr>>,
318) -> Result<Vc<PagesDirectoryStructure>> {
319    let span = {
320        let path = project_path.to_string().await?.to_string();
321        tracing::info_span!("analyse pages structure", name = path)
322    };
323    async move {
324        let page_extensions_raw = &*page_extensions.await?;
325
326        let mut children = vec![];
327        let mut items = vec![];
328        let dir_content = project_path.read_dir().await?;
329        if let DirectoryContent::Entries(entries) = &*dir_content {
330            for (name, entry) in entries.iter() {
331                match entry {
332                    DirectoryEntry::File(_) => {
333                        let Some(basename) = page_basename(name, page_extensions_raw) else {
334                            continue;
335                        };
336                        let item_next_router_path = match basename {
337                            "index" => next_router_path,
338                            _ => next_router_path.join(basename.into()),
339                        };
340                        let base_path = project_path.join(name.clone());
341                        let item_original_name = next_router_path.join(basename.into());
342                        items.push((
343                            basename,
344                            PagesStructureItem::new(
345                                base_path,
346                                page_extensions,
347                                None,
348                                item_next_router_path,
349                                item_original_name,
350                            ),
351                        ));
352                    }
353                    DirectoryEntry::Directory(dir_project_path) => {
354                        children.push((
355                            name,
356                            get_pages_structure_for_directory(
357                                **dir_project_path,
358                                next_router_path.join(name.clone()),
359                                position + 1,
360                                page_extensions,
361                            ),
362                        ));
363                    }
364                    _ => {}
365                }
366            }
367        }
368
369        // Ensure deterministic order since read_dir is not deterministic
370        items.sort_by_key(|(k, _)| *k);
371
372        // Ensure deterministic order since read_dir is not deterministic
373        children.sort_by_key(|(k, _)| *k);
374
375        Ok(PagesDirectoryStructure {
376            project_path: project_path.to_resolved().await?,
377            next_router_path: next_router_path.to_resolved().await?,
378            items: items
379                .into_iter()
380                .map(|(_, v)| v)
381                .map(|v| async move { v.to_resolved().await })
382                .try_join()
383                .await?,
384            children: children
385                .into_iter()
386                .map(|(_, v)| v)
387                .map(|v| async move { v.to_resolved().await })
388                .try_join()
389                .await?,
390        }
391        .cell())
392    }
393    .instrument(span)
394    .await
395}
396
397fn page_basename<'a>(name: &'a str, page_extensions: &'a [RcStr]) -> Option<&'a str> {
398    page_extensions
399        .iter()
400        .find_map(|allowed| name.strip_suffix(&**allowed)?.strip_suffix('.'))
401}
402
403fn next_router_path_for_basename(
404    next_router_path: Vc<FileSystemPath>,
405    basename: &str,
406) -> Vc<FileSystemPath> {
407    if basename == "index" {
408        next_router_path
409    } else {
410        next_router_path.join(basename.into())
411    }
412}