mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-19 13:33:35 +08:00
fix: normalize Windows drive-root sessions (#601)
Windows folder selection can surface drive roots as D:, which Node treats as drive-relative instead of the absolute D:\ root. Normalize drive-root inputs before resolving launch directories, filesystem access roots, transcript metadata, and workspace paths. Use path.relative containment checks so drive roots and child projects remain distinct in session grouping and file access. Constraint: Windows drive-root inputs may arrive as C:, D:, or any other single-letter drive prefix Rejected: Special-case only D: | every Windows drive letter has the same drive-relative semantics Confidence: high Scope-risk: moderate Directive: Keep Windows drive-root normalization centralized; do not reintroduce string-prefix containment checks for workspace roots Tested: bun test src/server/__tests__/windows-drive-path.test.ts src/server/__tests__/filesystem.test.ts src/server/__tests__/sessions.test.ts src/server/__tests__/workspace-service.test.ts Tested: cd desktop && bun run test -- --run src/components/layout/Sidebar.test.tsx Tested: bun run check:server Tested: cd desktop && bun run lint Not-tested: Real Windows desktop smoke on a physical Windows machine Related: #601
This commit is contained in:
parent
1b2bded1a2
commit
fe8aac6b83
@ -782,6 +782,23 @@ describe('Sidebar', () => {
|
|||||||
expect(screen.getAllByText('worktree')).toHaveLength(1)
|
expect(screen.getAllByText('worktree')).toHaveLength(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('keeps a Windows drive root session separate from sessions in child projects', () => {
|
||||||
|
const now = new Date().toISOString()
|
||||||
|
useSessionStore.setState({
|
||||||
|
sessions: [
|
||||||
|
makeSession('drive-root', 'Drive Root Session', 'D:\\', now),
|
||||||
|
makeSession('drive-project', 'Drive Project Session', 'D:\\SomeProject', now),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
render(<Sidebar />)
|
||||||
|
|
||||||
|
expect(screen.getByText('D:')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('SomeProject')).toBeInTheDocument()
|
||||||
|
expect(screen.getByRole('button', { name: /Drive Root Session/ })).toBeInTheDocument()
|
||||||
|
expect(screen.getByRole('button', { name: /Drive Project Session/ })).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
it('right-aligns running status, worktree marker, and update time on session rows', () => {
|
it('right-aligns running status, worktree marker, and update time on session rows', () => {
|
||||||
vi.useFakeTimers()
|
vi.useFakeTimers()
|
||||||
vi.setSystemTime(new Date('2026-05-19T12:00:00.000Z'))
|
vi.setSystemTime(new Date('2026-05-19T12:00:00.000Z'))
|
||||||
|
|||||||
30
src/server/__tests__/windows-drive-path.test.ts
Normal file
30
src/server/__tests__/windows-drive-path.test.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { describe, expect, it } from 'bun:test'
|
||||||
|
import {
|
||||||
|
isSameOrInsidePathForPlatform,
|
||||||
|
normalizeDriveRootPathForPlatform,
|
||||||
|
} from '../services/windowsDrivePath.js'
|
||||||
|
import { SessionService } from '../services/sessionService.js'
|
||||||
|
|
||||||
|
describe('Windows drive root path handling', () => {
|
||||||
|
it('normalizes drive-relative root inputs to absolute drive roots on Windows', () => {
|
||||||
|
expect(normalizeDriveRootPathForPlatform('D:', 'win32')).toBe('D:\\')
|
||||||
|
expect(normalizeDriveRootPathForPlatform('d:', 'win32')).toBe('d:\\')
|
||||||
|
expect(normalizeDriveRootPathForPlatform('D:\\', 'win32')).toBe('D:\\')
|
||||||
|
expect(normalizeDriveRootPathForPlatform('D:\\project', 'win32')).toBe('D:\\project')
|
||||||
|
expect(normalizeDriveRootPathForPlatform('D:', 'darwin')).toBe('D:')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('recovers sanitized Windows drive-root transcript directories', () => {
|
||||||
|
const service = new SessionService()
|
||||||
|
expect(service.desanitizePath('D--')).toBe('D:\\')
|
||||||
|
expect(service.desanitizePath('D--project')).toBe('D:\\project')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('treats absolute Windows drive-root children as inside the selected root', () => {
|
||||||
|
expect(isSameOrInsidePathForPlatform('D:\\', 'D:', 'win32')).toBe(true)
|
||||||
|
expect(isSameOrInsidePathForPlatform('D:\\child', 'D:', 'win32')).toBe(true)
|
||||||
|
expect(isSameOrInsidePathForPlatform('D:\\child', 'D:\\', 'win32')).toBe(true)
|
||||||
|
expect(isSameOrInsidePathForPlatform('D:\\project-extra', 'D:\\project', 'win32')).toBe(false)
|
||||||
|
expect(isSameOrInsidePathForPlatform('E:\\child', 'D:\\', 'win32')).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -13,6 +13,10 @@ import { findGitRoot, gitExe } from '../../utils/git.js'
|
|||||||
import { ripGrep } from '../../utils/ripgrep.js'
|
import { ripGrep } from '../../utils/ripgrep.js'
|
||||||
import { getInitialSettings } from '../../utils/settings/settings.js'
|
import { getInitialSettings } from '../../utils/settings/settings.js'
|
||||||
import { isWithinRegisteredFilesystemRoot } from '../services/filesystemAccessRoots.js'
|
import { isWithinRegisteredFilesystemRoot } from '../services/filesystemAccessRoots.js'
|
||||||
|
import {
|
||||||
|
isSameOrInsidePathForPlatform,
|
||||||
|
normalizeDriveRootPathForPlatform,
|
||||||
|
} from '../services/windowsDrivePath.js'
|
||||||
|
|
||||||
type FilesystemEntry = {
|
type FilesystemEntry = {
|
||||||
name: string
|
name: string
|
||||||
@ -41,14 +45,7 @@ const IMAGE_MIME_TYPES: Record<string, string> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isWithinRoot(targetPath: string, rootPath: string): boolean {
|
function isWithinRoot(targetPath: string, rootPath: string): boolean {
|
||||||
const target = normalizeComparablePath(targetPath)
|
return isSameOrInsidePathForPlatform(targetPath, rootPath)
|
||||||
const root = normalizeComparablePath(rootPath)
|
|
||||||
return target === root || target.startsWith(`${root}${path.sep}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeComparablePath(filePath: string): string {
|
|
||||||
const resolved = path.resolve(filePath)
|
|
||||||
return process.platform === 'win32' ? resolved.toLowerCase() : resolved
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isVcsMetadataDirectoryName(name: string): boolean {
|
function isVcsMetadataDirectoryName(name: string): boolean {
|
||||||
@ -56,7 +53,7 @@ function isVcsMetadataDirectoryName(name: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isAllowedFilesystemPath(targetPath: string): boolean {
|
function isAllowedFilesystemPath(targetPath: string): boolean {
|
||||||
const resolvedPath = path.resolve(targetPath)
|
const resolvedPath = path.resolve(normalizeDriveRootPathForPlatform(targetPath))
|
||||||
const homeDir = path.resolve(os.homedir())
|
const homeDir = path.resolve(os.homedir())
|
||||||
|
|
||||||
if (isWithinRoot(resolvedPath, homeDir) || isWithinRoot(resolvedPath, '/tmp')) {
|
if (isWithinRoot(resolvedPath, homeDir) || isWithinRoot(resolvedPath, '/tmp')) {
|
||||||
@ -93,7 +90,7 @@ async function handleServeFile(url: URL): Promise<Response> {
|
|||||||
return json({ error: 'Missing path parameter' }, 400)
|
return json({ error: 'Missing path parameter' }, 400)
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvedPath = path.resolve(filePath)
|
const resolvedPath = path.resolve(normalizeDriveRootPathForPlatform(filePath))
|
||||||
|
|
||||||
if (!isAllowedFilesystemPath(resolvedPath)) {
|
if (!isAllowedFilesystemPath(resolvedPath)) {
|
||||||
return json({ error: 'Access denied: path outside allowed directory' }, 403)
|
return json({ error: 'Access denied: path outside allowed directory' }, 403)
|
||||||
@ -132,7 +129,7 @@ async function handleServeFile(url: URL): Promise<Response> {
|
|||||||
|
|
||||||
async function handleBrowse(url: URL): Promise<Response> {
|
async function handleBrowse(url: URL): Promise<Response> {
|
||||||
const targetPath = url.searchParams.get('path') || os.homedir() || '/'
|
const targetPath = url.searchParams.get('path') || os.homedir() || '/'
|
||||||
const resolvedPath = path.resolve(targetPath)
|
const resolvedPath = path.resolve(normalizeDriveRootPathForPlatform(targetPath))
|
||||||
|
|
||||||
if (!isAllowedFilesystemPath(resolvedPath)) {
|
if (!isAllowedFilesystemPath(resolvedPath)) {
|
||||||
return json({ error: 'Access denied: path outside allowed directory' }, 403)
|
return json({ error: 'Access denied: path outside allowed directory' }, 403)
|
||||||
|
|||||||
@ -1,21 +1,18 @@
|
|||||||
import * as path from 'node:path'
|
import * as path from 'node:path'
|
||||||
|
import {
|
||||||
|
isSameOrInsidePathForPlatform,
|
||||||
|
normalizeDriveRootPathForPlatform,
|
||||||
|
} from './windowsDrivePath.js'
|
||||||
|
|
||||||
const registeredRoots = new Set<string>()
|
const registeredRoots = new Set<string>()
|
||||||
|
|
||||||
function normalizeComparablePath(filePath: string): string {
|
|
||||||
const resolved = path.resolve(filePath)
|
|
||||||
return process.platform === 'win32' ? resolved.toLowerCase() : resolved
|
|
||||||
}
|
|
||||||
|
|
||||||
function isWithinRoot(targetPath: string, rootPath: string): boolean {
|
function isWithinRoot(targetPath: string, rootPath: string): boolean {
|
||||||
const target = normalizeComparablePath(targetPath)
|
return isSameOrInsidePathForPlatform(targetPath, rootPath)
|
||||||
const root = normalizeComparablePath(rootPath)
|
|
||||||
return target === root || target.startsWith(`${root}${path.sep}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerFilesystemAccessRoot(rootPath: string | null | undefined): void {
|
export function registerFilesystemAccessRoot(rootPath: string | null | undefined): void {
|
||||||
if (!rootPath) return
|
if (!rootPath) return
|
||||||
registeredRoots.add(path.resolve(rootPath))
|
registeredRoots.add(path.resolve(normalizeDriveRootPathForPlatform(rootPath)))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isWithinRegisteredFilesystemRoot(targetPath: string): boolean {
|
export function isWithinRegisteredFilesystemRoot(targetPath: string): boolean {
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { promisify } from 'node:util'
|
|||||||
import { ApiError } from '../middleware/errorHandler.js'
|
import { ApiError } from '../middleware/errorHandler.js'
|
||||||
import { findCanonicalGitRoot, findGitRoot } from '../../utils/git.js'
|
import { findCanonicalGitRoot, findGitRoot } from '../../utils/git.js'
|
||||||
import { registerFilesystemAccessRoot } from './filesystemAccessRoots.js'
|
import { registerFilesystemAccessRoot } from './filesystemAccessRoots.js'
|
||||||
|
import { normalizeDriveRootPathForPlatform } from './windowsDrivePath.js'
|
||||||
import {
|
import {
|
||||||
ensureWorktreesDirExcluded,
|
ensureWorktreesDirExcluded,
|
||||||
performPostCreationSetup,
|
performPostCreationSetup,
|
||||||
@ -168,7 +169,7 @@ async function runGit(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function resolveDirectory(workDir: string): Promise<string> {
|
async function resolveDirectory(workDir: string): Promise<string> {
|
||||||
const resolved = path.resolve(workDir)
|
const resolved = path.resolve(normalizeDriveRootPathForPlatform(workDir))
|
||||||
let realPath: string
|
let realPath: string
|
||||||
try {
|
try {
|
||||||
realPath = await fs.realpath(resolved)
|
realPath = await fs.realpath(resolved)
|
||||||
@ -332,7 +333,7 @@ export async function getRepositoryContext(workDir: string): Promise<RepositoryC
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
state: 'missing_workdir',
|
state: 'missing_workdir',
|
||||||
workDir: path.resolve(workDir),
|
workDir: path.resolve(normalizeDriveRootPathForPlatform(workDir)),
|
||||||
repoRoot: null,
|
repoRoot: null,
|
||||||
repoName: null,
|
repoName: null,
|
||||||
currentBranch: null,
|
currentBranch: null,
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import {
|
|||||||
type PreparedSessionWorkspace,
|
type PreparedSessionWorkspace,
|
||||||
} from './repositoryLaunchService.js'
|
} from './repositoryLaunchService.js'
|
||||||
import { registerFilesystemAccessRoot } from './filesystemAccessRoots.js'
|
import { registerFilesystemAccessRoot } from './filesystemAccessRoots.js'
|
||||||
|
import { normalizeDriveRootPathForPlatform } from './windowsDrivePath.js'
|
||||||
import { cleanSessionTitleSource } from '../../utils/sessionTitleText.js'
|
import { cleanSessionTitleSource } from '../../utils/sessionTitleText.js'
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -294,14 +295,14 @@ export class SessionService {
|
|||||||
for (let i = entries.length - 1; i >= 0; i--) {
|
for (let i = entries.length - 1; i >= 0; i--) {
|
||||||
const entry = entries[i]
|
const entry = entries[i]
|
||||||
if (entry.type === 'session-meta' && typeof (entry as Record<string, unknown>).workDir === 'string') {
|
if (entry.type === 'session-meta' && typeof (entry as Record<string, unknown>).workDir === 'string') {
|
||||||
return (entry as Record<string, unknown>).workDir as string
|
return normalizeDriveRootPathForPlatform((entry as Record<string, unknown>).workDir as string)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = entries.length - 1; i >= 0; i--) {
|
for (let i = entries.length - 1; i >= 0; i--) {
|
||||||
const cwd = entries[i]?.cwd
|
const cwd = entries[i]?.cwd
|
||||||
if (typeof cwd === 'string' && cwd.trim()) {
|
if (typeof cwd === 'string' && cwd.trim()) {
|
||||||
return cwd
|
return normalizeDriveRootPathForPlatform(cwd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -915,7 +916,7 @@ export class SessionService {
|
|||||||
|
|
||||||
// Optionally filter to a specific project
|
// Optionally filter to a specific project
|
||||||
if (projectFilter) {
|
if (projectFilter) {
|
||||||
const sanitized = this.sanitizePath(projectFilter)
|
const sanitized = this.sanitizePath(normalizeDriveRootPathForPlatform(projectFilter))
|
||||||
projectDirs = projectDirs.filter((d) => d === sanitized)
|
projectDirs = projectDirs.filter((d) => d === sanitized)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -966,6 +967,11 @@ export class SessionService {
|
|||||||
return `${windowsDrivePath[1]}:${path.win32.sep}${windowsDrivePath[2].replace(/-/g, path.win32.sep)}`
|
return `${windowsDrivePath[1]}:${path.win32.sep}${windowsDrivePath[2].replace(/-/g, path.win32.sep)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const windowsDriveRoot = sanitized.match(/^([a-zA-Z])--$/)
|
||||||
|
if (windowsDriveRoot) {
|
||||||
|
return `${windowsDriveRoot[1]}:${path.win32.sep}`
|
||||||
|
}
|
||||||
|
|
||||||
// On POSIX the original path starts with '/', so the sanitized form starts with '-'.
|
// On POSIX the original path starts with '/', so the sanitized form starts with '-'.
|
||||||
// UNC-style Windows paths also recover to a leading double separator on Windows.
|
// UNC-style Windows paths also recover to a leading double separator on Windows.
|
||||||
return sanitized.replace(/-/g, path.sep)
|
return sanitized.replace(/-/g, path.sep)
|
||||||
@ -1644,7 +1650,7 @@ export class SessionService {
|
|||||||
async clearSessionTranscript(sessionId: string, fallbackWorkDir?: string): Promise<void> {
|
async clearSessionTranscript(sessionId: string, fallbackWorkDir?: string): Promise<void> {
|
||||||
let found = await this.findSessionFile(sessionId)
|
let found = await this.findSessionFile(sessionId)
|
||||||
if (!found && fallbackWorkDir) {
|
if (!found && fallbackWorkDir) {
|
||||||
const resolvedPath = path.resolve(fallbackWorkDir)
|
const resolvedPath = path.resolve(normalizeDriveRootPathForPlatform(fallbackWorkDir))
|
||||||
const absWorkDir = await fs.realpath(resolvedPath).catch(() => resolvedPath)
|
const absWorkDir = await fs.realpath(resolvedPath).catch(() => resolvedPath)
|
||||||
const dirPath = path.join(this.getProjectsDir(), this.sanitizePath(absWorkDir))
|
const dirPath = path.join(this.getProjectsDir(), this.sanitizePath(absWorkDir))
|
||||||
await fs.mkdir(dirPath, { recursive: true })
|
await fs.mkdir(dirPath, { recursive: true })
|
||||||
@ -1710,14 +1716,15 @@ export class SessionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetProjectDir = this.sanitizePath(metadata.workDir)
|
const normalizedWorkDir = normalizeDriveRootPathForPlatform(metadata.workDir)
|
||||||
|
const targetProjectDir = this.sanitizePath(normalizedWorkDir)
|
||||||
const targetFilePath = path.join(this.getProjectsDir(), targetProjectDir, `${sessionId}.jsonl`)
|
const targetFilePath = path.join(this.getProjectsDir(), targetProjectDir, `${sessionId}.jsonl`)
|
||||||
await fs.mkdir(path.dirname(targetFilePath), { recursive: true })
|
await fs.mkdir(path.dirname(targetFilePath), { recursive: true })
|
||||||
|
|
||||||
await this.appendJsonlEntry(targetFilePath, {
|
await this.appendJsonlEntry(targetFilePath, {
|
||||||
type: 'session-meta',
|
type: 'session-meta',
|
||||||
isMeta: true,
|
isMeta: true,
|
||||||
workDir: metadata.workDir,
|
workDir: normalizedWorkDir,
|
||||||
repository,
|
repository,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
})
|
})
|
||||||
@ -1746,7 +1753,7 @@ export class SessionService {
|
|||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
|
|
||||||
const keepProjectDir = this.sanitizePath(keepWorkDir)
|
const keepProjectDir = this.sanitizePath(normalizeDriveRootPathForPlatform(keepWorkDir))
|
||||||
let removed = 0
|
let removed = 0
|
||||||
for (const projectDir of projectDirs) {
|
for (const projectDir of projectDirs) {
|
||||||
if (!projectDir.isDirectory()) continue
|
if (!projectDir.isDirectory()) continue
|
||||||
|
|||||||
30
src/server/services/windowsDrivePath.ts
Normal file
30
src/server/services/windowsDrivePath.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import * as path from 'node:path'
|
||||||
|
|
||||||
|
export function normalizeDriveRootPathForPlatform(
|
||||||
|
filePath: string,
|
||||||
|
platform: NodeJS.Platform = process.platform,
|
||||||
|
): string {
|
||||||
|
if (platform !== 'win32') return filePath
|
||||||
|
|
||||||
|
const driveRootMatch = filePath.match(/^([a-zA-Z]):$/)
|
||||||
|
if (!driveRootMatch) return filePath
|
||||||
|
|
||||||
|
return `${driveRootMatch[1]}:\\`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSameOrInsidePathForPlatform(
|
||||||
|
targetPath: string,
|
||||||
|
rootPath: string,
|
||||||
|
platform: NodeJS.Platform = process.platform,
|
||||||
|
): boolean {
|
||||||
|
const pathApi = platform === 'win32' ? path.win32 : path
|
||||||
|
const normalize = (filePath: string) => {
|
||||||
|
const resolved = pathApi.resolve(normalizeDriveRootPathForPlatform(filePath, platform))
|
||||||
|
return platform === 'win32' ? resolved.toLowerCase() : resolved
|
||||||
|
}
|
||||||
|
const target = normalize(targetPath)
|
||||||
|
const root = normalize(rootPath)
|
||||||
|
const relative = pathApi.relative(root, target)
|
||||||
|
|
||||||
|
return relative === '' || (!!relative && !relative.startsWith('..') && !pathApi.isAbsolute(relative))
|
||||||
|
}
|
||||||
@ -6,6 +6,10 @@ import { diffLines } from 'diff'
|
|||||||
import type { MessageEntry } from './sessionService.js'
|
import type { MessageEntry } from './sessionService.js'
|
||||||
import type { FileHistorySnapshot } from '../../utils/fileHistory.js'
|
import type { FileHistorySnapshot } from '../../utils/fileHistory.js'
|
||||||
import { getClaudeConfigHomeDir } from '../../utils/envUtils.js'
|
import { getClaudeConfigHomeDir } from '../../utils/envUtils.js'
|
||||||
|
import {
|
||||||
|
isSameOrInsidePathForPlatform,
|
||||||
|
normalizeDriveRootPathForPlatform,
|
||||||
|
} from './windowsDrivePath.js'
|
||||||
|
|
||||||
const MAX_PREVIEW_BYTES = 1024 * 1024
|
const MAX_PREVIEW_BYTES = 1024 * 1024
|
||||||
const MAX_UNTRACKED_STAT_BYTES = 256 * 1024
|
const MAX_UNTRACKED_STAT_BYTES = 256 * 1024
|
||||||
@ -1004,7 +1008,7 @@ export class WorkspaceService {
|
|||||||
if (!workDir) {
|
if (!workDir) {
|
||||||
throw new Error(`Session not found: ${sessionId}`)
|
throw new Error(`Session not found: ${sessionId}`)
|
||||||
}
|
}
|
||||||
return path.resolve(workDir)
|
return path.resolve(normalizeDriveRootPathForPlatform(workDir))
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getWorkspaceRoot(
|
private async getWorkspaceRoot(
|
||||||
@ -1149,14 +1153,7 @@ export class WorkspaceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private isWithinRoot(targetPath: string, rootPath: string): boolean {
|
private isWithinRoot(targetPath: string, rootPath: string): boolean {
|
||||||
const target = this.normalizeComparableAbsolutePath(targetPath)
|
return isSameOrInsidePathForPlatform(targetPath, rootPath)
|
||||||
const root = this.normalizeComparableAbsolutePath(rootPath)
|
|
||||||
return target === root || target.startsWith(`${root}${path.sep}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
private normalizeComparableAbsolutePath(filePath: string): string {
|
|
||||||
const resolved = path.resolve(filePath)
|
|
||||||
return process.platform === 'win32' ? resolved.toLowerCase() : resolved
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private normalizeRelativePath(filePath: string): string {
|
private normalizeRelativePath(filePath: string): string {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user