From 964fb778b7d9e4811a6987eddc4f0a010bb713f6 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 29 Jan 2025 14:44:45 -0500 Subject: [PATCH] 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. --- .../angular/compilation/hmr-candidates.ts | 73 ++++++++++++++----- 1 file changed, 53 insertions(+), 20 deletions(-) diff --git a/packages/angular/build/src/tools/angular/compilation/hmr-candidates.ts b/packages/angular/build/src/tools/angular/compilation/hmr-candidates.ts index bc271d160cad..e10b935907c0 100644 --- a/packages/angular/build/src/tools/angular/compilation/hmr-candidates.ts +++ b/packages/angular/build/src/tools/angular/compilation/hmr-candidates.ts @@ -176,14 +176,13 @@ function analyzeFileUpdates( } // Compare component meta decorator object literals - if ( - hasUnsupportedMetaUpdates( - staleDecoratorExpression, - stale, - updatedDecoratorExpression, - updated, - ) - ) { + const analysis = analyzeMetaUpdates( + staleDecoratorExpression, + stale, + updatedDecoratorExpression, + updated, + ); + if (analysis === MetaUpdateAnalysis.Unsupported) { return null; } @@ -194,7 +193,9 @@ function analyzeFileUpdates( } // If all previous class checks passed, this class is supported for HMR updates - candidates.push(updatedNode); + if (analysis === MetaUpdateAnalysis.Supported) { + candidates.push(updatedNode); + } continue; } } @@ -213,7 +214,19 @@ function analyzeFileUpdates( /** * The set of Angular component metadata fields that are supported by HMR updates. */ -const SUPPORTED_FIELDS = new Set(['template', 'templateUrl', 'styles', 'styleUrl', 'stylesUrl']); +const SUPPORTED_FIELD_NAMES = new Set([ + 'template', + 'templateUrl', + 'styles', + 'styleUrl', + 'stylesUrl', +]); + +enum MetaUpdateAnalysis { + Supported, + Unsupported, + None, +} /** * 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 * @param staleSource The source file instance containing the stale call instance. * @param updatedCall A call expression instance. * @param updatedSource The source file instance containing the updated call instance. - * @returns true, if unsupported metadata updates are present; false, otherwise. + * @returns A MetaUpdateAnalysis enum value. */ -function hasUnsupportedMetaUpdates( +function analyzeMetaUpdates( staleCall: ts.CallExpression, staleSource: ts.SourceFile, updatedCall: ts.CallExpression, updatedSource: ts.SourceFile, -): boolean { +): MetaUpdateAnalysis { const staleObject = staleCall.arguments[0]; const updatedObject = updatedCall.arguments[0]; + let hasSupportedUpdate = false; if (!ts.isObjectLiteralExpression(staleObject) || !ts.isObjectLiteralExpression(updatedObject)) { - return true; + return MetaUpdateAnalysis.Unsupported; } + const supportedFields = new Map(); const unsupportedFields: ts.Node[] = []; for (const property of staleObject.properties) { if (!ts.isPropertyAssignment(property) || ts.isComputedPropertyName(property.name)) { // Unsupported object literal property - return true; + return MetaUpdateAnalysis.Unsupported; } const name = property.name.text; - if (SUPPORTED_FIELDS.has(name)) { + if (SUPPORTED_FIELD_NAMES.has(name)) { + supportedFields.set(name, property.initializer); continue; } @@ -257,21 +273,38 @@ function hasUnsupportedMetaUpdates( for (const property of updatedObject.properties) { if (!ts.isPropertyAssignment(property) || ts.isComputedPropertyName(property.name)) { // Unsupported object literal property - return true; + return MetaUpdateAnalysis.Unsupported; } const name = property.name.text; - if (SUPPORTED_FIELDS.has(name)) { + if (SUPPORTED_FIELD_NAMES.has(name)) { + const staleInitializer = supportedFields.get(name); + // If the supported field was added or has its content changed, there has been a supported update + if ( + !staleInitializer || + !equalRangeText(property.initializer, updatedSource, staleInitializer, staleSource) + ) { + hasSupportedUpdate = true; + } + // Remove the field entry to allow tracking removed fields + supportedFields.delete(name); continue; } // Compare in order if (!equalRangeText(property.initializer, updatedSource, unsupportedFields[i++], staleSource)) { - return true; + return MetaUpdateAnalysis.Unsupported; } } - return i !== unsupportedFields.length; + if (i !== unsupportedFields.length) { + return MetaUpdateAnalysis.Unsupported; + } + + // Any remaining supported field indicates a field removal. This is also considered a supported update. + hasSupportedUpdate ||= supportedFields.size > 0; + + return hasSupportedUpdate ? MetaUpdateAnalysis.Supported : MetaUpdateAnalysis.None; } /**