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.

@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