Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Presence of "module" or "exports" intrisics in an ESM module forces output to convert named exports into export default #769

Closed
diervo opened this issue Feb 8, 2021 · 6 comments

Comments

@diervo
Copy link

diervo commented Feb 8, 2021

I been reading all the comments about bundling esm/cjs, and I couldn't really figure out if is a duplicate or actually a genuine new issue, so posting it as a new issue as the repro is quite trivial just in case.

TL:DR:
When bundling one single file with format:esm that is already an ESM module, esbuild returns a wrapped __commonJS as a export default, which changes the semantics/shape of the original module (from named exports to default export)

Input:

...lots of code (no imports)
export { Foo, Bar }

Expected:

...lots of code (no imports)
export { Foo, Bar }

Actual:

require_modulename = __commonJS(() => ...)
export default require_modulename();

Repro steps:

I'm using esbuild for bundling npm modules. One package in particular (@coveo/headless) has a prepackaged ESM module, to which esbuild should do pretty much a noop (no imports just exports).

This is the file within the npm package:
@coveo/headless/dist/browser/headless.esm.js

For simplicity this is the file for testing:
https://gist.github.com/diervo/6b97c272526a470253e3d78a2a5dd6c7

// example config
    format: 'esm', // I tried only wit format:esm with no luck
    bundle: true,
    write: true,
    platform: 'browser',
    mainFields: ['browser', 'module', 'main'],
    logLevel: 'error'

I tried adding imports removing subparts, but still returns a wrapped export default module as result.

What is interesting that a lot of other packages that are ESM function correctly (ex. @codemirror/view), so might intuition is that some grammar inside ESM modules make esbuild do the __commonJS wrapping.

If this is indeed a bug (given there is a bunch of controversy on interoperability), I'm happy to try to provide a fix given some direction.

@diervo diervo changed the title Bundling ESM modules with format:esm returns default export __commonjs wrapped Bundling ESM modules with format:esm returns default export __commonjs wrapped instead of equivalent named exports Feb 8, 2021
@diervo diervo changed the title Bundling ESM modules with format:esm returns default export __commonjs wrapped instead of equivalent named exports Bundling single ESM module return default export __commonjs wrapped instead of equivalent named exports Feb 8, 2021
@diervo diervo changed the title Bundling single ESM module return default export __commonjs wrapped instead of equivalent named exports Bundling single ESM module returns "default export __commonjs wrapped" instead of original named exports Feb 8, 2021
@evanw
Copy link
Owner

evanw commented Feb 8, 2021

I'm guessing this is a duplicate of #706. Is that correct? You didn't provide a way for me to reproduce the issue so I can't tell from my end.

@diervo
Copy link
Author

diervo commented Feb 8, 2021

I think its likely related, but not sure if #706 is due to transitive CJS dependencies, whereas this one just reproduces with a single esm module to be bundled.

I created a repo with some examples that reproduce the problem:

https://github.com/diervo/esbuild_esm_repro

@ggoodman
Copy link

ggoodman commented Feb 8, 2021

I feel like I might have seen this in the past whenever there's something like references to module.exports, exports or require. This is sort of anecdotal but it would be interesting to check if any of those symbols are referenced.

@diervo
Copy link
Author

diervo commented Feb 8, 2021

@ggoodman You are 100% correct: The presence of module or exports drives the shape of the output.

This following repros the issue (I added it to my repo example above):

Input:

const isModule = typeof module !== 'undefined';

export function foo() {
    return isModule ? 'foo': 'bar';
}

Output:

var __defProp = Object.defineProperty;
var __markAsModule = (target) => __defProp(target, "__esModule", {value: true});
var __commonJS = (callback, module) => () => {
  if (!module) {
    module = {exports: {}};
    callback(module.exports, module);
  }
  return module.exports;
};
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, {get: all[name], enumerable: true});
};
var require_trivial_esm = __commonJS((exports, module) => {
  __markAsModule(exports);
  __export(exports, {
    foo: () => foo
  });
  const isModule = typeof module !== "undefined";
  function foo() {
    return isModule ? "foo" : "bar";
  }
});
export default require_trivial_esm();

So then the question really is: Should the presence of the intrinsic module or exports in a program influence the shape of the output, more concretely the moduleRecord?

Without thinking too much and without going back to the spec, seems like if the module is unambiguously ESM (has import/exports) module or exports shouldn't function as in-band-signals to change the output of the compilation. That being said there are likely a lot of ambiguous corner cases to consider which will make this a bit more tricky...

@evanw what do you think? Probably you have all the semantics and grammar on this topic fresh in your head :)

@diervo diervo changed the title Bundling single ESM module returns "default export __commonjs wrapped" instead of original named exports Presence of "module" or "exports" intrisics in an ESM module forces output to convert named exports into export default Feb 8, 2021
@evanw
Copy link
Owner

evanw commented Feb 11, 2021

Thanks for the additional detail. Closing as a duplicate of #706.

Using CommonJS-style exports causes esbuild to wrap the module in a CommonJS closure. This can happen either because some other module calls require() and needs CommonJS-style exports, or because a module references module or exports and causes CommonJS-style exports to exist. The bug is not that CommonJS-style exports are supported, but rather that generating an ESM-format module doesn't insert named exports for the ESM exports in the original file.

@evanw
Copy link
Owner

evanw commented Mar 26, 2021

FYI this behavior was changed in version 0.10.0 and the example in #769 (comment) now works even though #706 hasn't been fixed yet (although hopefully it will be fixed soon too).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants