turbopack_dev_server/source/
asset_graph.rs1use 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 #[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 #[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 #[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 #[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}