Skip to content

Instantly share code, notes, and snippets.

@joepie91
Last active April 12, 2025 00:19
Show Gist options
  • Save joepie91/bca2fda868c1e8b2c2caf76af7dfcad3 to your computer and use it in GitHub Desktop.
Save joepie91/bca2fda868c1e8b2c2caf76af7dfcad3 to your computer and use it in GitHub Desktop.
ES Modules are terrible, actually

ES Modules are terrible, actually

This post was adapted from an earlier Twitter thread.

It's incredible how many collective developer hours have been wasted on pushing through the turd that is ES Modules (often mistakenly called "ES6 Modules"). Causing a big ecosystem divide and massive tooling support issues, for... well, no reason, really. There are no actual advantages to it. At all.

It looks shiny and new and some libraries use it in their documentation without any explanation, so people assume that it's the new thing that must be used. And then I end up having to explain to them why, unlike CommonJS, it doesn't actually work everywhere yet, and may never do so. For example, you can't import ESM modules from a CommonJS file! (Update: I've released a module that works around this issue.)

And then there's Rollup, which apparently requires ESM to be used, at least to get things like treeshaking. Which then makes people believe that treeshaking is not possible with CommonJS modules. Well, it is - Rollup just chose not to support it.

And then there's Babel, which tried to transpile import/export to require/module.exports, sidestepping the ongoing effort of standardizing the module semantics for ESM, causing broken imports and require("foo").default nonsense and spec design issues all over the place.

And then people go "but you can use ESM in browsers without a build step!", apparently not realizing that that is an utterly useless feature because loading a full dependency tree over the network would be unreasonably and unavoidably slow - you'd need as many roundtrips as there are levels of depth in your dependency tree - and so you need some kind of build step anyway, eliminating this entire supposed benefit.

And then people go "well you can statically analyze it better!", apparently not realizing that ESM doesn't actually change any of the JS semantics other than the import/export syntax, and that the import/export statements are equally analyzable as top-level require/module.exports.

"But in CommonJS you can use those elsewhere too, and that breaks static analyzers!", I hear you say. Well, yes, absolutely. But that is inherent in dynamic imports, which by the way, ESM also supports with its dynamic import() syntax. So it doesn't solve that either! Any static analyzer still needs to deal with the case of dynamic imports somehow - it's just rearranging deck chairs on the Titanic.

And then, people go "but now we at least have a standard module system!", apparently not realizing that CommonJS was literally that, the result of an attempt to standardize the various competing module systems in JS. Which, against all odds, actually succeeded!

... and then promptly got destroyed by ESM, which reintroduced a split and all sorts of incompatibility in the ecosystem, rather than just importing some updated variant of CommonJS into the language specification, which would have sidestepped almost all of these issues.

And while the initial CommonJS standardization effort succeeded due to none of the competing module systems being in particularly widespread use yet, CommonJS is so ubiquitous in Javascript-land nowadays that it will never fully go away. Which means that runtimes will forever have to keep supporting two module systems, and developers will forever be paying the cost of the interoperability issues between them.

But it's the future!

Is it really? The vast majority of people who believe they're currently using ESM, aren't even actually doing so - they're feeding their entire codebase through Babel, which deftly converts all of those snazzy import and export statements back into CommonJS syntax. Which works. So what's the point of the new module system again, if it all works with CommonJS anyway?

