feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,53 @@
/**
* 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' | null | undefined;
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;
/**
* 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 | null | undefined,
): 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,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
* @flow strict-local
*/
import NativeEventEmitter from '../EventEmitter/NativeEventEmitter';
import Platform from '../Utilities/Platform';
import EventEmitter, {
type EventSubscription,
} from '../vendor/emitter/EventEmitter';
import {isAsyncDebugging} from './DebugEnvironment';
import NativeAppearance, {
type AppearancePreferences,
type ColorSchemeName,
} from './NativeAppearance';
import invariant from 'invariant';
type AppearanceListener = (preferences: AppearancePreferences) => void;
const eventEmitter = new EventEmitter<{
change: [AppearancePreferences],
}>();
type NativeAppearanceEventDefinitions = {
appearanceChanged: [AppearancePreferences],
};
if (NativeAppearance) {
const nativeEventEmitter =
new NativeEventEmitter<NativeAppearanceEventDefinitions>(
// 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 : NativeAppearance,
);
nativeEventEmitter.addListener(
'appearanceChanged',
(newAppearance: AppearancePreferences) => {
const {colorScheme} = newAppearance;
invariant(
colorScheme === 'dark' ||
colorScheme === 'light' ||
colorScheme == null,
"Unrecognized color scheme. Did you mean 'dark' or 'light'?",
);
eventEmitter.emit('change', {colorScheme});
},
);
}
module.exports = {
/**
* 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();`
*
* @returns {?ColorSchemeName} Value for the color scheme preference.
*/
getColorScheme(): ?ColorSchemeName {
if (__DEV__) {
if (isAsyncDebugging) {
// Hard code light theme when using the async debugger as
// sync calls aren't supported
return 'light';
}
}
// TODO: (hramos) T52919652 Use ?ColorSchemeName once codegen supports union
const nativeColorScheme: ?string =
NativeAppearance == null
? null
: NativeAppearance.getColorScheme() || null;
invariant(
nativeColorScheme === 'dark' ||
nativeColorScheme === 'light' ||
nativeColorScheme == null,
"Unrecognized color scheme. Did you mean 'dark' or 'light'?",
);
return nativeColorScheme;
},
setColorScheme(colorScheme: ?ColorSchemeName): void {
const nativeColorScheme = colorScheme == null ? 'unspecified' : colorScheme;
invariant(
colorScheme === 'dark' || colorScheme === 'light' || colorScheme == null,
"Unrecognized color scheme. Did you mean 'dark', 'light' or null?",
);
if (NativeAppearance != null && NativeAppearance.setColorScheme != null) {
NativeAppearance.setColorScheme(nativeColorScheme);
}
},
/**
* Add an event handler that is fired when appearance preferences change.
*/
addChangeListener(listener: AppearanceListener): EventSubscription {
return eventEmitter.addListener('change', listener);
},
};

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.
*
* @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';
const _backPressSubscriptions = [];
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: () => ?boolean,
) => {remove: () => void, ...},
+removeEventListener: (
eventName: BackPressEventName,
handler: () => ?boolean,
) => 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: () => ?boolean,
): {remove: () => void, ...} {
if (_backPressSubscriptions.indexOf(handler) === -1) {
_backPressSubscriptions.push(handler);
}
return {
remove: (): void => BackHandler.removeEventListener(eventName, handler),
};
},
/**
* Removes the event handler.
*/
removeEventListener: function (
eventName: BackPressEventName,
handler: () => ?boolean,
): void {
const index = _backPressSubscriptions.indexOf(handler);
if (index !== -1) {
_backPressSubscriptions.splice(index, 1);
}
},
};
module.exports = BackHandler;

View File

