Repository: nodejs/node
Pull request: module: include module information in require(esm) warning

When emitting the experimental warning for `require(esm)`, include information about the parent module and the module being require()-d to help users locate and update them.

For example, when using npm which relies on `debug` which attempts to load `supports-color` using `require()`:

```
$ cd tools/doc
$ ../../node ../../deps/npm/bin/npm-cli.js install

npm warn cli npm v10.9.0 does not support Node.js v23.0.0-pre. This version of npm supports the following node versions: `^18.17.0 || >=20.5.0`. You can find the latest version at https://nodejs.org/.
(node:88340) ExperimentalWarning: CommonJS module /Users/joyee/projects/node/deps/npm/node_modules/debug/src/node.js is loading ES Module /Users/joyee/projects/node/deps/npm/node_modules/supports-color/index.js using require().
Support for loading ES Module in require() is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
```

<!--
Before submitting a pull request, please read:

- the CONTRIBUTING guide at https://github.com/nodejs/node/blob/HEAD/CONTRIBUTING.md
- the commit message formatting guidelines at
  https://github.com/nodejs/node/blob/HEAD/doc/contributing/pull-requests.md#commit-message-guidelines

For code changes:
1. Include tests for any bug fixes or new features.
2. Update documentation if relevant.
3. Ensure that `make -j4 test` (UNIX), or `vcbuild test` (Windows) passes.

If you believe this PR should be highlighted in the Node.js CHANGELOG
please add the `notable-change` label.

Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.
-->


Changed files:
filename: lib/internal/modules/cjs/loader.js
patch: @@ -1369,11 +1369,31 @@ function loadESMFromCJS(mod, filename) {
     // ESM won't be accessible via process.mainModule.
     setOwnProperty(process, 'mainModule', undefined);
   } else {
-    emitExperimentalWarning('Support for loading ES Module in require()');
+    const parent = mod[kModuleParent];
+    let messagePrefix;
+    if (parent) {
+      // In the case of the module calling `require()`, it's more useful to know its absolute path.
+      let from = parent.filename || parent.id;
+      // In the case of the module being require()d, it's more useful to know the id passed into require().
+      const to = mod.id || mod.filename;
+      if (from === 'internal/preload') {
+        from = '--require';
+      } else if (from === '<repl>') {
+        from = 'The REPL';
+      } else if (from === '.') {
+        from = 'The entry point';
+      } else {
+        from &&= `CommonJS module ${from}`;
+      }
+      if (from && to) {
+        messagePrefix = `${from} is loading ES Module ${to} using require().\n`;
+      }
+    }
+    emitExperimentalWarning('Support for loading ES Module in require()', messagePrefix);
     const {
       wrap,
       namespace,
-    } = cascadedLoader.importSyncForRequire(mod, filename, source, isMain, mod[kModuleParent]);
+    } = cascadedLoader.importSyncForRequire(mod, filename, source, isMain, parent);
     // Tooling in the ecosystem have been using the __esModule property to recognize
     // transpiled ESM in consuming code. For example, a 'log' package written in ESM:
     //

filename: lib/internal/util.js
patch: @@ -266,10 +266,13 @@ function slowCases(enc) {
   }
 }
 
-function emitExperimentalWarning(feature) {
+function emitExperimentalWarning(feature, messagePrefix) {
   if (experimentalWarnings.has(feature)) return;
-  const msg = `${feature} is an experimental feature and might change at any time`;
   experimentalWarnings.add(feature);
+  let msg = `${feature} is an experimental feature and might change at any time`;
+  if (messagePrefix) {
+    msg = messagePrefix + msg;
+  }
   process.emitWarning(msg, 'ExperimentalWarning');
 }
 

filename: test/es-module/test-require-module-preload.js
patch: @@ -4,11 +4,9 @@ require('../common');
 const { spawnSyncAndAssert } = require('../common/child_process');
 const { fixturesDir } = require('../common/fixtures');
 
-const warningRE = /ExperimentalWarning: Support for loading ES Module in require/;
 function testPreload(preloadFlag) {
   // The warning is only emitted when ESM is loaded by --require.
-  const stderr = preloadFlag !== '--import' ? warningRE : undefined;
-
+  const isRequire = preloadFlag === '--require';
   // Test named exports.
   {
     spawnSyncAndAssert(
@@ -24,7 +22,8 @@ function testPreload(preloadFlag) {
       },
       {
         stdout: 'A',
-        stderr,
+        stderr: isRequire ?
+          /ExperimentalWarning: --require is loading ES Module .*module-named-exports\.mjs using require/ : undefined,
         trim: true,
       }
     );
@@ -44,7 +43,8 @@ function testPreload(preloadFlag) {
         cwd: fixturesDir
       },
       {
-        stderr,
+        stderr: isRequire ?
+          /ExperimentalWarning: --require is loading ES Module .*import-esm\.mjs using require/ : undefined,
         stdout: /^world\s+A$/,
         trim: true,
       }
@@ -66,7 +66,8 @@ function testPreload(preloadFlag) {
       },
       {
         stdout: /^ok\s+A$/,
-        stderr,
+        stderr: isRequire ?
+          /ExperimentalWarning: --require is loading ES Module .*cjs-exports\.mjs using require/ : undefined,
         trim: true,
       }
     );
@@ -89,7 +90,8 @@ function testPreload(preloadFlag) {
       },
       {
         stdout: /^world\s+A$/,
-        stderr,
+        stderr: isRequire ?
+          /ExperimentalWarning: --require is loading ES Module .*require-cjs\.mjs using require/ : undefined,
         trim: true,
       }
     );
@@ -115,7 +117,7 @@ testPreload('--import');
     },
     {
       stdout: /^package-type-module\s+A$/,
-      stderr: warningRE,
+      stderr: /ExperimentalWarning: --require is loading ES Module .*package-type-module[\\/]index\.js using require/,
       trim: true,
     }
   );

filename: test/es-module/test-require-module.js
patch: @@ -3,9 +3,13 @@
 
 const common = require('../common');
 const assert = require('assert');
+const path = require('path');
 
+// Only the first load will trigger the warning.
 common.expectWarning(
   'ExperimentalWarning',
+  `CommonJS module ${__filename} is loading ES Module ` +
+  `${path.resolve(__dirname, '../fixtures/es-module-loaders/module-named-exports.mjs')} using require().\n` +
   'Support for loading ES Module in require() is an experimental feature ' +
   'and might change at any time'
 );