turbopack_dev_server/source/
static_assets.rs1use anyhow::Result;
2use turbo_rcstr::{RcStr, rcstr};
3use turbo_tasks::{ResolvedVc, TryJoinIterExt, Vc};
4use turbo_tasks_fs::{DirectoryContent, DirectoryEntry, FileSystemPath};
5use turbopack_core::{
6 asset::Asset,
7 file_source::FileSource,
8 introspect::{Introspectable, IntrospectableChildren, source::IntrospectableSource},
9 version::VersionedContentExt,
10};
11
12use super::{
13 ContentSource, ContentSourceContent, ContentSourceData, GetContentSourceContent,
14 route_tree::{BaseSegment, RouteTree, RouteTrees, RouteType},
15};
16
17#[turbo_tasks::value(shared)]
18pub struct StaticAssetsContentSource {
19 pub prefix: ResolvedVc<RcStr>,
20 pub dir: FileSystemPath,
21}
22
23#[turbo_tasks::value_impl]
24impl StaticAssetsContentSource {
25 #[turbo_tasks::function]
27 pub fn new(prefix: RcStr, dir: FileSystemPath) -> Vc<StaticAssetsContentSource> {
28 StaticAssetsContentSource::with_prefix(Vc::cell(prefix), dir)
29 }
30
31 #[turbo_tasks::function]
32 pub async fn with_prefix(
33 prefix: ResolvedVc<RcStr>,
34 dir: FileSystemPath,
35 ) -> Result<Vc<StaticAssetsContentSource>> {
36 if cfg!(debug_assertions) {
37 let prefix_string = prefix.await?;
38 debug_assert!(prefix_string.is_empty() || prefix_string.ends_with('/'));
39 debug_assert!(!prefix_string.starts_with('/'));
40 }
41 Ok(StaticAssetsContentSource { prefix, dir }.cell())
42 }
43}
44
45#[turbo_tasks::function]
47async fn get_routes_from_directory(dir: FileSystemPath) -> Result<Vc<RouteTree>> {
48 let dir = dir.read_dir().await?;
49 let DirectoryContent::Entries(entries) = &*dir else {
50 return Ok(RouteTree::empty());
51 };
52
53 let routes = entries
54 .iter()
55 .flat_map(|(name, entry)| match entry {
56 DirectoryEntry::File(path) | DirectoryEntry::Symlink(path) => {
57 Some(RouteTree::new_route(
58 vec![BaseSegment::Static(name.clone())],
59 RouteType::Exact,
60 Vc::upcast(StaticAssetsContentSourceItem::new(path.clone())),
61 ))
62 }
63 DirectoryEntry::Directory(path) => Some(
64 get_routes_from_directory(path.clone())
65 .with_prepended_base(vec![BaseSegment::Static(name.clone())]),
66 ),
67 _ => None,
68 })
69 .map(|v| async move { v.to_resolved().await })
70 .try_join()
71 .await?;
72 Ok(Vc::<RouteTrees>::cell(routes).merge())
73}
74
75#[turbo_tasks::value_impl]
76impl ContentSource for StaticAssetsContentSource {
77 #[turbo_tasks::function]
78 async fn get_routes(&self) -> Result<Vc<RouteTree>> {
79 let prefix = self.prefix.await?;
80 let prefix = BaseSegment::from_static_pathname(prefix.as_str()).collect::<Vec<_>>();
81 Ok(get_routes_from_directory(self.dir.clone()).with_prepended_base(prefix))
82 }
83}
84
85#[turbo_tasks::value]
86struct StaticAssetsContentSourceItem {
87 path: FileSystemPath,
88}
89
90#[turbo_tasks::value_impl]
91impl StaticAssetsContentSourceItem {
92 #[turbo_tasks::function]
93 pub fn new(path: FileSystemPath) -> Vc<StaticAssetsContentSourceItem> {
94 StaticAssetsContentSourceItem { path }.cell()
95 }
96}
97
98#[turbo_tasks::value_impl]
99impl GetContentSourceContent for StaticAssetsContentSourceItem {
100 #[turbo_tasks::function]
101 fn get(&self, _path: RcStr, _data: ContentSourceData) -> Vc<ContentSourceContent> {
102 let content = Vc::upcast::<Box<dyn Asset>>(FileSource::new(self.path.clone())).content();
103 ContentSourceContent::static_content(content.versioned())
104 }
105}
106
107#[turbo_tasks::value_impl]
108impl Introspectable for StaticAssetsContentSource {
109 #[turbo_tasks::function]
110 fn ty(&self) -> Vc<RcStr> {
111 Vc::cell(rcstr!("static assets directory content source"))
112 }
113
114 #[turbo_tasks::function]
115 async fn children(&self) -> Result<Vc<IntrospectableChildren>> {
116 let dir = self.dir.read_dir().await?;
117 let DirectoryContent::Entries(entries) = &*dir else {
118 return Ok(Vc::cell(Default::default()));
119 };
120
121 let prefix = self.prefix.await?;
122 let children = entries
123 .iter()
124 .map(move |(name, entry)| {
125 let prefix = prefix.clone();
126 async move {
127 let child = match entry {
128 DirectoryEntry::File(path) | DirectoryEntry::Symlink(path) => {
129 ResolvedVc::upcast(
130 IntrospectableSource::new(Vc::upcast(FileSource::new(
131 path.clone(),
132 )))
133 .to_resolved()
134 .await?,
135 )
136 }
137 DirectoryEntry::Directory(path) => ResolvedVc::upcast(
138 StaticAssetsContentSource::with_prefix(
139 Vc::cell(format!("{}{name}/", &*prefix).into()),
140 path.clone(),
141 )
142 .to_resolved()
143 .await?,
144 ),
145 DirectoryEntry::Other(_) | DirectoryEntry::Error => {
146 todo!("unsupported DirectoryContent variant: {entry:?}")
147 }
148 };
149 Ok((name.clone(), child))
150 }
151 })
152 .try_join()
153 .await?
154 .into_iter()
155 .collect();
156 Ok(Vc::cell(children))
157 }
158}