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