use super::ProcessingResult;
use crate::staticfiles::DependencyGraph;
use std::collections::HashMap;
use std::path::PathBuf;
pub struct AssetBundler {
graph: DependencyGraph,
files: HashMap<PathBuf, Vec<u8>>,
}
impl AssetBundler {
pub fn new() -> Self {
Self {
graph: DependencyGraph::new(),
files: HashMap::new(),
}
}
pub fn add_file(&mut self, path: PathBuf, content: Vec<u8>) {
self.graph.add_file(path.to_string_lossy().to_string());
self.files.insert(path, content);
}
pub fn add_dependency(&mut self, from: PathBuf, to: PathBuf) {
self.graph.add_dependency(
from.to_string_lossy().to_string(),
to.to_string_lossy().to_string(),
);
}
pub fn bundle(&self) -> ProcessingResult<Vec<u8>> {
let order = self.graph.resolve_order();
let mut result = Vec::new();
for file_name in order {
let path = PathBuf::from(&file_name);
if let Some(content) = self.files.get(&path) {
let separator = format!("\n/* {} */\n", file_name);
result.extend_from_slice(separator.as_bytes());
result.extend_from_slice(content);
result.push(b'\n');
}
}
Ok(result)
}
pub fn bundle_files(&self, paths: &[PathBuf]) -> ProcessingResult<Vec<u8>> {
let mut result = Vec::new();
for path in paths {
if let Some(content) = self.files.get(path) {
let separator = format!("\n/* {} */\n", path.display());
result.extend_from_slice(separator.as_bytes());
result.extend_from_slice(content);
result.push(b'\n');
}
}
Ok(result)
}
pub fn len(&self) -> usize {
self.files.len()
}
pub fn is_empty(&self) -> bool {
self.files.is_empty()
}
}
impl Default for AssetBundler {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct BundleConfig {
pub output: PathBuf,
pub files: Vec<PathBuf>,
pub minify: bool,
pub source_map: bool,
}
impl BundleConfig {
pub fn new(output: PathBuf) -> Self {
Self {
output,
files: Vec::new(),
minify: false,
source_map: false,
}
}
pub fn add_file(&mut self, path: PathBuf) {
self.files.push(path);
}
pub fn with_minify(mut self, enable: bool) -> Self {
self.minify = enable;
self
}
pub fn with_source_map(mut self, enable: bool) -> Self {
self.source_map = enable;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bundler_creation() {
let bundler = AssetBundler::new();
assert!(bundler.is_empty());
assert_eq!(bundler.len(), 0);
}
#[test]
fn test_bundler_add_file() {
let mut bundler = AssetBundler::new();
bundler.add_file(PathBuf::from("test.js"), b"const x = 1;".to_vec());
assert_eq!(bundler.len(), 1);
assert!(!bundler.is_empty());
}
#[test]
fn test_bundler_simple_bundle() {
let mut bundler = AssetBundler::new();
bundler.add_file(PathBuf::from("a.js"), b"const a = 1;".to_vec());
bundler.add_file(PathBuf::from("b.js"), b"const b = 2;".to_vec());
let result = bundler.bundle().unwrap();
let output = String::from_utf8(result).unwrap();
assert!(output.contains("const a = 1;"));
assert!(output.contains("const b = 2;"));
}
#[test]
fn test_bundler_with_dependencies() {
let mut bundler = AssetBundler::new();
bundler.add_file(PathBuf::from("main.js"), b"// main".to_vec());
bundler.add_file(PathBuf::from("utils.js"), b"// utils".to_vec());
bundler.add_dependency(PathBuf::from("main.js"), PathBuf::from("utils.js"));
let result = bundler.bundle().unwrap();
let output = String::from_utf8(result).unwrap();
let utils_pos = output.find("// utils").unwrap();
let main_pos = output.find("// main").unwrap();
assert!(utils_pos < main_pos);
}
#[test]
fn test_bundler_bundle_files_custom_order() {
let mut bundler = AssetBundler::new();
bundler.add_file(PathBuf::from("a.js"), b"const a = 1;".to_vec());
bundler.add_file(PathBuf::from("b.js"), b"const b = 2;".to_vec());
let result = bundler
.bundle_files(&[PathBuf::from("b.js"), PathBuf::from("a.js")])
.unwrap();
let output = String::from_utf8(result).unwrap();
let b_pos = output.find("const b = 2;").unwrap();
let a_pos = output.find("const a = 1;").unwrap();
assert!(b_pos < a_pos);
}
#[test]
fn test_bundler_includes_separators() {
let mut bundler = AssetBundler::new();
bundler.add_file(PathBuf::from("test.js"), b"code".to_vec());
let result = bundler.bundle().unwrap();
let output = String::from_utf8(result).unwrap();
assert!(output.contains("/* test.js */"));
}
#[test]
fn test_bundle_config_creation() {
let config = BundleConfig::new(PathBuf::from("bundle.js"));
assert_eq!(config.output, PathBuf::from("bundle.js"));
assert!(config.files.is_empty());
assert!(!config.minify);
assert!(!config.source_map);
}
#[test]
fn test_bundle_config_add_file() {
let mut config = BundleConfig::new(PathBuf::from("bundle.js"));
config.add_file(PathBuf::from("a.js"));
config.add_file(PathBuf::from("b.js"));
assert_eq!(config.files.len(), 2);
}
#[test]
fn test_bundle_config_builder() {
let config = BundleConfig::new(PathBuf::from("bundle.js"))
.with_minify(true)
.with_source_map(true);
assert!(config.minify);
assert!(config.source_map);
}
#[test]
fn test_bundler_multiple_files() {
let mut bundler = AssetBundler::new();
for i in 0..5 {
bundler.add_file(
PathBuf::from(format!("file{}.js", i)),
format!("const x{} = {};", i, i).into_bytes(),
);
}
assert_eq!(bundler.len(), 5);
let result = bundler.bundle().unwrap();
assert!(!result.is_empty());
}
#[test]
fn test_bundler_empty_bundle() {
let bundler = AssetBundler::new();
let result = bundler.bundle().unwrap();
assert!(result.is_empty());
}
}