sveltejs/svelte-hmr

WebComponent HMR

ConProgramming opened this issue · 10 comments

This might not be the place for this, but it's the only work really out there with Svelte + HMR, so:

Any ideas on how to get the web component output working with HMR? I'm looking at https://open-wc.org/docs/development/hot-module-replacement/ which looks for a hotReplacedCallback on the outputted web component.

Not particularly familiar with HMR, so any pointers here on how I could implement this would be awesome!

rixo commented

Yes, this is the right place.

Unfortunately, I have previously tried to fix support for WC myself, got burnt, gave up...

iirc the first blocker was that Svelte registers successive versions of a web component under the same tag name, which doesn't go well with the browser... I believe I past this one, but then there were more... The code produced by Svelte for WC is very different from normal components.

This is a legitimate request so I'm keeping the issue open, but I have no immediate plan on trying to solve that again, sorry.

A general solution might be tough to implement for WC, but with the @open-wc/dev-server-hmr combined with the @web/dev-server it looks like it should be easier?

I've been playing around with the web dev server for the past bit, so far implemented it to resolve and return compiled files with just adding the file src as a script tag in html - adding hmr would be great

Has there been any progress on this? I started seeing hmr-related errors while using the above, vite-plugin-svelte.

rixo commented

Has there been any progress on this?

No sorry. WebComponent HMR implementation would be very different from what we already have and, to be honest, it is far from the top of my TODO list... There are a few items to complete support and increase stability before it.

Also very interested in this. I created svelte-retag which actually handles Svelte 3 & 4 custom elements and supports Vite HMR (along with proper support of slots in the light DOM and some other features).

Anyway, here's a proof of concept in plain Svelte 4 (if it's helpful)...

git clone https://github.com/patricknelson/svelte-v4-custom-elements.git
cd svelte-v4-custom-elements
npm i
npm run dev

... and then a demo of it working with svelte-retag: 🚀

git checkout hmr-via-svelte-retag
npm i

In both cases, if you edit src/lib/ExampleElement.svelte you'll see it in action.

In Svelte 3 the issue was that it got redeclared over and over.

However, in Svelte 4 the issue is that HMR simply doesn't trigger at all. My hunch here is that it's working in svelte-retag because the main Svelte component itself is proxied so that changes are reflected, but for some reason the custom element instance defined at Component.element (the .element property of the component itself, specifically) is not proxied and so it doesn't swap out.

Another hunch... after some research, it seems that if we can maybe have the compiled .svelte component emit the customElements.define(create_custom_element(...)); call after the proxy is setup, then there's some potentially that HMR might work. I think that's probably the reason why it's already working right now in svelte-retag (https://github.com/patricknelson/svelte-retag/), since this is effectively what my package is already doing. i.e.

Deriving the HTMLElement instance (or, the web component wrapper created by create_custom_element()) based on the proxied component rather than the original component instance. Essentially, relocating it here (reformatted compiled output):

image

This may in fact be a Svelte compiler issue but I'm not sure. Maybe the HMR adapter is able to ensure it's proxy is injected above this location?

Edit: See also: sveltejs/svelte#8681 (comment)

You can also try this yourself by setting tag: null and then hacking your component instance and create_custom_element() instance into the window object and calling it manually and it at least works that way (that's where I got the hunch). 🤔 e.g.

import ExampleElement from './lib/ExampleElement.svelte';
import { create_custom_element } from 'svelte/internal';

window.ExampleElement = ExampleElement; // this is now proxied for HMR
window.create_custom_element = create_custom_element;

Then later on:

customElements.define('native-example-element', create_custom_element(ExampleElement, {}, [], [], false));

cc @rixo

we cannot redefine the custom-element though, thats not how browsers work. so the best we can achieve would be to have the svelte-hmr proxy working for the svelte component that lives inside the wrapper.

As soon as the interface of the custom element changes, you have to do a full reload. (eg adding or removing a prop).

@dominikg are you addressing me?

If so: I understand. 😄 The multiple calls to customElements.define() what would occur as the module is reloaded is separate from the triggers that lead to that reload (i.e. the proxying that I was referring to above). The issue of multiple definitions is easily resolved by just checking to see if the element is already defined. When it is defined, it should point to the HMR proxy that does the heavy lifting of swapping out (“hot reloading”) the actual HTMLElement instance like svelte-hmr already does for the Svelte component.

In fact, that’s how it already works (in svelte-retag). In that case, the HTMLElement wrapper that initializes a Svelte component is actually just pointing to the HMR proxy that’s already being helpfully wrapped by svelte-hmr at dev runtime on the fly in Vite. That’s also why the redundant customElements.define() calls are avoided, coincidentally, because svelteRetag() happens to do this outside of the module that’s getting reloaded by HMR.

So in this case, I’m just addressing the HMR boundary issue (which then cascades to the next issue of multiple definitions, again easily resolved) by just moving the proxy definition up a tad. Does that make sense?

As soon as the interface of the custom element changes, you have to do a full reload. (eg adding or removing a prop).

Also: Presumably this interface never changes (at least during dev runtime). That’s because this is just a wrapper already (both in svelte-retag as well as in the implementation in svelte itself). This HTMLElement wrapper points to the proxy component wrapper (again, just in dev) which then points to the actual component which does change frequently at dev runtime. While those props do change, it’s behind the proxy which is why it’s fine (and works as intended).

Am I misunderstanding?