gitdataai/libs/frontend/build.rs
ZhenYi 215846b1db feat(api): pre-compress static assets with brotli and gzip
- 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)
2026-04-25 20:09:09 +08:00

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());
}