Skip to content

Commit 168a0d7

Browse files
cortinicometa-codesync[bot]
authored andcommitted
Allow configuring Babel runtime via caller in @react-native/babel-preset (#57974)
Summary: Pull Request resolved: #57974 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 fbshipit-source-id: c65928adc0886805fa56f944f7fff50a30ae268b
1 parent 2761132 commit 168a0d7

2 files changed

Lines changed: 173 additions & 8 deletions

File tree

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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+
// $FlowExpectedError[untyped-import] - Preset is untyped
14+
const preset = require('../index');
15+
const babel = require('@babel/core');
16+
17+
const MOCK_FILENAME = '/absolute/path/to/input.js';
18+
19+
function transformWithCaller(
20+
code: string,
21+
options: {[string]: unknown},
22+
caller: {
23+
enableBabelRuntime?: boolean | string,
24+
babelRuntimeModuleName?: string,
25+
},
26+
): string | null {
27+
const result = babel.transformSync(code, {
28+
babelrc: false,
29+
configFile: false,
30+
filename: MOCK_FILENAME,
31+
presets: [[preset, options]],
32+
caller: {...caller, name: 'test'},
33+
sourceMaps: false,
34+
compact: false,
35+
});
36+
return result?.code ?? null;
37+
}
38+
39+
describe('babel runtime caller options', () => {
40+
it('uses caller-provided babelRuntimeModuleName for runtime helpers', () => {
41+
const code = 'const {a, ...rest} = obj;';
42+
const result = transformWithCaller(
43+
code,
44+
{dev: false},
45+
{babelRuntimeModuleName: '@react-native/babel-runtime'},
46+
);
47+
expect(result).toContain(
48+
'@react-native/babel-runtime/helpers/objectWithoutProperties',
49+
);
50+
expect(result).not.toContain('@babel/runtime/helpers');
51+
});
52+
53+
it('options babelRuntimeModuleName takes precedence over caller', () => {
54+
const code = 'const {a, ...rest} = obj;';
55+
const result = transformWithCaller(
56+
code,
57+
{dev: false, babelRuntimeModuleName: '@from/options'},
58+
{babelRuntimeModuleName: '@from/caller'},
59+
);
60+
expect(result).toContain('@from/options/helpers/objectWithoutProperties');
61+
expect(result).not.toContain('@from/caller');
62+
});
63+
64+
it('uses caller-provided enableBabelRuntime version to select the helper interop', () => {
65+
// A default import triggers the `interopRequireDefault` helper, whose import
66+
// form depends on the runtime version. Modern versions append `.default`.
67+
const code = "import foo from './foo';\nfoo();";
68+
const result = transformWithCaller(
69+
code,
70+
{dev: false},
71+
{enableBabelRuntime: '7.25.0'},
72+
);
73+
expect(result).toContain(
74+
'require("@babel/runtime/helpers/interopRequireDefault").default',
75+
);
76+
});
77+
78+
it('options enableBabelRuntime takes precedence over caller', () => {
79+
const code = "import foo from './foo';\nfoo();";
80+
const result = transformWithCaller(
81+
code,
82+
{dev: false, enableBabelRuntime: '7.0.0'},
83+
{enableBabelRuntime: '7.25.0'},
84+
);
85+
// Options version (7.0.0) uses the legacy interop form without `.default`,
86+
// overriding the caller version (7.25.0) that would produce `.default`.
87+
expect(result).toContain(
88+
'require("@babel/runtime/helpers/interopRequireDefault")',
89+
);
90+
expect(result).not.toContain(
91+
'require("@babel/runtime/helpers/interopRequireDefault").default',
92+
);
93+
});
94+
95+
it('has no effect when caller options are absent', () => {
96+
const code = "import foo from './foo';\nfoo();";
97+
const result = transformWithCaller(code, {dev: false}, {});
98+
// Default runtime version uses the legacy interop form (no `.default`).
99+
expect(result).toContain(
100+
'require("@babel/runtime/helpers/interopRequireDefault")',
101+
);
102+
expect(result).not.toContain(
103+
'require("@babel/runtime/helpers/interopRequireDefault").default',
104+
);
105+
});
106+
107+
it('does not add the runtime plugin when caller disables enableBabelRuntime', () => {
108+
const code = 'const {a, ...rest} = obj;';
109+
const result = transformWithCaller(
110+
code,
111+
{dev: false},
112+
{
113+
enableBabelRuntime: false,
114+
babelRuntimeModuleName: '@react-native/babel-runtime',
115+
},
116+
);
117+
expect(result).not.toContain('@react-native/babel-runtime');
118+
expect(result).not.toContain('@babel/runtime/helpers');
119+
});
120+
121+
it('options enableBabelRuntime false takes precedence over caller true', () => {
122+
const code = 'const {a, ...rest} = obj;';
123+
const result = transformWithCaller(
124+
code,
125+
{dev: false, enableBabelRuntime: false},
126+
{enableBabelRuntime: '7.25.0'},
127+
);
128+
expect(result).not.toContain('@babel/runtime/helpers');
129+
});
130+
});

‎packages/react-native-babel-preset/src/configs/main.js‎

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,16 @@ function isFirstParty(fileName) {
4040
);
4141
}
4242

