npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@open-wc/dev-server-hmr

v0.2.0

Published

Plugin for HMR with web components

Downloads

20,843

Readme

Development >> Hot Module Replacement ||50

This project is currently experimental. Try it out and let us know what you think!

@web/dev-server plugin for "hot module replacement" or "fast refresh" with web components and es modules.

Keeps track of web component definitions in your code, and updates them at runtime on change. This is faster than a full page reload and preserves the page's state.

HMR requires the web component base class to implement a hotReplacedCallback.

Installation

First, install @web/dev-server if you don't already have this installed in your project.

Install the package:

npm i --save-dev @open-wc/dev-server-hmr@next

Add the plugin to your web-dev-server.config.mjs:

import { hmrPlugin } from '@open-wc/dev-server-hmr';

export default {
  plugins: [
    hmrPlugin({
      include: ['src/**/*'],
    }),
  ],
};

Pick one of the presets below if needed, then start the dev server like normal. You don't need to make any changes to your code. If a component or one of its dependencies is changed, the component is replaced. Otherwise, the page is reloaded.

Make sure to start the dev server without watch mode, as this always forces a page to reload on change.

Implementations

Vanilla

For vanilla web component projects that don't implement any base class or library this plugin should detect your components automatically. You need to implement a hotReplacedCallback on your element to trigger a re-render, read more about that below.

Lit & LitElement

LitElement v2 and Lit v2 (which is LitElement v3) need different "patches" to support HMR. To distinguish between v2 and v3 we take the import as a hint.

// v2 patch
import { LitElement } from 'lit-element';
// v3 patch
import { LitElement } from 'lit';

If you mix LitElement v2 and LitElement v3 in the same app be sure to import v2 from 'lit-element' and v3 from 'lit';

import { hmrPlugin, presets } from '@open-wc/dev-server-hmr';

export default {
  plugins: [
    hmrPlugin({
      include: ['src/**/*'],
      // only v3
      presets: [presets.lit],
      // only v2
      presets: [presets.litElement],
      // both v3 & v2
      presets: [presets.lit, presets.litElement],
    }),
  ],
};

FAST Element

We have experimental support for FAST element using a small code patch included in the preset. This might not cover all use cases yet, let us know if you run into any issues!

import { hmrPlugin, presets } from '@open-wc/dev-server-hmr';

export default {
  plugins: [
    hmrPlugin({
      include: ['src/**/*'],
      presets: [presets.fastElement],
    }),
  ],
};

Haunted

We have experimental support for Haunted using a small code patch included in the preset. This might not cover all use cases yet, let us know if you run into any issues!

import { hmrPlugin, presets } from '@open-wc/dev-server-hmr';

export default {
  plugins: [
    hmrPlugin({
      include: ['src/**/*'],
      presets: [presets.haunted],
    }),
  ],
};

Other libraries

If you know any other libraries that work correctly with HMR we can add presets for them here. Presets help by configuring the detection of base classes, decorators, and/or runtime code patches.

How it works

Web component HMR works by replacing class references and instance prototype chains with proxies. Whenever a class or a property on the prototype chain is accessed, the proxy will forward to the latest implementation of the web component class.

After updating a web component class, newly created elements will use the latest class and things work as expected from there.

For existing elements, the prototype chain is updated to reference the new class. This means that things like class methods are updated, but local class fields or properties are not. This is a feature because it retains component state, but also a limitation because newly added fields/properties are not available. The constructor is also not re-run for existing elements.

Web component HMR works best when editing HTML and CSS. Because we're overwriting and moving around code at runtime, assumptions you can normally make about how your code runs is broken. It's recommended to periodically do a full refresh of the page.

Limitations

The following limitations should be kept in mind when working with open-wc HMR:

  • For existing elements, constructors are not re-run when updating a class.
  • For existing elements, newly added fields/properties are not available.
  • A web component's observedAttributes list cannot be updated over time. Updates require a refresh.

