Skip to main content

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