And it gets worse; import and export are designed as special-cased statements. Aside from the obvious problem of needing to learn a special syntax (which doesn't quite work like object destructuring) instead of reusing core language concepts, this is also a downgrade from CommonJS' require, which is a first-class expression due to just being a function call.

That might sound irrelevant on the face of it, but it has very real consequences. For example, the following pattern is simply not possible with ESM:

const someInitializedModule = require("module-name")(someOptions);

Or how about this one? Also no longer possible:

const app = express();
// ...
app.use("/users", require("./routers/users"));

Having language features available as a first-class expression is one of the most desirable properties in language design; yet for some completely unclear reason, ESM proponents decided to remove that property. There's just no way anymore to directly combine an import statement with some other JS syntax, whether or not the module path is statically specified.

The only way around this is with await import, which would break the supposed static analyzer benefits, only work in async contexts, and even then require weird hacks with parentheses to make it work correctly.

It also means that you now need to make a choice: do you want to be able to use ESM-only dependencies, or do you want to have access to patterns like the above that help you keep your codebase maintainable? ESM or maintainability, your choice!

So, congratulations, ESM proponents. You've destroyed a successful userland specification, wasted many (hundreds of?) thousands of hours of collective developer time, many hours of my own personal unpaid time trying to support people with the fallout, and created ecosystem fragmentation that will never go away, in exchange for... fuck all.

This is a disaster, and the only remaining way I see to fix it is to stop trying to make ESM happen, and deprecate it in favour of some variant of CommonJS modules being absorbed into the spec. It's not too late yet; but at some point it will be.

@mk-pmb
Copy link

mk-pmb commented Apr 6, 2025

Sooner or later I'll have to find a good solution, so feel free to subscribe that thread to get notified.

@Rush
Copy link

Rush commented Apr 7, 2025

Just bundle everything and then .cjs and .mjs doesn't matter :-)

@mk-pmb
Copy link

mk-pmb commented Apr 7, 2025

In a not-website scenario, the downside of bundling everything is the waste of disk space and/or transmission time. It also complicates the effort required for updating popular dependencies used in many of your programs. It's why mankind invented shared libraries.
However, thinking about it, maybe using a package manager with a transpile step inserted after each install, may be a worthy stopgap. Not sure how feasible it is though. I hope the alternatives mentioned above will turn out to be even better, and easier.

@guest271314
Copy link

I'm just not seeing a problem statement anywhere here. It's 2025. Bundling everything to a standalone script, or just ECMAScript Modules in individuals files is not only technically possible, that capability is shipped with deno and bun. The only folks who havn't caught on is folks using node version from the past, instead of just using nightly release, consistently.

@iambumblehead
Copy link

Language users benefit from stable churn-free foundations. Most users don't want rely on third-party bundlers to support math, logic or string operations either.

@guest271314
Copy link

stable churn-free foundations

No such state exists, anywhere, in any domain of human activity. If things were "stable churn-free" Moore's Law wouldn't be true and correct. People wouldn't be rollingaround with Android or Apple devices. They'd still be using the first Kyocera "smart phone", Palm Pilot, Blackberry. I guess you could break out a Commodore 64, run Node 20 though nightly is on 24, and put that mobile device down, and read a book, by yourself, with all electronics turned off. That's close to stable.

Other than that, technology is not static, and not fair. If technology was fair Beta would have lasted longer than VHS. CD's wouldn't have cost less than a dollar and sold for upwards of 20 dollars - before CD's essentially became obsolete. Ask Tower Records.

@iambumblehead
Copy link

No such state exists, anywhere, in any domain of human activity.

The stability of the english language allows you to write such a message here that can be understood by other people.

@guest271314
Copy link

The English language is a bastard, equivocal language that is not remotely "stable". For example, the term "sheriff" came about, not by Anglo-Saxons, but by Normans who conquered Anglo-Saxons and created the officie of "sheriff" as laison between the conqueror and the conquered. Anybody can remix English. It's done every day. From E-40 to Madison Avenue, governments to GitHub with it's "AI-powered" made up slogan, knowing good and well humans make the decisions at GitHub.

@Kreijstal
Copy link

The English language is a bastard, equivocal language that is not remotely "stable". For example, the term "sheriff" came about, not by Anglo-Saxons, but by Normans who conquered Anglo-Saxons and created the officie of "sheriff" as laison between the conqueror and the conquered. Anybody can remix English. It's done every day. From E-40 to Madison Avenue, governments to GitHub with it's "AI-powered" made up slogan, knowing good and well humans make the decisions at GitHub.

it's stable in short timespans, not over millenia, language doesnt change that much in a lifespan.. aka 100 years.

@guest271314
Copy link

Depends. The blues, jazz, rock and roll, hip-hop which is an entire art form in an unto itself all came about in the 20th c., C.E. And all were pointed to as something bad by certain people, until they figured out a way to sell stuff with it. A few weeks ago people were complaining that they couldn't understand anything Kendrick Lamar was saying at the Superbowl halftime show. Good. It ain't for them. Them folks just can't fathom words, culture, art, expression not being developed for them, in their image. And if you examine that deeper you'll find Young and Champollion claiming to have "deciphered" the MTW NTR. That writing and speaking was not made for or by them, and no high priest in the Temple system ever told either of those folks their guesses were correct. Ain't for them. They ain't initiates. Never will be. The best they can do is grave rob and pretend like they know. They never will.

There was a time when the some of the Several States criminalized literacy for Africans held as prisoners-of-war in the fledgling U.S. And that leads into western academia calling African and Turtle Islanders "slaves", which they never were - they were captured in warfare waged against them by European powers. The same as U.S. soldiers captured by Japanese were never called "slaves".

So, I ain't gonna be subject to the whims of somebody trying to control me with their words, when they have disdain for the words my ancestors created. To hell with them and their culture, their thinkling, and their holiest of holies. And I'll still exploit their gear for my own purposes, without a second thought.

So, originally there was just Rhino, a couple others. Then node came about, and a whole bund a people fell in love and they became static and stable - in their own minds. Meanwhile there's dozens of JavaScript runtimes now. There's ECMAScript, ECMA-262, and ECMAScript Modules. It's almost cult-like to stay stuck in 2015 - just because. Whatever. I ain't under that myopic spell, and definitely ain't under the spell of some European colonists with their tricknowledge language that they turn on a dime - always against the natives.

@csvan
Copy link

csvan commented Apr 8, 2025

Oh God this thread <3

@guest271314
Copy link

@csvan

Oh God this thread <3

Perfect example. Are we supposed to know what you mean by "God"?

From https://languages.oup.com/google-dictionary-en

Old English God, of Germanic origin; related to Dutch god and German Gott .

That's a eurocentric term that has its roots in Germany, across the channel.

If the English language was really

stable in short timespans

there wouldn't be in lawsuits over the interpretation of words in laws, contracts. There have been court cases that lasted years turning on the use of "or" in a provision.

@mk-pmb
Copy link

mk-pmb commented Apr 9, 2025

As a side note, what is that "MTW NTR" you were speaking of?

@csvan
Copy link

csvan commented Apr 9, 2025

@guest271314 dunno man, the inherent instability of the English language makes it impossible to understand you, I guess. From what I can gather, you were simply admitting to trolling. You could have been ranting about gods and contracts as well, but it's anyone’s guess at this point.

@guest271314
Copy link

@mk-pmb

As a side note, what is that "MTW NTR" you were speaking of?

MDW NTR. Ancient Africa. Kemet. KMT. Ancient Egypt. The speaker and listener supply their own vowels. They are not written. You have to know the specific context for intonation, which vowel to insert, or not. More than just words. As language is, invokes frequencies, or another way to perceive, mood; vibration. Can't really be put in to words, as R.A. Schwaller de Lubicz said in The Temple in Man https://archive.org/stream/thetempleinmanbyraschwaller1981/The_Temple_in_Man_by_RASchwaller_1981_djvu.txt after visiting the Temple at Luxor, and as foreign German, realizing the architecture was not arbitrary

Nothing is sensual for them; and this shocks our Western sense
of aesthetics. Everything is solely didactic, of an esoteric nature; it
is a teaching for the Understanding, for pure Intellect, a teaching
that cannot be described in explicit terms.

THE SYMBOL In our modern languages there is no word that
designates the exact meaning of Symbol, as it was conceived by the
Ancients. This is why I should like to replace the word symbol with
the word Medu-Neter, which conveys the "signs that bear the
Neters" ("Neter" signifying the Principle or the Idea in the
Platonic sense).

