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#[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 pub next_router_path: FileSystemPath,
20 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 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 Ok(self.base_path.clone().cell())
65 }
66 }
67}
68
69#[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 #[turbo_tasks::function]
96 pub fn project_path(&self) -> Vc<FileSystemPath> {
97 self.project_path.clone().cell()
98 }
99}
100
101#[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().owned().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().owned().await?;
114 if *src_pages_root.get_type().await? == FileSystemEntryType::Directory {
115 Some(src_pages_root)
116 } else {
117 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#[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 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 1,
213 page_extensions,
214 )
215 .to_resolved()
216 .await?,
217 );
218 }
219 _ => {
220 children.push((
221 name,
222 get_pages_structure_for_directory(
223 dir_project_path.clone(),
224 next_router_path.join(name)?,
225 1,
226 page_extensions,
227 ),
228 ));
229 }
230 },
231 _ => {}
232 }
233 }
234 }
235
236 items.sort_by_key(|(k, _)| *k);
238 children.sort_by_key(|(k, _)| *k);
239
240 Some(
241 PagesDirectoryStructure {
242 project_path: project_path.clone(),
243 next_router_path: next_router_path.clone(),
244 items: items
245 .into_iter()
246 .map(|(_, v)| async move { v.to_resolved().await })
247 .try_join()
248 .await?,
249 children: children
250 .into_iter()
251 .map(|(_, v)| async move { v.to_resolved().await })
252 .try_join()
253 .await?,
254 }
255 .resolved_cell(),
256 )
257 } else {
258 None
259 };
260
261 let pages_path = if let Some(project_path) = &*project_path {
262 project_path.clone()
263 } else {
264 project_root.join("pages")?
265 };
266
267 let has_user_pages = pages_directory.is_some() || api_directory.is_some();
271
272 let should_create_pages_entries = has_user_pages || next_mode.await?.is_development();
274
275 let app_item = {
276 let app_router_path = next_router_path.join("_app")?;
277 PagesStructureItem::new(
278 pages_path.join("_app")?,
279 page_extensions,
280 Some(
281 get_next_package(project_root.clone())
282 .await?
283 .join("app.js")?,
284 ),
285 app_router_path.clone(),
286 app_router_path,
287 )
288 };
289
290 let document_item = {
291 let document_router_path = next_router_path.join("_document")?;
292 PagesStructureItem::new(
293 pages_path.join("_document")?,
294 page_extensions,
295 Some(
296 get_next_package(project_root.clone())
297 .await?
298 .join("document.js")?,
299 ),
300 document_router_path.clone(),
301 document_router_path,
302 )
303 };
304
305 let error_item = {
306 let error_router_path = next_router_path.join("_error")?;
307 PagesStructureItem::new(
308 pages_path.join("_error")?,
309 page_extensions,
310 Some(
311 get_next_package(project_root.clone())
312 .await?
313 .join("error.js")?,
314 ),
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 position: u32,
340 page_extensions: Vc<Vec<RcStr>>,
341) -> Result<Vc<PagesDirectoryStructure>> {
342 let span = {
343 let path = project_path.value_to_string().await?.to_string();
344 tracing::info_span!("analyse pages structure", name = path)
345 };
346 async move {
347 let page_extensions_raw = &*page_extensions.await?;
348
349 let mut children = vec![];
350 let mut items = vec![];
351 let dir_content = project_path.read_dir().await?;
352 if let DirectoryContent::Entries(entries) = &*dir_content {
353 for (name, entry) in entries.iter() {
354 match entry {
355 DirectoryEntry::File(_) => {
356 let Some(basename) = page_basename(name, page_extensions_raw) else {
357 continue;
358 };
359 let item_next_router_path = match basename {
360 "index" => next_router_path.clone(),
361 _ => next_router_path.join(basename)?,
362 };
363 let base_path = project_path.join(name)?;
364 let item_original_name = next_router_path.join(basename)?;
365 items.push((
366 basename,
367 PagesStructureItem::new(
368 base_path,
369 page_extensions,
370 None,
371 item_next_router_path,
372 item_original_name,
373 ),
374 ));
375 }
376 DirectoryEntry::Directory(dir_project_path) => {
377 children.push((
378 name,
379 get_pages_structure_for_directory(
380 dir_project_path.clone(),
381 next_router_path.join(name)?,
382 position + 1,
383 page_extensions,
384 ),
385 ));
386 }
387 _ => {}
388 }
389 }
390 }
391
392 items.sort_by_key(|(k, _)| *k);
394
395 children.sort_by_key(|(k, _)| *k);
397
398 Ok(PagesDirectoryStructure {
399 project_path: project_path.clone(),
400 next_router_path: next_router_path.clone(),
401 items: items
402 .into_iter()
403 .map(|(_, v)| v)
404 .map(|v| async move { v.to_resolved().await })
405 .try_join()
406 .await?,
407 children: children
408 .into_iter()
409 .map(|(_, v)| v)
410 .map(|v| async move { v.to_resolved().await })
411 .try_join()
412 .await?,
413 }
414 .cell())
415 }
416 .instrument(span)
417 .await
418}
419
420fn page_basename<'a>(name: &'a str, page_extensions: &'a [RcStr]) -> Option<&'a str> {
421 page_extensions
422 .iter()
423 .find_map(|allowed| name.strip_suffix(&**allowed)?.strip_suffix('.'))
424}
425
426fn next_router_path_for_basename(
427 next_router_path: FileSystemPath,
428 basename: &str,
429) -> Result<FileSystemPath> {
430 Ok(if basename == "index" {
431 next_router_path.clone()
432 } else {
433 next_router_path.join(basename)?
434 })
435}