Compare commits

...

4 Commits

Author SHA1 Message Date
RiskyMH
308562c260 fix: correct operator precedence in ESMConditions capacity calculation
The ternary expression 'if (allow_addons) 1 else 0 + conditions.len' was
incorrectly parsed as 'if (allow_addons) 1 else (0 + conditions.len)',
causing conditions.len to only be included when allow_addons is false.

This caused putAssumeCapacity to exceed reserved capacity, triggering
assertions in CI. The bug was pre-existing but became critical when
module-sync was added as a third default condition.
2025-12-01 21:44:43 +11:00
RiskyMH
9c4543db68 Merge remote-tracking branch 'origin/main' into riskymh/module-sync
# Conflicts:
#	docs/runtime/module-resolution.mdx
2025-12-01 16:29:36 +11:00
Michael H
e6b4191deb Merge branch 'main' into riskymh/module-sync 2025-07-04 01:03:35 +10:00
RiskyMH
b8ca277ff8 add module-sync to default export conditions
for bun it doesn't mean as much because we already support require(esm), but for compatibility we should still read it

see https://github.com/nodejs/node/blob/main/doc/api/packages.md#conditional-exports
2025-07-03 00:04:15 +10:00
4 changed files with 304 additions and 6 deletions

View File

@@ -216,6 +216,7 @@ Once it finds the `foo` package, Bun reads the `package.json` to determine how t
"node": "./index.js",
"require": "./index.js", // if importer is CommonJS
"import": "./index.mjs", // if importer is ES module
"module-sync": "./index.mjs",
"default": "./index.js"
}
}

View File

