├── .babelrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── examples ├── create-react-app │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ ├── src │ │ ├── app.js │ │ ├── components │ │ │ ├── App.js │ │ │ ├── Helmet.js │ │ │ ├── NotFound.js │ │ │ └── Page.js │ │ ├── images │ │ │ ├── js.jpg │ │ │ └── js.png │ │ ├── index.js │ │ └── store.js │ └── template.js └── webpack-blocks │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── server.js │ ├── src │ ├── favicon.ico │ └── index.html │ └── webpack.config.js ├── package.json ├── src ├── index.js ├── index.spec.js ├── lib.js ├── server.js ├── test.js ├── utils.js ├── utils.spec.js └── wrapper.js ├── typings.json ├── typings ├── globals │ └── jest │ │ ├── index.d.ts │ │ └── typings.json └── index.d.ts └── wrapper.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react-app" 4 | ], 5 | "plugins": [ 6 | "transform-ensure-ignore" 7 | ] 8 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | coverage 3 | node_modules 4 | npm-debug* 5 | package-lock.json -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | typings 2 | typings.json -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | cache: 5 | directories: 6 | - node_modules 7 | deploy: 8 | provider: npm 9 | email: kirill.konshin@gmail.com 10 | api_key: $NPM_TOKEN 11 | skip_cleanup: true 12 | on: 13 | tags: true 14 | repo: kirill-konshin/create-react-server -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Create React Server 2 | =================== 3 | 4 | Config-free server side rendering for React applications based on React Router. Compatible with Redux apps and 5 | Webpack Dev Middleware for simple and painless development and production usage. Comes as [CLI script](#cli-mode), 6 | [standalone server](#custom-server), and [middleware](#middleware). 7 | 8 | *This package is formerly known as `react-router-redux-middleware` (which is now deprecated).* 9 | 10 | - [Installation](#installation) 11 | - [Examples](#examples) 12 | - [Async Routes](#async-routes) 13 | - [Preconditions](#preconditions) 14 | - [CLI mode](#cli-mode) — simple integration with Create React App (aka React Scripts) 15 | - [Config](#config) 16 | - [Template Function](#template-function) 17 | - [Custom server](#custom-server) 18 | - [Use with Webpack Dev Middleware](#middleware) useful for 19 | - [Use with React Helmet](#use-with-react-helmet) 20 | - [Asynchronous require](#asynchronous-require) 21 | - [Handling props updates](#handling-props-updates) 22 | 23 | ## Installation 24 | 25 | ```bash 26 | npm install create-react-server babel-preset-react-app babel-preset-es2015 --save-dev 27 | ``` 28 | 29 | You don't have to install `babel-preset-react-app` if you use Create React App, it will be pre-installed already. 30 | 31 | ## Examples 32 | 33 | - With Create React App: [examples/create-react-app](https://github.com/kirill-konshin/create-react-server/tree/master/examples/create-react-app) 34 | - With Webpack Blocks (custom webpack config, server, dev middleware): [examples/webpack-blocks](https://github.com/kirill-konshin/create-react-server/tree/master/examples/webpack-blocks) 35 | 36 | In order to use examples you should clone the repository and do `npm install` inside the repo and then separate 37 | `npm install` in example dir. 38 | 39 | ```bash 40 | git clone https://github.com/kirill-konshin/create-react-server.git 41 | cd create-react-server 42 | npm install 43 | cd examples/[any-example] 44 | npm install 45 | npm run redeploy # launch in production mode 46 | ``` 47 | 48 | ## Async Routes 49 | 50 | **!!!ATTENTION!!! Due to changes in React Router 4 async routes are no longer supported by this package!** 51 | 52 | Official RR4 documentation says the following: 53 | 54 | > 1. You need synchronous module resolution on the server so you can get those bundles in the initial render. 55 | > 2. You need to load all the bundles in the client that were involved in the server render before rendering so that 56 | > the client render is the same as the server render. (The trickiest part, I think its possible but this is where 57 | > I gave up.) 58 | > 3. You need asynchronous resolution for the rest of the client app’s life. 59 | 60 | [Code Splitting & Server Rendering](https://reacttraining.com/react-router/web/guides/code-splitting/code-splitting-server-rendering) 61 | 62 | So not at this moment at least, stay tuned, we will try to add this in future releases! Especially if React Fiber (16) 63 | will take care of some async component lifecycle. 64 | 65 | ## Preconditions 66 | 67 | ### Add `.babelrc` file or `babel` section of `package.json` 68 | 69 | ```json 70 | { 71 | "presets": [ 72 | "es2015", 73 | "react-app" 74 | ] 75 | } 76 | ``` 77 | 78 | ### Page (e.g. leaf router node) 79 | 80 | Server rendering procedure takes `getInitialProps` static property and performs everything defined in it both on server 81 | and later on client (if needed). Anything that was returned from `getInitialProps` becomes the initial set of props 82 | when component will be rendered on server. 83 | 84 | On client these props will be available with some delay, so you may use custom property `initialLoading` which will be 85 | `true` while `getInitialProps` function is resolving. 86 | 87 | If an error occurs inside `getInitialProps` it will be available via `initialError` property. This property is populated 88 | from the server as well (as a string, no trace, you may capture this also in `template` function). 89 | 90 | Component also receives a wrapped version of `getInitialProps` in its `props`, so that it can be called when needed, 91 | for example when `componentWillReceiveProps` on React Router route change to load new data, but be careful and don't 92 | cause infinite loops or race conditions. 93 | 94 | If you use `withWrapper` you *must* wrap each leaf page, otherwise if you open unwrapped page in browser and then 95 | navigate to wrapped no `getInitialProps` will be called because wrapped will assume that it's first run. 96 | 97 | ```js 98 | // src/Page.js 99 | 100 | import React, {Component} from "react"; 101 | import {connect} from "react-redux"; // this is optional 102 | import {withWrapper} from "create-react-server/wrapper"; 103 | 104 | export class App extends Component { 105 | 106 | static async getInitialProps({location: {pathname, query}, params, store}) { 107 | await store.dispatch(barAction()); // this is optional 108 | return {custom: 'custom'}; 109 | }; 110 | 111 | render() { 112 | const {foo, bar, custom, initialError} = this.props; 113 | if (initialError) return
Initial Error: {initialError.stack}
; 114 | return ( 115 |
Foo {foo}, Bar {bar}, Custom {custom}
116 | ); 117 | } 118 | 119 | } 120 | 121 | App = connect(state => state)(App); // this is optional 122 | export default withWrapper(App); // here we connect to WrapperProvider 123 | ``` 124 | 125 | Component which will be used as 404 stub should have `notFound` static property: 126 | 127 | ```js 128 | // src/NotFound.js 129 | 130 | import React, {Component} from "react"; 131 | 132 | export default class NotFound extends Component { 133 | static notFound = true; 134 | render() { 135 | return ( 136 |
404 Not Found
137 | ); 138 | } 139 | 140 | } 141 | ``` 142 | 143 | ### Main App 144 | 145 | You have to make a `createApp` function that should return an app with React Router routes. 146 | 147 | ```js 148 | // src/app.js 149 | 150 | import React from "react"; 151 | import {Route, IndexRoute} from "react-router"; 152 | import NotFound from './NotFound'; 153 | import Page from './Page'; 154 | import IndexPage from './IndexPage'; 155 | 156 | export default ({state, props, req, res}) => { 157 | 158 | if (!state && !!req) { // this means function is called on server 159 | state = { 160 | 'foo': req.url + ':' + Date.now() 161 | }; 162 | } 163 | 164 | return ( 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | ); 175 | 176 | }; 177 | ``` 178 | 179 | If you don't use Redux then `Provider` is not needed. 180 | 181 | Parameters that function receives are (all parameters may be `null` depending on where the function is launched, you may 182 | have custom logic specifically for server or client based on what paratements are available): 183 | 184 | - `state` — initial Redux state received from server (if any) 185 | - `props` — initial props received from server (if any) 186 | - `req` — NodeJS Request (if any) 187 | - `res` — NodeJS Response (if any) 188 | 189 | ### Redux (optional) 190 | 191 | If your app is using Redux, then you will have to make a `createStore` function, that should take initial state as an 192 | argument and return a new `Store`: 193 | 194 | ```js 195 | // src/store.js 196 | 197 | import {createStore} from "redux"; 198 | 199 | function reducer(state, action) { return state; } 200 | 201 | export default function (initialState, {req, res}) { 202 | if (req) { // it means it's launched from server in CLI mode 203 | initialState = {foo: res.url}; // so we can pre-populate something 204 | } 205 | return createStore( 206 | reducer, 207 | initialState 208 | ); 209 | } 210 | ``` 211 | 212 | ### Main App Entry Point 213 | 214 | You have to create a root app component, which normally consists only of `BrowserRouter` or `HashRouter` and a call to 215 | `createApp`. 216 | 217 | ```js 218 | // src/index.js 219 | 220 | import React from "react"; 221 | import {render} from "react-dom"; 222 | import {BrowserRouter} from "react-router-dom"; 223 | import createApp from "./app"; 224 | 225 | const Root = () => ( 226 | 227 | {createApp({ 228 | state: window.__INITIAL_STATE__, // you can skip this if you don't use Redux 229 | props: window.__INITIAL__PROPS__ 230 | })} 231 | 232 | ); 233 | 234 | render((), document.getElementById('root')); 235 | ``` 236 | 237 | ## CLI Mode 238 | 239 | First of all prepare your application according to steps in [preconditions](#preconditions). 240 | 241 | It is convenient to put console command into `scripts` section of `package.json`: 242 | 243 | ```json 244 | { 245 | "build": "react-scripts build", 246 | "server": "create-react-server --app path-to/src/app.js [options]" 247 | } 248 | ``` 249 | 250 | All specified JS files must export functions as `default export` or as `module.exports`. It assumes that 251 | `--app path-to-app.js` as path to file which exports a `createApp` and so on. 252 | 253 | Available options: 254 | 255 | - `--app` or `-r` path to JS file with `createApp` function 256 | - `--template` or `-t` path to JS file with `template` function 257 | - `--outputPath` or `-o` as path to your `build` (e.g. your static files) 258 | - `--templatePath` or `-i` path to your `index.html` 259 | - `--debug` or `-d` if you want to get more information of requests handling & stack traces of errors 260 | - `--port` or `-p` to bind to something other than `3000`, port will also be automatically taken from `process.env.PORT` 261 | 262 | 263 | You may also run with `NODE_ENV=development` to get more info: 264 | 265 | ```bash 266 | NODE_ENV=development create-react-server [your options] 267 | ``` 268 | 269 | Then run from console: 270 | 271 | ```bash 272 | npm run build 273 | npm run server 274 | ``` 275 | 276 | Now if you open `http://localhost:3000` you will see a page rendered on the server. 277 | 278 | ## Config 279 | 280 | Middleware accepts following options: 281 | 282 | - `options.outputPath` **required** path with static files, usually equals to Webpack's `output.path` 283 | - `options.app({props, state, req, res})` **required** function must return an app that uses React Router 284 | - `options.template` *optional*, main [template function](#template-function), performs injection of rendered HTML to 285 | the template, default = replaces `
` with `
%HTML%
` 286 | completely failed to render 287 | - `options.templatePath` *optional* path to `index.html`, default = `%outputPath%/index.html` 288 | - `options.debug` *optional* emits to console some extra information about request handling, default = `false` 289 | - `options.initialStateKey` *optional* key in `window` object that will be used to pass initial props, 290 | default = `__INITIAL_PROPS__` 291 | - `options.initialPropsKey` *optional* key in `window` object that will be used to pass initial state, 292 | default = `__INITIAL_STATE__` 293 | 294 | Server accepts following options in addition to the ones accepted by middleware: 295 | 296 | - `options.skipExtensions` *optional* array of strings that represent most commonly imported non-JS extensions that has 297 | to be skipped during server build, default = `['css', 'jpg', 'gif', ...]` 298 | - `options.port` *optional* port to listen, default = `3000` 299 | - `options.listen` *optional* Express's listen function 300 | 301 | ## Template Function 302 | 303 | Template function performs injection of rendered HTML to the template. This function will also be called if React App 304 | failed to render (e.g. in case of server error). 305 | 306 | Function accepts `config` as parameter with the following always available properties: 307 | 308 | - `config.error` error object if function is called as error handler, equals `null` otherwise 309 | - `config.req` instance of NodeJS Request 310 | - `config.res` instance of NodeJS Response 311 | - `config.template` contents of `index.html` or any other file defined in `templatePath` option 312 | 313 | If function is called in a **normal** way, e.g. NOT as error handler, the following properties will also be provided, 314 | some of them still *may* be available in error handler mode, depending on which stage the error has happened: 315 | 316 | - `config.component` matched component (your leaf page) 317 | - `config.html` result of React rendering 318 | - `config.initialProps` result of `getInitialProps` (resolved result of Promise if it was returned) 319 | - `config.store` instance of Redux Store 320 | 321 | Most common cases when this function is called as error handler are: 322 | 323 | - You forgot to configure React Router's fallback route, e.g. `` 324 | - React Router has returned an error 325 | 326 | If you don't output any error information then the client will be rendered as if nothing happened. 327 | 328 | By default this function replaces `
` (exactly as written). If there is an error — it's HTML is 329 | injected right before `div#root`. 330 | 331 | If anything will be thrown from this function, then default error handler will take over. You should avoid this by 332 | placing `try {} catch {}` around your code, in this case default handler wiil not be called. 333 | 334 | ## Custom server 335 | 336 | First of all prepare your application according to steps in [preconditions](#preconditions). 337 | 338 | In these example we will use `express` server and `babel-cli`: 339 | 340 | ```bash 341 | npm install express babel-cli --save-dev 342 | ``` 343 | 344 | Modify `scripts` section of `package.json`: 345 | 346 | ```json 347 | { 348 | "build": "react-scripts build && npm run build-server", 349 | "build-server": "NODE_ENV=production babel --source-maps --out-dir build-lib src", 350 | "server": "node ./build-lib/server.js" 351 | } 352 | ``` 353 | 354 | It makes client side build using Create React App (React Scripts) and server side build using Babel, which takes 355 | everything from `src` and puts the outcome to `build-lib`. You may add this directory to `.gitignore`. 356 | 357 | ```js 358 | // src/server.js 359 | 360 | import path from "path"; 361 | import {createExpressServer} from "create-react-server"; 362 | import app from "./app"; 363 | 364 | createExpressServer({ 365 | port: process.env.PORT || 3000, 366 | app: app, 367 | template: ({template, html, req}) => ( 368 | template.replace( 369 | `
`, 370 | `
${html}
`)), 371 | outputPath: path.join(process.cwd(), 'build') 372 | }); 373 | ``` 374 | 375 | Check out the ready-to-use example in [examples/create-react-app](https://github.com/kirill-konshin/create-react-server/tree/master/examples/create-react-app) 376 | folder. 377 | 378 | In this mode your `createStore` function will on server will receive second config argument: `{req, res}` with request 379 | and response respectively. In other modes you can control what is passed where. 380 | 381 | ## Middleware 382 | 383 | There are two middleware modes: for Webpack Dev Server and for Express server. 384 | 385 | If you have access to `webpack.config.js` then you may run the Webpack Dev Server along with server side rendering, 386 | this example covers both. 387 | 388 | In order to do that we need to install `webpack-dev-server` (in addition to packages from 389 | [preconditions step](#preconditions)), you may skip this if you have already installed it. In these example we will use 390 | `express` server and `babel-cli` to make server side builds: 391 | 392 | ```bash 393 | npm install express babel-cli babel-preset-react-app webpack-dev-server html-webpack-plugin --save-dev 394 | ``` 395 | 396 | *Note: you don't have to install `babel-preset-react-app` if you use Create React App or you already have preset. 397 | 398 | ### Modify `scripts` section of `package.json` 399 | 400 | In this example we run server by Babel Node, in this case server will be transformed in runtime (which is not 401 | recommended for production). You also can build the server like in [custom server](#custom-server) section. 402 | 403 | ```json 404 | { 405 | "server-dev": "NODE_ENV=development babel-node ./src/server.js", 406 | "server-runtime": "NODE_ENV=production babel-node ./src/server.js" 407 | } 408 | ``` 409 | 410 | ### Webpack Config 411 | 412 | Main entry file `index.html` should be a part of webpack build, e.g. emitted to you output path. It could be a 413 | real file or generated by `HtmlWebpackPlugin`, but it has to be known by Webpack. 414 | 415 | Make sure your `webpack.config.js` has all the following: 416 | 417 | ```js 418 | // webpack.config.js 419 | 420 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 421 | module.exports = { 422 | //... 423 | "output": { 424 | path: process.cwd() + '/build', // mandatory 425 | publicPath: '/', 426 | }, 427 | "plugins": [new HtmlWebpackPlugin({ 428 | filename: 'index.html', 429 | favicon: './src/favicon.ico', // this is optional 430 | template: './src/index.html' 431 | })], 432 | devServer: { 433 | port: process.env.PORT || 3000, 434 | contentBase: './src', 435 | } 436 | //... 437 | } 438 | ``` 439 | 440 | ### Server 441 | 442 | ```js 443 | // src/server.js 444 | 445 | import path from "path"; 446 | import Express from "express"; 447 | import webpack from "webpack"; 448 | import Server from "webpack-dev-server"; 449 | import app from "./app"; // same file as in client side 450 | import config from "../webpack.config"; 451 | import {createExpressMiddleware, createWebpackMiddleware, skipRequireExtensions} from "create-react-server"; 452 | 453 | skipRequireExtensions(); // this may be omitted but then you need to manually teach Node to ignore non-js files 454 | 455 | const port = process.env.PORT || 3000; 456 | 457 | const options = { 458 | app: app, 459 | template: ({template, html}) => (template.replace( 460 | // !!!!! MUST MATCH THE INDEX.HTML 461 | `
`, 462 | `
${html}
` 463 | )), 464 | templatePath: path.join(config.output.path, 'index.html'), 465 | outputPath: config.output.path 466 | }; 467 | 468 | if (process.env.NODE_ENV !== 'production') { 469 | 470 | const compiler = webpack(config); 471 | 472 | config.devServer.setup = function(app) { 473 | app.use(createWebpackMiddleware(compiler, config)(options)); 474 | }; 475 | 476 | new Server(compiler, config.devServer).listen(port, '0.0.0.0', listen); 477 | 478 | } else { 479 | 480 | const app = Express(); 481 | 482 | app.use(createExpressMiddleware(options)); 483 | app.use(Express.static(config.output.path)); 484 | 485 | app.listen(port, listen); 486 | 487 | } 488 | 489 | function listen(err) { 490 | if (err) throw err; 491 | console.log('Listening %s', port); 492 | } 493 | ``` 494 | 495 | Check out the ready-to-use example in [examples/webpack-blocks](https://github.com/kirill-konshin/create-react-server/tree/master/examples/webpack-blocks) 496 | folder. 497 | 498 | ## Use with React Helmet 499 | 500 | Take a look at React Helmet's [readme note about server side rendering](https://github.com/nfl/react-helmet#server-usage). 501 | In a few words you have to add `renderStatic()` call to your implementation of `template` option: 502 | 503 | ```js 504 | import Helmet from "react-helmet"; 505 | 506 | const template = ({template, html, req}) => { 507 | 508 | const head = Helmet.renderStatic(); 509 | 510 | return template 511 | .replace( 512 | `
`, 513 | `
${html}
` 514 | ) 515 | .replace( 516 | /.*?<\/title>/g, 517 | head.title.toString() 518 | ) 519 | .replace( 520 | /<html>/g, 521 | '<html ' + head.htmlAttributes.toString() + '>' 522 | ); 523 | 524 | }; 525 | ``` 526 | 527 | ## Asynchronous Require 528 | 529 | If you use `require.ensure` in your app, you will have to install `babel-plugin-transform-ensure-ignore`. 530 | 531 | ```bash 532 | npm install babel-plugin-transform-ensure-ignore --save-dev 533 | ``` 534 | 535 | And add it to `.babelrc` file or `babel` section of `package.json`: 536 | 537 | ```json 538 | { 539 | "presets": [ 540 | "es2015", 541 | "react-app" 542 | ], 543 | "plugins": [ 544 | "transform-ensure-ignore" 545 | ] 546 | } 547 | ``` 548 | 549 | If you use dynamic `import()` function, then you will need more plugins `babel-plugin-dynamic-import-webpack`, it should 550 | be used together with `babel-plugin-transform-ensure-ignore`. Make sure it is used only on server, and Webpack (client 551 | build) will not pick it up. On client plugin `babel-plugin-syntax-dynamic-import` should be used. 552 | 553 | ## Handling props updates 554 | 555 | Your component may receive props from React Router without unmounting/mounting, for example `query` or `param` has 556 | changed. 557 | 558 | In this case you can create a `componentWillReceiveProps` lifecycle hook and call `this.props.getInitialProps()` from 559 | it to force static `getInitialProps` method to be called again: 560 | 561 | ```js 562 | export class Page extends React.Component { 563 | 564 | static async getInitialProps({params}) { 565 | var res = await fetch(`/pages?slug=${params.slug}`); 566 | return await res.json(); 567 | } 568 | 569 | componentWillReceiveProps(newProps) { 570 | if (this.props.params.slug !== newProps.params.slug) this.props.getInitialProps(); 571 | } 572 | 573 | render() { 574 | // your stuff here 575 | } 576 | 577 | } 578 | 579 | export default withWrapper(Page); 580 | ``` -------------------------------------------------------------------------------- /examples/create-react-app/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | build-lib -------------------------------------------------------------------------------- /examples/create-react-app/README.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | Runs Webpack Dev Server, **no Server Side Rendering**. 4 | 5 | ```bash 6 | $ npm start 7 | ``` 8 | 9 | # Production + Server Side Rendering 10 | 11 | The redeploy sequence is as follows: 12 | 13 | ```bash 14 | $ npm run build 15 | $ npm run server 16 | ``` 17 | 18 | # Known issues 19 | 20 | - [ ] Dynamic `import()` won't work until `react-scripts@0.10.0` 21 | - [ ] Can't have skins because CSS is extracted in one file -------------------------------------------------------------------------------- /examples/create-react-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "start": "react-scripts start", 5 | "build": "react-scripts build", 6 | "server": "node ../../src/server.js --app src/app.js --template template.js", 7 | "redeploy": "npm run build && npm run server", 8 | "postinstall": "linklocal" 9 | }, 10 | "babel": { 11 | "presets": [ 12 | "env", 13 | "react-app" 14 | ], 15 | "plugins": [ 16 | "transform-ensure-ignore" 17 | ] 18 | }, 19 | "dependencies": { 20 | "create-react-server": "file:../..", 21 | "es6-promise": "^4.1.1", 22 | "isomorphic-fetch": "^2.2.1", 23 | "react": "^16.0.0", 24 | "react-dom": "^16.0.0", 25 | "react-helmet": "^5.2.0", 26 | "react-redux": "^5.0.6", 27 | "react-router-dom": "^4.2.2", 28 | "redux": "^3.7.2", 29 | "redux-logger": "^3.0.6", 30 | "redux-promise-middleware": "^4.4.1", 31 | "redux-thunk": "^2.2.0" 32 | }, 33 | "devDependencies": { 34 | "babel-plugin-transform-ensure-ignore": "^0.1.0", 35 | "babel-preset-env": "^1.6.0", 36 | "linklocal": "^2.8.1", 37 | "react-scripts": "^1.0.14" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /examples/create-react-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirill-konshin/create-react-server/52a878456db0bf0e413dad99224fff885d2acebf/examples/create-react-app/public/favicon.ico -------------------------------------------------------------------------------- /examples/create-react-app/public/index.html: -------------------------------------------------------------------------------- 1 | <!doctype html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="utf-8"> 5 | <meta name="viewport" content="width=device-width, initial-scale=1"> 6 | <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico"> 7 | <title>Create React App Test 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/create-react-app/src/app.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {Provider} from "react-redux"; 3 | import {Route, Switch} from "react-router"; 4 | import {WrapperProvider} from "create-react-server/wrapper"; 5 | import NotFound from "./components/NotFound"; 6 | import App from "./components/App"; 7 | import Page from "./components/Page"; 8 | import createStore from "./store"; 9 | 10 | export default ({state, props, req}) => { 11 | 12 | if (!state && req) { 13 | state = { 14 | 'foo': req.url + ':' + Date.now() 15 | }; 16 | } 17 | 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | 30 | }; 31 | -------------------------------------------------------------------------------- /examples/create-react-app/src/components/App.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from "react"; 2 | import {connect} from "react-redux"; 3 | import {Link} from "react-router-dom"; 4 | import {withWrapper} from "create-react-server/wrapper"; 5 | import {barAction} from "../store"; 6 | import jpg from "../images/js.jpg"; 7 | import png from "../images/js.png"; 8 | import Helmet from "./Helmet"; 9 | 10 | console.log(jpg); 11 | 12 | const Loading = ({state}) => (
Loading: {state}...
); 13 | 14 | export class App extends Component { 15 | 16 | /** 17 | * This function is used for server-side rendering 18 | * @param location 19 | * @param params 20 | * @param query 21 | * @param store 22 | * @return {Promise} 23 | */ 24 | static async getInitialProps({location, query, params, store}) { 25 | 26 | console.log('getInitialProps before dispatch', store.getState().bar); 27 | 28 | await store.dispatch(barAction()); 29 | 30 | console.log('getInitialProps after dispatch', store.getState().bar); 31 | 32 | return {custom: 'custom' + Date.now()}; 33 | 34 | }; 35 | 36 | getPropsAgain(){ 37 | this.props.getInitialProps(); 38 | } 39 | 40 | render() { 41 | 42 | const {foo, bar, custom, initialError} = this.props; 43 | 44 | if (initialError) return
Initial Error: {initialError.stack}
; 45 | 46 | if (bar === 'initial' || bar === 'loading') return ; 47 | 48 | return ( 49 |
50 | 51 |

Index

52 |
Foo {foo}, Bar {bar}, Custom {custom}
53 | 54 |
55 | Open page 56 |
57 | JPG 58 | PNG 59 |
60 | ); 61 | 62 | } 63 | 64 | } 65 | 66 | App = connect(state => state)(App); 67 | 68 | export default withWrapper(App); -------------------------------------------------------------------------------- /examples/create-react-app/src/components/Helmet.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Helmet from "react-helmet"; 3 | 4 | export default (props) => ( 5 | 6 | ); -------------------------------------------------------------------------------- /examples/create-react-app/src/components/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {Link, withRouter} from "react-router-dom"; 3 | import Helmet from "./Helmet"; 4 | 5 | class NotFound extends React.Component { 6 | 7 | render() { 8 | 9 | if (this.props.staticContext) { 10 | this.props.staticContext.status = 404; 11 | } 12 | 13 | return ( 14 |
15 | 16 |

Page Not Found

17 |

Go to index page.

18 |
19 | ); 20 | 21 | } 22 | 23 | } 24 | 25 | export default withRouter(NotFound); -------------------------------------------------------------------------------- /examples/create-react-app/src/components/Page.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from "react"; 2 | import {Link} from "react-router-dom"; 3 | import Helmet from "./Helmet"; 4 | import {withWrapper} from "create-react-server/wrapper"; 5 | 6 | export class Page extends Component { 7 | 8 | render() { 9 | 10 | return ( 11 |
12 | 13 |

Page

14 |
15 | Open index 16 |
17 | ); 18 | 19 | } 20 | 21 | } 22 | 23 | export default withWrapper(Page); -------------------------------------------------------------------------------- /examples/create-react-app/src/images/js.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirill-konshin/create-react-server/52a878456db0bf0e413dad99224fff885d2acebf/examples/create-react-app/src/images/js.jpg -------------------------------------------------------------------------------- /examples/create-react-app/src/images/js.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirill-konshin/create-react-server/52a878456db0bf0e413dad99224fff885d2acebf/examples/create-react-app/src/images/js.png -------------------------------------------------------------------------------- /examples/create-react-app/src/index.js: -------------------------------------------------------------------------------- 1 | import "es6-promise/auto"; 2 | import "isomorphic-fetch"; 3 | import React from "react"; 4 | import {render} from "react-dom"; 5 | import {BrowserRouter} from "react-router-dom"; 6 | import createApp from "./app"; 7 | 8 | const Root = () => ( 9 | 10 | {createApp({state: window.__INITIAL__STATE__, props: window.__INITIAL__PROPS__})} 11 | 12 | ); 13 | 14 | render((), document.getElementById('root')); 15 | 16 | if (module.hot) module.hot.accept(); 17 | 18 | -------------------------------------------------------------------------------- /examples/create-react-app/src/store.js: -------------------------------------------------------------------------------- 1 | import {applyMiddleware, compose, createStore, combineReducers} from "redux"; 2 | import {createLogger} from "redux-logger"; 3 | import thunk from "redux-thunk"; 4 | import promiseMiddleware from "redux-promise-middleware"; 5 | 6 | function foo(state = 'initial', {type, payload}) { 7 | return state; 8 | } 9 | 10 | function bar(state = 'initial', {type, payload}) { 11 | switch (type) { 12 | case 'BAR_SUCCESS': 13 | return payload; 14 | case 'BAR_PENDING': 15 | return 'loading'; 16 | default: 17 | return state; 18 | } 19 | } 20 | 21 | export function barAction() { 22 | return { 23 | type: 'BAR', 24 | payload: new Promise((res, rej) => { 25 | setTimeout(() => { 26 | res(Date.now()); 27 | }, 250); 28 | }) 29 | } 30 | } 31 | 32 | const isBrowser = (typeof window !== 'undefined'); 33 | 34 | export default function configureStore(initialState, {req, res} = {}) { 35 | 36 | if (!initialState && req) { 37 | initialState = {foo: 'server'}; 38 | } 39 | 40 | console.log('Store created with initial state', initialState); 41 | 42 | let middlewares = [ 43 | promiseMiddleware({ 44 | promiseTypeSuffixes: ['PENDING', 'SUCCESS', 'ERROR'] 45 | }), 46 | thunk 47 | ]; 48 | 49 | if (isBrowser) { 50 | middlewares.push(createLogger({ 51 | collapsed: true 52 | })); 53 | } 54 | 55 | return createStore( 56 | combineReducers({ 57 | foo, 58 | bar 59 | }), 60 | initialState, 61 | compose( 62 | applyMiddleware(...middlewares), 63 | isBrowser && window['devToolsExtension'] ? window['devToolsExtension']() : f => f 64 | ) 65 | ); 66 | 67 | } -------------------------------------------------------------------------------- /examples/create-react-app/template.js: -------------------------------------------------------------------------------- 1 | import Helmet from "react-helmet"; 2 | 3 | export default ({template, html, error}) => { 4 | 5 | //@see https://github.com/nfl/react-helmet#server-usage 6 | const head = Helmet.renderStatic(); 7 | 8 | const errorHtml = error 9 | ? `

Server Error

${error.stack || error}
` 10 | : ''; 11 | 12 | return template 13 | .replace( 14 | `
`, 15 | `${errorHtml}
${html}
` 16 | ) 17 | .replace( 18 | /.*?<\/title>/g, 19 | head.title.toString() 20 | ) 21 | .replace( 22 | /<html>/g, 23 | '<html ' + head.htmlAttributes.toString() + '>' 24 | ); 25 | 26 | }; -------------------------------------------------------------------------------- /examples/webpack-blocks/.gitignore: -------------------------------------------------------------------------------- 1 | build -------------------------------------------------------------------------------- /examples/webpack-blocks/README.md: -------------------------------------------------------------------------------- 1 | [Create React App example](../create-react-app) does not allow to create common library outside its' `src` dir, so in 2 | this example we will import components from there. It also shows additional flexibility of file placement. 3 | 4 | # Development 5 | 6 | Runs Webpack Dev Server, **with Server Side Rendering**. 7 | 8 | ```bash 9 | $ npm start 10 | ``` 11 | 12 | # Production + Server Side Rendering 13 | 14 | ```bash 15 | $ npm run build 16 | $ npm run server 17 | ``` -------------------------------------------------------------------------------- /examples/webpack-blocks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "clean": "rimraf ./build/*", 5 | "build": "npm run clean && NODE_ENV=production webpack --progress", 6 | "start": "NODE_ENV=development babel-node ./server.js", 7 | "server": "NODE_ENV=production babel-node ./server.js", 8 | "redeploy": "npm run build && npm run server", 9 | "postinstall": "linklocal" 10 | }, 11 | "babel": { 12 | "presets": [ 13 | "env", 14 | "react-app" 15 | ], 16 | "plugins": [ 17 | "dynamic-import-webpack", 18 | "transform-ensure-ignore" 19 | ] 20 | }, 21 | "dependencies": { 22 | "create-react-server": "file:../..", 23 | "es6-promise": "^4.1.1", 24 | "isomorphic-fetch": "^2.2.1", 25 | "react": "^16.0.0", 26 | "react-dom": "^16.0.0", 27 | "react-helmet": "^5.2.0", 28 | "react-redux": "^5.0.6", 29 | "react-router-dom": "^4.2.2", 30 | "redux": "^3.7.2", 31 | "redux-logger": "^3.0.6", 32 | "redux-promise-middleware": "^4.4.1", 33 | "redux-thunk": "^2.2.0" 34 | }, 35 | "devDependencies": { 36 | "@webpack-blocks/babel6": "^0.4.1", 37 | "@webpack-blocks/dev-server2": "^0.4.0", 38 | "@webpack-blocks/webpack2": "^0.4.0", 39 | "babel-cli": "^6.26.0", 40 | "babel-plugin-syntax-dynamic-import": "^6.18.0", 41 | "babel-plugin-transform-ensure-ignore": "^0.1.0", 42 | "babel-preset-env": "^1.6.0", 43 | "babel-preset-react-app": "^3.0.3", 44 | "express": "^4.15.5", 45 | "html-webpack-plugin": "^2.30.1", 46 | "linklocal": "^2.8.1", 47 | "rimraf": "^2.6.2", 48 | "webpack": "^3.6.0", 49 | "webpack-dev-server": "^2.9.1" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/webpack-blocks/server.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import Express from "express"; 3 | import webpack from "webpack"; 4 | import Server from "webpack-dev-server"; 5 | import {createExpressMiddleware, createWebpackMiddleware, skipRequireExtensions} from "../../src/index"; // this should be create-react-server 6 | import config from "./webpack.config"; 7 | 8 | // Create React App does not allow to create common library outside its' src dir, so we import from there 9 | import template from "../create-react-app/template"; 10 | import createApp from "../create-react-app/src/app"; 11 | 12 | skipRequireExtensions(); 13 | 14 | const port = process.env.PORT || 3000; 15 | 16 | function isDevServer() { 17 | return process.env.NODE_ENV !== 'production'; 18 | } 19 | 20 | const options = { 21 | app: createApp, 22 | template: template, 23 | outputPath: config.output.path, 24 | templatePath: path.join(config.output.path, 'index.html'), 25 | debug: true 26 | }; 27 | 28 | if (isDevServer()) { 29 | 30 | const compiler = webpack(config); 31 | 32 | config.devServer.setup = (app) => { 33 | app.use(createWebpackMiddleware(compiler, config)(options)); 34 | }; 35 | 36 | new Server(compiler, config.devServer).listen(port, '0.0.0.0', listen); 37 | 38 | } else { 39 | 40 | // this can also be replaced with createExpressServer({...options, listen}) 41 | const app = Express(); 42 | app.use(createExpressMiddleware(options)); 43 | app.use(Express.static(config.output.path)); 44 | app.listen(port, listen); 45 | 46 | } 47 | 48 | function listen(err) { 49 | if (err) throw err; 50 | console.log('Listening %s', port); 51 | } 52 | 53 | -------------------------------------------------------------------------------- /examples/webpack-blocks/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirill-konshin/create-react-server/52a878456db0bf0e413dad99224fff885d2acebf/examples/webpack-blocks/src/favicon.ico -------------------------------------------------------------------------------- /examples/webpack-blocks/src/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html> 3 | <head> 4 | <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> 5 | <meta name="fragment" content="!"> 6 | <meta charset="utf-8"> 7 | <title>Webpack Blocks Test 8 | 9 |
10 | 11 | -------------------------------------------------------------------------------- /examples/webpack-blocks/webpack.config.js: -------------------------------------------------------------------------------- 1 | const {createConfig, setOutput, defineConstants, env, entryPoint, sourceMaps, addPlugins} = require('@webpack-blocks/webpack2'); 2 | const babel = require('@webpack-blocks/babel6'); 3 | const devServer = require('@webpack-blocks/dev-server2'); 4 | const webpack = require("webpack"); 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | 7 | module.exports = createConfig([ 8 | entryPoint({ 9 | index: '../create-react-app/src/index.js' 10 | }), 11 | setOutput({ 12 | path: __dirname + '/build', 13 | publicPath: '/', 14 | sourcePrefix: '', 15 | filename: '[name].[hash].js' 16 | }), 17 | babel({ 18 | cacheDirectory: true, 19 | presets: [require.resolve('babel-preset-react-app')], 20 | plugins: [require.resolve('babel-plugin-syntax-dynamic-import')] 21 | }), 22 | defineConstants({ 23 | 'process.env.NODE_ENV': process.env.NODE_ENV 24 | }), 25 | env('development', [ 26 | devServer({ 27 | port: process.env.PORT || 3000, 28 | contentBase: './src', 29 | stats: {colors: true} 30 | }) 31 | ]), 32 | sourceMaps(), 33 | addPlugins([ 34 | new HtmlWebpackPlugin({ 35 | filename: 'index.html', 36 | favicon: './src/favicon.ico', 37 | chunks: ['common', 'index'], 38 | template: './src/index.html' 39 | }) 40 | ]) 41 | ]); 42 | 43 | // console.log(module.exports); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-react-server", 3 | "version": "0.3.2", 4 | "description": "Server & middleware for React + Router + Redux with Server Side Rendering", 5 | "scripts": { 6 | "test": "jest" 7 | }, 8 | "jest": { 9 | "testRegex": "(/src/.*\\.spec.js)$", 10 | "collectCoverage": true 11 | }, 12 | "babel": { 13 | "presets": [ 14 | "env", 15 | "react-app" 16 | ], 17 | "plugins": [ 18 | "transform-ensure-ignore" 19 | ] 20 | }, 21 | "main": "src/index.js", 22 | "bin": { 23 | "create-react-server": "src/server.js" 24 | }, 25 | "devDependencies": { 26 | "babel-plugin-transform-ensure-ignore": "^0.1.0", 27 | "babel-preset-env": "^1.6.0", 28 | "babel-preset-react-app": "^3.0.3", 29 | "jest": "^21.2.1", 30 | "memory-fs": "^0.4.1", 31 | "node-fetch": "^1.7.3", 32 | "react": "^16.0.0", 33 | "react-dom": "^16.0.0", 34 | "react-redux": "^5.0.6", 35 | "react-router-dom": "^4.2.2", 36 | "redux": "^3.7.2" 37 | }, 38 | "dependencies": { 39 | "babel-register": "^6.26.0", 40 | "create-react-class": "^15.6.2", 41 | "express": "^4.15.5", 42 | "hoist-non-react-statics": "^2.3.1", 43 | "prop-types": "^15.6.0", 44 | "yargs": "^9.0.1" 45 | }, 46 | "peerDependencies": { 47 | "react": "*", 48 | "react-dom": "*", 49 | "react-router-dom": ">=4.0.0" 50 | }, 51 | "author": "Kirill Konshin", 52 | "repository": { 53 | "type": "git", 54 | "url": "git://github.com/kirill-konshin/create-react-server.git" 55 | }, 56 | "bugs": { 57 | "url": "https://github.com/kirill-konshin/create-react-server/issues" 58 | }, 59 | "homepage": "https://github.com/kirill-konshin/create-react-server", 60 | "engines": { 61 | "node": ">=0.10.36" 62 | }, 63 | "license": "MIT" 64 | } 65 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fs = require('fs'); 4 | var path = require("path"); 5 | var url = require("url"); 6 | var express = require('express'); 7 | var utils = require('./utils'); 8 | 9 | var roots = [ 10 | '/', 11 | '/index.html' 12 | ]; 13 | 14 | var skippedExtensions = [ 15 | '.coffee', 16 | '.css', 17 | '.eot', 18 | '.gif', 19 | '.jpg', 20 | '.jpeg', 21 | '.less', 22 | '.png', 23 | '.sass', 24 | '.scss', 25 | '.svg', 26 | '.ts', 27 | '.ttf', 28 | '.woff', 29 | '.woff2' 30 | ]; 31 | 32 | function skipRequireExtensions(additional) { 33 | 34 | skippedExtensions 35 | .concat(additional || []) 36 | .forEach(function(ext) { 37 | require.extensions[ext] = function() {}; 38 | }); 39 | 40 | } 41 | 42 | function createWebpackMiddleware(compiler, config) { 43 | 44 | if (!compiler.outputFileSystem) { 45 | throw new Error('Compiler has no file system yet, use inside config.devServer.setup(cb) callback'); 46 | } 47 | 48 | return function(options) { 49 | options.fs = compiler.outputFileSystem; 50 | return createExpressMiddleware(options); 51 | }; 52 | 53 | } 54 | 55 | /** 56 | * @param {function} options.app({state, props, req, res}) 57 | * @param {function} [options.template] main [template function](#template-function), performs injection of rendered HTML to the template, default = replaces `
` with `
%HTML%
` 58 | * @param {string} [options.outputPath] path with static files, usually equals to Webpack's `output.path` 59 | * @param {string} [options.templatePath] path to `index.html`, default = `%outputPath%/index.html` 60 | * @param {boolean} [options.debug] emits to console some extra information about request handling, default = `false` 61 | * @param {object} [options.fs] internal option used to pass an instance of file system object 62 | * @param {string} [options.initialStateKey] key in `window` object that will be used to pass initial props, default = `__INITIAL_PROPS__` 63 | * @param {string} [options.initialPropsKey] key in `window` object that will be used to pass initial state, default = `__INITIAL_STATE__` 64 | * @return {Function} 65 | */ 66 | function createExpressMiddleware(options) { 67 | 68 | options = options || {}; 69 | 70 | options.fs = options.fs || fs; 71 | options.outputPath = options.outputPath || path.join(process.cwd(), 'build'); 72 | options.templatePath = options.templatePath || path.join(options.outputPath, 'index.html'); 73 | options.initialStateKey = options.initialStateKey || '__INITIAL__STATE__'; 74 | options.initialPropsKey = options.initialPropsKey || '__INITIAL__PROPS__'; 75 | options.template = options.template || utils.defaultTemplate; 76 | 77 | return function(req, res, next) { 78 | 79 | utils.waitForTemplate(options).then(function(template) { 80 | 81 | var location = url.parse(req.url, true); 82 | 83 | if ( 84 | options.fs.existsSync(path.join(options.outputPath, location.pathname)) && 85 | !~roots.indexOf(location.pathname) 86 | ) { 87 | if (options.debug) console.log('Static', location.pathname); 88 | next(); 89 | return; 90 | } 91 | 92 | if (options.debug) console.log('Rendering', location.pathname + location.search); 93 | 94 | return utils.middleware(options, template, req, res, next); 95 | 96 | }); 97 | } 98 | 99 | } 100 | 101 | /** 102 | * @param {function} options.app({state, props, req, res}) 103 | * @param {function} [options.template] main [template function](#template-function), performs injection of rendered HTML to the template, default = replaces `
` with `
%HTML%
` 104 | * @param {string} [options.outputPath] path with static files, usually equals to Webpack's `output.path` 105 | * @param {string} [options.templatePath] path to `index.html`, default = `%outputPath%/index.html` 106 | * @param {boolean} [options.debug] emits to console some extra information about request handling, default = `false` 107 | * @param {object} [options.fs] internal option used to pass an instance of file system object 108 | * @param {string} [options.initialStateKey] key in `window` object that will be used to pass initial props, default = `__INITIAL_PROPS__` 109 | * @param {string} [options.initialPropsKey] key in `window` object that will be used to pass initial state, default = `__INITIAL_STATE__` 110 | * @param {string[]} [options.skipExtensions] array of strings that represent most commonly imported non-JS extensions that has to be skipped during server build, default = `['css', 'jpg', 'gif', ...]` 111 | * @param {number} [options.port] listening port, default = `3000` 112 | * @param {function} [options.listen] Express's listen function 113 | * @return {Function} 114 | */ 115 | function createExpressServer(options) { 116 | 117 | skipRequireExtensions(options.skipExtensions || null); 118 | 119 | var app = express(); 120 | var port = options.port || 3000; 121 | 122 | app.use(createExpressMiddleware(options)); 123 | 124 | app.use(express.static(options.outputPath)); 125 | 126 | app.listen(port, options.listen || function(err) { 127 | if (err) throw err; 128 | console.log('Listening', port); 129 | }); 130 | 131 | return app; 132 | 133 | } 134 | 135 | exports.skipRequireExtensions = skipRequireExtensions; 136 | exports.createWebpackMiddleware = createWebpackMiddleware; 137 | exports.createExpressMiddleware = createExpressMiddleware; 138 | exports.createExpressServer = createExpressServer; -------------------------------------------------------------------------------- /src/index.spec.js: -------------------------------------------------------------------------------- 1 | import fetch from "node-fetch"; 2 | import Express from "express"; 3 | import MemoryFileSystem from "memory-fs"; 4 | import React from "react"; 5 | import {createStore} from "redux"; 6 | import {Redirect, Route, Switch} from "react-router-dom"; 7 | import {connect, Provider} from "react-redux"; 8 | import {createExpressMiddleware} from "./index"; 9 | import {withWrapper, WrapperProvider} from "./wrapper"; 10 | import "./test"; 11 | 12 | // ------------------------------------------------------------------------------------------------------------------ // 13 | 14 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000; 15 | 16 | const reducer = (state = {foo: 'initial'}, {type, payload}) => { 17 | if (type === 'FOO') return {foo: payload}; 18 | return state; 19 | }; 20 | 21 | const template = '
'; 22 | 23 | const simpleErrorTemplate = (error) => ('[' + error.code + ']:' + error.stack); 24 | 25 | const defaultOptions = { 26 | templatePath: '/foo', 27 | outputPath: '/bar', 28 | template: ({template, html, error}) => { 29 | if (!!error) return simpleErrorTemplate(error); 30 | return template.replace('', html); 31 | }, 32 | debug: false 33 | }; 34 | 35 | const createOptions = (options = {}) => { 36 | options = { 37 | ...defaultOptions, 38 | fs: new MemoryFileSystem(), 39 | ...options 40 | }; 41 | options.fs.writeFileSync('/foo', template, 'utf-8'); 42 | return options; 43 | }; 44 | 45 | const serverTest = (options, test) => { 46 | return (new Promise((res) => { 47 | 48 | const app = Express(); 49 | 50 | app.use(createExpressMiddleware(options)); 51 | 52 | const server = app.listen(3333, () => { 53 | res(server); 54 | }); 55 | 56 | })).then(async(server) => { 57 | 58 | try { 59 | await test(server); 60 | server.close(); 61 | } catch (e) { 62 | server.close(); 63 | throw e; 64 | } 65 | 66 | }); 67 | }; 68 | 69 | // ------------------------------------------------------------------------------------------------------------------ // 70 | 71 | test('createExpressMiddleware e2e success', async() => { 72 | 73 | let Page = ({foo, custom}) => ({foo + '|' + custom}); 74 | Page.getInitialProps = ({store}) => { 75 | store.dispatch({type: 'FOO', payload: 'dispatched'}); 76 | return {custom: 'initial'}; 77 | }; 78 | Page = connect(state => state)(Page); 79 | Page = withWrapper(Page); 80 | 81 | const options = createOptions({ 82 | app: ({state, props}) => ( 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | ) 92 | }); 93 | 94 | return await serverTest(options, async(server) => { 95 | 96 | const expected = ( 97 | '' + 98 | '' + 99 | '' + 100 | '
' + 101 | 'dispatched|initial' + 102 | '
' 103 | ); 104 | 105 | expect(await (await fetch('http://localhost:3333/')).text()).toBe(expected); 106 | expect(await (await fetch('http://localhost:3333/redirect')).text()).toBe(expected); 107 | 108 | }); 109 | 110 | }); -------------------------------------------------------------------------------- /src/lib.js: -------------------------------------------------------------------------------- 1 | var _extends = Object.assign || function(target) { 2 | for (var i = 1; i < arguments.length; i++) { 3 | var source = arguments[i]; 4 | for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } 5 | } 6 | return target; 7 | }; 8 | 9 | function _objectWithoutProperties(obj, keys) { 10 | var target = {}; 11 | for (var i in obj) { 12 | if (keys.indexOf(i) >= 0) continue; 13 | if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; 14 | target[i] = obj[i]; 15 | } 16 | return target; 17 | } 18 | 19 | 20 | exports.extends = _extends; 21 | exports.objectWithoutProperties = _objectWithoutProperties; -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var debug = false; 4 | 5 | try { 6 | 7 | if (!process.env.NODE_ENV) process.env.NODE_ENV = 'production'; 8 | 9 | require('babel-register'); 10 | 11 | var path = require('path'); 12 | var fs = require('fs'); 13 | var createExpressServer = require('./index').createExpressServer; 14 | var skipRequireExtensions = require('./index').skipRequireExtensions; 15 | var cwd = process.cwd(); 16 | var pkg = require('../package.json'); 17 | 18 | skipRequireExtensions(); 19 | 20 | process.on('unhandledRejection', (reason, promise) => { 21 | console.error('Unhandled rejection:', debug && reason.stack ? reason.stack : reason.toString()); 22 | }); 23 | 24 | var argv = require('yargs') 25 | .usage( 26 | 'Usage: $0 --app path-to-app.js [...options]\n\n' + 27 | 'All specified JS files must export functions as default export or as module.exports.\n' + 28 | 'All options except --app are not required.' 29 | ) 30 | .help() 31 | .version(pkg.version) 32 | .alias('version', 'v') 33 | .wrap(null) 34 | .group(['createRoutes', 'createStore', 'template'], 'JS Files') 35 | .group(['outputPath', 'templatePath'], 'Paths') 36 | .option('app', { 37 | demandOption: true, 38 | alias: 'a', 39 | describe: 'JS file with app({state, props}) function ' 40 | }) 41 | .option('template', { 42 | alias: 't', 43 | describe: 'JS file with template() function' 44 | }) 45 | .option('outputPath', { 46 | alias: 'o', 47 | describe: 'Path to directory with static files', 48 | default: 'build' 49 | }) 50 | .option('templatePath', { 51 | alias: 'i', // because index 52 | describe: 'Path to index.html', 53 | default: 'build/index.html', 54 | }) 55 | .option('port', { 56 | alias: 'p', 57 | describe: 'Port to listen', 58 | default: 3000, 59 | type: 'number' 60 | }) 61 | .option('debug', { 62 | alias: 'd', 63 | describe: 'Emit some extra request handling information', 64 | default: false, 65 | type: 'boolean' 66 | }) 67 | .argv; 68 | 69 | function getFunction(pathRel, type) { 70 | 71 | var pathAbs = path.join(cwd, pathRel); 72 | if (!fs.existsSync(pathAbs)) throw new Error(type + ' file "' + pathAbs + '" not found'); 73 | 74 | var creator = require(pathAbs); 75 | 76 | console.log(type + ':', pathAbs); 77 | 78 | var result = creator.default || creator; 79 | 80 | if (!result) throw new Error(type + ' did not export anything via default export or module.exports'); 81 | if (typeof result !== 'function') throw new Error(type + ' export is expected to be a function but got ' + (typeof result)); 82 | 83 | return result; 84 | 85 | } 86 | 87 | debug = argv.debug; 88 | 89 | // Make paths absolute 90 | var outputPath = path.join(cwd, argv.outputPath); 91 | var templatePath = path.join(cwd, argv.templatePath); 92 | 93 | // Functions 94 | var app = argv.app ? getFunction(argv.app, 'App') : null; 95 | var template = argv.template ? getFunction(argv.template, 'Template') : null; 96 | 97 | console.log('Static:', outputPath); 98 | console.log('Template Path:', templatePath); 99 | 100 | createExpressServer({ 101 | app: app, 102 | template: template, 103 | outputPath: outputPath, 104 | templatePath: templatePath, 105 | port: argv.port, 106 | debug: argv.debug 107 | }); 108 | 109 | } catch (e) { 110 | 111 | console.error(e.stack && debug ? e.stack : e.toString()); 112 | console.error('Use "create-react-server --help"'); 113 | process.exit(1); 114 | 115 | } 116 | -------------------------------------------------------------------------------- /src/test.js: -------------------------------------------------------------------------------- 1 | process.on('unhandledRejection', (reason, promise) => { 2 | console.error('Unhandled rejection:', reason.stack ? reason.stack : reason); 3 | }); 4 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | var renderToString = require("react-dom/server").renderToString; 3 | var React = require("react"); 4 | var StaticRouter = require("react-router-dom").StaticRouter; 5 | var lib = require("./lib"); 6 | 7 | var httpCodes = { 8 | redirect: 301, 9 | ok: 200, 10 | notFound: 404, 11 | internalServerError: 500, 12 | notImplemented: 501 13 | }; 14 | 15 | var pollInterval = 200; 16 | 17 | function isRedirect(res) { 18 | return res.statusCode === httpCodes.redirect; 19 | } 20 | 21 | function setErrorStatus(res, e) { 22 | 23 | res.statusMessage = e.message || e; 24 | res.status(httpCodes.internalServerError); 25 | 26 | } 27 | 28 | function waitForTemplate(options) { 29 | 30 | return (new Promise(function(resolve) { 31 | 32 | var interval = setInterval(function() { 33 | 34 | if (options.fs.existsSync(options.templatePath)) { 35 | clearInterval(interval); 36 | resolve(options.fs.readFileSync(options.templatePath).toString()); 37 | } 38 | 39 | }, pollInterval); 40 | 41 | })); 42 | 43 | } 44 | 45 | function renderHTML(config, options) { 46 | 47 | var parsedTemplate = options.template({ 48 | component: config.component, 49 | error: config.error, 50 | html: config.html, 51 | initialProps: config.initialProps, 52 | store: config.store, 53 | renderProps: config.renderProps, 54 | req: config.req, 55 | res: config.res, 56 | template: config.template.replace( 57 | '', // this should be the first script on a page so that others can pick it up 58 | '' + 59 | '' + 60 | '' 61 | ) 62 | }); 63 | 64 | if (typeof parsedTemplate !== 'string') throw new Error('Return type of options.template() has to be a string'); 65 | 66 | return parsedTemplate; 67 | 68 | } 69 | 70 | function errorTemplate(config) { 71 | return ( 72 | "

" + httpCodes.internalServerError + " Server Error

" + 73 | "
" + (config.error.stack || config.error) + "
" 74 | ); 75 | } 76 | 77 | function defaultTemplate(config) { 78 | 79 | var error = config.error ? ('
' + errorTemplate(config) + '
') : ''; 80 | 81 | return config.template.replace( 82 | '
', 83 | error + '
' + config.html + '
' 84 | ); 85 | 86 | } 87 | 88 | function middleware(options, template, req, res) { 89 | 90 | var initialProps, context = {}; // these vars are passes all the way 91 | 92 | return (new Promise(function performRouting(resolve, reject) { 93 | 94 | ['app'].forEach((k) => { 95 | if (!options[k]) throw new Error('Mandatory option not defined: ' + k); 96 | }); 97 | 98 | var initialHtml = renderToString(React.createElement( 99 | StaticRouter, 100 | {location: req.url, context: context}, 101 | options.app({ 102 | props: undefined, 103 | req: req, 104 | res: res, 105 | state: undefined 106 | }) 107 | )); 108 | 109 | // console.log('Context', context); 110 | 111 | if (context.url) { 112 | res.redirect(httpCodes.redirect, context.url); //TODO Handle context.code 113 | return reject(new Error('Redirect')); 114 | } 115 | 116 | resolve(initialHtml); 117 | 118 | })).then(function getInitialPropsOfComponent() { 119 | 120 | return (new Promise(function(resolve) { 121 | resolve((context.getInitialProps) ? context.getInitialProps({ 122 | location: context.location, 123 | req: req, 124 | res: res, 125 | store: context.store 126 | }) : null); 127 | }).catch(function(e) { 128 | return {initialError: e.message || e.toString()}; 129 | })); 130 | 131 | }).then(function renderApp(props) { 132 | 133 | initialProps = props || {}; // client relies on truthy value of server-rendered props 134 | 135 | // console.log('Setting context initial props', initialProps); 136 | // console.log('Store state before rendering', context.store.getState()); 137 | 138 | return { 139 | html: renderToString(React.createElement( 140 | StaticRouter, 141 | {location: req.url, context: context}, 142 | options.app({ 143 | props: initialProps, 144 | req: req, 145 | res: res, 146 | state: context.store ? context.store.getState() : undefined 147 | }) 148 | )) 149 | }; 150 | 151 | }).catch(function renderErrorHandler(e) { 152 | 153 | if (isRedirect(res)) throw e; 154 | 155 | // If we end up here it means server-side error that can't be handled by application 156 | // By returning an object we are recovering from error 157 | return { 158 | error: e 159 | }; 160 | 161 | }).then(function renderAndSendHtml(result) { 162 | 163 | if (result.error) { 164 | setErrorStatus(res, result.error); 165 | } else { 166 | res.status(context.code || httpCodes.ok); 167 | } 168 | 169 | res.send(renderHTML( 170 | lib.extends({ 171 | error: null, 172 | initialProps: initialProps, 173 | html: '', 174 | req: req, 175 | res: res, 176 | store: context.store, 177 | template: template 178 | }, result), // appends error or html 179 | options 180 | )); 181 | 182 | }).catch(function finalErrorHandler(e) { 183 | 184 | if (isRedirect(res)) return; 185 | 186 | setErrorStatus(res, e); 187 | 188 | res.send(errorTemplate({ 189 | error: e, 190 | req: req, 191 | res: res, 192 | template: template 193 | })); 194 | 195 | return e; // re-throw to make it unhandled? 196 | 197 | }); 198 | 199 | } 200 | 201 | exports.middleware = middleware; 202 | exports.errorTemplate = errorTemplate; 203 | exports.defaultTemplate = defaultTemplate; 204 | exports.renderHTML = renderHTML; 205 | exports.waitForTemplate = waitForTemplate; -------------------------------------------------------------------------------- /src/utils.spec.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import MemoryFileSystem from "memory-fs"; 3 | import {errorTemplate, middleware, renderHTML, waitForTemplate} from "./utils"; 4 | import {createStore} from "redux"; 5 | import {Route, Switch, Redirect, withRouter} from "react-router-dom"; 6 | import {connect, Provider} from "react-redux"; 7 | import {withWrapper, WrapperProvider} from "./wrapper"; 8 | import "./test"; 9 | 10 | // ------------------------------------------------------------------------------------------------------------------ // 11 | 12 | const reducer = (state = {foo: 'initial'}, {type, payload}) => { 13 | if (type === 'FOO') return {foo: payload}; 14 | return state; 15 | }; 16 | 17 | const template = '
'; 18 | 19 | const simpleErrorTemplate = (error) => (error.stack); 20 | 21 | const defaultOptions = { 22 | initialPropsKey: 'iProps', 23 | initialStateKey: 'iState', 24 | template: ({template, html, error}) => { 25 | if (!!error) return simpleErrorTemplate(error); 26 | return template.replace('', html); 27 | } 28 | }; 29 | 30 | const getRes = () => { 31 | const res = { 32 | redirect: jest.fn((status) => { res.statusCode = status; }), 33 | send: jest.fn(), 34 | status: jest.fn(), 35 | statusCode: 0, 36 | statusMessage: '' 37 | }; 38 | return res; 39 | }; 40 | 41 | const getReq = (url = '/') => ({url: url}); 42 | 43 | const getMiddleware = (options, req, res) => middleware({...defaultOptions, ...options}, template, req, res); 44 | 45 | // ------------------------------------------------------------------------------------------------------------------ // 46 | 47 | test('utils.waitForTemplate', async() => { 48 | 49 | const options = { 50 | fs: new MemoryFileSystem(), 51 | templatePath: '/foo' 52 | }; 53 | 54 | options.fs.writeFileSync('/foo', 'bar', 'utf-8'); 55 | expect(await waitForTemplate(options)).toBe('bar'); 56 | 57 | options.fs.writeFileSync('/foo', 'baz', 'utf-8'); 58 | expect(await waitForTemplate(options)).toBe('baz'); 59 | 60 | }); 61 | 62 | // ------------------------------------------------------------------------------------------------------------------ // 63 | 64 | test('utils.errorTemplate', () => { 65 | 66 | expect(errorTemplate({error: {message: 'message', stack: 'stack'}})) 67 | .toBe('

500 Server Error

stack
'); 68 | 69 | expect(errorTemplate({error: 'string'})) 70 | .toBe('

500 Server Error

string
'); 71 | 72 | }); 73 | 74 | // ------------------------------------------------------------------------------------------------------------------ // 75 | 76 | test('utils.renderHTML', async() => { 77 | 78 | const options = { 79 | initialStateKey: 'initialStateKey', 80 | initialPropsKey: 'initialPropsKey', 81 | template: jest.fn(({template, html}) => (template.replace('', html))) 82 | }; 83 | 84 | const template = '
'; 85 | const expected = '' + 86 | '' + 87 | '' + 88 | '
html
'; 89 | 90 | const config = { 91 | component: 'component', 92 | html: 'html', 93 | initialProps: {foo: 'bar'}, 94 | store: {getState: () => ({baz: 'qux'})}, 95 | req: 'req', 96 | res: 'res', 97 | template: template 98 | }; 99 | 100 | expect(await renderHTML(config, options)).toBe(expected); 101 | 102 | expect(options.template.mock.calls[0][0].component).toEqual('component'); 103 | expect(options.template.mock.calls[0][0].html).toEqual('html'); 104 | expect(options.template.mock.calls[0][0].initialProps).toEqual({foo: 'bar'}); 105 | expect(options.template.mock.calls[0][0].store.getState()).toEqual({baz: 'qux'}); 106 | expect(options.template.mock.calls[0][0].req).toEqual('req'); 107 | expect(options.template.mock.calls[0][0].template).toEqual( 108 | '' + 109 | '' + 110 | '' + 111 | '
' 112 | ); 113 | 114 | }); 115 | 116 | // ------------------------------------------------------------------------------------------------------------------ // 117 | 118 | test('utils.middleware success with store', async() => { 119 | 120 | let Page = ({foo, custom}) => ({foo}); 121 | Page.getInitialProps = ({store}) => { 122 | store.dispatch({type: 'FOO', payload: 'dispatched'}); 123 | }; 124 | Page = connect(state => state)(Page); 125 | Page = withWrapper(Page); 126 | 127 | const app = ({state, props}) => ( 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | ); 137 | 138 | const expected = ( 139 | '' + 140 | '' + 141 | '' + 142 | '
' + 143 | 'dispatched' + 144 | '
' 145 | ); 146 | 147 | const req1 = getReq(); 148 | const res1 = getRes(); 149 | await getMiddleware({app}, req1, res1); 150 | expect(res1.send.mock.calls[0][0]).toEqual(expected); 151 | 152 | const req2 = getReq('/redirect'); 153 | const res2 = getRes(); 154 | await getMiddleware({app}, req2, res2); 155 | expect(res2.redirect.mock.calls[0][0]).toEqual(301); 156 | expect(res2.statusCode).toEqual(301); 157 | 158 | }); 159 | 160 | test('utils.middleware success with custom initial state', async() => { 161 | 162 | const req = getReq(); 163 | const res = getRes(); 164 | 165 | let Page = ({foo, custom}) => ({foo}); 166 | Page = connect(state => state)(Page); 167 | Page = withWrapper(Page); 168 | 169 | await getMiddleware({ 170 | app: ({state, props}) => ( 171 | 172 | 173 | 174 | 175 | 176 | ), 177 | }, req, res); 178 | 179 | expect(res.send.mock.calls[0][0]).toEqual( 180 | '' + 181 | '' + 182 | '' + 183 | '
' + 184 | 'override' + 185 | '
' 186 | ); 187 | 188 | }); 189 | 190 | test('utils.middleware no store success', async() => { 191 | 192 | const req = getReq(); 193 | const res = getRes(); 194 | 195 | let NoStoreComponent = ({foo}) => (
{foo}
); 196 | NoStoreComponent.getInitialProps = () => ({foo: 'initial'}); 197 | NoStoreComponent = withWrapper(NoStoreComponent); 198 | 199 | await getMiddleware({ 200 | app: ({state, props}) => ( 201 | 202 | 203 | 204 | ), 205 | }, req, res); 206 | 207 | expect(res.send.mock.calls[0][0]).toEqual( 208 | '' + 209 | '' + 210 | '' + 211 | '
' + 212 | '
initial
' + 213 | '
' 214 | ); 215 | 216 | }); 217 | 218 | test('utils.middleware 404', async() => { 219 | 220 | const req = getReq(); 221 | const res = getRes(); 222 | 223 | let NoStoreComponent = ({foo}) => (
{foo}
); 224 | NoStoreComponent.getInitialProps = () => ({foo: 'initial'}); 225 | NoStoreComponent = withWrapper(NoStoreComponent); 226 | 227 | await getMiddleware({ 228 | app: ({state, props}) => ( 229 | 230 | 231 | 232 | ), 233 | }, req, res); 234 | 235 | expect(res.send.mock.calls[0][0]).toEqual( 236 | '' + 237 | '' + 238 | '' + 239 | '
' + 240 | '
initial
' + 241 | '
' 242 | ); 243 | 244 | }); 245 | 246 | 247 | test('utils.middleware 500 when bad component', async() => { 248 | 249 | const req = getReq(); 250 | const res = getRes(); 251 | 252 | let BadComponent = () => { throw new Error('Bad Component'); }; 253 | 254 | await getMiddleware({ 255 | app: ({state, props}) => ( 256 | 257 | 258 | 259 | ), 260 | }, req, res); 261 | 262 | expect(res.send.mock.calls[0][0]).toContain('Error: Bad Component'); 263 | 264 | }); 265 | 266 | test('utils.middleware 500 when error in template', async() => { 267 | 268 | const req = getReq(); 269 | const res = getRes(); 270 | 271 | let BadComponent = () => { throw new Error('Bad Component'); }; 272 | 273 | await getMiddleware({ 274 | template: () => (null), 275 | app: ({state, props}) => ( 276 | 277 | 278 | 279 | ), 280 | }, req, res); 281 | 282 | expect(res.send.mock.calls[0][0]).toContain('

500 Server Error

'); 283 | expect(res.send.mock.calls[0][0]).toContain('Return type of options.template() has to be a string'); 284 | 285 | }); 286 | 287 | test('utils.middleware 500 when bad initialProps', async() => { 288 | 289 | const req = getReq(); 290 | const res = getRes(); 291 | 292 | let BadInitialProps = ({initialError}) => (
{initialError && initialError.message}
); 293 | BadInitialProps.getInitialProps = () => { throw new Error('Bad Initial Props'); }; 294 | BadInitialProps = withWrapper(BadInitialProps); 295 | BadInitialProps = withRouter(BadInitialProps); // adding withRouter to make life harder 296 | 297 | await getMiddleware({ 298 | app: ({state, props}) => ( 299 | 300 | 301 | 302 | ), 303 | }, req, res); 304 | 305 | expect(res.send.mock.calls[0][0]).toBe( 306 | '' + 307 | '' + 308 | '' + 309 | '
' + 310 | '
Bad Initial Props
' + 311 | '
' 312 | ); 313 | 314 | }); 315 | 316 | test.skip('utils.middleware unwrapped'); 317 | test.skip('utils.middleware wrapped and no getInitialProps and no state'); -------------------------------------------------------------------------------- /src/wrapper.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var withRouter = require('react-router-dom').withRouter; 3 | var PropTypes = require('prop-types'); 4 | var createClass = require('create-react-class'); 5 | var hoistStatics = require('hoist-non-react-statics'); 6 | var lib = require('./lib'); 7 | 8 | function isNode() { 9 | return (typeof process === 'object' && process + '' === '[object process]'); 10 | } 11 | 12 | function getDisplayName(Cmp) { 13 | return Cmp.displayName || Cmp.name || 'Component'; 14 | } 15 | 16 | function withWrapper(Cmp) { 17 | 18 | var Wrapper = createClass({ 19 | 20 | displayName: 'WithWrapper', 21 | 22 | contextTypes: { 23 | store: PropTypes.any, 24 | getInitialProps: PropTypes.func 25 | }, 26 | 27 | getInitialState: function getInitialState() { 28 | 29 | var initialProps = this.context.getInitialProps(); 30 | 31 | // console.log('Initial props from context', initialProps); 32 | 33 | return lib.extends({}, initialProps || {}, { 34 | initialLoading: !initialProps, // no props means it will load 35 | initialError: initialProps && initialProps.initialError && new Error(initialProps.initialError) // it comes as string 36 | }); 37 | 38 | }, 39 | 40 | componentWillMount: function componentWillMount() { 41 | 42 | var self = this; 43 | 44 | // On NodeJS setState is a no-op, besides, getInitialProps will be called by server rendering procedure 45 | if (isNode()) { 46 | 47 | // if (!this.state.initialLoading) return; 48 | //match, location, history, staticContext 49 | 50 | this.props.staticContext.store = this.context.store; //FIXME Brutal access to Redux Provider's store 51 | 52 | this.props.staticContext.location = this.props.location; 53 | 54 | if (Cmp.getInitialProps) { 55 | this.props.staticContext.getInitialProps = Cmp.getInitialProps.bind(Cmp); 56 | } 57 | 58 | return; 59 | 60 | } 61 | 62 | // On client side this function should not be called if props were passed from server 63 | if (!this.state.initialLoading) return; 64 | 65 | return new Promise(function(res) { 66 | 67 | res(Cmp.getInitialProps ? Cmp.getInitialProps({ 68 | location: self.props.location, 69 | params: self.props.params, 70 | req: null, 71 | res: null, 72 | store: self.context.store //FIXME Brutal access to Redux Provider's store 73 | }) : null); 74 | 75 | }).then(function(props) { 76 | 77 | self.setState(lib.extends({}, props, { 78 | initialLoading: false, 79 | initialError: null 80 | })); 81 | 82 | }).catch(function onError(e) { 83 | 84 | console.error(Wrapper.displayName + '.getInitialProps has failed:', e); 85 | 86 | self.setState({ 87 | initialLoading: false, 88 | initialError: e 89 | }); 90 | 91 | }); 92 | 93 | }, 94 | 95 | getInitialProps: function getInitialProps() { 96 | 97 | var self = this; 98 | 99 | return new Promise(function(resolve) { 100 | 101 | if (self.state.initialLoading) { 102 | console.warn(Wrapper.displayName + '.getInitialProps is already pending, make sure you won\'t have race condition'); 103 | } 104 | 105 | self.state = {}; 106 | 107 | self.setState({ 108 | initialLoading: true, 109 | initialError: null 110 | }, function() { 111 | resolve(self.componentWillMount()); 112 | }); 113 | 114 | }); 115 | 116 | }, 117 | 118 | render: function render() { 119 | 120 | var props = lib.objectWithoutProperties(this.props, ["children"]); 121 | 122 | return React.createElement( 123 | Cmp, 124 | //TODO Add mapping function 125 | lib.extends({getInitialProps: this.getInitialProps}, this.state, props), 126 | this.props.children 127 | ); 128 | 129 | } 130 | 131 | }); 132 | 133 | Wrapper = withRouter(Wrapper); 134 | 135 | Wrapper.displayName = 'withWrapper(' + getDisplayName(Cmp) + ')'; 136 | Wrapper.OriginalComponent = Cmp; 137 | 138 | return hoistStatics(Wrapper, Cmp); 139 | 140 | } 141 | 142 | var WrapperProvider = createClass({ 143 | 144 | getChildContext: function getChildContext() { 145 | var self = this; 146 | return { 147 | getInitialProps: function() { 148 | var initialProps = self.initialProps; 149 | self.initialProps = null; // initial props can be used only once 150 | return initialProps; 151 | } 152 | }; 153 | }, 154 | 155 | getInitialState: function getInitialState() { 156 | this.initialProps = this.props.initialProps; 157 | return {}; 158 | }, 159 | 160 | render: function render() { 161 | return this.props.children; 162 | } 163 | 164 | }); 165 | 166 | WrapperProvider.displayName = 'WrapperProvider'; 167 | 168 | WrapperProvider.propTypes = { 169 | initialProps: PropTypes.any 170 | }; 171 | 172 | WrapperProvider.defaultProps = { 173 | initialProps: null 174 | }; 175 | 176 | WrapperProvider.childContextTypes = { 177 | getInitialProps: PropTypes.func 178 | }; 179 | 180 | exports.withWrapper = withWrapper; 181 | exports.WrapperProvider = WrapperProvider; -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-react-server", 3 | "dependencies": {} 4 | } 5 | -------------------------------------------------------------------------------- /typings/globals/jest/index.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by typings 2 | // Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/1ace18d67a990bd2c8f6e53ffd4fa88560f39171/jest/index.d.ts 3 | declare var beforeAll: jest.Lifecycle; 4 | declare var beforeEach: jest.Lifecycle; 5 | declare var afterAll: jest.Lifecycle; 6 | declare var afterEach: jest.Lifecycle; 7 | declare var describe: jest.Describe; 8 | declare var fdescribe: jest.Describe; 9 | declare var xdescribe: jest.Describe; 10 | declare var it: jest.It; 11 | declare var fit: jest.It; 12 | declare var xit: jest.It; 13 | declare var test: jest.It; 14 | declare var xtest: jest.It; 15 | 16 | declare const expect: jest.Expect; 17 | 18 | interface NodeRequire { 19 | /** Returns the actual module instead of a mock, bypassing all checks on whether the module should receive a mock implementation or not. */ 20 | requireActual(moduleName: string): any; 21 | /** Returns a mock module instead of the actual module, bypassing all checks on whether the module should be required normally or not. */ 22 | requireMock(moduleName: string): any; 23 | } 24 | 25 | declare namespace jest { 26 | /** Provides a way to add Jasmine-compatible matchers into your Jest context. */ 27 | function addMatchers(matchers: jasmine.CustomMatcherFactories): typeof jest; 28 | /** Disables automatic mocking in the module loader. */ 29 | function autoMockOff(): typeof jest; 30 | /** Enables automatic mocking in the module loader. */ 31 | function autoMockOn(): typeof jest; 32 | /** 33 | * @deprecated use resetAllMocks instead 34 | */ 35 | function clearAllMocks(): typeof jest; 36 | /** Clears the mock.calls and mock.instances properties of all mocks. Equivalent to calling .mockClear() on every mocked function. */ 37 | function resetAllMocks(): typeof jest; 38 | /** Removes any pending timers from the timer system. If any timers have been scheduled, they will be cleared and will never have the opportunity to execute in the future. */ 39 | function clearAllTimers(): typeof jest; 40 | /** Indicates that the module system should never return a mocked version of the specified module, including all of the specificied module's dependencies. */ 41 | function deepUnmock(moduleName: string): typeof jest; 42 | /** Disables automatic mocking in the module loader. */ 43 | function disableAutomock(): typeof jest; 44 | /** Mocks a module with an auto-mocked version when it is being required. */ 45 | function doMock(moduleName: string): typeof jest; 46 | /** Indicates that the module system should never return a mocked version of the specified module from require() (e.g. that it should always return the real module). */ 47 | function dontMock(moduleName: string): typeof jest; 48 | /** Enables automatic mocking in the module loader. */ 49 | function enableAutomock(): typeof jest; 50 | /** Creates a mock function. Optionally takes a mock implementation. */ 51 | function fn(implementation: (...args: any[]) => T): Mock; 52 | function fn(implementation?: Function): Mock; 53 | /** Use the automatic mocking system to generate a mocked version of the given module. */ 54 | function genMockFromModule(moduleName: string): T; 55 | /** Returns whether the given function is a mock function. */ 56 | function isMockFunction(fn: any): fn is Mock; 57 | /** Mocks a module with an auto-mocked version when it is being required. */ 58 | function mock(moduleName: string, factory?: any, options?: MockOptions): typeof jest; 59 | /** Resets the module registry - the cache of all required modules. This is useful to isolate modules where local state might conflict between tests. */ 60 | function resetModuleRegistry(): typeof jest; 61 | /** Resets the module registry - the cache of all required modules. This is useful to isolate modules where local state might conflict between tests. */ 62 | function resetModules(): typeof jest; 63 | /** Exhausts tasks queued by setImmediate(). */ 64 | function runAllImmediates(): typeof jest; 65 | /** Exhausts the micro-task queue (usually interfaced in node via process.nextTick). */ 66 | function runAllTicks(): typeof jest; 67 | /** Exhausts the macro-task queue (i.e., all tasks queued by setTimeout() and setInterval()). */ 68 | function runAllTimers(): typeof jest; 69 | /** Executes only the macro-tasks that are currently pending (i.e., only the tasks that have been queued by setTimeout() or setInterval() up to this point). 70 | * If any of the currently pending macro-tasks schedule new macro-tasks, those new tasks will not be executed by this call. */ 71 | function runOnlyPendingTimers(): typeof jest; 72 | /** Executes only the macro task queue (i.e. all tasks queued by setTimeout() or setInterval() and setImmediate()). */ 73 | function runTimersToTime(msToRun: number): typeof jest; 74 | /** Explicitly supplies the mock object that the module system should return for the specified module. */ 75 | function setMock(moduleName: string, moduleExports: T): typeof jest; 76 | /** Indicates that the module system should never return a mocked version of the specified module from require() (e.g. that it should always return the real module). */ 77 | function unmock(moduleName: string): typeof jest; 78 | /** Instructs Jest to use fake versions of the standard timer functions. */ 79 | function useFakeTimers(): typeof jest; 80 | /** Instructs Jest to use the real versions of the standard timer functions. */ 81 | function useRealTimers(): typeof jest; 82 | 83 | interface MockOptions { 84 | virtual?: boolean; 85 | } 86 | 87 | interface EmptyFunction { 88 | (): void; 89 | } 90 | 91 | interface DoneCallback { 92 | (...args: any[]): any 93 | fail(error?: string | { message: string }): any; 94 | } 95 | 96 | interface ProvidesCallback { 97 | (cb: DoneCallback): any; 98 | } 99 | 100 | interface Lifecycle { 101 | (fn: ProvidesCallback): any; 102 | } 103 | 104 | /** Creates a test closure */ 105 | interface It { 106 | /** 107 | * Creates a test closure. 108 | * 109 | * @param {string} name The name of your test 110 | * @param {fn?} ProvidesCallback The function for your test 111 | */ 112 | (name: string, fn?: ProvidesCallback): void; 113 | /** Only runs this test in the current file. */ 114 | only: It; 115 | skip: It; 116 | concurrent: It; 117 | } 118 | 119 | interface Describe { 120 | (name: string, fn: EmptyFunction): void 121 | only: Describe; 122 | skip: Describe; 123 | } 124 | 125 | interface MatcherUtils { 126 | readonly isNot: boolean; 127 | utils: { 128 | readonly EXPECTED_COLOR: string; 129 | readonly RECEIVED_COLOR: string; 130 | ensureActualIsNumber(actual: any, matcherName?: string): void; 131 | ensureExpectedIsNumber(actual: any, matcherName?: string): void; 132 | ensureNoExpected(actual: any, matcherName?: string): void; 133 | ensureNumbers(actual: any, expected: any, matcherName?: string): void; 134 | /** get the type of a value with handling of edge cases like `typeof []` and `typeof null` */ 135 | getType(value: any): string; 136 | matcherHint(matcherName: string, received?: string, expected?: string, options?: { secondArgument?: string, isDirectExpectCall?: boolean }): string; 137 | pluralize(word: string, count: number): string; 138 | printExpected(value: any): string; 139 | printReceived(value: any): string; 140 | printWithType(name: string, received: any, print: (value: any) => string): string; 141 | stringify(object: {}, maxDepth?: number): string; 142 | } 143 | } 144 | 145 | interface ExpectExtendMap { 146 | [key: string]: (this: MatcherUtils, received: any, actual: any) => { message: () => string, pass: boolean }; 147 | } 148 | 149 | /** The `expect` function is used every time you want to test a value. You will rarely call `expect` by itself. */ 150 | interface Expect { 151 | /** 152 | * The `expect` function is used every time you want to test a value. You will rarely call `expect` by itself. 153 | * 154 | * @param {any} actual The value to apply matchers against. 155 | */ 156 | (actual: any): Matchers; 157 | anything(): void; 158 | /** Matches anything that was created with the given constructor. You can use it inside `toEqual` or `toBeCalledWith` instead of a literal value. */ 159 | any(classType: any): void; 160 | /** Matches any array made up entirely of elements in the provided array. You can use it inside `toEqual` or `toBeCalledWith` instead of a literal value. */ 161 | arrayContaining(arr: any[]): void; 162 | /** Verifies that a certain number of assertions are called during a test. This is often useful when testing asynchronous code, in order to make sure that assertions in a callback actually got called. */ 163 | assertions(num: number): void; 164 | /** You can use `expect.extend` to add your own matchers to Jest. */ 165 | extend(obj: ExpectExtendMap): void; 166 | /** Matches any object that recursively matches the provided keys. This is often handy in conjunction with other asymmetric matchers. */ 167 | objectContaining(obj: {}): void; 168 | /** Matches any string that contains the exact provided string */ 169 | stringMatching(str: string | RegExp): void; 170 | } 171 | 172 | interface Matchers { 173 | /** If you know how to test something, `.not` lets you test its opposite. */ 174 | not: Matchers; 175 | lastCalledWith(...args: any[]): void; 176 | /** Checks that a value is what you expect. It uses `===` to check strict equality. Don't use `toBe` with floating-point numbers. */ 177 | toBe(expected: any): void; 178 | /** Ensures that a mock function is called. */ 179 | toBeCalled(): void; 180 | /** Ensure that a mock function is called with specific arguments. */ 181 | toBeCalledWith(...args: any[]): void; 182 | /** Using exact equality with floating point numbers is a bad idea. Rounding means that intuitive things fail. */ 183 | toBeCloseTo(expected: number, delta?: number): void; 184 | /** Ensure that a variable is not undefined. */ 185 | toBeDefined(): void; 186 | /** When you don't care what a value is, you just want to ensure a value is false in a boolean context. */ 187 | toBeFalsy(): void; 188 | /** For comparing floating point numbers. */ 189 | toBeGreaterThan(expected: number): void; 190 | /** For comparing floating point numbers. */ 191 | toBeGreaterThanOrEqual(expected: number): void; 192 | /** Ensure that an object is an instance of a class. This matcher uses `instanceof` underneath. */ 193 | toBeInstanceOf(expected: any): void 194 | /** For comparing floating point numbers. */ 195 | toBeLessThan(expected: number): void; 196 | /** For comparing floating point numbers. */ 197 | toBeLessThanOrEqual(expected: number): void; 198 | /** This is the same as `.toBe(null)` but the error messages are a bit nicer. So use `.toBeNull()` when you want to check that something is null. */ 199 | toBeNull(): void; 200 | /** Use when you don't care what a value is, you just want to ensure a value is true in a boolean context. In JavaScript, there are six falsy values: `false`, `0`, `''`, `null`, `undefined`, and `NaN`. Everything else is truthy. */ 201 | toBeTruthy(): void; 202 | /** Used to check that a variable is undefined. */ 203 | toBeUndefined(): void; 204 | /** Used when you want to check that an item is in a list. For testing the items in the list, this uses `===`, a strict equality check. */ 205 | toContain(expected: any): void; 206 | /** Used when you want to check that an item is in a list. For testing the items in the list, this matcher recursively checks the equality of all fields, rather than checking for object identity. */ 207 | toContainEqual(expected: any): void; 208 | /** Used when you want to check that two objects have the same value. This matcher recursively checks the equality of all fields, rather than checking for object identity. */ 209 | toEqual(expected: any): void; 210 | /** Ensures that a mock function is called. */ 211 | toHaveBeenCalled(): boolean; 212 | /** Ensures that a mock function is called an exact number of times. */ 213 | toHaveBeenCalledTimes(expected: number): boolean; 214 | /** Ensure that a mock function is called with specific arguments. */ 215 | toHaveBeenCalledWith(...params: any[]): boolean; 216 | /** If you have a mock function, you can use `.toHaveBeenLastCalledWith` to test what arguments it was last called with. */ 217 | toHaveBeenLastCalledWith(...params: any[]): boolean; 218 | /** Used to check that an object has a `.length` property and it is set to a certain numeric value. */ 219 | toHaveLength(expected: number): void; 220 | toHaveProperty(propertyPath: string, value?: any): void; 221 | /** Check that a string matches a regular expression. */ 222 | toMatch(expected: string | RegExp): void; 223 | /** Used to check that a JavaScript object matches a subset of the properties of an objec */ 224 | toMatchObject(expected: {}): void; 225 | /** This ensures that a value matches the most recent snapshot. Check out [the Snapshot Testing guide](http://facebook.github.io/jest/docs/snapshot-testing.html) for more information. */ 226 | toMatchSnapshot(snapshotName?: string): void; 227 | /** Used to test that a function throws when it is called. */ 228 | toThrow(): void; 229 | /** If you want to test that a specific error is thrown inside a function. */ 230 | toThrowError(error?: string | Constructable | RegExp): void; 231 | /** Used to test that a function throws a error matching the most recent snapshot when it is called. */ 232 | toThrowErrorMatchingSnapshot(): void; 233 | } 234 | 235 | interface Constructable { 236 | new (...args: any[]): any 237 | } 238 | 239 | interface Mock extends Function, MockInstance { 240 | new (): T; 241 | (...args: any[]): any; 242 | } 243 | 244 | /** 245 | * Wrap module with mock definitions 246 | * @example 247 | * jest.mock("../api"); 248 | * import { Api } from "../api"; 249 | * 250 | * const myApi: jest.Mocked = new Api() as any; 251 | * myApi.myApiMethod.mockImplementation(() => "test"); 252 | */ 253 | type Mocked = { 254 | [P in keyof T]: T[P] & MockInstance; 255 | } & T; 256 | 257 | interface MockInstance { 258 | mock: MockContext; 259 | mockClear(): void; 260 | mockReset(): void; 261 | mockImplementation(fn: Function): Mock; 262 | mockImplementationOnce(fn: Function): Mock; 263 | mockReturnThis(): Mock; 264 | mockReturnValue(value: any): Mock; 265 | mockReturnValueOnce(value: any): Mock; 266 | } 267 | 268 | interface MockContext { 269 | calls: any[][]; 270 | instances: T[]; 271 | } 272 | } 273 | 274 | //Jest ships with a copy of Jasmine. They monkey-patch its APIs and divergence/deprecation are expected. 275 | //Relevant parts of Jasmine's API are below so they can be changed and removed over time. 276 | //This file can't reference jasmine.d.ts since the globals aren't compatible. 277 | 278 | declare function spyOn(object: any, method: string): jasmine.Spy; 279 | /** If you call the function pending anywhere in the spec body, no matter the expectations, the spec will be marked pending. */ 280 | declare function pending(reason?: string): void; 281 | /** Fails a test when called within one. */ 282 | declare function fail(error?: any): void; 283 | declare namespace jasmine { 284 | var clock: () => Clock; 285 | function any(aclass: any): Any; 286 | function anything(): Any; 287 | function arrayContaining(sample: any[]): ArrayContaining; 288 | function objectContaining(sample: any): ObjectContaining; 289 | function createSpy(name: string, originalFn?: Function): Spy; 290 | function createSpyObj(baseName: string, methodNames: any[]): any; 291 | function createSpyObj(baseName: string, methodNames: any[]): T; 292 | function pp(value: any): string; 293 | function addCustomEqualityTester(equalityTester: CustomEqualityTester): void; 294 | function addMatchers(matchers: CustomMatcherFactories): void; 295 | function stringMatching(value: string | RegExp): Any; 296 | 297 | interface Clock { 298 | install(): void; 299 | uninstall(): void; 300 | /** Calls to any registered callback are triggered when the clock is ticked forward via the jasmine.clock().tick function, which takes a number of milliseconds. */ 301 | tick(ms: number): void; 302 | mockDate(date?: Date): void; 303 | } 304 | 305 | interface Any { 306 | new (expectedClass: any): any; 307 | jasmineMatches(other: any): boolean; 308 | jasmineToString(): string; 309 | } 310 | 311 | interface ArrayContaining { 312 | new (sample: any[]): any; 313 | asymmetricMatch(other: any): boolean; 314 | jasmineToString(): string; 315 | } 316 | 317 | interface ObjectContaining { 318 | new (sample: any): any; 319 | jasmineMatches(other: any, mismatchKeys: any[], mismatchValues: any[]): boolean; 320 | jasmineToString(): string; 321 | } 322 | 323 | interface Spy { 324 | (...params: any[]): any; 325 | identity: string; 326 | and: SpyAnd; 327 | calls: Calls; 328 | mostRecentCall: { args: any[]; }; 329 | argsForCall: any[]; 330 | wasCalled: boolean; 331 | } 332 | 333 | interface SpyAnd { 334 | /** By chaining the spy with and.callThrough, the spy will still track all calls to it but in addition it will delegate to the actual implementation. */ 335 | callThrough(): Spy; 336 | /** By chaining the spy with and.returnValue, all calls to the function will return a specific value. */ 337 | returnValue(val: any): Spy; 338 | /** By chaining the spy with and.returnValues, all calls to the function will return specific values in order until it reaches the end of the return values list. */ 339 | returnValues(...values: any[]): Spy; 340 | /** By chaining the spy with and.callFake, all calls to the spy will delegate to the supplied function. */ 341 | callFake(fn: Function): Spy; 342 | /** By chaining the spy with and.throwError, all calls to the spy will throw the specified value. */ 343 | throwError(msg: string): Spy; 344 | /** When a calling strategy is used for a spy, the original stubbing behavior can be returned at any time with and.stub. */ 345 | stub(): Spy; 346 | } 347 | 348 | interface Calls { 349 | /** By chaining the spy with calls.any(), will return false if the spy has not been called at all, and then true once at least one call happens. */ 350 | any(): boolean; 351 | /** By chaining the spy with calls.count(), will return the number of times the spy was called */ 352 | count(): number; 353 | /** By chaining the spy with calls.argsFor(), will return the arguments passed to call number index */ 354 | argsFor(index: number): any[]; 355 | /** By chaining the spy with calls.allArgs(), will return the arguments to all calls */ 356 | allArgs(): any[]; 357 | /** By chaining the spy with calls.all(), will return the context (the this) and arguments passed all calls */ 358 | all(): CallInfo[]; 359 | /** By chaining the spy with calls.mostRecent(), will return the context (the this) and arguments for the most recent call */ 360 | mostRecent(): CallInfo; 361 | /** By chaining the spy with calls.first(), will return the context (the this) and arguments for the first call */ 362 | first(): CallInfo; 363 | /** By chaining the spy with calls.reset(), will clears all tracking for a spy */ 364 | reset(): void; 365 | } 366 | 367 | interface CallInfo { 368 | /** The context (the this) for the call */ 369 | object: any; 370 | /** All arguments passed to the call */ 371 | args: any[]; 372 | /** The return value of the call */ 373 | returnValue: any; 374 | } 375 | 376 | interface CustomMatcherFactories { 377 | [index: string]: CustomMatcherFactory; 378 | } 379 | 380 | interface CustomMatcherFactory { 381 | (util: MatchersUtil, customEqualityTesters: Array): CustomMatcher; 382 | } 383 | 384 | interface MatchersUtil { 385 | equals(a: any, b: any, customTesters?: Array): boolean; 386 | contains(haystack: ArrayLike | string, needle: any, customTesters?: Array): boolean; 387 | buildFailureMessage(matcherName: string, isNot: boolean, actual: any, ...expected: Array): string; 388 | } 389 | 390 | interface CustomEqualityTester { 391 | (first: any, second: any): boolean; 392 | } 393 | 394 | interface CustomMatcher { 395 | compare(actual: T, expected: T): CustomMatcherResult; 396 | compare(actual: any, expected: any): CustomMatcherResult; 397 | } 398 | 399 | interface CustomMatcherResult { 400 | pass: boolean; 401 | message: string | (() => string); 402 | } 403 | 404 | interface ArrayLike { 405 | length: number; 406 | [n: number]: T; 407 | } 408 | } 409 | -------------------------------------------------------------------------------- /typings/globals/jest/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "resolution": "main", 3 | "tree": { 4 | "src": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/1ace18d67a990bd2c8f6e53ffd4fa88560f39171/jest/index.d.ts", 5 | "raw": "registry:dt/jest#18.1.0+20170131225342", 6 | "typings": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/1ace18d67a990bd2c8f6e53ffd4fa88560f39171/jest/index.d.ts" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /typings/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /wrapper.js: -------------------------------------------------------------------------------- 1 | var w = require('./src/wrapper'); 2 | exports.withWrapper = w.withWrapper; 3 | exports.WrapperProvider = w.WrapperProvider; --------------------------------------------------------------------------------