Skip to content

bundler: keep exports.default as the default import of a bun: module in CommonJS output - #41953

Open
robobun wants to merge 2 commits into
mainfrom
robobun/811841e7/bun-builtin-default-import-cjs
Open

robobun wants to merge 2 commits into
mainfrom
robobun/811841e7/bun-builtin-default-import-cjs

Conversation

@robobun

@robobun robobun commented Sep 8, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • Since 1.4.1, bun build --target=bun --format=cjs of import Database from "bun:sqlite" in an ES module by type (.mjs, .mts, "type": "module") prints __toESM(require("bun:sqlite"), 1). new Database() throws TypeError: Object is not a constructor (evaluating 'new import_bun_sqlite.default(":memory:")'). The build exits 0. Also with --format=iife, --bytecode, --target=node.
  • Cause: bundler: pick the __toESM interop from the importer's module type #41150 closes every external __toESM(require(..)) with , 1 (isNodeMode) for such an importer (src/js_printer/lib.rs:2994). isNodeMode ignores __esModule, which require("bun:sqlite") sets (src/js/bun/sqlite.ts:742).

Fix

  • The external require() path prints no , 1 when the specifier starts with bun:. The default import is exports.default, as in 1.4.0.
  • Correct because only Bun loads a bun: module, and Bun's loader makes an own default of a built-in's exports the ES default export for every importer (generateInternalModuleSourceCode, src/jsc/bindings/ModuleLoader.cpp:72).
  • Verified: test/bundler/bundler_cjs.test.ts (3 new tests, all fail on main). Other suites: Notes.

Background

  • __toESM(mod, isNodeMode) builds the ES module view of a require() result. Without isNodeMode, default is mod.default when mod.__esModule is set. With it, default is mod, as in Node.
  • Considered one linker function that sets isNodeMode per import record. It waits on a decision about externals (see the comments). This check is the smallest change that restores 1.4.0.

Downsides

  • Not fixed: a type: "sqlite" file import and the default import of an external ES module package still get , 1 and give the module object.
  • Other bun: imports lose , 1 too: 3 bytes each, same value (12 of 258 corpus outputs).
  • Cost: .text +512 bytes. One printed __toESM(require()) runs 876 instructions, 996 before.
Notes

Scope and the open decision

This PR restores the 1.4.0 result for bun: modules only. The same line of #41150 also changed the default import of a type: "sqlite" file and of an external ES module package, on Bun and on Node. Whether an external require() must ever get , 1 from an importer that is an ES module by type needs a maintainer decision. The table for that decision, and a linker-level version of the fix on a branch, are in #41953 (comment).

Measurements (linux-x64, release builds of main 8a0e49e and of this branch at 3452faa, same machine and configuration)

  • Output bytes: one corpus of 250 builds gives 258 files. It has interop shapes for .js, .mjs, .mts, "type": "module" and "type": "commonjs" importers, 20,000 user-land externals, React 18.3.1 server rendering, every entry of builtinModules as a default and a star import, and a sqlite file import. Targets bun, node and browser. Formats esm, cjs and iife. Plain and --minify. 238 files are byte-identical. 8 change for bun:sqlite. 12 change only because bun:ffi, bun:jsc, bun:test, an unknown bun: specifier, and bun:fs under --target=node lose , 1.
  • bun:sqlite is the only built-in where __toESM(value, 1).default and __toESM(value).default differ, so the 12 other files keep their values.
  • Binary: .text 82,158,473 to 82,158,985 bytes (+512). .rodata does not change. print_require_or_import_expr (5 instantiations) 39,870 to 40,068 bytes.
  • Instructions (gdb stepi): one call of print_require_or_import_expr that prints __toESM(require("pkg-0"), 1) runs 996 instructions on main and 876 on this branch. The output is byte-identical. The compiler lays the function out differently.
  • Whole build of the 20,000 externals to cjs, user instructions from a perf_event_open counter, 20 interleaved runs: 355,885,852 to 354,263,988 (-0.456 %).
  • perf and valgrind are not installed on the machine that ran this.

