Skip to content

Commit f52fd6b

Browse files
committed
Add a few more missing watch maps
1 parent ab93f47 commit f52fd6b

File tree

3 files changed

+206
-62
lines changed

3 files changed

+206
-62
lines changed

index.js

Lines changed: 106 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
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
*/
133136
async 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

lib/identify-pages.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ test.describe('identify-pages', () => {
1818
assert.equal(Object.keys(results.pages).length, 27, '27 pages are found')
1919

2020
assert.equal(results.warnings.length, 1, '1 warning produced')
21-
assert.equal(results.warnings[0].code, 'DOM_STACK_WARNING_PAGE_MD_SHADOWS_README', 'page.md shadows README.md warning is produced')
21+
assert.equal(results.warnings[0]?.code, 'DOM_STACK_WARNING_PAGE_MD_SHADOWS_README', 'page.md shadows README.md warning is produced')
2222
// assert.equal(results.nonPageFolders.length, 4, '4 non-page-folder')
2323
assert.equal(results.pages.find(p => p.path === 'html-page')?.pageFile?.type, 'html', 'html page is type html')
2424
assert.equal(results.pages.find(p => p.path === 'md-page')?.pageFile?.type, 'md', 'md page is type md')

0 commit comments

Comments
 (0)