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

fix(metro-config): Remove extraneous Babel pass #25930

Merged
merged 9 commits into from
Feb 12, 2024

Conversation

kitten
Copy link
Member

@kitten kitten commented Dec 13, 2023

Closes ENG-10870

Why

We want to remove an extraneous Babel pass in the metro-transform-worker, as a follow-up task to #25833. The relevant todo is in the worker itself:

// TODO: This MUST be run even though no plugins are added, otherwise the babel runtime generators are broken.
// if (plugins.length) {

Previously, we couldn't remove this pass as without it dev: false builds would be missing _interopRequireWildcard and _interopRequireDefault Babel helper inline functions.

However, removing it gets rid of an unnecessary transformFromAstSync call which may run without plugins and clones the entire Babel Input AST.

How

Investigating the issue, it turns out that Babel has a cache for AST nodes that keeps track of previously created path and scope objects. This is kept on @babel/traverse's traverse.cache.

The cloneInputAst: true option was needed on at least one transformFromAstSync because it busts this path/scope cache. It internally performs a simple deep-clone of the AST and means that the next traverse call doesn't have a path/scope cache to fall back on. This will cause Babel to recrawl the scope of an AST.

This basically means that this problem has two sides to it, which work together to cause the bug that removing cloneInputAst: true triggers:

  1. A Babel plugin in our babel-transformer is corrupting the scope early on (as part of @react-native/babel-preset)
  2. A Babel plugin in one of our transformFromAstSync passes is corrupting the AST due to the corrupted scope.

In our case, the Babel plugin corrupting the scope is @babel/plugin-transform-modules-commonjs. This plugin uses hub.addHelper("interopRequireWildcard") and similar calls that inject Babel helpers. In most cases, these calls to hub.addHelper don't update any referencePaths and thus the function declarations it injects are un-referenced from the perspective of Babel’s scope.

The Babel plugin corrupting the AST and removing the above function declarations is Metro’s constantFoldingPlugin, which is only used when dev: true is set. This plugin, among other things, removes unused declarations. When it sees the inserted function declarations with the corrupted scope, it removes them.

There are several ways to ensure that the scope is correct after the initial Babel plugins are run, either:

  • each Babel plugin that uses hub.addHelper (or inserts declaration without letting traversal continue) must update the scope manually.
    • This bug may go away in Babel 8 and we don't control all plugins that a user may use, so this isn't a suitable fix
  • the AST may be cloned using cloneInputAst: true on the constantFoldingPlugin instead, or the via a deepClone utility.
    • Basically, we could also move cloneInputAst: true to the dev: true pass.
  • traverse.cache.clear() may be called to clear the entire traversal cache
    • This could have performance implications that are hard to analyse.
  • programPath.scope.crawl() can be called in or before the constantFoldingPlugin to make sure it always has an up-to-date scope.

This PR, when applied, uses the last solution in the above list.
It also disables cloneInputAst: true on all transform passes, since only the constantFoldingPlugin uses the scope to alter the AST in a way that could break the output code.

Test Plan

  • A new unit test has been added for metro-transform-worker that uses the constantFoldingPlugin and checks the output for code that will generate a _interopRequireWildcard declaration
    • This ensures we don't break the constantFoldingPlugin again without having to run E2E tests
    • I also used this unit test to test that reverting this fix breaks, as expected
  • I manually tracked down the bug by disabling cloneInputAst everywhere and then selectively stepping through code. Removing constantFoldingPlugin also fixes the output.
  • By tracking down some related Metro issues, I tested traverse.cache.clear() in conjunction with other changes

Checklist

Copy link

linear bot commented Dec 13, 2023

@expo-bot expo-bot added the bot: suggestions ExpoBot has some suggestions label Dec 13, 2023
@kitten kitten force-pushed the @kitten/metro/remove-extraneous-babel-pass branch from 68856a1 to f9cdacd Compare December 13, 2023 23:34
@expo-bot expo-bot added bot: passed checks ExpoBot has nothing to complain about and removed bot: suggestions ExpoBot has some suggestions labels Dec 13, 2023
@kitten kitten force-pushed the @kitten/metro/remove-extraneous-babel-pass branch from f9cdacd to 84cbeb9 Compare December 13, 2023 23:41
As per the removed code comment, we couldn't remove even an empty
re-traversal of the AST because `cloneInputAst: true` on it was needed.
This is essentially just a deep structural clone of the AST and
does nothing special.

This was basically causing the traverse cache to be ignored, since
all references of the AST would be swapped out.

The Babel traverse cache keeps track of paths and scopes, and re-uses
them to avoid having to re-crawl the AST.
Hence, the `cloneInputAst: true` was needed to clear this cache.

An alternative to this is:

    require('@babel/core').traverse.cache.clear();

However, this was only needed because the `constantFoldingPlugin`
removes unused declarations. This was breaking our output since
the `@babel/plugin-transform-modules-commonjs` wasn't creating
reference paths to its CommonJS helpers.

This in turn is a bug in Babel. All calls to `hub.addHelper` add
declarations, but don't actually add references to the identifiers it
generates.

This means that `constantFoldingPlugin` only works properly if we update
the scope of the AST by re-crawling it, i.e.:

    programPath.scope.crawl();

Which is likely cheaper than either cloning the AST or wiping out the
entire traversal cache.
@kitten kitten force-pushed the @kitten/metro/remove-extraneous-babel-pass branch from 144cd00 to f11ae49 Compare December 14, 2023 09:58
Copy link
Contributor

@EvanBacon EvanBacon left a comment

Choose a reason for hiding this comment

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

Finally had time to dig through this extensively today, genius work here.

@expo-bot
Copy link
Collaborator

Hi there! 👋 I'm a bot whose goal is to ensure your contributions meet our guidelines.

I've found some issues in your pull request that should be addressed (click on them for more details) 👇

⚠️ Suggestion: Missing changelog entries


Your changes should be noted in the changelog. Read Updating Changelogs guide and consider adding an appropriate entry to the following changelogs:


Generated by ExpoBot 🤖 against c7e3e4f

@expo-bot expo-bot added bot: suggestions ExpoBot has some suggestions and removed bot: passed checks ExpoBot has nothing to complain about labels Feb 11, 2024
@EvanBacon EvanBacon merged commit 2f69046 into main Feb 12, 2024
11 of 13 checks passed
@EvanBacon EvanBacon deleted the @kitten/metro/remove-extraneous-babel-pass branch February 12, 2024 18:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants