diff --git a/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts b/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts index cbfe70a3e5e5..787c82b4127e 100644 --- a/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts @@ -66,6 +66,18 @@ export class AotCompilation extends AngularCompilation { hostOptions.externalStylesheets ??= new Map(); } + // Collect stale source files for HMR analysis of inline component resources + let staleSourceFiles; + if (compilerOptions['_enableHmr'] && hostOptions.modifiedFiles && this.#state) { + for (const modifiedFile of hostOptions.modifiedFiles) { + const sourceFile = this.#state.typeScriptProgram.getSourceFile(modifiedFile); + if (sourceFile) { + staleSourceFiles ??= new Map(); + staleSourceFiles.set(modifiedFile, sourceFile); + } + } + } + // Create Angular compiler host const host = createAngularCompilerHost(ts, compilerOptions, hostOptions); @@ -95,14 +107,12 @@ export class AotCompilation extends AngularCompilation { await profileAsync('NG_ANALYZE_PROGRAM', () => angularCompiler.analyzeAsync()); let templateUpdates; - if ( - compilerOptions['_enableHmr'] && - hostOptions.modifiedFiles && - hasOnlyTemplates(hostOptions.modifiedFiles) - ) { - const componentNodes = [...hostOptions.modifiedFiles].flatMap((file) => [ - ...angularCompiler.getComponentsWithTemplateFile(file), - ]); + if (compilerOptions['_enableHmr'] && hostOptions.modifiedFiles && this.#state) { + const componentNodes = collectHmrCandidates( + hostOptions.modifiedFiles, + angularProgram, + staleSourceFiles, + ); for (const node of componentNodes) { if (!ts.isClassDeclaration(node)) { @@ -423,15 +433,46 @@ function findAffectedFiles( return affectedFiles; } -function hasOnlyTemplates(modifiedFiles: Set): boolean { +function collectHmrCandidates( + modifiedFiles: Set, + { compiler }: ng.NgtscProgram, + staleSourceFiles: Map | undefined, +): Set { + const candidates = new Set(); + for (const file of modifiedFiles) { - const lowerFile = file.toLowerCase(); - if (lowerFile.endsWith('.html') || lowerFile.endsWith('.svg')) { + const templateFileNodes = compiler.getComponentsWithTemplateFile(file); + if (templateFileNodes.size) { + templateFileNodes.forEach((node) => candidates.add(node as ts.ClassDeclaration)); + continue; + } + + const styleFileNodes = compiler.getComponentsWithStyleFile(file); + if (styleFileNodes.size) { + styleFileNodes.forEach((node) => candidates.add(node as ts.ClassDeclaration)); continue; } - return false; + const staleSource = staleSourceFiles?.get(file); + if (staleSource === undefined) { + // Unknown file requires a rebuild so clear out the candidates and stop collecting + candidates.clear(); + break; + } + + const updatedSource = compiler.getCurrentProgram().getSourceFile(file); + if (updatedSource === undefined) { + // No longer existing program file requires a rebuild so clear out the candidates and stop collecting + candidates.clear(); + break; + } + + // Compare the stale and updated file for changes + + // TODO: Implement -- for now assume a rebuild is needed + candidates.clear(); + break; } - return true; + return candidates; }