Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/**
* 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.
*
* @flow strict-local
* @format
*/

'use strict';

// $FlowExpectedError[untyped-import] - Preset is untyped
const preset = require('../index');
const babel = require('@babel/core');

const MOCK_FILENAME = '/absolute/path/to/input.js';

function transformWithCaller(
code: string,
options: {[string]: unknown},
caller: {
enableBabelRuntime?: boolean | string,
babelRuntimeModuleName?: string,
},
): string | null {
const result = babel.transformSync(code, {
babelrc: false,
configFile: false,
filename: MOCK_FILENAME,
presets: [[preset, options]],
caller: {...caller, name: 'test'},
sourceMaps: false,
compact: false,
});
return result?.code ?? null;
}

describe('babel runtime caller options', () => {
it('uses caller-provided babelRuntimeModuleName for runtime helpers', () => {
const code = 'const {a, ...rest} = obj;';
const result = transformWithCaller(
code,
{dev: false},
{babelRuntimeModuleName: '@react-native/babel-runtime'},
);
expect(result).toContain(
'@react-native/babel-runtime/helpers/objectWithoutProperties',
);
expect(result).not.toContain('@babel/runtime/helpers');
});

it('options babelRuntimeModuleName takes precedence over caller', () => {
const code = 'const {a, ...rest} = obj;';
const result = transformWithCaller(
code,
{dev: false, babelRuntimeModuleName: '@from/options'},
{babelRuntimeModuleName: '@from/caller'},
);
expect(result).toContain('@from/options/helpers/objectWithoutProperties');
expect(result).not.toContain('@from/caller');
});

it('uses caller-provided enableBabelRuntime version to select the helper interop', () => {
// A default import triggers the `interopRequireDefault` helper, whose import
// form depends on the runtime version. Modern versions append `.default`.
const code = "import foo from './foo';\nfoo();";
const result = transformWithCaller(
code,
{dev: false},
{enableBabelRuntime: '7.25.0'},
);
expect(result).toContain(
'require("@babel/runtime/helpers/interopRequireDefault").default',
);
});

it('options enableBabelRuntime takes precedence over caller', () => {
const code = "import foo from './foo';\nfoo();";
const result = transformWithCaller(
code,
{dev: false, enableBabelRuntime: '7.0.0'},
{enableBabelRuntime: '7.25.0'},
);
// Options version (7.0.0) uses the legacy interop form without `.default`,
// overriding the caller version (7.25.0) that would produce `.default`.
expect(result).toContain(
'require("@babel/runtime/helpers/interopRequireDefault")',
);
expect(result).not.toContain(
'require("@babel/runtime/helpers/interopRequireDefault").default',
);
});

it('has no effect when caller options are absent', () => {
const code = "import foo from './foo';\nfoo();";
const result = transformWithCaller(code, {dev: false}, {});
// Default runtime version uses the legacy interop form (no `.default`).
expect(result).toContain(
'require("@babel/runtime/helpers/interopRequireDefault")',
);
expect(result).not.toContain(
'require("@babel/runtime/helpers/interopRequireDefault").default',
);
});

it('does not add the runtime plugin when caller disables enableBabelRuntime', () => {
const code = 'const {a, ...rest} = obj;';
const result = transformWithCaller(
code,
{dev: false},
{
enableBabelRuntime: false,
babelRuntimeModuleName: '@react-native/babel-runtime',
},
);
expect(result).not.toContain('@react-native/babel-runtime');
expect(result).not.toContain('@babel/runtime/helpers');
});

it('options enableBabelRuntime false takes precedence over caller true', () => {
const code = 'const {a, ...rest} = obj;';
const result = transformWithCaller(
code,
{dev: false, enableBabelRuntime: false},
{enableBabelRuntime: '7.25.0'},
);
expect(result).not.toContain('@babel/runtime/helpers');
});
});
51 changes: 43 additions & 8 deletions packages/react-native-babel-preset/src/configs/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,16 @@ function isFirstParty(fileName) {
);
}

// Called by Babel whenever caller information changes between transform calls
// for a given config. If the return value changes, Babel re-evaluates
// getPreset, which is otherwise cached based on `options`. This must be pure,
// and should be cheap.
/**
* BEGIN BABEL CALLER GETTERS
* Called by Babel whenever caller information changes between transform calls
* for a given config. If the return value changes, Babel re-evaluates
* getPreset, which is otherwise cached based on `options`.
*
* These functions must be pure and should be cheap.
**/

// Target transform profile, influencing which plugins are active
function getTransformProfile(caller) {
return caller?.unstable_transformProfile ?? 'hermes-stable';
}
Expand All @@ -60,6 +66,20 @@ function getInlinePlatform(caller) {
return caller?.inlinePlatform ?? false;
}

// A boolean to toggle @babel/plugin-transform-runtime, or a string to pin
// a specific @babel/runtime version.
function getEnableBabelRuntime(caller) {
return caller?.enableBabelRuntime;
}

// Reads the caller-provided @babel/runtime module name that helpers are imported
// from.
function getBabelRuntimeModuleName(caller) {
return caller?.babelRuntimeModuleName;
}

/* END BABEL CALLER GETTERS */

// use `this.foo = bar` instead of `this.defineProperty('foo', ...)`
const loose = true;

Expand Down Expand Up @@ -230,16 +250,31 @@ const getPreset = (src, options, babel) => {
]);
}

if (!options || options.enableBabelRuntime !== false) {
// Allows configuring a specific runtime version to optimize output
const isVersion = typeof options?.enableBabelRuntime === 'string';
// `enableBabelRuntime` (a boolean toggle or a string @babel/runtime version to
// pin) and `babelRuntimeModuleName` (the module helpers are imported from) may
// be provided via options or, for programmatic callers such as Metro, via
// Babel caller data. Options take precedence over caller data, consistent with
// the other options resolved here.
const enableBabelRuntime =
options?.enableBabelRuntime ?? babel?.caller(getEnableBabelRuntime) ?? true;

if (enableBabelRuntime !== false) {
// A string value pins a specific runtime version to optimize output.
const isVersion = typeof enableBabelRuntime === 'string';

const babelRuntimeModuleName =
options?.babelRuntimeModuleName ??
babel?.caller(getBabelRuntimeModuleName);

extraPlugins.push([
require('@babel/plugin-transform-runtime'),
{
helpers: true,
regenerator: enableRegenerator,
...(isVersion && {version: options.enableBabelRuntime}),
...(isVersion && {version: enableBabelRuntime}),
...(babelRuntimeModuleName != null && {
moduleName: babelRuntimeModuleName,
}),
},
]);
} else if (enableRegenerator) {
Expand Down
Loading