turbopack_dev_server/source/
static_assets.rs

1use 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    // TODO(WEB-1151): Remove this method and migrate users to `with_prefix`.
26    #[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// TODO(WEB-1251) It would be better to lazily enumerate the directory
46#[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}