use std::path::{Path, PathBuf};
use miette::{Context, IntoDiagnostic, miette};
use super::DepFilter;
pub(crate) fn retarget_cwd(path: &Path) -> miette::Result<()> {
let path = if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir().into_diagnostic()?.join(path)
};
std::env::set_current_dir(&path)
.into_diagnostic()
.wrap_err_with(|| format!("failed to chdir into {}", path.display()))?;
crate::dirs::set_cwd(&path)?;
Ok(())
}
pub(crate) fn load_graph(
project_dir: &Path,
manifest: &aube_manifest::PackageJson,
missing_hint: &str,
) -> miette::Result<aube_lockfile::LockfileGraph> {
match aube_lockfile::parse_lockfile(project_dir, manifest) {
Ok(g) => Ok(g),
Err(aube_lockfile::Error::NotFound(_)) => Err(miette!("{missing_hint}")),
Err(e) => Err(miette::Report::new(e)).wrap_err("failed to parse lockfile"),
}
}
pub(crate) fn collect_dep_closure(
graph: &aube_lockfile::LockfileGraph,
filter: DepFilter,
no_optional: bool,
) -> std::collections::BTreeMap<String, &aube_lockfile::LockedPackage> {
let mut out: std::collections::BTreeMap<String, &aube_lockfile::LockedPackage> =
std::collections::BTreeMap::new();
let mut stack: Vec<String> = graph
.root_deps()
.iter()
.filter(|d| filter.keeps(d.dep_type))
.filter(|d| !(no_optional && matches!(d.dep_type, aube_lockfile::DepType::Optional)))
.map(|d| d.dep_path.clone())
.collect();
while let Some(dep_path) = stack.pop() {
if out.contains_key(&dep_path) {
continue;
}
let Some(pkg) = graph.get_package(&dep_path) else {
continue;
};
out.insert(dep_path.clone(), pkg);
for (name, version) in &pkg.dependencies {
if no_optional && pkg.optional_dependencies.contains_key(name) {
continue;
}
if let Some(child) = aube_lockfile::resolve_dep_edge(name, version, |key| {
graph.packages.contains_key(key)
}) {
stack.push(child);
}
}
if !no_optional {
for (name, version) in &pkg.optional_dependencies {
if let Some(child) = aube_lockfile::resolve_dep_edge(name, version, |key| {
graph.packages.contains_key(key)
}) {
stack.push(child);
}
}
}
}
out
}
pub(crate) fn finish_filtered_workspace(
cwd: &Path,
result: miette::Result<()>,
) -> miette::Result<()> {
let restore =
retarget_cwd(cwd).wrap_err_with(|| format!("failed to restore cwd to {}", cwd.display()));
match result {
Ok(()) => restore,
Err(err) => {
let _ = restore;
Err(err)
}
}
}
pub(crate) fn prepare_resolved_graph_for_lockfile_write(graph: &mut aube_lockfile::LockfileGraph) {
aube_resolver::platform::mark_optional_packages(graph);
aube_resolver::platform::mark_transitive_peer_dependencies(graph);
}
pub(crate) fn write_and_log_lockfile(
cwd: &Path,
graph: &aube_lockfile::LockfileGraph,
manifest: &aube_manifest::PackageJson,
) -> miette::Result<PathBuf> {
let kind = super::lockfile_kind_for_write(cwd);
let written_path = aube_lockfile::write_lockfile_as(cwd, graph, manifest, kind)
.into_diagnostic()
.wrap_err("failed to write lockfile")?;
eprintln!(
"Wrote {}",
written_path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| written_path.display().to_string())
);
Ok(written_path)
}
pub(crate) fn find_workspace_root(start: &Path) -> miette::Result<PathBuf> {
crate::dirs::find_workspace_root(start).ok_or_else(|| {
miette!(
"no workspace root (aube-workspace.yaml, pnpm-workspace.yaml, or package.json with a `workspaces` field) found above {}",
start.display()
)
})
}
pub(crate) fn select_workspace_packages(
cwd: &Path,
filter: &aube_workspace::selector::EffectiveFilter,
command: &str,
) -> miette::Result<(PathBuf, Vec<aube_workspace::selector::SelectedPackage>)> {
let root = crate::dirs::find_workspace_root(cwd).unwrap_or_else(|| cwd.to_path_buf());
let workspace_pkgs = aube_workspace::find_workspace_packages(&root)
.map_err(|e| miette!("failed to discover workspace packages: {e}"))?;
if workspace_pkgs.is_empty() {
return Err(miette!(
"aube {command}: --filter requires a workspace root (aube-workspace.yaml, pnpm-workspace.yaml, or package.json with a `workspaces` field) at or above {}",
cwd.display()
));
}
let matched =
aube_workspace::selector::select_workspace_packages(&root, &workspace_pkgs, filter)
.map_err(|e| miette!("invalid --filter selector: {e}"))?;
if matched.is_empty() {
return Err(miette!(
"aube {command}: filter {filter:?} did not match any workspace package"
));
}
Ok((root, matched))
}
pub(crate) fn workspace_importer_path(workspace_root: &Path, dir: &Path) -> miette::Result<String> {
let rel = pathdiff::diff_paths(dir, workspace_root).ok_or_else(|| {
miette!(
"could not compute path of workspace package {} relative to {}",
dir.display(),
workspace_root.display()
)
})?;
if rel.as_os_str().is_empty() {
Ok(".".to_string())
} else {
Ok(rel.to_string_lossy().replace('\\', "/"))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dep_closure_follows_yarn_optional_only_edges() {
let mut graph = aube_lockfile::LockfileGraph::default();
graph.importers.insert(
".".to_string(),
vec![aube_lockfile::DirectDep {
name: "host".to_string(),
dep_path: "host@1.0.0".to_string(),
dep_type: aube_lockfile::DepType::Production,
specifier: Some("1.0.0".to_string()),
}],
);
graph.packages.insert(
"host@1.0.0".to_string(),
aube_lockfile::LockedPackage {
name: "host".to_string(),
version: "1.0.0".to_string(),
dep_path: "host@1.0.0".to_string(),
optional_dependencies: [("native".to_string(), "1.0.0".to_string())].into(),
..Default::default()
},
);
graph.packages.insert(
"native@1.0.0".to_string(),
aube_lockfile::LockedPackage {
name: "native".to_string(),
version: "1.0.0".to_string(),
dep_path: "native@1.0.0".to_string(),
..Default::default()
},
);
let closure = collect_dep_closure(&graph, DepFilter::All, false);
assert!(closure.contains_key("native@1.0.0"));
let closure = collect_dep_closure(&graph, DepFilter::All, true);
assert!(!closure.contains_key("native@1.0.0"));
}
}