chore: verified require(esm) compatibility for ESM-only dependencies#2575
chore: verified require(esm) compatibility for ESM-only dependencies#2575aryamohanan wants to merge 7 commits into
Conversation
| 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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
We discussed this offline, but sharing key points here for future reference:
Why
.defaultis 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 triggerWhen 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'); |
There was a problem hiding this comment.
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.
| "sinon": "15.2.0", | ||
| "sinon-chai": "3.7.0", | ||
| "square-calc": "3.2.1", | ||
| "square-calc-v2": "npm:square-calc@2.4.0", |
There was a problem hiding this comment.
The author of this package is really cool 😆
|



Node.js
require(esm)compatibilityThis 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 bypassesimport().Note:
gotandnode-fetchhttp based and it works out of the box with the require(esm) format. No instrumentation or any changes required for these depsReference: 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