fix(ivy): ensure that changes to component resources trigger incremental builds (#30954)
Optimizations to skip compiling source files that had not changed did not account for the case where only a resource file changes, such as an external template or style file. Now we track such dependencies and trigger a recompilation if any of the previously tracked resources have changed. This will require a change on the CLI side to provide the list of resource files that changed to trigger the current compilation by implementing `CompilerHost.getModifiedResourceFiles()`. Closes #30947 PR Close #30954
This commit is contained in:

committed by
Kara Erickson

parent
dc613b336d
commit
48def92cad
@ -20,6 +20,7 @@ import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral} from
|
||||
import {LocalModuleScopeRegistry} from '../../scope';
|
||||
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence, ResolveResult} from '../../transform';
|
||||
import {TypeCheckContext} from '../../typecheck';
|
||||
import {NoopResourceDependencyRecorder, ResourceDependencyRecorder} from '../../util/src/resource_recorder';
|
||||
import {tsSourceMapBug29300Fixed} from '../../util/src/ts_source_map_bug_29300';
|
||||
|
||||
import {ResourceLoader} from './api';
|
||||
@ -48,7 +49,9 @@ export class ComponentDecoratorHandler implements
|
||||
private resourceLoader: ResourceLoader, private rootDirs: string[],
|
||||
private defaultPreserveWhitespaces: boolean, private i18nUseExternalIds: boolean,
|
||||
private moduleResolver: ModuleResolver, private cycleAnalyzer: CycleAnalyzer,
|
||||
private refEmitter: ReferenceEmitter, private defaultImportRecorder: DefaultImportRecorder) {}
|
||||
private refEmitter: ReferenceEmitter, private defaultImportRecorder: DefaultImportRecorder,
|
||||
private resourceDependencies:
|
||||
ResourceDependencyRecorder = new NoopResourceDependencyRecorder()) {}
|
||||
|
||||
private literalCache = new Map<Decorator, ts.ObjectLiteralExpression>();
|
||||
private elementSchemaRegistry = new DomElementSchemaRegistry();
|
||||
@ -182,6 +185,7 @@ export class ComponentDecoratorHandler implements
|
||||
}
|
||||
const templateUrl = this.resourceLoader.resolve(evalTemplateUrl, containingFile);
|
||||
const templateStr = this.resourceLoader.load(templateUrl);
|
||||
this.resourceDependencies.recordResourceDependency(node.getSourceFile(), templateUrl);
|
||||
|
||||
template = this._parseTemplate(
|
||||
component, templateStr, sourceMapUrl(templateUrl), /* templateRange */ undefined,
|
||||
@ -236,7 +240,9 @@ export class ComponentDecoratorHandler implements
|
||||
}
|
||||
for (const styleUrl of styleUrls) {
|
||||
const resourceUrl = this.resourceLoader.resolve(styleUrl, containingFile);
|
||||
styles.push(this.resourceLoader.load(resourceUrl));
|
||||
const resourceStr = this.resourceLoader.load(resourceUrl);
|
||||
styles.push(resourceStr);
|
||||
this.resourceDependencies.recordResourceDependency(node.getSourceFile(), resourceUrl);
|
||||
}
|
||||
}
|
||||
if (component.has('styles')) {
|
||||
@ -506,6 +512,7 @@ export class ComponentDecoratorHandler implements
|
||||
if (templatePromise !== undefined) {
|
||||
return templatePromise.then(() => {
|
||||
const templateStr = this.resourceLoader.load(resourceUrl);
|
||||
this.resourceDependencies.recordResourceDependency(node.getSourceFile(), resourceUrl);
|
||||
const template = this._parseTemplate(
|
||||
component, templateStr, sourceMapUrl(resourceUrl), /* templateRange */ undefined,
|
||||
/* escapedString */ false);
|
||||
|
@ -22,6 +22,7 @@ ts_library(
|
||||
"//packages/compiler-cli/src/ngtsc/scope",
|
||||
"//packages/compiler-cli/src/ngtsc/testing",
|
||||
"//packages/compiler-cli/src/ngtsc/translator",
|
||||
"//packages/compiler-cli/src/ngtsc/util",
|
||||
"@npm//typescript",
|
||||
],
|
||||
)
|
||||
|
@ -12,6 +12,7 @@ ts_library(
|
||||
"//packages/compiler-cli/src/ngtsc/metadata",
|
||||
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
|
||||
"//packages/compiler-cli/src/ngtsc/reflection",
|
||||
"//packages/compiler-cli/src/ngtsc/util",
|
||||
"@npm//typescript",
|
||||
],
|
||||
)
|
||||
|
@ -7,21 +7,26 @@
|
||||
*/
|
||||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {Reference} from '../../imports';
|
||||
import {DirectiveMeta, MetadataReader, MetadataRegistry, NgModuleMeta, PipeMeta} from '../../metadata';
|
||||
import {DependencyTracker} from '../../partial_evaluator';
|
||||
import {ClassDeclaration} from '../../reflection';
|
||||
import {ResourceDependencyRecorder} from '../../util/src/resource_recorder';
|
||||
|
||||
/**
|
||||
* Accumulates state between compilations.
|
||||
*/
|
||||
export class IncrementalState implements DependencyTracker, MetadataReader, MetadataRegistry {
|
||||
export class IncrementalState implements DependencyTracker, MetadataReader, MetadataRegistry,
|
||||
ResourceDependencyRecorder {
|
||||
private constructor(
|
||||
private unchangedFiles: Set<ts.SourceFile>,
|
||||
private metadata: Map<ts.SourceFile, FileMetadata>) {}
|
||||
private metadata: Map<ts.SourceFile, FileMetadata>,
|
||||
private modifiedResourceFiles: Set<string>|null) {}
|
||||
|
||||
static reconcile(previousState: IncrementalState, oldProgram: ts.Program, newProgram: ts.Program):
|
||||
IncrementalState {
|
||||
static reconcile(
|
||||
previousState: IncrementalState, oldProgram: ts.Program, newProgram: ts.Program,
|
||||
modifiedResourceFiles: Set<string>|null): IncrementalState {
|
||||
const unchangedFiles = new Set<ts.SourceFile>();
|
||||
const metadata = new Map<ts.SourceFile, FileMetadata>();
|
||||
const oldFiles = new Set<ts.SourceFile>(oldProgram.getSourceFiles());
|
||||
@ -46,14 +51,17 @@ export class IncrementalState implements DependencyTracker, MetadataReader, Meta
|
||||
}
|
||||
}
|
||||
|
||||
return new IncrementalState(unchangedFiles, metadata);
|
||||
return new IncrementalState(unchangedFiles, metadata, modifiedResourceFiles);
|
||||
}
|
||||
|
||||
static fresh(): IncrementalState {
|
||||
return new IncrementalState(new Set<ts.SourceFile>(), new Map<ts.SourceFile, FileMetadata>());
|
||||
return new IncrementalState(
|
||||
new Set<ts.SourceFile>(), new Map<ts.SourceFile, FileMetadata>(), null);
|
||||
}
|
||||
|
||||
safeToSkip(sf: ts.SourceFile): boolean { return this.unchangedFiles.has(sf); }
|
||||
safeToSkip(sf: ts.SourceFile): boolean|Promise<boolean> {
|
||||
return this.unchangedFiles.has(sf) && !this.hasChangedResourceDependencies(sf);
|
||||
}
|
||||
|
||||
trackFileDependency(dep: ts.SourceFile, src: ts.SourceFile) {
|
||||
const metadata = this.ensureMetadata(src);
|
||||
@ -92,11 +100,25 @@ export class IncrementalState implements DependencyTracker, MetadataReader, Meta
|
||||
metadata.pipeMeta.set(meta.ref.node, meta);
|
||||
}
|
||||
|
||||
recordResourceDependency(file: ts.SourceFile, resourcePath: string): void {
|
||||
const metadata = this.ensureMetadata(file);
|
||||
metadata.resourcePaths.add(resourcePath);
|
||||
}
|
||||
|
||||
private ensureMetadata(sf: ts.SourceFile): FileMetadata {
|
||||
const metadata = this.metadata.get(sf) || new FileMetadata();
|
||||
this.metadata.set(sf, metadata);
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private hasChangedResourceDependencies(sf: ts.SourceFile): boolean {
|
||||
if (this.modifiedResourceFiles === undefined || !this.metadata.has(sf)) {
|
||||
return false;
|
||||
}
|
||||
const resourceDeps = this.metadata.get(sf) !.resourcePaths;
|
||||
return Array.from(resourceDeps.keys())
|
||||
.some(resourcePath => this.modifiedResourceFiles !.has(resourcePath));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -105,6 +127,7 @@ export class IncrementalState implements DependencyTracker, MetadataReader, Meta
|
||||
class FileMetadata {
|
||||
/** A set of source files that this file depends upon. */
|
||||
fileDependencies = new Set<ts.SourceFile>();
|
||||
resourcePaths = new Set<string>();
|
||||
directiveMeta = new Map<ClassDeclaration, DirectiveMeta>();
|
||||
ngModuleMeta = new Map<ClassDeclaration, NgModuleMeta>();
|
||||
pipeMeta = new Map<ClassDeclaration, PipeMeta>();
|
||||
|
@ -43,7 +43,6 @@ export class NgtscProgram implements api.Program {
|
||||
private compilation: IvyCompilation|undefined = undefined;
|
||||
private factoryToSourceInfo: Map<string, FactoryInfo>|null = null;
|
||||
private sourceToFactorySymbols: Map<string, Set<string>>|null = null;
|
||||
private host: ts.CompilerHost;
|
||||
private _coreImportsFrom: ts.SourceFile|null|undefined = undefined;
|
||||
private _importRewriter: ImportRewriter|undefined = undefined;
|
||||
private _reflector: TypeScriptReflectionHost|undefined = undefined;
|
||||
@ -68,20 +67,23 @@ export class NgtscProgram implements api.Program {
|
||||
private incrementalState: IncrementalState;
|
||||
private typeCheckFilePath: AbsoluteFsPath;
|
||||
|
||||
private modifiedResourceFiles: Set<string>|null;
|
||||
|
||||
constructor(
|
||||
rootNames: ReadonlyArray<string>, private options: api.CompilerOptions,
|
||||
host: api.CompilerHost, oldProgram?: NgtscProgram) {
|
||||
private host: api.CompilerHost, oldProgram?: NgtscProgram) {
|
||||
if (shouldEnablePerfTracing(options)) {
|
||||
this.perfTracker = PerfTracker.zeroedToNow();
|
||||
this.perfRecorder = this.perfTracker;
|
||||
}
|
||||
|
||||
this.modifiedResourceFiles =
|
||||
this.host.getModifiedResourceFiles && this.host.getModifiedResourceFiles() || null;
|
||||
this.rootDirs = getRootDirs(host, options);
|
||||
this.closureCompilerEnabled = !!options.annotateForClosureCompiler;
|
||||
this.resourceManager = new HostResourceLoader(host, options);
|
||||
const shouldGenerateShims = options.allowEmptyCodegenFiles || false;
|
||||
const normalizedRootNames = rootNames.map(n => AbsoluteFsPath.from(n));
|
||||
this.host = host;
|
||||
if (host.fileNameToModuleName !== undefined) {
|
||||
this.fileToModuleHost = host as FileToModuleHost;
|
||||
}
|
||||
@ -159,7 +161,8 @@ export class NgtscProgram implements api.Program {
|
||||
this.incrementalState = IncrementalState.fresh();
|
||||
} else {
|
||||
this.incrementalState = IncrementalState.reconcile(
|
||||
oldProgram.incrementalState, oldProgram.reuseTsProgram, this.tsProgram);
|
||||
oldProgram.incrementalState, oldProgram.reuseTsProgram, this.tsProgram,
|
||||
this.modifiedResourceFiles);
|
||||
}
|
||||
}
|
||||
|
||||
@ -498,7 +501,7 @@ export class NgtscProgram implements api.Program {
|
||||
this.reflector, evaluator, metaRegistry, this.metaReader !, scopeRegistry, this.isCore,
|
||||
this.resourceManager, this.rootDirs, this.options.preserveWhitespaces || false,
|
||||
this.options.i18nUseExternalIds !== false, this.moduleResolver, this.cycleAnalyzer,
|
||||
this.refEmitter, this.defaultImportTracker),
|
||||
this.refEmitter, this.defaultImportTracker, this.incrementalState),
|
||||
new DirectiveDecoratorHandler(
|
||||
this.reflector, evaluator, metaRegistry, this.defaultImportTracker, this.isCore),
|
||||
new InjectableDecoratorHandler(
|
||||
|
@ -0,0 +1,20 @@
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. 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';
|
||||
|
||||
/**
|
||||
* Implement this interface to record what resources a source file depends upon.
|
||||
*/
|
||||
export interface ResourceDependencyRecorder {
|
||||
recordResourceDependency(file: ts.SourceFile, resourcePath: string): void;
|
||||
}
|
||||
|
||||
export class NoopResourceDependencyRecorder implements ResourceDependencyRecorder {
|
||||
recordResourceDependency(): void {}
|
||||
}
|
Reference in New Issue
Block a user