├── .gitignore ├── LICENSE.md ├── README.md ├── bin └── react-scripts-ssr.js ├── config ├── paths.js └── webpack.config.server.js ├── examples ├── codeSplitting │ ├── .gitignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ ├── src │ │ ├── App.css │ │ ├── App.js │ │ ├── App.test.js │ │ ├── LazyComponent.js │ │ ├── index.css │ │ ├── index.js │ │ ├── logo.svg │ │ ├── server.js │ │ └── serviceWorker.js │ └── yarn.lock └── helloWorld │ ├── .gitignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json │ └── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ ├── server.js │ └── serviceWorker.js ├── index.js ├── package.json ├── scripts ├── build-server.js ├── proxy.js └── start.js ├── src ├── middlewares.js └── render.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present LeanJS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-scripts-ssr 2 | 3 | Create React apps with server-side rendering (SSR) with no configuration 4 | 5 | # Installation 6 | 7 | `npm install react-scripts-ssr --save-dev` 8 | 9 | # Getting started 10 | 11 | ## Steps 12 | 13 | ### Step 1 14 | 15 | In the scripts section of your package.json: 16 | 17 | - Replace `"start": "react-scripts start"` with `"start": "react-scripts-ssr start",` 18 | - Add `"build-server": "react-scripts-ssr build-server",` 19 | 20 | ### Step 2 21 | 22 | - `npm install express --save` 23 | - Create the following file in src/server.js 24 | 25 | ```javascript 26 | const React = require("react"); 27 | const express = require("express"); 28 | const { createSSRMiddleware } = require("react-scripts-ssr"); 29 | const { renderToString } = require("react-dom/server"); 30 | import App from "./App"; 31 | 32 | const server = express(); 33 | 34 | server.use( 35 | createSSRMiddleware((req, res, next) => { 36 | const body = renderToString(); 37 | next({ body }, req, res); 38 | }) 39 | ); 40 | 41 | const PORT = process.env.REACT_APP_SERVER_SIDE_RENDERING_PORT || 8888; 42 | server.listen(PORT, () => { 43 | console.log(`server running on port ${PORT}`); 44 | }); 45 | ``` 46 | 47 | You can edit the server.js file with your custom code and other middlewares. 48 | 49 | ### Step 3 50 | 51 | `npm start` 52 | 53 | ## Caveats 54 | 55 | It only works with Create React App version 2 56 | -------------------------------------------------------------------------------- /bin/react-scripts-ssr.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var spawn = require("cross-spawn"); 3 | const args = process.argv.slice(2); 4 | 5 | const scriptIndex = args.findIndex(x => x === "build-server" || x === "start"); 6 | const script = scriptIndex === -1 ? args[0] : args[scriptIndex]; 7 | const nodeArgs = scriptIndex > 0 ? args.slice(0, scriptIndex) : []; 8 | 9 | switch (script) { 10 | case "proxy": 11 | case "build-server": 12 | case "start": { 13 | const result = spawn.sync( 14 | "node", 15 | nodeArgs 16 | .concat(require.resolve("../scripts/" + script)) 17 | .concat(args.slice(scriptIndex + 1)), 18 | { stdio: "inherit" } 19 | ); 20 | if (result.signal) { 21 | if (result.signal === "SIGKILL") { 22 | console.log( 23 | "The build failed because the process exited too early. " + 24 | "This probably means the system ran out of memory or someone called " + 25 | "`kill -9` on the process." 26 | ); 27 | } else if (result.signal === "SIGTERM") { 28 | console.log( 29 | "The build failed because the process exited too early. " + 30 | "Someone might have called `kill` or `killall`, or the system could " + 31 | "be shutting down." 32 | ); 33 | } 34 | process.exit(1); 35 | } 36 | process.exit(result.status); 37 | break; 38 | } 39 | default: 40 | console.log('Unknown script "' + script + '".'); 41 | console.log("Perhaps you need to update react-scripts?"); 42 | console.log( 43 | "See: https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#updating-to-new-releases" 44 | ); 45 | break; 46 | } 47 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | 4 | const appDirectory = fs.realpathSync(process.cwd()); 5 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 6 | 7 | module.exports = { 8 | serverBuild: resolveApp("build-server"), 9 | serverIndexJs: resolveApp("src/server.js"), 10 | customScriptConfig: resolveApp(".react-scripts-ssr.json") 11 | }; 12 | -------------------------------------------------------------------------------- /config/webpack.config.server.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const pathsServer = require('./paths'); 3 | const paths = require('react-scripts/config/paths'); 4 | const getCacheIdentifier = require('react-dev-utils/getCacheIdentifier'); 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | const ManifestPlugin = require('webpack-manifest-plugin'); 7 | 8 | const resolve = require('resolve'); 9 | 10 | const isEnvDevelopment = process.env.NODE_ENV === 'development' 11 | const isEnvProduction = process.env.NODE_ENV === 'production' 12 | 13 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 14 | const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent'); 15 | 16 | const publicPath = isEnvProduction 17 | ? paths.servedPath 18 | : isEnvDevelopment && '/'; 19 | 20 | // style files regexes 21 | const cssRegex = /\.css$/; 22 | const cssModuleRegex = /\.module\.css$/; 23 | const sassRegex = /\.(scss|sass)$/; 24 | const sassModuleRegex = /\.module\.(scss|sass)$/; 25 | 26 | const shouldUseRelativeAssetPaths = publicPath === './'; 27 | 28 | const getStyleLoaders = (cssOptions, preProcessor) => { 29 | const loaders = [ 30 | isEnvDevelopment && require.resolve('style-loader'), 31 | isEnvProduction && { 32 | loader: MiniCssExtractPlugin.loader, 33 | options: Object.assign( 34 | {}, 35 | shouldUseRelativeAssetPaths ? { publicPath: '../../' } : undefined 36 | ), 37 | }, 38 | { 39 | loader: require.resolve('css-loader'), 40 | options: cssOptions, 41 | }, 42 | { 43 | // Options for PostCSS as we reference these options twice 44 | // Adds vendor prefixing based on your specified browser support in 45 | // package.json 46 | loader: require.resolve('postcss-loader'), 47 | options: { 48 | // Necessary for external CSS imports to work 49 | // https://github.com/facebook/create-react-app/issues/2677 50 | ident: 'postcss', 51 | plugins: () => [ 52 | require('postcss-flexbugs-fixes'), 53 | require('postcss-preset-env')({ 54 | autoprefixer: { 55 | flexbox: 'no-2009', 56 | }, 57 | stage: 3, 58 | }), 59 | ], 60 | sourceMap: isEnvProduction && shouldUseSourceMap, 61 | }, 62 | }, 63 | ].filter(Boolean); 64 | if (preProcessor) { 65 | loaders.push({ 66 | loader: require.resolve(preProcessor), 67 | options: { 68 | sourceMap: isEnvProduction && shouldUseSourceMap, 69 | }, 70 | }); 71 | } 72 | return loaders; 73 | }; 74 | 75 | module.exports = { 76 | mode: 'development', 77 | entry: [pathsServer.serverIndexJs], 78 | devtool: 'source-map', 79 | output: { 80 | path: pathsServer.serverBuild, 81 | publicPath: publicPath, 82 | filename: 'index.js', 83 | }, 84 | module: { 85 | rules: [ 86 | // { 87 | // test: /\.(js|mjs|jsx|ts|tsx)$/, 88 | // include: paths.appSrc, 89 | // loader: require.resolve('babel-loader'), 90 | // options: { 91 | // customize: require.resolve( 92 | // 'babel-preset-react-app/webpack-overrides' 93 | // ), 94 | // // @remove-on-eject-begin 95 | // babelrc: false, 96 | // configFile: false, 97 | // presets: [require.resolve('babel-preset-react-app')], 98 | // // Make sure we have a unique cache identifier, erring on the 99 | // // side of caution. 100 | // plugins: [ 101 | // [ 102 | // require.resolve('babel-plugin-named-asset-import'), 103 | // { 104 | // loaderMap: { 105 | // svg: { 106 | // ReactComponent: 107 | // '@svgr/webpack?-prettier,-svgo![path]', 108 | // }, 109 | // }, 110 | // }, 111 | // ], 112 | // ], 113 | // // This is a feature of `babel-loader` for webpack (not Babel itself). 114 | // // It enables caching results in ./node_modules/.cache/babel-loader/ 115 | // // directory for faster rebuilds. 116 | // cacheDirectory: true, 117 | // cacheCompression: isEnvProduction, 118 | // compact: isEnvProduction, 119 | // }, 120 | // }, 121 | // // Process any JS outside of the app with Babel. 122 | // // Unlike the application JS, we only compile the standard ES features. 123 | // { 124 | // test: /\.(js|mjs)$/, 125 | // exclude: /@babel(?:\/|\\{1,2})runtime/, 126 | // loader: require.resolve('babel-loader'), 127 | // options: { 128 | // babelrc: false, 129 | // configFile: false, 130 | // compact: false, 131 | // presets: [ 132 | // [ 133 | // require.resolve('babel-preset-react-app/dependencies'), 134 | // { helpers: true }, 135 | // ], 136 | // ], 137 | // cacheDirectory: true, 138 | // cacheCompression: isEnvProduction, 139 | // // If an error happens in a package, it's possible to be 140 | // // because it was compiled. Thus, we don't want the browser 141 | // // debugger to show the original code. Instead, the code 142 | // // being evaluated would be much more helpful. 143 | // sourceMaps: false, 144 | // }, 145 | // }, 146 | { 147 | test: /\.(js|jsx)$/, 148 | include: paths.appSrc, 149 | exclude: /node_modules/, 150 | //loader: require.resolve('babel-loader'), 151 | use: { 152 | loader: "babel-loader", 153 | options: { 154 | // babelrc: false, 155 | // configFile: false, 156 | //presets: [require.resolve('babel-preset-react-app')], 157 | "presets": ["react-app"], 158 | // Make sure we have a unique cache identifier, erring on the 159 | // side of caution. 160 | // We remove this when the user ejects because the default 161 | // is sane and uses Babel options. Instead of options, we use 162 | // the react-scripts and babel-preset-react-app versions. 163 | // cacheIdentifier: getCacheIdentifier('production', [ 164 | // 'babel-plugin-named-asset-import', 165 | // 'babel-preset-react-app', 166 | // 'react-dev-utils', 167 | // 'react-scripts', 168 | // ]), 169 | // @remove-on-eject-end 170 | plugins: [ 171 | [ 172 | require.resolve('babel-plugin-named-asset-import'), 173 | { 174 | loaderMap: { 175 | svg: { 176 | ReactComponent: '@svgr/webpack?-prettier,-svgo![path]', 177 | }, 178 | }, 179 | }, 180 | ], 181 | ], 182 | //cacheDirectory: true, 183 | // Save disk space when time isn't as important 184 | //cacheCompression: false, 185 | //compact: false, 186 | }, 187 | }, 188 | }, 189 | { 190 | // "oneOf" will traverse all following loaders until one will 191 | // match the requirements. When no loader matches it will fall 192 | // back to the "file" loader at the end of the loader list. 193 | oneOf: [ 194 | // "url" loader works like "file" loader except that it embeds assets 195 | // smaller than specified limit in bytes as data URLs to avoid requests. 196 | // A missing `test` is equivalent to a match. 197 | { 198 | test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/], 199 | loader: require.resolve('url-loader'), 200 | options: { 201 | limit: 10000, 202 | name: 'static/media/[name].[hash:8].[ext]', 203 | }, 204 | }, 205 | // Process application JS with Babel. 206 | // The preset includes JSX, Flow, TypeScript, and some ESnext features. 207 | { 208 | test: /\.(js|mjs|jsx|ts|tsx)$/, 209 | include: paths.appSrc, 210 | loader: require.resolve('babel-loader'), 211 | options: { 212 | customize: require.resolve( 213 | 'babel-preset-react-app/webpack-overrides' 214 | ), 215 | // @remove-on-eject-begin 216 | babelrc: false, 217 | configFile: false, 218 | presets: [require.resolve('babel-preset-react-app')], 219 | // Make sure we have a unique cache identifier, erring on the 220 | // side of caution. 221 | // We remove this when the user ejects because the default 222 | // is sane and uses Babel options. Instead of options, we use 223 | // the react-scripts and babel-preset-react-app versions. 224 | cacheIdentifier: getCacheIdentifier( 225 | isEnvProduction 226 | ? 'production' 227 | : isEnvDevelopment && 'development', 228 | [ 229 | 'babel-plugin-named-asset-import', 230 | 'babel-preset-react-app', 231 | 'react-dev-utils', 232 | 'react-scripts', 233 | ] 234 | ), 235 | // @remove-on-eject-end 236 | plugins: [ 237 | [ 238 | require.resolve('babel-plugin-named-asset-import'), 239 | { 240 | loaderMap: { 241 | svg: { 242 | ReactComponent: 243 | '@svgr/webpack?-prettier,-svgo![path]', 244 | }, 245 | }, 246 | }, 247 | ], 248 | ], 249 | // This is a feature of `babel-loader` for webpack (not Babel itself). 250 | // It enables caching results in ./node_modules/.cache/babel-loader/ 251 | // directory for faster rebuilds. 252 | cacheDirectory: true, 253 | cacheCompression: isEnvProduction, 254 | compact: isEnvProduction, 255 | }, 256 | }, 257 | // Process any JS outside of the app with Babel. 258 | // Unlike the application JS, we only compile the standard ES features. 259 | { 260 | test: /\.(js|mjs)$/, 261 | exclude: /@babel(?:\/|\\{1,2})runtime/, 262 | loader: require.resolve('babel-loader'), 263 | options: { 264 | babelrc: false, 265 | configFile: false, 266 | compact: false, 267 | presets: [ 268 | [ 269 | require.resolve('babel-preset-react-app/dependencies'), 270 | { helpers: true }, 271 | ], 272 | ], 273 | cacheDirectory: true, 274 | cacheCompression: isEnvProduction, 275 | // If an error happens in a package, it's possible to be 276 | // because it was compiled. Thus, we don't want the browser 277 | // debugger to show the original code. Instead, the code 278 | // being evaluated would be much more helpful. 279 | sourceMaps: false, 280 | }, 281 | }, 282 | // // "postcss" loader applies autoprefixer to our CSS. 283 | // // "css" loader resolves paths in CSS and adds assets as dependencies. 284 | // // "style" loader turns CSS into JS modules that inject