Skip to content

Commit f63b2a1

Browse files
robhoganmeta-codesync[bot]
authored andcommitted
Babel preset: Make Platform inlining opt-in via inlinePlatform (default on via @react-native/metro-babel-transformer) (#57973)
Summary: Pull Request resolved: #57973 `react-native/babel-preset` inlines `Platform.OS` and `Platform.select(...)` whenever it is handed a `platform` (since yesterday - #57848 ), but this is over-zealous - `platform` is a pre-existing transform option whose intent is to set the transform target. In some cases (eg, to allow `Platform` mocking from tests), we don't want `Platform` inlining, even though `platform` may already be passed for other reasons. Gate the inlining behind a separate `inlinePlatform` opt-in, resolved as `options.inlinePlatform ?? babel.caller(...) ?? false`, mirroring how `platform` and `unstable_transformProfile` are already resolved. Metro already models this as a distinct transform option, so both Babel transformers pass it straight through. The caller channel covers the case where the preset is named in a `babel.config.js` and so receives no preset options at all. Bundling is unaffected: Metro sets `inlinePlatform` on every transform, so `Platform` continues to be inlined through `react-native/metro-babel-transformer`. Changelog: [General][Changed] - `Platform.OS` and `Platform.select(...)` inlining in `react-native/babel-preset` now requires the `inlinePlatform` option in addition to `platform` Reviewed By: GijsWeterings Differential Revision: D116281656 fbshipit-source-id: 0230cd74f38661862396391da62cf81b34773b85
1 parent 376b99f commit f63b2a1

4 files changed

Lines changed: 135 additions & 7 deletions

File tree

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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 FILENAME = '/app/src/App.js';
18+
const SRC = "import {Platform} from 'react-native';\nconst os = Platform.OS;";
19+
20+
type PresetOptions = {
21+
platform?: ?string,
22+
inlinePlatform?: boolean,
23+
};
24+
25+
type CallerOptions = {
26+
platform?: ?string,
27+
inlinePlatform?: boolean,
28+
};
29+
30+
function transform({
31+
presetOptions = {},
32+
caller = {},
33+
}: {
34+
presetOptions?: PresetOptions,
35+
caller?: CallerOptions,
36+
} = {}): string {
37+
const result = babel.transformSync(SRC, {
38+
babelrc: false,
39+
caller: {name: 'test', ...caller},
40+
compact: false,
41+
configFile: false,
42+
filename: FILENAME,
43+
presets: [[preset, {dev: false, ...presetOptions}]],
44+
sourceMaps: false,
45+
});
46+
const code = result?.code;
47+
if (code == null) {
48+
throw new Error('Expected the transform to produce code');
49+
}
50+
return code;
51+
}
52+
53+
function isInlined(code: string): boolean {
54+
return code.includes('"ios"') && !/\.OS\b/.test(code);
55+
}
56+
57+
describe('Platform inlining is opt-in', () => {
58+
test('does not inline when only a platform is given', () => {
59+
// A platform on its own says which platform we are compiling *for*. It is
60+
// also set by consumers that need platform-correct module resolution but
61+
// must keep `Platform` observable at runtime - Jest mocks it.
62+
expect(isInlined(transform({presetOptions: {platform: 'ios'}}))).toBe(
63+
false,
64+
);
65+
});
66+
67+
test('does not inline when only the Babel caller gives a platform', () => {
68+
expect(isInlined(transform({caller: {platform: 'ios'}}))).toBe(false);
69+
});
70+
71+
test('inlines when opted in via preset options', () => {
72+
expect(
73+
isInlined(
74+
transform({presetOptions: {platform: 'ios', inlinePlatform: true}}),
75+
),
76+
).toBe(true);
77+
});
78+
79+
test('inlines when opted in via the Babel caller', () => {
80+
// The only channel available when the preset is named in a babel.config.js,
81+
// where Babel supplies no preset options.
82+
expect(
83+
isInlined(transform({caller: {platform: 'ios', inlinePlatform: true}})),
84+
).toBe(true);
85+
});
86+
87+
test('preset options take precedence over the caller', () => {
88+
expect(
89+
isInlined(
90+
transform({
91+
presetOptions: {inlinePlatform: false},
92+
caller: {platform: 'ios', inlinePlatform: true},
93+
}),
94+
),
95+
).toBe(false);
96+
});
97+
98+
test('opting in without a platform is still a no-op', () => {
99+
expect(isInlined(transform({presetOptions: {inlinePlatform: true}}))).toBe(
100+
false,
101+
);
102+
});
103+
});

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

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,18 @@ function getTransformProfile(caller) {
4848
return caller?.unstable_transformProfile ?? 'hermes-stable';
4949
}
5050

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

56+
// Boolean, whether to inline `Platform`. Separate from `platform` (string)
57+
// because a platform already reaches the preset and may be used for other
58+
// purposes.
59+
function getInlinePlatform(caller) {
60+
return caller?.inlinePlatform ?? false;
61+
}
62+
6163
// use `this.foo = bar` instead of `this.defineProperty('foo', ...)`
6264
const loose = true;
6365

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

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

74+
const inlinePlatform =
75+
options?.inlinePlatform ?? babel?.caller(getInlinePlatform) ?? false;
76+
7277
// Hermes V1 uses more optimised transform profiles. There is currently no
7378
// difference between stable and canary, but canary may in future be used to
7479
// test features in pre-prod Hermes V1 versions.
@@ -121,7 +126,9 @@ const getPreset = (src, options, babel) => {
121126
// `disableImportExportTransform` is set), while the source-level import that
122127
// proves provenance is still intact. It is a no-op when `platform` is null or
123128
// the empty string.
124-
extraPlugins.push([require('../inline-platform-plugin'), {platform}]);
129+
if (inlinePlatform) {
130+
extraPlugins.push([require('../inline-platform-plugin'), {platform}]);
131+
}
125132

126133
if (!options.useTransformReactJSXExperimental) {
127134
extraPlugins.push([

packages/react-native-babel-transformer/src/__tests__/inline-platform-integration-test.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,12 @@ function transformToCode(
3030
filename = path.join(PROJECT_ROOT, 'App.js'),
3131
platform = 'ios',
3232
experimentalImportSupport = false,
33+
inlinePlatform = true,
3334
}: {
3435
filename?: string,
3536
platform?: ?string,
3637
experimentalImportSupport?: boolean,
38+
inlinePlatform?: boolean,
3739
} = {},
3840
): string {
3941
const {transform} = require('../index.js');
@@ -48,6 +50,7 @@ function transformToCode(
4850
experimentalImportSupport,
4951
globalPrefix: '__metro__',
5052
hot: false,
53+
inlinePlatform,
5154
minify: false,
5255
platform,
5356
publicPath: 'test',
@@ -151,6 +154,19 @@ describe.each([false, true])(
151154

152155
expect(code).toMatch(/\.OS\b/);
153156
});
157+
158+
test('does not inline without the inlinePlatform opt-in', () => {
159+
// Metro sets this per build; consumers that only need platform-correct
160+
// resolution (Jest) pass a platform without it and must keep `Platform`
161+
// observable so it can be mocked.
162+
const code = transformToCode(
163+
"import {Platform} from 'react-native';\nconst os = Platform.OS;",
164+
{inlinePlatform: false, experimentalImportSupport},
165+
);
166+
167+
expect(code).toMatch(/\.OS\b/);
168+
expect(code).not.toContain('"ios"');
169+
});
154170
},
155171
);
156172

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,8 @@ const transform /*: BabelTransformer['transform'] */ = ({
197197
name: 'metro',
198198
bundler: 'metro',
199199
platform: options.platform,
200+
// $FlowFixMe[prop-missing] Remove suppression after next Metro release
201+
inlinePlatform: options.inlinePlatform,
200202
unstable_transformProfile: options.unstable_transformProfile,
201203
},
202204
ast: true,

0 commit comments

Comments
 (0)