Photo by vaun0815 on Unsplash

There is a “slightly annoying” difference, or better, a limitation, in native ESM , compared to CommonJS : it’s not possible to deal with the module system internal cache, hence it’s not possible to invalidate such cache, or tell NodeJS that is actually referring to , unless importmap lands officially in NodeJS core too.

… but I’m sure there is a module for that …” and you’re obviously right, although we don’t always control how NodeJS is bootstrapped, as example via serverless services, or server side workers, so I’ve decided to explore a different approach.

A Home Made Registry

The technique I am going to show is not suitable for all scenarios, it’s not a substitute of regular modules, and most importantly, it cannot be bundled the way we usually bundle all files together.

As simple as it looks, the registry.js file should expose a Map that either client or server side ESM can consume and share, thanks to ESM’s internal cache.

This registry is in charge of setting arbitrary module names, so that libraries that could work with both client or server modules, can use such registry instead of importing the libraries themselves.

Check this µland example out:

The component exported by above counter.js file, retrieve its utilities via the previously mentioned registry.js, which once again, should not be bundled within the rest of the code.

The Client Side

So, how can we define that entry in the registry?

The reason we need to separate scripts, is that ESM is solved all at once, so that importing the counter within the same script, where the registry defines libraries, will parse such counter before any code is executed, hence once it’s executed, the registry won’t have the library in yet, as that happens after.

It’s also worth mentioning that this technique coincidentally allows bundling all 3rd party libraries dependencies eventually at once, as long as these are then set as registry entry, so that the bundle could be split in libraries, and the rest of the code could land on demand, via dynamic import, or as separate bundle, still being sure that the registry is not bundled within the process.

The Server Side

As we don’t have to deal with tags on the server, the only way to grant that the registry works as expected, is to define right away all libraries and dependencies ASAP, and import modules that are based on such registry asynchronously.

In this example, the module is provided instead by µland-ssr, so that our Counter component doesn’t need a single line change between client and server.

… but why …

I have different modules that work seamlessly in both client and server, and being able to reuse exact same code across these two different environments, and libraries targets, is something I’ve been doing, in a way or another, pretty much forever, but the ability to “not care about the env at the module level” is something still very hard to do, but I’d love to experiment this approach more.

µhtml and µhtml-ssr are just yet another example where this technique could make development pretty sweet, but the list of client/server modules that expose the same API, but produce dedicated results per environment, is bigger than what’s in my repositories, so I hope this technique will help other developers that, like me, would like to blur the line between client and server, as opposite of writing and , especially when would be identical for both scenarios 👋

P.S. I can’t wait for to land in NodeJS and make this technique less useful, or needed at all, in the long term!

Web, Mobile, IoT, and all JS things since 00's. Formerly JS engineer at @nokia, @facebook, @twitter.