webpack: Bug: require() doesn't support `default` exports in ES Modules

✅ ➡️ Edit 2020 for Webpack users:

  • always prefer import nowadays. Only use require if the package is not an ES Module.

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

Bug

What is the current behavior?

const ofi = require('object-fit-images');

In webpack 1.x, this picks up the CommonJS file defined in main.

In webpack 2.x, this loads the module file and sets ofi = {default: realOfi}, which means that users have to use the unnatural const ofi = require('object-fit-images').default. Example

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

npm i object-fit-images@3.1.3
npm i webpack@2.4.1 --global
echo 'var ofi = require("object-fit-images"); ofi()' > src.js; webpack src.js errors.js
echo 'var ofi = require("object-fit-images").default; ofi()' > src.js; webpack src.js works.js
echo 'import ofi from "object-fit-images"; ofi()' > src.js; webpack src.js works-esm.js

What is the expected behavior?

import is for ES Modules and it should try to read the module file if it exists. This works correctly.

require is for CJS files and it should only read the main entry point, not the module one. If it does, it should at least flatten default wherever possible.

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

node 7.8.0 webpack 2.4.1 OSX 10.11.6

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 39
  • Comments: 32 (4 by maintainers)

Commits related to this issue

Most upvoted comments

I just ran into this with modular-css after trying to offer named ES2015 exports. Some CSS classnames aren’t valid JS identifiers, so they can’t be part of the named exports. Now uses will have to use require("./file.css").default if they want access to all exported classnames.

I’m considering reverting the change to use ES2015 exports because of this, it’s now a bad experience for end-users. 😓

  1. It makes sense to me since both a = require() and import a from mean get the default export (“default” in the classical sense, not how it’s described by the ESM spec)

I can see how this makes sense, but it simply not how these things are spec-ed. Webpack is trying to be spec-compliant. Not what-makes-sense-to-someone-specific compliant. a = require() is the same as import * as a from "' in ESM terms, and not the same as import a from "". Which causes the confusion here.

Note that import * as is an anti-pattern; it kills tree-shaking, which is one of the clear benefits of using ESM over CommonJS (where require() always imports the full module!).


Edit: note that babel in the past has some interoperability magic that made import to work like require on commonJS modules (as explained here, it’s a great read). But that was additional, babel specific behavior added on top of the spec and imho a mistake, as it adds to the confusion (TypeScript can do the same, but they won’t by default IIRC)

Hrm … So I’ve got:

  • a dependency A that package.json -> ["module"] -> export default X
  • a dependency B that const X = require("A")
  • Webpack that implements require("A") -> { default: X }

Without getting into fixing any one of these (as far as I can see, if you implement require("A") -> X then in CommonJS you can’t require() any named exports, and if you implement require("A") -> { default: X } then from ESM you can’t replace the entire module, so ¯\_(ツ)_/¯) how can I configure Webpack?

I’m aware of:

  • resolve.alias – I can maybe redirect require("A") to some module.exports = require("A").default shim?
  • resolve.mainFields – I can use package.json -> ["main"] vs. ["module"], but not on a per-dependency basis?
  • It seems like what I want is libraryExport: "default" for dependency A vs. the output?

What’s the best solution?

Because currently I cannot ship ES Modules as they will cause that ugly .default when users require them.

@bfred-it you can ship ES module fine, the only thing you can’t do, is have a nice export default, which is a feature that never existed in the first place in common JS!

The only thing you can’t do, is replace the entire module exports with something like a function, which is possible in common JS modules but not in ESM, and which get’s confused with default exports in this thread, (for all practical matters you are probably trying to achieve the same thing).

Let me put it into a table for all clarity:

pattern export commonJS consume from common js export from ESM consume from ESM
1: normal export module.exports.cool = const { cool } = require("x") export const cool = import { cool } from "x"
2: default export module.exports.default = (not a special case!) const { default } = require("x") export default ... import cool from "x" (any other name works as well)*
3: replace entire module export module.exports = something cool const cool = require("x") Not possible! People try to achieve this with default, but it’s an entirely different beast import * as cool from "X"

