···338338339339 #[test]
340340 fn test_build_options_integration() {
341341- use crate::build::options::{AssetsOptions, BuildOptions};
341341+ use crate::build::options::BuildOptions;
342342343343 // Test that BuildOptions can configure the cache directory
344344 let custom_cache = PathBuf::from("/tmp/custom_maudit_cache");
345345 let build_options = BuildOptions {
346346- assets: AssetsOptions {
347347- image_cache_dir: custom_cache.clone(),
348348- ..Default::default()
349349- },
346346+ cache_dir: custom_cache.clone(),
350347 ..Default::default()
351348 };
352349353350 // Create cache with build options
354354- let cache = ImageCache::with_cache_dir(&build_options.assets.image_cache_dir);
351351+ let cache = ImageCache::with_cache_dir(&build_options.assets_cache_dir());
355352356353 // Verify it uses the configured directory
357357- assert_eq!(cache.get_cache_dir(), custom_cache);
354354+ assert_eq!(cache.get_cache_dir(), custom_cache.join("assets"));
358355 }
359356360357 #[test]
+11-13
crates/maudit/src/build.rs
···105105 // Create a directory for the output
106106 trace!(target: "build", "Setting up required directories...");
107107108108- // Determine build cache directory
109109- let build_cache_dir = options.assets.image_cache_dir.parent()
110110- .unwrap_or(Path::new("target/maudit_cache"))
111111- .to_path_buf();
108108+ // Use cache directory from options
109109+ let build_cache_dir = &options.cache_dir;
112110113113- // Load build state for incremental builds
114114- let mut build_state = if is_dev() {
115115- BuildState::load(&build_cache_dir).unwrap_or_else(|e| {
111111+ // Load build state for incremental builds (only if incremental is enabled)
112112+ let mut build_state = if options.incremental {
113113+ BuildState::load(build_cache_dir).unwrap_or_else(|e| {
116114 debug!(target: "build", "Failed to load build state: {}", e);
117115 BuildState::new()
118116 })
···121119 };
122120123121 // Determine if this is an incremental build
124124- let is_incremental = is_dev() && changed_files.is_some() && !build_state.asset_to_routes.is_empty();
122122+ let is_incremental = options.incremental && changed_files.is_some() && !build_state.asset_to_routes.is_empty();
125123126124 let routes_to_rebuild = if is_incremental {
127125 let changed = changed_files.unwrap();
···192190 };
193191194192 // Create the image cache early so it can be shared across routes
195195- let image_cache = ImageCache::with_cache_dir(&options.assets.image_cache_dir);
193193+ let image_cache = ImageCache::with_cache_dir(&options.assets_cache_dir());
196194 let _ = fs::create_dir_all(image_cache.get_cache_dir());
197195198196 // Create route_assets_options with the image cache
···660658 );
661659662660 // Store bundler inputs in build state for next incremental build
663663- if is_dev() {
661661+ if options.incremental {
664662 build_state.bundler_inputs = bundler_inputs
665663 .iter()
666664 .map(|input| input.import.clone())
···802800 info!(target: "SKIP_FORMAT", "{}", "");
803801 info!(target: "build", "{}", format!("Build completed in {}", format_elapsed_time(build_start.elapsed(), §ion_format_options)).bold());
804802805805- // Save build state for next incremental build
806806- if is_dev() {
807807- if let Err(e) = build_state.save(&build_cache_dir) {
803803+ // Save build state for next incremental build (only if incremental is enabled)
804804+ if options.incremental {
805805+ if let Err(e) = build_state.save(build_cache_dir) {
808806 warn!(target: "build", "Failed to save build state: {}", e);
809807 } else {
810808 debug!(target: "build", "Build state saved to {}", build_cache_dir.join("build_state.json").display());
+79-18
crates/maudit/src/build/options.rs
···3636/// assets: AssetsOptions {
3737/// assets_dir: "_assets".into(),
3838/// tailwind_binary_path: "./node_modules/.bin/tailwindcss".into(),
3939-/// image_cache_dir: ".cache/maudit/images".into(),
4039/// ..Default::default()
4140/// },
4241/// prefetch: PrefetchOptions {
···6059 ///
6160 /// At the speed Maudit operates at, not cleaning the output directory may offer a significant performance improvement at the cost of potentially serving stale content.
6261 pub clean_output_dir: bool,
6262+6363+ /// Whether to enable incremental builds.
6464+ ///
6565+ /// When enabled, Maudit tracks which assets are used by which routes and only rebuilds
6666+ /// routes affected by changed files. This can significantly speed up rebuilds when only
6767+ /// a few files have changed.
6868+ ///
6969+ /// Defaults to `true` in dev mode (`maudit dev`) and `false` in production builds.
7070+ pub incremental: bool,
7171+7272+ /// Directory for build cache storage (incremental build state, etc.).
7373+ ///
7474+ /// Defaults to `target/maudit_cache/{package_name}` where `{package_name}` is derived
7575+ /// from the current directory name.
7676+ pub cache_dir: PathBuf,
7777+7878+ /// Directory for caching processed assets (images, etc.).
7979+ ///
8080+ /// If `None`, defaults to `{cache_dir}/assets`.
8181+ pub assets_cache_dir: Option<PathBuf>,
63826483 pub assets: AssetsOptions,
6584···124143 hashing_strategy: self.assets.hashing_strategy,
125144 }
126145 }
146146+147147+ /// Returns the directory for caching processed assets (images, etc.).
148148+ /// Uses `assets_cache_dir` if set, otherwise defaults to `{cache_dir}/assets`.
149149+ pub fn assets_cache_dir(&self) -> PathBuf {
150150+ self.assets_cache_dir
151151+ .clone()
152152+ .unwrap_or_else(|| self.cache_dir.join("assets"))
153153+ }
127154}
128155129156#[derive(Clone)]
···139166 /// Note that this value is not automatically joined with the `output_dir` in `BuildOptions`. Use [`BuildOptions::route_assets_options()`] to get a `RouteAssetsOptions` with the correct final path.
140167 pub assets_dir: PathBuf,
141168142142- /// Directory to use for image cache storage.
143143- /// Defaults to `target/maudit_cache/images`.
144144- ///
145145- /// This cache is used to store processed images and their placeholders to speed up subsequent builds.
146146- pub image_cache_dir: PathBuf,
147147-148169 /// Strategy to use when hashing assets for fingerprinting.
149170 ///
150171 /// Defaults to [`AssetHashingStrategy::Precise`] in production builds, and [`AssetHashingStrategy::FastImprecise`] in development builds. Note that this means that the cache isn't shared between dev and prod builds by default, if you have a lot of assets you may want to set this to the same value in both environments.
···164185 Self {
165186 tailwind_binary_path: "tailwindcss".into(),
166187 assets_dir: "_maudit".into(),
167167- image_cache_dir: {
168168- find_target_dir()
169169- .unwrap_or_else(|_| PathBuf::from("target"))
170170- .join("maudit_cache/images")
171171- },
172188 hashing_strategy: if is_dev() {
173189 AssetHashingStrategy::FastImprecise
174190 } else {
···196212/// ```
197213impl Default for BuildOptions {
198214 fn default() -> Self {
215215+ let site_name = get_site_name();
216216+ let cache_dir = find_target_dir()
217217+ .unwrap_or_else(|_| PathBuf::from("target"))
218218+ .join("maudit_cache")
219219+ .join(&site_name);
220220+199221 Self {
200222 base_url: None,
201223 output_dir: "dist".into(),
202224 static_dir: "static".into(),
203225 clean_output_dir: true,
226226+ incremental: is_dev(),
227227+ cache_dir,
228228+ assets_cache_dir: None,
204229 prefetch: PrefetchOptions::default(),
205230 assets: AssetsOptions::default(),
206231 sitemap: SitemapOptions::default(),
···208233 }
209234}
210235236236+/// Get the site name for cache directory purposes.
237237+///
238238+/// Tries to read the package name from Cargo.toml in the current directory,
239239+/// falling back to the current directory name.
240240+fn get_site_name() -> String {
241241+ // Try to read package name from Cargo.toml
242242+ if let Ok(cargo_toml) = fs::read_to_string("Cargo.toml") {
243243+ // Simple parsing - look for name = "..." in [package] section
244244+ let mut in_package = false;
245245+ for line in cargo_toml.lines() {
246246+ let trimmed = line.trim();
247247+ if trimmed == "[package]" {
248248+ in_package = true;
249249+ } else if trimmed.starts_with('[') {
250250+ in_package = false;
251251+ } else if in_package && trimmed.starts_with("name") {
252252+ // Parse: name = "package-name" or name = 'package-name'
253253+ if let Some(eq_pos) = trimmed.find('=') {
254254+ let value = trimmed[eq_pos + 1..].trim();
255255+ let value = value.trim_matches('"').trim_matches('\'');
256256+ if !value.is_empty() {
257257+ return value.to_string();
258258+ }
259259+ }
260260+ }
261261+ }
262262+ }
263263+264264+ // Fallback to current directory name
265265+ std::env::current_dir()
266266+ .ok()
267267+ .and_then(|p| p.file_name().map(|s| s.to_string_lossy().to_string()))
268268+ .unwrap_or_else(|| "default".to_string())
269269+}
270270+211271/// Find the target directory using multiple strategies
212272///
213273/// This function tries multiple approaches to locate the target directory:
···238298 let cargo_toml = current.join("Cargo.toml");
239299 if cargo_toml.exists()
240300 && let Ok(content) = fs::read_to_string(&cargo_toml)
241241- && content.contains("[workspace]") {
242242- let workspace_target = current.join("target");
243243- if workspace_target.exists() {
244244- return Ok(workspace_target);
245245- }
246246- }
301301+ && content.contains("[workspace]")
302302+ {
303303+ let workspace_target = current.join("target");
304304+ if workspace_target.exists() {
305305+ return Ok(workspace_target);
306306+ }
307307+ }
247308248309 // Move up to parent directory
249310 if !current.pop() {