manganis_cli_support/
manifest.rs1pub use railwind::warning::Warning as TailwindWarning;
2use rustc_hash::FxHashSet;
3use std::{
4 fmt::Write,
5 path::{Path, PathBuf},
6};
7
8use cargo_lock::{
9 dependency::{self, graph::NodeIndex},
10 Lockfile,
11};
12use manganis_common::{
13 cache::{asset_cache_dir, package_identifier, push_package_identifier},
14 AssetManifest, AssetType, PackageAssets,
15};
16use petgraph::visit::EdgeRef;
17use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
18
19use crate::{
20 cache::{current_cargo_toml, lock_path},
21 file::process_file,
22};
23
24pub trait AssetManifestExt {
26 fn load(bin: Option<&str>) -> Self;
28
29 fn load_from_path(bin: Option<&str>, cargo_toml: PathBuf, cargo_lock: PathBuf) -> Self;
31
32 fn copy_static_assets_to(&self, location: impl Into<PathBuf>) -> anyhow::Result<()>;
34
35 fn collect_tailwind_css(
37 &self,
38 include_preflight: bool,
39 warnings: &mut Vec<TailwindWarning>,
40 ) -> String;
41}
42
43impl AssetManifestExt for AssetManifest {
44 fn load(bin: Option<&str>) -> Self {
45 let lock_path = lock_path();
46 let cargo_toml = current_cargo_toml();
47 Self::load_from_path(bin, cargo_toml, lock_path)
48 }
49
50 fn load_from_path(bin: Option<&str>, cargo_toml: PathBuf, cargo_lock: PathBuf) -> Self {
51 let lockfile = Lockfile::load(cargo_lock).unwrap();
52
53 let cargo_toml = cargo_toml::Manifest::from_path(cargo_toml).unwrap();
54 let this_package = cargo_toml.package.unwrap();
55
56 let mut all_assets = Vec::new();
57 let cache_dir = asset_cache_dir();
58 let tree = dependency::tree::Tree::new(&lockfile).unwrap();
59
60 let Some(this_package_lock) = tree.roots().into_iter().find(|&p| {
61 let package = tree.graph().node_weight(p).unwrap();
62 package.name.as_str() == this_package.name
63 }) else {
64 tracing::error!("Manganis: Failed to find this package in the lock file");
65 return Self::default();
66 };
67
68 collect_dependencies(&tree, this_package_lock, bin, &cache_dir, &mut all_assets);
69
70 Self::new(all_assets)
71 }
72
73 fn copy_static_assets_to(&self, location: impl Into<PathBuf>) -> anyhow::Result<()> {
74 let location = location.into();
75 match std::fs::create_dir_all(&location) {
76 Ok(_) => {}
77 Err(err) => {
78 tracing::error!("Failed to create directory for static assets: {}", err);
79 return Err(err.into());
80 }
81 }
82 self.packages().par_iter().try_for_each(|package| {
83 tracing::trace!("Copying static assets for package {}", package.package());
84 package.assets().par_iter().try_for_each(|asset| {
85 if let AssetType::File(file_asset) = asset {
86 tracing::info!("Optimizing and bundling {}", file_asset);
87 tracing::trace!("Copying asset from {:?} to {:?}", file_asset, location);
88 match process_file(file_asset, &location) {
89 Ok(_) => {}
90 Err(err) => {
91 tracing::error!("Failed to copy static asset: {}", err);
92 return Err(err);
93 }
94 }
95 }
96 Ok::<(), anyhow::Error>(())
97 })?;
98 Ok::<(), anyhow::Error>(())
99 })?;
100
101 Ok(())
102 }
103
104 fn collect_tailwind_css(
105 self: &AssetManifest,
106 include_preflight: bool,
107 warnings: &mut Vec<TailwindWarning>,
108 ) -> String {
109 let mut all_classes = String::new();
110
111 for package in self.packages() {
112 for asset in package.assets() {
113 if let AssetType::Tailwind(classes) = asset {
114 all_classes.push_str(classes.classes());
115 all_classes.push(' ');
116 }
117 }
118 }
119
120 let source = railwind::Source::String(all_classes, railwind::CollectionOptions::String);
121
122 let css = railwind::parse_to_string(source, include_preflight, warnings);
123
124 crate::file::minify_css(&css)
125 }
126}
127
128fn collect_dependencies(
129 tree: &cargo_lock::dependency::tree::Tree,
130 root_package_id: NodeIndex,
131 bin: Option<&str>,
132 cache_dir: &Path,
133 all_assets: &mut Vec<PackageAssets>,
134) {
135 let mut packages = FxHashSet::default();
137 match cache_dir.read_dir() {
138 Ok(read_dir) => {
139 for path in read_dir.flatten() {
140 if path.file_type().unwrap().is_dir() {
141 let file_name = path.file_name();
142 let package_name = file_name.to_string_lossy();
143 packages.insert(package_name.to_string());
144 }
145 }
146 }
147 Err(err) => {
148 tracing::error!("Failed to read asset cache directory: {}", err);
149 }
150 }
151 tracing::trace!(
152 "Found packages with assets: {:?}",
153 packages.iter().cloned().collect::<Vec<_>>().join(", ")
154 );
155
156 let mut packages_to_visit = vec![root_package_id];
157 let mut dependency_path = PathBuf::new();
158 while let Some(package_id) = packages_to_visit.pop() {
159 let package = tree.graph().node_weight(package_id).unwrap();
160 let identifier = package_identifier(
162 package.name.as_str(),
163 bin.filter(|_| package_id == root_package_id),
164 &package.version,
165 );
166 if !packages.contains(&identifier) {
167 continue;
168 }
169
170 dependency_path.clear();
172 dependency_path.push(cache_dir);
173 let os_string = dependency_path.as_mut_os_string();
174 os_string.write_char(std::path::MAIN_SEPARATOR).unwrap();
175 push_package_identifier(
176 package.name.as_str(),
177 bin.filter(|_| package_id == root_package_id),
178 &package.version,
179 os_string,
180 );
181 tracing::trace!("Looking for assets in {}", dependency_path.display());
182 dependency_path.push("assets.toml");
183 if dependency_path.exists() {
184 match std::fs::read_to_string(&dependency_path) {
185 Ok(contents) => {
186 match toml::from_str(&contents) {
187 Ok(package_assets) => {
188 all_assets.push(package_assets);
189 }
190 Err(err) => {
191 tracing::error!(
192 "Failed to parse asset manifest for dependency: {}",
193 err
194 );
195 }
196 };
197 }
198 Err(err) => {
199 tracing::error!("Failed to read asset manifest for dependency: {}", err);
200 }
201 }
202 }
203
204 let dependencies = tree.graph().edges(package_id);
206 for dependency in dependencies {
207 let dependency_index = dependency.target();
208 packages_to_visit.push(dependency_index);
209 }
210 }
211}