first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
/**
* 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.
*
* @format
*/
import {NativeEventSubscription} from '../EventEmitter/RCTNativeAppEventEmitter';
type ColorSchemeName = 'light' | 'dark' | 'unspecified';
export namespace Appearance {
type AppearancePreferences = {
colorScheme: ColorSchemeName;
};
type AppearanceListener = (preferences: AppearancePreferences) => void;
/**
* Note: Although color scheme is available immediately, it may change at any
* time. Any rendering logic or styles that depend on this should try to call
* this function on every render, rather than caching the value (for example,
* using inline styles rather than setting a value in a `StyleSheet`).
*
* Example: `const colorScheme = Appearance.getColorScheme();`
*/
export function getColorScheme(): ColorSchemeName | null | undefined;
/**
* Set the color scheme preference. This is useful for overriding the default
* color scheme preference for the app. Note that this will not change the
* appearance of the system UI, only the appearance of the app.
* Only available on iOS 13+ and Android 10+.
*/
export function setColorScheme(scheme: ColorSchemeName): void;
/**
* Add an event handler that is fired when appearance preferences change.
*/
export function addChangeListener(
listener: AppearanceListener,
): NativeEventSubscription;
}
/**
* A new useColorScheme hook is provided as the preferred way of accessing
* the user's preferred color scheme (e.g. Dark Mode).
*/
export function useColorScheme(): ColorSchemeName;

View File

@@ -0,0 +1,115 @@
/**
* 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
*/
import type {EventSubscription} from '../vendor/emitter/EventEmitter';
import type {AppearancePreferences, ColorSchemeName} from './NativeAppearance';
import typeof INativeAppearance from './NativeAppearance';
import NativeEventEmitter from '../EventEmitter/NativeEventEmitter';
import EventEmitter from '../vendor/emitter/EventEmitter';
export type {AppearancePreferences};
type Appearance = {
colorScheme: ?ColorSchemeName,
};
let lazyState: ?{
+NativeAppearance: INativeAppearance,
// Cache the color scheme to reduce the cost of reading it between changes.
// NOTE: If `NativeAppearance` is null, this will always be null.
appearance: ?Appearance,
// NOTE: This is non-nullable to make it easier for `onChangedListener` to
// return a non-nullable `EventSubscription` value. This is not the common
// path, so we do not have to over-optimize it.
+eventEmitter: EventEmitter<{change: [Appearance]}>,
};
/**
* Ensures that all state and listeners are lazily initialized correctly.
*/
function getState(): $NonMaybeType<typeof lazyState> {
if (lazyState != null) {
return lazyState;
}
const eventEmitter = new EventEmitter<{change: [Appearance]}>();
// NOTE: Avoid initializing `NativeAppearance` until it is actually used.
const NativeAppearance = require('./NativeAppearance').default;
if (NativeAppearance == null) {
// Assign `null` to avoid re-initializing on subsequent invocations.
lazyState = {
NativeAppearance: null,
appearance: null,
eventEmitter,
};
} else {
const state: $NonMaybeType<typeof lazyState> = {
NativeAppearance,
appearance: null,
eventEmitter,
};
new NativeEventEmitter<{
appearanceChanged: [AppearancePreferences],
}>(NativeAppearance).addListener('appearanceChanged', newAppearance => {
state.appearance = {
colorScheme: newAppearance.colorScheme,
};
eventEmitter.emit('change', state.appearance);
});
lazyState = state;
}
return lazyState;
}
/**
* Returns the current color scheme preference. This value may change, so the
* value should not be cached without either listening to changes or using
* the `useColorScheme` hook.
*/
export function getColorScheme(): ?ColorSchemeName {
let colorScheme = null;
const state = getState();
const {NativeAppearance} = state;
if (NativeAppearance != null) {
if (state.appearance == null) {
// Lazily initialize `state.appearance`. This should only
// happen once because we never reassign a null value to it.
state.appearance = {
colorScheme: NativeAppearance.getColorScheme(),
};
}
colorScheme = state.appearance.colorScheme;
}
return colorScheme;
}
/**
* Updates the current color scheme to the supplied value.
*/
export function setColorScheme(colorScheme: ColorSchemeName): void {
const state = getState();
const {NativeAppearance} = state;
if (NativeAppearance != null) {
NativeAppearance.setColorScheme(colorScheme);
state.appearance = {
colorScheme,
};
}
}
/**
* Add an event handler that is fired when appearance preferences change.
*/
export function addChangeListener(
listener: ({colorScheme: ?ColorSchemeName}) => void,
): EventSubscription {
const {eventEmitter} = getState();
return eventEmitter.addListener('change', listener);
}

View File

@@ -0,0 +1,96 @@
/**
* 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
*/
import NativeDeviceEventManager from '../../Libraries/NativeModules/specs/NativeDeviceEventManager';
import RCTDeviceEventEmitter from '../EventEmitter/RCTDeviceEventEmitter';
const DEVICE_BACK_EVENT = 'hardwareBackPress';
type BackPressEventName = 'backPress' | 'hardwareBackPress';
type BackPressHandler = () => ?boolean;
const _backPressSubscriptions: Array<BackPressHandler> = [];
RCTDeviceEventEmitter.addListener(DEVICE_BACK_EVENT, function () {
for (let i = _backPressSubscriptions.length - 1; i >= 0; i--) {
if (_backPressSubscriptions[i]?.()) {
return;
}
}
BackHandler.exitApp();
});
/**
* Detect hardware button presses for back navigation.
*
* Android: Detect hardware back button presses, and programmatically invoke the default back button
* functionality to exit the app if there are no listeners or if none of the listeners return true.
*
* iOS: Not applicable.
*
* The event subscriptions are called in reverse order (i.e. last registered subscription first),
* and if one subscription returns true then subscriptions registered earlier will not be called.
*
* Example:
*
* ```javascript
* BackHandler.addEventListener('hardwareBackPress', function() {
* // this.onMainScreen and this.goBack are just examples, you need to use your own implementation here
* // Typically you would use the navigator here to go to the last state.
*
* if (!this.onMainScreen()) {
* this.goBack();
* return true;
* }
* return false;
* });
* ```
*/
type TBackHandler = {
+exitApp: () => void,
+addEventListener: (
eventName: BackPressEventName,
handler: BackPressHandler,
) => {remove: () => void, ...},
};
const BackHandler: TBackHandler = {
exitApp: function (): void {
if (!NativeDeviceEventManager) {
return;
}
NativeDeviceEventManager.invokeDefaultBackPressHandler();
},
/**
* Adds an event handler. Supported events:
*
* - `hardwareBackPress`: Fires when the Android hardware back button is pressed.
*/
addEventListener: function (
eventName: BackPressEventName,
handler: BackPressHandler,
): {remove: () => void, ...} {
if (_backPressSubscriptions.indexOf(handler) === -1) {
_backPressSubscriptions.push(handler);
}
return {
remove: (): void => {
const index = _backPressSubscriptions.indexOf(handler);
if (index !== -1) {
_backPressSubscriptions.splice(index, 1);
}
},
};
},
};
export default BackHandler;

View File

@@ -0,0 +1,34 @@
/**
* 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.
*
* @format
*/
import {NativeEventSubscription} from '../EventEmitter/RCTNativeAppEventEmitter';
export type BackPressEventName = 'hardwareBackPress';
/**
* Detect hardware back button presses, and programmatically invoke the
* default back button functionality to exit the app if there are no
* listeners or if none of the listeners return true.
* The event subscriptions are called in reverse order
* (i.e. last registered subscription first), and if one subscription
* returns true then subscriptions registered earlier
* will not be called.
*
* @see https://reactnative.dev/docs/backhandler
*/
export interface BackHandlerStatic {
exitApp(): void;
addEventListener(
eventName: BackPressEventName,
handler: () => boolean | null | undefined,
): NativeEventSubscription;
}
export const BackHandler: BackHandlerStatic;
export type BackHandler = BackHandlerStatic;

View File

@@ -0,0 +1,33 @@
/**
* 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
*/
type BackPressEventName = 'backPress' | 'hardwareBackPress';
type BackPressHandler = () => ?boolean;
function emptyFunction(): void {}
type TBackHandler = {
exitApp(): void,
addEventListener(
eventName: BackPressEventName,
handler: BackPressHandler,
): {remove: () => void, ...},
};
const BackHandler: TBackHandler = {
exitApp: emptyFunction,
addEventListener(_eventName: BackPressEventName, _handler: BackPressHandler) {
return {
remove: emptyFunction,
};
},
};
export default BackHandler;

View File

@@ -0,0 +1,17 @@
/**
* 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
*/
// NOTE: This file supports backwards compatibility of subpath (deep) imports
// from 'react-native' with platform-specific extensions. It can be deleted
// once we remove the "./*" mapping from package.json "exports".
import BackHandler from './BackHandler';
export default BackHandler;

View File

@@ -0,0 +1,25 @@
/**
* 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
* @format
*/
'use strict';
export type BackPressEventName = 'backPress' | 'hardwareBackPress';
type TBackHandler = {
exitApp(): void,
addEventListener(
eventName: BackPressEventName,
handler: () => ?boolean,
): {remove: () => void, ...},
};
declare const BackHandler: TBackHandler;
declare export default typeof BackHandler;

View File

@@ -0,0 +1,72 @@
/**
* 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
*/
import processColor from '../StyleSheet/processColor';
import {getColorScheme} from './Appearance';
import NativeDevLoadingView from './NativeDevLoadingView';
const COLOR_SCHEME = {
dark: {
load: {
backgroundColor: '#fafafa',
textColor: '#242526',
},
refresh: {
backgroundColor: '#2584e8',
textColor: '#ffffff',
},
error: {
backgroundColor: '#1065AF',
textColor: '#ffffff',
},
},
default: {
load: {
backgroundColor: '#404040',
textColor: '#ffffff',
},
refresh: {
backgroundColor: '#2584e8',
textColor: '#ffffff',
},
error: {
backgroundColor: '#1065AF',
textColor: '#ffffff',
},
},
};
export default {
showMessage(message: string, type: 'load' | 'refresh' | 'error') {
if (NativeDevLoadingView) {
const colorScheme =
getColorScheme() === 'dark' ? COLOR_SCHEME.dark : COLOR_SCHEME.default;
const colorSet = colorScheme[type];
let backgroundColor;
let textColor;
if (colorSet) {
backgroundColor = processColor(colorSet.backgroundColor);
textColor = processColor(colorSet.textColor);
}
NativeDevLoadingView.showMessage(
message,
typeof textColor === 'number' ? textColor : null,
typeof backgroundColor === 'number' ? backgroundColor : null,
);
}
},
hide() {
NativeDevLoadingView && NativeDevLoadingView.hide();
},
};

View File

@@ -0,0 +1,32 @@
/**
* 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.
*
* @format
*/
import {NativeEventEmitter} from '../EventEmitter/NativeEventEmitter';
/**
* The DevSettings module exposes methods for customizing settings for developers in development.
*/
export interface DevSettingsStatic extends NativeEventEmitter {
/**
* Adds a custom menu item to the developer menu.
*
* @param title - The title of the menu item. Is internally used as id and should therefore be unique.
* @param handler - The callback invoked when pressing the menu item.
*/
addMenuItem(title: string, handler: () => any): void;
/**
* Reload the application.
*
* @param reason
*/
reload(reason?: string): void;
}
export const DevSettings: DevSettingsStatic;

View File

@@ -0,0 +1,86 @@
/**
* 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
*/
import type {EventSubscription} from '../vendor/emitter/EventEmitter';
import NativeEventEmitter from '../EventEmitter/NativeEventEmitter';
import NativeDevSettings from '../NativeModules/specs/NativeDevSettings';
import Platform from '../Utilities/Platform';
/**
* The DevSettings module exposes methods for customizing settings for developers in development.
*/
let DevSettings: {
/**
* Adds a custom menu item to the developer menu.
*
* @param title - The title of the menu item. Is internally used as id and should therefore be unique.
* @param handler - The callback invoked when pressing the menu item.
*/
addMenuItem(title: string, handler: () => mixed): void,
/**
* Reload the application.
*
* @param reason
*/
reload(reason?: string): void,
onFastRefresh(): void,
} = {
addMenuItem(title: string, handler: () => mixed): void {},
reload(reason?: string): void {},
onFastRefresh(): void {},
};
type DevSettingsEventDefinitions = {
didPressMenuItem: [{title: string}],
};
if (__DEV__) {
const emitter = new NativeEventEmitter<DevSettingsEventDefinitions>(
// T88715063: NativeEventEmitter only used this parameter on iOS. Now it uses it on all platforms, so this code was modified automatically to preserve its behavior
// If you want to use the native module on other platforms, please remove this condition and test its behavior
Platform.OS !== 'ios' ? null : NativeDevSettings,
);
const subscriptions = new Map<string, EventSubscription>();
DevSettings = {
addMenuItem(title: string, handler: () => mixed): void {
// Make sure items are not added multiple times. This can
// happen when hot reloading the module that registers the
// menu items. The title is used as the id which means we
// don't support multiple items with the same name.
let subscription = subscriptions.get(title);
if (subscription != null) {
subscription.remove();
} else {
NativeDevSettings.addMenuItem(title);
}
subscription = emitter.addListener('didPressMenuItem', event => {
if (event.title === title) {
handler();
}
});
subscriptions.set(title, subscription);
},
reload(reason?: string): void {
if (NativeDevSettings.reloadWithReason != null) {
NativeDevSettings.reloadWithReason(reason ?? 'Uncategorized from JS');
} else {
NativeDevSettings.reload();
}
},
onFastRefresh(): void {
NativeDevSettings.onFastRefresh?.();
},
};
}
export default DevSettings;

View File

@@ -0,0 +1,15 @@
/**
* 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
*/
export type {DeviceInfoConstants} from './NativeDeviceInfo';
import NativeDeviceInfo from './NativeDeviceInfo';
export default NativeDeviceInfo;

View File

@@ -0,0 +1,79 @@
/**
* 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.
*
* @format
*/
import {EmitterSubscription} from '../vendor/emitter/EventEmitter';
// Used by Dimensions below
export interface ScaledSize {
width: number;
height: number;
scale: number;
fontScale: number;
}
/**
* Initial dimensions are set before `runApplication` is called so they should
* be available before any other require's are run, but may be updated later.
*
* Note: Although dimensions are available immediately, they may change (e.g
* due to device rotation) so any rendering logic or styles that depend on
* these constants should try to call this function on every render, rather
* than caching the value (for example, using inline styles rather than
* setting a value in a `StyleSheet`).
*
* Example: `const {height, width} = Dimensions.get('window');`
*
* @param dim Name of dimension as defined when calling `set`.
* @returns Value for the dimension.
* @see https://reactnative.dev/docs/dimensions#content
*/
export interface Dimensions {
/**
* Initial dimensions are set before runApplication is called so they
* should be available before any other require's are run, but may be
* updated later.
* Note: Although dimensions are available immediately, they may
* change (e.g due to device rotation) so any rendering logic or
* styles that depend on these constants should try to call this
* function on every render, rather than caching the value (for
* example, using inline styles rather than setting a value in a
* StyleSheet).
* Example: const {height, width} = Dimensions.get('window');
@param dim Name of dimension as defined when calling set.
@returns Value for the dimension.
*/
get(dim: 'window' | 'screen'): ScaledSize;
/**
* This should only be called from native code by sending the didUpdateDimensions event.
* @param dims Simple string-keyed object of dimensions to set
*/
set(dims: {[key: string]: any}): void;
/**
* Add an event listener for dimension changes
*
* @param type the type of event to listen to
* @param handler the event handler
*/
addEventListener(
type: 'change',
handler: ({
window,
screen,
}: {
window: ScaledSize;
screen: ScaledSize;
}) => void,
): EmitterSubscription;
}
export function useWindowDimensions(): ScaledSize;
export const Dimensions: Dimensions;

View File

@@ -0,0 +1,128 @@
/**
* 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
* @format
*/
import RCTDeviceEventEmitter from '../EventEmitter/RCTDeviceEventEmitter';
import EventEmitter, {
type EventSubscription,
} from '../vendor/emitter/EventEmitter';
import NativeDeviceInfo, {
type DimensionsPayload,
type DisplayMetrics,
type DisplayMetricsAndroid,
} from './NativeDeviceInfo';
import invariant from 'invariant';
export type {DimensionsPayload, DisplayMetrics, DisplayMetricsAndroid};
/** @deprecated Use DisplayMetrics */
export type ScaledSize = DisplayMetrics;
const eventEmitter = new EventEmitter<{
change: [DimensionsPayload],
}>();
let dimensionsInitialized = false;
let dimensions: DimensionsPayload;
class Dimensions {
/**
* NOTE: `useWindowDimensions` is the preferred API for React components.
*
* Initial dimensions are set before `runApplication` is called so they should
* be available before any other require's are run, but may be updated later.
*
* Note: Although dimensions are available immediately, they may change (e.g
* due to device rotation) so any rendering logic or styles that depend on
* these constants should try to call this function on every render, rather
* than caching the value (for example, using inline styles rather than
* setting a value in a `StyleSheet`).
*
* Example: `const {height, width} = Dimensions.get('window');`
*
* @param {string} dim Name of dimension as defined when calling `set`.
* @returns {DisplayMetrics? | DisplayMetricsAndroid?} Value for the dimension.
*/
static get(dim: string): DisplayMetrics | DisplayMetricsAndroid {
// $FlowFixMe[invalid-computed-prop]
invariant(dimensions[dim], 'No dimension set for key ' + dim);
return dimensions[dim];
}
/**
* This should only be called from native code by sending the
* didUpdateDimensions event.
*
* @param {DimensionsPayload} dims Simple string-keyed object of dimensions to set
*/
static set(dims: $ReadOnly<DimensionsPayload>): void {
// We calculate the window dimensions in JS so that we don't encounter loss of
// precision in transferring the dimensions (which could be non-integers) over
// the bridge.
let {screen, window} = dims;
const {windowPhysicalPixels} = dims;
if (windowPhysicalPixels) {
window = {
width: windowPhysicalPixels.width / windowPhysicalPixels.scale,
height: windowPhysicalPixels.height / windowPhysicalPixels.scale,
scale: windowPhysicalPixels.scale,
fontScale: windowPhysicalPixels.fontScale,
};
}
const {screenPhysicalPixels} = dims;
if (screenPhysicalPixels) {
screen = {
width: screenPhysicalPixels.width / screenPhysicalPixels.scale,
height: screenPhysicalPixels.height / screenPhysicalPixels.scale,
scale: screenPhysicalPixels.scale,
fontScale: screenPhysicalPixels.fontScale,
};
} else if (screen == null) {
screen = window;
}
dimensions = {window, screen};
if (dimensionsInitialized) {
// Don't fire 'change' the first time the dimensions are set.
eventEmitter.emit('change', dimensions);
} else {
dimensionsInitialized = true;
}
}
/**
* Add an event handler. Supported events:
*
* - `change`: Fires when a property within the `Dimensions` object changes. The argument
* to the event handler is an object with `window` and `screen` properties whose values
* are the same as the return values of `Dimensions.get('window')` and
* `Dimensions.get('screen')`, respectively.
*/
static addEventListener(
type: 'change',
handler: Function,
): EventSubscription {
invariant(
type === 'change',
'Trying to subscribe to unknown event: "%s"',
type,
);
return eventEmitter.addListener(type, handler);
}
}
// Subscribe before calling getConstants to make sure we don't miss any updates in between.
RCTDeviceEventEmitter.addListener(
'didUpdateDimensions',
(update: DimensionsPayload) => {
Dimensions.set(update);
},
);
Dimensions.set(NativeDeviceInfo.getConstants().Dimensions);
export default Dimensions;

View File

@@ -0,0 +1,29 @@
/**
* 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
* @format
*/
/**
* @return whether or not a @param {function} f is provided natively by calling
* `toString` and check if the result includes `[native code]` in it.
*
* Note that a polyfill can technically fake this behavior but few does it.
* Therefore, this is usually good enough for our purpose.
*/
export function isNativeFunction(f: Function): boolean {
return typeof f === 'function' && f.toString().indexOf('[native code]') > -1;
}
/**
* @return whether or not the constructor of @param {object} o is an native
* function named with @param {string} expectedName.
*/
export function hasNativeConstructor(o: Object, expectedName: string): boolean {
const con = Object.getPrototypeOf(o).constructor;
return con.name === expectedName && isNativeFunction(con);
}

View File

@@ -0,0 +1,24 @@
/**
* 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
* @format
*/
import type {IPerformanceLogger} from './createPerformanceLogger';
import createPerformanceLogger from './createPerformanceLogger';
/**
* This is a global shared instance of IPerformanceLogger that is created with
* createPerformanceLogger().
* This logger should be used only for global performance metrics like the ones
* that are logged during loading bundle. If you want to log something from your
* React component you should use PerformanceLoggerContext instead.
*/
const GlobalPerformanceLogger: IPerformanceLogger = createPerformanceLogger();
export default GlobalPerformanceLogger;

View File

@@ -0,0 +1,381 @@
/**
* 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
*/
import type {ExtendedError} from '../Core/ExtendedError';
import getDevServer from '../Core/Devtools/getDevServer';
import LogBox from '../LogBox/LogBox';
import NativeRedBox from '../NativeModules/specs/NativeRedBox';
const DevSettings = require('./DevSettings').default;
const Platform = require('./Platform').default;
const invariant = require('invariant');
const MetroHMRClient = require('metro-runtime/src/modules/HMRClient');
const prettyFormat = require('pretty-format');
const pendingEntryPoints = [];
let hmrClient = null;
let hmrUnavailableReason: string | null = null;
let hmrOrigin: string | null = null;
let currentCompileErrorMessage: string | null = null;
let didConnect: boolean = false;
let pendingLogs: Array<[LogLevel, $ReadOnlyArray<mixed>]> = [];
type LogLevel =
| 'trace'
| 'info'
| 'warn'
| 'error'
| 'log'
| 'group'
| 'groupCollapsed'
| 'groupEnd'
| 'debug';
export type HMRClientNativeInterface = {
enable(): void,
disable(): void,
registerBundle(requestUrl: string): void,
log(level: LogLevel, data: $ReadOnlyArray<mixed>): void,
setup(
platform: string,
bundleEntry: string,
host: string,
port: number | string,
isEnabled: boolean,
scheme?: string,
): void,
};
/**
* HMR Client that receives from the server HMR updates and propagates them
* runtime to reflects those changes.
*/
const HMRClient: HMRClientNativeInterface = {
enable() {
if (hmrUnavailableReason !== null) {
// If HMR became unavailable while you weren't using it,
// explain why when you try to turn it on.
// This is an error (and not a warning) because it is shown
// in response to a direct user action.
throw new Error(hmrUnavailableReason);
}
invariant(hmrClient, 'Expected HMRClient.setup() call at startup.');
const DevLoadingView = require('./DevLoadingView').default;
// We use this for internal logging only.
// It doesn't affect the logic.
hmrClient.send(JSON.stringify({type: 'log-opt-in'}));
// When toggling Fast Refresh on, we might already have some stashed updates.
// Since they'll get applied now, we'll show a banner.
const hasUpdates = hmrClient.hasPendingUpdates();
if (hasUpdates) {
DevLoadingView.showMessage('Refreshing...', 'refresh');
}
try {
hmrClient.enable();
} finally {
if (hasUpdates) {
DevLoadingView.hide();
}
}
// There could be a compile error while Fast Refresh was off,
// but we ignored it at the time. Show it now.
showCompileError();
},
disable() {
invariant(hmrClient, 'Expected HMRClient.setup() call at startup.');
hmrClient.disable();
},
registerBundle(requestUrl: string) {
invariant(
hmrOrigin != null && hmrClient != null,
'Expected HMRClient.setup() call at startup.',
);
// only process registerBundle calls from the same origin
if (!requestUrl.startsWith(hmrOrigin)) {
return;
}
pendingEntryPoints.push(requestUrl);
registerBundleEntryPoints(hmrClient);
},
log(level: LogLevel, data: $ReadOnlyArray<mixed>) {
if (!hmrClient) {
// Catch a reasonable number of early logs
// in case hmrClient gets initialized later.
pendingLogs.push([level, data]);
if (pendingLogs.length > 100) {
pendingLogs.shift();
}
return;
}
try {
hmrClient.send(
JSON.stringify({
type: 'log',
level,
data: data.map(item =>
typeof item === 'string'
? item
: prettyFormat.format(item, {
escapeString: true,
highlight: true,
maxDepth: 3,
min: true,
plugins: [prettyFormat.plugins.ReactElement],
}),
),
}),
);
} catch (error) {
// If sending logs causes any failures we want to silently ignore them
// to ensure we do not cause infinite-logging loops.
}
},
// Called once by the bridge on startup, even if Fast Refresh is off.
// It creates the HMR client but doesn't actually set up the socket yet.
setup(
platform: string,
bundleEntry: string,
host: string,
port: number | string,
isEnabled: boolean,
scheme?: string = 'http',
) {
invariant(platform, 'Missing required parameter `platform`');
invariant(bundleEntry, 'Missing required parameter `bundleEntry`');
invariant(host, 'Missing required parameter `host`');
invariant(!hmrClient, 'Cannot initialize hmrClient twice');
// Moving to top gives errors due to NativeModules not being initialized
const DevLoadingView = require('./DevLoadingView').default;
/* $FlowFixMe[invalid-compare] Error discovered during Constant Condition
* roll out. See https://fburl.com/workplace/5whu3i34. */
const serverHost = port !== null && port !== '' ? `${host}:${port}` : host;
const serverScheme = scheme;
const origin = `${serverScheme}://${serverHost}`;
const client = new MetroHMRClient(`${origin}/hot`);
hmrOrigin = origin;
hmrClient = client;
const {fullBundleUrl} = getDevServer();
pendingEntryPoints.push(
// HMRServer understands regular bundle URLs, so prefer that in case
// there are any important URL parameters we can't reconstruct from
// `setup()`'s arguments.
fullBundleUrl ??
`${serverScheme}://${serverHost}/hot?bundleEntry=${bundleEntry}&platform=${platform}`,
);
client.on('connection-error', e => {
let error = `Cannot connect to Metro.
Try the following to fix the issue:
- Ensure that Metro is running and available on the same network`;
if (Platform.OS === 'ios') {
error += `
- Ensure that the Metro URL is correctly set in AppDelegate`;
} else {
error += `
- Ensure that your device/emulator is connected to your machine and has USB debugging enabled - run 'adb devices' to see a list of connected devices
- If you're on a physical device connected to the same machine, run 'adb reverse tcp:8081 tcp:8081' to forward requests from your device
- If your device is on the same Wi-Fi network, set 'Debug server host & port for device' in 'Dev settings' to your machine's IP address and the port of the local dev server - e.g. 10.0.1.1:8081`;
}
error += `
URL: ${host}:${port}
Error: ${e.message}`;
setHMRUnavailableReason(error);
});
let pendingUpdatesCount = 0;
client.on('update-start', ({isInitialUpdate}) => {
pendingUpdatesCount++;
currentCompileErrorMessage = null;
didConnect = true;
if (client.isEnabled() && !isInitialUpdate) {
DevLoadingView.showMessage('Refreshing...', 'refresh');
}
});
client.on('update', ({isInitialUpdate}) => {
if (client.isEnabled() && !isInitialUpdate) {
dismissRedbox();
LogBox.clearAllLogs();
}
});
client.on('update-done', () => {
pendingUpdatesCount--;
if (pendingUpdatesCount === 0) {
DevLoadingView.hide();
}
});
client.on('error', data => {
if (data.type === 'GraphNotFoundError') {
client.close();
setHMRUnavailableReason(
'Metro has restarted since the last edit. Reload to reconnect.',
);
} else if (data.type === 'RevisionNotFoundError') {
client.close();
setHMRUnavailableReason(
'Metro and the client are out of sync. Reload to reconnect.',
);
} else {
currentCompileErrorMessage = `${data.type} ${data.message}`;
if (client.isEnabled()) {
showCompileError();
}
}
});
client.on('close', closeEvent => {
// https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4.1
// https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.5
const isNormalOrUnsetCloseReason =
closeEvent == null ||
closeEvent.code === 1000 ||
closeEvent.code === 1005 ||
closeEvent.code == null;
setHMRUnavailableReason(
`${
isNormalOrUnsetCloseReason
? 'Disconnected from Metro.'
: `Disconnected from Metro (${closeEvent.code}: "${closeEvent.reason}").`
}
To reconnect:
- Ensure that Metro is running and available on the same network
- Reload this app (will trigger further help if Metro cannot be connected to)
`,
);
});
if (isEnabled) {
HMRClient.enable();
} else {
HMRClient.disable();
}
registerBundleEntryPoints(hmrClient);
flushEarlyLogs(hmrClient);
},
};
function setHMRUnavailableReason(reason: string) {
invariant(hmrClient, 'Expected HMRClient.setup() call at startup.');
if (hmrUnavailableReason !== null) {
// Don't show more than one warning.
return;
}
hmrUnavailableReason = reason;
const DevLoadingView = require('./DevLoadingView').default;
DevLoadingView.hide();
// We only want to show a warning if Fast Refresh is on *and* if we ever
// previously managed to connect successfully. We don't want to show
// the warning to native engineers who use cached bundles without Metro.
if (hmrClient.isEnabled() && didConnect) {
DevLoadingView.showMessage(
'Fast Refresh disconnected. Reload app to reconnect.',
'error',
);
console.warn(reason);
// (Not using the `warning` module to prevent a Buck cycle.)
}
}
function registerBundleEntryPoints(client: MetroHMRClient) {
if (hmrUnavailableReason != null) {
DevSettings.reload('Bundle Splitting Metro disconnected');
return;
}
if (pendingEntryPoints.length > 0) {
client.send(
JSON.stringify({
type: 'register-entrypoints',
entryPoints: pendingEntryPoints,
}),
);
pendingEntryPoints.length = 0;
}
}
function flushEarlyLogs(client: MetroHMRClient) {
try {
pendingLogs.forEach(([level, data]) => {
HMRClient.log(level, data);
});
} finally {
pendingLogs.length = 0;
}
}
function dismissRedbox() {
if (
Platform.OS === 'ios' &&
NativeRedBox != null &&
NativeRedBox.dismiss != null
) {
NativeRedBox.dismiss();
} else {
const NativeExceptionsManager =
require('../Core/NativeExceptionsManager').default;
NativeExceptionsManager &&
NativeExceptionsManager.dismissRedbox &&
NativeExceptionsManager.dismissRedbox();
}
}
function showCompileError() {
if (currentCompileErrorMessage === null) {
return;
}
// Even if there is already a redbox, syntax errors are more important.
// Otherwise you risk seeing a stale runtime error while a syntax error is more recent.
dismissRedbox();
const message = currentCompileErrorMessage;
currentCompileErrorMessage = null;
/* $FlowFixMe[class-object-subtyping] added when improving typing for this
* parameters */
// $FlowFixMe[incompatible-type]
const error: ExtendedError = new Error(message);
// Symbolicating compile errors is wasted effort
// because the stack trace is meaningless:
error.preventSymbolication = true;
throw error;
}
export default HMRClient;

View File

@@ -0,0 +1,30 @@
/**
* 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';
import type {HMRClientNativeInterface} from './HMRClient';
// This shim ensures DEV binary builds don't crash in JS
// when they're combined with a PROD JavaScript build.
const HMRClientProdShim: HMRClientNativeInterface = {
setup() {},
enable() {
console.error(
'Fast Refresh is disabled in JavaScript bundles built in production mode. ' +
'Did you forget to run Metro?',
);
},
disable() {},
registerBundle() {},
log() {},
};
export default HMRClientProdShim;

View File

@@ -0,0 +1,48 @@
/**
* 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.
*
* @format
*/
export type Timespan = {
startTime: number;
endTime?: number | undefined;
totalTime?: number | undefined;
startExtras?: Extras | undefined;
endExtras?: Extras | undefined;
};
// Extra values should be serializable primitives
export type ExtraValue = number | string | boolean;
export type Extras = {[key: string]: ExtraValue};
export interface IPerformanceLogger {
addTimespan(
key: string,
startTime: number,
endTime: number,
startExtras?: Extras,
endExtras?: Extras,
): void;
append(logger: IPerformanceLogger): void;
clear(): void;
clearCompleted(): void;
close(): void;
currentTimestamp(): number;
getExtras(): {[key: string]: ExtraValue | null};
getPoints(): {[key: string]: number | null};
getPointExtras(): {[key: string]: Extras | null};
getTimespans(): {[key: string]: Timespan | null};
hasTimespan(key: string): boolean;
isClosed(): boolean;
logEverything(): void;
markPoint(key: string, timestamp?: number, extras?: Extras): void;
removeExtra(key: string): ExtraValue | null;
setExtra(key: string, value: ExtraValue): void;
startTimespan(key: string, timestamp?: number, extras?: Extras): void;
stopTimespan(key: string, timestamp?: number, extras?: Extras): void;
}

View File

@@ -0,0 +1,49 @@
/**
* 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
* @format
*/
export type Timespan = {
startTime: number,
endTime?: number,
totalTime?: number,
startExtras?: Extras,
endExtras?: Extras,
};
// Extra values should be serializable primitives
export type ExtraValue = number | string | boolean;
export type Extras = {[key: string]: ExtraValue};
export interface IPerformanceLogger {
addTimespan(
key: string,
startTime: number,
endTime: number,
startExtras?: Extras,
endExtras?: Extras,
): void;
append(logger: IPerformanceLogger): void;
clear(): void;
clearCompleted(): void;
close(): void;
currentTimestamp(): number;
getExtras(): $ReadOnly<{[key: string]: ?ExtraValue, ...}>;
getPoints(): $ReadOnly<{[key: string]: ?number, ...}>;
getPointExtras(): $ReadOnly<{[key: string]: ?Extras, ...}>;
getTimespans(): $ReadOnly<{[key: string]: ?Timespan, ...}>;
hasTimespan(key: string): boolean;
isClosed(): boolean;
logEverything(): void;
markPoint(key: string, timestamp?: number, extras?: Extras): void;
removeExtra(key: string): ?ExtraValue;
setExtra(key: string, value: ExtraValue): void;
startTimespan(key: string, timestamp?: number, extras?: Extras): void;
stopTimespan(key: string, timestamp?: number, extras?: Extras): void;
}

View File

@@ -0,0 +1,14 @@
/**
* 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
* @format
*/
export type * from '../../src/private/specs_DEPRECATED/modules/NativeAppearance';
import NativeAppearance from '../../src/private/specs_DEPRECATED/modules/NativeAppearance';
export default NativeAppearance;

View File

@@ -0,0 +1,14 @@
/**
* 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
* @format
*/
export * from '../../src/private/specs_DEPRECATED/modules/NativeDevLoadingView';
import NativeDevLoadingView from '../../src/private/specs_DEPRECATED/modules/NativeDevLoadingView';
export default NativeDevLoadingView;

View File

@@ -0,0 +1,12 @@
/**
* 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
* @format
*/
export * from '../../src/private/specs_DEPRECATED/modules/NativeDeviceInfo';
export {default} from '../../src/private/specs_DEPRECATED/modules/NativeDeviceInfo';

View File

@@ -0,0 +1,14 @@
/**
* 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
* @format
*/
export * from '../../src/private/specs_DEPRECATED/modules/NativePlatformConstantsAndroid';
import NativePlatformConstantsAndroid from '../../src/private/specs_DEPRECATED/modules/NativePlatformConstantsAndroid';
export default NativePlatformConstantsAndroid;

View File

@@ -0,0 +1,14 @@
/**
* 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
* @format
*/
export * from '../../src/private/specs_DEPRECATED/modules/NativePlatformConstantsIOS';
import NativePlatformConstantsIOS from '../../src/private/specs_DEPRECATED/modules/NativePlatformConstantsIOS';
export default NativePlatformConstantsIOS;

View File

@@ -0,0 +1,33 @@
/**
* 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
* @format
*/
import type {IPerformanceLogger} from './createPerformanceLogger';
import GlobalPerformanceLogger from './GlobalPerformanceLogger';
import * as React from 'react';
import {createContext, useContext} from 'react';
/**
* This is a React Context that provides a scoped instance of IPerformanceLogger.
* We wrap every <AppContainer /> with a Provider for this context so the logger
* should be available in every component.
* See React docs about using Context: https://react.dev/docs/context.html
*/
const PerformanceLoggerContext: React.Context<IPerformanceLogger> =
createContext(GlobalPerformanceLogger);
if (__DEV__) {
PerformanceLoggerContext.displayName = 'PerformanceLoggerContext';
}
export function usePerformanceLogger(): IPerformanceLogger {
return useContext(PerformanceLoggerContext);
}
export default PerformanceLoggerContext;

