refactor(ivy): implement a virtual file-system layer in ngtsc + ngcc (#30921)

To improve cross platform support, all file access (and path manipulation)
is now done through a well known interface (`FileSystem`).

For testing a number of `MockFileSystem` implementations are provided.
These provide an in-memory file-system which emulates operating systems
like OS/X, Unix and Windows.

The current file system is always available via the static method,
`FileSystem.getFileSystem()`. This is also used by a number of static
methods on `AbsoluteFsPath` and `PathSegment`, to avoid having to pass
`FileSystem` objects around all the time. The result of this is that one
must be careful to ensure that the file-system has been initialized before
using any of these static methods. To prevent this happening accidentally
the current file system always starts out as an instance of `InvalidFileSystem`,
which will throw an error if any of its methods are called.

You can set the current file-system by calling `FileSystem.setFileSystem()`.
During testing you can call the helper function `initMockFileSystem(os)`
which takes a string name of the OS to emulate, and will also monkey-patch
aspects of the TypeScript library to ensure that TS is also using the
current file-system.

Finally there is the `NgtscCompilerHost` to be used for any TypeScript
compilation, which uses a given file-system.

All tests that interact with the file-system should be tested against each
of the mock file-systems. A series of helpers have been provided to support
such tests:

* `runInEachFileSystem()` - wrap your tests in this helper to run all the
wrapped tests in each of the mock file-systems.
* `addTestFilesToFileSystem()` - use this to add files and their contents
to the mock file system for testing.
* `loadTestFilesFromDisk()` - use this to load a mirror image of files on
disk into the in-memory mock file-system.
* `loadFakeCore()` - use this to load a fake version of `@angular/core`
into the mock file-system.

All ngcc and ngtsc source and tests now use this virtual file-system setup.

PR Close #30921
This commit is contained in:
Pete Bacon Darwin
2019-06-06 20:22:32 +01:00
committed by Kara Erickson
parent 1e7e065423
commit 7186f9c016
177 changed files with 16598 additions and 14829 deletions

View File

@ -0,0 +1,15 @@
package(default_visibility = ["//visibility:public"])
load("//tools:defaults.bzl", "ts_library")
ts_library(
name = "file_system",
srcs = ["index.ts"] + glob([
"src/*.ts",
]),
deps = [
"//packages:types",
"@npm//@types/node",
"@npm//typescript",
],
)

View File

@ -0,0 +1,42 @@
# Virtual file-system layer
To improve cross platform support, all file access (and path manipulation)
is now done through a well known interface (`FileSystem`).
For testing a number of `MockFileSystem` implementations are supplied.
These provide an in-memory file-system which emulates operating systems
like OS/X, Unix and Windows.
The current file system is always available via the helper method,
`getFileSystem()`. This is also used by a number of helper
methods to avoid having to pass `FileSystem` objects around all the time.
The result of this is that one must be careful to ensure that the file-system
has been initialized before using any of these helper methods.
To prevent this happening accidentally the current file system always starts out
as an instance of `InvalidFileSystem`, which will throw an error if any of its
methods are called.
You can set the current file-system by calling `setFileSystem()`.
During testing you can call the helper function `initMockFileSystem(os)`
which takes a string name of the OS to emulate, and will also monkey-patch
aspects of the TypeScript library to ensure that TS is also using the
current file-system.
Finally there is the `NgtscCompilerHost` to be used for any TypeScript
compilation, which uses a given file-system.
All tests that interact with the file-system should be tested against each
of the mock file-systems. A series of helpers have been provided to support
such tests:
* `runInEachFileSystem()` - wrap your tests in this helper to run all the
wrapped tests in each of the mock file-systems, it calls `initMockFileSystem()`
for each OS to emulate.
* `loadTestFiles()` - use this to add files and their contents
to the mock file system for testing.
* `loadStandardTestFiles()` - use this to load a mirror image of files on
disk into the in-memory mock file-system.
* `loadFakeCore()` - use this to load a fake version of `@angular/core`
into the mock file-system.
All ngcc and ngtsc source and tests now use this virtual file-system setup.

View File

@ -0,0 +1,13 @@
/**
* @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
*/
export {NgtscCompilerHost} from './src/compiler_host';
export {absoluteFrom, absoluteFromSourceFile, basename, dirname, getFileSystem, isRoot, join, relative, relativeFrom, resolve, setFileSystem} from './src/helpers';
export {LogicalFileSystem, LogicalProjectPath} from './src/logical';
export {NodeJSFileSystem} from './src/node_js_file_system';
export {AbsoluteFsPath, FileStats, FileSystem, PathSegment, PathString} from './src/types';
export {getSourceFileOrError} from './src/util';

View File

@ -0,0 +1,71 @@
/**
* @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
*/
/// <reference types="node" />
import * as os from 'os';
import * as ts from 'typescript';
import {absoluteFrom} from './helpers';
import {FileSystem} from './types';
export class NgtscCompilerHost implements ts.CompilerHost {
constructor(protected fs: FileSystem, protected options: ts.CompilerOptions = {}) {}
getSourceFile(fileName: string, languageVersion: ts.ScriptTarget): ts.SourceFile|undefined {
const text = this.readFile(fileName);
return text !== undefined ? ts.createSourceFile(fileName, text, languageVersion, true) :
undefined;
}
getDefaultLibFileName(options: ts.CompilerOptions): string {
return this.fs.join(this.getDefaultLibLocation(), ts.getDefaultLibFileName(options));
}
getDefaultLibLocation(): string { return this.fs.getDefaultLibLocation(); }
writeFile(
fileName: string, data: string, writeByteOrderMark: boolean,
onError: ((message: string) => void)|undefined,
sourceFiles?: ReadonlyArray<ts.SourceFile>): void {
const path = absoluteFrom(fileName);
this.fs.ensureDir(this.fs.dirname(path));
this.fs.writeFile(path, data);
}
getCurrentDirectory(): string { return this.fs.pwd(); }
getCanonicalFileName(fileName: string): string {
return this.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase();
}
useCaseSensitiveFileNames(): boolean { return this.fs.isCaseSensitive(); }
getNewLine(): string {
switch (this.options.newLine) {
case ts.NewLineKind.CarriageReturnLineFeed:
return '\r\n';
case ts.NewLineKind.LineFeed:
return '\n';
default:
return os.EOL;
}
}
fileExists(fileName: string): boolean {
const absPath = this.fs.resolve(fileName);
return this.fs.exists(absPath);
}
readFile(fileName: string): string|undefined {
const absPath = this.fs.resolve(fileName);
if (!this.fileExists(absPath)) {
return undefined;
}
return this.fs.readFile(absPath);
}
}

View File

@ -0,0 +1,88 @@
/**
* @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 * as ts from 'typescript';
import {InvalidFileSystem} from './invalid_file_system';
import {AbsoluteFsPath, FileSystem, PathSegment, PathString} from './types';
import {normalizeSeparators} from './util';
let fs: FileSystem = new InvalidFileSystem();
export function getFileSystem(): FileSystem {
return fs;
}
export function setFileSystem(fileSystem: FileSystem) {
fs = fileSystem;
}
/**
* Convert the path `path` to an `AbsoluteFsPath`, throwing an error if it's not an absolute path.
*/
export function absoluteFrom(path: string): AbsoluteFsPath {
if (!fs.isRooted(path)) {
throw new Error(`Internal Error: absoluteFrom(${path}): path is not absolute`);
}
return fs.resolve(path);
}
/**
* Extract an `AbsoluteFsPath` from a `ts.SourceFile`.
*/
export function absoluteFromSourceFile(sf: ts.SourceFile): AbsoluteFsPath {
return fs.resolve(sf.fileName);
}
/**
* Convert the path `path` to a `PathSegment`, throwing an error if it's not a relative path.
*/
export function relativeFrom(path: string): PathSegment {
const normalized = normalizeSeparators(path);
if (fs.isRooted(normalized)) {
throw new Error(`Internal Error: relativeFrom(${path}): path is not relative`);
}
return normalized as PathSegment;
}
/**
* Static access to `dirname`.
*/
export function dirname<T extends PathString>(file: T): T {
return fs.dirname(file);
}
/**
* Static access to `join`.
*/
export function join<T extends PathString>(basePath: T, ...paths: string[]): T {
return fs.join(basePath, ...paths);
}
/**
* Static access to `resolve`s.
*/
export function resolve(basePath: string, ...paths: string[]): AbsoluteFsPath {
return fs.resolve(basePath, ...paths);
}
/** Returns true when the path provided is the root path. */
export function isRoot(path: AbsoluteFsPath): boolean {
return fs.isRoot(path);
}
/**
* Static access to `relative`.
*/
export function relative<T extends PathString>(from: T, to: T): PathSegment {
return fs.relative(from, to);
}
/**
* Static access to `basename`.
*/
export function basename(filePath: PathString, extension?: string): PathSegment {
return fs.basename(filePath, extension) as PathSegment;
}

View File

@ -0,0 +1,48 @@
/**
* @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, FileStats, FileSystem, PathSegment, PathString} from './types';
/**
* The default `FileSystem` that will always fail.
*
* This is a way of ensuring that the developer consciously chooses and
* configures the `FileSystem` before using it; particularly important when
* considering static functions like `absoluteFrom()` which rely on
* the `FileSystem` under the hood.
*/
export class InvalidFileSystem implements FileSystem {
exists(path: AbsoluteFsPath): boolean { throw makeError(); }
readFile(path: AbsoluteFsPath): string { throw makeError(); }
writeFile(path: AbsoluteFsPath, data: string): void { throw makeError(); }
symlink(target: AbsoluteFsPath, path: AbsoluteFsPath): void { throw makeError(); }
readdir(path: AbsoluteFsPath): PathSegment[] { throw makeError(); }
lstat(path: AbsoluteFsPath): FileStats { throw makeError(); }
stat(path: AbsoluteFsPath): FileStats { throw makeError(); }
pwd(): AbsoluteFsPath { throw makeError(); }
extname(path: AbsoluteFsPath|PathSegment): string { throw makeError(); }
copyFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void { throw makeError(); }
moveFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void { throw makeError(); }
mkdir(path: AbsoluteFsPath): void { throw makeError(); }
ensureDir(path: AbsoluteFsPath): void { throw makeError(); }
isCaseSensitive(): boolean { throw makeError(); }
resolve(...paths: string[]): AbsoluteFsPath { throw makeError(); }
dirname<T extends PathString>(file: T): T { throw makeError(); }
join<T extends PathString>(basePath: T, ...paths: string[]): T { throw makeError(); }
isRoot(path: AbsoluteFsPath): boolean { throw makeError(); }
isRooted(path: string): boolean { throw makeError(); }
relative<T extends PathString>(from: T, to: T): PathSegment { throw makeError(); }
basename(filePath: string, extension?: string): PathSegment { throw makeError(); }
realpath(filePath: AbsoluteFsPath): AbsoluteFsPath { throw makeError(); }
getDefaultLibLocation(): AbsoluteFsPath { throw makeError(); }
normalize<T extends PathString>(path: T): T { throw makeError(); }
}
function makeError() {
return new Error(
'FileSystem has not been configured. Please call `setFileSystem()` before calling this method.');
}

View File

@ -0,0 +1,102 @@
/**
* @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 * as ts from 'typescript';
import {absoluteFrom, dirname, relative, resolve} from './helpers';
import {AbsoluteFsPath, BrandedPath, PathSegment} from './types';
import {stripExtension} from './util';
/**
* A path that's relative to the logical root of a TypeScript project (one of the project's
* rootDirs).
*
* Paths in the type system use POSIX format.
*/
export type LogicalProjectPath = BrandedPath<'LogicalProjectPath'>;
export const LogicalProjectPath = {
/**
* Get the relative path between two `LogicalProjectPath`s.
*
* This will return a `PathSegment` which would be a valid module specifier to use in `from` when
* importing from `to`.
*/
relativePathBetween: function(from: LogicalProjectPath, to: LogicalProjectPath): PathSegment {
let relativePath = relative(dirname(resolve(from)), resolve(to));
if (!relativePath.startsWith('../')) {
relativePath = ('./' + relativePath) as PathSegment;
}
return relativePath as PathSegment;
},
};
/**
* A utility class which can translate absolute paths to source files into logical paths in
* TypeScript's logical file system, based on the root directories of the project.
*/
export class LogicalFileSystem {
/**
* The root directories of the project, sorted with the longest path first.
*/
private rootDirs: AbsoluteFsPath[];
/**
* A cache of file paths to project paths, because computation of these paths is slightly
* expensive.
*/
private cache: Map<AbsoluteFsPath, LogicalProjectPath|null> = new Map();
constructor(rootDirs: AbsoluteFsPath[]) {
// Make a copy and sort it by length in reverse order (longest first). This speeds up lookups,
// since there's no need to keep going through the array once a match is found.
this.rootDirs = rootDirs.concat([]).sort((a, b) => b.length - a.length);
}
/**
* Get the logical path in the project of a `ts.SourceFile`.
*
* This method is provided as a convenient alternative to calling
* `logicalPathOfFile(absoluteFromSourceFile(sf))`.
*/
logicalPathOfSf(sf: ts.SourceFile): LogicalProjectPath|null {
return this.logicalPathOfFile(absoluteFrom(sf.fileName));
}
/**
* Get the logical path in the project of a source file.
*
* @returns A `LogicalProjectPath` to the source file, or `null` if the source file is not in any
* of the TS project's root directories.
*/
logicalPathOfFile(physicalFile: AbsoluteFsPath): LogicalProjectPath|null {
if (!this.cache.has(physicalFile)) {
let logicalFile: LogicalProjectPath|null = null;
for (const rootDir of this.rootDirs) {
if (physicalFile.startsWith(rootDir)) {
logicalFile = this.createLogicalProjectPath(physicalFile, rootDir);
// The logical project does not include any special "node_modules" nested directories.
if (logicalFile.indexOf('/node_modules/') !== -1) {
logicalFile = null;
} else {
break;
}
}
}
this.cache.set(physicalFile, logicalFile);
}
return this.cache.get(physicalFile) !;
}
private createLogicalProjectPath(file: AbsoluteFsPath, rootDir: AbsoluteFsPath):
LogicalProjectPath {
const logicalPath = stripExtension(file.substr(rootDir.length));
return (logicalPath.startsWith('/') ? logicalPath : '/' + logicalPath) as LogicalProjectPath;
}
}

View File

@ -0,0 +1,81 @@
/**
* @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
*/
/// <reference types="node" />
import * as fs from 'fs';
import * as p from 'path';
import {absoluteFrom, relativeFrom} from './helpers';
import {AbsoluteFsPath, FileStats, FileSystem, PathSegment, PathString} from './types';
/**
* A wrapper around the Node.js file-system (i.e the `fs` package).
*/
export class NodeJSFileSystem implements FileSystem {
private _caseSensitive: boolean|undefined = undefined;
exists(path: AbsoluteFsPath): boolean { return fs.existsSync(path); }
readFile(path: AbsoluteFsPath): string { return fs.readFileSync(path, 'utf8'); }
writeFile(path: AbsoluteFsPath, data: string): void {
return fs.writeFileSync(path, data, 'utf8');
}
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 { return this.normalize(process.cwd()) as AbsoluteFsPath; }
copyFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void { fs.copyFileSync(from, to); }
moveFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void { fs.renameSync(from, to); }
mkdir(path: AbsoluteFsPath): void { fs.mkdirSync(path); }
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.mkdir(parents.pop() !);
}
}
isCaseSensitive(): boolean {
if (this._caseSensitive === undefined) {
this._caseSensitive = this.exists(togglePathCase(__filename));
}
return this._caseSensitive;
}
resolve(...paths: string[]): AbsoluteFsPath {
return this.normalize(p.resolve(...paths)) as AbsoluteFsPath;
}
dirname<T extends string>(file: T): T { return this.normalize(p.dirname(file)) as T; }
join<T extends string>(basePath: T, ...paths: string[]): T {
return this.normalize(p.join(basePath, ...paths)) as T;
}
isRoot(path: AbsoluteFsPath): boolean { return this.dirname(path) === this.normalize(path); }
isRooted(path: string): boolean { return p.isAbsolute(path); }
relative<T extends PathString>(from: T, to: T): PathSegment {
return relativeFrom(this.normalize(p.relative(from, to)));
}
basename(filePath: string, extension?: string): PathSegment {
return p.basename(filePath, extension) as PathSegment;
}
extname(path: AbsoluteFsPath|PathSegment): string { return p.extname(path); }
realpath(path: AbsoluteFsPath): AbsoluteFsPath { return this.resolve(fs.realpathSync(path)); }
getDefaultLibLocation(): AbsoluteFsPath {
return this.resolve(require.resolve('typescript'), '..');
}
normalize<T extends string>(path: T): T {
// Convert backslashes to forward slashes
return path.replace(/\\/g, '/') as T;
}
}
/**
* Toggle the case of each character in a file path.
*/
function togglePathCase(str: string): AbsoluteFsPath {
return absoluteFrom(
str.replace(/\w/g, ch => ch.toUpperCase() === ch ? ch.toLowerCase() : ch.toUpperCase()));
}

