use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use bynk_emit::project::{Document, ProjectOutput, is_worker_asset_path, sibling_path};
pub fn write_output(out: &ProjectOutput, dir: &Path) -> std::io::Result<()> {
prune_stale_output(out, dir)?;
for (path, doc) in &out.artefacts.docs {
write_document(path, doc, &out.artefacts.docs, dir)?;
}
for (path, bytes) in &out.artefacts.assets {
debug_assert!(
!out.artefacts.docs.contains_key(path),
"`{}` is both a document and a static asset",
path.display()
);
write_asset(path, bytes, dir)?;
}
write_modules(&out.artefacts.modules, dir)
}
pub fn write_modules(modules: &BTreeMap<PathBuf, Vec<u8>>, dir: &Path) -> std::io::Result<()> {
for (path, bytes) in modules {
let target = dir.join(path);
if let Some(parent) = target.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&target, bytes)?;
}
Ok(())
}
pub fn write_modules_for_node(
modules: &BTreeMap<PathBuf, Vec<u8>>,
dir: &Path,
) -> std::io::Result<()> {
write_modules(modules, dir)?;
for (path, bytes) in modules {
std::fs::write(
dir.join(bynk_emit::project::wasm_shim_path(path)),
bynk_emit::project::node_wasm_shim(bytes),
)?;
}
Ok(())
}
fn write_asset(path: &Path, bytes: &[u8], dir: &Path) -> std::io::Result<()> {
debug_assert!(
is_worker_asset_path(path),
"static asset `{}` is outside every Worker's assets directory",
path.display()
);
let target = dir.join(path);
if let Some(parent) = target.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&target, bytes)
}
pub fn write_document(
path: &Path,
doc: &Document,
docs: &BTreeMap<PathBuf, Document>,
dir: &Path,
) -> std::io::Result<()> {
let target = dir.join(path);
if let Some(parent) = target.parent() {
std::fs::create_dir_all(parent)?;
}
let mapped_text = match doc {
Document::Ts(program) => {
Some(bynk_ts::print(program, "", "", &path.to_string_lossy()).text)
}
Document::Js(s) => Some(s.clone()),
_ => None,
};
if let Some(text) = mapped_text {
match docs.get(&sibling_path(path, "map")) {
Some(_) => {
let map_name = match target.file_name() {
Some(n) => format!("{}.map", n.to_string_lossy()),
None => "module.ts.map".to_string(),
};
let with_trailer = format!("{text}//# sourceMappingURL={map_name}\n");
std::fs::write(&target, with_trailer)?;
}
None => std::fs::write(&target, &text)?,
}
return Ok(());
}
match doc {
Document::Toml(t) => {
std::fs::write(
&target,
bynk_emit::emitter::toml_doc::print_toml_document(t),
)?;
}
Document::Json(s) | Document::SourceMap(s) | Document::DebugSidecar(s) => {
std::fs::write(&target, s)?;
}
Document::Ts(_) | Document::Js(_) => unreachable!("handled above"),
}
Ok(())
}
fn expected_output_paths(out: &ProjectOutput) -> std::collections::HashSet<PathBuf> {
out.artefacts
.docs
.keys()
.chain(out.artefacts.assets.keys())
.chain(out.artefacts.modules.keys())
.cloned()
.collect()
}
fn is_prunable_output_extension(ext: &str) -> bool {
matches!(ext, "ts" | "js" | "map" | "json" | "toml" | "wasm")
}
fn prune_stale_output(out: &ProjectOutput, dir: &Path) -> std::io::Result<()> {
if !dir.is_dir() {
return Ok(());
}
let expected = expected_output_paths(out);
let mut dirs_visited = Vec::new();
prune_stale_output_dir(dir, dir, &expected, &mut dirs_visited)?;
dirs_visited.sort_by_key(|p| std::cmp::Reverse(p.components().count()));
for d in dirs_visited {
let _ = std::fs::remove_dir(&d);
}
Ok(())
}
fn prune_stale_output_dir(
root: &Path,
dir: &Path,
expected: &std::collections::HashSet<std::path::PathBuf>,
dirs_visited: &mut Vec<std::path::PathBuf>,
) -> std::io::Result<()> {
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
let file_type = entry.file_type()?;
let rel = path.strip_prefix(root).unwrap_or(&path);
let owned_asset = is_worker_asset_path(rel);
if file_type.is_dir() {
let is_own_cache = path
.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| n == "node_modules" || n.starts_with('.'));
if is_own_cache && !owned_asset {
continue;
}
prune_stale_output_dir(root, &path, expected, dirs_visited)?;
dirs_visited.push(path);
} else if file_type.is_file() {
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
let prunable = is_prunable_output_extension(ext) || owned_asset;
if prunable && !expected.contains(rel) {
std::fs::remove_file(&path)?;
}
} else if file_type.is_symlink() && owned_asset && !expected.contains(rel) {
std::fs::remove_file(&path)?;
}
}
Ok(())
}