├── .babelrc ├── .editorconfig ├── .gitignore ├── .nvmrc ├── .prettierrc ├── LICENSE ├── README.md ├── dist ├── DispatcherModifier.js ├── SSRRenderer.js ├── index.js ├── react-dom │ └── src │ │ ├── server │ │ ├── DOMMarkupOperations.js │ │ ├── escapeTextForBrowser.js │ │ └── quoteAttributeValueForBrowser.js │ │ └── shared │ │ ├── CSSProperty.js │ │ ├── DOMProperty.js │ │ ├── dangerousStyleValue.js │ │ ├── isCustomComponent.js │ │ └── omittedCloseTags.js ├── reactMonkeyPatch.js └── reactUtils │ └── createMarkupForStyles.js ├── examples └── basic │ ├── dist │ └── main.js │ ├── index.js │ ├── server.js │ └── src │ └── index.js ├── jest.config.js ├── manualtest.js ├── package-lock.json ├── package.json ├── react.js ├── rollup.config.js ├── src ├── __tests__ │ └── resources.test.js ├── react │ ├── cache.js │ ├── hooks.js │ ├── index.js │ ├── render.js │ └── utils │ │ └── serialize.js └── renderer │ ├── DispatcherModifier.js │ ├── SSRRenderer.js │ ├── __tests__ │ ├── SSRRenderer.test.js │ └── hooks.test.js │ ├── index.js │ ├── react-dom │ └── src │ │ ├── server │ │ ├── DOMMarkupOperations.js │ │ ├── escapeTextForBrowser.js │ │ └── quoteAttributeValueForBrowser.js │ │ └── shared │ │ ├── CSSProperty.js │ │ ├── DOMProperty.js │ │ ├── dangerousStyleValue.js │ │ ├── isCustomComponent.js │ │ └── omittedCloseTags.js │ ├── reactMonkeyPatch.js │ └── reactUtils │ └── createMarkupForStyles.js └── test └── setup-tests.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "babel-preset-env", 5 | { 6 | "targets": { 7 | "node": "6" 8 | } 9 | } 10 | ], 11 | "react" 12 | ], 13 | "plugins": [ 14 | "transform-class-properties", 15 | "transform-object-rest-spread", 16 | "transform-flow-strip-types" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | 13 | [*.json] 14 | indent_style = space 15 | indent_size = 4 16 | 17 | [package.json] 18 | indent_style = space 19 | indent_size = 2 20 | 21 | [*.groovy] 22 | indent_style = space 23 | indent_size = 4 24 | 25 | [Makefile] 26 | indent_style = tab 27 | indent_size = 4 28 | 29 | [*.sh] 30 | indent_style = space 31 | indent_size = 4 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | TODO.md 2 | .vscode 3 | *.retry 4 | 5 | .DS_Store 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Typescript v1 declaration files 46 | typings/ 47 | 48 | # Optional npm cache directory 49 | .npm 50 | 51 | # Optional eslint cache 52 | .eslintcache 53 | 54 | # Optional REPL history 55 | .node_repl_history 56 | 57 | # Output of 'npm pack' 58 | *.tgz 59 | 60 | # Yarn Integrity file 61 | .yarn-integrity 62 | 63 | # dotenv environment variables file 64 | .env 65 | 66 | # next.js build output 67 | .next 68 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v6 -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-present, Fredrik Höglund 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # :moon: Aldrin - An Experimental React Suspense Serverside Renderer 2 | 3 | > Note: This project has been successed by https://github.com/Ephem/react-lightyear 4 | 5 | With a few important caveats, this project is a working serverside renderer for React, with out of the box support for Suspense data-fetching and hydration. 6 | 7 | > :warning: This project is highly experimental and is not suitable for production use 8 | 9 | > :warning: This project does not in any way represent future React-APIs, or how the new Fizz server renderer will work 10 | 11 | [This blogpost](https://blogg.svt.se/svti/react-suspense-server-rendering/) contains some background on React Suspense and SSR. 12 | 13 | ## Usage 14 | 15 | **Install** 16 | 17 | ```bash 18 | npm install react-aldrin react@16.7.0-alpha.2 react-dom@16.7.0-alpha.2 --save 19 | ``` 20 | 21 | See [examples/basic](https://github.com/Ephem/react-aldrin/tree/master/examples/basic) for a full working example. 22 | 23 | **Fetching data** 24 | 25 | ```jsx 26 | import React from 'react'; 27 | import { createResource, useReadResource } from 'react-aldrin/react'; 28 | 29 | // Create a resource 30 | const apiUrl = 'http://www.made-up-color-api.com/api/colors/'; 31 | const colorResource = createResource('colorResource', colorId => 32 | fetch(apiUrl + colorId).then(res => res.text()) 33 | ); 34 | 35 | // This component would have to be wrapped in a 36 | // -component from React 37 | export default function Color({ colorId }) { 38 | // Read data from the resource, result is automatically cached 39 | const colorName = useReadResource(colorResource, colorId); 40 | 41 | return

This is a color: {colorName}

; 42 | } 43 | ``` 44 | 45 | **Server rendering** 46 | 47 | ```jsx 48 | // Import react-aldrin at the top for monkey-patching to work 49 | import { renderToString } from 'react-aldrin'; 50 | import 'isomorphic-fetch'; 51 | import React from 'react'; 52 | import { App } from './App.js'; 53 | 54 | (...) 55 | 56 | app.get('/', async (req, res) => { 57 | // Rendering is now async, need to wait for it 58 | const { markupWithCacheData } = await renderToString(); 59 | 60 | // In this case we are using "markupWithCacheData" which already 61 | // contains the dehydrated data from the data fetching 62 | res.render('index', { markupWithCacheData }); 63 | }); 64 | ``` 65 | 66 | **Hydrate on the client** 67 | 68 | ```jsx 69 | import { hydrate } from 'react-aldrin/react'; 70 | import { App } from './App.js'; 71 | 72 | // Using hydrate from this package will automatically 73 | // hydrate cache data as well as markup 74 | hydrate(, document.getElementById('react-app')); 75 | ``` 76 | 77 | That's it! You can fetch data as deep in the component tree as you want and it will automatically be fetched within a single render pass and de/rehydrated to the client for you. No more hoisting data dependencies to the route-level (like in Next.js) or double-rendering (like in Apollo). 78 | 79 | ## :warning: Caveats and limitations 80 | 81 | This renderer is built on top of the React Reconciler, as opposed to the official serverside renderer which is a complete standalone implementation. This has a few important implications: 82 | 83 | * In many respects this renderer behaves as if it was a client-renderer! 84 | * :open_mouth: Both hooks and lifecycles would normally behave as on the client.. 85 | * :see_no_evil: ..but these have been monkey patched to not do so 86 | * :exclamation: Make sure you import `react-aldrin` at the very start of your application for monkey patching to work 87 | * Performance is (probably) not what it should be 88 | * Streaming is impossible 89 | * Etc.. 90 | 91 | There are also tons of other unsolved problems and limitations: 92 | 93 | * Cache invalidation strategies 94 | * Multiple roots sharing a cache 95 | * Only supports version `16.7.0-alpha.2` 96 | * Is likely to break with future React updates 97 | * Built on ugly hacks (secret internals and monkey patching), likely to be buggy 98 | 99 | Finally, this renderer only aim to explore possible future code patterns, not any other of the exciting stuff which the React team is also working on, like improved streaming rendering, partial hydration etc! :tada: 100 | 101 | > This is not a serious attempt at building a stable renderer, the aim is simply to explore what code patterns _could possibly_ look like with Suspense+SSR. 102 | 103 | ## API 104 | 105 | This package is split into two parts, `react-aldrin` contains the server renderer and `react-aldrin/react` contains helpers for React. 106 | 107 | ### `react-aldrin` 108 | 109 | #### `renderToString(element)` 110 | 111 | Asyncronously render a React element to its initial HTML. 112 | 113 | Automatically wraps the `element` in a `` so resources can be used. 114 | 115 | **Returns** 116 | 117 | This function will return a Promise which resolves to: 118 | 119 | ``` 120 | { 121 | markup, // Markup 122 | markupWithCacheData, // Markup which includes serialized cache-data 123 | cache // The cache 124 | } 125 | ``` 126 | 127 | #### `renderToStaticMarkup(element)` 128 | 129 | Asyncronously render the element to its initial HTML, but without the extra DOM attributes that React uses internally. Since it's not meant to hydrate, this never includes serialized cache-data (though you could do that yourself if needed). 130 | 131 | Automatically wraps the `element` in a `` so resources can be used. 132 | 133 | **Returns** 134 | 135 | This function will return a Promise which resolves to: 136 | 137 | ``` 138 | { 139 | markup, // Markup 140 | cache // The cache 141 | } 142 | ``` 143 | 144 | ### `react-aldrin/react` 145 | 146 | #### `render(element, container[, callback])` 147 | 148 | This proxies to the original `ReactDOM.render`. 149 | 150 | Automatically wraps the `element` in a `` so resources can be used. This means it is possible to use this package without the server renderer-part if you would want to. 151 | 152 | #### `hydrate(element, container[, callback])` 153 | 154 | This proxies to the original `ReactDOM.hydrate`, but it first hydrates the cache-data included in `markupWithCacheData` from `renderToString` and removes it from the DOM to avoid a hydration mismatch. 155 | 156 | Automatically wraps the `element` in a `` so resources can be used. 157 | 158 | #### `createResource(resourceName, loadResource[, hash])` 159 | 160 | 1. `resourceName` must be a unique name and the same when the server and client renders 161 | 2. `loadResource` is a function that takes an optional `key` as argument and returns a Promise which resolves to data, that is, the function that should be called to load the resource 162 | 3. `hash` is an optional function that is used to hash the `key` used to load some specific data before it is placed in the cache, useful if you want to use keys that are not serializeable by default 163 | 164 | **Returns** 165 | 166 | A `resource`, see below. 167 | 168 | #### `resource` 169 | 170 | This represents a resource. It is not meant to be used directly, but instead by passing it into the hook `useReadResource(resource, key)`. You can interact with it directly via a couple of functions by passing in a manually created cache, but this is currently undocumented. 171 | 172 | #### `useReadResource(resource, key)` 173 | 174 | This is a React-hook that reads from the resource, passing in `key` as argument. Directly returns the data for `key` if it is cached, throws data fetching-Promise and lets React re-render at a later point if data is not in cache. Uses `PrimaryCacheContext` behind the scenes. 175 | 176 | #### `PrimaryCacheContext`, `createCache([initialData])` and `cache` 177 | 178 | These are available for advanced behaviours like using multiple caches or taking care of cache-serialization and hydration yourself, but they are currently undocumented. This package and its examples are currently focused on showing off the easiest possible and most magical of worlds. :sparkles: :crystal_ball: :sparkles: 179 | 180 | ## Todo 181 | 182 | This list is really incomplete, but I thought I'd list at least a couple of things: 183 | 184 | * Bigger and better examples 185 | * :white_check_mark:~~Safer serialization of data~~ 186 | * More tests 187 | * Code cleanup 188 | * Better build/project setup 189 | * Better support for preloading, cache invalidation and a bunch of other stuff 190 | * Documenting currently undocumented APIs 191 | * Document experiments and lessons learned 192 | 193 | Just to be clear, I view this as an experiment and have no ambition to make it production ready. Even so, if you think it's fun, feel free to contribute, open issues or chat me up for a discussion. :smile: :envelope: 194 | 195 | I'd also love to hear from you if you experiment with it! :love_letter: 196 | 197 | ## Acknowledgements 198 | 199 | A lot of the code and ideas here are shamelessly borrowed directly from React and the React-team. Thank you so much for all your hard work! :clap: :100: 200 | 201 | --- 202 | 203 | Because it's really important, here is a final disclaimer: 204 | 205 | > :warning: This project does not in any way represent future React-APIs, or how the new Fizz server renderer will work 206 | 207 | If you do experiment with this, make sure you include similar disclaimers to avoid any fear, uncertainty and doubt. 208 | -------------------------------------------------------------------------------- /dist/DispatcherModifier.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _react = require('react'); 8 | 9 | var _react2 = _interopRequireDefault(_react); 10 | 11 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 12 | 13 | class DispatcherModifier extends _react2.default.Component { 14 | constructor(...args) { 15 | super(...args); 16 | 17 | const currentDispatcher = _react2.default.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner.currentDispatcher; 18 | 19 | currentDispatcher.useEffect = () => undefined; 20 | currentDispatcher.useImperativeMethods = () => undefined; 21 | currentDispatcher.useCallback = cb => cb; 22 | currentDispatcher.useLayoutEffect = () => { 23 | if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev') { 24 | console.warn('useLayoutEffect does nothing on the server, because its effect cannot ' + "be encoded into the server renderer's output format. This will lead " + 'to a mismatch between the initial, non-hydrated UI and the intended ' + 'UI. To avoid this, useLayoutEffect should only be used in ' + 'components that render exclusively on the client.'); 25 | } 26 | return undefined; 27 | }; 28 | } 29 | render() { 30 | return this.props.children; 31 | } 32 | } 33 | exports.default = DispatcherModifier; /** 34 | * Copyright (c) 2018-present, Fredrik Höglund 35 | * 36 | * This source code is licensed under the MIT license found in the 37 | * LICENSE file in the root directory of this source tree. 38 | */ -------------------------------------------------------------------------------- /dist/SSRRenderer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.SSRTreeNode = exports.RAW_TEXT_TYPE = exports.ROOT_STATIC_TYPE = exports.ROOT_TYPE = undefined; 7 | exports.renderToString = renderToString; 8 | exports.renderToStaticMarkup = renderToStaticMarkup; 9 | 10 | require('./reactMonkeyPatch'); 11 | 12 | require('raf/polyfill'); 13 | 14 | var _react = require('react'); 15 | 16 | var _react2 = _interopRequireDefault(_react); 17 | 18 | var _reactReconciler = require('react-reconciler'); 19 | 20 | var _reactReconciler2 = _interopRequireDefault(_reactReconciler); 21 | 22 | var _scheduler = require('scheduler'); 23 | 24 | var ReactScheduler = _interopRequireWildcard(_scheduler); 25 | 26 | var _emptyObject = require('fbjs/lib/emptyObject'); 27 | 28 | var _emptyObject2 = _interopRequireDefault(_emptyObject); 29 | 30 | var _omittedCloseTags = require('./react-dom/src/shared/omittedCloseTags'); 31 | 32 | var _omittedCloseTags2 = _interopRequireDefault(_omittedCloseTags); 33 | 34 | var _isCustomComponent = require('./react-dom/src/shared/isCustomComponent'); 35 | 36 | var _isCustomComponent2 = _interopRequireDefault(_isCustomComponent); 37 | 38 | var _escapeTextForBrowser = require('./react-dom/src/server/escapeTextForBrowser'); 39 | 40 | var _escapeTextForBrowser2 = _interopRequireDefault(_escapeTextForBrowser); 41 | 42 | var _DOMMarkupOperations = require('./react-dom/src/server/DOMMarkupOperations'); 43 | 44 | var _createMarkupForStyles = require('./reactUtils/createMarkupForStyles'); 45 | 46 | var _createMarkupForStyles2 = _interopRequireDefault(_createMarkupForStyles); 47 | 48 | var _DispatcherModifier = require('./DispatcherModifier'); 49 | 50 | var _DispatcherModifier2 = _interopRequireDefault(_DispatcherModifier); 51 | 52 | var _react3 = require('../react'); 53 | 54 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } 55 | 56 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 57 | 58 | // For now the scheduler uses requestAnimationFrame, 59 | // so we need to polyfill it 60 | const ROOT_TYPE = exports.ROOT_TYPE = Symbol('ROOT_TYPE'); /** 61 | * Copyright (c) 2018-present, Fredrik Höglund 62 | * 63 | * This source code is licensed under the MIT license found in the 64 | * LICENSE file in the root directory of this source tree. 65 | * 66 | * Some of the code in this file is copied or adapted from the React project, 67 | * used under the license below: 68 | * 69 | * Copyright (c) 2013-2018, Facebook, Inc. 70 | * 71 | * Permission is hereby granted, free of charge, to any person obtaining a copy 72 | * of this software and associated documentation files (the "Software"), to deal 73 | * in the Software without restriction, including without limitation the rights 74 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 75 | * copies of the Software, and to permit persons to whom the Software is 76 | * furnished to do so, subject to the following conditions: 77 | 78 | * The above copyright notice and this permission notice shall be included in all 79 | * copies or substantial portions of the Software. 80 | 81 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 82 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 83 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 84 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 85 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 86 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 87 | * SOFTWARE. 88 | */ 89 | 90 | const ROOT_STATIC_TYPE = exports.ROOT_STATIC_TYPE = Symbol('ROOT_STATIC_TYPE'); 91 | const RAW_TEXT_TYPE = exports.RAW_TEXT_TYPE = Symbol('RAW_TEXT_TYPE'); 92 | 93 | function isEventListener(propName) { 94 | return propName.slice(0, 2).toLowerCase() === 'on'; 95 | } 96 | 97 | function getMarkupForChildren(children, staticMarkup, selectedValue) { 98 | const childrenMarkup = []; 99 | for (let i = 0, l = children.length; i < l; i += 1) { 100 | const previousWasText = i > 0 && children[i - 1].type === RAW_TEXT_TYPE; 101 | childrenMarkup.push(children[i].toString(staticMarkup, previousWasText, undefined, selectedValue)); 102 | } 103 | return childrenMarkup.join(''); 104 | } 105 | 106 | const RESERVED_PROPS = { 107 | children: null, 108 | dangerouslySetInnerHTML: null, 109 | suppressContentEditableWarning: null, 110 | suppressHydrationWarning: null 111 | }; 112 | 113 | class SSRTreeNode { 114 | constructor(type, text) { 115 | this.children = []; 116 | 117 | this.type = type; 118 | this.text = text; 119 | this.attributes = {}; 120 | } 121 | 122 | appendChild(child) { 123 | this.children.push(child); 124 | } 125 | insertBefore(child, beforeChild) { 126 | this.children.splice(this.children.indexOf(beforeChild, 0, child)); 127 | } 128 | removeChild(child) { 129 | this.children = this.children.filter(c => c !== child); 130 | } 131 | setText(text) { 132 | this.text = text; 133 | } 134 | setAttribute(name, value) { 135 | this.attributes[name] = value; 136 | } 137 | attributesToString(attributes) { 138 | let ret = ''; 139 | for (const key in attributes) { 140 | if (!attributes.hasOwnProperty(key)) { 141 | continue; 142 | } 143 | let value = attributes[key]; 144 | if (value == null) { 145 | continue; 146 | } 147 | if (key === 'style') { 148 | value = (0, _createMarkupForStyles2.default)(value); 149 | } 150 | let markup = null; 151 | if ((0, _isCustomComponent2.default)(this.type.toLowerCase(), attributes)) { 152 | if (!RESERVED_PROPS.hasOwnProperty(key)) { 153 | markup = (0, _DOMMarkupOperations.createMarkupForCustomAttribute)(key, value); 154 | } 155 | } else { 156 | markup = (0, _DOMMarkupOperations.createMarkupForProperty)(key, value); 157 | } 158 | if (markup) { 159 | ret += ' ' + markup; 160 | } 161 | } 162 | return ret; 163 | } 164 | toString(staticMarkup, previousWasText, isRoot, selectedValue) { 165 | let renderAttributes = this.attributes; 166 | let selectSelectedValue; 167 | let childrenMarkup; 168 | const rawInnerHtml = this.attributes.dangerouslySetInnerHTML && this.attributes.dangerouslySetInnerHTML.__html; 169 | if (this.type === ROOT_STATIC_TYPE) { 170 | let markup = getMarkupForChildren(this.children, staticMarkup); 171 | return markup; 172 | } 173 | if (this.type === ROOT_TYPE) { 174 | return this.children.map(c => c.toString(staticMarkup, undefined, true)).join(''); 175 | } 176 | if (this.type === RAW_TEXT_TYPE) { 177 | if (!staticMarkup && previousWasText) { 178 | return '' + (0, _escapeTextForBrowser2.default)(this.text); 179 | } 180 | return (0, _escapeTextForBrowser2.default)(this.text); 181 | } 182 | if (this.type === 'input') { 183 | if (renderAttributes.defaultValue || renderAttributes.defaultChecked) { 184 | renderAttributes = Object.assign({}, renderAttributes, { 185 | value: renderAttributes.value != null ? renderAttributes.value : renderAttributes.defaultValue, 186 | defaultValue: undefined, 187 | checked: renderAttributes.Checked != null ? renderAttributes.Checked : renderAttributes.defaultChecked, 188 | defaultChecked: undefined 189 | }); 190 | } 191 | } else if (this.type === 'select') { 192 | if (renderAttributes.value || renderAttributes.defaultValue) { 193 | selectSelectedValue = renderAttributes.value || renderAttributes.defaultValue; 194 | renderAttributes = Object.assign({}, renderAttributes, { 195 | value: undefined, 196 | defaultValue: undefined 197 | }); 198 | } 199 | } else if (this.type === 'textarea') { 200 | if (renderAttributes.value || renderAttributes.defaultValue) { 201 | this.appendChild(new SSRTreeNode(RAW_TEXT_TYPE, renderAttributes.value || renderAttributes.defaultValue)); 202 | renderAttributes = Object.assign({}, renderAttributes, { 203 | value: undefined, 204 | defaultValue: undefined 205 | }); 206 | } 207 | } else if (this.type === 'option') { 208 | childrenMarkup = getMarkupForChildren(this.children, staticMarkup, selectSelectedValue); 209 | let selected = null; 210 | if (selectedValue != null) { 211 | let value = renderAttributes.value != null ? renderAttributes.value : childrenMarkup; 212 | if (Array.isArray(selectedValue)) { 213 | for (let i = 0; i < selectedValue.length; i++) { 214 | if (selectedValue[i] === value) { 215 | selected = true; 216 | break; 217 | } 218 | } 219 | } else { 220 | selected = selectedValue === value; 221 | } 222 | renderAttributes = Object.assign({}, { 223 | selected 224 | }, renderAttributes); 225 | } 226 | } 227 | 228 | const selfClose = !this.children.length && _omittedCloseTags2.default[this.type]; 229 | const startTag = `<${this.type}${this.attributesToString(renderAttributes)}${isRoot ? ' data-reactroot=""' : ''}${selfClose ? '/>' : '>'}`; 230 | childrenMarkup = rawInnerHtml || childrenMarkup || getMarkupForChildren(this.children, staticMarkup, selectSelectedValue); 231 | const endTag = selfClose ? '' : ``; 232 | return startTag + childrenMarkup + endTag; 233 | } 234 | } 235 | 236 | exports.SSRTreeNode = SSRTreeNode; 237 | const hostConfig = { 238 | getRootHostContext(rootInstance) { 239 | return _emptyObject2.default; 240 | }, 241 | getChildHostContext(parentHostContext, type) { 242 | return _emptyObject2.default; 243 | }, 244 | 245 | // Useful only for testing 246 | getPublicInstance(inst) { 247 | return inst; 248 | }, 249 | 250 | // Create the DOMElement, but attributes are set in `finalizeInitialChildren` 251 | createInstance(type, props, rootContainerInstance, hostContext, internalInstanceHandle) { 252 | return new SSRTreeNode(type); 253 | }, 254 | 255 | // appendChild for direct children 256 | appendInitialChild(parentInstance, child) { 257 | parentInstance.appendChild(child); 258 | }, 259 | 260 | // Actually set the attributes and text content to the domElement and check if 261 | // it needs focus, which will be eventually set in `commitMount` 262 | finalizeInitialChildren(element, type, props) { 263 | Object.keys(props).forEach(propName => { 264 | const propValue = props[propName]; 265 | 266 | if (propName === 'children') { 267 | if (typeof propValue === 'string' || typeof propValue === 'number') { 268 | element.appendChild(new SSRTreeNode(RAW_TEXT_TYPE, propValue)); 269 | } 270 | } else if (propName === 'className') { 271 | element.setAttribute('class', propValue); 272 | } else if (!isEventListener(propName)) { 273 | element.setAttribute(propName, propValue); 274 | } 275 | }); 276 | return false; 277 | }, 278 | 279 | // Calculate the updatePayload 280 | prepareUpdate(domElement, type, oldProps, newProps) {}, 281 | 282 | shouldSetTextContent(type, props) { 283 | return type === 'textarea' || typeof props.children === 'string' || typeof props.children === 'number'; 284 | }, 285 | shouldDeprioritizeSubtree(type, props) {}, 286 | createTextInstance(text, rootContainerInstance, hostContext, internalInstanceHandle) { 287 | return new SSRTreeNode(RAW_TEXT_TYPE, text); 288 | }, 289 | scheduleDeferredCallback: ReactScheduler.unstable_scheduleCallback, 290 | cancelDeferredCallback: ReactScheduler.unstable_cancelCallback, 291 | shouldYield: ReactScheduler.unstable_shouldYield, 292 | 293 | scheduleTimeout: setTimeout, 294 | cancelTimeout: clearTimeout, 295 | 296 | setTimeout: setTimeout, 297 | clearTimeout: clearTimeout, 298 | 299 | noTimeout: -1, 300 | 301 | // Commit hooks, useful mainly for react-dom syntethic events 302 | prepareForCommit() {}, 303 | resetAfterCommit() {}, 304 | 305 | now: ReactScheduler.unstable_now, 306 | isPrimaryRenderer: true, 307 | //useSyncScheduling: true, 308 | 309 | supportsMutation: true, 310 | commitUpdate(domElement, updatePayload, type, oldProps, newProps, internalInstanceHandle) {}, 311 | commitMount(domElement, type, newProps, internalInstanceHandle) {}, 312 | commitTextUpdate(textInstance, oldText, newText) { 313 | textInstance.setText(newText); 314 | }, 315 | resetTextContent(textInstance) { 316 | textInstance.setText(''); 317 | }, 318 | appendChild(parentInstance, child) { 319 | parentInstance.appendChild(child); 320 | }, 321 | 322 | // appendChild to root container 323 | appendChildToContainer(parentInstance, child) { 324 | parentInstance.appendChild(child); 325 | }, 326 | insertBefore(parentInstance, child, beforeChild) { 327 | parentInstance.insertBefore(child, beforeChild); 328 | }, 329 | insertInContainerBefore(parentInstance, child, beforeChild) { 330 | parentInstance.insertBefore(child, beforeChild); 331 | }, 332 | removeChild(parentInstance, child) { 333 | parentInstance.removeChild(child); 334 | }, 335 | removeChildFromContainer(parentInstance, child) { 336 | parentInstance.removeChild(child); 337 | }, 338 | 339 | // These are todo and not well understood on the server 340 | hideInstance() {}, 341 | hideTextInstance() {}, 342 | unhideInstance() {}, 343 | unhideTextInstance() {} 344 | }; 345 | 346 | const SSRRenderer = (0, _reactReconciler2.default)(hostConfig); 347 | 348 | function ReactRoot({ staticMarkup = false } = {}) { 349 | const rootType = staticMarkup ? ROOT_STATIC_TYPE : ROOT_TYPE; 350 | const ssrTreeRootNode = new SSRTreeNode(rootType); 351 | this._internalTreeRoot = ssrTreeRootNode; 352 | const root = SSRRenderer.createContainer(ssrTreeRootNode, true); 353 | this._internalRoot = root; 354 | this._staticMarkup = staticMarkup; 355 | } 356 | ReactRoot.prototype.render = function (children) { 357 | const root = this._internalRoot; 358 | const work = new ReactWork(this._internalTreeRoot, { 359 | staticMarkup: this._staticMarkup 360 | }); 361 | SSRRenderer.updateContainer(children, root, null, work._onCommit); 362 | return work; 363 | }; 364 | ReactRoot.prototype.unmount = function () { 365 | const root = this._internalRoot; 366 | const work = new ReactWork(this._internalTreeRoot); 367 | callback = callback === undefined ? null : callback; 368 | SSRRenderer.updateContainer(null, root, null, work._onCommit); 369 | return work; 370 | }; 371 | 372 | function ReactWork(root, { staticMarkup = false } = {}) { 373 | this._callbacks = null; 374 | this._didCommit = false; 375 | // TODO: Avoid need to bind by replacing callbacks in the update queue with 376 | // list of Work objects. 377 | this._onCommit = this._onCommit.bind(this); 378 | this._internalRoot = root; 379 | this._staticMarkup = staticMarkup; 380 | } 381 | ReactWork.prototype.then = function (onCommit) { 382 | if (this._didCommit) { 383 | onCommit(this._internalRoot.toString(this._staticMarkup)); 384 | return; 385 | } 386 | let callbacks = this._callbacks; 387 | if (callbacks === null) { 388 | callbacks = this._callbacks = []; 389 | } 390 | callbacks.push(onCommit); 391 | }; 392 | ReactWork.prototype._onCommit = function () { 393 | if (this._didCommit) { 394 | return; 395 | } 396 | this._didCommit = true; 397 | const callbacks = this._callbacks; 398 | if (callbacks === null) { 399 | return; 400 | } 401 | // TODO: Error handling. 402 | for (let i = 0; i < callbacks.length; i++) { 403 | const callback = callbacks[i]; 404 | callback(this._internalRoot.toString(this._staticMarkup)); 405 | } 406 | }; 407 | 408 | function createRoot(options) { 409 | return new ReactRoot(options); 410 | } 411 | 412 | function renderToString(element) { 413 | return new Promise((resolve, reject) => { 414 | const root = createRoot(); 415 | const cache = (0, _react3.createCache)(); 416 | return root.render(_react2.default.createElement( 417 | _DispatcherModifier2.default, 418 | null, 419 | _react2.default.createElement( 420 | _react3.PrimaryCacheContext.Provider, 421 | { value: cache }, 422 | element 423 | ) 424 | )).then(markup => { 425 | const cacheData = cache.serialize(); 426 | const innerHTML = `window.__REACT_CACHE_DATA__ = ${cacheData};`; 427 | const markupWithCacheData = `${markup}`; 428 | resolve({ markup, markupWithCacheData, cache }); 429 | }); 430 | }); 431 | } 432 | 433 | function renderToStaticMarkup(element) { 434 | return new Promise((resolve, reject) => { 435 | const root = createRoot({ staticMarkup: true }); 436 | const cache = (0, _react3.createCache)(); 437 | return root.render(_react2.default.createElement( 438 | _DispatcherModifier2.default, 439 | null, 440 | _react2.default.createElement( 441 | _react3.PrimaryCacheContext.Provider, 442 | { value: cache }, 443 | element 444 | ) 445 | )).then(markup => { 446 | resolve({ markup, cache }); 447 | }); 448 | }); 449 | } 450 | 451 | exports.default = { 452 | renderToString, 453 | renderToStaticMarkup 454 | }; -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _SSRRenderer = require('./SSRRenderer'); 8 | 9 | Object.keys(_SSRRenderer).forEach(function (key) { 10 | if (key === "default" || key === "__esModule") return; 11 | Object.defineProperty(exports, key, { 12 | enumerable: true, 13 | get: function get() { 14 | return _SSRRenderer[key]; 15 | } 16 | }); 17 | }); -------------------------------------------------------------------------------- /dist/react-dom/src/server/DOMMarkupOperations.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.createMarkupForID = createMarkupForID; 7 | exports.createMarkupForRoot = createMarkupForRoot; 8 | exports.createMarkupForProperty = createMarkupForProperty; 9 | exports.createMarkupForCustomAttribute = createMarkupForCustomAttribute; 10 | 11 | var _DOMProperty = require('../shared/DOMProperty'); 12 | 13 | var _quoteAttributeValueForBrowser = require('./quoteAttributeValueForBrowser'); 14 | 15 | var _quoteAttributeValueForBrowser2 = _interopRequireDefault(_quoteAttributeValueForBrowser); 16 | 17 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 18 | 19 | /** 20 | * Operations for dealing with DOM properties. 21 | */ 22 | 23 | /** 24 | * Creates markup for the ID property. 25 | * 26 | * @param {string} id Unescaped ID. 27 | * @return {string} Markup string. 28 | */ 29 | /** 30 | * Copyright (c) Facebook, Inc. and its affiliates. 31 | * 32 | * This source code is licensed under the MIT license found in the 33 | * LICENSE file in the root directory of this source tree. 34 | * 35 | * 36 | */ 37 | 38 | function createMarkupForID(id) { 39 | return _DOMProperty.ID_ATTRIBUTE_NAME + '=' + (0, _quoteAttributeValueForBrowser2.default)(id); 40 | } 41 | 42 | function createMarkupForRoot() { 43 | return _DOMProperty.ROOT_ATTRIBUTE_NAME + '=""'; 44 | } 45 | 46 | /** 47 | * Creates markup for a property. 48 | * 49 | * @param {string} name 50 | * @param {*} value 51 | * @return {?string} Markup string, or null if the property was invalid. 52 | */ 53 | function createMarkupForProperty(name, value) { 54 | const propertyInfo = (0, _DOMProperty.getPropertyInfo)(name); 55 | if (name !== 'style' && (0, _DOMProperty.shouldIgnoreAttribute)(name, propertyInfo, false)) { 56 | return ''; 57 | } 58 | if ((0, _DOMProperty.shouldRemoveAttribute)(name, value, propertyInfo, false)) { 59 | return ''; 60 | } 61 | if (propertyInfo !== null) { 62 | const attributeName = propertyInfo.attributeName; 63 | const type = propertyInfo.type; 64 | 65 | if (type === _DOMProperty.BOOLEAN || type === _DOMProperty.OVERLOADED_BOOLEAN && value === true) { 66 | return attributeName + '=""'; 67 | } else { 68 | return attributeName + '=' + (0, _quoteAttributeValueForBrowser2.default)(value); 69 | } 70 | } else if ((0, _DOMProperty.isAttributeNameSafe)(name)) { 71 | return name + '=' + (0, _quoteAttributeValueForBrowser2.default)(value); 72 | } 73 | return ''; 74 | } 75 | 76 | /** 77 | * Creates markup for a custom property. 78 | * 79 | * @param {string} name 80 | * @param {*} value 81 | * @return {string} Markup string, or empty string if the property was invalid. 82 | */ 83 | function createMarkupForCustomAttribute(name, value) { 84 | if (!(0, _DOMProperty.isAttributeNameSafe)(name) || value == null) { 85 | return ''; 86 | } 87 | return name + '=' + (0, _quoteAttributeValueForBrowser2.default)(value); 88 | } -------------------------------------------------------------------------------- /dist/react-dom/src/server/escapeTextForBrowser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | /** 7 | * Copyright (c) Facebook, Inc. and its affiliates. 8 | * 9 | * This source code is licensed under the MIT license found in the 10 | * LICENSE file in the root directory of this source tree. 11 | * 12 | * Based on the escape-html library, which is used under the MIT License below: 13 | * 14 | * Copyright (c) 2012-2013 TJ Holowaychuk 15 | * Copyright (c) 2015 Andreas Lubbe 16 | * Copyright (c) 2015 Tiancheng "Timothy" Gu 17 | * 18 | * Permission is hereby granted, free of charge, to any person obtaining 19 | * a copy of this software and associated documentation files (the 20 | * 'Software'), to deal in the Software without restriction, including 21 | * without limitation the rights to use, copy, modify, merge, publish, 22 | * distribute, sublicense, and/or sell copies of the Software, and to 23 | * permit persons to whom the Software is furnished to do so, subject to 24 | * the following conditions: 25 | * 26 | * The above copyright notice and this permission notice shall be 27 | * included in all copies or substantial portions of the Software. 28 | * 29 | * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 30 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 31 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 32 | * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 33 | * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 34 | * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 35 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 36 | */ 37 | 38 | // code copied and modified from escape-html 39 | /** 40 | * Module variables. 41 | * @private 42 | */ 43 | 44 | const matchHtmlRegExp = /["'&<>]/; 45 | 46 | /** 47 | * Escapes special characters and HTML entities in a given html string. 48 | * 49 | * @param {string} string HTML string to escape for later insertion 50 | * @return {string} 51 | * @public 52 | */ 53 | 54 | function escapeHtml(string) { 55 | const str = '' + string; 56 | const match = matchHtmlRegExp.exec(str); 57 | 58 | if (!match) { 59 | return str; 60 | } 61 | 62 | let escape; 63 | let html = ''; 64 | let index; 65 | let lastIndex = 0; 66 | 67 | for (index = match.index; index < str.length; index++) { 68 | switch (str.charCodeAt(index)) { 69 | case 34: 70 | // " 71 | escape = '"'; 72 | break; 73 | case 38: 74 | // & 75 | escape = '&'; 76 | break; 77 | case 39: 78 | // ' 79 | escape = '''; // modified from escape-html; used to be ''' 80 | break; 81 | case 60: 82 | // < 83 | escape = '<'; 84 | break; 85 | case 62: 86 | // > 87 | escape = '>'; 88 | break; 89 | default: 90 | continue; 91 | } 92 | 93 | if (lastIndex !== index) { 94 | html += str.substring(lastIndex, index); 95 | } 96 | 97 | lastIndex = index + 1; 98 | html += escape; 99 | } 100 | 101 | return lastIndex !== index ? html + str.substring(lastIndex, index) : html; 102 | } 103 | // end code copied and modified from escape-html 104 | 105 | /** 106 | * Escapes text to prevent scripting attacks. 107 | * 108 | * @param {*} text Text value to escape. 109 | * @return {string} An escaped string. 110 | */ 111 | function escapeTextForBrowser(text) { 112 | if (typeof text === 'boolean' || typeof text === 'number') { 113 | // this shortcircuit helps perf for types that we know will never have 114 | // special characters, especially given that this function is used often 115 | // for numeric dom ids. 116 | return '' + text; 117 | } 118 | return escapeHtml(text); 119 | } 120 | 121 | exports.default = escapeTextForBrowser; -------------------------------------------------------------------------------- /dist/react-dom/src/server/quoteAttributeValueForBrowser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _escapeTextForBrowser = require('./escapeTextForBrowser'); 8 | 9 | var _escapeTextForBrowser2 = _interopRequireDefault(_escapeTextForBrowser); 10 | 11 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 12 | 13 | /** 14 | * Escapes attribute value to prevent scripting attacks. 15 | * 16 | * @param {*} value Value to escape. 17 | * @return {string} An escaped string. 18 | */ 19 | function quoteAttributeValueForBrowser(value) { 20 | return '"' + (0, _escapeTextForBrowser2.default)(value) + '"'; 21 | } /** 22 | * Copyright (c) Facebook, Inc. and its affiliates. 23 | * 24 | * This source code is licensed under the MIT license found in the 25 | * LICENSE file in the root directory of this source tree. 26 | */ 27 | 28 | exports.default = quoteAttributeValueForBrowser; -------------------------------------------------------------------------------- /dist/react-dom/src/shared/CSSProperty.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | /** 7 | * Copyright (c) Facebook, Inc. and its affiliates. 8 | * 9 | * This source code is licensed under the MIT license found in the 10 | * LICENSE file in the root directory of this source tree. 11 | */ 12 | 13 | /** 14 | * CSS properties which accept numbers but are not in units of "px". 15 | */ 16 | const isUnitlessNumber = exports.isUnitlessNumber = { 17 | animationIterationCount: true, 18 | borderImageOutset: true, 19 | borderImageSlice: true, 20 | borderImageWidth: true, 21 | boxFlex: true, 22 | boxFlexGroup: true, 23 | boxOrdinalGroup: true, 24 | columnCount: true, 25 | columns: true, 26 | flex: true, 27 | flexGrow: true, 28 | flexPositive: true, 29 | flexShrink: true, 30 | flexNegative: true, 31 | flexOrder: true, 32 | gridArea: true, 33 | gridRow: true, 34 | gridRowEnd: true, 35 | gridRowSpan: true, 36 | gridRowStart: true, 37 | gridColumn: true, 38 | gridColumnEnd: true, 39 | gridColumnSpan: true, 40 | gridColumnStart: true, 41 | fontWeight: true, 42 | lineClamp: true, 43 | lineHeight: true, 44 | opacity: true, 45 | order: true, 46 | orphans: true, 47 | tabSize: true, 48 | widows: true, 49 | zIndex: true, 50 | zoom: true, 51 | 52 | // SVG-related properties 53 | fillOpacity: true, 54 | floodOpacity: true, 55 | stopOpacity: true, 56 | strokeDasharray: true, 57 | strokeDashoffset: true, 58 | strokeMiterlimit: true, 59 | strokeOpacity: true, 60 | strokeWidth: true 61 | }; 62 | 63 | /** 64 | * @param {string} prefix vendor-specific prefix, eg: Webkit 65 | * @param {string} key style name, eg: transitionDuration 66 | * @return {string} style name prefixed with `prefix`, properly camelCased, eg: 67 | * WebkitTransitionDuration 68 | */ 69 | function prefixKey(prefix, key) { 70 | return prefix + key.charAt(0).toUpperCase() + key.substring(1); 71 | } 72 | 73 | /** 74 | * Support style names that may come passed in prefixed by adding permutations 75 | * of vendor prefixes. 76 | */ 77 | const prefixes = ['Webkit', 'ms', 'Moz', 'O']; 78 | 79 | // Using Object.keys here, or else the vanilla for-in loop makes IE8 go into an 80 | // infinite loop, because it iterates over the newly added props too. 81 | Object.keys(isUnitlessNumber).forEach(function (prop) { 82 | prefixes.forEach(function (prefix) { 83 | isUnitlessNumber[prefixKey(prefix, prop)] = isUnitlessNumber[prop]; 84 | }); 85 | }); -------------------------------------------------------------------------------- /dist/react-dom/src/shared/DOMProperty.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.isAttributeNameSafe = isAttributeNameSafe; 7 | exports.shouldIgnoreAttribute = shouldIgnoreAttribute; 8 | exports.shouldRemoveAttributeWithWarning = shouldRemoveAttributeWithWarning; 9 | exports.shouldRemoveAttribute = shouldRemoveAttribute; 10 | exports.getPropertyInfo = getPropertyInfo; 11 | /** 12 | * Copyright (c) Facebook, Inc. and its affiliates. 13 | * 14 | * This source code is licensed under the MIT license found in the 15 | * LICENSE file in the root directory of this source tree. 16 | * 17 | * 18 | */ 19 | 20 | const warning = () => {}; 21 | 22 | // A reserved attribute. 23 | // It is handled by React separately and shouldn't be written to the DOM. 24 | const RESERVED = exports.RESERVED = 0; 25 | 26 | // A simple string attribute. 27 | // Attributes that aren't in the whitelist are presumed to have this type. 28 | const STRING = exports.STRING = 1; 29 | 30 | // A string attribute that accepts booleans in React. In HTML, these are called 31 | // "enumerated" attributes with "true" and "false" as possible values. 32 | // When true, it should be set to a "true" string. 33 | // When false, it should be set to a "false" string. 34 | const BOOLEANISH_STRING = exports.BOOLEANISH_STRING = 2; 35 | 36 | // A real boolean attribute. 37 | // When true, it should be present (set either to an empty string or its name). 38 | // When false, it should be omitted. 39 | const BOOLEAN = exports.BOOLEAN = 3; 40 | 41 | // An attribute that can be used as a flag as well as with a value. 42 | // When true, it should be present (set either to an empty string or its name). 43 | // When false, it should be omitted. 44 | // For any other value, should be present with that value. 45 | const OVERLOADED_BOOLEAN = exports.OVERLOADED_BOOLEAN = 4; 46 | 47 | // An attribute that must be numeric or parse as a numeric. 48 | // When falsy, it should be removed. 49 | const NUMERIC = exports.NUMERIC = 5; 50 | 51 | // An attribute that must be positive numeric or parse as a positive numeric. 52 | // When falsy, it should be removed. 53 | const POSITIVE_NUMERIC = exports.POSITIVE_NUMERIC = 6; 54 | 55 | /* eslint-disable max-len */ 56 | const ATTRIBUTE_NAME_START_CHAR = exports.ATTRIBUTE_NAME_START_CHAR = ':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'; 57 | /* eslint-enable max-len */ 58 | const ATTRIBUTE_NAME_CHAR = exports.ATTRIBUTE_NAME_CHAR = ATTRIBUTE_NAME_START_CHAR + '\\-.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040'; 59 | 60 | const ID_ATTRIBUTE_NAME = exports.ID_ATTRIBUTE_NAME = 'data-reactid'; 61 | const ROOT_ATTRIBUTE_NAME = exports.ROOT_ATTRIBUTE_NAME = 'data-reactroot'; 62 | const VALID_ATTRIBUTE_NAME_REGEX = exports.VALID_ATTRIBUTE_NAME_REGEX = new RegExp('^[' + ATTRIBUTE_NAME_START_CHAR + '][' + ATTRIBUTE_NAME_CHAR + ']*$'); 63 | 64 | const hasOwnProperty = Object.prototype.hasOwnProperty; 65 | const illegalAttributeNameCache = {}; 66 | const validatedAttributeNameCache = {}; 67 | 68 | function isAttributeNameSafe(attributeName) { 69 | if (hasOwnProperty.call(validatedAttributeNameCache, attributeName)) { 70 | return true; 71 | } 72 | if (hasOwnProperty.call(illegalAttributeNameCache, attributeName)) { 73 | return false; 74 | } 75 | if (VALID_ATTRIBUTE_NAME_REGEX.test(attributeName)) { 76 | validatedAttributeNameCache[attributeName] = true; 77 | return true; 78 | } 79 | illegalAttributeNameCache[attributeName] = true; 80 | if (__DEV__) { 81 | warning(false, 'Invalid attribute name: `%s`', attributeName); 82 | } 83 | return false; 84 | } 85 | 86 | function shouldIgnoreAttribute(name, propertyInfo, isCustomComponentTag) { 87 | if (propertyInfo !== null) { 88 | return propertyInfo.type === RESERVED; 89 | } 90 | if (isCustomComponentTag) { 91 | return false; 92 | } 93 | if (name.length > 2 && (name[0] === 'o' || name[0] === 'O') && (name[1] === 'n' || name[1] === 'N')) { 94 | return true; 95 | } 96 | return false; 97 | } 98 | 99 | function shouldRemoveAttributeWithWarning(name, value, propertyInfo, isCustomComponentTag) { 100 | if (propertyInfo !== null && propertyInfo.type === RESERVED) { 101 | return false; 102 | } 103 | switch (typeof value) { 104 | case 'function': 105 | // $FlowIssue symbol is perfectly valid here 106 | case 'symbol': 107 | // eslint-disable-line 108 | return true; 109 | case 'boolean': 110 | { 111 | if (isCustomComponentTag) { 112 | return false; 113 | } 114 | if (propertyInfo !== null) { 115 | return !propertyInfo.acceptsBooleans; 116 | } else { 117 | const prefix = name.toLowerCase().slice(0, 5); 118 | return prefix !== 'data-' && prefix !== 'aria-'; 119 | } 120 | } 121 | default: 122 | return false; 123 | } 124 | } 125 | 126 | function shouldRemoveAttribute(name, value, propertyInfo, isCustomComponentTag) { 127 | if (value === null || typeof value === 'undefined') { 128 | return true; 129 | } 130 | if (shouldRemoveAttributeWithWarning(name, value, propertyInfo, isCustomComponentTag)) { 131 | return true; 132 | } 133 | if (isCustomComponentTag) { 134 | return false; 135 | } 136 | if (propertyInfo !== null) { 137 | switch (propertyInfo.type) { 138 | case BOOLEAN: 139 | return !value; 140 | case OVERLOADED_BOOLEAN: 141 | return value === false; 142 | case NUMERIC: 143 | return isNaN(value); 144 | case POSITIVE_NUMERIC: 145 | return isNaN(value) || value < 1; 146 | } 147 | } 148 | return false; 149 | } 150 | 151 | function getPropertyInfo(name) { 152 | return properties.hasOwnProperty(name) ? properties[name] : null; 153 | } 154 | 155 | function PropertyInfoRecord(name, type, mustUseProperty, attributeName, attributeNamespace) { 156 | this.acceptsBooleans = type === BOOLEANISH_STRING || type === BOOLEAN || type === OVERLOADED_BOOLEAN; 157 | this.attributeName = attributeName; 158 | this.attributeNamespace = attributeNamespace; 159 | this.mustUseProperty = mustUseProperty; 160 | this.propertyName = name; 161 | this.type = type; 162 | } 163 | 164 | // When adding attributes to this list, be sure to also add them to 165 | // the `possibleStandardNames` module to ensure casing and incorrect 166 | // name warnings. 167 | const properties = {}; 168 | 169 | // These props are reserved by React. They shouldn't be written to the DOM. 170 | ['children', 'dangerouslySetInnerHTML', 171 | // TODO: This prevents the assignment of defaultValue to regular 172 | // elements (not just inputs). Now that ReactDOMInput assigns to the 173 | // defaultValue property -- do we need this? 174 | 'defaultValue', 'defaultChecked', 'innerHTML', 'suppressContentEditableWarning', 'suppressHydrationWarning', 'style'].forEach(name => { 175 | properties[name] = new PropertyInfoRecord(name, RESERVED, false, // mustUseProperty 176 | name, // attributeName 177 | null // attributeNamespace 178 | ); 179 | }); 180 | 181 | // A few React string attributes have a different name. 182 | // This is a mapping from React prop names to the attribute names. 183 | [['acceptCharset', 'accept-charset'], ['className', 'class'], ['htmlFor', 'for'], ['httpEquiv', 'http-equiv']].forEach(([name, attributeName]) => { 184 | properties[name] = new PropertyInfoRecord(name, STRING, false, // mustUseProperty 185 | attributeName, // attributeName 186 | null // attributeNamespace 187 | ); 188 | }); 189 | 190 | // These are "enumerated" HTML attributes that accept "true" and "false". 191 | // In React, we let users pass `true` and `false` even though technically 192 | // these aren't boolean attributes (they are coerced to strings). 193 | ['contentEditable', 'draggable', 'spellCheck', 'value'].forEach(name => { 194 | properties[name] = new PropertyInfoRecord(name, BOOLEANISH_STRING, false, // mustUseProperty 195 | name.toLowerCase(), // attributeName 196 | null // attributeNamespace 197 | ); 198 | }); 199 | 200 | // These are "enumerated" SVG attributes that accept "true" and "false". 201 | // In React, we let users pass `true` and `false` even though technically 202 | // these aren't boolean attributes (they are coerced to strings). 203 | // Since these are SVG attributes, their attribute names are case-sensitive. 204 | ['autoReverse', 'externalResourcesRequired', 'focusable', 'preserveAlpha'].forEach(name => { 205 | properties[name] = new PropertyInfoRecord(name, BOOLEANISH_STRING, false, // mustUseProperty 206 | name, // attributeName 207 | null // attributeNamespace 208 | ); 209 | }); 210 | 211 | // These are HTML boolean attributes. 212 | ['allowFullScreen', 'async', 213 | // Note: there is a special case that prevents it from being written to the DOM 214 | // on the client side because the browsers are inconsistent. Instead we call focus(). 215 | 'autoFocus', 'autoPlay', 'controls', 'default', 'defer', 'disabled', 'formNoValidate', 'hidden', 'loop', 'noModule', 'noValidate', 'open', 'playsInline', 'readOnly', 'required', 'reversed', 'scoped', 'seamless', 216 | // Microdata 217 | 'itemScope'].forEach(name => { 218 | properties[name] = new PropertyInfoRecord(name, BOOLEAN, false, // mustUseProperty 219 | name.toLowerCase(), // attributeName 220 | null // attributeNamespace 221 | ); 222 | }); 223 | 224 | // These are the few React props that we set as DOM properties 225 | // rather than attributes. These are all booleans. 226 | ['checked', 227 | // Note: `option.selected` is not updated if `select.multiple` is 228 | // disabled with `removeAttribute`. We have special logic for handling this. 229 | 'multiple', 'muted', 'selected' 230 | 231 | // NOTE: if you add a camelCased prop to this list, 232 | // you'll need to set attributeName to name.toLowerCase() 233 | // instead in the assignment below. 234 | ].forEach(name => { 235 | properties[name] = new PropertyInfoRecord(name, BOOLEAN, true, // mustUseProperty 236 | name, // attributeName 237 | null // attributeNamespace 238 | ); 239 | }); 240 | 241 | // These are HTML attributes that are "overloaded booleans": they behave like 242 | // booleans, but can also accept a string value. 243 | ['capture', 'download' 244 | 245 | // NOTE: if you add a camelCased prop to this list, 246 | // you'll need to set attributeName to name.toLowerCase() 247 | // instead in the assignment below. 248 | ].forEach(name => { 249 | properties[name] = new PropertyInfoRecord(name, OVERLOADED_BOOLEAN, false, // mustUseProperty 250 | name, // attributeName 251 | null // attributeNamespace 252 | ); 253 | }); 254 | 255 | // These are HTML attributes that must be positive numbers. 256 | ['cols', 'rows', 'size', 'span' 257 | 258 | // NOTE: if you add a camelCased prop to this list, 259 | // you'll need to set attributeName to name.toLowerCase() 260 | // instead in the assignment below. 261 | ].forEach(name => { 262 | properties[name] = new PropertyInfoRecord(name, POSITIVE_NUMERIC, false, // mustUseProperty 263 | name, // attributeName 264 | null // attributeNamespace 265 | ); 266 | }); 267 | 268 | // These are HTML attributes that must be numbers. 269 | ['rowSpan', 'start'].forEach(name => { 270 | properties[name] = new PropertyInfoRecord(name, NUMERIC, false, // mustUseProperty 271 | name.toLowerCase(), // attributeName 272 | null // attributeNamespace 273 | ); 274 | }); 275 | 276 | const CAMELIZE = /[\-\:]([a-z])/g; 277 | const capitalize = token => token[1].toUpperCase(); 278 | 279 | // This is a list of all SVG attributes that need special casing, namespacing, 280 | // or boolean value assignment. Regular attributes that just accept strings 281 | // and have the same names are omitted, just like in the HTML whitelist. 282 | // Some of these attributes can be hard to find. This list was created by 283 | // scrapping the MDN documentation. 284 | ['accent-height', 'alignment-baseline', 'arabic-form', 'baseline-shift', 'cap-height', 'clip-path', 'clip-rule', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'dominant-baseline', 'enable-background', 'fill-opacity', 'fill-rule', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'glyph-name', 'glyph-orientation-horizontal', 'glyph-orientation-vertical', 'horiz-adv-x', 'horiz-origin-x', 'image-rendering', 'letter-spacing', 'lighting-color', 'marker-end', 'marker-mid', 'marker-start', 'overline-position', 'overline-thickness', 'paint-order', 'panose-1', 'pointer-events', 'rendering-intent', 'shape-rendering', 'stop-color', 'stop-opacity', 'strikethrough-position', 'strikethrough-thickness', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'text-anchor', 'text-decoration', 'text-rendering', 'underline-position', 'underline-thickness', 'unicode-bidi', 'unicode-range', 'units-per-em', 'v-alphabetic', 'v-hanging', 'v-ideographic', 'v-mathematical', 'vector-effect', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'word-spacing', 'writing-mode', 'xmlns:xlink', 'x-height' 285 | 286 | // NOTE: if you add a camelCased prop to this list, 287 | // you'll need to set attributeName to name.toLowerCase() 288 | // instead in the assignment below. 289 | ].forEach(attributeName => { 290 | const name = attributeName.replace(CAMELIZE, capitalize); 291 | properties[name] = new PropertyInfoRecord(name, STRING, false, // mustUseProperty 292 | attributeName, null // attributeNamespace 293 | ); 294 | }); 295 | 296 | // String SVG attributes with the xlink namespace. 297 | ['xlink:actuate', 'xlink:arcrole', 'xlink:href', 'xlink:role', 'xlink:show', 'xlink:title', 'xlink:type' 298 | 299 | // NOTE: if you add a camelCased prop to this list, 300 | // you'll need to set attributeName to name.toLowerCase() 301 | // instead in the assignment below. 302 | ].forEach(attributeName => { 303 | const name = attributeName.replace(CAMELIZE, capitalize); 304 | properties[name] = new PropertyInfoRecord(name, STRING, false, // mustUseProperty 305 | attributeName, 'http://www.w3.org/1999/xlink'); 306 | }); 307 | 308 | // String SVG attributes with the xml namespace. 309 | ['xml:base', 'xml:lang', 'xml:space' 310 | 311 | // NOTE: if you add a camelCased prop to this list, 312 | // you'll need to set attributeName to name.toLowerCase() 313 | // instead in the assignment below. 314 | ].forEach(attributeName => { 315 | const name = attributeName.replace(CAMELIZE, capitalize); 316 | properties[name] = new PropertyInfoRecord(name, STRING, false, // mustUseProperty 317 | attributeName, 'http://www.w3.org/XML/1998/namespace'); 318 | }); 319 | 320 | // Special case: this attribute exists both in HTML and SVG. 321 | // Its "tabindex" attribute name is case-sensitive in SVG so we can't just use 322 | // its React `tabIndex` name, like we do for attributes that exist only in HTML. 323 | properties.tabIndex = new PropertyInfoRecord('tabIndex', STRING, false, // mustUseProperty 324 | 'tabindex', // attributeName 325 | null // attributeNamespace 326 | ); -------------------------------------------------------------------------------- /dist/react-dom/src/shared/dangerousStyleValue.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _CSSProperty = require('./CSSProperty'); 8 | 9 | /** 10 | * Convert a value into the proper css writable value. The style name `name` 11 | * should be logical (no hyphens), as specified 12 | * in `CSSProperty.isUnitlessNumber`. 13 | * 14 | * @param {string} name CSS property name such as `topMargin`. 15 | * @param {*} value CSS property value such as `10px`. 16 | * @return {string} Normalized style value with dimensions applied. 17 | */ 18 | function dangerousStyleValue(name, value, isCustomProperty) { 19 | // Note that we've removed escapeTextForBrowser() calls here since the 20 | // whole string will be escaped when the attribute is injected into 21 | // the markup. If you provide unsafe user data here they can inject 22 | // arbitrary CSS which may be problematic (I couldn't repro this): 23 | // https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet 24 | // http://www.thespanner.co.uk/2007/11/26/ultimate-xss-css-injection/ 25 | // This is not an XSS hole but instead a potential CSS injection issue 26 | // which has lead to a greater discussion about how we're going to 27 | // trust URLs moving forward. See #2115901 28 | 29 | const isEmpty = value == null || typeof value === 'boolean' || value === ''; 30 | if (isEmpty) { 31 | return ''; 32 | } 33 | 34 | if (!isCustomProperty && typeof value === 'number' && value !== 0 && !(_CSSProperty.isUnitlessNumber.hasOwnProperty(name) && _CSSProperty.isUnitlessNumber[name])) { 35 | return value + 'px'; // Presumes implicit 'px' suffix for unitless numbers 36 | } 37 | 38 | return ('' + value).trim(); 39 | } /** 40 | * Copyright (c) Facebook, Inc. and its affiliates. 41 | * 42 | * This source code is licensed under the MIT license found in the 43 | * LICENSE file in the root directory of this source tree. 44 | */ 45 | 46 | exports.default = dangerousStyleValue; -------------------------------------------------------------------------------- /dist/react-dom/src/shared/isCustomComponent.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | /** 7 | * Copyright (c) Facebook, Inc. and its affiliates. 8 | * 9 | * This source code is licensed under the MIT license found in the 10 | * LICENSE file in the root directory of this source tree. 11 | * 12 | * 13 | */ 14 | 15 | function isCustomComponent(tagName, props) { 16 | if (tagName.indexOf('-') === -1) { 17 | return typeof props.is === 'string'; 18 | } 19 | switch (tagName) { 20 | // These are reserved SVG and MathML elements. 21 | // We don't mind this whitelist too much because we expect it to never grow. 22 | // The alternative is to track the namespace in a few places which is convoluted. 23 | // https://w3c.github.io/webcomponents/spec/custom/#custom-elements-core-concepts 24 | case 'annotation-xml': 25 | case 'color-profile': 26 | case 'font-face': 27 | case 'font-face-src': 28 | case 'font-face-uri': 29 | case 'font-face-format': 30 | case 'font-face-name': 31 | case 'missing-glyph': 32 | return false; 33 | default: 34 | return true; 35 | } 36 | } 37 | 38 | exports.default = isCustomComponent; -------------------------------------------------------------------------------- /dist/react-dom/src/shared/omittedCloseTags.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | /** 7 | * Copyright (c) Facebook, Inc. and its affiliates. 8 | * 9 | * This source code is licensed under the MIT license found in the 10 | * LICENSE file in the root directory of this source tree. 11 | */ 12 | 13 | // For HTML, certain tags should omit their close tag. We keep a whitelist for 14 | // those special-case tags. 15 | 16 | const omittedCloseTags = { 17 | area: true, 18 | base: true, 19 | br: true, 20 | col: true, 21 | embed: true, 22 | hr: true, 23 | img: true, 24 | input: true, 25 | keygen: true, 26 | link: true, 27 | meta: true, 28 | param: true, 29 | source: true, 30 | track: true, 31 | wbr: true 32 | // NOTE: menuitem's close tag should be omitted, but that causes problems. 33 | }; 34 | 35 | exports.default = omittedCloseTags; -------------------------------------------------------------------------------- /dist/reactMonkeyPatch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _react = require('react'); 4 | 5 | var _react2 = _interopRequireDefault(_react); 6 | 7 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 8 | 9 | const noopLifecycles = ['componentDidMount', 'shouldComponentUpdate', 'getSnapshotBeforeUpdate', 'componentDidUpdate', 'componentWillUpdate', 'UNSAFE_componentWillUpdate', 'componentWillReceiveProps', 'UNSAFE_componentWillReceiveProps', 'componentWillUnmount', 'componentDidCatch']; 10 | 11 | const noopStaticLifecycles = ['getDerivedStateFromProps', 'getDerivedStateFromError']; 12 | 13 | const noop = () => {}; 14 | 15 | // Component 16 | const oldComponent = _react2.default.Component; 17 | const oldPrototype = _react2.default.Component.prototype; 18 | 19 | const newComp = function Component(props, context, updater) { 20 | noopLifecycles.forEach(lifecycleName => { 21 | if (this[lifecycleName]) { 22 | this[lifecycleName] = noop; 23 | } 24 | }); 25 | noopStaticLifecycles.forEach(lifecycleName => { 26 | if (this.constructor[lifecycleName]) { 27 | delete this.constructor[lifecycleName]; 28 | } 29 | }); 30 | 31 | oldComponent.call(this, props, context, updater); 32 | }; 33 | 34 | _react2.default.Component = newComp; 35 | _react2.default.Component.prototype = oldPrototype; 36 | 37 | // PureComponent 38 | const oldPureComponent = _react2.default.PureComponent; 39 | const oldPurePrototype = _react2.default.PureComponent.prototype; 40 | 41 | const newPureComp = function PureComponent(props, context, updater) { 42 | noopLifecycles.forEach(lifecycleName => { 43 | if (this[lifecycleName]) { 44 | this[lifecycleName] = noop; 45 | } 46 | }); 47 | noopStaticLifecycles.forEach(lifecycleName => { 48 | if (this.constructor[lifecycleName]) { 49 | delete this.constructor[lifecycleName]; 50 | } 51 | }); 52 | 53 | oldPureComponent.call(this, props, context, updater); 54 | }; 55 | 56 | _react2.default.PureComponent = newPureComp; 57 | _react2.default.PureComponent.prototype = oldPurePrototype; -------------------------------------------------------------------------------- /dist/reactUtils/createMarkupForStyles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = createMarkupForStyles; 7 | 8 | var _memoizeStringOnly = require('fbjs/lib/memoizeStringOnly'); 9 | 10 | var _memoizeStringOnly2 = _interopRequireDefault(_memoizeStringOnly); 11 | 12 | var _hyphenateStyleName = require('fbjs/lib/hyphenateStyleName'); 13 | 14 | var _hyphenateStyleName2 = _interopRequireDefault(_hyphenateStyleName); 15 | 16 | var _dangerousStyleValue = require('../react-dom/src/shared/dangerousStyleValue'); 17 | 18 | var _dangerousStyleValue2 = _interopRequireDefault(_dangerousStyleValue); 19 | 20 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 21 | 22 | const processStyleName = (0, _memoizeStringOnly2.default)(function (styleName) { 23 | return (0, _hyphenateStyleName2.default)(styleName); 24 | }); /** 25 | * Copyright (c) 2018-present, Fredrik Höglund 26 | * 27 | * This source code is licensed under the MIT license found in the 28 | * LICENSE file in the root directory of this source tree. 29 | * 30 | * This file is very heavily based on code from the React-project, 31 | * used under the MIT License below: 32 | * 33 | * Copyright (c) 2013-2018, Facebook, Inc. 34 | * 35 | * Permission is hereby granted, free of charge, to any person obtaining a copy 36 | * of this software and associated documentation files (the "Software"), to deal 37 | * in the Software without restriction, including without limitation the rights 38 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 39 | * copies of the Software, and to permit persons to whom the Software is 40 | * furnished to do so, subject to the following conditions: 41 | 42 | * The above copyright notice and this permission notice shall be included in all 43 | * copies or substantial portions of the Software. 44 | 45 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 46 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 47 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 48 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 49 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 50 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 51 | * SOFTWARE. 52 | */ 53 | 54 | function createMarkupForStyles(styles) { 55 | let serialized = ''; 56 | let delimiter = ''; 57 | for (const styleName in styles) { 58 | if (!styles.hasOwnProperty(styleName)) { 59 | continue; 60 | } 61 | const isCustomProperty = styleName.indexOf('--') === 0; 62 | const styleValue = styles[styleName]; 63 | 64 | if (styleValue != null) { 65 | serialized += delimiter + processStyleName(styleName) + ':'; 66 | serialized += (0, _dangerousStyleValue2.default)(styleName, styleValue, isCustomProperty); 67 | 68 | delimiter = ';'; 69 | } 70 | } 71 | return serialized || null; 72 | } -------------------------------------------------------------------------------- /examples/basic/index.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | require('./server'); 3 | -------------------------------------------------------------------------------- /examples/basic/server.js: -------------------------------------------------------------------------------- 1 | import 'isomorphic-fetch'; 2 | 3 | import path from 'path'; 4 | 5 | import React from 'react'; 6 | import express from 'express'; 7 | 8 | import { renderToString } from '../../src/renderer'; 9 | import { App } from './src'; 10 | 11 | const app = express(); 12 | const port = 3000; 13 | 14 | const createHtml = markup => ` 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
${markup}
26 | 27 | 28 | 29 | 30 | `; 31 | 32 | app.use(express.static(path.join(__dirname, 'dist'))); 33 | 34 | const colors = { 1: 'Red', 2: 'Green', 3: 'Blue' }; 35 | 36 | app.get('/api/colors/:colorId', (req, res) => { 37 | res.send(colors[req.params.colorId]); 38 | }); 39 | 40 | app.get('/', async (req, res) => { 41 | const { markup, markupWithCacheData, cache } = await renderToString( 42 | 43 | ); 44 | res.send(createHtml(markupWithCacheData)); 45 | }); 46 | 47 | app.listen(port, () => 48 | console.log(`Basic example app listening on port ${port}!`) 49 | ); 50 | -------------------------------------------------------------------------------- /examples/basic/src/index.js: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from 'react'; 2 | import { hydrate, createResource, useReadResource } from '../../../src/react'; 3 | 4 | const colorResource = createResource('colorResource', colorId => 5 | fetch(`http://localhost:3000/api/colors/${colorId}`).then(res => res.text()) 6 | ); 7 | 8 | function Color({ colorId }) { 9 | const colorName = useReadResource(colorResource, colorId); 10 | 11 | return

