fix(dev-server-hmr): allow prototype modification

This commit is contained in:
Lars den Bakker
2020-12-25 11:30:19 +01:00
parent 9405f2e589
commit be1f93e8b4
4 changed files with 37 additions and 15 deletions

View File

@@ -0,0 +1,5 @@
---
'@open-wc/dev-server-hmr': patch
---
allow prototype modification

View File

@@ -10,6 +10,4 @@ const template = html<TodoApp>`
name: 'todo-app',
template,
})
class TodoApp extends FASTElement {
static definition = { name: 'todo-app', template };
}
class TodoApp extends FASTElement {}

View File

@@ -1,7 +1,7 @@
const patch = `import { WebComponentHmr } from '/__web-dev-server__/wc-hmr/runtime.js';
HTMLElement.prototype.hotReplacedCallback = function hotReplacedCallback() {
const temp = new this.constructor();
const temp = document.createElement(this.localName);
this._scheduler.renderer = temp._scheduler.renderer;
this._scheduler.update();
};

View File

@@ -1,5 +1,5 @@
// @ts-nocheck
/* eslint-disable no-param-reassign */
/* eslint-disable no-param-reassign, no-console */
// override global define to allow double registrations
const originalDefine = window.customElements.define;
@@ -57,11 +57,7 @@ function createProxy(originalTarget, getCurrentTarget) {
proxyHandler[method] = (_, ...args) => {
if (method === 'get' && args[0] === 'prototype') {
// prototype must always return original target value
return Reflect[method](originalTarget, ...args);
}
if (args[0] === 'observedAttributes') {
// observedAttributes must always be forwarded to the original class
return Reflect[method](originalTarget, ...args);
return Reflect[method](_, ...args);
}
return Reflect[method](getCurrentTarget(), ...args);
};
@@ -96,7 +92,7 @@ export class WebComponentHmr extends HTMLElement {
const key = keysForClasses.get(this.constructor);
const p = proxiesForKeys.get(key);
// replace the constructor with a proxy that references the latest implementation of this class
this.constructor = p.proxy;
this.constructor = p.currentProxy;
// replace prototype chain with a proxy to the latest prototype implementation
replacePrototypesWithProxies(this);
}
@@ -110,6 +106,7 @@ window.WebComponentHmr = WebComponentHmr;
function injectInheritsHmrClass(clazz) {
let parent = clazz;
let proto = Object.getPrototypeOf(clazz);
// walk prototypes until we reach HTMLElement
while (proto && proto !== HTMLElement) {
parent = proto;
proto = Object.getPrototypeOf(proto);
@@ -144,7 +141,8 @@ export function register(importMeta, name, clazz) {
const connectedElements = trackConnectedElements(clazz);
proxiesForKeys.set(key, {
proxy,
originalProxy: proxy,
currentProxy: proxy,
originalClass: clazz,
currentClass: clazz,
connectedElements,
@@ -155,21 +153,42 @@ export function register(importMeta, name, clazz) {
// class was already registered before
// register new class, all calls will be proxied to this class
const previousProxy = existing.currentProxy;
const currentProxy = createProxy(clazz, () => proxiesForKeys.get(key).currentClass);
existing.currentClass = clazz;
existing.currentProxy = currentProxy;
Promise.resolve().then(() => {
// call optional HMR on the class if they exist, after next microtask to ensure
// module bodies have executed fully
if (clazz.hotReplacedCallback) {
clazz.hotReplacedCallback();
try {
clazz.hotReplacedCallback();
} catch (error) {
console.error(error);
}
}
for (const element of existing.connectedElements) {
if (element.constructor === previousProxy) {
// we need to update the constructor of the element to match to newly created proxy
// but we should only do this for elements that was directly created with this class
// and not for elements that extend this
element.constructor = currentProxy;
}
if (element.hotReplacedCallback) {
element.hotReplacedCallback();
try {
element.hotReplacedCallback();
} catch (error) {
console.error(error);
}
}
}
});
return existing.proxy;
// the original proxy already forwards to the new class but we're return a new proxy
// because access to `prototype` must return the original value and we need to be able to
// manipulate the prototype on the new class
return currentProxy;
}