Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Is there a quick summary of the problem somewhere? I haven't worked on frontend stuff in nearly a decade.


From a package maintainer standpoint, trying to support both ESM and CJS in the same published package is a _nightmare_.

I wrote an extensive post a few weeks ago detailing all the pain that I've dealt with and problems I've run into this year trying to modernize the Redux packages to fully support ESM and CJS:

- https://blog.isquaredsoftware.com/2023/08/esm-modernization-...

I've gotten a lot of feedback from other maintainers saying they've run into similar issues.

Also had a couple podcast discussions following up on that topic:

- https://syntax.fm/show/661/supper-club-shipping-esm-with-mar...

- https://changelog.com/jsparty/290


Just my 2 cents/anecdata in response to your tweet about what library authors have to put up with, hoping it helps some of the doomers replying:

> Build artifact formats (ESM, CJS, UMD)

I don't know anyone that ever used AMD/UMD. CJS is loadable in ESM, and tsup makes the two of them trivial.

> > Matrixed with: dev/prod/NODE_ENV builds

Not every library has this issue.

> Bundled or individual .js per source

Not a new issue, this has always been a toss up (I prefer .js per source)

> exports setup

Yup this is a pain, especially because I prefer multiple .js files, and I wish tsup would help here.

> Webpack 4 limits

I'm not sure what you mean by webpack not understanding optional chaining syntax, we use webpack 4 and use it all the time. Might be a benefit of using typescript.

And tsup helps with the .js requirement issue.

> TS moduleResolution options

Again it looks like tsup solves this.

> User environments

When is that ever not an issue? Very fair for maintainers to say "nope, sorry, that's too esoteric an environment"

> TS typedef output (bundled? individual? .d.ts, or .d.mts?)

Should match your .js file output 1:1 (.cjs/.mjs et al)

---

All that to say, if you use typescript and don't do anything fancy, you've got a pretty compatible library with just `tsc`

And if you're looking to do something cross compatible, tsup will help a lot.


I maintain a lot of packages consumed by thousands of engineers internally at my job.

We don’t have super rigid standards in many ways, however that experience has lead me to wonder why so many engineers are struggling with dual support or cjs and ESM.

That said, none of those packages are redux level of downloads, and imagine you hit more edge cases than I do in that

Edit: originally stated hundreds. Turns out we’ve grown enough that I can say thousands now


Oh my god, thank you for this. I expected a bad situation, but I underestimated the badness by several orders of magnitude. Makes me want to never touch JS again.


That blog post is exactly what I was looking for, thanks!


> From a package maintainer standpoint

Like why is this the standpoint that trumps everything else (eg stability of the ecosystem)? Have a tool to solve the problem and be done with it. It's not like with esm you can publish packages free of tooling anyway.


I never said it "trumps everything else".

I'm saying that I've spent much of this year dealing with all the pain around this, and linked an article on my own experiences, and that other package maintainers agree that this matches their experiences.

If you can build a tool that magically solves all these problems, great! Please let me know when that's available :)

(FWIW Bun looks like a genuine improvement on the _consuming_ side of things, but that doesn't help on the _publishing_ side of things.)


I'm having trouble understanding why ESM was needed in the first place. I've always found the require system to be very elegant. What benefits does the module syntax provide?

I forked and customized an old Javascript templating engine and have published it as npm packages. Should I spend effort migrating to the new way?


Jesus, I thought python packaging sucked, but wow.


Still not clear to me what ESM exactly fixes. CJS works pretty well for me. The amount of pain ESM gives in unit tests is enough to ignore ESM. Maybe Bun will make a difference here.


By being declarative instead of imperative:

- The dependencies of a module can be scraped without running the code

- The subset of declarations used in an import can be known

This allows modules to be loaded in parallel, and also allows for things like tree-shaking. The biggest benefits are for web browsers, but all around it's a more constrained way to express dependencies, which allows the bundler/runtime to take more liberties.


CJS does not work in browsers. Bundlers fake it well enough you might not feel that pain regularly, but ESM was designed for use in browsers and CJS wasn't.


You could get those benefits through special syntax that chose to not deviate from commonjs semantics.

We are in this mess because tc39 explicitly chose to not give a damn about nodejs ecosystem.

None of the secondary benefits of ESM to commonjs semantics incompatibility (like recursive imports, top level awaits etc) are IMHO worth the ecosystem disruption.


Commonjs is synchronous. If you used it in the browser, it would block the main thread each time you required a module.

That's bad.


ESM has dual syntax for static and dynamic imports too.

What I am saying is that

import Foo from "foo"

could be sematically equivalent to

const Foo = require("foo")

and

const Foo = await import("foo")

could be semantically equivalent to

const Foo = await Promise.resolve(() => require("foo"))

if tc39 designed ESM import spec with CJS compat in mind.

It is fully possible to introduce additional syntax while not breaking the ecosystem.


CJS makes an assumption that all of the main body of a module is synchronous including its imports and that the main body of all of its imports also ran synchronously prior to the main body of that module. A lot of CJS modules are built around side-effects during module loading, built on assumptions from the way that Node especially synchronously loads everything, that break in any attempt to make the process asynchronous. It's not just a matter of syntax sugar, it's a trouble of bad assumptions and presumptions in the CJS format itself that browsers have no way to paper over (but bundlers can fake by putting everything in the same file). Loading a new file is always an asynchronous operation in a browser. CJS has never supported that. Those problems were known at the time when Node picked CJS. (AMD was the competing format that worked in browsers. AMD had its own problems and was a pain to work in without tools like Typescript, but it was compatible with browsers. CJS never was. ESM imports are backward compatible with AMD imports but because AMD is mostly a dead format in 2023 no browser today actually implements the shims to support that, but they were on the table for a long time and can still be ponyfilled if anyone is crazy enough to have an AMD codebase in 2023 that didn't switch to a CJS-based hodge podge in Node at some point in the last decade and that they can't all-at-once migrate to ESM. Which in my experience mostly just describes what's left of Esri's over-reliance on Dojo's AMD loader for ArcGIS JS and just about nothing else in the wild in 2023.) This mess is all Node's fault and it should have never picked CJS in the first place, that was always the losing horse.

(Hypothetically there might have been a place and time in CJS history to force CJS require() to always return a promise and module.exports to always take a function that returns a promise and that hypothetical version might have been compatible enough to import directly from ESM. Nobody actually wanted that hypothetical "ACJS" so it never existed. ACJS could have at least met browsers half-way and might have made the transition less overall painful.)


Nobody is advocating for synchronously importable modules in browsers. Also, people had already written support utilities like require1k [1] to support cjs in browsers. We don't necessarily need a full bundler - we only need a module analysis step between parse and execute which a native js engine is quite well positioned to facilitate.

Having said that, the fact remains that vast majority of pain points that people who actually need to maintain isomorphic libraries face today have nothing to do with synchronous vs asynchronous nature of cjs and esm modules.

It is the myriad pointless nuances like default export, namespace imports etc. that are sources of biggest headaches in day to day work. In cjs there is a simple model that a module exports an object and while importing you import that object. Instead now we have a situation where we need to deal with:

    export default { 
        foo() {...} 
    }
is not same as:

    export function foo() { ... }
and

    import Foo from "foo"
is something different from

    import * as Foo from "foo"

Plus the additional complexities introduced by import bindings being live etc. are just annoyances that one has to deal with over and over again every time module interop is involved.

[1] https://github.com/Stuk/require1k




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: