From b03389fe771042e927dea9770fc77670db7094dc Mon Sep 17 00:00:00 2001 From: Ben Alpert Date: Sat, 21 May 2016 20:45:24 -0700 Subject: [PATCH 01/27] Server Rendering initial commit Originally authored by spicyj, tweakes to rebase it and make it run by tomocchino. Adding ReactDOMServerRendering and a createTagMarkup utility function which will be used in it. --- src/renderers/dom/ReactDOMServer.js | 6 +- .../dom/server/ReactDOMServerRendering.js | 220 ++++++++++++++++++ .../dom/server/utils/createOpenTagMarkup.js | 92 ++++++++ 3 files changed, 315 insertions(+), 3 deletions(-) create mode 100644 src/renderers/dom/server/ReactDOMServerRendering.js create mode 100644 src/renderers/dom/server/utils/createOpenTagMarkup.js diff --git a/src/renderers/dom/ReactDOMServer.js b/src/renderers/dom/ReactDOMServer.js index 1ab1687096b5..1655c02c3962 100644 --- a/src/renderers/dom/ReactDOMServer.js +++ b/src/renderers/dom/ReactDOMServer.js @@ -13,15 +13,15 @@ var ReactDOMInjection = require('ReactDOMInjection'); var ReactDOMStackInjection = require('ReactDOMStackInjection'); -var ReactServerRendering = require('ReactServerRendering'); +var ReactDOMServerRendering = require('ReactDOMServerRendering'); var ReactVersion = require('ReactVersion'); ReactDOMInjection.inject(); ReactDOMStackInjection.inject(); var ReactDOMServer = { - renderToString: ReactServerRendering.renderToString, - renderToStaticMarkup: ReactServerRendering.renderToStaticMarkup, + renderToString: ReactDOMServerRendering.renderToString, + renderToStaticMarkup: ReactDOMServerRendering.renderToStaticMarkup, version: ReactVersion, }; diff --git a/src/renderers/dom/server/ReactDOMServerRendering.js b/src/renderers/dom/server/ReactDOMServerRendering.js new file mode 100644 index 000000000000..8cc4c7d7956a --- /dev/null +++ b/src/renderers/dom/server/ReactDOMServerRendering.js @@ -0,0 +1,220 @@ +/** + * Copyright 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ReactDOMServerRendering + */ + +'use strict'; + +var ReactElement = require('ReactElement'); +var ReactMarkupChecksum = require('ReactMarkupChecksum'); +var ReactNoopUpdateQueue = require('ReactNoopUpdateQueue'); + +var createOpenTagMarkup = require('createOpenTagMarkup'); +var emptyObject = require('fbjs/lib/emptyObject'); +var escapeTextContentForBrowser = require('escapeTextContentForBrowser'); +var invariant = require('fbjs/lib/invariant'); +var omittedCloseTags = require('omittedCloseTags'); +var traverseAllChildren = require('traverseAllChildren'); + +//var Readable = require('readable-stream').Readable; + +function shouldConstruct(Component) { + return Component.prototype && Component.prototype.isReactComponent; +} + +function resolve(child, context) { + if (Array.isArray(child)) { + throw new Error('well that was unexpected'); + } + while ( + ReactElement.isValidElement(child) && + typeof child.type === 'function' + ) { + var Component = child.type; + // TODO: Mask context + var publicContext = context; + var updater = ReactNoopUpdateQueue; + if (shouldConstruct(Component)) { + var inst = new Component(child.props, publicContext, updater); + inst.props = child.props; + inst.context = publicContext; + inst.refs = emptyObject; + inst.updater = updater; + var initialState = inst.state; + if (initialState === undefined) { + inst.state = initialState = null; + } + if (inst.componentWillMount) { + inst.componentWillMount(); + // TODO: setState in componentWillMount should work. + } + child = inst.render(); + var childContext = inst.getChildContext && inst.getChildContext(); + context = Object.assign({}, context, childContext); + } else { + child = Component(child.props, publicContext, updater); + } + } + return {child, context}; +} + +function ReactDOMServerRenderer(element, makeStaticMarkup) { + this.stack = [{ + children: [element], + childIndex: 0, + context: emptyObject, + footer: '', + }]; + this.makeStaticMarkup = makeStaticMarkup; + this.idCounter = 1; + this.exhausted = false; +} + +ReactDOMServerRenderer.prototype.read = function(bytes) { + var out = ''; + while (out.length < bytes) { + if (this.stack.length === 0) { + this.exhausted = true; + break; + } + var frame = this.stack[this.stack.length - 1]; + if (frame.childIndex >= frame.children.length) { + out += frame.footer; + this.stack.pop(); + continue; + } + var child = frame.children[frame.childIndex++]; + out += this.render(child, frame.context); + } + return out; +}; + +ReactDOMServerRenderer.prototype.render = function(child, context) { + if (typeof child === 'string' || typeof child === 'number') { + return ( + '' + + escapeTextContentForBrowser('' + child) + + '' + ); + } else { + ({child, context} = resolve(child, context)); + if (child === null || child === false) { + return ''; + } else { + return this.renderDOM(child, context); + } + } +}; + +ReactDOMServerRenderer.prototype.renderDOM = function( + element, + context +) { + var tag = element.type.toLowerCase(); + var props = element.props; + if (tag === 'input') { + props = Object.assign({ + type: undefined, + }, props); + } else if (tag === 'textarea') { + props = Object.assign({}, props, { + value: undefined, + children: props.value, + }); + } + + var out = createOpenTagMarkup( + element.type, + tag, + props, + this.makeStaticMarkup, + this.stack.length === 1, + this.idCounter++, + null, + ); + var footer = ''; + if (omittedCloseTags.hasOwnProperty(tag)) { + out += '/>'; + } else { + out += '>'; + footer = ''; + } + var children = []; + var innerMarkup = getNonChildrenInnerMarkup(props); + if (innerMarkup != null) { + out += innerMarkup; + } else { + traverseAllChildren(props.children, function(ctx, child, name) { + if (child != null) { + children.push(child); + } + }); + } + this.stack.push({ + children, + childIndex: 0, + context: context, + footer: footer, + }); + return out; +}; +function getNonChildrenInnerMarkup(props) { + var innerHTML = props.dangerouslySetInnerHTML; + if (innerHTML != null) { + if (innerHTML.__html != null) { + return innerHTML.__html; + } + } else { + var content = props.children; + if (typeof content === 'string' || typeof content === 'number') { + return escapeTextContentForBrowser(content); + } + } + return null; +} + +function renderToStringImpl(element, makeStaticMarkup) { + var renderer = new ReactDOMServerRenderer(element, makeStaticMarkup); + var markup = renderer.read(Infinity); + markup = ReactMarkupChecksum.addChecksumToMarkup(markup); + return markup; +} + +/** + * Render a ReactElement to its initial HTML. This should only be used on the + * server. + * See https://facebook.github.io/react/docs/react-dom-server.html#rendertostring + */ +function renderToString(element) { + invariant( + ReactElement.isValidElement(element), + 'renderToString(): You must pass a valid ReactElement.', + ); + return renderToStringImpl(element, false); +} + +/** + * Similar to renderToString, except this doesn't create extra DOM attributes + * such as data-react-id that React uses internally. + * See https://facebook.github.io/react/docs/react-dom-server.html#rendertostaticmarkup + */ +function renderToStaticMarkup(element) { + invariant( + ReactElement.isValidElement(element), + 'renderToStaticMarkup(): You must pass a valid ReactElement.', + ); + return renderToStringImpl(element, true); +} + +var ReactDOMServerRendering = { + renderToString: renderToString, + renderToStaticMarkup: renderToStaticMarkup, +}; + +module.exports = ReactDOMServerRendering; diff --git a/src/renderers/dom/server/utils/createOpenTagMarkup.js b/src/renderers/dom/server/utils/createOpenTagMarkup.js new file mode 100644 index 000000000000..4932b53e7988 --- /dev/null +++ b/src/renderers/dom/server/utils/createOpenTagMarkup.js @@ -0,0 +1,92 @@ +/** + * Copyright 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule createOpenTagMarkup + */ + +'use strict'; + +var CSSPropertyOperations = require('CSSPropertyOperations'); +var DOMPropertyOperations = require('DOMPropertyOperations'); + +var {registrationNameModules} = require('EventPluginRegistry'); + +var STYLE = 'style'; +var RESERVED_PROPS = { + children: null, + dangerouslySetInnerHTML: null, + suppressContentEditableWarning: null, +}; + +function isCustomComponent(tagName, props) { + return tagName.indexOf('-') >= 0 || props.is != null; +} + +/** + * Creates markup for the open tag and attributes. Does not include closing ">". + */ +function createOpenTagMarkup( + tagVerbatim, + tagLowercase, + props, + makeStaticMarkup, + isRootElement, + domID, + instForDebug, +) { + var ret = '<' + tagVerbatim; + + for (var propKey in props) { + if (!props.hasOwnProperty(propKey)) { + continue; + } + var propValue = props[propKey]; + if (propValue == null) { + continue; + } + if (!registrationNameModules.hasOwnProperty(propKey)) { + if (propKey === STYLE) { + propValue = CSSPropertyOperations.createMarkupForStyles( + propValue, + instForDebug + ); + } + var markup = null; + if (isCustomComponent(tagLowercase, props)) { + if (!RESERVED_PROPS.hasOwnProperty(propKey)) { + markup = DOMPropertyOperations.createMarkupForCustomAttribute( + propKey, + propValue + ); + } + } else { + markup = DOMPropertyOperations.createMarkupForProperty( + propKey, + propValue + ); + } + if (markup) { + ret += ' ' + markup; + } + } + } + + // For static pages, no need to put React ID and checksum. Saves lots of + // bytes. + if (makeStaticMarkup) { + return ret; + } + + if (isRootElement) { + ret += ' ' + DOMPropertyOperations.createMarkupForRoot(); + } + ret += ' ' + DOMPropertyOperations.createMarkupForID(domID); + return ret; +} + +module.exports = createOpenTagMarkup; From 6b52db0e2e96cd664415b4ad67cfba06b5a9983b Mon Sep 17 00:00:00 2001 From: Tom Occhino Date: Thu, 27 Apr 2017 23:09:11 -0700 Subject: [PATCH 02/27] Fix build system, add shortcut scripts --- package.json | 3 +++ scripts/rollup/bundles.js | 9 +++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 98bfddac87a1..da932e574654 100644 --- a/package.json +++ b/package.json @@ -107,6 +107,9 @@ "version": 7 }, "scripts": { + "ssr-start": "node scripts/rollup/build.js dom-server --type=node_dev && cp -r build/packages/react-dom fixtures/ssr/node_modules/ && cd fixtures/ssr && yarn start", + "ssr-update": "node scripts/rollup/build.js dom-server --type=node_dev && cp -r build/packages/react-dom fixtures/ssr/node_modules/", + "bench": "npm run version-check && node scripts/bench/runner.js", "build": "npm run version-check && node scripts/rollup/build.js", "linc": "git diff --name-only --diff-filter=ACMRTUB `git merge-base HEAD master` | grep '\\.js$' | xargs eslint --", diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index e6bd5551ed68..9133f32953fb 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -169,7 +169,11 @@ const bundles = [ sourceMap: false, }, entry: 'src/renderers/dom/ReactDOMServer.js', - externals: ['prop-types', 'prop-types/checkPropTypes'], + externals: [ + 'prop-types', + 'prop-types/checkPropTypes', + 'create-react-class/factory', + ], fbEntry: 'src/renderers/dom/ReactDOMServer.js', hasteName: 'ReactDOMServerStack', isRenderer: true, @@ -177,11 +181,12 @@ const bundles = [ manglePropertiesOnProd: false, name: 'react-dom/server', paths: [ + 'src/isomorphic/**/*.js', 'src/renderers/dom/**/*.js', 'src/renderers/shared/**/*.js', - 'src/ReactVersion.js', 'src/shared/**/*.js', + 'src/addons/**/*.js', ], }, // TODO: there is no Fiber version of ReactDOMServer. From 345acc6d12fbe7f1e0732d7fd9dfda9bb94148d9 Mon Sep 17 00:00:00 2001 From: Tom Occhino Date: Fri, 28 Apr 2017 10:59:22 -0700 Subject: [PATCH 03/27] Make more ReactServerRendering-test unit tests pass --- .../dom/server/ReactDOMServerRendering.js | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/renderers/dom/server/ReactDOMServerRendering.js b/src/renderers/dom/server/ReactDOMServerRendering.js index 8cc4c7d7956a..9c7e05ec42bc 100644 --- a/src/renderers/dom/server/ReactDOMServerRendering.js +++ b/src/renderers/dom/server/ReactDOMServerRendering.js @@ -71,9 +71,9 @@ function ReactDOMServerRenderer(element, makeStaticMarkup) { context: emptyObject, footer: '', }]; - this.makeStaticMarkup = makeStaticMarkup; this.idCounter = 1; this.exhausted = false; + this.makeStaticMarkup = makeStaticMarkup; } ReactDOMServerRenderer.prototype.read = function(bytes) { @@ -97,6 +97,9 @@ ReactDOMServerRenderer.prototype.read = function(bytes) { ReactDOMServerRenderer.prototype.render = function(child, context) { if (typeof child === 'string' || typeof child === 'number') { + if (this.makeStaticMarkup) { + return escapeTextContentForBrowser('' + child); + } return ( '' + escapeTextContentForBrowser('' + child) + @@ -179,13 +182,6 @@ function getNonChildrenInnerMarkup(props) { return null; } -function renderToStringImpl(element, makeStaticMarkup) { - var renderer = new ReactDOMServerRenderer(element, makeStaticMarkup); - var markup = renderer.read(Infinity); - markup = ReactMarkupChecksum.addChecksumToMarkup(markup); - return markup; -} - /** * Render a ReactElement to its initial HTML. This should only be used on the * server. @@ -196,7 +192,10 @@ function renderToString(element) { ReactElement.isValidElement(element), 'renderToString(): You must pass a valid ReactElement.', ); - return renderToStringImpl(element, false); + var renderer = new ReactDOMServerRenderer(element, false); + var markup = renderer.read(Infinity); + markup = ReactMarkupChecksum.addChecksumToMarkup(markup); + return markup; } /** @@ -209,7 +208,9 @@ function renderToStaticMarkup(element) { ReactElement.isValidElement(element), 'renderToStaticMarkup(): You must pass a valid ReactElement.', ); - return renderToStringImpl(element, true); + var renderer = new ReactDOMServerRenderer(element, true); + var markup = renderer.read(Infinity); + return markup; } var ReactDOMServerRendering = { From 427bae29257153c59d4b92e7005bcd2fd156671f Mon Sep 17 00:00:00 2001 From: Tom Occhino Date: Tue, 2 May 2017 18:35:13 -0700 Subject: [PATCH 04/27] Make ReactServerRendering-test pass with copious help from Ben This is pretty hacky and I just inlined everything, but at least I sort of understand how it works now, and all of the tests are passing. There are also only 68 tests failing in the integration test suite. --- .../dom/server/ReactDOMServerRendering.js | 89 ++++++++++++++++++- 1 file changed, 86 insertions(+), 3 deletions(-) diff --git a/src/renderers/dom/server/ReactDOMServerRendering.js b/src/renderers/dom/server/ReactDOMServerRendering.js index 9c7e05ec42bc..1db8ec0e5d29 100644 --- a/src/renderers/dom/server/ReactDOMServerRendering.js +++ b/src/renderers/dom/server/ReactDOMServerRendering.js @@ -13,7 +13,6 @@ var ReactElement = require('ReactElement'); var ReactMarkupChecksum = require('ReactMarkupChecksum'); -var ReactNoopUpdateQueue = require('ReactNoopUpdateQueue'); var createOpenTagMarkup = require('createOpenTagMarkup'); var emptyObject = require('fbjs/lib/emptyObject'); @@ -21,9 +20,29 @@ var escapeTextContentForBrowser = require('escapeTextContentForBrowser'); var invariant = require('fbjs/lib/invariant'); var omittedCloseTags = require('omittedCloseTags'); var traverseAllChildren = require('traverseAllChildren'); +var warning = require('fbjs/lib/warning'); //var Readable = require('readable-stream').Readable; +function warnNoop( + publicInstance: ReactComponent, + callerName: string, +) { + if (__DEV__) { + var constructor = publicInstance.constructor; + warning( + false, + '%s(...): Can only update a mounting component. ' + + 'This usually means you called %s() outside componentWillMount() on the server. ' + + 'This is a no-op.\n\nPlease check the code for the %s component.', + callerName, + callerName, + (constructor && (constructor.displayName || constructor.name)) || + 'ReactClass', + ); + } +} + function shouldConstruct(Component) { return Component.prototype && Component.prototype.isReactComponent; } @@ -39,8 +58,43 @@ function resolve(child, context) { var Component = child.type; // TODO: Mask context var publicContext = context; - var updater = ReactNoopUpdateQueue; if (shouldConstruct(Component)) { + var queue = []; + var replace = false; + var updater = { + isMounted: function(publicInstance) { + return false; + }, + enqueueForceUpdate: function(publicInstance, callback, callerName) { + if (queue === null) { + warnNoop(inst, 'forceUpdate'); + return null; + } + }, + enqueueReplaceState: function( + publicInstance, + completeState, + callback, + callerName, + ) { + replace = true; + queue = [completeState]; + }, + enqueueSetState: function( + publicInstance, + partialState, + callback, + callerName, + ) { + if (queue === null) { + warnNoop(inst, 'setState'); + return null; + } + queue.push(partialState); + }, + }; + + var inst = new Component(child.props, publicContext, updater); inst.props = child.props; inst.context = publicContext; @@ -52,7 +106,36 @@ function resolve(child, context) { } if (inst.componentWillMount) { inst.componentWillMount(); - // TODO: setState in componentWillMount should work. + if (queue.length) { + var oldQueue = queue; + var oldReplace = replace; + queue = null; + replace = false; + + if (oldReplace && oldQueue.length === 1) { + inst.state = oldQueue[0]; + } else { + var nextState = oldReplace ? oldQueue[0] : inst.state; + var dontMutate = true; + for (var i = oldReplace ? 1 : 0; i < oldQueue.length; i++) { + var partial = oldQueue[i]; + let partialState = typeof partial === 'function' + ? partial.call(inst, nextState, child.props, publicContext) + : partial; + if (partialState) { + if (dontMutate) { + dontMutate = false; + nextState = Object.assign({}, nextState, partialState); + } else { + Object.assign(nextState, partialState); + } + } + } + inst.state = nextState; + } + } else { + queue = null; + } } child = inst.render(); var childContext = inst.getChildContext && inst.getChildContext(); From 103f00e8b32292b1bdf0d14734fca3786762efb2 Mon Sep 17 00:00:00 2001 From: Tom Occhino Date: Tue, 9 May 2017 19:12:31 -0700 Subject: [PATCH 05/27] remove some unnecessary cruft --- .../dom/server/ReactDOMServerRendering.js | 26 +++++-------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/src/renderers/dom/server/ReactDOMServerRendering.js b/src/renderers/dom/server/ReactDOMServerRendering.js index 1db8ec0e5d29..1b607442ae4d 100644 --- a/src/renderers/dom/server/ReactDOMServerRendering.js +++ b/src/renderers/dom/server/ReactDOMServerRendering.js @@ -65,41 +65,27 @@ function resolve(child, context) { isMounted: function(publicInstance) { return false; }, - enqueueForceUpdate: function(publicInstance, callback, callerName) { + enqueueForceUpdate: function(publicInstance) { if (queue === null) { - warnNoop(inst, 'forceUpdate'); + warnNoop(publicInstance, 'forceUpdate'); return null; } }, - enqueueReplaceState: function( - publicInstance, - completeState, - callback, - callerName, - ) { + enqueueReplaceState: function(publicInstance, completeState) { replace = true; queue = [completeState]; }, - enqueueSetState: function( - publicInstance, - partialState, - callback, - callerName, - ) { + enqueueSetState: function(publicInstance, partialState) { if (queue === null) { - warnNoop(inst, 'setState'); + warnNoop(publicInstance, 'setState'); return null; } queue.push(partialState); }, }; - var inst = new Component(child.props, publicContext, updater); - inst.props = child.props; - inst.context = publicContext; - inst.refs = emptyObject; - inst.updater = updater; + var initialState = inst.state; if (initialState === undefined) { inst.state = initialState = null; From ac550149fac615914027542ba94d338946bbad9d Mon Sep 17 00:00:00 2001 From: Tom Occhino Date: Tue, 9 May 2017 19:44:58 -0700 Subject: [PATCH 06/27] Run prettier on ReactDOMServerRendering.js --- .../dom/server/ReactDOMServerRendering.js | 59 ++++++++----------- 1 file changed, 23 insertions(+), 36 deletions(-) diff --git a/src/renderers/dom/server/ReactDOMServerRendering.js b/src/renderers/dom/server/ReactDOMServerRendering.js index 1b607442ae4d..cb4593ad25e9 100644 --- a/src/renderers/dom/server/ReactDOMServerRendering.js +++ b/src/renderers/dom/server/ReactDOMServerRendering.js @@ -24,10 +24,7 @@ var warning = require('fbjs/lib/warning'); //var Readable = require('readable-stream').Readable; -function warnNoop( - publicInstance: ReactComponent, - callerName: string, -) { +function warnNoop(publicInstance: ReactComponent, callerName: string) { if (__DEV__) { var constructor = publicInstance.constructor; warning( @@ -37,8 +34,7 @@ function warnNoop( 'This is a no-op.\n\nPlease check the code for the %s component.', callerName, callerName, - (constructor && (constructor.displayName || constructor.name)) || - 'ReactClass', + (constructor && (constructor.displayName || constructor.name)) || 'ReactClass' ); } } @@ -51,10 +47,7 @@ function resolve(child, context) { if (Array.isArray(child)) { throw new Error('well that was unexpected'); } - while ( - ReactElement.isValidElement(child) && - typeof child.type === 'function' - ) { + while (ReactElement.isValidElement(child) && typeof child.type === 'function') { var Component = child.type; // TODO: Mask context var publicContext = context; @@ -130,16 +123,18 @@ function resolve(child, context) { child = Component(child.props, publicContext, updater); } } - return {child, context}; + return { child, context }; } function ReactDOMServerRenderer(element, makeStaticMarkup) { - this.stack = [{ - children: [element], - childIndex: 0, - context: emptyObject, - footer: '', - }]; + this.stack = [ + { + children: [element], + childIndex: 0, + context: emptyObject, + footer: '', + }, + ]; this.idCounter = 1; this.exhausted = false; this.makeStaticMarkup = makeStaticMarkup; @@ -170,12 +165,10 @@ ReactDOMServerRenderer.prototype.render = function(child, context) { return escapeTextContentForBrowser('' + child); } return ( - '' + - escapeTextContentForBrowser('' + child) + - '' + '' + escapeTextContentForBrowser('' + child) + '' ); } else { - ({child, context} = resolve(child, context)); + ({ child, context } = resolve(child, context)); if (child === null || child === false) { return ''; } else { @@ -184,16 +177,16 @@ ReactDOMServerRenderer.prototype.render = function(child, context) { } }; -ReactDOMServerRenderer.prototype.renderDOM = function( - element, - context -) { +ReactDOMServerRenderer.prototype.renderDOM = function(element, context) { var tag = element.type.toLowerCase(); var props = element.props; if (tag === 'input') { - props = Object.assign({ - type: undefined, - }, props); + props = Object.assign( + { + type: undefined, + }, + props + ); } else if (tag === 'textarea') { props = Object.assign({}, props, { value: undefined, @@ -257,10 +250,7 @@ function getNonChildrenInnerMarkup(props) { * See https://facebook.github.io/react/docs/react-dom-server.html#rendertostring */ function renderToString(element) { - invariant( - ReactElement.isValidElement(element), - 'renderToString(): You must pass a valid ReactElement.', - ); + invariant(ReactElement.isValidElement(element), 'renderToString(): You must pass a valid ReactElement.'); var renderer = new ReactDOMServerRenderer(element, false); var markup = renderer.read(Infinity); markup = ReactMarkupChecksum.addChecksumToMarkup(markup); @@ -273,10 +263,7 @@ function renderToString(element) { * See https://facebook.github.io/react/docs/react-dom-server.html#rendertostaticmarkup */ function renderToStaticMarkup(element) { - invariant( - ReactElement.isValidElement(element), - 'renderToStaticMarkup(): You must pass a valid ReactElement.', - ); + invariant(ReactElement.isValidElement(element), 'renderToStaticMarkup(): You must pass a valid ReactElement.'); var renderer = new ReactDOMServerRenderer(element, true); var markup = renderer.read(Infinity); return markup; From bfeb8d41f33ca7c30763cf9b73d7928f035f59aa Mon Sep 17 00:00:00 2001 From: Tom Occhino Date: Wed, 10 May 2017 14:14:58 -0700 Subject: [PATCH 07/27] Fix more unit tests with Ben --- .../dom/server/ReactDOMServerRendering.js | 164 +++++++++++------- 1 file changed, 103 insertions(+), 61 deletions(-) diff --git a/src/renderers/dom/server/ReactDOMServerRendering.js b/src/renderers/dom/server/ReactDOMServerRendering.js index cb4593ad25e9..ddbaa69d3799 100644 --- a/src/renderers/dom/server/ReactDOMServerRendering.js +++ b/src/renderers/dom/server/ReactDOMServerRendering.js @@ -22,7 +22,22 @@ var omittedCloseTags = require('omittedCloseTags'); var traverseAllChildren = require('traverseAllChildren'); var warning = require('fbjs/lib/warning'); -//var Readable = require('readable-stream').Readable; +if (__DEV__) { + var { validateProperties: validateARIAProperties } = require('ReactDOMInvalidARIAHook'); + var { validateProperties: validateInputPropertes } = require('ReactDOMNullInputValuePropHook'); + var { validateProperties: validateUnknownPropertes } = require('ReactDOMUnknownPropertyHook'); + var validatePropertiesInDevelopment = function(type, props) { + validateARIAProperties(type, props); + validateInputPropertes(type, props); + validateUnknownPropertes(type, props); + }; +} + +var newlineEatingTags = { + listing: true, + pre: true, + textarea: true, +}; function warnNoop(publicInstance: ReactComponent, callerName: string) { if (__DEV__) { @@ -51,76 +66,85 @@ function resolve(child, context) { var Component = child.type; // TODO: Mask context var publicContext = context; - if (shouldConstruct(Component)) { - var queue = []; - var replace = false; - var updater = { - isMounted: function(publicInstance) { - return false; - }, - enqueueForceUpdate: function(publicInstance) { - if (queue === null) { - warnNoop(publicInstance, 'forceUpdate'); - return null; - } - }, - enqueueReplaceState: function(publicInstance, completeState) { - replace = true; - queue = [completeState]; - }, - enqueueSetState: function(publicInstance, partialState) { - if (queue === null) { - warnNoop(publicInstance, 'setState'); - return null; - } - queue.push(partialState); - }, - }; - var inst = new Component(child.props, publicContext, updater); + var inst; + var queue = []; + var replace = false; + var updater = { + isMounted: function(publicInstance) { + return false; + }, + enqueueForceUpdate: function(publicInstance) { + if (queue === null) { + warnNoop(publicInstance, 'forceUpdate'); + return null; + } + }, + enqueueReplaceState: function(publicInstance, completeState) { + replace = true; + queue = [completeState]; + }, + enqueueSetState: function(publicInstance, partialState) { + if (queue === null) { + warnNoop(publicInstance, 'setState'); + return null; + } + queue.push(partialState); + }, + }; - var initialState = inst.state; - if (initialState === undefined) { - inst.state = initialState = null; + if (shouldConstruct(Component)) { + inst = new Component(child.props, publicContext, updater); + } else { + inst = Component(child.props, publicContext, updater); + if (inst == null || inst.render == null) { + child = inst; + continue; } - if (inst.componentWillMount) { - inst.componentWillMount(); - if (queue.length) { - var oldQueue = queue; - var oldReplace = replace; - queue = null; - replace = false; + } + + var initialState = inst.state; + if (initialState === undefined) { + inst.state = initialState = null; + } + if (inst.componentWillMount) { + inst.componentWillMount(); + if (queue.length) { + var oldQueue = queue; + var oldReplace = replace; + queue = null; + replace = false; - if (oldReplace && oldQueue.length === 1) { - inst.state = oldQueue[0]; - } else { - var nextState = oldReplace ? oldQueue[0] : inst.state; - var dontMutate = true; - for (var i = oldReplace ? 1 : 0; i < oldQueue.length; i++) { - var partial = oldQueue[i]; - let partialState = typeof partial === 'function' - ? partial.call(inst, nextState, child.props, publicContext) - : partial; - if (partialState) { - if (dontMutate) { - dontMutate = false; - nextState = Object.assign({}, nextState, partialState); - } else { - Object.assign(nextState, partialState); - } + if (oldReplace && oldQueue.length === 1) { + inst.state = oldQueue[0]; + } else { + var nextState = oldReplace ? oldQueue[0] : inst.state; + var dontMutate = true; + for (var i = oldReplace ? 1 : 0; i < oldQueue.length; i++) { + var partial = oldQueue[i]; + let partialState = typeof partial === 'function' + ? partial.call(inst, nextState, child.props, publicContext) + : partial; + if (partialState) { + if (dontMutate) { + dontMutate = false; + nextState = Object.assign({}, nextState, partialState); + } else { + Object.assign(nextState, partialState); } } - inst.state = nextState; } - } else { - queue = null; + inst.state = nextState; } + } else { + queue = null; } - child = inst.render(); - var childContext = inst.getChildContext && inst.getChildContext(); + } + child = inst.render(); + + var childContext = inst.getChildContext && inst.getChildContext(); + if (childContext) { context = Object.assign({}, context, childContext); - } else { - child = Component(child.props, publicContext, updater); } } return { child, context }; @@ -194,6 +218,10 @@ ReactDOMServerRenderer.prototype.renderDOM = function(element, context) { }); } + if (__DEV__) { + validatePropertiesInDevelopment(tag, props); + } + var out = createOpenTagMarkup( element.type, tag, @@ -213,6 +241,19 @@ ReactDOMServerRenderer.prototype.renderDOM = function(element, context) { var children = []; var innerMarkup = getNonChildrenInnerMarkup(props); if (innerMarkup != null) { + if (newlineEatingTags[tag] && innerMarkup.charAt(0) === '\n') { + // text/html ignores the first character in these tags if it's a newline + // Prefer to break application/xml over text/html (for now) by adding + // a newline specifically to get eaten by the parser. (Alternately for + // textareas, replacing "^\n" with "\r\n" doesn't get eaten, and the first + // \r is normalized out by HTMLTextAreaElement#value.) + // See: + // See: + // See: + // See: Parsing of "textarea" "listing" and "pre" elements + // from + out += '\n'; + } out += innerMarkup; } else { traverseAllChildren(props.children, function(ctx, child, name) { @@ -229,6 +270,7 @@ ReactDOMServerRenderer.prototype.renderDOM = function(element, context) { }); return out; }; + function getNonChildrenInnerMarkup(props) { var innerHTML = props.dangerouslySetInnerHTML; if (innerHTML != null) { From 95ef6bca5f3ab5b222a05e936fda0aab9cb56c08 Mon Sep 17 00:00:00 2001 From: Tom Occhino Date: Wed, 10 May 2017 14:40:45 -0700 Subject: [PATCH 08/27] Add support for input and textarea by copy pasting a bunch of code from ReactDOMInput/ReactDOMTextarea --- .../dom/server/ReactDOMServerRendering.js | 116 +++++++++++++++++- 1 file changed, 114 insertions(+), 2 deletions(-) diff --git a/src/renderers/dom/server/ReactDOMServerRendering.js b/src/renderers/dom/server/ReactDOMServerRendering.js index ddbaa69d3799..155eaa2a0bdd 100644 --- a/src/renderers/dom/server/ReactDOMServerRendering.js +++ b/src/renderers/dom/server/ReactDOMServerRendering.js @@ -11,6 +11,7 @@ 'use strict'; +var ReactControlledValuePropTypes = require('ReactControlledValuePropTypes'); var ReactElement = require('ReactElement'); var ReactMarkupChecksum = require('ReactMarkupChecksum'); @@ -33,6 +34,10 @@ if (__DEV__) { }; } +var didWarnDefaultInputValue = false; +var didWarnDefaultChecked = false; +var didWarnDefaultTextareaValue = false; + var newlineEatingTags = { listing: true, pre: true, @@ -205,16 +210,123 @@ ReactDOMServerRenderer.prototype.renderDOM = function(element, context) { var tag = element.type.toLowerCase(); var props = element.props; if (tag === 'input') { + if (__DEV__) { + ReactControlledValuePropTypes.checkPropTypes( + 'input', + props, + () => '', //getCurrentFiberStackAddendum + ); + + if ( + props.checked !== undefined && + props.defaultChecked !== undefined && + !didWarnDefaultChecked + ) { + warning( + false, + '%s contains an input of type %s with both checked and defaultChecked props. ' + + 'Input elements must be either controlled or uncontrolled ' + + '(specify either the checked prop, or the defaultChecked prop, but not ' + + 'both). Decide between using a controlled or uncontrolled input ' + + 'element and remove one of these props. More info: ' + + 'https://fb.me/react-controlled-components', + 'A component', + props.type, + ); + didWarnDefaultChecked = true; + } + if ( + props.value !== undefined && + props.defaultValue !== undefined && + !didWarnDefaultInputValue + ) { + warning( + false, + '%s contains an input of type %s with both value and defaultValue props. ' + + 'Input elements must be either controlled or uncontrolled ' + + '(specify either the value prop, or the defaultValue prop, but not ' + + 'both). Decide between using a controlled or uncontrolled input ' + + 'element and remove one of these props. More info: ' + + 'https://fb.me/react-controlled-components', + 'A component', + props.type, + ); + didWarnDefaultInputValue = true; + } + } + props = Object.assign( { type: undefined, }, - props + props, + { + defaultChecked: undefined, + defaultValue: undefined, + value: props.value != null ? props.value : props.defaultValue, + checked: props.checked != null ? props.checked : props.defaultChecked, + } ); } else if (tag === 'textarea') { + if (__DEV__) { + ReactControlledValuePropTypes.checkPropTypes( + 'textarea', + props, + () => '', //getCurrentFiberStackAddendum + ); + if ( + props.value !== undefined && + props.defaultValue !== undefined && + !didWarnDefaultTextareaValue + ) { + warning( + false, + 'Textarea elements must be either controlled or uncontrolled ' + + '(specify either the value prop, or the defaultValue prop, but not ' + + 'both). Decide between using a controlled or uncontrolled textarea ' + + 'and remove one of these props. More info: ' + + 'https://fb.me/react-controlled-components', + ); + didWarnDefaultTextareaValue = true; + } + } + + var initialValue = props.value; + if (initialValue == null) { + var defaultValue = props.defaultValue; + // TODO (yungsters): Remove support for children content in