Skip to content

Question regarding mixing default and named exports #1961

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

Closed
transitive-bullshit opened this issue Feb 10, 2018 · 30 comments
Closed

Question regarding mixing default and named exports #1961

transitive-bullshit opened this issue Feb 10, 2018 · 30 comments

Comments

@transitive-bullshit
Copy link

I have a react module boilerplate which makes use of rollup, and some users are requesting the ability to export multiple components.

I'm bundling to cjs and es formats (rollup.config.js), so including both named and default exports from my bundle should be fine as I understand it, but rollup prints a warning about the use of mixed default and named exports.

import Foo from './Foo'
import Bar from './Bar'

// export Foo and Bar as named exports
export { Foo, Bar }

// alternative, more concise syntax for named exports
// export { default as Foo } from './Foo'

// you could also export a default object containing your module's interface
// note that if you export both named and default exports, rollup will complain.
// if you know how to prevent this warning, please let me know :)
// export default { Foo, Bar }

For more context, see this thread.

In particular, I can see why this would be a problem for umd bundles (source), but I'm not sure why rollup is complaining for es and cjs bundles.

Any idea how we could resolve this issue?

@TrySound
Copy link
Member

TrySound commented Feb 10, 2018

@transitive-bullshit I'm sure warning is quite descriptive and ask you to use explicit option to determine behavior.

@transitive-bullshit
Copy link
Author

Yep, the option docs were just a bit confusing, but changing my output to use exports: 'named' did the trick.

Thanks!

@transitive-bullshit
Copy link
Author

The docs state:

It is bad practice to mix default and named exports in the same module, though it is allowed by the specification.

I'd love to get some clarity on why this is considered a bad practice because empirically this is how many top-level libraries provide their exports.

Thanks!

@lukastaegert
Copy link
Member

It is not really a problem if you export to ESM (though some consider it bad style) but it definitely is a problem if you export to CJS and your consumer is not using a bundler.

As CJS only has a single exports or module.exports object, there is no concept of a default export other than assigning it directly to module.exports. In which case it is not possible to add additional named exports. Rollups exports: auto module illustrates this dilemma:

// only a default
// ESM input
export default 42;

// CJS output
var main = 42;
module.exports = main;
// only named
// ESM input
export var a = 42;

// CJS output
Object.defineProperty(exports, '__esModule', { value: true });
var a = 42;
exports.a = a;

Now if I would add a default export in the second case like I did in the first place, this would replace all named exports. Which is why the following happens if you add a default export:

// mixed
// ESM input
export var a = 42;
export default 42;

// CJS output
Object.defineProperty(exports, '__esModule', { value: true });
var a = 42;
var main = 42;
exports.a = a;
exports.default = main;

So your default export is added as a default key to your export and if someone requires your file directly in node, they will need to access the default via this property. Which is probably weird and counter-intuitive.

However if people use a bundler and ESM, they will not notice this because the __esModule property is used as a flag to tell the bundler: This was transpiled from ESM using named exports mode, if someone does a default import, just give them whatever is at the default key. But it is inconsistent with what happens for direct node consumers.

As for the "bad style": There are many articles describing the disadvantages of default exports, e.g. https://blog.neufund.org/why-we-have-banned-default-exports-and-you-should-do-the-same-d51fdc2cf2ad
But for CJS, you want to directly control what require('x') gives you, and a default export seems to be the perfect match for this. Mixing both, however, gives you all the disadvantages without the advantages. Hopes this helps a little.

@transitive-bullshit
Copy link
Author

Really appreciate the thorough answer @lukastaegert -- thanks so much!

@TrySound
Copy link
Member

I even prefer to omit default exports at all.

@morewry
Copy link

morewry commented Nov 17, 2018

I think this is only true depending on the type of the default export. If it's a function or class (e.g. constructor function), no big.

I have done both of these in CJS:

module.exports = function () {};
module.exports.foo = 'bar';
class Stuff {
  constructor() {}
  static get foo() {
    return 'bar';
  }
}
module.exports = Stuff;

Both are easy to wrangle with ESM.

@frenzzy
Copy link
Contributor

frenzzy commented Sep 25, 2019

// mixed
// ESM input
export var a = 42;
export default () => {};

// CJS output
Object.defineProperty(exports, '__esModule', { value: true });
var a = 42;
var main = () => {};
exports.a = a;
exports.default = main;

Is there a way in latest rollup to generate output without default keyword?
I would like to get something like this:

// desired CJS output
var a = 42;
var main = () => {};
exports = main;
exports.a = a;

I found --no-esModule option (Do not add __esModule property).
But can't find a way to assign default export directly to cjs exports.

@TrySound
Copy link
Member

Technically this is error prone way. What should produce import * as m from './module.js' statement?
If you want to achieve this for usage in commonjs better to be explicit.

var a = 42;
export default function main() {}
main.a = a;

@frenzzy
Copy link
Contributor

frenzzy commented Sep 25, 2019

I want to propose a ES version for path-to-regexp library to enable three-shaping in my code.
Currently its code looks like this:

module.exports = pathToRegexp
module.exports.parse = parse
module.exports.compile = compile
module.exports.tokensToFunction = tokensToFunction
module.exports.tokensToRegExp = tokensToRegExp

But there are a requirement to keep it backward compatible.
So, I converted exports to ES6 style like this and added new module field to package.json:

export default pathToRegexp
export { parse }
export { compile }
export { tokensToFunction }
export { tokensToRegExp }

now I am trying to generate backward compatible version for main field in package.json.
I see your point, then maybe there a way to generate something like this:

// CJS output
Object.defineProperty(exports, '__esModule', { value: true });
var a = 42;
var main = () => {};
exports = main; // <= how to add this line?
exports.a = a;
exports.default = main;

@TrySound
Copy link
Member

The best you can do here is to use different entry points and propose named exports later.

export default pathToRegexp
pathToRegexp.parse = parse
pathToRegexp.compile = compile
pathToRegexp.tokensToFunction = tokensToFunction
pathToRegexp.tokensToRegExp = tokensToRegExp
export { parse, compile, tokensToFunction, tokensToRegExp }
module.exports = pathToRegexp
pathToRegexp.default = pathToRegexp
pathToRegexp.parse = parse
pathToRegexp.compile = compile
pathToRegexp.tokensToFunction = tokensToFunction
pathToRegexp.tokensToRegExp = tokensToRegExp

Though mixing named and default exports is still error prune. If commonjs module uses package with module field which has mixed named and default exports webpack produces broken output. So even interop does not help here.

module.exports = {
  default = {
    default: function() {}
  }
}

So I recommend just do breaking change and migrate to named exports. They work fine. Default export was a mistake.

@frenzzy
Copy link
Contributor

frenzzy commented Sep 25, 2019

I agree mixing esm and cjs is wrong, but why generation of cjs file from esm with such content is a bad idea?

// CJS output
module.exports = main;
Object.defineProperty(module.exports, '__esModule', { value: true });
var a = 42;
var main = () => {};
module.exports.a = a;
module.exports.default = main;

UPD: I found a hackish workaround by using intro option
(CLI: rollup --intro "exports = module.exports = main;")

// CJS output
exports = module.exports = main;
Object.defineProperty(exports, '__esModule', { value: true });
var a = 42;
var main = () => {};
exports.a = a;
exports.default = main;

UPD2: See PR pillarjs/path-to-regexp#197

@LukasBombach
Copy link

@TrySound do you have a link to an article or something why mixing named and default exports is problematic? Because from a purely semantic / aesthetic point of view I find being able to import something like quite legit.

import Sblendid, { Peripheral } from "@sblendid/sblendid";

I don't want to exploit this issue for a discussion on this because this seems off topic and I could not find any proper articles on this, but maybe you know of something.

I came across this issue while implementing my library and posted a related question on StackOverflow, which may also be interesting to people who stumble across this issue

https://stackoverflow.com/questions/58246998/mixing-default-and-named-exports-with-rollup

@vvanpo
Copy link

vvanpo commented Nov 14, 2019

@lukastaegert

It is not really a problem if you export to ESM (though some consider it bad style) but it definitely is a problem if you export to CJS and your consumer is not using a bundler.

This kind of explanation needs to be added to the docs. I came here because the statement

It is bad practice

without further explanation or references was very bothersome to read in the documentation. Users of rollup shouldn't have to trawl the Github issues to make sense of the documentation.

@tonix-tuft
Copy link

Technically this is error prone way. What should produce import * as m from './module.js' statement?
If you want to achieve this for usage in commonjs better to be explicit.

var a = 42;
export default function main() {}
main.a = a;

Is this how React does it?

@TrySound
Copy link
Member

Yes, for now.

@tonix-tuft
Copy link

One thing I don't understand. If you look at React's index.js file, they do not export any default value at all: https://github.com/facebook/react/blob/master/packages/react/index.js

How then are we able to import React like this when using ES6:

import React, { useEffect, useState } from "react";

And this when using commonJS:

const React = require("react");
const { useEffect, useState } = require("react");

?

@TrySound
Copy link
Member

Rollup does not allow to generate such hacky exports. You should build them manually and then provide commonjs namedExports option because such exports cannot be detected.

Components.TimePicker = TimePicker
export default components

In upcoming commonjs version named exports will be detected from usage and you will not need to specify namedExports option.

@tonix-tuft
Copy link

tonix-tuft commented Apr 23, 2020

Rollup does not allow to generate such hacky exports.

But React uses Rollup, so I guess they achieve it somehow. How does the namedExports option work and how can I use it? Like this:

