'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = WatchmanWatcher; var _fs; function _load_fs() { return _fs = _interopRequireDefault(require('fs')); } var _path; function _load_path() { return _path = _interopRequireDefault(require('path')); } var _assert; function _load_assert() { return _assert = _interopRequireDefault(require('assert')); } var _common; function _load_common() { return _common = _interopRequireDefault(require('sane/src/common')); } var _fbWatchman; function _load_fbWatchman() { return _fbWatchman = _interopRequireDefault(require('fb-watchman')); } var _events; function _load_events() { return _events = require('events'); } var _recrawlWarningDedupe; function _load_recrawlWarningDedupe() { return _recrawlWarningDedupe = _interopRequireDefault(require('sane/src/utils/recrawl-warning-dedupe')); } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const CHANGE_EVENT = (_common || _load_common()).default.CHANGE_EVENT; /** * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ const DELETE_EVENT = (_common || _load_common()).default.DELETE_EVENT; const ADD_EVENT = (_common || _load_common()).default.ADD_EVENT; const ALL_EVENT = (_common || _load_common()).default.ALL_EVENT; const SUB_NAME = 'sane-sub'; /** * Watches `dir`. * * @class PollWatcher * @param String dir * @param {Object} opts * @public */ function WatchmanWatcher(dir, opts) { (_common || _load_common()).default.assignOptions(this, opts); this.root = (_path || _load_path()).default.resolve(dir); this.init(); } // eslint-disable-next-line no-proto WatchmanWatcher.prototype.__proto__ = (_events || _load_events()).EventEmitter.prototype; /** * Run the watchman `watch` command on the root and subscribe to changes. * * @private */ WatchmanWatcher.prototype.init = function () { if (this.client) { this.client.removeAllListeners(); } const self = this; this.client = new (_fbWatchman || _load_fbWatchman()).default.Client(); this.client.on('error', error => { self.emit('error', error); }); this.client.on('subscription', this.handleChangeEvent.bind(this)); this.client.on('end', () => { console.warn('[sane] Warning: Lost connection to watchman, reconnecting..'); self.init(); }); this.watchProjectInfo = null; function getWatchRoot() { return self.watchProjectInfo ? self.watchProjectInfo.root : self.root; } function onCapability(error, resp) { if (handleError(self, error)) { // The Watchman watcher is unusable on this system, we cannot continue return; } handleWarning(resp); self.capabilities = resp.capabilities; if (self.capabilities.relative_root) { self.client.command(['watch-project', getWatchRoot()], onWatchProject); } else { self.client.command(['watch', getWatchRoot()], onWatch); } } function onWatchProject(error, resp) { if (handleError(self, error)) { return; } handleWarning(resp); self.watchProjectInfo = { relativePath: resp.relative_path ? resp.relative_path : '', root: resp.watch }; self.client.command(['clock', getWatchRoot()], onClock); } function onWatch(error, resp) { if (handleError(self, error)) { return; } handleWarning(resp); self.client.command(['clock', getWatchRoot()], onClock); } function onClock(error, resp) { if (handleError(self, error)) { return; } handleWarning(resp); const options = { fields: ['name', 'exists', 'new'], since: resp.clock }; // If the server has the wildmatch capability available it supports // the recursive **/*.foo style match and we can offload our globs // to the watchman server. This saves both on data size to be // communicated back to us and compute for evaluating the globs // in our node process. if (self.capabilities.wildmatch) { if (self.globs.length === 0) { if (!self.dot) { // Make sure we honor the dot option if even we're not using globs. options.expression = ['match', '**', 'wholename', { includedotfiles: false }]; } } else { options.expression = ['anyof']; for (const i in self.globs) { options.expression.push(['match', self.globs[i], 'wholename', { includedotfiles: self.dot }]); } } } if (self.capabilities.relative_root) { options.relative_root = self.watchProjectInfo.relativePath; } self.client.command(['subscribe', getWatchRoot(), SUB_NAME, options], onSubscribe); } function onSubscribe(error, resp) { if (handleError(self, error)) { return; } handleWarning(resp); self.emit('ready'); } self.client.capabilityCheck({ optional: ['wildmatch', 'relative_root'] }, onCapability); }; /** * Handles a change event coming from the subscription. * * @param {Object} resp * @private */ WatchmanWatcher.prototype.handleChangeEvent = function (resp) { (_assert || _load_assert()).default.equal(resp.subscription, SUB_NAME, 'Invalid subscription event.'); if (resp.is_fresh_instance) { this.emit('fresh_instance'); } if (resp.is_fresh_instance) { this.emit('fresh_instance'); } if (Array.isArray(resp.files)) { resp.files.forEach(this.handleFileChange, this); } }; /** * Handles a single change event record. * * @param {Object} changeDescriptor * @private */ WatchmanWatcher.prototype.handleFileChange = function (changeDescriptor) { const self = this; let absPath; let relativePath; if (this.capabilities.relative_root) { relativePath = changeDescriptor.name; absPath = (_path || _load_path()).default.join(this.watchProjectInfo.root, this.watchProjectInfo.relativePath, relativePath); } else { absPath = (_path || _load_path()).default.join(this.root, changeDescriptor.name); relativePath = changeDescriptor.name; } if (!(self.capabilities.wildmatch && !this.hasIgnore) && !(_common || _load_common()).default.isFileIncluded(this.globs, this.dot, this.doIgnore, relativePath)) { return; } if (!changeDescriptor.exists) { self.emitEvent(DELETE_EVENT, relativePath, self.root); } else { (_fs || _load_fs()).default.lstat(absPath, (error, stat) => { // Files can be deleted between the event and the lstat call // the most reliable thing to do here is to ignore the event. if (error && error.code === 'ENOENT') { return; } if (handleError(self, error)) { return; } const eventType = changeDescriptor.new ? ADD_EVENT : CHANGE_EVENT; // Change event on dirs are mostly useless. if (!(eventType === CHANGE_EVENT && stat.isDirectory())) { self.emitEvent(eventType, relativePath, self.root, stat); } }); } }; /** * Dispatches the event. * * @param {string} eventType * @param {string} filepath * @param {string} root * @param {fs.Stat} stat * @private */ WatchmanWatcher.prototype.emitEvent = function (eventType, filepath, root, stat) { this.emit(eventType, filepath, root, stat); this.emit(ALL_EVENT, eventType, filepath, root, stat); }; /** * Closes the watcher. * * @param {function} callback * @private */ WatchmanWatcher.prototype.close = function (callback) { this.client.removeAllListeners(); this.client.end(); callback && callback(null, true); }; /** * Handles an error and returns true if exists. * * @param {WatchmanWatcher} self * @param {Error} error * @private */ function handleError(self, error) { if (error != null) { self.emit('error', error); return true; } else { return false; } } /** * Handles a warning in the watchman resp object. * * @param {object} resp * @private */ function handleWarning(resp) { if ('warning' in resp) { if ((_recrawlWarningDedupe || _load_recrawlWarningDedupe()).default.isRecrawlWarningDupe(resp.warning)) { return true; } console.warn(resp.warning); return true; } else { return false; } }