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