turbopack_dev_server/source/
static_assets.rs1use anyhow::Result;
2use turbo_rcstr::RcStr;
3use turbo_tasks::{ResolvedVc, TryJoinIterExt, Value, 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: ResolvedVc<FileSystemPath>,
21}
22
23#[turbo_tasks::value_impl]
24impl StaticAssetsContentSource {
25 #[turbo_tasks::function]
27 pub fn new(prefix: RcStr, dir: Vc<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: ResolvedVc<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: Vc<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)),
61 ))
62 }
63 DirectoryEntry::Directory(path) => Some(
64 get_routes_from_directory(**path)
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).with_prepended_base(prefix))
82 }
83}
84
85#[turbo_tasks::value]
86struct StaticAssetsContentSourceItem {
87 path: ResolvedVc<FileSystemPath>,
88}
89
90#[turbo_tasks::value_impl]
91impl StaticAssetsContentSourceItem {
92 #[turbo_tasks::function]
93 pub fn new(path: ResolvedVc<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: Value<ContentSourceData>) -> Vc<ContentSourceContent> {
102 let content = Vc::upcast::<Box<dyn Asset>>(FileSource::new(*self.path)).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("static assets directory content source".into())
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(**path)))
131 .to_resolved()
132 .await?,
133 )
134 }
135 DirectoryEntry::Directory(path) => ResolvedVc::upcast(
136 StaticAssetsContentSource::with_prefix(
137 Vc::cell(format!("{}{name}/", &*prefix).into()),
138 **path,
139 )
140 .to_resolved()
141 .await?,
142 ),
143 DirectoryEntry::Other(_) | DirectoryEntry::Error => {
144 todo!("unsupported DirectoryContent variant: {entry:?}")
145 }
146 };
147 Ok((ResolvedVc::cell(name.clone()), child))
148 }
149 })
150 .try_join()
151 .await?
152 .into_iter()
153 .collect();
154 Ok(Vc::cell(children))
155 }
156}