@@ -0,0 +1,38 @@
/**
* 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;
removeEventListener(
eventName: BackPressEventName,
handler: () => boolean | null | undefined,
): void;
}
export const BackHandler: BackHandlerStatic;
export type BackHandler = BackHandlerStatic;

View File

@@ -0,0 +1,41 @@
/**
* 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
* @flow
*/
'use strict';
module.exports = require('../Components/UnimplementedViews/UnimplementedView');
type BackPressEventName = 'backPress' | 'hardwareBackPress';
function emptyFunction(): void {}
type TBackHandler = {|
+exitApp: () => void,
+addEventListener: (
eventName: BackPressEventName,
handler: () => ?boolean,
) => {remove: () => void, ...},
+removeEventListener: (
eventName: BackPressEventName,
handler: () => ?boolean,
) => void,
|};
let BackHandler: TBackHandler = {
exitApp: emptyFunction,
addEventListener(_eventName: BackPressEventName, _handler: Function) {
return {
remove: emptyFunction,
};
},
removeEventListener(_eventName: BackPressEventName, _handler: Function) {},
};
module.exports = BackHandler;

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.
*
* @format
* @flow
*/
'use strict';
type BackPressEventName = 'backPress' | 'hardwareBackPress';
type TBackHandler = {|
+exitApp: () => void,
+addEventListener: (
eventName: BackPressEventName,
handler: () => ?boolean,
) => {remove: () => void, ...},
+removeEventListener: (
eventName: BackPressEventName,
handler: () => ?boolean,
) => void,
|};
declare module.exports: TBackHandler;

View File

@@ -0,0 +1,16 @@
/**
* 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
* @flow strict
*/
export let isAsyncDebugging: boolean = false;
if (__DEV__) {
// These native interfaces don't exist in asynchronous debugging environments.
isAsyncDebugging = !global.nativeCallSyncHook && !global.RN$Bridgeless;
}

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,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 type {EventSubscription} from '../vendor/emitter/EventEmitter';
import NativeEventEmitter from '../EventEmitter/NativeEventEmitter';
import NativeDevSettings from '../NativeModules/specs/NativeDevSettings';
import Platform from '../Utilities/Platform';
let DevSettings: {
addMenuItem(title: string, handler: () => mixed): void,
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?.();
},
};
}
module.exports = DevSettings;

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.
*
* @format
* @flow strict-local
*/
import NativeDeviceInfo from './NativeDeviceInfo';
module.exports = 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,122 @@
/**
* 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
* @flow
*/
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';
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 {
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,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
* @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.
*/
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.
*/
function hasNativeConstructor(o: Object, expectedName: string): boolean {
const con = Object.getPrototypeOf(o).constructor;
return con.name === expectedName && isNativeFunction(con);
}
module.exports = {isNativeFunction, hasNativeConstructor};

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();
module.exports = GlobalPerformanceLogger;

View File

@@ -0,0 +1,361 @@
/**
* 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
* @flow strict-local
*/
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');
const Platform = require('./Platform');
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 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 LoadingView = require('./LoadingView');
// 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) {
LoadingView.showMessage('Refreshing...', 'refresh');
}
try {
hmrClient.enable();
} finally {
if (hasUpdates) {
LoadingView.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(hmrClient, 'Expected HMRClient.setup() call at startup.');
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,
mode: global.RN$Bridgeless === true ? 'NOBRIDGE' : 'BRIDGE',
data: data.map(item =>
typeof item === 'string'
? item
: prettyFormat(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 LoadingView = require('./LoadingView');
const serverHost = port !== null && port !== '' ? `${host}:${port}` : host;
const serverScheme = scheme;
const client = new MetroHMRClient(`${serverScheme}://${serverHost}/hot`);
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);
});
client.on('update-start', ({isInitialUpdate}) => {
currentCompileErrorMessage = null;
didConnect = true;
if (client.isEnabled() && !isInitialUpdate) {
LoadingView.showMessage('Refreshing...', 'refresh');
}
});
client.on('update', ({isInitialUpdate}) => {
if (client.isEnabled() && !isInitialUpdate) {
dismissRedbox();
LogBox.clearAllLogs();
}
});
client.on('update-done', () => {
LoadingView.hide();
});
client.on('error', data => {
LoadingView.hide();
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 => {
LoadingView.hide();
// 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;
// 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) {
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 */
const error: ExtendedError = new Error(message);
// Symbolicating compile errors is wasted effort
// because the stack trace is meaningless:
error.preventSymbolication = true;
throw error;
}
module.exports = 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.
*
* @format
* @flow strict-local
*/
'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() {},
};
module.exports = 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,44 @@
/**
* 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
* @flow strict-local
*/
import processColor from '../StyleSheet/processColor';
import Appearance from './Appearance';
import NativeDevLoadingView from './NativeDevLoadingView';
module.exports = {
showMessage(message: string, type: 'load' | 'refresh') {
if (NativeDevLoadingView) {
let backgroundColor;
let textColor;
if (type === 'refresh') {
backgroundColor = processColor('#2584e8');
textColor = processColor('#ffffff');
} else if (type === 'load') {
if (Appearance.getColorScheme() === 'dark') {
backgroundColor = processColor('#fafafa');
textColor = processColor('#242526');
} else {
backgroundColor = processColor('#404040');
textColor = processColor('#ffffff');
}
}
NativeDevLoadingView.showMessage(
message,
typeof textColor === 'number' ? textColor : null,
typeof backgroundColor === 'number' ? backgroundColor : null,
);
}
},
hide() {
NativeDevLoadingView && NativeDevLoadingView.hide();
},
};

View File

@@ -0,0 +1,50 @@
/**
* 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
* @flow strict-local
*/
import processColor from '../StyleSheet/processColor';
import Appearance from './Appearance';
import NativeDevLoadingView from './NativeDevLoadingView';
module.exports = {
showMessage(message: string, type: 'load' | 'refresh') {
if (NativeDevLoadingView) {
if (type === 'refresh') {
const backgroundColor = processColor('#2584e8');
const textColor = processColor('#ffffff');
NativeDevLoadingView.showMessage(
message,
typeof textColor === 'number' ? textColor : null,
typeof backgroundColor === 'number' ? backgroundColor : null,
);
} else if (type === 'load') {
let backgroundColor;
let textColor;
if (Appearance.getColorScheme() === 'dark') {
backgroundColor = processColor('#fafafa');
textColor = processColor('#242526');
} else {
backgroundColor = processColor('#404040');
textColor = processColor('#ffffff');
}
NativeDevLoadingView.showMessage(
message,
typeof textColor === 'number' ? textColor : null,
typeof backgroundColor === 'number' ? backgroundColor : null,
);
}
}
},
hide() {
NativeDevLoadingView && NativeDevLoadingView.hide();
},
};

View File

@@ -0,0 +1,16 @@
/**
* 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
* @flow strict-local
*/
'use strict';
module.exports = {
showMessage(message: string, type: 'load' | 'refresh') {},
hide() {},
};

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
*/
export type * from '../../src/private/specs/modules/NativeAppearance';
import NativeAppearance from '../../src/private/specs/modules/NativeAppearance';
export default NativeAppearance;

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
*/
export * from '../../src/private/specs/modules/NativeDevLoadingView';
import NativeDevLoadingView from '../../src/private/specs/modules/NativeDevLoadingView';
export default NativeDevLoadingView;

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
*/
export * from '../../src/private/specs/modules/NativeDeviceInfo';
import NativeDeviceInfo from '../../src/private/specs/modules/NativeDeviceInfo';
export default NativeDeviceInfo;

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
*/
export * from '../../src/private/specs/modules/NativePlatformConstantsAndroid';
import NativePlatformConstantsAndroid from '../../src/private/specs/modules/NativePlatformConstantsAndroid';
export default NativePlatformConstantsAndroid;

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
*/
export * from '../../src/private/specs/modules/NativePlatformConstantsIOS';
import NativePlatformConstantsIOS from '../../src/private/specs/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 {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> =
React.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.
*
* @format
* @flow strict-local
*/
'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,87 @@
/**
* 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
* @flow strict
*/
import type {
Platform as PlatformType,
PlatformSelectSpec,
} from './Platform.flow';
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: ?number,
|},
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-return]
spec.android
: 'native' in spec
? // $FlowFixMe[incompatible-return]
spec.native
: // $FlowFixMe[incompatible-return]
spec.default,
};
module.exports = Platform;

View File

@@ -0,0 +1,106 @@
/**
* 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?: number | 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';
}
export type Platform =
| PlatformIOSStatic
| PlatformAndroidStatic
| PlatformWindowsOSStatic
| PlatformMacOSStatic
| PlatformWebStatic;
export const Platform: Platform;

View File

@@ -0,0 +1,91 @@
/**
* 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 PlatformSelectSpec<T> = {
default?: T,
native?: T,
ios?: T,
android?: T,
...
};
type IOSPlatform = {
__constants: null,
OS: $TEMPORARY$string<'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: ?number,
|},
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: $TEMPORARY$string<'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: ?number,
|},
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,
};
export type Platform = IOSPlatform | AndroidPlatform;

View File

@@ -0,0 +1,88 @@
/**
* 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
* @flow strict
*/
import type {
Platform as PlatformType,
PlatformSelectSpec,
} from './Platform.flow';
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: ?number,
|},
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-return]
'ios' in spec ? spec.ios : 'native' in spec ? spec.native : spec.default,
};
module.exports = 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 {Platform} from './Platform.flow';
declare module.exports: Platform;

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 defineLazyObjectProperty = require('./defineLazyObjectProperty');
/**
* 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
*/
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,
});
}
function polyfillGlobal<T>(name: string, getValue: () => T): void {
polyfillObjectProperty(global, name, getValue);
}
module.exports = {polyfillObjectProperty, polyfillGlobal};

View File

@@ -0,0 +1,55 @@
/**
* 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
* @flow strict
*/
'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 {
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;
},
};
module.exports = 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';
const Switch = require('../Components/Switch/Switch').default;
const TextInput = require('../Components/TextInput/TextInput');
const View = require('../Components/View/View');
const Text = require('../Text/Text');
const {VirtualizedList} = require('@react-native/virtualized-lists');
const React = require('react');
const ShallowRenderer = require('react-shallow-renderer');
const ReactTestRenderer = require('react-test-renderer');
/* $FlowFixMe[not-a-function] (>=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. */
// $FlowFixMe[invalid-constructor]
const shallowRenderer = new ShallowRenderer();
export type ReactTestInstance = $PropertyType<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.
(node.type === Text &&
node.props &&
typeof node.props.onPress === 'function') ||
// note: Special casing <Switch /> since it doesn't use touchable
(node.type === Switch && node.props && node.props.disabled !== true) ||
(node.type === View &&
node?.props?.onStartShouldSetResponder?.testOnly_pressabilityConfig) ||
// 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();
}
});
}
function expectRendersMatchingSnapshot(
name: string,
ComponentProvider: () => React.Element<any>,
unmockComponent: () => mixed,
) {
let instance;
jest.resetAllMocks();
instance = ReactTestRenderer.create(<ComponentProvider />);
expect(instance).toMatchSnapshot(
'should deep render when mocked (please verify output manually)',
);
jest.resetAllMocks();
unmockComponent();
instance = shallowRenderer.render(<ComponentProvider />);
expect(instance).toMatchSnapshot(
`should shallow render as <${name} /> when not mocked`,
);
jest.resetAllMocks();
instance = shallowRenderer.render(<ComponentProvider />);
expect(instance).toMatchSnapshot(
`should shallow render as <${name} /> when mocked`,
);
jest.resetAllMocks();
unmockComponent();
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());
if (touchable.type === Text && touchable.props && touchable.props.onPress) {
touchable.props.onPress();
} 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.
*
* @format
* @flow strict
*/
'use strict';
export type Scene = {name: string, [string]: mixed, ...};
let _listeners: Array<(scene: Scene) => void> = [];
let _activeScene = {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);
},
};
},
};
module.exports = SceneTracker;

View File

@@ -0,0 +1,50 @@
/**
* 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
*/
'use strict';
const _backPressSubscriptions = new Set();
const BackHandler = {
exitApp: jest.fn(),
addEventListener: function (
eventName: BackPressEventName,
handler: () => ?boolean,
): {remove: () => void} {
_backPressSubscriptions.add(handler);
return {
remove: () => BackHandler.removeEventListener(eventName, handler),
};
},
removeEventListener: function (
eventName: BackPressEventName,
handler: () => ?boolean,
): void {
_backPressSubscriptions.delete(handler);
},
mockPressBack: function () {
let invokeDefault = true;
const subscriptions = [..._backPressSubscriptions].reverse();
for (let i = 0; i < subscriptions.length; ++i) {
if (subscriptions[i]()) {
invokeDefault = false;
break;
}
}
if (invokeDefault) {
BackHandler.exitApp();
}
},
};
module.exports = BackHandler;

View File

@@ -0,0 +1,16 @@
/**
* 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
*/
'use strict';
const GlobalPerformanceLogger = jest
.unmock('../createPerformanceLogger')
.genMockFromModule('../GlobalPerformanceLogger');
module.exports = GlobalPerformanceLogger;

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.
*
* @format
*/
'use strict';
const PixelRatio = {
get: jest.fn().mockReturnValue(2),
getFontScale: jest.fn(() => PixelRatio.get()),
getPixelSizeForLayoutSize: jest.fn(layoutSize =>
Math.round(layoutSize * PixelRatio.get()),
),
roundToNearestPixel: jest.fn(layoutSize => {
const ratio = PixelRatio.get();
return Math.round(layoutSize * ratio) / ratio;
}),
startDetecting: jest.fn(),
};
export default PixelRatio;

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.
*
* @format
* @flow strict
*/
'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));
}
module.exports = binaryToBase64;

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

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.
*
* @format
* @flow strict-local
*/
// TODO: move this file to shims/ReactNative (requires React update and sync)
import type {HostComponent} from '../../Libraries/Renderer/shims/ReactNativeTypes';
import requireNativeComponent from '../../Libraries/ReactNative/requireNativeComponent';
import UIManager from '../ReactNative/UIManager';
// TODO: import from CodegenSchema once workspaces are enabled
type Options = $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?: Options,
): 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-call]
componentNameInUse,
): HostComponent<Props>);
}
export default codegenNativeComponent;

View File

