summaryrefslogtreecommitdiff
path: root/ext/node/polyfills/path
diff options
context:
space:
mode:
Diffstat (limited to 'ext/node/polyfills/path')
-rw-r--r--ext/node/polyfills/path/_constants.ts48
-rw-r--r--ext/node/polyfills/path/_interface.ts29
-rw-r--r--ext/node/polyfills/path/_util.ts131
-rw-r--r--ext/node/polyfills/path/common.ts39
-rw-r--r--ext/node/polyfills/path/glob.ts420
-rw-r--r--ext/node/polyfills/path/mod.ts37
-rw-r--r--ext/node/polyfills/path/posix.ts536
-rw-r--r--ext/node/polyfills/path/separator.ts6
-rw-r--r--ext/node/polyfills/path/win32.ts1025
9 files changed, 2271 insertions, 0 deletions
diff --git a/ext/node/polyfills/path/_constants.ts b/ext/node/polyfills/path/_constants.ts
new file mode 100644
index 000000000..814af2e18
--- /dev/null
+++ b/ext/node/polyfills/path/_constants.ts
@@ -0,0 +1,48 @@
+// Copyright the Browserify authors. MIT License.
+// Ported from https://github.com/browserify/path-browserify/
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+
+// Alphabet chars.
+export const CHAR_UPPERCASE_A = 65; /* A */
+export const CHAR_LOWERCASE_A = 97; /* a */
+export const CHAR_UPPERCASE_Z = 90; /* Z */
+export const CHAR_LOWERCASE_Z = 122; /* z */
+
+// Non-alphabetic chars.
+export const CHAR_DOT = 46; /* . */
+export const CHAR_FORWARD_SLASH = 47; /* / */
+export const CHAR_BACKWARD_SLASH = 92; /* \ */
+export const CHAR_VERTICAL_LINE = 124; /* | */
+export const CHAR_COLON = 58; /* : */
+export const CHAR_QUESTION_MARK = 63; /* ? */
+export const CHAR_UNDERSCORE = 95; /* _ */
+export const CHAR_LINE_FEED = 10; /* \n */
+export const CHAR_CARRIAGE_RETURN = 13; /* \r */
+export const CHAR_TAB = 9; /* \t */
+export const CHAR_FORM_FEED = 12; /* \f */
+export const CHAR_EXCLAMATION_MARK = 33; /* ! */
+export const CHAR_HASH = 35; /* # */
+export const CHAR_SPACE = 32; /* */
+export const CHAR_NO_BREAK_SPACE = 160; /* \u00A0 */
+export const CHAR_ZERO_WIDTH_NOBREAK_SPACE = 65279; /* \uFEFF */
+export const CHAR_LEFT_SQUARE_BRACKET = 91; /* [ */
+export const CHAR_RIGHT_SQUARE_BRACKET = 93; /* ] */
+export const CHAR_LEFT_ANGLE_BRACKET = 60; /* < */
+export const CHAR_RIGHT_ANGLE_BRACKET = 62; /* > */
+export const CHAR_LEFT_CURLY_BRACKET = 123; /* { */
+export const CHAR_RIGHT_CURLY_BRACKET = 125; /* } */
+export const CHAR_HYPHEN_MINUS = 45; /* - */
+export const CHAR_PLUS = 43; /* + */
+export const CHAR_DOUBLE_QUOTE = 34; /* " */
+export const CHAR_SINGLE_QUOTE = 39; /* ' */
+export const CHAR_PERCENT = 37; /* % */
+export const CHAR_SEMICOLON = 59; /* ; */
+export const CHAR_CIRCUMFLEX_ACCENT = 94; /* ^ */
+export const CHAR_GRAVE_ACCENT = 96; /* ` */
+export const CHAR_AT = 64; /* @ */
+export const CHAR_AMPERSAND = 38; /* & */
+export const CHAR_EQUAL = 61; /* = */
+
+// Digits
+export const CHAR_0 = 48; /* 0 */
+export const CHAR_9 = 57; /* 9 */
diff --git a/ext/node/polyfills/path/_interface.ts b/ext/node/polyfills/path/_interface.ts
new file mode 100644
index 000000000..46d6d00f8
--- /dev/null
+++ b/ext/node/polyfills/path/_interface.ts
@@ -0,0 +1,29 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+
+/**
+ * A parsed path object generated by path.parse() or consumed by path.format().
+ */
+export interface ParsedPath {
+ /**
+ * The root of the path such as '/' or 'c:\'
+ */
+ root: string;
+ /**
+ * The full directory path such as '/home/user/dir' or 'c:\path\dir'
+ */
+ dir: string;
+ /**
+ * The file name including extension (if any) such as 'index.html'
+ */
+ base: string;
+ /**
+ * The file extension (if any) such as '.html'
+ */
+ ext: string;
+ /**
+ * The file name without extension (if any) such as 'index'
+ */
+ name: string;
+}
+
+export type FormatInputPathObject = Partial<ParsedPath>;
diff --git a/ext/node/polyfills/path/_util.ts b/ext/node/polyfills/path/_util.ts
new file mode 100644
index 000000000..ccc12abc9
--- /dev/null
+++ b/ext/node/polyfills/path/_util.ts
@@ -0,0 +1,131 @@
+// Copyright the Browserify authors. MIT License.
+// Ported from https://github.com/browserify/path-browserify/
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+
+import type { FormatInputPathObject } from "internal:deno_node/polyfills/path/_interface.ts";
+import {
+ CHAR_BACKWARD_SLASH,
+ CHAR_DOT,
+ CHAR_FORWARD_SLASH,
+ CHAR_LOWERCASE_A,
+ CHAR_LOWERCASE_Z,
+ CHAR_UPPERCASE_A,
+ CHAR_UPPERCASE_Z,
+} from "internal:deno_node/polyfills/path/_constants.ts";
+import { ERR_INVALID_ARG_TYPE } from "internal:deno_node/polyfills/internal/errors.ts";
+
+export function assertPath(path: string) {
+ if (typeof path !== "string") {
+ throw new ERR_INVALID_ARG_TYPE("path", ["string"], path);
+ }
+}
+
+export function isPosixPathSeparator(code: number): boolean {
+ return code === CHAR_FORWARD_SLASH;
+}
+
+export function isPathSeparator(code: number): boolean {
+ return isPosixPathSeparator(code) || code === CHAR_BACKWARD_SLASH;
+}
+
+export function isWindowsDeviceRoot(code: number): boolean {
+ return (
+ (code >= CHAR_LOWERCASE_A && code <= CHAR_LOWERCASE_Z) ||
+ (code >= CHAR_UPPERCASE_A && code <= CHAR_UPPERCASE_Z)
+ );
+}
+
+// Resolves . and .. elements in a path with directory names
+export function normalizeString(
+ path: string,
+ allowAboveRoot: boolean,
+ separator: string,
+ isPathSeparator: (code: number) => boolean,
+): string {
+ let res = "";
+ let lastSegmentLength = 0;
+ let lastSlash = -1;
+ let dots = 0;
+ let code: number | undefined;
+ for (let i = 0, len = path.length; i <= len; ++i) {
+ if (i < len) code = path.charCodeAt(i);
+ else if (isPathSeparator(code!)) break;
+ else code = CHAR_FORWARD_SLASH;
+
+ if (isPathSeparator(code!)) {
+ if (lastSlash === i - 1 || dots === 1) {
+ // NOOP
+ } else if (lastSlash !== i - 1 && dots === 2) {
+ if (
+ res.length < 2 ||
+ lastSegmentLength !== 2 ||
+ res.charCodeAt(res.length - 1) !== CHAR_DOT ||
+ res.charCodeAt(res.length - 2) !== CHAR_DOT
+ ) {
+ if (res.length > 2) {
+ const lastSlashIndex = res.lastIndexOf(separator);
+ if (lastSlashIndex === -1) {
+ res = "";
+ lastSegmentLength = 0;
+ } else {
+ res = res.slice(0, lastSlashIndex);
+ lastSegmentLength = res.length - 1 - res.lastIndexOf(separator);
+ }
+ lastSlash = i;
+ dots = 0;
+ continue;
+ } else if (res.length === 2 || res.length === 1) {
+ res = "";
+ lastSegmentLength = 0;
+ lastSlash = i;
+ dots = 0;
+ continue;
+ }
+ }
+ if (allowAboveRoot) {
+ if (res.length > 0) res += `${separator}..`;
+ else res = "..";
+ lastSegmentLength = 2;
+ }
+ } else {
+ if (res.length > 0) res += separator + path.slice(lastSlash + 1, i);
+ else res = path.slice(lastSlash + 1, i);
+ lastSegmentLength = i - lastSlash - 1;
+ }
+ lastSlash = i;
+ dots = 0;
+ } else if (code === CHAR_DOT && dots !== -1) {
+ ++dots;
+ } else {
+ dots = -1;
+ }
+ }
+ return res;
+}
+
+export function _format(
+ sep: string,
+ pathObject: FormatInputPathObject,
+): string {
+ const dir: string | undefined = pathObject.dir || pathObject.root;
+ const base: string = pathObject.base ||
+ (pathObject.name || "") + (pathObject.ext || "");
+ if (!dir) return base;
+ if (dir === pathObject.root) return dir + base;
+ return dir + sep + base;
+}
+
+const WHITESPACE_ENCODINGS: Record<string, string> = {
+ "\u0009": "%09",
+ "\u000A": "%0A",
+ "\u000B": "%0B",
+ "\u000C": "%0C",
+ "\u000D": "%0D",
+ "\u0020": "%20",
+};
+
+export function encodeWhitespace(string: string): string {
+ return string.replaceAll(/[\s]/g, (c) => {
+ return WHITESPACE_ENCODINGS[c] ?? c;
+ });
+}
diff --git a/ext/node/polyfills/path/common.ts b/ext/node/polyfills/path/common.ts
new file mode 100644
index 000000000..e4efe7cc4
--- /dev/null
+++ b/ext/node/polyfills/path/common.ts
@@ -0,0 +1,39 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+// This module is browser compatible.
+
+import { SEP } from "internal:deno_node/polyfills/path/separator.ts";
+
+/** Determines the common path from a set of paths, using an optional separator,
+ * which defaults to the OS default separator.
+ *
+ * ```ts
+ * const p = common([
+ * "./deno/std/path/mod.ts",
+ * "./deno/std/fs/mod.ts",
+ * ]);
+ * console.log(p); // "./deno/std/"
+ * ```
+ */
+export function common(paths: string[], sep = SEP): string {
+ const [first = "", ...remaining] = paths;
+ if (first === "" || remaining.length === 0) {
+ return first.substring(0, first.lastIndexOf(sep) + 1);
+ }
+ const parts = first.split(sep);
+
+ let endOfPrefix = parts.length;
+ for (const path of remaining) {
+ const compare = path.split(sep);
+ for (let i = 0; i < endOfPrefix; i++) {
+ if (compare[i] !== parts[i]) {
+ endOfPrefix = i;
+ }
+ }
+
+ if (endOfPrefix === 0) {
+ return "";
+ }
+ }
+ const prefix = parts.slice(0, endOfPrefix).join(sep);
+ return prefix.endsWith(sep) ? prefix : `${prefix}${sep}`;
+}
diff --git a/ext/node/polyfills/path/glob.ts b/ext/node/polyfills/path/glob.ts
new file mode 100644
index 000000000..c0da29b9f
--- /dev/null
+++ b/ext/node/polyfills/path/glob.ts
@@ -0,0 +1,420 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+
+import { isWindows, osType } from "internal:deno_node/polyfills/_util/os.ts";
+import {
+ SEP,
+ SEP_PATTERN,
+} from "internal:deno_node/polyfills/path/separator.ts";
+import * as _win32 from "internal:deno_node/polyfills/path/win32.ts";
+import * as _posix from "internal:deno_node/polyfills/path/posix.ts";
+import type { OSType } from "internal:deno_node/polyfills/_util/os.ts";
+
+const path = isWindows ? _win32 : _posix;
+const { join, normalize } = path;
+
+export interface GlobOptions {
+ /** Extended glob syntax.
+ * See https://www.linuxjournal.com/content/bash-extended-globbing.
+ *
+ * @default {true}
+ */
+ extended?: boolean;
+ /** Globstar syntax.
+ * See https://www.linuxjournal.com/content/globstar-new-bash-globbing-option.
+ * If false, `**` is treated like `*`.
+ *
+ * @default {true}
+ */
+ globstar?: boolean;
+ /** Whether globstar should be case-insensitive. */
+ caseInsensitive?: boolean;
+ /** Operating system. Defaults to the native OS. */
+ os?: OSType;
+}
+
+export type GlobToRegExpOptions = GlobOptions;
+
+const regExpEscapeChars = [
+ "!",
+ "$",
+ "(",
+ ")",
+ "*",
+ "+",
+ ".",
+ "=",
+ "?",
+ "[",
+ "\\",
+ "^",
+ "{",
+ "|",
+];
+const rangeEscapeChars = ["-", "\\", "]"];
+
+/** Convert a glob string to a regular expression.
+ *
+ * Tries to match bash glob expansion as closely as possible.
+ *
+ * Basic glob syntax:
+ * - `*` - Matches everything without leaving the path segment.
+ * - `?` - Matches any single character.
+ * - `{foo,bar}` - Matches `foo` or `bar`.
+ * - `[abcd]` - Matches `a`, `b`, `c` or `d`.
+ * - `[a-d]` - Matches `a`, `b`, `c` or `d`.
+ * - `[!abcd]` - Matches any single character besides `a`, `b`, `c` or `d`.
+ * - `[[:<class>:]]` - Matches any character belonging to `<class>`.
+ * - `[[:alnum:]]` - Matches any digit or letter.
+ * - `[[:digit:]abc]` - Matches any digit, `a`, `b` or `c`.
+ * - See https://facelessuser.github.io/wcmatch/glob/#posix-character-classes
+ * for a complete list of supported character classes.
+ * - `\` - Escapes the next character for an `os` other than `"windows"`.
+ * - \` - Escapes the next character for `os` set to `"windows"`.
+ * - `/` - Path separator.
+ * - `\` - Additional path separator only for `os` set to `"windows"`.
+ *
+ * Extended syntax:
+ * - Requires `{ extended: true }`.
+ * - `?(foo|bar)` - Matches 0 or 1 instance of `{foo,bar}`.
+ * - `@(foo|bar)` - Matches 1 instance of `{foo,bar}`. They behave the same.
+ * - `*(foo|bar)` - Matches _n_ instances of `{foo,bar}`.
+ * - `+(foo|bar)` - Matches _n > 0_ instances of `{foo,bar}`.
+ * - `!(foo|bar)` - Matches anything other than `{foo,bar}`.
+ * - See https://www.linuxjournal.com/content/bash-extended-globbing.
+ *
+ * Globstar syntax:
+ * - Requires `{ globstar: true }`.
+ * - `**` - Matches any number of any path segments.
+ * - Must comprise its entire path segment in the provided glob.
+ * - See https://www.linuxjournal.com/content/globstar-new-bash-globbing-option.
+ *
+ * Note the following properties:
+ * - The generated `RegExp` is anchored at both start and end.
+ * - Repeating and trailing separators are tolerated. Trailing separators in the
+ * provided glob have no meaning and are discarded.
+ * - Absolute globs will only match absolute paths, etc.
+ * - Empty globs will match nothing.
+ * - Any special glob syntax must be contained to one path segment. For example,
+ * `?(foo|bar/baz)` is invalid. The separator will take precedence and the
+ * first segment ends with an unclosed group.
+ * - If a path segment ends with unclosed groups or a dangling escape prefix, a
+ * parse error has occurred. Every character for that segment is taken
+ * literally in this event.
+ *
+ * Limitations:
+ * - A negative group like `!(foo|bar)` will wrongly be converted to a negative
+ * look-ahead followed by a wildcard. This means that `!(foo).js` will wrongly
+ * fail to match `foobar.js`, even though `foobar` is not `foo`. Effectively,
+ * `!(foo|bar)` is treated like `!(@(foo|bar)*)`. This will work correctly if
+ * the group occurs not nested at the end of the segment. */
+export function globToRegExp(
+ glob: string,
+ {
+ extended = true,
+ globstar: globstarOption = true,
+ os = osType,
+ caseInsensitive = false,
+ }: GlobToRegExpOptions = {},
+): RegExp {
+ if (glob == "") {
+ return /(?!)/;
+ }
+
+ const sep = os == "windows" ? "(?:\\\\|/)+" : "/+";
+ const sepMaybe = os == "windows" ? "(?:\\\\|/)*" : "/*";
+ const seps = os == "windows" ? ["\\", "/"] : ["/"];
+ const globstar = os == "windows"
+ ? "(?:[^\\\\/]*(?:\\\\|/|$)+)*"
+ : "(?:[^/]*(?:/|$)+)*";
+ const wildcard = os == "windows" ? "[^\\\\/]*" : "[^/]*";
+ const escapePrefix = os == "windows" ? "`" : "\\";
+
+ // Remove trailing separators.
+ let newLength = glob.length;
+ for (; newLength > 1 && seps.includes(glob[newLength - 1]); newLength--);
+ glob = glob.slice(0, newLength);
+
+ let regExpString = "";
+
+ // Terminates correctly. Trust that `j` is incremented every iteration.
+ for (let j = 0; j < glob.length;) {
+ let segment = "";
+ const groupStack: string[] = [];
+ let inRange = false;
+ let inEscape = false;
+ let endsWithSep = false;
+ let i = j;
+
+ // Terminates with `i` at the non-inclusive end of the current segment.
+ for (; i < glob.length && !seps.includes(glob[i]); i++) {
+ if (inEscape) {
+ inEscape = false;
+ const escapeChars = inRange ? rangeEscapeChars : regExpEscapeChars;
+ segment += escapeChars.includes(glob[i]) ? `\\${glob[i]}` : glob[i];
+ continue;
+ }
+
+ if (glob[i] == escapePrefix) {
+ inEscape = true;
+ continue;
+ }
+
+ if (glob[i] == "[") {
+ if (!inRange) {
+ inRange = true;
+ segment += "[";
+ if (glob[i + 1] == "!") {
+ i++;
+ segment += "^";
+ } else if (glob[i + 1] == "^") {
+ i++;
+ segment += "\\^";
+ }
+ continue;
+ } else if (glob[i + 1] == ":") {
+ let k = i + 1;
+ let value = "";
+ while (glob[k + 1] != null && glob[k + 1] != ":") {
+ value += glob[k + 1];
+ k++;
+ }
+ if (glob[k + 1] == ":" && glob[k + 2] == "]") {
+ i = k + 2;
+ if (value == "alnum") segment += "\\dA-Za-z";
+ else if (value == "alpha") segment += "A-Za-z";
+ else if (value == "ascii") segment += "\x00-\x7F";
+ else if (value == "blank") segment += "\t ";
+ else if (value == "cntrl") segment += "\x00-\x1F\x7F";
+ else if (value == "digit") segment += "\\d";
+ else if (value == "graph") segment += "\x21-\x7E";
+ else if (value == "lower") segment += "a-z";
+ else if (value == "print") segment += "\x20-\x7E";
+ else if (value == "punct") {
+ segment += "!\"#$%&'()*+,\\-./:;<=>?@[\\\\\\]^_‘{|}~";
+ } else if (value == "space") segment += "\\s\v";
+ else if (value == "upper") segment += "A-Z";
+ else if (value == "word") segment += "\\w";
+ else if (value == "xdigit") segment += "\\dA-Fa-f";
+ continue;
+ }
+ }
+ }
+
+ if (glob[i] == "]" && inRange) {
+ inRange = false;
+ segment += "]";
+ continue;
+ }
+
+ if (inRange) {
+ if (glob[i] == "\\") {
+ segment += `\\\\`;
+ } else {
+ segment += glob[i];
+ }
+ continue;
+ }
+
+ if (
+ glob[i] == ")" && groupStack.length > 0 &&
+ groupStack[groupStack.length - 1] != "BRACE"
+ ) {
+ segment += ")";
+ const type = groupStack.pop()!;
+ if (type == "!") {
+ segment += wildcard;
+ } else if (type != "@") {
+ segment += type;
+ }
+ continue;
+ }
+
+ if (
+ glob[i] == "|" && groupStack.length > 0 &&
+ groupStack[groupStack.length - 1] != "BRACE"
+ ) {
+ segment += "|";
+ continue;
+ }
+
+ if (glob[i] == "+" && extended && glob[i + 1] == "(") {
+ i++;
+ groupStack.push("+");
+ segment += "(?:";
+ continue;
+ }
+
+ if (glob[i] == "@" && extended && glob[i + 1] == "(") {
+ i++;
+ groupStack.push("@");
+ segment += "(?:";
+ continue;
+ }
+
+ if (glob[i] == "?") {
+ if (extended && glob[i + 1] == "(") {
+ i++;
+ groupStack.push("?");
+ segment += "(?:";
+ } else {
+ segment += ".";
+ }
+ continue;
+ }
+
+ if (glob[i] == "!" && extended && glob[i + 1] == "(") {
+ i++;
+ groupStack.push("!");
+ segment += "(?!";
+ continue;
+ }
+
+ if (glob[i] == "{") {
+ groupStack.push("BRACE");
+ segment += "(?:";
+ continue;
+ }
+
+ if (glob[i] == "}" && groupStack[groupStack.length - 1] == "BRACE") {
+ groupStack.pop();
+ segment += ")";
+ continue;
+ }
+
+ if (glob[i] == "," && groupStack[groupStack.length - 1] == "BRACE") {
+ segment += "|";
+ continue;
+ }
+
+ if (glob[i] == "*") {
+ if (extended && glob[i + 1] == "(") {
+ i++;
+ groupStack.push("*");
+ segment += "(?:";
+ } else {
+ const prevChar = glob[i - 1];
+ let numStars = 1;
+ while (glob[i + 1] == "*") {
+ i++;
+ numStars++;
+ }
+ const nextChar = glob[i + 1];
+ if (
+ globstarOption && numStars == 2 &&
+ [...seps, undefined].includes(prevChar) &&
+ [...seps, undefined].includes(nextChar)
+ ) {
+ segment += globstar;
+ endsWithSep = true;
+ } else {
+ segment += wildcard;
+ }
+ }
+ continue;
+ }
+
+ segment += regExpEscapeChars.includes(glob[i]) ? `\\${glob[i]}` : glob[i];
+ }
+
+ // Check for unclosed groups or a dangling backslash.
+ if (groupStack.length > 0 || inRange || inEscape) {
+ // Parse failure. Take all characters from this segment literally.
+ segment = "";
+ for (const c of glob.slice(j, i)) {
+ segment += regExpEscapeChars.includes(c) ? `\\${c}` : c;
+ endsWithSep = false;
+ }
+ }
+
+ regExpString += segment;
+ if (!endsWithSep) {
+ regExpString += i < glob.length ? sep : sepMaybe;
+ endsWithSep = true;
+ }
+
+ // Terminates with `i` at the start of the next segment.
+ while (seps.includes(glob[i])) i++;
+
+ // Check that the next value of `j` is indeed higher than the current value.
+ if (!(i > j)) {
+ throw new Error("Assertion failure: i > j (potential infinite loop)");
+ }
+ j = i;
+ }
+
+ regExpString = `^${regExpString}$`;
+ return new RegExp(regExpString, caseInsensitive ? "i" : "");
+}
+
+/** Test whether the given string is a glob */
+export function isGlob(str: string): boolean {
+ const chars: Record<string, string> = { "{": "}", "(": ")", "[": "]" };
+ const regex =
+ /\\(.)|(^!|\*|\?|[\].+)]\?|\[[^\\\]]+\]|\{[^\\}]+\}|\(\?[:!=][^\\)]+\)|\([^|]+\|[^\\)]+\))/;
+
+ if (str === "") {
+ return false;
+ }
+
+ let match: RegExpExecArray | null;
+
+ while ((match = regex.exec(str))) {
+ if (match[2]) return true;
+ let idx = match.index + match[0].length;
+
+ // if an open bracket/brace/paren is escaped,
+ // set the index to the next closing character
+ const open = match[1];
+ const close = open ? chars[open] : null;
+ if (open && close) {
+ const n = str.indexOf(close, idx);
+ if (n !== -1) {
+ idx = n + 1;
+ }
+ }
+
+ str = str.slice(idx);
+ }
+
+ return false;
+}
+
+/** Like normalize(), but doesn't collapse "**\/.." when `globstar` is true. */
+export function normalizeGlob(
+ glob: string,
+ { globstar = false }: GlobOptions = {},
+): string {
+ if (glob.match(/\0/g)) {
+ throw new Error(`Glob contains invalid characters: "${glob}"`);
+ }
+ if (!globstar) {
+ return normalize(glob);
+ }
+ const s = SEP_PATTERN.source;
+ const badParentPattern = new RegExp(
+ `(?<=(${s}|^)\\*\\*${s})\\.\\.(?=${s}|$)`,
+ "g",
+ );
+ return normalize(glob.replace(badParentPattern, "\0")).replace(/\0/g, "..");
+}
+
+/** Like join(), but doesn't collapse "**\/.." when `globstar` is true. */
+export function joinGlobs(
+ globs: string[],
+ { extended = true, globstar = false }: GlobOptions = {},
+): string {
+ if (!globstar || globs.length == 0) {
+ return join(...globs);
+ }
+ if (globs.length === 0) return ".";
+ let joined: string | undefined;
+ for (const glob of globs) {
+ const path = glob;
+ if (path.length > 0) {
+ if (!joined) joined = path;
+ else joined += `${SEP}${path}`;
+ }
+ }
+ if (!joined) return ".";
+ return normalizeGlob(joined, { extended, globstar });
+}
diff --git a/ext/node/polyfills/path/mod.ts b/ext/node/polyfills/path/mod.ts
new file mode 100644
index 000000000..4b4de056b
--- /dev/null
+++ b/ext/node/polyfills/path/mod.ts
@@ -0,0 +1,37 @@
+// Copyright the Browserify authors. MIT License.
+// Ported mostly from https://github.com/browserify/path-browserify/
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+
+import { isWindows } from "internal:deno_node/polyfills/_util/os.ts";
+import _win32 from "internal:deno_node/polyfills/path/win32.ts";
+import _posix from "internal:deno_node/polyfills/path/posix.ts";
+
+const path = isWindows ? _win32 : _posix;
+
+export const win32 = _win32;
+export const posix = _posix;
+export const {
+ basename,
+ delimiter,
+ dirname,
+ extname,
+ format,
+ fromFileUrl,
+ isAbsolute,
+ join,
+ normalize,
+ parse,
+ relative,
+ resolve,
+ sep,
+ toFileUrl,
+ toNamespacedPath,
+} = path;
+
+export * from "internal:deno_node/polyfills/path/common.ts";
+export {
+ SEP,
+ SEP_PATTERN,
+} from "internal:deno_node/polyfills/path/separator.ts";
+export * from "internal:deno_node/polyfills/path/_interface.ts";
+export * from "internal:deno_node/polyfills/path/glob.ts";
diff --git a/ext/node/polyfills/path/posix.ts b/ext/node/polyfills/path/posix.ts
new file mode 100644
index 000000000..8ebf64629
--- /dev/null
+++ b/ext/node/polyfills/path/posix.ts
@@ -0,0 +1,536 @@
+// Copyright the Browserify authors. MIT License.
+// Ported from https://github.com/browserify/path-browserify/
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+
+import type {
+ FormatInputPathObject,
+ ParsedPath,
+} from "internal:deno_node/polyfills/path/_interface.ts";
+import {
+ CHAR_DOT,
+ CHAR_FORWARD_SLASH,
+} from "internal:deno_node/polyfills/path/_constants.ts";
+import { ERR_INVALID_ARG_TYPE } from "internal:deno_node/polyfills/internal/errors.ts";
+
+import {
+ _format,
+ assertPath,
+ encodeWhitespace,
+ isPosixPathSeparator,
+ normalizeString,
+} from "internal:deno_node/polyfills/path/_util.ts";
+
+export const sep = "/";
+export const delimiter = ":";
+
+// path.resolve([from ...], to)
+/**
+ * Resolves `pathSegments` into an absolute path.
+ * @param pathSegments an array of path segments
+ */
+export function resolve(...pathSegments: string[]): string {
+ let resolvedPath = "";
+ let resolvedAbsolute = false;
+
+ for (let i = pathSegments.length - 1; i >= -1 && !resolvedAbsolute; i--) {
+ let path: string;
+
+ if (i >= 0) path = pathSegments[i];
+ else {
+ // deno-lint-ignore no-explicit-any
+ const { Deno } = globalThis as any;
+ if (typeof Deno?.cwd !== "function") {
+ throw new TypeError("Resolved a relative path without a CWD.");
+ }
+ path = Deno.cwd();
+ }
+
+ assertPath(path);
+
+ // Skip empty entries
+ if (path.length === 0) {
+ continue;
+ }
+
+ resolvedPath = `${path}/${resolvedPath}`;
+ resolvedAbsolute = path.charCodeAt(0) === CHAR_FORWARD_SLASH;
+ }
+
+ // At this point the path should be resolved to a full absolute path, but
+ // handle relative paths to be safe (might happen when process.cwd() fails)
+
+ // Normalize the path
+ resolvedPath = normalizeString(
+ resolvedPath,
+ !resolvedAbsolute,
+ "/",
+ isPosixPathSeparator,
+ );
+
+ if (resolvedAbsolute) {
+ if (resolvedPath.length > 0) return `/${resolvedPath}`;
+ else return "/";
+ } else if (resolvedPath.length > 0) return resolvedPath;
+ else return ".";
+}
+
+/**
+ * Normalize the `path`, resolving `'..'` and `'.'` segments.
+ * @param path to be normalized
+ */
+export function normalize(path: string): string {
+ assertPath(path);
+
+ if (path.length === 0) return ".";
+
+ const isAbsolute = path.charCodeAt(0) === CHAR_FORWARD_SLASH;
+ const trailingSeparator =
+ path.charCodeAt(path.length - 1) === CHAR_FORWARD_SLASH;
+
+ // Normalize the path
+ path = normalizeString(path, !isAbsolute, "/", isPosixPathSeparator);
+
+ if (path.length === 0 && !isAbsolute) path = ".";
+ if (path.length > 0 && trailingSeparator) path += "/";
+
+ if (isAbsolute) return `/${path}`;
+ return path;
+}
+
+/**
+ * Verifies whether provided path is absolute
+ * @param path to be verified as absolute
+ */
+export function isAbsolute(path: string): boolean {
+ assertPath(path);
+ return path.length > 0 && path.charCodeAt(0) === CHAR_FORWARD_SLASH;
+}
+
+/**
+ * Join all given a sequence of `paths`,then normalizes the resulting path.
+ * @param paths to be joined and normalized
+ */
+export function join(...paths: string[]): string {
+ if (paths.length === 0) return ".";
+ let joined: string | undefined;
+ for (let i = 0, len = paths.length; i < len; ++i) {
+ const path = paths[i];
+ assertPath(path);
+ if (path.length > 0) {
+ if (!joined) joined = path;
+ else joined += `/${path}`;
+ }
+ }
+ if (!joined) return ".";
+ return normalize(joined);
+}
+
+/**
+ * Return the relative path from `from` to `to` based on current working directory.
+ * @param from path in current working directory
+ * @param to path in current working directory
+ */
+export function relative(from: string, to: string): string {
+ assertPath(from);
+ assertPath(to);
+
+ if (from === to) return "";
+
+ from = resolve(from);
+ to = resolve(to);
+
+ if (from === to) return "";
+
+ // Trim any leading backslashes
+ let fromStart = 1;
+ const fromEnd = from.length;
+ for (; fromStart < fromEnd; ++fromStart) {
+ if (from.charCodeAt(fromStart) !== CHAR_FORWARD_SLASH) break;
+ }
+ const fromLen = fromEnd - fromStart;
+
+ // Trim any leading backslashes
+ let toStart = 1;
+ const toEnd = to.length;
+ for (; toStart < toEnd; ++toStart) {
+ if (to.charCodeAt(toStart) !== CHAR_FORWARD_SLASH) break;
+ }
+ const toLen = toEnd - toStart;
+
+ // Compare paths to find the longest common path from root
+ const length = fromLen < toLen ? fromLen : toLen;
+ let lastCommonSep = -1;
+ let i = 0;
+ for (; i <= length; ++i) {
+ if (i === length) {
+ if (toLen > length) {
+ if (to.charCodeAt(toStart + i) === CHAR_FORWARD_SLASH) {
+ // We get here if `from` is the exact base path for `to`.
+ // For example: from='/foo/bar'; to='/foo/bar/baz'
+ return to.slice(toStart + i + 1);
+ } else if (i === 0) {
+ // We get here if `from` is the root
+ // For example: from='/'; to='/foo'
+ return to.slice(toStart + i);
+ }
+ } else if (fromLen > length) {
+ if (from.charCodeAt(fromStart + i) === CHAR_FORWARD_SLASH) {
+ // We get here if `to` is the exact base path for `from`.
+ // For example: from='/foo/bar/baz'; to='/foo/bar'
+ lastCommonSep = i;
+ } else if (i === 0) {
+ // We get here if `to` is the root.
+ // For example: from='/foo'; to='/'
+ lastCommonSep = 0;
+ }
+ }
+ break;
+ }
+ const fromCode = from.charCodeAt(fromStart + i);
+ const toCode = to.charCodeAt(toStart + i);
+ if (fromCode !== toCode) break;
+ else if (fromCode === CHAR_FORWARD_SLASH) lastCommonSep = i;
+ }
+
+ let out = "";
+ // Generate the relative path based on the path difference between `to`
+ // and `from`
+ for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i) {
+ if (i === fromEnd || from.charCodeAt(i) === CHAR_FORWARD_SLASH) {
+ if (out.length === 0) out += "..";
+ else out += "/..";
+ }
+ }
+
+ // Lastly, append the rest of the destination (`to`) path that comes after
+ // the common path parts
+ if (out.length > 0) return out + to.slice(toStart + lastCommonSep);
+ else {
+ toStart += lastCommonSep;
+ if (to.charCodeAt(toStart) === CHAR_FORWARD_SLASH) ++toStart;
+ return to.slice(toStart);
+ }
+}
+
+/**
+ * Resolves path to a namespace path
+ * @param path to resolve to namespace
+ */
+export function toNamespacedPath(path: string): string {
+ // Non-op on posix systems
+ return path;
+}
+
+/**
+ * Return the directory name of a `path`.
+ * @param path to determine name for
+ */
+export function dirname(path: string): string {
+ assertPath(path);
+ if (path.length === 0) return ".";
+ const hasRoot = path.charCodeAt(0) === CHAR_FORWARD_SLASH;
+ let end = -1;
+ let matchedSlash = true;
+ for (let i = path.length - 1; i >= 1; --i) {
+ if (path.charCodeAt(i) === CHAR_FORWARD_SLASH) {
+ if (!matchedSlash) {
+ end = i;
+ break;
+ }
+ } else {
+ // We saw the first non-path separator
+ matchedSlash = false;
+ }
+ }
+
+ if (end === -1) return hasRoot ? "/" : ".";
+ if (hasRoot && end === 1) return "//";
+ return path.slice(0, end);
+}
+
+/**
+ * Return the last portion of a `path`. Trailing directory separators are ignored.
+ * @param path to process
+ * @param ext of path directory
+ */
+export function basename(path: string, ext = ""): string {
+ if (ext !== undefined && typeof ext !== "string") {
+ throw new ERR_INVALID_ARG_TYPE("ext", ["string"], ext);
+ }
+ assertPath(path);
+
+ let start = 0;
+ let end = -1;
+ let matchedSlash = true;
+ let i: number;
+
+ if (ext !== undefined && ext.length > 0 && ext.length <= path.length) {
+ if (ext.length === path.length && ext === path) return "";
+ let extIdx = ext.length - 1;
+ let firstNonSlashEnd = -1;
+ for (i = path.length - 1; i >= 0; --i) {
+ const code = path.charCodeAt(i);
+ if (code === CHAR_FORWARD_SLASH) {
+ // If we reached a path separator that was not part of a set of path
+ // separators at the end of the string, stop now
+ if (!matchedSlash) {
+ start = i + 1;
+ break;
+ }
+ } else {
+ if (firstNonSlashEnd === -1) {
+ // We saw the first non-path separator, remember this index in case
+ // we need it if the extension ends up not matching
+ matchedSlash = false;
+ firstNonSlashEnd = i + 1;
+ }
+ if (extIdx >= 0) {
+ // Try to match the explicit extension
+ if (code === ext.charCodeAt(extIdx)) {
+ if (--extIdx === -1) {
+ // We matched the extension, so mark this as the end of our path
+ // component
+ end = i;
+ }
+ } else {
+ // Extension does not match, so our result is the entire path
+ // component
+ extIdx = -1;
+ end = firstNonSlashEnd;
+ }
+ }
+ }
+ }
+
+ if (start === end) end = firstNonSlashEnd;
+ else if (end === -1) end = path.length;
+ return path.slice(start, end);
+ } else {
+ for (i = path.length - 1; i >= 0; --i) {
+ if (path.charCodeAt(i) === CHAR_FORWARD_SLASH) {
+ // If we reached a path separator that was not part of a set of path
+ // separators at the end of the string, stop now
+ if (!matchedSlash) {
+ start = i + 1;
+ break;
+ }
+ } else if (end === -1) {
+ // We saw the first non-path separator, mark this as the end of our
+ // path component
+ matchedSlash = false;
+ end = i + 1;
+ }
+ }
+
+ if (end === -1) return "";
+ return path.slice(start, end);
+ }
+}
+
+/**
+ * Return the extension of the `path`.
+ * @param path with extension
+ */
+export function extname(path: string): string {
+ assertPath(path);
+ let startDot = -1;
+ let startPart = 0;
+ let end = -1;
+ let matchedSlash = true;
+ // Track the state of characters (if any) we see before our first dot and
+ // after any path separator we find
+ let preDotState = 0;
+ for (let i = path.length - 1; i >= 0; --i) {
+ const code = path.charCodeAt(i);
+ if (code === CHAR_FORWARD_SLASH) {
+ // If we reached a path separator that was not part of a set of path
+ // separators at the end of the string, stop now
+ if (!matchedSlash) {
+ startPart = i + 1;
+ break;
+ }
+ continue;
+ }
+ if (end === -1) {
+ // We saw the first non-path separator, mark this as the end of our
+ // extension
+ matchedSlash = false;
+ end = i + 1;
+ }
+ if (code === CHAR_DOT) {
+ // If this is our first dot, mark it as the start of our extension
+ if (startDot === -1) startDot = i;
+ else if (preDotState !== 1) preDotState = 1;
+ } else if (startDot !== -1) {
+ // We saw a non-dot and non-path separator before our dot, so we should
+ // have a good chance at having a non-empty extension
+ preDotState = -1;
+ }
+ }
+
+ if (
+ startDot === -1 ||
+ end === -1 ||
+ // We saw a non-dot character immediately before the dot
+ preDotState === 0 ||
+ // The (right-most) trimmed path component is exactly '..'
+ (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)
+ ) {
+ return "";
+ }
+ return path.slice(startDot, end);
+}
+
+/**
+ * Generate a path from `FormatInputPathObject` object.
+ * @param pathObject with path
+ */
+export function format(pathObject: FormatInputPathObject): string {
+ if (pathObject === null || typeof pathObject !== "object") {
+ throw new ERR_INVALID_ARG_TYPE("pathObject", ["Object"], pathObject);
+ }
+ return _format("/", pathObject);
+}
+
+/**
+ * Return a `ParsedPath` object of the `path`.
+ * @param path to process
+ */
+export function parse(path: string): ParsedPath {
+ assertPath(path);
+
+ const ret: ParsedPath = { root: "", dir: "", base: "", ext: "", name: "" };
+ if (path.length === 0) return ret;
+ const isAbsolute = path.charCodeAt(0) === CHAR_FORWARD_SLASH;
+ let start: number;
+ if (isAbsolute) {
+ ret.root = "/";
+ start = 1;
+ } else {
+ start = 0;
+ }
+ let startDot = -1;
+ let startPart = 0;
+ let end = -1;
+ let matchedSlash = true;
+ let i = path.length - 1;
+
+ // Track the state of characters (if any) we see before our first dot and
+ // after any path separator we find
+ let preDotState = 0;
+
+ // Get non-dir info
+ for (; i >= start; --i) {
+ const code = path.charCodeAt(i);
+ if (code === CHAR_FORWARD_SLASH) {
+ // If we reached a path separator that was not part of a set of path
+ // separators at the end of the string, stop now
+ if (!matchedSlash) {
+ startPart = i + 1;
+ break;
+ }
+ continue;
+ }
+ if (end === -1) {
+ // We saw the first non-path separator, mark this as the end of our
+ // extension
+ matchedSlash = false;
+ end = i + 1;
+ }
+ if (code === CHAR_DOT) {
+ // If this is our first dot, mark it as the start of our extension
+ if (startDot === -1) startDot = i;
+ else if (preDotState !== 1) preDotState = 1;
+ } else if (startDot !== -1) {
+ // We saw a non-dot and non-path separator before our dot, so we should
+ // have a good chance at having a non-empty extension
+ preDotState = -1;
+ }
+ }
+
+ if (
+ startDot === -1 ||
+ end === -1 ||
+ // We saw a non-dot character immediately before the dot
+ preDotState === 0 ||
+ // The (right-most) trimmed path component is exactly '..'
+ (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)
+ ) {
+ if (end !== -1) {
+ if (startPart === 0 && isAbsolute) {
+ ret.base = ret.name = path.slice(1, end);
+ } else {
+ ret.base = ret.name = path.slice(startPart, end);
+ }
+ }
+ } else {
+ if (startPart === 0 && isAbsolute) {
+ ret.name = path.slice(1, startDot);
+ ret.base = path.slice(1, end);
+ } else {
+ ret.name = path.slice(startPart, startDot);
+ ret.base = path.slice(startPart, end);
+ }
+ ret.ext = path.slice(startDot, end);
+ }
+
+ if (startPart > 0) ret.dir = path.slice(0, startPart - 1);
+ else if (isAbsolute) ret.dir = "/";
+
+ return ret;
+}
+
+/**
+ * Converts a file URL to a path string.
+ *
+ * ```ts
+ * fromFileUrl("file:///home/foo"); // "/home/foo"
+ * ```
+ * @param url of a file URL
+ */
+export function fromFileUrl(url: string | URL): string {
+ url = url instanceof URL ? url : new URL(url);
+ if (url.protocol != "file:") {
+ throw new TypeError("Must be a file URL.");
+ }
+ return decodeURIComponent(
+ url.pathname.replace(/%(?![0-9A-Fa-f]{2})/g, "%25"),
+ );
+}
+
+/**
+ * Converts a path string to a file URL.
+ *
+ * ```ts
+ * toFileUrl("/home/foo"); // new URL("file:///home/foo")
+ * ```
+ * @param path to convert to file URL
+ */
+export function toFileUrl(path: string): URL {
+ if (!isAbsolute(path)) {
+ throw new TypeError("Must be an absolute path.");
+ }
+ const url = new URL("file:///");
+ url.pathname = encodeWhitespace(
+ path.replace(/%/g, "%25").replace(/\\/g, "%5C"),
+ );
+ return url;
+}
+
+export default {
+ basename,
+ delimiter,
+ dirname,
+ extname,
+ format,
+ fromFileUrl,
+ isAbsolute,
+ join,
+ normalize,
+ parse,
+ relative,
+ resolve,
+ sep,
+ toFileUrl,
+ toNamespacedPath,
+};
diff --git a/ext/node/polyfills/path/separator.ts b/ext/node/polyfills/path/separator.ts
new file mode 100644
index 000000000..2cfde31d0
--- /dev/null
+++ b/ext/node/polyfills/path/separator.ts
@@ -0,0 +1,6 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+
+import { isWindows } from "internal:deno_node/polyfills/_util/os.ts";
+
+export const SEP = isWindows ? "\\" : "/";
+export const SEP_PATTERN = isWindows ? /[\\/]+/ : /\/+/;
diff --git a/ext/node/polyfills/path/win32.ts b/ext/node/polyfills/path/win32.ts
new file mode 100644
index 000000000..4b30e3430
--- /dev/null
+++ b/ext/node/polyfills/path/win32.ts
@@ -0,0 +1,1025 @@
+// Copyright the Browserify authors. MIT License.
+// Ported from https://github.com/browserify/path-browserify/
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+
+import type {
+ FormatInputPathObject,
+ ParsedPath,
+} from "internal:deno_node/polyfills/path/_interface.ts";
+import {
+ CHAR_BACKWARD_SLASH,
+ CHAR_COLON,
+ CHAR_DOT,
+ CHAR_QUESTION_MARK,
+} from "internal:deno_node/polyfills/path/_constants.ts";
+import { ERR_INVALID_ARG_TYPE } from "internal:deno_node/polyfills/internal/errors.ts";
+
+import {
+ _format,
+ assertPath,
+ encodeWhitespace,
+ isPathSeparator,
+ isWindowsDeviceRoot,
+ normalizeString,
+} from "internal:deno_node/polyfills/path/_util.ts";
+import { assert } from "internal:deno_node/polyfills/_util/asserts.ts";
+
+export const sep = "\\";
+export const delimiter = ";";
+
+/**
+ * Resolves path segments into a `path`
+ * @param pathSegments to process to path
+ */
+export function resolve(...pathSegments: string[]): string {
+ let resolvedDevice = "";
+ let resolvedTail = "";
+ let resolvedAbsolute = false;
+
+ for (let i = pathSegments.length - 1; i >= -1; i--) {
+ let path: string;
+ // deno-lint-ignore no-explicit-any
+ const { Deno } = globalThis as any;
+ if (i >= 0) {
+ path = pathSegments[i];
+ } else if (!resolvedDevice) {
+ if (typeof Deno?.cwd !== "function") {
+ throw new TypeError("Resolved a drive-letter-less path without a CWD.");
+ }
+ path = Deno.cwd();
+ } else {
+ if (
+ typeof Deno?.env?.get !== "function" || typeof Deno?.cwd !== "function"
+ ) {
+ throw new TypeError("Resolved a relative path without a CWD.");
+ }
+ path = Deno.cwd();
+
+ // Verify that a cwd was found and that it actually points
+ // to our drive. If not, default to the drive's root.
+ if (
+ path === undefined ||
+ path.slice(0, 3).toLowerCase() !== `${resolvedDevice.toLowerCase()}\\`
+ ) {
+ path = `${resolvedDevice}\\`;
+ }
+ }
+
+ assertPath(path);
+
+ const len = path.length;
+
+ // Skip empty entries
+ if (len === 0) continue;
+
+ let rootEnd = 0;
+ let device = "";
+ let isAbsolute = false;
+ const code = path.charCodeAt(0);
+
+ // Try to match a root
+ if (len > 1) {
+ if (isPathSeparator(code)) {
+ // Possible UNC root
+
+ // If we started with a separator, we know we at least have an
+ // absolute path of some kind (UNC or otherwise)
+ isAbsolute = true;
+
+ if (isPathSeparator(path.charCodeAt(1))) {
+ // Matched double path separator at beginning
+ let j = 2;
+ let last = j;
+ // Match 1 or more non-path separators
+ for (; j < len; ++j) {
+ if (isPathSeparator(path.charCodeAt(j))) break;
+ }
+ if (j < len && j !== last) {
+ const firstPart = path.slice(last, j);
+ // Matched!
+ last = j;
+ // Match 1 or more path separators
+ for (; j < len; ++j) {
+ if (!isPathSeparator(path.charCodeAt(j))) break;
+ }
+ if (j < len && j !== last) {
+ // Matched!
+ last = j;
+ // Match 1 or more non-path separators
+ for (; j < len; ++j) {
+ if (isPathSeparator(path.charCodeAt(j))) break;
+ }
+ if (j === len) {
+ // We matched a UNC root only
+ device = `\\\\${firstPart}\\${path.slice(last)}`;
+ rootEnd = j;
+ } else if (j !== last) {
+ // We matched a UNC root with leftovers
+
+ device = `\\\\${firstPart}\\${path.slice(last, j)}`;
+ rootEnd = j;
+ }
+ }
+ }
+ } else {
+ rootEnd = 1;
+ }
+ } else if (isWindowsDeviceRoot(code)) {
+ // Possible device root
+
+ if (path.charCodeAt(1) === CHAR_COLON) {
+ device = path.slice(0, 2);
+ rootEnd = 2;
+ if (len > 2) {
+ if (isPathSeparator(path.charCodeAt(2))) {
+ // Treat separator following drive name as an absolute path
+ // indicator
+ isAbsolute = true;
+ rootEnd = 3;
+ }
+ }
+ }
+ }
+ } else if (isPathSeparator(code)) {
+ // `path` contains just a path separator
+ rootEnd = 1;
+ isAbsolute = true;
+ }
+
+ if (
+ device.length > 0 &&
+ resolvedDevice.length > 0 &&
+ device.toLowerCase() !== resolvedDevice.toLowerCase()
+ ) {
+ // This path points to another device so it is not applicable
+ continue;
+ }
+
+ if (resolvedDevice.length === 0 && device.length > 0) {
+ resolvedDevice = device;
+ }
+ if (!resolvedAbsolute) {
+ resolvedTail = `${path.slice(rootEnd)}\\${resolvedTail}`;
+ resolvedAbsolute = isAbsolute;
+ }
+
+ if (resolvedAbsolute && resolvedDevice.length > 0) break;
+ }
+
+ // At this point the path should be resolved to a full absolute path,
+ // but handle relative paths to be safe (might happen when process.cwd()
+ // fails)
+
+ // Normalize the tail path
+ resolvedTail = normalizeString(
+ resolvedTail,
+ !resolvedAbsolute,
+ "\\",
+ isPathSeparator,
+ );
+
+ return resolvedDevice + (resolvedAbsolute ? "\\" : "") + resolvedTail || ".";
+}
+
+/**
+ * Normalizes a `path`
+ * @param path to normalize
+ */
+export function normalize(path: string): string {
+ assertPath(path);
+ const len = path.length;
+ if (len === 0) return ".";
+ let rootEnd = 0;
+ let device: string | undefined;
+ let isAbsolute = false;
+ const code = path.charCodeAt(0);
+
+ // Try to match a root
+ if (len > 1) {
+ if (isPathSeparator(code)) {
+ // Possible UNC root
+
+ // If we started with a separator, we know we at least have an absolute
+ // path of some kind (UNC or otherwise)
+ isAbsolute = true;
+
+ if (isPathSeparator(path.charCodeAt(1))) {
+ // Matched double path separator at beginning
+ let j = 2;
+ let last = j;
+ // Match 1 or more non-path separators
+ for (; j < len; ++j) {
+ if (isPathSeparator(path.charCodeAt(j))) break;
+ }
+ if (j < len && j !== last) {
+ const firstPart = path.slice(last, j);
+ // Matched!
+ last = j;
+ // Match 1 or more path separators
+ for (; j < len; ++j) {
+ if (!isPathSeparator(path.charCodeAt(j))) break;
+ }
+ if (j < len && j !== last) {
+ // Matched!
+ last = j;
+ // Match 1 or more non-path separators
+ for (; j < len; ++j) {
+ if (isPathSeparator(path.charCodeAt(j))) break;
+ }
+ if (j === len) {
+ // We matched a UNC root only
+ // Return the normalized version of the UNC root since there
+ // is nothing left to process
+
+ return `\\\\${firstPart}\\${path.slice(last)}\\`;
+ } else if (j !== last) {
+ // We matched a UNC root with leftovers
+
+ device = `\\\\${firstPart}\\${path.slice(last, j)}`;
+ rootEnd = j;
+ }
+ }
+ }
+ } else {
+ rootEnd = 1;
+ }
+ } else if (isWindowsDeviceRoot(code)) {
+ // Possible device root
+
+ if (path.charCodeAt(1) === CHAR_COLON) {
+ device = path.slice(0, 2);
+ rootEnd = 2;
+ if (len > 2) {
+ if (isPathSeparator(path.charCodeAt(2))) {
+ // Treat separator following drive name as an absolute path
+ // indicator
+ isAbsolute = true;
+ rootEnd = 3;
+ }
+ }
+ }
+ }
+ } else if (isPathSeparator(code)) {
+ // `path` contains just a path separator, exit early to avoid unnecessary
+ // work
+ return "\\";
+ }
+
+ let tail: string;
+ if (rootEnd < len) {
+ tail = normalizeString(
+ path.slice(rootEnd),
+ !isAbsolute,
+ "\\",
+ isPathSeparator,
+ );
+ } else {
+ tail = "";
+ }
+ if (tail.length === 0 && !isAbsolute) tail = ".";
+ if (tail.length > 0 && isPathSeparator(path.charCodeAt(len - 1))) {
+ tail += "\\";
+ }
+ if (device === undefined) {
+ if (isAbsolute) {
+ if (tail.length > 0) return `\\${tail}`;
+ else return "\\";
+ } else if (tail.length > 0) {
+ return tail;
+ } else {
+ return "";
+ }
+ } else if (isAbsolute) {
+ if (tail.length > 0) return `${device}\\${tail}`;
+ else return `${device}\\`;
+ } else if (tail.length > 0) {
+ return device + tail;
+ } else {
+ return device;
+ }
+}
+
+/**
+ * Verifies whether path is absolute
+ * @param path to verify
+ */
+export function isAbsolute(path: string): boolean {
+ assertPath(path);
+ const len = path.length;
+ if (len === 0) return false;
+
+ const code = path.charCodeAt(0);
+ if (isPathSeparator(code)) {
+ return true;
+ } else if (isWindowsDeviceRoot(code)) {
+ // Possible device root
+
+ if (len > 2 && path.charCodeAt(1) === CHAR_COLON) {
+ if (isPathSeparator(path.charCodeAt(2))) return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * Join all given a sequence of `paths`,then normalizes the resulting path.
+ * @param paths to be joined and normalized
+ */
+export function join(...paths: string[]): string {
+ const pathsCount = paths.length;
+ if (pathsCount === 0) return ".";
+
+ let joined: string | undefined;
+ let firstPart: string | null = null;
+ for (let i = 0; i < pathsCount; ++i) {
+ const path = paths[i];
+ assertPath(path);
+ if (path.length > 0) {
+ if (joined === undefined) joined = firstPart = path;
+ else joined += `\\${path}`;
+ }
+ }
+
+ if (joined === undefined) return ".";
+
+ // Make sure that the joined path doesn't start with two slashes, because
+ // normalize() will mistake it for an UNC path then.
+ //
+ // This step is skipped when it is very clear that the user actually
+ // intended to point at an UNC path. This is assumed when the first
+ // non-empty string arguments starts with exactly two slashes followed by
+ // at least one more non-slash character.
+ //
+ // Note that for normalize() to treat a path as an UNC path it needs to
+ // have at least 2 components, so we don't filter for that here.
+ // This means that the user can use join to construct UNC paths from
+ // a server name and a share name; for example:
+ // path.join('//server', 'share') -> '\\\\server\\share\\')
+ let needsReplace = true;
+ let slashCount = 0;
+ assert(firstPart != null);
+ if (isPathSeparator(firstPart.charCodeAt(0))) {
+ ++slashCount;
+ const firstLen = firstPart.length;
+ if (firstLen > 1) {
+ if (isPathSeparator(firstPart.charCodeAt(1))) {
+ ++slashCount;
+ if (firstLen > 2) {
+ if (isPathSeparator(firstPart.charCodeAt(2))) ++slashCount;
+ else {
+ // We matched a UNC path in the first part
+ needsReplace = false;
+ }
+ }
+ }
+ }
+ }
+ if (needsReplace) {
+ // Find any more consecutive slashes we need to replace
+ for (; slashCount < joined.length; ++slashCount) {
+ if (!isPathSeparator(joined.charCodeAt(slashCount))) break;
+ }
+
+ // Replace the slashes if needed
+ if (slashCount >= 2) joined = `\\${joined.slice(slashCount)}`;
+ }
+
+ return normalize(joined);
+}
+
+/**
+ * It will solve the relative path from `from` to `to`, for instance:
+ * from = 'C:\\orandea\\test\\aaa'
+ * to = 'C:\\orandea\\impl\\bbb'
+ * The output of the function should be: '..\\..\\impl\\bbb'
+ * @param from relative path
+ * @param to relative path
+ */
+export function relative(from: string, to: string): string {
+ assertPath(from);
+ assertPath(to);
+
+ if (from === to) return "";
+
+ const fromOrig = resolve(from);
+ const toOrig = resolve(to);
+
+ if (fromOrig === toOrig) return "";
+
+ from = fromOrig.toLowerCase();
+ to = toOrig.toLowerCase();
+
+ if (from === to) return "";
+
+ // Trim any leading backslashes
+ let fromStart = 0;
+ let fromEnd = from.length;
+ for (; fromStart < fromEnd; ++fromStart) {
+ if (from.charCodeAt(fromStart) !== CHAR_BACKWARD_SLASH) break;
+ }
+ // Trim trailing backslashes (applicable to UNC paths only)
+ for (; fromEnd - 1 > fromStart; --fromEnd) {
+ if (from.charCodeAt(fromEnd - 1) !== CHAR_BACKWARD_SLASH) break;
+ }
+ const fromLen = fromEnd - fromStart;
+
+ // Trim any leading backslashes
+ let toStart = 0;
+ let toEnd = to.length;
+ for (; toStart < toEnd; ++toStart) {
+ if (to.charCodeAt(toStart) !== CHAR_BACKWARD_SLASH) break;
+ }
+ // Trim trailing backslashes (applicable to UNC paths only)
+ for (; toEnd - 1 > toStart; --toEnd) {
+ if (to.charCodeAt(toEnd - 1) !== CHAR_BACKWARD_SLASH) break;
+ }
+ const toLen = toEnd - toStart;
+
+ // Compare paths to find the longest common path from root
+ const length = fromLen < toLen ? fromLen : toLen;
+ let lastCommonSep = -1;
+ let i = 0;
+ for (; i <= length; ++i) {
+ if (i === length) {
+ if (toLen > length) {
+ if (to.charCodeAt(toStart + i) === CHAR_BACKWARD_SLASH) {
+ // We get here if `from` is the exact base path for `to`.
+ // For example: from='C:\\foo\\bar'; to='C:\\foo\\bar\\baz'
+ return toOrig.slice(toStart + i + 1);
+ } else if (i === 2) {
+ // We get here if `from` is the device root.
+ // For example: from='C:\\'; to='C:\\foo'
+ return toOrig.slice(toStart + i);
+ }
+ }
+ if (fromLen > length) {
+ if (from.charCodeAt(fromStart + i) === CHAR_BACKWARD_SLASH) {
+ // We get here if `to` is the exact base path for `from`.
+ // For example: from='C:\\foo\\bar'; to='C:\\foo'
+ lastCommonSep = i;
+ } else if (i === 2) {
+ // We get here if `to` is the device root.
+ // For example: from='C:\\foo\\bar'; to='C:\\'
+ lastCommonSep = 3;
+ }
+ }
+ break;
+ }
+ const fromCode = from.charCodeAt(fromStart + i);
+ const toCode = to.charCodeAt(toStart + i);
+ if (fromCode !== toCode) break;
+ else if (fromCode === CHAR_BACKWARD_SLASH) lastCommonSep = i;
+ }
+
+ // We found a mismatch before the first common path separator was seen, so
+ // return the original `to`.
+ if (i !== length && lastCommonSep === -1) {
+ return toOrig;
+ }
+
+ let out = "";
+ if (lastCommonSep === -1) lastCommonSep = 0;
+ // Generate the relative path based on the path difference between `to` and
+ // `from`
+ for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i) {
+ if (i === fromEnd || from.charCodeAt(i) === CHAR_BACKWARD_SLASH) {
+ if (out.length === 0) out += "..";
+ else out += "\\..";
+ }
+ }
+
+ // Lastly, append the rest of the destination (`to`) path that comes after
+ // the common path parts
+ if (out.length > 0) {
+ return out + toOrig.slice(toStart + lastCommonSep, toEnd);
+ } else {
+ toStart += lastCommonSep;
+ if (toOrig.charCodeAt(toStart) === CHAR_BACKWARD_SLASH) ++toStart;
+ return toOrig.slice(toStart, toEnd);
+ }
+}
+
+/**
+ * Resolves path to a namespace path
+ * @param path to resolve to namespace
+ */
+export function toNamespacedPath(path: string): string {
+ // Note: this will *probably* throw somewhere.
+ if (typeof path !== "string") return path;
+ if (path.length === 0) return "";
+
+ const resolvedPath = resolve(path);
+
+ if (resolvedPath.length >= 3) {
+ if (resolvedPath.charCodeAt(0) === CHAR_BACKWARD_SLASH) {
+ // Possible UNC root
+
+ if (resolvedPath.charCodeAt(1) === CHAR_BACKWARD_SLASH) {
+ const code = resolvedPath.charCodeAt(2);
+ if (code !== CHAR_QUESTION_MARK && code !== CHAR_DOT) {
+ // Matched non-long UNC root, convert the path to a long UNC path
+ return `\\\\?\\UNC\\${resolvedPath.slice(2)}`;
+ }
+ }
+ } else if (isWindowsDeviceRoot(resolvedPath.charCodeAt(0))) {
+ // Possible device root
+
+ if (
+ resolvedPath.charCodeAt(1) === CHAR_COLON &&
+ resolvedPath.charCodeAt(2) === CHAR_BACKWARD_SLASH
+ ) {
+ // Matched device root, convert the path to a long UNC path
+ return `\\\\?\\${resolvedPath}`;
+ }
+ }
+ }
+
+ return path;
+}
+
+/**
+ * Return the directory name of a `path`.
+ * @param path to determine name for
+ */
+export function dirname(path: string): string {
+ assertPath(path);
+ const len = path.length;
+ if (len === 0) return ".";
+ let rootEnd = -1;
+ let end = -1;
+ let matchedSlash = true;
+ let offset = 0;
+ const code = path.charCodeAt(0);
+
+ // Try to match a root
+ if (len > 1) {
+ if (isPathSeparator(code)) {
+ // Possible UNC root
+
+ rootEnd = offset = 1;
+
+ if (isPathSeparator(path.charCodeAt(1))) {
+ // Matched double path separator at beginning
+ let j = 2;
+ let last = j;
+ // Match 1 or more non-path separators
+ for (; j < len; ++j) {
+ if (isPathSeparator(path.charCodeAt(j))) break;
+ }
+ if (j < len && j !== last) {
+ // Matched!
+ last = j;
+ // Match 1 or more path separators
+ for (; j < len; ++j) {
+ if (!isPathSeparator(path.charCodeAt(j))) break;
+ }
+ if (j < len && j !== last) {
+ // Matched!
+ last = j;
+ // Match 1 or more non-path separators
+ for (; j < len; ++j) {
+ if (isPathSeparator(path.charCodeAt(j))) break;
+ }
+ if (j === len) {
+ // We matched a UNC root only
+ return path;
+ }
+ if (j !== last) {
+ // We matched a UNC root with leftovers
+
+ // Offset by 1 to include the separator after the UNC root to
+ // treat it as a "normal root" on top of a (UNC) root
+ rootEnd = offset = j + 1;
+ }
+ }
+ }
+ }
+ } else if (isWindowsDeviceRoot(code)) {
+ // Possible device root
+
+ if (path.charCodeAt(1) === CHAR_COLON) {
+ rootEnd = offset = 2;
+ if (len > 2) {
+ if (isPathSeparator(path.charCodeAt(2))) rootEnd = offset = 3;
+ }
+ }
+ }
+ } else if (isPathSeparator(code)) {
+ // `path` contains just a path separator, exit early to avoid
+ // unnecessary work
+ return path;
+ }
+
+ for (let i = len - 1; i >= offset; --i) {
+ if (isPathSeparator(path.charCodeAt(i))) {
+ if (!matchedSlash) {
+ end = i;
+ break;
+ }
+ } else {
+ // We saw the first non-path separator
+ matchedSlash = false;
+ }
+ }
+
+ if (end === -1) {
+ if (rootEnd === -1) return ".";
+ else end = rootEnd;
+ }
+ return path.slice(0, end);
+}
+
+/**
+ * Return the last portion of a `path`. Trailing directory separators are ignored.
+ * @param path to process
+ * @param ext of path directory
+ */
+export function basename(path: string, ext = ""): string {
+ if (ext !== undefined && typeof ext !== "string") {
+ throw new ERR_INVALID_ARG_TYPE("ext", ["string"], ext);
+ }
+
+ assertPath(path);
+
+ let start = 0;
+ let end = -1;
+ let matchedSlash = true;
+ let i: number;
+
+ // Check for a drive letter prefix so as not to mistake the following
+ // path separator as an extra separator at the end of the path that can be
+ // disregarded
+ if (path.length >= 2) {
+ const drive = path.charCodeAt(0);
+ if (isWindowsDeviceRoot(drive)) {
+ if (path.charCodeAt(1) === CHAR_COLON) start = 2;
+ }
+ }
+
+ if (ext !== undefined && ext.length > 0 && ext.length <= path.length) {
+ if (ext.length === path.length && ext === path) return "";
+ let extIdx = ext.length - 1;
+ let firstNonSlashEnd = -1;
+ for (i = path.length - 1; i >= start; --i) {
+ const code = path.charCodeAt(i);
+ if (isPathSeparator(code)) {
+ // If we reached a path separator that was not part of a set of path
+ // separators at the end of the string, stop now
+ if (!matchedSlash) {
+ start = i + 1;
+ break;
+ }
+ } else {
+ if (firstNonSlashEnd === -1) {
+ // We saw the first non-path separator, remember this index in case
+ // we need it if the extension ends up not matching
+ matchedSlash = false;
+ firstNonSlashEnd = i + 1;
+ }
+ if (extIdx >= 0) {
+ // Try to match the explicit extension
+ if (code === ext.charCodeAt(extIdx)) {
+ if (--extIdx === -1) {
+ // We matched the extension, so mark this as the end of our path
+ // component
+ end = i;
+ }
+ } else {
+ // Extension does not match, so our result is the entire path
+ // component
+ extIdx = -1;
+ end = firstNonSlashEnd;
+ }
+ }
+ }
+ }
+
+ if (start === end) end = firstNonSlashEnd;
+ else if (end === -1) end = path.length;
+ return path.slice(start, end);
+ } else {
+ for (i = path.length - 1; i >= start; --i) {
+ if (isPathSeparator(path.charCodeAt(i))) {
+ // If we reached a path separator that was not part of a set of path
+ // separators at the end of the string, stop now
+ if (!matchedSlash) {
+ start = i + 1;
+ break;
+ }
+ } else if (end === -1) {
+ // We saw the first non-path separator, mark this as the end of our
+ // path component
+ matchedSlash = false;
+ end = i + 1;
+ }
+ }
+
+ if (end === -1) return "";
+ return path.slice(start, end);
+ }
+}
+
+/**
+ * Return the extension of the `path`.
+ * @param path with extension
+ */
+export function extname(path: string): string {
+ assertPath(path);
+ let start = 0;
+ let startDot = -1;
+ let startPart = 0;
+ let end = -1;
+ let matchedSlash = true;
+ // Track the state of characters (if any) we see before our first dot and
+ // after any path separator we find
+ let preDotState = 0;
+
+ // Check for a drive letter prefix so as not to mistake the following
+ // path separator as an extra separator at the end of the path that can be
+ // disregarded
+
+ if (
+ path.length >= 2 &&
+ path.charCodeAt(1) === CHAR_COLON &&
+ isWindowsDeviceRoot(path.charCodeAt(0))
+ ) {
+ start = startPart = 2;
+ }
+
+ for (let i = path.length - 1; i >= start; --i) {
+ const code = path.charCodeAt(i);
+ if (isPathSeparator(code)) {
+ // If we reached a path separator that was not part of a set of path
+ // separators at the end of the string, stop now
+ if (!matchedSlash) {
+ startPart = i + 1;
+ break;
+ }
+ continue;
+ }
+ if (end === -1) {
+ // We saw the first non-path separator, mark this as the end of our
+ // extension
+ matchedSlash = false;
+ end = i + 1;
+ }
+ if (code === CHAR_DOT) {
+ // If this is our first dot, mark it as the start of our extension
+ if (startDot === -1) startDot = i;
+ else if (preDotState !== 1) preDotState = 1;
+ } else if (startDot !== -1) {
+ // We saw a non-dot and non-path separator before our dot, so we should
+ // have a good chance at having a non-empty extension
+ preDotState = -1;
+ }
+ }
+
+ if (
+ startDot === -1 ||
+ end === -1 ||
+ // We saw a non-dot character immediately before the dot
+ preDotState === 0 ||
+ // The (right-most) trimmed path component is exactly '..'
+ (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)
+ ) {
+ return "";
+ }
+ return path.slice(startDot, end);
+}
+
+/**
+ * Generate a path from `FormatInputPathObject` object.
+ * @param pathObject with path
+ */
+export function format(pathObject: FormatInputPathObject): string {
+ if (pathObject === null || typeof pathObject !== "object") {
+ throw new ERR_INVALID_ARG_TYPE("pathObject", ["Object"], pathObject);
+ }
+ return _format("\\", pathObject);
+}
+
+/**
+ * Return a `ParsedPath` object of the `path`.
+ * @param path to process
+ */
+export function parse(path: string): ParsedPath {
+ assertPath(path);
+
+ const ret: ParsedPath = { root: "", dir: "", base: "", ext: "", name: "" };
+
+ const len = path.length;
+ if (len === 0) return ret;
+
+ let rootEnd = 0;
+ let code = path.charCodeAt(0);
+
+ // Try to match a root
+ if (len > 1) {
+ if (isPathSeparator(code)) {
+ // Possible UNC root
+
+ rootEnd = 1;
+ if (isPathSeparator(path.charCodeAt(1))) {
+ // Matched double path separator at beginning
+ let j = 2;
+ let last = j;
+ // Match 1 or more non-path separators
+ for (; j < len; ++j) {
+ if (isPathSeparator(path.charCodeAt(j))) break;
+ }
+ if (j < len && j !== last) {
+ // Matched!
+ last = j;
+ // Match 1 or more path separators
+ for (; j < len; ++j) {
+ if (!isPathSeparator(path.charCodeAt(j))) break;
+ }
+ if (j < len && j !== last) {
+ // Matched!
+ last = j;
+ // Match 1 or more non-path separators
+ for (; j < len; ++j) {
+ if (isPathSeparator(path.charCodeAt(j))) break;
+ }
+ if (j === len) {
+ // We matched a UNC root only
+
+ rootEnd = j;
+ } else if (j !== last) {
+ // We matched a UNC root with leftovers
+
+ rootEnd = j + 1;
+ }
+ }
+ }
+ }
+ } else if (isWindowsDeviceRoot(code)) {
+ // Possible device root
+
+ if (path.charCodeAt(1) === CHAR_COLON) {
+ rootEnd = 2;
+ if (len > 2) {
+ if (isPathSeparator(path.charCodeAt(2))) {
+ if (len === 3) {
+ // `path` contains just a drive root, exit early to avoid
+ // unnecessary work
+ ret.root = ret.dir = path;
+ return ret;
+ }
+ rootEnd = 3;
+ }
+ } else {
+ // `path` contains just a drive root, exit early to avoid
+ // unnecessary work
+ ret.root = ret.dir = path;
+ return ret;
+ }
+ }
+ }
+ } else if (isPathSeparator(code)) {
+ // `path` contains just a path separator, exit early to avoid
+ // unnecessary work
+ ret.root = ret.dir = path;
+ return ret;
+ }
+
+ if (rootEnd > 0) ret.root = path.slice(0, rootEnd);
+
+ let startDot = -1;
+ let startPart = rootEnd;
+ let end = -1;
+ let matchedSlash = true;
+ let i = path.length - 1;
+
+ // Track the state of characters (if any) we see before our first dot and
+ // after any path separator we find
+ let preDotState = 0;
+
+ // Get non-dir info
+ for (; i >= rootEnd; --i) {
+ code = path.charCodeAt(i);
+ if (isPathSeparator(code)) {
+ // If we reached a path separator that was not part of a set of path
+ // separators at the end of the string, stop now
+ if (!matchedSlash) {
+ startPart = i + 1;
+ break;
+ }
+ continue;
+ }
+ if (end === -1) {
+ // We saw the first non-path separator, mark this as the end of our
+ // extension
+ matchedSlash = false;
+ end = i + 1;
+ }
+ if (code === CHAR_DOT) {
+ // If this is our first dot, mark it as the start of our extension
+ if (startDot === -1) startDot = i;
+ else if (preDotState !== 1) preDotState = 1;
+ } else if (startDot !== -1) {
+ // We saw a non-dot and non-path separator before our dot, so we should
+ // have a good chance at having a non-empty extension
+ preDotState = -1;
+ }
+ }
+
+ if (
+ startDot === -1 ||
+ end === -1 ||
+ // We saw a non-dot character immediately before the dot
+ preDotState === 0 ||
+ // The (right-most) trimmed path component is exactly '..'
+ (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)
+ ) {
+ if (end !== -1) {
+ ret.base = ret.name = path.slice(startPart, end);
+ }
+ } else {
+ ret.name = path.slice(startPart, startDot);
+ ret.base = path.slice(startPart, end);
+ ret.ext = path.slice(startDot, end);
+ }
+
+ // If the directory is the root, use the entire root as the `dir` including
+ // the trailing slash if any (`C:\abc` -> `C:\`). Otherwise, strip out the
+ // trailing slash (`C:\abc\def` -> `C:\abc`).
+ if (startPart > 0 && startPart !== rootEnd) {
+ ret.dir = path.slice(0, startPart - 1);
+ } else ret.dir = ret.root;
+
+ return ret;
+}
+
+/**
+ * Converts a file URL to a path string.
+ *
+ * ```ts
+ * fromFileUrl("file:///home/foo"); // "\\home\\foo"
+ * fromFileUrl("file:///C:/Users/foo"); // "C:\\Users\\foo"
+ * fromFileUrl("file://localhost/home/foo"); // "\\\\localhost\\home\\foo"
+ * ```
+ * @param url of a file URL
+ */
+export function fromFileUrl(url: string | URL): string {
+ url = url instanceof URL ? url : new URL(url);
+ if (url.protocol != "file:") {
+ throw new TypeError("Must be a file URL.");
+ }
+ let path = decodeURIComponent(
+ url.pathname.replace(/\//g, "\\").replace(/%(?![0-9A-Fa-f]{2})/g, "%25"),
+ ).replace(/^\\*([A-Za-z]:)(\\|$)/, "$1\\");
+ if (url.hostname != "") {
+ // Note: The `URL` implementation guarantees that the drive letter and
+ // hostname are mutually exclusive. Otherwise it would not have been valid
+ // to append the hostname and path like this.
+ path = `\\\\${url.hostname}${path}`;
+ }
+ return path;
+}
+
+/**
+ * Converts a path string to a file URL.
+ *
+ * ```ts
+ * toFileUrl("\\home\\foo"); // new URL("file:///home/foo")
+ * toFileUrl("C:\\Users\\foo"); // new URL("file:///C:/Users/foo")
+ * toFileUrl("\\\\127.0.0.1\\home\\foo"); // new URL("file://127.0.0.1/home/foo")
+ * ```
+ * @param path to convert to file URL
+ */
+export function toFileUrl(path: string): URL {
+ if (!isAbsolute(path)) {
+ throw new TypeError("Must be an absolute path.");
+ }
+ const [, hostname, pathname] = path.match(
+ /^(?:[/\\]{2}([^/\\]+)(?=[/\\](?:[^/\\]|$)))?(.*)/,
+ )!;
+ const url = new URL("file:///");
+ url.pathname = encodeWhitespace(pathname.replace(/%/g, "%25"));
+ if (hostname != null && hostname != "localhost") {
+ url.hostname = hostname;
+ if (!url.hostname) {
+ throw new TypeError("Invalid hostname.");
+ }
+ }
+ return url;
+}
+
+export default {
+ basename,
+ delimiter,
+ dirname,
+ extname,
+ format,
+ fromFileUrl,
+ isAbsolute,
+ join,
+ normalize,
+ parse,
+ relative,
+ resolve,
+ sep,
+ toFileUrl,
+ toNamespacedPath,
+};