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
This commit is contained in:
Eli White 2018-08-23 12:46:43 -07:00 коммит произвёл Hector Ramos
Родитель 499e20766c
Коммит 967d47830b
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: F089BA69BBDF6A20
5 изменённых файлов: 230 добавлений и 176 удалений

Просмотреть файл

@ -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<ReactNative.NativeComponent<ViewProps>>;
let NativeViewComponent;
if (Platform.OS === 'android') {
if (__DEV__) {
verifyComponentAttributeEquivalence('RCTView', AndroidConfig);
}
if (Platform.OS === 'Android') {
NativeViewComponent = ReactNativeViewConfigRegistry.register('RCTView', () =>
require('ViewNativeComponentAndroidConfig'),
);

Просмотреть файл

@ -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<any>): ?Array<?number> {
return colors == null ? null : colors.map(processColor);
}
module.exports = getNativeComponentAttributes;

Просмотреть файл

@ -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<any>): ?Array<?number> {
return colors == null ? null : colors.map(processColor);
}
createReactNativeComponentClass(uiViewClassName, () =>
getNativeComponentAttributes(uiViewClassName),
);
module.exports = requireNativeComponent;

Просмотреть файл

@ -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;

Просмотреть файл

@ -90,7 +90,8 @@ jest
return ReactNative;
})
.mock('ensureComponentIsNative', () => () => true);
.mock('ensureComponentIsNative', () => () => true)
.mock('verifyComponentAttributeEquivalence', () => () => {});
const mockEmptyObject = {};
const mockNativeModules = {