watchman.js 11 KB

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