Skip to content

Commit 2174159

Browse files
dnluptargos
authored andcommitted
esm: improve commonjs hint on module not found
Run CommonJS resolver only if `error.code` is ERR_MODULE_NOT_FOUND. Avoid using absolute paths in hint by: * using a parent-relative path if the specifier is a relative path * using a `pkg/x.js` format if the specifier is bare (e.g. `pkg/x`) PR-URL: #31906 Reviewed-By: Guy Bedford <[email protected]> Reviewed-By: Myles Borins <[email protected]>
1 parent a4ec01c commit 2174159

9 files changed

+163
-6
lines changed

lib/internal/modules/esm/resolve.js

+84-6
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,22 @@
22

33
const {
44
ArrayIsArray,
5+
ArrayPrototypeJoin,
6+
ArrayPrototypeShift,
57
JSONParse,
68
JSONStringify,
79
ObjectFreeze,
810
ObjectGetOwnPropertyNames,
911
ObjectPrototypeHasOwnProperty,
12+
RegExp,
1013
SafeMap,
1114
SafeSet,
1215
StringPrototypeEndsWith,
1316
StringPrototypeIncludes,
1417
StringPrototypeIndexOf,
18+
StringPrototypeReplace,
1519
StringPrototypeSlice,
20+
StringPrototypeSplit,
1621
StringPrototypeStartsWith,
1722
StringPrototypeSubstr,
1823
} = primordials;
@@ -29,8 +34,8 @@ const {
2934
Stats,
3035
} = require('fs');
3136
const { getOptionValue } = require('internal/options');
32-
const { sep } = require('path');
33-
37+
const { sep, relative } = require('path');
38+
const { Module: CJSModule } = require('internal/modules/cjs/loader');
3439
const preserveSymlinks = getOptionValue('--preserve-symlinks');
3540
const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
3641
const typeFlag = getOptionValue('--input-type');
@@ -611,9 +616,11 @@ function packageResolve(specifier, base, conditions) {
611616
throw new ERR_MODULE_NOT_FOUND(packageName, fileURLToPath(base));
612617
}
613618

