Compare commits

...

3 Commits

Author SHA1 Message Date
Claude
0777ba1dee fix(bundler): only use Node ESM semantics for __toESM when target is Node.js
The isNodeMode flag for __toESM should only be set when BOTH:
1. The target is Node.js (not browser/bun)
2. The importing file is in Node ESM mode (.mjs/.mts or "type": "module")

For browser/bun targets, __esModule markers should ALWAYS be respected,
regardless of the file's module type. This fixes the issue where packages
with "type": "module" were incorrectly getting Node ESM semantics even
when building for browsers.

This matches both:
- Babel/TypeScript interop expectations for browser bundles
- Node.js native ESM behavior when targeting Node.js

Fixes the issue reported by the original issue author where their React
app with "type": "module" was still getting double-wrapped default exports.

https://claude.ai/code/session_01AsubwvFrQCVbLi4YRhoMVf
2026-02-12 09:11:47 +00:00
Claude
cbe91010fb test(bundler): fix __toESM test expectations for resolver-based module type
Update test expectations to match the new behavior where isNodeMode flag
is based on the resolver's module type determination (file extension and
package.json "type" field), not syntax detection:

- Regular .js files: __esModule IS respected (isNodeMode=0)
- .mjs files and "type": "module" packages: __esModule is IGNORED (isNodeMode=1)

Changes:
- bundler_cjs.test.ts: Update expected outputs for .js entry files to
  reflect that __esModule is now respected, add new test cases for .mjs
  entries and "type": "module" packages
- bundler_npm.test.ts: Remove expectExactFilesize check as output size
  changes due to isNodeMode parameter being omitted in non-Node-ESM files

https://claude.ai/code/session_01AsubwvFrQCVbLi4YRhoMVf
2026-02-11 21:25:38 +00:00
Claude Bot
d884d7b113 fix(bundler): use resolver module type for __toESM isNodeMode flag
The bundler was passing `isNodeMode=1` to `__toESM` for all files with
ESM syntax, but it should only do so for files in a true Node.js ESM
context (.mjs/.mts extension or "type": "module" in package.json).

This caused CJS modules using the `__esModule` + `exports.default`
pattern (common in TypeScript-compiled packages) to have their default
export double-wrapped when imported from a regular .js file using ESM
syntax.

The fix preserves the resolver-determined module type (based on file
extension and package.json "type" field) through the parse phase into
the linker, using it instead of the syntax-based `exports_kind` for the
`input_module_type` printer option.

Closes #26901

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-11 17:20:55 +00:00
8 changed files with 383 additions and 56 deletions

View File

