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().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().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 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.clone(),
223 next_router_path.join(name)?,
224 page_extensions,
225 ),
226 ));
227 }
228 },
229 _ => {}
230 }
231 }
232 }
233
234 items.sort_by_key(|(k, _)| *k);
236 children.sort_by_key(|(k, _)| *k);
237
238 Some(
239 PagesDirectoryStructure {
240 project_path: project_path.clone(),
241 next_router_path: next_router_path.clone(),
242 items: items
243 .into_iter()
244 .map(|(_, v)| async move { v.to_resolved().await })
245 .try_join()
246 .await?,
247 children: children
248 .into_iter()
249 .map(|(_, v)| async move { v.to_resolved().await })
250 .try_join()
251 .await?,
252 }
253 .resolved_cell(),
254 )
255 } else {
256 None
257 };
258
259 let pages_path = if let Some(project_path) = &*project_path {
260 project_path.clone()
261 } else {
262 project_root.join("pages")?
263 };
264
265 let has_user_pages = pages_directory.is_some() || api_directory.is_some();
269
270 let should_create_pages_entries = has_user_pages || next_mode.await?.is_development();
272 let next_package = get_next_package(project_root.clone()).await?;
273
274 let app_item = {
275 let app_router_path = next_router_path.join("_app")?;
276 PagesStructureItem::new(
277 pages_path.join("_app")?,
278 page_extensions,
279 Some(next_package.join("app.js")?),
280 app_router_path.clone(),
281 app_router_path,
282 )
283 };
284
285 let document_item = {
286 let document_router_path = next_router_path.join("_document")?;
287 PagesStructureItem::new(
288 pages_path.join("_document")?,
289 page_extensions,
290 Some(next_package.join("document.js")?),
291 document_router_path.clone(),
292 document_router_path,
293 )
294 };
295
296 let error_item = {
297 let error_router_path = next_router_path.join("_error")?;
298 PagesStructureItem::new(
299 pages_path.join("_error")?,
300 page_extensions,
301 Some(next_package.join("error.js")?),
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 has_user_pages,
315 should_create_pages_entries,
316 }
317 .cell())
318}
319
320#[turbo_tasks::function]
323async fn get_pages_structure_for_directory(
324 project_path: FileSystemPath,
325 next_router_path: FileSystemPath,
326 page_extensions: Vc<Vec<RcStr>>,
327) -> Result<Vc<PagesDirectoryStructure>> {
328 let span = tracing::info_span!(
329 "analyze pages structure",
330 name = display(project_path.value_to_string().await?)
331 );
332 async move {
333 let page_extensions_raw = &*page_extensions.await?;
334
335 let mut children = vec![];
336 let mut items = vec![];
337 let dir_content = project_path.read_dir().await?;
338 if let DirectoryContent::Entries(entries) = &*dir_content {
339 for (name, entry) in entries.iter() {
340 match entry {
341 DirectoryEntry::File(_) => {
342 let Some(basename) = page_basename(name, page_extensions_raw) else {
343 continue;
344 };
345 let item_next_router_path = match basename {
346 "index" => next_router_path.clone(),
347 _ => next_router_path.join(basename)?,
348 };
349 let base_path = project_path.join(name)?;
350 let item_original_name = next_router_path.join(basename)?;
351 items.push((
352 basename,
353 PagesStructureItem::new(
354 base_path,
355 page_extensions,
356 None,
357 item_next_router_path,
358 item_original_name,
359 ),
360 ));
361 }
362 DirectoryEntry::Directory(dir_project_path) => {
363 children.push((
364 name,
365 get_pages_structure_for_directory(
366 dir_project_path.clone(),
367 next_router_path.join(name)?,
368 page_extensions,
369 ),
370 ));
371 }
372 _ => {}
373 }
374 }
375 }
376
377 items.sort_by_key(|(k, _)| *k);
379
380 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}