614-
function shouldBeTreatedAsRelativeOrAbsolutePath(specifier) {
615-
if (specifier === '') return false;
616-
if (specifier[0] === '/') return true;
619+
function isBareSpecifier(specifier) {
620+
return specifier[0] && specifier[0] !== '/' && specifier[0] !== '.';
621+
}
622+
623+
function isRelativeSpecifier(specifier) {
617624
if (specifier[0] === '.') {
618625
if (specifier.length === 1 || specifier[1] === '/') return true;
619626
if (specifier[1] === '.') {
@@ -623,6 +630,12 @@ function shouldBeTreatedAsRelativeOrAbsolutePath(specifier) {
623630
return false;
624631
}
625632

633+
function shouldBeTreatedAsRelativeOrAbsolutePath(specifier) {
634+
if (specifier === '') return false;
635+
if (specifier[0] === '/') return true;
636+
return isRelativeSpecifier(specifier);
637+
}
638+
626639
/**
627640
* @param {string} specifier
628641
* @param {URL} base
@@ -645,6 +658,51 @@ function moduleResolve(specifier, base, conditions) {
645658
return finalizeResolution(resolved, base);
646659
}
647660

661+
/**
662+
* Try to resolve an import as a CommonJS module
663+
* @param {string} specifier
664+
* @param {string} parentURL
665+
* @returns {boolean|string}
666+
*/
667+
function resolveAsCommonJS(specifier, parentURL) {
668+
try {
669+
const parent = fileURLToPath(parentURL);
670+
const tmpModule = new CJSModule(parent, null);
671+
tmpModule.paths = CJSModule._nodeModulePaths(parent);
672+
673+
let found = CJSModule._resolveFilename(specifier, tmpModule, false);
674+
675+
// If it is a relative specifier return the relative path
676+
// to the parent
677+
if (isRelativeSpecifier(specifier)) {
678+
found = relative(parent, found);
679+
// Add '.separator if the path does not start with '..separator'
680+
// This should be a safe assumption because when loading
681+
// esm modules there should be always a file specified so
682+
// there should not be a specifier like '..' or '.'
683+
if (!StringPrototypeStartsWith(found, `..${sep}`)) {
684+
found = `.${sep}${found}`;
685+
}
686+
} else if (isBareSpecifier(specifier)) {
687+
// If it is a bare specifier return the relative path within the
688+
// module
689+
const pkg = StringPrototypeSplit(specifier, '/')[0];
690+
const index = StringPrototypeIndexOf(found, pkg);
691+
if (index !== -1) {
692+
found = StringPrototypeSlice(found, index);
693+
}
694+
}
695+
// Normalize the path separator to give a valid suggestion
696+
// on Windows
697+
if (process.platform === 'win32') {
698+
found = StringPrototypeReplace(found, new RegExp(`\\${sep}`, 'g'), '/');
699+
}
700+
return found;
701+
} catch {
702+
return false;
703+
}
704+
}
705+
648706
function defaultResolve(specifier, context = {}, defaultResolveUnused) {
649707
let { parentURL, conditions } = context;
650708
let parsed;
@@ -685,7 +743,27 @@ function defaultResolve(specifier, context = {}, defaultResolveUnused) {
685743
}
686744

687745
conditions = getConditionsSet(conditions);
688-
let url = moduleResolve(specifier, parentURL, conditions);
746+
let url;
747+
try {
748+
url = moduleResolve(specifier, parentURL, conditions);
749+
} catch (error) {
750+
// Try to give the user a hint of what would have been the
751+
// resolved CommonJS module
752+
if (error.code === 'ERR_MODULE_NOT_FOUND') {
753+
const found = resolveAsCommonJS(specifier, parentURL);
754+
if (found) {
755+
// Modify the stack and message string to include the hint
756+
const lines = StringPrototypeSplit(error.stack, '\n');
757+
const hint = `Did you mean to import ${found}?`;
758+
error.stack =
759+
ArrayPrototypeShift(lines) + '\n' +
760+
hint + '\n' +
761+
ArrayPrototypeJoin(lines, '\n');
762+
error.message += `\n${hint}`;
763+
}
764+
}
765+
throw error;
766+
}
689767

690768
if (isMain ? !preserveSymlinksMain : !preserveSymlinks) {
691769
const urlPath = fileURLToPath(url);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
'use strict';
2+
3+
import obj from 'some_module/obj';
4+
5+
throw new Error('Should have errored');

test/fixtures/node_modules/some_module/index.js

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/node_modules/some_module/obj.js

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/node_modules/some_module/package.json

+12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
'use strict';
2+
3+
require('../common');
4+
const { spawn } = require('child_process');
5+
const { join } = require('path');
6+
const { fixturesDir } = require('../common/fixtures');
7+
8+
spawn(
9+
process.execPath,
10+
[
11+
join(fixturesDir, 'esm_loader_not_found_cjs_hint_bare.mjs')
12+
],
13+
{
14+
cwd: fixturesDir,
15+
stdio: 'inherit'
16+
}
17+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
internal/modules/run_main.js:*
2+
internalBinding('errors').triggerUncaughtException(
3+
^
4+
5+
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '*test*fixtures*node_modules*some_module*obj' imported from *test*fixtures*esm_loader_not_found_cjs_hint_bare.mjs
6+
Did you mean to import some_module/obj.js?
7+
at finalizeResolution (internal/modules/esm/resolve.js:*:*)
8+
at packageResolve (internal/modules/esm/resolve.js:*:*)
9+
at moduleResolve (internal/modules/esm/resolve.js:*:*)
10+
at Loader.defaultResolve [as _resolve] (internal/modules/esm/resolve.js:*:*)
11+
at Loader.resolve (internal/modules/esm/loader.js:*:*)
12+
at Loader.getModuleJob (internal/modules/esm/loader.js:*:*)
13+
at ModuleWrap.<anonymous> (internal/modules/esm/module_job.js:*:*)
14+
at link (internal/modules/esm/module_job.js:*:*) {
15+
code: 'ERR_MODULE_NOT_FOUND'
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Flags: --experimental-loader ./test/common/fixtures
2+
import '../common/index.mjs';
3+
console.log('This should not be printed');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
(node:*) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time
2+
(Use `node --trace-warnings ...` to show where the warning was created)
3+
internal/modules/run_main.js:*
4+
internalBinding('errors').triggerUncaughtException(
5+
^
6+
7+
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '*test*common*fixtures' imported from *
8+
Did you mean to import ./test/common/fixtures.js?
9+
at finalizeResolution (internal/modules/esm/resolve.js:*:*)
10+
at moduleResolve (internal/modules/esm/resolve.js:*:*)
11+
at Loader.defaultResolve [as _resolve] (internal/modules/esm/resolve.js:*:*)
12+
at Loader.resolve (internal/modules/esm/loader.js:*:*)
13+
at Loader.getModuleJob (internal/modules/esm/loader.js:*:*)
14+
at Loader.import (internal/modules/esm/loader.js:*:*)
15+
at internal/process/esm_loader.js:*:*
16+
at Object.initializeLoader (internal/process/esm_loader.js:*:*)
17+
at runMainESM (internal/modules/run_main.js:*:*)
18+
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:*:*) {
19+
code: 'ERR_MODULE_NOT_FOUND'
20+
}

0 commit comments

Comments
 (0)