├── .env ├── .gitignore ├── README.md ├── config ├── env.js ├── jest │ ├── cssTransform.js │ └── fileTransform.js ├── paths.js ├── polyfills.js ├── production.js ├── webpack.config.base.js ├── webpack.config.client.dev.js ├── webpack.config.client.prod.js ├── webpack.config.server.js └── webpackDevServer.config.js ├── package.json ├── public ├── favicon.ico └── manifest.json ├── scripts ├── build-client.js ├── build-server.js ├── start.js └── test.js ├── src ├── client │ ├── index.css │ ├── index.js │ └── registerServiceWorker.js ├── server │ ├── app.js │ ├── index.js │ └── render.js └── shared │ ├── App.js │ ├── App.test.js │ └── logo.svg └── yarn.lock /.env: -------------------------------------------------------------------------------- 1 | REACT_APP_HOST=localhost 2 | REACT_APP_PORT=8080 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Universal Create React App 2 | 3 | This project is a refactoring of the default app created by [create-react-app v1.0.7](https://github.com/facebookincubator/create-react-app/tree/v1.0.7), and then ejected. 4 | 5 | A universal app runs both on the server and the client, and shares as much code as possible between the server and client - typically around 90%. 6 | 7 | Development 8 | 9 | ![](https://gifyu.com/images/universal-dev.gif) 10 | 11 | Production 12 | 13 | ![](https://gifyu.com/images/universal-build.gif) 14 | 15 | For a step by step explanation read the article 16 | https://medium.com/leanjs/universal-create-react-app-step-by-step-b80ba68d125d 17 | 18 | ## How to run this project 19 | 20 | - `yarn install` 21 | - `yarn start` to run it in development 22 | - `npm run build` to build the production bundle. You must build the production bundle before running the production bundle. 23 | - `npm run serve` to run the production bundle. 24 | - You can disable JavaScript on your browser, and use the app to test that the app is functional. 25 | - With JavaScript enabled and running the app in development mode (`yarn start`), you can test the CSS hot reloading by changing this file /src/client/index.css 26 | 27 | ## Explanation 28 | 29 | The source code (src) is split in 3 folders: 30 | - client. This is code that runs just on the browser. 31 | - server. This is code that runs just on the server. 32 | - shared. This is code that runs both on the server and on the client 33 | 34 | The server is implemented using [Express](http://expressjs.com/) 35 | 36 | There are two build scripts. One to build the JavaScript bundle that will be sent to the client. By default from the same server but you could serve it via a CDN or anywhere else. The other build script builds the JavaScript bundle that runs on the server. 37 | - /scripts/build-client.js 38 | - /scripts/build-server.js 39 | 40 | The start script will try to run the client (Webpack Dev Server) on a given port (3000 by default). If the port is not available it will try to find another port. We have implemented the same on the port used to run the server. The start script will try to run the server (Express compiled with Webpack) on a given port (5678 by default). If the port is not available it will try to find another port. 41 | 42 | For a step by step explanation read the article 43 | https://medium.com/leanjs/universal-create-react-app-step-by-step-b80ba68d125d 44 | 45 | ## Features 46 | 47 | All the features that you have in create-react-app are included in this project, plus react-router v4. 48 | 49 | - `yarn start` will start two servers. The first one (Webpack Dev Server), to build and serve the JavaScript bundle to the client. The Second one (Express), to render the app on the server. 50 | - CSS Hot reloading is enabled. You'll notice a quick adjustment to the layout in development mode when you start the app. This is because while in development env the CSS is served via the Webpack Hot Module Replacement. So the app is rendered without CSS from the server, and then on the client it is injected when the JavaScript is run. If you run the app in production mode by executing `npm run serve` (note, you must first build the production bundle by executing `npm run build`), the CSS will be displayed from the beginning. The reason for this is that we don't hot replace the CSS in production. 51 | - "Page Not found" with a 404 status on the server-side without defining any route on the server. 52 | 53 | ## Acknowledgments 54 | 55 | [Apollo GitHunt-React example](https://github.com/apollographql/GitHunt-React) was a great source of inspiration for finding solutions. 56 | 57 | ## README generated by create-react-app v1.0.7 58 | 59 | You can read here the original README.md generated by create-react-app in this repo https://github.com/facebookincubator/create-react-app/blob/v1.0.7/packages/react-scripts/template/README.md 60 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | 7 | // Make sure that including paths.js after env.js will read .env variables. 8 | delete require.cache[require.resolve('./paths')]; 9 | 10 | const NODE_ENV = process.env.NODE_ENV; 11 | if (!NODE_ENV) { 12 | throw new Error( 13 | 'The NODE_ENV environment variable is required but was not specified.' 14 | ); 15 | } 16 | 17 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 18 | var dotenvFiles = [ 19 | `${paths.dotenv}.${NODE_ENV}.local`, 20 | `${paths.dotenv}.${NODE_ENV}`, 21 | // Don't include `.env.local` for `test` environment 22 | // since normally you expect tests to produce the same 23 | // results for everyone 24 | NODE_ENV !== 'test' && `${paths.dotenv}.local`, 25 | paths.dotenv, 26 | ].filter(Boolean); 27 | 28 | // Load environment variables from .env* files. Suppress warnings using silent 29 | // if this file is missing. dotenv will never modify any environment variables 30 | // that have already been set. 31 | // https://github.com/motdotla/dotenv 32 | dotenvFiles.forEach(dotenvFile => { 33 | if (fs.existsSync(dotenvFile)) { 34 | require('dotenv').config({ 35 | path: dotenvFile, 36 | }); 37 | } 38 | }); 39 | 40 | // We support resolving modules according to `NODE_PATH`. 41 | // This lets you use absolute paths in imports inside large monorepos: 42 | // https://github.com/facebookincubator/create-react-app/issues/253. 43 | // It works similar to `NODE_PATH` in Node itself: 44 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 45 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 46 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 47 | // https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421 48 | // We also resolve them to make sure all tools using them work consistently. 49 | const appDirectory = fs.realpathSync(process.cwd()); 50 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 51 | .split(path.delimiter) 52 | .filter(folder => folder && !path.isAbsolute(folder)) 53 | .map(folder => path.resolve(appDirectory, folder)) 54 | .join(path.delimiter); 55 | 56 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 57 | // injected into the application via DefinePlugin in Webpack configuration. 58 | const REACT_APP = /^REACT_APP_/i; 59 | 60 | function getClientEnvironment(publicUrl) { 61 | const raw = Object.keys(process.env) 62 | .filter(key => REACT_APP.test(key)) 63 | .reduce( 64 | (env, key) => { 65 | env[key] = process.env[key]; 66 | return env; 67 | }, 68 | { 69 | // Useful for determining whether we’re running in production mode. 70 | // Most importantly, it switches React into the correct mode. 71 | NODE_ENV: process.env.NODE_ENV || 'development', 72 | // Useful for resolving the correct path to static assets in `public`. 73 | // For example, . 74 | // This should only be used as an escape hatch. Normally you would put 75 | // images into the `src` and `import` them in code to get their paths. 76 | PUBLIC_URL: publicUrl, 77 | } 78 | ); 79 | // Stringify all values so we can feed into Webpack DefinePlugin 80 | const stringified = { 81 | 'process.env': Object.keys(raw).reduce( 82 | (env, key) => { 83 | env[key] = JSON.stringify(raw[key]); 84 | return env; 85 | }, 86 | {} 87 | ), 88 | }; 89 | 90 | return { raw, stringified }; 91 | } 92 | 93 | module.exports = getClientEnvironment; 94 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebookincubator/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(path, needsSlash) { 15 | const hasSlash = path.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return path.substr(path, path.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${path}/`; 20 | } else { 21 | return path; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right 27 | 28 | 29 | ` 30 | -------------------------------------------------------------------------------- /src/shared/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { 5 | Route, 6 | Switch, 7 | Link 8 | } from 'react-router-dom'; 9 | 10 | import logo from './logo.svg'; 11 | 12 | const Header = () => ( 13 |
14 | logo 15 |

Welcome to Universal React

16 |
17 | ) 18 | 19 | export const PageNotFound = (props, context = {}) => { 20 | if (context.setStatus) { 21 | context.setStatus(404) 22 | } 23 | 24 | return ( 25 |
26 |

27 | Page not found 28 |

29 | 30 | Go home 31 | 32 |
33 | ) 34 | } 35 | PageNotFound.contextTypes = { 36 | setStatus: PropTypes.func.isRequired 37 | } 38 | 39 | const TestRouterPage = ({ match }) => ( 40 |
41 |

42 | Test page {match.params.id} 43 |

44 |

45 | 46 | Home 47 | 48 |

49 |

50 | 51 | Go to non-existent page 52 | 53 |

54 |
55 | ) 56 | 57 | const Home = () => ( 58 |
59 |

To get started, edit src/shared/App.js and save to reload.

60 | 61 | Test the router 62 | 63 |
64 | ) 65 | 66 | const App = () => ( 67 |
68 | ( 69 |
70 |
71 | 72 | 73 | 74 | 75 | 76 |
77 | )}/> 78 |
79 | ) 80 | 81 | export default App 82 | -------------------------------------------------------------------------------- /src/shared/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | }); 9 | -------------------------------------------------------------------------------- /src/shared/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | --------------------------------------------------------------------------------