Skip to content

Commit 6486b57

Browse files
committed
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.
1 parent fd44eb1 commit 6486b57

File tree

3 files changed

+96
-43
lines changed

3 files changed

+96
-43
lines changed

packages/angular/build/src/tools/angular/compilation/angular-compilation.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export abstract class AngularCompilation {
7676
compilerOptions: ng.CompilerOptions;
7777
referencedFiles: readonly string[];
7878
externalStylesheets?: ReadonlyMap<string, string>;
79+
templateUpdates?: ReadonlyMap<string, string>;
7980
}>;
8081

8182
abstract emitAffectedFiles(): Iterable<EmitFileResult> | Promise<Iterable<EmitFileResult>>;

packages/angular/build/src/tools/angular/compilation/aot-compilation.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import type ng from '@angular/compiler-cli';
1010
import assert from 'node:assert';
11+
import { relative } from 'node:path';
1112
import ts from 'typescript';
1213
import { profileAsync, profileSync } from '../../esbuild/profiling';
1314
import {
@@ -47,6 +48,7 @@ export class AotCompilation extends AngularCompilation {
4748
compilerOptions: ng.CompilerOptions;
4849
referencedFiles: readonly string[];
4950
externalStylesheets?: ReadonlyMap<string, string>;
51+
templateUpdates?: ReadonlyMap<string, string>;
5052
}> {
5153
// Dynamically load the Angular compiler CLI package
5254
const { NgtscProgram, OptimizeFor } = await AngularCompilation.loadCompilerCli();
@@ -91,6 +93,40 @@ export class AotCompilation extends AngularCompilation {
9193
);
9294

9395
await profileAsync('NG_ANALYZE_PROGRAM', () => angularCompiler.analyzeAsync());
96+
97+
let templateUpdates;
98+
if (
99+
compilerOptions['_enableHmr'] &&
100+
hostOptions.modifiedFiles &&
101+
hasOnlyTemplates(hostOptions.modifiedFiles)
102+
) {
103+
const componentNodes = [...hostOptions.modifiedFiles].flatMap((file) => [
104+
...angularCompiler.getComponentsWithTemplateFile(file),
105+
]);
106+
107+
for (const node of componentNodes) {
108+
if (!ts.isClassDeclaration(node)) {
109+
continue;
110+
}
111+
const componentFilename = node.getSourceFile().fileName;
112+
let relativePath = relative(host.getCurrentDirectory(), componentFilename);
113+
if (relativePath.startsWith('..')) {
114+
relativePath = componentFilename;
115+
}
116+
const updateId = encodeURIComponent(
117+
`${host.getCanonicalFileName(relativePath)}@${node.name?.text}`,
118+
);
119+
const updateText = angularCompiler.emitHmrUpdateModule(node);
120+
if (updateText === null) {
121+
// Build is needed if a template cannot be updated
122+
templateUpdates = undefined;
123+
break;
124+
}
125+
templateUpdates ??= new Map<string, string>();
126+
templateUpdates.set(updateId, updateText);
127+
}
128+
}
129+
94130
const affectedFiles = profileSync('NG_FIND_AFFECTED', () =>
95131
findAffectedFiles(typeScriptProgram, angularCompiler, usingBuildInfo),
96132
);
@@ -131,6 +167,7 @@ export class AotCompilation extends AngularCompilation {
131167
compilerOptions,
132168
referencedFiles,
133169
externalStylesheets: hostOptions.externalStylesheets,
170+
templateUpdates,
134171
};
135172
}
136173

@@ -385,3 +422,16 @@ function findAffectedFiles(
385422

386423
return affectedFiles;
387424
}
425+
426+
function hasOnlyTemplates(modifiedFiles: Set<string>): boolean {
427+
for (const file of modifiedFiles) {
428+
const lowerFile = file.toLowerCase();
429+
if (lowerFile.endsWith('.html') || lowerFile.endsWith('.svg')) {
430+
continue;
431+
}
432+
433+
return false;
434+
}
435+
436+
return true;
437+
}

packages/angular/build/src/tools/angular/compilation/parallel-worker.ts

Lines changed: 45 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -42,60 +42,62 @@ export async function initialize(request: InitRequest) {
4242
}
4343
});
4444

