Skip to content

Inline Platform within RN’s Babel preset (#57848) - #57848

Closed
robhogan wants to merge 1 commit into
mainfrom
export-D114647943
Closed

Inline Platform within RN’s Babel preset (#57848)#57848
robhogan wants to merge 1 commit into
mainfrom
export-D114647943

Conversation

@robhogan

@robhogan robhogan commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary:

Adds a React Native-owned Babel plugin that inlines Platform.OS and
Platform.select(...), from
react-native/babel-preset at the front of the preset's plugin list (importantly, before
the preset lowers ES modules to CommonJS).

Metro already inlines Platform, but its matcher (metro-transform-plugins'
inline-plugin via createInlinePlatformChecks) runs after ESM has been
lowered to CommonJS. By then a genuine import {Platform} from 'react-native'
has become _reactNative.Platform.OS and provenance is obscured and fragile to track. Metro compensates by matching on the local
identifier spelling - so Platform.OS inlines whether or not Platform is
actually React Native's, and a genuine deep or RN-relative import may be missed entirely.

Platform inlining is in any case much more of an RN concern than a Metro one - it has to be aware of RN APIs and module names, and even the module’s path.

This plugin recognises:

  • import {Platform} from 'react-native' (including aliased imports)
  • import * as RN from 'react-native', uses of RN.Platform
  • import P from 'react-native/Libraries/Utilities/Platform'
  • the equivalent require forms, plus destructuring and immutable aliases
  • RN's internal relative imports, e.g. ../../Utilities/Platform

…and deliberately does not recognise anything it cannot prove - a bare
Platform.OS global, React.Platform.OS, or a same-named import from another
package.

Metro's late matcher is left fully enabled and remains responsible for
those historical forms, so this change is purely additive: it inlines genuine
imports Metro was missing, and changes nothing Metro already handled. Metro’s plugin will be deprecated and removed in future.

Notable details:

  • Relative RN imports are resolved lexically, with no filesystem access and no
    platform-extension resolution — the identity we need is the extension-less
    module <rn-root>/Libraries/Utilities/Platform, and Metro picks
    Platform.ios.js / Platform.android.js later. The RN package root is
    identified by directory name, which covers node_modules/react-native,
    packages/react-native and pnpm layouts alike, and rejects
    react-native-web and friends (who would be expected to adjust the paths in their forks)
  • Imports are left in place. Removing them would change dependency collection,
    so it is handled separately.
  • A no-platform build (null, or the empty string that RN's Jest preprocessor
    passes) is a no-op — inlining Platform.OS to "" would be wrong.
  • The plugin source is added to the preset's getCacheKey so edits invalidate Metro's transform cache.

Changelog:
[General][Changed] - Inline Platform.OS and Platform.select(...) for
React Native Platform imports during the Babel preset, covering some cases that were previously left un-inlined.

Reviewed By: huntie

Differential Revision: D114647943

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 6, 2026
@meta-codesync

meta-codesync Bot commented Aug 6, 2026

Copy link
Copy Markdown

@robhogan has exported this pull request. If you are a Meta employee, you can view the originating Diff in D114647943.

@retyui

retyui commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

@robhogan I guess my PR to Metro doesn't make sense at all react/metro#1732 , right ?

// `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}]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe you should avoid adding this plugin in tests (Jest) environment

As devs need to be able to mock Platform.OS value

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks - I hadn't considered that and worth checking, but I think we're safe. The inline plugin is a no-op unless caller.platform is a non-empty string - i.e, it only inlines if platform is explicitly set at transform time.

The RN jest preset does not set platform - it uses babel-jest with no overrides

transform: {
'^.+\\.(js|ts|tsx)$': 'babel-jest',

That picks up Babel configs, but we don't encourage setting platform there either, and the template doesn't:

https://github.com/react-native-community/template/blob/ed3802bac7e7af9571c984d912b54262d7ce49c5/template/babel.config.js

expo-jest does set a platform, but Expo use their own preset entirely (I've added a line exporting this plugin so they can reuse it)

@robhogan

robhogan commented Aug 15, 2026

Copy link
Copy Markdown
Contributor Author

@robhogan I guess my PR to Metro doesn't make sense at all react/metro#1732 , right ?

This is intended to replace Metro's plugin, but all credit to you for identifying the problem here, it was reviewing that PR that led me to conclude that inlining living where it does/did in Metro is just unsalvageable. There are no good options that late in the transform pipeline (dealing with experimentalImportSupport, Expo ESM and Babel ESM after the fact, correctly, is hard, arguably impossible without graph context). Dealing with relative paths ends up either coupling too tightly to RN and being fragile, or leaving too many potential false positives.

Here, we can safely couple to RN, and OOT forks can modify their fork of the plugin if they need.

Summary:

Adds a React Native-owned Babel plugin that inlines `Platform.OS` and
`Platform.select(...)`, from
`react-native/babel-preset`  at the front of the preset's plugin list (importantly, before
the preset lowers ES modules to CommonJS).

Metro already inlines `Platform`, but its matcher (`metro-transform-plugins`'
`inline-plugin` via `createInlinePlatformChecks`) runs *after* ESM has been
lowered to CommonJS. By then a genuine `import {Platform} from 'react-native'`
has become `_reactNative.Platform.OS` and provenance is obscured and fragile to track. Metro compensates by matching on the local
identifier spelling - so `Platform.OS` inlines whether or not `Platform` is
actually React Native's, and a genuine deep or RN-relative import may be missed entirely.

`Platform` inlining is in any case much more of an RN concern than a Metro one - it has to be aware of RN APIs and module names, and even the module’s path.

This plugin recognises:

- `import {Platform} from 'react-native'` (including aliased imports)
- `import * as RN from 'react-native'`, uses of `RN.Platform`
- `import P from 'react-native/Libraries/Utilities/Platform'`
- the equivalent `require` forms, plus destructuring and immutable aliases
- RN's internal relative imports, e.g. `../../Utilities/Platform`

…and deliberately does *not* recognise anything it cannot prove - a bare
`Platform.OS` global, `React.Platform.OS`, or a same-named import from another
package. 

Metro's late matcher is left fully enabled and remains responsible for
those historical forms, so this change is purely additive: it inlines genuine
imports Metro was missing, and changes nothing Metro already handled. Metro’s plugin will be deprecated and removed in future.

Notable details:

- Relative RN imports are resolved lexically, with no filesystem access and no
  platform-extension resolution — the identity we need is the extension-less
  module `<rn-root>/Libraries/Utilities/Platform`, and Metro picks
  `Platform.ios.js` / `Platform.android.js` later. The RN package root is
  identified by directory name, which covers `node_modules/react-native`,
  `packages/react-native` and pnpm layouts alike, and rejects
  `react-native-web` and friends (who would be expected to adjust the paths in their forks)
- Imports are left in place. Removing them would change dependency collection,
  so it is handled separately.
- A no-platform build (`null`, or the empty string that RN's Jest preprocessor
  passes) is a no-op — inlining `Platform.OS` to `""` would be wrong.
- The plugin source is added to the preset's `getCacheKey` so edits invalidate Metro's transform cache.

Changelog:
[General][Changed] - Inline `Platform.OS` and `Platform.select(...)` for
React Native `Platform` imports during the Babel preset, covering some cases that were previously left un-inlined.

Reviewed By: huntie

Differential Revision: D114647943
@meta-codesync meta-codesync Bot changed the title Inline Platform within RN’s Babel preset Inline Platform within RN’s Babel preset (#57848) Aug 15, 2026
@meta-codesync
meta-codesync Bot force-pushed the export-D114647943 branch from 2dc5e3a to a802f54 Compare August 15, 2026 13:15
@meta-codesync meta-codesync Bot closed this in 40c0612 Aug 16, 2026
@meta-codesync meta-codesync Bot added the Merged This PR has been merged. label Aug 16, 2026
@meta-codesync

meta-codesync Bot commented Aug 16, 2026

Copy link
Copy Markdown

This pull request has been merged in 40c0612.

meta-codesync Bot pushed a commit that referenced this pull request Aug 17, 2026
…lt on via `@react-native/metro-babel-transformer`) (#57973)

Summary:

`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
meta-codesync Bot pushed a commit to react/metro that referenced this pull request Aug 17, 2026
…lt on via `@react-native/metro-babel-transformer`)

Summary:
X-link: react/react-native#57973

`react-native/babel-preset` inlines `Platform.OS` and `Platform.select(...)` whenever it is handed a `platform` (since yesterday - react/react-native#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
meta-codesync Bot pushed a commit that referenced this pull request Aug 17, 2026
…lt 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Merged This PR has been merged. meta-exported p: Facebook Partner: Facebook Partner

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants