Skip to content

Pass Metro's own @babel/runtime through @react-native/metro-babel-transformer - #59205

Open
robhogan wants to merge 2 commits into
mainfrom
robhogan/metro-babel-runtime-transformer
Open

robhogan wants to merge 2 commits into
mainfrom
robhogan/metro-babel-runtime-transformer

Conversation

@robhogan

@robhogan robhogan commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

This is the RN side of react/metro#1957

Summary:

Helpers that @babel/plugin-transform-runtime imports from @babel/runtime can fail to resolve, depending on where, or whether, a project installs it. Worse, @react-native/babel-preset doesn't know which version it'll get, so the plugin assumes the oldest and inlines newer helpers instead, eg _interopRequireWildcard into every module that uses import * as (#57123).

Metro now offers its own @babel/runtime, which always exists, at a version it knows. This forwards it from @react-native/metro-babel-transformer, which React Native projects use and which currently drops it. With transformer.enableBabelRuntime: true (the default), the wiring is:

  1. Metro's transform worker passes the Babel transformer babelRuntimeModuleName: 'metro:babel-runtime' and the installed version of Metro's @babel/runtime. (Landed in Pass Metro's own @babel/runtime to Babel presets metro#1957, unreleased)
  2. @react-native/metro-babel-transformer forwards both to the preset. (This PR)
  3. @react-native/babel-preset configures @babel/plugin-transform-runtime with that moduleName and version. (Landed in Allow configuring Babel runtime via caller in @react-native/babel-preset (#57974) #57974)
  4. Helpers are emitted as metro:babel-runtime/helpers/... imports, which Metro's metro: resolver maps to its own @babel/runtime. (Landed in Metro 0.87.1)
  5. Metro adds that @babel/runtime to the file map, wherever it's installed. (Landed in Add Metro's own @babel/runtime to the file map as a dynamic root metro#2065, unreleased)

How this PR forwards them depends on the project's Babel config:

  • With a project Babel config naming the preset, as in the template, they reach the preset as Babel caller data.
  • Without one, the version replaces enableBabelRuntime: true in the preset's options, because preset options take precedence over caller data. babelRuntimeModuleName already reaches the preset there.

Until the next Metro release, Metro doesn't pass either value, so this is a no-op, but harmless. The Flow suppression for the new fields can go once React Native depends on that release.

Changelog:

[GENERAL] [ADDED] - @react-native/metro-babel-transformer passes Metro's own @babel/runtime and its version to @react-native/babel-preset, when Metro provides them

Test Plan:

New tests transform import * as foo from './foo' with and without a project babel.config.js naming the preset. When Metro passes metro:babel-runtime and a version, the helper is imported from metro:babel-runtime/helpers/interopRequireWildcard, and otherwise it's inlined as before. The import cases fail without this change.

End to end, with Metro from react/metro#1957, the iOS release bundle of private/helloworld no longer inlines helpers: 199 copies of _interopRequireWildcard and 63 of _callSuper become three shared modules. Against current defaults:

Current defaults With this PR Change
Minified JS 906,504 B 807,389 B -10.9%
Minified JS, gzip -9 217,794 B 204,595 B -6.1%
Hermes bytecode 1,261,863 B 1,173,832 B -7.0%

Sample output is in react/metro#1957. Not run on a device.

…nsformer

## Summary:

Helpers that `@babel/plugin-transform-runtime` imports from `@babel/runtime` can fail to resolve, depending on where, or whether, a project installs it. Worse, `@react-native/babel-preset` doesn't know which version it'll get, so the plugin assumes the oldest and inlines newer helpers instead, eg `_interopRequireWildcard` into every module that uses `import * as` (#57123).

Metro now offers its own `@babel/runtime`, which always exists, at a version it knows. This forwards it from `@react-native/metro-babel-transformer`, which React Native projects use and which currently drops it. With `transformer.enableBabelRuntime: true` (the default), the wiring is:

1. Metro's transform worker passes the Babel transformer `babelRuntimeModuleName: 'metro:babel-runtime'` and the installed version of Metro's `@babel/runtime`. (Landed in react/metro#1957, unreleased)
2. `@react-native/metro-babel-transformer` forwards both to the preset. (This PR)
3. `@react-native/babel-preset` configures `@babel/plugin-transform-runtime` with that `moduleName` and `version`. (Landed in #57974)
4. Helpers are emitted as `metro:babel-runtime/helpers/...` imports, which Metro's `metro:` resolver maps to its own `@babel/runtime`. (Landed in Metro 0.87.1)
5. Metro adds that `@babel/runtime` to the file map, wherever it's installed. (Landed in react/metro#2065, unreleased)

How this PR forwards them depends on the project's Babel config:

- With a project Babel config naming the preset, as in the template, they reach the preset as Babel caller data.
- Without one, the version replaces `enableBabelRuntime: true` in the preset's options, because preset options take precedence over caller data. `babelRuntimeModuleName` already reaches the preset there.

Until the next Metro release, Metro doesn't pass either value, so this is a no-op, but harmless. The Flow suppression for the new fields can go once React Native depends on that release.

## Changelog:

[GENERAL] [ADDED] - `@react-native/metro-babel-transformer` passes Metro's own `@babel/runtime` and its version to `@react-native/babel-preset`, when Metro provides them

## Test Plan:

New tests transform `import * as foo from './foo'` with and without a project `babel.config.js` naming the preset. When Metro passes `metro:babel-runtime` and a version, the helper is imported from `metro:babel-runtime/helpers/interopRequireWildcard`, and otherwise it's inlined as before. The import cases fail without this change.

End to end, with Metro from react/metro#1957, the iOS release bundle of `private/helloworld` no longer inlines helpers: 199 copies of `_interopRequireWildcard` and 63 of `_callSuper` become three shared modules. Against current defaults:

| | Current defaults | With this PR | Change |
|---|---|---|---|
| Minified JS | 906,504 B | 807,389 B | -10.9% |
| Minified JS, gzip -9 | 217,794 B | 204,595 B | -6.1% |
| Hermes bytecode | 1,261,863 B | 1,173,832 B | -7.0% |

Sample output is in react/metro#1957. Not run on a device.
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 19, 2026
@robhogan
robhogan marked this pull request as ready for review October 19, 2026 18:52
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Oct 19, 2026
@robhogan
robhogan requested a balanced review from Copilot October 20, 2026 08:48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@robhogan
robhogan requested review from huntie and a balanced review from Copilot October 20, 2026 08:48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@vzaidman

Copy link
Copy Markdown
Contributor

Is there ever a case where we want to make sure the same @babel/runtime resolves in Metro and in some other tools? I can't think of any. Can you think of cases where "enableBabelRuntime" should be false (if it's not bugged)?

@vzaidman
vzaidman self-requested a review October 20, 2026 12:54
@robhogan

Copy link
Copy Markdown
Collaborator Author

Is there ever a case where we want to make sure the same @babel/runtime resolves in Metro and in some other tools? I can't think of any.

The runtime transform is just space-saving so it doesn't functionally matter if other tools use a different version (or disable the plugin entirely) - as long as they're not telling Babel that a newer version is installed.

Here, because we only pass the runtime version from Metro, other tools will fall back to the default ^7.0.0 (which is the very first release of @babel/runtime), so there's no risk they'll resolve to a too-old version.

It's still the case though that Jest etc need @babel/runtime installed near the source under transform. This new work only helps Metro, but doesn't hurt anyone else.

Tbh, it should probably be disabled by default for Jest just for simplicity. More verbose generated code doesn't really matter in that context.

Can you think of cases where "enableBabelRuntime" should be false (if it's not bugged)?

Not much, apart from maybe the above for Jest. The main reason before this stack was that users needed to make sure it was installed in all of their source roots, and that could go wrong.

Meta is one consumer I know of that intentionally disables it though - iirc using a global with all of the helpers on it, instead of @babel/runtime/helpers imports. Meta's approach has the upside that a global is faster to access than a require call, but has a couple of tradeoffs - the helper global is maintained semi-manually, and I don't think it's using the newer helpers, so probably producing a bigger bundle.

This stack is a possible step to making that whole thing go away. metro:babel-runtime dependencies are easily statically analysed and even transformed to prop accesses if you wanted. @motiz88 had some thoughts and a WP post on this years ago.

@javache javache closed this Oct 20, 2026
@javache javache reopened this Oct 20, 2026
@meta-codesync

meta-codesync Bot commented Oct 20, 2026

Copy link
Copy Markdown

@javache has imported this pull request. If you are a Meta employee, you can view this in D126404231.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants