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

@ -10,7 +10,7 @@ ts_library(
deps = [
"//packages:types",
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/path",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/util",
"@npm//@types/node",

View File

@ -5,19 +5,15 @@
* 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 {Expression, ExternalExpr, WrappedNodeExpr} from '@angular/compiler';
import {ExternalReference} from '@angular/compiler/src/compiler';
import * as ts from 'typescript';
import {LogicalFileSystem, LogicalProjectPath} from '../../path';
import {LogicalFileSystem, LogicalProjectPath, absoluteFrom} from '../../file_system';
import {ReflectionHost} from '../../reflection';
import {getSourceFile, isDeclaration, nodeNameForError, resolveModuleName} from '../../util/src/typescript';
import {getSourceFile, getSourceFileOrNull, isDeclaration, nodeNameForError, resolveModuleName} from '../../util/src/typescript';
import {findExportedNameOfNode} from './find_export';
import {ImportMode, Reference} from './references';
/**
* A host which supports an operation to convert a file name into a module name.
*
@ -170,8 +166,9 @@ export class AbsoluteModuleStrategy implements ReferenceEmitStrategy {
return null;
}
const entryPointFile = this.program.getSourceFile(resolvedModule.resolvedFileName);
if (entryPointFile === undefined) {
const entryPointFile =
getSourceFileOrNull(this.program, absoluteFrom(resolvedModule.resolvedFileName));
if (entryPointFile === null) {
return null;
}

View File

@ -5,10 +5,9 @@
* 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 {resolveModuleName} from '../../util/src/typescript';
import {absoluteFrom} from '../../file_system';
import {getSourceFileOrNull, resolveModuleName} from '../../util/src/typescript';
import {Reference} from './references';
export interface ReferenceResolver {
@ -33,6 +32,6 @@ export class ModuleResolver {
if (resolved === undefined) {
return null;
}
return this.program.getSourceFile(resolved.resolvedFileName) || null;
return getSourceFileOrNull(this.program, absoluteFrom(resolved.resolvedFileName));
}
}

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/imports",
"//packages/compiler-cli/src/ngtsc/testing",
"@npm//typescript",

View File

@ -5,86 +5,101 @@
* 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 {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
import {absoluteFrom} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing';
import {getDeclaration, makeProgram} from '../../testing';
import {DefaultImportTracker} from '../src/default';
describe('DefaultImportTracker', () => {
it('should prevent a default import from being elided if used', () => {
const {program, host} = makeProgram(
[
{name: 'dep.ts', contents: `export default class Foo {}`},
{name: 'test.ts', contents: `import Foo from './dep'; export function test(f: Foo) {}`},
runInEachFileSystem(() => {
describe('DefaultImportTracker', () => {
let _: typeof absoluteFrom;
beforeEach(() => _ = absoluteFrom);
// This control file is identical to the test file, but will not have its import marked
// for preservation. It exists to verify that it is in fact the action of
// DefaultImportTracker and not some other artifact of the test setup which causes the
// import to be preserved. It will also verify that DefaultImportTracker does not preserve
// imports which are not marked for preservation.
{name: 'ctrl.ts', contents: `import Foo from './dep'; export function test(f: Foo) {}`},
],
{
module: ts.ModuleKind.ES2015,
});
const fooClause = getDeclaration(program, 'test.ts', 'Foo', ts.isImportClause);
const fooId = fooClause.name !;
const fooDecl = fooClause.parent;
it('should prevent a default import from being elided if used', () => {
const {program, host} = makeProgram(
[
{name: _('/dep.ts'), contents: `export default class Foo {}`},
{
name: _('/test.ts'),
contents: `import Foo from './dep'; export function test(f: Foo) {}`
},
const tracker = new DefaultImportTracker();
tracker.recordImportedIdentifier(fooId, fooDecl);
tracker.recordUsedIdentifier(fooId);
program.emit(undefined, undefined, undefined, undefined, {
before: [tracker.importPreservingTransformer()],
// This control file is identical to the test file, but will not have its import marked
// for preservation. It exists to verify that it is in fact the action of
// DefaultImportTracker and not some other artifact of the test setup which causes the
// import to be preserved. It will also verify that DefaultImportTracker does not
// preserve imports which are not marked for preservation.
{
name: _('/ctrl.ts'),
contents: `import Foo from './dep'; export function test(f: Foo) {}`
},
],
{
module: ts.ModuleKind.ES2015,
});
const fooClause = getDeclaration(program, _('/test.ts'), 'Foo', ts.isImportClause);
const fooId = fooClause.name !;
const fooDecl = fooClause.parent;
const tracker = new DefaultImportTracker();
tracker.recordImportedIdentifier(fooId, fooDecl);
tracker.recordUsedIdentifier(fooId);
program.emit(undefined, undefined, undefined, undefined, {
before: [tracker.importPreservingTransformer()],
});
const testContents = host.readFile('/test.js') !;
expect(testContents).toContain(`import Foo from './dep';`);
// The control should have the import elided.
const ctrlContents = host.readFile('/ctrl.js');
expect(ctrlContents).not.toContain(`import Foo from './dep';`);
});
const testContents = host.readFile('/test.js') !;
expect(testContents).toContain(`import Foo from './dep';`);
// The control should have the import elided.
const ctrlContents = host.readFile('/ctrl.js');
expect(ctrlContents).not.toContain(`import Foo from './dep';`);
it('should transpile imports correctly into commonjs', () => {
const {program, host} = makeProgram(
[
{name: _('/dep.ts'), contents: `export default class Foo {}`},
{
name: _('/test.ts'),
contents: `import Foo from './dep'; export function test(f: Foo) {}`
},
],
{
module: ts.ModuleKind.CommonJS,
});
const fooClause = getDeclaration(program, _('/test.ts'), 'Foo', ts.isImportClause);
const fooId = ts.updateIdentifier(fooClause.name !);
const fooDecl = fooClause.parent;
const tracker = new DefaultImportTracker();
tracker.recordImportedIdentifier(fooId, fooDecl);
tracker.recordUsedIdentifier(fooId);
program.emit(undefined, undefined, undefined, undefined, {
before: [
addReferenceTransformer(fooId),
tracker.importPreservingTransformer(),
],
});
const testContents = host.readFile('/test.js') !;
expect(testContents).toContain(`var dep_1 = require("./dep");`);
expect(testContents).toContain(`var ref = dep_1["default"];`);
});
});
it('should transpile imports correctly into commonjs', () => {
const {program, host} = makeProgram(
[
{name: 'dep.ts', contents: `export default class Foo {}`},
{name: 'test.ts', contents: `import Foo from './dep'; export function test(f: Foo) {}`},
],
{
module: ts.ModuleKind.CommonJS,
});
const fooClause = getDeclaration(program, 'test.ts', 'Foo', ts.isImportClause);
const fooId = ts.updateIdentifier(fooClause.name !);
const fooDecl = fooClause.parent;
const tracker = new DefaultImportTracker();
tracker.recordImportedIdentifier(fooId, fooDecl);
tracker.recordUsedIdentifier(fooId);
program.emit(undefined, undefined, undefined, undefined, {
before: [
addReferenceTransformer(fooId),
tracker.importPreservingTransformer(),
],
});
const testContents = host.readFile('/test.js') !;
expect(testContents).toContain(`var dep_1 = require("./dep");`);
expect(testContents).toContain(`var ref = dep_1["default"];`);
});
});
function addReferenceTransformer(id: ts.Identifier): ts.TransformerFactory<ts.SourceFile> {
return (context: ts.TransformationContext) => {
return (sf: ts.SourceFile) => {
if (id.getSourceFile().fileName === sf.fileName) {
return ts.updateSourceFileNode(sf, [
...sf.statements, ts.createVariableStatement(undefined, ts.createVariableDeclarationList([
ts.createVariableDeclaration('ref', undefined, id),
]))
]);
}
return sf;
function addReferenceTransformer(id: ts.Identifier): ts.TransformerFactory<ts.SourceFile> {
return (context: ts.TransformationContext) => {
return (sf: ts.SourceFile) => {
if (id.getSourceFile().fileName === sf.fileName) {
return ts.updateSourceFileNode(sf, [
...sf.statements,
ts.createVariableStatement(undefined, ts.createVariableDeclarationList([
ts.createVariableDeclaration('ref', undefined, id),
]))
]);
}
return sf;
};
};
};
}
}
});