lib.rs 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  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. let hash_location = PathBuf::from("./src/js/");
  72. std::fs::create_dir_all(&hash_location).unwrap_or_else(|err| {
  73. panic!(
  74. "Failed to create directory for hash file: {} in {}",
  75. err,
  76. hash_location.display()
  77. )
  78. });
  79. let hash_location = hash_location.join("hash.txt");
  80. // If the hash matches the one on disk, we're good and don't need to update bindings
  81. let fs_hash_string = std::fs::read_to_string(&hash_location);
  82. let expected = fs_hash_string
  83. .as_ref()
  84. .map(|s| s.trim())
  85. .unwrap_or_default();
  86. let hashes_string = format!("{hashes:?}");
  87. if expected == hashes_string {
  88. return;
  89. }
  90. // Otherwise, generate the bindings and write the new hash to disk
  91. for path in &self.binding {
  92. gen_bindings(&path.input_path, &path.output_path, self.minify_level);
  93. }
  94. std::fs::write(hash_location, hashes_string).unwrap();
  95. }
  96. }
  97. /// The level of minification to apply to the bindings
  98. #[derive(Copy, Clone, Debug, Default)]
  99. pub enum MinifyLevel {
  100. /// Don't minify the bindings
  101. None,
  102. /// Minify whitespace
  103. Whitespace,
  104. /// Minify whitespace and syntax
  105. #[default]
  106. Syntax,
  107. /// Minify whitespace, syntax, and identifiers
  108. Identifiers,
  109. }
  110. impl MinifyLevel {
  111. fn as_args(&self) -> &'static [&'static str] {
  112. match self {
  113. MinifyLevel::None => &[],
  114. MinifyLevel::Whitespace => &["--minify-whitespace"],
  115. MinifyLevel::Syntax => &["--minify-whitespace", "--minify-syntax"],
  116. MinifyLevel::Identifiers => &[
  117. "--minify-whitespace",
  118. "--minify-syntax",
  119. "--minify-identifiers",
  120. ],
  121. }
  122. }
  123. }
  124. /// Hashes the contents of a directory
  125. fn hash_files(mut files: Vec<PathBuf>) -> Vec<u64> {
  126. // Different systems will read the files in different orders, so we sort them to make sure the hash is consistent
  127. files.sort();
  128. let mut hashes = Vec::new();
  129. for file in files {
  130. let mut hash = DefaultHasher::new();
  131. let Ok(contents) = std::fs::read_to_string(file) else {
  132. continue;
  133. };
  134. // windows + git does a weird thing with line endings, so we need to normalize them
  135. for line in contents.lines() {
  136. hash.write(line.as_bytes());
  137. }
  138. hashes.push(hash.finish());
  139. }
  140. hashes
  141. }
  142. // okay...... so bun might fail if the user doesn't have it installed
  143. // we don't really want to fail if that's the case
  144. // but if you started *editing* the .ts files, you're gonna have a bad time
  145. // so.....
  146. // we need to hash each of the .ts files and add that hash to the JS files
  147. // if the hashes don't match, we need to fail the build
  148. // that way we also don't need
  149. fn gen_bindings(input_path: &Path, output_path: &Path, minify_level: MinifyLevel) {
  150. // If the file is generated, and the hash is different, we need to generate it
  151. let status = Command::new("bun")
  152. .arg("build")
  153. .arg(input_path)
  154. .arg("--outfile")
  155. .arg(output_path)
  156. .args(minify_level.as_args())
  157. .status()
  158. .unwrap();
  159. if !status.success() {
  160. panic!(
  161. "Failed to generate bindings for {:?}. Make sure you have bun installed",
  162. input_path
  163. );
  164. }
  165. }