esbuild 0.14.4
-
Adjust esbuild's handling of
defaultexports and the__esModulemarker (#532, #1591, #1719)This change requires some background for context. Here's the history to the best of my understanding:
When the ECMAScript module
import/exportsyntax was being developed, the CommonJS module format (used in Node.js) was already widely in use. Because of this the export name calleddefaultwas given special a syntax. Instead of writingimport { default as foo } from 'bar'you can just writeimport foo from 'bar'. The idea was that when ECMAScript modules (a.k.a. ES modules) were introduced, you could import existing CommonJS modules using the new import syntax for compatibility. Since CommonJS module exports are dynamic while ES module exports are static, it's not generally possible to determine a CommonJS module's export names at module instantiation time since the code hasn't been evaluated yet. So the value ofmodule.exportsis just exported as thedefaultexport and the specialdefaultimport syntax gives you easy access tomodule.exports(i.e.const foo = require('bar')is the same asimport foo from 'bar').However, it took a while for ES module syntax to be supported natively by JavaScript runtimes, and people still wanted to start using ES module syntax in the meantime. The Babel JavaScript compiler let you do this. You could transform each ES module file into a CommonJS module file that behaved the same. However, this transformation has a problem: emulating the
importsyntax accurately as described above means thatexport default 0andimport foo from 'bar'will no longer line up when transformed to CommonJS. The codeexport default 0turns intomodule.exports.default = 0and the codeimport foo from 'bar'turns intoconst foo = require('bar'), meaningfoois0before the transformation butfoois{ default: 0 }after the transformation.To fix this, Babel sets the property
__esModuleto true as a signal to itself when it converts an ES module to a CommonJS module. Then, when importing adefaultexport, it can know to use the value ofmodule.exports.defaultinstead ofmodule.exportsto make sure the behavior of the CommonJS modules correctly matches the behavior of the original ES modules. This fix has been widely adopted across the ecosystem and has made it into other tools such as TypeScript and even esbuild.However, when Node.js finally released their ES module implementation, they went with the original implementation where the
defaultexport is alwaysmodule.exports, which broke compatibility with the existing ecosystem of ES modules that had been cross-compiled into CommonJS modules by Babel. You now have to either add or remove an additional.defaultproperty depending on whether your code needs to run in a Node environment or in a Babel environment, which created an interoperability headache. In addition, JavaScript tools such as esbuild now need to guess whether you want Node-style or Babel-styledefaultimports. There's no way for a tool to know with certainty which one a given file is expecting and if your tool guesses wrong, your code will break.This release changes esbuild's heuristics around
defaultexports and the__esModulemarker to attempt to improve compatibility with Webpack and Node, which is what most packages are tuned for. The behavior changes are as follows:Old behavior:
-
If an
importstatement is used to load a CommonJS file and a)module.exportsis an object, b)module.exports.__esModuleis truthy, and c) the propertydefaultexists inmodule.exports, then esbuild would set thedefaultexport tomodule.exports.default(like Babel). Otherwise thedefaultexport was set tomodule.exports(like Node). -
If a
requirecall is used to load an ES module file, the returned module namespace object had the__esModuleproperty set to true. This behaved as if the ES module had been converted to CommonJS via a Babel-compatible transformation. -
The
__esModulemarker could inconsistently appear on module namespace objects (i.e.import * as) when writing pure ESM code. Specifically, if a module namespace object was materialized then the__esModulemarker was present, but if it was optimized away then the__esModulemarker was absent. -
It was not allowed to create an ES module export named
__esModule. This avoided generating code that might break due to the inconsistency mentioned above, and also avoided issues with duplicate definitions of__esModule.
New behavior:
-
If an
importstatement is used to load a CommonJS file and a)module.exportsis an object, b)module.exports.__esModuleis truthy, and c) the file name does not end in either.mjsor.mtsand thepackage.jsonfile does not contain"type": "module", then esbuild will set thedefaultexport tomodule.exports.default(like Babel). Otherwise thedefaultexport is set tomodule.exports(like Node).Note that this means the
defaultexport may now be undefined in situations where it previously wasn't undefined. This matches Webpack's behavior so it should hopefully be more compatible.Also note that this means import behavior now depends on the file extension and on the contents of
package.json. This also matches Webpack's behavior to hopefully improve compatibility. -
If a
requirecall is used to load an ES module file, the returned module namespace object has the__esModuleproperty set totrue. This behaves as if the ES module had been converted to CommonJS via a Babel-compatible transformation. -
If an
importstatement orimport()expression is used to load an ES module, the__esModulemarker should now never be present on the module namespace object. This frees up the__esModuleexport name for use with ES modules. -
It's now allowed to use
__esModuleas a normal export name in an ES module. This property will be accessible to other ES modules but will not be accessible to code that loads the ES module usingrequire, where they will observe the property set totrueinstead.
-