NPM and NodeJS should do more to make ES Modules easy to use
borischerny.com
borischerny.com
I had been using adminjs at work. Their new major version was ESM-only, and it was easier to _write a new admin panel from scratch_ than it was to refactor our entire codebase to be ESM just so we could upgrade one library. I expect that's the situation at hundreds of thousands of other companies.
Like for all the belly aching that happened over async functions (and the whole function color rant), synchronous and asynchronous functions work together just fine through plain old promises. You can easily use async functions alongside functions that use old fashioned callbacks. ESM vs CJS is a file coloring (versus function coloring) problem, but there's no interoperability. There's no escape hatch when you just need one file to use another file but their colors are incompatible.
A middle ground answer would be using import() in CJS to asynchronously require the ESM module. It would require some hacking around your loading flow to ensure the module loads before you try to do anything with it but would still be preferable to rebuilding the entire thing.
Node has recently taken a more pragmatic approach, by treating local specifiers as potentially synchronous, allowing sync require() of ESM… which then fails if the required module uses top level await.
(But I'm a hardline "all you need is type=module" sort and think a CJS=>TS emitting ESM "rip off the bandaid" approach to CJS legacy libraries works and is fundamentally easier than most of the "pragmatic" solutions to CJS/ESM interop. It is past time to jettison CJS out the airlock.)
Both TC39 and node utterly cocked this up. The design of ESM is bad, async imports are a stupid idea, and node’s implementation has been beset by ideology.
Adding an importSync() function would be a perfect solution.
This is already available in Node.js 22 [1] with ` --experimental-require-module` flag.
[1]: https://nodejs.org/en/blog/announcements/v22-release-announc...
It's wild to think about: esm has taken nearly a decade to be feasible for folks to use, without heavy encumbrance; it was the key feature of es-2015/es6!
It took many more years for import-maps to become a thing, such that we could start using modules in a modular fashion; we've had to use bundlers/codemods to re-resplve dependencies, unless you've been going to go Deno's original path of just hitting absolute urls wherever they be. There's still no standing proposal for how we get import-maps on service-workers & shared workers; modules still are not a ubiquitous feature of the core web & JavaScript ecosystems. 9 years latter. https://github.com/WICG/import-maps/issues/2
It's been hard being a webdevs, when the future remains so partially implemented.
The article's exploration of the bomb ecosystem is telling. And is much of the reason I hope JavaScript Registry (jsr) project might succeed. Npm will never escape it's past. CJS is going to be in there for a long time. Being on a registry where everything is esm & typescript, that sounds divine. https://deno.com/blog/jsr-is-not-another-package-manager
The main thing you're missing out on is the optimizations that come with bundlers. But they should be treated as just that: optimizations. You shouldn't be required to use a bundler just to develop/distribute JS programs.
There's definitely some truth to your gripes. Back in 2015 there was a ton of latent hope http/2 push and ESM would let us get away from bundling & perhaps even needing build steps at all. That hasn't panned out.
Indeed, push was removed entirely! https://groups.google.com/a/chromium.org/g/blink-dev/c/K3rYL... Except it's still the core backend for Push API. Alas, fetch, in spite of many years of asking for it, never got the ability to observe a Push request, so devs literally never had a chance to explore the possible content update/content discovery use cases that make so so much sense in Web Push Protocol. Crying waste & shame; Push had promise outside of asset delivery but it never got a chance (except the deeply browser-intermediated Push API).
There are a variety of tools & options for bundlers to emit bundled esm. Which would nicely make your bundle usable by multiple different consumers.
It's a bit simpler (and I'd assume faster) than the traditional approach of dynamically creating a script tag at runtime.
Additional features like access to import.meta lets you get the path to the current script which also helps bundlers with resolving the paths to dynamic imports without requiring the developer to specify the base path explicitly.
There are problems though, for example you cannot retry an import() if the internet drops. The failed import is cached so the browser will fail the import again on retry, even if the connection is restored https://github.com/fatso83/retry-dynamic-import
(Though I'd be happier with importSync() to identify the use of ESM packages.)
Maybe I'm in the minority, but using NPM packages that have changed over to modules continue to be one of my biggest annoyances in NodeJS:
npm install node-fetch
const fetch = require('node-fetch');
Error [ERR_REQUIRE_ESM]: require() of ES Module
npm install node-fetch@2The syntax is a decade old, and people have been using CJS with ES6 import syntax this whole time without major issues. Node ESM support is an entirely different self-inflicted problem that's been around for just a couple years. Don't gaslight folks into thinking this was something that the ecosystem was planning for a decade. It simply wasn't.
Node was, from the get-go, strongly tied to V8's development roadmap. They (like everyone else in the JS language space at the time) were watching the ESM debate, and had every opportunity to plan for a "few years out" switchover, starting the moment TC39 accepted ESM into the official language spec, with V8 following suit.
They didn't, but thankfully it's never too late: Node should absolutely still go "we're finally deprecating CJS. You should use ESM, and for the next 2 years it's business as usual but after that we won't support CJS unless your project marks itself as such in package.json, and 2 years after that, we're removing CJS support entirely".
So this has two consequences:
1. NPM is split in two. All the packages that don't upgrade are useless. If you rely on one, you either need to do the work to port it (and all of its dependencies) before you can even consider migrating your own code. Or pray that someone else will do it.
2. Companies simply won't upgrade. The last version of Node to support CJS will persist for a decade or more. It might get forked and make it harder for Node to actually drive the ecosystem forward. If you think io.js won't happen again as a result of a decision like this, you're mistaken.
If a 5 year old library doesn't work with current Node, that's perfectly fine. Kind of already the case right now anyway, so nothing new under the sun there. Packages that are still actively used, though, get 4 years to uplift. that is plenty of time.
2) how is that different from companies that still use Node 14 or even 10 in production today because their dependency chains make it impossible to uplift even without taking CJS vs. ESM into consideration?
Everyone who's already stuck in the past is going to stay stuck in the past, anyone who can afford to uplift their codebase over two to four years will be better off, including the entire JS dev landscape. Let's not pretend that the bad habits of others should hold everyone else back. That's how we got here in the first place.
Nothing except doing things the standard way so that standards-compliant language implementations (and other tools) can work with them instead of targetting IE^H^H NodeJS's non-standard quirks.
> the payoff is "it's the shiny new thing"
NodeJS's reality distortion field has claimed another.
Ecmascript modules are going on 10 years old at this point. (For comparison, that's more than the amount of time that NodeJS existed when modules weren't a part of the language.)
False. Ecmascript defined the syntax for imports but not the mechanism for module loading. I've been using ES6 imports with CJS modules for almost as long as the syntax has existed (without problems!); the incompatibility you're describing is ones of years old.
> Nothing except doing things the standard way so that standards-compliant language implementations
Node's way of handling modules isn't the same as the way the browser works (and it can't be: see node_modules). There's no singular all-encompassing "JavaScript ESM" standard. There's not even consistency between server-side JS runtimes.
But more importantly, saying "the way your code worked for fifteen years is now shit and won't work going forward, you need to rewrite it because there's a new thing now" is an incredibly fucked way to run an ecosystem. See: Python 3.
That's a Node problem. 100% foreseeable (and avoidable).*
There's a reason why I compared NodeJS to Internet Explorer in my first comment—because it's extremely apt. Too bad most NPM programmers have blinders such that their entire worldview consists of NPM+NodeJS (not unlike the way programmers who were used to exclusively targeting Microsoft tech 20 years ago lived in their own parochial world) and an expectation that everyone else should bend over backwards to accommodate their way of life.
If you as a programmer want not to be burned by IE-/NodeJS-like shenanigans, then don't run arms outstretched into those flames.
* And, no, it's not an anachronism to say that. There's a reason why the notion of "forward compatibility" exists—at least among the wise. For an example of how to approach things sensibly (read: avoid getting burned exactly like one should otherwise expect to), have a look at the position/approach to forward compatibility that the TypeScript team (initially a Microsoft undertaking, funnily enough) has taken. There's no reason anyone should expect to be able to throw caution to the wind, YOLO their way forward with their hands covering their ears while shouting "la-la-la" and for everything to turn out okay. That is profoundly unwise.
> an incredibly fucked way to run an ecosystem
Right. Which makes it doubly crazy the way that folks underneath the NodeJS carnival tent have taken to approaching things for the last 15 years—and the last 10 years in particular. (Again, to emphasize: 10 years!)
But of course I'm being facetious, because when you said "an ecosystem" you didn't narrowly mean "the NodeJS ecosystem". You meant "the JS ecosystem"—leaning heavily on the very same merger doctrine of the-NodeJS-ecosystem-is-the-JS-ecosystem-which-is-the-NodeJS-ecosystem that I mentioned NPMers believing in before.
> See: Python 3.
Sure, and Python is proprietary—with a BDFL, a community rallying for the most part behind a single implementation (where you get what you pay for if you go astray), and no strong commitment to standards/compatibility for most of its existence; the Python project can do whatever the Python project wants to do. Likewise Perl. Likewise, you know... Dart. That's how those things go.
JS is fundamentally different, and that's what you and countless other NPM programmers seem not to be getting. See also my earlier comment:
The difference is the language/standard in question neither originated with NodeJS nor is NodeJS now nor has it ever been led by the people behind the language/standard
<https://news.ycombinator.com/item?id=40744081>
The node_modules ecosystem is, in fact, not the whole world, nor is it synonymous with "JS". (Heck, it's not even synonymous with CommonJS[1].) NPMers can ignore this fact at their peril, or they can wise up and accept that they can't simply will things to go the way they apparently think they should be able to make them go despite mountains of evidence and wisdom to the contrary.
Sure. It also "just works" at the CLI. Sometimes I just want to test code quickly at the CLI. I'm not into "architecting" this part of my code as much as ES thinks I want to.
Me? I'd prefer importSync() to having require() cover ESM files. I think it would be more clear but, either way, it would do exactly what you say, open the CJS world to migration.
My only criticism is that in order to do incremental conversion, both need to work. At that point, you'd might as well just eliminate the cognitive overhead and have one function.
All of that is something that I consider to be platform-level. It's insane that millions of feature-writing devs are expected to know all these arcana.
Then again, it might be fixed™ soon Ⓡ
https://joyeecheung.github.io/blog/2024/03/18/require-esm-in...
The fact that you don't know whether your codebase uses TypeScript, esbuild or Webpack is disappointing. It means that those worries have been handled for you and you don't care to learn them, which is never a good stance if you work with this on a daily basis. But I somewhat agree that it should all be vastly simpler.
I also kind of agree with the downvoted/flagged comment re: Golang. The way the JavaScript ecosystem works is highly dependent on the way Node is handled. And Node has, for many years, made ESM needlessly complicated.
En contraire. The problem is that some years ago I did learn the basics, but the ground has shifted underneath my feet multiple times. There's not really an incentive to learn the finer points of ESM-vs-CJS if Typescript hides the input into the system, Angular has its own ideas about modules, uses Webpack or babel internally and then spits out CJS anyway.
The whole JS ecosystem is nice if you edit 10 files by hand, but I just don't know what I should do with the knowledge that I should "require cjs" when I need Typescript for type safety and it'll only let me "import ts" anyway.
I agree with this, but the whole point of the blog-post is that the "platform" currently handles this rather poorly.
I have yet to see a frontend-project that was bigger than some three-person-garage-hobby that didn't occasionally run into CJS versus ESM issues. Maybe not something that pops up on the radar of all the devs in the project but at least at the level of folks who take care of the setup and whatnot, it often pops up in rather painful fashion. Case in point; a few angular-versions back, umd-bundles were dropped, which at least for the project I worked on caused me quite a lot of headache as some of our tool-chain (most notably our testing-setup) relied on angular shipping commonJS-compatible modules.
It's currently also a major pain for anyone publishing an npm-package, even if it's primarily intended to be run on node. The kind of incantations one has to do are just insane (especially if you dared to import from node:crypto or want to support more than just the latest lts); I've just stopped bothering after tearing my hair out for a weekend to no avail, even though I really wanted to support ESM as well.
[1] https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3...
[2] https://nodejs.org/api/packages.html#packages_dual_package_h...
But I do believe they got the syntax wrong - should have been "from fs import { readFile }" so that auto-complete works. Python got that right, but that's the only thing Python got right ;)
They won't do this without consensus in TC39. They shouldn't either; that'd be worse than this niggle.
If I said language foo that isn’t TS but transpiled to JS all the same was considering that syntax, people might have a different opinion.
Between that and some remaining warts from <1.0 mistakes/incorrect assumptions, there's plenty of evidence that it is a good thing that other than its type system TS focuses on following standards rather than trying to lead them.
There's a place for the language "foos" that want to lead and champion new standards. As an ecosystem we all seem to benefit from Typescript not being that language (anymore, mostly not since 1.0 with the few obvious mistakes aside). It's a part of what makes Typescript trustworthy as Production tooling. It's also what helps make Typescript mostly "cheap" and "unextraordinary" in Production build pipelines.
Not a two world reality where the guest language has its own mini-platform and tooling, on top of the actual platform.
import {} from "some/package"
And then go back with working autocompletionBut I do agree that it should be “from ‘foo’ import { … }”, because then the code would look nicer.
Sorry I don't follow. What the "from ..." syntax does (when typing it) is to narrow down 10k possibilities (the set of all exported functions in all in-scope modules) down to a few dozen. How would this be possible otherwise?
Illustration:
Case 1: import <just about anything is possible here - can't auto suggest>
Case 2: from fs import <only fs functions are in scope, start autosuggesting on keypress>
In my personal contrarian opinion I would have wanted something along the lines of
import "url" with {namedImport}, defaultImport;
With other keywords like import assertion mixed at the same level import "url" assert {type:"js"} with {namedImport}, defaultImport;
I admit that `with` is not a great choice...I guess tooling can still provide a solution - a snippet on “import”, where the first tab stop takes you to the module path string, and the next tab stop jumps back to inside the braces?
(I'd argue that default + wildcard imports are bad, but ESM went out of its way to have them :shrug:)
Personally, I prefer the "import x from y" format because it makes it easier to visually scan where an import is coming from; fair point about auto-complete though.
You get an approximation of conditionally loading a module by simply converting everything to ESM and enabling tree-shaking. When module loading has no side effects this is simple to do. If you’re unsure at build time whether you will call a certain function from a certain module and that’s the reason you’re conditionally loading it, again enable ESM and tree shaking and import the function and call it conditionally except do so at runtime. You’ll only get exactly what you need to avoid the bloat of the whole module and your dependency graph will also be more correct as a bonus!
I've been a full time NodeJS programmer for fourteen years. Module isn't some pure theoretical concept. It is a file that exports something in Node.
There's a ton of 'maybe I need this file' in the world. For example, I have things that are polymorphic by instance. I grab a config entry to decide which of several modules are going to be used. The others are completely unnecessary.
You might say, just add a build step that chooses the right ones. To which I would say, Are you nuts? Why would I screw up a perfectly working paradigm?
To get "an approximation of conditionally loading".
You’re missing the point entirely, perhaps intentionally? If you’re using modules you’re already using a “build step” - code that runs before your actual code. Your “perfectly working paradigm” is only such if you intentionally disregard the core reason for modules: modularity.
Turns out that the ESM purists struggle to find reasons to condemn those of us who don’t have the need or resources to the chase bleeding edge of purity.
I use modules for all the reasons. Your assertion that require() or importSync() represents a willful misunderstanding is silly and arrogant.
People outside the context of this discussion could simply be uninformed, they don’t need to be willfully misunderstanding.
I also love getting things to work. Three quarters of NPM packages are CJS. Several of them are ones I wrote.
The reason they are not ESM is that the theoretically pure folks had control and I voted with my fingers because everything I do has to fit in my a substantial amount of existing CJS code.
Arguing for practical solutions that accommodate an imperfect world is absolutely correct.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
To rewrite to be asynchronous gets me exactly nothing.
In work that is asynchronous, I use import(). It's fine. Also, it gives me nothing but, since that particular thing is already async, why not?
This is a terrible, badly thought out thing that was done by ECMA that harms a ton of developers and that would have been entirely saved if somebody had asked me.
I would have said, add importSync() and there will be literally no problems ever again.
IMO it would be a pandora's box of unending problems. A function doing a synchronous dynamic import creates a bad user experience. It's also a pretty rare circumstance in my experience. Dynamic imports are relatively uncommon to begin with. Having one that must be inside a non-async function is unheard of for me.
If you had your way, I think the net result would have been worse.
The only excuse is that ESM happened during the big Deno Node wars when the community was split, everyone was mad and, apparently making dumb decisions.
I read that a new version of NodeJS has revised require() to bring in ESM modules. That's good enough. (I would prefer importSync() for clarity.)
import "./foo_bar.js" (for FooBar)import {} from 'fs';
Then move your cursor back inside the {} and you'll have nice autocomplete. Works with object destructuring, too.
Parent article mentions static analysis and synchronous loading on startup but that has never been an issue for me despite building some large and complex Node apps over the years.
I’ve looked into this in the past but all I could find are strong opinions without solid technical reasoning.
https://github.com/tc39/proposal-type-annotations
At that point (and with ES modules) you'd be able to run your TS code directly without any transpilation necessary. I'd love to remove all the build process junk from my projects and have them run quicker.
I've written a few projects as plain ESM JS with JSDoc type annotations and it truly is a joy to bypass build steps. JSDoc type annotations do get annoying as a project grows to a certain size, though.
> if they stick within a certain reasonably large subset of the language. <-------- !!!!!!!!!!
js + type annotations !== typescript
The most recent discussion sums this up with, I think this comment:
> EAO: Okay. So the sense I get overall of this whole proposal, that it’s more of a – it’s a solution looking for a problem that it’s trying to solve a year ago when this got accepted for Stage 1, the problem statement that was in fact considered then was only effective we made up or made concrete during the meeting itself, and then – so what it looks like now is that since then, that problem statement has evolved to this current form of unifying or unforking JavaScript and somehow then presenting type annotations as a way of achieving this result. However, I’ve not been able to find any conversation anywhere or description of how in practice this unification is supposed to happen as a consequence of accepting type annotations.
No one had a good answer to that, because there isn't one.
Anyhow, tldr; this has basically nothing to do with any tangible reason to think ESM is good or useful in anything other than a massively broad, speculative 'maybe in the future' kind of way.
...it is most certainly not any kind of advantage or reason to use ESM for anyone, right now.
It's very close though. The proposal is well written and covers the cases where existing Typescript code would not parse correctly/work as intended under the proposal. Most of those cases are pre-1.0 Typescript features still in the language for backwards compatibility but generally frowned upon and that you probably already have strict warnings and linter errors preventing you from using today. The big exception is a lot of TS codebases heavily use `enum` and I've still not seen enough suggestions to tighten the linter warning against it. (But there are at least some of us suggesting avoiding TS `enum` today.)
That proposal seems to have stalled, and I doubt it's going anywhere. Here is the thing: people want hard, strict compiler checks that can be enforced at all time, not Python-style type hints. If you want JavaScript but type checked, Typescript is the de-facto standard. Either do "checkJS" or convert the project to Typescript. In fact, the latter is much better in terms of expressiveness.
* https://github.com/tc39/notes/blob/main/meetings/2023-09/sep...
If you're looking into other runtimes ESM used to be the way to go because of Deno, but these days you're likely either running Node or Bun and both work well with CommonJS and ESM.
If you do both frontend and backend work, or if your team has a lot of cross-over between the two sides of the ecosystem, then it'll likely be easier for you to use ESM on both ends as CommonJS isn't supported by browsers.
I think the primary reason CommonJS is still around is mostly because Typescript replaces it's import/export module system with something that's basically similar in syntax to the way ESM does it untill it gets transpiled into Javascript. If people actually had to work with CommonJS modules in 2024 then I think they'd likely go insane.
Not everyone will agree with me on this, but I don't think there is a reason to rewrite old projects into ESM unless you have a very good reason to do so. That being said, there isn't really a good reason to start new projects with CommonJS either... Unless you have a really good reason to do so.
It also opens up more options when Typescript is just "type stripped" rather than transpiled to an incompatible module syntax. You probably still want a full Typescript compile at CI time to get robust typechecking, and you'll have the Typescript LSP doing its thing in your IDE still, but you can use type-stripping tools like esbuild for very fast type stripping in some portions of your inner dev loop.
The static analysis that ESM supports is handy because it opens up technical benefits like Tree Shaking. Your apps might run on SSDs, you probably don't have a lot of reasons to bundle them, you likely aren't worried about on disk size, you might not be worried about bundle publish size to npm, and so you might not think Tree Shaking applies to you, but V8 under the hood of Node is still going to do Tree Shaking of memory and garbage collection for you with ESM in ways that it simply cannot with CJS. I've seen some real memory performance gains in Node apps just switching from CJS to ESM already, and V8's ESM optimizations only seem to get better as more ESM is deployed in the wild.
There are more, smaller technical benefits, but "type stripping not transpiling" and "in memory tree shaking" are strong ones that are easy to overlook.
1. The downstream users of your backend library can use ESM JS.
2. You can author (or output) isomorphic code that works in Node.js and other environments.
3. Static analysis tools can more reliably understand your dependency graph, e.g. find unnecessary code/modules/packages.
4. You can use a custom load API to do customized operations. E.g. import a YAML file as a JavaScript object.
If none of these apply to you individually, then you don't have a lot to be gained.
ESM code can use CJS packages.
> 2. You can author (or output) isomorphic code that works in Node.js and other environments.
TS compilation makes this possible already - e.g. I have authored API SDK packages that can be used in FE and BE code.
> 3. Static analysis tools can more reliably understand your dependency graph, e.g. find unnecessary code/modules/packages.
An earlier sibling comment mentions this - this is potentially a good reason but I need to do some benchmarking to see the actual impact (would make for a good blog post one day) :)
> 4. You can use a custom load API to do customized operations. E.g. import a YAML file as a JavaScript object.
TIL but it seems like a niche feature that I would never need in BE world.
True. Everything is a default import, but it does work.
> I have authored API SDK packages that can be used in FE and BE code.
FE code can't use CJS. You need to have an extra step to convert to ESM (or AMD, webpack chucks, etc).
And the general consensus is that frontend JS has too much complexity.
---
CJS works pretty well for Node.js.
If it didn't, it wouldn't have been adopted.
The primary advantage is authors can write and distribute isomorphic code. (Again, without relying on heavy or kludgy module system conversations.)
The proposal to disable node.js style imports will just split ecosystem and make a large part of industry stick to ancient version / make a fork. Is that really worth the gain? Just check how long it took some bigger projects to migrate from python2 to python3
When you hear "NodeJS", you really need to bethinking of it in the same category as IE (wrt browser behavior) or Visual C/C++. It's but one, often (knowingly/deliberately) quirky, non-standard implementation by a group that doesn't necessarily have your best interests at heart or the interests of those outside their own platform umbrella.
1. I'm developing a library that depends on d3js
2. I want the library to be usable without any build tools. Just clone my repo and host the files from a static server. Or just import directly from jsdelivr
3. I also want people to be able to use NPM to install my library if they so choose
The problem is if I vendor d3js, then developers who consume my library via NPM might end up with 2 copies of d3js in their app, if their app also uses d3 directly. But if I don't vendor it, then my ESM-only users have to use an import map to resolve the bare specifier in the browser, which is kind of ugly and confusing.
More details: https://stackoverflow.com/q/78645299/943814
It hasn’t changed because it’s not a real problem. This is like forcing main as the default branch in git.
Why? Because there is no importSync(). If that existed, it would be easy to interoperate.
You could achieve the same thing with a bunch of `import()` calls, but I'm not sure why you'd want to when you can just use the `import` keyword instead.
You cannot just use 'import' if you also use CJS. To use 'import', it must be an ESM module and the you cannot use require() to access CJS utilities.
In CJS code, one can use import() to bring in ESM modules but, import() is asynchronous. The problem is that I have a lot of code that is not asynchronous. To add an asynchronous function requires substantial rewriting that I am rarely willing to do.
If, in addition to the asynchronous import(), there was a synchronous importSync(), I could use ESM modules any time I want. That would allow me to...
1) Use and support new utilities that are ESM.
2) Write new utilities that are ESM. Presently, I always target CJS because that's what I have to use.
I have read that there is a new version of NodeJS that supports using require() for ESM modules. That will be a huge, huge, huge improvement although I would prefer importSync() so that it was clear when I was using an ESM module. Even so, I will take it.
The extensions were always silly to me. Who changes only a few files to esmodules? You either change your whole project or not at all.
Transition. It’s a much lighter lift to transform a project piece by piece than do the whole thing.
JSR says "here's a way to leave the original ecosystem alone, but have a better-fitting ecosystem that does the things you want". (not sure about the 'jsr doesn't change' bit? It literally requires ES modules, in lieu of common js modules).
Seems pretty relevant, to me, but YMMV. I understand it's not a direct answer, but it seems as relevant as "here's typescript" to someone saying "we should add types to javascript".
This will never, ever happen. Too much of the foundational ecosystem relies on it.
I hope for the sake of the nodjs ecosystem, you are wrong.
Fragmentation issues are one of the many reasons that nodejs struggle with adoption.
Woah there. If nodejs is struggling for adoption I don’t know what isn’t.
Not saying it needs to be done, but this can possibly be handled via a tranpiler layer. It might make CommonJS a bit slower to startup (but fully functional), which is an added incentive to move.
Again, I don't have any opinion on whether this is a good thing. Like you said, too many modules depend on it.
"The new version runs your old code slower and it's enough to notice" would be a death blow to the ecosystem. No major company is going to upgrade to that. It's already a project to drag a major codebase kicking and screaming to a new LTS release of your runtime (even if it makes code faster), do you really think folks will invest the time to work to make their code run slower unless they invest even more effort and modify ~every module in their codebase?
ESM isn't implicitly good. The benefits of ESM are almost entirely theoretical for pretty much everyone who is content with CJS. I manage codebases right now that total half a million lines of typescript and js and I think I simply wouldn't notice any improvements from converting any of the code. The point being: this isn't a problem for me or people in similar situations, it's a problem for the folks working on Node and the standards. Forcing folks to upgrade or making their code worse to exert any kind of pressure does nothing to help anyone.
I’ve worked on a few big React projects but haven’t really looked much into how the build works, I found out upgrading one thing forced me to upgrade other things and I wound up making a lot of changes by hand to the build scripts and figured I’d probably screw something up. Dependencies changing from CommonJS to ESM was probably the most common problem that can frequently solved by version bumps (at risks of adopting changes you don’t want)
At some point I decided to try the alternate path of switching to Vite for a build system since I’ve had good luck working with it for some VR side projects.
It’s funny how you can code on front-end Javascript and not need to learn about CommonJS until something like this hits.
Another angle that might be effective: take the most popular aspect of commonjs - `require(‘extensionless-string’)` - and tie it to the least popular aspect, .cjs extensions.
It's quite meaningful for dependencies to be fetched asynchronously, but sometimes you really need something to be executed in the order it's written.
And if you're bundling, then you can add them into the process. I've been avoiding most things that could require a polyfill since 2018 or so anyway as green browsers are pretty feature complete. As much as I'd like the F# style pipelines, and there are a couple other niceties, I don't miss much.
Give them an importSync() and the migration can happen incrementally.
Neither Node nor NPM are languages.
> when you try to use an external stylesheet you hit the wall of the modern js world: css can not be imported in js without
Uh...?
It will never happen because it would require coordination and money, but instead of having millions upon millions of different NPM packages, many of which are downright harmful, we should have a careful selection of maybe the top 20,000 or so.
And then maybe call the next generation of node something else, maybe ProJS.
I'm not sure I understand what you're going for.
And for what? In Node specifically, it's not as if esm actually solves any real problem! In the greater ecosystem, sure it has some benefits, but Node doesn't even have to choose. Just support both at once, like bun and build systems have for a while, and let's move on from this nonsense.
Why do you dislike `require()` so much?
Just imagine a person who doesn't write modules in Node.JS, but he would like his small scripts to be placed in one JS-file - without any additional directories and `package.json`. His script, for example, updates data for a desktop widget, is easily bypassed by standard nodejs modules and he has hundreds of such scripts in his folder. Why does he need all this `import` overhead?
new thing good
I agree with the article that type="module" should be the default and once it is the well established default the bandaid of .mjs, .cjs, et al extensions should probably provoke a deprecation warning rather than being another long-term tech debt part of Node.
There’s a billion browser clients out there. That ensures that whatever API they ship, an increasing amount of code will be written directly against it. Everybody else just needs to adapt or be seen as incompatible with the baseline of JavaScript.
If only there was something in the HTTP protocol to make it more efficient to load multiple requests from the same server. Alas that must be a pipe dream, and every little image and script is loaded separately.
Oh wait, it’s not 1996 anymore…?
/s
I promise you they will use any feature that's documented on MDN as being available in all browsers. And ES modules do make life easier for someone who's just writing HTML and JS and doesn't know anything about build tools.
IMO, it's the wrong attitude that we should focus on the professional ecosystem and forget about this use case. Probably most people writing front-end code got their start by creating a HTML file in Notepad (or equivalent) and loading it via file:// into the browser.
It will become more common in products. There are going to be startups that see that great development experience and long for some returns towards "just FTP it to the web server as-is" CI/CD processes. There are going to be more product owners everywhere (startups, Enterprise) that think that HTTP/2+ adoption (or even HTTP/1.1 properly configured) is acceptable in the real world and that they are done with most of the needs for bundling.
[0] So no Babel, only Typescript. You probably don't need Babel in 2024.
(ETA: I've even documented this development pattern for a library I built: https://worldmaker.net/butterfloat/#/getting-started)
( see also: ES modules are terrible https://news.ycombinator.com/item?id=29137663 )
To suggest deprecating CommonJS so as to reduce fragmentation seems so bizarre to me. Audacious.
We embraced ESM a couple of years back. It was a bit harder earlier on; for example our test frameworks didn't support mocking ESM code well enough. But now there's hardly anything to complain about. There's no going back really - we wouldn't want to.
what the fuck was the point of unifying this particular thing at such a great PITA cost, when obviously none of node APIs work in the browser?
should things like Buffer be removed too? the browser only has Uint8Array, after all.