ttsc
Version:
General-purpose TypeScript-Go compiler, runtime, plugin host, and LSP host.
276 lines • 12 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createFilesystemPathIdentityContext = createFilesystemPathIdentityContext;
exports.isFilesystemPathIdentityWithin = isFilesystemPathIdentityWithin;
exports.resolveFilesystemPath = resolveFilesystemPath;
exports.createProjectInputPathIdentityContext = createProjectInputPathIdentityContext;
exports.isProjectInputPathIdentityWithin = isProjectInputPathIdentityWithin;
exports.resolveProjectInputPath = resolveProjectInputPath;
const node_child_process_1 = __importDefault(require("node:child_process"));
const node_fs_1 = __importDefault(require("node:fs"));
const node_path_1 = __importDefault(require("node:path"));
const windowsDirectoryCaseSensitivity_1 = require("./windowsDirectoryCaseSensitivity");
/**
* Create one filesystem-identity resolver for a filesystem transaction.
*
* Existing segments use their physical spelling. A missing suffix keeps exact
* spelling only under a case-sensitive directory; otherwise its canonical
* spelling is folded so aliases remain one declaration before they exist.
*/
function createFilesystemPathIdentityContext(operations = {}) {
const identities = new Map();
const realpaths = new Map();
const sensitivities = new Map();
const platform = operations.platform ?? process.platform;
const pathApi = platform === "win32" ? node_path_1.default.win32 : node_path_1.default;
const throwOnRealpathError = operations.throwOnRealpathError ?? true;
const realpath = operations.realpath ?? physicalRealpath;
const caseSensitive = operations.caseSensitive ??
((directory) => filesystemDirectoryIsCaseSensitive(directory, platform));
const resolve = (location) => {
const normalized = resolveFilesystemPath(location, platform);
const cached = identities.get(normalized);
if (cached !== undefined)
return cached.identity;
let existing = normalized;
const missing = [];
while (true) {
const physical = cachedRealpath(realpaths, realpath, existing, platform, throwOnRealpathError);
if (physical !== undefined) {
const sensitive = missing.length === 0
? true
: cachedCaseSensitivity(sensitivities, caseSensitive, physical);
const suffix = sensitive
? missing
: missing.map((segment) => segment.toLowerCase());
const canonical = pathApi.resolve(physical, ...suffix);
const identity = {
key: filesystemPathIdentityKey(canonical, platform),
path: canonical,
};
identities.set(normalized, {
ancestor: physical,
identity,
});
return identity;
}
const parent = pathApi.dirname(existing);
if (parent === existing) {
const sensitive = cachedCaseSensitivity(sensitivities, caseSensitive, existing);
const identity = {
key: filesystemPathIdentityKey(sensitive ? normalized : normalized.toLowerCase(), platform),
path: normalized,
};
identities.set(normalized, {
ancestor: normalized,
identity,
});
return identity;
}
missing.unshift(pathApi.basename(existing));
existing = parent;
}
};
return {
caseSensitive: (directory) => {
const normalized = resolveFilesystemPath(directory, platform);
resolve(normalized);
return cachedCaseSensitivity(sensitivities, caseSensitive, identities.get(normalized).ancestor);
},
isWithin: (root, candidate) => isFilesystemPathIdentityWithin(resolve(root).key, resolve(candidate).key, platform),
resolve,
};
}
function isFilesystemPathIdentityWithin(root, candidate, platform = process.platform) {
if (candidate === root)
return true;
const separator = platform === "win32" ? node_path_1.default.win32.sep : node_path_1.default.sep;
return candidate.startsWith(root.endsWith(separator) ? root : `${root}${separator}`);
}
function resolveFilesystemPath(location, platform = process.platform) {
const pathApi = platform === "win32" ? node_path_1.default.win32 : node_path_1.default;
if (platform !== "win32") {
return pathApi.resolve(location);
}
const normalized = location.replaceAll("/", "\\");
if (normalized.toLowerCase().startsWith("\\\\?\\unc\\")) {
return pathApi.resolve(`\\\\${normalized.slice(8)}`);
}
if (normalized.startsWith("\\\\?\\") &&
/^[A-Za-z]:\\/.test(normalized.slice(4))) {
return pathApi.resolve(normalized.slice(4));
}
return pathApi.resolve(normalized);
}
function cachedRealpath(cache, realpath, location, platform, throwOnRealpathError) {
const cached = cache.get(location);
if (cached !== undefined)
return cached.found ? cached.path : undefined;
try {
const physical = resolveFilesystemPath(realpath(location), platform);
cache.set(location, { found: true, path: physical });
return physical;
}
catch (error) {
if (throwOnRealpathError && isMissingFilesystemEntry(error) === false) {
throw error;
}
cache.set(location, { found: false });
return undefined;
}
}
function cachedCaseSensitivity(cache, caseSensitive, directory) {
const cached = cache.get(directory);
if (cached !== undefined)
return cached;
const sensitive = caseSensitive(directory);
cache.set(directory, sensitive);
return sensitive;
}
function physicalRealpath(location) {
return node_fs_1.default.realpathSync.native?.(location) ?? node_fs_1.default.realpathSync(location);
}
function filesystemDirectoryIsCaseSensitive(directory, platform) {
let entries;
try {
entries = node_fs_1.default.readdirSync(directory);
}
catch {
return unprovenCaseSensitivity(platform);
}
const foldedNames = new Map();
for (const name of entries) {
const folded = name.toLowerCase();
const previous = foldedNames.get(folded);
if (previous !== undefined && previous !== name)
return true;
foldedNames.set(folded, name);
}
let rejectedAlternate = false;
for (const name of entries) {
const alternate = alternateCase(name);
if (alternate === name)
continue;
try {
node_fs_1.default.lstatSync(node_path_1.default.join(directory, alternate));
return false;
}
catch (error) {
if (isMissingFilesystemEntry(error)) {
rejectedAlternate = true;
continue;
}
throw error;
}
}
if (rejectedAlternate)
return true;
if (platform === "darwin") {
// APFS/HFS case semantics are volume-wide. An empty directory has no child
// name to probe, so ask the same read-only question of its existing name in
// the parent and walk upward until a name with ASCII case is available.
// This avoids a write probe while still distinguishing default APFS from a
// case-sensitive volume.
let current = resolveFilesystemPath(directory, platform);
while (true) {
const parent = node_path_1.default.dirname(current);
if (parent === current)
break;
const name = node_path_1.default.basename(current);
const alternate = alternateCase(name);
if (alternate !== name) {
try {
physicalRealpath(node_path_1.default.join(parent, alternate));
return false;
}
catch (error) {
if (isMissingFilesystemEntry(error) === false)
throw error;
}
}
current = parent;
}
return unprovenCaseSensitivity(platform);
}
if (platform !== "win32")
return true;
// Node does not expose the Windows per-directory flag. Prefer fsutil's
// read-only answer, then conservatively preserve distinct declarations.
const queried = queryWindowsDirectoryCaseSensitivity(directory);
if (queried !== undefined)
return queried;
return unprovenCaseSensitivity(platform);
}
/**
* Read the per-directory flag without depending on fsutil's display language.
*
* English output is cheap to recognize directly. Other Windows locales write
* console-code-page bytes that Node cannot reliably decode, so their raw
* message is interpreted against the volume-root query.
*/
function queryWindowsDirectoryCaseSensitivity(directory) {
const result = queryWindowsDirectoryCaseSensitivityBytes(directory);
if (result === undefined)
return undefined;
const volumeRoot = node_path_1.default.win32.parse(directory).root;
const direct = (0, windowsDirectoryCaseSensitivity_1.parseWindowsDirectoryCaseSensitivity)(result, undefined, volumeRoot);
if (direct !== undefined)
return direct;
const volume = queryWindowsDirectoryCaseSensitivityBytes(volumeRoot);
return (0, windowsDirectoryCaseSensitivity_1.parseWindowsDirectoryCaseSensitivity)(result, volume, volumeRoot);
}
function queryWindowsDirectoryCaseSensitivityBytes(directory) {
const result = node_child_process_1.default.spawnSync("fsutil.exe", ["file", "queryCaseSensitiveInfo", directory], { windowsHide: true });
return result.error === undefined &&
result.status === 0 &&
Buffer.isBuffer(result.stdout)
? result.stdout
: undefined;
}
/**
* What to assume when the volume refused to answer.
*
* Every probe above returns a proof: two names that fold together prove the
* volume keeps them apart, and a name that opens under the other case proves it
* does not. This is the remaining case — an empty or unreadable directory, a
* name with no letter to alter, a Windows build whose `fsutil` lacks the
* subcommand — and the only honest answer is the platform's own default.
*
* NTFS and APFS are case-insensitive unless a volume or a directory was opted
* out, so guessing sensitive there splits one file into two identities under
* two spellings, which is the failure this module exists to remove. The rarer
* mistake, on a volume that really was opted out, merges two files differing
* only in case into one watched identity.
*/
function unprovenCaseSensitivity(platform) {
return platform !== "win32" && platform !== "darwin";
}
function filesystemPathIdentityKey(location, platform) {
if (platform !== "win32")
return location;
const root = node_path_1.default.win32.parse(location).root;
return `${root.toLowerCase()}${location.slice(root.length)}`;
}
function alternateCase(value) {
return value.replace(/[A-Za-z]/g, (character) => character === character.toLowerCase()
? character.toUpperCase()
: character.toLowerCase());
}
function isMissingFilesystemEntry(error) {
return (error instanceof Error &&
"code" in error &&
(error.code === "ENOENT" || error.code === "ENOTDIR"));
}
function createProjectInputPathIdentityContext(operations = {}) {
return createFilesystemPathIdentityContext(operations);
}
function isProjectInputPathIdentityWithin(root, candidate) {
return isFilesystemPathIdentityWithin(root, candidate);
}
function resolveProjectInputPath(location) {
return resolveFilesystemPath(location);
}
//# sourceMappingURL=projectInputPathIdentity.js.map