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:
parent
dc613b336d
commit
48def92cad
|
@ -24,7 +24,8 @@ export function main(
|
||||||
args: string[], consoleError: (s: string) => void = console.error,
|
args: string[], consoleError: (s: string) => void = console.error,
|
||||||
config?: NgcParsedConfiguration, customTransformers?: api.CustomTransformers, programReuse?: {
|
config?: NgcParsedConfiguration, customTransformers?: api.CustomTransformers, programReuse?: {
|
||||||
program: api.Program | undefined,
|
program: api.Program | undefined,
|
||||||
}): number {
|
},
|
||||||
|
modifiedResourceFiles?: Set<string>): number {
|
||||||
let {project, rootNames, options, errors: configErrors, watch, emitFlags} =
|
let {project, rootNames, options, errors: configErrors, watch, emitFlags} =
|
||||||
config || readNgcCommandLineAndConfiguration(args);
|
config || readNgcCommandLineAndConfiguration(args);
|
||||||
if (configErrors.length) {
|
if (configErrors.length) {
|
||||||
|
@ -45,7 +46,7 @@ export function main(
|
||||||
options,
|
options,
|
||||||
emitFlags,
|
emitFlags,
|
||||||
oldProgram,
|
oldProgram,
|
||||||
emitCallback: createEmitCallback(options), customTransformers
|
emitCallback: createEmitCallback(options), customTransformers, modifiedResourceFiles
|
||||||
});
|
});
|
||||||
if (programReuse !== undefined) {
|
if (programReuse !== undefined) {
|
||||||
programReuse.program = program;
|
programReuse.program = program;
|
||||||
|
|
|
@ -20,6 +20,7 @@ import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral} from
|
||||||
import {LocalModuleScopeRegistry} from '../../scope';
|
import {LocalModuleScopeRegistry} from '../../scope';
|
||||||
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence, ResolveResult} from '../../transform';
|
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence, ResolveResult} from '../../transform';
|
||||||
import {TypeCheckContext} from '../../typecheck';
|
import {TypeCheckContext} from '../../typecheck';
|
||||||
|
import {NoopResourceDependencyRecorder, ResourceDependencyRecorder} from '../../util/src/resource_recorder';
|
||||||
import {tsSourceMapBug29300Fixed} from '../../util/src/ts_source_map_bug_29300';
|
import {tsSourceMapBug29300Fixed} from '../../util/src/ts_source_map_bug_29300';
|
||||||
|
|
||||||
import {ResourceLoader} from './api';
|
import {ResourceLoader} from './api';
|
||||||
|
@ -48,7 +49,9 @@ export class ComponentDecoratorHandler implements
|
||||||
private resourceLoader: ResourceLoader, private rootDirs: string[],
|
private resourceLoader: ResourceLoader, private rootDirs: string[],
|
||||||
private defaultPreserveWhitespaces: boolean, private i18nUseExternalIds: boolean,
|
private defaultPreserveWhitespaces: boolean, private i18nUseExternalIds: boolean,
|
||||||
private moduleResolver: ModuleResolver, private cycleAnalyzer: CycleAnalyzer,
|
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 literalCache = new Map<Decorator, ts.ObjectLiteralExpression>();
|
||||||
private elementSchemaRegistry = new DomElementSchemaRegistry();
|
private elementSchemaRegistry = new DomElementSchemaRegistry();
|
||||||
|
@ -182,6 +185,7 @@ export class ComponentDecoratorHandler implements
|
||||||
}
|
}
|
||||||
const templateUrl = this.resourceLoader.resolve(evalTemplateUrl, containingFile);
|
const templateUrl = this.resourceLoader.resolve(evalTemplateUrl, containingFile);
|
||||||
const templateStr = this.resourceLoader.load(templateUrl);
|
const templateStr = this.resourceLoader.load(templateUrl);
|
||||||
|
this.resourceDependencies.recordResourceDependency(node.getSourceFile(), templateUrl);
|
||||||
|
|
||||||
template = this._parseTemplate(
|
template = this._parseTemplate(
|
||||||
component, templateStr, sourceMapUrl(templateUrl), /* templateRange */ undefined,
|
component, templateStr, sourceMapUrl(templateUrl), /* templateRange */ undefined,
|
||||||
|
@ -236,7 +240,9 @@ export class ComponentDecoratorHandler implements
|
||||||
}
|
}
|
||||||
for (const styleUrl of styleUrls) {
|
for (const styleUrl of styleUrls) {
|
||||||
const resourceUrl = this.resourceLoader.resolve(styleUrl, containingFile);
|
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')) {
|
if (component.has('styles')) {
|
||||||
|
@ -506,6 +512,7 @@ export class ComponentDecoratorHandler implements
|
||||||
if (templatePromise !== undefined) {
|
if (templatePromise !== undefined) {
|
||||||
return templatePromise.then(() => {
|
return templatePromise.then(() => {
|
||||||
const templateStr = this.resourceLoader.load(resourceUrl);
|
const templateStr = this.resourceLoader.load(resourceUrl);
|
||||||
|
this.resourceDependencies.recordResourceDependency(node.getSourceFile(), resourceUrl);
|
||||||
const template = this._parseTemplate(
|
const template = this._parseTemplate(
|
||||||
component, templateStr, sourceMapUrl(resourceUrl), /* templateRange */ undefined,
|
component, templateStr, sourceMapUrl(resourceUrl), /* templateRange */ undefined,
|
||||||
/* escapedString */ false);
|
/* escapedString */ false);
|
||||||
|
|
|
@ -22,6 +22,7 @@ ts_library(
|
||||||
"//packages/compiler-cli/src/ngtsc/scope",
|
"//packages/compiler-cli/src/ngtsc/scope",
|
||||||
"//packages/compiler-cli/src/ngtsc/testing",
|
"//packages/compiler-cli/src/ngtsc/testing",
|
||||||
"//packages/compiler-cli/src/ngtsc/translator",
|
"//packages/compiler-cli/src/ngtsc/translator",
|
||||||
|
"//packages/compiler-cli/src/ngtsc/util",
|
||||||
"@npm//typescript",
|
"@npm//typescript",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,6 +12,7 @@ ts_library(
|
||||||
"//packages/compiler-cli/src/ngtsc/metadata",
|
"//packages/compiler-cli/src/ngtsc/metadata",
|
||||||
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
|
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
|
||||||
"//packages/compiler-cli/src/ngtsc/reflection",
|
"//packages/compiler-cli/src/ngtsc/reflection",
|
||||||
|
"//packages/compiler-cli/src/ngtsc/util",
|
||||||
"@npm//typescript",
|
"@npm//typescript",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,21 +7,26 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {Reference} from '../../imports';
|
import {Reference} from '../../imports';
|
||||||
import {DirectiveMeta, MetadataReader, MetadataRegistry, NgModuleMeta, PipeMeta} from '../../metadata';
|
import {DirectiveMeta, MetadataReader, MetadataRegistry, NgModuleMeta, PipeMeta} from '../../metadata';
|
||||||
import {DependencyTracker} from '../../partial_evaluator';
|
import {DependencyTracker} from '../../partial_evaluator';
|
||||||
import {ClassDeclaration} from '../../reflection';
|
import {ClassDeclaration} from '../../reflection';
|
||||||
|
import {ResourceDependencyRecorder} from '../../util/src/resource_recorder';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Accumulates state between compilations.
|
* Accumulates state between compilations.
|
||||||
*/
|
*/
|
||||||
export class IncrementalState implements DependencyTracker, MetadataReader, MetadataRegistry {
|
export class IncrementalState implements DependencyTracker, MetadataReader, MetadataRegistry,
|
||||||
|
ResourceDependencyRecorder {
|
||||||
private constructor(
|
private constructor(
|
||||||
private unchangedFiles: Set<ts.SourceFile>,
|
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):
|
static reconcile(
|
||||||
IncrementalState {
|
previousState: IncrementalState, oldProgram: ts.Program, newProgram: ts.Program,
|
||||||
|
modifiedResourceFiles: Set<string>|null): IncrementalState {
|
||||||
const unchangedFiles = new Set<ts.SourceFile>();
|
const unchangedFiles = new Set<ts.SourceFile>();
|
||||||
const metadata = new Map<ts.SourceFile, FileMetadata>();
|
const metadata = new Map<ts.SourceFile, FileMetadata>();
|
||||||
const oldFiles = new Set<ts.SourceFile>(oldProgram.getSourceFiles());
|
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 {
|
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) {
|
trackFileDependency(dep: ts.SourceFile, src: ts.SourceFile) {
|
||||||
const metadata = this.ensureMetadata(src);
|
const metadata = this.ensureMetadata(src);
|
||||||
|
@ -92,11 +100,25 @@ export class IncrementalState implements DependencyTracker, MetadataReader, Meta
|
||||||
metadata.pipeMeta.set(meta.ref.node, 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 {
|
private ensureMetadata(sf: ts.SourceFile): FileMetadata {
|
||||||
const metadata = this.metadata.get(sf) || new FileMetadata();
|
const metadata = this.metadata.get(sf) || new FileMetadata();
|
||||||
this.metadata.set(sf, metadata);
|
this.metadata.set(sf, metadata);
|
||||||
return 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 {
|
class FileMetadata {
|
||||||
/** A set of source files that this file depends upon. */
|
/** A set of source files that this file depends upon. */
|
||||||
fileDependencies = new Set<ts.SourceFile>();
|
fileDependencies = new Set<ts.SourceFile>();
|
||||||
|
resourcePaths = new Set<string>();
|
||||||
directiveMeta = new Map<ClassDeclaration, DirectiveMeta>();
|
directiveMeta = new Map<ClassDeclaration, DirectiveMeta>();
|
||||||
ngModuleMeta = new Map<ClassDeclaration, NgModuleMeta>();
|
ngModuleMeta = new Map<ClassDeclaration, NgModuleMeta>();
|
||||||
pipeMeta = new Map<ClassDeclaration, PipeMeta>();
|
pipeMeta = new Map<ClassDeclaration, PipeMeta>();
|
||||||
|
|
|
@ -43,7 +43,6 @@ export class NgtscProgram implements api.Program {
|
||||||
private compilation: IvyCompilation|undefined = undefined;
|
private compilation: IvyCompilation|undefined = undefined;
|
||||||
private factoryToSourceInfo: Map<string, FactoryInfo>|null = null;
|
private factoryToSourceInfo: Map<string, FactoryInfo>|null = null;
|
||||||
private sourceToFactorySymbols: Map<string, Set<string>>|null = null;
|
private sourceToFactorySymbols: Map<string, Set<string>>|null = null;
|
||||||
private host: ts.CompilerHost;
|
|
||||||
private _coreImportsFrom: ts.SourceFile|null|undefined = undefined;
|
private _coreImportsFrom: ts.SourceFile|null|undefined = undefined;
|
||||||
private _importRewriter: ImportRewriter|undefined = undefined;
|
private _importRewriter: ImportRewriter|undefined = undefined;
|
||||||
private _reflector: TypeScriptReflectionHost|undefined = undefined;
|
private _reflector: TypeScriptReflectionHost|undefined = undefined;
|
||||||
|
@ -68,20 +67,23 @@ export class NgtscProgram implements api.Program {
|
||||||
private incrementalState: IncrementalState;
|
private incrementalState: IncrementalState;
|
||||||
private typeCheckFilePath: AbsoluteFsPath;
|
private typeCheckFilePath: AbsoluteFsPath;
|
||||||
|
|
||||||
|
private modifiedResourceFiles: Set<string>|null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
rootNames: ReadonlyArray<string>, private options: api.CompilerOptions,
|
rootNames: ReadonlyArray<string>, private options: api.CompilerOptions,
|
||||||
host: api.CompilerHost, oldProgram?: NgtscProgram) {
|
private host: api.CompilerHost, oldProgram?: NgtscProgram) {
|
||||||
if (shouldEnablePerfTracing(options)) {
|
if (shouldEnablePerfTracing(options)) {
|
||||||
this.perfTracker = PerfTracker.zeroedToNow();
|
this.perfTracker = PerfTracker.zeroedToNow();
|
||||||
this.perfRecorder = this.perfTracker;
|
this.perfRecorder = this.perfTracker;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.modifiedResourceFiles =
|
||||||
|
this.host.getModifiedResourceFiles && this.host.getModifiedResourceFiles() || null;
|
||||||
this.rootDirs = getRootDirs(host, options);
|
this.rootDirs = getRootDirs(host, options);
|
||||||
this.closureCompilerEnabled = !!options.annotateForClosureCompiler;
|
this.closureCompilerEnabled = !!options.annotateForClosureCompiler;
|
||||||
this.resourceManager = new HostResourceLoader(host, options);
|
this.resourceManager = new HostResourceLoader(host, options);
|
||||||
const shouldGenerateShims = options.allowEmptyCodegenFiles || false;
|
const shouldGenerateShims = options.allowEmptyCodegenFiles || false;
|
||||||
const normalizedRootNames = rootNames.map(n => AbsoluteFsPath.from(n));
|
const normalizedRootNames = rootNames.map(n => AbsoluteFsPath.from(n));
|
||||||
this.host = host;
|
|
||||||
if (host.fileNameToModuleName !== undefined) {
|
if (host.fileNameToModuleName !== undefined) {
|
||||||
this.fileToModuleHost = host as FileToModuleHost;
|
this.fileToModuleHost = host as FileToModuleHost;
|
||||||
}
|
}
|
||||||
|
@ -159,7 +161,8 @@ export class NgtscProgram implements api.Program {
|
||||||
this.incrementalState = IncrementalState.fresh();
|
this.incrementalState = IncrementalState.fresh();
|
||||||
} else {
|
} else {
|
||||||
this.incrementalState = IncrementalState.reconcile(
|
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.reflector, evaluator, metaRegistry, this.metaReader !, scopeRegistry, this.isCore,
|
||||||
this.resourceManager, this.rootDirs, this.options.preserveWhitespaces || false,
|
this.resourceManager, this.rootDirs, this.options.preserveWhitespaces || false,
|
||||||
this.options.i18nUseExternalIds !== false, this.moduleResolver, this.cycleAnalyzer,
|
this.options.i18nUseExternalIds !== false, this.moduleResolver, this.cycleAnalyzer,
|
||||||
this.refEmitter, this.defaultImportTracker),
|
this.refEmitter, this.defaultImportTracker, this.incrementalState),
|
||||||
new DirectiveDecoratorHandler(
|
new DirectiveDecoratorHandler(
|
||||||
this.reflector, evaluator, metaRegistry, this.defaultImportTracker, this.isCore),
|
this.reflector, evaluator, metaRegistry, this.defaultImportTracker, this.isCore),
|
||||||
new InjectableDecoratorHandler(
|
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 {}
|
||||||
|
}
|
|
@ -220,10 +220,10 @@ export function exitCodeFromResult(diags: Diagnostics | undefined): number {
|
||||||
return diags.some(d => d.source === 'angular' && d.code === api.UNKNOWN_ERROR_CODE) ? 2 : 1;
|
return diags.some(d => d.source === 'angular' && d.code === api.UNKNOWN_ERROR_CODE) ? 2 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function performCompilation({rootNames, options, host, oldProgram, emitCallback,
|
export function performCompilation(
|
||||||
mergeEmitResultsCallback,
|
{rootNames, options, host, oldProgram, emitCallback, mergeEmitResultsCallback,
|
||||||
gatherDiagnostics = defaultGatherDiagnostics,
|
gatherDiagnostics = defaultGatherDiagnostics, customTransformers,
|
||||||
customTransformers, emitFlags = api.EmitFlags.Default}: {
|
emitFlags = api.EmitFlags.Default, modifiedResourceFiles}: {
|
||||||
rootNames: string[],
|
rootNames: string[],
|
||||||
options: api.CompilerOptions,
|
options: api.CompilerOptions,
|
||||||
host?: api.CompilerHost,
|
host?: api.CompilerHost,
|
||||||
|
@ -232,7 +232,8 @@ export function performCompilation({rootNames, options, host, oldProgram, emitCa
|
||||||
mergeEmitResultsCallback?: api.TsMergeEmitResultsCallback,
|
mergeEmitResultsCallback?: api.TsMergeEmitResultsCallback,
|
||||||
gatherDiagnostics?: (program: api.Program) => Diagnostics,
|
gatherDiagnostics?: (program: api.Program) => Diagnostics,
|
||||||
customTransformers?: api.CustomTransformers,
|
customTransformers?: api.CustomTransformers,
|
||||||
emitFlags?: api.EmitFlags
|
emitFlags?: api.EmitFlags,
|
||||||
|
modifiedResourceFiles?: Set<string>,
|
||||||
}): PerformCompilationResult {
|
}): PerformCompilationResult {
|
||||||
let program: api.Program|undefined;
|
let program: api.Program|undefined;
|
||||||
let emitResult: ts.EmitResult|undefined;
|
let emitResult: ts.EmitResult|undefined;
|
||||||
|
@ -241,6 +242,9 @@ export function performCompilation({rootNames, options, host, oldProgram, emitCa
|
||||||
if (!host) {
|
if (!host) {
|
||||||
host = ng.createCompilerHost({options});
|
host = ng.createCompilerHost({options});
|
||||||
}
|
}
|
||||||
|
if (modifiedResourceFiles) {
|
||||||
|
host.getModifiedResourceFiles = () => modifiedResourceFiles;
|
||||||
|
}
|
||||||
|
|
||||||
program = ng.createProgram({rootNames, host, options, oldProgram});
|
program = ng.createProgram({rootNames, host, options, oldProgram});
|
||||||
|
|
||||||
|
|
|
@ -103,6 +103,11 @@ interface CacheEntry {
|
||||||
content?: string;
|
content?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface QueuedCompilationInfo {
|
||||||
|
timerHandle: any;
|
||||||
|
modifiedResourceFiles: Set<string>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The logic in this function is adapted from `tsc.ts` from TypeScript.
|
* The logic in this function is adapted from `tsc.ts` from TypeScript.
|
||||||
*/
|
*/
|
||||||
|
@ -111,7 +116,8 @@ export function performWatchCompilation(host: PerformWatchHost):
|
||||||
let cachedProgram: api.Program|undefined; // Program cached from last compilation
|
let cachedProgram: api.Program|undefined; // Program cached from last compilation
|
||||||
let cachedCompilerHost: api.CompilerHost|undefined; // CompilerHost cached from last compilation
|
let cachedCompilerHost: api.CompilerHost|undefined; // CompilerHost cached from last compilation
|
||||||
let cachedOptions: ParsedConfiguration|undefined; // CompilerOptions cached from last compilation
|
let cachedOptions: ParsedConfiguration|undefined; // CompilerOptions cached from last compilation
|
||||||
let timerHandleForRecompilation: any; // Handle for 0.25s wait timer to trigger recompilation
|
let timerHandleForRecompilation: QueuedCompilationInfo|
|
||||||
|
undefined; // Handle for 0.25s wait timer to trigger recompilation
|
||||||
|
|
||||||
const ignoreFilesForWatch = new Set<string>();
|
const ignoreFilesForWatch = new Set<string>();
|
||||||
const fileCache = new Map<string, CacheEntry>();
|
const fileCache = new Map<string, CacheEntry>();
|
||||||
|
@ -141,13 +147,13 @@ export function performWatchCompilation(host: PerformWatchHost):
|
||||||
function close() {
|
function close() {
|
||||||
fileWatcher.close();
|
fileWatcher.close();
|
||||||
if (timerHandleForRecompilation) {
|
if (timerHandleForRecompilation) {
|
||||||
host.clearTimeout(timerHandleForRecompilation);
|
host.clearTimeout(timerHandleForRecompilation.timerHandle);
|
||||||
timerHandleForRecompilation = undefined;
|
timerHandleForRecompilation = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invoked to perform initial compilation or re-compilation in watch mode
|
// Invoked to perform initial compilation or re-compilation in watch mode
|
||||||
function doCompilation(): Diagnostics {
|
function doCompilation(modifiedResourceFiles?: Set<string>): Diagnostics {
|
||||||
if (!cachedOptions) {
|
if (!cachedOptions) {
|
||||||
cachedOptions = host.readConfiguration();
|
cachedOptions = host.readConfiguration();
|
||||||
}
|
}
|
||||||
|
@ -190,6 +196,9 @@ export function performWatchCompilation(host: PerformWatchHost):
|
||||||
}
|
}
|
||||||
return ce.content !;
|
return ce.content !;
|
||||||
};
|
};
|
||||||
|
// Provide access to the file paths that triggered this rebuild
|
||||||
|
cachedCompilerHost.getModifiedResourceFiles =
|
||||||
|
modifiedResourceFiles !== undefined ? () => modifiedResourceFiles : undefined;
|
||||||
}
|
}
|
||||||
ignoreFilesForWatch.clear();
|
ignoreFilesForWatch.clear();
|
||||||
const oldProgram = cachedProgram;
|
const oldProgram = cachedProgram;
|
||||||
|
@ -255,24 +264,30 @@ export function performWatchCompilation(host: PerformWatchHost):
|
||||||
|
|
||||||
if (!ignoreFilesForWatch.has(path.normalize(fileName))) {
|
if (!ignoreFilesForWatch.has(path.normalize(fileName))) {
|
||||||
// Ignore the file if the file is one that was written by the compiler.
|
// Ignore the file if the file is one that was written by the compiler.
|
||||||
startTimerForRecompilation();
|
startTimerForRecompilation(fileName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upon detecting a file change, wait for 250ms and then perform a recompilation. This gives batch
|
// Upon detecting a file change, wait for 250ms and then perform a recompilation. This gives batch
|
||||||
// operations (such as saving all modified files in an editor) a chance to complete before we kick
|
// operations (such as saving all modified files in an editor) a chance to complete before we kick
|
||||||
// off a new compilation.
|
// off a new compilation.
|
||||||
function startTimerForRecompilation() {
|
function startTimerForRecompilation(changedPath: string) {
|
||||||
if (timerHandleForRecompilation) {
|
if (timerHandleForRecompilation) {
|
||||||
host.clearTimeout(timerHandleForRecompilation);
|
host.clearTimeout(timerHandleForRecompilation.timerHandle);
|
||||||
|
} else {
|
||||||
|
timerHandleForRecompilation = {
|
||||||
|
modifiedResourceFiles: new Set<string>(),
|
||||||
|
timerHandle: undefined
|
||||||
|
};
|
||||||
}
|
}
|
||||||
timerHandleForRecompilation = host.setTimeout(recompile, 250);
|
timerHandleForRecompilation.timerHandle = host.setTimeout(recompile, 250);
|
||||||
|
timerHandleForRecompilation.modifiedResourceFiles.add(changedPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
function recompile() {
|
function recompile() {
|
||||||
timerHandleForRecompilation = undefined;
|
|
||||||
host.reportDiagnostics(
|
host.reportDiagnostics(
|
||||||
[createMessageDiagnostic('File change detected. Starting incremental compilation.')]);
|
[createMessageDiagnostic('File change detected. Starting incremental compilation.')]);
|
||||||
doCompilation();
|
doCompilation(timerHandleForRecompilation !.modifiedResourceFiles);
|
||||||
|
timerHandleForRecompilation = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -281,6 +281,12 @@ export interface CompilerHost extends ts.CompilerHost {
|
||||||
* rather than by path. See http://requirejs.org/docs/whyamd.html#namedmodules
|
* rather than by path. See http://requirejs.org/docs/whyamd.html#namedmodules
|
||||||
*/
|
*/
|
||||||
amdModuleName?(sf: ts.SourceFile): string|undefined;
|
amdModuleName?(sf: ts.SourceFile): string|undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the absolute paths to the changed files that triggered the current compilation
|
||||||
|
* or `undefined` if this is not an incremental build.
|
||||||
|
*/
|
||||||
|
getModifiedResourceFiles?(): Set<string>|undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum EmitFlags {
|
export enum EmitFlags {
|
||||||
|
|
|
@ -8,6 +8,7 @@ ts_library(
|
||||||
"//packages/compiler",
|
"//packages/compiler",
|
||||||
"//packages/compiler-cli",
|
"//packages/compiler-cli",
|
||||||
"//packages/compiler-cli/src/ngtsc/diagnostics",
|
"//packages/compiler-cli/src/ngtsc/diagnostics",
|
||||||
|
"//packages/compiler-cli/src/ngtsc/path",
|
||||||
"//packages/compiler-cli/src/ngtsc/routing",
|
"//packages/compiler-cli/src/ngtsc/routing",
|
||||||
"//packages/compiler-cli/src/ngtsc/util",
|
"//packages/compiler-cli/src/ngtsc/util",
|
||||||
"//packages/compiler-cli/test:test_utils",
|
"//packages/compiler-cli/test:test_utils",
|
||||||
|
|
|
@ -39,6 +39,7 @@ function setupFakeCore(support: TestSupport): void {
|
||||||
export class NgtscTestEnvironment {
|
export class NgtscTestEnvironment {
|
||||||
private multiCompileHostExt: MultiCompileHostExt|null = null;
|
private multiCompileHostExt: MultiCompileHostExt|null = null;
|
||||||
private oldProgram: Program|null = null;
|
private oldProgram: Program|null = null;
|
||||||
|
private changedResources: Set<string>|undefined = undefined;
|
||||||
|
|
||||||
private constructor(private support: TestSupport, readonly outDir: string) {}
|
private constructor(private support: TestSupport, readonly outDir: string) {}
|
||||||
|
|
||||||
|
@ -106,6 +107,7 @@ export class NgtscTestEnvironment {
|
||||||
}
|
}
|
||||||
|
|
||||||
enableMultipleCompilations(): void {
|
enableMultipleCompilations(): void {
|
||||||
|
this.changedResources = new Set();
|
||||||
this.multiCompileHostExt = new MultiCompileHostExt();
|
this.multiCompileHostExt = new MultiCompileHostExt();
|
||||||
setWrapHostForTest(makeWrapHost(this.multiCompileHostExt));
|
setWrapHostForTest(makeWrapHost(this.multiCompileHostExt));
|
||||||
}
|
}
|
||||||
|
@ -114,6 +116,7 @@ export class NgtscTestEnvironment {
|
||||||
if (this.multiCompileHostExt === null) {
|
if (this.multiCompileHostExt === null) {
|
||||||
throw new Error(`Not tracking written files - call enableMultipleCompilations()`);
|
throw new Error(`Not tracking written files - call enableMultipleCompilations()`);
|
||||||
}
|
}
|
||||||
|
this.changedResources !.clear();
|
||||||
this.multiCompileHostExt.flushWrittenFileTracking();
|
this.multiCompileHostExt.flushWrittenFileTracking();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,8 +136,9 @@ export class NgtscTestEnvironment {
|
||||||
|
|
||||||
write(fileName: string, content: string) {
|
write(fileName: string, content: string) {
|
||||||
if (this.multiCompileHostExt !== null) {
|
if (this.multiCompileHostExt !== null) {
|
||||||
const absFilePath = path.posix.resolve(this.support.basePath, fileName);
|
const absFilePath = path.resolve(this.support.basePath, fileName).replace(/\\/g, '/');
|
||||||
this.multiCompileHostExt.invalidate(absFilePath);
|
this.multiCompileHostExt.invalidate(absFilePath);
|
||||||
|
this.changedResources !.add(absFilePath);
|
||||||
}
|
}
|
||||||
this.support.write(fileName, content);
|
this.support.write(fileName, content);
|
||||||
}
|
}
|
||||||
|
@ -175,8 +179,9 @@ export class NgtscTestEnvironment {
|
||||||
program: this.oldProgram || undefined,
|
program: this.oldProgram || undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const exitCode =
|
const exitCode = main(
|
||||||
main(['-p', this.basePath], errorSpy, undefined, customTransformers, reuseProgram);
|
['-p', this.basePath], errorSpy, undefined, customTransformers, reuseProgram,
|
||||||
|
this.changedResources);
|
||||||
expect(errorSpy).not.toHaveBeenCalled();
|
expect(errorSpy).not.toHaveBeenCalled();
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
if (this.multiCompileHostExt !== null) {
|
if (this.multiCompileHostExt !== null) {
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/path';
|
||||||
|
|
||||||
import {NgtscTestEnvironment} from './env';
|
import {NgtscTestEnvironment} from './env';
|
||||||
|
|
||||||
describe('ngtsc incremental compilation', () => {
|
describe('ngtsc incremental compilation', () => {
|
||||||
|
@ -69,6 +71,33 @@ describe('ngtsc incremental compilation', () => {
|
||||||
expect(written).not.toContain('/component2.js');
|
expect(written).not.toContain('/component2.js');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should rebuild components whose templates have changed', () => {
|
||||||
|
env.write('component1.ts', `
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({selector: 'cmp', templateUrl: './component1.template.html'})
|
||||||
|
export class Cmp1 {}
|
||||||
|
`);
|
||||||
|
env.write('component1.template.html', 'cmp1');
|
||||||
|
env.write('component2.ts', `
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({selector: 'cmp2', templateUrl: './component2.template.html'})
|
||||||
|
export class Cmp2 {}
|
||||||
|
`);
|
||||||
|
env.write('component2.template.html', 'cmp2');
|
||||||
|
|
||||||
|
env.driveMain();
|
||||||
|
|
||||||
|
// Make a change to Cmp1 template
|
||||||
|
env.flushWrittenFileTracking();
|
||||||
|
env.write('component1.template.html', 'changed');
|
||||||
|
env.driveMain();
|
||||||
|
const written = env.getFilesWrittenSinceLastFlush();
|
||||||
|
expect(written).toContain('/component1.js');
|
||||||
|
expect(written).not.toContain('/component2.js');
|
||||||
|
});
|
||||||
|
|
||||||
it('should rebuild components whose partial-evaluation dependencies have changed', () => {
|
it('should rebuild components whose partial-evaluation dependencies have changed', () => {
|
||||||
env.write('component1.ts', `
|
env.write('component1.ts', `
|
||||||
import {Component} from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
|
|
Loading…
Reference in New Issue