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:

committed by
Kara Erickson

parent
1e7e065423
commit
7186f9c016
15
packages/compiler-cli/src/ngtsc/file_system/BUILD.bazel
Normal file
15
packages/compiler-cli/src/ngtsc/file_system/BUILD.bazel
Normal 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",
|
||||
],
|
||||
)
|
42
packages/compiler-cli/src/ngtsc/file_system/README.md
Normal file
42
packages/compiler-cli/src/ngtsc/file_system/README.md
Normal 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.
|
13
packages/compiler-cli/src/ngtsc/file_system/index.ts
Normal file
13
packages/compiler-cli/src/ngtsc/file_system/index.ts
Normal 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';
|
@ -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);
|
||||
}
|
||||
}
|
88
packages/compiler-cli/src/ngtsc/file_system/src/helpers.ts
Normal file
88
packages/compiler-cli/src/ngtsc/file_system/src/helpers.ts
Normal 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;
|
||||
}
|
@ -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.');
|
||||
}
|
102
packages/compiler-cli/src/ngtsc/file_system/src/logical.ts
Normal file
102
packages/compiler-cli/src/ngtsc/file_system/src/logical.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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()));
|
||||
}
|
74
packages/compiler-cli/src/ngtsc/file_system/src/types.ts
Normal file
74
packages/compiler-cli/src/ngtsc/file_system/src/types.ts
Normal 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;
|
||||
}
|
35
packages/compiler-cli/src/ngtsc/file_system/src/util.ts
Normal file
35
packages/compiler-cli/src/ngtsc/file_system/src/util.ts
Normal 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;
|
||||
}
|
25
packages/compiler-cli/src/ngtsc/file_system/test/BUILD.bazel
Normal file
25
packages/compiler-cli/src/ngtsc/file_system/test/BUILD.bazel
Normal 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",
|
||||
],
|
||||
)
|
@ -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(); });
|
||||
}
|
||||
});
|
||||
});
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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]);
|
||||
});
|
||||
});
|
||||
});
|
@ -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",
|
||||
],
|
||||
)
|
13
packages/compiler-cli/src/ngtsc/file_system/testing/index.ts
Normal file
13
packages/compiler-cli/src/ngtsc/file_system/testing/index.ts
Normal 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';
|
@ -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;
|
||||
}
|
@ -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(/[\\\/]/); }
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>;
|
||||
}
|
Reference in New Issue
Block a user