Skip to content

Conversation

@sokra
Copy link
Member

@sokra sokra commented Apr 15, 2020

refactor HMR javascript part to reduce duplication

What kind of change does this PR introduce?
feature

Did you add tests for your changes?
yes

Does this PR introduce a breaking change?
yes

What needs to be documented once your changes are merged?

module.hot.invalidate()

Calling this method will invalidate the current module which disposes and recreates this module when the HMR update is applied. This bubbles like a normal update of this module. invalidate can't be self-accepted by this module.

When called during the idle state, a new HMR update will be created containing this module. HMR will enter the ready state.

When called during the ready or prepare state, this module will be added to the current HMR update.

When called during the check state, the module will be added to the update when an update is available. If no update is available it will create a new update. HMR will enter the ready state.

When called during the dispose or apply state, HMR will automatically do an additional dispose and apply cycle after the current cycle has finished.

Use cases

Conditional accepting

A module can accept a dependency, but can call invalidate when the change of the dependency is not handle-able:

import { x, y } from "./dep";

const oldY = y;

processX(x);
export default processY(y);

module.hot.accept("./dep", () => {
  if(y !== oldY) {
    // This can't be handled, bubble to parent
    module.hot.invalidate();
    return;
  }
  // This can be handled
  processX(x);
});

Conditional self accept

A module can self-accept itself, but can invalidate itself when the change is not handle-able:

const VALUE = "constant"

export default VALUE;

if(module.hot.data && module.hot.data.value && module.hot.data.value !== VALUE) {
  module.hot.invalidate();
} else {

  doSomeSideeffect();

  module.hot.accept();
  module.hot.dispose(data => {
    revertSomeSideeffect();
    data.value = VALUE;
  });
}

Triggering custom HMR updates

const moduleId = chooseAModule();
const code = __webpack_modules__[moduleId].toString();
__webpack_modules__[moduleId] = eval(`(${makeChanges(code)})`);
if(require.cache[moduleId]) {
  require.cache[moduleId].hot.invalidate();
  module.hot.apply();
}

Notes

When invalidate is called, the dispose handler will eventually called and fill module.hot.data. If no dispose handler is registered an empty object is still provided for module.hot.data.

Make sure to not get catched in a invalidate loop, by calling invalidate again and again. This will result in stack overflow and HMR entering the fail state.

@webpack-bot
Copy link
Contributor

webpack-bot commented Apr 15, 2020

For maintainers only:

  • This needs to be documented (issue in webpack/webpack.js.org will be filed when merged)
  • This needs to be backported to webpack 4 (issue will be created when merged)

@sokra
Copy link
Member Author

sokra commented Apr 15, 2020

@pmmmwh @gaearon Do this help for react-fast-refresh in webpack?

@evilebottnawi for style-loader?

@webpack-bot
Copy link
Contributor

Thank you for your pull request! The most important CI builds succeeded, we’ll review the pull request soon.

@gaearon
Copy link
Contributor

gaearon commented Apr 15, 2020

A module can self-accept itself, but can invalidate itself when the change is not handle-able

Is there any way to reliably capture the list of exports without generating code? Whether CJS or ESM.

@sokra
Copy link
Member Author

sokra commented Apr 16, 2020

A module can self-accept itself, but can invalidate itself when the change is not handle-able

Is there any way to reliably capture the list of exports without generating code? Whether CJS or ESM.

import * as SELF from "./self.js";
Object.keys(SELF)

In loose ESM and CJS you could also use Object.keys(require.cache[module.id].exports).

Or Object.keys(__webpack_require__(module.id)).

In webpack 5 there is __webpack_exports_info__.usedExports, which is a list of all used exports. That probably doesn't help you as you can't access the value.

We could consider adding a __webpack_exports_info__.providedExports, which would list all exports.

Note that these variants above have different properties regarding marking all exports as used and enumerating mangled export names.

variant all exports marked as used export names list unused exports
import * as SELF yes real yes
require.cache no mangled no
__webpack_require__(module.id) no mangled no
__webpack_exports_info__.usedExports no real no
__webpack_exports_info__.providedExports no real yes

Note that export name mangling only happen in production mode.

@webpack-bot
Copy link
Contributor

I've created an issue to document this in webpack/webpack.js.org.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants