watchman.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. 'use strict';
  2. function path() {
  3. const data = _interopRequireWildcard(require('path'));
  4. path = function () {
  5. return data;
  6. };
  7. return data;
  8. }
  9. function _fbWatchman() {
  10. const data = _interopRequireDefault(require('fb-watchman'));
  11. _fbWatchman = function () {
  12. return data;
  13. };
  14. return data;
  15. }
  16. var _constants = _interopRequireDefault(require('../constants'));
  17. var fastPath = _interopRequireWildcard(require('../lib/fast_path'));
  18. var _normalizePathSep = _interopRequireDefault(
  19. require('../lib/normalizePathSep')
  20. );
  21. function _interopRequireDefault(obj) {
  22. return obj && obj.__esModule ? obj : {default: obj};
  23. }
  24. function _getRequireWildcardCache(nodeInterop) {
  25. if (typeof WeakMap !== 'function') return null;
  26. var cacheBabelInterop = new WeakMap();
  27. var cacheNodeInterop = new WeakMap();
  28. return (_getRequireWildcardCache = function (nodeInterop) {
  29. return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
  30. })(nodeInterop);
  31. }
  32. function _interopRequireWildcard(obj, nodeInterop) {
  33. if (!nodeInterop && obj && obj.__esModule) {
  34. return obj;
  35. }
  36. if (obj === null || (typeof obj !== 'object' && typeof obj !== 'function')) {
  37. return {default: obj};
  38. }
  39. var cache = _getRequireWildcardCache(nodeInterop);
  40. if (cache && cache.has(obj)) {
  41. return cache.get(obj);
  42. }
  43. var newObj = {};
  44. var hasPropertyDescriptor =
  45. Object.defineProperty && Object.getOwnPropertyDescriptor;
  46. for (var key in obj) {
  47. if (key !== 'default' && Object.prototype.hasOwnProperty.call(obj, key)) {
  48. var desc = hasPropertyDescriptor
  49. ? Object.getOwnPropertyDescriptor(obj, key)
  50. : null;
  51. if (desc && (desc.get || desc.set)) {
  52. Object.defineProperty(newObj, key, desc);
  53. } else {
  54. newObj[key] = obj[key];
  55. }
  56. }
  57. }
  58. newObj.default = obj;
  59. if (cache) {
  60. cache.set(obj, newObj);
  61. }
  62. return newObj;
  63. }
  64. /**
  65. * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
  66. *
  67. * This source code is licensed under the MIT license found in the
  68. * LICENSE file in the root directory of this source tree.
  69. */
  70. const watchmanURL = 'https://facebook.github.io/watchman/docs/troubleshooting';
  71. function WatchmanError(error) {
  72. error.message =
  73. `Watchman error: ${error.message.trim()}. Make sure watchman ` +
  74. `is running for this project. See ${watchmanURL}.`;
  75. return error;
  76. }
  77. /**
  78. * Wrap watchman capabilityCheck method as a promise.
  79. *
  80. * @param client watchman client
  81. * @param caps capabilities to verify
  82. * @returns a promise resolving to a list of verified capabilities
  83. */
  84. async function capabilityCheck(client, caps) {
  85. return new Promise((resolve, reject) => {
  86. client.capabilityCheck(
  87. // @ts-expect-error: incorrectly typed
  88. caps,
  89. (error, response) => {
  90. if (error) {
  91. reject(error);
  92. } else {
  93. resolve(response);
  94. }
  95. }
  96. );
  97. });
  98. }
  99. module.exports = async function watchmanCrawl(options) {
  100. const fields = ['name', 'exists', 'mtime_ms', 'size'];
  101. const {data, extensions, ignore, rootDir, roots} = options;
  102. const defaultWatchExpression = ['allof', ['type', 'f']];
  103. const clocks = data.clocks;
  104. const client = new (_fbWatchman().default.Client)(); // https://facebook.github.io/watchman/docs/capabilities.html
  105. // Check adds about ~28ms
  106. const capabilities = await capabilityCheck(client, {
  107. // If a required capability is missing then an error will be thrown,
  108. // we don't need this assertion, so using optional instead.
  109. optional: ['suffix-set']
  110. });
  111. if (
  112. capabilities !== null &&
  113. capabilities !== void 0 &&
  114. capabilities.capabilities['suffix-set']
  115. ) {
  116. // If available, use the optimized `suffix-set` operation:
  117. // https://facebook.github.io/watchman/docs/expr/suffix.html#suffix-set
  118. defaultWatchExpression.push(['suffix', extensions]);
  119. } else {
  120. // Otherwise use the older and less optimal suffix tuple array
  121. defaultWatchExpression.push([
  122. 'anyof',
  123. ...extensions.map(extension => ['suffix', extension])
  124. ]);
  125. }
  126. let clientError;
  127. client.on('error', error => (clientError = WatchmanError(error)));
  128. const cmd = (...args) =>
  129. new Promise((resolve, reject) =>
  130. client.command(args, (error, result) =>
  131. error ? reject(WatchmanError(error)) : resolve(result)
  132. )
  133. );
  134. if (options.computeSha1) {
  135. const {capabilities} = await cmd('list-capabilities');
  136. if (capabilities.indexOf('field-content.sha1hex') !== -1) {
  137. fields.push('content.sha1hex');
  138. }
  139. }
  140. async function getWatchmanRoots(roots) {
  141. const watchmanRoots = new Map();
  142. await Promise.all(
  143. roots.map(async root => {
  144. const response = await cmd('watch-project', root);
  145. const existing = watchmanRoots.get(response.watch); // A root can only be filtered if it was never seen with a
  146. // relative_path before.
  147. const canBeFiltered = !existing || existing.length > 0;
  148. if (canBeFiltered) {
  149. if (response.relative_path) {
  150. watchmanRoots.set(
  151. response.watch,
  152. (existing || []).concat(response.relative_path)
  153. );
  154. } else {
  155. // Make the filter directories an empty array to signal that this
  156. // root was already seen and needs to be watched for all files or
  157. // directories.
  158. watchmanRoots.set(response.watch, []);
  159. }
  160. }
  161. })
  162. );
  163. return watchmanRoots;
  164. }
  165. async function queryWatchmanForDirs(rootProjectDirMappings) {
  166. const results = new Map();
  167. let isFresh = false;
  168. await Promise.all(
  169. Array.from(rootProjectDirMappings).map(
  170. async ([root, directoryFilters]) => {
  171. var _since$scm;
  172. const expression = Array.from(defaultWatchExpression);
  173. const glob = [];
  174. if (directoryFilters.length > 0) {
  175. expression.push([
  176. 'anyof',
  177. ...directoryFilters.map(dir => ['dirname', dir])
  178. ]);
  179. for (const directory of directoryFilters) {
  180. for (const extension of extensions) {
  181. glob.push(`${directory}/**/*.${extension}`);
  182. }
  183. }
  184. } else {
  185. for (const extension of extensions) {
  186. glob.push(`**/*.${extension}`);
  187. }
  188. } // Jest is only going to store one type of clock; a string that
  189. // represents a local clock. However, the Watchman crawler supports
  190. // a second type of clock that can be written by automation outside of
  191. // Jest, called an "scm query", which fetches changed files based on
  192. // source control mergebases. The reason this is necessary is because
  193. // local clocks are not portable across systems, but scm queries are.
  194. // By using scm queries, we can create the haste map on a different
  195. // system and import it, transforming the clock into a local clock.
  196. const since = clocks.get(fastPath.relative(rootDir, root));
  197. const query =
  198. since !== undefined // Use the `since` generator if we have a clock available
  199. ? {
  200. expression,
  201. fields,
  202. since
  203. } // Otherwise use the `glob` filter
  204. : {
  205. expression,
  206. fields,
  207. glob,
  208. glob_includedotfiles: true
  209. };
  210. const response = await cmd('query', root, query);
  211. if ('warning' in response) {
  212. console.warn('watchman warning: ', response.warning);
  213. } // When a source-control query is used, we ignore the "is fresh"
  214. // response from Watchman because it will be true despite the query
  215. // being incremental.
  216. const isSourceControlQuery =
  217. typeof since !== 'string' &&
  218. (since === null || since === void 0
  219. ? void 0
  220. : (_since$scm = since.scm) === null || _since$scm === void 0
  221. ? void 0
  222. : _since$scm['mergebase-with']) !== undefined;
  223. if (!isSourceControlQuery) {
  224. isFresh = isFresh || response.is_fresh_instance;
  225. }
  226. results.set(root, response);
  227. }
  228. )
  229. );
  230. return {
  231. isFresh,
  232. results
  233. };
  234. }
  235. let files = data.files;
  236. let removedFiles = new Map();
  237. const changedFiles = new Map();
  238. let results;
  239. let isFresh = false;
  240. try {
  241. const watchmanRoots = await getWatchmanRoots(roots);
  242. const watchmanFileResults = await queryWatchmanForDirs(watchmanRoots); // Reset the file map if watchman was restarted and sends us a list of
  243. // files.
  244. if (watchmanFileResults.isFresh) {
  245. files = new Map();
  246. removedFiles = new Map(data.files);
  247. isFresh = true;
  248. }
  249. results = watchmanFileResults.results;
  250. } finally {
  251. client.end();
  252. }
  253. if (clientError) {
  254. throw clientError;
  255. }
  256. for (const [watchRoot, response] of results) {
  257. const fsRoot = (0, _normalizePathSep.default)(watchRoot);
  258. const relativeFsRoot = fastPath.relative(rootDir, fsRoot);
  259. clocks.set(
  260. relativeFsRoot, // Ensure we persist only the local clock.
  261. typeof response.clock === 'string' ? response.clock : response.clock.clock
  262. );
  263. for (const fileData of response.files) {
  264. const filePath =
  265. fsRoot + path().sep + (0, _normalizePathSep.default)(fileData.name);
  266. const relativeFilePath = fastPath.relative(rootDir, filePath);
  267. const existingFileData = data.files.get(relativeFilePath); // If watchman is fresh, the removed files map starts with all files
  268. // and we remove them as we verify they still exist.
  269. if (isFresh && existingFileData && fileData.exists) {
  270. removedFiles.delete(relativeFilePath);
  271. }
  272. if (!fileData.exists) {
  273. // No need to act on files that do not exist and were not tracked.
  274. if (existingFileData) {
  275. files.delete(relativeFilePath); // If watchman is not fresh, we will know what specific files were
  276. // deleted since we last ran and can track only those files.
  277. if (!isFresh) {
  278. removedFiles.set(relativeFilePath, existingFileData);
  279. }
  280. }
  281. } else if (!ignore(filePath)) {
  282. const mtime =
  283. typeof fileData.mtime_ms === 'number'
  284. ? fileData.mtime_ms
  285. : fileData.mtime_ms.toNumber();
  286. const size = fileData.size;
  287. let sha1hex = fileData['content.sha1hex'];
  288. if (typeof sha1hex !== 'string' || sha1hex.length !== 40) {
  289. sha1hex = undefined;
  290. }
  291. let nextData;
  292. if (
  293. existingFileData &&
  294. existingFileData[_constants.default.MTIME] === mtime
  295. ) {
  296. nextData = existingFileData;
  297. } else if (
  298. existingFileData &&
  299. sha1hex &&
  300. existingFileData[_constants.default.SHA1] === sha1hex
  301. ) {
  302. nextData = [
  303. existingFileData[0],
  304. mtime,
  305. existingFileData[2],
  306. existingFileData[3],
  307. existingFileData[4],
  308. existingFileData[5]
  309. ];
  310. } else {
  311. var _sha1hex;
  312. // See ../constants.ts
  313. nextData = [
  314. '',
  315. mtime,
  316. size,
  317. 0,
  318. '',
  319. (_sha1hex = sha1hex) !== null && _sha1hex !== void 0
  320. ? _sha1hex
  321. : null
  322. ];
  323. }
  324. files.set(relativeFilePath, nextData);
  325. changedFiles.set(relativeFilePath, nextData);
  326. }
  327. }
  328. }
  329. data.files = files;
  330. return {
  331. changedFiles: isFresh ? undefined : changedFiles,
  332. hasteMap: data,
  333. removedFiles
  334. };
  335. };