Closed sessionboy closed 2 years ago
Sorry, esbuild isn't designed for HMR. It's possible to use esbuild's transform API as part of another HMR tool (e.g. what Vite does) but this isn't something that esbuild's build API supports. I'm not sure if it can be done with an esbuild plugin because esbuild's plugin API deliberately doesn't let you substitute your own linker. I haven't ever implemented HMR before but I believe HMR requires a different type of linker. For example the react-refresh documentation says that integrating it requires your bundler to support HMR.
esbuild isn't designed for HMR.
I know this, I think hmr should be an additional feature of esbuild. I don't want to use webpack or other bundler, I think the future will be no-bundler. Many people expect esbuild to implement hmr. If esbuild implements hmr, then the no-bundler goal will be achieved immediately, this will make web go into the future. Snowpack proposed the esm-hmr spec, maybe this can help you implement hmr.
What's bad is that react-refresh currently only supports babel's transform. I haven't found any documentation or case of using esbuild's transform to implement react-refresh. https://github.com/facebook/react/blob/master/packages/react-refresh/src/ReactFreshBabelPlugin.js
Look forward to esbuild's realization of HMR and lead us into the future.
Cheers 🍻.
+1 on this. HMR support or webpack-dev-server like support for esbuild would be a god send. I’m willing to dig into the source code and work on some PRs if someone’s willing to guide and review PRs.
So I took a close look at how Dan outlined integrating HMR: https://github.com/facebook/react/issues/16604#issuecomment-528663101. At first it didn’t make any sense but then I started to get it.
I have written some custom hot module reloaders with webpack, and there interface is fairly clean https://webpack.js.org/concepts/hot-module-replacement/
The general idea is that you accept via module.hot.accept(moduleId, handlerFn)
and inside handlerFn, do a require call to get latest instance of module and do some sort of updating the app and change the module.exports to the new module. If it is not accepted, it bubbles up the require tree until something accepts it.
The general idea is that you accept via
module.hot.accept(moduleId, handlerFn)
and inside handlerFn, do a require call to get latest instance of module and do some sort of updating the app and change the module.exports to the new module. If it is not accepted, it bubbles up the require tree until something accepts it.
Note that module.hot
is undefined in esbuild, since esbuild doesn't implement HMR, so module.hot.accept()
will crash at run-time.
I don't want to use webpack or other bundler, I think the future will be no-bundler.
You may not want to use esbuild then since esbuild is a bundler. You may want to consider using Snowpack or Vite instead. They are both no-bundler options that implement HMR and use esbuild internally for speed.
I’ll probably have to investigate esbuild’s incremental build abilities. The whole idea of HMR and dev-server is that once you make a change in the editor, you immediately see it’s effect in the browser or similar runtime without having to rebuild the entire codebase or refresh the browser.
Incremental devloop being in milliseconds is a big deal. I wonder how snowpack gets around this. May be esbuild exposes some version of an incremental module update api?
So I figured out how to implement esbuild incremental recompilation + server-sent events to trigger reloads which feels like HMR but works today. I used this playground to validate my ideas: https://github.com/zaydek/esbuild-dev-server.
I use a poll-based file watcher to look for changes to source files. The public dir is served and uses the /sse route to send sever-sent events. Those events allow me to trigger refreshes on the client browser tab. Build errors are passed along to the client browser tab and also logs them on the server.
It’s not HMR but I basically can’t tell the difference. I also prefer this implementation since it doesn’t care about React anymore (React Refresh is tightly coupled to React) and there’s no quasi states to worry about anymore because state is never persisted between reloads (unless you write to localStorage).
@zaydek i did not dig into this yet, but how is esbuild-dev-server this different than https://github.com/osdevisnot/sorvor?
@osdevisnot It’s functionally similar. I actually reviewed your code which helped me a lot.
I’ll put this in a <details>
so I don’t distract from the original intent of this thread too much.
There is https://github.com/progrium/hotweb , which is using esbuild and gorilla ws.
You dont actually need to implement module.hot api to get stateful reloading. Most hmr servers send a “reload” event via ws.
My experience with go is borderline none, but was looking through the esbuild docs/code and i think there is the foundation to do everything needed using the esbuild prelude and output meta.json . The wrappers esbuilds appends at the top of bundles, like __toModule(_import(“react”)) or whatever it is could be augmented a little bit to listen for reload events. The reload event could have a data object that has the changed module identifier(s) and the new code. The logic for re-executing the bundle could be wrapped around the original bundle using the prelude api. Is there a hook to intercept the final write ? Changing the bundles identifiers to use src paths would be super helpful
there is a very simple implementation here using browserify (esbuild is setup also, not originally for esbuild hmr, but its a decent starting point to do some hacking ) https://github.com/jeremy-coleman/esbuild-vs-omnify-r3f the hmr logic is in tools/omnify/plugins . There is also a simplified version here https://github.com/jeremy-coleman/prefresh-browserify-help-wanted/blob/main/tools/livereload/plugin-no-sourcemap.js . Its kind of wtf hard to follow bc its mapping module deps jsonish output to something different, but all that hackery could be probably done much easier using the esbuild output format. It changes the output to strings , which are instantiated in the browser using new function and eval’d to update the runtime. Definitely hacky but its very fast and will maintain state on reloads in everything but internal component state without needing react hot loader or anything(although adding it will give u hot internal component state updates too, which is cool). However, implementing a jsx transform for hot updates is probably out of the question. Sucrase is fast as f though, so maybe just use that if you need the hottest of hot. Anyway, to give an idea of capabilities.. given this react/preact component:
const Test = () =>
useObserver(() => {
return (
<div>
<header>App Headers</header>
<h1 style={{ textAlign: "center" }}>Count: {model.counter}</h1>
<input
type="range"
min={0}
max={1000}
value={model.counter}
onInput={(evt) => (model.counter = Number((evt as any).target.value))}
style={{ width: "100%" }}
/>
</div>
);
});
Just as an example, you can update the text of App Headers and the range input slider will maintain state, because the value is held in a different module. This is without any hmr jsx transforms or anything, just the hot reload socket api. I think this is good enough for most apps. I think this level of reloading could be done via esbuild with very little code, i just have no idea how to do it.
@zaydek Hi. I'm in a similar situation as you were, when your were writing this.
It’s not a transformer like Babel (which is what React Refresh relies on).
From what i understand, esbuild can indeed be used as a transformer like babel. Could you elaborate, on why it is not suitable for this situation?
@zaydek Hi. I'm in a similar situation as you were, when your were writing this.
It’s not a transformer like Babel (which is what React Refresh relies on).
From what i understand, esbuild can indeed be used as a transformer like babel. Could you elaborate, on why it is not suitable for this situation?
I can try to help based on my understanding of esbuild.
It's important to think of esbuild as a build-time tool and not a runtime tool. HMR is basically a clever way to layer a system on top of the runtime. So that means in most cases, HMR needs to know something about your runtime, say for example React. React Refresh or whatever Facebook uses is a React tool. esbuild is a JavaScript tool. In order for esbuild to support HMR, esbuild would need to interact with React in a way that it's not designed to. esbuild does understand JSX but that's mostly a build-time transformation.
So the point is, I think, more philosophical. esbuild is trying to be a compile-time tool for JavaScript, but it's not runtime or framework-aware in the way it would need to be to truly support HMR. HMR if I'm not mistaken is a deeply runtime / framework-specific tool, and esbuild is anything but that.
I hope that clears up any of the misconceptions. I'm not saying HMR isn't possible with esbuild via plugins or something like that, I'm just trying to help explain why esbuild doesn't particularly have an opinion on HMR to begin with.
Closing this issue because this is out of scope. I recommend using another tool instead.
I use esbuild to build my react application, no webpack, no rollup, only esbuild.It works well, everything looks great. But I don't know how to implement hmr in esbuild.
I checked the hmr implementation of vite. But the difference is that vite only injects the HMR code when the module is requested, not during the build.So this is easy to implement.
I used code splitting.
I need to inject hmr code during the first build process. Re-execute the build when the code is updated, and update the code dynamically.
This is not a simple matter.
I guess esbuild plugin should be able to implement it, but I need some guidance. How can i implement it ?