Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
6c763df7ca fix(resolver): handle jsnext:main field correctly for require() calls
When a CommonJS module uses require() to load a package that has a
jsnext:main field but no explicit main field, Bun was incorrectly
resolving to the ESM entry point specified by jsnext:main. This caused
errors when the ESM module used default exports that CommonJS code
expected to be the module.exports value directly.

The fix extends the existing module/main fallback logic to also handle
jsnext:main - for require() calls, it now falls back to the main field
or index.js, matching Node.js behavior. ESM imports still correctly use
the jsnext:main entry point.

Fixes #26731

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 10:23:07 +00:00
2 changed files with 97 additions and 2 deletions

View File

@@ -3696,8 +3696,14 @@ pub const Resolver = struct {
// If the user did not manually configure a "main" field order, then
// use a special per-module automatic algorithm to decide whether to
// use "module" or "main" based on whether the package is imported
// using "import" or "require".
if (auto_main and strings.eqlComptime(key, "module")) {
// using "import" or "require". The same logic applies for "jsnext:main",
// which is an older equivalent of "module" - for require() calls, we
// should fall back to "main" or index.js to match Node.js behavior.
// Note: For "jsnext:main", we only apply this fallback logic for require()
// calls, since ESM imports should always use the jsnext:main entry point.
const should_check_fallback = strings.eqlComptime(key, "module") or
(strings.eqlComptime(key, "jsnext:main") and kind == ast.ImportKind.require);
if (auto_main and should_check_fallback) {
var absolute_result: ?MatchResult = null;
if (main_field_values.get("main")) |main_rel_path| {

View File

@@ -0,0 +1,89 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
// https://github.com/oven-sh/bun/issues/26731
// Bun incorrectly resolves jsnext:main field for CommonJS require() calls.
// When a CommonJS module requires a package that has jsnext:main but no explicit main,
// Bun should fall back to index.js (like Node.js does), not use the ESM entry point.
test("require() should not resolve jsnext:main field", async () => {
using dir = tempDir("issue-26731", {
"package.json": JSON.stringify({ name: "test", type: "module" }),
// Main entry point (ESM) that imports a CJS package
"index.js": `
import cjsPackage from './cjs-package/index.cjs';
console.log(cjsPackage);
`,
// CJS package that requires a dependency with jsnext:main
"cjs-package/index.cjs": `
const dep = require('../jsnext-dep');
module.exports = dep();
`,
// Dependency with jsnext:main but no explicit main field
// This simulates packages like code-point that have only jsnext:main
"jsnext-dep/package.json": JSON.stringify({
name: "jsnext-dep",
"jsnext:main": "esm.js",
// Note: no "main" field, so it should default to index.js
}),
// ESM entry (should NOT be used for require())
"jsnext-dep/esm.js": `
export default function() { return 'esm'; }
`,
// CJS entry (should be used for require())
"jsnext-dep/index.js": `
module.exports = function() { return 'cjs'; }
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "index.js"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// Should print "cjs" because require() should use index.js, not jsnext:main
expect(stdout.trim()).toBe("cjs");
expect(exitCode).toBe(0);
});
test("import should still resolve jsnext:main field", async () => {
using dir = tempDir("issue-26731-import", {
"package.json": JSON.stringify({ name: "test", type: "module" }),
// ESM import should use jsnext:main
"index.js": `
import dep from './jsnext-dep';
console.log(dep());
`,
// Dependency with jsnext:main
"jsnext-dep/package.json": JSON.stringify({
name: "jsnext-dep",
"jsnext:main": "esm.js",
}),
// ESM entry (should be used for import)
"jsnext-dep/esm.js": `
export default function() { return 'esm'; }
`,
// CJS entry
"jsnext-dep/index.js": `
module.exports = function() { return 'cjs'; }
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "index.js"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// Should print "esm" because import should use jsnext:main
expect(stdout.trim()).toBe("esm");
expect(exitCode).toBe(0);
});