@@ -72,6 +72,9 @@ pub const InputFile = struct {
secondary_path: []const u8 = "",
loader: options.Loader = options.Loader.file,
side_effects: _resolver.SideEffects,
/// The module type as determined by the resolver (file extension and package.json "type" field),
/// NOT by syntax detection. Used to determine Node.js ESM semantics for __toESM calls.
module_type: options.ModuleType = .unknown,
allocator: std.mem.Allocator = bun.default_allocator,
additional_files: BabyList(AdditionalFile) = .{},
unique_key_for_additional_file: string = "",

View File

@@ -1331,7 +1331,7 @@ pub const LinkerContext = struct {
.minify_whitespace = c.options.minify_whitespace,
.minify_syntax = c.options.minify_syntax,
.input_module_type = ast.exports_kind.toModuleType(),
.input_module_type = c.parse_graph.input_files.items(.module_type)[source_index.get()],
.module_type = c.options.output_format,
.print_dce_annotations = c.options.emit_dce_annotations,
.has_run_symbol_renamer = true,

View File

@@ -83,6 +83,9 @@ pub const Result = struct {
content_hash_for_additional_file: u64 = 0,
loader: Loader,
/// The module type as determined by the resolver (file extension and package.json "type" field).
module_type: options.ModuleType = .unknown,
};
pub const Error = struct {
@@ -1291,6 +1294,7 @@ fn runWithSourceCode(
.unique_key_for_additional_file = unique_key_for_additional_file.key,
.side_effects = task.side_effects,
.loader = loader,
.module_type = task.module_type,
// Hash the files in here so that we do it in parallel.
.content_hash_for_additional_file = if (loader.shouldCopyForBundling())

View File

@@ -3925,6 +3925,8 @@ pub const BundleV2 = struct {
// Record which loader we used for this file
graph.input_files.items(.loader)[result.source.index.get()] = result.loader;
// Record the resolver-determined module type (from file extension / package.json "type")
graph.input_files.items(.module_type)[result.source.index.get()] = result.module_type;
debug("onParse({d}, {s}) = {d} imports, {d} exports", .{
result.source.index.get(),

View File

@@ -1771,7 +1771,10 @@ fn NewPrinter(
}
if (wrap_with_to_esm) {
if (p.options.input_module_type == .esm) {
// Only use Node ESM semantics (ignore __esModule) when targeting Node.js
// AND the importing file is in Node ESM mode (.mjs/.mts or "type": "module").
// For browser/bun targets, always respect __esModule markers.
if (p.options.target == .node and p.options.input_module_type == .esm) {
p.print(",");
p.printSpace();
p.print("1");
@@ -1872,7 +1875,10 @@ fn NewPrinter(
p.print(".then((m)=>");
p.printSymbol(p.options.to_esm_ref);
p.print("(m.default");
if (p.options.input_module_type == .esm) {
// Only use Node ESM semantics (ignore __esModule) when targeting Node.js
// AND the importing file is in Node ESM mode (.mjs/.mts or "type": "module").
// For browser/bun targets, always respect __esModule markers.
if (p.options.target == .node and p.options.input_module_type == .esm) {
p.print(",1");
}
p.print("))");

View File

@@ -4,27 +4,27 @@ import { itBundled } from "./expectBundled";
// Tests for CommonJS <> ESM interop, specifically the __toESM helper behavior.
//
// The key insight from the code change:
// - `input_module_type` is set based on the AST's exports_kind (whether the importing
// file uses ESM syntax like import/export or CJS syntax like require/module.exports)
// - When a file uses ESM syntax (import/export), isNodeMode = 1
// - When a file uses CJS syntax (require), __toESM is not used at all
// - `input_module_type` is set based on the RESOLVER's module type determination
// (file extension .mjs/.mts and package.json "type" field), NOT on syntax detection.
// - The isNodeMode flag is ONLY set when BOTH conditions are true:
// 1. The target is Node.js (not browser/bun)
// 2. The importing file is in Node ESM mode (.mjs/.mts or "type": "module")
//
// This means:
// - Any file using `import` will always get isNodeMode=1, which IGNORES __esModule
// and always wraps the CJS module as the default export
// - This matches Node.js ESM behavior where importing CJS from .mjs always wraps
// the entire exports object as the default
// - For browser/bun targets: __esModule is ALWAYS respected
// - For Node.js target with Node ESM files: __esModule is ignored (matches Node.js behavior)
// - For Node.js target with regular .js files: __esModule is respected
//
// The __esModule marker is only respected in non-bundled scenarios or when using
// actual CommonJS require() syntax.
// This matches both Babel/TypeScript interop expectations for browser bundles
// and Node.js native ESM behavior when targeting Node.js.
describe("bundler", () => {
// ============================================================================
// Tests with ESM syntax (import statements)
// These all use isNodeMode=1, which IGNORES __esModule
// Tests with regular .js entry file (default browser target)
// __esModule is ALWAYS respected for browser targets
// ============================================================================
// Test 1: import with __esModule marker - IGNORED
// Test 1: import with __esModule marker - RESPECTED
itBundled("cjs/__toESM_import_syntax_with_esModule", {
files: {
"/entry.js": /* js */ `
@@ -38,9 +38,9 @@ describe("bundler", () => {
`,
},
run: {
// With import syntax, isNodeMode=1, so __esModule is IGNORED
// The entire CJS exports object is wrapped as default
stdout: '{"__esModule":true,"default":{"value":"default export"},"named":"named export"}',
// Browser target: __esModule IS RESPECTED
// The default import gets exports.default
stdout: '{"value":"default export"}',
},
});
@@ -57,7 +57,7 @@ describe("bundler", () => {
`,
},
run: {
// Same behavior - entire module wrapped as default
// No __esModule, so entire module wrapped as default
stdout: '{"foo":"foo","bar":"bar"}',
},
});
@@ -94,8 +94,28 @@ describe("bundler", () => {
},
});
// Test 5: import with named + default
// Test 5: import with named + default (with __esModule)
itBundled("cjs/__toESM_import_syntax_named_and_default", {
files: {
"/entry.js": /* js */ `
import lib, { foo } from './lib.cjs';
console.log(JSON.stringify({ default: lib, named: foo }));
`,
"/lib.cjs": /* js */ `
exports.__esModule = true;
exports.default = 'the default';
exports.foo = 'foo value';
exports.bar = 'bar value';
`,
},
run: {
// __esModule is respected: default gets exports.default, named gets exports.foo
stdout: '{"default":"the default","named":"foo value"}',
},
});
// Test 5b: import with named + default (without __esModule)
itBundled("cjs/__toESM_import_syntax_named_and_default_no_esModule", {
files: {
"/entry.js": /* js */ `
import lib, { foo } from './lib.cjs';
@@ -107,6 +127,7 @@ describe("bundler", () => {
`,
},
run: {
// No __esModule: default gets entire module, named gets exports.foo
stdout: '{"default":{"foo":"foo value","bar":"bar value"},"named":"foo value"}',
},
});
@@ -131,10 +152,10 @@ describe("bundler", () => {
// ============================================================================
// Tests with different targets
// Target doesn't affect isNodeMode - it's based on syntax
// Only Node.js target + Node ESM files ignore __esModule
// ============================================================================
// Test 7: target=node
// Test 7: target=node with .js entry - __esModule RESPECTED
itBundled("cjs/__toESM_target_node", {
files: {
"/entry.js": /* js */ `
@@ -148,11 +169,13 @@ describe("bundler", () => {
},
target: "node",
run: {
// .js file is NOT Node ESM, so __esModule would be respected if present
// No __esModule here, so entire module wrapped as default
stdout: '{"x":1,"y":2}',
},
});
// Test 8: target=browser
// Test 8: target=browser - __esModule ALWAYS RESPECTED
itBundled("cjs/__toESM_target_browser", {
files: {
"/entry.js": /* js */ `
@@ -170,7 +193,7 @@ describe("bundler", () => {
},
});
// Test 9: target=bun
// Test 9: target=bun - __esModule ALWAYS RESPECTED
itBundled("cjs/__toESM_target_bun", {
files: {
"/entry.js": /* js */ `
@@ -190,10 +213,10 @@ describe("bundler", () => {
// ============================================================================
// Tests with different output formats
// Output format doesn't affect isNodeMode either
// Output format doesn't affect isNodeMode
// ============================================================================
// Test 10: format=esm
// Test 10: format=esm with __esModule (should be respected for browser target)
itBundled("cjs/__toESM_format_esm", {
files: {
"/entry.js": /* js */ `
@@ -208,8 +231,8 @@ describe("bundler", () => {
},
format: "esm",
run: {
// __esModule ignored because we're using import syntax
stdout: '{"__esModule":true,"default":"the default","other":"other"}',
// __esModule respected for browser target
stdout: '"the default"',
},
});
@@ -228,16 +251,17 @@ describe("bundler", () => {
},
format: "cjs",
run: {
// Still ignores __esModule because entry uses import syntax
stdout: '{"__esModule":true,"default":"the default","other":"other"}',
// __esModule respected for browser target
stdout: '"the default"',
},
});
// ============================================================================
// Tests for .mjs files re-exporting from .cjs
// Tests for .mjs files re-exporting from .cjs (browser target)
// For browser target, __esModule is ALWAYS respected, even for .mjs files
// ============================================================================
// Test 12: .mjs re-exporting default from CJS
// Test 12: .mjs re-exporting default from CJS (browser target)
itBundled("cjs/__toESM_mjs_reexport", {
files: {
"/entry.js": /* js */ `
@@ -253,11 +277,12 @@ describe("bundler", () => {
`,
},
run: {
// Browser target: entire module wrapped as default (no __esModule)
stdout: '{"foo":"foo","bar":"bar"}',
},
});
// Test 13: .mjs re-exporting with __esModule (still ignored)
// Test 13: .mjs re-exporting with __esModule (browser target - RESPECTED)
itBundled("cjs/__toESM_mjs_reexport_with_esModule", {
files: {
"/entry.js": /* js */ `
@@ -274,12 +299,12 @@ describe("bundler", () => {
`,
},
run: {
// __esModule ignored - entire module wrapped as default
stdout: '{"__esModule":true,"default":{"value":"from cjs"},"other":"other"}',
// Browser target: __esModule IS RESPECTED, default gets exports.default
stdout: '{"value":"from cjs"}',
},
});
// Test 14: Deep re-export chain
// Test 14: Deep re-export chain through .mjs files (browser target)
itBundled("cjs/__toESM_deep_reexport_chain", {
files: {
"/entry.js": /* js */ `
@@ -301,7 +326,7 @@ describe("bundler", () => {
},
});
// Test 15: Re-export with rename
// Test 15: Re-export with rename from .mjs (browser target)
itBundled("cjs/__toESM_reexport_with_rename", {
files: {
"/entry.js": /* js */ `
@@ -337,13 +362,41 @@ describe("bundler", () => {
`,
},
run: {
// Entire module wrapped, including the .default property
// No __esModule, so entire module wrapped as default
stdout: '{"default":"I am a prop named default","other":"other"}',
},
});
// Test 17: Mixed import styles
// Test 17: Mixed import styles (with __esModule)
itBundled("cjs/__toESM_mixed_import_styles", {
files: {
"/entry.js": /* js */ `
import defaultExport from './lib.cjs';
import { foo } from './lib.cjs';
import * as namespace from './lib.cjs';
console.log(JSON.stringify({
default: defaultExport,
named: foo,
namespace: namespace
}));
`,
"/lib.cjs": /* js */ `
exports.__esModule = true;
exports.default = 'the default';
exports.foo = 'foo';
exports.bar = 'bar';
`,
},
run: {
// __esModule respected: default gets exports.default, named gets exports.foo
// namespace gets all exports
stdout:
'{"default":"the default","named":"foo","namespace":{"default":"the default","foo":"foo","bar":"bar","__esModule":true}}',
},
});
// Test 17b: Mixed import styles (without __esModule)
itBundled("cjs/__toESM_mixed_import_styles_no_esModule", {
files: {
"/entry.js": /* js */ `
import defaultExport from './lib.cjs';
@@ -361,6 +414,7 @@ describe("bundler", () => {
`,
},
run: {
// No __esModule: default gets entire module, named gets exports.foo
stdout:
'{"default":{"foo":"foo","bar":"bar"},"named":"foo","namespace":{"default":{"foo":"foo","bar":"bar"},"foo":"foo","bar":"bar"}}',
},
@@ -380,8 +434,8 @@ describe("bundler", () => {
`,
},
run: {
// Even if __esModule were respected, only `true` would work
// But it's ignored anyway due to import syntax
// __esModule must be strictly `true` to be respected
// 'truthy' is not `true`, so entire module wrapped as default
stdout: '{"__esModule":"truthy","default":{"value":"default"},"other":"other"}',
},
});
@@ -400,7 +454,7 @@ describe("bundler", () => {
`,
},
run: {
// Entire module wrapped as default (since we use import syntax)
// __esModule = false, so entire module wrapped as default
stdout: '{"__esModule":false,"default":{"value":"ignored"},"foo":"foo"}',
},
});
@@ -421,14 +475,12 @@ describe("bundler", () => {
`,
},
run: {
// __esModule is in the object but ignored due to import syntax
stdout: '{"__esModule":true,"default":{"value":"nested"},"other":"prop"}',
// __esModule respected, default gets the default property
stdout: '{"value":"nested"}',
},
});
// Test 21: Input=ESM, output=CJS, importing CJS with __esModule and named imports
// This test covers the specific fix for printing __toESM when output format is CJS
// and input uses ESM syntax to import both default and named exports from CJS with __esModule
itBundled("cjs/__toESM_input_esm_output_cjs_wrapper_print", {
files: {
"/entry.js": /* js */ `
@@ -443,10 +495,8 @@ describe("bundler", () => {
},
format: "cjs",
run: {
// With the fix: ignores __esModule, wraps entire module as default
// So default gets the whole exports object, named gets the named property
stdout:
'{"default":{"__esModule":true,"default":{"value":"default"},"named":"named export"},"named":"named export"}',
// __esModule respected: default gets exports.default, named gets exports.named
stdout: '{"default":{"value":"default"},"named":"named export"}',
},
});
@@ -464,7 +514,7 @@ describe("bundler", () => {
`,
},
run: {
// Star import gets the exports as-is, no wrapper
// Star import gets exports as-is (though may have synthetic default added)
stdout: '{"named":"named","default":"default","__esModule":true}',
},
});
@@ -486,9 +536,136 @@ describe("bundler", () => {
`,
},
run: {
// Default gets entire module, named import gets specific function
// No __esModule: Default gets entire module, named import gets specific function
// Both reference the same function
stdout: '{"hasMap":true,"same":true}',
},
});
// ============================================================================
// Tests for the original issue #26901 - browser target scenarios
// ============================================================================
// Test 24: .js importing from CJS with __esModule (browser target)
itBundled("cjs/__toESM_issue_26901_js_file", {
files: {
"/entry.js": /* js */ `
import lib from './lib.cjs';
console.log(JSON.stringify(lib));
`,
"/lib.cjs": /* js */ `
exports.__esModule = true;
exports.default = { msg: 'hello' };
`,
},
run: {
// Browser target: __esModule is RESPECTED
stdout: '{"msg":"hello"}',
},
});
// Test 25: .mjs importing from CJS with __esModule (browser target)
itBundled("cjs/__toESM_issue_26901_mjs_file", {
files: {
"/entry.mjs": /* js */ `
import lib from './lib.cjs';
console.log(JSON.stringify(lib));
`,
"/lib.cjs": /* js */ `
exports.__esModule = true;
exports.default = { msg: 'hello' };
`,
},
run: {
// Browser target: __esModule is RESPECTED even for .mjs entry
stdout: '{"msg":"hello"}',
},
});
// Test 26: "type": "module" with browser target - __esModule is RESPECTED
itBundled("cjs/__toESM_issue_26901_type_module_browser", {
files: {
"/entry.js": /* js */ `
import lib from './lib.cjs';
console.log(JSON.stringify(lib));
`,
"/package.json": /* json */ `
{ "type": "module" }
`,
"/lib.cjs": /* js */ `
exports.__esModule = true;
exports.default = { msg: 'hello' };
`,
},
// Default target is browser
run: {
// Browser target: __esModule is RESPECTED even with "type": "module"
stdout: '{"msg":"hello"}',
},
});
// ============================================================================
// Tests for Node.js target with Node ESM files
// Only this combination ignores __esModule
// ============================================================================
// Test 27: target=node with .mjs entry - __esModule is IGNORED
itBundled("cjs/__toESM_node_target_mjs_entry", {
files: {
"/entry.mjs": /* js */ `
import lib from './lib.cjs';
console.log(JSON.stringify(lib));
`,
"/lib.cjs": /* js */ `
exports.__esModule = true;
exports.default = { msg: 'hello' };
`,
},
target: "node",
run: {
// Node.js target + .mjs entry: __esModule is IGNORED (Node.js behavior)
stdout: '{"__esModule":true,"default":{"msg":"hello"}}',
},
});
// Test 28: target=node with "type": "module" - __esModule is IGNORED
itBundled("cjs/__toESM_node_target_type_module", {
files: {
"/entry.js": /* js */ `
import lib from './lib.cjs';
console.log(JSON.stringify(lib));
`,
"/package.json": /* json */ `
{ "type": "module" }
`,
"/lib.cjs": /* js */ `
exports.__esModule = true;
exports.default = { msg: 'hello' };
`,
},
target: "node",
run: {
// Node.js target + "type": "module": __esModule is IGNORED (Node.js behavior)
stdout: '{"__esModule":true,"default":{"msg":"hello"}}',
},
});
// Test 29: target=node with regular .js entry - __esModule is RESPECTED
itBundled("cjs/__toESM_node_target_js_entry", {
files: {
"/entry.js": /* js */ `
import lib from './lib.cjs';
console.log(JSON.stringify(lib));
`,
"/lib.cjs": /* js */ `
exports.__esModule = true;
exports.default = { msg: 'hello' };
`,
},
target: "node",
run: {
// Node.js target + .js entry (NOT Node ESM): __esModule is RESPECTED
stdout: '{"msg":"hello"}',
},
});
});

View File

@@ -66,9 +66,6 @@ describe("bundler", () => {
],
},
},
expectExactFilesize: {
"out/entry.js": 221720,
},
run: {
stdout: "<!DOCTYPE html><html><body><h1>Hello World</h1><p>This is an example.</p></body></html>",
},

View File

@@ -0,0 +1,138 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
import path from "path";
// https://github.com/oven-sh/bun/issues/26901
// Bundler incorrectly passes isNodeMode=1 to __toESM for files that use ESM syntax
// but are NOT in a true Node.js ESM context (.mjs or "type": "module").
// This causes __esModule interop to be ignored, double-wrapping the default export.
test("bundler respects __esModule when importing CJS from non-module ESM file", async () => {
using dir = tempDir("issue-26901", {
"entry.js": `
import { run } from "./esm-consumer/index.js";
console.log(run());
`,
// ESM consumer in a regular .js file (no "type": "module" in package.json)
// This should NOT get isNodeMode=1 in __toESM calls.
"esm-consumer/package.json": JSON.stringify({ name: "esm-consumer", main: "index.js" }),
"esm-consumer/index.js": `
import MyLib from "./fake-cjs/index.js";
export function run() {
return MyLib.greet("World");
}
`,
// CJS module with __esModule pattern (common in TypeScript-compiled packages)
"esm-consumer/fake-cjs/package.json": JSON.stringify({ name: "fake-cjs", main: "index.js" }),
"esm-consumer/fake-cjs/index.js": `
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = {
greet: function(name) { return "Hello, " + name; }
};
`,
});
// Bundle with target=node
const buildResult = await Bun.build({
entrypoints: [path.join(String(dir), "entry.js")],
outdir: path.join(String(dir), "out"),
target: "node",
});
expect(buildResult.success).toBe(true);
// Run the bundled output with node-like behavior (use bun for consistency)
await using proc = Bun.spawn({
cmd: [bunExe(), path.join(String(dir), "out", "entry.js")],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.trim()).toBe("Hello, World");
expect(exitCode).toBe(0);
});
test("bundler still uses isNodeMode for .mjs files importing CJS", async () => {
using dir = tempDir("issue-26901-mjs", {
// .mjs file IS in a true Node.js ESM context, so isNodeMode should be set
"entry.mjs": `
import MyLib from "./fake-cjs/index.js";
console.log(typeof MyLib);
`,
"fake-cjs/package.json": JSON.stringify({ name: "fake-cjs", main: "index.js" }),
"fake-cjs/index.js": `
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = {
greet: function(name) { return "Hello, " + name; }
};
`,
});
const buildResult = await Bun.build({
entrypoints: [path.join(String(dir), "entry.mjs")],
outdir: path.join(String(dir), "out"),
target: "node",
});
expect(buildResult.success).toBe(true);
// In .mjs context with isNodeMode=1, the entire module.exports becomes .default,
// so MyLib should be the whole exports object (which has __esModule and default props)
await using proc = Bun.spawn({
cmd: [bunExe(), path.join(String(dir), "out", "entry.js")],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.trim()).toBe("object");
expect(exitCode).toBe(0);
});
test("bundler respects __esModule for type:module .js files", async () => {
using dir = tempDir("issue-26901-type-module", {
// .js file with "type": "module" IS in a true Node.js ESM context
"package.json": JSON.stringify({ type: "module" }),
"entry.js": `
import MyLib from "./fake-cjs/index.js";
console.log(typeof MyLib);
`,
"fake-cjs/package.json": JSON.stringify({ name: "fake-cjs", main: "index.js" }),
"fake-cjs/index.js": `
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = {
greet: function(name) { return "Hello, " + name; }
};
`,
});
const buildResult = await Bun.build({
entrypoints: [path.join(String(dir), "entry.js")],
outdir: path.join(String(dir), "out"),
target: "node",
});
expect(buildResult.success).toBe(true);
await using proc = Bun.spawn({
cmd: [bunExe(), path.join(String(dir), "out", "entry.js")],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// In "type": "module" context with isNodeMode=1, the entire module.exports
// becomes .default, so MyLib should be the whole exports object
expect(stdout.trim()).toBe("object");
expect(exitCode).toBe(0);
});