View File

@@ -0,0 +1,64 @@
/**
* 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.
*
* @format
*/
export interface PixelRatioStatic {
/*
Returns the device pixel density. Some examples:
PixelRatio.get() === 1
mdpi Android devices (160 dpi)
PixelRatio.get() === 1.5
hdpi Android devices (240 dpi)
PixelRatio.get() === 2
iPhone 4, 4S
iPhone 5, 5c, 5s
iPhone 6
xhdpi Android devices (320 dpi)
PixelRatio.get() === 3
iPhone 6 plus
xxhdpi Android devices (480 dpi)
PixelRatio.get() === 3.5
Nexus 6
*/
get(): number;
/*
Returns the scaling factor for font sizes. This is the ratio that is
used to calculate the absolute font size, so any elements that
heavily depend on that should use this to do calculations.
* on Android value reflects the user preference set in Settings > Display > Font size
* on iOS value reflects the user preference set in Settings > Display & Brightness > Text Size,
value can also be updated in Settings > Accessibility > Display & Text Size > Larger Text
If a font scale is not set, this returns the device pixel ratio.
*/
getFontScale(): number;
/**
* Converts a layout size (dp) to pixel size (px).
* Guaranteed to return an integer number.
*/
getPixelSizeForLayoutSize(layoutSize: number): number;
/**
* Rounds a layout size (dp) to the nearest layout size that
* corresponds to an integer number of pixels. For example,
* on a device with a PixelRatio of 3,
* PixelRatio.roundToNearestPixel(8.4) = 8.33,
* which corresponds to exactly (8.33 * 3) = 25 pixels.
*/
roundToNearestPixel(layoutSize: number): number;
/**
* No-op for iOS, but used on the web. Should not be documented. [sic]
*/
startDetecting(): void;
}
export const PixelRatio: PixelRatioStatic;

View File

@@ -0,0 +1,127 @@
/**
* 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';
const Dimensions = require('./Dimensions').default;
/**
* PixelRatio class gives access to the device pixel density.
*
* ## Fetching a correctly sized image
*
* You should get a higher resolution image if you are on a high pixel density
* device. A good rule of thumb is to multiply the size of the image you display
* by the pixel ratio.
*
* ```
* var image = getImage({
* width: PixelRatio.getPixelSizeForLayoutSize(200),
* height: PixelRatio.getPixelSizeForLayoutSize(100),
* });
* <Image source={image} style={{width: 200, height: 100}} />
* ```
*
* ## Pixel grid snapping
*
* In iOS, you can specify positions and dimensions for elements with arbitrary
* precision, for example 29.674825. But, ultimately the physical display only
* have a fixed number of pixels, for example 640×960 for iPhone 4 or 750×1334
* for iPhone 6. iOS tries to be as faithful as possible to the user value by
* spreading one original pixel into multiple ones to trick the eye. The
* downside of this technique is that it makes the resulting element look
* blurry.
*
* In practice, we found out that developers do not want this feature and they
* have to work around it by doing manual rounding in order to avoid having
* blurry elements. In React Native, we are rounding all the pixels
* automatically.
*
* We have to be careful when to do this rounding. You never want to work with
* rounded and unrounded values at the same time as you're going to accumulate
* rounding errors. Having even one rounding error is deadly because a one
* pixel border may vanish or be twice as big.
*
* In React Native, everything in JavaScript and within the layout engine works
* with arbitrary precision numbers. It's only when we set the position and
* dimensions of the native element on the main thread that we round. Also,
* rounding is done relative to the root rather than the parent, again to avoid
* accumulating rounding errors.
*
*/
class PixelRatio {
/**
* Returns the device pixel density. Some examples:
*
* - PixelRatio.get() === 1
* - mdpi Android devices (160 dpi)
* - PixelRatio.get() === 1.5
* - hdpi Android devices (240 dpi)
* - PixelRatio.get() === 2
* - iPhone 4, 4S
* - iPhone 5, 5c, 5s
* - iPhone 6
* - iPhone 7
* - iPhone 8
* - iPhone SE
* - xhdpi Android devices (320 dpi)
* - PixelRatio.get() === 3
* - iPhone 6 Plus
* - iPhone 7 Plus
* - iPhone 8 Plus
* - iPhone X
* - xxhdpi Android devices (480 dpi)
* - PixelRatio.get() === 3.5
* - Nexus 6
*/
static get(): number {
return Dimensions.get('window').scale;
}
/**
* Returns the scaling factor for font sizes. This is the ratio that is used to calculate the
* absolute font size, so any elements that heavily depend on that should use this to do
* calculations.
*
* If a font scale is not set, this returns the device pixel ratio.
*
* This reflects the user preference set in:
* - Settings > Display > Font size on Android,
* - Settings > Display & Brightness > Text Size on iOS.
*/
static getFontScale(): number {
return Dimensions.get('window').fontScale || PixelRatio.get();
}
/**
* Converts a layout size (dp) to pixel size (px).
*
* Guaranteed to return an integer number.
*/
static getPixelSizeForLayoutSize(layoutSize: number): number {
return Math.round(layoutSize * PixelRatio.get());
}
/**
* Rounds a layout size (dp) to the nearest layout size that corresponds to
* an integer number of pixels. For example, on a device with a PixelRatio
* of 3, `PixelRatio.roundToNearestPixel(8.4) = 8.33`, which corresponds to
* exactly (8.33 * 3) = 25 pixels.
*/
static roundToNearestPixel(layoutSize: number): number {
const ratio = PixelRatio.get();
return Math.round(layoutSize * ratio) / ratio;
}
// No-op for iOS, but used on the web. Should not be documented.
static startDetecting() {}
}
export default PixelRatio;

View File

@@ -0,0 +1,84 @@
/**
* 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
* @format
*/
import type {PlatformSelectSpec, PlatformType} from './PlatformTypes';
import NativePlatformConstantsAndroid from './NativePlatformConstantsAndroid';
const Platform: PlatformType = {
__constants: null,
OS: 'android',
// $FlowFixMe[unsafe-getters-setters]
get Version(): number {
// $FlowFixMe[object-this-reference]
return this.constants.Version;
},
// $FlowFixMe[unsafe-getters-setters]
get constants(): {
isTesting: boolean,
isDisableAnimations?: boolean,
reactNativeVersion: {
major: number,
minor: number,
patch: number,
prerelease: ?string,
},
Version: number,
Release: string,
Serial: string,
Fingerprint: string,
Model: string,
ServerHost?: string,
uiMode: string,
Brand: string,
Manufacturer: string,
} {
// $FlowFixMe[object-this-reference]
if (this.__constants == null) {
// $FlowFixMe[object-this-reference]
this.__constants = NativePlatformConstantsAndroid.getConstants();
}
// $FlowFixMe[object-this-reference]
return this.__constants;
},
// $FlowFixMe[unsafe-getters-setters]
get isTesting(): boolean {
if (__DEV__) {
// $FlowFixMe[object-this-reference]
return this.constants.isTesting;
}
return false;
},
// $FlowFixMe[unsafe-getters-setters]
get isDisableAnimations(): boolean {
// $FlowFixMe[object-this-reference]
return this.constants.isDisableAnimations ?? this.isTesting;
},
// $FlowFixMe[unsafe-getters-setters]
get isTV(): boolean {
// $FlowFixMe[object-this-reference]
return this.constants.uiMode === 'tv';
},
// $FlowFixMe[unsafe-getters-setters]
get isVision(): boolean {
return false;
},
select: <T>(spec: PlatformSelectSpec<T>): T =>
'android' in spec
? // $FlowFixMe[incompatible-type]
spec.android
: 'native' in spec
? // $FlowFixMe[incompatible-type]
spec.native
: // $FlowFixMe[incompatible-type]
spec.default,
};
export default Platform;

View File

@@ -0,0 +1,107 @@
/**
* 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.
*
* @format
*/
/**
* @see https://reactnative.dev/docs/platform-specific-code#content
*/
export type PlatformOSType =
| 'ios'
| 'android'
| 'macos'
| 'windows'
| 'web'
| 'native';
type PlatformConstants = {
isTesting: boolean;
isDisableAnimations?: boolean | undefined;
reactNativeVersion: {
major: number;
minor: number;
patch: number;
prerelease?: string | null | undefined;
};
};
interface PlatformStatic {
isTV: boolean;
isTesting: boolean;
Version: number | string;
constants: PlatformConstants;
/**
* @see https://reactnative.dev/docs/platform-specific-code#content
*/
select<T>(
specifics:
| ({[platform in PlatformOSType]?: T} & {default: T})
| {[platform in PlatformOSType]: T},
): T;
select<T>(specifics: {[platform in PlatformOSType]?: T}): T | undefined;
}
interface PlatformIOSStatic extends PlatformStatic {
constants: PlatformConstants & {
forceTouchAvailable: boolean;
interfaceIdiom: string;
osVersion: string;
systemName: string;
isMacCatalyst?: boolean | undefined;
};
OS: 'ios';
isPad: boolean;
isTV: boolean;
isVision: boolean;
isMacCatalyst?: boolean | undefined;
Version: string;
}
interface PlatformAndroidStatic extends PlatformStatic {
constants: PlatformConstants & {
Version: number;
Release: string;
Serial: string;
Fingerprint: string;
Model: string;
Brand: string;
Manufacturer: string;
ServerHost?: string | undefined;
uiMode: 'car' | 'desk' | 'normal' | 'tv' | 'watch' | 'unknown';
};
OS: 'android';
Version: number;
}
interface PlatformMacOSStatic extends PlatformStatic {
OS: 'macos';
Version: string;
constants: PlatformConstants & {
osVersion: string;
};
}
interface PlatformWindowsOSStatic extends PlatformStatic {
OS: 'windows';
Version: number;
constants: PlatformConstants & {
osVersion: number;
};
}
interface PlatformWebStatic extends PlatformStatic {
OS: 'web';
Version: string;
}
export type Platform =
| PlatformIOSStatic
| PlatformAndroidStatic
| PlatformWindowsOSStatic
| PlatformMacOSStatic
| PlatformWebStatic;
export const Platform: Platform;

View File

@@ -0,0 +1,85 @@
/**
* 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
* @format
*/
import type {PlatformSelectSpec, PlatformType} from './PlatformTypes';
import NativePlatformConstantsIOS from './NativePlatformConstantsIOS';
const Platform: PlatformType = {
__constants: null,
OS: 'ios',
// $FlowFixMe[unsafe-getters-setters]
get Version(): string {
// $FlowFixMe[object-this-reference]
return this.constants.osVersion;
},
// $FlowFixMe[unsafe-getters-setters]
get constants(): {
forceTouchAvailable: boolean,
interfaceIdiom: string,
isTesting: boolean,
isDisableAnimations?: boolean,
osVersion: string,
reactNativeVersion: {
major: number,
minor: number,
patch: number,
prerelease: ?string,
},
systemName: string,
isMacCatalyst?: boolean,
} {
// $FlowFixMe[object-this-reference]
if (this.__constants == null) {
// $FlowFixMe[object-this-reference]
this.__constants = NativePlatformConstantsIOS.getConstants();
}
// $FlowFixMe[object-this-reference]
return this.__constants;
},
// $FlowFixMe[unsafe-getters-setters]
get isPad(): boolean {
// $FlowFixMe[object-this-reference]
return this.constants.interfaceIdiom === 'pad';
},
// $FlowFixMe[unsafe-getters-setters]
get isTV(): boolean {
// $FlowFixMe[object-this-reference]
return this.constants.interfaceIdiom === 'tv';
},
// $FlowFixMe[unsafe-getters-setters]
get isVision(): boolean {
// $FlowFixMe[object-this-reference]
return this.constants.interfaceIdiom === 'vision';
},
// $FlowFixMe[unsafe-getters-setters]
get isTesting(): boolean {
if (__DEV__) {
// $FlowFixMe[object-this-reference]
return this.constants.isTesting;
}
return false;
},
// $FlowFixMe[unsafe-getters-setters]
get isDisableAnimations(): boolean {
// $FlowFixMe[object-this-reference]
return this.constants.isDisableAnimations ?? this.isTesting;
},
// $FlowFixMe[unsafe-getters-setters]
get isMacCatalyst(): boolean {
// $FlowFixMe[object-this-reference]
return this.constants.isMacCatalyst ?? false;
},
select: <T>(spec: PlatformSelectSpec<T>): T =>
// $FlowFixMe[incompatible-type]
'ios' in spec ? spec.ios : 'native' in spec ? spec.native : spec.default,
};
export default Platform;

