diff --git a/packages/angular/build/src/tools/angular/compilation/angular-compilation.ts b/packages/angular/build/src/tools/angular/compilation/angular-compilation.ts index 4cb4852e54f7..2e44e0bdab33 100644 --- a/packages/angular/build/src/tools/angular/compilation/angular-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/angular-compilation.ts @@ -76,6 +76,7 @@ export abstract class AngularCompilation { compilerOptions: ng.CompilerOptions; referencedFiles: readonly string[]; externalStylesheets?: ReadonlyMap; + templateUpdates?: ReadonlyMap; }>; abstract emitAffectedFiles(): Iterable | Promise>; 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 9e566803fb58..cbfe70a3e5e5 100644 --- a/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts @@ -8,6 +8,7 @@ import type ng from '@angular/compiler-cli'; import assert from 'node:assert'; +import { relative } from 'node:path'; import ts from 'typescript'; import { profileAsync, profileSync } from '../../esbuild/profiling'; import { @@ -47,6 +48,7 @@ export class AotCompilation extends AngularCompilation { compilerOptions: ng.CompilerOptions; referencedFiles: readonly string[]; externalStylesheets?: ReadonlyMap; + templateUpdates?: ReadonlyMap; }> { // Dynamically load the Angular compiler CLI package const { NgtscProgram, OptimizeFor } = await AngularCompilation.loadCompilerCli(); @@ -91,6 +93,40 @@ 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), + ]); + + for (const node of componentNodes) { + if (!ts.isClassDeclaration(node)) { + continue; + } + const componentFilename = node.getSourceFile().fileName; + let relativePath = relative(host.getCurrentDirectory(), componentFilename); + if (relativePath.startsWith('..')) { + relativePath = componentFilename; + } + const updateId = encodeURIComponent( + `${host.getCanonicalFileName(relativePath)}@${node.name?.text}`, + ); + const updateText = angularCompiler.emitHmrUpdateModule(node); + if (updateText === null) { + // Build is needed if a template cannot be updated + templateUpdates = undefined; + break; + } + templateUpdates ??= new Map(); + templateUpdates.set(updateId, updateText); + } + } + const affectedFiles = profileSync('NG_FIND_AFFECTED', () => findAffectedFiles(typeScriptProgram, angularCompiler, usingBuildInfo), ); @@ -131,6 +167,7 @@ export class AotCompilation extends AngularCompilation { compilerOptions, referencedFiles, externalStylesheets: hostOptions.externalStylesheets, + templateUpdates, }; } @@ -385,3 +422,16 @@ function findAffectedFiles( return affectedFiles; } + +function hasOnlyTemplates(modifiedFiles: Set): boolean { + for (const file of modifiedFiles) { + const lowerFile = file.toLowerCase(); + if (lowerFile.endsWith('.html') || lowerFile.endsWith('.svg')) { + continue; + } + + return false; + } + + return true; +} diff --git a/packages/angular/build/src/tools/angular/compilation/parallel-worker.ts b/packages/angular/build/src/tools/angular/compilation/parallel-worker.ts index a9d1816ac76d..2669951c12e4 100644 --- a/packages/angular/build/src/tools/angular/compilation/parallel-worker.ts +++ b/packages/angular/build/src/tools/angular/compilation/parallel-worker.ts @@ -42,60 +42,62 @@ export async function initialize(request: InitRequest) { } }); - const { compilerOptions, referencedFiles, externalStylesheets } = await compilation.initialize( - request.tsconfig, - { - fileReplacements: request.fileReplacements, - sourceFileCache, - modifiedFiles: sourceFileCache.modifiedFiles, - transformStylesheet(data, containingFile, stylesheetFile, order, className) { - const requestId = randomUUID(); - const resultPromise = new Promise((resolve, reject) => - stylesheetRequests.set(requestId, [resolve, reject]), - ); - - request.stylesheetPort.postMessage({ - requestId, - data, - containingFile, - stylesheetFile, - order, - className, - }); - - return resultPromise; + const { compilerOptions, referencedFiles, externalStylesheets, templateUpdates } = + await compilation.initialize( + request.tsconfig, + { + fileReplacements: request.fileReplacements, + sourceFileCache, + modifiedFiles: sourceFileCache.modifiedFiles, + transformStylesheet(data, containingFile, stylesheetFile, order, className) { + const requestId = randomUUID(); + const resultPromise = new Promise((resolve, reject) => + stylesheetRequests.set(requestId, [resolve, reject]), + ); + + request.stylesheetPort.postMessage({ + requestId, + data, + containingFile, + stylesheetFile, + order, + className, + }); + + return resultPromise; + }, + processWebWorker(workerFile, containingFile) { + Atomics.store(request.webWorkerSignal, 0, 0); + request.webWorkerPort.postMessage({ workerFile, containingFile }); + + Atomics.wait(request.webWorkerSignal, 0, 0); + const result = receiveMessageOnPort(request.webWorkerPort)?.message; + + if (result?.error) { + throw result.error; + } + + return result?.workerCodeFile ?? workerFile; + }, }, - processWebWorker(workerFile, containingFile) { - Atomics.store(request.webWorkerSignal, 0, 0); - request.webWorkerPort.postMessage({ workerFile, containingFile }); + (compilerOptions) => { + Atomics.store(request.optionsSignal, 0, 0); + request.optionsPort.postMessage(compilerOptions); - Atomics.wait(request.webWorkerSignal, 0, 0); - const result = receiveMessageOnPort(request.webWorkerPort)?.message; + Atomics.wait(request.optionsSignal, 0, 0); + const result = receiveMessageOnPort(request.optionsPort)?.message; if (result?.error) { throw result.error; } - return result?.workerCodeFile ?? workerFile; + return result?.transformedOptions ?? compilerOptions; }, - }, - (compilerOptions) => { - Atomics.store(request.optionsSignal, 0, 0); - request.optionsPort.postMessage(compilerOptions); - - Atomics.wait(request.optionsSignal, 0, 0); - const result = receiveMessageOnPort(request.optionsPort)?.message; - - if (result?.error) { - throw result.error; - } - - return result?.transformedOptions ?? compilerOptions; - }, - ); + ); return { externalStylesheets, + templateUpdates, referencedFiles, // TODO: Expand? `allowJs`, `isolatedModules`, `sourceMap`, `inlineSourceMap` are the only fields needed currently. compilerOptions: {