Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions packages/jest-haste-map/src/__tests__/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,25 @@ jest.mock('../crawlers/watchman', () =>
}),
);

const mockWatcherConstructor = jest.fn(root => {
const EventEmitter = require('events').EventEmitter;
mockEmitters[root] = new EventEmitter();
mockEmitters[root].close = jest.fn(callback => callback());
setTimeout(() => mockEmitters[root].emit('ready'), 0);
return mockEmitters[root];
});

jest.mock('sane', () => {
const watcher = jest.fn(root => {
const EventEmitter = require('events').EventEmitter;
mockEmitters[root] = new EventEmitter();
mockEmitters[root].close = jest.fn(callback => callback());
setTimeout(() => mockEmitters[root].emit('ready'), 0);
return mockEmitters[root];
});
return {
NodeWatcher: watcher,
WatchmanWatcher: watcher,
NodeWatcher: mockWatcherConstructor,
WatchmanWatcher: mockWatcherConstructor,
};
});

jest.mock('../lib/watchman_watcher.js', () => {
return mockWatcherConstructor;
});

let mockChangedFiles;
let mockFs;

Expand Down
3 changes: 2 additions & 1 deletion packages/jest-haste-map/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import getMockName from './get_mock_name';
import getPlatformExtension from './lib/get_platform_extension';
import normalizePathSep from './lib/normalize_path_sep';
import Worker from 'jest-worker';
import WatchmanWatcher from './lib/watchman_watcher';

// eslint-disable-next-line import/default
import nodeCrawl from './crawlers/node';
Expand Down Expand Up @@ -608,7 +609,7 @@ class HasteMap extends EventEmitter {

const Watcher =
canUseWatchman && this._options.useWatchman
? sane.WatchmanWatcher
? WatchmanWatcher
: os.platform() === 'darwin' ? sane.FSEventsWatcher : sane.NodeWatcher;
const extensions = this._options.extensions;
const ignorePattern = this._options.ignorePattern;
Expand Down
325 changes: 325 additions & 0 deletions packages/jest-haste-map/src/lib/watchman_watcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,325 @@
/**
* 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.
*/

import fs from 'fs';
import path from 'path';
import assert from 'assert';
import common from 'sane/src/common';
import watchman from 'fb-watchman';
import {EventEmitter} from 'events';
import RecrawlWarning from 'sane/src/utils/recrawl-warning-dedupe';

const CHANGE_EVENT = common.CHANGE_EVENT;
const DELETE_EVENT = common.DELETE_EVENT;
const ADD_EVENT = common.ADD_EVENT;
const ALL_EVENT = common.ALL_EVENT;
const SUB_NAME = 'sane-sub';

/**
* Watches `dir`.
*
* @class PollWatcher
* @param String dir
* @param {Object} opts
* @public
*/

export default function WatchmanWatcher(dir, opts) {
common.assignOptions(this, opts);
this.root = path.resolve(dir);
this.init();
}

// eslint-disable-next-line no-proto
WatchmanWatcher.prototype.__proto__ = 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 watchman.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.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.join(
this.watchProjectInfo.root,
this.watchProjectInfo.relativePath,
relativePath,
);
} else {
absPath = path.join(this.root, changeDescriptor.name);
relativePath = changeDescriptor.name;
}

if (
!(self.capabilities.wildmatch && !this.hasIgnore) &&
!common.isFileIncluded(this.globs, this.dot, this.doIgnore, relativePath)
) {
return;
}

if (!changeDescriptor.exists) {
self.emitEvent(DELETE_EVENT, relativePath, self.root);
} else {
fs.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 (RecrawlWarning.isRecrawlWarningDupe(resp.warning)) {
return true;
}
console.warn(resp.warning);
return true;
} else {
return false;
}
}
2 changes: 1 addition & 1 deletion packages/jest-util/src/create_process_object.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ function createProcessEnv() {

const proxy = new Proxy(real, {
get(target, key) {
if ((typeof key === 'string') && (process.platform === 'win32')) {
if (typeof key === 'string' && process.platform === 'win32') {
return lookup[key in proto ? key : key.toLowerCase()];
} else {
return real[key];
Expand Down