@expo/metro-file-map 55.0.0-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/README.md +1 -0
- package/build/Watcher.d.ts +53 -0
- package/build/Watcher.js +249 -0
- package/build/cache/DiskCacheManager.d.ts +25 -0
- package/build/cache/DiskCacheManager.js +110 -0
- package/build/constants.d.ts +23 -0
- package/build/constants.js +27 -0
- package/build/crawlers/node/fallback.d.ts +20 -0
- package/build/crawlers/node/fallback.js +148 -0
- package/build/crawlers/node/index.d.ts +10 -0
- package/build/crawlers/node/index.js +152 -0
- package/build/crawlers/watchman/index.d.ts +8 -0
- package/build/crawlers/watchman/index.js +288 -0
- package/build/crawlers/watchman/planQuery.d.ts +31 -0
- package/build/crawlers/watchman/planQuery.js +99 -0
- package/build/index.d.ts +140 -0
- package/build/index.js +836 -0
- package/build/lib/FileProcessor.d.ts +42 -0
- package/build/lib/FileProcessor.js +180 -0
- package/build/lib/FileSystemChangeAggregator.d.ts +18 -0
- package/build/lib/FileSystemChangeAggregator.js +114 -0
- package/build/lib/RootPathUtils.d.ts +24 -0
- package/build/lib/RootPathUtils.js +311 -0
- package/build/lib/TreeFS.d.ts +161 -0
- package/build/lib/TreeFS.js +1006 -0
- package/build/lib/checkWatchmanCapabilities.d.ts +9 -0
- package/build/lib/checkWatchmanCapabilities.js +50 -0
- package/build/lib/normalizePathSeparatorsToPosix.d.ts +8 -0
- package/build/lib/normalizePathSeparatorsToPosix.js +20 -0
- package/build/lib/normalizePathSeparatorsToSystem.d.ts +8 -0
- package/build/lib/normalizePathSeparatorsToSystem.js +20 -0
- package/build/lib/removeOverlappingRoots.d.ts +10 -0
- package/build/lib/removeOverlappingRoots.js +37 -0
- package/build/lib/rootRelativeCacheKeys.d.ts +11 -0
- package/build/lib/rootRelativeCacheKeys.js +56 -0
- package/build/lib/sorting.d.ts +8 -0
- package/build/lib/sorting.js +31 -0
- package/build/plugins/DependencyPlugin.d.ts +23 -0
- package/build/plugins/DependencyPlugin.js +61 -0
- package/build/plugins/FileDataPlugin.d.ts +28 -0
- package/build/plugins/FileDataPlugin.js +46 -0
- package/build/plugins/HastePlugin.d.ts +32 -0
- package/build/plugins/HastePlugin.js +358 -0
- package/build/plugins/MockPlugin.d.ts +27 -0
- package/build/plugins/MockPlugin.js +157 -0
- package/build/plugins/dependencies/dependencyExtractor.d.ts +7 -0
- package/build/plugins/dependencies/dependencyExtractor.js +66 -0
- package/build/plugins/dependencies/worker.d.ts +18 -0
- package/build/plugins/dependencies/worker.js +30 -0
- package/build/plugins/haste/DuplicateHasteCandidatesError.d.ts +14 -0
- package/build/plugins/haste/DuplicateHasteCandidatesError.js +51 -0
- package/build/plugins/haste/HasteConflictsError.d.ts +12 -0
- package/build/plugins/haste/HasteConflictsError.js +49 -0
- package/build/plugins/haste/computeConflicts.d.ts +19 -0
- package/build/plugins/haste/computeConflicts.js +74 -0
- package/build/plugins/haste/getPlatformExtension.d.ts +7 -0
- package/build/plugins/haste/getPlatformExtension.js +19 -0
- package/build/plugins/haste/worker.d.ts +16 -0
- package/build/plugins/haste/worker.js +48 -0
- package/build/plugins/mocks/getMockName.d.ts +8 -0
- package/build/plugins/mocks/getMockName.js +17 -0
- package/build/ts-declarations/fb-watchman.d.ts +20 -0
- package/build/ts-declarations/fb-watchman.js +2 -0
- package/build/types.d.ts +404 -0
- package/build/types.js +8 -0
- package/build/watchers/AbstractWatcher.d.ts +30 -0
- package/build/watchers/AbstractWatcher.js +97 -0
- package/build/watchers/FallbackWatcher.d.ts +18 -0
- package/build/watchers/FallbackWatcher.js +411 -0
- package/build/watchers/NativeWatcher.d.ts +39 -0
- package/build/watchers/NativeWatcher.js +156 -0
- package/build/watchers/RecrawlWarning.d.ts +18 -0
- package/build/watchers/RecrawlWarning.js +59 -0
- package/build/watchers/WatchmanWatcher.d.ts +22 -0
- package/build/watchers/WatchmanWatcher.js +263 -0
- package/build/watchers/common.d.ts +39 -0
- package/build/watchers/common.js +60 -0
- package/build/worker.d.ts +24 -0
- package/build/worker.js +80 -0
- package/build/workerExclusionList.d.ts +8 -0
- package/build/workerExclusionList.js +57 -0
- package/package.json +54 -0
|
@@ -0,0 +1,1006 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const fs_1 = __importDefault(require("fs"));
|
|
13
|
+
const invariant_1 = __importDefault(require("invariant"));
|
|
14
|
+
const path_1 = __importDefault(require("path"));
|
|
15
|
+
const constants_1 = __importDefault(require("../constants"));
|
|
16
|
+
const normalizePathSeparatorsToPosix_1 = __importDefault(require("./normalizePathSeparatorsToPosix"));
|
|
17
|
+
const normalizePathSeparatorsToSystem_1 = __importDefault(require("./normalizePathSeparatorsToSystem"));
|
|
18
|
+
const fallback_1 = require("../crawlers/node/fallback");
|
|
19
|
+
const RootPathUtils_1 = require("./RootPathUtils");
|
|
20
|
+
function isDirectory(node) {
|
|
21
|
+
return node instanceof Map;
|
|
22
|
+
}
|
|
23
|
+
function isRegularFile(node) {
|
|
24
|
+
return node != null && node[constants_1.default.SYMLINK] === 0;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* OVERVIEW:
|
|
28
|
+
*
|
|
29
|
+
* TreeFS is Metro's in-memory representation of the file system. It is
|
|
30
|
+
* structured as a tree of non-empty maps and leaves (tuples), with the root
|
|
31
|
+
* node representing the given `rootDir`, typically Metro's _project root_
|
|
32
|
+
* (not a filesystem root). Map keys are path segments, and branches outside
|
|
33
|
+
* the project root are accessed via `'..'`.
|
|
34
|
+
*
|
|
35
|
+
* EXAMPLE:
|
|
36
|
+
*
|
|
37
|
+
* For a root dir '/data/project', the file '/data/other/app/index.js' would
|
|
38
|
+
* have metadata at #rootNode.get('..').get('other').get('app').get('index.js')
|
|
39
|
+
*
|
|
40
|
+
* SERIALISATION:
|
|
41
|
+
*
|
|
42
|
+
* #rootNode is designed to be directly serialisable and directly portable (for
|
|
43
|
+
* a given project) between different root directories and operating systems.
|
|
44
|
+
*
|
|
45
|
+
* SYMLINKS:
|
|
46
|
+
*
|
|
47
|
+
* Symlinks are represented as nodes whose metadata contains their literal
|
|
48
|
+
* target. Literal targets are resolved to normal paths at runtime, and cached.
|
|
49
|
+
* If a symlink is encountered during traversal, we restart traversal at the
|
|
50
|
+
* root node targeting join(normal symlink target, remaining path suffix).
|
|
51
|
+
*
|
|
52
|
+
* NODE TYPES:
|
|
53
|
+
*
|
|
54
|
+
* - A directory (including a parent directory at '..') is represented by a
|
|
55
|
+
* `Map` of basenames to any other node type.
|
|
56
|
+
* - A file is represented by an `Array` (tuple) of metadata, of which:
|
|
57
|
+
* - A regular file has node[H.SYMLINK] === 0
|
|
58
|
+
* - A symlink has node[H.SYMLINK] === 1 or
|
|
59
|
+
* typeof node[H.SYMLINK] === 'string', where a string is the literal
|
|
60
|
+
* content of the symlink (i.e. from readlink), if known.
|
|
61
|
+
*
|
|
62
|
+
* TERMINOLOGY:
|
|
63
|
+
*
|
|
64
|
+
* - mixedPath
|
|
65
|
+
* A root-relative or absolute path
|
|
66
|
+
* - relativePath
|
|
67
|
+
* A root-relative path
|
|
68
|
+
* - normalPath
|
|
69
|
+
* A root-relative, normalised path (no extraneous '.' or '..'), may have a
|
|
70
|
+
* single trailing slash
|
|
71
|
+
* - canonicalPath
|
|
72
|
+
* A root-relative, normalised, real path (no symlinks in dirname), never has
|
|
73
|
+
* a trailing slash
|
|
74
|
+
*/
|
|
75
|
+
class TreeFS {
|
|
76
|
+
#fallbackBoundaryDepth;
|
|
77
|
+
#fallbackFilesystem;
|
|
78
|
+
#pathUtils;
|
|
79
|
+
#processFile;
|
|
80
|
+
#rootDir;
|
|
81
|
+
#rootPattern;
|
|
82
|
+
#rootNode = new Map();
|
|
83
|
+
constructor(opts) {
|
|
84
|
+
const { rootDir, files, processFile, fallbackFilesystem, roots, serverRoot } = opts;
|
|
85
|
+
this.#rootDir = rootDir;
|
|
86
|
+
this.#pathUtils = new RootPathUtils_1.RootPathUtils(rootDir);
|
|
87
|
+
this.#processFile = processFile;
|
|
88
|
+
this.#fallbackFilesystem = fallbackFilesystem ?? null;
|
|
89
|
+
if (serverRoot != null) {
|
|
90
|
+
this.#fallbackBoundaryDepth = (0, RootPathUtils_1.getAncestorOfRootIdx)(this.#pathUtils.absoluteToNormal(serverRoot));
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
this.#fallbackBoundaryDepth = null;
|
|
94
|
+
}
|
|
95
|
+
this.#rootPattern = (0, RootPathUtils_1.pathsToPattern)(roots ?? [], this.#pathUtils);
|
|
96
|
+
if (files != null) {
|
|
97
|
+
this.bulkAddOrModify(files);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
getSerializableSnapshot() {
|
|
101
|
+
return this.#cloneTree(this.#rootNode, '');
|
|
102
|
+
}
|
|
103
|
+
static fromDeserializedSnapshot(args) {
|
|
104
|
+
const { rootDir, fileSystemData, processFile, fallbackFilesystem, roots, serverRoot } = args;
|
|
105
|
+
const tfs = new TreeFS({
|
|
106
|
+
processFile,
|
|
107
|
+
rootDir,
|
|
108
|
+
fallbackFilesystem,
|
|
109
|
+
roots,
|
|
110
|
+
serverRoot,
|
|
111
|
+
});
|
|
112
|
+
tfs.#rootNode = fileSystemData;
|
|
113
|
+
return tfs;
|
|
114
|
+
}
|
|
115
|
+
getSize(mixedPath) {
|
|
116
|
+
const fileMetadata = this.#getFileData(mixedPath);
|
|
117
|
+
return (fileMetadata && fileMetadata[constants_1.default.SIZE]) ?? null;
|
|
118
|
+
}
|
|
119
|
+
getDifference(files, options) {
|
|
120
|
+
const changedFiles = new Map(files);
|
|
121
|
+
const removedFiles = new Set();
|
|
122
|
+
const subpath = options?.subpath;
|
|
123
|
+
// If a subpath is specified, start iteration from that node
|
|
124
|
+
let rootNode = this.#rootNode;
|
|
125
|
+
let prefix = '';
|
|
126
|
+
if (subpath != null && subpath !== '') {
|
|
127
|
+
const lookupResult = this.#lookupByNormalPath(subpath, {
|
|
128
|
+
followLeaf: true,
|
|
129
|
+
});
|
|
130
|
+
if (!lookupResult.exists || !isDirectory(lookupResult.node)) {
|
|
131
|
+
// Directory doesn't exist, nothing to compare - all files are new
|
|
132
|
+
return { changedFiles, removedFiles };
|
|
133
|
+
}
|
|
134
|
+
rootNode = lookupResult.node;
|
|
135
|
+
prefix = lookupResult.canonicalPath;
|
|
136
|
+
}
|
|
137
|
+
for (const { canonicalPath, metadata } of this.#metadataIterator(rootNode, {
|
|
138
|
+
includeNodeModules: true,
|
|
139
|
+
includeSymlinks: true,
|
|
140
|
+
}, prefix)) {
|
|
141
|
+
const newMetadata = files.get(canonicalPath);
|
|
142
|
+
if (newMetadata) {
|
|
143
|
+
if (isRegularFile(newMetadata) !== isRegularFile(metadata)) {
|
|
144
|
+
// Types differ, file has changed
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (newMetadata[constants_1.default.MTIME] != null &&
|
|
148
|
+
newMetadata[constants_1.default.MTIME] !== 0 &&
|
|
149
|
+
newMetadata[constants_1.default.MTIME] === metadata[constants_1.default.MTIME]) {
|
|
150
|
+
// Types and modified time match - not changed.
|
|
151
|
+
changedFiles.delete(canonicalPath);
|
|
152
|
+
}
|
|
153
|
+
else if ((newMetadata[constants_1.default.MTIME] == null || newMetadata[constants_1.default.MTIME] === 0) &&
|
|
154
|
+
(metadata[constants_1.default.MTIME] == null || metadata[constants_1.default.MTIME] === 0)) {
|
|
155
|
+
// If file is still untouched then mark it as unchanged
|
|
156
|
+
changedFiles.delete(canonicalPath);
|
|
157
|
+
}
|
|
158
|
+
else if (newMetadata[constants_1.default.SHA1] != null &&
|
|
159
|
+
newMetadata[constants_1.default.SHA1] === metadata[constants_1.default.SHA1] &&
|
|
160
|
+
metadata[constants_1.default.VISITED] === 1) {
|
|
161
|
+
// Content matches - update modified time but don't revisit
|
|
162
|
+
const updatedMetadata = [...metadata];
|
|
163
|
+
updatedMetadata[constants_1.default.MTIME] = newMetadata[constants_1.default.MTIME];
|
|
164
|
+
changedFiles.set(canonicalPath, updatedMetadata);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
removedFiles.add(canonicalPath);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
changedFiles,
|
|
173
|
+
removedFiles,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
getMtimeByNormalPath(normalPath) {
|
|
177
|
+
const result = this.#lookupByNormalPath(normalPath, {
|
|
178
|
+
followLeaf: false,
|
|
179
|
+
skipFallback: true,
|
|
180
|
+
});
|
|
181
|
+
return result.exists && !isDirectory(result.node) ? result.node[constants_1.default.MTIME] : null;
|
|
182
|
+
}
|
|
183
|
+
getSha1(mixedPath) {
|
|
184
|
+
const fileMetadata = this.#getFileData(mixedPath);
|
|
185
|
+
return (fileMetadata && fileMetadata[constants_1.default.SHA1]) ?? null;
|
|
186
|
+
}
|
|
187
|
+
async getOrComputeSha1(mixedPath) {
|
|
188
|
+
const normalPath = this.#normalizePath(mixedPath);
|
|
189
|
+
const result = this.#lookupByNormalPath(normalPath, {
|
|
190
|
+
followLeaf: true,
|
|
191
|
+
});
|
|
192
|
+
if (!result.exists || isDirectory(result.node)) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
const { canonicalPath, node: fileMetadata } = result;
|
|
196
|
+
// Populate mtime and size on demand
|
|
197
|
+
if (fileMetadata[constants_1.default.MTIME] == null || fileMetadata[constants_1.default.MTIME] === 0) {
|
|
198
|
+
fileMetadata[constants_1.default.SHA1] = null;
|
|
199
|
+
const absolutePath = this.#pathUtils.normalToAbsolute(canonicalPath);
|
|
200
|
+
try {
|
|
201
|
+
const stat = await fs_1.default.promises.lstat(absolutePath);
|
|
202
|
+
const diskMtime = stat.mtime.getTime();
|
|
203
|
+
fileMetadata[constants_1.default.MTIME] = diskMtime;
|
|
204
|
+
fileMetadata[constants_1.default.SIZE] = stat.size;
|
|
205
|
+
}
|
|
206
|
+
catch { }
|
|
207
|
+
}
|
|
208
|
+
// Empty strings
|
|
209
|
+
const existing = fileMetadata[constants_1.default.SHA1];
|
|
210
|
+
if (existing != null && existing.length > 0) {
|
|
211
|
+
return { sha1: existing };
|
|
212
|
+
}
|
|
213
|
+
// Mutate the metadata we first retrieved. This may be orphaned or about
|
|
214
|
+
// to be overwritten if the file changes while we are processing it -
|
|
215
|
+
// by only mutating the original metadata, we don't risk caching a stale
|
|
216
|
+
// SHA-1 after a change event.
|
|
217
|
+
const maybeContent = await this.#processFile(canonicalPath, fileMetadata, {
|
|
218
|
+
computeSha1: true,
|
|
219
|
+
});
|
|
220
|
+
const sha1 = fileMetadata[constants_1.default.SHA1];
|
|
221
|
+
(0, invariant_1.default)(sha1 != null && sha1.length > 0, "File processing didn't populate a SHA-1 hash for %s", canonicalPath);
|
|
222
|
+
return maybeContent
|
|
223
|
+
? {
|
|
224
|
+
content: maybeContent,
|
|
225
|
+
sha1,
|
|
226
|
+
}
|
|
227
|
+
: { sha1 };
|
|
228
|
+
}
|
|
229
|
+
exists(mixedPath) {
|
|
230
|
+
const result = this.#getFileData(mixedPath);
|
|
231
|
+
return result != null;
|
|
232
|
+
}
|
|
233
|
+
lookup(mixedPath) {
|
|
234
|
+
const normalPath = this.#normalizePath(mixedPath);
|
|
235
|
+
const links = new Set();
|
|
236
|
+
const result = this.#lookupByNormalPath(normalPath, {
|
|
237
|
+
collectLinkPaths: links,
|
|
238
|
+
followLeaf: true,
|
|
239
|
+
});
|
|
240
|
+
if (!result.exists) {
|
|
241
|
+
const { canonicalMissingPath } = result;
|
|
242
|
+
return {
|
|
243
|
+
exists: false,
|
|
244
|
+
links,
|
|
245
|
+
missing: this.#pathUtils.normalToAbsolute(canonicalMissingPath),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
const { canonicalPath, node } = result;
|
|
249
|
+
const realPath = this.#pathUtils.normalToAbsolute(canonicalPath);
|
|
250
|
+
if (isDirectory(node)) {
|
|
251
|
+
return { exists: true, links, realPath, type: 'd' };
|
|
252
|
+
}
|
|
253
|
+
(0, invariant_1.default)(isRegularFile(node), 'lookup follows symlinks, so should never return one (%s -> %s)', mixedPath, canonicalPath);
|
|
254
|
+
return { exists: true, links, realPath, type: 'f', metadata: node };
|
|
255
|
+
}
|
|
256
|
+
getAllFiles() {
|
|
257
|
+
return Array.from(this.metadataIterator({
|
|
258
|
+
includeNodeModules: true,
|
|
259
|
+
includeSymlinks: false,
|
|
260
|
+
}), ({ canonicalPath }) => this.#pathUtils.normalToAbsolute(canonicalPath));
|
|
261
|
+
}
|
|
262
|
+
linkStats(mixedPath) {
|
|
263
|
+
const fileMetadata = this.#getFileData(mixedPath, { followLeaf: false });
|
|
264
|
+
if (fileMetadata == null) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
const fileType = isRegularFile(fileMetadata) ? 'f' : 'l';
|
|
268
|
+
return {
|
|
269
|
+
fileType,
|
|
270
|
+
modifiedTime: fileMetadata[constants_1.default.MTIME],
|
|
271
|
+
size: fileMetadata[constants_1.default.SIZE],
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Given a search context, return a list of file paths matching the query.
|
|
276
|
+
* The query matches against normalized paths which start with `./`,
|
|
277
|
+
* for example: `a/b.js` -> `./a/b.js`
|
|
278
|
+
*/
|
|
279
|
+
*matchFiles(opts) {
|
|
280
|
+
const { filter = null, filterCompareAbsolute = false, filterComparePosix = false, follow = false, recursive = true, rootDir = null, } = opts;
|
|
281
|
+
const normalRoot = rootDir == null ? '' : this.#normalizePath(rootDir);
|
|
282
|
+
const contextRootResult = this.#lookupByNormalPath(normalRoot);
|
|
283
|
+
if (!contextRootResult.exists) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const { ancestorOfRootIdx, canonicalPath: rootRealPath, node: contextRoot, parentNode: contextRootParent, } = contextRootResult;
|
|
287
|
+
if (!isDirectory(contextRoot)) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const contextRootAbsolutePath = rootRealPath === '' ? this.#rootDir : path_1.default.join(this.#rootDir, rootRealPath);
|
|
291
|
+
const prefix = filterComparePosix ? './' : '.' + path_1.default.sep;
|
|
292
|
+
const contextRootAbsolutePathForComparison = filterComparePosix && path_1.default.sep !== '/'
|
|
293
|
+
? contextRootAbsolutePath.replaceAll(path_1.default.sep, '/')
|
|
294
|
+
: contextRootAbsolutePath;
|
|
295
|
+
for (const relativePathForComparison of this.#pathIterator(contextRoot, contextRootParent, ancestorOfRootIdx, {
|
|
296
|
+
alwaysYieldPosix: filterComparePosix,
|
|
297
|
+
canonicalPathOfRoot: rootRealPath,
|
|
298
|
+
follow,
|
|
299
|
+
recursive,
|
|
300
|
+
subtreeOnly: rootDir != null,
|
|
301
|
+
})) {
|
|
302
|
+
if (filter == null ||
|
|
303
|
+
filter.test(
|
|
304
|
+
// NOTE(EvanBacon): Ensure files start with `./` for matching purposes
|
|
305
|
+
// this ensures packages work across Metro and Webpack (ex: Storybook for React DOM / React Native).
|
|
306
|
+
// `a/b.js` -> `./a/b.js`
|
|
307
|
+
filterCompareAbsolute === true
|
|
308
|
+
? path_1.default.join(contextRootAbsolutePathForComparison, relativePathForComparison)
|
|
309
|
+
: prefix + relativePathForComparison)) {
|
|
310
|
+
const relativePath = filterComparePosix === true && path_1.default.sep !== '/'
|
|
311
|
+
? relativePathForComparison.replaceAll('/', path_1.default.sep)
|
|
312
|
+
: relativePathForComparison;
|
|
313
|
+
yield path_1.default.join(contextRootAbsolutePath, relativePath);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
addOrModify(mixedPath, metadata, changeListener) {
|
|
318
|
+
const normalPath = this.#normalizePath(mixedPath);
|
|
319
|
+
// Walk the tree to find the *real* path of the parent node, creating
|
|
320
|
+
// directories as we need.
|
|
321
|
+
const parentDirNode = this.#lookupByNormalPath(path_1.default.dirname(normalPath), {
|
|
322
|
+
changeListener,
|
|
323
|
+
makeDirectories: true,
|
|
324
|
+
});
|
|
325
|
+
if (!parentDirNode.exists) {
|
|
326
|
+
throw new Error(`TreeFS: Failed to make parent directory entry for ${mixedPath}`);
|
|
327
|
+
}
|
|
328
|
+
// Normalize the resulting path to account for the parent node being root.
|
|
329
|
+
const canonicalPath = this.#normalizePath(parentDirNode.canonicalPath + path_1.default.sep + path_1.default.basename(normalPath));
|
|
330
|
+
this.bulkAddOrModify(new Map([[canonicalPath, metadata]]), changeListener);
|
|
331
|
+
}
|
|
332
|
+
bulkAddOrModify(addedOrModifiedFiles, changeListener) {
|
|
333
|
+
// Optimisation: Bulk FileData are typically clustered by directory, so we
|
|
334
|
+
// optimise for that case by remembering the last directory we looked up.
|
|
335
|
+
// Experiments with large result sets show this to be significantly (~30%)
|
|
336
|
+
// faster than caching all lookups in a Map, and 70% faster than no cache.
|
|
337
|
+
let lastDir;
|
|
338
|
+
let directoryNode;
|
|
339
|
+
for (const [normalPath, metadata] of addedOrModifiedFiles) {
|
|
340
|
+
const lastSepIdx = normalPath.lastIndexOf(path_1.default.sep);
|
|
341
|
+
const dirname = lastSepIdx === -1 ? '' : normalPath.slice(0, lastSepIdx);
|
|
342
|
+
const basename = lastSepIdx === -1 ? normalPath : normalPath.slice(lastSepIdx + 1);
|
|
343
|
+
if (directoryNode == null || dirname !== lastDir) {
|
|
344
|
+
const lookup = this.#lookupByNormalPath(dirname, {
|
|
345
|
+
changeListener,
|
|
346
|
+
followLeaf: false,
|
|
347
|
+
makeDirectories: true,
|
|
348
|
+
});
|
|
349
|
+
if (!lookup.exists) {
|
|
350
|
+
// This should only be possible if the input is non-real and
|
|
351
|
+
// lookup hits a broken symlink.
|
|
352
|
+
throw new Error(`TreeFS: Unexpected error adding ${normalPath}.\nMissing: ` +
|
|
353
|
+
lookup.canonicalMissingPath);
|
|
354
|
+
}
|
|
355
|
+
if (!isDirectory(lookup.node)) {
|
|
356
|
+
throw new Error(`TreeFS: Could not add directory ${dirname}, adding ${normalPath}. ` +
|
|
357
|
+
`${dirname} already exists in the file map as a file.`);
|
|
358
|
+
}
|
|
359
|
+
lastDir = dirname;
|
|
360
|
+
directoryNode = lookup.node;
|
|
361
|
+
}
|
|
362
|
+
if (changeListener != null) {
|
|
363
|
+
const existingNode = directoryNode.get(basename);
|
|
364
|
+
if (existingNode != null) {
|
|
365
|
+
(0, invariant_1.default)(!isDirectory(existingNode), 'Detected addition or modification of file %s, but it is tracked as a non-empty directory', normalPath);
|
|
366
|
+
// File already exists - this is a modification
|
|
367
|
+
changeListener.fileModified(normalPath, existingNode, metadata);
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
// New file
|
|
371
|
+
changeListener.fileAdded(normalPath, metadata);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
directoryNode.set(basename, metadata);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
remove(mixedPath, changeListener) {
|
|
378
|
+
const normalPath = this.#normalizePath(mixedPath);
|
|
379
|
+
const result = this.#lookupByNormalPath(normalPath, { followLeaf: false });
|
|
380
|
+
if (!result.exists) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const { parentNode, canonicalPath, node } = result;
|
|
384
|
+
if (isDirectory(node) && node.size > 0) {
|
|
385
|
+
for (const basename of node.keys()) {
|
|
386
|
+
this.remove(canonicalPath + path_1.default.sep + basename, changeListener);
|
|
387
|
+
}
|
|
388
|
+
// Removing the last file will delete this directory
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (parentNode != null) {
|
|
392
|
+
if (changeListener != null) {
|
|
393
|
+
if (isDirectory(node)) {
|
|
394
|
+
changeListener.directoryRemoved(canonicalPath);
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
changeListener.fileRemoved(canonicalPath, node);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
parentNode.delete(path_1.default.basename(canonicalPath));
|
|
401
|
+
if (parentNode.size === 0 && parentNode !== this.#rootNode) {
|
|
402
|
+
// NB: This isn't the most efficient algorithm - in the case of
|
|
403
|
+
// removing the last file in a deep hierarchy it's O(depth^2), but
|
|
404
|
+
// that's not expected to be a case common enough to justify
|
|
405
|
+
// implementation complexity, or slowing down more common uses of
|
|
406
|
+
// _lookupByNormalPath.
|
|
407
|
+
this.remove(path_1.default.dirname(canonicalPath), changeListener);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* The core traversal algorithm of TreeFS - takes a normal path and traverses
|
|
413
|
+
* through a tree of maps keyed on path segments, returning the node,
|
|
414
|
+
* canonical path, and other metadata if successful, or the first missing
|
|
415
|
+
* segment otherwise.
|
|
416
|
+
*
|
|
417
|
+
* When a symlink is encountered, we set a new target of the symlink's
|
|
418
|
+
* normalised target path plus the remainder of the original target path. In
|
|
419
|
+
* this way, the eventual target path in a successful lookup has all symlinks
|
|
420
|
+
* resolved, and gives us the real path "for free". Similarly if a traversal
|
|
421
|
+
* fails, we automatically have the real path of the first non-existent node.
|
|
422
|
+
*
|
|
423
|
+
* Note that this code is extremely hot during resolution, being the most
|
|
424
|
+
* expensive part of a file existence check. Benchmark any modifications!
|
|
425
|
+
*/
|
|
426
|
+
#lookupByNormalPath(requestedNormalPath, opts = { followLeaf: true, makeDirectories: false }) {
|
|
427
|
+
// We'll update the target if we hit a symlink.
|
|
428
|
+
let targetNormalPath = requestedNormalPath;
|
|
429
|
+
// Lazy-initialised set of seen target paths, to detect symlink cycles.
|
|
430
|
+
let seen;
|
|
431
|
+
// Pointer to the first character of the current path segment in
|
|
432
|
+
// targetNormalPath.
|
|
433
|
+
let fromIdx = opts.start?.pathIdx ?? 0;
|
|
434
|
+
// The parent of the current segment.
|
|
435
|
+
let parentNode = opts.start?.node ?? this.#rootNode;
|
|
436
|
+
// If a returned node is (an ancestor of) the root, this is the number of
|
|
437
|
+
// levels below the root, i.e. '' is 0, '..' is 1, '../..' is 2, otherwise
|
|
438
|
+
// null.
|
|
439
|
+
let ancestorOfRootIdx = opts.start?.ancestorOfRootIdx ?? 0;
|
|
440
|
+
const { collectAncestors, changeListener } = opts;
|
|
441
|
+
// Used only when collecting ancestors, to avoid double-counting nodes and
|
|
442
|
+
// paths when traversing a symlink takes us back to rootNode and out again.
|
|
443
|
+
// This tracks the first character of the first segment not already
|
|
444
|
+
// collected.
|
|
445
|
+
let unseenPathFromIdx = 0;
|
|
446
|
+
while (targetNormalPath.length > fromIdx) {
|
|
447
|
+
const nextSepIdx = targetNormalPath.indexOf(path_1.default.sep, fromIdx);
|
|
448
|
+
const isLastSegment = nextSepIdx === -1;
|
|
449
|
+
const segmentName = isLastSegment
|
|
450
|
+
? targetNormalPath.slice(fromIdx)
|
|
451
|
+
: targetNormalPath.slice(fromIdx, nextSepIdx);
|
|
452
|
+
const isUnseen = fromIdx >= unseenPathFromIdx;
|
|
453
|
+
fromIdx = !isLastSegment ? nextSepIdx + 1 : targetNormalPath.length;
|
|
454
|
+
if (segmentName === '.') {
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
let segmentNode = parentNode.get(segmentName);
|
|
458
|
+
// In normal paths all indirections are at the prefix, so we are at the
|
|
459
|
+
// nth ancestor of the root iff the path so far is n '..' segments.
|
|
460
|
+
if (segmentName === '..' && ancestorOfRootIdx != null) {
|
|
461
|
+
ancestorOfRootIdx++;
|
|
462
|
+
}
|
|
463
|
+
else if (segmentNode != null) {
|
|
464
|
+
ancestorOfRootIdx = null;
|
|
465
|
+
}
|
|
466
|
+
if (segmentNode == null) {
|
|
467
|
+
if (opts.makeDirectories !== true && segmentName !== '..') {
|
|
468
|
+
if (!opts.skipFallback && this.#fallbackFilesystem != null) {
|
|
469
|
+
const parentEnd = isLastSegment
|
|
470
|
+
? fromIdx - segmentName.length - 1
|
|
471
|
+
: fromIdx - segmentName.length - 2;
|
|
472
|
+
const parentCanonicalPath = parentEnd > 0 ? targetNormalPath.slice(0, parentEnd) : '';
|
|
473
|
+
segmentNode = this.#populateFromFilesystem(parentNode, segmentName, parentCanonicalPath);
|
|
474
|
+
if (segmentNode != null) {
|
|
475
|
+
ancestorOfRootIdx = null;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (segmentNode == null) {
|
|
479
|
+
return {
|
|
480
|
+
canonicalMissingPath: isLastSegment
|
|
481
|
+
? targetNormalPath
|
|
482
|
+
: targetNormalPath.slice(0, fromIdx - 1),
|
|
483
|
+
exists: false,
|
|
484
|
+
missingSegmentName: segmentName,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (segmentNode == null) {
|
|
489
|
+
segmentNode = new Map();
|
|
490
|
+
if (opts.makeDirectories === true) {
|
|
491
|
+
if (changeListener != null) {
|
|
492
|
+
const canonicalPath = isLastSegment
|
|
493
|
+
? targetNormalPath
|
|
494
|
+
: targetNormalPath.slice(0, fromIdx - 1);
|
|
495
|
+
changeListener.directoryAdded(canonicalPath);
|
|
496
|
+
}
|
|
497
|
+
parentNode.set(segmentName, segmentNode);
|
|
498
|
+
}
|
|
499
|
+
else if (!opts.skipFallback &&
|
|
500
|
+
this.#fallbackFilesystem != null) {
|
|
501
|
+
parentNode.set(segmentName, segmentNode);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
// We are done if...
|
|
506
|
+
if (
|
|
507
|
+
// ...at a directory node and the only subsequent character is `/`, or
|
|
508
|
+
(nextSepIdx === targetNormalPath.length - 1 && isDirectory(segmentNode)) ||
|
|
509
|
+
// there are no subsequent `/`, and this node is anything but a symlink
|
|
510
|
+
// we're required to resolve due to followLeaf.
|
|
511
|
+
(isLastSegment &&
|
|
512
|
+
(isDirectory(segmentNode) || isRegularFile(segmentNode) || opts.followLeaf === false))) {
|
|
513
|
+
return {
|
|
514
|
+
ancestorOfRootIdx,
|
|
515
|
+
canonicalPath: isLastSegment ? targetNormalPath : targetNormalPath.slice(0, -1), // remove trailing `/`
|
|
516
|
+
exists: true,
|
|
517
|
+
node: segmentNode,
|
|
518
|
+
parentNode,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
// If the next node is a directory, go into it
|
|
522
|
+
if (isDirectory(segmentNode)) {
|
|
523
|
+
parentNode = segmentNode;
|
|
524
|
+
if (collectAncestors && isUnseen) {
|
|
525
|
+
const currentPath = isLastSegment
|
|
526
|
+
? targetNormalPath
|
|
527
|
+
: targetNormalPath.slice(0, fromIdx - 1);
|
|
528
|
+
collectAncestors.push({
|
|
529
|
+
ancestorOfRootIdx,
|
|
530
|
+
node: segmentNode,
|
|
531
|
+
normalPath: currentPath,
|
|
532
|
+
segmentName,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
const currentPath = isLastSegment
|
|
538
|
+
? targetNormalPath
|
|
539
|
+
: targetNormalPath.slice(0, fromIdx - 1);
|
|
540
|
+
if (isRegularFile(segmentNode)) {
|
|
541
|
+
// Regular file in a directory path
|
|
542
|
+
return {
|
|
543
|
+
canonicalMissingPath: currentPath,
|
|
544
|
+
exists: false,
|
|
545
|
+
missingSegmentName: segmentName,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
// Symlink in a directory path
|
|
549
|
+
const normalSymlinkTarget = this.#resolveSymlinkTargetToNormalPath(segmentNode, currentPath);
|
|
550
|
+
if (normalSymlinkTarget == null) {
|
|
551
|
+
return {
|
|
552
|
+
canonicalMissingPath: currentPath,
|
|
553
|
+
exists: false,
|
|
554
|
+
missingSegmentName: segmentName,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
if (opts.collectLinkPaths) {
|
|
558
|
+
opts.collectLinkPaths.add(this.#pathUtils.normalToAbsolute(currentPath));
|
|
559
|
+
}
|
|
560
|
+
const remainingTargetPath = isLastSegment ? '' : targetNormalPath.slice(fromIdx);
|
|
561
|
+
// Append any subsequent path segments to the symlink target, and reset
|
|
562
|
+
// with our new target.
|
|
563
|
+
const joinedResult = this.#pathUtils.joinNormalToRelative(normalSymlinkTarget, remainingTargetPath);
|
|
564
|
+
targetNormalPath = joinedResult.normalPath;
|
|
565
|
+
// Two special cases (covered by unit tests):
|
|
566
|
+
//
|
|
567
|
+
// If the symlink target is the root, the root should be a counted as
|
|
568
|
+
// an ancestor. We'd otherwise miss counting it because we normally
|
|
569
|
+
// push new ancestors only when entering a directory.
|
|
570
|
+
//
|
|
571
|
+
// If the symlink target is an ancestor of the root *and* joining it
|
|
572
|
+
// with the remaining path results in collapsing segments, e.g:
|
|
573
|
+
// '../..' + 'parentofroot/root/foo.js' = 'foo.js', then we must add
|
|
574
|
+
// parentofroot and root as ancestors.
|
|
575
|
+
if (collectAncestors &&
|
|
576
|
+
!isLastSegment &&
|
|
577
|
+
// No-op optimisation to bail out the common case of nothing to do.
|
|
578
|
+
((ancestorOfRootIdx = this.#pathUtils.getAncestorOfRootIdx(normalSymlinkTarget)) === 0 ||
|
|
579
|
+
joinedResult.collapsedSegments > 0)) {
|
|
580
|
+
let node = this.#rootNode;
|
|
581
|
+
let collapsedPath = '';
|
|
582
|
+
const reverseAncestors = [];
|
|
583
|
+
for (let i = 0; i <= joinedResult.collapsedSegments && isDirectory(node); i++) {
|
|
584
|
+
if (
|
|
585
|
+
// Add the root only if the target is the root or we have
|
|
586
|
+
// collapsed segments.
|
|
587
|
+
i > 0 ||
|
|
588
|
+
ancestorOfRootIdx === 0 ||
|
|
589
|
+
joinedResult.collapsedSegments > 0) {
|
|
590
|
+
reverseAncestors.push({
|
|
591
|
+
ancestorOfRootIdx: i,
|
|
592
|
+
node,
|
|
593
|
+
normalPath: collapsedPath,
|
|
594
|
+
segmentName: this.#pathUtils.getBasenameOfNthAncestor(i),
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
node = node.get('..') ?? new Map();
|
|
598
|
+
collapsedPath = collapsedPath === '' ? '..' : collapsedPath + path_1.default.sep + '..';
|
|
599
|
+
}
|
|
600
|
+
collectAncestors.push(...reverseAncestors.reverse());
|
|
601
|
+
}
|
|
602
|
+
// For the purpose of collecting ancestors: Ignore the traversal to
|
|
603
|
+
// the symlink target, and start collecting ancestors only
|
|
604
|
+
// from the target itself (ie, the basename of the normal target path)
|
|
605
|
+
// onwards.
|
|
606
|
+
unseenPathFromIdx = normalSymlinkTarget.lastIndexOf(path_1.default.sep) + 1;
|
|
607
|
+
if (seen == null) {
|
|
608
|
+
// Optimisation: set this lazily only when we've encountered a symlink
|
|
609
|
+
seen = new Set([requestedNormalPath]);
|
|
610
|
+
}
|
|
611
|
+
if (seen.has(targetNormalPath)) {
|
|
612
|
+
// TODO: Warn `Symlink cycle detected: ${[...seen, node].join(' -> ')}`
|
|
613
|
+
return {
|
|
614
|
+
canonicalMissingPath: targetNormalPath,
|
|
615
|
+
exists: false,
|
|
616
|
+
missingSegmentName: segmentName,
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
seen.add(targetNormalPath);
|
|
620
|
+
fromIdx = 0;
|
|
621
|
+
parentNode = this.#rootNode;
|
|
622
|
+
ancestorOfRootIdx = 0;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
(0, invariant_1.default)(parentNode === this.#rootNode, 'Unexpectedly escaped traversal');
|
|
626
|
+
return {
|
|
627
|
+
ancestorOfRootIdx: 0,
|
|
628
|
+
canonicalPath: targetNormalPath,
|
|
629
|
+
exists: true,
|
|
630
|
+
node: this.#rootNode,
|
|
631
|
+
parentNode: null,
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Given a start path (which need not exist), a subpath and type, and
|
|
636
|
+
* optionally a 'breakOnSegment', performs the following:
|
|
637
|
+
*
|
|
638
|
+
* X = mixedStartPath
|
|
639
|
+
* do
|
|
640
|
+
* if basename(X) === opts.breakOnSegment
|
|
641
|
+
* return null
|
|
642
|
+
* if X + subpath exists and has type opts.subpathType
|
|
643
|
+
* return {
|
|
644
|
+
* absolutePath: realpath(X + subpath)
|
|
645
|
+
* containerRelativePath: relative(mixedStartPath, X)
|
|
646
|
+
* }
|
|
647
|
+
* X = dirname(X)
|
|
648
|
+
* while X !== dirname(X)
|
|
649
|
+
*
|
|
650
|
+
* If opts.invalidatedBy is given, collects all absolute, real paths that if
|
|
651
|
+
* added or removed may invalidate this result.
|
|
652
|
+
*
|
|
653
|
+
* Useful for finding the closest package scope (subpath: package.json,
|
|
654
|
+
* type f, breakOnSegment: node_modules) or closest potential package root
|
|
655
|
+
* (subpath: node_modules/pkg, type: d) in Node.js resolution.
|
|
656
|
+
*/
|
|
657
|
+
hierarchicalLookup(mixedStartPath, subpath, opts) {
|
|
658
|
+
const ancestorsOfInput = [];
|
|
659
|
+
const normalPath = this.#normalizePath(mixedStartPath);
|
|
660
|
+
const invalidatedBy = opts.invalidatedBy;
|
|
661
|
+
const closestLookup = this.#lookupByNormalPath(normalPath, {
|
|
662
|
+
collectAncestors: ancestorsOfInput,
|
|
663
|
+
collectLinkPaths: invalidatedBy,
|
|
664
|
+
});
|
|
665
|
+
if (closestLookup.exists && isDirectory(closestLookup.node)) {
|
|
666
|
+
const maybeAbsolutePathMatch = this.#checkCandidateHasSubpath(closestLookup.canonicalPath, subpath, opts.subpathType, invalidatedBy, null);
|
|
667
|
+
if (maybeAbsolutePathMatch != null) {
|
|
668
|
+
return {
|
|
669
|
+
absolutePath: maybeAbsolutePathMatch,
|
|
670
|
+
containerRelativePath: '',
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
else {
|
|
675
|
+
if (invalidatedBy && (!closestLookup.exists || !isDirectory(closestLookup.node))) {
|
|
676
|
+
invalidatedBy.add(this.#pathUtils.normalToAbsolute(closestLookup.exists ? closestLookup.canonicalPath : closestLookup.canonicalMissingPath));
|
|
677
|
+
}
|
|
678
|
+
if (opts.breakOnSegment != null &&
|
|
679
|
+
!closestLookup.exists &&
|
|
680
|
+
closestLookup.missingSegmentName === opts.breakOnSegment) {
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
// Let the "common root" be the nearest common ancestor of this.rootDir
|
|
685
|
+
// and the input path. We'll look for a match in two stages:
|
|
686
|
+
// 1. Every collected ancestor of the input path, from nearest to furthest,
|
|
687
|
+
// that is a descendent of the common root
|
|
688
|
+
// 2. The common root, and its ancestors.
|
|
689
|
+
let commonRoot = this.#rootNode;
|
|
690
|
+
let commonRootDepth = 0;
|
|
691
|
+
// Collected ancestors do not include the lookup result itself, so go one
|
|
692
|
+
// further if the input path is itself a root ancestor.
|
|
693
|
+
if (closestLookup.exists && closestLookup.ancestorOfRootIdx != null) {
|
|
694
|
+
commonRootDepth = closestLookup.ancestorOfRootIdx;
|
|
695
|
+
(0, invariant_1.default)(isDirectory(closestLookup.node), 'ancestors of the root must be directories');
|
|
696
|
+
commonRoot = closestLookup.node;
|
|
697
|
+
}
|
|
698
|
+
else {
|
|
699
|
+
// Establish the common root by counting the '..' segments at the start
|
|
700
|
+
// of the collected ancestors.
|
|
701
|
+
for (const ancestor of ancestorsOfInput) {
|
|
702
|
+
if (ancestor.ancestorOfRootIdx == null) {
|
|
703
|
+
break;
|
|
704
|
+
}
|
|
705
|
+
commonRootDepth = ancestor.ancestorOfRootIdx;
|
|
706
|
+
commonRoot = ancestor.node;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
// Phase 1: Consider descendenants of the common root, from deepest to
|
|
710
|
+
// shallowest.
|
|
711
|
+
for (let candidateIdx = ancestorsOfInput.length - 1; candidateIdx >= commonRootDepth; --candidateIdx) {
|
|
712
|
+
const candidate = ancestorsOfInput[candidateIdx];
|
|
713
|
+
if (candidate.segmentName === opts.breakOnSegment) {
|
|
714
|
+
return null;
|
|
715
|
+
}
|
|
716
|
+
const maybeAbsolutePathMatch = this.#checkCandidateHasSubpath(candidate.normalPath, subpath, opts.subpathType, invalidatedBy, {
|
|
717
|
+
ancestorOfRootIdx: candidate.ancestorOfRootIdx,
|
|
718
|
+
node: candidate.node,
|
|
719
|
+
pathIdx: candidate.normalPath.length > 0 ? candidate.normalPath.length + 1 : 0,
|
|
720
|
+
});
|
|
721
|
+
if (maybeAbsolutePathMatch != null) {
|
|
722
|
+
// Determine the input path relative to the current candidate. Note
|
|
723
|
+
// that the candidate path will always be canonical (real), whereas the
|
|
724
|
+
// input may contain symlinks, so the candidate is not necessarily a
|
|
725
|
+
// prefix of the input. Use the fact that each remaining candidate
|
|
726
|
+
// corresponds to a leading segment of the input normal path, and
|
|
727
|
+
// discard the first candidateIdx + 1 segments of the input path.
|
|
728
|
+
//
|
|
729
|
+
// The next 5 lines are equivalent to (but faster than)
|
|
730
|
+
// normalPath.split('/').slice(candidateIdx + 1).join('/').
|
|
731
|
+
let prefixLength = commonRootDepth * 3; // Leading '../'
|
|
732
|
+
for (let i = commonRootDepth; i <= candidateIdx; i++) {
|
|
733
|
+
prefixLength = normalPath.indexOf(path_1.default.sep, prefixLength + 1);
|
|
734
|
+
}
|
|
735
|
+
const containerRelativePath = normalPath.slice(prefixLength + 1);
|
|
736
|
+
return {
|
|
737
|
+
absolutePath: maybeAbsolutePathMatch,
|
|
738
|
+
containerRelativePath,
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
// Phase 2: Consider the common root and its ancestors
|
|
743
|
+
// This will be '', '..', '../..', etc.
|
|
744
|
+
let candidateNormalPath = commonRootDepth > 0 ? normalPath.slice(0, 3 * commonRootDepth - 1) : '';
|
|
745
|
+
const remainingNormalPath = normalPath.slice(commonRootDepth * 3);
|
|
746
|
+
let nextNode = commonRoot;
|
|
747
|
+
let depthBelowCommonRoot = 0;
|
|
748
|
+
while (isDirectory(nextNode)) {
|
|
749
|
+
const maybeAbsolutePathMatch = this.#checkCandidateHasSubpath(candidateNormalPath, subpath, opts.subpathType, invalidatedBy, null);
|
|
750
|
+
if (maybeAbsolutePathMatch != null) {
|
|
751
|
+
const rootDirParts = this.#pathUtils.getParts();
|
|
752
|
+
const relativeParts = depthBelowCommonRoot > 0
|
|
753
|
+
? rootDirParts.slice(-(depthBelowCommonRoot + commonRootDepth), commonRootDepth > 0 ? -commonRootDepth : undefined)
|
|
754
|
+
: [];
|
|
755
|
+
if (remainingNormalPath !== '') {
|
|
756
|
+
relativeParts.push(remainingNormalPath);
|
|
757
|
+
}
|
|
758
|
+
return {
|
|
759
|
+
absolutePath: maybeAbsolutePathMatch,
|
|
760
|
+
containerRelativePath: relativeParts.join(path_1.default.sep),
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
depthBelowCommonRoot++;
|
|
764
|
+
candidateNormalPath =
|
|
765
|
+
candidateNormalPath === '' ? '..' : candidateNormalPath + path_1.default.sep + '..';
|
|
766
|
+
nextNode = nextNode.get('..');
|
|
767
|
+
}
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
#checkCandidateHasSubpath(normalCandidatePath, subpath, subpathType, invalidatedBy, start) {
|
|
771
|
+
const lookupResult = this.#lookupByNormalPath(this.#pathUtils.joinNormalToRelative(normalCandidatePath, subpath).normalPath, {
|
|
772
|
+
collectLinkPaths: invalidatedBy,
|
|
773
|
+
});
|
|
774
|
+
if (lookupResult.exists &&
|
|
775
|
+
// Should be a Map iff subpathType is directory
|
|
776
|
+
isDirectory(lookupResult.node) === (subpathType === 'd')) {
|
|
777
|
+
return this.#pathUtils.normalToAbsolute(lookupResult.canonicalPath);
|
|
778
|
+
}
|
|
779
|
+
else if (invalidatedBy) {
|
|
780
|
+
invalidatedBy.add(this.#pathUtils.normalToAbsolute(lookupResult.exists ? lookupResult.canonicalPath : lookupResult.canonicalMissingPath));
|
|
781
|
+
}
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
*metadataIterator(opts) {
|
|
785
|
+
yield* this.#metadataIterator(this.#rootNode, opts);
|
|
786
|
+
}
|
|
787
|
+
*#metadataIterator(rootNode, opts, prefix = '') {
|
|
788
|
+
for (const [name, node] of rootNode) {
|
|
789
|
+
if (node == null) {
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
else if (!opts.includeNodeModules && isDirectory(node) && name === 'node_modules') {
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
const prefixedName = prefix === '' ? name : prefix + path_1.default.sep + name;
|
|
796
|
+
if (isDirectory(node)) {
|
|
797
|
+
yield* this.#metadataIterator(node, opts, prefixedName);
|
|
798
|
+
}
|
|
799
|
+
else if (isRegularFile(node) || opts.includeSymlinks) {
|
|
800
|
+
yield { baseName: name, canonicalPath: prefixedName, metadata: node };
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
#normalizePath(relativeOrAbsolutePath) {
|
|
805
|
+
return path_1.default.isAbsolute(relativeOrAbsolutePath)
|
|
806
|
+
? this.#pathUtils.absoluteToNormal(relativeOrAbsolutePath)
|
|
807
|
+
: this.#pathUtils.relativeToNormal(relativeOrAbsolutePath);
|
|
808
|
+
}
|
|
809
|
+
*#directoryNodeIterator(node, parent, ancestorOfRootIdx) {
|
|
810
|
+
if (ancestorOfRootIdx != null && ancestorOfRootIdx > 0 && parent) {
|
|
811
|
+
yield [this.#pathUtils.getBasenameOfNthAncestor(ancestorOfRootIdx - 1), parent];
|
|
812
|
+
}
|
|
813
|
+
yield* node.entries();
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Enumerate paths under a given node, including symlinks and through
|
|
817
|
+
* symlinks (if `follow` is enabled).
|
|
818
|
+
*/
|
|
819
|
+
*#pathIterator(iterationRootNode, iterationRootParentNode, ancestorOfRootIdx, opts, pathPrefix = '', followedLinks = new Set()) {
|
|
820
|
+
const pathSep = opts.alwaysYieldPosix ? '/' : path_1.default.sep;
|
|
821
|
+
const prefixWithSep = pathPrefix === '' ? pathPrefix : pathPrefix + pathSep;
|
|
822
|
+
// Optimization: We can attempt to eagerly populate directories we're visiting
|
|
823
|
+
// if they're missing and not accessing a parent ('..')
|
|
824
|
+
if (this.#fallbackFilesystem != null &&
|
|
825
|
+
iterationRootNode.size === 0 &&
|
|
826
|
+
pathPrefix !== '..' &&
|
|
827
|
+
!pathPrefix.endsWith(pathSep + '..')) {
|
|
828
|
+
const canonicalRoot = opts.canonicalPathOfRoot;
|
|
829
|
+
const rootCanonical = pathPrefix === '' ? canonicalRoot : canonicalRoot + path_1.default.sep + pathPrefix;
|
|
830
|
+
this.#populateDirFromFilesystem(iterationRootNode, rootCanonical, false);
|
|
831
|
+
}
|
|
832
|
+
for (const [name, node] of this.#directoryNodeIterator(iterationRootNode, iterationRootParentNode, ancestorOfRootIdx)) {
|
|
833
|
+
if (node == null) {
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
else if (opts.subtreeOnly && name === '..') {
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
const nodePath = prefixWithSep + name;
|
|
840
|
+
if (!isDirectory(node)) {
|
|
841
|
+
if (isRegularFile(node)) {
|
|
842
|
+
// regular file
|
|
843
|
+
yield nodePath;
|
|
844
|
+
}
|
|
845
|
+
else {
|
|
846
|
+
// symlink
|
|
847
|
+
const nodePathWithSystemSeparators = pathSep === path_1.default.sep ? nodePath : nodePath.replaceAll(pathSep, path_1.default.sep);
|
|
848
|
+
// Although both paths are normal, the node path may begin '..' so we
|
|
849
|
+
// can't simply concatenate.
|
|
850
|
+
const normalPathOfSymlink = path_1.default.join(opts.canonicalPathOfRoot, nodePathWithSystemSeparators);
|
|
851
|
+
// We can't resolve the symlink directly here because we only have
|
|
852
|
+
// its normal path, and we need a canonical path for resolution
|
|
853
|
+
// (imagine our normal path contains a symlink 'bar' -> '.', and we
|
|
854
|
+
// are at /foo/bar/baz where baz -> '..' - that should resolve to
|
|
855
|
+
// /foo, not /foo/bar). We *can* use _lookupByNormalPath to walk to
|
|
856
|
+
// the canonical symlink, and then to its target.
|
|
857
|
+
const resolved = this.#lookupByNormalPath(normalPathOfSymlink, {
|
|
858
|
+
followLeaf: true,
|
|
859
|
+
});
|
|
860
|
+
if (!resolved.exists) {
|
|
861
|
+
// Symlink goes nowhere, nothing to report.
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
const target = resolved.node;
|
|
865
|
+
if (!isDirectory(target)) {
|
|
866
|
+
// Symlink points to a file, just yield the path of the symlink.
|
|
867
|
+
yield nodePath;
|
|
868
|
+
}
|
|
869
|
+
else if (opts.recursive && opts.follow && !followedLinks.has(node)) {
|
|
870
|
+
// Symlink points to a directory - iterate over its contents using
|
|
871
|
+
// the path where we found the symlink as a prefix.
|
|
872
|
+
yield* this.#pathIterator(target, resolved.parentNode, resolved.ancestorOfRootIdx, opts, nodePath, new Set([...followedLinks, node]));
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
else if (opts.recursive) {
|
|
877
|
+
// Optimization: We can attempt to eagerly popuplate directories we're visiting
|
|
878
|
+
// if they're missing and not accessing a parent ('..')
|
|
879
|
+
if (this.#fallbackFilesystem != null && node.size === 0 && name !== '..') {
|
|
880
|
+
const nodePathWithSystemSeparators = pathSep === path_1.default.sep ? nodePath : nodePath.replaceAll(pathSep, path_1.default.sep);
|
|
881
|
+
const canonicalPath = opts.canonicalPathOfRoot === ''
|
|
882
|
+
? nodePathWithSystemSeparators
|
|
883
|
+
: opts.canonicalPathOfRoot + path_1.default.sep + nodePathWithSystemSeparators;
|
|
884
|
+
this.#populateDirFromFilesystem(node, canonicalPath, false);
|
|
885
|
+
}
|
|
886
|
+
yield* this.#pathIterator(node, iterationRootParentNode, ancestorOfRootIdx != null && ancestorOfRootIdx > 0 ? ancestorOfRootIdx - 1 : null, opts, nodePath, followedLinks);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
#resolveSymlinkTargetToNormalPath(symlinkNode, canonicalPathOfSymlink) {
|
|
891
|
+
const symlinkTarget = symlinkNode[constants_1.default.SYMLINK];
|
|
892
|
+
if (symlinkTarget === 1) {
|
|
893
|
+
// Symlink target not yet resolved — read it lazily on first traversal
|
|
894
|
+
const absoluteSymlink = this.#pathUtils.normalToAbsolute(canonicalPathOfSymlink);
|
|
895
|
+
try {
|
|
896
|
+
const literalSymlinkTarget = fs_1.default.readlinkSync(absoluteSymlink);
|
|
897
|
+
const normalTarget = this.#pathUtils.resolveSymlinkToNormal(canonicalPathOfSymlink, literalSymlinkTarget);
|
|
898
|
+
symlinkNode[constants_1.default.SYMLINK] = (0, normalizePathSeparatorsToPosix_1.default)(normalTarget);
|
|
899
|
+
symlinkNode[constants_1.default.VISITED] = 1;
|
|
900
|
+
return normalTarget;
|
|
901
|
+
}
|
|
902
|
+
catch {
|
|
903
|
+
return null;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
else if (symlinkTarget === 0 || symlinkTarget == null) {
|
|
907
|
+
// WARN: We shouldn't call this method on non-symlinks. Outside of tests
|
|
908
|
+
// this condition shouldn't trigger. It's fine not to resolve a symlink if
|
|
909
|
+
// it does trigger however
|
|
910
|
+
return null;
|
|
911
|
+
}
|
|
912
|
+
else {
|
|
913
|
+
(0, invariant_1.default)(typeof symlinkTarget === 'string', 'Expected symlink target to be populated.');
|
|
914
|
+
return (0, normalizePathSeparatorsToSystem_1.default)(symlinkTarget);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
#getFileData(filePath, opts = { followLeaf: true }) {
|
|
918
|
+
const normalPath = this.#normalizePath(filePath);
|
|
919
|
+
const result = this.#lookupByNormalPath(normalPath, {
|
|
920
|
+
followLeaf: opts.followLeaf,
|
|
921
|
+
});
|
|
922
|
+
if (!result.exists || isDirectory(result.node)) {
|
|
923
|
+
return null;
|
|
924
|
+
}
|
|
925
|
+
return result.node;
|
|
926
|
+
}
|
|
927
|
+
#cloneTree(root, prefix) {
|
|
928
|
+
const clone = new Map();
|
|
929
|
+
for (const [name, node] of root) {
|
|
930
|
+
if (node == null) {
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
else if (isDirectory(node)) {
|
|
934
|
+
const childPath = prefix === '' ? name : prefix + path_1.default.sep + name;
|
|
935
|
+
if (this.#rootPattern == null || this.#rootPattern.test(childPath + path_1.default.sep)) {
|
|
936
|
+
clone.set(name, this.#cloneTree(node, childPath));
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
else {
|
|
940
|
+
clone.set(name, [...node]);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
return clone;
|
|
944
|
+
}
|
|
945
|
+
#isOutsideFallbackBoundary(canonicalPath) {
|
|
946
|
+
const maxDepth = this.#fallbackBoundaryDepth;
|
|
947
|
+
return maxDepth != null && (0, RootPathUtils_1.getAncestorOfRootIdx)(canonicalPath) > maxDepth;
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* Synchronously populate a missing tree node by querying the injected
|
|
951
|
+
* fallback filesystem. The fallback returns tree-compatible nodes
|
|
952
|
+
* (FileMetadata tuples or directory Maps) that are inserted directly.
|
|
953
|
+
*
|
|
954
|
+
* Returns the newly created node, or null if the path doesn't exist on disk.
|
|
955
|
+
*/
|
|
956
|
+
#populateFromFilesystem(parentNode, segmentName, parentCanonicalPath) {
|
|
957
|
+
const fallback = this.#fallbackFilesystem;
|
|
958
|
+
if (fallback == null) {
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
const childCanonicalPath = parentCanonicalPath === '' ? segmentName : parentCanonicalPath + path_1.default.sep + segmentName;
|
|
962
|
+
if (this.#rootPattern?.test(childCanonicalPath + path_1.default.sep) ||
|
|
963
|
+
this.#isOutsideFallbackBoundary(childCanonicalPath)) {
|
|
964
|
+
return null;
|
|
965
|
+
}
|
|
966
|
+
else if (parentCanonicalPath !== '' && (0, fallback_1.shouldFallbackCrawlDir)(parentCanonicalPath)) {
|
|
967
|
+
this.#populateDirFromFilesystem(parentNode, parentCanonicalPath, true);
|
|
968
|
+
return parentNode.get(segmentName) ?? null;
|
|
969
|
+
}
|
|
970
|
+
else if (parentNode.has(segmentName)) {
|
|
971
|
+
return parentNode.get(segmentName) ?? null;
|
|
972
|
+
}
|
|
973
|
+
else {
|
|
974
|
+
const parentAbsolute = this.#pathUtils.normalToAbsolute(parentCanonicalPath);
|
|
975
|
+
const absolutePath = parentAbsolute + path_1.default.sep + segmentName;
|
|
976
|
+
const node = fallback.lookup(childCanonicalPath, absolutePath, parentNode.get(segmentName));
|
|
977
|
+
parentNode.set(segmentName, node);
|
|
978
|
+
return node;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Populate an existing (potentially empty sentinel) directory node from
|
|
983
|
+
* the filesystem. Used by #pathIterator to fill lazy directories before
|
|
984
|
+
* iteration, and by #populateFromFilesystem for optimistic parent
|
|
985
|
+
* population.
|
|
986
|
+
*/
|
|
987
|
+
#populateDirFromFilesystem(dirNode, canonicalPath, skipCheck) {
|
|
988
|
+
const fallback = this.#fallbackFilesystem;
|
|
989
|
+
if (fallback == null ||
|
|
990
|
+
(!skipCheck &&
|
|
991
|
+
(this.#rootPattern?.test(canonicalPath + path_1.default.sep) ||
|
|
992
|
+
this.#isOutsideFallbackBoundary(canonicalPath)))) {
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
const absolutePath = this.#pathUtils.normalToAbsolute(canonicalPath);
|
|
996
|
+
const entries = fallback.readdir(canonicalPath, absolutePath, dirNode);
|
|
997
|
+
if (entries != null && entries !== dirNode) {
|
|
998
|
+
for (const [name, entry] of entries) {
|
|
999
|
+
if (!dirNode.has(name)) {
|
|
1000
|
+
dirNode.set(name, entry);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
exports.default = TreeFS;
|