Verification on the merged head

  • On a debug build of main 3396aea the three new tests fail. On this branch test/bundler/bundler_cjs.test.ts passes 65 of 65.
  • Also run with this change merged with main 3396aea: bundler_bun, bundler_edgecase, regression/issue/03844.
  • Bun 1.4.0 prints function for the repro below. Main 8a0e49e prints object.

The account from the first version of this PR

Repro (1.4.1, 1.4.2, 1.4.3, main):

cat > a.mjs <<'JS'
import Database from "bun:sqlite";
console.log(typeof Database);
new Database(":memory:");
JS
bun a.mjs                                                    # function
bun build a.mjs --target=bun --format=cjs --outfile=o.cjs && bun o.cjs
# object
# TypeError: Object is not a constructor (evaluating 'new import_bun_sqlite.default(":memory:")')

The same file named a.ts without "type": "module" was fine: it printed __toESM(require("bun:sqlite")). --format=esm keeps the import statement and was fine too.

Why the specifier prefix and not ImportRecordTag::Builtin: the tag is only set under --target=bun, where it also covers node:* and the packages Bun overrides (ws, node-fetch, undici, ...). Under --target=node a bun:sqlite import stays external with tag None and had the same bug, and the output can only run its require("bun:sqlite") on Bun. The prefix covers both targets and leaves node:* output byte-identical to esbuild's.

Matrix used to find affected modules, run on bun 1.4.3: for each builtin m, compare (await import(m)).default with __toESM(require(m), 1).default and __toESM(require(m), 0).default. Mismatches: bun:sqlite (node mode only) and bun:wrap (no default export at all, so import d from "bun:wrap" is a SyntaxError unbundled). The overridden packages (ws, node-fetch, undici, isomorphic-fetch, @vercel/fetch, abort-controller) agree in both modes.

Not changed here, for a maintainer to judge: for a user-land CommonJS file, bun run honors __esModule for every importer and keys only on the CommonJS file's own package.json "type" (populateESMExports, src/jsc/bindings/JSCommonJSModule.cpp:1012). The bundler keys on the importer, as esbuild and Node do. So entry.mjs importing a Babel-style dep.cjs prints function under bun entry.mjs and object after bun build --target=bun. #41150 chose the esbuild rule on purpose. This PR only takes bun: modules out of it.

import Bun from "bun" under --format=cjs is globalThis.Bun.default, which is undefined. That is older than 1.4.1 and a different code path (ImportRecordTag::Bun), so it is not touched here.


[human-review] gate passed · iteration 0 · 2 files touched

fails on main (without fix)
ASAN without fix: 3 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/bundler/bundler_cjs.test.ts
bun test v1.4.3 (f42e98025)

test/bundler/bundler_cjs.test.ts:
(pass) bundler > cjs/__toESM_import_syntax_with_esModule [1074.32ms]
(pass) bundler > cjs/__toESM_import_syntax_without_esModule [536.28ms]
(pass) bundler > cjs/__toESM_import_syntax_function [491.71ms]
(pass) bundler > cjs/__toESM_import_syntax_primitive [589.95ms]
(pass) bundler > cjs/__toESM_import_syntax_named_and_default [438.88ms]
(pass) bundler > cjs/__toESM_import_syntax_namespace [450.25ms]
(pass) bundler > cjs/__toESM_target_node [651.59ms]
(pass) bundler > cjs/__toESM_target_browser [463.40ms]
(pass) bundler > cjs/__toESM_target_bun [514.26ms]
(pass) bundler > cjs/__toESM_format_esm [695.29ms]
(pass) bundler > cjs/__toESM_format_cjs_with_import [538.64ms]
(pass) bundler > cjs/__toESM_mjs_reexport [561.26ms]
(pass) bundler > cjs/__toESM_mjs_reexport_with_esModule [430.48ms]
(pass) bundler > cjs/__toESM_deep_reexport_chain [471.39ms]
(pass) bundler > cjs/__toESM_reexport_with_rename [671.39ms]
(pass) bundler > cjs/__toESM_default_pro
... (truncated)

