Only compress web assets in the CLI (#2329)

* only compress web assets in the CLI

* precompressed -> pre-compressed

* only enable pre-compression in release builds
This commit is contained in:
Evan Almloff authored and GitHub committed 2024-04-26 00:47:04 -04:00
1 parent b147f5a737
commit a27d4e71ed
13 files changed
+461 -468

No files matched your search

Generated
+320 -303
View File
File diff suppressed because it is too large. Load diff
+9
View File
@@ -211,6 +211,7 @@ impl Default for DioxusConfig {
key_path: None,
cert_path: None,
},
pre_compress: true,
},
bundle: BundleConfig {
identifier: Some(format!("io.github.{name}")),
@@ -280,6 +281,9 @@ pub struct WebConfig {
pub resource: WebResourceConfig,
#[serde(default)]
pub https: WebHttpsConfig,
/// Whether to enable pre-compression of assets and wasm during a web build in release mode
#[serde(default = "true_bool")]
pub pre_compress: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -571,6 +575,11 @@ impl CrateConfig {
self.add_features(features);
self
}
/// Check if assets should be pre_compressed. This will only be true in release mode if the user has enabled pre_compress in the web config.
pub fn should_pre_compress_web_assets(&self) -> bool {
self.dioxus_config.web.pre_compress && self.release
}
}
fn true_bool() -> bool {
+2
View File
@@ -83,7 +83,9 @@ tauri-bundler = { version = "=1.4.*", features = ["native-tls-vendored"] }
syn = { workspace = true }
prettyplease = { workspace = true }
# Assets
manganis-cli-support = { workspace = true, features = ["html"] }
brotli = "5.0.0"
dioxus-autofmt = { workspace = true }
dioxus-check = { workspace = true }
+102 -1
View File
@@ -1,7 +1,13 @@
use std::{fs::File, io::Write, path::PathBuf};
use brotli::enc::BrotliEncoderParams;
use std::path::Path;
use std::{ffi::OsString, path::PathBuf};
use walkdir::WalkDir;
use std::{fs::File, io::Write};
use crate::Result;
use dioxus_cli_config::CrateConfig;
use dioxus_cli_config::Platform;
use manganis_cli_support::{AssetManifest, AssetManifestExt};
pub fn asset_manifest(bin: Option<&str>, crate_config: &CrateConfig) -> AssetManifest {
@@ -56,3 +62,98 @@ impl Drop for AssetConfigDropGuard {
manganis_cli_support::Config::default().save();
}
}
pub fn copy_assets_dir(config: &CrateConfig, platform: Platform) -> anyhow::Result<()> {
tracing::info!("Copying public assets to the output directory...");
let out_dir = config.out_dir();
let asset_dir = config.asset_dir();
if asset_dir.is_dir() {
// Only pre-compress the assets from the web build. Desktop assets are not served, so they don't need to be pre_compressed
let pre_compress = platform == Platform::Web && config.should_pre_compress_web_assets();
copy_dir_to(asset_dir, out_dir, pre_compress)?;
}
Ok(())
}
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(&entry_path.clone()) {
tracing::error!(
"Failed to pre-compress static assets {}: {}",
entry_path.display(),
err
);
}
}
}
Ok(())
}));
}
for child in children {
child.join().unwrap()?;
}
Ok(())
}
/// pre-compress a file with brotli
pub(crate) fn pre_compress_file(path: &Path) -> std::io::Result<()> {
let new_extension = match path.extension() {
Some(ext) => {
if ext.to_string_lossy().to_lowercase().ends_with("br") {
return Ok(());
}
let mut ext = ext.to_os_string();
ext.push(".br");
ext
}
None => OsString::from("br"),
};
let file = std::fs::File::open(path)?;
let mut stream = std::io::BufReader::new(file);
let output = path.with_extension(new_extension);
let mut buffer = std::fs::File::create(output)?;
let params = BrotliEncoderParams::default();
brotli::BrotliCompress(&mut stream, &mut buffer, &params)?;
Ok(())
}
/// pre-compress all files in a folder
pub(crate) fn pre_compress_folder(path: &Path) -> 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() {
pre_compress_file(entry_path)?;
}
}
Ok(())
}
+21 -76
View File
@@ -1,5 +1,8 @@
use crate::{
assets::{asset_manifest, create_assets_head, process_assets, AssetConfigDropGuard},
assets::{
asset_manifest, copy_assets_dir, create_assets_head, pre_compress_folder, process_assets,
AssetConfigDropGuard,
},
error::{Error, Result},
tools::Tool,
};
@@ -85,13 +88,12 @@ pub fn build_web(
..
} = config;
let out_dir = config.out_dir();
let asset_dir = config.asset_dir();
let _guard = AssetConfigDropGuard::new();
let _manganis_support = ManganisSupportGuard::default();
// start to build the assets
let ignore_files = build_assets(config)?;
build_assets(config)?;
let t_start = std::time::Instant::now();
let _guard = dioxus_cli_config::__private::save_config(config);
@@ -156,16 +158,16 @@ pub fn build_web(
ExecutableType::Example(name) => cmd.arg("--example").arg(name),
};
let warning_messages = prettier_build(cmd)?;
let CargoBuildResult {
warnings,
output_location,
} = prettier_build(cmd)?;
let output_location = output_location.context("No output location found")?;
// [2] Establish the output directory structure
let bindgen_outdir = out_dir.join("assets").join("dioxus");
let input_path = warning_messages
.output_location
.as_ref()
.context("No output location found")?
.with_extension("wasm");
let input_path = output_location.with_extension("wasm");
tracing::info!("Running wasm-bindgen");
let run_wasm_bindgen = || {
@@ -234,6 +236,11 @@ pub fn build_web(
tracing::info!("Skipping optimization with wasm-opt, binaryen tool not found.");
}
// If pre-compressing is enabled, we can pre_compress the wasm-bindgen output
if config.should_pre_compress_web_assets() {
pre_compress_folder(&bindgen_outdir)?;
}
// [5][OPTIONAL] If tailwind is enabled and installed we run it to generate the CSS
if dioxus_tools.contains_key("tailwindcss") {
let info = dioxus_tools.get("tailwindcss").unwrap();
@@ -274,38 +281,7 @@ pub fn build_web(
}
// this code will copy all public file to the output dir
let copy_options = fs_extra::dir::CopyOptions {
overwrite: true,
skip_exist: false,
buffer_size: 64000,
copy_inside: false,
content_only: false,
depth: 0,
};
tracing::info!("Copying public assets to the output directory...");
if asset_dir.is_dir() {
for entry in std::fs::read_dir(config.asset_dir())?.flatten() {
let path = entry.path();
if path.is_file() {
std::fs::copy(&path, out_dir.join(path.file_name().unwrap()))?;
} else {
match fs_extra::dir::copy(&path, &out_dir, &copy_options) {
Ok(_) => {}
Err(_e) => {
tracing::warn!("Error copying dir: {}", _e);
}
}
for ignore in &ignore_files {
let ignore = ignore.strip_prefix(&config.asset_dir()).unwrap();
let ignore = out_dir.join(ignore);
if ignore.is_file() {
std::fs::remove_file(ignore)?;
}
}
}
}
}
copy_assets_dir(config, dioxus_cli_config::Platform::Web)?;
let assets = if !skip_assets {
tracing::info!("Processing assets");
@@ -317,8 +293,8 @@ pub fn build_web(
};
Ok(BuildResult {
warnings: warning_messages.warnings,
executable: warning_messages.output_location,
warnings,
executable: Some(output_location),
elapsed_time: t_start.elapsed().as_millis(),
assets,
})
@@ -366,7 +342,7 @@ pub fn build_desktop(
tracing::info!("🚅 Running build [Desktop] command...");
let t_start = std::time::Instant::now();
let ignore_files = build_assets(config)?;
build_assets(config)?;
let _guard = dioxus_cli_config::__private::save_config(config);
let _manganis_support = ManganisSupportGuard::default();
let _guard = AssetConfigDropGuard::new();
@@ -427,38 +403,7 @@ pub fn build_desktop(
copy(res_path, &output_path)?;
}
// this code will copy all public file to the output dir
if config.asset_dir().is_dir() {
let copy_options = fs_extra::dir::CopyOptions {
overwrite: true,
skip_exist: false,
buffer_size: 64000,
copy_inside: false,
content_only: false,
depth: 0,
};
for entry in std::fs::read_dir(config.asset_dir())?.flatten() {
let path = entry.path();
if path.is_file() {
std::fs::copy(&path, &config.out_dir().join(path.file_name().unwrap()))?;
} else {
match fs_extra::dir::copy(&path, &config.out_dir(), &copy_options) {
Ok(_) => {}
Err(e) => {
tracing::warn!("Error copying dir: {}", e);
}
}
for ignore in &ignore_files {
let ignore = ignore.strip_prefix(&config.asset_dir()).unwrap();
let ignore = config.out_dir().join(ignore);
if ignore.is_file() {
std::fs::remove_file(ignore)?;
}
}
}
}
}
copy_assets_dir(config, dioxus_cli_config::Platform::Desktop)?;
let assets = if !skip_assets {
tracing::info!("Processing assets");
+1 -4
View File
@@ -8,10 +8,7 @@ use dioxus_html::HtmlCtx;
use dioxus_rsx::hot_reload::*;
use fs_extra::dir::CopyOptions;
use notify::{RecommendedWatcher, Watcher};
use std::{
path::PathBuf,
sync::{Arc, Mutex},
};
use std::{path::PathBuf, sync::Arc};
use tokio::sync::broadcast::{self};
mod output;
+1 -1
View File
@@ -12,7 +12,7 @@ use dioxus_cli_config::CrateConfig;
use dioxus_rsx::hot_reload::*;
use std::{
net::{SocketAddr, UdpSocket},
sync::{Arc, Mutex},
sync::Arc,
};
use tokio::sync::broadcast;
-1
View File
@@ -112,7 +112,6 @@ mod test {
use super::*;
use axum::Router;
use axum_server::{Handle, Server};
async fn setup_servers(mut config: WebProxyConfig) -> String {
-1
View File
@@ -365,7 +365,6 @@ pub fn extract_zip(file: &Path, target: &Path) -> anyhow::Result<()> {
#[cfg(test)]
mod test {
use super::*;
use std::path::PathBuf;
use tempfile::tempdir;
#[test]
+1 -2
View File
@@ -18,7 +18,6 @@ dioxus_server_macro = { workspace = true }
# axum
axum = { workspace = true, features = ["ws", "macros"], optional = true }
tower-http = { workspace = true, optional = true, features = ["fs"] }
async-compression = { version = "0.4.6", features = ["gzip", "tokio"], optional = true }
dioxus-lib = { workspace = true }
@@ -77,7 +76,7 @@ desktop = ["dioxus-desktop", "server_fn/reqwest", "dioxus_server_macro/reqwest"]
mobile = ["dioxus-mobile"]
default-tls = ["server_fn/default-tls"]
rustls = ["server_fn/rustls"]
axum = ["dep:axum", "tower-http", "server", "server_fn/axum", "dioxus_server_macro/axum", "async-compression"]
axum = ["dep:axum", "tower-http", "server", "server_fn/axum", "dioxus_server_macro/axum"]
server = [
"server_fn/ssr",
"dioxus_server_macro/server",
-70
View File
@@ -1,70 +0,0 @@
//! Handles pre-compression for any static assets
use std::{ffi::OsString, path::PathBuf, pin::Pin};
use async_compression::tokio::bufread::GzipEncoder;
use futures_util::Future;
use tokio::task::JoinSet;
#[allow(unused)]
pub async fn pre_compress_files(directory: PathBuf) -> tokio::io::Result<()> {
// print to stdin encoded gzip data
pre_compress_dir(directory).await?;
Ok(())
}
fn pre_compress_dir(
path: PathBuf,
) -> Pin<Box<dyn Future<Output = tokio::io::Result<()>> + Send + Sync>> {
Box::pin(async move {
let mut entries = tokio::fs::read_dir(&path).await?;
let mut set: JoinSet<tokio::io::Result<()>> = JoinSet::new();
while let Some(entry) = entries.next_entry().await? {
set.spawn(async move {
if entry.file_type().await?.is_dir() {
if let Err(err) = pre_compress_dir(entry.path()).await {
tracing::error!(
"Failed to pre-compress directory {}: {}",
entry.path().display(),
err
);
}
} else if let Err(err) = pre_compress_file(entry.path()).await {
tracing::error!(
"Failed to pre-compress static assets {}: {}",
entry.path().display(),
err
);
}
Ok(())
});
}
while let Some(res) = set.join_next().await {
res??;
}
Ok(())
})
}
async fn pre_compress_file(path: PathBuf) -> tokio::io::Result<()> {
let file = tokio::fs::File::open(&path).await?;
let stream = tokio::io::BufReader::new(file);
let mut encoder = GzipEncoder::new(stream);
let new_extension = match path.extension() {
Some(ext) => {
if ext.to_string_lossy().to_lowercase().ends_with("gz") {
return Ok(());
}
let mut ext = ext.to_os_string();
ext.push(".gz");
ext
}
None => OsString::from("gz"),
};
let output = path.with_extension(new_extension);
let mut buffer = tokio::fs::File::create(&output).await?;
tokio::io::copy(&mut encoder, &mut buffer).await?;
Ok(())
}
+4 -7
View File
@@ -207,6 +207,8 @@ where
self
}
// TODO: This is a breaking change, but we should probably serve static assets from a different directory than dist where the server executable is located
// This would prevent issues like https://github.com/DioxusLabs/dioxus/issues/2327
fn serve_static_assets(
mut self,
assets_path: impl Into<std::path::PathBuf>,
@@ -215,11 +217,6 @@ where
let assets_path = assets_path.into();
async move {
#[cfg(not(debug_assertions))]
if let Err(err) = crate::assets::pre_compress_files(assets_path.clone()).await {
tracing::error!("Failed to pre-compress static assets: {}", err);
}
// Serve all files in dist folder except index.html
let dir = std::fs::read_dir(&assets_path).unwrap_or_else(|e| {
panic!(
@@ -246,9 +243,9 @@ where
.join("/");
let route = format!("/{}", route);
if path.is_dir() {
self = self.nest_service(&route, ServeDir::new(path).precompressed_gzip());
self = self.nest_service(&route, ServeDir::new(path).precompressed_br());
} else {
self = self.nest_service(&route, ServeFile::new(path).precompressed_gzip());
self = self.nest_service(&route, ServeFile::new(path).precompressed_br());
}
}
-2
View File
@@ -8,8 +8,6 @@ pub use once_cell;
mod html_storage;
#[cfg(feature = "axum")]
mod assets;
#[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
#[cfg(feature = "axum")]
mod axum_adapter;