webpack: Bug: require() doesn't support `default` exports in ES Modules
✅ ➡️ Edit 2020 for Webpack users:
- always prefer
import
nowadays. Only userequire
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
- Improve webpack 2 usage/compat Fixes #71 Context: https://github.com/webpack/webpack/issues/4742 — committed to fregante/object-fit-images by bfred-it 7 years ago
- Remove es-modules package https://github.com/webpack/webpack/issues/4742 — committed to fregante/image-promise by bfred-it 7 years ago
- Make ES module opt-in After releasing 7.3.0, we've seen folks run into issues (#221) with the new `module` field added to package.json. I've tried to read up on how webpack uses this field, and it lo... — committed to civiccc/react-waypoint by trotzig 7 years ago
- fix: configure webpack to not use esmodules in dependencies Big.js just released an update that added esmodule support by adding a "module" field to their package.json. By default, webpack will use t... — committed to ipfs/js-ipfs by alanshaw 6 years ago
- fix: configure webpack to not use esmodules in dependencies Big.js just released an update that added esmodule support by adding a "module" field to their package.json. By default, webpack will use t... — committed to ipfs-inactive/js-ipfs-http-client by alanshaw 6 years ago
- fix: configure webpack to not use esmodules in dependencies Big.js just released an update that added esmodule support by adding a "module" field to their package.json. By default, webpack will use t... — committed to ipfs/js-ipfs by alanshaw 6 years ago
- fix: configure webpack to not use esmodules in dependencies Big.js just released an update that added esmodule support by adding a "module" field to their package.json. By default, webpack will use t... — committed to ipfs-inactive/js-ipfs-http-client by alanshaw 6 years ago
- fixed loading of CommonJS modules (webpack/webpack#4742) — committed to prusswan/SBF2018May by prusswan 6 years ago
- fix: configure webpack to not use esmodules in dependencies Big.js just released an update that added esmodule support by adding a "module" field to their package.json. By default, webpack will use t... — committed to danieldaf/js-ipfs-api by alanshaw 6 years ago
- Fix TypeScript types — committed to fregante/delegate-it by fregante 5 years ago
- improve webpack support https://github.com/webpack/webpack/issues/4742 — committed to zsqk/r2 by iugo 5 years ago
- fix: load typeorm-aurora-data-api-driver correctly when using webpack (#4788) TypeORM uses require() within PlatformTools to load driver packages. The typeorm-aurora-data-api-driver is exported with ... — committed to coyoteecd/typeorm by coyoteecd 4 years ago
- fix: load typeorm-aurora-data-api-driver correctly when using webpack (#4788) (#5302) TypeORM uses require() within PlatformTools to load driver packages. The typeorm-aurora-data-api-driver is expor... — committed to typeorm/typeorm by coyoteecd 4 years ago
- Fix webpack generation of names for images see https://github.com/webpack/webpack/issues/4742 — committed to toebes/ciphers by toebes 3 years ago
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 userequire("./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. 😓
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 asimport * as a from "'
in ESM terms, and not the same asimport 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 (whererequire()
always imports the full module!).Edit: note that babel in the past has some interoperability magic that made
import
to work likerequire
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
thatpackage.json
->["module"]
->export default X
B
thatconst X = require("A")
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’trequire()
any named exports, and if you implementrequire("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 redirectrequire("A")
to somemodule.exports = require("A").default
shim?resolve.mainFields
– I can usepackage.json
->["main"]
vs.["module"]
, but not on a per-dependency basis?libraryExport: "default"
for dependencyA
vs. the output?What’s the best solution?
@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:
module.exports.cool =
const { cool } = require("x")
export const cool =
import { cool } from "x"
module.exports.default =
(not a special case!)const { default } = require("x")
export default ...
import cool from "x"
(any other name works as well)*module.exports = something cool
const cool = require("x")
default
, but it’s an entirely different beastimport * 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 themodule
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 themain
field, and underscorejs in themodule
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:
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):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:
lodash-es
), ormodule
field and use a es-specific entry point, likeimport a from 'yourpkg/es'
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 userequire('foo')
. It doesn’t work the same if you have to dorequire('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 CJSmain
files. If all the source is CJS, you don’t have to worry about equivalency ofrequire()
toimport
statements. You can just userequire()
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 withrequire()
. 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.
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:
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 inverseinteropDefault
to facilitate… interoperability?Because currently I cannot ship ES Modules as they will cause that ugly
.default
when usersrequire
them.It makes sense to me since both
a = require()
andimport a from
mean get the default export (“default” in the classical sense, not how it’s described by the ESM spec)rollup does that when requiring es modules:
rollup does that when bundling exporting modules,
export default X
becomesmodule.exports = X
, demoI 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?.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 themodule
field fordefault
-exporting modules.That makes sense, but in reality neither of those options is ideal. You wouldn’t expect anyone to use something like this:
It defeats the purpose of having a separate
module
file at all since it only complicates things when using default exports. Withoutmodule
both of these work as expected:Packages should not export different things from
module
andmain
.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 toexports.objectFitImages = function() {}
ordefault
.objectFitImages = require("object-fit-images")
->objectFitImages = require("object-fit-images").objectFitImages
orobjectFitImages = require("object-fit-images").default
.