Skip to content

Commit 17fb20f

Browse files
crisbetothePunderWoman
authored andcommitted
fix(compiler-cli): preserve defer block dependencies during HMR when class metadata is disabled (#59313)
Fixes that the compiler wasn't capturing defer block dependencies correctly when `supportTestBed` is disabled. We had tests for this, but we didn't notice the issue because the dependencies ended up being captured because of the `setClassMetadata` calls. Once they're disabled, the dependencies stopped being recorded. Fixes #59310. PR Close #59313
1 parent 06a55e9 commit 17fb20f

File tree

4 files changed

+101
-10
lines changed

4 files changed

+101
-10
lines changed

packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ export class ComponentDecoratorHandler
271271

272272
// Dependencies can't be deferred during HMR, because the HMR update module can't have
273273
// dynamic imports and its dependencies need to be passed in directly. If dependencies
274-
// are deferred, their imports will be deleted so we won't may lose the reference to them.
274+
// are deferred, their imports will be deleted so we may lose the reference to them.
275275
this.canDeferDeps = !enableHmr;
276276
}
277277

@@ -1610,10 +1610,11 @@ export class ComponentDecoratorHandler
16101610
const perComponentDeferredDeps = this.canDeferDeps
16111611
? this.resolveAllDeferredDependencies(resolution)
16121612
: null;
1613+
const defer = this.compileDeferBlocks(resolution);
16131614
const meta: R3ComponentMetadata<R3TemplateDependency> = {
16141615
...analysis.meta,
16151616
...resolution,
1616-
defer: this.compileDeferBlocks(resolution),
1617+
defer,
16171618
};
16181619
const fac = compileNgFactoryDefField(toFactoryMetadata(meta, FactoryTarget.Component));
16191620

@@ -1639,6 +1640,7 @@ export class ComponentDecoratorHandler
16391640
this.rootDirs,
16401641
def,
16411642
fac,
1643+
defer,
16421644
classMetadata,
16431645
debugInfo,
16441646
)
@@ -1680,10 +1682,11 @@ export class ComponentDecoratorHandler
16801682
const perComponentDeferredDeps = this.canDeferDeps
16811683
? this.resolveAllDeferredDependencies(resolution)
16821684
: null;
1685+
const defer = this.compileDeferBlocks(resolution);
16831686
const meta: R3ComponentMetadata<R3TemplateDependencyMetadata> = {
16841687
...analysis.meta,
16851688
...resolution,
1686-
defer: this.compileDeferBlocks(resolution),
1689+
defer,
16871690
};
16881691
const fac = compileDeclareFactory(toFactoryMetadata(meta, FactoryTarget.Component));
16891692
const inputTransformFields = compileInputTransformFields(analysis.inputs);
@@ -1703,6 +1706,7 @@ export class ComponentDecoratorHandler
17031706
this.rootDirs,
17041707
def,
17051708
fac,
1709+
defer,
17061710
classMetadata,
17071711
null,
17081712
)
@@ -1734,10 +1738,11 @@ export class ComponentDecoratorHandler
17341738
// doesn't have information on which dependencies belong to which defer blocks.
17351739
const deferrableTypes = this.canDeferDeps ? analysis.explicitlyDeferredTypes : null;
17361740

1741+
const defer = this.compileDeferBlocks(resolution);
17371742
const meta = {
17381743
...analysis.meta,
17391744
...resolution,
1740-
defer: this.compileDeferBlocks(resolution),
1745+
defer,
17411746
} as R3ComponentMetadata<R3TemplateDependency>;
17421747

17431748
if (deferrableTypes !== null) {
@@ -1763,6 +1768,7 @@ export class ComponentDecoratorHandler
17631768
this.rootDirs,
17641769
def,
17651770
fac,
1771+
defer,
17661772
classMetadata,
17671773
debugInfo,
17681774
)
@@ -1794,10 +1800,11 @@ export class ComponentDecoratorHandler
17941800

