metro-file-map 0.72.3 → 0.73.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metro-file-map",
3
- "version": "0.72.3",
3
+ "version": "0.73.0",
4
4
  "description": "[Experimental] - 🚇 File crawling, watching and mapping for Metro",
5
5
  "main": "src/index.js",
6
6
  "repository": {
@@ -30,6 +30,6 @@
30
30
  "slash": "^3.0.0"
31
31
  },
32
32
  "optionalDependencies": {
33
- "fsevents": "^2.1.2"
33
+ "fsevents": "^2.3.2"
34
34
  }
35
35
  }
package/src/ModuleMap.js CHANGED
@@ -63,6 +63,7 @@ function _interopRequireDefault(obj) {
63
63
  *
64
64
  *
65
65
  * @format
66
+ * @oncall react_native
66
67
  */
67
68
  const EMPTY_OBJ = {};
68
69
  const EMPTY_MAP = new Map();
@@ -6,6 +6,7 @@
6
6
  *
7
7
  * @flow strict-local
8
8
  * @format
9
+ * @oncall react_native
9
10
  */
10
11
 
11
12
  import type {
@@ -29,6 +29,7 @@ function _interopRequireDefault(obj) {
29
29
  *
30
30
  *
31
31
  * @format
32
+ * @oncall react_native
32
33
  */
33
34
  const DEFAULT_PREFIX = "metro-file-map";
34
35
  const DEFAULT_DIRECTORY = (0, _os.tmpdir)();
@@ -66,9 +67,14 @@ class DiskCacheManager {
66
67
  return (0, _v.deserialize)(
67
68
  (0, _gracefulFs.readFileSync)(this._cachePath)
68
69
  );
69
- } catch {}
70
+ } catch (e) {
71
+ if ((e === null || e === void 0 ? void 0 : e.code) === "ENOENT") {
72
+ // Cache file not found - not considered an error.
73
+ return null;
74
+ } // Rethrow anything else.
70
75
 
71
- return null;
76
+ throw e;
77
+ }
72
78
  }
73
79
 
74
80
  async write(dataSnapshot, { changed, removed }) {
@@ -6,6 +6,7 @@
6
6
  *
7
7
  * @flow strict-local
8
8
  * @format
9
+ * @oncall react_native
9
10
  */
10
11
 
11
12
  import type {
@@ -68,8 +69,14 @@ export class DiskCacheManager implements CacheManager {
68
69
  async read(): Promise<?InternalData> {
69
70
  try {
70
71
  return deserialize(readFileSync(this._cachePath));
71
- } catch {}
72
- return null;
72
+ } catch (e) {
73
+ if (e?.code === 'ENOENT') {
74
+ // Cache file not found - not considered an error.
75
+ return null;
76
+ }
77
+ // Rethrow anything else.
78
+ throw e;
79
+ }
73
80
  }
74
81
 
75
82
  async write(
@@ -64,6 +64,7 @@ function _interopRequireDefault(obj) {
64
64
  *
65
65
  *
66
66
  * @format
67
+ * @oncall react_native
67
68
  */
68
69
  async function hasNativeFindSupport(forceNodeFilesystemAPI) {
69
70
  if (forceNodeFilesystemAPI) {
@@ -6,6 +6,7 @@
6
6
  *
7
7
  * @flow strict-local
8
8
  * @format
9
+ * @oncall react_native
9
10
  */
10
11
 
11
12
  import type {
@@ -1,13 +1,17 @@
1
1
  "use strict";
2
2
 
3
- var _constants = _interopRequireDefault(require("../constants"));
3
+ var _planQuery = require("./planQuery");
4
4
 
5
- var fastPath = _interopRequireWildcard(require("../lib/fast_path"));
5
+ var _constants = _interopRequireDefault(require("../../constants"));
6
+
7
+ var fastPath = _interopRequireWildcard(require("../../lib/fast_path"));
6
8
 
7
9
  var _normalizePathSep = _interopRequireDefault(
8
- require("../lib/normalizePathSep")
10
+ require("../../lib/normalizePathSep")
9
11
  );
10
12
 
13
+ var _invariant = _interopRequireDefault(require("invariant"));
14
+
11
15
  var path = _interopRequireWildcard(require("path"));
12
16
 
13
17
  function _getRequireWildcardCache(nodeInterop) {
@@ -64,8 +68,9 @@ function _interopRequireDefault(obj) {
64
68
  *
65
69
  *
66
70
  * @format
71
+ * @oncall react_native
67
72
  */
68
- const watchman = require("fb-watchman"); // $FlowFixMe[unclear-type] - Improve fb-watchman types to cover our uses
73
+ const watchman = require("fb-watchman");
69
74
 
70
75
  const WATCHMAN_WARNING_INITIAL_DELAY_MILLISECONDS = 10000;
71
76
  const WATCHMAN_WARNING_INTERVAL_MILLISECONDS = 20000;
@@ -77,68 +82,30 @@ function makeWatchmanError(error) {
77
82
  `is running for this project. See ${watchmanURL}.`;
78
83
  return error;
79
84
  }
80
- /**
81
- * Wrap watchman capabilityCheck method as a promise.
82
- *
83
- * @param client watchman client
84
- * @param caps capabilities to verify
85
- * @returns a promise resolving to a list of verified capabilities
86
- */
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 != null || response == null) {
95
- reject(
96
- error !== null && error !== void 0
97
- ? error
98
- : new Error("capabilityCheck: Response missing")
99
- );
100
- } else {
101
- resolve(response);
102
- }
103
- }
104
- );
105
- });
106
- }
107
85
 
108
- module.exports = async function watchmanCrawl(options) {
109
- var _options$abortSignal;
110
-
111
- const fields = ["name", "exists", "mtime_ms", "size"];
112
- const { data, extensions, ignore, rootDir, roots, perfLogger } = options;
113
- const clocks = data.clocks;
86
+ module.exports = async function watchmanCrawl({
87
+ abortSignal,
88
+ computeSha1,
89
+ data,
90
+ enableSymlinks,
91
+ extensions,
92
+ ignore,
93
+ rootDir,
94
+ roots,
95
+ perfLogger,
96
+ }) {
114
97
  perfLogger === null || perfLogger === void 0
115
98
  ? void 0
116
99
  : perfLogger.point("watchmanCrawl_start");
100
+ const clocks = data.clocks;
117
101
  const client = new watchman.Client();
118
- (_options$abortSignal = options.abortSignal) === null ||
119
- _options$abortSignal === void 0
120
- ? void 0
121
- : _options$abortSignal.addEventListener("abort", () => client.end());
122
- perfLogger === null || perfLogger === void 0
102
+ abortSignal === null || abortSignal === void 0
123
103
  ? void 0
124
- : perfLogger.point("watchmanCrawl/negotiateCapabilities_start"); // https://facebook.github.io/watchman/docs/capabilities.html
125
- // Check adds about ~28ms
126
-
127
- const capabilities = await capabilityCheck(client, {
128
- // If a required capability is missing then an error will be thrown,
129
- // we don't need this assertion, so using optional instead.
130
- optional: ["suffix-set"],
104
+ : abortSignal.addEventListener("abort", () => client.end());
105
+ let clientError;
106
+ client.on("error", (error) => {
107
+ clientError = makeWatchmanError(error);
131
108
  });
132
- const suffixExpression =
133
- capabilities !== null &&
134
- capabilities !== void 0 &&
135
- capabilities.capabilities["suffix-set"] // If available, use the optimized `suffix-set` operation:
136
- ? // https://facebook.github.io/watchman/docs/expr/suffix.html#suffix-set
137
- ["suffix", extensions]
138
- : ["anyof", ...extensions.map((extension) => ["suffix", extension])];
139
- let clientError; // $FlowFixMe[prop-missing] - Client is not typed as an EventEmitter
140
-
141
- client.on("error", (error) => (clientError = makeWatchmanError(error)));
142
109
  let didLogWatchmanWaitMessage = false; // $FlowFixMe[unclear-type] - Fix to use fb-watchman types
143
110
 
144
111
  const cmd = async (command, ...args) => {
@@ -171,18 +138,6 @@ module.exports = async function watchmanCrawl(options) {
171
138
  }
172
139
  };
173
140
 
174
- if (options.computeSha1) {
175
- const { capabilities } = await cmd("list-capabilities");
176
-
177
- if (capabilities.indexOf("field-content.sha1hex") !== -1) {
178
- fields.push("content.sha1hex");
179
- }
180
- }
181
-
182
- perfLogger === null || perfLogger === void 0
183
- ? void 0
184
- : perfLogger.point("watchmanCrawl/negotiateCapabilities_end");
185
-
186
141
  async function getWatchmanRoots(roots) {
187
142
  perfLogger === null || perfLogger === void 0
188
143
  ? void 0
@@ -250,61 +205,13 @@ module.exports = async function watchmanCrawl(options) {
250
205
  [`watchmanCrawl/query_${index}_has_clock`]: since != null,
251
206
  },
252
207
  });
253
- const query = {
254
- fields,
255
- expression: [
256
- "allof", // Match regular files only. Different Watchman generators treat
257
- // symlinks differently, so this ensures consistent results.
258
- ["type", "f"],
259
- ],
260
- };
261
- /**
262
- * Watchman "query planner".
263
- *
264
- * Watchman file queries consist of 1 or more generators that feed
265
- * files through the expression evaluator.
266
- *
267
- * Strategy:
268
- * 1. Select the narrowest possible generator so that the expression
269
- * evaluator has fewer candidates to process.
270
- * 2. Evaluate expressions from narrowest to broadest.
271
- * 3. Don't use an expression to recheck a condition that the
272
- * generator already guarantees.
273
- * 4. Compose expressions to avoid combinatorial explosions in the
274
- * number of terms.
275
- *
276
- * The ordering of generators/filters, from narrow to broad, is:
277
- * - since = O(changes)
278
- * - glob / dirname = O(files in a subtree of the repo)
279
- * - suffix = O(files in the repo)
280
- *
281
- * We assume that file extensions are ~uniformly distributed in the
282
- * repo but Haste map projects are focused on a handful of
283
- * directories. Therefore `glob` < `suffix`.
284
- */
285
-
286
- let queryGenerator;
287
-
288
- if (since != null) {
289
- // Use the `since` generator and filter by both path and extension.
290
- query.since = since;
291
- queryGenerator = "since";
292
- query.expression.push(
293
- ["anyof", ...directoryFilters.map((dir) => ["dirname", dir])],
294
- suffixExpression
295
- );
296
- } else if (directoryFilters.length > 0) {
297
- // Use the `glob` generator and filter only by extension.
298
- query.glob = directoryFilters.map((directory) => `${directory}/**`);
299
- query.glob_includedotfiles = true;
300
- queryGenerator = "glob";
301
- query.expression.push(suffixExpression);
302
- } else {
303
- // Use the `suffix` generator with no path/extension filtering.
304
- query.suffix = extensions;
305
- queryGenerator = "suffix";
306
- }
307
-
208
+ const { query, queryGenerator } = (0, _planQuery.planQuery)({
209
+ since,
210
+ extensions,
211
+ directoryFilters,
212
+ includeSha1: computeSha1,
213
+ includeSymlinks: enableSymlinks,
214
+ });
308
215
  perfLogger === null || perfLogger === void 0
309
216
  ? void 0
310
217
  : perfLogger.annotate({
@@ -435,6 +342,11 @@ module.exports = async function watchmanCrawl(options) {
435
342
  );
436
343
 
437
344
  for (const fileData of response.files) {
345
+ if (fileData.symlink_target != null) {
346
+ // TODO: Process symlinks
347
+ continue;
348
+ }
349
+
438
350
  const filePath =
439
351
  fsRoot + path.sep + (0, _normalizePathSep.default)(fileData.name);
440
352
  const relativeFilePath = fastPath.relative(rootDir, filePath);
@@ -456,11 +368,13 @@ module.exports = async function watchmanCrawl(options) {
456
368
  }
457
369
  }
458
370
  } else if (!ignore(filePath)) {
371
+ const { mtime_ms, size } = fileData;
372
+ (0, _invariant.default)(
373
+ mtime_ms != null && size != null,
374
+ "missing file data in watchman response"
375
+ );
459
376
  const mtime =
460
- typeof fileData.mtime_ms === "number"
461
- ? fileData.mtime_ms
462
- : fileData.mtime_ms.toNumber();
463
- const size = fileData.size;
377
+ typeof mtime_ms === "number" ? mtime_ms : mtime_ms.toNumber();
464
378
  let sha1hex = fileData["content.sha1hex"];
465
379
 
466
380
  if (typeof sha1hex !== "string" || sha1hex.length !== 40) {
@@ -6,6 +6,7 @@
6
6
  *
7
7
  * @flow strict-local
8
8
  * @format
9
+ * @oncall react_native
9
10
  */
10
11
 
11
12
  import type {
@@ -14,55 +15,20 @@ import type {
14
15
  FileMetaData,
15
16
  InternalData,
16
17
  Path,
17
- } from '../flow-types';
18
-
19
- import H from '../constants';
20
- import * as fastPath from '../lib/fast_path';
21
- import normalizePathSep from '../lib/normalizePathSep';
18
+ } from '../../flow-types';
19
+ import type {WatchmanQueryResponse, WatchmanWatchResponse} from 'fb-watchman';
20
+
21
+ import {planQuery} from './planQuery';
22
+ import H from '../../constants';
23
+ import * as fastPath from '../../lib/fast_path';
24
+ import normalizePathSep from '../../lib/normalizePathSep';
25
+ import invariant from 'invariant';
22
26
  import * as path from 'path';
23
27
 
24
28
  const watchman = require('fb-watchman');
25
29
 
26
- // $FlowFixMe[unclear-type] - Improve fb-watchman types to cover our uses
27
- type WatchmanQuery = any;
28
-
29
30
  type WatchmanRoots = Map<string, Array<string>>;
30
31
 
31
- type WatchmanListCapabilitiesResponse = {
32
- capabilities: Array<string>,
33
- };
34
-
35
- type WatchmanCapabilityCheckResponse = {
36
- // { 'suffix-set': true }
37
- capabilities: $ReadOnly<{[string]: boolean}>,
38
- // '2021.06.07.00'
39
- version: string,
40
- };
41
-
42
- type WatchmanWatchProjectResponse = {
43
- watch: string,
44
- relative_path: string,
45
- };
46
-
47
- type WatchmanQueryResponse = {
48
- warning?: string,
49
- is_fresh_instance: boolean,
50
- version: string,
51
- clock:
52
- | string
53
- | {
54
- scm: {'mergebase-with': string, mergebase: string},
55
- clock: string,
56
- },
57
- files: Array<{
58
- name: string,
59
- exists: boolean,
60
- mtime_ms: number | {toNumber: () => number},
61
- size: number,
62
- 'content.sha1hex'?: string,
63
- }>,
64
- };
65
-
66
32
  const WATCHMAN_WARNING_INITIAL_DELAY_MILLISECONDS = 10000;
67
33
  const WATCHMAN_WARNING_INTERVAL_MILLISECONDS = 20000;
68
34
 
@@ -75,65 +41,32 @@ function makeWatchmanError(error: Error): Error {
75
41
  return error;
76
42
  }
77
43
 
78
- /**
79
- * Wrap watchman capabilityCheck method as a promise.
80
- *
81
- * @param client watchman client
82
- * @param caps capabilities to verify
83
- * @returns a promise resolving to a list of verified capabilities
84
- */
85
- async function capabilityCheck(
86
- client: watchman.Client,
87
- caps: $ReadOnly<{optional?: $ReadOnlyArray<string>}>,
88
- ): Promise<WatchmanCapabilityCheckResponse> {
89
- return new Promise((resolve, reject) => {
90
- client.capabilityCheck(
91
- // @ts-expect-error: incorrectly typed
92
- caps,
93
- (error, response) => {
94
- if (error != null || response == null) {
95
- reject(error ?? new Error('capabilityCheck: Response missing'));
96
- } else {
97
- resolve(response);
98
- }
99
- },
100
- );
101
- });
102
- }
103
-
104
- module.exports = async function watchmanCrawl(
105
- options: CrawlerOptions,
106
- ): Promise<{
44
+ module.exports = async function watchmanCrawl({
45
+ abortSignal,
46
+ computeSha1,
47
+ data,
48
+ enableSymlinks,
49
+ extensions,
50
+ ignore,
51
+ rootDir,
52
+ roots,
53
+ perfLogger,
54
+ }: CrawlerOptions): Promise<{
107
55
  changedFiles?: FileData,
108
56
  removedFiles: FileData,
109
57
  hasteMap: InternalData,
110
58
  }> {
111
- const fields = ['name', 'exists', 'mtime_ms', 'size'];
112
- const {data, extensions, ignore, rootDir, roots, perfLogger} = options;
59
+ perfLogger?.point('watchmanCrawl_start');
60
+
113
61
  const clocks = data.clocks;
114
62
 
115
- perfLogger?.point('watchmanCrawl_start');
116
63
  const client = new watchman.Client();
117
- options.abortSignal?.addEventListener('abort', () => client.end());
118
-
119
- perfLogger?.point('watchmanCrawl/negotiateCapabilities_start');
120
- // https://facebook.github.io/watchman/docs/capabilities.html
121
- // Check adds about ~28ms
122
- const capabilities = await capabilityCheck(client, {
123
- // If a required capability is missing then an error will be thrown,
124
- // we don't need this assertion, so using optional instead.
125
- optional: ['suffix-set'],
126
- });
127
-
128
- const suffixExpression = capabilities?.capabilities['suffix-set']
129
- ? // If available, use the optimized `suffix-set` operation:
130
- // https://facebook.github.io/watchman/docs/expr/suffix.html#suffix-set
131
- ['suffix', extensions]
132
- : ['anyof', ...extensions.map(extension => ['suffix', extension])];
64
+ abortSignal?.addEventListener('abort', () => client.end());
133
65
 
134
66
  let clientError;
135
- // $FlowFixMe[prop-missing] - Client is not typed as an EventEmitter
136
- client.on('error', error => (clientError = makeWatchmanError(error)));
67
+ client.on('error', error => {
68
+ clientError = makeWatchmanError(error);
69
+ });
137
70
 
138
71
  let didLogWatchmanWaitMessage = false;
139
72
 
@@ -163,18 +96,6 @@ module.exports = async function watchmanCrawl(
163
96
  }
164
97
  };
165
98
 
166
- if (options.computeSha1) {
167
- const {capabilities} = await cmd<WatchmanListCapabilitiesResponse>(
168
- 'list-capabilities',
169
- );
170
-
171
- if (capabilities.indexOf('field-content.sha1hex') !== -1) {
172
- fields.push('content.sha1hex');
173
- }
174
- }
175
-
176
- perfLogger?.point('watchmanCrawl/negotiateCapabilities_end');
177
-
178
99
  async function getWatchmanRoots(
179
100
  roots: $ReadOnlyArray<Path>,
180
101
  ): Promise<WatchmanRoots> {
@@ -183,7 +104,7 @@ module.exports = async function watchmanCrawl(
183
104
  await Promise.all(
184
105
  roots.map(async (root, index) => {
185
106
  perfLogger?.point(`watchmanCrawl/watchProject_${index}_start`);
186
- const response = await cmd<WatchmanWatchProjectResponse>(
107
+ const response = await cmd<WatchmanWatchResponse>(
187
108
  'watch-project',
188
109
  root,
189
110
  );
@@ -235,61 +156,13 @@ module.exports = async function watchmanCrawl(
235
156
  },
236
157
  });
237
158
 
238
- const query: WatchmanQuery = {
239
- fields,
240
- expression: [
241
- 'allof',
242
- // Match regular files only. Different Watchman generators treat
243
- // symlinks differently, so this ensures consistent results.
244
- ['type', 'f'],
245
- ],
246
- };
247
-
248
- /**
249
- * Watchman "query planner".
250
- *
251
- * Watchman file queries consist of 1 or more generators that feed
252
- * files through the expression evaluator.
253
- *
254
- * Strategy:
255
- * 1. Select the narrowest possible generator so that the expression
256
- * evaluator has fewer candidates to process.
257
- * 2. Evaluate expressions from narrowest to broadest.
258
- * 3. Don't use an expression to recheck a condition that the
259
- * generator already guarantees.
260
- * 4. Compose expressions to avoid combinatorial explosions in the
261
- * number of terms.
262
- *
263
- * The ordering of generators/filters, from narrow to broad, is:
264
- * - since = O(changes)
265
- * - glob / dirname = O(files in a subtree of the repo)
266
- * - suffix = O(files in the repo)
267
- *
268
- * We assume that file extensions are ~uniformly distributed in the
269
- * repo but Haste map projects are focused on a handful of
270
- * directories. Therefore `glob` < `suffix`.
271
- */
272
- let queryGenerator: ?string;
273
- if (since != null) {
274
- // Use the `since` generator and filter by both path and extension.
275
- query.since = since;
276
- queryGenerator = 'since';
277
- query.expression.push(
278
- ['anyof', ...directoryFilters.map(dir => ['dirname', dir])],
279
- suffixExpression,
280
- );
281
- } else if (directoryFilters.length > 0) {
282
- // Use the `glob` generator and filter only by extension.
283
- query.glob = directoryFilters.map(directory => `${directory}/**`);
284
- query.glob_includedotfiles = true;
285
- queryGenerator = 'glob';
286
-
287
- query.expression.push(suffixExpression);
288
- } else {
289
- // Use the `suffix` generator with no path/extension filtering.
290
- query.suffix = extensions;
291
- queryGenerator = 'suffix';
292
- }
159
+ const {query, queryGenerator} = planQuery({
160
+ since,
161
+ extensions,
162
+ directoryFilters,
163
+ includeSha1: computeSha1,
164
+ includeSymlinks: enableSymlinks,
165
+ });
293
166
 
294
167
  perfLogger?.annotate({
295
168
  string: {
@@ -392,6 +265,10 @@ module.exports = async function watchmanCrawl(
392
265
  );
393
266
 
394
267
  for (const fileData of response.files) {
268
+ if (fileData.symlink_target != null) {
269
+ // TODO: Process symlinks
270
+ continue;
271
+ }
395
272
  const filePath = fsRoot + path.sep + normalizePathSep(fileData.name);
396
273
  const relativeFilePath = fastPath.relative(rootDir, filePath);
397
274
  const existingFileData = data.files.get(relativeFilePath);
@@ -414,11 +291,13 @@ module.exports = async function watchmanCrawl(
414
291
  }
415
292
  }
416
293
  } else if (!ignore(filePath)) {
294
+ const {mtime_ms, size} = fileData;
295
+ invariant(
296
+ mtime_ms != null && size != null,
297
+ 'missing file data in watchman response',
298
+ );
417
299
  const mtime =
418
- typeof fileData.mtime_ms === 'number'
419
- ? fileData.mtime_ms
420
- : fileData.mtime_ms.toNumber();
421
- const size = fileData.size;
300
+ typeof mtime_ms === 'number' ? mtime_ms : mtime_ms.toNumber();
422
301
 
423
302
  let sha1hex = fileData['content.sha1hex'];
424
303
  if (typeof sha1hex !== 'string' || sha1hex.length !== 40) {