The Ongoing War Between CJS & ESM: A Tale of Two Module Systems

Nathan G Bornstein - Jul 11 - - Dev Community

(This was originally an in-person talk I gave, so the following graphics are from said presentation slides, the bulk of which are attributed to SlidesGO; check them out for awesome & free slides! Just be sure to credit them during your presentation <3)


A battle has raged on for millennia, claiming numerous casualties in its wake, where no single individual, corporation nor entity was safe from the wreckage that followed in its trail; hidden deep within the binary bunkers, there festered something so awful, so terrible that mankind is still reeling from it today.


Of course, I'm speaking of the dreaded 20-year war that is commonly known as...


COMMON JAVASCRIPT (CJS)

VS

ECMA SCRIPT MODULES (ESM)


So with that, my friends, let's dive into the ongoing war between the two different kinds of module implementations that exist within our favorite language, known as JavaScript; CJS & ESM.


What even is CJS and ESM?

Yeah, what is ESM and CJS, anyways? Well, by definition, ESM stands for ECMAScript modules and CJS stands for Common JavaScript. ECMA is the governing body for JavaScript standardization and CJS, well…that’s kind of just the term that was given before any governing body had any say in the matter. Let’s first dive into some of the history surrounding these two methodologies


Differences between ESM & CJS

By now, I’m sure we’ve all used both variations of these syntaxes to bring in some sort of code that we’ve needed within our existing codebase. CJS syntax uses the require() and export.modules() commands to either bring in and/or send out some bit of code we’ve either written ourselves or have used by someone else to make our code do what it’s supposed to do.

Same exact thing for ESM syntax; it uses the import and export commands to either bring in or ship out some code we’ve either written ourselves or have used by someone else to make our code work. Seems like both are basically doing the same thing, right? So...


What's the difference between ESM and CJS?

Well, that’s my current job to explain to you, but just let me say that difference is literally EVERYTHING. Let's explore how these two seemingly comparative modules differ so greatly.


CJS vs ESM methods

At its core, CJS is synchronous and dynamic in its methods of importing and exporting modules. This means that a single module is only able to be loaded at a time and the next module is unable to be processed until the previous one has finished. The dynamic aspect comes into play when only certain modules are required in, as they’re needed at run-time.

ESM on the other hand, is asynchronous and static; this means that modules are able to be loaded in, regardless of any current processes occurring. The static aspect of ESM pertains to the analysis of a given module and its dependencies, once they’re compiled.

Overall, both of these module implementations have their own strengths and weaknesses; the most common use-case for either of these, is for front end processes to use ESM and back end processes to use CJS.

But how did we arrive at this point? And why are computer nerds everywhere choosing factions to represent their chosen side to fight the good fight? To answer that, we need to consult the dusty tomb of JavaScript’s history.


ESM & CJS history

CJS was first introduced in the year 2009 by Mozilla engineer Kevin Dangoor, after the need for the modularizing of code became evident for larger code bases. Imagine having to contain an entire full stack project within a single file of code; NO THANK YOU!

So with the mother of innovation fully at work, CJS was born to address the lack of a module system in JavaScript, primarily for server-side applications. And a fun fact about CJS is that it was initially called ServerJS.

So that’s great and all, but if CJS solved so many problems, why was there a need for ESM to exist in the first place? The answer to that lies within the need for a standardized approach to the existing module system, as well as the need for asynchronous functionality that front end based applications necessitate.


ENTER: ESM

ESM was introduced in 2015, alongside the advent of ES6, which brought about significant changes that a lot of us take for granted as the baseline approach for operating within JavaScript.

Ever since, the question on everyone’s mind has been, “why don’t we get rid of CJS and only implement ESM as the main source of truth for the modules we use?”

…do you want the truth? Do you really want the truth?

Well I don’t think you can handle the truth!

But alright, the truth is…


Legacy code and an unwillingness to change

What you see before your very eyes. That’s right my fellow friends, CJS solely exists because old engineers refuse to adapt to any kind of new technology and are firmly set in their tired and decrepit ways.

Alright, so not really, but based off of the digging around I’ve done, that seems to make up at least 20% of the reason why.

The bulk of the reason why these two approaches exist lies within the fact that Node.js has signed a blood-pact with CJS and isn’t functional without it, unless of course you bring in some external configurations to make server-side processes use ESM syntax, against their will (for example Typescript).

