Today I read this thread by Evan Plaice which took my thinking to discussions from almost a year ago in the Node.js Module bi-weekly… One particular remark that was maybe not yet relevant:
Thought: Wonder how a
CJS
system would look like if it was written inESM
— not a dumb one!
Today, this may no longer be irrelevant at all, but not for CJS
per say, and here is why.
Why not CJS
It is sad for us to have to repeatedly revisit how CJS
as successful as it is cannot scale as intended. But we all forget (at least I do, a lot) and then we remember, and it somehow hurts.
-
Scaling beyond the synchronous façades offered by Node.js and bundles takes one solving the paradox of synthetically synchronous asynchronous calls to
require
. -
Doing that breaks many modules not written to be loaded
async
, where linking attempts to pierce across the threshold of the cycle that is to happen to resolve the one that is to conclude.
Aside: Top-level
await
does not solve this problem. That is like a proposal being a little a more ambitious saying something likeawait
as a prefix keyword to any function call is okay, and then we want to magically slap that before we evaluate the function body to all relevantrequire(…)
calls — and what about modules that return a promise because they have to!
What if not CJS
?!
Let’s start with legacy:
-
AMD
style -
CJS
self-contained graph payloads (aka bundles)
This was what we learned from all those years of browser based hacking, before the tooling façade made us all forget.
But today we can say:
-
import
style
Details?!
It should be on everyone’s radar by now that await import(…)
is completely valid CJS
code, provided it lives inside an async function () { /* body */ }
— did not yet land, but likely not far out. And that forces us to reconsider how we go about dependency graphs.
This is not as clean-cut yet, like if import(cjs)
will one day give us named-exports.
This will likely start leading to funny outcomes, imho. In most cases, it is best to port to ESM
, and offer CJS
for backcompat where necessary. The drawback there is that you must consider how far back you are going. And my best intuition here is to deprecate CJS
support once you port to ESM
, ie in the next few months, if we do not find a good solution for code to run without the maintenance for consistency costs that will pile up with it, we will drop it.
Aside:
ESM
code runs in Node.js fine with solutions like @jdalton’sesm
, and if you are wondering it supports{ "engines": { "node": ">=6" } }
as defined in the package.json.
You can say this is a gentle push, there are questions without answers, and ESM
certainly is not the superior thing, but it is the thing that is standard, because it can be.
It came to be because of what we learned from AMD
versus CJS
back in the day. And we can no longer say that userland loader complications are not a problem, because userland loaders that eval
are at least harder to properly trust.