├── .gitignore ├── yarn.lock ├── index.d.ts ├── README.md ├── package.json ├── LICENSE └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | yarn-error.log 4 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from 'vite'; 2 | 3 | type PluginFactory = () => Plugin; 4 | 5 | declare const createPlugin: PluginFactory & { preambleCode: string }; 6 | 7 | export = createPlugin; 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @dblechoc/plugin-lit-refresh 2 | 3 | Provides Lit HMR support support for Vite. State won't reload, just styles. 4 | 5 | Basically [dev-server-hmr](https://github.com/open-wc/open-wc/blob/master/packages/dev-server-hmr/src/wcHmrRuntime.js) republished for Vite. 6 | 7 | ```js 8 | // vite.config.js 9 | import LitRefresh from "@dblechoc/plugin-lit-refresh"; 10 | 11 | export default { 12 | plugins: [LitRefresh()], 13 | }; 14 | ``` 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dblechoc/plugin-lit-refresh", 3 | "version": "0.0.6", 4 | "license": "MIT", 5 | "author": "Anthony Mittaz", 6 | "description": "Lit HMR for Vite", 7 | "repository": "https://github.com/sync/plugin-lit-refresh.git", 8 | "files": [ 9 | "index.js", 10 | "index.d.ts" 11 | ], 12 | "main": "index.js", 13 | "types": "index.d.ts", 14 | "engines": { 15 | "node": ">=12.0.0" 16 | }, 17 | "peerDependencies": { 18 | "vite": ">=2.0.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Anthony Mittaz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /* eslint-disable consistent-return, global-require */ 3 | 4 | const preambleCode = ` 5 | // taken from: https://github.com/open-wc/open-wc/blob/master/packages/dev-server-hmr/src/wcHmrRuntime.js 6 | 7 | const proxiesForKeys = new Map(); 8 | const keysForClasses = new Map(); 9 | 10 | function trackConnectedElements(hmrClass) { 11 | const connectedElements = new Set(); 12 | const originalCb = hmrClass.prototype.connectedCallback; 13 | 14 | // eslint-disable-next-line no-param-reassign 15 | hmrClass.prototype.connectedCallback = function connectedCallback(...args) { 16 | if (originalCb) { 17 | originalCb.call(this, ...args); 18 | } 19 | connectedElements.add(this); 20 | }; 21 | 22 | const originalDcb = hmrClass.prototype.disconnectedCallback; 23 | // eslint-disable-next-line no-param-reassign 24 | hmrClass.prototype.disconnectedCallback = function disconnectedCallback( 25 | ...args 26 | ) { 27 | if (originalDcb) { 28 | originalDcb.call(this, ...args); 29 | } 30 | connectedElements.delete(this); 31 | }; 32 | return connectedElements; 33 | } 34 | 35 | const proxyMethods = [ 36 | 'construct', 37 | 'defineProperty', 38 | 'deleteProperty', 39 | 'getOwnPropertyDescriptor', 40 | 'getPrototypeOf', 41 | 'setPrototypeOf', 42 | 'isExtensible', 43 | 'ownKeys', 44 | 'preventExtensions', 45 | 'has', 46 | 'get', 47 | 'set', 48 | ]; 49 | 50 | /** 51 | * Creates a proxy for the given target, and fowards any calls to the most up to the latest 52 | * version of the target. (ex. the latest hot replaced class). 53 | */ 54 | function createProxy(originalTarget, getCurrentTarget) { 55 | const proxyHandler = {}; 56 | for (const method of proxyMethods) { 57 | proxyHandler[method] = (_, ...args) => { 58 | if (method === 'get' && args[0] === 'prototype') { 59 | // prototype must always return original target value 60 | return Reflect[method](_, ...args); 61 | } 62 | return Reflect[method](getCurrentTarget(), ...args); 63 | }; 64 | } 65 | return new Proxy(originalTarget, proxyHandler); 66 | } 67 | 68 | /** 69 | * Replaces all prototypes in the inheritance chain with a proxy 70 | * that references the latest implementation 71 | */ 72 | function replacePrototypesWithProxies(instance) { 73 | let previous = instance; 74 | let proto = Object.getPrototypeOf(instance); 75 | 76 | while (proto && proto.constructor !== HTMLElement) { 77 | const key = keysForClasses.get(proto.constructor); 78 | if (key) { 79 | // this is a prototype that might be hot-replaced later 80 | const getCurrentProto = () => 81 | proxiesForKeys.get(key).currentClass.prototype; 82 | Object.setPrototypeOf(previous, createProxy(proto, getCurrentProto)); 83 | } 84 | 85 | previous = proto; 86 | proto = Object.getPrototypeOf(proto); 87 | } 88 | } 89 | 90 | export class WebComponentHmr extends HTMLElement { 91 | constructor(...args) { 92 | super(...args); 93 | const key = keysForClasses.get(this.constructor); 94 | // check if the constructor is registered 95 | if (key) { 96 | const p = proxiesForKeys.get(key); 97 | // replace the constructor with a proxy that references the latest implementation of this class 98 | this.constructor = p.currentProxy; 99 | } 100 | // replace prototype chain with a proxy to the latest prototype implementation 101 | replacePrototypesWithProxies(this); 102 | } 103 | } 104 | 105 | window.WebComponentHmr = WebComponentHmr; 106 | 107 | /** 108 | * Injects the WebComponentHmr class into the inheritance chain 109 | */ 110 | function injectInheritsHmrClass(clazz) { 111 | let parent = clazz; 112 | let proto = Object.getPrototypeOf(clazz); 113 | // walk prototypes until we reach HTMLElement 114 | while (proto && proto !== HTMLElement) { 115 | parent = proto; 116 | proto = Object.getPrototypeOf(proto); 117 | } 118 | 119 | if (proto !== HTMLElement) { 120 | // not a web component 121 | return; 122 | } 123 | if (parent === WebComponentHmr) { 124 | // class already inherits WebComponentHmr 125 | return; 126 | } 127 | Object.setPrototypeOf(parent, WebComponentHmr); 128 | } 129 | 130 | /** 131 | * Registers a web component class. Triggers a hot replacement if the 132 | * class was already registered before. 133 | */ 134 | export function register(key, clazz) { 135 | const existing = proxiesForKeys.get(key); 136 | if (!existing) { 137 | // this class was not yet registered, 138 | 139 | // create a proxy that will forward to the latest implementation 140 | const proxy = createProxy( 141 | clazz, 142 | () => proxiesForKeys.get(key).currentClass, 143 | ); 144 | // inject a HMR class into the inheritance chain 145 | injectInheritsHmrClass(clazz); 146 | // keep track of all connected elements for this class 147 | const connectedElements = trackConnectedElements(clazz); 148 | 149 | proxiesForKeys.set(key, { 150 | originalProxy: proxy, 151 | currentProxy: proxy, 152 | originalClass: clazz, 153 | currentClass: clazz, 154 | connectedElements, 155 | }); 156 | keysForClasses.set(clazz, key); 157 | return proxy; 158 | } 159 | // class was already registered before 160 | 161 | // register new class, all calls will be proxied to this class 162 | const previousProxy = existing.currentProxy; 163 | const currentProxy = createProxy( 164 | clazz, 165 | () => proxiesForKeys.get(key).currentClass, 166 | ); 167 | existing.currentClass = clazz; 168 | existing.currentProxy = currentProxy; 169 | 170 | Promise.resolve().then(() => { 171 | // call optional HMR on the class if they exist, after next microtask to ensure 172 | // module bodies have executed fully 173 | if (clazz.hotReplacedCallback) { 174 | try { 175 | clazz.hotReplacedCallback(); 176 | } catch (error) { 177 | console.error(error); 178 | } 179 | } 180 | 181 | for (const element of existing.connectedElements) { 182 | if (element.constructor === previousProxy) { 183 | // we need to update the constructor of the element to match to newly created proxy 184 | // but we should only do this for elements that was directly created with this class 185 | // and not for elements that extend this 186 | element.constructor = currentProxy; 187 | } 188 | 189 | if (element.hotReplacedCallback) { 190 | try { 191 | element.hotReplacedCallback(); 192 | } catch (error) { 193 | console.error(error); 194 | } 195 | } 196 | } 197 | }); 198 | 199 | // the original proxy already forwards to the new class but we're return a new proxy 200 | // because access to 'prototype' must return the original value and we need to be able to 201 | // manipulate the prototype on the new class 202 | return currentProxy; 203 | } 204 | 205 | // override global define to allow double registrations 206 | const originalDefine = window.customElements.define; 207 | window.customElements.define = (tagName, classObj, ...rest) => { 208 | register(tagName, classObj); 209 | 210 | if (!window.customElements.get(tagName)) { 211 | originalDefine.call(window.customElements, tagName, classObj, ...rest); 212 | } 213 | }; 214 | `; 215 | 216 | /** 217 | * Transform plugin for transforming and injecting per-file refresh code. 218 | * 219 | * @returns {import('vite').Plugin} 220 | */ 221 | module.exports = function LitRefreshPlugin() { 222 | let shouldSkip = false; 223 | 224 | return { 225 | name: "lit-element-refresh", 226 | 227 | configResolved(config) { 228 | shouldSkip = config.command === "build" || config.isProduction; 229 | }, 230 | 231 | transform(code, id, options) { 232 | // (options was boolean ssr in vite <2.7.0) 233 | const ssr = typeof options === "object" ? options.ssr : options; 234 | if (shouldSkip || ssr) { 235 | return; 236 | } 237 | 238 | if (!/\.(t|j)sx?$/.test(id) || id.includes("node_modules")) { 239 | return; 240 | } 241 | 242 | // plain js/ts files can't use React without importing it, so skip 243 | // them whenever possible 244 | if (!code.includes("lit")) { 245 | return; 246 | } 247 | 248 | const header = ` 249 | // taken from: https://github.com/open-wc/open-wc/blob/master/packages/dev-server-hmr/src/presets/lit.js 250 | 251 | import { adoptStyles, supportsAdoptingStyleSheets, LitElement as Element } from 'lit'; 252 | 253 | // static callback 254 | Element.hotReplacedCallback = function hotReplacedCallback() { 255 | this.finalize(); 256 | }; 257 | // instance callback 258 | Element.prototype.hotReplacedCallback = function hotReplacedCallback() { 259 | if (!supportsAdoptingStyleSheets) { 260 | const nodes = Array.from(this.renderRoot.children); 261 | for (const node of nodes) { 262 | if (node.tagName.toLowerCase() === 'style') { 263 | node.remove(); 264 | } 265 | } 266 | } 267 | this.constructor.finalizeStyles(); 268 | if (window.ShadowRoot && this.renderRoot instanceof window.ShadowRoot) { 269 | adoptStyles( 270 | this.renderRoot, 271 | this.constructor.elementStyles 272 | ); 273 | } 274 | this.requestUpdate(); 275 | }; 276 | `; 277 | 278 | const footer = ` 279 | if (import.meta.hot) { 280 | import.meta.hot.accept(() => {}); 281 | } 282 | `; 283 | 284 | return { 285 | code: `${header}${code}${footer}`, 286 | }; 287 | }, 288 | 289 | transformIndexHtml() { 290 | if (shouldSkip) { 291 | return; 292 | } 293 | 294 | return [ 295 | { 296 | tag: "script", 297 | attrs: { type: "module" }, 298 | children: preambleCode, 299 | }, 300 | ]; 301 | }, 302 | }; 303 | }; 304 | 305 | module.exports.preambleCode = preambleCode; 306 | --------------------------------------------------------------------------------