It’s also worth it to note that the vast majority of browsers simply refuse to acknowledge CJS’s existence, unless you bring in some kind of transpiler to force that acknowledgement. Although, before I get too ahead of myself, let’s gaze into the opening statement I had first written out at the beginning of this post; insecurity vs. security:


insecurity vs. security

Back in 2016, a single individual disrupted the entire ecosystem of the web by deleting a mere 11 lines of code, as you see above. Sites that used React, such as Facebook, Instagram…basically any website you visit these days, experienced a downtime of about 2 hours thanks to this deletion of only 11 lines of code.

Seems like nothing super impressive, right?

Wrong.

The single fact that over half of the Internet’s most-visited sites experienced that long of a down time, and billions of dollars lost in ad revenue, speaks volumes to just how fragile of an ecosystem we’re operating within. This piece of code, called "left-pad", is a package found within npm which solely operates based off of CJS modules.

The beauty and downfall of npm-based packages, and in turn, CJS modules, is that anyone is able to alter (or delete) their packages operating within CJS boundaries, and if the vast majority of websites are using that very code you’ve written, either knowingly or blissfully ignorant, significant changes can happen in the blink of an eye.

I encourage you to research more into this event, to see the full brevity of this small change that disrupted countless operations.

This isn’t to say that if this code had been using ESM modules that this could’ve been avoided, I only point this out to show that regardless of how much security we try to incorporate into the tools we use, we very much rely on external factors, even if we know it or not.

So with that, let’s dive into how ESM and CJS incorporate their different versions of security:


Pick your flavor of security

ESM introduced a number of security enhancements, which I’ll touch on a few of them briefly.

The first big implementation was an improved module-specific scoping, which means whoever is importing a package has to define their own variables, as opposed to having variables brought into the global scope.

Another big security feature upgrade was the use of import maps. Import maps are aiming to replace the use of bundlers, such as webpack, by calling in the packages you need in the actual <script> tag in an HTML file. You simply link the URL within an imports property and this helps with the resolution of the module, as well as controlling the origin of where that module is coming from. Currently, only a single import map is allowed per document, but changes are in the pipeline to address this limitation.

Now let’s get into the extremely limited security features of the CJS module.

CJS offers a little bit of encapsulation, but this is basically just from the encapsulation that JavaScript already offers natively from its scope within functions. So, not so much of a CJS-specific security implementation, it’s moreover a nice thing that happened to already be there.

The next security feature that was, in fact, implemented specifically for CJS is our trusty friend package-lock.json. This concept was introduced by npm to ensure reproducibility within any given module, so that what you got was going to function as the creator intended.

Although the beauty and terror within that statement is the latter; you’re at the behest of the creator. So by and large, most npm packages aren’t going to be malicious. But it only takes one bad actor to alter a single file and half of the Internet is down for an indeterminate amount of time.

So with all of these competing factors...


What's a dev to choose?

Should we choose team ESM for their improved security features and the slowly encroaching takeover that all modules, both back and front end, are eventually going to succumb to?

Should we choose Team CJS for their tried and true methods standing the test of time and the ease of use that a vast majority of npm packages are based on?

Or should we all just go the nihilism route and finally realize either of these viewpoints are essentially meaningless?

Well, allow me to give you a 100% objective and absolutely NOT biased take:


It depends

Though it may seem like I’ve been framing either module systems as at odds against each other, the truth is that each system has their own use-case.

The main take-away here is that, if you’re writing a new app from scratch, try to implement ESM if you can. ESM has significant security improvements, and just the fact that you only need to use a single type of syntax to import and export for both the front end and the back end, is a pretty cool thing.

CJS has been the de-facto standard for a very long time and is still widely implemented within the entire ecosystem that we all call home. The fact that it’s existed, and still is thriving for this long, stands as a testament to just how well it’s doing its job. So if you’re working within an existing full stack application and the back end is already using CJS, it’s probably fine to stick with it. Plus, you can also convert it to ESM pretty effortlessly.

Overall, both methods have their own use-cases and a single one isn’t necessarily better than the other and both will more than likely be around for a very long time.


And with that my friends, we’ve reached the end of this controversial talk. If there’s anything you take away from this, it’s that absolute power corrupts absolutely and ESM and CJS are friends, rather than foes.

Thank you for reading! ~~ <3

. . . . . . . . . . . . .