lazy_js_bundle/lib.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
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,
}
/// A builder for generating TypeScript bindings lazily
#[derive(Default)]
pub struct LazyTypeScriptBindings {
binding: Vec<Binding>,
minify_level: MinifyLevel,
watching: Vec<PathBuf>,
}
impl LazyTypeScriptBindings {
/// Create a new builder for generating TypeScript bindings that inputs from the given path and outputs javascript to the given path
pub fn new() -> Self {
Self::default()
}
/// Add a binding to generate
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
}
/// Set the minify level for the bindings
pub fn with_minify_level(mut self, minify_level: MinifyLevel) -> Self {
self.minify_level = minify_level;
self
}
/// Watch any .js or .ts files in a directory and re-generate the bindings when they change
// TODO: we should watch any files that get bundled by bun by reading the source map
pub fn with_watching(mut self, path: impl AsRef<Path>) -> Self {
let path = path.as_ref();
self.watching.push(path.to_path_buf());
self
}
/// Run the bindings
pub fn run(&self) {
// If any TS changes, re-run the build script
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());
}
// Compute the hash of the input files
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");
// If the hash matches the one on disk, we're good and don't need to update bindings
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;
}
// Otherwise, generate the bindings and write the new hash to disk
for path in &self.binding {
gen_bindings(&path.input_path, &path.output_path, self.minify_level);
}
std::fs::write(hash_location, hashes_string).unwrap();
}
}
/// The level of minification to apply to the bindings
#[derive(Copy, Clone, Debug, Default)]
pub enum MinifyLevel {
/// Don't minify the bindings
None,
/// Minify whitespace
Whitespace,
/// Minify whitespace and syntax
#[default]
Syntax,
/// Minify whitespace, syntax, and identifiers
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",
],
}
}
}
/// Hashes the contents of a directory
fn hash_files(mut files: Vec<PathBuf>) -> Vec<u64> {
// Different systems will read the files in different orders, so we sort them to make sure the hash is consistent
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;
};
// windows + git does a weird thing with line endings, so we need to normalize them
for line in contents.lines() {
hash.write(line.as_bytes());
}
hashes.push(hash.finish());
}
hashes
}
// okay...... so bun might fail if the user doesn't have it installed
// we don't really want to fail if that's the case
// but if you started *editing* the .ts files, you're gonna have a bad time
// so.....
// we need to hash each of the .ts files and add that hash to the JS files
// if the hashes don't match, we need to fail the build
// that way we also don't need
fn gen_bindings(input_path: &Path, output_path: &Path, minify_level: MinifyLevel) {
// If the file is generated, and the hash is different, we need to generate it
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
);
}
}