Skip to content

Commit 89db5ce

Browse files
robhoganmeta-codesync[bot]
authored andcommitted
Pass Metro's own @babel/runtime through @react-native/metro-babel-transformer (#59205)
Summary: This is the RN side of react/metro#1957 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 Pull Request resolved: #59205 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. Reviewed By: christophpurrer Differential Revision: D126404231 Pulled By: javache fbshipit-source-id: b85e157205abc5de437f7b68aae887d5c9010c95
1 parent 3864bc3 commit 89db5ce

2 files changed

Lines changed: 126 additions & 2 deletions

File tree

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
'use strict';
12+
13+
const generate = require('@babel/generator').default;
14+
const path = require('node:path');
15+
16+
const PROJECT_ROOT = path.sep === '/' ? '/my/project' : 'C:\\my\\project';
17+
const PROJECT_BABEL_CONFIG = path.join(
18+
__dirname,
19+
'__fixtures__',
20+
'babel.config.js',
21+
);
22+
// `import * as` uses the `interopRequireWildcard` helper, which
23+
// `@babel/plugin-transform-runtime` imports only when told a `@babel/runtime`
24+
// version of at least 7.14.0, and inlines otherwise.
25+
const SRC = "import * as foo from './foo';\nexport const bar = foo;";
26+
27+
// The transformer memoizes its resolved Babel config in a module-level
28+
// closure, so a fresh module instance is required for every distinct config.
29+
beforeEach(() => {
30+
jest.resetModules();
31+
});
32+
33+
const METRO_BABEL_RUNTIME = {
34+
babelRuntimeModuleName: 'metro:babel-runtime',
35+
babelRuntimeVersion: '7.29.0',
36+
};
37+
38+
function transformToCode({
39+
babelRuntime,
40+
extendsBabelConfigPath,
41+
}: {
42+
babelRuntime: ?Readonly<{
43+
babelRuntimeModuleName?: string,
44+
babelRuntimeVersion?: string,
45+
}>,
46+
extendsBabelConfigPath?: string,
47+
}): string {
48+
const {transform} = require('../index.js');
49+
const {ast} = transform({
50+
filename: path.join(PROJECT_ROOT, 'App.js'),
51+
src: SRC,
52+
plugins: [],
53+
options: {
54+
...babelRuntime,
55+
dev: true,
56+
enableBabelRuntime: true,
57+
enableBabelRCLookup: false,
58+
experimentalImportSupport: false,
59+
extendsBabelConfigPath,
60+
globalPrefix: '__metro__',
61+
hot: false,
62+
minify: false,
63+
platform: 'ios',
64+
publicPath: 'test',
65+
projectRoot: PROJECT_ROOT,
66+
},
67+
});
68+
return generate(ast).code;
69+
}
70+
71+
describe.each([
72+
['no project Babel config', undefined],
73+
['a project Babel config naming the preset', PROJECT_BABEL_CONFIG],
74+
])('with %s', (_name, extendsBabelConfigPath) => {
75+
test("imports helpers from Metro's own @babel/runtime when Metro passes it", () => {
76+
const code = transformToCode({
77+
extendsBabelConfigPath,
78+
babelRuntime: METRO_BABEL_RUNTIME,
79+
});
80+
81+
expect(code).toContain(
82+
'require("metro:babel-runtime/helpers/interopRequireWildcard")',
83+
);
84+
expect(code).not.toContain('function _interopRequireWildcard(');
85+
});
86+
87+
test('inlines newer helpers when Metro passes no @babel/runtime', () => {
88+
const code = transformToCode({
89+
extendsBabelConfigPath,
90+
babelRuntime: null,
91+
});
92+
93+
expect(code).not.toContain('metro:babel-runtime');
94+
expect(code).toContain('function _interopRequireWildcard(');
95+
});
96+
97+
test('ignores a @babel/runtime version without a module name', () => {
98+
const code = transformToCode({
99+
babelRuntime: {babelRuntimeVersion: '7.29.0'},
100+
extendsBabelConfigPath,
101+
});
102+
103+
expect(code).toContain('function _interopRequireWildcard(');
104+
});
105+
});

‎packages/react-native-babel-transformer/src/index.js‎

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,12 @@ const getBabelRC = (function () {
102102
// $FlowFixMe[incompatible-use] `extends` is missing in null or undefined.
103103
// $FlowFixMe[incompatible-type] `extends` is missing in null or undefined.
104104
if (!babelRC.extends) {
105-
const {experimentalImportSupport, ...presetOptions} = options;
105+
const {
106+
babelRuntimeModuleName,
107+
babelRuntimeVersion,
108+
experimentalImportSupport,
109+
...presetOptions
110+
} = options;
106111

107112
// $FlowFixMe[incompatible-use] `presets` is missing in null or undefined.
108113
babelRC.presets = [
@@ -112,7 +117,14 @@ const getBabelRC = (function () {
112117
projectRoot,
113118
...presetOptions,
114119
disableImportExportTransform: experimentalImportSupport,
115-
enableBabelRuntime: options.enableBabelRuntime,
120+
// A version is only safe to target alongside the module it
121+
// describes, so Metro's `@babel/runtime` is passed as a pair.
122+
...(babelRuntimeModuleName != null && babelRuntimeVersion != null
123+
? {
124+
babelRuntimeModuleName,
125+
enableBabelRuntime: babelRuntimeVersion,
126+
}
127+
: {enableBabelRuntime: options.enableBabelRuntime}),
116128
},
117129
],
118130
];
@@ -187,6 +199,8 @@ const transform /*: BabelTransformer['transform'] */ = ({
187199
src,
188200
plugins,
189201
}) => {
202+
// $FlowFixMe[prop-missing] Remove suppression after next Metro release
203+
const {babelRuntimeModuleName, babelRuntimeVersion} = options;
190204
const babelConfig /*: BabelCoreOptions */ = {
191205
// ES modules require sourceType='module' but OSS may not always want that
192206
sourceType: 'unambiguous',
@@ -201,6 +215,11 @@ const transform /*: BabelTransformer['transform'] */ = ({
201215
inlinePlatform: options.inlinePlatform,
202216
experimentalImportSupport: options.experimentalImportSupport,
203217
unstable_transformProfile: options.unstable_transformProfile,
218+
// Metro's own `@babel/runtime`, for a preset configured by a project
219+
// Babel config. The keys match the preset's own options.
220+
...(babelRuntimeModuleName != null && babelRuntimeVersion != null
221+
? {babelRuntimeModuleName, enableBabelRuntime: babelRuntimeVersion}
222+
: null),
204223
},
205224
ast: true,
206225

0 commit comments

Comments
 (0)