React: Formalize top-level ES exports

Created on 9 Nov 2017  ·  104Comments  ·  Source: facebook/react

Currently we only ship CommonJS versions of all packages. However we might want to ship them as ESM in the future (https://github.com/facebook/react/issues/10021).

We can't quite easily do this because we haven't really decided on what top-level ES exports would look like from each package. For example, does react have a bunch of named exports, but also a default export called React? Should we encourage people to import * for better tree shaking? What about react-test-renderer/shallow that currently exports a class (and thus would start failing in Node were it converted to be a default export)?

Build Infrastructure React Core Team Breaking Change Discussion

Most helpful comment

It's almost 2020 now, I would like to know if there are any updates from the official FB team? Would there be any changes related to this in React v17?

All 104 comments

Imho import * is a way to go, Im not opposed to having a default export too, but it shouldnt be used to reexport other stuff like in this example:

export const Component = ...
export default React
React.Component = Component

but it shouldnt be used to reexport other stuff like in this example:

Is there a technical reason why? (Aside from having two ways to do the same thing.)

My impression is that people who would import * (and not use the default) wouldn't have problems tree shaking since default would stay unused. But maybe I overestimate Rollup etc.

That questions can be probably best answered by @lukastaegert. Ain't sure if something has changed since https://github.com/facebook/react/issues/10021#issuecomment-335128611

Also Rollup is not the only tree shaker out there, and while webpack's tree-shaking algorithm is worse than the one in rollup, it's usage is probably way higher than rollup's (both tools do excellent jobs ofc, I don't want to offend anyone, just stating facts) and if we can (as the community) help both tools at once we should do so whenever we can.

is tree-shaking going to _do_ anything in React's case, given that everything is preprocessed into a single flat bundle? I wonder what the primary import style is for React, personally i tend to treat it like a default export e.g. React.Component, React.Children but occasionally do the named thing with cloneElement

As @gaearon already stated elsewhere, size improvements in case of react are expected to be minimal. Nevertheless, there ARE advantages:

  • React.Children might probably be removed in some cases (so I heard 😉)
  • React itself can be hoisted into the top scope by module bundlers that support this. This could again remove quite a few bytes and might also grant an oh-so-slight performance improvement. The main improvement would lie in the fact that there does not need to be another variable that references React.Component for every module but just one that is shared everywhere (this is how rollup usually does it). Also, though this is just me guessing, this might reduce the chance of webpack's ModuleConcatenationPlugin bailing out
  • Static analysis for react is easier not only for module bundlers but also for e.g. IDEs and other tools. Many such tools already do a reasonable job at this for CJS modules but in the end, there is a lot of guessing involved on their side. With ES6 modules, analysis is a no-brainer.

As for the kind of exports, of course only named export really provide the benefit of easy tree-shaking (unless you use GCC which might be able to do a little more in its aggressive move and maybe the latest rollup if you are really lucky). The question if you provide a default export as well is more difficult to decide:

  • PRO: Painless migration for existing ES6 code bases (e.g. what @jquense describes)
  • CON: Since everything is attached to a common object, once this object is included, all its keys are included at once which again defeats any attempts at tree-shaking. Even GCC might have a hard time here.

As a two-version migration strategy, you might add a default export in the next version for compatibility purposes which is declared deprecated (it might even display a warning via a getter etc.) and then remove it in a later version.

This is also an interesting case: https://github.com/facebook/react/issues/11526. While monkeypatching for testing is a bit shady, we'll want to be conscious about breaking this (or having a workaround for it).

