UNPKG

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