Hot module replacement API
Opened this issue · 6 comments
Currently it seems a fair amount of projects are working towards implementing HMR support.
A couple of existing implementations related to webcomponents/ESM:
- lit/lit-element#802 (@rictic)
- modernweb-dev/web#685
- salesforce/lwc#2071 (@diervo, @caridy)
- vite
- snowpack (@FredKSchott)
These are only a couple, there will be more. However, there is no consistent API right now across these.
There are three parts to HMR as far as I can see:
- Server-side (basically a file watcher which notifies the client when a module changes)
- Client-side (an API to communicate with these server updates)
- Framework/library specific (an integration of the client-side API into a specific ecosystem like lit-element)
Server-side
The server-side implementation should be as simple as a web socket service which emits messages of the following types:
update
- a message specifying that a particular module needs reloadingreload
- a message specifying that the page must reload as a whole
Client-side
An API should be made available at import.meta.hot
which can have methods for the following:
- Accept updates (notify the server this module can handle updates, via an
accept
message) - Refuse updates (notify the server this module cannot handle updates)
- Invalidate the current module (if something went wrong, force a full reload)
- Disposer (handle teardown of the module before a new version is loaded)
The client-side implementation should primarily exist to handle the server-side messages, though it should also emit its own message:
accept
- a message specifying that the current module supports HMR
Handling of the server-side messages could look like this:
update
- dynamically import the specified module and execute a user-supplied callback for dealing with the updatereload
- callwindow.location.reload
i suppose
Example implementation
Within the modernweb repo I wrote the following message types:
// emitted by the server
export interface HmrReloadMessage {
type: 'hmr:reload';
}
// emitted by the server
export interface HmrUpdateMessage {
type: 'hmr:update';
url: string;
}
// emitted by the client
export interface HmrAcceptMessage {
type: 'hmr:accept';
id: string;
}
Note that the message types are prefixed here because we already had a web socket open and didn't want to have a second just to specify the protocol. Though it could be argued a protocol is better here than a prefixed set of types.
Meanwhile, i used snowpack as inspiration to write a client API which looks like this:
// at import.meta.hot
{
accept(callback);
accept(deps[], callback);
dispose(callback);
decline();
invalidate();
}
However i'm not such a fan of it even though i did it. As confusion can quickly come about by weak naming.
I would suggest more like:
{
acceptCallback(callback);
acceptCallback(deps[], callback);
disposeCallback(callback);
decline();
invalidate();
}
Framework/library specific
For example, the work being done to lit-element around HMR will produce an overridden customElements.define
which then understands how to update an element when it is re-defined.
Peter's work in the lit branch has this:
static notifyOnHotModuleReload(tag, newClass)
Which i agree with, though maybe named with a Callback
suffix like connectedCallback
and such.
The idea here being every hmr-compatible web component would have this standard static method which the library or user must implement.
Summary
I think the most important thing to get right here is the client API available at import.meta.hot
and the framework/library specific interface.
I think es module HMR is something to "standardize" on outside of this platform, basically what has been started at https://github.com/snowpackjs/esm-hmr.
I think the main point of interest here is to try to standardize on an update pattern for web components, such as a standard callback to call.
You are right, i've updated the post to focus a bit more on the client and the web component stuff.
Another reference from Salesforce from @diervo and @caridy: salesforce/lwc#2071
Thanks @abdonrd for the heads up.
Given that I was implementing HMR recently and looking at other implementations, here are some observations:
In general I think many frameworks need the per module import.meta.hot
described above, primarily because they need to observe and potentially revert or change some side-effects and swap bindings of things that they reference or hold.
However I believe there are also frameworks where they might be simple enough to have a centralize module swapping. For example:
// centralized hmr example
import { updateModule } from 'lwc';
export function onModuleUpdate(oldUri, newUri) {
const oldModule = await import(oldUri);
const newModule = await import(newUri);
updateModule({
oldModule,
newModule,
});
}
On our LWC framework at Salesforce for example, we can swap any component js, css or html from a unique centralize place without issues with side-effects.
I know that this approach might not work for cases due to identity discontinuity or other odd side-effects, but I just wanted to put it here in case its worth exploring different angles and/or APIs.
While mostly its implementation framework-land details, if we could avoid for certain apps de need to prepend/append import.meta stuff will be interesting (it is observable nonetheless). However the proposed APIs seems a simple and complete way to go forward as well.
Swapping an element's HTML/CSS is simple enough, but what about adding/removing/changing class methods and/or changes in other extended elements or applied mixins?
I've started an approach for universal web component HMR at https://open-wc.org/docs/development/hot-module-replacement/
The idea is that the base class can implement a static and/or instance hotReplaceCallback
, callback. This decides how to do the actual replacement.
I'm going through different base libraries to see if this approach works for them. So far lit-element and fast element works, and I got haunted working with some code changes.
The plugin I linked is based on es module HMR, I wasn't able to figure out yet how to use LWC with regular es modules.