1use std::collections::hash_map::DefaultHasher;
2use std::path::{Path, PathBuf};
3use std::{hash::Hasher, process::Command};
4
5struct Binding {
6 input_path: PathBuf,
7 output_path: PathBuf,
8}
9
10#[derive(Default)]
12pub struct LazyTypeScriptBindings {
13 binding: Vec<Binding>,
14 minify_level: MinifyLevel,
15 watching: Vec<PathBuf>,
16}
17
18impl LazyTypeScriptBindings {
19 pub fn new() -> Self {
21 Self::default()
22 }
23
24 pub fn with_binding(
26 mut self,
27 input_path: impl AsRef<Path>,
28 output_path: impl AsRef<Path>,
29 ) -> Self {
30 let input_path = input_path.as_ref();
31 let output_path = output_path.as_ref();
32
33 self.binding.push(Binding {
34 input_path: input_path.to_path_buf(),
35 output_path: output_path.to_path_buf(),
36 });
37
38 self
39 }
40
41 pub fn with_minify_level(mut self, minify_level: MinifyLevel) -> Self {
43 self.minify_level = minify_level;
44 self
45 }
46
47 pub fn with_watching(mut self, path: impl AsRef<Path>) -> Self {
50 let path = path.as_ref();
51 self.watching.push(path.to_path_buf());
52 self
53 }
54
55 pub fn run(&self) {
57 let mut watching_paths = Vec::new();
59 for path in &self.watching {
60 if let Ok(dir) = std::fs::read_dir(path) {
61 for entry in dir.flatten() {
62 let path = entry.path();
63 if path
64 .extension()
65 .map(|ext| ext == "ts" || ext == "js")
66 .unwrap_or(false)
67 {
68 watching_paths.push(path);
69 }
70 }
71 } else {
72 watching_paths.push(path.to_path_buf());
73 }
74 }
75 for path in &watching_paths {
76 println!("cargo:rerun-if-changed={}", path.display());
77 }
78
79 let hashes = hash_files(watching_paths);
81
82 let hash_location = PathBuf::from("./src/js/");
83 std::fs::create_dir_all(&hash_location).unwrap_or_else(|err| {
84 panic!(
85 "Failed to create directory for hash file: {} in {}",
86 err,
87 hash_location.display()
88 )
89 });
90 let hash_location = hash_location.join("hash.txt");
91
92 let fs_hash_string = std::fs::read_to_string(&hash_location);
94 let expected = fs_hash_string
95 .as_ref()
96 .map(|s| s.trim())
97 .unwrap_or_default();
98 let hashes_string = format!("{hashes:?}");
99 if expected == hashes_string {
100 return;
101 }
102
103 for path in &self.binding {
105 gen_bindings(&path.input_path, &path.output_path, self.minify_level);
106 }
107
108 std::fs::write(hash_location, hashes_string).unwrap();
109 }
110}
111
112#[derive(Copy, Clone, Debug, Default)]
114pub enum MinifyLevel {
115 None,
117 Whitespace,
119 #[default]
121 Syntax,
122 Identifiers,
124}
125
126impl MinifyLevel {
127 fn as_args(&self) -> &'static [&'static str] {
128 match self {
129 MinifyLevel::None => &[],
130 MinifyLevel::Whitespace => &["--minify-whitespace"],
131 MinifyLevel::Syntax => &["--minify-whitespace", "--minify-syntax"],
132 MinifyLevel::Identifiers => &[
133 "--minify-whitespace",
134 "--minify-syntax",
135 "--minify-identifiers",
136 ],
137 }
138 }
139}
140
141fn hash_files(mut files: Vec<PathBuf>) -> Vec<u64> {
143 files.sort();
145 let mut hashes = Vec::new();
146 for file in files {
147 let mut hash = DefaultHasher::new();
148 let Ok(contents) = std::fs::read_to_string(file) else {
149 continue;
150 };
151 for line in contents.lines() {
153 hash.write(line.as_bytes());
154 }
155 hashes.push(hash.finish());
156 }
157 hashes
158}
159
160fn gen_bindings(input_path: &Path, output_path: &Path, minify_level: MinifyLevel) {
168 let status = Command::new("bun")
170 .arg("build")
171 .arg(input_path)
172 .arg("--outfile")
173 .arg(output_path)
174 .args(minify_level.as_args())
175 .status()
176 .unwrap();
177
178 if !status.success() {
179 panic!(
180 "Failed to generate bindings for {:?}. Make sure you have bun installed",
181 input_path
182 );
183 }
184}