17951801
// Create a brand-new constant pool since there shouldn't be any constant sharing.
17961802
const pool = new ConstantPool();
1803+
const defer = this.compileDeferBlocks(resolution);
17971804
const meta: R3ComponentMetadata<R3TemplateDependency> = {
17981805
...analysis.meta,
17991806
...resolution,
1800-
defer: this.compileDeferBlocks(resolution),
1807+
defer,
18011808
};
18021809
const fac = compileNgFactoryDefField(toFactoryMetadata(meta, FactoryTarget.Component));
18031810
const def = compileComponentFromMetadata(meta, pool, makeBindingParser());
@@ -1817,6 +1824,7 @@ export class ComponentDecoratorHandler
18171824
this.rootDirs,
18181825
def,
18191826
fac,
1827+
defer,
18201828
classMetadata,
18211829
debugInfo,
18221830
)

packages/compiler-cli/src/ngtsc/hmr/src/extract_dependencies.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {R3CompiledExpression, R3HmrNamespaceDependency, outputAst as o} from '@angular/compiler';
9+
import {
10+
DeferBlockDepsEmitMode,
11+
R3CompiledExpression,
12+
R3ComponentDeferMetadata,
13+
R3HmrNamespaceDependency,
14+
outputAst as o,
15+
} from '@angular/compiler';
1016
import {DeclarationNode} from '../../reflection';
1117
import {CompileResult} from '../../transform';
1218
import ts from 'typescript';
@@ -16,21 +22,23 @@ import ts from 'typescript';
1622
* @param sourceFile File in which the file is being compiled.
1723
* @param definition Compiled component definition.
1824
* @param factory Compiled component factory.
25+
* @param deferBlockMetadata Metadata about the defer blocks in the component.
1926
* @param classMetadata Compiled `setClassMetadata` expression, if any.
2027
* @param debugInfo Compiled `setClassDebugInfo` expression, if any.
2128
*/
2229
export function extractHmrDependencies(
2330
node: DeclarationNode,
2431
definition: R3CompiledExpression,
2532
factory: CompileResult,
33+
deferBlockMetadata: R3ComponentDeferMetadata,
2634
classMetadata: o.Statement | null,
2735
debugInfo: o.Statement | null,
2836
): {local: string[]; external: R3HmrNamespaceDependency[]} {
2937
const name = ts.isClassDeclaration(node) && node.name ? node.name.text : null;
3038
const visitor = new PotentialTopLevelReadsVisitor();
3139
const sourceFile = node.getSourceFile();
3240

33-
// Visit all of the compiled expression to look for potential
41+
// Visit all of the compiled expressions to look for potential
3442
// local references that would have to be retained.
3543
definition.expression.visitExpression(visitor, null);
3644
definition.statements.forEach((statement) => statement.visitStatement(visitor, null));
@@ -39,6 +47,12 @@ export function extractHmrDependencies(
3947
classMetadata?.visitStatement(visitor, null);
4048
debugInfo?.visitStatement(visitor, null);
4149

50+
if (deferBlockMetadata.mode === DeferBlockDepsEmitMode.PerBlock) {
51+
deferBlockMetadata.blocks.forEach((loader) => loader?.visitExpression(visitor, null));
52+
} else {
53+
deferBlockMetadata.dependenciesFn?.visitExpression(visitor, null);
54+
}
55+
4256
// Filter out only the references to defined top-level symbols. This allows us to ignore local
4357
// variables inside of functions. Note that we filter out the class name since it is always
4458
// defined and it saves us having to repeat this logic wherever the locals are consumed.

packages/compiler-cli/src/ngtsc/hmr/src/metadata.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {R3CompiledExpression, R3HmrMetadata, outputAst as o} from '@angular/compiler';
9+
import {
10+
R3CompiledExpression,
11+
R3ComponentDeferMetadata,
12+
R3HmrMetadata,
13+
outputAst as o,
14+
} from '@angular/compiler';
1015
import {DeclarationNode, ReflectionHost} from '../../reflection';
1116
import {getProjectRelativePath} from '../../util/src/path';
1217
import {CompileResult} from '../../transform';
@@ -21,6 +26,7 @@ import ts from 'typescript';
2126
* @param rootDirs Root directories configured by the user.
2227
* @param definition Analyzed component definition.
2328
* @param factory Analyzed component factory.
29+
* @param deferBlockMetadata Metadata about the defer blocks in the component.
2430
* @param classMetadata Analyzed `setClassMetadata` expression, if any.
2531
* @param debugInfo Analyzed `setClassDebugInfo` expression, if any.
2632
*/
@@ -31,6 +37,7 @@ export function extractHmrMetatadata(
3137
rootDirs: readonly string[],
3238
definition: R3CompiledExpression,
3339
factory: CompileResult,
40+
deferBlockMetadata: R3ComponentDeferMetadata,
3441
classMetadata: o.Statement | null,
3542
debugInfo: o.Statement | null,
3643
): R3HmrMetadata | null {
@@ -43,7 +50,14 @@ export function extractHmrMetatadata(
4350
getProjectRelativePath(sourceFile, rootDirs, compilerHost) ||
4451
compilerHost.getCanonicalFileName(sourceFile.fileName);
4552

46-
const dependencies = extractHmrDependencies(clazz, definition, factory, classMetadata, debugInfo);
53+
const dependencies = extractHmrDependencies(
54+
clazz,
55+
definition,
56+
factory,
57+
deferBlockMetadata,
58+
classMetadata,
59+
debugInfo,
60+
);
4761
const meta: R3HmrMetadata = {
4862
type: new o.WrappedNodeExpr(clazz.name),
4963
className: clazz.name.text,

packages/compiler-cli/test/ngtsc/hmr_spec.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@ runInEachFileSystem(() => {
2020
env.tsconfig();
2121
});
2222

23-
function enableHmr(): void {
23+
function enableHmr(additionalOptions: Record<string, unknown> = {}): void {
2424
env.write(
2525
'tsconfig.json',
2626
JSON.stringify({
2727
extends: './tsconfig-base.json',
2828
angularCompilerOptions: {
2929
_enableHmr: true,
30+
...additionalOptions,
3031
},
3132
}),
3233
);
@@ -298,6 +299,60 @@ runInEachFileSystem(() => {
298299
expect(hmrContents).not.toContain('import(');
299300
});
300301

302+
it('should capture deferred dependencies when no class metadata is produced', () => {
303+
// `supportTestBed` determines whether we produce `setClassMetadata` calls.
304+
enableHmr({supportTestBed: false});
305+
env.write(
306+
'dep.ts',
307+
`
308+
import {Directive} from '@angular/core';
309+
310+
@Directive({
311+
selector: '[dep]',
312+
standalone: true,
313+
})
314+
export class Dep {}
315+
`,
316+
);
317+
318+
env.write(
319+
'test.ts',
320+
`
321+
import {Component} from '@angular/core';
322+
import {Dep} from './dep';
323+
324+
@Component({
325+
selector: 'cmp',
326+
standalone: true,
327+
template: '@defer (on timer(1000)) {<div dep></div>}',
328+
imports: [Dep],
329+
})
330+
export class Cmp {}
331+
`,
332+
);
333+
334+
env.driveMain();
335+
336+
const jsContents = env.getContents('test.js');
337+
const hmrContents = env.driveHmr('test.ts', 'Cmp');
338+
339+
expect(jsContents).toContain(`import { Dep } from './dep';`);
340+
expect(jsContents).toContain('const Cmp_Defer_1_DepsFn = () => [Dep];');
341+
expect(jsContents).toContain('function Cmp_Defer_0_Template(rf, ctx) { if (rf & 1) {');
342+
expect(jsContents).toContain('i0.ɵɵdefer(1, 0, Cmp_Defer_1_DepsFn);');
343+
expect(jsContents).toContain('ɵɵreplaceMetadata(Cmp, m.default, [i0], [Dep]));');
344+
expect(jsContents).not.toContain('setClassMetadata');
345+
346+
expect(hmrContents).toContain(
347+
'export default function Cmp_UpdateMetadata(Cmp, ɵɵnamespaces, Dep) {',
348+
);
349+
expect(hmrContents).toContain('const Cmp_Defer_1_DepsFn = () => [Dep];');
350+
expect(hmrContents).toContain('function Cmp_Defer_0_Template(rf, ctx) {');
351+
expect(hmrContents).toContain('ɵhmr0.ɵɵdefer(1, 0, Cmp_Defer_1_DepsFn);');
352+
expect(hmrContents).not.toContain('import(');
353+
expect(hmrContents).not.toContain('setClassMetadata');
354+
});
355+
301356
it('should not generate an HMR update function for a component that has errors', () => {
302357
enableHmr();
303358
env.write(

0 commit comments

Comments
 (0)