PHP WebShell
Текущая директория: /usr/lib/node_modules/bitgo/node_modules/metro-file-map/src/lib
Просмотр файла: TreeFS.js.flow
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow strict-local
*/
import type {
CacheData,
FileData,
FileMetadata,
FileStats,
LookupResult,
MutableFileSystem,
Path,
ProcessFileFunction,
} from '../flow-types';
import H from '../constants';
import {RootPathUtils} from './RootPathUtils';
import invariant from 'invariant';
import path from 'path';
type DirectoryNode = Map<string, MixedNode>;
type FileNode = FileMetadata;
type MixedNode = FileNode | DirectoryNode;
function isDirectory(node: ?MixedNode): node is DirectoryNode {
return node instanceof Map;
}
function isRegularFile(node: FileNode): boolean {
return node[H.SYMLINK] === 0;
}
type NormalizedSymlinkTarget = {
ancestorOfRootIdx: ?number,
normalPath: string,
startOfBasenameIdx: number,
};
/**
* OVERVIEW:
*
* TreeFS is Metro's in-memory representation of the file system. It is
* structured as a tree of non-empty maps and leaves (tuples), with the root
* node representing the given `rootDir`, typically Metro's _project root_
* (not a filesystem root). Map keys are path segments, and branches outside
* the project root are accessed via `'..'`.
*
* EXAMPLE:
*
* For a root dir '/data/project', the file '/data/other/app/index.js' would
* have metadata at #rootNode.get('..').get('other').get('app').get('index.js')
*
* SERIALISATION:
*
* #rootNode is designed to be directly serialisable and directly portable (for
* a given project) between different root directories and operating systems.
*
* SYMLINKS:
*
* Symlinks are represented as nodes whose metadata contains their literal
* target. Literal targets are resolved to normal paths at runtime, and cached.
* If a symlink is encountered during traversal, we restart traversal at the
* root node targeting join(normal symlink target, remaining path suffix).
*
* NODE TYPES:
*
* - A directory (including a parent directory at '..') is represented by a
* `Map` of basenames to any other node type.
* - A file is represented by an `Array` (tuple) of metadata, of which:
* - A regular file has node[H.SYMLINK] === 0
* - A symlink has node[H.SYMLINK] === 1 or
* typeof node[H.SYMLINK] === 'string', where a string is the literal
* content of the symlink (i.e. from readlink), if known.
*
* TERMINOLOGY:
*
* - mixedPath
* A root-relative or absolute path
* - relativePath
* A root-relative path
* - normalPath
* A root-relative, normalised path (no extraneous '.' or '..'), may have a
* single trailing slash
* - canonicalPath
* A root-relative, normalised, real path (no symlinks in dirname), never has
* a trailing slash
*/
export default class TreeFS implements MutableFileSystem {
+#cachedNormalSymlinkTargets: WeakMap<FileNode, NormalizedSymlinkTarget> =
new WeakMap();
+#rootDir: Path;
#rootNode: DirectoryNode = new Map();
#pathUtils: RootPathUtils;
#processFile: ProcessFileFunction;
constructor({
rootDir,
files,
processFile,
}: {
rootDir: Path,
files?: FileData,
processFile: ProcessFileFunction,
}) {
this.#rootDir = rootDir;
this.#pathUtils = new RootPathUtils(rootDir);
this.#processFile = processFile;
if (files != null) {
this.bulkAddOrModify(files);
}
}
getSerializableSnapshot(): CacheData['fileSystemData'] {
return this._cloneTree(this.#rootNode);
}
static fromDeserializedSnapshot({
rootDir,
fileSystemData,
processFile,
}: {
rootDir: string,
fileSystemData: DirectoryNode,
processFile: ProcessFileFunction,
}): TreeFS {
const tfs = new TreeFS({rootDir, processFile});
tfs.#rootNode = fileSystemData;
return tfs;
}
getModuleName(mixedPath: Path): ?string {
const fileMetadata = this._getFileData(mixedPath);
return (fileMetadata && fileMetadata[H.ID]) ?? null;
}
getSize(mixedPath: Path): ?number {
const fileMetadata = this._getFileData(mixedPath);
return (fileMetadata && fileMetadata[H.SIZE]) ?? null;
}
getDependencies(mixedPath: Path): ?Array<string> {
const fileMetadata = this._getFileData(mixedPath);
if (fileMetadata) {
return fileMetadata[H.DEPENDENCIES]
? fileMetadata[H.DEPENDENCIES].split(H.DEPENDENCY_DELIM)
: [];
} else {
return null;
}
}
getDifference(files: FileData): {
changedFiles: FileData,
removedFiles: Set<string>,
} {
const changedFiles: FileData = new Map(files);
const removedFiles: Set<string> = new Set();
for (const {canonicalPath, metadata} of this.metadataIterator({
includeSymlinks: true,
includeNodeModules: true,
})) {
const newMetadata = files.get(canonicalPath);
if (newMetadata) {
if (isRegularFile(newMetadata) !== isRegularFile(metadata)) {
// Types differ, file has changed
continue;
}
if (
newMetadata[H.MTIME] != null &&
// TODO: Remove when mtime is null if not populated
newMetadata[H.MTIME] != 0 &&
newMetadata[H.MTIME] === metadata[H.MTIME]
) {
// Types and modified time match - not changed.
changedFiles.delete(canonicalPath);
} else if (
newMetadata[H.SHA1] != null &&
newMetadata[H.SHA1] === metadata[H.SHA1] &&
metadata[H.VISITED] === 1
) {
// Content matches - update modified time but don't revisit
const updatedMetadata = [...metadata];
updatedMetadata[H.MTIME] = newMetadata[H.MTIME];
changedFiles.set(canonicalPath, updatedMetadata);
}
} else {
removedFiles.add(canonicalPath);
}
}
return {
changedFiles,
removedFiles,
};
}
getSha1(mixedPath: Path): ?string {
const fileMetadata = this._getFileData(mixedPath);
return (fileMetadata && fileMetadata[H.SHA1]) ?? null;
}
async getOrComputeSha1(
mixedPath: Path,
): Promise<?{sha1: string, content?: Buffer}> {
const normalPath = this._normalizePath(mixedPath);
const result = this._lookupByNormalPath(normalPath, {
followLeaf: true,
});
if (!result.exists || isDirectory(result.node)) {
return null;
}
const {canonicalPath, node: fileMetadata} = result;
// Empty strings
const existing = fileMetadata[H.SHA1];
if (existing != null && existing.length > 0) {
return {sha1: existing};
}
const absolutePath = this.#pathUtils.normalToAbsolute(canonicalPath);
// Mutate the metadata we first retrieved. This may be orphaned or about
// to be overwritten if the file changes while we are processing it -
// by only mutating the original metadata, we don't risk caching a stale
// SHA-1 after a change event.
const maybeContent = await this.#processFile(absolutePath, fileMetadata, {
computeSha1: true,
});
const sha1 = fileMetadata[H.SHA1];
invariant(
sha1 != null && sha1.length > 0,
"File processing didn't populate a SHA-1 hash for %s",
absolutePath,
);
return maybeContent
? {
sha1,
content: maybeContent,
}
: {sha1};
}
exists(mixedPath: Path): boolean {
const result = this._getFileData(mixedPath);
return result != null;
}
lookup(mixedPath: Path): LookupResult {
const normalPath = this._normalizePath(mixedPath);
const links = new Set<string>();
const result = this._lookupByNormalPath(normalPath, {
collectLinkPaths: links,
followLeaf: true,
});
if (!result.exists) {
const {canonicalMissingPath} = result;
return {
exists: false,
links,
missing: this.#pathUtils.normalToAbsolute(canonicalMissingPath),
};
}
const {canonicalPath, node} = result;
const type = isDirectory(node) ? 'd' : isRegularFile(node) ? 'f' : 'l';
invariant(
type !== 'l',
'lookup follows symlinks, so should never return one (%s -> %s)',
mixedPath,
canonicalPath,
);
return {
exists: true,
links,
realPath: this.#pathUtils.normalToAbsolute(canonicalPath),
type,
};
}
getAllFiles(): Array<Path> {
return Array.from(
this.metadataIterator({
includeSymlinks: false,
includeNodeModules: true,
}),
({canonicalPath}) => this.#pathUtils.normalToAbsolute(canonicalPath),
);
}
linkStats(mixedPath: Path): ?FileStats {
const fileMetadata = this._getFileData(mixedPath, {followLeaf: false});
if (fileMetadata == null) {
return null;
}
const fileType = isRegularFile(fileMetadata) ? 'f' : 'l';
return {
fileType,
modifiedTime: fileMetadata[H.MTIME],
size: fileMetadata[H.SIZE],
};
}
/**
* Given a search context, return a list of file paths matching the query.
* The query matches against normalized paths which start with `./`,
* for example: `a/b.js` -> `./a/b.js`
*/
*matchFiles({
filter = null,
filterCompareAbsolute = false,
filterComparePosix = false,
follow = false,
recursive = true,
rootDir = null,
}: $ReadOnly<{
/* Filter relative paths against a pattern. */
filter?: ?RegExp,
/* `filter` is applied against absolute paths, vs rootDir-relative. (default: false) */
filterCompareAbsolute?: boolean,
/* `filter` is applied against posix-delimited paths, even on Windows. (default: false) */
filterComparePosix?: boolean,
/* Follow symlinks when enumerating paths. (default: false) */
follow?: boolean,
/* Should search for files recursively. (default: true) */
recursive?: boolean,
/* Match files under a given root, or null for all files */
rootDir?: ?Path,
}>): Iterable<Path> {
const normalRoot = rootDir == null ? '' : this._normalizePath(rootDir);
const contextRootResult = this._lookupByNormalPath(normalRoot);
if (!contextRootResult.exists) {
return;
}
const {
ancestorOfRootIdx,
canonicalPath: rootRealPath,
node: contextRoot,
parentNode: contextRootParent,
} = contextRootResult;
if (!isDirectory(contextRoot)) {
return;
}
const contextRootAbsolutePath =
rootRealPath === ''
? this.#rootDir
: path.join(this.#rootDir, rootRealPath);
const prefix = filterComparePosix ? './' : '.' + path.sep;
const contextRootAbsolutePathForComparison =
filterComparePosix && path.sep !== '/'
? contextRootAbsolutePath.replaceAll(path.sep, '/')
: contextRootAbsolutePath;
for (const relativePathForComparison of this._pathIterator(
contextRoot,
contextRootParent,
ancestorOfRootIdx,
{
alwaysYieldPosix: filterComparePosix,
canonicalPathOfRoot: rootRealPath,
follow,
recursive,
subtreeOnly: rootDir != null,
},
)) {
if (
filter == null ||
filter.test(
// NOTE(EvanBacon): Ensure files start with `./` for matching purposes
// this ensures packages work across Metro and Webpack (ex: Storybook for React DOM / React Native).
// `a/b.js` -> `./a/b.js`
filterCompareAbsolute === true
? path.join(
contextRootAbsolutePathForComparison,
relativePathForComparison,
)
: prefix + relativePathForComparison,
)
) {
const relativePath =
filterComparePosix === true && path.sep !== '/'
? relativePathForComparison.replaceAll('/', path.sep)
: relativePathForComparison;
yield path.join(contextRootAbsolutePath, relativePath);
}
}
}
addOrModify(mixedPath: Path, metadata: FileMetadata): void {
const normalPath = this._normalizePath(mixedPath);
// Walk the tree to find the *real* path of the parent node, creating
// directories as we need.
const parentDirNode = this._lookupByNormalPath(path.dirname(normalPath), {
makeDirectories: true,
});
if (!parentDirNode.exists) {
throw new Error(
`TreeFS: Failed to make parent directory entry for ${mixedPath}`,
);
}
// Normalize the resulting path to account for the parent node being root.
const canonicalPath = this._normalizePath(
parentDirNode.canonicalPath + path.sep + path.basename(normalPath),
);
this.bulkAddOrModify(new Map([[canonicalPath, metadata]]));
}
bulkAddOrModify(addedOrModifiedFiles: FileData): void {
// Optimisation: Bulk FileData are typically clustered by directory, so we
// optimise for that case by remembering the last directory we looked up.
// Experiments with large result sets show this to be significantly (~30%)
// faster than caching all lookups in a Map, and 70% faster than no cache.
let lastDir: ?string;
let directoryNode: DirectoryNode;
for (const [normalPath, metadata] of addedOrModifiedFiles) {
const lastSepIdx = normalPath.lastIndexOf(path.sep);
const dirname = lastSepIdx === -1 ? '' : normalPath.slice(0, lastSepIdx);
const basename =
lastSepIdx === -1 ? normalPath : normalPath.slice(lastSepIdx + 1);
if (directoryNode == null || dirname !== lastDir) {
const lookup = this._lookupByNormalPath(dirname, {
followLeaf: false,
makeDirectories: true,
});
if (!lookup.exists) {
// This should only be possible if the input is non-real and
// lookup hits a broken symlink.
throw new Error(
`TreeFS: Unexpected error adding ${normalPath}.\nMissing: ` +
lookup.canonicalMissingPath,
);
}
if (!isDirectory(lookup.node)) {
throw new Error(
`TreeFS: Could not add directory ${dirname}, adding ${normalPath}. ` +
`${dirname} already exists in the file map as a file.`,
);
}
lastDir = dirname;
directoryNode = lookup.node;
}
directoryNode.set(basename, metadata);
}
}
remove(mixedPath: Path): ?FileMetadata {
const normalPath = this._normalizePath(mixedPath);
const result = this._lookupByNormalPath(normalPath, {followLeaf: false});
if (!result.exists) {
return null;
}
const {parentNode, canonicalPath, node} = result;
if (isDirectory(node) && node.size > 0) {
throw new Error(
`TreeFS: remove called on a non-empty directory: ${mixedPath}`,
);
}
if (parentNode != null) {
parentNode.delete(path.basename(canonicalPath));
if (parentNode.size === 0 && parentNode !== this.#rootNode) {
// NB: This isn't the most efficient algorithm - in the case of
// removing the last file in a deep hierarchy it's O(depth^2), but
// that's not expected to be a case common enough to justify
// implementation complexity, or slowing down more common uses of
// _lookupByNormalPath.
this.remove(path.dirname(canonicalPath));
}
}
return isDirectory(node) ? null : node;
}
/**
* The core traversal algorithm of TreeFS - takes a normal path and traverses
* through a tree of maps keyed on path segments, returning the node,
* canonical path, and other metadata if successful, or the first missing
* segment otherwise.
*
* When a symlink is encountered, we set a new target of the symlink's
* normalised target path plus the remainder of the original target path. In
* this way, the eventual target path in a successful lookup has all symlinks
* resolved, and gives us the real path "for free". Similarly if a traversal
* fails, we automatically have the real path of the first non-existent node.
*
* Note that this code is extremely hot during resolution, being the most
* expensive part of a file existence check. Benchmark any modifications!
*/
_lookupByNormalPath(
requestedNormalPath: string,
opts: {
collectAncestors?: Array<{
ancestorOfRootIdx: ?number,
node: DirectoryNode,
normalPath: string,
segmentName: string,
}>,
// Mutable Set into which absolute real paths of traversed symlinks will
// be added. Omit for performance if not needed.
collectLinkPaths?: ?Set<string>,
// Like lstat vs stat, whether to follow a symlink at the basename of
// the given path, or return the details of the symlink itself.
followLeaf?: boolean,
// Whether to (recursively) create missing directory nodes during
// traversal, useful when adding files. Will throw if an expected
// directory is already present as a file.
makeDirectories?: boolean,
startPathIdx?: number,
startNode?: DirectoryNode,
start?: {
ancestorOfRootIdx: ?number,
node: DirectoryNode,
pathIdx: number,
},
} = {followLeaf: true, makeDirectories: false},
):
| {
ancestorOfRootIdx: ?number,
canonicalPath: string,
exists: true,
node: MixedNode,
parentNode: DirectoryNode,
}
| {
ancestorOfRootIdx: ?number,
canonicalPath: string,
exists: true,
node: DirectoryNode,
parentNode: null,
}
| {
canonicalMissingPath: string,
missingSegmentName: string,
exists: false,
} {
// We'll update the target if we hit a symlink.
let targetNormalPath = requestedNormalPath;
// Lazy-initialised set of seen target paths, to detect symlink cycles.
let seen: ?Set<string>;
// Pointer to the first character of the current path segment in
// targetNormalPath.
let fromIdx = opts.start?.pathIdx ?? 0;
// The parent of the current segment.
let parentNode = opts.start?.node ?? this.#rootNode;
// If a returned node is (an ancestor of) the root, this is the number of
// levels below the root, i.e. '' is 0, '..' is 1, '../..' is 2, otherwise
// null.
let ancestorOfRootIdx: ?number = opts.start?.ancestorOfRootIdx ?? 0;
const collectAncestors = opts.collectAncestors;
// Used only when collecting ancestors, to avoid double-counting nodes and
// paths when traversing a symlink takes us back to rootNode and out again.
// This tracks the first character of the first segment not already
// collected.
let unseenPathFromIdx = 0;
while (targetNormalPath.length > fromIdx) {
const nextSepIdx = targetNormalPath.indexOf(path.sep, fromIdx);
const isLastSegment = nextSepIdx === -1;
const segmentName = isLastSegment
? targetNormalPath.slice(fromIdx)
: targetNormalPath.slice(fromIdx, nextSepIdx);
const isUnseen = fromIdx >= unseenPathFromIdx;
fromIdx = !isLastSegment ? nextSepIdx + 1 : targetNormalPath.length;
if (segmentName === '.') {
continue;
}
let segmentNode = parentNode.get(segmentName);
// In normal paths all indirections are at the prefix, so we are at the
// nth ancestor of the root iff the path so far is n '..' segments.
if (segmentName === '..' && ancestorOfRootIdx != null) {
ancestorOfRootIdx++;
} else if (segmentNode != null) {
ancestorOfRootIdx = null;
}
if (segmentNode == null) {
if (opts.makeDirectories !== true && segmentName !== '..') {
return {
canonicalMissingPath: isLastSegment
? targetNormalPath
: targetNormalPath.slice(0, fromIdx - 1),
exists: false,
missingSegmentName: segmentName,
};
}
segmentNode = new Map();
if (opts.makeDirectories === true) {
parentNode.set(segmentName, segmentNode);
}
}
// We are done if...
if (
// ...at a directory node and the only subsequent character is `/`, or
(nextSepIdx === targetNormalPath.length - 1 &&
isDirectory(segmentNode)) ||
// there are no subsequent `/`, and this node is anything but a symlink
// we're required to resolve due to followLeaf.
(isLastSegment &&
(isDirectory(segmentNode) ||
isRegularFile(segmentNode) ||
opts.followLeaf === false))
) {
return {
ancestorOfRootIdx,
canonicalPath: isLastSegment
? targetNormalPath
: targetNormalPath.slice(0, -1), // remove trailing `/`
exists: true,
node: segmentNode,
parentNode,
};
}
// If the next node is a directory, go into it
if (isDirectory(segmentNode)) {
parentNode = segmentNode;
if (collectAncestors && isUnseen) {
const currentPath = isLastSegment
? targetNormalPath
: targetNormalPath.slice(0, fromIdx - 1);
collectAncestors.push({
ancestorOfRootIdx,
node: segmentNode,
normalPath: currentPath,
segmentName,
});
}
} else {
const currentPath = isLastSegment
? targetNormalPath
: targetNormalPath.slice(0, fromIdx - 1);
if (isRegularFile(segmentNode)) {
// Regular file in a directory path
return {
canonicalMissingPath: currentPath,
exists: false,
missingSegmentName: segmentName,
};
}
// Symlink in a directory path
const normalSymlinkTarget = this._resolveSymlinkTargetToNormalPath(
segmentNode,
currentPath,
);
if (opts.collectLinkPaths) {
opts.collectLinkPaths.add(
this.#pathUtils.normalToAbsolute(currentPath),
);
}
const remainingTargetPath = isLastSegment
? ''
: targetNormalPath.slice(fromIdx);
// Append any subsequent path segments to the symlink target, and reset
// with our new target.
const joinedResult = this.#pathUtils.joinNormalToRelative(
normalSymlinkTarget.normalPath,
remainingTargetPath,
);
targetNormalPath = joinedResult.normalPath;
// Two special cases (covered by unit tests):
//
// If the symlink target is the root, the root should be a counted as
// an ancestor. We'd otherwise miss counting it because we normally
// push new ancestors only when entering a directory.
//
// If the symlink target is an ancestor of the root *and* joining it
// with the remaining path results in collapsing segments, e.g:
// '../..' + 'parentofroot/root/foo.js' = 'foo.js', then we must add
// parentofroot and root as ancestors.
if (
collectAncestors &&
!isLastSegment &&
// No-op optimisation to bail out the common case of nothing to do.
(normalSymlinkTarget.ancestorOfRootIdx === 0 ||
joinedResult.collapsedSegments > 0)
) {
let node: MixedNode = this.#rootNode;
let collapsedPath = '';
const reverseAncestors = [];
for (
let i = 0;
i <= joinedResult.collapsedSegments &&
/* for Flow, always true: */ isDirectory(node);
i++
) {
if (
// Add the root only if the target is the root or we have
// collapsed segments.
i > 0 ||
normalSymlinkTarget.ancestorOfRootIdx === 0 ||
joinedResult.collapsedSegments > 0
) {
reverseAncestors.push({
ancestorOfRootIdx: i,
node,
normalPath: collapsedPath,
segmentName: this.#pathUtils.getBasenameOfNthAncestor(i),
});
}
node = node.get('..') ?? new Map();
collapsedPath =
collapsedPath === '' ? '..' : collapsedPath + path.sep + '..';
}
/* $FlowFixMe[incompatible-type] Natural Inference rollout. See
* https://fburl.com/gdoc/y8dn025u */
collectAncestors.push(...reverseAncestors.reverse());
}
// For the purpose of collecting ancestors: Ignore the traversal to
// the symlink target, and start collecting ancestors only
// from the target itself (ie, the basename of the normal target path)
// onwards.
unseenPathFromIdx = normalSymlinkTarget.startOfBasenameIdx;
if (seen == null) {
// Optimisation: set this lazily only when we've encountered a symlink
seen = new Set([requestedNormalPath]);
}
if (seen.has(targetNormalPath)) {
// TODO: Warn `Symlink cycle detected: ${[...seen, node].join(' -> ')}`
return {
canonicalMissingPath: targetNormalPath,
exists: false,
missingSegmentName: segmentName,
};
}
seen.add(targetNormalPath);
fromIdx = 0;
parentNode = this.#rootNode;
ancestorOfRootIdx = 0;
}
}
invariant(parentNode === this.#rootNode, 'Unexpectedly escaped traversal');
return {
ancestorOfRootIdx: 0,
canonicalPath: targetNormalPath,
exists: true,
node: this.#rootNode,
parentNode: null,
};
}
/**
* Given a start path (which need not exist), a subpath and type, and
* optionally a 'breakOnSegment', performs the following:
*
* X = mixedStartPath
* do
* if basename(X) === opts.breakOnSegment
* return null
* if X + subpath exists and has type opts.subpathType
* return {
* absolutePath: realpath(X + subpath)
* containerRelativePath: relative(mixedStartPath, X)
* }
* X = dirname(X)
* while X !== dirname(X)
*
* If opts.invalidatedBy is given, collects all absolute, real paths that if
* added or removed may invalidate this result.
*
* Useful for finding the closest package scope (subpath: package.json,
* type f, breakOnSegment: node_modules) or closest potential package root
* (subpath: node_modules/pkg, type: d) in Node.js resolution.
*/
hierarchicalLookup(
mixedStartPath: string,
subpath: string,
opts: {
breakOnSegment: ?string,
invalidatedBy: ?Set<string>,
subpathType: 'f' | 'd',
},
): ?{
absolutePath: string,
containerRelativePath: string,
} {
const ancestorsOfInput: Array<{
ancestorOfRootIdx: ?number,
node: DirectoryNode,
normalPath: string,
segmentName: string,
}> = [];
const normalPath = this._normalizePath(mixedStartPath);
const invalidatedBy = opts.invalidatedBy;
const closestLookup = this._lookupByNormalPath(normalPath, {
collectAncestors: ancestorsOfInput,
collectLinkPaths: invalidatedBy,
});
if (closestLookup.exists && isDirectory(closestLookup.node)) {
const maybeAbsolutePathMatch = this.#checkCandidateHasSubpath(
closestLookup.canonicalPath,
subpath,
opts.subpathType,
invalidatedBy,
null,
);
if (maybeAbsolutePathMatch != null) {
return {
absolutePath: maybeAbsolutePathMatch,
containerRelativePath: '',
};
}
} else {
if (
invalidatedBy &&
(!closestLookup.exists || !isDirectory(closestLookup.node))
) {
invalidatedBy.add(
this.#pathUtils.normalToAbsolute(
closestLookup.exists
? closestLookup.canonicalPath
: closestLookup.canonicalMissingPath,
),
);
}
if (
opts.breakOnSegment != null &&
!closestLookup.exists &&
closestLookup.missingSegmentName === opts.breakOnSegment
) {
return null;
}
}
// Let the "common root" be the nearest common ancestor of this.rootDir
// and the input path. We'll look for a match in two stages:
// 1. Every collected ancestor of the input path, from nearest to furthest,
// that is a descendent of the common root
// 2. The common root, and its ancestors.
let commonRoot = this.#rootNode;
let commonRootDepth = 0;
// Collected ancestors do not include the lookup result itself, so go one
// further if the input path is itself a root ancestor.
if (closestLookup.exists && closestLookup.ancestorOfRootIdx != null) {
commonRootDepth = closestLookup.ancestorOfRootIdx;
invariant(
isDirectory(closestLookup.node),
'ancestors of the root must be directories',
);
commonRoot = closestLookup.node;
} else {
// Establish the common root by counting the '..' segments at the start
// of the collected ancestors.
for (const ancestor of ancestorsOfInput) {
if (ancestor.ancestorOfRootIdx == null) {
break;
}
commonRootDepth = ancestor.ancestorOfRootIdx;
commonRoot = ancestor.node;
}
}
// Phase 1: Consider descendenants of the common root, from deepest to
// shallowest.
for (
let candidateIdx = ancestorsOfInput.length - 1;
candidateIdx >= commonRootDepth;
--candidateIdx
) {
const candidate = ancestorsOfInput[candidateIdx];
if (candidate.segmentName === opts.breakOnSegment) {
return null;
}
const maybeAbsolutePathMatch = this.#checkCandidateHasSubpath(
candidate.normalPath,
subpath,
opts.subpathType,
invalidatedBy,
{
ancestorOfRootIdx: candidate.ancestorOfRootIdx,
node: candidate.node,
pathIdx:
candidate.normalPath.length > 0
? candidate.normalPath.length + 1
: 0,
},
);
if (maybeAbsolutePathMatch != null) {
// Determine the input path relative to the current candidate. Note
// that the candidate path will always be canonical (real), whereas the
// input may contain symlinks, so the candidate is not necessarily a
// prefix of the input. Use the fact that each remaining candidate
// corresponds to a leading segment of the input normal path, and
// discard the first candidateIdx + 1 segments of the input path.
//
// The next 5 lines are equivalent to (but faster than)
// normalPath.split('/').slice(candidateIdx + 1).join('/').
let prefixLength = commonRootDepth * 3; // Leading '../'
for (let i = commonRootDepth; i <= candidateIdx; i++) {
prefixLength = normalPath.indexOf(path.sep, prefixLength + 1);
}
const containerRelativePath = normalPath.slice(prefixLength + 1);
return {
absolutePath: maybeAbsolutePathMatch,
containerRelativePath,
};
}
}
// Phase 2: Consider the common root and its ancestors
// This will be '', '..', '../..', etc.
let candidateNormalPath =
commonRootDepth > 0 ? normalPath.slice(0, 3 * commonRootDepth - 1) : '';
const remainingNormalPath = normalPath.slice(commonRootDepth * 3);
let nextNode: ?MixedNode = commonRoot;
let depthBelowCommonRoot = 0;
while (isDirectory(nextNode)) {
const maybeAbsolutePathMatch = this.#checkCandidateHasSubpath(
candidateNormalPath,
subpath,
opts.subpathType,
invalidatedBy,
null,
);
if (maybeAbsolutePathMatch != null) {
const rootDirParts = this.#pathUtils.getParts();
const relativeParts =
depthBelowCommonRoot > 0
? rootDirParts.slice(
-(depthBelowCommonRoot + commonRootDepth),
commonRootDepth > 0 ? -commonRootDepth : undefined,
)
: [];
if (remainingNormalPath !== '') {
relativeParts.push(remainingNormalPath);
}
return {
absolutePath: maybeAbsolutePathMatch,
containerRelativePath: relativeParts.join(path.sep),
};
}
depthBelowCommonRoot++;
candidateNormalPath =
candidateNormalPath === ''
? '..'
: candidateNormalPath + path.sep + '..';
nextNode = nextNode.get('..');
}
return null;
}
#checkCandidateHasSubpath(
normalCandidatePath: string,
subpath: string,
subpathType: 'f' | 'd',
invalidatedBy: ?Set<string>,
start: ?{
ancestorOfRootIdx: ?number,
node: DirectoryNode,
pathIdx: number,
},
): ?string {
const lookupResult = this._lookupByNormalPath(
this.#pathUtils.joinNormalToRelative(normalCandidatePath, subpath)
.normalPath,
{
collectLinkPaths: invalidatedBy,
},
);
if (
lookupResult.exists &&
// Should be a Map iff subpathType is directory
isDirectory(lookupResult.node) === (subpathType === 'd')
) {
return this.#pathUtils.normalToAbsolute(lookupResult.canonicalPath);
} else if (invalidatedBy) {
invalidatedBy.add(
this.#pathUtils.normalToAbsolute(
lookupResult.exists
? lookupResult.canonicalPath
: lookupResult.canonicalMissingPath,
),
);
}
return null;
}
*metadataIterator(
opts: $ReadOnly<{
includeSymlinks: boolean,
includeNodeModules: boolean,
}>,
): Iterable<{
baseName: string,
canonicalPath: string,
metadata: FileMetadata,
}> {
yield* this._metadataIterator(this.#rootNode, opts);
}
*_metadataIterator(
rootNode: DirectoryNode,
opts: $ReadOnly<{includeSymlinks: boolean, includeNodeModules: boolean}>,
prefix: string = '',
): Iterable<{
baseName: string,
canonicalPath: string,
metadata: FileMetadata,
}> {
for (const [name, node] of rootNode) {
if (
!opts.includeNodeModules &&
isDirectory(node) &&
name === 'node_modules'
) {
continue;
}
const prefixedName = prefix === '' ? name : prefix + path.sep + name;
if (isDirectory(node)) {
yield* this._metadataIterator(node, opts, prefixedName);
} else if (isRegularFile(node) || opts.includeSymlinks) {
yield {canonicalPath: prefixedName, metadata: node, baseName: name};
}
}
}
_normalizePath(relativeOrAbsolutePath: Path): string {
return path.isAbsolute(relativeOrAbsolutePath)
? this.#pathUtils.absoluteToNormal(relativeOrAbsolutePath)
: this.#pathUtils.relativeToNormal(relativeOrAbsolutePath);
}
*#directoryNodeIterator(
node: DirectoryNode,
parent: ?DirectoryNode,
ancestorOfRootIdx: ?number,
): Iterator<[string, MixedNode]> {
if (ancestorOfRootIdx != null && ancestorOfRootIdx > 0 && parent) {
yield ([
this.#pathUtils.getBasenameOfNthAncestor(ancestorOfRootIdx - 1),
parent,
]: [string, MixedNode]);
}
yield* node.entries();
}
/**
* Enumerate paths under a given node, including symlinks and through
* symlinks (if `follow` is enabled).
*/
*_pathIterator(
iterationRootNode: DirectoryNode,
iterationRootParentNode: ?DirectoryNode,
ancestorOfRootIdx: ?number,
opts: $ReadOnly<{
alwaysYieldPosix: boolean,
canonicalPathOfRoot: string,
follow: boolean,
recursive: boolean,
subtreeOnly: boolean,
}>,
pathPrefix: string = '',
followedLinks: $ReadOnlySet<FileMetadata> = new Set(),
): Iterable<Path> {
const pathSep = opts.alwaysYieldPosix ? '/' : path.sep;
const prefixWithSep = pathPrefix === '' ? pathPrefix : pathPrefix + pathSep;
for (const [name, node] of this.#directoryNodeIterator(
iterationRootNode,
iterationRootParentNode,
ancestorOfRootIdx,
)) {
if (opts.subtreeOnly && name === '..') {
continue;
}
const nodePath = prefixWithSep + name;
if (!isDirectory(node)) {
if (isRegularFile(node)) {
// regular file
yield nodePath;
} else {
// symlink
const nodePathWithSystemSeparators =
pathSep === path.sep
? nodePath
: nodePath.replaceAll(pathSep, path.sep);
// Although both paths are normal, the node path may begin '..' so we
// can't simply concatenate.
const normalPathOfSymlink = path.join(
opts.canonicalPathOfRoot,
nodePathWithSystemSeparators,
);
// We can't resolve the symlink directly here because we only have
// its normal path, and we need a canonical path for resolution
// (imagine our normal path contains a symlink 'bar' -> '.', and we
// are at /foo/bar/baz where baz -> '..' - that should resolve to
// /foo, not /foo/bar). We *can* use _lookupByNormalPath to walk to
// the canonical symlink, and then to its target.
const resolved = this._lookupByNormalPath(normalPathOfSymlink, {
followLeaf: true,
});
if (!resolved.exists) {
// Symlink goes nowhere, nothing to report.
continue;
}
const target = resolved.node;
if (!isDirectory(target)) {
// Symlink points to a file, just yield the path of the symlink.
yield nodePath;
} else if (
opts.recursive &&
opts.follow &&
!followedLinks.has(node)
) {
// Symlink points to a directory - iterate over its contents using
// the path where we found the symlink as a prefix.
yield* this._pathIterator(
target,
resolved.parentNode,
resolved.ancestorOfRootIdx,
opts,
nodePath,
new Set([...followedLinks, node]),
);
}
}
} else if (opts.recursive) {
yield* this._pathIterator(
node,
iterationRootParentNode,
ancestorOfRootIdx != null && ancestorOfRootIdx > 0
? ancestorOfRootIdx - 1
: null,
opts,
nodePath,
followedLinks,
);
}
}
}
_resolveSymlinkTargetToNormalPath(
symlinkNode: FileMetadata,
canonicalPathOfSymlink: Path,
): NormalizedSymlinkTarget {
const cachedResult = this.#cachedNormalSymlinkTargets.get(symlinkNode);
if (cachedResult != null) {
return cachedResult;
}
const literalSymlinkTarget = symlinkNode[H.SYMLINK];
invariant(
typeof literalSymlinkTarget === 'string',
'Expected symlink target to be populated.',
);
const absoluteSymlinkTarget = path.resolve(
this.#rootDir,
canonicalPathOfSymlink,
'..', // Symlink target is relative to its containing directory.
literalSymlinkTarget, // May be absolute, in which case the above are ignored
);
const normalSymlinkTarget = path.relative(
this.#rootDir,
absoluteSymlinkTarget,
);
const result = {
ancestorOfRootIdx:
this.#pathUtils.getAncestorOfRootIdx(normalSymlinkTarget),
normalPath: normalSymlinkTarget,
startOfBasenameIdx: normalSymlinkTarget.lastIndexOf(path.sep) + 1,
};
this.#cachedNormalSymlinkTargets.set(symlinkNode, result);
return result;
}
_getFileData(
filePath: Path,
opts: {followLeaf: boolean} = {followLeaf: true},
): ?FileMetadata {
const normalPath = this._normalizePath(filePath);
const result = this._lookupByNormalPath(normalPath, {
followLeaf: opts.followLeaf,
});
if (!result.exists || isDirectory(result.node)) {
return null;
}
return result.node;
}
_cloneTree(root: DirectoryNode): DirectoryNode {
const clone: DirectoryNode = new Map();
for (const [name, node] of root) {
if (isDirectory(node)) {
clone.set(name, this._cloneTree(node));
} else {
clone.set(name, [...node]);
}
}
return clone;
}
}
Выполнить команду
Для локальной разработки. Не используйте в интернете!