But notice still the vowels in how the German, while giving credit where due, write MTW NTR. Insatiable need for vowels in the English language. Well, you can't transliterate symbols in to words. You have to be taught what thought and energy they provoke, and exude.

See, in brief, https://scholarshare.temple.edu/server/api/core/bitstreams/7ae8c8e4-ebaa-49b8-bd5b-73e0d22dd26f/content.

Now, Thomas Young first, then Jean-François Champollion after him, neiter initiates into the Temple System claim to have "deciphered" the MTR MDW, on what they call the "Rosetta Stone". They never did. They can't. The sacred language was not written for them to understand, and again, African symbols cannot be converted to English words. There is no concept in the western, or English-speaking peoples' minds for the concepts and living of African culture.

@guest271314
Copy link

@csvan

@guest271314 dunno man, the inherent instability of the English language makes it impossible to understand you, I guess. From what I can gather, you were simply admitting to trolling. You could have been ranting about gods and contracts as well, but it's anyone’s guess at this point.

I'm just checkin' y'all out. If I was that in to CommonJS, I would just marshall everybody's resources and form a CommonJS collective, to reserve the module loading system, formally. Collect a list of all CommonJS packages, and so forth. The ship has sailed for saying esm is terrible. There's a lot of terrible technology, proposals, and implementations. You, the programmer, gets to program however you want. Or not. I think y'all got enough interested stakeholders in this thread alone to do your thing. So, take this paragraph for what it's worth.

