├── README.md
├── package.json
└── src
├── server
├── ComponentCache.js
├── DOMMarkupOperations.js
├── ReactDOMNodeStreamRenderer.js
├── ReactDOMServerNode.js
├── ReactDOMStringRenderer.js
├── ReactPartialRenderer.js
├── escapeTextForBrowser.js
└── quoteAttributeValueForBrowser.js
└── shared
├── CSSProperty.js
├── CSSPropertyOperations.js
├── DOMNamespaces.js
├── DOMProperty.js
├── HTMLDOMPropertyConfig.js
├── HTMLNodeType.js
├── ReactControlledValuePropTypes.js
├── ReactDOMInjection.js
├── ReactDOMInvalidARIAHook.js
├── ReactDOMNullInputValuePropHook.js
├── ReactDOMUnknownPropertyHook.js
├── SVGDOMPropertyConfig.js
├── assertValidProps.js
├── checkReact.js
├── createMicrosoftUnsafeLocalFunction.js
├── dangerousStyleValue.js
├── isCustomComponent.js
├── omittedCloseTags.js
├── possibleStandardNames.js
├── validAriaProperties.js
├── voidElementTags.js
└── warnValidStyle.js
/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)
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/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/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/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