use std::fs;
use std::path::Path;
use crate::types::DiagTrace;
pub(crate) fn is_cargo_workspace_only(root: &Path) -> bool {
let Ok(raw) = fs::read_to_string(root.join("Cargo.toml")) else {
return false;
};
let Ok(v) = toml::from_str::<toml::Value>(&raw) else {
return false;
};
v.get("workspace").is_some() && v.get("package").is_none()
}
pub(crate) fn is_npm_workspace_only(root: &Path) -> bool {
let Some(raw) = fs::read_to_string(root.join("package.json")).ok() else {
return false;
};
let Ok(v) = serde_json::from_str::<serde_json::Value>(&raw) else {
return false;
};
v.get("workspaces").is_some() && v.get("bin").is_none()
}
pub(crate) fn pyproject_has_tool(root: &Path, name: &str) -> bool {
let Some(raw) = fs::read_to_string(root.join("pyproject.toml")).ok() else {
return false;
};
let Ok(v) = toml::from_str::<toml::Value>(&raw) else {
return false;
};
v.get("tool").and_then(|t| t.get(name)).is_some()
}
pub(crate) fn expand_workspace_members(root: &Path, raw_members: &[String]) -> Vec<String> {
let mut out = Vec::new();
for m in raw_members {
if let Some(prefix) = m.strip_suffix("/*").or_else(|| m.strip_suffix("/**")) {
let parent = root.join(prefix);
if parent.is_dir() {
if let Ok(entries) = fs::read_dir(&parent) {
let mut subdirs: Vec<_> =
entries.flatten().filter(|e| e.path().is_dir()).collect();
subdirs.sort_by_key(|e| e.file_name());
for sub in subdirs {
if let Some(sub_name) = sub.file_name().to_str() {
out.push(format!("{prefix}/{sub_name}"));
}
}
}
}
} else {
out.push(m.clone());
}
}
out
}
pub(crate) fn first_cargo_member_name(root: &Path, diag: &mut DiagTrace) -> Option<String> {
let raw = fs::read_to_string(root.join("Cargo.toml")).ok()?;
let v = toml::from_str::<toml::Value>(&raw).ok()?;
let members = v.get("workspace")?.get("members")?.as_array()?;
let raw_list: Vec<String> = members
.iter()
.filter_map(|m| m.as_str().map(String::from))
.collect();
let expanded = expand_workspace_members(root, &raw_list);
for rel in expanded {
let member_root = root.join(&rel);
let name = fs::read_to_string(member_root.join("Cargo.toml"))
.ok()
.and_then(|r| toml::from_str::<toml::Value>(&r).ok())
.and_then(|mv| {
mv.get("package")
.and_then(|p| p.get("name"))
.and_then(|n| n.as_str())
.map(String::from)
});
if let Some(n) = name {
diag.push(
"detect_language.rust.workspace",
format!("workspace member `{rel}` supplied tool name `{n}`"),
);
return Some(n);
}
}
diag.push(
"detect_language.rust.workspace",
"no workspace member has a [package].name; name fell back to dir tail".to_string(),
);
None
}
pub(crate) fn first_npm_member_name(root: &Path, diag: &mut DiagTrace) -> Option<String> {
let raw = fs::read_to_string(root.join("package.json")).ok()?;
let v = serde_json::from_str::<serde_json::Value>(&raw).ok()?;
let ws = v.get("workspaces")?;
let paths: Vec<String> = match ws {
serde_json::Value::String(s) => vec![s.clone()],
serde_json::Value::Array(arr) => arr
.iter()
.filter_map(|e| e.as_str().map(String::from))
.collect(),
_ => return None,
};
let expanded = expand_workspace_members(root, &paths);
for rel in expanded {
let pkg = root.join(&rel).join("package.json");
let name = fs::read_to_string(&pkg)
.ok()
.and_then(|r| serde_json::from_str::<serde_json::Value>(&r).ok())
.and_then(|mv| mv.get("name").and_then(|n| n.as_str()).map(String::from));
if let Some(n) = name {
diag.push(
"detect_language.node.workspace",
format!("workspace member `{rel}` supplied tool name `{n}`"),
);
return Some(n);
}
}
diag.push(
"detect_language.node.workspace",
"no workspace member has a package.json `name`; name fell back to dir tail".to_string(),
);
None
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn expand_workspace_members_resolves_globs_and_literals() {
let tmp = tempdir().unwrap();
let root = tmp.path();
fs::create_dir_all(root.join("crates/cli")).unwrap();
fs::create_dir_all(root.join("crates/core")).unwrap();
fs::create_dir_all(root.join("other/pkg")).unwrap();
let raw = vec!["crates/*".to_string(), "other/pkg".to_string()];
let expanded = expand_workspace_members(root, &raw);
assert_eq!(
expanded,
vec![
"crates/cli".to_string(),
"crates/core".to_string(),
"other/pkg".to_string()
]
);
}
}