1use crate::graphs::{ChunkGraph, ChunkImportPathGraph, ChunkLoadGraph, ModuleParentGraph};
18use meshed::graph::traversal::{
19 traverse_graph, GraphTraversal, Instruction, Mode, Pathing, TraversalLog,
20};
21use meshed::prelude::*;
22
23use std::collections::{HashMap, HashSet};
24use std::fmt::{write, Display, Formatter};
25
26use meshed::graph::node::Node;
27use meshed::graph::traversal::Mode::Acyclic;
28use meshed::graph::traversal::Pathing::DFS;
29use meshed::graph::{Graph, GraphDefinition, Inverted};
30use thiserror::Error;
31use webpack_stats::chunk::{Chunk, ChunkId, Chunks, Files};
32use webpack_stats::entry::Entrypoint;
33use webpack_stats::module::{Module, ModuleIdentifier, ModuleName, Modules};
34use webpack_stats::SizeBytes;
35
36#[derive(Debug, Error)]
37pub enum EntrypointTraversalError {
38 #[error("Module {id} does not exist")]
39 NoEntrypoint { id: String },
40 #[error("Entrypoint {id} contains invalid chunks: {chunks:?}. Expected chunk {expected}")]
41 InvalidEntrypointChunks {
42 chunks: HashSet<ChunkId>,
43 id: String,
44 expected: ChunkId,
45 },
46
47 #[error("An unexpected error occured when traversing the graph")]
48 GraphError,
49}
50
51pub fn find_possible_chunk_for(
52 node: &Node<ModuleParentGraph>,
53 origin_chunk_id: ChunkId,
54 origin_chunk: Option<Node<ChunkGraph>>,
55 import_paths: &Graph<ChunkImportPathGraph>,
56) -> Option<ChunkId> {
57 let chunks = node.node_data();
58
59 if chunks.is_empty() || chunks.contains(&origin_chunk_id) {
61 return Some(origin_chunk_id);
62 }
63
64 if chunks.len() == 1 {
66 return chunks.iter().next().cloned();
67 }
68
69 let origin_chunk = origin_chunk?;
70
71 if let Some(edge) = origin_chunk.find_edge(|edge| chunks.contains(&edge.target.get_id())) {
73 return Some(edge.target.get_id());
74 }
75
76 let origin_chunk_node = import_paths
77 .query(&origin_chunk.get_id())
78 .cloned()
79 .expect("Origin chunk did not exist in inverted chunk graph");
80
81 let traversal = traverse_graph(origin_chunk_node)
82 .set_pathing(Pathing::DFS)
83 .set_mode(Mode::Acyclic)
84 .into_iterator(|_, edge| {
85 tracing::trace!(
86 "Check ancestor {} -> {}",
87 edge.origin.label(),
88 edge.target.label()
89 );
90 if chunks.contains(&edge.target.get_id()) {
91 Instruction::Halt(Some(edge.target.get_id()))
92 } else {
93 Instruction::Continue(None)
94 }
95 });
96
97 traversal.flatten().next()
98}
99
100fn annotate_with_chunk<T: GraphDefinition>(
101 node: &Node<T>,
102 chunk: Option<ChunkId>,
103 fallback: ChunkId,
104) {
105 #[derive(Clone)]
106 struct Defaulted;
107
108 if let Some(chunk) = chunk {
109 tracing::trace!("Annotate {}", &chunk);
110 if let Some(existing_annotation) = node.get_annotation::<ChunkId>() {
111 if existing_annotation != chunk && node.get_annotation::<Defaulted>().is_none() {
112 tracing::warn!("Subsequent traversal of {} resulted in inconsistent chunk assignment {} -> {}. Module belongs to multiple chunks in traversal", node.label(), &existing_annotation, &chunk);
113 }
114 }
115 node.annotate(chunk);
116 } else if node.get_annotation::<ChunkId>().is_some() {
117 } else {
118 node.annotate(Defaulted);
119 node.annotate(fallback);
120 }
121}
122
123pub fn traverse_entrypoint(
124 entrypoint_id: ModuleIdentifier,
125 initial_chunk_id: ChunkId,
126 module_graph: &Inverted<ModuleParentGraph>,
127 truncated_chunk_graph: &Graph<ChunkGraph>,
128 import_paths: &Graph<ChunkImportPathGraph>,
129) -> Result<GraphTraversal<ModuleParentGraph>, EntrypointTraversalError> {
130 let entrypoint = module_graph.inner().query(&entrypoint_id).ok_or(
131 EntrypointTraversalError::NoEntrypoint {
132 id: entrypoint_id.to_string(),
133 },
134 )?;
135
136 {
137 let module_chunks = entrypoint.node_data();
138 if module_chunks.is_empty() {
139 entrypoint.annotate(initial_chunk_id);
140 } else if module_chunks.len() == 1 {
141 entrypoint.annotate(module_chunks.iter().cloned().next().unwrap());
142 } else if !module_chunks.contains(&initial_chunk_id) {
143 return Err(EntrypointTraversalError::InvalidEntrypointChunks {
144 chunks: module_chunks.clone(),
145 id: entrypoint_id.to_string(),
146 expected: initial_chunk_id.clone(),
147 });
148 } else {
149 entrypoint.annotate(initial_chunk_id);
150 }
151 }
152
153 let traversal = traverse_graph(entrypoint.clone())
154 .set_pathing(Pathing::DFS)
155 .set_mode(Mode::Acyclic)
156 .execute(|_depth, edge| {
157 tracing::trace!(
158 "Evaluate {} -> {}",
159 edge.origin.label(),
160 edge.target.label()
161 );
162 let origin_chunk = edge
163 .origin
164 .get_annotation::<ChunkId>()
165 .expect("Traversal did not have a source chunk");
166
167 let target_node = &edge.target;
168
169 let origin_chunk_node = truncated_chunk_graph.query(&origin_chunk).cloned();
171
172 let chunk = find_possible_chunk_for(
173 target_node,
174 origin_chunk.clone(),
175 origin_chunk_node,
176 import_paths,
177 );
178 annotate_with_chunk(&edge.target, chunk, origin_chunk);
179 Instruction::Continue(())
180 });
181
182 Ok(traversal)
183}
184
185pub fn traverse_entry_chunk<M, C, Mv, Cv, E>(
186 modules: M,
187 chunks: C,
188 entrypoint: &E,
189) -> Result<Inverted<ModuleParentGraph>, EntrypointTraversalError>
190where
191 M: Modules<Mv>,
192 Mv: Module,
193 C: Chunks<Cv>,
194 Cv: Chunk,
195 E: Entrypoint,
196{
197 let chunk_ids = entrypoint.chunks();
198 let mut traversal: Option<GraphTraversal<ModuleParentGraph>> = None;
199 let chunk_graph = ChunkGraph::build_graph(&chunks);
200 let valid_import_graph = ChunkImportPathGraph::build_graph(&chunks);
201
202 let module_graph = ModuleParentGraph::build_graph(&modules).invert();
203
204 for entrypoint_id in chunk_ids.iter().cloned() {
205 let chunk = chunk_graph
206 .query(&entrypoint_id)
207 .cloned()
208 .ok_or(EntrypointTraversalError::GraphError)?;
209
210 let chunk_traversal = {
211 traverse_graph(chunk.clone())
212 .set_mode(Acyclic)
213 .execute(|_depth, _edge| Instruction::Continue(()))
214 };
215
216 let truncated_chunk_graph = chunk_traversal.project_into_graph(&chunk_graph);
217 let import_paths = chunk_traversal.prune_graph(&valid_import_graph);
218
219 let entrypoints = chunk.node_data();
220
221 for entrypoint in entrypoints {
222 let traversal_log = traverse_entrypoint(
223 entrypoint.clone(),
224 entrypoint_id,
225 &module_graph,
226 &truncated_chunk_graph,
227 &import_paths,
228 );
229
230 match traversal_log {
231 Ok(traversal_log) => {
232 if let Some(old_traversal) = traversal.take() {
233 let next = old_traversal.merge_with(traversal_log);
234 traversal = Some(next);
235 } else {
236 traversal = Some(traversal_log)
237 }
238 }
239 Err(err) => {
240 tracing::warn!("{}. Skipping", err);
241 }
242 }
243 }
244 }
245
246 let traversal = traversal.unwrap();
247 Ok(module_graph.map_project(traversal))
248}
249
250pub struct Entrypoints<'a> {
251 entries: HashMap<&'a str, &'a [ChunkId]>,
252}
253
254impl<'a> Display for Entrypoints<'a> {
255 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
256 for (name, chunks) in self.entries.iter() {
257 writeln!(f, "{}:", name)?;
258 writeln!(f, " Chunks:")?;
259 for chunk in chunks.iter() {
260 writeln!(f, " {}", chunk)?;
261 }
262 }
263 Ok(())
264 }
265}
266
267pub fn display_entrypoints<'a, E>(entrypoints: &'a [&'a E]) -> Entrypoints<'a>
268where
269 E: webpack_stats::entry::Entrypoint,
270{
271 let mut map = HashMap::new();
272 for entry in entrypoints {
273 map.insert(entry.name(), entry.chunks());
274 }
275
276 Entrypoints { entries: map }
277}
278
279pub fn paths_to_chunk<E, C, Cv, M, Mv>(
280 entrypoint: &E,
281 target_chunk: ChunkId,
282 chunks: &C,
283 modules: &M,
284) -> Inverted<ModuleParentGraph>
285where
286 M: Modules<Mv>,
287 Mv: Module,
288 C: Chunks<Cv>,
289 Cv: Chunk,
290 E: Entrypoint,
291{
292 let chunk_graph = ChunkGraph::build_graph(&chunks);
293 let import_chunk_graph = ChunkImportPathGraph::build_graph(&chunks);
294 let module_graph = ModuleParentGraph::build_graph(&modules).invert();
295 let mut paths = vec![] as Vec<Vec<(ModuleIdentifier, ModuleIdentifier)>>;
297 for root_chunk in entrypoint.chunks() {
298 if root_chunk == &target_chunk {
299 continue;
300 }
301 let chunk_node = chunk_graph.query(root_chunk).unwrap().clone();
302 for module in chunk_node.node_data() {
304 let module_node = module_graph.inner().query(module).unwrap().clone();
305 module_node.annotate(chunk_node.get_id());
306
307 let traversal = traverse_graph(module_node)
308 .set_mode(Acyclic)
309 .set_pathing(DFS);
310
311 traversal.execute(|meta, edge| {
312 let origin_chunk = edge
313 .origin
314 .get_annotation::<ChunkId>()
315 .expect("Did not have an origin chunk");
316 let origin_chunk_node = chunk_graph.query(&&origin_chunk).cloned();
317 let mut path = meta
318 .get_annotation::<Vec<(ModuleIdentifier, ModuleIdentifier)>>()
319 .unwrap_or_default();
320 path.push((edge.origin.get_id(), edge.target.get_id()));
321
322 let node_chunk = find_possible_chunk_for(
323 &edge.target,
324 origin_chunk.clone(),
325 origin_chunk_node,
326 &import_chunk_graph,
327 );
328
329 annotate_with_chunk(&edge.target, node_chunk.clone(), origin_chunk);
330
331 if let Some(chunk) = node_chunk {
333 if chunk == target_chunk {
334 paths.push(path);
335 return Instruction::Backtrack(());
336 }
337 }
338 edge.target.annotate(path);
339 Instruction::Continue(())
340 });
341 }
342 }
343
344 let mut log = TraversalLog::default();
345 for path in paths {
346 let next_log = TraversalLog::from(path);
347 log = log.merge_with(next_log);
348 }
349
350 module_graph.map_project(log)
351 }
353
354pub struct EntrypointDescription<'a> {
355 name: &'a str,
356 initial_load_size: SizeBytes,
357 roots: Vec<Node<ChunkLoadGraph>>,
358}
359
360impl<'a> Display for EntrypointDescription<'a> {
361 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
362 writeln!(f, "{}:", &self.name)?;
363 writeln!(
364 f,
365 "Initial size (uncompressed): {}",
366 &self.initial_load_size
367 )?;
368 writeln!(f, "Chunk Imports (* denotes asynchronous chunk):")?;
369 for root in self.roots.iter() {
370 let traversal = traverse_graph(root.clone())
371 .set_pathing(Pathing::DFS)
372 .set_mode(Mode::Acyclic)
373 .into_iterator(|meta, edge| Instruction::Continue((meta, edge)));
374
375 #[derive(Copy, Clone)]
376 struct Async;
377
378 write!(f, "├── {} ({}) [", root.get_id(), root.node_data().1)?;
379
380 for file in root.node_data().3 .0.iter() {
381 write!(f, "{} ", file)?;
382 }
383 writeln!(f, "]")?;
384
385 for (meta, edge) in traversal {
386 let depth = meta.depth();
387 let node = edge.target;
388
389 let indent = (depth * 4) + 4;
390
391 if !node.node_data().2 .0 {
392 meta.annotate(Async);
393 }
394
395 if meta.get_annotation::<Async>().is_some() {
396 write!(f, "{:>indent$}", "├*- ")?;
397 } else {
398 write!(f, "{:>indent$}", "├── ",)?;
399 }
400
401 write!(f, "{} ({}) [", node.label(), node.node_data().1)?;
402
403 for file in node.node_data().3 .0.iter() {
404 write!(f, "{},", file)?;
405 }
406 writeln!(f, "]")?;
407 }
408 }
409
410 Ok(())
411 }
412}
413
414#[derive(Debug, Error)]
415pub enum EntrypointDescriptionError {}
416pub fn describe_entrypoints<'a, C, Cv>(
417 chunks: C,
418 entrypoint_name: &'a str,
419 entrypoints: Entrypoints,
420) -> Result<EntrypointDescription<'a>, EntrypointDescriptionError>
421where
422 C: Chunks<Cv>,
423 Cv: Chunk,
424{
425 let graph = ChunkLoadGraph::build_graph(&chunks);
426
427 let entrypoint = *entrypoints.entries.get(entrypoint_name).unwrap();
428 let mut output_traversal = None as Option<GraphTraversal<ChunkLoadGraph>>;
429 let mut root_nodes = vec![];
430 for chunk in entrypoint.iter() {
431 let chunk_node = graph.query(chunk);
432 let chunk_node = match chunk_node {
433 None => {
434 continue;
435 }
436 Some(node) => node.clone(),
437 };
438
439 let truncated = traverse_graph(chunk_node.clone())
440 .set_pathing(Pathing::DFS)
441 .set_mode(Mode::Acyclic)
442 .execute(|_, edge| {
443 let initial = &edge.target.node_data().2;
444 if !initial.0 {
445 Instruction::Skip(())
446 } else {
447 Instruction::Continue(())
448 }
449 });
450
451 let unique_paths = traverse_graph(chunk_node.clone())
452 .set_pathing(DFS)
453 .set_mode(Mode::Acyclic)
454 .execute(|meta, edge| {
455 if let Some(mut seen_in_this_path) = meta.get_annotation::<HashSet<ChunkId>>() {
456 if seen_in_this_path.contains(&edge.target.get_id()) {
457 return Instruction::Skip(());
458 } else {
459 seen_in_this_path.insert(edge.target.get_id());
460 }
461 meta.annotate(seen_in_this_path);
462 } else {
463 meta.annotate::<HashSet<ChunkId>>(HashSet::from([edge.origin.get_id()]));
464 }
465 Instruction::Continue(())
466 });
467
468 let projection = unique_paths.project_into_graph(&graph);
469
470 let root_node = projection.query(&chunk_node.get_id()).expect("");
471 root_nodes.push(root_node.clone());
472 if let Some(tra) = output_traversal.take() {
473 output_traversal = Some(tra.merge_with(truncated))
474 } else {
475 output_traversal = Some(truncated)
476 }
477 }
478
479 let output_traversal = output_traversal.unwrap();
480 let output_graph = output_traversal.project_into_graph(&graph);
481
482 let initial_size = output_graph
483 .all_nodes()
484 .fold(SizeBytes::default(), |acc, n| acc + n.node_data().1);
485
486 Ok(EntrypointDescription {
487 name: entrypoint_name,
488 roots: root_nodes,
489 initial_load_size: initial_size,
490 })
491}
492
493#[derive(Debug)]
494pub struct ChunkDescription {
495 id: ChunkId,
496 size: SizeBytes,
497 files: Files,
498 modules: Vec<ModuleName>,
499}
500
501impl Display for ChunkDescription {
502 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
503 writeln!(f, "Chunk: {}", &self.id)?;
504 writeln!(f, "size: {}", &self.size)?;
505 writeln!(f, "Files:")?;
506 for file in self.files.0.iter() {
507 writeln!(f, " {}", &file)?;
508 }
509
510 writeln!(f, "Modules:")?;
511 for file in self.modules.iter() {
512 writeln!(f, " {}", &file)?;
513 }
514
515 Ok(())
516 }
517}
518
519pub fn describe_chunk<C: Chunks<Cv>, Cv: Chunk, M: Modules<Mv>, Mv: Module>(
520 chunk_id: ChunkId,
521 chunks: &C,
522 modules: &M,
523) -> Option<ChunkDescription> {
524 let node = chunks.query(&chunk_id)?;
525
526 let index = modules.create_index();
527 let modules: Vec<ModuleIdentifier> = node.extract_data();
528 let names = modules.iter().filter_map(|id| {
529 let module = index.query(id)?;
530 Some(module.label())
531 });
532 Some(ChunkDescription {
533 id: chunk_id,
534 size: node.extract_data(),
535 files: node.extract_data(),
536 modules: names.collect(),
537 })
538}