webcomponents-cg/community-protocols

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:

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 reloading
  • reload - 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 update
  • reload - call window.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.