release without fix: 3 FAILED
bun test v1.4.3-canary.1 (f42e98025)

test/bundler/bundler_cjs.test.ts:
(pass) bundler > cjs/__toESM_import_syntax_with_esModule [20.73ms]
(pass) bundler > cjs/__toESM_import_syntax_without_esModule [9.66ms]
(pass) bundler > cjs/__toESM_import_syntax_function [13.40ms]
(pass) bundler > cjs/__toESM_import_syntax_primitive [13.85ms]
(pass) bundler > cjs/__toESM_import_syntax_named_and_default [13.31ms]
(pass) bundler > cjs/__toESM_import_syntax_namespace [12.10ms]
(pass) bundler > cjs/__toESM_target_node [10.25ms]
(pass) bundler > cjs/__toESM_target_browser [10.89ms]
(pass) bundler > cjs/__toESM_target_bun [11.33ms]
(pass) bundler > cjs/__toESM_format_esm [10.98ms]
(pass) bundler > cjs/__toESM_format_cjs_with_import [9.84ms]
(pass) bundler > cjs/__toESM_mjs_reexport [11.28ms]
(pass) bundler > cjs/__toESM_mjs_reexport_with_esModule [10.68ms]
(pass) bundler > cjs/__toESM_deep_reexport_chain [10.82ms]
(pass) bundler > cjs/__toESM_reexport_with_rename [9.93ms]
(pass) bundler > cjs/__toESM_default_prop_no_esModule [10.50ms]
(pass) bundler > cjs/__toESM_mixed_import_styles [9.91ms]
(pass) bundler > cjs/__toESM_esModule_non_true [10.33ms]
(pass) bundler > cjs/__toESM_esModul
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/bundler/bundler_cjs.test.ts
bun test v1.4.3 (f42e98025)

