UNPKG

ttsc

Version:

General-purpose TypeScript-Go compiler, runtime, plugin host, and LSP host.

276 lines 12 kB
"use strict"; 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