View File

@ -0,0 +1,74 @@
/**
* @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
*/
/**
* A `string` representing a specific type of path, with a particular brand `B`.
*
* A `string` is not assignable to a `BrandedPath`, but a `BrandedPath` is assignable to a `string`.
* Two `BrandedPath`s with different brands are not mutually assignable.
*/
export type BrandedPath<B extends string> = string & {
_brand: B;
};
/**
* A fully qualified path in the file system, in POSIX form.
*/
export type AbsoluteFsPath = BrandedPath<'AbsoluteFsPath'>;
/**
* A path that's relative to another (unspecified) root.
*
* This does not necessarily have to refer to a physical file.
*/
export type PathSegment = BrandedPath<'PathSegment'>;
/**
* A basic interface to abstract the underlying file-system.
*
* This makes it easier to provide mock file-systems in unit tests,
* but also to create clever file-systems that have features such as caching.
*/
export interface FileSystem {
exists(path: AbsoluteFsPath): boolean;
readFile(path: AbsoluteFsPath): string;
writeFile(path: AbsoluteFsPath, data: string): void;
symlink(target: AbsoluteFsPath, path: AbsoluteFsPath): void;
readdir(path: AbsoluteFsPath): PathSegment[];
lstat(path: AbsoluteFsPath): FileStats;
stat(path: AbsoluteFsPath): FileStats;
pwd(): AbsoluteFsPath;
extname(path: AbsoluteFsPath|PathSegment): string;
copyFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void;
moveFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void;
mkdir(path: AbsoluteFsPath): void;
ensureDir(path: AbsoluteFsPath): void;
isCaseSensitive(): boolean;
isRoot(path: AbsoluteFsPath): boolean;
isRooted(path: string): boolean;
resolve(...paths: string[]): AbsoluteFsPath;
dirname<T extends PathString>(file: T): T;
join<T extends PathString>(basePath: T, ...paths: string[]): T;
relative<T extends PathString>(from: T, to: T): PathSegment;
basename(filePath: string, extension?: string): PathSegment;
realpath(filePath: AbsoluteFsPath): AbsoluteFsPath;
getDefaultLibLocation(): AbsoluteFsPath;
normalize<T extends PathString>(path: T): T;
}
export type PathString = string | AbsoluteFsPath | PathSegment;
/**
* Information about an object in the FileSystem.
* This is analogous to the `fs.Stats` class in Node.js.
*/
export interface FileStats {
isFile(): boolean;
isDirectory(): boolean;
isSymbolicLink(): boolean;
}

