use std::fs;
use std::path::{Path, PathBuf};
use serde_json::Value;
use crate::core::detection::package_json::PackageJson;
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub enum WorkspaceManager {
Npm,
Pnpm,
Yarn,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct WorkspacePackage {
pub name: String,
pub path: PathBuf,
}
#[derive(Debug, Clone, Default, serde::Serialize)]
pub struct WorkspaceSummary {
pub managers: Vec<WorkspaceManager>,
pub packages: Vec<WorkspacePackage>,
}
impl WorkspaceSummary {
pub fn is_workspace(&self) -> bool {
!self.managers.is_empty() || !self.packages.is_empty()
}
pub fn find_package(&self, name: &str) -> Option<&WorkspacePackage> {
self.packages.iter().find(|package| package.name == name)
}
}
pub fn detect_workspaces(root: &Path) -> WorkspaceSummary {
let root_package = PackageJson::load(&root.join("package.json"));
let mut managers = Vec::new();
let mut patterns = Vec::new();
if let Some(package) = &root_package
&& let Some(workspaces) = &package.workspaces
{
managers.push(if root.join("yarn.lock").exists() {
WorkspaceManager::Yarn
} else {
WorkspaceManager::Npm
});
patterns.extend(workspace_patterns_from_package_json(workspaces));
}
if root.join("pnpm-workspace.yaml").exists() {
managers.push(WorkspaceManager::Pnpm);
patterns.extend(workspace_patterns_from_pnpm(&root.join("pnpm-workspace.yaml")));
}
managers.dedup();
let packages = find_packages(root, &patterns);
WorkspaceSummary { managers, packages }
}
fn workspace_patterns_from_package_json(workspaces: &Value) -> Vec<String> {
match workspaces {
Value::Array(values) => values
.iter()
.filter_map(|value| value.as_str().map(ToOwned::to_owned))
.collect(),
Value::Object(object) => object
.get("packages")
.and_then(|packages| packages.as_array())
.map(|packages| {
packages
.iter()
.filter_map(|value| value.as_str().map(ToOwned::to_owned))
.collect()
})
.unwrap_or_default(),
_ => Vec::new(),
}
}
fn workspace_patterns_from_pnpm(path: &Path) -> Vec<String> {
let Ok(content) = fs::read_to_string(path) else {
return Vec::new();
};
let mut patterns = Vec::new();
let mut in_packages = false;
for line in content.lines() {
let trimmed = line.trim();
if trimmed == "packages:" {
in_packages = true;
continue;
}
if in_packages {
if let Some(pattern) = trimmed.strip_prefix('-') {
patterns.push(pattern.trim().trim_matches(['"', '\'']).to_string());
} else if !trimmed.is_empty() && !line.starts_with(' ') {
break;
}
}
}
patterns
}
fn find_packages(root: &Path, patterns: &[String]) -> Vec<WorkspacePackage> {
let mut packages = Vec::new();
for pattern in patterns {
if pattern.starts_with('!') || pattern.contains("node_modules") {
continue;
}
let base = pattern.trim_end_matches("/*").trim_end_matches("/**");
let base_path = root.join(base);
if !base_path.exists() {
continue;
}
if pattern.ends_with("/*") || pattern.ends_with("/**") {
if let Ok(entries) = fs::read_dir(&base_path) {
for entry in entries.filter_map(Result::ok) {
add_package_if_present(&mut packages, &entry.path());
}
}
} else {
add_package_if_present(&mut packages, &base_path);
}
}
packages.sort_by(|left, right| left.name.cmp(&right.name));
packages.dedup_by(|left, right| left.name == right.name || left.path == right.path);
packages
}
fn add_package_if_present(packages: &mut Vec<WorkspacePackage>, path: &Path) {
let package_path = path.join("package.json");
let Some(package) = PackageJson::load(&package_path) else {
return;
};
if package.name.is_empty() {
return;
}
packages.push(WorkspacePackage {
name: package.name,
path: path.to_path_buf(),
});
}