200 lines
6.8 KiB
JavaScript
Executable File
200 lines
6.8 KiB
JavaScript
Executable File
// native imports
|
|
import { EventEmitter as Emitter } from 'events'
|
|
import path from 'path'
|
|
// third party imports
|
|
import { watch } from 'chokidar'
|
|
import debounce from 'debounce-fn'
|
|
// UCI imports
|
|
import readIgnoreLists from '@uci-utils/read-lines'
|
|
import logger from '@uci-utils/logger'
|
|
|
|
let log = {}
|
|
|
|
const READY_TIMEOUT = 10000 //default
|
|
|
|
class Watcher extends Emitter {
|
|
constructor(opts = {}) {
|
|
super()
|
|
log = logger({ package: '@uci-utils/watcher', class: 'Watcher', file: 'src/watcher.js' })
|
|
opts.unlinkDir = 'unlinkDir' in opts ? opts.unlinkDir : true // delete file even on by default
|
|
this.opts = opts
|
|
this.timeout = process.env.READY_TIMEOUT || opts.readyTimeout || READY_TIMEOUT
|
|
this._ignored = opts.ignored ? (Array.isArray(opts.ignored) ? opts.ignored : opts.ignored.split(',')) : []
|
|
this.handler = opts.handler || _handler
|
|
this._ready = false
|
|
this._watching = false
|
|
// .goutputstream files arise during atomic writes, ignore them by default
|
|
this.ignore_goutputstream = opts.ignore_goutputstream === false ? false : true
|
|
return this
|
|
}
|
|
|
|
get watching() {
|
|
return this._watching
|
|
}
|
|
|
|
get ready() {
|
|
return this._ready
|
|
}
|
|
|
|
registerHandler(func, opts) {
|
|
opts = Object.assign({}, this.opts, opts)
|
|
if (!this._watcher) return 'failed: watcher not initialized'
|
|
if (typeof func === 'function') this.handler = func
|
|
else if (func) opts = func
|
|
opts = Object.assign({}, this.opts, opts)
|
|
let handler
|
|
if (opts.debounce)
|
|
handler = debounce((type, file) => {
|
|
log.debug(`waited : ${opts.debounce}ms before calling handler`)
|
|
log.info('debounced handler, only last event is emitted')
|
|
this.handler.call(this, type, file)
|
|
}, { wait: opts.debounce })
|
|
else
|
|
handler = (type, file) => { this.handler.call(this, type, file) }
|
|
|
|
this._watcher.removeAllListeners()
|
|
|
|
if (opts.delDir) this._watcher.on('unlinkDir', handler.bind(this, 'dir-deleted'))
|
|
if (opts.addDir) this._watcher.on('addDir', handler.bind(this, 'dir-added'))
|
|
|
|
// these are watched by default, one can take action or not in the handler
|
|
this._watcher
|
|
.on('add', handler.bind(this, 'added'))
|
|
.on('change', handler.bind(this, 'modified'))
|
|
.on('unlink', handler.bind(this, 'removed'))
|
|
|
|
// reset the error event since it got scrubbed
|
|
this._watcher.on('error', error => {
|
|
const msg = 'chokidar watcher error'
|
|
log.error({ error: error, msg: msg })
|
|
this.emit('error', msg, error)
|
|
})
|
|
}
|
|
|
|
async start(opts = {}) {
|
|
if (this._watching) {
|
|
log.warn(`watching aleady running for ${opts.source || this.opts.source}`)
|
|
return false
|
|
}
|
|
if (!this._watcher) opts = await this._init(opts)
|
|
if (this._ready) {
|
|
this.emit('ready', true, opts)
|
|
log.trace({ watching: this._watcher.getWatched(), msg: 'initial files watched' })
|
|
log.info(`now watching ${opts.source || this.opts.source}`)
|
|
if (this.registerHandler(opts)) {
|
|
log.fatal('watcher was not initialized so could not register handler')
|
|
return new Error('watcher was not initialized so could not register handler')
|
|
}
|
|
this._watching = true
|
|
this.emit('watching', true, opts)
|
|
} else {
|
|
const msg = 'watcher is not ready to start, check options and try again'
|
|
log.fatal(msg)
|
|
this.emit('error', msg)
|
|
return new Error('not ready to start check configuration options')
|
|
}
|
|
}
|
|
|
|
stop() {
|
|
if (this._watching) {
|
|
this._watching = false
|
|
this._watcher.close()
|
|
this.emit('watching', false)
|
|
}
|
|
else log.warn('not watching, nothing to stop')
|
|
}
|
|
|
|
remove() {
|
|
if (this._watcher) {
|
|
this.stop()
|
|
this._watcher.removeAllListeners()
|
|
delete (this._watcher)
|
|
} else log.warn('no watcher started nothing to remove')
|
|
this._ready = false
|
|
this.emit('ready', false)
|
|
}
|
|
|
|
async restart(opts, force) {
|
|
if (typeof opts === 'boolean') { force = opts, opts = {} }
|
|
this.stop()
|
|
await this.start(opts)
|
|
}
|
|
|
|
getWatcher() {
|
|
return this._watcher
|
|
}
|
|
|
|
// private methods
|
|
// set up chokidar
|
|
async _init(opts = {}) {
|
|
if (!opts.overwrite) {
|
|
await this._fetchIgnoreLists(this.opts.excludeFrom)
|
|
await this._fetchIgnoreLists(this.opts.ignoreList)
|
|
}
|
|
await this._fetchIgnoreLists(opts.excludeFrom, opts.overwrite)
|
|
await this._fetchIgnoreLists(opts.ignoreList, opts.overwrite)
|
|
if (!opts.ignored) opts.ignored = []
|
|
opts.ignored = Array.isArray(opts.ignored) ? opts.ignored : opts.ignored.split(',')
|
|
opts.ignored = opts.overwrite ? opts.ignored : [...this._ignored, ...opts.ignored]
|
|
if (this.ignore_goutputstream) opts.ignored.push('**/.goutputstream*')
|
|
opts = Object.assign({}, this.opts, opts) // now that ignore arrays are dealt with merge options
|
|
return new Promise(async (resolve, reject) => {
|
|
log.debug({ options: opts, msg: 'initializing watch with options' })
|
|
if (opts.source) {
|
|
// create chokidar watcher
|
|
this._watcher = watch(opts.source, opts)
|
|
this._watcher.once('error', error => {
|
|
log.error({ error: error, msg: 'Watcher error' })
|
|
reject({ error: error, msg: 'Watcher error' })
|
|
})
|
|
this._watcher.once('ready', () => {
|
|
clearTimeout(readyTimeout)
|
|
log.info('initial scan sucessful, ready to start')
|
|
this._ready = true
|
|
resolve(opts)
|
|
})
|
|
log.debug(`initial scanning, timeout in ${this.timeout}ms`)
|
|
let readyTimeout = setTimeout(() => {
|
|
log.fatal({ options: opts, timeout: this.timeout, msg: 'Timeout: unable to complete initial scan' })
|
|
reject('timeout during initial scan, maybe increase ready timeout')
|
|
}, this.timeout)
|
|
}
|
|
else {
|
|
log.fatal('MUST provide a source directory(s) option to watch')
|
|
reject('watching - no source provided')
|
|
}
|
|
})
|
|
}
|
|
|
|
async _fetchIgnoreLists(lists, overwrite) {
|
|
if (typeof lists === 'string') lists = [lists]
|
|
if (!Array.isArray(lists)) return // no lists
|
|
let ignored = await readIgnoreLists(lists)
|
|
this._ignored = overwrite ? ignored : [...this._ignored, ...ignored]
|
|
}
|
|
|
|
|
|
|
|
} //end class
|
|
|
|
// default handler
|
|
function _handler(type, f) {
|
|
log.debug(`file ${f} was ${type}`)
|
|
const fname = path.basename(f)
|
|
if (fname.toLowerCase() === 'package.json' && path.dirname(f) === this.opts.source.replace(/\/$/, ''))
|
|
if (type !== 'modified') {
|
|
const msg = `a package.json in root of ${this.opts.source} was added or removed`
|
|
log.warning(msg)
|
|
this.emit('warning', f, msg)
|
|
return
|
|
} else {
|
|
this.emit('install', f)
|
|
}
|
|
// user might want to run debounce on the listener for this event
|
|
log.debug({ file: f, type: type, msg: 'file system changed, emitting' })
|
|
this.emit('changed', f, type)
|
|
} // end handler
|
|
|
|
export default Watcher
|
|
export { Watcher }
|