Skip to content

Commit 7d0ba0c

Browse files
crisbetothePunderWoman
authored andcommitted
refactor(compiler): trigger hmr load on initialization (angular#58465)
Adjusts the HMR initialization to avoid the edge case where a developer makes change to a non-rendered component that exists in a lazy loaded chunk that has not been loaded yet. The changes include: * Moving the `import` statement out into a separate function. * Adding a null check for `d.default` before calling `replaceMEtadata`. * Triggering the `import` callback eagerly on initialization. Example of the new generated code: ```js (() => { function Cmp_HmrLoad(t) { import( /* @vite-ignore */ "/@ng/component?c=test.ts%40Cmp&t=" + encodeURIComponent(t) ).then((m) => m.default && i0.ɵɵreplaceMetadata(Cmp, m.default, [/* Dependencies go here */])); } (typeof ngDevMode === "undefined" || ngDevMode) && Cmp_HmrLoad(Date.now()); (typeof ngDevMode === "undefined" || ngDevMode) && import.meta.hot && import.meta.hot.on("angular:component-update", (d) => { if (d.id === "test.ts%40Cmp") { Cmp_HmrLoad(d.timestamp); } }); })(); ``` PR Close angular#58465
1 parent fbd7b7c commit 7d0ba0c

File tree

2 files changed

+71
-36
lines changed

2 files changed

+71
-36
lines changed

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

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

105-
// We need a regex match here, because the file path changes based on
106-
// the file system and the timestamp will be different for each test run.
107-
expect(jsContents).toMatch(
108-
/import\.meta\.hot && import\.meta\.hot\.on\("angular:component-update", d => { if \(d\.id == "test\.ts%40Cmp"\) {/,
105+
expect(jsContents).toContain('function Cmp_HmrLoad(t) {');
106+
expect(jsContents).toContain(
107+
'import(/* @vite-ignore */\n"/@ng/component?c=test.ts%40Cmp&t=" + encodeURIComponent(t))',
109108
);
110-
expect(jsContents).toMatch(
111-
/import\(\s*\/\* @vite-ignore \*\/\s+"\/@ng\/component\?c=test\.ts%40Cmp&t=" \+ encodeURIComponent\(d.timestamp\)/,
109+
expect(jsContents).toContain(
110+
').then(m => m.default && i0.ɵɵreplaceMetadata(Cmp, m.default, i0, ' +
111+
'[Dep, transformValue, TOKEN, Component, Inject, ViewChild, Input]));',
112112
);
113-
expect(jsContents).toMatch(
114-
/\).then\(m => i0\.ɵɵreplaceMetadata\(Cmp, m\.default, i0, \[Dep, transformValue, TOKEN, Component, Inject, ViewChild, Input\]\)\);/,
113+
expect(jsContents).toContain('Cmp_HmrLoad(Date.now());');
114+
expect(jsContents).toContain(
115+
'import.meta.hot && import.meta.hot.on("angular:component-update", ' +
116+
'd => d.id === "test.ts%40Cmp" && Cmp_HmrLoad(d.timestamp)',
115117
);
116118

117119
expect(hmrContents).toContain(

packages/compiler/src/render3/r3_hmr_compiler.ts

Lines changed: 61 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -41,51 +41,84 @@ export function compileHmrInitializer(meta: R3HmrMetadata): o.Expression {
4141
const urlPartial = `/@ng/component?c=${id}&t=`;
4242
const moduleName = 'm';
4343
const dataName = 'd';
44+
const timestampName = 't';
45+
const importCallbackName = `${meta.className}_HmrLoad`;
4446
const locals = meta.locals.map((localName) => o.variable(localName));
4547

46-
// ɵɵreplaceMetadata(Comp, m.default, core, [...]);
47-
const replaceMetadata = o
48+
// m.default
49+
const defaultRead = o.variable(moduleName).prop('default');
50+
51+
// ɵɵreplaceMetadata(Comp, m.default, [...]);
52+
const replaceCall = o
4853
.importExpr(R3.replaceMetadata)
49-
.callFn([
50-
meta.type,
51-
o.variable(moduleName).prop('default'),
52-
new o.ExternalExpr(R3.core),
53-
o.literalArr(locals),
54-
]);
54+
.callFn([meta.type, defaultRead, new o.ExternalExpr(R3.core), o.literalArr(locals)]);
5555

56-
// (m) => ɵɵreplaceMetadata(...)
57-
const replaceCallback = o.arrowFn([new o.FnParam(moduleName)], replaceMetadata);
56+
// (m) => m.default && ɵɵreplaceMetadata(...)
57+
const replaceCallback = o.arrowFn([new o.FnParam(moduleName)], defaultRead.and(replaceCall));
5858

59-
// '<urlPartial>' + encodeURIComponent(d.timestamp)
59+
// '<urlPartial>' + encodeURIComponent(t)
6060
const urlValue = o
6161
.literal(urlPartial)
62-
.plus(o.variable('encodeURIComponent').callFn([o.variable(dataName).prop('timestamp')]));
63-
64-
// import(/* @vite-ignore */ url).then(() => replaceMetadata(...));
65-
// The vite-ignore special comment is required to avoid Vite from generating a superfluous
66-
// warning for each usage within the development code. If Vite provides a method to
67-
// programmatically avoid this warning in the future, this added comment can be removed here.
68-
const dynamicImport = new o.DynamicImportExpr(urlValue, null, '@vite-ignore')
69-
.prop('then')
70-
.callFn([replaceCallback]);
71-
72-
// (d) => { if (d.id === <id>) { replaceMetadata(...) } }
73-
const listenerCallback = o.arrowFn(
62+
.plus(o.variable('encodeURIComponent').callFn([o.variable(timestampName)]));
63+
64+
// function Cmp_HmrLoad(t) {
65+
// import(/* @vite-ignore */ url).then((m) => m.default && replaceMetadata(...));
66+
// }
67+
const importCallback = new o.DeclareFunctionStmt(
68+
importCallbackName,
69+
[new o.FnParam(timestampName)],
70+
[
71+
// The vite-ignore special comment is required to prevent Vite from generating a superfluous
72+
// warning for each usage within the development code. If Vite provides a method to
73+
// programmatically avoid this warning in the future, this added comment can be removed here.
74+
new o.DynamicImportExpr(urlValue, null, '@vite-ignore')
75+
.prop('then')
76+
.callFn([replaceCallback])
77+
.toStmt(),
78+
],
79+
null,
80+
o.StmtModifier.Final,
81+
);
82+
83+
// (d) => d.id === <id> && Cmp_HmrLoad(d.timestamp)
84+
const updateCallback = o.arrowFn(
7485
[new o.FnParam(dataName)],
75-
[o.ifStmt(o.variable(dataName).prop('id').equals(o.literal(id)), [dynamicImport.toStmt()])],
86+
o
87+
.variable(dataName)
88+
.prop('id')
89+
.identical(o.literal(id))
90+
.and(o.variable(importCallbackName).callFn([o.variable(dataName).prop('timestamp')])),
7691
);
7792

93+
// Cmp_HmrLoad(Date.now());
94+
// Initial call to kick off the loading in order to avoid edge cases with components
95+
// coming from lazy chunks that change before the chunk has loaded.
96+
const initialCall = o
97+
.variable(importCallbackName)
98+
.callFn([o.variable('Date').prop('now').callFn([])]);
99+
78100
// import.meta.hot
79101
const hotRead = o.variable('import').prop('meta').prop('hot');
80102

81103
// import.meta.hot.on('angular:component-update', () => ...);
82104
const hotListener = hotRead
83105
.clone()
84106
.prop('on')
85-
.callFn([o.literal('angular:component-update'), listenerCallback]);
86-
87-
// import.meta.hot && import.meta.hot.on(...)
88-
return o.arrowFn([], [devOnlyGuardedExpression(hotRead.and(hotListener)).toStmt()]).callFn([]);
107+
.callFn([o.literal('angular:component-update'), updateCallback]);
108+
109+
return o
110+
.arrowFn(
111+
[],
112+
[
113+
// function Cmp_HmrLoad() {...}.
114+
importCallback,
115+
// ngDevMode && Cmp_HmrLoad(Date.now());
116+
devOnlyGuardedExpression(initialCall).toStmt(),
117+
// ngDevMode && import.meta.hot && import.meta.hot.on(...)
118+
devOnlyGuardedExpression(hotRead.and(hotListener)).toStmt(),
119+
],
120+
)
121+
.callFn([]);
89122
}
90123

91124
/**

0 commit comments

Comments
 (0)