├── .prettierignore
├── .env.example
├── .gitignore
├── .eslintignore
├── src
├── layouts
│ └── CoreLayout
│ │ ├── CoreLayout.css
│ │ ├── index.js
│ │ └── CoreLayout.js
├── routes
│ ├── Home
│ │ ├── index.js
│ │ ├── back.png
│ │ ├── HomeView.css
│ │ ├── TestView.js
│ │ └── HomeView.js
│ ├── NotFoundPage
│ │ ├── NotFoundPage.css
│ │ ├── index.js
│ │ └── NotFoundPage.js
│ └── index.js
├── components
│ ├── RouterStatus
│ │ ├── index.js
│ │ └── RouterStatus.js
│ └── RedirectWithStatus
│ │ ├── index.js
│ │ └── RedirectWithStatus.js
├── constant.js
├── url.js
├── utils
│ └── fetch.js
└── renderer
│ ├── root.js
│ ├── server.js
│ ├── client.js
│ ├── serverRenderer.js
│ └── app.js
├── .prettierrc
├── .travis.yml
├── jsconfig.json
├── babel.config.js
├── config
├── env.config.js
├── webpack.server.config.babel.js
├── webpack.base.config.js
├── project.config.js
└── webpack.client.config.babel.js
├── .client.babelrc
├── .server.babelrc
├── .eslintrc
├── README.md
└── package.json
/.prettierignore:
--------------------------------------------------------------------------------
1 | .babelrc
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | APP_BACKEND_URL=https://example.com
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 | public
4 | dist
5 | .env
6 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/**
2 | build/**
3 | public/**
4 | dist/**
5 |
--------------------------------------------------------------------------------
/src/layouts/CoreLayout/CoreLayout.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | }
4 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "singleQuote": false,
4 | "semi": false
5 | }
6 |
--------------------------------------------------------------------------------
/src/routes/Home/index.js:
--------------------------------------------------------------------------------
1 | import HomeView from "./HomeView"
2 |
3 | export default HomeView
4 |
--------------------------------------------------------------------------------
/src/routes/NotFoundPage/NotFoundPage.css:
--------------------------------------------------------------------------------
1 | .not-found-page {
2 | background-color: aquamarine;
3 | }
4 |
--------------------------------------------------------------------------------
/src/routes/Home/back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonybudianto/react-ssr-starter/HEAD/src/routes/Home/back.png
--------------------------------------------------------------------------------
/src/components/RouterStatus/index.js:
--------------------------------------------------------------------------------
1 | import RouterStatus from './RouterStatus'
2 |
3 | export default RouterStatus
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "10.1"
4 | - "8.11"
5 | script:
6 | - yarn lint
7 | - yarn deploy:prod
8 |
--------------------------------------------------------------------------------
/src/components/RedirectWithStatus/index.js:
--------------------------------------------------------------------------------
1 | import RedirectWithStatus from './RedirectWithStatus'
2 |
3 | export default RedirectWithStatus
4 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "experimentalDecorators": true
5 | }
6 | }
--------------------------------------------------------------------------------
/src/routes/Home/HomeView.css:
--------------------------------------------------------------------------------
1 | .home-view {
2 | color: white;
3 | padding: 100px;
4 | background-size: cover;
5 | background: url('./back.png');
6 | }
--------------------------------------------------------------------------------
/src/layouts/CoreLayout/index.js:
--------------------------------------------------------------------------------
1 | import CoreLayout from "./CoreLayout"
2 | import { hot } from "react-hot-loader/root"
3 |
4 | export default hot(CoreLayout)
5 |
--------------------------------------------------------------------------------
/src/constant.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Make sure it has slash '/' at the end
3 | */
4 |
5 | export const ASSET_URL_STAG = '/'
6 | export const ASSET_URL_PROD = '/'
7 | export const ASSET_URL_DEV = '/'
8 |
--------------------------------------------------------------------------------
/src/routes/NotFoundPage/index.js:
--------------------------------------------------------------------------------
1 | // import NotFoundPage from './NotFoundPage'
2 | import loadable from "@loadable/component"
3 |
4 | const NotFoundPage = loadable(() => import("./NotFoundPage"))
5 |
6 | export default NotFoundPage
7 |
--------------------------------------------------------------------------------
/src/url.js:
--------------------------------------------------------------------------------
1 | import { ASSET_URL_PROD, ASSET_URL_STAG, ASSET_URL_DEV } from './constant'
2 |
3 | let assetUrl = ASSET_URL_PROD
4 |
5 | if (__DEV__) {
6 | assetUrl = ASSET_URL_DEV
7 | } else if (__STAG__) {
8 | assetUrl = ASSET_URL_STAG
9 | }
10 |
11 | export const HOME_PATH = '/'
12 | export const ASSET_URL = assetUrl
13 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | [
4 | "@babel/preset-env",
5 | {
6 | targets: {
7 | browsers: ["last 2 versions"]
8 | }
9 | }
10 | ],
11 | "@babel/preset-react"
12 | ],
13 | plugins: ["@babel/plugin-proposal-class-properties", "@loadable/babel-plugin"]
14 | }
15 |
--------------------------------------------------------------------------------
/src/utils/fetch.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | export const fetch = async (url, headers) => {
4 | let header = {
5 | ...headers,
6 | url: url,
7 | withCredentials: 'true'
8 | }
9 | try {
10 | let data = await axios.request(header)
11 | return data.data
12 | } catch (err) {
13 | throw err.response
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/config/env.config.js:
--------------------------------------------------------------------------------
1 | import { ASSET_URL_DEV, ASSET_URL_PROD, ASSET_URL_STAG } from "../src/constant"
2 | import config from "./project.config"
3 |
4 | let assetUrl = ASSET_URL_DEV
5 |
6 | if (config.globals.__STAG__) {
7 | assetUrl = ASSET_URL_STAG
8 | } else if (config.globals.__PROD__) {
9 | assetUrl = ASSET_URL_PROD
10 | }
11 |
12 | export default {
13 | assetUrl
14 | }
15 |
--------------------------------------------------------------------------------
/.client.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/preset-env",
5 | {
6 | "modules": false,
7 | "targets": {
8 | "browsers": ["last 2 versions"]
9 | }
10 | }
11 | ],
12 | "@babel/preset-react"
13 | ],
14 | "plugins": [
15 | "react-hot-loader/babel",
16 | "@babel/plugin-proposal-class-properties",
17 | "@loadable/babel-plugin"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/.server.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/preset-env",
5 | {
6 | "modules": false,
7 | "targets": {
8 | "browsers": ["last 2 versions"]
9 | }
10 | }
11 | ],
12 | "@babel/preset-react"
13 | ],
14 | "plugins": [
15 | "@babel/plugin-syntax-dynamic-import",
16 | "@babel/plugin-proposal-class-properties",
17 | "@loadable/babel-plugin"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/src/renderer/root.js:
--------------------------------------------------------------------------------
1 | import "react-hot-loader"
2 | import React from "react"
3 | import { BrowserRouter } from "react-router-dom"
4 | import { HelmetProvider } from "react-helmet-async"
5 | import CoreLayout from "../layouts/CoreLayout"
6 |
7 | const Root = () => {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 | )
15 | }
16 |
17 | export default Root
18 |
--------------------------------------------------------------------------------
/src/renderer/server.js:
--------------------------------------------------------------------------------
1 | import app from './app'
2 | import { serverPort, serverHost } from '../../config/project.config'
3 |
4 | const httpServer = app.listen(serverPort, serverHost, error => {
5 | if (error) {
6 | console.error(error)
7 | } else {
8 | const address = httpServer.address()
9 | console.info(
10 | `==> 🌎 Listening on ${address.port}. Open up http://localhost:${
11 | address.port
12 | }/ in your browser.`
13 | )
14 | }
15 | })
16 |
--------------------------------------------------------------------------------
/src/routes/NotFoundPage/NotFoundPage.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { Helmet } from "react-helmet-async"
3 |
4 | import RouterStatus from "../../components/RouterStatus"
5 | import "./NotFoundPage.css"
6 |
7 | const NotFoundPage = () => (
8 |
9 |
10 |
11 |
12 | Sorry, Page Not found
13 |
14 | )
15 |
16 | export default NotFoundPage
17 |
--------------------------------------------------------------------------------
/src/routes/Home/TestView.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 |
3 | class TestView extends Component {
4 | state = {
5 | count: 0
6 | }
7 |
8 | componentDidMount() {
9 | this.timer = setInterval(() => {
10 | this.setState({
11 | count: this.state.count + 1
12 | })
13 | }, 1000)
14 | }
15 |
16 | componentWillUnmount() {
17 | clearInterval(this.timer)
18 | }
19 |
20 | render() {
21 | return
Count: {this.state.count}
22 | }
23 | }
24 |
25 | export default TestView
26 |
--------------------------------------------------------------------------------
/src/components/RouterStatus/RouterStatus.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { Route } from "react-router-dom"
3 | import PropTypes from "prop-types"
4 |
5 | const RouterStatus = ({ code, children }) => (
6 | {
8 | if (staticContext) {
9 | staticContext.status = code
10 | }
11 | return children
12 | }}
13 | />
14 | )
15 |
16 | RouterStatus.propTypes = {
17 | code: PropTypes.number,
18 | children: PropTypes.node
19 | }
20 |
21 | export default RouterStatus
22 |
--------------------------------------------------------------------------------
/src/routes/index.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { Switch, Route } from "react-router-dom"
3 |
4 | import HomeView from "./Home"
5 | import NotFoundPage from "./NotFoundPage"
6 |
7 | // eslint-disable-next-line no-unused-vars
8 | export const getInitialData = (req, store) => {
9 | return []
10 | }
11 |
12 | export default function initRenderRoutes() {
13 | return (
14 |
15 |
16 |
17 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/src/renderer/client.js:
--------------------------------------------------------------------------------
1 | import "raf/polyfill"
2 | import React from "react"
3 | import { hydrate } from "react-dom"
4 | import { loadableReady } from "@loadable/component"
5 |
6 | import Root from "./root"
7 | import "basscss/css/basscss.css"
8 |
9 | function render(MyApp) {
10 | hydrate(, document.querySelector("#root"))
11 | }
12 |
13 | loadableReady(() => {
14 | render(Root)
15 | })
16 |
17 | // if (module.hot) {
18 | // module.hot.accept("../layouts/CoreLayout", () => {
19 | // const MyApp = require("../layouts/CoreLayout").default
20 | // render(MyApp)
21 | // })
22 | // }
23 |
--------------------------------------------------------------------------------
/src/components/RedirectWithStatus/RedirectWithStatus.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Route, Redirect } from 'react-router-dom'
3 | import PropTypes from 'prop-types'
4 |
5 | const RedirectWithStatus = ({ from, to, status }) => (
6 | {
8 | // there is no `staticContext` on the client, so
9 | // we need to guard against that here
10 | if (staticContext) {
11 | staticContext.status = status
12 | }
13 | return
14 | }}
15 | />
16 | )
17 |
18 | RedirectWithStatus.propTypes = {
19 | from: PropTypes.string,
20 | to: PropTypes.string,
21 | status: PropTypes.number
22 | }
23 |
24 | export default RedirectWithStatus
25 |
--------------------------------------------------------------------------------
/src/routes/Home/HomeView.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react"
2 | import PropTypes from "prop-types"
3 |
4 | import "./HomeView.css"
5 |
6 | const HomeView = () => {
7 | const [count, setCount] = useState(0)
8 | return (
9 |
10 |
home {count}
11 |
12 |
19 |
20 |
21 | )
22 | }
23 |
24 | HomeView.propTypes = {
25 | user: PropTypes.object,
26 | toggleLogin: PropTypes.func
27 | }
28 |
29 | export default HomeView
30 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": [
4 | "eslint:recommended",
5 | "plugin:react/recommended",
6 | "plugin:jest/recommended"
7 | ],
8 | "plugins": ["babel", "react", "jest"],
9 | "env": {
10 | "jest/globals": true,
11 | "browser": true
12 | },
13 | "globals": {
14 | "__DEV__": false,
15 | "__STAG__": false,
16 | "__PROD__": false,
17 | "jest": false,
18 | "process": false,
19 | "module": false,
20 | "__dirname": false,
21 | "Promise": false
22 | },
23 | "rules": {
24 | "space-before-function-paren": 0,
25 | "key-spacing": 0,
26 | "no-unused-vars": 2,
27 | "jsx-quotes": [2, "prefer-double"],
28 | "max-len": [2, 120, 2],
29 | "object-curly-spacing": [2, "always"],
30 | "no-console": 0
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/layouts/CoreLayout/CoreLayout.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react"
2 | import { Helmet } from "react-helmet-async"
3 |
4 | import renderRoutes from "../../routes"
5 |
6 | import "./CoreLayout.css"
7 |
8 | class CoreLayout extends Component {
9 | render() {
10 | return (
11 |
12 |
13 | React App
14 |
15 |
16 |
17 |
18 |
19 |
{renderRoutes()}
20 |
21 | )
22 | }
23 | }
24 |
25 | export default CoreLayout
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-ssr-starter
2 |
3 | [](https://greenkeeper.io/)
4 | [](https://travis-ci.com/antonybudianto/react-ssr-starter)
5 |
6 | > Personal and opinionated React Fullstack starter.
7 |
8 | ## Features:
9 |
10 | - SSR (Server side rendering)
11 | - Code-splitting
12 | - HMR (Hot module reload, **both** client and server)
13 | - Webpack 4, ESLint, Jest, Prettier
14 |
15 | ## How to use
16 |
17 | 1. Start
18 | ```
19 | npm start
20 | ```
21 |
22 | 2. Build
23 | ```
24 | npm run deploy:prod
25 | ```
26 |
27 | ## Alternatives
28 | - Do you use [create-react-app](https://github.com/facebook/create-react-app)?
29 | - Try on [CRA Universal](https://github.com/antonybudianto/cra-universal) for SSR, without eject!
30 |
31 | ## License
32 | MIT
33 |
--------------------------------------------------------------------------------
/config/webpack.server.config.babel.js:
--------------------------------------------------------------------------------
1 | const nodeExternals = require('webpack-node-externals')
2 | const merge = require('webpack-merge')
3 | const baseConfig = require('./webpack.base.config')
4 | const project = require('./project.config')
5 | const { assetUrl } = require('./env.config').default
6 | const path = require('path')
7 | const StartServerPlugin = require('start-server-webpack-plugin')
8 |
9 | let config = {
10 | target: 'node',
11 | node: {
12 | fs: 'empty',
13 | __dirname: false,
14 | __filename: false
15 | },
16 | module: {
17 | rules: [
18 | {
19 | test: /\.js?$/,
20 | exclude: /node_modules/,
21 | loader: 'babel-loader',
22 | options: {
23 | babelrc: false,
24 | extends: path.resolve(__dirname, '../.server.babelrc')
25 | }
26 | },
27 | {
28 | test: /\.s?[ac]ss$/,
29 | use: 'null-loader'
30 | }
31 | ]
32 | },
33 | entry: {
34 | bundle: project.paths.client('renderer/server')
35 | },
36 | externals: [nodeExternals()],
37 | output: {
38 | filename: '[name].js',
39 | publicPath: assetUrl,
40 | path: project.paths.dist()
41 | }
42 | }
43 |
44 | if (project.globals.__DEV__) {
45 | const addConfig = {
46 | plugins: [
47 | new StartServerPlugin({
48 | entryName: 'bundle'
49 | })
50 | ]
51 | }
52 | config = merge(config, addConfig)
53 | }
54 |
55 | module.exports = merge(config, baseConfig)
56 |
--------------------------------------------------------------------------------
/src/renderer/serverRenderer.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { renderToString } from "react-dom/server"
3 | import { StaticRouter } from "react-router-dom"
4 | import path from "path"
5 | import { ChunkExtractor } from "@loadable/server"
6 |
7 | import { HelmetProvider } from "react-helmet-async"
8 |
9 | import CoreLayout from "../layouts/CoreLayout"
10 |
11 | export default async (locPath, store, context) => {
12 | const helmetCtx = {}
13 |
14 | const statsFile = path.resolve(
15 | __dirname,
16 | path.resolve("./dist/loadable-stats.json")
17 | )
18 | const extractor = new ChunkExtractor({ statsFile, entrypoints: ["app"] })
19 |
20 | const App = (
21 |
22 |
23 |
24 |
25 |
26 | )
27 |
28 | const jsx = extractor.collectChunks(App)
29 | const content = renderToString(jsx)
30 |
31 | const helmet = helmetCtx.helmet
32 |
33 | return `
34 |
35 |
36 | ${helmet.title.toString()}
37 |
38 |
39 |
40 | ${helmet.meta.toString()}
41 | ${extractor.getStyleTags()}
42 |
43 | ${helmet.link.toString()}
44 |
45 |
46 | ${content}
47 | ${extractor.getScriptTags()}
48 |
49 | `
50 | }
51 |
--------------------------------------------------------------------------------
/config/webpack.base.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require("webpack")
2 | const UglifyJsPlugin = require("uglifyjs-webpack-plugin")
3 | const project = require("./project.config")
4 | const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin")
5 |
6 | const devMode = project.globals.__DEV__
7 | let config = {
8 | mode: devMode ? "development" : "production",
9 | stats: {
10 | chunks: true,
11 | chunkModules: true,
12 | colors: true,
13 | children: false
14 | },
15 | module: {
16 | rules: [
17 | {
18 | test: /\.woff(\?.*)?$/,
19 | loader:
20 | "url-loader?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=application/font-woff"
21 | },
22 | {
23 | test: /\.woff2(\?.*)?$/,
24 | loader:
25 | "url-loader?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=application/font-woff2"
26 | },
27 | {
28 | test: /\.otf(\?.*)?$/,
29 | loader:
30 | "file-loader?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=font/opentype"
31 | },
32 | {
33 | test: /\.ttf(\?.*)?$/,
34 | loader:
35 | "url-loader?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=application/octet-stream"
36 | },
37 | {
38 | test: /\.eot(\?.*)?$/,
39 | loader: "file-loader?prefix=fonts/&name=[path][name].[ext]"
40 | },
41 | {
42 | test: /\.svg(\?.*)?$/,
43 | loader:
44 | "url-loader?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=image/svg+xml"
45 | },
46 | {
47 | test: /\.(png|jpg)$/,
48 | loader: "url-loader?limit=8192"
49 | }
50 | ]
51 | },
52 | optimization: {
53 | minimizer: [
54 | new UglifyJsPlugin({
55 | uglifyOptions: {
56 | compress: true
57 | }
58 | }),
59 | new OptimizeCSSAssetsPlugin({})
60 | ]
61 | },
62 | plugins: [new webpack.DefinePlugin(project.globals)]
63 | }
64 |
65 | console.log("webpack mode: ", config.mode)
66 |
67 | module.exports = config
68 |
--------------------------------------------------------------------------------
/config/project.config.js:
--------------------------------------------------------------------------------
1 | /* eslint key-spacing:0 spaced-comment:0 */
2 | const path = require('path')
3 |
4 | console.log('Creating default configuration.')
5 | // ========================================================
6 | // Default Configuration
7 | // ========================================================
8 | const config = {
9 | env: process.env.NODE_ENV || 'development',
10 |
11 | // ----------------------------------
12 | // Project Structure
13 | // ----------------------------------
14 | path_base: path.resolve(__dirname, '..'),
15 | dir_client: path.resolve(__dirname, '../src'),
16 | dir_dist: path.resolve(__dirname, '../dist'),
17 | dir_public: path.resolve(__dirname, '../public'),
18 |
19 | // ----------------------------------
20 | // Server Configuration
21 | // ----------------------------------
22 | // serverHost: ip.address(), // use string 'localhost' to prevent exposure on local network
23 | serverHost: 'localhost',
24 | serverPort: process.env.PORT || 3000
25 | }
26 |
27 | /************************************************
28 | -------------------------------------------------
29 |
30 | All Internal Configuration Below
31 | Edit at Your Own Risk
32 |
33 | -------------------------------------------------
34 | ************************************************/
35 |
36 | // ------------------------------------
37 | // Environment
38 | // ------------------------------------
39 | // N.B.: globals added here must _also_ be added to .eslintrc
40 | config.globals = {
41 | 'process.env.NODE_ENV': JSON.stringify(config.env),
42 | __DEV__: config.env === 'development',
43 | __STAG__: config.env === 'staging',
44 | __PROD__: config.env === 'production'
45 | }
46 |
47 | // ------------------------------------
48 | // Utilities
49 | // ------------------------------------
50 | function base() {
51 | const args = [config.path_base].concat([].slice.call(arguments))
52 | return path.resolve.apply(path, args)
53 | }
54 |
55 | config.paths = {
56 | base: base,
57 | client: base.bind(null, config.dir_client),
58 | public: base.bind(null, config.dir_public),
59 | dist: base.bind(null, config.dir_dist)
60 | }
61 |
62 | // ========================================================
63 | // Environment Configuration
64 | // ========================================================
65 | console.log(`Environment NODE_ENV "${config.env}".`)
66 |
67 | module.exports = config
68 |
--------------------------------------------------------------------------------
/config/webpack.client.config.babel.js:
--------------------------------------------------------------------------------
1 | const merge = require("webpack-merge")
2 | const baseConfig = require("./webpack.base.config")
3 | const project = require("./project.config")
4 | const { assetUrl } = require("./env.config").default
5 | const path = require("path")
6 | const webpack = require("webpack")
7 | const MiniCssExtractPlugin = require("mini-css-extract-plugin")
8 | const ManifestPlugin = require("webpack-manifest-plugin")
9 | const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
10 | .BundleAnalyzerPlugin
11 | const LoadablePlugin = require("@loadable/webpack-plugin")
12 |
13 | const devMode = project.globals.__DEV__
14 |
15 | const config = {
16 | devtool: project.globals.__PROD__ ? false : "source-map",
17 | entry: {
18 | app: [
19 | ...(project.globals.__DEV__
20 | ? [
21 | "react-hot-loader/patch",
22 | "webpack-hot-middleware/client?timeout=1000&reload=true"
23 | ]
24 | : []),
25 | project.paths.client("renderer/client")
26 | ]
27 | },
28 | module: {
29 | rules: [
30 | {
31 | test: /\.js?$/,
32 | exclude: /node_modules/,
33 | loader: "babel-loader",
34 | options: {
35 | babelrc: false,
36 | extends: path.resolve(__dirname, "../.client.babelrc")
37 | }
38 | },
39 | {
40 | test: /\.css$/,
41 | use: [
42 | devMode ? "style-loader" : MiniCssExtractPlugin.loader,
43 | {
44 | loader: "css-loader",
45 | options: {
46 | minimize: true,
47 | url: true
48 | }
49 | }
50 | ]
51 | }
52 | ]
53 | },
54 | output: {
55 | filename: project.globals.__DEV__ ? "[name].js" : `[name].[chunkhash].js`,
56 | publicPath: assetUrl,
57 | path: project.paths.dist()
58 | },
59 | plugins: [
60 | ...(project.globals.__DEV__
61 | ? [new webpack.HotModuleReplacementPlugin()]
62 | : [
63 | new ManifestPlugin(),
64 | new BundleAnalyzerPlugin({
65 | analyzerMode: "static",
66 | openAnalyzer: false
67 | })
68 | ]),
69 | new LoadablePlugin({
70 | writeToDisk: true
71 | }),
72 | new MiniCssExtractPlugin({
73 | filename: devMode ? "[name].css" : "[name].[hash].css",
74 | chunkFilename: devMode ? "[id].css" : "[id].[hash].css"
75 | })
76 | ],
77 | optimization: {
78 | splitChunks: {
79 | cacheGroups: {
80 | default: false,
81 | commons: {
82 | test: /[\\/]node_modules[\\/].+\.js$/,
83 | name: "vendor",
84 | chunks: "all"
85 | }
86 | }
87 | }
88 | }
89 | }
90 |
91 | module.exports = merge(config, baseConfig)
92 |
--------------------------------------------------------------------------------
/src/renderer/app.js:
--------------------------------------------------------------------------------
1 | import "@babel/polyfill"
2 |
3 | import express from "express"
4 | import morgan from "morgan"
5 | import proxy from "http-proxy-middleware"
6 |
7 | import serverRender from "./serverRenderer"
8 | import { getInitialData } from "../routes"
9 | import { HOME_PATH } from "../url"
10 |
11 | const app = express()
12 | const loggerEnv = __DEV__ ? "dev" : "combined"
13 | const logger = morgan(loggerEnv, {
14 | skip: function(req, res) {
15 | if (__DEV__) {
16 | return false
17 | }
18 | return res.statusCode < 400
19 | }
20 | })
21 |
22 | app.use(logger)
23 |
24 | let devAssets = {
25 | appJs: "",
26 | vendorJs: "",
27 | appCss: ""
28 | }
29 |
30 | if (__DEV__) {
31 | var webpack = require("webpack")
32 | var webpackConfig = require("../../config/webpack.client.config.babel")
33 | var compiler = webpack(webpackConfig)
34 |
35 | app.use(
36 | require("webpack-dev-middleware")(compiler, {
37 | serverSideRender: true,
38 | publicPath: webpackConfig.output.publicPath
39 | })
40 | )
41 | app.use(require("webpack-hot-middleware")(compiler))
42 | }
43 |
44 | app.use(HOME_PATH, express.static("dist"))
45 |
46 | if (__DEV__) {
47 | const backendUrl = process.env.APP_BACKEND_URL || "https://example.com"
48 | console.log("APP_BACKEND_URL = " + backendUrl)
49 |
50 | app.use(
51 | ["/api"],
52 | proxy({
53 | secure: false,
54 | target: backendUrl,
55 | changeOrigin: true,
56 | prependPath: false
57 | })
58 | )
59 | }
60 |
61 | app.get(HOME_PATH + "(*)", (req, res) => {
62 | if (__DEV__) {
63 | const assetsByChunkName = res.locals.webpackStats.toJson().assetsByChunkName
64 | devAssets.appJs = assetsByChunkName.app.find(f =>
65 | /^app(\.[a-z0-9]+)?\.js$/.test(f)
66 | )
67 | devAssets.appCss = assetsByChunkName.app.find(f =>
68 | /^app(\.[a-z0-9]+)?\.css$/.test(f)
69 | )
70 | devAssets.vendorJs = assetsByChunkName.vendor.find(f =>
71 | /^vendor(\.[a-z0-9]+)?\.js$/.test(f)
72 | )
73 | devAssets.vendorCss = assetsByChunkName.vendor.find(f =>
74 | /^vendor(\.[a-z0-9]+)?\.css$/.test(f)
75 | )
76 | }
77 |
78 | // attach cookies to store object as a way to let cookies to be passed into server fetching
79 | // req.headers.cookie && (store["cookies"] = req.headers.cookie)
80 | const path = req.path
81 | const promises = getInitialData(req, {})
82 | Promise.all(promises)
83 | .then(() => {
84 | let context = {}
85 | serverRender(path, {}, context, devAssets).then(html => {
86 | if (context.status === 404) {
87 | return res.status(404).send(html)
88 | }
89 | if (context.url) {
90 | return res.redirect(302, context.url)
91 | }
92 | res.send(html)
93 | })
94 | })
95 | .catch(err => {
96 | console.log("Error fetching", err)
97 | res.status(500).send("Error")
98 | })
99 | })
100 |
101 | if (module.hot) {
102 | module.hot.accept("./serverRenderer", () => {
103 | console.log("✅ Server hot reloaded ./serverRenderer")
104 | })
105 | module.hot.accept("../routes", () => {
106 | console.log("✅ Server hot reloaded ../routes")
107 | })
108 | }
109 |
110 | export default app
111 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-ssr-starter",
3 | "version": "1.0.0",
4 | "description": "react ssr starter",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "better-npm-run start",
8 | "clean": "rimraf dist",
9 | "lint": "eslint config src",
10 | "test": "jest",
11 | "deploy": "better-npm-run deploy",
12 | "dev:server": "better-npm-run dev:server",
13 | "build:client": "better-npm-run build:client",
14 | "build:server": "better-npm-run build:server",
15 | "deploy:dev": "better-npm-run deploy:dev",
16 | "deploy:staging": "better-npm-run deploy:staging",
17 | "deploy:prod": "better-npm-run deploy:prod",
18 | "start:dev": "node dist/bundle.js",
19 | "start:prod": "npm run deploy:prod && npm run start:dev",
20 | "prettier": "prettier --write \"{src,config}/**/*.js\""
21 | },
22 | "betterScripts": {
23 | "start": {
24 | "command": "npm run clean && npm run dev:server",
25 | "env": {
26 | "NODE_ENV": "development"
27 | }
28 | },
29 | "dev:server": {
30 | "command": "webpack --config ./config/webpack.server.config.babel.js --hot --watch"
31 | },
32 | "build:server": {
33 | "command": "webpack --config ./config/webpack.server.config.babel.js --progress"
34 | },
35 | "build:client": {
36 | "command": "webpack --config ./config/webpack.client.config.babel.js --progress"
37 | },
38 | "deploy": {
39 | "command": "npm run clean && npm-run-all build:*"
40 | },
41 | "deploy:dev": {
42 | "command": "npm run deploy",
43 | "env": {
44 | "NODE_ENV": "development"
45 | }
46 | },
47 | "deploy:staging": {
48 | "command": "npm run deploy",
49 | "env": {
50 | "NODE_ENV": "staging"
51 | }
52 | },
53 | "deploy:prod": {
54 | "command": "npm run deploy",
55 | "env": {
56 | "NODE_ENV": "production"
57 | }
58 | }
59 | },
60 | "author": "Antony",
61 | "license": "MIT",
62 | "jest": {
63 | "globals": {
64 | "__DEV__": false,
65 | "__TEST__": true,
66 | "__PROD__": false,
67 | "__STAG__": false
68 | }
69 | },
70 | "dependencies": {
71 | "@babel/core": "7.14.6",
72 | "@babel/plugin-syntax-dynamic-import": "7.8.3",
73 | "@babel/polyfill": "7.12.1",
74 | "@babel/preset-env": "7.14.7",
75 | "@babel/preset-react": "7.14.5",
76 | "@babel/register": "7.14.5",
77 | "@loadable/component": "5.15.0",
78 | "@loadable/server": "^5.15.0",
79 | "axios": "0.18.0",
80 | "babel-loader": "8.2.2",
81 | "basscss": "^8.0.4",
82 | "better-npm-run": "^0.1.0",
83 | "classnames": "^2.2.5",
84 | "css-loader": "^0.28.11",
85 | "dotenv": "6.0.0",
86 | "express": "4.16.3",
87 | "file-loader": "^1.1.11",
88 | "mini-css-extract-plugin": "0.4.1",
89 | "morgan": "1.9.0",
90 | "npm-run-all": "4.1.3",
91 | "null-loader": "0.1.1",
92 | "optimize-css-assets-webpack-plugin": "^4.0.2",
93 | "prop-types": "^15.6.1",
94 | "query-string": "^6.1.0",
95 | "raf": "3.4.0",
96 | "react": "17.0.2",
97 | "react-dom": "17.0.2",
98 | "react-helmet-async": "1.0.2",
99 | "react-hot-loader": "4.13.0",
100 | "react-router-dom": "5.2.0",
101 | "rimraf": "^2.6.2",
102 | "style-loader": "^0.21.0",
103 | "url-loader": "^1.0.1",
104 | "webpack": "4",
105 | "webpack-cli": "^3.0.2",
106 | "webpack-merge": "4.1.3"
107 | },
108 | "devDependencies": {
109 | "@babel/plugin-proposal-class-properties": "7.14.5",
110 | "@loadable/babel-plugin": "^5.13.2",
111 | "@loadable/webpack-plugin": "^5.15.0",
112 | "@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
113 | "babel-eslint": "^8.2.3",
114 | "eslint": "^5.0.1",
115 | "eslint-plugin-babel": "^5.1.0",
116 | "eslint-plugin-jest": "^21.17.0",
117 | "eslint-plugin-react": "^7.9.1",
118 | "http-proxy-middleware": "^0.18.0",
119 | "jest": "27.0.5",
120 | "prettier": "1.13.7",
121 | "react-refresh": "^0.10.0",
122 | "start-server-webpack-plugin": "^3.0.0-rc3",
123 | "uglifyjs-webpack-plugin": "^1.2.5",
124 | "webpack-bundle-analyzer": "^2.13.1",
125 | "webpack-dev-server": "^3.1.4",
126 | "webpack-hot-middleware": "^2.25.0",
127 | "webpack-manifest-plugin": "^2.0.3",
128 | "webpack-node-externals": "^1.7.2"
129 | }
130 | }
131 |
--------------------------------------------------------------------------------