174 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			174 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
|  | /** | ||
|  |  * @license | ||
|  |  * Copyright Google Inc. All Rights Reserved. | ||
|  |  * | ||
|  |  * Use of this source code is governed by an MIT-style license that can be | ||
|  |  * found in the LICENSE file at https://angular.io/license
 | ||
|  |  */ | ||
|  | import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path'; | ||
|  | import {FileStats, FileSystem} from '../../src/file_system/file_system'; | ||
|  | 
 | ||
|  | /** | ||
|  |  * An in-memory file system that can be used in unit tests. | ||
|  |  */ | ||
|  | export class MockFileSystem implements FileSystem { | ||
|  |   files: Folder = {}; | ||
|  |   constructor(...folders: Folder[]) { | ||
|  |     folders.forEach(files => this.processFiles(this.files, files)); | ||
|  |   } | ||
|  | 
 | ||
|  |   exists(path: AbsoluteFsPath): boolean { return this.findFromPath(path) !== null; } | ||
|  | 
 | ||
|  |   readFile(path: AbsoluteFsPath): string { | ||
|  |     const file = this.findFromPath(path); | ||
|  |     if (isFile(file)) { | ||
|  |       return file; | ||
|  |     } else { | ||
|  |       throw new MockFileSystemError('ENOENT', path, `File "${path}" does not exist.`); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   writeFile(path: AbsoluteFsPath, data: string): void { | ||
|  |     const [folderPath, basename] = this.splitIntoFolderAndFile(path); | ||
|  |     const folder = this.findFromPath(folderPath); | ||
|  |     if (!isFolder(folder)) { | ||
|  |       throw new MockFileSystemError( | ||
|  |           'ENOENT', path, `Unable to write file "${path}". The containing folder does not exist.`); | ||
|  |     } | ||
|  |     folder[basename] = data; | ||
|  |   } | ||
|  | 
 | ||
|  |   readdir(path: AbsoluteFsPath): PathSegment[] { | ||
|  |     const folder = this.findFromPath(path); | ||
|  |     if (folder === null) { | ||
|  |       throw new MockFileSystemError( | ||
|  |           'ENOENT', path, `Unable to read directory "${path}". It does not exist.`); | ||
|  |     } | ||
|  |     if (isFile(folder)) { | ||
|  |       throw new MockFileSystemError( | ||
|  |           'ENOTDIR', path, `Unable to read directory "${path}". It is a file.`); | ||
|  |     } | ||
|  |     return Object.keys(folder) as PathSegment[]; | ||
|  |   } | ||
|  | 
 | ||
|  |   lstat(path: AbsoluteFsPath): FileStats { | ||
|  |     const fileOrFolder = this.findFromPath(path); | ||
|  |     if (fileOrFolder === null) { | ||
|  |       throw new MockFileSystemError('ENOENT', path, `File "${path}" does not exist.`); | ||
|  |     } | ||
|  |     return new MockFileStats(fileOrFolder); | ||
|  |   } | ||
|  | 
 | ||
|  |   stat(path: AbsoluteFsPath): FileStats { | ||
|  |     const fileOrFolder = this.findFromPath(path, {followSymLinks: true}); | ||
|  |     if (fileOrFolder === null) { | ||
|  |       throw new MockFileSystemError('ENOENT', path, `File "${path}" does not exist.`); | ||
|  |     } | ||
|  |     return new MockFileStats(fileOrFolder); | ||
|  |   } | ||
|  | 
 | ||
|  |   pwd(): AbsoluteFsPath { return AbsoluteFsPath.fromUnchecked('/'); } | ||
|  | 
 | ||
|  |   copyFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void { | ||
|  |     this.writeFile(to, this.readFile(from)); | ||
|  |   } | ||
|  | 
 | ||
|  |   moveFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void { | ||
|  |     this.writeFile(to, this.readFile(from)); | ||
|  |     const folder = this.findFromPath(AbsoluteFsPath.dirname(from)) as Folder; | ||
|  |     const basename = PathSegment.basename(from); | ||
|  |     delete folder[basename]; | ||
|  |   } | ||
|  | 
 | ||
|  |   ensureDir(path: AbsoluteFsPath): void { this.ensureFolders(this.files, path.split('/')); } | ||
|  | 
 | ||
|  |   private processFiles(current: Folder, files: Folder): void { | ||
|  |     Object.keys(files).forEach(path => { | ||
|  |       const segments = path.split('/'); | ||
|  |       const lastSegment = segments.pop() !; | ||
|  |       const containingFolder = this.ensureFolders(current, segments); | ||
|  |       const entity = files[path]; | ||
|  |       if (isFolder(entity)) { | ||
|  |         const processedFolder = containingFolder[lastSegment] = {} as Folder; | ||
|  |         this.processFiles(processedFolder, entity); | ||
|  |       } else { | ||
|  |         containingFolder[lastSegment] = entity; | ||
|  |       } | ||
|  |     }); | ||
|  |   } | ||
|  | 
 | ||
|  |   private ensureFolders(current: Folder, segments: string[]): Folder { | ||
|  |     for (const segment of segments) { | ||
|  |       if (isFile(current[segment])) { | ||
|  |         throw new Error(`Folder already exists as a file.`); | ||
|  |       } | ||
|  |       if (!current[segment]) { | ||
|  |         current[segment] = {}; | ||
|  |       } | ||
|  |       current = current[segment] as Folder; | ||
|  |     } | ||
|  |     return current; | ||
|  |   } | ||
|  | 
 | ||
|  |   private findFromPath(path: AbsoluteFsPath, options?: {followSymLinks: boolean}): Entity|null { | ||
|  |     const followSymLinks = !!options && options.followSymLinks; | ||
|  |     const segments = path.split('/'); | ||
|  |     let current = this.files; | ||
|  |     while (segments.length) { | ||
|  |       const next: Entity = current[segments.shift() !]; | ||
|  |       if (next === undefined) { | ||
|  |         return null; | ||
|  |       } | ||
|  |       if (segments.length > 0 && (!isFolder(next))) { | ||
|  |         return null; | ||
|  |       } | ||
|  |       if (isFile(next)) { | ||
|  |         return next; | ||
|  |       } | ||
|  |       if (isSymLink(next)) { | ||
|  |         return followSymLinks ? | ||
|  |             this.findFromPath(AbsoluteFsPath.resolve(next.path, ...segments), {followSymLinks}) : | ||
|  |             next; | ||
|  |       } | ||
|  |       current = next; | ||
|  |     } | ||
|  |     return current || null; | ||
|  |   } | ||
|  | 
 | ||
|  |   private splitIntoFolderAndFile(path: AbsoluteFsPath): [AbsoluteFsPath, string] { | ||
|  |     const segments = path.split('/'); | ||
|  |     const file = segments.pop() !; | ||
|  |     return [AbsoluteFsPath.fromUnchecked(segments.join('/')), file]; | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | export type Entity = Folder | File | SymLink; | ||
|  | export interface Folder { [pathSegments: string]: Entity; } | ||
|  | export type File = string; | ||
|  | export class SymLink { | ||
|  |   constructor(public path: AbsoluteFsPath) {} | ||
|  | } | ||
|  | 
 | ||
|  | class MockFileStats implements FileStats { | ||
|  |   constructor(private entity: Entity) {} | ||
|  |   isFile(): boolean { return isFile(this.entity); } | ||
|  |   isDirectory(): boolean { return isFolder(this.entity); } | ||
|  |   isSymbolicLink(): boolean { return isSymLink(this.entity); } | ||
|  | } | ||
|  | 
 | ||
|  | class MockFileSystemError extends Error { | ||
|  |   constructor(public code: string, public path: string, message: string) { super(message); } | ||
|  | } | ||
|  | 
 | ||
|  | function isFile(item: Entity | null): item is File { | ||
|  |   return typeof item === 'string'; | ||
|  | } | ||
|  | 
 | ||
|  | function isSymLink(item: Entity | null): item is SymLink { | ||
|  |   return item instanceof SymLink; | ||
|  | } | ||
|  | 
 | ||
|  | function isFolder(item: Entity | null): item is Folder { | ||
|  |   return item !== null && !isFile(item) && !isSymLink(item); | ||
|  | } |