@@ -510,6 +510,7 @@ pub const Target = enum {
array.set(Target.node, &.{
"node",
"module-sync",
});
array.set(Target.browser, &.{
"browser",
@@ -518,15 +519,18 @@ pub const Target = enum {
array.set(Target.bun, &.{
"bun",
"node",
"module-sync",
});
array.set(Target.bake_server_components_ssr, &.{
"bun",
"node",
"module-sync",
});
array.set(Target.bun_macro, &.{
"macro",
"bun",
"node",
"module-sync",
});
break :brk array;
@@ -1133,9 +1137,10 @@ pub const ESMConditions = struct {
var require_condition_map = ConditionsMap.init(allocator);
var style_condition_map = ConditionsMap.init(allocator);
try default_condition_amp.ensureTotalCapacity(defaults.len + 2 + if (allow_addons) 1 else 0 + conditions.len);
try import_condition_map.ensureTotalCapacity(defaults.len + 2 + if (allow_addons) 1 else 0 + conditions.len);
try require_condition_map.ensureTotalCapacity(defaults.len + 2 + if (allow_addons) 1 else 0 + conditions.len);
const addon_count: usize = if (allow_addons) 1 else 0;
try default_condition_amp.ensureTotalCapacity(defaults.len + 2 + addon_count + conditions.len);
try import_condition_map.ensureTotalCapacity(defaults.len + 2 + addon_count + conditions.len);
try require_condition_map.ensureTotalCapacity(defaults.len + 2 + addon_count + conditions.len);
try style_condition_map.ensureTotalCapacity(defaults.len + 2 + conditions.len);
import_condition_map.putAssumeCapacity("import", {});

View File

@@ -1844,10 +1844,10 @@ pub const ESModule = struct {
if (strings.eqlComptime(key, "import")) {
r.module_type.* = .esm;
}
if (strings.eqlComptime(key, "require")) {
} else if (strings.eqlComptime(key, "require")) {
r.module_type.* = .cjs;
} else if (strings.eqlComptime(key, "module-sync")) {
r.module_type.* = .esm;
}
return result;
@@ -2084,6 +2084,8 @@ pub const ESModule = struct {
r.module_type.* = .esm;
} else if (strings.eqlComptime(map_key, "require")) {
r.module_type.* = .cjs;
} else if (strings.eqlComptime(map_key, "module-sync")) {
r.module_type.* = .esm;
}
return result;

View File

@@ -0,0 +1,290 @@
import { describe, expect, it } from "bun:test";
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
describe("module-sync condition", () => {
it("takes precedence over require/import conditions", async () => {
const dir = tempDirWithFiles("module-sync-precedence-test", {
"test-require.js": 'const pkg = require("test-pkg"); console.log(pkg.source);',
"test-import.js": 'import { source } from "test-pkg"; console.log(source);',
"node_modules/test-pkg/package.json": JSON.stringify({
name: "test-pkg",
exports: {
".": {
"module-sync": "./module-sync.mjs",
"import": "./import.mjs",
"require": "./require.cjs",
"default": "./fallback.js",
},
},
}),
"node_modules/test-pkg/module-sync.mjs": "export const source = 'module-sync';",
"node_modules/test-pkg/require.cjs": "module.exports = { source: 'require' };",
"node_modules/test-pkg/import.mjs": "export const source = 'import';",
"node_modules/test-pkg/fallback.js": "export const source = 'fallback';",
});
const { exitCode: reqExit, stdout: reqOut } = Bun.spawnSync({
cmd: [bunExe(), "./test-require.js"],
env: bunEnv,
cwd: dir,
});
expect(reqExit).toBe(0);
expect(reqOut.toString("utf8").trim()).toBe("module-sync");
const { exitCode: impExit, stdout: impOut } = Bun.spawnSync({
cmd: [bunExe(), "./test-import.js"],
env: bunEnv,
cwd: dir,
});
expect(impExit).toBe(0);
expect(impOut.toString("utf8").trim()).toBe("module-sync");
});
it("works with nested conditions", async () => {
const dir = tempDirWithFiles("module-sync-nested-test", {
"test-require.js": 'const pkg = require("test-pkg/sub"); console.log(pkg.source);',
"test-import.js": 'import { source } from "test-pkg/sub"; console.log(source);',
"node_modules/test-pkg/package.json": JSON.stringify({
name: "test-pkg",
exports: {
"./sub": {
"bun": {
"module-sync": "./bun-module-sync.mjs",
"require": "./bun-require.cjs",
"import": "./bun-import.mjs",
},
"node": {
"module-sync": "./node-module-sync.mjs",
"require": "./node-require.cjs",
"import": "./node-import.mjs",
},
"default": "./fallback.js",
},
},
}),
"node_modules/test-pkg/bun-module-sync.mjs": "export const source = 'bun-module-sync';",
"node_modules/test-pkg/bun-require.cjs": "module.exports = { source: 'bun-require' };",
"node_modules/test-pkg/bun-import.mjs": "export const source = 'bun-import';",
"node_modules/test-pkg/node-module-sync.mjs": "export const source = 'node-module-sync';",
"node_modules/test-pkg/node-require.cjs": "module.exports = { source: 'node-require' };",
"node_modules/test-pkg/node-import.mjs": "export const source = 'node-import';",
"node_modules/test-pkg/fallback.js": "export const source = 'fallback';",
});
const { exitCode: reqExit, stdout: reqOut } = Bun.spawnSync({
cmd: [bunExe(), "./test-require.js"],
env: bunEnv,
cwd: dir,
});
expect(reqExit).toBe(0);
expect(reqOut.toString("utf8").trim()).toBe("bun-module-sync");
const { exitCode: impExit, stdout: impOut } = Bun.spawnSync({
cmd: [bunExe(), "./test-import.js"],
env: bunEnv,
cwd: dir,
});
expect(impExit).toBe(0);
expect(impOut.toString("utf8").trim()).toBe("bun-module-sync");
});
it("works in bundler", async () => {
const dir = tempDirWithFiles("module-sync-bundler-test", {
"entry.js": 'import pkg from "test-pkg"; console.log(pkg.source);',
"node_modules/test-pkg/package.json": JSON.stringify({
name: "test-pkg",
main: "./fallback.js",
exports: {
".": {
"module-sync": "./module-sync.mjs",
"require": "./require.cjs",
"import": "./import.mjs",
"default": "./fallback.js",
},
},
}),
"node_modules/test-pkg/module-sync.mjs": "export default { source: 'module-sync' };",
"node_modules/test-pkg/require.cjs": "module.exports = { source: 'require' };",
"node_modules/test-pkg/import.mjs": "export default { source: 'import' };",
"node_modules/test-pkg/fallback.js": "module.exports = { source: 'fallback' };",
});
const { exitCode: bundleExit } = Bun.spawnSync({
cmd: [bunExe(), "build", "./entry.js", "--outdir=./dist"],
env: bunEnv,
cwd: dir,
});
expect(bundleExit).toBe(0);
const { exitCode: runExit, stdout: runOut } = Bun.spawnSync({
cmd: [bunExe(), "./dist/entry.js"],
env: bunEnv,
cwd: dir,
});
expect(runExit).toBe(0);
const output = runOut.toString("utf8").trim();
expect(["module-sync", "import"]).toContain(output);
});
it("works with custom conditions", async () => {
const testDir = tempDirWithFiles("module-sync-with-custom-test", {
"test.js": 'const pkg = require("test-pkg"); console.log(pkg.source);',
"node_modules/test-pkg/package.json": JSON.stringify({
name: "test-pkg",
exports: {
".": {
"custom": "./custom.js",
"module-sync": "./module-sync.mjs",
"require": "./require.cjs",
"import": "./import.mjs",
"default": "./fallback.js",
},
},
}),
"node_modules/test-pkg/custom.js": "module.exports = { source: 'custom' };",
"node_modules/test-pkg/module-sync.mjs": "export const source = 'module-sync';",
"node_modules/test-pkg/require.cjs": "module.exports = { source: 'require' };",
"node_modules/test-pkg/import.mjs": "export const source = 'import';",
"node_modules/test-pkg/fallback.js": "module.exports = { source: 'fallback' };",
});
// With custom condition
{
const { exitCode, stdout } = Bun.spawnSync({
cmd: [bunExe(), "--conditions=custom", "./test.js"],
env: bunEnv,
cwd: testDir,
});
expect(exitCode).toBe(0);
expect(stdout.toString("utf8").trim()).toBe("custom");
}
// Without custom condition
{
const { exitCode, stdout } = Bun.spawnSync({
cmd: [bunExe(), "./test.js"],
env: bunEnv,
cwd: testDir,
});
expect(exitCode).toBe(0);
expect(stdout.toString("utf8").trim()).toBe("module-sync");
}
});
});
describe("require/import conditions", () => {
it("require() uses require condition", async () => {
const dir = tempDirWithFiles("normal-require-test", {
"test-require.js": 'const pkg = require("test-pkg"); console.log(pkg.source);',
"node_modules/test-pkg/package.json": JSON.stringify({
name: "test-pkg",
exports: {
".": {
"require": "./require.cjs",
"import": "./import.mjs",
"default": "./fallback.js",
},
},
}),
"node_modules/test-pkg/require.cjs": "module.exports = { source: 'require' };",
"node_modules/test-pkg/import.mjs": "export const source = 'import';",
"node_modules/test-pkg/fallback.js": "export const source = 'fallback';",
});
const { exitCode, stdout } = Bun.spawnSync({
cmd: [bunExe(), "./test-require.js"],
env: bunEnv,
cwd: dir,
});
expect(exitCode).toBe(0);
expect(stdout.toString("utf8").trim()).toBe("require");
});
it("import() uses import condition", async () => {
const dir = tempDirWithFiles("normal-import-test", {
"test-import.js": 'import { source } from "test-pkg"; console.log(source);',
"node_modules/test-pkg/package.json": JSON.stringify({
name: "test-pkg",
exports: {
".": {
"require": "./require.cjs",
"import": "./import.mjs",
"default": "./fallback.js",
},
},
}),
"node_modules/test-pkg/require.cjs": "module.exports = { source: 'require' };",
"node_modules/test-pkg/import.mjs": "export const source = 'import';",
"node_modules/test-pkg/fallback.js": "export const source = 'fallback';",
});
const { exitCode, stdout } = Bun.spawnSync({
cmd: [bunExe(), "./test-import.js"],
env: bunEnv,
cwd: dir,
});
expect(exitCode).toBe(0);
expect(stdout.toString("utf8").trim()).toBe("import");
});
it("falls back to default", async () => {
const dir = tempDirWithFiles("fallback-test", {
"test-require.js": 'const pkg = require("test-pkg"); console.log(pkg.source);',
"test-import.js": 'import { source } from "test-pkg"; console.log(source);',
"node_modules/test-pkg/package.json": JSON.stringify({
name: "test-pkg",
exports: {
".": {
"worker": "./worker.js",
"default": "./fallback.js",
},
},
}),
"node_modules/test-pkg/worker.js": "export const source = 'worker';",
"node_modules/test-pkg/fallback.js": "export const source = 'fallback';",
});
const { exitCode: reqExit, stdout: reqOut } = Bun.spawnSync({
cmd: [bunExe(), "./test-require.js"],
env: bunEnv,
cwd: dir,
});
expect(reqExit).toBe(0);
expect(reqOut.toString("utf8").trim()).toBe("fallback");
const { exitCode: impExit, stdout: impOut } = Bun.spawnSync({
cmd: [bunExe(), "./test-import.js"],
env: bunEnv,
cwd: dir,
});
expect(impExit).toBe(0);
expect(impOut.toString("utf8").trim()).toBe("fallback");
});
it("bun condition takes precedence", async () => {
const dir = tempDirWithFiles("bun-condition-test", {
"test-require.js": 'const pkg = require("test-pkg"); console.log(pkg.source);',
"node_modules/test-pkg/package.json": JSON.stringify({
name: "test-pkg",
exports: {
".": {
"bun": "./bun.js",
"node": "./node.js",
"default": "./fallback.js",
},
},
}),
"node_modules/test-pkg/bun.js": "module.exports = { source: 'bun' };",
"node_modules/test-pkg/node.js": "module.exports = { source: 'node' };",
"node_modules/test-pkg/fallback.js": "module.exports = { source: 'fallback' };",
});
const { exitCode, stdout } = Bun.spawnSync({
cmd: [bunExe(), "./test-require.js"],
env: bunEnv,
cwd: dir,
});
expect(exitCode).toBe(0);
expect(stdout.toString("utf8").trim()).toBe("bun");
});
});