use crate::package::{DependencyType, Package};
use crate::package_graph_error::PackageGraphError;
use clean_path::Clean;
use nodejs_package_json::{
DependenciesMap, PackageJson, Version, VersionProtocol, WorkspaceProtocol, WorkspacesField,
};
use nodejs_package_managers::{PackageManager, pnpm::PnpmWorkspaceYaml};
use petgraph::Direction;
use petgraph::graph::DiGraph;
use petgraph::visit::EdgeRef;
use starbase_utils::{glob, json, yaml};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
pub type PackageGraphType = DiGraph<String, DependencyType>;
pub struct PackageGraph {
pub cwd: PathBuf,
pub manager: PackageManager,
pub packages: BTreeMap<String, Package>,
pub root: PathBuf,
pub root_package: Package,
graph: PackageGraphType,
package_globs: Vec<String>,
}
impl PackageGraph {
pub fn generate<T: AsRef<Path>>(working_dir: T) -> Result<PackageGraph, PackageGraphError> {
let mut graph = Self::load_from(working_dir)?;
graph.load_workspace_packages()?;
graph.generate_graph()?;
Ok(graph)
}
pub fn load_from<T: AsRef<Path>>(working_dir: T) -> Result<PackageGraph, PackageGraphError> {
let working_dir = working_dir.as_ref();
let (root, package_manager) = Self::find_package_root(working_dir)
.unwrap_or_else(|| (working_dir.to_owned(), PackageManager::Npm));
let root_manifest: PackageJson = json::read_file(root.join("package.json"))?;
let mut package_globs = vec![];
if package_manager == PackageManager::Pnpm {
let ws_file = root.join("pnpm-workspace.yaml");
if ws_file.exists() {
let ws: PnpmWorkspaceYaml = yaml::read_file(ws_file)?;
package_globs = ws.packages;
}
} else if let Some(workspaces) = &root_manifest.workspaces {
package_globs = match workspaces {
WorkspacesField::Globs(globs) => globs.to_owned(),
WorkspacesField::Config { packages, .. } => packages.to_owned(),
};
}
Ok(PackageGraph {
cwd: working_dir.to_owned(),
graph: DiGraph::new(),
manager: package_manager,
package_globs,
packages: BTreeMap::new(),
root_package: Package::new(root.clone(), root_manifest),
root,
})
}
pub fn find_package_root<T: AsRef<Path>>(starting_dir: T) -> Option<(PathBuf, PackageManager)> {
let starting_dir = starting_dir.as_ref();
let mut current_dir = Some(starting_dir);
while let Some(dir) = current_dir {
if dir.join("bun.lockb").exists() {
return Some((dir.to_owned(), PackageManager::Bun));
}
else if dir.join("pnpm-lock.yaml").exists()
|| dir.join("pnpm-workspace.yaml").exists()
{
return Some((dir.to_owned(), PackageManager::Pnpm));
}
else if dir.join("yarn.lock").exists() {
return Some((
dir.to_owned(),
if dir.join(".yarn").exists() || dir.join(".yarnrc.yml").exists() {
PackageManager::Yarn
} else {
PackageManager::YarnLegacy
},
));
}
else if dir.join("package-lock.json").exists()
|| dir.join("npm-shrinkwrap.json").exists()
{
return Some((dir.to_owned(), PackageManager::Npm));
}
current_dir = dir.parent();
}
None
}
pub fn is_workspaces_enabled(&self) -> bool {
!self.package_globs.is_empty() && !self.packages.is_empty()
}
pub fn load_workspace_packages(&mut self) -> Result<(), PackageGraphError> {
if self.package_globs.is_empty() {
return Ok(());
}
let mut packages = BTreeMap::new();
let mut index = 1;
let mut dirs = glob::walk(&self.root, &self.package_globs)?;
dirs.sort();
for dir in dirs {
if !dir.is_dir() {
continue;
}
let manifest_file = dir.join("package.json");
if manifest_file.exists() {
let manifest: PackageJson = json::read_file(manifest_file)?;
let mut package = Package::new(dir, manifest);
package.index = index;
packages.insert(package.get_name()?.to_owned(), package);
index += 1;
}
}
self.packages = packages;
Ok(())
}
pub fn generate_graph(&mut self) -> Result<(), PackageGraphError> {
let mut graph = DiGraph::new();
graph.add_node(if self.is_workspaces_enabled() {
self.root_package
.manifest
.name
.as_deref()
.unwrap_or("(root)")
.to_owned()
} else {
self.root_package.get_name()?.to_owned()
});
if self.package_globs.is_empty() {
self.graph = graph;
return Ok(());
}
{
for package in self.packages.values_mut() {
package.node_index = graph.add_node(package.get_name()?.to_owned());
}
}
let add_edge_via_path = |graph: &mut PackageGraphType,
package: &Package,
dep_package: &Package,
path: &Path,
dep_type: DependencyType| {
if path.is_absolute() && path == dep_package.root
|| path.is_relative() && package.root.join(path).clean() == dep_package.root
{
graph.add_edge(package.node_index, dep_package.node_index, dep_type);
}
};
let add_edge_via_version = |graph: &mut PackageGraphType,
package: &Package,
dep_package: &Package,
version: Option<&Version>,
dep_type: DependencyType| {
if version.is_none() || version == dep_package.manifest.version.as_ref() {
graph.add_edge(package.node_index, dep_package.node_index, dep_type);
}
};
let add_edges = |graph: &mut PackageGraphType,
package: &Package,
deps: &DependenciesMap<VersionProtocol>,
dep_type: DependencyType| {
for (name, version) in deps {
match version {
VersionProtocol::Requirement(req) => {
if let Some(dep_package) = self.packages.get(name) &&
(req.comparators.is_empty()
|| dep_package
.manifest
.version
.as_ref()
.is_some_and(|ver| req.matches(ver)))
{
graph.add_edge(package.node_index, dep_package.node_index, dep_type);
}
}
VersionProtocol::Version(ver) => {
if let Some(dep_package) = self.packages.get(name) {
add_edge_via_version(graph, package, dep_package, Some(ver), dep_type);
}
}
VersionProtocol::File(path)
| VersionProtocol::Link(path)
| VersionProtocol::Portal(path) => {
if let Some(dep_package) = self.packages.get(name) {
add_edge_via_path(graph, package, dep_package, path, dep_type);
}
}
VersionProtocol::Workspace(ws) => {
let (alias, version) = match ws {
WorkspaceProtocol::Any { alias } => (alias, &None),
WorkspaceProtocol::Tilde { alias, version } => (alias, version),
WorkspaceProtocol::Caret { alias, version } => (alias, version),
WorkspaceProtocol::File(path) => {
if let Some(dep_package) = self.packages.get(name) {
add_edge_via_path(graph, package, dep_package, path, dep_type);
}
continue;
}
WorkspaceProtocol::Version(ver) => {
if let Some(dep_package) = self.packages.get(name) {
add_edge_via_version(
graph,
package,
dep_package,
Some(ver),
dep_type,
);
}
continue;
}
};
if let Some(dep_package) =
self.packages.get(alias.as_deref().unwrap_or(name))
{
add_edge_via_version(
graph,
package,
dep_package,
version.as_ref(),
dep_type,
);
}
}
_ => {}
};
}
};
let mut packages = vec![&self.root_package];
packages.extend(self.packages.values());
for package in packages {
if let Some(deps) = &package.manifest.dependencies {
add_edges(&mut graph, package, deps, DependencyType::Production);
}
if let Some(deps) = &package.manifest.dev_dependencies {
add_edges(&mut graph, package, deps, DependencyType::Development);
}
if let Some(deps) = &package.manifest.peer_dependencies {
add_edges(&mut graph, package, deps, DependencyType::Peer);
}
if let Some(deps) = &package.manifest.optional_dependencies {
add_edges(&mut graph, package, deps, DependencyType::Optional);
}
}
self.graph = graph;
Ok(())
}
pub fn dependencies_of(
&self,
name: &str,
) -> Result<Vec<(String, DependencyType)>, PackageGraphError> {
let package = self
.packages
.get(name)
.ok_or_else(|| PackageGraphError::UnknownPackage(name.to_owned()))?;
let deps = self
.graph
.edges_directed(package.node_index, Direction::Outgoing)
.map(|edge| {
(
self.graph.node_weight(edge.target()).unwrap().to_owned(),
edge.weight().to_owned(),
)
})
.collect();
Ok(deps)
}
pub fn dependents_of(
&self,
name: &str,
) -> Result<Vec<(String, DependencyType)>, PackageGraphError> {
let package = self
.packages
.get(name)
.ok_or_else(|| PackageGraphError::UnknownPackage(name.to_owned()))?;
let deps = self
.graph
.edges_directed(package.node_index, Direction::Incoming)
.map(|edge| {
(
self.graph.node_weight(edge.target()).unwrap().to_owned(),
edge.weight().to_owned(),
)
})
.collect();
Ok(deps)
}
pub fn to_dot(&self) -> String {
format!("{:?}", petgraph::dot::Dot::new(&self.graph))
}
}