test/bundler/bundler_cjs.test.ts:
(pass) bundler > cjs/__toESM_import_syntax_with_esModule [1086.82ms]
(pass) bundler > cjs/__toESM_import_syntax_without_esModule [427.98ms]
(pass) bundler > cjs/__toESM_import_syntax_function [543.56ms]
(pass) bundler > cjs/__toESM_import_syntax_primitive [647.02ms]
(pass) bundler > cjs/__toESM_import_syntax_named_and_default [439.75ms]
(pass) bundler > cjs/__toESM_import_syntax_namespace [441.75ms]
(pass) bundler > cjs/__toESM_target_node [454.73ms]
(pass) bundler > cjs/__toESM_target_browser [571.77ms]
(pass) bundler > cjs/__toESM_target_bun [534.17ms]
(pass) bundler > cjs/__toESM_format_esm [640.35ms]
(pass) bundler > cjs/__toESM_format_cjs_with_import [837.72ms]
(pass) bundler > cjs/__toESM_mjs_reexport [552.13ms]
(pass) bundler > cjs/__toESM_mjs_reexport_with_esModule [441.33ms]
(pass) bundler > cjs/__toESM_deep_reexport_chain [601.10ms]
(pass) bundler > cjs/__toESM_reexport_with_rename [436.59ms]
(pass) bundler > cjs/__toESM_default_pro
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 764ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/123] gen generated_host_exports.rs
generated_host_exports.rs: 122 exports (host=5, lazy=10, generic=107, rust=0); 242 extern-C blocks audited
[2/123] gen cpp.rs (cppbind)
[2/123] cargo bun_runtime → libbun_runtime.a
^[[1m^[[92m   Compiling^[[0m bun_js_printer v0.0.0 (/workspace/bun/src/js_printer)
^[[1m^[[92m   Compiling^[[0m bun_http v0.0.0 (/workspace/bun/src/http)
^[[1m^[[92m   Compiling^[[0m bun_bundler v0.0.0 (/workspace/bun/src/bundler)
^[[1m^[[92m   Compiling^[[0m bun_standalone_graph v0.0.0 (/workspace/bun/src/standalone_graph)
^[[1m^[[92m   Compiling^[[0m bun_transpiler v0.0.0 (/workspace/bun/src/transpiler)
^[[1m^[[92m   Compiling^[[0m bun_bunfig v0.0.0 (/workspace/bun/src/bunfig)
^[[1m^[[92m   Compiling^[[0m bun_install v0.0.0 (/workspace/bun/src/install)
^[[1m^[[92m   Compiling^[[0m bun_jsc v0.0.0 (/workspace/bun/src/jsc)
^[[1m^[[92m   Compiling^[[0m bun_js_parser_jsc v0.0.0 (/workspace/bun/src/js_parser_jsc)
^[[1m^[[92m   Compiling^[[0m bun_bundler_jsc v0.0.0 (/workspace/bun/src/bundler_jsc)
^[[1m^[[92m   Compiling^[[0m 
... (truncated)
diff hotspot
src/js_printer/lib.rs            |  9 ++++++-
 test/bundler/bundler_cjs.test.ts | 54 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 62 insertions(+), 1 deletion(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                              reads  edits  tests
src/js_printer/lib.rs                 2      1      9
test/bundler/bundler_cjs.test.ts      1      1      9

…in CommonJS output

An ESM-by-type importer (.mjs, .mts, "type": "module") printed
__toESM(require("bun:sqlite"), 1) under --format=cjs and --format=iife.
isNodeMode ignores __esModule, so the default import was the whole
require("bun:sqlite") object and new Database() threw. Bun's loader
makes an own default property of a builtin's exports the ES default
export for every importer, so a bun: specifier never takes isNodeMode.
@coderabbitai

coderabbitai Bot commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 8a0e49e and 3452faa.

Files selected for processing: 2
  • src/js_printer/lib.rs
  • test/bundler/bundler_cjs.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review.

Walkthrough

The printer now omits the Node-mode argument from wrapped external bun: imports. Tests cover bun:sqlite interop across Bun and Node targets, importer module types, and output formats. They also verify that node:fs retains Node-mode handling.

Changes

Bun built-in import interop

Layer / File(s) Summary
Bun built-in import wrapping
src/js_printer/lib.rs, test/bundler/bundler_cjs.test.ts
The printer omits the Node-mode argument when a wrapped external import path starts with bun:. Tests check bun:sqlite default and namespace behavior across importer types, targets, and output formats, and check Node-mode handling for node:fs.

Possibly related PRs

  • oven-sh/bun#46736: Changes __toESM Node-mode selection for CommonJS data-file imports, which is also the behavior affected by this PR.
  • oven-sh/bun#40940: Changes module-type detection that informs the isNodeMode decision covered by this PR.

Suggested reviewers: jarred-sumner

Priority: Normal

Merge Risk: Minimal · up to 3452f

The change corrects Bun built-in default imports. No merge-blocking risk was identified.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title clearly and concisely identifies the main change: preserving the default export for external bun: modules in CommonJS output.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description check Passed The description is complete and relevant. It explains the problem, cause, fix, scope, known limitations, performance impact, and verification results. It does not use the template headings exactly, bu…
Full details: Description check

Explanation

The description is complete and relevant. It explains the problem, cause, fix, scope, known limitations, performance impact, and verification results. It does not use the template headings exactly, but it provides the required information, including how the code was verified.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the claude label Sep 8, 2026
@robobun

robobun commented Sep 8, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: ready for review. The head is 3452faa, with main 8a0e49e merged.

Reproduced on bun 1.4.3-canary.1 and on builds of main 3396aea and 8a0e49e. Put import Database from "bun:sqlite"; console.log(typeof Database); in a.mjs. Then bun build a.mjs --target=bun --format=cjs --outfile=o.cjs && bun o.cjs prints object, and the output contains __toESM(require("bun:sqlite"), 1). Bun 1.4.0 and this branch print function.

The three new tests in test/bundler/bundler_cjs.test.ts (cjs/__toESM_bun_builtin_*) fail on a debug build of main 3396aea and pass on this branch (65 of 65).

Scope: this PR restores the 1.4.0 result for bun: modules only. The same line of #41150 also changed the default import of a type: "sqlite" file and of an external ES module package, and this PR does not change those. They need a maintainer decision on the rule for externals. The table for that decision, and a linker-level version of the fix on a branch, are in #41953 (comment).

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — narrow, well-reasoned regression fix restoring 1.4.0 behavior for bun: default imports.

What was reviewed:

  • The bun: prefix check uses strings::has_prefix_comptime per src/CLAUDE.md and only touches the external-require() emission arm; the other print_to_esm_suffix() sites (lines 2794, 2887) are guarded by record.source_index.is_valid() so they never see external bun: records, and the dynamic-import() arm keeps a real import(...) so Bun's loader already returns the correct namespace there.
  • Tests cover the variant matrix (.mjs/.mts/type: module × cjs/iife × target: bun/node), assert both bundle text and runtime stdout, and include a node:fs control showing non-bun: externals still get , 1.
  • No CODEOWNERS cover the changed paths.
Extended reasoning...

Overview

This PR fixes a 1.4.1 regression where bun build --format=cjs (or iife) of an ES-module-typed importer wrapped require("bun:sqlite") in __toESM(..., 1), making the default import resolve to the whole exports object instead of the Database class. The fix is a 7-line branch in src/js_printer/lib.rs (external-require() arm of print_require_or_import_expr) that emits a bare ) instead of calling print_to_esm_suffix() when the record's specifier starts with bun:. Three itBundled cases are added to the existing test/bundler/bundler_cjs.test.ts.

Security risks

None. This changes only whether a , 1 argument is emitted to the __toESM runtime helper for bun:-prefixed specifiers in bundled output. No parsing of untrusted input, no auth/crypto/permissions surface, no memory-safety implications.

Level of scrutiny

Moderate — the printer is hot bundler code, but the change is a single narrowly-gated branch. I checked the other three wrap_with_to_esm emission sites: two (lines 2794 and 2887) are inside the record.source_index.is_valid() internal-module path, which bun: builtins never enter (they are always external), and the third (line 3040) is the external dynamic-import() path, which keeps a real import("bun:...") at runtime where Bun's ESM loader already produces the correct default. So the single site changed is the whole class for this bug. The prefix check uses the mandated strings::has_prefix_comptime helper, and the comment is a load-bearing why-comment per REVIEW.md.

Other factors

Tests follow the harness conventions: added to the existing bundler_cjs.test.ts, use itBundled, share a fixture across the three importer-type × format × target combinations, assert exact bundle text (__toESM(require("bun:sqlite")) with no , 1) plus runtime stdout, and the target: node case includes a node:fs control asserting , 1 is still emitted for non-bun: externals — so the change is proven not to over-apply. The PR description documents that the author diffed all builtins and only bun:sqlite diverges between the two __toESM modes. Exit reason was dry_streak and no CODEOWNERS cover these paths.

@robobun

robobun commented Sep 8, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 9:09 PM PT - Oct 26th, 2026

✅ @robobun, your commit 3452faac8e8f02854cb5b83c88a63d47c11a72d7 passed in Build #133382! 🎉


🧪   To try this PR locally:

bunx bun-pr 41953

That installs a local version of the PR into your bun-41953 executable, so you can run:

bun-41953 --bun

Comment thread src/js_printer/lib.rs
Comment on lines +2995 to +2997
// Only Bun loads a `bun:` module, and its loader makes an own
// `default` of the exports the ES default for every importer.
// `isNodeMode` would make it the whole exports object.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. The test is a special case for one specifier, and the cause is wider than bun: modules.

The two comments of 2026-10-27 on this PR list three rules for this arm. Under the first and the third rule this test and its comment are deleted. The third rule is built on robobun/811841e7/external-import-no-mode-target-bun. Under the second rule the linker makes the decision and the printer reads a flag.

The arm stays as it is in this PR until a maintainer picks the rule. I leave this thread open until then.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

@robobun

robobun commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator Author

A linker-level version of this fix is ready on a branch. It is not a second PR. It is here for the shape that the decision below selects.

Branch robobun/54676ab8/linker-picks-toesm-interop, commit 1643d9b, one commit on main 8a0e49e.

What the branch does

The decision that selects the shape

Must an import that is not bundled ever get , 1 when the importer is an ES module by type?

Default import from an .mjs file, --format=cjs. The Bun columns use --target=bun. The Node columns use --target=node on Node 26.3.0.

Importee bun run 1.4.0 on Bun main on Bun node main on Node no , 1 on Node
bun:sqlite function function object n/a n/a n/a
sqlite file with type: "sqlite" Database Database module object n/a n/a n/a
ES module package function function object function object function
CommonJS package with __esModule and exports.default function function object object object function
plain CommonJS package function function function function function function
Notes
  • Tests on the branch: cjs/__toESM_bun_builtin_* (Tests 63 to 72 in test/bundler/bundler_cjs.test.ts), bun/sqlite-file-format-cjs-mjs-importer, bun/embedded-sqlite-file-external-format-cjs-mjs-importer and bun/BuiltinDefaultImportCJS in test/bundler/bundler_bun.test.ts, and cjs2esm/DynamicImportOfAsyncLiftedCommonJS* in test/bundler/bundler_cjs2esm.test.ts. They cover cjs, iife and bytecode output, the bun, node and browser targets, the CLI and Bun.build, every import form, a .ts and an .mjs importer in one bundle, a package.json imports alias, and a plugin external.
  • bun/BuiltinDefaultImportCJS walks require("node:module").builtinModules. bun:sqlite is the only built-in whose require() value has an own default that is not the value itself.
  • Also run on the branch with no change in expectations: bundler_cjs2esm, bundler_npm, bundler_splitting, bundler_edgecase, bundler_loader, bundler_minify, bundler_plugin, bundler_browser, esbuild/*, transpiler/react-compiler, regression/issue/03844.
  • Cost, measured on release builds of main and of an earlier revision of the branch: 0 instructions at print and +14 per wrapped external at link (gdb stepi). .text +768 bytes. ImportRecord stays 96 bytes. The final revision is simpler than the measured one. I did not measure it again.
  • Not covered by the branch: an untyped ./db.sqlite import, and a database with another extension. Those fail before any __toESM call, on 1.4.0 too.

@robobun
robobun marked this pull request as ready for review October 27, 2026 11:15

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

@robobun

robobun commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator Author

The third option in the table above (--target=bun only) is built and measured. It is on a branch and not in this PR: robobun/811841e7/external-import-no-mode-target-bun, commit c276809, on main c643ea8.

What the branch does

  • In the printer's external require() arm, a file whose target is bun closes __toESM(require("x") with ). That is 5 lines of code in src/js_printer/lib.rs. It has no linker change, no flag and no list of importees.
  • The bun: prefix test of this PR is gone. Bundled imports, and the node and browser targets, keep the rule of bundler: pick the __toESM interop from the importer's module type #41150.
  • In that arm the linker sets WRAP_WITH_TO_ESM only for a static import of a module that is not in the bundle (scanImportsAndExports.rs, the insert under kind != ImportKind::Dynamic). A bundled or split record cannot reach the change.

Numbers for the decision

Release builds of main c643ea8 and of the branch, linux-x64.

What main branch
11 kinds of external importee from a "type": "module" file, target bun, cjs: rows equal to bun run 4 10
Outputs for the node and browser targets that differ (30 builds) 0
bun build --compile --bytecode of import Database from "bun:sqlite" with a top-level new Database() exit 1 exit 0
Output bytes for each wrapped external import -3 (-2 minified)
print_require_or_import_expr, each target-bun instantiation (llvm-nm) 7,308 bytes 7,236 bytes
Instructions to print one wrapped external import (gdb stepi) 962 813
.text 83,484,521 83,484,521

I also took the default import of each of the 1192 package versions in a clean test store, from a "type": "module" file, with --target=bun --format=cjs --packages=external:

  • 69 change between main and the branch.
  • 53 of them then equal bun run and Node: 40 ES module packages and 13 dual packages. On main their default import is the module object.
  • 13 versions (12 names, for example @babel/code-frame, pretty-format, webpack-merge) are CommonJS-only packages with __esModule and default. They go from module.exports (Node, 1.4.1 to 1.4.4) to exports.default (bun run, 1.4.0).
  • The other 3 have no two references to compare with: bun-plugin-yaml does not load in Node, the ES build of elysia did not load in the fixture, and bun run itself gives undefined for mongodb-connection-string-url (the branch gives the class, as Node does).
  • The second option (keep bundler: pick the __toESM interop from the importer's module type #41150 with a list) leaves the 53 as they are on main.

One project outside this repository reports the bun:sqlite case. tikoci/rosetta#142 pins its compiler to Bun 1.3.14 after an Object is not a constructor failure at startup of a --compile --bytecode build. Its src/db.ts has import sqlite from "bun:sqlite" under "type": "module".

What gets worse or stays broken on the branch

  • Three kinds of importee move away from bun run. They are a CommonJS file under a "type": "module" package.json with __esModule and default, a CommonJS file whose __esModule is a getter, and a JSON file with both keys. bun run gives the whole value. The branch gives .default, as 1.4.0 did. require() returns the same value for the first kind as for a dual package, so the build cannot tell them apart. A test pins the first kind. None of the 69 store versions that change has one of these shapes.
  • A require cycle can throw at load. An ES module requires the bundle before its own default export is evaluated, and the bundle imports that module as an external. __toESM then throws ReferenceError: Cannot access 'default' before initialization for 4 of 7 forms of default export. For a "type": "module" importer, main loads and gives the module object. For a .ts importer with no "type", main already throws. The cause is the own-default read that bundler: use module.exports as the default import when __esModule has no default #41213 added to the helper. That fix belongs in src/runtime.js and must land before or with this change.
  • The node and browser targets do not change. An external ES module package there still gets its namespace as the default import, and bun:sqlite under --target=node still fails. fix: re-export the named userEvent from the built entries dev-five-git/bun-test-env-dom#12 describes the target node case.
  • A typed import whose extension does not select the loader (./my.db with { type: "sqlite" }) still fails to load. bundler, repl: keep an import's type when it becomes require() or import() #46294 has that change.
  • import Bun from "bun" in cjs output is still undefined (Bun import fails (undefined) when using the bytecode flag #20670). It does not reach this arm.

Recommendation

Take the third option now. It changes no output outside target bun, and it is the only one of the three that fixes external ES module and dual packages there. The first option is the same change without the target test: it also fixes the node target and flips cjs/__toESM_external_require_mjs_importer.

Tests and relation to the other open changes
  • 14 new cases in test/bundler/bundler_cjs.test.ts, bundler_bun.test.ts and bundler_compile.test.ts. Each fails on a release build of main and passes on the branch.
  • They cover cjs, iife and bytecode output, the CLI and Bun.build, bun:sqlite, an ES module package, a dual package, a text file, a typed sqlite file, the re-export forms, a .ts and an .mjs importer in one bundle, and an external and a bundled import in one entry.
  • All of test/bundler/ on a release build of the branch: 7795 pass, 0 fail.
  • Two entries of bundler_bytecode_portable.test.ts move. Their emitted JavaScript differs from main only by the removed , 1 at 18 call sites.
  • Not measured: an instruction count with cachegrind. valgrind is not available on the machine that made these builds.
  • With this rule the , 1 exemption list of bundler, repl: keep an import's type when it becomes require() or import() #46294 is not necessary for target bun, and its toml pin flips.
  • bundler: pass isNodeMode to __toESM when the importee is a data file #46736 sets its flag for bundled data files. It does not meet this arm.
  • The linker branch above and this branch print the same bytes for bun:sqlite and for a typed sqlite file under target bun. They differ for every other external import.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants