use std::path::{Path, PathBuf};
use rustc_hash::{FxHashMap, FxHashSet};
use fallow_config::PackageJson;
use fallow_types::trace::PeerClosureSeeds;
use crate::graph::ModuleGraph;
use super::{
WorkspaceOwnershipIndex, closure_listed_names, collect_workspace_used_packages,
credited_plugin_tooling, is_package_json_ignored_under,
};
pub(super) struct PeerDependencyResolver {
cache: FxHashMap<(PathBuf, String), Vec<InstalledPeer>>,
}
#[derive(Clone)]
struct InstalledPeer {
name: String,
optional: bool,
}
impl PeerDependencyResolver {
pub(super) fn new() -> Self {
Self {
cache: FxHashMap::default(),
}
}
pub(super) fn peer_dependency_closure<'b>(
&mut self,
package_root: &Path,
seeds: impl IntoIterator<Item = &'b str>,
listed: &FxHashSet<String>,
) -> FxHashMap<String, Vec<String>> {
let mut hosts_by_peer: FxHashMap<String, Vec<String>> = FxHashMap::default();
let mut expanded = FxHashSet::default();
let mut queue: Vec<String> = seeds.into_iter().map(str::to_string).collect();
while let Some(package_name) = queue.pop() {
if !expanded.insert(package_name.clone()) {
continue;
}
for peer in self.peer_dependencies_for(package_root, &package_name) {
if !peer.optional || listed.contains(peer.name.as_str()) {
queue.push(peer.name.clone());
}
hosts_by_peer
.entry(peer.name)
.or_default()
.push(package_name.clone());
}
}
hosts_by_peer
}
fn peer_dependencies_for(
&mut self,
package_root: &Path,
package_name: &str,
) -> Vec<InstalledPeer> {
let key = (package_root.to_path_buf(), package_name.to_string());
if let Some(cached) = self.cache.get(&key) {
return cached.clone();
}
let peer_dependencies: Vec<InstalledPeer> =
find_installed_package_json(package_root, package_name)
.and_then(|path| PackageJson::load(&path).ok())
.map(|pkg| {
pkg.peer_dependency_names()
.into_iter()
.map(|name| InstalledPeer {
optional: pkg.peer_dependency_is_optional(&name),
name,
})
.collect()
})
.unwrap_or_default();
self.cache.insert(key, peer_dependencies.clone());
peer_dependencies
}
}
pub fn peer_dependency_hosts(
graph: &ModuleGraph,
project_root: &Path,
workspace_roots: &[&Path],
ignore_patterns: &fallow_config::IgnorePatternSet,
package_name: &str,
extra_seeds: &PeerClosureSeeds,
) -> Vec<String> {
let root_declared = PackageJson::load(&project_root.join("package.json"))
.map(|pkg| pkg.all_dependency_names())
.unwrap_or_default();
let workspace_manifests: Vec<(&Path, Vec<String>)> = workspace_roots
.iter()
.copied()
.filter_map(|ws_root| {
let pkg_path = ws_root.join("package.json");
if is_package_json_ignored_under(&pkg_path, project_root, ignore_patterns) {
return None;
}
let pkg = PackageJson::load(&pkg_path).ok()?;
Some((ws_root, pkg.all_dependency_names()))
})
.collect();
let listed = closure_listed_names(
&root_declared,
workspace_manifests
.iter()
.flat_map(|(_, declared)| declared.iter()),
);
let workspace_roots: Vec<&Path> = workspace_manifests
.iter()
.map(|(ws_root, _)| *ws_root)
.collect();
let mut resolver = PeerDependencyResolver::new();
let mut hosts = resolver
.peer_dependency_closure(
project_root,
graph
.package_usage
.keys()
.map(String::as_str)
.chain(extra_seeds.for_manifest(&project_root.join("package.json"))),
&listed,
)
.remove(package_name)
.unwrap_or_default();
let ownership = WorkspaceOwnershipIndex::new(graph, &workspace_roots);
let used_by_workspace = collect_workspace_used_packages(graph, &workspace_roots, &ownership);
for ws_root in &workspace_roots {
if let Some(ws_hosts) = resolver
.peer_dependency_closure(
ws_root,
used_by_workspace
.get(ws_root)
.into_iter()
.flatten()
.copied()
.chain(extra_seeds.for_manifest(&ws_root.join("package.json"))),
&listed,
)
.remove(package_name)
{
hosts.extend(ws_hosts);
}
}
hosts.sort_unstable();
hosts.dedup();
hosts
}
fn find_installed_package_json(package_root: &Path, package_name: &str) -> Option<PathBuf> {
for base in package_root.ancestors() {
let candidate = node_modules_package_json(base, package_name);
if candidate.is_file() {
return Some(candidate);
}
}
None
}
fn node_modules_package_json(base: &Path, package_name: &str) -> PathBuf {
let mut path = base.join("node_modules");
for segment in package_name.split('/') {
path.push(segment);
}
path.join("package.json")
}
pub(super) fn peer_closure_shared_seeds(
plugin_result: &crate::plugins::AggregatedPluginResult,
) -> FxHashSet<&str> {
plugin_result
.script_used_packages
.iter()
.chain(&plugin_result.referenced_dependencies)
.map(String::as_str)
.chain(credited_plugin_tooling(plugin_result))
.collect()
}
pub fn peer_closure_seeds(
plugin_result: &crate::plugins::AggregatedPluginResult,
) -> PeerClosureSeeds {
let mut seeds = PeerClosureSeeds::from_shared(
peer_closure_shared_seeds(plugin_result)
.into_iter()
.map(str::to_string),
);
for (manifest, name) in &plugin_result.package_referenced_dependencies {
seeds.push_manifest_seed(manifest.clone(), name.clone());
}
seeds
}
pub(super) struct PeerSeedInput<'s, 'a> {
pub(super) shared: &'s FxHashSet<&'a str>,
pub(super) manifest: Option<&'s FxHashSet<&'a str>>,
}
impl<'a> PeerSeedInput<'_, 'a> {
pub(super) fn iter(&self) -> impl Iterator<Item = &'a str> + '_ {
self.shared
.iter()
.chain(self.manifest.into_iter().flatten())
.copied()
}
}