Skip to content

Commit

Permalink
fix(@angular/build): support per component updates of multi-component…
Browse files Browse the repository at this point in the history
… 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.
  • Loading branch information
clydin committed Feb 12, 2025
1 parent 25007e1 commit 964fb77
Showing 1 changed file with 53 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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;
}
}
Expand All @@ -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.
Expand All @@ -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<string, ts.Node>();
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;
}

Expand All @@ -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;
}

/**
Expand Down

0 comments on commit 964fb77

Please sign in to comment.