From 80095b80059a4cafb75b8897cd8508d13f9f92e6 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Sun, 20 Oct 2024 11:23:58 -0400 Subject: [PATCH] refactor(@angular/build): add internal support for generating template update functions The internal AOT Angular compilation processing can now generate the newly introduced template update functions during development server rebuilds of template only file changes. These template update functions are not yet used but provide the infrastructure to enable template hot replacement in a future change. --- .../compilation/angular-compilation.ts | 1 + .../angular/compilation/aot-compilation.ts | 50 +++++++++++ .../angular/compilation/parallel-worker.ts | 88 ++++++++++--------- 3 files changed, 96 insertions(+), 43 deletions(-) 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: {