watchexec_watcher.js
1 'use strict'; 2 3 const execa = require('execa'); 4 5 const { statSync } = require('fs'); 6 const path = require('path'); 7 const common = require('./common'); 8 const EventEmitter = require('events').EventEmitter; 9 10 const { EOL } = require('os'); 11 12 /** 13 * Constants 14 */ 15 16 const CHANGE_EVENT = common.CHANGE_EVENT; 17 const DELETE_EVENT = common.DELETE_EVENT; 18 const ADD_EVENT = common.ADD_EVENT; 19 const ALL_EVENT = common.ALL_EVENT; 20 21 const typeMap = { 22 rename: CHANGE_EVENT, 23 write: CHANGE_EVENT, 24 remove: DELETE_EVENT, 25 create: ADD_EVENT, 26 }; 27 28 const messageRegexp = /(rename|write|remove|create)\s(.+)/; 29 30 /** 31 * Manages streams from subprocess and turns into sane events 32 * 33 * @param {Stream} data 34 * @private 35 */ 36 function _messageHandler(data) { 37 data 38 .toString() 39 .split(EOL) 40 .filter(str => str.trim().length) 41 .filter(str => messageRegexp.test(str)) 42 .map(line => { 43 const [, command, path] = [...line.match(messageRegexp)]; 44 return [command, path]; 45 }) 46 .forEach(([command, file]) => { 47 let stat; 48 const type = typeMap[command]; 49 if (type === DELETE_EVENT) { 50 stat = null; 51 } else { 52 try { 53 stat = statSync(file); 54 } catch (e) { 55 // There is likely a delete coming down the pipe. 56 if (e.code === 'ENOENT') { 57 return; 58 } 59 throw e; 60 } 61 } 62 this.emitEvent(type, path.relative(this.root, file), stat); 63 }); 64 } 65 66 /** 67 * Export `WatchexecWatcher` class. 68 * Watches `dir`. 69 * 70 * @class WatchexecWatcher 71 * @param String dir 72 * @param {Object} opts 73 * @public 74 */ 75 class WatchexecWatcher extends EventEmitter { 76 constructor(dir, opts) { 77 super(); 78 79 common.assignOptions(this, opts); 80 81 this.root = path.resolve(dir); 82 83 this._process = execa( 84 'watchexec', 85 ['-n', '--', 'node', __dirname + '/watchexec_client.js'], 86 { cwd: dir } 87 ); 88 89 this._process.stdout.on('data', _messageHandler.bind(this)); 90 this._readyTimer = setTimeout(this.emit.bind(this, 'ready'), 1000); 91 } 92 93 close(callback) { 94 clearTimeout(this._readyTimer); 95 this.removeAllListeners(); 96 this._process && !this._process.killed && this._process.kill(); 97 if (typeof callback === 'function') { 98 setImmediate(callback.bind(null, null, true)); 99 } 100 } 101 102 /** 103 * Transform and emit an event comming from the poller. 104 * 105 * @param {EventEmitter} monitor 106 * @public 107 */ 108 emitEvent(type, file, stat) { 109 this.emit(type, file, this.root, stat); 110 this.emit(ALL_EVENT, type, file, this.root, stat); 111 } 112 } 113 114 WatchexecWatcher._messageHandler = _messageHandler; 115 116 module.exports = WatchexecWatcher;