123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219 |
- use crate::builder::{
- BuildMessage, BuildRequest, MessageSource, MessageType, Stage, UpdateBuildProgress, UpdateStage,
- };
- use crate::Result;
- use anyhow::Context;
- use brotli::enc::BrotliEncoderParams;
- use futures_channel::mpsc::UnboundedSender;
- use manganis_cli_support::{process_file, AssetManifest, AssetManifestExt, AssetType};
- use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
- use std::fs;
- use std::path::Path;
- use std::sync::atomic::AtomicUsize;
- use std::sync::Arc;
- use std::{ffi::OsString, path::PathBuf};
- use std::{fs::File, io::Write};
- use tracing::Level;
- use walkdir::WalkDir;
- /// The temp file name for passing manganis json from linker to current exec.
- pub const MG_JSON_OUT: &str = "mg-out";
- pub fn asset_manifest(build: &BuildRequest) -> AssetManifest {
- let file_path = build.target_out_dir().join(MG_JSON_OUT);
- let read = fs::read_to_string(&file_path).unwrap();
- _ = fs::remove_file(file_path);
- let json: Vec<String> = serde_json::from_str(&read).unwrap();
- AssetManifest::load(json)
- }
- /// Create a head file that contains all of the imports for assets that the user project uses
- pub fn create_assets_head(build: &BuildRequest, manifest: &AssetManifest) -> Result<()> {
- let out_dir = build.target_out_dir();
- std::fs::create_dir_all(&out_dir)?;
- let mut file = File::create(out_dir.join("__assets_head.html"))?;
- file.write_all(manifest.head().as_bytes())?;
- Ok(())
- }
- /// Process any assets collected from the binary
- pub(crate) fn process_assets(
- build: &BuildRequest,
- manifest: &AssetManifest,
- progress: &mut UnboundedSender<UpdateBuildProgress>,
- ) -> anyhow::Result<()> {
- let static_asset_output_dir = build.target_out_dir();
- std::fs::create_dir_all(&static_asset_output_dir)
- .context("Failed to create static asset output directory")?;
- let assets_finished = Arc::new(AtomicUsize::new(0));
- let assets = manifest.assets();
- let asset_count = assets.len();
- assets.par_iter().try_for_each_init(
- || progress.clone(),
- move |progress, asset| {
- if let AssetType::File(file_asset) = asset {
- match process_file(file_asset, &static_asset_output_dir) {
- Ok(_) => {
- // Update the progress
- _ = progress.start_send(UpdateBuildProgress {
- stage: Stage::OptimizingAssets,
- update: UpdateStage::AddMessage(BuildMessage {
- level: Level::INFO,
- message: MessageType::Text(format!(
- "Optimized static asset {}",
- file_asset
- )),
- source: MessageSource::Build,
- }),
- });
- let assets_finished =
- assets_finished.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
- _ = progress.start_send(UpdateBuildProgress {
- stage: Stage::OptimizingAssets,
- update: UpdateStage::SetProgress(
- assets_finished as f64 / asset_count as f64,
- ),
- });
- }
- Err(err) => {
- tracing::error!("Failed to copy static asset: {}", err);
- return Err(err);
- }
- }
- }
- Ok::<(), anyhow::Error>(())
- },
- )?;
- Ok(())
- }
- /// 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
- pub(crate) struct AssetConfigDropGuard;
- impl AssetConfigDropGuard {
- pub fn new(base_path: Option<&str>) -> Self {
- // Set up the collect asset config
- let base = match base_path {
- Some(base) => format!("/{}/", base.trim_matches('/')),
- None => "/".to_string(),
- };
- manganis_cli_support::Config::default()
- .with_assets_serve_location(base)
- .save();
- Self {}
- }
- }
- impl Drop for AssetConfigDropGuard {
- fn drop(&mut self) {
- // Reset the config
- manganis_cli_support::Config::default().save();
- }
- }
- pub(crate) fn copy_dir_to(
- src_dir: PathBuf,
- dest_dir: PathBuf,
- pre_compress: bool,
- ) -> std::io::Result<()> {
- let entries = std::fs::read_dir(&src_dir)?;
- let mut children: Vec<std::thread::JoinHandle<std::io::Result<()>>> = Vec::new();
- for entry in entries.flatten() {
- let entry_path = entry.path();
- let path_relative_to_src = entry_path.strip_prefix(&src_dir).unwrap();
- let output_file_location = dest_dir.join(path_relative_to_src);
- children.push(std::thread::spawn(move || {
- if entry.file_type()?.is_dir() {
- // If the file is a directory, recursively copy it into the output directory
- if let Err(err) =
- copy_dir_to(entry_path.clone(), output_file_location, pre_compress)
- {
- tracing::error!(
- "Failed to pre-compress directory {}: {}",
- entry_path.display(),
- err
- );
- }
- } else {
- // Make sure the directory exists
- std::fs::create_dir_all(output_file_location.parent().unwrap())?;
- // Copy the file to the output directory
- std::fs::copy(&entry_path, &output_file_location)?;
- // Then pre-compress the file if needed
- if pre_compress {
- if let Err(err) = pre_compress_file(&output_file_location) {
- tracing::error!(
- "Failed to pre-compress static assets {}: {}",
- output_file_location.display(),
- err
- );
- }
- // If pre-compression isn't enabled, we should remove the old compressed file if it exists
- } else if let Some(compressed_path) = compressed_path(&output_file_location) {
- _ = std::fs::remove_file(compressed_path);
- }
- }
- Ok(())
- }));
- }
- for child in children {
- child.join().unwrap()?;
- }
- Ok(())
- }
- /// Get the path to the compressed version of a file
- fn compressed_path(path: &Path) -> Option<PathBuf> {
- let new_extension = match path.extension() {
- Some(ext) => {
- if ext.to_string_lossy().to_lowercase().ends_with("br") {
- return None;
- }
- let mut ext = ext.to_os_string();
- ext.push(".br");
- ext
- }
- None => OsString::from("br"),
- };
- Some(path.with_extension(new_extension))
- }
- /// pre-compress a file with brotli
- pub(crate) fn pre_compress_file(path: &Path) -> std::io::Result<()> {
- let Some(compressed_path) = compressed_path(path) else {
- return Ok(());
- };
- let file = std::fs::File::open(path)?;
- let mut stream = std::io::BufReader::new(file);
- let mut buffer = std::fs::File::create(compressed_path)?;
- let params = BrotliEncoderParams::default();
- brotli::BrotliCompress(&mut stream, &mut buffer, ¶ms)?;
- Ok(())
- }
- /// pre-compress all files in a folder
- pub(crate) fn pre_compress_folder(path: &Path, pre_compress: bool) -> std::io::Result<()> {
- let walk_dir = WalkDir::new(path);
- for entry in walk_dir.into_iter().filter_map(|e| e.ok()) {
- let entry_path = entry.path();
- if entry_path.is_file() {
- if pre_compress {
- if let Err(err) = pre_compress_file(entry_path) {
- tracing::error!("Failed to pre-compress file {entry_path:?}: {err}");
- }
- }
- // If pre-compression isn't enabled, we should remove the old compressed file if it exists
- else if let Some(compressed_path) = compressed_path(entry_path) {
- _ = std::fs::remove_file(compressed_path);
- }
- }
- }
- Ok(())
- }
|