43-
// Called by Babel whenever caller information changes between transform calls
44-
// for a given config. If the return value changes, Babel re-evaluates
45-
// getPreset, which is otherwise cached based on `options`. This must be pure,
46-
// and should be cheap.
43+
/**
44+
* BEGIN BABEL CALLER GETTERS
45+
* Called by Babel whenever caller information changes between transform calls
46+
* for a given config. If the return value changes, Babel re-evaluates
47+
* getPreset, which is otherwise cached based on `options`.
48+
*
49+
* These functions must be pure and should be cheap.
50+
**/
51+
52+
// Target transform profile, influencing which plugins are active
4753
function getTransformProfile(caller) {
4854
return caller?.unstable_transformProfile ?? 'hermes-stable';
4955
}
@@ -66,6 +72,20 @@ function getExperimentalImportSupport(caller) {
6672
return caller?.experimentalImportSupport ?? false;
6773
}
6874

75+
// A boolean to toggle @babel/plugin-transform-runtime, or a string to pin
76+
// a specific @babel/runtime version.
77+
function getEnableBabelRuntime(caller) {
78+
return caller?.enableBabelRuntime;
79+
}
80+
81+
// Reads the caller-provided @babel/runtime module name that helpers are imported
82+
// from.
83+
function getBabelRuntimeModuleName(caller) {
84+
return caller?.babelRuntimeModuleName;
85+
}
86+
87+
/* END BABEL CALLER GETTERS */
88+
6989
// use `this.foo = bar` instead of `this.defineProperty('foo', ...)`
7090
const loose = true;
7191

@@ -248,16 +268,31 @@ const getPreset = (src, options, babel) => {
248268
]);
249269
}
250270

251-
if (options.enableBabelRuntime !== false) {
252-
// Allows configuring a specific runtime version to optimize output
253-
const isVersion = typeof options.enableBabelRuntime === 'string';
271+
// `enableBabelRuntime` (a boolean toggle or a string @babel/runtime version to
272+
// pin) and `babelRuntimeModuleName` (the module helpers are imported from) may
273+
// be provided via options or, for programmatic callers such as Metro, via
274+
// Babel caller data. Options take precedence over caller data, consistent with
275+
// the other options resolved here.
276+
const enableBabelRuntime =
277+
options.enableBabelRuntime ?? babel?.caller(getEnableBabelRuntime) ?? true;
278+
279+
if (enableBabelRuntime !== false) {
280+
// A string value pins a specific runtime version to optimize output.
281+
const isVersion = typeof enableBabelRuntime === 'string';
282+
283+
const babelRuntimeModuleName =
284+
options.babelRuntimeModuleName ??
285+
babel?.caller(getBabelRuntimeModuleName);
254286

255287
extraPlugins.push([
256288
require('@babel/plugin-transform-runtime'),
257289
{
258290
helpers: true,
259291
regenerator: enableRegenerator,
260-
...(isVersion && {version: options.enableBabelRuntime}),
292+
...(isVersion && {version: enableBabelRuntime}),
293+
...(babelRuntimeModuleName != null && {
294+
moduleName: babelRuntimeModuleName,
295+
}),
261296
},
262297
]);
263298
} else if (enableRegenerator) {

0 commit comments

Comments
 (0)