diff --git a/src/resolver/resolver.rs b/src/resolver/resolver.rs index f0c8cfd7451a..086c684c2a7f 100644 --- a/src/resolver/resolver.rs +++ b/src/resolver/resolver.rs @@ -5001,7 +5001,7 @@ impl<'a> Resolver<'a> { extension_order: options::ExtOrder, out: &mut MatchResult, ) -> MatchStatus { - let mut field_rel_path = _field_rel_path; + let field_rel_path = _field_rel_path; // Is this a directory? if let Some(debug) = self.debug_logs.as_mut() { debug.add_note_fmt(format_args!( @@ -5022,6 +5022,10 @@ impl<'a> Resolver<'a> { }}; } + let mut field_abs_path: &[u8] = self + .fs_ref() + .abs_buf(&[path, field_rel_path], bufs!(field_abs_path)); + if self.care_about_browser_field { // Potentially remap using the "browser" field if let Some(browser_scope) = dir_info.get_enclosing_browser_scope() { @@ -5029,7 +5033,7 @@ impl<'a> Resolver<'a> { if let Some(remap) = self .check_browser_map::<{ BrowserMapPathKind::AbsolutePath }>( &browser_scope, - field_rel_path, + field_abs_path, ) { // Is the path disabled? @@ -5049,13 +5053,14 @@ impl<'a> Resolver<'a> { dec_ret!(MatchStatus::Success); } - field_rel_path = remap; + // `remap` is relative to the package that owns the browser map. + field_abs_path = self + .fs_ref() + .abs_buf(&[browser_scope.abs_path, remap], bufs!(field_abs_path)); } } } } - let _paths = [path, field_rel_path]; - let field_abs_path = self.fs_ref().abs_buf(&_paths, bufs!(field_abs_path)); // Is this a file? if let Some(result) = self.load_as_file(field_abs_path, extension_order) { @@ -5258,16 +5263,18 @@ impl<'a> Resolver<'a> { const FIELD_REL_PATH: &[u8] = b"index"; if let Some(browser_json) = browser_scope.package_json() { + let index_paths = [path, FIELD_REL_PATH]; + let index_abs_path = self.fs_ref().abs_buf(&index_paths, bufs!(remap_path)); if let Some(remap) = self .check_browser_map::<{ BrowserMapPathKind::AbsolutePath }>( &browser_scope, - FIELD_REL_PATH, + index_abs_path, ) { // Is the path disabled? if remap.is_empty() { - let paths = [path, FIELD_REL_PATH]; - let new_path = self.fs_ref().abs_buf(&paths, bufs!(remap_path)); + let new_path = + self.fs_ref().abs_alloc(&index_paths).expect("unreachable"); let mut _path = Path::init(new_path); _path.is_disabled = true; *out = MatchResult { @@ -5281,7 +5288,8 @@ impl<'a> Resolver<'a> { return MatchStatus::Success; } - let new_paths = [path, remap]; + // `remap` is relative to the browser scope, which may be an ancestor of `path`. + let new_paths = [browser_scope.abs_path, remap]; let remapped_abs = self.fs_ref().abs_buf(&new_paths, bufs!(remap_path)); // Is this a file diff --git a/test/bundler/esbuild/packagejson.test.ts b/test/bundler/esbuild/packagejson.test.ts index 8af6f3ec38d7..bee2f930b945 100644 --- a/test/bundler/esbuild/packagejson.test.ts +++ b/test/bundler/esbuild/packagejson.test.ts @@ -2105,4 +2105,149 @@ describe("bundler", () => { stdout: "browser-override", }, }); + // The "main" field value is checked against the browser map before extension + // resolution. Packages like jszip ship + // "main": "./lib/index", + // "browser": {"./lib/index": "./dist/jszip.min.js"} + // and expect the prebuilt bundle to be picked for browser targets. + itBundled("packagejson/BrowserMapMainFieldNoExt", { + files: { + "/Users/user/project/src/entry.js": /* js */ ` + import value from 'demo-pkg' + console.log(value) + `, + "/Users/user/project/node_modules/demo-pkg/package.json": /* json */ ` + { + "main": "./lib/index", + "browser": { + "./lib/index": "./dist/demo-pkg.min.js" + } + } + `, + "/Users/user/project/node_modules/demo-pkg/lib/index.js": `module.exports = 'lib'`, + "/Users/user/project/node_modules/demo-pkg/dist/demo-pkg.min.js": `module.exports = 'prebuilt'`, + }, + target: "browser", + run: { + stdout: "prebuilt", + }, + }); + // Extension matching for main-field browser remaps should match esbuild: + // the main-field value is probed as-is and with extensions appended, so an + // extensionless main still hits a ".js"-keyed browser entry, but a ".js" + // main does not hit an extensionless browser key. + itBundled("packagejson/BrowserMapMainFieldExtPermutations", { + files: { + "/Users/user/project/src/entry.js": /* js */ ` + import a from 'pkg-a' + import b from 'pkg-b' + import c from 'pkg-c' + import d from 'pkg-d' + console.log(a, b, c, d) + `, + "/Users/user/project/node_modules/pkg-a/package.json": `{"main":"./lib/index.js","browser":{"./lib/index.js":"./browser.js"}}`, + "/Users/user/project/node_modules/pkg-a/lib/index.js": `module.exports = 'lib'`, + "/Users/user/project/node_modules/pkg-a/browser.js": `module.exports = 'browser'`, + "/Users/user/project/node_modules/pkg-b/package.json": `{"main":"./lib/index","browser":{"./lib/index.js":"./browser.js"}}`, + "/Users/user/project/node_modules/pkg-b/lib/index.js": `module.exports = 'lib'`, + "/Users/user/project/node_modules/pkg-b/browser.js": `module.exports = 'browser'`, + "/Users/user/project/node_modules/pkg-c/package.json": `{"main":"./lib/index.js","browser":{"./lib/index":"./browser.js"}}`, + "/Users/user/project/node_modules/pkg-c/lib/index.js": `module.exports = 'lib'`, + "/Users/user/project/node_modules/pkg-c/browser.js": `module.exports = 'browser'`, + "/Users/user/project/node_modules/pkg-d/package.json": `{"main":"./lib/index","browser":{"./lib/index":"./browser.js"}}`, + "/Users/user/project/node_modules/pkg-d/lib/index.js": `module.exports = 'lib'`, + "/Users/user/project/node_modules/pkg-d/browser.js": `module.exports = 'browser'`, + }, + target: "browser", + run: { + stdout: "browser browser lib browser", + }, + }); + itBundled("packagejson/BrowserMapMainFieldDisabledNoExt", { + files: { + "/Users/user/project/src/entry.js": /* js */ ` + console.log(require('demo-pkg') === 'lib' ? 'loaded-lib' : 'disabled') + `, + "/Users/user/project/node_modules/demo-pkg/package.json": /* json */ ` + { + "main": "./lib/index", + "browser": { + "./lib/index": false + } + } + `, + "/Users/user/project/node_modules/demo-pkg/lib/index.js": `module.exports = 'lib'`, + }, + target: "browser", + run: { + stdout: "disabled", + }, + }); + // A subpath that resolves to a directory's implicit "index" is checked + // against the enclosing package's browser map, and the remap target is + // resolved relative to that package (not the subdirectory). + itBundled("packagejson/BrowserMapSubpathDirIndex", { + files: { + "/Users/user/project/src/entry.js": /* js */ ` + import value from 'demo-pkg/sub' + console.log(value) + `, + "/Users/user/project/node_modules/demo-pkg/package.json": /* json */ ` + { + "browser": { + "./sub/index.js": "./browser-sub.js" + } + } + `, + "/Users/user/project/node_modules/demo-pkg/sub/index.js": `module.exports = 'sub-index'`, + "/Users/user/project/node_modules/demo-pkg/browser-sub.js": `module.exports = 'browser-sub'`, + }, + target: "browser", + run: { + stdout: "browser-sub", + }, + }); + itBundled("packagejson/BrowserMapSubpathDirIndexDisabled", { + files: { + "/Users/user/project/src/entry.js": /* js */ ` + console.log(require('demo-pkg/sub') === 'sub-index' ? 'loaded' : 'disabled') + `, + "/Users/user/project/node_modules/demo-pkg/package.json": /* json */ ` + { + "browser": { + "./sub/index": false + } + } + `, + "/Users/user/project/node_modules/demo-pkg/sub/index.js": `module.exports = 'sub-index'`, + }, + target: "browser", + run: { + stdout: "disabled", + }, + }); + // When "main" points at a directory, the implicit "