use super::DeployArgs;
use super::filtering::StripFields;
use aube_manifest::PackageJson;
use miette::{Context, IntoDiagnostic, miette};
use std::collections::{BTreeMap, VecDeque};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub(super) struct Injection {
pub(super) source_dir: PathBuf,
pub(super) is_tarball: bool,
pub(super) target_dir: PathBuf,
pub(super) tarball_filename: String,
}
pub(super) type InjectionPlan = BTreeMap<PathBuf, Injection>;
pub(super) fn plan_injections(
deployed_pkg_dir: &Path,
target_root: &Path,
ws_index: &BTreeMap<String, (PathBuf, Option<String>)>,
args: &DeployArgs,
) -> miette::Result<InjectionPlan> {
let injected_root =
target_root.join(format!(".{}-deploy-injected", aube_util::embedder().name));
let mut plan: InjectionPlan = BTreeMap::new();
let mut used_ids: BTreeMap<String, u32> = BTreeMap::new();
let deployed_canonical = super::canonicalize(deployed_pkg_dir);
let mut queue: VecDeque<(PathBuf, StripFields)> = VecDeque::new();
queue.push_back((deployed_pkg_dir.to_path_buf(), StripFields::for_args(args)));
while let Some((pkg_dir, strip)) = queue.pop_front() {
let manifest_path = pkg_dir.join("package.json");
let manifest = crate::commands::load_manifest(&manifest_path)?;
for (dep_name, dep_spec) in iter_strippable_deps(&manifest, strip) {
if aube_util::pkg::is_workspace_spec(&dep_spec) {
let Some((sibling_dir, _)) = ws_index.get(&dep_name) else {
return Err(miette!(
"{}: {} declares `{dep_name}: {dep_spec}` but no workspace package named {dep_name:?} was found",
aube_util::cmd("deploy"),
manifest_path.display()
));
};
let canonical = super::canonicalize(sibling_dir);
if canonical == deployed_canonical {
continue;
}
if !plan.contains_key(&canonical) {
let id = unique_id(&dep_name, &mut used_ids);
plan.insert(
canonical.clone(),
Injection {
source_dir: canonical.clone(),
is_tarball: false,
target_dir: injected_root.join(&id),
tarball_filename: String::new(),
},
);
queue.push_back((canonical, StripFields::for_bundled_sibling(args)));
}
} else if let Some(local) = aube_lockfile::LocalSource::parse(&dep_spec, &pkg_dir) {
match local {
aube_lockfile::LocalSource::Directory(rel)
| aube_lockfile::LocalSource::Link(rel)
| aube_lockfile::LocalSource::Portal(rel) => {
let abs = pkg_dir.join(&rel);
let canonical = super::canonicalize(&abs);
if canonical == deployed_canonical {
continue;
}
if !plan.contains_key(&canonical) {
let id_seed = canonical
.file_name()
.and_then(|s| s.to_str())
.unwrap_or(&dep_name);
let id = unique_id(id_seed, &mut used_ids);
plan.insert(
canonical.clone(),
Injection {
source_dir: canonical.clone(),
is_tarball: false,
target_dir: injected_root.join(&id),
tarball_filename: String::new(),
},
);
queue.push_back((
canonical.clone(),
StripFields::for_bundled_sibling(args),
));
}
}
aube_lockfile::LocalSource::Tarball(rel) => {
let abs = pkg_dir.join(&rel);
let canonical = super::canonicalize(&abs);
if !plan.contains_key(&canonical) {
let stem = canonical
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or(&dep_name);
let id = unique_id(stem, &mut used_ids);
let filename = canonical
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| format!("{stem}.tgz"));
plan.insert(
canonical.clone(),
Injection {
source_dir: canonical.clone(),
is_tarball: true,
target_dir: injected_root.join(&id),
tarball_filename: filename,
},
);
}
}
aube_lockfile::LocalSource::Exec(_)
| aube_lockfile::LocalSource::Git(_)
| aube_lockfile::LocalSource::RemoteTarball(_) => {}
}
}
}
}
Ok(plan)
}
fn iter_strippable_deps(manifest: &PackageJson, strip: StripFields) -> Vec<(String, String)> {
let mut out = Vec::new();
if !strip.dependencies {
for (k, v) in &manifest.dependencies {
out.push((k.clone(), v.clone()));
}
}
if !strip.dev_dependencies {
for (k, v) in &manifest.dev_dependencies {
out.push((k.clone(), v.clone()));
}
}
if !strip.optional_dependencies {
for (k, v) in &manifest.optional_dependencies {
out.push((k.clone(), v.clone()));
}
}
out
}
fn unique_id(seed: &str, used: &mut BTreeMap<String, u32>) -> String {
let cleaned: String = seed
.chars()
.map(|c| {
if matches!(c, '/' | '\\' | ':' | ' ' | '\t') {
'_'
} else {
c
}
})
.collect();
let base = if cleaned.is_empty() {
"pkg".to_string()
} else {
cleaned
};
let count = used.entry(base.clone()).or_insert(0);
*count += 1;
if *count == 1 {
base
} else {
format!("{base}_{count}")
}
}
pub(super) fn materialize_injections(
plan: &InjectionPlan,
ws_index: &BTreeMap<String, (PathBuf, Option<String>)>,
deploy_all_files: bool,
) -> miette::Result<()> {
for inj in plan.values() {
std::fs::create_dir_all(&inj.target_dir)
.into_diagnostic()
.wrap_err_with(|| format!("failed to create {}", inj.target_dir.display()))?;
if inj.is_tarball {
let dst = inj.target_dir.join(&inj.tarball_filename);
std::fs::copy(&inj.source_dir, &dst)
.into_diagnostic()
.wrap_err_with(|| {
format!(
"failed to copy {} -> {}",
inj.source_dir.display(),
dst.display()
)
})?;
continue;
}
let source_is_workspace_sibling = ws_index
.values()
.any(|(p, _)| super::canonicalize(p) == inj.source_dir);
let files: Vec<(PathBuf, String)> = if deploy_all_files && source_is_workspace_sibling {
super::staging::collect_all_files(&inj.source_dir, &inj.target_dir)?
} else {
let manifest = crate::commands::load_manifest(&inj.source_dir.join("package.json"))?;
crate::commands::pack::collect_package_files(&inj.source_dir, &manifest)?
};
for (src, rel) in &files {
let dst = inj.target_dir.join(rel);
if let Some(parent) = dst.parent() {
std::fs::create_dir_all(parent)
.into_diagnostic()
.wrap_err_with(|| format!("failed to create {}", parent.display()))?;
}
std::fs::copy(src, &dst)
.into_diagnostic()
.wrap_err_with(|| {
format!("failed to copy {} -> {}", src.display(), dst.display())
})?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unique_id_disambiguates_collisions() {
let mut used = BTreeMap::new();
assert_eq!(unique_id("lib", &mut used), "lib");
assert_eq!(unique_id("lib", &mut used), "lib_2");
}
#[test]
fn unique_id_sanitizes_unsafe_chars() {
let mut used = BTreeMap::new();
assert_eq!(unique_id("@scope/name", &mut used), "@scope_name");
}
}