From 82837f16e432d0c524e7180c72339ab4520f5fd8 Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Fri, 16 Oct 2026 16:44:07 +0100 Subject: [PATCH] Pass Metro's own @babel/runtime to Babel presets 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` (https://github.com/react/react-native/issues/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 https://github.com/react/metro/pull/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 https://github.com/react/react-native/pull/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 https://github.com/react/metro/pull/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 https://github.com/react/metro/pull/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. --- docs/Configuration.md | 6 +- .../src/__tests__/transform-test.js | 67 +++++++++++++++ packages/metro-babel-transformer/src/index.js | 12 +++ .../src/__tests__/index-test.js | 56 +++++++++++++ packages/metro-transform-worker/src/index.js | 16 ++++ .../__tests__/babel-runtime-test.js | 84 +++++++++++++++++++ .../babel-runtime/babel.config.js | 42 ++++++++++ .../basic_bundle/babel-runtime/index.js | 16 ++++ .../basic_bundle/babel-runtime/values.js | 11 +++ packages/metro/src/lib/metroBabelRuntime.js | 23 +++++ 10 files changed, 331 insertions(+), 2 deletions(-) create mode 100644 packages/metro/src/integration_tests/__tests__/babel-runtime-test.js create mode 100644 packages/metro/src/integration_tests/basic_bundle/babel-runtime/babel.config.js create mode 100644 packages/metro/src/integration_tests/basic_bundle/babel-runtime/index.js create mode 100644 packages/metro/src/integration_tests/basic_bundle/babel-runtime/values.js diff --git a/docs/Configuration.md b/docs/Configuration.md index a0797d3cd6..e3c2fbbdbb 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -603,9 +603,11 @@ This option only has an effect under the default [`transformerPath`](#transforme Type: `boolean | string` -Whether the transformer should use the `@babel/transform/runtime` plugin. Defaults to `true`. +Whether the transformer should use the `@babel/plugin-transform-runtime` plugin. Defaults to `true`. -If the value is a string, it is treated as a runtime version number and passed as `version` to the `@babel/plugin-transform-runtime` configuration. This allows you to optimize the generated Babel runtime calls based on the version installed in your project. +When `true`, Metro tells Babel presets that support it, through [caller data](https://babeljs.io/docs/options#caller), to import helpers from Metro's own `@babel/runtime` (`babelRuntimeModuleName: 'metro:babel-runtime'`) at its installed version (`enableBabelRuntime`). Helpers then come from a `@babel/runtime` that always exists at a known version, whether or not your project installs one. + +
Deprecated
If the value is a string, it is treated as a version of your project's own `@babel/runtime`, passed as `version` to `@babel/plugin-transform-runtime`, and helpers are imported from that copy. Use `true` instead. :::note This option only works under the default settings for React Native. It may have no effect in a project that uses custom [`transformerPath`](#transformerpath), a custom [`babelTransformerPath`](#babeltransformerpath) or a custom [Babel config file](https://babeljs.io/docs/en/config-files). diff --git a/packages/metro-babel-transformer/src/__tests__/transform-test.js b/packages/metro-babel-transformer/src/__tests__/transform-test.js index 8e1170d88c..cc0a1c089d 100644 --- a/packages/metro-babel-transformer/src/__tests__/transform-test.js +++ b/packages/metro-babel-transformer/src/__tests__/transform-test.js @@ -49,3 +49,70 @@ test('exposes the correct absolute path to a source file to plugins', () => { expect(pluginCwd).toEqual(PROJECT_ROOT); expect(visitorFilename).toEqual(path.resolve(PROJECT_ROOT, 'foo.js')); }); + +test('exposes the Babel runtime module name and version to presets via the caller', () => { + let callerRuntime; + transform({ + filename: 'foo.js', + src: 'console.log("foo");', + plugins: [ + babel => { + callerRuntime = { + babelRuntimeModuleName: babel.caller( + caller => caller?.babelRuntimeModuleName, + ), + enableBabelRuntime: babel.caller( + caller => caller?.enableBabelRuntime, + ), + }; + return {visitor: {}}; + }, + ], + options: { + babelRuntimeModuleName: 'metro:babel-runtime', + babelRuntimeVersion: '7.29.7', + dev: true, + enableBabelRuntime: true, + enableBabelRCLookup: false, + globalPrefix: '__metro__', + minify: false, + platform: null, + publicPath: 'test', + projectRoot: PROJECT_ROOT, + }, + }); + expect(callerRuntime).toEqual({ + babelRuntimeModuleName: 'metro:babel-runtime', + enableBabelRuntime: '7.29.7', + }); +}); + +test('omits the Babel runtime from the caller when not provided', () => { + let callerKeys; + transform({ + filename: 'foo.js', + src: 'console.log("foo");', + plugins: [ + babel => { + callerKeys = ['babelRuntimeModuleName', 'enableBabelRuntime'].filter( + key => + babel.caller( + caller => caller != null && Object.hasOwn(caller, key), + ), + ); + return {visitor: {}}; + }, + ], + options: { + dev: true, + enableBabelRuntime: false, + enableBabelRCLookup: false, + globalPrefix: '__metro__', + minify: false, + platform: null, + publicPath: 'test', + projectRoot: PROJECT_ROOT, + }, + }); + expect(callerKeys).toEqual([]); +}); diff --git a/packages/metro-babel-transformer/src/index.js b/packages/metro-babel-transformer/src/index.js index 91a3c2a15e..006d127eed 100644 --- a/packages/metro-babel-transformer/src/index.js +++ b/packages/metro-babel-transformer/src/index.js @@ -33,6 +33,8 @@ export type CustomTransformOptions = { export type TransformProfile = 'default' | 'hermes-stable' | 'hermes-canary'; type BabelTransformerOptions = Readonly<{ + babelRuntimeModuleName?: string, + babelRuntimeVersion?: string, customTransformOptions?: CustomTransformOptions, dev: boolean, enableBabelRCLookup?: boolean, @@ -111,6 +113,16 @@ function transform( name: 'metro', platform: options.platform, inlinePlatform: options.inlinePlatform, + // Named for the options of presets such as `@react-native/babel-preset`, + // which read them from the caller: a string `enableBabelRuntime` is the + // `@babel/runtime` version to target. + ...(options.babelRuntimeModuleName != null && + options.babelRuntimeVersion != null + ? { + babelRuntimeModuleName: options.babelRuntimeModuleName, + enableBabelRuntime: options.babelRuntimeVersion, + } + : null), }, // NOTE(EvanBacon): We split the parse/transform steps up to accommodate // Hermes parsing, but this defaults to cloning the AST which increases diff --git a/packages/metro-transform-worker/src/__tests__/index-test.js b/packages/metro-transform-worker/src/__tests__/index-test.js index 7d2956104a..1b8046c28c 100644 --- a/packages/metro-transform-worker/src/__tests__/index-test.js +++ b/packages/metro-transform-worker/src/__tests__/index-test.js @@ -30,12 +30,17 @@ jest import type {JsTransformerConfig, JsTransformOptions} from '../index'; import typeof * as TransformerType from '../index'; +import type { + BabelTransformer, + BabelTransformerArgs, +} from 'metro-babel-transformer'; import typeof FSType from 'node:fs'; const { posixToSystemPath: p, } = require('metro-resolver/private/__tests__/utils'); const {Buffer} = require('node:buffer'); +const path = require('node:path'); const babelTransformerPath = require.resolve('@react-native/metro-babel-transformer'); @@ -304,6 +309,57 @@ test('does not add "use strict" on non-modules', async () => { ); }); +function mockBabelTransformer(): JestMockFn< + [BabelTransformerArgs], + ReturnType, +> { + const actual = jest.requireActual(babelTransformerPath); + const transform = jest.fn(actual.transform); + jest.doMock(babelTransformerPath, () => ({...actual, transform})); + return transform; +} + +test("passes Metro's own Babel runtime when enableBabelRuntime is true", async () => { + const babelTransform = mockBabelTransformer(); + const {version} = jest.requireActual<{version: string, ...}>( + require.resolve('@babel/runtime/package.json', { + paths: [path.dirname(require.resolve('metro-runtime/package.json'))], + }), + ); + + await Transformer.transform( + baseConfig, + '/root', + 'local/file.js', + Buffer.from('arbitrary(code)', 'utf8'), + baseTransformOptions, + ); + + expect(babelTransform.mock.calls[0][0].options).toMatchObject({ + babelRuntimeModuleName: 'metro:babel-runtime', + babelRuntimeVersion: version, + }); +}); + +test.each([false, '7.25.0'])( + "does not pass Metro's own Babel runtime when enableBabelRuntime is %p", + async enableBabelRuntime => { + const babelTransform = mockBabelTransformer(); + + await Transformer.transform( + {...baseConfig, enableBabelRuntime}, + '/root', + 'local/file.js', + Buffer.from('arbitrary(code)', 'utf8'), + baseTransformOptions, + ); + + const {options} = babelTransform.mock.calls[0][0]; + expect(options).not.toHaveProperty('babelRuntimeModuleName'); + expect(options).not.toHaveProperty('babelRuntimeVersion'); + }, +); + test('preserves require() calls when module wrapping is disabled', async () => { const contents = ['require("./c");'].join('\n'); diff --git a/packages/metro-transform-worker/src/index.js b/packages/metro-transform-worker/src/index.js index fbae0fdaf2..9a3f9f92fe 100644 --- a/packages/metro-transform-worker/src/index.js +++ b/packages/metro-transform-worker/src/index.js @@ -53,6 +53,11 @@ import { vlqMapFromTuples, } from 'metro-source-map'; import metroTransformPlugins from 'metro-transform-plugins'; +import { + METRO_BABEL_RUNTIME_MODULE_NAME, + getMetroBabelRuntimePackageJsonPath, + getMetroBabelRuntimeVersion, +} from 'metro/private/lib/metroBabelRuntime'; import collectDependencies from 'metro/private/ModuleGraph/worker/collectDependencies'; import generateImportNames from 'metro/private/ModuleGraph/worker/generateImportNames'; import { @@ -662,6 +667,12 @@ function getBabelTransformArgs( filename: file.filename, // System-separated, project-root-relative options: { ...babelTransformerOptions, + ...(config.enableBabelRuntime === true + ? { + babelRuntimeModuleName: METRO_BABEL_RUNTIME_MODULE_NAME, + babelRuntimeVersion: getMetroBabelRuntimeVersion(), + } + : null), enableBabelRCLookup: config.enableBabelRCLookup, enableBabelRuntime: config.enableBabelRuntime, globalPrefix: config.globalPrefix, @@ -764,6 +775,11 @@ export const getCacheKey = ( require.resolve('metro/private/ModuleGraph/worker/generateImportNames'), require.resolve('metro/private/ModuleGraph/worker/JsFileWrapping'), ...metroTransformPlugins.getTransformPluginCacheKeyFiles(), + // Transform output depends on the installed version of Metro's own + // `@babel/runtime`, which is read from its `package.json`. + ...(config.enableBabelRuntime === true + ? [getMetroBabelRuntimePackageJsonPath()] + : []), ]); // $FlowFixMe[unsupported-syntax] diff --git a/packages/metro/src/integration_tests/__tests__/babel-runtime-test.js b/packages/metro/src/integration_tests/__tests__/babel-runtime-test.js new file mode 100644 index 0000000000..ed7ac81f5b --- /dev/null +++ b/packages/metro/src/integration_tests/__tests__/babel-runtime-test.js @@ -0,0 +1,84 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @oncall react_native + */ + +'use strict'; + +const Metro = require('../../..'); +const execBundle = require('../execBundle'); +const path = require('node:path'); + +jest.setTimeout(30 * 1000); + +const PROJECT_ROOT = path.resolve(__dirname, '../basic_bundle/babel-runtime'); + +// The `interopRequireWildcard` helper of the `@babel/runtime` that +// metro-runtime depends on. +function getBabelRuntimeHelperPath() { + return require.resolve('@babel/runtime/helpers/interopRequireWildcard', { + paths: [path.dirname(require.resolve('metro-runtime/package.json'))], + }); +} + +async function build(enableBabelRuntime) { + const baseConfig = await Metro.loadConfig({ + config: require.resolve('../metro.config.js'), + }); + // Build with Metro's own Babel transformer, which leaves Babel to discover + // the fixture's `babel.config.js` from the project root, and have Babel + // (rather than Metro) compile ESM so its helpers are exercised. + const config = { + ...baseConfig, + // Metro's own `@babel/runtime` is hoisted to the repo root, outside the + // shared config's watch folders, and Metro adds it to the file map itself. + projectRoot: PROJECT_ROOT, + transformer: { + ...baseConfig.transformer, + babelTransformerPath: require.resolve('metro-babel-transformer'), + enableBabelRuntime, + getTransformOptions: async () => ({ + transform: {experimentalImportSupport: false, inlineRequires: false}, + }), + }, + }; + const result = await Metro.runBuild(config, { + entry: 'index.js', + dev: true, + minify: false, + }); + return result.code; +} + +test("imports helpers from Metro's own @babel/runtime when enableBabelRuntime is true", async () => { + const code = await build(true); + + expect(code).toContain( + '"metro:babel-runtime/helpers/interopRequireWildcard"', + ); + + // The helper is bundled from the `@babel/runtime` that metro-runtime depends + // on. + const helperPath = getBabelRuntimeHelperPath(); + expect(code.replaceAll('\\\\', '/')).toContain( + JSON.stringify( + path.relative(PROJECT_ROOT, helperPath).replaceAll('\\', '/'), + ), + ); + + expect(execBundle(code)).toMatchObject({answer: 42}); +}); + +test('inlines helpers when enableBabelRuntime is false', async () => { + const code = await build(false); + + expect(code).not.toContain('metro:babel-runtime'); + expect(code).toContain('function _interopRequireWildcard('); + + expect(execBundle(code)).toMatchObject({answer: 42}); +}); diff --git a/packages/metro/src/integration_tests/basic_bundle/babel-runtime/babel.config.js b/packages/metro/src/integration_tests/basic_bundle/babel-runtime/babel.config.js new file mode 100644 index 0000000000..2e4c40b546 --- /dev/null +++ b/packages/metro/src/integration_tests/basic_bundle/babel-runtime/babel.config.js @@ -0,0 +1,42 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @oncall react_native + */ + +'use strict'; + +// A minimal stand-in for a preset such as `@react-native/babel-preset`, which +// reads `@babel/runtime` configuration from Babel caller data. As a root config +// it applies to every file in the bundle, so beyond stripping Flow (from +// Metro's own polyfills) it only transforms the fixture's modules. +module.exports = api => { + const moduleName = api.caller(caller => caller?.babelRuntimeModuleName); + const version = api.caller(caller => caller?.enableBabelRuntime); + return { + plugins: [ + require.resolve('flow-parser/babel-plugin'), + require.resolve('@babel/plugin-transform-flow-strip-types'), + ], + overrides: [ + { + test: __dirname, + plugins: [ + require.resolve('@babel/plugin-transform-modules-commonjs'), + ...(typeof moduleName === 'string' && typeof version === 'string' + ? [ + [ + require.resolve('@babel/plugin-transform-runtime'), + {helpers: true, regenerator: false, moduleName, version}, + ], + ] + : []), + ], + }, + ], + }; +}; diff --git a/packages/metro/src/integration_tests/basic_bundle/babel-runtime/index.js b/packages/metro/src/integration_tests/basic_bundle/babel-runtime/index.js new file mode 100644 index 0000000000..fff6e1482f --- /dev/null +++ b/packages/metro/src/integration_tests/basic_bundle/babel-runtime/index.js @@ -0,0 +1,16 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @oncall react_native + */ + +// `import * as` uses Babel's `interopRequireWildcard` helper, which +// `@babel/runtime` only provides from 7.14.0. It is imported from the runtime +// only when the preset is told a recent enough version, and inlined otherwise. +import * as values from './values'; + +export const answer = values.answer; diff --git a/packages/metro/src/integration_tests/basic_bundle/babel-runtime/values.js b/packages/metro/src/integration_tests/basic_bundle/babel-runtime/values.js new file mode 100644 index 0000000000..850632777d --- /dev/null +++ b/packages/metro/src/integration_tests/basic_bundle/babel-runtime/values.js @@ -0,0 +1,11 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @oncall react_native + */ + +export const answer = 42; diff --git a/packages/metro/src/lib/metroBabelRuntime.js b/packages/metro/src/lib/metroBabelRuntime.js index 593848c6e6..cc00b105c0 100644 --- a/packages/metro/src/lib/metroBabelRuntime.js +++ b/packages/metro/src/lib/metroBabelRuntime.js @@ -11,10 +11,17 @@ import * as path from 'node:path'; +/** + * The specifier that Metro's `metro:` scheme resolver resolves to the + * `@babel/runtime` that metro-runtime depends on. + */ +export const METRO_BABEL_RUNTIME_MODULE_NAME = 'metro:babel-runtime'; + // Resolved on first use rather than at module load, so that importing this // module (e.g. transitively from a resolution context) cannot fail in projects // that never use Metro's own `@babel/runtime`. let packageJsonPath: ?string = null; +let version: ?string = null; /** * The `package.json` path of the `@babel/runtime` that metro-runtime depends @@ -31,3 +38,19 @@ export function getMetroBabelRuntimePackageJsonPath(): string { } return packageJsonPath; } + +/** + * The installed version of the `@babel/runtime` that `metro:babel-runtime` + * resolves to. + */ +export function getMetroBabelRuntimeVersion(): string { + if (version == null) { + // $FlowFixMe[unsupported-syntax] Dynamic require of a resolved JSON path + const packageJson = require(getMetroBabelRuntimePackageJsonPath()) as { + version: string, + ... + }; + version = packageJson.version; + } + return version; +}