Did you run into other limitations? Let us know so we can improve this list.

Detecting web components

To "hot replace" an edited web component we have to be able to detect component definitions in your code. By default we look for usage of customElements.define and/or classes that derive from HTMLElement directly.

For other use cases, you can specify base classes or decorators to indicate component definitions.

If you are using a preset, the detection will already be configured correctly.

Base classes

The base class option detects web components that extend a specific base class. The base class can be matched by name, or as an import from a specific module.

hmrPlugin({
  include: ['src/**/*'],
  baseClasses: [
    // anything that extends a class called MyElement
    { name: 'MyElement' },
    // anything that extends a class called MyElement imported from 'my-element'
    { name: 'MyElement', import: 'my-element' },
    // anything that extends a class called MyElement imported from './src/MyElement.js' (relative to current working directory)
    { name: 'MyElement', import: './src/MyElement.js' },
    // anything that extends a default importeded class from 'my-element'
    { name: 'default', import: 'my-element' },
  ],
});

Decorator

The plugin can also detect web components defined using decorators. The decorators can be matched by name, or as an import from a specific module.

hmrPlugin({
  include: ['src/**/*'],
  decorators: [
    // any class that uses a decorator called customElement
    { name: 'customElement' },
    // any class that uses a decorator called customElement imported from 'my-element'
    { name: 'customElement', import: 'my-element' },
    // any class that uses a decorator called customElement imported from './src/MyElement.js' (relative to current working directory)
    { name: 'customElement', import: './src/MyElement.js' },
    // any class that uses a decorator default imported from 'my-element'
    { name: 'default', import: 'my-element' },
  ],
});

Functions

Some libraries create the web component class definition from a function. The functions can be matched by name, or as an import from a specific module.

hmrPlugin({
  include: ['src/**/*'],
  functions: [
    // any class that uses a function called component
    { name: 'component' },
    // any class that uses a function called component imported from 'my-element'
    { name: 'component', import: 'my-element' },
    // any class that uses a function called component imported from './src/MyElement.js' (relative to current working directory)
    { name: 'component', import: './src/MyElement.js' },
    // any class that uses a function default imported from 'my-element'
    { name: 'default', import: 'my-element' },
  ],
});

Implementing HMR

To implement HMR your element or element's base class should implement one of the hotReplacedCallback methods. In your method you can do custom updating logic, and kick off re-rendering of your element.

class MyElement extends HTMLElement {
  // static callback, called once when a class updates
  static hotReplacedCallback() {
    this.update();
  }

  // instance callback, called for each connected element
  hotReplacedCallback() {
    // this should kick off re-rendering
    this.rerender();
  }
}

This plugin currently only works for Web Dev Server. The approach should be compatible with other ESM-HMR implementations in other dev servers. This is something that can be explored.

Compatibility with non es modules HMR, such as webpack, is not currently a goal.

Patching

If you don't want to include the HMR code in your production code, you could patch in the callbacks externally:

import { MyElement } from 'my-element';

MyElement.hotReplacedCallback = function hotReplacedCallback() {
  // code for the static callback
};

MyElement.prototype.hotReplacedCallback = function hotReplacedCallback() {
  // code for the instance callback
};

Make sure this code is loaded before any of your components are loaded. You could also do this using the patch option in the config:

import { hmrPlugin } from '@open-wc/dev-server-hmr';

const myElementPatch = `
import { MyElement } from 'my-element';

MyElement.hotReplacedCallback = function hotReplacedCallback() {
  // code for the static callback
};

MyElement.prototype.hotReplacedCallback = function hotReplacedCallback() {
  // code for the instance callback
};
`;

export default {
  plugins: [
    hmrPlugin({
      include: ['src/**/*'],
      baseClasses: [{ name: 'MyElement', import: 'my-element' }],
      patches: [myElementPatch],
    }),
  ],
};
import '@rocket/launch/inline-notification/inline-notification.js';