Came here via this Twitter conversation. For me, there's a clear correct answer to this question: React and ReactDOM should only export named exports. They're not objects that contain state, or that other libraries can mutate or attach properties to (#11526 notwithstanding) — the only reason they exist is as a place to 'put' Component, createElement and so on. In other words, namespaces, which should be imported as such.

(It also makes life easier for bundlers, but that's neither here nor there.)

Of course, that does present a breaking change for people currently using a default import and transpiling. @lukastaegert probably has the right idea here, using accessors to print deprecation warnings. These could be removed in version 17, perhaps?

I don't have a ready-made suggestion for #11526 though. Perhaps shipping ESM would have wait for v17 for that reason anyway, in which case there'd be no need to worry about deprecation warnings.

People have really come to like

import React, { Component } from 'react'

so convincing them to give it up might be difficult.

I guess this is not too bad, even if a bit odd:

import * as React from 'react';
import { Component } from 'react';

To clarify, we need React to be in scope (in this case, as a namespace) because JSX transpiles to React.createElement(). We could break JSX and say it depends on global jsx() function instead. Then imports would look like:

import {jsx, Component} from 'react';

which is maybe okay but a huge change. This would also mean React UMD builds now need to set window.jsx too.

Why am I suggesting jsx instead of createElement? Well, createElement is already overloaded (document.createElement) and while it's okay with React. qualifier, without it claiming it on the global is just too much. Tbh I’m not super excited about either of these options, and think this would probably be the best middle ground:

import * as React from 'react';
import { Component } from 'react';

and keep JSX transpiling to React.createElement by default.

Confession: I always found it slightly odd that you have to explicitly import React in order to use JSX, even though you're not actually using that identifier anywhere. Perhaps in future, transpilers could insert import * as React from 'react' (configurable for the sake of Preact etc) on encountering JSX, if it doesn't already exist? That way you'd only need to do this...

import { Component } from 'react';

...and the namespace import would be taken care of automatically.

In a distant future, maybe. For now we need to make sure transpilers work with other module systems (CommonJS or globals). Making this configurable is also a hurdle, and further splits the community.

What @Rich-Harris suggested (inserting a specific import when jsx is used) is easily done by transpilers plugin. The community would have to upgrade their babel-plugin-transform-react-jsx and that's it. And of course even existing setups would still work if only one adds import * as React from 'react'; to the file.

Of course we need to consider other module systems, but it doesn't seem like a hard problem to solve. Are there any specific gotchas in mind?

Of course we need to consider other module systems, but it doesn't seem like a hard problem to solve. Are there any specific gotchas in mind?

I don’t know, what is your specific suggestion as to how to handle it? Would what the default be for Babel JSX plugin?

People have really come to like

import React, { Component } from 'react'

What people? Come forth so that I may mock thee.

I did that a lot 🙂 Pretty sure I've seen this in other places too.

Default is at the moment React.createElement and it would pretty much stay the same. The only problem is that it assumes a global now (or already available in the scope).

I think as es modules are basically the standard way (although not yet adopted by all) of doing modules, it is reasonable to assume majority is (or should) use it. Vast majority already uses various build step tools to create their bundles - which is even more true in this discussion because we are talking about transpiling jsx syntax. Changing the default behaviour of the jsx plugin to auto insertion of React.createElement into the scope is imho reasonable thing to do. We are at the perfect time for this change with babel@7 coming soon (-ish). With recent addition of babel-helper-module-imports it is also easier than ever to insert the right type of the import (es/cjs) to the file.

Having this configurable to bail out to today's behaviour (assuming present in scope) seems really like a minor change in configuration needed for a minority of users and an improvement (sure, not a big one - but still) for majority.

Should we encourage people to import * for better tree shaking?

Thanks to @alexlamsl uglify-es has eliminated the export default penalty in common scenarios:

$ cat mod.js 
export default {
    foo: 1,
    bar: 2,
    square: (x) => x * x,
    cube: (x) => x * x * x,
};
$ cat main.js 
import mod from './mod.js'
console.log(mod.foo, mod.cube(mod.bar));



md5-d6d4ede42fc8d7f66e23b62d7795acb9



$ uglifyjs -V
uglify-es 3.2.1

```js
$ cat bundle.js | uglifyjs --toplevel -bc
var mod_foo = 1, mod_bar = 2, mod_cube = x => x * x * x;

console.log(mod_foo, mod_cube(mod_bar));
$ cat bundle.js | uglifyjs --toplevel -mc passes=3
console.log(1,8);

wow, that's great new 👏 is uglify-es considered to be stable now? I recall you mentioning few months back that it isn't there quite yet, but I can remember that incorrectly, so ain't sure.

Anyway - that's all and nice in a rollup world, but considering that React is bundled mostly in apps and those use mostly webpack which does not do scope hoisting by default, I'd still say that exporting an object as default should be avoided to aid other tools than uglisy-es+rollup in their efforts to produce smaller bundle sizes. Also for me it is semantically better to avoid this - what libs actually do in such cases is providing a namespace and it is better represented when using import * as Namespace from 'namespace'

is uglify-es considered to be stable now?

As stable as anything else in the JS ecosystem. Over 500K downloads per week.

that's all and nice in a rollup world, but considering that React is bundled mostly in apps and those use mostly webpack which does not do scope hoisting by default

Anyway, it's an option. Webpack defaults are not ideal anyway - you have to use ModuleConcatenationPlugin as you know.

Adding a few cents here:

  • I totally agree with @Rich-Harris that semantically, named exports are the right choice
  • I really do not like either import React from 'react' or import * as React from 'react' just to be able to use JSX syntax. In my eyes, this design is clearly violating the Interface Segregation Principle in that it forces users to import all of React just to be able to use the createElement part (though admittedly with a namespace export, a bundler like Rollup will strip out the unneeded exports again)

So if we are at a point where we might make breaking-change decisions, I would advise to change this so that JSX depends on a single (global or imported) function. I would have called it createJSXElement(), which in my opinion describes it even better than createElement() and no longer needs the React context to make sense. But in a world where every byte counts, jsx() is probably ok, too.

This would also at last decouple JSX from React in a way such that other libraries can choose to support JSX by using the same transformation and supplying a different jsx function. Of course you have a lot of responsibility here guiding countless established applications through such a transformation but from an architectural point of view, this is where I think React and JSX should be heading. Using Babel to do the heavy lifting of such a transformation sounds like a great idea to me!

Personally I do not see much gain in migrating to jsx helper as the default IMHO for the babel plugin should be importing it from the react package, so the name of the actual helper doesn't really matter - the rest is just matter of having it configurable.

This is probably slightly tangential to the main discussion, but I'm curious how well ES modules work with checking process.env.NODE_ENV to conditionally export dev/prod bundles? For example,

https://github.com/facebook/react/blob/d9c1dbd61772f8f8ab0cdf389e70463d704c480b/packages/react/npm/index.js#L3-L7

I may be missing something obvious here, but I'm struggling to see how to translate this pattern into ES modules?

@NMinhNguyen Conditional exports aren't possible with ES modules.

process.env.NODE_ENV checks can be at more granular (code) level though, ready to be replaced by the bundler with appropriate values.

@Andarist @milesj Thanks for confirming my suspicion :)

process.env.NODE_ENV checks can be at more granular (code) level though, ready to be replaced by the bundler with appropriate values.

From the React 16 blog post I thought that the process.env.NODE_ENV checks were pulled out to the very top on purpose (as opposed to them being more granular, which is what they are in the source, if I'm not mistaken), to help performance in Node.js?

Better server-side rendering

React 16 includes a completely rewritten server renderer. It's really fast. It supports streaming, so you can start sending bytes to the client faster. And thanks to a new packaging strategy that compiles away process.env checks (Believe it or not, reading process.env in Node is really slow!), you no longer need to bundle React to get good server-rendering performance.

Like, I'm not sure how one could use the module field in package.json and differentiate between dev/prod for ESM while keeping ES bundles flat and not affecting Node.js perf

Like, I'm not sure how one could use the module field in package.json and differentiate between dev/prod for ESM while keeping ES bundles flat and not affecting Node.js perf

This for sure is a drawback, because there is no standard way at the moment for doing this. OTOH it's just a matter of tooling, it is possible (and it's rather easy) to compile this in build steps of your application even today. Ofc it would be easier if package could expose dev/prod builds and the resolver would just know which one to pick, but maybe that's just a matter of pushing this idea to tooling authors.

For class:

import Component from 'react/Component'

class MyButton extends Component{
  constructor(){
    this.state = {}
  }

  render() {
    return <button> Button <Button>
  }
}

Where transform will use super.createElement() to transform to jsx or use static Component.createElement().

For stateless components:

import jsx from 'react/jsx'

const MyButton = () => jsx`<button> Button <Button>`;

it is maybe possible to use tagged template literal?

Node hopefully accept this PR https://github.com/nodejs/node/pull/18392

We agree here with @Rich-Harris.

Just dropping a comment on this thread which hasn't really been mentioned specifically.

I’m in a situation where I’m not using a bundler at all and just want to import react and various components for use natively through the browser (<script type="module" src="...">), i.e.

import React from “https://unpkg.com/[email protected]/umd/react.development.js”;
import ReactDOM from “https://unpkg.com/[email protected]/umd/react-dom.development.js”;
ReactDOM.render(
  React.createElement(...),
  document.getElementById('root')
);

From what I can tell, this isn't possible today. Instead, I have to include the UMD version of react via a <script> tag from the CDN and then assume it’s presence on the window in any <script type="module"> module I write:

// myPage.html
<div id="myComponentRoot"></div>
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script type="module" src="/assets/scripts/components/MyComponent.js"></script>

// MyComponent.js
import AnotherComponent from "/assets/scripts/components/AnotherComponent.js";
window.ReactDOM.render(
  window.React.createElement(AnotherComponent),
  document.getElementById('root')
);

// AnotherComponent.js
export default class AnotherComponent extends window.React.Component {...}

Having a react import from a CDN would be fantastic. It would make prototyping in the browser very quick and easy while still being able to maintain separation of files. One thing I always felt I was sacrificing when using React without a bundler was the ability to separate components (and other utility functions, etc) by file. But now with browser support for native ES modules, I can write my React components in separate files and have the browser just consume them as they're written. Granted that's if I'm not using JSX, but even if I was using JSX, I could transpire all the files in place via a build step and all my imports would still work in the browser.

// /assets/scripts/entry.js
import React from “https://unpkg.com/[email protected]/umd/react.development.js”;
import React from “https://unpkg.com/[email protected]/umd/react-dom.development.js”;
import RelatedPosts from "/assets/scripts/components/RelatedPosts.js";
ReactDOM.render(
  React.createElement(RelatedPosts),
  document.getElementById('root')
);

// /assets/scripts/components/RelatedPosts.js
import React from “https://unpkg.com/[email protected]/umd/react.development.js”;
import ListItem from "/assets/scripts/components/ListItem.js"
export default class MyComponent extends React.Component {
  componentDidMount() { /* fetch some data */ }
  render() { 
    return React.createElement(
      'ul',
      {},
      this.state.items.map(item => React.createElement(ListItem, { item: item })
    )
  }
}

// /assets/scripts/components/ListItem.js
import React from “https://unpkg.com/[email protected]/umd/react.development.js”;
export default function ListItem(props) {
  return React.createElement('li', null, ...)
}

I'm sure some people would argue typing that CDN url all the time is a problem (a problem some people are trying to fix) but the tradeoffs are worth it to me. Changing/updating that url is a simple find/replace. For my use case, this outweighs the trouble of setting up a bundler.

If React had support for something like this, there would be no need for tooling. I'm just using the browser. I could ship code like this in a few personal projects which assume modern browsers and use react as a progressive enhancement on the page. What makes this fantastic is when I come back to the code base in 12 months, I don't have to change a bunch of tooling APIs or even have NPM as a package manager. I'm just using APIs from the browser, nothing else.

FWIW: if/when React does ship with support like this, I think it could be very valuable to show how you could use React like this in the docs, teaching that you can leverage React and its component model by separating each components logic via its own file and you don't need a bundler to do it, just use native <script type="module">, import React from a CDN (or your own local copy), and your off!”

Now, all of the modern browsers including mobile versions support ESM. ESM is no longer a future module system but a current defacto-standard.

Please be aware of not providing the standardized module is a critical problem, especially for a defacto-standard web library.

import * as React from 'react';
import * as ReactDOM from 'react-dom';

This is the typical code to apply React libraries, and the fact has been there are not actually libraries that can be imported, instead, 3rd party transpilers and bundlers emulate the import process.

It's been slightly justified not providing the real ESM since browsers had not supported the native ESM anyway, but obviously, the time is up, and now is the time to provide ESM as specified the typical sample code to import.

I started working on this here and here

@TrySound Thanks for your contribution.
Is there any place to grab and test the ESM build?

It's ready only for react-is package.

@TrySound
Ok, I found your branch https://github.com/TrySound/react/tree/react-is-esm, and have built, and now I know what you meant. Looking forward to react-dom too.

I think the React community discussed this issue quite enough for a while.
https://discuss.reactjs.org/t/es6-import-as-react-vs-import-react/360/

Please decide the official ES6 module specification, and publish soon.

@kenokabe We are on the way. Don't force us please. It's not that easy.

Current plan is migrating all packages with only named exports. This change won't affect libraries code and shouldn't introduce breaking changes since docs uses named exports too.

For another packages we need to handle both default and named exports which work differently with various tools.

@TrySound My apologies.
I didn't mean to you, since the head mention of this topic is

We can't quite easily do this because we haven't really decided on what top-level ES exports would look like from each package. For example, does react have a bunch of named exports, but also a default export called React? Should we encourage people to import * for better tree shaking?

and, the day mentioned is a while ago, and I just thought it's been discussed in React community, so I wanted to suggest the decision would be clear. Thanks!

Want get some update on this...

I'm using webpack v4 for bundling our application, while my IDE intellisense (WebStorm) suggest me to use import * as React from 'react'; while my coworker ask me to change import React from 'react'; in a code review. Both works fine so I thought he is saying some nonsense, but to make him happy I'm changing it anyway. That's also how I find this thread.

While out of curious, I compare the differences at the final build size between it (with React 16.8.1):

In import * as React from 'react';: 6,618,723 bytes
In import React from 'react';: 6,619,077 bytes

So obviously, it did have some differences, marginal though. (note. I have did the same with propTypes)

If my understanding in this thread correctly, it would be favour of having import * as React from 'react';, right?! Because (1) yes, it did save some size; (2) ESM is a standardized way so no more CJS leftovers. If that is the case I'd like to change this today and align with my IDE.

@leoyli In long term yes. But first there will be both named and default exports to not break existing code.

I took matters into my own hands here, somewhat as an experiment as I am not using a bundler in my projects anymore and wanted to still use react (direct from unpkg.com like you can with other libraries such as Vue, Hyperapp etc.). This is what I came up with, nothing fancy, just a hand edited umd:

https://github.com/lukejacksonn/es-react

An ES6 module exposing the latest version of React and ReactDOM

As described in the README this is mostly a POC but for people that cannot wait for this build to land then it is a 16.8.3 build which includes hooks, suspense, lazy etc. and works as expected by doing:

import { React, ReactDOM } from 'https://unpkg.com/es-react'

Maybe some of you in this thread will find it useful.. personally I have been using this approach to create a build step free react starter project. It is also still a work in progress.

@lukejacksonn We've been using such a solution on production as well, while our approach was different in the sense that it is more a transformer script for the UMD version of React and ReactDOM in your current project. And that it outputs these file separately so for the most code out there it should be a drop in replacement. If you're interested https://github.com/wearespindle/react-ecmascript and you can also load it from unpkg https://unpkg.com/react-ecmascript/

@PM5544 Oh wow.. this is a much more comprehensive solution than mine! Great job 💯

Awesome stuff @PM5544. Would love to hear more about it someday. Maybe a guest appearance back at Xebia?
I've recently adopted pack to bundle my open source packages, which supports UNPKG.
Anyone know of a good article on loading dependencies from UNPKG directly rather than using a bundler?

I'm currently writing one and will be giving it as a talk at React Norway in June too!

@TrySound Has there been any update on this since February? What's left to get this issue moving, and can I participate in getting this issue closed by some coding work? I already signed the CA, and I have time available today to work on it.

This need to be merged first https://github.com/facebook/react/pull/15037

@TrySound Okay thanks I have forwarded my offer for assistance to that thread.

When you go with a default React export you can go with this approach:

// react/index.js
import * as React from "./react";
export { React as default }
export * from "./react";

// react/react.js
export function createElement() {}
...

This make it statically analyse-able that the default export is a namespace object which allows tree-shaking for these constructs in webpack 5 and rollup:

import React from "react";

React.createElement(); // <- only `createElement` export is used

I am in the rollup gitter chat for 1.5 years and this kind of issues comes up every 2 weeks or so ...

BTW, @lukejacksonn did a tremendous job on the inofficial es-react fork, can highly recommend it.

BTW, @lukejacksonn did a tremendous job on the inofficial es-react fork, can highly recommend it.

I wonder why the official FB team does nothing to this.

I was also hoping this would generate some pressure to move things forward, and I guess so was @lukejacksonn himself. As I understand, though, he actually received some support from the React team to build his fork from the original sources, so there seems to be at least some interest in this.

I hoped this too indeed. Actually I received next to no support from the react team in the creation of the package (besides some kind words of encouragement from @threepointone). Recently a colleague here at Formidable helped me build the package programatically which is an improvement over doing it by hand every time a new version of react is released but does result in much less clean output in the network tab so I am not sure if it will stay like this yet. We shall see!

It's almost 2020 now, I would like to know if there are any updates from the official FB team? Would there be any changes related to this in React v17?

For those needing an updated ES-module React NOW, try @pica/react, which is already on v16.13.x
https://www.npmjs.com/package/@pika/react
https://www.npmjs.com/package/@pika/react-dom
https://github.com/pikapkg/react

In a distant future, maybe.

Ok, already in the future. Is it in the near future now?

@gaearon what is the blocker for deciding how to structure the exports (default export vs named exports)? Is there anything the community can help you with making this decision?

Apparently that decision has already been made a while ago: #18102. This issue can be closed now.

I'll give a small update on this.

No Default Exports

Our eventual plan is to move away from default exports entirely:

import { useState } from 'react';

In that world, this would not work:

import React from 'react'; // no

This would work although it's a bit noisy:

import * as React from 'react';

But here's the kicker. There wouldn't actually be much reason to import React at all.

JSX Auto Import

The reason people import React today is mostly due to JSX. But @lunaruan is finishing work on the new JSX transform and related codemods, which remove the need for it.

So you would go from this:

import React from 'react';
import { useState } from 'react';

function Button() {
  const [pressed, setPressed] = useState(false)
  return <button />
}

to this:

import { useState } from 'react';

function Button() {
  const [pressed, setPressed] = useState(false)
  return <button />
}

JSX inserts the correct import automatically under the hood, so no need for React in scope.
This is what makes the move to remove default exports tolerable. You just don't need them as much.

ES Modules

Deploying ESM beyond an overall small slice of enthusiasts is challenging. The broad ecosystem isn't really ready and there's a ton of ways in which things go wrong with different tool combinations. The way CJS and ESM interact is very complex, and that interop (and how it fails) is the source of most of these issues.

So our current thinking is that when we go ESM, we might want to try go ESM all the way. No CJS at all — or separated in a compat legacy package. This won't happen in React 17 and is unlikely in 18, but is plausible to try in React 19.

For anybody looking for an alternative to @pika/react's ESM build, check out https://github.com/esm-bundle/react and https://github.com/esm-bundle/react-dom. The difference is that those are usable in browsers without an import map polyfill - the import React from 'react'; inside of react-dom's source code is altered to import React from a full CDN url. Another difference is that new versions are autopublished whenever a new react version is published, without any manual steps.

Code Sandbox demonstrating: https://codesandbox.io/s/gifted-roentgen-qcqoj?file=/index.html

A few people have challenged the notion that the ecosystem isn't ready. I don't have full context on this but if you think this is a good time to start making changes, I'd appreciate if you could look over https://github.com/reactjs/rfcs/pull/38 and express any concerns about it. Is that roughly would you had in mind, or would you prefer a different approach?

But here's the kicker. There wouldn't actually be much reason to import React at all.

There is if you are using TypeScript, and will continue so for the foreseeable future. Until TS learns about this new magic behaviour of babel, developers will have to continue to explicitly import React, and they need to know what the correct import statement is.

JSX inserts the correct import automatically under the hood, so no need for React in scope.

s/JSX/The new babel react jsx transform plugin/. JSX is a syntax extension to JavaScript, it by itself doesn't do anything.

There is if you are using TypeScript, and will continue so for the foreseeable future. Until TS learns about this new magic behaviour of babel, developers will have to continue to explicitly import React, and they need to know what the correct import statement is.

The TypeScript team is aware of this change, and it's being tracked in https://github.com/microsoft/TypeScript/issues/34547. We are in close contact with them so please be assured it's not a second-class citizen for us.

s/JSX/The new babel react jsx transform plugin/

Yes, this is what I meant!

I'd appreciate if you could look over reactjs/rfcs#38 and express any concerns about it. Is that roughly would you had in mind, or would you prefer a different approach?

Some of the RFC's original text is out of date. NodeJS allows for ESM to run in .js files instead of .mjs now, when you specify a "type" in your package.json. (docs).

Specifically, the RFC original text says the following which is not true:

ESM code must be inside of a .mjs file

After talking with @frehner, here's our proposal for how React could be incrementally converted to ESM. Note that we do not couple the named/default exports problem with publishing an ESM version of React. @gaearon has clarified that the default export will eventually go away, but that is not listed in our proposal until Phase 4.

| | Phase 1 | Phase 2 | Phase 3 | Phase 4 |
| - | -------- | -------- | -------- | ------- |
| ESM published? | ✔️ | ✔️ | ✔️ | ✔️ |
| package.json "module" | ❌ | ✔️ | ✔️ | ✔️ |
| webpack/rollup use esm 1, 2 | ❌ | ✔️ | ✔️ | ✔️ |
| package.json "exports" | ❌ | ❌ | ✔️ | ✔️ |
| package.json "type" | ❌ | ❌ | ✔️ | ✔️ |
| NodeJS uses esm | ❌ | ❌ | ✔️ | ✔️ |
| Breaking change? | ❌ | ❌ | ❓ | ✔️ |
| Default export gone? | ❌ | ❌ | ❌ | ✔️ |
| File extensions required in imports | ❌ | ❌ | ❌ | ❌ |
| mjs file extensions | ❌ | ❌ | ❌ | ❌ |

I think there is a valid argument for combining Phase 1 and Phase 2 together, since Phase 1 really only targets enthusiasts. However, I split them because I think that separating the phases gives a chance to very slowly roll out ESM in a way that won't immediately break CRA and the entire ecosystem without first giving the early adopters a chance to report issues and find any fixes.

@joeldenning what would be the rough timing of the phases? is it just time-based or are those related to some time checkpoints in the ecosystem? Would require('react') still work in Phase 3?

what would be the rough timing of the phases? is it just time-based or are those related to some time checkpoints in the ecosystem?

The timing for all of the phases is only limited by the work to be done and React's release schedule. My understanding is that all of the proposed things are supported by bundlers and nodejs, with appropriate fallbacks when using older versions that don't support them.

Would require('react') still work in Phase 3?

I think so, yes. See https://nodejs.org/api/esm.html#esm_dual_commonjs_es_module_packages and https://nodejs.org/api/esm.html#esm_package_entry_points. It's of course quite possible that I don't know all the corner cases for everything, but my understanding is that the transition path I've proposed "will work everywhere." Maybe those are famous last words 😄

An additional clarification, here is what we're proposing the published tarball would look like for React:

node_modules/react/
  cjs/
    react.development.js
    react.production.min.js
    react.profiling.min.js
  umd/
    react.development.js
    react.production.min.js
    react.profiling.min.js
  esm/
    react.development.js
    react.production.min.js
    react.profiling.min.js
  index.js

And here is an approximation of what the package.json would be at the end:

{
  "type": "module",
  "main": "index.js",
  "module": "esm/react.development.js",
  "exports": {
    "import": "./esm/react.development.js",
    "require": "./cjs/react.development.js"
  }
}

^ This isn't fully thought through and perfected, but I'm sharing to give concrete context to the proposal.

So - React suffers from the dual package hazard because it's stateful (hooks), so this state would have to be carefully isolated. Either it should live in a small CJS file that could be imported by both CJS and ESM entries or maybe there is a way load .json and mutate it with that state from both entries (I'm not 100% sure about the second approach).

I also think that it's important to list in your table adding named exports, which would happen already in Phase 1.

I'm still not 100% sure if I haven't missed some corner cases, this topic is very complex. At the same time, the schedule should be tied to the ecosystem checkpoints - Phase 3 can only be shipped once exports has sufficient support in bundlers, otherwise I feel this could lead to potential issues.

React suffers from the dual package hazard because it's stateful (hooks), so this state would have to be carefully isolated.

Good point, I hadn't considered this (more info). In that case, perhaps your suggestion of a shared file between the CJS / ESM builds would be useful. Or perhaps the ESM version is not a full copy of react, but just the ESM public interface that calls into the CJS build.

I also think that it's important to list in your table adding named exports, which would happen already in Phase 1.

It seems that this is already the case, though, in the source code? From what I can tell, the source code already exports things as named exports.

https://github.com/facebook/react/blob/ef22aecfc52cdf0d7cedc723c590719c009c2a64/packages/react/index.js#L39

https://github.com/facebook/react/blob/ef22aecfc52cdf0d7cedc723c590719c009c2a64/packages/react/index.js#L39

I'm still not 100% sure if I haven't missed some corner cases, this topic is very complex. At the same time, the schedule should be tied to the ecosystem checkpoints - Phase 3 can only be shipped once exports has sufficient support in bundlers, otherwise I feel this could lead to potential issues.

Agreed about Phase 3 - this is why I put a question mark for whether it was a breaking change. I know that adding package.json exports has often been a breaking change for other packages in the ecosystem. And the topic is definitely complex. One thing to note is that the order of Phase 3 and 4 could be swapped, if desired. I think that those implementing each phase would have to do very very very thorough testing of many versions of webpack, rollup, nodejs, etc. I'm not saying that the work to be done is trivial - just saying that I do think that there is likely a transition pathway here :)

It seems that this is already the case, though, in the source code? From what I can tell, the source code already exports things as named exports.

Ah - right, in this case there should be a table row for adding export default 😂 as it currently works in most bundlers and is popular in the wild, but adding true ESM entry without providing default would break those usages.

in this case there should be a table row for adding export default

Yes, good point. I'll add that. I think that a PR doing that would "seem bad," but I view it as just accepting the current public interface for what it is, with plans to improve in the future.

We should note that as soon as some version is published, there's no going back and any semantic change to it would be breaking. Given we don't release majors very often, we should consider how to batch any changes so that there's least churn.

There's also a question of how development/production build split would be handled. And indeed, stateful nature of the React build is pretty important to preserve.

We should note that as soon as some version is published, there's no going back and any semantic change to it would be breaking. Given we don't release majors very often, we should consider how to batch any changes so that there's least churn.

Good points. My idea is to add an explicit export default React to an ESM build that is published in React 16. I think the PR might raise some eyebrows and might be controversial, as that is not the destination that has been decided on. However, my view is that we can have an ESM build in React 16 and then remove the export default in a future major version. To me, using react via import React from 'react'; is so overwhelmingly common that exporting default in the ESM build is simply "accepting where we are." A future major version would remove it.

Also related to minimizing breaking changes - phase 3 and 4 could be part of the same major release. It's possible that phase 3 could be done in a fully backwards compatible way, in which case it could also sneak into a 16 release. But that one is trickier and I don't know enough about it to be confident in that.

There's also a question of how development/production build split would be handled.

This is something I overlooked. I don't know how to do this with ESM in both bundlers and in NodeJS, but will do some research to see what's possible. I found this dead proposal, but will look into alive ones :)

And indeed, stateful nature of the React build is pretty important to preserve.

Agreed. One thing to note is that the stateful nature of the React build only needs to be solved in Phase 3, not in Phases 1 and 2. The options that @Andarist and I suggested would work for solving it.

The easiest, most backwards-compatible approach for now is to add a simple ESM wrapper to allow for named imports.

I'd be happy to make a PR to add this to React, ensuring that the addition of the "exports" field is not a breaking change (i wrote a tool, npx ls-exports, which makes determining this easy). It won't help folks trying to use ESM without a build process, but that's a problem that individual packages aren't capable of solving anyways.

@gaearon would that be useful? It could land in React 16 as a semver-minor.

The CJS build would continue to use environment detection, as it does now, so that (hard, unsolved) problem in ESM doesn't have to be figured out yet.

However, my view is that we can have an ESM build in React 16 and then remove the export default in a future major version.

Another concern is that increasing the number of configurations puts the strain on the ecosystem. E.g. I think if we do release an ESM build it should not include something that we're definitely going to remove in the very next release, such as a default export. In other words, I don't think it makes a lot of sense to introduce something that's going away (default exports) into something that's just being added (ESM build).

However, my view is that we can have an ESM build in React 16 and then remove the export default in a future major version.

Another concern is that increasing the number of configurations puts the strain on the ecosystem. E.g. I think if we do release an ESM build it should not include something that we're definitely going to remove in the very next release, such as a default export. In other words, I don't think it makes a lot of sense to introduce something that's going away (default exports) into something that's just being added (ESM build).

I think it depends on how long that next release is though. If we’re talking about a week difference, then that seems reasonable. However, if we’re talking months/years, then I don’t see why it would be bad to get it out for that amount of time

The concern in this case is that the longer it's out there, the more people depend on it in the current form. And then introducing a breaking change there would make it more difficult for them to upgrade React. Now there is not just one ESM migration, but several, and some people will be left behind because they jumped too early and later don't have the resources for another migration. Which, in case of ESM, will ripple across the ecosystem.

I think if we do release an ESM build it should not include something that we're definitely going to remove in the very next release, such as a default export. In other words, I don't think it makes a lot of sense to introduce something that's going away (default exports) into something that's just being added (ESM build).

I don't view import React from 'react' as something newly added, but rather an acceptance of current reality. Even though it was perhaps unintentional and only a side effect of now-obsolete ESM/CJS interop, there are still thousands (millions?) of lines of code that do it. The alternative (only exporting named exports) says to users "we published an ESM build in a minor version, but you can't use it without changing all your code" which to me is more confusing for users than seeing "removed default export" in a major version's release notes.

I'm curious - how can ESM with a default export be added in a backwards compatible way? This has come up before (e.g. https://github.com/facebook/react/pull/18187 which also links to related issues). The problem is with webpack CJS <-> ESM interop where if you have CJS code doing require('react') what webpack will return in the presence of an ESM react with a default export, is an object with default property (meaning it now requires require('react').default) regardless of the CJS react. But maybe if you also export named then it won't be a problem? I think @TrySound has run into such issues in other packages before.

But maybe if you also export named then it won't be a problem?

Yes, this is the approach I am thinking of. See the last 40 lines of https://unpkg.com/browse/@esm-bundle/react@16.13.1/esm/react.development.js - it's an unofficial version of React that does exactly this and is used by several organizations as a drop-in replacement for the official React. No breaking changes.

But maybe if you also export named then it won't be a problem?

Yes, this is the approach I am thinking of. See the last 40 lines of https://unpkg.com/browse/@esm-bundle/react@16.13.1/esm/react.development.js - it's an unofficial version of React that does exactly this and is used by several organizations as a drop-in replacement for the official React. No breaking changes.

But are you consuming it from CJS code or ESM? Because it's the CJS <-> ESM interop issues that can be very surprising.

@gaearon to be clear; it makes sense to have no default export in the ESM wrapper i'm proposing; anyone doing native ESM would do import * as React from 'react' to work around it. However, it's fair that anyone doing that now would see it as a breaking change to suddenly not have the default export, so it'd have to wait til v17 if you didn't want to add the default now.

But are you consuming it from CJS code or ESM? Because it's the CJS <-> ESM interop issues that can be very surprising.

I've successfully imported it in webpack from both CJS and ESM files.

anyone doing native ESM would do import * as React from 'react' to work around it. However, it's fair that anyone doing that now would see it as a breaking change to suddenly not have the default export, so it'd have to wait til v17 if you didn't want to add the default now.

Agreed. If starting from scratch, there'd be no need to have the default export. However, without adding a default export, it is not possible to implement Phase 2 without it being a breaking change. I personally would be fine with doing Phase 1 in react 16 and Phases 2-4 in React 17+, although my preference is to do Phase 1, 2, and maybe even 3 (with the help of @ljharb's exports checker tool) in React 16 without breaking changes. The reason is that phase 2 is the big one where the majority of users start using the ESM bundle, whereas phase 1 is mostly for early adopters / enthusiasts.

Continuing from this proposal on. This seems to be a proposal for a switch to a dual-package with full CJS and ESM source dealing with the dual-package hazard by isolating state somewhere else (approach 2). As opposed to using an ESM wrapper like my previous RFC (approach 1).

Assuming that, I do have a few notes of things that have not been mentioned yet.

  • You cannot have a Node.js package where both ESM and CommonJS files use .js. If you set "type": "module" then all of the CommonJS files need to use the .cjs file extension instead.
  • State and equality dual-package hazard is not the only issue with having both CJS and ESM. Having potentially 2 versions of the same package loaded also increases the memory footprint of React in those cases, and React is not a small library. This is not a deal breaker, but it is worth keeping in mind.
  • I do see potential for a dual-package hazard besides React's internal state. For instance if the implementation of the Component class is not part of the CJS code where we share state and is instead part of the CJS/ESM bundles then there is risk of instanceof Component checks in various libraries breaking.

You cannot have a Node.js package where both ESM and CommonJS files use .js. If you set "type": "module" then all of the CommonJS files need to use the .cjs file extension instead.

This is true for NodeJS (but not for bundlers), so the files in the cjs directory would have to end with .cjs.

Having potentially 2 versions of the same package loaded also increases the memory footprint of React in those cases

I see how this increases the tarball size published to npm, and thus the overall size on disk. But I don't see how this impacts memory. As far as I know, bundlers and NodeJS do not bring code into memory that has not been loaded via import / require(). Could you clarify how the memory footprint would change?

For instance if the implementation of the Component class is not part of the CJS code where we share state and is instead part of the CJS/ESM bundles then there is risk of instanceof Component checks in various libraries breaking.

One proposed solution (1, 2) is to have the NodeJS esm implementation be simply an ESM interface that calls into the CJS code. That way there is only one definition of Component that is ever used in Node. Phase 1 and Phase 2 would not change what runs in NodeJS, though, so this would only apply to Phase 3.

As we (webpack) recently also added exports field support to webpack 5 I want to give my 2 cents to this topic:

  • This work-in-process document has a lot information about the exports field regarding webpack but also Node.js and in general: https://gist.github.com/sokra/e032a0f17c1721c71cfced6f14516c62
  • These are the keypoints compared to Node.js:

    • webpack 5 also adds development and production conditions, which are very useful for react. (process.env.NODE_ENV, while still supported, should be avoided for frontend code in general, it's Node.js specific)

    • webpack (and other bundlers) supports require("esm"), which allows to avoid the dual-state problem by always using ESM (even for require()). webpack has introduced a special condition for that: module. For this CommonJs and ESM version must export the same interface. Currently there is no other thing that has the dual-state-problem than Node.js. I don't expect that we will see something in future, as this is mostly a backward-compat problem.

      For maximum compatibility I would recommend the following:

package.json

{
    "type": "commonjs",
    "main": "index.js",
    "module": "esm/wrapper.js",
    "exports": {
        ".": {
            "node": {
                "development": {
                    "module": "./esm/index.development.js",
                    "import": "./esm/wrapper.development.js",
                    "require": "./cjs/index.development.js"
                },
                "production": {
                    "module": "./esm/index.production.min.js",
                    "import": "./esm/wrapper.production.min.js",
                    "require": "./cjs/index.production.min.js"
                },
                "import": "./esm/wrapper.js",
                "default": "./cjs/index.js"
            },
            "development": "./esm/index.development.js",
            "production": "./esm/index.production.min.js",
            "default": "./esm/index.production.min.js"
        },
        "./index": "./index.js",
        "./index.js": "./index.js",
        "./umd/react.development": "./umd/react.development.js",
        "./umd/react.development.js": "./umd/react.development.js",
        "./umd/react.production.min": "./umd/react.production.min.js",
        "./umd/react.production.min.js": "./umd/react.production.min.js",
        "./umd/react.profiling.min": "./umd/react.profiling.min.js",
        "./umd/react.profiling.min.js": "./umd/react.profiling.min.js",
        "./package.json": "./package.json"
    }
}

esm/package.json

Allows to use .js as extension in these directory. Alternatively .mjs could be used, but this could have potential side-effects when tools check the extension. So .js to be safe.

{
    "type": "module"
}

esm/wrapper.js

This wrapper is needed for Node.js to avoid the dual-state problem.

import React from "../cjs/index.js";
export const {
    Children,
    Component,
    ...,
    useState,
    version
} = React;
export { React as default };

cjs/index.js

This is used by Node.js when development and production conditions are not supported.

'use strict';

if (process.env.NODE_ENV === 'production') {
  module.exports = require('./cjs/react.production.min.js');
} else {
  module.exports = require('./cjs/react.development.js');
}

esm/wrapper.development.js (esm/wrapper.production.min.js similar)

These wrappers are needed for Node.js to avoid the dual-state problem.
They are only used once Node.js adds development and production conditions.

import React from "../cjs/index.development.js";
export const {
    Children,
    Component,
    ...,
    useState,
    version
} = React;
export { React as default };

index.js

For backward-compat.

module.exports = require('./cjs/index.js');

esm/index.development.js, esm/index.production.min.js

This is used by tools that does support the exports field, module condition and production/development conditions.

/* React in ESM format */

// compat for the default exports with support for tree-shaking
import * as self from "./esm/index.development.js";
export { self as default }

Results

  • webpack 5: ./esm/index.development.js or ./esm/index.production.min.js
  • browserify: ./cjs/index.js
  • webpack 4 from .mjs: ./cjs/index.js
  • other bundlers: ./esm/wrapper.js
  • Node.js (ESM): ./cjs/index.js (require) or ./esm/wrapper.js (import)
  • Node.js (old): ./cjs/index.js
  • Node.js (ESM + dev/prod): ./esm/wrapper.development.js or ./esm/wrapper.production.min.js for import, ./cjs/index.development.js or ./cjs/index.production.min.js for require

Notes

There is no esm/index.js as conditionally choosing a version is not possible in ESM without major trade-offs.
Tools can only benefit fully from react ESM when they support the exports field, the module condition (because of the dual-state problem) and production/development conditions (because of the conditional import problem).

Tools can partially benefit from react ESM when they support the module field or the exports field.

import { useState } from "react" or import * as React from "react" are technically illegal as long react is an CommonJs module.
Most tooling still support it for backward-compat, but some don't, e. g. Node.js
So currently the only way to use react which is valid everywhere is: import React from "react".
This way should stay supported, otherwise there would be cases (e. g. Node.js 14) where there is no syntax which is valid in react now and react after ESM addition.

Node.js rejected the addition of a development/production condition for exports for now.
That's sad, and I still hope the best that they will eventually add that.
That's why support for that is prepared in the exports field above.

@sokra great breakdown, very helpful, thank you!

One small question:

Node.js rejected the addition of a development/production condition for exports for now.

my understanding is that it's still being worked on? https://github.com/nodejs/node/pull/33171 but maybe I'm misunderstanding that PR

[edit] the above PR I linked to was superseded by https://github.com/nodejs/node/pull/34637

[edit2] and has now been merged into nodejs

Thanks @sokra, those are very helpful suggestions.

Here are the options I see. It seems that all of them are technically possible, and that the decision is one of strategy more than of technical implementation:

Option 1

Add export default React to a React 17 ESM build, and remove it once CJS support for import React from 'react' is dropped (perhaps in React 18?).

Option 2

Do not add export default React, and create a React 17 ESM build with named exports only.

Option 3

Do not publish a React 17 ESM build. (😢) Wait until import React from 'react'; support is dropped before creating an ESM build.

Comparison

| | Option 1 | Option 2 | Option 3 |
| - | -------- | -------- | -------- |
| Unreferenced ESM build | v17 | v17 | v18+ |
| package.json "module" (tree shaking by default) | v17 | v18+ | v18+ |
| package.json "type" / "exports" (NodeJS uses ESM) | v18+ 1 | v18+ | v18+ |

  1. It might possible to implement package.json type/exports in a fully backwards compatible way, in which case it could be part of React 17 if Option 1 is chosen.

My preference is towards Option 1, as I've explained above. However, Option 2 also is pretty exciting to me. Option 3 is of course less exciting. From what I have gathered in this github issue, we have the technical expertise to make any of these happen (and probably even the labor!).

Initial reaction to this issue was, why is this issue open even after 3 years? After reading through part of it, makes sense why it is taking so long. Maintaining a library like React is a huge task. So 🙇🏻

Given the recent news with React 17, I have updated my previous comment to reference React 17 instead of 16 for any future plans.

I'd appreciate feedback from people on which of the three above options is preferred.

I think we can add the exports field in package.json in React 17, we could probably backport it to previous versions as well:

{
  "exports": {
    ".": {
      "development": "./esm/react.development.mjs",
      "production": "./esm/react.production.mjs",
      "node": {
        "import": "./esm/react.node.mjs",
        "require": "./index.js"
      },
      "default": "./index.js"
    },
    "./jsx-dev-runtime": {
      "development": "./esm/react-jsx-dev-runtime.development.mjs",
      "production": "./esm/react-jsx-dev-runtime.production.mjs",
      "node": {
        "import": "./esm/react-jsx-dev-runtime.node.mjs",
        "require": "./jsx-dev-runtime.js"
      },
      "default": "./jsx-dev-runtime.js"
    },
    "./jsx-runtime": {
      "development": "./esm/react-jsx-runtime.development.mjs",
      "production": "./esm/react-jsx-runtime.production.mjs",
      "node": {
        "import": "./esm/react-jsx-runtime.node.mjs",
        "require": "./jsx-runtime.js"
      },
      "default": "./jsx-runtime.js"
    },
    "./": "./"
  },
}

We'd need new esm bundles, though that shouldn't be too difficult to add with rollup.

  • The ./esm/react.development.mjs and ./esm/react.production.mjs bundles should be free of process.env.NODE_ENV checks:

    • the condition is resolved at import/bundle time via the exports field.

    • process is a node API, it doesn't make sense in a browser environment, and isn't supported by _default_ by webpack 5 for example.

  • The ./esm/react.node.mjs would keep the process.env.NODE_ENV checks.
  • AFAIK only webpack 5 and node support the exports field right now.

I think this is rather safe to add, WDYT?

https://webpack.js.org/guides/package-exports/
https://nodejs.org/dist/latest-v15.x/docs/api/packages.html

The "./": "./" makes it safe, yes, but also prevents any encapsulation, so you'd want to remove that as soon as you had a semver-major.

FWIW babel outputs the new jsx runtime import as

import { jsxs, jsx, Fragment } from 'react/jsx-runtime';

but if you load a module like that in Node, it'll complain about the lack of file extension:

> node .\node.mjs
node:internal/process/esm_loader:74
    internalBinding('errors').triggerUncaughtException(
                              ^

Error [ERR_MODULE_NOT_FOUND]: Cannot find module 'test\node_modules\react\jsx-runtime' imported from test\node_modules\react-data-grid\lib\bundle.js
Did you mean to import react/jsx-runtime.js?
    at new NodeError (node:internal/errors:259:15)
    at finalizeResolution (node:internal/modules/esm/resolve:307:11)
    at moduleResolve (node:internal/modules/esm/resolve:742:10)
    at Loader.defaultResolve [as _resolve] (node:internal/modules/esm/resolve:853:11)
    at Loader.resolve (node:internal/modules/esm/loader:85:40)
    at Loader.getModuleJob (node:internal/modules/esm/loader:229:28)
    at ModuleWrap.<anonymous> (node:internal/modules/esm/module_job:51:40)
    at link (node:internal/modules/esm/module_job:50:36) {
  code: 'ERR_MODULE_NOT_FOUND'
}

Adding exports should fix it 🤔

@nstepien providing a full exports map as you have shown in your previous post is not an option from what I believe. What node implements regarding cjs interop and stuff doesn't really play well with the existing ecosystem. Dual package hazard is real - especially for packages like Reac that requires a single copy of them.

An exports map with commonjs only files could potentially be added without breaking anything but would also have to be done extra carefully and with appropriate e2e tests for this (given how complex things are to get right)

@Andarist it works fine, and is no different in react’s case, which has always had that hazard and the ecosystem solves it by making react a peer dep everywhere. An “exports” map can work just fine here, as long as the ESM files and CJS files share the same state - which can be achieved by writing simple ESM wrappers.

If all dependencies, transitive or not, of React are under the control of React (which in this case they are) and ESM entry points of all of them are just reexporting CJS content then ye - maybe that is achievable in this particular case.

There is still the whole drama of what the actual shape of ESM entry should be though (named, default, both):

  • named-only: not really backward-compatible because a lot of the code out there is using import React from 'react', which is also the only way to actually import React in node right now when using ESM
  • default-only: not really backward-compatible because a lot of the code our there is using import * as React from 'react', this has often been promoted by type-checkers and other tools
  • both: the only way to make it fully backward-compatible, so it can work with all current loading styles and when mixing ESM & CJS modules across the dependency tree

I constantly forget about the possibility of ESM wrappers as they feel like a cheat but also because this technique only works if you control all of your dependencies and can't really be used as a universal strategy that would "just work" 😢

I'm afraid the only universal strategy to provide both is, in fact, having all your actual code in CJS, and writing ESM wrappers around it to provide named exports. Code that is not stateful nor relies on identity can be natively written in both, but that's a subset, a caveat.

jsx-runtime isn't stateful right? Should be safe to ship both esm/cjs with no wrapper for it.

Should I log a separate issue for importing react/jsx-runtime in esm node?

For frontend packages an extra challenge joines the dual state hazard: let call it the double bundle hazard.

When exporting ESM and CJS versions and the package is used via require and import, both versions would be bundled and double the effective bundle size for the package.

@sokra is that possible in practice? For example with rollup I would assume that since cjs modules are transformed into esm modules, that it would then prefer importing the esm modules whenever available.

Its possible in practice for bundlers following node semantics - such as webpack 5. Its important to match semantics because otherwise u would potentially experience different results when running in node and when running a bundles code.

However, webpack 5 handles a special "module" condition that is used to deduplicate esm/cjs (the ones comming from exports map).

If we consider @ljharb’s proposal this isnt really relevant though because he proposes that esm file could be just a few lines of wrapper code that would just reexport the cjs file.

With a thin ESM wrapper, there is zero additional hazard, with or without bundlers - only the normal one that peer deps avoid about dupes in the graph.

Was this page helpful?
0 / 5 - 0 ratings