386 lines
12 KiB
JavaScript
386 lines
12 KiB
JavaScript
'use strict';
|
|
|
|
function path() {
|
|
const data = _interopRequireWildcard(require('path'));
|
|
|
|
path = function () {
|
|
return data;
|
|
};
|
|
|
|
return data;
|
|
}
|
|
|
|
function _fbWatchman() {
|
|
const data = _interopRequireDefault(require('fb-watchman'));
|
|
|
|
_fbWatchman = function () {
|
|
return data;
|
|
};
|
|
|
|
return data;
|
|
}
|
|
|
|
var _constants = _interopRequireDefault(require('../constants'));
|
|
|
|
var fastPath = _interopRequireWildcard(require('../lib/fast_path'));
|
|
|
|
var _normalizePathSep = _interopRequireDefault(
|
|
require('../lib/normalizePathSep')
|
|
);
|
|
|
|
function _interopRequireDefault(obj) {
|
|
return obj && obj.__esModule ? obj : {default: obj};
|
|
}
|
|
|
|
function _getRequireWildcardCache(nodeInterop) {
|
|
if (typeof WeakMap !== 'function') return null;
|
|
var cacheBabelInterop = new WeakMap();
|
|
var cacheNodeInterop = new WeakMap();
|
|
return (_getRequireWildcardCache = function (nodeInterop) {
|
|
return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
|
|
})(nodeInterop);
|
|
}
|
|
|
|
function _interopRequireWildcard(obj, nodeInterop) {
|
|
if (!nodeInterop && obj && obj.__esModule) {
|
|
return obj;
|
|
}
|
|
if (obj === null || (typeof obj !== 'object' && typeof obj !== 'function')) {
|
|
return {default: obj};
|
|
}
|
|
var cache = _getRequireWildcardCache(nodeInterop);
|
|
if (cache && cache.has(obj)) {
|
|
return cache.get(obj);
|
|
}
|
|
var newObj = {};
|
|
var hasPropertyDescriptor =
|
|
Object.defineProperty && Object.getOwnPropertyDescriptor;
|
|
for (var key in obj) {
|
|
if (key !== 'default' && Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
var desc = hasPropertyDescriptor
|
|
? Object.getOwnPropertyDescriptor(obj, key)
|
|
: null;
|
|
if (desc && (desc.get || desc.set)) {
|
|
Object.defineProperty(newObj, key, desc);
|
|
} else {
|
|
newObj[key] = obj[key];
|
|
}
|
|
}
|
|
}
|
|
newObj.default = obj;
|
|
if (cache) {
|
|
cache.set(obj, newObj);
|
|
}
|
|
return newObj;
|
|
}
|
|
|
|
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates. 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 watchmanURL = 'https://facebook.github.io/watchman/docs/troubleshooting';
|
|
|
|
function WatchmanError(error) {
|
|
error.message =
|
|
`Watchman error: ${error.message.trim()}. Make sure watchman ` +
|
|
`is running for this project. See ${watchmanURL}.`;
|
|
return error;
|
|
}
|
|
/**
|
|
* Wrap watchman capabilityCheck method as a promise.
|
|
*
|
|
* @param client watchman client
|
|
* @param caps capabilities to verify
|
|
* @returns a promise resolving to a list of verified capabilities
|
|
*/
|
|
|
|
async function capabilityCheck(client, caps) {
|
|
return new Promise((resolve, reject) => {
|
|
client.capabilityCheck(
|
|
// @ts-expect-error: incorrectly typed
|
|
caps,
|
|
(error, response) => {
|
|
if (error) {
|
|
reject(error);
|
|
} else {
|
|
resolve(response);
|
|
}
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
module.exports = async function watchmanCrawl(options) {
|
|
const fields = ['name', 'exists', 'mtime_ms', 'size'];
|
|
const {data, extensions, ignore, rootDir, roots} = options;
|
|
const defaultWatchExpression = ['allof', ['type', 'f']];
|
|
const clocks = data.clocks;
|
|
const client = new (_fbWatchman().default.Client)(); // https://facebook.github.io/watchman/docs/capabilities.html
|
|
// Check adds about ~28ms
|
|
|
|
const capabilities = await capabilityCheck(client, {
|
|
// If a required capability is missing then an error will be thrown,
|
|
// we don't need this assertion, so using optional instead.
|
|
optional: ['suffix-set']
|
|
});
|
|
|
|
if (
|
|
capabilities !== null &&
|
|
capabilities !== void 0 &&
|
|
capabilities.capabilities['suffix-set']
|
|
) {
|
|
// If available, use the optimized `suffix-set` operation:
|
|
// https://facebook.github.io/watchman/docs/expr/suffix.html#suffix-set
|
|
defaultWatchExpression.push(['suffix', extensions]);
|
|
} else {
|
|
// Otherwise use the older and less optimal suffix tuple array
|
|
defaultWatchExpression.push([
|
|
'anyof',
|
|
...extensions.map(extension => ['suffix', extension])
|
|
]);
|
|
}
|
|
|
|
let clientError;
|
|
client.on('error', error => (clientError = WatchmanError(error)));
|
|
|
|
const cmd = (...args) =>
|
|
new Promise((resolve, reject) =>
|
|
client.command(args, (error, result) =>
|
|
error ? reject(WatchmanError(error)) : resolve(result)
|
|
)
|
|
);
|
|
|
|
if (options.computeSha1) {
|
|
const {capabilities} = await cmd('list-capabilities');
|
|
|
|
if (capabilities.indexOf('field-content.sha1hex') !== -1) {
|
|
fields.push('content.sha1hex');
|
|
}
|
|
}
|
|
|
|
async function getWatchmanRoots(roots) {
|
|
const watchmanRoots = new Map();
|
|
await Promise.all(
|
|
roots.map(async root => {
|
|
const response = await cmd('watch-project', root);
|
|
const existing = watchmanRoots.get(response.watch); // A root can only be filtered if it was never seen with a
|
|
// relative_path before.
|
|
|
|
const canBeFiltered = !existing || existing.length > 0;
|
|
|
|
if (canBeFiltered) {
|
|
if (response.relative_path) {
|
|
watchmanRoots.set(
|
|
response.watch,
|
|
(existing || []).concat(response.relative_path)
|
|
);
|
|
} else {
|
|
// Make the filter directories an empty array to signal that this
|
|
// root was already seen and needs to be watched for all files or
|
|
// directories.
|
|
watchmanRoots.set(response.watch, []);
|
|
}
|
|
}
|
|
})
|
|
);
|
|
return watchmanRoots;
|
|
}
|
|
|
|
async function queryWatchmanForDirs(rootProjectDirMappings) {
|
|
const results = new Map();
|
|
let isFresh = false;
|
|
await Promise.all(
|
|
Array.from(rootProjectDirMappings).map(
|
|
async ([root, directoryFilters]) => {
|
|
var _since$scm;
|
|
|
|
const expression = Array.from(defaultWatchExpression);
|
|
const glob = [];
|
|
|
|
if (directoryFilters.length > 0) {
|
|
expression.push([
|
|
'anyof',
|
|
...directoryFilters.map(dir => ['dirname', dir])
|
|
]);
|
|
|
|
for (const directory of directoryFilters) {
|
|
for (const extension of extensions) {
|
|
glob.push(`${directory}/**/*.${extension}`);
|
|
}
|
|
}
|
|
} else {
|
|
for (const extension of extensions) {
|
|
glob.push(`**/*.${extension}`);
|
|
}
|
|
} // Jest is only going to store one type of clock; a string that
|
|
// represents a local clock. However, the Watchman crawler supports
|
|
// a second type of clock that can be written by automation outside of
|
|
// Jest, called an "scm query", which fetches changed files based on
|
|
// source control mergebases. The reason this is necessary is because
|
|
// local clocks are not portable across systems, but scm queries are.
|
|
// By using scm queries, we can create the haste map on a different
|
|
// system and import it, transforming the clock into a local clock.
|
|
|
|
const since = clocks.get(fastPath.relative(rootDir, root));
|
|
const query =
|
|
since !== undefined // Use the `since` generator if we have a clock available
|
|
? {
|
|
expression,
|
|
fields,
|
|
since
|
|
} // Otherwise use the `glob` filter
|
|
: {
|
|
expression,
|
|
fields,
|
|
glob,
|
|
glob_includedotfiles: true
|
|
};
|
|
const response = await cmd('query', root, query);
|
|
|
|
if ('warning' in response) {
|
|
console.warn('watchman warning: ', response.warning);
|
|
} // When a source-control query is used, we ignore the "is fresh"
|
|
// response from Watchman because it will be true despite the query
|
|
// being incremental.
|
|
|
|
const isSourceControlQuery =
|
|
typeof since !== 'string' &&
|
|
(since === null || since === void 0
|
|
? void 0
|
|
: (_since$scm = since.scm) === null || _since$scm === void 0
|
|
? void 0
|
|
: _since$scm['mergebase-with']) !== undefined;
|
|
|
|
if (!isSourceControlQuery) {
|
|
isFresh = isFresh || response.is_fresh_instance;
|
|
}
|
|
|
|
results.set(root, response);
|
|
}
|
|
)
|
|
);
|
|
return {
|
|
isFresh,
|
|
results
|
|
};
|
|
}
|
|
|
|
let files = data.files;
|
|
let removedFiles = new Map();
|
|
const changedFiles = new Map();
|
|
let results;
|
|
let isFresh = false;
|
|
|
|
try {
|
|
const watchmanRoots = await getWatchmanRoots(roots);
|
|
const watchmanFileResults = await queryWatchmanForDirs(watchmanRoots); // Reset the file map if watchman was restarted and sends us a list of
|
|
// files.
|
|
|
|
if (watchmanFileResults.isFresh) {
|
|
files = new Map();
|
|
removedFiles = new Map(data.files);
|
|
isFresh = true;
|
|
}
|
|
|
|
results = watchmanFileResults.results;
|
|
} finally {
|
|
client.end();
|
|
}
|
|
|
|
if (clientError) {
|
|
throw clientError;
|
|
}
|
|
|
|
for (const [watchRoot, response] of results) {
|
|
const fsRoot = (0, _normalizePathSep.default)(watchRoot);
|
|
const relativeFsRoot = fastPath.relative(rootDir, fsRoot);
|
|
clocks.set(
|
|
relativeFsRoot, // Ensure we persist only the local clock.
|
|
typeof response.clock === 'string' ? response.clock : response.clock.clock
|
|
);
|
|
|
|
for (const fileData of response.files) {
|
|
const filePath =
|
|
fsRoot + path().sep + (0, _normalizePathSep.default)(fileData.name);
|
|
const relativeFilePath = fastPath.relative(rootDir, filePath);
|
|
const existingFileData = data.files.get(relativeFilePath); // If watchman is fresh, the removed files map starts with all files
|
|
// and we remove them as we verify they still exist.
|
|
|
|
if (isFresh && existingFileData && fileData.exists) {
|
|
removedFiles.delete(relativeFilePath);
|
|
}
|
|
|
|
if (!fileData.exists) {
|
|
// No need to act on files that do not exist and were not tracked.
|
|
if (existingFileData) {
|
|
files.delete(relativeFilePath); // If watchman is not fresh, we will know what specific files were
|
|
// deleted since we last ran and can track only those files.
|
|
|
|
if (!isFresh) {
|
|
removedFiles.set(relativeFilePath, existingFileData);
|
|
}
|
|
}
|
|
} else if (!ignore(filePath)) {
|
|
const mtime =
|
|
typeof fileData.mtime_ms === 'number'
|
|
? fileData.mtime_ms
|
|
: fileData.mtime_ms.toNumber();
|
|
const size = fileData.size;
|
|
let sha1hex = fileData['content.sha1hex'];
|
|
|
|
if (typeof sha1hex !== 'string' || sha1hex.length !== 40) {
|
|
sha1hex = undefined;
|
|
}
|
|
|
|
let nextData;
|
|
|
|
if (
|
|
existingFileData &&
|
|
existingFileData[_constants.default.MTIME] === mtime
|
|
) {
|
|
nextData = existingFileData;
|
|
} else if (
|
|
existingFileData &&
|
|
sha1hex &&
|
|
existingFileData[_constants.default.SHA1] === sha1hex
|
|
) {
|
|
nextData = [
|
|
existingFileData[0],
|
|
mtime,
|
|
existingFileData[2],
|
|
existingFileData[3],
|
|
existingFileData[4],
|
|
existingFileData[5]
|
|
];
|
|
} else {
|
|
var _sha1hex;
|
|
|
|
// See ../constants.ts
|
|
nextData = [
|
|
'',
|
|
mtime,
|
|
size,
|
|
0,
|
|
'',
|
|
(_sha1hex = sha1hex) !== null && _sha1hex !== void 0
|
|
? _sha1hex
|
|
: null
|
|
];
|
|
}
|
|
|
|
files.set(relativeFilePath, nextData);
|
|
changedFiles.set(relativeFilePath, nextData);
|
|
}
|
|
}
|
|
}
|
|
|
|
data.files = files;
|
|
return {
|
|
changedFiles: isFresh ? undefined : changedFiles,
|
|
hasteMap: data,
|
|
removedFiles
|
|
};
|
|
};
|