Note: for the consume column, it doesn’t matter wheter it is consuming from a commonJS or ESM module!

Important point two is that the above table is totally unrelated to webpack or whatever bundler you are using! Webpack has nothing to do with this. It’s just an implementation of the above table.

*: the confusing part is that babel can mess up here, by checking if a module has a default export, and if it hasn’t, assumes you want the same behave as * as .... But this really a babel only thing and unspecced behavior.

The only place where webpack comes into play, is if package.json specifies multiple entry fields which are not consitent with each other, that is, the main field follows pattern 3, but the ESM build in the same package, under the module follows (incorrectly!) pattern 2. Then, because webpack prefers the ESM version if available, you “suddenly” need to add .default after the require. But that is not something webpack could have prevent; that just happens because the module is shipping two entirely different things. Like shipping lodash in the main field, and underscorejs in the module field. Which seems the bug in some of the packages mentioned in this thread.

yes, that is what I would have done if I had learned this lesson before using the default export 😅

@TrySound yes, I try to stay away from default exports (and entire module exports as well) indeed for similar reasons.

What I did in libraries like immer is providing both a default and a named export. The latter because it is still kinda nice in CJS:

// ESM source
function _produce() { } 

export default _produce;
export const produce = _produce;

// consume from ESM:
import produce from "immer"

// consume from CJS
const { produce } = require("immer")

Not perfect, but does get rid of the the .default in a consistent way while staying a true ESM module

@Lonniebiz You need to change your webpack config, like what I did in mine (skip the module field):

environment.config.set('resolve.mainFields', ['browser', 'main']) // default: ['module', 'main']

I am not sure if that will work with all libraries, but the consensus remains that this is an issue at webpack’s end and library authors have no obligation to support webpack in particular…

More context: http://2ality.com/2017/01/babel-esm-spec-mode.html#an-es-module-can-only-default-import-commonjs-modules

For library authors: As a workaround, you could:

  • offer a separate package like lodash does (lodash-es), or
  • skip the module field and use a es-specific entry point, like import a from 'yourpkg/es'
  • skip the module field, avoid es modules (my preferred solution for most of my modules)

The first two choices will be explicit and won’t depend on tooling (but they’ll need to be documented) The last choice just works with all CommonJS loaders/tools.

I can see both sides of this issue. On the one hand, you have the mathematical approach of @mweststrate’s table – looking for equivalence of expressions. That’s a natural way to look at things if you’re working on a tool like webpack.

On the other hand, a consumer doesn’t care about mathematical equivalence. He just cares about the UX of the tools he uses to build his webapps. Does the tool behave as programmer Joe Blow expects, or does the tool surprise him in unpleasant ways? Well, webpack doesn’t behave as expected for Joe Blow. Node is his reference point for require(). If it doesn’t work like node, Joe considers it broken. He expects webpack to act the same as node when you use require('foo'). It doesn’t work the same if you have to do require('foo').default.

Both sides are important. So is there a way to not break with the former while satisfying the latter? The issue exists only on the CJS side. Joe Blow’s app code works fine today if he writes it all in ESM with webpack. So we only need to consider CJS apps, whose source is wholly written with require() – this would probably be most of the existing and legacy apps.

So how do you allow for require('foo') to work with webpack consistently with node? One approach is simple: If you are bundling a CJS app, only pull in CJS main files. If all the source is CJS, you don’t have to worry about equivalency of require() to import statements. You can just use require() semantics as defined by node.

If you are bundling a CJS app, there isn’t much reason to pull in ESM dependencies: Every package on npm has a CJS main file that already works as expected with require(). And for older or legacy CJS apps, Joe Blow would expect it to be CJS all the way down anyway.

Let the new apps that use ESM get all the new module dependencies and dead code elimination goodies. Legacy CJS apps should just work, even if it’s without all the bells and whistles that webpack offers.

Why not this? Always works for me without any tricks and api is the same on both sides.

