Skip to content

Commit 47548d9

Browse files
aduh95RafaelGSS
authored andcommitted
esm: fix hint on invalid module specifier
PR-URL: #51223Fixes: #51216 Reviewed-By: Geoffrey Booth <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent 8412751 commit 47548d9

File tree

7 files changed

+66
-31
lines changed

7 files changed

+66
-31
lines changed

‎lib/internal/modules/esm/resolve.js‎

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@
33
const{
44
ArrayIsArray,
55
ArrayPrototypeJoin,
6-
ArrayPrototypeShift,
6+
ArrayPrototypeMap,
77
JSONStringify,
88
ObjectGetOwnPropertyNames,
99
ObjectPrototypeHasOwnProperty,
10-
RegExp,
1110
RegExpPrototypeExec,
1211
RegExpPrototypeSymbolReplace,
1312
SafeMap,
@@ -21,6 +20,7 @@ const{
2120
StringPrototypeSlice,
2221
StringPrototypeSplit,
2322
StringPrototypeStartsWith,
23+
encodeURIComponent,
2424
}=primordials;
2525
constinternalFS=require('internal/fs/utils');
2626
const{ BuiltinModule }=require('internal/bootstrap/realm');
@@ -30,7 +30,7 @@ const{getOptionValue } = require('internal/options');
3030
constpolicy=getOptionValue('--experimental-policy') ?
3131
require('internal/process/policy') :
3232
null;
33-
const{ sep, relative, toNamespacedPath, resolve }=require('path');
33+
const{ sep,posix: {relative: relativePosixPath}, toNamespacedPath, resolve }=require('path');
3434
constpreserveSymlinks=getOptionValue('--preserve-symlinks');
3535
constpreserveSymlinksMain=getOptionValue('--preserve-symlinks-main');
3636
constexperimentalNetworkImports=
@@ -912,6 +912,7 @@ function moduleResolve(specifier, base, conditions, preserveSymlinks){
912912
* Try to resolve an import as a CommonJS module.
913913
* @param{string} specifier - The specifier to resolve.
914914
* @param{string} parentURL - The base URL.
915+
* @returns{string | Buffer | false}
915916
*/
916917
functionresolveAsCommonJS(specifier,parentURL){
917918
try{
@@ -924,29 +925,38 @@ function resolveAsCommonJS(specifier, parentURL){
924925
// If it is a relative specifier return the relative path
925926
// to the parent
926927
if(isRelativeSpecifier(specifier)){
927-
found=relative(parent,found);
928-
// Add '.separator if the path does not start with '..separator'
928+
constfoundURL=pathToFileURL(found).pathname;
929+
found=relativePosixPath(
930+
StringPrototypeSlice(parentURL,'file://'.length,StringPrototypeLastIndexOf(parentURL,'/')),
931+
foundURL);
932+
933+
// Add './' if the path does not start with '../'
929934
// This should be a safe assumption because when loading
930935
// esm modules there should be always a file specified so
931936
// there should not be a specifier like '..' or '.'
932-
if(!StringPrototypeStartsWith(found,`..${sep}`)){
933-
found=`.${sep}${found}`;
937+
if(!StringPrototypeStartsWith(found,'../')){
938+
found=`./${found}`;
934939
}
935940
}elseif(isBareSpecifier(specifier)){
936941
// If it is a bare specifier return the relative path within the
937942
// module
938-
constpkg=StringPrototypeSplit(specifier,'/')[0];
939-
constindex=StringPrototypeIndexOf(found,pkg);
943+
consti=StringPrototypeIndexOf(specifier,'/');
944+
constpkg=i===-1 ? specifier : StringPrototypeSlice(specifier,0,i);
945+
constneedle=`${sep}node_modules${sep}${pkg}${sep}`;
946+
constindex=StringPrototypeLastIndexOf(found,needle);
940947
if(index!==-1){
941-
found=StringPrototypeSlice(found,index);
948+
found=pkg+'/'+ArrayPrototypeJoin(
949+
ArrayPrototypeMap(
950+
StringPrototypeSplit(StringPrototypeSlice(found,index+needle.length),sep),
951+
// Escape URL-special characters to avoid generating a incorrect suggestion
952+
encodeURIComponent,
953+
),
954+
'/',
955+
);
956+
}else{
957+
found=`${pathToFileURL(found)}`;
942958
}
943959
}
944-
// Normalize the path separator to give a valid suggestion
945-
// on Windows
946-
if(process.platform==='win32'){
947-
found=RegExpPrototypeSymbolReplace(newRegExp(`\\${sep}`,'g'),
948-
found,'/');
949-
}
950960
returnfound;
951961
}catch{
952962
returnfalse;
@@ -1154,14 +1164,14 @@ function defaultResolve(specifier, context ={}){
11541164
*/
11551165
functiondecorateErrorWithCommonJSHints(error,specifier,parentURL){
11561166
constfound=resolveAsCommonJS(specifier,parentURL);
1157-
if(found){
1167+
if(found&&found!==specifier){// Don't suggest the same input the user provided.
11581168
// Modify the stack and message string to include the hint
1159-
constlines=StringPrototypeSplit(error.stack,'\n');
1160-
consthint=`Did you mean to import ${found}?`;
1169+
constendOfFirstLine=StringPrototypeIndexOf(error.stack,'\n');
1170+
consthint=`Did you mean to import ${JSONStringify(found)}?`;
11611171
error.stack=
1162-
ArrayPrototypeShift(lines)+'\n'+
1163-
hint+'\n'+
1164-
ArrayPrototypeJoin(lines,'\n');
1172+
StringPrototypeSlice(error.stack,0,endOfFirstLine)+'\n'+
1173+
hint+
1174+
StringPrototypeSlice(error.stack,endOfFirstLine);
11651175
error.message+=`\n${hint}`;
11661176
}
11671177
}

‎test/es-module/test-esm-module-not-found-commonjs-hint.mjs‎

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,53 @@
11
import{spawnPromisified}from'../common/index.mjs';
2-
import{fixturesDir}from'../common/fixtures.mjs';
2+
import{fixturesDir,fileURLasfixtureSubDir}from'../common/fixtures.mjs';
33
import{match,notStrictEqual}from'node:assert';
44
import{execPath}from'node:process';
55
import{describe,it}from'node:test';
66

77

88
describe('ESM: module not found hint',{concurrency: true},()=>{
99
for(
10-
const{ input, expected }
10+
const{ input, expected, cwd =fixturesDir}
1111
of[
1212
{
1313
input: 'import "./print-error-message"',
14-
// Did you mean to import ../print-error-message.js?
15-
expected: /\.\.\/print-error-message\.js\?/,
14+
// Did you mean to import "./print-error-message.js"?
15+
expected: /"\.\/print-error-message\.js"\?/,
16+
},
17+
{
18+
input: 'import "./es-modules/folder%25with percentage#/index.js"',
19+
// Did you mean to import "./es-modules/folder%2525with%20percentage%23/index.js"?
20+
expected: /"\.\/es-modules\/folder%2525with%20percentage%23\/index\.js"\?/,
21+
},
22+
{
23+
input: 'import "../folder%25with percentage#/index.js"',
24+
// Did you mean to import "../es-modules/folder%2525with%20percentage%23/index.js"?
25+
expected: /"\.\.\/folder%2525with%20percentage%23\/index\.js"\?/,
26+
cwd: fixtureSubDir('es-modules/tla/'),
1627
},
1728
{
1829
input: 'import obj from "some_module/obj"',
19-
expected: /some_module\/obj\.js\?/,
30+
expected: /"some_module\/obj\.js"\?/,
31+
},
32+
{
33+
input: 'import obj from "some_module/folder%25with percentage#/index.js"',
34+
expected: /"some_module\/folder%2525with%20percentage%23\/index\.js"\?/,
35+
},
36+
{
37+
input: 'import "@nodejsscope/pkg/index"',
38+
expected: /"@nodejsscope\/pkg\/index\.js"\?/,
39+
},
40+
{
41+
input: 'import obj from "lone_file.js"',
42+
expected: /node_modules\/lone_file\.js"\?/,
2043
},
2144
]
2245
)it('should cite a variant form',async()=>{
2346
const{ code, stderr }=awaitspawnPromisified(execPath,[
2447
'--input-type=module',
2548
'--eval',
2649
input,
27-
],{
28-
cwd: fixturesDir,
29-
});
50+
],{ cwd });
3051

3152
match(stderr,expected);
3253
notStrictEqual(code,0);

‎test/es-module/test-esm-type-flag-cli-entry.mjs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ describe('--experimental-default-type=module should not support extension search
2626
cwd: fixtures.path('es-modules/package-without-type'),
2727
});
2828

29-
match(stderr,/ENOENT.*Didyoumeantoimport.*index\.js\?/s);
29+
match(stderr,/ENOENT.*Didyoumeantoimport.*index\.js"\?/s);
3030
strictEqual(stdout,'');
3131
strictEqual(code,1);
3232
strictEqual(signal,null);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
'use strict';

‎test/fixtures/node_modules/@nodejsscope/pkg/index.js‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎test/fixtures/node_modules/lone_file.js‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎test/fixtures/node_modules/some_module/folder%25with percentage#/index.js‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
(0)