View File

@ -0,0 +1,35 @@
/**
* @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 * as ts from 'typescript';
import {AbsoluteFsPath} from './types';
const TS_DTS_JS_EXTENSION = /(?:\.d)?\.ts$|\.js$/;
/**
* Convert Windows-style separators to POSIX separators.
*/
export function normalizeSeparators(path: string): string {
// TODO: normalize path only for OS that need it.
return path.replace(/\\/g, '/');
}
/**
* Remove a .ts, .d.ts, or .js extension from a file name.
*/
export function stripExtension(path: string): string {
return path.replace(TS_DTS_JS_EXTENSION, '');
}
export function getSourceFileOrError(program: ts.Program, fileName: AbsoluteFsPath): ts.SourceFile {
const sf = program.getSourceFile(fileName);
if (sf === undefined) {
throw new Error(
`Program does not contain "${fileName}" - available files are ${program.getSourceFiles().map(sf => sf.fileName).join(', ')}`);
}
return sf;
}

View File

@ -0,0 +1,25 @@
package(default_visibility = ["//visibility:public"])
load("//tools:defaults.bzl", "jasmine_node_test", "ts_library")
ts_library(
name = "test_lib",
testonly = True,
srcs = glob([
"**/*.ts",
]),
deps = [
"//packages:types",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/file_system/testing",
],
)
jasmine_node_test(
name = "test",
bootstrap = ["angular/tools/testing/init_node_no_angular_spec.js"],
deps = [
":test_lib",
"//tools/testing:node_no_angular",
],
)

