Skip to content
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

Remove useESModules in favor of conditional exports for @babel/runtime #12295

Conversation

nicolo-ribaudo
Copy link
Member

@nicolo-ribaudo nicolo-ribaudo commented Nov 2, 2020

Q                       A
Fixed Issues? Ref: #10746 (comment)
Patch: Bug Fix?
Major: Breaking Change? Yes
Minor: New Feature?
Tests Added + Pass? Yes
Documentation PR Link
Any Dependency Changes?
License MIT

Using Node.js' exports mappings, we can get rid of the "useESModules": true option: it will automatically pick up the preferred version based on how the helper is requried/imported.

exports are also supported by webpack and rollup, but I have structured the files in a way that, even if exports isn't supported by some tool, it will load the CJS version because the file is named index.js.

In order to avoid loading both the CommonJS and the ESM version, which can lead to duplicated state, I structured exports so that:

  • Node.js always loads the CommonJS version. When using require it "just works", when using import it will get the value of module.exports. (The node export condition)
  • When using Webpack/Rollup, it will always load the .mjs version. Webpack requires that the CJS and ESM files have the same interface, so the CJS helpers also export their function as module.exports.default. (The module condition)
  • For other targets that don't support the module/node conditions, it will always load the .mjs version. (The default condition)

@nicolo-ribaudo nicolo-ribaudo added the PR: Breaking Change 💥 A type of pull request used for our changelog categories for next major release label Nov 2, 2020
@babel-bot
Copy link
Collaborator

babel-bot commented Nov 2, 2020

Build successful! You can test your changes in the REPL here: https://babeljs.io/repl/build/31682/

@codesandbox-ci
Copy link

codesandbox-ci bot commented Nov 2, 2020

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 948c463:

Sandbox Source
babel-repl-custom-plugin Configuration
babel-plugin-multi-config Configuration

Copy link
Member

@Andarist Andarist left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a single comment so far but I will also have to check out this branch to see what exactly is being built, the dir structure etc - will be easier to check some things that way. Gonna do that later, hopefully today.

for (const helperName of helpers.list) {
const helperPath = path.join("helpers", helperName);
helperSubExports[`./${helperPath}`] = {
import: writeHelperFile(runtimeName, pkgDirname, helperPath, helperName, {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for the ideal experience, the very same value should be repeated for a priority "module" condition that is supported by webpack (and will most likely be supported by Rollup as well), in node require cant load ESM so it can lead to the same module being loaded twice (once for ESM loader and once for the CJS loader) which ain't ideal for the web consumers so this condition allows the same file (authored in ESM) to be loaded for both loaders, so this behaves pretty much as the package.json#module

@Andarist
Copy link
Member

Andarist commented Nov 2, 2020

Ok, I've managed to check this quickly while hanging over the phone - one additional thing. I believe that it would generally be best (and required for "module" condition) to keep the same interface of CJS and ESM versions of a single package. That would mean dropping the special-casing of CJS helpers (and other Babel modules) when inserting imports through a helper + changing the content of those modules so they use .default like everything else rather than a direct assignment to module.exports.

As those modules are mostly consumed automatically through plugins this shouldn't be much of a problem even for CJS-only consumers.

return arrayWithHoles(arr) || iterableToArray(arr) || unsupportedIterableToArray(arr) || nonIterableRest();
}

module.exports = _toArray;
Copy link
Member

@Andarist Andarist Nov 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as mentioned on Slack - this won't work because one can write:

// node_modules/cast-array/index.js
const toArray = require("@babel/runtime/helpers/toArray")
module.exports = maybeArr => toArray(maybeArr) // it doesn't quite matter what's here, the prior line is important

and then one can consume such a package using webpack (4 and 5 included, maybe up to 2) with:

import castArray from 'cast-array'

castArray(document.querySelectorAll('.foo'))

Here webpack would consume the first module (cast-array) and assume that what it has imported has the shape of module.exports = { default: toArray } which would fail loudly at runtime (or at build time? not sure actually).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one can write

This package is only meant to be consumed by generated code, not manually. It webpack throws in that case, or generates invalid code, that's ok. Babel will always inject imports like require("@babel/runtime/helpers/toArray").default.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whats the reason for the direct assignment then? Why it exists? When it will be used?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So that we can emit this code in Node.js:

import toArray from "@babel/runtime/helpers/toArray"

instead of

import toArray_moduleExports from "@babel/runtime/helpers/toArray"
const toArray = toArray_moduleExports.default;

because Node.js's default import is the value of module.exports.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could u describe the scenario end to end?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that a problem is that the output consumer might not be a final consumer. The transform-runtime plugin is often used when building packages and im mostly worried about scenarios related to that while i think u are describing the former scenario - correct me if i have misunderstood your intention

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are different usages I'm thinking of, and I don't think that whether the file is a pre-compiled package or a compiled source file of the final app matters.

  1. .mjs output, used in Node.js (:warning: This is where we need module.exports to be the helper itself)
    import toArray from "@babel/runtime/helpers/toArray";
    This will load @babel/runtime/helpers/toArray/index.cjs, and it's module.exports will be used by Node.js as the value of toArray. Internally, Node.js does something like const toArray = require("@babel/runtime/helpers/toArray/index.cjs");
  2. .cjs output, used in Node.js
    const toArray = require("@babel/runtime/helpers/toArray").default;
    This will load @babel/runtime/helpers/toArray/index.cjs, and it's module.exports.default will be used as the value of toArray.
  3. .mjs output, used in [bundler]
    import toArray from "@babel/runtime/helpers/toArray";
    This will load @babel/runtime/helpers/toArray/index.mjs, and Webpack will internally use the default property of that file's namespace object (import("@babel/runtime/helpers/toArray/index.mjs").default).
  4. .cjs output, used in [bundler]
    const toArray = require("@babel/runtime/helpers/toArray").default;
    This will load @babel/runtime/helpers/toArray/index.mjs, and it's default will be used as the value of toArray (import("@babel/runtime/helpers/toArray/index.mjs").default).
  5. .mjs output, directly used in browsers (for example, with Snowpack)
    import toArray from "@babel/runtime/helpers/toArray";
    This will load @babel/runtime/helpers/toArray/index.mjs, and use it's default export as the value of toArray (similarly to bundlers)
  6. .cjs output, directly used in browsers (for example, with Snowpack)
    It doesn't work (as expected).

By doing so, Node.js will only load the .cjs file and [bundler] will only load the .mjs file, and they will never be loaded together.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this indeed might work 👍 Would not bet money on it though 😉 but that's mainly because how the overall story is complicated, not because of the chosen approach

import _inherits from "@babel/runtime-corejs3/helpers/esm/inherits";
import _possibleConstructorReturn from "@babel/runtime-corejs3/helpers/esm/possibleConstructorReturn";
import _getPrototypeOf from "@babel/runtime-corejs3/helpers/esm/getPrototypeOf";
import _classCallCheck from "@babel/runtime-corejs3/helpers/classCallCheck";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is useES6Modules a thing (it's in the fixture path)? Seems like maybe this test is not relevant any longer?

@@ -1,10 +1,10 @@
var _classCallCheck = require("@babel/runtime/helpers/classCallCheck");
var _classCallCheck = require("@babel/runtime/helpers/classCallCheck").default;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similarly here but for other reasons - this PR removes useESModules so this fixture can be removed

@@ -1,10 +1,10 @@
var _classCallCheck = require("@babel/runtime/helpers/classCallCheck");
var _classCallCheck = require("@babel/runtime/helpers/classCallCheck").default;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import _inherits from "@babel/runtime/helpers/esm/inherits";
import _possibleConstructorReturn from "@babel/runtime/helpers/esm/possibleConstructorReturn";
import _getPrototypeOf from "@babel/runtime/helpers/esm/getPrototypeOf";
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import _inherits from "@babel/runtime/helpers/esm/inherits";
import _possibleConstructorReturn from "@babel/runtime/helpers/esm/possibleConstructorReturn";
import _getPrototypeOf from "@babel/runtime/helpers/esm/getPrototypeOf";
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

module.exports = _toArray;
module.exports["default"] = module.exports;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure that the sole existence of .default is enough here? It seems that it might be because of the same thing being assigned both to module.exports and module.exports.default.

When consuming ESM file webpack 4 would load this file directly (as it would just fallback to /index.js because it's not aware of pkg.json#exports) and if it's using the exact same logic as Babel:

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

this would work even though __esModule is not present here 🤔 I'm wondering though if it still wouldn't be worth adding __esModule as maybe some other tools treat this different somehow? Although not sure how that would look like (such a tool's interop logic). Just thinking out loud.

@nicolo-ribaudo
Copy link
Member Author

Moved to #12632

@nicolo-ribaudo nicolo-ribaudo deleted the babel-runtime-dual-modules branch February 19, 2021 15:01
@github-actions github-actions bot added the outdated A closed issue/PR that is archived due to age. Recommended to make a new issue label May 22, 2021
@github-actions github-actions bot locked as resolved and limited conversation to collaborators May 22, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area: helpers outdated A closed issue/PR that is archived due to age. Recommended to make a new issue PR: Breaking Change 💥 A type of pull request used for our changelog categories for next major release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants