angular/packages/compiler-cli/src/ngtsc/tsc_plugin.ts
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

151 lines
5.5 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 * as ts from 'typescript';
import {CompilationTicket, freshCompilationTicket, incrementalFromDriverTicket, NgCompiler, NgCompilerHost} from './core';
import {NgCompilerOptions, UnifiedModulesHost} from './core/api';
import {NodeJSFileSystem, setFileSystem} from './file_system';
import {PatchedProgramIncrementalBuildStrategy} from './incremental';
import {NOOP_PERF_RECORDER} from './perf';
import {untagAllTsFiles} from './shims';
import {OptimizeFor} from './typecheck/api';
import {ReusedProgramStrategy} from './typecheck/src/augmented_program';
// The following is needed to fix a the chicken-and-egg issue where the sync (into g3) script will
// refuse to accept this file unless the following string appears:
// import * as plugin from '@bazel/typescript/internal/tsc_wrapped/plugin_api';
/**
* A `ts.CompilerHost` which also returns a list of input files, out of which the `ts.Program`
* should be created.
*
* Currently mirrored from @bazel/typescript/internal/tsc_wrapped/plugin_api (with the naming of
* `fileNameToModuleName` corrected).
*/
interface PluginCompilerHost extends ts.CompilerHost, Partial<UnifiedModulesHost> {
readonly inputFiles: ReadonlyArray<string>;
}
/**
* Mirrors the plugin interface from tsc_wrapped which is currently under active development. To
* enable progress to be made in parallel, the upstream interface isn't implemented directly.
* Instead, `TscPlugin` here is structurally assignable to what tsc_wrapped expects.
*/
interface TscPlugin {
readonly name: string;
wrapHost(
host: ts.CompilerHost&Partial<UnifiedModulesHost>, inputFiles: ReadonlyArray<string>,
options: ts.CompilerOptions): PluginCompilerHost;
setupCompilation(program: ts.Program, oldProgram?: ts.Program): {
ignoreForDiagnostics: Set<ts.SourceFile>,
ignoreForEmit: Set<ts.SourceFile>,
};
getDiagnostics(file?: ts.SourceFile): ts.Diagnostic[];
getOptionDiagnostics(): ts.Diagnostic[];
getNextProgram(): ts.Program;
createTransformers(): ts.CustomTransformers;
}
/**
* A plugin for `tsc_wrapped` which allows Angular compilation from a plain `ts_library`.
*/
export class NgTscPlugin implements TscPlugin {
name = 'ngtsc';
private options: NgCompilerOptions|null = null;
private host: NgCompilerHost|null = null;
private _compiler: NgCompiler|null = null;
get compiler(): NgCompiler {
if (this._compiler === null) {
throw new Error('Lifecycle error: setupCompilation() must be called first.');
}
return this._compiler;
}
constructor(private ngOptions: {}) {
setFileSystem(new NodeJSFileSystem());
}
wrapHost(
host: ts.CompilerHost&Partial<UnifiedModulesHost>, inputFiles: readonly string[],
options: ts.CompilerOptions): PluginCompilerHost {
// TODO(alxhub): Eventually the `wrapHost()` API will accept the old `ts.Program` (if one is
// available). When it does, its `ts.SourceFile`s need to be re-tagged to enable proper
// incremental compilation.
this.options = {...this.ngOptions, ...options} as NgCompilerOptions;
this.host = NgCompilerHost.wrap(host, inputFiles, this.options, /* oldProgram */ null);
return this.host;
}
setupCompilation(program: ts.Program, oldProgram?: ts.Program): {
ignoreForDiagnostics: Set<ts.SourceFile>,
ignoreForEmit: Set<ts.SourceFile>,
} {
if (this.host === null || this.options === null) {
throw new Error('Lifecycle error: setupCompilation() before wrapHost().');
}
this.host.postProgramCreationCleanup();
untagAllTsFiles(program);
const typeCheckStrategy = new ReusedProgramStrategy(
program, this.host, this.options, this.host.shimExtensionPrefixes);
const strategy = new PatchedProgramIncrementalBuildStrategy();
const oldDriver = oldProgram !== undefined ? strategy.getIncrementalDriver(oldProgram) : null;
let ticket: CompilationTicket;
let modifiedResourceFiles: Set<string>|undefined = undefined;
if (this.host.getModifiedResourceFiles !== undefined) {
modifiedResourceFiles = this.host.getModifiedResourceFiles();
}
if (modifiedResourceFiles === undefined) {
modifiedResourceFiles = new Set<string>();
}
if (oldProgram === undefined || oldDriver === null) {
ticket = freshCompilationTicket(
program, this.options, strategy, typeCheckStrategy,
/* enableTemplateTypeChecker */ false, /* usePoisonedData */ false);
} else {
strategy.toNextBuildStrategy().getIncrementalDriver(oldProgram);
ticket = incrementalFromDriverTicket(
oldProgram, oldDriver, program, this.options, strategy, typeCheckStrategy,
modifiedResourceFiles, false, false);
}
this._compiler = NgCompiler.fromTicket(ticket, this.host, NOOP_PERF_RECORDER);
return {
ignoreForDiagnostics: this._compiler.ignoreForDiagnostics,
ignoreForEmit: this._compiler.ignoreForEmit,
};
}
getDiagnostics(file?: ts.SourceFile): ts.Diagnostic[] {
if (file === undefined) {
return this.compiler.getDiagnostics();
}
return this.compiler.getDiagnosticsForFile(file, OptimizeFor.WholeProgram);
}
getOptionDiagnostics(): ts.Diagnostic[] {
return this.compiler.getOptionDiagnostics();
}
getNextProgram(): ts.Program {
return this.compiler.getNextProgram();
}
createTransformers(): ts.CustomTransformers {
return this.compiler.prepareEmit().transformers;
}
}