import { produce } from 'immer'

Common.js is not part of any standard I’m aware of. I expect it to go away like Macromedia Flash in the long run.

In the meantime, I’d like Common.js to be a graceful predecessor, and do all that it can to facilitate easy migration to its imminent successor (which is indeed specified).

It would be very kind for Common.js to evolve in a manner that eases migration to ES modules (while not breaking its own backwards compatibility, of course).

There is objection, https://www.bignerdranch.com/blog/default-exports-or-named-exports-why-not-both/ I am bitten by these things.

maybe webpack better to provide a optionMap to config each node module are default export or named export.

This is where default exports suck. They doesn’t work well for the purpose they exist: interop with commonjs. I treat all modules as a namespace for two years and didn’t had any of this problems. Probably we all should drop default exports and use only named ones?

Also, I am reporting you to support because these emails cannot be retracted…

On Tue, Feb 12, 2019, 7:44 PM Lonnie Best <notifications@github.com wrote:

So who or what is enforcing those specs? Internet Explorer?

I think you mother is the one doing it. That’s what I’ve been told.

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/webpack/webpack/issues/4742#issuecomment-462728551, or mute the thread https://github.com/notifications/unsubscribe-auth/AApA4qocIM4AHkba3yBlGwkJO54LiUeuks5vMqk4gaJpZM4NBHFU .

What spec says how require should handle ES Modules? Isn’t this essentially no-man’s land? Isn’t it better for everyone if there was an inverse interopDefault to facilitate… interoperability?

Because currently I cannot ship ES Modules as they will cause that ugly .default when users require them.

No other CommonJs impl does something like you proposed and it doesn’t make sense.

  1. It makes sense to me since both a = require() and import a from mean get the default export (“default” in the classical sense, not how it’s described by the ESM spec)

  2. rollup does that when requiring es modules:

    $ cat module.js 
    export default function foo () {}
    
    $ cat requirer.js 
    const test = require('./module.js');
    console.log(test)
    
    $ cat rollup.config.js 
    export default {
      format: 'cjs',
      plugins: [ require('rollup-plugin-commonjs')() ]
    };
    
    $ rollup requirer.js --config | node
    [Function: foo] # this is console.log(test)
    
  3. rollup does that when bundling exporting modules, export default X becomes module.exports = X, demo

The purpose of the module field is to offer the same package content

I already agreed with that in my previous comment.

What I’m suggesting is the equivalent of interopDefault when requiring ES default imports. Would you ever suggest using this API for your own modules?

const webpack = require('webpack').default;

.default should never be part of an API.


Now we could even agree that the current situation makes perfect technical sense, but this kills interoperability: it either results in poor APIs (.default) or into the unusability of the module field for default-exporting modules.

Packages should not export different things from module and main.

That makes sense, but in reality neither of those options is ideal. You wouldn’t expect anyone to use something like this:

var webpack = require('webpack').default; //<--
webpack(...);
//or
var webpack = require('webpack');
webpack.webpack(...); //<-- almost ok, but awkward

It defeats the purpose of having a separate module file at all since it only complicates things when using default exports. Without module both of these work as expected:

echo 'var ofi = require("object-fit-images"); ofi()' > src.js; webpack src.js dist.js
echo 'import ofi from "object-fit-images"; ofi()' > src.js; webpack src.js dist.js

Packages should not export different things from module and main.

The main/module fields in the package.json describe the entrypoint of the package in ESM and CJS. It doesn’t depend on the way you require/import it.

If if would depend on it, it would cause duplication i. e. if you require a module in one file and import it in another file.

So in my opinion this is working correctly. But like to hear more opinions…


I know this is bad for package author because this could mean they need to break the API when adding a ESM export. i. e. when previously exporting a function with module.exports = function objectFitImages() {} they need to change their API to exports.objectFitImages = function() {} or default. objectFitImages = require("object-fit-images") -> objectFitImages = require("object-fit-images").objectFitImages or objectFitImages = require("object-fit-images").default.