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
347 lines
12 KiB
TypeScript
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};
|
|
}
|