1111 * @import { GlobalDataFunction, AsyncGlobalDataFunction, WorkerBuildStepResult, GlobalDataFunctionParams } from './lib/build-pages/index.js'
1212 * @import { BuildOptions, BuildContext } from 'esbuild'
1313 * @import { PageInfo, TemplateInfo } from './lib/identify-pages.js'
14+ * @import { BsInstance } from '@domstack/sync'
1415*/
1516import { once } from 'events'
1617import assert from 'node:assert'
@@ -23,7 +24,7 @@ import makeArray from 'make-array'
2324import ignore from 'ignore'
2425import { watch as cpxWatch } from 'cpx2'
2526import { inspect } from 'util'
26- import browserSync from 'browser- sync'
27+ import { createLogger as createSyncLogger , createServer } from '@domstack/ sync'
2728import { find } from '@11ty/dependency-tree-typescript'
2829
2930import { getCopyGlob } from './lib/build-static/index.js'
@@ -52,6 +53,13 @@ import { ensureDest } from './lib/helpers/ensure-dest.js'
5253import { DomStackAggregateError } from './lib/helpers/domstack-aggregate-error.js'
5354
5455export { PageData } from './lib/build-pages/page-data.js'
56+ export { wrapPinoLogger } from '@domstack/sync'
57+
58+ const LOG_PREFIX = '[domstack]'
59+
60+ export function createLogger ( level = 'info' , streams = { } , options = { } ) {
61+ return createSyncLogger ( level , streams , { prefix : LOG_PREFIX , ...options } )
62+ }
5563
5664/**
5765 * @typedef {BuildOptions } BuildOptions
@@ -164,9 +172,10 @@ export class DomStack {
164172 /** @type {Readonly<CurrentOpts & { ignore: string[] }> } */ opts
165173 /** @type {FSWatcher? } */ #watcher = null
166174 /** @type {any[]? } */ #cpxWatchers = null
167- /** @type {browserSync.BrowserSyncInstance ? } */ #browserSyncServer = null
175+ /** @type {BsInstance ? } */ #syncServer = null
168176 /** @type {BuildContext? } */ #esbuildContext = null
169177 /** @type {SiteData? } */ #siteData = null
178+ /** @type {ReturnType<typeof createLogger> } */ #logger
170179
171180 // Watch maps (rebuilt after every full rebuild)
172181 /** @type {Map<string, Set<string>> } depFilepath → Set<layoutName> */
@@ -206,18 +215,20 @@ export class DomStack {
206215 this . #src = src
207216 this . #dest = dest
208217
209- const copyDirs = ( opts ?. copy ?? [ ] ) . map ( dir => resolve ( dir ) )
218+ const { logger, ...buildOpts } = opts
219+ const copyDirs = ( buildOpts ?. copy ?? [ ] ) . map ( dir => resolve ( dir ) )
210220
211- this . opts = {
212- ...opts ,
221+ this . opts = /** @type { Readonly<CurrentOpts & { ignore: string[] }> } */ ( {
222+ ...buildOpts ,
213223 copy : copyDirs ,
214224 ignore : [
215225 ...DEFAULT_IGNORES ,
216226 basename ( dest ) ,
217227 ...copyDirs . map ( dir => basename ( dir ) ) ,
218- ...makeArray ( opts . ignore ) ,
228+ ...makeArray ( buildOpts . ignore ) ,
219229 ] ,
220- }
230+ } )
231+ this . #logger = logger ?? createLogger ( 'info' )
221232
222233 if ( copyDirs . length > 0 ) {
223234 const absDest = resolve ( this . #dest)
@@ -243,10 +254,12 @@ export class DomStack {
243254 * Build and watch a domstack build
244255 * @param {object } [params]
245256 * @param {boolean } params.serve
257+ * @param {(results: Results) => void | Promise<void> } [params.onInitialBuild]
246258 * @return {Promise<Results> }
247259 */
248260 async watch ( {
249261 serve,
262+ onInitialBuild,
250263 } = {
251264 serve : true ,
252265 } ) {
@@ -282,7 +295,7 @@ export class DomStack {
282295 pageBuildResults,
283296 }
284297 buildLogger ( report )
285- console . log ( 'Initial JS, CSS and Page Build Complete ' )
298+ this . #logger . info ( 'Initial JS, CSS and page build complete ' )
286299 } catch ( err ) {
287300 errorLogger ( err )
288301 if ( ! ( err instanceof DomStackAggregateError ) ) throw new Error ( 'Non-aggregate error thrown' , { cause : err } )
@@ -292,38 +305,29 @@ export class DomStack {
292305 // Build watch maps after initial build
293306 await this . #rebuildMaps( siteData )
294307
295- // ── Copy watchers & browser-sync ─────────────────────────────────────
308+ // ── Copy watchers & dev server ── ─────────────────────────────────────
296309 const copyDirs = getCopyDirs ( this . opts . copy )
297310
298311 this . #cpxWatchers = [
299312 cpxWatch ( getCopyGlob ( this . #src) , this . #dest, { ignore : this . opts . ignore } ) ,
300313 ...copyDirs . map ( copyDir => cpxWatch ( copyDir , this . #dest) )
301314 ]
302- if ( serve ) {
303- const bs = browserSync . create ( )
304- this . #browserSyncServer = bs
305- bs . watch ( basename ( this . #dest) , { ignoreInitial : true } ) . on ( 'change' , bs . reload )
306- bs . init ( {
307- server : this . #dest,
308- } )
309- }
310-
311- this . #cpxWatchers. forEach ( w => {
312- w . on ( 'watch-ready' , ( ) => {
313- console . log ( 'Copy watcher ready' )
314315
315- w . on ( 'copy' , ( /** @type {{ srcPath: string, dstPath: string } } */ e ) => {
316- console . log ( `Copy ${ e . srcPath } to ${ e . dstPath } ` )
317- } )
316+ const copyWatchersReady = this . #cpxWatchers. map ( async w => {
317+ w . on ( 'copy' , ( /** @type {{ srcPath: string, dstPath: string } } */ e ) => {
318+ this . #logger. info ( { srcPath : e . srcPath , dstPath : e . dstPath } , 'Copy file' )
319+ } )
318320
319- w . on ( 'remove' , ( /** @type {{ path: string } } */ e ) => {
320- console . log ( `Remove ${ e . path } ` )
321- } )
321+ w . on ( 'remove' , ( /** @type {{ path: string } } */ e ) => {
322+ this . #logger . info ( { path : e . path } , 'Remove file' )
323+ } )
322324
323- w . on ( 'watch-error' , ( /** @type {Error } */ err ) => {
324- console . log ( `Copy error: ${ err . message } ` )
325- } )
325+ w . on ( 'watch-error' , ( /** @type {Error } */ err ) => {
326+ this . #logger. error ( { err } , 'Copy error' )
326327 } )
328+
329+ await once ( w , 'watch-ready' )
330+ this . #logger. info ( 'Copy watcher ready' )
327331 } )
328332
329333 // ── Chokidar watcher ─────────────────────────────────────────────────
@@ -354,7 +358,20 @@ export class DomStack {
354358
355359 this . #watcher = watcher
356360
357- await once ( watcher , 'ready' )
361+ await Promise . all ( [
362+ ...copyWatchersReady ,
363+ once ( watcher , 'ready' ) ,
364+ ] )
365+
366+ await onInitialBuild ?. ( report )
367+
368+ if ( serve ) {
369+ this . #syncServer = await createServer ( {
370+ server : this . #dest,
371+ files : basename ( this . #dest) ,
372+ logger : this . #logger. child ( { component : 'sync' } , { prefix : '[domstack-sync]' } ) ,
373+ } )
374+ }
358375
359376 const enqueue = ( /** @type {() => Promise<void> } */ fn ) => {
360377 this . #buildLock = this . #buildLock. then ( ( ) => fn ( ) . catch ( errorLogger ) )
@@ -381,7 +398,7 @@ export class DomStack {
381398 * Used for structural changes (add/unlink), global.vars.*, esbuild.settings.*.
382399 */
383400 async #fullRebuild ( ) {
384- console . log ( 'Triggering full rebuild... ' )
401+ this . #logger . info ( 'Triggering full rebuild' )
385402 // Dispose the old esbuild context
386403 if ( this . #esbuildContext) {
387404 await this . #esbuildContext. dispose ( )
@@ -391,8 +408,7 @@ export class DomStack {
391408 const siteData = await identifyPages ( this . #src, this . opts )
392409
393410 if ( siteData . errors . length > 0 ) {
394- console . error ( 'identifyPages errors:' )
395- for ( const err of siteData . errors ) console . error ( ' ' , err . message )
411+ this . #logger. error ( { errors : siteData . errors . map ( err => err . message ) } , 'identifyPages errors' )
396412 return
397413 }
398414
@@ -429,13 +445,12 @@ export class DomStack {
429445 )
430446
431447 if ( isEsbuildEntry ) {
432- console . log ( `" ${ changedBasename } " ${ event } , restarting esbuild...` )
448+ this . #logger . info ( { file : changedBasename , event } , 'Restarting esbuild' )
433449
434450 // Re-identify pages to discover the new/removed entry point
435451 const siteData = await identifyPages ( this . #src, this . opts )
436452 if ( siteData . errors . length > 0 ) {
437- console . error ( 'identifyPages errors:' )
438- for ( const err of siteData . errors ) console . error ( ' ' , err . message )
453+ this . #logger. error ( { errors : siteData . errors . map ( err => err . message ) } , 'identifyPages errors' )
439454 return
440455 }
441456
@@ -490,7 +505,7 @@ export class DomStack {
490505 await this . #rebuildMaps( siteData )
491506 } else {
492507 // Non-esbuild file: structural change (page, layout, template, config, etc.)
493- console . log ( `" ${ changedBasename } " ${ event } , triggering full rebuild...` )
508+ this . #logger . info ( { file : changedBasename , event } , 'Triggering full rebuild' )
494509 return this . #fullRebuild( )
495510 }
496511 }
@@ -654,19 +669,19 @@ export class DomStack {
654669
655670 // 2. global.vars.* → full rebuild (esbuild restart + all pages)
656671 if ( globalVarsNames . some ( n => changedBasename === n ) ) {
657- console . log ( `" ${ changedBasename } " changed, triggering full rebuild...` )
672+ this . #logger . info ( { file : changedBasename } , 'Triggering full rebuild' )
658673 return this . #fullRebuild( )
659674 }
660675
661676 // 3. global.data.* → full page rebuild (no esbuild restart)
662677 if ( globalDataNames . some ( n => changedBasename === n ) ) {
663- console . log ( `" ${ changedBasename } " changed, rebuilding all pages...` )
678+ this . #logger . info ( { file : changedBasename } , 'Rebuilding all pages' )
664679 return this . #runPageBuild( siteData )
665680 }
666681
667682 // 4. esbuild.settings.* → full rebuild
668683 if ( esbuildSettingsNames . some ( n => changedBasename === n ) ) {
669- console . log ( `" ${ changedBasename } " changed, triggering full rebuild...` )
684+ this . #logger . info ( { file : changedBasename } , 'Triggering full rebuild' )
670685 return this . #fullRebuild( )
671686 }
672687
@@ -681,7 +696,7 @@ export class DomStack {
681696 // esbuild's own watcher handles these. Stable filenames mean page HTML doesn't
682697 // change, so no page rebuild is needed.
683698 if ( this . #esbuildEntryPoints. has ( changedPath ) ) {
684- console . log ( `" ${ changedBasename } " changed, esbuild will handle rebundling.` )
699+ this . #logger . info ( { file : changedBasename } , 'Esbuild will handle rebundling' )
685700 return
686701 }
687702
@@ -695,7 +710,7 @@ export class DomStack {
695710 const pageFilterPaths = Array . from ( affectedPages ) . map ( p => p . pageFile . filepath )
696711 return this . #runPageBuild( siteData , pageFilterPaths , [ ] )
697712 }
698- console . log ( `" ${ changedBasename } " changed but no pages use layout " ${ layoutName } ", skipping.` )
713+ this . #logger . info ( { file : changedBasename , layout : layoutName } , 'Layout changed but no pages use it; skipping' )
699714 return
700715 }
701716 // Not a registered layout — fall through to dep checks
@@ -755,7 +770,7 @@ export class DomStack {
755770 }
756771
757772 // 13. No matching rule — skip.
758- console . log ( `" ${ changedBasename } " changed but did not match any rebuild rule, skipping.` )
773+ this . #logger . info ( { file : changedBasename } , 'Changed file did not match any rebuild rule; skipping' )
759774 }
760775
761776 async stopWatching ( ) {
@@ -770,8 +785,8 @@ export class DomStack {
770785 await this . #esbuildContext. dispose ( )
771786 this . #esbuildContext = null
772787 }
773- this . #browserSyncServer ?. exit ( ) // This will kill the process
774- this . #browserSyncServer = null
788+ await this . #syncServer ?. exit ( )
789+ this . #syncServer = null
775790 }
776791
777792 /**
0 commit comments