[change] StyleSheet validation

Stop relying on React internals and propTypes validation.
This commit is contained in:
Nicolas Gallagher
2019-03-01 11:24:10 -08:00
parent d4417e93a3
commit c68b532696
6 changed files with 167 additions and 145 deletions
@@ -4,10 +4,9 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @noflow
* @flow
*/
import StyleSheetValidation from './StyleSheetValidation';
import ReactNativePropRegistry from '../../modules/ReactNativePropRegistry';
import flattenStyle from './flattenStyle';
@@ -23,18 +22,34 @@ const absoluteFill = ReactNativePropRegistry.register(absoluteFillObject);
const StyleSheet = {
absoluteFill,
absoluteFillObject,
compose(style1, style2) {
compose(style1: any, style2: any) {
if (process.env.NODE_ENV !== 'production') {
/* eslint-disable prefer-rest-params */
const len = arguments.length;
if (len > 2) {
const readableStyles = [...arguments].map(a => flattenStyle(a));
throw new Error(
`StyleSheet.compose() only accepts 2 arguments, received ${len}: ${JSON.stringify(
readableStyles
)}`
);
}
/* eslint-enable prefer-rest-params */
}
if (style1 && style2) {
return [style1, style2];
} else {
return style1 || style2;
}
},
create(styles) {
create(styles: Object) {
const result = {};
Object.keys(styles).forEach(key => {
if (process.env.NODE_ENV !== 'production') {
StyleSheetValidation.validateStyle(key, styles);
const validate = require('./validate');
const interopValidate = validate.default ? validate.default : validate;
interopValidate(key, styles);
}
const id = styles[key] && ReactNativePropRegistry.register(styles[key]);
result[key] = id;
@@ -1,109 +0,0 @@
/**
* Copyright (c) 2016-present, Nicolas Gallagher.
* 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
*/
import ImageStylePropTypes from '../Image/ImageStylePropTypes';
import TextInputStylePropTypes from '../TextInput/TextInputStylePropTypes';
import TextStylePropTypes from '../Text/TextStylePropTypes';
import ViewStylePropTypes from '../View/ViewStylePropTypes';
import warning from 'fbjs/lib/warning';
import { number, oneOf, string } from 'prop-types';
// Hardcoded because this is a legit case but we don't want to load it from
// a private API. We might likely want to unify style sheet creation with how it
// is done in the DOM so this might move into React. I know what I'm doing so
// plz don't fire me.
const ReactPropTypesSecret = 'SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED';
class StyleSheetValidation {
static validateStyleProp(prop: string, style: Object, caller: string) {
if (process.env.NODE_ENV !== 'production') {
const value = style[prop];
const isCustomProperty = prop.indexOf('--') === 0;
if (isCustomProperty) return;
if (allStylePropTypes[prop] === undefined) {
const message1 = '"' + prop + '" is not a valid style property.';
const message2 =
'\nValid style props: ' +
JSON.stringify(Object.keys(allStylePropTypes).sort(), null, ' ');
styleError(message1, style, caller, message2);
} else if (typeof value === 'string' && value.indexOf('!important') > -1) {
styleError(
`Invalid value of "${value}" set on prop "${prop}". Values cannot include "!important"`,
style,
caller
);
} else {
const error = allStylePropTypes[prop](
style,
prop,
caller,
'prop',
null,
ReactPropTypesSecret
);
if (error) {
styleError(error.message, style, caller);
}
}
}
}
static validateStyle(name: string, styles: Object) {
if (process.env.NODE_ENV !== 'production') {
for (const prop in styles[name]) {
StyleSheetValidation.validateStyleProp(prop, styles[name], 'StyleSheet ' + name);
}
}
}
static addValidStylePropTypes(stylePropTypes: Object) {
for (const key in stylePropTypes) {
allStylePropTypes[key] = stylePropTypes[key];
}
}
}
const styleError = function(message1, style, caller?, message2?) {
warning(
false,
message1 +
'\n' +
(caller || '<<unknown>>') +
': ' +
JSON.stringify(style, null, ' ') +
(message2 || '')
);
};
const allStylePropTypes = {};
StyleSheetValidation.addValidStylePropTypes(ImageStylePropTypes);
StyleSheetValidation.addValidStylePropTypes(TextStylePropTypes);
StyleSheetValidation.addValidStylePropTypes(TextInputStylePropTypes);
StyleSheetValidation.addValidStylePropTypes(ViewStylePropTypes);
StyleSheetValidation.addValidStylePropTypes({
appearance: string,
borderCollapse: string,
borderSpacing: oneOf([number, string]),
clear: string,
cursor: string,
fill: string,
float: oneOf(['end', 'left', 'none', 'right', 'start']),
listStyle: string,
objectFit: oneOf(['fill', 'contain', 'cover', 'none', 'scale-down']),
objectPosition: string,
pointerEvents: string,
tableLayout: string
});
export default StyleSheetValidation;
@@ -114,14 +114,11 @@ to { left: 10px; }
exports[`StyleSheet/compile inline converts style to inline styles 1`] = `
Object {
"WebkitFlexBasis": "auto",
"WebkitFlexShrink": 1,
"display": "flex",
"flexBasis": "auto",
"flexShrink": 1,
"marginLeft": "10px",
"marginRight": "10px",
"msFlexNegative": 1,
"msFlexPreferredSize": "auto",
}
`;
@@ -23,3 +23,27 @@ export const STYLE_GROUPS = {
paddingVertical: 2.1
}
};
export const STYLE_SHORT_FORM_EXPANSIONS = {
borderColor: ['borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor'],
borderRadius: [
'borderTopLeftRadius',
'borderTopRightRadius',
'borderBottomRightRadius',
'borderBottomLeftRadius'
],
borderStyle: ['borderTopStyle', 'borderRightStyle', 'borderBottomStyle', 'borderLeftStyle'],
borderWidth: ['borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth'],
margin: ['marginTop', 'marginRight', 'marginBottom', 'marginLeft'],
marginHorizontal: ['marginRight', 'marginLeft'],
marginVertical: ['marginTop', 'marginBottom'],
overflow: ['overflowX', 'overflowY'],
overscrollBehavior: ['overscrollBehaviorX', 'overscrollBehaviorY'],
padding: ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'],
paddingHorizontal: ['paddingRight', 'paddingLeft'],
paddingVertical: ['paddingTop', 'paddingBottom']
};
export const MONOSPACE_FONT_STACK = 'monospace, monospace';
export const SYSTEM_FONT_STACK =
'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, "Helvetica Neue", sans-serif';
@@ -7,6 +7,7 @@
* @noflow
*/
import { MONOSPACE_FONT_STACK, SYSTEM_FONT_STACK, STYLE_SHORT_FORM_EXPANSIONS } from './constants';
import normalizeValueWithProperty from './normalizeValueWithProperty';
/**
@@ -21,30 +22,6 @@ import normalizeValueWithProperty from './normalizeValueWithProperty';
*/
const emptyObject = {};
const styleShortFormProperties = {
borderColor: ['borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor'],
borderRadius: [
'borderTopLeftRadius',
'borderTopRightRadius',
'borderBottomRightRadius',
'borderBottomLeftRadius'
],
borderStyle: ['borderTopStyle', 'borderRightStyle', 'borderBottomStyle', 'borderLeftStyle'],
borderWidth: ['borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth'],
margin: ['marginTop', 'marginRight', 'marginBottom', 'marginLeft'],
marginHorizontal: ['marginRight', 'marginLeft'],
marginVertical: ['marginTop', 'marginBottom'],
overflow: ['overflowX', 'overflowY'],
overscrollBehavior: ['overscrollBehaviorX', 'overscrollBehaviorY'],
padding: ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'],
paddingHorizontal: ['paddingRight', 'paddingLeft'],
paddingVertical: ['paddingTop', 'paddingBottom'],
writingDirection: ['direction']
};
const monospaceFontStack = 'monospace, monospace';
const systemFontStack =
'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, "Helvetica Neue", sans-serif';
/**
* Transform
@@ -135,17 +112,17 @@ const createReactDOMStyle = style => {
}
case 'font': {
resolvedStyle[prop] = value.replace('System', systemFontStack);
resolvedStyle[prop] = value.replace('System', SYSTEM_FONT_STACK);
break;
}
case 'fontFamily': {
if (value.indexOf('System') > -1) {
const stack = value.split(/,\s*/);
stack[stack.indexOf('System')] = systemFontStack;
stack[stack.indexOf('System')] = SYSTEM_FONT_STACK;
resolvedStyle[prop] = stack.join(', ');
} else if (value === 'monospace') {
resolvedStyle[prop] = monospaceFontStack;
resolvedStyle[prop] = MONOSPACE_FONT_STACK;
} else {
resolvedStyle[prop] = value;
}
@@ -177,8 +154,13 @@ const createReactDOMStyle = style => {
break;
}
case 'writingDirection': {
resolvedStyle.direction = value;
break;
}
default: {
const longFormProperties = styleShortFormProperties[prop];
const longFormProperties = STYLE_SHORT_FORM_EXPANSIONS[prop];
if (longFormProperties) {
longFormProperties.forEach((longForm, i) => {
// The value of any longform property in the original styles takes
@@ -0,0 +1,113 @@
/**
* Copyright (c) Nicolas Gallagher.
*
* 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
*/
// import { STYLE_SHORT_FORM_EXPANSIONS } from './constants';
import ImageStylePropTypes from '../Image/ImageStylePropTypes';
import TextInputStylePropTypes from '../TextInput/TextInputStylePropTypes';
import TextStylePropTypes from '../Text/TextStylePropTypes';
import ViewStylePropTypes from '../View/ViewStylePropTypes';
import warning from 'fbjs/lib/warning';
const validProperties = [
...Object.keys(ImageStylePropTypes),
...Object.keys(TextInputStylePropTypes),
...Object.keys(TextStylePropTypes),
...Object.keys(ViewStylePropTypes),
'appearance',
'borderCollapse',
'borderSpacing',
'clear',
'cursor',
'fill',
'float',
'listStyle',
'objectFit',
'objectPosition',
'pointerEvents',
'placeholderTextColor',
'tableLayout'
]
.sort()
.reduce((acc, curr) => {
acc[curr] = true;
return acc;
}, {});
const invalidShortforms = {
background: true,
borderBottom: true,
borderLeft: true,
borderRight: true,
borderTop: true,
font: true,
grid: true,
outline: true,
textDecoration: true
};
/*
const singleValueShortForms = Object.keys(STYLE_SHORT_FORM_EXPANSIONS).reduce((a, c) => {
a[c] = true;
return a;
}, {});
*/
function error(message) {
warning(false, message);
}
export default function validate(key: string, styles: Object) {
const obj = styles[key];
for (const k in obj) {
const prop = k.trim();
const value = obj[prop];
const isValidProp = validProperties[prop];
const isInvalidShorthand = invalidShortforms[prop];
let isInvalid = false;
if (value === null) {
continue;
}
if (!isValidProp) {
let suggestion = '';
if (prop === 'animation' || prop === 'animationName') {
suggestion = 'Did you mean "animationKeyframes"?';
// } else if (prop === 'boxShadow') {
// suggestion = 'Did you mean "shadow{Color,Offset,Opacity,Radius}"?';
} else if (prop === 'direction') {
suggestion = 'Did you mean "writingDirection"?';
} else if (prop === 'verticalAlign') {
suggestion = 'Did you mean "textAlignVertical"?';
} else if (isInvalidShorthand) {
suggestion = 'Please use long-form properties.';
}
isInvalid = true;
error(`Invalid style property of "${prop}". ${suggestion}`);
}
// else if (singleValueShortForms[prop]) {
// TODO: fix check
// if (typeof value === 'string' && value.indexOf(' ') > -1) {
// error(
// `Invalid style declaration "${prop}:${value}". This property must only specify a single value.`
// );
// isInvalid = true;
// }
// }
else if (typeof value === 'string' && value.indexOf('!important') > -1) {
error(`Invalid style declaration "${prop}:${value}". Values cannot include "!important"`);
isInvalid = true;
}
if (isInvalid) {
delete obj[k];
}
}
}