turbopack_dev_server/source/
asset_graph.rs

1use std::{collections::VecDeque, iter::once};
2
3use anyhow::Result;
4use rustc_hash::FxHashSet;
5use turbo_rcstr::RcStr;
6use turbo_tasks::{
7    Completion, FxIndexMap, FxIndexSet, ResolvedVc, State, TryJoinIterExt, Value, ValueToString,
8    Vc, fxindexset,
9};
10use turbo_tasks_fs::FileSystemPath;
11use turbopack_core::{
12    asset::Asset,
13    introspect::{Introspectable, IntrospectableChildren, output_asset::IntrospectableOutputAsset},
14    output::{OutputAsset, OutputAssetsSet},
15};
16
17use super::{
18    ContentSource, ContentSourceContent, ContentSourceData, ContentSourceSideEffect,
19    GetContentSourceContent,
20    route_tree::{BaseSegment, RouteTree, RouteTrees, RouteType},
21};
22
23#[turbo_tasks::value(transparent)]
24struct OutputAssetsMap(FxIndexMap<RcStr, ResolvedVc<Box<dyn OutputAsset>>>);
25
26type ExpandedState = State<FxHashSet<RcStr>>;
27
28#[turbo_tasks::value(serialization = "none", eq = "manual", cell = "new")]
29pub struct AssetGraphContentSource {
30    root_path: ResolvedVc<FileSystemPath>,
31    root_assets: ResolvedVc<OutputAssetsSet>,
32    expanded: Option<ExpandedState>,
33}
34
35#[turbo_tasks::value_impl]
36impl AssetGraphContentSource {
37    /// Serves all assets references by root_asset.
38    #[turbo_tasks::function]
39    pub fn new_eager(
40        root_path: ResolvedVc<FileSystemPath>,
41        root_asset: ResolvedVc<Box<dyn OutputAsset>>,
42    ) -> Vc<Self> {
43        Self::cell(AssetGraphContentSource {
44            root_path,
45            root_assets: ResolvedVc::cell(fxindexset! { root_asset }),
46            expanded: None,
47        })
48    }
49
50    /// Serves all assets references by root_asset. Only serve references of an
51    /// asset when it has served its content before.
52    #[turbo_tasks::function]
53    pub fn new_lazy(
54        root_path: ResolvedVc<FileSystemPath>,
55        root_asset: ResolvedVc<Box<dyn OutputAsset>>,
56    ) -> Vc<Self> {
57        Self::cell(AssetGraphContentSource {
58            root_path,
59            root_assets: ResolvedVc::cell(fxindexset! { root_asset }),
60            expanded: Some(State::new(FxHashSet::default())),
61        })
62    }
63
64    /// Serves all assets references by all root_assets.
65    #[turbo_tasks::function]
66    pub fn new_eager_multiple(
67        root_path: ResolvedVc<FileSystemPath>,
68        root_assets: ResolvedVc<OutputAssetsSet>,
69    ) -> Vc<Self> {
70        Self::cell(AssetGraphContentSource {
71            root_path,
72            root_assets,
73            expanded: None,
74        })
75    }
76
77    /// Serves all assets references by all root_assets. Only serve references
78    /// of an asset when it has served its content before.
79    #[turbo_tasks::function]
80    pub fn new_lazy_multiple(
81        root_path: ResolvedVc<FileSystemPath>,
82        root_assets: ResolvedVc<OutputAssetsSet>,
83    ) -> Vc<Self> {
84        Self::cell(AssetGraphContentSource {
85            root_path,
86            root_assets,
87            expanded: Some(State::new(FxHashSet::default())),
88        })
89    }
90
91    #[turbo_tasks::function]
92    async fn all_assets_map(&self) -> Result<Vc<OutputAssetsMap>> {
93        Ok(Vc::cell(
94            expand(
95                &*self.root_assets.await?,
96                &*self.root_path.await?,
97                self.expanded.as_ref(),
98            )
99            .await?,
100        ))
101    }
102}
103
104async fn expand(
105    root_assets: &FxIndexSet<ResolvedVc<Box<dyn OutputAsset>>>,
106    root_path: &FileSystemPath,
107    expanded: Option<&ExpandedState>,
108) -> Result<FxIndexMap<RcStr, ResolvedVc<Box<dyn OutputAsset>>>> {
109    let mut map = FxIndexMap::default();
110    let mut assets = Vec::new();
111    let mut queue = VecDeque::with_capacity(32);
112    let mut assets_set = FxHashSet::default();
113    let root_assets_with_path = root_assets
114        .iter()
115        .map(|&asset| async move {
116            let path = asset.path().await?;
117            Ok((path, asset))
118        })
119        .try_join()
120        .await?;
121
122    if let Some(expanded) = &expanded {
123        let expanded = expanded.get();
124        for (path, root_asset) in root_assets_with_path.into_iter() {
125            if let Some(sub_path) = root_path.get_path_to(&path) {
126                let (sub_paths_buffer, sub_paths) = get_sub_paths(sub_path);
127                let expanded = sub_paths_buffer
128                    .iter()
129                    .take(sub_paths)
130                    .any(|sub_path| expanded.contains(sub_path));
131                for sub_path in sub_paths_buffer.into_iter().take(sub_paths) {
132                    assets.push((sub_path, root_asset));
133                }
134                assets_set.insert(root_asset);
135                if expanded {
136                    queue.push_back(root_asset.references());
137                }
138            }
139        }
140    } else {
141        for (path, root_asset) in root_assets_with_path.into_iter() {
142            if let Some(sub_path) = root_path.get_path_to(&path) {
143                let (sub_paths_buffer, sub_paths) = get_sub_paths(sub_path);
144                for sub_path in sub_paths_buffer.into_iter().take(sub_paths) {
145                    assets.push((sub_path, root_asset));
146                }
147                queue.push_back(root_asset.references());
148                assets_set.insert(root_asset);
149            }
150        }
151    }
152
153    while let Some(references) = queue.pop_front() {
154        for asset in references.await?.iter() {
155            if assets_set.insert(*asset) {
156                let path = asset.path().await?;
157                if let Some(sub_path) = root_path.get_path_to(&path) {
158                    let (sub_paths_buffer, sub_paths) = get_sub_paths(sub_path);
159                    let expanded = if let Some(expanded) = &expanded {
160                        let expanded = expanded.get();
161                        sub_paths_buffer
162                            .iter()
163                            .take(sub_paths)
164                            .any(|sub_path| expanded.contains(sub_path))
165                    } else {
166                        true
167                    };
168                    if expanded {
169                        queue.push_back(asset.references());
170                    }
171                    for sub_path in sub_paths_buffer.into_iter().take(sub_paths) {
172                        assets.push((sub_path, *asset));
173                    }
174                }
175            }
176        }
177    }
178    for (sub_path, asset) in assets {
179        if &*sub_path == "index.html" {
180            map.insert("".into(), asset);
181        } else if let Some(p) = sub_path.strip_suffix("/index.html") {
182            map.insert(p.into(), asset);
183            map.insert(format!("{p}/").into(), asset);
184        } else if let Some(p) = sub_path.strip_suffix(".html") {
185            map.insert(p.into(), asset);
186        }
187        map.insert(sub_path, asset);
188    }
189    Ok(map)
190}
191
192fn get_sub_paths(sub_path: &str) -> ([RcStr; 3], usize) {
193    let sub_paths_buffer: [RcStr; 3];
194    let n = if sub_path == "index.html" {
195        sub_paths_buffer = ["".into(), sub_path.into(), Default::default()];
196        2
197    } else if let Some(p) = sub_path.strip_suffix("/index.html") {
198        sub_paths_buffer = [p.into(), format!("{p}/").into(), sub_path.into()];
199        3
200    } else if let Some(p) = sub_path.strip_suffix(".html") {
201        sub_paths_buffer = [p.into(), sub_path.into(), Default::default()];
202        2
203    } else {
204        sub_paths_buffer = [sub_path.into(), Default::default(), Default::default()];
205        1
206    };
207    (sub_paths_buffer, n)
208}
209
210#[turbo_tasks::function(operation)]
211fn all_assets_map_operation(source: ResolvedVc<AssetGraphContentSource>) -> Vc<OutputAssetsMap> {
212    source.all_assets_map()
213}
214
215#[turbo_tasks::value_impl]
216impl ContentSource for AssetGraphContentSource {
217    #[turbo_tasks::function]
218    async fn get_routes(self: ResolvedVc<Self>) -> Result<Vc<RouteTree>> {
219        let assets = all_assets_map_operation(self)
220            .read_strongly_consistent()
221            .await?;
222        let mut paths = Vec::new();
223        let routes = assets
224            .iter()
225            .map(|(path, asset)| {
226                paths.push(path.as_str());
227                RouteTree::new_route(
228                    BaseSegment::from_static_pathname(path).collect(),
229                    RouteType::Exact,
230                    Vc::upcast(AssetGraphGetContentSourceContent::new(
231                        *self,
232                        path.clone(),
233                        **asset,
234                    )),
235                )
236            })
237            .map(|v| async move { v.to_resolved().await })
238            .try_join()
239            .await?;
240        Ok(Vc::<RouteTrees>::cell(routes).merge())
241    }
242}
243
244#[turbo_tasks::value]
245struct AssetGraphGetContentSourceContent {
246    source: ResolvedVc<AssetGraphContentSource>,
247    path: RcStr,
248    asset: ResolvedVc<Box<dyn OutputAsset>>,
249}
250
251#[turbo_tasks::value_impl]
252impl AssetGraphGetContentSourceContent {
253    #[turbo_tasks::function]
254    pub fn new(
255        source: ResolvedVc<AssetGraphContentSource>,
256        path: RcStr,
257        asset: ResolvedVc<Box<dyn OutputAsset>>,
258    ) -> Vc<Self> {
259        Self::cell(AssetGraphGetContentSourceContent {
260            source,
261            path,
262            asset,
263        })
264    }
265}
266
267#[turbo_tasks::value_impl]
268impl GetContentSourceContent for AssetGraphGetContentSourceContent {
269    #[turbo_tasks::function]
270    async fn get(
271        self: ResolvedVc<Self>,
272        _path: RcStr,
273        _data: Value<ContentSourceData>,
274    ) -> Result<Vc<ContentSourceContent>> {
275        let this = self.await?;
276        turbo_tasks::emit(ResolvedVc::upcast::<Box<dyn ContentSourceSideEffect>>(self));
277        Ok(ContentSourceContent::static_content(
278            this.asset.versioned_content(),
279        ))
280    }
281}
282
283#[turbo_tasks::value_impl]
284impl ContentSourceSideEffect for AssetGraphGetContentSourceContent {
285    #[turbo_tasks::function]
286    async fn apply(&self) -> Result<Vc<Completion>> {
287        let source = self.source.await?;
288
289        if let Some(expanded) = &source.expanded {
290            expanded.update_conditionally(|expanded| expanded.insert(self.path.clone()));
291        }
292        Ok(Completion::new())
293    }
294}
295
296#[turbo_tasks::function]
297fn introspectable_type() -> Vc<RcStr> {
298    Vc::cell("asset graph content source".into())
299}
300
301#[turbo_tasks::value_impl]
302impl Introspectable for AssetGraphContentSource {
303    #[turbo_tasks::function]
304    fn ty(&self) -> Vc<RcStr> {
305        introspectable_type()
306    }
307
308    #[turbo_tasks::function]
309    fn title(&self) -> Vc<RcStr> {
310        self.root_path.to_string()
311    }
312
313    #[turbo_tasks::function]
314    fn details(&self) -> Vc<RcStr> {
315        Vc::cell(if let Some(expanded) = &self.expanded {
316            format!("{} assets expanded", expanded.get().len()).into()
317        } else {
318            "eager".into()
319        })
320    }
321
322    #[turbo_tasks::function]
323    async fn children(self: Vc<Self>) -> Result<Vc<IntrospectableChildren>> {
324        let this = self.await?;
325        let key = ResolvedVc::cell("root".into());
326        let inner_key = ResolvedVc::cell("inner".into());
327        let expanded_key = ResolvedVc::cell("expanded".into());
328
329        let root_assets = this.root_assets.await?;
330        let root_asset_children = root_assets
331            .iter()
332            .map(|&asset| async move {
333                Ok((
334                    key,
335                    IntrospectableOutputAsset::new(*ResolvedVc::upcast(asset))
336                        .to_resolved()
337                        .await?,
338                ))
339            })
340            .try_join()
341            .await?;
342
343        let expanded_assets = self.all_assets_map().await?;
344        let expanded_asset_children = expanded_assets
345            .values()
346            .filter(|&a| !root_assets.contains(a))
347            .map(|&asset| async move {
348                Ok((
349                    inner_key,
350                    IntrospectableOutputAsset::new(*ResolvedVc::upcast(asset))
351                        .to_resolved()
352                        .await?,
353                ))
354            })
355            .try_join()
356            .await?;
357
358        Ok(Vc::cell(
359            root_asset_children
360                .into_iter()
361                .chain(expanded_asset_children)
362                .chain(once((
363                    expanded_key,
364                    ResolvedVc::upcast(FullyExpanded(self.to_resolved().await?).resolved_cell()),
365                )))
366                .collect(),
367        ))
368    }
369}
370
371#[turbo_tasks::function]
372fn fully_expanded_introspectable_type() -> Vc<RcStr> {
373    Vc::cell("fully expanded asset graph content source".into())
374}
375
376#[turbo_tasks::value]
377struct FullyExpanded(ResolvedVc<AssetGraphContentSource>);
378
379#[turbo_tasks::value_impl]
380impl Introspectable for FullyExpanded {
381    #[turbo_tasks::function]
382    fn ty(&self) -> Vc<RcStr> {
383        fully_expanded_introspectable_type()
384    }
385
386    #[turbo_tasks::function]
387    async fn title(&self) -> Result<Vc<RcStr>> {
388        Ok(self.0.await?.root_path.to_string())
389    }
390
391    #[turbo_tasks::function]
392    async fn children(&self) -> Result<Vc<IntrospectableChildren>> {
393        let source = self.0.await?;
394        let key = ResolvedVc::cell("asset".into());
395
396        let expanded_assets =
397            expand(&*source.root_assets.await?, &*source.root_path.await?, None).await?;
398        let children = expanded_assets
399            .iter()
400            .map(|(_k, &v)| async move {
401                Ok((key, IntrospectableOutputAsset::new(*v).to_resolved().await?))
402            })
403            .try_join()
404            .await?
405            .into_iter()
406            .collect();
407
408        Ok(Vc::cell(children))
409    }
410}