From 7b29458554dd84300be827b064713c501f0efbf2 Mon Sep 17 00:00:00 2001 From: sangwook Date: Sun, 1 Mar 2026 18:13:29 +0900 Subject: [PATCH] module: add requireStack to all error paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, `requireStack` was only set on the final fallback error in `Module._resolveFilename`. Errors thrown earlier in the resolution pipeline — from `tryPackage` (via `Module._findPath`) when a `package.json` `main` field points to a missing file, from `trySelf` during self-referential package resolution, and from `createEsmNotFoundErr` — did not include `requireStack`. Fix this by building `requireStack` from the parent chain before any resolution attempt and propagating it to all `MODULE_NOT_FOUND` error paths: - Wrap `trySelf` and `Module._findPath` calls in try-catch blocks that attach `requireStack` to any thrown `MODULE_NOT_FOUND` error that does not already have one. - Add an optional `requireStack` parameter to `createEsmNotFoundErr` and pass it from `_resolveFilename` where the error is thrown directly. --- lib/internal/modules/cjs/loader.js | 45 +++++++++++++++------- test/parallel/test-module-loading-error.js | 3 +- test/sequential/test-module-loading.js | 1 + 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index a320736d1b6fd7..9f719290c21149 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -534,7 +534,6 @@ function tryPackage(requestPath, exts, isMain, originalPath) { err.code = 'MODULE_NOT_FOUND'; err.path = pjsonPath; err.requestPath = originalPath; - // TODO(BridgeAR): Add the requireStack as well. throw err; } else { process.emitWarning( @@ -1406,6 +1405,14 @@ Module._resolveFilename = function(request, parent, isMain, options) { paths = Module._resolveLookupPaths(request, parent); } + const requireStack = []; + for (let cursor = parent; + cursor; + // TODO(joyeecheung): it makes more sense to use kLastModuleParent here. + cursor = cursor[kFirstModuleParent]) { + ArrayPrototypePush(requireStack, cursor.filename || cursor.id); + } + if (request[0] === '#' && (parent?.filename || parent?.id === '')) { const parentPath = parent?.filename ?? process.cwd() + path.sep; const pkg = packageJsonReader.getNearestParentPackageJSON(parentPath); @@ -1419,7 +1426,7 @@ Module._resolveFilename = function(request, parent, isMain, options) { ); } catch (e) { if (e.code === 'ERR_MODULE_NOT_FOUND') { - throw createEsmNotFoundErr(request); + throw createEsmNotFoundErr(request, undefined, requireStack); } throw e; } @@ -1428,7 +1435,15 @@ Module._resolveFilename = function(request, parent, isMain, options) { // Try module self resolution first const parentPath = trySelfParentPath(parent); - const selfResolved = trySelf(parentPath, request, conditions); + let selfResolved; + try { + selfResolved = trySelf(parentPath, request, conditions); + } catch (e) { + if (e.code === 'MODULE_NOT_FOUND' && e.requireStack === undefined) { + e.requireStack = requireStack; + } + throw e; + } if (selfResolved) { const cacheKey = request + '\x00' + (paths.length === 1 ? paths[0] : ArrayPrototypeJoin(paths, '\x00')); @@ -1437,15 +1452,16 @@ Module._resolveFilename = function(request, parent, isMain, options) { } // Look up the filename first, since that's the cache key. - const filename = Module._findPath(request, paths, isMain, conditions); - if (filename) { return filename; } - const requireStack = []; - for (let cursor = parent; - cursor; - // TODO(joyeecheung): it makes more sense to use kLastModuleParent here. - cursor = cursor[kFirstModuleParent]) { - ArrayPrototypePush(requireStack, cursor.filename || cursor.id); + let filename; + try { + filename = Module._findPath(request, paths, isMain, conditions); + } catch (e) { + if (e.code === 'MODULE_NOT_FOUND' && e.requireStack === undefined) { + e.requireStack = requireStack; + } + throw e; } + if (filename) { return filename; } let message = `Cannot find module '${request}'`; if (requireStack.length > 0) { message = message + '\nRequire stack:\n- ' + @@ -1485,16 +1501,19 @@ function finalizeEsmResolution(resolved, parentPath, pkgPath) { * Creates an error object for when a requested ES module cannot be found. * @param {string} request The name of the requested module * @param {string} [path] The path to the requested module + * @param {string[]} [requireStack] The require stack at the time of the error * @returns {Error} */ -function createEsmNotFoundErr(request, path) { +function createEsmNotFoundErr(request, path, requireStack) { // eslint-disable-next-line no-restricted-syntax const err = new Error(`Cannot find module '${request}'`); err.code = 'MODULE_NOT_FOUND'; if (path) { err.path = path; } - // TODO(BridgeAR): Add the requireStack as well. + if (requireStack) { + err.requireStack = requireStack; + } return err; } diff --git a/test/parallel/test-module-loading-error.js b/test/parallel/test-module-loading-error.js index 3496a4104df090..b26e395373e042 100644 --- a/test/parallel/test-module-loading-error.js +++ b/test/parallel/test-module-loading-error.js @@ -91,6 +91,7 @@ assert.throws( () => { require('../fixtures/packages/is-dir'); }, common.isAIX ? { code: 'ERR_INVALID_PACKAGE_CONFIG' } : { code: 'MODULE_NOT_FOUND', - message: /Cannot find module '\.\.\/fixtures\/packages\/is-dir'/ + message: /Cannot find module '\.\.\/fixtures\/packages\/is-dir'/, + requireStack: [__filename], } ); diff --git a/test/sequential/test-module-loading.js b/test/sequential/test-module-loading.js index 1e2efc6fd26a6b..118fcb1ff18d42 100644 --- a/test/sequential/test-module-loading.js +++ b/test/sequential/test-module-loading.js @@ -123,6 +123,7 @@ assert.throws( message: /packages[/\\]missing-main-no-index[/\\]doesnotexist\.js'\. Please.+package\.json.+valid "main"/, path: /fixtures[/\\]packages[/\\]missing-main-no-index[/\\]package\.json/, requestPath: /^\.\.[/\\]fixtures[/\\]packages[/\\]missing-main-no-index$/, + requireStack: [__filename], } );