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'
@@ -21,7 +22,7 @@ import makeArray from 'make-array'
2122import ignore from 'ignore'
2223import { watch as cpxWatch } from 'cpx2'
2324import { inspect } from 'util'
24- import browserSync from 'browser- sync'
25+ import { createLogger as createSyncLogger , createServer } from '@domstack/ sync'
2526import { find } from '@11ty/dependency-tree-typescript'
2627
2728import { getCopyGlob } from './lib/build-static/index.js'
@@ -50,6 +51,13 @@ import { ensureDest } from './lib/helpers/ensure-dest.js'
5051import { DomStackAggregateError } from './lib/helpers/domstack-aggregate-error.js'
5152
5253export { PageData } from './lib/build-pages/page-data.js'
54+ export { wrapPinoLogger } from '@domstack/sync'
55+
56+ const LOG_PREFIX = '[domstack]'
57+
58+ export function createLogger ( level = 'info' , streams = { } , options = { } ) {
59+ return createSyncLogger ( level , streams , { prefix : LOG_PREFIX , ...options } )
60+ }
5361
5462/**
5563 * @typedef {BuildOptions } BuildOptions
@@ -154,9 +162,10 @@ export class DomStack {
154162 /** @type {Readonly<CurrentOpts & { ignore: string[] }> } */ opts
155163 /** @type {FSWatcher? } */ #watcher = null
156164 /** @type {any[]? } */ #cpxWatchers = null
157- /** @type {browserSync.BrowserSyncInstance ? } */ #browserSyncServer = null
165+ /** @type {BsInstance ? } */ #syncServer = null
158166 /** @type {BuildContext? } */ #esbuildContext = null
159167 /** @type {SiteData? } */ #siteData = null
168+ /** @type {ReturnType<typeof createLogger> } */ #logger
160169
161170 // Watch maps (rebuilt after every full rebuild)
162171 /** @type {Map<string, Set<string>> } depFilepath → Set<layoutName> */
@@ -192,17 +201,19 @@ export class DomStack {
192201 this . #src = src
193202 this . #dest = dest
194203
195- const copyDirs = opts ?. copy ?? [ ]
204+ const { logger, ...buildOpts } = opts
205+ const copyDirs = buildOpts ?. copy ?? [ ]
196206
197- this . opts = {
198- ...opts ,
207+ this . opts = /** @type { Readonly<CurrentOpts & { ignore: string[] }> } */ ( {
208+ ...buildOpts ,
199209 ignore : [
200210 ...DEFAULT_IGNORES ,
201211 basename ( dest ) ,
202212 ...copyDirs . map ( dir => basename ( dir ) ) ,
203- ...makeArray ( opts . ignore ) ,
213+ ...makeArray ( buildOpts . ignore ) ,
204214 ] ,
205- }
215+ } )
216+ this . #logger = logger ?? createLogger ( 'info' )
206217
207218 if ( copyDirs && copyDirs . length > 0 ) {
208219 const absDest = resolve ( this . #dest)
@@ -229,10 +240,12 @@ export class DomStack {
229240 * Build and watch a domstack build
230241 * @param {object } [params]
231242 * @param {boolean } params.serve
243+ * @param {(results: Results) => void | Promise<void> } [params.onInitialBuild]
232244 * @return {Promise<Results> }
233245 */
234246 async watch ( {
235247 serve,
248+ onInitialBuild,
236249 } = {
237250 serve : true ,
238251 } ) {
@@ -268,7 +281,7 @@ export class DomStack {
268281 pageBuildResults,
269282 }
270283 buildLogger ( report )
271- console . log ( 'Initial JS, CSS and Page Build Complete ' )
284+ this . #logger . info ( 'Initial JS, CSS and page build complete ' )
272285 } catch ( err ) {
273286 errorLogger ( err )
274287 if ( ! ( err instanceof DomStackAggregateError ) ) throw new Error ( 'Non-aggregate error thrown' , { cause : err } )
@@ -278,38 +291,29 @@ export class DomStack {
278291 // Build watch maps after initial build
279292 await this . #rebuildMaps( siteData )
280293
281- // ── Copy watchers & browser-sync ─────────────────────────────────────
294+ // ── Copy watchers & dev server ── ─────────────────────────────────────
282295 const copyDirs = getCopyDirs ( this . opts . copy )
283296
284297 this . #cpxWatchers = [
285298 cpxWatch ( getCopyGlob ( this . #src) , this . #dest, { ignore : this . opts . ignore } ) ,
286299 ...copyDirs . map ( copyDir => cpxWatch ( copyDir , this . #dest) )
287300 ]
288- if ( serve ) {
289- const bs = browserSync . create ( )
290- this . #browserSyncServer = bs
291- bs . watch ( basename ( this . #dest) , { ignoreInitial : true } ) . on ( 'change' , bs . reload )
292- bs . init ( {
293- server : this . #dest,
294- } )
295- }
296-
297- this . #cpxWatchers. forEach ( w => {
298- w . on ( 'watch-ready' , ( ) => {
299- console . log ( 'Copy watcher ready' )
300301
301- w . on ( 'copy' , ( /** @type {{ srcPath: string, dstPath: string } } */ e ) => {
302- console . log ( `Copy ${ e . srcPath } to ${ e . dstPath } ` )
303- } )
302+ const copyWatchersReady = this . #cpxWatchers. map ( async w => {
303+ w . on ( 'copy' , ( /** @type {{ srcPath: string, dstPath: string } } */ e ) => {
304+ this . #logger. info ( { srcPath : e . srcPath , dstPath : e . dstPath } , 'Copy file' )
305+ } )
304306
305- w . on ( 'remove' , ( /** @type {{ path: string } } */ e ) => {
306- console . log ( `Remove ${ e . path } ` )
307- } )
307+ w . on ( 'remove' , ( /** @type {{ path: string } } */ e ) => {
308+ this . #logger . info ( { path : e . path } , 'Remove file' )
309+ } )
308310
309- w . on ( 'watch-error' , ( /** @type {Error } */ err ) => {
310- console . log ( `Copy error: ${ err . message } ` )
311- } )
311+ w . on ( 'watch-error' , ( /** @type {Error } */ err ) => {
312+ this . #logger. error ( { err } , 'Copy error' )
312313 } )
314+
315+ await once ( w , 'watch-ready' )
316+ this . #logger. info ( 'Copy watcher ready' )
313317 } )
314318
315319 // ── Chokidar watcher ─────────────────────────────────────────────────
@@ -340,7 +344,20 @@ export class DomStack {
340344
341345 this . #watcher = watcher
342346
343- await once ( watcher , 'ready' )
347+ await Promise . all ( [
348+ ...copyWatchersReady ,
349+ once ( watcher , 'ready' ) ,
350+ ] )
351+
352+ await onInitialBuild ?. ( report )
353+
354+ if ( serve ) {
355+ this . #syncServer = await createServer ( {
356+ server : this . #dest,
357+ files : basename ( this . #dest) ,
358+ logger : this . #logger. child ( { component : 'sync' } , { prefix : '[domstack-sync]' } ) ,
359+ } )
360+ }
344361
345362 const enqueue = ( /** @type {() => Promise<void> } */ fn ) => {
346363 this . #buildLock = this . #buildLock. then ( ( ) => fn ( ) . catch ( errorLogger ) )
@@ -367,7 +384,7 @@ export class DomStack {
367384 * Used for structural changes (add/unlink), global.vars.*, esbuild.settings.*.
368385 */
369386 async #fullRebuild ( ) {
370- console . log ( 'Triggering full rebuild... ' )
387+ this . #logger . info ( 'Triggering full rebuild' )
371388 // Dispose the old esbuild context
372389 if ( this . #esbuildContext) {
373390 await this . #esbuildContext. dispose ( )
@@ -377,8 +394,7 @@ export class DomStack {
377394 const siteData = await identifyPages ( this . #src, this . opts )
378395
379396 if ( siteData . errors . length > 0 ) {
380- console . error ( 'identifyPages errors:' )
381- for ( const err of siteData . errors ) console . error ( ' ' , err . message )
397+ this . #logger. error ( { errors : siteData . errors . map ( err => err . message ) } , 'identifyPages errors' )
382398 return
383399 }
384400
@@ -415,13 +431,12 @@ export class DomStack {
415431 )
416432
417433 if ( isEsbuildEntry ) {
418- console . log ( `" ${ changedBasename } " ${ event } , restarting esbuild...` )
434+ this . #logger . info ( { file : changedBasename , event } , 'Restarting esbuild' )
419435
420436 // Re-identify pages to discover the new/removed entry point
421437 const siteData = await identifyPages ( this . #src, this . opts )
422438 if ( siteData . errors . length > 0 ) {
423- console . error ( 'identifyPages errors:' )
424- for ( const err of siteData . errors ) console . error ( ' ' , err . message )
439+ this . #logger. error ( { errors : siteData . errors . map ( err => err . message ) } , 'identifyPages errors' )
425440 return
426441 }
427442
@@ -476,7 +491,7 @@ export class DomStack {
476491 await this . #rebuildMaps( siteData )
477492 } else {
478493 // Non-esbuild file: structural change (page, layout, template, config, etc.)
479- console . log ( `" ${ changedBasename } " ${ event } , triggering full rebuild...` )
494+ this . #logger . info ( { file : changedBasename , event } , 'Triggering full rebuild' )
480495 return this . #fullRebuild( )
481496 }
482497 }
@@ -640,19 +655,19 @@ export class DomStack {
640655
641656 // 2. global.vars.* → full rebuild (esbuild restart + all pages)
642657 if ( globalVarsNames . some ( n => changedBasename === n ) ) {
643- console . log ( `" ${ changedBasename } " changed, triggering full rebuild...` )
658+ this . #logger . info ( { file : changedBasename } , 'Triggering full rebuild' )
644659 return this . #fullRebuild( )
645660 }
646661
647662 // 3. global.data.* → full page rebuild (no esbuild restart)
648663 if ( globalDataNames . some ( n => changedBasename === n ) ) {
649- console . log ( `" ${ changedBasename } " changed, rebuilding all pages...` )
664+ this . #logger . info ( { file : changedBasename } , 'Rebuilding all pages' )
650665 return this . #runPageBuild( siteData )
651666 }
652667
653668 // 4. esbuild.settings.* → full rebuild
654669 if ( esbuildSettingsNames . some ( n => changedBasename === n ) ) {
655- console . log ( `" ${ changedBasename } " changed, triggering full rebuild...` )
670+ this . #logger . info ( { file : changedBasename } , 'Triggering full rebuild' )
656671 return this . #fullRebuild( )
657672 }
658673
@@ -667,7 +682,7 @@ export class DomStack {
667682 // esbuild's own watcher handles these. Stable filenames mean page HTML doesn't
668683 // change, so no page rebuild is needed.
669684 if ( this . #esbuildEntryPoints. has ( changedPath ) ) {
670- console . log ( `" ${ changedBasename } " changed, esbuild will handle rebundling.` )
685+ this . #logger . info ( { file : changedBasename } , 'Esbuild will handle rebundling' )
671686 return
672687 }
673688
@@ -681,7 +696,7 @@ export class DomStack {
681696 const pageFilterPaths = Array . from ( affectedPages ) . map ( p => p . pageFile . filepath )
682697 return this . #runPageBuild( siteData , pageFilterPaths , [ ] )
683698 }
684- console . log ( `" ${ changedBasename } " changed but no pages use layout " ${ layoutName } ", skipping.` )
699+ this . #logger . info ( { file : changedBasename , layout : layoutName } , 'Layout changed but no pages use it; skipping' )
685700 return
686701 }
687702 // Not a registered layout — fall through to dep checks
@@ -741,7 +756,7 @@ export class DomStack {
741756 }
742757
743758 // 13. No matching rule — skip.
744- console . log ( `" ${ changedBasename } " changed but did not match any rebuild rule, skipping.` )
759+ this . #logger . info ( { file : changedBasename } , 'Changed file did not match any rebuild rule; skipping' )
745760 }
746761
747762 async stopWatching ( ) {
@@ -756,8 +771,8 @@ export class DomStack {
756771 await this . #esbuildContext. dispose ( )
757772 this . #esbuildContext = null
758773 }
759- this . #browserSyncServer ?. exit ( ) // This will kill the process
760- this . #browserSyncServer = null
774+ await this . #syncServer ?. exit ( )
775+ this . #syncServer = null
761776 }
762777
763778 /**
0 commit comments