@@ -0,0 +1,299 @@
/**
* 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';
import * as Systrace from '../Performance/Systrace';
import infoLog from './infoLog';
const _cookies: {[key: string]: number, ...} = {};
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) {
if (PRINT_TO_CONSOLE && __DEV__) {
infoLog('PerformanceLogger: addTimespan - has closed ignoring: ', key);
}
return;
}
if (this._timespans[key]) {
if (PRINT_TO_CONSOLE && __DEV__) {
infoLog(
'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 = {};
if (PRINT_TO_CONSOLE) {
infoLog('PerformanceLogger.js', 'clear');
}
}
clearCompleted() {
for (const key in this._timespans) {
if (this._timespans[key]?.totalTime != null) {
delete this._timespans[key];
}
}
this._extras = {};
this._points = {};
if (PRINT_TO_CONSOLE) {
infoLog('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() {
if (PRINT_TO_CONSOLE) {
// log timespans
for (const key in this._timespans) {
if (this._timespans[key]?.totalTime != null) {
infoLog(key + ': ' + this._timespans[key].totalTime + 'ms');
}
}
// log extras
infoLog(this._extras);
// log points
for (const key in this._points) {
if (this._points[key] != null) {
infoLog(key + ': ' + this._points[key] + 'ms');
}
}
}
}
markPoint(
key: string,
timestamp?: number = getCurrentTimestamp(),
extras?: Extras,
) {
if (this._closed) {
if (PRINT_TO_CONSOLE && __DEV__) {
infoLog('PerformanceLogger: markPoint - has closed ignoring: ', key);
}
return;
}
if (this._points[key] != null) {
if (PRINT_TO_CONSOLE && __DEV__) {
infoLog(
'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) {
if (PRINT_TO_CONSOLE && __DEV__) {
infoLog('PerformanceLogger: setExtra - has closed ignoring: ', key);
}
return;
}
if (this._extras.hasOwnProperty(key)) {
if (PRINT_TO_CONSOLE && __DEV__) {
infoLog(
'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) {
if (PRINT_TO_CONSOLE && __DEV__) {
infoLog(
'PerformanceLogger: startTimespan - has closed ignoring: ',
key,
);
}
return;
}
if (this._timespans[key]) {
if (PRINT_TO_CONSOLE && __DEV__) {
infoLog(
'PerformanceLogger: Attempting to start a timespan that already exists ',
key,
);
}
return;
}
this._timespans[key] = {
startTime: timestamp,
startExtras: extras,
};
_cookies[key] = Systrace.beginAsyncEvent(key);
if (PRINT_TO_CONSOLE) {
infoLog('PerformanceLogger.js', 'start: ' + key);
}
}
stopTimespan(
key: string,
timestamp?: number = getCurrentTimestamp(),
extras?: Extras,
) {
if (this._closed) {
if (PRINT_TO_CONSOLE && __DEV__) {
infoLog('PerformanceLogger: stopTimespan - has closed ignoring: ', key);
}
return;
}
const timespan = this._timespans[key];
if (!timespan || timespan.startTime == null) {
if (PRINT_TO_CONSOLE && __DEV__) {
infoLog(
'PerformanceLogger: Attempting to end a timespan that has not started ',
key,
);
}
return;
}
if (timespan.endTime != null) {
if (PRINT_TO_CONSOLE && __DEV__) {
infoLog(
'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);
if (PRINT_TO_CONSOLE) {
infoLog('PerformanceLogger.js', 'end: ' + key);
}
if (_cookies[key] != null) {
Systrace.endAsyncEvent(key, _cookies[key]);
delete _cookies[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,90 @@
/**
* 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
* @flow strict
*/
'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' ||
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;
}
module.exports = 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.
*
* @format
* @flow strict
*/
'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,
});
}
module.exports = 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.
*
* @format
* @flow
*/
'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
*/
const deepDiffer = function (
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;
module.exports = 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.
*
* @format
* @flow
*/
'use strict';
type Inset = {
top: ?number,
left: ?number,
right: ?number,
bottom: ?number,
...
};
const dummyInsets = {
top: undefined,
left: undefined,
right: undefined,
bottom: undefined,
};
const insetsDiffer = function (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)
);
};
module.exports = insetsDiffer;

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
* @flow strict
*/
'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.
*/
const matricesDiffer = function (
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]
);
};
module.exports = 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.
*
* @format
* @flow
*/
'use strict';
type Point = {
x: ?number,
y: ?number,
...
};
const dummyPoint = {x: undefined, y: undefined};
const pointsDiffer = function (one: ?Point, two: ?Point): boolean {
one = one || dummyPoint;
two = two || dummyPoint;
return one !== two && (one.x !== two.x || one.y !== two.y);
};
module.exports = 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};
const sizesDiffer = function (one: Size, two: Size): boolean {
const defaultedOne = one || dummySize;
const defaultedTwo = two || dummySize;
return (
defaultedOne !== defaultedTwo &&
(defaultedOne.width !== defaultedTwo.width ||
defaultedOne.height !== defaultedTwo.height)
);
};
module.exports = sizesDiffer;

View File

