Skip to content

module: increased surface for hazards with require(esm) experimental flag #52173

Closed
@WebReflection

Description

@WebReflection

What is the problem this feature will solve?

In this MR #51977 it's being proposed to allow CJS to require(esm) with the goal of helping people stuck in CJS to use ESM only modules, somehow conflicting with the plethora of dual modules already published and maintained by authors (and I am one of them).

To understand the issue there's no better way than a concrete use case.


Where we are now ...

A project using module a as dependency, where a is published as dual module, can be consumed from pure CJS, where by pure I mean no dynamic import(a) in the mix, as that usually undesired or not common in CJS land due lack of TLA, as well as pure ESM.

module a

// a.mjs
export default Math.random();

// a.cjs
module.exports = Math.random();

module b

console.log(require("a")); // ./a.cjs
// 0.123456789

In this CJS scenario the module a will always provide the same random number once, no matter how many modules require it.

Now, because it was not possible before to synchronously import module c, the module b has no hazards in a CJS only environment.

Now enters module c as ./c.mjs

// module c depends on module `a`
// and because it's ESM only it will
// consume module `a` as ESM
import random from "a"; // ./a.mjs

console.log(random);
// 0.234567891

export default random * 2;

Where we're potentially going ...

edit test the use case if you want

In a scenario where the flag lands "unflagged" and the CJS is still the default, developers will carelessly believe they can finally require(esm) without thinking twice about possible consequences ... if no error is encountered due TLA in the required ESM module, they think they're good!

module b after the flag

console.log(require("a")); // ./a.cjs
// 0.123456789

console.log(require("c").default); // ./c.mjs -> ./a.mjs
// NOT 0.246913578
// BUT 0.469135782

Conclusion

It is true that hazards related to dual modules where already possible before but there was no easy way to synchronously require ESM modules so that either they chose dual modules, they used a tool able to normalize everything as CJS or ESM, but any re-published package as CJS would've avoided or inlined somehow ESM modules with TLA.

In short, if the default require, when it comes to dual modules, still prefers CJS once this flag lands unflagged, we will see dual module authors blamed for issues that were not so easy to bring in before with potentially catastrophic results beyond the dummy Math.random() use case: databases, cache, file system, workers, you name it ... bootstrapping with ease dual modules will likely create more damage than solve instead anything it's aiming to solve and currently published dual modules cannot just stop providing their CJS counterpart until all users and dependents modules are capable of requiring ESM out of the box.

What is the feature you are proposing to solve the problem?

Before proposing anything I don't understand how this experimental flag is being considered as shippable in its current state (broken ESM require if TLA is used behind, increased dual module hazards surface) instead of fixing at the root level the issue by:

  • allow ESM as default in NodeJS
  • allow TLA in CJS too so that dynamic imports won't scare anyone anymore and the TLA behind the scene would also just work

Back to this flag though, I would like to propose at least the following solutions:

  • the default require(anything), once this flag lands unflagged, is to return the ESM version of the module, if such module is a dual module. This would solve the presented use case / issue because the module b that require("a") will already have the ESM version so that once module b finally can also require("c") from the ESM only world, nothing will break and no hazard will be present
  • there's gonna be a flag to impose the preferred way to require a module from CJS so that dual module authors can explicitly indicate in their package.json that the preferred way to require that module is through its ESM code and not its CJS artifact (or vice-versa whenever that's the case). In this case I believe having the "type": "module" in the package.json of a SHOULD already be enough to tell the new require(esm) ability that even when require(cjs) was used, and that module is dual module, the returned module is actually the ESM one.

The latter point was breaking before so it should be a no-brainer to consider while the former suggestion might break with default exports that were not expected before but I am hear to discuss possible solutions there too or propose we give developers time to test and adjust their code, after all they need to do so anyway the moment they finally require(ESM).

Having ESM as preferred way to any require will eventually convince developers to publish ESM only modules so that the dual module story can fade away in time, but if that's not the case I see a catch 22 like situation where authors can't stop publishing dual modules and users can't stop worrying about possible hazards introduced by the new feature.

What alternatives have you considered?

As already mentioned, I would rather bring in TLA in CJS behind a flag instead and call it a day, without mixing a tad too much the require ability of the legacy, non standard, module system that CJS is. This would be the easiest migration for everyone caring to migrate and it will still grant pure CJS software to work without worrying about hazards in the graph.

Thank you.


After Thoughts

It's OK to explore solutions to the problem (we're doing that already) but if NodeJS lands these kind of changes it better be sure that the entirety of the tooling based projects out there also get the memo and implement the right thing or we'll create a new kind of hell to deal with, explain, and resolve ... at least until everything is ESM only and CJS can be just a memory to find in outdated books.

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature requestIssues that request new features to be added to Node.js.loadersIssues and PRs related to ES module loaders

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions