ES Modules Are Terrible
gist.github.com
gist.github.com
What we gained is a native module system that works the same everywhere (Nodejs, browsers, deno, Bun) vs CommonJS which was really only built for Nodejs (with compatibility layers for others).
We have a specification for this system and any changes done to it have to follow the same process as any other change done to the language (for better or worse).
We have top-level async support that works across import boundaries. This is less useful on the server, but in the browser? With just a `import thing from "https://example.com/foo.js"` I get a fully intialised library even if does async requests as part of its initialisation.
What we lost is just this
``` const someInitializedModule = require("module-name")(someOptions); ```
The `app.use()` example can be replicated with an async `import()`. Maybe it's not as elegant as before.
There's going to be a lot pain of transitioning a big ecosystem like Javascript's to ESM but is that really a reason to not evolve the language?
This is indeed convenient if you want to quickly try a lib. However, what happens in a larger project with dozens of dependencies? Those http requests will get inefficient soon, and you'll be back having to compile everything with Babel or similar.
Plenty of websites are just server-side rendered and just need to sprinkle some client side JS. For them it's perfect to be able to drop in a script and not have to worry about bundling or polluting the `window` object while loading external scripts.
You're also now fighting for response time and bandwidth from a public resource you don't control. You are beholden to their traffic spikes, their downtime and their security incidents.
Just send it from your servers, or your edge nodes. They already have the DNS hit cached, they are already getting everything else from there. Chances are high you're sending image data that far exceeds the JS library anyway. This is especially prudent if you serve users in your own country, and that country isn't the US. Chances are very high your site's largest response delays are US CDN resources if you use them.
That said, tree-shaking can sometimes be a premature optimization if your site isn't a SPA with a comprehensive view of its tree to shake. Some MPA designs may still benefit from caching the whole ESM .min.js of a site-wide dependency and letting the browser tree-shake at runtime.
No, it won't any more: https://www.stefanjudis.com/notes/say-goodbye-to-resource-ca...
It was like an idea people had that this would be a cache benefit, but just theory not actually from real-world observation. I recall several people trying to do investigations to determine how often cache hits would happen in these cases, and finding it wasn't that often after all in real-world use. But I can't find em now, google is crowded out by people talking about what you link to above!
Standardised functionality, in this case, is better than needing tooling.
All of that said I'm super happy I moved away from mobile/frontend into backend. Way less annoyances.
>``` const someInitializedModule = require("module-name")(someOptions); ```
That can partially be done such as:
import {jason} from 'https://example.com/foo.js?name=jason
On the script site use: const url = new URL(import.meta.url); console.log(`${url.searchParams.get("name")`);
require("module-name")(someOptions)
is instead represented as import { someModule as someModuleFactory } from 'module-name';
const someInitializedModule = someModuleFactory(someOptions); (await import("module-name")).default(someOptions)
The biggest issue is import() is async and module proxies aren't allowed to be directly callable so you need to pick an export from the other module to call. But `default` is an appropriate export to call, even if there's no nice shorthand for default imports in the import() case (as there is syntax sugar in the import keyword case for default). Neither of those things seem like deal breakers to me, and have very good reasons for why they are the way they are.I've long dreaded touching the JS ecosystem but recently I published a package on NPM using ESM + esbuild to bundle it and it was--dare I say--pleasant. Top level await is a killer feature, too. Finally we get a real spec for modules instead of some ad-hoc thing that we twist and turn to get running on browsers.
Recently there is a movement in the JS ecosystem to rally around browser APIs, as you can see in edge computing services like Cloudflare Workers and Deno. Modules are a big part of that and one I am quite grateful for. Soon enough Babel will be thought of like Grunt and Gulp and all of the other JS churn.
Edit: originally I borrowed the "is terrible" language from the article but honestly I feel kind of bad about that. They're both projects worthy of respect, but I don't think they should remain as "the status quo".
- You don't get typescript to the browser without them
- You don't get next-generation language features right now without them
- You can't co-locate non-javascript assets with javascript modules via imports without them (e.g. import css or images from within the script that is using them) without them
- You don't get smart assets management (automatic code splitting, hashing, etc.) without them
That said, I much prefer tools other than babel and webpack myself. Parcel, Vite, etc. are much nicer to work with, and faster.
In other words, I don't think bundlers are evil, and bundlers can deal with ES Modules just fine if they want to. I will admit that I have a minority philosophy in that I try to minimize my dependency tree for JS projects as much as possible, so maybe some Webpack features are unnecessary for me.
With everything in ES Modules I often don't see much need for other than the Typescript compiler alone (just need type stripping), and when I do need a tool for spot-bundling a library here or there I find esbuild fine to work directly against without the "swiss army knife" weight of Vite sitting on top of it.
For me "generations" are a lot more about how they approach things than how much they implement. Esbuild kicked off a new generation of fast tooling, and Vite builds on it and integrates other tooling with a shared and stable API, without falling into the same pitfalls Webpack etc. did.
The "pitfalls" of Webpack et al, as I've come to see them included that "everything is nearly always bundled and the bundle should always support everything and the kitchen sink" mentality that Vite inherited pretty directly from Webpack. (API instability and speed were rarely my worst problems with Webpack. Obviously everyone's mileage has varied here.) It's the "kitchen sink approach" that I find a useful marker for "last generation" and why I find it useful to lump Vite into that group.
Meanwhile, esbuild is unequivocally a different thing than Webpack. (If it is a different thing than Parcel or Rollup, other than implementation language and speed, is a different conversation, of course.)
But yeah, I don't expect you to agree with me either, and I'm not exactly trying to change your mind, just offering a perspective as someone that looks at Vite and neither sees a need to upgrade legacy Webpack configs to Vite nor to use Vite where I'd just use esbuild directly without the ceremony or the extra wrappers or the API I don't need/use or the extra "tools" I don't see a need for post-Webpack/post-ESM Everywhere.
That's the thing - I don't believe this is true. I can honestly say that I've used most Vite features at some point. I love playing around with silly and over-complicated ideas, and with Vite there is pretty much always a feature that allows me to implement exactly what I want, or if there isn't, there is a plugin! This wasn't the case for me with Webpack. Let me show you a simple example: glob imports[1] are probably not something you think should be part of a "next generation" bundler, but when I tried implementing a feature based on the same requirement with Webpack, I simply couldn't! With Vite it just works out of the box, in all situations, without me having to manually implement it. It allowed me to implement a code browser that contains its own source code through glob imports in roughly 20 minutes (5 for trying it out, and 15 for implementing a web worker to keep performance up). Because Vite also makes web workers and everything else extremely simple.
And this goes for most features of Vite - they are features that would be a nightmare to pull together with plugins and the like, which I can simply enable in a wonderful config file.
> Meanwhile, esbuild is unequivocally a different thing than Webpack. (If it is a different thing than Parcel or Rollup, other than implementation language and speed, is a different conversation, of course.)
That's kind of my point, esbuild doesn't do anything really new. It just does what it does extremely well, same as Vite.
> That's kind of my point, esbuild doesn't do anything really new. It just does what it does extremely well, same as Vite.
Ah, yeah, if you want to argue that there is no "new generation" at all yet in frontend tooling, then I can agree with that. To some extent with ESM Everywhere and trusting things like HTTP/2+ the nice thing is we're very close to a revenge of "no tools" is the "new generation of tools", which I think is great.
> Ah, yeah, if you want to argue that there is no "new generation" at all yet in frontend tooling, then I can agree with that.
But I don't want to do that. For me, the "new generation" of frontend tooling is characterized by great UX and speed, which puts Vite and esbuild in the same new generation. :)
In the most literal sense of course you can: Typescript has its own compiler!
But more broadly it is a bummer that you need a build tool to use TypeScript. Fingers crossed the “Type annotations as comments” proposal happens: https://github.com/tc39/proposal-type-annotations
I think we're both just used to one thing over the other.
from os inport listdir
After you typed `from os`, IDE could suggest signature names based on the module you are importing.> Which then makes people believe that treeshaking is not possible with CommonJS modules. Well, it is - Rollup just chose not to support it.
It is possible with CommonJS but it’s enormously brittle and unintuitive. I can write:
module.exports = { blah: 1 }
module.exports.blah = 1
let name = “blah”;
module.exports[name] = 1
And countless other possibilities. Then on the other side: const { blah } = require(“module”)
const blah = require(“module”).blah;
const mod = require(“blah”);
mod.blah;
let key = value ? “blah” : “something else”;
mod[blah];
Footguns all over the place that could easily ruin your treeshaking and result in unnecessarily large bundles.> 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
Also plainly untrue. Similar to the above, I can do:
let name = “module”;
const mod = require(name);
With even a slight bit of added complication in that logic static analysis becomes enormously difficult. ESM does not let you do any of that.> loading a full dependency tree over the network would be unreasonably and unavoidably slow
Allow me to introduce you to HTTP2. It’s also fast to load dependencies because ESM is statically analysable in a way CommonJS isn’t, so you don’t actually have to evaluate the JS. No, it won’t be as fast as one request but the ability to just write a file and immediately upload it is tremendously liberating, IMO. For small projects Webpack, Vite or whatever is tremendous overkill.
> There are no actual advantages to it. At all.
Igorance is bliss, I guess.
It's been too many years since I seriously looked at the problem, so my memory is fuzzy at best. But I do remember coming away thinking that it's a lot less of a magical or useful feature than I'd originally expected it to be.
> How are polyfills done in a pure ESM world?
Maybe you could give an example? In my experience you’d just
import “polyfill-module”;
At the top of the file. But I suspect you have something specific in mind.You can still `await import(someVariable)` and your tree-shaking will be limited at that point without a lot more manual configuration. But import syntax makes that an edge case, not the base case.
> You still need a build step to get value out of tree shaking, no?
ESM was also designed so that browsers can tree-shake at runtime. (Whether they will or not is an implementation detail based on whatever optimizations the browser's engine decides to make, of course.) Imports to modules that are never referenced by URL are never loaded, for one obvious part, that's the most basic form of tree-shaking in general, the browser never has a full view of your file system tree.
Beyond that, the module objects that `await import()` most directly returns (but are used to back all imports and are also represented as `something` in `import * as something from 'module'`) are designed to be "immutable" proxies that the Browsers can store as weak-references until needed and can in turn store the exports of that module themselves as weak-references. If the reference is never dereferenced it stands possible to be garbage collected like anything else. The browser might not have parsed anything beyond the top-level of a module and the name of the export, delaying paying any attention at all to the definition itself of that export until dereferenced. (It delays even parsing in some optimization cases; any JIT compilation is obviously delayed further until after parsing.) An export that was never parsed/compiled and eventually garbage collected away as the app ran is a very close equivalent to tree-shaking from an optimization standpoint (of nearly all but raw bandwidth/memory usage). The garbage collection churn isn't nearly as optimized as build-time treeshaking can be, but most engines have heavily optimized garbage collectors and there are worse garbage collection churn in the average application than that.
Relying on the browser to do most of your tree-shaking for you at runtime is maybe not the best idea, but ESM was certainly designed that browsers could optimize it as much as possible. There are still runtime benefits/optimizations to expect from ESM that CommonJS was never designed to support.
ESM specifically doesn't ignore the edge case, it makes a special syntax for it!
The point is that require() is surrounded by footguns. It's a magic function with untold side effects. That's bad. If an inexperienced developer sees this:
const mod = require('module');
they might think it's fine to change it to: const moduleName = 'module';
const mod = require(moduleName);
because why not? Or why not move it from the top level? There's nothing obvious about require() that would inform you you're about to cause a giant side effect. Indeed, on Node you wouldn't be. But you would be on a client-side build. This is the problem with non-standard implementations.Switching from a top-level import to using import() has a drastic effect on your code: import() is async, requires you to wait on a promise to resolve, etc. etc. It's obvious that making the switch alters the structure of your code and the way it loads.
I've worked in this space and I've heard all of the argument
> [require] is a magic function with untold side effects
Sure, how about making it standard with a new directive? "use require" or something like that. Now require is special. You can't redefine it etc
Let’s say we standardise around require only being used with static strings. Now we’re in a situation where every JavaScript function can be called with a variable as an argument except this special function called “require”, which can only receive strings. There’s no concept of that in the JS language.
What we’re doing is creating a new concept. It makes sense, looking to the future, to create new syntax for it.
Web components were supposed to make it easier to share components between frameworks. But from what I can tell, this never fully materialized because the tools provided by web components are insufficient and people end up having to import additional libraries. Despite web components being around, most developers continue on with React and friends.
With ES modules cascading loads I think there was a lot of handwaving done by claiming you would eventually use HTTP2 to push the full asset graph to the client. Did that ever materialize? What tools support that? Generating this asset graph sure sounds like a build step, and since it's possible to have dynamic imports you also have to be prepared to handle those somehow.
Adding async support to CommonJS might've been easier than migrating to ES modules. I know that Webpack had support for that at some point, although it wasn't widely supported in other ecosystems.
A lot of interesting features of the module system are considered out of band and have barely been developed and explored, since it's not part of ECMA Script. Module import maps and the like are starting to get supported by browsers. Generating those probably requires a build step too.
[0] - https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/103
Both approaches would still require the server to have some knowledge about the semantic request/response structure of client applications, though.
Web components do make it possible to share components between projects. See, e.g. Nordhealth, whose design system are build as web components with Lit, but whose big framework is Nuxt.
> Despite web components being around, most developers continue on with React and friends.
Most developers are taught React when they start; and do not learn the web platform properly.
It just feels like a bait and switch when platform developers continue claiming things which never fully materialize. Maybe part of the blame goes to me as the reader for making assumptions. I've read through the HTML spec and try to keep up with web platform updates. That doesn't change the fact that React and friends _is_ the de facto web platform for tons of developers.
I agree. And I don't believe that you have to import a giant framework either. Web components today, especially with a bit of extra convenience that comes, for example, from Lit, are now where React was around 2015, before all the big frameworks came about, and before React changed its api from the one based on component's life cycle to the one based on data dependencies.
What's the benefit is unclear. Maybe we don't need a build step anymore for the browser? But not really - for any project of a significant size, sooner or later a build step will be needed for this or something else. Not to mention that many projects are now done in TypeScript, which means there's a build step anyway.
For me it sounds like an influential npm developer got this idea that we should avoid a Python 3 situation and decided to force everybody right now to switch to ESM. He maintains enough npm packages to be able to force this and he did. Now we are in this situation where numerous codebases are stuck with certain pre-ESM packages, and there's very little we can do about it.
https://github.com/sveltejs/svelte/pull/8569
> As a Svelte compiler developer, debugging without a build step greatly simplifies compiler development. Previously, debugging was complicated by the fact that we had to debug using the build step. In addition, using JSDoc does not affect compiler’s development safety because the type is almost equivalent to TS.
Deno also switched away from using build-requiring TS
Neither of these projects are trying to pipe thousands of files to a browser, which is what OP was talking about, I suspect. HTTP2 is better than 1 in this regard but not good enough that most people abandoned bundling.
Not true, I have developed 3 systems with plain ESMs loading them in the browser with no build step (> 100K lines of code for each project, too many modules to count :)
Update: I bundle for release
Since then I'm yet to see ES modules solve real problems that CommonJS couldn't as the OP argues.
The rest of his complaints are about how `import`s are static, whereas dynamic `import()`s are async; and he doesn't like that. To which I say, "meh".
That's not fair. Python's 2-to-3 transition is JavaScript's every other Wednesday.
(yes that's a hyperbole, but the churn in the JS ecosystem really is enormous compared to any other programming language).
The problem is how it was introduced to the ecosystem with basically no official support until very recently.
First draft in 2009, official release was 2017, a very long time in the Javascript world. Yet, Browsers have minimal support for it; Node.js supported fully in 2020 (node 14, backported to 12); Typescript finally became usable in 2022 (v 4.7); etc.
On top of that, some developers arbitrarily pushed this switch to ESM with their very famous lib (eg: sindresorhus) which created a lot of frustration and forks, further more deteriorating the public opinion for ESM.
I recently created a new nodejs project in 2023, no personal issue with the modules themselves or the syntax, but still countless of issues with the tooling.
I always found this to be an anti-pattern that I tried to avoid, it felt way too “cute”.
I won’t pretend the switch to ESM has been completely smooth but it does feel like the better way to pull in dependencies. It reads better IMHO and makes more sense to me.
This is not true if you use a CDN like esm.sh[1] or skypack[2].
[1]: https://esm.sh/
>import {URLSearchParams} from "url"; >const {URLSearchParams} = require("url");
or different destructuring from regular javascript
>import {URLSearchParams as P} from 'url'; >const {P: URLSearchParams} = require("url");
also you could have used await with require, it would be intuitive for developers as they likely have come across the same syntax before.
> const lib = await require("lib")
I don't see any argument why the require approach couldn't have been implemented in the browser. What is the actual gain from using ESModules syntax?
Similar to that is exports: the export syntax requires you to give static names for exports in a module. There is no `export [someVariable]` that can give a dynamic export name. The closest is `export default` which the import syntax treats as unnamed (and `(await import('module-name'))` calls `.default`). In CommonJS `module.exports[someVariable] = something` and worse `module.exports = { /* entirely dynamic object */ }` were extremely common.
That sort of import/export static analysis is used for JIT compiler optimizations at run time in the browsers. That sort of import/export static analysis is used for safe tree-shaking at build time. (Yes, you can treeshake CommonJS, but it is dangerous and makes few guarantees that the final bundle is consistent/correct.)
Yes, you can still dynamically `await import(someVariable)` but browsers don't optimize that in the same way (and bundlers don't treeshake that in the same way). That doesn't mean that it isn't very useful that the base case better supports static analysis and compiler optimizations and tree-shaking.
Which of the issues raised in the article are actually actionable at this point?
Due to the priorities of the web ecosystem, no breaking changes to ES modules are possible. Can any of the issues raised in the post be addressed in non-breaking fashion?
I see libraries providing dist files for both and node refusing to run if you don't name your file one way or the other and it seems like a fight over something without distinction.
As ridiculous as having to decide if you can only use while loops or for loops. Support both.
But the current state of the JS industry where everyone is forced to supporting both ES Modules and CommonJS is hellish. It is the worst of all worlds. It increases testing surface area, slows down build times, adds complexity.
I have wasted many days of time on tooling issues related to this and I am a competent developer.
At this point we are too far down the ES Modules path, so let's just commit to it and get this transition over. CommonJS has lost the argument for all practical purposes a long time ago.
Could we please forcibly deprecate CommonJS? Lets please only pick one, no more supporting both because they we do not get the benefits of either.
[1] https://nodejs.org/api/esm.html#differences-between-es-modul...
> I have no idea how ESM syntax in my apps works and I hope that I'll never know. It just works.
Once you manage a big enough application, you will know because shit shine through a lot. I spend countless hours to work around ESM issues at work (1mil LoC).
https://devblogs.microsoft.com/typescript/announcing-typescr...
Before that it was not possible to write ESM that would work in node.js or browser.
AMD require RequireJS runtime to work. It is not browser native technology. It is also not ESM. AMD as module systems is dead.
> Typescript's CommonJS resolution was always fine in Node for ESM when "type": "module"
It was not fine, when you use "type": "module" node.js expect extensions in imports. CommonJS resolution is not compatible with node.js ESM implementation. There is plenty of differences.
> 4.7's new resolution modes/tweaks added support for "mixed-type" packages
It added support for node.js ESM module resolution, making it possible to write ESM that would work under "type": "module" packages.
Just mention that mixed packages are not possible, you cannot require() ESM module and
> which again were a Node-specific thing, and "idempotent" browser ESM where Typescript does (nearly) no transformations other than type stripping, which is a nice-to-have but not entirely necessary to running in a browser.
In practical terms, any JS project needs to be compatible with node.js. If you want to use ESM it needs to be node.js variant. If you try to use anything other, your tooling would break.
I said the "classic" resolution was built for AMD, but that it worked just fine for browser-intended ESM. I didn't say to use AMD, I said that ESM has worked for a long time in Typescript, in part because the "classic" resolution has always worked in browsers (whether transpiling to AMD, UMD, or ESM). Maybe you should read my comment again?
> It was not fine, when you use "type": "module" node.js expect extensions in imports.
Typescript has always included extensions in the output of imports. When you used a bare TS file import without an extension it added a .js extension in the transpiled output. .js file extensions have always worked in Typescript (going back to 0.x days). TS 4.7 added support for .cjs and .mjs, that's the new thing.
Versions of Typescript have been just fine with ESM output since 1.x somewhere with the right configuration. You don't need 4.7 to do ESM in Typescript. It helps with some nice-to-haves in Node if for some reason you are trapped into support both .cjs and .mjs files side-by-side in the same library and need all of those to be Typescript, but you don't need it for anything else. Sure, Node compatibility can be important (it just isn't important to Browsers).
There's (luckily) no such thing as an "ESM variant" of Node and browsers will never have to know anything about the .mjs file extension. (Web servers will to make sure the present the right mime type, but that's a separate issue.) They just need ESM. I stand by accusing Node of doing the silly thing by adding the .cjs and .mjs file extensions. (I understand why they did it and mixed-module libraries were a thing that needed to exist until LTS support for ESM was well adopted, but we're past that transition phase, it wasn't that long of a transition phase, and in hindsight I think it still feels a little silly. Useful, but silly.)
(ETA: I did AMD in Typescript a long time ago. I've done ESM in one form or another in Typescript for many years before TS 4.7. TS 4.7 isn't needed to do ESM in Typescript. I know this from plenty of past experience.)
It was not. https://github.com/microsoft/TypeScript/issues/16577 There was proposal and PR, but It was rejected. When writing ESM in typescript, you need to import with .js extension.
>There's (luckily) no such thing as an "ESM variant" of Node and browsers will never have to know anything about the .mjs file extension. (Web servers will to make sure the present the right mime type, but that's a separate issue.) They just need ESM. I stand by accusing Node of doing the silly thing by adding the .cjs and .mjs file extensions. (I understand why they did it and mixed-module libraries were a thing that needed to exist until LTS support for ESM was well adopted, but we're past that transition phase, it wasn't that long of a transition phase, and in hindsight I think it still feels a little silly. Useful, but silly.)
There is absolutely such thing. Spec is either intentionally not being followed (babel) or it allows for host implementation to define some things (node.js, deno).
> but we're past that transition phase,
We are not. 99% of new projects use ESM with CommonJS resolution today!. Most codebases have no way to transition to ESM. It was a Herculean effort for typescript codebase to be migrated into ESM https://devblogs.microsoft.com/typescript/typescripts-migrat... in March this year!.
Which could be a legitimate annoyance if they really offer no advantages, I don't know. Having two competing systems is definitely worse than having one standard.
Am I missing anything, or is this it?
Leaving the module system undefined and unstandardised was going to be bad for JS. There is no future proofing at all.
With standardisation comes strong guarantees of future proofing.
It seems obvious to me too that leaving the module system not officially spec'd as a JS standard would be terrible to JS... but maybe not to OP? Or they think "exactly like what CommonJS currently is" could and should have been that standard?
(I'm guessing there are reasons people, at least at the time, thought CommonJS wouldn't have worked as the standard, or needed to be improved upon and it was worth it to do so?)
I wish the OP actually covered some of this stuff. Without it, not being an expert, I'm having trouble following what exactly their critique is, although I understand they wish they didn't have to deal with both CommonJS and ES Modules... but yeah, we clearly needed a JS standard, right? Or not?
CommonJS cannot run in the browser unbundled. Eventually the Node ecosystem built a lot of good bundlers that fake it well enough to kill AMD (and UMD) modules, but before all of that AMD (and UMD, which was basically CommonJS in an AMD wrapper that could itself run as CommonJS, and exactly the turducken that sounds like as a design pattern) was built to solve real browser runtime problems that CommonJS ignored or never solved: browsers are inherently asynchronous (downloading a JS file from a URL takes wall clock time more often than not); browsers don't have any view of a server's file system beyond the basic HTTP verbs on previously-formed URLs and to query for files even if it isn't to download them is still a slow discovery process; etc and so forth.
It was good riddance to AMD (and UMD) when bundlers killed them, but bundlers in turn gave too much of an impression that CommonJS was "sufficient" as people forgot the lessons of AMD modules. It kind of was sufficient, for a time, if you didn't mind browser code being trapped in bundlers and build processes for presumably the rest of time.
ESM took the lessons of AMD and applied nice syntax to them (writing AMD modules by hand was an awful experience, from personal experience; it led me to adopting Typescript 0.x in a past life before Typescript was even officially "stable") with further advantages that even AMD didn't support. ESM is async-always (and tree shakeable) and makes no assumptions about server file system trees.
Good riddance to AMD and UMD. I can't wait for CommonJS to go the way of that dodo and finally wish good riddance to "mandatory" bundlers for CommonJS dependencies, too. I'm glad so many frontend developers never experienced the pains of AMD that they've almost collectively lost the lessons of AMD already. I can't wait for CommonJS to similarly feel like a bad memory of an old nightmare.
It works rather well, it's not confusing at all and if you really want something else you can use require etc.
Coming from other languages ES modules seemed much more familiar.
Ecosystem divide? What would be the point of ESM if it did not do this? We could have kept using `require`s if the bar is that the new, language-native module system must not cause any sort of a transition period where people adopt it and displaces the previous ad-hoc (IMO anti-) patterns that were used for "modules" before ESM.
Doesn't work everywhere yet? The support is growing and developers IMO should learn about and default to ESM. That's how the support and availability of packages will continue to grow to eventually 99.9 displace previous methods.
> 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.
I am not convinced the people referred to here are real humans and not made up to support the author's argument.
Babel's problems are Babel's problems, ESM is not the cause of the problem here.
> ESM in browsers without a build step!", apparently not realizing that that is an utterly useless feature
A hands down amazing feature, something I was pining for for so long and it is finally here. Performance? Sure, if you are building high traffic sites, you will still reach for a bundler, but browser ESM without a build step will allow many intranet and low traffic Internet development teams to ship much nicer code than they do now after buying into bundle-everytime voodoo. If this existed before WebPack, so much spaghetti code could have been avoided.
The point of static analysis I can't comment on as I have never authored any tools for static analysis of JavaScript codebases using modules, but if I were to, I would be very happy to have an actual language feature for modules with a spec at hand rather than a bunch of IMO ugly patterns that only existed out of necessity because the module system at language level took long to arrive.
> CommonJS is so ubiquitous in Javascript-land nowadays that it will never fully go away
Unfortunately true, but that doesn't mean we shouldn't strive for something better. I know the author used the word standard and it can be interpreted many ways, but ESM is IMO better because it is a feature of the language, not the ecosystem.
> 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.
But browsers supporting ESM directly is somehow a problem? It's better to rely on bundlers for everything even at the cost of them doing stupid shit like this by default? Not to mention this is completely configurable and avoidable with minimal amount of effort.
> the following pattern is simply not possible with ESM
That's a good riddance from me! ESM being statement keywords and not expression keywords (aside from dynamic imports) is IMO the right choice.
> do you want to be able to use ESM-only dependencies
Yes, please.
> You've destroyed a successful userland specification
I am happy to have contributed to proliferation of a successful kernel specification if we're sticking with this metaphor.
Before or around the same time CommonJS appeared there was another module system before Webpack existed, I think it was called Async JS. It was browser-first and allowed you to do JS development in the manner which you describe. It was also supported by Webpack's bundler. A bit surprising to see this piece of history getting memory holed.
Edit: I think it was called RequireJS? You would give it an array of dependency paths and a callback function to get executed once everything was loaded.
So doing stuff like `var foo` wouldn't affect the global scope, since your code was always supposed to be placed inside a function call which receives all dependencies as a parameter.
Bundlers made people forget the pains but also the useful lessons of AMD modules.
I do not understand how this should even remotely be possible. It is a de-facto standard now and used in moste Javascript projects.
Require semantics are bad, and it's even worse for a browser. The problem is that the ecosystem is of low quality, and that people bandwagon behind node require compatibility whereas is was a dead end for browsers (no way to implement it with builtin api, processors everywhere became mandatory).
Rant author, really, you are the problem. ES module could be a solution.
The fact that Babel is often used to transpile ES modules to CommonJS says nothing about whether ES modules are terrible; it mostly speaks to the state of the Node.js community, and perhaps the state of bundling applications for the web. It's possible to use pure ES modules today in Node.js, Deno, and in the browser; it just may mean that whatever Node.js package you want to use won't make that easy (without something like JSPM.io).
As far as lacking advantages over CommonJS, I don't know what he's talking about either. ESM is much more practical to statically analyze given that it's a language feature and not a function that just happens to be in the global scope. If ESM was more of a copy of CommonJS, the author would probably be complaining that it doesn't support static analysis or tree shaking.
Those aside, the use of the word "terrible" comes across as insincere. ES modules are not complicated. They have their own scope and they can have exports that can be imported elsewhere. If there's any other detail or behavior I didn't mention, my guess is that it's barely important. In my experience, modules work quite well and are an essential language feature. The fact that they have their own scope is particularly nice given that there are many other languages that don't give their modules their own scope by default.
The author wants import syntax to both support static analysis and call it as a function within any JavaScript expression, yet they provide no example of a hypothetical syntax that would make this possible. Those needs are necessarily in conflict. In other words, the author wants to eat their cake and have it too. As they point out, ESM does provide an import function that can be called this way, but the author is annoyed that it's async and may require parentheses (which they believe is a "weird hack" for some reason).
Making the `import` function async is a good design choice. In the browser, which is JavaScript's natural habitat, importing is necessarily async because other modules have to be fetched as separate files via HTTP. If it weren't async, we'd be stuck bundling "modules" together with Webpack, which goes against what the author actually seems to want, which is actual modules. In runtimes like Deno, being able to asynchronously import from URLs on the backend is pretty nice as well. CommonJS doesn't support this without something more akin to a "hack" than the parentheses "hack" the author complains about.
Also, there's some very foolish comments in that Gist. For one, you can't blame ESM because of Typescript. ES doesn't give a hoot about Typescript, nor should it. ES modules "[don't] work in the browser" because of performance? Nothing is stopping you from bundling parts of your application, ESM or not. It's as if http3+ isn't a thing either. In fairness, most JavaScript applications rely far too heavily on a bajillion 3rd party packages, and the answer is we should stop doing this as often or to such degrees.
But does it really matter?
I’m solving problems for today that hopefully work tomorrow because who knows what can happen next week. Money could run out and then it doesn’t even matter lol.
> 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.
Factually and speculatively incorrect.
I think all the major JS runtimes support it, and because it is standardised, those that won't in the future either probably are not being maintained anyway (and so won't be used in the future) or will level-up and add it in.
> 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
Not all browser code is a huge mess of spaghetti; sometimes you really just do want to use just one library to make your wordle game (or whatever). Even with a large deptree, CDN's and the browser itself can easily help reduce downloads by using cached files.
> And then, people go "but now we at least have a standard module system!", apparently not realizing that CommonJS was literally that,
No, it wasn't. It was not standardised. It was never something that you could assume was already in your JS implementation. Author also doesn't know what the meaning of "literally" is.
> 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.
I doubt it. Non-standard things almost always lose out to the standardised equivalent. The writing is on the wall for any and all ES module equivalents.
> 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.
And? At some point if you want to stop using some non-standardised extra tooling (switch to a babel competitor, for example) then using standardised constructs is much more future proof than directly spitting out CommonJS.
> 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.
I dunno who this guy is; maybe someone important in the JS world? Even if he is, though, this is an awfully arrogant viewpoint. ES modules have already happened, are already supported almost everywhere and works as intended.
Just what reason should there be for deprecating it completely? Just because one author is salty that he doesn't get to use his favourite library?
C'mon - we all have to move on when our favourite thing goes away. It's the way of the world, and having been programing professionally since the mid 90s, I too have had to let go of what I considered fantastic tech in favour of what I considered inferior tech.
Isn't that the case for commonjs et al for similar situations? And wouldn't the solutions for commonjs also apply to ESM?
> and even then require weird hacks with parentheses to make it work correctly.
Operator/et-al precedence is something you'll fight regardless, no? Its annoying, yes, but that doesn't make it a "hack", its a oft well understood system.