@@ -0,0 +1,21 @@
/**
* 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');
function dismissKeyboard() {
TextInputState.blurTextInput(TextInputState.currentlyFocusedInput());
}
module.exports = dismissKeyboard;

View File

@@ -0,0 +1,20 @@
/**
* 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
* @flow strict
*/
'use strict';
/**
* Intentional info-level logging for clear separation from ad-hoc console debug logging.
*/
function infoLog(...args: Array<mixed>): void {
return console.log(...args);
}
module.exports = infoLog;

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.
*
* @format
* @flow strict
*/
'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);
}
};
module.exports = 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.
*
* @format
* @flow strict
*/
'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;
}
module.exports = 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.
*
* @format
* @flow strict
*/
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,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.
*
* @format
* @flow strict-local
*/
'use strict';
import type {ColorSchemeName} from './NativeAppearance';
import Appearance from './Appearance';
import {useSyncExternalStore} from 'react';
const subscribe = (onStoreChange: () => void) => {
const appearanceSubscription = Appearance.addChangeListener(onStoreChange);
return () => appearanceSubscription.remove();
};
export default function useColorScheme(): ?ColorSchemeName {
return useSyncExternalStore(subscribe, Appearance.getColorScheme);
}

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 strict
* @format
*/
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>>
): (Instance | null) => void {
return useCallback(
(current: Instance | null) => {
for (const ref of refs) {
if (ref != null) {
if (typeof ref === 'function') {
ref(current);
} else {
ref.current = current;
}
}
}
},
[...refs], // eslint-disable-line react-hooks/exhaustive-deps
);
}

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.
*
* @format
* @flow strict-local
*/
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,130 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow
*/
import PlatformBaseViewConfig from '../NativeComponent/PlatformBaseViewConfig';
import {type ViewConfig} from '../Renderer/shims/ReactNativeTypes';
const IGNORED_KEYS = ['transform', 'hitSlop'];
/**
* The purpose of this function is to validate that the view config that
* native exposes for a given view manager is the same as the view config
* that is specified for that view manager in JS.
*
* In order to improve perf, we want to avoid calling into native to get
* the view config when each view manager is used. To do this, we are moving
* the configs to JS. In the future we will use these JS based view configs
* to codegen the view manager on native to ensure they stay in sync without
* this runtime check.
*
* If this function fails, that likely means a change was made to the native
* view manager without updating the JS config as well. Ideally you can make
* that direct change to the JS config. If you don't know what the differences
* are, the best approach I've found is to create a view that prints
* the return value of getNativeComponentAttributes, and then copying that
* text and pasting it back into JS:
* <Text selectable={true}>{JSON.stringify(getNativeComponentAttributes('RCTView'))}</Text>
*
* This is meant to be a stopgap until the time comes when we only have a
* single source of truth. I wonder if this message will still be here two
* years from now...
*/
export default function verifyComponentAttributeEquivalence(
nativeViewConfig: ViewConfig,
staticViewConfig: ViewConfig,
) {
for (const prop of [
'validAttributes',
'bubblingEventTypes',
'directEventTypes',
]) {
const diff = Object.keys(
lefthandObjectDiff(nativeViewConfig[prop], staticViewConfig[prop]),
);
if (diff.length > 0) {
const name =
staticViewConfig.uiViewClassName ?? nativeViewConfig.uiViewClassName;
console.error(
`'${name}' has a view config that does not match native. ` +
`'${prop}' is missing: ${diff.join(', ')}`,
);
}
}
}
// Return the different key-value pairs of the right object, by iterating through the keys in the left object
// Note it won't return a difference where a key is missing in the left but exists the right.
function lefthandObjectDiff(leftObj: Object, rightObj: Object): Object {
const differentKeys: {[string]: any | {...}} = {};
function compare(leftItem: any, rightItem: any, key: string) {
if (typeof leftItem !== typeof rightItem && leftItem != null) {
differentKeys[key] = rightItem;
return;
}
if (typeof leftItem === 'object') {
const objDiff = lefthandObjectDiff(leftItem, rightItem);
if (Object.keys(objDiff).length > 1) {
differentKeys[key] = objDiff;
}
return;
}
if (leftItem !== rightItem) {
differentKeys[key] = rightItem;
return;
}
}
for (const key in leftObj) {
if (IGNORED_KEYS.includes(key)) {
continue;
}
if (!rightObj) {
differentKeys[key] = {};
} else if (leftObj.hasOwnProperty(key)) {
compare(leftObj[key], rightObj[key], key);
}
}
return differentKeys;
}
export function getConfigWithoutViewProps(
viewConfig: ViewConfig,
propName: string,
): {...} {
if (!viewConfig[propName]) {
return {};
}
return Object.keys(viewConfig[propName])
.filter(prop => !PlatformBaseViewConfig[propName][prop])
.reduce<{[string]: any}>((obj, prop) => {
obj[prop] = viewConfig[propName][prop];
return obj;
}, {});
}
export 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,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
* @flow strict
*/
'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;
}
module.exports = warnOnce;