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}
80
81#[turbo_tasks::value]
82pub struct PagesDirectoryStructure {
83 pub project_path: FileSystemPath,
84 pub next_router_path: FileSystemPath,
85 pub items: Vec<ResolvedVc<PagesStructureItem>>,
86 pub children: Vec<ResolvedVc<PagesDirectoryStructure>>,
87}
88
89#[turbo_tasks::value_impl]
90impl PagesDirectoryStructure {
91 #[turbo_tasks::function]
94 pub fn project_path(&self) -> Vc<FileSystemPath> {
95 self.project_path.clone().cell()
96 }
97}
98
99#[turbo_tasks::function]
101pub async fn find_pages_structure(
102 project_root: FileSystemPath,
103 next_router_root: FileSystemPath,
104 page_extensions: Vc<Vec<RcStr>>,
105) -> Result<Vc<PagesStructure>> {
106 let pages_root = project_root.join("pages")?.realpath().owned().await?;
107 let pages_root = if *pages_root.get_type().await? == FileSystemEntryType::Directory {
108 Some(pages_root)
109 } else {
110 let src_pages_root = project_root.join("src/pages")?.realpath().owned().await?;
111 if *src_pages_root.get_type().await? == FileSystemEntryType::Directory {
112 Some(src_pages_root)
113 } else {
114 None
118 }
119 };
120
121 Ok(get_pages_structure_for_root_directory(
122 project_root,
123 Vc::cell(pages_root),
124 next_router_root,
125 page_extensions,
126 ))
127}
128
129#[turbo_tasks::function]
131async fn get_pages_structure_for_root_directory(
132 project_root: FileSystemPath,
133 project_path: Vc<FileSystemPathOption>,
134 next_router_path: FileSystemPath,
135 page_extensions: Vc<Vec<RcStr>>,
136) -> Result<Vc<PagesStructure>> {
137 let page_extensions_raw = &*page_extensions.await?;
138
139 let mut api_directory = None;
140 let mut error_500_item = None;
141
142 let project_path = project_path.await?;
143 let pages_directory = if let Some(project_path) = &*project_path {
144 let mut children = vec![];
145 let mut items = vec![];
146
147 let dir_content = project_path.read_dir().await?;
148 if let DirectoryContent::Entries(entries) = &*dir_content {
149 for (name, entry) in entries.iter() {
150 let entry = entry.clone().resolve_symlink().await?;
151 match entry {
152 DirectoryEntry::File(_) => {
153 if name.ends_with(".d.ts") {
155 continue;
156 }
157 let Some(basename) = page_basename(name, page_extensions_raw) else {
158 continue;
159 };
160 let base_path = project_path.join(basename)?;
161 match basename {
162 "_app" | "_document" | "_error" => {}
163 "500" => {
164 let item_next_router_path = next_router_path_for_basename(
165 next_router_path.clone(),
166 basename,
167 )?;
168 let item_original_path = next_router_path.join(basename)?;
169 let item = PagesStructureItem::new(
170 base_path,
171 page_extensions,
172 None,
173 item_next_router_path,
174 item_original_path,
175 );
176
177 error_500_item = Some(item);
178
179 items.push((basename, item));
180 }
181
182 basename => {
183 let item_next_router_path = next_router_path_for_basename(
184 next_router_path.clone(),
185 basename,
186 )?;
187 let item_original_path = next_router_path.join(basename)?;
188 items.push((
189 basename,
190 PagesStructureItem::new(
191 base_path,
192 page_extensions,
193 None,
194 item_next_router_path,
195 item_original_path,
196 ),
197 ));
198 }
199 }
200 }
201 DirectoryEntry::Directory(dir_project_path) => match name.as_str() {
202 "api" => {
203 api_directory = Some(
204 get_pages_structure_for_directory(
205 dir_project_path.clone(),
206 next_router_path.join(name)?,
207 1,
208 page_extensions,
209 )
210 .to_resolved()
211 .await?,
212 );
213 }
214 _ => {
215 children.push((
216 name,
217 get_pages_structure_for_directory(
218 dir_project_path.clone(),
219 next_router_path.join(name)?,
220 1,
221 page_extensions,
222 ),
223 ));
224 }
225 },
226 _ => {}
227 }
228 }
229 }
230
231 items.sort_by_key(|(k, _)| *k);
233 children.sort_by_key(|(k, _)| *k);
234
235 Some(
236 PagesDirectoryStructure {
237 project_path: project_path.clone(),
238 next_router_path: next_router_path.clone(),
239 items: items
240 .into_iter()
241 .map(|(_, v)| async move { v.to_resolved().await })
242 .try_join()
243 .await?,
244 children: children
245 .into_iter()
246 .map(|(_, v)| async move { v.to_resolved().await })
247 .try_join()
248 .await?,
249 }
250 .resolved_cell(),
251 )
252 } else {
253 None
254 };
255
256 let pages_path = if let Some(project_path) = &*project_path {
257 project_path.clone()
258 } else {
259 project_root.join("pages")?
260 };
261
262 let app_item = {
263 let app_router_path = next_router_path.join("_app")?;
264 PagesStructureItem::new(
265 pages_path.join("_app")?,
266 page_extensions,
267 Some(
268 get_next_package(project_root.clone())
269 .await?
270 .join("app.js")?,
271 ),
272 app_router_path.clone(),
273 app_router_path,
274 )
275 };
276
277 let document_item = {
278 let document_router_path = next_router_path.join("_document")?;
279 PagesStructureItem::new(
280 pages_path.join("_document")?,
281 page_extensions,
282 Some(
283 get_next_package(project_root.clone())
284 .await?
285 .join("document.js")?,
286 ),
287 document_router_path.clone(),
288 document_router_path,
289 )
290 };
291
292 let error_item = {
293 let error_router_path = next_router_path.join("_error")?;
294 PagesStructureItem::new(
295 pages_path.join("_error")?,
296 page_extensions,
297 Some(
298 get_next_package(project_root.clone())
299 .await?
300 .join("error.js")?,
301 ),
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 }
315 .cell())
316}
317
318#[turbo_tasks::function]
321async fn get_pages_structure_for_directory(
322 project_path: FileSystemPath,
323 next_router_path: FileSystemPath,
324 position: u32,
325 page_extensions: Vc<Vec<RcStr>>,
326) -> Result<Vc<PagesDirectoryStructure>> {
327 let span = {
328 let path = project_path.value_to_string().await?.to_string();
329 tracing::info_span!("analyse pages structure", name = path)
330 };
331 async move {
332 let page_extensions_raw = &*page_extensions.await?;
333
334 let mut children = vec![];
335 let mut items = vec![];
336 let dir_content = project_path.read_dir().await?;
337 if let DirectoryContent::Entries(entries) = &*dir_content {
338 for (name, entry) in entries.iter() {
339 match entry {
340 DirectoryEntry::File(_) => {
341 let Some(basename) = page_basename(name, page_extensions_raw) else {
342 continue;
343 };
344 let item_next_router_path = match basename {
345 "index" => next_router_path.clone(),
346 _ => next_router_path.join(basename)?,
347 };
348 let base_path = project_path.join(name)?;
349 let item_original_name = next_router_path.join(basename)?;
350 items.push((
351 basename,
352 PagesStructureItem::new(
353 base_path,
354 page_extensions,
355 None,
356 item_next_router_path,
357 item_original_name,
358 ),
359 ));
360 }
361 DirectoryEntry::Directory(dir_project_path) => {
362 children.push((
363 name,
364 get_pages_structure_for_directory(
365 dir_project_path.clone(),
366 next_router_path.join(name)?,
367 position + 1,
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}