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:
parent
bb6d791dab
commit
e27b920ac3
|
@ -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()));
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue