// Copyright the Browserify authors. MIT License. // Ported from https://github.com/browserify/path-browserify/ // Copyright 2018-2025 the Deno authors. MIT license. import type { FormatInputPathObject, ParsedPath, } from "ext:deno_node/path/_interface.ts"; import { CHAR_DOT, CHAR_FORWARD_SLASH } from "ext:deno_node/path/_constants.ts"; import { ERR_INVALID_ARG_TYPE } from "ext:deno_node/internal/errors.ts"; import { _format, assertPath, isPosixPathSeparator, normalizeString, } from "ext:deno_node/path/_util.ts"; import { primordials } from "ext:core/mod.js"; import { validateString } from "ext:deno_node/internal/validators.mjs"; import { isWindows } from "ext:deno_node/_util/os.ts"; import process from "node:process"; const { StringPrototypeReplace, StringPrototypeCharCodeAt, StringPrototypeIndexOf, StringPrototypeSlice, SafeRegExp, } = primordials; export const sep = "/"; export const delimiter = ":"; const posixCwd = (() => { if (isWindows) { // Converts Windows' backslash path separators to POSIX forward slashes // and truncates any drive indicator const regexp = new SafeRegExp(/\\/g); return () => { const cwd = StringPrototypeReplace(process.cwd(), regexp, "/"); return StringPrototypeSlice(cwd, StringPrototypeIndexOf(cwd, "/")); }; } // We're already on POSIX, no need for any transformations return () => process.cwd(); })(); // path.resolve([from ...], to) /** * Resolves `pathSegments` into an absolute path. * @param pathSegments an array of path segments */ export function resolve(...pathSegments: string[]): string { if ( pathSegments.length === 0 || (pathSegments.length === 1 && (pathSegments[0] === "" || pathSegments[0] === ".")) ) { const cwd = posixCwd(); if (StringPrototypeCharCodeAt(cwd, 0) === CHAR_FORWARD_SLASH) { return cwd; } } let resolvedPath = ""; let resolvedAbsolute = false; for (let i = pathSegments.length - 1; i >= 0 && !resolvedAbsolute; i--) { const path = pathSegments[i]; validateString(path, `paths[${i}]`); // Skip empty entries if (path.length === 0) { continue; } resolvedPath = `${path}/${resolvedPath}`; resolvedAbsolute = StringPrototypeCharCodeAt(path, 0) === CHAR_FORWARD_SLASH; } if (!resolvedAbsolute) { const cwd = posixCwd(); resolvedPath = `${cwd}/${resolvedPath}`; resolvedAbsolute = StringPrototypeCharCodeAt(cwd, 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) { return `/${resolvedPath}`; } return resolvedPath.length > 0 ? resolvedPath : "."; } /** * 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 = StringPrototypeCharCodeAt(path, 0) === CHAR_FORWARD_SLASH; const trailingSeparator = StringPrototypeCharCodeAt(path, 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 && StringPrototypeCharCodeAt(path, 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 ""; // Trim leading forward slashes. from = resolve(from); to = resolve(to); if (from === to) return ""; const fromStart = 1; const fromEnd = from.length; const fromLen = fromEnd - fromStart; const toStart = 1; const toLen = to.length - 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++) { const fromCode = StringPrototypeCharCodeAt(from, fromStart + i); if (fromCode !== StringPrototypeCharCodeAt(to, toStart + i)) { break; } else if (fromCode === CHAR_FORWARD_SLASH) { lastCommonSep = i; } } if (i === length) { if (toLen > length) { if (StringPrototypeCharCodeAt(to, 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 StringPrototypeSlice(to, toStart + i + 1); } if (i === 0) { // We get here if `from` is the root // For example: from='/'; to='/foo' return StringPrototypeSlice(to, toStart + i); } } else if (fromLen > length) { if ( StringPrototypeCharCodeAt(from, 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/bar'; to='/' lastCommonSep = 0; } } } 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 || StringPrototypeCharCodeAt(from, i) === CHAR_FORWARD_SLASH ) { out += out.length === 0 ? ".." : "/.."; } } // Lastly, append the rest of the destination (`to`) path that comes after // the common path parts return out + StringPrototypeSlice(to, toStart + lastCommonSep); } /** * 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 = StringPrototypeCharCodeAt(path, 0) === CHAR_FORWARD_SLASH; let end = -1; let matchedSlash = true; for (let i = path.length - 1; i >= 1; --i) { if (StringPrototypeCharCodeAt(path, 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 StringPrototypeSlice(path, 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 = StringPrototypeCharCodeAt(path, 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 === StringPrototypeCharCodeAt(ext, 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 StringPrototypeSlice(path, start, end); } else { for (i = path.length - 1; i >= 0; --i) { if (StringPrototypeCharCodeAt(path, 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 StringPrototypeSlice(path, 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 = StringPrototypeCharCodeAt(path, 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 StringPrototypeSlice(path, 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 = StringPrototypeCharCodeAt(path, 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 = StringPrototypeCharCodeAt(path, 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 = StringPrototypeSlice(path, 1, end); } else { ret.base = ret.name = StringPrototypeSlice(path, startPart, end); } } } else { if (startPart === 0 && isAbsolute) { ret.name = StringPrototypeSlice(path, 1, startDot); ret.base = StringPrototypeSlice(path, 1, end); } else { ret.name = StringPrototypeSlice(path, startPart, startDot); ret.base = StringPrototypeSlice(path, startPart, end); } ret.ext = StringPrototypeSlice(path, startDot, end); } if (startPart > 0) ret.dir = StringPrototypeSlice(path, 0, startPart - 1); else if (isAbsolute) ret.dir = "/"; return ret; } export const _makeLong = toNamespacedPath; export default { basename, delimiter, dirname, extname, format, isAbsolute, join, normalize, parse, relative, resolve, sep, toNamespacedPath, _makeLong, };