45-
const { compilerOptions, referencedFiles, externalStylesheets } = await compilation.initialize(
46-
request.tsconfig,
47-
{
48-
fileReplacements: request.fileReplacements,
49-
sourceFileCache,
50-
modifiedFiles: sourceFileCache.modifiedFiles,
51-
transformStylesheet(data, containingFile, stylesheetFile, order, className) {
52-
const requestId = randomUUID();
53-
const resultPromise = new Promise<string>((resolve, reject) =>
54-
stylesheetRequests.set(requestId, [resolve, reject]),
55-
);
56-
57-
request.stylesheetPort.postMessage({
58-
requestId,
59-
data,
60-
containingFile,
61-
stylesheetFile,
62-
order,
63-
className,
64-
});
65-
66-
return resultPromise;
45+
const { compilerOptions, referencedFiles, externalStylesheets, templateUpdates } =
46+
await compilation.initialize(
47+
request.tsconfig,
48+
{
49+
fileReplacements: request.fileReplacements,
50+
sourceFileCache,
51+
modifiedFiles: sourceFileCache.modifiedFiles,
52+
transformStylesheet(data, containingFile, stylesheetFile, order, className) {
53+
const requestId = randomUUID();
54+
const resultPromise = new Promise<string>((resolve, reject) =>
55+
stylesheetRequests.set(requestId, [resolve, reject]),
56+
);
57+
58+
request.stylesheetPort.postMessage({
59+
requestId,
60+
data,
61+
containingFile,
62+
stylesheetFile,
63+
order,
64+
className,
65+
});
66+
67+
return resultPromise;
68+
},
69+
processWebWorker(workerFile, containingFile) {
70+
Atomics.store(request.webWorkerSignal, 0, 0);
71+
request.webWorkerPort.postMessage({ workerFile, containingFile });
72+
73+
Atomics.wait(request.webWorkerSignal, 0, 0);
74+
const result = receiveMessageOnPort(request.webWorkerPort)?.message;
75+
76+
if (result?.error) {
77+
throw result.error;
78+
}
79+
80+
return result?.workerCodeFile ?? workerFile;
81+
},
6782
},
68-
processWebWorker(workerFile, containingFile) {
69-
Atomics.store(request.webWorkerSignal, 0, 0);
70-
request.webWorkerPort.postMessage({ workerFile, containingFile });
83+
(compilerOptions) => {
84+
Atomics.store(request.optionsSignal, 0, 0);
85+
request.optionsPort.postMessage(compilerOptions);
7186

72-
Atomics.wait(request.webWorkerSignal, 0, 0);
73-
const result = receiveMessageOnPort(request.webWorkerPort)?.message;
87+
Atomics.wait(request.optionsSignal, 0, 0);
88+
const result = receiveMessageOnPort(request.optionsPort)?.message;
7489

7590
if (result?.error) {
7691
throw result.error;
7792
}
7893

79-
return result?.workerCodeFile ?? workerFile;
94+
return result?.transformedOptions ?? compilerOptions;
8095
},
81-
},
82-
(compilerOptions) => {
83-
Atomics.store(request.optionsSignal, 0, 0);
84-
request.optionsPort.postMessage(compilerOptions);
85-
86-
Atomics.wait(request.optionsSignal, 0, 0);
87-
const result = receiveMessageOnPort(request.optionsPort)?.message;
88-
89-
if (result?.error) {
90-
throw result.error;
91-
}
92-
93-
return result?.transformedOptions ?? compilerOptions;
94-
},
95-
);
96+
);
9697

9798
return {
9899
externalStylesheets,
100+
templateUpdates,
99101
referencedFiles,
100102
// TODO: Expand? `allowJs`, `isolatedModules`, `sourceMap`, `inlineSourceMap` are the only fields needed currently.
101103
compilerOptions: {

0 commit comments

Comments
 (0)