Skip to content

Commit 964fb77

Browse files
committed
fix(@angular/build): support per component updates of multi-component files
The HMR component update candidate analysis has been improved to now perform more fine-grained checks for cases where more than one component is present in a single TypeScript file. Previously it was possible for all components present in a TypeScript file to be considered update candidates when only one of the components had relevant changes to its template and/or styles.
1 parent 25007e1 commit 964fb77

File tree

1 file changed

+53
-20
lines changed

1 file changed

+53
-20
lines changed

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

+53-20
Original file line numberDiff line numberDiff line change
@@ -176,14 +176,13 @@ function analyzeFileUpdates(
176176
}
177177

178178
// Compare component meta decorator object literals
179-
if (
180-
hasUnsupportedMetaUpdates(
181-
staleDecoratorExpression,
182-
stale,
183-
updatedDecoratorExpression,
184-
updated,
185-
)
186-
) {
179+
const analysis = analyzeMetaUpdates(
180+
staleDecoratorExpression,
181+
stale,
182+
updatedDecoratorExpression,
183+
updated,
184+
);
185+
if (analysis === MetaUpdateAnalysis.Unsupported) {
187186
return null;
188187
}
189188

@@ -194,7 +193,9 @@ function analyzeFileUpdates(
194193
}
195194

196195
// If all previous class checks passed, this class is supported for HMR updates
197-
candidates.push(updatedNode);
196+
if (analysis === MetaUpdateAnalysis.Supported) {
197+
candidates.push(updatedNode);
198+
}
198199
continue;
199200
}
200201
}
@@ -213,7 +214,19 @@ function analyzeFileUpdates(
213214
/**
214215
* The set of Angular component metadata fields that are supported by HMR updates.
215216
*/
216-
const SUPPORTED_FIELDS = new Set(['template', 'templateUrl', 'styles', 'styleUrl', 'stylesUrl']);
217+
const SUPPORTED_FIELD_NAMES = new Set([
218+
'template',
219+
'templateUrl',
220+
'styles',
221+
'styleUrl',
222+
'stylesUrl',
223+
]);
224+
225+
enum MetaUpdateAnalysis {
226+
Supported,
227+
Unsupported,
228+
None,
229+
}
217230

218231
/**
219232
* Analyzes the metadata fields of a decorator call expression for unsupported HMR updates.
@@ -222,31 +235,34 @@ const SUPPORTED_FIELDS = new Set(['template', 'templateUrl', 'styles', 'styleUrl
222235
* @param staleSource The source file instance containing the stale call instance.
223236
* @param updatedCall A call expression instance.
224237
* @param updatedSource The source file instance containing the updated call instance.
225-
* @returns true, if unsupported metadata updates are present; false, otherwise.
238+
* @returns A MetaUpdateAnalysis enum value.
226239
*/
227-
function hasUnsupportedMetaUpdates(
240+
function analyzeMetaUpdates(
228241
staleCall: ts.CallExpression,
229242
staleSource: ts.SourceFile,
230243
updatedCall: ts.CallExpression,
231244
updatedSource: ts.SourceFile,
232-
): boolean {
245+
): MetaUpdateAnalysis {
233246
const staleObject = staleCall.arguments[0];
234247
const updatedObject = updatedCall.arguments[0];
248+
let hasSupportedUpdate = false;
235249

236250
if (!ts.isObjectLiteralExpression(staleObject) || !ts.isObjectLiteralExpression(updatedObject)) {
237-
return true;
251+
return MetaUpdateAnalysis.Unsupported;
238252
}
239253

254+
const supportedFields = new Map<string, ts.Node>();
240255
const unsupportedFields: ts.Node[] = [];
241256

242257
for (const property of staleObject.properties) {
243258
if (!ts.isPropertyAssignment(property) || ts.isComputedPropertyName(property.name)) {
244259
// Unsupported object literal property
245-
return true;
260+
return MetaUpdateAnalysis.Unsupported;
246261
}
247262

248263
const name = property.name.text;
249-
if (SUPPORTED_FIELDS.has(name)) {
264+
if (SUPPORTED_FIELD_NAMES.has(name)) {
265+
supportedFields.set(name, property.initializer);
250266
continue;
251267
}
252268

@@ -257,21 +273,38 @@ function hasUnsupportedMetaUpdates(
257273
for (const property of updatedObject.properties) {
258274
if (!ts.isPropertyAssignment(property) || ts.isComputedPropertyName(property.name)) {
259275
// Unsupported object literal property
260-
return true;
276+
return MetaUpdateAnalysis.Unsupported;
261277
}
262278

263279
const name = property.name.text;
264-
if (SUPPORTED_FIELDS.has(name)) {
280+
if (SUPPORTED_FIELD_NAMES.has(name)) {
281+
const staleInitializer = supportedFields.get(name);
282+
// If the supported field was added or has its content changed, there has been a supported update
283+
if (
284+
!staleInitializer ||
285+
!equalRangeText(property.initializer, updatedSource, staleInitializer, staleSource)
286+
) {
287+
hasSupportedUpdate = true;
288+
}
289+
// Remove the field entry to allow tracking removed fields
290+
supportedFields.delete(name);
265291
continue;
266292
}
267293

268294
// Compare in order
269295
if (!equalRangeText(property.initializer, updatedSource, unsupportedFields[i++], staleSource)) {
270-
return true;
296+
return MetaUpdateAnalysis.Unsupported;
271297
}
272298
}
273299

274-
return i !== unsupportedFields.length;
300+
if (i !== unsupportedFields.length) {
301+
return MetaUpdateAnalysis.Unsupported;
302+
}
303+
304+
// Any remaining supported field indicates a field removal. This is also considered a supported update.
305+
hasSupportedUpdate ||= supportedFields.size > 0;
306+
307+
return hasSupportedUpdate ? MetaUpdateAnalysis.Supported : MetaUpdateAnalysis.None;
275308
}
276309

277310
/**

0 commit comments

Comments
 (0)