Bug in Webpack 3 and 4: can’t `require` a module with a ESM entry point

Do you want to request a feature or report a bug?

Bug.

What is the current behavior?

If you require a package with a module field in its package.json, and the module has an export default, the require call does not return the default export like it should.

Instead, it returns an object with default and all properties.

Both of these forms will return the default export:

const merge = require('deepmerge').default
import merge from 'deepmerge'

If the current behavior is a bug, please provide the steps to reproduce.

Reproduction at https://github.com/perry-mitchell/repo-deepmerge-webpack

What is the expected behavior?

require('any-esm-module') should return the default export.

For many Webpack users, require('any-esm-module').default is not even an option, since they are importing a package that depends on some CJS/ESM module (like deepmerge).

Please mention other relevant information such as the browser version, Node.js version, webpack version, and Operating System.

Tested with Webpack 3.11.0 and 4.0.0.


Edit: per the discussion below, my original report above is incorrect.

What should probably happen: require('my-module') should import the main (or browser) entry point in the package.json file.

What happens now: require('my-module') imports the module entry point in the package.json file.

Author: Fantashit

5 thoughts on “Bug in Webpack 3 and 4: can’t `require` a module with a ESM entry point

  1. This is especially an issue for users that have deepmerge as a transitive dependency.

    If a module is written that has require('deepmerge'), it will work fine for the author, as long as they test in node, or use any other bundler besides Webpack.

    But when a Webpack user tries to use that other module that depends on deepmerge, they will get a runtime error that happens inside code that they don’t control themselves.


    One potential solution would be: when require is used, prioritize the main entry point over the module entry point.

  2. @sokra From my view, deepmerge does expose the same entry point for both ESM and CJS.

    CJS:

    module.exports = deepmergeFunction

    ESM:

    export default deepmergeFunction

    By doing this, and exposing two separate entry points, it enables both of these forms:

    CJS:

    const deepmerge = require('deepmerge')

    ESM:

    import deepmerge from 'deepmerge'

    I’m not particularly interested in changing deepmerge to be less friendly to either CJS or ESM consumers.


    I’m guessing “how do we handle default exports” is an argument that’s been hashed over among Webpack maintainers plenty already, so I’m not really expecting big changes in how ESM modules are exposed to consumers.

    However, since the bundling behavior is different from what anyone who tests in node would expect, using require as a hint to import from main instead of module seems viable in my head.

  3. The OP says that the expected behavior is:

    require(‘any-esm-module’) should return the default export

    but IMO, the expected behavior would be that if foo is a package that has both "main" and "module" fields in its package.json, then require('foo') targets the "main" script using CJS semantics. In other words, I think the following is the root of the problem:

    For ESM the “namespace” object is returned by require() (and for import())

    The vast majority of existing code that uses require() implicitly assumes that it’s targeting a CJS module.

    This gist demonstrates what I’d expect: https://gist.github.com/anandthakker/40464d1e684ba5857e12c87821bb3664test-node-esm, test-node-cjs work fine, but test-webpack causes an error.

  4. If you’re using deepmerge directly, you can update to the latest version to resolve the issue. I dropped the esm export a while back rather than try to help folks work past Webpack’s confusing module resolution.

Comments are closed.