pub use railwind::warning::Warning as TailwindWarning;
use rustc_hash::FxHashSet;
use std::{
fmt::Write,
path::{Path, PathBuf},
};
use cargo_lock::{
dependency::{self, graph::NodeIndex},
Lockfile,
};
use manganis_common::{
cache::{asset_cache_dir, package_identifier, push_package_identifier},
AssetManifest, AssetType, PackageAssets,
};
use petgraph::visit::EdgeRef;
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
use crate::{
cache::{current_cargo_toml, lock_path},
file::process_file,
};
pub trait AssetManifestExt {
fn load(bin: Option<&str>) -> Self;
fn load_from_path(bin: Option<&str>, cargo_toml: PathBuf, cargo_lock: PathBuf) -> Self;
fn copy_static_assets_to(&self, location: impl Into<PathBuf>) -> anyhow::Result<()>;
fn collect_tailwind_css(
&self,
include_preflight: bool,
warnings: &mut Vec<TailwindWarning>,
) -> String;
}
impl AssetManifestExt for AssetManifest {
fn load(bin: Option<&str>) -> Self {
let lock_path = lock_path();
let cargo_toml = current_cargo_toml();
Self::load_from_path(bin, cargo_toml, lock_path)
}
fn load_from_path(bin: Option<&str>, cargo_toml: PathBuf, cargo_lock: PathBuf) -> Self {
let lockfile = Lockfile::load(cargo_lock).unwrap();
let cargo_toml = cargo_toml::Manifest::from_path(cargo_toml).unwrap();
let this_package = cargo_toml.package.unwrap();
let mut all_assets = Vec::new();
let cache_dir = asset_cache_dir();
let tree = dependency::tree::Tree::new(&lockfile).unwrap();
let Some(this_package_lock) = tree.roots().into_iter().find(|&p| {
let package = tree.graph().node_weight(p).unwrap();
package.name.as_str() == this_package.name
}) else {
tracing::error!("Manganis: Failed to find this package in the lock file");
return Self::default();
};
collect_dependencies(&tree, this_package_lock, bin, &cache_dir, &mut all_assets);
Self::new(all_assets)
}
fn copy_static_assets_to(&self, location: impl Into<PathBuf>) -> anyhow::Result<()> {
let location = location.into();
match std::fs::create_dir_all(&location) {
Ok(_) => {}
Err(err) => {
tracing::error!("Failed to create directory for static assets: {}", err);
return Err(err.into());
}
}
self.packages().par_iter().try_for_each(|package| {
tracing::trace!("Copying static assets for package {}", package.package());
package.assets().par_iter().try_for_each(|asset| {
if let AssetType::File(file_asset) = asset {
tracing::info!("Optimizing and bundling {}", file_asset);
tracing::trace!("Copying asset from {:?} to {:?}", file_asset, location);
match process_file(file_asset, &location) {
Ok(_) => {}
Err(err) => {
tracing::error!("Failed to copy static asset: {}", err);
return Err(err);
}
}
}
Ok::<(), anyhow::Error>(())
})?;
Ok::<(), anyhow::Error>(())
})?;
Ok(())
}
fn collect_tailwind_css(
self: &AssetManifest,
include_preflight: bool,
warnings: &mut Vec<TailwindWarning>,
) -> String {
let mut all_classes = String::new();
for package in self.packages() {
for asset in package.assets() {
if let AssetType::Tailwind(classes) = asset {
all_classes.push_str(classes.classes());
all_classes.push(' ');
}
}
}
let source = railwind::Source::String(all_classes, railwind::CollectionOptions::String);
let css = railwind::parse_to_string(source, include_preflight, warnings);
crate::file::minify_css(&css)
}
}
fn collect_dependencies(
tree: &cargo_lock::dependency::tree::Tree,
root_package_id: NodeIndex,
bin: Option<&str>,
cache_dir: &Path,
all_assets: &mut Vec<PackageAssets>,
) {
let mut packages = FxHashSet::default();
match cache_dir.read_dir() {
Ok(read_dir) => {
for path in read_dir.flatten() {
if path.file_type().unwrap().is_dir() {
let file_name = path.file_name();
let package_name = file_name.to_string_lossy();
packages.insert(package_name.to_string());
}
}
}
Err(err) => {
tracing::error!("Failed to read asset cache directory: {}", err);
}
}
tracing::trace!(
"Found packages with assets: {:?}",
packages.iter().cloned().collect::<Vec<_>>().join(", ")
);
let mut packages_to_visit = vec![root_package_id];
let mut dependency_path = PathBuf::new();
while let Some(package_id) = packages_to_visit.pop() {
let package = tree.graph().node_weight(package_id).unwrap();
let identifier = package_identifier(
package.name.as_str(),
bin.filter(|_| package_id == root_package_id),
&package.version,
);
if !packages.contains(&identifier) {
continue;
}
dependency_path.clear();
dependency_path.push(cache_dir);
let os_string = dependency_path.as_mut_os_string();
os_string.write_char(std::path::MAIN_SEPARATOR).unwrap();
push_package_identifier(
package.name.as_str(),
bin.filter(|_| package_id == root_package_id),
&package.version,
os_string,
);
tracing::trace!("Looking for assets in {}", dependency_path.display());
dependency_path.push("assets.toml");
if dependency_path.exists() {
match std::fs::read_to_string(&dependency_path) {
Ok(contents) => {
match toml::from_str(&contents) {
Ok(package_assets) => {
all_assets.push(package_assets);
}
Err(err) => {
tracing::error!(
"Failed to parse asset manifest for dependency: {}",
err
);
}
};
}
Err(err) => {
tracing::error!("Failed to read asset manifest for dependency: {}", err);
}
}
}
let dependencies = tree.graph().edges(package_id);
for dependency in dependencies {
let dependency_index = dependency.target();
packages_to_visit.push(dependency_index);
}
}
}