From d0fe577b2766b7fd4cbbbd535e0152551a001500 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Mon, 8 Feb 2021 22:11:05 +0100 Subject: [PATCH] add some better hints when resolving fails --- lib/NormalModuleFactory.js | 159 +++++++++++++++--- .../cases/errors/module-request/dependency.js | 1 + test/cases/errors/module-request/errors.js | 3 + test/cases/errors/module-request/index.js | 5 + test/cases/errors/module-request/module.mjs | 1 + 5 files changed, 146 insertions(+), 23 deletions(-) create mode 100644 test/cases/errors/module-request/dependency.js create mode 100644 test/cases/errors/module-request/errors.js create mode 100644 test/cases/errors/module-request/index.js create mode 100644 test/cases/errors/module-request/module.mjs diff --git a/lib/NormalModuleFactory.js b/lib/NormalModuleFactory.js index 2e347be21..9b52203c3 100644 --- a/lib/NormalModuleFactory.js +++ b/lib/NormalModuleFactory.js @@ -750,35 +750,148 @@ class NormalModuleFactory extends ModuleFactory { resolveContext, (err, resolvedResource, resolvedResourceResolveData) => { if (err) { - if (resolver.options.fullySpecified) { - resolver - .withOptions({ - fullySpecified: false - }) - .resolve( - contextInfo, - context, - unresolvedResource, - resolveContext, - (err2, resolvedResource) => { - if (!err2 && resolvedResource) { - const resource = parseResource( - resolvedResource - ).path.replace(/^.*[\\/]/, ""); - err.message += ` -Did you mean '${resource}'? + return this._resolveResourceErrorHints( + err, + contextInfo, + context, + unresolvedResource, + resolver, + resolveContext, + (err2, hints) => { + if (err2) { + err.message += ` +An fatal error happened during resolving additional hints for this error: ${err2.message}`; + err.stack += ` + +An fatal error happened during resolving additional hints for this error: +${err2.stack}`; + return callback(err); + } + if (hints && hints.length > 0) { + err.message += ` +${hints.join("\n\n")}`; + } + callback(err); + } + ); + } + callback(err, resolvedResource, resolvedResourceResolveData); + } + ); + } + + _resolveResourceErrorHints( + error, + contextInfo, + context, + unresolvedResource, + resolver, + resolveContext, + callback + ) { + asyncLib.parallel( + [ + callback => { + if (!resolver.options.fullySpecified) return callback(); + resolver + .withOptions({ + fullySpecified: false + }) + .resolve( + contextInfo, + context, + unresolvedResource, + resolveContext, + (err, resolvedResource) => { + if (!err && resolvedResource) { + const resource = parseResource(resolvedResource).path.replace( + /^.*[\\/]/, + "" + ); + return callback( + null, + `Did you mean '${resource}'? BREAKING CHANGE: The request '${unresolvedResource}' failed to resolve only because it was resolved as fully specified (probably because the origin is a '*.mjs' file or a '*.js' file where the package.json contains '"type": "module"'). The extension in the request is mandatory for it to be fully specified. -Add the extension to the request.`; - } - callback(err); +Add the extension to the request.` + ); } - ); - return; + callback(); + } + ); + }, + callback => { + if (!resolver.options.enforceExtension) return callback(); + resolver + .withOptions({ + enforceExtension: false, + extensions: [] + }) + .resolve( + contextInfo, + context, + unresolvedResource, + resolveContext, + (err, resolvedResource) => { + if (!err && resolvedResource) { + let hint = ""; + const match = /(\.[^.]+)(\?|$)/.exec(unresolvedResource); + if (match) { + const fixedRequest = unresolvedResource.replace( + /(\.[^.]+)(\?|$)/, + "$2" + ); + if (resolver.options.extensions.has(match[1])) { + hint = `Did you mean '${fixedRequest}'?`; + } else { + hint = `Did you mean '${fixedRequest}'? Also note that '${match[1]}' is not in 'resolve.extensions' yet and need to be added for this to work?`; + } + } else { + hint = `Did you mean to omit the extension or to remove 'resolve.enforceExtension'?`; + } + return callback( + null, + `The request '${unresolvedResource}' failed to resolve only because 'resolve.enforceExtension' was specified. +${hint} +Including the extension in the request is no longer possible. Did you mean to enforce including the extension in requests with 'resolve.extensions: []' instead?` + ); + } + callback(); + } + ); + }, + callback => { + if ( + /^\.\.?\//.test(unresolvedResource) || + resolver.options.preferRelative + ) { + return callback(); } + resolver.resolve( + contextInfo, + context, + `./${unresolvedResource}`, + resolveContext, + (err, resolvedResource) => { + if (err || !resolvedResource) return callback(); + const moduleDirectories = resolver.options.modules + .map(m => (Array.isArray(m) ? m.join(", ") : m)) + .join(", "); + callback( + null, + `Did you mean './${unresolvedResource}'? +Requests that should resolve in the current directory need to start with './'. +Requests that start with a name are treated as module requests and resolve within module directories (${moduleDirectories}). +If changing the source code is not an option there is also a resolve options called 'preferRelative' which tries to resolve these kind of requests in the current directory too.` + ); + } + ); } - callback(err, resolvedResource, resolvedResourceResolveData); + ], + (err, hints) => { + if (err) return callback(err); + callback(null, hints.filter(Boolean)); } ); } diff --git a/test/cases/errors/module-request/dependency.js b/test/cases/errors/module-request/dependency.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/test/cases/errors/module-request/dependency.js @@ -0,0 +1 @@ +export {}; diff --git a/test/cases/errors/module-request/errors.js b/test/cases/errors/module-request/errors.js new file mode 100644 index 000000000..3a9f5a9a1 --- /dev/null +++ b/test/cases/errors/module-request/errors.js @@ -0,0 +1,3 @@ +module.exports = [ + [/Can't resolve 'dependency\.js'/, /Did you mean '\.\/dependency\.js'\?/] +]; diff --git a/test/cases/errors/module-request/index.js b/test/cases/errors/module-request/index.js new file mode 100644 index 000000000..52b1155ca --- /dev/null +++ b/test/cases/errors/module-request/index.js @@ -0,0 +1,5 @@ +it("should not resolve module requests relative", async () => { + await expect(import("./module.mjs")).rejects.toMatchObject({ + code: "MODULE_NOT_FOUND" + }); +}); diff --git a/test/cases/errors/module-request/module.mjs b/test/cases/errors/module-request/module.mjs new file mode 100644 index 000000000..2fbe53360 --- /dev/null +++ b/test/cases/errors/module-request/module.mjs @@ -0,0 +1 @@ +import "dependency.js";