// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use std::collections::HashMap; use std::collections::HashSet; use std::collections::VecDeque; use deno_ast::ModuleSpecifier; use deno_config::WorkspaceConfig; use deno_core::anyhow::bail; use deno_core::anyhow::Context; use deno_core::error::AnyError; use crate::graph_util::ModuleGraphBuilder; pub struct PublishOrderGraph { packages: HashMap>, in_degree: HashMap, reverse_map: HashMap>, } impl PublishOrderGraph { pub fn new_single(package_name: String) -> Self { Self { packages: HashMap::from([(package_name.clone(), HashSet::new())]), in_degree: HashMap::from([(package_name.clone(), 0)]), reverse_map: HashMap::from([(package_name, Vec::new())]), } } pub fn next(&mut self) -> Vec { let mut package_names_with_depth = self .in_degree .iter() .filter_map(|(name, °ree)| if degree == 0 { Some(name) } else { None }) .map(|item| (item.clone(), self.compute_depth(item, HashSet::new()))) .collect::>(); // sort by depth to in order to prioritize those packages package_names_with_depth.sort_by(|a, b| match b.1.cmp(&a.1) { std::cmp::Ordering::Equal => a.0.cmp(&b.0), other => other, }); let sorted_package_names = package_names_with_depth .into_iter() .map(|(name, _)| name) .collect::>(); for name in &sorted_package_names { self.in_degree.remove(name); } sorted_package_names } pub fn finish_package(&mut self, name: &str) { if let Some(package_names) = self.reverse_map.remove(name) { for name in package_names { *self.in_degree.get_mut(&name).unwrap() -= 1; } } } /// There could be pending packages if there's a circular dependency. pub fn ensure_no_pending(&self) -> Result<(), AnyError> { // this is inefficient, but that's ok because it's simple and will // only ever happen when there's an error fn identify_cycle<'a>( current_name: &'a String, mut visited: HashSet<&'a String>, packages: &HashMap>, ) -> Option> { if visited.insert(current_name) { let deps = packages.get(current_name).unwrap(); for dep in deps { if let Some(mut cycle) = identify_cycle(dep, visited.clone(), packages) { cycle.push(current_name.to_string()); return Some(cycle); } } None } else { Some(vec![current_name.to_string()]) } } if self.in_degree.is_empty() { Ok(()) } else { let mut pkg_names = self.in_degree.keys().collect::>(); pkg_names.sort(); // determinism let mut cycle = identify_cycle(pkg_names[0], HashSet::new(), &self.packages).unwrap(); cycle.reverse(); bail!( "Circular package dependency detected: {}", cycle.join(" -> ") ); } } fn compute_depth( &self, package_name: &String, mut visited: HashSet, ) -> usize { if visited.contains(package_name) { return 0; // cycle } visited.insert(package_name.clone()); let Some(parents) = self.reverse_map.get(package_name) else { return 0; }; let max_depth = parents .iter() .map(|child| self.compute_depth(child, visited.clone())) .max() .unwrap_or(0); 1 + max_depth } } pub async fn build_publish_graph( workspace_config: &WorkspaceConfig, module_graph_builder: &ModuleGraphBuilder, ) -> Result { let roots = get_workspace_roots(workspace_config)?; let graph = module_graph_builder .create_graph( deno_graph::GraphKind::All, roots.iter().flat_map(|r| r.exports.clone()).collect(), ) .await?; graph.valid()?; let packages = build_pkg_deps(graph, roots); Ok(build_graph(packages)) } #[derive(Debug)] struct MemberRoots { name: String, dir_url: ModuleSpecifier, exports: Vec, } fn get_workspace_roots( config: &WorkspaceConfig, ) -> Result, AnyError> { let mut members = Vec::with_capacity(config.members.len()); let mut seen_names = HashSet::with_capacity(config.members.len()); for member in &config.members { let exports_config = member .config_file .to_exports_config() .with_context(|| { format!( "Failed to parse exports at {}", member.config_file.specifier ) })? .into_map(); if !seen_names.insert(&member.package_name) { bail!( "Cannot have two workspace packages with the same name ('{}' at {})", member.package_name, member.path.display(), ); } let mut member_root = MemberRoots { name: member.package_name.clone(), dir_url: member.config_file.specifier.join("./").unwrap().clone(), exports: Vec::with_capacity(exports_config.len()), }; for (_, value) in exports_config { let entry_point = member.config_file.specifier.join(&value).with_context(|| { format!( "Failed to join {} with {}", member.config_file.specifier, value ) })?; member_root.exports.push(entry_point); } members.push(member_root); } Ok(members) } fn build_pkg_deps( graph: deno_graph::ModuleGraph, roots: Vec, ) -> HashMap> { let mut members = HashMap::with_capacity(roots.len()); let mut seen_modules = HashSet::with_capacity(graph.modules().count()); for root in &roots { let mut deps = HashSet::new(); let mut pending = VecDeque::new(); pending.extend(root.exports.clone()); while let Some(specifier) = pending.pop_front() { let Some(module) = graph.get(&specifier).and_then(|m| m.esm()) else { continue; }; let mut dep_specifiers = Vec::with_capacity(module.dependencies.len() + 1); if let Some(types_dep) = &module.maybe_types_dependency { if let Some(specifier) = types_dep.dependency.maybe_specifier() { dep_specifiers.push(specifier); } } for (_, dep) in &module.dependencies { if let Some(specifier) = dep.maybe_code.maybe_specifier() { dep_specifiers.push(specifier); } if let Some(specifier) = dep.maybe_type.maybe_specifier() { dep_specifiers.push(specifier); } } for specifier in dep_specifiers { let specifier = graph.resolve(specifier); if specifier.scheme() != "file" { continue; } if specifier.as_str().starts_with(root.dir_url.as_str()) { if seen_modules.insert(specifier.clone()) { pending.push_back(specifier.clone()); } } else { let found_root = roots .iter() .find(|root| specifier.as_str().starts_with(root.dir_url.as_str())); if let Some(root) = found_root { deps.insert(root.name.clone()); } } } } members.insert(root.name.clone(), deps); } members } fn build_graph( packages: HashMap>, ) -> PublishOrderGraph { let mut in_degree = HashMap::new(); let mut reverse_map: HashMap> = HashMap::new(); // build the graph, in-degree map, and set of all nodes for (pkg_name, deps) in &packages { in_degree.insert(pkg_name.clone(), deps.len()); for dep in deps { reverse_map .entry(dep.clone()) .or_default() .push(pkg_name.clone()); } } PublishOrderGraph { packages: packages.clone(), in_degree, reverse_map, } } #[cfg(test)] mod test { use super::*; #[test] fn test_graph_no_deps() { let mut graph = build_graph(HashMap::from([ ("a".to_string(), HashSet::new()), ("b".to_string(), HashSet::new()), ("c".to_string(), HashSet::new()), ])); assert_eq!( graph.next(), vec!["a".to_string(), "b".to_string(), "c".to_string()], ); graph.finish_package("a"); assert!(graph.next().is_empty()); graph.finish_package("b"); assert!(graph.next().is_empty()); graph.finish_package("c"); assert!(graph.next().is_empty()); graph.ensure_no_pending().unwrap(); } #[test] fn test_graph_single_dep() { let mut graph = build_graph(HashMap::from([ ("a".to_string(), HashSet::from(["b".to_string()])), ("b".to_string(), HashSet::from(["c".to_string()])), ("c".to_string(), HashSet::new()), ])); assert_eq!(graph.next(), vec!["c".to_string()]); graph.finish_package("c"); assert_eq!(graph.next(), vec!["b".to_string()]); graph.finish_package("b"); assert_eq!(graph.next(), vec!["a".to_string()]); graph.finish_package("a"); assert!(graph.next().is_empty()); graph.ensure_no_pending().unwrap(); } #[test] fn test_graph_multiple_dep() { let mut graph = build_graph(HashMap::from([ ( "a".to_string(), HashSet::from(["b".to_string(), "c".to_string()]), ), ("b".to_string(), HashSet::from(["c".to_string()])), ("c".to_string(), HashSet::new()), ("d".to_string(), HashSet::new()), ("e".to_string(), HashSet::from(["f".to_string()])), ("f".to_string(), HashSet::new()), ])); assert_eq!( graph.next(), vec!["c".to_string(), "f".to_string(), "d".to_string()] ); graph.finish_package("f"); assert_eq!(graph.next(), vec!["e".to_string()]); graph.finish_package("e"); assert!(graph.next().is_empty()); graph.finish_package("d"); assert!(graph.next().is_empty()); graph.finish_package("c"); assert_eq!(graph.next(), vec!["b".to_string()]); graph.finish_package("b"); assert_eq!(graph.next(), vec!["a".to_string()]); graph.finish_package("a"); assert!(graph.next().is_empty()); graph.ensure_no_pending().unwrap(); } #[test] fn test_graph_circular_dep() { let mut graph = build_graph(HashMap::from([ ("a".to_string(), HashSet::from(["b".to_string()])), ("b".to_string(), HashSet::from(["c".to_string()])), ("c".to_string(), HashSet::from(["a".to_string()])), ])); assert!(graph.next().is_empty()); assert_eq!( graph.ensure_no_pending().unwrap_err().to_string(), "Circular package dependency detected: a -> b -> c -> a" ); } }