@@ -19,7 +19,7 @@ use crate::{
1919 options:: PrefetchStrategy ,
2020 state:: { BuildState , RouteIdentifier } ,
2121 } ,
22- content:: ContentSources ,
22+ content:: { ContentSources , finish_tracking_content_files , start_tracking_content_files } ,
2323 is_dev,
2424 logging:: print_title,
2525 route:: { CachedRoute , DynamicRouteContext , FullRoute , InternalRoute , PageContext , PageParams } ,
@@ -142,6 +142,29 @@ fn track_route_source_file(
142142 build_state. track_source_file ( source_path, route_id. clone ( ) ) ;
143143}
144144
145+ /// Helper to track content files accessed during page rendering.
146+ /// Only performs work when incremental builds are enabled and route_id is provided.
147+ /// This should be called after `finish_tracking_content_files()` to get the accessed files.
148+ fn track_route_content_files (
149+ build_state : & mut BuildState ,
150+ route_id : Option < & RouteIdentifier > ,
151+ accessed_files : Option < FxHashSet < PathBuf > > ,
152+ ) {
153+ // Skip tracking entirely when route_id is not provided (incremental disabled)
154+ let Some ( route_id) = route_id else {
155+ return ;
156+ } ;
157+
158+ // Skip if no files were tracked
159+ let Some ( files) = accessed_files else {
160+ return ;
161+ } ;
162+
163+ for file_path in files {
164+ build_state. track_content_file ( file_path, route_id. clone ( ) ) ;
165+ }
166+ }
167+
145168pub fn execute_build (
146169 routes : & [ & dyn FullRoute ] ,
147170 content_sources : & mut ContentSources ,
@@ -177,7 +200,7 @@ pub async fn build(
177200 BuildState :: new ( )
178201 } ;
179202
180- debug ! ( target: "build" , "Loaded build state with {} asset mappings, {} source mappings" , build_state. asset_to_routes. len( ) , build_state. source_to_routes. len( ) ) ;
203+ debug ! ( target: "build" , "Loaded build state with {} asset mappings, {} source mappings, {} content file mappings " , build_state. asset_to_routes. len( ) , build_state. source_to_routes. len ( ) , build_state . content_file_to_routes . len( ) ) ;
181204 debug ! ( target: "build" , "options.incremental: {}, changed_files.is_some(): {}" , options. incremental, changed_files. is_some( ) ) ;
182205
183206 // Determine if this is an incremental build
@@ -191,7 +214,7 @@ pub async fn build(
191214 info ! ( target: "build" , "Incremental build: {} files changed" , changed. len( ) ) ;
192215 info ! ( target: "build" , "Changed files: {:?}" , changed) ;
193216
194- info ! ( target: "build" , "Build state has {} asset mappings, {} source mappings" , build_state. asset_to_routes. len( ) , build_state. source_to_routes. len( ) ) ;
217+ info ! ( target: "build" , "Build state has {} asset mappings, {} source mappings, {} content file mappings " , build_state. asset_to_routes. len( ) , build_state. source_to_routes. len ( ) , build_state . content_file_to_routes . len( ) ) ;
195218
196219 match build_state. get_affected_routes ( changed) {
197220 Some ( affected) => {
@@ -287,18 +310,86 @@ pub async fn build(
287310
288311 let content_sources_start = Instant :: now ( ) ;
289312 print_title ( "initializing content sources" ) ;
290- content_sources. sources_mut ( ) . iter_mut ( ) . for_each ( |source| {
291- let source_start = Instant :: now ( ) ;
292- source. init ( ) ;
293313
294- info ! ( target: "content" , "{} initialized in {}" , source. get_name( ) , format_elapsed_time( source_start. elapsed( ) , & FormatElapsedTimeOptions :: default ( ) ) ) ;
295- } ) ;
314+ // Determine which content sources need to be initialized
315+ // For incremental builds, only re-init sources whose files have changed
316+ let sources_to_init: Option < FxHashSet < String > > = if is_incremental {
317+ if let Some ( changed) = changed_files {
318+ build_state. get_affected_content_sources ( changed)
319+ } else {
320+ None // Full init
321+ }
322+ } else {
323+ None // Full init
324+ } ;
325+
326+ // Initialize content sources (all or selective)
327+ let initialized_sources: Vec < String > = match & sources_to_init {
328+ Some ( source_names) if !source_names. is_empty ( ) => {
329+ info ! ( target: "content" , "Selectively initializing {} content source(s): {:?}" , source_names. len( ) , source_names) ;
330+
331+ // Clear mappings for sources being re-initialized before init
332+ build_state. clear_content_mappings_for_sources ( source_names) ;
333+
334+ // Initialize only the affected sources
335+ let mut initialized = Vec :: new ( ) ;
336+ for source in content_sources. sources_mut ( ) {
337+ if source_names. contains ( source. get_name ( ) ) {
338+ let source_start = Instant :: now ( ) ;
339+ source. init ( ) ;
340+ info ! ( target: "content" , "{} initialized in {}" , source. get_name( ) , format_elapsed_time( source_start. elapsed( ) , & FormatElapsedTimeOptions :: default ( ) ) ) ;
341+ initialized. push ( source. get_name ( ) . to_string ( ) ) ;
342+ } else {
343+ info ! ( target: "content" , "{} (unchanged, skipped)" , source. get_name( ) ) ;
344+ }
345+ }
346+ initialized
347+ }
348+ Some ( _) => {
349+ // Empty set means no content files changed, skip all initialization
350+ info ! ( target: "content" , "No content files changed, skipping content source initialization" ) ;
351+ Vec :: new ( )
352+ }
353+ None => {
354+ // Full initialization (first build, unknown files, or non-incremental)
355+ info ! ( target: "content" , "Initializing all content sources" ) ;
356+
357+ // Clear all content mappings for full init
358+ build_state. clear_content_file_mappings ( ) ;
359+ build_state. content_file_to_source . clear ( ) ;
360+
361+ let mut initialized = Vec :: new ( ) ;
362+ for source in content_sources. sources_mut ( ) {
363+ let source_start = Instant :: now ( ) ;
364+ source. init ( ) ;
365+ info ! ( target: "content" , "{} initialized in {}" , source. get_name( ) , format_elapsed_time( source_start. elapsed( ) , & FormatElapsedTimeOptions :: default ( ) ) ) ;
366+ initialized. push ( source. get_name ( ) . to_string ( ) ) ;
367+ }
368+ initialized
369+ }
370+ } ;
371+
372+ // Track file->source mappings for all initialized sources
373+ for source in content_sources. sources ( ) {
374+ if initialized_sources. contains ( & source. get_name ( ) . to_string ( ) ) {
375+ let source_name = source. get_name ( ) . to_string ( ) ;
376+ for file_path in source. get_entry_file_paths ( ) {
377+ build_state. track_content_file_source ( file_path, source_name. clone ( ) ) ;
378+ }
379+ }
380+ }
296381
297382 info ! ( target: "content" , "{}" , format!( "Content sources initialized in {}" , format_elapsed_time(
298383 content_sources_start. elapsed( ) ,
299384 & FormatElapsedTimeOptions :: default ( ) ,
300385 ) ) . bold( ) ) ;
301386
387+ // Clear content file->routes mappings for routes being rebuilt
388+ // (so they get fresh tracking during this build)
389+ if let Some ( ref routes) = routes_to_rebuild {
390+ build_state. clear_content_file_mappings_for_routes ( routes) ;
391+ }
392+
302393 print_title ( "generating pages" ) ;
303394 let pages_start = Instant :: now ( ) ;
304395
@@ -405,6 +496,11 @@ pub async fn build(
405496 let params = PageParams :: default ( ) ;
406497 let url = cached_route. url ( & params) ;
407498
499+ // Start tracking content file access for incremental builds
500+ if options. incremental {
501+ start_tracking_content_files ( ) ;
502+ }
503+
408504 let result = route. build ( & mut PageContext :: from_static_route (
409505 content_sources,
410506 & mut route_assets,
@@ -413,15 +509,23 @@ pub async fn build(
413509 None ,
414510 ) ) ?;
415511
512+ // Finish tracking and record accessed content files
513+ let accessed_files = if options. incremental {
514+ finish_tracking_content_files ( )
515+ } else {
516+ None
517+ } ;
518+
416519 let file_path = cached_route. file_path ( & params, & options. output_dir ) ;
417520
418521 write_route_file ( & result, & file_path) ?;
419522
420523 info ! ( target: "pages" , "{} -> {} {}" , url, file_path. to_string_lossy( ) . dimmed( ) , format_elapsed_time( route_start. elapsed( ) , & route_format_options) ) ;
421524
422- // Track assets and source file for this route
525+ // Track assets, source file, and content files for this route
423526 track_route_assets ( & mut build_state, route_id. as_ref ( ) , & route_assets) ;
424527 track_route_source_file ( & mut build_state, route_id. as_ref ( ) , route. source_file ( ) ) ;
528+ track_route_content_files ( & mut build_state, route_id. as_ref ( ) , accessed_files) ;
425529
426530 build_pages_images. extend ( route_assets. images ) ;
427531 build_pages_scripts. extend ( route_assets. scripts ) ;
@@ -482,6 +586,11 @@ pub async fn build(
482586 let url = cached_route. url ( & page. 0 ) ;
483587 let file_path = cached_route. file_path ( & page. 0 , & options. output_dir ) ;
484588
589+ // Start tracking content file access for incremental builds
590+ if options. incremental {
591+ start_tracking_content_files ( ) ;
592+ }
593+
485594 let content = route. build ( & mut PageContext :: from_dynamic_route (
486595 & page,
487596 content_sources,
@@ -491,13 +600,21 @@ pub async fn build(
491600 None ,
492601 ) ) ?;
493602
603+ // Finish tracking and record accessed content files
604+ let accessed_files = if options. incremental {
605+ finish_tracking_content_files ( )
606+ } else {
607+ None
608+ } ;
609+
494610 write_route_file ( & content, & file_path) ?;
495611
496612 info ! ( target: "pages" , "├─ {} {}" , file_path. to_string_lossy( ) . dimmed( ) , format_elapsed_time( page_start. elapsed( ) , & route_format_options) ) ;
497613
498- // Track assets and source file for this page
614+ // Track assets, source file, and content files for this page
499615 track_route_assets ( & mut build_state, route_id. as_ref ( ) , & route_assets) ;
500616 track_route_source_file ( & mut build_state, route_id. as_ref ( ) , route. source_file ( ) ) ;
617+ track_route_content_files ( & mut build_state, route_id. as_ref ( ) , accessed_files) ;
501618
502619 build_metadata. add_page (
503620 base_path. clone ( ) ,
@@ -558,6 +675,11 @@ pub async fn build(
558675 & variant_id,
559676 ) ?;
560677
678+ // Start tracking content file access for incremental builds
679+ if options. incremental {
680+ start_tracking_content_files ( ) ;
681+ }
682+
561683 let result = route. build ( & mut PageContext :: from_static_route (
562684 content_sources,
563685 & mut route_assets,
@@ -566,13 +688,21 @@ pub async fn build(
566688 Some ( variant_id. clone ( ) ) ,
567689 ) ) ?;
568690
691+ // Finish tracking and record accessed content files
692+ let accessed_files = if options. incremental {
693+ finish_tracking_content_files ( )
694+ } else {
695+ None
696+ } ;
697+
569698 write_route_file ( & result, & file_path) ?;
570699
571700 info ! ( target: "pages" , "├─ {} {}" , file_path. to_string_lossy( ) . dimmed( ) , format_elapsed_time( variant_start. elapsed( ) , & route_format_options) ) ;
572701
573- // Track assets and source file for this variant
702+ // Track assets, source file, and content files for this variant
574703 track_route_assets ( & mut build_state, route_id. as_ref ( ) , & route_assets) ;
575704 track_route_source_file ( & mut build_state, route_id. as_ref ( ) , route. source_file ( ) ) ;
705+ track_route_content_files ( & mut build_state, route_id. as_ref ( ) , accessed_files) ;
576706
577707 build_pages_images. extend ( route_assets. images ) ;
578708 build_pages_scripts. extend ( route_assets. scripts ) ;
@@ -640,6 +770,11 @@ pub async fn build(
640770 & variant_id,
641771 ) ?;
642772
773+ // Start tracking content file access for incremental builds
774+ if options. incremental {
775+ start_tracking_content_files ( ) ;
776+ }
777+
643778 let content = route. build ( & mut PageContext :: from_dynamic_route (
644779 & page,
645780 content_sources,
@@ -649,13 +784,21 @@ pub async fn build(
649784 Some ( variant_id. clone ( ) ) ,
650785 ) ) ?;
651786
787+ // Finish tracking and record accessed content files
788+ let accessed_files = if options. incremental {
789+ finish_tracking_content_files ( )
790+ } else {
791+ None
792+ } ;
793+
652794 write_route_file ( & content, & file_path) ?;
653795
654796 info ! ( target: "pages" , "│ ├─ {} {}" , file_path. to_string_lossy( ) . dimmed( ) , format_elapsed_time( variant_page_start. elapsed( ) , & route_format_options) ) ;
655797
656- // Track assets and source file for this variant page
798+ // Track assets, source file, and content files for this variant page
657799 track_route_assets ( & mut build_state, route_id. as_ref ( ) , & route_assets) ;
658800 track_route_source_file ( & mut build_state, route_id. as_ref ( ) , route. source_file ( ) ) ;
801+ track_route_content_files ( & mut build_state, route_id. as_ref ( ) , accessed_files) ;
659802
660803 build_metadata. add_page (
661804 variant_path. clone ( ) ,
0 commit comments