• Stars
    star
    412
  • Rank 104,631 (Top 3 %)
  • Language
    TypeScript
  • License
    MIT License
  • Created over 4 years ago
  • Updated 4 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

a Hot Module Replacement (HMR) API for your ESM-based dev server.

ESM Hot Module Replacement (ESM-HMR) Spec

Authors: Fred K. Schott (Snowpack), Jovi De Croock (Preact), Evan You (Vue)
Status: Archived, no longer under development.

Hot Module Replacement (HMR) lets your browser live-update individual JavaScript modulesย in your application during development without triggering a full browser reload or losing the current web application state. This speeds up your development speed with faster updates on every change.

Web bundlers like Webpack, Rollup, and Parcel all implemented different, bundler-specific HMR interfaces. This makes it hard to share HMR integrations across dev environments. As a result, many framework integrations like React Fast Refresh and Preact's Prefresh need to be rewritten for every bundler that they'd like to support. See:

ESM-HMR is a standard HMR API for ESM-based dev environments. The rise of bundle-free development creates the opportunity for a common, standard HMR API built on top of the browser's native module system. ESM-HMR is built for the browser's native module system, so it can be used in any ESM-based dev environment.

Who's Using ESM-HMR?

What's in This Repo?

  1. esm-hmr/client.js - A client-side ESM-HMR runtime.
  2. esm-hmr/server.js - A server-side ESM-HMR engine to manage connected clients.
  3. An ESM-HMR spec to help your write your own client/server pieces. (coming soon)

Usage Example

export let foo = 1;

if (import.meta.hot) {
  // Receive any updates from the dev server, and update accordingly.
  import.meta.hot.accept(({ module }) => {
    try {
      foo = module.foo;
    } catch (err) {
      // If you have trouble accepting an update, mark it as invalid (reload the page).
      import.meta.hot.invalidate();
    }
  });
  // Optionally, clean up any side-effects in the module before loading a new copy.
  import.meta.hot.dispose(() => {
    /* ... */
  });
}

ESM-HMR API Overview

All ESM-HMR implementations will follow this API the behavior outlined below. If you have any questions (or would like clarity on some undefined behavior) file an issue and we'll take a look!

import.meta.hot

if (import.meta.hot) {
  // Your HMR code here...
}
  • If HMR is enabled, import.meta.hot will be defined.
  • If HMR is disabled (ex: you are building for production), import.meta.hot should be undefined.
  • You can expect your production build to strip out if (import.meta.hot) { ... } as dead code.
  • Important: You must use the fully expanded import.meta.hot statement somewhere in the file so that the server can statically check and enable HMR usage.

Note: import.meta is the new location for module metadata in ES Modules.

import.meta.hot.accept

accept()

import.meta.hot.accept();
  • Accept HMR updates for this module.
  • When this module is updated, it will be automatically re-imported by the browser.
  • Important: Re-importing an updated module instance doesn't automatically replace the current module instance in your application. If you need to update your current module's exports, you'll need a callback handler.

USE CASE: Your module has no exports, and runs just by being imported (ex: adds a <style> element to the page).

accept(handler: ({module: any}) => void)

export let foo = 1;
import.meta.hot.accept(
  ({
    module, // An imported instance of the new module
  }) => {
    foo = module.foo;
  }
);
  • Accept HMR updates for this module.
  • Runs the accept handler with the updated module instance.
  • Use this to apply the new module exports to the current application's module instance. This is what accepts your update into the the running application.

This is an important distinction! ESM-HMR never replaces the accepting module for you. Instead, the current module is given an instance of the updated module in the accept() callback. It's up to the accept() callback to apply that update to the current module in the current application.

USE CASE: Your module has exports that need to be updated.

accept(deps: string[], handler: ({deps: any[]; module: any;}) => void)

import moduleA from "./modules/a.js";
import moduleB from "./modules/b.js";

