forked from react/metro
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathRootPathUtils.js
More file actions
328 lines (304 loc) Β· 12 KB
/
Copy pathRootPathUtils.js
File metadata and controls
328 lines (304 loc) Β· 12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
/**
* 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.
*
* @flow strict
* @format
*/
import invariant from 'invariant';
import * as path from 'path';
/**
* This module provides path utility functions - similar to `node:path` -
* optimised for Metro's use case (many paths, few roots) under assumptions
* typically safe to make within Metro - namely:
*
* - All input path separators must be system-native.
* - Double/redundant separators like '/foo//bar' are not supported.
* - All characters except separators are assumed to be valid in path segments.
*
* - A "well-formed" path is any path following the rules above.
* - A "normal" path is a root-relative well-formed path with no redundant
* indirections. Normal paths have no leading './`, and the normal path of
* the root is the empty string.
*
* Output and input paths are at least well-formed (normal where indicated by
* naming).
*
* Trailing path separators are preserved, except for fs roots in
* normalToAbsolute (fs roots always have a trailing separator), and the
* project root in absoluteToNormal and relativeToNormal (the project root is
* always the empty string, and is always a directory, so a trailing separator
* is redundant).
*
* As of Node 20, absoluteToNormal is ~8x faster than `path.relative` and
* `normalToAbsolute` is ~20x faster than `path.resolve`, benchmarked on the
* real inputs from building FB's product graph. Some well-formed inputs
* (e.g., /project/./foo/../bar), are handled but not optimised, and we fall
* back to `node:path` equivalents in those cases.
*/
const UP_FRAGMENT_SEP = '..' + path.sep;
const SEP_UP_FRAGMENT = path.sep + '..';
const UP_FRAGMENT_SEP_LENGTH = UP_FRAGMENT_SEP.length;
const CURRENT_FRAGMENT = '.' + path.sep;
const IS_WIN32 = path.sep === '\\';
const ROOT_BASE_IDX = IS_WIN32 ? 0 : 1;
export class RootPathUtils {
#rootDir: string;
#rootDirnames: ReadonlyArray<string>;
#rootParts: ReadonlyArray<string>;
#rootDepth: number;
constructor(rootDir: string) {
this.#rootDir = rootDir;
const rootDirnames = [];
for (
let next = rootDir, previous = null;
/* $FlowFixMe[invalid-compare] Error discovered during Constant Condition
* roll out. See https://fburl.com/workplace/5whu3i34. */
previous !== next;
previous = next, next = path.dirname(next)
) {
rootDirnames.push(next);
}
this.#rootDirnames = rootDirnames;
this.#rootParts = rootDir.split(path.sep);
this.#rootDepth = rootDirnames.length - 1;
// If rootDir is a filesystem root (C:\ or /), it will end in a separator and
// give a spurious empty entry at the end of rootParts.
if (this.#rootDepth === 0) {
this.#rootParts.pop();
}
}
getBasenameOfNthAncestor(n: number): string {
return this.#rootParts[this.#rootParts.length - 1 - n];
}
getParts(): ReadonlyArray<string> {
return this.#rootParts;
}
// absolutePath may be any well-formed absolute path.
absoluteToNormal(absolutePath: string): string {
let endOfMatchingPrefix = 0;
let lastMatchingPartIdx = 0;
for (
let nextPart = this.#rootParts[0], nextLength = nextPart.length;
nextPart != null &&
// Check that absolutePath is equal to nextPart + '/' or ends with
// nextPart, starting from endOfMatchingPrefix.
absolutePath.startsWith(nextPart, endOfMatchingPrefix) &&
(absolutePath.length === endOfMatchingPrefix + nextLength ||
absolutePath[endOfMatchingPrefix + nextLength] === path.sep);
) {
// Move our matching pointer forward and load the next part.
endOfMatchingPrefix += nextLength + 1;
nextPart = this.#rootParts[++lastMatchingPartIdx];
nextLength = nextPart?.length;
}
// If our root is /project/root and we're given /project/bar/foo.js, we
// have matched up to '/project', and will need to return a path
// beginning '../' (one prepended indirection, to go up from 'root').
//
// If we're given /project/../project2/otherroot, we have one level of
// indirection up to prepend in the same way as above. There's another
// explicit indirection already present in the input - we'll account for
// that in tryCollapseIndirectionsInSuffix.
const upIndirectionsToPrepend =
this.#rootParts.length - lastMatchingPartIdx;
return (
this.#tryCollapseIndirectionsInSuffix(
absolutePath,
endOfMatchingPrefix,
upIndirectionsToPrepend,
)?.collapsedPath ?? this.#slowAbsoluteToNormal(absolutePath)
);
}
#slowAbsoluteToNormal(absolutePath: string): string {
const endsWithSep = absolutePath.endsWith(path.sep);
const result = path.relative(this.#rootDir, absolutePath);
return endsWithSep && !result.endsWith(path.sep)
? result + path.sep
: result;
}
// `normalPath` is assumed to be normal (root-relative, no redundant
// indirection), per the definition above.
normalToAbsolute(normalPath: string): string {
let left = this.#rootDir;
let i = 0;
let pos = 0;
while (
normalPath.startsWith(UP_FRAGMENT_SEP, pos) ||
(normalPath.endsWith('..') && normalPath.length === 2 + pos)
) {
left = this.#rootDirnames[i === this.#rootDepth ? this.#rootDepth : ++i];
pos += UP_FRAGMENT_SEP_LENGTH;
}
const right = pos === 0 ? normalPath : normalPath.slice(pos);
if (right.length === 0) {
return left;
} else if (IS_WIN32 && pos > this.#rootDepth * UP_FRAGMENT_SEP_LENGTH) {
// On a real file system, navigating to `..` at the top level (posix `/`
// or Windows drive) is a no-op, but we can't respect that on Windows
// because Metro uses e.g. `..\..\D:\foo` to represent cross-drive
// relative paths.
return right;
}
// left may already end in a path separator only if it is a filesystem root,
// '/' or 'X:\'.
if (i === this.#rootDepth) {
return left + right;
}
return left + path.sep + right;
}
relativeToNormal(relativePath: string): string {
return (
this.#tryCollapseIndirectionsInSuffix(relativePath, 0, 0)
?.collapsedPath ??
path.relative(this.#rootDir, path.join(this.#rootDir, relativePath))
);
}
// If a path is a direct ancestor of the project root (or the root itself),
// return a number with the degrees of separation, e.g. root=0, parent=1,..
// or null otherwise.
getAncestorOfRootIdx(normalPath: string): ?number {
if (normalPath === '') {
return 0;
}
if (normalPath === '..') {
return 1;
}
// Otherwise a *normal* path is only a root ancestor if it is a sequence of
// '../' segments followed by '..', so the length tells us the number of
// up fragments.
if (normalPath.endsWith(SEP_UP_FRAGMENT)) {
return (normalPath.length + 1) / 3;
}
return null;
}
// Takes a normal and relative path, and joins them efficiently into a normal
// path, including collapsing trailing '..' in the first part with leading
// project root segments in the relative part.
joinNormalToRelative(
normalPath: string,
relativePath: string,
): {normalPath: string, collapsedSegments: number} {
if (normalPath === '') {
return {collapsedSegments: 0, normalPath: relativePath};
}
if (relativePath === '') {
return {collapsedSegments: 0, normalPath};
}
const left = normalPath.endsWith(path.sep)
? normalPath
: normalPath + path.sep;
const rawPath = left + relativePath;
if (normalPath === '..' || normalPath.endsWith(SEP_UP_FRAGMENT)) {
const collapsed = this.#tryCollapseIndirectionsInSuffix(rawPath, 0, 0);
invariant(collapsed != null, 'Failed to collapse');
return {
collapsedSegments: collapsed.collapsedSegments,
normalPath: collapsed.collapsedPath,
};
}
return {
collapsedSegments: 0,
normalPath: rawPath,
};
}
relative(from: string, to: string): string {
return path.relative(from, to);
}
// Internal: Tries to collapse sequences like `../root/foo` for root
// `/project/root` down to the normal 'foo'.
#tryCollapseIndirectionsInSuffix(
fullPath: string, // A string ending with the relative path to process
startOfRelativePart: number, // Index of the start of part to process
implicitUpIndirections: number, // 0=root-relative, 1=dirname(root)-relative...
): ?{collapsedPath: string, collapsedSegments: number} {
let totalUpIndirections = implicitUpIndirections;
let collapsedSegments = 0;
// Allow any sequence of indirection fragments at the start of the
// unmatched suffix e.g /project/[../../foo], but bail out to Node's
// path.relative if we find a possible indirection after any later segment,
// or on any "./" that isn't a "../".
for (let pos = startOfRelativePart; ; pos += UP_FRAGMENT_SEP_LENGTH) {
const nextIndirection = fullPath.indexOf(CURRENT_FRAGMENT, pos);
if (nextIndirection === -1) {
// If we have any indirections, they may "collapse" if a subsequent
// segment re-enters a directory we had previously exited, e.g:
// /project/root/../root/foo should collapse to /project/root/foo' and
// return foo, not ../root/foo.
//
// We match each segment following redirections, in turn, against the
// part of the root path they may collapse into, and break on the first
// mismatch.
while (totalUpIndirections > 0) {
const segmentToMaybeCollapse =
this.#rootParts[this.#rootParts.length - totalUpIndirections];
if (
fullPath.startsWith(segmentToMaybeCollapse, pos) &&
// The following character should be either a separator or end of
// string
(fullPath.length === segmentToMaybeCollapse.length + pos ||
fullPath[segmentToMaybeCollapse.length + pos] === path.sep)
) {
pos += segmentToMaybeCollapse.length + 1;
collapsedSegments++;
totalUpIndirections--;
} else {
break;
}
}
// After collapsing we may have no more segments remaining (following
// '..' indirections). Ensure that we don't drop or add a trailing
// separator in this case by taking .slice(pos-1). In any other case,
// we know that fullPath[pos] is a separator.
if (pos >= fullPath.length) {
return {
collapsedPath:
totalUpIndirections > 0
? UP_FRAGMENT_SEP.repeat(totalUpIndirections - 1) +
'..' +
fullPath.slice(pos - 1)
: '',
collapsedSegments,
};
}
const right = pos > 0 ? fullPath.slice(pos) : fullPath;
if (
right === '..' &&
totalUpIndirections >= this.#rootParts.length - 1
) {
// If we have no right side (or an indirection that would take us
// below the root), just ensure we don't include a trailing separtor.
return {
collapsedPath: UP_FRAGMENT_SEP.repeat(totalUpIndirections).slice(
0,
-1,
),
collapsedSegments,
};
}
// Optimisation for the common case, saves a concatenation.
if (totalUpIndirections === 0) {
return {collapsedPath: right, collapsedSegments};
}
return {
collapsedPath: UP_FRAGMENT_SEP.repeat(totalUpIndirections) + right,
collapsedSegments,
};
}
// Cap the number of indirections at the total number of root parts.
// File systems treat '..' at the root as '.'. For Windows, cross-device
// paths need to survive this.
if (totalUpIndirections < this.#rootParts.length - ROOT_BASE_IDX) {
totalUpIndirections++;
}
if (
nextIndirection !== pos + 1 || // Fallback when ./ later in the path, or leading
fullPath[pos] !== '.' // and for anything other than a leading ../
) {
return null;
}
}
}
}