use crate::commands::workspace_importer_path;
use miette::{Context, miette};
pub(super) fn filter_graph_to_workspace_selection(
workspace_root: &std::path::Path,
workspace_packages: &[std::path::PathBuf],
graph: &aube_lockfile::LockfileGraph,
filters: &aube_workspace::selector::EffectiveFilter,
) -> miette::Result<aube_lockfile::LockfileGraph> {
let selected = aube_workspace::selector::select_workspace_packages(
workspace_root,
workspace_packages,
filters,
)
.map_err(|e| miette!("invalid --filter selector: {e}"))?;
if selected.is_empty() {
return Err(miette!(
"aube install: filter {filters:?} did not match any workspace package"
));
}
let mut keep_importers = std::collections::BTreeSet::new();
if graph.importers.contains_key(".") {
keep_importers.insert(".".to_string());
}
for pkg in selected {
keep_importers.insert(workspace_importer_path(workspace_root, &pkg.dir)?);
}
let importers: std::collections::BTreeMap<String, Vec<aube_lockfile::DirectDep>> = graph
.importers
.iter()
.filter(|(importer, _)| keep_importers.contains(*importer))
.map(|(importer, deps)| (importer.clone(), deps.clone()))
.collect();
let filtered = aube_lockfile::LockfileGraph {
importers,
..graph.clone()
};
Ok(filtered.filter_deps(|_| true))
}
pub(super) fn importer_project_dir(
workspace_root: &std::path::Path,
importer_path: &str,
) -> std::path::PathBuf {
if importer_path == "." {
workspace_root.to_path_buf()
} else {
aube_util::path::normalize_lexical(&workspace_root.join(importer_path))
}
}
pub(super) fn order_lifecycle_manifests(
manifests: Vec<(String, aube_manifest::PackageJson)>,
) -> Vec<(String, aube_manifest::PackageJson)> {
if manifests.len() < 2 {
return manifests;
}
let importer_index: std::collections::HashMap<&str, usize> = manifests
.iter()
.enumerate()
.map(|(idx, (importer, _))| (importer.as_str(), idx))
.collect();
let workspace_name_to_importer: std::collections::HashMap<&str, &str> = manifests
.iter()
.filter_map(|(importer, manifest)| {
manifest
.name
.as_deref()
.map(|name| (name, importer.as_str()))
})
.collect();
let mut edges = vec![Vec::<usize>::new(); manifests.len()];
let mut indegree = vec![0usize; manifests.len()];
for (dependent_idx, (dependent_importer, manifest)) in manifests.iter().enumerate() {
for dep_name in manifest
.dependencies
.keys()
.chain(manifest.dev_dependencies.keys())
.chain(manifest.optional_dependencies.keys())
{
let Some(dependency_importer) = workspace_name_to_importer.get(dep_name.as_str())
else {
continue;
};
if *dependency_importer == dependent_importer {
continue;
}
let Some(&dependency_idx) = importer_index.get(dependency_importer) else {
continue;
};
if !edges[dependency_idx].contains(&dependent_idx) {
edges[dependency_idx].push(dependent_idx);
indegree[dependent_idx] += 1;
}
}
}
let mut ready: std::collections::VecDeque<usize> = indegree
.iter()
.enumerate()
.filter_map(|(idx, degree)| (*degree == 0).then_some(idx))
.collect();
let mut ordered = Vec::with_capacity(manifests.len());
let mut emitted = vec![false; manifests.len()];
while let Some(idx) = ready.pop_front() {
if emitted[idx] {
continue;
}
emitted[idx] = true;
ordered.push(idx);
for &dependent_idx in &edges[idx] {
indegree[dependent_idx] -= 1;
if indegree[dependent_idx] == 0 {
ready.push_back(dependent_idx);
}
}
}
for (idx, is_emitted) in emitted.iter().enumerate() {
if !is_emitted {
ordered.push(idx);
}
}
let mut manifests = manifests
.into_iter()
.map(Some)
.collect::<Vec<Option<(String, aube_manifest::PackageJson)>>>();
ordered
.into_iter()
.filter_map(|idx| manifests[idx].take())
.collect()
}
pub(super) fn write_per_project_lockfiles(
workspace_root: &std::path::Path,
graph: &aube_lockfile::LockfileGraph,
workspace_manifests: &[(String, aube_manifest::PackageJson)],
write_kind: aube_lockfile::LockfileKind,
) -> miette::Result<()> {
use miette::IntoDiagnostic;
for (importer_path, pkg_manifest) in workspace_manifests {
if importer_path == "." {
continue;
}
let Some(subset) = graph.subset_to_importer(importer_path, |_| true) else {
tracing::debug!(
"sharedWorkspaceLockfile=false: skipping {importer_path} (no graph importer entry)"
);
continue;
};
let pkg_dir = workspace_root.join(importer_path);
let written = aube_lockfile::write_lockfile_as(&pkg_dir, &subset, pkg_manifest, write_kind)
.into_diagnostic()
.wrap_err_with(|| format!("failed to write per-project lockfile at {importer_path}"))?;
tracing::debug!(
"sharedWorkspaceLockfile=false: wrote {} for importer {importer_path}",
written
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| written.display().to_string())
);
}
Ok(())
}
pub(super) fn filter_graph_to_importers<const N: usize>(
graph: &aube_lockfile::LockfileGraph,
keep_importers: [&str; N],
) -> aube_lockfile::LockfileGraph {
let keep_importers: std::collections::BTreeSet<&str> = keep_importers.into_iter().collect();
let importers: std::collections::BTreeMap<String, Vec<aube_lockfile::DirectDep>> = graph
.importers
.iter()
.filter(|(importer, _)| keep_importers.contains(importer.as_str()))
.map(|(importer, deps)| (importer.clone(), deps.clone()))
.collect();
let filtered = aube_lockfile::LockfileGraph {
importers,
..graph.clone()
};
filtered.filter_deps(|_| true)
}
#[cfg(test)]
mod lifecycle_manifest_order_tests {
use super::order_lifecycle_manifests;
#[test]
fn lifecycle_manifests_follow_workspace_dependency_order() {
let ordered = order_lifecycle_manifests(vec![
(".".to_string(), named_manifest("root")),
(
"packages/app".to_string(),
manifest_with_dep("app", "@scope/lib"),
),
("packages/lib".to_string(), named_manifest("@scope/lib")),
]);
let importers = ordered
.iter()
.map(|(importer, _)| importer.as_str())
.collect::<Vec<_>>();
assert_eq!(importers, [".", "packages/lib", "packages/app"]);
}
fn named_manifest(name: &str) -> aube_manifest::PackageJson {
aube_manifest::PackageJson {
name: Some(name.to_string()),
..Default::default()
}
}
fn manifest_with_dep(name: &str, dep: &str) -> aube_manifest::PackageJson {
let mut manifest = named_manifest(name);
manifest
.dependencies
.insert(dep.to_string(), "workspace:*".to_string());
manifest
}
}