// node:path. The algorithms are Node's (lib/path.js): lexical normalization, resolve // against process.cwd(). path.win32 is a full port (path-win32.ts) — it's pure string // logic in Node, true on every host, so it runs the same here. import { makeWin32Path } from "./path-win32"; function assertPath(path: unknown, name = "path"): asserts path is string { if (typeof path !== "string") { const got = path === null ? "null" : path === undefined ? "undefined" : `type ${typeof path} (${String(path)})`; throw Object.assign(new TypeError(`The "${name}" argument must be of type string. Received ${got}`), { code: "ERR_INVALID_ARG_TYPE" }); } } // ---- matchesGlob ---- // Node's own matching (lib/internal/fs/glob.js), not bash/minimatch: a pattern with no // glob metachar is a plain (case-sensitive) string compare; any metachar anywhere // makes the WHOLE match case-insensitive, including its literal segments — a real // quirk, golden-verified against Node 24.16, not a design choice of ours. A trailing // "**" segment must still consume at least one real segment ("a/**" misses "a" but // hits "a/b"); elsewhere "**" may match zero. No path segment's leading "." is ever // matched by a wildcard unless the pattern segment itself starts with ".". const GLOB_MAGIC = /[*?{}[\]]/; /** {a,b} alternation, expanded into every concrete pattern (no nested braces). */ function expandBraces(pattern: string): string[] { const open = pattern.indexOf("{"); if (open === -1) return [pattern]; const close = pattern.indexOf("}", open); if (close === -1) return [pattern]; const prefix = pattern.slice(0, open); const suffix = pattern.slice(close + 1); const out: string[] = []; for (const option of pattern.slice(open + 1, close).split(",")) { out.push(...expandBraces(prefix + option + suffix)); } return out; } /** One glob segment (no "/"; "**" is handled by the caller) as a regex source. */ function compileGlobSegment(seg: string): string { let out = ""; for (let i = 0; i < seg.length; i++) { const c = seg[i]!; if (c === "*") { out += "[^/]*"; while (seg[i + 1] === "*") i++; } else if (c === "?") { out += "[^/]"; } else if (c === "[") { let j = i + 1; let neg = false; if (seg[j] === "!" || seg[j] === "^") { neg = true; j++; } let body = ""; while (j < seg.length && seg[j] !== "]") { body += seg[j]; j++; } if (j >= seg.length) { out += "\\["; continue; } // unmatched "[": literal out += `[${neg ? "^" : ""}${body.replace(/\\/g, "\\\\")}]`; i = j; } else { out += /[.+^${}()|\\]/.test(c) ? `\\${c}` : c; } } return out; } /** Does `seg` match path segment `s`? Dotfiles are off-limits unless `seg` itself * starts with a literal ".". */ function globSegmentMatches(seg: string, s: string): boolean { const guard = seg.startsWith(".") ? "" : "(?!\\.)"; return new RegExp(`^${guard}${compileGlobSegment(seg)}$`, "i").test(s); } /** "**" may match zero or more segments anywhere it isn't the pattern's last segment. */ function globSegsMatch(pats: string[], segs: string[]): boolean { if (pats.length === 0) return segs.length === 0; const p = pats[0]!; if (p === "**") { if (globSegsMatch(pats.slice(1), segs)) return true; return segs.length > 0 && !segs[0]!.startsWith(".") && globSegsMatch(pats, segs.slice(1)); } return segs.length > 0 && globSegmentMatches(p, segs[0]!) && globSegsMatch(pats.slice(1), segs.slice(1)); } /** One expanded (brace-free) pattern against the whole path. */ function globOneMatches(pattern: string, segs: string[]): boolean { const pats = pattern.split("/"); if (pats.length > 0 && pats[pats.length - 1] === "**") { const head = pats.slice(0, -1); for (let k = 1; k <= segs.length; k++) { const tail = segs.slice(segs.length - k); if (tail.some((s) => s.startsWith("."))) continue; if (globSegsMatch(head, segs.slice(0, segs.length - k))) return true; } return false; } return globSegsMatch(pats, segs); } export function matchesGlob(path: unknown, pattern: unknown): boolean { assertPath(path); assertPath(pattern, "pattern"); if (!GLOB_MAGIC.test(pattern)) return path === pattern; const segs = path.split("/"); return expandBraces(pattern).some((p) => globOneMatches(p, segs)); } /** Resolve "." and ".." segments; `allowAboveRoot` keeps leading ".." for relative paths. */ function normalizeString(path: string, allowAboveRoot: boolean): string { let res = ""; let lastSegmentLength = 0; let lastSlash = -1; let dots = 0; let code = 0; for (let i = 0; i <= path.length; ++i) { if (i < path.length) code = path.charCodeAt(i); else if (code === 47) break; else code = 47; if (code === 47) { if (lastSlash === i - 1 || dots === 1) { // empty segment or "." } else if (dots === 2) { if (res.length < 2 || lastSegmentLength !== 2 || !res.endsWith("..")) { if (res.length > 2) { const lastSlashIndex = res.lastIndexOf("/"); if (lastSlashIndex === -1) { res = ""; lastSegmentLength = 0; } else { res = res.slice(0, lastSlashIndex); lastSegmentLength = res.length - 1 - res.lastIndexOf("/"); } lastSlash = i; dots = 0; continue; } else if (res.length !== 0) { res = ""; lastSegmentLength = 0; lastSlash = i; dots = 0; continue; } } if (allowAboveRoot) { res += res.length > 0 ? "/.." : ".."; lastSegmentLength = 2; } } else { if (res.length > 0) res += `/${path.slice(lastSlash + 1, i)}`; else res = path.slice(lastSlash + 1, i); lastSegmentLength = i - lastSlash - 1; } lastSlash = i; dots = 0; } else if (code === 46 && dots !== -1) { ++dots; } else { dots = -1; } } return res; } export interface ParsedPath { root: string; dir: string; base: string; ext: string; name: string; } export function makePath(cwd: () => string, env: () => Record = () => ({})) { const posix = { sep: "/", delimiter: ":", resolve(...args: string[]): string { let resolvedPath = ""; let resolvedAbsolute = false; for (let i = args.length - 1; i >= -1 && !resolvedAbsolute; i--) { const path = i >= 0 ? args[i] : cwd(); assertPath(path, `paths[${i}]`); if (path.length === 0) continue; resolvedPath = `${path}/${resolvedPath}`; resolvedAbsolute = path.charCodeAt(0) === 47; } resolvedPath = normalizeString(resolvedPath, !resolvedAbsolute); if (resolvedAbsolute) return `/${resolvedPath}`; return resolvedPath.length > 0 ? resolvedPath : "."; }, normalize(path: string): string { assertPath(path); if (path.length === 0) return "."; const isAbsolute = path.charCodeAt(0) === 47; const trailingSeparator = path.charCodeAt(path.length - 1) === 47; let out = normalizeString(path, !isAbsolute); if (out.length === 0) { if (isAbsolute) return "/"; return trailingSeparator ? "./" : "."; } if (trailingSeparator) out += "/"; return isAbsolute ? `/${out}` : out; }, isAbsolute(path: string): boolean { assertPath(path); return path.length > 0 && path.charCodeAt(0) === 47; }, join(...args: string[]): string { if (args.length === 0) return "."; let joined: string | undefined; for (const arg of args) { assertPath(arg); if (arg.length > 0) joined = joined === undefined ? arg : `${joined}/${arg}`; } return joined === undefined ? "." : posix.normalize(joined); }, relative(from: string, to: string): string { assertPath(from, "from"); assertPath(to, "to"); if (from === to) return ""; from = posix.resolve(from); to = posix.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; const length = fromLen < toLen ? fromLen : toLen; let lastCommonSep = -1; let i = 0; for (; i < length; i++) { const fromCode = from.charCodeAt(fromStart + i); if (fromCode !== to.charCodeAt(toStart + i)) break; else if (fromCode === 47) lastCommonSep = i; } if (i === length) { if (toLen > length) { if (to.charCodeAt(toStart + i) === 47) return to.slice(toStart + i + 1); if (i === 0) return to.slice(toStart + i); } else if (fromLen > length) { if (from.charCodeAt(fromStart + i) === 47) lastCommonSep = i; else if (i === 0) lastCommonSep = 0; } } let out = ""; for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i) { if (i === fromEnd || from.charCodeAt(i) === 47) out += out.length === 0 ? ".." : "/.."; } return `${out}${to.slice(toStart + lastCommonSep)}`; }, toNamespacedPath(path: string): string { return path; }, dirname(path: string): string { assertPath(path); if (path.length === 0) return "."; const hasRoot = path.charCodeAt(0) === 47; let end = -1; let matchedSlash = true; for (let i = path.length - 1; i >= 1; --i) { if (path.charCodeAt(i) === 47) { if (!matchedSlash) { end = i; break; } } else { matchedSlash = false; } } if (end === -1) return hasRoot ? "/" : "."; if (hasRoot && end === 1) return "//"; return path.slice(0, end); }, basename(path: string, suffix?: string): string { if (suffix !== undefined) assertPath(suffix, "suffix"); assertPath(path); let start = 0; let end = -1; let matchedSlash = true; if (suffix !== undefined && suffix.length > 0 && suffix.length <= path.length) { if (suffix === path) return ""; let extIdx = suffix.length - 1; let firstNonSlashEnd = -1; for (let i = path.length - 1; i >= 0; --i) { const code = path.charCodeAt(i); if (code === 47) { if (!matchedSlash) { start = i + 1; break; } } else { if (firstNonSlashEnd === -1) { matchedSlash = false; firstNonSlashEnd = i + 1; } if (extIdx >= 0) { if (code === suffix.charCodeAt(extIdx)) { if (--extIdx === -1) end = i; } else { extIdx = -1; end = firstNonSlashEnd; } } } } if (start === end) end = firstNonSlashEnd; else if (end === -1) end = path.length; return path.slice(start, end); } for (let i = path.length - 1; i >= 0; --i) { if (path.charCodeAt(i) === 47) { if (!matchedSlash) { start = i + 1; break; } } else if (end === -1) { matchedSlash = false; end = i + 1; } } return end === -1 ? "" : path.slice(start, end); }, extname(path: string): string { assertPath(path); let startDot = -1; let startPart = 0; let end = -1; let matchedSlash = true; let preDotState = 0; for (let i = path.length - 1; i >= 0; --i) { const code = path.charCodeAt(i); if (code === 47) { if (!matchedSlash) { startPart = i + 1; break; } continue; } if (end === -1) { matchedSlash = false; end = i + 1; } if (code === 46) { if (startDot === -1) startDot = i; else if (preDotState !== 1) preDotState = 1; } else if (startDot !== -1) { preDotState = -1; } } if (startDot === -1 || end === -1 || preDotState === 0 || (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)) { return ""; } return path.slice(startDot, end); }, format(pathObject: Partial): string { if (pathObject === null || typeof pathObject !== "object") { throw Object.assign(new TypeError(`The "pathObject" argument must be of type object. Received ${pathObject === null ? "null" : `type ${typeof pathObject}`}`), { code: "ERR_INVALID_ARG_TYPE" }); } const dir = pathObject.dir || pathObject.root; const ext = pathObject.ext ? (pathObject.ext[0] === "." ? pathObject.ext : `.${pathObject.ext}`) : ""; const base = pathObject.base || `${pathObject.name || ""}${ext}`; if (!dir) return base; return dir === pathObject.root ? `${dir}${base}` : `${dir}/${base}`; }, 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) === 47; 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; let preDotState = 0; for (; i >= start; --i) { const code = path.charCodeAt(i); if (code === 47) { if (!matchedSlash) { startPart = i + 1; break; } continue; } if (end === -1) { matchedSlash = false; end = i + 1; } if (code === 46) { if (startDot === -1) startDot = i; else if (preDotState !== 1) preDotState = 1; } else if (startDot !== -1) { preDotState = -1; } } if (end !== -1) { const start2 = startPart === 0 && isAbsolute ? 1 : startPart; if (startDot === -1 || preDotState === 0 || (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)) { ret.base = ret.name = path.slice(start2, end); } else { ret.name = path.slice(start2, startDot); ret.base = path.slice(start2, end); ret.ext = path.slice(startDot, end); } } if (startPart > 0) ret.dir = path.slice(0, startPart - 1); else if (isAbsolute) ret.dir = "/"; return ret; }, matchesGlob, posix: undefined as unknown, win32: undefined as unknown, }; const win32 = makeWin32Path(cwd, env) as unknown as Record; Object.assign(win32, { sep: "\\", delimiter: ";", posix, win32 }); posix.posix = posix; posix.win32 = win32; return posix; }