2020-10-09 19:46:55 -04:00
|
|
|
/**
|
|
|
|
* @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
|
|
|
|
*/
|
|
|
|
|
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-19 16:10:43 -05:00
|
|
|
import {CompilationTicket, freshCompilationTicket, incrementalFromCompilerTicket, NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
|
2020-10-09 19:46:55 -04:00
|
|
|
import {NgCompilerOptions} from '@angular/compiler-cli/src/ngtsc/core/api';
|
|
|
|
import {TrackedIncrementalBuildStrategy} from '@angular/compiler-cli/src/ngtsc/incremental';
|
|
|
|
import {TypeCheckingProgramStrategy} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
|
|
|
|
import * as ts from 'typescript/lib/tsserverlibrary';
|
|
|
|
|
2020-10-30 18:16:39 -04:00
|
|
|
import {LanguageServiceAdapter} from './adapters';
|
2020-10-09 14:22:03 -04:00
|
|
|
import {isExternalTemplate} from './utils';
|
2020-10-09 19:46:55 -04:00
|
|
|
|
2020-11-04 15:28:59 -05:00
|
|
|
/**
|
|
|
|
* Manages the `NgCompiler` instance which backs the language service, updating or replacing it as
|
|
|
|
* needed to produce an up-to-date understanding of the current program.
|
|
|
|
*
|
|
|
|
* TODO(alxhub): currently the options used for the compiler are specified at `CompilerFactory`
|
|
|
|
* construction, and are not changable. In a real project, users can update `tsconfig.json`. We need
|
|
|
|
* to properly handle a change in the compiler options, either by having an API to update the
|
|
|
|
* `CompilerFactory` to use new options, or by replacing it entirely.
|
|
|
|
*/
|
2020-10-09 19:46:55 -04:00
|
|
|
export class CompilerFactory {
|
|
|
|
private readonly incrementalStrategy = new TrackedIncrementalBuildStrategy();
|
|
|
|
private compiler: NgCompiler|null = null;
|
|
|
|
private lastKnownProgram: ts.Program|null = null;
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
private readonly adapter: LanguageServiceAdapter,
|
|
|
|
private readonly programStrategy: TypeCheckingProgramStrategy,
|
2020-11-04 15:28:59 -05:00
|
|
|
private readonly options: NgCompilerOptions,
|
2020-10-09 19:46:55 -04:00
|
|
|
) {}
|
|
|
|
|
2020-11-04 15:28:59 -05:00
|
|
|
getOrCreate(): NgCompiler {
|
2020-10-09 19:46:55 -04:00
|
|
|
const program = this.programStrategy.getProgram();
|
2020-11-04 15:28:59 -05:00
|
|
|
if (this.compiler === null || program !== this.lastKnownProgram) {
|
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-19 16:10:43 -05:00
|
|
|
let ticket: CompilationTicket;
|
|
|
|
if (this.compiler === null || this.lastKnownProgram === null) {
|
|
|
|
ticket = freshCompilationTicket(
|
|
|
|
program, this.options, this.incrementalStrategy, this.programStrategy, true, true);
|
|
|
|
} else {
|
|
|
|
ticket = incrementalFromCompilerTicket(
|
|
|
|
this.compiler, program, this.incrementalStrategy, this.programStrategy, new Set());
|
|
|
|
}
|
|
|
|
this.compiler = NgCompiler.fromTicket(ticket, this.adapter);
|
2020-10-09 19:46:55 -04:00
|
|
|
this.lastKnownProgram = program;
|
|
|
|
}
|
2020-11-04 15:28:59 -05:00
|
|
|
return this.compiler;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a new instance of the Ivy compiler if the program has changed since
|
|
|
|
* the last time the compiler was instantiated. If the program has not changed,
|
|
|
|
* return the existing instance.
|
|
|
|
* @param fileName override the template if this is an external template file
|
|
|
|
* @param options angular compiler options
|
|
|
|
*/
|
|
|
|
getOrCreateWithChangedFile(fileName: string): NgCompiler {
|
|
|
|
const compiler = this.getOrCreate();
|
2020-10-09 19:46:55 -04:00
|
|
|
if (isExternalTemplate(fileName)) {
|
2020-11-04 15:28:59 -05:00
|
|
|
this.overrideTemplate(fileName, compiler);
|
2020-10-09 19:46:55 -04:00
|
|
|
}
|
2020-11-04 15:28:59 -05:00
|
|
|
return compiler;
|
2020-10-09 19:46:55 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
private overrideTemplate(fileName: string, compiler: NgCompiler) {
|
|
|
|
if (!this.adapter.isTemplateDirty(fileName)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// 1. Get the latest snapshot
|
|
|
|
const latestTemplate = this.adapter.readResource(fileName);
|
|
|
|
// 2. Find all components that use the template
|
|
|
|
const ttc = compiler.getTemplateTypeChecker();
|
|
|
|
const components = compiler.getComponentsWithTemplateFile(fileName);
|
|
|
|
// 3. Update component template
|
|
|
|
for (const component of components) {
|
|
|
|
if (ts.isClassDeclaration(component)) {
|
|
|
|
ttc.overrideComponentTemplate(component, latestTemplate);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
registerLastKnownProgram() {
|
|
|
|
this.lastKnownProgram = this.programStrategy.getProgram();
|
|
|
|
}
|
|
|
|
}
|