export default {
  input: "src/index.js",
  output: [
    {
      file: pkg.main,
      format: "cjs",
      sourcemap: true,
      exports: "named" // <---- ???
    },
    {
      file: pkg.module,
      format: "es",
      sourcemap: true,
    },
  ],

?

Thank you!

@lukastaegert
Copy link
Member

lukastaegert commented Apr 23, 2020

React is distributed as CommonJS. There is no defined interface between CommonJS and ESM except possibly what NodeJS has implemented now which is that CommonJS modules only have a default export. Bundler creators in the past however have allowed to use named import syntax with CommonJS. But this is a feature of whoever is IMPORTING your stuff. Now React has a huge problem. Because people now want for quite a while that React is distributed as ESM. However with ESM this hacky syntax no longer works. Which is why they are now telling everyone to use import * as React from React because that is what will work best for ESM. They also want to provide a fake default import but it is discouraged and may produce suboptimal bundling results when used.

@tonix-tuft
Copy link

@lukastaegert Sorry Lukas, one last question.

I have rechecked the React source here:

https://unpkg.com/[email protected]/cjs/react.development.js

Their CommonJS build exports stuff in the following way:

exports.Children = Children;
exports.Component = Component;
exports.Fragment = REACT_FRAGMENT_TYPE;
exports.Profiler = REACT_PROFILER_TYPE;
// And so ...

When I import React in my ESM consuming code I can use React this way:

import React from "react";

console.log(React); // { Children: ..., Component: ..., Fragment: ... , Profiler: ..., ... }

React.memo(...);
React.Children;
React.Component;
// And so on...

But, if I bundle my own library with Rollup using this code and Rollup config:

// src/index.js
import Component1 from "./Component1";
import Component2 from "./Component2";
import Component3 from "./Component3";

export {
    Component1,
    Component2,
    Component3
};
// rollup.config.js
import babel from "rollup-plugin-babel";
import commonjs from "@rollup/plugin-commonjs";
import external from "rollup-plugin-peer-deps-external";
import postcss from "rollup-plugin-postcss";
import resolve from "@rollup/plugin-node-resolve";
import url from "@rollup/plugin-url";
import svgr from "@svgr/rollup";
import inject from "@rollup/plugin-inject";

export default {
  input: "src/index.js",
  output: [
    // Output only CommonJS module in dist/index.js, just like React does.
    {
      file: "dist/index.js",
      format: "cjs",
      sourcemap: true,
    }
  ],
  plugins: [
    external(),
    postcss({
      modules: true,
    }),
    url(),
    svgr(),
    babel({
      exclude: "node_modules/**",
      runtimeHelpers: true,
      plugins: [["@babel/transform-runtime"]],
    }),
    resolve(),
    commonjs(),
    inject({
      moment: "moment"
    }),
  ],
};

I end up with this dist/index.js file:

'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }

...

exports.Component1 = Component1; // <--- Similar to React's CommonJS exports
exports.Component2 = Component2; // <--- "
exports.Component3 = Component3; // <--- "
//# sourceMappingURL=index.js.map // <--- "

Then, when I import it this way in some ESM consuming code:

import Components from "my-library";

console.log(Components); // undefined - But expected: Components.Component1, Components.Component2, ...

How can I obtain the same behaviour as React?

Thank you!

@lukastaegert
Copy link
Member

The __esModule property tells compiled ES module code where to find the default export in a CJS module. If it is present, consumers may assume that it can be found in exports.default, which of course is missing here. The easiest is probably to just skip this property by setting https://rollupjs.org/guide/en/#outputesmodule to false.

@tonix-tuft
Copy link

@lukastaegert That option did it, thank you for your reply, Lukas!

pastelmind added a commit to pastelmind/d2calc that referenced this issue Jul 18, 2020
When transpiling ESM to CJS, Rollup converts default exports to an explicitly exported
variable named 'default'. Since this is undesireable, let's convert it
to a named export.

Incidentally, Rollup's documentation states that:

> It is bad practice to mix default and named exports in the same module,
> though it is allowed by the specification.

More details on this here:

- rollup/rollup#1961 (comment)
- https://blog.neufund.org/why-we-have-banned-default-exports-and-you-should-do-the-same-d51fdc2cf2ad
@danielnunez
Copy link

@lukastaegert That option did it, thank you for your reply, Lukas!

hi, you have an example of the use of esModule, I have tried to do the same but it doesn't work for me

@dperera-innerspec
Copy link

Hi, I was able to achieve the functionality mentioned above with default setup of Rollup. Here is what i have in my setup:

rollup.config.js

export default {
  input: pkg.source,
  output: [
    {
      file: pkg.browser,
      format: 'cjs',
      exports: 'named'
    },
    {
      file: pkg.module,
      format: 'es',
      name: pkg.name
    }
  ]
}

I hope it helps.

@isc30
Copy link

isc30 commented Apr 12, 2022

faced this issue and it was related to using esModule: false. Removing that made it work

@avisek
Copy link

avisek commented Oct 8, 2023

Hi, I am able to achieve the functionality requested above by @frenzzy and @LukasBombach by patching rollup. I know it sounds bad. But for those who really wants it this way,

Here it is: https://github.com/avisek/rollup-patch-seamless-default-export

// Default export
const lib = require('your-library')
lib('Hello') // <-- instead of using `.default` property

// Named exports
const { namedExport1, namedExport2 } = lib

// One liner syntex. This is also supported.
const { default: defaultExport, namedExport1, namedExport2 } = require('your-library')

@mnrendra
Copy link

mnrendra commented Mar 6, 2024

Hi, you can use @mnrendra/rollup-plugin-mixexport. It will automatically generate the mixexport named and default.

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