Alex Rickabaugh 21e24d1474 refactor(compiler-cli): introduce CompilationTicket system for NgCompiler (#40561)
Previously, the incremental flow for NgCompiler was simple: when creating a
new NgCompiler instance, the consumer could pass state from a previous
compilation, which would cause the new compilation to be performed
incrementally. "Local" information about TypeScript files which had not
changed would be passed from the old compilation to the new and reused,
while "global" information would always be recalculated.

However, this flow could be made more efficient in certain cases, such as
when no TypeScript files are changed in a new compilation. In this case,
_all_ information extracted during the first compilation is reusable. Doing
this involves reusing the previous `NgCompiler` instance (the container for
such global information) and updating it, instead of creating a new one for
the next compilation. This approach works cleanly, but complicates the
lifecycle of `NgCompiler`.

To prevent consumers from having to deal with the mechanics of reuse vs
incremental steps of `NgCompiler`, a new `CompilationTicket` mechanism is
added in this commit. Consumers obtain a `CompilationTicket` via one of
several code paths depending on the nature of the incoming compilation, and
use the `CompilationTicket` to obtain an `NgCompiler` instance. This
instance may be a fresh compilation, a new `NgCompiler` for an incremental
compilation, or an existing `NgCompiler` that's been updated to optimally
process a resource-only change. Consumers can use the new `NgCompiler`
without knowledge of its provenance.

PR Close #40561
2021-01-27 10:45:57 -08:00

347 lines
12 KiB
TypeScript

/**
* @license
* Copyright Google LLC 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 {GeneratedFile} from '@angular/compiler';
import * as ts from 'typescript';
import * as api from '../transformers/api';
import {verifySupportedTypeScriptVersion} from '../typescript_support';
import {CompilationTicket, freshCompilationTicket, incrementalFromCompilerTicket, NgCompiler, NgCompilerHost} from './core';
import {NgCompilerOptions} from './core/api';
import {absoluteFrom, AbsoluteFsPath} from './file_system';
import {TrackedIncrementalBuildStrategy} from './incremental';
import {IndexedComponent} from './indexer';
import {NOOP_PERF_RECORDER, PerfRecorder, PerfTracker} from './perf';
import {DeclarationNode} from './reflection';
import {retagAllTsFiles, untagAllTsFiles} from './shims';
import {ReusedProgramStrategy} from './typecheck';
import {OptimizeFor} from './typecheck/api';
/**
* Entrypoint to the Angular Compiler (Ivy+) which sits behind the `api.Program` interface, allowing
* it to be a drop-in replacement for the legacy View Engine compiler to tooling such as the
* command-line main() function or the Angular CLI.
*/
export class NgtscProgram implements api.Program {
readonly compiler: NgCompiler;
/**
* The primary TypeScript program, which is used for analysis and emit.
*/
private tsProgram: ts.Program;
/**
* The TypeScript program to use for the next incremental compilation.
*
* Once a TS program is used to create another (an incremental compilation operation), it can no
* longer be used to do so again.
*
* Since template type-checking uses the primary program to create a type-checking program, after
* this happens the primary program is no longer suitable for starting a subsequent compilation,
* and the template type-checking program should be used instead.
*
* Thus, the program which should be used for the next incremental compilation is tracked in
* `reuseTsProgram`, separately from the "primary" program which is always used for emit.
*/
private reuseTsProgram: ts.Program;
private closureCompilerEnabled: boolean;
private host: NgCompilerHost;
private perfRecorder: PerfRecorder = NOOP_PERF_RECORDER;
private perfTracker: PerfTracker|null = null;
private incrementalStrategy: TrackedIncrementalBuildStrategy;
constructor(
rootNames: ReadonlyArray<string>, private options: NgCompilerOptions,
delegateHost: api.CompilerHost, oldProgram?: NgtscProgram) {
// First, check whether the current TS version is supported.
if (!options.disableTypeScriptVersionCheck) {
verifySupportedTypeScriptVersion();
}
if (options.tracePerformance !== undefined) {
this.perfTracker = PerfTracker.zeroedToNow();
this.perfRecorder = this.perfTracker;
}
this.closureCompilerEnabled = !!options.annotateForClosureCompiler;
const reuseProgram = oldProgram?.reuseTsProgram;
this.host = NgCompilerHost.wrap(delegateHost, rootNames, options, reuseProgram ?? null);
if (reuseProgram !== undefined) {
// Prior to reusing the old program, restore shim tagging for all its `ts.SourceFile`s.
// TypeScript checks the `referencedFiles` of `ts.SourceFile`s for changes when evaluating
// incremental reuse of data from the old program, so it's important that these match in order
// to get the most benefit out of reuse.
retagAllTsFiles(reuseProgram);
}
this.tsProgram = ts.createProgram(this.host.inputFiles, options, this.host, reuseProgram);
this.reuseTsProgram = this.tsProgram;
this.host.postProgramCreationCleanup();
// Shim tagging has served its purpose, and tags can now be removed from all `ts.SourceFile`s in
// the program.
untagAllTsFiles(this.tsProgram);
const reusedProgramStrategy = new ReusedProgramStrategy(
this.tsProgram, this.host, this.options, this.host.shimExtensionPrefixes);
this.incrementalStrategy = oldProgram !== undefined ?
oldProgram.incrementalStrategy.toNextBuildStrategy() :
new TrackedIncrementalBuildStrategy();
const modifiedResourceFiles = new Set<AbsoluteFsPath>();
if (this.host.getModifiedResourceFiles !== undefined) {
const strings = this.host.getModifiedResourceFiles();
if (strings !== undefined) {
for (const fileString of strings) {
modifiedResourceFiles.add(absoluteFrom(fileString));
}
}
}
let ticket: CompilationTicket;
if (oldProgram === undefined) {
ticket = freshCompilationTicket(
this.tsProgram, options, this.incrementalStrategy, reusedProgramStrategy,
/* enableTemplateTypeChecker */ false, /* usePoisonedData */ false);
} else {
ticket = incrementalFromCompilerTicket(
oldProgram.compiler,
this.tsProgram,
this.incrementalStrategy,
reusedProgramStrategy,
modifiedResourceFiles,
);
}
// Create the NgCompiler which will drive the rest of the compilation.
this.compiler = NgCompiler.fromTicket(ticket, this.host, this.perfRecorder);
}
getTsProgram(): ts.Program {
return this.tsProgram;
}
getReuseTsProgram(): ts.Program {
return this.reuseTsProgram;
}
getTsOptionDiagnostics(cancellationToken?: ts.CancellationToken|
undefined): readonly ts.Diagnostic[] {
return this.tsProgram.getOptionsDiagnostics(cancellationToken);
}
getTsSyntacticDiagnostics(
sourceFile?: ts.SourceFile|undefined,
cancellationToken?: ts.CancellationToken|undefined): readonly ts.Diagnostic[] {
const ignoredFiles = this.compiler.ignoreForDiagnostics;
if (sourceFile !== undefined) {
if (ignoredFiles.has(sourceFile)) {
return [];
}
return this.tsProgram.getSyntacticDiagnostics(sourceFile, cancellationToken);
} else {
const diagnostics: ts.Diagnostic[] = [];
for (const sf of this.tsProgram.getSourceFiles()) {
if (!ignoredFiles.has(sf)) {
diagnostics.push(...this.tsProgram.getSyntacticDiagnostics(sf, cancellationToken));
}
}
return diagnostics;
}
}
getTsSemanticDiagnostics(
sourceFile?: ts.SourceFile|undefined,
cancellationToken?: ts.CancellationToken|undefined): readonly ts.Diagnostic[] {
const ignoredFiles = this.compiler.ignoreForDiagnostics;
if (sourceFile !== undefined) {
if (ignoredFiles.has(sourceFile)) {
return [];
}
return this.tsProgram.getSemanticDiagnostics(sourceFile, cancellationToken);
} else {
const diagnostics: ts.Diagnostic[] = [];
for (const sf of this.tsProgram.getSourceFiles()) {
if (!ignoredFiles.has(sf)) {
diagnostics.push(...this.tsProgram.getSemanticDiagnostics(sf, cancellationToken));
}
}
return diagnostics;
}
}
getNgOptionDiagnostics(cancellationToken?: ts.CancellationToken|
undefined): readonly(ts.Diagnostic|api.Diagnostic)[] {
return this.compiler.getOptionDiagnostics();
}
getNgStructuralDiagnostics(cancellationToken?: ts.CancellationToken|
undefined): readonly api.Diagnostic[] {
return [];
}
getNgSemanticDiagnostics(
fileName?: string|undefined, cancellationToken?: ts.CancellationToken|undefined):
readonly(ts.Diagnostic|api.Diagnostic)[] {
let sf: ts.SourceFile|undefined = undefined;
if (fileName !== undefined) {
sf = this.tsProgram.getSourceFile(fileName);
if (sf === undefined) {
// There are no diagnostics for files which don't exist in the program - maybe the caller
// has stale data?
return [];
}
}
const diagnostics = sf === undefined ?
this.compiler.getDiagnostics() :
this.compiler.getDiagnosticsForFile(sf, OptimizeFor.WholeProgram);
this.reuseTsProgram = this.compiler.getNextProgram();
return diagnostics;
}
/**
* Ensure that the `NgCompiler` has properly analyzed the program, and allow for the asynchronous
* loading of any resources during the process.
*
* This is used by the Angular CLI to allow for spawning (async) child compilations for things
* like SASS files used in `styleUrls`.
*/
loadNgStructureAsync(): Promise<void> {
return this.compiler.analyzeAsync();
}
listLazyRoutes(entryRoute?: string|undefined): api.LazyRoute[] {
return this.compiler.listLazyRoutes(entryRoute);
}
emit(opts?: {
emitFlags?: api.EmitFlags|undefined;
cancellationToken?: ts.CancellationToken | undefined;
customTransformers?: api.CustomTransformers | undefined;
emitCallback?: api.TsEmitCallback | undefined;
mergeEmitResultsCallback?: api.TsMergeEmitResultsCallback | undefined;
}|undefined): ts.EmitResult {
const {transformers} = this.compiler.prepareEmit();
const ignoreFiles = this.compiler.ignoreForEmit;
const emitCallback = opts && opts.emitCallback || defaultEmitCallback;
const writeFile: ts.WriteFileCallback =
(fileName: string, data: string, writeByteOrderMark: boolean,
onError: ((message: string) => void)|undefined,
sourceFiles: ReadonlyArray<ts.SourceFile>|undefined) => {
if (sourceFiles !== undefined) {
// Record successful writes for any `ts.SourceFile` (that's not a declaration file)
// that's an input to this write.
for (const writtenSf of sourceFiles) {
if (writtenSf.isDeclarationFile) {
continue;
}
this.compiler.incrementalDriver.recordSuccessfulEmit(writtenSf);
}
}
this.host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles);
};
const customTransforms = opts && opts.customTransformers;
const beforeTransforms = transformers.before || [];
const afterDeclarationsTransforms = transformers.afterDeclarations;
if (customTransforms !== undefined && customTransforms.beforeTs !== undefined) {
beforeTransforms.push(...customTransforms.beforeTs);
}
const emitSpan = this.perfRecorder.start('emit');
const emitResults: ts.EmitResult[] = [];
for (const targetSourceFile of this.tsProgram.getSourceFiles()) {
if (targetSourceFile.isDeclarationFile || ignoreFiles.has(targetSourceFile)) {
continue;
}
if (this.compiler.incrementalDriver.safeToSkipEmit(targetSourceFile)) {
continue;
}
const fileEmitSpan = this.perfRecorder.start('emitFile', targetSourceFile);
emitResults.push(emitCallback({
targetSourceFile,
program: this.tsProgram,
host: this.host,
options: this.options,
emitOnlyDtsFiles: false,
writeFile,
customTransformers: {
before: beforeTransforms,
after: customTransforms && customTransforms.afterTs,
afterDeclarations: afterDeclarationsTransforms,
} as any,
}));
this.perfRecorder.stop(fileEmitSpan);
}
this.perfRecorder.stop(emitSpan);
if (this.perfTracker !== null && this.options.tracePerformance !== undefined) {
this.perfTracker.serializeToFile(this.options.tracePerformance, this.host);
}
// Run the emit, including a custom transformer that will downlevel the Ivy decorators in code.
return ((opts && opts.mergeEmitResultsCallback) || mergeEmitResults)(emitResults);
}
getIndexedComponents(): Map<DeclarationNode, IndexedComponent> {
return this.compiler.getIndexedComponents();
}
getLibrarySummaries(): Map<string, api.LibrarySummary> {
throw new Error('Method not implemented.');
}
getEmittedGeneratedFiles(): Map<string, GeneratedFile> {
throw new Error('Method not implemented.');
}
getEmittedSourceFiles(): Map<string, ts.SourceFile> {
throw new Error('Method not implemented.');
}
}
const defaultEmitCallback: api.TsEmitCallback = ({
program,
targetSourceFile,
writeFile,
cancellationToken,
emitOnlyDtsFiles,
customTransformers
}) =>
program.emit(
targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers);
function mergeEmitResults(emitResults: ts.EmitResult[]): ts.EmitResult {
const diagnostics: ts.Diagnostic[] = [];
let emitSkipped = false;
const emittedFiles: string[] = [];
for (const er of emitResults) {
diagnostics.push(...er.diagnostics);
emitSkipped = emitSkipped || er.emitSkipped;
emittedFiles.push(...(er.emittedFiles || []));
}
return {diagnostics, emitSkipped, emittedFiles};
}