This is a color: {colorName}

; 12 | } 13 | 14 | function App() { 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | 24 | if (typeof window !== 'undefined') { 25 | hydrate(, document.getElementById('react-app')); 26 | } 27 | 28 | module.exports = { 29 | App 30 | }; 31 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setupTestFrameworkScriptFile: require.resolve('./test/setup-tests.js') 3 | }; 4 | -------------------------------------------------------------------------------- /manualtest.js: -------------------------------------------------------------------------------- 1 | import React, { Suspense, useState, useEffect } from 'react'; 2 | import { renderToString } from './src/renderer/SSRRenderer'; 3 | import { createResource, createCache } from './src/react/cache'; 4 | 5 | const cache = createCache(); 6 | 7 | const mockResource = createResource('mock', () => { 8 | return new Promise(resolve => { 9 | setTimeout(() => resolve('Text2'), 3000); 10 | }); 11 | }); 12 | 13 | const Inner = () => { 14 | const text = mockResource.read(cache); 15 | return
{text}
; 16 | }; 17 | 18 | const FirstInner = () => { 19 | const [state, setState] = useState('Loading...'); 20 | 21 | // Effect should not be fired 22 | useEffect(() => console.log('Effect!')); 23 | 24 | return ( 25 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | class App extends React.Component { 32 | componentDidMount() { 33 | // componentDidMount should not be called 34 | console.log('Mount'); 35 | } 36 | render() { 37 | return ( 38 |
39 | 40 |
41 | ); 42 | } 43 | } 44 | 45 | function render() { 46 | let startTime = Date.now(); 47 | console.log('Start'); 48 | renderToString().then(({ markup }) => { 49 | console.log('Render took', Date.now() - startTime); 50 | console.log('HTML', markup); 51 | }); 52 | } 53 | 54 | render(); 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-aldrin", 3 | "version": "0.1.1", 4 | "description": "An experimental React Suspense serverside renderer", 5 | "author": "Fredrik Höglund", 6 | "license": "MIT", 7 | "repository": "github:Ephem/react-aldrin", 8 | "homepage": "https://github.com/Ephem/react-aldrin#readme", 9 | "bugs": "https://github.com/Ephem/react-aldrin/issues", 10 | "main": "dist/index.js", 11 | "engines": { 12 | "node": ">=6.0.0" 13 | }, 14 | "files": ["dist/", "react.js"], 15 | "scripts": { 16 | "build": 17 | "npm run build:babel && npm run build:rollup && npm run examples:basic:build", 18 | "build:babel": 19 | "rimraf dist && babel src/renderer --out-dir dist --ignore __tests__", 20 | "build:rollup": "rimraf react.js && rollup -c", 21 | "build:babel:watch": "npm run build:babel -- --watch", 22 | "build:rollup:watch": "npm run build:rollup -- --watch", 23 | "test": "jest", 24 | "test:watch": "jest --watch", 25 | "test:manual": "babel-node manualtest.js", 26 | "examples:basic:build": 27 | "cd examples/basic && webpack --mode production --module-bind js=babel-loader && cd ../..", 28 | "examples:basic:start": "node examples/basic" 29 | }, 30 | "dependencies": { 31 | "fbjs": "0.8.16", 32 | "raf": "^3.4.0", 33 | "react-reconciler": "0.18.0-alpha.2" 34 | }, 35 | "peerDependencies": { 36 | "react": "16.7.0-alpha.2", 37 | "react-dom": "16.7.0-alpha.2" 38 | }, 39 | "devDependencies": { 40 | "babel-cli": "^6.26.0", 41 | "babel-core": "^6.26.3", 42 | "babel-loader": "^7.1.5", 43 | "babel-plugin-external-helpers": "^6.22.0", 44 | "babel-plugin-transform-class-properties": "^6.24.1", 45 | "babel-plugin-transform-flow-strip-types": "^6.22.0", 46 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 47 | "babel-preset-env": "^1.7.0", 48 | "babel-preset-react": "^6.24.1", 49 | "babel-register": "^6.26.0", 50 | "express": "^4.16.4", 51 | "isomorphic-fetch": "^2.2.1", 52 | "jest": "^22.4.3", 53 | "jest-dom": "^3.0.0", 54 | "nodemon": "^1.17.3", 55 | "prettier": "^1.12.1", 56 | "prettier-cli": "^0.1.0", 57 | "react-testing-library": "^5.3.2", 58 | "react": "16.7.0-alpha.2", 59 | "react-dom": "16.7.0-alpha.2", 60 | "rimraf": "^2.6.2", 61 | "rollup": "^0.58.2", 62 | "rollup-plugin-babel": "^3.0.4", 63 | "rollup-plugin-node-resolve": "^3.3.0", 64 | "rollup-plugin-uglify": "^3.0.0", 65 | "webpack": "^4.27.1", 66 | "webpack-cli": "^3.1.2" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /react.js: -------------------------------------------------------------------------------- 1 | !function(e,r){"object"==typeof exports&&"undefined"!=typeof module?r(exports,require("react"),require("react-dom")):"function"==typeof define&&define.amd?define(["exports","react","react-dom"],r):r(e.SSRRenderer={},e.React,e.ReactDOM)}(this,function(e,r,t){"use strict";var n="default"in r?r.default:r;t=t&&t.hasOwnProperty("default")?t.default:t;var a={"<":"\\u003C",">":"\\u003E","/":"\\u002F","\u2028":"\\u2028","\u2029":"\\u2029"};var u=0,o=1,i=2,c=3;function d(e,r){var t=e||{};function n(e,r){var n=t[e];if(void 0!==n){var a=n[r];if(void 0!==a)return a}else n={},t[e]=n;var o={status:u,suspender:null,value:null,error:null};return n[r]=o,o}function d(e,r){var t=e;t.status=o,t.suspender=r,r.then(function(e){var r=t;r.status=i,r.suspender=null,r.value=e},function(e){var r=t;r.status=c,r.suspender=null,r.error=e})}return{invalidate:function(){r()},preload:function(e,r,t,a){var s=n(e.name,r);switch(s.status){case u:return void d(s,t(a));case o:case i:case c:return}},get:function(e,r,t,a){var d=n(e.name,r);switch(d.status){case u:return;case o:return d.suspender;case i:return d.value;case c:default:throw d.error}},read:function(e,r,t,a){var s=n(e.name,r);switch(s.status){case u:var f=t(a);throw d(s,f),f;case o:throw s.suspender;case i:return s.value;case c:default:throw s.error}},serialize:function(){return function(e,r){return JSON.stringify(e,r).replace(/[<>\/\u2028\u2029]/g,function(e){return a[e]})}(t,function(e,r){if("suspender"!==e)return r})}}}var s=r.createContext(d());e.createCache=d,e.createResource=function(e,r,t){var n={name:e,get:function(e){var a=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"NO_KEY";if(void 0===t)return e.get(n,a,r,a);var u=t(a);return e.get(n,u,r,a)},read:function(e){var a=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"NO_KEY";if(void 0===t)return e.read(n,a,r,a);var u=t(a);return e.read(n,u,r,a)},preload:function(e){var a=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"NO_KEY";if(void 0!==t){var u=t(a);e.preload(n,u,r,a)}else e.preload(n,a,r,a)}};return n},e.PrimaryCacheContext=s,e.useReadResource=function(e,t){var n=r.useContext(s);return e.read(n,t)},e.render=function(e){for(var r=d(),a=arguments.length,u=Array(a>1?a-1:0),o=1;o1?u-1:0),i=1;i { 11 | let calls = { count: 0 }; 12 | return { 13 | calls, 14 | resource: createResource('test-resource', () => { 15 | calls.count += 1; 16 | return new Promise(resolve => { 17 | setTimeout(() => resolve(value), timeout); 18 | }); 19 | }) 20 | }; 21 | }; 22 | 23 | const CacheFromProp = ({ cache, resource }) => { 24 | const text = resource.read(cache); 25 | return text; 26 | }; 27 | 28 | const CacheFromContext = ({ resource }) => { 29 | const cache = useContext(PrimaryCacheContext); 30 | const text = resource.read(cache); 31 | return text; 32 | }; 33 | 34 | const SuspenseApp = ({ timeout = 5000, children }) => { 35 | return ( 36 |
37 | 38 | {children} 39 | 40 |
41 | ); 42 | }; 43 | 44 | describe('SSRRenderer resources', () => { 45 | it('renders with async resource', async () => { 46 | const cache = createCache(); 47 | const { resource } = getResource(); 48 | 49 | const { markup } = await renderToString( 50 | 51 | 52 | 53 | ); 54 | expect(markup).toBe('
Async resource
'); 55 | }); 56 | it('renders with fallback if timed out', async () => { 57 | const cache = createCache(); 58 | const { resource } = getResource(10000); 59 | 60 | const { markup } = await renderToString( 61 | 62 | 63 | 64 | ); 65 | expect(markup).toBe('
Loading...
'); 66 | }); 67 | 68 | it('can use a context to store the cache in order to serialize and later rehydrate it', async () => { 69 | // Server rendering part of test 70 | const expectedHtml = '
Async resource
'; 71 | const { resource, calls } = getResource(); 72 | 73 | const { markup, cache } = await renderToString( 74 | 75 | 76 | 77 | ); 78 | expect(markup).toBe(expectedHtml); 79 | expect(calls.count).toBe(1); 80 | const serialized = cache.serialize(); 81 | 82 | // Server rendering ends here, client part of the test starts 83 | const deserialized = JSON.parse(serialized); 84 | expect(deserialized).toEqual({ 85 | 'test-resource': { 86 | NO_KEY: { 87 | error: null, 88 | status: 2, 89 | value: 'Async resource' 90 | } 91 | } 92 | }); 93 | 94 | const rehydratedCache = createCache(deserialized); 95 | 96 | const { markup: rehydratedResult } = await renderToString( 97 | 98 | 99 | 100 | 101 | 102 | ); 103 | 104 | expect(rehydratedResult).toBe(expectedHtml); 105 | expect(calls.count).toBe(1); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /src/react/cache.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2018-present, Fredrik Höglund 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 | * This file is heavily based on code from the React-project, 8 | * namely simple-cache-provider, used under the MIT License below: 9 | * 10 | * Copyright (c) 2014-2018, Facebook, Inc. 11 | * 12 | * Permission is hereby granted, free of charge, to any person obtaining a copy 13 | * of this software and associated documentation files (the "Software"), to deal 14 | * in the Software without restriction, including without limitation the rights 15 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | * copies of the Software, and to permit persons to whom the Software is 17 | * furnished to do so, subject to the following conditions: 18 | 19 | * The above copyright notice and this permission notice shall be included in all 20 | * copies or substantial portions of the Software. 21 | 22 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 28 | * SOFTWARE. 29 | */ 30 | 31 | import { createContext } from 'react'; 32 | import serialize from './utils/serialize'; 33 | 34 | const Empty = 0; 35 | const Pending = 1; 36 | const Resolved = 2; 37 | const Rejected = 3; 38 | 39 | export function createCache(initial, invalidator) { 40 | let resourceCache = initial || {}; 41 | 42 | function getRecord(resourceName, key) { 43 | let recordCache = resourceCache[resourceName]; 44 | if (recordCache !== undefined) { 45 | const record = recordCache[key]; 46 | if (record !== undefined) { 47 | return record; 48 | } 49 | } else { 50 | recordCache = {}; 51 | resourceCache[resourceName] = recordCache; 52 | } 53 | 54 | const record = { 55 | status: Empty, 56 | suspender: null, 57 | value: null, 58 | error: null 59 | }; 60 | recordCache[key] = record; 61 | return record; 62 | } 63 | 64 | function load(emptyRecord, suspender) { 65 | const pendingRecord = emptyRecord; 66 | pendingRecord.status = Pending; 67 | pendingRecord.suspender = suspender; 68 | suspender.then( 69 | value => { 70 | // Resource loaded successfully. 71 | const resolvedRecord = pendingRecord; 72 | resolvedRecord.status = Resolved; 73 | resolvedRecord.suspender = null; 74 | resolvedRecord.value = value; 75 | }, 76 | error => { 77 | // Resource failed to load. Stash the error for later so we can throw it 78 | // the next time it's requested. 79 | const rejectedRecord = pendingRecord; 80 | rejectedRecord.status = Rejected; 81 | rejectedRecord.suspender = null; 82 | rejectedRecord.error = error; 83 | } 84 | ); 85 | } 86 | 87 | const cache = { 88 | invalidate() { 89 | invalidator(); 90 | }, 91 | preload(resource, key, miss, missArg) { 92 | const record = getRecord(resource.name, key); 93 | switch (record.status) { 94 | case Empty: 95 | // Warm the cache. 96 | const suspender = miss(missArg); 97 | load(record, suspender); 98 | return; 99 | case Pending: 100 | // There's already a pending request. 101 | return; 102 | case Resolved: 103 | // The resource is already in the cache. 104 | return; 105 | case Rejected: 106 | // The request failed. 107 | return; 108 | } 109 | }, 110 | get(resource, key, miss, missArg) { 111 | const record = getRecord(resource.name, key); 112 | switch (record.status) { 113 | case Empty: 114 | return undefined; 115 | case Pending: 116 | // There's already a pending request. 117 | return record.suspender; 118 | case Resolved: 119 | return record.value; 120 | case Rejected: 121 | default: 122 | // The requested resource previously failed loading. 123 | const error = record.error; 124 | throw error; 125 | } 126 | }, 127 | read(resource, key, miss, missArg) { 128 | const record = getRecord(resource.name, key); 129 | switch (record.status) { 130 | case Empty: 131 | // Load the requested resource. 132 | const suspender = miss(missArg); 133 | load(record, suspender); 134 | throw suspender; 135 | case Pending: 136 | // There's already a pending request. 137 | throw record.suspender; 138 | case Resolved: 139 | return record.value; 140 | case Rejected: 141 | default: 142 | // The requested resource previously failed loading. 143 | const error = record.error; 144 | throw error; 145 | } 146 | }, 147 | serialize() { 148 | function replacer(key, value) { 149 | if (key === 'suspender') { 150 | return undefined; 151 | } 152 | return value; 153 | } 154 | return serialize(resourceCache, replacer); 155 | } 156 | }; 157 | 158 | return cache; 159 | } 160 | 161 | export function createResource(resourceName, loadResource, hash) { 162 | const resource = { 163 | name: resourceName, 164 | get(cache, key = 'NO_KEY') { 165 | if (hash === undefined) { 166 | return cache.get(resource, key, loadResource, key); 167 | } 168 | const hashedKey = hash(key); 169 | return cache.get(resource, hashedKey, loadResource, key); 170 | }, 171 | read(cache, key = 'NO_KEY') { 172 | if (hash === undefined) { 173 | return cache.read(resource, key, loadResource, key); 174 | } 175 | const hashedKey = hash(key); 176 | return cache.read(resource, hashedKey, loadResource, key); 177 | }, 178 | preload(cache, key = 'NO_KEY') { 179 | if (hash === undefined) { 180 | cache.preload(resource, key, loadResource, key); 181 | return; 182 | } 183 | const hashedKey = hash(key); 184 | cache.preload(resource, hashedKey, loadResource, key); 185 | } 186 | }; 187 | return resource; 188 | } 189 | 190 | export const PrimaryCacheContext = createContext(createCache()); 191 | -------------------------------------------------------------------------------- /src/react/hooks.js: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | 3 | import { PrimaryCacheContext } from './cache'; 4 | 5 | // Custom hook 6 | export function useReadResource(resource, key) { 7 | const cache = useContext(PrimaryCacheContext); 8 | return resource.read(cache, key); 9 | } 10 | -------------------------------------------------------------------------------- /src/react/index.js: -------------------------------------------------------------------------------- 1 | export * from './cache'; 2 | export * from './hooks'; 3 | export * from './render'; 4 | -------------------------------------------------------------------------------- /src/react/render.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { createCache, PrimaryCacheContext } from './cache'; 4 | 5 | function removeCacheElement() { 6 | const cacheContainer = document.getElementById( 7 | 'react_cache_data_container' 8 | ); 9 | if (cacheContainer) { 10 | cacheContainer.parentNode.removeChild(cacheContainer); 11 | } 12 | } 13 | 14 | export function render(app, ...args) { 15 | const cache = createCache(); 16 | return ReactDOM.render( 17 | 18 | {app} 19 | , 20 | ...args 21 | ); 22 | } 23 | 24 | export function hydrate(app, ...args) { 25 | let cache; 26 | if (window.__REACT_CACHE_DATA__) { 27 | cache = createCache(window.__REACT_CACHE_DATA__); 28 | } else { 29 | console.error( 30 | 'Warning: Did not find any serialized cache-data, trying to hydrate with empty cache.' 31 | ); 32 | cache = createCache(); 33 | } 34 | removeCacheElement(); 35 | return ReactDOM.hydrate( 36 | 37 | {app} 38 | , 39 | ...args 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/react/utils/serialize.js: -------------------------------------------------------------------------------- 1 | const UNSAFE_TO_SAFE = { 2 | '<': '\\u003C', 3 | '>': '\\u003E', 4 | '/': '\\u002F', 5 | '\u2028': '\\u2028', 6 | '\u2029': '\\u2029' 7 | }; 8 | 9 | export default function serialize(obj, replacer) { 10 | return JSON.stringify(obj, replacer).replace( 11 | /[<>\/\u2028\u2029]/g, 12 | c => UNSAFE_TO_SAFE[c] 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/renderer/DispatcherModifier.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2018-present, Fredrik Höglund 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 React from 'react'; 9 | 10 | export default class DispatcherModifier extends React.Component { 11 | constructor(...args) { 12 | super(...args); 13 | 14 | const currentDispatcher = 15 | React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED 16 | .ReactCurrentOwner.currentDispatcher; 17 | 18 | currentDispatcher.useEffect = () => undefined; 19 | currentDispatcher.useImperativeMethods = () => undefined; 20 | currentDispatcher.useCallback = cb => cb; 21 | currentDispatcher.useLayoutEffect = () => { 22 | if ( 23 | process.env.NODE_ENV === 'development' || 24 | process.env.NODE_ENV === 'dev' 25 | ) { 26 | console.warn( 27 | 'useLayoutEffect does nothing on the server, because its effect cannot ' + 28 | "be encoded into the server renderer's output format. This will lead " + 29 | 'to a mismatch between the initial, non-hydrated UI and the intended ' + 30 | 'UI. To avoid this, useLayoutEffect should only be used in ' + 31 | 'components that render exclusively on the client.' 32 | ); 33 | } 34 | return undefined; 35 | }; 36 | } 37 | render() { 38 | return this.props.children; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/renderer/SSRRenderer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2018-present, Fredrik Höglund 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 | * Some of the code in this file is copied or adapted from the React project, 8 | * used under the license below: 9 | * 10 | * Copyright (c) 2013-2018, Facebook, Inc. 11 | * 12 | * Permission is hereby granted, free of charge, to any person obtaining a copy 13 | * of this software and associated documentation files (the "Software"), to deal 14 | * in the Software without restriction, including without limitation the rights 15 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | * copies of the Software, and to permit persons to whom the Software is 17 | * furnished to do so, subject to the following conditions: 18 | 19 | * The above copyright notice and this permission notice shall be included in all 20 | * copies or substantial portions of the Software. 21 | 22 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 28 | * SOFTWARE. 29 | */ 30 | 31 | import './reactMonkeyPatch'; 32 | 33 | // For now the scheduler uses requestAnimationFrame, 34 | // so we need to polyfill it 35 | import 'raf/polyfill'; 36 | import React from 'react'; 37 | import Reconciler from 'react-reconciler'; 38 | import * as ReactScheduler from 'scheduler'; 39 | import emptyObject from 'fbjs/lib/emptyObject'; 40 | 41 | import omittedCloseTags from './react-dom/src/shared/omittedCloseTags'; 42 | import isCustomComponent from './react-dom/src/shared/isCustomComponent'; 43 | import escapeTextForBrowser from './react-dom/src/server/escapeTextForBrowser'; 44 | import { 45 | createMarkupForCustomAttribute, 46 | createMarkupForProperty 47 | } from './react-dom/src/server/DOMMarkupOperations'; 48 | import createMarkupForStyles from './reactUtils/createMarkupForStyles'; 49 | 50 | import DispatcherModifier from './DispatcherModifier'; 51 | import { PrimaryCacheContext, createCache } from '../react'; 52 | 53 | export const ROOT_TYPE = Symbol('ROOT_TYPE'); 54 | export const ROOT_STATIC_TYPE = Symbol('ROOT_STATIC_TYPE'); 55 | export const RAW_TEXT_TYPE = Symbol('RAW_TEXT_TYPE'); 56 | 57 | function isEventListener(propName) { 58 | return propName.slice(0, 2).toLowerCase() === 'on'; 59 | } 60 | 61 | function getMarkupForChildren(children, staticMarkup, selectedValue) { 62 | const childrenMarkup = []; 63 | for (let i = 0, l = children.length; i < l; i += 1) { 64 | const previousWasText = i > 0 && children[i - 1].type === RAW_TEXT_TYPE; 65 | childrenMarkup.push( 66 | children[i].toString( 67 | staticMarkup, 68 | previousWasText, 69 | undefined, 70 | selectedValue 71 | ) 72 | ); 73 | } 74 | return childrenMarkup.join(''); 75 | } 76 | 77 | const RESERVED_PROPS = { 78 | children: null, 79 | dangerouslySetInnerHTML: null, 80 | suppressContentEditableWarning: null, 81 | suppressHydrationWarning: null 82 | }; 83 | 84 | export class SSRTreeNode { 85 | constructor(type, text) { 86 | this.type = type; 87 | this.text = text; 88 | this.attributes = {}; 89 | } 90 | children = []; 91 | appendChild(child) { 92 | this.children.push(child); 93 | } 94 | insertBefore(child, beforeChild) { 95 | this.children.splice(this.children.indexOf(beforeChild, 0, child)); 96 | } 97 | removeChild(child) { 98 | this.children = this.children.filter(c => c !== child); 99 | } 100 | setText(text) { 101 | this.text = text; 102 | } 103 | setAttribute(name, value) { 104 | this.attributes[name] = value; 105 | } 106 | attributesToString(attributes) { 107 | let ret = ''; 108 | for (const key in attributes) { 109 | if (!attributes.hasOwnProperty(key)) { 110 | continue; 111 | } 112 | let value = attributes[key]; 113 | if (value == null) { 114 | continue; 115 | } 116 | if (key === 'style') { 117 | value = createMarkupForStyles(value); 118 | } 119 | let markup = null; 120 | if (isCustomComponent(this.type.toLowerCase(), attributes)) { 121 | if (!RESERVED_PROPS.hasOwnProperty(key)) { 122 | markup = createMarkupForCustomAttribute(key, value); 123 | } 124 | } else { 125 | markup = createMarkupForProperty(key, value); 126 | } 127 | if (markup) { 128 | ret += ' ' + markup; 129 | } 130 | } 131 | return ret; 132 | } 133 | toString(staticMarkup, previousWasText, isRoot, selectedValue) { 134 | let renderAttributes = this.attributes; 135 | let selectSelectedValue; 136 | let childrenMarkup; 137 | const rawInnerHtml = 138 | this.attributes.dangerouslySetInnerHTML && 139 | this.attributes.dangerouslySetInnerHTML.__html; 140 | if (this.type === ROOT_STATIC_TYPE) { 141 | let markup = getMarkupForChildren(this.children, staticMarkup); 142 | return markup; 143 | } 144 | if (this.type === ROOT_TYPE) { 145 | return this.children 146 | .map(c => c.toString(staticMarkup, undefined, true)) 147 | .join(''); 148 | } 149 | if (this.type === RAW_TEXT_TYPE) { 150 | if (!staticMarkup && previousWasText) { 151 | return '' + escapeTextForBrowser(this.text); 152 | } 153 | return escapeTextForBrowser(this.text); 154 | } 155 | if (this.type === 'input') { 156 | if ( 157 | renderAttributes.defaultValue || 158 | renderAttributes.defaultChecked 159 | ) { 160 | renderAttributes = Object.assign({}, renderAttributes, { 161 | value: 162 | renderAttributes.value != null 163 | ? renderAttributes.value 164 | : renderAttributes.defaultValue, 165 | defaultValue: undefined, 166 | checked: 167 | renderAttributes.Checked != null 168 | ? renderAttributes.Checked 169 | : renderAttributes.defaultChecked, 170 | defaultChecked: undefined 171 | }); 172 | } 173 | } else if (this.type === 'select') { 174 | if (renderAttributes.value || renderAttributes.defaultValue) { 175 | selectSelectedValue = 176 | renderAttributes.value || renderAttributes.defaultValue; 177 | renderAttributes = Object.assign({}, renderAttributes, { 178 | value: undefined, 179 | defaultValue: undefined 180 | }); 181 | } 182 | } else if (this.type === 'textarea') { 183 | if (renderAttributes.value || renderAttributes.defaultValue) { 184 | this.appendChild( 185 | new SSRTreeNode( 186 | RAW_TEXT_TYPE, 187 | renderAttributes.value || renderAttributes.defaultValue 188 | ) 189 | ); 190 | renderAttributes = Object.assign({}, renderAttributes, { 191 | value: undefined, 192 | defaultValue: undefined 193 | }); 194 | } 195 | } else if (this.type === 'option') { 196 | childrenMarkup = getMarkupForChildren( 197 | this.children, 198 | staticMarkup, 199 | selectSelectedValue 200 | ); 201 | let selected = null; 202 | if (selectedValue != null) { 203 | let value = 204 | renderAttributes.value != null 205 | ? renderAttributes.value 206 | : childrenMarkup; 207 | if (Array.isArray(selectedValue)) { 208 | for (let i = 0; i < selectedValue.length; i++) { 209 | if (selectedValue[i] === value) { 210 | selected = true; 211 | break; 212 | } 213 | } 214 | } else { 215 | selected = selectedValue === value; 216 | } 217 | renderAttributes = Object.assign( 218 | {}, 219 | { 220 | selected 221 | }, 222 | renderAttributes 223 | ); 224 | } 225 | } 226 | 227 | const selfClose = !this.children.length && omittedCloseTags[this.type]; 228 | const startTag = `<${this.type}${this.attributesToString( 229 | renderAttributes 230 | )}${isRoot ? ' data-reactroot=""' : ''}${selfClose ? '/>' : '>'}`; 231 | childrenMarkup = 232 | rawInnerHtml || 233 | childrenMarkup || 234 | getMarkupForChildren( 235 | this.children, 236 | staticMarkup, 237 | selectSelectedValue 238 | ); 239 | const endTag = selfClose ? '' : ``; 240 | return startTag + childrenMarkup + endTag; 241 | } 242 | } 243 | 244 | const hostConfig = { 245 | getRootHostContext(rootInstance) { 246 | return emptyObject; 247 | }, 248 | getChildHostContext(parentHostContext, type) { 249 | return emptyObject; 250 | }, 251 | 252 | // Useful only for testing 253 | getPublicInstance(inst) { 254 | return inst; 255 | }, 256 | 257 | // Create the DOMElement, but attributes are set in `finalizeInitialChildren` 258 | createInstance( 259 | type, 260 | props, 261 | rootContainerInstance, 262 | hostContext, 263 | internalInstanceHandle 264 | ) { 265 | return new SSRTreeNode(type); 266 | }, 267 | 268 | // appendChild for direct children 269 | appendInitialChild(parentInstance, child) { 270 | parentInstance.appendChild(child); 271 | }, 272 | 273 | // Actually set the attributes and text content to the domElement and check if 274 | // it needs focus, which will be eventually set in `commitMount` 275 | finalizeInitialChildren(element, type, props) { 276 | Object.keys(props).forEach(propName => { 277 | const propValue = props[propName]; 278 | 279 | if (propName === 'children') { 280 | if ( 281 | typeof propValue === 'string' || 282 | typeof propValue === 'number' 283 | ) { 284 | element.appendChild( 285 | new SSRTreeNode(RAW_TEXT_TYPE, propValue) 286 | ); 287 | } 288 | } else if (propName === 'className') { 289 | element.setAttribute('class', propValue); 290 | } else if (!isEventListener(propName)) { 291 | element.setAttribute(propName, propValue); 292 | } 293 | }); 294 | return false; 295 | }, 296 | 297 | // Calculate the updatePayload 298 | prepareUpdate(domElement, type, oldProps, newProps) {}, 299 | 300 | shouldSetTextContent(type, props) { 301 | return ( 302 | type === 'textarea' || 303 | typeof props.children === 'string' || 304 | typeof props.children === 'number' 305 | ); 306 | }, 307 | shouldDeprioritizeSubtree(type, props) {}, 308 | createTextInstance( 309 | text, 310 | rootContainerInstance, 311 | hostContext, 312 | internalInstanceHandle 313 | ) { 314 | return new SSRTreeNode(RAW_TEXT_TYPE, text); 315 | }, 316 | scheduleDeferredCallback: ReactScheduler.unstable_scheduleCallback, 317 | cancelDeferredCallback: ReactScheduler.unstable_cancelCallback, 318 | shouldYield: ReactScheduler.unstable_shouldYield, 319 | 320 | scheduleTimeout: setTimeout, 321 | cancelTimeout: clearTimeout, 322 | 323 | setTimeout: setTimeout, 324 | clearTimeout: clearTimeout, 325 | 326 | noTimeout: -1, 327 | 328 | // Commit hooks, useful mainly for react-dom syntethic events 329 | prepareForCommit() {}, 330 | resetAfterCommit() {}, 331 | 332 | now: ReactScheduler.unstable_now, 333 | isPrimaryRenderer: true, 334 | //useSyncScheduling: true, 335 | 336 | supportsMutation: true, 337 | commitUpdate( 338 | domElement, 339 | updatePayload, 340 | type, 341 | oldProps, 342 | newProps, 343 | internalInstanceHandle 344 | ) {}, 345 | commitMount(domElement, type, newProps, internalInstanceHandle) {}, 346 | commitTextUpdate(textInstance, oldText, newText) { 347 | textInstance.setText(newText); 348 | }, 349 | resetTextContent(textInstance) { 350 | textInstance.setText(''); 351 | }, 352 | appendChild(parentInstance, child) { 353 | parentInstance.appendChild(child); 354 | }, 355 | 356 | // appendChild to root container 357 | appendChildToContainer(parentInstance, child) { 358 | parentInstance.appendChild(child); 359 | }, 360 | insertBefore(parentInstance, child, beforeChild) { 361 | parentInstance.insertBefore(child, beforeChild); 362 | }, 363 | insertInContainerBefore(parentInstance, child, beforeChild) { 364 | parentInstance.insertBefore(child, beforeChild); 365 | }, 366 | removeChild(parentInstance, child) { 367 | parentInstance.removeChild(child); 368 | }, 369 | removeChildFromContainer(parentInstance, child) { 370 | parentInstance.removeChild(child); 371 | }, 372 | 373 | // These are todo and not well understood on the server 374 | hideInstance() {}, 375 | hideTextInstance() {}, 376 | unhideInstance() {}, 377 | unhideTextInstance() {} 378 | }; 379 | 380 | const SSRRenderer = Reconciler(hostConfig); 381 | 382 | function ReactRoot({ staticMarkup = false } = {}) { 383 | const rootType = staticMarkup ? ROOT_STATIC_TYPE : ROOT_TYPE; 384 | const ssrTreeRootNode = new SSRTreeNode(rootType); 385 | this._internalTreeRoot = ssrTreeRootNode; 386 | const root = SSRRenderer.createContainer(ssrTreeRootNode, true); 387 | this._internalRoot = root; 388 | this._staticMarkup = staticMarkup; 389 | } 390 | ReactRoot.prototype.render = function(children) { 391 | const root = this._internalRoot; 392 | const work = new ReactWork(this._internalTreeRoot, { 393 | staticMarkup: this._staticMarkup 394 | }); 395 | SSRRenderer.updateContainer(children, root, null, work._onCommit); 396 | return work; 397 | }; 398 | ReactRoot.prototype.unmount = function() { 399 | const root = this._internalRoot; 400 | const work = new ReactWork(this._internalTreeRoot); 401 | callback = callback === undefined ? null : callback; 402 | SSRRenderer.updateContainer(null, root, null, work._onCommit); 403 | return work; 404 | }; 405 | 406 | function ReactWork(root, { staticMarkup = false } = {}) { 407 | this._callbacks = null; 408 | this._didCommit = false; 409 | // TODO: Avoid need to bind by replacing callbacks in the update queue with 410 | // list of Work objects. 411 | this._onCommit = this._onCommit.bind(this); 412 | this._internalRoot = root; 413 | this._staticMarkup = staticMarkup; 414 | } 415 | ReactWork.prototype.then = function(onCommit) { 416 | if (this._didCommit) { 417 | onCommit(this._internalRoot.toString(this._staticMarkup)); 418 | return; 419 | } 420 | let callbacks = this._callbacks; 421 | if (callbacks === null) { 422 | callbacks = this._callbacks = []; 423 | } 424 | callbacks.push(onCommit); 425 | }; 426 | ReactWork.prototype._onCommit = function() { 427 | if (this._didCommit) { 428 | return; 429 | } 430 | this._didCommit = true; 431 | const callbacks = this._callbacks; 432 | if (callbacks === null) { 433 | return; 434 | } 435 | // TODO: Error handling. 436 | for (let i = 0; i < callbacks.length; i++) { 437 | const callback = callbacks[i]; 438 | callback(this._internalRoot.toString(this._staticMarkup)); 439 | } 440 | }; 441 | 442 | function createRoot(options) { 443 | return new ReactRoot(options); 444 | } 445 | 446 | export function renderToString(element) { 447 | return new Promise((resolve, reject) => { 448 | const root = createRoot(); 449 | const cache = createCache(); 450 | return root 451 | .render( 452 | 453 | 454 | {element} 455 | 456 | 457 | ) 458 | .then(markup => { 459 | const cacheData = cache.serialize(); 460 | const innerHTML = `window.__REACT_CACHE_DATA__ = ${cacheData};`; 461 | const markupWithCacheData = `${markup}`; 462 | resolve({ markup, markupWithCacheData, cache }); 463 | }); 464 | }); 465 | } 466 | 467 | export function renderToStaticMarkup(element) { 468 | return new Promise((resolve, reject) => { 469 | const root = createRoot({ staticMarkup: true }); 470 | const cache = createCache(); 471 | return root 472 | .render( 473 | 474 | 475 | {element} 476 | 477 | 478 | ) 479 | .then(markup => { 480 | resolve({ markup, cache }); 481 | }); 482 | }); 483 | } 484 | 485 | export default { 486 | renderToString, 487 | renderToStaticMarkup 488 | }; 489 | -------------------------------------------------------------------------------- /src/renderer/__tests__/SSRRenderer.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOMServer from 'react-dom/server'; 3 | import { renderToString, renderToStaticMarkup } from '../SSRRenderer'; 4 | 5 | async function expectMarkupToMatch(app) { 6 | const staticOutput = renderToStaticMarkup(app); 7 | const output = renderToString(app); 8 | const expectedStatic = ReactDOMServer.renderToStaticMarkup(app); 9 | const expectedMarkup = ReactDOMServer.renderToString(app); 10 | expect((await staticOutput).markup).toBe(expectedStatic); 11 | expect((await output).markup).toBe(expectedMarkup); 12 | } 13 | 14 | describe('SSRRenderer', () => { 15 | it('should render an empty div', async () => { 16 | await expectMarkupToMatch(
); 17 | }); 18 | it('should render a div with text content', async () => { 19 | await expectMarkupToMatch(
Some content
); 20 | }); 21 | it('should render a div with literal text content', async () => { 22 | await expectMarkupToMatch(
{'Some content'}
); 23 | }); 24 | it('should render a div with a nested span', async () => { 25 | await expectMarkupToMatch( 26 |
27 | 28 |
29 | ); 30 | }); 31 | it('should render multiple text nodes correctly', async () => { 32 | await expectMarkupToMatch( 33 |
34 | {'Text 1'} 35 | {'Text 2'} 36 | {'Text 3'} 37 |
38 | ); 39 | }); 40 | it('should escape text correctly', async () => { 41 | await expectMarkupToMatch(
{`"&'<>`}
); 42 | }); 43 | it('should render a div with a two nested span and text', async () => { 44 | await expectMarkupToMatch( 45 |
46 | Outer text 47 | Text 1 48 | Outer text 49 | Text 2 50 | Outer text 51 |
52 | ); 53 | }); 54 | it('should render multiple roots with array', async () => { 55 | await expectMarkupToMatch([
,
]); 56 | }); 57 | it('should render multiple roots with fragment', async () => { 58 | await expectMarkupToMatch( 59 | 60 |
61 |
62 | 63 | ); 64 | }); 65 | it('should support self-closed tags', async () => { 66 | await expectMarkupToMatch(); 67 | }); 68 | it('should render with id', async () => { 69 | await expectMarkupToMatch(
); 70 | }); 71 | it('should render with other attributes', async () => { 72 | await expectMarkupToMatch(); 73 | }); 74 | it('should render with className', async () => { 75 | await expectMarkupToMatch(
); 76 | }); 77 | it('should render with styles', async () => { 78 | await expectMarkupToMatch( 79 |
80 | ); 81 | }); 82 | it('should automatically insert px for correct styles', async () => { 83 | await expectMarkupToMatch(
); 84 | }); 85 | it('should not render with event listeners', async () => { 86 | jest.spyOn(console, 'error'); 87 | console.error.mockImplementation(() => {}); 88 | await expectMarkupToMatch( 89 |
{}} onclick="alert('Noo');" /> 90 | ); 91 | console.error.mockRestore(); 92 | }); 93 | it('should render input with defaultValue correctly', async () => { 94 | await expectMarkupToMatch(); 95 | }); 96 | it('should render input with defaultChecked correctly', async () => { 97 | await expectMarkupToMatch(); 98 | }); 99 | it('should render textarea with defaultValue', async () => { 100 | await expectMarkupToMatch(