In v0.11.0, the way esbuild handles require() and import() of ESM files changed to improve performance and fix bugs related to entry point exports.
Previous Behavior: Calling require() on an ESM file (or import() with code splitting disabled) converted the ESM file to CommonJS. This often resulted in exports being trapped inside a lazy-initialized closure, making them inaccessible to ESM-style export {} clauses.
New Behavior: esbuild now uses lazy initialization instead of full CommonJS conversion. Variables are pulled out of the closure and are accessible to the rest of the module's scope.
Key Implications:
- Performance: ESM imports can now reference exports directly without the overhead of dynamic property access used in CommonJS-style exports.
- Top-level Await:
import() of a module with top-level await is now allowed even when code splitting is disabled, because the lazy initializer can now be async. - Correctness: Calling
require() on an ESM file now recursively wraps all transitive dependencies to ensure correct runtime evaluation order. This increases code size but ensures correctness. - Recommendation: If you want to avoid the increased code size from recursive wrapping, use
import statements instead of require() calls.
// cjs-file.js
console.log(require('./esm-file.js').foo)
// esm-file.js
export let foo = bar()