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