View File

@@ -0,0 +1,17 @@
/**
* 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
* @format
*/
// NOTE: This file supports backwards compatibility of subpath (deep) imports
// from 'react-native' with platform-specific extensions. It can be deleted
// once we remove the "./*" mapping from package.json "exports".
import Platform from './Platform';
export default Platform;

View File

@@ -0,0 +1,13 @@
/**
* 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
* @format
*/
import type {PlatformType} from './PlatformTypes';
declare export default PlatformType;

View File

@@ -0,0 +1,189 @@
/**
* 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
* @format
*/
export type PlatformOSType =
| 'ios'
| 'android'
| 'macos'
| 'windows'
| 'web'
| 'native';
type OptionalPlatformSelectSpec<T> = {
[key in PlatformOSType]?: T, // eslint-disable-line no-unused-vars
};
export type PlatformSelectSpec<T> =
| {
...OptionalPlatformSelectSpec<T>,
default: T,
}
| OptionalPlatformSelectSpec<T>;
type IOSPlatform = {
__constants: null,
OS: 'ios',
// $FlowFixMe[unsafe-getters-setters]
get Version(): string,
// $FlowFixMe[unsafe-getters-setters]
get constants(): {
forceTouchAvailable: boolean,
interfaceIdiom: string,
isTesting: boolean,
isDisableAnimations?: boolean,
osVersion: string,
reactNativeVersion: {
major: number,
minor: number,
patch: number,
prerelease: ?string,
},
systemName: string,
isMacCatalyst?: boolean,
},
// $FlowFixMe[unsafe-getters-setters]
get isPad(): boolean,
// $FlowFixMe[unsafe-getters-setters]
get isTV(): boolean,
// $FlowFixMe[unsafe-getters-setters]
get isVision(): boolean,
// $FlowFixMe[unsafe-getters-setters]
get isTesting(): boolean,
// $FlowFixMe[unsafe-getters-setters]
get isDisableAnimations(): boolean,
// $FlowFixMe[unsafe-getters-setters]
get isMacCatalyst(): boolean,
select: <T>(spec: PlatformSelectSpec<T>) => T,
};
type AndroidPlatform = {
__constants: null,
OS: 'android',
// $FlowFixMe[unsafe-getters-setters]
get Version(): number,
// $FlowFixMe[unsafe-getters-setters]
get constants(): {
isTesting: boolean,
isDisableAnimations?: boolean,
reactNativeVersion: {
major: number,
minor: number,
patch: number,
prerelease: ?string,
},
Version: number,
Release: string,
Serial: string,
Fingerprint: string,
Model: string,
ServerHost?: string,
uiMode: string,
Brand: string,
Manufacturer: string,
},
// $FlowFixMe[unsafe-getters-setters]
get isTV(): boolean,
// $FlowFixMe[unsafe-getters-setters]
get isVision(): boolean,
// $FlowFixMe[unsafe-getters-setters]
get isTesting(): boolean,
// $FlowFixMe[unsafe-getters-setters]
get isDisableAnimations(): boolean,
select: <T>(spec: PlatformSelectSpec<T>) => T,
};
type WindowsPlatform = {
__constants: null,
OS: 'windows',
// $FlowFixMe[unsafe-getters-setters]
get Version(): number,
// $FlowFixMe[unsafe-getters-setters]
get constants(): {
// [Windows]
isTesting: boolean,
isDisableAnimations?: boolean,
reactNativeVersion: {
major: number,
minor: number,
patch: number,
prerelease: ?string,
},
reactNativeWindowsVersion: {
major: number,
minor: number,
patch: number,
},
osVersion: number,
},
// $FlowFixMe[unsafe-getters-setters]
get isTesting(): boolean,
// $FlowFixMe[unsafe-getters-setters]
get isDisableAnimations(): boolean,
// $FlowFixMe[unsafe-getters-setters]
get isTV(): boolean,
select: <T>(spec: PlatformSelectSpec<T>) => T,
};
type MacOSPlatform = {
__constants: null,
OS: 'macos',
// $FlowFixMe[unsafe-getters-setters]
get Version(): string,
// $FlowFixMe[unsafe-getters-setters]
get constants(): {
isTesting: boolean,
osVersion: string,
reactNativeVersion: {
major: number,
minor: number,
patch: number,
prerelease: ?number,
},
systemName: string,
},
// $FlowFixMe[unsafe-getters-setters]
get isTV(): boolean,
// $FlowFixMe[unsafe-getters-setters]
get isVision(): boolean,
// $FlowFixMe[unsafe-getters-setters]
get isTesting(): boolean,
// $FlowFixMe[unsafe-getters-setters]
get isDisableAnimations(): boolean,
select: <T>(spec: PlatformSelectSpec<T>) => T,
};
type WebPlatform = {
OS: 'web',
// $FlowFixMe[unsafe-getters-setters]
get Version(): string,
// $FlowFixMe[unsafe-getters-setters]
get constants(): {
reactNativeVersion: {
major: number,
minor: number,
patch: number,
prerelease: ?string,
},
},
// $FlowFixMe[unsafe-getters-setters]
get isTV(): boolean,
// $FlowFixMe[unsafe-getters-setters]
get isTesting(): boolean,
// $FlowFixMe[unsafe-getters-setters]
get isDisableAnimations(): boolean,
select: <T>(spec: PlatformSelectSpec<T>) => T,
};
export type PlatformType =
| IOSPlatform
| AndroidPlatform
| WindowsPlatform
| MacOSPlatform
| WebPlatform;

View File

@@ -0,0 +1,54 @@
/**
* 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
* @format
*/
'use strict';
const defineLazyObjectProperty = require('./defineLazyObjectProperty').default;
/**
* Sets an object's property. If a property with the same name exists, this will
* replace it but maintain its descriptor configuration. The property will be
* replaced with a lazy getter.
*
* In DEV mode the original property value will be preserved as `original[PropertyName]`
* so that, if necessary, it can be restored. For example, if you want to route
* network requests through DevTools (to trace them):
*
* global.XMLHttpRequest = global.originalXMLHttpRequest;
*
* @see https://github.com/facebook/react-native/issues/934
*/
export function polyfillObjectProperty<T>(
object: {...},
name: string,
getValue: () => T,
): void {
const descriptor = Object.getOwnPropertyDescriptor<$FlowFixMe>(object, name);
if (__DEV__ && descriptor) {
const backupName = `original${name[0].toUpperCase()}${name.slice(1)}`;
Object.defineProperty(object, backupName, descriptor);
}
const {enumerable, writable, configurable = false} = descriptor || {};
if (descriptor && !configurable) {
console.error('Failed to set polyfill. ' + name + ' is not configurable.');
return;
}
defineLazyObjectProperty(object, name, {
get: getValue,
enumerable: enumerable !== false,
writable: writable !== false,
});
}
export function polyfillGlobal<T>(name: string, getValue: () => T): void {
polyfillObjectProperty(global, name, getValue);
}

View File

@@ -0,0 +1,56 @@
/**
* 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
* @format
*/
'use strict';
const invariant = require('invariant');
const levelsMap = {
log: 'log',
info: 'info',
warn: 'warn',
error: 'error',
fatal: 'error',
};
let warningHandler: ?(...Array<mixed>) => void = null;
const RCTLog = {
// level one of log, info, warn, error, mustfix
logIfNoNativeHook(level: string, ...args: Array<mixed>): void {
// We already printed in the native console, so only log here if using a js debugger
if (typeof global.nativeLoggingHook === 'undefined') {
RCTLog.logToConsole(level, ...args);
} else {
// Report native warnings to LogBox
if (warningHandler && level === 'warn') {
warningHandler(...args);
}
}
},
// Log to console regardless of nativeLoggingHook
logToConsole(level: string, ...args: Array<mixed>): void {
// $FlowFixMe[invalid-computed-prop]
const logFn = levelsMap[level];
invariant(
logFn,
'Level "' + level + '" not one of ' + Object.keys(levelsMap).toString(),
);
console[logFn](...args);
},
setWarningHandler(handler: typeof warningHandler): void {
warningHandler = handler;
},
};
export default RCTLog;

View File

@@ -0,0 +1,237 @@
/**
* 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
* @format
*/
/* eslint-env jest */
import type {ReactTestRenderer as ReactTestRendererType} from 'react-test-renderer';
import TouchableWithoutFeedback from '../Components/Touchable/TouchableWithoutFeedback';
import * as React from 'react';
import ReactTestRenderer from 'react-test-renderer';
const Switch = require('../Components/Switch/Switch').default;
const TextInput = require('../Components/TextInput/TextInput').default;
const View = require('../Components/View/View').default;
const Text = require('../Text/Text').default;
const {VirtualizedList} = require('@react-native/virtualized-lists').default;
export type ReactTestInstance = ReactTestRendererType['root'];
export type Predicate = (node: ReactTestInstance) => boolean;
/* $FlowFixMe[value-as-type] (>=0.125.1 site=react_native_fb) This comment
* suppresses an error found when Flow v0.125.1 was deployed. To see the error,
* delete this comment and run Flow. */
export type ReactTestRendererJSON =
/* $FlowFixMe[prop-missing] (>=0.125.1 site=react_native_fb) This comment
* suppresses an error found when Flow v0.125.1 was deployed. To see the error,
* delete this comment and run Flow. */
ReturnType<ReactTestRenderer.create.toJSON>;
function byClickable(): Predicate {
return withMessage(
node =>
// note: <Text /> lazy-mounts press handlers after the first press,
// so this is a workaround for targeting text nodes.
/* $FlowFixMe[invalid-compare] Error discovered during Constant Condition
* roll out. See https://fburl.com/workplace/4oq3zi07. */
(node.type === Text &&
node.props &&
typeof node.props.onPress === 'function') ||
// note: Special casing <Switch /> since it doesn't use touchable
/* $FlowFixMe[invalid-compare] Error discovered during Constant Condition
* roll out. See https://fburl.com/workplace/4oq3zi07. */
(node.type === Switch && node.props && node.props.disabled !== true) ||
/* $FlowFixMe[invalid-compare] Error discovered during Constant Condition
* roll out. See https://fburl.com/workplace/4oq3zi07. */
(node.type === View &&
node?.props?.onStartShouldSetResponder?.testOnly_pressabilityConfig) ||
/* $FlowFixMe[invalid-compare] Error discovered during Constant Condition
* roll out. See https://fburl.com/workplace/4oq3zi07. */
(node.type === TouchableWithoutFeedback &&
node.props &&
typeof node.props.onPress === 'function') ||
// HACK: Find components that use `Pressability`.
node.instance?.state?.pressability != null ||
// TODO: Remove this after deleting `Touchable`.
(node.instance != null &&
// $FlowFixMe[prop-missing]
typeof node.instance.touchableHandlePress === 'function'),
'is clickable',
);
}
function byTestID(testID: string): Predicate {
return withMessage(
node => node.props && node.props.testID === testID,
`testID prop equals ${testID}`,
);
}
function byTextMatching(regex: RegExp): Predicate {
return withMessage(
node => node.props != null && regex.exec(node.props.children) !== null,
`text content matches ${regex.toString()}`,
);
}
function enter(instance: ReactTestInstance, text: string) {
const input = instance.findByType(TextInput);
input.props.onChange && input.props.onChange({nativeEvent: {text}});
input.props.onChangeText && input.props.onChangeText(text);
}
// Returns null if there is no error, otherwise returns an error message string.
function maximumDepthError(
tree: ReactTestRendererType,
maxDepthLimit: number,
): ?string {
const maxDepth = maximumDepthOfJSON(tree.toJSON());
if (maxDepth > maxDepthLimit) {
return (
`maximumDepth of ${maxDepth} exceeded limit of ${maxDepthLimit} - this is a proxy ` +
'metric to protect against stack overflow errors:\n\n' +
'https://fburl.com/rn-view-stack-overflow.\n\n' +
'To fix, you need to remove native layers from your hierarchy, such as unnecessary View ' +
'wrappers.'
);
} else {
return null;
}
}
function expectNoConsoleWarn() {
(jest: $FlowFixMe).spyOn(console, 'warn').mockImplementation((...args) => {
expect(args).toBeFalsy();
});
}
function expectNoConsoleError() {
let hasNotFailed = true;
(jest: $FlowFixMe).spyOn(console, 'error').mockImplementation((...args) => {
if (hasNotFailed) {
hasNotFailed = false; // set false to prevent infinite recursion
expect(args).toBeFalsy();
}
});
}
async function expectRendersMatchingSnapshot(
name: string,
ComponentProvider: () => React.MixedElement,
unmockComponent: () => mixed,
) {
let instance;
jest.resetAllMocks();
await ReactTestRenderer.act(() => {
instance = ReactTestRenderer.create(<ComponentProvider />);
});
expect(instance).toMatchSnapshot(
'should deep render when mocked (please verify output manually)',
);
jest.resetAllMocks();
unmockComponent();
await ReactTestRenderer.act(() => {
instance = ReactTestRenderer.create(<ComponentProvider />);
});
expect(instance).toMatchSnapshot(
'should deep render when not mocked (please verify output manually)',
);
}
// Takes a node from toJSON()
function maximumDepthOfJSON(node: ?ReactTestRendererJSON): number {
if (node == null) {
return 0;
} else if (typeof node === 'string' || node.children == null) {
return 1;
} else {
let maxDepth = 0;
node.children.forEach(child => {
maxDepth = Math.max(maximumDepthOfJSON(child) + 1, maxDepth);
});
return maxDepth;
}
}
function renderAndEnforceStrictMode(element: React.Node): any {
expectNoConsoleError();
return renderWithStrictMode(element);
}
function renderWithStrictMode(element: React.Node): ReactTestRendererType {
const WorkAroundBugWithStrictModeInTestRenderer = (prps: {
children: React.Node,
}) => prps.children;
const StrictMode = (React: $FlowFixMe).StrictMode;
return ReactTestRenderer.create(
<WorkAroundBugWithStrictModeInTestRenderer>
<StrictMode>{element}</StrictMode>
</WorkAroundBugWithStrictModeInTestRenderer>,
);
}
function tap(instance: ReactTestInstance) {
const touchable = instance.find(byClickable());
/* $FlowFixMe[invalid-compare] Error discovered during Constant Condition
* roll out. See https://fburl.com/workplace/4oq3zi07. */
if (touchable.type === Text && touchable.props && touchable.props.onPress) {
touchable.props.onPress();
/* $FlowFixMe[invalid-compare] Error discovered during Constant Condition
* roll out. See https://fburl.com/workplace/4oq3zi07. */
} else if (touchable.type === Switch && touchable.props) {
const value = !touchable.props.value;
const {onChange, onValueChange} = touchable.props;
onChange && onChange({nativeEvent: {value}});
onValueChange && onValueChange(value);
} else if (
touchable?.props?.onStartShouldSetResponder?.testOnly_pressabilityConfig
) {
const {onPress, disabled} =
touchable.props.onStartShouldSetResponder.testOnly_pressabilityConfig();
if (!disabled) {
onPress({nativeEvent: {}});
}
} else {
// Only tap when props.disabled isn't set (or there aren't any props)
if (!touchable.props || !touchable.props.disabled) {
touchable.props.onPress({nativeEvent: {}});
}
}
}
function scrollToBottom(instance: ReactTestInstance) {
const list = instance.findByType(VirtualizedList);
list.props && list.props.onEndReached();
}
// To make error messages a little bit better, we attach a custom toString
// implementation to a predicate
function withMessage(fn: Predicate, message: string): Predicate {
(fn: any).toString = () => message;
return fn;
}
export {byClickable};
export {byTestID};
export {byTextMatching};
export {enter};
export {expectNoConsoleWarn};
export {expectNoConsoleError};
export {expectRendersMatchingSnapshot};
export {maximumDepthError};
export {maximumDepthOfJSON};
export {renderAndEnforceStrictMode};
export {renderWithStrictMode};
export {scrollToBottom};
export {tap};
export {withMessage};

View File

@@ -0,0 +1,42 @@
/**
* 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
* @format
*/
'use strict';
export type Scene = {name: string, [string]: mixed, ...};
let _listeners: Array<(scene: Scene) => void> = [];
let _activeScene: Scene = {name: 'default'};
const SceneTracker = {
setActiveScene(scene: Scene) {
_activeScene = scene;
_listeners.forEach(listener => listener(_activeScene));
},
getActiveScene(): Scene {
return _activeScene;
},
addActiveSceneChangedListener(callback: (scene: Scene) => void): {
remove: () => void,
...
} {
_listeners.push(callback);
return {
remove: () => {
_listeners = _listeners.filter(listener => callback !== listener);
},
};
},
};
export default SceneTracker;

View File

@@ -0,0 +1,31 @@
/**
* 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
* @format
*/
'use strict';
const base64 = require('base64-js');
function binaryToBase64(data: ArrayBuffer | $ArrayBufferView): string {
if (data instanceof ArrayBuffer) {
// $FlowFixMe[reassign-const]
data = new Uint8Array(data);
}
if (data instanceof Uint8Array) {
return base64.fromByteArray(data);
}
if (!ArrayBuffer.isView(data)) {
throw new Error('data must be ArrayBuffer or typed array');
}
// Already checked that `data` is `DataView` in `ArrayBuffer.isView(data)`
const {buffer, byteOffset, byteLength}: DataView = (data: $FlowFixMe);
return base64.fromByteArray(new Uint8Array(buffer, byteOffset, byteLength));
}
export default binaryToBase64;

View File

@@ -0,0 +1,18 @@
/**
* 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.
*
* @format
*/
interface Options<T extends string> {
readonly supportedCommands: ReadonlyArray<T>;
}
declare function codegenNativeCommands<T extends object>(
options: Options<keyof T extends string ? keyof T : never>,
): T;
export default codegenNativeCommands;

View File

@@ -0,0 +1,33 @@
/**
* 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
* @format
*/
const {dispatchCommand} = require('../ReactNative/RendererProxy');
type NativeCommandsOptions<T = string> = $ReadOnly<{
supportedCommands: $ReadOnlyArray<T>,
}>;
function codegenNativeCommands<T: interface {}>(
options: NativeCommandsOptions<$Keys<T>>,
): T {
const commandObj: {[$Keys<T>]: (...$ReadOnlyArray<mixed>) => void} = {};
options.supportedCommands.forEach(command => {
// $FlowFixMe[missing-local-annot]
commandObj[command] = (ref, ...args) => {
// $FlowFixMe[incompatible-type]
dispatchCommand(ref, command, args);
};
});
return ((commandObj: any): T);
}
export default codegenNativeCommands;

View File

@@ -0,0 +1,26 @@
/**
* 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.
*
* @format
*/
import type {HostComponent} from 'react-native';
interface Options {
readonly interfaceOnly?: boolean | undefined;
readonly paperComponentName?: string | undefined;
readonly paperComponentNameDeprecated?: string | undefined;
readonly excludedPlatforms?: ReadonlyArray<'iOS' | 'android'> | undefined;
}
type NativeComponentType<T> = HostComponent<T>;
declare function codegenNativeComponent<Props extends object>(
componentName: string,
options?: Options,
): NativeComponentType<Props>;
export default codegenNativeComponent;

View File

@@ -0,0 +1,73 @@
/**
* 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
*/
// TODO: move this file to shims/ReactNative (requires React update and sync)
import type {HostComponent} from '../../src/private/types/HostComponent';
import requireNativeComponent from '../../Libraries/ReactNative/requireNativeComponent';
import UIManager from '../ReactNative/UIManager';
// TODO: import from CodegenSchema once workspaces are enabled
type NativeComponentOptions = $ReadOnly<{
interfaceOnly?: boolean,
paperComponentName?: string,
paperComponentNameDeprecated?: string,
excludedPlatforms?: $ReadOnlyArray<'iOS' | 'android'>,
}>;
export type NativeComponentType<T: {...}> = HostComponent<T>;
// If this function runs then that means the view configs were not
// generated at build time using `GenerateViewConfigJs.js`. Thus
// we need to `requireNativeComponent` to get the view configs from view managers.
// `requireNativeComponent` is not available in Bridgeless mode.
// e.g. This function runs at runtime if `codegenNativeComponent` was not called
// from a file suffixed with NativeComponent.js.
function codegenNativeComponent<Props: {...}>(
componentName: string,
options?: NativeComponentOptions,
): NativeComponentType<Props> {
if (global.RN$Bridgeless === true && __DEV__) {
console.warn(
`Codegen didn't run for ${componentName}. This will be an error in the future. Make sure you are using @react-native/babel-preset when building your JavaScript code.`,
);
}
let componentNameInUse =
options && options.paperComponentName != null
? options.paperComponentName
: componentName;
if (options != null && options.paperComponentNameDeprecated != null) {
if (UIManager.hasViewManagerConfig(componentName)) {
componentNameInUse = componentName;
} else if (
options.paperComponentNameDeprecated != null &&
UIManager.hasViewManagerConfig(options.paperComponentNameDeprecated)
) {
// $FlowFixMe[incompatible-type]
componentNameInUse = options.paperComponentNameDeprecated;
} else {
throw new Error(
`Failed to find native component for either ${componentName} or ${
options.paperComponentNameDeprecated ?? '(unknown)'
}`,
);
}
}
return (requireNativeComponent<Props>(
// $FlowFixMe[incompatible-type]
componentNameInUse,
): HostComponent<Props>);
}
export default codegenNativeComponent;

View File

