From 7316212c1e923f0617f4ebf3d1b3e18414c92716 Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Mon, 18 Mar 2019 11:21:29 -0700 Subject: [PATCH] test(ivy): support multiple compilations in the ngtsc test env (#29380) This commit adds support for compiling the same program repeatedly in a way that's similar to how incremental builds work in a tool such as the CLI. * support is added to the compiler entrypoint for reuse of the Program object between compilations. This is the basis of the compiler's incremental compilation model. * support is added to wrap the CompilerHost the compiler creates and cache ts.SourceFiles in between compilations. * support is added to track when files are emitted, for assertion purposes. * an 'exclude' section is added to the base tsconfig to prevent .d.ts outputs from the first compilation from becoming inputs to any subsequent compilations. PR Close #29380 --- packages/compiler-cli/src/main.ts | 16 +- .../src/transformers/compiler_host.ts | 13 +- packages/compiler-cli/test/ngtsc/env.ts | 137 ++++++++++++++++-- 3 files changed, 145 insertions(+), 21 deletions(-) diff --git a/packages/compiler-cli/src/main.ts b/packages/compiler-cli/src/main.ts index ff84617083..2c77b31adc 100644 --- a/packages/compiler-cli/src/main.ts +++ b/packages/compiler-cli/src/main.ts @@ -22,7 +22,9 @@ import {performWatchCompilation, createPerformWatchHost} from './perform_watch' export function main( args: string[], consoleError: (s: string) => void = console.error, - config?: NgcParsedConfiguration, customTransformers?: api.CustomTransformers): number { + config?: NgcParsedConfiguration, customTransformers?: api.CustomTransformers, programReuse?: { + program: api.Program | undefined, + }): number { let {project, rootNames, options, errors: configErrors, watch, emitFlags} = config || readNgcCommandLineAndConfiguration(args); if (configErrors.length) { @@ -32,12 +34,22 @@ export function main( const result = watchMode(project, options, consoleError); return reportErrorsAndExit(result.firstCompileResult, options, consoleError); } - const {diagnostics: compileDiags} = performCompilation({ + + let oldProgram: api.Program|undefined; + if (programReuse !== undefined) { + oldProgram = programReuse.program; + } + + const {diagnostics: compileDiags, program} = performCompilation({ rootNames, options, emitFlags, + oldProgram, emitCallback: createEmitCallback(options), customTransformers }); + if (programReuse !== undefined) { + programReuse.program = program; + } return reportErrorsAndExit(compileDiags, options, consoleError); } diff --git a/packages/compiler-cli/src/transformers/compiler_host.ts b/packages/compiler-cli/src/transformers/compiler_host.ts index 988e4a486e..a4f16c3548 100644 --- a/packages/compiler-cli/src/transformers/compiler_host.ts +++ b/packages/compiler-cli/src/transformers/compiler_host.ts @@ -21,19 +21,18 @@ const NODE_MODULES_PACKAGE_NAME = /node_modules\/((\w|-|\.)+|(@(\w|-|\.)+\/(\w|- const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/; const CSS_PREPROCESSOR_EXT = /(\.scss|\.less|\.styl)$/; -let augmentHostForTest: {[name: string]: Function}|null = null; +let wrapHostForTest: ((host: ts.CompilerHost) => ts.CompilerHost)|null = null; -export function setAugmentHostForTest(augmentation: {[name: string]: Function} | null): void { - augmentHostForTest = augmentation; +export function setWrapHostForTest(wrapFn: ((host: ts.CompilerHost) => ts.CompilerHost) | null): + void { + wrapHostForTest = wrapFn; } export function createCompilerHost( {options, tsHost = ts.createCompilerHost(options, true)}: {options: CompilerOptions, tsHost?: ts.CompilerHost}): CompilerHost { - if (augmentHostForTest !== null) { - for (const name of Object.keys(augmentHostForTest)) { - (tsHost as any)[name] = augmentHostForTest[name]; - } + if (wrapHostForTest !== null) { + tsHost = wrapHostForTest(tsHost); } return tsHost; } diff --git a/packages/compiler-cli/test/ngtsc/env.ts b/packages/compiler-cli/test/ngtsc/env.ts index 6ce40e673c..3cdc5d7dc2 100644 --- a/packages/compiler-cli/test/ngtsc/env.ts +++ b/packages/compiler-cli/test/ngtsc/env.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {CustomTransformers} from '@angular/compiler-cli'; -import {setAugmentHostForTest} from '@angular/compiler-cli/src/transformers/compiler_host'; +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'; @@ -37,6 +37,9 @@ function setupFakeCore(support: TestSupport): void { * 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; } @@ -50,7 +53,7 @@ export class NgtscTestEnvironment { process.chdir(support.basePath); setupFakeCore(support); - setAugmentHostForTest(null); + setWrapHostForTest(null); const env = new NgtscTestEnvironment(support, outDir); @@ -74,7 +77,10 @@ export class NgtscTestEnvironment { }, "angularCompilerOptions": { "enableIvy": true - } + }, + "exclude": [ + "built" + ] }`); return env; @@ -98,7 +104,47 @@ export class NgtscTestEnvironment { return fs.readFileSync(modulePath, 'utf8'); } - write(fileName: string, content: string) { this.support.write(fileName, content); } + 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 { + 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(); + 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} = { @@ -113,12 +159,7 @@ export class NgtscTestEnvironment { this.write('tsconfig.json', JSON.stringify(tsconfig, null, 2)); if (extraOpts['_useHostForImportGeneration'] === true) { - const cwd = process.cwd(); - setAugmentHostForTest({ - fileNameToModuleName: (importedFilePath: string) => { - return 'root' + importedFilePath.substr(cwd.length).replace(/(\.d)?.ts$/, ''); - } - }); + setWrapHostForTest(makeWrapHost(new FileNameToModuleNameHost())); } } @@ -127,9 +168,19 @@ export class NgtscTestEnvironment { */ driveMain(customTransformers?: CustomTransformers): void { const errorSpy = jasmine.createSpy('consoleError').and.callFake(console.error); - const exitCode = main(['-p', this.basePath], errorSpy, undefined, customTransformers); + 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 !; + } } /** @@ -147,3 +198,65 @@ export class NgtscTestEnvironment { 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 { + private cache = new Map(); + private writtenFiles = new Set(); + + 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): void { + this.delegate.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles); + this.writtenFiles.add(fileName); + } + + getFilesWrittenSinceLastFlush(): Set { 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]; + } + }); + }; +}