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

@ -5,19 +5,13 @@
* 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 '../../../src/ngtsc/path';
import {makeProgram} from '../../../src/ngtsc/testing/in_memory_typescript';
import {BundleProgram} from '../../src/packages/bundle_program';
import {AbsoluteFsPath, NgtscCompilerHost, absoluteFrom, getFileSystem} from '../../../src/ngtsc/file_system';
import {TestFile} from '../../../src/ngtsc/file_system/testing';
import {BundleProgram, makeBundleProgram} from '../../src/packages/bundle_program';
import {EntryPointFormat, EntryPointJsonProperty} from '../../src/packages/entry_point';
import {EntryPointBundle} from '../../src/packages/entry_point_bundle';
import {patchTsGetExpandoInitializer, restoreGetExpandoInitializer} from '../../src/packages/patch_ts_expando_initializer';
import {Folder} from './mock_file_system';
import {NgccSourcesCompilerHost} from '../../src/packages/ngcc_compiler_host';
export {getDeclaration} from '../../../src/ngtsc/testing/in_memory_typescript';
const _ = AbsoluteFsPath.fromUnchecked;
/**
*
* @param format The format of the bundle.
@ -26,86 +20,31 @@ const _ = AbsoluteFsPath.fromUnchecked;
*/
export function makeTestEntryPointBundle(
formatProperty: EntryPointJsonProperty, format: EntryPointFormat, isCore: boolean,
files: {name: string, contents: string, isRoot?: boolean}[],
dtsFiles?: {name: string, contents: string, isRoot?: boolean}[]): EntryPointBundle {
const src = makeTestBundleProgram(files);
const dts = dtsFiles ? makeTestBundleProgram(dtsFiles) : null;
srcRootNames: AbsoluteFsPath[], dtsRootNames?: AbsoluteFsPath[]): EntryPointBundle {
const src = makeTestBundleProgram(srcRootNames[0], isCore);
const dts = dtsRootNames ? makeTestDtsBundleProgram(dtsRootNames[0], isCore) : null;
const isFlatCore = isCore && src.r3SymbolsFile === null;
return {formatProperty, format, rootDirs: [_('/')], src, dts, isCore, isFlatCore};
return {formatProperty, format, rootDirs: [absoluteFrom('/')], src, dts, isCore, isFlatCore};
}
/**
* Create a bundle program for testing.
* @param files The source files of the bundle program.
*/
export function makeTestBundleProgram(files: {name: string, contents: string}[]): BundleProgram {
const {program, options, host} = makeTestProgramInternal(...files);
const path = _(files[0].name);
const file = program.getSourceFile(path) !;
const r3SymbolsInfo = files.find(file => file.name.indexOf('r3_symbols') !== -1) || null;
const r3SymbolsPath = r3SymbolsInfo && _(r3SymbolsInfo.name);
const r3SymbolsFile = r3SymbolsPath && program.getSourceFile(r3SymbolsPath) || null;
return {program, options, host, path, file, r3SymbolsPath, r3SymbolsFile};
export function makeTestBundleProgram(
path: AbsoluteFsPath, isCore: boolean = false): BundleProgram {
const fs = getFileSystem();
const options = {allowJs: true, checkJs: false};
const entryPointPath = fs.dirname(path);
const host = new NgccSourcesCompilerHost(fs, options, entryPointPath);
return makeBundleProgram(fs, isCore, path, 'r3_symbols.js', options, host);
}
function makeTestProgramInternal(
...files: {name: string, contents: string, isRoot?: boolean | undefined}[]): {
program: ts.Program,
host: ts.CompilerHost,
options: ts.CompilerOptions,
} {
const originalTsGetExpandoInitializer = patchTsGetExpandoInitializer();
const program =
makeProgram([getFakeCore(), getFakeTslib(), ...files], {allowJs: true, checkJs: false});
restoreGetExpandoInitializer(originalTsGetExpandoInitializer);
return program;
export function makeTestDtsBundleProgram(
path: AbsoluteFsPath, isCore: boolean = false): BundleProgram {
const fs = getFileSystem();
const options = {};
const host = new NgtscCompilerHost(fs, options);
return makeBundleProgram(fs, isCore, path, 'r3_symbols.d.ts', options, host);
}
export function makeTestProgram(
...files: {name: string, contents: string, isRoot?: boolean | undefined}[]): ts.Program {
return makeTestProgramInternal(...files).program;
}
// TODO: unify this with the //packages/compiler-cli/test/ngtsc/fake_core package
export function getFakeCore() {
return {
name: 'node_modules/@angular/core/index.d.ts',
contents: `
type FnWithArg<T> = (arg?: any) => T;
export declare const Component: FnWithArg<(clazz: any) => any>;
export declare const Directive: FnWithArg<(clazz: any) => any>;
export declare const Injectable: FnWithArg<(clazz: any) => any>;
export declare const NgModule: FnWithArg<(clazz: any) => any>;
export declare const Input: any;
export declare const Inject: FnWithArg<(a: any, b: any, c: any) => void>;
export declare const Self: FnWithArg<(a: any, b: any, c: any) => void>;
export declare const SkipSelf: FnWithArg<(a: any, b: any, c: any) => void>;
export declare const Optional: FnWithArg<(a: any, b: any, c: any) => void>;
export declare class InjectionToken {
constructor(name: string);
}
export declare interface ModuleWithProviders<T = any> {}
`
};
}
export function getFakeTslib() {
return {
name: 'node_modules/tslib/index.d.ts',
contents: `
export declare function __decorate(decorators: any[], target: any, key?: string | symbol, desc?: any);
export declare function __param(paramIndex: number, decorator: any);
export declare function __metadata(metadataKey: any, metadataValue: any);
`
};
}
export function convertToDirectTsLibImport(filesystem: {name: string, contents: string}[]) {
export function convertToDirectTsLibImport(filesystem: TestFile[]) {
return filesystem.map(file => {
const contents =
file.contents
@ -117,10 +56,6 @@ export function convertToDirectTsLibImport(filesystem: {name: string, contents:
});
}
export function createFileSystemFromProgramFiles(
...fileCollections: ({name: string, contents: string}[] | undefined)[]): Folder {
const folder: Folder = {};
fileCollections.forEach(
files => files && files.forEach(file => folder[file.name] = file.contents));
return folder;
export function getRootFiles(testFiles: TestFile[]): AbsoluteFsPath[] {
return testFiles.filter(f => f.isRoot !== false).map(f => absoluteFrom(f.name));
}