View File

@ -0,0 +1,47 @@
/**
* @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 * as os from 'os';
import {absoluteFrom, relativeFrom, setFileSystem} from '../src/helpers';
import {NodeJSFileSystem} from '../src/node_js_file_system';
describe('path types', () => {
beforeEach(() => { setFileSystem(new NodeJSFileSystem()); });
describe('absoluteFrom', () => {
it('should not throw when creating one from an absolute path',
() => { expect(() => absoluteFrom('/test.txt')).not.toThrow(); });
if (os.platform() === 'win32') {
it('should not throw when creating one from a windows absolute path',
() => { expect(absoluteFrom('C:\\test.txt')).toEqual('C:/test.txt'); });
it('should not throw when creating one from a windows absolute path with POSIX separators',
() => { expect(absoluteFrom('C:/test.txt')).toEqual('C:/test.txt'); });
it('should support windows drive letters',
() => { expect(absoluteFrom('D:\\foo\\test.txt')).toEqual('D:/foo/test.txt'); });
it('should convert Windows path separators to POSIX separators',
() => { expect(absoluteFrom('C:\\foo\\test.txt')).toEqual('C:/foo/test.txt'); });
}
it('should throw when creating one from a non-absolute path',
() => { expect(() => absoluteFrom('test.txt')).toThrow(); });
});
describe('relativeFrom', () => {
it('should not throw when creating one from a relative path',
() => { expect(() => relativeFrom('a/b/c.txt')).not.toThrow(); });
it('should throw when creating one from an absolute path',
() => { expect(() => relativeFrom('/a/b/c.txt')).toThrow(); });
if (os.platform() === 'win32') {
it('should throw when creating one from a Windows absolute path',
() => { expect(() => relativeFrom('C:/a/b/c.txt')).toThrow(); });
}
});
});

View File

@ -0,0 +1,65 @@
/**
* @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 {absoluteFrom} from '../src/helpers';
import {LogicalFileSystem, LogicalProjectPath} from '../src/logical';
import {runInEachFileSystem} from '../testing';
runInEachFileSystem(() => {
describe('logical paths', () => {
let _: typeof absoluteFrom;
beforeEach(() => _ = absoluteFrom);
describe('LogicalFileSystem', () => {
it('should determine logical paths in a single root file system', () => {
const fs = new LogicalFileSystem([_('/test')]);
expect(fs.logicalPathOfFile(_('/test/foo/foo.ts')))
.toEqual('/foo/foo' as LogicalProjectPath);
expect(fs.logicalPathOfFile(_('/test/bar/bar.ts')))
.toEqual('/bar/bar' as LogicalProjectPath);
expect(fs.logicalPathOfFile(_('/not-test/bar.ts'))).toBeNull();
});
it('should determine logical paths in a multi-root file system', () => {
const fs = new LogicalFileSystem([_('/test/foo'), _('/test/bar')]);
expect(fs.logicalPathOfFile(_('/test/foo/foo.ts'))).toEqual('/foo' as LogicalProjectPath);
expect(fs.logicalPathOfFile(_('/test/bar/bar.ts'))).toEqual('/bar' as LogicalProjectPath);
});
it('should continue to work when one root is a child of another', () => {
const fs = new LogicalFileSystem([_('/test'), _('/test/dist')]);
expect(fs.logicalPathOfFile(_('/test/foo.ts'))).toEqual('/foo' as LogicalProjectPath);
expect(fs.logicalPathOfFile(_('/test/dist/foo.ts'))).toEqual('/foo' as LogicalProjectPath);
});
it('should always return `/` prefixed logical paths', () => {
const rootFs = new LogicalFileSystem([_('/')]);
expect(rootFs.logicalPathOfFile(_('/foo/foo.ts')))
.toEqual('/foo/foo' as LogicalProjectPath);
const nonRootFs = new LogicalFileSystem([_('/test/')]);
expect(nonRootFs.logicalPathOfFile(_('/test/foo/foo.ts')))
.toEqual('/foo/foo' as LogicalProjectPath);
});
});
describe('utilities', () => {
it('should give a relative path between two adjacent logical files', () => {
const res = LogicalProjectPath.relativePathBetween(
'/foo' as LogicalProjectPath, '/bar' as LogicalProjectPath);
expect(res).toEqual('./bar');
});
it('should give a relative path between two non-adjacent logical files', () => {
const res = LogicalProjectPath.relativePathBetween(
'/foo/index' as LogicalProjectPath, '/bar/index' as LogicalProjectPath);
expect(res).toEqual('../bar/index');
});
});
});
});

View File

@ -0,0 +1,148 @@
/**
* @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 * as realFs from 'fs';
import {absoluteFrom, relativeFrom, setFileSystem} from '../src/helpers';
import {NodeJSFileSystem} from '../src/node_js_file_system';
import {AbsoluteFsPath} from '../src/types';
describe('NodeJSFileSystem', () => {
let fs: NodeJSFileSystem;
let abcPath: AbsoluteFsPath;
let xyzPath: AbsoluteFsPath;
beforeEach(() => {
fs = new NodeJSFileSystem();
// Set the file-system so that calls like `absoluteFrom()`
// and `relativeFrom()` work correctly.
setFileSystem(fs);
abcPath = absoluteFrom('/a/b/c');
xyzPath = absoluteFrom('/x/y/z');
});
describe('exists()', () => {
it('should delegate to fs.existsSync()', () => {
const spy = spyOn(realFs, 'existsSync').and.returnValues(true, false);
expect(fs.exists(abcPath)).toBe(true);
expect(spy).toHaveBeenCalledWith(abcPath);
expect(fs.exists(xyzPath)).toBe(false);
expect(spy).toHaveBeenCalledWith(xyzPath);
});
});
describe('readFile()', () => {
it('should delegate to fs.readFileSync()', () => {
const spy = spyOn(realFs, 'readFileSync').and.returnValue('Some contents');
const result = fs.readFile(abcPath);
expect(result).toBe('Some contents');
expect(spy).toHaveBeenCalledWith(abcPath, 'utf8');
});
});
describe('writeFile()', () => {
it('should delegate to fs.writeFileSync()', () => {
const spy = spyOn(realFs, 'writeFileSync');
fs.writeFile(abcPath, 'Some contents');
expect(spy).toHaveBeenCalledWith(abcPath, 'Some contents', 'utf8');
});
});
describe('readdir()', () => {
it('should delegate to fs.readdirSync()', () => {
const spy = spyOn(realFs, 'readdirSync').and.returnValue(['x', 'y/z']);
const result = fs.readdir(abcPath);
expect(result).toEqual([relativeFrom('x'), relativeFrom('y/z')]);
expect(spy).toHaveBeenCalledWith(abcPath);
});
});
describe('lstat()', () => {
it('should delegate to fs.lstatSync()', () => {
const stats = new realFs.Stats();
const spy = spyOn(realFs, 'lstatSync').and.returnValue(stats);
const result = fs.lstat(abcPath);
expect(result).toBe(stats);
expect(spy).toHaveBeenCalledWith(abcPath);
});
});
describe('stat()', () => {
it('should delegate to fs.statSync()', () => {
const stats = new realFs.Stats();
const spy = spyOn(realFs, 'statSync').and.returnValue(stats);
const result = fs.stat(abcPath);
expect(result).toBe(stats);
expect(spy).toHaveBeenCalledWith(abcPath);
});
});
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();
});
});
describe('copyFile()', () => {
it('should delegate to fs.copyFileSync()', () => {
const spy = spyOn(realFs, 'copyFileSync');
fs.copyFile(abcPath, xyzPath);
expect(spy).toHaveBeenCalledWith(abcPath, xyzPath);
});
});
describe('moveFile()', () => {
it('should delegate to fs.renameSync()', () => {
const spy = spyOn(realFs, 'renameSync');
fs.moveFile(abcPath, xyzPath);
expect(spy).toHaveBeenCalledWith(abcPath, xyzPath);
});
});
describe('mkdir()', () => {
it('should delegate to fs.mkdirSync()', () => {
const spy = spyOn(realFs, 'mkdirSync');
fs.mkdir(xyzPath);
expect(spy).toHaveBeenCalledWith(xyzPath);
});
});
describe('ensureDir()', () => {
it('should call exists() and fs.mkdir()', () => {
const aPath = absoluteFrom('/a');
const abPath = absoluteFrom('/a/b');
const xPath = absoluteFrom('/x');
const xyPath = absoluteFrom('/x/y');
const mkdirCalls: string[] = [];
const existsCalls: string[] = [];
spyOn(realFs, 'mkdirSync').and.callFake((path: string) => mkdirCalls.push(path));
spyOn(fs, 'exists').and.callFake((path: AbsoluteFsPath) => {
existsCalls.push(path);
switch (path) {
case aPath:
return true;
case abPath:
return true;
default:
return false;
}
});
fs.ensureDir(abcPath);
expect(existsCalls).toEqual([abcPath, abPath]);
expect(mkdirCalls).toEqual([abcPath]);
mkdirCalls.length = 0;
existsCalls.length = 0;
fs.ensureDir(xyzPath);
expect(existsCalls).toEqual([xyzPath, xyPath, xPath]);
expect(mkdirCalls).toEqual([xPath, xyPath, xyzPath]);
});
});
});

View File

@ -0,0 +1,16 @@
load("//tools:defaults.bzl", "ts_library")
package(default_visibility = ["//visibility:public"])
ts_library(
name = "testing",
testonly = True,
srcs = glob([
"**/*.ts",
]),
deps = [
"//packages:types",
"//packages/compiler-cli/src/ngtsc/file_system",
"@npm//typescript",
],
)

