/ cloudformation-templates / node_modules / sane / src / watchman_watcher.js
watchman_watcher.js
  1  'use strict';
  2  
  3  const fs = require('fs');
  4  const path = require('path');
  5  const common = require('./common');
  6  const watchmanClient = require('./watchman_client');
  7  const EventEmitter = require('events').EventEmitter;
  8  const RecrawlWarning = require('./utils/recrawl-warning-dedupe');
  9  
 10  /**
 11   * Constants
 12   */
 13  
 14  const CHANGE_EVENT = common.CHANGE_EVENT;
 15  const DELETE_EVENT = common.DELETE_EVENT;
 16  const ADD_EVENT = common.ADD_EVENT;
 17  const ALL_EVENT = common.ALL_EVENT;
 18  
 19  /**
 20   * Export `WatchmanWatcher` class.
 21   */
 22  
 23  module.exports = WatchmanWatcher;
 24  
 25  /**
 26   * Watches `dir`.
 27   *
 28   * @class WatchmanWatcher
 29   * @param String dir
 30   * @param {Object} opts
 31   * @public
 32   */
 33  
 34  function WatchmanWatcher(dir, opts) {
 35    common.assignOptions(this, opts);
 36    this.root = path.resolve(dir);
 37    this._init();
 38  }
 39  
 40  WatchmanWatcher.prototype.__proto__ = EventEmitter.prototype;
 41  
 42  /**
 43   * Run the watchman `watch` command on the root and subscribe to changes.
 44   *
 45   * @private
 46   */
 47  WatchmanWatcher.prototype._init = function() {
 48    if (this._client) {
 49      this._client = null;
 50    }
 51  
 52    // Get the WatchmanClient instance corresponding to our watchmanPath (or nothing).
 53    // Then subscribe, which will do the appropriate setup so that we will receive
 54    // calls to handleChangeEvent when files change.
 55    this._client = watchmanClient.getInstance(this.watchmanPath);
 56  
 57    return this._client.subscribe(this, this.root).then(
 58      resp => {
 59        this._handleWarning(resp);
 60        this.emit('ready');
 61      },
 62      error => {
 63        this._handleError(error);
 64      }
 65    );
 66  };
 67  
 68  /**
 69   * Called by WatchmanClient to create the options, either during initial 'subscribe'
 70   * or to resubscribe after a disconnect+reconnect. Note that we are leaving out
 71   * the watchman 'since' and 'relative_root' options, which are handled inside the
 72   * WatchmanClient.
 73   */
 74  WatchmanWatcher.prototype.createOptions = function() {
 75    let options = {
 76      fields: ['name', 'exists', 'new'],
 77    };
 78  
 79    // If the server has the wildmatch capability available it supports
 80    // the recursive **/*.foo style match and we can offload our globs
 81    // to the watchman server.  This saves both on data size to be
 82    // communicated back to us and compute for evaluating the globs
 83    // in our node process.
 84    if (this._client.wildmatch) {
 85      if (this.globs.length === 0) {
 86        if (!this.dot) {
 87          // Make sure we honor the dot option if even we're not using globs.
 88          options.expression = [
 89            'match',
 90            '**',
 91            'wholename',
 92            {
 93              includedotfiles: false,
 94            },
 95          ];
 96        }
 97      } else {
 98        options.expression = ['anyof'];
 99        for (let i in this.globs) {
100          options.expression.push([
101            'match',
102            this.globs[i],
103            'wholename',
104            {
105              includedotfiles: this.dot,
106            },
107          ]);
108        }
109      }
110    }
111  
112    return options;
113  };
114  
115  /**
116   * Called by WatchmanClient when it receives an error from the watchman daemon.
117   *
118   * @param {Object} resp
119   */
120  WatchmanWatcher.prototype.handleErrorEvent = function(error) {
121    this.emit('error', error);
122  };
123  
124  /**
125   * Called by the WatchmanClient when it is notified about a file change in
126   * the tree for this particular watcher's root.
127   *
128   * @param {Object} resp
129   * @private
130   */
131  
132  WatchmanWatcher.prototype.handleChangeEvent = function(resp) {
133    if (Array.isArray(resp.files)) {
134      resp.files.forEach(this.handleFileChange, this);
135    }
136  };
137  
138  /**
139   * Handles a single change event record.
140   *
141   * @param {Object} changeDescriptor
142   * @private
143   */
144  
145  WatchmanWatcher.prototype.handleFileChange = function(changeDescriptor) {
146    let absPath;
147    let relativePath;
148  
149    relativePath = changeDescriptor.name;
150    absPath = path.join(this.root, relativePath);
151  
152    if (
153      !(this._client.wildmatch && !this.hasIgnore) &&
154      !common.isFileIncluded(this.globs, this.dot, this.doIgnore, relativePath)
155    ) {
156      return;
157    }
158  
159    if (!changeDescriptor.exists) {
160      this.emitEvent(DELETE_EVENT, relativePath, this.root);
161    } else {
162      fs.lstat(absPath, (error, stat) => {
163        // Files can be deleted between the event and the lstat call
164        // the most reliable thing to do here is to ignore the event.
165        if (error && error.code === 'ENOENT') {
166          return;
167        }
168  
169        if (this._handleError(error)) {
170          return;
171        }
172  
173        let eventType = changeDescriptor.new ? ADD_EVENT : CHANGE_EVENT;
174  
175        // Change event on dirs are mostly useless.
176        if (!(eventType === CHANGE_EVENT && stat.isDirectory())) {
177          this.emitEvent(eventType, relativePath, this.root, stat);
178        }
179      });
180    }
181  };
182  
183  /**
184   * Dispatches an event.
185   *
186   * @param {string} eventType
187   * @param {string} filepath
188   * @param {string} root
189   * @param {fs.Stat} stat
190   * @private
191   */
192  
193  WatchmanWatcher.prototype.emitEvent = function(
194    eventType,
195    filepath,
196    root,
197    stat
198  ) {
199    this.emit(eventType, filepath, root, stat);
200    this.emit(ALL_EVENT, eventType, filepath, root, stat);
201  };
202  
203  /**
204   * Closes the watcher.
205   *
206   * @param {function} callback
207   * @private
208   */
209  
210  WatchmanWatcher.prototype.close = function(callback) {
211    this._client.closeWatcher(this);
212    callback && callback(null, true);
213  };
214  
215  /**
216   * Handles an error and returns true if exists.
217   *
218   * @param {WatchmanWatcher} self
219   * @param {Error} error
220   * @private
221   */
222  
223  WatchmanWatcher.prototype._handleError = function(error) {
224    if (error != null) {
225      this.emit('error', error);
226      return true;
227    } else {
228      return false;
229    }
230  };
231  
232  /**
233   * Handles a warning in the watchman resp object.
234   *
235   * @param {object} resp
236   * @private
237   */
238  
239  WatchmanWatcher.prototype._handleWarning = function(resp) {
240    if ('warning' in resp) {
241      if (RecrawlWarning.isRecrawlWarningDupe(resp.warning)) {
242        return true;
243      }
244      console.warn(resp.warning);
245      return true;
246    } else {
247      return false;
248    }
249  };