refactor(compiler-cli): split up `NodeJSFileSystem` class (#40281)

This class is refactored to extend the new `NodeJSReadonlyFileSystem`
which itself extends `NodeJSPathManipulation`. These new classes allow
consumers to create file-systems that provide a subset of the full file-system.

PR Close #40281
This commit is contained in:
Pete Bacon Darwin 2020-12-31 11:05:16 +00:00 committed by Andrew Scott
parent bb6d791dab
commit e27b920ac3
2 changed files with 154 additions and 124 deletions

View File

@ -9,74 +9,18 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as fsExtra from 'fs-extra'; import * as fsExtra from 'fs-extra';
import * as p from 'path'; import * as p from 'path';
import {absoluteFrom} from './helpers'; import {AbsoluteFsPath, FileStats, FileSystem, PathManipulation, PathSegment, PathString, ReadonlyFileSystem} from './types';
import {AbsoluteFsPath, FileStats, FileSystem, PathSegment, PathString} from './types';
/** /**
* A wrapper around the Node.js file-system (i.e the `fs` package). * A wrapper around the Node.js file-system that supports path manipulation.
*/ */
export class NodeJSFileSystem implements FileSystem { export class NodeJSPathManipulation implements PathManipulation {
private _caseSensitive: boolean|undefined = undefined;
exists(path: AbsoluteFsPath): boolean {
return fs.existsSync(path);
}
readFile(path: AbsoluteFsPath): string {
return fs.readFileSync(path, 'utf8');
}
readFileBuffer(path: AbsoluteFsPath): Uint8Array {
return fs.readFileSync(path);
}
writeFile(path: AbsoluteFsPath, data: string|Uint8Array, exclusive: boolean = false): void {
fs.writeFileSync(path, data, exclusive ? {flag: 'wx'} : undefined);
}
removeFile(path: AbsoluteFsPath): void {
fs.unlinkSync(path);
}
symlink(target: AbsoluteFsPath, path: AbsoluteFsPath): void {
fs.symlinkSync(target, path);
}
readdir(path: AbsoluteFsPath): PathSegment[] {
return fs.readdirSync(path) as PathSegment[];
}
lstat(path: AbsoluteFsPath): FileStats {
return fs.lstatSync(path);
}
stat(path: AbsoluteFsPath): FileStats {
return fs.statSync(path);
}
pwd(): AbsoluteFsPath { pwd(): AbsoluteFsPath {
return this.normalize(process.cwd()) as AbsoluteFsPath; return this.normalize(process.cwd()) as AbsoluteFsPath;
} }
chdir(dir: AbsoluteFsPath): void { chdir(dir: AbsoluteFsPath): void {
process.chdir(dir); process.chdir(dir);
} }
copyFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void {
fs.copyFileSync(from, to);
}
moveFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void {
fs.renameSync(from, to);
}
ensureDir(path: AbsoluteFsPath): void {
const parents: AbsoluteFsPath[] = [];
while (!this.isRoot(path) && !this.exists(path)) {
parents.push(path);
path = this.dirname(path);
}
while (parents.length) {
this.safeMkdir(parents.pop()!);
}
}
removeDeep(path: AbsoluteFsPath): void {
fsExtra.removeSync(path);
}
isCaseSensitive(): boolean {
if (this._caseSensitive === undefined) {
// Note the use of the real file-system is intentional:
// `this.exists()` relies upon `isCaseSensitive()` so that would cause an infinite recursion.
this._caseSensitive = !fs.existsSync(togglePathCase(__filename));
}
return this._caseSensitive;
}
resolve(...paths: string[]): AbsoluteFsPath { resolve(...paths: string[]): AbsoluteFsPath {
return this.normalize(p.resolve(...paths)) as AbsoluteFsPath; return this.normalize(p.resolve(...paths)) as AbsoluteFsPath;
} }
@ -102,15 +46,82 @@ export class NodeJSFileSystem implements FileSystem {
extname(path: AbsoluteFsPath|PathSegment): string { extname(path: AbsoluteFsPath|PathSegment): string {
return p.extname(path); return p.extname(path);
} }
normalize<T extends string>(path: T): T {
// Convert backslashes to forward slashes
return path.replace(/\\/g, '/') as T;
}
}
/**
* A wrapper around the Node.js file-system that supports readonly operations and path manipulation.
*/
export class NodeJSReadonlyFileSystem extends NodeJSPathManipulation implements ReadonlyFileSystem {
private _caseSensitive: boolean|undefined = undefined;
isCaseSensitive(): boolean {
if (this._caseSensitive === undefined) {
// Note the use of the real file-system is intentional:
// `this.exists()` relies upon `isCaseSensitive()` so that would cause an infinite recursion.
this._caseSensitive = !fs.existsSync(this.normalize(toggleCase(__filename)));
}
return this._caseSensitive;
}
exists(path: AbsoluteFsPath): boolean {
return fs.existsSync(path);
}
readFile(path: AbsoluteFsPath): string {
return fs.readFileSync(path, 'utf8');
}
readFileBuffer(path: AbsoluteFsPath): Uint8Array {
return fs.readFileSync(path);
}
readdir(path: AbsoluteFsPath): PathSegment[] {
return fs.readdirSync(path) as PathSegment[];
}
lstat(path: AbsoluteFsPath): FileStats {
return fs.lstatSync(path);
}
stat(path: AbsoluteFsPath): FileStats {
return fs.statSync(path);
}
realpath(path: AbsoluteFsPath): AbsoluteFsPath { realpath(path: AbsoluteFsPath): AbsoluteFsPath {
return this.resolve(fs.realpathSync(path)); return this.resolve(fs.realpathSync(path));
} }
getDefaultLibLocation(): AbsoluteFsPath { getDefaultLibLocation(): AbsoluteFsPath {
return this.resolve(require.resolve('typescript'), '..'); return this.resolve(require.resolve('typescript'), '..');
} }
normalize<T extends string>(path: T): T { }
// Convert backslashes to forward slashes
return path.replace(/\\/g, '/') as T; /**
* A wrapper around the Node.js file-system (i.e. the `fs` package).
*/
export class NodeJSFileSystem extends NodeJSReadonlyFileSystem implements FileSystem {
writeFile(path: AbsoluteFsPath, data: string|Uint8Array, exclusive: boolean = false): void {
fs.writeFileSync(path, data, exclusive ? {flag: 'wx'} : undefined);
}
removeFile(path: AbsoluteFsPath): void {
fs.unlinkSync(path);
}
symlink(target: AbsoluteFsPath, path: AbsoluteFsPath): void {
fs.symlinkSync(target, path);
}
copyFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void {
fs.copyFileSync(from, to);
}
moveFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void {
fs.renameSync(from, to);
}
ensureDir(path: AbsoluteFsPath): void {
const parents: AbsoluteFsPath[] = [];
while (!this.isRoot(path) && !this.exists(path)) {
parents.push(path);
path = this.dirname(path);
}
while (parents.length) {
this.safeMkdir(parents.pop()!);
}
}
removeDeep(path: AbsoluteFsPath): void {
fsExtra.removeSync(path);
} }
private safeMkdir(path: AbsoluteFsPath): void { private safeMkdir(path: AbsoluteFsPath): void {
@ -127,9 +138,8 @@ export class NodeJSFileSystem implements FileSystem {
} }
/** /**
* Toggle the case of each character in a file path. * Toggle the case of each character in a string.
*/ */
function togglePathCase(str: string): AbsoluteFsPath { function toggleCase(str: string): string {
return absoluteFrom( return str.replace(/\w/g, ch => ch.toUpperCase() === ch ? ch.toLowerCase() : ch.toUpperCase());
str.replace(/\w/g, ch => ch.toUpperCase() === ch ? ch.toLowerCase() : ch.toUpperCase()));
} }

View File

@ -8,22 +8,55 @@
import * as realFs from 'fs'; import * as realFs from 'fs';
import * as fsExtra from 'fs-extra'; import * as fsExtra from 'fs-extra';
import * as os from 'os'; import * as os from 'os';
import {absoluteFrom, dirname, relativeFrom, setFileSystem} from '../src/helpers'; import {NodeJSFileSystem, NodeJSPathManipulation, NodeJSReadonlyFileSystem} from '../src/node_js_file_system';
import {NodeJSFileSystem} from '../src/node_js_file_system'; import {AbsoluteFsPath, PathSegment} from '../src/types';
import {AbsoluteFsPath} from '../src/types';
describe('NodeJSFileSystem', () => { describe('NodeJSPathManipulation', () => {
let fs: NodeJSFileSystem; let fs: NodeJSPathManipulation;
let abcPath: AbsoluteFsPath; let abcPath: AbsoluteFsPath;
let xyzPath: AbsoluteFsPath; let xyzPath: AbsoluteFsPath;
beforeEach(() => { beforeEach(() => {
fs = new NodeJSFileSystem(); fs = new NodeJSPathManipulation();
// Set the file-system so that calls like `absoluteFrom()` abcPath = fs.resolve('/a/b/c');
// and `relativeFrom()` work correctly. xyzPath = fs.resolve('/x/y/z');
setFileSystem(fs); });
abcPath = absoluteFrom('/a/b/c');
xyzPath = absoluteFrom('/x/y/z'); describe('pwd()', () => {
it('should delegate to process.cwd()', () => {
const spy = spyOn(process, 'cwd').and.returnValue(abcPath);
const result = fs.pwd();
expect(result).toEqual(abcPath);
expect(spy).toHaveBeenCalledWith();
});
});
if (os.platform() === 'win32') {
// Only relevant on Windows
describe('relative', () => {
it('should handle Windows paths on different drives', () => {
expect(fs.relative('C:\\a\\b\\c', 'D:\\a\\b\\d')).toEqual(fs.resolve('D:\\a\\b\\d'));
});
});
}
});
describe('NodeJSReadonlyFileSystem', () => {
let fs: NodeJSReadonlyFileSystem;
let abcPath: AbsoluteFsPath;
let xyzPath: AbsoluteFsPath;
beforeEach(() => {
fs = new NodeJSReadonlyFileSystem();
abcPath = fs.resolve('/a/b/c');
xyzPath = fs.resolve('/x/y/z');
});
describe('isCaseSensitive()', () => {
it('should return true if the FS is case-sensitive', () => {
const isCaseSensitive = !realFs.existsSync(__filename.toUpperCase());
expect(fs.isCaseSensitive()).toEqual(isCaseSensitive);
});
}); });
describe('exists()', () => { describe('exists()', () => {
@ -55,30 +88,11 @@ describe('NodeJSFileSystem', () => {
}); });
}); });
describe('writeFile()', () => {
it('should delegate to fs.writeFileSync()', () => {
const spy = spyOn(realFs, 'writeFileSync');
fs.writeFile(abcPath, 'Some contents');
expect(spy).toHaveBeenCalledWith(abcPath, 'Some contents', undefined);
spy.calls.reset();
fs.writeFile(abcPath, 'Some contents', /* exclusive */ true);
expect(spy).toHaveBeenCalledWith(abcPath, 'Some contents', {flag: 'wx'});
});
});
describe('removeFile()', () => {
it('should delegate to fs.unlink()', () => {
const spy = spyOn(realFs, 'unlinkSync');
fs.removeFile(abcPath);
expect(spy).toHaveBeenCalledWith(abcPath);
});
});
describe('readdir()', () => { describe('readdir()', () => {
it('should delegate to fs.readdirSync()', () => { it('should delegate to fs.readdirSync()', () => {
const spy = spyOn(realFs, 'readdirSync').and.returnValue(['x', 'y/z'] as any); const spy = spyOn(realFs, 'readdirSync').and.returnValue(['x', 'y/z'] as any);
const result = fs.readdir(abcPath); const result = fs.readdir(abcPath);
expect(result).toEqual([relativeFrom('x'), relativeFrom('y/z')]); expect(result).toEqual(['x' as PathSegment, 'y/z' as PathSegment]);
// TODO: @JiaLiPassion need to wait for @types/jasmine update to handle optional parameters. // TODO: @JiaLiPassion need to wait for @types/jasmine update to handle optional parameters.
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/43486 // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/43486
expect(spy as any).toHaveBeenCalledWith(abcPath); expect(spy as any).toHaveBeenCalledWith(abcPath);
@ -106,13 +120,35 @@ describe('NodeJSFileSystem', () => {
expect(spy as any).toHaveBeenCalledWith(abcPath); expect(spy as any).toHaveBeenCalledWith(abcPath);
}); });
}); });
});
describe('pwd()', () => { describe('NodeJSFileSystem', () => {
it('should delegate to process.cwd()', () => { let fs: NodeJSFileSystem;
const spy = spyOn(process, 'cwd').and.returnValue(abcPath); let abcPath: AbsoluteFsPath;
const result = fs.pwd(); let xyzPath: AbsoluteFsPath;
expect(result).toEqual(abcPath);
expect(spy).toHaveBeenCalledWith(); beforeEach(() => {
fs = new NodeJSFileSystem();
abcPath = fs.resolve('/a/b/c');
xyzPath = fs.resolve('/x/y/z');
});
describe('writeFile()', () => {
it('should delegate to fs.writeFileSync()', () => {
const spy = spyOn(realFs, 'writeFileSync');
fs.writeFile(abcPath, 'Some contents');
expect(spy).toHaveBeenCalledWith(abcPath, 'Some contents', undefined);
spy.calls.reset();
fs.writeFile(abcPath, 'Some contents', /* exclusive */ true);
expect(spy).toHaveBeenCalledWith(abcPath, 'Some contents', {flag: 'wx'});
});
});
describe('removeFile()', () => {
it('should delegate to fs.unlink()', () => {
const spy = spyOn(realFs, 'unlinkSync');
fs.removeFile(abcPath);
expect(spy).toHaveBeenCalledWith(abcPath);
}); });
}); });
@ -134,10 +170,10 @@ describe('NodeJSFileSystem', () => {
describe('ensureDir()', () => { describe('ensureDir()', () => {
it('should call exists() and fs.mkdir()', () => { it('should call exists() and fs.mkdir()', () => {
const aPath = absoluteFrom('/a'); const aPath = fs.resolve('/a');
const abPath = absoluteFrom('/a/b'); const abPath = fs.resolve('/a/b');
const xPath = absoluteFrom('/x'); const xPath = fs.resolve('/x');
const xyPath = absoluteFrom('/x/y'); const xyPath = fs.resolve('/x/y');
const mkdirCalls: string[] = []; const mkdirCalls: string[] = [];
const existsCalls: string[] = []; const existsCalls: string[] = [];
spyOn(realFs, 'mkdirSync').and.callFake(((path: string) => mkdirCalls.push(path)) as any); spyOn(realFs, 'mkdirSync').and.callFake(((path: string) => mkdirCalls.push(path)) as any);
@ -190,7 +226,7 @@ describe('NodeJSFileSystem', () => {
fs.ensureDir(abcPath); fs.ensureDir(abcPath);
expect(mkdirSyncSpy).toHaveBeenCalledTimes(3); expect(mkdirSyncSpy).toHaveBeenCalledTimes(3);
expect(mkdirSyncSpy).toHaveBeenCalledWith(abcPath); expect(mkdirSyncSpy).toHaveBeenCalledWith(abcPath);
expect(mkdirSyncSpy).toHaveBeenCalledWith(dirname(abcPath)); expect(mkdirSyncSpy).toHaveBeenCalledWith(fs.dirname(abcPath));
}); });
it('should fail if creating the directory throws and the directory does not exist', () => { it('should fail if creating the directory throws and the directory does not exist', () => {
@ -239,20 +275,4 @@ describe('NodeJSFileSystem', () => {
expect(spy).toHaveBeenCalledWith(abcPath); expect(spy).toHaveBeenCalledWith(abcPath);
}); });
}); });
describe('isCaseSensitive()', () => {
it('should return true if the FS is case-sensitive', () => {
const isCaseSensitive = !realFs.existsSync(__filename.toUpperCase());
expect(fs.isCaseSensitive()).toEqual(isCaseSensitive);
});
});
if (os.platform() === 'win32') {
// Only relevant on Windows
describe('relative', () => {
it('should handle Windows paths on different drives', () => {
expect(fs.relative('C:\\a\\b\\c', 'D:\\a\\b\\d')).toEqual(absoluteFrom('D:\\a\\b\\d'));
});
});
}
}); });