fix(compiler-cli): extract parenthesized dependencies during HMR (#59644)

Fixes that the HMR dependency extraction logic wasn't accounting for parenthesized identifiers correctly.

PR Close #59644
This commit is contained in:
Kristiyan Kostadinov 2025-01-21 11:00:36 +01:00 committed by Andrew Kushnir
parent 65f51e16aa
commit 67be7d2e06
2 changed files with 48 additions and 3 deletions

View file

@ -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;

View file

@ -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<number>('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.