Skip to content

Commit 7c3dca6

Browse files
robhoganfacebook-github-bot
authored andcommitted
Allow configuring Babel runtime via caller in @react-native/babel-preset
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
1 parent 376b99f commit 7c3dca6

2 files changed

Lines changed: 164 additions & 4 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: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,21 @@ function getPlatform(caller) {
5858
return caller?.platform ?? null;
5959
}
6060

61+
// Reads the caller-provided `enableBabelRuntime` value: a boolean to toggle
62+
// @babel/plugin-transform-runtime, or a string to pin a specific
63+
// @babel/runtime version. Allows callers such as Metro to configure the runtime
64+
// programmatically. As with getTransformProfile, the return value is used by
65+
// Babel for cache invalidation, so this must be pure and cheap.
66+
function getEnableBabelRuntime(caller) {
67+
return caller?.enableBabelRuntime;
68+
}
69+
70+
// Reads the caller-provided @babel/runtime module name that helpers are imported
71+
// from. As above, this must be pure and cheap.
72+
function getBabelRuntimeModuleName(caller) {
73+
return caller?.babelRuntimeModuleName;
74+
}
75+
6176
// use `this.foo = bar` instead of `this.defineProperty('foo', ...)`
6277
const loose = true;
6378

@@ -223,16 +238,31 @@ const getPreset = (src, options, babel) => {
223238
]);
224239
}
225240

226-
if (!options || options.enableBabelRuntime !== false) {
227-
// Allows configuring a specific runtime version to optimize output
228-
const isVersion = typeof options?.enableBabelRuntime === 'string';
241+
// `enableBabelRuntime` (a boolean toggle or a string @babel/runtime version to
242+
// pin) and `babelRuntimeModuleName` (the module helpers are imported from) may
243+
// be provided via options or, for programmatic callers such as Metro, via
244+
// Babel caller data. Options take precedence over caller data, consistent with
245+
// the other options resolved here.
246+
const enableBabelRuntime =
247+
options?.enableBabelRuntime ?? babel?.caller(getEnableBabelRuntime) ?? true;
248+
249+
if (enableBabelRuntime !== false) {
250+
// A string value pins a specific runtime version to optimize output.
251+
const isVersion = typeof enableBabelRuntime === 'string';
252+
253+
const babelRuntimeModuleName =
254+
options?.babelRuntimeModuleName ??
255+
babel?.caller(getBabelRuntimeModuleName);
229256

230257
extraPlugins.push([
231258
require('@babel/plugin-transform-runtime'),
232259
{
233260
helpers: true,
234261
regenerator: enableRegenerator,
235-
...(isVersion && {version: options.enableBabelRuntime}),
262+
...(isVersion && {version: enableBabelRuntime}),
263+
...(babelRuntimeModuleName != null && {
264+
moduleName: babelRuntimeModuleName,
265+
}),
236266
},
237267
]);
238268
} else if (enableRegenerator) {

0 commit comments

Comments
 (0)