33const assert = require ( 'assert' )
44const Mode = require ( 'stat-mode' )
55const path = require ( 'path' )
6+ const watcher = require ( './watcher' )
7+
68const {
79 readdir,
810 batchAsync,
@@ -26,7 +28,9 @@ const { Debugger, fileLogHandler } = require('./debug')
2628
2729const symbol = {
2830 env : Symbol ( 'env' ) ,
29- log : Symbol ( 'log' )
31+ log : Symbol ( 'log' ) ,
32+ watch : Symbol ( 'watch' ) ,
33+ closeWatcher : Symbol ( 'closeWatcher' )
3034}
3135
3236/**
@@ -109,6 +113,9 @@ module.exports = Metalsmith
109113 * @return {Metalsmith }
110114 */
111115
116+ /**
117+ * @constructor
118+ */
112119function Metalsmith ( directory ) {
113120 if ( ! ( this instanceof Metalsmith ) ) return new Metalsmith ( directory )
114121 assert ( directory , 'You must pass a working directory path.' )
@@ -131,6 +138,16 @@ function Metalsmith(directory) {
131138 enumerable : false ,
132139 writable : true
133140 } )
141+ Object . defineProperty ( this , symbol . watch , {
142+ value : false ,
143+ enumerable : false ,
144+ writable : true
145+ } )
146+ Object . defineProperty ( this , symbol . closeWatcher , {
147+ value : null ,
148+ enumerable : false ,
149+ writable : true
150+ } )
134151}
135152
136153/**
@@ -412,20 +429,22 @@ Metalsmith.prototype.build = function (callback) {
412429 if ( this [ symbol . log ] . pending ) {
413430 this [ symbol . log ] . on ( 'ready' , ( ) => resolve ( ) )
414431 } else {
415- /* istanbul ignore next */
416432 resolve ( )
417433 }
418434 } )
419435 }
420436 } )
421- . then ( this . process . bind ( this ) )
422- . then ( ( files ) => {
423- return this . write ( files ) . then ( ( ) => {
424- if ( this [ symbol . log ] ) this [ symbol . log ] . end ( )
425- return files
437+ . then (
438+ this . process . bind ( this , ( err , files ) => {
439+ if ( err ) throw err
440+ return this . write ( files )
441+ . then ( ( ) => {
442+ if ( this [ symbol . log ] ) this [ symbol . log ] . end ( )
443+ if ( isFunction ( callback ) ) callback ( null , files )
444+ } )
445+ . catch ( callback )
426446 } )
427- } )
428-
447+ )
429448 /* block required for Metalsmith 2.x callback-flow compat */
430449 if ( isFunction ( callback ) ) {
431450 result . then ( ( files ) => callback ( null , files ) , callback )
@@ -434,6 +453,65 @@ Metalsmith.prototype.build = function (callback) {
434453 }
435454}
436455
456+ /**
457+ * **EXPERIMENTAL — Caution**
458+ * * not to be used with @metalsmith/metadata <= 0.2.0: a bug may trigger an infinite loop
459+ * * not to be used with existing watch plugins
460+ * * metalsmith.process/build are **not awaitable** when watching is enabled.
461+ * Instead of running once at the build's end, callbacks passed to these methods will run on every rebuild.
462+ *
463+ * Set the list of paths to watch and trigger rebuilds on. The watch method will skip files ignored with `metalsmith.ignore()`
464+ * and will do partial (true) or full (false) rebuilds depending on the `metalsmith.clean()` setting.
465+ * It can be used both for rebuilding in-memory with `metalsmith.process` or writing to file system with `metalsmith.build`,
466+ * @method Metalsmith#watch
467+ * @param {boolean|string|string[] } [paths]
468+ * @return {Metalsmith|Promise<void>|boolean|import('chokidar').WatchOptions }
469+ * @example
470+ *
471+ * metalsmith
472+ * .ignore(['wont-be-watched']) // ignored
473+ * .clean(false) // do partial rebuilds
474+ * .watch(true) // watch all files in metalsmith.source()
475+ * .watch(['lib','src']) // or watch files in directories 'lib' and 'src'
476+ *
477+ * if (process.argv[2] === '--dry-run') {
478+ * metalsmith.process(onRebuild) // reprocess in memory without writing to disk
479+ * } else {
480+ * metalsmith.build(onRebuild) // rewrite to disk
481+ * }
482+ *
483+ * function onRebuild(err, files) {
484+ * if (err) {
485+ * metalsmith.watch(false) // stop watching
486+ * .finally(() => console.log(err)) // and log build error
487+ * }
488+ * console.log('reprocessed files', Object.keys(files).join(', ')))
489+ * }
490+ */
491+ Metalsmith . prototype . watch = function ( options ) {
492+ if ( isUndefined ( options ) ) return this [ symbol . watch ]
493+ if ( ! options ) {
494+ // if watch has previously been enabled and is now passed false, close the watcher
495+ this [ symbol . watch ] = false
496+ if ( options === false && typeof this [ symbol . closeWatcher ] === 'function' ) {
497+ return this [ symbol . closeWatcher ] ( )
498+ }
499+ } else {
500+ if ( isString ( options ) || Array . isArray ( options ) ) options = { paths : options }
501+ else if ( options === true ) options = { paths : this . source ( ) }
502+
503+ this [ symbol . watch ] = {
504+ paths : options . paths ,
505+ alwaysStat : false ,
506+ cwd : this . directory ( ) ,
507+ ignored : this . ignore ( ) ,
508+ ignoreInitial : true ,
509+ awaitWriteFinish : true
510+ }
511+ }
512+ return this
513+ }
514+
437515/**
438516 * Process files through plugins without writing out files.
439517 *
@@ -450,15 +528,24 @@ Metalsmith.prototype.build = function (callback) {
450528 */
451529
452530Metalsmith . prototype . process = function ( callback ) {
453- const result = this . read ( this . source ( ) ) . then ( ( files ) => {
454- return this . run ( files , this . plugins )
455- } )
531+ const result = this . read ( this . source ( ) )
456532
457- /* block required for Metalsmith 2.x callback-flow compat */
458- if ( callback ) {
459- result . then ( ( files ) => callback ( null , files ) , callback )
533+ if ( this . watch ( ) ) {
534+ return result . then ( ( files ) => {
535+ const msWatcher = watcher ( files , this )
536+ msWatcher ( this [ symbol . watch ] , callback ) . then ( ( close ) => {
537+ this [ symbol . closeWatcher ] = close
538+ } )
539+ } )
460540 } else {
461- return result
541+ result . then ( ( files ) => this . run ( files , this . plugins ) )
542+
543+ /* block required for Metalsmith 2.x callback-flow compat */
544+ if ( callback ) {
545+ result . then ( ( files ) => callback ( null , files ) , callback )
546+ } else {
547+ return result
548+ }
462549 }
463550}
464551
0 commit comments