assets.rs 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. use crate::builder::{
  2. BuildMessage, BuildRequest, MessageSource, MessageType, Stage, UpdateBuildProgress, UpdateStage,
  3. };
  4. use crate::Result;
  5. use anyhow::Context;
  6. use brotli::enc::BrotliEncoderParams;
  7. use futures_channel::mpsc::UnboundedSender;
  8. use manganis_cli_support::{process_file, AssetManifest, AssetManifestExt, AssetType};
  9. use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
  10. use std::fs;
  11. use std::path::Path;
  12. use std::sync::atomic::AtomicUsize;
  13. use std::sync::Arc;
  14. use std::{ffi::OsString, path::PathBuf};
  15. use std::{fs::File, io::Write};
  16. use tracing::Level;
  17. use walkdir::WalkDir;
  18. /// The temp file name for passing manganis json from linker to current exec.
  19. pub const MG_JSON_OUT: &str = "mg-out";
  20. pub fn asset_manifest(build: &BuildRequest) -> AssetManifest {
  21. let file_path = build.target_out_dir().join(MG_JSON_OUT);
  22. let read = fs::read_to_string(&file_path).unwrap();
  23. _ = fs::remove_file(file_path);
  24. let json: Vec<String> = serde_json::from_str(&read).unwrap();
  25. AssetManifest::load(json)
  26. }
  27. /// Create a head file that contains all of the imports for assets that the user project uses
  28. pub fn create_assets_head(build: &BuildRequest, manifest: &AssetManifest) -> Result<()> {
  29. let out_dir = build.target_out_dir();
  30. std::fs::create_dir_all(&out_dir)?;
  31. let mut file = File::create(out_dir.join("__assets_head.html"))?;
  32. file.write_all(manifest.head().as_bytes())?;
  33. Ok(())
  34. }
  35. /// Process any assets collected from the binary
  36. pub(crate) fn process_assets(
  37. build: &BuildRequest,
  38. manifest: &AssetManifest,
  39. progress: &mut UnboundedSender<UpdateBuildProgress>,
  40. ) -> anyhow::Result<()> {
  41. let static_asset_output_dir = build.target_out_dir();
  42. std::fs::create_dir_all(&static_asset_output_dir)
  43. .context("Failed to create static asset output directory")?;
  44. let assets_finished = Arc::new(AtomicUsize::new(0));
  45. let assets = manifest.assets();
  46. let asset_count = assets.len();
  47. assets.par_iter().try_for_each_init(
  48. || progress.clone(),
  49. move |progress, asset| {
  50. if let AssetType::File(file_asset) = asset {
  51. match process_file(file_asset, &static_asset_output_dir) {
  52. Ok(_) => {
  53. // Update the progress
  54. _ = progress.start_send(UpdateBuildProgress {
  55. stage: Stage::OptimizingAssets,
  56. update: UpdateStage::AddMessage(BuildMessage {
  57. level: Level::INFO,
  58. message: MessageType::Text(format!(
  59. "Optimized static asset {}",
  60. file_asset
  61. )),
  62. source: MessageSource::Build,
  63. }),
  64. });
  65. let assets_finished =
  66. assets_finished.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
  67. _ = progress.start_send(UpdateBuildProgress {
  68. stage: Stage::OptimizingAssets,
  69. update: UpdateStage::SetProgress(
  70. assets_finished as f64 / asset_count as f64,
  71. ),
  72. });
  73. }
  74. Err(err) => {
  75. tracing::error!("Failed to copy static asset: {}", err);
  76. return Err(err);
  77. }
  78. }
  79. }
  80. Ok::<(), anyhow::Error>(())
  81. },
  82. )?;
  83. Ok(())
  84. }
  85. /// A guard that sets up the environment for the web renderer to compile in. This guard sets the location that assets will be served from
  86. pub(crate) struct AssetConfigDropGuard;
  87. impl AssetConfigDropGuard {
  88. pub fn new(base_path: Option<&str>) -> Self {
  89. // Set up the collect asset config
  90. let base = match base_path {
  91. Some(base) => format!("/{}/", base.trim_matches('/')),
  92. None => "/".to_string(),
  93. };
  94. manganis_cli_support::Config::default()
  95. .with_assets_serve_location(base)
  96. .save();
  97. Self {}
  98. }
  99. }
  100. impl Drop for AssetConfigDropGuard {
  101. fn drop(&mut self) {
  102. // Reset the config
  103. manganis_cli_support::Config::default().save();
  104. }
  105. }
  106. pub(crate) fn copy_dir_to(
  107. src_dir: PathBuf,
  108. dest_dir: PathBuf,
  109. pre_compress: bool,
  110. ) -> std::io::Result<()> {
  111. let entries = std::fs::read_dir(&src_dir)?;
  112. let mut children: Vec<std::thread::JoinHandle<std::io::Result<()>>> = Vec::new();
  113. for entry in entries.flatten() {
  114. let entry_path = entry.path();
  115. let path_relative_to_src = entry_path.strip_prefix(&src_dir).unwrap();
  116. let output_file_location = dest_dir.join(path_relative_to_src);
  117. children.push(std::thread::spawn(move || {
  118. if entry.file_type()?.is_dir() {
  119. // If the file is a directory, recursively copy it into the output directory
  120. if let Err(err) =
  121. copy_dir_to(entry_path.clone(), output_file_location, pre_compress)
  122. {
  123. tracing::error!(
  124. "Failed to pre-compress directory {}: {}",
  125. entry_path.display(),
  126. err
  127. );
  128. }
  129. } else {
  130. // Make sure the directory exists
  131. std::fs::create_dir_all(output_file_location.parent().unwrap())?;
  132. // Copy the file to the output directory
  133. std::fs::copy(&entry_path, &output_file_location)?;
  134. // Then pre-compress the file if needed
  135. if pre_compress {
  136. if let Err(err) = pre_compress_file(&output_file_location) {
  137. tracing::error!(
  138. "Failed to pre-compress static assets {}: {}",
  139. output_file_location.display(),
  140. err
  141. );
  142. }
  143. // If pre-compression isn't enabled, we should remove the old compressed file if it exists
  144. } else if let Some(compressed_path) = compressed_path(&output_file_location) {
  145. _ = std::fs::remove_file(compressed_path);
  146. }
  147. }
  148. Ok(())
  149. }));
  150. }
  151. for child in children {
  152. child.join().unwrap()?;
  153. }
  154. Ok(())
  155. }
  156. /// Get the path to the compressed version of a file
  157. fn compressed_path(path: &Path) -> Option<PathBuf> {
  158. let new_extension = match path.extension() {
  159. Some(ext) => {
  160. if ext.to_string_lossy().to_lowercase().ends_with("br") {
  161. return None;
  162. }
  163. let mut ext = ext.to_os_string();
  164. ext.push(".br");
  165. ext
  166. }
  167. None => OsString::from("br"),
  168. };
  169. Some(path.with_extension(new_extension))
  170. }
  171. /// pre-compress a file with brotli
  172. pub(crate) fn pre_compress_file(path: &Path) -> std::io::Result<()> {
  173. let Some(compressed_path) = compressed_path(path) else {
  174. return Ok(());
  175. };
  176. let file = std::fs::File::open(path)?;
  177. let mut stream = std::io::BufReader::new(file);
  178. let mut buffer = std::fs::File::create(compressed_path)?;
  179. let params = BrotliEncoderParams::default();
  180. brotli::BrotliCompress(&mut stream, &mut buffer, &params)?;
  181. Ok(())
  182. }
  183. /// pre-compress all files in a folder
  184. pub(crate) fn pre_compress_folder(path: &Path, pre_compress: bool) -> std::io::Result<()> {
  185. let walk_dir = WalkDir::new(path);
  186. for entry in walk_dir.into_iter().filter_map(|e| e.ok()) {
  187. let entry_path = entry.path();
  188. if entry_path.is_file() {
  189. if pre_compress {
  190. if let Err(err) = pre_compress_file(entry_path) {
  191. tracing::error!("Failed to pre-compress file {entry_path:?}: {err}");
  192. }
  193. }
  194. // If pre-compression isn't enabled, we should remove the old compressed file if it exists
  195. else if let Some(compressed_path) = compressed_path(entry_path) {
  196. _ = std::fs::remove_file(compressed_path);
  197. }
  198. }
  199. }
  200. Ok(())
  201. }