metro-file-map 0.76.0 → 0.76.2

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.
@@ -26,6 +26,13 @@ type FileNode = FileMetaData;
26
26
  type LinkNode = string;
27
27
  type AnyNode = FileNode | DirectoryNode | LinkNode;
28
28
 
29
+ // Terminology:
30
+ //
31
+ // mixedPath - a root-relative or absolute path
32
+ // relativePath - a root-relative path
33
+ // normalPath - a root-relative, normalised path (no extraneous '.' or '..')
34
+ // canonicalPath - a root-relative, normalised, real path (no symlinks in dirname)
35
+
29
36
  export default class TreeFS implements MutableFileSystem {
30
37
  +#rootDir: Path;
31
38
  +#files: FileData;
@@ -46,18 +53,18 @@ export default class TreeFS implements MutableFileSystem {
46
53
  );
47
54
  }
48
55
 
49
- getModuleName(file: Path): ?string {
50
- const fileMetadata = this._getFileData(file);
56
+ getModuleName(mixedPath: Path): ?string {
57
+ const fileMetadata = this._getFileData(mixedPath);
51
58
  return (fileMetadata && fileMetadata[H.ID]) ?? null;
52
59
  }
53
60
 
54
- getSize(file: Path): ?number {
55
- const fileMetadata = this._getFileData(file);
61
+ getSize(mixedPath: Path): ?number {
62
+ const fileMetadata = this._getFileData(mixedPath);
56
63
  return (fileMetadata && fileMetadata[H.SIZE]) ?? null;
57
64
  }
58
65
 
59
- getDependencies(file: Path): ?Array<string> {
60
- const fileMetadata = this._getFileData(file);
66
+ getDependencies(mixedPath: Path): ?Array<string> {
67
+ const fileMetadata = this._getFileData(mixedPath);
61
68
 
62
69
  if (fileMetadata) {
63
70
  return fileMetadata[H.DEPENDENCIES]
@@ -68,13 +75,13 @@ export default class TreeFS implements MutableFileSystem {
68
75
  }
69
76
  }
70
77
 
71
- getSha1(file: Path): ?string {
72
- const fileMetadata = this._getFileData(file);
78
+ getSha1(mixedPath: Path): ?string {
79
+ const fileMetadata = this._getFileData(mixedPath);
73
80
  return (fileMetadata && fileMetadata[H.SHA1]) ?? null;
74
81
  }
75
82
 
76
- exists(file: Path): boolean {
77
- const result = this._getFileData(file);
83
+ exists(mixedPath: Path): boolean {
84
+ const result = this._getFileData(mixedPath);
78
85
  return result != null;
79
86
  }
80
87
 
@@ -84,8 +91,8 @@ export default class TreeFS implements MutableFileSystem {
84
91
  );
85
92
  }
86
93
 
87
- linkStats(file: Path): ?FileStats {
88
- const fileMetadata = this._getFileData(file, {follow: false});
94
+ linkStats(mixedPath: Path): ?FileStats {
95
+ const fileMetadata = this._getFileData(mixedPath, {followLeaf: false});
89
96
  if (fileMetadata == null) {
90
97
  return null;
91
98
  }
@@ -133,7 +140,7 @@ export default class TreeFS implements MutableFileSystem {
133
140
  if (!contextRootResult) {
134
141
  return [];
135
142
  }
136
- const {normalPath: rootRealPath, node: contextRoot} = contextRootResult;
143
+ const {canonicalPath: rootRealPath, node: contextRoot} = contextRootResult;
137
144
  if (!(contextRoot instanceof Map)) {
138
145
  return [];
139
146
  }
@@ -171,30 +178,72 @@ export default class TreeFS implements MutableFileSystem {
171
178
  return files;
172
179
  }
173
180
 
174
- getRealPath(filePath: Path): ?string {
175
- const normalPath = this._normalizePath(filePath);
181
+ getRealPath(mixedPath: Path): ?string {
182
+ const normalPath = this._normalizePath(mixedPath);
176
183
  const metadata = this.#files.get(normalPath);
177
184
  if (metadata && metadata[H.SYMLINK] === 0) {
178
185
  return fastPath.resolve(this.#rootDir, normalPath);
179
186
  }
180
- const result = this._lookupByNormalPath(normalPath, {follow: true});
187
+ const result = this._lookupByNormalPath(normalPath, {followLeaf: true});
181
188
  if (!result || result.node instanceof Map) {
182
189
  return null;
183
190
  }
184
- return fastPath.resolve(this.#rootDir, result.normalPath);
191
+ return fastPath.resolve(this.#rootDir, result.canonicalPath);
185
192
  }
186
193
 
187
- addOrModify(filePath: Path, metadata: FileMetaData): void {
188
- const normalPath = this._normalizePath(filePath);
189
- this.bulkAddOrModify(new Map([[normalPath, metadata]]));
194
+ addOrModify(mixedPath: Path, metadata: FileMetaData): void {
195
+ const normalPath = this._normalizePath(mixedPath);
196
+ // Walk the tree to find the *real* path of the parent node, creating
197
+ // directories as we need.
198
+ const parentDirNode = this._lookupByNormalPath(path.dirname(normalPath), {
199
+ makeDirectories: true,
200
+ });
201
+ if (!parentDirNode) {
202
+ throw new Error(
203
+ `TreeFS: Failed to make parent directory entry for ${mixedPath}`,
204
+ );
205
+ }
206
+ // Normalize the resulting path to account for the parent node being root.
207
+ const canonicalPath = this._normalizePath(
208
+ parentDirNode.canonicalPath + path.sep + path.basename(normalPath),
209
+ );
210
+ this.bulkAddOrModify(new Map([[canonicalPath, metadata]]));
190
211
  }
191
212
 
192
213
  bulkAddOrModify(addedOrModifiedFiles: FileData): void {
214
+ const files = this.#files;
215
+
216
+ // Optimisation: Bulk FileData are typically clustered by directory, so we
217
+ // optimise for that case by remembering the last directory we looked up.
218
+ // Experiments with large result sets show this to be significantly (~30%)
219
+ // faster than caching all lookups in a Map, and 70% faster than no cache.
220
+ let lastDir: ?string;
221
+ let directoryNode: DirectoryNode;
222
+
193
223
  for (const [normalPath, metadata] of addedOrModifiedFiles) {
194
- this.#files.set(normalPath, metadata);
195
- const directoryParts = normalPath.split(path.sep);
196
- const basename = directoryParts.pop();
197
- const directoryNode = this._mkdirp(directoryParts);
224
+ if (addedOrModifiedFiles !== files) {
225
+ files.set(normalPath, metadata);
226
+ }
227
+
228
+ const lastSepIdx = normalPath.lastIndexOf(path.sep);
229
+ const dirname = lastSepIdx === -1 ? '' : normalPath.slice(0, lastSepIdx);
230
+ const basename =
231
+ lastSepIdx === -1 ? normalPath : normalPath.slice(lastSepIdx + 1);
232
+
233
+ if (directoryNode == null || dirname !== lastDir) {
234
+ const lookup = this._lookupByNormalPath(dirname, {
235
+ followLeaf: false,
236
+ makeDirectories: true,
237
+ });
238
+ if (!(lookup?.node instanceof Map)) {
239
+ throw new Error(
240
+ `TreeFS: Could not add directory ${dirname} when adding files`,
241
+ );
242
+ }
243
+ lastDir = dirname;
244
+ directoryNode = lookup.node;
245
+ }
246
+
198
247
  if (metadata[H.SYMLINK] !== 0) {
199
248
  const symlinkTarget = metadata[H.SYMLINK];
200
249
  invariant(
@@ -219,73 +268,116 @@ export default class TreeFS implements MutableFileSystem {
219
268
  }
220
269
  }
221
270
 
222
- remove(filePath: Path): ?FileMetaData {
223
- const normalPath = this._normalizePath(filePath);
224
- const fileMetadata = this.#files.get(normalPath);
225
- if (fileMetadata == null) {
271
+ remove(mixedPath: Path): ?FileMetaData {
272
+ const normalPath = this._normalizePath(mixedPath);
273
+ const result = this._lookupByNormalPath(normalPath, {followLeaf: false});
274
+ if (!result || result.node instanceof Map) {
226
275
  return null;
227
276
  }
228
- this.#files.delete(normalPath);
229
- const directoryParts = normalPath.split(path.sep);
230
- const basename = directoryParts.pop();
231
- const directoryNode = this._mkdirp(directoryParts);
232
- directoryNode.delete(basename);
277
+ const {parentNode, canonicalPath, node} = result;
278
+
279
+ // If node is a symlink, get its metadata from the file map. Otherwise, we
280
+ // already have it in the lookup result.
281
+ const fileMetadata =
282
+ typeof node === 'string' ? this.#files.get(canonicalPath) : node;
283
+ if (fileMetadata == null) {
284
+ throw new Error(`TreeFS: Missing metadata for ${mixedPath}`);
285
+ }
286
+ if (parentNode == null) {
287
+ throw new Error(`TreeFS: Missing parent node for ${mixedPath}`);
288
+ }
289
+ this.#files.delete(canonicalPath);
290
+ parentNode.delete(path.basename(canonicalPath));
233
291
  return fileMetadata;
234
292
  }
235
293
 
236
294
  _lookupByNormalPath(
237
- relativePath: string,
295
+ requestedNormalPath: string,
238
296
  opts: {
239
297
  // Like lstat vs stat, whether to follow a symlink at the basename of
240
298
  // the given path, or return the details of the symlink itself.
241
- follow: boolean,
242
- } = {follow: true},
243
- seen: Set<string> = new Set(),
244
- ): ?{normalPath: string, node: AnyNode} {
245
- if (relativePath === '') {
246
- return {normalPath: '', node: this.#rootNode};
247
- }
248
- seen.add(relativePath);
249
- const directoryParts = relativePath.split(path.sep);
250
- const basename = directoryParts.pop();
251
- let node = this.#rootNode;
252
- for (const [idx, directoryPart] of directoryParts.entries()) {
253
- if (directoryPart === '.') {
299
+ followLeaf?: boolean,
300
+ makeDirectories?: boolean,
301
+ } = {followLeaf: true, makeDirectories: false},
302
+ ): ?(
303
+ | {canonicalPath: string, node: AnyNode, parentNode: DirectoryNode}
304
+ | {canonicalPath: string, node: DirectoryNode, parentNode: null}
305
+ ) {
306
+ // We'll update the target if we hit a symlink.
307
+ let targetNormalPath = requestedNormalPath;
308
+ // Lazy-initialised set of seen target paths, to detect symlink cycles.
309
+ let seen: ?Set<string>;
310
+ // Pointer to the first character of the current path segment in
311
+ // targetNormalPath.
312
+ let fromIdx = 0;
313
+ // The parent of the current segment
314
+ let parentNode = this.#rootNode;
315
+
316
+ while (targetNormalPath.length > fromIdx) {
317
+ const nextSepIdx = targetNormalPath.indexOf(path.sep, fromIdx);
318
+ const isLastSegment = nextSepIdx === -1;
319
+ const segmentName = isLastSegment
320
+ ? targetNormalPath.slice(fromIdx)
321
+ : targetNormalPath.slice(fromIdx, nextSepIdx);
322
+ fromIdx = !isLastSegment ? nextSepIdx + 1 : targetNormalPath.length;
323
+
324
+ if (segmentName === '.') {
254
325
  continue;
255
326
  }
256
- const nextNode = node.get(directoryPart);
257
- if (nextNode == null) {
258
- return null;
327
+
328
+ let segmentNode = parentNode.get(segmentName);
329
+ if (segmentNode == null) {
330
+ if (opts.makeDirectories !== true) {
331
+ return null;
332
+ }
333
+ segmentNode = new Map();
334
+ parentNode.set(segmentName, segmentNode);
335
+ }
336
+
337
+ // If there are no more '/' to come, we're done unless this is a symlink
338
+ // we must follow.
339
+ if (
340
+ isLastSegment &&
341
+ (typeof segmentNode !== 'string' || opts.followLeaf === false)
342
+ ) {
343
+ return {
344
+ canonicalPath: targetNormalPath,
345
+ node: segmentNode,
346
+ parentNode,
347
+ };
259
348
  }
260
- if (Array.isArray(nextNode)) {
349
+
350
+ // If the next node is a directory, go into it
351
+ if (segmentNode instanceof Map) {
352
+ parentNode = segmentNode;
353
+ } else if (Array.isArray(segmentNode)) {
261
354
  // Regular file in a directory path
262
355
  return null;
263
- } else if (typeof nextNode === 'string') {
264
- if (seen.has(nextNode)) {
356
+ } else if (typeof segmentNode === 'string') {
357
+ // segmentNode is a normalised symlink target. Append any subsequent
358
+ // path segments to the symlink target, and reset with our new target.
359
+ targetNormalPath = isLastSegment
360
+ ? segmentNode
361
+ : segmentNode + path.sep + targetNormalPath.slice(fromIdx);
362
+ if (seen == null) {
363
+ // Optimisation: set this lazily only when we've encountered a symlink
364
+ seen = new Set([requestedNormalPath]);
365
+ }
366
+ if (seen.has(targetNormalPath)) {
265
367
  // TODO: Warn `Symlink cycle detected: ${[...seen, node].join(' -> ')}`
266
368
  return null;
267
369
  }
268
- return this._lookupByNormalPath(
269
- path.join(nextNode, ...directoryParts.slice(idx + 1), basename),
270
- opts,
271
- seen,
272
- );
273
- }
274
- node = nextNode;
275
- }
276
- const basenameNode = node.get(basename);
277
- if (typeof basenameNode === 'string') {
278
- // basenameNode is a symlink target
279
- if (!opts.follow) {
280
- return {normalPath: relativePath, node: basenameNode};
281
- }
282
- if (seen.has(basenameNode)) {
283
- // TODO: Warn `Symlink cycle detected: ${[...seen, target].join(' -> ')}`
284
- return null;
370
+ seen.add(targetNormalPath);
371
+ fromIdx = 0;
372
+ parentNode = this.#rootNode;
285
373
  }
286
- return this._lookupByNormalPath(basenameNode, opts, seen);
287
374
  }
288
- return basenameNode ? {normalPath: relativePath, node: basenameNode} : null;
375
+ invariant(parentNode === this.#rootNode, 'Unexpectedly escaped traversal');
376
+ return {
377
+ canonicalPath: targetNormalPath,
378
+ node: this.#rootNode,
379
+ parentNode: null,
380
+ };
289
381
  }
290
382
 
291
383
  _normalizePath(relativeOrAbsolutePath: Path): string {
@@ -366,39 +458,19 @@ export default class TreeFS implements MutableFileSystem {
366
458
 
367
459
  _getFileData(
368
460
  filePath: Path,
369
- opts: {follow: boolean} = {follow: true},
461
+ opts: {followLeaf: boolean} = {followLeaf: true},
370
462
  ): ?FileMetaData {
371
463
  const normalPath = this._normalizePath(filePath);
372
464
  const metadata = this.#files.get(normalPath);
373
- if (metadata && (!opts.follow || metadata[H.SYMLINK] === 0)) {
465
+ if (metadata && (!opts.followLeaf || metadata[H.SYMLINK] === 0)) {
374
466
  return metadata;
375
467
  }
376
- const result = this._lookupByNormalPath(normalPath, opts);
468
+ const result = this._lookupByNormalPath(normalPath, {
469
+ followLeaf: opts.followLeaf,
470
+ });
377
471
  if (!result || result.node instanceof Map) {
378
472
  return null;
379
473
  }
380
- return this.#files.get(result.normalPath);
381
- }
382
-
383
- _mkdirp(directoryParts: $ReadOnlyArray<string>): DirectoryNode {
384
- let node = this.#rootNode;
385
- for (const directoryPart of directoryParts) {
386
- if (directoryPart === '.') {
387
- continue;
388
- }
389
- let nextNode = node.get(directoryPart);
390
- if (nextNode == null) {
391
- nextNode = new Map();
392
- node.set(directoryPart, nextNode);
393
- }
394
- invariant(
395
- nextNode instanceof Map,
396
- '%s in %s is a file, directory expected',
397
- directoryPart,
398
- directoryParts,
399
- );
400
- node = nextNode;
401
- }
402
- return node;
474
+ return this.#files.get(result.canonicalPath);
403
475
  }
404
476
  }
package/src/HasteFS.js DELETED
@@ -1,220 +0,0 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true,
5
- });
6
- exports.default = void 0;
7
- var _constants = _interopRequireDefault(require("./constants"));
8
- var fastPath = _interopRequireWildcard(require("./lib/fast_path"));
9
- var _invariant = _interopRequireDefault(require("invariant"));
10
- var path = _interopRequireWildcard(require("path"));
11
- var _jestUtil = require("jest-util");
12
- function _getRequireWildcardCache(nodeInterop) {
13
- if (typeof WeakMap !== "function") return null;
14
- var cacheBabelInterop = new WeakMap();
15
- var cacheNodeInterop = new WeakMap();
16
- return (_getRequireWildcardCache = function (nodeInterop) {
17
- return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
18
- })(nodeInterop);
19
- }
20
- function _interopRequireWildcard(obj, nodeInterop) {
21
- if (!nodeInterop && obj && obj.__esModule) {
22
- return obj;
23
- }
24
- if (obj === null || (typeof obj !== "object" && typeof obj !== "function")) {
25
- return { default: obj };
26
- }
27
- var cache = _getRequireWildcardCache(nodeInterop);
28
- if (cache && cache.has(obj)) {
29
- return cache.get(obj);
30
- }
31
- var newObj = {};
32
- var hasPropertyDescriptor =
33
- Object.defineProperty && Object.getOwnPropertyDescriptor;
34
- for (var key in obj) {
35
- if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
36
- var desc = hasPropertyDescriptor
37
- ? Object.getOwnPropertyDescriptor(obj, key)
38
- : null;
39
- if (desc && (desc.get || desc.set)) {
40
- Object.defineProperty(newObj, key, desc);
41
- } else {
42
- newObj[key] = obj[key];
43
- }
44
- }
45
- }
46
- newObj.default = obj;
47
- if (cache) {
48
- cache.set(obj, newObj);
49
- }
50
- return newObj;
51
- }
52
- function _interopRequireDefault(obj) {
53
- return obj && obj.__esModule ? obj : { default: obj };
54
- }
55
- /**
56
- * Copyright (c) Meta Platforms, Inc. and affiliates.
57
- *
58
- * This source code is licensed under the MIT license found in the
59
- * LICENSE file in the root directory of this source tree.
60
- *
61
- * @format
62
- *
63
- */
64
-
65
- class HasteFS {
66
- #rootDir;
67
- #files;
68
- constructor({ rootDir, files }) {
69
- this.#rootDir = rootDir;
70
- this.#files = files;
71
- }
72
- _normalizePath(relativeOrAbsolutePath) {
73
- return path.isAbsolute(relativeOrAbsolutePath)
74
- ? fastPath.relative(this.#rootDir, relativeOrAbsolutePath)
75
- : path.normalize(relativeOrAbsolutePath);
76
- }
77
- remove(filePath) {
78
- const normalPath = this._normalizePath(filePath);
79
- const fileMetadata = this.#files.get(normalPath);
80
- if (!fileMetadata) {
81
- return null;
82
- }
83
- this.#files.delete(normalPath);
84
- return fileMetadata;
85
- }
86
- bulkAddOrModify(changedFiles) {
87
- for (const [relativePath, metadata] of changedFiles) {
88
- this.#files.set(relativePath, metadata);
89
- }
90
- }
91
- addOrModify(filePath, metadata) {
92
- this.#files.set(this._normalizePath(filePath), metadata);
93
- }
94
- getSerializableSnapshot() {
95
- return new Map(Array.from(this.#files.entries(), ([k, v]) => [k, [...v]]));
96
- }
97
- getModuleName(file) {
98
- const fileMetadata = this._getFileData(file);
99
- return (fileMetadata && fileMetadata[_constants.default.ID]) ?? null;
100
- }
101
- getSize(file) {
102
- const fileMetadata = this._getFileData(file);
103
- return (fileMetadata && fileMetadata[_constants.default.SIZE]) ?? null;
104
- }
105
- getDependencies(file) {
106
- const fileMetadata = this._getFileData(file);
107
- if (fileMetadata) {
108
- return fileMetadata[_constants.default.DEPENDENCIES]
109
- ? fileMetadata[_constants.default.DEPENDENCIES].split(
110
- _constants.default.DEPENDENCY_DELIM
111
- )
112
- : [];
113
- } else {
114
- return null;
115
- }
116
- }
117
- getSha1(file) {
118
- const fileMetadata = this._getFileData(file);
119
- return (fileMetadata && fileMetadata[_constants.default.SHA1]) ?? null;
120
- }
121
- exists(file) {
122
- return this._getFileData(file) != null;
123
- }
124
- getAllFiles() {
125
- return Array.from(this.getAbsoluteFileIterator());
126
- }
127
- getFileIterator() {
128
- return this.#files.keys();
129
- }
130
- *getAbsoluteFileIterator() {
131
- for (const file of this.getFileIterator()) {
132
- yield fastPath.resolve(this.#rootDir, file);
133
- }
134
- }
135
- linkStats(file) {
136
- const fileMetadata = this._getFileData(file);
137
- if (fileMetadata == null) {
138
- return null;
139
- }
140
- const fileType = fileMetadata[_constants.default.SYMLINK] === 0 ? "f" : "l";
141
- const modifiedTime = fileMetadata[_constants.default.MTIME];
142
- (0, _invariant.default)(
143
- typeof modifiedTime === "number",
144
- "File in HasteFS missing modified time"
145
- );
146
- return {
147
- fileType,
148
- modifiedTime,
149
- };
150
- }
151
- matchFiles(pattern) {
152
- const regexpPattern =
153
- pattern instanceof RegExp ? pattern : new RegExp(pattern);
154
- const files = [];
155
- for (const file of this.getAbsoluteFileIterator()) {
156
- if (regexpPattern.test(file)) {
157
- files.push(file);
158
- }
159
- }
160
- return files;
161
- }
162
-
163
- /**
164
- * Given a search context, return a list of file paths matching the query.
165
- * The query matches against normalized paths which start with `./`,
166
- * for example: `a/b.js` -> `./a/b.js`
167
- */
168
- matchFilesWithContext(root, context) {
169
- const files = [];
170
- const prefix = "./";
171
- for (const file of this.getAbsoluteFileIterator()) {
172
- const filePath = fastPath.relative(root, file);
173
- const isUnderRoot = filePath && !filePath.startsWith("..");
174
- // Ignore everything outside of the provided `root`.
175
- if (!isUnderRoot) {
176
- continue;
177
- }
178
-
179
- // Prevent searching in child directories during a non-recursive search.
180
- if (!context.recursive && filePath.includes(path.sep)) {
181
- continue;
182
- }
183
- if (
184
- context.filter.test(
185
- // NOTE(EvanBacon): Ensure files start with `./` for matching purposes
186
- // this ensures packages work across Metro and Webpack (ex: Storybook for React DOM / React Native).
187
- // `a/b.js` -> `./a/b.js`
188
- prefix + filePath.replace(/\\/g, "/")
189
- )
190
- ) {
191
- files.push(file);
192
- }
193
- }
194
- return files;
195
- }
196
- matchFilesWithGlob(globs, root) {
197
- const files = new Set();
198
- const matcher = (0, _jestUtil.globsToMatcher)(globs);
199
- for (const file of this.getAbsoluteFileIterator()) {
200
- const filePath = root != null ? fastPath.relative(root, file) : file;
201
- if (matcher((0, _jestUtil.replacePathSepForGlob)(filePath))) {
202
- files.add(file);
203
- }
204
- }
205
- return files;
206
- }
207
- _getFileData(filePath) {
208
- // Shortcut to avoid any file path parsing if the given path is already
209
- // normalised.
210
- const optimisticMetadata = this.#files.get(filePath);
211
- if (optimisticMetadata) {
212
- return optimisticMetadata;
213
- }
214
- return this.#files.get(this._normalizePath(filePath));
215
- }
216
- getRealPath(filePath) {
217
- throw new Error("HasteFS.getRealPath() is not implemented.");
218
- }
219
- }
220
- exports.default = HasteFS;