Skip to main content

next_core/
pages_structure.rs

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