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#[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 pub next_router_path: FileSystemPath,
21 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 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 Ok(self.base_path.clone().cell())
66 }
67 }
68}
69
70#[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 #[turbo_tasks::function]
97 pub fn project_path(&self) -> Vc<FileSystemPath> {
98 self.project_path.clone().cell()
99 }
100}
101
102#[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 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#[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 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 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 let has_user_pages = pages_directory.is_some() || api_directory.is_some();
282
283 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#[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 items.sort_by_key(|(k, _)| *k);
392
393 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}