turbopack_dev_server/source/
static_assets.rs

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