File size: 4,880 Bytes
f500658 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | import { realpathSync, statSync } from "node:fs";
import { homedir } from "node:os";
import { isAbsolute, join, resolve as nodeResolvePath, relative, sep } from "node:path";
import { fileURLToPath } from "node:url";
import { spawnProcessSync } from "./child-process.ts";
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
export interface PathInputOptions {
/** Trim leading/trailing whitespace before normalization. */
trim?: boolean;
/** Expand leading `~` to a home directory. Defaults to true. */
expandTilde?: boolean;
/** Home directory used for `~` expansion. Defaults to `os.homedir()`. */
homeDir?: string;
/** Strip a leading `@`, used for CLI @file paths. */
stripAtPrefix?: boolean;
/** Normalize unicode space variants to regular spaces. */
normalizeUnicodeSpaces?: boolean;
}
/**
* Resolve a path to its canonical (real) form, following symlinks.
* Falls back to the raw path if resolution fails (e.g. the target does
* not exist yet), so that callers never crash on missing filesystem
* entries.
*/
export function canonicalizePath(path: string): string {
try {
return realpathSync(path);
} catch {
return path;
}
}
export function getFileRevision(path: string): string | undefined {
try {
const stats = statSync(path, { bigint: true });
return `${stats.dev}:${stats.ino}:${stats.size}:${stats.mtimeNs}:${stats.ctimeNs}`;
} catch {
return undefined;
}
}
/**
* Returns true if the value is NOT a package source (npm:, git:, etc.)
* or a remote URL protocol. Bare names, relative paths, and file: URLs
* are considered local.
*/
export function isLocalPath(value: string): boolean {
const trimmed = value.trim();
// Known non-local prefixes. file: URLs are local paths and are intentionally resolved by resolvePath().
if (
trimmed.startsWith("npm:") ||
trimmed.startsWith("git:") ||
trimmed.startsWith("github:") ||
trimmed.startsWith("http:") ||
trimmed.startsWith("https:") ||
trimmed.startsWith("ssh:")
) {
return false;
}
return true;
}
/** Convert Git Bash, MSYS, Cygwin, and WSL drive paths to a form native Windows APIs accept. */
export function normalizeWindowsShellPath(filePath: string): string {
if (!filePath.startsWith("/") || filePath.startsWith("//") || filePath.includes("\\")) return filePath;
const match = filePath.match(/^\/(?:mnt\/|cygdrive\/)?([a-z])(?:\/(.*))?$/i);
if (!match) return filePath;
const suffix = match[2]?.replaceAll("/", "\\");
return `${match[1].toUpperCase()}:\\${suffix ?? ""}`;
}
export function normalizePath(input: string, options: PathInputOptions = {}): string {
let normalized = options.trim ? input.trim() : input;
if (options.normalizeUnicodeSpaces) {
normalized = normalized.replace(UNICODE_SPACES, " ");
}
if (options.stripAtPrefix && normalized.startsWith("@")) {
normalized = normalized.slice(1);
}
if (process.platform === "win32") {
normalized = normalizeWindowsShellPath(normalized);
}
if (options.expandTilde ?? true) {
const home = options.homeDir ?? homedir();
if (normalized === "~") return home;
if (normalized.startsWith("~/") || (process.platform === "win32" && normalized.startsWith("~\\"))) {
return join(home, normalized.slice(2));
}
}
if (/^file:\/\//.test(normalized)) {
return fileURLToPath(normalized);
}
return normalized;
}
export function resolvePath(input: string, baseDir: string = process.cwd(), options: PathInputOptions = {}): string {
const normalized = normalizePath(input, options);
const normalizedBaseDir = normalizePath(baseDir);
return isAbsolute(normalized) ? nodeResolvePath(normalized) : nodeResolvePath(normalizedBaseDir, normalized);
}
export function getCwdRelativePath(filePath: string, cwd: string): string | undefined {
const resolvedCwd = resolvePath(cwd);
const resolvedPath = resolvePath(filePath, resolvedCwd);
const relativePath = relative(resolvedCwd, resolvedPath);
const isInsideCwd =
relativePath === "" ||
(relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath));
return isInsideCwd ? relativePath || "." : undefined;
}
export function formatPathRelativeToCwdOrAbsolute(filePath: string, cwd: string): string {
const absolutePath = resolvePath(filePath, cwd);
return (getCwdRelativePath(absolutePath, cwd) ?? absolutePath).split(sep).join("/");
}
export function markPathIgnoredByCloudSync(path: string): void {
const attrs =
process.platform === "darwin"
? ["com.dropbox.ignored", "com.apple.fileprovider.ignore#P"]
: process.platform === "linux"
? ["user.com.dropbox.ignored"]
: [];
for (const attr of attrs) {
if (process.platform === "darwin") {
spawnProcessSync("xattr", ["-w", attr, "1", path], { encoding: "utf-8", stdio: "ignore" });
} else {
spawnProcessSync("setfattr", ["-n", attr, "-v", "1", path], { encoding: "utf-8", stdio: "ignore" });
}
}
}
|