@@ -0,0 +1,329 @@
/**
* 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
* @format
*/
import type {
Extras,
ExtraValue,
IPerformanceLogger,
Timespan,
} from './IPerformanceLogger';
const PRINT_TO_CONSOLE: false = false; // Type as false to prevent accidentally committing `true`;
export const getCurrentTimestamp: () => number =
global.nativeQPLTimestamp ?? (() => global.performance.now());
class PerformanceLogger implements IPerformanceLogger {
_timespans: {[key: string]: ?Timespan} = {};
_extras: {[key: string]: ?ExtraValue} = {};
_points: {[key: string]: ?number} = {};
_pointExtras: {[key: string]: ?Extras, ...} = {};
_closed: boolean = false;
addTimespan(
key: string,
startTime: number,
endTime: number,
startExtras?: Extras,
endExtras?: Extras,
) {
if (this._closed) {
/* $FlowFixMe[constant-condition] Error discovered during Constant
* Condition roll out. See https://fburl.com/workplace/1v97vimq. */
if (PRINT_TO_CONSOLE && __DEV__) {
console.log(
'PerformanceLogger: addTimespan - has closed ignoring: ',
key,
);
}
return;
}
if (this._timespans[key]) {
/* $FlowFixMe[constant-condition] Error discovered during Constant
* Condition roll out. See https://fburl.com/workplace/1v97vimq. */
if (PRINT_TO_CONSOLE && __DEV__) {
console.log(
'PerformanceLogger: Attempting to add a timespan that already exists ',
key,
);
}
return;
}
this._timespans[key] = {
startTime,
endTime,
totalTime: endTime - (startTime || 0),
startExtras,
endExtras,
};
}
append(performanceLogger: IPerformanceLogger) {
this._timespans = {
...performanceLogger.getTimespans(),
...this._timespans,
};
this._extras = {...performanceLogger.getExtras(), ...this._extras};
this._points = {...performanceLogger.getPoints(), ...this._points};
this._pointExtras = {
...performanceLogger.getPointExtras(),
...this._pointExtras,
};
}
clear() {
this._timespans = {};
this._extras = {};
this._points = {};
/* $FlowFixMe[constant-condition] Error discovered during Constant
* Condition roll out. See https://fburl.com/workplace/1v97vimq. */
if (PRINT_TO_CONSOLE) {
console.log('PerformanceLogger.js', 'clear');
}
}
clearCompleted() {
for (const key in this._timespans) {
if (this._timespans[key]?.totalTime != null) {
delete this._timespans[key];
}
}
this._extras = {};
this._points = {};
/* $FlowFixMe[constant-condition] Error discovered during Constant
* Condition roll out. See https://fburl.com/workplace/1v97vimq. */
if (PRINT_TO_CONSOLE) {
console.log('PerformanceLogger.js', 'clearCompleted');
}
}
close() {
this._closed = true;
}
currentTimestamp(): number {
return getCurrentTimestamp();
}
getExtras(): {[key: string]: ?ExtraValue} {
return this._extras;
}
getPoints(): {[key: string]: ?number} {
return this._points;
}
getPointExtras(): {[key: string]: ?Extras} {
return this._pointExtras;
}
getTimespans(): {[key: string]: ?Timespan} {
return this._timespans;
}
hasTimespan(key: string): boolean {
return !!this._timespans[key];
}
isClosed(): boolean {
return this._closed;
}
logEverything() {
/* $FlowFixMe[constant-condition] Error discovered during Constant
* Condition roll out. See https://fburl.com/workplace/1v97vimq. */
if (PRINT_TO_CONSOLE) {
// log timespans
for (const key in this._timespans) {
if (this._timespans[key]?.totalTime != null) {
console.log(key + ': ' + this._timespans[key].totalTime + 'ms');
}
}
// log extras
console.log(this._extras);
// log points
for (const key in this._points) {
if (this._points[key] != null) {
console.log(key + ': ' + this._points[key] + 'ms');
}
}
}
}
markPoint(
key: string,
timestamp?: number = getCurrentTimestamp(),
extras?: Extras,
) {
if (this._closed) {
/* $FlowFixMe[constant-condition] Error discovered during Constant
* Condition roll out. See https://fburl.com/workplace/1v97vimq. */
if (PRINT_TO_CONSOLE && __DEV__) {
console.log(
'PerformanceLogger: markPoint - has closed ignoring: ',
key,
);
}
return;
}
if (this._points[key] != null) {
/* $FlowFixMe[constant-condition] Error discovered during Constant
* Condition roll out. See https://fburl.com/workplace/1v97vimq. */
if (PRINT_TO_CONSOLE && __DEV__) {
console.log(
'PerformanceLogger: Attempting to mark a point that has been already logged ',
key,
);
}
return;
}
this._points[key] = timestamp;
if (extras) {
this._pointExtras[key] = extras;
}
}
removeExtra(key: string): ?ExtraValue {
const value = this._extras[key];
delete this._extras[key];
return value;
}
setExtra(key: string, value: ExtraValue) {
if (this._closed) {
/* $FlowFixMe[constant-condition] Error discovered during Constant
* Condition roll out. See https://fburl.com/workplace/1v97vimq. */
if (PRINT_TO_CONSOLE && __DEV__) {
console.log('PerformanceLogger: setExtra - has closed ignoring: ', key);
}
return;
}
if (this._extras.hasOwnProperty(key)) {
/* $FlowFixMe[constant-condition] Error discovered during Constant
* Condition roll out. See https://fburl.com/workplace/1v97vimq. */
if (PRINT_TO_CONSOLE && __DEV__) {
console.log(
'PerformanceLogger: Attempting to set an extra that already exists ',
{key, currentValue: this._extras[key], attemptedValue: value},
);
}
return;
}
this._extras[key] = value;
}
startTimespan(
key: string,
timestamp?: number = getCurrentTimestamp(),
extras?: Extras,
) {
if (this._closed) {
/* $FlowFixMe[constant-condition] Error discovered during Constant
* Condition roll out. See https://fburl.com/workplace/1v97vimq. */
if (PRINT_TO_CONSOLE && __DEV__) {
console.log(
'PerformanceLogger: startTimespan - has closed ignoring: ',
key,
);
}
return;
}
if (this._timespans[key]) {
/* $FlowFixMe[constant-condition] Error discovered during Constant
* Condition roll out. See https://fburl.com/workplace/1v97vimq. */
if (PRINT_TO_CONSOLE && __DEV__) {
console.log(
'PerformanceLogger: Attempting to start a timespan that already exists ',
key,
);
}
return;
}
this._timespans[key] = {
startTime: timestamp,
startExtras: extras,
};
/* $FlowFixMe[constant-condition] Error discovered during Constant
* Condition roll out. See https://fburl.com/workplace/1v97vimq. */
if (PRINT_TO_CONSOLE) {
console.log('PerformanceLogger.js', 'start: ' + key);
}
}
stopTimespan(
key: string,
timestamp?: number = getCurrentTimestamp(),
extras?: Extras,
) {
if (this._closed) {
/* $FlowFixMe[constant-condition] Error discovered during Constant
* Condition roll out. See https://fburl.com/workplace/1v97vimq. */
if (PRINT_TO_CONSOLE && __DEV__) {
console.log(
'PerformanceLogger: stopTimespan - has closed ignoring: ',
key,
);
}
return;
}
const timespan = this._timespans[key];
if (!timespan || timespan.startTime == null) {
/* $FlowFixMe[constant-condition] Error discovered during Constant
* Condition roll out. See https://fburl.com/workplace/1v97vimq. */
if (PRINT_TO_CONSOLE && __DEV__) {
console.log(
'PerformanceLogger: Attempting to end a timespan that has not started ',
key,
);
}
return;
}
if (timespan.endTime != null) {
/* $FlowFixMe[constant-condition] Error discovered during Constant
* Condition roll out. See https://fburl.com/workplace/1v97vimq. */
if (PRINT_TO_CONSOLE && __DEV__) {
console.log(
'PerformanceLogger: Attempting to end a timespan that has already ended ',
key,
);
}
return;
}
timespan.endExtras = extras;
timespan.endTime = timestamp;
timespan.totalTime = timespan.endTime - (timespan.startTime || 0);
/* $FlowFixMe[constant-condition] Error discovered during Constant
* Condition roll out. See https://fburl.com/workplace/1v97vimq. */
if (PRINT_TO_CONSOLE) {
console.log('PerformanceLogger.js', 'end: ' + key);
}
}
}
// Re-exporting for backwards compatibility with all the clients that
// may still import it from this module.
export type {Extras, ExtraValue, IPerformanceLogger, Timespan};
/**
* This function creates performance loggers that can be used to collect and log
* various performance data such as timespans, points and extras.
* The loggers need to have minimal overhead since they're used in production.
*/
export default function createPerformanceLogger(): IPerformanceLogger {
return new PerformanceLogger();
}

View File

@@ -0,0 +1,92 @@
/**
* 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
* @format
*/
'use strict';
/**
* If your application is accepting different values for the same field over
* time and is doing a diff on them, you can either (1) create a copy or
* (2) ensure that those values are not mutated behind two passes.
* This function helps you with (2) by freezing the object and throwing if
* the user subsequently modifies the value.
*
* There are two caveats with this function:
* - If the call site is not in strict mode, it will only throw when
* mutating existing fields, adding a new one
* will unfortunately fail silently :(
* - If the object is already frozen or sealed, it will not continue the
* deep traversal and will leave leaf nodes unfrozen.
*
* Freezing the object and adding the throw mechanism is expensive and will
* only be used in DEV.
*/
function deepFreezeAndThrowOnMutationInDev<T: {...} | Array<mixed>>(
object: T,
): T {
if (__DEV__) {
if (
typeof object !== 'object' ||
/* $FlowFixMe[invalid-compare] Error discovered during Constant Condition
* roll out. See https://fburl.com/workplace/5whu3i34. */
object === null ||
Object.isFrozen(object) ||
Object.isSealed(object)
) {
return object;
}
// $FlowFixMe[not-an-object] `object` can be an array, but Object.keys works with arrays too
const keys = Object.keys((object: {...} | Array<mixed>));
// $FlowFixMe[method-unbinding] added when improving typing for this parameters
const hasOwnProperty = Object.prototype.hasOwnProperty;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (hasOwnProperty.call(object, key)) {
Object.defineProperty(object, key, {
get: identity.bind(null, object[key]),
});
Object.defineProperty(object, key, {
set: throwOnImmutableMutation.bind(null, key),
});
}
}
Object.freeze(object);
Object.seal(object);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (hasOwnProperty.call(object, key)) {
deepFreezeAndThrowOnMutationInDev(object[key]);
}
}
}
return object;
}
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
* LTI update could not be added via codemod */
function throwOnImmutableMutation(key: empty, value) {
throw Error(
'You attempted to set the key `' +
key +
'` with the value `' +
JSON.stringify(value) +
'` on an object that is meant to be immutable ' +
'and has been frozen.',
);
}
function identity(value: mixed) {
return value;
}
export default deepFreezeAndThrowOnMutationInDev;

View File

@@ -0,0 +1,66 @@
/**
* 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
* @format
*/
'use strict';
/**
* Defines a lazily evaluated property on the supplied `object`.
*/
function defineLazyObjectProperty<T>(
object: interface {},
name: string,
descriptor: {
get: () => T,
enumerable?: boolean,
writable?: boolean,
...
},
): void {
const {get} = descriptor;
const enumerable = descriptor.enumerable !== false;
const writable = descriptor.writable !== false;
let value;
let valueSet = false;
function getValue(): T {
// WORKAROUND: A weird infinite loop occurs where calling `getValue` calls
// `setValue` which calls `Object.defineProperty` which somehow triggers
// `getValue` again. Adding `valueSet` breaks this loop.
if (!valueSet) {
// Calling `get()` here can trigger an infinite loop if it fails to
// remove the getter on the property, which can happen when executing
// JS in a V8 context. `valueSet = true` will break this loop, and
// sets the value of the property to undefined, until the code in `get()`
// finishes, at which point the property is set to the correct value.
valueSet = true;
setValue(get());
}
return value;
}
function setValue(newValue: T): void {
value = newValue;
valueSet = true;
Object.defineProperty(object, name, {
value: newValue,
configurable: true,
enumerable,
writable,
});
}
Object.defineProperty(object, name, {
get: getValue,
set: setValue,
configurable: true,
enumerable,
});
}
export default defineLazyObjectProperty;

View File

@@ -0,0 +1,101 @@
/**
* 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
* @format
*/
'use strict';
let logListeners;
type LogListeners = {
+onDifferentFunctionsIgnored: (nameOne: ?string, nameTwo: ?string) => void,
};
type Options = {+unsafelyIgnoreFunctions?: boolean};
function unstable_setLogListeners(listeners: ?LogListeners) {
logListeners = listeners;
}
/*
* @returns {bool} true if different, false if equal
*/
function deepDiffer(
one: any,
two: any,
maxDepthOrOptions: Options | number = -1,
maybeOptions?: Options,
): boolean {
const options =
typeof maxDepthOrOptions === 'number' ? maybeOptions : maxDepthOrOptions;
const maxDepth =
typeof maxDepthOrOptions === 'number' ? maxDepthOrOptions : -1;
if (maxDepth === 0) {
return true;
}
if (one === two) {
// Short circuit on identical object references instead of traversing them.
return false;
}
if (typeof one === 'function' && typeof two === 'function') {
// We consider all functions equal unless explicitly configured otherwise
let unsafelyIgnoreFunctions = options?.unsafelyIgnoreFunctions;
if (unsafelyIgnoreFunctions == null) {
if (
logListeners &&
logListeners.onDifferentFunctionsIgnored &&
(!options || !('unsafelyIgnoreFunctions' in options))
) {
logListeners.onDifferentFunctionsIgnored(one.name, two.name);
}
unsafelyIgnoreFunctions = true;
}
return !unsafelyIgnoreFunctions;
}
if (typeof one !== 'object' || one === null) {
// Primitives can be directly compared
return one !== two;
}
if (typeof two !== 'object' || two === null) {
// We know they are different because the previous case would have triggered
// otherwise.
return true;
}
if (one.constructor !== two.constructor) {
return true;
}
if (Array.isArray(one)) {
// We know two is also an array because the constructors are equal
const len = one.length;
if (two.length !== len) {
return true;
}
for (let ii = 0; ii < len; ii++) {
if (deepDiffer(one[ii], two[ii], maxDepth - 1, options)) {
return true;
}
}
} else {
for (const key in one) {
if (deepDiffer(one[key], two[key], maxDepth - 1, options)) {
return true;
}
}
for (const twoKey in two) {
// The only case we haven't checked yet is keys that are in two but aren't
// in one, which means they are different.
if (one[twoKey] === undefined && two[twoKey] !== undefined) {
return true;
}
}
}
return false;
}
deepDiffer.unstable_setLogListeners = unstable_setLogListeners;
export default deepDiffer;

View File

