Repository navigation
Conversation
The linker marks a file async when a module with top-level await is on its chain of static imports. It does that before tree shaking, and for CommonJS files too. import() of such a CommonJS file was printed as if require_x() returned a promise: `await require_c()` gave module.exports and no namespace, and `require_c().then(...)` threw. is_wrapper_async is now true only for a file in an __esm wrapper, so import() of a CommonJS file prints `Promise.resolve().then(() => __toESM(require_c()))` as for every other CommonJS file.
|
No actionable comments were generated in the recent review.
|
| Layer / File(s) | Summary |
|---|---|
Async wrapper classification src/bundler/LinkerContext.rs |
The linker now requires an ESM wrapper, an async dependency, and a valid wrapper reference before marking a wrapper async. |
Dynamic import behavior and coverage test/bundler/bundler_cjs.test.ts |
Tests cover namespace results for object and function exports, execution order, and re-exports from a top-level-await module. The added fixture has a side-effect-free top-level await and an unused export. |
Possibly related PRs
- oven-sh/bun#46736: Changes the bundled CommonJS dynamic-import path by adding
TO_ESM_NODE_MODEand changing the__toESMcall.
Suggested reviewers: jarred-sumner, dylan-conway
Priority:
Merge Risk: 
Minimal · up to 085bb
-
No actionable merge-blocking issue remains; the change is ready for normal merge checks.
Pre-merge checks: 
4
-
Full details: Description check
-
Explanation
The description explains the problem, root cause, fix, behavioral changes, limitations, and verification results. It does not use the exact template headings, but it provides the required content and is substantially complete.
-
- 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.
Comment @coderabbitai help to get the list of available commands.
|
Status Reproduced on main (179664e, release build) and on Bun 1.4.3:
With this PR the bundle prints PR: #46748 CI (build 132046, 085bbaa): the diff is green on every job that ran. 179 jobs passed. 7 test files failed once and passed on retry. One of them is |
|
Updated 10:23 AM PT - Oct 24th, 2026
✅ @robobun, your commit 085bbaa41c843259906726ae606bbc496dd4020b passed in 🧪 To try this PR locally: bunx bun-pr 46748That installs a local version of the PR into your bun-46748 --bun |
There was a problem hiding this comment.
Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.
Beyond the inline finding on the export-star test, I checked the sibling sites keyed on the async flag: postProcessJSChunk.rs:867 already gates on wrap == Esm, and generateCodeForFileInChunkJS.rs emits the async wrapper only in the WrapKind::Esm arm, so the new gate matches the only place a wrapper can be a promise; is_wrapper_async has a single consumer (js_printer/lib.rs:2791). The namespace-vs-module.exports behavior change for import() of a function/class export is intentional and documented in the description, so I did not flag it.
Extended reasoning...
The change adds a flags.wrap == WrapKind::Esm condition to is_wrapper_async in src/bundler/LinkerContext.rs and five itBundled cases in test/bundler/bundler_cjs.test.ts; no security-sensitive surface. The gate is consistent with the other async-wrapper sites, but one inline finding (the export-star test certifying a dropped re-export) stands, so a human should look.
…ile with export star
There was a problem hiding this comment.
I reviewed the updated PR and didn't find any bugs; because the one-line fix deliberately diverges from esbuild's requireOrImportMetaForSource and changes what await import() resolves to for existing bundles of linker-marked CommonJS files, a human look is still worthwhile.
What was reviewed:
asyncis only ever emitted for theWrapKind::Esmarm insrc/bundler/linker_context/generateCodeForFileInChunkJS.rs(~647-652); the__commonJSwrapper is a plain function, so gatingis_wrapper_asynconEsmmatches the only async wrapper the linker prints, andsrc/js_printer/lib.rs:2791is its sole consumer.- The sibling static-import site in
src/bundler/LinkerContext.rs(~2344-2379) already awaitsinit()only inside theWrapKind::Esmarm, so no otheris_async_or_has_async_dependencyreader needed the same guard. - The amended export-star test (085bbaa) now asserts the full namespace including the re-exported
t, addressing the earlier inline note; the test comment records thattis observable only because the entry'sawaitlands a microtask after the un-awaitedinit_async().
Extended reasoning...
The diff adds one condition in require_or_import_meta_for_source in src/bundler/LinkerContext.rs so is_wrapper_async is true only for __esm-wrapped files, plus five itBundled cases in test/bundler/bundler_cjs.test.ts. It touches no security-sensitive surface. The change is small and consistent with where the linker actually emits async wrappers, and the follow-up commit addressed the prior inline comment, but it intentionally departs from esbuild's ported logic and alters runtime results for some bundles that currently run (a module.exports function now resolves to a namespace), which is a product-semantics call better confirmed by a maintainer. No third-party CHANGES_REQUESTED review is present and the changed paths are not covered by CODEOWNERS.
|
#41601 changed at So the line "#41601 makes that |
Problem
import()of a bundled CommonJS file can throw at run time:TypeError: undefined is not an object (evaluating 'ns.default.t').bun buildexits 0. The output hasvar ns = await require_c();.importof a package with top-level await ("sideEffects": false). Anexport * fromsuch a module is a second trigger.require_or_import_meta_for_source(src/bundler/LinkerContext.rs:2574) reads the async flag of the file. It ignores which wrapper the file has.Fix
is_wrapper_asyncis true only for a file in an__esmwrapper.import()of a CommonJS file then printsPromise.resolve().then(() => __toESM(require_c())), as for every other CommonJS file.test/bundler/bundler_cjs.test.ts(5 new tests, all fail on main). Other suites: Notes.Background
var init_x = __esm(...). For an async fileinit_x()returns a promise, andimport()printsinit_x().then(...).var require_x = __commonJS(...).require_x()returnsmodule.exports, never a promise.Downsides
const f = await import("./c.js"); f()worked for a function inmodule.exports. The result is now the namespace, andf()throws. esbuild still printsawait require_c().import()expression..text+0 B, no new pass, no allocation.Notes
Repro
bun build entry.js --outfile=out.js && bun out.js. On main (179664e) the bundle hasvar ns = await require_c();and throwsTypeError: undefined is not an object (evaluating 'ns.default.t'). With this PR it hasvar ns = await Promise.resolve().then(() => __toESM(require_c()));and prints5. Delete theimportline ofc.js, and main prints that second form too.With
import("./c.js").then(ns => ...)main printsrequire_c().then(...)and throwsTypeError: require_c().then is not a function.Origin. No user reported this. It was found by a read of the code, with #46749.
Why the flag is set.
validate_tlaandpropagate_async_dependenciesmark a file async when a static import leads to a top-level await. They run before tree shaking and for every file, so a CommonJS file keeps the mark when its import is removed. The printer (src/js_printer/lib.rs,print_require_or_import_expr) reads the mark throughis_wrapper_asyncand prints the wrapper call as a promise. That is right forinit_x()of an async__esmwrapper only.What changes in a bundle that runs today. Only for
import(), without--splitting, of a CommonJS file that has the mark. The value is now the one that the same file gets when the mark is absent.entry.jsns.default.t5import("./c.js").then(...)const f = await import("./c.js"); f(),module.exportsis a function5fis the namespacenew (await import("./c.js"))(),module.exportsis a classtypeofof the result,module.exports = "hello"stringobjectObject.keys(ns),module.exports = { t, u }t, udefault, t, uns === c, wherecis the default importtruefalse, andns.default === cns.t = 9module.exportsconsole.loginc.js, between two logs aroundimport()esbuild 0.21.5 and 0.25.1 print
require_c()for the repro too. So they have the first two rows of main, and they runf().Fixtures. Release builds of main (179664e, md5 105b11a1) and of this PR (0e6f008, md5 ceb94ddd), 259 builds of small graphs:
bun buildis the same for 259 of 259.import()of a marked CommonJS file: 8 threw and now run, 17 run before and after (rows 6 to 9 of the table), 4 ran and now throw (rows 3 to 5), 19 did not parse before or after (anawaitinside the__commonJSwrapper, see bundler: error when a file bundled as CommonJS imports a module with top-level await #46749).Measurements.
size -A:.text82,164,617 to 82,164,617. No section changes size.Not in this PR.
async__esmwrapper, andrequire()of it is still refused. A pass that narrows the mark after tree shaking fixes that.importof a module with top-level await that stays in the bundle, in a CommonJS file: the bundle does not parse. bundler: error when a file bundled as CommonJS imports a module with top-level await #46749 reports it at build time.export * froma module with top-level await, in a CommonJS-wrapped file: the wrapper callsinit_x()and does not wait, on main and here. A value that the module assigns after itsawaitisundefineduntil the promise settles. bundler: error when a file bundled as CommonJS imports a module with top-level await #46749 lists it as a question for a maintainer. The fifth test uses a literal for the re-exported name, so its result does not depend on that.export *wait. The fixture of the fifth test then has anawaitin the__commonJSwrapper, and the test needs a change, likecjs/tla_import_export_starin bundler: error when a file bundled as CommonJS imports a module with top-level await #46749.Tests.
test/bundler/bundler_cjs.test.ts: 67 pass with the debug build of this branch. With a release build of main the 5 new tests fail.Suites. Release build of this branch: all 71 files of
test/bundler/andtest/bundler/esbuild/andtest/regression/issue/cyclic-imports-async-bundler.test.js: 3,133 pass, 2 fail. The 2 are inbundler_bytecode_portable.test.tsand fail the same way with the build of main. I cannot run Windows or macOS here.