1
0

lib.rs 6.7 KB

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