diff --git a/packages/compiler-cli/src/ngtsc/hmr/src/extract_dependencies.ts b/packages/compiler-cli/src/ngtsc/hmr/src/extract_dependencies.ts index f64d5c11a97..46d258f4139 100644 --- a/packages/compiler-cli/src/ngtsc/hmr/src/extract_dependencies.ts +++ b/packages/compiler-cli/src/ngtsc/hmr/src/extract_dependencies.ts @@ -196,10 +196,11 @@ class PotentialTopLevelReadsVisitor extends o.RecursiveAstVisitor { * TypeScript identifiers are used both when referring to a variable (e.g. `console.log(foo)`) * and for names (e.g. `{foo: 123}`). This function determines if the identifier is a top-level * variable read, rather than a nested name. - * @param node Identifier to check. + * @param identifier Identifier to check. */ - private isTopLevelIdentifierReference(node: ts.Identifier): boolean { - const parent = node.parent; + private isTopLevelIdentifierReference(identifier: ts.Identifier): boolean { + let node = identifier as ts.Expression; + let parent = node.parent; // The parent might be undefined for a synthetic node or if `setParentNodes` is set to false // when the SourceFile was created. We can account for such cases using the type checker, at @@ -209,6 +210,15 @@ class PotentialTopLevelReadsVisitor extends o.RecursiveAstVisitor { return false; } + // Unwrap parenthesized identifiers, but use the closest parenthesized expression + // as the reference node so that we can check cases like `{prop: ((value))}`. + if (ts.isParenthesizedExpression(parent) && parent.expression === node) { + while (parent && ts.isParenthesizedExpression(parent)) { + node = parent; + parent = parent.parent; + } + } + // Identifier referenced at the top level. Unlikely. if (ts.isSourceFile(parent)) { return true; diff --git a/packages/compiler-cli/test/ngtsc/hmr_spec.ts b/packages/compiler-cli/test/ngtsc/hmr_spec.ts index 7166bedbe0b..502bce6a063 100644 --- a/packages/compiler-cli/test/ngtsc/hmr_spec.ts +++ b/packages/compiler-cli/test/ngtsc/hmr_spec.ts @@ -549,6 +549,41 @@ runInEachFileSystem(() => { ); }); + it('should capture parenthesized dependencies', () => { + enableHmr(); + env.write( + 'test.ts', + ` + import {Component, InjectionToken} from '@angular/core'; + + const token = new InjectionToken('TEST'); + const value = 123; + const otherValue = 321; + + @Component({ + template: '', + providers: [{ + provide: token, + useFactory: () => [(value), ((((otherValue))))] + }] + }) + export class Cmp {} + `, + ); + + env.driveMain(); + + const jsContents = env.getContents('test.js'); + const hmrContents = env.driveHmr('test.ts', 'Cmp'); + expect(jsContents).toContain( + 'ɵɵreplaceMetadata(Cmp, m.default, [i0], [token, value, otherValue, Component]));', + ); + expect(jsContents).toContain('useFactory: () => [(value), ((((otherValue))))]'); + expect(hmrContents).toContain( + 'export default function Cmp_UpdateMetadata(Cmp, ɵɵnamespaces, token, value, otherValue, Component) {', + ); + }); + it('should preserve eager standalone imports in HMR even if they are not used in the template', () => { enableHmr({ // Disable class metadata since it can add noise to the test.