export let store = createStore({ a: moduleA, b: moduleB });

import.meta.hot.accept(
  ["./modules/a.js", "./modules/b.js"],
  ({ module, deps }) => {
    // Get the new
    store.replaceModules({
      a: deps[0].default,
      b: deps[1].default,
    });
  }
);

Sometimes, it's not possible to update an existing module without a reference to its dependencies. If you pass an array of dependency import specifiers to your accept handler, those modules will be available to the callback via the deps property. Otherwise, the deps property will be empty.

USE CASE: You need a way to reference your dependencies to update the current module.

dispose(callback: () => void)

document.head.appendChild(styleEl);
import.meta.hot.dispose(() => {
  document.head.removeChild(styleEl);
});

The dispose() callback executes before a new module is loaded and before accept() is called. Use this to remove any side-effects and perform any cleanup before loading a second (or third, or forth, or...) copy of your module.

USE CASE: Your module has side-effects that need to be cleaned up.

decline()

import.meta.hot.decline();
  • This module is not HMR-compatible.
  • Decline any updates, forcing a full page reload.

USE CASE: Your module cannot accept HMR updates, for example due to permenant side-effects.

invalidate()

import.meta.hot.accept(({ module }) => {
  if (!module.foo) {
    import.meta.hot.invalidate();
  }
});
  • Conditionally invalidate the current module when called.
  • This will reject an in-progress update and force a page reload.

USE CASE: Conditionally reject an update if some condition is met.

import.meta.hot.data

export let foo = 1;

if (import.meta.hot) {
  // Recieve data from the dispose() handler
  import.meta.hot.accept(({ module }) => {
    foo = import.meta.hot.data.foo || module.foo;
  });
  // Pass data to the next accept handler call
  import.meta.hot.dispose(() => {
    import.meta.hot.data = { foo };
  });
}
  • You can use import.meta.hot.data to pass data from the dispose() handler(s) to the accept() handler(s).
  • Defaults to an empty object ({}) every time an update starts.

Prior Art

This spec wouldn't exist without the prior work of the following projects:

More Repositories

1

snowpack

ESM-powered frontend build tool. Instant, lightweight, unbundled development. โœŒ๏ธ
JavaScript
19,494
star
2

CoVim

Collaborative Editing for Vim
Vim Script
2,945
star
3

pika-pack

๐Ÿ“ฆโšก๏ธ Build your npm package using composable plugins. https://www.pika.dev/blog/introducing-pika-pack/
JavaScript
2,312
star
4

the-node-way

Design patterns and best practices for building scaleable, maintainable and beautiful Node.js applications. Now with website! -->
JavaScript
1,492
star
5

create-snowpack-app

The all-in-one app template for Snowpack. [moved]
JavaScript
728
star
6

fflip

Flexible Feature Flipping/Flagging for Node.js
JavaScript
655
star
7

rollup-plugin-polyfill-node

A modern Node.js polyfill for your Rollup bundle.
TypeScript
174
star
8

pika-pack-builders

๐Ÿ— A collection of official & community @pika/pack build plugins
JavaScript
162
star
9

react

ESM builds of React & React DOM (v16)
JavaScript
53
star
10

standard-pkg

Standard NPM Package Format
TypeScript
42
star
11

analyze-npm

Analyze npm, hunt for es module packages
JavaScript
35
star
12

pika-pack-examples

Example @pika/pack packages & projects
JavaScript
33
star
13

slackey

The simple, easy-to-use JavaScript SDK for Slack
JavaScript
32
star
14

fflip-express

fflip integration for the express.js web framework
JavaScript
16
star
15

dbplace

JavaScript
12
star
16

snowpack-plugin-starter-template

Blank Snowpack plugin template to get up and running fast
TypeScript
6
star
17

headless-garden

TypeScript
3
star
18

middle-man

Simple express middleware manager
JavaScript
3
star
19

ie11-going-away-party

Working title
HTML
1
star