use super::detect_workspace_packages;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct PackageRoot {
pub root: PathBuf,
pub declares_executable: bool,
}
pub fn package_roots(root: &Path) -> Vec<PackageRoot> {
let mut roots = Vec::new();
if has_manifest(root) {
roots.push(PackageRoot {
root: root.to_path_buf(),
declares_executable: declares_executable(root),
});
}
for pkg in detect_workspace_packages(root) {
roots.push(PackageRoot {
declares_executable: declares_executable(&pkg.root),
root: pkg.root,
});
}
roots
}
pub fn path_in_executable_package(path: &Path, roots: &[PackageRoot]) -> bool {
roots
.iter()
.filter(|candidate| path.starts_with(&candidate.root))
.max_by_key(|candidate| candidate.root.components().count())
.is_some_and(|nearest| nearest.declares_executable)
}
fn has_manifest(dir: &Path) -> bool {
dir.join("package.json").is_file() || dir.join("Cargo.toml").is_file()
}
fn declares_executable(dir: &Path) -> bool {
npm_declares_bin(dir) || cargo_declares_bin(dir)
}
fn npm_declares_bin(dir: &Path) -> bool {
let Ok(content) = std::fs::read_to_string(dir.join("package.json")) else {
return false;
};
let Ok(value) = serde_json::from_str::<serde_json::Value>(&content) else {
return false;
};
matches!(value.get("bin"), Some(v) if v.is_string() || v.is_object())
}
fn cargo_declares_bin(dir: &Path) -> bool {
if dir.join("src/bin").is_dir() || dir.join("src/main.rs").is_file() {
return true;
}
std::fs::read_to_string(dir.join("Cargo.toml"))
.map(|content| content.contains("[[bin]]"))
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::{package_roots, path_in_executable_package};
use std::fs;
use tempfile::TempDir;
#[test]
fn npm_bin_object_marks_package() {
let dir = TempDir::new().unwrap();
fs::write(
dir.path().join("package.json"),
r#"{ "name": "ignite-cli", "bin": { "ignite": "bin/ignite" } }"#,
)
.unwrap();
let roots = package_roots(dir.path());
assert!(path_in_executable_package(
&dir.path().join("src/commands/new.ts"),
&roots
));
}
#[test]
fn npm_bin_string_marks_package() {
let dir = TempDir::new().unwrap();
fs::write(
dir.path().join("package.json"),
r#"{ "name": "tool", "bin": "./cli.js" }"#,
)
.unwrap();
let roots = package_roots(dir.path());
assert!(path_in_executable_package(
&dir.path().join("src/index.js"),
&roots
));
}
#[test]
fn package_without_bin_is_not_executable() {
let dir = TempDir::new().unwrap();
fs::write(
dir.path().join("package.json"),
r#"{ "name": "web-app", "dependencies": {} }"#,
)
.unwrap();
let roots = package_roots(dir.path());
assert!(!path_in_executable_package(
&dir.path().join("src/server.ts"),
&roots
));
}
#[test]
fn cargo_bin_target_marks_package() {
let dir = TempDir::new().unwrap();
fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"tool\"\n\n[[bin]]\nname = \"tool\"\npath = \"src/main.rs\"\n",
)
.unwrap();
let roots = package_roots(dir.path());
assert!(path_in_executable_package(
&dir.path().join("src/lib.rs"),
&roots
));
}
#[test]
fn root_cli_does_not_mark_non_cli_workspace_member() {
let dir = TempDir::new().unwrap();
fs::write(
dir.path().join("package.json"),
r#"{ "name": "monorepo", "workspaces": ["packages/*"], "bin": { "my-tool": "./src/cli.ts" } }"#,
)
.unwrap();
let web = dir.path().join("packages/web");
let core = dir.path().join("packages/core");
fs::create_dir_all(&web).unwrap();
fs::create_dir_all(&core).unwrap();
fs::write(web.join("package.json"), r#"{ "name": "web" }"#).unwrap();
fs::write(
core.join("package.json"),
r#"{ "name": "core", "bin": "./index.js" }"#,
)
.unwrap();
let roots = package_roots(dir.path());
assert!(path_in_executable_package(
&dir.path().join("src/cli.ts"),
&roots
));
assert!(!path_in_executable_package(
&web.join("src/server.ts"),
&roots
));
assert!(path_in_executable_package(
&core.join("src/main.js"),
&roots
));
}
}