View File

@ -0,0 +1,13 @@
/**
* @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
*/
export {Folder, MockFileSystem} from './src/mock_file_system';
export {MockFileSystemNative} from './src/mock_file_system_native';
export {MockFileSystemPosix} from './src/mock_file_system_posix';
export {MockFileSystemWindows} from './src/mock_file_system_windows';
export {TestFile, initMockFileSystem, runInEachFileSystem} from './src/test_helper';

View File

@ -0,0 +1,239 @@
/**
* @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 {basename, dirname, resolve} from '../../src/helpers';
import {AbsoluteFsPath, FileStats, FileSystem, PathSegment, PathString} from '../../src/types';
/**
* An in-memory file system that can be used in unit tests.
*/
export abstract class MockFileSystem implements FileSystem {
private _fileTree: Folder = {};
private _cwd: AbsoluteFsPath;
constructor(private _isCaseSensitive = false, cwd: AbsoluteFsPath = '/' as AbsoluteFsPath) {
this._cwd = this.normalize(cwd);
}
isCaseSensitive() { return this._isCaseSensitive; }
exists(path: AbsoluteFsPath): boolean { return this.findFromPath(path).entity !== null; }
readFile(path: AbsoluteFsPath): string {
const {entity} = this.findFromPath(path);
if (isFile(entity)) {
return entity;
} else {
throw new MockFileSystemError('ENOENT', path, `File "${path}" does not exist.`);
}
}
writeFile(path: AbsoluteFsPath, data: string): void {
const [folderPath, basename] = this.splitIntoFolderAndFile(path);
const {entity} = this.findFromPath(folderPath);
if (entity === null || !isFolder(entity)) {
throw new MockFileSystemError(
'ENOENT', path, `Unable to write file "${path}". The containing folder does not exist.`);
}
entity[basename] = data;
}
symlink(target: AbsoluteFsPath, path: AbsoluteFsPath): void {
const [folderPath, basename] = this.splitIntoFolderAndFile(path);
const {entity} = this.findFromPath(folderPath);
if (entity === null || !isFolder(entity)) {
throw new MockFileSystemError(
'ENOENT', path,
`Unable to create symlink at "${path}". The containing folder does not exist.`);
}
entity[basename] = new SymLink(target);
}
readdir(path: AbsoluteFsPath): PathSegment[] {
const {entity} = this.findFromPath(path);
if (entity === null) {
throw new MockFileSystemError(
'ENOENT', path, `Unable to read directory "${path}". It does not exist.`);
}
if (isFile(entity)) {
throw new MockFileSystemError(
'ENOTDIR', path, `Unable to read directory "${path}". It is a file.`);
}
return Object.keys(entity) as PathSegment[];
}
lstat(path: AbsoluteFsPath): FileStats {
const {entity} = this.findFromPath(path);
if (entity === null) {
throw new MockFileSystemError('ENOENT', path, `File "${path}" does not exist.`);
}
return new MockFileStats(entity);
}
stat(path: AbsoluteFsPath): FileStats {
const {entity} = this.findFromPath(path, {followSymLinks: true});
if (entity === null) {
throw new MockFileSystemError('ENOENT', path, `File "${path}" does not exist.`);
}
return new MockFileStats(entity);
}
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 result = this.findFromPath(dirname(from));
const folder = result.entity as Folder;
const name = basename(from);
delete folder[name];
}
mkdir(path: AbsoluteFsPath): void { this.ensureFolders(this._fileTree, this.splitPath(path)); }
ensureDir(path: AbsoluteFsPath): void {
this.ensureFolders(this._fileTree, this.splitPath(path));
}
isRoot(path: AbsoluteFsPath): boolean { return this.dirname(path) === path; }
extname(path: AbsoluteFsPath|PathSegment): string {
const match = /.+(\.[^.]*)$/.exec(path);
return match !== null ? match[1] : '';
}
realpath(filePath: AbsoluteFsPath): AbsoluteFsPath {
const result = this.findFromPath(filePath, {followSymLinks: true});
if (result.entity === null) {
throw new MockFileSystemError(
'ENOENT', filePath, `Unable to find the real path of "${filePath}". It does not exist.`);
} else {
return result.path;
}
}
pwd(): AbsoluteFsPath { return this._cwd; }
getDefaultLibLocation(): AbsoluteFsPath { return this.resolve('node_modules/typescript/lib'); }
abstract resolve(...paths: string[]): AbsoluteFsPath;
abstract dirname<T extends string>(file: T): T;
abstract join<T extends string>(basePath: T, ...paths: string[]): T;
abstract relative<T extends PathString>(from: T, to: T): PathSegment;
abstract basename(filePath: string, extension?: string): PathSegment;
abstract isRooted(path: string): boolean;
abstract normalize<T extends PathString>(path: T): T;
protected abstract splitPath<T extends PathString>(path: T): string[];
dump(): Folder { return cloneFolder(this._fileTree); }
init(folder: Folder): void { this._fileTree = cloneFolder(folder); }
protected findFromPath(path: AbsoluteFsPath, options?: {followSymLinks: boolean}): FindResult {
const followSymLinks = !!options && options.followSymLinks;
const segments = this.splitPath(path);
if (segments.length > 1 && segments[segments.length - 1] === '') {
// Remove a trailing slash (unless the path was only `/`)
segments.pop();
}
// Convert the root folder to a canonical empty string `""` (on Windows it would be `C:`).
segments[0] = '';
let current: Entity|null = this._fileTree;
while (segments.length) {
current = current[segments.shift() !];
if (current === undefined) {
return {path, entity: null};
}
if (segments.length > 0 && (!isFolder(current))) {
current = null;
break;
}
if (isFile(current)) {
break;
}
if (isSymLink(current)) {
if (followSymLinks) {
return this.findFromPath(resolve(current.path, ...segments), {followSymLinks});
} else {
break;
}
}
}
return {path, entity: current};
}
protected splitIntoFolderAndFile(path: AbsoluteFsPath): [AbsoluteFsPath, string] {
const segments = this.splitPath(path);
const file = segments.pop() !;
return [path.substring(0, path.length - file.length - 1) as AbsoluteFsPath, file];
}
protected ensureFolders(current: Folder, segments: string[]): Folder {
// Convert the root folder to a canonical empty string `""` (on Windows it would be `C:`).
segments[0] = '';
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;
}
}
export interface FindResult {
path: AbsoluteFsPath;
entity: Entity|null;
}
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); }
}
export function isFile(item: Entity | null): item is File {
return typeof item === 'string';
}
export function isSymLink(item: Entity | null): item is SymLink {
return item instanceof SymLink;
}
export function isFolder(item: Entity | null): item is Folder {
return item !== null && !isFile(item) && !isSymLink(item);
}
function cloneFolder(folder: Folder): Folder {
const clone: Folder = {};
for (const path in folder) {
const item = folder[path];
if (isSymLink(item)) {
clone[path] = new SymLink(item.path);
} else if (isFolder(item)) {
clone[path] = cloneFolder(item);
} else {
clone[path] = folder[path];
}
}
return clone;
}