One interesting note on this. Historically, I think the most widely used programming
linkages have come not from the programming language research committee, but rather
from people who build systems and wanted a language to help themselves.

  • A brief interview with Tcl creator John Ousterhout

@guest271314
Copy link

What I find useful in CommonJS as a core principle https://wiki.commonjs.org/wiki/CommonJS

  1. system: System Interface (stdin, stdout, stderr, &c) (1.0, amendments proposed)

@iambumblehead
Copy link

iambumblehead commented Apr 10, 2025

Module-import systems seem to be a primary target for injecting complexity into a language.

Writing a resolver for CJS is maybe ~2-3 hour task. Writing a resolver for ESM is far more complicated and requires a special logic to resolve package.json exports field. It is written that import trees load faster through CJS than ESM, however, from just a user-perspective, the simplicity of CJS is a notable improvement over ESM.

@guest271314
Copy link

Writing a resolver for ESM is far more complicated and requires a special logic to resolve package.json exports field.

Who said a package.json file is necessary at all? With import it's possible to import scripts from the network, for example GitHub in raw form. Using deno. Not using node. The Node.js loader system is not necessarily geared towards CommonJS or ECMAScript Modules. You can do whatever you want when you analyze the script.

@iambumblehead
Copy link

Many reasons give cause for one to resolve a module, relative another module, following the spec; that's why many packages use import-meta-resolve. For example, building a dag to find the dependency tree of a module or locating some files relative to modules in the import tree that are not imported but are used by some external system like a code editor.

Politely, did not find anything interesting in the gist.

@guest271314
Copy link

The gist is not intended to be interesting. The gist demostrates how to handle arbitrary specifiers, that is a "loader" system, for any kind of system you want to implement.

There's no package.json in deno. Unless you want one. there's no CommonJS, unless you set that with --unstable-detect-cjs. ECMAScript Modules with import and export from anywhere, local or Web. No node_modules by default, unless you set that with --node-modules-dir=auto. You can fetch all dependencies without executing anything, locally and from remote sources using deno install --entrypoint then specify a URL, multiple URL's, repositories on GitHub, whatever; written to a local vendor folder, or in ~/.cache/deno.

Using node CommonJS, ECMAScript Modules, and Microsoft TypeScript can be used and executed, respectively.

With bun CommonJS, ECMAScript Modules, Microsoft TypeScript, AssemblyScript, and WASM can all be executed directly. If you want you want you can execute C from JavaScript dynamically using the built in TinyCC. Bundle CommonJS, ECMAScript Modules, Microsoft TypeScript, JSX, to a single script or split scripts with bun build, or use the module loader system with Plugin, as demonstrated in the gist, to again, do whatever you want, for .cjs, .js, .ts files.

The gist being you can do whatever you want yo do using node or deno, or bun, with or without CommonJS or ECMAScript Modules, or Microsoft TypeScript.

So if there's in limitations in what you want to do and are not able to do right now it might be because your toolbox doesn't have the appropriate tools to do exactly what you want.

The loader system is just to get the resources there. Doesn't really matter how that's done. It's user choice all the way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment