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 🙏

© 2025 – Pkg Stats / Ryan Hefner

esm-class-reloading

v1.0.0

Published

A live reloading decorator for ESM classes

Readme

"Live Reload" for ESM classes

"Write code, run code, update code while it's running. There is no restart"

This little module exports a reloading function that decorates ESM classes with watch and "hot reload" functionality, so that any time you modify your code, both the class definition itself and all instance objects get updated to whatever your new code is, so that you don't need to constantly restart your code, losing your runtime state.

The way it does this is really pretty straight forward: when you register a class for hot reloading, the code creates a Proxy with a trap for the constructor function so that when you create new instances of your class, you're not. Instead, the Proxy trap creates new instances of whatever is "the current version of your class" according to what's in your module file, which we monitor using a Node file watcher. At the same time, the Proxy constructor also saves every instance you build to a keyed list so that any time the class definition updates, we can also update all object instances of your class.

Meaning that your code "just live-reloads" while you're working on it.

A simple example

Say we had this simple class:

export class SomeClass {
  constructor(a, b, c) {
    Object.assign(this, {a, b, c});
  }
  toString() {
    const { a, b, c } = this;
    return [a, b, c].join(`, `);
  }
};

we can make this is a live-reloading class with minimally invasive changes:

// Import the reloading decorator
import { reloading } from "esm-class-reloading";

// Then decorate our original class:
const SomeClass = reloading(
  import.meta,

  class SomeClass {
    constructor(a, b, c) {
      Object.assign(this, {a, b, c});
    }
    toString() {
      const { a, b, c } = this;
      return [a, b, c].join(`, `);
    }
  }
);

// And then we export that decorated class instead.
export { SomeClass };

Now let's use that class in some code that's going to stick around for a bit:

// Import the web server module and our class
import http from "node:http";
import { SomeClass } from "./some-class.js";

// Create a global SomeClass instance:
const s = new SomeClass(4, 5, 6);

// Then define a server so we have a persistent process
// that we can use to show off live-reloading:
const server = http.createServer((req, res) => {
  // Any time you request any URL, we'll create a new
  // SomeClass instance and then log both the global
  // and this new local object:
  res.writeHead(200, { "Content-Type": "text/plain" });
  const t = new SomeClass(1, 2, 3, 4, 5, 6, 7, 8, 9);
  res.end(`"t" is "${t}" and "s" is "${s}"`);
});

// Let's go.
server.listen(8000);

When we hit up http://localhost:8000, this will respond with:

`"t" is "1, 2, 3" and "s" is "4, 5, 6"

This is, of course, what you'd expect: that's just how JS works, nothing special.

However, with our http server still running, we now update our SomeClass code to take a fourth argument, d, and save it:

import { reloading } from "reloading-classes";

const SomeClass = reloading(
  import.meta,
  class SomeClass {
    // let's add a fourth argument to the constructor
    constructor(a b, c, d) {
      Object.assign(this, {a, b, c, d});
    }
  }
  toString() {
    // and then include that in our toString
    const { a, b, c, d } = this;
    return [a, b, c, d].join(`, `);
  }
);

export { SomeClass };

When we save our file, the console will log the fact that a live-reload occurred:

Reloading module .../some-class.js at 1765737262461
  in reloading (.../esm-class-reloading/index.js:82:5)
  in .../some-class.js:3:19
  in ModuleJob.run (node:internal/modules/esm/module_job:371:25)
  in async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:702:26)
  in async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)

And reloading http://localhost:8000, we see that our class has updated and the response is:

"t" is "1, 2, 3, 4" and "s" is "4, 5, 6, undefined"

Both the original class instance and the newly created instance of what seemed like "the same class" now know about our new property d, and the live-reload has preserved the runtime state of s: it didn't have that property before, so it doesn't magically have anything in it after the reload, whereas our newly created instance (that we gave way more constructor arguments than it needed) was able to populate that property during construction.

And that's it.

Just slap a bunch of reloading(import.meta, ...) decorations in your code and stop restarting your server all the time. Just update your code, as needed, on the fly.

... Is what I would say if we didn't need to deploy code to production

None of your code should even be importing the reload module in production, so: don't just copy the example, it's working code, to show off what the module does, but it's not good code, and leaves your code open to exploits.

Instead, allocate your class using a reassignable variable and then conditionally wrap it so that it only uses the reloading decorator in development.

You can either do that with an actual "in-code conditionals" (the classic if (process.env.ENV_VAR === .. ) { ... } block), but a *much better solution is to use labeled statements:

let SomeClass = class { ... };

devOnly: {
    const { reloading } = await import(`esm-class-reloading`);
    SomeClass = reloading(import.meta, SomeClass);
}

export { SomeClass}

We can now tell whatever tools we're using in our "build:prod" task to literally throw away any code blocks marked with the devOnly label, so the production code (a) never imports the reloading module, and (b) doesn't even have any code clues in it that suggest you might be using this module to begin with.

"What if I'm just using this for toy projects?"

Under the "if your source code changes while running in production, someone's hacking your production instance" assumption, even if you do decide to just use this module without any kind of production preprocessing pass, at the very least remember to set your NODE_ENV environment variable to production, as that will "disable" this module.

If NODE_ENV is production, any reloading(import.meta, X) will immediately return X, unaltered and unproxied.

AND YOU SHOULD CONFIRM THAT WHAT YOU JUST READ IS TRUE!

No matter which approach you're using, you should open the index.js file in your node_modules/esm-class-reloading dir, and confirm that both the watch and reloading functions start with an immediate return when NODE_ENV is set to production.

"But I'm just running things locally!"

Then all the above is advisory, do whatever you want. Just remember that bad habits lead to bad code, and writing code in the assumption you're going to deploy it at some point means you're going to be in the habit of writing deploy-ready code when you do finally work on a project that is going to be used by the wider world.

Caveats

There is one fairly glaring "gotcha" with this approach: ESM does not have module unloading, so the way reloading works us by watching your modules for changes and then loading them "as new modules" using a cache-busting query argument (remember, modules are imported from URL, not from filepath, and are cached based on that URL). As such, any time a reload happens, the old module is kept in memory, and so you're going to run out of memory... well, not really, modules aren't hundreds of megabytes in memory, they're not even single megabytes in memory, so you'd need to be reloading an incredible number of times before you'd even notice this problem, but it does exist and it's good to be aware of, especially if you need to track down a memory leak: this module can absolutely interfere with that.

"This thing is too verbose!"

You're using live-reloading. One would think you would want to know whether or not what you expect to have happen, actually happens.

However, you can change the logging code by importing the setLogger function from the module and giving it your own logger with .log and .error functions.

And even if you want to suppress the constant "module was reloaded" messages, you still want an error logger: the watcher does not care about whether or not your module is valid ESM code, but the reload code has some checks to make sure your application doesn't crash just because you saved your file with syntax errors or the like, and it will yell at you.

I have thoughts!

Awesomesauce, let me know about them over on the issue tracker.

-- Pomax