esbuild: The export name "__esModule" is reserved and cannot be used

Recently, I’ve been attempting to bundle modules from jspm.io (via a custom esbuild plugin which implements imports from URLs and import maps), and I’ve been running into this error a lot:

error: The export name “__esModule” is reserved and cannot be used (it’s needed as an export marker when converting ES module syntax to CommonJS)

Here’s an example module where I’ve run into this for the first time: https://jspm.dev/npm:react-calendly@2.2.1!cjs

It all boils down to something like this:

// MRE.js

const exports = {}
Object.defineProperty(exports, "__esModule", { value: true })
export default exports

const __esModule = exports.__esModule
export { __esModule }

It’s that named __esModule export that upsets esbuild – if I comment it out, it builds fine.

The way I understood it from reading https://github.com/esnext/es6-module-transpiler/issues/85, the magic __esModule export was introduced so that when you transpile an ESM module to CJS, and do const foo = require("esm-foo-transpiled-to-cjs"), it’ll know that you really mean const foo = require("esm-foo-transpiled-to-cjs").default. However, I belive JSPM’s doing things the other way, i.e., it transpiles CJS modules to ESM. So I’m not sure if their usage of the __esModule export is correct.

We’ve had a brief conversation about this with @guybedford on the JSPM Discord, but we decided to move it here. I’m curious, what’s the story behind this “__esModule is reserved” check/error in esbuild?

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 1
  • Comments: 20 (13 by maintainers)

Most upvoted comments

I just wanted to see if there’s any new perspectives to this issue now that it’s been dormant for a while? To give some practical context on why we (Framer) care a lot about this:

Framer has introduced a feature called “Handshake” which lets users build a visual component which gets turned into React under the hood. They can then import its ESM module via a URL, which is now supported in Webpack 5 (and by extension, Next.js).

Now Framer is building two more features:

  1. The ability to construct an entire site visually and host it on a domain, which for performance reasons gets bundled with esbuild
  2. “Instant NPM” – basically a way to use NPM without installation, allowing copy/pasting of these components across different Framer projects and have them appear (load & evaluate) instantly without delay – this is done using import maps on top of the JSPM CDN (ga.jspm.io)

These two features together (bundling with esbuild and loading NPM packages via ga.jspm.io) very often fail on the error described in this issue. However, without this check they would (to the best of my knowledge) bundle and evaluate correctly. While I see there are a lot of edge cases to consider here, I also get the feeling that maybe we’re not seeing the forest for the trees.

There must be a way for us to allow the legitimate use cases above while handling most edge cases (i.e. sacrifice handling some edge cases in favor of allowing legitimate use cases). I thought Piotr’s PR was a good step in that direction – @evanw is there any reason we can’t merge it?

I finally started looking into this. One reason why I haven’t tackled it yet is that it’s a huge interop mess with several entangled issues. I’d like to get them all fixed together so I have a holistic picture of the changes instead of addressing issues individually.

I have started putting together a set of test cases for what I think should be fixed in this cluster of issues: https://github.com/evanw/bundler-esm-cjs-tests. I’ll also start working on getting esbuild to pass these test cases. I think this will unfortunately bloat the injected shim code even more, but that’s likely unavoidable.

On the JSPM side, the issue here is that JSPM will convert CJS into ESM, and in doing so follow the Node.js semantics for what exports to expose.

Ah, I think I understand now. So e.g. react-calendly’s dist/index.js does Object.defineProperty(exports, '__esModule', { value: true }). JSPM then iterates over all properties on exports, and converts them to ESM exports. So it sees __esModule, and treats it as any other export, hence generating the “offending” __esModule export. Which of course is not really “offending” by any standards.

But esbuild complains because if it now were to take that ESM with the__esModule export, and convert it to CJS, the export will clash with the __esModule magic marker which is added per https://github.com/esnext/es6-module-transpiler/issues/85. 'Cause esbuild can’t know that the __esModule export already is the magic marker from the original CJS module.

(It’s like a game of telephone, CJS => ESM => CJS 😄)

But in my case, I’m esbuilding with format=esm, so it doesn’t need to convert anything to CJS, so maybe it doesn’t need to complain about the reserved export?

On the JSPM side, the issue here is that JSPM will convert CJS into ESM, and in doing so follow the Node.js semantics for what exports to expose.

Per the Node.js ESM CJS wrapper semantics, __esModule is a valid ESM export of a CJS module wrapper, and hence is reflected in the converted ESM of the CJS module, which then doesn’t build with esbuild.