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
This commit is contained in:
parent
aaa16f286d
commit
7316212c1e
|
@ -22,7 +22,9 @@ import {performWatchCompilation, createPerformWatchHost} from './perform_watch'
|
||||||
|
|
||||||
export function main(
|
export function main(
|
||||||
args: string[], consoleError: (s: string) => void = console.error,
|
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} =
|
let {project, rootNames, options, errors: configErrors, watch, emitFlags} =
|
||||||
config || readNgcCommandLineAndConfiguration(args);
|
config || readNgcCommandLineAndConfiguration(args);
|
||||||
if (configErrors.length) {
|
if (configErrors.length) {
|
||||||
|
@ -32,12 +34,22 @@ export function main(
|
||||||
const result = watchMode(project, options, consoleError);
|
const result = watchMode(project, options, consoleError);
|
||||||
return reportErrorsAndExit(result.firstCompileResult, 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,
|
rootNames,
|
||||||
options,
|
options,
|
||||||
emitFlags,
|
emitFlags,
|
||||||
|
oldProgram,
|
||||||
emitCallback: createEmitCallback(options), customTransformers
|
emitCallback: createEmitCallback(options), customTransformers
|
||||||
});
|
});
|
||||||
|
if (programReuse !== undefined) {
|
||||||
|
programReuse.program = program;
|
||||||
|
}
|
||||||
return reportErrorsAndExit(compileDiags, options, consoleError);
|
return reportErrorsAndExit(compileDiags, options, consoleError);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,19 +21,18 @@ const NODE_MODULES_PACKAGE_NAME = /node_modules\/((\w|-|\.)+|(@(\w|-|\.)+\/(\w|-
|
||||||
const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/;
|
const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/;
|
||||||
const CSS_PREPROCESSOR_EXT = /(\.scss|\.less|\.styl)$/;
|
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 {
|
export function setWrapHostForTest(wrapFn: ((host: ts.CompilerHost) => ts.CompilerHost) | null):
|
||||||
augmentHostForTest = augmentation;
|
void {
|
||||||
|
wrapHostForTest = wrapFn;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createCompilerHost(
|
export function createCompilerHost(
|
||||||
{options, tsHost = ts.createCompilerHost(options, true)}:
|
{options, tsHost = ts.createCompilerHost(options, true)}:
|
||||||
{options: CompilerOptions, tsHost?: ts.CompilerHost}): CompilerHost {
|
{options: CompilerOptions, tsHost?: ts.CompilerHost}): CompilerHost {
|
||||||
if (augmentHostForTest !== null) {
|
if (wrapHostForTest !== null) {
|
||||||
for (const name of Object.keys(augmentHostForTest)) {
|
tsHost = wrapHostForTest(tsHost);
|
||||||
(tsHost as any)[name] = augmentHostForTest[name];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return tsHost;
|
return tsHost;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,8 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {CustomTransformers} from '@angular/compiler-cli';
|
import {CustomTransformers, Program} from '@angular/compiler-cli';
|
||||||
import {setAugmentHostForTest} from '@angular/compiler-cli/src/transformers/compiler_host';
|
import {setWrapHostForTest} from '@angular/compiler-cli/src/transformers/compiler_host';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
@ -37,6 +37,9 @@ function setupFakeCore(support: TestSupport): void {
|
||||||
* TypeScript code.
|
* TypeScript code.
|
||||||
*/
|
*/
|
||||||
export class NgtscTestEnvironment {
|
export class NgtscTestEnvironment {
|
||||||
|
private multiCompileHostExt: MultiCompileHostExt|null = null;
|
||||||
|
private oldProgram: Program|null = null;
|
||||||
|
|
||||||
private constructor(private support: TestSupport, readonly outDir: string) {}
|
private constructor(private support: TestSupport, readonly outDir: string) {}
|
||||||
|
|
||||||
get basePath(): string { return this.support.basePath; }
|
get basePath(): string { return this.support.basePath; }
|
||||||
|
@ -50,7 +53,7 @@ export class NgtscTestEnvironment {
|
||||||
process.chdir(support.basePath);
|
process.chdir(support.basePath);
|
||||||
|
|
||||||
setupFakeCore(support);
|
setupFakeCore(support);
|
||||||
setAugmentHostForTest(null);
|
setWrapHostForTest(null);
|
||||||
|
|
||||||
const env = new NgtscTestEnvironment(support, outDir);
|
const env = new NgtscTestEnvironment(support, outDir);
|
||||||
|
|
||||||
|
@ -74,7 +77,10 @@ export class NgtscTestEnvironment {
|
||||||
},
|
},
|
||||||
"angularCompilerOptions": {
|
"angularCompilerOptions": {
|
||||||
"enableIvy": true
|
"enableIvy": true
|
||||||
}
|
},
|
||||||
|
"exclude": [
|
||||||
|
"built"
|
||||||
|
]
|
||||||
}`);
|
}`);
|
||||||
|
|
||||||
return env;
|
return env;
|
||||||
|
@ -98,7 +104,47 @@ export class NgtscTestEnvironment {
|
||||||
return fs.readFileSync(modulePath, 'utf8');
|
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<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 {
|
tsconfig(extraOpts: {[key: string]: string | boolean} = {}, extraRootDirs?: string[]): void {
|
||||||
const tsconfig: {[key: string]: any} = {
|
const tsconfig: {[key: string]: any} = {
|
||||||
|
@ -113,12 +159,7 @@ export class NgtscTestEnvironment {
|
||||||
this.write('tsconfig.json', JSON.stringify(tsconfig, null, 2));
|
this.write('tsconfig.json', JSON.stringify(tsconfig, null, 2));
|
||||||
|
|
||||||
if (extraOpts['_useHostForImportGeneration'] === true) {
|
if (extraOpts['_useHostForImportGeneration'] === true) {
|
||||||
const cwd = process.cwd();
|
setWrapHostForTest(makeWrapHost(new FileNameToModuleNameHost()));
|
||||||
setAugmentHostForTest({
|
|
||||||
fileNameToModuleName: (importedFilePath: string) => {
|
|
||||||
return 'root' + importedFilePath.substr(cwd.length).replace(/(\.d)?.ts$/, '');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,9 +168,19 @@ export class NgtscTestEnvironment {
|
||||||
*/
|
*/
|
||||||
driveMain(customTransformers?: CustomTransformers): void {
|
driveMain(customTransformers?: CustomTransformers): void {
|
||||||
const errorSpy = jasmine.createSpy('consoleError').and.callFake(console.error);
|
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(errorSpy).not.toHaveBeenCalled();
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
|
if (this.multiCompileHostExt !== null) {
|
||||||
|
this.oldProgram = reuseProgram !.program !;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -147,3 +198,65 @@ export class NgtscTestEnvironment {
|
||||||
return program.listLazyRoutes(entryPoint);
|
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];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue