> 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
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.
I have a custom loader that can operate in both sync and async mode with the same syntax; it started off as a single loader to load AMD and CommonJS but I’ve been meaning to add ESM support as well (it was more opaque than I liked when I last tried, required too much browser magic).
I don't disagree! But is there really an argument (other than an ideological one) for why it needs to be an awaited import? Why does it matter that it's async?
ESM (as standardized, not necessarily as implemented in Node) treats import specifiers as URLs. That alone would inherently be async because network requests are async across basically all JS runtimes. ESM was also later extended to support top level await, making module execution itself async.
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.
Fetching code at runtime over the network makes sense in a browser, it doesn’t make sense outside of a browser and would actually be a huge security risk without all of the security features that browsers have. So it just reinforces the perception that ESM was only designed for browsers.
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.
"Sometimes works, but can also blow up at runtime after any upgrade" seems to me like a "pragmatic cure" that is worse than the existing "disease" of needing to treat all ESM imports as async and/or migrating all uses of `require()` to `await import()`. If incrementally moving a CJS code base to use `await import()` in more places is too hard then you probably have other problems you've overlooked for too long (bad promise usage, callback waterfalls, etc).
(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.)
I'd say it's exactly the opposite. Neither require() or import require an async code structure. The biggest reason I avoid ESM like a plague is that many of my projects are not, and do not need to be, async. Adding import() is a huge hassle because it is async.
Adding an importSync() function would be a perfect solution.
That was my first thought, and what I attempted first in my project. But it turned out that rewriting everything, while being incredibly tedious, was significantly easier to reason about and ensure continuity than developing my own bespoke dependency loader in a large project where not all portions are well understood.
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.