From 0e6f008cb7987f64a65cc8eaccb02f68f492fb0c Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:39:52 +0000 Subject: [PATCH 1/2] bundler: do not treat a __commonJS wrapper as async in import() 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. --- src/bundler/LinkerContext.rs | 5 ++- test/bundler/bundler_cjs.test.ts | 75 ++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/src/bundler/LinkerContext.rs b/src/bundler/LinkerContext.rs index ea8170943457..63f38ded6318 100644 --- a/src/bundler/LinkerContext.rs +++ b/src/bundler/LinkerContext.rs @@ -2571,7 +2571,10 @@ impl<'a> LinkerContext<'a> { } else { Ref::NONE }, - is_wrapper_async: flags.is_async_or_has_async_dependency && wrapper_ref.is_valid(), + // A `__commonJS` wrapper returns `module.exports`, never a promise. + is_wrapper_async: flags.wrap == WrapKind::Esm + && flags.is_async_or_has_async_dependency + && wrapper_ref.is_valid(), wrapper_ref, was_unwrapped_require: was_unwrapped_require diff --git a/test/bundler/bundler_cjs.test.ts b/test/bundler/bundler_cjs.test.ts index 81a4caed037a..0da29251046f 100644 --- a/test/bundler/bundler_cjs.test.ts +++ b/test/bundler/bundler_cjs.test.ts @@ -793,6 +793,81 @@ describe("bundler", () => { }, }); + // ============================================================================ + // `import()` of a bundled CommonJS file that the linker marks async + // + // The linker marks a file async when a module with top-level await is on its + // chain of static imports, also when tree shaking removes that import. The + // `__commonJS` wrapper of the file still returns `module.exports`, not a + // promise, so `import()` builds the namespace as for any other CommonJS file. + // ============================================================================ + + // Tree shaking removes this package when nothing reads its exports. + const unusedAsyncPackage = { + "/node_modules/async-pkg/package.json": `{ "name": "async-pkg", "main": "index.js", "sideEffects": false }`, + "/node_modules/async-pkg/index.js": `await 0;\nexport const unused = 1;`, + }; + + // `await import()` + itBundled("cjs/__toESM_dynamic_import_of_file_marked_async", { + files: { + "/entry.js": `const ns = await import("./c.js");\nconsole.log(ns.default.t, ns.t);`, + "/c.js": `import { unused } from "async-pkg";\nmodule.exports = { t: 5 };`, + ...unusedAsyncPackage, + }, + run: { + stdout: "5 5", + }, + }); + + // `import().then()` + itBundled("cjs/__toESM_dynamic_import_then_of_file_marked_async", { + files: { + "/entry.js": `import("./c.js").then(ns => console.log(ns.default.t));`, + "/c.js": `import { unused } from "async-pkg";\nmodule.exports = { t: 5 };`, + ...unusedAsyncPackage, + }, + run: { + stdout: "5", + }, + }); + + // The promise is for the namespace, not for `module.exports`. + itBundled("cjs/__toESM_dynamic_import_of_function_marked_async", { + files: { + "/entry.js": `const ns = await import("./c.js");\nconsole.log(typeof ns, typeof ns.default, ns.default());`, + "/c.js": `import { unused } from "async-pkg";\nmodule.exports = function () { return 5; };`, + ...unusedAsyncPackage, + }, + run: { + stdout: "object function 5", + }, + }); + + // The file runs in a microtask, after the code that follows `import()`. + itBundled("cjs/__toESM_dynamic_import_order_of_file_marked_async", { + files: { + "/entry.js": `console.log("before");\nconst promise = import("./c.js");\nconsole.log("after");\nconsole.log((await promise).default.t);`, + "/c.js": `import { unused } from "async-pkg";\nconsole.log("c runs");\nmodule.exports = { t: 5 };`, + ...unusedAsyncPackage, + }, + run: { + stdout: "before\nafter\nc runs\n5", + }, + }); + + // `export *` of a module with top-level await marks the file async too. + itBundled("cjs/__toESM_dynamic_import_of_file_with_export_star_marked_async", { + files: { + "/entry.js": `const ns = await import("./c.js");\nconsole.log(typeof ns.default, ns.default.a);`, + "/c.js": `export * from "./async.js";\nexports.a = 1;\nObject.keys(exports);`, + "/async.js": `await 0;\nexport const t = 1;`, + }, + run: { + stdout: "object 1", + }, + }); + const externalImportLog = /* js */ ` import d, { named } from "ext"; console.log(typeof d, named); From 085bbaa41c843259906726ae606bbc496dd4020b Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 24 Oct 2026 13:17:15 +0000 Subject: [PATCH 2/2] test: assert the whole namespace that import() gives for a CommonJS file with export star --- test/bundler/bundler_cjs.test.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/bundler/bundler_cjs.test.ts b/test/bundler/bundler_cjs.test.ts index 0da29251046f..f1b5d8fbc360 100644 --- a/test/bundler/bundler_cjs.test.ts +++ b/test/bundler/bundler_cjs.test.ts @@ -857,14 +857,16 @@ describe("bundler", () => { }); // `export *` of a module with top-level await marks the file async too. + // The namespace has the names of that module. `t` is a literal, so its value + // does not depend on `init_async()`, which the wrapper of `c.js` does not await. itBundled("cjs/__toESM_dynamic_import_of_file_with_export_star_marked_async", { files: { - "/entry.js": `const ns = await import("./c.js");\nconsole.log(typeof ns.default, ns.default.a);`, + "/entry.js": `const ns = await import("./c.js");\nconsole.log(JSON.stringify(ns));`, "/c.js": `export * from "./async.js";\nexports.a = 1;\nObject.keys(exports);`, - "/async.js": `await 0;\nexport const t = 1;`, + "/async.js": `await 0;\nexport const t = 2;`, }, run: { - stdout: "object 1", + stdout: `{"default":{"t":2,"a":1},"t":2,"a":1}`, }, });