@@ -0,0 +1,40 @@
/**
* 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
* @format
*/
'use strict';
type Inset = {
top: ?number,
left: ?number,
right: ?number,
bottom: ?number,
...
};
const dummyInsets = {
top: undefined,
left: undefined,
right: undefined,
bottom: undefined,
};
function insetsDiffer(one: Inset, two: Inset): boolean {
one = one || dummyInsets;
two = two || dummyInsets;
return (
one !== two &&
(one.top !== two.top ||
one.left !== two.left ||
one.right !== two.right ||
one.bottom !== two.bottom)
);
}
export default insetsDiffer;

View File

@@ -0,0 +1,48 @@
/**
* 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
* @format
*/
'use strict';
/**
* Unrolls an array comparison specially for matrices. Prioritizes
* checking of indices that are most likely to change so that the comparison
* bails as early as possible.
*
* @param {MatrixMath.Matrix} one First matrix.
* @param {MatrixMath.Matrix} two Second matrix.
* @return {boolean} Whether or not the two matrices differ.
*/
function matricesDiffer(one: ?Array<number>, two: ?Array<number>): boolean {
if (one === two) {
return false;
}
return (
!one ||
!two ||
one[12] !== two[12] ||
one[13] !== two[13] ||
one[14] !== two[14] ||
one[5] !== two[5] ||
one[10] !== two[10] ||
one[0] !== two[0] ||
one[1] !== two[1] ||
one[2] !== two[2] ||
one[3] !== two[3] ||
one[4] !== two[4] ||
one[6] !== two[6] ||
one[7] !== two[7] ||
one[8] !== two[8] ||
one[9] !== two[9] ||
one[11] !== two[11] ||
one[15] !== two[15]
);
}
export default matricesDiffer;

View File

@@ -0,0 +1,27 @@
/**
* 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
* @format
*/
'use strict';
type Point = {
x: ?number,
y: ?number,
...
};
const dummyPoint: Point = {x: undefined, y: undefined};
function pointsDiffer(one: ?Point, two: ?Point): boolean {
one = one || dummyPoint;
two = two || dummyPoint;
return one !== two && (one.x !== two.x || one.y !== two.y);
}
export default pointsDiffer;

View File

@@ -0,0 +1,26 @@
/**
* 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
* @format
*/
'use strict';
const dummySize = {width: undefined, height: undefined};
type Size = {width: ?number, height: ?number};
function sizesDiffer(one: Size, two: Size): boolean {
const defaultedOne = one || dummySize;
const defaultedTwo = two || dummySize;
return (
defaultedOne !== defaultedTwo &&
(defaultedOne.width !== defaultedTwo.width ||
defaultedOne.height !== defaultedTwo.height)
);
}
export default sizesDiffer;

View File

@@ -0,0 +1,22 @@
/**
* 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
*/
// This function dismisses the currently-open keyboard, if any.
'use strict';
const TextInputState =
require('../Components/TextInput/TextInputState').default;
function dismissKeyboard() {
TextInputState.blurTextInput(TextInputState.currentlyFocusedInput());
}
export default dismissKeyboard;

View File

@@ -0,0 +1,27 @@
/**
* 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
* @format
*/
'use strict';
/**
* Small utility that can be used as an error handler. You cannot just pass
* `console.error` as a failure callback - it's not properly bound. If passes an
* `Error` object, it will print the message and stack.
*/
const logError = function (...args: $ReadOnlyArray<mixed>) {
if (args.length === 1 && args[0] instanceof Error) {
const err = args[0];
console.error('Error: "' + err.message + '". Stack:\n' + err.stack);
} else {
console.error.apply(console, args);
}
};
export default logError;

View File

@@ -0,0 +1,28 @@
/**
* 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
* @format
*/
'use strict';
function mapWithSeparator<TFrom, TTo>(
items: Array<TFrom>,
itemRenderer: (item: TFrom, index: number, items: Array<TFrom>) => TTo,
spacerRenderer: (index: number) => TTo,
): Array<TTo> {
const mapped = [];
if (items.length > 0) {
mapped.push(itemRenderer(items[0], 0, items));
for (let ii = 1; ii < items.length; ii++) {
mapped.push(spacerRenderer(ii - 1), itemRenderer(items[ii], ii, items));
}
}
return mapped;
}
export default mapWithSeparator;

View File

@@ -0,0 +1,121 @@
/**
* 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
* @format
*/
import invariant from 'invariant';
/**
* Tries to stringify with JSON.stringify and toString, but catches exceptions
* (e.g. from circular objects) and always returns a string and never throws.
*/
export function createStringifySafeWithLimits(limits: {
maxDepth?: number,
maxStringLimit?: number,
maxArrayLimit?: number,
maxObjectKeysLimit?: number,
}): mixed => string {
const {
maxDepth = Number.POSITIVE_INFINITY,
maxStringLimit = Number.POSITIVE_INFINITY,
maxArrayLimit = Number.POSITIVE_INFINITY,
maxObjectKeysLimit = Number.POSITIVE_INFINITY,
} = limits;
const stack: Array<mixed> = [];
/* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by
* Flow's LTI update could not be added via codemod */
function replacer(key: string, value: mixed): mixed {
while (stack.length && this !== stack[0]) {
stack.shift();
}
if (typeof value === 'string') {
const truncatedString = '...(truncated)...';
if (value.length > maxStringLimit + truncatedString.length) {
return value.substring(0, maxStringLimit) + truncatedString;
}
return value;
}
if (typeof value !== 'object' || value === null) {
return value;
}
let retval: mixed = value;
if (Array.isArray(value)) {
if (stack.length >= maxDepth) {
retval = `[ ... array with ${value.length} values ... ]`;
} else if (value.length > maxArrayLimit) {
retval = value
.slice(0, maxArrayLimit)
.concat([
`... extra ${value.length - maxArrayLimit} values truncated ...`,
]);
}
} else {
// Add refinement after Array.isArray call.
invariant(typeof value === 'object', 'This was already found earlier');
let keys = Object.keys(value);
if (stack.length >= maxDepth) {
retval = `{ ... object with ${keys.length} keys ... }`;
} else if (keys.length > maxObjectKeysLimit) {
// Return a sample of the keys.
retval = ({}: {[string]: mixed});
for (let k of keys.slice(0, maxObjectKeysLimit)) {
retval[k] = value[k];
}
const truncatedKey = '...(truncated keys)...';
retval[truncatedKey] = keys.length - maxObjectKeysLimit;
}
}
stack.unshift(retval);
return retval;
}
return function stringifySafe(arg: mixed): string {
if (arg === undefined) {
return 'undefined';
} else if (arg === null) {
return 'null';
} else if (typeof arg === 'function') {
try {
return arg.toString();
} catch (e) {
return '[function unknown]';
}
} else if (arg instanceof Error) {
return arg.name + ': ' + arg.message;
} else {
// Perform a try catch, just in case the object has a circular
// reference or stringify throws for some other reason.
try {
const ret = JSON.stringify(arg, replacer);
if (ret === undefined) {
return '["' + typeof arg + '" failed to stringify]';
}
return ret;
} catch (e) {
if (typeof arg.toString === 'function') {
try {
// $FlowFixMe[incompatible-use] : toString shouldn't take any arguments in general.
return arg.toString();
} catch (E) {}
}
}
}
return '["' + typeof arg + '" failed to stringify]';
};
}
const stringifySafe: mixed => string = createStringifySafeWithLimits({
maxDepth: 10,
maxStringLimit: 100,
maxArrayLimit: 50,
maxObjectKeysLimit: 50,
});
export default stringifySafe;

View File

@@ -0,0 +1,22 @@
/**
* 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
* @format
*/
export default function stringifyViewConfig(viewConfig: any): string {
return JSON.stringify(
viewConfig,
(key, val) => {
if (typeof val === 'function') {
return `ƒ ${val.name}`;
}
return val;
},
2,
);
}

View File

@@ -0,0 +1,25 @@
/**
* 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';
import type {ColorSchemeName} from './NativeAppearance';
import {addChangeListener, getColorScheme} from './Appearance';
import {useSyncExternalStore} from 'react';
const subscribe = (onStoreChange: () => void) => {
const appearanceSubscription = addChangeListener(onStoreChange);
return () => appearanceSubscription.remove();
};
export default function useColorScheme(): ?ColorSchemeName {
return useSyncExternalStore(subscribe, getColorScheme);
}

View File

@@ -0,0 +1,59 @@
/**
* 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
* @format
*/
import useRefEffect from './useRefEffect';
import * as React from 'react';
import {useCallback} from 'react';
/**
* Constructs a new ref that forwards new values to each of the given refs. The
* given refs will always be invoked in the order that they are supplied.
*
* WARNING: A known problem of merging refs using this approach is that if any
* of the given refs change, the returned callback ref will also be changed. If
* the returned callback ref is supplied as a `ref` to a React element, this may
* lead to problems with the given refs being invoked more times than desired.
*/
export default function useMergeRefs<Instance>(
...refs: $ReadOnlyArray<?React.RefSetter<Instance>>
): React.RefSetter<Instance> {
const refEffect = useCallback(
(current: Instance) => {
const cleanups: $ReadOnlyArray<void | (() => void)> = refs.map(ref => {
if (ref == null) {
return undefined;
} else {
if (typeof ref === 'function') {
// $FlowFixMe[incompatible-type] - Flow does not understand ref cleanup.
const cleanup: void | (() => void) = ref(current);
return typeof cleanup === 'function'
? cleanup
: () => {
ref(null);
};
} else {
ref.current = current;
return () => {
ref.current = null;
};
}
}
});
return () => {
for (const cleanup of cleanups) {
cleanup?.();
}
};
},
[...refs], // eslint-disable-line react-hooks/exhaustive-deps
);
return useRefEffect(refEffect);
}

View File

@@ -0,0 +1,45 @@
/**
* 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
* @format
*/
import {useCallback, useRef} from 'react';
type CallbackRef<T> = T => mixed;
/**
* Constructs a callback ref that provides similar semantics as `useEffect`. The
* supplied `effect` callback will be called with non-null component instances.
* The `effect` callback can also optionally return a cleanup function.
*
* When a component is updated or unmounted, the cleanup function is called. The
* `effect` callback will then be called again, if applicable.
*
* When a new `effect` callback is supplied, the previously returned cleanup
* function will be called before the new `effect` callback is called with the
* same instance.
*
* WARNING: The `effect` callback should be stable (e.g. using `useCallback`).
*/
export default function useRefEffect<TInstance>(
effect: TInstance => (() => void) | void,
): CallbackRef<TInstance | null> {
const cleanupRef = useRef<(() => void) | void>(undefined);
return useCallback(
(instance: null | TInstance) => {
if (cleanupRef.current) {
cleanupRef.current();
cleanupRef.current = undefined;
}
if (instance != null) {
cleanupRef.current = effect(instance);
}
},
[effect],
);
}

View File

@@ -0,0 +1,47 @@
/**
* 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
*/
import Dimensions from './Dimensions';
import {
type DisplayMetrics,
type DisplayMetricsAndroid,
} from './NativeDeviceInfo';
import {useEffect, useState} from 'react';
export default function useWindowDimensions():
| DisplayMetrics
| DisplayMetricsAndroid {
const [dimensions, setDimensions] = useState(() => Dimensions.get('window'));
useEffect(() => {
function handleChange({
window,
}: {
window: DisplayMetrics | DisplayMetricsAndroid,
}) {
if (
dimensions.width !== window.width ||
dimensions.height !== window.height ||
dimensions.scale !== window.scale ||
dimensions.fontScale !== window.fontScale
) {
setDimensions(window);
}
}
const subscription = Dimensions.addEventListener('change', handleChange);
// We might have missed an update between calling `get` in render and
// `addEventListener` in this handler, so we set it here. If there was
// no change, React will filter out this update as a no-op.
handleChange({window: Dimensions.get('window')});
return () => {
subscription.remove();
};
}, [dimensions]);
return dimensions;
}

View File

@@ -0,0 +1,32 @@
/**
* 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
* @format
*/
'use strict';
const warnedKeys: {[string]: boolean, ...} = {};
/**
* A simple function that prints a warning message once per session.
*
* @param {string} key - The key used to ensure the message is printed once.
* This should be unique to the callsite.
* @param {string} message - The message to print
*/
function warnOnce(key: string, message: string) {
if (warnedKeys[key]) {
return;
}
console.warn(message);
warnedKeys[key] = true;
}
export default warnOnce;