Alex Rickabaugh d9ce8a4ab5 feat(ivy): introduce a flag to control template type-checking for Ivy (#29698)
Template type-checking is enabled by default in the View Engine compiler.
The feature in Ivy is not quite ready for this yet, so this flag will
temporarily control whether templates are type-checked in ngtsc.

The goal is to remove this flag after rolling out template type-checking in
google3 in Ivy mode, and making sure the feature is as compatible with the
View Engine implementation as possible.

Initially, the default value of the flag will leave checking disabled.

PR Close #29698
2019-04-19 11:15:25 -07:00

264 lines
8.7 KiB
TypeScript

/**
* @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 {CustomTransformers, Program} from '@angular/compiler-cli';
import {setWrapHostForTest} from '@angular/compiler-cli/src/transformers/compiler_host';
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
import {createCompilerHost, createProgram} from '../../ngtools2';
import {main, mainDiagnosticsForTest, readNgcCommandLineAndConfiguration} from '../../src/main';
import {LazyRoute} from '../../src/ngtsc/routing';
import {resolveNpmTreeArtifact} from '../runfile_helpers';
import {TestSupport, setup} from '../test_support';
function setupFakeCore(support: TestSupport): void {
if (!process.env.TEST_SRCDIR) {
throw new Error('`setupFakeCore` must be run within a Bazel test');
}
const fakeNpmPackageDir =
resolveNpmTreeArtifact('angular/packages/compiler-cli/test/ngtsc/fake_core/npm_package');
const nodeModulesPath = path.join(support.basePath, 'node_modules');
const angularCoreDirectory = path.join(nodeModulesPath, '@angular/core');
fs.symlinkSync(fakeNpmPackageDir, angularCoreDirectory, 'dir');
}
/**
* Manages a temporary testing directory structure and environment for testing ngtsc by feeding it
* TypeScript code.
*/
export class NgtscTestEnvironment {
private multiCompileHostExt: MultiCompileHostExt|null = null;
private oldProgram: Program|null = null;
private constructor(private support: TestSupport, readonly outDir: string) {}
get basePath(): string { return this.support.basePath; }
/**
* Set up a new testing environment.
*/
static setup(): NgtscTestEnvironment {
const support = setup();
const outDir = path.join(support.basePath, 'built');
process.chdir(support.basePath);
setupFakeCore(support);
setWrapHostForTest(null);
const env = new NgtscTestEnvironment(support, outDir);
env.write('tsconfig-base.json', `{
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"noImplicitAny": true,
"strictNullChecks": true,
"outDir": "built",
"rootDir": ".",
"baseUrl": ".",
"declaration": true,
"target": "es5",
"newLine": "lf",
"module": "es2015",
"moduleResolution": "node",
"lib": ["es6", "dom"],
"typeRoots": ["node_modules/@types"]
},
"angularCompilerOptions": {
"enableIvy": true,
"ivyTemplateTypeCheck": false
},
"exclude": [
"built"
]
}`);
return env;
}
assertExists(fileName: string) {
if (!fs.existsSync(path.resolve(this.outDir, fileName))) {
throw new Error(`Expected ${fileName} to be emitted (outDir: ${this.outDir})`);
}
}
assertDoesNotExist(fileName: string) {
if (fs.existsSync(path.resolve(this.outDir, fileName))) {
throw new Error(`Did not expect ${fileName} to be emitted (outDir: ${this.outDir})`);
}
}
getContents(fileName: string): string {
this.assertExists(fileName);
const modulePath = path.resolve(this.outDir, fileName);
return fs.readFileSync(modulePath, 'utf8');
}
enableMultipleCompilations(): void {
this.multiCompileHostExt = new MultiCompileHostExt();
setWrapHostForTest(makeWrapHost(this.multiCompileHostExt));
}
flushWrittenFileTracking(): void {
if (this.multiCompileHostExt === null) {
throw new Error(`Not tracking written files - call enableMultipleCompilations()`);
}
this.multiCompileHostExt.flushWrittenFileTracking();
}
getFilesWrittenSinceLastFlush(): Set<string> {
if (this.multiCompileHostExt === null) {
throw new Error(`Not tracking written files - call enableMultipleCompilations()`);
}
const outDir = path.join(this.support.basePath, 'built');
const writtenFiles = new Set<string>();
this.multiCompileHostExt.getFilesWrittenSinceLastFlush().forEach(rawFile => {
if (rawFile.startsWith(outDir)) {
writtenFiles.add(rawFile.substr(outDir.length));
}
});
return writtenFiles;
}
write(fileName: string, content: string) {
if (this.multiCompileHostExt !== null) {
const absFilePath = path.resolve(this.support.basePath, fileName);
this.multiCompileHostExt.invalidate(absFilePath);
}
this.support.write(fileName, content);
}
invalidateCachedFile(fileName: string): void {
if (this.multiCompileHostExt === null) {
throw new Error(`Not caching files - call enableMultipleCompilations()`);
}
const fullFile = path.join(this.support.basePath, fileName);
this.multiCompileHostExt.invalidate(fullFile);
}
tsconfig(extraOpts: {[key: string]: string | boolean} = {}, extraRootDirs?: string[]): void {
const tsconfig: {[key: string]: any} = {
extends: './tsconfig-base.json',
angularCompilerOptions: {...extraOpts, enableIvy: true},
};
if (extraRootDirs !== undefined) {
tsconfig.compilerOptions = {
rootDirs: ['.', ...extraRootDirs],
};
}
this.write('tsconfig.json', JSON.stringify(tsconfig, null, 2));
if (extraOpts['_useHostForImportGeneration'] === true) {
setWrapHostForTest(makeWrapHost(new FileNameToModuleNameHost()));
}
}
/**
* Run the compiler to completion, and assert that no errors occurred.
*/
driveMain(customTransformers?: CustomTransformers): void {
const errorSpy = jasmine.createSpy('consoleError').and.callFake(console.error);
let reuseProgram: {program: Program | undefined}|undefined = undefined;
if (this.multiCompileHostExt !== null) {
reuseProgram = {
program: this.oldProgram || undefined,
};
}
const exitCode =
main(['-p', this.basePath], errorSpy, undefined, customTransformers, reuseProgram);
expect(errorSpy).not.toHaveBeenCalled();
expect(exitCode).toBe(0);
if (this.multiCompileHostExt !== null) {
this.oldProgram = reuseProgram !.program !;
}
}
/**
* Run the compiler to completion, and return any `ts.Diagnostic` errors that may have occurred.
*/
driveDiagnostics(): ReadonlyArray<ts.Diagnostic> {
// Cast is safe as ngtsc mode only produces ts.Diagnostics.
return mainDiagnosticsForTest(['-p', this.basePath]) as ReadonlyArray<ts.Diagnostic>;
}
driveRoutes(entryPoint?: string): LazyRoute[] {
const {rootNames, options} = readNgcCommandLineAndConfiguration(['-p', this.basePath]);
const host = createCompilerHost({options});
const program = createProgram({rootNames, host, options});
return program.listLazyRoutes(entryPoint);
}
}
class AugmentedCompilerHost {
delegate !: ts.CompilerHost;
}
class FileNameToModuleNameHost extends AugmentedCompilerHost {
// CWD must be initialized lazily as `this.delegate` is not set until later.
private cwd: string|null = null;
fileNameToModuleName(importedFilePath: string): string {
if (this.cwd === null) {
this.cwd = this.delegate.getCurrentDirectory();
}
return 'root' + importedFilePath.substr(this.cwd.length).replace(/(\.d)?.ts$/, '');
}
}
class MultiCompileHostExt extends AugmentedCompilerHost implements Partial<ts.CompilerHost> {
private cache = new Map<string, ts.SourceFile>();
private writtenFiles = new Set<string>();
getSourceFile(
fileName: string, languageVersion: ts.ScriptTarget, onError?: (message: string) => void,
shouldCreateNewSourceFile?: boolean): ts.SourceFile|undefined {
if (this.cache.has(fileName)) {
return this.cache.get(fileName) !;
}
const sf =
this.delegate.getSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile);
if (sf !== undefined) {
this.cache.set(sf.fileName, sf);
}
return sf;
}
flushWrittenFileTracking(): void { this.writtenFiles.clear(); }
writeFile(
fileName: string, data: string, writeByteOrderMark: boolean,
onError: ((message: string) => void)|undefined,
sourceFiles?: ReadonlyArray<ts.SourceFile>): void {
this.delegate.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles);
this.writtenFiles.add(fileName);
}
getFilesWrittenSinceLastFlush(): Set<string> { return this.writtenFiles; }
invalidate(fileName: string): void { this.cache.delete(fileName); }
}
function makeWrapHost(wrapped: AugmentedCompilerHost): (host: ts.CompilerHost) => ts.CompilerHost {
return (delegate) => {
wrapped.delegate = delegate;
return new Proxy(delegate, {
get: (target: ts.CompilerHost, name: string): any => {
if ((wrapped as any)[name] !== undefined) {
return (wrapped as any)[name] !.bind(wrapped);
}
return (target as any)[name];
}
});
};
}