use std::path::{Path, PathBuf};
use std::time::Instant;
use anyhow::{bail, Context, Result};
use super::dev::host::monorepo::monorepo_dev_dir;
use super::dev::sources_resolver;
use super::dev::{build_and_stage, copy_dir_recursive, ipc_call, load_manifest};
fn ensure_reloadable(name: &str, app_type: &str) -> Result<()> {
if app_type.eq_ignore_ascii_case("platform-runtime") {
bail!("'{name}' is a platform-runtime packaging dep — nothing to reload");
}
if app_type.eq_ignore_ascii_case("standalone") {
bail!(
"'{name}' is a standalone app (own OS process, supervised by the dev session) — \
it can't be hot-swapped from outside; use `node-app restart` or the dev TUI"
);
}
Ok(())
}
pub fn run(project_path: &Path, names: &[String], instances: &[String]) -> Result<()> {
let project_path = project_path
.canonicalize()
.with_context(|| format!("canonicalize {}", project_path.display()))?;
if !project_path.join("infra/debian/platform-depends").is_file() {
bail!(
"{} is not the platform repo (no infra/debian/platform-depends) — run from the monorepo root or pass --path",
project_path.display()
);
}
if names.is_empty() {
bail!("pass one or more app names (as in platform-depends, without the node-app- prefix)");
}
let instances: Vec<String> = if instances.is_empty() {
vec!["alice".to_string()]
} else {
instances.to_vec()
};
let mut targets: Vec<(String, PathBuf, PathBuf)> = Vec::new(); for instance in &instances {
let dev_dir = monorepo_dev_dir(&project_path, instance, None)?;
let socket = dev_dir
.parent()
.map(|env| env.join("control.sock"))
.ok_or_else(|| anyhow::anyhow!("dev dir {} has no parent", dev_dir.display()))?;
if !socket.exists() {
bail!(
"no running dev session for instance '{instance}' ({} missing) — start `node-app dev` first",
socket.display()
);
}
targets.push((instance.clone(), dev_dir, socket));
}
let mut seen = std::collections::HashSet::new();
let names: Vec<String> = names
.iter()
.filter(|n| seen.insert((*n).clone()))
.cloned()
.collect();
let dependencies: Vec<super::dev::platform::PlatformDependency> = names
.iter()
.map(|name| super::dev::platform::PlatformDependency {
key: name.clone(),
package: format!("node-app-{name}"),
})
.collect();
let sources = sources_resolver::resolve(&project_path, &dependencies, &[], None)?;
if sources.len() != names.len() {
bail!("resolver returned {} source(s) for {} name(s)", sources.len(), names.len());
}
for (name, resolved) in names.iter().zip(&sources) {
let src = &resolved.source_path;
let manifest = load_manifest(src)
.with_context(|| format!("read manifest for '{name}' at {}", src.display()))?;
ensure_reloadable(name, &manifest.app_type)?;
let build_t0 = Instant::now();
let (first_instance, first_dev_dir, _) = &targets[0];
println!("→ building '{name}' from {}…", src.display());
build_and_stage(src, first_dev_dir, None)
.with_context(|| format!("build dep '{name}' ({})", src.display()))?;
let build_secs = build_t0.elapsed().as_secs_f32();
let staged = first_dev_dir.join(&manifest.name);
for (_, dev_dir, _) in &targets[1..] {
let dst = dev_dir.join(&manifest.name);
if dst.exists() {
std::fs::remove_dir_all(&dst).ok();
}
copy_dir_recursive(&staged, &dst)
.with_context(|| format!("mirror staged '{name}' into {}", dev_dir.display()))?;
}
for (instance, dev_dir, socket) in &targets {
let load_t0 = Instant::now();
let dest = dev_dir.join(&manifest.name);
ipc_call(
socket,
"app.dev_load",
serde_json::json!({
"name": manifest.name,
"path": dest.to_string_lossy(),
}),
)
.with_context(|| format!("app.dev_load '{name}' into '{instance}'"))?;
let extra = if instance == first_instance {
format!("build {build_secs:.1}s, ")
} else {
String::new()
};
println!(
"✓ reloaded '{name}' into {instance} ({extra}dev_load {:.1}s)",
load_t0.elapsed().as_secs_f32()
);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reload_rejects_standalone() {
let err = ensure_reloadable("lcd", "standalone").unwrap_err();
assert!(err.to_string().contains("standalone app"));
assert!(err.to_string().contains("node-app restart"));
}
#[test]
fn reload_rejects_platform_runtime() {
let err = ensure_reloadable("bun-runtime", "platform-runtime").unwrap_err();
assert!(err.to_string().contains("nothing to reload"));
}
#[test]
fn reload_accepts_managed_types() {
for t in ["bun", "native", "cdylib", "bun-fullstack"] {
assert!(ensure_reloadable("x", t).is_ok(), "type {t} should be reloadable");
}
}
}