FredKSchott / esm-hmr

a Hot Module Replacement (HMR) API for your ESM-based dev server.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Support: Preact Prefresh

FredKSchott opened this issue · comments

/cc @JoviDeCroock @developit

I've been working on a Prefresh integration with @JoviDeCroock for Snowpack for a while now, so I believe the current interface addresses all needs. But, still wanted to ping this group for feedback and highlight anything that actually is missing.

First and foremost it's awesome working with you @FredKSchott Thank you so much for all the effort you've put into this, you're a hero pushing this ideology forward!

Potential missing scenario

I can only imagine one scenario that we could potentially be missing and not entirely sure if we can support it but here we go.

So in traditional bundlers when you enable HotModuleReplacement it by default accepts and swaps out the hot module and notifies the dependents of this module (an equivalent of this could be module.hot.accept(true) in snowpack).

I noticed this isn't the case for prefresh (current snowpack), let's look at an example.

Our custom hook:

const useCounter = (initialValue) => {
  const [count, setCount] = useState(initialValue);
  return [count, () => setCount(c => c + 1)]
}

Our Component:

const Counter = () => {
  const [count, increment] = useCounter(0);
  return (<p>....</p>);
}

When we update code in Counter we have to swap this out with prefresh.replaceComponent to keep the dom up-to-date, .... However when we swap out a custom-hook we don't need any special logic, we should only rely on this automatic swapping. This means that the reference to useCounter is the new function and the nit goes into every Component that relies on this dependency and swaps it out there.

When I just do accept(true) the function changes but the descendants don't seem to be checked.

Just as an example I'll add the webpack one, as you can see here we only bind when it's an actual component. This makes it so that a custom hook will just get replaced since we can't really derive where to replace it in module.hot and then the components are left to decide whether or not to reset the hook state for that component, based on the signature of the new nested set of custom-hooks and usage of those.
In the above case a save in useCounter will hot-swap the module (default behavior) and then call the accept added in Counter to notify about the newly added module, this way the component can check how to behave.

Invalidate API

Webpack supports invalidating a module, when something goes wrong we call invalidate() and the module starts with a clean slate. I think this is an ideal candidate to be added to snowpack as well since it should be possible but I think this could also be subject to the prior issue since invalidate should then reset the hook states of for instance a custom hook in the dependent modules.

Invalidate API

import.meta.hot.invalidate() has been added to the spec and implemented in snowpack rc.1+!

Potential missing scenario

Gotcha, thanks for writing that out. Bubbling HMR updates up the dependency tree is still TODO in both this spec and the reference implementation.

Question for you: Does your usecase need a way to whitelist different dependencies in the accept call to only bubble up those updates? aka:

// Only bubble up updates from './foo.js', ignore all other HMR updates
import.meta.hot.accept(['./foo.js'], () => ...);

I'm hoping that this was a bundling limitation, and not actually required for bundle-free dev. Because we're not bundling the performance impact of always bubbling up may not be terrible, and we can skip the idea of whitelisting entirely. That's the hope, at least...

We don't need to scope bubbling in our case, also the bubbling will be stopped at the first dependent. So because Counter does an accept with a callback it doesn't (or shouldn't atleast) bubble on.

Okay, so just to make sure I understand your example above:

Setup

  • Component.js imports useCounter.js
  • Component.js contains a call to hot.accept(true)
  • useCounter.js contains no call to hot.accept()
  • You make a change to useCounter.js, and hit save

Current Behavior

Snowpack triggers a full page reload, since useCounter.js has no hot.accept() call and we haven't yet implemented event bubbling

Desired Behavior

The event should bubble up to Component.js, where it will be swallowed via hot.accept(true) so that no more bubbling needed. Prefresh handles the rest for us.

^ Is that all correct? If I got anything wrong, would love if you could re-write it to match what you're trying to say using this format.

That's correct yes, an accept indicates the end of a bubble-chain. This way we could have utils, ... within various things and eventually end up in a DOM-representation which we could then refresh.

However I don't know for sure if we want to by default enable HMR so that every plugin all of a sudden has to use location.reload() and stuff. Maybe we should add hot as a config option? Not entirely sure there, you probably know better what users expect there.

Not sure that I follow, but sounds like that flow outlined above is what you need along with the hot.invalidate() support we shipped earlier. I'll get to work on that!

My thinking was mainly, do we want to force everyone on hot module reloading? Not everyone will implement the .hot() api so saying that a lack just bubbles might be an issue for some users.

For instance in webpack you need to add HotModuleReplacementPlugin else you'll go to the classic location.reload(), does that make sense? Do we want something similar where we need to do hot: true on the snowpackConfig?

Oh gotcha! yes, right now only modules that actually reference import.meta.hot are considered for HMR. If you (or your plugins) never do that, you don't opt-in to HMR and you get full page reloads.

You're right that a hot: true/false config option would be good to pass to plugins, so that you can make sure that they don't add HMR code to your responses if you explicitly don't want it.

Actually, we already pass the isDev option to build/transform. I guess this could also just be a more explicit isHmr :)

Implemented! Thanks @JoviDeCroock !