- build.rs: generate .gz and .br compressed variants at build time, embed both into the FRONTEND constant - dist.rs: content-type for SPA fallback uses index.html path explicitly, explicit extension mapping for Vite content-hashed filenames - frontend lib: get_frontend_asset_compressed returns (data, encoding, etag)
174 lines
5.7 KiB
Rust
174 lines
5.7 KiB
Rust
use std::env;
|
|
use std::fs;
|
|
use std::io::Write;
|
|
use std::path::PathBuf;
|
|
use std::process::Command;
|
|
|
|
fn run_pnpm(args: &[&str], cwd: &str) {
|
|
let mut cmd = if cfg!(target_os = "windows") {
|
|
let mut c = Command::new("cmd");
|
|
c.args(["/C", "pnpm"]);
|
|
c
|
|
} else {
|
|
Command::new("pnpm")
|
|
};
|
|
|
|
let status = cmd
|
|
.args(args)
|
|
.current_dir(cwd)
|
|
.status()
|
|
.expect("failed to run pnpm");
|
|
|
|
if !status.success() {
|
|
panic!("pnpm command failed: {:?}", args);
|
|
}
|
|
}
|
|
|
|
fn find_all_files(path: PathBuf) -> Vec<PathBuf> {
|
|
let mut files = vec![];
|
|
for entry in fs::read_dir(path).unwrap() {
|
|
let entry = entry.unwrap();
|
|
let path = entry.path();
|
|
if path.is_file() {
|
|
files.push(path);
|
|
} else if path.is_dir() {
|
|
files.extend(find_all_files(path));
|
|
}
|
|
}
|
|
files
|
|
}
|
|
|
|
/// Returns true if the file type is already compressed and not worth re-compressing.
|
|
fn is_compressed_asset(path: &str) -> bool {
|
|
path.ends_with(".png")
|
|
|| path.ends_with(".jpg")
|
|
|| path.ends_with(".jpeg")
|
|
|| path.ends_with(".gif")
|
|
|| path.ends_with(".webp")
|
|
|| path.ends_with(".avif")
|
|
|| path.ends_with(".ico")
|
|
|| path.ends_with(".woff")
|
|
|| path.ends_with(".woff2")
|
|
|| path.ends_with(".ttf")
|
|
|| path.ends_with(".otf")
|
|
|| path.ends_with(".eot")
|
|
|| path.ends_with(".zip")
|
|
|| path.ends_with(".gz")
|
|
|| path.ends_with(".br")
|
|
|| path.ends_with(".xz")
|
|
|| path.ends_with(".mp4")
|
|
|| path.ends_with(".mp3")
|
|
|| path.ends_with(".webm")
|
|
|| path.ends_with(".ogg")
|
|
|| path.ends_with(".wasm")
|
|
}
|
|
|
|
/// Minimum file size (bytes) before compression is applied.
|
|
const MIN_COMPRESS_SIZE: usize = 256;
|
|
|
|
fn main() {
|
|
println!("cargo:rerun-if-changed=../../package.json");
|
|
println!("cargo:rerun-if-changed=../../pnpm-lock.yaml");
|
|
println!("cargo:rerun-if-changed=../../tsconfig.json");
|
|
println!("cargo:rerun-if-changed=../../src/");
|
|
|
|
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
|
let project_root = manifest_dir.parent().unwrap().parent().unwrap();
|
|
|
|
println!("cargo:warning=Building frontend...");
|
|
run_pnpm(&["run", "build"], project_root.to_str().unwrap());
|
|
|
|
let dist = project_root.join("dist");
|
|
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
|
|
let blob_dir = out_dir.join("dist_blobs");
|
|
fs::create_dir_all(&blob_dir).unwrap();
|
|
|
|
let mut strs = vec![];
|
|
for file in find_all_files(dist.clone()) {
|
|
let key = file
|
|
.strip_prefix(&dist)
|
|
.unwrap()
|
|
.components()
|
|
.collect::<PathBuf>()
|
|
.to_string_lossy()
|
|
.replace('\\', "/");
|
|
let safe_name = key.replace('/', "_").replace('\\', "_");
|
|
let blob_path = blob_dir.join(&safe_name);
|
|
let content = fs::read(&file).unwrap();
|
|
fs::write(&blob_path, &content).unwrap();
|
|
|
|
let hash = md5::compute(&content);
|
|
let etag_literal = format!("\"{:x}\"", hash);
|
|
let key_literal = format!("\"{}\"", key.replace('"', "\\\""));
|
|
|
|
let (gz_literal, br_literal) =
|
|
if !is_compressed_asset(&key) && content.len() >= MIN_COMPRESS_SIZE {
|
|
let gz_path = blob_dir.join(format!("{}.gz", safe_name));
|
|
let br_path = blob_dir.join(format!("{}.br", safe_name));
|
|
|
|
let gz_data = {
|
|
let mut encoder =
|
|
flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
|
|
let _ = encoder.write_all(&content);
|
|
encoder.finish().unwrap()
|
|
};
|
|
let br_data = {
|
|
let mut encoder = brotli::CompressorWriter::new(Vec::new(), 4096, 11, 22);
|
|
let _ = encoder.write_all(&content);
|
|
encoder.flush().unwrap();
|
|
encoder.into_inner()
|
|
};
|
|
|
|
if gz_data.len() < content.len() {
|
|
fs::write(&gz_path, &gz_data).unwrap();
|
|
}
|
|
if br_data.len() < content.len() {
|
|
fs::write(&br_path, &br_data).unwrap();
|
|
}
|
|
|
|
let gz = if gz_data.len() < content.len() {
|
|
format!(
|
|
"Some((include_bytes!(\"dist_blobs/{}.gz\").to_vec(), \"gzip\"))",
|
|
safe_name
|
|
)
|
|
} else {
|
|
"None".to_string()
|
|
};
|
|
let br = if br_data.len() < content.len() {
|
|
format!(
|
|
"Some((include_bytes!(\"dist_blobs/{}.br\").to_vec(), \"br\"))",
|
|
safe_name
|
|
)
|
|
} else {
|
|
"None".to_string()
|
|
};
|
|
(gz, br)
|
|
} else {
|
|
("None".to_string(), "None".to_string())
|
|
};
|
|
|
|
strs.push(format!(
|
|
r#" (
|
|
{},
|
|
include_bytes!("dist_blobs/{}"),
|
|
{},
|
|
{},
|
|
{},
|
|
),"#,
|
|
key_literal,
|
|
safe_name,
|
|
etag_literal,
|
|
gz_literal,
|
|
br_literal
|
|
));
|
|
}
|
|
|
|
let out_file = out_dir.join("frontend.rs");
|
|
let generated = format!(
|
|
"lazy_static::lazy_static! {{\n pub static ref FRONTEND: Vec<(\n &'static str,\n &'static [u8],\n &'static str,\n Option<(Vec<u8>, &'static str)>,\n Option<(Vec<u8>, &'static str)>,\n )> = vec![\n{}\n ];\n}}\n",
|
|
strs.join("\n")
|
|
);
|
|
fs::write(&out_file, generated).unwrap();
|
|
println!("cargo:include={}", out_file.display());
|
|
}
|