As of now, I still don't know what we got from this switch to ESM. It's a terrible breaking change which in some cases make it impossible to upgrade packages, unless you convert you whole codebase first which in many cases is not possible.
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.
> 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.
> 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
Funny, since both svelte and deno are runtimes that require compile steps for user code before passing the code to the JS engine (svelte itself is a compiler, deno still has to compile typescript for V8 to run).
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.
Even if you use Typescript to build your .ts into .js, in a case of very small scripts in Node.js you now also cannot use new-er versions of your dependencies because one of the dependencies happened to move from CommonJS to ESM. Every short script you ever develop will require ALL of its dependencies be either one, or the other, or the third option - which is setup a hugely complex bundler system to somehow make sense of what to do with the different types of code by rewriting it.
It's python 2 to python 3 transition all over again, only this time it's in javascript. Which is most of what the complaints in that gist are — i.e. how much the transitional state that we are in currently sucks.
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".
Oh my god have I paid my dues in this transition. I have lost days of developer time trying to support tooling that compiles into both ES Modules and CommonJS while also supporting TypeScript and tree shaking and what not. It brutally complex and the testing surface area is large with all the possible combinations.
I don't think it's hyperbole -- I think the changes are often as large and collectively more significant than 2-to-3, but the ecosystem is better at change. Even with that there are clearly factions and currents in different directions, but things generally work.
Does anyone know if there are any records remaining about TC39's thought process from when they standardized the syntax of the javascript module system?
All the meeting notes are recorded here: https://github.com/tc39/notes/tree/main/meetings. You’ll have to do a bit of spelunking in the corresponding agendas and proposal repo to narrow down the exact meetings you’re interested in.
> The only way around this is with `await import`, which would break the supposed static analyzer benefits,
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.
> 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.
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.
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.
> 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.
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.
I agree, bundling isn't going anywhere for sure, but the web isn't all single-page-apps built with perfect engineering and top-notch frameworks.
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.
If I’m serious about shipping some production app/page, I want complete control over all of this anyways. I never want to require/import an entire .min.js file. I want to tree shake the 15% of it I need.
This is a myth in production environments I feel. It's a security risk to import the library on the fly from a source you don't control, and the caching is per user, so you are banking on each specific user having visited a site that happens to have the same version of some library you use from the same domain that you included it from, so that it's cached.
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.
Privacy concerns led to browsers caching per-user and per-site, so there is even less advantage to "shared CDNs" in 2023's browsers.
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.
And it was always pretty minimal benefit. Depended on the exact same version of the library being cached from the same CDN... in the days of jquery hegemony, maybe jquery cache hits could be a thing, but even that was probably minimal. These days JS usage is much more diverse.
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!
Due to security/privacy concerns, browser caching is now scoped to the origin of the website loading the content, so linking to popular libraries from CDNs provides no caching benefits when loading your site for the first time.
I also disagree with the article, I happen to find ESM modules very useful and although I've had to work around some of the ways I would have previously done things, mostly they weren't good best-practices in the first place.
Really? I remember using the commonjs module pattern even before nodejs. It basically just felt like it was a way to write javascript to a standard of quality more than anything in those times. Lots of closure enforcement, and everything wrapped in a top-level function - The module imports were just a side effect. "require" is just a top-level first-class function call. I believe I was using it to build crossplatform mobile applications at one point, and there was absolutely no nodejs involved in that.
All of that said I'm super happy I moved away from mobile/frontend into backend. Way less annoyances.
My bad, my past experience/bias got the better of me here. I only got exposed to CommonJS as a part of Nodejs but it does look like CommonJS was started independently of Node.
yes, RingoJS is also using CommonJS, but yeah CommonJS was mainly for Node. However, now we have standard from import that is part of ES standard. Would rather be using that now that most runtime and browser has implemented it.
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.
Bait and switch tactics seem really common in the web space.
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.
The intended purpose of both Server Push and Early Hints is basically "Lower overall latency by letting the client know early on that there is more data it might need in subsequent requests." But in practice Early Hints are much better because only the client knows definitively if it needs the resource or not i.e. a 103 might say "you might need this file soon" but you already have it in cache.
Both approaches would still require the server to have some knowledge about the semantic request/response structure of client applications, though.
> 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.
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.
I remember a big selling point for web components was that it wouldn't matter which framework you were using because they would just give you a common interface. But this isn't that useful if you still have to import some giant framework as a dependency, maybe the main exception to this is something like Svelte. The claim certainly feels like it has been downsized over time. If I'm already using the same framework between projects I can share components without web components.
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.
> But this isn't that useful if you still have to import some giant framework as a dependency
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.
One major thing that has held back Web Components, IMO, is that the standard never provided a JSX equivalent: a convenient declarative, condensed way to express your component's structure along with event handlers. So if you choose to build web components, unless you want to have a terrible developer experience, you're still going to reach for a library anyways, like Lit (the successor to Polymer). Only unlike using popular libraries like React/Vue/Angular, the ecosystem, community and resources at your disposal will be much smaller.
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.
Because millions of mature packages and libraries already use CommonJS. If we have to rebuild the JavaScript ecosystem from the ground up we might as well do it in a different language.
I recently built react app and everything worked with ESM. Bump major version and that's about it. If someone need to work with old dependencies, they have it.
In today's supported Node (works just fine in LTS) just "type": "module" in package.json (and learn "exports" for ESM only), never use anything but ESM, and everything mostly "just works" with no CommonJS in sight within your library (other than maybe .cjs config files for slower build/dev time tools like Jest).
You cannot, 99% of real life applications is ususing it. Jest only support esm as experimental, typecript added support very recently. Most people use sytax but modules are transpilled to commonjs.
If both library and consumer use esm syntax, why does it matter what code below it's translated to? As long as ESM semantics is preserved, that's just an implementation detail. I have no idea how ESM syntax in my apps works and I hope that I'll never know. It just works.
It matters for things like testing, module resolution and typescript. ESM runtimes are different, most people use ESM syntax with commonjs module resolution (index files, extensionless imports, babel) but this is not compatible with node.js ESM (lack of index files, required file extension) [1]
> 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).
Typescript has just about always supported ESM. (Since way back in 1.x somewhere. I'm too lazy to look up exactly where the line is that you get good ESM modules, but it was very early.) Typescript took a long time supporting Node's wacky multi-file-extension hijinks, but that's mostly on Node. You can make a package.json file with "type": "module" in current Node and use just about any ancient version of Typescript you want to output ESM.
CommonJS ("node") resolution was always opt-in in Typescript. Typescript's "classic" resolution, built for AMD, was perfectly capable of working in the browser. Typescript's CommonJS resolution was always fine in Node for ESM when "type": "module". 4.7's new resolution modes/tweaks added support for "mixed-type" packages with plenty of non-.js file extensions, 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. ("Classic" transformations that assumed ".js" file extensions worked just fine for the browser and work just fine for Node when "type": "module".)
> Typescript's "classic" resolution, built for AMD, was perfectly capable of working in the 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.)
> 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).
>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!.
Sometimes I feel like I am the only human on Earth who likes ES Modules end to end, 100 %.
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.
> 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.
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.
I have not heard of it but I think the main benefit of ESM over a bunch of `script` tags (what I used before CJS came about) is that the individual modules files are isolated and do not share globals which was a common pain point when using `script` tag soup and something that IMO drove a lot of people to use bundlers to avoid this who did not benefit from bundling otherwise (intranet web apps and low traffic Internet apps - their debugability went downhill and stayed there for a long time and even with the introduction of source maps and such, those weren't supported right away in all tooling). If the thing you describe solved this back then I regret not having come across it.
It's been a really long time since I last used this tool, but I think it always executed asynchronously within a callback function.
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.
AMD modules (the name of the types of modules that RequireJS and other loaders, there were multiple competitors) were very much a thing. (Still are in the ArcGIS world that bet heavily on ancient framework Dojo and will take years still to migrate to ESM. Though luckily for them they get to skip CommonJS altogether.)
Bundlers made people forget the pains but also the useful lessons of AMD modules.
Imagine being so full of yourself that you write that last paragraph with a straight face. There's obviously no turning back and yet this guy thinks his opinion is so important that it may change the course of things.
The lack of general knowledge in this rant is incredible.
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.
Many interesting and fair points in the article. Given perfect hindsight, perhaps the ES Module specification would look quite different today. It might more closely resemble CommonJS and prioritized compatibility with it. But that ship seems to have sailed. The ES module specification has been build into every JS runtime of note.
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?
> 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.
This is not true if you use a CDN like esm.sh[1] or skypack[2].
I really don't get the point the author is trying to make.
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.
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".
> Counterpoint: Babel and Webpack are terrible, actually.
- 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
Ok, I thought OP was expressing a dissatisfaction with the fact that there are build tools in general, and "webpack and babel" were just an example of such tools. In which case esbuild or vite would be essentially the same, but just faster (and probably less flexible, because flexibility is traded for speed). I didn't realise that OP's complaint was strictly about these two tools.
I feel like esbuild covers everything I need. I haven't tried the Rust-based ones but they look promising as well. esbuild can deal with Typescript, embed WASM binaries that are importable, do minification, etc. Yes, it's true that Babel will let you use more bleeding edge features but I'm OK with waiting for those to stabilize.
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.
You can do all of that with Vite. At this point I see Webpack/Babel as "last generation". You still see them everywhere but there are much better tools out there that get the same thing done but much faster with less configuration.
I consider Vite to be in the same "last generation" family as Webpack/Babel; the "last" of the "last generation", kind of.
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.
My view of it is that the kitchen sink/"swiss army knife" mentality is the "previous generation" of Webpack/Babel that Vite fits, and esbuild is the closest to next generation mentality (something of a return to "do one thing/do it well/limit scope"). Vite is obviously at the generation transition boundary by being built on top of esbuild, but that doesn't make it "next generation" in my mind, that just makes it the "last of the dinosaurs, the smart one that figured out some of the meteor about to strike".
Okay, I understand. I don't think this way of divvying up generations is useful, but I see how you get there.
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.
Yeah, I think we are agreeing that the dividing lines are "how they approach things", I just have a very different idea of the approaches to divide. Vite stills "smells" to me like Webpack because it takes a "kitchen sink" approach and I keep finding that I don't need kitchen sinks in my living rooms when all I want is a sofa in there (the occasional spot-bundle of maybe even just a single lone dependency via esbuild; to bend the metaphor near to its breaking point).
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.
I disagree that Vite takes a "kitchen sink" approach, and I disagree that the amount of features is relevant to "generations of tooling". But that's fine, who cares that I disagree? :)
As I said, "kitchen sink" is almost a broken metaphor here. I don't think it has to do with "amount" of features, but "approach" to features. I look at the Vite feature list and lot of it reads to me as "Vite does this thing because Webpack did". It isn't doing something innovatively different from Webpack, even if the underpinning toolset is newer and different and faster and maybe has a more stable API. It makes sense for me to categorize Vite with Webpack despite its relative "youth" because of such similarities in approach (whether or not "kitchen sink" is exactly the right metaphor to describe whatever you want to call that approach).
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.
> I look at the Vite feature list and lot of it reads to me as "Vite does this thing because Webpack did".
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.
I think I spent too much time configuring Webpack in previous lives that I got all of that out of Webpack at some point or another (and Webpack always had plenty of plugins, too). I'm glad you feel productive in Vite, I'm not arguing to take Vite away from anyone. I just already have Webpack configurations doing everything Vite does that I don't have reason to touch and I prefer to do things in a lighter-weight way going forward (including "no bundling" as much as possible).
> 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.
I have had the same experience you describe with Webpack, and I've never felt anything similar with 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.
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. :)
> You don't get typescript to the browser without them
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 have nothing against ES modules, actually it's a good thing objectively.
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.
Some months ago, I was building some plugins and other code in Javascript, and to my surprise, my biggest problem back then was to figure out how become code form another file into my working context. There were multiple different code-snippets with different syntax, some worked, others not. This rant explains my problems back then. Getting some up to date documentation for modern javascript seems some kind of gamble at the moment.
I've been following ES module standardization for a while now. From the early days I was not convinced that we need a new syntax. We could adopt require/module as language features without breaking the entire ecosystem. It was just not "impactful" and "sexy" enough to do so.
Since then I'm yet to see ES modules solve real problems that CommonJS couldn't as the OP argues.
The "break the whole ecosystem" bit is what bugs me the most. It is nearly impossible now to write a short script in Node.js these days when you have mixed dependencies of ESM & CommonJS. Every 100 line script now forces you to include a code bundler of some sort, just because the two will not work in the same script.
I don't know much about this topic, but I found the OP making an argument that ES Modules are not, as hoped for/promissed, better than CommonJS. But it doesn't seem to explain any ways they are worse either. The argument seems to be just that we already had CommonJS requires, and it's annoying that things are now split with ES Modules additionally.
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.
The argument is we'd be better off if ES Modules were never proposed or implemented because the require syntax is better. By splitting the ecosystem, the developer experience has been made worse for no tangible benefit. Like this is not just the pain of transition from an old thing to a new thing, it's a straight up mistake and it should've never happened.
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?
I think any discussion of the comparisons between CommonJS and ES Modules can be tough because "everybody" has already forgotten (or never learned in the first place) the lessons of AMD modules (and UMD modules).
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.
Every language sucks there’s always something annoying or confusing or limiting.
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.
126 comments
[ 3.0 ms ] story [ 183 ms ] threadWhat'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.
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).
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.
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.
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")`);
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.
I do not understand how this should even remotely be possible. It is a de-facto standard now and used in moste Javascript projects.
It works rather well, it's not confusing at all and if you really want something else you can use require etc.
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!.
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.
https://requirejs.org/docs/api.html
Bundlers made people forget the pains but also the useful lessons of AMD modules.
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.
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?
This is not true if you use a CDN like esm.sh[1] or skypack[2].
[1]: https://esm.sh/
[2]: https://www.skypack.dev/
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.
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.
[1] https://vitejs.dev/guide/features.html#glob-import
> 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
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.
Coming from other languages ES modules seemed much more familiar.
Since then I'm yet to see ES modules solve real problems that CommonJS couldn't as the OP argues.
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.
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.