Skip to content
Closed
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,103 @@
/**
* 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 FILENAME = '/app/src/App.js';
const SRC = "import {Platform} from 'react-native';\nconst os = Platform.OS;";

type PresetOptions = {
platform?: ?string,
inlinePlatform?: boolean,
};

type CallerOptions = {
platform?: ?string,
inlinePlatform?: boolean,
};

function transform({
presetOptions = {},
caller = {},
}: {
presetOptions?: PresetOptions,
caller?: CallerOptions,
} = {}): string {
const result = babel.transformSync(SRC, {
babelrc: false,
caller: {name: 'test', ...caller},
compact: false,
configFile: false,
filename: FILENAME,
presets: [[preset, {dev: false, ...presetOptions}]],
sourceMaps: false,
});
const code = result?.code;
if (code == null) {
throw new Error('Expected the transform to produce code');
}
return code;
}

function isInlined(code: string): boolean {
return code.includes('"ios"') && !/\.OS\b/.test(code);
}

describe('Platform inlining is opt-in', () => {
test('does not inline when only a platform is given', () => {
// A platform on its own says which platform we are compiling *for*. It is
// also set by consumers that need platform-correct module resolution but
// must keep `Platform` observable at runtime - Jest mocks it.
expect(isInlined(transform({presetOptions: {platform: 'ios'}}))).toBe(
false,
);
});

test('does not inline when only the Babel caller gives a platform', () => {
expect(isInlined(transform({caller: {platform: 'ios'}}))).toBe(false);
});

test('inlines when opted in via preset options', () => {
expect(
isInlined(
transform({presetOptions: {platform: 'ios', inlinePlatform: true}}),
),
).toBe(true);
});

test('inlines when opted in via the Babel caller', () => {
// The only channel available when the preset is named in a babel.config.js,
// where Babel supplies no preset options.
expect(
isInlined(transform({caller: {platform: 'ios', inlinePlatform: true}})),
).toBe(true);
});

test('preset options take precedence over the caller', () => {
expect(
isInlined(
transform({
presetOptions: {inlinePlatform: false},
caller: {platform: 'ios', inlinePlatform: true},
}),
),
).toBe(false);
});

test('opting in without a platform is still a no-op', () => {
expect(isInlined(transform({presetOptions: {inlinePlatform: true}}))).toBe(
false,
);
});
});
21 changes: 14 additions & 7 deletions packages/react-native-babel-preset/src/configs/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,18 @@ function getTransformProfile(caller) {
return caller?.unstable_transformProfile ?? 'hermes-stable';
}

// The target platform for `Platform.OS` / `Platform.select` inlining. Metro
// passes this in transform options; when the preset is consumed directly as a
// Babel preset (no `options.platform`), fall back to the Babel caller so any
// Metro-driven consumer (bare Metro, Expo, @fb-tools/transformer) works without
// extra wiring. Reading it via `babel.caller` also makes Babel re-evaluate the
// preset when the platform changes between transform calls.
// The target platform, currently only used for platform inlining.
function getPlatform(caller) {
return caller?.platform ?? null;
}

// Boolean, whether to inline `Platform`. Separate from `platform` (string)
// because a platform already reaches the preset and may be used for other
// purposes.
function getInlinePlatform(caller) {
return caller?.inlinePlatform ?? false;
}

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

Expand All @@ -69,6 +71,9 @@ const getPreset = (src, options, babel) => {

const platform = options?.platform ?? babel?.caller(getPlatform);

const inlinePlatform =
options?.inlinePlatform ?? babel?.caller(getInlinePlatform) ?? false;

// Hermes V1 uses more optimised transform profiles. There is currently no
// difference between stable and canary, but canary may in future be used to
// test features in pre-prod Hermes V1 versions.
Expand Down Expand Up @@ -121,7 +126,9 @@ const getPreset = (src, options, babel) => {
// `disableImportExportTransform` is set), while the source-level import that
// proves provenance is still intact. It is a no-op when `platform` is null or
// the empty string.
extraPlugins.push([require('../inline-platform-plugin'), {platform}]);
if (inlinePlatform) {
extraPlugins.push([require('../inline-platform-plugin'), {platform}]);
}

if (!options.useTransformReactJSXExperimental) {
extraPlugins.push([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@ function transformToCode(
filename = path.join(PROJECT_ROOT, 'App.js'),
platform = 'ios',
experimentalImportSupport = false,
inlinePlatform = true,
}: {
filename?: string,
platform?: ?string,
experimentalImportSupport?: boolean,
inlinePlatform?: boolean,
} = {},
): string {
const {transform} = require('../index.js');
Expand All @@ -48,6 +50,7 @@ function transformToCode(
experimentalImportSupport,
globalPrefix: '__metro__',
hot: false,
inlinePlatform,
minify: false,
platform,
publicPath: 'test',
Expand Down Expand Up @@ -151,6 +154,19 @@ describe.each([false, true])(

expect(code).toMatch(/\.OS\b/);
});

test('does not inline without the inlinePlatform opt-in', () => {
// Metro sets this per build; consumers that only need platform-correct
// resolution (Jest) pass a platform without it and must keep `Platform`
// observable so it can be mocked.
const code = transformToCode(
"import {Platform} from 'react-native';\nconst os = Platform.OS;",
{inlinePlatform: false, experimentalImportSupport},
);

expect(code).toMatch(/\.OS\b/);
expect(code).not.toContain('"ios"');
});
},
);

Expand Down
2 changes: 2 additions & 0 deletions packages/react-native-babel-transformer/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,8 @@ const transform /*: BabelTransformer['transform'] */ = ({
name: 'metro',
bundler: 'metro',
platform: options.platform,
// $FlowFixMe[prop-missing] Remove suppression after next Metro release
inlinePlatform: options.inlinePlatform,
unstable_transformProfile: options.unstable_transformProfile,
},
ast: true,
Expand Down
Loading