use super::DeployArgs;
use super::filtering::StripFields;
use super::injection::{materialize_injections, plan_injections};
use super::rewrite::{DeployRoot, rewrite_local_refs};
use crate::commands::CatalogMap;
use crate::commands::pack::collect_package_files;
use miette::{Context, IntoDiagnostic, miette};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
pub(super) struct StagedDeploy {
pub(super) name: String,
pub(super) version: String,
pub(super) target: PathBuf,
pub(super) bundled_local_refs: bool,
}
pub(super) fn stage_one(
source_pkg_dir: &Path,
target: &Path,
ws_index: &BTreeMap<String, (PathBuf, Option<String>)>,
catalogs: &CatalogMap,
args: &DeployArgs,
deploy_all_files: bool,
) -> miette::Result<StagedDeploy> {
ensure_target_writable(target)?;
std::fs::create_dir_all(target)
.into_diagnostic()
.wrap_err_with(|| format!("failed to create {}", target.display()))?;
let manifest = crate::commands::load_manifest(&source_pkg_dir.join("package.json"))?;
let name = manifest
.name
.clone()
.ok_or_else(|| miette!("deploy: package.json has no `name` field"))?;
let version = manifest
.version
.clone()
.unwrap_or_else(|| "0.0.0".to_string());
let files = if deploy_all_files {
collect_all_files(source_pkg_dir, target)?
} else {
collect_package_files(source_pkg_dir, &manifest)?
};
for (src, rel) in &files {
let dst = target.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()))?;
}
let plan = plan_injections(source_pkg_dir, target, ws_index, args)?;
materialize_injections(&plan, ws_index, deploy_all_files)?;
let deployed_canonical = super::canonicalize(source_pkg_dir);
let root = DeployRoot {
deployed_canonical: &deployed_canonical,
target_root: target,
};
rewrite_local_refs(
&target.join("package.json"),
source_pkg_dir,
target,
ws_index,
catalogs,
&plan,
StripFields::for_args(args),
root,
)?;
let bundled_strip = StripFields::for_bundled_sibling(args);
for inj in plan.values() {
if inj.is_tarball {
continue;
}
rewrite_local_refs(
&inj.target_dir.join("package.json"),
&inj.source_dir,
&inj.target_dir,
ws_index,
catalogs,
&plan,
bundled_strip,
root,
)?;
}
Ok(StagedDeploy {
name,
version,
target: target.to_path_buf(),
bundled_local_refs: !plan.is_empty(),
})
}
pub(super) fn collect_all_files(
source: &Path,
target: &Path,
) -> miette::Result<Vec<(PathBuf, String)>> {
let target_canon = std::fs::canonicalize(target).unwrap_or_else(|_| target.to_path_buf());
let mut out = Vec::new();
let mut stack = vec![source.to_path_buf()];
while let Some(dir) = stack.pop() {
let iter = std::fs::read_dir(&dir)
.into_diagnostic()
.wrap_err_with(|| format!("deploy: read_dir({}) failed", dir.display()))?;
for entry in iter {
let entry = entry
.into_diagnostic()
.wrap_err_with(|| format!("deploy: failed to read entry in {}", dir.display()))?;
let name = entry.file_name();
if matches!(name.to_string_lossy().as_ref(), "node_modules" | ".git") {
continue;
}
let path = entry.path();
let canon = std::fs::canonicalize(&path).unwrap_or_else(|_| path.clone());
if canon == target_canon {
continue;
}
let ft = entry
.file_type()
.into_diagnostic()
.wrap_err_with(|| format!("deploy: failed to stat {}", path.display()))?;
let (is_dir, is_file) = if ft.is_symlink() {
match std::fs::metadata(&path) {
Ok(md) => (md.is_dir(), md.is_file()),
Err(_) => (false, false),
}
} else {
(ft.is_dir(), ft.is_file())
};
if is_dir && !ft.is_symlink() {
stack.push(path);
} else if is_file && let Ok(rel) = path.strip_prefix(source) {
out.push((path.clone(), rel.to_string_lossy().replace('\\', "/")));
}
}
}
Ok(out)
}
pub(super) fn ensure_target_writable(target: &Path) -> miette::Result<()> {
match std::fs::read_dir(target) {
Ok(mut entries) => {
if entries.next().is_some() {
return Err(miette!(
"{}: target directory {} is not empty",
aube_util::cmd("deploy"),
target.display()
));
}
Ok(())
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(miette!(
"{}: failed to inspect {}: {e}",
aube_util::cmd("deploy"),
target.display()
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ensure_target_writable_empty_dir_is_ok() {
let tmp = tempfile::tempdir().unwrap();
ensure_target_writable(tmp.path()).unwrap();
}
#[test]
fn ensure_target_writable_missing_is_ok() {
let tmp = tempfile::tempdir().unwrap();
ensure_target_writable(&tmp.path().join("nope")).unwrap();
}
#[test]
fn ensure_target_writable_nonempty_errors() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join("stuff"), "hi").unwrap();
assert!(ensure_target_writable(tmp.path()).is_err());
}
}