Skip to content

chore: verified require(esm) compatibility for ESM-only dependencies#2575

Open
aryamohanan wants to merge 7 commits into
mainfrom
chore-require-esm
Open

chore: verified require(esm) compatibility for ESM-only dependencies#2575
aryamohanan wants to merge 7 commits into
mainfrom
chore-require-esm

Conversation

@aryamohanan
Copy link
Copy Markdown
Contributor

@aryamohanan aryamohanan commented Jun 2, 2026

Node.js require(esm) compatibility

This PR adds test coverage and validation for Node.js native require(esm) support, ensuring ESM-only dependencies can be correctly loaded from CommonJS applications.

Previously, ESM-only packages (got, node-fetch) worked only in ESM mode in their latest versions (Node.js v24 +ESM). With this change, they are now also supported in CommonJS environments across all Node.js versions that support require(esm)

Case:
require(got) returns a Module Namespace Object wrapper.

Beacuse when we require('got'), Node.js returns a wrapper object containing all of got's various exports. The main function is trapped inside the .default key:

// require('got') returns this object:
{
default: [Function: got],
HTTPError: [class HTTPError],
Options: [class Options]
}
To let users skip .default, the package author must explicitly add Node's special 'module.exports' string export to their ESM source code:

something like:

export default got;
export { got as 'module.exports' }; // Special Node.js trigger
When Node.js sees this magic string export, it automatically strips away the wrapper object and returns the function directly to require().

Why this change

Node.js (v20.19+) supports loading ESM modules directly from CommonJS using require(esm), introducing a new interoperability path that bypasses import().

Note: got and node-fetch http based and it works out of the box with the require(esm) format. No instrumentation or any changes required for these deps

Reference: https://joyeecheung.github.io/blog/2025/12/30/require-esm-in-node-js-from-experiment-to-stability/

Ticket: https://jsw.ibm.com/browse/INSTA-73235

@aryamohanan aryamohanan requested a review from a team as a code owner June 2, 2026 12:26
@aryamohanan aryamohanan changed the title chore: verified Node.js require(esm) compatibility for ESM-only dependencies chore: verified require(esm) compatibility for ESM-only dependencies Jun 3, 2026
Copy link
Copy Markdown
Contributor

@abhilash-sivan abhilash-sivan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

if (process.env.USE_REQUIRE_ESM === 'true') {
// got v14+ is ESM-only and uses Node.js native require(esm) to load on CJS app
// Reference: https://joyeecheung.github.io/blog/2025/12/30/require-esm-in-node-js-from-experiment-to-stability/
got = require('got').default;
Copy link
Copy Markdown
Contributor

@kirrg001 kirrg001 Jun 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest to run this only on the v25 stable version.
Then we do not need any USE_REQUIRE_ESM logic or .default.
And we can drop esmOnly completely.
The instrumentation and tests stay untouched.

We can add nodeEsm to define the node esm version.
This might be helpful for the future anyway.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As ".default" is still required from the Node env, I would suggest to just have one proof test that this works under misc

More & more libs will move to esm, we do not want to add this code for all instrumentations

const isGotV14Plus = semver.gte(version, '14.0.0');
if (process.env.USE_REQUIRE_ESM === 'true') {

Do we care if we skip CJS for ESM apps?
No I don't think so.

We care that tracing works if customers use native esm pkg for CJS pkg.
Its anyway an edge case I think.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We discussed this offline, but sharing key points here for future reference:

Why .default is required?

When we require('got'), Node.js returns a wrapper object containing all of got's various exports. The main function is trapped inside the .default key:

// require('got') returns this object:
{
  default: [Function: got],  
  HTTPError: [class HTTPError],
  Options: [class Options]
}

To let users skip .default, the package author must explicitly add Node's special 'module.exports' string export to their ESM source code:

something like:

export default got;
export { got as 'module.exports' }; // Special Node.js trigger

When Node.js sees this magic string export, it automatically strips away the wrapper object and returns the function directly to require().

ref: https://nodejs.github.io/package-examples/05-cjs-esm-migration/migrating-exports/

require(esm) availability

While officially stabilized in Node.js v25.4.0, unflagged across all active LTS release lines (including v20.19.0+ and v22.12.0+ etc)

const morgan = require('morgan');
const bodyParser = require('body-parser');
const getAppPort = require('@_local/collector/test/test_util/app-port');
const calculateSquare = require('square-calc');
Copy link
Copy Markdown
Contributor Author

@aryamohanan aryamohanan Jun 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

square-calc is a pure ESM package(our test package) that natively supports CommonJS require(). Thanks to the 'module.exports' export trick, CJS apps don't have to deal with the .default wrapper anymore for this package.

Per the Node.js ESM migration guide, to allow CommonJS apps to call this package via require() without appending .default, the ESM source must explicitly provide the special export { fn as 'module.exports' }; named export. This instructs Node.js to automatically unwrap the namespace object for CommonJS consumers.

Comment thread package.json
"sinon": "15.2.0",
"sinon-chai": "3.7.0",
"square-calc": "3.2.1",
"square-calc-v2": "npm:square-calc@2.4.0",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really cool

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The author of this package is really cool 😆

Copy link
Copy Markdown
Contributor

@kirrg001 kirrg001 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great!

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Jun 3, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants