11/**
22 * @import { DomStackOpts as DomStackOpts, Results, SiteData } from './lib/builder.js'
3- * @import { FSWatcher, Stats } from 'node:fs'
3+ * @import { FSWatcher } from 'chokidar'
4+ * @import { Stats } from 'node:fs'
45 * @import { PostVarsFunction, AsyncPostVarsFunction, AsyncLayoutFunction, LayoutFunction } from './lib/build-pages/page-data.js'
56 * @import { PageFunction, AsyncPageFunction } from './lib/build-pages/page-builders/page-writer.js'
67 * @import { TemplateFunction } from './lib/build-pages/page-builders/template-builder.js'
@@ -128,6 +129,8 @@ async function findDepsOf (filepath) {
128129 * layoutPageMap: Map<string, Set<PageInfo>>,
129130 * pageFileMap: Map<string, PageInfo>,
130131 * layoutFileMap: Map<string, string>,
132+ * pageDepMap: Map<string, Set<PageInfo>>,
133+ * templateDepMap: Map<string, Set<TemplateInfo>>,
131134 * }>}
132135 */
133136async function buildWatchMaps ( siteData ) {
@@ -139,6 +142,10 @@ async function buildWatchMaps (siteData) {
139142 const pageFileMap = new Map ( )
140143 /** @type {Map<string, string> } layout filepath -> layoutName */
141144 const layoutFileMap = new Map ( )
145+ /** @type {Map<string, Set<PageInfo>> } depFilepath -> Set<PageInfo> */
146+ const pageDepMap = new Map ( )
147+ /** @type {Map<string, Set<TemplateInfo>> } depFilepath -> Set<TemplateInfo> */
148+ const templateDepMap = new Map ( )
142149
143150 // Build layoutDepMap and layoutFileMap via static dep analysis
144151 await pMap ( Object . values ( siteData . layouts ) , async ( layout ) => {
@@ -150,7 +157,8 @@ async function buildWatchMaps (siteData) {
150157 }
151158 } , { concurrency : 8 } )
152159
153- // Build layoutPageMap and pageFileMap by resolving each page's layout var.
160+ // Build layoutPageMap, pageFileMap, and pageDepMap by resolving each page's layout var
161+ // and static dep analysis of its page file and page.vars file.
154162 // This runs in the main process — ESM cache is acceptable since we only need
155163 // to know the layout name string, not call the module.
156164 await pMap ( siteData . pages , async ( pageInfo ) => {
@@ -162,9 +170,29 @@ async function buildWatchMaps (siteData) {
162170
163171 if ( ! layoutPageMap . has ( layoutName ) ) layoutPageMap . set ( layoutName , new Set ( ) )
164172 layoutPageMap . get ( layoutName ) ?. add ( pageInfo )
173+
174+ // Track transitive deps of page.js and page.vars so changes to shared modules trigger a page rebuild
175+ const filesToTrack = [ pageInfo . pageFile . filepath ]
176+ if ( pageInfo . pageVars ) filesToTrack . push ( pageInfo . pageVars . filepath )
177+ for ( const file of filesToTrack ) {
178+ const deps = await findDepsOf ( file )
179+ for ( const dep of deps ) {
180+ if ( ! pageDepMap . has ( dep ) ) pageDepMap . set ( dep , new Set ( ) )
181+ pageDepMap . get ( dep ) ?. add ( pageInfo )
182+ }
183+ }
184+ } , { concurrency : 8 } )
185+
186+ // Build templateDepMap via static dep analysis of each template file
187+ await pMap ( siteData . templates , async ( templateInfo ) => {
188+ const deps = await findDepsOf ( templateInfo . templateFile . filepath )
189+ for ( const dep of deps ) {
190+ if ( ! templateDepMap . has ( dep ) ) templateDepMap . set ( dep , new Set ( ) )
191+ templateDepMap . get ( dep ) ?. add ( templateInfo )
192+ }
165193 } , { concurrency : 8 } )
166194
167- return { layoutDepMap, layoutPageMap, pageFileMap, layoutFileMap }
195+ return { layoutDepMap, layoutPageMap, pageFileMap, layoutFileMap, pageDepMap , templateDepMap }
168196}
169197
170198/**
@@ -302,8 +330,9 @@ export class DomStack {
302330 console . error ( 'esbuild rebuild errors:' , result . errors )
303331 return
304332 }
305- console . log ( 'esbuild rebuilt JS/CSS, re-rendering all pages...' )
306- runPageBuild ( ) . catch ( errorLogger )
333+ // Stable filenames in watch mode mean page HTML doesn't change when bundles rebuild.
334+ // Browser-sync reloads the browser directly — no page rebuild needed.
335+ console . log ( 'esbuild rebuilt JS/CSS' )
307336 }
308337
309338 // Run static copy and esbuild (watch mode) concurrently for the initial build.
@@ -344,14 +373,16 @@ export class DomStack {
344373 console . log ( 'Initial JS, CSS and Page Build Complete' )
345374
346375 // --- Build watch maps ---
347- let { layoutDepMap, layoutPageMap, pageFileMap, layoutFileMap } = await buildWatchMaps ( siteData )
376+ let { layoutDepMap, layoutPageMap, pageFileMap, layoutFileMap, pageDepMap , templateDepMap } = await buildWatchMaps ( siteData )
348377
349378 const rebuildMaps = async ( ) => {
350379 const maps = await buildWatchMaps ( siteData )
351- layoutDepMap = maps . layoutDepMap
352- layoutPageMap = maps . layoutPageMap
353- pageFileMap = maps . pageFileMap
354- layoutFileMap = maps . layoutFileMap
380+ layoutDepMap = maps . layoutDepMap // depFilepath -> Set<layoutName>
381+ layoutPageMap = maps . layoutPageMap // layoutName -> Set<PageInfo>
382+ pageFileMap = maps . pageFileMap // pageFile/pageVars filepath -> PageInfo
383+ layoutFileMap = maps . layoutFileMap // layout filepath -> layoutName
384+ pageDepMap = maps . pageDepMap // depFilepath -> Set<PageInfo> (via page.js + page.vars deps)
385+ templateDepMap = maps . templateDepMap // depFilepath -> Set<TemplateInfo>
355386 }
356387
357388 /**
@@ -455,15 +486,16 @@ export class DomStack {
455486 watcher . on ( 'change' , async path => {
456487 assert ( src )
457488 assert ( dest )
458- console . log ( `File ${ path } has been changed` )
459-
460489 const fileName = basename ( path )
461490 const absPath = resolve ( path )
462491
463- // 1. global.vars.* — data change, rebuild all pages + postVars
492+ // 1. global.vars.* — always do a full rebuild. The `browser` key is read by
493+ // buildEsbuild() in the main process and passed to esbuild as `define` substitutions.
494+ // esbuild's own watcher does NOT track global.vars as an input, so any change could
495+ // affect bundle output and requires restarting esbuild with fresh `define` values.
464496 if ( globalVarsNames . has ( fileName ) ) {
465- console . log ( 'global.vars changed, rebuilding all pages ...' )
466- runPageBuild ( ) . catch ( errorLogger )
497+ console . log ( 'global.vars changed, running full rebuild ...' )
498+ await fullRebuild ( )
467499 return
468500 }
469501
@@ -477,7 +509,7 @@ export class DomStack {
477509 // 3. markdown-it.settings.* — rebuild all .md pages only (rendering change)
478510 if ( markdownItSettingsNames . has ( fileName ) ) {
479511 const mdPages = new Set ( siteData . pages . filter ( p => p . type === 'md' ) )
480- console . log ( `markdown-it.settings changed, rebuilding ${ mdPages . size } .md page(s)...` )
512+ logRebuildTree ( fileName , mdPages )
481513 runPageBuild ( mdPages ) . catch ( errorLogger )
482514 return
483515 }
@@ -486,7 +518,7 @@ export class DomStack {
486518 if ( layoutFileMap . has ( absPath ) ) {
487519 const layoutName = /** @type {string } */ ( layoutFileMap . get ( absPath ) )
488520 const affectedPages = layoutPageMap . get ( layoutName ) ?? new Set ( )
489- console . log ( `Layout " ${ layoutName } " changed, rebuilding ${ affectedPages . size } page(s)...` )
521+ logRebuildTree ( fileName , affectedPages )
490522 runPageBuild ( affectedPages ) . catch ( errorLogger )
491523 return
492524 }
@@ -501,34 +533,62 @@ export class DomStack {
501533 affectedPages . add ( pageInfo )
502534 }
503535 }
504- console . log ( `Layout dep " ${ fileName } " changed, rebuilding ${ affectedPages . size } page(s)...` )
536+ logRebuildTree ( fileName , affectedPages )
505537 runPageBuild ( affectedPages ) . catch ( errorLogger )
506538 return
507539 }
508540
509541 // 6. Page file or page.vars changed — data change, rebuild page + postVarsPages
510542 if ( pageFileMap . has ( absPath ) ) {
511543 const affectedPage = /** @type {PageInfo } */ ( pageFileMap . get ( absPath ) )
512- const pagesToRebuild = new Set ( [ affectedPage , ... postVarsPages ] )
513- console . log ( `Page " ${ relname ( src , path ) } " changed, rebuilding ${ pagesToRebuild . size } page(s) (incl. ${ postVarsPages . size } postVars page(s))...` )
514- runPageBuild ( pagesToRebuild ) . catch ( errorLogger )
544+ const directPages = new Set ( [ affectedPage ] )
545+ logRebuildTree ( relname ( src , path ) , directPages , undefined , postVarsPages )
546+ runPageBuild ( new Set ( [ affectedPage , ... postVarsPages ] ) ) . catch ( errorLogger )
515547 return
516548 }
517549
518550 // 7. Template file changed — rebuild that template only
519551 if ( templateSuffixes . some ( s => fileName . endsWith ( s ) ) ) {
520552 const affectedTemplate = siteData . templates . find ( t => t . templateFile . filepath === absPath )
521553 if ( affectedTemplate ) {
522- console . log ( `Template " ${ fileName } " changed, rebuilding template...` )
554+ logRebuildTree ( fileName , undefined , new Set ( [ affectedTemplate ] ) )
523555 runPageBuild ( new Set ( ) , new Set ( [ affectedTemplate ] ) ) . catch ( errorLogger )
524556 return
525557 }
526558 }
527559
528- // 8. Layout style/client (.layout.css, .layout.client.*) — esbuild watches these,
529- // onEnd will fire a full page rebuild automatically. Nothing to do here.
560+ // 8. Dep of a page.js or page.vars file — data change, rebuild affected pages + postVarsPages
561+ if ( pageDepMap . has ( absPath ) ) {
562+ const affectedPages = /** @type {Set<PageInfo> } */ ( pageDepMap . get ( absPath ) )
563+ logRebuildTree ( fileName , affectedPages , undefined , postVarsPages )
564+ runPageBuild ( new Set ( [ ...affectedPages , ...postVarsPages ] ) ) . catch ( errorLogger )
565+ return
566+ }
567+
568+ // 9. Dep of a template file — rebuild affected templates only
569+ if ( templateDepMap . has ( absPath ) ) {
570+ const affectedTemplates = /** @type {Set<TemplateInfo> } */ ( templateDepMap . get ( absPath ) )
571+ logRebuildTree ( fileName , undefined , affectedTemplates )
572+ runPageBuild ( new Set ( ) , affectedTemplates ) . catch ( errorLogger )
573+ return
574+ }
575+
576+ // 10. Any JS/CSS bundle (client.js, page.css, .layout.css, .layout.client.*, etc.)
577+ // esbuild's own watcher picks these up and rebuilds the bundle. Since watch mode
578+ // uses stable (unhashed) filenames, page HTML doesn't change — browser-sync reloads
579+ // the browser directly. Nothing to do here.
580+ const esbuildEntryPoints = new Set ( [
581+ siteData . globalClient ?. filepath ,
582+ siteData . globalStyle ?. filepath ,
583+ ...siteData . pages . flatMap ( p => [ p . clientBundle ?. filepath , p . pageStyle ?. filepath , ...Object . values ( p . workers ?? { } ) . map ( w => w . filepath ) ] ) ,
584+ ...Object . values ( siteData . layouts ) . flatMap ( l => [ l . layoutClient ?. filepath , l . layoutStyle ?. filepath ] ) ,
585+ ] . filter ( Boolean ) )
586+ if ( esbuildEntryPoints . has ( absPath ) ) {
587+ console . log ( `"${ fileName } " changed — esbuild will rebuild (browser-sync will reload)` )
588+ return
589+ }
530590
531- // 9 . Unrecognized — skip
591+ // 11 . Unrecognized — skip
532592 console . log ( `"${ fileName } " changed but did not match any rebuild rule, skipping.` )
533593 } )
534594
@@ -564,6 +624,27 @@ function relname (root, name) {
564624 return root === name ? basename ( name ) : relative ( root , name )
565625}
566626
627+ /**
628+ * Log a rebuild tree showing what triggered a rebuild and what will be rebuilt.
629+ * @param {string } trigger - The changed file (display name)
630+ * @param {Set<PageInfo> } [pages]
631+ * @param {Set<import('./lib/identify-pages.js').TemplateInfo> } [templates]
632+ * @param {Set<PageInfo> } [postVarsPages]
633+ */
634+ function logRebuildTree ( trigger , pages , templates , postVarsPages ) {
635+ const lines = [ `"${ trigger } " changed:` ]
636+ for ( const p of pages ?? [ ] ) {
637+ lines . push ( ` → ${ p . outputRelname } ` )
638+ }
639+ for ( const p of postVarsPages ?? [ ] ) {
640+ if ( ! pages ?. has ( p ) ) lines . push ( ` → ${ p . outputRelname } (postVars)` )
641+ }
642+ for ( const t of templates ?? [ ] ) {
643+ lines . push ( ` → ${ t . outputName } (template)` )
644+ }
645+ console . log ( lines . join ( '\n' ) )
646+ }
647+
567648/**
568649 * An error logger
569650 * @param {Error | AggregateError | any } err The error to log
0 commit comments