├── src ├── shared │ ├── checkReact.js │ ├── HTMLNodeType.js │ ├── ReactDOMInjection.js │ ├── voidElementTags.js │ ├── omittedCloseTags.js │ ├── createMicrosoftUnsafeLocalFunction.js │ ├── isCustomComponent.js │ ├── DOMNamespaces.js │ ├── ReactDOMNullInputValuePropHook.js │ ├── validAriaProperties.js │ ├── dangerousStyleValue.js │ ├── ReactControlledValuePropTypes.js │ ├── assertValidProps.js │ ├── CSSProperty.js │ ├── CSSPropertyOperations.js │ ├── warnValidStyle.js │ ├── SVGDOMPropertyConfig.js │ ├── ReactDOMInvalidARIAHook.js │ ├── HTMLDOMPropertyConfig.js │ ├── DOMProperty.js │ ├── ReactDOMUnknownPropertyHook.js │ └── possibleStandardNames.js └── server │ ├── ComponentCache.js │ ├── quoteAttributeValueForBrowser.js │ ├── ReactDOMServerNode.js │ ├── ReactDOMStringRenderer.js │ ├── escapeTextForBrowser.js │ ├── DOMMarkupOperations.js │ ├── ReactDOMNodeStreamRenderer.js │ └── ReactPartialRenderer.js ├── package.json └── README.md /src/shared/checkReact.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import React from 'react'; 11 | import invariant from 'fbjs/lib/invariant'; 12 | 13 | invariant( 14 | React, 15 | 'ReactDOM was loaded before React. Make sure you load ' + 16 | 'the React package before loading ReactDOM.', 17 | ); 18 | -------------------------------------------------------------------------------- /src/shared/HTMLNodeType.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | /** 9 | * HTML nodeType values that represent the type of the node 10 | */ 11 | 12 | export const ELEMENT_NODE = 1; 13 | export const TEXT_NODE = 3; 14 | export const COMMENT_NODE = 8; 15 | export const DOCUMENT_NODE = 9; 16 | export const DOCUMENT_FRAGMENT_NODE = 11; 17 | -------------------------------------------------------------------------------- /src/shared/ReactDOMInjection.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import * as DOMProperty from './DOMProperty'; 9 | import HTMLDOMPropertyConfig from './HTMLDOMPropertyConfig'; 10 | import SVGDOMPropertyConfig from './SVGDOMPropertyConfig'; 11 | 12 | DOMProperty.injection.injectDOMPropertyConfig(HTMLDOMPropertyConfig); 13 | DOMProperty.injection.injectDOMPropertyConfig(SVGDOMPropertyConfig); 14 | -------------------------------------------------------------------------------- /src/shared/voidElementTags.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import omittedCloseTags from './omittedCloseTags'; 9 | 10 | // For HTML, certain tags cannot have children. This has the same purpose as 11 | // `omittedCloseTags` except that `menuitem` should still have its closing tag. 12 | 13 | var voidElementTags = { 14 | menuitem: true, 15 | ...omittedCloseTags, 16 | }; 17 | 18 | export default voidElementTags; 19 | -------------------------------------------------------------------------------- /src/server/ComponentCache.js: -------------------------------------------------------------------------------- 1 | import lru from 'lru-cache'; 2 | 3 | export default class ComponentCache { 4 | constructor(config = {}) { 5 | 6 | if (Number.isInteger(config)) { 7 | config = { 8 | max:config, 9 | }; 10 | } 11 | 12 | this.storage = lru({ 13 | max: config.max || 1000000000, 14 | length: (n, key) => { 15 | return n.length + key.length; 16 | }, 17 | }); 18 | } 19 | 20 | get(cacheKey, cb) { 21 | let reply = this.storage.get(cacheKey); 22 | cb(null, reply); 23 | } 24 | 25 | set(cacheKey, html) { 26 | this.storage.set(cacheKey, html); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/server/quoteAttributeValueForBrowser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import escapeTextForBrowser from './escapeTextForBrowser'; 9 | 10 | /** 11 | * Escapes attribute value to prevent scripting attacks. 12 | * 13 | * @param {*} value Value to escape. 14 | * @return {string} An escaped string. 15 | */ 16 | function quoteAttributeValueForBrowser(value) { 17 | return '"' + escapeTextForBrowser(value) + '"'; 18 | } 19 | 20 | export default quoteAttributeValueForBrowser; 21 | -------------------------------------------------------------------------------- /src/shared/omittedCloseTags.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | // For HTML, certain tags should omit their close tag. We keep a whitelist for 9 | // those special-case tags. 10 | 11 | var omittedCloseTags = { 12 | area: true, 13 | base: true, 14 | br: true, 15 | col: true, 16 | embed: true, 17 | hr: true, 18 | img: true, 19 | input: true, 20 | keygen: true, 21 | link: true, 22 | meta: true, 23 | param: true, 24 | source: true, 25 | track: true, 26 | wbr: true, 27 | // NOTE: menuitem's close tag should be omitted, but that causes problems. 28 | }; 29 | 30 | export default omittedCloseTags; 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-component-caching", 3 | "version": "1.1.0", 4 | "description": "React Component Caching is a component-level caching library for faster server-side rendering with React 16", 5 | "main": "index.js", 6 | "scripts": { 7 | "webpack": "./node_modules/.bin/webpack -w" 8 | }, 9 | "keywords": [ 10 | "ssr", 11 | "react", 12 | "fiber", 13 | "server side rendering", 14 | "component caching" 15 | ], 16 | "author": { 17 | "name": "rookLab" 18 | }, 19 | "license": "MIT", 20 | "dependencies": { 21 | "fbjs": "^0.8.16", 22 | "lru-cache": "^4.1.1", 23 | "object-assign": "^4.1.1", 24 | "prop-types": "^15.6.1", 25 | "react": "^16.2.0" 26 | }, 27 | "devDependencies": { 28 | "webpack": "^3.5.5" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/shared/createMicrosoftUnsafeLocalFunction.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | /* globals MSApp */ 9 | 10 | /** 11 | * Create a function which has 'unsafe' privileges (required by windows8 apps) 12 | */ 13 | var createMicrosoftUnsafeLocalFunction = function(func) { 14 | if (typeof MSApp !== 'undefined' && MSApp.execUnsafeLocalFunction) { 15 | return function(arg0, arg1, arg2, arg3) { 16 | MSApp.execUnsafeLocalFunction(function() { 17 | return func(arg0, arg1, arg2, arg3); 18 | }); 19 | }; 20 | } else { 21 | return func; 22 | } 23 | }; 24 | 25 | export default createMicrosoftUnsafeLocalFunction; 26 | -------------------------------------------------------------------------------- /src/server/ReactDOMServerNode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import '../shared/ReactDOMInjection'; 9 | import ReactVersion from 'shared/ReactVersion'; 10 | 11 | import {renderToString, renderToStaticMarkup} from './ReactDOMStringRenderer'; 12 | import { 13 | renderToNodeStream, 14 | renderToStaticNodeStream, 15 | } from './ReactDOMNodeStreamRenderer'; 16 | 17 | import ComponentCache from './ComponentCache'; 18 | 19 | 20 | // Note: when changing this, also consider https://github.com/facebook/react/issues/11526 21 | export default { 22 | renderToString, 23 | renderToStaticMarkup, 24 | renderToNodeStream, 25 | renderToStaticNodeStream, 26 | ComponentCache, 27 | version: ReactVersion, 28 | }; 29 | -------------------------------------------------------------------------------- /src/shared/isCustomComponent.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | function isCustomComponent(tagName: string, props: Object) { 11 | if (tagName.indexOf('-') === -1) { 12 | return typeof props.is === 'string'; 13 | } 14 | switch (tagName) { 15 | // These are reserved SVG and MathML elements. 16 | // We don't mind this whitelist too much because we expect it to never grow. 17 | // The alternative is to track the namespace in a few places which is convoluted. 18 | // https://w3c.github.io/webcomponents/spec/custom/#custom-elements-core-concepts 19 | case 'annotation-xml': 20 | case 'color-profile': 21 | case 'font-face': 22 | case 'font-face-src': 23 | case 'font-face-uri': 24 | case 'font-face-format': 25 | case 'font-face-name': 26 | case 'missing-glyph': 27 | return false; 28 | default: 29 | return true; 30 | } 31 | } 32 | 33 | export default isCustomComponent; 34 | -------------------------------------------------------------------------------- /src/server/ReactDOMStringRenderer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import ReactPartialRenderer from './ReactPartialRenderer'; 9 | 10 | /** 11 | * Render a ReactElement to its initial HTML. This should only be used on the 12 | * server. 13 | * See https://reactjs.org/docs/react-dom-server.html#rendertostring 14 | */ 15 | export async function renderToString(element, cache, memLife=0) { 16 | // If and only if using memcached, pass the lifetime of your cache entry (in seconds) into 'memLife'. 17 | var renderer = new ReactPartialRenderer(element, false); 18 | var markup = await renderer.read(Infinity, cache, false, null, memLife); 19 | return markup; 20 | } 21 | 22 | /** 23 | * Similar to renderToString, except this doesn't create extra DOM attributes 24 | * such as data-react-id that React uses internally. 25 | * See https://reactjs.org/docs/react-dom-server.html#rendertostaticmarkup 26 | */ 27 | export async function renderToStaticMarkup(element, cache, memLife=0) { 28 | var renderer = new ReactPartialRenderer(element, true); 29 | var markup = await renderer.read(Infinity, cache, false, null, memLife); 30 | return markup; 31 | } 32 | -------------------------------------------------------------------------------- /src/shared/DOMNamespaces.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml'; 9 | const MATH_NAMESPACE = 'http://www.w3.org/1998/Math/MathML'; 10 | const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; 11 | 12 | export const Namespaces = { 13 | html: HTML_NAMESPACE, 14 | mathml: MATH_NAMESPACE, 15 | svg: SVG_NAMESPACE, 16 | }; 17 | 18 | // Assumes there is no parent namespace. 19 | export function getIntrinsicNamespace(type: string): string { 20 | switch (type) { 21 | case 'svg': 22 | return SVG_NAMESPACE; 23 | case 'math': 24 | return MATH_NAMESPACE; 25 | default: 26 | return HTML_NAMESPACE; 27 | } 28 | } 29 | 30 | export function getChildNamespace( 31 | parentNamespace: string | null, 32 | type: string, 33 | ): string { 34 | if (parentNamespace == null || parentNamespace === HTML_NAMESPACE) { 35 | // No (or default) parent namespace: potential entry point. 36 | return getIntrinsicNamespace(type); 37 | } 38 | if (parentNamespace === SVG_NAMESPACE && type === 'foreignObject') { 39 | // We're leaving SVG. 40 | return HTML_NAMESPACE; 41 | } 42 | // By default, pass namespace below. 43 | return parentNamespace; 44 | } 45 | -------------------------------------------------------------------------------- /src/shared/ReactDOMNullInputValuePropHook.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import {ReactDebugCurrentFrame} from 'shared/ReactGlobalSharedState'; 9 | import warning from 'fbjs/lib/warning'; 10 | 11 | var didWarnValueNull = false; 12 | 13 | function getStackAddendum() { 14 | var stack = ReactDebugCurrentFrame.getStackAddendum(); 15 | return stack != null ? stack : ''; 16 | } 17 | 18 | export function validateProperties(type, props) { 19 | if (type !== 'input' && type !== 'textarea' && type !== 'select') { 20 | return; 21 | } 22 | 23 | if (props != null && props.value === null && !didWarnValueNull) { 24 | didWarnValueNull = true; 25 | if (type === 'select' && props.multiple) { 26 | warning( 27 | false, 28 | '`value` prop on `%s` should not be null. ' + 29 | 'Consider using an empty array when `multiple` is set to `true` ' + 30 | 'to clear the component or `undefined` for uncontrolled components.%s', 31 | type, 32 | getStackAddendum(), 33 | ); 34 | } else { 35 | warning( 36 | false, 37 | '`value` prop on `%s` should not be null. ' + 38 | 'Consider using an empty string to clear the component or `undefined` ' + 39 | 'for uncontrolled components.%s', 40 | type, 41 | getStackAddendum(), 42 | ); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/shared/validAriaProperties.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | var ariaProperties = { 9 | 'aria-current': 0, // state 10 | 'aria-details': 0, 11 | 'aria-disabled': 0, // state 12 | 'aria-hidden': 0, // state 13 | 'aria-invalid': 0, // state 14 | 'aria-keyshortcuts': 0, 15 | 'aria-label': 0, 16 | 'aria-roledescription': 0, 17 | // Widget Attributes 18 | 'aria-autocomplete': 0, 19 | 'aria-checked': 0, 20 | 'aria-expanded': 0, 21 | 'aria-haspopup': 0, 22 | 'aria-level': 0, 23 | 'aria-modal': 0, 24 | 'aria-multiline': 0, 25 | 'aria-multiselectable': 0, 26 | 'aria-orientation': 0, 27 | 'aria-placeholder': 0, 28 | 'aria-pressed': 0, 29 | 'aria-readonly': 0, 30 | 'aria-required': 0, 31 | 'aria-selected': 0, 32 | 'aria-sort': 0, 33 | 'aria-valuemax': 0, 34 | 'aria-valuemin': 0, 35 | 'aria-valuenow': 0, 36 | 'aria-valuetext': 0, 37 | // Live Region Attributes 38 | 'aria-atomic': 0, 39 | 'aria-busy': 0, 40 | 'aria-live': 0, 41 | 'aria-relevant': 0, 42 | // Drag-and-Drop Attributes 43 | 'aria-dropeffect': 0, 44 | 'aria-grabbed': 0, 45 | // Relationship Attributes 46 | 'aria-activedescendant': 0, 47 | 'aria-colcount': 0, 48 | 'aria-colindex': 0, 49 | 'aria-colspan': 0, 50 | 'aria-controls': 0, 51 | 'aria-describedby': 0, 52 | 'aria-errormessage': 0, 53 | 'aria-flowto': 0, 54 | 'aria-labelledby': 0, 55 | 'aria-owns': 0, 56 | 'aria-posinset': 0, 57 | 'aria-rowcount': 0, 58 | 'aria-rowindex': 0, 59 | 'aria-rowspan': 0, 60 | 'aria-setsize': 0, 61 | }; 62 | 63 | export default ariaProperties; 64 | -------------------------------------------------------------------------------- /src/shared/dangerousStyleValue.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import {isUnitlessNumber} from './CSSProperty'; 9 | 10 | /** 11 | * Convert a value into the proper css writable value. The style name `name` 12 | * should be logical (no hyphens), as specified 13 | * in `CSSProperty.isUnitlessNumber`. 14 | * 15 | * @param {string} name CSS property name such as `topMargin`. 16 | * @param {*} value CSS property value such as `10px`. 17 | * @return {string} Normalized style value with dimensions applied. 18 | */ 19 | function dangerousStyleValue(name, value, isCustomProperty) { 20 | // Note that we've removed escapeTextForBrowser() calls here since the 21 | // whole string will be escaped when the attribute is injected into 22 | // the markup. If you provide unsafe user data here they can inject 23 | // arbitrary CSS which may be problematic (I couldn't repro this): 24 | // https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet 25 | // http://www.thespanner.co.uk/2007/11/26/ultimate-xss-css-injection/ 26 | // This is not an XSS hole but instead a potential CSS injection issue 27 | // which has lead to a greater discussion about how we're going to 28 | // trust URLs moving forward. See #2115901 29 | 30 | var isEmpty = value == null || typeof value === 'boolean' || value === ''; 31 | if (isEmpty) { 32 | return ''; 33 | } 34 | 35 | if ( 36 | !isCustomProperty && 37 | typeof value === 'number' && 38 | value !== 0 && 39 | !(isUnitlessNumber.hasOwnProperty(name) && isUnitlessNumber[name]) 40 | ) { 41 | return value + 'px'; // Presumes implicit 'px' suffix for unitless numbers 42 | } 43 | 44 | return ('' + value).trim(); 45 | } 46 | 47 | export default dangerousStyleValue; 48 | -------------------------------------------------------------------------------- /src/shared/ReactControlledValuePropTypes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import checkPropTypes from 'prop-types/checkPropTypes'; 9 | 10 | var ReactControlledValuePropTypes = { 11 | checkPropTypes: null, 12 | }; 13 | 14 | if (__DEV__) { 15 | var hasReadOnlyValue = { 16 | button: true, 17 | checkbox: true, 18 | image: true, 19 | hidden: true, 20 | radio: true, 21 | reset: true, 22 | submit: true, 23 | }; 24 | 25 | var propTypes = { 26 | value: function(props, propName, componentName) { 27 | if ( 28 | !props[propName] || 29 | hasReadOnlyValue[props.type] || 30 | props.onChange || 31 | props.readOnly || 32 | props.disabled 33 | ) { 34 | return null; 35 | } 36 | return new Error( 37 | 'You provided a `value` prop to a form field without an ' + 38 | '`onChange` handler. This will render a read-only field. If ' + 39 | 'the field should be mutable use `defaultValue`. Otherwise, ' + 40 | 'set either `onChange` or `readOnly`.', 41 | ); 42 | }, 43 | checked: function(props, propName, componentName) { 44 | if ( 45 | !props[propName] || 46 | props.onChange || 47 | props.readOnly || 48 | props.disabled 49 | ) { 50 | return null; 51 | } 52 | return new Error( 53 | 'You provided a `checked` prop to a form field without an ' + 54 | '`onChange` handler. This will render a read-only field. If ' + 55 | 'the field should be mutable use `defaultChecked`. Otherwise, ' + 56 | 'set either `onChange` or `readOnly`.', 57 | ); 58 | }, 59 | }; 60 | 61 | /** 62 | * Provide a linked `value` attribute for controlled forms. You should not use 63 | * this outside of the ReactDOM controlled form components. 64 | */ 65 | ReactControlledValuePropTypes.checkPropTypes = function( 66 | tagName, 67 | props, 68 | getStack, 69 | ) { 70 | checkPropTypes(propTypes, props, 'prop', tagName, getStack); 71 | }; 72 | } 73 | 74 | export default ReactControlledValuePropTypes; 75 | -------------------------------------------------------------------------------- /src/shared/assertValidProps.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import invariant from 'fbjs/lib/invariant'; 9 | import warning from 'fbjs/lib/warning'; 10 | 11 | import voidElementTags from './voidElementTags'; 12 | 13 | var HTML = '__html'; 14 | 15 | function assertValidProps(tag: string, props: ?Object, getStack: () => string) { 16 | if (!props) { 17 | return; 18 | } 19 | // Note the use of `==` which checks for null or undefined. 20 | if (voidElementTags[tag]) { 21 | invariant( 22 | props.children == null && props.dangerouslySetInnerHTML == null, 23 | '%s is a void element tag and must neither have `children` nor ' + 24 | 'use `dangerouslySetInnerHTML`.%s', 25 | tag, 26 | getStack(), 27 | ); 28 | } 29 | if (props.dangerouslySetInnerHTML != null) { 30 | invariant( 31 | props.children == null, 32 | 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.', 33 | ); 34 | invariant( 35 | typeof props.dangerouslySetInnerHTML === 'object' && 36 | HTML in props.dangerouslySetInnerHTML, 37 | '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' + 38 | 'Please visit https://fb.me/react-invariant-dangerously-set-inner-html ' + 39 | 'for more information.', 40 | ); 41 | } 42 | if (__DEV__) { 43 | warning( 44 | props.suppressContentEditableWarning || 45 | !props.contentEditable || 46 | props.children == null, 47 | 'A component is `contentEditable` and contains `children` managed by ' + 48 | 'React. It is now your responsibility to guarantee that none of ' + 49 | 'those nodes are unexpectedly modified or duplicated. This is ' + 50 | 'probably not intentional.%s', 51 | getStack(), 52 | ); 53 | } 54 | invariant( 55 | props.style == null || typeof props.style === 'object', 56 | 'The `style` prop expects a mapping from style properties to values, ' + 57 | "not a string. For example, style={{marginRight: spacing + 'em'}} when " + 58 | 'using JSX.%s', 59 | getStack(), 60 | ); 61 | } 62 | 63 | export default assertValidProps; 64 | -------------------------------------------------------------------------------- /src/shared/CSSProperty.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | /** 9 | * CSS properties which accept numbers but are not in units of "px". 10 | */ 11 | export const isUnitlessNumber = { 12 | animationIterationCount: true, 13 | borderImageOutset: true, 14 | borderImageSlice: true, 15 | borderImageWidth: true, 16 | boxFlex: true, 17 | boxFlexGroup: true, 18 | boxOrdinalGroup: true, 19 | columnCount: true, 20 | columns: true, 21 | flex: true, 22 | flexGrow: true, 23 | flexPositive: true, 24 | flexShrink: true, 25 | flexNegative: true, 26 | flexOrder: true, 27 | gridRow: true, 28 | gridRowEnd: true, 29 | gridRowSpan: true, 30 | gridRowStart: true, 31 | gridColumn: true, 32 | gridColumnEnd: true, 33 | gridColumnSpan: true, 34 | gridColumnStart: true, 35 | fontWeight: true, 36 | lineClamp: true, 37 | lineHeight: true, 38 | opacity: true, 39 | order: true, 40 | orphans: true, 41 | tabSize: true, 42 | widows: true, 43 | zIndex: true, 44 | zoom: true, 45 | 46 | // SVG-related properties 47 | fillOpacity: true, 48 | floodOpacity: true, 49 | stopOpacity: true, 50 | strokeDasharray: true, 51 | strokeDashoffset: true, 52 | strokeMiterlimit: true, 53 | strokeOpacity: true, 54 | strokeWidth: true, 55 | }; 56 | 57 | /** 58 | * @param {string} prefix vendor-specific prefix, eg: Webkit 59 | * @param {string} key style name, eg: transitionDuration 60 | * @return {string} style name prefixed with `prefix`, properly camelCased, eg: 61 | * WebkitTransitionDuration 62 | */ 63 | function prefixKey(prefix, key) { 64 | return prefix + key.charAt(0).toUpperCase() + key.substring(1); 65 | } 66 | 67 | /** 68 | * Support style names that may come passed in prefixed by adding permutations 69 | * of vendor prefixes. 70 | */ 71 | var prefixes = ['Webkit', 'ms', 'Moz', 'O']; 72 | 73 | // Using Object.keys here, or else the vanilla for-in loop makes IE8 go into an 74 | // infinite loop, because it iterates over the newly added props too. 75 | Object.keys(isUnitlessNumber).forEach(function(prop) { 76 | prefixes.forEach(function(prefix) { 77 | isUnitlessNumber[prefixKey(prefix, prop)] = isUnitlessNumber[prop]; 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /src/shared/CSSPropertyOperations.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import dangerousStyleValue from './dangerousStyleValue'; 9 | import hyphenateStyleName from 'fbjs/lib/hyphenateStyleName'; 10 | import warnValidStyle from './warnValidStyle'; 11 | 12 | /** 13 | * Operations for dealing with CSS properties. 14 | */ 15 | 16 | /** 17 | * This creates a string that is expected to be equivalent to the style 18 | * attribute generated by server-side rendering. It by-passes warnings and 19 | * security checks so it's not safe to use this value for anything other than 20 | * comparison. It is only used in DEV for SSR validation. 21 | */ 22 | export function createDangerousStringForStyles(styles) { 23 | if (__DEV__) { 24 | var serialized = ''; 25 | var delimiter = ''; 26 | for (var styleName in styles) { 27 | if (!styles.hasOwnProperty(styleName)) { 28 | continue; 29 | } 30 | var styleValue = styles[styleName]; 31 | if (styleValue != null) { 32 | var isCustomProperty = styleName.indexOf('--') === 0; 33 | serialized += delimiter + hyphenateStyleName(styleName) + ':'; 34 | serialized += dangerousStyleValue( 35 | styleName, 36 | styleValue, 37 | isCustomProperty, 38 | ); 39 | 40 | delimiter = ';'; 41 | } 42 | } 43 | return serialized || null; 44 | } 45 | } 46 | 47 | /** 48 | * Sets the value for multiple styles on a node. If a value is specified as 49 | * '' (empty string), the corresponding style property will be unset. 50 | * 51 | * @param {DOMElement} node 52 | * @param {object} styles 53 | */ 54 | export function setValueForStyles(node, styles, getStack) { 55 | var style = node.style; 56 | for (var styleName in styles) { 57 | if (!styles.hasOwnProperty(styleName)) { 58 | continue; 59 | } 60 | var isCustomProperty = styleName.indexOf('--') === 0; 61 | if (__DEV__) { 62 | if (!isCustomProperty) { 63 | warnValidStyle(styleName, styles[styleName], getStack); 64 | } 65 | } 66 | var styleValue = dangerousStyleValue( 67 | styleName, 68 | styles[styleName], 69 | isCustomProperty, 70 | ); 71 | if (styleName === 'float') { 72 | styleName = 'cssFloat'; 73 | } 74 | if (isCustomProperty) { 75 | style.setProperty(styleName, styleValue); 76 | } else { 77 | style[styleName] = styleValue; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/shared/warnValidStyle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import emptyFunction from 'fbjs/lib/emptyFunction'; 9 | import camelizeStyleName from 'fbjs/lib/camelizeStyleName'; 10 | import warning from 'fbjs/lib/warning'; 11 | 12 | var warnValidStyle = emptyFunction; 13 | 14 | if (__DEV__) { 15 | // 'msTransform' is correct, but the other prefixes should be capitalized 16 | var badVendoredStyleNamePattern = /^(?:webkit|moz|o)[A-Z]/; 17 | 18 | // style values shouldn't contain a semicolon 19 | var badStyleValueWithSemicolonPattern = /;\s*$/; 20 | 21 | var warnedStyleNames = {}; 22 | var warnedStyleValues = {}; 23 | var warnedForNaNValue = false; 24 | var warnedForInfinityValue = false; 25 | 26 | var warnHyphenatedStyleName = function(name, getStack) { 27 | if (warnedStyleNames.hasOwnProperty(name) && warnedStyleNames[name]) { 28 | return; 29 | } 30 | 31 | warnedStyleNames[name] = true; 32 | warning( 33 | false, 34 | 'Unsupported style property %s. Did you mean %s?%s', 35 | name, 36 | camelizeStyleName(name), 37 | getStack(), 38 | ); 39 | }; 40 | 41 | var warnBadVendoredStyleName = function(name, getStack) { 42 | if (warnedStyleNames.hasOwnProperty(name) && warnedStyleNames[name]) { 43 | return; 44 | } 45 | 46 | warnedStyleNames[name] = true; 47 | warning( 48 | false, 49 | 'Unsupported vendor-prefixed style property %s. Did you mean %s?%s', 50 | name, 51 | name.charAt(0).toUpperCase() + name.slice(1), 52 | getStack(), 53 | ); 54 | }; 55 | 56 | var warnStyleValueWithSemicolon = function(name, value, getStack) { 57 | if (warnedStyleValues.hasOwnProperty(value) && warnedStyleValues[value]) { 58 | return; 59 | } 60 | 61 | warnedStyleValues[value] = true; 62 | warning( 63 | false, 64 | "Style property values shouldn't contain a semicolon. " + 65 | 'Try "%s: %s" instead.%s', 66 | name, 67 | value.replace(badStyleValueWithSemicolonPattern, ''), 68 | getStack(), 69 | ); 70 | }; 71 | 72 | var warnStyleValueIsNaN = function(name, value, getStack) { 73 | if (warnedForNaNValue) { 74 | return; 75 | } 76 | 77 | warnedForNaNValue = true; 78 | warning( 79 | false, 80 | '`NaN` is an invalid value for the `%s` css style property.%s', 81 | name, 82 | getStack(), 83 | ); 84 | }; 85 | 86 | var warnStyleValueIsInfinity = function(name, value, getStack) { 87 | if (warnedForInfinityValue) { 88 | return; 89 | } 90 | 91 | warnedForInfinityValue = true; 92 | warning( 93 | false, 94 | '`Infinity` is an invalid value for the `%s` css style property.%s', 95 | name, 96 | getStack(), 97 | ); 98 | }; 99 | 100 | warnValidStyle = function(name, value, getStack) { 101 | if (name.indexOf('-') > -1) { 102 | warnHyphenatedStyleName(name, getStack); 103 | } else if (badVendoredStyleNamePattern.test(name)) { 104 | warnBadVendoredStyleName(name, getStack); 105 | } else if (badStyleValueWithSemicolonPattern.test(value)) { 106 | warnStyleValueWithSemicolon(name, value, getStack); 107 | } 108 | 109 | if (typeof value === 'number') { 110 | if (isNaN(value)) { 111 | warnStyleValueIsNaN(name, value, getStack); 112 | } else if (!isFinite(value)) { 113 | warnStyleValueIsInfinity(name, value, getStack); 114 | } 115 | } 116 | }; 117 | } 118 | 119 | export default warnValidStyle; 120 | -------------------------------------------------------------------------------- /src/server/escapeTextForBrowser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * Based on the escape-html library, which is used under the MIT License below: 8 | * 9 | * Copyright (c) 2012-2013 TJ Holowaychuk 10 | * Copyright (c) 2015 Andreas Lubbe 11 | * Copyright (c) 2015 Tiancheng "Timothy" Gu 12 | * 13 | * Permission is hereby granted, free of charge, to any person obtaining 14 | * a copy of this software and associated documentation files (the 15 | * 'Software'), to deal in the Software without restriction, including 16 | * without limitation the rights to use, copy, modify, merge, publish, 17 | * distribute, sublicense, and/or sell copies of the Software, and to 18 | * permit persons to whom the Software is furnished to do so, subject to 19 | * the following conditions: 20 | * 21 | * The above copyright notice and this permission notice shall be 22 | * included in all copies or substantial portions of the Software. 23 | * 24 | * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 25 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 26 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 27 | * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 28 | * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 29 | * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 30 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 31 | */ 32 | 33 | // code copied and modified from escape-html 34 | /** 35 | * Module variables. 36 | * @private 37 | */ 38 | 39 | var matchHtmlRegExp = /["'&<>]/; 40 | 41 | /** 42 | * Escapes special characters and HTML entities in a given html string. 43 | * 44 | * @param {string} string HTML string to escape for later insertion 45 | * @return {string} 46 | * @public 47 | */ 48 | 49 | function escapeHtml(string) { 50 | var str = '' + string; 51 | var match = matchHtmlRegExp.exec(str); 52 | 53 | if (!match) { 54 | return str; 55 | } 56 | 57 | var escape; 58 | var html = ''; 59 | var index = 0; 60 | var lastIndex = 0; 61 | 62 | for (index = match.index; index < str.length; index++) { 63 | switch (str.charCodeAt(index)) { 64 | case 34: // " 65 | escape = '"'; 66 | break; 67 | case 38: // & 68 | escape = '&'; 69 | break; 70 | case 39: // ' 71 | escape = '''; // modified from escape-html; used to be ''' 72 | break; 73 | case 60: // < 74 | escape = '<'; 75 | break; 76 | case 62: // > 77 | escape = '>'; 78 | break; 79 | default: 80 | continue; 81 | } 82 | 83 | if (lastIndex !== index) { 84 | html += str.substring(lastIndex, index); 85 | } 86 | 87 | lastIndex = index + 1; 88 | html += escape; 89 | } 90 | 91 | return lastIndex !== index ? html + str.substring(lastIndex, index) : html; 92 | } 93 | // end code copied and modified from escape-html 94 | 95 | /** 96 | * Escapes text to prevent scripting attacks. 97 | * 98 | * @param {*} text Text value to escape. 99 | * @return {string} An escaped string. 100 | */ 101 | function escapeTextForBrowser(text) { 102 | if (typeof text === 'boolean' || typeof text === 'number') { 103 | // this shortcircuit helps perf for types that we know will never have 104 | // special characters, especially given that this function is used often 105 | // for numeric dom ids. 106 | return '' + text; 107 | } 108 | return escapeHtml(text); 109 | } 110 | 111 | export default escapeTextForBrowser; 112 | -------------------------------------------------------------------------------- /src/server/DOMMarkupOperations.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { 9 | ATTRIBUTE_NAME_CHAR, 10 | ATTRIBUTE_NAME_START_CHAR, 11 | ID_ATTRIBUTE_NAME, 12 | ROOT_ATTRIBUTE_NAME, 13 | getPropertyInfo, 14 | shouldAttributeAcceptBooleanValue, 15 | shouldSetAttribute, 16 | } from '../shared/DOMProperty'; 17 | import quoteAttributeValueForBrowser from './quoteAttributeValueForBrowser'; 18 | import warning from 'fbjs/lib/warning'; 19 | 20 | // isAttributeNameSafe() is currently duplicated in DOMPropertyOperations. 21 | // TODO: Find a better place for this. 22 | var VALID_ATTRIBUTE_NAME_REGEX = new RegExp( 23 | '^[' + ATTRIBUTE_NAME_START_CHAR + '][' + ATTRIBUTE_NAME_CHAR + ']*$', 24 | ); 25 | var illegalAttributeNameCache = {}; 26 | var validatedAttributeNameCache = {}; 27 | function isAttributeNameSafe(attributeName) { 28 | if (validatedAttributeNameCache.hasOwnProperty(attributeName)) { 29 | return true; 30 | } 31 | if (illegalAttributeNameCache.hasOwnProperty(attributeName)) { 32 | return false; 33 | } 34 | if (VALID_ATTRIBUTE_NAME_REGEX.test(attributeName)) { 35 | validatedAttributeNameCache[attributeName] = true; 36 | return true; 37 | } 38 | illegalAttributeNameCache[attributeName] = true; 39 | if (__DEV__) { 40 | warning(false, 'Invalid attribute name: `%s`', attributeName); 41 | } 42 | return false; 43 | } 44 | 45 | // shouldIgnoreValue() is currently duplicated in DOMPropertyOperations. 46 | // TODO: Find a better place for this. 47 | function shouldIgnoreValue(propertyInfo, value) { 48 | return ( 49 | value == null || 50 | (propertyInfo.hasBooleanValue && !value) || 51 | (propertyInfo.hasNumericValue && isNaN(value)) || 52 | (propertyInfo.hasPositiveNumericValue && value < 1) || 53 | (propertyInfo.hasOverloadedBooleanValue && value === false) 54 | ); 55 | } 56 | 57 | /** 58 | * Operations for dealing with DOM properties. 59 | */ 60 | 61 | /** 62 | * Creates markup for the ID property. 63 | * 64 | * @param {string} id Unescaped ID. 65 | * @return {string} Markup string. 66 | */ 67 | export function createMarkupForID(id) { 68 | return ID_ATTRIBUTE_NAME + '=' + quoteAttributeValueForBrowser(id); 69 | } 70 | 71 | export function createMarkupForRoot() { 72 | return ROOT_ATTRIBUTE_NAME + '=""'; 73 | } 74 | 75 | /** 76 | * Creates markup for a property. 77 | * 78 | * @param {string} name 79 | * @param {*} value 80 | * @return {?string} Markup string, or null if the property was invalid. 81 | */ 82 | export function createMarkupForProperty(name, value) { 83 | var propertyInfo = getPropertyInfo(name); 84 | if (propertyInfo) { 85 | if (shouldIgnoreValue(propertyInfo, value)) { 86 | return ''; 87 | } 88 | var attributeName = propertyInfo.attributeName; 89 | if ( 90 | propertyInfo.hasBooleanValue || 91 | (propertyInfo.hasOverloadedBooleanValue && value === true) 92 | ) { 93 | return attributeName + '=""'; 94 | } else if ( 95 | typeof value !== 'boolean' || 96 | shouldAttributeAcceptBooleanValue(name) 97 | ) { 98 | return attributeName + '=' + quoteAttributeValueForBrowser(value); 99 | } 100 | } else if (shouldSetAttribute(name, value)) { 101 | if (value == null) { 102 | return ''; 103 | } 104 | return name + '=' + quoteAttributeValueForBrowser(value); 105 | } 106 | return null; 107 | } 108 | 109 | /** 110 | * Creates markup for a custom property. 111 | * 112 | * @param {string} name 113 | * @param {*} value 114 | * @return {string} Markup string, or empty string if the property was invalid. 115 | */ 116 | export function createMarkupForCustomAttribute(name, value) { 117 | if (!isAttributeNameSafe(name) || value == null) { 118 | return ''; 119 | } 120 | return name + '=' + quoteAttributeValueForBrowser(value); 121 | } 122 | -------------------------------------------------------------------------------- /src/shared/SVGDOMPropertyConfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import {injection} from './DOMProperty'; 9 | 10 | var {HAS_STRING_BOOLEAN_VALUE} = injection; 11 | 12 | var NS = { 13 | xlink: 'http://www.w3.org/1999/xlink', 14 | xml: 'http://www.w3.org/XML/1998/namespace', 15 | }; 16 | 17 | /** 18 | * This is a list of all SVG attributes that need special casing, 19 | * namespacing, or boolean value assignment. 20 | * 21 | * When adding attributes to this list, be sure to also add them to 22 | * the `possibleStandardNames` module to ensure casing and incorrect 23 | * name warnings. 24 | * 25 | * SVG Attributes List: 26 | * https://www.w3.org/TR/SVG/attindex.html 27 | * SMIL Spec: 28 | * https://www.w3.org/TR/smil 29 | */ 30 | var ATTRS = [ 31 | 'accent-height', 32 | 'alignment-baseline', 33 | 'arabic-form', 34 | 'baseline-shift', 35 | 'cap-height', 36 | 'clip-path', 37 | 'clip-rule', 38 | 'color-interpolation', 39 | 'color-interpolation-filters', 40 | 'color-profile', 41 | 'color-rendering', 42 | 'dominant-baseline', 43 | 'enable-background', 44 | 'fill-opacity', 45 | 'fill-rule', 46 | 'flood-color', 47 | 'flood-opacity', 48 | 'font-family', 49 | 'font-size', 50 | 'font-size-adjust', 51 | 'font-stretch', 52 | 'font-style', 53 | 'font-variant', 54 | 'font-weight', 55 | 'glyph-name', 56 | 'glyph-orientation-horizontal', 57 | 'glyph-orientation-vertical', 58 | 'horiz-adv-x', 59 | 'horiz-origin-x', 60 | 'image-rendering', 61 | 'letter-spacing', 62 | 'lighting-color', 63 | 'marker-end', 64 | 'marker-mid', 65 | 'marker-start', 66 | 'overline-position', 67 | 'overline-thickness', 68 | 'paint-order', 69 | 'panose-1', 70 | 'pointer-events', 71 | 'rendering-intent', 72 | 'shape-rendering', 73 | 'stop-color', 74 | 'stop-opacity', 75 | 'strikethrough-position', 76 | 'strikethrough-thickness', 77 | 'stroke-dasharray', 78 | 'stroke-dashoffset', 79 | 'stroke-linecap', 80 | 'stroke-linejoin', 81 | 'stroke-miterlimit', 82 | 'stroke-opacity', 83 | 'stroke-width', 84 | 'text-anchor', 85 | 'text-decoration', 86 | 'text-rendering', 87 | 'underline-position', 88 | 'underline-thickness', 89 | 'unicode-bidi', 90 | 'unicode-range', 91 | 'units-per-em', 92 | 'v-alphabetic', 93 | 'v-hanging', 94 | 'v-ideographic', 95 | 'v-mathematical', 96 | 'vector-effect', 97 | 'vert-adv-y', 98 | 'vert-origin-x', 99 | 'vert-origin-y', 100 | 'word-spacing', 101 | 'writing-mode', 102 | 'x-height', 103 | 'xlink:actuate', 104 | 'xlink:arcrole', 105 | 'xlink:href', 106 | 'xlink:role', 107 | 'xlink:show', 108 | 'xlink:title', 109 | 'xlink:type', 110 | 'xml:base', 111 | 'xmlns:xlink', 112 | 'xml:lang', 113 | 'xml:space', 114 | ]; 115 | 116 | var SVGDOMPropertyConfig = { 117 | Properties: { 118 | autoReverse: HAS_STRING_BOOLEAN_VALUE, 119 | externalResourcesRequired: HAS_STRING_BOOLEAN_VALUE, 120 | preserveAlpha: HAS_STRING_BOOLEAN_VALUE, 121 | }, 122 | DOMAttributeNames: { 123 | autoReverse: 'autoReverse', 124 | externalResourcesRequired: 'externalResourcesRequired', 125 | preserveAlpha: 'preserveAlpha', 126 | }, 127 | DOMAttributeNamespaces: { 128 | xlinkActuate: NS.xlink, 129 | xlinkArcrole: NS.xlink, 130 | xlinkHref: NS.xlink, 131 | xlinkRole: NS.xlink, 132 | xlinkShow: NS.xlink, 133 | xlinkTitle: NS.xlink, 134 | xlinkType: NS.xlink, 135 | xmlBase: NS.xml, 136 | xmlLang: NS.xml, 137 | xmlSpace: NS.xml, 138 | }, 139 | }; 140 | 141 | var CAMELIZE = /[\-\:]([a-z])/g; 142 | var capitalize = token => token[1].toUpperCase(); 143 | 144 | ATTRS.forEach(original => { 145 | var reactName = original.replace(CAMELIZE, capitalize); 146 | 147 | SVGDOMPropertyConfig.Properties[reactName] = 0; 148 | SVGDOMPropertyConfig.DOMAttributeNames[reactName] = original; 149 | }); 150 | 151 | export default SVGDOMPropertyConfig; 152 | -------------------------------------------------------------------------------- /src/shared/ReactDOMInvalidARIAHook.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import warning from 'fbjs/lib/warning'; 9 | import {ReactDebugCurrentFrame} from 'shared/ReactGlobalSharedState'; 10 | 11 | import {ATTRIBUTE_NAME_CHAR} from './DOMProperty'; 12 | import isCustomComponent from './isCustomComponent'; 13 | import validAriaProperties from './validAriaProperties'; 14 | 15 | var warnedProperties = {}; 16 | var rARIA = new RegExp('^(aria)-[' + ATTRIBUTE_NAME_CHAR + ']*$'); 17 | var rARIACamel = new RegExp('^(aria)[A-Z][' + ATTRIBUTE_NAME_CHAR + ']*$'); 18 | 19 | var hasOwnProperty = Object.prototype.hasOwnProperty; 20 | 21 | function getStackAddendum() { 22 | var stack = ReactDebugCurrentFrame.getStackAddendum(); 23 | return stack != null ? stack : ''; 24 | } 25 | 26 | function validateProperty(tagName, name) { 27 | if (hasOwnProperty.call(warnedProperties, name) && warnedProperties[name]) { 28 | return true; 29 | } 30 | 31 | if (rARIACamel.test(name)) { 32 | var ariaName = 'aria-' + name.slice(4).toLowerCase(); 33 | var correctName = validAriaProperties.hasOwnProperty(ariaName) 34 | ? ariaName 35 | : null; 36 | 37 | // If this is an aria-* attribute, but is not listed in the known DOM 38 | // DOM properties, then it is an invalid aria-* attribute. 39 | if (correctName == null) { 40 | warning( 41 | false, 42 | 'Invalid ARIA attribute `%s`. ARIA attributes follow the pattern aria-* and must be lowercase.%s', 43 | name, 44 | getStackAddendum(), 45 | ); 46 | warnedProperties[name] = true; 47 | return true; 48 | } 49 | // aria-* attributes should be lowercase; suggest the lowercase version. 50 | if (name !== correctName) { 51 | warning( 52 | false, 53 | 'Invalid ARIA attribute `%s`. Did you mean `%s`?%s', 54 | name, 55 | correctName, 56 | getStackAddendum(), 57 | ); 58 | warnedProperties[name] = true; 59 | return true; 60 | } 61 | } 62 | 63 | if (rARIA.test(name)) { 64 | var lowerCasedName = name.toLowerCase(); 65 | var standardName = validAriaProperties.hasOwnProperty(lowerCasedName) 66 | ? lowerCasedName 67 | : null; 68 | 69 | // If this is an aria-* attribute, but is not listed in the known DOM 70 | // DOM properties, then it is an invalid aria-* attribute. 71 | if (standardName == null) { 72 | warnedProperties[name] = true; 73 | return false; 74 | } 75 | // aria-* attributes should be lowercase; suggest the lowercase version. 76 | if (name !== standardName) { 77 | warning( 78 | false, 79 | 'Unknown ARIA attribute `%s`. Did you mean `%s`?%s', 80 | name, 81 | standardName, 82 | getStackAddendum(), 83 | ); 84 | warnedProperties[name] = true; 85 | return true; 86 | } 87 | } 88 | 89 | return true; 90 | } 91 | 92 | function warnInvalidARIAProps(type, props) { 93 | const invalidProps = []; 94 | 95 | for (var key in props) { 96 | var isValid = validateProperty(type, key); 97 | if (!isValid) { 98 | invalidProps.push(key); 99 | } 100 | } 101 | 102 | const unknownPropString = invalidProps 103 | .map(prop => '`' + prop + '`') 104 | .join(', '); 105 | 106 | if (invalidProps.length === 1) { 107 | warning( 108 | false, 109 | 'Invalid aria prop %s on <%s> tag. ' + 110 | 'For details, see https://fb.me/invalid-aria-prop%s', 111 | unknownPropString, 112 | type, 113 | getStackAddendum(), 114 | ); 115 | } else if (invalidProps.length > 1) { 116 | warning( 117 | false, 118 | 'Invalid aria props %s on <%s> tag. ' + 119 | 'For details, see https://fb.me/invalid-aria-prop%s', 120 | unknownPropString, 121 | type, 122 | getStackAddendum(), 123 | ); 124 | } 125 | } 126 | 127 | export function validateProperties(type, props) { 128 | if (isCustomComponent(type, props)) { 129 | return; 130 | } 131 | warnInvalidARIAProps(type, props); 132 | } 133 | -------------------------------------------------------------------------------- /src/shared/HTMLDOMPropertyConfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import {injection} from './DOMProperty'; 9 | 10 | var MUST_USE_PROPERTY = injection.MUST_USE_PROPERTY; 11 | var HAS_BOOLEAN_VALUE = injection.HAS_BOOLEAN_VALUE; 12 | var HAS_NUMERIC_VALUE = injection.HAS_NUMERIC_VALUE; 13 | var HAS_POSITIVE_NUMERIC_VALUE = injection.HAS_POSITIVE_NUMERIC_VALUE; 14 | var HAS_OVERLOADED_BOOLEAN_VALUE = injection.HAS_OVERLOADED_BOOLEAN_VALUE; 15 | var HAS_STRING_BOOLEAN_VALUE = injection.HAS_STRING_BOOLEAN_VALUE; 16 | 17 | var HTMLDOMPropertyConfig = { 18 | // When adding attributes to this list, be sure to also add them to 19 | // the `possibleStandardNames` module to ensure casing and incorrect 20 | // name warnings. 21 | Properties: { 22 | allowFullScreen: HAS_BOOLEAN_VALUE, 23 | // specifies target context for links with `preload` type 24 | async: HAS_BOOLEAN_VALUE, 25 | // Note: there is a special case that prevents it from being written to the DOM 26 | // on the client side because the browsers are inconsistent. Instead we call focus(). 27 | autoFocus: HAS_BOOLEAN_VALUE, 28 | autoPlay: HAS_BOOLEAN_VALUE, 29 | capture: HAS_OVERLOADED_BOOLEAN_VALUE, 30 | checked: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE, 31 | cols: HAS_POSITIVE_NUMERIC_VALUE, 32 | contentEditable: HAS_STRING_BOOLEAN_VALUE, 33 | controls: HAS_BOOLEAN_VALUE, 34 | default: HAS_BOOLEAN_VALUE, 35 | defer: HAS_BOOLEAN_VALUE, 36 | disabled: HAS_BOOLEAN_VALUE, 37 | download: HAS_OVERLOADED_BOOLEAN_VALUE, 38 | draggable: HAS_STRING_BOOLEAN_VALUE, 39 | formNoValidate: HAS_BOOLEAN_VALUE, 40 | hidden: HAS_BOOLEAN_VALUE, 41 | loop: HAS_BOOLEAN_VALUE, 42 | // Caution; `option.selected` is not updated if `select.multiple` is 43 | // disabled with `removeAttribute`. 44 | multiple: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE, 45 | muted: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE, 46 | noValidate: HAS_BOOLEAN_VALUE, 47 | open: HAS_BOOLEAN_VALUE, 48 | playsInline: HAS_BOOLEAN_VALUE, 49 | readOnly: HAS_BOOLEAN_VALUE, 50 | required: HAS_BOOLEAN_VALUE, 51 | reversed: HAS_BOOLEAN_VALUE, 52 | rows: HAS_POSITIVE_NUMERIC_VALUE, 53 | rowSpan: HAS_NUMERIC_VALUE, 54 | scoped: HAS_BOOLEAN_VALUE, 55 | seamless: HAS_BOOLEAN_VALUE, 56 | selected: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE, 57 | size: HAS_POSITIVE_NUMERIC_VALUE, 58 | start: HAS_NUMERIC_VALUE, 59 | // support for projecting regular DOM Elements via V1 named slots ( shadow dom ) 60 | span: HAS_POSITIVE_NUMERIC_VALUE, 61 | spellCheck: HAS_STRING_BOOLEAN_VALUE, 62 | // Style must be explicitly set in the attribute list. React components 63 | // expect a style object 64 | style: 0, 65 | // Keep it in the whitelist because it is case-sensitive for SVG. 66 | tabIndex: 0, 67 | // itemScope is for for Microdata support. 68 | // See http://schema.org/docs/gs.html 69 | itemScope: HAS_BOOLEAN_VALUE, 70 | // These attributes must stay in the white-list because they have 71 | // different attribute names (see DOMAttributeNames below) 72 | acceptCharset: 0, 73 | className: 0, 74 | htmlFor: 0, 75 | httpEquiv: 0, 76 | // Attributes with mutation methods must be specified in the whitelist 77 | // Set the string boolean flag to allow the behavior 78 | value: HAS_STRING_BOOLEAN_VALUE, 79 | }, 80 | DOMAttributeNames: { 81 | acceptCharset: 'accept-charset', 82 | className: 'class', 83 | htmlFor: 'for', 84 | httpEquiv: 'http-equiv', 85 | }, 86 | DOMMutationMethods: { 87 | value: function(node, value) { 88 | if (value == null) { 89 | return node.removeAttribute('value'); 90 | } 91 | 92 | // Number inputs get special treatment due to some edge cases in 93 | // Chrome. Let everything else assign the value attribute as normal. 94 | // https://github.com/facebook/react/issues/7253#issuecomment-236074326 95 | if (node.type !== 'number' || node.hasAttribute('value') === false) { 96 | node.setAttribute('value', '' + value); 97 | } else if ( 98 | node.validity && 99 | !node.validity.badInput && 100 | node.ownerDocument.activeElement !== node 101 | ) { 102 | // Don't assign an attribute if validation reports bad 103 | // input. Chrome will clear the value. Additionally, don't 104 | // operate on inputs that have focus, otherwise Chrome might 105 | // strip off trailing decimal places and cause the user's 106 | // cursor position to jump to the beginning of the input. 107 | // 108 | // In ReactDOMInput, we have an onBlur event that will trigger 109 | // this function again when focus is lost. 110 | node.setAttribute('value', '' + value); 111 | } 112 | }, 113 | }, 114 | }; 115 | 116 | export default HTMLDOMPropertyConfig; 117 | -------------------------------------------------------------------------------- /src/server/ReactDOMNodeStreamRenderer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import {Readable} from 'stream'; 9 | import { Transform } from 'stream'; 10 | 11 | import ReactPartialRenderer from './ReactPartialRenderer'; 12 | 13 | // This is a Readable Node.js stream which wraps the ReactDOMPartialRenderer. 14 | class ReactMarkupReadableStream extends Readable { 15 | constructor(element, makeStaticMarkup, cache, streamingStart, memLife) { 16 | // Calls the stream.Readable(options) constructor. Consider exposing built-in 17 | // features like highWaterMark in the future. 18 | super({}); 19 | this.cache = cache; 20 | this.streamingStart = streamingStart; 21 | this.memLife = memLife; 22 | this.partialRenderer = new ReactPartialRenderer(element, makeStaticMarkup); 23 | } 24 | 25 | async _read(size) { 26 | try { 27 | let readOutput = await this.partialRenderer.read(size, 28 | this.cache, 29 | true, 30 | this.streamingStart, 31 | this.memLife); 32 | this.push(readOutput); 33 | } catch (err) { 34 | this.emit('error', err); 35 | } 36 | } 37 | } 38 | 39 | function createCacheStream(cache, streamingStart, memLife = 0) { 40 | const bufferedChunks = []; 41 | return new Transform({ 42 | // transform() is called with each chunk of data 43 | transform(data, enc, cb) { 44 | // We store the chunk of data (which is a Buffer) in memory 45 | bufferedChunks.push(data); 46 | // Then pass the data unchanged onwards to the next stream 47 | cb(null, data); 48 | }, 49 | 50 | // flush() is called when everything is done 51 | flush(cb) { 52 | // We concatenate all the buffered chunks of HTML to get the full HTML, then cache it at "key" 53 | let html = bufferedChunks.join(""); 54 | delete streamingStart.sliceStartCount; 55 | 56 | for (let component in streamingStart) { 57 | let tagStack = []; 58 | let tagStart; 59 | let tagEnd; 60 | 61 | do { 62 | if (!tagStart) { 63 | tagStart = streamingStart[component]; 64 | } else { 65 | tagStart = (html[tagEnd] === '<') ? tagEnd : html.indexOf('<', tagEnd); 66 | } 67 | tagEnd = html.indexOf('>', tagStart) + 1; 68 | // Skip stack logic for void/self-closing elements and HTML comments 69 | if (html[tagEnd - 2] !== '/' && html[tagStart + 1] !== '!') { 70 | // Push opening tags onto stack; pop closing tags off of stack 71 | if (html[tagStart + 1] !== '/') { 72 | tagStack.push(html.slice(tagStart, tagEnd)); 73 | } else { 74 | tagStack.pop(); 75 | } 76 | } 77 | } while (tagStack.length !== 0); 78 | // cache component by slicing 'html' 79 | if (memLife) { 80 | cache.set(component, html.slice(streamingStart[component], tagEnd), memLife, (err) => { 81 | if (err) { 82 | console.log(err); 83 | } 84 | }); 85 | } else { 86 | cache.set(component, html.slice(streamingStart[component], tagEnd)); 87 | } 88 | } 89 | cb(); 90 | }, 91 | }); 92 | } 93 | /** 94 | * Render a ReactElement to its initial HTML. This should only be used on the 95 | * server. 96 | * See https://reactjs.org/docs/react-dom-stream.html#rendertonodestream 97 | */ 98 | function originalRenderToNodeStream(element, cache, streamingStart, memLife=0) { 99 | return new ReactMarkupReadableStream(element, false, cache, streamingStart, memLife); 100 | } 101 | 102 | export function renderToNodeStream(element, cache, res, htmlStart, htmlEnd, memLife) { 103 | 104 | const streamingStart = { 105 | sliceStartCount: htmlStart.length, 106 | }; 107 | 108 | const cacheStream = createCacheStream(cache, streamingStart); 109 | cacheStream.pipe(res); 110 | cacheStream.write(htmlStart); 111 | 112 | const stream = originalRenderToNodeStream(element, cache, streamingStart, memLife); 113 | stream.pipe(cacheStream, { end: false }); 114 | stream.on("end", () => { 115 | cacheStream.end(htmlEnd); 116 | }); 117 | 118 | } 119 | 120 | /** 121 | * Similar to renderToNodeStream, except this doesn't create extra DOM attributes 122 | * such as data-react-id that React uses internally. 123 | * See https://reactjs.org/docs/react-dom-stream.html#rendertostaticnodestream 124 | */ 125 | function originalRenderToStaticNodeStream(element, cache, streamingStart, memLife=0) { 126 | return new ReactMarkupReadableStream(element, true, cache, streamingStart, memLife); 127 | } 128 | 129 | export function renderToStaticNodeStream(element, cache, res, htmlStart, htmlEnd, memLife) { 130 | 131 | const streamingStart = { 132 | sliceStartCount: htmlStart.length, 133 | }; 134 | 135 | const cacheStream = createCacheStream(cache, streamingStart); 136 | cacheStream.pipe(res); 137 | cacheStream.write(htmlStart); 138 | 139 | const stream = originalRenderToStaticNodeStream(element, cache, streamingStart, memLife); 140 | stream.pipe(cacheStream, { end: false }); 141 | stream.on("end", () => { 142 | cacheStream.end(htmlEnd); 143 | }); 144 | } 145 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Component Caching 2 | 3 | ## Overview 4 | React Component Caching is a component-level caching library for faster server-side rendering with React 16. 5 | - Use any of React's four server-side rendering methods. Rendering is **asynchronous**. 6 | - Cache components using a simple or template strategy. 7 | - Choose from three cache implementations (LRU, Redis, or Memcached). 8 | 9 | ## Installation 10 | Using npm: 11 | ```shell 12 | $ npm install react-component-caching 13 | ``` 14 | 15 | ## Usage 16 | ### In Node rendering server: 17 | Instantiate a cache and pass it to any rendering method (`renderToString`, `renderToStaticMarkup`, `renderToNodeStream`, or `renderToStaticNodeStream`) as a second argument. Wherever you would use `ReactDOM.renderToString`, use `ReactCC.renderToString`. 18 | 19 | **Note: All of these methods are asynchronous, and return a promise. To use them, `await` the response before rendering** 20 | ```javascript 21 | const ReactCC = require("react-component-caching"); 22 | const cache = new ReactCC.ComponentCache(); 23 | 24 | app.get('/example', async (req,res) => { 25 | const renderString = await ReactCC.renderToString(, cache); 26 | res.send(renderString); 27 | }); 28 | 29 | // ... 30 | ``` 31 | 32 | ### In React app: 33 | To flag a component for caching, simply add a `cache` property to it. 34 | 35 | ```javascript 36 | export default class App extends Component { 37 | render() { 38 | return ( 39 |
40 | 41 | 42 |
43 | ); 44 | } 45 | } 46 | // ... 47 | ``` 48 | 49 | ## Templatizing Cached Components 50 | The example above employs a simple caching strategy: a rendered component is saved with its prop values. Each time the component is rendered with different prop values, a separate copy is saved to the cache. If a component is frequently rendered with different prop values, you may prefer to cache a template of the component to save space in the cache. The template strategy stores a version of the component with placeholders (e.g. `{{0}}`, `{{1}}`) in place of actual prop values. 51 | 52 | To create a cache template, add both `cache` and `templatized` to the component along with an array of props to templatize. Templatized props should have **string** or **number** values. **Be aware that templates are not currently supported with the `renderToNodeStream` or `renderToStaticNodeStream` methods.** 53 | 54 | ```javascript 55 | export default class App extends Component { 56 | render() { 57 | return ( 58 |
59 | 60 | 61 | 67 |
68 | ); 69 | } 70 | } 71 | // ... 72 | ``` 73 | ## Streaming HTML Markup 74 | To use streaming on the server side, use either the renderToStaticNodeStream or renderToNodeStream function. Both streaming option works with caching, but not yet compatible with templatization. To use the streaming functions, simply pass in these 5 arguments: 75 | ( 76 | `component`: The React component being rendered 77 | `cache`: The component cache object 78 | `res`: The response object that Express provides 79 | `htmlStart`: Start of html markup in string form 80 | `htmlEnd`: End of html markup in string form 81 | ). 82 | The benefit that comes with streaming is faster time to first byte, which translates to faster viewing of page content. 83 | 84 | ## Cache Options 85 | React Component Caching provides its own cache implementation as well as support for Redis and Memcached. Simply create your preferred cache and pass it into one of the rendering methods. 86 | 87 | **Standard (LRU) Cache Example:** 88 | 89 | ```javascript 90 | const ReactCC = require("react-component-caching"); 91 | const cache = new ReactCC.ComponentCache(); 92 | ``` 93 | 94 | **Redis Example:** 95 | 96 | ```javascript 97 | const ReactCC = require("react-component-caching"); 98 | const redis = require("redis"); 99 | const cache = redis.createClient(); 100 | ``` 101 | 102 | **Memcached Example:** 103 | 104 | ```javascript 105 | const ReactCC = require("react-component-caching"); 106 | const Memcached = require("memcached"); 107 | const cache = new Memcached(server location, options); 108 | 109 | // If using Memcached, make sure to pass in the lifetime of the data (in seconds) as a number. 110 | ReactCC.renderToString(, cache, 1000); 111 | ``` 112 | 113 | ## API 114 | 115 | ### React Component Caching 116 | React Component Caching gives you access to all four of React 16's server-side rendering methods, as well as additional functionality. Available methods are described below. 117 | 118 | ### ComponentCache 119 | - `size`: (*Optional*) An integer representing the maximum size (in characters) of the cache. Defaults to 1 million. 120 | 121 | **Example:** 122 | ```javascript 123 | const cache = new ReactCC.ComponentCache(); 124 | ``` 125 | 126 | ### renderToString 127 | - `component`: The React component being rendered 128 | - `cache`: The component cache 129 | - `memLife`: (*Only if using Memcached*) A number representing the lifetime (in seconds) of each Memcached entry. Defaults to 0. 130 | 131 | **Example:** 132 | ```javascript 133 | ReactCC.renderToString(, cache); 134 | ``` 135 | 136 | ### renderToStaticMarkup 137 | - `component`: The React component being rendered 138 | - `cache`: The component cache 139 | - `memLife`: (*Only if using Memcached*) An integer representing the lifetime (in seconds) of each Memcached entry. Defaults to 0. 140 | 141 | **Example:** 142 | ```javascript 143 | ReactCC.renderToStaticMarkup(, cache); 144 | ``` 145 | 146 | ### renderToNodeStream 147 | - `component`: The React component being rendered 148 | - `cache`: The component cache object 149 | - `res`: The response object that Express provides 150 | - `htmlStart`: Start of html markup in string form 151 | - `htmlEnd`: End of html markup in string form 152 | - `memLife`: (*Only if using Memcached*) An integer representing the lifetime (in seconds) of each Memcached entry. Defaults to 0. 153 | 154 | **Example:** 155 | ```javascript 156 | let htmlStart = 'Page
'; 157 | let htmlEnd = '
'; 158 | ReactCC.renderToNodeStream(, cache, res, htmlStart, htmlEnd); 159 | ``` 160 | 161 | ### renderToStaticNodeStream 162 | - `component`: The React component being rendered 163 | - `cache`: The component cache object 164 | - `res`: The response object that Express provides 165 | - `htmlStart`: Start of html markup in string form 166 | - `htmlEnd`: End of html markup in string form 167 | - `memLife`: (*Only if using Memcached*) An integer representing the lifetime (in seconds) of each Memcached entry. Defaults to 0. 168 | 169 | **Example:** 170 | ```javascript 171 | let htmlStart = 'Page
'; 172 | let htmlEnd = '
'; 173 | ReactCC.renderToStaticNodeStream(, cache, res, htmlStart, htmlEnd); 174 | ``` 175 | 176 | ## Authors 177 | - [Mejin Leechor](https://github.com/mejincodes) 178 | - [Annie DiFiore](https://github.com/adifiore) 179 | - [Steven Lee](https://github.com/stevedorke) 180 | - [Tim Hong](https://github.com/tjhong30) 181 | - [Zihao Li](https://github.com/kodakyellow) -------------------------------------------------------------------------------- /src/shared/DOMProperty.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import invariant from 'fbjs/lib/invariant'; 9 | 10 | // These attributes should be all lowercase to allow for 11 | // case insensitive checks 12 | var RESERVED_PROPS = { 13 | children: true, 14 | dangerouslySetInnerHTML: true, 15 | defaultValue: true, 16 | defaultChecked: true, 17 | innerHTML: true, 18 | suppressContentEditableWarning: true, 19 | suppressHydrationWarning: true, 20 | style: true, 21 | }; 22 | 23 | function checkMask(value, bitmask) { 24 | return (value & bitmask) === bitmask; 25 | } 26 | 27 | var DOMPropertyInjection = { 28 | /** 29 | * Mapping from normalized, camelcased property names to a configuration that 30 | * specifies how the associated DOM property should be accessed or rendered. 31 | */ 32 | MUST_USE_PROPERTY: 0x1, 33 | HAS_BOOLEAN_VALUE: 0x4, 34 | HAS_NUMERIC_VALUE: 0x8, 35 | HAS_POSITIVE_NUMERIC_VALUE: 0x10 | 0x8, 36 | HAS_OVERLOADED_BOOLEAN_VALUE: 0x20, 37 | HAS_STRING_BOOLEAN_VALUE: 0x40, 38 | 39 | /** 40 | * Inject some specialized knowledge about the DOM. This takes a config object 41 | * with the following properties: 42 | * 43 | * Properties: object mapping DOM property name to one of the 44 | * DOMPropertyInjection constants or null. If your attribute isn't in here, 45 | * it won't get written to the DOM. 46 | * 47 | * DOMAttributeNames: object mapping React attribute name to the DOM 48 | * attribute name. Attribute names not specified use the **lowercase** 49 | * normalized name. 50 | * 51 | * DOMAttributeNamespaces: object mapping React attribute name to the DOM 52 | * attribute namespace URL. (Attribute names not specified use no namespace.) 53 | * 54 | * DOMPropertyNames: similar to DOMAttributeNames but for DOM properties. 55 | * Property names not specified use the normalized name. 56 | * 57 | * DOMMutationMethods: Properties that require special mutation methods. If 58 | * `value` is undefined, the mutation method should unset the property. 59 | * 60 | * @param {object} domPropertyConfig the config as described above. 61 | */ 62 | injectDOMPropertyConfig: function(domPropertyConfig) { 63 | var Injection = DOMPropertyInjection; 64 | var Properties = domPropertyConfig.Properties || {}; 65 | var DOMAttributeNamespaces = domPropertyConfig.DOMAttributeNamespaces || {}; 66 | var DOMAttributeNames = domPropertyConfig.DOMAttributeNames || {}; 67 | var DOMMutationMethods = domPropertyConfig.DOMMutationMethods || {}; 68 | 69 | for (var propName in Properties) { 70 | invariant( 71 | !properties.hasOwnProperty(propName), 72 | "injectDOMPropertyConfig(...): You're trying to inject DOM property " + 73 | "'%s' which has already been injected. You may be accidentally " + 74 | 'injecting the same DOM property config twice, or you may be ' + 75 | 'injecting two configs that have conflicting property names.', 76 | propName, 77 | ); 78 | 79 | var lowerCased = propName.toLowerCase(); 80 | var propConfig = Properties[propName]; 81 | 82 | var propertyInfo = { 83 | attributeName: lowerCased, 84 | attributeNamespace: null, 85 | propertyName: propName, 86 | mutationMethod: null, 87 | 88 | mustUseProperty: checkMask(propConfig, Injection.MUST_USE_PROPERTY), 89 | hasBooleanValue: checkMask(propConfig, Injection.HAS_BOOLEAN_VALUE), 90 | hasNumericValue: checkMask(propConfig, Injection.HAS_NUMERIC_VALUE), 91 | hasPositiveNumericValue: checkMask( 92 | propConfig, 93 | Injection.HAS_POSITIVE_NUMERIC_VALUE, 94 | ), 95 | hasOverloadedBooleanValue: checkMask( 96 | propConfig, 97 | Injection.HAS_OVERLOADED_BOOLEAN_VALUE, 98 | ), 99 | hasStringBooleanValue: checkMask( 100 | propConfig, 101 | Injection.HAS_STRING_BOOLEAN_VALUE, 102 | ), 103 | }; 104 | invariant( 105 | propertyInfo.hasBooleanValue + 106 | propertyInfo.hasNumericValue + 107 | propertyInfo.hasOverloadedBooleanValue <= 108 | 1, 109 | 'DOMProperty: Value can be one of boolean, overloaded boolean, or ' + 110 | 'numeric value, but not a combination: %s', 111 | propName, 112 | ); 113 | 114 | if (DOMAttributeNames.hasOwnProperty(propName)) { 115 | var attributeName = DOMAttributeNames[propName]; 116 | 117 | propertyInfo.attributeName = attributeName; 118 | } 119 | 120 | if (DOMAttributeNamespaces.hasOwnProperty(propName)) { 121 | propertyInfo.attributeNamespace = DOMAttributeNamespaces[propName]; 122 | } 123 | 124 | if (DOMMutationMethods.hasOwnProperty(propName)) { 125 | propertyInfo.mutationMethod = DOMMutationMethods[propName]; 126 | } 127 | 128 | // Downcase references to whitelist properties to check for membership 129 | // without case-sensitivity. This allows the whitelist to pick up 130 | // `allowfullscreen`, which should be written using the property configuration 131 | // for `allowFullscreen` 132 | properties[propName] = propertyInfo; 133 | } 134 | }, 135 | }; 136 | 137 | /* eslint-disable max-len */ 138 | export const ATTRIBUTE_NAME_START_CHAR = 139 | ':A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD'; 140 | /* eslint-enable max-len */ 141 | export const ATTRIBUTE_NAME_CHAR = 142 | ATTRIBUTE_NAME_START_CHAR + '\\-.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040'; 143 | 144 | export const ID_ATTRIBUTE_NAME = 'data-reactid'; 145 | export const ROOT_ATTRIBUTE_NAME = 'data-reactroot'; 146 | 147 | /** 148 | * Map from property "standard name" to an object with info about how to set 149 | * the property in the DOM. Each object contains: 150 | * 151 | * attributeName: 152 | * Used when rendering markup or with `*Attribute()`. 153 | * attributeNamespace 154 | * propertyName: 155 | * Used on DOM node instances. (This includes properties that mutate due to 156 | * external factors.) 157 | * mutationMethod: 158 | * If non-null, used instead of the property or `setAttribute()` after 159 | * initial render. 160 | * mustUseProperty: 161 | * Whether the property must be accessed and mutated as an object property. 162 | * hasBooleanValue: 163 | * Whether the property should be removed when set to a falsey value. 164 | * hasNumericValue: 165 | * Whether the property must be numeric or parse as a numeric and should be 166 | * removed when set to a falsey value. 167 | * hasPositiveNumericValue: 168 | * Whether the property must be positive numeric or parse as a positive 169 | * numeric and should be removed when set to a falsey value. 170 | * hasOverloadedBooleanValue: 171 | * Whether the property can be used as a flag as well as with a value. 172 | * Removed when strictly equal to false; present without a value when 173 | * strictly equal to true; present with a value otherwise. 174 | */ 175 | export const properties = {}; 176 | 177 | /** 178 | * Checks whether a property name is a writeable attribute. 179 | * @method 180 | */ 181 | export function shouldSetAttribute(name, value) { 182 | if (isReservedProp(name)) { 183 | return false; 184 | } 185 | if ( 186 | name.length > 2 && 187 | (name[0] === 'o' || name[0] === 'O') && 188 | (name[1] === 'n' || name[1] === 'N') 189 | ) { 190 | return false; 191 | } 192 | if (value === null) { 193 | return true; 194 | } 195 | switch (typeof value) { 196 | case 'boolean': 197 | return shouldAttributeAcceptBooleanValue(name); 198 | case 'undefined': 199 | case 'number': 200 | case 'string': 201 | case 'object': 202 | return true; 203 | default: 204 | // function, symbol 205 | return false; 206 | } 207 | } 208 | 209 | export function getPropertyInfo(name) { 210 | return properties.hasOwnProperty(name) ? properties[name] : null; 211 | } 212 | 213 | export function shouldAttributeAcceptBooleanValue(name) { 214 | if (isReservedProp(name)) { 215 | return true; 216 | } 217 | let propertyInfo = getPropertyInfo(name); 218 | if (propertyInfo) { 219 | return ( 220 | propertyInfo.hasBooleanValue || 221 | propertyInfo.hasStringBooleanValue || 222 | propertyInfo.hasOverloadedBooleanValue 223 | ); 224 | } 225 | var prefix = name.toLowerCase().slice(0, 5); 226 | return prefix === 'data-' || prefix === 'aria-'; 227 | } 228 | 229 | /** 230 | * Checks to see if a property name is within the list of properties 231 | * reserved for internal React operations. These properties should 232 | * not be set on an HTML element. 233 | * 234 | * @private 235 | * @param {string} name 236 | * @return {boolean} If the name is within reserved props 237 | */ 238 | export function isReservedProp(name) { 239 | return RESERVED_PROPS.hasOwnProperty(name); 240 | } 241 | 242 | export const injection = DOMPropertyInjection; 243 | -------------------------------------------------------------------------------- /src/shared/ReactDOMUnknownPropertyHook.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { 9 | registrationNameModules, 10 | possibleRegistrationNames, 11 | } from 'events/EventPluginRegistry'; 12 | import {ReactDebugCurrentFrame} from 'shared/ReactGlobalSharedState'; 13 | import warning from 'fbjs/lib/warning'; 14 | 15 | import { 16 | ATTRIBUTE_NAME_CHAR, 17 | isReservedProp, 18 | shouldAttributeAcceptBooleanValue, 19 | shouldSetAttribute, 20 | } from './DOMProperty'; 21 | import isCustomComponent from './isCustomComponent'; 22 | import possibleStandardNames from './possibleStandardNames'; 23 | 24 | function getStackAddendum() { 25 | var stack = ReactDebugCurrentFrame.getStackAddendum(); 26 | return stack != null ? stack : ''; 27 | } 28 | 29 | if (__DEV__) { 30 | var warnedProperties = {}; 31 | var hasOwnProperty = Object.prototype.hasOwnProperty; 32 | var EVENT_NAME_REGEX = /^on./; 33 | var INVALID_EVENT_NAME_REGEX = /^on[^A-Z]/; 34 | var rARIA = new RegExp('^(aria)-[' + ATTRIBUTE_NAME_CHAR + ']*$'); 35 | var rARIACamel = new RegExp('^(aria)[A-Z][' + ATTRIBUTE_NAME_CHAR + ']*$'); 36 | 37 | var validateProperty = function(tagName, name, value, canUseEventSystem) { 38 | if (hasOwnProperty.call(warnedProperties, name) && warnedProperties[name]) { 39 | return true; 40 | } 41 | 42 | var lowerCasedName = name.toLowerCase(); 43 | if (lowerCasedName === 'onfocusin' || lowerCasedName === 'onfocusout') { 44 | warning( 45 | false, 46 | 'React uses onFocus and onBlur instead of onFocusIn and onFocusOut. ' + 47 | 'All React events are normalized to bubble, so onFocusIn and onFocusOut ' + 48 | 'are not needed/supported by React.', 49 | ); 50 | warnedProperties[name] = true; 51 | return true; 52 | } 53 | 54 | // We can't rely on the event system being injected on the server. 55 | if (canUseEventSystem) { 56 | if (registrationNameModules.hasOwnProperty(name)) { 57 | return true; 58 | } 59 | var registrationName = possibleRegistrationNames.hasOwnProperty( 60 | lowerCasedName, 61 | ) 62 | ? possibleRegistrationNames[lowerCasedName] 63 | : null; 64 | if (registrationName != null) { 65 | warning( 66 | false, 67 | 'Invalid event handler property `%s`. Did you mean `%s`?%s', 68 | name, 69 | registrationName, 70 | getStackAddendum(), 71 | ); 72 | warnedProperties[name] = true; 73 | return true; 74 | } 75 | if (EVENT_NAME_REGEX.test(name)) { 76 | warning( 77 | false, 78 | 'Unknown event handler property `%s`. It will be ignored.%s', 79 | name, 80 | getStackAddendum(), 81 | ); 82 | warnedProperties[name] = true; 83 | return true; 84 | } 85 | } else if (EVENT_NAME_REGEX.test(name)) { 86 | // If no event plugins have been injected, we are in a server environment. 87 | // So we can't tell if the event name is correct for sure, but we can filter 88 | // out known bad ones like `onclick`. We can't suggest a specific replacement though. 89 | if (INVALID_EVENT_NAME_REGEX.test(name)) { 90 | warning( 91 | false, 92 | 'Invalid event handler property `%s`. ' + 93 | 'React events use the camelCase naming convention, for example `onClick`.%s', 94 | name, 95 | getStackAddendum(), 96 | ); 97 | } 98 | warnedProperties[name] = true; 99 | return true; 100 | } 101 | 102 | // Let the ARIA attribute hook validate ARIA attributes 103 | if (rARIA.test(name) || rARIACamel.test(name)) { 104 | return true; 105 | } 106 | 107 | if (lowerCasedName === 'innerhtml') { 108 | warning( 109 | false, 110 | 'Directly setting property `innerHTML` is not permitted. ' + 111 | 'For more information, lookup documentation on `dangerouslySetInnerHTML`.', 112 | ); 113 | warnedProperties[name] = true; 114 | return true; 115 | } 116 | 117 | if (lowerCasedName === 'aria') { 118 | warning( 119 | false, 120 | 'The `aria` attribute is reserved for future use in React. ' + 121 | 'Pass individual `aria-` attributes instead.', 122 | ); 123 | warnedProperties[name] = true; 124 | return true; 125 | } 126 | 127 | if ( 128 | lowerCasedName === 'is' && 129 | value !== null && 130 | value !== undefined && 131 | typeof value !== 'string' 132 | ) { 133 | warning( 134 | false, 135 | 'Received a `%s` for a string attribute `is`. If this is expected, cast ' + 136 | 'the value to a string.%s', 137 | typeof value, 138 | getStackAddendum(), 139 | ); 140 | warnedProperties[name] = true; 141 | return true; 142 | } 143 | 144 | if (typeof value === 'number' && isNaN(value)) { 145 | warning( 146 | false, 147 | 'Received NaN for the `%s` attribute. If this is expected, cast ' + 148 | 'the value to a string.%s', 149 | name, 150 | getStackAddendum(), 151 | ); 152 | warnedProperties[name] = true; 153 | return true; 154 | } 155 | 156 | const isReserved = isReservedProp(name); 157 | 158 | // Known attributes should match the casing specified in the property config. 159 | if (possibleStandardNames.hasOwnProperty(lowerCasedName)) { 160 | var standardName = possibleStandardNames[lowerCasedName]; 161 | if (standardName !== name) { 162 | warning( 163 | false, 164 | 'Invalid DOM property `%s`. Did you mean `%s`?%s', 165 | name, 166 | standardName, 167 | getStackAddendum(), 168 | ); 169 | warnedProperties[name] = true; 170 | return true; 171 | } 172 | } else if (!isReserved && name !== lowerCasedName) { 173 | // Unknown attributes should have lowercase casing since that's how they 174 | // will be cased anyway with server rendering. 175 | warning( 176 | false, 177 | 'React does not recognize the `%s` prop on a DOM element. If you ' + 178 | 'intentionally want it to appear in the DOM as a custom ' + 179 | 'attribute, spell it as lowercase `%s` instead. ' + 180 | 'If you accidentally passed it from a parent component, remove ' + 181 | 'it from the DOM element.%s', 182 | name, 183 | lowerCasedName, 184 | getStackAddendum(), 185 | ); 186 | warnedProperties[name] = true; 187 | return true; 188 | } 189 | 190 | if ( 191 | typeof value === 'boolean' && 192 | !shouldAttributeAcceptBooleanValue(name) 193 | ) { 194 | if (value) { 195 | warning( 196 | false, 197 | 'Received `%s` for a non-boolean attribute `%s`.\n\n' + 198 | 'If you want to write it to the DOM, pass a string instead: ' + 199 | '%s="%s" or %s={value.toString()}.%s', 200 | value, 201 | name, 202 | name, 203 | value, 204 | name, 205 | getStackAddendum(), 206 | ); 207 | } else { 208 | warning( 209 | false, 210 | 'Received `%s` for a non-boolean attribute `%s`.\n\n' + 211 | 'If you want to write it to the DOM, pass a string instead: ' + 212 | '%s="%s" or %s={value.toString()}.\n\n' + 213 | 'If you used to conditionally omit it with %s={condition && value}, ' + 214 | 'pass %s={condition ? value : undefined} instead.%s', 215 | value, 216 | name, 217 | name, 218 | value, 219 | name, 220 | name, 221 | name, 222 | getStackAddendum(), 223 | ); 224 | } 225 | warnedProperties[name] = true; 226 | return true; 227 | } 228 | 229 | // Now that we've validated casing, do not validate 230 | // data types for reserved props 231 | if (isReserved) { 232 | return true; 233 | } 234 | 235 | // Warn when a known attribute is a bad type 236 | if (!shouldSetAttribute(name, value)) { 237 | warnedProperties[name] = true; 238 | return false; 239 | } 240 | 241 | return true; 242 | }; 243 | } 244 | 245 | var warnUnknownProperties = function(type, props, canUseEventSystem) { 246 | var unknownProps = []; 247 | for (var key in props) { 248 | var isValid = validateProperty(type, key, props[key], canUseEventSystem); 249 | if (!isValid) { 250 | unknownProps.push(key); 251 | } 252 | } 253 | 254 | var unknownPropString = unknownProps.map(prop => '`' + prop + '`').join(', '); 255 | if (unknownProps.length === 1) { 256 | warning( 257 | false, 258 | 'Invalid value for prop %s on <%s> tag. Either remove it from the element, ' + 259 | 'or pass a string or number value to keep it in the DOM. ' + 260 | 'For details, see https://fb.me/react-attribute-behavior%s', 261 | unknownPropString, 262 | type, 263 | getStackAddendum(), 264 | ); 265 | } else if (unknownProps.length > 1) { 266 | warning( 267 | false, 268 | 'Invalid values for props %s on <%s> tag. Either remove them from the element, ' + 269 | 'or pass a string or number value to keep them in the DOM. ' + 270 | 'For details, see https://fb.me/react-attribute-behavior%s', 271 | unknownPropString, 272 | type, 273 | getStackAddendum(), 274 | ); 275 | } 276 | }; 277 | 278 | export function validateProperties(type, props, canUseEventSystem) { 279 | if (isCustomComponent(type, props)) { 280 | return; 281 | } 282 | warnUnknownProperties(type, props, canUseEventSystem); 283 | } 284 | -------------------------------------------------------------------------------- /src/shared/possibleStandardNames.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | // When adding attributes to the HTML or SVG whitelist, be sure to 9 | // also add them to this module to ensure casing and incorrect name 10 | // warnings. 11 | var possibleStandardNames = { 12 | // HTML 13 | accept: 'accept', 14 | acceptcharset: 'acceptCharset', 15 | 'accept-charset': 'acceptCharset', 16 | accesskey: 'accessKey', 17 | action: 'action', 18 | allowfullscreen: 'allowFullScreen', 19 | alt: 'alt', 20 | as: 'as', 21 | async: 'async', 22 | autocapitalize: 'autoCapitalize', 23 | autocomplete: 'autoComplete', 24 | autocorrect: 'autoCorrect', 25 | autofocus: 'autoFocus', 26 | autoplay: 'autoPlay', 27 | autosave: 'autoSave', 28 | capture: 'capture', 29 | cellpadding: 'cellPadding', 30 | cellspacing: 'cellSpacing', 31 | challenge: 'challenge', 32 | charset: 'charSet', 33 | checked: 'checked', 34 | children: 'children', 35 | cite: 'cite', 36 | class: 'className', 37 | classid: 'classID', 38 | classname: 'className', 39 | cols: 'cols', 40 | colspan: 'colSpan', 41 | content: 'content', 42 | contenteditable: 'contentEditable', 43 | contextmenu: 'contextMenu', 44 | controls: 'controls', 45 | controlslist: 'controlsList', 46 | coords: 'coords', 47 | crossorigin: 'crossOrigin', 48 | dangerouslysetinnerhtml: 'dangerouslySetInnerHTML', 49 | data: 'data', 50 | datetime: 'dateTime', 51 | default: 'default', 52 | defaultchecked: 'defaultChecked', 53 | defaultvalue: 'defaultValue', 54 | defer: 'defer', 55 | dir: 'dir', 56 | disabled: 'disabled', 57 | download: 'download', 58 | draggable: 'draggable', 59 | enctype: 'encType', 60 | for: 'htmlFor', 61 | form: 'form', 62 | formmethod: 'formMethod', 63 | formaction: 'formAction', 64 | formenctype: 'formEncType', 65 | formnovalidate: 'formNoValidate', 66 | formtarget: 'formTarget', 67 | frameborder: 'frameBorder', 68 | headers: 'headers', 69 | height: 'height', 70 | hidden: 'hidden', 71 | high: 'high', 72 | href: 'href', 73 | hreflang: 'hrefLang', 74 | htmlfor: 'htmlFor', 75 | httpequiv: 'httpEquiv', 76 | 'http-equiv': 'httpEquiv', 77 | icon: 'icon', 78 | id: 'id', 79 | innerhtml: 'innerHTML', 80 | inputmode: 'inputMode', 81 | integrity: 'integrity', 82 | is: 'is', 83 | itemid: 'itemID', 84 | itemprop: 'itemProp', 85 | itemref: 'itemRef', 86 | itemscope: 'itemScope', 87 | itemtype: 'itemType', 88 | keyparams: 'keyParams', 89 | keytype: 'keyType', 90 | kind: 'kind', 91 | label: 'label', 92 | lang: 'lang', 93 | list: 'list', 94 | loop: 'loop', 95 | low: 'low', 96 | manifest: 'manifest', 97 | marginwidth: 'marginWidth', 98 | marginheight: 'marginHeight', 99 | max: 'max', 100 | maxlength: 'maxLength', 101 | media: 'media', 102 | mediagroup: 'mediaGroup', 103 | method: 'method', 104 | min: 'min', 105 | minlength: 'minLength', 106 | multiple: 'multiple', 107 | muted: 'muted', 108 | name: 'name', 109 | nonce: 'nonce', 110 | novalidate: 'noValidate', 111 | open: 'open', 112 | optimum: 'optimum', 113 | pattern: 'pattern', 114 | placeholder: 'placeholder', 115 | playsinline: 'playsInline', 116 | poster: 'poster', 117 | preload: 'preload', 118 | profile: 'profile', 119 | radiogroup: 'radioGroup', 120 | readonly: 'readOnly', 121 | referrerpolicy: 'referrerPolicy', 122 | rel: 'rel', 123 | required: 'required', 124 | reversed: 'reversed', 125 | role: 'role', 126 | rows: 'rows', 127 | rowspan: 'rowSpan', 128 | sandbox: 'sandbox', 129 | scope: 'scope', 130 | scoped: 'scoped', 131 | scrolling: 'scrolling', 132 | seamless: 'seamless', 133 | selected: 'selected', 134 | shape: 'shape', 135 | size: 'size', 136 | sizes: 'sizes', 137 | span: 'span', 138 | spellcheck: 'spellCheck', 139 | src: 'src', 140 | srcdoc: 'srcDoc', 141 | srclang: 'srcLang', 142 | srcset: 'srcSet', 143 | start: 'start', 144 | step: 'step', 145 | style: 'style', 146 | summary: 'summary', 147 | tabindex: 'tabIndex', 148 | target: 'target', 149 | title: 'title', 150 | type: 'type', 151 | usemap: 'useMap', 152 | value: 'value', 153 | width: 'width', 154 | wmode: 'wmode', 155 | wrap: 'wrap', 156 | 157 | // SVG 158 | about: 'about', 159 | accentheight: 'accentHeight', 160 | 'accent-height': 'accentHeight', 161 | accumulate: 'accumulate', 162 | additive: 'additive', 163 | alignmentbaseline: 'alignmentBaseline', 164 | 'alignment-baseline': 'alignmentBaseline', 165 | allowreorder: 'allowReorder', 166 | alphabetic: 'alphabetic', 167 | amplitude: 'amplitude', 168 | arabicform: 'arabicForm', 169 | 'arabic-form': 'arabicForm', 170 | ascent: 'ascent', 171 | attributename: 'attributeName', 172 | attributetype: 'attributeType', 173 | autoreverse: 'autoReverse', 174 | azimuth: 'azimuth', 175 | basefrequency: 'baseFrequency', 176 | baselineshift: 'baselineShift', 177 | 'baseline-shift': 'baselineShift', 178 | baseprofile: 'baseProfile', 179 | bbox: 'bbox', 180 | begin: 'begin', 181 | bias: 'bias', 182 | by: 'by', 183 | calcmode: 'calcMode', 184 | capheight: 'capHeight', 185 | 'cap-height': 'capHeight', 186 | clip: 'clip', 187 | clippath: 'clipPath', 188 | 'clip-path': 'clipPath', 189 | clippathunits: 'clipPathUnits', 190 | cliprule: 'clipRule', 191 | 'clip-rule': 'clipRule', 192 | color: 'color', 193 | colorinterpolation: 'colorInterpolation', 194 | 'color-interpolation': 'colorInterpolation', 195 | colorinterpolationfilters: 'colorInterpolationFilters', 196 | 'color-interpolation-filters': 'colorInterpolationFilters', 197 | colorprofile: 'colorProfile', 198 | 'color-profile': 'colorProfile', 199 | colorrendering: 'colorRendering', 200 | 'color-rendering': 'colorRendering', 201 | contentscripttype: 'contentScriptType', 202 | contentstyletype: 'contentStyleType', 203 | cursor: 'cursor', 204 | cx: 'cx', 205 | cy: 'cy', 206 | d: 'd', 207 | datatype: 'datatype', 208 | decelerate: 'decelerate', 209 | descent: 'descent', 210 | diffuseconstant: 'diffuseConstant', 211 | direction: 'direction', 212 | display: 'display', 213 | divisor: 'divisor', 214 | dominantbaseline: 'dominantBaseline', 215 | 'dominant-baseline': 'dominantBaseline', 216 | dur: 'dur', 217 | dx: 'dx', 218 | dy: 'dy', 219 | edgemode: 'edgeMode', 220 | elevation: 'elevation', 221 | enablebackground: 'enableBackground', 222 | 'enable-background': 'enableBackground', 223 | end: 'end', 224 | exponent: 'exponent', 225 | externalresourcesrequired: 'externalResourcesRequired', 226 | fill: 'fill', 227 | fillopacity: 'fillOpacity', 228 | 'fill-opacity': 'fillOpacity', 229 | fillrule: 'fillRule', 230 | 'fill-rule': 'fillRule', 231 | filter: 'filter', 232 | filterres: 'filterRes', 233 | filterunits: 'filterUnits', 234 | floodopacity: 'floodOpacity', 235 | 'flood-opacity': 'floodOpacity', 236 | floodcolor: 'floodColor', 237 | 'flood-color': 'floodColor', 238 | focusable: 'focusable', 239 | fontfamily: 'fontFamily', 240 | 'font-family': 'fontFamily', 241 | fontsize: 'fontSize', 242 | 'font-size': 'fontSize', 243 | fontsizeadjust: 'fontSizeAdjust', 244 | 'font-size-adjust': 'fontSizeAdjust', 245 | fontstretch: 'fontStretch', 246 | 'font-stretch': 'fontStretch', 247 | fontstyle: 'fontStyle', 248 | 'font-style': 'fontStyle', 249 | fontvariant: 'fontVariant', 250 | 'font-variant': 'fontVariant', 251 | fontweight: 'fontWeight', 252 | 'font-weight': 'fontWeight', 253 | format: 'format', 254 | from: 'from', 255 | fx: 'fx', 256 | fy: 'fy', 257 | g1: 'g1', 258 | g2: 'g2', 259 | glyphname: 'glyphName', 260 | 'glyph-name': 'glyphName', 261 | glyphorientationhorizontal: 'glyphOrientationHorizontal', 262 | 'glyph-orientation-horizontal': 'glyphOrientationHorizontal', 263 | glyphorientationvertical: 'glyphOrientationVertical', 264 | 'glyph-orientation-vertical': 'glyphOrientationVertical', 265 | glyphref: 'glyphRef', 266 | gradienttransform: 'gradientTransform', 267 | gradientunits: 'gradientUnits', 268 | hanging: 'hanging', 269 | horizadvx: 'horizAdvX', 270 | 'horiz-adv-x': 'horizAdvX', 271 | horizoriginx: 'horizOriginX', 272 | 'horiz-origin-x': 'horizOriginX', 273 | ideographic: 'ideographic', 274 | imagerendering: 'imageRendering', 275 | 'image-rendering': 'imageRendering', 276 | in2: 'in2', 277 | in: 'in', 278 | inlist: 'inlist', 279 | intercept: 'intercept', 280 | k1: 'k1', 281 | k2: 'k2', 282 | k3: 'k3', 283 | k4: 'k4', 284 | k: 'k', 285 | kernelmatrix: 'kernelMatrix', 286 | kernelunitlength: 'kernelUnitLength', 287 | kerning: 'kerning', 288 | keypoints: 'keyPoints', 289 | keysplines: 'keySplines', 290 | keytimes: 'keyTimes', 291 | lengthadjust: 'lengthAdjust', 292 | letterspacing: 'letterSpacing', 293 | 'letter-spacing': 'letterSpacing', 294 | lightingcolor: 'lightingColor', 295 | 'lighting-color': 'lightingColor', 296 | limitingconeangle: 'limitingConeAngle', 297 | local: 'local', 298 | markerend: 'markerEnd', 299 | 'marker-end': 'markerEnd', 300 | markerheight: 'markerHeight', 301 | markermid: 'markerMid', 302 | 'marker-mid': 'markerMid', 303 | markerstart: 'markerStart', 304 | 'marker-start': 'markerStart', 305 | markerunits: 'markerUnits', 306 | markerwidth: 'markerWidth', 307 | mask: 'mask', 308 | maskcontentunits: 'maskContentUnits', 309 | maskunits: 'maskUnits', 310 | mathematical: 'mathematical', 311 | mode: 'mode', 312 | numoctaves: 'numOctaves', 313 | offset: 'offset', 314 | opacity: 'opacity', 315 | operator: 'operator', 316 | order: 'order', 317 | orient: 'orient', 318 | orientation: 'orientation', 319 | origin: 'origin', 320 | overflow: 'overflow', 321 | overlineposition: 'overlinePosition', 322 | 'overline-position': 'overlinePosition', 323 | overlinethickness: 'overlineThickness', 324 | 'overline-thickness': 'overlineThickness', 325 | paintorder: 'paintOrder', 326 | 'paint-order': 'paintOrder', 327 | panose1: 'panose1', 328 | 'panose-1': 'panose1', 329 | pathlength: 'pathLength', 330 | patterncontentunits: 'patternContentUnits', 331 | patterntransform: 'patternTransform', 332 | patternunits: 'patternUnits', 333 | pointerevents: 'pointerEvents', 334 | 'pointer-events': 'pointerEvents', 335 | points: 'points', 336 | pointsatx: 'pointsAtX', 337 | pointsaty: 'pointsAtY', 338 | pointsatz: 'pointsAtZ', 339 | prefix: 'prefix', 340 | preservealpha: 'preserveAlpha', 341 | preserveaspectratio: 'preserveAspectRatio', 342 | primitiveunits: 'primitiveUnits', 343 | property: 'property', 344 | r: 'r', 345 | radius: 'radius', 346 | refx: 'refX', 347 | refy: 'refY', 348 | renderingintent: 'renderingIntent', 349 | 'rendering-intent': 'renderingIntent', 350 | repeatcount: 'repeatCount', 351 | repeatdur: 'repeatDur', 352 | requiredextensions: 'requiredExtensions', 353 | requiredfeatures: 'requiredFeatures', 354 | resource: 'resource', 355 | restart: 'restart', 356 | result: 'result', 357 | results: 'results', 358 | rotate: 'rotate', 359 | rx: 'rx', 360 | ry: 'ry', 361 | scale: 'scale', 362 | security: 'security', 363 | seed: 'seed', 364 | shaperendering: 'shapeRendering', 365 | 'shape-rendering': 'shapeRendering', 366 | slope: 'slope', 367 | spacing: 'spacing', 368 | specularconstant: 'specularConstant', 369 | specularexponent: 'specularExponent', 370 | speed: 'speed', 371 | spreadmethod: 'spreadMethod', 372 | startoffset: 'startOffset', 373 | stddeviation: 'stdDeviation', 374 | stemh: 'stemh', 375 | stemv: 'stemv', 376 | stitchtiles: 'stitchTiles', 377 | stopcolor: 'stopColor', 378 | 'stop-color': 'stopColor', 379 | stopopacity: 'stopOpacity', 380 | 'stop-opacity': 'stopOpacity', 381 | strikethroughposition: 'strikethroughPosition', 382 | 'strikethrough-position': 'strikethroughPosition', 383 | strikethroughthickness: 'strikethroughThickness', 384 | 'strikethrough-thickness': 'strikethroughThickness', 385 | string: 'string', 386 | stroke: 'stroke', 387 | strokedasharray: 'strokeDasharray', 388 | 'stroke-dasharray': 'strokeDasharray', 389 | strokedashoffset: 'strokeDashoffset', 390 | 'stroke-dashoffset': 'strokeDashoffset', 391 | strokelinecap: 'strokeLinecap', 392 | 'stroke-linecap': 'strokeLinecap', 393 | strokelinejoin: 'strokeLinejoin', 394 | 'stroke-linejoin': 'strokeLinejoin', 395 | strokemiterlimit: 'strokeMiterlimit', 396 | 'stroke-miterlimit': 'strokeMiterlimit', 397 | strokewidth: 'strokeWidth', 398 | 'stroke-width': 'strokeWidth', 399 | strokeopacity: 'strokeOpacity', 400 | 'stroke-opacity': 'strokeOpacity', 401 | suppresscontenteditablewarning: 'suppressContentEditableWarning', 402 | suppresshydrationwarning: 'suppressHydrationWarning', 403 | surfacescale: 'surfaceScale', 404 | systemlanguage: 'systemLanguage', 405 | tablevalues: 'tableValues', 406 | targetx: 'targetX', 407 | targety: 'targetY', 408 | textanchor: 'textAnchor', 409 | 'text-anchor': 'textAnchor', 410 | textdecoration: 'textDecoration', 411 | 'text-decoration': 'textDecoration', 412 | textlength: 'textLength', 413 | textrendering: 'textRendering', 414 | 'text-rendering': 'textRendering', 415 | to: 'to', 416 | transform: 'transform', 417 | typeof: 'typeof', 418 | u1: 'u1', 419 | u2: 'u2', 420 | underlineposition: 'underlinePosition', 421 | 'underline-position': 'underlinePosition', 422 | underlinethickness: 'underlineThickness', 423 | 'underline-thickness': 'underlineThickness', 424 | unicode: 'unicode', 425 | unicodebidi: 'unicodeBidi', 426 | 'unicode-bidi': 'unicodeBidi', 427 | unicoderange: 'unicodeRange', 428 | 'unicode-range': 'unicodeRange', 429 | unitsperem: 'unitsPerEm', 430 | 'units-per-em': 'unitsPerEm', 431 | unselectable: 'unselectable', 432 | valphabetic: 'vAlphabetic', 433 | 'v-alphabetic': 'vAlphabetic', 434 | values: 'values', 435 | vectoreffect: 'vectorEffect', 436 | 'vector-effect': 'vectorEffect', 437 | version: 'version', 438 | vertadvy: 'vertAdvY', 439 | 'vert-adv-y': 'vertAdvY', 440 | vertoriginx: 'vertOriginX', 441 | 'vert-origin-x': 'vertOriginX', 442 | vertoriginy: 'vertOriginY', 443 | 'vert-origin-y': 'vertOriginY', 444 | vhanging: 'vHanging', 445 | 'v-hanging': 'vHanging', 446 | videographic: 'vIdeographic', 447 | 'v-ideographic': 'vIdeographic', 448 | viewbox: 'viewBox', 449 | viewtarget: 'viewTarget', 450 | visibility: 'visibility', 451 | vmathematical: 'vMathematical', 452 | 'v-mathematical': 'vMathematical', 453 | vocab: 'vocab', 454 | widths: 'widths', 455 | wordspacing: 'wordSpacing', 456 | 'word-spacing': 'wordSpacing', 457 | writingmode: 'writingMode', 458 | 'writing-mode': 'writingMode', 459 | x1: 'x1', 460 | x2: 'x2', 461 | x: 'x', 462 | xchannelselector: 'xChannelSelector', 463 | xheight: 'xHeight', 464 | 'x-height': 'xHeight', 465 | xlinkactuate: 'xlinkActuate', 466 | 'xlink:actuate': 'xlinkActuate', 467 | xlinkarcrole: 'xlinkArcrole', 468 | 'xlink:arcrole': 'xlinkArcrole', 469 | xlinkhref: 'xlinkHref', 470 | 'xlink:href': 'xlinkHref', 471 | xlinkrole: 'xlinkRole', 472 | 'xlink:role': 'xlinkRole', 473 | xlinkshow: 'xlinkShow', 474 | 'xlink:show': 'xlinkShow', 475 | xlinktitle: 'xlinkTitle', 476 | 'xlink:title': 'xlinkTitle', 477 | xlinktype: 'xlinkType', 478 | 'xlink:type': 'xlinkType', 479 | xmlbase: 'xmlBase', 480 | 'xml:base': 'xmlBase', 481 | xmllang: 'xmlLang', 482 | 'xml:lang': 'xmlLang', 483 | xmlns: 'xmlns', 484 | 'xml:space': 'xmlSpace', 485 | xmlnsxlink: 'xmlnsXlink', 486 | 'xmlns:xlink': 'xmlnsXlink', 487 | xmlspace: 'xmlSpace', 488 | y1: 'y1', 489 | y2: 'y2', 490 | y: 'y', 491 | ychannelselector: 'yChannelSelector', 492 | z: 'z', 493 | zoomandpan: 'zoomAndPan', 494 | }; 495 | 496 | export default possibleStandardNames; 497 | -------------------------------------------------------------------------------- /src/server/ReactPartialRenderer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013-present, Facebook, Inc. 3 | * 4 | * Portions of this source code are licensed under the MIT license found in 5 | * the LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import type {ReactElement} from 'shared/ReactElementType'; 11 | 12 | import React from 'react'; 13 | import emptyFunction from 'fbjs/lib/emptyFunction'; 14 | import emptyObject from 'fbjs/lib/emptyObject'; 15 | import hyphenateStyleName from 'fbjs/lib/hyphenateStyleName'; 16 | import invariant from 'fbjs/lib/invariant'; 17 | import memoizeStringOnly from 'fbjs/lib/memoizeStringOnly'; 18 | import warning from 'fbjs/lib/warning'; 19 | import checkPropTypes from 'prop-types/checkPropTypes'; 20 | import describeComponentFrame from 'shared/describeComponentFrame'; 21 | import {ReactDebugCurrentFrame} from 'shared/ReactGlobalSharedState'; 22 | import {REACT_FRAGMENT_TYPE} from 'shared/ReactSymbols'; 23 | 24 | import { 25 | createMarkupForCustomAttribute, 26 | createMarkupForProperty, 27 | createMarkupForRoot, 28 | } from './DOMMarkupOperations'; 29 | import escapeTextForBrowser from './escapeTextForBrowser'; 30 | import { 31 | Namespaces, 32 | getIntrinsicNamespace, 33 | getChildNamespace, 34 | } from '../shared/DOMNamespaces'; 35 | import ReactControlledValuePropTypes from '../shared/ReactControlledValuePropTypes'; 36 | import assertValidProps from '../shared/assertValidProps'; 37 | import dangerousStyleValue from '../shared/dangerousStyleValue'; 38 | import isCustomComponent from '../shared/isCustomComponent'; 39 | import omittedCloseTags from '../shared/omittedCloseTags'; 40 | import warnValidStyle from '../shared/warnValidStyle'; 41 | import {validateProperties as validateARIAProperties} from '../shared/ReactDOMInvalidARIAHook'; 42 | import {validateProperties as validateInputProperties} from '../shared/ReactDOMNullInputValuePropHook'; 43 | import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook'; 44 | 45 | // Based on reading the React.Children implementation. TODO: type this somewhere? 46 | type ReactNode = string | number | ReactElement; 47 | type FlatReactChildren = Array; 48 | type toArrayType = (children: mixed) => FlatReactChildren; 49 | var toArray = ((React.Children.toArray: any): toArrayType); 50 | 51 | var getStackAddendum = emptyFunction.thatReturns(''); 52 | 53 | if (__DEV__) { 54 | var validatePropertiesInDevelopment = function(type, props) { 55 | validateARIAProperties(type, props); 56 | validateInputProperties(type, props); 57 | validateUnknownProperties(type, props, /* canUseEventSystem */ false); 58 | }; 59 | 60 | var describeStackFrame = function(element): string { 61 | var source = element._source; 62 | var type = element.type; 63 | var name = getComponentName(type); 64 | var ownerName = null; 65 | return describeComponentFrame(name, source, ownerName); 66 | }; 67 | 68 | var currentDebugStack = null; 69 | var currentDebugElementStack = null; 70 | var setCurrentDebugStack = function(stack: Array) { 71 | var frame: Frame = stack[stack.length - 1]; 72 | currentDebugElementStack = ((frame: any): FrameDev).debugElementStack; 73 | // We are about to enter a new composite stack, reset the array. 74 | currentDebugElementStack.length = 0; 75 | currentDebugStack = stack; 76 | ReactDebugCurrentFrame.getCurrentStack = getStackAddendum; 77 | }; 78 | var pushElementToDebugStack = function(element: ReactElement) { 79 | if (currentDebugElementStack !== null) { 80 | currentDebugElementStack.push(element); 81 | } 82 | }; 83 | var resetCurrentDebugStack = function() { 84 | currentDebugElementStack = null; 85 | currentDebugStack = null; 86 | ReactDebugCurrentFrame.getCurrentStack = null; 87 | }; 88 | getStackAddendum = function(): null | string { 89 | if (currentDebugStack === null) { 90 | return ''; 91 | } 92 | let stack = ''; 93 | let debugStack = currentDebugStack; 94 | for (let i = debugStack.length - 1; i >= 0; i--) { 95 | const frame: Frame = debugStack[i]; 96 | let debugElementStack = ((frame: any): FrameDev).debugElementStack; 97 | for (let ii = debugElementStack.length - 1; ii >= 0; ii--) { 98 | stack += describeStackFrame(debugElementStack[ii]); 99 | } 100 | } 101 | return stack; 102 | }; 103 | } 104 | 105 | var didWarnDefaultInputValue = false; 106 | var didWarnDefaultChecked = false; 107 | var didWarnDefaultSelectValue = false; 108 | var didWarnDefaultTextareaValue = false; 109 | var didWarnInvalidOptionChildren = false; 110 | var didWarnAboutNoopUpdateForComponent = {}; 111 | var valuePropNames = ['value', 'defaultValue']; 112 | var newlineEatingTags = { 113 | listing: true, 114 | pre: true, 115 | textarea: true, 116 | }; 117 | 118 | function getComponentName(type) { 119 | return typeof type === 'string' 120 | ? type 121 | : typeof type === 'function' ? type.displayName || type.name : null; 122 | } 123 | 124 | // We accept any tag to be rendered but since this gets injected into arbitrary 125 | // HTML, we want to make sure that it's a safe tag. 126 | // http://www.w3.org/TR/REC-xml/#NT-Name 127 | var VALID_TAG_REGEX = /^[a-zA-Z][a-zA-Z:_\.\-\d]*$/; // Simplified subset 128 | var validatedTagCache = {}; 129 | function validateDangerousTag(tag) { 130 | if (!validatedTagCache.hasOwnProperty(tag)) { 131 | invariant(VALID_TAG_REGEX.test(tag), 'Invalid tag: %s', tag); 132 | validatedTagCache[tag] = true; 133 | } 134 | } 135 | 136 | var processStyleName = memoizeStringOnly(function(styleName) { 137 | return hyphenateStyleName(styleName); 138 | }); 139 | 140 | function createMarkupForStyles(styles): string | null { 141 | var serialized = ''; 142 | var delimiter = ''; 143 | for (var styleName in styles) { 144 | if (!styles.hasOwnProperty(styleName)) { 145 | continue; 146 | } 147 | var isCustomProperty = styleName.indexOf('--') === 0; 148 | var styleValue = styles[styleName]; 149 | if (__DEV__) { 150 | if (!isCustomProperty) { 151 | warnValidStyle(styleName, styleValue, getStackAddendum); 152 | } 153 | } 154 | if (styleValue != null) { 155 | serialized += delimiter + processStyleName(styleName) + ':'; 156 | serialized += dangerousStyleValue( 157 | styleName, 158 | styleValue, 159 | isCustomProperty, 160 | ); 161 | 162 | delimiter = ';'; 163 | } 164 | } 165 | return serialized || null; 166 | } 167 | 168 | function warnNoop( 169 | publicInstance: React$Component, 170 | callerName: string, 171 | ) { 172 | if (__DEV__) { 173 | var constructor = publicInstance.constructor; 174 | const componentName = 175 | (constructor && getComponentName(constructor)) || 'ReactClass'; 176 | const warningKey = `${componentName}.${callerName}`; 177 | if (didWarnAboutNoopUpdateForComponent[warningKey]) { 178 | return; 179 | } 180 | 181 | warning( 182 | false, 183 | '%s(...): Can only update a mounting component. ' + 184 | 'This usually means you called %s() outside componentWillMount() on the server. ' + 185 | 'This is a no-op.\n\nPlease check the code for the %s component.', 186 | callerName, 187 | callerName, 188 | componentName, 189 | ); 190 | didWarnAboutNoopUpdateForComponent[warningKey] = true; 191 | } 192 | } 193 | 194 | function shouldConstruct(Component) { 195 | return Component.prototype && Component.prototype.isReactComponent; 196 | } 197 | 198 | function getNonChildrenInnerMarkup(props) { 199 | var innerHTML = props.dangerouslySetInnerHTML; 200 | if (innerHTML != null) { 201 | if (innerHTML.__html != null) { 202 | return innerHTML.__html; 203 | } 204 | } else { 205 | var content = props.children; 206 | if (typeof content === 'string' || typeof content === 'number') { 207 | return escapeTextForBrowser(content); 208 | } 209 | } 210 | return null; 211 | } 212 | 213 | function flattenTopLevelChildren(children: mixed): FlatReactChildren { 214 | if (!React.isValidElement(children)) { 215 | return toArray(children); 216 | } 217 | const element = ((children: any): ReactElement); 218 | if (element.type !== REACT_FRAGMENT_TYPE) { 219 | return [element]; 220 | } 221 | const fragmentChildren = element.props.children; 222 | if (!React.isValidElement(fragmentChildren)) { 223 | return toArray(fragmentChildren); 224 | } 225 | const fragmentChildElement = ((fragmentChildren: any): ReactElement); 226 | return [fragmentChildElement]; 227 | } 228 | 229 | function flattenOptionChildren(children: mixed): string { 230 | var content = ''; 231 | // Flatten children and warn if they aren't strings or numbers; 232 | // invalid types are ignored. 233 | React.Children.forEach(children, function(child) { 234 | if (child == null) { 235 | return; 236 | } 237 | if (typeof child === 'string' || typeof child === 'number') { 238 | content += child; 239 | } else { 240 | if (__DEV__) { 241 | if (!didWarnInvalidOptionChildren) { 242 | didWarnInvalidOptionChildren = true; 243 | warning( 244 | false, 245 | 'Only strings and numbers are supported as