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