diff --git a/packages/compiler-cli/ngcc/src/analysis/ngcc_trait_compiler.ts b/packages/compiler-cli/ngcc/src/analysis/ngcc_trait_compiler.ts index e5dde6cbf5..2edd9dc737 100644 --- a/packages/compiler-cli/ngcc/src/analysis/ngcc_trait_compiler.ts +++ b/packages/compiler-cli/ngcc/src/analysis/ngcc_trait_compiler.ts @@ -85,7 +85,7 @@ export class NgccTraitCompiler extends TraitCompiler { } class NoIncrementalBuild implements IncrementalBuild { - priorWorkFor(sf: ts.SourceFile): any[]|null { + priorAnalysisFor(sf: ts.SourceFile): any[]|null { return null; } diff --git a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts index 6a0ebb4a13..1c312c5fd1 100644 --- a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts +++ b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts @@ -15,7 +15,7 @@ import {COMPILER_ERRORS_WITH_GUIDES, ERROR_DETAILS_PAGE_BASE_URL, ErrorCode, ngE import {checkForPrivateExports, ReferenceGraph} from '../../entry_point'; import {AbsoluteFsPath, LogicalFileSystem, resolve} from '../../file_system'; import {AbsoluteModuleStrategy, AliasingHost, AliasStrategy, DefaultImportTracker, ImportRewriter, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NoopImportRewriter, PrivateExportAliasingHost, R3SymbolsImportRewriter, Reference, ReferenceEmitStrategy, ReferenceEmitter, RelativePathStrategy, UnifiedModulesAliasingHost, UnifiedModulesStrategy} from '../../imports'; -import {IncrementalBuildStrategy, IncrementalDriver} from '../../incremental'; +import {IncrementalBuildStrategy, IncrementalCompilation, IncrementalState} from '../../incremental'; import {SemanticSymbol} from '../../incremental/semantic_graph'; import {generateAnalysis, IndexedComponent, IndexingContext} from '../../indexer'; import {ComponentResources, CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry, MetadataReader, ResourceRegistry} from '../../metadata'; @@ -89,11 +89,10 @@ export interface FreshCompilationTicket { export interface IncrementalTypeScriptCompilationTicket { kind: CompilationTicketKind.IncrementalTypeScript; options: NgCompilerOptions; - oldProgram: ts.Program; newProgram: ts.Program; incrementalBuildStrategy: IncrementalBuildStrategy; + incrementalCompilation: IncrementalCompilation; programDriver: ProgramDriver; - newDriver: IncrementalDriver; enableTemplateTypeChecker: boolean; usePoisonedData: boolean; perfRecorder: ActivePerfRecorder; @@ -143,10 +142,11 @@ export function freshCompilationTicket( export function incrementalFromCompilerTicket( oldCompiler: NgCompiler, newProgram: ts.Program, incrementalBuildStrategy: IncrementalBuildStrategy, programDriver: ProgramDriver, - modifiedResourceFiles: Set, perfRecorder: ActivePerfRecorder|null): CompilationTicket { + modifiedResourceFiles: Set, + perfRecorder: ActivePerfRecorder|null): CompilationTicket { const oldProgram = oldCompiler.getCurrentProgram(); - const oldDriver = oldCompiler.incrementalStrategy.getIncrementalDriver(oldProgram); - if (oldDriver === null) { + const oldState = oldCompiler.incrementalStrategy.getIncrementalState(oldProgram); + if (oldState === null) { // No incremental step is possible here, since no IncrementalDriver was found for the old // program. return freshCompilationTicket( @@ -158,8 +158,8 @@ export function incrementalFromCompilerTicket( perfRecorder = ActivePerfRecorder.zeroedToNow(); } - const newDriver = IncrementalDriver.reconcile( - oldProgram, oldDriver, newProgram, modifiedResourceFiles, perfRecorder); + const incrementalCompilation = IncrementalCompilation.incremental( + newProgram, oldProgram, oldState, modifiedResourceFiles, perfRecorder); return { kind: CompilationTicketKind.IncrementalTypeScript, @@ -167,9 +167,8 @@ export function incrementalFromCompilerTicket( usePoisonedData: oldCompiler.usePoisonedData, options: oldCompiler.options, incrementalBuildStrategy, + incrementalCompilation, programDriver, - newDriver, - oldProgram, newProgram, perfRecorder, }; @@ -179,24 +178,23 @@ export function incrementalFromCompilerTicket( * Create a `CompilationTicket` directly from an old `ts.Program` and associated Angular compilation * state, along with a new `ts.Program`. */ -export function incrementalFromDriverTicket( - oldProgram: ts.Program, oldDriver: IncrementalDriver, newProgram: ts.Program, +export function incrementalFromStateTicket( + oldProgram: ts.Program, oldState: IncrementalState, newProgram: ts.Program, options: NgCompilerOptions, incrementalBuildStrategy: IncrementalBuildStrategy, - programDriver: ProgramDriver, modifiedResourceFiles: Set, + programDriver: ProgramDriver, modifiedResourceFiles: Set, perfRecorder: ActivePerfRecorder|null, enableTemplateTypeChecker: boolean, usePoisonedData: boolean): CompilationTicket { if (perfRecorder === null) { perfRecorder = ActivePerfRecorder.zeroedToNow(); } - const newDriver = IncrementalDriver.reconcile( - oldProgram, oldDriver, newProgram, modifiedResourceFiles, perfRecorder); + const incrementalCompilation = IncrementalCompilation.incremental( + newProgram, oldProgram, oldState, modifiedResourceFiles, perfRecorder); return { kind: CompilationTicketKind.IncrementalTypeScript, - oldProgram, newProgram, options, incrementalBuildStrategy, - newDriver, + incrementalCompilation, programDriver, enableTemplateTypeChecker, usePoisonedData, @@ -284,7 +282,7 @@ export class NgCompiler { ticket.tsProgram, ticket.programDriver, ticket.incrementalBuildStrategy, - IncrementalDriver.fresh(ticket.tsProgram), + IncrementalCompilation.fresh(ticket.tsProgram), ticket.enableTemplateTypeChecker, ticket.usePoisonedData, ticket.perfRecorder, @@ -296,7 +294,7 @@ export class NgCompiler { ticket.newProgram, ticket.programDriver, ticket.incrementalBuildStrategy, - ticket.newDriver, + ticket.incrementalCompilation, ticket.enableTemplateTypeChecker, ticket.usePoisonedData, ticket.perfRecorder, @@ -314,7 +312,7 @@ export class NgCompiler { private inputProgram: ts.Program, readonly programDriver: ProgramDriver, readonly incrementalStrategy: IncrementalBuildStrategy, - readonly incrementalDriver: IncrementalDriver, + readonly incrementalCompilation: IncrementalCompilation, readonly enableTemplateTypeChecker: boolean, readonly usePoisonedData: boolean, private livePerfRecorder: ActivePerfRecorder, @@ -343,7 +341,7 @@ export class NgCompiler { this.resourceManager = new AdapterResourceLoader(adapter, this.options); this.cycleAnalyzer = new CycleAnalyzer( new ImportGraph(inputProgram.getTypeChecker(), this.delegatingPerfRecorder)); - this.incrementalStrategy.setIncrementalDriver(this.incrementalDriver, inputProgram); + this.incrementalStrategy.setIncrementalState(this.incrementalCompilation.state, inputProgram); this.ignoreForDiagnostics = new Set(inputProgram.getSourceFiles().filter(sf => this.adapter.isShim(sf))); @@ -367,6 +365,16 @@ export class NgCompiler { return this.livePerfRecorder; } + /** + * Exposes the `IncrementalCompilation` under an old property name that the CLI uses, avoiding a + * chicken-and-egg problem with the rename to `incrementalCompilation`. + * + * TODO(alxhub): remove when the CLI uses the new name. + */ + get incrementalDriver(): IncrementalCompilation { + return this.incrementalCompilation; + } + private updateWithChangedResources( changedResources: Set, perfRecorder: ActivePerfRecorder): void { this.livePerfRecorder = perfRecorder; @@ -411,7 +419,7 @@ export class NgCompiler { getResourceDependencies(file: ts.SourceFile): string[] { this.ensureAnalyzed(); - return this.incrementalDriver.depGraph.getResourceDependencies(file); + return this.incrementalCompilation.depGraph.getResourceDependencies(file); } /** @@ -684,7 +692,7 @@ export class NgCompiler { // At this point, analysis is complete and the compiler can now calculate which files need to // be emitted, so do that. - this.incrementalDriver.recordSuccessfulAnalysis(traitCompiler); + this.incrementalCompilation.recordSuccessfulAnalysis(traitCompiler); this.perfRecorder.memory(PerfCheckpoint.Resolve); }); @@ -829,7 +837,7 @@ export class NgCompiler { } const program = this.programDriver.getProgram(); - this.incrementalStrategy.setIncrementalDriver(this.incrementalDriver, program); + this.incrementalStrategy.setIncrementalState(this.incrementalCompilation.state, program); this.currentProgram = program; return diagnostics; @@ -846,7 +854,7 @@ export class NgCompiler { } const program = this.programDriver.getProgram(); - this.incrementalStrategy.setIncrementalDriver(this.incrementalDriver, program); + this.incrementalStrategy.setIncrementalState(this.incrementalCompilation.state, program); this.currentProgram = program; return diagnostics; @@ -935,7 +943,8 @@ export class NgCompiler { aliasingHost = new UnifiedModulesAliasingHost(this.adapter.unifiedModulesHost); } - const evaluator = new PartialEvaluator(reflector, checker, this.incrementalDriver.depGraph); + const evaluator = + new PartialEvaluator(reflector, checker, this.incrementalCompilation.depGraph); const dtsReader = new DtsMetadataReader(checker, reflector); const localMetaRegistry = new LocalMetadataRegistry(); const localMetaReader: MetadataReader = localMetaRegistry; @@ -943,7 +952,7 @@ export class NgCompiler { const scopeRegistry = new LocalModuleScopeRegistry(localMetaReader, depScopeReader, refEmitter, aliasingHost); const scopeReader: ComponentScopeReader = scopeRegistry; - const semanticDepGraphUpdater = this.incrementalDriver.getSemanticDepGraphUpdater(); + const semanticDepGraphUpdater = this.incrementalCompilation.semanticDepGraphUpdater; const metaRegistry = new CompoundMetadataRegistry([localMetaRegistry, scopeRegistry]); const injectableRegistry = new InjectableClassRegistry(reflector); @@ -992,8 +1001,9 @@ export class NgCompiler { this.options.i18nUseExternalIds !== false, this.options.enableI18nLegacyMessageIdFormat !== false, this.usePoisonedData, this.options.i18nNormalizeLineEndingsInICUs, this.moduleResolver, this.cycleAnalyzer, - cycleHandlingStrategy, refEmitter, this.incrementalDriver.depGraph, injectableRegistry, - semanticDepGraphUpdater, this.closureCompilerEnabled, this.delegatingPerfRecorder), + cycleHandlingStrategy, refEmitter, this.incrementalCompilation.depGraph, + injectableRegistry, semanticDepGraphUpdater, this.closureCompilerEnabled, + this.delegatingPerfRecorder), // TODO(alxhub): understand why the cast here is necessary (something to do with `null` // not being assignable to `unknown` when wrapped in `Readonly`). @@ -1020,7 +1030,7 @@ export class NgCompiler { ]; const traitCompiler = new TraitCompiler( - handlers, reflector, this.delegatingPerfRecorder, this.incrementalDriver, + handlers, reflector, this.delegatingPerfRecorder, this.incrementalCompilation, this.options.compileNonExportedClasses !== false, compilationMode, dtsTransforms, semanticDepGraphUpdater); @@ -1028,13 +1038,13 @@ export class NgCompiler { // happens, they need to be tracked by the `NgCompiler`. const notifyingDriver = new NotifyingProgramDriverWrapper(this.programDriver, (program: ts.Program) => { - this.incrementalStrategy.setIncrementalDriver(this.incrementalDriver, program); + this.incrementalStrategy.setIncrementalState(this.incrementalCompilation.state, program); this.currentProgram = program; }); const templateTypeChecker = new TemplateTypeCheckerImpl( this.inputProgram, notifyingDriver, traitCompiler, this.getTypeCheckingConfig(), refEmitter, - reflector, this.adapter, this.incrementalDriver, scopeRegistry, typeCheckScopeRegistry, + reflector, this.adapter, this.incrementalCompilation, scopeRegistry, typeCheckScopeRegistry, this.delegatingPerfRecorder); return { diff --git a/packages/compiler-cli/src/ngtsc/incremental/api.ts b/packages/compiler-cli/src/ngtsc/incremental/api.ts index 0f3ca34258..5a52c8627e 100644 --- a/packages/compiler-cli/src/ngtsc/incremental/api.ts +++ b/packages/compiler-cli/src/ngtsc/incremental/api.ts @@ -21,7 +21,7 @@ export interface IncrementalBuild { /** * Retrieve the prior analysis work, if any, done for the given source file. */ - priorWorkFor(sf: ts.SourceFile): AnalysisT[]|null; + priorAnalysisFor(sf: ts.SourceFile): AnalysisT[]|null; /** * Retrieve the prior type-checking work, if any, that's been done for the given source file. diff --git a/packages/compiler-cli/src/ngtsc/incremental/index.ts b/packages/compiler-cli/src/ngtsc/incremental/index.ts index 887ea6ede7..8bcda25025 100644 --- a/packages/compiler-cli/src/ngtsc/incremental/index.ts +++ b/packages/compiler-cli/src/ngtsc/incremental/index.ts @@ -6,6 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ +export {IncrementalCompilation} from './src/incremental'; export {NOOP_INCREMENTAL_BUILD} from './src/noop'; -export {IncrementalDriver} from './src/state'; +export {AnalyzedIncrementalState, DeltaIncrementalState, FreshIncrementalState, IncrementalState, IncrementalStateKind} from './src/state'; + export * from './src/strategy'; diff --git a/packages/compiler-cli/src/ngtsc/incremental/src/dependency_tracking.ts b/packages/compiler-cli/src/ngtsc/incremental/src/dependency_tracking.ts index 0dded69344..63a557da16 100644 --- a/packages/compiler-cli/src/ngtsc/incremental/src/dependency_tracking.ts +++ b/packages/compiler-cli/src/ngtsc/incremental/src/dependency_tracking.ts @@ -8,7 +8,7 @@ import * as ts from 'typescript'; -import {AbsoluteFsPath} from '../../file_system'; +import {absoluteFromSourceFile, AbsoluteFsPath} from '../../file_system'; import {DependencyTracker} from '../api'; /** @@ -28,7 +28,7 @@ export class FileDependencyGraph i private nodes = new Map(); addDependency(from: T, on: T): void { - this.nodeFor(from).dependsOn.add(on.fileName); + this.nodeFor(from).dependsOn.add(absoluteFromSourceFile(on)); } addResourceDependency(from: T, resource: AbsoluteFsPath): void { @@ -67,15 +67,17 @@ export class FileDependencyGraph i * P(n) = the physically changed files from build n - 1 to build n. */ updateWithPhysicalChanges( - previous: FileDependencyGraph, changedTsPaths: Set, deletedTsPaths: Set, - changedResources: Set): Set { - const logicallyChanged = new Set(); + previous: FileDependencyGraph, changedTsPaths: Set, + deletedTsPaths: Set, + changedResources: Set): Set { + const logicallyChanged = new Set(); for (const sf of previous.nodes.keys()) { + const sfPath = absoluteFromSourceFile(sf); const node = previous.nodeFor(sf); if (isLogicallyChanged(sf, node, changedTsPaths, deletedTsPaths, changedResources)) { - logicallyChanged.add(sf.fileName); - } else if (!deletedTsPaths.has(sf.fileName)) { + logicallyChanged.add(sfPath); + } else if (!deletedTsPaths.has(sfPath)) { this.nodes.set(sf, { dependsOn: new Set(node.dependsOn), usesResources: new Set(node.usesResources), @@ -90,7 +92,7 @@ export class FileDependencyGraph i private nodeFor(sf: T): FileNode { if (!this.nodes.has(sf)) { this.nodes.set(sf, { - dependsOn: new Set(), + dependsOn: new Set(), usesResources: new Set(), failedAnalysis: false, }); @@ -104,7 +106,8 @@ export class FileDependencyGraph i * changed files and resources. */ function isLogicallyChanged( - sf: T, node: FileNode, changedTsPaths: ReadonlySet, deletedTsPaths: ReadonlySet, + sf: T, node: FileNode, changedTsPaths: ReadonlySet, + deletedTsPaths: ReadonlySet, changedResources: ReadonlySet): boolean { // A file is assumed to have logically changed if its dependencies could not be determined // accurately. @@ -112,8 +115,10 @@ function isLogicallyChanged( return true; } + const sfPath = absoluteFromSourceFile(sf); + // A file is logically changed if it has physically changed itself (including being deleted). - if (changedTsPaths.has(sf.fileName) || deletedTsPaths.has(sf.fileName)) { + if (changedTsPaths.has(sfPath) || deletedTsPaths.has(sfPath)) { return true; } @@ -134,7 +139,7 @@ function isLogicallyChanged( } interface FileNode { - dependsOn: Set; + dependsOn: Set; usesResources: Set; failedAnalysis: boolean; } diff --git a/packages/compiler-cli/src/ngtsc/incremental/src/incremental.ts b/packages/compiler-cli/src/ngtsc/incremental/src/incremental.ts new file mode 100644 index 0000000000..42fc222137 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/incremental/src/incremental.ts @@ -0,0 +1,348 @@ +/** + * @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 {absoluteFromSourceFile, AbsoluteFsPath, resolve} from '../../file_system'; +import {PerfPhase, PerfRecorder} from '../../perf'; +import {ClassRecord, TraitCompiler} from '../../transform'; +import {FileTypeCheckingData} from '../../typecheck'; +import {toUnredirectedSourceFile} from '../../util/src/typescript'; +import {IncrementalBuild} from '../api'; +import {SemanticDepGraphUpdater} from '../semantic_graph'; +import {FileDependencyGraph} from './dependency_tracking'; +import {AnalyzedIncrementalState, DeltaIncrementalState, IncrementalState, IncrementalStateKind} from './state'; + +/** + * Information about the previous compilation being used as a starting point for the current one, + * including the delta of files which have logically changed and need to be reanalyzed. + */ +interface IncrementalStep { + priorState: AnalyzedIncrementalState; + logicallyChangedTsFiles: Set; +} + +/** + * Discriminant of the `Phase` type union. + */ +enum PhaseKind { + Analysis, + TypeCheckAndEmit, +} + +/** + * An incremental compilation undergoing analysis, and building a semantic dependency graph. + */ +interface AnalysisPhase { + kind: PhaseKind.Analysis; + semanticDepGraphUpdater: SemanticDepGraphUpdater; +} + +/** + * An incremental compilation that completed analysis and is undergoing template type-checking and + * emit. + */ +interface TypeCheckAndEmitPhase { + kind: PhaseKind.TypeCheckAndEmit; + needsEmit: Set; + needsTypeCheckEmit: Set; +} + +/** + * Represents the current phase of a compilation. + */ +type Phase = AnalysisPhase|TypeCheckAndEmitPhase; + +/** + * Manages the incremental portion of an Angular compilation, allowing for reuse of a prior + * compilation if available, and producing an output state for reuse of the current compilation in a + * future one. + */ +export class IncrementalCompilation implements IncrementalBuild { + private phase: Phase; + + /** + * `IncrementalState` of this compilation if it were to be reused in a subsequent incremental + * compilation at the current moment. + * + * Exposed via the `state` read-only getter. + */ + private _state: IncrementalState; + + private constructor( + state: IncrementalState, readonly depGraph: FileDependencyGraph, + private step: IncrementalStep|null) { + this._state = state; + + // The compilation begins in analysis phase. + this.phase = { + kind: PhaseKind.Analysis, + semanticDepGraphUpdater: + new SemanticDepGraphUpdater(step !== null ? step.priorState.semanticDepGraph : null), + }; + } + + /** + * Begin a fresh `IncrementalCompilation`. + */ + static fresh(program: ts.Program): IncrementalCompilation { + const state: IncrementalState = { + kind: IncrementalStateKind.Fresh, + }; + return new IncrementalCompilation(state, new FileDependencyGraph(), /* reuse */ null); + } + + static incremental( + program: ts.Program, oldProgram: ts.Program, oldState: IncrementalState, + modifiedResourceFiles: Set|null, perf: PerfRecorder): IncrementalCompilation { + return perf.inPhase(PerfPhase.Reconciliation, () => { + let priorAnalysis: AnalyzedIncrementalState; + const physicallyChangedTsFiles = new Set(); + const changedResourceFiles = new Set(modifiedResourceFiles ?? []); + + switch (oldState.kind) { + case IncrementalStateKind.Fresh: + // Since this line of program has never been successfully analyzed to begin with, treat + // this as a fresh compilation. + return IncrementalCompilation.fresh(program); + case IncrementalStateKind.Analyzed: + // The most recent program was analyzed successfully, so we can use that as our prior + // state and don't need to consider any other deltas except changes in the most recent + // program. + priorAnalysis = oldState; + break; + case IncrementalStateKind.Delta: + // There is an ancestor program which was analyzed successfully and can be used as a + // starting point, but we need to determine what's changed since that program. + priorAnalysis = oldState.lastAnalyzedState; + for (const sfPath of oldState.physicallyChangedTsFiles) { + physicallyChangedTsFiles.add(sfPath); + } + for (const resourcePath of oldState.changedResourceFiles) { + changedResourceFiles.add(resourcePath); + } + break; + } + + const oldFilesArray = oldProgram.getSourceFiles().map(sf => toUnredirectedSourceFile(sf)); + const oldFiles = new Set(oldFilesArray); + const deletedTsFiles = new Set(oldFilesArray.map(sf => absoluteFromSourceFile(sf))); + + for (const possiblyRedirectedNewFile of program.getSourceFiles()) { + const sf = toUnredirectedSourceFile(possiblyRedirectedNewFile); + const sfPath = absoluteFromSourceFile(sf); + // Since we're seeing a file in the incoming program with this name, it can't have been + // deleted. + deletedTsFiles.delete(sfPath); + + if (oldFiles.has(sf)) { + // This file hasn't changed. + continue; + } + + // Bail out if a .d.ts file changes - the semantic dep graph is not able to process such + // changes correctly yet. + if (sf.isDeclarationFile) { + return IncrementalCompilation.fresh(program); + } + + // The file has changed physically, so record it. + physicallyChangedTsFiles.add(sfPath); + } + + // Remove any files that have been deleted from the list of physical changes. + for (const deletedFileName of deletedTsFiles) { + physicallyChangedTsFiles.delete(resolve(deletedFileName)); + } + + // Use the prior dependency graph to project physical changes into a set of logically changed + // files. + const depGraph = new FileDependencyGraph(); + const logicallyChangedTsFiles = depGraph.updateWithPhysicalChanges( + priorAnalysis.depGraph, physicallyChangedTsFiles, deletedTsFiles, changedResourceFiles); + + // Physically changed files aren't necessarily counted as logically changed by the dependency + // graph (files do not have edges to themselves), so add them to the logical changes + // explicitly. + for (const sfPath of physicallyChangedTsFiles) { + logicallyChangedTsFiles.add(sfPath); + } + + // Start off in a `DeltaIncrementalState` as a delta against the previous successful analysis, + // until this compilation completes its own analysis. + const state: DeltaIncrementalState = { + kind: IncrementalStateKind.Delta, + physicallyChangedTsFiles, + changedResourceFiles, + lastAnalyzedState: priorAnalysis, + }; + + return new IncrementalCompilation(state, depGraph, { + priorState: priorAnalysis, + logicallyChangedTsFiles, + }); + }); + } + + get state(): IncrementalState { + return this._state; + } + + get semanticDepGraphUpdater(): SemanticDepGraphUpdater { + if (this.phase.kind !== PhaseKind.Analysis) { + throw new Error( + `AssertionError: Cannot update the SemanticDepGraph after analysis completes`); + } + return this.phase.semanticDepGraphUpdater; + } + + recordSuccessfulAnalysis(traitCompiler: TraitCompiler): void { + if (this.phase.kind !== PhaseKind.Analysis) { + throw new Error(`AssertionError: Incremental compilation in phase ${ + PhaseKind[this.phase.kind]}, expected Analysis`); + } + + const {needsEmit, needsTypeCheckEmit, newGraph} = this.phase.semanticDepGraphUpdater.finalize(); + + // Determine the set of files which have already been emitted. + let emitted: Set; + if (this.step === null) { + // Since there is no prior compilation, no files have yet been emitted. + emitted = new Set(); + } else { + // Begin with the files emitted by the prior successful compilation, but remove those which we + // know need to bee re-emitted. + emitted = new Set(this.step.priorState.emitted); + + // Files need re-emitted if they've logically changed. + for (const sfPath of this.step.logicallyChangedTsFiles) { + emitted.delete(sfPath); + } + + // Files need re-emitted if they've semantically changed. + for (const sfPath of needsEmit) { + emitted.delete(sfPath); + } + } + + // Transition to a successfully analyzed compilation. At this point, a subsequent compilation + // could use this state as a starting point. + this._state = { + kind: IncrementalStateKind.Analyzed, + depGraph: this.depGraph, + semanticDepGraph: newGraph, + traitCompiler, + typeCheckResults: null, + emitted, + }; + + // We now enter the type-check and emit phase of compilation. + this.phase = { + kind: PhaseKind.TypeCheckAndEmit, + needsEmit, + needsTypeCheckEmit, + }; + } + + recordSuccessfulTypeCheck(results: Map): void { + if (this._state.kind !== IncrementalStateKind.Analyzed) { + throw new Error(`AssertionError: Expected successfully analyzed compilation.`); + } else if (this.phase.kind !== PhaseKind.TypeCheckAndEmit) { + throw new Error(`AssertionError: Incremental compilation in phase ${ + PhaseKind[this.phase.kind]}, expected TypeCheck`); + } + + this._state.typeCheckResults = results; + } + + + recordSuccessfulEmit(sf: ts.SourceFile): void { + if (this._state.kind !== IncrementalStateKind.Analyzed) { + throw new Error(`AssertionError: Expected successfully analyzed compilation.`); + } + this._state.emitted.add(absoluteFromSourceFile(sf)); + } + + priorAnalysisFor(sf: ts.SourceFile): ClassRecord[]|null { + if (this.step === null) { + return null; + } + + const sfPath = absoluteFromSourceFile(sf); + + // If the file has logically changed, its previous analysis cannot be reused. + if (this.step.logicallyChangedTsFiles.has(sfPath)) { + return null; + } + + return this.step.priorState.traitCompiler.recordsFor(sf); + } + + priorTypeCheckingResultsFor(sf: ts.SourceFile): FileTypeCheckingData|null { + if (this.phase.kind !== PhaseKind.TypeCheckAndEmit) { + throw new Error(`AssertionError: Expected successfully analyzed compilation.`); + } + + if (this.step === null) { + return null; + } + + const sfPath = absoluteFromSourceFile(sf); + + // If the file has logically changed, or its template type-checking results have semantically + // changed, then past type-checking results cannot be reused. + if (this.step.logicallyChangedTsFiles.has(sfPath) || + this.phase.needsTypeCheckEmit.has(sfPath)) { + return null; + } + + // Past results also cannot be reused if they're not available. + if (this.step.priorState.typeCheckResults === null || + !this.step.priorState.typeCheckResults.has(sfPath)) { + return null; + } + + const priorResults = this.step.priorState.typeCheckResults.get(sfPath)!; + // If the past results relied on inlining, they're not safe for reuse. + if (priorResults.hasInlines) { + return null; + } + + return priorResults; + } + + safeToSkipEmit(sf: ts.SourceFile): boolean { + // If this is a fresh compilation, it's never safe to skip an emit. + if (this.step === null) { + return false; + } + + const sfPath = absoluteFromSourceFile(sf); + + // If the file has itself logically changed, it must be emitted. + if (this.step.logicallyChangedTsFiles.has(sfPath)) { + return false; + } + + if (this.phase.kind !== PhaseKind.TypeCheckAndEmit) { + throw new Error( + `AssertionError: Expected successful analysis before attempting to emit files`); + } + + // If during analysis it was determined that this file has semantically changed, it must be + // emitted. + if (this.phase.needsEmit.has(sfPath)) { + return false; + } + + // Generally it should be safe to assume here that the file was previously emitted by the last + // successful compilation. However, as a defense-in-depth against incorrectness, we explicitly + // check that the last emit included this file, and re-emit it otherwise. + return this.step.priorState.emitted.has(sfPath); + } +} diff --git a/packages/compiler-cli/src/ngtsc/incremental/src/noop.ts b/packages/compiler-cli/src/ngtsc/incremental/src/noop.ts index 4f4803232e..378afd375c 100644 --- a/packages/compiler-cli/src/ngtsc/incremental/src/noop.ts +++ b/packages/compiler-cli/src/ngtsc/incremental/src/noop.ts @@ -9,7 +9,7 @@ import {IncrementalBuild} from '../api'; export const NOOP_INCREMENTAL_BUILD: IncrementalBuild = { - priorWorkFor: () => null, + priorAnalysisFor: () => null, priorTypeCheckingResultsFor: () => null, recordSuccessfulTypeCheck: () => {}, }; diff --git a/packages/compiler-cli/src/ngtsc/incremental/src/state.ts b/packages/compiler-cli/src/ngtsc/incremental/src/state.ts index 6dcf4706be..6c7c4fb768 100644 --- a/packages/compiler-cli/src/ngtsc/incremental/src/state.ts +++ b/packages/compiler-cli/src/ngtsc/incremental/src/state.ts @@ -6,409 +6,101 @@ * found in the LICENSE file at https://angular.io/license */ -import * as ts from 'typescript'; - -import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath} from '../../file_system'; -import {PerfEvent, PerfPhase, PerfRecorder} from '../../perf'; -import {ClassDeclaration} from '../../reflection'; -import {ClassRecord, TraitCompiler} from '../../transform'; +import {AbsoluteFsPath} from '../../file_system'; +import {TraitCompiler} from '../../transform'; import {FileTypeCheckingData} from '../../typecheck/src/checker'; -import {toUnredirectedSourceFile} from '../../util/src/typescript'; -import {IncrementalBuild} from '../api'; -import {SemanticDepGraph, SemanticDepGraphUpdater} from '../semantic_graph'; +import {SemanticDepGraph} from '../semantic_graph'; import {FileDependencyGraph} from './dependency_tracking'; /** - * Drives an incremental build, by tracking changes and determining which files need to be emitted. + * Discriminant of the `IncrementalState` union. */ -export class IncrementalDriver implements IncrementalBuild { - /** - * State of the current build. - * - * This transitions as the compilation progresses. - */ - private state: BuildState; - - private constructor( - state: PendingBuildState, readonly depGraph: FileDependencyGraph, - private logicalChanges: Set|null) { - this.state = state; - } - - /** - * Construct an `IncrementalDriver` with a starting state that incorporates the results of a - * previous build. - * - * The previous build's `BuildState` is reconciled with the new program's changes, and the results - * are merged into the new build's `PendingBuildState`. - */ - static reconcile( - oldProgram: ts.Program, oldDriver: IncrementalDriver, newProgram: ts.Program, - modifiedResourceFiles: Set|null, perf: PerfRecorder): IncrementalDriver { - return perf.inPhase(PerfPhase.Reconciliation, () => { - // Initialize the state of the current build based on the previous one. - let state: PendingBuildState; - if (oldDriver.state.kind === BuildStateKind.Pending) { - // The previous build never made it past the pending state. Reuse it as the starting state - // for this build. - state = oldDriver.state; - } else { - let priorGraph: SemanticDepGraph|null = null; - if (oldDriver.state.lastGood !== null) { - priorGraph = oldDriver.state.lastGood.semanticDepGraph; - } - - // The previous build was successfully analyzed. `pendingEmit` is the only state carried - // forward into this build. - state = { - kind: BuildStateKind.Pending, - pendingEmit: oldDriver.state.pendingEmit, - pendingTypeCheckEmit: oldDriver.state.pendingTypeCheckEmit, - changedResourcePaths: new Set(), - changedTsPaths: new Set(), - lastGood: oldDriver.state.lastGood, - semanticDepGraphUpdater: new SemanticDepGraphUpdater(priorGraph), - }; - } - - // Merge the freshly modified resource files with any prior ones. - if (modifiedResourceFiles !== null) { - for (const resFile of modifiedResourceFiles) { - state.changedResourcePaths.add(absoluteFrom(resFile)); - } - } - - // Next, process the files in the new program, with a couple of goals: - // 1) Determine which TS files have changed, if any, and merge them into `changedTsFiles`. - // 2) Produce a list of TS files which no longer exist in the program (they've been deleted - // since the previous compilation). These need to be removed from the state tracking to - // avoid leaking memory. - - // All files in the old program, for easy detection of changes. - const oldFiles = - new Set(oldProgram.getSourceFiles().map(toUnredirectedSourceFile)); - - // Assume all the old files were deleted to begin with. Only TS files are tracked. - const deletedTsPaths = new Set(tsOnlyFiles(oldProgram).map(sf => sf.fileName)); - - for (const possiblyRedirectedNewFile of newProgram.getSourceFiles()) { - const newFile = toUnredirectedSourceFile(possiblyRedirectedNewFile); - if (!newFile.isDeclarationFile) { - // This file exists in the new program, so remove it from `deletedTsPaths`. - deletedTsPaths.delete(newFile.fileName); - } - - if (oldFiles.has(newFile)) { - // This file hasn't changed; no need to look at it further. - continue; - } - - // The file has changed since the last successful build. The appropriate reaction depends on - // what kind of file it is. - if (!newFile.isDeclarationFile) { - // It's a .ts file, so track it as a change. - state.changedTsPaths.add(newFile.fileName); - } else { - // It's a .d.ts file. Currently the compiler does not do a great job of tracking - // dependencies on .d.ts files, so bail out of incremental builds here and do a full - // build. This usually only happens if something in node_modules changes. - return IncrementalDriver.fresh(newProgram); - } - } - - // The next step is to remove any deleted files from the state. - for (const filePath of deletedTsPaths) { - state.pendingEmit.delete(filePath); - state.pendingTypeCheckEmit.delete(filePath); - - // Even if the file doesn't exist in the current compilation, it still might have been - // changed in a previous one, so delete it from the set of changed TS files, just in case. - state.changedTsPaths.delete(filePath); - } - - perf.eventCount(PerfEvent.SourceFilePhysicalChange, state.changedTsPaths.size); - - // Now, changedTsPaths contains physically changed TS paths. Use the previous program's - // logical dependency graph to determine logically changed files. - const depGraph = new FileDependencyGraph(); - - // If a previous compilation exists, use its dependency graph to determine the set of - // logically changed files. - let logicalChanges: Set|null = null; - if (state.lastGood !== null) { - // Extract the set of logically changed files. At the same time, this operation populates - // the current (fresh) dependency graph with information about those files which have not - // logically changed. - logicalChanges = depGraph.updateWithPhysicalChanges( - state.lastGood.depGraph, state.changedTsPaths, deletedTsPaths, - state.changedResourcePaths); - perf.eventCount(PerfEvent.SourceFileLogicalChange, logicalChanges.size); - for (const fileName of state.changedTsPaths) { - logicalChanges.add(fileName); - } - - // Any logically changed files need to be re-emitted. Most of the time this would happen - // regardless because the new dependency graph would _also_ identify the file as stale. - // However there are edge cases such as removing a component from an NgModule without adding - // it to another one, where the previous graph identifies the file as logically changed, but - // the new graph (which does not have that edge) fails to identify that the file should be - // re-emitted. - for (const change of logicalChanges) { - state.pendingEmit.add(change); - state.pendingTypeCheckEmit.add(change); - } - } - - // `state` now reflects the initial pending state of the current compilation. - return new IncrementalDriver(state, depGraph, logicalChanges); - }); - } - - static fresh(program: ts.Program): IncrementalDriver { - // Initialize the set of files which need to be emitted to the set of all TS files in the - // program. - const tsFiles = tsOnlyFiles(program); - - const state: PendingBuildState = { - kind: BuildStateKind.Pending, - pendingEmit: new Set(tsFiles.map(sf => sf.fileName)), - pendingTypeCheckEmit: new Set(tsFiles.map(sf => sf.fileName)), - changedResourcePaths: new Set(), - changedTsPaths: new Set(), - lastGood: null, - semanticDepGraphUpdater: new SemanticDepGraphUpdater(/* priorGraph */ null), - }; - - return new IncrementalDriver(state, new FileDependencyGraph(), /* logicalChanges */ null); - } - - getSemanticDepGraphUpdater(): SemanticDepGraphUpdater { - if (this.state.kind !== BuildStateKind.Pending) { - throw new Error('Semantic dependency updater is only available when pending analysis'); - } - return this.state.semanticDepGraphUpdater; - } - - recordSuccessfulAnalysis(traitCompiler: TraitCompiler): void { - if (this.state.kind !== BuildStateKind.Pending) { - // Changes have already been incorporated. - return; - } - - const {needsEmit, needsTypeCheckEmit, newGraph} = this.state.semanticDepGraphUpdater.finalize(); - - const pendingEmit = this.state.pendingEmit; - for (const path of needsEmit) { - pendingEmit.add(path); - } - - const pendingTypeCheckEmit = this.state.pendingTypeCheckEmit; - for (const path of needsTypeCheckEmit) { - pendingTypeCheckEmit.add(path); - } - - // Update the state to an `AnalyzedBuildState`. - this.state = { - kind: BuildStateKind.Analyzed, - pendingEmit, - pendingTypeCheckEmit, - - // Since this compilation was successfully analyzed, update the "last good" artifacts to the - // ones from the current compilation. - lastGood: { - depGraph: this.depGraph, - semanticDepGraph: newGraph, - traitCompiler: traitCompiler, - typeCheckingResults: null, - }, - - priorTypeCheckingResults: - this.state.lastGood !== null ? this.state.lastGood.typeCheckingResults : null, - }; - } - - recordSuccessfulTypeCheck(results: Map): void { - if (this.state.lastGood === null || this.state.kind !== BuildStateKind.Analyzed) { - return; - } - this.state.lastGood.typeCheckingResults = results; - - // Delete the files for which type-check code was generated from the set of pending type-check - // files. - for (const fileName of results.keys()) { - this.state.pendingTypeCheckEmit.delete(fileName); - } - } - - recordSuccessfulEmit(sf: ts.SourceFile): void { - this.state.pendingEmit.delete(sf.fileName); - } - - safeToSkipEmit(sf: ts.SourceFile): boolean { - return !this.state.pendingEmit.has(sf.fileName); - } - - priorWorkFor(sf: ts.SourceFile): ClassRecord[]|null { - if (this.state.lastGood === null || this.logicalChanges === null) { - // There is no previous good build, so no prior work exists. - return null; - } else if (this.logicalChanges.has(sf.fileName)) { - // Prior work might exist, but would be stale as the file in question has logically changed. - return null; - } else { - // Prior work might exist, and if it does then it's usable! - return this.state.lastGood.traitCompiler.recordsFor(sf); - } - } - - priorTypeCheckingResultsFor(sf: ts.SourceFile): FileTypeCheckingData|null { - if (this.state.kind !== BuildStateKind.Analyzed || - this.state.priorTypeCheckingResults === null || this.logicalChanges === null) { - return null; - } - - if (this.logicalChanges.has(sf.fileName) || this.state.pendingTypeCheckEmit.has(sf.fileName)) { - return null; - } - - const fileName = absoluteFromSourceFile(sf); - if (!this.state.priorTypeCheckingResults.has(fileName)) { - return null; - } - const data = this.state.priorTypeCheckingResults.get(fileName)!; - if (data.hasInlines) { - return null; - } - - return data; - } -} - -type BuildState = PendingBuildState|AnalyzedBuildState; - -enum BuildStateKind { - Pending, +export enum IncrementalStateKind { + Fresh, + Delta, Analyzed, } -interface BaseBuildState { - kind: BuildStateKind; - - /** - * The heart of incremental builds. This `Set` tracks the set of files which need to be emitted - * during the current compilation. - * - * This starts out as the set of files which are still pending from the previous program (or the - * full set of .ts files on a fresh build). - * - * After analysis, it's updated to include any files which might have changed and need a re-emit - * as a result of incremental changes. - * - * If an emit happens, any written files are removed from the `Set`, as they're no longer - * pending. - * - * Thus, after compilation `pendingEmit` should be empty (on a successful build) or contain the - * files which still need to be emitted but have not yet been (due to errors). - * - * `pendingEmit` is tracked as as `Set` instead of a `Set`, because the - * contents of the file are not important here, only whether or not the current version of it - * needs to be emitted. The `string`s here are TS file paths. - * - * See the README.md for more information on this algorithm. - */ - pendingEmit: Set; - - /** - * Similar to `pendingEmit`, but then for representing the set of files for which the type-check - * file should be regenerated. It behaves identically with respect to errored compilations as - * `pendingEmit`. - */ - pendingTypeCheckEmit: Set; - - - /** - * Specific aspects of the last compilation which successfully completed analysis, if any. - */ - lastGood: { - /** - * The dependency graph from the last successfully analyzed build. - * - * This is used to determine the logical impact of physical file changes. - */ - depGraph: FileDependencyGraph; - - /** - * The semantic dependency graph from the last successfully analyzed build. - * - * This is used to perform in-depth comparison of Angular decorated classes, to determine - * which files have to be re-emitted and/or re-type-checked. - */ - semanticDepGraph: SemanticDepGraph; - - /** - * The `TraitCompiler` from the last successfully analyzed build. - * - * This is used to extract "prior work" which might be reusable in this compilation. - */ - traitCompiler: TraitCompiler; - - /** - * Type checking results which will be passed onto the next build. - */ - typeCheckingResults: Map| null; - }|null; +/** + * Placeholder state for a fresh compilation that has never been successfully analyzed. + */ +export interface FreshIncrementalState { + kind: IncrementalStateKind.Fresh; } /** - * State of a build before the Angular analysis phase completes. + * State captured from a compilation that completed analysis successfully, that can serve as a + * starting point for a future incremental build. */ -interface PendingBuildState extends BaseBuildState { - kind: BuildStateKind.Pending; +export interface AnalyzedIncrementalState { + kind: IncrementalStateKind.Analyzed; /** - * Set of files which are known to need an emit. + * Dependency graph extracted from the build, to be used to determine the logical impact of + * physical file changes. + */ + depGraph: FileDependencyGraph; + + /** + * The semantic dependency graph from the build. * - * Before the compiler's analysis phase completes, `pendingEmit` only contains files that were - * still pending after the previous build. + * This is used to perform in-depth comparison of Angular decorated classes, to determine + * which files have to be re-emitted and/or re-type-checked. */ - pendingEmit: Set; + semanticDepGraph: SemanticDepGraph; /** - * Set of TypeScript file paths which have changed since the last successfully analyzed build. + * `TraitCompiler` which contains records of all analyzed classes within the build. */ - changedTsPaths: Set; + traitCompiler: TraitCompiler; /** - * Set of resource file paths which have changed since the last successfully analyzed build. + * All generated template type-checking files produced as part of this compilation, or `null` if + * type-checking was not (yet) performed. */ - changedResourcePaths: Set; + typeCheckResults: Map|null; /** - * In a pending state, the semantic dependency graph is available to the compilation to register - * the incremental symbols into. + * Cumulative set of source file paths which were definitively emitted by this compilation or + * carried forward from a prior one. */ - semanticDepGraphUpdater: SemanticDepGraphUpdater; + emitted: Set; } -interface AnalyzedBuildState extends BaseBuildState { - kind: BuildStateKind.Analyzed; +/** + * Incremental state for a compilation that has not been successfully analyzed, but that can be + * based on a previous compilation which was. + * + * This is the state produced by an incremeental compilation until its own analysis succeeds. If + * analysis fails, this state carries forward information about which files have changed since the + * last successful build (the `lastAnalyzedState`), so that the next incremental build can consider + * the total delta between the `lastAnalyzedState` and the current program in its incremental + * analysis. + */ +export interface DeltaIncrementalState { + kind: IncrementalStateKind.Delta; /** - * Set of files which are known to need an emit. - * - * After analysis completes (that is, the state transitions to `AnalyzedBuildState`), the - * `pendingEmit` set takes into account any on-disk changes made since the last successfully - * analyzed build. + * If available, the `AnalyzedIncrementalState` for the most recent ancestor of the current + * program which was successfully analyzed. */ - pendingEmit: Set; + lastAnalyzedState: AnalyzedIncrementalState; /** - * Type checking results from the previous compilation, which can be reused in this one. + * Set of file paths which have changed since the `lastAnalyzedState` compilation. */ - priorTypeCheckingResults: Map|null; + physicallyChangedTsFiles: Set; + + /** + * Set of resource file paths which have changed since the `lastAnalyzedState` compilation. + */ + changedResourceFiles: Set; } -function tsOnlyFiles(program: ts.Program): ReadonlyArray { - return program.getSourceFiles().filter(sf => !sf.isDeclarationFile); -} +/** + * State produced by a compilation that's usable as the starting point for a subsequent compilation. + * + * Discriminated by the `IncrementalStateKind` enum. + */ +export type IncrementalState = AnalyzedIncrementalState|DeltaIncrementalState|FreshIncrementalState; diff --git a/packages/compiler-cli/src/ngtsc/incremental/src/strategy.ts b/packages/compiler-cli/src/ngtsc/incremental/src/strategy.ts index 9eacb262c7..94525a767a 100644 --- a/packages/compiler-cli/src/ngtsc/incremental/src/strategy.ts +++ b/packages/compiler-cli/src/ngtsc/incremental/src/strategy.ts @@ -7,7 +7,7 @@ */ import * as ts from 'typescript'; -import {IncrementalDriver} from './state'; +import {IncrementalState} from './state'; /** * Strategy used to manage the association between a `ts.Program` and the `IncrementalDriver` which @@ -17,13 +17,13 @@ export interface IncrementalBuildStrategy { /** * Determine the Angular `IncrementalDriver` for the given `ts.Program`, if one is available. */ - getIncrementalDriver(program: ts.Program): IncrementalDriver|null; + getIncrementalState(program: ts.Program): IncrementalState|null; /** * Associate the given `IncrementalDriver` with the given `ts.Program` and make it available to * future compilations. */ - setIncrementalDriver(driver: IncrementalDriver, program: ts.Program): void; + setIncrementalState(driver: IncrementalState, program: ts.Program): void; /** * Convert this `IncrementalBuildStrategy` into a possibly new instance to be used in the next @@ -37,11 +37,11 @@ export interface IncrementalBuildStrategy { * incremental data. */ export class NoopIncrementalBuildStrategy implements IncrementalBuildStrategy { - getIncrementalDriver(): null { + getIncrementalState(): null { return null; } - setIncrementalDriver(): void {} + setIncrementalState(): void {} toNextBuildStrategy(): IncrementalBuildStrategy { return this; @@ -52,22 +52,22 @@ export class NoopIncrementalBuildStrategy implements IncrementalBuildStrategy { * Tracks an `IncrementalDriver` within the strategy itself. */ export class TrackedIncrementalBuildStrategy implements IncrementalBuildStrategy { - private driver: IncrementalDriver|null = null; + private state: IncrementalState|null = null; private isSet: boolean = false; - getIncrementalDriver(): IncrementalDriver|null { - return this.driver; + getIncrementalState(): IncrementalState|null { + return this.state; } - setIncrementalDriver(driver: IncrementalDriver): void { - this.driver = driver; + setIncrementalState(state: IncrementalState): void { + this.state = state; this.isSet = true; } toNextBuildStrategy(): TrackedIncrementalBuildStrategy { const strategy = new TrackedIncrementalBuildStrategy(); - // Only reuse a driver that was explicitly set via `setIncrementalDriver`. - strategy.driver = this.isSet ? this.driver : null; + // Only reuse state that was explicitly set via `setIncrementalState`. + strategy.state = this.isSet ? this.state : null; return strategy; } } @@ -77,16 +77,16 @@ export class TrackedIncrementalBuildStrategy implements IncrementalBuildStrategy * program under `SYM_INCREMENTAL_DRIVER`. */ export class PatchedProgramIncrementalBuildStrategy implements IncrementalBuildStrategy { - getIncrementalDriver(program: ts.Program): IncrementalDriver|null { - const driver = (program as any)[SYM_INCREMENTAL_DRIVER]; - if (driver === undefined || !(driver instanceof IncrementalDriver)) { + getIncrementalState(program: ts.Program): IncrementalState|null { + const state = (program as MayHaveIncrementalState)[SYM_INCREMENTAL_STATE]; + if (state === undefined) { return null; } - return driver; + return state; } - setIncrementalDriver(driver: IncrementalDriver, program: ts.Program): void { - (program as any)[SYM_INCREMENTAL_DRIVER] = driver; + setIncrementalState(state: IncrementalState, program: ts.Program): void { + (program as MayHaveIncrementalState)[SYM_INCREMENTAL_STATE] = state; } toNextBuildStrategy(): IncrementalBuildStrategy { @@ -108,4 +108,8 @@ export class PatchedProgramIncrementalBuildStrategy implements IncrementalBuildS * support this behind the API of passing an old `ts.Program`, the `IncrementalDriver` is stored on * the `ts.Program` under this symbol. */ -const SYM_INCREMENTAL_DRIVER = Symbol('NgIncrementalDriver'); +const SYM_INCREMENTAL_STATE = Symbol('NgIncrementalState'); + +interface MayHaveIncrementalState { + [SYM_INCREMENTAL_STATE]?: IncrementalState; +} diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index 356cbc8451..8fab53d43a 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -253,7 +253,7 @@ export class NgtscProgram implements api.Program { continue; } - this.compiler.incrementalDriver.recordSuccessfulEmit(writtenSf); + this.compiler.incrementalCompilation.recordSuccessfulEmit(writtenSf); } } this.host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles); @@ -274,7 +274,7 @@ export class NgtscProgram implements api.Program { continue; } - if (this.compiler.incrementalDriver.safeToSkipEmit(targetSourceFile)) { + if (this.compiler.incrementalCompilation.safeToSkipEmit(targetSourceFile)) { this.compiler.perfRecorder.eventCount(PerfEvent.EmitSkipSourceFile); continue; } diff --git a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts index d26059c9b6..aabc0cf5a7 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts @@ -118,7 +118,7 @@ export class TraitCompiler implements ProgramTypeCheckAdapter { // type of 'void', so `undefined` is used instead. const promises: Promise[] = []; - const priorWork = this.incrementalBuild.priorWorkFor(sf); + const priorWork = this.incrementalBuild.priorAnalysisFor(sf); if (priorWork !== null) { for (const priorRecord of priorWork) { this.adopt(priorRecord); diff --git a/packages/compiler-cli/src/ngtsc/tsc_plugin.ts b/packages/compiler-cli/src/ngtsc/tsc_plugin.ts index 5a214876b1..7f90bd12a7 100644 --- a/packages/compiler-cli/src/ngtsc/tsc_plugin.ts +++ b/packages/compiler-cli/src/ngtsc/tsc_plugin.ts @@ -8,9 +8,9 @@ import * as ts from 'typescript'; -import {CompilationTicket, freshCompilationTicket, incrementalFromDriverTicket, NgCompiler, NgCompilerHost} from './core'; +import {CompilationTicket, freshCompilationTicket, incrementalFromStateTicket, NgCompiler, NgCompilerHost} from './core'; import {NgCompilerOptions, UnifiedModulesHost} from './core/api'; -import {NodeJSFileSystem, setFileSystem} from './file_system'; +import {AbsoluteFsPath, NodeJSFileSystem, resolve, setFileSystem} from './file_system'; import {PatchedProgramIncrementalBuildStrategy} from './incremental'; import {ActivePerfRecorder, PerfPhase} from './perf'; import {TsCreateProgramDriver} from './program_driver'; @@ -109,25 +109,24 @@ export class NgTscPlugin implements TscPlugin { const programDriver = new TsCreateProgramDriver( program, this.host, this.options, this.host.shimExtensionPrefixes); const strategy = new PatchedProgramIncrementalBuildStrategy(); - const oldDriver = oldProgram !== undefined ? strategy.getIncrementalDriver(oldProgram) : null; + const oldState = oldProgram !== undefined ? strategy.getIncrementalState(oldProgram) : null; let ticket: CompilationTicket; - let modifiedResourceFiles: Set|undefined = undefined; + const modifiedResourceFiles = new Set(); if (this.host.getModifiedResourceFiles !== undefined) { - modifiedResourceFiles = this.host.getModifiedResourceFiles(); - } - if (modifiedResourceFiles === undefined) { - modifiedResourceFiles = new Set(); + for (const resourceFile of this.host.getModifiedResourceFiles() ?? []) { + modifiedResourceFiles.add(resolve(resourceFile)); + } } - if (oldProgram === undefined || oldDriver === null) { + if (oldProgram === undefined || oldState === null) { ticket = freshCompilationTicket( program, this.options, strategy, programDriver, perfRecorder, /* enableTemplateTypeChecker */ false, /* usePoisonedData */ false); } else { - strategy.toNextBuildStrategy().getIncrementalDriver(oldProgram); - ticket = incrementalFromDriverTicket( - oldProgram, oldDriver, program, this.options, strategy, programDriver, + strategy.toNextBuildStrategy().getIncrementalState(oldProgram); + ticket = incrementalFromStateTicket( + oldProgram, oldState, program, this.options, strategy, programDriver, modifiedResourceFiles, perfRecorder, false, false); } this._compiler = NgCompiler.fromTicket(ticket, this.host); diff --git a/packages/language-service/ivy/compiler_factory.ts b/packages/language-service/ivy/compiler_factory.ts index ee990ee8bd..b424d02761 100644 --- a/packages/language-service/ivy/compiler_factory.ts +++ b/packages/language-service/ivy/compiler_factory.ts @@ -8,6 +8,7 @@ import {CompilationTicket, freshCompilationTicket, incrementalFromCompilerTicket, NgCompiler, resourceChangeTicket} from '@angular/compiler-cli/src/ngtsc/core'; import {NgCompilerOptions} from '@angular/compiler-cli/src/ngtsc/core/api'; +import {AbsoluteFsPath, resolve} from '@angular/compiler-cli/src/ngtsc/file_system'; import {TrackedIncrementalBuildStrategy} from '@angular/compiler-cli/src/ngtsc/incremental'; import {ProgramDriver} from '@angular/compiler-cli/src/ngtsc/program_driver'; @@ -34,7 +35,10 @@ export class CompilerFactory { getOrCreate(): NgCompiler { const program = this.programStrategy.getProgram(); - const modifiedResourceFiles = this.adapter.getModifiedResourceFiles() ?? new Set(); + const modifiedResourceFiles = new Set(); + for (const fileName of this.adapter.getModifiedResourceFiles() ?? []) { + modifiedResourceFiles.add(resolve(fileName)); + } if (this.compiler !== null && program === this.compiler.getCurrentProgram()) { if (modifiedResourceFiles.size > 0) {