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

@ -9,7 +9,7 @@ ts_library(
]),
deps = [
"//packages:types",
"//packages/compiler-cli/src/ngtsc/path",
"//packages/compiler-cli/src/ngtsc/file_system",
"@npm//@types/node",
"@npm//typescript",
],

View File

@ -5,26 +5,23 @@
* 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 path from 'path';
import {dirname, relative, resolve} from '../../file_system';
const TS_DTS_JS_EXTENSION = /(?:\.d)?\.ts$|\.js$/;
export function relativePathBetween(from: string, to: string): string|null {
let relative = path.posix.relative(path.dirname(from), to).replace(TS_DTS_JS_EXTENSION, '');
let relativePath = relative(dirname(resolve(from)), resolve(to)).replace(TS_DTS_JS_EXTENSION, '');
if (relative === '') {
if (relativePath === '') {
return null;
}
// path.relative() does not include the leading './'.
if (!relative.startsWith('.')) {
relative = `./${relative}`;
if (!relativePath.startsWith('.')) {
relativePath = `./${relativePath}`;
}
return relative;
return relativePath;
}
export function normalizeSeparators(path: string): string {

View File

@ -10,7 +10,7 @@ const TS = /\.tsx?$/i;
const D_TS = /\.d\.ts$/i;
import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../path';
import {AbsoluteFsPath, absoluteFrom} from '../../file_system';
export function isDtsPath(filePath: string): boolean {
return D_TS.test(filePath);
@ -47,6 +47,12 @@ export function getSourceFile(node: ts.Node): ts.SourceFile {
return directSf !== undefined ? directSf : ts.getOriginalNode(node).getSourceFile();
}
export function getSourceFileOrNull(program: ts.Program, fileName: AbsoluteFsPath): ts.SourceFile|
null {
return program.getSourceFile(fileName) || null;
}
export function identifierOfNode(decl: ts.Node & {name?: ts.Node}): ts.Identifier|null {
if (decl.name !== undefined && ts.isIdentifier(decl.name)) {
return decl.name;
@ -83,7 +89,7 @@ export function getRootDirs(host: ts.CompilerHost, options: ts.CompilerOptions):
// See:
// https://github.com/Microsoft/TypeScript/blob/3f7357d37f66c842d70d835bc925ec2a873ecfec/src/compiler/sys.ts#L650
// Also compiler options might be set via an API which doesn't normalize paths
return rootDirs.map(rootDir => AbsoluteFsPath.from(rootDir));
return rootDirs.map(rootDir => absoluteFrom(rootDir));
}
export function nodeDebugInfo(node: ts.Node): string {

View File

@ -10,6 +10,8 @@ ts_library(
]),
deps = [
"//packages:types",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/file_system/testing",
"//packages/compiler-cli/src/ngtsc/testing",
"//packages/compiler-cli/src/ngtsc/util",
"@npm//typescript",

View File

@ -5,10 +5,10 @@
* 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 {makeProgram} from '../../testing/in_memory_typescript';
import {absoluteFrom, getSourceFileOrError} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing';
import {makeProgram} from '../../testing';
import {VisitListEntryResult, Visitor, visit} from '../src/visitor';
class TestAstVisitor extends Visitor {
@ -43,37 +43,41 @@ function testTransformerFactory(context: ts.TransformationContext): ts.Transform
return (file: ts.SourceFile) => visit(file, new TestAstVisitor(), context);
}
describe('AST Visitor', () => {
it('should add a statement before class in plain file', () => {
const {program, host} =
makeProgram([{name: 'main.ts', contents: `class A { static id = 3; }`}]);
const sf = program.getSourceFile('main.ts') !;
program.emit(sf, undefined, undefined, undefined, {before: [testTransformerFactory]});
const main = host.readFile('/main.js');
expect(main).toMatch(/^var A_id = 3;/);
});
runInEachFileSystem(() => {
describe('AST Visitor', () => {
let _: typeof absoluteFrom;
beforeEach(() => _ = absoluteFrom);
it('should add a statement before class inside function definition', () => {
const {program, host} = makeProgram([{
name: 'main.ts',
contents: `
it('should add a statement before class in plain file', () => {
const {program, host} =
makeProgram([{name: _('/main.ts'), contents: `class A { static id = 3; }`}]);
const sf = getSourceFileOrError(program, _('/main.ts'));
program.emit(sf, undefined, undefined, undefined, {before: [testTransformerFactory]});
const main = host.readFile('/main.js');
expect(main).toMatch(/^var A_id = 3;/);
});
it('should add a statement before class inside function definition', () => {
const {program, host} = makeProgram([{
name: _('/main.ts'),
contents: `
export function foo() {
var x = 3;
class A { static id = 2; }
return A;
}
`
}]);
const sf = program.getSourceFile('main.ts') !;
program.emit(sf, undefined, undefined, undefined, {before: [testTransformerFactory]});
const main = host.readFile('/main.js');
expect(main).toMatch(/var x = 3;\s+var A_id = 2;\s+var A =/);
});
}]);
const sf = getSourceFileOrError(program, _('/main.ts'));
program.emit(sf, undefined, undefined, undefined, {before: [testTransformerFactory]});
const main = host.readFile(_('/main.js'));
expect(main).toMatch(/var x = 3;\s+var A_id = 2;\s+var A =/);
});
it('handles nested statements', () => {
const {program, host} = makeProgram([{
name: 'main.ts',
contents: `
it('handles nested statements', () => {
const {program, host} = makeProgram([{
name: _('/main.ts'),
contents: `
export class A {
static id = 3;
@ -84,11 +88,12 @@ describe('AST Visitor', () => {
return B;
}
}`
}]);
const sf = program.getSourceFile('main.ts') !;
program.emit(sf, undefined, undefined, undefined, {before: [testTransformerFactory]});
const main = host.readFile('/main.js');
expect(main).toMatch(/var A_id = 3;\s+var A = /);
expect(main).toMatch(/var B_id = 4;\s+var B = /);
}]);
const sf = getSourceFileOrError(program, _('/main.ts'));
program.emit(sf, undefined, undefined, undefined, {before: [testTransformerFactory]});
const main = host.readFile(_('/main.js'));
expect(main).toMatch(/var A_id = 3;\s+var A = /);
expect(main).toMatch(/var B_id = 4;\s+var B = /);
});
});
});