Skip to content

Commit ee99879

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 ce3b664 commit ee99879

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
@@ -272,7 +272,7 @@ export class ComponentDecoratorHandler
272272

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

@@ -1617,10 +1617,11 @@ export class ComponentDecoratorHandler
16171617
const perComponentDeferredDeps = this.canDeferDeps
16181618
? this.resolveAllDeferredDependencies(resolution)
16191619
: null;
1620+
const defer = this.compileDeferBlocks(resolution);
16201621
const meta: R3ComponentMetadata<R3TemplateDependency> = {
16211622
...analysis.meta,
16221623
...resolution,
1623-
defer: this.compileDeferBlocks(resolution),
1624+
defer,
16241625
};
16251626
const fac = compileNgFactoryDefField(toFactoryMetadata(meta, FactoryTarget.Component));
16261627

@@ -1646,6 +1647,7 @@ export class ComponentDecoratorHandler
16461647
this.rootDirs,
16471648
def,
16481649
fac,
1650+
defer,
16491651
classMetadata,
16501652
debugInfo,
16511653
)
@@ -1687,10 +1689,11 @@ export class ComponentDecoratorHandler
16871689
const perComponentDeferredDeps = this.canDeferDeps
16881690
? this.resolveAllDeferredDependencies(resolution)
16891691
: null;
1692+
const defer = this.compileDeferBlocks(resolution);
16901693
const meta: R3ComponentMetadata<R3TemplateDependencyMetadata> = {
16911694
...analysis.meta,
16921695
...resolution,
1693-
defer: this.compileDeferBlocks(resolution),
1696+
defer,
16941697
};
16951698
const fac = compileDeclareFactory(toFactoryMetadata(meta, FactoryTarget.Component));
16961699
const inputTransformFields = compileInputTransformFields(analysis.inputs);
@@ -1710,6 +1713,7 @@ export class ComponentDecoratorHandler
17101713
this.rootDirs,
17111714
def,
17121715
fac,
1716+
defer,
17131717
classMetadata,
17141718
null,
17151719
)
@@ -1741,10 +1745,11 @@ export class ComponentDecoratorHandler
17411745
// doesn't have information on which dependencies belong to which defer blocks.
17421746
const deferrableTypes = this.canDeferDeps ? analysis.explicitlyDeferredTypes : null;
17431747

1748+
const defer = this.compileDeferBlocks(resolution);
17441749
const meta = {
17451750
...analysis.meta,
17461751
...resolution,
1747-
defer: this.compileDeferBlocks(resolution),
1752+
defer,
17481753
} as R3ComponentMetadata<R3TemplateDependency>;
17491754

17501755
if (deferrableTypes !== null) {
@@ -1770,6 +1775,7 @@ export class ComponentDecoratorHandler
17701775
this.rootDirs,
17711776
def,
17721777
fac,
1778+
defer,
17731779
classMetadata,
17741780
debugInfo,
17751781
)
@@ -1801,10 +1807,11 @@ export class ComponentDecoratorHandler
18011807

18021808
// Create a brand-new constant pool since there shouldn't be any constant sharing.
18031809
const pool = new ConstantPool();
1810+
const defer = this.compileDeferBlocks(resolution);
18041811
const meta: R3ComponentMetadata<R3TemplateDependency> = {
18051812
...analysis.meta,
18061813
...resolution,
1807-
defer: this.compileDeferBlocks(resolution),
1814+
defer,
18081815
};
18091816
const fac = compileNgFactoryDefField(toFactoryMetadata(meta, FactoryTarget.Component));
18101817
const def = compileComponentFromMetadata(meta, pool, makeBindingParser());
@@ -1824,6 +1831,7 @@ export class ComponentDecoratorHandler
18241831
this.rootDirs,
18251832
def,
18261833
fac,
1834+
defer,
18271835
classMetadata,
18281836
debugInfo,
18291837
)

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.fileName, 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)