···1414 self, HashAssetType, HashConfig, PrefetchPlugin, RouteAssets, Script, TailwindPlugin,
1515 calculate_hash, image_cache::ImageCache, prefetch,
1616 },
1717- build::{images::process_image, options::PrefetchStrategy, state::{BuildState, RouteIdentifier}},
1717+ build::{
1818+ images::process_image,
1919+ options::PrefetchStrategy,
2020+ state::{BuildState, RouteIdentifier},
2121+ },
1822 content::ContentSources,
1923 is_dev,
2024 logging::print_title,
···4852 Some(set) => set.contains(route_id),
4953 None => true, // Full build
5054 };
5151-5555+5256 if !result {
5357 trace!(target: "build", "Skipping route {:?} (not in rebuild set)", route_id);
5458 }
5555-5959+5660 result
5761}
5862···6872 build_state.track_asset(canonical, route_id.clone());
6973 }
7074 }
7171-7575+7276 // Track scripts
7377 for script in &route_assets.scripts {
7478 if let Ok(canonical) = script.path().canonicalize() {
7579 build_state.track_asset(canonical, route_id.clone());
7680 }
7781 }
7878-8282+7983 // Track styles
8084 for style in &route_assets.styles {
8185 if let Ok(canonical) = style.path().canonicalize() {
···123127 debug!(target: "build", "options.incremental: {}, changed_files.is_some(): {}", options.incremental, changed_files.is_some());
124128125129 // Determine if this is an incremental build
126126- let is_incremental = options.incremental && changed_files.is_some() && !build_state.asset_to_routes.is_empty();
127127-130130+ let is_incremental =
131131+ options.incremental && changed_files.is_some() && !build_state.asset_to_routes.is_empty();
132132+128133 let routes_to_rebuild = if is_incremental {
129134 let changed = changed_files.unwrap();
130135 info!(target: "build", "Incremental build: {} files changed", changed.len());
131136 info!(target: "build", "Changed files: {:?}", changed);
132132-137137+133138 info!(target: "build", "Build state has {} asset mappings", build_state.asset_to_routes.len());
134134-139139+135140 let affected = build_state.get_affected_routes(changed);
136141 info!(target: "build", "Rebuilding {} affected routes", affected.len());
137142 info!(target: "build", "Affected routes: {:?}", affected);
138138-143143+139144 Some(affected)
140145 } else {
141146 if changed_files.is_some() {
···145150 build_state.clear();
146151 None
147152 };
148148-153153+149154 // Check if we should rebundle during incremental builds
150155 // Rebundle if a changed file is either:
151156 // 1. A direct bundler input (entry point)
152152- // 2. A transitive dependency tracked in asset_to_routes (JS/CSS/TS files)
157157+ // 2. A transitive dependency tracked in asset_to_routes (any file the bundler processed)
153158 let should_rebundle = if is_incremental && !build_state.bundler_inputs.is_empty() {
154159 let changed = changed_files.unwrap();
155160 let should = changed.iter().any(|changed_file| {
···157162 let is_bundler_input = build_state.bundler_inputs.iter().any(|bundler_input| {
158163 if let (Ok(changed_canonical), Ok(bundler_canonical)) = (
159164 changed_file.canonicalize(),
160160- PathBuf::from(bundler_input).canonicalize()
165165+ PathBuf::from(bundler_input).canonicalize(),
161166 ) {
162167 changed_canonical == bundler_canonical
163168 } else {
164169 false
165170 }
166171 });
167167-172172+168173 if is_bundler_input {
169174 return true;
170175 }
171171-172172- // Check if it's a transitive dependency (JS/CSS/TS file in asset_to_routes)
173173- if let Some(ext) = changed_file.extension().and_then(|e| e.to_str()) {
174174- let is_bundleable = matches!(ext.to_lowercase().as_str(), "js" | "ts" | "jsx" | "tsx" | "css");
175175- if is_bundleable
176176- && let Ok(canonical) = changed_file.canonicalize() {
177177- return build_state.asset_to_routes.contains_key(&canonical);
178178- }
176176+177177+ // Check if it's a transitive dependency tracked by the bundler
178178+ // (JS/TS modules, CSS files, or assets like images/fonts referenced via url())
179179+ if let Ok(canonical) = changed_file.canonicalize() {
180180+ return build_state.asset_to_routes.contains_key(&canonical);
179181 }
180180-182182+181183 false
182184 });
183183-185185+184186 if should {
185187 info!(target: "build", "Rebundling needed: changed file affects bundled assets");
186188 } else {
187189 info!(target: "build", "Skipping bundler: no changed files affect bundled assets");
188190 }
189189-191191+190192 should
191193 } else {
192194 // Not incremental or no previous bundler inputs
···320322 // Static base route
321323 if base_params.is_empty() {
322324 let route_id = RouteIdentifier::base(base_path.clone(), None);
323323-325325+324326 // Check if we need to rebuild this route
325327 if should_rebuild_route(&route_id, &routes_to_rebuild) {
326328 let mut route_assets = RouteAssets::with_default_assets(
···396398397399 // Build all pages for this route
398400 for page in pages {
399399- let route_id = RouteIdentifier::base(
400400- base_path.clone(),
401401- Some(page.0.0.clone()),
402402- );
403403-401401+ let route_id =
402402+ RouteIdentifier::base(base_path.clone(), Some(page.0.0.clone()));
403403+404404 // Check if we need to rebuild this specific page
405405 if should_rebuild_route(&route_id, &routes_to_rebuild) {
406406 let page_start = Instant::now();
···458458459459 if variant_params.is_empty() {
460460 // Static variant
461461- let route_id = RouteIdentifier::variant(
462462- variant_id.clone(),
463463- variant_path.clone(),
464464- None,
465465- );
466466-461461+ let route_id =
462462+ RouteIdentifier::variant(variant_id.clone(), variant_path.clone(), None);
463463+467464 // Check if we need to rebuild this variant
468465 if should_rebuild_route(&route_id, &routes_to_rebuild) {
469466 let mut route_assets = RouteAssets::with_default_assets(
···475472476473 let params = PageParams::default();
477474 let url = cached_route.variant_url(¶ms, &variant_id)?;
478478- let file_path =
479479- cached_route.variant_file_path(¶ms, &options.output_dir, &variant_id)?;
475475+ let file_path = cached_route.variant_file_path(
476476+ ¶ms,
477477+ &options.output_dir,
478478+ &variant_id,
479479+ )?;
480480481481 let result = route.build(&mut PageContext::from_static_route(
482482 content_sources,
···543543 variant_path.clone(),
544544 Some(page.0.0.clone()),
545545 );
546546-546546+547547 // Check if we need to rebuild this specific variant page
548548 if should_rebuild_route(&route_id, &routes_to_rebuild) {
549549 let variant_page_start = Instant::now();
···608608 fs::create_dir_all(&route_assets_options.output_assets_dir)?;
609609 }
610610611611- if !build_pages_styles.is_empty() || !build_pages_scripts.is_empty() || (is_incremental && should_rebundle) {
611611+ if !build_pages_styles.is_empty()
612612+ || !build_pages_scripts.is_empty()
613613+ || (is_incremental && should_rebundle)
614614+ {
612615 let assets_start = Instant::now();
613616 print_title("generating assets");
614617···645648 // to ensure we bundle all assets, not just from rebuilt routes
646649 if is_incremental && !build_state.bundler_inputs.is_empty() {
647650 debug!(target: "bundling", "Merging with {} previous bundler inputs", build_state.bundler_inputs.len());
648648-651651+649652 let current_imports: FxHashSet<String> = bundler_inputs
650653 .iter()
651654 .map(|input| input.import.clone())
652655 .collect();
653653-656656+654657 // Add previous inputs that aren't in the current set
655658 for prev_input in &build_state.bundler_inputs {
656659 if !current_imports.contains(prev_input) {
···687690688691 if !bundler_inputs.is_empty() {
689692 let mut module_types_hashmap = FxHashMap::default();
693693+ // Fonts
690694 module_types_hashmap.insert("woff".to_string(), ModuleType::Asset);
691695 module_types_hashmap.insert("woff2".to_string(), ModuleType::Asset);
696696+ module_types_hashmap.insert("ttf".to_string(), ModuleType::Asset);
697697+ module_types_hashmap.insert("otf".to_string(), ModuleType::Asset);
698698+ module_types_hashmap.insert("eot".to_string(), ModuleType::Asset);
699699+ // Images
700700+ module_types_hashmap.insert("png".to_string(), ModuleType::Asset);
701701+ module_types_hashmap.insert("jpg".to_string(), ModuleType::Asset);
702702+ module_types_hashmap.insert("jpeg".to_string(), ModuleType::Asset);
703703+ module_types_hashmap.insert("gif".to_string(), ModuleType::Asset);
704704+ module_types_hashmap.insert("svg".to_string(), ModuleType::Asset);
705705+ module_types_hashmap.insert("webp".to_string(), ModuleType::Asset);
706706+ module_types_hashmap.insert("avif".to_string(), ModuleType::Asset);
707707+ module_types_hashmap.insert("ico".to_string(), ModuleType::Asset);
692708693709 let mut bundler = Bundler::with_plugins(
694710 BundlerOptions {
···726742727743 // Track transitive dependencies from bundler output
728744 // For each chunk, map all its modules to the routes that use the entry point
745745+ // For assets (images, fonts via CSS url()), map them to all routes using any entry point
729746 if options.incremental {
747747+ // First, collect all routes that use any bundler entry point
748748+ let mut all_bundler_routes: FxHashSet<RouteIdentifier> = FxHashSet::default();
749749+730750 for output in &result.assets {
731751 if let Output::Chunk(chunk) = output {
732752 // Get the entry point for this chunk
···734754 // Try to find routes using this entry point
735755 let entry_path = PathBuf::from(facade_module_id.as_str());
736756 let canonical_entry = entry_path.canonicalize().ok();
737737-757757+738758 // Look up routes for this entry point
739759 let routes = canonical_entry
740760 .as_ref()
741761 .and_then(|p| build_state.asset_to_routes.get(p))
742762 .cloned();
743743-763763+744764 if let Some(routes) = routes {
765765+ // Collect routes for asset tracking later
766766+ all_bundler_routes.extend(routes.iter().cloned());
767767+745768 // Register all modules in this chunk as dependencies for those routes
746769 let mut transitive_count = 0;
747770 for module_id in &chunk.module_ids {
···750773 // Skip the entry point itself (already tracked)
751774 if Some(&canonical_module) != canonical_entry.as_ref() {
752775 for route in &routes {
753753- build_state.track_asset(canonical_module.clone(), route.clone());
776776+ build_state.track_asset(
777777+ canonical_module.clone(),
778778+ route.clone(),
779779+ );
754780 }
755781 transitive_count += 1;
756782 }
···761787 }
762788 }
763789 }
790790+ }
791791+ }
792792+793793+ // Now track Output::Asset items (images, fonts, etc. referenced via CSS url() or JS imports)
794794+ // These are mapped to all routes that use any bundler entry point
795795+ if !all_bundler_routes.is_empty() {
796796+ let mut asset_count = 0;
797797+ for output in &result.assets {
798798+ if let Output::Asset(asset) = output {
799799+ for original_file in &asset.original_file_names {
800800+ let asset_path = PathBuf::from(original_file);
801801+ if let Ok(canonical_asset) = asset_path.canonicalize() {
802802+ for route in &all_bundler_routes {
803803+ build_state.track_asset(
804804+ canonical_asset.clone(),
805805+ route.clone(),
806806+ );
807807+ }
808808+ asset_count += 1;
809809+ }
810810+ }
811811+ }
812812+ }
813813+ if asset_count > 0 {
814814+ debug!(target: "build", "Tracked {} bundler assets for {} routes", asset_count, all_bundler_routes.len());
764815 }
765816 }
766817 }
+10-27
crates/maudit/src/build/options.rs
···235235236236/// Get the site name for cache directory purposes.
237237///
238238-/// Tries to read the package name from Cargo.toml in the current directory,
238238+/// Uses the current executable's name (which matches the package/binary name),
239239/// falling back to the current directory name.
240240fn 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()
241241+ // Get the binary name from the current executable
242242+ std::env::current_exe()
266243 .ok()
267244 .and_then(|p| p.file_name().map(|s| s.to_string_lossy().to_string()))
268268- .unwrap_or_else(|| "default".to_string())
245245+ .unwrap_or_else(|| {
246246+ // Fallback to current directory name
247247+ std::env::current_dir()
248248+ .ok()
249249+ .and_then(|p| p.file_name().map(|s| s.to_string_lossy().to_string()))
250250+ .unwrap_or_else(|| "default".to_string())
251251+ })
269252}
270253271254/// Find the target directory using multiple strategies