Skip to content

Commit 9f99196

Browse files
crisbetopkozlowski-opensource
authored andcommitted
fix(compiler-cli): account for multiple generated namespace imports in HMR (#58924)
The current HMR compiler assumes that there will only be one namespace import in the generated code (`@angular/core`). This is incorrect, because the compiler may need to generate additional imports in some cases (e.g. importing directives through a module). These changes adjust the compiler to capture all the namespaces in an array and pass them along. Fixes #58915. PR Close #58924
1 parent d6f4b10 commit 9f99196

File tree

8 files changed

+161
-38
lines changed

8 files changed

+161
-38
lines changed

packages/compiler-cli/src/ngtsc/hmr/src/extract_locals.ts renamed to packages/compiler-cli/src/ngtsc/hmr/src/extract_dependencies.ts

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

9-
import {R3CompiledExpression, outputAst as o} from '@angular/compiler';
9+
import {R3CompiledExpression, R3HmrNamespaceDependency, outputAst as o} from '@angular/compiler';
1010
import {DeclarationNode} from '../../reflection';
1111
import {CompileResult} from '../../transform';
1212
import ts from 'typescript';
1313

1414
/**
15-
* Determines the names of the file-level locals that the HMR
16-
* initializer needs to capture and pass along.
15+
* Determines the file-level dependencies that the HMR initializer needs to capture and pass along.
1716
* @param sourceFile File in which the file is being compiled.
1817
* @param definition Compiled component definition.
1918
* @param factory Compiled component factory.
2019
* @param classMetadata Compiled `setClassMetadata` expression, if any.
2120
* @param debugInfo Compiled `setClassDebugInfo` expression, if any.
2221
*/
23-
export function extractHmrLocals(
22+
export function extractHmrDependencies(
2423
node: DeclarationNode,
2524
definition: R3CompiledExpression,
2625
factory: CompileResult,
2726
classMetadata: o.Statement | null,
2827
debugInfo: o.Statement | null,
29-
): string[] {
28+
): {local: string[]; external: R3HmrNamespaceDependency[]} {
3029
const name = ts.isClassDeclaration(node) && node.name ? node.name.text : null;
3130
const visitor = new PotentialTopLevelReadsVisitor();
3231
const sourceFile = node.getSourceFile();
@@ -44,7 +43,14 @@ export function extractHmrLocals(
4443
// variables inside of functions. Note that we filter out the class name since it is always
4544
// defined and it saves us having to repeat this logic wherever the locals are consumed.
4645
const availableTopLevel = getTopLevelDeclarationNames(sourceFile);
47-
return Array.from(visitor.allReads).filter((r) => r !== name && availableTopLevel.has(r));
46+
47+
return {
48+
local: Array.from(visitor.allReads).filter((r) => r !== name && availableTopLevel.has(r)),
49+
external: Array.from(visitor.namespaceReads, (name, index) => ({
50+
moduleName: name,
51+
assignedName: `ɵhmr${index}`,
52+
})),
53+
};
4854
}
4955

5056
/**
@@ -138,6 +144,14 @@ function trackBindingName(node: ts.BindingName, results: Set<string>): void {
138144
*/
139145
class PotentialTopLevelReadsVisitor extends o.RecursiveAstVisitor {
140146
readonly allReads = new Set<string>();
147+
readonly namespaceReads = new Set<string>();
148+
149+
override visitExternalExpr(ast: o.ExternalExpr, context: any) {
150+
if (ast.value.moduleName !== null) {
151+
this.namespaceReads.add(ast.value.moduleName);
152+
}
153+
super.visitExternalExpr(ast, context);
154+
}
141155

142156
override visitReadVarExpr(ast: o.ReadVarExpr, context: any) {
143157
this.allReads.add(ast.name);

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {R3CompiledExpression, R3HmrMetadata, outputAst as o} from '@angular/comp
1010
import {DeclarationNode, ReflectionHost} from '../../reflection';
1111
import {getProjectRelativePath} from '../../util/src/path';
1212
import {CompileResult} from '../../transform';
13-
import {extractHmrLocals} from './extract_locals';
13+
import {extractHmrDependencies} from './extract_dependencies';
1414
import ts from 'typescript';
1515

1616
/**
@@ -43,12 +43,13 @@ export function extractHmrMetatadata(
4343
getProjectRelativePath(sourceFile, rootDirs, compilerHost) ||
4444
compilerHost.getCanonicalFileName(sourceFile.fileName);
4545

46+
const dependencies = extractHmrDependencies(clazz, definition, factory, classMetadata, debugInfo);
4647
const meta: R3HmrMetadata = {
4748
type: new o.WrappedNodeExpr(clazz.name),
4849
className: clazz.name.text,
4950
filePath,
50-
locals: extractHmrLocals(clazz, definition, factory, classMetadata, debugInfo),
51-
coreName: '__ngCore__',
51+
localDependencies: dependencies.local,
52+
namespaceDependencies: dependencies.external,
5253
};
5354

5455
return meta;

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@ export function getHmrUpdateDeclaration(
2727
meta: R3HmrMetadata,
2828
sourceFile: ts.SourceFile,
2929
): ts.FunctionDeclaration {
30-
const importRewriter = new HmrModuleImportRewriter(meta.coreName);
30+
const namespaceSpecifiers = meta.namespaceDependencies.reduce((result, current) => {
31+
result.set(current.moduleName, current.assignedName);
32+
return result;
33+
}, new Map<string, string>());
34+
const importRewriter = new HmrModuleImportRewriter(namespaceSpecifiers);
3135
const importManager = new ImportManager({
3236
...presetImportManagerForceNamespaceImports,
3337
rewriter: importRewriter,
@@ -52,12 +56,11 @@ export function getHmrUpdateDeclaration(
5256
);
5357
}
5458

55-
/** Rewriter that replaces namespace imports to `@angular/core` with a specifier identifier. */
5659
class HmrModuleImportRewriter {
57-
constructor(private readonly coreName: string) {}
60+
constructor(private readonly lookup: Map<string, string>) {}
5861

5962
rewriteNamespaceImportIdentifier(specifier: string, moduleName: string): string {
60-
return moduleName === '@angular/core' ? this.coreName : specifier;
63+
return this.lookup.has(moduleName) ? this.lookup.get(moduleName)! : specifier;
6164
}
6265

6366
rewriteSymbol(symbol: string): string {

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

Lines changed: 82 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,13 @@ runInEachFileSystem(() => {
102102
const jsContents = env.getContents('test.js');
103103
const hmrContents = env.driveHmr('test.ts', 'Cmp');
104104

105+
expect(jsContents).toContain(`import * as i0 from "@angular/core";`);
105106
expect(jsContents).toContain('function Cmp_HmrLoad(t) {');
106107
expect(jsContents).toContain(
107108
'import(/* @vite-ignore */\n"/@ng/component?c=test.ts%40Cmp&t=" + encodeURIComponent(t))',
108109
);
109110
expect(jsContents).toContain(
110-
').then(m => m.default && i0.ɵɵreplaceMetadata(Cmp, m.default, i0, ' +
111+
').then(m => m.default && i0.ɵɵreplaceMetadata(Cmp, m.default, [i0], ' +
111112
'[Dep, transformValue, TOKEN, Component, Inject, ViewChild, Input]));',
112113
);
113114
expect(jsContents).toContain('Cmp_HmrLoad(Date.now());');
@@ -117,12 +118,82 @@ runInEachFileSystem(() => {
117118
);
118119

119120
expect(hmrContents).toContain(
120-
'export default function Cmp_UpdateMetadata(Cmp, __ngCore__, Dep, transformValue, TOKEN, Component, Inject, ViewChild, Input) {',
121+
'export default function Cmp_UpdateMetadata(Cmp, ɵɵnamespaces, Dep, transformValue, TOKEN, Component, Inject, ViewChild, Input) {',
121122
);
123+
expect(hmrContents).toContain(`const ɵhmr0 = ɵɵnamespaces[0];`);
122124
expect(hmrContents).toContain('Cmp.ɵfac = function Cmp_Factory');
123-
expect(hmrContents).toContain('Cmp.ɵcmp = /*@__PURE__*/ __ngCore__.ɵɵdefineComponent');
124-
expect(hmrContents).toContain('__ngCore__.ɵsetClassMetadata(Cmp,');
125-
expect(hmrContents).toContain('__ngCore__.ɵsetClassDebugInfo(Cmp,');
125+
expect(hmrContents).toContain('Cmp.ɵcmp = /*@__PURE__*/ ɵhmr0.ɵɵdefineComponent');
126+
expect(hmrContents).toContain('ɵhmr0.ɵsetClassMetadata(Cmp,');
127+
expect(hmrContents).toContain('ɵhmr0.ɵsetClassDebugInfo(Cmp,');
128+
});
129+
130+
it('should generate an HMR initializer and update function for a class that depends on multiple namespaces', () => {
131+
enableHmr();
132+
env.write(
133+
'dep.ts',
134+
`
135+
import {Directive, NgModule} from '@angular/core';
136+
137+
@Directive({
138+
selector: '[dep]',
139+
standalone: true,
140+
})
141+
export class Dep {}
142+
143+
@NgModule({
144+
imports: [Dep],
145+
exports: [Dep]
146+
})
147+
export class DepModule {}
148+
`,
149+
);
150+
151+
env.write(
152+
'test.ts',
153+
`
154+
import {Component, ViewChild, Input, Inject} from '@angular/core';
155+
import {DepModule} from './dep';
156+
157+
@Component({
158+
selector: 'cmp',
159+
standalone: true,
160+
template: '<div dep><div>',
161+
imports: [DepModule],
162+
})
163+
export class Cmp {}
164+
`,
165+
);
166+
167+
env.driveMain();
168+
169+
const jsContents = env.getContents('test.js');
170+
const hmrContents = env.driveHmr('test.ts', 'Cmp');
171+
expect(jsContents).toContain(`import * as i0 from "@angular/core";`);
172+
expect(jsContents).toContain(`import * as i1 from "./dep";`);
173+
expect(jsContents).toContain('function Cmp_HmrLoad(t) {');
174+
expect(jsContents).toContain(
175+
'import(/* @vite-ignore */\n"/@ng/component?c=test.ts%40Cmp&t=" + encodeURIComponent(t))',
176+
);
177+
expect(jsContents).toContain(
178+
').then(m => m.default && i0.ɵɵreplaceMetadata(Cmp, m.default, [i0, i1], ' +
179+
'[DepModule, Component]));',
180+
);
181+
expect(jsContents).toContain('Cmp_HmrLoad(Date.now());');
182+
expect(jsContents).toContain(
183+
'import.meta.hot && import.meta.hot.on("angular:component-update", ' +
184+
'd => d.id === "test.ts%40Cmp" && Cmp_HmrLoad(d.timestamp)',
185+
);
186+
187+
expect(hmrContents).toContain(
188+
'export default function Cmp_UpdateMetadata(Cmp, ɵɵnamespaces, DepModule, Component) {',
189+
);
190+
expect(hmrContents).toContain(`const ɵhmr0 = ɵɵnamespaces[0];`);
191+
expect(hmrContents).toContain(`const ɵhmr1 = ɵɵnamespaces[1];`);
192+
expect(hmrContents).toContain('Cmp.ɵfac = function Cmp_Factory');
193+
expect(hmrContents).toContain('Cmp.ɵcmp = /*@__PURE__*/ ɵhmr0.ɵɵdefineComponent');
194+
expect(hmrContents).toContain('ɵhmr0.ɵsetClassMetadata(Cmp,');
195+
expect(hmrContents).toContain('ɵhmr0.ɵsetClassDebugInfo(Cmp,');
196+
expect(hmrContents).toContain('dependencies: [DepModule, ɵhmr1.Dep]');
126197
});
127198

128199
it('should generate an HMR update function for a component that has embedded views', () => {
@@ -144,10 +215,10 @@ runInEachFileSystem(() => {
144215
const hmrContents = env.driveHmr('test.ts', 'Cmp');
145216

146217
expect(hmrContents).toContain(
147-
'export default function Cmp_UpdateMetadata(Cmp, __ngCore__, Component) {',
218+
'export default function Cmp_UpdateMetadata(Cmp, ɵɵnamespaces, Component) {',
148219
);
149220
expect(hmrContents).toContain('function Cmp_Conditional_0_Template(rf, ctx) {');
150-
expect(hmrContents).toContain('__ngCore__.ɵɵtemplate(0, Cmp_Conditional_0_Template, 1, 0);');
221+
expect(hmrContents).toContain('ɵhmr0.ɵɵtemplate(0, Cmp_Conditional_0_Template, 1, 0);');
151222
});
152223

153224
it('should generate an HMR update function for a component whose definition produces variables', () => {
@@ -169,12 +240,12 @@ runInEachFileSystem(() => {
169240
const hmrContents = env.driveHmr('test.ts', 'Cmp');
170241

171242
expect(hmrContents).toContain(
172-
'export default function Cmp_UpdateMetadata(Cmp, __ngCore__, Component) {',
243+
'export default function Cmp_UpdateMetadata(Cmp, ɵɵnamespaces, Component) {',
173244
);
174245
expect(hmrContents).toContain('const _c0 = [[["header"]], "*"];');
175246
expect(hmrContents).toContain('const _c1 = ["header", "*"];');
176247
expect(hmrContents).toContain('ngContentSelectors: _c1');
177-
expect(hmrContents).toContain('__ngCore__.ɵɵprojectionDef(_c0);');
248+
expect(hmrContents).toContain('ɵhmr0.ɵɵprojectionDef(_c0);');
178249
});
179250

180251
it('should not defer dependencies when HMR is enabled', () => {
@@ -219,11 +290,11 @@ runInEachFileSystem(() => {
219290
expect(jsContents).toContain('i0.ɵɵdefer(1, 0, Cmp_Defer_1_DepsFn);');
220291

221292
expect(hmrContents).toContain(
222-
'export default function Cmp_UpdateMetadata(Cmp, __ngCore__, Component, Dep) {',
293+
'export default function Cmp_UpdateMetadata(Cmp, ɵɵnamespaces, Component, Dep) {',
223294
);
224295
expect(hmrContents).toContain('const Cmp_Defer_1_DepsFn = () => [Dep];');
225296
expect(hmrContents).toContain('function Cmp_Defer_0_Template(rf, ctx) {');
226-
expect(hmrContents).toContain('__ngCore__.ɵɵdefer(1, 0, Cmp_Defer_1_DepsFn);');
297+
expect(hmrContents).toContain('ɵhmr0.ɵɵdefer(1, 0, Cmp_Defer_1_DepsFn);');
227298
expect(hmrContents).not.toContain('import(');
228299
});
229300

packages/compiler/src/compiler.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ export {
177177
compileHmrInitializer,
178178
compileHmrUpdateCallback,
179179
R3HmrMetadata,
180+
R3HmrNamespaceDependency,
180181
} from './render3/r3_hmr_compiler';
181182
export {
182183
compileFactoryFunction,

packages/compiler/src/render3/r3_hmr_compiler.ts

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,31 @@ export interface R3HmrMetadata {
2121
/** File path of the component class. */
2222
filePath: string;
2323

24-
/** Name under which `@angular/core` should be referred to in the compiled HMR code. */
25-
coreName: string;
24+
/**
25+
* When the compiler generates new imports, they get produced as namespace imports
26+
* (e.g. import * as i0 from '@angular/core'). These namespaces have to be captured and passed
27+
* along to the update callback.
28+
*/
29+
namespaceDependencies: R3HmrNamespaceDependency[];
2630

2731
/**
2832
* HMR update functions cannot contain imports so any locals the generated code depends on
2933
* (e.g. references to imports within the same file or imported symbols) have to be passed in
3034
* as function parameters. This array contains the names of those local symbols.
3135
*/
32-
locals: string[];
36+
localDependencies: string[];
37+
}
38+
39+
/** HMR dependency on a namespace import. */
40+
export interface R3HmrNamespaceDependency {
41+
/** Module name of the import. */
42+
moduleName: string;
43+
44+
/**
45+
* Name under which to refer to the namespace inside
46+
* HMR-related code. Must be a valid JS identifier.
47+
*/
48+
assignedName: string;
3349
}
3450

3551
/**
@@ -43,15 +59,18 @@ export function compileHmrInitializer(meta: R3HmrMetadata): o.Expression {
4359
const dataName = 'd';
4460
const timestampName = 't';
4561
const importCallbackName = `${meta.className}_HmrLoad`;
46-
const locals = meta.locals.map((localName) => o.variable(localName));
62+
const locals = meta.localDependencies.map((localName) => o.variable(localName));
63+
const namespaces = meta.namespaceDependencies.map((dep) => {
64+
return new o.ExternalExpr({moduleName: dep.moduleName, name: null});
65+
});
4766

4867
// m.default
4968
const defaultRead = o.variable(moduleName).prop('default');
5069

51-
// ɵɵreplaceMetadata(Comp, m.default, [...]);
70+
// ɵɵreplaceMetadata(Comp, m.default, [...namespaces], [...locals]);
5271
const replaceCall = o
5372
.importExpr(R3.replaceMetadata)
54-
.callFn([meta.type, defaultRead, new o.ExternalExpr(R3.core), o.literalArr(locals)]);
73+
.callFn([meta.type, defaultRead, o.literalArr(namespaces), o.literalArr(locals)]);
5574

5675
// (m) => m.default && ɵɵreplaceMetadata(...)
5776
const replaceCallback = o.arrowFn([new o.FnParam(moduleName)], defaultRead.and(replaceCall));
@@ -133,11 +152,25 @@ export function compileHmrUpdateCallback(
133152
constantStatements: o.Statement[],
134153
meta: R3HmrMetadata,
135154
): o.DeclareFunctionStmt {
136-
// The class name should always be first and core should be second.
137-
const params = [meta.className, meta.coreName, ...meta.locals].map(
155+
const namespaces = 'ɵɵnamespaces';
156+
const params = [meta.className, namespaces, ...meta.localDependencies].map(
138157
(name) => new o.FnParam(name, o.DYNAMIC_TYPE),
139158
);
140-
const body: o.Statement[] = [...constantStatements];
159+
const body: o.Statement[] = [];
160+
161+
// Declare variables that read out the individual namespaces.
162+
for (let i = 0; i < meta.namespaceDependencies.length; i++) {
163+
body.push(
164+
new o.DeclareVarStmt(
165+
meta.namespaceDependencies[i].assignedName,
166+
o.variable(namespaces).key(o.literal(i)),
167+
o.DYNAMIC_TYPE,
168+
o.StmtModifier.Final,
169+
),
170+
);
171+
}
172+
173+
body.push(...constantStatements);
141174

142175
for (const field of definitions) {
143176
if (field.initializer !== null) {

packages/core/src/render3/hmr.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,14 @@ import {RendererFactory} from './interfaces/renderer';
4646
* Replaces the metadata of a component type and re-renders all live instances of the component.
4747
* @param type Class whose metadata will be replaced.
4848
* @param applyMetadata Callback that will apply a new set of metadata on the `type` when invoked.
49-
* @param environment Core runtime environment to use when applying the HMR update.
49+
* @param environment Syntehtic namespace imports that need to be passed along to the callback.
5050
* @param locals Local symbols from the source location that have to be exposed to the callback.
5151
* @codeGenApi
5252
*/
5353
export function ɵɵreplaceMetadata(
5454
type: Type<unknown>,
55-
applyMetadata: (...args: [Type<unknown>, Record<string, unknown>, ...unknown[]]) => void,
56-
environment: Record<string, unknown>,
55+
applyMetadata: (...args: [Type<unknown>, unknown[], ...unknown[]]) => void,
56+
namespaces: unknown[],
5757
locals: unknown[],
5858
) {
5959
ngDevMode && assertComponentDef(type);
@@ -64,7 +64,7 @@ export function ɵɵreplaceMetadata(
6464
// can be functions for embedded views, the variables for the constant pool and `setClassMetadata`
6565
// calls. The callback allows us to keep them isolate from the rest of the app and to invoke
6666
// them at the right time.
67-
applyMetadata.apply(null, [type, environment, ...locals]);
67+
applyMetadata.apply(null, [type, namespaces, ...locals]);
6868

6969
// If a `tView` hasn't been created yet, it means that this component hasn't been instantianted
7070
// before. In this case there's nothing left for us to do aside from patching it in.

packages/core/test/acceptance/hmr_spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1967,7 +1967,7 @@ describe('hot module replacement', () => {
19671967
(type as any)[ɵNG_COMP_DEF] = null;
19681968
compileComponent(type, metadata);
19691969
},
1970-
angularCoreEnv,
1970+
[angularCoreEnv],
19711971
[],
19721972
);
19731973
}

0 commit comments

Comments
 (0)