webpack_q/
operations.rs

1/*
2 * Copyright [2022] [Kevin Velasco]
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17use 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    // Included in current chunk
60    if chunks.is_empty() || chunks.contains(&origin_chunk_id) {
61        return Some(origin_chunk_id);
62    }
63
64    // only has one option really
65    if chunks.len() == 1 {
66        return chunks.iter().next().cloned();
67    }
68
69    let origin_chunk = origin_chunk?;
70
71    // Origin chunk's children
72    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            // find all the outgoing edges in the chunk graph (subtraversal)
170            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    // traverse every chunk entrypoint until we hit  the target chunk. Store the paths.
296    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        // each module
303        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                // check if we've arrived at the target chunk
332                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    // merge into a single traversal
352}
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}