diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b330cee4a2..b2bbe26c98d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,28 @@ Please never actually write code like this. +* Improve the error message for `null` entries in `exports` ([#3377](https://github.com/evanw/esbuild/issues/3377)) + + Package authors can disable package export paths with the `exports` map in `package.json`. With this release, esbuild now has a clearer error message that points to the `null` token in `package.json` itself instead of to the surrounding context. Here is an example of the new error message: + + ``` + ✘ [ERROR] Could not resolve "msw/browser" + + lib/msw-config.ts:2:28: + 2 │ import { setupWorker } from 'msw/browser'; + ╵ ~~~~~~~~~~~~~ + + The path "./browser" cannot be imported from package "msw" because it was explicitly disabled by + the package author here: + + node_modules/msw/package.json:17:14: + 17 │ "node": null, + ╵ ~~~~ + + You can mark the path "msw/browser" as external to exclude it from the bundle, which will remove + this error and leave the unresolved path in the bundle. + ``` + ## 0.19.2 * Update how CSS nesting is parsed again diff --git a/internal/bundler_tests/bundler_packagejson_test.go b/internal/bundler_tests/bundler_packagejson_test.go index f8f3fb9bf95..435774b6887 100644 --- a/internal/bundler_tests/bundler_packagejson_test.go +++ b/internal/bundler_tests/bundler_packagejson_test.go @@ -2803,3 +2803,41 @@ func TestPackageJsonNodePathsIssue2752(t *testing.T) { }, }) } + +// See: https://github.com/evanw/esbuild/issues/3377 +func TestPackageJsonReversePackageExportsIssue3377(t *testing.T) { + packagejson_suite.expectBundled(t, bundled{ + files: map[string]string{ + "/lib/msw-config.ts": ` + import { setupWorker, type SetupWorker } from 'msw/browser' + setupWorker(); + `, + "/node_modules/msw/package.json": `{ + "exports": { + "./browser": { + "node": null, + "require": "./lib/browser/index.js", + "import": "./lib/browser/index.mjs", + "default": "./lib/browser/index.js" + } + } + }`, + "/node_modules/msw/browser/package.json": `{ + "main": "../lib/browser/index.js", + "module": "../lib/browser/index.mjs" + }`, + "/node_modules/msw/lib/browser/index.js": `TEST FAILURE`, + "/node_modules/msw/lib/browser/index.mjs": `TEST FAILURE`, + }, + entryPaths: []string{"/lib/msw-config.ts"}, + options: config.Options{ + Mode: config.ModeBundle, + AbsOutputFile: "/out.js", + Platform: config.PlatformNode, + }, + expectedScanLog: `lib/msw-config.ts: ERROR: Could not resolve "msw/browser" +node_modules/msw/package.json: NOTE: The path "./browser" cannot be imported from package "msw" because it was explicitly disabled by the package author here: +NOTE: You can mark the path "msw/browser" as external to exclude it from the bundle, which will remove this error and leave the unresolved path in the bundle. +`, + }) +} diff --git a/internal/resolver/package_json.go b/internal/resolver/package_json.go index c95bc5e0310..004c72070fb 100644 --- a/internal/resolver/package_json.go +++ b/internal/resolver/package_json.go @@ -808,6 +808,9 @@ type pjDebug struct { // This is the range of the token to use for error messages token logger.Range + + // If true, the token is a "null" literal + isBecauseOfNullLiteral bool } func (r resolverQuery) esmHandlePostConditions( @@ -892,6 +895,7 @@ func (r resolverQuery) esmPackageExportsResolve( return "", pjStatusInvalidPackageConfiguration, pjDebug{token: exports.firstToken} } + debugToReturn := pjDebug{token: exports.firstToken} if subpath == "." { mainExport := pjEntry{kind: pjNull} if exports.kind == pjString || exports.kind == pjArray || (exports.kind == pjObject && !exports.keysStartWithDot()) { @@ -908,19 +912,23 @@ func (r resolverQuery) esmPackageExportsResolve( resolved, status, debug := r.esmPackageTargetResolve(packageURL, mainExport, "", false, false, conditions) if status != pjStatusNull && status != pjStatusUndefined { return resolved, status, debug + } else { + debugToReturn = debug } } } else if exports.kind == pjObject && exports.keysStartWithDot() { resolved, status, debug := r.esmPackageImportsExportsResolve(subpath, exports, packageURL, false, conditions) if status != pjStatusNull && status != pjStatusUndefined { return resolved, status, debug + } else { + debugToReturn = debug } } if r.debugLogs != nil { r.debugLogs.addNote(fmt.Sprintf("The path %q is not exported", subpath)) } - return "", pjStatusPackagePathNotExported, pjDebug{token: exports.firstToken} + return "", pjStatusPackagePathNotExported, debugToReturn } func (r resolverQuery) esmPackageImportsExportsResolve( @@ -1237,7 +1245,7 @@ func (r resolverQuery) esmPackageTargetResolve( if r.debugLogs != nil { r.debugLogs.addNote(fmt.Sprintf("The path %q is set to null", subpath)) } - return "", pjStatusNull, pjDebug{token: target.firstToken} + return "", pjStatusNull, pjDebug{token: target.firstToken, isBecauseOfNullLiteral: true} } if r.debugLogs != nil { diff --git a/internal/resolver/resolver.go b/internal/resolver/resolver.go index 173ccea2931..77703b0d0ad 100644 --- a/internal/resolver/resolver.go +++ b/internal/resolver/resolver.go @@ -2609,6 +2609,12 @@ func (r resolverQuery) finalizeImportsExportsResult( r.debugMeta.notes = []logger.MsgData{tracker.MsgData(debug.token, why)} case pjStatusPackagePathNotExported: + if debug.isBecauseOfNullLiteral { + r.debugMeta.notes = []logger.MsgData{tracker.MsgData(debug.token, + fmt.Sprintf("The path %q cannot be imported from package %q because it was explicitly disabled by the package author here:", esmPackageSubpath, esmPackageName))} + break + } + r.debugMeta.notes = []logger.MsgData{tracker.MsgData(debug.token, fmt.Sprintf("The path %q is not exported by package %q:", esmPackageSubpath, esmPackageName))} @@ -2706,7 +2712,10 @@ func (r resolverQuery) finalizeImportsExportsResult( } default: - if !didSuggestEnablingCondition { + // Note: Don't suggest the adding the "types" condition because + // TypeScript uses that for type definitions, which are not + // intended to be included in a bundle as executable code + if !didSuggestEnablingCondition && key.Text != "types" { var how string switch logger.API { case logger.CLIAPI: