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

Inline regeneratorRuntime as a normal helper #14538

Merged
merged 14 commits into from May 19, 2022

Conversation

nicolo-ribaudo
Copy link
Member

@nicolo-ribaudo nicolo-ribaudo commented May 8, 2022

Q                       A
Fixed Issues? Fixes #12106, fixes #8724, fixes #11663, fixes #8258
Patch: Bug Fix?
Major: Breaking Change?
Minor: New Feature? Yes? Or just better ergonomics
Tests Added + Pass? Yes
Documentation PR Link babel/website#2645
Any Dependency Changes?
License MIT

I wanted to test this approach before opening an issue to discuss about it, but I ended up having a complete implementation 😅 I'm marking it as i: discussion.

Also, it can be reviewed commit-by-commit.


One of the major pain points when using regenerator is that by default it relies on a globally available regeneratorRuntime helper. For example, if you compile function* f() {} you get:

var _marked = /*#__PURE__*/regeneratorRuntime.mark(f);

function f() {
  return regeneratorRuntime.wrap(function f$(_context) {
    while (1) {
      switch (_context.prev = _context.next) {
        case 0:
        case "end":
          return _context.stop();
      }
    }
  }, _marked);
}

This means that compiled generators don't work "by default", but you have to load the runtime helper:

  • you can manually do it, either with import "regenerator-runtime" or with a script tag
  • you can use @babel/preset-env or @babel/transform-runtime to automatically inject an import/require

Additionally, regeneratorRuntime is always a global: even if you use @babel/runtime (where all the other helpers are "pure"), it will self-install itself as a global variable.

Why is it a problem if the helper is not be automatically available?
If you look at our closed issues, it's common for people to be surprised that they need to do "something else" other than just enabling the regenerator plugin: https://github.com/babel/babel/issues?q=is%3Aissue+sort%3Aupdated-desc+is%3Aclosed+regeneratorRuntime+is+not+defined. Also, all the other helpers are automatically available and they show that a nicer DX is possible.

Why is it a problem if the regenerator-runtime always self-installs itself as a global?
It's common agreement that libraries should not modify the global scope, now that JavaScript has a proper module system. Additionally, detecting the global object is hard: regenerator-runtime first tries with a global regeneratorRuntime = ... assignment that only works in non-strict mode, then fallbacks to globalThis (which only works in modern browsers), and then to eval (which may be blocked by many common CSPs).


The approach I'm proposing with this PR

regeneratorRuntime can just be a normal helper, similarly to how classCallCheck or asyncToGenerator work:

  • by default it can be injected inline in the code, so that function* f() {} will be transpiled to something like
    function _regeneratorRuntime() { /* ... helper contents ... */ }
    
    var _marked = /*#__PURE__*/_regeneratorRuntime().mark(f);
    
    function f() {
      return _regeneratorRuntime().wrap(function f$(_context) {
        while (1) {
          switch (_context.prev = _context.next) {
            case 0:
            case "end":
              return _context.stop();
          }
        }
      }, _marked);
    }
    this code is 100% self-contained: you can paste it in your browser's console and it will work.
  • when using @babel/runtime, it can inject an import like
    import _regeneratorRuntime from "@babel/runtime/helpers/regeneratorRuntime";
    
    var _marked = /*#__PURE__*/_regeneratorRuntime().mark(f);
    
    function f() {
      return _regeneratorRuntime().wrap(function f$(_context) {
        while (1) {
          switch (_context.prev = _context.next) {
            case 0:
            case "end":
              return _context.stop();
          }
        }
      }, _marked);
    }
    @babel/runtime/helpers/regeneratorRuntime just exports the helper, without trying to attach it to a global variable.

For compatibility with older @babel/core versions that don't know about the built-in regeneratorRuntime helper, we will fallback to the old behavior relying on the regeneratorRuntime global. @babel/runtime or @babel/preset-env (via babel-plugin-polyfill-regenerator) can than take care of injecting the helper import, if they are being used.

Drawbacks

The regeneratorRuntime helper is huge: it's about 10k, which is ~10% more than our current other biggest helper (applyDecs). It's code that users already had to load, but now you could have multiple copies of it in different files (while the old behavior was to just throw at runtime, if you don't use @babel/runtime). However, people who care about their bundle size are already highly encouraged to use @babel/runtime to deduplicate helpers.

License and code sharing

I decided to have a build-step to copy regenerator-runtime's contents to https://github.com/nicolo-ribaudo/babel/blob/vendor-regenerator-runtime/packages/babel-helpers/src/helpers/regeneratorRuntime.js, and then to process it with the existing scripts that we have to bundle our helpers from the babel-helpers/src/helpers folder.

  • I didn't just manually copy regenerator-runtime's contents, because we need to update our copy of the code whenever regenerator-runtime has a new release. Note that regenerator-runtime is "finished": it does not have known bugs, there are no new planned features, and so it new releases are rare.
  • The build script slightly transforms the code, to make it an ESM that exports a function rather than defining a global variable.

regenerator-runtime is MIT licensed, and its copyright is owned by Facebook. The helper contains a short comment containing the copyright notice and a link to regenerator's original license. That comment is kept in every copy of the helper: both when it's inlined in user code, and in @babel/runtime.

Follow ups

  • There is a @babel/generator bug and sometimes the comment is duplicated, but that's a low-priority bug.
  • In Babel 8 we won't need to have the fallback for the old behavior, I'll open a PR to clean it up.

cc @benjamn What do you think about this approach? My main goal is to simplify Babel's user experience, especially for beginners and for quick projects. Since you are the author/maintainer of regenerator, I'd love to have your 👍!

@nicolo-ribaudo nicolo-ribaudo added PR: Polish 💅 A type of pull request used for our changelog categories i: discussion area: helpers labels May 8, 2022
@babel-bot
Copy link
Collaborator

babel-bot commented May 8, 2022

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

@matthieusieben
Copy link

Note that the first time var _marked = /*#__PURE__*/_regeneratorRuntime().mark(f); is called, all Symbol polyfils must have been already imported.

This is due to the fact that regenerator-runtime/runtime.js caches the value of Symbol.iterable (and others) when it is first loaded.

Failing to load the polyfills before the execution of the runtime will cause iteratorSymbol inside the regeneratorRuntime's code to use @@iterator as iterator Symbol.

@nicolo-ribaudo
Copy link
Member Author

Ok so, another benefit that I didn't list in the original description:

@liuxingbaoyu
Copy link
Member

Hint: there is also a ci exception about windows.
https://github.com/babel/babel/runs/6390876850?check_suite_focus=true
We may need to rerun the Update fixtures (Windows) action.

@liuxingbaoyu
Copy link
Member

It occurred to me that we can compress the code. Size can be reduced by 30%+.

@nicolo-ribaudo
Copy link
Member Author

@liuxingbaoyu We currently minify helpers without renaming the variables to make stack traces easier to debug. Most users run a minifier on Babel's output anyway.

@nicolo-ribaudo nicolo-ribaudo added this to the v7.18.0 milestone May 17, 2022
Copy link
Member Author

@nicolo-ribaudo nicolo-ribaudo left a comment

Choose a reason for hiding this comment

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

I have re-reviewed this. I'll merge for 7.18.0 when CI passes 🙂

@nicolo-ribaudo nicolo-ribaudo merged commit e0a2a71 into babel:main May 19, 2022
@nicolo-ribaudo nicolo-ribaudo deleted the vendor-regenerator-runtime branch May 19, 2022 17:39
@francois-codes
Copy link

@nicolo-ribaudo I'm seeing babel errors for about an hour, which coincides with the publication of 7.18.0
image

I checked in my lock file which packages have been updated and locking the versions before 7.18.0 for these
image

fixes these issues, and the app now runs properly. It looks like there is something broken on this version :/

@nicolo-ribaudo
Copy link
Member Author

Not related to this PR, but thanks for the report! I'm working on a fix.

@francois-codes
Copy link

oh, my bad, I tried to trace the diffs from 7.18.0

@nicolo-ribaudo
Copy link
Member Author

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: Polish 💅 A type of pull request used for our changelog categories
Projects
None yet
7 participants