Skip to content

Commit

Permalink
refactor(compiler): trigger hmr load on initialization (angular#58465)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
crisbeto authored and thePunderWoman committed Nov 1, 2024
1 parent fbd7b7c commit 7d0ba0c
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 36 deletions.
18 changes: 10 additions & 8 deletions packages/compiler-cli/test/ngtsc/hmr_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,16 +102,18 @@ runInEachFileSystem(() => {
const jsContents = env.getContents('test.js');
const hmrContents = env.driveHmr('test.ts', 'Cmp');

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

expect(hmrContents).toContain(
Expand Down
89 changes: 61 additions & 28 deletions packages/compiler/src/render3/r3_hmr_compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,51 +41,84 @@ export function compileHmrInitializer(meta: R3HmrMetadata): o.Expression {
const urlPartial = `/@ng/component?c=${id}&t=`;
const moduleName = 'm';
const dataName = 'd';
const timestampName = 't';
const importCallbackName = `${meta.className}_HmrLoad`;
const locals = meta.locals.map((localName) => o.variable(localName));

// ɵɵreplaceMetadata(Comp, m.default, core, [...]);
const replaceMetadata = o
// m.default
const defaultRead = o.variable(moduleName).prop('default');

// ɵɵreplaceMetadata(Comp, m.default, [...]);
const replaceCall = o
.importExpr(R3.replaceMetadata)
.callFn([
meta.type,
o.variable(moduleName).prop('default'),
new o.ExternalExpr(R3.core),
o.literalArr(locals),
]);
.callFn([meta.type, defaultRead, new o.ExternalExpr(R3.core), o.literalArr(locals)]);

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

// '<urlPartial>' + encodeURIComponent(d.timestamp)
// '<urlPartial>' + encodeURIComponent(t)
const urlValue = o
.literal(urlPartial)
.plus(o.variable('encodeURIComponent').callFn([o.variable(dataName).prop('timestamp')]));

// import(/* @vite-ignore */ url).then(() => replaceMetadata(...));
// The vite-ignore special comment is required to avoid Vite from generating a superfluous
// warning for each usage within the development code. If Vite provides a method to
// programmatically avoid this warning in the future, this added comment can be removed here.
const dynamicImport = new o.DynamicImportExpr(urlValue, null, '@vite-ignore')
.prop('then')
.callFn([replaceCallback]);

// (d) => { if (d.id === <id>) { replaceMetadata(...) } }
const listenerCallback = o.arrowFn(
.plus(o.variable('encodeURIComponent').callFn([o.variable(timestampName)]));

// function Cmp_HmrLoad(t) {
// import(/* @vite-ignore */ url).then((m) => m.default && replaceMetadata(...));
// }
const importCallback = new o.DeclareFunctionStmt(
importCallbackName,
[new o.FnParam(timestampName)],
[
// The vite-ignore special comment is required to prevent Vite from generating a superfluous
// warning for each usage within the development code. If Vite provides a method to
// programmatically avoid this warning in the future, this added comment can be removed here.
new o.DynamicImportExpr(urlValue, null, '@vite-ignore')
.prop('then')
.callFn([replaceCallback])
.toStmt(),
],
null,
o.StmtModifier.Final,
);

// (d) => d.id === <id> && Cmp_HmrLoad(d.timestamp)
const updateCallback = o.arrowFn(
[new o.FnParam(dataName)],
[o.ifStmt(o.variable(dataName).prop('id').equals(o.literal(id)), [dynamicImport.toStmt()])],
o
.variable(dataName)
.prop('id')
.identical(o.literal(id))
.and(o.variable(importCallbackName).callFn([o.variable(dataName).prop('timestamp')])),
);

// Cmp_HmrLoad(Date.now());
// Initial call to kick off the loading in order to avoid edge cases with components
// coming from lazy chunks that change before the chunk has loaded.
const initialCall = o
.variable(importCallbackName)
.callFn([o.variable('Date').prop('now').callFn([])]);

// import.meta.hot
const hotRead = o.variable('import').prop('meta').prop('hot');

// import.meta.hot.on('angular:component-update', () => ...);
const hotListener = hotRead
.clone()
.prop('on')
.callFn([o.literal('angular:component-update'), listenerCallback]);

// import.meta.hot && import.meta.hot.on(...)
return o.arrowFn([], [devOnlyGuardedExpression(hotRead.and(hotListener)).toStmt()]).callFn([]);
.callFn([o.literal('angular:component-update'), updateCallback]);

return o
.arrowFn(
[],
[
// function Cmp_HmrLoad() {...}.
importCallback,
// ngDevMode && Cmp_HmrLoad(Date.now());
devOnlyGuardedExpression(initialCall).toStmt(),
// ngDevMode && import.meta.hot && import.meta.hot.on(...)
devOnlyGuardedExpression(hotRead.and(hotListener)).toStmt(),
],
)
.callFn([]);
}

/**
Expand Down

0 comments on commit 7d0ba0c

Please sign in to comment.