From 1329d18c6a71e78ed84bd055f624ff18c5ece784 Mon Sep 17 00:00:00 2001 From: Eli White Date: Thu, 23 Aug 2018 12:46:43 -0700 Subject: [PATCH] Require that JS defined Component Attributes match Native ones in dev Summary: As we move these configs to JS from native, until we have codegen that ensures everything stays up to date, this adds a dev mode check to ensure they are consistent. Reviewed By: yungsters Differential Revision: D9475011 fbshipit-source-id: 9d6f7b6c649229cae569d840eda3d5f7b7aa7cb2 --- .../Components/View/ViewNativeComponent.js | 7 +- .../getNativeComponentAttributes.js | 188 ++++++++++++++++++ .../ReactNative/requireNativeComponent.js | 179 +---------------- .../verifyComponentAttributeEquivalence.js | 29 +++ jest/setup.js | 3 +- 5 files changed, 230 insertions(+), 176 deletions(-) create mode 100644 Libraries/ReactNative/getNativeComponentAttributes.js create mode 100644 Libraries/Utilities/verifyComponentAttributeEquivalence.js diff --git a/Libraries/Components/View/ViewNativeComponent.js b/Libraries/Components/View/ViewNativeComponent.js index 00c58a121..677b253be 100644 --- a/Libraries/Components/View/ViewNativeComponent.js +++ b/Libraries/Components/View/ViewNativeComponent.js @@ -10,9 +10,11 @@ 'use strict'; +const AndroidConfig = require('ViewNativeComponentAndroidConfig'); const Platform = require('Platform'); const ReactNative = require('ReactNative'); +const verifyComponentAttributeEquivalence = require('verifyComponentAttributeEquivalence'); const requireNativeComponent = require('requireNativeComponent'); const ReactNativeViewConfigRegistry = require('ReactNativeViewConfigRegistry'); @@ -21,8 +23,11 @@ import type {ViewProps} from 'ViewPropTypes'; type ViewNativeComponentType = Class>; let NativeViewComponent; +if (Platform.OS === 'android') { + if (__DEV__) { + verifyComponentAttributeEquivalence('RCTView', AndroidConfig); + } -if (Platform.OS === 'Android') { NativeViewComponent = ReactNativeViewConfigRegistry.register('RCTView', () => require('ViewNativeComponentAndroidConfig'), ); diff --git a/Libraries/ReactNative/getNativeComponentAttributes.js b/Libraries/ReactNative/getNativeComponentAttributes.js new file mode 100644 index 000000000..0d4399cc1 --- /dev/null +++ b/Libraries/ReactNative/getNativeComponentAttributes.js @@ -0,0 +1,188 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +const ReactNativeStyleAttributes = require('ReactNativeStyleAttributes'); +const UIManager = require('UIManager'); + +const insetsDiffer = require('insetsDiffer'); +const matricesDiffer = require('matricesDiffer'); +const pointsDiffer = require('pointsDiffer'); +const processColor = require('processColor'); +const resolveAssetSource = require('resolveAssetSource'); +const sizesDiffer = require('sizesDiffer'); +const invariant = require('fbjs/lib/invariant'); +const warning = require('fbjs/lib/warning'); + +function getNativeComponentAttributes(uiViewClassName: string) { + const viewConfig = UIManager[uiViewClassName]; + + invariant( + viewConfig != null && viewConfig.NativeProps != null, + 'requireNativeComponent: "%s" was not found in the UIManager.', + uiViewClassName, + ); + + // TODO: This seems like a whole lot of runtime initialization for every + // native component that can be either avoided or simplified. + let {baseModuleName, bubblingEventTypes, directEventTypes} = viewConfig; + let nativeProps = viewConfig.NativeProps; + while (baseModuleName) { + const baseModule = UIManager[baseModuleName]; + if (!baseModule) { + warning(false, 'Base module "%s" does not exist', baseModuleName); + baseModuleName = null; + } else { + bubblingEventTypes = { + ...baseModule.bubblingEventTypes, + ...bubblingEventTypes, + }; + directEventTypes = { + ...baseModule.directEventTypes, + ...directEventTypes, + }; + nativeProps = { + ...baseModule.NativeProps, + ...nativeProps, + }; + baseModuleName = baseModule.baseModuleName; + } + } + + const validAttributes = {}; + + for (const key in nativeProps) { + const typeName = nativeProps[key]; + const diff = getDifferForType(typeName); + const process = getProcessorForType(typeName); + + validAttributes[key] = + diff == null && process == null ? true : {diff, process}; + } + + // Unfortunately, the current setup declares style properties as top-level + // props. This makes it so we allow style properties in the `style` prop. + // TODO: Move style properties into a `style` prop and disallow them as + // top-level props on the native side. + validAttributes.style = ReactNativeStyleAttributes; + + Object.assign(viewConfig, { + uiViewClassName, + validAttributes, + bubblingEventTypes, + directEventTypes, + }); + + if (!hasAttachedDefaultEventTypes) { + attachDefaultEventTypes(viewConfig); + hasAttachedDefaultEventTypes = true; + } + + return viewConfig; +} + +// TODO: Figure out how this makes sense. We're using a global boolean to only +// initialize this on the first eagerly initialized native component. +let hasAttachedDefaultEventTypes = false; +function attachDefaultEventTypes(viewConfig: any) { + // This is supported on UIManager platforms (ex: Android), + // as lazy view managers are not implemented for all platforms. + // See [UIManager] for details on constants and implementations. + if (UIManager.ViewManagerNames) { + // Lazy view managers enabled. + viewConfig = merge(viewConfig, UIManager.getDefaultEventTypes()); + } else { + viewConfig.bubblingEventTypes = merge( + viewConfig.bubblingEventTypes, + UIManager.genericBubblingEventTypes, + ); + viewConfig.directEventTypes = merge( + viewConfig.directEventTypes, + UIManager.genericDirectEventTypes, + ); + } +} + +// TODO: Figure out how to avoid all this runtime initialization cost. +function merge(destination: ?Object, source: ?Object): ?Object { + if (!source) { + return destination; + } + if (!destination) { + return source; + } + + for (const key in source) { + if (!source.hasOwnProperty(key)) { + continue; + } + + let sourceValue = source[key]; + if (destination.hasOwnProperty(key)) { + const destinationValue = destination[key]; + if ( + typeof sourceValue === 'object' && + typeof destinationValue === 'object' + ) { + sourceValue = merge(destinationValue, sourceValue); + } + } + destination[key] = sourceValue; + } + return destination; +} + +function getDifferForType( + typeName: string, +): ?(prevProp: any, nextProp: any) => boolean { + switch (typeName) { + // iOS Types + case 'CATransform3D': + return matricesDiffer; + case 'CGPoint': + return pointsDiffer; + case 'CGSize': + return sizesDiffer; + case 'UIEdgeInsets': + return insetsDiffer; + // Android Types + // (not yet implemented) + } + return null; +} + +function getProcessorForType(typeName: string): ?(nextProp: any) => any { + switch (typeName) { + // iOS Types + case 'CGColor': + case 'UIColor': + return processColor; + case 'CGColorArray': + case 'UIColorArray': + return processColorArray; + case 'CGImage': + case 'UIImage': + case 'RCTImageSource': + return resolveAssetSource; + // Android Types + case 'Color': + return processColor; + case 'ColorArray': + return processColorArray; + } + return null; +} + +function processColorArray(colors: ?Array): ?Array { + return colors == null ? null : colors.map(processColor); +} + +module.exports = getNativeComponentAttributes; diff --git a/Libraries/ReactNative/requireNativeComponent.js b/Libraries/ReactNative/requireNativeComponent.js index dce6a900b..3a4ad7f4e 100644 --- a/Libraries/ReactNative/requireNativeComponent.js +++ b/Libraries/ReactNative/requireNativeComponent.js @@ -4,24 +4,14 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @flow + * @flow strict-local * @format */ 'use strict'; -const ReactNativeStyleAttributes = require('ReactNativeStyleAttributes'); -const UIManager = require('UIManager'); - const createReactNativeComponentClass = require('createReactNativeComponentClass'); -const insetsDiffer = require('insetsDiffer'); -const matricesDiffer = require('matricesDiffer'); -const pointsDiffer = require('pointsDiffer'); -const processColor = require('processColor'); -const resolveAssetSource = require('resolveAssetSource'); -const sizesDiffer = require('sizesDiffer'); -const invariant = require('fbjs/lib/invariant'); -const warning = require('fbjs/lib/warning'); +const getNativeComponentAttributes = require('getNativeComponentAttributes'); /** * Creates values that can be used like React components which represent native @@ -32,167 +22,8 @@ const warning = require('fbjs/lib/warning'); * */ const requireNativeComponent = (uiViewClassName: string): string => - createReactNativeComponentClass(uiViewClassName, () => { - const viewConfig = UIManager[uiViewClassName]; - - invariant( - viewConfig != null && viewConfig.NativeProps != null, - 'requireNativeComponent: "%s" was not found in the UIManager.', - uiViewClassName, - ); - - // TODO: This seems like a whole lot of runtime initialization for every - // native component that can be either avoided or simplified. - let {baseModuleName, bubblingEventTypes, directEventTypes} = viewConfig; - let nativeProps = viewConfig.NativeProps; - while (baseModuleName) { - const baseModule = UIManager[baseModuleName]; - if (!baseModule) { - warning(false, 'Base module "%s" does not exist', baseModuleName); - baseModuleName = null; - } else { - bubblingEventTypes = { - ...baseModule.bubblingEventTypes, - ...bubblingEventTypes, - }; - directEventTypes = { - ...baseModule.directEventTypes, - ...directEventTypes, - }; - nativeProps = { - ...baseModule.NativeProps, - ...nativeProps, - }; - baseModuleName = baseModule.baseModuleName; - } - } - - const validAttributes = {}; - - for (const key in nativeProps) { - const typeName = nativeProps[key]; - const diff = getDifferForType(typeName); - const process = getProcessorForType(typeName); - - validAttributes[key] = - diff == null && process == null ? true : {diff, process}; - } - - // Unfortunately, the current setup declares style properties as top-level - // props. This makes it so we allow style properties in the `style` prop. - // TODO: Move style properties into a `style` prop and disallow them as - // top-level props on the native side. - validAttributes.style = ReactNativeStyleAttributes; - - Object.assign(viewConfig, { - uiViewClassName, - validAttributes, - bubblingEventTypes, - directEventTypes, - }); - - if (!hasAttachedDefaultEventTypes) { - attachDefaultEventTypes(viewConfig); - hasAttachedDefaultEventTypes = true; - } - - return viewConfig; - }); - -// TODO: Figure out how this makes sense. We're using a global boolean to only -// initialize this on the first eagerly initialized native component. -let hasAttachedDefaultEventTypes = false; -function attachDefaultEventTypes(viewConfig: any) { - // This is supported on UIManager platforms (ex: Android), - // as lazy view managers are not implemented for all platforms. - // See [UIManager] for details on constants and implementations. - if (UIManager.ViewManagerNames) { - // Lazy view managers enabled. - viewConfig = merge(viewConfig, UIManager.getDefaultEventTypes()); - } else { - viewConfig.bubblingEventTypes = merge( - viewConfig.bubblingEventTypes, - UIManager.genericBubblingEventTypes, - ); - viewConfig.directEventTypes = merge( - viewConfig.directEventTypes, - UIManager.genericDirectEventTypes, - ); - } -} - -// TODO: Figure out how to avoid all this runtime initialization cost. -function merge(destination: ?Object, source: ?Object): ?Object { - if (!source) { - return destination; - } - if (!destination) { - return source; - } - - for (const key in source) { - if (!source.hasOwnProperty(key)) { - continue; - } - - let sourceValue = source[key]; - if (destination.hasOwnProperty(key)) { - const destinationValue = destination[key]; - if ( - typeof sourceValue === 'object' && - typeof destinationValue === 'object' - ) { - sourceValue = merge(destinationValue, sourceValue); - } - } - destination[key] = sourceValue; - } - return destination; -} - -function getDifferForType( - typeName: string, -): ?(prevProp: any, nextProp: any) => boolean { - switch (typeName) { - // iOS Types - case 'CATransform3D': - return matricesDiffer; - case 'CGPoint': - return pointsDiffer; - case 'CGSize': - return sizesDiffer; - case 'UIEdgeInsets': - return insetsDiffer; - // Android Types - // (not yet implemented) - } - return null; -} - -function getProcessorForType(typeName: string): ?(nextProp: any) => any { - switch (typeName) { - // iOS Types - case 'CGColor': - case 'UIColor': - return processColor; - case 'CGColorArray': - case 'UIColorArray': - return processColorArray; - case 'CGImage': - case 'UIImage': - case 'RCTImageSource': - return resolveAssetSource; - // Android Types - case 'Color': - return processColor; - case 'ColorArray': - return processColorArray; - } - return null; -} - -function processColorArray(colors: ?Array): ?Array { - return colors == null ? null : colors.map(processColor); -} + createReactNativeComponentClass(uiViewClassName, () => + getNativeComponentAttributes(uiViewClassName), + ); module.exports = requireNativeComponent; diff --git a/Libraries/Utilities/verifyComponentAttributeEquivalence.js b/Libraries/Utilities/verifyComponentAttributeEquivalence.js new file mode 100644 index 000000000..7947f6134 --- /dev/null +++ b/Libraries/Utilities/verifyComponentAttributeEquivalence.js @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * 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'; + +const deepDiffer = require('deepDiffer'); +const getNativeComponentAttributes = require('getNativeComponentAttributes'); + +import type {ReactNativeBaseComponentViewConfig} from 'ReactNativeTypes'; + +function verifyComponentAttributeEquivalence( + componentName: string, + config: ReactNativeBaseComponentViewConfig<>, +) { + if (deepDiffer(getNativeComponentAttributes(componentName), config)) { + console.error( + `${componentName} config in JS does not match config specified by Native`, + ); + } +} + +module.exports = verifyComponentAttributeEquivalence; diff --git a/jest/setup.js b/jest/setup.js index 491260d93..4d5b67749 100644 --- a/jest/setup.js +++ b/jest/setup.js @@ -90,7 +90,8 @@ jest return ReactNative; }) - .mock('ensureComponentIsNative', () => () => true); + .mock('ensureComponentIsNative', () => () => true) + .mock('verifyComponentAttributeEquivalence', () => () => {}); const mockEmptyObject = {}; const mockNativeModules = {