├── .all-contributorsrc
├── .babelrc
├── .editorconfig
├── .env_example
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .modernizrrc
├── .npmrc
├── .nvmrc
├── CHANGELOG.md
├── LICENSE
├── README.md
├── client
├── components
│ └── ReactHotLoader.js
├── index.js
├── polyfills
│ └── index.js
└── registerServiceWorker.js
├── config
├── components
│ └── ClientConfig.js
├── index.js
├── utils
│ └── envVars.js
└── values.js
├── internal
├── .eslintrc
├── development
│ ├── createVendorDLL.js
│ ├── hotClientServer.js
│ ├── hotDevelopment.js
│ ├── hotNodeServer.js
│ ├── index.js
│ └── listenerManager.js
├── docs
│ ├── ADDING_AN_API_BUNDLE.md
│ ├── DEPLOY_TO_NOW.md
│ ├── FAQ.md
│ ├── FEATURE_BRANCHES.md
│ ├── PKG_SCRIPTS.md
│ ├── PROJECT_CONFIG.md
│ └── PROJECT_OVERVIEW.md
├── jest
│ ├── assetMock.js
│ └── styleMock.js
├── scripts
│ ├── analyze.js
│ ├── build.js
│ ├── clean.js
│ ├── deploy.js
│ └── preinstall.js
├── utils.js
└── webpack
│ ├── configFactory.js
│ └── withServiceWorker
│ ├── index.js
│ └── offlinePageTemplate.js
├── package-lock.json
├── package.json
├── public
├── browserconfig.xml
├── favicon.ico
├── favicons
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ ├── apple-touch-icon-114x114.png
│ ├── apple-touch-icon-120x120.png
│ ├── apple-touch-icon-144x144.png
│ ├── apple-touch-icon-152x152.png
│ ├── apple-touch-icon-180x180.png
│ ├── apple-touch-icon-57x57.png
│ ├── apple-touch-icon-60x60.png
│ ├── apple-touch-icon-72x72.png
│ ├── apple-touch-icon-76x76.png
│ ├── favicon-128.png
│ ├── favicon-16x16.png
│ ├── favicon-196x196.png
│ ├── favicon-32x32.png
│ ├── favicon-96x96.png
│ ├── mstile-144x144.png
│ ├── mstile-150x150.png
│ ├── mstile-310x150.png
│ ├── mstile-310x310.png
│ ├── mstile-70x70.png
│ └── safari-pinned-tab.svg
├── manifest.json
└── robots.txt
├── server
├── index.js
└── middleware
│ ├── clientBundle.js
│ ├── errorHandlers.js
│ ├── offlinePage.js
│ ├── reactApplication
│ ├── ServerHTML.js
│ ├── getClientBundleEntryAssets.js
│ └── index.js
│ ├── security.js
│ └── serviceWorker.js
└── shared
├── README.md
├── components
├── DemoApp
│ ├── AsyncAboutRoute
│ │ ├── AboutRoute.js
│ │ ├── __tests__
│ │ │ ├── AboutRoute.test.js
│ │ │ └── __snapshots__
│ │ │ │ └── AboutRoute.test.js.snap
│ │ └── index.js
│ ├── AsyncCounterRoute
│ │ ├── CounterRoute.js
│ │ └── index.js
│ ├── AsyncHomeRoute
│ │ ├── HomeRoute.js
│ │ ├── __tests__
│ │ │ ├── HomeRoute.test.js
│ │ │ └── __snapshots__
│ │ │ │ └── HomeRoute.test.js.snap
│ │ └── index.js
│ ├── Error404
│ │ ├── __tests__
│ │ │ ├── Error404.test.js
│ │ │ └── __snapshots__
│ │ │ │ └── Error404.test.js.snap
│ │ └── index.js
│ ├── Header
│ │ ├── Logo
│ │ │ ├── index.js
│ │ │ └── logo.png
│ │ ├── Menu
│ │ │ ├── __tests__
│ │ │ │ ├── Menu.test.js
│ │ │ │ └── __snapshots__
│ │ │ │ │ └── Menu.test.js.snap
│ │ │ └── index.js
│ │ └── index.js
│ ├── globals.css
│ └── index.js
└── HTML
│ └── index.js
└── utils
├── arrays
├── __tests__
│ └── removeNil.test.js
├── index.js
└── removeNil.js
├── logic
├── __tests__
│ └── ifElse.test.js
├── ifElse.js
└── index.js
└── objects
├── __tests__
├── filterWithRules.test.js
└── mergeDeep.test.js
├── filterWithRules.js
├── index.js
└── mergeDeep.js
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "projectName": "react-universally",
3 | "projectOwner": "ctrlplusb",
4 | "files": [
5 | "README.md"
6 | ],
7 | "imageSize": 100,
8 | "commit": true,
9 | "contributors": [
10 | {
11 | "login": "aoc",
12 | "name": "Andrés Calabrese",
13 | "avatar_url": "https://avatars3.githubusercontent.com/u/243161?v=3",
14 | "profile": "https://github.com/aoc",
15 | "contributions": [
16 | "code"
17 | ]
18 | },
19 | {
20 | "login": "andreyluiz",
21 | "name": "Andrey Luiz",
22 | "avatar_url": "https://avatars3.githubusercontent.com/u/1965897?v=3",
23 | "profile": "https://andreyluiz.github.io/",
24 | "contributions": [
25 | "code"
26 | ]
27 | },
28 | {
29 | "login": "alinporumb",
30 | "name": "Alin Porumb",
31 | "avatar_url": "https://avatars3.githubusercontent.com/u/3148205?v=3",
32 | "profile": "https://github.com/alinporumb",
33 | "contributions": [
34 | "code"
35 | ]
36 | },
37 | {
38 | "login": "bkniffler",
39 | "name": "Benjamin Kniffler",
40 | "avatar_url": "https://avatars0.githubusercontent.com/u/4349324?v=3",
41 | "profile": "https://github.com/bkniffler",
42 | "contributions": [
43 | "code"
44 | ]
45 | },
46 | {
47 | "login": "birkir",
48 | "name": "Birkir Rafn Guðjónsson",
49 | "avatar_url": "https://avatars0.githubusercontent.com/u/180773?v=3",
50 | "profile": "https://medium.com/@birkir.gudjonsson",
51 | "contributions": [
52 | "question",
53 | "bug",
54 | "code",
55 | "review"
56 | ]
57 | },
58 | {
59 | "login": "carsonperrotti",
60 | "name": "Carson Perrotti",
61 | "avatar_url": "https://avatars0.githubusercontent.com/u/2063102?v=3",
62 | "profile": "http://carsonperrotti.com",
63 | "contributions": [
64 | "question",
65 | "code",
66 | "doc",
67 | "review"
68 | ]
69 | },
70 | {
71 | "login": "LorbusChris",
72 | "name": "Christian Glombek",
73 | "avatar_url": "https://avatars1.githubusercontent.com/u/13365531?v=3",
74 | "profile": "https://github.com/LorbusChris",
75 | "contributions": [
76 | "bug",
77 | "code"
78 | ]
79 | },
80 | {
81 | "login": "codepunkt",
82 | "name": "Christoph Werner",
83 | "avatar_url": "https://avatars3.githubusercontent.com/u/603683?v=3",
84 | "profile": "https://twitter.com/code_punkt",
85 | "contributions": [
86 | "question",
87 | "bug",
88 | "code",
89 | "review"
90 | ]
91 | },
92 | {
93 | "login": "threehams",
94 | "name": "David Edmondson",
95 | "avatar_url": "https://avatars0.githubusercontent.com/u/1399894?v=3",
96 | "profile": "https://github.com/threehams",
97 | "contributions": [
98 | "code"
99 | ]
100 | },
101 | {
102 | "login": "diondirza",
103 | "name": "Dion Dirza",
104 | "avatar_url": "https://avatars0.githubusercontent.com/u/10954870?v=3",
105 | "profile": "https://github.com/diondirza",
106 | "contributions": [
107 | "question",
108 | "bug",
109 | "code",
110 | "doc",
111 | "review"
112 | ]
113 | },
114 | {
115 | "login": "evgenyboxer",
116 | "name": "Evgeny Boxer",
117 | "avatar_url": "https://avatars0.githubusercontent.com/u/254095?v=3",
118 | "profile": "https://github.com/evgenyboxer",
119 | "contributions": [
120 | "bug",
121 | "code"
122 | ]
123 | },
124 | {
125 | "login": "kohlmannj",
126 | "name": "Joe Kohlmann",
127 | "avatar_url": "https://avatars2.githubusercontent.com/u/191304?v=3",
128 | "profile": "http://kohlmannj.com",
129 | "contributions": [
130 | "bug",
131 | "code"
132 | ]
133 | },
134 | {
135 | "login": "lucianlature",
136 | "name": "Lucian Lature",
137 | "avatar_url": "https://avatars2.githubusercontent.com/u/24992?v=3",
138 | "profile": "https://www.linkedin.com/in/lucianlature/",
139 | "contributions": [
140 | "bug",
141 | "code",
142 | "review"
143 | ]
144 | },
145 | {
146 | "login": "markshlick",
147 | "name": "Mark Shlick",
148 | "avatar_url": "https://avatars1.githubusercontent.com/u/1624703?v=3",
149 | "profile": "https://github.com/markshlick",
150 | "contributions": [
151 | "code"
152 | ]
153 | },
154 | {
155 | "login": "rlindskog",
156 | "name": "Ryan Lindskog",
157 | "avatar_url": "https://avatars1.githubusercontent.com/u/7436773?v=3",
158 | "profile": "https://www.RyanLindskog.com/",
159 | "contributions": [
160 | "code"
161 | ]
162 | },
163 | {
164 | "login": "enten",
165 | "name": "Steven Enten",
166 | "avatar_url": "https://avatars1.githubusercontent.com/u/977713?v=3",
167 | "profile": "http://enten.fr",
168 | "contributions": [
169 | "question",
170 | "bug",
171 | "code",
172 | "review"
173 | ]
174 | },
175 | {
176 | "login": "ctrlplusb",
177 | "name": "Sean Matheson",
178 | "avatar_url": "https://avatars1.githubusercontent.com/u/12164768?v=3",
179 | "profile": "http://www.ctrlplusb.com",
180 | "contributions": [
181 | "question",
182 | "bug",
183 | "code",
184 | "doc",
185 | "example",
186 | "review",
187 | "test",
188 | "tool"
189 | ]
190 | },
191 | {
192 | "login": "strues",
193 | "name": "Steven Truesdell",
194 | "avatar_url": "https://avatars0.githubusercontent.com/u/6218853?v=3",
195 | "profile": "https://steventruesdell.com",
196 | "contributions": [
197 | "question",
198 | "bug",
199 | "code",
200 | "doc",
201 | "test"
202 | ]
203 | },
204 | {
205 | "login": "datoml",
206 | "name": "Thomas Leitgeb",
207 | "avatar_url": "https://avatars0.githubusercontent.com/u/10552487?v=3",
208 | "profile": "https://twitter.com/_datoml",
209 | "contributions": [
210 | "bug",
211 | "code"
212 | ]
213 | },
214 | {
215 | "login": "tsnieman",
216 | "name": "Tyler Nieman",
217 | "avatar_url": "https://avatars0.githubusercontent.com/u/595711?v=3",
218 | "profile": "http://tsnieman.net/",
219 | "contributions": [
220 | "code"
221 | ]
222 | }
223 | ]
224 | }
225 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | // We use the .babelrc here to help transpile our tools and support tooling
2 | // (such as testing libs).
3 | // I would recommend that you keep this file in feature parity with
4 | // the config/babelConfigResolver. That way you get to use the same level of
5 | // javascript syntax across the entire project.
6 | // It's nice not having to worry/think about what level of javascript syntax
7 | // is supported for each context.
8 | {
9 | "presets": [
10 | ["env", { "targets": { "node": true } }],
11 | "stage-3",
12 | "react"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # See http://editorconfig.org/
2 | # EditorConfig helps developers define and maintain consistent coding styles
3 | # between different editors and IDEs. The EditorConfig project consists of a
4 | # file format for defining coding styles and a collection of text editor plugins
5 | # that enable editors to read the file format and adhere to defined styles.
6 | # EditorConfig files are easily readable and they work nicely with version
7 | # control systems.
8 |
9 | root = true
10 |
11 | [*]
12 | indent_style = space
13 | indent_size = 2
14 | end_of_line = lf
15 | charset = utf-8
16 | trim_trailing_whitespace = true
17 | insert_final_newline = true
18 |
19 | [*.md]
20 | trim_trailing_whitespace = false
21 |
--------------------------------------------------------------------------------
/.env_example:
--------------------------------------------------------------------------------
1 | # ==============================================================================
2 | # This is an example environment configuration file. You can create your own
3 | # .env implementation or pass them via the CLI.
4 | #
5 | # You could also decide to supply some of your environment variables from a .env
6 | # file, and others via the CLI. This allows you to easily store "safe"
7 | # environment variables within this file, and then manage your more sensitive
8 | # environment variables seperately.
9 | # ==============================================================================
10 |
11 | # The host on which to run the server.
12 | HOST=localhost
13 |
14 | # The port on which to run the server.
15 | PORT=1337
16 |
17 | # The port on which to run the client bundle dev server (i.e. used during
18 | # development only).
19 | CLIENT_DEV_PORT=7331
20 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | build/
3 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": "airbnb",
4 | "env": {
5 | "browser": true,
6 | "es6": true,
7 | "node": true,
8 | "jest": true
9 | },
10 | "parserOptions": {
11 | "ecmaVersion": 6,
12 | "sourceType": "module",
13 | "ecmaFeatures": {
14 | "defaultParams": true
15 | }
16 | },
17 | "rules": {
18 | // A jsx extension is not required for files containing jsx
19 | "react/jsx-filename-extension": 0,
20 | // This rule struggles with flow and class properties
21 | "react/sort-comp": 0,
22 | // ignore linebreak style. the CRLF / LF endings wont matter
23 | // if a windows user correctly converts CRLF to LF upon commits otherwise
24 | // there are errors every line.
25 | "linebreak-style": 0
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Environment Configuration
2 | .env
3 |
4 | # Build output folders
5 | build/
6 |
7 | # Logs
8 | logs
9 | *.log
10 |
11 | # Runtime data
12 | pids
13 | *.pid
14 | *.seed
15 |
16 | # Coverage directory used by tools like istanbul
17 | coverage
18 |
19 | # node-waf configuration
20 | .lock-wscript
21 |
22 | # Dependency directory
23 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git-
24 | node_modules
25 |
26 | # Debug log from npm
27 | npm-debug.log
28 |
29 | # IntelliJ IDE ignore
30 | .idea
31 |
32 | # Visual Studio Code
33 | .vscode
34 | .history
35 |
36 | # flow-typed Lib Defs
37 | flow-typed/
38 |
39 | # Flow Coverage Report
40 | flow-coverage/
41 |
42 | # Happypack
43 | .happypack
44 |
45 | # OSX Files
46 | .DS_Store
47 |
--------------------------------------------------------------------------------
/.modernizrrc:
--------------------------------------------------------------------------------
1 | {
2 | "minify": true,
3 | "options": [],
4 | "feature-detects": [
5 | "elem/picture"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | save-exact = true
2 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v6.11.1
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Sean Matheson
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 |
2 |
React, Universally
3 |
4 |
A starter kit for universal react applications.
5 |
6 |
7 | [](#contributors)
8 |
9 | ## About
10 |
11 | This starter kit contains all the build tooling and configuration you need to kick off your next universal React project, whilst containing a minimal "project" set up allowing you to make your own architecture decisions (Redux/MobX etc).
12 |
13 | > NOTICE: Please read this important [issue](https://github.com/ctrlplusb/react-universally/issues/409) about the behaviour of this project when using `react-async-component`, which is by default bundled with it.
14 |
15 | ## Features
16 |
17 | - 👀 `react` as the view.
18 | - 🔀 `react-router` v4 as the router.
19 | - 🚄 `express` server.
20 | - 🎭 `jest` as the test framework.
21 | - 💄 Combines `prettier` and Airbnb's ESlint configuration - performing code formatting on commit. Stop worrying about code style consistency.
22 | - 🖌 Very basic CSS support - it's up to you to extend it with CSS Modules etc.
23 | - ✂️ Code splitting - easily define code split points in your source using `react-async-component`.
24 | - 🌍 Server Side Rendering.
25 | - 😎 Progressive Web Application ready, with offline support, via a Service Worker.
26 | - 🐘 Long term browser caching of assets with automated cache invalidation.
27 | - 📦 All source is bundled using Webpack v3.
28 | - 🚀 Full ES2017+ support - use the exact same JS syntax across the entire project. No more folder context switching! We also only use syntax that is stage-3 or later in the TC39 process.
29 | - 🔧 Centralised application configuration with helpers to avoid boilerplate in your code. Also has support for environment specific configuration files.
30 | - 🔥 Extreme live development - hot reloading of ALL changes to client/server source, with auto development server restarts when your application configuration changes. All this with a high level of error tolerance and verbose logging to the console.
31 | - ⛑ SEO friendly - `react-helmet` provides control of the page title/meta/styles/scripts from within your components.
32 | - 🤖 Optimised Webpack builds via HappyPack and an auto generated Vendor DLL for smooth development experiences.
33 | - 🍃 Tree-shaking, courtesy of Webpack.
34 | - 👮 Security on the `express` server using `helmet` and `hpp`.
35 | - 🏜 Asset bundling support. e.g. images/fonts.
36 | - 🎛 Preconfigured to support development and optimised production builds.
37 | - ❤️ Preconfigured to deploy to `now` with a single command.
38 |
39 | Redux/MobX, data persistence, modern styling frameworks and all the other bells and whistles have been explicitly excluded from this starter kit. It's up to you to decide what technologies you would like to add to your own implementation based upon your own needs.
40 |
41 | > However, we now include a set of "feature branches", each implementing a technology on top of the clean master branch. This provides you with an example on how to integrate said technologies, or use the branches to merge in a configuration that meets your requirements. See the [`Feature Branches`](/internal/docs/FEATURE_BRANCHES.md) documentation for more.
42 |
43 | ## Getting started
44 |
45 | ```bash
46 | git clone https://github.com/ctrlplusb/react-universally my-project
47 | cd my-project
48 | npm install
49 | npm run develop
50 | ```
51 |
52 | Now go make some changes to the `Home` component to see the tooling in action.
53 |
54 | ## Docs
55 |
56 | - [Project Overview](/internal/docs/PROJECT_OVERVIEW.md)
57 | - [Project Configuration](/internal/docs/PROJECT_CONFIG.md)
58 | - [Package Script Commands](/internal/docs/PKG_SCRIPTS.md)
59 | - [FAQ](/internal/docs/FAQ.md)
60 | - [Feature Branches](/internal/docs/FEATURE_BRANCHES.md)
61 | - [Deploy your very own Server Side Rendering React App in 5 easy steps](/internal/docs/DEPLOY_TO_NOW.md)
62 | - [Changelog](/CHANGELOG.md)
63 |
64 | ## Who's using it and where?
65 |
66 | You can see who is using it and how in [the comments here](https://github.com/ctrlplusb/react-universally/issues/437). Feel free to add to that telling us how you are using it, we'd love to hear from you.
67 |
68 | ## Contributors
69 |
70 | Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)):
71 |
72 |
73 | | [ Andrés Calabrese](https://github.com/aoc) [💻](https://github.com/ctrlplusb/react-universally/commits?author=aoc) | [ Andrey Luiz](https://andreyluiz.github.io/) [💻](https://github.com/ctrlplusb/react-universally/commits?author=andreyluiz) | [ Alin Porumb](https://github.com/alinporumb) [💻](https://github.com/ctrlplusb/react-universally/commits?author=alinporumb) | [ Benjamin Kniffler](https://github.com/bkniffler) [💻](https://github.com/ctrlplusb/react-universally/commits?author=bkniffler) | [ Birkir Rafn Guðjónsson](https://medium.com/@birkir.gudjonsson) 💬 [🐛](https://github.com/ctrlplusb/react-universally/issues?q=author%3Abirkir) [💻](https://github.com/ctrlplusb/react-universally/commits?author=birkir) 👀 | [ Carson Perrotti](http://carsonperrotti.com) 💬 [💻](https://github.com/ctrlplusb/react-universally/commits?author=carsonperrotti) [📖](https://github.com/ctrlplusb/react-universally/commits?author=carsonperrotti) 👀 | [ Christian Glombek](https://github.com/LorbusChris) [🐛](https://github.com/ctrlplusb/react-universally/issues?q=author%3ALorbusChris) [💻](https://github.com/ctrlplusb/react-universally/commits?author=LorbusChris) |
74 | | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
75 | | [ Christoph Werner](https://twitter.com/code_punkt) 💬 [🐛](https://github.com/ctrlplusb/react-universally/issues?q=author%3Acodepunkt) [💻](https://github.com/ctrlplusb/react-universally/commits?author=codepunkt) 👀 | [ David Edmondson](https://github.com/threehams) [💻](https://github.com/ctrlplusb/react-universally/commits?author=threehams) | [ Dion Dirza](https://github.com/diondirza) 💬 [🐛](https://github.com/ctrlplusb/react-universally/issues?q=author%3Adiondirza) [💻](https://github.com/ctrlplusb/react-universally/commits?author=diondirza) [📖](https://github.com/ctrlplusb/react-universally/commits?author=diondirza) 👀 | [ Evgeny Boxer](https://github.com/evgenyboxer) [🐛](https://github.com/ctrlplusb/react-universally/issues?q=author%3Aevgenyboxer) [💻](https://github.com/ctrlplusb/react-universally/commits?author=evgenyboxer) | [ Joe Kohlmann](http://kohlmannj.com) [🐛](https://github.com/ctrlplusb/react-universally/issues?q=author%3Akohlmannj) [💻](https://github.com/ctrlplusb/react-universally/commits?author=kohlmannj) | [ Lucian Lature](https://www.linkedin.com/in/lucianlature/) [🐛](https://github.com/ctrlplusb/react-universally/issues?q=author%3Alucianlature) [💻](https://github.com/ctrlplusb/react-universally/commits?author=lucianlature) 👀 | [ Mark Shlick](https://github.com/markshlick) [💻](https://github.com/ctrlplusb/react-universally/commits?author=markshlick) |
76 | | [ Ryan Lindskog](https://www.RyanLindskog.com/) [💻](https://github.com/ctrlplusb/react-universally/commits?author=rlindskog) | [ Steven Enten](http://enten.fr) 💬 [🐛](https://github.com/ctrlplusb/react-universally/issues?q=author%3Aenten) [💻](https://github.com/ctrlplusb/react-universally/commits?author=enten) 👀 | [ Sean Matheson](http://www.ctrlplusb.com) 💬 [🐛](https://github.com/ctrlplusb/react-universally/issues?q=author%3Actrlplusb) [💻](https://github.com/ctrlplusb/react-universally/commits?author=ctrlplusb) [📖](https://github.com/ctrlplusb/react-universally/commits?author=ctrlplusb) 💡 👀 [⚠️](https://github.com/ctrlplusb/react-universally/commits?author=ctrlplusb) 🔧 | [ Steven Truesdell](https://steventruesdell.com) 💬 [🐛](https://github.com/ctrlplusb/react-universally/issues?q=author%3Astrues) [💻](https://github.com/ctrlplusb/react-universally/commits?author=strues) [📖](https://github.com/ctrlplusb/react-universally/commits?author=strues) [⚠️](https://github.com/ctrlplusb/react-universally/commits?author=strues) | [ Thomas Leitgeb](https://twitter.com/_datoml) [🐛](https://github.com/ctrlplusb/react-universally/issues?q=author%3Adatoml) [💻](https://github.com/ctrlplusb/react-universally/commits?author=datoml) | [ Tyler Nieman](http://tsnieman.net/) [💻](https://github.com/ctrlplusb/react-universally/commits?author=tsnieman) |
77 |
78 |
79 | This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!
80 |
--------------------------------------------------------------------------------
/client/components/ReactHotLoader.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable global-require */
2 | /* eslint-disable import/no-extraneous-dependencies */
3 |
4 | import React from 'react';
5 |
6 | // We create this wrapper so that we only import react-hot-loader for a
7 | // development build. Small savings. :)
8 | const ReactHotLoader = process.env.NODE_ENV === 'development'
9 | ? require('react-hot-loader').AppContainer
10 | : ({ children }) => React.Children.only(children);
11 |
12 | export default ReactHotLoader;
13 |
--------------------------------------------------------------------------------
/client/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable global-require */
2 |
3 | import React from 'react';
4 | import { render } from 'react-dom';
5 | import BrowserRouter from 'react-router-dom/BrowserRouter';
6 | import asyncBootstrapper from 'react-async-bootstrapper';
7 | import { AsyncComponentProvider } from 'react-async-component';
8 |
9 | import './polyfills';
10 |
11 | import ReactHotLoader from './components/ReactHotLoader';
12 | import DemoApp from '../shared/components/DemoApp';
13 |
14 | // Get the DOM Element that will host our React application.
15 | const container = document.querySelector('#app');
16 |
17 | // Does the user's browser support the HTML5 history API?
18 | // If the user's browser doesn't support the HTML5 history API then we
19 | // will force full page refreshes on each page change.
20 | const supportsHistory = 'pushState' in window.history;
21 |
22 | // Get any rehydrateState for the async components.
23 | // eslint-disable-next-line no-underscore-dangle
24 | const asyncComponentsRehydrateState = window.__ASYNC_COMPONENTS_REHYDRATE_STATE__;
25 |
26 | /**
27 | * Renders the given React Application component.
28 | */
29 | function renderApp(TheApp) {
30 | // Firstly, define our full application component, wrapping the given
31 | // component app with a browser based version of react router.
32 | const app = (
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | );
41 |
42 | // We use the react-async-component in order to support code splitting of
43 | // our bundle output. It's important to use this helper.
44 | // @see https://github.com/ctrlplusb/react-async-component
45 | asyncBootstrapper(app).then(() => render(app, container));
46 | }
47 |
48 | // Execute the first render of our app.
49 | renderApp(DemoApp);
50 |
51 | // This registers our service worker for asset caching and offline support.
52 | // Keep this as the last item, just in case the code execution failed (thanks
53 | // to react-boilerplate for that tip.)
54 | require('./registerServiceWorker');
55 |
56 | // The following is needed so that we can support hot reloading our application.
57 | if (process.env.BUILD_FLAG_IS_DEV === 'true' && module.hot) {
58 | // Accept changes to this file for hot reloading.
59 | module.hot.accept('./index.js');
60 | // Any changes to our App will cause a hotload re-render.
61 | module.hot.accept('../shared/components/DemoApp', () => {
62 | renderApp(require('../shared/components/DemoApp').default);
63 | });
64 | }
65 |
--------------------------------------------------------------------------------
/client/polyfills/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | import Modernizr from 'modernizr';
4 |
5 | // This is just an illustrative example. Here you are testing the client's
6 | // support for the "picture" element, and if it isn't supported then you
7 | // load a polyfill.
8 | if (!Modernizr.picture) {
9 | console.log('Client does not support "picture", polyfilling it...');
10 | // If you want to use the below do a `npm install picturefill -E -S` and then
11 | // uncomment the lines below:
12 | /*
13 | require('picturefill');
14 | require('picturefill/dist/plugins/mutation/pf.mutation');
15 | */
16 | } else {
17 | console.log('Client has support for "picture".');
18 | }
19 |
--------------------------------------------------------------------------------
/client/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | /**
2 | * We use the offline-plugin to generate a service worker. See the webpack
3 | * config for more details.
4 | *
5 | * We need to ensure that the runtime is installed so that the generated
6 | * service worker is executed.
7 | *
8 | * NOTE: We only enable the service worker for non-development environments.
9 | */
10 |
11 | import config from '../config';
12 |
13 | if (process.env.BUILD_FLAG_IS_DEV === 'false') {
14 | // We check the shared config, ensuring that the service worker has been
15 | // enabled.
16 | if (config('serviceWorker.enabled')) {
17 | // eslint-disable-next-line global-require
18 | const OfflinePluginRuntime = require('offline-plugin/runtime');
19 |
20 | // Install the offline plugin, which instantiates our service worker and app
21 | // cache to support precaching of assets and offline support.
22 | OfflinePluginRuntime.install({
23 | onUpdating: () => undefined,
24 | // When an update is ready we will tell the new SW to take control immediately.
25 | onUpdateReady: () => OfflinePluginRuntime.applyUpdate(),
26 | // After the new SW update has been applied we will reload the users page
27 | // to ensure they are using the latest assets.
28 | // This only gets run if there were updates available for our cached assets.
29 | onUpdated: () => window.location.reload(),
30 | onUpdateFailed: () => undefined,
31 | });
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/config/components/ClientConfig.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import serialize from 'serialize-javascript';
4 | import filterWithRules from '../../shared/utils/objects/filterWithRules';
5 | import values from '../values';
6 |
7 | // Filter the config down to the properties that are allowed to be included
8 | // in the HTML response.
9 | const clientConfig = filterWithRules(
10 | // These are the rules used to filter the config.
11 | values.clientConfigFilter,
12 | // The config values to filter.
13 | values,
14 | );
15 |
16 | const serializedClientConfig = serialize(clientConfig);
17 |
18 | /**
19 | * A react component that generates a script tag that binds the allowed
20 | * values to the window so that config values can be read within the
21 | * browser.
22 | *
23 | * They get bound to window.__CLIENT_CONFIG__
24 | */
25 | function ClientConfig({ nonce }) {
26 | return (
27 |
35 | );
36 | }
37 |
38 | ClientConfig.propTypes = {
39 | nonce: PropTypes.string.isRequired,
40 | };
41 |
42 | export default ClientConfig;
43 |
--------------------------------------------------------------------------------
/config/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Unified Configuration Reader
3 | *
4 | * This helper function allows you to use the same API in accessing configuration
5 | * values no matter where the code is being executed (i.e. browser/node).
6 | *
7 | * e.g.
8 | * import config from '../config';
9 | * config('welcomeMessage'); // => "Hello World!"
10 | */
11 |
12 | /* eslint-disable no-console */
13 | /* eslint-disable import/global-require */
14 | /* eslint-disable no-underscore-dangle */
15 |
16 | // PRIVATES
17 |
18 | let configCache;
19 |
20 | /**
21 | * This resolves the correct configuration source based on the execution
22 | * environment. For node we use the standard config file, however, for browsers
23 | * we need to access the configuration object that would have been bound to
24 | * the "window" by our "reactApplication" middleware.
25 | *
26 | * @return {Object} The executing environment configuration object.
27 | */
28 | function resolveConfigForBrowserOrServer() {
29 | if (configCache) {
30 | return configCache;
31 | }
32 |
33 | // NOTE: By using the "process.env.BUILD_FLAG_IS_NODE" flag here this block of code
34 | // will be removed when "process.env.BUILD_FLAG_IS_NODE === true".
35 | // If no "BUILD_FLAG_IS_NODE" env var is undefined we can assume that we are running outside
36 | // of a webpack run, and will therefore return the config file.
37 | if (
38 | typeof process.env.BUILD_FLAG_IS_NODE === 'undefined' ||
39 | process.env.BUILD_FLAG_IS_NODE === 'true'
40 | ) {
41 | // i.e. running in our server/node process.
42 | // eslint-disable-next-line global-require
43 | configCache = require('./values').default;
44 | return configCache;
45 | }
46 |
47 | // To get here we are likely running in the browser.
48 |
49 | if (typeof window !== 'undefined' && typeof window.__CLIENT_CONFIG__ === 'object') {
50 | configCache = window.__CLIENT_CONFIG__;
51 | } else {
52 | // To get here we must be running in the browser.
53 | console.warn('No client configuration object was bound to the window.');
54 | configCache = {};
55 | }
56 |
57 | return configCache;
58 | }
59 |
60 | // EXPORT
61 |
62 | /**
63 | * This function wraps up the boilerplate needed to access the correct
64 | * configuration depending on whether your code will get executed in the
65 | * browser/node.
66 | *
67 | * i.e.
68 | * - For the browser the config values are available at window.__CLIENT_CONFIG__
69 | * - For a node process they are within the "/config".
70 | *
71 | * To request a configuration value you must provide the repective path. For
72 | * example, f you had the following configuration structure:
73 | * {
74 | * foo: {
75 | * bar: [1, 2, 3]
76 | * },
77 | * bob: 'bob'
78 | * }
79 | *
80 | * You could use this function to access "bar" like so:
81 | * import config from '../config';
82 | * const value = config('foo.bar');
83 | *
84 | * And you could access "bob" like so:
85 | * import config from '../config';
86 | * const value = config('bob');
87 | *
88 | * If any part of the path isn't available as a configuration key/value then
89 | * an error will be thrown indicating that a respective configuration value
90 | * could not be found at the given path.
91 | */
92 | export default function configGet(path) {
93 | const parts = typeof path === 'string' ? path.split('.') : path;
94 |
95 | if (parts.length === 0) {
96 | throw new Error(
97 | 'You must provide the path to the configuration value you would like to consume.',
98 | );
99 | }
100 | let result = resolveConfigForBrowserOrServer();
101 | for (let i = 0; i < parts.length; i += 1) {
102 | if (result === undefined) {
103 | const errorMessage = `Failed to resolve configuration value at "${parts.join('.')}".`;
104 | // This "if" block gets stripped away by webpack for production builds.
105 | if (process.env.BUILD_FLAG_IS_DEV === 'true' && process.env.BUILD_FLAG_IS_CLIENT === 'true') {
106 | throw new Error(
107 | `${errorMessage} We have noticed that you are trying to access this configuration value from the client bundle (i.e. code that will be executed in a browser). For configuration values to be exposed to the client bundle you must ensure that the path is added to the client configuration filter in the project configuration values file.`,
108 | );
109 | }
110 | throw new Error(errorMessage);
111 | }
112 | result = result[parts[i]];
113 | }
114 | return result;
115 | }
116 |
--------------------------------------------------------------------------------
/config/utils/envVars.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Helper for resolving environment specific configuration files.
3 | *
4 | * It resolves .env files that are supported by the `dotenv` library.
5 | *
6 | * Please read the application configuration docs for more info.
7 | */
8 |
9 | import appRootDir from 'app-root-dir';
10 | import dotenv from 'dotenv';
11 | import fs from 'fs';
12 | import path from 'path';
13 |
14 | import ifElse from '../../shared/utils/logic/ifElse';
15 | import removeNil from '../../shared/utils/arrays/removeNil';
16 |
17 | import { log } from '../../internal/utils';
18 |
19 | // PRIVATES
20 |
21 | function registerEnvFile() {
22 | const DEPLOYMENT = process.env.DEPLOYMENT;
23 | const envFile = '.env';
24 |
25 | // This is the order in which we will try to resolve an environment configuration
26 | // file.
27 | const envFileResolutionOrder = removeNil([
28 | // Is there an environment config file at the app root?
29 | // This always takes preference.
30 | // e.g. /projects/react-universally/.env
31 | path.resolve(appRootDir.get(), envFile),
32 | // Is there an environment config file at the app root for our target
33 | // environment name?
34 | // e.g. /projects/react-universally/.env.staging
35 | ifElse(DEPLOYMENT)(path.resolve(appRootDir.get(), `${envFile}.${DEPLOYMENT}`)),
36 | ]);
37 |
38 | // Find the first env file path match.
39 | const envFilePath = envFileResolutionOrder.find(filePath => fs.existsSync(filePath));
40 |
41 | // If we found an env file match the register it.
42 | if (envFilePath) {
43 | // eslint-disable-next-line no-console
44 | log({
45 | title: 'server',
46 | level: 'special',
47 | message: `Registering environment variables from: ${envFilePath}`,
48 | });
49 | dotenv.config({ path: envFilePath });
50 | }
51 | }
52 |
53 | // Ensure that we first register any environment variables from an existing
54 | // env file.
55 | registerEnvFile();
56 |
57 | // EXPORTED HELPERS
58 |
59 | /**
60 | * Gets a string environment variable by the given name.
61 | *
62 | * @param {String} name - The name of the environment variable.
63 | * @param {String} defaultVal - The default value to use.
64 | *
65 | * @return {String} The value.
66 | */
67 | export function string(name, defaultVal) {
68 | return process.env[name] || defaultVal;
69 | }
70 |
71 | /**
72 | * Gets a number environment variable by the given name.
73 | *
74 | * @param {String} name - The name of the environment variable.
75 | * @param {number} defaultVal - The default value to use.
76 | *
77 | * @return {number} The value.
78 | */
79 | export function number(name, defaultVal) {
80 | return process.env[name] ? parseInt(process.env[name], 10) : defaultVal;
81 | }
82 |
83 | export function bool(name, defaultVal) {
84 | return process.env[name] ? process.env[name] === 'true' || process.env[name] === '1' : defaultVal;
85 | }
86 |
--------------------------------------------------------------------------------
/config/values.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Project Configuration.
3 | *
4 | * NOTE: All file/folder paths should be relative to the project root. The
5 | * absolute paths should be resolved during runtime by our build internal/server.
6 | */
7 |
8 | import * as EnvVars from './utils/envVars';
9 |
10 | const values = {
11 | // The configuration values that should be exposed to our client bundle.
12 | // This value gets passed through the /shared/utils/objects/filterWithRules
13 | // util to create a filter object that can be serialised and included
14 | // with our client bundle.
15 | clientConfigFilter: {
16 | // This is here as an example showing that you can expose variables
17 | // that were potentially provivded by the environment
18 | welcomeMessage: true,
19 | // We only need to expose the enabled flag of the service worker.
20 | serviceWorker: {
21 | enabled: true,
22 | },
23 | // We need to expose all the polyfill.io settings.
24 | polyfillIO: true,
25 | // We need to expose all the htmlPage settings.
26 | htmlPage: true,
27 | },
28 |
29 | // The host on which the server should run.
30 | host: EnvVars.string('HOST', '0.0.0.0'),
31 | // The port on which the server should run.
32 | port: EnvVars.number('PORT', 1337),
33 |
34 | // The port on which the client bundle development server should run.
35 | clientDevServerPort: EnvVars.number('CLIENT_DEV_PORT', 7331),
36 |
37 | // This is an example environment variable which is used within the react
38 | // application to demonstrate the usage of environment variables across
39 | // the client and server bundles.
40 | welcomeMessage: EnvVars.string('WELCOME_MSG', 'Hello world!'),
41 |
42 | // Disable server side rendering?
43 | disableSSR: false,
44 |
45 | // How long should we set the browser cache for the served assets?
46 | // Don't worry, we add hashes to the files, so if they change the new files
47 | // will be served to browsers.
48 | // We are using the "ms" format to set the length.
49 | // @see https://www.npmjs.com/package/ms
50 | browserCacheMaxAge: '365d',
51 |
52 | // We use the polyfill.io service which provides the polyfills that a
53 | // client needs, which is far more optimal than the large output
54 | // generated by babel-polyfill.
55 | // Note: we have to keep this seperate from our "htmlPage" configuration
56 | // as the polyfill needs to be loaded BEFORE any of our other javascript
57 | // gets parsed.
58 | polyfillIO: {
59 | enabled: true,
60 | url: '//cdn.polyfill.io/v2/polyfill.min.js',
61 | // Reference https://qa.polyfill.io/v2/docs/features for a full list
62 | // of features.
63 | features: [
64 | // The default list.
65 | 'default',
66 | 'es6',
67 | ],
68 | },
69 |
70 | // Basic configuration for the HTML page that hosts our application.
71 | // We make use of react-helmet to consume the values below.
72 | // @see https://github.com/nfl/react-helmet
73 | htmlPage: {
74 | titleTemplate: 'React, Universally - %s',
75 | defaultTitle: 'React, Universally',
76 | description:
77 | 'A starter kit giving you the minimum requirements for a production ready universal react application.',
78 | },
79 |
80 | // Content Security Policy (CSP)
81 | // @see server/middleware/security for more info.
82 | cspExtensions: {
83 | childSrc: [],
84 | connectSrc: [],
85 | defaultSrc: [],
86 | fontSrc: ['fonts.googleapis.com/css', 'fonts.gstatic.com'],
87 | imgSrc: [],
88 | mediaSrc: [],
89 | manifestSrc: [],
90 | objectSrc: [],
91 | scriptSrc: [
92 | // Allow scripts from cdn.polyfill.io so that we can import the
93 | // polyfill.
94 | 'cdn.polyfill.io',
95 | ],
96 | styleSrc: [
97 | 'cdn.rawgit.com/milligram/milligram/master/dist/milligram.min.css',
98 | 'fonts.googleapis.com/css',
99 | ],
100 | },
101 |
102 | // Path to the public assets that will be served off the root of the
103 | // HTTP server.
104 | publicAssetsPath: './public',
105 |
106 | // Where does our build output live?
107 | buildOutputPath: './build',
108 |
109 | // Do you want to included source maps for optimised builds of the client
110 | // bundle?
111 | includeSourceMapsForOptimisedClientBundle: false,
112 |
113 | // These extensions are tried when resolving src files for our bundles..
114 | bundleSrcTypes: ['js', 'jsx', 'json'],
115 |
116 | // What should we name the json output file that webpack generates
117 | // containing details of all output files for a bundle?
118 | bundleAssetsFileName: 'assets.json',
119 |
120 | // node_modules are not included in any bundles that target "node" as a
121 | // runtime (e.g.. the server bundle) as including them often breaks builds
122 | // due to thinks like require statements containing expressions..
123 | // However. some of the modules contain files need to be processed by
124 | // one of our Webpack loaders (e.g. CSS). Add any file types to the list
125 | // below to allow them to be processed by Webpack.
126 | nodeExternalsFileTypeWhitelist: [
127 | /\.(eot|woff|woff2|ttf|otf)$/,
128 | /\.(svg|png|jpg|jpeg|gif|ico)$/,
129 | /\.(mp4|mp3|ogg|swf|webp)$/,
130 | /\.(css|scss|sass|sss|less)$/,
131 | ],
132 |
133 | // Note: you can only have a single service worker instance. Our service
134 | // worker implementation is bound to the "client" and "server" bundles.
135 | // It includes the "client" bundle assets, as well as the public folder assets,
136 | // and it is served by the "server" bundle.
137 | serviceWorker: {
138 | // Enabled?
139 | enabled: true,
140 | // Service worker name
141 | fileName: 'sw.js',
142 | // Paths to the public assets which should be included within our
143 | // service worker. Relative to our public folder path, and accepts glob
144 | // syntax.
145 | includePublicAssets: [
146 | // NOTE: This will include ALL of our public folder assets. We do
147 | // a glob pull of them and then map them to /foo paths as all the
148 | // public folder assets get served off the root of our application.
149 | // You may or may not want to be including these assets. Feel free
150 | // to remove this or instead include only a very specific set of
151 | // assets.
152 | './**/*',
153 | ],
154 | // Offline page file name.
155 | offlinePageFileName: 'offline.html',
156 | },
157 |
158 | bundles: {
159 | client: {
160 | // Src entry file.
161 | srcEntryFile: './client/index.js',
162 |
163 | // Src paths.
164 | srcPaths: [
165 | './client',
166 | './shared',
167 | // The service worker offline page generation needs access to the
168 | // config folder. Don't worry we have guards within the config files
169 | // to ensure they never get included in a client bundle.
170 | './config',
171 | ],
172 |
173 | // Where does the client bundle output live?
174 | outputPath: './build/client',
175 |
176 | // What is the public http path at which we must serve the bundle from?
177 | webPath: '/client/',
178 |
179 | // Configuration settings for the development vendor DLL. This will be created
180 | // by our development server and provides an improved dev experience
181 | // by decreasing the number of modules that webpack needs to process
182 | // for every rebuild of our client bundle. It by default uses the
183 | // dependencies configured in package.json however you can customise
184 | // which of these dependencies are excluded, whilst also being able to
185 | // specify the inclusion of additional modules below.
186 | devVendorDLL: {
187 | // Enabled?
188 | enabled: true,
189 |
190 | // Specify any dependencies that you would like to include in the
191 | // Vendor DLL.
192 | //
193 | // NOTE: It is also possible that some modules require specific
194 | // webpack loaders in order to be processed (e.g. CSS/SASS etc).
195 | // For these cases you don't want to include them in the Vendor DLL.
196 | include: [
197 | 'react-async-component',
198 | 'react',
199 | 'react-dom',
200 | 'react-helmet',
201 | 'react-router-dom',
202 | ],
203 |
204 | // The name of the vendor DLL.
205 | name: '__dev_vendor_dll__',
206 | },
207 | },
208 |
209 | server: {
210 | // Src entry file.
211 | srcEntryFile: './server/index.js',
212 |
213 | // Src paths.
214 | srcPaths: ['./server', './shared', './config'],
215 |
216 | // Where does the server bundle output live?
217 | outputPath: './build/server',
218 | },
219 | },
220 |
221 | additionalNodeBundles: {
222 | // NOTE: The webpack configuration and build scripts have been built so
223 | // that you can add arbitrary additional node bundle configurations here.
224 | //
225 | // A common requirement for larger projects is to add additional "node"
226 | // target bundles (e.g an APi server endpoint). Therefore flexibility has been
227 | // baked into our webpack config factory to allow for this.
228 | //
229 | // Simply define additional configurations similar to below. The development
230 | // server will manage starting them up for you. The only requirement is that
231 | // within the entry for each bundle you create and return the "express"
232 | // listener.
233 | /*
234 | apiServer: {
235 | srcEntryFile: './api/index.js',
236 | srcPaths: [
237 | './api',
238 | './shared',
239 | './config',
240 | ],
241 | outputPath: './build/api',
242 | }
243 | */
244 | },
245 |
246 | // These plugin definitions provide you with advanced hooks into customising
247 | // the project without having to reach into the internals of the tools.
248 | //
249 | // We have decided to create this plugin approach so that you can come to
250 | // a centralised configuration folder to do most of your application
251 | // configuration adjustments. Additionally it helps to make merging
252 | // from the origin starter kit a bit easier.
253 | plugins: {
254 | // This plugin allows you to provide final adjustments your babel
255 | // configurations for each bundle before they get processed.
256 | //
257 | // This function will be called once for each for your bundles. It will be
258 | // provided the current webpack config, as well as the buildOptions which
259 | // detail which bundle and mode is being targetted for the current function run.
260 | babelConfig: (babelConfig, buildOptions) => {
261 | // eslint-disable-next-line no-unused-vars
262 | const { target, mode } = buildOptions;
263 |
264 | // Example
265 | /*
266 | if (target === 'server' && mode === 'development') {
267 | babelConfig.presets.push('foo');
268 | }
269 | */
270 |
271 | return babelConfig;
272 | },
273 |
274 | // This plugin allows you to provide final adjustments your webpack
275 | // configurations for each bundle before they get processed.
276 | //
277 | // I would recommend looking at the "webpack-merge" module to help you with
278 | // merging modifications to each config.
279 | //
280 | // This function will be called once for each for your bundles. It will be
281 | // provided the current webpack config, as well as the buildOptions which
282 | // detail which bundle and mode is being targetted for the current function run.
283 | webpackConfig: (webpackConfig, buildOptions) => {
284 | // eslint-disable-next-line no-unused-vars
285 | const { target, mode } = buildOptions;
286 |
287 | // Example:
288 | /*
289 | if (target === 'server' && mode === 'development') {
290 | webpackConfig.plugins.push(new MyCoolWebpackPlugin());
291 | }
292 | */
293 |
294 | // Debugging/Logging Example:
295 | /*
296 | if (target === 'server') {
297 | console.log(JSON.stringify(webpackConfig, null, 4));
298 | }
299 | */
300 |
301 | return webpackConfig;
302 | },
303 | },
304 | };
305 |
306 | // This protects us from accidentally including this configuration in our
307 | // client bundle. That would be a big NO NO to do. :)
308 | if (process.env.BUILD_FLAG_IS_CLIENT === 'true') {
309 | throw new Error(
310 | "You shouldn't be importing the `/config/values.js` directly into code that will be included in your 'client' bundle as the configuration object will be sent to user's browsers. This could be a security risk! Instead, use the `config` helper function located at `/config/index.js`.",
311 | );
312 | }
313 |
314 | export default values;
315 |
--------------------------------------------------------------------------------
/internal/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "global-require": 0,
4 | "no-console": 0,
5 | "import/no-extraneous-dependencies": 0,
6 | "import/no-dynamic-require": 0,
7 | "import/newline-after-import": 0
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/internal/development/createVendorDLL.js:
--------------------------------------------------------------------------------
1 | import webpack from 'webpack';
2 | import { resolve as pathResolve } from 'path';
3 | import appRootDir from 'app-root-dir';
4 | import md5 from 'md5';
5 | import fs from 'fs';
6 | import config from '../../config';
7 | import { log } from '../utils';
8 |
9 | function createVendorDLL(bundleName, bundleConfig) {
10 | const dllConfig = config('bundles.client.devVendorDLL');
11 |
12 | // $FlowFixMe
13 | const pkg = require(pathResolve(appRootDir.get(), './package.json'));
14 |
15 | const devDLLDependencies = dllConfig.include.sort();
16 |
17 | // We calculate a hash of the package.json's dependencies, which we can use
18 | // to determine if dependencies have changed since the last time we built
19 | // the vendor dll.
20 | const currentDependenciesHash = md5(
21 | JSON.stringify(
22 | devDLLDependencies.map(dep => [dep, pkg.dependencies[dep], pkg.devDependencies[dep]]),
23 | // We do this to include any possible version numbers we may have for
24 | // a dependency. If these change then our hash should too, which will
25 | // result in a new dev dll build.
26 | ),
27 | );
28 |
29 | const vendorDLLHashFilePath = pathResolve(
30 | appRootDir.get(),
31 | bundleConfig.outputPath,
32 | `${dllConfig.name}_hash`,
33 | );
34 |
35 | function webpackConfigFactory() {
36 | return {
37 | // We only use this for development, so lets always include source maps.
38 | devtool: 'inline-source-map',
39 | entry: {
40 | [dllConfig.name]: devDLLDependencies,
41 | },
42 | output: {
43 | path: pathResolve(appRootDir.get(), bundleConfig.outputPath),
44 | filename: `${dllConfig.name}.js`,
45 | library: dllConfig.name,
46 | },
47 | plugins: [
48 | new webpack.DllPlugin({
49 | path: pathResolve(appRootDir.get(), bundleConfig.outputPath, `./${dllConfig.name}.json`),
50 | name: dllConfig.name,
51 | }),
52 | ],
53 | };
54 | }
55 |
56 | function buildVendorDLL() {
57 | return new Promise((resolve, reject) => {
58 | log({
59 | title: 'vendorDLL',
60 | level: 'info',
61 | message: `Vendor DLL build complete. The following dependencies have been included:\n\t-${devDLLDependencies.join('\n\t-')}\n`,
62 | });
63 |
64 | const webpackConfig = webpackConfigFactory();
65 | const vendorDLLCompiler = webpack(webpackConfig);
66 | vendorDLLCompiler.run((err) => {
67 | if (err) {
68 | reject(err);
69 | return;
70 | }
71 | // Update the dependency hash
72 | fs.writeFileSync(vendorDLLHashFilePath, currentDependenciesHash);
73 |
74 | resolve();
75 | });
76 | });
77 | }
78 |
79 | return new Promise((resolve, reject) => {
80 | if (!fs.existsSync(vendorDLLHashFilePath)) {
81 | // builddll
82 | log({
83 | title: 'vendorDLL',
84 | level: 'warn',
85 | message: `Generating a new "${bundleName}" Vendor DLL for boosted development performance.
86 | The Vendor DLL helps to speed up your development workflow by reducing Webpack build times. It does this by seperating Vendor DLLs from your primary bundles, thereby allowing Webpack to ignore them when having to rebuild your code for changes. We recommend that you add all your client bundle specific dependencies to the Vendor DLL configuration (within /config).`,
87 | });
88 | buildVendorDLL().then(resolve).catch(reject);
89 | } else {
90 | // first check if the md5 hashes match
91 | const dependenciesHash = fs.readFileSync(vendorDLLHashFilePath, 'utf8');
92 | const dependenciesChanged = dependenciesHash !== currentDependenciesHash;
93 |
94 | if (dependenciesChanged) {
95 | log({
96 | title: 'vendorDLL',
97 | level: 'warn',
98 | message: `New "${bundleName}" vendor dependencies detected. Regenerating the vendor dll...`,
99 | });
100 | buildVendorDLL().then(resolve).catch(reject);
101 | } else {
102 | log({
103 | title: 'vendorDLL',
104 | level: 'info',
105 | message: `No changes to existing "${bundleName}" vendor dependencies. Using the existing vendor dll.`,
106 | });
107 | resolve();
108 | }
109 | }
110 | });
111 | }
112 |
113 | export default createVendorDLL;
114 |
--------------------------------------------------------------------------------
/internal/development/hotClientServer.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import createWebpackMiddleware from 'webpack-dev-middleware';
3 | import createWebpackHotMiddleware from 'webpack-hot-middleware';
4 | import ListenerManager from './listenerManager';
5 | import config from '../../config';
6 | import { log } from '../utils';
7 |
8 | class HotClientServer {
9 | constructor(compiler) {
10 | const app = express();
11 |
12 | const httpPathRegex = /^https?:\/\/(.*):([\d]{1,5})/i;
13 | const httpPath = compiler.options.output.publicPath;
14 | if (!httpPath.startsWith('http') && !httpPathRegex.test(httpPath)) {
15 | throw new Error(
16 | 'You must supply an absolute public path to a development build of a web target bundle as it will be hosted on a seperate development server to any node target bundles.',
17 | );
18 | }
19 |
20 | // eslint-disable-next-line no-unused-vars
21 | const [_, host, port] = httpPathRegex.exec(httpPath);
22 |
23 | this.webpackDevMiddleware = createWebpackMiddleware(compiler, {
24 | quiet: true,
25 | noInfo: true,
26 | headers: {
27 | 'Access-Control-Allow-Origin': `http://${config('host')}:${config('port')}`,
28 | },
29 | // Ensure that the public path is taken from the compiler webpack config
30 | // as it will have been created as an absolute path to avoid conflicts
31 | // with an node servers.
32 | publicPath: compiler.options.output.publicPath,
33 | });
34 |
35 | app.use(this.webpackDevMiddleware);
36 | app.use(createWebpackHotMiddleware(compiler));
37 |
38 | const listener = app.listen(port);
39 |
40 | this.listenerManager = new ListenerManager(listener, 'client');
41 |
42 | compiler.plugin('compile', () => {
43 | log({
44 | title: 'client',
45 | level: 'info',
46 | message: 'Building new bundle...',
47 | });
48 | });
49 |
50 | compiler.plugin('done', (stats) => {
51 | if (stats.hasErrors()) {
52 | log({
53 | title: 'client',
54 | level: 'error',
55 | message: 'Build failed, please check the console for more information.',
56 | notify: true,
57 | });
58 | console.error(stats.toString());
59 | } else {
60 | log({
61 | title: 'client',
62 | level: 'info',
63 | message: 'Running with latest changes.',
64 | notify: true,
65 | });
66 | }
67 | });
68 | }
69 |
70 | dispose() {
71 | this.webpackDevMiddleware.close();
72 |
73 | return this.listenerManager ? this.listenerManager.dispose() : Promise.resolve();
74 | }
75 | }
76 |
77 | export default HotClientServer;
78 |
--------------------------------------------------------------------------------
/internal/development/hotDevelopment.js:
--------------------------------------------------------------------------------
1 | import { resolve as pathResolve } from 'path';
2 | import webpack from 'webpack';
3 | import appRootDir from 'app-root-dir';
4 | import { log } from '../utils';
5 | import HotNodeServer from './hotNodeServer';
6 | import HotClientServer from './hotClientServer';
7 | import createVendorDLL from './createVendorDLL';
8 | import webpackConfigFactory from '../webpack/configFactory';
9 | import config from '../../config';
10 |
11 | const usesDevVendorDLL = bundleConfig =>
12 | bundleConfig.devVendorDLL != null && bundleConfig.devVendorDLL.enabled;
13 |
14 | const vendorDLLsFailed = (err) => {
15 | log({
16 | title: 'vendorDLL',
17 | level: 'error',
18 | message: 'Unfortunately an error occured whilst trying to build the vendor dll(s) used by the development server. Please check the console for more information.',
19 | notify: true,
20 | });
21 | if (err) {
22 | console.error(err);
23 | }
24 | };
25 |
26 | const initializeBundle = (name, bundleConfig) => {
27 | const createCompiler = () => {
28 | try {
29 | const webpackConfig = webpackConfigFactory({
30 | target: name,
31 | mode: 'development',
32 | });
33 | // Install the vendor DLL config for the client bundle if required.
34 | if (name === 'client' && usesDevVendorDLL(bundleConfig)) {
35 | // Install the vendor DLL plugin.
36 | webpackConfig.plugins.push(
37 | new webpack.DllReferencePlugin({
38 | // $FlowFixMe
39 | manifest: require(pathResolve(
40 | appRootDir.get(),
41 | bundleConfig.outputPath,
42 | `${bundleConfig.devVendorDLL.name}.json`,
43 | )),
44 | }),
45 | );
46 | }
47 | return webpack(webpackConfig);
48 | } catch (err) {
49 | log({
50 | title: 'development',
51 | level: 'error',
52 | message: 'Webpack config is invalid, please check the console for more information.',
53 | notify: true,
54 | });
55 | console.error(err);
56 | throw err;
57 | }
58 | };
59 |
60 | return { name, bundleConfig, createCompiler };
61 | };
62 |
63 | class HotDevelopment {
64 | constructor() {
65 | this.hotClientServer = null;
66 | this.hotNodeServers = [];
67 |
68 | const clientBundle = initializeBundle('client', config('bundles.client'));
69 |
70 | const nodeBundles = [initializeBundle('server', config('bundles.server'))].concat(
71 | Object.keys(config('additionalNodeBundles')).map(name =>
72 | initializeBundle(name, config('additionalNodeBundles')[name]),
73 | ),
74 | );
75 |
76 | Promise.resolve(
77 | // First ensure the client dev vendor DLLs is created if needed.
78 | usesDevVendorDLL(config('bundles.client'))
79 | ? createVendorDLL('client', config('bundles.client'))
80 | : true,
81 | )
82 | // Then start the client development server.
83 | .then(
84 | () =>
85 | new Promise((resolve) => {
86 | const { createCompiler } = clientBundle;
87 | const compiler = createCompiler();
88 | compiler.plugin('done', (stats) => {
89 | if (!stats.hasErrors()) {
90 | resolve(compiler);
91 | }
92 | });
93 | this.hotClientServer = new HotClientServer(compiler);
94 | }),
95 | vendorDLLsFailed,
96 | )
97 | // Then start the node development server(s).
98 | .then((clientCompiler) => {
99 | this.hotNodeServers = nodeBundles.map(
100 | ({ name, createCompiler }) =>
101 | // $FlowFixMe
102 | new HotNodeServer(name, createCompiler(), clientCompiler),
103 | );
104 | });
105 | }
106 |
107 | dispose() {
108 | const safeDisposer = server => (server ? server.dispose() : Promise.resolve());
109 |
110 | // First the hot client server.
111 | return (
112 | safeDisposer(this.hotClientServer)
113 | // Then dispose the hot node server(s).
114 | .then(() => Promise.all(this.hotNodeServers.map(safeDisposer)))
115 | );
116 | }
117 | }
118 |
119 | export default HotDevelopment;
120 |
--------------------------------------------------------------------------------
/internal/development/hotNodeServer.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import appRootDir from 'app-root-dir';
3 | import { spawn } from 'child_process';
4 | import { log } from '../utils';
5 |
6 | class HotNodeServer {
7 | constructor(name, compiler, clientCompiler) {
8 | const compiledEntryFile = path.resolve(
9 | appRootDir.get(),
10 | compiler.options.output.path,
11 | `${Object.keys(compiler.options.entry)[0]}.js`,
12 | );
13 |
14 | const startServer = () => {
15 | if (this.server) {
16 | this.server.kill();
17 | this.server = null;
18 | log({
19 | title: name,
20 | level: 'info',
21 | message: 'Restarting server...',
22 | });
23 | }
24 |
25 | const newServer = spawn('node', [compiledEntryFile, '--color']);
26 |
27 | log({
28 | title: name,
29 | level: 'info',
30 | message: 'Server running with latest changes.',
31 | notify: true,
32 | });
33 |
34 | newServer.stdout.on('data', data => console.log(data.toString().trim()));
35 | newServer.stderr.on('data', (data) => {
36 | log({
37 | title: name,
38 | level: 'error',
39 | message: 'Error in server execution, check the console for more info.',
40 | });
41 | console.error(data.toString().trim());
42 | });
43 | this.server = newServer;
44 | };
45 |
46 | // We want our node server bundles to only start after a successful client
47 | // build. This avoids any issues with node server bundles depending on
48 | // client bundle assets.
49 | const waitForClientThenStartServer = () => {
50 | if (this.serverCompiling) {
51 | // A new server bundle is building, break this loop.
52 | return;
53 | }
54 | if (this.clientCompiling) {
55 | setTimeout(waitForClientThenStartServer, 50);
56 | } else {
57 | startServer();
58 | }
59 | };
60 |
61 | clientCompiler.plugin('compile', () => {
62 | this.clientCompiling = true;
63 | });
64 |
65 | clientCompiler.plugin('done', (stats) => {
66 | if (!stats.hasErrors()) {
67 | this.clientCompiling = false;
68 | }
69 | });
70 |
71 | compiler.plugin('compile', () => {
72 | this.serverCompiling = true;
73 | log({
74 | title: name,
75 | level: 'info',
76 | message: 'Building new bundle...',
77 | });
78 | });
79 |
80 | compiler.plugin('done', (stats) => {
81 | this.serverCompiling = false;
82 |
83 | if (this.disposing) {
84 | return;
85 | }
86 |
87 | try {
88 | if (stats.hasErrors()) {
89 | log({
90 | title: name,
91 | level: 'error',
92 | message: 'Build failed, check the console for more information.',
93 | notify: true,
94 | });
95 | console.log(stats.toString());
96 | return;
97 | }
98 |
99 | waitForClientThenStartServer();
100 | } catch (err) {
101 | log({
102 | title: name,
103 | level: 'error',
104 | message: 'Failed to start, please check the console for more information.',
105 | notify: true,
106 | });
107 | console.error(err);
108 | }
109 | });
110 |
111 | // Lets start the compiler.
112 | this.watcher = compiler.watch(null, () => undefined);
113 | }
114 |
115 | dispose() {
116 | this.disposing = true;
117 |
118 | const stopWatcher = new Promise((resolve) => {
119 | this.watcher.close(resolve);
120 | });
121 |
122 | return stopWatcher.then(() => {
123 | if (this.server) this.server.kill();
124 | });
125 | }
126 | }
127 |
128 | export default HotNodeServer;
129 |
--------------------------------------------------------------------------------
/internal/development/index.js:
--------------------------------------------------------------------------------
1 | import chokidar from 'chokidar';
2 | import { resolve as pathResolve } from 'path';
3 | import appRootDir from 'app-root-dir';
4 | import { log } from '../utils';
5 |
6 | let HotDevelopment = require('./hotDevelopment').default;
7 | let devServer = new HotDevelopment();
8 |
9 | // Any changes to our webpack bundleConfigs should restart the development devServer.
10 | const watcher = chokidar.watch([
11 | pathResolve(appRootDir.get(), 'internal'),
12 | pathResolve(appRootDir.get(), 'config'),
13 | ]);
14 |
15 | watcher.on('ready', () => {
16 | watcher.on('change', () => {
17 | log({
18 | title: 'webpack',
19 | level: 'warn',
20 | message: 'Project build configuration has changed. Restarting the development devServer...',
21 | });
22 | devServer.dispose().then(() => {
23 | // Make sure our new webpack bundleConfigs aren't in the module cache.
24 | Object.keys(require.cache).forEach((modulePath) => {
25 | if (modulePath.indexOf('config') !== -1) {
26 | delete require.cache[modulePath];
27 | } else if (modulePath.indexOf('internal') !== -1) {
28 | delete require.cache[modulePath];
29 | }
30 | });
31 |
32 | // Re-require the development devServer so that all new configs are used.
33 | HotDevelopment = require('./hotDevelopment').default;
34 |
35 | // Create a new development devServer.
36 | devServer = new HotDevelopment();
37 | });
38 | });
39 | });
40 |
41 | // If we receive a kill cmd then we will first try to dispose our listeners.
42 | process.on('SIGTERM', () => devServer && devServer.dispose().then(() => process.exit(0)));
43 |
--------------------------------------------------------------------------------
/internal/development/listenerManager.js:
--------------------------------------------------------------------------------
1 | const { log } = require('../utils');
2 |
3 | class ListenerManager {
4 | constructor(listener, name) {
5 | this.name = name || 'listener';
6 | this.lastConnectionKey = 0;
7 | this.connectionMap = {};
8 | this.listener = listener;
9 |
10 | // Track all connections to our server so that we can close them when needed.
11 | this.listener.on('connection', (connection) => {
12 | // Increment the connection key.
13 | this.lastConnectionKey += 1;
14 | // Generate a new key to represent the connection
15 | const connectionKey = this.lastConnectionKey;
16 | // Add the connection to our map.
17 | this.connectionMap[connectionKey] = connection;
18 | // Remove the connection from our map when it closes.
19 | connection.on('close', () => {
20 | delete this.connectionMap[connectionKey];
21 | });
22 | });
23 | }
24 |
25 | killAllConnections() {
26 | Object.keys(this.connectionMap).forEach((connectionKey) => {
27 | this.connectionMap[connectionKey].destroy();
28 | });
29 | }
30 |
31 | dispose() {
32 | return new Promise((resolve) => {
33 | if (this.listener) {
34 | this.killAllConnections();
35 |
36 | log({
37 | title: this.name,
38 | level: 'info',
39 | message: 'Destroyed all existing connections.',
40 | });
41 |
42 | this.listener.close(() => {
43 | log({
44 | title: this.name,
45 | level: 'info',
46 | message: 'Closed listener.',
47 | });
48 |
49 | resolve();
50 | });
51 | } else {
52 | resolve();
53 | }
54 | });
55 | }
56 | }
57 |
58 | export default ListenerManager;
59 |
--------------------------------------------------------------------------------
/internal/docs/ADDING_AN_API_BUNDLE.md:
--------------------------------------------------------------------------------
1 | # Adding an "API" Bundle
2 |
3 | A fairly common requirement for a project that scales is to create additional servers bundles, e.g. an API server.
4 |
5 | Instead of requiring you to hack the Webpack configuration we have have provided a section within the centralised project configuration that allows you to easily declare additional bundles. You simply need to provide the source, entry, and output paths - we take care of the rest.
6 |
7 | _IMPORTANT:_ One further requirement for this feature is that within your new server bundle you export the created http listener. This exported listener will be used by the development server so that it can automatically restart your server any time the source files for it change.
8 |
--------------------------------------------------------------------------------
/internal/docs/DEPLOY_TO_NOW.md:
--------------------------------------------------------------------------------
1 | - [Project Overview](/internal/docs/PROJECT_OVERVIEW.md)
2 | - [Project Configuration](/internal/docs/PROJECT_CONFIG.md)
3 | - [Package Script Commands](/internal/docs/PKG_SCRIPTS.md)
4 | - [Feature Branches](/internal/docs/FEATURE_BRANCHES.md)
5 | - __[Deploy your very own Server Side Rendering React App in 5 easy steps](/internal/docs/DEPLOY_TO_NOW.md)__
6 | - [FAQ](/internal/docs/FAQ.md)
7 |
8 | # Deploy your very own "React, Universally" App in 5 easy steps
9 |
10 | __Step 1: Clone the repository.__
11 |
12 | git clone https://github.com/ctrlplusb/react-universally
13 |
14 | __Step 2: `cd` into the cloned directory__
15 |
16 | cd react-universally
17 |
18 | __Step 3: Install the project's dependencies__
19 |
20 | npm install
21 |
22 | __Step 4: Install the awesome [`now`](https://zeit.co/now) CLI globally__
23 |
24 | npm install -g now
25 |
26 | __Step 5: Deploy to "now"__
27 |
28 | npm run deploy
29 |
30 | That's it. Your clipboard will contain the address of the deployed app. Open your browser, paste, go. These guys are seriously awesome hosts. [Check them out.](https://zeit.co/now)
31 |
--------------------------------------------------------------------------------
/internal/docs/FAQ.md:
--------------------------------------------------------------------------------
1 | - [Project Overview](/internal/docs/PROJECT_OVERVIEW.md)
2 | - [Project Configuration](/internal/docs/PROJECT_CONFIG.md)
3 | - [Package Script Commands](/internal/docs/PKG_SCRIPTS.md)
4 | - [Feature Branches](/internal/docs/FEATURE_BRANCHES.md)
5 | - [Deploy your very own Server Side Rendering React App in 5 easy steps](/internal/docs/DEPLOY_TO_NOW.md)
6 | - __[FAQ](/internal/docs/FAQ.md)__
7 |
8 | # Frequently Asked Questions
9 |
10 | ___Q:___ __Why do you structure your dependencies like they are?__
11 |
12 | The dependencies within `package.json` are structured so that the libraries required to transpile/bundle the source are contained within the `devDependencies` section, whilst the libraries required during the server runtime are contained within the `dependencies` section.
13 |
14 | If you perform build tasks on your production environment you must ensure that you have allowed the installation of the `devDependencies` too (Heroku, for example doesn't do this by default).
15 |
16 | There have been talks about creating a "dist" build, which would avoid target environment build steps however Webpack has an issue with bundle node_module dependencies if they include `require` statements using expressions/variables to resolve the module names.
17 |
18 | ___Q:___ __My project fails to build and execute when I deploy it to my host__
19 |
20 | The likely issue in this case, is that your hosting provider doesn't install the `devDependencies` by default. The dependencies within `package.json` are structured so that the libraries required to transpile/bundle the source are contained within the `devDependencies` section, whilst the libraries required during the server runtime are contained within the `dependencies` section.
21 | You two options to fix this:
22 |
23 | 1. Prebuild your project and then deploy it along with the build output.
24 | 2. Change your host configuration so that it will install the `devDependencies` too. In the case of Heroku for example see [here](https://devcenter.heroku.com/articles/nodejs-support#devdependencies).
25 |
26 | ___Q:___ __How do I keep my project up to date with changes/fixes made on `react-universally`?__
27 |
28 | This project wants to be a base starter kit allowing you to mould it as you like for each of your project's needs. This comes with the trade off that updates/fixes will be more "involved" to apply.
29 |
30 | One example workflow is:
31 |
32 | ```bash
33 | # First clone this repo
34 | git clone https://github.com/ctrlplusb/react-universally my-project
35 |
36 | # Go into your project
37 | cd my-project
38 |
39 | # Now rename the "origin" git remote to "upstream"
40 | git remote rename origin upstream
41 |
42 | # I would then recommend creating a hosted repository for your
43 | # project.
44 |
45 | # Then add your newly created repository as the new "origin"
46 | git remote add origin https://github.com/my-github-username/my-project
47 |
48 | # Then push the master branch. This will also bind it to new
49 | # "origin" remote.
50 | git push -u origin master
51 |
52 | # You can now code/commit/push to origin as normal.
53 | # If you want to at some stage get new changes from the
54 | # react-universally project, then do something like this:
55 |
56 | # First fetch the latest changes
57 | git fetch upstream
58 |
59 | # Then merge them into your project
60 | git merge upstream/master
61 |
62 | # Deal with the merge conflicts, delete the package-lock.json file and
63 | # rebuild it, then commit and push.
64 | ```
65 |
66 | ___Q:___ __My development server starts and bundles correctly, but the JavaScript bundles don't load. What causes this to happen?__
67 |
68 | Chances are you might be running on Windows. By default the server is bound to `0.0.0.0` for compatibility with Docker and other services. Everything is functioning correctly. The server listens fine on `0.0.0.0` and the problem is only client-side. Windows doesn't like to connecting to `0.0.0.0`. Change the host value in `config/values.js` to `localhost` or `127.0.0.1`. Another option is to specify `HOST=127.0.0.1` in the develop task within your `package.json` or `.env` file.
69 |
70 |
--------------------------------------------------------------------------------
/internal/docs/FEATURE_BRANCHES.md:
--------------------------------------------------------------------------------
1 | - [Project Overview](/internal/docs/PROJECT_OVERVIEW.md)
2 | - [Project Configuration](/internal/docs/PROJECT_CONFIG.md)
3 | - [Package Script Commands](/internal/docs/PKG_SCRIPTS.md)
4 | - __[Feature Branches](/internal/docs/FEATURE_BRANCHES.md)__
5 | - [Deploy your very own Server Side Rendering React App in 5 easy steps](/internal/docs/DEPLOY_TO_NOW.md)
6 | - [FAQ](/internal/docs/FAQ.md)
7 |
8 | # Feature Branches
9 |
10 | Below are a list of extensions to this repository, in the form of branches. Each of them has been tailored to add an individual technology. It is possible to merge multiple branches together in order to create a technology mix that suits your project's needs. We'll detail this workflow after the repository list.
11 |
12 | - [`apollo`](https://github.com/ctrlplusb/react-universally/tree/feature/apollo) - Adds the Apollo Stack (i.e. Graphql).
13 | - [`mobx`](https://github.com/andreyluiz/react-universally/tree/feature/mobx) - Adds MobX as a state management library.
14 | - [`postcss-sass`](https://github.com/ctrlplusb/react-universally/tree/feature/postcss-sass) - Adds PostCSS and SASS.
15 | - [`redux-opinionated`](https://github.com/ctrlplusb/react-universally/tree/feature/redux-opinionated) - Adds an opinionated Redux implementation, using `redux-thunk` and `react-jobs` to support data loading across the client/server.
16 |
17 | If you would like to add a new feature branch log an issue describing your chosen technology and we can come up with a plan together. :)
18 |
19 | ## An example workflow
20 |
21 | Ok, so how do you go about creating a repo that uses a mix mash of these feature branches? Well, say you wanted a combo of `apollo` and `styletron`, you could do the following:
22 |
23 | > _NOTE:_ Merging the yarn.lock file is messy in my opinion. I rather select "merge all" from "theirs" or "ours" and then after the merge I delete the yarn.lock file and run the `yarn` command to rebuild it properly.
24 |
25 | ```bash
26 | # First clone this repo
27 | git clone https://github.com/ctrlplusb/react-universally my-project
28 |
29 | # Go into your project
30 | cd my-project
31 |
32 | # Now rename the "origin" git remote to "upstream"
33 | git remote rename origin upstream
34 |
35 | # I would then recommend creating a hosted repository for your
36 | # project.
37 |
38 | # Then add your newly created repository as the new "origin"
39 | git remote add origin https://github.com/my-github-username/my-project
40 |
41 | # Then push the master branch. This will also bind it to new
42 | # "origin" remote.
43 | git push -u origin master
44 |
45 | # Ok, so now you need to choose and merge each feature branch.
46 |
47 | # -------------------------------------------------------------
48 | # First up, apollo:
49 |
50 | # First fetch the latest changes from the upstream
51 | git fetch upstream
52 |
53 | # Then merge the apollo branch into your project
54 | git merge upstream/feature/apollo
55 |
56 | # Deal with the merge conflicts, delete the yarn.lock file and
57 | # rebuild it, then commit and push.
58 |
59 | # -------------------------------------------------------------
60 | # Next, styletron:
61 |
62 | # First fetch the latest changes from the upstream
63 | git fetch upstream
64 |
65 | # Then merge the styletron branch into your project
66 | git merge upstream/feature/styletron
67 |
68 | # Deal with the merge conflicts, delete the yarn.lock file and
69 | # rebuild it, then commit and push.
70 |
71 | # --------------------------------------------------------------
72 |
73 | # You now have an apollo SSR app with styletron powered styles!
74 |
75 | # Any time you want to pull changes from one of the branches
76 | # simply repeat:
77 | git fetch upstream
78 | git merge upstream/feature/FEATURENAME
79 | # deal with conflicts, rebuild yarn.lock, commit, push
80 | ```
81 |
--------------------------------------------------------------------------------
/internal/docs/PKG_SCRIPTS.md:
--------------------------------------------------------------------------------
1 | - [Project Overview](/internal/docs/PROJECT_OVERVIEW.md)
2 | - [Project Configuration](/internal/docs/PROJECT_CONFIG.md)
3 | - __[Package Script Commands](/internal/docs/PKG_SCRIPTS.md)__
4 | - [Feature Branches](/internal/docs/FEATURE_BRANCHES.md)
5 | - [Deploy your very own Server Side Rendering React App in 5 easy steps](/internal/docs/DEPLOY_TO_NOW.md)
6 | - [FAQ](/internal/docs/FAQ.md)
7 |
8 | # Package Scripts
9 |
10 | ## `npm run analyze:client`
11 |
12 | Creates an `webpack-bundle-analyze` session against the production build of the client bundle.
13 |
14 | ## `npm run analyze:server`
15 |
16 | Creates an `webpack-bundle-analyze` session against the production build of the server bundle.
17 |
18 | ## `npm run build`
19 |
20 | Builds the client and server bundles, with the output being optimized.
21 |
22 | ## `npm run build:dev`
23 |
24 | Builds the client and server bundles, with the output including development related code.
25 |
26 | ## `npm run clean`
27 |
28 | Deletes any build output that would have originated from the other commands.
29 |
30 | ## `npm run deploy`
31 |
32 | Deploys your application to [`now`](https://zeit.co/now). If you haven't heard of these guys, please check them out. They allow you to hit the ground running! I've included them within this repo as it requires almost zero configuration to allow your project to be deployed to their servers.
33 |
34 | ## `npm run develop`
35 |
36 | Starts a development server for both the client and server bundles. We use `react-hot-loader` v3 to power the hot reloading of the client bundle, whilst a filesystem watch is implemented to reload the server bundle when any changes have occurred.
37 |
38 | ## `npm run lint`
39 |
40 | Executes `eslint` against the project. Alternatively you could look to install the `eslint-loader` and integrate it into the `webpack` bundle process.
41 |
42 | ## `npm run start`
43 |
44 | Executes the server. It expects you to have already built the bundles using the `npm run build` command.
45 |
46 | ## `npm run test`
47 |
48 | Runs the `jest` tests.
49 |
50 | ## `npm run test:coverage`
51 |
52 | Runs the `jest` tests and generates a coverage report. I recommend you look at [codecov.io](https://codecov.io) to host your coverage reports.
53 |
--------------------------------------------------------------------------------
/internal/docs/PROJECT_CONFIG.md:
--------------------------------------------------------------------------------
1 | - [Project Overview](/internal/docs/PROJECT_OVERVIEW.md)
2 | - __[Project Configuration](/internal/docs/PROJECT_CONFIG.md)__
3 | - [Package Script Commands](/internal/docs/PKG_SCRIPTS.md)
4 | - [Feature Branches](/internal/docs/FEATURE_BRANCHES.md)
5 | - [Deploy your very own Server Side Rendering React App in 5 easy steps](/internal/docs/DEPLOY_TO_NOW.md)
6 | - [FAQ](/internal/docs/FAQ.md)
7 |
8 | # Project Configuration
9 |
10 | The application configuration has been centralised to live within the `/config` folder.
11 |
12 | You read configuration values using the `/config/index.js` helper, and you edit the configuration values in the `/config/values.js` file.
13 |
14 | ## TOC
15 |
16 | - [Background and Usage](#background-and-usage)
17 | - [Declaring the configuration values that are safe for client bundles](#declaring-the-configuration-values-that-are-safe-for-client-bundles)
18 | - [Environment Specific Values](#environment-specifc-values)
19 |
20 | ## Background and Usage
21 |
22 | Below are some of the problems that we faced, and how we ended up with our current implementation...
23 |
24 | As this is a universal application you are mostly creating code that is shared between your "client" and "server" bundles. The "client" is sent across the wire to be executed in the browser therefore you have to be extra careful in what you include in the bundle. Webpack by default bundles code if it is imported by your target entry file (or it's dependencies). Therefore if you were to import the application configuration values within a module, the entire application configuration would be included with your "client" bundle. This is extremely risky as the configuration exposes the internal structure of your application and may contain sensitive data such as database connection strings.
25 |
26 | One possible solution to the above would be to use Webpack's `DefinePlugin` in order to statically inject/replace only the required configuration values into our client bundle. However, these configuration values are statically bound during our build step, meaning that we are unable to expose execution time provided values (e.g. `FOO=bar npm run start`) to our client bundle. Therefore we decided on a strategy of making the server be responsible for attaching a configuration object to `window.__CLIENT_CONFIG__` within the HTML response that gets sent to the browser. This would then allow us to ensure that environment variables can be properly exposed. This works well, however, it introduces a new problem, we want a unified API to read configuration values without having to figure out if the code is in a browser/server context.
27 |
28 | For this we created a helper function in the root of the `config` folder. It is located in `/config/index.js`. You can use it like so:
29 |
30 | ```js
31 | import config from '../config';
32 |
33 | export function MyComponent() {
34 | return
{config('welcomeMessage')}
;
35 | }
36 | ```
37 |
38 | The `config` helper allows you to specify nested path structures in the form of a dot-notated string or array. For example the following resolve to the same config value:
39 |
40 | ```js
41 | config('messages.welcome');
42 | config(['messages', 'welcome']);
43 | ```
44 |
45 | The `config` helper is also configured to throw helpful error messages when trying to request configuration values that either do not exist or have not been exposed to the client bundles.
46 |
47 | ## Declaring the configuration values that are safe for client bundles
48 |
49 | Within the centralised config (`/config/values.js`) you will see that a `clientConfigFilter` property. This value is a ruleset/filter that details which of the configuration values you deem required (and safe) for inclusion within your client bundles. Please go to this section of the configuration file for more detail on how this filtering mechanism works.
50 |
51 | When a server request is being processed this filtering configuration export will be serialised and attached to the `window.__CLIENT_CONFIG__` within the HTML response, thereby allowing our browser executed code to have access to the respective configuration values.
52 |
53 | ## Environment Specific Values
54 |
55 | Environment specific values are support via host system environment variables (e.g. `FOO=bar npm run start`) and/or by providing an "env" file.
56 |
57 | "env" files is an optional feature that is supported by the [`dotenv`](https://github.com/motdotla/dotenv) module. This module allows you to define files containing key/value pairs representing your required environment variables (e.g. `PORT=1337`). To use this feature create an `.env` file within the root of the project (we have provided an example file called `.env_example`, which contains all the environment variables this project currently relies on).
58 |
59 | > Note: The `.env` file has been ignored from the git repository in anticipation that it will most likely be used to house development specific configuration.
60 |
61 | We generally recommend that you don't persist any "env" files within the repository, and instead rely on your target host environments and/or deployment servers to provide the necessary values per environment.
62 |
63 | If you do however have the requirement to create and persist "env" files for multiple target environments, the system does support it. To do so create a ".env" file that is postfix'ed with the environment you are targeting. For e.g. `.env.development` or `.env.staging` or `.env.production`.
64 |
65 | In order to target a specific environment configuration file you have to provide a matching `DEPLOYMENT` environment variable. For example:
66 |
67 | ```bash
68 | npm run build
69 | DEPLOYMENT=staging npm run start # This will look for a .env.staging file
70 | ```
71 |
72 | > Note: you may be used to using NODE_ENV to distinguish between environment configuration, however, when using the React ecosystem it is highly recommended that you set NODE_ENV=production any time you want an optimised version of React (and other libs). Given this requirement, we instead defer to the use of a "DEPLOYMENT" variable. See [here](https://github.com/facebook/react/issues/6582) for more info on this.
73 |
74 | > Note: if an environment specific configuration file exists, it will be used over the more generic `.env` file.
75 |
76 | As stated before, the application has been configured to accept a mix-match of sources for the environment variables. i.e. you can provide some/all of the environment variables via a `.env` file, and others via the cli/host (e.g. `FOO=bar npm run build`). This gives you greater flexibility and grants you the opportunity to control the provision of sensitive values (e.g. db connection string). Please do note that "env" file values will take preference over any values provided by the host/CLI.
77 |
78 | > Note: It is recommended that you bind your environment configuration values to the global `./config/values.js`. See the existing items within as an example.
79 |
--------------------------------------------------------------------------------
/internal/docs/PROJECT_OVERVIEW.md:
--------------------------------------------------------------------------------
1 | - __[Project Overview](/internal/docs/PROJECT_OVERVIEW.md)__
2 | - [Project Configuration](/internal/docs/PROJECT_CONFIG.md)
3 | - [Package Script Commands](/internal/docs/PKG_SCRIPTS.md)
4 | - [Feature Branches](/internal/docs/FEATURE_BRANCHES.md)
5 | - [Deploy your very own Server Side Rendering React App in 5 easy steps](/internal/docs/DEPLOY_TO_NOW.md)
6 | - [FAQ](/internal/docs/FAQ.md)
7 |
8 | # Project Overview
9 |
10 | Below is a general overview of the project.
11 |
12 | ## ToC
13 |
14 | - [Bundled by Webpack](#bundled-by-webpack)
15 | - [Transpiled by Babel](#transpiled-by-babel)
16 | - [Security](#security)
17 | - [Folder Structure](#folder-structure)
18 |
19 | ## Bundled by Webpack
20 |
21 | This starter uses Webpack 3 to produce bundles for both the client and the server. The `internal/webpack/configFactory.js` is used to generate the respective Webpack configuration for all our bundles. The factory is heavily commented to help you understand what is going on within the Webpack configuration.
22 |
23 | > Note: Given that we are bundling our server code I have included the `source-map-support` module to ensure that we still get nice stack traces when executing our code.
24 |
25 | ## Transpiled by Babel
26 |
27 | We use babel across the entire project, which allows us to use the same level of javascript (e.g. es2015/2016/2017) without having to worry which level of the language is supported within each of the project's modules. We have decided to only support syntax that is stage-3 or up in the TC39 process, anything lower is considered too much of a risk to include by default, so it is up to you if you would like to extend your Babel configuration to include more "experimental" features.
28 |
29 | We additionally make use of the `babel-preset-env` preset so that we only transpile the syntax that is not supported by target node platforms.
30 |
31 | ## Security
32 |
33 | We make use of the `helmet` and `hpp` middleware libraries to provide a fairly advanced security configuration for our Express server, attempting to follow industry best practices. If you are unfamiliar with Content Security Policies then I highly recommend that you do some reading on the subject:
34 |
35 | - https://content-security-policy.com/
36 | - https://developers.google.com/web/fundamentals/security/csp/
37 | - https://developer.mozilla.org/en/docs/Web/Security/CSP
38 | - https://helmetjs.github.io/docs/csp/
39 |
40 | For example, if you are relying on scripts/styles/assets from CDN or from any other server/application that is not hosted on the same URL as your application then you will need to explicitly add the respective CSN/Server URLs to the security middleware within the project. For example you can see I have had to add the polyfill.io CDN in order to allow us to use the polyfill script.
41 |
42 | You may find CSPs annoying at first, but it is a great habit to build. The CSP configuration is an optional item for helmet, however you should not remove it without making a serious consideration that you do not require the added security.
43 |
44 | ## Folder Structure
45 |
46 | Below are some of the critical folders of the project along with a comment describing them.
47 |
48 | ```
49 | /
50 | |- config // Centralised project configuration.
51 | | |- values.js // Configuration values
52 | | |- index.js // Unified Configuration Reader API
53 | |
54 | |- build // The target output dir for our build commands.
55 | | |- client // The built client module.
56 | | |- server // The built server module.
57 | |
58 | |- server // The server bundle entry and specific source.
59 | |- client // The client bundle entry and specific source.
60 | |- shared // The shared code between the bundles.
61 | |
62 | |- internal
63 | | |- docs // Documentation
64 | | |- development // Development server.
65 | | |- webpack
66 | | |- configFactory.js // Webpack configuration builder.
67 | |
68 | |- .env_example // An example from which to create your own .env file.
69 | ```
70 |
71 | I highly recommend putting most of your application code into the `shared` folder where possible. Then put anything that is specific to the `server`/`client` within their respective folder.
72 |
--------------------------------------------------------------------------------
/internal/jest/assetMock.js:
--------------------------------------------------------------------------------
1 | module.exports = '/asset/mock';
2 |
--------------------------------------------------------------------------------
/internal/jest/styleMock.js:
--------------------------------------------------------------------------------
1 | // internal/test/styleMock.js
2 | // Return an object to emulate css modules (if you are using them)
3 | module.exports = {};
4 |
--------------------------------------------------------------------------------
/internal/scripts/analyze.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This script creates a webpack stats file on our production build of the
3 | * client bundle and then launches the webpack-bundle-analyzer tool allowing
4 | * you to easily see what is being included within your bundle.
5 | *
6 | * @see https://github.com/th0r/webpack-bundle-analyzer
7 | */
8 |
9 | import webpack from 'webpack';
10 | import fs from 'fs';
11 | import { resolve as pathResolve } from 'path';
12 | import appRootDir from 'app-root-dir';
13 | import webpackConfigFactory from '../webpack/configFactory';
14 | import { exec } from '../utils';
15 | import config from '../../config';
16 |
17 | // eslint-disable-next-line no-unused-vars
18 | const [x, y, ...args] = process.argv;
19 | const analyzeServer = args.findIndex(arg => arg === '--server') !== -1;
20 | const analyzeClient = args.findIndex(arg => arg === '--client') !== -1;
21 |
22 | let target;
23 |
24 | if (analyzeServer) target = 'server';
25 | else if (analyzeClient) target = 'client';
26 | else throw new Error('Please specify --server OR --client as target');
27 |
28 | const anaylzeFilePath = pathResolve(
29 | appRootDir.get(),
30 | config('bundles.client.outputPath'),
31 | '__analyze__.json',
32 | );
33 |
34 | const clientCompiler = webpack(webpackConfigFactory({ target, optimize: true }));
35 |
36 | clientCompiler.run((err, stats) => {
37 | if (err) {
38 | console.error(err);
39 | } else {
40 | // Write out the json stats file.
41 | fs.writeFileSync(anaylzeFilePath, JSON.stringify(stats.toJson('verbose'), null, 4));
42 |
43 | // Run the bundle analyzer against the stats file.
44 | const cmd = `webpack-bundle-analyzer ${anaylzeFilePath} ${config('bundles.client.outputPath')}`;
45 | exec(cmd);
46 | }
47 | });
48 |
--------------------------------------------------------------------------------
/internal/scripts/build.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This script builds a production output of all of our bundles.
3 | */
4 |
5 | import webpack from 'webpack';
6 | import appRootDir from 'app-root-dir';
7 | import { resolve as pathResolve } from 'path';
8 | import webpackConfigFactory from '../webpack/configFactory';
9 | import { exec } from '../utils';
10 | import config from '../../config';
11 |
12 | // eslint-disable-next-line no-unused-vars
13 | const [x, y, ...args] = process.argv;
14 |
15 | const optimize = args.findIndex(arg => arg === '--optimize') !== -1;
16 |
17 | // First clear the build output dir.
18 | exec(`rimraf ${pathResolve(appRootDir.get(), config('buildOutputPath'))}`);
19 |
20 | // Get our "fixed" bundle names
21 | Object.keys(config('bundles'))
22 | // And the "additional" bundle names
23 | .concat(Object.keys(config('additionalNodeBundles')))
24 | // And then build them all.
25 | .forEach((bundleName) => {
26 | const compiler = webpack(webpackConfigFactory({ target: bundleName, optimize }));
27 | compiler.run((err, stats) => {
28 | if (err) {
29 | console.error(err);
30 | return;
31 | }
32 | console.log(stats.toString({ colors: true }));
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/internal/scripts/clean.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This script removes any exisitng build output.
3 | */
4 |
5 | import { resolve as pathResolve } from 'path';
6 | import appRootDir from 'app-root-dir';
7 | import rimraf from 'rimraf';
8 | import config from '../../config';
9 |
10 | function clean() {
11 | rimraf(pathResolve(appRootDir.get(), config('buildOutputPath')), () => {
12 | console.log(`Cleaned ${pathResolve(appRootDir.get(), config('buildOutputPath'))}`);
13 | });
14 | }
15 |
16 | clean();
17 |
--------------------------------------------------------------------------------
/internal/scripts/deploy.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Deploys to now.
3 | * @see https://zeit.co/now
4 | */
5 |
6 | import { exec } from '../utils';
7 | const cmd = 'now';
8 | exec(cmd);
9 |
--------------------------------------------------------------------------------
/internal/scripts/preinstall.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This script will ensure that users are using a supported version of node
3 | * for the project.
4 | *
5 | * NOTE: Ensure this script uses ES5 only as the user may be running an old
6 | * version of Node, which this script wants to test against.
7 | */
8 |
9 | /* eslint-disable */
10 |
11 | var exec = require('child_process').exec;
12 | var existsSync = require('fs').existsSync;
13 | var pathResolve = require('path').resolve;
14 |
15 | if (existsSync(pathResolve(__dirname, '../../node_modules'))){
16 | // An install has already occurred.
17 | return;
18 | }
19 |
20 | // Inspired by "create-react-app". Thanks @gaearon :)
21 | function checkNodeVersion() {
22 | var semver = require('semver');
23 |
24 | if (!semver.satisfies(process.version, packageJson.engines.node)) {
25 | console.error(
26 | 'You are currently running Node %s but %s requires %s. Please use a supported version of Node.\n',
27 | process.version,
28 | packageJson.name,
29 | packageJson.engines.node
30 | );
31 | process.exit(1);
32 | }
33 | }
34 |
35 | var packageJson = require('../../package.json');
36 | if (!packageJson.engines
37 | || !packageJson.engines.node
38 | || !packageJson.devDependencies
39 | || !packageJson.devDependencies.semver) {
40 | // The package has already been customised. Ignore this script.
41 | return;
42 | }
43 |
44 | exec(
45 | 'npm install semver@' + packageJson.devDependencies.semver,
46 | function installSemverCb(err, stdout, stderr) {
47 | if (err) throw err;
48 | checkNodeVersion();
49 | }
50 | )
51 |
--------------------------------------------------------------------------------
/internal/utils.js:
--------------------------------------------------------------------------------
1 | import HappyPack from 'happypack';
2 | import notifier from 'node-notifier';
3 | import colors from 'colors/safe';
4 | import { execSync } from 'child_process';
5 | import appRootDir from 'app-root-dir';
6 |
7 | // Generates a HappyPack plugin.
8 | // @see https://github.com/amireh/happypack/
9 | export function happyPackPlugin({ name, loaders }) {
10 | return new HappyPack({
11 | id: name,
12 | verbose: false,
13 | threads: 4,
14 | loaders,
15 | });
16 | }
17 |
18 | export function log(options) {
19 | const title = `${options.title.toUpperCase()}`;
20 |
21 | if (options.notify) {
22 | notifier.notify({
23 | title,
24 | message: options.message,
25 | });
26 | }
27 |
28 | const level = options.level || 'info';
29 | const msg = `${title}: ${options.message}`;
30 |
31 | switch (level) {
32 | case 'warn':
33 | console.log(colors.yellow(msg));
34 | break;
35 | case 'error':
36 | console.log(colors.bgRed.white(msg));
37 | break;
38 | case 'special':
39 | console.log(colors.italic.cyan(msg));
40 | break;
41 | case 'info':
42 | default:
43 | console.log(colors.green.dim(msg));
44 | }
45 | }
46 |
47 | export function exec(command) {
48 | execSync(command, { stdio: 'inherit', cwd: appRootDir.get() });
49 | }
50 |
--------------------------------------------------------------------------------
/internal/webpack/configFactory.js:
--------------------------------------------------------------------------------
1 | import appRootDir from 'app-root-dir';
2 | import AssetsPlugin from 'assets-webpack-plugin';
3 | import ExtractTextPlugin from 'extract-text-webpack-plugin';
4 | import nodeExternals from 'webpack-node-externals';
5 | import path from 'path';
6 | import webpack from 'webpack';
7 | import WebpackMd5Hash from 'webpack-md5-hash';
8 |
9 | import { happyPackPlugin, log } from '../utils';
10 | import { ifElse } from '../../shared/utils/logic';
11 | import { mergeDeep } from '../../shared/utils/objects';
12 | import { removeNil } from '../../shared/utils/arrays';
13 | import withServiceWorker from './withServiceWorker';
14 | import config from '../../config';
15 |
16 | /**
17 | * Generates a webpack configuration for the target configuration.
18 | *
19 | * This function has been configured to support one "client/web" bundle, and any
20 | * number of additional "node" bundles (e.g. our "server"). You can define
21 | * additional node bundles by editing the project confuguration.
22 | *
23 | * @param {Object} buildOptions - The build options.
24 | * @param {target} buildOptions.target - The bundle target (e.g 'clinet' || 'server').
25 | * @param {target} buildOptions.optimize - Build an optimised version of the bundle?
26 | *
27 | * @return {Object} The webpack configuration.
28 | */
29 | export default function webpackConfigFactory(buildOptions) {
30 | const { target, optimize = false } = buildOptions;
31 |
32 | const isProd = optimize;
33 | const isDev = !isProd;
34 | const isClient = target === 'client';
35 | const isServer = target === 'server';
36 | const isNode = !isClient;
37 |
38 | // Preconfigure some ifElse helper instnaces. See the util docs for more
39 | // information on how this util works.
40 | const ifDev = ifElse(isDev);
41 | const ifProd = ifElse(isProd);
42 | const ifNode = ifElse(isNode);
43 | const ifClient = ifElse(isClient);
44 | const ifDevClient = ifElse(isDev && isClient);
45 | const ifProdClient = ifElse(isProd && isClient);
46 |
47 | log({
48 | level: 'info',
49 | title: 'Webpack',
50 | message: `Creating ${isProd
51 | ? 'an optimised'
52 | : 'a development'} bundle configuration for the "${target}"`,
53 | });
54 |
55 | const bundleConfig =
56 | isServer || isClient
57 | ? // This is either our "server" or "client" bundle.
58 | config(['bundles', target])
59 | : // Otherwise it must be an additional node bundle.
60 | config(['additionalNodeBundles', target]);
61 |
62 | if (!bundleConfig) {
63 | throw new Error('No bundle configuration exists for target:', target);
64 | }
65 |
66 | let webpackConfig = {
67 | // Define our entry chunks for our bundle.
68 | entry: {
69 | // We name our entry files "index" as it makes it easier for us to
70 | // import bundle output files (e.g. `import server from './build/server';`)
71 | index: removeNil([
72 | // We are using polyfill.io instead of the very heavy babel-polyfill.
73 | // Therefore we need to add the regenerator-runtime as polyfill.io
74 | // doesn't support this.
75 | ifClient('regenerator-runtime/runtime'),
76 | // Extends hot reloading with the ability to hot path React Components.
77 | // This should always be at the top of your entries list. Only put
78 | // polyfills above it.
79 | ifDevClient('react-hot-loader/patch'),
80 | // Required to support hot reloading of our client.
81 | ifDevClient(
82 | () =>
83 | `webpack-hot-middleware/client?reload=true&path=http://${config('host')}:${config(
84 | 'clientDevServerPort',
85 | )}/__webpack_hmr`,
86 | ),
87 | // The source entry file for the bundle.
88 | path.resolve(appRootDir.get(), bundleConfig.srcEntryFile),
89 | ]),
90 | },
91 |
92 | // Bundle output configuration.
93 | output: {
94 | // The dir in which our bundle should be output.
95 | path: path.resolve(appRootDir.get(), bundleConfig.outputPath),
96 | // The filename format for our bundle's entries.
97 | filename: ifProdClient(
98 | // For our production client bundles we include a hash in the filename.
99 | // That way we won't hit any browser caching issues when our bundle
100 | // output changes.
101 | // Note: as we are using the WebpackMd5Hash plugin, the hashes will
102 | // only change when the file contents change. This means we can
103 | // set very aggressive caching strategies on our bundle output.
104 | '[name]-[chunkhash].js',
105 | // For any other bundle (typically a server/node) bundle we want a
106 | // determinable output name to allow for easier importing/execution
107 | // of the bundle by our scripts.
108 | '[name].js',
109 | ),
110 | // The name format for any additional chunks produced for the bundle.
111 | chunkFilename: '[name]-[chunkhash].js',
112 | // When targetting node we will output our bundle as a commonjs2 module.
113 | libraryTarget: ifNode('commonjs2', 'var'),
114 | // This is the web path under which our webpack bundled client should
115 | // be considered as being served from.
116 | publicPath: ifDev(
117 | // As we run a seperate development server for our client and server
118 | // bundles we need to use an absolute http path for the public path.
119 | `http://${config('host')}:${config('clientDevServerPort')}${config(
120 | 'bundles.client.webPath',
121 | )}`,
122 | // Otherwise we expect our bundled client to be served from this path.
123 | bundleConfig.webPath,
124 | ),
125 | },
126 |
127 | target: isClient
128 | ? // Only our client bundle will target the web as a runtime.
129 | 'web'
130 | : // Any other bundle must be targetting node as a runtime.
131 | 'node',
132 |
133 | // Ensure that webpack polyfills the following node features for use
134 | // within any bundles that are targetting node as a runtime. This will be
135 | // ignored otherwise.
136 | node: {
137 | __dirname: true,
138 | __filename: true,
139 | },
140 |
141 | // Source map settings.
142 | devtool: ifElse(
143 | // Include source maps for ANY node bundle so that we can support
144 | // nice stack traces for errors (the source maps get consumed by
145 | // the `node-source-map-support` module to allow for this).
146 | isNode ||
147 | // Always include source maps for any development build.
148 | isDev ||
149 | // Allow for the following flag to force source maps even for production
150 | // builds.
151 | config('includeSourceMapsForOptimisedClientBundle'),
152 | )(
153 | // Produces an external source map (lives next to bundle output files).
154 | 'source-map',
155 | // Produces no source map.
156 | 'hidden-source-map',
157 | ),
158 |
159 | // Performance budget feature.
160 | // This enables checking of the output bundle size, which will result in
161 | // warnings/errors if the bundle sizes are too large.
162 | // We only want this enabled for our production client. Please
163 | // see the webpack docs on how you can configure this to your own needs:
164 | // https://webpack.js.org/configuration/performance/
165 | performance: ifProdClient(
166 | // Enable webpack's performance hints for production client builds.
167 | { hints: 'warning' },
168 | // Else we have to set a value of "false" if we don't want the feature.
169 | false,
170 | ),
171 |
172 | resolve: {
173 | // These extensions are tried when resolving a file.
174 | extensions: config('bundleSrcTypes').map(ext => `.${ext}`),
175 |
176 | // This is required for the modernizr-loader
177 | // @see https://github.com/peerigon/modernizr-loader
178 | alias: {
179 | modernizr$: path.resolve(appRootDir.get(), './.modernizrrc'),
180 | },
181 | },
182 |
183 | // We don't want our node_modules to be bundled with any bundle that is
184 | // targetting the node environment, prefering them to be resolved via
185 | // native node module system. Therefore we use the `webpack-node-externals`
186 | // library to help us generate an externals configuration that will
187 | // ignore all the node_modules.
188 | externals: removeNil([
189 | ifNode(() =>
190 | nodeExternals(
191 | // Some of our node_modules may contain files that depend on our
192 | // webpack loaders, e.g. CSS or SASS.
193 | // For these cases please make sure that the file extensions are
194 | // registered within the following configuration setting.
195 | {
196 | whitelist: removeNil([
197 | // We always want the source-map-support included in
198 | // our node target bundles.
199 | 'source-map-support/register',
200 | ])
201 | // And any items that have been whitelisted in the config need
202 | // to be included in the bundling process too.
203 | .concat(config('nodeExternalsFileTypeWhitelist') || []),
204 | },
205 | ),
206 | ),
207 | ]),
208 |
209 | plugins: removeNil([
210 | // This grants us source map support, which combined with our webpack
211 | // source maps will give us nice stack traces for our node executed
212 | // bundles.
213 | // We use the BannerPlugin to make sure all of our chunks will get the
214 | // source maps support installed.
215 | ifNode(
216 | () =>
217 | new webpack.BannerPlugin({
218 | banner: 'require("source-map-support").install();',
219 | raw: true,
220 | entryOnly: false,
221 | }),
222 | ),
223 |
224 | // Implement webpack 3 scope hoisting that will remove function wrappers
225 | // around your modules you may see some small size improvements. However,
226 | // the significant improvement will be how fast the JavaScript loads in the browser.
227 | ifProdClient(new webpack.optimize.ModuleConcatenationPlugin()),
228 |
229 | // We use this so that our generated [chunkhash]'s are only different if
230 | // the content for our respective chunks have changed. This optimises
231 | // our long term browser caching strategy for our client bundle, avoiding
232 | // cases where browsers end up having to download all the client chunks
233 | // even though 1 or 2 may have only changed.
234 | ifClient(() => new WebpackMd5Hash()),
235 |
236 | // These are process.env flags that you can use in your code in order to
237 | // have advanced control over what is included/excluded in your bundles.
238 | // For example you may only want certain parts of your code to be
239 | // included/ran under certain conditions.
240 | //
241 | // Any process.env.X values that are matched will be code substituted for
242 | // the associated values below.
243 | //
244 | // For example you may have the following in your code:
245 | // if (process.env.BUILD_FLAG_IS_CLIENT === 'true') {
246 | // console.log('Foo');
247 | // }
248 | //
249 | // If the BUILD_FLAG_IS_CLIENT was assigned a value of `false` the above
250 | // code would be converted to the following by the webpack bundling
251 | // process:
252 | // if ('false' === 'true') {
253 | // console.log('Foo');
254 | // }
255 | //
256 | // When your bundle is built using the UglifyJsPlugin unreachable code
257 | // blocks like in the example above will be removed from the bundle
258 | // final output. This is helpful for extreme cases where you want to
259 | // ensure that code is only included/executed on specific targets, or for
260 | // doing debugging.
261 | //
262 | // NOTE: We are stringifying the values to keep them in line with the
263 | // expected type of a typical process.env member (i.e. string).
264 | // @see https://github.com/ctrlplusb/react-universally/issues/395
265 | new webpack.EnvironmentPlugin({
266 | // It is really important to use NODE_ENV=production in order to use
267 | // optimised versions of some node_modules, such as React.
268 | NODE_ENV: isProd ? 'production' : 'development',
269 | // Is this the "client" bundle?
270 | BUILD_FLAG_IS_CLIENT: JSON.stringify(isClient),
271 | // Is this the "server" bundle?
272 | BUILD_FLAG_IS_SERVER: JSON.stringify(isServer),
273 | // Is this a node bundle?
274 | BUILD_FLAG_IS_NODE: JSON.stringify(isNode),
275 | // Is this a development build?
276 | BUILD_FLAG_IS_DEV: JSON.stringify(isDev),
277 | }),
278 |
279 | // Generates a JSON file containing a map of all the output files for
280 | // our webpack bundle. A necessisty for our server rendering process
281 | // as we need to interogate these files in order to know what JS/CSS
282 | // we need to inject into our HTML. We only need to know the assets for
283 | // our client bundle.
284 | ifClient(
285 | () =>
286 | new AssetsPlugin({
287 | filename: config('bundleAssetsFileName'),
288 | path: path.resolve(appRootDir.get(), bundleConfig.outputPath),
289 | }),
290 | ),
291 |
292 | // We don't want webpack errors to occur during development as it will
293 | // kill our dev servers.
294 | ifDev(() => new webpack.NoEmitOnErrorsPlugin()),
295 |
296 | // We need this plugin to enable hot reloading of our client.
297 | ifDevClient(() => new webpack.HotModuleReplacementPlugin()),
298 |
299 | // For our production client we need to make sure we pass the required
300 | // configuration to ensure that the output is minimized/optimized.
301 | ifProdClient(
302 | () =>
303 | new webpack.LoaderOptionsPlugin({
304 | minimize: true,
305 | }),
306 | ),
307 |
308 | // For our production client we need to make sure we pass the required
309 | // configuration to ensure that the output is minimized/optimized.
310 | ifProdClient(
311 | () =>
312 | new webpack.optimize.UglifyJsPlugin({
313 | sourceMap: config('includeSourceMapsForOptimisedClientBundle'),
314 | compress: {
315 | screw_ie8: true,
316 | warnings: false,
317 | },
318 | mangle: {
319 | screw_ie8: true,
320 | },
321 | output: {
322 | comments: false,
323 | screw_ie8: true,
324 | },
325 | }),
326 | ),
327 |
328 | // For the production build of the client we need to extract the CSS into
329 | // CSS files.
330 | ifProdClient(
331 | () =>
332 | new ExtractTextPlugin({
333 | filename: '[name]-[contenthash].css',
334 | allChunks: true,
335 | }),
336 | ),
337 |
338 | // -----------------------------------------------------------------------
339 | // START: HAPPY PACK PLUGINS
340 | //
341 | // @see https://github.com/amireh/happypack/
342 | //
343 | // HappyPack allows us to use threads to execute our loaders. This means
344 | // that we can get parallel execution of our loaders, significantly
345 | // improving build and recompile times.
346 | //
347 | // This may not be an issue for you whilst your project is small, but
348 | // the compile times can be signficant when the project scales. A lengthy
349 | // compile time can significantly impare your development experience.
350 | // Therefore we employ HappyPack to do threaded execution of our
351 | // "heavy-weight" loaders.
352 |
353 | // HappyPack 'javascript' instance.
354 | happyPackPlugin({
355 | name: 'happypack-javascript',
356 | // We will use babel to do all our JS processing.
357 | loaders: [
358 | {
359 | path: 'babel-loader',
360 | // We will create a babel config and pass it through the plugin
361 | // defined in the project configuration, allowing additional
362 | // items to be added.
363 | query: config('plugins.babelConfig')(
364 | // Our "standard" babel config.
365 | {
366 | // We need to ensure that we do this otherwise the babelrc will
367 | // get interpretted and for the current configuration this will mean
368 | // that it will kill our webpack treeshaking feature as the modules
369 | // transpilation has not been disabled within in.
370 | babelrc: false,
371 |
372 | presets: [
373 | // JSX
374 | 'react',
375 | // Stage 3 javascript syntax.
376 | // "Candidate: complete spec and initial browser implementations."
377 | // Add anything lower than stage 3 at your own risk. :)
378 | 'stage-3',
379 | // For our client bundles we transpile all the latest ratified
380 | // ES201X code into ES5, safe for browsers. We exclude module
381 | // transilation as webpack takes care of this for us, doing
382 | // tree shaking in the process.
383 | ifClient(['env', { es2015: { modules: false } }]),
384 | // For a node bundle we use the specific target against
385 | // babel-preset-env so that only the unsupported features of
386 | // our target node version gets transpiled.
387 | ifNode(['env', { targets: { node: true } }]),
388 | ].filter(x => x != null),
389 |
390 | plugins: [
391 | // Required to support react hot loader.
392 | ifDevClient('react-hot-loader/babel'),
393 | // This decorates our components with __self prop to JSX elements,
394 | // which React will use to generate some runtime warnings.
395 | ifDev('transform-react-jsx-self'),
396 | // Adding this will give us the path to our components in the
397 | // react dev tools.
398 | ifDev('transform-react-jsx-source'),
399 | // Replaces the React.createElement function with one that is
400 | // more optimized for production.
401 | // NOTE: Symbol needs to be polyfilled. Ensure this feature
402 | // is enabled in the polyfill.io configuration.
403 | ifProd('transform-react-inline-elements'),
404 | // Hoists element creation to the top level for subtrees that
405 | // are fully static, which reduces call to React.createElement
406 | // and the resulting allocations. More importantly, it tells
407 | // React that the subtree hasn’t changed so React can completely
408 | // skip it when reconciling.
409 | ifProd('transform-react-constant-elements'),
410 | ].filter(x => x != null),
411 | },
412 | buildOptions,
413 | ),
414 | },
415 | ],
416 | }),
417 |
418 | // HappyPack 'css' instance for development client.
419 | ifDevClient(() =>
420 | happyPackPlugin({
421 | name: 'happypack-devclient-css',
422 | loaders: [
423 | 'style-loader',
424 | {
425 | path: 'css-loader',
426 | // Include sourcemaps for dev experience++.
427 | query: { sourceMap: true },
428 | },
429 | ],
430 | }),
431 | ),
432 |
433 | // END: HAPPY PACK PLUGINS
434 | // -----------------------------------------------------------------------
435 | ]),
436 | module: {
437 | // Use strict export presence so that a missing export becomes a compile error.
438 | strictExportPresence: true,
439 | rules: [
440 | {
441 | // "oneOf" will traverse all imports with following loaders until one will
442 | // match the requirements. When no loader matches it will fallback to the
443 | // "file" loader at the end of the loader list.
444 | oneOf: removeNil([
445 | // JAVASCRIPT
446 | {
447 | test: /\.jsx?$/,
448 | // We will defer all our js processing to the happypack plugin
449 | // named "happypack-javascript".
450 | // See the respective plugin within the plugins section for full
451 | // details on what loader is being implemented.
452 | loader: 'happypack/loader?id=happypack-javascript',
453 | include: removeNil([
454 | ...bundleConfig.srcPaths.map(srcPath => path.resolve(appRootDir.get(), srcPath)),
455 | ifProdClient(path.resolve(appRootDir.get(), 'src/html')),
456 | ]),
457 | },
458 |
459 | // CSS
460 | // This is bound to our server/client bundles as we only expect to be
461 | // serving the client bundle as a Single Page Application through the
462 | // server.
463 | ifElse(isClient || isServer)(
464 | mergeDeep(
465 | {
466 | test: /\.css$/,
467 | },
468 | // For development clients we will defer all our css processing to the
469 | // happypack plugin named "happypack-devclient-css".
470 | // See the respective plugin within the plugins section for full
471 | // details on what loader is being implemented.
472 | ifDevClient({
473 | loaders: ['happypack/loader?id=happypack-devclient-css'],
474 | }),
475 | // For a production client build we use the ExtractTextPlugin which
476 | // will extract our CSS into CSS files. We don't use happypack here
477 | // as there are some edge cases where it fails when used within
478 | // an ExtractTextPlugin instance.
479 | // Note: The ExtractTextPlugin needs to be registered within the
480 | // plugins section too.
481 | ifProdClient(() => ({
482 | loader: ExtractTextPlugin.extract({
483 | fallback: 'style-loader',
484 | use: ['css-loader'],
485 | }),
486 | })),
487 | // When targetting the server we use the "/locals" version of the
488 | // css loader, as we don't need any css files for the server.
489 | ifNode({
490 | loaders: ['css-loader/locals'],
491 | }),
492 | ),
493 | ),
494 |
495 | // MODERNIZR
496 | // This allows you to do feature detection.
497 | // @see https://modernizr.com/docs
498 | // @see https://github.com/peerigon/modernizr-loader
499 | ifClient({
500 | test: /\.modernizrrc.js$/,
501 | loader: 'modernizr-loader',
502 | }),
503 | ifClient({
504 | test: /\.modernizrrc(\.json)?$/,
505 | loader: 'modernizr-loader!json-loader',
506 | }),
507 |
508 | // ASSETS (Images/Fonts/etc)
509 | // This is bound to our server/client bundles as we only expect to be
510 | // serving the client bundle as a Single Page Application through the
511 | // server.
512 | ifElse(isClient || isServer)(() => ({
513 | loader: 'file-loader',
514 | exclude: [/\.js$/, /\.html$/, /\.json$/],
515 | query: {
516 | // What is the web path that the client bundle will be served from?
517 | // The same value has to be used for both the client and the
518 | // server bundles in order to ensure that SSR paths match the
519 | // paths used on the client.
520 | publicPath: isDev
521 | ? // When running in dev mode the client bundle runs on a
522 | // seperate port so we need to put an absolute path here.
523 | `http://${config('host')}:${config('clientDevServerPort')}${config(
524 | 'bundles.client.webPath',
525 | )}`
526 | : // Otherwise we just use the configured web path for the client.
527 | config('bundles.client.webPath'),
528 | // We only emit files when building a web bundle, for the server
529 | // bundle we only care about the file loader being able to create
530 | // the correct asset URLs.
531 | emitFile: isClient,
532 | },
533 | })),
534 |
535 | // Do not add any loader after file loader (fallback loader)
536 | // Make sure to add the new loader(s) before the "file" loader.
537 | ]),
538 | },
539 | ],
540 | },
541 | };
542 |
543 | if (isProd && isClient) {
544 | webpackConfig = withServiceWorker(webpackConfig, bundleConfig);
545 | }
546 |
547 | // Apply the configuration middleware.
548 | return config('plugins.webpackConfig')(webpackConfig, buildOptions);
549 | }
550 |
--------------------------------------------------------------------------------
/internal/webpack/withServiceWorker/index.js:
--------------------------------------------------------------------------------
1 | import { sync as globSync } from 'glob';
2 | import appRootDir from 'app-root-dir';
3 | import path from 'path';
4 | import HtmlWebpackPlugin from 'html-webpack-plugin';
5 | import OfflinePlugin from 'offline-plugin';
6 |
7 | import config from '../../../config';
8 |
9 | import ClientConfig from '../../../config/components/ClientConfig';
10 |
11 | export default function withServiceWorker(webpackConfig, bundleConfig) {
12 | if (!config('serviceWorker.enabled')) {
13 | return webpackConfig;
14 | }
15 |
16 | // Offline Page generation.
17 | //
18 | // We use the HtmlWebpackPlugin to produce an "offline" html page that
19 | // can be used by our service worker (see the OfflinePlugin below) in
20 | // order support offline rendering of our application.
21 | // We will only create the service worker required page if enabled in
22 | // config and if we are building the production version of client.
23 | webpackConfig.plugins.push(
24 | new HtmlWebpackPlugin({
25 | filename: config('serviceWorker.offlinePageFileName'),
26 | template: `babel-loader!${path.resolve(__dirname, './offlinePageTemplate.js')}`,
27 | production: true,
28 | minify: {
29 | removeComments: true,
30 | collapseWhitespace: true,
31 | removeRedundantAttributes: true,
32 | useShortDoctype: true,
33 | removeNilAttributes: true,
34 | removeStyleLinkTypeAttributes: true,
35 | keepClosingSlash: true,
36 | minifyJS: true,
37 | minifyCSS: true,
38 | minifyURLs: true,
39 | },
40 | inject: true,
41 | // We pass our config and client config script compoent as it will
42 | // be needed by the offline template.
43 | custom: {
44 | config,
45 | ClientConfig,
46 | },
47 | }),
48 | );
49 |
50 | // We use the offline-plugin to generate the service worker. It also
51 | // provides a runtime installation script which gets executed within
52 | // the client.
53 | // @see https://github.com/NekR/offline-plugin
54 | //
55 | // This plugin generates a service worker script which as configured below
56 | // will precache all our generated client bundle assets as well as our
57 | // static "public" folder assets.
58 | //
59 | // It has also been configured to make use of a HtmlWebpackPlugin
60 | // generated "offline" page so that users can still used the application
61 | // offline.
62 | //
63 | // Any time our static files or generated bundle files change the user's
64 | // cache will be updated.
65 | //
66 | // We will only include the service worker if enabled in config.
67 | webpackConfig.plugins.push(
68 | new OfflinePlugin({
69 | // Setting this value lets the plugin know where our generated client
70 | // assets will be served from.
71 | // e.g. /client/
72 | publicPath: bundleConfig.webPath,
73 | // When using the publicPath we need to disable the "relativePaths"
74 | // feature of this plugin.
75 | relativePaths: false,
76 | // Our offline support will be done via a service worker.
77 | // Read more on them here:
78 | // http://bit.ly/2f8q7Td
79 | ServiceWorker: {
80 | // The name of the service worker script that will get generated.
81 | output: config('serviceWorker.fileName'),
82 | // Enable events so that we can register updates.
83 | events: true,
84 | // By default the service worker will be ouput and served from the
85 | // publicPath setting above in the root config of the OfflinePlugin.
86 | // This means that it would be served from /client/sw.js
87 | // We do not want this! Service workers have to be served from the
88 | // root of our application in order for them to work correctly.
89 | // Therefore we override the publicPath here. The sw.js will still
90 | // live in at the /build/client/sw.js output location therefore in
91 | // our server configuration we need to make sure that any requests
92 | // to /sw.js will serve the /build/client/sw.js file.
93 | publicPath: `/${config('serviceWorker.fileName')}`,
94 | // When the user is offline then this html page will be used at
95 | // the base that loads all our cached client scripts. This page
96 | // is generated by the HtmlWebpackPlugin above, which takes care
97 | // of injecting all of our client scripts into the body.
98 | // Please see the HtmlWebpackPlugin configuration above for more
99 | // information on this page.
100 | navigateFallbackURL: `${bundleConfig.webPath}${config('serviceWorker.offlinePageFileName')}`,
101 | },
102 | // According to the Mozilla docs, AppCache is considered deprecated.
103 | // @see https://mzl.la/1pOZ5wF
104 | // It does however have much wider support compared to the newer
105 | // Service Worker specification, so you could consider enabling it
106 | // if you needed.
107 | AppCache: false,
108 | // Which external files should be included with the service worker?
109 | // Add the polyfill io script as an external if it is enabled.
110 | externals: (config('polyfillIO.enabled')
111 | ? [`${config('polyfillIO.url')}?features=${config('polyfillIO.features').join(',')}`]
112 | : [])
113 | // Add any included public folder assets.
114 | .concat(
115 | config('serviceWorker.includePublicAssets').reduce((acc, cur) => {
116 | const publicAssetPathGlob = path.resolve(
117 | appRootDir.get(),
118 | config('publicAssetsPath'),
119 | cur,
120 | );
121 | const publicFileWebPaths = acc.concat(
122 | // First get all the matching public folder files.
123 | globSync(publicAssetPathGlob, { nodir: true })
124 | // Then map them to relative paths against the public folder.
125 | // We need to do this as we need the "web" paths for each one.
126 | .map(publicFile =>
127 | path.relative(
128 | path.resolve(appRootDir.get(), config('publicAssetsPath')),
129 | publicFile,
130 | ),
131 | )
132 | // Add the leading "/" indicating the file is being hosted
133 | // off the root of the application.
134 | .map(relativePath => `/${relativePath}`),
135 | );
136 | return publicFileWebPaths;
137 | }, []),
138 | ),
139 | }),
140 | );
141 |
142 | return webpackConfig;
143 | }
144 |
--------------------------------------------------------------------------------
/internal/webpack/withServiceWorker/offlinePageTemplate.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This is used by the HtmlWebpackPlugin to generate an html page that we will
3 | * use as a fallback for our service worker when the user is offline. It will
4 | * embed all the required asset paths needed to bootstrap the application
5 | * in an offline session.
6 | */
7 |
8 | import React from 'react';
9 | import { renderToStaticMarkup } from 'react-dom/server';
10 |
11 | import HTML from '../../../shared/components/HTML';
12 |
13 | module.exports = function generate(context) {
14 | // const config = context.htmlWebpackPlugin.options.custom.config;
15 | const ClientConfig = context.htmlWebpackPlugin.options.custom.ClientConfig;
16 | const html = renderToStaticMarkup(
17 | } />,
18 | );
19 | return `${html}`;
20 | };
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-universally",
3 | "version": "13.0.0",
4 | "description": "A starter kit for universal react applications.",
5 | "main": "build/server/index.js",
6 | "engines": {
7 | "node": ">=6"
8 | },
9 | "scripts": {
10 | "analyze:client": "babel-node internal/scripts/analyze --client",
11 | "analyze:server": "babel-node internal/scripts/analyze --server",
12 | "build": "babel-node internal/scripts/build --optimize",
13 | "build:dev": "babel-node internal/scripts/build",
14 | "clean": "cross-env babel-node internal/scripts/clean",
15 | "deploy": "babel-node internal/scripts/deploy",
16 | "develop": "cross-env DEPLOYMENT=development babel-node internal/development",
17 | "lint": "eslint client server shared config internal",
18 | "precommit": "lint-staged",
19 | "preinstall": "node internal/scripts/preinstall",
20 | "prepush": "jest",
21 | "start": "cross-env NODE_ENV=production node build/server",
22 | "test": "jest",
23 | "test:coverage": "jest --coverage"
24 | },
25 | "lint-staged": {
26 | "*.js": [
27 | "prettier-eslint --write",
28 | "git add"
29 | ]
30 | },
31 | "jest": {
32 | "collectCoverageFrom": [
33 | "shared/**/*.{js,jsx}"
34 | ],
35 | "snapshotSerializers": [
36 | "/node_modules/enzyme-to-json/serializer"
37 | ],
38 | "testPathIgnorePatterns": [
39 | "/(build|internal|node_modules|flow-typed|public)/"
40 | ]
41 | },
42 | "repository": {
43 | "type": "git",
44 | "url": "git+https://github.com/ctrlplusb/react-universally.git"
45 | },
46 | "keywords": [
47 | "react",
48 | "boilerplate",
49 | "starter kit",
50 | "universal",
51 | "javascript",
52 | "express",
53 | "webpack"
54 | ],
55 | "license": "MIT",
56 | "bugs": {
57 | "url": "https://github.com/ctrlplusb/react-universally/issues"
58 | },
59 | "homepage": "https://github.com/ctrlplusb/react-universally#readme",
60 | "dependencies": {
61 | "app-root-dir": "1.0.2",
62 | "colors": "1.1.2",
63 | "compression": "1.7.0",
64 | "cross-env": "5.0.5",
65 | "dotenv": "4.0.0",
66 | "express": "4.15.4",
67 | "helmet": "3.8.1",
68 | "hpp": "0.2.2",
69 | "modernizr": "3.5.0",
70 | "normalize.css": "7.0.0",
71 | "offline-plugin": "4.8.3",
72 | "pretty-error": "2.1.1",
73 | "prop-types": "15.5.10",
74 | "react": "15.6.1",
75 | "react-async-bootstrapper": "1.1.1",
76 | "react-async-component": "1.0.1",
77 | "react-dom": "15.6.1",
78 | "react-helmet": "5.2.0",
79 | "react-router-dom": "4.2.2",
80 | "serialize-javascript": "1.4.0",
81 | "uuid": "3.1.0"
82 | },
83 | "devDependencies": {
84 | "assets-webpack-plugin": "3.5.1",
85 | "babel-cli": "6.26.0",
86 | "babel-core": "6.26.0",
87 | "babel-eslint": "7.2.3",
88 | "babel-jest": "21.0.2",
89 | "babel-loader": "7.1.2",
90 | "babel-plugin-transform-react-constant-elements": "6.23.0",
91 | "babel-plugin-transform-react-inline-elements": "6.22.0",
92 | "babel-plugin-transform-react-jsx-self": "6.22.0",
93 | "babel-plugin-transform-react-jsx-source": "6.22.0",
94 | "babel-polyfill": "6.26.0",
95 | "babel-preset-env": "1.6.0",
96 | "babel-preset-react": "6.24.1",
97 | "babel-preset-stage-3": "6.24.1",
98 | "babel-template": "6.26.0",
99 | "chokidar": "1.7.0",
100 | "css-loader": "0.28.7",
101 | "enzyme": "2.9.1",
102 | "enzyme-to-json": "2.0.0",
103 | "eslint": "4.7.2",
104 | "eslint-config-airbnb": "15.1.0",
105 | "eslint-plugin-import": "2.7.0",
106 | "eslint-plugin-jsx-a11y": "5.1.1",
107 | "eslint-plugin-react": "7.3.0",
108 | "extract-text-webpack-plugin": "3.0.0",
109 | "file-loader": "0.11.2",
110 | "glob": "7.1.2",
111 | "happypack": "3.0.3",
112 | "html-webpack-plugin": "2.30.1",
113 | "husky": "0.14.3",
114 | "jest": "21.1.0",
115 | "lint-staged": "4.2.2",
116 | "md5": "2.2.1",
117 | "modernizr-loader": "1.0.1",
118 | "node-notifier": "5.1.2",
119 | "prettier": "1.7.0",
120 | "prettier-eslint": "8.1.1",
121 | "prettier-eslint-cli": "4.3.2",
122 | "react-addons-test-utils": "15.6.0",
123 | "react-hot-loader": "3.0.0-beta.6",
124 | "react-test-renderer": "15.6.1",
125 | "regenerator-runtime": "0.11.0",
126 | "rimraf": "2.6.2",
127 | "semver": "5.4.1",
128 | "source-map-support": "0.4.18",
129 | "style-loader": "0.18.2",
130 | "webpack": "3.6.0",
131 | "webpack-bundle-analyzer": "2.9.0",
132 | "webpack-dev-middleware": "1.12.0",
133 | "webpack-hot-middleware": "2.19.1",
134 | "webpack-md5-hash": "0.0.5",
135 | "webpack-node-externals": "1.6.0"
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/public/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #2b2b2b
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ctrlplusb/react-universally/83d533a9c780716d18f034f7fb52dbd3a1c4051b/public/favicon.ico
--------------------------------------------------------------------------------
/public/favicons/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ctrlplusb/react-universally/83d533a9c780716d18f034f7fb52dbd3a1c4051b/public/favicons/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/favicons/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ctrlplusb/react-universally/83d533a9c780716d18f034f7fb52dbd3a1c4051b/public/favicons/android-chrome-512x512.png
--------------------------------------------------------------------------------
/public/favicons/apple-touch-icon-114x114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ctrlplusb/react-universally/83d533a9c780716d18f034f7fb52dbd3a1c4051b/public/favicons/apple-touch-icon-114x114.png
--------------------------------------------------------------------------------
/public/favicons/apple-touch-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ctrlplusb/react-universally/83d533a9c780716d18f034f7fb52dbd3a1c4051b/public/favicons/apple-touch-icon-120x120.png
--------------------------------------------------------------------------------
/public/favicons/apple-touch-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ctrlplusb/react-universally/83d533a9c780716d18f034f7fb52dbd3a1c4051b/public/favicons/apple-touch-icon-144x144.png
--------------------------------------------------------------------------------
/public/favicons/apple-touch-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ctrlplusb/react-universally/83d533a9c780716d18f034f7fb52dbd3a1c4051b/public/favicons/apple-touch-icon-152x152.png
--------------------------------------------------------------------------------
/public/favicons/apple-touch-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ctrlplusb/react-universally/83d533a9c780716d18f034f7fb52dbd3a1c4051b/public/favicons/apple-touch-icon-180x180.png
--------------------------------------------------------------------------------
/public/favicons/apple-touch-icon-57x57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ctrlplusb/react-universally/83d533a9c780716d18f034f7fb52dbd3a1c4051b/public/favicons/apple-touch-icon-57x57.png
--------------------------------------------------------------------------------
/public/favicons/apple-touch-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ctrlplusb/react-universally/83d533a9c780716d18f034f7fb52dbd3a1c4051b/public/favicons/apple-touch-icon-60x60.png
--------------------------------------------------------------------------------
/public/favicons/apple-touch-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ctrlplusb/react-universally/83d533a9c780716d18f034f7fb52dbd3a1c4051b/public/favicons/apple-touch-icon-72x72.png
--------------------------------------------------------------------------------
/public/favicons/apple-touch-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ctrlplusb/react-universally/83d533a9c780716d18f034f7fb52dbd3a1c4051b/public/favicons/apple-touch-icon-76x76.png
--------------------------------------------------------------------------------
/public/favicons/favicon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ctrlplusb/react-universally/83d533a9c780716d18f034f7fb52dbd3a1c4051b/public/favicons/favicon-128.png
--------------------------------------------------------------------------------
/public/favicons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ctrlplusb/react-universally/83d533a9c780716d18f034f7fb52dbd3a1c4051b/public/favicons/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicons/favicon-196x196.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ctrlplusb/react-universally/83d533a9c780716d18f034f7fb52dbd3a1c4051b/public/favicons/favicon-196x196.png
--------------------------------------------------------------------------------
/public/favicons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ctrlplusb/react-universally/83d533a9c780716d18f034f7fb52dbd3a1c4051b/public/favicons/favicon-32x32.png
--------------------------------------------------------------------------------
/public/favicons/favicon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ctrlplusb/react-universally/83d533a9c780716d18f034f7fb52dbd3a1c4051b/public/favicons/favicon-96x96.png
--------------------------------------------------------------------------------
/public/favicons/mstile-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ctrlplusb/react-universally/83d533a9c780716d18f034f7fb52dbd3a1c4051b/public/favicons/mstile-144x144.png
--------------------------------------------------------------------------------
/public/favicons/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ctrlplusb/react-universally/83d533a9c780716d18f034f7fb52dbd3a1c4051b/public/favicons/mstile-150x150.png
--------------------------------------------------------------------------------
/public/favicons/mstile-310x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ctrlplusb/react-universally/83d533a9c780716d18f034f7fb52dbd3a1c4051b/public/favicons/mstile-310x150.png
--------------------------------------------------------------------------------
/public/favicons/mstile-310x310.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ctrlplusb/react-universally/83d533a9c780716d18f034f7fb52dbd3a1c4051b/public/favicons/mstile-310x310.png
--------------------------------------------------------------------------------
/public/favicons/mstile-70x70.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ctrlplusb/react-universally/83d533a9c780716d18f034f7fb52dbd3a1c4051b/public/favicons/mstile-70x70.png
--------------------------------------------------------------------------------
/public/favicons/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
113 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "React Universally",
3 | "short_name": "Rct-Unvrslly",
4 | "description": "A starter kit giving you the minimum requirements for a production ready universal react application.",
5 | "lang": "en",
6 | "start_url": "./",
7 | "icons": [
8 | {
9 | "src": "\/android-chrome-512x512.png",
10 | "sizes": "512x512",
11 | "type": "image\/png"
12 | },
13 | {
14 | "src": "\/android-chrome-192x192.png",
15 | "sizes": "192x192",
16 | "type": "image\/png"
17 | },
18 | {
19 | "src": "\/apple-touch-icon.png",
20 | "sizes": "180x180",
21 | "type": "image\/png"
22 | },
23 | {
24 | "src": "\/favicon-32x32.png",
25 | "sizes": "32x32",
26 | "type": "image\/png"
27 | },
28 | {
29 | "src": "\/favicon-16x16.png",
30 | "sizes": "16x16",
31 | "type": "image\/png"
32 | }
33 | ],
34 | "theme_color": "#2b2b2b",
35 | "background_color": "#ffffff",
36 | "display": "standalone"
37 | }
38 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Allow: /
3 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | import express from 'express';
4 | import compression from 'compression';
5 | import { resolve as pathResolve } from 'path';
6 | import appRootDir from 'app-root-dir';
7 | import reactApplication from './middleware/reactApplication';
8 | import security from './middleware/security';
9 | import clientBundle from './middleware/clientBundle';
10 | import serviceWorker from './middleware/serviceWorker';
11 | import offlinePage from './middleware/offlinePage';
12 | import errorHandlers from './middleware/errorHandlers';
13 | import config from '../config';
14 | import { log } from '../internal/utils';
15 |
16 | // Create our express based server.
17 | const app = express();
18 |
19 | // Don't expose any software information to potential hackers.
20 | app.disable('x-powered-by');
21 |
22 | // Security middlewares.
23 | app.use(...security);
24 |
25 | // Gzip compress the responses.
26 | app.use(compression());
27 |
28 | // Register our service worker generated by our webpack config.
29 | // We do not want the service worker registered for development builds, and
30 | // additionally only want it registered if the config allows.
31 | if (process.env.BUILD_FLAG_IS_DEV === 'false' && config('serviceWorker.enabled')) {
32 | app.get(`/${config('serviceWorker.fileName')}`, serviceWorker);
33 | app.get(
34 | `${config('bundles.client.webPath')}${config('serviceWorker.offlinePageFileName')}`,
35 | offlinePage,
36 | );
37 | }
38 |
39 | // Configure serving of our client bundle.
40 | app.use(config('bundles.client.webPath'), clientBundle);
41 |
42 | // Configure static serving of our "public" root http path static files.
43 | // Note: these will be served off the root (i.e. '/') of our application.
44 | app.use(express.static(pathResolve(appRootDir.get(), config('publicAssetsPath'))));
45 |
46 | // The React application middleware.
47 | app.get('*', (request, response) => {
48 | log({
49 | title: 'Request',
50 | level: 'special',
51 | message: `Received for "${request.url}"`,
52 | });
53 |
54 | return reactApplication(request, response);
55 | });
56 |
57 | // Error Handler middlewares.
58 | app.use(...errorHandlers);
59 |
60 | // Create an http listener for our express app.
61 | const listener = app.listen(config('port'), () =>
62 | log({
63 | title: 'server',
64 | level: 'special',
65 | message: `✓
66 |
67 | ${config('welcomeMessage')}
68 |
69 | ${config('htmlPage.defaultTitle')} is ready!
70 |
71 | with
72 |
73 | Service Workers: ${config('serviceWorker.enabled')}
74 | Polyfills: ${config('polyfillIO.enabled')} (${config('polyfillIO.features').join(', ')})
75 |
76 | Server is now listening on Port ${config('port')}
77 | You can access it in the browser at http://${config('host')}:${config('port')}
78 | Press Ctrl-C to stop.
79 |
80 |
81 |
82 | `,
83 | }),
84 | );
85 |
86 | // We export the listener as it will be handy for our development hot reloader,
87 | // or for exposing a general extension layer for application customisations.
88 | export default listener;
89 |
--------------------------------------------------------------------------------
/server/middleware/clientBundle.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import { resolve as pathResolve } from 'path';
3 | import appRootDir from 'app-root-dir';
4 | import config from '../../config';
5 |
6 | /**
7 | * Middleware to server our client bundle.
8 | */
9 | export default express.static(pathResolve(appRootDir.get(), config('bundles.client.outputPath')), {
10 | maxAge: config('browserCacheMaxAge'),
11 | });
12 |
--------------------------------------------------------------------------------
/server/middleware/errorHandlers.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | /* eslint-disable no-unused-vars */
3 |
4 | const prettyError = require('pretty-error').start();
5 |
6 | // Configure prettyError to simplify the stack trace:
7 |
8 | // skip events.js and http.js and similar core node files
9 | prettyError.skipNodeFiles();
10 |
11 | // skip all the trace lines about express` core and sub-modules
12 | prettyError.skipPackage('express');
13 |
14 | const errorHandlersMiddleware = [
15 | /**
16 | * 404 errors middleware.
17 | *
18 | * NOTE: the react application middleware hands 404 paths, but it is good to
19 | * have this backup for paths not handled by the react middleware. For
20 | * example you may bind a /api path to express.
21 | */
22 | function notFoundMiddlware(req, res, next) {
23 | res.status(404).send('Sorry, that resource was not found.');
24 | },
25 |
26 | /**
27 | * 500 errors middleware.
28 | *
29 | * NOTE: You must provide specify all 4 parameters on this callback function
30 | * even if they aren't used, otherwise it won't be used.
31 | */
32 | function unexpectedErrorMiddleware(err, req, res, next) {
33 | if (err) {
34 | console.log(prettyError.render(err));
35 | }
36 | res.status(500).send('Sorry, an unexpected error occurred.');
37 | },
38 | ];
39 |
40 | export default errorHandlersMiddleware;
41 |
--------------------------------------------------------------------------------
/server/middleware/offlinePage.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 |
3 | import { readFile } from 'fs';
4 | import { resolve as pathResolve } from 'path';
5 | import appRootDir from 'app-root-dir';
6 |
7 | import config from '../../config';
8 |
9 | /**
10 | * Middleware to intercept calls to our offline page to ensure that
11 | * inline scripts get a nonce value attached to them.
12 | */
13 | export default function offlinePageMiddleware(req, res, next) {
14 | // We should have had a nonce provided to us. See the server/index.js for
15 | // more information on what this is.
16 | if (typeof res.locals.nonce !== 'string') {
17 | throw new Error('A "nonce" value has not been attached to the response');
18 | }
19 | const nonce = res.locals.nonce;
20 |
21 | readFile(
22 | // Path to the offline page.
23 | pathResolve(
24 | appRootDir.get(),
25 | config('bundles.client.outputPath'),
26 | config('serviceWorker.offlinePageFileName'),
27 | ),
28 | // Charset for read
29 | 'utf-8',
30 | // Read handler
31 | (err, data) => {
32 | if (err) {
33 | res.status(500).send('Error returning offline page.');
34 | return;
35 | }
36 | // We replace the placeholder with the actual nonce.
37 | const offlinePageWithNonce = data.replace('OFFLINE_PAGE_NONCE_PLACEHOLDER', nonce);
38 | // Send back the page as the response
39 | res.send(offlinePageWithNonce);
40 | },
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/server/middleware/reactApplication/ServerHTML.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This module is responsible for generating the HTML page response for
3 | * the react application middleware.
4 | */
5 |
6 | /* eslint-disable react/no-danger */
7 | /* eslint-disable react/no-array-index-key */
8 |
9 | import React, { Children } from 'react';
10 | import PropTypes from 'prop-types';
11 | import serialize from 'serialize-javascript';
12 |
13 | import config from '../../../config';
14 | import ifElse from '../../../shared/utils/logic/ifElse';
15 | import removeNil from '../../../shared/utils/arrays/removeNil';
16 | import getClientBundleEntryAssets from './getClientBundleEntryAssets';
17 |
18 | import ClientConfig from '../../../config/components/ClientConfig';
19 | import HTML from '../../../shared/components/HTML';
20 |
21 | // PRIVATES
22 |
23 | function KeyedComponent({ children }) {
24 | return Children.only(children);
25 | }
26 |
27 | // Resolve the assets (js/css) for the client bundle's entry chunk.
28 | const clientEntryAssets = getClientBundleEntryAssets();
29 |
30 | function stylesheetTag(stylesheetFilePath) {
31 | return (
32 |
33 | );
34 | }
35 |
36 | function scriptTag(jsFilePath) {
37 | return ;
38 | }
39 |
40 | // COMPONENT
41 |
42 | function ServerHTML(props) {
43 | const { asyncComponentsState, helmet, nonce, reactAppString } = props;
44 |
45 | // Creates an inline script definition that is protected by the nonce.
46 | const inlineScript = body =>
47 | ;
48 |
49 | const headerElements = removeNil([
50 | ...ifElse(helmet)(() => helmet.meta.toComponent(), []),
51 | ...ifElse(helmet)(() => helmet.title.toComponent(), []),
52 | ...ifElse(helmet)(() => helmet.base.toComponent(), []),
53 | ...ifElse(helmet)(() => helmet.link.toComponent(), []),
54 | ifElse(clientEntryAssets && clientEntryAssets.css)(() => stylesheetTag(clientEntryAssets.css)),
55 | ...ifElse(helmet)(() => helmet.style.toComponent(), []),
56 | ]);
57 |
58 | const bodyElements = removeNil([
59 | // Binds the client configuration object to the window object so
60 | // that we can safely expose some configuration values to the
61 | // client bundle that gets executed in the browser.
62 | ,
63 | // Bind our async components state so the client knows which ones
64 | // to initialise so that the checksum matches the server response.
65 | // @see https://github.com/ctrlplusb/react-async-component
66 | ifElse(asyncComponentsState)(() =>
67 | inlineScript(
68 | `window.__ASYNC_COMPONENTS_REHYDRATE_STATE__=${serialize(asyncComponentsState)};`,
69 | ),
70 | ),
71 | // Enable the polyfill io script?
72 | // This can't be configured within a react-helmet component as we
73 | // may need the polyfill's before our client JS gets parsed.
74 | ifElse(config('polyfillIO.enabled'))(() =>
75 | scriptTag(`${config('polyfillIO.url')}?features=${config('polyfillIO.features').join(',')}`),
76 | ),
77 | // When we are in development mode our development server will
78 | // generate a vendor DLL in order to dramatically reduce our
79 | // compilation times. Therefore we need to inject the path to the
80 | // vendor dll bundle below.
81 | ifElse(
82 | process.env.BUILD_FLAG_IS_DEV === 'true' && config('bundles.client.devVendorDLL.enabled'),
83 | )(() =>
84 | scriptTag(
85 | `${config('bundles.client.webPath')}${config(
86 | 'bundles.client.devVendorDLL.name',
87 | )}.js?t=${Date.now()}`,
88 | ),
89 | ),
90 | ifElse(clientEntryAssets && clientEntryAssets.js)(() => scriptTag(clientEntryAssets.js)),
91 | ...ifElse(helmet)(() => helmet.script.toComponent(), []),
92 | ]);
93 |
94 | return (
95 | helmet.htmlAttributes.toComponent(), null)}
97 | headerElements={headerElements.map((x, idx) =>
98 | (
99 | {x}
100 | ),
101 | )}
102 | bodyElements={bodyElements.map((x, idx) =>
103 | (
104 | {x}
105 | ),
106 | )}
107 | appBodyString={reactAppString}
108 | />
109 | );
110 | }
111 |
112 | ServerHTML.propTypes = {
113 | // eslint-disable-next-line react/forbid-prop-types
114 | asyncComponentsState: PropTypes.object,
115 | // eslint-disable-next-line react/forbid-prop-types
116 | helmet: PropTypes.object,
117 | nonce: PropTypes.string,
118 | reactAppString: PropTypes.string,
119 | };
120 |
121 | // EXPORT
122 |
123 | export default ServerHTML;
124 |
--------------------------------------------------------------------------------
/server/middleware/reactApplication/getClientBundleEntryAssets.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file resolves the entry assets available from our client bundle.
3 | */
4 |
5 | import fs from 'fs';
6 | import { resolve as pathResolve } from 'path';
7 | import appRootDir from 'app-root-dir';
8 | import config from '../../../config';
9 |
10 | let resultCache;
11 |
12 | /**
13 | * Retrieves the js/css for the named chunks that belong to our client bundle.
14 | *
15 | * Note: the order of the chunk names is important. The same ordering will be
16 | * used when rendering the scripts.
17 | *
18 | * This is useful to us for a couple of reasons:
19 | * - It allows us to target the assets for a specific chunk, thereby only
20 | * loading the assets we know we will need for a specific request.
21 | * - The assets are hashed, and therefore they can't be "manually" added
22 | * to the render logic. Having this method allows us to easily fetch
23 | * the respective assets simply by using a chunk name. :)
24 | */
25 | export default function getClientBundleEntryAssets() {
26 | // Return the assets json cache if it exists.
27 | // In development mode we always read the assets json file from disk to avoid
28 | // any cases where an older version gets cached.
29 | if (process.env.BUILD_FLAG_IS_DEV === 'false' && resultCache) {
30 | return resultCache;
31 | }
32 |
33 | const assetsFilePath = pathResolve(
34 | appRootDir.get(),
35 | config('bundles.client.outputPath'),
36 | `./${config('bundleAssetsFileName')}`,
37 | );
38 |
39 | if (!fs.existsSync(assetsFilePath)) {
40 | throw new Error(
41 | `We could not find the "${assetsFilePath}" file, which contains a list of the assets of the client bundle. Please ensure that the client bundle has been built.`,
42 | );
43 | }
44 |
45 | const readAssetsJSONFile = () => JSON.parse(fs.readFileSync(assetsFilePath, 'utf8'));
46 | const assetsJSONCache = readAssetsJSONFile();
47 | if (typeof assetsJSONCache.index === 'undefined') {
48 | throw new Error('No asset data found for expected "index" entry chunk of client bundle.');
49 | }
50 | resultCache = assetsJSONCache.index;
51 | return resultCache;
52 | }
53 |
--------------------------------------------------------------------------------
/server/middleware/reactApplication/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Helmet from 'react-helmet';
3 | import { renderToString, renderToStaticMarkup } from 'react-dom/server';
4 | import { StaticRouter } from 'react-router-dom';
5 | import { AsyncComponentProvider, createAsyncContext } from 'react-async-component';
6 | import asyncBootstrapper from 'react-async-bootstrapper';
7 |
8 | import config from '../../../config';
9 |
10 | import ServerHTML from './ServerHTML';
11 | import DemoApp from '../../../shared/components/DemoApp';
12 | import { log } from '../../../internal/utils';
13 |
14 | /**
15 | * React application middleware, supports server side rendering.
16 | */
17 | export default function reactApplicationMiddleware(request, response) {
18 | // Ensure a nonce has been provided to us.
19 | // See the server/middleware/security.js for more info.
20 | if (typeof response.locals.nonce !== 'string') {
21 | throw new Error('A "nonce" value has not been attached to the response');
22 | }
23 | const nonce = response.locals.nonce;
24 |
25 | // It's possible to disable SSR, which can be useful in development mode.
26 | // In this case traditional client side only rendering will occur.
27 | if (config('disableSSR')) {
28 | if (process.env.BUILD_FLAG_IS_DEV === 'true') {
29 | // eslint-disable-next-line no-console
30 | log({
31 | title: 'Server',
32 | level: 'info',
33 | message: `Handling react route without SSR: ${request.url}`,
34 | });
35 | }
36 | // SSR is disabled so we will return an "empty" html page and
37 | // rely on the client to initialize and render the react application.
38 | const html = renderToStaticMarkup();
39 | response.status(200).send(`${html}`);
40 | return;
41 | }
42 |
43 | // Create a context for our AsyncComponentProvider.
44 | const asyncComponentsContext = createAsyncContext();
45 |
46 | // Create a context for , which will allow us to
47 | // query for the results of the render.
48 | const reactRouterContext = {};
49 |
50 | // Declare our React application.
51 | const app = (
52 |
53 |
54 |
55 |
56 |
57 | );
58 |
59 | // Pass our app into the react-async-component helper so that any async
60 | // components are resolved for the render.
61 | asyncBootstrapper(app).then(() => {
62 | const appString = renderToString(app);
63 |
64 | // Generate the html response.
65 | const html = renderToStaticMarkup(
66 | ,
72 | );
73 |
74 | // Check if the router context contains a redirect, if so we need to set
75 | // the specific status and redirect header and end the response.
76 | if (reactRouterContext.url) {
77 | response.status(302).setHeader('Location', reactRouterContext.url);
78 | response.end();
79 | return;
80 | }
81 |
82 | response
83 | .status(
84 | reactRouterContext.missed
85 | ? // If the renderResult contains a "missed" match then we set a 404 code.
86 | // Our App component will handle the rendering of an Error404 view.
87 | 404
88 | : // Otherwise everything is all good and we send a 200 OK status.
89 | 200,
90 | )
91 | .send(`${html}`);
92 | });
93 | }
94 |
--------------------------------------------------------------------------------
/server/middleware/security.js:
--------------------------------------------------------------------------------
1 | import uuid from 'uuid';
2 | import hpp from 'hpp';
3 | import helmet from 'helmet';
4 | import config from '../../config';
5 |
6 | const cspConfig = {
7 | directives: {
8 | childSrc: ["'self'"],
9 | // Note: Setting this to stricter than * breaks the service worker. :(
10 | // I can't figure out how to get around this, so if you know of a safer
11 | // implementation that is kinder to service workers please let me know.
12 | connectSrc: ['*'], // ["'self'", 'ws:'],
13 | defaultSrc: ["'self'"],
14 | imgSrc: [
15 | "'self'",
16 | // If you use Base64 encoded images (i.e. inlined images), then you will
17 | // need the following:
18 | // 'data:',
19 | ],
20 | fontSrc: ["'self'", 'data:'],
21 | objectSrc: ["'self'"],
22 | mediaSrc: ["'self'"],
23 | manifestSrc: ["'self'"],
24 | scriptSrc: [
25 | // Allow scripts hosted from our application.
26 | "'self'",
27 | // Note: We will execution of any inline scripts that have the following
28 | // nonce identifier attached to them.
29 | // This is useful for guarding your application whilst allowing an inline
30 | // script to do data store rehydration (redux/mobx/apollo) for example.
31 | // @see https://helmetjs.github.io/docs/csp/
32 | (req, res) => `'nonce-${res.locals.nonce}'`,
33 | // This is a know workaround for browsers that don't support nonces.
34 | // It will be ignored by browsers that do support nonces as they will
35 | // recognise that we have also provided a nonce configuration and
36 | // use the stricter rule.
37 | "'unsafe-inline'",
38 | ],
39 | styleSrc: [
40 | "'self'",
41 | // Webpack generates JS that loads our CSS, so this is needed:
42 | "'unsafe-inline'",
43 | 'blob:',
44 | ],
45 | },
46 | };
47 |
48 | // Add any additional CSP from the static config.
49 | const cspExtensions = config('cspExtensions');
50 | Object.keys(cspExtensions).forEach((key) => {
51 | if (cspConfig.directives[key]) {
52 | cspConfig.directives[key] = cspConfig.directives[key].concat(cspExtensions[key]);
53 | } else {
54 | cspConfig.directives[key] = cspExtensions[key];
55 | }
56 | });
57 |
58 | if (process.env.BUILD_FLAG_IS_DEV === 'true') {
59 | // When in development mode we need to add our secondary express server that
60 | // is used to host our client bundle to our csp config.
61 | Object.keys(cspConfig.directives).forEach((directive) => {
62 | cspConfig.directives[directive].push(`${config('host')}:${config('clientDevServerPort')}`);
63 | });
64 | }
65 |
66 | // Attach a unique "nonce" to every response. This allows use to declare
67 | // inline scripts as being safe for execution against our content security policy.
68 | // @see https://helmetjs.github.io/docs/csp/
69 | function nonceMiddleware(req, res, next) {
70 | // eslint-disable-next-line no-param-reassign
71 | res.locals.nonce = uuid.v4();
72 | next();
73 | }
74 |
75 | const securityMiddleware = [
76 | nonceMiddleware,
77 |
78 | // Prevent HTTP Parameter pollution.
79 | // @see http://bit.ly/2f8q7Td
80 | hpp(),
81 |
82 | // The xssFilter middleware sets the X-XSS-Protection header to prevent
83 | // reflected XSS attacks.
84 | // @see https://helmetjs.github.io/docs/xss-filter/
85 | helmet.xssFilter(),
86 |
87 | // Frameguard mitigates clickjacking attacks by setting the X-Frame-Options header.
88 | // @see https://helmetjs.github.io/docs/frameguard/
89 | helmet.frameguard('deny'),
90 |
91 | // Sets the X-Download-Options to prevent Internet Explorer from executing
92 | // downloads in your site’s context.
93 | // @see https://helmetjs.github.io/docs/ienoopen/
94 | helmet.ieNoOpen(),
95 |
96 | // Don’t Sniff Mimetype middleware, noSniff, helps prevent browsers from trying
97 | // to guess (“sniff”) the MIME type, which can have security implications. It
98 | // does this by setting the X-Content-Type-Options header to nosniff.
99 | // @see https://helmetjs.github.io/docs/dont-sniff-mimetype/
100 | helmet.noSniff(),
101 |
102 | // Content Security Policy
103 | //
104 | // If you are unfamiliar with CSPs then I highly recommend that you do some
105 | // reading on the subject:
106 | // - https://content-security-policy.com/
107 | // - https://developers.google.com/web/fundamentals/security/csp/
108 | // - https://developer.mozilla.org/en/docs/Web/Security/CSP
109 | // - https://helmetjs.github.io/docs/csp/
110 | //
111 | // If you are relying on scripts/styles/assets from other servers (internal
112 | // or external to your company) then you will need to explicitly configure
113 | // the CSP below to allow for this. For example you can see I have had to
114 | // add the polyfill.io CDN in order to allow us to use the polyfill script.
115 | // It can be a pain to manage these, but it's a really great habit to get
116 | // in to.
117 | //
118 | // You may find CSPs annoying at first, but it is a great habit to build.
119 | // The CSP configuration is an optional item for helmet, however you should
120 | // not remove it without making a serious consideration that you do not
121 | // require the added security.
122 | helmet.contentSecurityPolicy(cspConfig),
123 | ];
124 |
125 | export default securityMiddleware;
126 |
--------------------------------------------------------------------------------
/server/middleware/serviceWorker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 |
3 | import { resolve as pathResolve } from 'path';
4 | import appRootDir from 'app-root-dir';
5 | import config from '../../config';
6 |
7 | // Middleware to serve our service worker.
8 | function serviceWorkerMiddleware(req, res, next) {
9 | res.sendFile(
10 | pathResolve(
11 | appRootDir.get(),
12 | config('bundles.client.outputPath'),
13 | config('serviceWorker.fileName'),
14 | ),
15 | );
16 | }
17 |
18 | export default serviceWorkerMiddleware;
19 |
--------------------------------------------------------------------------------
/shared/README.md:
--------------------------------------------------------------------------------
1 | # src/shared
2 |
3 | This directory contains code that is shared between our bundles and should be considered safe to execute on either a `node` or `web` target (i.e. "Universal" code).
4 |
--------------------------------------------------------------------------------
/shared/components/DemoApp/AsyncAboutRoute/AboutRoute.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Helmet from 'react-helmet';
3 |
4 | function AboutRoute() {
5 | return (
6 |
20 |
21 | This is a small demo component that contains state. It's useful for
22 | testing the hot reloading experience of an asyncComponent.
23 |
24 |
16 | This starter kit contains all the build tooling and configuration you
17 | need to kick off your next universal React project, whilst containing a
18 | minimal project set up allowing you to make your own architecture
19 | decisions (Redux/Mobx etc).
20 |
17 | This starter kit contains all the build tooling and configuration you need to kick off your next universal React project, whilst containing a minimal project set up allowing you to make your own architecture decisions (Redux/Mobx etc).
18 |