View File

@ -0,0 +1,48 @@
/**
* @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 {NodeJSFileSystem} from '../../src/node_js_file_system';
import {AbsoluteFsPath, PathSegment, PathString} from '../../src/types';
import {MockFileSystem} from './mock_file_system';
export class MockFileSystemNative extends MockFileSystem {
constructor(cwd: AbsoluteFsPath = '/' as AbsoluteFsPath) { super(undefined, cwd); }
// Delegate to the real NodeJSFileSystem for these path related methods
resolve(...paths: string[]): AbsoluteFsPath {
return NodeJSFileSystem.prototype.resolve.call(this, this.pwd(), ...paths);
}
dirname<T extends string>(file: T): T {
return NodeJSFileSystem.prototype.dirname.call(this, file) as T;
}
join<T extends string>(basePath: T, ...paths: string[]): T {
return NodeJSFileSystem.prototype.join.call(this, basePath, ...paths) as T;
}
relative<T extends PathString>(from: T, to: T): PathSegment {
return NodeJSFileSystem.prototype.relative.call(this, from, to);
}
basename(filePath: string, extension?: string): PathSegment {
return NodeJSFileSystem.prototype.basename.call(this, filePath, extension);
}
isCaseSensitive() { return NodeJSFileSystem.prototype.isCaseSensitive.call(this); }
isRooted(path: string): boolean { return NodeJSFileSystem.prototype.isRooted.call(this, path); }
isRoot(path: AbsoluteFsPath): boolean {
return NodeJSFileSystem.prototype.isRoot.call(this, path);
}
normalize<T extends PathString>(path: T): T {
return NodeJSFileSystem.prototype.normalize.call(this, path) as T;
}
protected splitPath<T>(path: string): string[] { return path.split(/[\\\/]/); }
}

View File

@ -0,0 +1,41 @@
/**
* @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
*/
/// <reference types="node" />
import * as p from 'path';
import {AbsoluteFsPath, PathSegment, PathString} from '../../src/types';
import {MockFileSystem} from './mock_file_system';
export class MockFileSystemPosix extends MockFileSystem {
resolve(...paths: string[]): AbsoluteFsPath {
const resolved = p.posix.resolve(this.pwd(), ...paths);
return this.normalize(resolved) as AbsoluteFsPath;
}
dirname<T extends string>(file: T): T { return this.normalize(p.posix.dirname(file)) as T; }
join<T extends string>(basePath: T, ...paths: string[]): T {
return this.normalize(p.posix.join(basePath, ...paths)) as T;
}
relative<T extends PathString>(from: T, to: T): PathSegment {
return this.normalize(p.posix.relative(from, to)) as PathSegment;
}
basename(filePath: string, extension?: string): PathSegment {
return p.posix.basename(filePath, extension) as PathSegment;
}
isRooted(path: string): boolean { return path.startsWith('/'); }
protected splitPath<T extends PathString>(path: T): string[] { return path.split('/'); }
normalize<T extends PathString>(path: T): T {
return path.replace(/^[a-z]:\//i, '/').replace(/\\/g, '/') as T;
}
}

View File

@ -0,0 +1,41 @@
/**
* @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
*/
/// <reference types="node" />
import * as p from 'path';
import {AbsoluteFsPath, PathSegment, PathString} from '../../src/types';
import {MockFileSystem} from './mock_file_system';
export class MockFileSystemWindows extends MockFileSystem {
resolve(...paths: string[]): AbsoluteFsPath {
const resolved = p.win32.resolve(this.pwd(), ...paths);
return this.normalize(resolved as AbsoluteFsPath);
}
dirname<T extends string>(path: T): T { return this.normalize(p.win32.dirname(path) as T); }
join<T extends string>(basePath: T, ...paths: string[]): T {
return this.normalize(p.win32.join(basePath, ...paths)) as T;
}
relative<T extends PathString>(from: T, to: T): PathSegment {
return this.normalize(p.win32.relative(from, to)) as PathSegment;
}
basename(filePath: string, extension?: string): PathSegment {
return p.win32.basename(filePath, extension) as PathSegment;
}
isRooted(path: string): boolean { return /^([A-Z]:)?([\\\/]|$)/i.test(path); }
protected splitPath<T extends PathString>(path: T): string[] { return path.split(/[\\\/]/); }
normalize<T extends PathString>(path: T): T {
return path.replace(/^[\/\\]/i, 'C:/').replace(/\\/g, '/') as T;
}
}

View File

@ -0,0 +1,149 @@
/**
* @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
*/
/// <reference types="jasmine"/>
import * as ts from 'typescript';
import {absoluteFrom, setFileSystem} from '../../src/helpers';
import {AbsoluteFsPath} from '../../src/types';
import {MockFileSystem} from './mock_file_system';
import {MockFileSystemNative} from './mock_file_system_native';
import {MockFileSystemPosix} from './mock_file_system_posix';
import {MockFileSystemWindows} from './mock_file_system_windows';
export interface TestFile {
name: AbsoluteFsPath;
contents: string;
isRoot?: boolean|undefined;
}
export interface RunInEachFileSystemFn {
(callback: (os: string) => void): void;
windows(callback: (os: string) => void): void;
unix(callback: (os: string) => void): void;
native(callback: (os: string) => void): void;
osX(callback: (os: string) => void): void;
}
const FS_NATIVE = 'Native';
const FS_OS_X = 'OS/X';
const FS_UNIX = 'Unix';
const FS_WINDOWS = 'Windows';
const FS_ALL = [FS_OS_X, FS_WINDOWS, FS_UNIX, FS_NATIVE];
function runInEachFileSystemFn(callback: (os: string) => void) {
FS_ALL.forEach(os => runInFileSystem(os, callback, false));
}
function runInFileSystem(os: string, callback: (os: string) => void, error: boolean) {
describe(`<<FileSystem: ${os}>>`, () => {
beforeEach(() => initMockFileSystem(os));
callback(os);
if (error) {
afterAll(() => { throw new Error(`runInFileSystem limited to ${os}, cannot pass`); });
}
});
}
export const runInEachFileSystem: RunInEachFileSystemFn =
runInEachFileSystemFn as RunInEachFileSystemFn;
runInEachFileSystem.native = (callback: (os: string) => void) =>
runInFileSystem(FS_NATIVE, callback, true);
runInEachFileSystem.osX = (callback: (os: string) => void) =>
runInFileSystem(FS_OS_X, callback, true);
runInEachFileSystem.unix = (callback: (os: string) => void) =>
runInFileSystem(FS_UNIX, callback, true);
runInEachFileSystem.windows = (callback: (os: string) => void) =>
runInFileSystem(FS_WINDOWS, callback, true);
export function initMockFileSystem(os: string, cwd?: AbsoluteFsPath): void {
const fs = createMockFileSystem(os, cwd);
setFileSystem(fs);
monkeyPatchTypeScript(os, fs);
}
function createMockFileSystem(os: string, cwd?: AbsoluteFsPath): MockFileSystem {
switch (os) {
case 'OS/X':
return new MockFileSystemPosix(/* isCaseSensitive */ false, cwd);
case 'Unix':
return new MockFileSystemPosix(/* isCaseSensitive */ true, cwd);
case 'Windows':
return new MockFileSystemWindows(/* isCaseSensitive*/ false, cwd);
case 'Native':
return new MockFileSystemNative(cwd);
default:
throw new Error('FileSystem not supported');
}
}
function monkeyPatchTypeScript(os: string, fs: MockFileSystem) {
ts.sys.directoryExists = path => {
const absPath = fs.resolve(path);
return fs.exists(absPath) && fs.stat(absPath).isDirectory();
};
ts.sys.fileExists = path => {
const absPath = fs.resolve(path);
return fs.exists(absPath) && fs.stat(absPath).isFile();
};
ts.sys.getCurrentDirectory = () => fs.pwd();
ts.sys.getDirectories = getDirectories;
ts.sys.readFile = fs.readFile.bind(fs);
ts.sys.resolvePath = fs.resolve.bind(fs);
ts.sys.writeFile = fs.writeFile.bind(fs);
ts.sys.readDirectory = readDirectory;
function getDirectories(path: string): string[] {
return fs.readdir(absoluteFrom(path)).filter(p => fs.stat(fs.resolve(path, p)).isDirectory());
}
function getFileSystemEntries(path: string): FileSystemEntries {
const files: string[] = [];
const directories: string[] = [];
const absPath = fs.resolve(path);
const entries = fs.readdir(absPath);
for (const entry of entries) {
if (entry == '.' || entry === '..') {
continue;
}
const absPath = fs.resolve(path, entry);
const stat = fs.stat(absPath);
if (stat.isDirectory()) {
directories.push(absPath);
} else if (stat.isFile()) {
files.push(absPath);
}
}
return {files, directories};
}
function realPath(path: string): string { return fs.realpath(fs.resolve(path)); }
// Rather than completely re-implementing we are using the `ts.matchFiles` function,
// which is internal to the `ts` namespace.
const tsMatchFiles: (
path: string, extensions: ReadonlyArray<string>| undefined,
excludes: ReadonlyArray<string>| undefined, includes: ReadonlyArray<string>| undefined,
useCaseSensitiveFileNames: boolean, currentDirectory: string, depth: number | undefined,
getFileSystemEntries: (path: string) => FileSystemEntries,
realpath: (path: string) => string) => string[] = (ts as any).matchFiles;
function readDirectory(
path: string, extensions?: ReadonlyArray<string>, excludes?: ReadonlyArray<string>,
includes?: ReadonlyArray<string>, depth?: number): string[] {
return tsMatchFiles(
path, extensions, excludes, includes, fs.isCaseSensitive(), fs.pwd(), depth,
getFileSystemEntries, realPath);
}
}
interface FileSystemEntries {
readonly files: ReadonlyArray<string>;
readonly directories: ReadonlyArray<string>;
}