1import { 2 CharacterCodes, 3 compareStringsCaseInsensitive, 4 compareStringsCaseSensitive, 5 compareValues, 6 Comparison, 7 Debug, 8 endsWith, 9 equateStringsCaseInsensitive, 10 equateStringsCaseSensitive, 11 GetCanonicalFileName, 12 getStringComparer, 13 identity, 14 lastOrUndefined, 15 Path, 16 some, 17 startsWith, 18 stringContains, 19} from "./_namespaces/ts"; 20 21/** 22 * Internally, we represent paths as strings with '/' as the directory separator. 23 * When we make system calls (eg: LanguageServiceHost.getDirectory()), 24 * we expect the host to correctly handle paths in our specified format. 25 * 26 * @internal 27 */ 28export const directorySeparator = "/"; 29/** @internal */ 30export const altDirectorySeparator = "\\"; 31const urlSchemeSeparator = "://"; 32const backslashRegExp = /\\/g; 33 34//// Path Tests 35 36/** 37 * Determines whether a charCode corresponds to `/` or `\`. 38 * 39 * @internal 40 */ 41export function isAnyDirectorySeparator(charCode: number): boolean { 42 return charCode === CharacterCodes.slash || charCode === CharacterCodes.backslash; 43} 44 45/** 46 * Determines whether a path starts with a URL scheme (e.g. starts with `http://`, `ftp://`, `file://`, etc.). 47 * 48 * @internal 49 */ 50export function isUrl(path: string) { 51 return getEncodedRootLength(path) < 0; 52} 53 54/** 55 * Determines whether a path is an absolute disk path (e.g. starts with `/`, or a dos path 56 * like `c:`, `c:\` or `c:/`). 57 * 58 * @internal 59 */ 60export function isRootedDiskPath(path: string) { 61 return getEncodedRootLength(path) > 0; 62} 63 64/** 65 * Determines whether a path consists only of a path root. 66 * 67 * @internal 68 */ 69export function isDiskPathRoot(path: string) { 70 const rootLength = getEncodedRootLength(path); 71 return rootLength > 0 && rootLength === path.length; 72} 73 74/** 75 * Determines whether a path starts with an absolute path component (i.e. `/`, `c:/`, `file://`, etc.). 76 * 77 * ```ts 78 * // POSIX 79 * pathIsAbsolute("/path/to/file.ext") === true 80 * // DOS 81 * pathIsAbsolute("c:/path/to/file.ext") === true 82 * // URL 83 * pathIsAbsolute("file:///path/to/file.ext") === true 84 * // Non-absolute 85 * pathIsAbsolute("path/to/file.ext") === false 86 * pathIsAbsolute("./path/to/file.ext") === false 87 * ``` 88 * 89 * @internal 90 */ 91export function pathIsAbsolute(path: string): boolean { 92 return getEncodedRootLength(path) !== 0; 93} 94 95/** 96 * Determines whether a path starts with a relative path component (i.e. `.` or `..`). 97 * 98 * @internal 99 */ 100export function pathIsRelative(path: string): boolean { 101 return /^\.\.?($|[\\/])/.test(path); 102} 103 104/** 105 * Determines whether a path is neither relative nor absolute, e.g. "path/to/file". 106 * Also known misleadingly as "non-relative". 107 * 108 * @internal 109 */ 110export function pathIsBareSpecifier(path: string): boolean { 111 return !pathIsAbsolute(path) && !pathIsRelative(path); 112} 113 114/** @internal */ 115export function hasExtension(fileName: string): boolean { 116 return stringContains(getBaseFileName(fileName), "."); 117} 118 119/** @internal */ 120export function fileExtensionIs(path: string, extension: string): boolean { 121 return path.length > extension.length && endsWith(path, extension); 122} 123 124/** @internal */ 125export function fileExtensionIsOneOf(path: string, extensions: readonly string[]): boolean { 126 for (const extension of extensions) { 127 if (fileExtensionIs(path, extension)) { 128 return true; 129 } 130 } 131 132 return false; 133} 134 135/** 136 * Determines whether a path has a trailing separator (`/` or `\\`). 137 * 138 * @internal 139 */ 140export function hasTrailingDirectorySeparator(path: string) { 141 return path.length > 0 && isAnyDirectorySeparator(path.charCodeAt(path.length - 1)); 142} 143 144//// Path Parsing 145 146function isVolumeCharacter(charCode: number) { 147 return (charCode >= CharacterCodes.a && charCode <= CharacterCodes.z) || 148 (charCode >= CharacterCodes.A && charCode <= CharacterCodes.Z); 149} 150 151function getFileUrlVolumeSeparatorEnd(url: string, start: number) { 152 const ch0 = url.charCodeAt(start); 153 if (ch0 === CharacterCodes.colon) return start + 1; 154 if (ch0 === CharacterCodes.percent && url.charCodeAt(start + 1) === CharacterCodes._3) { 155 const ch2 = url.charCodeAt(start + 2); 156 if (ch2 === CharacterCodes.a || ch2 === CharacterCodes.A) return start + 3; 157 } 158 return -1; 159} 160 161/** 162 * Returns length of the root part of a path or URL (i.e. length of "/", "x:/", "//server/share/, file:///user/files"). 163 * If the root is part of a URL, the twos-complement of the root length is returned. 164 */ 165function getEncodedRootLength(path: string): number { 166 if (!path) return 0; 167 const ch0 = path.charCodeAt(0); 168 169 // POSIX or UNC 170 if (ch0 === CharacterCodes.slash || ch0 === CharacterCodes.backslash) { 171 if (path.charCodeAt(1) !== ch0) return 1; // POSIX: "/" (or non-normalized "\") 172 173 const p1 = path.indexOf(ch0 === CharacterCodes.slash ? directorySeparator : altDirectorySeparator, 2); 174 if (p1 < 0) return path.length; // UNC: "//server" or "\\server" 175 176 return p1 + 1; // UNC: "//server/" or "\\server\" 177 } 178 179 // DOS 180 if (isVolumeCharacter(ch0) && path.charCodeAt(1) === CharacterCodes.colon) { 181 const ch2 = path.charCodeAt(2); 182 if (ch2 === CharacterCodes.slash || ch2 === CharacterCodes.backslash) return 3; // DOS: "c:/" or "c:\" 183 if (path.length === 2) return 2; // DOS: "c:" (but not "c:d") 184 } 185 186 // URL 187 const schemeEnd = path.indexOf(urlSchemeSeparator); 188 if (schemeEnd !== -1) { 189 const authorityStart = schemeEnd + urlSchemeSeparator.length; 190 const authorityEnd = path.indexOf(directorySeparator, authorityStart); 191 if (authorityEnd !== -1) { // URL: "file:///", "file://server/", "file://server/path" 192 // For local "file" URLs, include the leading DOS volume (if present). 193 // Per https://www.ietf.org/rfc/rfc1738.txt, a host of "" or "localhost" is a 194 // special case interpreted as "the machine from which the URL is being interpreted". 195 const scheme = path.slice(0, schemeEnd); 196 const authority = path.slice(authorityStart, authorityEnd); 197 if (scheme === "file" && (authority === "" || authority === "localhost") && 198 isVolumeCharacter(path.charCodeAt(authorityEnd + 1))) { 199 const volumeSeparatorEnd = getFileUrlVolumeSeparatorEnd(path, authorityEnd + 2); 200 if (volumeSeparatorEnd !== -1) { 201 if (path.charCodeAt(volumeSeparatorEnd) === CharacterCodes.slash) { 202 // URL: "file:///c:/", "file://localhost/c:/", "file:///c%3a/", "file://localhost/c%3a/" 203 return ~(volumeSeparatorEnd + 1); 204 } 205 if (volumeSeparatorEnd === path.length) { 206 // URL: "file:///c:", "file://localhost/c:", "file:///c$3a", "file://localhost/c%3a" 207 // but not "file:///c:d" or "file:///c%3ad" 208 return ~volumeSeparatorEnd; 209 } 210 } 211 } 212 return ~(authorityEnd + 1); // URL: "file://server/", "http://server/" 213 } 214 return ~path.length; // URL: "file://server", "http://server" 215 } 216 217 // relative 218 return 0; 219} 220 221/** 222 * Returns length of the root part of a path or URL (i.e. length of "/", "x:/", "//server/share/, file:///user/files"). 223 * 224 * For example: 225 * ```ts 226 * getRootLength("a") === 0 // "" 227 * getRootLength("/") === 1 // "/" 228 * getRootLength("c:") === 2 // "c:" 229 * getRootLength("c:d") === 0 // "" 230 * getRootLength("c:/") === 3 // "c:/" 231 * getRootLength("c:\\") === 3 // "c:\\" 232 * getRootLength("//server") === 7 // "//server" 233 * getRootLength("//server/share") === 8 // "//server/" 234 * getRootLength("\\\\server") === 7 // "\\\\server" 235 * getRootLength("\\\\server\\share") === 8 // "\\\\server\\" 236 * getRootLength("file:///path") === 8 // "file:///" 237 * getRootLength("file:///c:") === 10 // "file:///c:" 238 * getRootLength("file:///c:d") === 8 // "file:///" 239 * getRootLength("file:///c:/path") === 11 // "file:///c:/" 240 * getRootLength("file://server") === 13 // "file://server" 241 * getRootLength("file://server/path") === 14 // "file://server/" 242 * getRootLength("http://server") === 13 // "http://server" 243 * getRootLength("http://server/path") === 14 // "http://server/" 244 * ``` 245 * 246 * @internal 247 */ 248export function getRootLength(path: string) { 249 const rootLength = getEncodedRootLength(path); 250 return rootLength < 0 ? ~rootLength : rootLength; 251} 252 253/** 254 * Returns the path except for its basename. Semantics align with NodeJS's `path.dirname` 255 * except that we support URLs as well. 256 * 257 * ```ts 258 * // POSIX 259 * getDirectoryPath("/path/to/file.ext") === "/path/to" 260 * getDirectoryPath("/path/to/") === "/path" 261 * getDirectoryPath("/") === "/" 262 * // DOS 263 * getDirectoryPath("c:/path/to/file.ext") === "c:/path/to" 264 * getDirectoryPath("c:/path/to/") === "c:/path" 265 * getDirectoryPath("c:/") === "c:/" 266 * getDirectoryPath("c:") === "c:" 267 * // URL 268 * getDirectoryPath("http://typescriptlang.org/path/to/file.ext") === "http://typescriptlang.org/path/to" 269 * getDirectoryPath("http://typescriptlang.org/path/to") === "http://typescriptlang.org/path" 270 * getDirectoryPath("http://typescriptlang.org/") === "http://typescriptlang.org/" 271 * getDirectoryPath("http://typescriptlang.org") === "http://typescriptlang.org" 272 * ``` 273 * 274 * @internal 275 */ 276export function getDirectoryPath(path: Path): Path; 277/** 278 * Returns the path except for its basename. Semantics align with NodeJS's `path.dirname` 279 * except that we support URLs as well. 280 * 281 * ```ts 282 * // POSIX 283 * getDirectoryPath("/path/to/file.ext") === "/path/to" 284 * getDirectoryPath("/path/to/") === "/path" 285 * getDirectoryPath("/") === "/" 286 * // DOS 287 * getDirectoryPath("c:/path/to/file.ext") === "c:/path/to" 288 * getDirectoryPath("c:/path/to/") === "c:/path" 289 * getDirectoryPath("c:/") === "c:/" 290 * getDirectoryPath("c:") === "c:" 291 * // URL 292 * getDirectoryPath("http://typescriptlang.org/path/to/file.ext") === "http://typescriptlang.org/path/to" 293 * getDirectoryPath("http://typescriptlang.org/path/to") === "http://typescriptlang.org/path" 294 * getDirectoryPath("http://typescriptlang.org/") === "http://typescriptlang.org/" 295 * getDirectoryPath("http://typescriptlang.org") === "http://typescriptlang.org" 296 * getDirectoryPath("file://server/path/to/file.ext") === "file://server/path/to" 297 * getDirectoryPath("file://server/path/to") === "file://server/path" 298 * getDirectoryPath("file://server/") === "file://server/" 299 * getDirectoryPath("file://server") === "file://server" 300 * getDirectoryPath("file:///path/to/file.ext") === "file:///path/to" 301 * getDirectoryPath("file:///path/to") === "file:///path" 302 * getDirectoryPath("file:///") === "file:///" 303 * getDirectoryPath("file://") === "file://" 304 * ``` 305 * 306 * @internal 307 */ 308export function getDirectoryPath(path: string): string; 309/** @internal */ 310export function getDirectoryPath(path: string): string { 311 path = normalizeSlashes(path); 312 313 // If the path provided is itself the root, then return it. 314 const rootLength = getRootLength(path); 315 if (rootLength === path.length) return path; 316 317 // return the leading portion of the path up to the last (non-terminal) directory separator 318 // but not including any trailing directory separator. 319 path = removeTrailingDirectorySeparator(path); 320 return path.slice(0, Math.max(rootLength, path.lastIndexOf(directorySeparator))); 321} 322 323/** 324 * Returns the path except for its containing directory name. 325 * Semantics align with NodeJS's `path.basename` except that we support URL's as well. 326 * 327 * ```ts 328 * // POSIX 329 * getBaseFileName("/path/to/file.ext") === "file.ext" 330 * getBaseFileName("/path/to/") === "to" 331 * getBaseFileName("/") === "" 332 * // DOS 333 * getBaseFileName("c:/path/to/file.ext") === "file.ext" 334 * getBaseFileName("c:/path/to/") === "to" 335 * getBaseFileName("c:/") === "" 336 * getBaseFileName("c:") === "" 337 * // URL 338 * getBaseFileName("http://typescriptlang.org/path/to/file.ext") === "file.ext" 339 * getBaseFileName("http://typescriptlang.org/path/to/") === "to" 340 * getBaseFileName("http://typescriptlang.org/") === "" 341 * getBaseFileName("http://typescriptlang.org") === "" 342 * getBaseFileName("file://server/path/to/file.ext") === "file.ext" 343 * getBaseFileName("file://server/path/to/") === "to" 344 * getBaseFileName("file://server/") === "" 345 * getBaseFileName("file://server") === "" 346 * getBaseFileName("file:///path/to/file.ext") === "file.ext" 347 * getBaseFileName("file:///path/to/") === "to" 348 * getBaseFileName("file:///") === "" 349 * getBaseFileName("file://") === "" 350 * ``` 351 * 352 * @internal 353 */ 354export function getBaseFileName(path: string): string; 355/** 356 * Gets the portion of a path following the last (non-terminal) separator (`/`). 357 * Semantics align with NodeJS's `path.basename` except that we support URL's as well. 358 * If the base name has any one of the provided extensions, it is removed. 359 * 360 * ```ts 361 * getBaseFileName("/path/to/file.ext", ".ext", true) === "file" 362 * getBaseFileName("/path/to/file.js", ".ext", true) === "file.js" 363 * getBaseFileName("/path/to/file.js", [".ext", ".js"], true) === "file" 364 * getBaseFileName("/path/to/file.ext", ".EXT", false) === "file.ext" 365 * ``` 366 * 367 * @internal 368 */ 369export function getBaseFileName(path: string, extensions: string | readonly string[], ignoreCase: boolean): string; 370/** @internal */ 371export function getBaseFileName(path: string, extensions?: string | readonly string[], ignoreCase?: boolean) { 372 path = normalizeSlashes(path); 373 374 // if the path provided is itself the root, then it has not file name. 375 const rootLength = getRootLength(path); 376 if (rootLength === path.length) return ""; 377 378 // return the trailing portion of the path starting after the last (non-terminal) directory 379 // separator but not including any trailing directory separator. 380 path = removeTrailingDirectorySeparator(path); 381 const name = path.slice(Math.max(getRootLength(path), path.lastIndexOf(directorySeparator) + 1)); 382 const extension = extensions !== undefined && ignoreCase !== undefined ? getAnyExtensionFromPath(name, extensions, ignoreCase) : undefined; 383 return extension ? name.slice(0, name.length - extension.length) : name; 384} 385 386function tryGetExtensionFromPath(path: string, extension: string, stringEqualityComparer: (a: string, b: string) => boolean) { 387 if (!startsWith(extension, ".")) extension = "." + extension; 388 if (path.length >= extension.length && path.charCodeAt(path.length - extension.length) === CharacterCodes.dot) { 389 const pathExtension = path.slice(path.length - extension.length); 390 if (stringEqualityComparer(pathExtension, extension)) { 391 return pathExtension; 392 } 393 } 394} 395 396function getAnyExtensionFromPathWorker(path: string, extensions: string | readonly string[], stringEqualityComparer: (a: string, b: string) => boolean) { 397 if (typeof extensions === "string") { 398 return tryGetExtensionFromPath(path, extensions, stringEqualityComparer) || ""; 399 } 400 for (const extension of extensions) { 401 const result = tryGetExtensionFromPath(path, extension, stringEqualityComparer); 402 if (result) return result; 403 } 404 return ""; 405} 406 407/** 408 * Gets the file extension for a path. 409 * 410 * ```ts 411 * getAnyExtensionFromPath("/path/to/file.ext") === ".ext" 412 * getAnyExtensionFromPath("/path/to/file.ext/") === ".ext" 413 * getAnyExtensionFromPath("/path/to/file") === "" 414 * getAnyExtensionFromPath("/path/to.ext/file") === "" 415 * ``` 416 * 417 * @internal 418 */ 419export function getAnyExtensionFromPath(path: string): string; 420/** 421 * Gets the file extension for a path, provided it is one of the provided extensions. 422 * 423 * ```ts 424 * getAnyExtensionFromPath("/path/to/file.ext", ".ext", true) === ".ext" 425 * getAnyExtensionFromPath("/path/to/file.js", ".ext", true) === "" 426 * getAnyExtensionFromPath("/path/to/file.js", [".ext", ".js"], true) === ".js" 427 * getAnyExtensionFromPath("/path/to/file.ext", ".EXT", false) === "" 428 * 429 * @internal 430 */ 431export function getAnyExtensionFromPath(path: string, extensions: string | readonly string[], ignoreCase: boolean): string; 432/** @internal */ 433export function getAnyExtensionFromPath(path: string, extensions?: string | readonly string[], ignoreCase?: boolean): string { 434 // Retrieves any string from the final "." onwards from a base file name. 435 // Unlike extensionFromPath, which throws an exception on unrecognized extensions. 436 if (extensions) { 437 return getAnyExtensionFromPathWorker(removeTrailingDirectorySeparator(path), extensions, ignoreCase ? equateStringsCaseInsensitive : equateStringsCaseSensitive); 438 } 439 const baseFileName = getBaseFileName(path); 440 const extensionIndex = baseFileName.lastIndexOf("."); 441 if (extensionIndex >= 0) { 442 return baseFileName.substring(extensionIndex); 443 } 444 return ""; 445} 446 447function pathComponents(path: string, rootLength: number) { 448 const root = path.substring(0, rootLength); 449 const rest = path.substring(rootLength).split(directorySeparator); 450 if (rest.length && !lastOrUndefined(rest)) rest.pop(); 451 return [root, ...rest]; 452} 453 454/** 455 * Parse a path into an array containing a root component (at index 0) and zero or more path 456 * components (at indices > 0). The result is not normalized. 457 * If the path is relative, the root component is `""`. 458 * If the path is absolute, the root component includes the first path separator (`/`). 459 * 460 * ```ts 461 * // POSIX 462 * getPathComponents("/path/to/file.ext") === ["/", "path", "to", "file.ext"] 463 * getPathComponents("/path/to/") === ["/", "path", "to"] 464 * getPathComponents("/") === ["/"] 465 * // DOS 466 * getPathComponents("c:/path/to/file.ext") === ["c:/", "path", "to", "file.ext"] 467 * getPathComponents("c:/path/to/") === ["c:/", "path", "to"] 468 * getPathComponents("c:/") === ["c:/"] 469 * getPathComponents("c:") === ["c:"] 470 * // URL 471 * getPathComponents("http://typescriptlang.org/path/to/file.ext") === ["http://typescriptlang.org/", "path", "to", "file.ext"] 472 * getPathComponents("http://typescriptlang.org/path/to/") === ["http://typescriptlang.org/", "path", "to"] 473 * getPathComponents("http://typescriptlang.org/") === ["http://typescriptlang.org/"] 474 * getPathComponents("http://typescriptlang.org") === ["http://typescriptlang.org"] 475 * getPathComponents("file://server/path/to/file.ext") === ["file://server/", "path", "to", "file.ext"] 476 * getPathComponents("file://server/path/to/") === ["file://server/", "path", "to"] 477 * getPathComponents("file://server/") === ["file://server/"] 478 * getPathComponents("file://server") === ["file://server"] 479 * getPathComponents("file:///path/to/file.ext") === ["file:///", "path", "to", "file.ext"] 480 * getPathComponents("file:///path/to/") === ["file:///", "path", "to"] 481 * getPathComponents("file:///") === ["file:///"] 482 * getPathComponents("file://") === ["file://"] 483 * 484 * @internal 485 */ 486export function getPathComponents(path: string, currentDirectory = "") { 487 path = combinePaths(currentDirectory, path); 488 return pathComponents(path, getRootLength(path)); 489} 490 491//// Path Formatting 492 493/** 494 * Formats a parsed path consisting of a root component (at index 0) and zero or more path 495 * segments (at indices > 0). 496 * 497 * ```ts 498 * getPathFromPathComponents(["/", "path", "to", "file.ext"]) === "/path/to/file.ext" 499 * ``` 500 * 501 * @internal 502 */ 503export function getPathFromPathComponents(pathComponents: readonly string[]) { 504 if (pathComponents.length === 0) return ""; 505 506 const root = pathComponents[0] && ensureTrailingDirectorySeparator(pathComponents[0]); 507 return root + pathComponents.slice(1).join(directorySeparator); 508} 509 510//// Path Normalization 511 512/** 513 * Normalize path separators, converting `\` into `/`. 514 * 515 * @internal 516 */ 517export function normalizeSlashes(path: string): string { 518 return path.indexOf("\\") !== -1 519 ? path.replace(backslashRegExp, directorySeparator) 520 : path; 521} 522 523/** 524 * Reduce an array of path components to a more simplified path by navigating any 525 * `"."` or `".."` entries in the path. 526 * 527 * @internal 528 */ 529export function reducePathComponents(components: readonly string[]) { 530 if (!some(components)) return []; 531 const reduced = [components[0]]; 532 for (let i = 1; i < components.length; i++) { 533 const component = components[i]; 534 if (!component) continue; 535 if (component === ".") continue; 536 if (component === "..") { 537 if (reduced.length > 1) { 538 if (reduced[reduced.length - 1] !== "..") { 539 reduced.pop(); 540 continue; 541 } 542 } 543 else if (reduced[0]) continue; 544 } 545 reduced.push(component); 546 } 547 return reduced; 548} 549 550/** 551 * Combines paths. If a path is absolute, it replaces any previous path. Relative paths are not simplified. 552 * 553 * ```ts 554 * // Non-rooted 555 * combinePaths("path", "to", "file.ext") === "path/to/file.ext" 556 * combinePaths("path", "dir", "..", "to", "file.ext") === "path/dir/../to/file.ext" 557 * // POSIX 558 * combinePaths("/path", "to", "file.ext") === "/path/to/file.ext" 559 * combinePaths("/path", "/to", "file.ext") === "/to/file.ext" 560 * // DOS 561 * combinePaths("c:/path", "to", "file.ext") === "c:/path/to/file.ext" 562 * combinePaths("c:/path", "c:/to", "file.ext") === "c:/to/file.ext" 563 * // URL 564 * combinePaths("file:///path", "to", "file.ext") === "file:///path/to/file.ext" 565 * combinePaths("file:///path", "file:///to", "file.ext") === "file:///to/file.ext" 566 * ``` 567 * 568 * @internal 569 */ 570export function combinePaths(path: string, ...paths: (string | undefined)[]): string { 571 if (path) path = normalizeSlashes(path); 572 for (let relativePath of paths) { 573 if (!relativePath) continue; 574 relativePath = normalizeSlashes(relativePath); 575 if (!path || getRootLength(relativePath) !== 0) { 576 path = relativePath; 577 } 578 else { 579 path = ensureTrailingDirectorySeparator(path) + relativePath; 580 } 581 } 582 return path; 583} 584 585/** 586 * Combines and resolves paths. If a path is absolute, it replaces any previous path. Any 587 * `.` and `..` path components are resolved. Trailing directory separators are preserved. 588 * 589 * ```ts 590 * resolvePath("/path", "to", "file.ext") === "path/to/file.ext" 591 * resolvePath("/path", "to", "file.ext/") === "path/to/file.ext/" 592 * resolvePath("/path", "dir", "..", "to", "file.ext") === "path/to/file.ext" 593 * ``` 594 * 595 * @internal 596 */ 597export function resolvePath(path: string, ...paths: (string | undefined)[]): string { 598 return normalizePath(some(paths) ? combinePaths(path, ...paths) : normalizeSlashes(path)); 599} 600 601/** 602 * Parse a path into an array containing a root component (at index 0) and zero or more path 603 * components (at indices > 0). The result is normalized. 604 * If the path is relative, the root component is `""`. 605 * If the path is absolute, the root component includes the first path separator (`/`). 606 * 607 * ```ts 608 * getNormalizedPathComponents("to/dir/../file.ext", "/path/") === ["/", "path", "to", "file.ext"] 609 * ``` 610 * 611 * @internal 612 */ 613export function getNormalizedPathComponents(path: string, currentDirectory: string | undefined) { 614 return reducePathComponents(getPathComponents(path, currentDirectory)); 615} 616 617/** @internal */ 618export function getNormalizedAbsolutePath(fileName: string, currentDirectory: string | undefined) { 619 return getPathFromPathComponents(getNormalizedPathComponents(fileName, currentDirectory)); 620} 621 622/** @internal */ 623export function normalizePath(path: string): string { 624 path = normalizeSlashes(path); 625 // Most paths don't require normalization 626 if (!relativePathSegmentRegExp.test(path)) { 627 return path; 628 } 629 // Some paths only require cleanup of `/./` or leading `./` 630 const simplified = path.replace(/\/\.\//g, "/").replace(/^\.\//, ""); 631 if (simplified !== path) { 632 path = simplified; 633 if (!relativePathSegmentRegExp.test(path)) { 634 return path; 635 } 636 } 637 // Other paths require full normalization 638 const normalized = getPathFromPathComponents(reducePathComponents(getPathComponents(path))); 639 return normalized && hasTrailingDirectorySeparator(path) ? ensureTrailingDirectorySeparator(normalized) : normalized; 640} 641 642function getPathWithoutRoot(pathComponents: readonly string[]) { 643 if (pathComponents.length === 0) return ""; 644 return pathComponents.slice(1).join(directorySeparator); 645} 646 647/** @internal */ 648export function getNormalizedAbsolutePathWithoutRoot(fileName: string, currentDirectory: string | undefined) { 649 return getPathWithoutRoot(getNormalizedPathComponents(fileName, currentDirectory)); 650} 651 652/** @internal */ 653export function toPath(fileName: string, basePath: string | undefined, getCanonicalFileName: (path: string) => string): Path { 654 const nonCanonicalizedPath = isRootedDiskPath(fileName) 655 ? normalizePath(fileName) 656 : getNormalizedAbsolutePath(fileName, basePath); 657 return getCanonicalFileName(nonCanonicalizedPath) as Path; 658} 659 660//// Path Mutation 661 662/** 663 * Removes a trailing directory separator from a path, if it does not already have one. 664 * 665 * ```ts 666 * removeTrailingDirectorySeparator("/path/to/file.ext") === "/path/to/file.ext" 667 * removeTrailingDirectorySeparator("/path/to/file.ext/") === "/path/to/file.ext" 668 * ``` 669 * 670 * @internal 671 */ 672export function removeTrailingDirectorySeparator(path: Path): Path; 673/** @internal */ 674export function removeTrailingDirectorySeparator(path: string): string; 675/** @internal */ 676export function removeTrailingDirectorySeparator(path: string) { 677 if (hasTrailingDirectorySeparator(path)) { 678 return path.substr(0, path.length - 1); 679 } 680 681 return path; 682} 683 684/** 685 * Adds a trailing directory separator to a path, if it does not already have one. 686 * 687 * ```ts 688 * ensureTrailingDirectorySeparator("/path/to/file.ext") === "/path/to/file.ext/" 689 * ensureTrailingDirectorySeparator("/path/to/file.ext/") === "/path/to/file.ext/" 690 * ``` 691 * 692 * @internal 693 */ 694export function ensureTrailingDirectorySeparator(path: Path): Path; 695/** @internal */ 696export function ensureTrailingDirectorySeparator(path: string): string; 697/** @internal */ 698export function ensureTrailingDirectorySeparator(path: string) { 699 if (!hasTrailingDirectorySeparator(path)) { 700 return path + directorySeparator; 701 } 702 703 return path; 704} 705 706/** 707 * Ensures a path is either absolute (prefixed with `/` or `c:`) or dot-relative (prefixed 708 * with `./` or `../`) so as not to be confused with an unprefixed module name. 709 * 710 * ```ts 711 * ensurePathIsNonModuleName("/path/to/file.ext") === "/path/to/file.ext" 712 * ensurePathIsNonModuleName("./path/to/file.ext") === "./path/to/file.ext" 713 * ensurePathIsNonModuleName("../path/to/file.ext") === "../path/to/file.ext" 714 * ensurePathIsNonModuleName("path/to/file.ext") === "./path/to/file.ext" 715 * ``` 716 * 717 * @internal 718 */ 719export function ensurePathIsNonModuleName(path: string): string { 720 return !pathIsAbsolute(path) && !pathIsRelative(path) ? "./" + path : path; 721} 722 723/** 724 * Changes the extension of a path to the provided extension. 725 * 726 * ```ts 727 * changeAnyExtension("/path/to/file.ext", ".js") === "/path/to/file.js" 728 * ``` 729 * 730 * @internal 731 */ 732export function changeAnyExtension(path: string, ext: string): string; 733/** 734 * Changes the extension of a path to the provided extension if it has one of the provided extensions. 735 * 736 * ```ts 737 * changeAnyExtension("/path/to/file.ext", ".js", ".ext") === "/path/to/file.js" 738 * changeAnyExtension("/path/to/file.ext", ".js", ".ts") === "/path/to/file.ext" 739 * changeAnyExtension("/path/to/file.ext", ".js", [".ext", ".ts"]) === "/path/to/file.js" 740 * ``` 741 * 742 * @internal 743 */ 744export function changeAnyExtension(path: string, ext: string, extensions: string | readonly string[], ignoreCase: boolean): string; 745/** @internal */ 746export function changeAnyExtension(path: string, ext: string, extensions?: string | readonly string[], ignoreCase?: boolean) { 747 const pathext = extensions !== undefined && ignoreCase !== undefined ? getAnyExtensionFromPath(path, extensions, ignoreCase) : getAnyExtensionFromPath(path); 748 return pathext ? path.slice(0, path.length - pathext.length) + (startsWith(ext, ".") ? ext : "." + ext) : path; 749} 750 751//// Path Comparisons 752 753// check path for these segments: '', '.'. '..' 754const relativePathSegmentRegExp = /(?:\/\/)|(?:^|\/)\.\.?(?:$|\/)/; 755 756function comparePathsWorker(a: string, b: string, componentComparer: (a: string, b: string) => Comparison) { 757 if (a === b) return Comparison.EqualTo; 758 if (a === undefined) return Comparison.LessThan; 759 if (b === undefined) return Comparison.GreaterThan; 760 761 // NOTE: Performance optimization - shortcut if the root segments differ as there would be no 762 // need to perform path reduction. 763 const aRoot = a.substring(0, getRootLength(a)); 764 const bRoot = b.substring(0, getRootLength(b)); 765 const result = compareStringsCaseInsensitive(aRoot, bRoot); 766 if (result !== Comparison.EqualTo) { 767 return result; 768 } 769 770 // NOTE: Performance optimization - shortcut if there are no relative path segments in 771 // the non-root portion of the path 772 const aRest = a.substring(aRoot.length); 773 const bRest = b.substring(bRoot.length); 774 if (!relativePathSegmentRegExp.test(aRest) && !relativePathSegmentRegExp.test(bRest)) { 775 return componentComparer(aRest, bRest); 776 } 777 778 // The path contains a relative path segment. Normalize the paths and perform a slower component 779 // by component comparison. 780 const aComponents = reducePathComponents(getPathComponents(a)); 781 const bComponents = reducePathComponents(getPathComponents(b)); 782 const sharedLength = Math.min(aComponents.length, bComponents.length); 783 for (let i = 1; i < sharedLength; i++) { 784 const result = componentComparer(aComponents[i], bComponents[i]); 785 if (result !== Comparison.EqualTo) { 786 return result; 787 } 788 } 789 return compareValues(aComponents.length, bComponents.length); 790} 791 792/** 793 * Performs a case-sensitive comparison of two paths. Path roots are always compared case-insensitively. 794 * 795 * @internal 796 */ 797export function comparePathsCaseSensitive(a: string, b: string) { 798 return comparePathsWorker(a, b, compareStringsCaseSensitive); 799} 800 801/** 802 * Performs a case-insensitive comparison of two paths. 803 * 804 * @internal 805 */ 806export function comparePathsCaseInsensitive(a: string, b: string) { 807 return comparePathsWorker(a, b, compareStringsCaseInsensitive); 808} 809 810/** 811 * Compare two paths using the provided case sensitivity. 812 * 813 * @internal 814 */ 815export function comparePaths(a: string, b: string, ignoreCase?: boolean): Comparison; 816/** @internal */ 817export function comparePaths(a: string, b: string, currentDirectory: string, ignoreCase?: boolean): Comparison; 818/** @internal */ 819export function comparePaths(a: string, b: string, currentDirectory?: string | boolean, ignoreCase?: boolean) { 820 if (typeof currentDirectory === "string") { 821 a = combinePaths(currentDirectory, a); 822 b = combinePaths(currentDirectory, b); 823 } 824 else if (typeof currentDirectory === "boolean") { 825 ignoreCase = currentDirectory; 826 } 827 return comparePathsWorker(a, b, getStringComparer(ignoreCase)); 828} 829 830/** 831 * Determines whether a `parent` path contains a `child` path using the provide case sensitivity. 832 * 833 * @internal 834 */ 835export function containsPath(parent: string, child: string, ignoreCase?: boolean): boolean; 836/** @internal */ 837export function containsPath(parent: string, child: string, currentDirectory: string, ignoreCase?: boolean): boolean; 838/** @internal */ 839export function containsPath(parent: string, child: string, currentDirectory?: string | boolean, ignoreCase?: boolean) { 840 if (typeof currentDirectory === "string") { 841 parent = combinePaths(currentDirectory, parent); 842 child = combinePaths(currentDirectory, child); 843 } 844 else if (typeof currentDirectory === "boolean") { 845 ignoreCase = currentDirectory; 846 } 847 if (parent === undefined || child === undefined) return false; 848 if (parent === child) return true; 849 const parentComponents = reducePathComponents(getPathComponents(parent)); 850 const childComponents = reducePathComponents(getPathComponents(child)); 851 if (childComponents.length < parentComponents.length) { 852 return false; 853 } 854 855 const componentEqualityComparer = ignoreCase ? equateStringsCaseInsensitive : equateStringsCaseSensitive; 856 for (let i = 0; i < parentComponents.length; i++) { 857 const equalityComparer = i === 0 ? equateStringsCaseInsensitive : componentEqualityComparer; 858 if (!equalityComparer(parentComponents[i], childComponents[i])) { 859 return false; 860 } 861 } 862 863 return true; 864} 865 866/** 867 * Determines whether `fileName` starts with the specified `directoryName` using the provided path canonicalization callback. 868 * Comparison is case-sensitive between the canonical paths. 869 * 870 * Use `containsPath` if file names are not already reduced and absolute. 871 * 872 * @internal 873 */ 874export function startsWithDirectory(fileName: string, directoryName: string, getCanonicalFileName: GetCanonicalFileName): boolean { 875 const canonicalFileName = getCanonicalFileName(fileName); 876 const canonicalDirectoryName = getCanonicalFileName(directoryName); 877 return startsWith(canonicalFileName, canonicalDirectoryName + "/") || startsWith(canonicalFileName, canonicalDirectoryName + "\\"); 878} 879 880//// Relative Paths 881 882/** @internal */ 883export function getPathComponentsRelativeTo(from: string, to: string, stringEqualityComparer: (a: string, b: string) => boolean, getCanonicalFileName: GetCanonicalFileName) { 884 const fromComponents = reducePathComponents(getPathComponents(from)); 885 const toComponents = reducePathComponents(getPathComponents(to)); 886 887 let start: number; 888 for (start = 0; start < fromComponents.length && start < toComponents.length; start++) { 889 const fromComponent = getCanonicalFileName(fromComponents[start]); 890 const toComponent = getCanonicalFileName(toComponents[start]); 891 const comparer = start === 0 ? equateStringsCaseInsensitive : stringEqualityComparer; 892 if (!comparer(fromComponent, toComponent)) break; 893 } 894 895 if (start === 0) { 896 return toComponents; 897 } 898 899 const components = toComponents.slice(start); 900 const relative: string[] = []; 901 for (; start < fromComponents.length; start++) { 902 relative.push(".."); 903 } 904 return ["", ...relative, ...components]; 905} 906 907/** 908 * Gets a relative path that can be used to traverse between `from` and `to`. 909 * 910 * @internal 911 */ 912export function getRelativePathFromDirectory(from: string, to: string, ignoreCase: boolean): string; 913/** 914 * Gets a relative path that can be used to traverse between `from` and `to`. 915 * 916 * @internal 917 */ 918export function getRelativePathFromDirectory(fromDirectory: string, to: string, getCanonicalFileName: GetCanonicalFileName): string; // eslint-disable-line @typescript-eslint/unified-signatures 919/** @internal */ 920export function getRelativePathFromDirectory(fromDirectory: string, to: string, getCanonicalFileNameOrIgnoreCase: GetCanonicalFileName | boolean) { 921 Debug.assert((getRootLength(fromDirectory) > 0) === (getRootLength(to) > 0), "Paths must either both be absolute or both be relative"); 922 const getCanonicalFileName = typeof getCanonicalFileNameOrIgnoreCase === "function" ? getCanonicalFileNameOrIgnoreCase : identity; 923 const ignoreCase = typeof getCanonicalFileNameOrIgnoreCase === "boolean" ? getCanonicalFileNameOrIgnoreCase : false; 924 const pathComponents = getPathComponentsRelativeTo(fromDirectory, to, ignoreCase ? equateStringsCaseInsensitive : equateStringsCaseSensitive, getCanonicalFileName); 925 return getPathFromPathComponents(pathComponents); 926} 927 928/** @internal */ 929export function convertToRelativePath(absoluteOrRelativePath: string, basePath: string, getCanonicalFileName: (path: string) => string): string { 930 return !isRootedDiskPath(absoluteOrRelativePath) 931 ? absoluteOrRelativePath 932 : getRelativePathToDirectoryOrUrl(basePath, absoluteOrRelativePath, basePath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false); 933} 934 935/** @internal */ 936export function getRelativePathFromFile(from: string, to: string, getCanonicalFileName: GetCanonicalFileName) { 937 return ensurePathIsNonModuleName(getRelativePathFromDirectory(getDirectoryPath(from), to, getCanonicalFileName)); 938} 939 940/** @internal */ 941export function getRelativePathToDirectoryOrUrl(directoryPathOrUrl: string, relativeOrAbsolutePath: string, currentDirectory: string, getCanonicalFileName: GetCanonicalFileName, isAbsolutePathAnUrl: boolean) { 942 const pathComponents = getPathComponentsRelativeTo( 943 resolvePath(currentDirectory, directoryPathOrUrl), 944 resolvePath(currentDirectory, relativeOrAbsolutePath), 945 equateStringsCaseSensitive, 946 getCanonicalFileName 947 ); 948 949 const firstComponent = pathComponents[0]; 950 if (isAbsolutePathAnUrl && isRootedDiskPath(firstComponent)) { 951 const prefix = firstComponent.charAt(0) === directorySeparator ? "file://" : "file:///"; 952 pathComponents[0] = prefix + firstComponent; 953 } 954 955 return getPathFromPathComponents(pathComponents); 956} 957 958//// Path Traversal 959 960/** 961 * Calls `callback` on `directory` and every ancestor directory it has, returning the first defined result. 962 * 963 * @internal 964 */ 965export function forEachAncestorDirectory<T>(directory: Path, callback: (directory: Path) => T | undefined): T | undefined; 966/** @internal */ 967export function forEachAncestorDirectory<T>(directory: string, callback: (directory: string) => T | undefined): T | undefined; 968/** @internal */ 969export function forEachAncestorDirectory<T>(directory: Path, callback: (directory: Path) => T | undefined): T | undefined { 970 while (true) { 971 const result = callback(directory); 972 if (result !== undefined) { 973 return result; 974 } 975 976 const parentPath = getDirectoryPath(directory); 977 if (parentPath === directory) { 978 return undefined; 979 } 980 981 directory = parentPath; 982 } 983} 984 985/** @internal */ 986export function isNodeModulesDirectory(dirPath: Path) { 987 return endsWith(dirPath, "/node_modules"); 988} 989