import { KeyValueCache , SortedKeyValueArray } from "../collections" ; import { LocaleStringComparer } from "../comparers" ; import { errors } from "../errors" ; import { ArrayUtils } from "../utils" ; import { FileSystemHost } from "./FileSystemHost" ; import { FileUtils } from "./FileUtils" ; import { StandardizedFilePath } from "./StandardizedFilePath" ; type Operation = DeleteDirectoryOperation | DeleteFileOperation | MoveDirectoryOperation | MakeDirectoryOperation | CopyDirectoryOperation ; type MoveOrCopyOperation = MoveDirectoryOperation | CopyDirectoryOperation ; type MoveCopyOrDeleteOperation = MoveOrCopyOperation | DeleteDirectoryOperation ; interface OperationBase < T > { kind : T ; index : number ; } interface DeleteFileOperation extends OperationBase < "deleteFile" > { filePath : StandardizedFilePath ; } interface MakeDirectoryOperation extends OperationBase < "mkdir" > { dir : Directory ; } interface DeleteDirectoryOperation extends OperationBase < "deleteDir" > { dir : Directory ; } interface MoveDirectoryOperation extends OperationBase < "move" > { oldDir : Directory ; newDir : Directory ; } interface CopyDirectoryOperation extends OperationBase < "copy" > { oldDir : Directory ; newDir : Directory ; } class Directory { readonly operations : Operation [ ] = [ ] ; readonly inboundOperations : ( MoveDirectoryOperation | CopyDirectoryOperation ) [ ] = [ ] ; private isDeleted = false ; private wasEverDeleted = false ; private parent : Directory | undefined ; private readonly childDirs = new SortedKeyValueArray < StandardizedFilePath , Directory > ( item => item . path , LocaleStringComparer . instance ) ; constructor ( public readonly path ) { } getExternalOperations ( ) { return [ ... ArrayUtils . flatten ( this . getAncestors ( ) . map ( a => getMoveCopyOrDeleteOperations ( a ) ) ) . filter ( o => isAncestorAffectedOperation ( this , o ) ) , ... ArrayUtils . flatten ( [ this , ... this . getDescendants ( ) ] . map ( d => getMoveOrCopyOperations ( d ) ) ) . filter ( o => ! isInternalOperation ( this , o ) ) , ] ; function isInternalOperation ( thisDir , operation ) { return operation . oldDir . isDescendantOrEqual ( thisDir ) && operation . newDir . isDescendantOrEqual ( thisDir ) ; } function isAncestorAffectedOperation ( thisDir , operation ) { switch ( operation . kind ) { case "move" : case "copy" : return thisDir . isDescendantOrEqual ( operation . oldDir ) || thisDir . isDescendantOrEqual ( operation . newDir ) ; case "deleteDir" : return thisDir . isDescendantOrEqual ( operation . dir ) ; default : return errors . throwNotImplementedForNeverValueError ( operation ) ; } } function getMoveOrCopyOperations ( dir ) { return dir . operations . filter ( o => o . kind === "move" || o . kind === "copy" ) as MoveOrCopyOperation [ ] ; } function getMoveCopyOrDeleteOperations ( dir ) { return dir . operations . filter ( o => o . kind === "move" || o . kind === "deleteDir" || o . kind === "copy" ) as MoveCopyOrDeleteOperation [ ] ; } } isDescendantOrEqual ( directory ) { return this . isDescendant ( directory ) || this === directory ; } isDescendant ( directory ) { return FileUtils . pathStartsWith ( this . path , directory . path ) ; } getIsDeleted ( ) { return this . isDeleted ; } getWasEverDeleted ( ) { if ( this . wasEverDeleted ) return true ; for ( const ancestor of this . getAncestorsIterator ( ) ) { if ( ancestor . wasEverDeleted ) return true ; } return false ; } setIsDeleted ( isDeleted ) { if ( this . isDeleted === isDeleted ) return ; if ( isDeleted ) { this . wasEverDeleted = true ; for ( const child of this . childDirs . entries ( ) ) child . setIsDeleted ( true ) ; } else { if ( this . parent != null ) this . parent . setIsDeleted ( false ) ; } this . isDeleted = isDeleted ; } getParent ( ) { return this . parent ; } setParent ( parent ) { if ( this . parent != null ) throw new errors . InvalidOperationError ( "For some reason, a parent was being set when the directory already had a parent. Please open an issue." ) ; this . parent = parent ; parent . childDirs . set ( this ) ; if ( parent . isDeleted && ! this . isDeleted ) parent . setIsDeleted ( false ) ; } removeParent ( ) { const parent = this . parent ; if ( parent == null ) return ; parent . childDirs . removeByValue ( this ) ; this . parent = undefined ; } getAncestors ( ) { return Array . from ( this . getAncestorsIterator ( ) ) ; } * getAncestorsIterator ( ) { let parent = this . parent ; while ( parent != null ) { yield parent ; parent = parent . parent ; } } * getChildrenEntriesIterator ( ) { for ( const childDir of this . childDirs . entries ( ) ) { yield { path : childDir . path , isDirectory : true , isFile : false , isSymlink : false , } ; } } getDescendants ( ) { const descendants = [ ] ; for ( const child of this . childDirs . entries ( ) ) { descendants . push ( child ) ; descendants . push ( ... child . getDescendants ( ) ) ; } return descendants ; } isFileQueuedForDelete ( filePath ) { return this . hasOperation ( operation => operation . kind === "deleteFile" && operation . filePath === filePath ) ; } private hasOperation ( operationMatches ) { for ( const operation of this . operations ) { if ( operationMatches ( operation ) ) return true ; } return false ; } dequeueFileDelete ( filePath ) { this . removeMatchingOperations ( operation => operation . kind === "deleteFile" && operation . filePath === filePath ) ; } dequeueDirDelete ( dirPath ) { this . removeMatchingOperations ( operation => operation . kind === "deleteDir" && operation . dir . path === dirPath ) ; } isRootDir ( ) { return FileUtils . isRootDirPath ( this . path ) ; } private removeMatchingOperations ( operationMatches ) { ArrayUtils . removeAll ( this . operations , operationMatches ) ; } } export interface DirEntry { path : StandardizedFilePath ; isFile : boolean ; isDirectory : boolean ; isSymlink : boolean ; } /**
 * FileSystemHost wrapper that allows transactionally queuing operations to the file system.
 */ export class TransactionalFileSystem { private readonly directories = new KeyValueCache < StandardizedFilePath , Directory > ( ) ; private readonly pathCasingMaintainer : PathCasingMaintainer ; param fileSystem constructor ( private readonly fileSystem ) { this . pathCasingMaintainer = new PathCasingMaintainer ( fileSystem ) ; } queueFileDelete ( filePath ) { const parentDir = this . getOrCreateParentDirectory ( filePath ) ; parentDir . operations . push ( { kind : "deleteFile" , index : this . getNextOperationIndex ( ) , filePath , } ) ; this . pathCasingMaintainer . removePath ( filePath ) ; } removeFileDelete ( filePath ) { this . getOrCreateParentDirectory ( filePath ) . dequeueFileDelete ( filePath ) ; } queueMkdir ( dirPath ) { const dir = this . getOrCreateDirectory ( dirPath ) ; dir . setIsDeleted ( false ) ; const parentDir = this . getOrCreateParentDirectory ( dirPath ) ; parentDir . operations . push ( { kind : "mkdir" , index : this . getNextOperationIndex ( ) , dir , } ) ; } queueDirectoryDelete ( dirPath ) { const dir = this . getOrCreateDirectory ( dirPath ) ; dir . setIsDeleted ( true ) ; const parentDir = this . getOrCreateParentDirectory ( dirPath ) ; parentDir . operations . push ( { kind : "deleteDir" , index : this . getNextOperationIndex ( ) , dir , } ) ; this . pathCasingMaintainer . removePath ( dirPath ) ; } queueMoveDirectory ( srcPath , destPath ) { const parentDir = this . getOrCreateParentDirectory ( srcPath ) ; const moveDir = this . getOrCreateDirectory ( srcPath ) ; const destinationDir = this . getOrCreateDirectory ( destPath ) ; const moveOperation = { kind : "move" , index : this . getNextOperationIndex ( ) , oldDir : moveDir , newDir : destinationDir , } ; parentDir . operations . push ( moveOperation ) ; ( destinationDir . getParent ( ) || destinationDir ) . inboundOperations . push ( moveOperation ) ; moveDir . setIsDeleted ( true ) ; this . pathCasingMaintainer . removePath ( srcPath ) ; } queueCopyDirectory ( srcPath , destPath ) { const parentDir = this . getOrCreateParentDirectory ( srcPath ) ; const copyDir = this . getOrCreateDirectory ( srcPath ) ; const destinationDir = this . getOrCreateDirectory ( destPath ) ; const copyOperation = { kind : "copy" , index : this . getNextOperationIndex ( ) , oldDir : copyDir , newDir : destinationDir , } ; parentDir . operations . push ( copyOperation ) ; ( destinationDir . getParent ( ) || destinationDir ) . inboundOperations . push ( copyOperation ) ; } async flush ( ) { const operations = this . getAndClearOperations ( ) ; for ( const operation of operations ) await this . executeOperation ( operation ) ; } flushSync ( ) { for ( const operation of this . getAndClearOperations ( ) ) this . executeOperationSync ( operation ) ; } async saveForDirectory ( dirPath ) { const dir = this . getOrCreateDirectory ( dirPath ) ; this . throwIfHasExternalOperations ( dir , "save directory" ) ; const operations = this . getAndClearOperationsForDir ( dir ) ; await this . ensureDirectoryExists ( dir ) ; for ( const operation of operations ) await this . executeOperation ( operation ) ; } saveForDirectorySync ( dirPath ) { const dir = this . getOrCreateDirectory ( dirPath ) ; this . throwIfHasExternalOperations ( dir , "save directory" ) ; this . ensureDirectoryExistsSync ( dir ) ; for ( const operation of this . getAndClearOperationsForDir ( dir ) ) this . executeOperationSync ( operation ) ; } private getAndClearOperationsForDir ( dir ) { const operations = getAndClearParentMkDirOperations ( dir . getParent ( ) , dir ) ; for ( const currentDir of [ dir , ... dir . getDescendants ( ) ] ) operations . push ( ... currentDir . operations ) ; ArrayUtils . sortByProperty ( operations , item => item . index ) ; this . removeDirAndSubDirs ( dir ) ; return operations ; function getAndClearParentMkDirOperations ( parentDir , childDir ) { if ( parentDir == null ) return [ ] ; const parentOperations = ArrayUtils . removeAll ( parentDir . operations , operation => operation . kind === "mkdir" && operation . dir === childDir ) ; return [ ... parentOperations , ... getAndClearParentMkDirOperations ( parentDir . getParent ( ) , parentDir ) ] ; } } private async executeOperation ( operation ) { switch ( operation . kind ) { case "deleteDir" : await this . deleteSuppressNotFound ( operation . dir . path ) ; break ; case "deleteFile" : await this . deleteSuppressNotFound ( operation . filePath ) ; break ; case "move" : await this . fileSystem . move ( operation . oldDir . path , operation . newDir . path ) ; break ; case "copy" : await this . fileSystem . copy ( operation . oldDir . path , operation . newDir . path ) ; break ; case "mkdir" : await this . fileSystem . mkdir ( operation . dir . path ) ; break ; default : errors . throwNotImplementedForNeverValueError ( operation ) ; } } private executeOperationSync ( operation ) { switch ( operation . kind ) { case "deleteDir" : this . deleteSuppressNotFoundSync ( operation . dir . path ) ; break ; case "deleteFile" : this . deleteSuppressNotFoundSync ( operation . filePath ) ; break ; case "move" : this . fileSystem . moveSync ( operation . oldDir . path , operation . newDir . path ) ; break ; case "copy" : this . fileSystem . copySync ( operation . oldDir . path , operation . newDir . path ) ; break ; case "mkdir" : this . fileSystem . mkdirSync ( operation . dir . path ) ; break ; default : errors . throwNotImplementedForNeverValueError ( operation ) ; } } private getAndClearOperations ( ) { const operations = [ ] ; for ( const dir of this . directories . getValues ( ) ) operations . push ( ... dir . operations ) ; ArrayUtils . sortByProperty ( operations , item => item . index ) ; this . directories . clear ( ) ; return operations ; } async moveFileImmediately ( oldFilePath , newFilePath , fileText ) { this . throwIfHasExternalOperations ( this . getOrCreateParentDirectory ( oldFilePath ) , "move file" ) ; this . throwIfHasExternalOperations ( this . getOrCreateParentDirectory ( newFilePath ) , "move file" ) ; await this . writeFile ( newFilePath , fileText ) ; await this . deleteFileImmediately ( oldFilePath ) ; } moveFileImmediatelySync ( oldFilePath , newFilePath , fileText ) { this . throwIfHasExternalOperations ( this . getOrCreateParentDirectory ( oldFilePath ) , "move file" ) ; this . throwIfHasExternalOperations ( this . getOrCreateParentDirectory ( newFilePath ) , "move file" ) ; this . writeFileSync ( newFilePath , fileText ) ; this . deleteFileImmediatelySync ( oldFilePath ) ; } async deleteFileImmediately ( filePath ) { const dir = this . getOrCreateParentDirectory ( filePath ) ; this . throwIfHasExternalOperations ( dir , "delete file" ) ; dir . dequeueFileDelete ( filePath ) ; this . pathCasingMaintainer . removePath ( filePath ) ; try { await this . deleteSuppressNotFound ( filePath ) ; } catch ( err ) { this . queueFileDelete ( filePath ) ; throw err ; } } deleteFileImmediatelySync ( filePath ) { const dir = this . getOrCreateParentDirectory ( filePath ) ; this . throwIfHasExternalOperations ( dir , "delete file" ) ; dir . dequeueFileDelete ( filePath ) ; this . pathCasingMaintainer . removePath ( filePath ) ; try { this . deleteSuppressNotFoundSync ( filePath ) ; } catch ( err ) { this . queueFileDelete ( filePath ) ; throw err ; } } async copyDirectoryImmediately ( srcDirPath , destDirPath ) { const srcDir = this . getOrCreateDirectory ( srcDirPath ) ; const destDir = this . getOrCreateDirectory ( destDirPath ) ; this . throwIfHasExternalOperations ( srcDir , "copy directory" ) ; this . throwIfHasExternalOperations ( destDir , "copy directory" ) ; const saveTask = Promise . all ( [ this . saveForDirectory ( srcDirPath ) , this . saveForDirectory ( destDirPath ) ] ) ; this . removeDirAndSubDirs ( srcDir ) ; await saveTask ; await this . fileSystem . copy ( srcDirPath , destDirPath ) ; } copyDirectoryImmediatelySync ( srcDirPath , destDirPath ) { const srcDir = this . getOrCreateDirectory ( srcDirPath ) ; const destDir = this . getOrCreateDirectory ( destDirPath ) ; this . throwIfHasExternalOperations ( srcDir , "copy directory" ) ; this . throwIfHasExternalOperations ( destDir , "copy directory" ) ; this . saveForDirectorySync ( srcDirPath ) ; this . saveForDirectorySync ( destDirPath ) ; this . removeDirAndSubDirs ( srcDir ) ; this . fileSystem . copySync ( srcDirPath , destDirPath ) ; } async moveDirectoryImmediately ( srcDirPath , destDirPath ) { const srcDir = this . getOrCreateDirectory ( srcDirPath ) ; const destDir = this . getOrCreateDirectory ( destDirPath ) ; this . throwIfHasExternalOperations ( srcDir , "move directory" ) ; this . throwIfHasExternalOperations ( destDir , "move directory" ) ; const saveTask = Promise . all ( [ this . saveForDirectory ( srcDirPath ) , this . saveForDirectory ( destDirPath ) ] ) ; this . removeDirAndSubDirs ( srcDir ) ; this . pathCasingMaintainer . removePath ( srcDirPath ) ; await saveTask ; await this . fileSystem . move ( srcDirPath , destDirPath ) ; } moveDirectoryImmediatelySync ( srcDirPath , destDirPath ) { const srcDir = this . getOrCreateDirectory ( srcDirPath ) ; const destDir = this . getOrCreateDirectory ( destDirPath ) ; this . throwIfHasExternalOperations ( srcDir , "move directory" ) ; this . throwIfHasExternalOperations ( destDir , "move directory" ) ; this . saveForDirectorySync ( srcDirPath ) ; this . saveForDirectorySync ( destDirPath ) ; this . removeDirAndSubDirs ( srcDir ) ; this . pathCasingMaintainer . removePath ( srcDirPath ) ; this . fileSystem . moveSync ( srcDirPath , destDirPath ) ; } async deleteDirectoryImmediately ( dirPath ) { const dir = this . getOrCreateDirectory ( dirPath ) ; this . throwIfHasExternalOperations ( dir , "delete" ) ; this . removeDirAndSubDirs ( dir ) ; this . pathCasingMaintainer . removePath ( dirPath ) ; try { await this . deleteSuppressNotFound ( dirPath ) ; } catch ( err ) { this . addBackDirAndSubDirs ( dir ) ; this . queueDirectoryDelete ( dirPath ) ; } } /** Recreates a directory on the underlying file system asynchronously. */ async clearDirectoryImmediately ( dirPath ) { await this . deleteDirectoryImmediately ( dirPath ) ; this . getOrCreateDirectory ( dirPath ) . setIsDeleted ( false ) ; await this . fileSystem . mkdir ( dirPath ) ; } /** Recreates a directory on the underlying file system synchronously. */ clearDirectoryImmediatelySync ( dirPath ) { this . deleteDirectoryImmediatelySync ( dirPath ) ; this . getOrCreateDirectory ( dirPath ) . setIsDeleted ( false ) ; this . fileSystem . mkdirSync ( dirPath ) ; } deleteDirectoryImmediatelySync ( dirPath ) { const dir = this . getOrCreateDirectory ( dirPath ) ; this . throwIfHasExternalOperations ( dir , "delete" ) ; this . removeDirAndSubDirs ( dir ) ; this . pathCasingMaintainer . removePath ( dirPath ) ; try { this . deleteSuppressNotFoundSync ( dirPath ) ; } catch ( err ) { this . addBackDirAndSubDirs ( dir ) ; this . queueDirectoryDelete ( dirPath ) ; } } private async deleteSuppressNotFound ( path ) { try { await this . fileSystem . delete ( path ) ; } catch ( err ) { if ( ! FileUtils . isNotExistsError ( err ) ) throw err ; } } private deleteSuppressNotFoundSync ( path ) { try { this . fileSystem . deleteSync ( path ) ; } catch ( err ) { if ( ! FileUtils . isNotExistsError ( err ) ) throw err ; } } fileExists ( filePath ) { if ( this . _fileDeletedInMemory ( filePath ) ) return false ; return this . fileSystem . fileExists ( filePath ) ; } fileExistsSync ( filePath ) { if ( this . _fileDeletedInMemory ( filePath ) ) return false ; return this . fileSystem . fileExistsSync ( filePath ) ; } private _fileDeletedInMemory ( filePath ) { if ( this . isPathQueuedForDeletion ( filePath ) ) return true ; const parentDir = this . getParentDirectoryIfExists ( filePath ) ; if ( parentDir != null && parentDir . getWasEverDeleted ( ) ) return true ; return false ; } directoryExistsSync ( dirPath ) { if ( this . isPathQueuedForDeletion ( dirPath ) ) return false ; if ( this . isPathDirectoryInQueueThatExists ( dirPath ) ) return true ; const dir = this . getDirectoryIfExists ( dirPath ) ; if ( dir != null && dir . getWasEverDeleted ( ) ) return false ; return this . fileSystem . directoryExistsSync ( dirPath ) ; } readFileIfExistsSync ( filePath , encoding ) { if ( this . _fileDeletedInMemory ( filePath ) ) return undefined ; try { return this . readFileSync ( filePath , encoding ) ; } catch ( err ) { if ( err instanceof errors . FileNotFoundError ) return undefined ; else throw err ; } } readFileSync ( filePath , encoding ) { this . _verifyCanReadFile ( filePath ) ; return this . fileSystem . readFileSync ( filePath , encoding ) ; } readFileIfExists ( filePath , encoding ) { if ( this . _fileDeletedInMemory ( filePath ) ) return Promise . resolve ( undefined ) ; return this . readFile ( filePath , encoding ) . catch ( err => { if ( err instanceof errors . FileNotFoundError ) return Promise . resolve ( undefined ) ; else return Promise . reject ( err ) ; } ) ; } readFile ( filePath , encoding ) { this . _verifyCanReadFile ( filePath ) ; return this . fileSystem . readFile ( filePath , encoding ) ; } private _verifyCanReadFile ( filePath ) { if ( this . isPathQueuedForDeletion ( filePath ) ) throw new errors . InvalidOperationError ( `Cannot read file at ${ filePath } when it is queued for deletion.` ) ; if ( this . getOrCreateParentDirectory ( filePath ) . getWasEverDeleted ( ) ) throw new errors . InvalidOperationError ( `Cannot read file at ${ filePath } because one of its ancestor directories was once deleted or moved.` ) ; } readDirSync ( dirPath ) { const dir = this . getOrCreateDirectory ( dirPath ) ; if ( dir . getIsDeleted ( ) ) throw new errors . InvalidOperationError ( `Cannot read directory at ${ dirPath } when it is queued for deletion.` ) ; if ( dir . getWasEverDeleted ( ) ) throw new errors . InvalidOperationError ( `Cannot read directory at ${ dirPath } because one of its ancestor directories was once deleted or moved.` ) ; const uniqueDirPaths = new Map < StandardizedFilePath , DirEntry > ( ) ; for ( const entry of dir . getChildrenEntriesIterator ( ) ) uniqueDirPaths . set ( entry . path , entry ) ; for ( const runtimeDirEntry of this . fileSystem . readDirSync ( dirPath ) ) { const standardizedChildDirOrFilePath = this . getStandardizedAbsolutePath ( runtimeDirEntry . name ) ; if ( ! this . isPathQueuedForDeletion ( standardizedChildDirOrFilePath ) ) { uniqueDirPaths . set ( standardizedChildDirOrFilePath , { path : standardizedChildDirOrFilePath , isDirectory : runtimeDirEntry . isDirectory , isFile : runtimeDirEntry . isFile , isSymlink : runtimeDirEntry . isSymlink , } ) ; } } return ArrayUtils . sortByProperty ( Array . from ( uniqueDirPaths . values ( ) ) , e => e . path ) ; } async * glob ( patterns ) { const filePaths = await this . fileSystem . glob ( patterns ) ; for ( const filePath of filePaths ) { const standardizedFilePath = this . getStandardizedAbsolutePath ( filePath ) ; if ( ! this . isPathQueuedForDeletion ( standardizedFilePath ) ) yield standardizedFilePath ; } } * globSync ( patterns ) { const filePaths = this . fileSystem . globSync ( patterns ) ; for ( const filePath of filePaths ) { const standardizedFilePath = this . getStandardizedAbsolutePath ( filePath ) ; if ( ! this . isPathQueuedForDeletion ( standardizedFilePath ) ) yield standardizedFilePath ; } } getFileSystem ( ) { return this . fileSystem ; } getCurrentDirectory ( ) { return this . getStandardizedAbsolutePath ( this . fileSystem . getCurrentDirectory ( ) ) ; } getDirectories ( dirPath ) { return this . readDirSync ( dirPath ) . filter ( entry => entry . isDirectory ) . map ( d => d . path ) ; } realpathSync ( path ) { try { return this . getStandardizedAbsolutePath ( this . fileSystem . realpathSync ( path ) ) ; } catch { return path ; } } getStandardizedAbsolutePath ( fileOrDirPath , relativeBase ? ) { const standardizedFileOrDirPath = FileUtils . getStandardizedAbsolutePath ( this . fileSystem , fileOrDirPath , relativeBase ) ; return this . pathCasingMaintainer . getPath ( standardizedFileOrDirPath ) ; } readFileOrNotExists ( filePath , encoding ) { if ( this . isPathQueuedForDeletion ( filePath ) ) return false ; return FileUtils . readFileOrNotExists ( this . fileSystem , filePath , encoding ) ; } readFileOrNotExistsSync ( filePath , encoding ) { if ( this . isPathQueuedForDeletion ( filePath ) ) return false ; return FileUtils . readFileOrNotExistsSync ( this . fileSystem , filePath , encoding ) ; } async writeFile ( filePath , fileText ) { const parentDir = this . getOrCreateParentDirectory ( filePath ) ; this . throwIfHasExternalOperations ( parentDir , "write file" ) ; parentDir . dequeueFileDelete ( filePath ) ; await this . ensureDirectoryExists ( parentDir ) ; await this . fileSystem . writeFile ( filePath , fileText ) ; } writeFileSync ( filePath , fileText ) { const parentDir = this . getOrCreateParentDirectory ( filePath ) ; this . throwIfHasExternalOperations ( parentDir , "write file" ) ; parentDir . dequeueFileDelete ( filePath ) ; this . ensureDirectoryExistsSync ( parentDir ) ; this . fileSystem . writeFileSync ( filePath , fileText ) ; } private isPathDirectoryInQueueThatExists ( path ) { const pathDir = this . getDirectoryIfExists ( path ) ; return pathDir == null ? false : ! pathDir . getIsDeleted ( ) ; } private isPathQueuedForDeletion ( path ) { const pathDir = this . getDirectoryIfExists ( path ) ; if ( pathDir != null ) return pathDir . getIsDeleted ( ) ; const parentDir = this . getParentDirectoryIfExists ( path ) ; if ( parentDir == null ) return false ; return parentDir . isFileQueuedForDelete ( path ) || parentDir . getIsDeleted ( ) ; } private removeDirAndSubDirs ( dir ) { const originalParent = dir . getParent ( ) ; dir . removeParent ( ) ; for ( const dirToRemove of [ dir , ... dir . getDescendants ( ) ] ) this . directories . removeByKey ( dirToRemove . path ) ; if ( originalParent != null ) originalParent . dequeueDirDelete ( dir . path ) ; } private addBackDirAndSubDirs ( dir ) { for ( const dirToAdd of [ dir , ... dir . getDescendants ( ) ] ) this . directories . set ( dirToAdd . path , dirToAdd ) ; if ( ! dir . isRootDir ( ) ) dir . setParent ( this . getOrCreateParentDirectory ( dir . path ) ) ; } private operationIndex = 0 ; private getNextOperationIndex ( ) { return this . operationIndex ++ ; } private getParentDirectoryIfExists ( filePath ) { return this . getDirectoryIfExists ( FileUtils . getDirPath ( filePath ) ) ; } private getOrCreateParentDirectory ( filePath ) { return this . getOrCreateDirectory ( FileUtils . getDirPath ( filePath ) ) ; } private getDirectoryIfExists ( dirPath ) { return this . directories . get ( dirPath ) ; } private getOrCreateDirectory ( dirPath ) { let dir = this . directories . get ( dirPath ) ; if ( dir != null ) return dir ; const getOrCreateDir = ( creatingDirPath ) => this . directories . getOrCreate ( creatingDirPath , ( ) => new Directory ( creatingDirPath ) ) ; dir = getOrCreateDir ( dirPath ) ; let currentDirPath = dirPath ; let currentDir = dir ; while ( ! FileUtils . isRootDirPath ( currentDirPath ) ) { const nextDirPath = FileUtils . getDirPath ( currentDirPath ) ; const hadNextDir = this . directories . has ( nextDirPath ) ; const nextDir = getOrCreateDir ( nextDirPath ) ; currentDir . setParent ( nextDir ) ; if ( hadNextDir ) return dir ; currentDir = nextDir ; currentDirPath = nextDirPath ; } return dir ; } private throwIfHasExternalOperations ( dir , commandName ) { const operations = dir . getExternalOperations ( ) ; if ( operations . length === 0 ) return ; throw new errors . InvalidOperationError ( getErrorText ( ) ) ; function getErrorText ( ) { let hasCopy = false ; let errorText = `Cannot execute immediate operation '${ commandName }' because of the following external operations:\n` ; for ( const operation of operations ) { if ( operation . kind === "move" ) errorText += `\n* Move: ${ operation . oldDir . path } --> ${ operation . newDir . path }` ; else if ( operation . kind === "copy" ) { errorText += `\n* Copy: ${ operation . oldDir . path } --> ${ operation . newDir . path }` ; hasCopy = true ; } else if ( operation . kind === "deleteDir" ) errorText += `\n* Delete: ${ operation . dir . path }` ; else { const expectNever = operation ; errorText += `\n* Unknown operation: Please report this as a bug.` ; } } if ( hasCopy ) errorText += "\n\nNote: Copy operations can be removed from external operations by setting `includeUntrackedFiles` to `false` when copying." ; return errorText ; } } private async ensureDirectoryExists ( dir ) { if ( dir . isRootDir ( ) ) return ; this . removeMkDirOperationsForDir ( dir ) ; await this . fileSystem . mkdir ( dir . path ) ; } private ensureDirectoryExistsSync ( dir ) { if ( dir . isRootDir ( ) ) return ; this . removeMkDirOperationsForDir ( dir ) ; this . fileSystem . mkdirSync ( dir . path ) ; } private removeMkDirOperationsForDir ( dir ) { const parentDir = dir . getParent ( ) ; if ( parentDir != null ) { ArrayUtils . removeAll ( parentDir . operations , operation => operation . kind === "mkdir" && operation . dir === dir ) ; this . removeMkDirOperationsForDir ( parentDir ) ; } } } /** Maintains the file or dir path casing by using the first file path found for case insensistive file systems. */ class PathCasingMaintainer { private readonly caseInsensitiveMappings : Map < StandardizedFilePath , StandardizedFilePath > | undefined ; constructor ( fileSystem ) { if ( fileSystem . isCaseSensitive != null && ! fileSystem . isCaseSensitive ( ) ) this . caseInsensitiveMappings = new Map ( ) ; } getPath ( fileOrDirPath ) { if ( this . caseInsensitiveMappings == null ) return fileOrDirPath ; const key = fileOrDirPath . toLowerCase ( ) as StandardizedFilePath ; let path = this . caseInsensitiveMappings . get ( key ) ; if ( path == null ) { path = fileOrDirPath ; this . caseInsensitiveMappings . set ( key , path ) ; } return path ; } removePath ( dirPath ) { if ( this . caseInsensitiveMappings == null ) return ; this . caseInsensitiveMappings . delete ( dirPath . toLowerCase ( ) as StandardizedFilePath ) ; } }