Skip to content

Allow configuring Babel runtime via caller in @react-native/babel-preset (#57974) - #57974

Closed
robhogan wants to merge 1 commit into
mainfrom
export-D114070686
Closed

robhogan wants to merge 1 commit into
mainfrom
export-D114070686

Conversation

@robhogan

@robhogan robhogan commented Aug 17, 2026 •

Copy link
Copy Markdown
Collaborator

Summary:

Adds a mechanism for callers to configure babel/plugin-transform-runtime
programmatically via Babel caller data, in addition to preset options.

  • enableBabelRuntime (boolean toggle, or a string to pin a specific
    babel/runtime version) can now be read from the Babel caller.
  • babelRuntimeModuleName (the module helpers are imported from) can be
    provided via preset options or the Babel caller.

Both values are passed as separate primitives (Babel only permits primitive
caller values). Preset options take precedence over caller data, consistent
with how unstable_transformProfile is resolved.

Changelog:
[General][Added] - Allow react-native/babel-preset to read enableBabelRuntime and babelRuntimeModuleName from Babel caller data

Reviewed By: vzaidman

Differential Revision: D114070686

@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 Aug 17, 2026
@meta-codesync

meta-codesync Bot commented Aug 17, 2026

Copy link
Copy Markdown

@robhogan has exported this pull request. If you are a Meta employee, you can view the originating Diff in D114070686.

@meta-codesync meta-codesync Bot changed the title Allow configuring Babel runtime via caller in @react-native/babel-preset Allow configuring Babel runtime via caller in @react-native/babel-preset (#57974) Aug 17, 2026
meta-codesync Bot pushed a commit that referenced this pull request Aug 17, 2026
…set (#57974)

Summary:

Adds a mechanism for callers to configure `babel/plugin-transform-runtime`
programmatically via Babel caller data, in addition to preset options.

- `enableBabelRuntime` (boolean toggle, or a string to pin a specific
  `babel/runtime` version) can now be read from the Babel caller.
- `babelRuntimeModuleName` (the module helpers are imported from) can be
  provided via preset options or the Babel caller.

Both values are passed as separate primitives (Babel only permits primitive
caller values). Preset options take precedence over caller data, consistent
with how `unstable_transformProfile` is resolved.

Changelog:
[General][Added] - Allow `react-native/babel-preset` to read `enableBabelRuntime` and `babelRuntimeModuleName` from Babel caller data

Reviewed By: vzaidman

Differential Revision: D114070686
@meta-codesync
meta-codesync Bot force-pushed the export-D114070686 branch from 7c3dca6 to 683dc7c Compare August 17, 2026 14:32
@retyui

retyui commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

will this help to fix this issue?

#57123

@robhogan

Copy link
Copy Markdown
Collaborator Author

@cortinico / @vzaidman / @cipolleschi - IIRC this was mid-land when I left, I think it's already approved. Could someone help land it?

meta-codesync Bot pushed a commit that referenced this pull request Sep 21, 2026
…set (#57974)

Summary:

Adds a mechanism for callers to configure `babel/plugin-transform-runtime`
programmatically via Babel caller data, in addition to preset options.

- `enableBabelRuntime` (boolean toggle, or a string to pin a specific
  `babel/runtime` version) can now be read from the Babel caller.
- `babelRuntimeModuleName` (the module helpers are imported from) can be
  provided via preset options or the Babel caller.

Both values are passed as separate primitives (Babel only permits primitive
caller values). Preset options take precedence over caller data, consistent
with how `unstable_transformProfile` is resolved.

Changelog:
[General][Added] - Allow `react-native/babel-preset` to read `enableBabelRuntime` and `babelRuntimeModuleName` from Babel caller data

Reviewed By: vzaidman

Differential Revision: D114070686
robhogan added a commit to react/metro that referenced this pull request Sep 21, 2026
…via caller

Summary:
Follow-up to 8388f71 (the default `metro:` scheme resolver), which maps `metro:babel-runtime/<path>` to metro-runtime's own `@babel/runtime` dependency. This diff is the Metro half of pointing `@babel/plugin-transform-runtime` at it.

Whenever `transformer.enableBabelRuntime` is truthy, the transform worker now passes `babelRuntimeModuleName: 'metro:babel-runtime'` to the Babel transformer, and `metro-babel-transformer` forwards it to presets as Babel caller data. Caller data reaches a preset however it's configured - including via a project `babel.config.js`, where the preset's options come from the user.

This is a no-op until a preset reads it. `@react-native/babel-preset` does from react/react-native#57974, which maps it to `@babel/plugin-transform-runtime`'s `moduleName`, so emitted helpers become `metro:babel-runtime/helpers/*` and resolve deterministically regardless of where (or whether) the project hoists `@babel/runtime`.

Changelog:
```
 - **[Feature]**: Pass `babelRuntimeModuleName: 'metro:babel-runtime'` to Babel presets via caller data when `transformer.enableBabelRuntime` is enabled
```

Test plan:
```
yarn jest packages/metro-transform-worker packages/metro-babel-transformer
yarn flow check
yarn build-ts-defs && yarn verify-api-snapshots
```
robhogan added a commit to react/metro that referenced this pull request Sep 21, 2026
…via caller

Summary:
Follow-up to 8388f71 (the default `metro:` scheme resolver), which maps `metro:babel-runtime/<path>` to metro-runtime's own `@babel/runtime` dependency. This diff is the Metro half of pointing `@babel/plugin-transform-runtime` at it.

Whenever `transformer.enableBabelRuntime` is truthy, the transform worker now passes `babelRuntimeModuleName: 'metro:babel-runtime'` to the Babel transformer, and `metro-babel-transformer` forwards it to presets as Babel caller data. Caller data reaches a preset however it's configured - including via a project `babel.config.js`, where the preset's options come from the user.

This is a no-op until a preset reads it. `@react-native/babel-preset` does from react/react-native#57974, which maps it to `@babel/plugin-transform-runtime`'s `moduleName`, so emitted helpers become `metro:babel-runtime/helpers/*` and resolve deterministically regardless of where (or whether) the project hoists `@babel/runtime`.

Changelog:
```
 - **[Feature]**: Pass `babelRuntimeModuleName: 'metro:babel-runtime'` to Babel presets via caller data when `transformer.enableBabelRuntime` is enabled
```

Test plan:
```
yarn jest packages/metro-transform-worker packages/metro-babel-transformer
yarn flow check
yarn build-ts-defs && yarn verify-api-snapshots
```
robhogan added a commit to react/metro that referenced this pull request Sep 21, 2026
…caller

Summary:
Follow-up to 8388f71 (the default `metro:` scheme resolver), which maps `metro:babel-runtime/<path>` to metro-runtime's own `@babel/runtime` dependency. This diff is the Metro half of pointing `@babel/plugin-transform-runtime` at it.

When `transformer.enableBabelRuntime` is literally `true` (the default), the transform worker passes the Babel transformer `babelRuntimeModuleName: 'metro:babel-runtime'` and `babelRuntimeVersion` - the installed version of that `@babel/runtime`. `metro-babel-transformer` forwards them to presets as Babel caller data, as `babelRuntimeModuleName` and `enableBabelRuntime` respectively, matching what `@react-native/babel-preset` reads from react/react-native#57974. Caller data reaches a preset however it's configured - including via a project `babel.config.js`, where the preset's options come from the user.

The version is read from the installed `package.json` rather than pinned to metro-runtime's range floor, so output benefits from runtime updates without a manual sync. Because that makes transform output depend on it, the `package.json` is added to the transform worker's cache key when `enableBabelRuntime` is `true`.

A string `enableBabelRuntime` keeps its current meaning (a version for the project's own `@babel/runtime`) and passes neither value. It's now documented as deprecated in favour of `true`, since a version describing the project's copy is meaningless once helpers come from Metro's.

The lookup of metro-runtime's `@babel/runtime` moves from `metroSchemeResolver` into a shared `metro/private/lib/metroBabelRuntime`, so the resolver and the worker agree on the same copy.

This is a no-op until a preset reads it.

Changelog:
```
 - **[Feature]**: When `transformer.enableBabelRuntime` is `true`, pass Metro's own `@babel/runtime` (`metro:babel-runtime`) and its installed version to Babel presets via caller data
 - **[Deprecated]**: String values of `transformer.enableBabelRuntime` (use `true`)
```

Test plan:
```
yarn jest packages/metro-transform-worker packages/metro-babel-transformer packages/metro/src/lib
yarn flow check
yarn build-ts-defs && yarn verify-api-snapshots
yarn typecheck-ts
```
robhogan added a commit to react/metro that referenced this pull request Sep 21, 2026
…caller

Summary:
Follow-up to 8388f71 (the default `metro:` scheme resolver), which maps `metro:babel-runtime/<path>` to metro-runtime's own `@babel/runtime` dependency. This diff is the Metro half of pointing `@babel/plugin-transform-runtime` at it.

When `transformer.enableBabelRuntime` is literally `true` (the default), the transform worker passes the Babel transformer `babelRuntimeModuleName: 'metro:babel-runtime'` and `babelRuntimeVersion` - the installed version of that `@babel/runtime`. `metro-babel-transformer` forwards them to presets as Babel caller data, as `babelRuntimeModuleName` and `enableBabelRuntime` respectively, matching what `@react-native/babel-preset` reads from react/react-native#57974. Caller data reaches a preset however it's configured - including via a project `babel.config.js`, where the preset's options come from the user.

The version is read from the installed `package.json` rather than pinned to metro-runtime's range floor, so output benefits from runtime updates without a manual sync. Because that makes transform output depend on it, the `package.json` is added to the transform worker's cache key when `enableBabelRuntime` is `true`.

A string `enableBabelRuntime` keeps its current meaning (a version for the project's own `@babel/runtime`) and passes neither value. It's now documented as deprecated in favour of `true`, since a version describing the project's copy is meaningless once helpers come from Metro's.

The lookup of metro-runtime's `@babel/runtime` moves from `metroSchemeResolver` into a shared `metro/private/lib/metroBabelRuntime`, so the resolver and the worker agree on the same copy.

This is a no-op until a preset reads it.

Changelog:
```
 - **[Feature]**: When `transformer.enableBabelRuntime` is `true`, pass Metro's own `@babel/runtime` (`metro:babel-runtime`) and its installed version to Babel presets via caller data
 - **[Deprecated]**: String values of `transformer.enableBabelRuntime` (use `true`)
```

Test plan:
New integration test (`integration_tests/__tests__/babel-runtime-test.js`) builds and executes a bundle through the full Metro + Babel pipeline with Metro's own `metro-babel-transformer`, and a fixture `babel.config.js` whose preset reads `@babel/runtime` configuration from caller data, as `@react-native/babel-preset` does. The fixture uses `import * as`, whose `interopRequireWildcard` helper is only available from `@babel/runtime` 7.14.0, so it's imported rather than inlined only if the installed version reaches the preset:
 - With `enableBabelRuntime: true`, the bundle imports `metro:babel-runtime/helpers/interopRequireWildcard`, which resolves to metro-runtime's own `@babel/runtime`, and executes correctly.
 - With `enableBabelRuntime: false`, the helper is inlined, and the bundle executes correctly.

```
yarn jest packages/metro/src/integration_tests packages/metro-transform-worker packages/metro-babel-transformer packages/metro/src/lib
yarn flow check
yarn build-ts-defs && yarn verify-api-snapshots
yarn typecheck-ts
```
…set (#57974)

Summary:

Adds a mechanism for callers to configure `babel/plugin-transform-runtime`
programmatically via Babel caller data, in addition to preset options.

- `enableBabelRuntime` (boolean toggle, or a string to pin a specific
  `babel/runtime` version) can now be read from the Babel caller.
- `babelRuntimeModuleName` (the module helpers are imported from) can be
  provided via preset options or the Babel caller.

Both values are passed as separate primitives (Babel only permits primitive
caller values). Preset options take precedence over caller data, consistent
with how `unstable_transformProfile` is resolved.

Changelog:
[General][Added] - Allow `react-native/babel-preset` to read `enableBabelRuntime` and `babelRuntimeModuleName` from Babel caller data

Reviewed By: vzaidman

Differential Revision: D114070686
@meta-codesync meta-codesync Bot closed this in 168a0d7 Sep 21, 2026
@meta-codesync meta-codesync Bot added the Merged This PR has been merged. label Sep 21, 2026
@meta-codesync

meta-codesync Bot commented Sep 21, 2026

Copy link
Copy Markdown

This pull request has been merged in 168a0d7.

@cortinico

Copy link
Copy Markdown
Contributor

Done 👍

@robhogan
robhogan deleted the export-D114070686 branch September 22, 2026 06:10
robhogan added a commit to react/metro that referenced this pull request Oct 16, 2026
8388f71 added the `metro:` scheme resolver, which maps `metro:babel-runtime/<path>` to metro-runtime's own `@babel/runtime`. This is the Metro half of pointing `@babel/plugin-transform-runtime` at it - the preset half is react/react-native#57974, and until a preset reads it this is a no-op.

When `transformer.enableBabelRuntime` is `true` (the default), Metro now passes presets `babelRuntimeModuleName: 'metro:babel-runtime'` and that copy's installed version as Babel caller data, matching what `@react-native/babel-preset` reads. Caller data reaches a preset however it's configured, including from a project `babel.config.js`. The version is read from the installed package, so its `package.json` joins the transform cache key.

A string `enableBabelRuntime` (a version of the project's own `@babel/runtime`) keeps its meaning and passes neither. It's now documented as deprecated.

Changelog:
```
 - **[Feature]**: When `transformer.enableBabelRuntime` is `true`, pass Metro's own `@babel/runtime` (`metro:babel-runtime`) and its installed version to Babel presets via caller data
 - **[Deprecated]**: String values of `transformer.enableBabelRuntime` (use `true`)
```

Test plan:
New integration test builds and runs a bundle through the full Metro + Babel pipeline, with a preset that reads the caller data as `@react-native/babel-preset` does. Its fixture uses `import * as`, whose helper `@babel/runtime` only exports from 7.14.0, so the helper is imported rather than inlined only if the version reaches the preset. Covered with `enableBabelRuntime` both `true` and `false`. Metro's `@babel/runtime` sits outside the test's watch folders, so this also exercises the dynamic root from #2065 end to end.
robhogan added a commit to react/metro that referenced this pull request Oct 16, 2026
8388f71 added the `metro:` scheme resolver, which maps `metro:babel-runtime/<path>` to metro-runtime's own `@babel/runtime`. This is the Metro half of pointing `@babel/plugin-transform-runtime` at it - the preset half is react/react-native#57974, and until a preset reads it this is a no-op.

When `transformer.enableBabelRuntime` is `true` (the default), Metro now passes presets `babelRuntimeModuleName: 'metro:babel-runtime'` and that copy's installed version as Babel caller data, matching what `@react-native/babel-preset` reads. Caller data reaches a preset however it's configured, including from a project `babel.config.js`. The version is read from the installed package, so its `package.json` joins the transform cache key.

A string `enableBabelRuntime` (a version of the project's own `@babel/runtime`) keeps its meaning and passes neither. It's now documented as deprecated.

With presets importing from it, Metro's `@babel/runtime` needs to be in the file map, and Metro adds it as a root at startup when no configured root contains it. If that fails (eg the watcher can't watch it), Metro now warns and starts anyway, rather than failing for projects that never import from it.

Changelog:
```
 - **[Feature]**: When `transformer.enableBabelRuntime` is `true`, pass Metro's own `@babel/runtime` (`metro:babel-runtime`) and its installed version to Babel presets via caller data
 - **[Deprecated]**: String values of `transformer.enableBabelRuntime` (use `true`)
```

Test plan:
New integration test builds and runs a bundle through the full Metro + Babel pipeline, with a preset that reads the caller data as `@react-native/babel-preset` does. Its fixture uses `import * as`, whose helper `@babel/runtime` only exports from 7.14.0, so the helper is imported rather than inlined only if the version reaches the preset. Covered with `enableBabelRuntime` both `true` and `false`. Metro's `@babel/runtime` sits outside the test's watch folders, so this also exercises the dynamic root from #2065 end to end.
robhogan added a commit to react/metro that referenced this pull request Oct 16, 2026
8388f71 added a `metro:` scheme resolver that maps `metro:babel-runtime/<path>` to metro-runtime's own `@babel/runtime`. This tells Babel presets to use it, so `@babel/plugin-transform-runtime` imports helpers from a `@babel/runtime` that always exists at a known version.

With `transformer.enableBabelRuntime: true` (the default), the wiring is:

1. The transform worker passes the Babel transformer `metro:babel-runtime` and the installed version of Metro's `@babel/runtime`. That copy's `package.json` joins the transform cache key.
2. `metro-babel-transformer` forwards both to presets as Babel caller data, named `babelRuntimeModuleName` and `enableBabelRuntime` as `@react-native/babel-preset` expects.
3. The preset configures `@babel/plugin-transform-runtime` with that `moduleName` and `version`. The preset half is react/react-native#57974.
4. Helpers are emitted as `metro:babel-runtime/helpers/...` imports, which the `metro:` resolver maps to Metro's `@babel/runtime`. #2065 makes sure that's in the file map, wherever it's installed.

Caller data reaches a preset however it's configured, including from a project `babel.config.js`. Until a preset reads it, this is a no-op.

A string `enableBabelRuntime` (a version of the project's own `@babel/runtime`) keeps its meaning and passes neither value. It's now documented as deprecated.

If Metro can't add its `@babel/runtime` to the file map at startup, it now warns and starts anyway, rather than failing for projects that never import from it.

Changelog:
```
 - **[Feature]**: When `transformer.enableBabelRuntime` is `true`, pass Metro's own `@babel/runtime` (`metro:babel-runtime`) and its installed version to Babel presets via caller data
 - **[Deprecated]**: String values of `transformer.enableBabelRuntime` (use `true`)
```

Test plan:
New integration test builds and runs a bundle through the full Metro + Babel pipeline, with a preset that reads the caller data as `@react-native/babel-preset` does. Its fixture uses `import * as`, whose helper `@babel/runtime` only exports from 7.14.0, so the helper is imported rather than inlined only if the version reaches the preset. Covered with `enableBabelRuntime` both `true` and `false`. Metro's `@babel/runtime` sits outside the test's watch folders, so this also exercises #2065 end to end.
robhogan added a commit to react/metro that referenced this pull request Oct 16, 2026
Part of fixing react/react-native#57123. Presets don't know which `@babel/runtime` version helpers will resolve to, so `@babel/plugin-transform-runtime` inlines newer helpers such as `_interopRequireWildcard` into every module that uses them.

#1804 added scheme resolvers, and Metro's default `metro:` resolver maps `metro:babel-runtime/<path>` to metro-runtime's own `@babel/runtime`. This tells Babel presets to import helpers from there, at that copy's installed version.

With `transformer.enableBabelRuntime: true` (the default), the wiring is:

1. The transform worker passes the Babel transformer `metro:babel-runtime` and the installed version of Metro's `@babel/runtime`. That copy's `package.json` joins the transform cache key.
2. `metro-babel-transformer` forwards both to presets as Babel caller data, named `babelRuntimeModuleName` and `enableBabelRuntime` as `@react-native/babel-preset` expects.
3. The preset configures `@babel/plugin-transform-runtime` with that `moduleName` and `version`. The preset half is react/react-native#57974.
4. Helpers are emitted as `metro:babel-runtime/helpers/...` imports, which the `metro:` resolver maps to Metro's `@babel/runtime`. #2065 makes sure that's in the file map, wherever it's installed.

Caller data reaches a preset however it's configured, including from a project `babel.config.js`. Until a preset reads it, this is a no-op.

A string `enableBabelRuntime` (a version of the project's own `@babel/runtime`) keeps its meaning and passes neither value. It's now documented as deprecated.

If Metro can't add its `@babel/runtime` to the file map at startup, it now warns and starts anyway, rather than failing for projects that never import from it.

## Changelog

```
 - **[Feature]**: When `transformer.enableBabelRuntime` is `true`, pass Metro's own `@babel/runtime` (`metro:babel-runtime`) and its installed version to Babel presets via caller data
 - **[Deprecated]**: String values of `transformer.enableBabelRuntime` (use `true`)
```

## Test plan

New integration test bundles and runs a fixture that does `import * as values from './values'`, with a `babel.config.js` that reads the caller data as `@react-native/babel-preset` does. `@babel/runtime` only exports this helper from 7.14.0, so it's imported rather than inlined only if the version reaches the preset. Metro's `@babel/runtime` is outside the test's watch folders, so this also exercises #2065.

With `enableBabelRuntime: false`, the preset gets no caller data and the helper is inlined, as it is in React Native today:

```js
__d(function (global, require, _$$_IMPORT_DEFAULT, _$$_IMPORT_ALL, module, exports, _dependencyMap) {
  "use strict";

  Object.defineProperty(exports, "__esModule", {
    value: true
  });
  exports.answer = void 0;
  var values = _interopRequireWildcard(require(_dependencyMap[0], "./values"));
  function _interopRequireWildcard(e, t) { /* 616 characters, in every module that uses `import * as` */ }
  const answer = exports.answer = values.answer;
},0,[1],"index.js");
```

With `enableBabelRuntime: true`, it's imported from Metro's own copy, which the test checks is metro-runtime's `@babel/runtime` (module `1`, `node_modules/@babel/runtime/helpers/interopRequireWildcard.js`):

```js
__d(function (global, require, _$$_IMPORT_DEFAULT, _$$_IMPORT_ALL, module, exports, _dependencyMap) {
  "use strict";

  var _interopRequireWildcard = require(_dependencyMap[0], "metro:babel-runtime/helpers/interopRequireWildcard").default;
  Object.defineProperty(exports, "__esModule", {
    value: true
  });
  exports.answer = void 0;
  var values = _interopRequireWildcard(require(_dependencyMap[1], "./values"));
  const answer = exports.answer = values.answer;
},0,[1,3],"index.js");
```
robhogan added a commit to react/metro that referenced this pull request Oct 16, 2026
Part of fixing react/react-native#57123. Presets don't know which `@babel/runtime` version helpers will resolve to, so `@babel/plugin-transform-runtime` inlines newer helpers such as `_interopRequireWildcard` into every module that uses them.

#1804 added scheme resolvers, which Metro uses for its own `metro:` namespace. This tells Babel presets to import helpers from Metro's own `@babel/runtime`, at its installed version.

With `transformer.enableBabelRuntime: true` (the default), the wiring is:

1. The transform worker passes the Babel transformer `metro:babel-runtime` and the installed version of Metro's `@babel/runtime`. That copy's `package.json` joins the transform cache key. (This PR)
2. `metro-babel-transformer` forwards both to presets as Babel caller data, named `babelRuntimeModuleName` and `enableBabelRuntime`. (This PR)
3. `@react-native/metro-babel-transformer`, which React Native projects use instead, builds its own caller and needs to forward them too. (To do)
4. `@react-native/babel-preset` configures `@babel/plugin-transform-runtime` with that `moduleName` and `version`. (Landed in react/react-native#57974)
5. Helpers are emitted as `metro:babel-runtime/helpers/...` imports, which Metro's `metro:` resolver maps to metro-runtime's `@babel/runtime`. (Landed in Metro 0.87.1)
6. Metro adds that `@babel/runtime` to the file map, wherever it's installed. (Landed in #2065, unreleased)

Caller data reaches a preset however it's configured, including from a project `babel.config.js`. In a React Native project, this is a no-op until step 3. Expo CLI's Babel transformer doesn't forward these yet either.

A string `enableBabelRuntime` (a version of the project's own `@babel/runtime`) keeps its meaning and passes neither value. It's now documented as deprecated.

If Metro can't add its `@babel/runtime` to the file map at startup, it now warns and starts anyway, rather than failing for projects that never import from it.

## Changelog

```
 - **[Feature]**: When `transformer.enableBabelRuntime` is `true`, pass Metro's own `@babel/runtime` (`metro:babel-runtime`) and its installed version to Babel presets via caller data
 - **[Deprecated]**: String values of `transformer.enableBabelRuntime` (use `true`)
```

## Test plan

New integration test bundles and runs a fixture that does `import * as values from './values'`, with a `babel.config.js` that reads the caller data as `@react-native/babel-preset` does. `@babel/runtime` only exports this helper from 7.14.0, so it's imported rather than inlined only if the version reaches the preset. Metro's `@babel/runtime` is outside the test's watch folders, so this also exercises #2065.

With `enableBabelRuntime: false`, the preset gets no caller data and the helper is inlined, as it is in React Native today:

```js
__d(function (global, require, _$$_IMPORT_DEFAULT, _$$_IMPORT_ALL, module, exports, _dependencyMap) {
  "use strict";

  Object.defineProperty(exports, "__esModule", {
    value: true
  });
  exports.answer = void 0;
  var values = _interopRequireWildcard(require(_dependencyMap[0], "./values"));
  function _interopRequireWildcard(e, t) { /* 616 characters, in every module that uses `import * as` */ }
  const answer = exports.answer = values.answer;
},0,[1],"index.js");
```

With `enableBabelRuntime: true`, it's imported from Metro's own copy, which the test checks is metro-runtime's `@babel/runtime` (module `1`, `node_modules/@babel/runtime/helpers/interopRequireWildcard.js`):

```js
__d(function (global, require, _$$_IMPORT_DEFAULT, _$$_IMPORT_ALL, module, exports, _dependencyMap) {
  "use strict";

  var _interopRequireWildcard = require(_dependencyMap[0], "metro:babel-runtime/helpers/interopRequireWildcard").default;
  Object.defineProperty(exports, "__esModule", {
    value: true
  });
  exports.answer = void 0;
  var values = _interopRequireWildcard(require(_dependencyMap[1], "./values"));
  const answer = exports.answer = values.answer;
},0,[1,3],"index.js");
```
robhogan added a commit to react/metro that referenced this pull request Oct 16, 2026
Part of fixing react/react-native#57123. Presets don't know which `@babel/runtime` version helpers will resolve to, so `@babel/plugin-transform-runtime` inlines newer helpers such as `_interopRequireWildcard` into every module that uses them.

#1804 added scheme resolvers, which Metro uses for its own `metro:` namespace. This tells Babel presets to import helpers from Metro's own `@babel/runtime`, at its installed version.

With `transformer.enableBabelRuntime: true` (the default), the wiring is:

1. The transform worker passes the Babel transformer `metro:babel-runtime` and the installed version of Metro's `@babel/runtime`. That copy's `package.json` joins the transform cache key. (This PR)
2. `metro-babel-transformer` forwards both to presets as Babel caller data, named `babelRuntimeModuleName` and `enableBabelRuntime`. (This PR)
3. `@react-native/metro-babel-transformer`, which React Native projects use instead, builds its own caller and needs to forward them too. (To do)
4. `@react-native/babel-preset` configures `@babel/plugin-transform-runtime` with that `moduleName` and `version`. (Landed in react/react-native#57974)
5. Helpers are emitted as `metro:babel-runtime/helpers/...` imports, which Metro's `metro:` resolver maps to metro-runtime's `@babel/runtime`. (Landed in Metro 0.87.1)
6. Metro adds that `@babel/runtime` to the file map, wherever it's installed. (Landed in #2065, unreleased)

Caller data reaches a preset however it's configured, including from a project `babel.config.js`. In a React Native project, this is a no-op until step 3. Expo CLI's Babel transformer doesn't forward these yet either.

A string `enableBabelRuntime` (a version of the project's own `@babel/runtime`) keeps its meaning and passes neither value. It's now documented as deprecated.

## Changelog

```
 - **[Feature]**: When `transformer.enableBabelRuntime` is `true`, pass Metro's own `@babel/runtime` (`metro:babel-runtime`) and its installed version to Babel presets via caller data
 - **[Deprecated]**: String values of `transformer.enableBabelRuntime` (use `true`)
```

## Test plan

New integration test bundles and runs a fixture that does `import * as values from './values'`, with a `babel.config.js` that reads the caller data as `@react-native/babel-preset` does. `@babel/runtime` only exports this helper from 7.14.0, so it's imported rather than inlined only if the version reaches the preset. Metro's `@babel/runtime` is outside the test's watch folders, so this also exercises #2065.

With `enableBabelRuntime: false`, the preset gets no caller data and the helper is inlined, as it is in React Native today:

```js
__d(function (global, require, _$$_IMPORT_DEFAULT, _$$_IMPORT_ALL, module, exports, _dependencyMap) {
  "use strict";

  Object.defineProperty(exports, "__esModule", {
    value: true
  });
  exports.answer = void 0;
  var values = _interopRequireWildcard(require(_dependencyMap[0], "./values"));
  function _interopRequireWildcard(e, t) { /* 616 characters, in every module that uses `import * as` */ }
  const answer = exports.answer = values.answer;
},0,[1],"index.js");
```

With `enableBabelRuntime: true`, it's imported from Metro's own copy, which the test checks is metro-runtime's `@babel/runtime` (module `1`, `node_modules/@babel/runtime/helpers/interopRequireWildcard.js`):

```js
__d(function (global, require, _$$_IMPORT_DEFAULT, _$$_IMPORT_ALL, module, exports, _dependencyMap) {
  "use strict";

  var _interopRequireWildcard = require(_dependencyMap[0], "metro:babel-runtime/helpers/interopRequireWildcard").default;
  Object.defineProperty(exports, "__esModule", {
    value: true
  });
  exports.answer = void 0;
  var values = _interopRequireWildcard(require(_dependencyMap[1], "./values"));
  const answer = exports.answer = values.answer;
},0,[1,3],"index.js");
```
robhogan added a commit to react/metro that referenced this pull request Oct 16, 2026
Helpers that `@babel/plugin-transform-runtime` imports from `@babel/runtime` can fail to resolve, depending on where, or whether, a project installs it. Worse, presets don't know which version they'll get, so the plugin assumes the oldest and inlines newer helpers instead, eg `_interopRequireWildcard` into every module that uses `import * as` (react/react-native#57123).

This tells Babel presets to import helpers from Metro's own `@babel/runtime`, which always exists, at its installed version, so the plugin can target that version. It builds on the scheme resolvers from #1804, which Metro uses for its own `metro:` namespace.

With `transformer.enableBabelRuntime: true` (the default), the wiring is:

1. The transform worker passes the Babel transformer `metro:babel-runtime` and the installed version of Metro's `@babel/runtime`. That copy's `package.json` joins the transform cache key. (This PR)
2. `metro-babel-transformer` forwards both to presets as Babel caller data, named `babelRuntimeModuleName` and `enableBabelRuntime`. (This PR)
3. `@react-native/metro-babel-transformer`, which React Native projects use instead, builds its own caller and needs to forward them too. (To do)
4. `@react-native/babel-preset` configures `@babel/plugin-transform-runtime` with that `moduleName` and `version`. (Landed in react/react-native#57974)
5. Helpers are emitted as `metro:babel-runtime/helpers/...` imports, which Metro's `metro:` resolver maps to metro-runtime's `@babel/runtime`. (Landed in Metro 0.87.1)
6. Metro adds that `@babel/runtime` to the file map, wherever it's installed. (Landed in #2065, unreleased)

Caller data reaches a preset however it's configured, including from a project `babel.config.js`. In a React Native project, this is a no-op until step 3. Expo CLI's Babel transformer doesn't forward these yet either.

A string `enableBabelRuntime` (a version of the project's own `@babel/runtime`) keeps its meaning and passes neither value. It's now documented as deprecated.

## Changelog

```
 - **[Feature]**: When `transformer.enableBabelRuntime` is `true`, pass Metro's own `@babel/runtime` (`metro:babel-runtime`) and its installed version to Babel presets via caller data
 - **[Deprecated]**: String values of `transformer.enableBabelRuntime` (use `true`)
```

## Test plan

New integration test bundles and runs a fixture that does `import * as values from './values'`, with a `babel.config.js` that reads the caller data as `@react-native/babel-preset` does. `@babel/runtime` only exports this helper from 7.14.0, so it's imported rather than inlined only if the version reaches the preset. Metro's `@babel/runtime` is outside the test's watch folders, so this also exercises #2065.

With `enableBabelRuntime: false`, the preset gets no caller data and the helper is inlined, as it is in React Native today:

```js
__d(function (global, require, _$$_IMPORT_DEFAULT, _$$_IMPORT_ALL, module, exports, _dependencyMap) {
  "use strict";

  Object.defineProperty(exports, "__esModule", {
    value: true
  });
  exports.answer = void 0;
  var values = _interopRequireWildcard(require(_dependencyMap[0], "./values"));
  function _interopRequireWildcard(e, t) { /* 616 characters, in every module that uses `import * as` */ }
  const answer = exports.answer = values.answer;
},0,[1],"index.js");
```

With `enableBabelRuntime: true`, it's imported from Metro's own copy, which the test checks is metro-runtime's `@babel/runtime` (module `1`, `node_modules/@babel/runtime/helpers/interopRequireWildcard.js`):

```js
__d(function (global, require, _$$_IMPORT_DEFAULT, _$$_IMPORT_ALL, module, exports, _dependencyMap) {
  "use strict";

  var _interopRequireWildcard = require(_dependencyMap[0], "metro:babel-runtime/helpers/interopRequireWildcard").default;
  Object.defineProperty(exports, "__esModule", {
    value: true
  });
  exports.answer = void 0;
  var values = _interopRequireWildcard(require(_dependencyMap[1], "./values"));
  const answer = exports.answer = values.answer;
},0,[1,3],"index.js");
```
robhogan added a commit to react/metro that referenced this pull request Oct 16, 2026
Helpers that `@babel/plugin-transform-runtime` imports from `@babel/runtime` can fail to resolve, depending on where, or whether, a project installs it. Worse, presets don't know which version they'll get, so the plugin assumes the oldest and inlines newer helpers instead, eg `_interopRequireWildcard` into every module that uses `import * as` (react/react-native#57123).

This tells Babel presets to import helpers from Metro's own `@babel/runtime`, which always exists, at its installed version, so the plugin can target that version. It builds on the scheme resolvers from #1804, which Metro uses for its own `metro:` namespace.

With `transformer.enableBabelRuntime: true` (the default), the wiring is:

1. The transform worker passes the Babel transformer `metro:babel-runtime` and the installed version of Metro's `@babel/runtime`. That copy's `package.json` joins the transform cache key. (This PR)
2. `metro-babel-transformer` forwards both to presets as Babel caller data, named `babelRuntimeModuleName` and `enableBabelRuntime`. (This PR)
3. `@react-native/metro-babel-transformer`, which React Native projects use instead, builds its own caller and needs to forward them too. (To do)
4. `@react-native/babel-preset` configures `@babel/plugin-transform-runtime` with that `moduleName` and `version`. (Landed in react/react-native#57974)
5. Helpers are emitted as `metro:babel-runtime/helpers/...` imports, which Metro's `metro:` resolver maps to metro-runtime's `@babel/runtime`. (Landed in Metro 0.87.1)
6. Metro adds that `@babel/runtime` to the file map, wherever it's installed. (Landed in #2065, unreleased)

Caller data reaches a preset however it's configured, including from a project `babel.config.js`. In a React Native project, this is a no-op until step 3. Expo CLI's Babel transformer doesn't forward these yet either.

A string `enableBabelRuntime` (a version of the project's own `@babel/runtime`) keeps its meaning and passes neither value. It's now documented as deprecated.

## Changelog

```
 - **[Feature]**: When `transformer.enableBabelRuntime` is `true`, pass Metro's own `@babel/runtime` (`metro:babel-runtime`) and its installed version to Babel presets via caller data
 - **[Deprecated]**: String values of `transformer.enableBabelRuntime` (use `true`)
```

## Test plan

New integration test bundles and runs a fixture that does `import * as values from './values'`, with a `babel.config.js` that reads the caller data as `@react-native/babel-preset` does. `@babel/runtime` only exports this helper from 7.14.0, so it's imported rather than inlined only if the version reaches the preset. Metro's `@babel/runtime` is outside the test's watch folders, so this also exercises #2065.

With `enableBabelRuntime: false`, the preset gets no caller data and the helper is inlined, as it is in React Native today:

```js
__d(function (global, require, _$$_IMPORT_DEFAULT, _$$_IMPORT_ALL, module, exports, _dependencyMap) {
  "use strict";

  Object.defineProperty(exports, "__esModule", {
    value: true
  });
  exports.answer = void 0;
  var values = _interopRequireWildcard(require(_dependencyMap[0], "./values"));
  function _interopRequireWildcard(e, t) { /* 616 characters, in every module that uses `import * as` */ }
  const answer = exports.answer = values.answer;
},0,[1],"index.js");
```

With `enableBabelRuntime: true`, it's imported from Metro's own copy, which the test checks is metro-runtime's `@babel/runtime` (module `1`, `node_modules/@babel/runtime/helpers/interopRequireWildcard.js`):

```js
__d(function (global, require, _$$_IMPORT_DEFAULT, _$$_IMPORT_ALL, module, exports, _dependencyMap) {
  "use strict";

  var _interopRequireWildcard = require(_dependencyMap[0], "metro:babel-runtime/helpers/interopRequireWildcard").default;
  Object.defineProperty(exports, "__esModule", {
    value: true
  });
  exports.answer = void 0;
  var values = _interopRequireWildcard(require(_dependencyMap[1], "./values"));
  const answer = exports.answer = values.answer;
},0,[1,3],"index.js");
```

### End to end with the RN wiring

This compares the iOS release bundle of React Native's `private/helloworld`, which has a `babel.config.js` naming `@react-native/babel-preset` as in the template. Current defaults are Metro `main` with React Native `main`, against this PR with the RN wiring from step 3 above. Metro's `@babel/runtime` is 7.29.7.

| | Current defaults | This PR + RN wiring | Change |
|---|---|---|---|
| Minified JS | 906,504 B | 807,389 B | -99,115 B (-10.9%) |
| Minified JS, gzip -9 | 217,794 B | 204,595 B | -13,199 B (-6.1%) |
| Hermes bytecode (`hermesc -O`) | 1,261,863 B | 1,173,832 B | -88,031 B (-7.0%) |
| Modules | 518 | 521 | +3 |
| Inlined `_interopRequireWildcard` | 199 | 0 | |
| Inlined `_callSuper` / `_isNativeReflectConstruct` | 63 / 65 | 0 / 0 | |
| Inlined `_superPropGet` | 25 | 0 | |

Every module that uses `import * as` or extends a class carries its own copy of these helpers today. For example, React Native's `UnimplementedView` with current defaults (the three helper bodies, 1,091 bytes unminified, elided):

```js
__d(function (global, require, _$$_IMPORT_DEFAULT, _$$_IMPORT_ALL, module, exports, _dependencyMap) {
  'use strict';

  var _interopRequireDefault = require(_dependencyMap[0]);
  Object.defineProperty(exports, "__esModule", {
    value: true
  });
  exports.default = undefined;
  var _classCallCheck2 = _interopRequireDefault(require(_dependencyMap[1]));
  var _createClass2 = _interopRequireDefault(require(_dependencyMap[2]));
  var _possibleConstructorReturn2 = _interopRequireDefault(require(_dependencyMap[3]));
  var _getPrototypeOf2 = _interopRequireDefault(require(_dependencyMap[4]));
  var _inherits2 = _interopRequireDefault(require(_dependencyMap[5]));
  var _StyleSheet = _interopRequireDefault(require(_dependencyMap[6]));
  var React = _interopRequireWildcard(require(_dependencyMap[7]));
  var _jsxRuntime = require(_dependencyMap[8]);
  function _interopRequireWildcard(e, t) { /* ... */ }
  function _callSuper(t, o, e) { /* ... */ }
  function _isNativeReflectConstruct() { /* ... */ }
  var UnimplementedView = /*#__PURE__*/function (_React$Component) {
    function UnimplementedView() {
      (0, _classCallCheck2.default)(this, UnimplementedView);
      return _callSuper(this, UnimplementedView, arguments);
    }
    (0, _inherits2.default)(UnimplementedView, _React$Component);
    return (0, _createClass2.default)(UnimplementedView, [{
      key: "render",
      value: function render() {
        var View = require(_dependencyMap[9]).default;
        return /*#__PURE__*/(0, _jsxRuntime.jsx)(View, {
          style: [styles.unimplementedView, this.props.style],
          children: this.props.children
        });
      }
    }]);
  }(React.Component);
  var styles = _StyleSheet.default.create({
    unimplementedView: {}
  });
  var _default = exports.default = UnimplementedView;
},404,[1,25,26,58,60,63,7,3,220,70]);
```

With this PR and the RN wiring, it imports them from Metro's `@babel/runtime` instead:

```js
__d(function (global, require, _$$_IMPORT_DEFAULT, _$$_IMPORT_ALL, module, exports, _dependencyMap) {
  'use strict';

  Object.defineProperty(exports, "__esModule", {
    value: true
  });
  exports.default = undefined;
  var _classCallCheck2 = require(_dependencyMap[0]).default(require(_dependencyMap[1]));
  var _createClass2 = require(_dependencyMap[0]).default(require(_dependencyMap[2]));
  var _callSuper2 = require(_dependencyMap[0]).default(require(_dependencyMap[3]));
  var _inherits2 = require(_dependencyMap[0]).default(require(_dependencyMap[4]));
  var _StyleSheet = require(_dependencyMap[0]).default(require(_dependencyMap[5]));
  var React = require(_dependencyMap[6]).default(require(_dependencyMap[7]));
  var _jsxRuntime = require(_dependencyMap[8]);
  var UnimplementedView = /*#__PURE__*/function (_React$Component) {
    function UnimplementedView() {
      (0, _classCallCheck2.default)(this, UnimplementedView);
      return (0, _callSuper2.default)(this, UnimplementedView, arguments);
    }
    (0, _inherits2.default)(UnimplementedView, _React$Component);
    return (0, _createClass2.default)(UnimplementedView, [{
      key: "render",
      value: function render() {
        var View = require(_dependencyMap[9]).default;
        return /*#__PURE__*/(0, _jsxRuntime.jsx)(View, {
          style: [styles.unimplementedView, this.props.style],
          children: this.props.children
        });
      }
    }]);
  }(React.Component);
  var styles = _StyleSheet.default.create({
    unimplementedView: {}
  });
  var _default = exports.default = UnimplementedView;
},407,[1,27,28,59,67,9,3,5,223,74]);
```

Not run on a device.
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. Merged This PR has been merged. meta-exported p: Facebook Partner: Facebook Partner

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants