use clap::Args;
use miette::{Context, IntoDiagnostic, miette};
use std::collections::BTreeSet;
pub const AFTER_LONG_HELP: &str = "\
Examples:
$ aube ignored-builds
The following builds were ignored during install:
esbuild@0.20.2
puppeteer@22.8.0
# When nothing was skipped
$ aube ignored-builds
No ignored builds.
# Approve them for this project
$ aube approve-builds
";
#[derive(Debug, Args)]
pub struct IgnoredBuildsArgs {
#[arg(short = 'g', long)]
pub global: bool,
}
pub async fn run(args: IgnoredBuildsArgs) -> miette::Result<()> {
if args.global {
return Err(miette!(
"`--global` is not yet implemented for `ignored-builds`"
));
}
let cwd = crate::dirs::project_root()?;
let ignored = collect_ignored(&cwd)?;
if ignored.is_empty() {
println!("No ignored builds.");
return Ok(());
}
println!("The following builds were ignored during install:");
for entry in &ignored {
println!(" {}@{}", entry.name, entry.version);
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct IgnoredEntry {
pub name: String,
pub version: String,
}
impl std::cmp::Ord for IgnoredEntry {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.name
.cmp(&other.name)
.then_with(|| self.version.cmp(&other.version))
}
}
impl std::cmp::PartialOrd for IgnoredEntry {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
pub(super) fn collect_ignored(project_dir: &std::path::Path) -> miette::Result<Vec<IgnoredEntry>> {
let manifest = aube_manifest::PackageJson::from_path(&project_dir.join("package.json"))
.into_diagnostic()
.wrap_err("failed to read package.json")?;
let graph = match aube_lockfile::parse_lockfile(project_dir, &manifest) {
Ok(g) => g,
Err(aube_lockfile::Error::NotFound(_)) => return Ok(Vec::new()),
Err(e) => return Err(miette!(e)).wrap_err("failed to parse lockfile"),
};
let workspace = aube_manifest::WorkspaceConfig::load(project_dir)
.into_diagnostic()
.wrap_err("failed to load workspace config")?;
let (policy, _warnings) =
super::install::build_policy_from_sources(&manifest, &workspace, false);
let store = super::open_store(project_dir)?;
let mut seen: BTreeSet<(String, String)> = BTreeSet::new();
let mut out: Vec<IgnoredEntry> = Vec::new();
for pkg in graph.packages.values() {
if !seen.insert((pkg.name.clone(), pkg.version.clone())) {
continue;
}
if matches!(
policy.decide(&pkg.name, &pkg.version),
aube_scripts::AllowDecision::Allow
) {
continue;
}
if !has_lifecycle_scripts(&store, &pkg.name, &pkg.version) {
continue;
}
out.push(IgnoredEntry {
name: pkg.name.clone(),
version: pkg.version.clone(),
});
}
out.sort();
Ok(out)
}
fn has_lifecycle_scripts(store: &aube_store::Store, name: &str, version: &str) -> bool {
let Some(index) = store.load_index(name, version) else {
return false;
};
let Some(stored) = index.get("package.json") else {
return false;
};
let Ok(content) = std::fs::read_to_string(&stored.store_path) else {
return false;
};
let Ok(manifest) = serde_json::from_str::<aube_manifest::PackageJson>(&content) else {
return false;
};
if aube_scripts::DEP_LIFECYCLE_HOOKS
.iter()
.any(|h| manifest.scripts.contains_key(h.script_name()))
{
return true;
}
aube_scripts::implicit_install_script(&manifest, index.contains_key("binding.gyp")).is_some()
}