Skip to content

esm: fix relative require in non-file CJS source - #65557

Open
zeexzeex wants to merge 1 commit into
nodejs:mainfrom
zeexzeex:esm-cjs-relative-require
Open

esm: fix relative require in non-file CJS source#65557
zeexzeex wants to merge 1 commit into
nodejs:mainfrom
zeexzeex:esm-cjs-relative-require

Conversation

@zeexzeex

Copy link
Copy Markdown

Fixes: #53198

The load hook documentation states that when a hook provides the source
of a CommonJS module, the require calls that source makes go through the
ESM loader, so the registered hooks apply to them:

When a source is provided, all require calls from this module will be
processed by the ESM loader with registered resolve and load hooks

That does not hold when the module has a URL that is not a file:. The
require built for those modules calls Module._resolveFilename() before
handing the specifier to the ESM resolver, only to read the extension of
the resulting path and decide whether the request is JSON or a native
addon. The CJS resolver works on file paths, so it cannot resolve
./relative against a referrer such as custom:cjs. It throws
MODULE_NOT_FOUND and the ESM resolution below it is never reached, which
is why the resolve hook is not called at all.

Error: Cannot find module './relative'
Require stack:
- custom:cjs
    at Module._resolveFilename (node:internal/modules/cjs/loader:1572:15)
    at require (node:internal/modules/esm/translators:132:30)
  code: 'MODULE_NOT_FOUND',

The error code is the tell: the ESM loader reports ERR_MODULE_NOT_FOUND,
so a bare MODULE_NOT_FOUND means the CJS resolver produced it. This is
what @aduh95 pointed out when confirming the bug.

The lookup has to stay, since the JSON and .node branches depend on the
resolved path, so this lets it fail when the referrer is not a file and
leaves the specifier for the ESM resolver to handle. When the referrer is
a file: URL the error is rethrown, so nothing changes there.

This only affects module.register(). The same case already works with
module.registerHooks(), which takes a different path
(loadCJSModuleWithModuleLoad).

Behaviour change

A specifier that the hooks do not claim now fails with
ERR_UNSUPPORTED_RESOLVE_REQUEST instead of MODULE_NOT_FOUND. The
referrer scheme is not hierarchical, so the specifier genuinely cannot be
resolved, and the new error says that; the old one wrongly suggested the
CJS resolver was in charge. Flagging it here in case reviewers would
rather keep the previous code.

When a load hook provides the source of a CommonJS module, the require
calls in that source are meant to go through the ESM loader so that the
registered hooks apply to them, as documented for the load hook.

That did not happen when the module had a URL that is not a file:. The
require function built for those modules called
Module._resolveFilename() before handing the specifier to the ESM
resolver, only to look at the extension of the resulting path and decide
whether to treat the request as JSON or as a native addon. Because the
CJS resolver works on file paths, it cannot resolve a relative specifier
against a referrer such as custom:cjs, so it threw MODULE_NOT_FOUND and
the ESM resolution below it was never reached. The resolve hook was not
called at all.

Keep that lookup, since the JSON and .node branches depend on it, but
let it fail when the referrer is not a file and leave the specifier for
the ESM resolver to handle.

A specifier that the hooks do not claim now fails with
ERR_UNSUPPORTED_RESOLVE_REQUEST, which reports that the referrer scheme
is not hierarchical, instead of MODULE_NOT_FOUND coming from the CJS
resolver.

Signed-off-by: Avocado <ujubongbong@gmail.com>
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/loaders

@nodejs-github-bot nodejs-github-bot added esm Issues and PRs related to the ECMAScript Modules implementation. needs-ci PRs that need a full CI run. labels Aug 26, 2026
@codecov

codecov Bot commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.05%. Comparing base (7b6b21a) to head (06fde1d).

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #65557      +/-   ##
==========================================
- Coverage   90.05%   90.05%   -0.01%     
==========================================
  Files         751      751              
  Lines      254420   254433      +13     
  Branches    47975    47981       +6     
==========================================
+ Hits       229121   229131      +10     
+ Misses      16483    16481       -2     
- Partials     8816     8821       +5     
Files with missing lines Coverage Δ
lib/internal/modules/esm/translators.js 97.60% <100.00%> (+0.04%) ⬆️

... and 28 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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

Labels

esm Issues and PRs related to the ECMAScript Modules implementation. needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Relative require() in commonjs source not processed by ESM Loader

2 participants