1use anyhow::Result;
2use tracing::Instrument;
3use turbo_rcstr::RcStr;
4use turbo_tasks::{OptionVcExt, ResolvedVc, TryJoinIterExt, ValueToString, 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: ResolvedVc<FileSystemPath>,
15 pub extensions: ResolvedVc<Vec<RcStr>>,
16 pub fallback_path: Option<ResolvedVc<FileSystemPath>>,
17
18 pub next_router_path: ResolvedVc<FileSystemPath>,
20 pub original_path: ResolvedVc<FileSystemPath>,
25}
26
27#[turbo_tasks::value_impl]
28impl PagesStructureItem {
29 #[turbo_tasks::function]
30 fn new(
31 base_path: ResolvedVc<FileSystemPath>,
32 extensions: ResolvedVc<Vec<RcStr>>,
33 fallback_path: Option<ResolvedVc<FileSystemPath>>,
34 next_router_path: ResolvedVc<FileSystemPath>,
35 original_path: ResolvedVc<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: Vc<FileSystemPath> = self.base_path.append(format!(".{ext}").into());
53 let ty = *file_path.get_type().await?;
54 if matches!(ty, FileSystemEntryType::File | FileSystemEntryType::Symlink) {
55 return Ok(file_path);
56 }
57 }
58 if let Some(fallback_path) = self.fallback_path {
59 Ok(*fallback_path)
60 } else {
61 Ok(*self.base_path)
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: ResolvedVc<FileSystemPath>,
84 pub next_router_path: ResolvedVc<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
96 }
97}
98
99#[turbo_tasks::function]
101pub async fn find_pages_structure(
102 project_root: Vc<FileSystemPath>,
103 next_router_root: Vc<FileSystemPath>,
104 page_extensions: Vc<Vec<RcStr>>,
105) -> Result<Vc<PagesStructure>> {
106 let pages_root = project_root
107 .join("pages".into())
108 .realpath()
109 .to_resolved()
110 .await?;
111 let pages_root = if *pages_root.get_type().await? == FileSystemEntryType::Directory {
112 Some(pages_root)
113 } else {
114 let src_pages_root = project_root
115 .join("src/pages".into())
116 .realpath()
117 .to_resolved()
118 .await?;
119 if *src_pages_root.get_type().await? == FileSystemEntryType::Directory {
120 Some(src_pages_root)
121 } else {
122 None
126 }
127 };
128
129 Ok(get_pages_structure_for_root_directory(
130 project_root,
131 Vc::cell(pages_root),
132 next_router_root,
133 page_extensions,
134 ))
135}
136
137#[turbo_tasks::function]
139async fn get_pages_structure_for_root_directory(
140 project_root: Vc<FileSystemPath>,
141 project_path: Vc<FileSystemPathOption>,
142 next_router_path: Vc<FileSystemPath>,
143 page_extensions: Vc<Vec<RcStr>>,
144) -> Result<Vc<PagesStructure>> {
145 let page_extensions_raw = &*page_extensions.await?;
146
147 let mut api_directory = None;
148 let mut error_500_item = None;
149
150 let project_path = project_path.await?;
151 let pages_directory = if let Some(project_path) = &*project_path {
152 let mut children = vec![];
153 let mut items = vec![];
154
155 let dir_content = project_path.read_dir().await?;
156 if let DirectoryContent::Entries(entries) = &*dir_content {
157 for (name, entry) in entries.iter() {
158 let entry = entry.resolve_symlink().await?;
159 match entry {
160 DirectoryEntry::File(_) => {
161 if name.ends_with(".d.ts") {
163 continue;
164 }
165 let Some(basename) = page_basename(name, page_extensions_raw) else {
166 continue;
167 };
168 let base_path = project_path.join(basename.into());
169 match basename {
170 "_app" | "_document" | "_error" => {}
171 "500" => {
172 let item_next_router_path =
173 next_router_path_for_basename(next_router_path, basename);
174 let item_original_path = next_router_path.join(basename.into());
175 let item = PagesStructureItem::new(
176 base_path,
177 page_extensions,
178 None,
179 item_next_router_path,
180 item_original_path,
181 );
182
183 error_500_item = Some(item);
184
185 items.push((basename, item));
186 }
187
188 basename => {
189 let item_next_router_path =
190 next_router_path_for_basename(next_router_path, basename);
191 let item_original_path = next_router_path.join(basename.into());
192 items.push((
193 basename,
194 PagesStructureItem::new(
195 base_path,
196 page_extensions,
197 None,
198 item_next_router_path,
199 item_original_path,
200 ),
201 ));
202 }
203 }
204 }
205 DirectoryEntry::Directory(dir_project_path) => match name.as_str() {
206 "api" => {
207 api_directory = Some(
208 get_pages_structure_for_directory(
209 *dir_project_path,
210 next_router_path.join(name.clone()),
211 1,
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,
223 next_router_path.join(name.clone()),
224 1,
225 page_extensions,
226 ),
227 ));
228 }
229 },
230 _ => {}
231 }
232 }
233 }
234
235 items.sort_by_key(|(k, _)| *k);
237 children.sort_by_key(|(k, _)| *k);
238
239 Some(
240 PagesDirectoryStructure {
241 project_path: *project_path,
242 next_router_path: next_router_path.to_resolved().await?,
243 items: items
244 .into_iter()
245 .map(|(_, v)| async move { v.to_resolved().await })
246 .try_join()
247 .await?,
248 children: children
249 .into_iter()
250 .map(|(_, v)| async move { v.to_resolved().await })
251 .try_join()
252 .await?,
253 }
254 .resolved_cell(),
255 )
256 } else {
257 None
258 };
259
260 let pages_path = if let Some(project_path) = *project_path {
261 *project_path
262 } else {
263 project_root.join("pages".into())
264 };
265
266 let app_item = {
267 let app_router_path = next_router_path.join("_app".into());
268 PagesStructureItem::new(
269 pages_path.join("_app".into()),
270 page_extensions,
271 Some(get_next_package(project_root).join("app.js".into())),
272 app_router_path,
273 app_router_path,
274 )
275 };
276
277 let document_item = {
278 let document_router_path = next_router_path.join("_document".into());
279 PagesStructureItem::new(
280 pages_path.join("_document".into()),
281 page_extensions,
282 Some(get_next_package(project_root).join("document.js".into())),
283 document_router_path,
284 document_router_path,
285 )
286 };
287
288 let error_item = {
289 let error_router_path = next_router_path.join("_error".into());
290 PagesStructureItem::new(
291 pages_path.join("_error".into()),
292 page_extensions,
293 Some(get_next_package(project_root).join("error.js".into())),
294 error_router_path,
295 error_router_path,
296 )
297 };
298
299 Ok(PagesStructure {
300 app: app_item.to_resolved().await?,
301 document: document_item.to_resolved().await?,
302 error: error_item.to_resolved().await?,
303 error_500: error_500_item.to_resolved().await?,
304 api: api_directory,
305 pages: pages_directory,
306 }
307 .cell())
308}
309
310#[turbo_tasks::function]
313async fn get_pages_structure_for_directory(
314 project_path: Vc<FileSystemPath>,
315 next_router_path: Vc<FileSystemPath>,
316 position: u32,
317 page_extensions: Vc<Vec<RcStr>>,
318) -> Result<Vc<PagesDirectoryStructure>> {
319 let span = {
320 let path = project_path.to_string().await?.to_string();
321 tracing::info_span!("analyse pages structure", name = path)
322 };
323 async move {
324 let page_extensions_raw = &*page_extensions.await?;
325
326 let mut children = vec![];
327 let mut items = vec![];
328 let dir_content = project_path.read_dir().await?;
329 if let DirectoryContent::Entries(entries) = &*dir_content {
330 for (name, entry) in entries.iter() {
331 match entry {
332 DirectoryEntry::File(_) => {
333 let Some(basename) = page_basename(name, page_extensions_raw) else {
334 continue;
335 };
336 let item_next_router_path = match basename {
337 "index" => next_router_path,
338 _ => next_router_path.join(basename.into()),
339 };
340 let base_path = project_path.join(name.clone());
341 let item_original_name = next_router_path.join(basename.into());
342 items.push((
343 basename,
344 PagesStructureItem::new(
345 base_path,
346 page_extensions,
347 None,
348 item_next_router_path,
349 item_original_name,
350 ),
351 ));
352 }
353 DirectoryEntry::Directory(dir_project_path) => {
354 children.push((
355 name,
356 get_pages_structure_for_directory(
357 **dir_project_path,
358 next_router_path.join(name.clone()),
359 position + 1,
360 page_extensions,
361 ),
362 ));
363 }
364 _ => {}
365 }
366 }
367 }
368
369 items.sort_by_key(|(k, _)| *k);
371
372 children.sort_by_key(|(k, _)| *k);
374
375 Ok(PagesDirectoryStructure {
376 project_path: project_path.to_resolved().await?,
377 next_router_path: next_router_path.to_resolved().await?,
378 items: items
379 .into_iter()
380 .map(|(_, v)| v)
381 .map(|v| async move { v.to_resolved().await })
382 .try_join()
383 .await?,
384 children: children
385 .into_iter()
386 .map(|(_, v)| v)
387 .map(|v| async move { v.to_resolved().await })
388 .try_join()
389 .await?,
390 }
391 .cell())
392 }
393 .instrument(span)
394 .await
395}
396
397fn page_basename<'a>(name: &'a str, page_extensions: &'a [RcStr]) -> Option<&'a str> {
398 page_extensions
399 .iter()
400 .find_map(|allowed| name.strip_suffix(&**allowed)?.strip_suffix('.'))
401}
402
403fn next_router_path_for_basename(
404 next_router_path: Vc<FileSystemPath>,
405 basename: &str,
406) -> Vc<FileSystemPath> {
407 if basename == "index" {
408 next_router_path
409 } else {
410 next_router_path.join(basename.into())
411 }
412}