use std::collections::hash_map::DefaultHasher;
use std::path::{Path, PathBuf};
use std::{hash::Hasher, process::Command};
struct Binding {
input_path: PathBuf,
output_path: PathBuf,
}
#[derive(Default)]
pub struct LazyTypeScriptBindings {
binding: Vec<Binding>,
minify_level: MinifyLevel,
watching: Vec<PathBuf>,
}
impl LazyTypeScriptBindings {
pub fn new() -> Self {
Self::default()
}
pub fn with_binding(
mut self,
input_path: impl AsRef<Path>,
output_path: impl AsRef<Path>,
) -> Self {
let input_path = input_path.as_ref();
let output_path = output_path.as_ref();
self.binding.push(Binding {
input_path: input_path.to_path_buf(),
output_path: output_path.to_path_buf(),
});
self
}
pub fn with_minify_level(mut self, minify_level: MinifyLevel) -> Self {
self.minify_level = minify_level;
self
}
pub fn with_watching(mut self, path: impl AsRef<Path>) -> Self {
let path = path.as_ref();
self.watching.push(path.to_path_buf());
self
}
pub fn run(&self) {
let mut watching_paths = Vec::new();
for path in &self.watching {
if let Ok(dir) = std::fs::read_dir(path) {
for entry in dir.flatten() {
let path = entry.path();
if path
.extension()
.map(|ext| ext == "ts" || ext == "js")
.unwrap_or(false)
{
watching_paths.push(path);
}
}
} else {
watching_paths.push(path.to_path_buf());
}
}
for path in &watching_paths {
println!("cargo:rerun-if-changed={}", path.display());
}
let hashes = hash_files(watching_paths);
let hash_location = PathBuf::from("./src/js/");
std::fs::create_dir_all(&hash_location).unwrap_or_else(|err| {
panic!(
"Failed to create directory for hash file: {} in {}",
err,
hash_location.display()
)
});
let hash_location = hash_location.join("hash.txt");
let fs_hash_string = std::fs::read_to_string(&hash_location);
let expected = fs_hash_string
.as_ref()
.map(|s| s.trim())
.unwrap_or_default();
let hashes_string = format!("{hashes:?}");
if expected == hashes_string {
return;
}
for path in &self.binding {
gen_bindings(&path.input_path, &path.output_path, self.minify_level);
}
std::fs::write(hash_location, hashes_string).unwrap();
}
}
#[derive(Copy, Clone, Debug, Default)]
pub enum MinifyLevel {
None,
Whitespace,
#[default]
Syntax,
Identifiers,
}
impl MinifyLevel {
fn as_args(&self) -> &'static [&'static str] {
match self {
MinifyLevel::None => &[],
MinifyLevel::Whitespace => &["--minify-whitespace"],
MinifyLevel::Syntax => &["--minify-whitespace", "--minify-syntax"],
MinifyLevel::Identifiers => &[
"--minify-whitespace",
"--minify-syntax",
"--minify-identifiers",
],
}
}
}
fn hash_files(mut files: Vec<PathBuf>) -> Vec<u64> {
files.sort();
let mut hashes = Vec::new();
for file in files {
let mut hash = DefaultHasher::new();
let Ok(contents) = std::fs::read_to_string(file) else {
continue;
};
for line in contents.lines() {
hash.write(line.as_bytes());
}
hashes.push(hash.finish());
}
hashes
}
fn gen_bindings(input_path: &Path, output_path: &Path, minify_level: MinifyLevel) {
let status = Command::new("bun")
.arg("build")
.arg(input_path)
.arg("--outfile")
.arg(output_path)
.args(minify_level.as_args())
.status()
.unwrap();
if !status.success() {
panic!(
"Failed to generate bindings for {:?}. Make sure you have bun installed",
input_path
);
}
}