manganis_cli_support/
manifest.rs

1pub 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
24/// An extension trait CLI support for the asset manifest
25pub trait AssetManifestExt {
26    /// Loads the asset manifest for the current working directory
27    fn load(bin: Option<&str>) -> Self;
28
29    /// Loads the asset manifest from the cargo toml and lock file
30    fn load_from_path(bin: Option<&str>, cargo_toml: PathBuf, cargo_lock: PathBuf) -> Self;
31
32    /// Copies all static assets to the given location
33    fn copy_static_assets_to(&self, location: impl Into<PathBuf>) -> anyhow::Result<()>;
34
35    /// Collects all tailwind classes from all assets and outputs the CSS file
36    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    // First find any assets that do have assets. The vast majority of packages will not have any so we can rule them out quickly with a hashset before touching the filesystem
136    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        // First make sure this package has assets
161        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        // Add the assets for this dependency
171        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        // Then recurse into its dependencies
205        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}